💾 Archived View for thrig.me › blog › 2024 › 03 › 31 › midi2dev.pl captured on 2024-07-09 at 02:20:00.
⬅️ Previous capture (2024-05-10)
-=-=-=-=-=-=-
#!/usr/bin/env perl # # midi2dev.pl - send MIDI events to a device, with hopefully suitable # delays; a problem with midicat(1) is that it sends the events without # delays, which is maybe not so good for playback. # # midi2dev.pl -s 500000 foo.midi # default tempo # midi2dev.pl -b 144 foo.midi # tempo by BPM (quarter notes) # midi2dev.pl -n foo.midi # preview, does not open MIDI device # # Probably MIDI::Score could be used instead of manually parsing the # track events, though I'm not sure whether it combines different track # events to play at the same time. use 5.36.0; use Data::Dumper; use Getopt::Std 'getopts'; use MIDI; use Time::HiRes 'usleep'; use constant { NAME => 0, # slot into a MIDI::Event array DTIME => 1, CHAN => 2, }; getopts( 'nb:s:', \my %options ) or exit 1; my $filename = shift // die "Usage: $0 midi-file [midi-device]\n"; my $dev = shift // '/dev/rmidi0'; my $fh; unless ( $options{n} ) { open $fh, '>', $dev or die "open '$dev': $!\n"; binmode $fh; $fh->autoflush(1); } if ( exists $options{b} ) { # BPM in quarter notes; see Music::Tempo for where this code came # from, though the conversion here is for usleep, not to ms. die "$0: cannot use -b and -s together\n" if exists $options{s}; die "$0: -b must be a value >= 1\n" unless $options{b} >= 1; $options{s} = 240000_000 / ( $options{b} * 4 ); } elsif ( exists $options{s} ) { die "$0: -s must be >= 1\n" unless $options{s} >= 1; } else { $options{s} = 500_000; } my $opus = MIDI::Opus->new( { from_file => $filename } ); my $ticks = $opus->ticks; my $format = $opus->format; # Until I figure out what format 2 "sequentially independent single # track patterns" means, or any other formats that may be out there. die "$0: unsupported format $format\n" unless 0 <= $format <= 1; # conversion between dtime (from midi events) to usleep my $dtime2usec = $ticks / $options{s}; $Data::Dumper::Indent = 0; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Terse = 1; $Data::Dumper::Useqq = 1; my $time = 0; # global dtime into the MIDI file (for reference) # Events are references of events for each track; events_dtime holds # the duration (dtime) to the next event in the track, and credit is so # that longer events actually get played alongside shorter events in # other tracks. my @events = map { $_->events_r } $opus->tracks_r->@*; my @events_dtime; my @credit = (0) x @events; while (1) { # Drain events that have 0 dtime, and record when the next event for # the track is. for my $i ( 0 .. $#events ) { $events_dtime[$i] = ~0; drain( $i, $events[$i] ); } # Find what to play next (and how long to wait for it). my $min = ~0; for my $dtime (@events_dtime) { $min = $dtime if $dtime < $min; } if ( $min == ~0 ) { warn "notice: no remaining events (global time $time)\n"; last; } $time += $min; # May want a factor here so things can be sped up indepdent of tempo # changes, e.g. "/ 4" for a faster playback. usleep( $min / $dtime2usec ); # Consume (play) any minimum duration events. for my $i ( 0 .. $#events ) { advance( $i, $events[$i], $min ) if @{ $events[$i] }; } } sub advance ( $track_number, $event_list, $min_dtime ) { my $dtime = $event_list->[0][DTIME] - $credit[$track_number]; if ( $dtime == $min_dtime ) { play( $track_number, shift @$event_list ); $credit[$track_number] = 0; } else { $credit[$track_number] += $min_dtime; } } sub drain ( $track_number, $event_list ) { while (1) { last unless @$event_list; my $dtime = $event_list->[0][DTIME]; if ( $dtime == 0 ) { play( $track_number, shift @$event_list ); $credit[$track_number] = 0; } else { $events_dtime[$track_number] = $dtime - $credit[$track_number]; last; } } } # The MIDI::Event documentation details the array slots being used here. sub play ( $track_number, $event ) { if ( $event->[0] eq 'note_on' ) { #say "$track_number\t", $event->[NAME], "\t", Dumper($event); printf $fh "%c%c%c", 0x90 | $event->[CHAN], $event->[3], $event->[4] if $fh; } elsif ( $event->[0] eq 'note_off' ) { #say "$track_number\t", $event->[NAME], "\t", Dumper($event); printf $fh "%c%c%c", 0x80 | $event->[CHAN], $event->[3], $event->[4] if $fh; } elsif ( $event->[0] eq 'set_tempo' ) { # NOTE this changes the tempo in this code as that's where the # time control is done. May need a flag to ignore this? warn "set_tempo $event->[2]\n"; $options{s} = $event->[2]; $dtime2usec = $ticks / $options{s}; } else { say "$track_number\tTODO\t", Dumper($event); } }