0e92d35Pizzicato standup bass: coupled string-body modal model, numbl MATLAB reference, and interactive web appJeremy Magland 1function [tOn, midi, str, stop, force, ninfo] = bass_notes(P)
2% BASS_NOTES The gesture: a single note, or a seeded walking line.
3%
4% note_count = 1 gives one deterministic note at root_midi, plucked with
5% pluck_force_n at t = 0; no random numbers are drawn. note_count > 1 gives
6% a walking line: quarter notes at tempo_bpm on the major scale of the root,
7% generated as a random walk with direction persistence (mostly stepwise,
8% an occasional third, an occasional leap home to the root), reflected off
9% the playable range E1 to G3. Pluck forces get a lognormal spread and the
10% onsets after the first a Gaussian jitter. All randomness comes from
11% bass_rng seeded with P.seed, so a given seed reproduces the identical
12% line in the CLI, Node, and the browser.
13%
14% Each pitch is assigned a fingering the way a player would in low
15% positions: the highest-tuned string whose open pitch does not exceed the
16% note, so the stop stays small. Open strings come out as stop = 0, which
17% bass_basis.m treats as an undamped (nut) termination.
18%
19% Returns row vectors sorted by time: tOn (s), midi (pitch), str (1 = G2,
20% 2 = D2, 3 = A1, 4 = E1), stop (semitones above the open string), force
21% (pluck force, N), and ninfo with summary statistics.
23K = max(1, round(P.note_count));
24beat = 60 / max(20, P.tempo_bpm);
25root = round(P.root_midi);
26openMidi = [43 38 33 28]; % G2 D2 A1 E1
27loMidi = 28; % E1
28hiMidi = 55; % G3
30% ---- the scale grid: major-scale pitches of the key across the range ----
31degs = [0 2 4 5 7 9 11];
32grid = zeros(1, hiMidi - loMidi + 1);
33ng = 0;
34for m = loMidi:hiMidi
35 rel = mod(m - root, 12);
36 if sum(degs == rel) > 0
37 ng = ng + 1;
38 grid(ng) = m;
39 end
40end
41grid = grid(1:ng);
43% index of the grid note nearest the root (the walk starts and leaps here)
44rootIdx = 1;
45best = 1e9;
46for i = 1:ng
47 d = abs(grid(i) - root);
48 if d < best
49 best = d;
50 rootIdx = i;
51 end
52end
54% ---- draw the walk ----
55state = mod(floor(abs(P.seed)), 4294967296);
56uWalk = zeros(K, 1);
57z1 = zeros(K, 1);
58z2 = zeros(K, 1);
59if K > 1
60 [u, state] = bass_rng(state, 5 * K);
61 U = reshape(u, K, 5);
62 uWalk = U(:, 1);
63 z1 = sqrt(-2 * log(U(:, 2))) .* cos(2 * pi * U(:, 3));
64 z2 = sqrt(-2 * log(U(:, 4))) .* cos(2 * pi * U(:, 5));
65end
67tOn = zeros(1, K);
68midi = zeros(1, K);
69str = zeros(1, K);
70stop = zeros(1, K);
71force = zeros(1, K);
72idx = rootIdx;
73dir = 1;
74for i = 1:K
75 if i > 1
76 u = uWalk(i);
77 if u < 0.50
78 idx = idx + dir; % continue stepwise
79 elseif u < 0.75
80 dir = -dir; % turn around
81 idx = idx + dir;
82 elseif u < 0.90
83 idx = idx + 2 * dir; % skip a third
84 else
85 idx = rootIdx; % leap home
86 dir = 1;
87 end
88 if idx > ng
89 idx = ng - (idx - ng);
90 dir = -1;
91 end
92 if idx < 1
93 idx = 1 + (1 - idx);
94 dir = 1;
95 end
96 idx = min(ng, max(1, idx));
97 end
98 m = grid(idx);
99 if K == 1
100 m = min(hiMidi, max(loMidi, root));
101 end
103 % fingering: highest-tuned string whose open pitch is at or below the note
104 s = 4;
105 for cand = 1:4
106 if openMidi(cand) <= m
107 s = cand;
108 break;
109 end
110 end
112 t = (i - 1) * beat;
113 if i > 1
114 t = max(0, t + P.timing_jitter_ms * 1e-3 * z1(i));
115 end
116 fpl = P.pluck_force_n;
117 if K > 1
118 fpl = fpl * exp(P.force_spread * z2(i));
119 end
121 tOn(i) = t;
122 midi(i) = m;
123 str(i) = s;
124 stop(i) = m - openMidi(s);
125 force(i) = max(0.1, fpl);
126end
128[tOn, order] = sort(tOn);
129tOn = reshape(tOn, 1, K);
130midi = reshape(midi(order), 1, K);
131str = reshape(str(order), 1, K);
132stop = reshape(stop(order), 1, K);
133force = reshape(force(order), 1, K);
135ninfo.count = K;
136ninfo.durationS = (K - 1) * beat + P.tail_s;
137ninfo.beatS = beat;
138ninfo.meanForce = sum(force) / K;
139ninfo.maxForce = max(force);
140ninfo.loMidi = min(midi);
141ninfo.hiMidi = max(midi);
142end