#!/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 ( ) { 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