@hraness/dawg 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -50,6 +50,8 @@ self.onmessage = async (event: MessageEvent<RenderRequest>) => {
50
50
  const audio = renderArranged(renderer, parsed, {
51
51
  sampleRate,
52
52
  loop: true,
53
+ // The live loop sounds a vocal track's guide notes (0.7 clips).
54
+ guide: true,
53
55
  samples,
54
56
  });
55
57
  const reply: RenderReply = {
@@ -87,6 +87,8 @@ export class LoopRenderer {
87
87
  const audio = renderArranged(this.inline, score, {
88
88
  sampleRate: this.sampleRate,
89
89
  loop: true,
90
+ // The live loop sounds a vocal track's guide notes (0.7 clips).
91
+ guide: true,
90
92
  samples,
91
93
  });
92
94
  return {
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Resample (0.6.1): render one track, one orbit or the whole mix, over the
3
+ * song, a section or a bar range, to a 16-bit WAV a new sampler or granular
4
+ * track plays. The render is the same offline path `dawg render` writes
5
+ * (`renderArrangedPcm`), with the source soloed and, unless `master` is
6
+ * asked for, the song master left out (a pre-master stem), so the same score
7
+ * always gives the same bytes and the same sha256.
8
+ *
9
+ * Level: the sampler plays a resampled stereo file (a ref with `from`) as
10
+ * stereo, each channel scaled by SAMPLE_LEVEL and the centre pan's
11
+ * equal-power gain, so the file is written RESAMPLE_FILE_SCALE louder than
12
+ * the stem (about +0.09 dB) for the new track, at voice gain RESAMPLE_GAIN
13
+ * and pan 0, to match the source in both channels (panned sources and
14
+ * stereo effects keep their image). A granular track reads the mono
15
+ * mixdown. Peaks past full scale clamp.
16
+ *
17
+ * A section or bar range renders from PREROLL_BARS before it (or from the
18
+ * earliest source note still sounding at its start) and drops those
19
+ * frames, so held notes and effect tails cross into the range as in the
20
+ * song.
21
+ */
22
+ import { withClipLengths } from "./clips.ts";
23
+ import { createHash } from "node:crypto";
24
+ import {
25
+ SCORE_LIMITS,
26
+ TrackScore,
27
+ ScoreValidationError,
28
+ type SampleProvenance,
29
+ type Track,
30
+ } from "../../core/score.ts";
31
+ import { barTicks, findSection, sectionScore } from "../../core/sections.ts";
32
+ import { secondsAtTick } from "../../core/tempo.ts";
33
+ import { renderArrangedPcm } from "./arrange.ts";
34
+ import { orbitOf } from "./effects/duck.ts";
35
+ import { exportSampleRate } from "./measure.ts";
36
+ import { SAMPLE_LEVEL } from "./sampler.ts";
37
+ import type { SampleBank } from "./samples.ts";
38
+ import { RENDER_CHANNELS, encodeWav } from "./wav.ts";
39
+
40
+ /** Bars rendered before a section or bar range for effect tails. */
41
+ export const PREROLL_BARS = 2;
42
+
43
+ /** Voice gain the new sampler track plays the file at. */
44
+ export const RESAMPLE_GAIN = 2;
45
+ /** File level over the rendered stem so the voice reproduces it. */
46
+ export const RESAMPLE_FILE_SCALE =
47
+ Math.SQRT1_2 / (SAMPLE_LEVEL * RESAMPLE_GAIN * 0.5);
48
+
49
+ export type ResampleSource =
50
+ | Readonly<{ kind: "track"; trackId: string }>
51
+ | Readonly<{ kind: "orbit"; orbit: number }>
52
+ | Readonly<{ kind: "master" }>;
53
+
54
+ export type ResampleRange =
55
+ | Readonly<{ kind: "song" }>
56
+ | Readonly<{ kind: "section"; name: string }>
57
+ | Readonly<{ kind: "bars"; from: number; to: number }>;
58
+
59
+ export type ResampleRequest = Readonly<{
60
+ source: ResampleSource;
61
+ range: ResampleRange;
62
+ /** Keep the song master on the render (always on for `master`). */
63
+ post?: boolean;
64
+ }>;
65
+
66
+ export type ResampleRender = Readonly<{
67
+ wav: Uint8Array;
68
+ sha256: string;
69
+ sampleRate: number;
70
+ seconds: number;
71
+ /** Score tick the rendered range starts at (where the new note goes). */
72
+ startTick: number;
73
+ /** Ticks the range covers (the new note's length). */
74
+ durationTicks: number;
75
+ from: SampleProvenance;
76
+ }>;
77
+
78
+ /** `track:<id>`, `orbit:<n>` or `master`: the provenance source name. */
79
+ export function resampleSourceName(source: ResampleSource): string {
80
+ if (source.kind === "track") return `track:${source.trackId}`;
81
+ if (source.kind === "orbit") return `orbit:${source.orbit}`;
82
+ return "master";
83
+ }
84
+
85
+ /** sha256 of the score's canonical JSON (the provenance pin). */
86
+ export function scoreSha256(score: TrackScore): string {
87
+ return createHash("sha256")
88
+ .update(JSON.stringify(score.toJSON()))
89
+ .digest("hex");
90
+ }
91
+
92
+ /** The tracks a source plays (throws for an unknown track or empty orbit). */
93
+ export function resampleTracks(
94
+ score: TrackScore,
95
+ source: ResampleSource,
96
+ ): readonly Track[] {
97
+ if (source.kind === "master") return score.tracks;
98
+ const tracks =
99
+ source.kind === "track"
100
+ ? score.tracks.filter((track) => track.id === source.trackId)
101
+ : score.tracks.filter((track) => orbitOf(track) === source.orbit);
102
+ if (tracks.length === 0)
103
+ throw new ScoreValidationError(
104
+ source.kind === "track"
105
+ ? `no track ${source.trackId}`
106
+ : `no track plays on orbit ${source.orbit}`,
107
+ "invalid-track",
108
+ );
109
+ return tracks;
110
+ }
111
+
112
+ /**
113
+ * The score the render plays: the source soloed (every other track muted),
114
+ * the master removed unless `post`, cut to the range. Returns where the
115
+ * range sits in the song.
116
+ */
117
+ export function resampleScore(
118
+ score: TrackScore,
119
+ request: ResampleRequest,
120
+ ): Readonly<{
121
+ score: TrackScore;
122
+ startTick: number;
123
+ durationTicks: number;
124
+ /** Ticks of lead-in the render plays before the range (sliced off). */
125
+ leadTicks: number;
126
+ }> {
127
+ const sources = new Set(
128
+ resampleTracks(score, request.source).map((track) => track.id),
129
+ );
130
+ const post = request.post === true || request.source.kind === "master";
131
+ const json = { ...score.toJSON() } as Record<string, unknown>;
132
+ json.tracks = score.tracks.map((track) => {
133
+ const copy = { ...track } as Record<string, unknown>;
134
+ delete copy.solo;
135
+ // Solo, not mute, the others: a granular `quant chord` source still
136
+ // hears the song's harmony (muted tracks leave the chord).
137
+ if (request.source.kind !== "master" && sources.has(track.id)) {
138
+ delete copy.muted;
139
+ copy.solo = true;
140
+ }
141
+ return copy;
142
+ });
143
+ if (!post) delete json.master;
144
+ let played = new TrackScore(json as never);
145
+ const ticks = barTicks(played);
146
+ const range = request.range;
147
+ if (range.kind === "song")
148
+ return {
149
+ score: played,
150
+ startTick: 0,
151
+ durationTicks: played.bars * ticks,
152
+ leadTicks: 0,
153
+ };
154
+ let section;
155
+ if (range.kind === "section") {
156
+ section = findSection(played, range.name);
157
+ if (!section)
158
+ throw new ScoreValidationError(
159
+ `no section named ${range.name}${played.sections.length > 0 ? ` (sections: ${played.sections.map((s) => s.name).join(", ")})` : ""}`,
160
+ "invalid-track",
161
+ );
162
+ } else {
163
+ if (
164
+ !Number.isInteger(range.from) ||
165
+ !Number.isInteger(range.to) ||
166
+ range.from < 1 ||
167
+ range.to < range.from ||
168
+ range.to > played.bars
169
+ )
170
+ throw new ScoreValidationError(
171
+ `bars must be a-b inside 1-${played.bars}`,
172
+ "invalid-track",
173
+ );
174
+ section = {
175
+ name: "resample",
176
+ startBar: range.from - 1,
177
+ bars: range.to - range.from + 1,
178
+ };
179
+ }
180
+ // Lead-in: notes still sounding at the range start, and the effect tails
181
+ // of the bars before it, play from where they began (PREROLL_BARS back at
182
+ // least); the render then drops the lead-in frames.
183
+ const from = section.startBar * ticks;
184
+ let leadBar = Math.max(0, section.startBar - PREROLL_BARS);
185
+ for (const note of played.notes)
186
+ if (
187
+ sources.has(note.trackId) &&
188
+ note.startTick < from &&
189
+ note.startTick + note.durationTicks > from
190
+ )
191
+ leadBar = Math.min(leadBar, Math.floor(note.startTick / ticks));
192
+ const lead = section.startBar - leadBar;
193
+ played = sectionScore(played, {
194
+ ...section,
195
+ startBar: leadBar,
196
+ bars: section.bars + lead,
197
+ });
198
+ return {
199
+ score: played,
200
+ startTick: from,
201
+ durationTicks: section.bars * ticks,
202
+ leadTicks: lead * ticks,
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Render a resample to WAV bytes. `samples` is the source score's loaded
208
+ * bank (needed when the source plays samples).
209
+ */
210
+ export function renderResample(
211
+ score: TrackScore,
212
+ request: ResampleRequest,
213
+ samples?: SampleBank,
214
+ ): ResampleRender {
215
+ const {
216
+ score: played,
217
+ startTick,
218
+ durationTicks,
219
+ leadTicks,
220
+ } = resampleScore(withClipLengths(score, samples), request);
221
+ // The song's export rate (48 kHz once it has a master), so the new
222
+ // track nulls against the source when both play at that rate.
223
+ const sampleRate = exportSampleRate(score);
224
+ const rendered = renderArrangedPcm(played, {
225
+ sampleRate,
226
+ loop: false,
227
+ ...(samples ? { samples } : {}),
228
+ });
229
+ const skip = Math.min(
230
+ rendered.frames,
231
+ Math.round(secondsAtTick(played, leadTicks) * rendered.sampleRate),
232
+ );
233
+ const audio =
234
+ skip === 0
235
+ ? rendered
236
+ : {
237
+ ...rendered,
238
+ frames: rendered.frames - skip,
239
+ pcm: rendered.pcm.subarray(skip * RENDER_CHANNELS),
240
+ };
241
+ const full = new Int16Array(audio.pcm.length);
242
+ for (let index = 0; index < full.length; index += 1)
243
+ full[index] = Math.max(
244
+ -32_768,
245
+ Math.min(32_767, Math.round(audio.pcm[index]! * RESAMPLE_FILE_SCALE)),
246
+ );
247
+ // Trim the tail that rounds to digital silence (within 1 LSB): the new
248
+ // track's note length comes from the range, not from the file.
249
+ let end = full.length;
250
+ while (end > 0 && Math.abs(full[end - 1]!) <= 1) end -= 1;
251
+ if (end === 0)
252
+ throw new ScoreValidationError(
253
+ `${resampleSourceName(request.source)} is silent here · nothing to resample (add notes, unmute it or pick other bars)`,
254
+ );
255
+ // Keep 20 ms of silence so the sampler's end fade lands on nothing.
256
+ const frames = Math.min(
257
+ audio.frames,
258
+ Math.ceil(end / RENDER_CHANNELS) + Math.round(0.02 * audio.sampleRate),
259
+ );
260
+ const pcm = full.subarray(0, frames * RENDER_CHANNELS);
261
+ const seconds = frames / audio.sampleRate;
262
+ if (seconds > SCORE_LIMITS.maxResampleSeconds)
263
+ throw new ScoreValidationError(
264
+ `the resample is ${Math.round(seconds)} s; the limit is ${SCORE_LIMITS.maxResampleSeconds} s · pick a section or bars`,
265
+ "score-limit",
266
+ );
267
+ const wav = encodeWav(pcm, audio.sampleRate, RENDER_CHANNELS);
268
+ const range = request.range;
269
+ const from: SampleProvenance = {
270
+ source: resampleSourceName(request.source),
271
+ ...(range.kind === "section"
272
+ ? { section: findSection(score, range.name)?.name ?? range.name }
273
+ : {}),
274
+ ...(range.kind === "bars" ? { bars: [range.from, range.to] as const } : {}),
275
+ score: scoreSha256(score),
276
+ };
277
+ return {
278
+ wav,
279
+ sha256: createHash("sha256").update(wav).digest("hex"),
280
+ sampleRate: audio.sampleRate,
281
+ seconds,
282
+ startTick,
283
+ durationTicks,
284
+ from,
285
+ };
286
+ }
@@ -158,6 +158,11 @@ export function renderResonatorTrack(
158
158
  : 1);
159
159
  const seedTick = context.seedTick ?? 0;
160
160
  const base = modalSettings(track.modal);
161
+ // An ombak pair (f061-gamelan-winds): the pengisep names its partner in
162
+ // `pair` and sounds `ombak` Hz above the same note, single voice; the
163
+ // partner (pengumbang) carries ombak 0. Each track renders from its own
164
+ // field, so stems key per track.
165
+ const paired = track.modal?.pair !== undefined;
161
166
  const banks: ModalBank[] = [];
162
167
  // Conservative end of each planned voice (its ring cap, or a steal).
163
168
  const ends: number[] = [];
@@ -186,10 +191,11 @@ export function renderResonatorTrack(
186
191
  performance?.accent || note.articulation === "marcato"
187
192
  ? ACCENT_HARDNESS
188
193
  : 0;
194
+ const hz = noteHz(note.pitch, note.cents, context.tuning);
189
195
  const bank = new ModalBank(
190
- settings,
196
+ paired ? { ...settings, ombak: 0 } : settings,
191
197
  {
192
- hz: noteHz(note.pitch, note.cents, context.tuning),
198
+ hz: paired ? hz + settings.ombak : hz,
193
199
  velocity: note.velocity,
194
200
  start: start / sampleRate,
195
201
  duration: length / sampleRate,
@@ -282,6 +288,8 @@ export const MODAL_ENGINE: InstrumentEngine = Object.freeze({
282
288
  },
283
289
  tailSeconds: (track: Track, lowestPitch?: number) =>
284
290
  modalTailSeconds(track.modal, lowestPitch),
291
+ // Undamped bars and gongs ring on after the key lifts, as struck metal does.
292
+ ringOut: (track: Track) => modalSettings(track.modal).damp === 0,
285
293
  stereo: () => false,
286
294
  assetDigests: () => [`resonators:${RESONATOR_TABLE_VERSION}`],
287
295
  });