diff --git a/DSP/README.org b/DSP/README.org index b866ed8..00f738e 100644 --- a/DSP/README.org +++ b/DSP/README.org @@ -18,3 +18,73 @@ Ndef(~synth[\name]).play; ) #+END_SRC +* Словари-обработки (Fx-словари) через makeBufferProcessingNdef +#+BEGIN_SRC sclang +( +// Загрузка словаря как buffer-processing Fx. +~dict = BmDspAlgs.getDict("spectral/fft_freeze.scd"); + +// Вход через Bus — стандартный паттерн: makeBufferProcessingNdef +// передаёт \buf как OSC-параметр (control-rate число = номер bus), +// func получает его первым аргументом. +~inBus = Bus.audio(s, 2); +{ Out.ar(~inBus, SoundIn.ar([0, 1])) }.play; + +// Создаём Ndef: name = имя нового Ndef, buf = audio-сигнал. +~dict.makeBufferProcessingNdef(\freezeOnly, ~inBus); + +// Параметры func (fftBufnum, freezeGate, phaseJitter) теперь — +// control-параметры SynthDef, доступные через .set / .map. +Ndef(\freezeOnly).set(\fftBufnum, someBuffer); +Ndef(\freezeOnly).set(\freezeGate, 1); +Ndef(\freezeOnly).play; +) +#+END_SRC + +** Контракт func для Fx-словарей + +Func в Fx-словаре объявляется с одним обязательным первым аргументом — *buf*: + +#+BEGIN_SRC sclang +func: { + | buf | + // buf — это audio-сигнал, пришедший через OSC-параметр \buf. + // В скетче он = номер Bus.audio, из которого читает Ndef. + // Остальные параметры объявляются через \name.kr(...) / + // \name.tr(...) и становятся control-параметрами SynthDef. + var sig = ...; + sig; +} +#+END_SRC + +Имя первого аргумента (=buf) — это convention, не контракт. Важно, +что func принимает ровно один positional аргумент первым; обёртка +=makeBufferProcessingNdef= делает =SynthDef.wrap(func, prependArgs: [buf])=. + +** Цепочки Fx-словарей + +Чтобы пропустить выход одного Ndef через другой Fx-словарь, +используется промежуточный Bus: + +#+BEGIN_SRC sclang +~midBus = Bus.audio(s, 2); +Ndef(\first)[0] = { Out.ar(~midBus, Ndef(\first).bus.asMap) }; + +~dict2.makeBufferProcessingNdef(\second, ~midBus); +Ndef(\second).play; +#+END_SRC + +Это даёт последовательную обработку: =first → midBus → second=. + +** Отличия от нотных словарей + +Нотные словари (=makeSourceSynthDef= / =makeSourceNdef=) оборачивают func +с prependArgs =[freq, gate]= и добавляют ADSR-огибающую, panning, amp. +Func должна быть вида =| freq, gate |=. + +Fx-словари (=makeBufferProcessingSynthDef= / =makeBufferProcessingNdef=) +оборачивают func с prependArgs =[buf]= и добавляют Pan2 + amp. +Func должна быть вида =| buf |=. Дополнительные обёртки (DetectSilence, +limitDuration через Line) включаются ключами =detectSilence: true= и +=limitDuration: true= в словаре. + diff --git a/DSP/examples/spectral_freeze_scatter.scd b/DSP/examples/spectral_freeze_scatter.scd new file mode 100644 index 0000000..102d554 --- /dev/null +++ b/DSP/examples/spectral_freeze_scatter.scd @@ -0,0 +1,104 @@ +( +// Пример: spectral freeze + grain scatter. +// Концепция из README.org дуэта 24_09_2026 (концепция №1). +// +// Два независимых словаря: +// - fft_freeze — удержание magnitude, фазовый джиттер (PV_Freeze + PV_PhaseShift). +// - grain_scatter — WarpZ по кольцевому буферу, scatter позиции. +// +// Оба словаря идут через makeBufferProcessingNdef — это стандартный +// путь для Fx/сэмплер-словарей в bmSClib: func получает audio-сигнал +// первым аргументом (| buf |), обёртка делает SynthDef.wrap(func, [buf]) +// и кладёт результат в Ndef. +// +// Входной звук подаётся через Bus — это стандартный паттерн для Fx, +// потому что makeBufferProcessingNdef передаёт \buf как OSC-параметр +// (control-rate число = номер bus), а не как UGen. + +s.options.numBuffers = 1024; // FFT-буферы + ringbuffer для scatter + +s.waitForBoot({ + // ---- Распределение буферов ------------------------------------- + // FFT: явное распределение, как в шаблоне make-scd.el. + ~fftBuf = Buffer.alloc(s, 1024, 2); // stereo FFT, 1024 сэмплов + + // Кольцевой буфер для grain_scatter: 2 секунды, стерео. + ~ringBuf = Buffer.alloc(s, s.sampleRate * 2.0, 2); + + // ---- Входной bus ------------------------------------------------ + // В реальном концерте — звук от Letov или печатной машинки. + // Здесь — SoundIn.ar как stand-in, пишется в ~inBus отдельным + // синтом. Ndef читает этот bus по номеру через \buf.kr. + ~inBus = Bus.audio(s, 2); + { Out.ar(~inBus, SoundIn.ar([0, 1])) }.play; + + // ---- Загрузка словарей ------------------------------------------ + ~freezeDict = BmDspAlgs.getDict("spectral/fft_freeze.scd"); + ~scatterDict = BmDspAlgs.getDict("spectral/grain_scatter.scd"); + + // ---- Режим 1: только freeze -------------------------------------- + // makeBufferProcessingNdef(name, buf) — создаёт Ndef(\name) с + // func, обёрнутой через SynthDef.wrap(func, prependArgs: [buf]). + // buf — это audio-сигнал; здесь передаём номер bus. + ~freezeDict.makeBufferProcessingNdef(\freezeOnly, ~inBus); + Ndef(\freezeOnly).set(\fftBufnum, ~fftBuf.bufnum); + Ndef(\freezeOnly).play; + + // Управление freeze-педалью: gate=1 триггерит freeze, gate=0 + // отпускает. Удобно вешать на MIDI CC или OSC. + Ndef(\freezeOnly).addSpec(\freezeGate, [0, 1, \lin, 1, 0]); + Ndef(\freezeOnly).set(\freezeGate, 0); + + // ---- Режим 2: только scatter ------------------------------------- + ~scatterDict.makeBufferProcessingNdef(\scatterOnly, ~inBus); + Ndef(\scatterOnly).set(\bufnum, ~ringBuf.bufnum); + Ndef(\scatterOnly).play; + + // ---- Режим 3: оба последовательно ------------------------------- + // freeze → scatter: frozen-сигнал проходит через WarpZ. + // Ndef-цепочка: Ndef(\freezeOnly) пишет на свой bus, scatter + // читает с него. + ~freezeBus = Bus.audio(s, 2); + Ndef(\freezeOnly)[0] = { Out.ar(~freezeBus, Ndef(\freezeOnly).bus.asMap) }; + + ~scatterDict.makeBufferProcessingNdef(\freezeAndScatter, ~freezeBus); + Ndef(\freezeAndScatter).set(\bufnum, ~ringBuf.bufnum); + Ndef(\freezeAndScatter).play; + + // ---- Педали ------------------------------------------------------ + // Управление через OSC / MIDI / HID. Ndef.set принимает числа, + // для live-управления — .map(\param, .kr). + Ndef(\freezeOnly).addSpec(\phaseJitter, [0, 1, \lin, 0.01, 0.1]); + Ndef(\freezeOnly).set(\phaseJitter, 0.1); + + Ndef(\scatterOnly).addSpec(\positionJitter, [0, 1, \lin, 0.01, 0.3]); + Ndef(\scatterOnly).set(\positionJitter, 0.3); +}); + +// ---- Использование в перформансе ------------------------------------- +// В живом концерте: +// +// 1. freezeOnly: установить \freezeGate в 1, чтобы "заморозить" +// текущий вход. phaseJitter через педаль добавляет "дыхание" +// frozen-спектра. +// +// 2. На тихой паузе (Letov молчит) — включить freeze, и звук +// "дышит" сам по себе. Снять freeze — вернуться к реальному +// входу. +// +// 3. Когда Letov играет активно — scatterOnly поверх, с +// разбросом позиции через .map(\positionJitter, .kr). +// +// 4. freezeAndScatter: сначала freeze, затем scatter по +// frozen-сигналу. Получается "зависший" звук, который +// разбивается на зёрна — главный спецэффект скетча. +// +// ---- Остановка ------------------------------------------------------- +// Ndef(\freezeOnly).stop; +// Ndef(\scatterOnly).stop; +// Ndef(\freezeAndScatter).stop; +// ~fftBuf.free; +// ~ringBuf.free; +// ~inBus.free; +// ~freezeBus.free; +) diff --git a/DSP/examples/using_dictionaries_as_synths.scd b/DSP/examples/using_dictionaries_as_synths.scd new file mode 100644 index 0000000..4329ecb --- /dev/null +++ b/DSP/examples/using_dictionaries_as_synths.scd @@ -0,0 +1,77 @@ +s.boot; + +~dict = BmDspAlgs.getDict("synths/bass/saw_bass.scd"); +~dict[\postAmp] = true; +~dict[\useEnv] = true; + +~dict.makeSourceSynthDef().add(); + +~sawBass = (); +~sawBass[\name] = \saw; + +Ndef(~sawBass[\name]).clear; +Ndef(~sawBass[\name]).ar(2); +Ndef(~sawBass[\name]).prime(~dict[\name]); +Ndef(~sawBass[\name]).play; + +( +Tdef(\t, { + loop { + [40, 45, 47].do({|n| + Ndef(~sawBass[\name]).put( + n, + Ndef(~sawBass[\name]).source, + 0, + [\freq, n.midicps, \gate, 1, \amp, 0.25, \release, 0.5, \attack, 0.01] + ); + 0.25.wait; + }); + } +}).play; +) + +( +Tdef(\t2, { + loop { + [55, 61].do({|n| + Ndef(~sawBass[\name]).put( + n, + Ndef(~sawBass[\name]).source, + 0, + [\freq, n.midicps, \gate, 1, \amp, 0.25, \release, 0.1, \attack, 0.01] + ); + (4/7).wait; + }); + } +}).play; +) + +( +Ndef(~sawBass[\name]).put( 159, \filter -> { arg in; + d = LPF.ar(in, 1100); + d +}); +) + +( +Ndef(~sawBass[\name]).put( 160, \filter -> { arg in; + var d = in; + var diffusion = \diffusion.kr(1); + var spacesize = \spacesize.kr(0.1); + 8.do { |i| + d = AllpassC.ar( + d, + delaytime: LFNoise1.kr(0.01!2).range(0.01, spacesize), + decaytime: diffusion + ) + }; + + d +}); +) + +Ndef(~sawBass[\name]).set(\diffusion, 0.03); +Ndef(~sawBass[\name]).set(\spacesize, 0.1); + +Tdef(\t2).stop; +Tdef(\t).stop; \ No newline at end of file diff --git a/DSP/spectral/fft_freeze.scd b/DSP/spectral/fft_freeze.scd new file mode 100644 index 0000000..1c9de45 --- /dev/null +++ b/DSP/spectral/fft_freeze.scd @@ -0,0 +1,53 @@ +( +name: 'fft_freeze', +func: { + | buf | + + // Входной звук → FFT → freeze magnitude (PV_Freeze) + phase-jitter + // (PV_PhaseShift с шумом) → IFFT. + // + // FFT-буфер передаётся снаружи через \fftBufnum — это позволяет + // распределять буферы в скетче (s.options.numBuffers) и переиспользовать + // между перезапусками синта. + // + // Почему не кастомная пересборка chain из magnitude/phase: готовых + // PV_* UGen для неё нет, а PV_Chain не имеет метода .polar. + // UnpackFFT/PackFFT подходят для поканальной обработки, но не для + // uniform-операций вроде freeze или jitter. PV_Freeze + PV_PhaseShift + // дают ближайший готовый результат. + + var fftBuf = \fftBufnum.kr(0); + + var chain = FFT( + fftBuf, + buf, + \hop.kr(0.25), + \wintype.kr(1) + ); + + // PV_Freeze принимает gate trigger: при freezeGate=1 удерживает + // magnitude, при 0 — пропускает как есть. + var frozen = PV_Freeze(chain, \freezeGate.tr(0)); + + // Phase jitter: случайный сдвиг фазы всех бинов. Субъективно — + // "дыхание" frozen-спектра. + var jittered = PV_PhaseShift(frozen, \phaseJitter.kr(0) * TRand.kr(0, 2pi)); + + var rawSig = IFFT(jittered, \wintype.kr(1)); + + // Duck на громкие моменты — общий паттерн для защиты от клиппинга + var sig = rawSig * (1 - \duckOnLoud.kr(0).clip(0, 1) * + (Amplitude.kr(rawSig) > \duckThreshold.kr(0.95)).lag(0.05)); + + sig +}, +inChannels: 2, +outChannels: 2, +requiresBuffer: true, +specs: ( + phaseJitter: [0, 1, \lin, 0.01, 0.1], + hop: [0.0625, 1, \exp, 0.01, 0.25], + duckOnLoud: [0, 1, \lin, 0.01, 0.5], + duckThreshold: [0, 1, \lin, 0.01, 0.95] +), +) diff --git a/DSP/spectral/grain_scatter.scd b/DSP/spectral/grain_scatter.scd new file mode 100644 index 0000000..e09b3c6 --- /dev/null +++ b/DSP/spectral/grain_scatter.scd @@ -0,0 +1,54 @@ +( +name: 'grain_scatter', +func: { + | buf | + var bufnum, rawSig, sig; + // Кольцевой буфер (явно создаётся в скетче): RecordBuf пишет в + // живой вход, WarpZ читает с растяжением и рандомизацией позиции. + // Возвращает развёрнутый во времени сигнал, "разбросанный" по буферу. + + bufnum = \bufnum.kr(0); + + // Захват входа в кольцевой буфер. RecordBuf.ar() пишет в буфер + // как побочный эффект; результат не используется. + RecordBuf.ar( + buf, + bufnum, + doneAction: 0 + ); + + // WarpZ: чтение с растяжением и рандомизацией позиции + rawSig = WarpZ.ar( + numChannels: 2, + bufnum: bufnum, + freq: \warpFreq.kr(40), + windowSize: \warpWindowSize.kr(0.15), + position: \positionJitter.kr(0.3), + posBase: \posBase.kr(0), + overlaps: \overlaps.kr(4), + windowRandRatio: \windowRandRatio.kr(0.2), + freqMul: \freqMul.kr(1), + interp: 2 + ); + + // Duck на громкие моменты — общий паттерн для защиты от клиппинга + sig = rawSig * (1 - \duckOnLoud.kr(0).clip(0, 1) * + (Amplitude.kr(rawSig) > \duckThreshold.kr(0.95)).lag(0.05)); + + sig; +}, +inChannels: 2, +outChannels: 2, +requiresBuffer: true, +specs: ( + warpFreq: [1, 200, \exp, 1, 40], + warpWindowSize: [0.01, 0.5, \lin, 0.01, 0.15], + positionJitter: [0, 1, \lin, 0.01, 0.3], + posBase: [0, 1, \lin, 0.01, 0.5], + overlaps: [1, 16, \lin, 1, 4], + windowRandRatio: [0, 1, \lin, 0.01, 0.2], + freqMul: [0.25, 4, \exp, 0.01, 1], + duckOnLoud: [0, 1, \lin, 0.01, 0.5], + duckThreshold: [0, 1, \lin, 0.01, 0.95] +), +) diff --git a/NodeDictionary/makeBufferProcessingNdef.sc b/NodeDictionary/makeBufferProcessingNdef.sc index 145e6b2..03e87f3 100644 --- a/NodeDictionary/makeBufferProcessingNdef.sc +++ b/NodeDictionary/makeBufferProcessingNdef.sc @@ -1,14 +1,18 @@ + NodeDictionary { makeBufferProcessingNdef { | name, buf, quant = 1.0 | - var ndef = Ndef(name); - ndef.proxyspace.quant = quant; - ndef[0] = SynthDef.wrap( - func: this[\func], - prependArgs: [buf] - ); - - ndef.play; - ^ndef; + + var internalName = if(name.isNil, {this[\name]}, {name}); + + Ndef(internalName, { + var freq, signal, env, gate; + + signal = SynthDef.wrap( + func: this[\func], + prependArgs: [buf] + ); + + signal * \amp.kr(1); + }); } } \ No newline at end of file