@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 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, then the average fundamental frequency of
4
- * the recording exactly the reference pitch we need for correct resampling.
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 the recorded pitch in Hz
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 normalized PCM of the source WAV (48kHz / 16bit / mono) */
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 normalized PCM phonemes into voice.bin + manifest.json.
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
- /** Normalize then convert a WAV to 48kHz/16bit/mono Int16 PCM. */
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
- /** Stop current playback, preload the phonemes for `notes`, then queue them. */
226
- play(notes: NoteEvent[]): Promise<void>;
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
- /** Target output pitch in Hz. */
293
- pitch: number;
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 };