Adventures in Real-Time MIDI
Furnished content.
trigger_notes() routine, given the object.Let’s look at each of these in closer detail.use v5.36; # use a modern Perluse IO::Async::Loop (); # asyncuse IO::Async::Timer::Periodic (); # asyncuse List::Util qw(max sum0); # duration scalinguse MIDI::RtMidi::Util qw(out_port stop_all_notes); # rt-midiuse Music::MelodicDevice::Arpeggiation (); # arpeggiosuse Music::Scales qw(get_scale_MIDI); # pitchesmy %opt = ( port => 'synth', # required MIDI device (e.g. microKORG or fluidsynth) bpm => 80, # beats-per-minute arp_type => 'any', # 'any' or any known to the arp module note_num => '5,7', # number of notes to arp repeats => 1, # arp repeats before the next one begins spread => 4, # beats an arp should stretch across octave => '3,4,5', # octaves (0 .. 9) tonic => 'C', # scale key base note scale => 'minor', # scale name as known to Music::Scales program => 0, # synth program channel => 0, # the MIDI channel);# one arpeggiator instance is reusedmy = Music::MelodicDevice::Arpeggiation->new( repeats => {repeats}, verbose => 1,);# used to rescale durationsmy = Music::MelodicDevice::Arpeggiation::TICKS();# split thingsmy @octave = split /,/, {octave};my @note_nums = split /,/, {note_num};my @arp_types = {arp_type} eq 'any' ? keys ->arp_type->%* : split /,/, {arp_type}; # see the Music::MelodicDevice::Arpeggiation docs# get full range of pitches by octavemy @pitches = map { get_scale_MIDI({tonic}, $_, {scale}) } @octave;die "No pitches for tonic={tonic} scale={scale}\n" unless @pitches;say "Arp types: {arp_type}";say "Arp nums: {note_num}";say "Pitches: @pitches";# we are in 4/4 time...my = 4; # useful factor :)my = 6 * ; # PPQNmy = 60 / {bpm} / ; # time / bpm / ppqn# when spread is falsy, fall back to one bar's worth of beatsmy = {spread} || ;my @active; # { note => , off_tick => }my @pending; # { note => , on_tick => }my = 0; # clock ticksmy = 0; # beats!# open the MIDI device for outputmy = out_port({port});->program_change({channel}, {program});say "Opened {port}";# Ctrl-C clean shutdown{INT} = sub { say "\nStop"; stop_all_notes(); # make sure all notes are off exit;};# loop object to own and drive all timers/eventsmy = IO::Async::Loop->new;# drive the whole sequencer clock out, note off, note on, retriggermy = IO::Async::Timer::Periodic->new( interval => , reschedule => 'hard', # anchor to a fixed schedule, don't let tempo drift on_tick => sub { ->clock; # emit a MIDI clock tick ++; # advance the master tick counter # release any notes whose time is up for my $i (reverse 0 .. $#active) { # iterate in reverse so splice() below doesn't invalidate remaining indices if ( >= [$i]{off_tick}) { ->note_off({channel}, [$i]{note}, 0); splice @active, $i, 1; # remove from the "currently sounding" list } } # align to fire exactly on beat boundaries if (( - 1) % $clocks_per_beat == 0) { if ( % $phrase_beats == 0) { # retrigger every trigger_notes(); # start a new arp phrase! } ++; # only increment on beat boundaries } # collect every pending note whose start time has arrived my @ready = grep { >= $_->{on_tick} } @pending; # keep the notes still waiting for a future tick @pending = grep { < $_->{on_tick} } @pending; for my $p (@ready) { ->note_on({channel}, $p->{note}, velocity(-10, 10, 110)); # remember the note, so the release loop above can turn it off at the right tick push @active, { note => $p->{note}, off_tick => $p->{off_tick} }; } },);->start;->add();->run;sub trigger_notes { # pick a random note count, then that many random pitches, sorted low-to-high for the arpeggiator my @notes = sort { $a <=> $b } map { [int rand @pitches] } 1 .. [int rand @note_nums]; # XXX clunky # get an arpeggiated note list given a random arp_type my = ->arp(\@notes, 1, [int rand @arp_types]); # convert from the arp's 96-ticks-per-quarter-note scale to our clock ticks my @raw_ticks = map { my () = $_->[0] =~ /^d(\d+)$/; # duration encoded as a string like "d96" # rescale from the module's tick resolution to our own clock's ticks-per-beat max(1, int( * / )); } @; my = 1; # default multiplier: 1 = no rescaling if ({spread}) { # total unscaled duration of the arp my = sum0(@raw_ticks) || 1; # convert the desired spread (in beats) into tick units my = {spread} * ; # factor that stretches or squeezes the arp's total length to exactly fill the available ticks = / ; } # anchor the first note of this phrase to "right now" of the master clock my = ; for my $i (0 .. $#) { my (undef, ) = @{ ->[$i] }; # a note is a duration and a pitch # note's scaled length, floored at 1 tick my = max(1, int([$i] * )); # schedule the note to be turned on/off push @pending, { note => , on_tick => , off_tick => + , }; # advance the cursor so the next note in the arp starts right where this one ends += ; }}sub velocity (00, , ) { # generate a randomized velocity within [min+offset, max+offset] my = + int(rand( - 00 + 1)) + 00; return ;}> perl perl.com/arpeggios.plArp types: anyArp nums: 5,7Pitches: 48 50 52 53 55 57 59 60 62 64 65 67 69 71 72 74 76 77 79 81 83Opened midimate on program 21Repeat: 1, Type: pedal_down, Pattern: 6 5 6 4 6 3 6 2 6 1 6 0Ticks: 96, Duration: 8Arp: [ [ 'd8', 77 ], [ 'd8', 77 ], [ 'd8', 77 ], [ 'd8', 77 ], [ 'd8', 77 ], [ 'd8', 67 ], [ 'd8', 77 ], [ 'd8', 65 ], [ 'd8', 77 ], [ 'd8', 53 ], [ 'd8', 77 ], [ 'd8', 52 ],]Repeat: 1, Type: pedal_up, Pattern: 0 1 0 2 0 3 0 4 0 5 0 6Ticks: 96, Duration: 8Arp: [ [ 'd8', 50 ], [ 'd8', 53 ], [ 'd8', 50 ], [ 'd8', 53 ], [ 'd8', 50 ], [ 'd8', 72 ], [ 'd8', 50 ], [ 'd8', 72 ], [ 'd8', 50 ], [ 'd8', 76 ], [ 'd8', 50 ], [ 'd8', 76 ],]Repeat: 1, Type: down, Pattern: 4 3 2 1 0Ticks: 96, Duration: 19Arp: [ [ 'd19', 77 ], [ 'd19', 76 ], [ 'd19', 67 ], [ 'd19', 65 ], [ 'd19', 50 ] ]Repeat: 1, Type: down, Pattern: 4 3 2 1 0Ticks: 96, Duration: 19Arp: [ [ 'd19', 81 ], [ 'd19', 65 ], [ 'd19', 62 ], [ 'd19', 57 ], [ 'd19', 55 ] ]Repeat: 1, Type: pedal_updown, Pattern: 0 1 0 2 0 3 0 4 0 5 0 6 5 6 4 6 3 6 2 6 1 6 0Ticks: 96, Duration: 4Arp: [ [ 'd4', 48 ], [ 'd4', 60 ], [ 'd4', 48 ], [ 'd4', 64 ], [ 'd4', 48 ], [ 'd4', 65 ], [ 'd4', 48 ], [ 'd4', 67 ], [ 'd4', 48 ], [ 'd4', 71 ], [ 'd4', 48 ], [ 'd4', 76 ], [ 'd4', 71 ], [ 'd4', 76 ], [ 'd4', 67 ], [ 'd4', 76 ], [ 'd4', 65 ], [ 'd4', 76 ], [ 'd4', 64 ], [ 'd4', 76 ], [ 'd4', 60 ], [ 'd4', 76 ], [ 'd4', 48 ],]Repeat: 1, Type: random, Pattern: 0 3 3 2 2Ticks: 96, Duration: 19Arp: [ [ 'd19', 52 ], [ 'd19', 65 ], [ 'd19', 65 ], [ 'd19', 59 ], [ 'd19', 59 ] ]^CStopmidimate. It is worth pointing out that the out_port() function uses case-insensitive substrings/regexes to match MIDI device names.sleeping is not an option.These techniques have wider usefulness. Knowledge of event systems is essential for programming graphical and interactive applications, high-concurrency network services, simulations, , and more! :-)edit: Programming/Perl/auto___adventures_in_real_time_midi.wikieditish...