electro-smith / libDaisy

Hardware Library for the Daisy Audio Platform
https://www.electro-smith.com/daisy
MIT License
332 stars 143 forks source link

Feature request - Tempo class #393

Open nekomatic opened 3 years ago

nekomatic commented 3 years ago

In order to buid any tempo-locked projects like: sequencers, delays, clock dividers, multipliers or generators we need a component which can read tempo information from sources including Midi Clock, a gate (i.e. 24ppq), Tap Tempo (on Petal or Field) or by a value defined via software and use this data to trigger events locked to the tempo - in case of sequencers - or use the tempo value to determine the time - in the case of a delay. It should be possible to switch between tempo sources during runtime - it should be also possibel to switch on or off during runtime clock generation via gate and or MIDI and obviously also during runtime change tempo value.

The Tempo class would enable scenarios where MIDI start, stop, continue messages are useful.

nekomatic commented 3 years ago

This class would need to interact with backend APIs which would should not be explicitly exposed:

brbrr commented 3 years ago

libDaisy already has a System::GetNow() API that returns ms since the hardware reset: https://electro-smith.github.io/libDaisy/classdaisy_1_1_system.html#a14360469574f87fec69d7b72e39029cf

FWIW, I use DaisySP::Metro for the sequencer clock, which is basically a sin wave. And it works well for me. Its usage is based on assumption that it runs in real-time, meaning that at 120 BPM(2 Hz), its cycle would be exactly 500ms.

But I assume it might not be the case all the time, especially during high loads.

nekomatic commented 3 years ago

What about detecting tempo from MIDI Clock or Sync via Gate? I.e. 300 bpm would require detecting pulsed incoming at 120Hz, the System::GetNow() returns milliseconds which does not provide high enough resolution, valies would have to be averaged over a numner of samples. Tempo is one of fundamental parts of music - I believe it deserves dedicated API.

TheSlowGrowth commented 3 years ago

This sounds like a worthy addition to libDaisy and something I need for a private project as well. This could potentially be split into two parts:

  1. The clock signal aquisition (measuring the timing of gate/trigger signals or MIDI messages or buttons presses and extracting a clock signal from that).
  2. The generation of a clock signal - either running from an internal clock or synced to some external signal.

Just brainstorming here...

Since it's much easier to implement things in small pieces, how about something like this:

We implement the clock signal capturing as separate classes, e.g. GpioClockSource, MidiClockSource. They are initialised with a callback function which they call whenever a new clock pulse arrives. Then we implement the actual Clock so that it can be driven by none/one/multiple of these capturing classes and output pulses with a selectable division / multiplier.

I'm currently away from my developer computer but I may have some time later this month to work on something like this. If anyone want's to do it, I'll happily proof read, review, discuss, etc.

TheSlowGrowth commented 3 years ago

Thinking about the Clock class, here's what I think it should be able to do (please add stuff that's missing):

I would vote against adding a "song position" functionality. I think that goes beyond the scope of a clock generator and is something that can be added with another class.

nekomatic commented 3 years ago

Also brainstorming here... ;) I would rather think of it as a tempo and a clock - tempo is just a number which can be taken by measuring time between pulses - like suggested MidiTempoProvider, TapTempoProvider, GpioTempoProvider or in case of Patch - GateTempoProvider Providers should not need any callback API, all they should do is to expose a method which returns an up to date tempo as a float

The other side would be the actual clock generator - it would be simply given a number and it would launch a callback at the frequency related to the tempo, or provide API similiar to the Gate in the Patch - this way we could acieve i.e. clock multiplier. The division should be configurable.

stephenhensley commented 3 years ago

Love this idea, and it's definitely an important angle that we haven't tackled yet.

I agree with @TheSlowGrowth that this should be broken into two (or more) classes: Something to handle acquiring trigger, reset, sync, etc. signals from the hardware, and something else to manage the generation of a clock signal including various divisions/multiplications.

I think the former, the acquisition class, should be a part of libdaisy, and should be able to listen to at least MIDI and GPIO, but possibly other inputs later.

The latter part, the actual clock generator itself, I think would be more welcome in DaisySP where it can be used as a portable clock generator that isn't useful only to the Daisy hardware.

We'll probably also want some helper functions/conversions for different rates (ms, hz, BPM, etc.) and those seem most useful in DaisySP, but could see them living in either library.

nekomatic commented 3 years ago

