Fold out-of-range pitches into the fingering chart by octaves
The chart spans C5..A6 (MIDI 72..93), so every note below it was
landing on the nearest entry, C5, and a melody played around middle C
came out as one repeated pitch. The driver now transposes by whole
octaves into the chart span before choosing the nearest fingering,
keeping the contour an octave up rather than collapsing it.
3 changed files+34−11
export-src/recorder/export.jsonmodified+15−3View file
@@ -1,10 +1,22 @@
11 {
22 "id": "recorder",
33 "name": "Recorder",
4- "description": "Soprano recorder, C5 to A6 on the baroque fingering chart; velocity onto breath pressure, note length onto blown duration.",
4+ "description": "Soprano recorder, C5 to A6 on the baroque fingering chart; out-of-range pitches fold in by octaves, velocity onto breath pressure, note length onto blown duration.",
55 "source": "simulated_instruments/recorder",
66 "knobs": [
7- { "key": "attack_ms", "label": "attack (ms)", "min": 2, "max": 60, "default": 12 },
8- { "key": "release_ms", "label": "release (ms)", "min": 5, "max": 80, "default": 25 }
7+ {
8+ "key": "attack_ms",
9+ "label": "attack (ms)",
10+ "min": 2,
11+ "max": 60,
12+ "default": 12
13+ },
14+ {
15+ "key": "release_ms",
16+ "label": "release (ms)",
17+ "min": 5,
18+ "max": 80,
19+ "default": 25
20+ }
921 ]
1022 }
export-src/recorder/render_note.mmodified+17−6View file
@@ -1,9 +1,13 @@
11 function y = render_note(note)
2-% One MIDI note through the recorder model: the nearest playable fingering
3-% from the chart in the prepared state, velocity onto breath pressure, and
4-% the note's own length onto the blown duration (clamped to what the model
5-% accepts). Fixed: a single tongued note, a fixed turbulence seed,
6-% normalize off so velocity survives in pascals.
2+% One MIDI note through the recorder model: the pitch is folded by
3+% octaves into the fingering chart's span (C5..A6, MIDI 72..93), then the
4+% nearest playable fingering from the prepared state is taken, so a
5+% melody played below the instrument keeps its contour an octave up
6+% instead of collapsing onto the lowest note. Velocity scales breath
7+% pressure (the model's C5 reference; it tracks pressure up with pitch
8+% itself), and the note's own length sets the blown duration (clamped to
9+% what the model accepts). Fixed: a single tongued note, a fixed
10+% turbulence seed, normalize off so velocity survives in pascals.
711 P = recorder_defaults();
812 P.sample_rate = note.sample_rate;
913 P.note_count = 1;
@@ -13,7 +17,14 @@ P.normalize = 0;
1317 if note.preview ~= 0
1418 P.oversample = 1;
1519 end
16-[~, ix] = min(abs(note.state.chart_midi - note.pitch));
20+p = note.pitch;
21+if p < 72
22+ p = p + 12 * ceil((72 - p) / 12);
23+end
24+if p > 93
25+ p = p - 12 * ceil((p - 93) / 12);
26+end
27+[~, ix] = min(abs(note.state.chart_midi - p));
1728 P.note1 = note.state.chart_note(ix);
1829 P.breath_pa = 220 + (520 - 220) * note.velocity;
1930 P.note_dur_s = max(0.15, min(4, note.duration_s));
exports/recorder.jsonmodified+2−2View file
@@ -2,7 +2,7 @@
22 "format": "numbl-instrument-v1",
33 "id": "recorder",
44 "name": "Recorder",
5- "description": "Soprano recorder, C5 to A6 on the baroque fingering chart; velocity onto breath pressure, note length onto blown duration.",
5+ "description": "Soprano recorder, C5 to A6 on the baroque fingering chart; out-of-range pitches fold in by octaves, velocity onto breath pressure, note length onto blown duration.",
66 "source": "simulated_instruments/recorder",
77 "knobs": [
88 {
@@ -27,7 +27,7 @@
2727 },
2828 {
2929 "name": "render_note.m",
30- "content": "function y = render_note(note)\n% One MIDI note through the recorder model: the nearest playable fingering\n% from the chart in the prepared state, velocity onto breath pressure, and\n% the note's own length onto the blown duration (clamped to what the model\n% accepts). Fixed: a single tongued note, a fixed turbulence seed,\n% normalize off so velocity survives in pascals.\nP = recorder_defaults();\nP.sample_rate = note.sample_rate;\nP.note_count = 1;\nP.seed = 1;\nP.pre_delay_ms = 0;\nP.normalize = 0;\nif note.preview ~= 0\n P.oversample = 1;\nend\n[~, ix] = min(abs(note.state.chart_midi - note.pitch));\nP.note1 = note.state.chart_note(ix);\nP.breath_pa = 220 + (520 - 220) * note.velocity;\nP.note_dur_s = max(0.15, min(4, note.duration_s));\nP.attack_ms = note.params.attack_ms;\nP.release_ms = note.params.release_ms;\ny = recorder_render(P);\nend\n"
30+ "content": "function y = render_note(note)\n% One MIDI note through the recorder model: the pitch is folded by\n% octaves into the fingering chart's span (C5..A6, MIDI 72..93), then the\n% nearest playable fingering from the prepared state is taken, so a\n% melody played below the instrument keeps its contour an octave up\n% instead of collapsing onto the lowest note. Velocity scales breath\n% pressure (the model's C5 reference; it tracks pressure up with pitch\n% itself), and the note's own length sets the blown duration (clamped to\n% what the model accepts). Fixed: a single tongued note, a fixed\n% turbulence seed, normalize off so velocity survives in pascals.\nP = recorder_defaults();\nP.sample_rate = note.sample_rate;\nP.note_count = 1;\nP.seed = 1;\nP.pre_delay_ms = 0;\nP.normalize = 0;\nif note.preview ~= 0\n P.oversample = 1;\nend\np = note.pitch;\nif p < 72\n p = p + 12 * ceil((72 - p) / 12);\nend\nif p > 93\n p = p - 12 * ceil((p - 93) / 12);\nend\n[~, ix] = min(abs(note.state.chart_midi - p));\nP.note1 = note.state.chart_note(ix);\nP.breath_pa = 220 + (520 - 220) * note.velocity;\nP.note_dur_s = max(0.15, min(4, note.duration_s));\nP.attack_ms = note.params.attack_ms;\nP.release_ms = note.params.release_ms;\ny = recorder_render(P);\nend\n"
3131 },
3232 {
3333 "name": "recorder_basis.m",