@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dong-Ho Choi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @coldsurf/synth-kit
|
|
2
|
+
|
|
3
|
+
Color-blind Web Audio synthesis engines, built for [COLDSURF Tape](https://coldsurf.io) — a generative music series produced entirely from code, no samples, no external audio.
|
|
4
|
+
|
|
5
|
+
## Mental model
|
|
6
|
+
|
|
7
|
+
Think VST: **one engine, many patches.**
|
|
8
|
+
|
|
9
|
+
- An **engine** (`createWall`, `createDrumKit`, `createBass`, `createDubChamber`, `createStrip`) knows nothing about "sound." It only accepts parameters.
|
|
10
|
+
- A **preset** is the color — a plain object of parameter values (`SUNWALL`, `BLUEWALL`, `DRY_KIT`, `DUB_HOLY_BASS`, ...).
|
|
11
|
+
- Every engine takes an `AudioContext` and a `bus: AudioNode` it connects into — nothing else. Compose engines by wiring their outputs into shared buses, channel strips, or a dub chamber.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createWall, SUNWALL, runStepClock } from '@coldsurf/synth-kit'
|
|
15
|
+
|
|
16
|
+
const ctx = new AudioContext()
|
|
17
|
+
const wall = createWall(ctx, ctx.destination, SUNWALL)
|
|
18
|
+
|
|
19
|
+
wall.fadeIn(0.8, 2)
|
|
20
|
+
wall.setChord(0)
|
|
21
|
+
|
|
22
|
+
// drive the time-varying parts (distortion ramp, tremolo depth) from your own clock
|
|
23
|
+
runStepClock(ctx, {
|
|
24
|
+
start: ctx.currentTime + 0.1,
|
|
25
|
+
step: 60 / 88 / 4, // 16th notes at 88 BPM
|
|
26
|
+
totalSteps: 64,
|
|
27
|
+
onStep: (_stepIdx, when) => {
|
|
28
|
+
wall.update(when - ctx.currentTime)
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Engines
|
|
34
|
+
|
|
35
|
+
| Engine | Import | Vocabulary | Built-in presets |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| Wall | `createWall` | sawtooth chord stack → WaveShaper distortion → tremolo LFO. Shoegaze/blackgaze/post-rock/post-metal walls. | `SUNWALL` · `BLUEWALL` · `WHITEWALL` · `BLACKWALL` · `SUBLIME_HATE_WALL` · `GRATIS_WALL` |
|
|
38
|
+
| Drumkit | `createDrumKit` (`./drumkit`) | kick (pitched body + optional sub/click) · snare (tone + noise, optional gated tails) · hat (filtered noise, open/closed). | `DRY_KIT` · `WET_KIT` · `IRON_KIT` · `BATHEDAY_KIT` · `SUBLIME_KIT` · `REVEAL_KIT` · `DUB_RELAY_KIT` · `DUB_HOLY_KIT` · `DUB_POWER_KIT` |
|
|
39
|
+
| Bass | `createBass` (`./bass`) | reese + sub layers. | `DUB_RELAY_BASS` · `DUB_HOLY_BASS` · `DUB_POWER_BASS` |
|
|
40
|
+
| Dub chamber | `createDubChamber` (`./dub`) | sidechain duck bus + cross-feedback stereo delay return + tape saturation → master. | `DUB_RELAY_CHAMBER` · `DUB_HOLY_CHAMBER` · `DUB_POWER_CHAMBER` · `CRUSH_CHAMBER` |
|
|
41
|
+
| Space | `createStrip` (`./space`) | per-instrument channel strip — pan + carve EQ (HP/LP/bell), opt-in only. | — (mixer, not an instrument) |
|
|
42
|
+
| Scheduler | `runStepClock` | color-blind lookahead step clock (`AudioContext.currentTime` based, no `setInterval` jitter). | — |
|
|
43
|
+
|
|
44
|
+
All engine handles expose `dispose()`. Instrument engines that hold time-varying parameters (wall's distortion/tremolo ramps) expose `update(elapsedSec)` — call it from your own clock (e.g. `runStepClock`).
|
|
45
|
+
|
|
46
|
+
## Design invariants
|
|
47
|
+
|
|
48
|
+
- **Zero runtime dependencies.** Pure Web Audio API + TypeScript.
|
|
49
|
+
- **Single-entry barrels.** Import from the package root or its `./drumkit` / `./bass` / `./dub` / `./space` subpaths only — no deep imports into internal files.
|
|
50
|
+
- **Engines never own scheduling, master fades, sidechain, or panning.** Those are the caller's responsibility — engines only build the node graph and expose handles.
|
|
51
|
+
- **Presets never merge "close enough" values.** Two presets that look nearly identical (e.g. two kick bodies 3 Hz apart) stay separate constants — the goal is preserving an exact, previously-shipped sound, not deduplicating numbers.
|
|
52
|
+
|
|
53
|
+
## Browser vs. offline rendering
|
|
54
|
+
|
|
55
|
+
This package is built and tested against real browser `AudioContext`. It does **not** currently ship an offline-rendering runtime.
|
|
56
|
+
|
|
57
|
+
If you plan to render score/patterns to a `.wav` file outside a browser (e.g. from a CLI or server process), be aware: some engines' time-varying parameters (the `wall` engine's distortion envelope, specifically) reassign `WaveShaperNode.curve` at runtime, which real browsers allow but [`node-web-audio-api`](https://github.com/ircam-ircam/node-web-audio-api) currently does not. Presets with a static (non-ramping) distortion value, and all `drumkit`/`bass`/`dub` engines, are unaffected. A dedicated offline-rendering story (headless-browser based, or an engine-level fix) is tracked separately — see the issues tab.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bass — *저역* 합성 엔진. dub 트랙들이 글자 그대로 복제하던 sub·reese 베이스 골격을
|
|
3
|
+
* 한 악기로. sunwall·drumkit 과 같은 VST 모델: `createBass` 는 *색-불문 엔진*,
|
|
4
|
+
* `*_BASS` 는 *패치*.
|
|
5
|
+
*
|
|
6
|
+
* sub : sine + 보조 osc(triangle/같은·살짝 detune) → AD-sustain env → bus
|
|
7
|
+
* reese : detuned saw × 2(한 옥타브 위) → resonant lowpass → tanh saturation → bus
|
|
8
|
+
*
|
|
9
|
+
* 엔진은 *voice 합성* 만 한다 — `(when, freq, dur, velocity)` 절대시간으로 `bus` 에 한 음을
|
|
10
|
+
* 그린다. 스케줄링(스텝 루프)·master fade·사이드체인 duck·dub send 는 *트랙* 의 책임이며
|
|
11
|
+
* 엔진 밖이다 (drumkit 과 동일한 정규화 기준). dub-holy 처럼 sub 만 쓰는 트랙은 preset 에서
|
|
12
|
+
* `reese` 를 비우면 `reese()` 가 no-op.
|
|
13
|
+
*
|
|
14
|
+
* 보존 기준: 들리는 envelope(attack·sustain·release)·필터·saturation·디튠은 원본 상수 그대로
|
|
15
|
+
* preset 으로 주입. 들리지 않는 것만 정규화 — saturation 커브 Float32Array 는 핸들당 한 번
|
|
16
|
+
* 계산해 음마다 재사용(내용 동일), ephemeral voice 노드의 지연 disconnect 는 drumkit 과 동일.
|
|
17
|
+
*
|
|
18
|
+
* 트랙 전용 저역(dig-it 의 motorik saw bass 등)은 rule-of-three 미달이라 트랙 인라인으로 남는다.
|
|
19
|
+
*/
|
|
20
|
+
/** sub 베이스 voice — sine + 보조 osc 의 sustained 저역 드론. */
|
|
21
|
+
interface SubPreset {
|
|
22
|
+
/** 보조 osc 파형. 세 트랙 'triangle'. */
|
|
23
|
+
secondType: OscillatorType;
|
|
24
|
+
/** 보조 osc freq 배율 — *맥놀이* detune. relay·power 1, holy 1.005. 기본 1. */
|
|
25
|
+
secondRatio?: number;
|
|
26
|
+
/** gain attack(0.0001 → velocity) 시간. relay 0.04 / power 0.05 / holy 0.08. */
|
|
27
|
+
attack: number;
|
|
28
|
+
/** sustain 종료~exp decay 구간 길이. hold 는 `when + dur - release` 까지. relay 0.12 / power 0.15 / holy 0.2. */
|
|
29
|
+
release: number;
|
|
30
|
+
}
|
|
31
|
+
/** reese 베이스 voice — *우글거리는* mid-bass(detuned saw → resonant LP → tanh sat). */
|
|
32
|
+
interface ReesePreset {
|
|
33
|
+
/** root 대비 옥타브 배율. 세 트랙 2(한 옥타브 위). 기본 2. */
|
|
34
|
+
octaveRatio?: number;
|
|
35
|
+
/** saw voice 디튠 ±cents. relay 8 / power 10. */
|
|
36
|
+
detuneCents: number;
|
|
37
|
+
/** lowpass cutoff(Hz). relay 380 / power 420. */
|
|
38
|
+
lpHz: number;
|
|
39
|
+
/** lowpass Q(resonance). relay 6 / power 5.5. */
|
|
40
|
+
lpQ: number;
|
|
41
|
+
/** tanh saturation drive. relay 3.5 / power 3.2. */
|
|
42
|
+
satK: number;
|
|
43
|
+
/** gain attack 시간. relay 0.012 / power 0.015. */
|
|
44
|
+
attack: number;
|
|
45
|
+
/** sustain 종료~exp decay 구간 길이. relay 0.05 / power 0.06. */
|
|
46
|
+
release: number;
|
|
47
|
+
}
|
|
48
|
+
interface BassPreset {
|
|
49
|
+
sub?: SubPreset;
|
|
50
|
+
reese?: ReesePreset;
|
|
51
|
+
}
|
|
52
|
+
interface BassHandle {
|
|
53
|
+
/** when 에 sub 한 음(dur 동안 sustain). preset.sub 없으면 no-op. */
|
|
54
|
+
sub(when: number, freq: number, dur: number, velocity: number): void;
|
|
55
|
+
/** when 에 reese 한 음. preset.reese 없으면 no-op. */
|
|
56
|
+
reese(when: number, freq: number, dur: number, velocity: number): void;
|
|
57
|
+
/** 공유 노드 정리. ephemeral voice 노드는 self-stop + 내부 타임아웃 disconnect. */
|
|
58
|
+
dispose(): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 베이스를 만들어 `bus` 로 voice 를 합성하는 핸들을 돌려준다.
|
|
62
|
+
*
|
|
63
|
+
* @param bus 각 음이 연결될 목적지(트랙의 ductable·master 등).
|
|
64
|
+
*/
|
|
65
|
+
declare function createBass(ctx: AudioContext, bus: AudioNode, preset: BassPreset): BassHandle;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Bass presets — *bass 패치*. 트랙당 하나. 각 dub 트랙 인라인 베이스의 *들리는 상수* 를 그대로
|
|
69
|
+
* 옮긴 것이라, 거의 같아 보여도(예: relay lp380/Q6 vs power lp420/Q5.5) 합치지 않는다 —
|
|
70
|
+
* *소리 보존* 이 기준. dedup 이득은 엔진(`createBass`)이지 preset 수가 아니다.
|
|
71
|
+
*
|
|
72
|
+
* dub-holy 는 reese 없이 sub 만 — *대성당의 드론* 은 sustained sub + organ pad 가 핵이라
|
|
73
|
+
* reese 의 *우글거림* 을 쓰지 않는다.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/** Dub the Wall(21) — *둥둥둥* 의 잔음. relay 의 sub(38–55 Hz)·reese(Berghain 결). */
|
|
77
|
+
declare const DUB_RELAY_BASS: BassPreset;
|
|
78
|
+
/** Dub Holy(20) — *대성당의 sub 드론*. 60–120 Hz, 보조 osc 살짝 detune(맥놀이). reese 없음. */
|
|
79
|
+
declare const DUB_HOLY_BASS: BassPreset;
|
|
80
|
+
/** Dub Power(22) — *peak 64.6 Hz* 의 sub + *권위의 우글거림* reese. */
|
|
81
|
+
declare const DUB_POWER_BASS: BassPreset;
|
|
82
|
+
|
|
83
|
+
export { type BassHandle, type BassPreset, DUB_HOLY_BASS, DUB_POWER_BASS, DUB_RELAY_BASS, type ReesePreset, type SubPreset, createBass };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/space/strip.ts
|
|
2
|
+
function createStrip(ctx, dest, preset) {
|
|
3
|
+
const input = ctx.createGain();
|
|
4
|
+
input.gain.value = preset.gain ?? 1;
|
|
5
|
+
const nodes = [input];
|
|
6
|
+
let tail = input;
|
|
7
|
+
const BUTTERWORTH_Q = Math.SQRT1_2;
|
|
8
|
+
if (preset.hpHz !== void 0) {
|
|
9
|
+
const hp = ctx.createBiquadFilter();
|
|
10
|
+
hp.type = "highpass";
|
|
11
|
+
hp.frequency.value = preset.hpHz;
|
|
12
|
+
hp.Q.value = preset.hpQ ?? BUTTERWORTH_Q;
|
|
13
|
+
tail.connect(hp);
|
|
14
|
+
nodes.push(hp);
|
|
15
|
+
tail = hp;
|
|
16
|
+
}
|
|
17
|
+
if (preset.lpHz !== void 0) {
|
|
18
|
+
const lp = ctx.createBiquadFilter();
|
|
19
|
+
lp.type = "lowpass";
|
|
20
|
+
lp.frequency.value = preset.lpHz;
|
|
21
|
+
lp.Q.value = preset.lpQ ?? BUTTERWORTH_Q;
|
|
22
|
+
tail.connect(lp);
|
|
23
|
+
nodes.push(lp);
|
|
24
|
+
tail = lp;
|
|
25
|
+
}
|
|
26
|
+
if (preset.bell !== void 0) {
|
|
27
|
+
const bell = ctx.createBiquadFilter();
|
|
28
|
+
bell.type = "peaking";
|
|
29
|
+
bell.frequency.value = preset.bell.hz;
|
|
30
|
+
bell.Q.value = preset.bell.q;
|
|
31
|
+
bell.gain.value = preset.bell.gainDb;
|
|
32
|
+
tail.connect(bell);
|
|
33
|
+
nodes.push(bell);
|
|
34
|
+
tail = bell;
|
|
35
|
+
}
|
|
36
|
+
const panner = ctx.createStereoPanner();
|
|
37
|
+
panner.pan.value = preset.pan ?? 0;
|
|
38
|
+
tail.connect(panner).connect(dest);
|
|
39
|
+
nodes.push(panner);
|
|
40
|
+
return {
|
|
41
|
+
input,
|
|
42
|
+
setPan(value, rampSec) {
|
|
43
|
+
const now = ctx.currentTime;
|
|
44
|
+
panner.pan.cancelScheduledValues(now);
|
|
45
|
+
if (rampSec <= 0) {
|
|
46
|
+
panner.pan.setValueAtTime(value, now);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
panner.pan.setValueAtTime(panner.pan.value, now);
|
|
50
|
+
panner.pan.linearRampToValueAtTime(value, now + rampSec);
|
|
51
|
+
},
|
|
52
|
+
dispose() {
|
|
53
|
+
for (const node of nodes) node.disconnect();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
createStrip
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=chunk-CMHDHRZ6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/space/strip.ts"],"sourcesContent":["/**\n * Channel strip — *공간/믹스* 어휘의 공유 도구. sunwall·drumkit·dub·bass 가 한 트랙에서\n * 동시에 울릴 때 저-중역이 겹쳐 뭉개지는 문제를, 악기 *밖* 의 per-instrument 채널 스트립으로\n * 푼다(패닝 + carve EQ). VST 모델: `createStrip` 은 *믹서 한 채널*, `StripPreset` 은 *그 설정*.\n *\n * 엔진은 무변경. 모든 엔진이 출력을 *주입받은 `AudioNode` bus* 로만 쓰므로(`createWall(ctx, bus,…)`\n * 등), 스트립은 그 `bus` 자리에 끼워넣는 한 노드 묶음일 뿐이다 — 엔진 시그니처·내부 그래프 불변.\n *\n * input(trim gain) → [HP] → [LP] → [bell peaking] → StereoPanner → dest\n *\n * **옵트인 불변식**: preset 의 carve 필드(`hpHz`·`lpHz`·`bell`)는 *있을 때만* 노드를 만든다\n * (sunwall 의 `bend` 미지정 = 미생성 패턴). 빈 preset(`{}`)이면 trim gain(=1) + StereoPanner(pan 0)\n * 뿐 → 센터 모노로 투명. 즉 트랙이 *명시적으로* preset 을 줄 때만 소리가 변한다.\n *\n * offline wav 렌더(실 Chromium `OfflineAudioContext` 바운스) 호환: `StereoPannerNode`·\n * `BiquadFilterNode` 는 표준 노드라 별도 facade 불필요(하니스 `scripts/sound-render-harness.ts`).\n */\n\nexport interface StripPreset {\n /** StereoPanner pan. -1(L)~+1(R). 기본 0(센터·모노 유지). */\n pan?: number\n /** carve HP cutoff(Hz). 베이스/킥 자리 비우기. 기본 미생성(bypass). */\n hpHz?: number\n /** HP Q. 기본 BiquadFilter 기본값(0.7071). */\n hpQ?: number\n /** carve LP cutoff(Hz). 위 악기에 고역 양보. 기본 미생성(bypass). */\n lpHz?: number\n /** LP Q. 기본 BiquadFilter 기본값. */\n lpQ?: number\n /** 보충 notch/peak — 특정 충돌 대역만 ±dB(예: 벽 250Hz -3dB). 기본 미생성(bypass). */\n bell?: { hz: number; q: number; gainDb: number }\n /** 스트립 트림 gain. 기본 1. */\n gain?: number\n}\n\nexport interface StripHandle {\n /** 엔진이 출력을 꽂는 입력 노드(= 체인 맨 앞 trim gain). */\n readonly input: AudioNode\n /** pan 을 value 로 ramp(섹션 자동화용·옵션). rampSec<=0 이면 즉시. */\n setPan(value: number, rampSec: number): void\n /** stop 없음 — 패시브 체인. 전체 노드 disconnect. */\n dispose(): void\n}\n\n/**\n * 채널 스트립을 만들어 `dest` 앞에 끼운다. 엔진은 돌려받은 `input` 으로 출력을 꽂는다.\n *\n * carve 필드(`hpHz`·`lpHz`·`bell`)는 *있을 때만* 노드를 만든다 — 빈 preset 은 trim gain +\n * pan 0 panner 뿐(투명).\n *\n * @param dest 스트립 출력이 연결될 목적지(트랙의 ductable·master 등).\n */\nexport function createStrip(ctx: AudioContext, dest: AudioNode, preset: StripPreset): StripHandle {\n // 체인 맨 앞 = trim gain. 엔진이 여기 꽂는다(빈 preset 이면 gain 1 → 투명).\n const input = ctx.createGain()\n input.gain.value = preset.gain ?? 1\n\n // 패시브 노드 모음 — dispose 시 전부 disconnect.\n const nodes: AudioNode[] = [input]\n let tail: AudioNode = input\n\n // carve 필터 Q 기본 = 1/√2(Butterworth, 평탄). BiquadFilter 의 기본 Q 는 1 이라 cutoff 에\n // +1.25 dB *공진 봉우리* 가 생기는데, carve(자리 비우기)용 필터엔 부적절하다 — 봉우리가 깎으려던\n // 대역(예: 150~200 Hz)에 오히려 에너지를 더해, 모인 신호가 챔버 saturation rail 을 더 세게 때린다.\n const BUTTERWORTH_Q = Math.SQRT1_2\n\n if (preset.hpHz !== undefined) {\n const hp = ctx.createBiquadFilter()\n hp.type = 'highpass'\n hp.frequency.value = preset.hpHz\n hp.Q.value = preset.hpQ ?? BUTTERWORTH_Q\n tail.connect(hp)\n nodes.push(hp)\n tail = hp\n }\n\n if (preset.lpHz !== undefined) {\n const lp = ctx.createBiquadFilter()\n lp.type = 'lowpass'\n lp.frequency.value = preset.lpHz\n lp.Q.value = preset.lpQ ?? BUTTERWORTH_Q\n tail.connect(lp)\n nodes.push(lp)\n tail = lp\n }\n\n if (preset.bell !== undefined) {\n const bell = ctx.createBiquadFilter()\n bell.type = 'peaking'\n bell.frequency.value = preset.bell.hz\n bell.Q.value = preset.bell.q\n bell.gain.value = preset.bell.gainDb\n tail.connect(bell)\n nodes.push(bell)\n tail = bell\n }\n\n // panner 는 항상 존재(pan 0 = 투명). setPan 자동화의 고정 핸들.\n const panner = ctx.createStereoPanner()\n panner.pan.value = preset.pan ?? 0\n tail.connect(panner).connect(dest)\n nodes.push(panner)\n\n return {\n input,\n setPan(value: number, rampSec: number) {\n const now = ctx.currentTime\n panner.pan.cancelScheduledValues(now)\n if (rampSec <= 0) {\n panner.pan.setValueAtTime(value, now)\n return\n }\n panner.pan.setValueAtTime(panner.pan.value, now)\n panner.pan.linearRampToValueAtTime(value, now + rampSec)\n },\n dispose() {\n for (const node of nodes) node.disconnect()\n },\n }\n}\n"],"mappings":";AAoDO,SAAS,YAAY,KAAmB,MAAiB,QAAkC;AAEhG,QAAM,QAAQ,IAAI,WAAW;AAC7B,QAAM,KAAK,QAAQ,OAAO,QAAQ;AAGlC,QAAM,QAAqB,CAAC,KAAK;AACjC,MAAI,OAAkB;AAKtB,QAAM,gBAAgB,KAAK;AAE3B,MAAI,OAAO,SAAS,QAAW;AAC7B,UAAM,KAAK,IAAI,mBAAmB;AAClC,OAAG,OAAO;AACV,OAAG,UAAU,QAAQ,OAAO;AAC5B,OAAG,EAAE,QAAQ,OAAO,OAAO;AAC3B,SAAK,QAAQ,EAAE;AACf,UAAM,KAAK,EAAE;AACb,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,SAAS,QAAW;AAC7B,UAAM,KAAK,IAAI,mBAAmB;AAClC,OAAG,OAAO;AACV,OAAG,UAAU,QAAQ,OAAO;AAC5B,OAAG,EAAE,QAAQ,OAAO,OAAO;AAC3B,SAAK,QAAQ,EAAE;AACf,UAAM,KAAK,EAAE;AACb,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,SAAS,QAAW;AAC7B,UAAM,OAAO,IAAI,mBAAmB;AACpC,SAAK,OAAO;AACZ,SAAK,UAAU,QAAQ,OAAO,KAAK;AACnC,SAAK,EAAE,QAAQ,OAAO,KAAK;AAC3B,SAAK,KAAK,QAAQ,OAAO,KAAK;AAC9B,SAAK,QAAQ,IAAI;AACjB,UAAM,KAAK,IAAI;AACf,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,IAAI,mBAAmB;AACtC,SAAO,IAAI,QAAQ,OAAO,OAAO;AACjC,OAAK,QAAQ,MAAM,EAAE,QAAQ,IAAI;AACjC,QAAM,KAAK,MAAM;AAEjB,SAAO;AAAA,IACL;AAAA,IACA,OAAO,OAAe,SAAiB;AACrC,YAAM,MAAM,IAAI;AAChB,aAAO,IAAI,sBAAsB,GAAG;AACpC,UAAI,WAAW,GAAG;AAChB,eAAO,IAAI,eAAe,OAAO,GAAG;AACpC;AAAA,MACF;AACA,aAAO,IAAI,eAAe,OAAO,IAAI,OAAO,GAAG;AAC/C,aAAO,IAAI,wBAAwB,OAAO,MAAM,OAAO;AAAA,IACzD;AAAA,IACA,UAAU;AACR,iBAAW,QAAQ,MAAO,MAAK,WAAW;AAAA,IAC5C;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/bass/bass.ts
|
|
2
|
+
function makeSaturationCurve(k) {
|
|
3
|
+
const n = 512;
|
|
4
|
+
const buffer = new ArrayBuffer(n * Float32Array.BYTES_PER_ELEMENT);
|
|
5
|
+
const curve = new Float32Array(buffer);
|
|
6
|
+
const denom = Math.tanh(k);
|
|
7
|
+
for (let i = 0; i < n; i++) {
|
|
8
|
+
const x = i / (n - 1) * 2 - 1;
|
|
9
|
+
curve[i] = Math.tanh(k * x) / denom;
|
|
10
|
+
}
|
|
11
|
+
return curve;
|
|
12
|
+
}
|
|
13
|
+
function createBass(ctx, bus, preset) {
|
|
14
|
+
const reeseCurve = preset.reese ? makeSaturationCurve(preset.reese.satK) : null;
|
|
15
|
+
let stopped = false;
|
|
16
|
+
const pending = /* @__PURE__ */ new Set();
|
|
17
|
+
const trackDispose = (fn, lifetimeMs) => {
|
|
18
|
+
pending.add(fn);
|
|
19
|
+
window.setTimeout(() => {
|
|
20
|
+
if (pending.has(fn)) {
|
|
21
|
+
fn();
|
|
22
|
+
pending.delete(fn);
|
|
23
|
+
}
|
|
24
|
+
}, lifetimeMs);
|
|
25
|
+
};
|
|
26
|
+
function sub(when, freq, dur, velocity) {
|
|
27
|
+
if (stopped || !preset.sub) return;
|
|
28
|
+
const s = preset.sub;
|
|
29
|
+
const osc = ctx.createOscillator();
|
|
30
|
+
osc.type = "sine";
|
|
31
|
+
osc.frequency.value = freq;
|
|
32
|
+
const osc2 = ctx.createOscillator();
|
|
33
|
+
osc2.type = s.secondType;
|
|
34
|
+
osc2.frequency.value = freq * (s.secondRatio ?? 1);
|
|
35
|
+
const g = ctx.createGain();
|
|
36
|
+
g.gain.setValueAtTime(1e-4, when);
|
|
37
|
+
g.gain.linearRampToValueAtTime(velocity, when + s.attack);
|
|
38
|
+
g.gain.setValueAtTime(velocity, when + dur - s.release);
|
|
39
|
+
g.gain.exponentialRampToValueAtTime(1e-4, when + dur);
|
|
40
|
+
osc.connect(g).connect(bus);
|
|
41
|
+
osc2.connect(g);
|
|
42
|
+
osc.start(when);
|
|
43
|
+
osc2.start(when);
|
|
44
|
+
const stop = dur + 0.05;
|
|
45
|
+
osc.stop(when + stop);
|
|
46
|
+
osc2.stop(when + stop);
|
|
47
|
+
trackDispose(
|
|
48
|
+
() => {
|
|
49
|
+
try {
|
|
50
|
+
osc.stop();
|
|
51
|
+
osc2.stop();
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
osc.disconnect();
|
|
55
|
+
osc2.disconnect();
|
|
56
|
+
g.disconnect();
|
|
57
|
+
},
|
|
58
|
+
(stop + 0.1) * 1e3
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
function reese(when, freq, dur, velocity) {
|
|
62
|
+
if (stopped || !preset.reese || !reeseCurve) return;
|
|
63
|
+
const r = preset.reese;
|
|
64
|
+
const out = ctx.createGain();
|
|
65
|
+
out.gain.setValueAtTime(1e-4, when);
|
|
66
|
+
out.gain.linearRampToValueAtTime(velocity, when + r.attack);
|
|
67
|
+
out.gain.setValueAtTime(velocity, when + dur - r.release);
|
|
68
|
+
out.gain.exponentialRampToValueAtTime(1e-4, when + dur);
|
|
69
|
+
const lp = ctx.createBiquadFilter();
|
|
70
|
+
lp.type = "lowpass";
|
|
71
|
+
lp.frequency.value = r.lpHz;
|
|
72
|
+
lp.Q.value = r.lpQ;
|
|
73
|
+
out.connect(lp);
|
|
74
|
+
const sat = ctx.createWaveShaper();
|
|
75
|
+
sat.curve = reeseCurve;
|
|
76
|
+
sat.oversample = "2x";
|
|
77
|
+
lp.connect(sat).connect(bus);
|
|
78
|
+
const octave = r.octaveRatio ?? 2;
|
|
79
|
+
const oscs = [];
|
|
80
|
+
const gains = [];
|
|
81
|
+
const stop = dur + 0.05;
|
|
82
|
+
for (let v = 0; v < 2; v++) {
|
|
83
|
+
const osc = ctx.createOscillator();
|
|
84
|
+
osc.type = "sawtooth";
|
|
85
|
+
const cents = v === 0 ? -r.detuneCents : r.detuneCents;
|
|
86
|
+
osc.frequency.value = freq * octave * 2 ** (cents / 1200);
|
|
87
|
+
const og = ctx.createGain();
|
|
88
|
+
og.gain.value = 0.5;
|
|
89
|
+
osc.connect(og).connect(out);
|
|
90
|
+
osc.start(when);
|
|
91
|
+
osc.stop(when + stop);
|
|
92
|
+
oscs.push(osc);
|
|
93
|
+
gains.push(og);
|
|
94
|
+
}
|
|
95
|
+
trackDispose(
|
|
96
|
+
() => {
|
|
97
|
+
for (const osc of oscs) {
|
|
98
|
+
try {
|
|
99
|
+
osc.stop();
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
osc.disconnect();
|
|
103
|
+
}
|
|
104
|
+
for (const og of gains) og.disconnect();
|
|
105
|
+
out.disconnect();
|
|
106
|
+
lp.disconnect();
|
|
107
|
+
sat.disconnect();
|
|
108
|
+
},
|
|
109
|
+
(stop + 0.1) * 1e3
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
sub,
|
|
114
|
+
reese,
|
|
115
|
+
dispose() {
|
|
116
|
+
stopped = true;
|
|
117
|
+
for (const fn of pending) {
|
|
118
|
+
try {
|
|
119
|
+
fn();
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
pending.clear();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/bass/basses.ts
|
|
129
|
+
var DUB_RELAY_BASS = {
|
|
130
|
+
sub: { secondType: "triangle", attack: 0.04, release: 0.12 },
|
|
131
|
+
reese: { detuneCents: 8, lpHz: 380, lpQ: 6, satK: 3.5, attack: 0.012, release: 0.05 }
|
|
132
|
+
};
|
|
133
|
+
var DUB_HOLY_BASS = {
|
|
134
|
+
sub: { secondType: "triangle", secondRatio: 1.005, attack: 0.08, release: 0.2 }
|
|
135
|
+
};
|
|
136
|
+
var DUB_POWER_BASS = {
|
|
137
|
+
sub: { secondType: "triangle", attack: 0.05, release: 0.15 },
|
|
138
|
+
reese: { detuneCents: 10, lpHz: 420, lpQ: 5.5, satK: 3.2, attack: 0.015, release: 0.06 }
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
createBass,
|
|
143
|
+
DUB_RELAY_BASS,
|
|
144
|
+
DUB_HOLY_BASS,
|
|
145
|
+
DUB_POWER_BASS
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=chunk-CNJ6DQV5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bass/bass.ts","../src/bass/basses.ts"],"sourcesContent":["/**\n * Bass — *저역* 합성 엔진. dub 트랙들이 글자 그대로 복제하던 sub·reese 베이스 골격을\n * 한 악기로. sunwall·drumkit 과 같은 VST 모델: `createBass` 는 *색-불문 엔진*,\n * `*_BASS` 는 *패치*.\n *\n * sub : sine + 보조 osc(triangle/같은·살짝 detune) → AD-sustain env → bus\n * reese : detuned saw × 2(한 옥타브 위) → resonant lowpass → tanh saturation → bus\n *\n * 엔진은 *voice 합성* 만 한다 — `(when, freq, dur, velocity)` 절대시간으로 `bus` 에 한 음을\n * 그린다. 스케줄링(스텝 루프)·master fade·사이드체인 duck·dub send 는 *트랙* 의 책임이며\n * 엔진 밖이다 (drumkit 과 동일한 정규화 기준). dub-holy 처럼 sub 만 쓰는 트랙은 preset 에서\n * `reese` 를 비우면 `reese()` 가 no-op.\n *\n * 보존 기준: 들리는 envelope(attack·sustain·release)·필터·saturation·디튠은 원본 상수 그대로\n * preset 으로 주입. 들리지 않는 것만 정규화 — saturation 커브 Float32Array 는 핸들당 한 번\n * 계산해 음마다 재사용(내용 동일), ephemeral voice 노드의 지연 disconnect 는 drumkit 과 동일.\n *\n * 트랙 전용 저역(dig-it 의 motorik saw bass 등)은 rule-of-three 미달이라 트랙 인라인으로 남는다.\n */\n\n/** sub 베이스 voice — sine + 보조 osc 의 sustained 저역 드론. */\nexport interface SubPreset {\n /** 보조 osc 파형. 세 트랙 'triangle'. */\n secondType: OscillatorType\n /** 보조 osc freq 배율 — *맥놀이* detune. relay·power 1, holy 1.005. 기본 1. */\n secondRatio?: number\n /** gain attack(0.0001 → velocity) 시간. relay 0.04 / power 0.05 / holy 0.08. */\n attack: number\n /** sustain 종료~exp decay 구간 길이. hold 는 `when + dur - release` 까지. relay 0.12 / power 0.15 / holy 0.2. */\n release: number\n}\n\n/** reese 베이스 voice — *우글거리는* mid-bass(detuned saw → resonant LP → tanh sat). */\nexport interface ReesePreset {\n /** root 대비 옥타브 배율. 세 트랙 2(한 옥타브 위). 기본 2. */\n octaveRatio?: number\n /** saw voice 디튠 ±cents. relay 8 / power 10. */\n detuneCents: number\n /** lowpass cutoff(Hz). relay 380 / power 420. */\n lpHz: number\n /** lowpass Q(resonance). relay 6 / power 5.5. */\n lpQ: number\n /** tanh saturation drive. relay 3.5 / power 3.2. */\n satK: number\n /** gain attack 시간. relay 0.012 / power 0.015. */\n attack: number\n /** sustain 종료~exp decay 구간 길이. relay 0.05 / power 0.06. */\n release: number\n}\n\nexport interface BassPreset {\n sub?: SubPreset\n reese?: ReesePreset\n}\n\nexport interface BassHandle {\n /** when 에 sub 한 음(dur 동안 sustain). preset.sub 없으면 no-op. */\n sub(when: number, freq: number, dur: number, velocity: number): void\n /** when 에 reese 한 음. preset.reese 없으면 no-op. */\n reese(when: number, freq: number, dur: number, velocity: number): void\n /** 공유 노드 정리. ephemeral voice 노드는 self-stop + 내부 타임아웃 disconnect. */\n dispose(): void\n}\n\n/** tanh saturation 커브 — reese 의 *우글거림*. drive 가 클수록 거칠다. */\nfunction makeSaturationCurve(k: number): Float32Array<ArrayBuffer> {\n const n = 512\n const buffer = new ArrayBuffer(n * Float32Array.BYTES_PER_ELEMENT)\n const curve = new Float32Array(buffer)\n const denom = Math.tanh(k)\n for (let i = 0; i < n; i++) {\n const x = (i / (n - 1)) * 2 - 1\n curve[i] = Math.tanh(k * x) / denom\n }\n return curve\n}\n\n/**\n * 베이스를 만들어 `bus` 로 voice 를 합성하는 핸들을 돌려준다.\n *\n * @param bus 각 음이 연결될 목적지(트랙의 ductable·master 등).\n */\nexport function createBass(ctx: AudioContext, bus: AudioNode, preset: BassPreset): BassHandle {\n // reese saturation 커브 — 핸들당 한 번. 음마다 새 WaveShaper 가 같은 Float32Array 를 공유.\n const reeseCurve = preset.reese ? makeSaturationCurve(preset.reese.satK) : null\n\n // ephemeral voice 노드의 지연 disconnect — drumkit 과 동일한 cleanup.\n let stopped = false\n const pending = new Set<() => void>()\n const trackDispose = (fn: () => void, lifetimeMs: number) => {\n pending.add(fn)\n window.setTimeout(() => {\n if (pending.has(fn)) {\n fn()\n pending.delete(fn)\n }\n }, lifetimeMs)\n }\n\n function sub(when: number, freq: number, dur: number, velocity: number) {\n if (stopped || !preset.sub) return\n const s = preset.sub\n\n const osc = ctx.createOscillator()\n osc.type = 'sine'\n osc.frequency.value = freq\n const osc2 = ctx.createOscillator()\n osc2.type = s.secondType\n osc2.frequency.value = freq * (s.secondRatio ?? 1)\n\n const g = ctx.createGain()\n g.gain.setValueAtTime(0.0001, when)\n g.gain.linearRampToValueAtTime(velocity, when + s.attack)\n g.gain.setValueAtTime(velocity, when + dur - s.release)\n g.gain.exponentialRampToValueAtTime(0.0001, when + dur)\n osc.connect(g).connect(bus)\n osc2.connect(g)\n osc.start(when)\n osc2.start(when)\n const stop = dur + 0.05\n osc.stop(when + stop)\n osc2.stop(when + stop)\n trackDispose(\n () => {\n try {\n osc.stop()\n osc2.stop()\n } catch {\n // 이미 정지 — 무해.\n }\n osc.disconnect()\n osc2.disconnect()\n g.disconnect()\n },\n (stop + 0.1) * 1000,\n )\n }\n\n function reese(when: number, freq: number, dur: number, velocity: number) {\n if (stopped || !preset.reese || !reeseCurve) return\n const r = preset.reese\n\n const out = ctx.createGain()\n out.gain.setValueAtTime(0.0001, when)\n out.gain.linearRampToValueAtTime(velocity, when + r.attack)\n out.gain.setValueAtTime(velocity, when + dur - r.release)\n out.gain.exponentialRampToValueAtTime(0.0001, when + dur)\n\n const lp = ctx.createBiquadFilter()\n lp.type = 'lowpass'\n lp.frequency.value = r.lpHz\n lp.Q.value = r.lpQ\n out.connect(lp)\n\n const sat = ctx.createWaveShaper()\n sat.curve = reeseCurve\n sat.oversample = '2x'\n lp.connect(sat).connect(bus)\n\n const octave = r.octaveRatio ?? 2\n const oscs: OscillatorNode[] = []\n const gains: GainNode[] = []\n const stop = dur + 0.05\n for (let v = 0; v < 2; v++) {\n const osc = ctx.createOscillator()\n osc.type = 'sawtooth'\n const cents = v === 0 ? -r.detuneCents : r.detuneCents\n osc.frequency.value = freq * octave * 2 ** (cents / 1200)\n const og = ctx.createGain()\n og.gain.value = 0.5\n osc.connect(og).connect(out)\n osc.start(when)\n osc.stop(when + stop)\n oscs.push(osc)\n gains.push(og)\n }\n trackDispose(\n () => {\n for (const osc of oscs) {\n try {\n osc.stop()\n } catch {\n // 이미 정지 — 무해.\n }\n osc.disconnect()\n }\n for (const og of gains) og.disconnect()\n out.disconnect()\n lp.disconnect()\n sat.disconnect()\n },\n (stop + 0.1) * 1000,\n )\n }\n\n return {\n sub,\n reese,\n dispose() {\n stopped = true\n for (const fn of pending) {\n try {\n fn()\n } catch {\n // 이미 정리됨 — 무해.\n }\n }\n pending.clear()\n },\n }\n}\n","/**\n * Bass presets — *bass 패치*. 트랙당 하나. 각 dub 트랙 인라인 베이스의 *들리는 상수* 를 그대로\n * 옮긴 것이라, 거의 같아 보여도(예: relay lp380/Q6 vs power lp420/Q5.5) 합치지 않는다 —\n * *소리 보존* 이 기준. dedup 이득은 엔진(`createBass`)이지 preset 수가 아니다.\n *\n * dub-holy 는 reese 없이 sub 만 — *대성당의 드론* 은 sustained sub + organ pad 가 핵이라\n * reese 의 *우글거림* 을 쓰지 않는다.\n */\n\nimport type { BassPreset } from './bass'\n\n/** Dub the Wall(21) — *둥둥둥* 의 잔음. relay 의 sub(38–55 Hz)·reese(Berghain 결). */\nexport const DUB_RELAY_BASS: BassPreset = {\n sub: { secondType: 'triangle', attack: 0.04, release: 0.12 },\n reese: { detuneCents: 8, lpHz: 380, lpQ: 6, satK: 3.5, attack: 0.012, release: 0.05 },\n}\n\n/** Dub Holy(20) — *대성당의 sub 드론*. 60–120 Hz, 보조 osc 살짝 detune(맥놀이). reese 없음. */\nexport const DUB_HOLY_BASS: BassPreset = {\n sub: { secondType: 'triangle', secondRatio: 1.005, attack: 0.08, release: 0.2 },\n}\n\n/** Dub Power(22) — *peak 64.6 Hz* 의 sub + *권위의 우글거림* reese. */\nexport const DUB_POWER_BASS: BassPreset = {\n sub: { secondType: 'triangle', attack: 0.05, release: 0.15 },\n reese: { detuneCents: 10, lpHz: 420, lpQ: 5.5, satK: 3.2, attack: 0.015, release: 0.06 },\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drumkit — *타악* 합성 엔진. catalog 트랙들이 글자 그대로 복제하던 kick·snare·hat 골격을
|
|
3
|
+
* 한 악기로. sunwall 과 같은 VST 모델: `createDrumKit` 은 *색-불문 엔진*, `*_KIT` 은 *패치*.
|
|
4
|
+
*
|
|
5
|
+
* kick : body osc(피치 envelope) → gain env → bus (+ 옵션 sub body, click transient)
|
|
6
|
+
* snare : tone osc + bandpass noise (+ 옵션 gated reverb tail × N)
|
|
7
|
+
* hat : highpass(+옵션 bandpass) noise → gain env (closed/open)
|
|
8
|
+
*
|
|
9
|
+
* 엔진은 *voice 합성* 만 한다 — `(when, velocity)` 절대시간으로 `bus` 에 한 타격을 그린다.
|
|
10
|
+
* 스케줄링(스텝 루프)·master fade·사이드체인 duck·dub send 는 *트랙* 의 책임이며 엔진 밖이다.
|
|
11
|
+
* 한 곡 전용 voice(metalHit·tom·shimmer·salute·
|
|
12
|
+
* crash·clap·rim)는 rule-of-three 미달이라 트랙 인라인으로 남는다.
|
|
13
|
+
*
|
|
14
|
+
* 보존 기준: 들리는 envelope(attack·peak·decay)·필터·피치는 원본 상수 그대로. 들리지 않는
|
|
15
|
+
* 것만 정규화 — ① white noise 버퍼 내용(통계적으로 동일한 난수), ② 일부 노드 stop() 시점은
|
|
16
|
+
* preset 의 `stop`/`stopPad` 로 그대로 박는다.
|
|
17
|
+
*/
|
|
18
|
+
/** envelope ramp 종류. dub kick 은 exp attack(peak>velocity 허용), 그 외엔 lin. */
|
|
19
|
+
type Ramp = 'lin' | 'exp';
|
|
20
|
+
/** 피치 + gain envelope 한 layer (kick body·sub 공용). */
|
|
21
|
+
interface KickBody {
|
|
22
|
+
startHz: number;
|
|
23
|
+
endHz: number;
|
|
24
|
+
/** freq exponentialRamp 도달 시간. */
|
|
25
|
+
pitchTau: number;
|
|
26
|
+
/** gain attack 시간. */
|
|
27
|
+
attack: number;
|
|
28
|
+
attackCurve: Ramp;
|
|
29
|
+
/** peak gain = velocity * peakMul. 기본 1. dub body1 은 1.3 등. */
|
|
30
|
+
peakMul?: number;
|
|
31
|
+
/** gain exponentialRamp→0 도달 시간(=감쇠 끝). */
|
|
32
|
+
decay: number;
|
|
33
|
+
/** osc.stop(when + stop). */
|
|
34
|
+
stop: number;
|
|
35
|
+
}
|
|
36
|
+
/** kick transient click — 짧은 highpass noise burst. */
|
|
37
|
+
interface KickClick {
|
|
38
|
+
hpHz: number;
|
|
39
|
+
/** peak = velocity * level. */
|
|
40
|
+
level: number;
|
|
41
|
+
attack: number;
|
|
42
|
+
attackCurve: Ramp;
|
|
43
|
+
decay: number;
|
|
44
|
+
stop: number;
|
|
45
|
+
}
|
|
46
|
+
interface KickPreset {
|
|
47
|
+
body: KickBody;
|
|
48
|
+
/** dub 전용 — 깊은 sub tail layer. */
|
|
49
|
+
sub?: KickBody;
|
|
50
|
+
click?: KickClick;
|
|
51
|
+
}
|
|
52
|
+
interface SnareTone {
|
|
53
|
+
type: OscillatorType;
|
|
54
|
+
startHz: number;
|
|
55
|
+
/** 있으면 startHz → endHz 피치 envelope. */
|
|
56
|
+
endHz?: number;
|
|
57
|
+
pitchTau?: number;
|
|
58
|
+
/** peak = velocity * level. */
|
|
59
|
+
level: number;
|
|
60
|
+
decay: number;
|
|
61
|
+
stop: number;
|
|
62
|
+
}
|
|
63
|
+
interface SnareNoise {
|
|
64
|
+
bpHz: number;
|
|
65
|
+
q: number;
|
|
66
|
+
/** playbackRate ∈ [prMin, prMax] 무작위. 고정이면 두 값 동일. */
|
|
67
|
+
prMin: number;
|
|
68
|
+
prMax: number;
|
|
69
|
+
level: number;
|
|
70
|
+
decay: number;
|
|
71
|
+
stop: number;
|
|
72
|
+
}
|
|
73
|
+
/** Gated reverb tail (wet-room 의 *In the Air Tonight* 결). 자연 감쇠 대신 plateau → cut. */
|
|
74
|
+
interface GatedTail {
|
|
75
|
+
centerHz: number;
|
|
76
|
+
q: number;
|
|
77
|
+
/** peak = velocity * level. */
|
|
78
|
+
level: number;
|
|
79
|
+
plateau: number;
|
|
80
|
+
cut: number;
|
|
81
|
+
}
|
|
82
|
+
interface SnarePreset {
|
|
83
|
+
tone?: SnareTone;
|
|
84
|
+
noise?: SnareNoise;
|
|
85
|
+
gatedTails?: GatedTail[];
|
|
86
|
+
}
|
|
87
|
+
interface HatPreset {
|
|
88
|
+
hpHz: number;
|
|
89
|
+
hpQ?: number;
|
|
90
|
+
/** 있으면 highpass 뒤에 bandpass 한 단(dub hat). */
|
|
91
|
+
bpHz?: number;
|
|
92
|
+
bpQ?: number;
|
|
93
|
+
/** playbackRate = prCenter + (random - 0.5) * prSpread. */
|
|
94
|
+
prCenter: number;
|
|
95
|
+
prSpread: number;
|
|
96
|
+
attack: number;
|
|
97
|
+
closedDecay: number;
|
|
98
|
+
openDecay: number;
|
|
99
|
+
/** peak = velocity * (open ? openLevel : closedLevel). dry/iron/reveal 은 1. */
|
|
100
|
+
closedLevel: number;
|
|
101
|
+
openLevel: number;
|
|
102
|
+
/** stop = when + tail + stopPad. */
|
|
103
|
+
stopPad: number;
|
|
104
|
+
}
|
|
105
|
+
interface DrumKitPreset {
|
|
106
|
+
kick: KickPreset;
|
|
107
|
+
snare?: SnarePreset;
|
|
108
|
+
hat?: HatPreset;
|
|
109
|
+
}
|
|
110
|
+
interface DrumKitHandle {
|
|
111
|
+
/** when 에 kick 한 타. */
|
|
112
|
+
kick(when: number, velocity: number): void;
|
|
113
|
+
/** when 에 snare 한 타. preset.snare 없으면 no-op. */
|
|
114
|
+
snare(when: number, velocity: number): void;
|
|
115
|
+
/** when 에 hat 한 타(open/closed). preset.hat 없으면 no-op. */
|
|
116
|
+
hat(when: number, velocity: number, open: boolean): void;
|
|
117
|
+
/** 공유 노드 정리. ephemeral voice 노드는 self-stop + 내부 타임아웃 disconnect. */
|
|
118
|
+
dispose(): void;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 드럼킷을 만들어 `bus` 로 voice 를 합성하는 핸들을 돌려준다.
|
|
122
|
+
*
|
|
123
|
+
* @param bus 각 타격이 연결될 목적지(트랙의 drumBus·tone·master 등).
|
|
124
|
+
*/
|
|
125
|
+
declare function createDrumKit(ctx: AudioContext, bus: AudioNode, preset: DrumKitPreset): DrumKitHandle;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Drumkit presets — *kit 패치*. 트랙당 하나. 각 트랙 인라인 드럼의 *들리는 상수* 를 그대로
|
|
129
|
+
* 옮긴 것이라, 거의 같아 보여도(예: batheday 150→45 vs gratis 140→42) 합치지 않는다 —
|
|
130
|
+
* *소리 보존* 이 기준. dedup 이득은 엔진(`createDrumKit`)이지
|
|
131
|
+
* preset 수가 아니다.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/** dry-room — LinnDrum/DR-55 식 dry punch. tail 없는 마른 머신. */
|
|
135
|
+
declare const DRY_KIT: DrumKitPreset;
|
|
136
|
+
/** wet-room — 깊고 긴 sine kick + *gated reverb* snare(In the Air Tonight). */
|
|
137
|
+
declare const WET_KIT: DrumKitPreset;
|
|
138
|
+
/** iron-pulse — 산업 sub 펌프 kick(+click) + 거친 highpass grit(hat). */
|
|
139
|
+
declare const IRON_KIT: DrumKitPreset;
|
|
140
|
+
/** batheday — 블랙게이즈 계열 결의 kick/snare. */
|
|
141
|
+
declare const BATHEDAY_KIT: DrumKitPreset;
|
|
142
|
+
/** sublime-hate — kick/snare 가 batheday 와 완전 동일. */
|
|
143
|
+
declare const SUBLIME_KIT: DrumKitPreset;
|
|
144
|
+
/** reveal-the-truth — IDM 계열의 *snappy* kick(+click) + snare + hat. */
|
|
145
|
+
declare const REVEAL_KIT: DrumKitPreset;
|
|
146
|
+
/** dub-relay — 144 BPM, 2-body sub kick. */
|
|
147
|
+
declare const DUB_RELAY_KIT: DrumKitPreset;
|
|
148
|
+
/** dub-holy — 70 BPM half-time, *더 깊고 긴* sub kick. hat 미사용. */
|
|
149
|
+
declare const DUB_HOLY_KIT: DrumKitPreset;
|
|
150
|
+
/** dub-power — 78 BPM, *권위의 박* kick + hat. */
|
|
151
|
+
declare const DUB_POWER_KIT: DrumKitPreset;
|
|
152
|
+
|
|
153
|
+
export { BATHEDAY_KIT, DRY_KIT, DUB_HOLY_KIT, DUB_POWER_KIT, DUB_RELAY_KIT, type DrumKitHandle, type DrumKitPreset, type GatedTail, type HatPreset, IRON_KIT, type KickBody, type KickClick, type KickPreset, REVEAL_KIT, SUBLIME_KIT, type SnareNoise, type SnarePreset, type SnareTone, WET_KIT, createDrumKit };
|