-
Notifications
You must be signed in to change notification settings - Fork 5
/
lindenmayer-midi
227 lines (191 loc) · 5.44 KB
/
lindenmayer-midi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#!/usr/bin/env perl
# Create a MIDI file of various L-systems
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
use MIDI::Util qw(setup_score set_chan_patch);
use Music::ScaleNote ();
use Music::Note ();
use Pod::Usage;
pod2usage(1) unless @ARGV;
my %opts = (
rule => 2, # Rule number in the list of rules below
iterations => 4, # Number of iterations of the fractal curve
n_duration => 'qn', # Space separated list of note durations from which to choose *
r_duration => 'qn', # Space separated list of rest durations from which to choose *
midi_note => 60, # Initial midinum format note. 60 = Middle C
offset => 1, # +/- Distance to move in the scale for a new note value
scale => 'major', # Name of the scale to traverse
bpm => 300, # Beats per minute of the rendered MIDI
);
GetOptions( \%opts,
'rule=i',
'iterations=i',
'n_duration=s',
'r_duration=s',
'midi_note=i',
'offset=i',
'scale=i',
'bpm=i',
'help|?',
'man',
) or pod2usage(2);
pod2usage(1) if $opts{help};
pod2usage( -exitval => 0, -verbose => 2 ) if $opts{man};
# Split the durations into a list so that they can be randomly selected
my $n_duration = [ split /\s+/, $opts{n_duration} ];
my $r_duration = [ split /\s+/, $opts{r_duration} ];
# Default variables
my $format = 'midinum';
my $patchf = 0;
my $patchg = 13;
# The master list of fractals by rule number, their axioms and production rules
my %rules = (
1 => { # Branches
axiom => 'X',
X => 'YF-X+X',
Y => 'F',
},
2 => { # Koch curve
axiom => 'F',
F => 'F+F-F-F+F',
},
3 => { # Fractal plant
axiom => 'X',
X => 'F-XXF-X+FX',
F => 'FF',
},
4 => { # Dragon curve
axiom => 'FX',
X => 'X+YF+',
Y => '-FX-Y',
},
5 => { # Sierpiński arrowhead curve
axiom => 'F',
F => 'G-F-G',
G => 'F+G+F',
},
6 => { # Sierpiński triangle
axiom => 'F-G-G',
F => 'F-G+F+G-F',
G => 'GG',
},
7 => { # Koch snowflake
axiom => 'F++F++F',
F => 'F-F++F-F',
X => 'FF',
},
8 => { # Sierpiński carpet
axiom => 'F',
F => 'F+F-F-F-G+F+F+F-F',
G => 'GGG',
},
9 => { # Koch island
axiom => 'F-F-F-F',
F => 'F-F+F+FF-F-F+F',
},
10 => { # Koch islands and lakes
axiom => 'F+F+F+F',
F => 'F+f-FF+F+FF+Ff+FF-f+FF-F-FF-Ff-FFF',
f => 'ffffff',
},
11 => { # Grid
axiom => 'F-F-F-F',
F => 'FF-F-F-F-FF',
},
12 => { # Terndrils
axiom => 'F-F-F-F',
F => 'FF-F--F-F',
},
13 => { # Custom
axiom => 'F+G-F+G',
F => 'FG+F--F+F',
},
14 => { # Branches with space
axiom => 'X',
X => 'YF-X+X',
Y => 'f',
},
15 => { # Leaf
axiom => 'X',
X => 'F[+X][-X]FX',
F => 'FF',
},
);
my $midi_note = $opts{midi_note};
# Get the axiom to use based on the given rule
my $string = $rules{ $opts{rule} }{axiom};
# Prepare to create MIDI
my $score = setup_score( bpm => $opts{bpm} );
# Create a note object for the given start note value
my $note = Music::Note->new( $midi_note, $format );
# Create a scale-note object to use to traverse the given scale
my $msn = Music::ScaleNote->new(
scale_note => $note->format('isobase'),
scale_name => $opts{scale},
# verbose => 1,
);
# The dispatch table of MIDI routines based on "turtle graphic" moves
my %translate = (
# Add a rest to the score
'f' => sub { $score->r( random_duration( @$r_duration ) ) },
# Add a note to the score
'F' => sub {
set_chan_patch( $score, 0, $patchf );
$score->n( random_duration( @$n_duration ), $midi_note );
},
# Add a note to the score
'G' => sub {
set_chan_patch( $score, 1, $patchg );
$score->n( random_duration( @$n_duration ), $midi_note );
},
# Decrement the scale-note
'-' => sub {
$midi_note = $msn->get_offset(
note_name => $midi_note,
note_format => $format,
offset => -$opts{offset},
)->format($format);
},
# Increment the scale-note
'+' => sub {
$midi_note = $msn->get_offset(
note_name => $midi_note,
note_format => $format,
offset => $opts{offset},
)->format($format);
},
);
# Apply the string re-writing production rules
for ( 1 .. $opts{iterations} ) {
$string =~ s/(.)/defined($rules{ $opts{rule} }{$1}) ? $rules{ $opts{rule} }{$1} : $1/eg;
}
warn "$string\n";
# Execute the dispatch routines defined by the string elements
for my $command ( split //, $string ) {
last if $midi_note < 0 || $midi_note > 127;
$translate{$command}->() if exists $translate{$command};
}
# Write the MIDI file
$score->write_score( $0 . '.mid' );
# Return a random duration. * A set of durations makes things disjointed and strange. YMMV
sub random_duration {
my (@duration) = @_;
return $duration[ int rand @duration ];
}
__END__
=head1 NAME
lindenmayer-midi - Create a MIDI file of L-system transformations
=head1 SYNOPSIS
lindenmayer-midi [--options|-o]
TODO!
=head1 OPTIONS
=over 4
=item B<help>
Print a brief help message.
=item B<man>
Print the full help (the "manpage).
=back
=head1 DESCRIPTION
B<lindenmayer-midi> transforms the input into a MIDI file of L-systems!
=cut