@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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio clips (0.7): files placed on a track's timeline, summed into the
|
|
3
|
+
* track's dry buffer before its effect chain (sources.md section 7). The
|
|
4
|
+
* pure placement and section cuts live in core/clips.ts; this module reads
|
|
5
|
+
* the decoded audio, places it through the tempo map, resamples it to the
|
|
6
|
+
* render rate (4-point Hermite), applies the take's clock-drift stretch,
|
|
7
|
+
* reverse, gain and equal-power fades, and multiplies by the track's
|
|
8
|
+
* volume the way every voice does. A clip whose audio did not load renders
|
|
9
|
+
* silence (the loader reported it as a warning).
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
CLIP_VOICE_PREFIX,
|
|
13
|
+
DEFAULT_CLIP_FADE,
|
|
14
|
+
clipSongTick,
|
|
15
|
+
resolveClipLengths,
|
|
16
|
+
} from "../../core/clips.ts";
|
|
17
|
+
import type { AudioClip, Take, Track, TrackScore } from "../../core/score.ts";
|
|
18
|
+
import { autotuneClip } from "./autotune.ts";
|
|
19
|
+
import { sampleKey, type DecodedSample, type SampleBank } from "./samples.ts";
|
|
20
|
+
import type { SampleWarp } from "./warp.ts";
|
|
21
|
+
|
|
22
|
+
/** What clip rendering reads of the render context. */
|
|
23
|
+
export type ClipContext = Readonly<{
|
|
24
|
+
sampleRate: number;
|
|
25
|
+
samples: number;
|
|
26
|
+
samplesPerTick: number;
|
|
27
|
+
warp?: SampleWarp;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
/** True when a track has clips that sound. */
|
|
31
|
+
export function hasClips(track: Track | undefined): boolean {
|
|
32
|
+
return track?.clips?.some((clip) => !clip.mute) ?? false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The decoded audio of a clip, if it loaded. */
|
|
36
|
+
export function clipAudio(
|
|
37
|
+
bank: SampleBank | undefined,
|
|
38
|
+
trackId: string,
|
|
39
|
+
clip: AudioClip,
|
|
40
|
+
): DecodedSample | undefined {
|
|
41
|
+
if (!bank) return undefined;
|
|
42
|
+
const exact = bank.voices.get(
|
|
43
|
+
sampleKey(trackId, `${CLIP_VOICE_PREFIX}${clip.id}`),
|
|
44
|
+
);
|
|
45
|
+
if (exact) return exact;
|
|
46
|
+
// A section or form pass of a clip is `<id>~<n>`: it plays the audio
|
|
47
|
+
// the bank loaded for `<id>` from the unsliced score.
|
|
48
|
+
const pass = clip.id.lastIndexOf("~");
|
|
49
|
+
return pass > 0
|
|
50
|
+
? bank.voices.get(
|
|
51
|
+
sampleKey(trackId, `${CLIP_VOICE_PREFIX}${clip.id.slice(0, pass)}`),
|
|
52
|
+
)
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `score` with each loaded clip's missing `dur` resolved from its file
|
|
58
|
+
* (`resolveClipLengths`), so section, form and window cuts see its real
|
|
59
|
+
* end. A score without clips, or without a bank, comes back as it is.
|
|
60
|
+
*/
|
|
61
|
+
export function withClipLengths(
|
|
62
|
+
score: TrackScore,
|
|
63
|
+
bank: SampleBank | undefined,
|
|
64
|
+
): TrackScore {
|
|
65
|
+
if (!bank || !score.tracks.some((track) => track.clips)) return score;
|
|
66
|
+
return resolveClipLengths(score, (trackId, clip) => {
|
|
67
|
+
const audio = clipAudio(bank, trackId, clip);
|
|
68
|
+
return audio && audio.frames > 0
|
|
69
|
+
? audio.frames / audio.sampleRate
|
|
70
|
+
: undefined;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Fractional render sample where a score tick sounds. */
|
|
75
|
+
function sampleAt(context: ClipContext, tick: number): number {
|
|
76
|
+
return context.warp
|
|
77
|
+
? context.warp.sample(tick)
|
|
78
|
+
: tick * context.samplesPerTick;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The take a clip plays from, if any. */
|
|
82
|
+
function takeOf(track: Track, clip: AudioClip): Take | undefined {
|
|
83
|
+
if (clip.take === undefined) return undefined;
|
|
84
|
+
return track.takes?.find((take) => take.name === clip.take);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Catmull-Rom (4-point Hermite) read at a fractional frame, 0 outside. */
|
|
88
|
+
function readHermite(data: Float32Array, position: number): number {
|
|
89
|
+
const base = Math.floor(position);
|
|
90
|
+
const t = position - base;
|
|
91
|
+
const at = (index: number) =>
|
|
92
|
+
index >= 0 && index < data.length ? data[index]! : 0;
|
|
93
|
+
const y0 = at(base - 1);
|
|
94
|
+
const y1 = at(base);
|
|
95
|
+
const y2 = at(base + 1);
|
|
96
|
+
const y3 = at(base + 2);
|
|
97
|
+
const c1 = 0.5 * (y2 - y0);
|
|
98
|
+
const c2 = y0 - 2.5 * y1 + 2 * y2 - 0.5 * y3;
|
|
99
|
+
const c3 = 0.5 * (y3 - y0) + 1.5 * (y1 - y2);
|
|
100
|
+
return ((c3 * t + c2) * t + c1) * t + y1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Equal-power fade gain for `x` in 0..1 (sin of a quarter turn). */
|
|
104
|
+
export function equalPowerFade(x: number): number {
|
|
105
|
+
if (x <= 0) return 0;
|
|
106
|
+
if (x >= 1) return 1;
|
|
107
|
+
return Math.sin((x * Math.PI) / 2);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sum `track`'s clips into `target` (the mono dry buffer). `gainAt(tick)`
|
|
112
|
+
* is the track volume with automation; `tickAt(sample)` maps a render
|
|
113
|
+
* sample back to a score tick for it. Returns the clips that did not
|
|
114
|
+
* load (they stay silent). With `score` and a `Track.autotune` (0.7), each
|
|
115
|
+
* clip plays retuned (`autotuneClip`, keyed by its offset and nudge);
|
|
116
|
+
* reversed clips play untuned.
|
|
117
|
+
*/
|
|
118
|
+
export function renderClips(
|
|
119
|
+
target: Float64Array,
|
|
120
|
+
track: Track,
|
|
121
|
+
context: ClipContext,
|
|
122
|
+
bank: SampleBank | undefined,
|
|
123
|
+
gainAt: (tick: number) => number,
|
|
124
|
+
score?: TrackScore,
|
|
125
|
+
): string[] {
|
|
126
|
+
const missing: string[] = [];
|
|
127
|
+
if (!track.clips) return missing;
|
|
128
|
+
const { sampleRate, samples } = context;
|
|
129
|
+
const tickAt = (index: number) =>
|
|
130
|
+
context.warp ? context.warp.tick(index) : index / context.samplesPerTick;
|
|
131
|
+
for (const clip of track.clips) {
|
|
132
|
+
if (clip.mute) continue;
|
|
133
|
+
const audio = clipAudio(bank, track.id, clip);
|
|
134
|
+
if (!audio || audio.frames === 0) {
|
|
135
|
+
missing.push(clip.id);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const take = takeOf(track, clip);
|
|
139
|
+
// A take recorded on a drifting clock (`ppm`) plays slightly faster or
|
|
140
|
+
// slower so it stays on the grid; `nudge` moves it in milliseconds.
|
|
141
|
+
const drift = 1 + (take?.ppm ?? 0) / 1e6;
|
|
142
|
+
const nudge = (take?.nudge ?? 0) / 1000;
|
|
143
|
+
const fileSeconds = audio.frames / audio.sampleRate;
|
|
144
|
+
const offset = Math.min(fileSeconds, clip.offset ?? 0);
|
|
145
|
+
const length = Math.max(
|
|
146
|
+
0,
|
|
147
|
+
Math.min(
|
|
148
|
+
clip.dur ?? Number.POSITIVE_INFINITY,
|
|
149
|
+
(fileSeconds - offset) / drift,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
if (length <= 0) continue;
|
|
153
|
+
const start =
|
|
154
|
+
sampleAt(context, clipSongTick(clip, track.time)) + nudge * sampleRate;
|
|
155
|
+
const total = length * sampleRate;
|
|
156
|
+
const first = Math.max(0, Math.ceil(start));
|
|
157
|
+
const last = Math.min(samples, Math.ceil(start + total));
|
|
158
|
+
if (first >= last) continue;
|
|
159
|
+
const step = (audio.sampleRate / sampleRate) * drift;
|
|
160
|
+
const begin = offset * audio.sampleRate;
|
|
161
|
+
const span = length * audio.sampleRate * drift;
|
|
162
|
+
const fadeIn = Math.min(clip.fadeInTime ?? DEFAULT_CLIP_FADE, length / 2);
|
|
163
|
+
const fadeOut = Math.min(clip.fadeTime ?? DEFAULT_CLIP_FADE, length / 2);
|
|
164
|
+
const fadeInFrames = fadeIn * sampleRate;
|
|
165
|
+
const fadeOutFrames = fadeOut * sampleRate;
|
|
166
|
+
const gain = clip.gain ?? 1;
|
|
167
|
+
// 0.7 autotune: the clip's audio retuned at the song second its
|
|
168
|
+
// offset sounds (`start` already holds the nudge).
|
|
169
|
+
const tuned =
|
|
170
|
+
track.autotune && score && !clip.rev
|
|
171
|
+
? autotuneClip(
|
|
172
|
+
score,
|
|
173
|
+
track,
|
|
174
|
+
{
|
|
175
|
+
sha256: audio.sha256,
|
|
176
|
+
sampleRate: audio.sampleRate,
|
|
177
|
+
mono: audio.mono,
|
|
178
|
+
},
|
|
179
|
+
start / sampleRate,
|
|
180
|
+
offset,
|
|
181
|
+
clip.id,
|
|
182
|
+
0,
|
|
183
|
+
nudge,
|
|
184
|
+
)
|
|
185
|
+
: undefined;
|
|
186
|
+
const data = tuned ? tuned.mono : audio.mono;
|
|
187
|
+
const shiftFrom = tuned ? tuned.from : 0;
|
|
188
|
+
// Track volume (with automation) is read once per 32-sample block.
|
|
189
|
+
let blockGain = 0;
|
|
190
|
+
for (let index = first; index < last; index += 1) {
|
|
191
|
+
if ((index - first) % 32 === 0) blockGain = gainAt(tickAt(index));
|
|
192
|
+
const elapsed = index - start;
|
|
193
|
+
let position = elapsed * step;
|
|
194
|
+
if (position >= span) break;
|
|
195
|
+
position = clip.rev ? begin + span - 1 - position : begin + position;
|
|
196
|
+
let shape = gain * blockGain;
|
|
197
|
+
if (fadeInFrames > 0 && elapsed < fadeInFrames)
|
|
198
|
+
shape *= equalPowerFade(elapsed / fadeInFrames);
|
|
199
|
+
const remaining = total - elapsed;
|
|
200
|
+
if (fadeOutFrames > 0 && remaining < fadeOutFrames)
|
|
201
|
+
shape *= equalPowerFade(remaining / fadeOutFrames);
|
|
202
|
+
target[index]! += readHermite(data, position - shiftFrom) * shape;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return missing;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Stem-cache key part for a track's clips: each clip's sha256 (the one
|
|
210
|
+
* loaded, so an edited file re-renders), placement, cut, gain, fades and
|
|
211
|
+
* reverse, and the drift of the take it plays from.
|
|
212
|
+
*/
|
|
213
|
+
export function clipsDigest(
|
|
214
|
+
track: Track,
|
|
215
|
+
bank: SampleBank | undefined,
|
|
216
|
+
guide = false,
|
|
217
|
+
): string | undefined {
|
|
218
|
+
if (!track.clips || track.clips.length === 0) return undefined;
|
|
219
|
+
return (
|
|
220
|
+
(guide ? "guide|" : "") +
|
|
221
|
+
track.clips
|
|
222
|
+
.map((clip) => {
|
|
223
|
+
const take = takeOf(track, clip);
|
|
224
|
+
return [
|
|
225
|
+
clip.id,
|
|
226
|
+
clipAudio(bank, track.id, clip)?.sha256 ?? `missing:${clip.sha256}`,
|
|
227
|
+
clip.startTick,
|
|
228
|
+
clip.offset ?? "",
|
|
229
|
+
clip.dur ?? "",
|
|
230
|
+
clip.gain ?? "",
|
|
231
|
+
clip.fadeInTime ?? "",
|
|
232
|
+
clip.fadeTime ?? "",
|
|
233
|
+
clip.rev ? "r" : "",
|
|
234
|
+
clip.mute ? "m" : "",
|
|
235
|
+
take ? `${take.ppm ?? 0}/${take.nudge ?? 0}` : "",
|
|
236
|
+
].join(",");
|
|
237
|
+
})
|
|
238
|
+
.join(";")
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dawg doctor`: how dawg makes sound on this machine. The backend and why
|
|
3
|
+
* it was chosen, the native sink's verification (or why it fell back), the
|
|
4
|
+
* audio devices, and the play-mode lead.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
AudioEngine,
|
|
8
|
+
detectAudioBackend,
|
|
9
|
+
type AudioBackendInfo,
|
|
10
|
+
} from "./engine.ts";
|
|
11
|
+
import { PREBUILT_DIR, nativeTarget } from "./native.ts";
|
|
12
|
+
import { DEFAULT_SAMPLE_RATE } from "./wav.ts";
|
|
13
|
+
|
|
14
|
+
export type DoctorReport = Readonly<{
|
|
15
|
+
backend: AudioBackendInfo["backend"];
|
|
16
|
+
detail: string;
|
|
17
|
+
native: Readonly<{
|
|
18
|
+
target: string | null;
|
|
19
|
+
dir: string;
|
|
20
|
+
loaded: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}>;
|
|
23
|
+
playLeadMs: number;
|
|
24
|
+
outputs: readonly string[];
|
|
25
|
+
inputs: readonly string[];
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export function audioDoctor(info = detectAudioBackend()): DoctorReport {
|
|
29
|
+
const engine = new AudioEngine({ info, worker: false, timer: false });
|
|
30
|
+
const playLeadMs = engine.playLeadMs;
|
|
31
|
+
void engine.dispose();
|
|
32
|
+
const list = (input: boolean) =>
|
|
33
|
+
info.native
|
|
34
|
+
?.devices(input)
|
|
35
|
+
.map(
|
|
36
|
+
(d) =>
|
|
37
|
+
`${d.name}${d.default ? " (default)" : ""} · ${d.channels} ch · ${d.rate} Hz`,
|
|
38
|
+
) ?? [];
|
|
39
|
+
return {
|
|
40
|
+
backend: info.backend,
|
|
41
|
+
detail: info.detail,
|
|
42
|
+
native: {
|
|
43
|
+
target: nativeTarget() ?? null,
|
|
44
|
+
dir: PREBUILT_DIR,
|
|
45
|
+
loaded: info.backend === "native",
|
|
46
|
+
...(info.nativeUnavailable ? { reason: info.nativeUnavailable } : {}),
|
|
47
|
+
},
|
|
48
|
+
playLeadMs,
|
|
49
|
+
outputs: list(false),
|
|
50
|
+
inputs: list(true),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatAudioDoctor(report: DoctorReport): string[] {
|
|
55
|
+
const lines = [
|
|
56
|
+
`audio: ${report.backend} · ${report.detail}`,
|
|
57
|
+
report.native.loaded
|
|
58
|
+
? `native sink: loaded (${report.native.target})`
|
|
59
|
+
: `native sink: not in use · ${report.native.reason ?? "another backend was forced"} · falls back to ffplay, sox or afplay`,
|
|
60
|
+
`play lead: ${report.playLeadMs} ms · render rate ${DEFAULT_SAMPLE_RATE} Hz`,
|
|
61
|
+
];
|
|
62
|
+
if (report.outputs.length > 0)
|
|
63
|
+
lines.push("outputs:", ...report.outputs.map((d) => ` ${d}`));
|
|
64
|
+
if (report.inputs.length > 0)
|
|
65
|
+
lines.push("inputs:", ...report.inputs.map((d) => ` ${d}`));
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runDoctorCommand(
|
|
70
|
+
args: readonly string[],
|
|
71
|
+
stdout: { write(text: string): unknown },
|
|
72
|
+
): Promise<number> {
|
|
73
|
+
if (args.some((arg) => arg === "--help" || arg === "-h")) {
|
|
74
|
+
stdout.write(
|
|
75
|
+
"usage: dawg doctor [--json] audio backend, native sink, devices\n",
|
|
76
|
+
);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const report = audioDoctor();
|
|
80
|
+
stdout.write(
|
|
81
|
+
args.includes("--json")
|
|
82
|
+
? `${JSON.stringify(report, null, 2)}\n`
|
|
83
|
+
: `${formatAudioDoctor(report).join("\n")}\n`,
|
|
84
|
+
);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Band-pass banks (0.7 vocoder): RBJ biquads, their group delay and a
|
|
3
|
+
* sample-rate-independent band layout. Band centres and widths depend only
|
|
4
|
+
* on the band count, range and width, never on the sample rate: a band whose
|
|
5
|
+
* centre lies above 0.45 sr is muted instead, so a 22 050 Hz audition and a
|
|
6
|
+
* 48 kHz export have the same layout.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Biquad = Readonly<{
|
|
10
|
+
b0: number;
|
|
11
|
+
b1: number;
|
|
12
|
+
b2: number;
|
|
13
|
+
a1: number;
|
|
14
|
+
a2: number;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/** RBJ constant-0-dB-peak band-pass, bandwidth in octaves, pre-warped. */
|
|
18
|
+
export function bandpass(
|
|
19
|
+
hz: number,
|
|
20
|
+
octaves: number,
|
|
21
|
+
sampleRate: number,
|
|
22
|
+
): Biquad {
|
|
23
|
+
const w = (2 * Math.PI * hz) / sampleRate;
|
|
24
|
+
const s = Math.sin(w);
|
|
25
|
+
const alpha = s * Math.sinh(((Math.LN2 / 2) * octaves * w) / s);
|
|
26
|
+
const a0 = 1 + alpha;
|
|
27
|
+
return {
|
|
28
|
+
b0: alpha / a0,
|
|
29
|
+
b1: 0,
|
|
30
|
+
b2: -alpha / a0,
|
|
31
|
+
a1: (-2 * Math.cos(w)) / a0,
|
|
32
|
+
a2: (1 - alpha) / a0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** RBJ Butterworth high-pass (Q = 1/sqrt 2). */
|
|
37
|
+
export function highpass(hz: number, sampleRate: number): Biquad {
|
|
38
|
+
const w = (2 * Math.PI * hz) / sampleRate;
|
|
39
|
+
const alpha = Math.sin(w) / (2 * Math.SQRT1_2);
|
|
40
|
+
const c = Math.cos(w);
|
|
41
|
+
const a0 = 1 + alpha;
|
|
42
|
+
return {
|
|
43
|
+
b0: (1 + c) / 2 / a0,
|
|
44
|
+
b1: -(1 + c) / a0,
|
|
45
|
+
b2: (1 + c) / 2 / a0,
|
|
46
|
+
a1: (-2 * c) / a0,
|
|
47
|
+
a2: (1 - alpha) / a0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Group delay in samples of one biquad at angle `w` (numeric phase slope). */
|
|
52
|
+
export function groupDelay(f: Biquad, w: number): number {
|
|
53
|
+
const phase = (x: number): number => {
|
|
54
|
+
const nr = f.b0 + f.b1 * Math.cos(x) + f.b2 * Math.cos(2 * x);
|
|
55
|
+
const ni = -(f.b1 * Math.sin(x) + f.b2 * Math.sin(2 * x));
|
|
56
|
+
const dr = 1 + f.a1 * Math.cos(x) + f.a2 * Math.cos(2 * x);
|
|
57
|
+
const di = -(f.a1 * Math.sin(x) + f.a2 * Math.sin(2 * x));
|
|
58
|
+
return Math.atan2(ni, nr) - Math.atan2(di, dr);
|
|
59
|
+
};
|
|
60
|
+
const d = 1e-5;
|
|
61
|
+
let dp = phase(w + d) - phase(w - d);
|
|
62
|
+
while (dp > Math.PI) dp -= 2 * Math.PI;
|
|
63
|
+
while (dp < -Math.PI) dp += 2 * Math.PI;
|
|
64
|
+
return -dp / (2 * d);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Runs a biquad over `x` in place (transposed direct form II). */
|
|
68
|
+
export function runBiquad(f: Biquad, x: Float64Array): void {
|
|
69
|
+
let z1 = 0;
|
|
70
|
+
let z2 = 0;
|
|
71
|
+
const { b0, b1, b2, a1, a2 } = f;
|
|
72
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
73
|
+
const v = x[i]!;
|
|
74
|
+
const y = b0 * v + z1;
|
|
75
|
+
z1 = b1 * v - a1 * y + z2;
|
|
76
|
+
z2 = b2 * v - a2 * y;
|
|
77
|
+
x[i] = y;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type BandLayout = Readonly<{
|
|
82
|
+
bands: number;
|
|
83
|
+
lo: number;
|
|
84
|
+
hi: number;
|
|
85
|
+
width: number;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Log-spaced centres from `lo` to `hi`. Each of the two cascaded sections is
|
|
90
|
+
* about 1.55x wider than the target so the pair's -3 dB width matches
|
|
91
|
+
* spacing x width.
|
|
92
|
+
*/
|
|
93
|
+
export function bandLayout(p: BandLayout): {
|
|
94
|
+
centres: number[];
|
|
95
|
+
ratio: number;
|
|
96
|
+
octaves: number;
|
|
97
|
+
} {
|
|
98
|
+
const n = Math.max(2, Math.round(p.bands));
|
|
99
|
+
const ratio = (p.hi / p.lo) ** (1 / (n - 1));
|
|
100
|
+
const centres = Array.from({ length: n }, (_, k) => p.lo * ratio ** k);
|
|
101
|
+
return { centres, ratio, octaves: Math.log2(ratio) * p.width * 1.55 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type BankBand = Readonly<{
|
|
105
|
+
hz: number;
|
|
106
|
+
live: boolean;
|
|
107
|
+
filter: Biquad;
|
|
108
|
+
/** Modulator advance: the pair's group delay at hz plus the attack. */
|
|
109
|
+
advance: number;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The bank at one sample rate. Each band's modulator advance cancels the
|
|
114
|
+
* analysis filters' group delay and the follower attack, so consonants land
|
|
115
|
+
* on the beat.
|
|
116
|
+
*/
|
|
117
|
+
export function bankDesign(
|
|
118
|
+
p: BandLayout & { attack?: number },
|
|
119
|
+
sampleRate: number,
|
|
120
|
+
): { bands: BankBand[]; ratio: number } {
|
|
121
|
+
const { centres, ratio, octaves } = bandLayout(p);
|
|
122
|
+
const bands = centres.map((hz) => {
|
|
123
|
+
const live = hz <= 0.45 * sampleRate;
|
|
124
|
+
const filter = bandpass(
|
|
125
|
+
Math.min(hz, 0.45 * sampleRate),
|
|
126
|
+
octaves,
|
|
127
|
+
sampleRate,
|
|
128
|
+
);
|
|
129
|
+
const advance = live
|
|
130
|
+
? Math.round(
|
|
131
|
+
2 * groupDelay(filter, (2 * Math.PI * hz) / sampleRate) +
|
|
132
|
+
(p.attack ?? 0) * sampleRate,
|
|
133
|
+
)
|
|
134
|
+
: 0;
|
|
135
|
+
return { hz, live, filter, advance };
|
|
136
|
+
});
|
|
137
|
+
return { bands, ratio };
|
|
138
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cepstral spectral envelope (0.6 B2): the smooth outline of a magnitude
|
|
3
|
+
* spectrum, used by `shift.ts` to hold formants still while the pitch moves.
|
|
4
|
+
* Log magnitude, inverse transform to the real cepstrum, keep the
|
|
5
|
+
* quefrencies below the lifter, forward transform, exponentiate
|
|
6
|
+
* (Oppenheim and Schafer, *Discrete-Time Signal Processing*, ch. 13;
|
|
7
|
+
* J. O. Smith, *Spectral Audio Signal Processing*, "Cepstral Windowing";
|
|
8
|
+
* Röbel and Rodet, "Efficient spectral envelope estimation and its
|
|
9
|
+
* application to pitch shifting and envelope preservation", DAFx 2005, for
|
|
10
|
+
* the true-envelope iteration).
|
|
11
|
+
*/
|
|
12
|
+
import { fftInPlace } from "./fft.ts";
|
|
13
|
+
import { inverse } from "./stft.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default lifter: quefrencies below 2 ms (measured on a synthetic vowel:
|
|
17
|
+
* with 16 true-envelope passes F1 stays within 3% after +-7 and +-12 st at
|
|
18
|
+
* 110 Hz; 1 ms smeared F1 by up to 15%).
|
|
19
|
+
*/
|
|
20
|
+
export const ENVELOPE_LIFTER_SECONDS = 0.002;
|
|
21
|
+
/** True-envelope passes used by `pitchShift` when keeping formants. */
|
|
22
|
+
export const ENVELOPE_ITERATIONS = 16;
|
|
23
|
+
|
|
24
|
+
/** Adaptive lifter as a fraction of the cepstral period. */
|
|
25
|
+
export const PERIOD_FRACTION = 0.25;
|
|
26
|
+
|
|
27
|
+
/** Scratch buffers for one frame size, reused across frames. */
|
|
28
|
+
export type EnvelopeScratch = Readonly<{
|
|
29
|
+
re: Float64Array;
|
|
30
|
+
im: Float64Array;
|
|
31
|
+
log: Float64Array;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export function envelopeScratch(n: number): EnvelopeScratch {
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
re: new Float64Array(n),
|
|
37
|
+
im: new Float64Array(n),
|
|
38
|
+
log: new Float64Array(n / 2 + 1),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Writes the cepstral envelope of `mag` (bins 0..n/2 of an n-point
|
|
44
|
+
* spectrum) into `out` (length n/2 + 1). `lifter` is in samples (at least
|
|
45
|
+
* 4). With `iterations` > 0 it is the true envelope (Röbel and Rodet 2005):
|
|
46
|
+
* the log spectrum is raised to the smoothed curve wherever it lies below
|
|
47
|
+
* it and smoothed again, so the envelope rides the harmonic peaks instead
|
|
48
|
+
* of the valleys between them. With `periodRange` (samples) the lifter
|
|
49
|
+
* shrinks to a quarter of the frame's cepstral pitch period when that is
|
|
50
|
+
* shorter, so higher voices keep their harmonics out of the envelope.
|
|
51
|
+
*/
|
|
52
|
+
export function cepstralEnvelope(
|
|
53
|
+
mag: Float64Array,
|
|
54
|
+
n: number,
|
|
55
|
+
lifter: number,
|
|
56
|
+
out: Float64Array,
|
|
57
|
+
scratch: EnvelopeScratch = envelopeScratch(n),
|
|
58
|
+
iterations = 0,
|
|
59
|
+
periodRange?: readonly [number, number],
|
|
60
|
+
): Float64Array {
|
|
61
|
+
const half = n / 2;
|
|
62
|
+
const { re, im, log } = scratch;
|
|
63
|
+
let peak = 0;
|
|
64
|
+
for (let k = 0; k <= half; k += 1) peak = Math.max(peak, mag[k]!);
|
|
65
|
+
// Floor 120 dB under the frame's peak (silence stays finite).
|
|
66
|
+
const floor = peak * 1e-6 + 1e-12;
|
|
67
|
+
for (let k = 0; k <= half; k += 1) log[k] = Math.log(mag[k]! + floor);
|
|
68
|
+
let cut = Math.max(4, Math.min(half, Math.round(lifter)));
|
|
69
|
+
for (let pass = 0; pass <= iterations; pass += 1) {
|
|
70
|
+
for (let k = 0; k < n; k += 1) {
|
|
71
|
+
re[k] = log[k <= half ? k : n - k]!;
|
|
72
|
+
im[k] = 0;
|
|
73
|
+
}
|
|
74
|
+
inverse(re, im);
|
|
75
|
+
if (pass === 0 && periodRange) {
|
|
76
|
+
// Pitch-adaptive lifter: a quarter of the period found as the
|
|
77
|
+
// strongest cepstral peak in range, never longer than `lifter`.
|
|
78
|
+
const [lo, hi] = periodRange;
|
|
79
|
+
let q = Math.max(2, Math.round(lo));
|
|
80
|
+
for (let i = q; i <= Math.min(half, Math.round(hi)); i += 1)
|
|
81
|
+
if (re[i]! > re[q]!) q = i;
|
|
82
|
+
cut = Math.max(4, Math.min(cut, Math.round(q * PERIOD_FRACTION)));
|
|
83
|
+
}
|
|
84
|
+
for (let q = cut; q <= n - cut; q += 1) {
|
|
85
|
+
re[q] = 0;
|
|
86
|
+
im[q] = 0;
|
|
87
|
+
}
|
|
88
|
+
fftInPlace(re, im);
|
|
89
|
+
if (pass < iterations)
|
|
90
|
+
for (let k = 0; k <= half; k += 1) log[k] = Math.max(log[k]!, re[k]!);
|
|
91
|
+
}
|
|
92
|
+
for (let k = 0; k <= half; k += 1) out[k] = Math.exp(re[k]!);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A peak joins the envelope only within this factor (-9 dB) of the smooth
|
|
98
|
+
* cepstral envelope: harmonics sit on it, while window sidelobes and breath
|
|
99
|
+
* noise in the valleys between harmonics sit far below. Without this the
|
|
100
|
+
* envelope followed the source's harmonic comb, and `formant 0` imposed the
|
|
101
|
+
* old pitch on the shifted one (pitch.md 3.5).
|
|
102
|
+
*/
|
|
103
|
+
const HARMONIC_PEAK_FLOOR = 10 ** (-9 / 20);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Harmonic-peak envelope (0.6.1): log magnitude interpolated linearly
|
|
107
|
+
* between the spectral peaks (each refined by a parabola to its true bin
|
|
108
|
+
* and height) of `mag` that stand within `range` of the
|
|
109
|
+
* frame's loudest, held flat past the first and last. Between harmonics it
|
|
110
|
+
* follows the line joining them instead of the smoothed valley, so it stays
|
|
111
|
+
* on the formant at any pitch where the cepstral envelope (whose lifter
|
|
112
|
+
* must shrink with the period) starts to smear or ripple. `smooth` (an
|
|
113
|
+
* envelope from `cepstralEnvelope`) is used where no peak stands.
|
|
114
|
+
* Measured with `formant 0` on a synthetic vowel: F1 within 1.1% of the
|
|
115
|
+
* same vowel sung at the new pitch for f0 110-330 Hz at +-7 and +12 st
|
|
116
|
+
* (the cepstral envelope alone: -29% to +46% above 200 Hz).
|
|
117
|
+
*/
|
|
118
|
+
export function peakEnvelope(
|
|
119
|
+
mag: Float64Array,
|
|
120
|
+
half: number,
|
|
121
|
+
out: Float64Array,
|
|
122
|
+
smooth: Float64Array,
|
|
123
|
+
peaks: number[],
|
|
124
|
+
range = 1e-4,
|
|
125
|
+
): Float64Array {
|
|
126
|
+
let top = 0;
|
|
127
|
+
for (let k = 0; k <= half; k += 1) top = Math.max(top, mag[k]!);
|
|
128
|
+
peaks.length = 0;
|
|
129
|
+
const floor = top * range;
|
|
130
|
+
for (let k = 2; k <= half - 2; k += 1) {
|
|
131
|
+
const v = mag[k]!;
|
|
132
|
+
if (
|
|
133
|
+
v > floor &&
|
|
134
|
+
v >= smooth[k]! * HARMONIC_PEAK_FLOOR &&
|
|
135
|
+
v > mag[k - 1]! &&
|
|
136
|
+
v >= mag[k + 1]! &&
|
|
137
|
+
v > mag[k - 2]! &&
|
|
138
|
+
v >= mag[k + 2]!
|
|
139
|
+
)
|
|
140
|
+
peaks.push(k);
|
|
141
|
+
}
|
|
142
|
+
if (peaks.length < 2) {
|
|
143
|
+
out.set(smooth.subarray(0, half + 1));
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
// Each peak refined by a parabola on log magnitude: its true frequency
|
|
147
|
+
// (fractional bin) and height.
|
|
148
|
+
const at = (i: number) => {
|
|
149
|
+
const k = peaks[i]!;
|
|
150
|
+
const a = Math.log(mag[k - 1]! + 1e-30);
|
|
151
|
+
const b = Math.log(mag[k]! + 1e-30);
|
|
152
|
+
const c = Math.log(mag[k + 1]! + 1e-30);
|
|
153
|
+
const d = a - 2 * b + c;
|
|
154
|
+
const delta =
|
|
155
|
+
d < 0 ? Math.max(-0.5, Math.min(0.5, (0.5 * (a - c)) / d)) : 0;
|
|
156
|
+
return [k + delta, b - 0.25 * (a - c) * delta] as const;
|
|
157
|
+
};
|
|
158
|
+
let j = 0;
|
|
159
|
+
let [x0, y0] = at(0);
|
|
160
|
+
let [x1, y1] = at(1);
|
|
161
|
+
for (let k = 0; k <= half; k += 1) {
|
|
162
|
+
while (j + 2 < peaks.length && x1 <= k) {
|
|
163
|
+
j += 1;
|
|
164
|
+
[x0, y0] = [x1, y1];
|
|
165
|
+
[x1, y1] = at(j + 1);
|
|
166
|
+
}
|
|
167
|
+
const t = Math.max(0, Math.min(1, (k - x0) / (x1 - x0)));
|
|
168
|
+
out[k] = Math.exp(y0 + (y1 - y0) * t);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|