-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
code styling #4
Comments
For 1/2. I don't really have an opinion yet. I suspect that a pointer would be preferable but I don't really have any basis for that. I agree that having predefined formats would probably be useful. 3/4. Constructors all the way. It's pretty easy to screw up the buffer size if you're doing it manually. 5/6/7. I think we need to iterate a couple of sample applications to figure this out. I think the best thing to do would be to write a simple application like a format converter and suss out some of the API. It would:
Then the application could detect the format of the input file from a known set, and perform a conversion to the desired output type. This is pretty much the bare minimum "round trip" that the audio API would need to be able to handle (apart from the slightly more simple case of synthesizing audio to an output file). |
1 vs 2: not really either. Embedding is useful to pick up methods, but that's not a big win here. I'd rather explicitly name the field, but drop the pointer indirection:
You don't need a pointer in the type to provide pre-defined Format values. 3 vs 4: 4 although on a similar note, you might be able to drop the "&". 5 vs 6 vs 7: hard to say in the abstract. It depends on what the Buffer type actually is and how sources, sinks and filters are supposed to use it. On an unrelated note (but not having anywhere better to say this), good luck with the repo, but I don't really have the spare time to pay this a lot of attention. |
@kisielk I have an example like going between @nigeltao Thanks, your feedback is very much appreciated. We'll ping you once we'll have made more progress to see what you think. In the meantime, I tried to compare the 2 synth examples @egonelbre and I worked on: // synthesis API 1 (similar to web audio)
sine := &osc.Sine{}
sine.Frequency.Set(440.0)
gain := &effect.Gain{}
gain.Value.Set(0.5)
buf := audio.NewBuffer32(audio.Format{
Channels: 1,
SampleRate: 44100,
}, 512)
// get/set values on the transform
gain.Value.Set(gain.Value.Get() + 0.10)
// chain generator
sine.Process32(buf)
// and processor
gain.Process32(buf) // synthesis API 2
buf := &audio.FloatBuffer{
Data: make([]float64, bufferSize),
Format: audio.FormatMono44100,
}
osc := generator.NewOsc(generator.WaveSine, 440.0,
buf.Format.SampleRate)
// get/set values on the generator
osc.Amplitude += 0.10
// chain generator
osc.Fill(buf)
// and processor
transforms.Gain(buf, 0.70) API 1 reminded me of https://developer.mozilla.org/en-US/docs/Web/API/AudioNode which is quite a nice API but feels clunky at time. In API 2, thread safety isn't taken in consideration and it would be up to the developer to lock their code. Because a lot of audio code doesn't run on multiple threads, that might be fine. |
For optimized struct sizes there is also:
I would go with convenient, without pointers and pay the extra 8 byte cost. When you need to optimize for memory usage, you probably have more knowledge of the whole system and can use a single format in a group of buffers. Constructors or constant defaults.
Interface/type-switching. Here are my arguments: For the core of processing we shouldn't be dealing with N-different buffer formats, otherwise trying to make everything consistent will be quite difficult. Hence the interface will be unnecessary. With different Single Using
|
I agree, most of the time a given application will only deal with one buffer format. There may be some rare cases where you want to be able to process differently in float32 or float64, but I feel like in most cases you'd just convert from one to the other. Having to type switch every time you write a Process function is a nuisance so I would prefer to just have separate It's true that it doesn't leave any way to express the number of supported channels in the function signature, but I haven't really seen any API that does that yet. Most of the time the number of channels is either set by convention or reported via some other functions or properties. For example in web audio, the |
I tried to avoid this for few reasons:
It will be harder to write generic processors such as:
With regards to thread-safety. I think having the "atomic" values in the node is necessary, however we should have examples available how to synchronize, when your audio code runs in a thread. There could be some package
|
Synchronizing on a per-sample basis seems a bit extreme to me. I think if you were doing any kind of parallel processing it would be at the buffer level. The main use case I can see is where you have a single source feeding multiple destinations and then those are joined together later. |
We can provide automatic converters:
Not sure where to allocate/keep the intermediate buffers, for this approach. Alternative approach would be to implement
|
Yeah, the
|
Yeah... that should have been "...atomic values in the node are not necessary...". So, I'm fine with not having atomic controls in |
I'd like to discuss code styling for a minute. It would be good to agree on a general approach of the code we'd like to write. Here are some examples, I'd like to hear more about what you think
##Code styling
Embedded type and optimized struct sizes
vs pointers and convenient field types
Convenient but straight forward constructor
vs DIY approach. (note that in this code, the sample count should probably
multiple by the number of channels which is an easy thing to forget.)
Explicit, manually, duplicated functions
vs interface and type switching
vs using go generate for any functions implemented the same way in float32 and 64
I personally like:
2: I don't think the convenience of a having a field in a type we can easily manipulate trumps the memory size gain. Plus using a pointer would allow us to provide predefined formats.
3: Constructors are often convenient, especially as entry points and when the setup isn't trivial. This example is a good example since it was was written by hand and technically has a bug. This bug would be mitigated by a constructor. It does come at a price of a bigger API and potentially surprising behaviors.
Finally 5: It might be a bit of a pain at first but it works better with code completion (easier for people not knowing the API), it can be optimized (custom logic and/or SIMD) but can be the source of more bugs (fix something on the float32 side but not float64 for instance). 7 (code generation) is nice but it always feels a bit clunky to have to move things outside of the code generation path because of some edge cases.
@egonelbre @kisielk @nigeltao what do you think?
The text was updated successfully, but these errors were encountered: