deprecate SCViewHolder-layRight
[supercollider.git] / SCClassLibrary / Common / Files / SoundFile.sc
blob78683eb0e104a80c562611201a982d8de6d509a4
1 /*
2         Sound File Format strings:
3                 header formats:
4                         read/write formats:
5                                 "AIFF",         - Apple's AIFF
6                                 "WAV","RIFF"    - Microsoft .WAV
7                                 "SD2",  - Sound Designer 2
8                                 "Sun",  - NeXT/Sun
9                                 "IRCAM",        - old IRCAM format
10                                 "none"  - no header = raw data
11                         A huge number of other formats are supported read only.
13                 sample formats:
14                         "int8", "int16", "int24", "int32"
15                         "mulaw", "alaw",
16                         "float"
17                 not all header formats support all sample formats.
20 SoundFile {
21         classvar <openFiles;
23         var <>fileptr;
24         var <>headerFormat = "AIFF";
25         var <>sampleFormat = "float";
26         var <numFrames = 0;             // number of frames
27         var <>numChannels = 1;  // number of channels
28         var <>sampleRate = 44100.0;
29         var <> path;
32         *closeAll {
33                 if (openFiles.notNil, {
34                         openFiles.copy.do({ arg file; file.close; });
35                 });
36         }
37         isOpen {
38                 ^fileptr.notNil
39         }
41         *new { arg pathName;
42                 ^super.new.path_(pathName);
43         }
45         *openRead{ arg pathName;
46                 var file;
47                 file = SoundFile(pathName);
48                 if(file.openRead(pathName)){^file}{^nil}
49         }
51         *openWrite{ arg pathName;
52                 var file;
53                 file = SoundFile(pathName);
54                 if(file.openWrite(pathName)){ ^file}{^nil}
55         }
57         *use { arg path, function;
58                 var file = this.new, res;
59                 protect {
60                         file.openRead(path);
61                         res = function.value(file);
62                 } {
63                         file.close;
64                 }
65                 ^res
66         }
68         openRead{ arg pathName;
69                 path = pathName ? path;
70                 ^this.prOpenRead(path);
71         }
73         prOpenRead { arg pathName;
74                 // returns true if success, false if file not found or error reading.
75                 _SFOpenRead
76                 ^this.primitiveFailed;
77         }
79         readData { arg rawArray;
80                 // must have called openRead first!
81                 // returns true if success, false if file not found or error reading.
82                 _SFRead
83                 ^this.primitiveFailed;
84         }
86         readHeaderAsString {
87                 // must have called openRead first!
88                 //returns the whole header as String
89                 _SFHeaderInfoString
90                 ^this.primitiveFailed;
92         }
94         openWrite{ arg pathName;
95                 pathName = pathName ? path;
96                 ^this.prOpenWrite(pathName)
97         }
99         prOpenWrite { arg pathName;
100                 // write the header
101                 // format written is that indicated in headerFormat and sampleFormat.
102                 // return true if successful, false if not found or error writing.
103                 _SFOpenWrite
104                 ^this.primitiveFailed;
105         }
106         writeData { arg rawArray;
107                 // must have called openWrite first!
108                 // format written is that indicated in sampleFormat.
109                 // return true if successful, false if not found or error writing.
110                 _SFWrite
111                 ^this.primitiveFailed;
112         }
113         close {
114                 _SFClose
115                 ^this.primitiveFailed;
116         }
118         seek { arg offset = 0, origin = 0;
119                 // offset is in frames
120                 // origin is an integer, one of:
121                 // 0 - from beginning of file
122                 // 1 - from current position
123                 // 2 - from end of file
124                 _SFSeek
125                 ^this.primitiveFailed;
126         }
128         duration { ^numFrames/sampleRate }
132                 // normalizer utility
134         *normalize { |path, outPath, newHeaderFormat, newSampleFormat,
135                 startFrame = 0, numFrames, maxAmp = 1.0, linkChannels = true, chunkSize = 4194304,
136                 threaded = false|
138                 var     file, outFile,
139                         action = {
140                                 protect {
141                                         outFile = file.normalize(outPath, newHeaderFormat, newSampleFormat,
142                                                 startFrame, numFrames, maxAmp, linkChannels, chunkSize, threaded);
143                                 } { file.close };
144                                 file.close;
145                         };
147                 (file = SoundFile.openRead(path.standardizePath)).notNil.if({
148                                 // need to clean up in case of error
149                         if(threaded, {
150                                 Routine(action).play(AppClock)
151                         }, action);
152                         ^outFile
153                 }, {
154                         MethodError("Unable to read soundfile at: " ++ path, this).throw;
155                 });
156         }
158         normalize { |outPath, newHeaderFormat, newSampleFormat,
159                 startFrame = 0, numFrames, maxAmp = 1.0, linkChannels = true, chunkSize = 4194304,
160                 threaded = false|
162                 var     peak, outFile;
164                 outFile = SoundFile.new.headerFormat_(newHeaderFormat ?? { this.headerFormat })
165                         .sampleFormat_(newSampleFormat ?? { this.sampleFormat })
166                         .numChannels_(this.numChannels)
167                         .sampleRate_(this.sampleRate);
169                         // can we open soundfile for writing?
170                 outFile.openWrite(outPath.standardizePath).if({
171                         protect {
172                                 "Calculating maximum levels...".postln;
173                                 peak = this.channelPeaks(startFrame, numFrames, chunkSize, threaded);
174                                 Post << "Peak values per channel are: " << peak << "\n";
175                                 peak.includes(0.0).if({
176                                         MethodError("At least one of the soundfile channels is zero. Aborting.",
177                                                 this).throw;
178                                 });
179                                         // if all channels should be scaled by the same amount,
180                                         // choose the highest peak among all channels
181                                         // otherwise, retain the array of peaks
182                                 linkChannels.if({ peak = peak.maxItem });
183                                 "Writing normalized file...".postln;
184                                 this.scaleAndWrite(outFile, maxAmp / peak, startFrame, numFrames, chunkSize,
185                                         threaded);
186                                 "Done.".postln;
187                         } { outFile.close };
188                         outFile.close;
189                         ^outFile
190                 }, {
191                         MethodError("Unable to write soundfile at: " ++ outPath, this).throw;
192                 });
193         }
195         *groupNormalize { |paths, outDir, newHeaderFormat, newSampleFormat,
196                 maxAmp = 1.0, chunkSize = 4194304,
197                 threaded = true|
199                 var action;
201                 action = {
202                         var groupPeak = 0.0, files, outFiles;
204                         "Calculating maximum levels...".postln;
205                         paths.do({|path|
206                                 var     file, peak;
208                                 (file = SoundFile.openRead(path.standardizePath)).notNil.if({
209                                         "Checking levels for file %\n".postf(path.standardizePath);
210                                         files = files.add(file);
212                                                 peak = file.channelPeaks(0, nil, chunkSize, threaded);
213                                                 Post << "Peak values per channel are: " << peak << "\n";
214                                                 peak.includes(0.0).if({
215                                                         MethodError("At least one of the soundfile channels is zero. Aborting.",
216                                                                 this).throw;
217                                                 });
219                                         groupPeak = max(groupPeak, peak.maxItem);
221                                 }, {
222                                         MethodError("Unable to read soundfile at: " ++ path, this).throw;
223                                 });
224                         });
226                         "Overall peak level: %\n".postf(groupPeak);
227                         outDir = outDir.standardizePath.withTrailingSlash;
229                         files.do({|file|
231                                 var outPath, outFile;
233                                 outPath = outDir ++ file.path.basename;
235                                 outFile = SoundFile.new.headerFormat_(newHeaderFormat ?? { file.headerFormat })
236                                         .sampleFormat_(newSampleFormat ?? { file.sampleFormat })
237                                         .numChannels_(file.numChannels)
238                                         .sampleRate_(file.sampleRate);
240                                 outFile.openWrite(outPath).if({
241                                         protect {
242                                                 "Writing normalized file %\n".postf(outPath);
243                                                 file.scaleAndWrite(outFile, maxAmp / groupPeak, 0, nil, chunkSize,
244                                                         threaded);
245                                         } { outFile.close };
246                                         outFile.close;
247                                 }, {
248                                         MethodError("Unable to write soundfile at: " ++ outPath, this).throw;
249                                 });
251                         });
253                         "////// Group Normalize complete //////".postln;
255                 };
257                 if(threaded, {
258                         Routine(action).play(AppClock)
259                 }, action);
261         }
263         channelPeaks { |startFrame = 0, numFrames, chunkSize = 1048576, threaded = false|
264                 var rawData, peak, numChunks, chunksDone, test;
266                 peak = 0 ! numChannels;
267                 numFrames.isNil.if({ numFrames = this.numFrames });
268                 numFrames = numFrames * numChannels;
270                         // chunkSize must be a multiple of numChannels
271                 chunkSize = (chunkSize/numChannels).floor * numChannels;
273                 if(threaded) {
274                         numChunks = (numFrames / chunkSize).roundUp(1);
275                         chunksDone = 0;
276                 };
278                 this.seek(startFrame, 0);
280                 {       (numFrames > 0) and: {
281                                 rawData = FloatArray.newClear(min(numFrames, chunkSize));
282                                 this.readData(rawData);
283                                 rawData.size > 0
284                         }
285                 }.while({
286                         rawData.do({ |samp, i|
287                                 (samp.abs > peak[i % numChannels]).if({
288                                         peak[i % numChannels] = samp.abs
289                                 });
290                         });
291                         numFrames = numFrames - chunkSize;
292                         if(threaded) {
293                                 chunksDone = chunksDone + 1;
294                                 test = chunksDone / numChunks;
295                                 (((chunksDone-1) / numChunks) < test.round(0.02) and: { test >= test.round(0.02) }).if({
296                                         $..post;
297                                 });
298                                 0.0001.wait;
299                         };
300                 });
301                 if(threaded) { $\n.postln };
302                 ^peak
303         }
305         scaleAndWrite { |outFile, scale, startFrame, numFrames, chunkSize, threaded = false|
306                 var     rawData, numChunks, chunksDone, test;
308                 numFrames.isNil.if({ numFrames = this.numFrames });
309                 numFrames = numFrames * numChannels;
310                 scale = scale.asArray;
311 //              (scale.size == 0).if({ scale = [scale] });
313                         // chunkSize must be a multiple of numChannels
314                 chunkSize = (chunkSize/numChannels).floor * numChannels;
316                 if(threaded) {
317                         numChunks = (numFrames / chunkSize).roundUp(1);
318                         chunksDone = 0;
319                 };
321                 this.seek(startFrame, 0);
323                 {       (numFrames > 0) and: {
324                                 rawData = FloatArray.newClear(min(numFrames, chunkSize));
325                                 this.readData(rawData);
326                                 rawData.size > 0
327                         }
328                 }.while({
329                         rawData.do({ |samp, i|
330                                 rawData[i] = rawData[i] * scale.wrapAt(i)
331                         });
332                                 // write, and check whether successful
333                                 // throwing the error invokes error handling that closes the files
334                         (outFile.writeData(rawData) == false).if({
335                                 MethodError("SoundFile writeData failed.", this).throw
336                         });
338                         numFrames = numFrames - chunkSize;
339                         if(threaded) {
340                                 chunksDone = chunksDone + 1;
341                                 test = chunksDone / numChunks;
342                                 (((chunksDone-1) / numChunks) < test.round(0.02) and: { test >= test.round(0.02) }).if({
343                                         $..post;
344                                 });
345                                 0.0001.wait;
346                         };
347                 });
348                 if(threaded) { $\n.postln };
349                 ^outFile
350         }
352                 // diskIn synthdefs are now created on demand in SoundFile:cue
353 //      *initClass {
354 //              StartUp.add {
355 //                      (1..16).do { | i |
356 //                              SynthDef("diskIn" ++ i, { | out, amp = 1, bufnum, sustain, ar = 0, dr = 0.01 gate = 1 |
357 //                                      Out.ar(out, DiskIn.ar(i, bufnum)
358 //                                      * Linen.kr(gate, ar, 1, dr, 2)
359 //                                      * EnvGen.kr(Env.linen(ar, sustain - ar - dr max: 0 ,dr),1, doneAction: 2) * amp)
360 //                              }).store
361 //                      }
362 //              };
363 //      }
365         info { | path |
366                 var flag = this.openRead;
367                 if (flag) {
368                         this.close;
369                 } {
370                         ^nil
371                 }
372         }
374         *collect { | path = "sounds/*" |
375                 var paths, files;
376                 paths = path.pathMatch;
377                 files = paths.collect { | p | SoundFile(p).info };
378                 files = files.select(_.notNil);
379                 ^files;
380         }
382         *collectIntoBuffers { | path = "sounds/*", server |
383                 server = server ?? { Server.default };
384                 if (server.serverRunning) {
385                         ^SoundFile.collect(path)
386                                 .collect { |  sf |
387                                         Buffer(server, sf.numFrames, sf.numChannels).allocRead(sf.path)
388                                 }
389                 } {
390                         "the server must be running to collection soundfiles into buffers ".error
391                 }
392         }
395         asBuffer { |server|
396                 var buffer, rawData;
397                 server = server ? Server.default;
398                 if(server.serverRunning.not) { Error("SoundFile:asBuffer - Server not running.").throw };
399                 if(this.isOpen.not) { Error("SoundFile:asBuffer - SoundFile not open.").throw };
400                 if(server.isLocal) {
401                         buffer = Buffer.read(server, path)
402                 } {
403                         forkIfNeeded {
404                                 buffer = Buffer.alloc(server, numFrames, numChannels);
405                                 rawData = FloatArray.newClear(numFrames * numChannels);
406                                 this.readData(rawData);
407                                 server.sync;
408                                 buffer.sendCollection(rawData);
409                         }
410                 };
411                 ^buffer
412         }
414         cue { | ev, playNow = false |
415                 var server, packet, defname = "diskIn" ++ numChannels, condition;
416                 ev = ev ? ();
417                 if (this.numFrames == 0) { this.info };
418                 fork {
419                         ev.use {
420                                 server = ~server ?? { Server.default};
421                                 if(~instrument.isNil) {
422                                         SynthDef(defname, { | out, amp = 1, bufnum, sustain, ar = 0, dr = 0.01 gate = 1 |
423                                                 Out.ar(out, VDiskIn.ar(numChannels, bufnum, BufRateScale.kr(bufnum) )
424                                                 * Linen.kr(gate, ar, 1, dr, 2)
425                                                 * EnvGen.kr(Env.linen(ar, sustain - ar - dr max: 0 ,dr),1, doneAction: 2) * amp)
426                                         }).add;
427                                         ~instrument = defname;
428                                         condition = Condition.new;
429                                         server.sync(condition);
430                                 };
431                                 ev.synth;       // set up as a synth event (see Event)
432                                 ~bufnum =  server.bufferAllocator.alloc(1);
433                                 ~bufferSize = 0x10000;
434                                 ~firstFrame = ~firstFrame ? 0;
435                                 ~lastFrame = ~lastFrame ? numFrames;
436                                 ~sustain = (~lastFrame - ~firstFrame)/(sampleRate ?? {server.options.sampleRate ? 44100});
437                                 ~close = { | ev |
438                                                 server.bufferAllocator.free(ev[\bufnum]);
439                                                 server.sendBundle(server.latency, ["/b_close", ev[\bufnum]],
440                                                         ["/b_free", ev[\bufnum] ]  )
441                                 };
442                                 ~setwatchers = { |ev|
443                                         OSCpathResponder(server.addr, ["/n_end", ev[\id][0]],
444                                         { | time, resp, msg |
445                                                 server.sendBundle(server.latency, ["/b_close", ev[\bufnum]],
446                                                 ["/b_read", ev[\bufnum], path, ev[\firstFrame], ev[\bufferSize], 0, 1]);
447                                                 resp.remove;
448                                         }
449                                         ).add;
450                                 };
451                                 if (playNow) {
452                                         packet = server.makeBundle(false, {ev.play})[0];
453                                                 // makeBundle creates an array of messages
454                                                 // need one message, take the first
455                                 } {
456                                         packet = [];
457                                 };
458                                 server.sendBundle(server.latency,["/b_alloc", ~bufnum, ~bufferSize, numChannels,
459                                                         ["/b_read", ~bufnum, path, ~firstFrame, ~bufferSize, 0, 1, packet]
460                                                 ]);
461                         };
462                 };
463                 ^ev;
464         }
466         play { | ev, playNow = true |
467                 ^this.cue(ev, playNow)
468         }
470         asEvent { | type = \allocRead |
471                 if (type == \cue) {
472                         ^(      type:                   type,
473                                 path:                   path,
474                                 numFrames:              numFrames,
475                                 sampleRate:             sampleRate,
476                                 numChannels:            numChannels,
477                                 bufferSize:             0x10000,
478                                 firstFileFrame: 0,
479                                 firstBufferFrame:       0,
480                                 leaveOpen:              1
481                         )
482                 } {
483                         ^(      type:                   type,
484                                 path:                   path,
485                                 numFrames:              numFrames,
486                                 sampleRate:             sampleRate,
487                                 numChannels:            numChannels,
488                                 firstFileFrame: 0
489                         )
490                 }
492         }
494         toCSV { |outpath, headers, delim=",", append=false, func, action|
496                 var outfile, dataChunk;
498                 // Prepare input
499                 if(this.openRead(this.path).not){
500                         ^"SoundFile:toCSV could not open the sound file".error
501                 };
502                 dataChunk = FloatArray.newClear(this.numChannels * min(this.numFrames, 1024));
504                 // Prepare output
505                 if(outpath.isNil){      outpath = path.splitext.at(0) ++ ".csv" };
506                 outfile = File(outpath, if(append, "a", "w"));
507                 if(headers.notNil){
508                         if(headers.isString){
509                                 outfile.write(headers ++ Char.nl);
510                         }{
511                                 outfile.write(headers.join(delim) ++ Char.nl);
512                         }
513                 };
515                 // Now do it
516                 while{this.readData(dataChunk); dataChunk.size > 0}{
517                         dataChunk.clump(this.numChannels).do{|row|
518                                 outfile.write(if(func.isNil, {row}, {func.value(row)}).join(delim) ++ Char.nl)
519                         };
520                 };
521                 outfile.close;
522                 this.close;
524                 action.value(outpath);
525         }