@hraness/dawg 0.6.1 → 0.8.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 +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- 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/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- 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 +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -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 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -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 +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -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 +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- 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/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -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/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- 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 +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- 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 +82 -32
- 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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- 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 +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- 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 +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- 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 +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- 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 +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
package/src/audio/resample.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* frames, so held notes and effect tails cross into the range as in the
|
|
20
20
|
* song.
|
|
21
21
|
*/
|
|
22
|
+
import { withClipLengths } from "./clips.ts";
|
|
22
23
|
import { createHash } from "node:crypto";
|
|
23
24
|
import {
|
|
24
25
|
SCORE_LIMITS,
|
|
@@ -216,7 +217,7 @@ export function renderResample(
|
|
|
216
217
|
startTick,
|
|
217
218
|
durationTicks,
|
|
218
219
|
leadTicks,
|
|
219
|
-
} = resampleScore(score, request);
|
|
220
|
+
} = resampleScore(withClipLengths(score, samples), request);
|
|
220
221
|
// The song's export rate (48 kHz once it has a master), so the new
|
|
221
222
|
// track nulls against the source when both play at that rate.
|
|
222
223
|
const sampleRate = exportSampleRate(score);
|
package/src/audio/sampler.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
type SampleWarp,
|
|
49
49
|
} from "./warp.ts";
|
|
50
50
|
import { fittedBuffer, shiftedBuffer, windowAt } from "./fit.ts";
|
|
51
|
+
import { autotuneVoice } from "./autotune.ts";
|
|
51
52
|
import {
|
|
52
53
|
performanceTimingFor,
|
|
53
54
|
tunedTiming,
|
|
@@ -419,9 +420,36 @@ export function planSamplerVoices(
|
|
|
419
420
|
: ref.loopEnd * sample.frames;
|
|
420
421
|
const start = noteStartFrame(note, timing);
|
|
421
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 };
|
|
422
450
|
const voice: SamplerVoice = {
|
|
423
451
|
voice: target.voice,
|
|
424
|
-
sample,
|
|
452
|
+
sample: tuned.sample,
|
|
425
453
|
startTick: note.startTick,
|
|
426
454
|
start,
|
|
427
455
|
end: start + length,
|
|
@@ -429,8 +457,8 @@ export function planSamplerVoices(
|
|
|
429
457
|
step,
|
|
430
458
|
reverse: speed < 0,
|
|
431
459
|
loop,
|
|
432
|
-
regionStart,
|
|
433
|
-
regionEnd,
|
|
460
|
+
regionStart: regionStart - tuned.from,
|
|
461
|
+
regionEnd: regionEnd - tuned.from,
|
|
434
462
|
gain: (ref.gain ?? 1) * Math.max(0, Math.min(1, note.velocity)),
|
|
435
463
|
choke: ref.choke,
|
|
436
464
|
loopStart: speed < 0 ? regionEnd - loopTo : loopFrom - regionStart,
|
|
@@ -440,7 +468,7 @@ export function planSamplerVoices(
|
|
|
440
468
|
...(ref.fadeInTime !== undefined
|
|
441
469
|
? { attack: Math.max(1, ref.fadeInTime * sampleRate) }
|
|
442
470
|
: {}),
|
|
443
|
-
...(ref.from !== undefined && sample.left && sample.right
|
|
471
|
+
...(ref.from !== undefined && tuned.sample.left && tuned.sample.right
|
|
444
472
|
? { stereo: true }
|
|
445
473
|
: {}),
|
|
446
474
|
...(performance?.cents ? { cents: performance.cents } : {}),
|
|
@@ -535,6 +563,7 @@ function fitVoice(
|
|
|
535
563
|
length,
|
|
536
564
|
);
|
|
537
565
|
if (!buffer) return null;
|
|
566
|
+
audioIds.set(buffer, key);
|
|
538
567
|
const sample: DecodedSample = Object.freeze({
|
|
539
568
|
sha256: decoded.sha256,
|
|
540
569
|
sampleRate,
|
|
@@ -558,6 +587,57 @@ function fitVoice(
|
|
|
558
587
|
});
|
|
559
588
|
}
|
|
560
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
|
+
|
|
561
641
|
/** A SampleRef with a pitch or formant shift (0.6.1). */
|
|
562
642
|
export function isShifted(ref: SampleRef): boolean {
|
|
563
643
|
return (
|
|
@@ -597,6 +677,7 @@ function shiftVoice(
|
|
|
597
677
|
ref.formant,
|
|
598
678
|
);
|
|
599
679
|
if (!buffer) return undefined;
|
|
680
|
+
audioIds.set(buffer, key);
|
|
600
681
|
return Object.freeze({ ...sample, frames: buffer.length, mono: buffer });
|
|
601
682
|
}
|
|
602
683
|
|
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,
|
|
@@ -103,7 +104,8 @@ export function hasSamplerTracks(score: TrackScore): boolean {
|
|
|
103
104
|
packWavetable(track) !== undefined ||
|
|
104
105
|
localWavetable(track) !== undefined ||
|
|
105
106
|
reverbIrSample(track) !== undefined ||
|
|
106
|
-
granularSample(track) !== undefined
|
|
107
|
+
granularSample(track) !== undefined ||
|
|
108
|
+
(track.clips?.length ?? 0) > 0,
|
|
107
109
|
);
|
|
108
110
|
}
|
|
109
111
|
|
|
@@ -128,6 +130,9 @@ function trackSampleRefs(track: Track): [string, SampleRef][] {
|
|
|
128
130
|
}
|
|
129
131
|
const granular = granularSample(track);
|
|
130
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)]);
|
|
131
136
|
return refs;
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -623,6 +628,14 @@ export class SampleLibrary implements SampleSource {
|
|
|
623
628
|
);
|
|
624
629
|
}
|
|
625
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
|
+
|
|
626
639
|
public async load(score: TrackScore): Promise<SampleBank> {
|
|
627
640
|
const voices = new Map<string, DecodedSample>();
|
|
628
641
|
const problems: SampleProblem[] = [];
|
|
@@ -753,7 +766,12 @@ export class SampleLibrary implements SampleSource {
|
|
|
753
766
|
);
|
|
754
767
|
voices.set(sampleKey(track.id, voice), loaded);
|
|
755
768
|
} catch (error) {
|
|
756
|
-
|
|
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
|
+
);
|
|
757
775
|
}
|
|
758
776
|
}
|
|
759
777
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only measurement helpers for the sing engine (formant.md section 9;
|
|
3
|
+
* never on the render path): a YIN f0 tracker (the pitch lane's
|
|
4
|
+
* src/audio/dsp/pitch.ts replaces it once merged), harmonic levels, the
|
|
5
|
+
* alias-to-harmonic power ratio and harmonic-peak formant fits. Ported from
|
|
6
|
+
* the reviewed prototype (proto/formant/analysis.ts, review.ts).
|
|
7
|
+
*/
|
|
8
|
+
import { fftInPlace } from "../dsp/fft.ts";
|
|
9
|
+
import { hann } from "../dsp/stft.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* YIN (de Cheveigne and Kawahara 2002) with an FFT difference function,
|
|
13
|
+
* CMNDF threshold, parabolic refinement. Returns f0 per hop (0 unvoiced).
|
|
14
|
+
*/
|
|
15
|
+
export function yin(
|
|
16
|
+
x: Float64Array,
|
|
17
|
+
sampleRate: number,
|
|
18
|
+
hop: number,
|
|
19
|
+
minHz = 60,
|
|
20
|
+
maxHz = 1000,
|
|
21
|
+
threshold = 0.12,
|
|
22
|
+
): Float64Array {
|
|
23
|
+
const tauMax = Math.ceil(sampleRate / minHz);
|
|
24
|
+
const tauMin = Math.floor(sampleRate / maxHz);
|
|
25
|
+
const w = tauMax; // integration window
|
|
26
|
+
let n = 1;
|
|
27
|
+
while (n < w + tauMax) n <<= 1;
|
|
28
|
+
const frames = Math.floor(x.length / hop);
|
|
29
|
+
const result = new Float64Array(frames);
|
|
30
|
+
const re = new Float64Array(n);
|
|
31
|
+
const im = new Float64Array(n);
|
|
32
|
+
const re2 = new Float64Array(n);
|
|
33
|
+
const im2 = new Float64Array(n);
|
|
34
|
+
const d = new Float64Array(tauMax + 1);
|
|
35
|
+
for (let f = 0; f < frames; f += 1) {
|
|
36
|
+
const start = f * hop - Math.floor(w / 2);
|
|
37
|
+
// r(tau) = sum_{j<w} x[j] x[j+tau]: correlate window (len w) with segment (len w+tauMax)
|
|
38
|
+
re.fill(0);
|
|
39
|
+
im.fill(0);
|
|
40
|
+
re2.fill(0);
|
|
41
|
+
im2.fill(0);
|
|
42
|
+
let energy0 = 0;
|
|
43
|
+
for (let j = 0; j < w + tauMax; j += 1) {
|
|
44
|
+
const k = start + j;
|
|
45
|
+
const v = k >= 0 && k < x.length ? x[k]! : 0;
|
|
46
|
+
re2[j] = v;
|
|
47
|
+
if (j < w) {
|
|
48
|
+
re[j] = v;
|
|
49
|
+
energy0 += v * v;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (energy0 < 1e-7) continue;
|
|
53
|
+
fftInPlace(re, im);
|
|
54
|
+
fftInPlace(re2, im2);
|
|
55
|
+
for (let k = 0; k < n; k += 1) {
|
|
56
|
+
// conj(A) * B
|
|
57
|
+
const a = re[k]!;
|
|
58
|
+
const b = -im[k]!;
|
|
59
|
+
const c = re2[k]!;
|
|
60
|
+
const e = im2[k]!;
|
|
61
|
+
re[k] = a * c - b * e;
|
|
62
|
+
im[k] = a * e + b * c;
|
|
63
|
+
}
|
|
64
|
+
fftInPlace(re, im, true);
|
|
65
|
+
// running energy of x[start+tau .. start+tau+w)
|
|
66
|
+
let et = energy0;
|
|
67
|
+
d[0] = 0;
|
|
68
|
+
let running = 0;
|
|
69
|
+
let found = -1;
|
|
70
|
+
const cm = new Float64Array(tauMax + 1);
|
|
71
|
+
cm[0] = 1;
|
|
72
|
+
for (let tau = 1; tau <= tauMax; tau += 1) {
|
|
73
|
+
const outIdx = start + tau - 1;
|
|
74
|
+
const inIdx = start + tau - 1 + w;
|
|
75
|
+
const vOut = outIdx >= 0 && outIdx < x.length ? x[outIdx]! : 0;
|
|
76
|
+
const vIn = inIdx >= 0 && inIdx < x.length ? x[inIdx]! : 0;
|
|
77
|
+
et += vIn * vIn - vOut * vOut;
|
|
78
|
+
d[tau] = energy0 + et - (2 * re[tau]!) / n;
|
|
79
|
+
running += d[tau]!;
|
|
80
|
+
cm[tau] = running > 0 ? (d[tau]! * tau) / running : 1;
|
|
81
|
+
}
|
|
82
|
+
for (let tau = tauMin; tau < tauMax; tau += 1) {
|
|
83
|
+
if (cm[tau]! < threshold) {
|
|
84
|
+
while (tau + 1 < tauMax && cm[tau + 1]! < cm[tau]!) tau += 1;
|
|
85
|
+
found = tau;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (found < 0) continue;
|
|
90
|
+
const a = cm[found - 1]!;
|
|
91
|
+
const b = cm[found]!;
|
|
92
|
+
const c = cm[found + 1]!;
|
|
93
|
+
const den = a - 2 * b + c;
|
|
94
|
+
const shift = den !== 0 ? (0.5 * (a - c)) / den : 0;
|
|
95
|
+
result[f] = sampleRate / (found + Math.max(-1, Math.min(1, shift)));
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** dB magnitude of harmonics 1..count at `f0` from a Hann frame centred at `centre`. */
|
|
101
|
+
export function harmonicLevels(
|
|
102
|
+
x: Float64Array,
|
|
103
|
+
sampleRate: number,
|
|
104
|
+
centre: number,
|
|
105
|
+
f0: number,
|
|
106
|
+
maxHz: number,
|
|
107
|
+
n = 4096,
|
|
108
|
+
): number[] {
|
|
109
|
+
const re = new Float64Array(n);
|
|
110
|
+
const im = new Float64Array(n);
|
|
111
|
+
const w = hann(n);
|
|
112
|
+
for (let i = 0; i < n; i += 1) {
|
|
113
|
+
const j = centre - n / 2 + i;
|
|
114
|
+
re[i] = j >= 0 && j < x.length ? x[j]! * w[i]! : 0;
|
|
115
|
+
}
|
|
116
|
+
fftInPlace(re, im);
|
|
117
|
+
const levels: number[] = [];
|
|
118
|
+
for (let h = 1; h * f0 < maxHz; h += 1) {
|
|
119
|
+
const bin = (h * f0 * n) / sampleRate;
|
|
120
|
+
let best = 0;
|
|
121
|
+
for (let k = Math.floor(bin) - 2; k <= Math.ceil(bin) + 2; k += 1)
|
|
122
|
+
best = Math.max(best, Math.hypot(re[k]!, im[k]!));
|
|
123
|
+
levels.push(20 * Math.log10(best + 1e-12));
|
|
124
|
+
}
|
|
125
|
+
return levels;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function median(v: number[]): number {
|
|
129
|
+
const s = [...v].sort((a, b) => a - b);
|
|
130
|
+
return s.length ? s[Math.floor(s.length / 2)]! : NaN;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Inharmonic over harmonic power between 50 Hz and 5 kHz, dB. */
|
|
134
|
+
export function aliasDb(x: Float64Array, sr: number, f0: number): number {
|
|
135
|
+
const n = 16384;
|
|
136
|
+
const re = new Float64Array(n);
|
|
137
|
+
const im = new Float64Array(n);
|
|
138
|
+
const off = x.length - n;
|
|
139
|
+
for (let i = 0; i < n; i += 1) {
|
|
140
|
+
const t = (2 * Math.PI * i) / (n - 1);
|
|
141
|
+
const w =
|
|
142
|
+
0.35875 -
|
|
143
|
+
0.48829 * Math.cos(t) +
|
|
144
|
+
0.14128 * Math.cos(2 * t) -
|
|
145
|
+
0.01168 * Math.cos(3 * t);
|
|
146
|
+
re[i] = x[off + i]! * w;
|
|
147
|
+
}
|
|
148
|
+
fftInPlace(re, im);
|
|
149
|
+
let harm = 0;
|
|
150
|
+
let alias = 0;
|
|
151
|
+
for (let k = Math.ceil((50 * n) / sr); k < (5000 * n) / sr; k += 1) {
|
|
152
|
+
const hz = (k * sr) / n;
|
|
153
|
+
const h = Math.round(hz / f0);
|
|
154
|
+
const p = re[k]! ** 2 + im[k]! ** 2;
|
|
155
|
+
if (h >= 1 && Math.abs(hz - h * f0) <= (5 * sr) / n) harm += p;
|
|
156
|
+
else alias += p;
|
|
157
|
+
}
|
|
158
|
+
return 10 * Math.log10(alias / harm);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Peaks of a harmonic-level curve, parabolically refined, in Hz. */
|
|
162
|
+
export function harmonicPeaks(levels: readonly number[], f0: number): number[] {
|
|
163
|
+
const peaks: number[] = [];
|
|
164
|
+
for (let h = 1; h < levels.length - 1; h += 1) {
|
|
165
|
+
const [a, b, c] = [levels[h - 1]!, levels[h]!, levels[h + 1]!];
|
|
166
|
+
if (b > a && b >= c) {
|
|
167
|
+
const d = a - 2 * b + c;
|
|
168
|
+
const off = d < 0 ? (0.5 * (a - c)) / d : 0;
|
|
169
|
+
peaks.push((h + 1 + off) * f0);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return peaks;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Spectral centroid (Hz) of an 8192-sample frame from `from`. */
|
|
176
|
+
export function centroidHz(x: Float64Array, sr: number, from: number): number {
|
|
177
|
+
const n = 8192;
|
|
178
|
+
const re = new Float64Array(n);
|
|
179
|
+
const im = new Float64Array(n);
|
|
180
|
+
const w = hann(n);
|
|
181
|
+
for (let i = 0; i < n; i += 1) re[i] = (x[from + i] ?? 0) * w[i]!;
|
|
182
|
+
fftInPlace(re, im);
|
|
183
|
+
let num = 0;
|
|
184
|
+
let den = 0;
|
|
185
|
+
for (let k = 1; k < n / 2; k += 1) {
|
|
186
|
+
const p = Math.hypot(re[k]!, im[k]!);
|
|
187
|
+
num += p * ((k * sr) / n);
|
|
188
|
+
den += p;
|
|
189
|
+
}
|
|
190
|
+
return num / den;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const cents = (a: number, b: number): number => 1200 * Math.log2(a / b);
|