From 7dd3f075e8fe51355b6a817aeb4a726e5ab5ba07 Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Sat, 30 Jan 2021 13:48:06 -0600 Subject: [PATCH 1/9] Initial commit, still need to add/check on long sample support --- classes/DirtEvent.sc | 1 + classes/DirtSoundLibrary.sc | 9 ++++++- synths/core-modules.scd | 37 ++++++++++++++++++++--------- synths/core-synths.scd | 47 +++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/classes/DirtEvent.sc b/classes/DirtEvent.sc index a24d4873..7af9255b 100644 --- a/classes/DirtEvent.sc +++ b/classes/DirtEvent.sc @@ -100,6 +100,7 @@ DirtEvent { delta * ~legato.value } { unitDuration = unitDuration ? delta; + if (~timescale.notNil) {unitDuration = unitDuration * ~timescale }; loop !? { unitDuration = unitDuration * loop.abs }; } }; diff --git a/classes/DirtSoundLibrary.sc b/classes/DirtSoundLibrary.sc index c3cb0729..73763078 100644 --- a/classes/DirtSoundLibrary.sc +++ b/classes/DirtSoundLibrary.sc @@ -247,6 +247,7 @@ DirtSoundLibrary { ^( buffer: buffer.bufnum, instrument: this.instrumentForBuffer(buffer), + stretchInstrument: this.stretchInstrumentForBuffer(buffer), bufNumFrames: buffer.numFrames, bufNumChannels: buffer.numChannels, unitDuration: { buffer.duration * baseFreq / ~freq.value }, @@ -262,6 +263,11 @@ DirtSoundLibrary { ^format(synthName, buffer.numChannels, this.numChannels).asSymbol } + stretchInstrumentForBuffer { |buffer| + // may need to add support for long samples? + ^format("dirt_stretchsample_%_%", buffer.numChannels, this.numChannels).asSymbol + } + openFolder { |name, index = 0| var buf, list; list = buffers.at(name); @@ -281,7 +287,8 @@ DirtSoundLibrary { numChannels = n; bufferEvents = bufferEvents.collect { |list| list.do { |event| - event[\instrument] = this.instrumentForBuffer(event[\buffer]) + event[\instrument] = this.instrumentForBuffer(event[\buffer]); + event[\stretchInstrument] = this.stretchInstrumentForBuffer(event[\buffer]); } } } diff --git a/synths/core-modules.scd b/synths/core-modules.scd index 241320a6..5957d467 100644 --- a/synths/core-modules.scd +++ b/synths/core-modules.scd @@ -38,17 +38,32 @@ this may be refacored later. if(~diversion.value.isNil) { if(~buffer.notNil) { // argumets could be omitted using getMsgFunc, but for making it easier to understand, we write them out - dirtEvent.sendSynth(~instrument, [ - bufnum: ~buffer, - sustain: ~sustain, - speed: ~speed, - freq: ~freq, - endSpeed: ~endSpeed, - begin: ~begin, - end: ~end, - pan: ~pan, - out: ~out - ]) + if(~timescale.notNil) { + dirtEvent.sendSynth(~stretchInstrument, [ + bufnum: ~buffer, + sustain: ~sustain, + speed: ~speed, + freq: ~freq, + endSpeed: ~endSpeed, + begin: ~begin, + end: ~end, + pan: ~pan, + timescale: ~timescale, + out: ~out + ]) + } { + dirtEvent.sendSynth(~instrument, [ + bufnum: ~buffer, + sustain: ~sustain, + speed: ~speed, + freq: ~freq, + endSpeed: ~endSpeed, + begin: ~begin, + end: ~end, + pan: ~pan, + out: ~out + ]) + } } { if(~instrument.isNil) { "module 'sound': instrument not found: %".format(~s).postln diff --git a/synths/core-synths.scd b/synths/core-synths.scd index 8c617325..21044a13 100644 --- a/synths/core-synths.scd +++ b/synths/core-synths.scd @@ -98,6 +98,53 @@ live coding them requires that you have your SuperDirt instance in an environmen }, [\ir, \ir, \ir, \ir, \ir, \ir, \ir, \ir, \kr]).add; // pan can be modulated }; + // this is the time-stretching sample player, based on an overlap-add method + // TODO: long sample version + (1..SuperDirt.maxSampleNumChannels).do { |sampleNumChannels| + + var name = format("dirt_stretchsample_%_%", sampleNumChannels, numChannels); + + SynthDef(name, { |out, bufnum, sustain = 1, begin = 0, end = 1, speed = 1, endSpeed = 1, freq = 440, pan = 0, timescale = 1| + + var sound, rate, phase, sawrate, numFrames, index, windowIndex, window, timescaleStep, windowSize; + + // playback speed + rate = Line.kr(speed, endSpeed, sustain) * (freq / 60.midicps); + + // sample phase + // BufSampleRate adjusts the rate if the sound file doesn't have the same rate as the soundcard + //phase = Sweep.ar(1, rate * BufSampleRate.ir(bufnum)) + (BufFrames.ir(bufnum) * begin); + + numFrames = BufFrames.ir(bufnum); + // try windows that are 1/29th the width of the result, but not too small or large or it sounds weird + windowSize = numFrames * timescale / 29.0; + windowSize = windowSize.clip(1000*timescale, BufSampleRate.ir(bufnum)/10); + timescaleStep = windowSize / timescale; + sawrate = rate * BufSampleRate.ir(bufnum) / (absdif(begin, end) * timescale * numFrames); + phase = (speed.sign * LFSaw.ar(sawrate, 1)).range(begin,end) * numFrames * timescale; + // do the overlap-add by running through a pair of indices, shifting weights with the window function + index = (phase.div(timescaleStep) - [1.0, 0.0]) * (timescaleStep / timescale - timescaleStep) + phase; + windowIndex = phase - (phase.div(timescaleStep) - [1.0, 0.0] * timescaleStep); + // Gaussian window, the "50" means it's about 3.5 sigma to the edge of the window + window = exp(-50 * squared(windowIndex/windowSize - 0.5)); + + sound = window * BufRd.ar( + numChannels: sampleNumChannels, + bufnum: bufnum, + phase: index, + loop: 0, + interpolation: 4 // cubic interpolation + ).flop; + sound = sound / window.sum; + + sound = DirtPan.ar(sound, numChannels, pan); + + Out.ar(out, sound) + }, [\ir, \ir, \ir, \ir, \ir, \ir, \ir, \ir, \ir, \ir]).add; + }; + + + /* Bus Routing Monitor From 65932483cd49b51721f2a78b788c88798d316614 Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Sun, 31 Jan 2021 08:43:56 -0600 Subject: [PATCH 2/9] small changes as requested --- synths/core-modules.scd | 45 ++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/synths/core-modules.scd b/synths/core-modules.scd index 5957d467..d113659a 100644 --- a/synths/core-modules.scd +++ b/synths/core-modules.scd @@ -37,33 +37,28 @@ this may be refacored later. { |dirtEvent| if(~diversion.value.isNil) { if(~buffer.notNil) { - // argumets could be omitted using getMsgFunc, but for making it easier to understand, we write them out + // arguments could be omitted using getMsgFunc, but for making it easier to understand, we write them out + args = [ + bufnum: ~buffer, + sustain: ~sustain, + speed: ~speed, + freq: ~freq, + endSpeed: ~endSpeed, + begin: ~begin, + end: ~end, + pan: ~pan, + out: ~out + ] + if(~timescale.notNil) { - dirtEvent.sendSynth(~stretchInstrument, [ - bufnum: ~buffer, - sustain: ~sustain, - speed: ~speed, - freq: ~freq, - endSpeed: ~endSpeed, - begin: ~begin, - end: ~end, - pan: ~pan, - timescale: ~timescale, - out: ~out - ]) + instrument = ~stretchInstrument; + args = args.add(\timescale).add(~timescale); } { - dirtEvent.sendSynth(~instrument, [ - bufnum: ~buffer, - sustain: ~sustain, - speed: ~speed, - freq: ~freq, - endSpeed: ~endSpeed, - begin: ~begin, - end: ~end, - pan: ~pan, - out: ~out - ]) - } + instrument = ~instrument; + }; + + dirtEvent.sendSynth(instrument, args); + } { if(~instrument.isNil) { "module 'sound': instrument not found: %".format(~s).postln From 8938e129bebb10c57ef70b5bb306c5a1b4f383b3 Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Wed, 3 Feb 2021 15:07:47 -0600 Subject: [PATCH 3/9] remove outdated comments The timestretch algorithm unfortunately does not play well with the workaround for dealing with long samples. --- classes/DirtSoundLibrary.sc | 1 - synths/core-synths.scd | 1 - 2 files changed, 2 deletions(-) diff --git a/classes/DirtSoundLibrary.sc b/classes/DirtSoundLibrary.sc index 73763078..b2045ea1 100644 --- a/classes/DirtSoundLibrary.sc +++ b/classes/DirtSoundLibrary.sc @@ -264,7 +264,6 @@ DirtSoundLibrary { } stretchInstrumentForBuffer { |buffer| - // may need to add support for long samples? ^format("dirt_stretchsample_%_%", buffer.numChannels, this.numChannels).asSymbol } diff --git a/synths/core-synths.scd b/synths/core-synths.scd index 21044a13..91d9f020 100644 --- a/synths/core-synths.scd +++ b/synths/core-synths.scd @@ -99,7 +99,6 @@ live coding them requires that you have your SuperDirt instance in an environmen }; // this is the time-stretching sample player, based on an overlap-add method - // TODO: long sample version (1..SuperDirt.maxSampleNumChannels).do { |sampleNumChannels| var name = format("dirt_stretchsample_%_%", sampleNumChannels, numChannels); From 455b95801105f83076f78f2a74165cfa65014480 Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Wed, 3 Feb 2021 15:13:57 -0600 Subject: [PATCH 4/9] syntax correction --- synths/core-modules.scd | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/synths/core-modules.scd b/synths/core-modules.scd index d113659a..24dd1f8d 100644 --- a/synths/core-modules.scd +++ b/synths/core-modules.scd @@ -38,7 +38,8 @@ this may be refacored later. if(~diversion.value.isNil) { if(~buffer.notNil) { // arguments could be omitted using getMsgFunc, but for making it easier to understand, we write them out - args = [ + var instrument; + var args = [ bufnum: ~buffer, sustain: ~sustain, speed: ~speed, @@ -48,7 +49,7 @@ this may be refacored later. end: ~end, pan: ~pan, out: ~out - ] + ]; if(~timescale.notNil) { instrument = ~stretchInstrument; From e84cdae4ff1a75bd6f2d2b2f1e566f7c7cb7d7f8 Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Wed, 3 Feb 2021 15:39:40 -0600 Subject: [PATCH 5/9] bugfix Wasn't handling multiple channels correctly, sounds much better now --- synths/core-synths.scd | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/synths/core-synths.scd b/synths/core-synths.scd index 91d9f020..dec7b4e7 100644 --- a/synths/core-synths.scd +++ b/synths/core-synths.scd @@ -105,7 +105,8 @@ live coding them requires that you have your SuperDirt instance in an environmen SynthDef(name, { |out, bufnum, sustain = 1, begin = 0, end = 1, speed = 1, endSpeed = 1, freq = 440, pan = 0, timescale = 1| - var sound, rate, phase, sawrate, numFrames, index, windowIndex, window, timescaleStep, windowSize; + var sound, rate, phase, sawrate, numFrames, index, windowIndex, window, timescaleStep; + var sound0, sound1, windowSize; // playback speed rate = Line.kr(speed, endSpeed, sustain) * (freq / 60.midicps); @@ -127,14 +128,21 @@ live coding them requires that you have your SuperDirt instance in an environmen // Gaussian window, the "50" means it's about 3.5 sigma to the edge of the window window = exp(-50 * squared(windowIndex/windowSize - 0.5)); - sound = window * BufRd.ar( + sound0 = window[0] * BufRd.ar( numChannels: sampleNumChannels, bufnum: bufnum, - phase: index, + phase: index[0], loop: 0, interpolation: 4 // cubic interpolation - ).flop; - sound = sound / window.sum; + ); + sound1 = window[1] * BufRd.ar( + numChannels: sampleNumChannels, + bufnum: bufnum, + phase: index[1], + loop: 0, + interpolation: 4 // cubic interpolation + ); + sound = (sound0 + sound1) / window.sum; sound = DirtPan.ar(sound, numChannels, pan); From 7784fb8602f204520951e41cc6ff88e53f12cba3 Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Thu, 11 Mar 2021 12:10:18 -0600 Subject: [PATCH 6/9] Update DirtEvent.sc defer timescale recalculation until sustain is almost done --- classes/DirtEvent.sc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/DirtEvent.sc b/classes/DirtEvent.sc index 7af9255b..a5f7888c 100644 --- a/classes/DirtEvent.sc +++ b/classes/DirtEvent.sc @@ -100,7 +100,6 @@ DirtEvent { delta * ~legato.value } { unitDuration = unitDuration ? delta; - if (~timescale.notNil) {unitDuration = unitDuration * ~timescale }; loop !? { unitDuration = unitDuration * loop.abs }; } }; @@ -111,6 +110,7 @@ DirtEvent { ~fadeTime = min(~fadeTime.value, sustain * 0.19098); ~fadeInTime = if(~begin != 0) { ~fadeTime } { 0.0 }; + if (~timescale.notNil) {sustain = sustain * ~timescale }; ~sustain = sustain - (~fadeTime + ~fadeInTime); ~speed = speed; ~endSpeed = endSpeed; From d5f813b0dd5b954f3e2447b4629c5e138d1b8667 Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Thu, 11 Mar 2021 13:26:32 -0600 Subject: [PATCH 7/9] window size adjustment Clarify how the window size is chosen, and add a new parameter to make it tweakable --- synths/core-modules.scd | 2 +- synths/core-synths.scd | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/synths/core-modules.scd b/synths/core-modules.scd index 24dd1f8d..f6bc0532 100644 --- a/synths/core-modules.scd +++ b/synths/core-modules.scd @@ -53,7 +53,7 @@ this may be refacored later. if(~timescale.notNil) { instrument = ~stretchInstrument; - args = args.add(\timescale).add(~timescale); + args = args.add(\timescale).add(~timescale).add(\timescalewin).add(~timescalewin ? 1); } { instrument = ~instrument; }; diff --git a/synths/core-synths.scd b/synths/core-synths.scd index dec7b4e7..48f8a8c5 100644 --- a/synths/core-synths.scd +++ b/synths/core-synths.scd @@ -103,7 +103,7 @@ live coding them requires that you have your SuperDirt instance in an environmen var name = format("dirt_stretchsample_%_%", sampleNumChannels, numChannels); - SynthDef(name, { |out, bufnum, sustain = 1, begin = 0, end = 1, speed = 1, endSpeed = 1, freq = 440, pan = 0, timescale = 1| + SynthDef(name, { |out, bufnum, sustain = 1, begin = 0, end = 1, speed = 1, endSpeed = 1, freq = 440, pan = 0, timescale = 1, timescalewin = 1| var sound, rate, phase, sawrate, numFrames, index, windowIndex, window, timescaleStep; var sound0, sound1, windowSize; @@ -116,9 +116,11 @@ live coding them requires that you have your SuperDirt instance in an environmen //phase = Sweep.ar(1, rate * BufSampleRate.ir(bufnum)) + (BufFrames.ir(bufnum) * begin); numFrames = BufFrames.ir(bufnum); - // try windows that are 1/29th the width of the result, but not too small or large or it sounds weird - windowSize = numFrames * timescale / 29.0; - windowSize = windowSize.clip(1000*timescale, BufSampleRate.ir(bufnum)/10); + // Picking the right window size is a tricky thing, something around 2000 samples is usually + // OK for sounds of moderate length (around 0.5 to several seconds long). + // But this is also scaled below to be a bit smaller/larger based on desired playback rate. + // If it's still not good, you can use the "timescalewin" parameter to multiply the window size + windowSize = timescale.clip(0.1,2) * 0.05 * BufSampleRate.ir(bufnum) * timescalewin; timescaleStep = windowSize / timescale; sawrate = rate * BufSampleRate.ir(bufnum) / (absdif(begin, end) * timescale * numFrames); phase = (speed.sign * LFSaw.ar(sawrate, 1)).range(begin,end) * numFrames * timescale; @@ -147,7 +149,7 @@ live coding them requires that you have your SuperDirt instance in an environmen sound = DirtPan.ar(sound, numChannels, pan); Out.ar(out, sound) - }, [\ir, \ir, \ir, \ir, \ir, \ir, \ir, \ir, \ir, \ir]).add; + }, [\ir, \ir, \ir, \ir, \ir, \ir, \ir, \ir, \kr, \ir, \ir]).add; }; From c78d427f140d8faebb6abc063ce2fae2a5d5d59e Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Tue, 23 Mar 2021 15:47:36 -0500 Subject: [PATCH 8/9] rewrites for clarity --- synths/core-modules.scd | 2 +- synths/core-synths.scd | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/synths/core-modules.scd b/synths/core-modules.scd index f6bc0532..e7750874 100644 --- a/synths/core-modules.scd +++ b/synths/core-modules.scd @@ -53,7 +53,7 @@ this may be refacored later. if(~timescale.notNil) { instrument = ~stretchInstrument; - args = args.add(\timescale).add(~timescale).add(\timescalewin).add(~timescalewin ? 1); + args = args ++ [\timescale, ~timescale, \timescalewin, ~timescalewin ? 1]; } { instrument = ~instrument; }; diff --git a/synths/core-synths.scd b/synths/core-synths.scd index 48f8a8c5..fdb22db6 100644 --- a/synths/core-synths.scd +++ b/synths/core-synths.scd @@ -106,7 +106,7 @@ live coding them requires that you have your SuperDirt instance in an environmen SynthDef(name, { |out, bufnum, sustain = 1, begin = 0, end = 1, speed = 1, endSpeed = 1, freq = 440, pan = 0, timescale = 1, timescalewin = 1| var sound, rate, phase, sawrate, numFrames, index, windowIndex, window, timescaleStep; - var sound0, sound1, windowSize; + var sound0, sound1, windowSize, nSteps; // playback speed rate = Line.kr(speed, endSpeed, sustain) * (freq / 60.midicps); @@ -125,8 +125,9 @@ live coding them requires that you have your SuperDirt instance in an environmen sawrate = rate * BufSampleRate.ir(bufnum) / (absdif(begin, end) * timescale * numFrames); phase = (speed.sign * LFSaw.ar(sawrate, 1)).range(begin,end) * numFrames * timescale; // do the overlap-add by running through a pair of indices, shifting weights with the window function - index = (phase.div(timescaleStep) - [1.0, 0.0]) * (timescaleStep / timescale - timescaleStep) + phase; - windowIndex = phase - (phase.div(timescaleStep) - [1.0, 0.0] * timescaleStep); + nSteps = phase.div(timescaleStep) - [1.0, 0.0]; + index = nSteps * (timescaleStep / timescale - timescaleStep) + phase; + windowIndex = phase - (nSteps * timescaleStep); // Gaussian window, the "50" means it's about 3.5 sigma to the edge of the window window = exp(-50 * squared(windowIndex/windowSize - 0.5)); From 26dba058c35836440f271b7cf12b7cccf7b7e076 Mon Sep 17 00:00:00 2001 From: Ben Gold Date: Wed, 18 Aug 2021 11:16:15 -0500 Subject: [PATCH 9/9] Further checking and updating description of algorithm --- synths/core-synths.scd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/synths/core-synths.scd b/synths/core-synths.scd index fdb22db6..e0677793 100644 --- a/synths/core-synths.scd +++ b/synths/core-synths.scd @@ -99,6 +99,8 @@ live coding them requires that you have your SuperDirt instance in an environmen }; // this is the time-stretching sample player, based on an overlap-add method + // the method is designed for timescale > 1 (i.e. stretching), + // but works reasonably well for values between 0.1 and 3, depending on the sample (1..SuperDirt.maxSampleNumChannels).do { |sampleNumChannels| var name = format("dirt_stretchsample_%_%", sampleNumChannels, numChannels); @@ -121,6 +123,13 @@ live coding them requires that you have your SuperDirt instance in an environmen // But this is also scaled below to be a bit smaller/larger based on desired playback rate. // If it's still not good, you can use the "timescalewin" parameter to multiply the window size windowSize = timescale.clip(0.1,2) * 0.05 * BufSampleRate.ir(bufnum) * timescalewin; + // Next is the (pre-scaled) number of samples between indices. Note that while windowSize + // is clipped, timescaleStep cannot be without the duration of the resulting sound becoming + // different from timescale * sample_duration. + // For very small values of timescale, this means it'll "skip" through the sound, only playing + // a few samples at regular intervals + // For very large values of timescale, the overlap will be very large, possibly leading to + // some audible repetition timescaleStep = windowSize / timescale; sawrate = rate * BufSampleRate.ir(bufnum) / (absdif(begin, end) * timescale * numFrames); phase = (speed.sign * LFSaw.ar(sawrate, 1)).range(begin,end) * numFrames * timescale;