/ simulated_instruments / maraca
Sign in
simulated_instruments / maraca
maraca / webapp / src / params.ts
260 lines · 11.1 KBCodeBlameHistory
aa03146Maraca shake simulator: MATLAB model and interactive web appmagland 1// Parameter definitions for the maraca model, mirroring maraca_defaults.m.
2// Each parameter is assigned a tier that determines how much recomputation a
3// change requires:
4// solve -> mode basis + impact process + radiation + output
5// radiate -> radiation + output only (cached basis and impacts are reused)
6// output -> output stage only (decimation product is reused)
7//
8// Note that the render duration is not a parameter: it is
9// stroke_count / stroke_rate_hz + tail_s.
11export interface MaracaParams {
12 // Shell geometry
13 shell_radius_m: number;
14 shell_thickness_m: number;
15 // Shell material
16 density: number;
17 youngs_modulus: number;
18 poisson_ratio: number;
19 stiffness_trim: number;
20 max_mode_l: number;
21 max_mode_hz: number;
22 // Damping
23 loss_factor: number;
24 loss_slope: number;
25 radiation_loss: number;
26 // Grains
27 grain_radius_m: number;
28 grain_density: number;
29 grain_youngs: number;
30 grain_size_spread: number;
31 restitution: number;
32 // Shake gesture / impact process
33 stroke_rate_hz: number;
34 stroke_count: number;
35 accent_period: number;
36 accent_gain: number;
37 grain_count: number;
38 bounces_per_grain: number;
39 shake_velocity: number;
40 slosh_lag_ms: number;
41 burst_width_ms: number;
42 impact_spread_deg: number;
43 seed: number;
44 // Air cavity
45 cavity_coupling: number;
46 // Radiation / listener
47 direct_gain: number;
48 listener_x_m: number;
49 listener_y_m: number;
50 listener_z_m: number;
51 speed_of_sound: number;
52 air_density: number;
53 // Rendering
54 sample_rate: number;
55 oversample: number;
56 tail_s: number;
57 pre_delay_ms: number;
58 normalize: boolean;
59 peak_dbfs: number;
60 full_scale_pa: number;
61 bit_depth: number;
64export const DEFAULTS: MaracaParams = {
65 shell_radius_m: 0.034,
66 shell_thickness_m: 0.0022,
67 density: 720.0,
68 youngs_modulus: 2.5e9,
69 poisson_ratio: 0.3,
70 stiffness_trim: 1.0,
71 max_mode_l: 16,
72 max_mode_hz: 22000.0,
73 loss_factor: 0.015,
74 loss_slope: 6.0e-7,
75 radiation_loss: 1.0,
76 grain_radius_m: 0.0015,
77 grain_density: 1300.0,
78 grain_youngs: 6.0e8,
79 grain_size_spread: 0.25,
80 restitution: 0.55,
81 stroke_rate_hz: 4.5,
82 stroke_count: 8,
83 accent_period: 2,
84 accent_gain: 1.5,
85 grain_count: 320,
86 bounces_per_grain: 1.3,
87 shake_velocity: 2.2,
88 slosh_lag_ms: 14.0,
89 burst_width_ms: 45.0,
90 impact_spread_deg: 35.0,
91 seed: 1,
92 cavity_coupling: 1.0,
93 direct_gain: 1.0,
94 listener_x_m: 0.15,
95 listener_y_m: 0.25,
96 listener_z_m: 0.3,
97 speed_of_sound: 343.0,
98 air_density: 1.204,
99 sample_rate: 48000,
100 oversample: 2,
101 tail_s: 0.4,
102 pre_delay_ms: 3.0,
103 normalize: true,
104 peak_dbfs: -3.0,
105 full_scale_pa: 20.0,
106 bit_depth: 24,
107};
109export type Tier = 'solve' | 'radiate' | 'output';
111// Keys whose change invalidates the cached mode basis or impact process. The
112// listener position and direct_gain enter only the radiation stage, so they
113// are deliberately absent.
114export const SOLVE_KEYS: (keyof MaracaParams)[] = [
115 'shell_radius_m', 'shell_thickness_m',
116 'density', 'youngs_modulus', 'poisson_ratio', 'stiffness_trim',
117 'max_mode_l', 'max_mode_hz',
118 'loss_factor', 'loss_slope', 'radiation_loss',
119 'grain_radius_m', 'grain_density', 'grain_youngs', 'grain_size_spread',
120 'restitution',
121 'stroke_rate_hz', 'stroke_count', 'accent_period', 'accent_gain',
122 'grain_count', 'bounces_per_grain', 'shake_velocity', 'slosh_lag_ms', 'burst_width_ms',
123 'impact_spread_deg', 'seed',
124 'cavity_coupling',
125 'speed_of_sound', 'air_density', 'sample_rate', 'tail_s',
126];
128export const RADIATE_KEYS: (keyof MaracaParams)[] = [
129 'direct_gain', 'listener_x_m', 'listener_y_m', 'listener_z_m',
130];
132export const OUTPUT_KEYS: (keyof MaracaParams)[] = [
133 'pre_delay_ms', 'normalize', 'peak_dbfs', 'full_scale_pa',
134];
136export interface ParamSpec {
137 key: keyof MaracaParams;
138 label: string;
139 unit?: string;
140 min: number;
141 max: number;
142 step?: number;
143 log?: boolean;
144 integer?: boolean;
145 help?: string;
148export interface ParamGroup {
149 name: string;
150 specs: ParamSpec[];
153export const PARAM_GROUPS: ParamGroup[] = [
154 {
155 name: 'Shake gesture',
156 specs: [
157 { key: 'stroke_rate_hz', label: 'Stroke rate', unit: 'Hz', min: 0.5, max: 12, step: 0.1, help: 'Strokes per second. Sets the rhythm and the render length.' },
158 { key: 'stroke_count', label: 'Strokes', min: 1, max: 32, step: 1, integer: true, help: 'Number of strokes in the pattern.' },
159 { key: 'accent_period', label: 'Accent every', unit: 'strokes', min: 1, max: 8, step: 1, integer: true, help: '1 accents every stroke, 2 alternates, 4 gives a bar-like pulse.' },
160 { key: 'accent_gain', label: 'Accent strength', min: 1.0, max: 3.0, step: 0.05, help: 'Velocity and count multiplier on accented strokes.' },
161 { key: 'shake_velocity', label: 'Shake velocity', unit: 'm/s', min: 0.2, max: 6.0, step: 0.05, help: 'Mean grain impact speed. Dynamics and timbre together: faster grains give shorter Hertz contacts and a brighter hiss.' },
162 { key: 'slosh_lag_ms', label: 'Slosh lag', unit: 'ms', min: 0, max: 80, step: 0.5, help: 'Delay from the hand reversing to the grain mass arriving at the far wall.' },
163 { key: 'burst_width_ms', label: 'Burst width', unit: 'ms', min: 3, max: 200, step: 1, help: 'Spread of one stroke’s impacts in time. Small = a tight chick, large = a wash.' },
164 { key: 'seed', label: 'Random seed', min: 1, max: 9999, step: 1, integer: true, help: 'Any change redraws the whole impact process; the same seed always reproduces it.' },
165 ],
166 },
167 {
168 name: 'Grains',
169 specs: [
170 { key: 'grain_count', label: 'Grain count', min: 5, max: 3000, step: 1, integer: true, help: 'How many seeds or beads are inside the shell. With bounces per grain, this sets the expected impacts per stroke.' },
171 { key: 'bounces_per_grain', label: 'Bounces per grain', unit: '/stroke', min: 0.2, max: 6.0, step: 0.05, help: 'Mean wall contacts each grain makes in one stroke: one arrival plus whatever rattle follows before the hand reverses.' },
172 { key: 'grain_radius_m', label: 'Grain radius', unit: 'm', min: 0.0003, max: 0.006, step: 0.0001, help: 'Seed or bead size. Bigger grains give heavier, slower, lower-pitched clicks.' },
173 { key: 'grain_density', label: 'Grain density', unit: 'kg/m³', min: 300, max: 8000, log: true, help: 'Seed ~1300, glass ~2500, steel shot ~7800.' },
174 { key: 'grain_youngs', label: 'Grain modulus', unit: 'Pa', min: 1e7, max: 2e11, log: true, help: 'Contact hardness; sets the Hertz contact time and therefore the brightness.' },
175 { key: 'grain_size_spread', label: 'Size spread', min: 0.0, max: 0.8, step: 0.01, help: 'Lognormal spread of the grain radius. 0 = identical grains, which sounds mechanical.' },
176 { key: 'restitution', label: 'Restitution', min: 0.05, max: 0.95, step: 0.01, help: 'Coefficient of restitution; scales the momentum each impact transfers.' },
177 { key: 'impact_spread_deg', label: 'Impact spread', unit: '°', min: 5, max: 90, step: 1, help: 'Angular scatter of the impact points about the pole the grains are thrown at.' },
178 ],
179 },
180 {
181 name: 'Shell geometry',
182 specs: [
183 { key: 'shell_radius_m', label: 'Shell radius', unit: 'm', min: 0.015, max: 0.09, step: 0.001, help: 'Mode frequencies scale as 1/R.' },
184 { key: 'shell_thickness_m', label: 'Shell thickness', unit: 'm', min: 0.0005, max: 0.008, step: 0.0001, help: 'Sets the bending term that spreads the high-degree modes, and the wall mass.' },
185 ],
186 },
187 {
188 name: 'Shell material',
189 specs: [
190 { key: 'density', label: 'Density', unit: 'kg/m³', min: 200, max: 2500, step: 10, help: 'Dried gourd ~700. Frequencies scale as 1/sqrt(rho).' },
191 { key: 'youngs_modulus', label: "Young's modulus", unit: 'Pa', min: 2e8, max: 2e11, log: true, help: 'Frequencies scale as sqrt(E).' },
192 { key: 'poisson_ratio', label: 'Poisson ratio', min: 0.0, max: 0.48, step: 0.01 },
193 { key: 'stiffness_trim', label: 'Stiffness trim', min: 0.4, max: 2.0, step: 0.01, help: 'Overall pitch trim for the departures of a real gourd from a perfect sphere.' },
194 { key: 'cavity_coupling', label: 'Cavity coupling', min: 0.0, max: 4.0, step: 0.05, help: 'Air stiffness inside the closed shell, which raises the breathing mode only.' },
195 ],
196 },
197 {
198 name: 'Damping',
199 specs: [
200 { key: 'loss_factor', label: 'Loss factor η₀', min: 0.001, max: 0.12, step: 0.001, help: 'Frequency-independent wall loss; sets how long each impact rings.' },
201 { key: 'loss_slope', label: 'Loss slope η₁', unit: 's', min: 1e-8, max: 4e-6, log: true, help: 'Extra high-frequency damping; the main brightness control.' },
202 { key: 'radiation_loss', label: 'Radiation loss', min: 0.0, max: 3.0, step: 0.05, help: 'Scales the acoustic radiation damping computed from the multipole impedance.' },
203 ],
204 },
205 {
206 name: 'Listener',
207 specs: [
208 { key: 'listener_x_m', label: 'Listener x', unit: 'm', min: -1.5, max: 1.5, step: 0.005, help: 'Along the shake axis. Radiation-only: no re-solve needed.' },
209 { key: 'listener_y_m', label: 'Listener y', unit: 'm', min: -1.5, max: 1.5, step: 0.005 },
210 { key: 'listener_z_m', label: 'Listener z', unit: 'm', min: -1.5, max: 1.5, step: 0.005 },
211 { key: 'direct_gain', label: 'Direct click gain', min: 0.0, max: 3.0, step: 0.05, help: 'Broadband forced response at the contact point, standing in for the truncated high-degree modes. 0 leaves only the resolved modes.' },
212 ],
213 },
214 {
215 name: 'Output',
216 specs: [
217 { key: 'tail_s', label: 'Tail', unit: 's', min: 0.05, max: 2.0, step: 0.05, help: 'Silence-and-decay appended after the last stroke.' },
218 { key: 'pre_delay_ms', label: 'Pre-delay', unit: 'ms', min: 0, max: 50, step: 0.5 },
219 { key: 'peak_dbfs', label: 'Peak level', unit: 'dBFS', min: -24, max: 0, step: 0.5, help: 'Used when normalize is on.' },
220 { 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.' },
221 ],
222 },
223 {
224 name: 'Advanced / quality',
225 specs: [
226 { key: 'max_mode_l', label: 'Max degree l', min: 2, max: 40, step: 1, integer: true, help: 'Highest spherical-harmonic degree retained.' },
227 { key: 'max_mode_hz', label: 'Max mode freq', unit: 'Hz', min: 2000, max: 24000, step: 500 },
228 { key: 'oversample', label: 'Oversample', min: 1, max: 8, step: 1, integer: true, help: 'Solver rate multiple; 2 for render, 1 for preview.' },
229 { key: 'speed_of_sound', label: 'Speed of sound', unit: 'm/s', min: 300, max: 400, step: 1 },
230 { key: 'air_density', label: 'Air density', unit: 'kg/m³', min: 0.9, max: 1.5, step: 0.01 },
231 ],
232 },
233];
235const STORAGE_KEY = 'maraca-webapp-params-v1';
237export function loadParams(): MaracaParams {
238 try {
239 const raw = localStorage.getItem(STORAGE_KEY);
240 if (!raw) return { ...DEFAULTS };
241 const parsed = JSON.parse(raw);
242 const out = { ...DEFAULTS };
243 for (const k of Object.keys(DEFAULTS) as (keyof MaracaParams)[]) {
244 if (typeof parsed[k] === typeof DEFAULTS[k]) {
245 (out as Record<string, unknown>)[k] = parsed[k];
246 }
247 }
248 return out;
249 } catch {
250 return { ...DEFAULTS };
251 }
254export function saveParams(p: MaracaParams): void {
255 try {
256 localStorage.setItem(STORAGE_KEY, JSON.stringify(p));
257 } catch {
258 // storage unavailable; persistence is a convenience only
259 }
↑↓ move↵openescclose