@onjmin/koe 1.0.3 → 1.0.5

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,3 +1,20 @@
1
+ /**
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.
5
+ *
6
+ * Layout:
7
+ * char[8] "FREQ0003"
8
+ * int32 hopSize
9
+ * float64 averageF0 ← the recorded pitch in Hz
10
+ * byte[16] (blank)
11
+ * int32 length
12
+ * { float64 f0, float64 amp } × length
13
+ */
14
+ declare function parseFrqAverageF0(buffer: ArrayBuffer): number | null;
15
+ /** Map a WAV filename to its sibling frq filename: "あ.wav" → "あ_wav.frq". */
16
+ declare function frqFileName(wavName: string): string;
17
+
1
18
  /**
2
19
  * One phoneme, already trimmed to its usable oto region.
3
20
  * Sample 0 corresponds to the oto `offset` (left blank); everything before it
@@ -36,6 +53,113 @@ interface NoteEvent {
36
53
  duration: number;
37
54
  }
38
55
 
56
+ interface OtoEntry {
57
+ /** Source WAV filename */
58
+ wav: string;
59
+ /** Phoneme alias */
60
+ alias: string;
61
+ /** Left blank — offset from WAV start (ms) */
62
+ offset: number;
63
+ /** Consonant portion end from offset (ms) */
64
+ consonant: number;
65
+ /** Right blank — negative = from WAV end, positive = from offset (ms) */
66
+ cutoff: number;
67
+ /** Preutterance from offset (ms) */
68
+ pre: number;
69
+ /** Overlap / crossfade region (ms) */
70
+ overlap: number;
71
+ }
72
+ /**
73
+ * Parse oto.ini content (already decoded to UTF-8 string).
74
+ * Silently skips malformed lines.
75
+ */
76
+ declare function parseOto(content: string): OtoEntry[];
77
+
78
+ interface PackInput {
79
+ oto: OtoEntry;
80
+ /** Full normalized PCM of the source WAV (48kHz / 16bit / mono) */
81
+ pcm: Int16Array;
82
+ /** Known recorded pitch in Hz (e.g. from the .frq file). 0/undefined → auto-detect. */
83
+ recordedPitch?: number;
84
+ }
85
+ interface PackOutput {
86
+ manifest: Manifest;
87
+ /** Raw PCM blob — Int16 / 48kHz / mono */
88
+ bin: ArrayBuffer;
89
+ }
90
+ interface TrimmedPhoneme {
91
+ /** PCM trimmed to the oto region [offset, cutoff] */
92
+ pcm: Int16Array;
93
+ /** Manifest params relative to the trimmed start (sample 0 = oto offset) */
94
+ entry: Omit<PhonemeEntry, "offset">;
95
+ }
96
+ /**
97
+ * Cut the full WAV PCM down to its usable oto region and recompute parameters
98
+ * relative to the trimmed start.
99
+ *
100
+ * UTAU oto.ini values are all in ms and measured from `offset` (the left blank),
101
+ * except `cutoff` (right blank):
102
+ * - cutoff >= 0 : measured from the END of the file
103
+ * - cutoff < 0 : region length from offset = |cutoff|
104
+ *
105
+ * After trimming, sample 0 == oto offset, so pre/overlap/consonant carry over
106
+ * unchanged (just converted to samples), and the slice length is the region end.
107
+ */
108
+ declare function trimToOto(pcm: Int16Array, oto: OtoEntry, recordedPitch?: number): TrimmedPhoneme;
109
+ /**
110
+ * Pack normalized PCM phonemes into voice.bin + manifest.json.
111
+ * Each phoneme is trimmed to its oto region first.
112
+ * Duplicate aliases are silently overwritten by the later entry.
113
+ */
114
+ declare function pack(inputs: PackInput[], referencePitch?: number): PackOutput;
115
+
116
+ /** Parse a note name like "E4", "G#4", "Db5" → frequency in Hz (null if invalid). */
117
+ declare function noteNameToHz(name: string): number | null;
118
+ /** Recorded pitch encoded in a multi-pitch alias suffix: "a い_E4" → 329.63 Hz. */
119
+ declare function pitchFromAliasSuffix(alias: string): number | null;
120
+ /**
121
+ * Estimate the fundamental frequency (Hz) of a voiced region by normalized
122
+ * autocorrelation. Returns 0 when no clear pitch is found (unvoiced consonant,
123
+ * silence, or a region too short to analyse).
124
+ *
125
+ * The signal is decimated to a lower analysis rate for speed; f0 below ~700 Hz
126
+ * is well within the resulting Nyquist limit. A parabolic interpolation around
127
+ * the best lag gives sub-sample (sub-semitone) accuracy.
128
+ */
129
+ declare function detectF0(pcm: Int16Array, start: number, end: number): number;
130
+
131
+ interface WavData {
132
+ sampleRate: number;
133
+ channels: number;
134
+ /** Normalized samples in [-1, 1], interleaved if multi-channel */
135
+ samples: Float32Array;
136
+ }
137
+ /** Parse a WAV file from an ArrayBuffer. Supports PCM 8/16/24-bit and IEEE float 32-bit. */
138
+ declare function parseWav(buf: ArrayBuffer): WavData;
139
+ /** Mix down to mono by averaging all channels. */
140
+ declare function toMono(wav: WavData): WavData;
141
+ /** Linear interpolation resample to targetRate. Expects mono input. */
142
+ declare function resample(wav: WavData, targetRate: number): WavData;
143
+ /** Convert Float32 [-1,1] samples to Int16 PCM. */
144
+ declare function toInt16(samples: Float32Array): Int16Array;
145
+ /** Normalize then convert a WAV to 48kHz/16bit/mono Int16 PCM. */
146
+ declare function normalizePcm(buf: ArrayBuffer): Int16Array;
147
+
148
+ /** Minimal file-like handle so zip entries can stand in for `File` objects. */
149
+ interface ZipFile {
150
+ arrayBuffer(): Promise<ArrayBuffer>;
151
+ }
152
+ /**
153
+ * Extracts a zip archive into a flat path → file map, mirroring the shape of
154
+ * `webkitdirectory` file lists so the converter can treat both sources the same.
155
+ *
156
+ * Many UTAU voicebank zips are packed on Windows without the UTF-8 flag, so
157
+ * entry names are Shift_JIS bytes. fflate decodes those as latin1 (1 byte =
158
+ * 1 code point) rather than mangling them, so we can losslessly recover the
159
+ * original bytes and re-decode with the right encoding per entry.
160
+ */
161
+ declare function unzipToFileMap(data: ArrayBuffer): Promise<Record<string, ZipFile>>;
162
+
39
163
  /**
40
164
  * Read-only access to a .koe voice bank: its manifest plus per-phoneme PCM,
41
165
  * fetched on demand (Blob slice or HTTP Range). The full bank is never held in
@@ -59,6 +183,7 @@ declare class VoiceBank {
59
183
  * @param koe a Blob/File of the .koe archive, or a URL (served with Range support)
60
184
  */
