💾 Archived View for thrig.me › blog › 2023 › 10 › 09 › marimba-madness3.pl captured on 2023-12-28 at 16:55:03.

View Raw

More Information

-=-=-=-=-=-=-

#!/usr/bin/env perl
#
# marimba-madess3 - another Music::Factory example script
#
#     perl eg/marimba-madness out.midi
#     ...
#
# it may require a few runs for rand() to behave nicely, as the
# shuffle() might not result in anything good
#
# but computers writing music.... in that way only madness lies.

use 5.26.0;
use List::Util 'shuffle';
use MIDI;
use Music::Factory;
use Object::Pad;

my $srand = time();
srand $srand;

# the voices select from the sub-lists; the higher level array is picked
# from randomly each time the "what to do next" event shuffling thing
# happens down in makeatrack
my @field = (
    [   [qw(39 52)], [qw(52 56)], [qw(56 58)], [qw(58 59)],
        [qw(59 63)], [qw(63 64)], [qw(64 68)],
    ],
    [   [qw(40 52)], [qw(52 56)], [qw(58 59)], [qw(59 63)],
        [qw(64 68)], [qw(68 70)], [qw(70 71)],
    ],
    [   [qw(47 52)], [qw(52 56)], [qw(58 59)], [qw(59 63)],
        [qw(64 68)], [qw(68 70)], [qw(71 75)],
    ],
    [   [qw(51 52)], [qw(56 58)], [qw(59 63)], [qw(64 68)],
        [qw(70 71)], [qw(75 76)], [qw(80 82)]
    ]
);

# short long. second beat is a bit less loud
class DoubleBeat :isa(Music::Factory::Generator) {
    field $chan :param = 0;    # MIDI channel
    field $mul :param  = 1;
    field $pitch :param;       # callback
    field $velo :param;        # callback

    method update ( $epoch, $maxlen ) {
        my $p = $pitch->($epoch);
        my $d = 96;
        $epoch += $d;
        if ( $epoch > $maxlen ) {
            $d -= $epoch - $maxlen;
            return $d, [ [ note_off => $d, $chan, 0, 0 ] ];
        }
        my $v = $velo->($epoch);
        return 96,
          [ [ note_on  => 0,  $chan, $p, $v ],
            [ note_off => 32, $chan, $p, 0 ],
            [ note_on  => 0,  $chan, $p, $v - 15 ],
            [ note_off => 64, $chan, $p, 0 ],
          ];
    }
}

# a steady beat of some (not too) random interval
class Periodic :isa(Music::Factory::Generator) {
    field $chan :param = 0;    # MIDI channel
    field $mul :param  = 1;
    field $pitch :param;       # callback
    field $velo :param;        # callback
    field $interval :param = 96;
    method reset () { $interval = 64 * ( 1 + int rand 2 ) * $mul }

    method update ( $epoch, $maxlen ) {
        my $p = $pitch->($epoch);
        my $d = $interval;
        $epoch += $d;
        if ( $epoch > $maxlen ) {
            $d -= $epoch - $maxlen;
            return $d, [ [ note_off => $d, $chan, 0, 0 ] ];
        }
        return $interval,
          [ [ note_on  => 0,  $chan, $p, $velo->($epoch) ],
            [ note_off => $d, $chan, $p, 0 ],
          ];
    }
}

# MIDI track header
sub make_events {
    my %param = @_;
    $param{chan} //= 0;
    my @events = (
        [ track_name   => 0, $param{name} ],
        [ set_tempo    => 0, 420_000 ],
        [ patch_change => 0, $param{chan}, 12 ],
    );
    return \@events;
}

my @velo = qw( 105 85 100 80 );

sub velonoise { int( rand 4 + rand 5 + rand 6 ) }

sub makeatrack ( $name, $chan ) {
    my $ps     = $field[0];
    my $events = make_events( chan => $chan, name => $name );
    # the rests serve to offset the beats from one another, a little,
    # maybe, depending on where they get shuffled into the mix
    #
    # otherwise each voice gets a doublebeat and a periodic beat in some
    # order, using some selection of random pitch sets
    my @makers = (
        Music::Factory::AssemblyLine->new(
            events => $events,
            gen    => Music::Factory::Rest->new,
            maxlen => $chan * 1,
        ),
        Music::Factory::AssemblyLine->new(
            events => $events,
            gen    => Music::Factory::Rest->new,
            maxlen => 12 - ( $chan * 1 ),
        ),
        Music::Factory::AssemblyLine->new(
            events => $events,
            gen    => DoubleBeat->new(
                chan  => $chan,
                pitch => sub ($epoch) {
                    state $index  = 0;
                    state $switch = 4;
                    if ( $switch == 0 ) {
                        $switch = 4;
                        $index ^= 1;
                    } else {
                        $switch--;
                    }
                    $ps->[$chan][$index];
                },
                velo => sub ($epoch) {
                    state $i = 0;
                    $i = 0 if $epoch == 0;
                    my $v = $velo[$i];
                    $i = ( $i + 1 ) % @velo;
                    $v + velonoise();
                },
            ),
            maxlen => 336 * 5,
        ),
        Music::Factory::AssemblyLine->new(
            events => $events,
            gen    => Periodic->new(
                chan  => $chan,
                pitch => sub ($epoch) {
                    state $index  = 0;
                    state $switch = 4;
                    if ( $switch == 0 ) {
                        $switch = 4;
                        $index ^= 1;
                    } else {
                        $switch--;
                    }
                    $ps->[$chan][$index];
                },
                velo => sub ($epoch) {
                    state $i = 0;
                    $i = 0 if $epoch == 0;
                    my $v = $velo[$i];
                    $i = ( $i + 1 ) % @velo;
                    $v + velonoise();
                },
            ),
            maxlen => 576 * 4,
        ),
    );

    my $x = 0;
    for ( 1 .. 9 ) {
        @makers = shuffle @makers;
        $_->update for @makers;
        $ps = $field[ rand @field ];
    }
    # TODO better coda here
    my @lp = qw(58 58 63 70 75 82);
    push @$events,
      ( [ note_on  => 64,  $chan, $lp[$chan], 85 + velonoise() ],
        [ note_off => 16,  $chan, $lp[$chan], 0 ],
        [ note_on  => 0,   $chan, $lp[$chan], 95 + velonoise() ],
        [ note_off => 128, $chan, $lp[$chan], 0 ],
      );
    MIDI::Track->new( { events => $events } );
}

sub onefile ($file) {
    my @tracks;
    for my $i ( 1 .. 6 ) {
        my $chan  = $i - 1;
        my $track = sprintf "Track %03d $srand", $i;
        push @tracks, makeatrack( $track, $chan );
    }
    MIDI::Opus->new( { tracks => \@tracks } )->write_to_file($file);
}

sub manyfiles {
    for my $i ( 1 .. 6 ) {
        my $chan  = $i - 1;
        my $file  = sprintf 'out%03d.midi',      $i;
        my $track = sprintf "Track %03d $srand", $i;
        MIDI::Opus->new( { tracks => [ makeatrack( $track, $chan ) ] } )
          ->write_to_file($file);
    }
}

onefile( shift // 'out.midi' );