@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
@@ -32,6 +32,7 @@ import {
32
32
  type TrackScore,
33
33
  } from "../../core/score.ts";
34
34
  import { sampleKey, type DecodedSample, type SampleBank } from "./samples.ts";
35
+ import { seedHash } from "./dsp/rng.ts";
35
36
  import {
36
37
  barAt,
37
38
  loopSecondsOf,
@@ -46,7 +47,8 @@ import {
46
47
  type FitMap,
47
48
  type SampleWarp,
48
49
  } from "./warp.ts";
49
- import { fittedBuffer, windowAt } from "./fit.ts";
50
+ import { fittedBuffer, shiftedBuffer, windowAt } from "./fit.ts";
51
+ import { autotuneVoice } from "./autotune.ts";
50
52
  import {
51
53
  performanceTimingFor,
52
54
  tunedTiming,
@@ -106,6 +108,10 @@ export type SamplerVoice = {
106
108
  readonly cents?: NotePerformance["cents"];
107
109
  /** Half pedal: the level fades from `from` seconds. */
108
110
  readonly damp?: NotePerformance["damp"];
111
+ /** `fadeInTime` in output frames (0.6.1); absent is the 1 ms anti-click. */
112
+ readonly attack?: number;
113
+ /** A resampled stereo file (0.6.1): plays its own left and right. */
114
+ readonly stereo?: boolean;
109
115
  };
110
116
 
111
117
  /**
@@ -160,38 +166,111 @@ function noteLengthFrames(note: Note, timing: SamplerTiming): number {
160
166
  function voiceResolver(
161
167
  track: Track,
162
168
  tuning?: TuningTable,
163
- ): (pitch: number) => { voice: string; ratio: number } | undefined {
169
+ ): (
170
+ pitch: number,
171
+ velocity: number,
172
+ ) => { voice: string; ratio: number } | undefined {
164
173
  const sampler = track.sampler!;
174
+ const pick = layerPicker(
175
+ track.id,
176
+ sampler.voices,
177
+ sampler.mode === "oneshot",
178
+ );
165
179
  if (sampler.mode === "oneshot") {
166
180
  const byPitch = new Map<number, string>();
167
181
  for (const [voice, slot] of samplerVoiceSlots(sampler))
168
182
  byPitch.set(slot, voice);
169
- return (pitch) => {
170
- const voice = byPitch.get(pitch);
183
+ return (pitch, velocity) => {
184
+ const slotted = byPitch.get(pitch);
185
+ if (slotted === undefined) return undefined;
186
+ const voice = pick(slotted, velocity);
171
187
  return voice === undefined ? undefined : { voice, ratio: 1 };
172
188
  };
173
189
  }
174
190
  // Keyed: the voice whose root is the highest at or below the pitch (a
175
- // multi-sampled instrument), else the lowest root.
191
+ // multi-sampled instrument), else the lowest root. With velocity layers
192
+ // only the voices whose `vel` holds the note's velocity take part.
176
193
  const keyed = Object.entries(sampler.voices)
177
- .map(([voice, ref]) => ({ voice, root: ref.root ?? 60 }))
194
+ .map(([voice, ref]) => ({ voice, root: ref.root ?? 60, vel: ref.vel }))
178
195
  .sort((a, b) => a.root - b.root || (a.voice < b.voice ? -1 : 1));
179
- return (pitch) => {
180
- let chosen = keyed[0];
181
- for (const candidate of keyed)
182
- if (candidate.root <= pitch) chosen = candidate;
196
+ return (pitch, velocity) => {
197
+ let chosen: (typeof keyed)[number] | undefined;
198
+ for (const candidate of keyed) {
199
+ if (!inLayer(candidate.vel, velocity)) continue;
200
+ if (chosen === undefined || candidate.root <= pitch) chosen = candidate;
201
+ }
183
202
  if (!chosen) return undefined;
184
- if (!tuning)
185
- return { voice: chosen.voice, ratio: 2 ** ((pitch - chosen.root) / 12) };
203
+ const voice = pick(chosen.voice, velocity) ?? chosen.voice;
204
+ if (!tuning) return { voice, ratio: 2 ** ((pitch - chosen.root) / 12) };
186
205
  const hz = tuning.hz[pitch]!;
187
206
  if (!(hz > 0)) return undefined;
188
207
  return {
189
- voice: chosen.voice,
208
+ voice,
190
209
  ratio: hz / (440 * 2 ** ((chosen.root - 69) / 12)),
191
210
  };
192
211
  };
193
212
  }
194
213
 
214
+ /** MIDI velocity (0..127) of a note velocity 0..1. */
215
+ export function midiVelocity(velocity: number): number {
216
+ return Math.max(0, Math.min(127, Math.round(velocity * 127)));
217
+ }
218
+
219
+ function inLayer(
220
+ vel: readonly [number, number] | undefined,
221
+ velocity: number,
222
+ ): boolean {
223
+ if (!vel) return true;
224
+ const v = midiVelocity(velocity);
225
+ return v >= vel[0] && v <= vel[1];
226
+ }
227
+
228
+ /**
229
+ * Velocity layers and round robin (0.6.1, SFZ lovel/hivel and seq_length).
230
+ * A voice with `rr` stands for its whole group: of the group's voices that
231
+ * hold the note's velocity (and, keyed, share its root) the next one in
232
+ * name order plays. When no layer of the group holds the velocity, the
233
+ * nearest layer plays rather than nothing. Each group keeps its own
234
+ * counter in note order, starting at a turn seeded by the track id and
235
+ * the group, so a render is deterministic (A B A B) while two tracks
236
+ * with the same samples need not alternate in lockstep. A keyed voice
237
+ * without `rr` plays itself when its `vel` holds the velocity, else
238
+ * nothing; a one-shot voice without `rr` (its own pad) always plays.
239
+ */
240
+ function layerPicker(
241
+ trackId: string,
242
+ voices: Readonly<Record<string, SampleRef>>,
243
+ oneshot: boolean,
244
+ ): (voice: string, velocity: number) => string | undefined {
245
+ const counters = new Map<string, number>();
246
+ const names = Object.keys(voices).sort();
247
+ return (voice, velocity) => {
248
+ const ref = voices[voice]!;
249
+ if (ref.rr === undefined)
250
+ return oneshot || inLayer(ref.vel, velocity) ? voice : undefined;
251
+ const members = names.filter((name) => {
252
+ const other = voices[name]!;
253
+ return other.rr === ref.rr && (other.root ?? 60) === (ref.root ?? 60);
254
+ });
255
+ let group = members.filter((name) => inLayer(voices[name]!.vel, velocity));
256
+ if (group.length === 0) {
257
+ const v = midiVelocity(velocity);
258
+ const gap = (name: string) => {
259
+ const vel = voices[name]!.vel!;
260
+ return v < vel[0] ? vel[0] - v : v - vel[1];
261
+ };
262
+ const nearest = Math.min(...members.map(gap));
263
+ group = members.filter((name) => gap(name) === nearest);
264
+ }
265
+ if (group.length === 0) return undefined;
266
+ const key = `${ref.rr}:${ref.root ?? 60}:${group.join(",")}`;
267
+ const turn =
268
+ counters.get(key) ?? seedHash(`${trackId}:${key}`) % group.length;
269
+ counters.set(key, turn + 1);
270
+ return group[turn % group.length];
271
+ };
272
+ }
273
+
195
274
  /** Plans every voice a sampler track plays, with choke and polyphony applied. */
196
275
  export function planSamplerVoices(
197
276
  track: Track,
@@ -224,7 +303,7 @@ export function planSamplerVoices(
224
303
  voice.fade = Math.min(fade, end - voice.start);
225
304
  };
226
305
  for (const note of ordered) {
227
- const resolved = resolve(note.pitch);
306
+ const resolved = resolve(note.pitch, note.velocity);
228
307
  if (!resolved) continue;
229
308
  // A note's cents repitch every voice, keyed or one-shot.
230
309
  const target = note.cents
@@ -256,8 +335,19 @@ export function planSamplerVoices(
256
335
  ? fitVoice(ref, decoded, note.startTick, held, timing, reach)
257
336
  : undefined;
258
337
  if (fitted === null) continue;
259
- const sample = fitted?.sample ?? decoded;
260
- const speed = fitted ? 1 : (ref.speed ?? 1);
338
+ // 0.6.1 `shift`/`formant`: a cached constant-length pitch shift of the
339
+ // played buffer (after any fit, so it follows fitmode).
340
+ const shifted = isShifted(ref)
341
+ ? shiftVoice(ref, fitted?.sample ?? decoded, fitted ? "fit" : "raw")
342
+ : undefined;
343
+ const sample = shifted ?? fitted?.sample ?? decoded;
344
+ // Live, until the cached shift is ready: a plain repitch at the shifted
345
+ // pitch (the length follows, like tape).
346
+ const repitch =
347
+ shifted === undefined && isShifted(ref)
348
+ ? 2 ** ((ref.shift ?? 0) / 12)
349
+ : 1;
350
+ const speed = fitted ? repitch : (ref.speed ?? 1) * repitch;
261
351
  const regionStart = fitted ? 0 : (ref.begin ?? 0) * sample.frames;
262
352
  const regionEnd = fitted ? sample.frames : (ref.end ?? 1) * sample.frames;
263
353
  // `fit`, `unit: "c"` and `unit: "s"` give the window a duration; the
@@ -277,7 +367,14 @@ export function planSamplerVoices(
277
367
  ? ((Math.abs(speed) * sample.sampleRate) / sampleRate) * target.ratio
278
368
  : ((regionEnd - regionStart) / (seconds * sampleRate)) * target.ratio;
279
369
  const natural = Math.max(1, Math.floor((regionEnd - regionStart) / step));
280
- const release = Math.max(1, Math.round(RELEASE_SECONDS * sampleRate));
370
+ const release = Math.max(
371
+ 1,
372
+ Math.round((ref.fadeTime ?? RELEASE_SECONDS) * sampleRate),
373
+ );
374
+ const endFade = Math.max(
375
+ 1,
376
+ Math.round((ref.fadeTime ?? END_FADE_SECONDS) * sampleRate),
377
+ );
281
378
  const loop = ref.loop === true;
282
379
  const clipped =
283
380
  ref.clip === undefined ? held : Math.max(1, Math.floor(held * ref.clip));
@@ -294,7 +391,7 @@ export function planSamplerVoices(
294
391
  fade = release;
295
392
  } else {
296
393
  length = natural;
297
- fade = Math.max(1, Math.round(END_FADE_SECONDS * sampleRate));
394
+ fade = endFade;
298
395
  }
299
396
  // accelerate: the rate ramps by `accelerate`× over the planned length;
300
397
  // a non-looping voice ends where the ramped read runs out (or stops).
@@ -308,7 +405,7 @@ export function planSamplerVoices(
308
405
  const until = Math.max(1, Math.floor(reach));
309
406
  if (until < length) {
310
407
  length = until;
311
- fade = Math.max(1, Math.round(END_FADE_SECONDS * sampleRate));
408
+ fade = endFade;
312
409
  }
313
410
  }
314
411
  const loopFrom = fitted
@@ -323,9 +420,36 @@ export function planSamplerVoices(
323
420
  : ref.loopEnd * sample.frames;
324
421
  const start = noteStartFrame(note, timing);
325
422
  const performance = (note as PerformedNote).performance;
423
+ // 0.7 autotune: the played buffer (after fit and shift) retuned at the
424
+ // pitch and place it sounds; same length, so the plan above holds.
425
+ // Reversed voices play untuned. Absent `autotune` skips this.
426
+ // Timed targets tune only the frames this voice can read.
427
+ const reads =
428
+ loop || ramp !== 0 || performance?.cents
429
+ ? {
430
+ from: Math.min(regionStart, loopFrom),
431
+ to: Math.max(regionEnd, loopTo),
432
+ }
433
+ : {
434
+ from: regionStart,
435
+ to: Math.min(regionEnd, regionStart + length * step + 2),
436
+ };
437
+ const tuned =
438
+ track.autotune && speed > 0
439
+ ? autotuneSample(
440
+ timing,
441
+ track,
442
+ sample,
443
+ sample === decoded,
444
+ start,
445
+ regionStart,
446
+ step,
447
+ reads,
448
+ )
449
+ : { sample, from: 0 };
326
450
  const voice: SamplerVoice = {
327
451
  voice: target.voice,
328
- sample,
452
+ sample: tuned.sample,
329
453
  startTick: note.startTick,
330
454
  start,
331
455
  end: start + length,
@@ -333,14 +457,20 @@ export function planSamplerVoices(
333
457
  step,
334
458
  reverse: speed < 0,
335
459
  loop,
336
- regionStart,
337
- regionEnd,
460
+ regionStart: regionStart - tuned.from,
461
+ regionEnd: regionEnd - tuned.from,
338
462
  gain: (ref.gain ?? 1) * Math.max(0, Math.min(1, note.velocity)),
339
463
  choke: ref.choke,
340
464
  loopStart: speed < 0 ? regionEnd - loopTo : loopFrom - regionStart,
341
465
  loopSpan: loopTo - loopFrom,
342
466
  ramp,
343
467
  squiz: ref.squiz ?? 1,
468
+ ...(ref.fadeInTime !== undefined
469
+ ? { attack: Math.max(1, ref.fadeInTime * sampleRate) }
470
+ : {}),
471
+ ...(ref.from !== undefined && tuned.sample.left && tuned.sample.right
472
+ ? { stereo: true }
473
+ : {}),
344
474
  ...(performance?.cents ? { cents: performance.cents } : {}),
345
475
  ...(performance?.damp ? { damp: performance.damp } : {}),
346
476
  };
@@ -433,6 +563,7 @@ function fitVoice(
433
563
  length,
434
564
  );
435
565
  if (!buffer) return null;
566
+ audioIds.set(buffer, key);
436
567
  const sample: DecodedSample = Object.freeze({
437
568
  sha256: decoded.sha256,
438
569
  sampleRate,
@@ -456,6 +587,100 @@ function fitVoice(
456
587
  });
457
588
  }
458
589
 
590
+ /**
591
+ * The voice's sample retuned by the track's autotune (0.7), and the source
592
+ * frame its first frame came from (timed targets tune only `reads`). A
593
+ * shifted or fitted buffer keeps the file's sha256 but not its audio, so
594
+ * it is named by its own shift or fit key (`audioIds`).
595
+ */
596
+ function autotuneSample(
597
+ timing: SamplerTiming,
598
+ track: Track,
599
+ sample: DecodedSample,
600
+ decoded: boolean,
601
+ start: number,
602
+ regionStart: number,
603
+ step: number,
604
+ reads: Readonly<{ from: number; to: number }>,
605
+ ): Readonly<{ sample: DecodedSample; from: number }> {
606
+ const id = decoded ? undefined : audioIds.get(sample.mono);
607
+ const tuned = autotuneVoice(
608
+ timing.score,
609
+ track,
610
+ {
611
+ ...(id !== undefined ? { id } : decoded ? { sha256: sample.sha256 } : {}),
612
+ sampleRate: sample.sampleRate,
613
+ mono: sample.mono,
614
+ // A shifted or fitted copy keeps the file's channels unshifted:
615
+ // only the decoded file tunes in stereo.
616
+ ...(decoded && sample.left && sample.right
617
+ ? { left: sample.left, right: sample.right }
618
+ : {}),
619
+ },
620
+ start / timing.sampleRate,
621
+ regionStart,
622
+ (step * timing.sampleRate) / sample.sampleRate,
623
+ reads,
624
+ );
625
+ if (tuned.mono === sample.mono) return { sample, from: 0 };
626
+ return {
627
+ sample: Object.freeze({
628
+ ...sample,
629
+ frames: tuned.mono.length,
630
+ mono: tuned.mono,
631
+ left: tuned.left,
632
+ right: tuned.right,
633
+ }),
634
+ from: tuned.from,
635
+ };
636
+ }
637
+
638
+ /** Shifted and fitted buffers by their own cache key (0.7 autotune ids). */
639
+ const audioIds = new WeakMap<Float32Array, string>();
640
+
641
+ /** A SampleRef with a pitch or formant shift (0.6.1). */
642
+ export function isShifted(ref: SampleRef): boolean {
643
+ return (
644
+ (ref.shift !== undefined && ref.shift !== 0) || ref.formant !== undefined
645
+ );
646
+ }
647
+
648
+ /** Buffers already shifted, by the identity of the buffer they shift. */
649
+ const shiftKeys = new WeakMap<Float32Array, string>();
650
+ let shiftSerial = 0;
651
+
652
+ /**
653
+ * The shifted copy of `sample` (whole buffer, same length). Returns null
654
+ * while a live shift is still computing (the voice stays silent).
655
+ */
656
+ function shiftVoice(
657
+ ref: SampleRef,
658
+ sample: DecodedSample,
659
+ kind: "raw" | "fit",
660
+ ): DecodedSample | undefined {
661
+ // Decoded files key by content; fitted buffers (already cached by their
662
+ // own key) by identity.
663
+ let base = shiftKeys.get(sample.mono);
664
+ if (base === undefined) {
665
+ base =
666
+ kind === "raw"
667
+ ? `${sample.sha256}:${sample.sampleRate}:${sample.frames}`
668
+ : `fit#${(shiftSerial += 1)}`;
669
+ shiftKeys.set(sample.mono, base);
670
+ }
671
+ const key = `shift:${base}:${ref.shift ?? 0}:${ref.formant ?? "follow"}`;
672
+ const buffer = shiftedBuffer(
673
+ key,
674
+ sample.mono,
675
+ sample.sampleRate,
676
+ ref.shift ?? 0,
677
+ ref.formant,
678
+ );
679
+ if (!buffer) return undefined;
680
+ audioIds.set(buffer, key);
681
+ return Object.freeze({ ...sample, frames: buffer.length, mono: buffer });
682
+ }
683
+
459
684
  /** Fingerprint of a position map: its length and 64 exact samples. */
460
685
  function mapPrint(map: FitMap, frames: number): string {
461
686
  const parts = [map.length.toString()];
@@ -483,6 +708,8 @@ export function renderSamplerVoices(
483
708
  voices: readonly SamplerVoice[],
484
709
  timing: SamplerTiming,
485
710
  gainAt: (tick: number) => number,
711
+ /** The channel a stereo voice reads (absent: every voice reads mono). */
712
+ channel?: "left" | "right",
486
713
  ): void {
487
714
  const { sampleRate, score, warp } = timing;
488
715
  const samplesPerTick =
@@ -491,7 +718,8 @@ export function renderSamplerVoices(
491
718
  for (const voice of voices) {
492
719
  const { sample, step, regionStart, regionEnd, reverse, loop } = voice;
493
720
  const { loopStart, loopSpan, ramp } = voice;
494
- const mono = sample.mono;
721
+ const mono =
722
+ channel && voice.stereo ? (sample[channel] ?? sample.mono) : sample.mono;
495
723
  const span = regionEnd - regionStart;
496
724
  const crossfade = loop
497
725
  ? Math.min(LOOP_CROSSFADE_SECONDS * sample.sampleRate, loopSpan / 4)
@@ -549,7 +777,7 @@ export function renderSamplerVoices(
549
777
  const elapsed = index - voice.start;
550
778
  const value = squizzed ? squizzed[elapsed]! : read(elapsed);
551
779
  if (value === undefined) break;
552
- let envelope = Math.min(1, elapsed / attack);
780
+ let envelope = Math.min(1, elapsed / (voice.attack ?? attack));
553
781
  if (index >= fadeFrom) envelope *= (voice.end - index) / voice.fade;
554
782
  const damp = voice.damp;
555
783
  if (damp && elapsed / sampleRate > damp.from)
@@ -36,6 +36,7 @@ import {
36
36
  type TrackScore,
37
37
  } from "../../core/score.ts";
38
38
  import { GRANULAR_SOURCE_VOICE } from "../../core/granular.ts";
39
+ import { CLIP_VOICE_PREFIX, clipSampleRef } from "../../core/clips.ts";
39
40
  import { PackError, PackStore } from "./packs.ts";
40
41
  import {
41
42
  DEFAULT_MEMORY_CACHE_BYTES,
@@ -58,6 +59,12 @@ export type DecodedSample = Readonly<{
58
59
  channels: number;
59
60
  frames: number;
60
61
  mono: Float32Array;
62
+ /**
63
+ * The two channels of a stereo file (0.6.1): a resampled ref (one with
64
+ * `from`) plays them as stereo. Absent for mono and multichannel files.
65
+ */
66
+ left?: Float32Array;
67
+ right?: Float32Array;
61
68
  }>;
62
69
 
63
70
  export type SampleProblem = Readonly<{
@@ -97,7 +104,8 @@ export function hasSamplerTracks(score: TrackScore): boolean {
97
104
  packWavetable(track) !== undefined ||
98
105
  localWavetable(track) !== undefined ||
99
106
  reverbIrSample(track) !== undefined ||
100
- granularSample(track) !== undefined,
107
+ granularSample(track) !== undefined ||
108
+ (track.clips?.length ?? 0) > 0,
101
109
  );
102
110
  }
103
111
 
@@ -122,6 +130,9 @@ function trackSampleRefs(track: Track): [string, SampleRef][] {
122
130
  }
123
131
  const granular = granularSample(track);
124
132
  if (granular) refs.push([GRANULAR_SOURCE_VOICE, granular]);
133
+ // Audio clips (0.7) load as `clip:<id>` voices, sha256-pinned alike.
134
+ for (const clip of track.clips ?? [])
135
+ refs.push([`${CLIP_VOICE_PREFIX}${clip.id}`, clipSampleRef(clip)]);
125
136
  return refs;
126
137
  }
127
138
 
@@ -324,6 +335,20 @@ function checkShape(sampleRate: number, channels: number, frames: number) {
324
335
  }
325
336
 
326
337
  /** Average the channels; the renderer plays sampler voices in mono before pan. */
338
+ /** A stereo file's left and right channels (nothing for other shapes). */
339
+ export function stereoChannels(
340
+ pcm: PcmData,
341
+ ): { left: Float32Array; right: Float32Array } | Record<string, never> {
342
+ if (pcm.channels !== 2) return {};
343
+ const left = new Float32Array(pcm.frames);
344
+ const right = new Float32Array(pcm.frames);
345
+ for (let frame = 0; frame < pcm.frames; frame += 1) {
346
+ left[frame] = pcm.data[frame * 2]!;
347
+ right[frame] = pcm.data[frame * 2 + 1]!;
348
+ }
349
+ return { left, right };
350
+ }
351
+
327
352
  export function monoMix(pcm: PcmData): Float32Array {
328
353
  if (pcm.channels === 1) return pcm.data;
329
354
  const mono = new Float32Array(pcm.frames);
@@ -603,6 +628,14 @@ export class SampleLibrary implements SampleSource {
603
628
  );
604
629
  }
605
630
 
631
+ /**
632
+ * Decodes one project file (0.7: an audio clip for pitch analysis), with
633
+ * the same path rules, limits and caches as a sampler voice.
634
+ */
635
+ public decodeFile(src: string): Promise<DecodedSample> {
636
+ return this.loadFile(src);
637
+ }
638
+
606
639
  public async load(score: TrackScore): Promise<SampleBank> {
607
640
  const voices = new Map<string, DecodedSample>();
608
641
  const problems: SampleProblem[] = [];
@@ -733,7 +766,12 @@ export class SampleLibrary implements SampleSource {
733
766
  );
734
767
  voices.set(sampleKey(track.id, voice), loaded);
735
768
  } catch (error) {
736
- report("error", describeFailure(voice, src, error, this.ffmpeg));
769
+ // A clip whose audio is missing renders as silence with a warning;
770
+ // a missing instrument voice still fails the render.
771
+ report(
772
+ voice.startsWith(CLIP_VOICE_PREFIX) ? "warning" : "error",
773
+ describeFailure(voice, src, error, this.ffmpeg),
774
+ );
737
775
  }
738
776
  }
739
777
  }
@@ -896,6 +934,7 @@ export class SampleLibrary implements SampleSource {
896
934
  channels: pcm.channels,
897
935
  frames: pcm.frames,
898
936
  mono: monoMix(pcm),
937
+ ...stereoChannels(pcm),
899
938
  });
900
939
  this.remember(sample);
901
940
  return sample;
@@ -943,6 +982,7 @@ export class SampleLibrary implements SampleSource {
943
982
  channels: pcm.channels,
944
983
  frames: pcm.frames,
945
984
  mono: monoMix(pcm),
985
+ ...stereoChannels(pcm),
946
986
  });
947
987
  this.remember(sample);
948
988
  return sample;
@@ -966,7 +1006,10 @@ export class SampleLibrary implements SampleSource {
966
1006
  }
967
1007
 
968
1008
  private remember(sample: DecodedSample): void {
969
- const bytes = sample.mono.byteLength;
1009
+ const bytes =
1010
+ sample.mono.byteLength +
1011
+ (sample.left?.byteLength ?? 0) +
1012
+ (sample.right?.byteLength ?? 0);
970
1013
  this.memory.set(sample.sha256, { sample, bytes });
971
1014
  this.memoryBytes += bytes;
972
1015
  for (const [sha, entry] of this.memory) {