@onjmin/koe 1.0.4 → 1.0.6
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/dist/index.d.ts +144 -13
- package/dist/index.js +646 -21
- package/dist/index.js.map +1 -1
- package/dist/koe-convert.js +71 -13
- package/dist/koe-worklet.js +1 -1
- package/package.json +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UTAU `.frq` frequency-analysis files (FREQ0003). The same format OpenUtau
|
|
3
|
-
* reads: an 8-byte header, hop size,
|
|
4
|
-
*
|
|
3
|
+
* reads: an 8-byte header, hop size, the average fundamental frequency of the
|
|
4
|
+
* whole recording, then a per-frame f0 / amplitude curve.
|
|
5
5
|
*
|
|
6
6
|
* Layout:
|
|
7
7
|
* char[8] "FREQ0003"
|
|
8
|
-
* int32 hopSize
|
|
9
|
-
* float64 averageF0
|
|
8
|
+
* int32 hopSize ← in ORIGINAL WAV samples
|
|
9
|
+
* float64 averageF0 ← whole-file average in Hz
|
|
10
10
|
* byte[16] (blank)
|
|
11
11
|
* int32 length
|
|
12
12
|
* { float64 f0, float64 amp } × length
|
|
13
13
|
*/
|
|
14
|
+
interface FrqData {
|
|
15
|
+
/** Analysis hop in samples of the ORIGINAL wav (not the 48 kHz conversion) */
|
|
16
|
+
hopSize: number;
|
|
17
|
+
/** Whole-file average f0 in Hz, as stored in the header */
|
|
18
|
+
averageF0: number;
|
|
19
|
+
/** Per-frame f0 in Hz — 0 where the analyser found no clear pitch */
|
|
20
|
+
f0: Float64Array;
|
|
21
|
+
/** Per-frame amplitude, parallel to {@link f0} */
|
|
22
|
+
amp: Float64Array;
|
|
23
|
+
}
|
|
24
|
+
/** Parse a `.frq` file, including the per-frame curve. Null if not FREQ0003. */
|
|
25
|
+
declare function parseFrq(buffer: ArrayBuffer): FrqData | null;
|
|
26
|
+
/**
|
|
27
|
+
* Average f0 over the voiced frames covering a time span of the recording.
|
|
28
|
+
*
|
|
29
|
+
* The header's whole-file average includes leading silence and unvoiced
|
|
30
|
+
* consonants, so it can sit well away from the pitch actually sounding in the
|
|
31
|
+
* region a note is built from. Playback resamples the whole phoneme by one
|
|
32
|
+
* scalar ratio, so an inaccurate value both detunes the note and makes two
|
|
33
|
+
* crossfading notes drift apart in phase across the overlap — a 1% error at
|
|
34
|
+
* 233 Hz drifts ~25° over a 30 ms overlap and ~84° over 100 ms.
|
|
35
|
+
*
|
|
36
|
+
* @param startMs / endMs span within the ORIGINAL recording, in milliseconds
|
|
37
|
+
* @param sourceRate sample rate of the original WAV the frq describes
|
|
38
|
+
* @returns Hz, or 0 when the span holds no voiced frames
|
|
39
|
+
*/
|
|
40
|
+
declare function frqAverageF0InRange(frq: FrqData, startMs: number, endMs: number, sourceRate: number): number;
|
|
41
|
+
/** Whole-file average f0 in Hz from a `.frq` file, or null. */
|
|
14
42
|
declare function parseFrqAverageF0(buffer: ArrayBuffer): number | null;
|
|
15
43
|
/** Map a WAV filename to its sibling frq filename: "あ.wav" → "あ_wav.frq". */
|
|
16
44
|
declare function frqFileName(wavName: string): string;
|
|
@@ -77,7 +105,7 @@ declare function parseOto(content: string): OtoEntry[];
|
|
|
77
105
|
|
|
78
106
|
interface PackInput {
|
|
79
107
|
oto: OtoEntry;
|
|
80
|
-
/** Full
|
|
108
|
+
/** Full PCM of the source WAV (48kHz / 16bit / mono) */
|
|
81
109
|
pcm: Int16Array;
|
|
82
110
|
/** Known recorded pitch in Hz (e.g. from the .frq file). 0/undefined → auto-detect. */
|
|
83
111
|
recordedPitch?: number;
|
|
@@ -93,6 +121,16 @@ interface TrimmedPhoneme {
|
|
|
93
121
|
/** Manifest params relative to the trimmed start (sample 0 = oto offset) */
|
|
94
122
|
entry: Omit<PhonemeEntry, "offset">;
|
|
95
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve an oto entry's usable region within the full 48 kHz PCM.
|
|
126
|
+
*
|
|
127
|
+
* Shared with the converter CLI, which needs the same bounds to look up the
|
|
128
|
+
* region's local f0 in a `.frq` curve.
|
|
129
|
+
*/
|
|
130
|
+
declare function otoRegion(pcmLength: number, oto: OtoEntry): {
|
|
131
|
+
start: number;
|
|
132
|
+
end: number;
|
|
133
|
+
};
|
|
96
134
|
/**
|
|
97
135
|
* Cut the full WAV PCM down to its usable oto region and recompute parameters
|
|
98
136
|
* relative to the trimmed start.
|
|
@@ -107,8 +145,8 @@ interface TrimmedPhoneme {
|
|
|
107
145
|
*/
|
|
108
146
|
declare function trimToOto(pcm: Int16Array, oto: OtoEntry, recordedPitch?: number): TrimmedPhoneme;
|
|
109
147
|
/**
|
|
110
|
-
* Pack
|
|
111
|
-
* Each phoneme is trimmed to its oto region first.
|
|
148
|
+
* Pack phonemes into voice.bin + manifest.json.
|
|
149
|
+
* Each phoneme is trimmed to its oto region and DC-centred first.
|
|
112
150
|
* Duplicate aliases are silently overwritten by the later entry.
|
|
113
151
|
*/
|
|
114
152
|
declare function pack(inputs: PackInput[], referencePitch?: number): PackOutput;
|
|
@@ -142,9 +180,44 @@ declare function toMono(wav: WavData): WavData;
|
|
|
142
180
|
declare function resample(wav: WavData, targetRate: number): WavData;
|
|
143
181
|
/** Convert Float32 [-1,1] samples to Int16 PCM. */
|
|
144
182
|
declare function toInt16(samples: Float32Array): Int16Array;
|
|
145
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Decode a WAV to 48kHz/16bit/mono Int16 PCM, reporting the source sample rate.
|
|
185
|
+
*
|
|
186
|
+
* The rate is needed to index a sibling `.frq` file, whose analysis hop is
|
|
187
|
+
* counted in ORIGINAL samples — see {@link frqAverageF0InRange}.
|
|
188
|
+
*
|
|
189
|
+
* Note this does not touch amplitude: peak levels are carried through
|
|
190
|
+
* unchanged, so a bank's own relative loudness between phonemes is preserved
|
|
191
|
+
* (UTAU's engine instead normalises each region to −6 dBFS, which is why its
|
|
192
|
+
* イ/エ段 and 語尾 samples come out louder than recorded).
|
|
193
|
+
*/
|
|
194
|
+
declare function readWavPcm48k(buf: ArrayBuffer): {
|
|
195
|
+
pcm: Int16Array;
|
|
196
|
+
sourceRate: number;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Convert a WAV to 48kHz/16bit/mono Int16 PCM.
|
|
200
|
+
*
|
|
201
|
+
* @deprecated Misnomer — this never normalised amplitude. Use
|
|
202
|
+
* {@link readWavPcm48k}, which also reports the source sample rate.
|
|
203
|
+
*/
|
|
146
204
|
declare function normalizePcm(buf: ArrayBuffer): Int16Array;
|
|
147
205
|
|
|
206
|
+
/** Minimal file-like handle so zip entries can stand in for `File` objects. */
|
|
207
|
+
interface ZipFile {
|
|
208
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Extracts a zip archive into a flat path → file map, mirroring the shape of
|
|
212
|
+
* `webkitdirectory` file lists so the converter can treat both sources the same.
|
|
213
|
+
*
|
|
214
|
+
* Many UTAU voicebank zips are packed on Windows without the UTF-8 flag, so
|
|
215
|
+
* entry names are Shift_JIS bytes. fflate decodes those as latin1 (1 byte =
|
|
216
|
+
* 1 code point) rather than mangling them, so we can losslessly recover the
|
|
217
|
+
* original bytes and re-decode with the right encoding per entry.
|
|
218
|
+
*/
|
|
219
|
+
declare function unzipToFileMap(data: ArrayBuffer): Promise<Record<string, ZipFile>>;
|
|
220
|
+
|
|
148
221
|
/**
|
|
149
222
|
* Read-only access to a .koe voice bank: its manifest plus per-phoneme PCM,
|
|
150
223
|
* fetched on demand (Blob slice or HTTP Range). The full bank is never held in
|
|
@@ -188,6 +261,21 @@ interface KoeEngineOptions {
|
|
|
188
261
|
/** URL to koe-worklet.js. Defaults to './koe-worklet.js'. */
|
|
189
262
|
workletUrl?: string;
|
|
190
263
|
}
|
|
264
|
+
interface PlayOptions {
|
|
265
|
+
/**
|
|
266
|
+
* Play the first note's lead-in (its consonant / preutterance region) instead
|
|
267
|
+
* of skipping straight to the vowel.
|
|
268
|
+
*
|
|
269
|
+
* Every note but the first gets its lead-in from the crossfade with the note
|
|
270
|
+
* before it. The first note has no predecessor, so the lead-in has to come
|
|
271
|
+
* from somewhere: with `leadIn` the phrase starts one preutterance EARLIER
|
|
272
|
+
* relative to its beats, and {@link KoeEngine.play} returns that offset in
|
|
273
|
+
* samples so a sequencer can schedule around it. Left off (the default), the
|
|
274
|
+
* first note keeps its beat exactly but opens on its vowel, dropping the
|
|
275
|
+
* consonant.
|
|
276
|
+
*/
|
|
277
|
+
leadIn?: boolean;
|
|
278
|
+
}
|
|
191
279
|
/**
|
|
192
280
|
* Main-thread API for the koe concatenative synthesis engine.
|
|
193
281
|
*
|
|
@@ -222,8 +310,18 @@ declare class KoeEngine {
|
|
|
222
310
|
load(koe: Blob | string): Promise<void>;
|
|
223
311
|
/** Fetch one phoneme's PCM and deliver it to the worklet (deduped, cached). */
|
|
224
312
|
private ensurePhoneme;
|
|
225
|
-
/**
|
|
226
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Stop current playback, preload the phonemes for `notes`, then queue them.
|
|
315
|
+
*
|
|
316
|
+
* @returns the lead-in offset in samples — how far the first note's audio
|
|
317
|
+
* starts ahead of its beat. 0 unless {@link PlayOptions.leadIn}.
|
|
318
|
+
*/
|
|
319
|
+
play(notes: NoteEvent[], options?: PlayOptions): Promise<number>;
|
|
320
|
+
/**
|
|
321
|
+
* Output samples the first note's lead-in occupies ahead of its beat —
|
|
322
|
+
* mirrors what the worklet does with `leadIn`, so callers can compensate.
|
|
323
|
+
*/
|
|
324
|
+
private leadInSamples;
|
|
227
325
|
/** Stop playback and clear the queue. */
|
|
228
326
|
stop(): void;
|
|
229
327
|
/** Resume the AudioContext if suspended (e.g. after autoplay block). */
|
|
@@ -283,14 +381,29 @@ interface WorldlineLoadOptions {
|
|
|
283
381
|
*/
|
|
284
382
|
scriptUrl: string;
|
|
285
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* A per-frame expression value: either a flat constant for the whole note, or
|
|
386
|
+
* a function evaluated once per 10ms WORLD frame for a custom curve (vibrato,
|
|
387
|
+
* portamento, scoop-in, humanize jitter, hand-drawn automation, …).
|
|
388
|
+
*
|
|
389
|
+
* `tMs` is elapsed time from the start of the rendered buffer (0 = includes
|
|
390
|
+
* the `preMs` lead-in), `totalMs` is the full rendered length (`preMs +
|
|
391
|
+
* durationMs`). Callers own all curve shaping — worldline itself has no
|
|
392
|
+
* opinion on what "vibrato" or "scoop" means, it just samples whatever
|
|
393
|
+
* function you give it once per frame.
|
|
394
|
+
*/
|
|
395
|
+
type CurveInput = number | ((tMs: number, totalMs: number) => number);
|
|
286
396
|
interface RenderNoteParams {
|
|
287
397
|
/**
|
|
288
398
|
* Source phoneme PCM normalised to [-1, 1] (e.g. from
|
|
289
399
|
* `VoiceBank.getPcm()` / `KoeEngine.getPcm()`).
|
|
290
400
|
*/
|
|
291
401
|
pcm: Float64Array;
|
|
292
|
-
/**
|
|
293
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Target output pitch in Hz. Pass a function for vibrato, portamento,
|
|
404
|
+
* scoop-in, pitch-drift humanize, etc. — it is sampled once per 10ms frame.
|
|
405
|
+
*/
|
|
406
|
+
pitch: CurveInput;
|
|
294
407
|
/** Sustain / vowel duration in ms (the lead-in below is rendered on top). */
|
|
295
408
|
durationMs: number;
|
|
296
409
|
/** Preutterance / lead-in in ms — convert from {@link PhonemeEntry.pre}. */
|
|
@@ -299,6 +412,24 @@ interface RenderNoteParams {
|
|
|
299
412
|
consonantMs: number;
|
|
300
413
|
/** Reference tempo in BPM for worldline's internal timing. Default 120. */
|
|
301
414
|
tempo?: number;
|
|
415
|
+
/**
|
|
416
|
+
* Formant/gender shift, 0-1. 0.5 (default) = unmodified. Below 0.5 skews
|
|
417
|
+
* toward a lower/thicker formant (older, huskier); above 0.5 toward a
|
|
418
|
+
* higher/thinner one (younger, brighter) — pitch itself is unaffected.
|
|
419
|
+
*/
|
|
420
|
+
gender?: CurveInput;
|
|
421
|
+
/**
|
|
422
|
+
* Tension, 0-1. 0.5 (default) = neutral. Higher = tighter/more strained
|
|
423
|
+
* ("こぶし"-style push); lower = more relaxed/breathy-adjacent.
|
|
424
|
+
*/
|
|
425
|
+
tension?: CurveInput;
|
|
426
|
+
/** Breathiness, 0-1. 0.5 (default) = neutral. Higher = airier/whispered. */
|
|
427
|
+
breathiness?: CurveInput;
|
|
428
|
+
/**
|
|
429
|
+
* Voicing ratio, 0-1. 1.0 (default) = fully voiced. Lower blends toward an
|
|
430
|
+
* unvoiced/falsetto-adjacent texture.
|
|
431
|
+
*/
|
|
432
|
+
voicing?: CurveInput;
|
|
302
433
|
}
|
|
303
434
|
/** Convert a sample count at 48 kHz to milliseconds. */
|
|
304
435
|
declare const samplesToMs: (samples: number) => number;
|
|
@@ -372,4 +503,4 @@ declare function parseKoeHeader(headerBytes: ArrayBuffer): {
|
|
|
372
503
|
/** Byte offset where PCM data begins, given the JSON length. */
|
|
373
504
|
declare const pcmBase: (jsonLength: number) => number;
|
|
374
505
|
|
|
375
|
-
export { KoeEngine, type KoeEngineOptions, MIN_WORLDLINE_SAMPLES, type Manifest, type NoteEvent, type OtoEntry, type PackInput, type PackOutput, type PhonemeEntry, type RenderNoteParams, type TrimmedPhoneme, VoiceBank, WORLDLINE_SAMPLE_RATE, type WavData, Worldline, type WorldlineLoadOptions, detectF0, frqFileName, leadInFromEntry, normalizePcm, noteNameToHz, pack, packKoe, parseFrqAverageF0, parseKoeHeader, parseOto, parseWav, pcmBase, pitchFromAliasSuffix, resample, samplesToMs, toInt16, toMono, trimToOto };
|
|
506
|
+
export { type FrqData, KoeEngine, type KoeEngineOptions, MIN_WORLDLINE_SAMPLES, type Manifest, type NoteEvent, type OtoEntry, type PackInput, type PackOutput, type PhonemeEntry, type PlayOptions, type RenderNoteParams, type TrimmedPhoneme, VoiceBank, WORLDLINE_SAMPLE_RATE, type WavData, Worldline, type WorldlineLoadOptions, type ZipFile, detectF0, frqAverageF0InRange, frqFileName, leadInFromEntry, normalizePcm, noteNameToHz, otoRegion, pack, packKoe, parseFrq, parseFrqAverageF0, parseKoeHeader, parseOto, parseWav, pcmBase, pitchFromAliasSuffix, readWavPcm48k, resample, samplesToMs, toInt16, toMono, trimToOto, unzipToFileMap };
|