/ simulated_instruments / woodblock
Sign in
simulated_instruments / woodblock
woodblock / webapp / src / params.ts
259 lines · 10.6 KBBlameHistoryRaw
1// Parameter definitions for the woodblock model, mirroring WoodblockConfig in
2// woodblock.py. Each parameter is assigned a tier that determines how much
3// recomputation a change requires:
4// solve -> full re-simulation (ODE solve) + radiation + output
5// radiate -> radiation + output only (cached modal solve is reused)
6// output -> output stage only (decimation product is reused)
8export interface WoodblockParams {
9 // Body geometry
10 length_m: number;
11 width_m: number;
12 wall_thickness_m: number;
13 block_height_m: number;
14 // Wood material
15 density: number;
16 youngs_modulus: number;
17 hollow_softening: number;
18 participating_mass_kg: number;
19 inharmonicity: number;
20 n_modes: number;
21 max_mode_hz: number;
22 // Damping
23 loss_factor: number;
24 loss_slope: number;
25 // Mallet / strike
26 strike_position: number;
27 mallet_mass: number;
28 mallet_velocity: number;
29 mallet_stiffness: number;
30 contact_exponent: number;
31 contact_damping: number;
32 // Air cavity
33 cavity_volume_m3: number;
34 slit_area_m2: number;
35 slit_length_m: number;
36 cavity_damping: number;
37 cavity_coupling: number;
38 cavity_span_lo: number;
39 cavity_span_hi: number;
40 // Acoustic field
41 listener_x_m: number;
42 listener_y_m: number;
43 listener_z_m: number;
44 slit_x_m: number;
45 slit_y_m: number;
46 slit_z_m: number;
47 radiating_elements: number;
48 speed_of_sound: number;
49 air_density: number;
50 // Rendering
51 sample_rate: number;
52 oversample: number;
53 duration_s: number;
54 pre_delay_ms: number;
55 normalize: boolean;
56 peak_dbfs: number;
57 full_scale_pa: number;
58 bit_depth: number;
61export const DEFAULTS: WoodblockParams = {
62 length_m: 0.2,
63 width_m: 0.055,
64 wall_thickness_m: 0.01,
65 block_height_m: 0.05,
66 density: 850.0,
67 youngs_modulus: 1.6e10,
68 hollow_softening: 0.9,
69 participating_mass_kg: 0.35,
70 inharmonicity: 1.0,
71 n_modes: 26,
72 max_mode_hz: 20000.0,
73 loss_factor: 0.01,
74 loss_slope: 1.2e-7,
75 strike_position: 0.42,
76 mallet_mass: 0.025,
77 mallet_velocity: 2.5,
78 mallet_stiffness: 1.0e8,
79 contact_exponent: 1.5,
80 contact_damping: 0.8,
81 cavity_volume_m3: 2.6e-4,
82 slit_area_m2: 8.0e-4,
83 slit_length_m: 0.012,
84 cavity_damping: 0.06,
85 cavity_coupling: 1.0,
86 cavity_span_lo: 0.3,
87 cavity_span_hi: 0.7,
88 listener_x_m: 0.15,
89 listener_y_m: 0.25,
90 listener_z_m: 0.3,
91 slit_x_m: 0.0,
92 slit_y_m: 0.0275,
93 slit_z_m: 0.0,
94 radiating_elements: 48,
95 speed_of_sound: 343.0,
96 air_density: 1.204,
97 sample_rate: 48000,
98 oversample: 16,
99 duration_s: 0.45,
100 pre_delay_ms: 3.0,
101 normalize: true,
102 peak_dbfs: -3.0,
103 full_scale_pa: 20.0,
104 bit_depth: 24,
105};
107export type Tier = 'solve' | 'radiate' | 'output';
109// Keys whose change invalidates the cached ODE solve. Note that
110// block_height_m, the listener/slit positions, and radiating_elements only
111// enter the radiation post-process, so they are deliberately absent here.
112export const SOLVE_KEYS: (keyof WoodblockParams)[] = [
113 'length_m', 'width_m', 'wall_thickness_m',
114 'density', 'youngs_modulus', 'hollow_softening', 'participating_mass_kg',
115 'inharmonicity', 'n_modes', 'max_mode_hz',
116 'loss_factor', 'loss_slope',
117 'strike_position', 'mallet_mass', 'mallet_velocity', 'mallet_stiffness',
118 'contact_exponent', 'contact_damping',
119 'cavity_volume_m3', 'slit_area_m2', 'slit_length_m', 'cavity_damping',
120 'cavity_coupling', 'cavity_span_lo', 'cavity_span_hi',
121 'speed_of_sound', 'air_density', 'sample_rate', 'duration_s',
122];
124export const RADIATE_KEYS: (keyof WoodblockParams)[] = [
125 'block_height_m',
126 'listener_x_m', 'listener_y_m', 'listener_z_m',
127 'slit_x_m', 'slit_y_m', 'slit_z_m',
128];
130export const OUTPUT_KEYS: (keyof WoodblockParams)[] = [
131 'pre_delay_ms', 'normalize', 'peak_dbfs', 'full_scale_pa',
132];
134export interface ParamSpec {
135 key: keyof WoodblockParams;
136 label: string;
137 unit?: string;
138 min: number;
139 max: number;
140 step?: number;
141 log?: boolean;
142 integer?: boolean;
143 help?: string;
146export interface ParamGroup {
147 name: string;
148 specs: ParamSpec[];
151export const PARAM_GROUPS: ParamGroup[] = [
152 {
153 name: 'Body geometry',
154 specs: [
155 { key: 'length_m', label: 'Length', unit: 'm', min: 0.08, max: 0.5, step: 0.001, help: 'Long axis of the block; pitch scales as 1/L^2.' },
156 { key: 'width_m', label: 'Width', unit: 'm', min: 0.02, max: 0.15, step: 0.001, help: 'Across the grain; sets source area and level.' },
157 { key: 'wall_thickness_m', label: 'Wall thickness', unit: 'm', min: 0.003, max: 0.03, step: 0.0005, help: 'Bending depth of the vibrating top wall. Pitch scales linearly with this.' },
158 { key: 'block_height_m', label: 'Block height', unit: 'm', min: 0.02, max: 0.12, step: 0.001, help: 'Outer height; sets the dipole arm between the antiphase faces. Radiation-only.' },
159 ],
160 },
161 {
162 name: 'Wood material',
163 specs: [
164 { key: 'density', label: 'Density', unit: 'kg/m³', min: 300, max: 1400, step: 10, help: 'Rosewood ~850, maple ~700. Pitch scales as 1/sqrt(rho).' },
165 { key: 'youngs_modulus', label: "Young's modulus", unit: 'Pa', min: 2e9, max: 5e10, log: true, help: 'Along the grain. Pitch scales as sqrt(E).' },
166 { key: 'hollow_softening', label: 'Hollow softening', min: 0.5, max: 1.0, step: 0.01, help: 'Stiffness removed by the cut slot; overall pitch trim.' },
167 { key: 'participating_mass_kg', label: 'Participating mass', unit: 'kg', min: 0.05, max: 1.0, step: 0.01, help: 'Lumped modal mass; amplitude scales as 1/mass.' },
168 { key: 'inharmonicity', label: 'Inharmonicity', min: 0.8, max: 1.3, step: 0.01, help: '>1 stretches the upper partials (more knock).' },
169 ],
170 },
171 {
172 name: 'Damping',
173 specs: [
174 { key: 'loss_factor', label: 'Loss factor η₀', min: 0.001, max: 0.08, step: 0.001, help: 'Frequency-independent wood loss; sets T60.' },
175 { key: 'loss_slope', label: 'Loss slope η₁', unit: 's', min: 1e-8, max: 2e-6, log: true, help: 'Extra HF damping; the main brightness control.' },
176 ],
177 },
178 {
179 name: 'Mallet / strike',
180 specs: [
181 { key: 'strike_position', label: 'Strike position', min: 0.05, max: 0.95, step: 0.01, help: '0..1 along the bar. Avoid exactly 0.5 (node of every antisymmetric mode).' },
182 { key: 'mallet_mass', label: 'Mallet mass', unit: 'kg', min: 0.005, max: 0.1, step: 0.001, help: 'Contact time and level.' },
183 { key: 'mallet_velocity', label: 'Mallet velocity', unit: 'm/s', min: 0.2, max: 8.0, step: 0.1, help: 'Dynamics and timbre (the contact is nonlinear).' },
184 { key: 'mallet_stiffness', label: 'Mallet stiffness', unit: 'N/m^α', min: 1e7, max: 3e10, log: true, help: 'Harder = shorter contact, brighter click.' },
185 { key: 'contact_exponent', label: 'Contact exponent', min: 1.1, max: 3.0, step: 0.05, help: '1.5 Hertzian (wood/plastic), 2-3 for felt.' },
186 { key: 'contact_damping', label: 'Contact damping λ', unit: 's/m', min: 0.0, max: 3.0, step: 0.05, help: 'Hunt-Crossley loss; damps rebound, softens attack.' },
187 ],
188 },
189 {
190 name: 'Air cavity',
191 specs: [
192 { key: 'cavity_volume_m3', label: 'Cavity volume', unit: 'm³', min: 2e-5, max: 2e-3, log: true, help: 'Helmholtz pitch.' },
193 { key: 'slit_area_m2', label: 'Slit area', unit: 'm²', min: 5e-5, max: 5e-3, log: true, help: 'Helmholtz pitch and cavity output.' },
194 { key: 'slit_length_m', label: 'Slit length', unit: 'm', min: 0.002, max: 0.05, step: 0.001, help: 'Wall thickness at the slot.' },
195 { key: 'cavity_damping', label: 'Cavity damping ζ', min: 0.005, max: 0.5, step: 0.005, help: 'Hollow-resonance Q.' },
196 { key: 'cavity_coupling', label: 'Cavity coupling', min: 0.0, max: 3.0, step: 0.05, help: '0 = dry bar, 1 = nominal, >1 = hollower.' },
197 { key: 'cavity_span_lo', label: 'Cavity span low', min: 0.0, max: 0.5, step: 0.01, help: 'Fraction of the bar where the cavity begins.' },
198 { key: 'cavity_span_hi', label: 'Cavity span high', min: 0.5, max: 1.0, step: 0.01, help: 'Fraction of the bar where the cavity ends.' },
199 ],
200 },
201 {
202 name: 'Listener / slit position',
203 specs: [
204 { key: 'listener_x_m', label: 'Listener x', unit: 'm', min: -1.0, max: 1.0, step: 0.005, help: 'Along the bar axis. Radiation-only: no re-solve needed.' },
205 { key: 'listener_y_m', label: 'Listener y', unit: 'm', min: -1.0, max: 1.0, step: 0.005, help: 'Across the width.' },
206 { key: 'listener_z_m', label: 'Listener z', unit: 'm', min: -1.0, max: 1.0, step: 0.005, help: 'Up, out of the struck face.' },
207 { key: 'slit_x_m', label: 'Slit x', unit: 'm', min: -0.25, max: 0.25, step: 0.001, help: 'Slit opening position (monopole source point).' },
208 { key: 'slit_y_m', label: 'Slit y', unit: 'm', min: -0.1, max: 0.1, step: 0.001 },
209 { key: 'slit_z_m', label: 'Slit z', unit: 'm', min: -0.1, max: 0.1, step: 0.001 },
210 ],
211 },
212 {
213 name: 'Output',
214 specs: [
215 { key: 'duration_s', label: 'Duration', unit: 's', min: 0.1, max: 2.0, step: 0.05 },
216 { key: 'pre_delay_ms', label: 'Pre-delay', unit: 'ms', min: 0, max: 50, step: 0.5 },
217 { key: 'peak_dbfs', label: 'Peak level', unit: 'dBFS', min: -24, max: 0, step: 0.5, help: 'Used when normalize is on.' },
218 { key: 'full_scale_pa', label: 'Full scale', unit: 'Pa', min: 1, max: 200, step: 1, help: 'Used when normalize is off: this many pascals maps to 0 dBFS.' },
219 ],
220 },
221 {
222 name: 'Advanced / quality',
223 specs: [
224 { key: 'n_modes', label: 'Max modes', min: 2, max: 40, step: 1, integer: true },
225 { key: 'max_mode_hz', label: 'Max mode freq', unit: 'Hz', min: 2000, max: 24000, step: 500 },
226 { key: 'radiating_elements', label: 'Radiating elements', min: 8, max: 128, step: 1, integer: true, help: 'Surface discretisation; accuracy, not timbre.' },
227 { key: 'oversample', label: 'Oversample', min: 4, max: 32, step: 1, integer: true, help: 'Solver rate multiple; 16 for render, 8 for preview.' },
228 { key: 'speed_of_sound', label: 'Speed of sound', unit: 'm/s', min: 300, max: 400, step: 1 },
229 { key: 'air_density', label: 'Air density', unit: 'kg/m³', min: 0.9, max: 1.5, step: 0.01 },
230 ],
231 },
232];
234const STORAGE_KEY = 'woodblock-webapp-params-v1';
236export function loadParams(): WoodblockParams {
237 try {
238 const raw = localStorage.getItem(STORAGE_KEY);
239 if (!raw) return { ...DEFAULTS };
240 const parsed = JSON.parse(raw);
241 const out = { ...DEFAULTS };
242 for (const k of Object.keys(DEFAULTS) as (keyof WoodblockParams)[]) {
243 if (typeof parsed[k] === typeof DEFAULTS[k]) {
244 (out as Record<string, unknown>)[k] = parsed[k];
245 }
246 }
247 return out;
248 } catch {
249 return { ...DEFAULTS };
250 }
253export function saveParams(p: WoodblockParams): void {
254 try {
255 localStorage.setItem(STORAGE_KEY, JSON.stringify(p));
256 } catch {
257 // storage unavailable; persistence is a convenience only
258 }
↑↓ move↵openescclose