Retilt the body radiativity toward the low register
The mid corpus modes radiated too well relative to A0 and the low wood modes, so the 3rd-6th partials and the attack click carried the loudness and the tone read as tinny. Make the radiating-area table bottom-heavy, lighten the A0 and low-mode bridge masses, shrink the direct-term area, raise loss_slope so the mid partials decay faster, and lengthen the default finger release. The low band now sits about 10 dB above the mids during the attack (was 2.4 dB). Re-pin the end-to-end diagnostics.
6 changed files+13−13
README.mdmodified+2−2View file
@@ -32,7 +32,7 @@ pluck -> initial condition on the coupled modes
3232
3333 Three points are worth stating plainly, because they are where the model commits itself.
3434
35-**The string and the body are diagonalized together.** Each fingering (a string and a stop) is one symmetric generalized eigenproblem: the mass-normalized modes of the stiff pinned-pinned string, the modal body, and a bridge-ramp shape that lets the string's end move with the bridge. With that ansatz the elastic coupling integrals vanish and the whole coupling sits in the mass matrix, so the problem reduces through the Cholesky factor of M and is diagonalized by svd, which keeps near-degenerate coupled pairs orthonormal where a nonsymmetric eigensolver does not. The coupling is two-way: the body pulls the open A fundamental 14 cents flat at the defaults, and a note whose fundamental lands on a strong body resonance comes out as a split, fast-beating pair, which is the wolf-note mechanism, checked in the validation by its square-root-of-mobility splitting. The body itself is the empirical part: a table of twelve measured-plausible resonances (A0 near 60 Hz, the main wood modes from 97 Hz up) with effective bridge masses, Qs, and radiating areas, not a plate solve; above the table the dense modal region enters only as a resistive residual admittance. The bending stiffness of the wound string comes from a single inharmonicity parameter and stretches the high partials sharp, more so on stopped notes, as it should.
35+**The string and the body are diagonalized together.** Each fingering (a string and a stop) is one symmetric generalized eigenproblem: the mass-normalized modes of the stiff pinned-pinned string, the modal body, and a bridge-ramp shape that lets the string's end move with the bridge. With that ansatz the elastic coupling integrals vanish and the whole coupling sits in the mass matrix, so the problem reduces through the Cholesky factor of M and is diagonalized by svd, which keeps near-degenerate coupled pairs orthonormal where a nonsymmetric eigensolver does not. The coupling is two-way: the body pulls the open A fundamental 17 cents flat at the defaults, and a note whose fundamental lands on a strong body resonance comes out as a split, fast-beating pair, which is the wolf-note mechanism, checked in the validation by its square-root-of-mobility splitting. The body itself is the empirical part: a table of twelve measured-plausible resonances (A0 near 60 Hz, the main wood modes from 97 Hz up) with effective bridge masses, Qs, and radiating areas, not a plate solve; above the table the dense modal region enters only as a resistive residual admittance. The bending stiffness of the wound string comes from a single inharmonicity parameter and stretches the high partials sharp, more so on stopped notes, as it should.
3636
3737 **The pluck is an initial condition, not a force history.** Releasing a string pulled with force F leaves the classic triangular shape, whose projection through the coupled shapes gives every mode an initial amplitude, rolled off by a first-order finite-release filter standing in for the fingertip (the difference between a fleshy jazz pizzicato and a hard snap). From there every coupled mode is a damped exponential, so a note renders in closed form, faster than real time, unlike the sibling `recorder` repository whose oscillation must be integrated sample by sample. When the next note of a walking line lands, the previous note is choked with an extra decay standing in for the player stopping the string. What this gives up is the contact detail: there is no fingerboard collision, so the rattle of a very hard pluck is outside the model, and notes are otherwise independent, which is wrong for true legato slides. Damping is assigned in the uncoupled coordinates and projected: string internal loss, a finger termination on stopped notes (open strings ring longer, as on the instrument), the residual bridge conductance (a resistive end gives a constant decay rate, so the high partials die in under a second while the fundamental rings for ten), and the body Qs.
3838
@@ -46,4 +46,4 @@ As in the sibling `maraca`, `triangle`, and `recorder` repositories, there is no
4646
4747 ## Defaults
4848
49-A 3/4-size bass with a 1.04 m scale, strung G2 D2 A1 E1 at 285 to 310 N (the linear densities follow from tension and pitch, about 7 g/m for the G and 42 g/m for the E). The default gesture is a single open A1 plucked with 15 N at 30% of the string from the bridge, released over 0.9 ms: 61 coupled modes (49 string, 12 body), a fundamental at 54.57 Hz that the body holds 14 cents flat of A1, a T60 of 10.7 s on that fundamental, and a peak of 0.371 Pa (85.4 dB SPL) at the 133 cm listener before normalization. Setting `note_count` to 8 walks a seeded quarter-note line in A major at 96 bpm, each note choked as the next lands.
49+A 3/4-size bass with a 1.04 m scale, strung G2 D2 A1 E1 at 285 to 310 N (the linear densities follow from tension and pitch, about 7 g/m for the G and 42 g/m for the E). The default gesture is a single open A1 plucked with 15 N at 30% of the string from the bridge, released over 1.3 ms: 61 coupled modes (49 string, 12 body), a fundamental at 54.45 Hz that the body holds 17 cents flat of A1, a T60 of 9.8 s on that fundamental, and a peak of 0.270 Pa (82.6 dB SPL) at the 133 cm listener before normalization. The radiativity of the body table is deliberately bottom-heavy (A0 and the 97 and 113 Hz wood modes carry most of the output), which is what keeps the low band about 10 dB above the mids during the attack; flattening `rad_area_scale` against a lower `dipole_length_m` will thin it back out. Setting `note_count` to 8 walks a seeded quarter-note line in A major at 96 bpm, each note choked as the next lands.
matlab/bass_body.mmodified+3−3View file
@@ -23,9 +23,9 @@ function body = bass_body(P)
2323 % so bridge_mobility scales the whole admittance without retuning the table.
2424
2525 fb = [60.0 97.0 113.0 135.0 167.0 205.0 252.0 310.0 382.0 470.0 578.0 711.0];
26-Qb = [25.0 28.0 30.0 30.0 28.0 26.0 24.0 22.0 20.0 19.0 18.0 17.0];
27-mb = [2.2 1.6 1.3 1.1 0.95 0.85 0.80 0.80 0.85 0.95 1.1 1.3]; % kg at the bridge
28-Sb = [0.040 0.032 0.028 0.024 0.020 0.016 0.013 0.011 0.009 0.0075 0.006 0.005]; % m^2
26+Qb = [28.0 30.0 30.0 30.0 28.0 26.0 24.0 22.0 20.0 19.0 18.0 17.0];
27+mb = [1.6 1.2 1.1 1.0 0.95 0.90 0.90 1.0 1.1 1.3 1.5 1.8]; % kg at the bridge
28+Sb = [0.055 0.055 0.048 0.032 0.022 0.015 0.010 0.0075 0.0055 0.004 0.003 0.0022]; % m^2
2929 mono = [1 0 0 0 0 0 0 0 0 0 0 0];
3030
3131 nb = length(fb);
matlab/bass_defaults.mmodified+2−2View file
@@ -17,7 +17,7 @@ P.tuning_trim = 1.0; % overall pitch trim (1 = standard orchestral tuning)
1717
1818 % String damping
1919 P.loss_factor = 1.6e-3; % frequency-independent internal loss
20-P.loss_slope = 1.2e-7; % extra high-frequency loss, s
20+P.loss_slope = 2.5e-7; % extra high-frequency loss, s
2121 P.finger_loss = 6.0e-3; % extra loss factor on stopped notes (finger termination)
2222 P.residual_y = 0.015; % residual bridge conductance above the retained body modes, s/kg
2323
@@ -33,7 +33,7 @@ P.radiation_loss = 1.0; % scales the radiation damping added to the body Qs
3333 % Pluck
3434 P.pluck_force_n = 15.0; % transverse pull at release
3535 P.pluck_pos = 0.30; % fraction of the vibrating length, measured from the bridge
36-P.finger_release_ms = 0.9; % finite release time of the fingertip (spectral rolloff)
36+P.finger_release_ms = 1.3; % finite release time of the fingertip (spectral rolloff)
3737 P.choke_ms = 45.0; % decay time constant applied when the next note lands
3838
3939 % Gesture
matlab/bass_radiate.mmodified+1−1View file
@@ -56,7 +56,7 @@ r2 = max(0.05, sqrt((lx + fhX)^2 + (ly - fhY)^2 + (lz - fhZ)^2));
5656 rd = max(0.05, sqrt((lx - dpX)^2 + (ly - dpY)^2 + (lz - dpZ)^2));
5757 cosg = (lz - dpZ) / rd; % dipole axis is the plate normal +z
5858 listenerInside = rc < 0.35;
59-Sdir = 0.006 * P.rad_area_scale;
59+Sdir = 0.004 * P.rad_area_scale;
6060
6161 % ---- per-fingering radiation transfer, poles, and unit pluck amplitudes ----
6262 nf = basis.nf;
matlab/bass_validate.mmodified+3−3View file
@@ -122,12 +122,12 @@ fprintf('walk (seed %d): %d notes, sum midi %d, mean force %.6f N %s\n', ...
122122
123123 % ---- 7. pinned end-to-end diagnostics at the defaults ----
124124 [y, basis, dinfo] = bass_render(P);
125-ok = abs(dinfo.peakPa - 0.370774) < 0.001 ...
126- && abs(dinfo.fundHz - 54.5678) < 0.005 ...
125+ok = abs(dinfo.peakPa - 0.270351) < 0.001 ...
126+ && abs(dinfo.fundHz - 54.4515) < 0.005 ...
127127 && basis.n == 61;
128128 fails = fails + (~ok);
129129 fprintf('pinned render: peak %.6f Pa (target %.6f), fund %.4f Hz (target %.4f), %d modes %s\n', ...
130- dinfo.peakPa, 0.370774, dinfo.fundHz, 54.5678, basis.n, bv_verdict(ok));
130+ dinfo.peakPa, 0.270351, dinfo.fundHz, 54.4515, basis.n, bv_verdict(ok));
131131
132132 % ---- 8. the note decays ----
133133 % An open-string pizzicato rings for seconds, so the tail is not silent;
webapp/src/params.tsmodified+2−2View file
@@ -71,7 +71,7 @@ export const DEFAULTS: BassParams = {
7171 inharmonicity: 8.0e-5,
7272 tuning_trim: 1.0,
7373 loss_factor: 1.6e-3,
74- loss_slope: 1.2e-7,
74+ loss_slope: 2.5e-7,
7575 finger_loss: 6.0e-3,
7676 residual_y: 0.015,
7777 a0_hz: 60.0,
@@ -83,7 +83,7 @@ export const DEFAULTS: BassParams = {
8383 radiation_loss: 1.0,
8484 pluck_force_n: 15.0,
8585 pluck_pos: 0.3,
86- finger_release_ms: 0.9,
86+ finger_release_ms: 1.3,
8787 choke_ms: 45.0,
8888 root_midi: 33,
8989 note_count: 1,