http://qs1969.pair.com?node_id=192657
Category: Miscellaneous
Author/Contact Info Simon Leidig (Django)
django@bitrock.net
Description:

This node is obsolete!
A completely rewritten and enhanced version can now be found here:
http://dmcgen.bitrock.net/
The original posting below remains unchanged - just ignore it.


This is a tool for musicians to generate MIDI controller variations. The program takes a template MIDI file, or a simple plain text representation of that, and generates from the given "Alpha" and "Omega" values a given number of variations, using the algorithms "random", "morph" or "switch". A typical use for that would be to create random settings of certain controllers of an instrument within a specified range.

You can copy the POD "for example.txt" at the end of the code and use it as template text file.

Instead of a one-line command or a GUI I've made up some kind of "talking shell", who asks you for one argument after the other, reports and provides a short help text.

It's my first "more than few lines" perl program and as you may see I'm experimenting with vertically oriented formatting to increase readability - an even spacier version of the code is on my Scratchpad. I would be glad to hear your oppinion concerning that. I know that there could be some structural improvement - please tell me if you find any major do-nots or not very elegant phrases.

I'd also welcome suggestions where else to post a tool like that.

  #!/usr/bin/perl

  use 5.6.1         ;
  use strict        ;
  use warnings      ;
   no warnings
qw/   uninitialized
      once          /;
  use MIDI          ;
  use File::Basename;
  use FileHandle    ;
  use Fatal
qw/:void  open
          close
          write     /;

# settings
# ---------------------------------------
  my @Modes =
qw/   Random
      Morph
      Switch    /;
  my @Range =
qw/     1
       40
      100       /;
  our $Test = 0 ;
  our($Message  ,
      $eType    ,
      $eNumber  ,
      $eValue   ,
      $tName    ,
      $FileName ,
      $Song     );

  format Header =

    DJANGOS MIDI CONTROLLER GENERATOR 1.2
    ================================================================

.
  format Message  =
~~  ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    $Message

.
  format Report =
    @<< @>>>>>>>>>>>>>>>>>> @>> @< @>> @<<<<<<< @<<<<<<<<<<<<<<<<<<<
    "set", $eType, $eNumber, "to", $eValue, "in track", $tName
.
  format Nothing  =
.

# subroutines
# ---------------------------------------

sub Say # prints formatted output of $_[0], new page if $_[1]
{ our $Message = shift or return 0  ;
  STDOUT->format_name('Message')    ;
  STDOUT->format_top_name('Header') ;
  if( @_ && !$Test )
  { system( "cls" ) if $^O =~ /MSWin32/ ;
    $^L = chr 0                     ;
    $-  = 0                         ;
  }
  write                             ;
}

sub Report  ($$$$)  # prints four values in 'Report' format
{ our($eType  ,
      $eNumber,
      $eValue ,
      $tName
  ) = @_ or return 0                ;
  STDOUT->format_name('Report')     ;
  STDOUT->format_top_name('Nothing');
  $^L = ""                          ;
  write                             ;
}

sub Help  ()
{ Say "I can generate variations of MIDI controllers. You have to
provide a template MIDI file (.mid) containing one instrument with two
tracks. They should consist of the same controllers, because the range
of variations for each controller will be determined by their values
in track A and B. I'll write a new file in the same folder, containing
a single track for each variation. You have the following options:\r
  - RANDOM generates random values ranging from A to B.\r
  - MORPH produces linear steps from A to B.\r
  - SWITCH switches each value randomly between A and B.\r
  You can also specify the number of variations to generate within a
range of 1 to 100.",                1;
  Say "If you give me a plain text file (.txt) instead of a MIDI file,
I will generate a MIDI template out of it, which can be used later.
See 'example.txt' for that.\r
  Now give me your template filename, e.g. C:\\foo\\bar.mid or
C:\\foo\\bar.txt\r
  Type 'exit' if you've had enough." ;
}

sub Input ($) # executes commands, returns input as scalar or list
{ no warnings                             ;
  my %Commands =
  ( 'exit' => sub{ exit; },
    'help' => sub{ Help && goto START; }
  )                                       ;
  chomp                                   ;
  $Commands{lc $_}() if $Commands{lc $_}  ;
  wantarray ? split /[\s,;]+/ : $_        ;
}

sub Confirmed ($)   # checks for something like 'Yes' or 'OK'
{ /(?:    \b   Y\w{0,3} [.!]?   \b
      |   \b   Ja?      [.!]?   \b
      |   \b   OK       [.!]?   \b
      |   ^    \n       $
  )/ix                              ;
}

