// Parameter definitions for the woodblock model, mirroring WoodblockConfig in // woodblock.py. Each parameter is assigned a tier that determines how much // recomputation a change requires: // solve -> full re-simulation (ODE solve) + radiation + output // radiate -> radiation + output only (cached modal solve is reused) // output -> output stage only (decimation product is reused) export interface WoodblockParams { // Body geometry length_m: number; width_m: number; wall_thickness_m: number; block_height_m: number; // Wood material density: number; youngs_modulus: number; hollow_softening: number; participating_mass_kg: number; inharmonicity: number; n_modes: number; max_mode_hz: number; // Damping loss_factor: number; loss_slope: number; // Mallet / strike strike_position: number; mallet_mass: number; mallet_velocity: number; mallet_stiffness: number; contact_exponent: number; contact_damping: number; // Air cavity cavity_volume_m3: number; slit_area_m2: number; slit_length_m: number; cavity_damping: number; cavity_coupling: number; cavity_span_lo: number; cavity_span_hi: number; // Acoustic field listener_x_m: number; listener_y_m: number; listener_z_m: number; slit_x_m: number; slit_y_m: number; slit_z_m: number; radiating_elements: number; speed_of_sound: number; air_density: number; // Rendering sample_rate: number; oversample: number; duration_s: number; pre_delay_ms: number; normalize: boolean; peak_dbfs: number; full_scale_pa: number; bit_depth: number; } export const DEFAULTS: WoodblockParams = { length_m: 0.2, width_m: 0.055, wall_thickness_m: 0.01, block_height_m: 0.05, density: 850.0, youngs_modulus: 1.6e10, hollow_softening: 0.9, participating_mass_kg: 0.35, inharmonicity: 1.0, n_modes: 26, max_mode_hz: 20000.0, loss_factor: 0.01, loss_slope: 1.2e-7, strike_position: 0.42, mallet_mass: 0.025, mallet_velocity: 2.5, mallet_stiffness: 1.0e8, contact_exponent: 1.5, contact_damping: 0.8, cavity_volume_m3: 2.6e-4, slit_area_m2: 8.0e-4, slit_length_m: 0.012, cavity_damping: 0.06, cavity_coupling: 1.0, cavity_span_lo: 0.3, cavity_span_hi: 0.7, listener_x_m: 0.15, listener_y_m: 0.25, listener_z_m: 0.3, slit_x_m: 0.0, slit_y_m: 0.0275, slit_z_m: 0.0, radiating_elements: 48, speed_of_sound: 343.0, air_density: 1.204, sample_rate: 48000, oversample: 16, duration_s: 0.45, pre_delay_ms: 3.0, normalize: true, peak_dbfs: -3.0, full_scale_pa: 20.0, bit_depth: 24, }; export type Tier = 'solve' | 'radiate' | 'output'; // Keys whose change invalidates the cached ODE solve. Note that // block_height_m, the listener/slit positions, and radiating_elements only // enter the radiation post-process, so they are deliberately absent here. export const SOLVE_KEYS: (keyof WoodblockParams)[] = [ 'length_m', 'width_m', 'wall_thickness_m', 'density', 'youngs_modulus', 'hollow_softening', 'participating_mass_kg', 'inharmonicity', 'n_modes', 'max_mode_hz', 'loss_factor', 'loss_slope', 'strike_position', 'mallet_mass', 'mallet_velocity', 'mallet_stiffness', 'contact_exponent', 'contact_damping', 'cavity_volume_m3', 'slit_area_m2', 'slit_length_m', 'cavity_damping', 'cavity_coupling', 'cavity_span_lo', 'cavity_span_hi', 'speed_of_sound', 'air_density', 'sample_rate', 'duration_s', ]; export const RADIATE_KEYS: (keyof WoodblockParams)[] = [ 'block_height_m', 'listener_x_m', 'listener_y_m', 'listener_z_m', 'slit_x_m', 'slit_y_m', 'slit_z_m', ]; export const OUTPUT_KEYS: (keyof WoodblockParams)[] = [ 'pre_delay_ms', 'normalize', 'peak_dbfs', 'full_scale_pa', ]; export interface ParamSpec { key: keyof WoodblockParams; label: string; unit?: string; min: number; max: number; step?: number; log?: boolean; integer?: boolean; help?: string; } export interface ParamGroup { name: string; specs: ParamSpec[]; } export const PARAM_GROUPS: ParamGroup[] = [ { name: 'Body geometry', specs: [ { 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.' }, { 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.' }, { 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.' }, { 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.' }, ], }, { name: 'Wood material', specs: [ { key: 'density', label: 'Density', unit: 'kg/m³', min: 300, max: 1400, step: 10, help: 'Rosewood ~850, maple ~700. Pitch scales as 1/sqrt(rho).' }, { key: 'youngs_modulus', label: "Young's modulus", unit: 'Pa', min: 2e9, max: 5e10, log: true, help: 'Along the grain. Pitch scales as sqrt(E).' }, { 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.' }, { 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.' }, { key: 'inharmonicity', label: 'Inharmonicity', min: 0.8, max: 1.3, step: 0.01, help: '>1 stretches the upper partials (more knock).' }, ], }, { name: 'Damping', specs: [ { key: 'loss_factor', label: 'Loss factor η₀', min: 0.001, max: 0.08, step: 0.001, help: 'Frequency-independent wood loss; sets T60.' }, { key: 'loss_slope', label: 'Loss slope η₁', unit: 's', min: 1e-8, max: 2e-6, log: true, help: 'Extra HF damping; the main brightness control.' }, ], }, { name: 'Mallet / strike', specs: [ { 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).' }, { key: 'mallet_mass', label: 'Mallet mass', unit: 'kg', min: 0.005, max: 0.1, step: 0.001, help: 'Contact time and level.' }, { 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).' }, { key: 'mallet_stiffness', label: 'Mallet stiffness', unit: 'N/m^α', min: 1e7, max: 3e10, log: true, help: 'Harder = shorter contact, brighter click.' }, { 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.' }, { 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.' }, ], }, { name: 'Air cavity', specs: [ { key: 'cavity_volume_m3', label: 'Cavity volume', unit: 'm³', min: 2e-5, max: 2e-3, log: true, help: 'Helmholtz pitch.' }, { key: 'slit_area_m2', label: 'Slit area', unit: 'm²', min: 5e-5, max: 5e-3, log: true, help: 'Helmholtz pitch and cavity output.' }, { key: 'slit_length_m', label: 'Slit length', unit: 'm', min: 0.002, max: 0.05, step: 0.001, help: 'Wall thickness at the slot.' }, { key: 'cavity_damping', label: 'Cavity damping ζ', min: 0.005, max: 0.5, step: 0.005, help: 'Hollow-resonance Q.' }, { key: 'cavity_coupling', label: 'Cavity coupling', min: 0.0, max: 3.0, step: 0.05, help: '0 = dry bar, 1 = nominal, >1 = hollower.' }, { 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.' }, { 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.' }, ], }, { name: 'Listener / slit position', specs: [ { 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.' }, { key: 'listener_y_m', label: 'Listener y', unit: 'm', min: -1.0, max: 1.0, step: 0.005, help: 'Across the width.' }, { 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.' }, { 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).' }, { key: 'slit_y_m', label: 'Slit y', unit: 'm', min: -0.1, max: 0.1, step: 0.001 }, { key: 'slit_z_m', label: 'Slit z', unit: 'm', min: -0.1, max: 0.1, step: 0.001 }, ], }, { name: 'Output', specs: [ { key: 'duration_s', label: 'Duration', unit: 's', min: 0.1, max: 2.0, step: 0.05 }, { key: 'pre_delay_ms', label: 'Pre-delay', unit: 'ms', min: 0, max: 50, step: 0.5 }, { key: 'peak_dbfs', label: 'Peak level', unit: 'dBFS', min: -24, max: 0, step: 0.5, help: 'Used when normalize is on.' }, { 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.' }, ], }, { name: 'Advanced / quality', specs: [ { key: 'n_modes', label: 'Max modes', min: 2, max: 40, step: 1, integer: true }, { key: 'max_mode_hz', label: 'Max mode freq', unit: 'Hz', min: 2000, max: 24000, step: 500 }, { key: 'radiating_elements', label: 'Radiating elements', min: 8, max: 128, step: 1, integer: true, help: 'Surface discretisation; accuracy, not timbre.' }, { key: 'oversample', label: 'Oversample', min: 4, max: 32, step: 1, integer: true, help: 'Solver rate multiple; 16 for render, 8 for preview.' }, { key: 'speed_of_sound', label: 'Speed of sound', unit: 'm/s', min: 300, max: 400, step: 1 }, { key: 'air_density', label: 'Air density', unit: 'kg/m³', min: 0.9, max: 1.5, step: 0.01 }, ], }, ]; const STORAGE_KEY = 'woodblock-webapp-params-v1'; export function loadParams(): WoodblockParams { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return { ...DEFAULTS }; const parsed = JSON.parse(raw); const out = { ...DEFAULTS }; for (const k of Object.keys(DEFAULTS) as (keyof WoodblockParams)[]) { if (typeof parsed[k] === typeof DEFAULTS[k]) { (out as Record)[k] = parsed[k]; } } return out; } catch { return { ...DEFAULTS }; } } export function saveParams(p: WoodblockParams): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(p)); } catch { // storage unavailable; persistence is a convenience only } }