[alsa-devel] pyalsa: synchronizing queue with MIDI clock events
Hi everybody,
this is my first post on this list, so please excuse me, should it not be the right place to post this.
I started exploring programming the ALSA sequencer via the pyalsa Python interface, i.e the alsaseq module. I wrote a small utility that reads MIDI events from one port, filters/processes them and writes it to another. This allows for routing by channel, note range, controller number etc. and to filter out, replace or add MIDI events.
Now to synchronize the queue tempo to incoming MIDI clock events, to be able implement synchronized delays or arpeggiators, I use the method in the code shown below. This works ok, but I notice that the measured BPM oscillates +-1 BPM around the value displayed by the clock source (my synth's sequencer) if I measure/average only a few (~10) ticks or do not round the result to an integer value.
Is this the right approach? What is a sensible number of ticks to take into account for measurement? Should I use another reference timer than Python's time.time() function? Is pyalsa generally suitable for this kind of application even on older/weaker hardware (e.g. a NSLU2)?
Any comments or suggestions for improvement would be very much appreciated!
Chris
class MidiProcessor(object):
def run(self): # This is simplified to only show the general logic while True: events = self.sequencer.receive_events( timeout=RECEIVE_TIMEOUT, maxevents=1) for event in events: if event.type == SEQ_EVENT_CLOCK: self.sync_queue(event) else: # do other MIDI processing / routing
def sync_queue(self, event): """Sync queue tempo to incoming MIDI clock events.""" # list to collect the timestamps of the last few ticks lt = self._last_ticks lt.append(time.time()) ltlen = len(lt) if ltlen > 1: # calculate & set bpm: calculate difference between # the times the last few ticks were received and average # all results avg_delta = sum( [y-x for x,y in zip(lt, lt[1:])]) / (ltlen-1) # tick length is a 24th of a quarter note bpm = round(60 / avg_delta / 24) if bpm != self.bpm: self.bpm = bpm self.sequencer.queue_tempo(self.queue, tempo=int(6e7 / self.bpm), ppq=self.ppq) # only remember last 24 received ticks # (length of a quarter note) if ltlen > 24: lt.pop(0)
Christopher Arndt wrote:
Now to synchronize the queue tempo to incoming MIDI clock events, to be able implement synchronized delays or arpeggiators, I use the method in the code shown below. This works ok, but I notice that the measured BPM oscillates +-1 BPM around the value displayed by the clock source (my synth's sequencer) if I measure/average only a few (~10) ticks or do not round the result to an integer value.
Is this the right approach? What is a sensible number of ticks to take into account for measurement?
Large enough that you get a stable average, but small enough that you can detect actual changes.
Should I use another reference timer than Python's time.time() function?
That timer probably has a high enough resolution, but you measure the time when your code is executed, which may be later then when the event was actually received.
You can tell ALSA to timestamp all events that are received; just call the snd_seq_port_info_set_timestamping() function and set a queue that is running, then you'll get the queue's current time in each event. This isn't implemented in pyalsa, but you can set this for a specific connection with the queue, time_update, and time_real parameters of the connect_ports function.
Is pyalsa generally suitable for this kind of application even on older/weaker hardware (e.g. a NSLU2)?
The biggest problem is probably scheduling delays, so I guess using Python instead of C should not add any noticeable delay.
Best regards, Clemens
Clemens Ladisch schrieb:
Christopher Arndt wrote:
What is a sensible number of ticks to take into account for measurement?
Large enough that you get a stable average, but small enough that you can detect actual changes.
I figured out that much on my own :) I was just wondering if anybody had already experimented with this...
You can tell ALSA to timestamp all events that are received; just call the snd_seq_port_info_set_timestamping() function and set a queue that is running, then you'll get the queue's current time in each event. This isn't implemented in pyalsa, but you can set this for a specific connection with the queue, time_update, and time_real parameters of the connect_ports function.
I'll look into this.
Many thanks for your comments!
Chris
Christopher Arndt schrieb:
You can tell ALSA to timestamp all events that are received; just call the snd_seq_port_info_set_timestamping() function and set a queue that is running, then you'll get the queue's current time in each event. This isn't implemented in pyalsa, but you can set this for a specific connection with the queue, time_update, and time_real parameters of the connect_ports function.
I've tested this now and encountered a very strange bevahior of the event time set by the queue:
a) SeqEvent.time seems to be in milliseconds instead of seconds as would be expected by the ALSA documentation and looking at the source of pyalsa.alsaseq.
5) Consecutive timestamp go up to ~1000,00 and then seem to wrap around and start from zereo again.
I have created a small test program, which demonstrates this:
http://paste.chrisarndt.de/paste/fa8ca7201bc7419f9e001315ca02b1ef
It outputs the time of each MIDI clock signal it receives and whether it is in tick or real time and whether it's absolute (= from the start of the queue) or relative (to the preceding event). Here's some output from the script:
MIDI CLOCK 0.0 ABS REAL MIDI CLOCK 23.697488 ABS REAL MIDI CLOCK 48.701538 ABS REAL MIDI CLOCK 73.70515 ABS REAL MIDI CLOCK 98.705762 ABS REAL MIDI CLOCK 123.709812 ABS REAL MIDI CLOCK 148.702424 ABS REAL MIDI CLOCK 173.707474 ABS REAL MIDI CLOCK 198.711086 ABS REAL [...] MIDI CLOCK 898.73904 ABS REAL MIDI CLOCK 923.74909 ABS REAL MIDI CLOCK 948.744702 ABS REAL MIDI CLOCK 973.742314 ABS REAL MIDI CLOCK 998.749364 ABS REAL MIDI CLOCK 24.738976 ABS REAL MIDI CLOCK 49.741026 ABS REAL MIDI CLOCK 74.743638 ABS REAL MIDI CLOCK 99.74725 ABS REAL
As you can see, once the time would exceed 1000.0 it seems to start from zero again. This happens only when the queue sets the event times to REAL. When ticks are used, the increase infinitely, as expected.
I fail to understand why this is and what I am doing wrong. I would expect SeqEvent.time to be in seconds and always ascending until I stop the queue. Please, somebody enlighten me!
Chris
Christopher Arndt schrieb:
I've tested this now and encountered a very strange bevahior of the event time set by the queue: [...]
Should I report this as a bug then? Is there a special category or convention for filing pyalsa issues at https://bugtrack.alsa-project.org/ ?
Chris
Christopher Arndt wrote:
I've tested this now and encountered a very strange bevahior of the event time set by the queue:
a) SeqEvent.time seems to be in milliseconds instead of seconds as would be expected by the ALSA documentation and looking at the source of pyalsa.alsaseq.
- Consecutive timestamp go up to ~1000,00 and then seem to wrap around
and start from zereo again.
Apparently, you are the first one who tried to use this. Fixed now: http://git.alsa-project.org/?p=alsa-python.git;a=commitdiff;h=9a7dea33dddeb2...
HTH Clemens
Clemens Ladisch schrieb:
Christopher Arndt wrote:
I've tested this now and encountered a very strange bevahior of the event time set by the queue:
Apparently, you are the first one who tried to use this. Fixed now: http://git.alsa-project.org/?p=alsa-python.git;a=commitdiff;h=9a7dea33dddeb2...
Excelllent! The test script now produces correct results. Thanks for the fix!
Might I suggest another patch to pyalsa? This would allow to use developers to use "python setup.py develop" to install their git version as a Python egg-link and makes it much easier to use pyalasa with virtual Python environments.
Chris
diff --git a/setup.py b/setup.py index fa7f2d6..c2b6389 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,10 @@
import os import sys -from distutils.core import setup, Extension +try: + from setuptools import setup, Extension +except ImportError: + from distutils.core import setup, Extension
VERSION='1.0.20'
Clemens Ladisch schrieb:
Christopher Arndt wrote:
Might I suggest another patch to pyalsa?
Please provide a Signed-off-by line.
Please forgive my ignorance, but what is that? I'm unfamiliar with the alsa development process. Is that a git-specific thing? Is there some documentation on contributing to alsa?
Chris
Christopher Arndt wrote:
Clemens Ladisch schrieb:
Please provide a Signed-off-by line.
Please forgive my ignorance, but what is that? I'm unfamiliar with the alsa development process.
It's originally a Linux kernel development process thing, but we use it for all of ALSA. See section 12 of http://git.alsa-project.org/?p=alsa-kernel.git;a=blob;f=Documentation/SubmittingPatches;hb=HEAD.
HTH Clemens
This patch adds support for setuptools to the setup.py file of python-alsa.
Signed-off-by: Christopher Arndt chris@chrisarndt.de
diff --git a/setup.py b/setup.py index fa7f2d6..c2b6389 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,10 @@
import os import sys -from distutils.core import setup, Extension +try: + from setuptools import setup, Extension +except ImportError: + from distutils.core import setup, Extension
VERSION='1.0.20'
participants (2)
-
Christopher Arndt
-
Clemens Ladisch