2 Sound File Format strings:
6 "WAV","RIFF" - Microsoft .WAV
7 "SD2", - Sound Designer 2
9 "IRCAM", - old IRCAM format
10 "none" - no header = raw data
11 A huge number of other formats are supported read only.
14 "int8", "int16", "int24", "int32"
17 not all header formats support all sample formats.
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;
33 if (openFiles.notNil, {
34 openFiles.copy.do({ arg file; file.close; });
42 ^super.new.path_(pathName);
45 *openRead{ arg pathName;
47 file = SoundFile(pathName);
48 if(file.openRead(pathName)){^file}{^nil}
51 *openWrite{ arg pathName;
53 file = SoundFile(pathName);
54 if(file.openWrite(pathName)){ ^file}{^nil}
57 *use { arg path, function;
58 var file = this.new, res;
61 res = function.value(file);
68 openRead{ arg pathName;
69 path = pathName ? path;
70 ^this.prOpenRead(path);
73 prOpenRead { arg pathName;
74 // returns true if success, false if file not found or error reading.
76 ^this.primitiveFailed;
79 readData { arg rawArray;
80 // must have called openRead first!
81 // returns true if success, false if file not found or error reading.
83 ^this.primitiveFailed;
87 // must have called openRead first!
88 //returns the whole header as String
90 ^this.primitiveFailed;
94 openWrite{ arg pathName;
95 pathName = pathName ? path;
96 ^this.prOpenWrite(pathName)
99 prOpenWrite { arg pathName;
101 // format written is that indicated in headerFormat and sampleFormat.
102 // return true if successful, false if not found or error writing.
104 ^this.primitiveFailed;
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.
111 ^this.primitiveFailed;
115 ^this.primitiveFailed;
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
125 ^this.primitiveFailed;
128 duration { ^numFrames/sampleRate }
132 // normalizer utility
134 *normalize { |path, outPath, newHeaderFormat, newSampleFormat,
135 startFrame = 0, numFrames, maxAmp = 1.0, linkChannels = true, chunkSize = 4194304,
141 outFile = file.normalize(outPath, newHeaderFormat, newSampleFormat,
142 startFrame, numFrames, maxAmp, linkChannels, chunkSize, threaded);
147 (file = SoundFile.openRead(path.standardizePath)).notNil.if({
148 // need to clean up in case of error
150 Routine(action).play(AppClock)
154 MethodError("Unable to read soundfile at: " ++ path, this).throw;
158 normalize { |outPath, newHeaderFormat, newSampleFormat,
159 startFrame = 0, numFrames, maxAmp = 1.0, linkChannels = true, chunkSize = 4194304,
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({
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.",
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,
191 MethodError("Unable to write soundfile at: " ++ outPath, this).throw;
195 *groupNormalize { |paths, outDir, newHeaderFormat, newSampleFormat,
196 maxAmp = 1.0, chunkSize = 4194304,
202 var groupPeak = 0.0, files, outFiles;
204 "Calculating maximum levels...".postln;
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.",
219 groupPeak = max(groupPeak, peak.maxItem);
222 MethodError("Unable to read soundfile at: " ++ path, this).throw;
226 "Overall peak level: %\n".postf(groupPeak);
227 outDir = outDir.standardizePath.withTrailingSlash;
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({
242 "Writing normalized file %\n".postf(outPath);
243 file.scaleAndWrite(outFile, maxAmp / groupPeak, 0, nil, chunkSize,
248 MethodError("Unable to write soundfile at: " ++ outPath, this).throw;
253 "////// Group Normalize complete //////".postln;
258 Routine(action).play(AppClock)
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;
274 numChunks = (numFrames / chunkSize).roundUp(1);
278 this.seek(startFrame, 0);
280 { (numFrames > 0) and: {
281 rawData = FloatArray.newClear(min(numFrames, chunkSize));
282 this.readData(rawData);
286 rawData.do({ |samp, i|
287 (samp.abs > peak[i % numChannels]).if({
288 peak[i % numChannels] = samp.abs
291 numFrames = numFrames - chunkSize;
293 chunksDone = chunksDone + 1;
294 test = chunksDone / numChunks;
295 (((chunksDone-1) / numChunks) < test.round(0.02) and: { test >= test.round(0.02) }).if({
301 if(threaded) { $\n.postln };
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;
317 numChunks = (numFrames / chunkSize).roundUp(1);
321 this.seek(startFrame, 0);
323 { (numFrames > 0) and: {
324 rawData = FloatArray.newClear(min(numFrames, chunkSize));
325 this.readData(rawData);
329 rawData.do({ |samp, i|
330 rawData[i] = rawData[i] * scale.wrapAt(i)
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
338 numFrames = numFrames - chunkSize;
340 chunksDone = chunksDone + 1;
341 test = chunksDone / numChunks;
342 (((chunksDone-1) / numChunks) < test.round(0.02) and: { test >= test.round(0.02) }).if({
348 if(threaded) { $\n.postln };
352 // diskIn synthdefs are now created on demand in SoundFile:cue
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)
366 var flag = this.openRead;
374 *collect { | path = "sounds/*" |
376 paths = path.pathMatch;
377 files = paths.collect { | p | SoundFile(p).info };
378 files = files.select(_.notNil);
382 *collectIntoBuffers { | path = "sounds/*", server |
383 server = server ?? { Server.default };
384 if (server.serverRunning) {
385 ^SoundFile.collect(path)
387 Buffer(server, sf.numFrames, sf.numChannels)
389 .sampleRate_(sf.sampleRate);
392 "the server must be running to collect soundfiles into buffers".error
399 server = server ? Server.default;
400 if(server.serverRunning.not) { Error("SoundFile:asBuffer - Server not running.").throw };
401 if(this.isOpen.not) { Error("SoundFile:asBuffer - SoundFile not open.").throw };
403 buffer = Buffer.read(server, path)
406 buffer = Buffer.alloc(server, numFrames, numChannels);
407 rawData = FloatArray.newClear(numFrames * numChannels);
408 this.readData(rawData);
410 buffer.sendCollection(rawData, wait: -1);
416 cue { | ev, playNow = false |
417 var server, packet, defname = "diskIn" ++ numChannels, condition;
419 if (this.numFrames == 0) { this.info };
422 server = ~server ?? { Server.default};
423 if(~instrument.isNil) {
424 SynthDef(defname, { | out, amp = 1, bufnum, sustain, ar = 0, dr = 0.01 gate = 1 |
425 Out.ar(out, VDiskIn.ar(numChannels, bufnum, BufRateScale.kr(bufnum) )
426 * Linen.kr(gate, ar, 1, dr, 2)
427 * EnvGen.kr(Env.linen(ar, sustain - ar - dr max: 0 ,dr),1, doneAction: 2) * amp)
429 ~instrument = defname;
430 condition = Condition.new;
431 server.sync(condition);
433 ev.synth; // set up as a synth event (see Event)
434 ~bufnum = server.bufferAllocator.alloc(1);
435 ~bufferSize = 0x10000;
436 ~firstFrame = ~firstFrame ? 0;
437 ~lastFrame = ~lastFrame ? numFrames;
438 ~sustain = (~lastFrame - ~firstFrame)/(sampleRate ?? {server.options.sampleRate ? 44100});
440 server.bufferAllocator.free(ev[\bufnum]);
441 server.sendBundle(server.latency, ["/b_close", ev[\bufnum]],
442 ["/b_free", ev[\bufnum] ] )
444 ~setwatchers = { |ev|
445 OSCpathResponder(server.addr, ["/n_end", ev[\id][0]],
446 { | time, resp, msg |
447 server.sendBundle(server.latency, ["/b_close", ev[\bufnum]],
448 ["/b_read", ev[\bufnum], path, ev[\firstFrame], ev[\bufferSize], 0, 1]);
454 packet = server.makeBundle(false, {ev.play})[0];
455 // makeBundle creates an array of messages
456 // need one message, take the first
460 server.sendBundle(server.latency,["/b_alloc", ~bufnum, ~bufferSize, numChannels,
461 ["/b_read", ~bufnum, path, ~firstFrame, ~bufferSize, 0, 1, packet]
468 play { | ev, playNow = true |
469 ^this.cue(ev, playNow)
472 asEvent { | type = \allocRead |
476 numFrames: numFrames,
477 sampleRate: sampleRate,
478 numChannels: numChannels,
487 numFrames: numFrames,
488 sampleRate: sampleRate,
489 numChannels: numChannels,
496 toCSV { |outpath, headers, delim=",", append=false, func, action|
498 var outfile, dataChunk;
501 if(this.openRead(this.path).not){
502 ^"SoundFile:toCSV could not open the sound file".error
504 dataChunk = FloatArray.newClear(this.numChannels * min(this.numFrames, 1024));
507 if(outpath.isNil){ outpath = path.splitext.at(0) ++ ".csv" };
508 outfile = File(outpath, if(append, "a", "w"));
510 if(headers.isString){
511 outfile.write(headers ++ Char.nl);
513 outfile.write(headers.join(delim) ++ Char.nl);
518 while{this.readData(dataChunk); dataChunk.size > 0}{
519 dataChunk.clump(this.numChannels).do{|row|
520 outfile.write(if(func.isNil, {row}, {func.value(row)}).join(delim) ++ Char.nl)
526 action.value(outpath);