@stephenhensley, agree, obtaining the tempo may be need to consider low level interaction with other functionality like MIDI etc. but as long as we have the tempo value as a number the actual clock generation should be trivial - no reason to force this into the actual platform API

nekomatic commented 3 years ago

regarding MIDI - can we simply add tempo sensing as a functionality of the MIDI parser?

Midi config would require an extra Enum parameter: EnableTempoOnly, EnableMessagesOnly, EnableBoth with the EnableMessagesOnly as the default if not provided - this would obviously make sense if there no other types of MIDI messages which should trigger a backend action... Maybe the number of samples to average would also make sense? With enough samples we may even be OK with the milliseconds precision of the System::GetNow() even if some DAWs can send something like 400bpm...

Reading the tempo value would be as simple as hw.midi.GetBpm() which would return i.e. negative value if tempo is not being detected or no MidiClock messages showing up.

GPIO based tempo detection could be done as a completly separate class - it would be up to the app developer to decide which providers to set up and which value to use.

grrrwaaa commented 3 years ago

Just a comment for consideration:

Whereas MIDI clock tempo almost demands averaging over multiple data points due to the transport inaccuracy itself, tempo derivation using GPIO inputs (e.g. gate inputs on modular) might want to offer the option to avoid averaging and simply use the two most recent edges, particularly if using small block sizes, to reduce the slop that averaging can imply. At a block size of 1ms or less, the timing can be very tight indeed. It can have creative advantages too: modules that respond only to the 'last two triggers' can do interesting things when you send them irregular rhythms -- or simply clocks with swing :-) But it does perhaps imply a different conceptual model than "tempo".

nekomatic commented 3 years ago

@grrrwaaa What I mean by Tempo or the Clock is MIDI Clock or the analogue sync signal as gates - in both cases those should be detected at the frequency of 24 pulses per quarter note. It's worth to mention that some synths work at different frequencies per quarter note but I'll ignore this for the time being.

I'm not yet that familiar with the platform so I maybe wrong but it would be good if the GPIO based tempo was not depending on the audio handler's frequency but rather triggered by an iterrupt - we may even need to consider dedicating a timer for this with resolution at least 10KHz. With more than 24ppq the sampling frequency may need to be higher. Here is why. I.e. my DAW can work with up to 400bpm, if I add something like VoltageModular as aplugin and expose its tempo as a gate via something like ES-8 then the the pulses I'll be sending will be at exactly at 160Hz (to the precision of audio frequency) which is one pulse every 6.25 ms. This is an edge case but even going down to 120 bpm we have the pulses at 48Hz - one pulse every ~20.83 ms... if we happen to get this wrong and for some reason get the value between pulses as ~20.5 ms then we have ~500ms drift after just 16 bars... even when we read this as 20.80ms we still have ~50ms after 16 bars which can turn into a whole 1 second after ~300 bars... and this kind of drift may happen when we measure time between last two pulses at 10KHz frequency.. With more than 24ppq the sampling frequency may need to be higher. In some edge cases the drift may occur even with long averaging and won't be compensated because some pulses may simply be missed. That may be fine for reverbs or delays but not for a sequencer.

However I also agree that the averaging range should be configurable. If the app does not handle audio then the measurments can be done pretty much as the pulses occur so averaging over i.e. one quarter note will be an overkill for pretty much any tempo but with a 1KHz timer we can still get to a point when me miss one pulses once a while at some tempo values.

Even with long averaging the only side effect will be a bit of lag when the tempo changes but I would rather live with 1 bar worth lag than 0.5 sec drift every 16 bars. Note, in the lag scenario any delays introduced when temo goes up will be compensated when the tempo goes down. The effective tempo changes will be smoother than the changes in the incoming pulses frequency.

grrrwaaa commented 3 years ago

@nekomatic Yes, it makes sense for 24ppqn/MIDI style regular clock.