sub Morph   ($$$$)  # min, max, step, total steps
{ sprintf( "%.0f", $_[0] + ( $_[1] - $_[0] ) * $_[2] / $_[3] )  ;
}

sub Random  ($$)    # min, max
{ splice @_, 2                                      ;
  @_ = reverse unless $_[1] >= $_[0]                ;
  sprintf( "%.0f", $_[0] + rand( $_[1] - $_[0] ) )  ;
}

sub Switch  ($$)    # returns $_[0] or $_[1]
{ splice @_, 2              ;
  int rand 2 ? shift : pop  ;
}

sub Move    ($$$)   # pos0, pos1, step
{ sprintf( "%.0f", ( $_[1] - $_[0] ) * ( $_[2] + 1 ) )  ;
}

sub Int7Bit         # value, ( min, max )?
{ (      (    0   <= $_[0] )    # check for integrity and round
      && ( $_[0]  <=   127 )
      ? return sprintf( "%.0f", $_[0] )
      : Say("There are some strange values in your text!") && return 0
  ) unless $_[1] ||  $_[2]    ;
           $_[1] ||=     0    ; # or transform from range
           $_[2] ||=   127    ;
         ( $_[1]  <= $_[0] )
      && ( $_[0]  <= $_[2] )
      && ( $_[1]  <  $_[2] )
  or Say("There are some strange values in your text!") && return 0 ;
  sprintf( "%.0f", ( ($_[0] - $_[1]) / ($_[2] - $_[1]) ) * 127 )    ;
}

# parsing arguments
# ---------------------------------------
# 1)  source (required)
# ---------------------------------------
  Say "Give me your template filename, e.g.:\rC:\\foo\\bar.mid\ror a
text template like\rC:\\foo\\bar.txt\rJust type 'help' if you don't
know what this is all about.",
                                      1;
START:  { $_ = <> ; }
  my $Src = Input $_                  ;
  my( $File, $Dir, $Ext ) = fileparse( $Src, qr/\b(\.mid|\.txt)\b/i )
  and open SRC, "< $Src"
  or Say( "That won't work, try again." ) && goto START
                                      ;
  goto TEXT2MIDI if lc $Ext eq '.txt' ;
# ---------------------------------------
  Say "OK, now you may specify your desired mode and number of
variations, e.g.: Morph 23",          1;
  $_ = <>                             ;
  my( $Mode, $Quant ) = Input $_      ;
# ---------------------------------------
# 2)  mode (implied)
# ---------------------------------------
  $Mode ||= "FOO"                     ;
  grep( /\b$Mode\b/i, @Modes )
  or ( $Mode = $Modes[0] ) && Say "Used default mode $Mode."
                                      ;
# ---------------------------------------
# 3)  quantity (implied)
# ---------------------------------------
{ $Quant = int $Quant                 ;
  Say "Used default quantity $Range[1]."
    unless $Quant                     ;
  $Quant ||= $Range[1]                ;
  ( $Quant = $Range[0] ) && Say "Used minimum quantity $Quant."
    unless $Quant >= $Range[0]        ;
  ( $Quant = $Range[2] ) && Say "Used maximum quantity $Quant."
    unless $Quant <= $Range[2]        ;
}

