@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.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/src/audio/sampler.ts
CHANGED
|
@@ -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
|
-
): (
|
|
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
|
|
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
|
|
181
|
-
for (const candidate of keyed)
|
|
182
|
-
if (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
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
260
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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)
|
package/src/audio/samples.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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) {
|