File Coverage

blib/lib/MIDI/SoundFont.pm
Criterion Covered Total %
statement 712 1086 65.5
branch 118 278 42.4
condition 14 44 31.8
subroutine 17 35 48.5
pod 0 29 0.0
total 861 1472 58.4


line stmt bran cond sub pod time code
1             # MIDI::SoundFont.pm
2             #########################################################################
3             # This Perl module is Copyright (c) 2002, Peter J Billam #
4             # c/o P J B Computing, www.pjb.com.au #
5             # #
6             # This module is free software; you can redistribute it and/or #
7             # modify it under the same terms as Perl itself. #
8             #########################################################################
9              
10             package MIDI::SoundFont;
11 1     1   990 no strict;
  1         2  
  1         33  
12 1     1   1025 use bytes;
  1         12  
  1         5  
13             #my $debug = 1; use Data::Dumper;
14             $VERSION = '1.08';
15             $VERSION_DATE = '18may2013';
16              
17             # 20130518 1.07 Makefile.PL specifies PREREQ_PM, to improve test results :-)
18             # 20130515 1.06 test.pl skips Gravis.zip test if String::Approx not installed
19             # 20120809 1.05 added the csound_scoresynth and csound_midisynth examples
20             # 20120322 1.04 pack a=zeropadded rather than A=spacepadded; introduce
21             # new_gf(), gravis2file now works, and make_bank5 does gravis too
22             # 20120320 1.03 new_sf(), and chCorrection is packed as signed
23             # 20120318 1.02 detect duplicate Preset,Inst,Sample names and uniquely rename
24             # 20120216 1.01 gravis2file writes .zip files
25             # 20120215 1.00 first released version
26              
27             require Exporter;
28             require DynaLoader;
29             @ISA = qw(Exporter DynaLoader);
30             @EXPORT = ();
31             @EXPORT_OK = qw( GeneratorOperators GenAmountType bytes2sf file2sf
32             sf2bytes sf2file new_sf file2gravis gravis2file new_pat timidity_cfg
33             );
34             @EXPORT_CONSTS = qw(GeneratorOperators GenOpname2num GenAmountType
35             MODES_16BIT MODES_UNSIGNED MODES_LOOPING MODES_PINGPONG
36             MODES_REVERSE MODES_SUSTAIN MODES_ENVELOPE MODES_CLAMPED);
37             %EXPORT_TAGS = (ALL => [@EXPORT_OK], CONSTS => [@EXPORT_CONSTS]);
38              
39             eval 'require File::Format::RIFF';
40             if ($@) {
41             die "you need to install the File::Format::RIFF module from www.cpan.org\n";
42             }
43             # local $[ = 0; # SoundFont indexes start at zero but setting $[ is deprecated
44             my %SampleName = (); # to avoid duplicating sample-names...
45              
46             # ----------------------- exportable constants -----------------------
47             @GeneratorOperators = qw(
48             startAddrsOffset endAddrsOffset startloopAddrsOffset endloopAddrsOffset
49             startAddrsCoarseOffset modLfoToPitch vibLfoToPitch modEnvToPitch
50             initialFilterFc initialFilterQ modLfoToFilterFc modEnvToFilterFc
51             endAddrsCoarseOffset modLfoToVolume unused1 chorusEffectsSend
52             reverbEffectsSend pan unused2 unused3
53             unused4 delayModLFO freqModLFO delayVibLFO
54             freqVibLFO delayModEnv attackModEnv holdModEnv
55             decayModEnv sustainModEnv releaseModEnv keynumToModEnvHold
56             keynumToModEnvDecay delayVolEnv attackVolEnv holdVolEnv
57             decayVolEnv sustainVolEnv releaseVolEnv keynumToVolEnvHold
58             keynumToVolEnvDecay instrument reserved1 keyRange
59             velRange startloopAddrsCoarseOffset keynum velocity
60             initialAttenuation reserved2 endloopAddrsCoarseOffset coarseTune
61             fineTune sampleID sampleModes reserved3
62             scaleTuning exclusiveClass overridingRootKey unused5
63             endOper
64             );
65             %GenOpname2num = (); {
66             my $i=0; while ($i <= $#GeneratorOperators) {
67             $GenOpname2num{$GeneratorOperators[$i]} = $i;
68             $i += 1;
69             }
70             }
71             @GenAmountType = qw (
72             S s s s
73             s s s s
74             s s s s
75             s s x s
76             s s x x
77             x s s s
78             s s s s
79             s s s s
80             s s s s
81             s s s s
82             s S x C2
83             C2 s S S
84             S x s s
85             s S S x
86             s S s x
87             x
88             ); # s signed, S unsigned, C2 two bytes, x null; sfspec21 8.1.2 & guesswork
89              
90             $MODES_16BIT = 1; $MODES_UNSIGNED = 2;
91             $MODES_LOOPING = 4; $MODES_PINGPONG = 8;
92             $MODES_REVERSE = 16; $MODES_SUSTAIN = 32;
93             $MODES_ENVELOPE = 64; $MODES_CLAMPED = 128;
94              
95             # sf:
96             # see http://www.pjb.com.au/midi.sfspec21.html#8.1.3
97             my %OnlyValidInInstr = map { $_, 1 } (0,1,2,3,4,12,45,50,54,57,58);
98             # gravis:
99             my $DefaultEnvelopeData = "\x3f\x46\x81\x42\x3f\x3f\xd5\xf2\xf6\x08\x08\x08";
100              
101              
102             # ----------------------- exportable functions -----------------------
103             sub file2bytes {
104             # read bytes from file, or url, or filehandle, or - is stdin
105 2     2 0 7 my $bytes;
106 2 50       28 if ($_[0] eq '-') {
    50          
    50          
107 0         0 undef $/; binmode STDIN, ':raw';
  0         0  
108 0         0 $bytes = ;
109             } elsif ($_[0] =~ /^[a-z]+:\//) {
110 0 0       0 eval 'require LWP::Simple'; if ($@) {
  0         0  
111 0         0 die "you'll need to install libwww-perl from www.cpan.org\n";
112             }
113 0         0 my $bytes = LWP::Simple::get($_[0]);
114 0 0       0 if (! defined $bytes) { die("can't fetch $_[0]\n"); }
  0         0  
115             } elsif (ref($_[0]) eq 'GLOB') {
116             # must open the file ?
117 0         0 undef $/; binmode $_[0], ':raw';
  0         0  
118 0         0 $bytes = <$_[0]>;
119 0         0 close $_[0];
120             } else {
121 2 50       143 if (! open(F, '<:raw', $_[0])) {
122 0         0 warn "can't open $_[0]: $!\n"; return '';
  0         0  
123             }
124 2         11 undef $/; binmode F; $bytes = ; close F;
  2         8  
  2         9590  
  2         50  
125             }
126 2         16 return $bytes;
127             }
128              
129             sub file2sf {
130 1     1 0 24 my $bytes = file2bytes($_[0]);
131 1         12 return bytes2sf($bytes);
132             }
133              
134             sub file2dump {
135 0     0 0 0 my $bytes = file2bytes($_[0]);
136 0         0 return bytes2dump($bytes);
137             }
138              
139 0     0 0 0 sub bytes2dump { my $bytes = $_[0];
140 0         0 my %sf = ();
141 0 0       0 if (! open(P, '<', \$bytes)) {
142 0         0 warn "can't open in-memory filehandle: $!\n"; return;
  0         0  
143             }
144 0         0 undef $/; binmode P;
  0         0  
145 0         0 my $riff = File::Format::RIFF->read(\*P, length($bytes));
146 0         0 close P;
147 0         0 my $info = $riff->at(0); $info->dump;
  0         0  
148 0         0 my $sdta = $riff->at(1); $sdta->dump;
  0         0  
149 0         0 my $smpl = $sdta->shift();
150 0         0 my $smpl_data = $smpl->data();
151 0         0 my $pdta = $riff->at(2); $pdta->dump;
  0         0  
152             }
153              
154 2     2 0 24 sub bytes2sf { my $bytes = $_[0]; # take it apart with RIFF
155 2         9 my %sf = ();
156 1 50   1   17 if (! open(P, '<', \$bytes)) {
  1         2  
  1         16  
  2         109  
157 0         0 warn "can't open in-memory filehandle: $!\n"; return;
  0         0  
158             }
159 2         2320 undef $/; binmode P;
  2         8  
160 2         35 my $riff = File::Format::RIFF->read(\*P, length($bytes));
161 2         22022 close P;
162 2         17 my $info = $riff->at(0);
163 2         29 my $sdta = $riff->at(1);
164 2         50 my $smpl = $sdta->shift();
165 2         28 my $smpl_data = $smpl->data();
166 2         16 my $pdta = $riff->at(2);
167              
168 2         11 while (1) { # INFO
169 20         51 my $chunk = $info->shift();
170 20 100       182 if (! defined $chunk) { last; }
  2         7  
171 18         48 my $id = $chunk->id();
172 18         110 my $data = $chunk->data();
173 18 100 66     130 if ($id eq 'ifil' or $id eq 'iver') {
174 2         9 my ($wMajor, $wMinor) = unpack('SS', $data);
175 2         26 $sf{$id} = "$wMajor.$wMinor";
176             } else {
177 16         212 $data =~ s/\0*$//s;
178 16         51 $sf{$id} = $data;
179             }
180             }
181 2         6 my %pdta = ();
182 2         4 while (1) { # PDTA
183 20         195 my $chunk = $pdta->shift();
184 20 100       194 if (! defined $chunk) { last; }
  2         3  
185 18         44 $pdta{$chunk->id()} = $chunk->data();
186             # warn $chunk->id()." is ".length($pdta{$chunk->id()})." bytes long\n";
187             }
188              
189             # http://www.pjb.com.au/midi/sfspec21.html#7.2
190 2 50       12 if (! $pdta{'phdr'}) { warn "missing phdr sub-chunk\n"; return undef; }
  0         0  
  0         0  
191 2 50       5 my $len = length $pdta{'phdr'}; if ($len % 38) {
  2         8  
192 0         0 warn "phdr sub-chunk not a multiple of 38 bytes\n"; return undef;
  0         0  
193             }
194 2         3 my $ind = 0; # $[ must be zero
195 2         4 my @phdr_list = ();
196 2         5 my %preset_names_seen = (); # 1.02
197 2         8 while ($ind < $len) { # sfspec21.txt 7.2
198 410         528 my $phdr_rec = substr $pdta{'phdr'}, $ind, 38;
199 410         1111 my ($achPresetName,$wPreset,$wBank,$wPresetBagNdx,$dwLibrary,
200             $dwGenre,$dwMorphology) = unpack 'A20SSSLLL', $phdr_rec;
201 410         816 $achPresetName =~ s/\0.*$//s;
202             # 1.02 detect duplicate names and rename as necessary (7.2)
203 410         401 my $orig = $achPresetName;
204 410         350 my $x = 2; while ($preset_names_seen{$achPresetName}) {
  410         877  
205 0         0 $achPresetName = $orig."_$x"; $x += 1;
  0         0  
206             }
207 410         759 $preset_names_seen{$achPresetName} = 1;
208 410         1201 push @phdr_list, {
209             achPresetName => $achPresetName,
210             wPreset => $wPreset,
211             wBank => $wBank,
212             wPresetBagNdx => $wPresetBagNdx,
213             # dwLibrary => $dwLibrary,
214             # dwGenre => $dwGenre,
215             # dwMorphology => $dwMorphology,
216             };
217 410         1097 $ind += 38;
218             }
219              
220             # http://www.pjb.com.au/midi/sfspec21.html#7.3
221 2 50       40 if (! $pdta{'pbag'}) { warn "missing pbag sub-chunk\n"; return undef; }
  0         0  
  0         0  
222 2 50       4 $len = length $pdta{'pbag'}; if ($len % 4) {
  2         8  
223 0         0 warn "pbag sub-chunk not a multiple of 4 bytes\n"; return undef;
  0         0  
224             }
225 2         3 $ind = 0;
226 2         4 my @pbag_list = ();
227 2         12 while ($ind < $len) { # sfspec21.txt 7.3
228 1944         2219 my $pbag_rec = substr $pdta{'pbag'}, $ind, 4;
229 1944         2516 my ($wGenNdx,$wModNdx) = unpack 'SS', $pbag_rec;
230 1944         3923 push @pbag_list, {
231             wGenNdx => $wGenNdx,
232             wModNdx => $wModNdx,
233             };
234 1944         3411 $ind += 4;
235             }
236              
237             # http://www.pjb.com.au/midi/sfspec21.html#7.4
238 2 50       19 if (! $pdta{'pmod'}) { warn "missing pmod sub-chunk\n"; return undef; }
  0         0  
  0         0  
239 2 50       4 $len = length $pdta{'pmod'}; if ($len % 10) {
  2         9  
240 0         0 warn "pmod sub-chunk not a multiple of 10 bytes\n"; return undef;
  0         0  
241             }
242 2         3 $ind = 0;
243 2         6 my @pmod_list = ();
244 2         10 while ($ind < $len) { # sfspec21.txt 7.4
245 2         8 my $pmod_rec = substr $pdta{'pmod'}, $ind, 10;
246 2         7 my ($sfModSrcOper,$sfModDestOper,$modAmount,$sfModAmtSrcOper,
247             $sfModTransOper) = unpack 'SSSSS', $pmod_rec;
248 2         14 push @pmod_list, {
249             sfModSrcOper => $sfModSrcOper,
250             sfModDestOper => $sfModDestOper,
251             modAmount => $modAmount,
252             sfModAmtSrcOper => $sfModAmtSrcOper,
253             sfModTransOper => $sfModTransOper,
254             };
255 2         8 $ind += 10;
256             }
257              
258             # http://www.pjb.com.au/midi/sfspec21.html#7.6
259 2 50       11 if (! $pdta{'inst'}) { warn "missing inst sub-chunk\n"; return undef; }
  0         0  
  0         0  
260 2 50       4 $len = length $pdta{'inst'}; if ($len % 22) {
  2         6  
261 0         0 warn "inst sub-chunk not a multiple of 22 bytes\n"; return undef;
  0         0  
262             }
263 2         4 $ind = 0; # $[ _must_ be zero
264 2         3 my @inst_list = ();
265 2         5 my %inst_names_seen = (); # 1.02
266 2         6 while ($ind < $len) { # sfspec21.html#7.6
267 166         218 my $inst_rec = substr $pdta{'inst'}, $ind, 22;
268 166         317 my ($achInstName,$wInstBagNdx) = unpack 'A20S', $inst_rec;
269 166         323 $achInstName =~ s/\0.*$//s;
270             # 1.02 detect duplicate names and rename as necessary (7.6)
271 166         158 my $orig = $achInstName;
272 166         148 my $x = 2; while ($inst_names_seen{$achInstName}) {
  166         328  
273 0         0 $achInstName = $orig."_$x"; $x += 1;
  0         0  
274             }
275 166         311 $inst_names_seen{$achInstName} = 1;
276 166         388 push @inst_list, {
277             achInstName => $achInstName,
278             wInstBagNdx => $wInstBagNdx,
279             };
280 166         329 $ind += 22;
281             }
282              
283             # http://www.pjb.com.au/midi/sfspec21.html#7.7
284 2 50       10 if (! $pdta{'ibag'}) { warn "missing ibag sub-chunk\n"; return undef; }
  0         0  
  0         0  
285 2 50       4 $len = length $pdta{'ibag'}; if ($len % 4) {
  2         7  
286 0         0 warn "ibag sub-chunk not a multiple of 4 bytes\n"; return undef;
  0         0  
287             }
288 2         3 $ind = 0;
289 2         6 my @ibag_list = ();
290 2         16 while ($ind < $len) { # sfspec21.txt 7.7
291 958         1162 my $ibag_rec = substr $pdta{'ibag'}, $ind, 4;
292 958         1435 my ($wInstGenNdx,$wInstModNdx) = unpack 'SS', $ibag_rec;
293 958         2067 push @ibag_list, {
294             wInstGenNdx => $wInstGenNdx,
295             wInstModNdx => $wInstModNdx,
296             };
297 958         1740 $ind += 4;
298             }
299             # now go though @inst_list extracting each preset's lists of bags
300 2         5 $i = 0; while ($i < $#inst_list) {
  2         10  
301 164         206 my $from = $inst_list[$i]{'wInstBagNdx'};
302 164         208 my $to = $inst_list[$i+1]{'wInstBagNdx'};
303             # should check monotonicity and in-rangeness
304 164         227 my @ibags = (); my $j = $from;
  164         159  
305 164         276 while ($j < $to) { push @ibags, $ibag_list[$j]; $j += 1; }
  956         1104  
  956         1469  
306 164         254 $inst_list[$i]{'ibags'} = \@ibags;
307 164         345 $i += 1;
308             }
309              
310             # http://www.pjb.com.au/midi/sfspec21.html#7.8
311 2 50       10 if (! $pdta{'imod'}) { warn "missing imod sub-chunk\n"; return undef; }
  0         0  
  0         0  
312 2 50       3 $len = length $pdta{'imod'}; if ($len % 10) {
  2         7  
313 0         0 warn "imod sub-chunk not a multiple of 10 bytes\n"; return undef;
  0         0  
314             }
315 2         4 $ind = 0;
316 2         5 my @imod_list = ();
317 2         6 while ($ind < $len) { # sfspec21.txt 7.8
318 68         91 my $imod_rec = substr $pdta{'imod'}, $ind, 10;
319 68         147 my ($sfModSrcOper,$sfModDestOper,$modAmount,$sfModAmtSrcOper,
320             $sfModTransOper) = unpack 'SSSSS', $imod_rec;
321 68         229 push @imod_list, {
322             sfModSrcOper => $sfModSrcOper,
323             sfModDestOper => $sfModDestOper,
324             modAmount => $modAmount,
325             sfModAmtSrcOper => $sfModAmtSrcOper,
326             sfModTransOper => $sfModTransOper,
327             };
328 68         139 $ind += 10;
329             }
330              
331             # http://www.pjb.com.au/midi/sfspec21.html#7.10
332 2 50       9 if (! $pdta{'shdr'}) { warn "missing shdr sub-chunk\n"; return undef; }
  0         0  
  0         0  
333 2 50       4 $len = length $pdta{'shdr'}; if ($len % 46) {
  2         8  
334 0         0 warn "shdr sub-chunk not a multiple of 46 bytes\n"; return undef;
  0         0  
335             }
336 2         3 $ind = 0;
337 2         5 my @shdr_list = ();
338 2         7 my %sample_names_seen = (); # 1.02
339 2         6 while ($ind < $len) { # sfspec21.html#7.10
340 334         540 my $shdr_rec = substr $pdta{'shdr'}, $ind, 46;
341 334         1266 my ($achSampleName,$dwStart,$dwEnd,$dwStartloop,$dwEndloop,
342             $dwSampleRate,$byOriginalKey,$chCorrection,$wSampleLink,$sfSampleType)
343             = unpack 'A20LLLLLCcSS', $shdr_rec;
344 334         873 $achSampleName =~ s/\0.*$//s;
345             # 1.02 detect duplicate names and rename as necessary (7.10)
346 334         354 my $orig = $achSampleName;
347 334         336 my $x = 2; while ($sample_names_seen{$achSampleName}) {
  334         730  
348 0         0 $achSampleName = $orig."_$x"; $x += 1;
  0         0  
349             }
350 334         649 $sample_names_seen{$achSampleName} = 1;
351             # extract the sample from $smpl_data
352 334         387 my $smpl_length = $dwEnd - $dwStart; # could test
353 334         9663 my $this_sample = substr($smpl_data, # 16 bits is 2 bytes
354             $dwStart+$dwStart, $smpl_length+$smpl_length);
355 334 100       654 if ($achSampleName ne 'EOS') {
356 332         2170 push @shdr_list, {
357             achSampleName => $achSampleName,
358             dwStart => 0,
359             dwEnd => $dwEnd-$dwStart,
360             dwStartloop => $dwStartloop-$dwStart,
361             dwEndloop => $dwEndloop-$dwStart,
362             dwSampleRate => $dwSampleRate,
363             byOriginalKey => $byOriginalKey,
364             chCorrection => $chCorrection,
365             wSampleLink => $wSampleLink,
366             sfSampleType => $sfSampleType,
367             sampledata => $this_sample,
368             };
369             }
370 334         847 $ind += 46;
371             }
372              
373             # http://www.pjb.com.au/midi/sfspec21.html#7.9
374             # http://www.pjb.com.au/midi/sfspec21.html#8.1.2
375             # http://www.pjb.com.au/midi/sfspec21.html#8.1.3
376 2 50       12 if (! $pdta{'igen'}) { warn "missing igen sub-chunk\n"; return undef; }
  0         0  
  0         0  
377 2 50       3 $len = length $pdta{'igen'}; if ($len % 4) {
  2         13  
378 0         0 warn "igen sub-chunk not a multiple of 4 bytes\n"; return undef;
  0         0  
379             }
380 2         4 $ind = 0;
381 2         5 my @igen_list = ();
382 2         7 while ($ind < $len) {
383 8186         10973 my $igen_rec = substr $pdta{'igen'}, $ind, 4;
384 8186         11458 my ($sfGenOper,$dummy) = unpack 'SS', $igen_rec;
385 8186         9377 my $type = $GenAmountType[$sfGenOper];
386 8186 50       14145 if (! defined $type) {
387 0         0 warn "sfGenOper=$sfGenOper out of range\n"; return;
  0         0  
388             }
389 8186 50       20881 if ($sfGenOper == 41) { # extract the instrument ILLEGAL HERE
    100          
    50          
    100          
390 0         0 warn "instruments are not allowed in instrument zones\n"; return;
  0         0  
391 0         0 my ($dummy,$shAmount) = unpack "SS", $igen_rec;
392 0         0 push @igen_list, {
393             sfGenOper=>$sfGenOper,
394             shAmount =>$inst_list[$shAmount]{'achInstName'}
395             };
396             } elsif ($sfGenOper == 53) { # extract the sample
397 792         1128 my ($dummy,$shAmount) = unpack "SS", $igen_rec;
398 792         2818 push @igen_list, {
399             sfGenOper=>$sfGenOper,
400             shAmount =>$shdr_list[$shAmount]{'achSampleName'}
401             };
402             } elsif ($type eq 'x') {
403             # unused; ignore
404             } elsif ($type eq 'C2') {
405 772         1457 my ($dummy,$min,$max) = unpack "S$type", $igen_rec;
406 772         2537 push @igen_list, { sfGenOper=>$sfGenOper, shAmount=>[$min,$max] };
407             } else {
408 6622         15493 my ($dummy,$shAmount) = unpack "S$type", $igen_rec;
409 6622         17171 push @igen_list, { sfGenOper=>$sfGenOper, shAmount=>$shAmount, };
410             }
411 8186         16585 $ind += 4;
412             }
413              
414             # http://www.pjb.com.au/midi/sfspec21.html#7.5
415             # http://www.pjb.com.au/midi/sfspec21.html#8.1.2
416             # http://www.pjb.com.au/midi/sfspec21.html#8.1.3
417 2 50       11 if (! $pdta{'pgen'}) { warn "missing pgen sub-chunk\n"; return undef; }
  0         0  
  0         0  
418 2 50       8 $len = length $pdta{'pgen'}; if ($len % 4) {
  2         9  
419 0         0 warn "pgen sub-chunk not a multiple of 4 bytes\n"; return undef;
  0         0  
420             }
421 2         3 $ind = 0;
422 2         5 my @pgen_list = ();
423 2         6 while ($ind < $len) {
424 7416         11304 my $pgen_rec = substr $pdta{'pgen'}, $ind, 4;
425 7416         11644 my ($sfGenOper,$dummy) = unpack 'SS', $pgen_rec;
426 7416         9608 my $type = $GenAmountType[$sfGenOper];
427 7416 50       13695 if (! defined $type) {
428 0         0 warn "sfGenOper=$sfGenOper out of range\n"; return;
  0         0  
429             }
430 7416 100       21735 if ($OnlyValidInInstr{$sfGenOper}) {
    100          
    50          
    100          
431             #warn "sfGenOper=$sfGenOper ($GeneratorOperators[$sfGenOper]) "
432             # . "invalid in presets; ignoring\n";
433             # invalid in presets; ignore! see sfspec21.html#8.5
434             } elsif ($sfGenOper == 41) { # extract the instrument
435 1550         2419 my ($dummy,$shAmount) = unpack "SS", $pgen_rec;
436 1550         6216 push @pgen_list, {
437             sfGenOper=>$sfGenOper,
438             shAmount =>$inst_list[$shAmount]{'achInstName'}
439             };
440             } elsif ($type eq 'x') {
441 0         0 warn "sfGenOper=$sfGenOper unused; ignoring\n";
442             } elsif ($type eq 'C2') {
443 1010         2159 my ($dummy,$min,$max) = unpack "S$type", $pgen_rec;
444 1010         4300 push @pgen_list, { sfGenOper=>$sfGenOper, shAmount=>[$min,$max] };
445             } else {
446 4854         9547 my ($dummy,$shAmount) = unpack "S$type", $pgen_rec;
447 4854         16019 push @pgen_list, { sfGenOper=>$sfGenOper, shAmount=>$shAmount, };
448             }
449 7416         15604 $ind += 4;
450             }
451             # go though @pbag_list extracting each hash of modulators and generators
452 2         6 my $i = 0; while ($i < $#pbag_list) {
  2         8  
453 1942         2829 my $from = $pbag_list[$i]{'wGenNdx'};
454 1942         2953 my $to = $pbag_list[$i+1]{'wGenNdx'};
455             # should check monotonicity and in-rangeness
456 1942         2450 my %gens = (); my $j = $from;
  1942         1868  
457 1942         3759 while ($j < $to) {
458 7414 50       17569 if (defined $pgen_list[$j]{'shAmount'}) {
459 7414         17921 $gens{$GeneratorOperators[$pgen_list[$j]{'sfGenOper'}]}
460             = $pgen_list[$j]{'shAmount'};
461             }
462 7414         14677 $j += 1;
463             }
464 1942         3690 $pbag_list[$i]{'generators'} = \%gens;
465 1942         3014 delete $pbag_list[$i]{'wGenNdx'};
466              
467             # should check monotonicity and in-rangeness
468 1942         2369 $from = $pbag_list[$i]{'wModNdx'};
469 1942         2612 $to = $pbag_list[$i+1]{'wModNdx'};
470 1942         2472 my @mods = (); $j = $from;
  1942         1998  
471 1942         4745 while ($j < $to) { push @mods, $pmod_list[$j]; $j += 1; }
  0         0  
  0         0  
472 1942         3288 $pbag_list[$i]{'modulators'} = \@mods;
473 1942         2708 delete $pbag_list[$i]{'wModNdx'};
474 1942         5488 $i += 1;
475             }
476             # now go though @phdr_list extracting each preset's lists of pbags
477 2         5 $i = 0; while ($i < $#phdr_list) {
  2         9  
478 408         567 my $from = $phdr_list[$i]{'wPresetBagNdx'};
479 408         722 my $to = $phdr_list[$i+1]{'wPresetBagNdx'};
480             # should check monotonicity and in-rangeness
481 408         489 my @pbags = (); my $j = $from;
  408         410  
482 408         744 while ($j < $to) { push @pbags, $pbag_list[$j]; $j += 1; }
  1942         2680  
  1942         3687  
483 408         677 $phdr_list[$i]{'pbags'} = \@pbags;
484 408         627 delete $phdr_list[$i]{'wPresetBagNdx'};
485 408         938 $i += 1;
486             }
487             # go though @ibag_list extracting each list of modulators and generators
488 2         6 $i = 0; while ($i < $#ibag_list) {
  2         7  
489 956         1326 my $from = $ibag_list[$i]{'wInstGenNdx'};
490 956         1455 my $to = $ibag_list[$i+1]{'wInstGenNdx'};
491 956         1194 my %gens = (); my $j = $from;
  956         1203  
492 956         1675 while ($j < $to) {
493 8184         19653 $gens{$GeneratorOperators[$igen_list[$j]{'sfGenOper'}]}
494             = $igen_list[$j]{'shAmount'};
495 8184         14919 $j += 1;
496             }
497 956         1868 $ibag_list[$i]{'generators'} = \%gens;
498 956         1613 delete $ibag_list[$i]{'wInstGenNdx'};
499             # should check monotonicity and in-rangeness
500 956         1244 $from = $ibag_list[$i]{'wInstModNdx'};
501 956         1232 $to = $ibag_list[$i+1]{'wInstModNdx'};
502 956         1177 my @mods = (); $j = $from;
  956         955  
503 956         2137 while ($j < $to) { push @mods, $imod_list[$j]; $j += 1; }
  66         140  
  66         137  
504 956         1563 $ibag_list[$i]{'modulators'} = \@mods;
505 956         1532 delete $ibag_list[$i]{'wInstModNdx'};
506 956         2458 $i += 1;
507             }
508             # now go though @inst_list extracting each preset's lists of bags
509 2         5 $i = 0; while ($i < $#inst_list) {
  2         11  
510 164         232 my $from = $inst_list[$i]{'wInstBagNdx'};
511 164         288 my $to = $inst_list[$i+1]{'wInstBagNdx'};
512             # should check monotonicity and in-rangeness
513 164         218 my @ibags = (); my $j = $from;
  164         173  
514 164         397 while ($j < $to) { push @ibags, $ibag_list[$j]; $j += 1; }
  956         1190  
  956         1747  
515 164         283 $inst_list[$i]{'ibags'} = \@ibags;
516 164         350 delete $inst_list[$i]{'wInstBagNdx'};
517 164         386 $i += 1;
518             }
519              
520             # pop EOP off the end of @phdr_list
521 2 50       14 if ($phdr_list[$#phdr_list]{'achPresetName'} eq 'EOP') { pop @phdr_list; }
  2         6  
522             # pop EOI off the end of @inst_list
523 2 50       14 if ($inst_list[$#inst_list]{'achInstName'} eq 'EOI') { pop @inst_list; }
  2         3  
524             # construct %inst_hash and %shdr_hash
525 2         7 my %inst_hash = (); foreach (@inst_list) {
  2         8  
526 164         299 $inst_hash{$_->{'achInstName'}} = $_;
527 164         211 delete $_->{'achInstName'};
528             }
529 2         8 my %shdr_hash = (); foreach (@shdr_list) {
  2         6  
530 332         747 $shdr_hash{$_->{'achSampleName'}} = $_;
531 332         549 delete $_->{'achSampleName'};
532             }
533             # $sf{'pbag'} = \@pbag_list;
534 2         6 $sf{'phdr'} = \@phdr_list;
535             # $sf{'pmod'} = \@pmod_list;
536             # $sf{'pgen'} = \@pgen_list;
537             # $sf{'ibag'} = \@ibag_list;
538 2         6 $sf{'inst'} = \%inst_hash;
539             # $sf{'imod'} = \@imod_list;
540             # $sf{'igen'} = \@igen_list;
541 2         4 $sf{'shdr'} = \%shdr_hash;
542              
543 2 50       25 if ($debug) {
544 0         0 $Data::Dumper::Indent=1; $Data::Dumper::Sortkeys=1; print Dumper(%sf);
  0         0  
  0         0  
545             }
546 2         6567 return %sf;
547             }
548              
549 1   0 1 0 7746 sub new_sf { my $inam = $_[$[] || 'Name of this SoundFont';
  1     0   888  
  1         4743  
  0         0  
550 0         0 my ($name,$passwd,$uid,$gid, $quota,$comment,$gcos,$dir,$shell,$expire)
551             = getpwuid($>);
552 0         0 $gcos =~ s/,+$//;
553 0         0 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime;
554 0         0 my $y = sprintf ("%4.4d", $year+1900);
555 0         0 my @abbr = qw( January February March April May June
556             July August September October November December );
557 0         0 my $now = "$abbr[$mon] $mday, $y"; # sfspec21.html#5.6
558             return (
559 0         0 ICMT => "insert comment here",
560             ICOP => "Copyright (c) $y $gcos; may be freely copied and modified",
561             ICRD => $now,
562             IENG => "$name, $gcos",
563             INAM => $inam,
564             IPRD => 'TiMidity',
565             ISFT => "MIDI-SoundFont $VERSION",
566             IVER => '',
567             ifil => '2.1',
568             inst => {
569             inst_0 => {
570             ibags => [
571             {
572             generators => {
573             keyRange => [ 0, 127 ],
574             pan => -190,
575             sampleID => 'smpl_0',
576             sampleModes => 1,
577             },
578             modulators => [],
579             },
580             ],
581             },
582             },
583             phdr => [
584             {
585             achPresetName => 'Instrument number 0',
586             pbags => [
587             {
588             generators => {
589             velRange => [ 0, 127 ],
590             instrument => 'inst_0',
591             },
592             modulators => [],
593             },
594             ],
595             wBank => 0,
596             wPreset => 0,
597             },
598             ],
599             shdr => {
600             inst_0 => {
601             byOriginalKey => 69,
602             chCorrection => 0,
603             dwEnd => 10000,
604             dwEndloop => 9990,
605             dwSampleRate => 44100,
606             dwStart => 0,
607             dwStartloop => 9890,
608             sampledata => ' ... ',
609             sfSampleType => 1,
610             wSampleLink => 0,
611             },
612             },
613             );
614             }
615              
616 0     0 0 0 sub sf2file { my $file = shift;
617             # write bytes to file, or filehandle (if GLOB), or - is stdout
618 0 0       0 if (! $file) { warn "sf2file: missing arguments.\n"; return; }
  0         0  
  0         0  
619 0         0 my $bytes = sf2bytes(@_);
620 0 0       0 if (ref($file) eq 'GLOB') {
    0          
621 0         0 } elsif ($file eq '-') { binmode STDOUT; print STDOUT $bytes;
  0         0  
622             } else {
623 0 0       0 if (! open(F, '>', $file)) { warn "can't open $file: $!\n"; return; }
  0         0  
  0         0  
624 0         0 binmode F; print F $bytes;
  0         0  
625 0         0 close F;
626             }
627             }
628              
629 1     1 0 634 sub sf2bytes{ my %sf = @_; # put it back together with RIFF
630             # must be careful not to modify %sf ! it's full of references !
631             # using sf_edit to change the banks, and saving to /tmp/k.sf2
632             # timidity -idvv -x 'soundfont /tmp/k.sf2' /tmp/t.mid | less
633              
634 1         11 my $info = new File::Format::RIFF::List('INFO');
635              
636 1         143 my $ifil_ck = new File::Format::RIFF::Chunk;
637 1         36 my ($wMajor,$wMinor) = split '\.', $sf{'ifil'};
638 1         7 $ifil_ck->id('ifil');
639 1         18 $ifil_ck->data(pack 'SS', $wMajor,$wMinor);
640 1         11 $info->push($ifil_ck);
641              
642 1         26 my $isng_ck = new File::Format::RIFF::Chunk;
643 1         26 $isng_ck->id('isng');
644 1   50     12 $isng_ck->data(zero_pad_to_even($sf{'isng'} || 'EMU8000'));
645 1         10 $info->push($isng_ck);
646              
647 1         19 my $INAM_ck = new File::Format::RIFF::Chunk;
648 1         26 $INAM_ck->id('INAM');
649 1   50     12 $INAM_ck->data(zero_pad_to_even($sf{'INAM'} || 'General MIDI'));
650 1         10 $info->push($INAM_ck);
651              
652 1 50       18 if ($sf{'irom'}) { # it's optional
653 0         0 my $irom_ck = new File::Format::RIFF::Chunk;
654 0         0 $irom_ck->id('irom');
655 0         0 $irom_ck->data(zero_pad_to_even($sf{'irom'}));
656 0         0 $info->push($irom_ck);
657             }
658              
659 1 50 33     6 if ($sf{'iver'} and $sf{'iver'}=~/^(\d+)\.(\d+)$/) { # it's optional
660 0         0 my $iver_ck = new File::Format::RIFF::Chunk;
661 0         0 $iver_ck->id('iver');
662 0         0 $iver_ck->data(pack 'SS', 0+$1,0+$2);
663 0         0 $info->push($iver_ck);
664             }
665              
666 1 50       5 if ($sf{'ICRD'}) { # it's optional
667 1         6 my $ICRD_ck = new File::Format::RIFF::Chunk;
668 1         29 $ICRD_ck->id('ICRD');
669 1         10 $ICRD_ck->data(zero_pad_to_even($sf{'ICRD'}));
670 1         10 $info->push($ICRD_ck);
671             }
672              
673 1 50       19 if ($sf{'IENG'}) { # it's optional
674 1         4 my $IENG_ck = new File::Format::RIFF::Chunk;
675 1         33 $IENG_ck->id('IENG');
676 1         9 $IENG_ck->data(zero_pad_to_even($sf{'IENG'}));
677 1         11 $info->push($IENG_ck);
678             }
679              
680 1 50       17 if ($sf{'IPRD'}) { # it's optional
681 1         5 my $IPRD_ck = new File::Format::RIFF::Chunk;
682 1         24 $IPRD_ck->id('IPRD');
683 1         9 $IPRD_ck->data(zero_pad_to_even($sf{'IPRD'}));
684 1         9 $info->push($IPRD_ck);
685             }
686              
687 1 50       19 if ($sf{'ICOP'}) { # it's optional
688 1         4 my $ICOP_ck = new File::Format::RIFF::Chunk;
689 1         22 $ICOP_ck->id('ICOP');
690 1         8 $ICOP_ck->data(zero_pad_to_even($sf{'ICOP'}));
691 1         12 $info->push($ICOP_ck);
692             }
693              
694 1 50       20 if ($sf{'ICMT'}) { # it's optional
695 1         5 my $ICMT_ck = new File::Format::RIFF::Chunk;
696 1         24 $ICMT_ck->id('ICMT');
697 1         8 $ICMT_ck->data(zero_pad_to_even($sf{'ICMT'}));
698 1         8 $info->push($ICMT_ck);
699             }
700              
701 1         17 my $ISFT_ck = new File::Format::RIFF::Chunk;
702 1         22 my $isft_data = "MIDI-SoundFont $VERSION";
703 1 50       3 if ($sf{'ISFT'}) { # it's optional, but we create it here anyway
704 1         3 my $s = $sf{'ISFT'};
705 1         9 $s =~ s/:.*$//s; # truncate to 20 less than max (max actually 256)
706 1         10 $isft_data = sprintf('%0.58s:%s', $s,$isft_data);
707             } else {
708 0         0 $isft_data = "$isft_data:";
709             }
710 1         5 $ISFT_ck->id('ISFT');
711 1         7 $ISFT_ck->data(zero_pad_to_even($isft_data));
712 1         9 $info->push($ISFT_ck);
713              
714             # go through @phdr_list, move the pbags out into a @pbag_list, note Ndx's
715             # http://www.pjb.com.au/midi/sfspec21.html#7.2
716 1         14 my @pbag_list = ();
717 1         3 my @phdr_data = ();
718             # could sort:
719             # @phdr_list = sort { (1000*$a->{'wBank'}+$a->{'wPreset'})
720             # <=> (1000*$b->{'wBank'}+$b->{'wPreset'})} @{$sf{'phdr'}};
721 1         2 foreach my $p_ref (@{$sf{'phdr'}}) {
  1         3  
722 204         248 my $wPresetBagNdx = scalar @pbag_list;
723 204         205 my @these_pbags = @{$p_ref->{'pbags'}};
  204         506  
724             # check that the instrument-generator is last in @these_pbags #7.3
725 204         332 push @pbag_list, @these_pbags;
726 204         769 push @phdr_data, pack('a20SSSLLL', $p_ref->{'achPresetName'},
727             $p_ref->{'wPreset'}, $p_ref->{'wBank'}, $wPresetBagNdx, 0,0,0);
728             }
729 1         6 push @phdr_data, pack('a20SSSLLL', 'EOP',0,0,(scalar @pbag_list),0,0,0);
730 1         7 my $phdr_ck = new File::Format::RIFF::Chunk;
731 1         30 $phdr_ck->id('phdr');
732 1         30 $phdr_ck->data(join('', @phdr_data));
733              
734             # go through @pbag_list moving the generators and modulators
735             # out into their own lists, and noting their Ndx's
736             # "the gen and mod lists are in the same order as the phdr and pbag lists"
737             # http://www.pjb.com.au/midi/sfspec21.html#7.3
738 1         8 my @pgen_list = (); # #7.3
739 1         3 my @pmod_list = (); # #7.4
740 1         3 my @pbag_data = (); # #7.5
741 1         3 foreach my $b_ref (@pbag_list) {
742 971         3995 my $wGenNdx = scalar @pgen_list;
743 971         1258 my $wModNdx = scalar @pmod_list;
744 971         3155 my @these_pgens = gen_hashref2list($b_ref->{'generators'}, 'p');
745 971         1240 my @these_pmods = @{$b_ref->{'modulators'}};
  971         2236  
746 971         2125 push @pgen_list, @these_pgens;
747 971         1153 push @pmod_list, @these_pmods;
748 971         3400 push @pbag_data, pack('SS', $wGenNdx, $wModNdx);
749             }
750 1         6 push @pbag_data, pack('SS', scalar @pgen_list, scalar @pmod_list);
751 1         13 my $pbag_ck = new File::Format::RIFF::Chunk;
752 1         54 $pbag_ck->id('pbag');
753 1         236 $pbag_ck->data(join('', @pbag_data));
754              
755             # go through pgen_list to put together @inst_list in the required order
756             # replace the instrument-names in pgen_list with their indexes in inst
757 1         10 my %inst_name2index = ();
758 1         3 my @inst_list = ();
759 1         2 my @pgen_data = ();
760 1         4 foreach my $g_ref (@pgen_list) {
761 3707 100       8310 if ($g_ref->[0] == 41) {
762 775         1050 my $inst_name = $g_ref->[1];
763 775 100       1502 if (defined $inst_name2index{$inst_name}) {
764 693         1177 $g_ref->[1] = $inst_name2index{$inst_name};
765             } else {
766 82         119 $g_ref->[1] = scalar @inst_list;
767 82         193 $inst_name2index{$inst_name} = $g_ref->[1];
768 82         160 push @inst_list, $inst_name;
769             }
770             }
771 3707         4854 my $type = $GenAmountType[$g_ref->[0]];
772 3707         4462 push @pgen_data, pack("S$type", @{$g_ref});
  3707         10246  
773             }
774 1         4 push @pgen_data, ("\0"x4);
775 1         13 my $pgen_ck = new File::Format::RIFF::Chunk;
776 1         53 $pgen_ck->id('pgen');
777 1         331 $pgen_ck->data(join('', @pgen_data));
778              
779             # pack the pmod chunk
780 1         11 my @pmod_data = ();
781 1         4 foreach my $m_ref (@pmod_list) { # #7.4
782             # All modulators within a zone should have a unique set
783             # of sfModSrcOper, sfModDestOper, and sfModSrcAmtOper.
784 0         0 push @pmod_data, pack( 'SSSSS', $m_ref->{'sfModSrcOper'},
785             $m_ref->{'sfModDestOper'}, $m_ref->{'modAmount'},
786             $m_ref->{'sfModAmtSrcOper'}, $m_ref->{'sfModTransOper'},
787             );
788             }
789 1         4 push @pmod_data, ("\0"x10);
790 1         6 my $pmod_ck = new File::Format::RIFF::Chunk;
791 1         29 $pmod_ck->id('pmod');
792 1         11 $pmod_ck->data(join('', @pmod_data));
793              
794             # go through @inst_list, move the ibags out into an @ibag_list, note Ndx's
795             # http://www.pjb.com.au/midi/sfspec21.html#7.6
796 1         8 my @ibag_list = ();
797 1         3 my @inst_data = ();
798 1         56 foreach my $inst_name (@inst_list) {
799 82         172 my $i_ref = $sf{'inst'}{$inst_name};
800 82         90 my $wInstBagNdx = scalar @ibag_list;
801 82         82 my @these_ibags = @{$i_ref->{'ibags'}};
  82         222  
802 82         155 push @ibag_list, @these_ibags;
803 82         242 push @inst_data, pack('a20S', $inst_name, $wInstBagNdx);
804             }
805 1         5 push @inst_data, pack('a20S', 'EOI',(scalar @ibag_list));
806 1         6 my $inst_ck = new File::Format::RIFF::Chunk;
807 1         27 $inst_ck->id('inst');
808 1         17 $inst_ck->data(join('', @inst_data));
809              
810             # go through @ibag_list moving the generators and modulators out into
811             # their own lists, and noting their Ndx's
812             # http://www.pjb.com.au/midi/sfspec21.html#7.7
813 1         7 my @igen_list = (); # #7.3
814 1         4 my @imod_list = (); # #7.4
815 1         2 my @ibag_data = (); # #7.5
816 1         110 foreach my $b_ref (@ibag_list) {
817 478         565 my $wGenNdx = scalar @igen_list;
818 478         509 my $wModNdx = scalar @imod_list;
819 478         1254 my @these_igens = gen_hashref2list($b_ref->{'generators'}, 'i');
820 478         675 my @these_imods = @{$b_ref->{'modulators'}};
  478         997  
821 478         1202 push @igen_list, @these_igens;
822 478         477 push @imod_list, @these_imods;
823 478         1618 push @ibag_data, pack('SS', $wGenNdx, $wModNdx);
824             }
825 1         6 push @ibag_data, pack('SS', scalar @igen_list, scalar @imod_list);
826 1         14 my $ibag_ck = new File::Format::RIFF::Chunk;
827 1         53 $ibag_ck->id('ibag');
828 1         151 $ibag_ck->data(join('', @ibag_data));
829              
830             # go through igen_list constructing the list of required sample-names,
831             # and replace the sample-names in igen_list with their index in that list
832 1         10 my @igen_data = ();
833 1         5 my @smpl_list = ();
834 1         4 my %smpl_name2index = ();
835 1         3 foreach my $g_ref (@igen_list) {
836 4092 100       9050 if ($g_ref->[0] == 53) {
837 396         482 my $samplename = $g_ref->[1];
838 396 100       760 if (defined $smpl_name2index{$samplename}) {
839 230         396 $g_ref->[1] = $smpl_name2index{$samplename};
840             } else {
841 166         212 $g_ref->[1] = scalar @smpl_list;
842 166         231 push @smpl_list, $samplename;
843 166         452 $smpl_name2index{$samplename} = $g_ref->[1];
844             }
845             }
846 4092         4967 my $type = $GenAmountType[$g_ref->[0]];
847 4092         4767 push @igen_data, pack("S$type", @{$g_ref});
  4092         10636  
848             }
849 1         5 push @igen_data, ("\0"x4);
850 1         14 my $igen_ck = new File::Format::RIFF::Chunk;
851 1         57 $igen_ck->id('igen');
852 1         250 $igen_ck->data(join('', @igen_data));
853              
854             # pack the imod chunk
855 1         8 my @imod_data = ();
856 1         5 foreach my $m_ref (@imod_list) { # #7.4
857             # All modulators within a zone should have a unique set
858             # of sfModSrcOper, sfModDestOper, and sfModSrcAmtOper.
859 33         117 push @imod_data, pack( 'SSSSS', $m_ref->{'sfModSrcOper'},
860             $m_ref->{'sfModDestOper'}, $m_ref->{'modAmount'},
861             $m_ref->{'sfModAmtSrcOper'}, $m_ref->{'sfModTransOper'},
862             );
863             }
864 1         5 push @imod_data, ("\0"x10);
865 1         6 my $imod_ck = new File::Format::RIFF::Chunk;
866 1         25 $imod_ck->id('imod');
867 1         11 $imod_ck->data(join('', @imod_data));
868              
869             # need to append in order of occurence in Presets and Instruments!!
870 1         7 my %shdr_hash = %{$sf{'shdr'}};
  1         167  
871 1         11 my $samples = ''; # append to $samples to be able to measure its length
872 1         2 my @shdr_data = ();
873 1         3 my $index = 0;
874 1         3 foreach my $samplename (@smpl_list) { # must append in order!!
875             # adjust dwStart dwEnd dwStartloop dwEndloop
876 166         381 my $shdr = $shdr_hash{$samplename};
877 166         331 my $smpl_length = $shdr->{'dwEnd'} - $shdr->{'dwStart'};
878 166         224 my $to_startloop = $shdr->{'dwStartloop'} - $shdr->{'dwStart'};
879 166         219 my $to_endloop = $shdr->{'dwEndloop'} - $shdr->{'dwStart'};
880 166         152 my $start;
881 166         188 $start = (length $samples)/2;
882 166         12961 $samples .= $shdr->{'sampledata'} . "\0"x92;
883 166         995 push @shdr_data, pack 'a20LLLLLCcSS', $samplename,
884             $start, $start+$smpl_length, $start+$to_startloop,
885             $start+$to_endloop, $shdr->{'dwSampleRate'},
886             $shdr->{'byOriginalKey'}, $shdr->{'chCorrection'},
887             $shdr->{'wSampleLink'}, $shdr->{'sfSampleType'},
888             $index += 1;
889             }
890 1         8 push @shdr_data, 'EOS'."\0"x43;
891 1         17 my $shdr_ck = new File::Format::RIFF::Chunk;
892 1         61 $shdr_ck->id('shdr');
893 1         27 $shdr_ck->data(join '', @shdr_data);
894 1         11 my $smpl_ck = new File::Format::RIFF::Chunk;
895 1         26 $smpl_ck->id('smpl');
896 1         10 $smpl_ck->data($samples);
897              
898 1         4499 my $sdta = new File::Format::RIFF::List('sdta');
899 1         124 $sdta->push($smpl_ck);
900 1         34 my $pdta = new File::Format::RIFF::List('pdta');
901 1         48 $pdta->push($phdr_ck);
902 1         18 $pdta->push($pbag_ck);
903 1         15 $pdta->push($pmod_ck);
904 1         18 $pdta->push($pgen_ck);
905 1         17 $pdta->push($inst_ck);
906 1         19 $pdta->push($ibag_ck);
907 1         19 $pdta->push($imod_ck);
908 1         18 $pdta->push($igen_ck);
909 1         16 $pdta->push($shdr_ck);
910              
911 1         23 my $riff = new File::Format::RIFF('sfbk'); # section 4.1
912 1         78 $riff->push($info);
913 1         21 $riff->push($sdta);
914 1         16 $riff->push($pdta);
915 1         14 my $bytes;
916 1 50       23 if (! open(P, '>', \$bytes)) {
917 0         0 warn "can't open in-memory filehandle: $!\n"; return;
  0         0  
918             }
919 1         5 binmode P;
920 1         12 $riff->write(\*P);
921             #warn "RIFF:\n"; $riff->dump();
922 1         7303 close P;
923 1         9924 return $bytes;
924             }
925 1450     1450 0 5666 sub gen_hashref2list { my ($gen_hashref, $is_p_or_i) = @_;
926 1450         2056 my @gen_list = (); # list of [sfGenOper,genAmount] listrefs, see 7.5 8.1.1
927 1450         1563 my $last_item;
928 1450         2090 while (my ($name, $shAmount) = each %{$gen_hashref}) {
  9268         39253  
929 7818         12630 my $sfGenOper = $GenOpname2num{$name};
930 7818         10241 my $type = $GenAmountType[$sfGenOper];
931 7818 50       16783 if (! defined $type) {
932 0         0 warn "unrecognised generator=$generator\n"; return;
  0         0  
933             }
934 7818 50 66     20338 if (($sfGenOper == 41) && ($is_p_or_i eq 'i')) {
935 0         0 warn "instruments are not allowed in instrument zones\n"; return;
  0         0  
936             }
937 7818 50 66     24739 if (($sfGenOper == 53) && ($is_p_or_i eq 'p')) {
938 0         0 warn "samples are not allowed in preset zones\n"; return;
  0         0  
939             }
940 7818 50 66     25884 if ($OnlyValidInInstr{$sfGenOper} && ($is_p_or_i eq 'p')) {
941 0         0 warn "sfGenOper=$sfGenOper not valid in preset zones\n"; next;
  0         0  
942             }
943             # we leave the instruments and samples referred to by name,
944             # to be replaced by an index when we know the list.
945 7818 50 100     37591 if ($type eq 'x') {
    100          
    100          
946             # unused; ignore
947             } elsif ($type eq 'C2') {
948 892 50       1740 if ($sfGenOper == 43) { # keyRange MUST be first
949 892         983 unshift @gen_list, [$sfGenOper, @{$shAmount}];
  892         4327  
950             } else {
951 0         0 push @gen_list, [$sfGenOper, @{$shAmount}];
  0         0  
952             }
953             } elsif (($sfGenOper == 41) || ($sfGenOper == 53)) { # MUST be last
954 1172         4249 $last_item = [$sfGenOper, $shAmount,];
955             } else {
956 5754         18863 push @gen_list, [$sfGenOper, $shAmount,];
957             }
958             }
959             # could check for global zone
960 1450 100       3476 if ($last_item) { push @gen_list, $last_item; }
  1172         1727  
961 1450         4854 return @gen_list;
962             }
963              
964 0     0 0 0 sub file2smpl { my ($filename, $original_key, $opt_ref) = @_;
965 0         0 my %opt = %{$opt_ref}; # noloop
  0         0  
966             # NB: from_key, to_key, from_vel, to_vel go in the ibags, not the shdrs
967             # we generate $SampleID = $achSampleName from the basename of $filename
968             # keeping an eye out for duplicates.
969             # $original_key can be fractional, e.g. 60.4
970             # looping seems mandatory in sf, so 'noloop' means:
971             # pushing >32 zero-samples onto the end, and looping them.
972             # invoke file2raw then raw2shdr
973 0         0 my ($sample_rate, $data) = file2raw($filename);
974 0 0       0 if (! defined $data) { return; }
  0         0  
975 1     1   12 use File::Basename;
  1         58  
  1         3932  
976 0         0 my $base = basename($filename);
977 0         0 $base =~s/\.\w\w\w\w?$//;
978 0         0 my $sample_name = $base;
979 0 0       0 if ($SampleName{$sample_name}) {
980 0         0 my $i = 0;
981 0         0 while (1) {
982 0         0 $i += 1;
983 0         0 $sample_name = $base.'_'.$i;
984 0 0       0 if (! $SampleName{$sample_name}) { last; }
  0         0  
985             }
986             }
987 0         0 my %shdr = ();
988 0 0       0 if ($opt{'noloop'}) {
989 0         0 my $len = length $data; # warn "len = $len\n";
990 0         0 my @zeros = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
991 0         0 @zeros = (@zeros,@zeros,$zeros); # 48 of them
992 0         0 %shdr = raw2shdr($sample_name, $original_key, $sample_rate,
993             $data . pack('s<*', @zeros));
994 0         0 $shdr{$sample_name}->{'dwStartloop'} = $len+4;
995 0         0 $shdr{$sample_name}->{'dwEndloop'} = $len+44;
996             } else {
997 0         0 %shdr = raw2shdr($sample_name, $original_key, $sample_rate, $data);
998 0         0 set_looppoints($shdr{$sample_name});
999             }
1000 0         0 return $shdr{$sample_name};
1001             }
1002 0     0 0 0 sub file2wavsample { my $filename = $_[$[]; # provide a 'noloop' option ?
1003             # need to know sample_name, byOriginalKey
1004             # invoke file2raw then raw2wavsample
1005             }
1006              
1007 0     0 0 0 sub file2raw { my $file = $_[$[];
1008 0 0       0 if (! -e $file) { warn "does not exist: $file\n"; return; }
  0         0  
  0         0  
1009 0 0       0 if (! -f $file) { warn "not a file: $file\n"; return; }
  0         0  
  0         0  
1010 0 0       0 if (! -r $file) { warn "not readable: $file\n"; return; }
  0         0  
  0         0  
1011             # Use soxi $file to ascertain the channels and sample-rate
1012             # (don't use File::Format::RIFF because that only handles .wav files)
1013             # It would be more elegant if there were a CPAN libsox module...
1014 0 0       0 if (! open(P, '-|', "soxi",$file)) {
1015 0         0 warn "can't run soxi '$file': $!\n"; return;
  0         0  
1016             }
1017 0         0 my $channels = 2; my $sample_rate = 44100;
  0         0  
1018 0         0 while (

) {

1019 0 0       0 if (/^Channels\s+:\s*(\d+)/) { $channels = 0+$1; next; }
  0         0  
  0         0  
1020 0 0       0 if (/^Sample Rate\s+:\s*(\d+)/) { $sample_rate = 0+$1; next; }
  0         0  
  0         0  
1021             }
1022 0         0 close P;
1023 0         0 my $tmp; my @data = ();
  0         0  
1024 0 0       0 if ($channels == 1) { # if already mono:
1025 0 0       0 if (! open(P, '-|', "sox '$file' -t raw -c 1 -b 16 -e signed -")) {
1026 0         0 warn "can't run sox '$file': $!\n"; return;
  0         0  
1027             }
1028             } else { # stereo to mono:
1029 0 0       0 if (!open(P,"sox '$file' -t raw -c 1 -b 16 -e signed - remix 1,2 |")) {
1030 0         0 warn "can't run sox '$file': $!\n"; return;
  0         0  
1031             }
1032             }
1033 0         0 while (read P, $tmp, 65536) { push @data, $tmp; }
  0         0  
1034 0         0 close P;
1035 0         0 return ($sample_rate, join('',@data));
1036             }
1037              
1038 0     0 0 0 sub raw2shdr { my ($achSampleName,$original_key,$dwSampleRate,$sampledata)=@_;
1039             # the 16-bit signed $sampledata might come from a file through file2raw(),
1040             # but might come from a user-supplied wavetable
1041             # Do we invoke set_looppoints($sampledata) from here ? noloop option...
1042 0         0 my $byOriginalKey = round($original_key);
1043 0         0 my $chCorrection = round(100 * ($original_key-$byOriginalKey));
1044 0         0 return ($achSampleName => { # 1.03
1045             byOriginalKey => $byOriginalKey,
1046             chCorrection => $chCorrection,
1047             dwEnd => length $sampledata,
1048             dwEndloop => $dwEndloop,
1049             dwSampleRate => $dwSampleRate,
1050             dwStart => 0,
1051             dwStartloop => $dwStartloop,
1052             sampledata => $sampledata,
1053             sfSampleType => 1,
1054             wSampleLink => 0
1055             });
1056             }
1057              
1058 0     0 0 0 sub raw2wavsample { my ($sample_name,$root_freq,$sample_rate,$data)=@_;
1059             # hopefully you used sox to convert data to .s16 raw signed 16bit mono
1060             # Need to be able to handle non-looped samples...
1061             # invoke raw2looppoints( ... , $data);
1062             return {
1063 0   0     0 balance => 7,
1064             data => $data,
1065             envelope_data => $DefaultEnvelopeData,
1066             high_freq => 10000000,
1067             loop_end => 87800,
1068             loop_start => 87400,
1069             low_freq => 20000,
1070             mode => 101,
1071             root_freq => 440000,
1072             sample_name => $sample_name||'NoName',
1073             sample_rate => 44100,
1074             scale_factor => 1024,
1075             scale_freq => 60,
1076             tremolo_depth => 0,
1077             tremolo_phase => 0,
1078             tremolo_sweep => 0,
1079             tune => 1,
1080             vibrato_ctl => 0,
1081             vibrato_depth => 0,
1082             vibrato_sweep => 0
1083             };
1084             }
1085 0     0 0 0 sub set_looppoints { my $sr = $_[$[]; # shdr_ref_or_wavsample_ref
1086             # pass it a shdr_ref or a wavsample_ref and it will fill in the
1087             # dwStartloop and dwEndloop, or loop_start and loop_end, and adjust
1088             # the sample values in the loop for smoothest possible looping
1089 0         0 my $samples_per_cycle;
1090 0 0       0 if (defined $sr->{'dwStart'}) { # it's a soundfont shdr
    0          
1091             # dwStart dwEnd byOriginalKey chCorrection dwSampleRate sampledata
1092 0         0 my @data = unpack 's<*', $sr->{'sampledata'};
1093 0         0 splice @data, $[, round(0.5 * ($[+$sr->{'dwStart'})); # bytes 2 samples
1094 0         0 $sr->{'dwEnd'} -= $sr->{'dwStart'};
1095 0         0 $sr->{'dwStart'} = $[;
1096 0         0 my $samples_per_cycle = $sr->{'dwSampleRate'}
1097             / midipitch2freq($sr->{'byOriginalKey'} + 0.01*$sr->{'chCorrection'});
1098 0         0 ($start, $end) = raw2looppoints($samples_per_cycle, \@data);
1099 0         0 $sr->{'dwStartloop'} = 2 * $start; # samples to bytes
1100 0         0 $sr->{'dwEndloop'} = 2 * $end; # samples to bytes
1101 0         0 $sr->{'sampledata'} = pack 's<*', @data;
1102 0         0 return $sr;
1103             } elsif (defined $sr->{'mode'}) { # it's a gravis wavsample
1104             # root_freq sample_rate data
1105 0         0 my @data = ();
1106 0 0 0     0 if ($MODES_UNSIGNED && $sr->{'data'}) { # could test 16BIT ?
1107 0         0 @data = unpack 's<*', @{$sr->{'data'}};
  0         0  
1108             } else {
1109 0         0 @data = unpack 'S<*', @{$sr->{'data'}};
  0         0  
1110             }
1111 0         0 my $samples_per_cycle = $sr->{'sample_rate'}
1112             / ($sr->{'sample_rate'} + $sr->{'tune'}); # check tune spec...
1113 0         0 ($start, $end) = raw2looppoints($samples_per_cycle, \@data);
1114 0         0 $sr->{'loop_start'} = $start; # samples
1115 0         0 $sr->{'loop_end'} = $end; # samples
1116 0         0 return $sr;
1117             } else {
1118 0         0 warn "set_looppoints: neither dwStart nor mode present\n";
1119 0         0 return undef;
1120             }
1121             }
1122              
1123 0     0 0 0 sub raw2looppoints { my ($samples_per_cycle, $data_ref) = @_;
1124             # Find the pair of zero-crossings exactly integer cycles apart for which
1125             # the neighboring samples v[x] are situated in the most similar-in-shape
1126             # curves, weighting nearby samples heavier of course; return indexes
1127             # in _samples_ not bytes. Assume 16-bit signed little-endian.
1128 0         0 my @data = @{$data_ref};
  0         0  
1129 0         0 my @up_crossings = ();
1130 0         0 my @down_crossings = ();
1131 0         0 my $i = $[+1; while ($i < $#data) {
  0         0  
1132 0 0 0     0 if ($data[$i]==0) {
    0 0        
    0          
1133 0 0 0     0 if ($data[$i-1]>0 and $data[$i+1]<0) { push @down_crossings, $i;
  0 0 0     0  
1134 0         0 } elsif ($data[$i-1]<0 and $data[$i+1]>0) { push @up_crossings, $i;
1135             }
1136 0         0 } elsif (($data[$i-1]>0) and ($data[$i]<0)) { push @down_crossings, $i;
1137 0         0 } elsif (($data[$i-1]<0) and ($data[$i]>0)) { push @up_crossings, $i;
1138             }
1139 0         0 $i += 1;
1140             }
1141 0         0 warn "there are ".scalar(@up_crossings)." up_crossings\n";
1142 0         0 warn "there are ".scalar(@down_crossings)." down_crossings\n";
1143 0         0 my $best_start = $[; my $best_end = $#data; my $best_goodness = 0;
  0         0  
  0         0  
1144             # too slow. must choose a loop_length .1
1145             # $samples_per_cycle, then look for the several pairs of crossings closest
1146             # to that distance apart, then choose the pair with the best goodness.
1147 0         0 foreach my $is (round(0.75*scalar(@up_crossings)) .. ($#up_crossings-1)) {
1148 0         0 foreach my $ie (($is+1) .. $#up_crossings) {
1149 0         0 my $goodness = goodness_of_fit($is,$ie,$samples_per_cycle,\@data);
1150 0 0       0 if ($goodness > $best_goodness) {
1151 0         0 $best_start=$is; $best_end=$ie; $best_goodness=$goodness;
  0         0  
  0         0  
1152             }
1153             }
1154             }
1155 0         0 foreach my $is (round(0.75*scalar(@down_crossings))..($#down_crossings-1)){
1156 0         0 foreach my $ie (($is+1) .. $#down_crossings) {
1157 0         0 my $goodness = goodness_of_fit($is,$ie,$samples_per_cycle,\@data);
1158 0 0       0 if ($goodness > $best_goodness) {
1159 0         0 $best_start=$is; $best_end=$ie; $best_goodness=$goodness;
  0         0  
  0         0  
1160             }
1161             }
1162             }
1163             #warn "best_start=$best_start best_end=$best_end goodness=$best_goodness\n";
1164 0         0 return ($best_start, $best_end);
1165             }
1166 0     0 0 0 sub smooth_a_loop { my ($start, $end, $samples_per_cycle, $data_ref) = @_;
1167             # 1) +a*t to line up the end to the curve of the beginning
1168             # then *b*t so that the graph of power/cycle is as horizontal as possible
1169             # 2) then make a graph of the power (x*x)/cycle
1170             # then *b*t so that the graph of power/cycle is as horizontal as possible
1171             }
1172 0     0 0 0 sub goodness_of_fit { my ($start, $end, $samples_per_cycle, $data_ref) = @_;
1173 0         0 my @data = @{$data_ref};
  0         0  
1174             # how close is $end-$start to a multiple of the cycle ?
1175 0         0 my $cycles = ($end-$start) / $samples_per_cycle;
1176 0         0 my $cycle_badness = 2.0 * abs($cycles - round($cycles)); # or square?
1177             #warn "cycle_badness=$cycle_badness\n"; # 0..1
1178             # how close are $end and $start to .8 and .95 of the data ?
1179 0         0 my $size = scalar @data;
1180 0         0 my $space_badness = abs(0.625*($start-(0.8*$size))/$size)
1181             + abs(0.475*($end-(0.95*$size))/$size);
1182             #warn "space_badness=$space_badness\n"; # 0..1
1183             # how well do the +/-1/4 of a cycle data points match ?
1184 0         0 my $match_badness = 0;
1185 0         0 my $quarter_cycle = round(0.25 * $samples_per_cycle);
1186 0         0 foreach my $i (0 .. $quarter_cycle) {
1187 0         0 my $weight = (1+$quarter_cycle-$i) / $quarter_cycle;
1188 0         0 $match_badness += $weight * abs($data[$end+$i] - $data[$start+$i]);
1189 0         0 $match_badness += $weight * abs($data[$end-$i] - $data[$start-$i]);
1190             }
1191 0         0 $match_badness = $match_badness / ($quarter_cycle*32000);
1192             #warn "match_badness=$match_badness\n"; # 0..1
1193 0         0 my $goodness = 1.0 - 0.15*$cycle_badness
1194             - 0.15*$space_badness - 0.7*$match_badness;
1195             #warn "goodness=$goodness\n"; # 0..1
1196             }
1197 0     0 0 0 sub midipitch2freq { my $pitch = $_[$[];
1198 0         0 return 440 * (1.0594630943348**($pitch-69));
1199             }
1200 0     0 0 0 sub round { my $x = $_[$[];
1201 0 0       0 if ($x > 0.0) { return int ($x + 0.5); }
  0         0  
1202 0 0       0 if ($x < 0.0) { return int ($x - 0.5); }
  0         0  
1203 0         0 return 0;
1204             }
1205              
1206             # --------------------------- gravis routines -----------------------
1207              
1208 2     2 0 22097 sub file2gravis { my $file = $_[0];
1209 2         8 my $file_type = filetype($file);
1210 2 100       12 if ($file_type eq 'pat') {
    50          
1211 1         5 my %pat = bytes2pat(file2bytes($file));
1212 1         18 $pat{'filename'} = $file;
1213 1 50       106 eval 'require File::Basename'; if ($@) {
  1         7  
1214 0         0 die "you'll need to install File::Basename from www.cpan.org\n";
1215             }
1216 1         70 my ($name,$path,$suffix) = File::Basename::fileparse($file,'.pat');
1217 1         8 return "$name$suffix", \%pat; # kv, will be assigned into a hash
1218             } elsif ($file_type eq 'zip') {
1219 1 50       73 eval 'require Archive::Zip'; if ($@) {
  1         93372  
1220 0         0 die "you'll need to install Archive::Zip from www.cpan.org\n";
1221             }
1222             # take it apart with Archive::Zip
1223 1         27 my $zip = Archive::Zip->new();
1224             # if ($zip->read($file) != Archive::Zip::AZ_OK) {
1225 1 50       65 if ($zip->read($file) != 0) {
1226 0         0 warn "can't read zipfile $file: read error\n"; return undef;
  0         0  
1227             }
1228 1         10302 my @memberNames = $zip->memberNames();
1229 1         294 my @gr; # key/value/key/value...
1230 1         4 foreach my $memberName ($zip->memberNames()) {
1231 61 100       577 if ($memberName !~ /\.pat$/) { next; }
  1         4  
1232 60         277 my $bytes = $zip->contents($memberName);
1233 60         90142 my %pat = bytes2pat($bytes);
1234 60         150 push @gr, $memberName;
1235 60         146 push @gr, \%pat;
1236             }
1237 1         373 return @gr; # kvkv, will be assigned into a hash
1238             }
1239             }
1240 0     0 0 0 sub bytes2gravis { my $bytes = $_[0];
1241             # Archive::Zip only does files, not even filehandles;
1242             # might have to use /tmp in order to handle urls etc.
1243 0 0       0 eval 'require File::Temp'; if ($@) {
  0         0  
1244 0         0 die "you'll need to install File::Temp from www.cpan.org\n";
1245             }
1246 0         0 my ($fh, $filename) = File::Temp::tempfile(SUFFIX => '.zip');
1247 0         0 return file2gravis($filename);
1248             }
1249 62     62 0 549 sub bytes2pat { my $bytes = $_[0];
1250 62         103 my $index = 0;
1251 62         149 my $header = substr $bytes, $index, 129; $index += 129;
  62         108  
1252 62         516 my ($ID,$manufacturer,$description, $num_instrs,$num_voices,$num_channels,
1253             $num_waveforms,$master_vol,$data_length, $reserved)
1254             = unpack ('A12 A10 A60 C C C S S L C36', $header);
1255 62         217 my %pat = ();
1256             # $pat{'manufacturer'} = $manufacturer; ID#000002 is mandatory for timidity
1257 62         155 $description =~ s/\0.*$//s;
1258 62         135 $description =~ tr /\cZ//d;
1259 62         157 $pat{'description'} = $description;
1260 62         133 $pat{'num_voices'} = $num_voices;
1261 62         121 $pat{'num_channels'} = $num_channels;
1262             # $pat{'num_instrs'} = $num_instrs;
1263 62         170 $pat{'instruments'} = [];
1264 62         176 foreach (1 .. $num_instrs) {
1265 62         123 my %instr = ();
1266 62         113 my $instr_header = substr $bytes, $index, 63; $index += 63;
  62         85  
1267 62         493 my ($instr_num, $instr_name, $instr_size, $num_layers, $reserved)
1268             = unpack('S A16 L C A40', $instr_header);
1269 62         168 $instr{'instr_num'} = $instr_num;
1270 62         96 $instr_name =~ s/\0.*$//s;
1271 62         87 $instr_name =~ tr /\cZ//d;
1272 62         122 $instr{'instr_name'} = $instr_name;
1273             # $instr{'instr_size'} = $instr_size; # 1.04
1274             # $instr{'num_layers'} = $num_layers;
1275 62         135 my @layers = ();
1276 62         112 foreach (1 .. $num_layers) {
1277 61         114 my $layer_header = substr $bytes, $index, 47; $index += 47;
  61         87  
1278 61         259 my ($previous, $id, $size, $num_wavsamples, $reserved)
1279             = unpack('C C L C A40', $layer_header);
1280 61         109 my @wavsamples = ();
1281 61         121 foreach (1 .. $num_wavsamples) {
1282 82         147 my $wav_header = substr $bytes, $index, 96; $index += 96;
  82         101  
1283             # tremolo: sweep 46, phase 43, depth 32
1284             # vibrato: sweep 1443, ctl 818, depth 32
1285 82         566 my ($sample_name, $fractions, $data_size, $loop_start,
1286             $loop_end, $sample_rate, $low_freq, $high_freq, $root_freq,
1287             $tune, $balance, $envelope_data,
1288             $tremolo_sweep, $tremolo_phase, $tremolo_depth,
1289             $vibrato_sweep, $vibrato_ctl, $vibrato_depth,
1290             $mode, $scale_freq, $scale_factor) =
1291             unpack('a7 C L L L S L L L S C a12 C6 C S S',$wav_header);
1292             # see doc/headers.c doc/gravis.c doc/timidity/instrum.c
1293             # 6 bytes envelope_velf and 6 bytes envelope_keyf, ?
1294             # (or Filter envelope rate and Filter envelope offset ?)
1295             # perhaps bytes: attack_vol attack_time decay_vol decay_time
1296             # release_vol final_vol; then attack_freq attack_time
1297             # decay_freq decay_time release_freq final_freq ?
1298             # See convert_envelope_rate() and convert_envelope_offset()
1299 82         471 $sample_name =~ s/\0.*$//s;
1300 82         854 my $data=substr $bytes,$index,$data_size; $index+=$data_size;
  82         114  
1301 82         1206 push @wavsamples, {
1302             sample_name => $sample_name,
1303             loop_start => $loop_start,
1304             loop_end => $loop_end,
1305             sample_rate => $sample_rate,
1306             low_freq => $low_freq,
1307             high_freq => $high_freq,
1308             root_freq => $root_freq,
1309             tune => $tune,
1310             balance => $balance,
1311             envelope_data => $envelope_data,
1312             tremolo_sweep => $tremolo_sweep,
1313             tremolo_phase => $tremolo_phase,
1314             tremolo_depth => $tremolo_depth,
1315             vibrato_sweep => $vibrato_sweep,
1316             vibrato_ctl => $vibrato_ctl,
1317             vibrato_depth => $vibrato_depth,
1318             mode => $mode,
1319             scale_freq => $scale_freq,
1320             scale_factor => $scale_factor,
1321             # data_size => $data_size ,
1322             data => $data,
1323             };
1324             }
1325 61         372 push @layers, {
1326             previous => $previous, id => $id,
1327             # num_wavsamples => $num_wavsamples,
1328             wavsamples => \@wavsamples,
1329             };
1330             }
1331 62         144 $instr{'layers'} = \@layers;
1332 62         95 push @{$pat{'instruments'}}, \%instr;
  62         254  
1333             }
1334 62         444 return %pat;
1335             }
1336              
1337 1     1 0 86 sub pat2bytes { my %pat = @_;
1338 1     1   8 use bytes;
  1         2  
  1         9  
1339 1         3 my @pat_data = ();
1340 1         10 my @instruments = @{$pat{'instruments'}};
  1         6  
1341 1         5 my $instr_num = 0;
1342 1         3 my $num_waveforms = 0;
1343 1         4 foreach my $instref (@instruments) {
1344 1         2 my @inst_data = ();
1345 1         3 my @layers = @{$instref->{'layers'}};
  1         4  
1346 1         2 my $previous = 0; my $id = 0;
  1         2  
1347 1         3 my @all_layer_data = ();
1348 1         2 foreach my $layerref (@layers) {
1349 1         4 my @this_layer_data = ();
1350 1         2 my @wavsamples = @{$layerref->{'wavsamples'}};
  1         5  
1351 1         2 foreach my $wref (@wavsamples) {
1352 3         6 my $wave_size = length($wref->{'data'}); # bytes? samples?
1353             # XXX to relate to timidity -idvv I should extract:
1354             # tremolo: sweep 46, phase 43, depth 32
1355             # vibrato: sweep 1443, ctl 818, depth 32
1356             # mode: 0x65
1357             # ? what's this ? volume comp: 1.024000
1358 3         42 push @this_layer_data,
1359             pack('a7 C L L L S L L L S C a12 C6 C S S C36',
1360             $wref->{'sample_name'}, 0, $wave_size,
1361             $wref->{'loop_start'}, $wref->{'loop_end'},
1362             $wref->{'sample_rate'}, $wref->{'low_freq'},
1363             $wref->{'high_freq'}, $wref->{'root_freq'}, $wref->{'tune'},
1364             $wref->{'balance'}, $wref->{'envelope_data'},
1365             $wref->{'tremolo_sweep'}, $wref->{'tremolo_phase'},
1366             $wref->{'tremolo_depth'},
1367             $wref->{'vibrato_sweep'}, $wref->{'vibrato_ctl'},
1368             $wref->{'vibrato_depth'},
1369             $wref->{'mode'},
1370             $wref->{'scale_freq'}, $wref->{'scale_factor'}, 0
1371             );
1372 3         6 push @this_layer_data, $wref->{'data'};
1373 3         7 $num_waveforms += 1;
1374             }
1375 1         20 unshift @this_layer_data, pack('C C L C A40', $previous, $id,
1376             length(join '',@this_layer_data), scalar @wavsamples, '');
1377 1         4 push @all_layer_data, @this_layer_data;
1378 1         1 $previous = $id; $id += 1;
  1         5  
1379             }
1380 1         7 my $instr_size = length(join '',@all_layer_data);
1381 1         2 my $num_layers = scalar @layers;
1382 1         5 push @inst_data, pack('S a16 L C A40', $instr_num,
1383             $instref->{'instr_name'}, $instr_size, $num_layers, '');
1384 1         4 push @inst_data, @all_layer_data;
1385 1         3 push @pat_data, @inst_data;
1386 1         4 $instr_num += 1;
1387             }
1388 1         13 unshift @pat_data, pack ('a12 a10 a60 C C C S S L C36', 'GF1PATCH110',
1389             'ID#000002', # manufacturer=ID#000002 is mandatory for timidity
1390             $pat{'description'}, (scalar @instruments),
1391             14, 1, $num_waveforms, 100, length(join '',@pat_data), 0);
1392 1         19 return join '', @pat_data;
1393             }
1394              
1395 0     0 0 0 sub gravis2file { my $file = shift;
1396 0 0       0 if (! $file) { warn "gravis2file: missing arguments.\n"; return 0; }
  0         0  
  0         0  
1397 0         0 my %gravis = @_;
1398 0 0       0 if (! %gravis) { warn "gravis2file: missing 2nd argument.\n"; return 0; }
  0         0  
  0         0  
1399             # write bytes to file, or filehandle (if GLOB), or - is stdout
1400 0 0       0 eval 'require Archive::Zip'; if ($@) {
  0         0  
1401 0         0 die "you'll need to install Archive::Zip from www.cpan.org\n";
1402             }
1403 0         0 my @pat_names = sort keys %gravis;
1404 0         0 my $n_pat_names = scalar @pat_names;
1405 0 0 0     0 if (($n_pat_names != 1) && ($file =~ /\.pat/)) {
1406 0         0 warn "can't store $n_pat_names patches in one .pat file\n"; return 0;
  0         0  
1407             }
1408 0 0       0 if ($file =~ /\.pat/) {
    0          
1409 0         0 my $bytes = pat2bytes(%{$gravis{$pat_names[0]}});
  0         0  
1410 0 0       0 if (! $bytes) { return 0; } # pat2bytes has already warned
  0         0  
1411 0 0       0 if (! open(F, '>', $file)) { warn "can't open $file:$!\n"; return 0; }
  0         0  
  0         0  
1412 0         0 print F $bytes; close F;
  0         0  
1413             } elsif ($file =~ /\.zip/) { # 1.01
1414 0 0       0 eval 'require Archive::Zip'; if ($@) {
  0         0  
1415 0         0 die "you'll need to install Archive::Zip from www.cpan.org\n";
1416             }
1417 0         0 my $zip = Archive::Zip->new();
1418 0         0 foreach my $pat_name (@pat_names) {
1419 0         0 my $bytes = pat2bytes(%{$gravis{$pat_name}});
  0         0  
1420 0 0       0 if (0 == length $bytes) { warn "gravis{$pat_name} was empty\n"; }
  0         0  
1421 0 0       0 if ($bytes) { my $member = $zip->addString($bytes,$pat_name); }
  0         0  
1422             }
1423 0 0       0 if ($zip->overwriteAs($file) != 0) {
1424 0         0 warn "can't write zipfile $file: write error\n"; return 0;
  0         0  
1425             }
1426             } else {
1427 0         0 warn "it has to be either a .pat or a .zip file\n"; return 0;
  0         0  
1428             }
1429 0         0 return 1;
1430             }
1431              
1432             sub new_pat {
1433             # See doc/timidity/instrum.[ch]
1434             # MODES_16BIT 1 MODES_UNSIGNED 2 MODES_LOOPING 4 MODES_PINGPONG 8
1435             # MODES_REVERSE 16 MODES_SUSTAIN 32 MODES_ENVELOPE 64 MODES_CLAMPED 128
1436             return (
1437 0     0 0 0 description => 'description of patch',
1438             filename => 'filename of patch',
1439             num_channels => 0,
1440             num_voices => 14,
1441             instruments => [
1442             {
1443             instr_name => 'instrument name',
1444             instr_num => 'instrument number',
1445             layers => [
1446             {
1447             id => 0,
1448             previous => 0,
1449             wavsamples => [
1450             {
1451             balance => 7,
1452             data => ' ... ',
1453             envelope_data => $DefaultEnvelopeData,
1454             high_freq => 10000000,
1455             loop_end => 266282,
1456             loop_start => 149902,
1457             low_freq => 20000,
1458             mode => 1+4+32+64,
1459             root_freq => 261625,
1460             sample_name => 'name of sample',
1461             sample_rate => 44100,
1462             scale_factor => 1024,
1463             scale_freq => 69,
1464             tremolo_depth => 0,
1465             tremolo_phase => 0,
1466             tremolo_sweep => 0,
1467             vibrato_depth => 0,
1468             vibrato_ctl => 0,
1469             vibrato_sweep => 0,
1470             tune => 1,
1471             }
1472             ]
1473             },
1474             ],
1475             },
1476             ],
1477             );
1478             }
1479              
1480 3     3 0 2168 sub timidity_cfg { my $file = shift; my %sf_or_gr = @_;
  3         45  
1481 3 50       273 eval 'require File::Basename'; if ($@) {
  3         13  
1482 0         0 die "you'll need to install File::Basename from www.cpan.org\n";
1483             }
1484 3         9 my @cfg = ('# See man timidity.cfg ...'); # array of lines
1485 3 100       11 if ($sf_or_gr{'ifil'}) { # it's a sf
1486 1         67 my ($name,$path,$suffix) = File::Basename::fileparse($file,'.sf2');
1487 1         3 my $current_bank = -1;
1488 1         3 push @cfg, "dir $path";
1489 376         594 my @phdr_list = sort { (1000*$a->{'wBank'}+$a->{'wPreset'})
  1         16  
1490 1         2 <=> (1000*$b->{'wBank'}+$b->{'wPreset'})} @{$sf_or_gr{'phdr'}};
1491 1         5 foreach my $a (@phdr_list) {
1492 204         246 my $patch = $a->{'wPreset'};
1493 204         209 my $bank = $a->{'wBank'};
1494 204         253 my $pname = $a->{'achPresetName'};
1495 204 100       389 if ($bank != $current_bank) {
1496 2         9 push @cfg, "bank $bank # $bank,0 cc0=$bank cc32=0";
1497 2         4 $current_bank = $bank;
1498             }
1499 204         594 push @cfg, "$patch %font $name.sf2 $bank $patch # $pname";
1500             }
1501             } else { # it's a gr
1502 2         47 my @patnames = sort keys %sf_or_gr;
1503 2         7 my $bank = 0;
1504 2 100       9 if (1 == scalar @patnames) {
1505 1         5 push @cfg, "bank $bank # $bank,0 cc0=$bank cc32=0";
1506 1         5 push @cfg, "0 /home/gravis/$patnames[0]";
1507             } else {
1508 1         5 push @cfg, "dir $file#";
1509 1         5 push @cfg, "bank $bank # $bank,0 cc0=$bank cc32=0";
1510 1         10 my @barenames = @patnames;
1511 1         3 foreach (@barenames) { s/\.pat$//; }
  60         190  
1512 1         5 my %unpaired_barename = map {$_,1} (@barenames);
  60         150  
1513 1 50       74 eval 'require String::Approx'; if ($@) {
  1         4943  
1514 0         0 warn "for more appropriate patch numbers "
1515             . "you need the String::Approx module\n";
1516             } else {
1517 1 50       108 eval 'require MIDI'; if ($@) {
  1         22850  
1518 0         0 warn "you need to install the MIDI-Perl module\n";
1519             } else {
1520 1         4 push @cfg, '# the bank 0 patches have been assigned by '
1521             . 'approximate string matching';
1522 1         3 push @cfg, '# to the general-midi patches; '
1523             . "you'll probably want to edit them...";
1524             # for each patname note the distance to each gmname
1525             # then as long as their are patnames which are the closest
1526             # of more than one gmname, the patname chooses the closest
1527             # and the other(s) forget their first pref, and next;
1528             # when there are no further contested patchnames every
1529             # remaining first choice is fulfilled.
1530             # any remaining patchnames are presented alphabetically.
1531 1         5 my @suitors = ();
1532             # $suitors[$gm_pnum] = [$barename=>$distance, ...]
1533 1         3 my %gm_pn2barename = ();
1534 1         6 foreach my $gm_pnum (0..127) {
1535 128         390 my $gm_patch = $MIDI::number2patch{$gm_pnum};
1536 128         185 my %dh; @dh{@barenames}= map { abs }
  128         635  
  7680         391583  
1537             String::Approx::adistr($gm_patch, @barenames);
1538 128         1458 my @da = sort { $dh{$a} <=> $dh{$b} } @barenames;
  32592         43889  
1539 128         216 my @da2dist = (); foreach (@da) {
  128         304  
1540 7680         12825 push @da2dist, $_, $dh{$_};
1541             }
1542 128         1484 $suitors[$gm_pnum] = \@da2dist;
1543             }
1544             # then patchnums first-pick-of-more-than-one-suitor, choose
1545             # closest until remaining first choices can be fulfilled
1546 1         5 my %unpaired_gm_pnum = map {$_,1} (0..127);
  128         270  
1547 1         15 while (1) {
1548             # go through the unpaired_gm_pnums seeking 1st
1549             # .pat, and note the .pat which occurs most often
1550 61         113 my %pat2gmtarget = (); # hash of [gm_pn,distance, ..]
1551 61         90 my $pat_with_most_gmtargets = undef;
1552 61         80 my $most_targets = 0;
1553             my @unpaired_gm_pnums
1554 61         1242 = sort {0+$a<=>0+$b} keys %unpaired_gm_pnum;
  32509         30746  
1555 61 50       479 if (! @unpaired_gm_pnums) { last; }
  0         0  
1556 61         1032 my @unpaired_barenames = sort keys %unpaired_barename;
1557 61 100       234 if (! @unpaired_barenames) { last; }
  1         8  
1558 60         109 foreach my $gm_pnum (@unpaired_gm_pnums) {
1559 5910 50       11973 if ($gm_pn2barename{$gm_pnum}) { next; }
  0         0  
1560 5910         6546 my @suitrs = @{$suitors[$gm_pnum]};
  5910         81255  
1561 5910         8912 $pat2gmtarget{$suitrs[0]} += 1;
1562 5910 100       37782 if ($pat2gmtarget{$suitrs[0]} > $most_targets) {
1563 845         912 $pat_with_most_gmtargets = $suitrs[0];
1564 845         4395 $most_targets = $pat2gmtarget{$suitrs[0]};
1565             }
1566             }
1567 60 50       163 if ($most_targets == 0) { last; }
  0         0  
1568 60 50       172 if ($most_targets > 1.5) { # barename's choice!
1569 60         120 my $closest_gm_pnum = undef;
1570 60         102 my $closest_gm_dist = 10**30;
1571 60         125 foreach my $gm_pnum (@unpaired_gm_pnums) {
1572 5910         5755 my @suitrs = @{$suitors[$gm_pnum]};
  5910         68621  
1573 5910 100       35686 if ($suitrs[0] eq $pat_with_most_gmtargets) {
1574 845 100       4099 if ($suitrs[1] < $closest_gm_dist) {
1575 194         266 $closest_gm_pnum = $gm_pnum;
1576 194         1123 $closest_gm_dist = $suitrs[1];
1577             }
1578             }
1579             }
1580             # if (! defined $closest_gm_pnum) { last; }
1581 60         168 $gm_pn2barename{$closest_gm_pnum}
1582             = $pat_with_most_gmtargets;
1583 60         191 delete $unpaired_gm_pnum{$closest_gm_pnum};
1584 60         132 delete $unpaired_barename{$pat_with_most_gmtargets};
1585             # chop $suitrs[0,1] off the lists of the losers
1586 60         98 foreach my $gm_pnum (@unpaired_gm_pnums) {
1587 5910         5952 while (@{$suitors[$gm_pnum]}) {
  10533         19462  
1588 10464         9786 my @suitrs = @{$suitors[$gm_pnum]};
  10464         100932  
1589 10464 100       21547 if ($unpaired_barename{$suitrs[0]}) {last;}
  5841         31651  
1590 4623         4096 shift @{$suitors[$gm_pnum]};
  4623         5824  
1591 4623         4520 shift @{$suitors[$gm_pnum]};
  4623         19188  
1592             }
1593             }
1594 60         1037 next;
1595             }
1596             # none left: fulfill all remaining first choices
1597 0         0 foreach my $gm_pnum (@unpaired_gm_pnums) {
1598 0         0 my @suitrs = @{$suitors[$gm_pnum]};
  0         0  
1599 0         0 $gm_pn2barename{$gm_pnum} = $suitrs[0];
1600 0         0 delete $unpaired_barename{$suitrs[0]};
1601 0         0 delete $unpaired_gm_pnum{$gm_pnum};
1602             }
1603             }
1604 1         16 foreach my $k (sort {$a<=>$b} keys %gm_pn2barename) {
  277         227  
1605 60         139 push @cfg, "$k $gm_pn2barename{$k}.pat";
1606             }
1607 1 50       510 if (%unpaired_barename) {
1608 0         0 $bank = 1;
1609 0         0 push @cfg, "bank $bank # $bank,0 cc0=$bank cc32=0";
1610             }
1611             }
1612             }
1613 1         9 my $patch = 0;
1614 1         39 foreach my $bn (sort keys %unpaired_barename) {
1615 0 0       0 if ($patch >= 127) {
1616 0         0 $bank += 1;
1617 0         0 push @cfg, "bank $bank # $bank,0 cc0=$bank cc32=0";
1618 0         0 $patch = 0;
1619             }
1620 0         0 push @cfg, "$patch $bn.pat";
1621 0         0 $patch += 1;
1622             }
1623             }
1624             }
1625 3         485 return join("\n",@cfg,"\n"); # could detect wantarray ...
1626             }
1627              
1628             # ----------------------- infrastructure -----------------------------
1629 8     8 0 29 sub zero_pad_to_even { my $str = $_[$[];
1630 8 100       21 if (length($str) % 2) { return "$str\0" } else { return "$str\0\0"; }
  4         23  
  4         31  
1631             }
1632 2     2 0 16 sub filetype { my $f = $_[$[];
1633 2 50       120 if (! open(F, $f)) { warn "can't open $f: $!\n"; return undef; }
  0         0  
  0         0  
1634 2         49 read F, my $s, 12;
1635 2         55 close F;
1636 2 50       9 if ($s =~ /^RIFF....sfbk/) { return 'sf2'; }
  0         0  
1637 2 100       11 if ($s =~ /^PK/) { return 'zip'; }
  1         5  
1638 1 50       7 if ($s =~ /^GF1PATCH/) { return 'pat'; }
  1         5  
1639 0 0         if ($f =~ /.sf2$/) { return 'sf2'; }
  0            
1640 0 0         if ($f =~ /.zip$/) { return 'zip'; }
  0            
1641 0 0         if ($f =~ /.pat$/) { return 'pat'; }
  0            
1642 0           return '';
1643             }
1644              
1645             1;
1646              
1647             __END__