物理ベース・シンセサイザー(Karplus–Strong)の Phase 4a (Phase 3 + LFO + Mod Wheel + Preset + 多楽器 6 種 + wasm-opt -O3) 対応版。Rust + WebAssembly + Svelte 5 (SvelteKit) で実装。
wasm32-unknown-unknown)rustup target add wasm32-unknown-unknown
corepack enable
corepack prepare pnpm@latest --activate
pnpm install
pnpm dev
http://localhost:5173/ を開いて「▶ Start Audio」をクリック → A〜L キーで発音。
| コマンド | 内容 |
|---|---|
pnpm gen:params |
params.json から Rust / TS の params モジュールを生成 |
pnpm check:params-sync |
生成物 drift を CI で検証 (drift で exit 1) |
pnpm build:wasm:dev |
gen:params → dev WASM ビルド → コピー → export 検証 |
pnpm build:wasm |
release WASM ビルド (同上の release 版) |
pnpm dev |
WASM(dev) ビルド後、Vite dev server 起動 (5173) |
pnpm build |
本番ビルド(静的サイト → web/build/) |
pnpm preview |
本番プレビュー (http://localhost:4173) |
pnpm check |
cargo check --workspace + svelte-check + check:params-sync |
pnpm lint |
cargo clippy --workspace --all-targets -- -D warnings |
pnpm fmt |
cargo fmt + prettier |
Svelte UI (main thread) ── MessagePort ─→ AudioWorkletProcessor
PresetSelector / ModWheel │ FFI (C ABI、wasm-bindgen 不使用)
LfoSection (rate/waveform/3 depth) ▼
VoiceMeter / PolyphonyToggle wasm-audio (cdylib)
PickPosition / BodyWet スライダー 18 関数 + memory export
WebMIDI CC handler (CC#1/7/64/123) + synth_apply_instrument
Pitch Bend + synth_lfo_set_rate / waveform / depth
▼
dsp-core (rlib)
Engine + Lfo + mod_wheel + lfo_*_depth
VoicePool<8> / KarplusStrong (Thiran allpass)
ModalBodyResonator (M=8、楽器 7 種切替)
LossFilter / SoftClip / SustainState / HoldStack
FractionalDelay (Thiran) / NoteAllocator
SmoothedValue / XorShift32 / ParamDescriptor (生成)
InstrumentKind enum + body_modes_for_instrument
詳細は仕様書 (docs/specs/) を参照:
docs/specs/2026-05-06-001-mvp/docs/specs/2026-05-07-002-phase2/docs/specs/2026-05-07-003-phase3/docs/specs/2026-05-08-004-phase4a/(1+ρ·z⁻¹)/(1+ρ)、note_on 時に周波数依存式で算出note_on 内で noise[n] − noise[n−K] を in-place 適用KarplusStrong の補間を LagrangeCoeffs から ThiranCoeffs 単一型に解消。A4 で 0.0002% 級のピッチ精度(Lagrange 0.89% → Thiran 0.0002% = ~4000× 改善)。C8 は damping=0.9999 では loop gain<1 で物理的に自己発振せず、ignore 継続。note_on 時に adjusted_length = raw_len - τ_g で補正。|x| ≤ 0.95 で完全 linear、|x| > 0.95 で rational mapping、|x| → ∞ で ±1.0 に厳密漸近。__synthDev.setMode も維持)。Phase 1 README を参照。実機検証は持ち越し継続。
Phase 2 06 章 を参照。実機検証は持ち越し継続。
| ID | 手順 | 期待結果 |
|---|---|---|
| F26 | cargo test -p dsp-core test_modal_body_ / test_single_biquad_ |
単体: DC<0.001、ピーク mode.gain ± 5%、aggregate: ピーク 0.5〜1.5 倍 |
| F27 | cargo test -p dsp-core test_loss_filter_ |
DC ゲイン 1.0、Nyquist 減衰 (1-ρ)/(1+ρ)、A2<A4 で ρ 増 |
| F28 | cargo test -p dsp-core test_pick_position_ |
β=0.5 で K=L/2 lag に強い anti-correlation、buffer.len() 不変 |
| F29 | (Step 1 試作評価) Thiran 採用判定 | 案 D 採用済み (D36)、A1〜C6 で 0.02% 級、C8 のみ ignore 継続 |
| F30 | cargo test -p dsp-core test_engine_brightness_pitch_correction |
brightness=0.5 で A4 誤差 < 0.5% |
| F31 | cargo test -p dsp-core test_engine_midi_cc_ |
CC#7 直交 / CC#64 defer / CC#123 reset、Mono+Sustain は no-op |
| F32 | cargo test -p dsp-core test_pitch_bend_ |
±2 clamp、ring buffer 不変、bend→0 で base_length 復帰 |
| F33 | cargo test -p dsp-core test_sustain_ |
active/pending bitmap、retrigger で clear、reset で active=false |
| F34 | 実機: pnpm dev → 8 鍵同時押下 |
VoiceMeter 8 セルすべて active 表示、振幅で輝度変化 |
| F35 | cargo test -p dsp-core test_soft_clip_ |
|x|≤0.95 で完全 linear、|x|→∞ で |y|<1.0 漸近 |
| F36 | pnpm build 後 gzip -c web/build/_app/immutable/assets/wasm_audio.*.wasm | wc -c |
gzip < 30 KB (実測 28 KB) |
| F37 | cargo test --release -p dsp-core test_engine_process_block_timing -- --nocapture |
平均 < 1.5 ms (実測 0.012 ms) |
| F38 | cargo test -p dsp-core test_no_allocation_with_modal_body_and_midi_cc |
8 voice 全 active + CC + Pitch Bend で buffer.len() 不変 |
| F38b | (Phase 3 完成判定) pnpm preview → Chrome DevTools Performance タブで Worklet process Self time avg/max を計測 |
avg < 1.5 ms / max < 2.5 ms。実機操作のため手動検証 |
cargo test -p dsp-core で 120 件パス + 1 件 ignored (Phase 3 既存 93 + Phase 4a 新規 27):
#[ignore] C8、damping<1 で物理限界)| クレート | 種類 | 役割 |
|---|---|---|
crates/dsp-core |
rlib(純粋 Rust、依存ゼロ) | Engine / VoicePool / KarplusStrong (Thiran 単一型) / ModalBodyResonator / LossFilter / SoftClip / SustainState / NoteAllocator / HoldStack / SmoothedValue / XorShift32 / ParamDescriptor (生成) |
crates/wasm-audio |
cdylib(C ABI、wasm-bindgen 不使用) | synth_* 18 関数 + memory export = 19 required exports(Phase 2 の 12 関数 + Phase 3 の midi_cc / pitch_bend / voice_state_ptr + Phase 4a の apply_instrument / lfo_set_rate / lfo_set_waveform / lfo_set_depth) |
web |
SvelteKit + adapter-static | UI / AudioWorklet / Web MIDI / VoiceMeter / PolyphonyToggle |
scripts/copy-wasm.mjs に Binaryen 製 wasm-opt を組み込み。release ビルドで --strip-debug + 全最適化 pass。WASM gzip 27.78 → 18.42 KB に圧縮。excitation_snapshot cfg(test) 化 (D45): production binary から完全除外、関連 test を unit test mod に移動して件数保持。BODY_MODES_<INSTRUMENT>_L/R 8 mode + STEREO_SPREAD_<INSTRUMENT>。Default kind の係数は Phase 3 既存値の完全 alias で後方互換を保証。pitch_factor = exp2(...) を 1 回計算して全 voice に fan-out (per voice exp2 を回避、+15 演算/sample)。Engine::handle_midi_cc の CC#1 分岐を有効化。mod_wheel: SmoothedValue (tau=0.05s) を全 LFO destination depth の master 乗数として保持。Mod Wheel = 0 で LFO 効果ゼロ → Phase 3 互換動作と完全一致 (test_mod_wheel_zero_disables_lfo で機械保証)。Engine::apply_instrument(kind) (D52/D53): 楽器切替で pool.all_notes_off() → hold_stack.clear() → sustain_state.reset() → current_instrument 更新 → modal_body.set_instrument(kind, sample_rate)。即時 release (fade-out なし)、UI で「楽器を切り替えると現在の音は止まります」を告知。synth_apply_instrument / synth_lfo_set_rate / synth_lfo_set_waveform / synth_lfo_set_depth。Phase 3 既存 14 関数 + memory export = 15 → Phase 4a で 18 関数 + memory = 19 required exports。physbase.preset.v1.*)、Factory Preset 7 種 + User Preset 最大 32 件。isValidPresetV1 で schema レベル一括バリデーション (型 / 値域 / NaN / Infinity / 空名 / name.length > 64 / 未知 enum)、Factory 名衝突 / User 上限は store-specific で別途チェック。optgroup で Factory / User を分離、Save / Delete ボタン (Factory 削除 disabled)、onMount で last preset 復元。| ID | 手順 | 期待結果 |
|---|---|---|
| F38b | Phase 3 持ち越し: pnpm preview → Chrome DevTools Performance タブで Worklet process Self time avg/max を計測 |
avg < 1.5 ms / max < 2.5 ms (Phase 3 完成判定)、再計測で avg < 1.7 ms / max < 2.7 ms |
| F39 | pnpm build 後 gzip -kc web/build/_app/immutable/assets/wasm_audio.*.wasm | wc -c |
gzip 目標 < 15 KB / 警戒 < 18 KB / 撤退 < 30 KB (実測 18.42 KB、警戒微超過) |
| F40 | cargo test -p dsp-core test_lfo_ + 実機操作 |
LFO Sine/Triangle 範囲 / 5Hz 周期 / rate 平滑 / phase wrap、実機で vibrato/tremolo/wah 確認 |
| F41 | cargo test -p dsp-core test_midi_cc_mod_wheel_ + 実機操作 |
CC#1 で mod_wheel.target() 更新 / 0..1 clamp、WebMIDI 物理 wheel と UI スライダー同期 |
| F42 | pnpm --filter ./web check + 実機操作 |
isValidPresetV1 が schema 違反を一括 reject、Save / Delete / リロードで User Preset が残る、32 件超過で errorMessage |
| F43 | cargo test -p dsp-core test_apply_instrument_ + 実機操作 |
apply_instrument で modal coeffs 変化 / 全 voice release / sustain pending=0、6 種で音色切替 |
| F44 | cargo build --target wasm32-unknown-unknown --release で excitation_snapshot シンボル除外 |
production binary で関数除外、cargo test 件数保持 |
| F45 | cargo test -p dsp-core test_no_allocation_with_lfo_and_instrument |
8 voice + LFO + Mod Wheel + 楽器切替で voice buffer / LFO 状態 capacity 不変 |
| F46 | cargo test --release -p dsp-core test_engine_process_block_timing_phase4a -- --nocapture |
平均 < 1.7 ms (実測 0.023 ms、74× 余裕) |
| F47 | Phase 3 既存 93 件 + 1 IGNORED 維持 + Default プリセット + Mod Wheel = 0 で Phase 3 と同じ音 | regression なし |
target-feature=+simd128) — Safari/Firefox 対応再評価storage event)未定(開発段階)。