(I was just pointing out that in modular hardware, sometimes clock means a regular pulse like that, but sometimes it can mean any arbitrary triggers that can change quite rapidly. The 4MS QPLFO is a good common example. But you should probably ignore me -- this is most likely out of scope for the issue here, and simply processing the GPIO ins at a small block size should already be quite enough for what I'm talking about. Sorry for the distraction!)

Back to regular clock: I'm not sure I follow the discussion about clock drift. If there is a regular series of pulses coming in, you have the ability to align tempo clock phase to the timing incoming pulses themselves. As you say, at 400bpm/24ppqn that's 6.25ms between clocks; at a typical SR=48kHz/BS=48 samples, you can process the output of the clock in the audio context every 1ms. That implies a jitter of at most +/-0.5ms (and a 1ms latency on top). But just as you use an averaging filter on the tempo estimation, you can also use a filter to track your phase error and correct it (e.g. PLL), to keep it aligned to the incoming clock. (And you can also fold in latency compensation to that algorithm too.) So AFAICT there shouldn't be any worry about clock drift unless clock pulses stop coming in.

EDIT: DJ analogy: the tempo estimation adjusts the turntable rate slider, the phase estimation is nudging the record back & forth a little to line the beats up.

Anyway, sorry for derailing the topic!

nekomatic commented 3 years ago

@grrrwaaa the drift is an edge case, it may happen in case of short averaging and if we loose the clock signal for some time - unfortunately my day job turns me into a devil's advocate when it comes to extreeme scenarios in software design :D

In the end we need to ballance between clock stability, the lag in tempo changes and all-we-can-imagine edge cases in terms of risk management.

I don't know enough about PLL, but isn't this a hardware concept? I dont think it would be easy to add i.e. to the Patch.. and the jitter is not just the incoming signal problem, it will also be introduced by low measurment frequency - this is something we need to resolve on the software level.

grrrwaaa commented 3 years ago

Sigh -- I wrote a response and then a crash took it away.

PLL principle works digitally too: phase comparator between leader & follower oscililators can be used to update phase of the follower. Period counter on leader, averaged/filtered over samples, can be used to set frequency of follower.

A 1kHz sampling rate might be good enough to track 160Hz pulses. I knocked up a quick example in gen~ (Max) to try this out. I generate clock signal at 400bpm (160Hz/6.25ms for 24ppqn), then I downsample this by 1kHz. This is fed into a pulse timer, with averaging over the past N pulses. The resulting estimated frequency is used to drive an internal phase accumulator that generates a pulse. The internal external (downsampled) pulses are compared (using another timer), divided by clock period to get phase difference, wrapped in +/-0.5, massively attenuated, and subtracted into the internal phase accumulator. This internal clock phasor is then used to generate a clock output signal, with a 0.5ms latency compensation to balance the average latency of a 1kHz sampling rate. With that I get a decent clock, which aligns to the input with a phase error of around 0.1ms or less.

The compromise between accuracy and responsiveness comes down to the N samples. If N is low it is very responsive but inaccurate. N=24 means it will align to an extreme tempo change in about one beat. N=64 seems about minimum for a reasonably accurate tempo estimation. But the choice of N can be dynamic and adaptive: If any incoming clock period is significantly different than the current estimation, use a smaller N so that it adapts quicker, until the two become similar again. If an expected incoming clock doesn't arrive, use a much larger N (using old data points) so that the estimated frequency is more accurate, and drift is minimized. All we need to do is store the last N timer readings in a ringbuffer.

Of course, N could be much smaller if the timer readings for the incoming clock were more accurate than a 1ms(1kHz) error window.

nekomatic commented 3 years ago

@grrrwaaa I'm not sure if I follow but it seems to me the PLL would be applicable only in scenarios where we want to generate stable clock from a jittered one with a predefined phase lag. It this correct? How would this work for an unstable source clock where the incoming clock jitter exceeds the sampling frequency errors as in case of MIDI clock over USB like on this video?

If I'm not mistaken we need at least two hardware timers for PLL- one for measuring period of incoming pulses and other - higher frequency - for setting the time of the output pulses. Is this correct? If it is correct then we may reduce the solution to use that extra clock at higher frequency to measure time between GPIO triggered hardware interrupts. A lot less MPU cycles... Generating output pulses can be left to the top level app using same timer and it's phase can be aligned i.e. to a start trigger contolled separately. Unless we really want to keep the same phase... which increased the number of possible based solutions:

  1. GPIO clock source with mimial phase lag based on PLL
  2. GPIO (or MIDI) clock source with minimal MPU load with running average to reduce jitter.

Personally I would vote for the second one - clock phase offet as small as 1/3 of a 32nd note is not a problem for me but I want to squeze as much of the MPU cycles as possible for other purposes. A jitter or tempo change lags of the regenerated clock is not a massive problem as long as the output is more stable then the clock from the video link above :)