Making an Asynchronous Clocking Drum Machine App in Perl
Furnished content.
Let’s Make a Drum Machine application! Yeah! :DThere are basically two important things to handle: A MIDI “clock” and a groove to play.Why asynchronous? Well, a simple while (1) { Time::HiRes::sleep(); ... } will not do because the time between ticks will fluctuate, often dramatically. IO::Async::Timer::Periodic is a great timer for this purpose. Its default scheduler uses system time, so intervals happen as close to the correct real-world time as possible.
#!/usr/bin/env perluse v5.36;use feature 'try';use IO::Async::Loop ();use IO::Async::Timer::Periodic ();use MIDI::RtMidi::FFI::Device ();my = shift || 'usb'; # MIDI sequencer devicemy = shift || 120; # beats per minutemy = 60 / / 24; # time / bpm / clocks-per-beat# open the named midi device for outputmy = RtMidiOut->new;try { # this will die on Windows but is needed for Mac ->open_virtual_port('RtMidiOut');}catch ($e) {}->open_port_by_name(qr/\Q/i);->start; # start the sequencer{INT} = sub { # halt gracefully say "\nStop"; try { ->stop; # stop the sequencer ->panic; # make sure all notes are off } catch ($e) { warn "Can't halt the MIDI out device: $e\n"; } exit;};my = IO::Async::Loop->new;my = IO::Async::Timer::Periodic->new( interval => , on_tick => sub { ->clock }, # send a clock tick!);->start;->add();->run; variable is captured for a unique MIDI device. (And to see what the names of MIDI devices on the system are, use JBARRETT’s little list_devices script.) Also, the beats per minute is taken from the command-line. If neither is given, usb is used for the name, and the BPM is set to “dance tempo.”The clock needs a time interval to tick off. For us, this is a fraction of a second based on the beats per minute, and is assigned to the variable.To get the job done, we will need to open the named MIDI device for sending output messages to. This is done with the provided.In order to not just die when we want to stop, {INT} is redefined to gracefully halt. This also sends a stop message to the open MIDI device. This stops the sequencer from playing.Now for the meat and potatoes: The asynchronous loop and periodic timer. These tell the program to do its thing, in a non-blocking and event-driven manner. The periodic timer ticks off a clock message every . Pretty simple!As an example, here is the above code controlling my Volca Drum drum machine on a stock, funky groove. We invoke it on the command-line like this:perl clock-gen-async.pluse statements. “What are syncopated patterns?”, you may ask. Good question! “Syncopated” means, “characterized by displaced beats.” That is, every beat does not happen evenly, at exactly the same time. Instead, some are displaced. For example, a repeated [1 1 1 1] is even and boring. But when it becomes a repeated [1 1 0 1] things get spicier and more syncopated.The desired MIDI channel is added to the command-line inputs. Most commonly, this will be channel 9 (in zero-based numbering). But some drum machines and sequencers are “multi-timbral” and use multiple channels simultaneously for individual sounds.Next we define the drums to use. This is a hash-reference that includes the MIDI patch number, the channel it’s on, and the pattern to play. The combined patterns of all the drums, when played together at tempo, make a groove.Now we compute intervals and friends. Previously, there was one . Now there are a whole host of measurements to make before sending MIDI messages.Then, as before, a named MIDI output device is opened, and a graceful stop is defined.Next, a Music::CreatingRhythms object is created. And then, again as before, an asynchronous loop and periodic timer are instantiated and set in motion.The meaty bits are in the timer’s on_tick callback. This contains all the logic needed to trigger our drum grooves.As was done in the previous clock code, a clock message is sent, but also we keep track of the number of clock ticks that have passed. This number of ticks is used to trigger the drums. We care about 16 beats. So every 16th beat, we construct and play a queue of events.Adjusting the drum patterns is where Math::Prime::XS and Music::CreatingRhythms come into play. The subroutine that does that is adjust_drums() and is fired every 4th measure. A measure is equal to four quarter-notes, and we use four pulses for each, to make 16 beats per measure. This routine reassigns either Euclidean or manual patterns of 16 beats to each drum pattern.Managing the queue is next. If a drum is to be played at the current beat (as tallied by the variable), it is added to the queue at full velocity (127). Then, after all the drums have been accounted for, the queue is played with ->note_on() messages. Lastly, the queue is “drained” by sending ->note_off() messages.#!/usr/bin/env perluse v5.36;use feature 'try';use IO::Async::Loop ();use IO::Async::Timer::Periodic ();use Math::Prime::XS qw(primes);use MIDI::RtMidi::FFI::Device ();use Music::CreatingRhythms ();my = shift || 'usb'; # MIDI sequencer devicemy = shift || 120; # beats-per-minutemy = shift // 9; # 0-15, 9=percussion, -1=multi-timbralmy = { kick => { num => 36, chan => < 0 ? 0 : , pat => [] }, snare => { num => 38, chan => < 0 ? 1 : , pat => [] }, hihat => { num => 42, chan => < 0 ? 2 : , pat => [] },};my = 16; # beats in a measuremy = 4; # divisions of a quarter-note into 16thsmy = 24; # PPQNmy = 60 / / ; # time / bpm / ppqnmy = / ; # clocks per 16th-notemy %primes = ( # for computing the pattern all => [ primes() ], to_5 => [ primes(5) ], to_7 => [ primes(7) ],);my = 0; # clock ticksmy = 0; # how many beats?my = 0; # part A or B?my @queue; # priority queue for note_on/off messages# open the named midi output devicemy = RtMidiOut->new;try { # this will die on Windows but is needed for Mac ->open_virtual_port('RtMidiOut');}catch ($e) {}->open_port_by_name(qr/\Q/i);{INT} = sub { # halt gracefully say "\nStop"; try { ->stop; # stop the sequencer ->panic; # make sure all notes are off } catch ($e) { warn "Can't halt the MIDI out device: $e\n"; } exit;};# for computing the patternmy = Music::CreatingRhythms->new;my = IO::Async::Loop->new;my = IO::Async::Timer::Periodic->new( interval => , on_tick => sub { ->clock; ++; if ( % $sixteenth == 0) { # adjust the drum pattern every 4th measure if ( % ($beats * ) == 0) { adjust_drums(, , \%primes, \); } # add simultaneous drums to the queue for my (keys %) { if (->{}{pat}[ % $beats ]) { push @queue, { drum => , velocity => 127 }; } } # play the queue for my (@queue) { ->note_on( ->{ ->{drum} }{chan}, ->{ ->{drum} }{num}, ->{velocity} ); } ++; } else { # drain the queue with note_off messages while (my = pop @queue) { ->note_off( ->{ ->{drum} }{chan}, ->{ ->{drum} }{num}, 0 ); } @queue = (); # ensure the queue is empty } },);->start;->add();->run;sub adjust_drums(, , , ) { # choose random primes to use by the hihat, kick, and snare my ($p, $q, $r) = map { ->{$_}[ int rand ->{$_}->@* ] } sort keys %; if ($ == 0) { say 'part A'; ->{hihat}{pat} = ->euclid($p, ); ->{kick}{pat} = ->euclid($q, ); ->{snare}{pat} = ->rotate_n($r, ->euclid(2, )); $ = 1; # set to part B } else { say 'part B'; ->{hihat}{pat} = ->euclid($p, ); ->{kick}{pat} = [qw(1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1)]; ->{snare}{pat} = [qw(0 0 0 0 1 0 0 0 0 0 0 0 1 0 1 0)]; $ = 0; # set to part A }}perl clocked-euclidean-drums.pl "gs wavetable" 90fluidsynth and hear the General MIDI percussion sounds, open a fresh new terminal session, and start up fluidsynth like so (mac syntax):fluidsynth -a coreaudio -m coremidi -g 2.0 ~/Music/soundfont/FluidR3_GM.sf2FluidR3_GM.sf2 is a MIDI “soundfont” file and can be downloaded for free.Next, enter this on the command-line (back in the previous terminal session):perl clocked-euclidean-drums.pl fluid 90perl clocked-euclidean-drums.pl usb 90 -1eg/euclidean.pl example program in the distribution. It is a work in progress. YMMV.edit: Programming/Perl/auto___making_an_asynchronous_clocking_drum_machine_app_in_perl.wikieditish...