61
185
  static load(koe: Blob | string): Promise<VoiceBank>;
186
+ private static fromBlob;
62
187
  /** True if the bank contains a phoneme under this alias. */
63
188
  has(phoneme: string): boolean;
64
189
  /**
@@ -118,6 +243,11 @@ declare class KoeEngine {
118
243
  stop(): void;
119
244
  /** Resume the AudioContext if suspended (e.g. after autoplay block). */
120
245
  resume(): Promise<void>;
246
+ /**
247
+ * Tear down the worklet node and close the AudioContext, releasing the audio
248
+ * hardware. The engine cannot be reused afterwards — create a new one.
249
+ */
250
+ dispose(): Promise<void>;
121
251
  /**
122
252
  * Read a phoneme's raw PCM and return it as a Float64Array normalised to
123
253
  * [-1, 1]. Convenience that forwards to the underlying {@link VoiceBank}.
@@ -168,14 +298,29 @@ interface WorldlineLoadOptions {
168
298
  */
169
299
  scriptUrl: string;
170
300
  }
301
+ /**
302
+ * A per-frame expression value: either a flat constant for the whole note, or
303
+ * a function evaluated once per 10ms WORLD frame for a custom curve (vibrato,
304
+ * portamento, scoop-in, humanize jitter, hand-drawn automation, …).
305
+ *
306
+ * `tMs` is elapsed time from the start of the rendered buffer (0 = includes
307
+ * the `preMs` lead-in), `totalMs` is the full rendered length (`preMs +
308
+ * durationMs`). Callers own all curve shaping — worldline itself has no
309
+ * opinion on what "vibrato" or "scoop" means, it just samples whatever
310
+ * function you give it once per frame.
311
+ */
312
+ type CurveInput = number | ((tMs: number, totalMs: number) => number);
171
313
  interface RenderNoteParams {
172
314
  /**
173
315
  * Source phoneme PCM normalised to [-1, 1] (e.g. from
174
316
  * `VoiceBank.getPcm()` / `KoeEngine.getPcm()`).
175
317
  */
176
318
  pcm: Float64Array;
177
- /** Target output pitch in Hz. */
178
- pitch: number;
319
+ /**
320
+ * Target output pitch in Hz. Pass a function for vibrato, portamento,
321
+ * scoop-in, pitch-drift humanize, etc. — it is sampled once per 10ms frame.
322
+ */
323
+ pitch: CurveInput;
179
324
  /** Sustain / vowel duration in ms (the lead-in below is rendered on top). */
180
325
  durationMs: number;
181
326
  /** Preutterance / lead-in in ms — convert from {@link PhonemeEntry.pre}. */
@@ -184,6 +329,24 @@ interface RenderNoteParams {
184
329
  consonantMs: number;
185
330
  /** Reference tempo in BPM for worldline's internal timing. Default 120. */
186
331
  tempo?: number;
332
+ /**
333
+ * Formant/gender shift, 0-1. 0.5 (default) = unmodified. Below 0.5 skews
334
+ * toward a lower/thicker formant (older, huskier); above 0.5 toward a
335
+ * higher/thinner one (younger, brighter) — pitch itself is unaffected.
336
+ */
337
+ gender?: CurveInput;
338
+ /**
339
+ * Tension, 0-1. 0.5 (default) = neutral. Higher = tighter/more strained
340
+ * ("こぶし"-style push); lower = more relaxed/breathy-adjacent.
341
+ */
342
+ tension?: CurveInput;
343
+ /** Breathiness, 0-1. 0.5 (default) = neutral. Higher = airier/whispered. */
344
+ breathiness?: CurveInput;
345
+ /**
346
+ * Voicing ratio, 0-1. 1.0 (default) = fully voiced. Lower blends toward an
347
+ * unvoiced/falsetto-adjacent texture.
348
+ */
349
+ voicing?: CurveInput;
187
350
  }
188
351
  /** Convert a sample count at 48 kHz to milliseconds. */
189
352
  declare const samplesToMs: (samples: number) => number;
@@ -242,115 +405,6 @@ declare class Worldline {
242
405
  renderNote(params: RenderNoteParams): Float32Array | null;
243
406
  }
244
407
 
245
- interface OtoEntry {
246
- /** Source WAV filename */
247
- wav: string;
248
- /** Phoneme alias */
249
- alias: string;
250
- /** Left blank — offset from WAV start (ms) */
251
- offset: number;
252
- /** Consonant portion end from offset (ms) */
253
- consonant: number;
254
- /** Right blank — negative = from WAV end, positive = from offset (ms) */
255
- cutoff: number;
256
- /** Preutterance from offset (ms) */
257
- pre: number;
258
- /** Overlap / crossfade region (ms) */
259
- overlap: number;
260
- }
261
- /**
262
- * Parse oto.ini content (already decoded to UTF-8 string).
263
- * Silently skips malformed lines.
264
- */
265
- declare function parseOto(content: string): OtoEntry[];
266
-
267
- interface WavData {
268
- sampleRate: number;
269
- channels: number;
270
- /** Normalized samples in [-1, 1], interleaved if multi-channel */
271
- samples: Float32Array;
272
- }
273
- /** Parse a WAV file from an ArrayBuffer. Supports PCM 8/16/24-bit and IEEE float 32-bit. */
274
- declare function parseWav(buf: ArrayBuffer): WavData;
275
- /** Mix down to mono by averaging all channels. */
276
- declare function toMono(wav: WavData): WavData;
277
- /** Linear interpolation resample to targetRate. Expects mono input. */
278
- declare function resample(wav: WavData, targetRate: number): WavData;
279
- /** Convert Float32 [-1,1] samples to Int16 PCM. */
280
- declare function toInt16(samples: Float32Array): Int16Array;
281
- /** Normalize then convert a WAV to 48kHz/16bit/mono Int16 PCM. */
282
- declare function normalizePcm(buf: ArrayBuffer): Int16Array;
283
-
284
- interface PackInput {
285
- oto: OtoEntry;
286
- /** Full normalized PCM of the source WAV (48kHz / 16bit / mono) */
287
- pcm: Int16Array;
288
- /** Known recorded pitch in Hz (e.g. from the .frq file). 0/undefined → auto-detect. */
289
- recordedPitch?: number;
290
- }
291
- interface PackOutput {
292
- manifest: Manifest;
293
- /** Raw PCM blob — Int16 / 48kHz / mono */
294
- bin: ArrayBuffer;
295
- }
296
- interface TrimmedPhoneme {
297
- /** PCM trimmed to the oto region [offset, cutoff] */
298
- pcm: Int16Array;
299
- /** Manifest params relative to the trimmed start (sample 0 = oto offset) */
300
- entry: Omit<PhonemeEntry, "offset">;
301
- }
302
- /**
303
- * Cut the full WAV PCM down to its usable oto region and recompute parameters
304
- * relative to the trimmed start.
305
- *
306
- * UTAU oto.ini values are all in ms and measured from `offset` (the left blank),
307
- * except `cutoff` (right blank):
308
- * - cutoff >= 0 : measured from the END of the file
309
- * - cutoff < 0 : region length from offset = |cutoff|
310
- *
311
- * After trimming, sample 0 == oto offset, so pre/overlap/consonant carry over
312
- * unchanged (just converted to samples), and the slice length is the region end.
313
- */
314
- declare function trimToOto(pcm: Int16Array, oto: OtoEntry, recordedPitch?: number): TrimmedPhoneme;
315
- /**
316
- * Pack normalized PCM phonemes into voice.bin + manifest.json.
317
- * Each phoneme is trimmed to its oto region first.
318
- * Duplicate aliases are silently overwritten by the later entry.
319
- */
320
- declare function pack(inputs: PackInput[], referencePitch?: number): PackOutput;
321
-
322
- /** Parse a note name like "E4", "G#4", "Db5" → frequency in Hz (null if invalid). */
323
- declare function noteNameToHz(name: string): number | null;
324
- /** Recorded pitch encoded in a multi-pitch alias suffix: "a い_E4" → 329.63 Hz. */
325
- declare function pitchFromAliasSuffix(alias: string): number | null;
326
- /**
327
- * Estimate the fundamental frequency (Hz) of a voiced region by normalized
328
- * autocorrelation. Returns 0 when no clear pitch is found (unvoiced consonant,
329
- * silence, or a region too short to analyse).
330
- *
331
- * The signal is decimated to a lower analysis rate for speed; f0 below ~700 Hz
332
- * is well within the resulting Nyquist limit. A parabolic interpolation around
333
- * the best lag gives sub-sample (sub-semitone) accuracy.
334
- */
335
- declare function detectF0(pcm: Int16Array, start: number, end: number): number;
336
-
337
- /**
338
- * UTAU `.frq` frequency-analysis files (FREQ0003). The same format OpenUtau
339
- * reads: an 8-byte header, hop size, then the average fundamental frequency of
340
- * the recording — exactly the reference pitch we need for correct resampling.
341
- *
342
- * Layout:
343
- * char[8] "FREQ0003"
344
- * int32 hopSize
345
- * float64 averageF0 ← the recorded pitch in Hz
346
- * byte[16] (blank)
347
- * int32 length
348
- * { float64 f0, float64 amp } × length
349
- */
350
- declare function parseFrqAverageF0(buffer: ArrayBuffer): number | null;
351
- /** Map a WAV filename to its sibling frq filename: "あ.wav" → "あ_wav.frq". */
352
- declare function frqFileName(wavName: string): string;
353
-
354
408
  /**
355
409
  * Koe Archive Format (.koe)
356
410
  * [4B] magic 'KOE\0' (big-endian)
@@ -366,4 +420,4 @@ declare function parseKoeHeader(headerBytes: ArrayBuffer): {
366
420
  /** Byte offset where PCM data begins, given the JSON length. */
367
421
  declare const pcmBase: (jsonLength: number) => number;
368
422
 
369
- 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 };
423
+ 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, type ZipFile, detectF0, frqFileName, leadInFromEntry, normalizePcm, noteNameToHz, pack, packKoe, parseFrqAverageF0, parseKoeHeader, parseOto, parseWav, pcmBase, pitchFromAliasSuffix, resample, samplesToMs, toInt16, toMono, trimToOto, unzipToFileMap };