Compare commits

..

4 Commits

@ -18,3 +18,73 @@ Ndef(~synth[\name]).play;
) )
#+END_SRC #+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,25 @@
(
name: 'fft_freeze',
func: {
| buf |
var in = In.ar(buf, 1);
var isOn = \freezeGate.kr(0);
var chain = FFT(
LocalBuf(2048),
in
);
var frozen = PV_Freeze(chain, isOn);
var jittered = PV_PhaseShift(frozen, \phaseJitter.kr(0) * TRand.kr(0, 2pi) * isOn);
var sig = IFFT(jittered);
sig;
},
inChannels: 1,
outChannels: 1,
requiresBuffer: true,
specs: (
freezeGate: [0, 1, \step, 1, 0],
phaseJitter: [0, 1, \lin, 0.01, 0.1],
),
)

@ -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,29 +1,89 @@
NodeDictionary : IdentityDictionary { NodeDictionary : IdentityDictionary {
getDefaultOr { getDefaultOr {
| key, defaultValue | | key, defaultValue |
if(this[\defaults][key].isNil, { if(this[\defaults][key].isNil, {
^defaultValue ^defaultValue
}, { }, {
^this[\defaults][key] ^this[\defaults][key]
}); });
} }
makeAnalysysNdef { processSig {
^Ndef(this[\name], { | sig, gate |
var input = SoundIn.ar(\bus.kr(0));
var signal = SynthDef.wrap( var releaseNode = if (
func: this[\func], this[\sustained].asBoolean,
prependArgs: [input] { 2 },
); { nil }
);
if(this[\convertToKr].isNil.not
and: var envDoneAction = if (
this[\convertToKr] == true, this[\sustained].asBoolean,
{ { 0 },
A2K.kr(signal); { 2 }
}, { );
signal;
}); if(this[\useLfo].asBoolean, {
}); var lfo = switch(this[\lfoWave])
} { \sin } { SinOsc }
{ \saw } { Saw }
{ \tri } { LFTri }
{ \pulse } { Pulse }
? SinOsc;
signal = signal * lfo.kr(
\lfoFreq.kr(1)
).range(
1 - \lfoGain.kr(0.5), 1
);
});
if(this[\useSaturation].asBoolean, {
this.getDefaultOr(\saturationSteps, 4).do {
signal = signal.tanh;
}
});
if(this[\useEnv].asBoolean, {
var att = this.getDefaultOr(\attack, 0.1);
var rel = this.getDefaultOr(\release, 0.1);
var curve = this.getDefaultOr(\curve, 1);
var env = Env(
[0, 0, 1, 0],
[0, \attack.kr(att), \release.kr(rel)],
[0, \attackCurve.kr(curve), \releaseCurve.kr(curve)],
releaseNode: releaseNode,
).kr(envDoneAction, gate: gate);
signal = signal * env;
});
if(this[\usePan].asBoolean, {
switch(this[\panning],
\splay, { signal = Splay.ar(signal, \spread.kr(1), center: \pan.kr(0)); },
\splayAz, { signal = SplayAz.ar(this[\panningChannels], signal, orientation: \orientation.kr(0.5)); },
\pan, { signal = Pan2.ar(signal, \pan.kr(0)); }
);
});
if(this[\postAmp].asBoolean, {
signal = signal * \postAmp.kr(1);
});
// if(this[\detectSilence].asBoolean, {
// FreeSelf.kr(
// DetectSilence.ar(
// [signal[0], signal[1]] + Impulse.ar(0)
// ).product)
// });
if(this[\limitDuration].asBoolean, {
Line.kr(0, 1, \dur.kr(1), doneAction: 2);
});
signal = signal * \amp.kr(1);
^signal;
}
} }

@ -1,14 +1,19 @@
+ NodeDictionary { + NodeDictionary {
makeBufferProcessingNdef { makeBufferProcessingNdef {
| name, buf, quant = 1.0 | | name |
var ndef = Ndef(name);
ndef.proxyspace.quant = quant; var internalName = if(name.isNil, {this[\name]}, {name});
ndef[0] = SynthDef.wrap(
func: this[\func], Ndef(internalName, {
prependArgs: [buf] var signal, gate;
);
gate = \gate.tr(0);
ndef.play;
^ndef; signal = SynthDef.wrap(
} func: this[\func],
);
this.processSig(signal, gate);
});
}
} }

@ -1,32 +1,18 @@
+ NodeDictionary { + NodeDictionary {
makeBufferProcessingSynthDef { makeBufferProcessingSynthDef {
SynthDef(this[\name], { SynthDef(this[\name], {
var buf = \buf.kr; var signal, gate;
var signal = SynthDef.wrap(
func: this[\func],
prependArgs: [buf]
);
signal = Pan2.ar( gate = \gate.tr(0);
signal * \amp.kr(1),
\pan.kr(0)
);
if(this[\detectSilence].asBoolean, { signal = SynthDef.wrap(
FreeSelf.kr( func: this[\func],
DetectSilence.ar( );
[signal[0], signal[1]] + Impulse.ar(0)
).product)
});
if(this[\limitDuration].asBoolean, { Out.ar(
Line.kr(0, 1, \dur.kr(1), doneAction: 2); \out.kr(0),
}); this.processSig(signal, gate);
);
Out.ar( });
\out.kr(0), }
signal
);
}).add;
}
} }

