WIP: Добавил синты и обновил документацию

spectral-algs
lambda 4 weeks ago
parent aa9e6a1e91
commit cfb1196081
No known key found for this signature in database
GPG Key ID: CF45A8EE158BBCB6

@ -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= в словаре.

@ -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, <CC>.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, <CC>.kr).
//
// 4. freezeAndScatter: сначала freeze, затем scatter по
// frozen-сигналу. Получается "зависший" звук, который
// разбивается на зёрна — главный спецэффект скетча.
//
// ---- Остановка -------------------------------------------------------
// Ndef(\freezeOnly).stop;
// Ndef(\scatterOnly).stop;
// Ndef(\freezeAndScatter).stop;
// ~fftBuf.free;
// ~ringBuf.free;
// ~inBus.free;
// ~freezeBus.free;
)

@ -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;

@ -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]
),
)

@ -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]
),
)

@ -1,14 +1,18 @@
+ NodeDictionary {
makeBufferProcessingNdef {
| name, buf, quant = 1.0 |
var ndef = Ndef(name);
ndef.proxyspace.quant = quant;
ndef[0] = SynthDef.wrap(
var internalName = if(name.isNil, {this[\name]}, {name});
Ndef(internalName, {
var freq, signal, env, gate;
signal = SynthDef.wrap(
func: this[\func],
prependArgs: [buf]
);
ndef.play;
^ndef;
signal * \amp.kr(1);
});
}
}
Loading…
Cancel
Save