tmhglnd / mercury-playground

The Mercury live coding environment running in the browser
GNU General Public License v3.0
62 stars 6 forks source link

Add a latency/delay feature for instruments and/or groups (to align/synchronize external synth audio) #44

Open totalgee opened 9 months ago

totalgee commented 9 months ago

I'd like to be able to delay an instrument, for the purpose of synchronizing internal and external synths. If I try to play MIDI along with sample/synth notes they don't quite match up. I'd like a per-track (or per-group!) latency or lag or something I could use (by ear) to line them up in time.

I'm sure the current MIDI latency you handle internally is good enough for most MIDI devices, but the "headless" Dirtywave M8 has some additional latency in reproducing its audio, which gets redirected to the PC via USB (at least, in the way I'm using it). An additional/optional way to offset the timing would be wonderful. In my case I think the MIDI might actually need to be sent a bit sooner than non-MIDI stuff (by delaying them, or by allowing a negative offset on MIDI stuff).

Note that I could work around this issue by using the time() offset parameter (e.g. time(1/8, 0.05/1), but this makes it complicated if you're also using the offset for other (musical) purposes.

tmhglnd commented 9 months ago

Yes, I will look into it! I think I can just add a Delay-node to the main stereo output that is 0ms by default (or maybe even disconnected) and as soon as you set delay 10 it will be connected before the main output and add some latency to the sound.

Optionally I could maybe also implement set audioDelay and set midiDelay so in other scenarios it is also possible to delay the midi a bit if this is needed.

totalgee commented 9 months ago

Yes that (those) sounds like a good solution, it doesn't need to be per-track for my use case. (Unless someone tries to use multiple MIDI devices with different latency requirements ;-).

tmhglnd commented 9 months ago

So at the moment I've decided to add the wait() method to the instrument. This method works for all instrument types allowing you to manually set latency values in ms or division per instrument. Give it a try and let me know what you think. Currently only supported in the web version, some documentation here: https://tmhglnd.github.io/mercury/02-instrument.html#wait

Having to set wait() individually is maybe less ideal if the latency is the same for all instruments but overall maybe more flexible. Also my plan is to fix the group() method (that is still an open issue from a long time). When that is fixed it would be possible to do something like this:

new midi default note(spread(12) 1) time(1/16) group(midis)
new midi default note(random(4) 0) time(1/4) group(midis)
// apply wait() with 20 ms to both midi devices
set midis wait(20)

One interesting musical benefit from the wait() method is that it allows for lists. So this can also be used to create some rhythmical modifications:

new sample kick_909 time(1/4) wait([0 1/8])
new sample hat_909 time(1/8) wait([0 0 1/16])
totalgee commented 9 months ago

Thanks for adding this! I tried it today in Firefox (Windows) and it works! For my use case with the Dirtywave M8 connected with audio and MIDI via USB, I was able to synchronize the M8 sounds with Mercury (browser) instruments by adding wait(40) on the MIDI instruments (somewhere around 30-40 ms seems pretty close).

The only thing I noticed is that sometimes after re-evaluating several times, things get out of phase, but this may not be a "delay" issue but related to how things synchronize in Mercury each time you evaluate (I tried using name() and not using it, for both sample and MIDI instruments). Or maybe just related to browser timing...? But on some evaluations it seems to get back in sync, so I'm not quite sure what's happening. When it's off, it's off by more than 40 ms...

set tempo 89
new sample kick_909 time(1/4) speed(4) play([1 0]) name(bd)
new midi M8 time(1/16) program([16 32]) note(0 0) wait(40) length(0.99/16) name(mid)
tmhglnd commented 9 months ago

Yeah the synchronisation between what index different instruments are on is still something I'm struggling with to fix correctly! Depending on the moment of evaluating the counters don't seem to get transfered correctly to the newly introduced instruments. In general they should do this when the instrument has a name(), if the instrument doesn't have a name then every time you evaluate it restarts at 0.

One thing you could do is add play([1 0] 1) which will reset the counter in the instrument after 1 bar. So this should align everything even if the counter was briefly not in sync when evaluating. But in any case this is of course less desirable