@ -1,74 +1,24 @@
+ NodeDictionary { + NodeDictionary {
makeSourceNdef { makeSourceNdef {
| name | | name |
var internalName = if(name.isNil, {this[\name]}, {name}); var internalName = if(name.isNil, {this[\name]}, {name});
Ndef(internalName, { Ndef(internalName, {
var freq, signal, env, gate; var freq, signal, gate;
var releaseNode = if ( gate = \gate.tr(0);
this[\sustained].asBoolean, freq = \freq.kr(220);
{ 2 },
{ nil }
);
gate = \gate.tr(0); if(this[\useFreqLag].asBoolean, {
freq = \freq.kr(220); freq = Lag.kr(freq, \freqLag.kr(0.5));
});
if(this[\useFreqLag].asBoolean, { signal = SynthDef.wrap(
freq = Lag.kr(freq, \freqLag.kr(0.5)); func: this[\func],
}); prependArgs: [freq, gate]
);
signal = SynthDef.wrap( this.processSig(signal, gate);
func: this[\func], });
prependArgs: [freq, gate] }
);
if(this[\useLfo].asBoolean, {
var lfo = switch(this[\lfoWave])
{ \sin } { SinOsc }
{ \saw } { Saw }
{ \tri } { LFTri }
{ \pulse } { Pulse }
? SinOsc;
signal = signal * lfo.kr(
\lfoFreq.kr(1)
).range(
1 - \lfoGain.kr(0.5), 1
);
});
if(this[\useSaturation].asBoolean, {
this[\saturationSteps].do {
signal = signal.tanh;
}
});
if(this[\useEnv].asBoolean, {
var att = this.getDefaultOr(\attack, 0.1);
var rel = this.getDefaultOr(\release, 0.1);
var curve = this.getDefaultOr(\curve, 1);
var env = Env(
[0, 0, 1, 0],
[0, \attack.kr(att), \release.kr(rel)],
[0, \attackCurve.kr(curve), \releaseCurve.kr(curve)],
releaseNode: releaseNode,
).kr(0, gate: gate);
signal = signal * env;
});
if(this[\usePan].asBoolean, {
switch(this[\panning],
\splay, { signal = Splay.ar(signal, \spread.kr(1), center: \pan.kr(0)); },
\splayAz, { signal = SplayAz.ar(this[\panningChannels], signal, orientation: \orientation.kr(0.5)); },
\pan, { signal = Pan2.ar(signal, \pan.kr(0)); }
);
});
signal * \amp.kr(1);
});
}
} }

@ -1,84 +1,24 @@
+ NodeDictionary { + NodeDictionary {
makeSourceSynthDef { makeSourceSynthDef {
^SynthDef(this[\name], { ^SynthDef(this[\name], {
var freq, signal, env, gate; var freq, signal, gate;
var releaseNode = if ( gate = \gate.tr(0);
this[\sustained].asBoolean, freq = \freq.kr(220);
{ 2 },
{ nil } if(this[\useFreqLag].asBoolean, {
); freq = Lag.kr(freq, \freqLag.kr(0.5));
});
var envDoneAction = if (
this[\sustained].asBoolean, signal = SynthDef.wrap(
{ 0 }, func: this[\func],
{ 2 } prependArgs: [freq, gate]
); );
gate = \gate.tr(0); Out.ar(
freq = \freq.kr(220); \out.kr(0),
signal,
if(this[\useFreqLag].asBoolean, { );
freq = Lag.kr(freq, \freqLag.kr(0.5)); });
}); }
signal = SynthDef.wrap(
func: this[\func],
prependArgs: [freq, gate]
);
if(this[\useLfo].asBoolean, {
var lfo = switch(this[\lfoWave])
{ \sin } { SinOsc }
{ \saw } { Saw }
{ \tri } { LFTri }
{ \pulse } { Pulse }
? SinOsc;
signal = signal * lfo.kr(
\lfoFreq.kr(1)
).range(
1 - \lfoGain.kr(0.5), 1
);
});
if(this[\useSaturation].asBoolean, {
this[\saturationSteps].do {
signal = signal.tanh;
}
});
if(this[\useEnv].asBoolean, {
var att = this.getDefaultOr(\attack, 0.1);
var rel = this.getDefaultOr(\release, 0.1);
var curve = this.getDefaultOr(\curve, 1);
var env = Env(
[0, 0, 1, 0],
[0, \attack.kr(att), \release.kr(rel)],
\envCurve.kr(curve),
releaseNode: releaseNode,
).kr(envDoneAction, gate: gate);
signal = signal * env;
});
switch(this[\panning],
\splay, { signal = Splay.ar(signal, \spread.kr(1), center: \pan.kr(0)); },
\splayAz, { signal = SplayAz.ar(this[\panningChannels], signal, orientation: \orientation.kr(0.5)); },
nil, { signal = Pan2.ar(signal, \pan.kr(0)); }
);
signal = signal * \amp.kr(1);
if(this[\postAmp].asBoolean, {
signal = signal * \postAmp.kr(1);
});
Out.ar(
\out.kr(0),
signal,
);
});
}
} }
Loading…
Cancel
Save