Hello, I am trying to write a component that will loop audio at specified loop points (e.g. as detailed in the metadata via a tag). I've run into a couple problems, and since I'm not well-versed in the SDK I was hoping for a pointer or two.
It appears I can do this via a dsp component, as you get to modify the audio data. This way, if the buffer would go past the loop end, I can change the samples after that point by swapping them with ones starting from the loop start point. It appears that I can have a decoder to hold a copy of the audio so I can swap the data with the loop start data, following foo_sample's calculate_peak_process. Here comes the issue: at the loop point, I need to seek so the audio matches the timeline. This can't be done on the dsp thread, but I can call fb2k::inMainThread() to async seek to the correct location (loop start + num samples written from the loop start point). However, on_chunk() may be called multiple times before the seek actually takes effect, and I have no idea when the seek does take effect as onChunk() doesn't provide any timing information (such as the sample offset in the entire track where the chunk begins).
Would I be able to listen for the seek event via a play_callback_impl_base on_playback_seek() callback, have onChunk() return false once the loop point it hit until the on_playback_seek() is invoked, then start returning true again? I am concerned with the seek event possibly immediately cancelling all buffered audio rather than letting it all play out, which would lead to playback that doesn't match the loop points. I'm also unsure if the async nature of seek being such that you cannot make the assumption that the first call to onChunk() after the seek callback is accurate (but perhaps after seek, onChunk() gets synchronized?). I don't even know if onChunk() returning false will do what I expect here - the documentation says it will drop the chunk, but does that mean it will attempt the next chunk or play silence? Or perhaps I'm missing a simple option here?
If it's not possible to implement this with a dsp and default seek support, another option I had was to implement a stream, or at least a file that has an unknown size (then implement play/pause/stop/etc myself in a new window). However, I couldn't find the source for any of the existing streaming components to get an idea how that would need to be hooked up. Searching through the sdk I also didn't see any descriptive names that might help. I couldn't find a way to write audio data to foobar, such that all visualizations, dsps, etc work. Searching around the forums here, someone inferred that a custom output implementation and process_samples() may be a way to do this, but again, I'm not able to see how one can be hooked up (process_samples() is private on output_impl and write() doesn't have an implementation so I wouldn't know how to implement it as a way to send the audio data to foobar).
Does anyone have any pointers of where I can look to get something like this working, or a different design that would work? I feel like I'm missing a piece of the audio data routing puzzle that would enable this.
doesn't this open source component already do looping ~ https://github.com/ptytb/foo_rehearsal
It does! But it's implementation is very naive, and isn't, for lack of a better term, "sample accurate." What it does is set a windows periodic timer to fire every 5 milliseconds. On the timer callback, it checks to see what the current time in the track is, and if it has gone past the loop point, seeks to the loop start. This is pretty inefficient, but will work in a pinch. Windows also likes to do a bunch of stuff to limit how often callbacks occur - there is a "quantum" for a lot of its APIs that typically works in ~15.67 ms intervals (even if the period is smaller). However, since there will be audio playing, this quantum may be shorter. But this changes with every OS version and is dependent on a ton of factors, including what is happening at runtime, power savings modes, etc - I'm just saying it's highly variable. I was hoping to plug into foobar events to get an accurate looping mechanism - if that's possible.
Thank you for pointing out foo_rehearsal! I didn't see it listed on the site (https://www.foobar2000.org/components), but I do see it listed here on the forums in the 3rd party plugins section. At the very least, it's given me a fallback option. I could make it a bit more efficient by setting a timer to fire on start/seek events so it isn't firing every 5ms.
Looping DSPs aren't really possible. A DSP must at least continuously absorb input sample data. Files won't progress if the DSP doesn't absorb more. You could try a gross hack whereby you report your play position by messing with the latency you report. But I don't expect that to work well.
Thanks for the response - it was pretty much what I was expecting :(. What about the other approach - where the file is "streamed" in such a way that the length is unknown, mimicking a streaming service? How could I tell foobar the next raw audio to play? Thanks!
You could do that, too. And there's an input helper service for decoding arbitrary files/subsongs. It does support sample accurate seeking, as long as the referenced input itself does.
Sweet :). Looking through the SDK, there appears to be some interesting classes that might be what you are referring to. Are you talking about the input_helper class? Taking a look at it, it appears I could subclass input_helper and provide my own version of run() that keeps track of the current time via how many samples have been called through, and defaults to calling the parent's version of run() when not at the loop point. When at the loop point, I just call seek(loopStart).
Is that what you were referring to? If so, how would I insert my input_helper object into the chain? I don't see it as a service where I can register for callbacks. input_helper contains an input_decoder, which is a input_info_reader which is itself a service_base object, but I don't a way to tell the main foobar logic to call into a provided instance.
Yeah, you use an input_helper to decode the file you want to loop, and you create your own input to return the audio. You can use some custom URL protocol to match for input, one which would contain the full URL of the source file, and loop commands.
Or you could create a custom text file with a custom extension, then open that from your input, and it would reference files to loop, and possibly even resample and/or mix.
Thanks for the reply! But, I think you are a few steps ahead of me on this and assume I know more about the architecture than I do :). I don't want to read from a file any differently, or convert from bytes to PCM data any differently, and I definitely don't want to use a URL. Based on your earlier reply, I was just looking to hook into existing mechanisms, ideally something like the equivalent of returning the next time of audio data to foobar's core. That way I could sit on top of the real workers that read and process e.g. mp3 encoded data into PCM, maintain parts of the PCM stream they've already read/decoded, and swap out parts of the returned audio buffers at the loop point, then tell the lower layer to seek to the loop start so when the next GetNextAudioChunk(durationInSamples) (or equivalent) gets called, it will grab data from the loop point to hand it up to my wrapper (which would do nothing).
From what I can see by browsing the SDK, it seems at least mostly doable, perhaps doable. The problem is I just don't know where to hook into this (somewhere in the input* classes it seems?), and the examples don't touch upon how to use that set of classes. The comments help, but don't seem to tell you how they can be plugged into the system so foobar invokes your code (or at least I missed it). More like they tell you how to read from files, so you could call them yourself when a button in your custom UI is clicked.
A context menu handler could be created to open a dialog, but you still need to invoke playback to actually produce any audio. And I don't know how to do that, other than creating another playlist entry for your referenced virtual file and letting the player open it using your input service.