function [tOn, midi, str, stop, force, ninfo] = bass_notes(P) % BASS_NOTES The gesture: a single note, or a seeded walking line. % % note_count = 1 gives one deterministic note at root_midi, plucked with % pluck_force_n at t = 0; no random numbers are drawn. note_count > 1 gives % a walking line: quarter notes at tempo_bpm on the major scale of the root, % generated as a random walk with direction persistence (mostly stepwise, % an occasional third, an occasional leap home to the root), reflected off % the playable range E1 to G3. Pluck forces get a lognormal spread and the % onsets after the first a Gaussian jitter. All randomness comes from % bass_rng seeded with P.seed, so a given seed reproduces the identical % line in the CLI, Node, and the browser. % % Each pitch is assigned a fingering the way a player would in low % positions: the highest-tuned string whose open pitch does not exceed the % note, so the stop stays small. Open strings come out as stop = 0, which % bass_basis.m treats as an undamped (nut) termination. % % Returns row vectors sorted by time: tOn (s), midi (pitch), str (1 = G2, % 2 = D2, 3 = A1, 4 = E1), stop (semitones above the open string), force % (pluck force, N), and ninfo with summary statistics. K = max(1, round(P.note_count)); beat = 60 / max(20, P.tempo_bpm); root = round(P.root_midi); openMidi = [43 38 33 28]; % G2 D2 A1 E1 loMidi = 28; % E1 hiMidi = 55; % G3 % ---- the scale grid: major-scale pitches of the key across the range ---- degs = [0 2 4 5 7 9 11]; grid = zeros(1, hiMidi - loMidi + 1); ng = 0; for m = loMidi:hiMidi rel = mod(m - root, 12); if sum(degs == rel) > 0 ng = ng + 1; grid(ng) = m; end end grid = grid(1:ng); % index of the grid note nearest the root (the walk starts and leaps here) rootIdx = 1; best = 1e9; for i = 1:ng d = abs(grid(i) - root); if d < best best = d; rootIdx = i; end end % ---- draw the walk ---- state = mod(floor(abs(P.seed)), 4294967296); uWalk = zeros(K, 1); z1 = zeros(K, 1); z2 = zeros(K, 1); if K > 1 [u, state] = bass_rng(state, 5 * K); U = reshape(u, K, 5); uWalk = U(:, 1); z1 = sqrt(-2 * log(U(:, 2))) .* cos(2 * pi * U(:, 3)); z2 = sqrt(-2 * log(U(:, 4))) .* cos(2 * pi * U(:, 5)); end tOn = zeros(1, K); midi = zeros(1, K); str = zeros(1, K); stop = zeros(1, K); force = zeros(1, K); idx = rootIdx; dir = 1; for i = 1:K if i > 1 u = uWalk(i); if u < 0.50 idx = idx + dir; % continue stepwise elseif u < 0.75 dir = -dir; % turn around idx = idx + dir; elseif u < 0.90 idx = idx + 2 * dir; % skip a third else idx = rootIdx; % leap home dir = 1; end if idx > ng idx = ng - (idx - ng); dir = -1; end if idx < 1 idx = 1 + (1 - idx); dir = 1; end idx = min(ng, max(1, idx)); end m = grid(idx); if K == 1 m = min(hiMidi, max(loMidi, root)); end % fingering: highest-tuned string whose open pitch is at or below the note s = 4; for cand = 1:4 if openMidi(cand) <= m s = cand; break; end end t = (i - 1) * beat; if i > 1 t = max(0, t + P.timing_jitter_ms * 1e-3 * z1(i)); end fpl = P.pluck_force_n; if K > 1 fpl = fpl * exp(P.force_spread * z2(i)); end tOn(i) = t; midi(i) = m; str(i) = s; stop(i) = m - openMidi(s); force(i) = max(0.1, fpl); end [tOn, order] = sort(tOn); tOn = reshape(tOn, 1, K); midi = reshape(midi(order), 1, K); str = reshape(str(order), 1, K); stop = reshape(stop(order), 1, K); force = reshape(force(order), 1, K); ninfo.count = K; ninfo.durationS = (K - 1) * beat + P.tail_s; ninfo.beatS = beat; ninfo.meanForce = sum(force) / K; ninfo.maxForce = max(force); ninfo.loMidi = min(midi); ninfo.hiMidi = max(midi); end