# main program
# ---------------------------------------
  $Song = MIDI::Opus->new({ 'from_file' => $Src })
                                      ;
  my $Track1 = ( $Song->tracks )[ 1]  ;
  my $Track2 = ( $Song->tracks )[-1]  ;
  my @Events1 = $Track1->events       ;
  my @Events2 = $Track2->events       ;
  for my $Step ( 1 .. $Quant )
  { push @{ $Song->tracks_r }, $Track2->copy  ;
    my $Track = ( $Song->tracks )[-1]         ;
    my @Events = $Track->events               ;
    my $Trackname = $Mode . $Step             ;
    $Events[1][-1] = $Trackname               ;
    $Events[3][ 1] = Move
        $Events1[3][1], $Events2[3][1], $Step ;
    for my $i ( 3 .. $#Events )
    { no strict 'refs'                        ;
      $Events[$i][-1] = &$Mode
        ( $Events1[$i][-1], $Events2[$i][-1], $Step, $Quant )        ;
      Report
        $Events[$i][0], $Events[$i][-2], $Events[$i][-1], $Trackname ;
    }
    $Track->events_r( \@Events )              ;
  }
  Say "Guess I set those values according to your wishes."  ;
  $FileName = $Dir . $File . "_" . $Mode . $Quant           ;

WRITEMIDI: { 1; }
  my $NewFile = $FileName . '.mid'    ;
  goto WRITEFILE unless -e $NewFile   ;
  Say "'$NewFile' already exists. Shall I overwrite it?"  ;
  $_ = <>                             ;
  goto WRITEFILE if Confirmed $_      ;
  my $i                               ;

COUNT:  { ++$i ; }
  my $NewName = $FileName . "_" . $i  ;
  $NewFile = $NewName . '.mid'        ;
  goto COUNT if -e $NewFile           ;

WRITEFILE: { 1; }
  close SRC                           ;
  open NEWFILE, ">", $NewFile         ;
  $Song->write_to_handle( *NEWFILE )  ;
  close NEWFILE                       ;
  Say "I wrote your file to '$NewFile'.\r
  May I switch myself off now?",      1;
  $_ = <>                             ;
  exit if Confirmed $_                ;
  Say "So give me another template to chew on." ;
  goto START                          ;


# text to midi template converter
# ---------------------------------------
TEXT2MIDI: { 1; }
  my @tEvents                 ;
  while ( <SRC> )
  { next unless /^\s*\d/      ;
    my @tLine =
    /    ^\s* (  \d+\.?\d*)
          \s+ (-?\d+\.?\d*)
          \s+ (-?\d+\.?\d*)
      (?: \s+ (-?\d+\.?\d*)
          \s+ (-?\d+\.?\d*) )?
    /x                        ;
    push @tEvents, \@tLine    ;
  }
  my @tEvents1 =
  ( [ 'raw_meta_event',  0, 32, "\x00"     ],
    [ 'track_name',      0, 'ALPHA   '     ],
    [ 'instrument_name', 0, 'GM Device  1' ] )  ;
  my @tEvents2 =
  ( [ 'raw_meta_event',  0, 32, "\x00"     ],
    [ 'track_name',      0, 'OMEGA   '     ],
    [ 'instrument_name', 0, 'GM Device  1' ] )  ;
  for my $i ( 0 .. $#tEvents )
  { my $Control = Int7Bit ( $tEvents[$i][0]                  )  ;
    my $Alpha   = Int7Bit ( $tEvents[$i][1],
                            $tEvents[$i][3], $tEvents[$i][4] )  ;
    my $Omega   = Int7Bit ( $tEvents[$i][2],
                            $tEvents[$i][3], $tEvents[$i][4] )  ;
    push @tEvents1, [ 'control_change', 1, 0, $Control, $Alpha ];
    push @tEvents2, [ 'control_change', 1, 0, $Control, $Omega ];
    Report 'control_change', $Control, $Alpha, 'ALPHA'          ;
    Report 'control_change', $Control, $Omega, 'OMEGA'          ;
  }
  $tEvents2[3][1] = 7680          ;
  my @tTracks                     ;
  $tTracks[0] = MIDI::Track->new
  ({  'type'  => 'MTrk',
      'events'=> [  [ 'time_signature',  0,  4,  2, 24,  8     ],
                    [ 'key_signature' ,  0,  0,  0             ],
                    [ 'smpte_offset'  ,  0, 33,  0,  0,  0,  0 ],
                    [ 'set_tempo'     ,  0, 500000             ]
                 ]  })            ;
  $tTracks[1] = MIDI::Track->new
  ({  'type'  => 'MTrk',
      'events'=> \@tEvents1 })    ;
  $tTracks[2] = MIDI::Track->new
  ({  'type'  => 'MTrk',
      'events'=> \@tEvents2 })    ;
  $Song       = MIDI::Opus->new
  ({  'format'=>   1,
      'ticks' => 480,
      'tracks'=> \@tTracks  })    ;
  Say "Guess I set those values according to your wishes."
                                  ;
  $FileName = $Dir . $File        ;
  goto WRITEMIDI                  ;


END { Say "Thanx for (ab)using me ;)",  1; }


=for example.txt

Control Alpha   Omega   Range   Range    Comments
number  value   value   minimum maximum
========================================
 0        0      127                   (cntr  0 ranges from  0 to 127)
 1      -10       10    -10       10   (cntr  1 ranges from  0 to 127)
 2        0       64                   (cntr  2 ranges from  0 to  64)
 3      -10        0    -10       10   (cntr  3 ranges from  0 to  64)
 4        0        1                   (cntr  4 ranges from  0 to   1)
 5        0.5      1      0        1   (cntr  5 ranges from 64 to 127)
42      500     1000      0     2000   (cntr 42 ranges from 32 to  64)
99       -1        1     -3        1   (guess what...)

Syntax description:
Mask a line with anything else than space or digit.
Use numbers like that: 23 or 23.45 or -0.678.
Separate with anything spacey.
controller number, alpha and omega values are required,
controller range minimum and maximum are optional -
use them for other ranges than 0 to 127.

=cut
Replies are listed 'Best First'.