Enhancing Your MIDI Devices: Round II

Control Your MIDI Controllers!

As we discovered previously, your MIDI devices can be enhanced to function in different ways besides just triggering a single note per key (or pad) press.

Being a serial module creator, and with the help of the author John, I bundled these concepts and more into a few handy CPAN packages that allow you to control your devices with minimal lines of code. So far, these are: MIDI::RtController, MIDI::RtController::Filter::Tonal, and MIDI::RtController::Filter::Drums.

With these, you can do lots of cool things to enhance your MIDI device with filters (special subroutines). These routines are then executed in real-time when a key or pad is pressed on your MIDI device.

First, let’s inspect the module MIDI::RtController itself.

Crucially, it has required input and output attributes that are turned into instances of MIDI::RtMidi::FFI::Device. The first is your controller. The second is your MIDI output, like fluidsynth, timidity, virtual port, your DAW (“digital audio workstation”), etc.

Also, because RtController can operate asynchronously, it uses IO::Async::Loop and IO::Async::Channels. Within the module, the latter serves as MIDI in. This channel is listened to, and messages from the input device are processed by the known filters, before being sent out.

How about an example of this in action?

The module’s public interface has four methods: add_filter, send_it, delay_send, and run.

#!/usr/bin/env perl
use v5.36;
use MIDI::RtController;

my $in  = $ARGV[0] || 'oxy'; # part of the name of the MIDI controller device
my $out = $ARGV[1] || 'gs';  # part of the name of the MIDI output device

my $rtc = MIDI::RtController->new(input => $in, output => $out);

$rtc->add_filter('pedal', [qw(note_on note_off)], \&pedal_tone);

$rtc->run;

sub pedal_notes ($note) {
    return 55, $note, $note + 7; # 55 = G below middle-C
}
sub pedal_tone ($dt, $event) {
    my ($ev, $chan, $note, $vel) = $event->@*;
    my @notes = pedal_notes($note);
    my $delay_time = 0;
    for my $n (@notes) {
        $delay_time += $delay;
        $rtc->delay_send($delay_time, [ $ev, $chan, $n, $vel ]);
    }
    return 0;
}

The filter subroutine, “pedal_tone”, is called with a “delta-time ($dt) and the MIDI event ($event). The event is first broken into its 4 parts and the $note is used to compute and return the pedal_notes. Next the notes are played, with a delay (but could be played simultanously with the send_it method, instead).

First, let’s hear the unprocessed sound, to have a point of reference:

Ok. Here’s what the pedal-tone filter sounds like with roughly the same phrase:

Pretty different!

How do I see the MIDI devices known to my system?

You can use this example program in the MIDI::RtMidi::FFI::Device distribution. Also, you can install and use the cross-platform program ReceiveMIDI, which is useful for many things.

Right now on my system, executing receivemidi list returns:

IAC Driver Bus 1
Synido TempoPAD Z-1
Logic Pro Virtual Out

And I start the fluidsynth program with:

fluidsynth -a coreaudio -m coremidi -g 2.0 ~/Music/soundfont/FluidR3_GM.sf2

Currently, I’m on my Mac, so this command tells fluidsynth that I’m using coreaudio for the audio driver, coremidi for the midi driver, 2.0 for the gain (because rendered MIDI playback is quiet), and finally my soundfont file.

So what if I don’t want to write filters?

You are in luck! There are currently tonal and drums filters on CPAN. Each includes example programs (tonal and drums respectively). Here is an example of one of the simpler tonal filters:

#!/usr/bin/env perl
use curry;
use MIDI::RtController ();
use MIDI::RtController::Filter::Tonal ();

my $input_name  = shift || 'tempopad'; # midi controller device
my $output_name = shift || 'fluid';    # fluidsynth

my $rtc = MIDI::RtController->new(
    input  => $input_name,
    output => $output_name,
);

my $rtf = MIDI::RtController::Filter::Tonal->new(rtc => $rtc);

$rtc->add_filter('pedal', [qw(note_on note_off)], $rtf->curry::pedal_tone);

$rtc->run;

By the way, curry allows us to refer to an object-oriented method as a CODE reference in a smooth way.

And yes, this pedal_tone routine is the same as the previous, above - just OO now.

What if I do want to create my own filters?

If you would like to craft your own musical or control filters, you can use MIDI::RtController::Filter::Math as a spring-board, point-of-reference example. This implements a “stair-step” filter (detailed below). Here is an example of that in action:

#!/usr/bin/env perl
use curry;
use MIDI::RtController ();
use MIDI::RtController::Filter::Math ();

my $input_name  = shift || 'tempopad'; # midi controller device
my $output_name = shift || 'fluid';    # fluidsynth

my $rtc = MIDI::RtController->new(
    input  => $input_name,
    output => $output_name,
);

my $rtf = MIDI::RtController::Filter::Math->new(rtc => $rtc);

# $rtf->delay(0.15); # slow down the delay time
# $rtf->feedback(6); # increase the number of steps

$rtc->add_filter('stair', [qw(note_on note_off)], $rtf->curry::stair_step);

$rtc->run;

And here’s what that sounds like:

Wacky! It’s like you’re Liberace, but crazier.

Ok, let’s look at how a filter is made

First-up is that [MIDI::RtController::Filter::Math]() is a [Moo]() module, but any OO will do the job. Second is that attributes are defined for all the parameters our filter routine(s) will need, like feedback for instance:

has feedback => (
    is      => 'rw',      # changable on-the-fly
    isa     => Num,       # as defined by a Types::* module
    default => sub { 1 }, # single echo default
);

Please see the source for these.

The public object oriented routine, stair_step uses a private _stair_step_notes local method and the delay_send RtController method. The first decides what notes we will play, and the second sends a MIDI event to the MIDI output device, with a number of (usually fractional) seconds to delay output. So we gather the notes (more on this in a bit), then play them one at a time with a steadily incrementing delay time. Lastly we return false AKA 0 (zero), so that RtController knows to continue processing other filters.

sub stair_step ($self, $dt, $event) {
    my ($ev, $chan, $note, $vel) = $event->@*;
    my @notes = $self->_stair_step_notes($note);
    my $delay_time = 0;
    for my $n (@notes) {
        $delay_time += $self->delay;
        $self->rtc->delay_send($delay_time, [ $ev, $self->channel, $n, $vel ]);
    }
    return 0;
}

For this particular “stair-step” filter, notes are played from the beginning event note, given the up and down attributes. Each note is first incremented by the up value, then the next note is decremented by the value of down - rinse, repeat. The value of feedback determines how many steps will be made. (You may notice that the object channel is used instead of the event $chan. This is done in order to change channels regardless of the MIDI input device channel setting.)

Lastly, here is the subroutine that computes the notes to play:

sub _stair_step_notes ($self, $note) {
    my @notes;
    my $factor;
    my $current = $note;
    for my $i (1 .. $self->feedback) {
        if ($i % 2 == 0) {
            $factor = ($i - 1) * $self->down;
        }
        else {
            $factor = $i * $self->up;
        }
        $current += $factor;
        push @notes, $current;
    }
    return @notes;
}

Conclusion

You can soup-up your MIDI controller with this code, to fabulous effect and without much ado!

And for a more complete, real-world example (that is a work-in-progress), please see the code in rtmidi-callback.pl.

(And personally, I just rediscovered my MIDI Rock joystick controller and am very anxious to make an app like this, for it. Woo!)

Happy controlling!

Tags

Gene Boggs

Gene Boggs is a long-time Perl user, software engineer, and musician.

Browse their articles

Feedback

Something wrong with this article? Help us out by opening an issue or pull request on GitHub