The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

our $AUTHORITY = 'cpan:GENE';
# ABSTRACT: Control your MIDI controller
use v5.36;
our $VERSION = '0.0203';
use Moo;
use strictures 2;
use Carp qw(croak carp);
has verbose => (
is => 'ro',
isa => sub { croak "$_[0] is not a boolean" unless $_[0] =~ /^[01]$/ },
default => sub { 0 },
);
has input => (
is => 'ro',
required => 1,
);
has output => (
is => 'ro',
required => 1,
);
has loop => (
is => 'ro',
default => sub { IO::Async::Loop->new },
);
has filters => (
is => 'rw',
default => sub { {} },
);
# Private attributes
has _msg_channel => (
is => 'ro',
default => sub { IO::Async::Channel->new },
);
has _midi_channel => (
is => 'ro',
default => sub { IO::Async::Channel->new },
);
has _midi_out => (
is => 'ro',
default => sub { RtMidiOut->new },
);
sub BUILD {
my ($self) = @_;
my $midi_rtn = IO::Async::Routine->new(
channels_in => [ $self->_msg_channel ],
channels_out => [ $self->_midi_channel ],
model => 'spawn',
module => __PACKAGE__,
func => '_rtmidi_loop',
);
$self->loop->add($midi_rtn);
$self->_midi_channel->configure(
on_recv => sub ($channel, $event) {
$self->_filter_and_forward($event);
}
);
my $input_name = $self->input;
$self->_msg_channel->send(\$input_name);
$self->_midi_out->open_virtual_port('foo');
_open_port($self->_midi_out, $self->output);
}
sub _log {
return unless $ENV{PERL_FUTURE_DEBUG};
carp @_;
}
sub _open_port($device, $name) {
_log("Opening $device->{type} port $name ...");
$device->open_port_by_name(qr/\Q$name/i)
|| croak "Failed to open port $name";
_log("Opened $device->{type} port $name");
}
sub _rtmidi_loop ($msg_ch, $midi_ch) {
my $midi_in = MIDI::RtMidi::FFI::Device->new(type => 'in');
_open_port($midi_in, ${ $msg_ch->recv });
$midi_in->set_callback_decoded(sub { $midi_ch->send($_[2]) });
sleep;
}
sub _filter_and_forward ($self, $event) {
my $event_filters = $self->filters->{ $event->[0] } // [];
for my $filter ($event_filters->@*) {
return if $filter->($event);
}
$self->send_it($event);
}
sub send_it ($self, $event) {
$self->_midi_out->send_event($event->@*);
}
sub delay_send ($self, $delay_time, $event) {
$self->loop->add(
IO::Async::Timer::Countdown->new(
delay => $delay_time,
on_expire => sub { $self->send_it($event) }
)->start
)
}
sub run ($self) {
$self->loop->run;
}
sub add_filter ($self, $event_type, $action) {
push $self->filters->{$event_type}->@*, $action;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
MIDI::RtController - Control your MIDI controller
=head1 VERSION
version 0.0203
=head1 SYNOPSIS
use MIDI::RtController ();
my $rtc = MIDI::RtController->new(
input => 'input-MIDI-device',
output => 'output-MIDI-device',
);
sub filter_notes {
my ($note) = @_;
return $note, $note + 7, $note + 12;
}
sub filter_tone {
my ($event) = @_;
my ($ev, $channel, $note, $vel) = $event->@*;
my @notes = filter_notes($note);
$rtc->send_it([ $ev, $channel, $_, $vel ]) for @notes;
return 0;
}
$rtc->add_filter($_ => \&filter_tone) for qw(note_on note_off);
# add other stuff to the $rtc->loop...
$rtc->run;
=head1 DESCRIPTION
C<MIDI::RtController> allows you to control your MIDI controller using
plug-in filters.
=head1 ATTRIBUTES
=head2 verbose
$verbose = $rtc->verbose;
Show progress.
=head2 input
$input = $rtc->input;
Return the MIDI B<input> port.
=head2 output
$output = $rtc->output;
Return the MIDI B<output> port.
=head2 loop
$loop = $rtc->loop;
Return the L<IO::Async::Loop>.
=head2 filters
$filters = $rtc->filters;
Return or set the B<filters>.
=head1 METHODS
=head2 new
$rtc = MIDI::RtController->new(verbose => 1);
Create a new C<MIDI::RtController> object.
=for Pod::Coverage BUILD
=head2 send_it
$rtc->send_it($event);
Send a MIDI B<event> to the output port.
=head2 delay_send
$rtc->delay_send($delay_time, $event);
Send a MIDI B<event> to the output port when the B<delay_time> expires.
=head2 run
$rtc->run;
Run the asynchronous B<loop>!
=head2 add_filter
$rtc->add_filter($event_type => $action);
Add a filter, defined by the CODE reference B<action>, for an
B<event_type> like C<note_on> or C<note_off>.
=head1 THANK YOU
This code would not exist without the help of CPAN's JBARRETT (John
Barrett AKA fuzzix).
=head1 SEE ALSO
The F<eg/*.pl> example program(s)
L<Future::AsyncAwait>
L<IO::Async::Channel>
L<IO::Async::Loop>
L<IO::Async::Routine>
L<IO::Async::Timer::Countdown>
L<MIDI::RtMidi::FFI::Device>
L<Moo>
=head1 AUTHOR
Gene Boggs <gene.boggs@gmail.com>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2025 by Gene Boggs.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut