@coldsurf/synth-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/bass/index.d.ts +83 -0
- package/dist/bass/index.js +13 -0
- package/dist/bass/index.js.map +1 -0
- package/dist/chunk-CMHDHRZ6.js +61 -0
- package/dist/chunk-CMHDHRZ6.js.map +1 -0
- package/dist/chunk-CNJ6DQV5.js +147 -0
- package/dist/chunk-CNJ6DQV5.js.map +1 -0
- package/dist/drumkit/index.d.ts +153 -0
- package/dist/drumkit/index.js +494 -0
- package/dist/drumkit/index.js.map +1 -0
- package/dist/dub/index.d.ts +103 -0
- package/dist/dub/index.js +183 -0
- package/dist/dub/index.js.map +1 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +246 -0
- package/dist/index.js.map +1 -0
- package/dist/space/index.d.ts +56 -0
- package/dist/space/index.js +7 -0
- package/dist/space/index.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// src/dub/chamber.ts
|
|
2
|
+
function makeSaturationCurve(drive) {
|
|
3
|
+
const n = 1024;
|
|
4
|
+
const buffer = new ArrayBuffer(n * Float32Array.BYTES_PER_ELEMENT);
|
|
5
|
+
const curve = new Float32Array(buffer);
|
|
6
|
+
const denom = Math.tanh(drive);
|
|
7
|
+
for (let i = 0; i < n; i++) {
|
|
8
|
+
const x = i / (n - 1) * 2 - 1;
|
|
9
|
+
curve[i] = Math.tanh(drive * x) / denom;
|
|
10
|
+
}
|
|
11
|
+
return curve;
|
|
12
|
+
}
|
|
13
|
+
function createDubChamber(ctx, destination, preset) {
|
|
14
|
+
const { master, fade, duck, delay } = preset;
|
|
15
|
+
const masterTape = ctx.createBiquadFilter();
|
|
16
|
+
masterTape.type = "lowpass";
|
|
17
|
+
masterTape.frequency.value = master.lowpassHz;
|
|
18
|
+
masterTape.Q.value = 0.5;
|
|
19
|
+
const masterSat = ctx.createWaveShaper();
|
|
20
|
+
masterSat.curve = makeSaturationCurve(master.satDrive);
|
|
21
|
+
masterSat.oversample = "4x";
|
|
22
|
+
const masterOut = ctx.createGain();
|
|
23
|
+
masterOut.gain.value = 1e-4;
|
|
24
|
+
masterTape.connect(masterSat).connect(masterOut).connect(destination);
|
|
25
|
+
const bus = ctx.createGain();
|
|
26
|
+
bus.gain.value = 1;
|
|
27
|
+
bus.connect(masterTape);
|
|
28
|
+
const ductable = ctx.createGain();
|
|
29
|
+
ductable.gain.value = 1;
|
|
30
|
+
ductable.connect(bus);
|
|
31
|
+
const drumBus = ctx.createGain();
|
|
32
|
+
drumBus.gain.value = 1;
|
|
33
|
+
drumBus.connect(bus);
|
|
34
|
+
const dubSend = ctx.createGain();
|
|
35
|
+
dubSend.gain.value = 0;
|
|
36
|
+
const delayL = ctx.createDelay(delay.maxSec);
|
|
37
|
+
const delayR = ctx.createDelay(delay.maxSec);
|
|
38
|
+
delayL.delayTime.value = delay.timeL;
|
|
39
|
+
delayR.delayTime.value = delay.timeR;
|
|
40
|
+
const fbL = ctx.createGain();
|
|
41
|
+
const fbR = ctx.createGain();
|
|
42
|
+
fbL.gain.value = delay.feedback;
|
|
43
|
+
fbR.gain.value = delay.feedback;
|
|
44
|
+
const dubHP = ctx.createBiquadFilter();
|
|
45
|
+
dubHP.type = "highpass";
|
|
46
|
+
dubHP.frequency.value = delay.hpHz;
|
|
47
|
+
const dubLP = ctx.createBiquadFilter();
|
|
48
|
+
dubLP.type = "lowpass";
|
|
49
|
+
dubLP.frequency.value = delay.lpHz;
|
|
50
|
+
dubLP.Q.value = 0.7;
|
|
51
|
+
const dubReturn = ctx.createGain();
|
|
52
|
+
dubReturn.gain.value = delay.returnGain;
|
|
53
|
+
dubSend.connect(delayL);
|
|
54
|
+
dubSend.connect(delayR);
|
|
55
|
+
delayL.connect(fbL);
|
|
56
|
+
fbL.connect(delayR);
|
|
57
|
+
delayR.connect(fbR);
|
|
58
|
+
fbR.connect(delayL);
|
|
59
|
+
delayL.connect(dubHP);
|
|
60
|
+
delayR.connect(dubHP);
|
|
61
|
+
dubHP.connect(dubLP).connect(dubReturn).connect(ductable);
|
|
62
|
+
return {
|
|
63
|
+
ductable,
|
|
64
|
+
drumBus,
|
|
65
|
+
dubSend,
|
|
66
|
+
duck(when) {
|
|
67
|
+
ductable.gain.cancelScheduledValues(when);
|
|
68
|
+
ductable.gain.setValueAtTime(ductable.gain.value, when);
|
|
69
|
+
ductable.gain.linearRampToValueAtTime(duck.min, when + duck.attackSec);
|
|
70
|
+
ductable.gain.linearRampToValueAtTime(1, when + duck.recoverSec);
|
|
71
|
+
},
|
|
72
|
+
open(start, totalSec) {
|
|
73
|
+
masterOut.gain.setValueAtTime(1e-4, start);
|
|
74
|
+
masterOut.gain.linearRampToValueAtTime(1, start + fade.inSec);
|
|
75
|
+
masterOut.gain.setValueAtTime(1, start + totalSec - fade.outLeadSec);
|
|
76
|
+
masterOut.gain.exponentialRampToValueAtTime(1e-4, start + totalSec);
|
|
77
|
+
ductable.gain.setValueAtTime(1, start);
|
|
78
|
+
},
|
|
79
|
+
dispose() {
|
|
80
|
+
const now = ctx.currentTime;
|
|
81
|
+
try {
|
|
82
|
+
masterOut.gain.cancelScheduledValues(now);
|
|
83
|
+
masterOut.gain.setValueAtTime(masterOut.gain.value, now);
|
|
84
|
+
masterOut.gain.exponentialRampToValueAtTime(1e-4, now + 0.2);
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
window.setTimeout(() => {
|
|
88
|
+
try {
|
|
89
|
+
bus.disconnect();
|
|
90
|
+
ductable.disconnect();
|
|
91
|
+
drumBus.disconnect();
|
|
92
|
+
dubSend.disconnect();
|
|
93
|
+
delayL.disconnect();
|
|
94
|
+
delayR.disconnect();
|
|
95
|
+
fbL.disconnect();
|
|
96
|
+
fbR.disconnect();
|
|
97
|
+
dubHP.disconnect();
|
|
98
|
+
dubLP.disconnect();
|
|
99
|
+
dubReturn.disconnect();
|
|
100
|
+
masterTape.disconnect();
|
|
101
|
+
masterSat.disconnect();
|
|
102
|
+
masterOut.disconnect();
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}, 250);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/dub/chambers.ts
|
|
111
|
+
var DUB_RELAY_CHAMBER = {
|
|
112
|
+
master: { lowpassHz: 12e3, satDrive: 2.4 },
|
|
113
|
+
fade: { inSec: 2.5, outLeadSec: 8 },
|
|
114
|
+
duck: { min: 0.7, attackSec: 0.018, recoverSec: 0.22 },
|
|
115
|
+
delay: {
|
|
116
|
+
maxSec: 1,
|
|
117
|
+
timeL: 0.3125,
|
|
118
|
+
// dotted 8th @ 144 BPM
|
|
119
|
+
timeR: 0.4167,
|
|
120
|
+
// quarter
|
|
121
|
+
feedback: 0.55,
|
|
122
|
+
hpHz: 220,
|
|
123
|
+
lpHz: 1800,
|
|
124
|
+
returnGain: 0.9
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var DUB_HOLY_CHAMBER = {
|
|
128
|
+
master: { lowpassHz: 14e3, satDrive: 2 },
|
|
129
|
+
fade: { inSec: 4, outLeadSec: 10 },
|
|
130
|
+
duck: { min: 0.55, attackSec: 0.02, recoverSec: 0.45 },
|
|
131
|
+
delay: {
|
|
132
|
+
maxSec: 2,
|
|
133
|
+
timeL: 0.6429,
|
|
134
|
+
// dotted 8th @ 70 BPM
|
|
135
|
+
timeR: 0.857,
|
|
136
|
+
// quarter
|
|
137
|
+
feedback: 0.62,
|
|
138
|
+
hpHz: 180,
|
|
139
|
+
lpHz: 1500,
|
|
140
|
+
returnGain: 1.1
|
|
141
|
+
// dub-relay 0.9 보다 두껍게
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var DUB_POWER_CHAMBER = {
|
|
145
|
+
master: { lowpassHz: 12e3, satDrive: 2.2 },
|
|
146
|
+
fade: { inSec: 3, outLeadSec: 10 },
|
|
147
|
+
duck: { min: 0.55, attackSec: 0.022, recoverSec: 0.32 },
|
|
148
|
+
delay: {
|
|
149
|
+
maxSec: 1.5,
|
|
150
|
+
timeL: 0.5769,
|
|
151
|
+
// dotted 8th @ 78 BPM
|
|
152
|
+
timeR: 0.7692,
|
|
153
|
+
// quarter
|
|
154
|
+
feedback: 0.6,
|
|
155
|
+
hpHz: 240,
|
|
156
|
+
lpHz: 2e3,
|
|
157
|
+
returnGain: 0.95
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var CRUSH_CHAMBER = {
|
|
161
|
+
master: { lowpassHz: 8e3, satDrive: 2.6 },
|
|
162
|
+
fade: { inSec: 4, outLeadSec: 12 },
|
|
163
|
+
duck: { min: 0.6, attackSec: 0.02, recoverSec: 0.3 },
|
|
164
|
+
delay: {
|
|
165
|
+
maxSec: 2,
|
|
166
|
+
timeL: 0.6818,
|
|
167
|
+
// dotted 8th @ 66 BPM
|
|
168
|
+
timeR: 0.9091,
|
|
169
|
+
// quarter
|
|
170
|
+
feedback: 0.6,
|
|
171
|
+
hpHz: 200,
|
|
172
|
+
lpHz: 1600,
|
|
173
|
+
returnGain: 1
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
export {
|
|
177
|
+
CRUSH_CHAMBER,
|
|
178
|
+
DUB_HOLY_CHAMBER,
|
|
179
|
+
DUB_POWER_CHAMBER,
|
|
180
|
+
DUB_RELAY_CHAMBER,
|
|
181
|
+
createDubChamber
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/dub/chamber.ts","../../src/dub/chambers.ts"],"sourcesContent":["/**\n * Dub chamber — dub-relay·dub-holy·dub-power 가 글자 그대로 복제하던 *마스터/믹스 챔버* 를 한\n * 모듈로. drumkit·sunwall 과 같은 VST 모델: `createDubChamber` 은 *색-불문 챔버 엔진*,\n * `*_CHAMBER` 은 *패치*. 챔버는 *세 단의 직렬 믹스* 만 소유한다 —\n *\n * ① 사이드체인 ducking : kick 이 떨어지면 chord·pad·sub(=`ductable`)가 잠깐 눌린다.\n * drum/perc(=`drumBus`)는 우회해 *직격* 으로 친다.\n * ② dub delay return : cross-feedback stereo delay → HP+LP → return → ductable(덥 tail 도 호흡).\n * send 량의 *per-section automation* 은 트랙에 남는다(`dubSend` 노드만 노출).\n * ③ tape saturation : soft-clip(tanh) waveshaper → 마스터 lowpass → masterOut → destination.\n *\n * 챔버 밖(트랙)에 남는 것: voice 합성(kick·chord·pad·…), dubSend 의 section 곡선, 16th 시퀀스.\n * 16th lookahead 루프는 `../scheduler` 의 `runStepClock` 으로 따로 추출됐다.\n * 보존 기준: 노드 토폴로지·상수 그대로 — 순수 추출.\n */\n\n/** soft-clip(tanh) saturation. drive=k. drumkit 의 attackTo 처럼 챔버 공용 1줄. */\nfunction makeSaturationCurve(drive: number): Float32Array<ArrayBuffer> {\n const n = 1024\n const buffer = new ArrayBuffer(n * Float32Array.BYTES_PER_ELEMENT)\n const curve = new Float32Array(buffer)\n const denom = Math.tanh(drive)\n for (let i = 0; i < n; i++) {\n const x = (i / (n - 1)) * 2 - 1\n curve[i] = Math.tanh(drive * x) / denom\n }\n return curve\n}\n\n/** 마스터 단 — tape lowpass + soft-clip saturation. */\nexport interface MasterStage {\n /** tape lowpass 컷(Hz). relay/power 12k, holy 14k. */\n lowpassHz: number\n /** tanh saturation drive. relay 2.4, holy 2.0, power 2.2. */\n satDrive: number\n}\n\n/** 마스터 fade in/out envelope. */\nexport interface FadeStage {\n /** 0 → 1 까지 linear ramp 시간(초). */\n inSec: number\n /** 끝나기 `outLeadSec` 초 전부터 exp ramp → 0. */\n outLeadSec: number\n}\n\n/** kick 사이드체인 duck — ductable 을 눌렀다 복귀. */\nexport interface DuckStage {\n /** 눌리는 최저 gain. relay 0.7(완만), holy/power 0.55(깊게). */\n min: number\n /** min 도달 시간(초). */\n attackSec: number\n /** 1.0 복귀 완료 시간(초). half-time 일수록 길다. */\n recoverSec: number\n}\n\n/** dub delay return — cross-feedback stereo delay + HP/LP tail. */\nexport interface DelayStage {\n /** DelayNode maxDelayTime. */\n maxSec: number\n /** L delay(점음 8분), R delay(4분). */\n timeL: number\n timeR: number\n /** cross feedback gain. */\n feedback: number\n /** tail HP(sub 분리) / LP(dark dub). */\n hpHz: number\n lpHz: number\n /** return gain — 두께. */\n returnGain: number\n}\n\nexport interface DubChamberPreset {\n master: MasterStage\n fade: FadeStage\n duck: DuckStage\n delay: DelayStage\n}\n\nexport interface DubChamber {\n /** chord·pad·sub·reese 가 모이는 *ducked* bus. */\n readonly ductable: GainNode\n /** drum·perc 가 모이는 *duck 우회* bus. */\n readonly drumBus: GainNode\n /** dub delay send. 트랙이 section 별로 gain automation. */\n readonly dubSend: GainNode\n /** kick 마다 호출 — ductable 을 눌렀다 복귀(사이드체인). */\n duck(when: number): void\n /** 마스터 fade in/out 을 한 번에 스케줄. start=그리드 원점, totalSec=곡 길이. */\n open(start: number, totalSec: number): void\n /** teardown — 빠른 fade out 후 250 ms 뒤 챔버 노드 전부 disconnect. */\n dispose(): void\n}\n\n/**\n * dub 마스터/믹스 챔버를 만들어 핸들을 돌려준다. 트랙 voice 는 `ductable`·`drumBus`·`dubSend`\n * 에 연결하고, kick 마다 `duck(when)`, 시작에 `open(start, total)`, teardown 에 `dispose()`.\n *\n * @param destination 트랙 score 가 받은 최종 목적지(AudioContext.destination 등).\n */\nexport function createDubChamber(\n ctx: AudioContext,\n destination: AudioNode,\n preset: DubChamberPreset,\n): DubChamber {\n const { master, fade, duck, delay } = preset\n\n // ─── 마스터: tape lowpass ← saturation ← masterOut ← destination ──────────\n const masterTape = ctx.createBiquadFilter()\n masterTape.type = 'lowpass'\n masterTape.frequency.value = master.lowpassHz\n masterTape.Q.value = 0.5\n\n const masterSat = ctx.createWaveShaper()\n masterSat.curve = makeSaturationCurve(master.satDrive)\n masterSat.oversample = '4x'\n\n const masterOut = ctx.createGain()\n masterOut.gain.value = 0.0001 // 시작 mute, open() 에서 fade in\n\n masterTape.connect(masterSat).connect(masterOut).connect(destination)\n\n // sum bus — 모든 voice 가 (ductable/drumBus 경유로) 모인다.\n const bus = ctx.createGain()\n bus.gain.value = 1\n bus.connect(masterTape)\n\n // ductable — chord/pad/sub. kick 시 눌린다. drumBus — drum/perc, 우회 직결.\n const ductable = ctx.createGain()\n ductable.gain.value = 1\n ductable.connect(bus)\n\n const drumBus = ctx.createGain()\n drumBus.gain.value = 1\n drumBus.connect(bus)\n\n // ─── dub delay return — cross-feedback stereo ────────────────────────────\n const dubSend = ctx.createGain()\n dubSend.gain.value = 0 // 트랙이 section 별로 modulate\n\n const delayL = ctx.createDelay(delay.maxSec)\n const delayR = ctx.createDelay(delay.maxSec)\n delayL.delayTime.value = delay.timeL\n delayR.delayTime.value = delay.timeR\n\n const fbL = ctx.createGain()\n const fbR = ctx.createGain()\n fbL.gain.value = delay.feedback\n fbR.gain.value = delay.feedback\n\n const dubHP = ctx.createBiquadFilter()\n dubHP.type = 'highpass'\n dubHP.frequency.value = delay.hpHz\n const dubLP = ctx.createBiquadFilter()\n dubLP.type = 'lowpass'\n dubLP.frequency.value = delay.lpHz\n dubLP.Q.value = 0.7\n\n const dubReturn = ctx.createGain()\n dubReturn.gain.value = delay.returnGain\n\n // dubSend → delayL/R, cross feedback, 두 출력 → HP → LP → return → ductable(덥 tail 도 호흡).\n dubSend.connect(delayL)\n dubSend.connect(delayR)\n delayL.connect(fbL)\n fbL.connect(delayR)\n delayR.connect(fbR)\n fbR.connect(delayL)\n delayL.connect(dubHP)\n delayR.connect(dubHP)\n dubHP.connect(dubLP).connect(dubReturn).connect(ductable)\n\n return {\n ductable,\n drumBus,\n dubSend,\n\n duck(when) {\n ductable.gain.cancelScheduledValues(when)\n ductable.gain.setValueAtTime(ductable.gain.value, when)\n ductable.gain.linearRampToValueAtTime(duck.min, when + duck.attackSec)\n ductable.gain.linearRampToValueAtTime(1.0, when + duck.recoverSec)\n },\n\n open(start, totalSec) {\n masterOut.gain.setValueAtTime(0.0001, start)\n masterOut.gain.linearRampToValueAtTime(1.0, start + fade.inSec)\n masterOut.gain.setValueAtTime(1.0, start + totalSec - fade.outLeadSec)\n masterOut.gain.exponentialRampToValueAtTime(0.0001, start + totalSec)\n // 평시 1.0 — kick 마다 duck() 가 눌렀다 복귀.\n ductable.gain.setValueAtTime(1, start)\n },\n\n dispose() {\n // 빠른 fade out — abrupt stop 방지.\n const now = ctx.currentTime\n try {\n masterOut.gain.cancelScheduledValues(now)\n masterOut.gain.setValueAtTime(masterOut.gain.value, now)\n masterOut.gain.exponentialRampToValueAtTime(0.0001, now + 0.2)\n } catch {\n // 무해\n }\n // 250 ms 후 챔버 노드 전부 disconnect.\n window.setTimeout(() => {\n try {\n bus.disconnect()\n ductable.disconnect()\n drumBus.disconnect()\n dubSend.disconnect()\n delayL.disconnect()\n delayR.disconnect()\n fbL.disconnect()\n fbR.disconnect()\n dubHP.disconnect()\n dubLP.disconnect()\n dubReturn.disconnect()\n masterTape.disconnect()\n masterSat.disconnect()\n masterOut.disconnect()\n } catch {\n // 이미 정리됨\n }\n }, 250)\n },\n }\n}\n","/**\n * Dub chamber presets — *챔버 패치*. 트랙당 하나. 각 dub 트랙 인라인 마스터/믹스 챔버의 *들리는\n * 상수* 를 그대로 옮긴 것이라, 거의 같아 보여도 합치지 않는다 — *소리 보존* 이 기준.\n * dedup 이득은 엔진(`createDubChamber`)이지 preset 수가 아니다.\n *\n * delay time 은 *BPM 의 점음 8분/4분* 이라 트랙마다 다르다(144 / 70 / 78 BPM).\n */\n\nimport type { DubChamberPreset } from './chamber'\n\n/** dub-relay — 144 BPM. 얕은 duck(0.7, 베이스 *지속 압력*) + 짧은 313 ms echo. */\nexport const DUB_RELAY_CHAMBER: DubChamberPreset = {\n master: { lowpassHz: 12000, satDrive: 2.4 },\n fade: { inSec: 2.5, outLeadSec: 8 },\n duck: { min: 0.7, attackSec: 0.018, recoverSec: 0.22 },\n delay: {\n maxSec: 1.0,\n timeL: 0.3125, // dotted 8th @ 144 BPM\n timeR: 0.4167, // quarter\n feedback: 0.55,\n hpHz: 220,\n lpHz: 1800,\n returnGain: 0.9,\n },\n}\n\n/** dub-holy — 70 BPM half-time. *깊고 긴* duck(0.55 / 0.45 s) + 643 ms 두꺼운 echo. */\nexport const DUB_HOLY_CHAMBER: DubChamberPreset = {\n master: { lowpassHz: 14000, satDrive: 2.0 },\n fade: { inSec: 4.0, outLeadSec: 10 },\n duck: { min: 0.55, attackSec: 0.02, recoverSec: 0.45 },\n delay: {\n maxSec: 2.0,\n timeL: 0.6429, // dotted 8th @ 70 BPM\n timeR: 0.857, // quarter\n feedback: 0.62,\n hpHz: 180,\n lpHz: 1500,\n returnGain: 1.1, // dub-relay 0.9 보다 두껍게\n },\n}\n\n/** dub-power — 78 BPM. 깊고 긴 duck(0.55 / 0.32 s) + 577 ms *권위의 잔향*. */\nexport const DUB_POWER_CHAMBER: DubChamberPreset = {\n master: { lowpassHz: 12000, satDrive: 2.2 },\n fade: { inSec: 3.0, outLeadSec: 10 },\n duck: { min: 0.55, attackSec: 0.022, recoverSec: 0.32 },\n delay: {\n maxSec: 1.5,\n timeL: 0.5769, // dotted 8th @ 78 BPM\n timeR: 0.7692, // quarter\n feedback: 0.6,\n hpHz: 240,\n lpHz: 2000,\n returnGain: 0.95,\n },\n}\n\n/**\n * crush(22) — 66 BPM sludge. *세 엔진 동시 최대치* 의 마스터(벽 크레셴도 + 쇠 박 + 덥 잔향).\n * 가장 거친 saturation(2.6) + 가장 어두운 LP(8000) — 검은 벽의 거친 고역을 한 겹 더 깎는다.\n * duck 은 얕게(0.6) — *쇠* kick 이 벽을 뚫되 벽을 무너뜨리진 않게. 682 ms *짓누르는* dub tail.\n */\nexport const CRUSH_CHAMBER: DubChamberPreset = {\n master: { lowpassHz: 8000, satDrive: 2.6 },\n fade: { inSec: 4.0, outLeadSec: 12 },\n duck: { min: 0.6, attackSec: 0.02, recoverSec: 0.3 },\n delay: {\n maxSec: 2.0,\n timeL: 0.6818, // dotted 8th @ 66 BPM\n timeR: 0.9091, // quarter\n feedback: 0.6,\n hpHz: 200,\n lpHz: 1600,\n returnGain: 1.0,\n },\n}\n"],"mappings":";AAiBA,SAAS,oBAAoB,OAA0C;AACrE,QAAM,IAAI;AACV,QAAM,SAAS,IAAI,YAAY,IAAI,aAAa,iBAAiB;AACjE,QAAM,QAAQ,IAAI,aAAa,MAAM;AACrC,QAAM,QAAQ,KAAK,KAAK,KAAK;AAC7B,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAM,IAAK,KAAK,IAAI,KAAM,IAAI;AAC9B,UAAM,CAAC,IAAI,KAAK,KAAK,QAAQ,CAAC,IAAI;AAAA,EACpC;AACA,SAAO;AACT;AAwEO,SAAS,iBACd,KACA,aACA,QACY;AACZ,QAAM,EAAE,QAAQ,MAAM,MAAM,MAAM,IAAI;AAGtC,QAAM,aAAa,IAAI,mBAAmB;AAC1C,aAAW,OAAO;AAClB,aAAW,UAAU,QAAQ,OAAO;AACpC,aAAW,EAAE,QAAQ;AAErB,QAAM,YAAY,IAAI,iBAAiB;AACvC,YAAU,QAAQ,oBAAoB,OAAO,QAAQ;AACrD,YAAU,aAAa;AAEvB,QAAM,YAAY,IAAI,WAAW;AACjC,YAAU,KAAK,QAAQ;AAEvB,aAAW,QAAQ,SAAS,EAAE,QAAQ,SAAS,EAAE,QAAQ,WAAW;AAGpE,QAAM,MAAM,IAAI,WAAW;AAC3B,MAAI,KAAK,QAAQ;AACjB,MAAI,QAAQ,UAAU;AAGtB,QAAM,WAAW,IAAI,WAAW;AAChC,WAAS,KAAK,QAAQ;AACtB,WAAS,QAAQ,GAAG;AAEpB,QAAM,UAAU,IAAI,WAAW;AAC/B,UAAQ,KAAK,QAAQ;AACrB,UAAQ,QAAQ,GAAG;AAGnB,QAAM,UAAU,IAAI,WAAW;AAC/B,UAAQ,KAAK,QAAQ;AAErB,QAAM,SAAS,IAAI,YAAY,MAAM,MAAM;AAC3C,QAAM,SAAS,IAAI,YAAY,MAAM,MAAM;AAC3C,SAAO,UAAU,QAAQ,MAAM;AAC/B,SAAO,UAAU,QAAQ,MAAM;AAE/B,QAAM,MAAM,IAAI,WAAW;AAC3B,QAAM,MAAM,IAAI,WAAW;AAC3B,MAAI,KAAK,QAAQ,MAAM;AACvB,MAAI,KAAK,QAAQ,MAAM;AAEvB,QAAM,QAAQ,IAAI,mBAAmB;AACrC,QAAM,OAAO;AACb,QAAM,UAAU,QAAQ,MAAM;AAC9B,QAAM,QAAQ,IAAI,mBAAmB;AACrC,QAAM,OAAO;AACb,QAAM,UAAU,QAAQ,MAAM;AAC9B,QAAM,EAAE,QAAQ;AAEhB,QAAM,YAAY,IAAI,WAAW;AACjC,YAAU,KAAK,QAAQ,MAAM;AAG7B,UAAQ,QAAQ,MAAM;AACtB,UAAQ,QAAQ,MAAM;AACtB,SAAO,QAAQ,GAAG;AAClB,MAAI,QAAQ,MAAM;AAClB,SAAO,QAAQ,GAAG;AAClB,MAAI,QAAQ,MAAM;AAClB,SAAO,QAAQ,KAAK;AACpB,SAAO,QAAQ,KAAK;AACpB,QAAM,QAAQ,KAAK,EAAE,QAAQ,SAAS,EAAE,QAAQ,QAAQ;AAExD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IAEA,KAAK,MAAM;AACT,eAAS,KAAK,sBAAsB,IAAI;AACxC,eAAS,KAAK,eAAe,SAAS,KAAK,OAAO,IAAI;AACtD,eAAS,KAAK,wBAAwB,KAAK,KAAK,OAAO,KAAK,SAAS;AACrE,eAAS,KAAK,wBAAwB,GAAK,OAAO,KAAK,UAAU;AAAA,IACnE;AAAA,IAEA,KAAK,OAAO,UAAU;AACpB,gBAAU,KAAK,eAAe,MAAQ,KAAK;AAC3C,gBAAU,KAAK,wBAAwB,GAAK,QAAQ,KAAK,KAAK;AAC9D,gBAAU,KAAK,eAAe,GAAK,QAAQ,WAAW,KAAK,UAAU;AACrE,gBAAU,KAAK,6BAA6B,MAAQ,QAAQ,QAAQ;AAEpE,eAAS,KAAK,eAAe,GAAG,KAAK;AAAA,IACvC;AAAA,IAEA,UAAU;AAER,YAAM,MAAM,IAAI;AAChB,UAAI;AACF,kBAAU,KAAK,sBAAsB,GAAG;AACxC,kBAAU,KAAK,eAAe,UAAU,KAAK,OAAO,GAAG;AACvD,kBAAU,KAAK,6BAA6B,MAAQ,MAAM,GAAG;AAAA,MAC/D,QAAQ;AAAA,MAER;AAEA,aAAO,WAAW,MAAM;AACtB,YAAI;AACF,cAAI,WAAW;AACf,mBAAS,WAAW;AACpB,kBAAQ,WAAW;AACnB,kBAAQ,WAAW;AACnB,iBAAO,WAAW;AAClB,iBAAO,WAAW;AAClB,cAAI,WAAW;AACf,cAAI,WAAW;AACf,gBAAM,WAAW;AACjB,gBAAM,WAAW;AACjB,oBAAU,WAAW;AACrB,qBAAW,WAAW;AACtB,oBAAU,WAAW;AACrB,oBAAU,WAAW;AAAA,QACvB,QAAQ;AAAA,QAER;AAAA,MACF,GAAG,GAAG;AAAA,IACR;AAAA,EACF;AACF;;;ACtNO,IAAM,oBAAsC;AAAA,EACjD,QAAQ,EAAE,WAAW,MAAO,UAAU,IAAI;AAAA,EAC1C,MAAM,EAAE,OAAO,KAAK,YAAY,EAAE;AAAA,EAClC,MAAM,EAAE,KAAK,KAAK,WAAW,OAAO,YAAY,KAAK;AAAA,EACrD,OAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AAAA;AAAA,IACP,OAAO;AAAA;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,IACN,MAAM;AAAA,IACN,YAAY;AAAA,EACd;AACF;AAGO,IAAM,mBAAqC;AAAA,EAChD,QAAQ,EAAE,WAAW,MAAO,UAAU,EAAI;AAAA,EAC1C,MAAM,EAAE,OAAO,GAAK,YAAY,GAAG;AAAA,EACnC,MAAM,EAAE,KAAK,MAAM,WAAW,MAAM,YAAY,KAAK;AAAA,EACrD,OAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AAAA;AAAA,IACP,OAAO;AAAA;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,IACN,MAAM;AAAA,IACN,YAAY;AAAA;AAAA,EACd;AACF;AAGO,IAAM,oBAAsC;AAAA,EACjD,QAAQ,EAAE,WAAW,MAAO,UAAU,IAAI;AAAA,EAC1C,MAAM,EAAE,OAAO,GAAK,YAAY,GAAG;AAAA,EACnC,MAAM,EAAE,KAAK,MAAM,WAAW,OAAO,YAAY,KAAK;AAAA,EACtD,OAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AAAA;AAAA,IACP,OAAO;AAAA;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,IACN,MAAM;AAAA,IACN,YAAY;AAAA,EACd;AACF;AAOO,IAAM,gBAAkC;AAAA,EAC7C,QAAQ,EAAE,WAAW,KAAM,UAAU,IAAI;AAAA,EACzC,MAAM,EAAE,OAAO,GAAK,YAAY,GAAG;AAAA,EACnC,MAAM,EAAE,KAAK,KAAK,WAAW,MAAM,YAAY,IAAI;AAAA,EACnD,OAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AAAA;AAAA,IACP,OAAO;AAAA;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,IACN,MAAM;AAAA,IACN,YAAY;AAAA,EACd;AACF;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export { BassHandle, BassPreset, DUB_HOLY_BASS, DUB_POWER_BASS, DUB_RELAY_BASS, ReesePreset, SubPreset, createBass } from './bass/index.js';
|
|
2
|
+
export { StripHandle, StripPreset, createStrip } from './space/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Step clock — catalog 트랙들이 글자 그대로 복제하던 *lookahead 16th 스케줄러* 를 한 transport
|
|
6
|
+
* 헬퍼로. `setInterval` 의 jitter 없이 `AudioContext.currentTime` 위에 앞창(lookahead)을 열고
|
|
7
|
+
* 그 안에 든 step 만 *정확한 절대시간* 으로 schedule 한다.
|
|
8
|
+
*
|
|
9
|
+
* 색-불문 · 음색-불문 — 그저 `(stepIdx, when)` 을 박는다. *언제 무엇을* 은 `onStep` 콜백(트랙의
|
|
10
|
+
* 시퀀서)에 남는다. 보존 기준: 기존 트랙들의 `LOOKAHEAD = 0.1` · `TICK_MS = 25` · `while` 창
|
|
11
|
+
* 채우기 루프와 *동작 동일* — 순수 추출.
|
|
12
|
+
*/
|
|
13
|
+
interface StepClockOptions {
|
|
14
|
+
/** 그리드 원점(절대시간, 보통 ctx.currentTime + 0.1). step 0 의 시각. */
|
|
15
|
+
start: number;
|
|
16
|
+
/** step 간격(초). 16th = 60 / BPM / 4. */
|
|
17
|
+
step: number;
|
|
18
|
+
/** 마지막 step + 1. when 이 아니라 *개수* 로 끝을 막는다. */
|
|
19
|
+
totalSteps: number;
|
|
20
|
+
/** 창 안에 든 각 step 을 박는다 — 트랙의 시퀀서. */
|
|
21
|
+
onStep: (stepIdx: number, when: number) => void;
|
|
22
|
+
/** 앞창 길이(초). 기본 0.1. */
|
|
23
|
+
lookahead?: number;
|
|
24
|
+
/** tick 주기(ms). 기본 25. */
|
|
25
|
+
tickMs?: number;
|
|
26
|
+
}
|
|
27
|
+
interface StepClock {
|
|
28
|
+
/** interval 정지 + 이후 tick no-op. teardown 에서 호출. */
|
|
29
|
+
stop(): void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* lookahead 스텝 클럭을 돌린다. 즉시 첫 tick 을 한 번 박고, 이후 `tickMs` 마다 앞창을 채운다.
|
|
33
|
+
*/
|
|
34
|
+
declare function runStepClock(ctx: AudioContext, { start, step, totalSteps, onStep, lookahead, tickMs }: StepClockOptions): StepClock;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sunwall — *블랙게이즈 벽* 신스 엔진. COLDSURF SOUND 의 *벽* 어휘를 한 악기로.
|
|
38
|
+
*
|
|
39
|
+
* VST 모델: `createWall` 은 *색-불문 엔진*, `SUNWALL`·`SUBLIME_HATE_WALL`·`GRATIS_WALL`
|
|
40
|
+
* 은 *패치(preset)*. Batheday(11) 의 합성 골격을 그대로 떼어낸 것 —
|
|
41
|
+
*
|
|
42
|
+
* sawtooth 5 voice × 4 화음 톤
|
|
43
|
+
* → (per-voice pitch-bend LFO, 옵션)
|
|
44
|
+
* → WaveShaper(distortion curve)
|
|
45
|
+
* → HP / LP 톤 다듬기
|
|
46
|
+
* → tremolo LFO (sawtooth, gain 변조)
|
|
47
|
+
* → 트랙 master bus
|
|
48
|
+
*
|
|
49
|
+
* 시간에 따라 움직이는 값(디스토션 amount ramp·트레몰로 depth·pitch-bend depth·화음)은
|
|
50
|
+
* *트랙 시퀀서* 가 핸들 메서드로 구동한다. 엔진은 *정적 골격* 만 만든다. 소리 보존이 기준이라
|
|
51
|
+
* 트랙별 미세 상수(glide τ·LP cutoff·trem base)는 preset 으로 *그대로* 주입된다.
|
|
52
|
+
*
|
|
53
|
+
* 드럼·crash·pluck·pad 는 본 엔진 밖 — 트랙 인라인 (드럼은 후속 카드에서 별도 분리 예정).
|
|
54
|
+
*/
|
|
55
|
+
/** 디스토션 커브 — *벽* 톤. amount 가 클수록 거칠다. 세 트랙 공통 커브. */
|
|
56
|
+
declare function makeDistortionCurve(amount: number): Float32Array<ArrayBuffer>;
|
|
57
|
+
/** 트레몰로 LFO 설정. */
|
|
58
|
+
interface WallTrem {
|
|
59
|
+
/** LFO 파형. 세 트랙 'sawtooth'. */
|
|
60
|
+
type: OscillatorType;
|
|
61
|
+
/** LFO rate (Hz). 세 트랙 13. */
|
|
62
|
+
hz: number;
|
|
63
|
+
/** tremGain 의 base gain. batheday 0.62 / sublime·gratis 1.0. */
|
|
64
|
+
baseGain: number;
|
|
65
|
+
/** tremDepth 초기값. batheday -0.42(정적) / sublime·gratis 0(시퀀서가 ramp). */
|
|
66
|
+
initialDepth: number;
|
|
67
|
+
}
|
|
68
|
+
/** per-voice pitch-bend LFO (슈게이즈/블랙게이즈 계열의 *흔들리는 톤*). 없으면 bend 미생성. */
|
|
69
|
+
interface WallBend {
|
|
70
|
+
/** LFO rate 하한 (Hz). */
|
|
71
|
+
hzMin: number;
|
|
72
|
+
/** LFO rate 상한 (Hz). voice 마다 [hzMin, hzMax] 에서 무작위 — *동기 없는* wobble. */
|
|
73
|
+
hzMax: number;
|
|
74
|
+
}
|
|
75
|
+
/** 엔진 골격 preset — 색-불문 정적 파라미터. 화음(initialTones)은 런타임 주입. */
|
|
76
|
+
interface WallPreset {
|
|
77
|
+
/** voice 디튠 cents. 기본 [-12,-5,0,5,12]. */
|
|
78
|
+
detunes?: number[];
|
|
79
|
+
/** 각 voice osc → wallBus 사이 gain. 세 트랙 0.05. */
|
|
80
|
+
voiceGain?: number;
|
|
81
|
+
/** WaveShaper 전 부스트. 세 트랙 1.4. */
|
|
82
|
+
preGain?: number;
|
|
83
|
+
/** 디스토션 후 HP. 세 트랙 110. */
|
|
84
|
+
hpHz?: number;
|
|
85
|
+
/** 디스토션 후 LP. batheday 3800 / sublime·gratis 4200. */
|
|
86
|
+
lpHz?: number;
|
|
87
|
+
/** LP Q. 세 트랙 0.6. */
|
|
88
|
+
lpQ?: number;
|
|
89
|
+
/** setChord glide 시간상수. batheday 0.08 / sublime 0.12 / gratis 0.14. */
|
|
90
|
+
glideTau?: number;
|
|
91
|
+
/** WaveShaper 초기 amount. batheday 45(정적) / sublime·gratis 0. */
|
|
92
|
+
initialDistortion: number;
|
|
93
|
+
trem: WallTrem;
|
|
94
|
+
/** 있으면 voice 마다 pitch-bend LFO 생성. */
|
|
95
|
+
bend?: WallBend;
|
|
96
|
+
}
|
|
97
|
+
interface WallHandle {
|
|
98
|
+
/** 4 화음 톤(루트·3·5·옥타브) 으로 모든 voice glide. 색/모드는 트랙이 결정. */
|
|
99
|
+
setChord(tones: number[]): void;
|
|
100
|
+
/** WaveShaper amount 갱신 (curve 재할당). 0.5 미만 변화는 무시 — 잦은 재할당 방지. */
|
|
101
|
+
setDistortion(amount: number): void;
|
|
102
|
+
/** tremDepth 를 value 로 ramp (현재값 → value, rampSec). */
|
|
103
|
+
setTremDepth(value: number, rampSec: number): void;
|
|
104
|
+
/** per-voice pitch-bend depth 를 ±cents 로 ramp. bend 없으면 no-op. */
|
|
105
|
+
setBendDepth(cents: number, rampSec: number): void;
|
|
106
|
+
/** wallBus gain 을 target 으로 linear ramp (페이드 인/아웃 공용). */
|
|
107
|
+
rampGain(target: number, rampSec: number): void;
|
|
108
|
+
/** 모든 oscillator 정지 (idempotent). 트랙 autoStop 용. */
|
|
109
|
+
stop(): void;
|
|
110
|
+
/** stop + 전체 노드 disconnect. 트랙 cleanup 용. */
|
|
111
|
+
dispose(): void;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 벽 신스를 만들어 `bus` 에 연결하고 핸들을 돌려준다.
|
|
115
|
+
*
|
|
116
|
+
* @param initialTones 시작 화음의 4 톤(루트·3·5·옥타브). 트랙의 chordProg[0].tones.
|
|
117
|
+
*/
|
|
118
|
+
declare function createWall(ctx: AudioContext, bus: AudioNode, preset: WallPreset, initialTones: number[]): WallHandle;
|
|
119
|
+
/** Batheday(11) — *햇볕의 벽*. 블랙게이즈 계열 결. 정적 디스토션 45, bend 없음. */
|
|
120
|
+
declare const SUNWALL: WallPreset;
|
|
121
|
+
/** Sublime / Hate(12) — *두 얼굴의 벽*. 디스토션 0 → 60 ramp, bend 0.5–0.9 Hz. */
|
|
122
|
+
declare const SUBLIME_HATE_WALL: WallPreset;
|
|
123
|
+
/** Gratis(13) — *(ㄲ)거저의 벽*. 디스토션 0 → 60 → 0 왕복, bend 0.5–0.9 Hz. */
|
|
124
|
+
declare const GRATIS_WALL: WallPreset;
|
|
125
|
+
/**
|
|
126
|
+
* Blue Wall(17) — *흔들리는 벽*. 슈게이즈 계열의 *디튠 글라이드*.
|
|
127
|
+
*
|
|
128
|
+
* 다른 패치와 결이 다른 자리: 디스토션이 *덜 거칠고*(정적 25), tremolo 가 *픽 어택*(톱니 13Hz)
|
|
129
|
+
* 이 아니라 *몸 전체가 호흡하는* sine 5 Hz·얕은 depth, 그리고 더 느린 bend(0.4–0.8 Hz) 가 코드
|
|
130
|
+
* 톤 자체를 흔든다 — *합성으로 옮긴 pitch-bend bar*. bend depth 는 트랙 시퀀서가 ±15 cents 로
|
|
131
|
+
* ramp 해 *벽 자체* 를 곡의 주인공으로 세운다(synth-forward).
|
|
132
|
+
*/
|
|
133
|
+
declare const BLUEWALL: WallPreset;
|
|
134
|
+
/**
|
|
135
|
+
* White Wall(18) — *디스토션 없는 벽*. post-rock 계열의 *클린 swell wall*.
|
|
136
|
+
*
|
|
137
|
+
* 본 패치의 시험: 디스토션이 *0* 일 때도 합성 골격이 *벽* 으로 들리는가. 답은 *질량을 옮긴다* —
|
|
138
|
+
* 디스토션이 만들던 두께를 *voice 수* (5 → 8 디튠 × 4 화음 = 32 oscillator) 와 *넓은 디튠*
|
|
139
|
+
* (±18 cents) 로 보충한다. initialDistortion 0 의 커브는 거의 선형(x/3)이라 *harmonic* 을 더하지
|
|
140
|
+
* 않고 레벨만 다듬는다 — *클린* 한 saw 벽. tremolo 는 *어택 픽* (톱니 13Hz) 도 *호흡* (sine 5Hz)
|
|
141
|
+
* 도 아닌 *가장 느린* triangle 0.3Hz — 벽 전체가 *밀물처럼* 차오르고 빠진다. bend 없음(움직임은
|
|
142
|
+
* 디튠 자체로). *해가 진 후의 흰 벽* — 본 카탈로그 최초의 밝은 색.
|
|
143
|
+
*/
|
|
144
|
+
declare const WHITEWALL: WallPreset;
|
|
145
|
+
/**
|
|
146
|
+
* Black Wall(19) — *짓누르는 벽*. post-metal 계열의 *sludge wall*.
|
|
147
|
+
*
|
|
148
|
+
* [[WHITEWALL]] 이 디스토션을 *0* 으로 잠가 *정적(靜寂) 극* 을 시험했다면, 본 패치는 반대 끝 —
|
|
149
|
+
* 디스토션을 *세 트랙 중 가장 거칠게*(0 → 72 크레셴도) 밀어 *바디 극* 을 [[SUNWALL]](정적 45)
|
|
150
|
+
* 너머로 끌고 간다. 색의 무게는 *세 상수* 가 옮긴다: ① 가장 어두운 LP(3000 — White 5200·Blue
|
|
151
|
+
* 4200 보다 한참 닫힘)로 고역을 깎아 *검은* 톤, ② 가장 낮은 HP(80 — 저역을 더 남겨 *sub 까지
|
|
152
|
+
* 닿는 무게*), ③ *타이트한* 디튠(±10)으로 voice 를 흩지 않고 한 덩어리로 *뭉친다*. tremolo 는
|
|
153
|
+
* Batheday 의 톱니 13Hz *어택 픽* 도, White 의 triangle 0.3Hz *밀물* 도 아닌 *느린* sine 3Hz —
|
|
154
|
+
* 거대한 화음이 무게를 못 이겨 천천히 *heave* 하는 결. depth 는 시퀀서가 ramp(0 → -0.3).
|
|
155
|
+
* bend 없음 — 움직임은 *디스토션 크레셴도* 와 *sub 펄스* 가 만든다. *해도 달도 없는 검은 벽*.
|
|
156
|
+
*/
|
|
157
|
+
declare const BLACKWALL: WallPreset;
|
|
158
|
+
|
|
159
|
+
export { BLACKWALL, BLUEWALL, GRATIS_WALL, SUBLIME_HATE_WALL, SUNWALL, type StepClock, type StepClockOptions, WHITEWALL, type WallBend, type WallHandle, type WallPreset, type WallTrem, createWall, makeDistortionCurve, runStepClock };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DUB_HOLY_BASS,
|
|
3
|
+
DUB_POWER_BASS,
|
|
4
|
+
DUB_RELAY_BASS,
|
|
5
|
+
createBass
|
|
6
|
+
} from "./chunk-CNJ6DQV5.js";
|
|
7
|
+
import {
|
|
8
|
+
createStrip
|
|
9
|
+
} from "./chunk-CMHDHRZ6.js";
|
|
10
|
+
|
|
11
|
+
// src/scheduler.ts
|
|
12
|
+
function runStepClock(ctx, { start, step, totalSteps, onStep, lookahead = 0.1, tickMs = 25 }) {
|
|
13
|
+
let nextStep = 0;
|
|
14
|
+
let stopped = false;
|
|
15
|
+
const tick = () => {
|
|
16
|
+
if (stopped) return;
|
|
17
|
+
const horizon = ctx.currentTime + lookahead;
|
|
18
|
+
while (true) {
|
|
19
|
+
const when = start + nextStep * step;
|
|
20
|
+
if (when > horizon) break;
|
|
21
|
+
if (nextStep >= totalSteps) break;
|
|
22
|
+
onStep(nextStep, when);
|
|
23
|
+
nextStep++;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const intervalId = window.setInterval(tick, tickMs);
|
|
27
|
+
tick();
|
|
28
|
+
return {
|
|
29
|
+
stop() {
|
|
30
|
+
stopped = true;
|
|
31
|
+
window.clearInterval(intervalId);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/sunwall.ts
|
|
37
|
+
function makeDistortionCurve(amount) {
|
|
38
|
+
const n = 4096;
|
|
39
|
+
const buffer = new ArrayBuffer(n * Float32Array.BYTES_PER_ELEMENT);
|
|
40
|
+
const curve = new Float32Array(buffer);
|
|
41
|
+
const deg = Math.PI / 180;
|
|
42
|
+
for (let i = 0; i < n; i++) {
|
|
43
|
+
const x = i * 2 / n - 1;
|
|
44
|
+
curve[i] = (3 + amount) * x * 20 * deg / (Math.PI + amount * Math.abs(x));
|
|
45
|
+
}
|
|
46
|
+
return curve;
|
|
47
|
+
}
|
|
48
|
+
var DEFAULT_DETUNES = [-12, -5, 0, 5, 12];
|
|
49
|
+
function createWall(ctx, bus, preset, initialTones) {
|
|
50
|
+
const detunes = preset.detunes ?? DEFAULT_DETUNES;
|
|
51
|
+
const voiceGain = preset.voiceGain ?? 0.05;
|
|
52
|
+
const preGain = preset.preGain ?? 1.4;
|
|
53
|
+
const hpHz = preset.hpHz ?? 110;
|
|
54
|
+
const lpHz = preset.lpHz ?? 4200;
|
|
55
|
+
const lpQ = preset.lpQ ?? 0.6;
|
|
56
|
+
const glideTau = preset.glideTau ?? 0.12;
|
|
57
|
+
let currentTones = initialTones.slice();
|
|
58
|
+
const wallBus = ctx.createGain();
|
|
59
|
+
wallBus.gain.value = 1e-4;
|
|
60
|
+
const wallShaper = ctx.createWaveShaper();
|
|
61
|
+
wallShaper.curve = makeDistortionCurve(preset.initialDistortion);
|
|
62
|
+
wallShaper.oversample = "2x";
|
|
63
|
+
const wallPre = ctx.createGain();
|
|
64
|
+
wallPre.gain.value = preGain;
|
|
65
|
+
const wallHp = ctx.createBiquadFilter();
|
|
66
|
+
wallHp.type = "highpass";
|
|
67
|
+
wallHp.frequency.value = hpHz;
|
|
68
|
+
const wallLp = ctx.createBiquadFilter();
|
|
69
|
+
wallLp.type = "lowpass";
|
|
70
|
+
wallLp.frequency.value = lpHz;
|
|
71
|
+
wallLp.Q.value = lpQ;
|
|
72
|
+
const tremGain = ctx.createGain();
|
|
73
|
+
tremGain.gain.value = preset.trem.baseGain;
|
|
74
|
+
const tremLfo = ctx.createOscillator();
|
|
75
|
+
tremLfo.type = preset.trem.type;
|
|
76
|
+
tremLfo.frequency.value = preset.trem.hz;
|
|
77
|
+
const tremDepth = ctx.createGain();
|
|
78
|
+
tremDepth.gain.value = preset.trem.initialDepth;
|
|
79
|
+
tremLfo.connect(tremDepth).connect(tremGain.gain);
|
|
80
|
+
tremLfo.start();
|
|
81
|
+
wallBus.connect(wallPre).connect(wallShaper).connect(wallHp).connect(wallLp).connect(tremGain).connect(bus);
|
|
82
|
+
const voices = [];
|
|
83
|
+
for (let toneIdx = 0; toneIdx < 4; toneIdx++) {
|
|
84
|
+
for (const cents of detunes) {
|
|
85
|
+
const baseFreq = initialTones[toneIdx] * 2 ** (cents / 1200);
|
|
86
|
+
const osc = ctx.createOscillator();
|
|
87
|
+
osc.type = "sawtooth";
|
|
88
|
+
osc.frequency.value = baseFreq;
|
|
89
|
+
let bendLfo = null;
|
|
90
|
+
let bendDepth = null;
|
|
91
|
+
if (preset.bend) {
|
|
92
|
+
bendLfo = ctx.createOscillator();
|
|
93
|
+
bendLfo.type = "sine";
|
|
94
|
+
bendLfo.frequency.value = preset.bend.hzMin + Math.random() * (preset.bend.hzMax - preset.bend.hzMin);
|
|
95
|
+
bendDepth = ctx.createGain();
|
|
96
|
+
bendDepth.gain.value = 0;
|
|
97
|
+
bendLfo.connect(bendDepth).connect(osc.frequency);
|
|
98
|
+
bendLfo.start();
|
|
99
|
+
}
|
|
100
|
+
const gain = ctx.createGain();
|
|
101
|
+
gain.gain.value = voiceGain;
|
|
102
|
+
osc.connect(gain).connect(wallBus);
|
|
103
|
+
osc.start();
|
|
104
|
+
voices.push({ osc, gain, baseToneIdx: toneIdx, detuneCents: cents, bendLfo, bendDepth });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let lastAmount = preset.initialDistortion;
|
|
108
|
+
return {
|
|
109
|
+
setChord(tones) {
|
|
110
|
+
currentTones = tones.slice();
|
|
111
|
+
const t = ctx.currentTime;
|
|
112
|
+
for (const v of voices) {
|
|
113
|
+
const target = tones[v.baseToneIdx] * 2 ** (v.detuneCents / 1200);
|
|
114
|
+
v.osc.frequency.cancelScheduledValues(t);
|
|
115
|
+
v.osc.frequency.setTargetAtTime(target, t, glideTau);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
setDistortion(amount) {
|
|
119
|
+
if (Math.abs(amount - lastAmount) < 0.5) return;
|
|
120
|
+
lastAmount = amount;
|
|
121
|
+
wallShaper.curve = makeDistortionCurve(amount);
|
|
122
|
+
},
|
|
123
|
+
setTremDepth(value, rampSec) {
|
|
124
|
+
const t = ctx.currentTime;
|
|
125
|
+
tremDepth.gain.cancelScheduledValues(t);
|
|
126
|
+
tremDepth.gain.setValueAtTime(tremDepth.gain.value, t);
|
|
127
|
+
tremDepth.gain.linearRampToValueAtTime(value, t + rampSec);
|
|
128
|
+
},
|
|
129
|
+
setBendDepth(cents, rampSec) {
|
|
130
|
+
const t = ctx.currentTime;
|
|
131
|
+
for (const v of voices) {
|
|
132
|
+
if (!v.bendDepth) continue;
|
|
133
|
+
const base = currentTones[v.baseToneIdx];
|
|
134
|
+
const target = base * (cents / 1200);
|
|
135
|
+
v.bendDepth.gain.cancelScheduledValues(t);
|
|
136
|
+
v.bendDepth.gain.setTargetAtTime(target, t, rampSec);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
rampGain(target, rampSec) {
|
|
140
|
+
const t = ctx.currentTime;
|
|
141
|
+
wallBus.gain.cancelScheduledValues(t);
|
|
142
|
+
wallBus.gain.setValueAtTime(wallBus.gain.value, t);
|
|
143
|
+
wallBus.gain.linearRampToValueAtTime(target, t + rampSec);
|
|
144
|
+
},
|
|
145
|
+
stop() {
|
|
146
|
+
try {
|
|
147
|
+
tremLfo.stop();
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
for (const v of voices) {
|
|
151
|
+
try {
|
|
152
|
+
v.osc.stop();
|
|
153
|
+
v.bendLfo?.stop();
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
dispose() {
|
|
159
|
+
this.stop();
|
|
160
|
+
for (const v of voices) {
|
|
161
|
+
v.osc.disconnect();
|
|
162
|
+
v.gain.disconnect();
|
|
163
|
+
v.bendLfo?.disconnect();
|
|
164
|
+
v.bendDepth?.disconnect();
|
|
165
|
+
}
|
|
166
|
+
tremLfo.disconnect();
|
|
167
|
+
tremDepth.disconnect();
|
|
168
|
+
tremGain.disconnect();
|
|
169
|
+
wallBus.disconnect();
|
|
170
|
+
wallPre.disconnect();
|
|
171
|
+
wallShaper.disconnect();
|
|
172
|
+
wallHp.disconnect();
|
|
173
|
+
wallLp.disconnect();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
var SUNWALL = {
|
|
178
|
+
lpHz: 3800,
|
|
179
|
+
glideTau: 0.08,
|
|
180
|
+
initialDistortion: 45,
|
|
181
|
+
trem: { type: "sawtooth", hz: 13, baseGain: 0.62, initialDepth: -0.42 }
|
|
182
|
+
};
|
|
183
|
+
var SUBLIME_HATE_WALL = {
|
|
184
|
+
lpHz: 4200,
|
|
185
|
+
glideTau: 0.12,
|
|
186
|
+
initialDistortion: 0,
|
|
187
|
+
trem: { type: "sawtooth", hz: 13, baseGain: 1, initialDepth: 0 },
|
|
188
|
+
bend: { hzMin: 0.5, hzMax: 0.9 }
|
|
189
|
+
};
|
|
190
|
+
var GRATIS_WALL = {
|
|
191
|
+
lpHz: 4200,
|
|
192
|
+
glideTau: 0.14,
|
|
193
|
+
initialDistortion: 0,
|
|
194
|
+
trem: { type: "sawtooth", hz: 13, baseGain: 1, initialDepth: 0 },
|
|
195
|
+
bend: { hzMin: 0.5, hzMax: 0.9 }
|
|
196
|
+
};
|
|
197
|
+
var BLUEWALL = {
|
|
198
|
+
lpHz: 4200,
|
|
199
|
+
glideTau: 0.12,
|
|
200
|
+
initialDistortion: 25,
|
|
201
|
+
trem: { type: "sine", hz: 5, baseGain: 0.7, initialDepth: -0.18 },
|
|
202
|
+
bend: { hzMin: 0.4, hzMax: 0.8 }
|
|
203
|
+
};
|
|
204
|
+
var WHITEWALL = {
|
|
205
|
+
detunes: [-18, -11, -5, -2, 2, 5, 11, 18],
|
|
206
|
+
// 8 voice — 디스토션이 빠진 질량을 voice 수로 보충.
|
|
207
|
+
voiceGain: 0.032,
|
|
208
|
+
// 32 voice 합산 — 클리핑 회피로 voice 당 낮춤 (≈ 20 voice × 0.05 등가).
|
|
209
|
+
lpHz: 5200,
|
|
210
|
+
// 디스토션이 없어 고역이 거칠지 않다 — 더 열어 *공기* 를 남긴다.
|
|
211
|
+
glideTau: 0.2,
|
|
212
|
+
// 가장 느린 글라이드 — 화음이 *번지며* 바뀐다.
|
|
213
|
+
initialDistortion: 0,
|
|
214
|
+
trem: { type: "triangle", hz: 0.3, baseGain: 0.85, initialDepth: -0.22 }
|
|
215
|
+
};
|
|
216
|
+
var BLACKWALL = {
|
|
217
|
+
detunes: [-10, -4, 0, 4, 10],
|
|
218
|
+
// 타이트한 stack — sludge 의 *뭉친* 두께 (흩지 않는다).
|
|
219
|
+
voiceGain: 0.046,
|
|
220
|
+
// 디스토션이 가장 두꺼워 클리핑 회피로 voice 당 낮춤.
|
|
221
|
+
hpHz: 80,
|
|
222
|
+
// 가장 낮은 HP — 저역을 더 남겨 *sub 까지 닿는 무게*.
|
|
223
|
+
lpHz: 3e3,
|
|
224
|
+
// 가장 어두운 LP — *검은* 벽 (고역을 깎는다).
|
|
225
|
+
glideTau: 0.1,
|
|
226
|
+
initialDistortion: 0,
|
|
227
|
+
// sludge 크레셴도 — 시퀀서가 0 → 72 로 ramp.
|
|
228
|
+
trem: { type: "sine", hz: 3, baseGain: 0.85, initialDepth: 0 }
|
|
229
|
+
};
|
|
230
|
+
export {
|
|
231
|
+
BLACKWALL,
|
|
232
|
+
BLUEWALL,
|
|
233
|
+
DUB_HOLY_BASS,
|
|
234
|
+
DUB_POWER_BASS,
|
|
235
|
+
DUB_RELAY_BASS,
|
|
236
|
+
GRATIS_WALL,
|
|
237
|
+
SUBLIME_HATE_WALL,
|
|
238
|
+
SUNWALL,
|
|
239
|
+
WHITEWALL,
|
|
240
|
+
createBass,
|
|
241
|
+
createStrip,
|
|
242
|
+
createWall,
|
|
243
|
+
makeDistortionCurve,
|
|
244
|
+
runStepClock
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scheduler.ts","../src/sunwall.ts"],"sourcesContent":["/**\n * Step clock — catalog 트랙들이 글자 그대로 복제하던 *lookahead 16th 스케줄러* 를 한 transport\n * 헬퍼로. `setInterval` 의 jitter 없이 `AudioContext.currentTime` 위에 앞창(lookahead)을 열고\n * 그 안에 든 step 만 *정확한 절대시간* 으로 schedule 한다.\n *\n * 색-불문 · 음색-불문 — 그저 `(stepIdx, when)` 을 박는다. *언제 무엇을* 은 `onStep` 콜백(트랙의\n * 시퀀서)에 남는다. 보존 기준: 기존 트랙들의 `LOOKAHEAD = 0.1` · `TICK_MS = 25` · `while` 창\n * 채우기 루프와 *동작 동일* — 순수 추출.\n */\n\nexport interface StepClockOptions {\n /** 그리드 원점(절대시간, 보통 ctx.currentTime + 0.1). step 0 의 시각. */\n start: number\n /** step 간격(초). 16th = 60 / BPM / 4. */\n step: number\n /** 마지막 step + 1. when 이 아니라 *개수* 로 끝을 막는다. */\n totalSteps: number\n /** 창 안에 든 각 step 을 박는다 — 트랙의 시퀀서. */\n onStep: (stepIdx: number, when: number) => void\n /** 앞창 길이(초). 기본 0.1. */\n lookahead?: number\n /** tick 주기(ms). 기본 25. */\n tickMs?: number\n}\n\nexport interface StepClock {\n /** interval 정지 + 이후 tick no-op. teardown 에서 호출. */\n stop(): void\n}\n\n/**\n * lookahead 스텝 클럭을 돌린다. 즉시 첫 tick 을 한 번 박고, 이후 `tickMs` 마다 앞창을 채운다.\n */\nexport function runStepClock(\n ctx: AudioContext,\n { start, step, totalSteps, onStep, lookahead = 0.1, tickMs = 25 }: StepClockOptions,\n): StepClock {\n let nextStep = 0\n let stopped = false\n\n const tick = () => {\n if (stopped) return\n const horizon = ctx.currentTime + lookahead\n while (true) {\n const when = start + nextStep * step\n if (when > horizon) break\n if (nextStep >= totalSteps) break\n onStep(nextStep, when)\n nextStep++\n }\n }\n\n const intervalId = window.setInterval(tick, tickMs)\n tick()\n\n return {\n stop() {\n stopped = true\n window.clearInterval(intervalId)\n },\n }\n}\n","/**\n * Sunwall — *블랙게이즈 벽* 신스 엔진. COLDSURF SOUND 의 *벽* 어휘를 한 악기로.\n *\n * VST 모델: `createWall` 은 *색-불문 엔진*, `SUNWALL`·`SUBLIME_HATE_WALL`·`GRATIS_WALL`\n * 은 *패치(preset)*. Batheday(11) 의 합성 골격을 그대로 떼어낸 것 —\n *\n * sawtooth 5 voice × 4 화음 톤\n * → (per-voice pitch-bend LFO, 옵션)\n * → WaveShaper(distortion curve)\n * → HP / LP 톤 다듬기\n * → tremolo LFO (sawtooth, gain 변조)\n * → 트랙 master bus\n *\n * 시간에 따라 움직이는 값(디스토션 amount ramp·트레몰로 depth·pitch-bend depth·화음)은\n * *트랙 시퀀서* 가 핸들 메서드로 구동한다. 엔진은 *정적 골격* 만 만든다. 소리 보존이 기준이라\n * 트랙별 미세 상수(glide τ·LP cutoff·trem base)는 preset 으로 *그대로* 주입된다.\n *\n * 드럼·crash·pluck·pad 는 본 엔진 밖 — 트랙 인라인 (드럼은 후속 카드에서 별도 분리 예정).\n */\n\n/** 디스토션 커브 — *벽* 톤. amount 가 클수록 거칠다. 세 트랙 공통 커브. */\nexport function makeDistortionCurve(amount: number): Float32Array<ArrayBuffer> {\n const n = 4096\n const buffer = new ArrayBuffer(n * Float32Array.BYTES_PER_ELEMENT)\n const curve = new Float32Array(buffer)\n const deg = Math.PI / 180\n for (let i = 0; i < n; i++) {\n const x = (i * 2) / n - 1\n curve[i] = ((3 + amount) * x * 20 * deg) / (Math.PI + amount * Math.abs(x))\n }\n return curve\n}\n\n/** 트레몰로 LFO 설정. */\nexport interface WallTrem {\n /** LFO 파형. 세 트랙 'sawtooth'. */\n type: OscillatorType\n /** LFO rate (Hz). 세 트랙 13. */\n hz: number\n /** tremGain 의 base gain. batheday 0.62 / sublime·gratis 1.0. */\n baseGain: number\n /** tremDepth 초기값. batheday -0.42(정적) / sublime·gratis 0(시퀀서가 ramp). */\n initialDepth: number\n}\n\n/** per-voice pitch-bend LFO (슈게이즈/블랙게이즈 계열의 *흔들리는 톤*). 없으면 bend 미생성. */\nexport interface WallBend {\n /** LFO rate 하한 (Hz). */\n hzMin: number\n /** LFO rate 상한 (Hz). voice 마다 [hzMin, hzMax] 에서 무작위 — *동기 없는* wobble. */\n hzMax: number\n}\n\n/** 엔진 골격 preset — 색-불문 정적 파라미터. 화음(initialTones)은 런타임 주입. */\nexport interface WallPreset {\n /** voice 디튠 cents. 기본 [-12,-5,0,5,12]. */\n detunes?: number[]\n /** 각 voice osc → wallBus 사이 gain. 세 트랙 0.05. */\n voiceGain?: number\n /** WaveShaper 전 부스트. 세 트랙 1.4. */\n preGain?: number\n /** 디스토션 후 HP. 세 트랙 110. */\n hpHz?: number\n /** 디스토션 후 LP. batheday 3800 / sublime·gratis 4200. */\n lpHz?: number\n /** LP Q. 세 트랙 0.6. */\n lpQ?: number\n /** setChord glide 시간상수. batheday 0.08 / sublime 0.12 / gratis 0.14. */\n glideTau?: number\n /** WaveShaper 초기 amount. batheday 45(정적) / sublime·gratis 0. */\n initialDistortion: number\n trem: WallTrem\n /** 있으면 voice 마다 pitch-bend LFO 생성. */\n bend?: WallBend\n}\n\nexport interface WallHandle {\n /** 4 화음 톤(루트·3·5·옥타브) 으로 모든 voice glide. 색/모드는 트랙이 결정. */\n setChord(tones: number[]): void\n /** WaveShaper amount 갱신 (curve 재할당). 0.5 미만 변화는 무시 — 잦은 재할당 방지. */\n setDistortion(amount: number): void\n /** tremDepth 를 value 로 ramp (현재값 → value, rampSec). */\n setTremDepth(value: number, rampSec: number): void\n /** per-voice pitch-bend depth 를 ±cents 로 ramp. bend 없으면 no-op. */\n setBendDepth(cents: number, rampSec: number): void\n /** wallBus gain 을 target 으로 linear ramp (페이드 인/아웃 공용). */\n rampGain(target: number, rampSec: number): void\n /** 모든 oscillator 정지 (idempotent). 트랙 autoStop 용. */\n stop(): void\n /** stop + 전체 노드 disconnect. 트랙 cleanup 용. */\n dispose(): void\n}\n\ninterface WallVoice {\n osc: OscillatorNode\n gain: GainNode\n baseToneIdx: number\n detuneCents: number\n bendLfo: OscillatorNode | null\n bendDepth: GainNode | null\n}\n\nconst DEFAULT_DETUNES = [-12, -5, 0, 5, 12]\n\n/**\n * 벽 신스를 만들어 `bus` 에 연결하고 핸들을 돌려준다.\n *\n * @param initialTones 시작 화음의 4 톤(루트·3·5·옥타브). 트랙의 chordProg[0].tones.\n */\nexport function createWall(\n ctx: AudioContext,\n bus: AudioNode,\n preset: WallPreset,\n initialTones: number[],\n): WallHandle {\n const detunes = preset.detunes ?? DEFAULT_DETUNES\n const voiceGain = preset.voiceGain ?? 0.05\n const preGain = preset.preGain ?? 1.4\n const hpHz = preset.hpHz ?? 110\n const lpHz = preset.lpHz ?? 4200\n const lpQ = preset.lpQ ?? 0.6\n const glideTau = preset.glideTau ?? 0.12\n\n // 현재 화음 톤 — setBendDepth 가 voice 의 base freq 를 알아야 ±cents 가 정확.\n let currentTones = initialTones.slice()\n\n const wallBus = ctx.createGain()\n wallBus.gain.value = 0.0001 // 트랙 시퀀서가 페이드인.\n\n const wallShaper = ctx.createWaveShaper()\n wallShaper.curve = makeDistortionCurve(preset.initialDistortion)\n wallShaper.oversample = '2x'\n\n const wallPre = ctx.createGain()\n wallPre.gain.value = preGain\n\n const wallHp = ctx.createBiquadFilter()\n wallHp.type = 'highpass'\n wallHp.frequency.value = hpHz\n const wallLp = ctx.createBiquadFilter()\n wallLp.type = 'lowpass'\n wallLp.frequency.value = lpHz\n wallLp.Q.value = lpQ\n\n const tremGain = ctx.createGain()\n tremGain.gain.value = preset.trem.baseGain\n const tremLfo = ctx.createOscillator()\n tremLfo.type = preset.trem.type\n tremLfo.frequency.value = preset.trem.hz\n const tremDepth = ctx.createGain()\n tremDepth.gain.value = preset.trem.initialDepth\n tremLfo.connect(tremDepth).connect(tremGain.gain)\n tremLfo.start()\n\n wallBus\n .connect(wallPre)\n .connect(wallShaper)\n .connect(wallHp)\n .connect(wallLp)\n .connect(tremGain)\n .connect(bus)\n\n const voices: WallVoice[] = []\n for (let toneIdx = 0; toneIdx < 4; toneIdx++) {\n for (const cents of detunes) {\n const baseFreq = initialTones[toneIdx] * 2 ** (cents / 1200)\n const osc = ctx.createOscillator()\n osc.type = 'sawtooth'\n osc.frequency.value = baseFreq\n\n let bendLfo: OscillatorNode | null = null\n let bendDepth: GainNode | null = null\n if (preset.bend) {\n bendLfo = ctx.createOscillator()\n bendLfo.type = 'sine'\n bendLfo.frequency.value =\n preset.bend.hzMin + Math.random() * (preset.bend.hzMax - preset.bend.hzMin)\n bendDepth = ctx.createGain()\n bendDepth.gain.value = 0 // 트랙 시퀀서가 ramp.\n bendLfo.connect(bendDepth).connect(osc.frequency)\n bendLfo.start()\n }\n\n const gain = ctx.createGain()\n gain.gain.value = voiceGain\n osc.connect(gain).connect(wallBus)\n osc.start()\n voices.push({ osc, gain, baseToneIdx: toneIdx, detuneCents: cents, bendLfo, bendDepth })\n }\n }\n\n let lastAmount = preset.initialDistortion\n\n return {\n setChord(tones: number[]) {\n currentTones = tones.slice()\n const t = ctx.currentTime\n for (const v of voices) {\n const target = tones[v.baseToneIdx] * 2 ** (v.detuneCents / 1200)\n v.osc.frequency.cancelScheduledValues(t)\n v.osc.frequency.setTargetAtTime(target, t, glideTau)\n }\n },\n\n setDistortion(amount: number) {\n if (Math.abs(amount - lastAmount) < 0.5) return\n lastAmount = amount\n wallShaper.curve = makeDistortionCurve(amount)\n },\n\n setTremDepth(value: number, rampSec: number) {\n const t = ctx.currentTime\n tremDepth.gain.cancelScheduledValues(t)\n tremDepth.gain.setValueAtTime(tremDepth.gain.value, t)\n tremDepth.gain.linearRampToValueAtTime(value, t + rampSec)\n },\n\n setBendDepth(cents: number, rampSec: number) {\n const t = ctx.currentTime\n for (const v of voices) {\n if (!v.bendDepth) continue\n const base = currentTones[v.baseToneIdx]\n const target = base * (cents / 1200)\n v.bendDepth.gain.cancelScheduledValues(t)\n v.bendDepth.gain.setTargetAtTime(target, t, rampSec)\n }\n },\n\n rampGain(target: number, rampSec: number) {\n const t = ctx.currentTime\n wallBus.gain.cancelScheduledValues(t)\n wallBus.gain.setValueAtTime(wallBus.gain.value, t)\n wallBus.gain.linearRampToValueAtTime(target, t + rampSec)\n },\n\n stop() {\n try {\n tremLfo.stop()\n } catch {\n // 이미 정지 — 무해.\n }\n for (const v of voices) {\n try {\n v.osc.stop()\n v.bendLfo?.stop()\n } catch {\n // 이미 정지 — 무해.\n }\n }\n },\n\n dispose() {\n this.stop()\n for (const v of voices) {\n v.osc.disconnect()\n v.gain.disconnect()\n v.bendLfo?.disconnect()\n v.bendDepth?.disconnect()\n }\n tremLfo.disconnect()\n tremDepth.disconnect()\n tremGain.disconnect()\n wallBus.disconnect()\n wallPre.disconnect()\n wallShaper.disconnect()\n wallHp.disconnect()\n wallLp.disconnect()\n },\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Presets — 색 패치. 정적 골격만; 시간 변주는 트랙 시퀀서가 핸들로 구동.\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Batheday(11) — *햇볕의 벽*. 블랙게이즈 계열 결. 정적 디스토션 45, bend 없음. */\nexport const SUNWALL: WallPreset = {\n lpHz: 3800,\n glideTau: 0.08,\n initialDistortion: 45,\n trem: { type: 'sawtooth', hz: 13, baseGain: 0.62, initialDepth: -0.42 },\n}\n\n/** Sublime / Hate(12) — *두 얼굴의 벽*. 디스토션 0 → 60 ramp, bend 0.5–0.9 Hz. */\nexport const SUBLIME_HATE_WALL: WallPreset = {\n lpHz: 4200,\n glideTau: 0.12,\n initialDistortion: 0,\n trem: { type: 'sawtooth', hz: 13, baseGain: 1.0, initialDepth: 0 },\n bend: { hzMin: 0.5, hzMax: 0.9 },\n}\n\n/** Gratis(13) — *(ㄲ)거저의 벽*. 디스토션 0 → 60 → 0 왕복, bend 0.5–0.9 Hz. */\nexport const GRATIS_WALL: WallPreset = {\n lpHz: 4200,\n glideTau: 0.14,\n initialDistortion: 0,\n trem: { type: 'sawtooth', hz: 13, baseGain: 1.0, initialDepth: 0 },\n bend: { hzMin: 0.5, hzMax: 0.9 },\n}\n\n/**\n * Blue Wall(17) — *흔들리는 벽*. 슈게이즈 계열의 *디튠 글라이드*.\n *\n * 다른 패치와 결이 다른 자리: 디스토션이 *덜 거칠고*(정적 25), tremolo 가 *픽 어택*(톱니 13Hz)\n * 이 아니라 *몸 전체가 호흡하는* sine 5 Hz·얕은 depth, 그리고 더 느린 bend(0.4–0.8 Hz) 가 코드\n * 톤 자체를 흔든다 — *합성으로 옮긴 pitch-bend bar*. bend depth 는 트랙 시퀀서가 ±15 cents 로\n * ramp 해 *벽 자체* 를 곡의 주인공으로 세운다(synth-forward).\n */\nexport const BLUEWALL: WallPreset = {\n lpHz: 4200,\n glideTau: 0.12,\n initialDistortion: 25,\n trem: { type: 'sine', hz: 5, baseGain: 0.7, initialDepth: -0.18 },\n bend: { hzMin: 0.4, hzMax: 0.8 },\n}\n\n/**\n * White Wall(18) — *디스토션 없는 벽*. post-rock 계열의 *클린 swell wall*.\n *\n * 본 패치의 시험: 디스토션이 *0* 일 때도 합성 골격이 *벽* 으로 들리는가. 답은 *질량을 옮긴다* —\n * 디스토션이 만들던 두께를 *voice 수* (5 → 8 디튠 × 4 화음 = 32 oscillator) 와 *넓은 디튠*\n * (±18 cents) 로 보충한다. initialDistortion 0 의 커브는 거의 선형(x/3)이라 *harmonic* 을 더하지\n * 않고 레벨만 다듬는다 — *클린* 한 saw 벽. tremolo 는 *어택 픽* (톱니 13Hz) 도 *호흡* (sine 5Hz)\n * 도 아닌 *가장 느린* triangle 0.3Hz — 벽 전체가 *밀물처럼* 차오르고 빠진다. bend 없음(움직임은\n * 디튠 자체로). *해가 진 후의 흰 벽* — 본 카탈로그 최초의 밝은 색.\n */\nexport const WHITEWALL: WallPreset = {\n detunes: [-18, -11, -5, -2, 2, 5, 11, 18], // 8 voice — 디스토션이 빠진 질량을 voice 수로 보충.\n voiceGain: 0.032, // 32 voice 합산 — 클리핑 회피로 voice 당 낮춤 (≈ 20 voice × 0.05 등가).\n lpHz: 5200, // 디스토션이 없어 고역이 거칠지 않다 — 더 열어 *공기* 를 남긴다.\n glideTau: 0.2, // 가장 느린 글라이드 — 화음이 *번지며* 바뀐다.\n initialDistortion: 0,\n trem: { type: 'triangle', hz: 0.3, baseGain: 0.85, initialDepth: -0.22 },\n}\n\n/**\n * Black Wall(19) — *짓누르는 벽*. post-metal 계열의 *sludge wall*.\n *\n * [[WHITEWALL]] 이 디스토션을 *0* 으로 잠가 *정적(靜寂) 극* 을 시험했다면, 본 패치는 반대 끝 —\n * 디스토션을 *세 트랙 중 가장 거칠게*(0 → 72 크레셴도) 밀어 *바디 극* 을 [[SUNWALL]](정적 45)\n * 너머로 끌고 간다. 색의 무게는 *세 상수* 가 옮긴다: ① 가장 어두운 LP(3000 — White 5200·Blue\n * 4200 보다 한참 닫힘)로 고역을 깎아 *검은* 톤, ② 가장 낮은 HP(80 — 저역을 더 남겨 *sub 까지\n * 닿는 무게*), ③ *타이트한* 디튠(±10)으로 voice 를 흩지 않고 한 덩어리로 *뭉친다*. tremolo 는\n * Batheday 의 톱니 13Hz *어택 픽* 도, White 의 triangle 0.3Hz *밀물* 도 아닌 *느린* sine 3Hz —\n * 거대한 화음이 무게를 못 이겨 천천히 *heave* 하는 결. depth 는 시퀀서가 ramp(0 → -0.3).\n * bend 없음 — 움직임은 *디스토션 크레셴도* 와 *sub 펄스* 가 만든다. *해도 달도 없는 검은 벽*.\n */\nexport const BLACKWALL: WallPreset = {\n detunes: [-10, -4, 0, 4, 10], // 타이트한 stack — sludge 의 *뭉친* 두께 (흩지 않는다).\n voiceGain: 0.046, // 디스토션이 가장 두꺼워 클리핑 회피로 voice 당 낮춤.\n hpHz: 80, // 가장 낮은 HP — 저역을 더 남겨 *sub 까지 닿는 무게*.\n lpHz: 3000, // 가장 어두운 LP — *검은* 벽 (고역을 깎는다).\n glideTau: 0.1,\n initialDistortion: 0, // sludge 크레셴도 — 시퀀서가 0 → 72 로 ramp.\n trem: { type: 'sine', hz: 3, baseGain: 0.85, initialDepth: 0 },\n}\n"],"mappings":"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","names":[]}
|