@onjmin/koe 1.0.2 → 1.0.4
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/README.md +9 -3
- package/dist/index.d.ts +123 -110
- package/dist/index.js +506 -403
- package/dist/index.js.map +1 -1
- package/dist/koe-convert.js +182 -154
- package/dist/koe-worklet.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
UTAU の oto.ini で定義された音源を `.koe` アーカイブに変換し、WebAssembly + AudioWorklet でリアルタイム再生・高品質再合成を行う。
|
|
6
6
|
|
|
7
7
|
- [DEMO](https://onjmin.github.io/koe/demo) koeフォーマット作成もこちらで
|
|
8
|
-
- [npm](https://www.npmjs.com/package/@onjmin/
|
|
8
|
+
- [npm](https://www.npmjs.com/package/@onjmin/koe)
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -94,11 +94,13 @@ await fs.writeFile("voice.koe", buf);
|
|
|
94
94
|
|
|
95
95
|
AudioWorklet を使った連接合成エンジン。`koe-worklet.js` を同じオリジンから配信する必要がある。
|
|
96
96
|
|
|
97
|
+
GitHub Pages にホストされているファイルをそのまま使える:
|
|
98
|
+
|
|
97
99
|
```ts
|
|
98
100
|
import { KoeEngine } from "@onjmin/koe";
|
|
99
101
|
|
|
100
102
|
const engine = new KoeEngine({
|
|
101
|
-
workletUrl: "/koe-worklet.js",
|
|
103
|
+
workletUrl: "https://onjmin.github.io/koe/demo/koe-worklet.js",
|
|
102
104
|
});
|
|
103
105
|
|
|
104
106
|
// .koe ファイルをロード (Blob でも URL でも可)
|
|
@@ -163,11 +165,15 @@ bank.has("a"); // boolean
|
|
|
163
165
|
|
|
164
166
|
OpenUtau の worldline WASM で F0 分析・再合成を行う。`worldline.js` と `worldline.wasm` を配信する必要がある。
|
|
165
167
|
|
|
168
|
+
GitHub Pages にホストされているファイルをそのまま使える:
|
|
169
|
+
|
|
166
170
|
```ts
|
|
167
171
|
import { VoiceBank, Worldline, leadInFromEntry } from "@onjmin/koe";
|
|
168
172
|
|
|
169
173
|
const bank = await VoiceBank.load("/voice.koe");
|
|
170
|
-
const wl = await Worldline.load({
|
|
174
|
+
const wl = await Worldline.load({
|
|
175
|
+
scriptUrl: "https://onjmin.github.io/koe/demo/world/worldline.js",
|
|
176
|
+
});
|
|
171
177
|
|
|
172
178
|
const alias = "a";
|
|
173
179
|
const entry = bank.manifest.phonemes[alias];
|
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,98 @@ 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
|
+
|
|
39
148
|
/**
|
|
40
149
|
* Read-only access to a .koe voice bank: its manifest plus per-phoneme PCM,
|
|
41
150
|
* fetched on demand (Blob slice or HTTP Range). The full bank is never held in
|
|
@@ -59,6 +168,7 @@ declare class VoiceBank {
|
|
|
59
168
|
* @param koe a Blob/File of the .koe archive, or a URL (served with Range support)
|
|
60
169
|
*/
|
|
61
170
|
static load(koe: Blob | string): Promise<VoiceBank>;
|
|
171
|
+
private static fromBlob;
|
|
62
172
|
/** True if the bank contains a phoneme under this alias. */
|
|
63
173
|
has(phoneme: string): boolean;
|
|
64
174
|
/**
|
|
@@ -118,6 +228,11 @@ declare class KoeEngine {
|
|
|
118
228
|
stop(): void;
|
|
119
229
|
/** Resume the AudioContext if suspended (e.g. after autoplay block). */
|
|
120
230
|
resume(): Promise<void>;
|
|
231
|
+
/**
|
|
232
|
+
* Tear down the worklet node and close the AudioContext, releasing the audio
|
|
233
|
+
* hardware. The engine cannot be reused afterwards — create a new one.
|
|
234
|
+
*/
|
|
235
|
+
dispose(): Promise<void>;
|
|
121
236
|
/**
|
|
122
237
|
* Read a phoneme's raw PCM and return it as a Float64Array normalised to
|
|
123
238
|
* [-1, 1]. Convenience that forwards to the underlying {@link VoiceBank}.
|
|
@@ -155,6 +270,7 @@ type WorldlineFactory = (opts?: {
|
|
|
155
270
|
}) => Promise<WorldlineWasm>;
|
|
156
271
|
declare global {
|
|
157
272
|
var WorldlineModule: WorldlineFactory | undefined;
|
|
273
|
+
var importScripts: ((...urls: string[]) => void) | undefined;
|
|
158
274
|
}
|
|
159
275
|
interface WorldlineLoadOptions {
|
|
160
276
|
/**
|
|
@@ -217,7 +333,13 @@ declare class Worldline {
|
|
|
217
333
|
private wasm;
|
|
218
334
|
readonly sampleRate = 48000;
|
|
219
335
|
private constructor();
|
|
220
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* Load + instantiate the worldline WASM module (deduped per scriptUrl).
|
|
338
|
+
*
|
|
339
|
+
* Works on the main thread (loads via `<script>`) and inside a classic Web
|
|
340
|
+
* Worker (loads via `importScripts`), so the heavy synthesis can run
|
|
341
|
+
* off-thread. The matching `worldline.wasm` is fetched next to scriptUrl.
|
|
342
|
+
*/
|
|
221
343
|
static load(options: WorldlineLoadOptions): Promise<Worldline>;
|
|
222
344
|
/**
|
|
223
345
|
* Render one note to Float32 PCM at 48 kHz.
|
|
@@ -235,115 +357,6 @@ declare class Worldline {
|
|
|
235
357
|
renderNote(params: RenderNoteParams): Float32Array | null;
|
|
236
358
|
}
|
|
237
359
|
|
|
238
|
-
interface OtoEntry {
|
|
239
|
-
/** Source WAV filename */
|
|
240
|
-
wav: string;
|
|
241
|
-
/** Phoneme alias */
|
|
242
|
-
alias: string;
|
|
243
|
-
/** Left blank — offset from WAV start (ms) */
|
|
244
|
-
offset: number;
|
|
245
|
-
/** Consonant portion end from offset (ms) */
|
|
246
|
-
consonant: number;
|
|
247
|
-
/** Right blank — negative = from WAV end, positive = from offset (ms) */
|
|
248
|
-
cutoff: number;
|
|
249
|
-
/** Preutterance from offset (ms) */
|
|
250
|
-
pre: number;
|
|
251
|
-
/** Overlap / crossfade region (ms) */
|
|
252
|
-
overlap: number;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Parse oto.ini content (already decoded to UTF-8 string).
|
|
256
|
-
* Silently skips malformed lines.
|
|
257
|
-
*/
|
|
258
|
-
declare function parseOto(content: string): OtoEntry[];
|
|
259
|
-
|
|
260
|
-
interface WavData {
|
|
261
|
-
sampleRate: number;
|
|
262
|
-
channels: number;
|
|
263
|
-
/** Normalized samples in [-1, 1], interleaved if multi-channel */
|
|
264
|
-
samples: Float32Array;
|
|
265
|
-
}
|
|
266
|
-
/** Parse a WAV file from an ArrayBuffer. Supports PCM 8/16/24-bit and IEEE float 32-bit. */
|
|
267
|
-
declare function parseWav(buf: ArrayBuffer): WavData;
|
|
268
|
-
/** Mix down to mono by averaging all channels. */
|
|
269
|
-
declare function toMono(wav: WavData): WavData;
|
|
270
|
-
/** Linear interpolation resample to targetRate. Expects mono input. */
|
|
271
|
-
declare function resample(wav: WavData, targetRate: number): WavData;
|
|
272
|
-
/** Convert Float32 [-1,1] samples to Int16 PCM. */
|
|
273
|
-
declare function toInt16(samples: Float32Array): Int16Array;
|
|
274
|
-
/** Normalize then convert a WAV to 48kHz/16bit/mono Int16 PCM. */
|
|
275
|
-
declare function normalizePcm(buf: ArrayBuffer): Int16Array;
|
|
276
|
-
|
|
277
|
-
interface PackInput {
|
|
278
|
-
oto: OtoEntry;
|
|
279
|
-
/** Full normalized PCM of the source WAV (48kHz / 16bit / mono) */
|
|
280
|
-
pcm: Int16Array;
|
|
281
|
-
/** Known recorded pitch in Hz (e.g. from the .frq file). 0/undefined → auto-detect. */
|
|
282
|
-
recordedPitch?: number;
|
|
283
|
-
}
|
|
284
|
-
interface PackOutput {
|
|
285
|
-
manifest: Manifest;
|
|
286
|
-
/** Raw PCM blob — Int16 / 48kHz / mono */
|
|
287
|
-
bin: ArrayBuffer;
|
|
288
|
-
}
|
|
289
|
-
interface TrimmedPhoneme {
|
|
290
|
-
/** PCM trimmed to the oto region [offset, cutoff] */
|
|
291
|
-
pcm: Int16Array;
|
|
292
|
-
/** Manifest params relative to the trimmed start (sample 0 = oto offset) */
|
|
293
|
-
entry: Omit<PhonemeEntry, "offset">;
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Cut the full WAV PCM down to its usable oto region and recompute parameters
|
|
297
|
-
* relative to the trimmed start.
|
|
298
|
-
*
|
|
299
|
-
* UTAU oto.ini values are all in ms and measured from `offset` (the left blank),
|
|
300
|
-
* except `cutoff` (right blank):
|
|
301
|
-
* - cutoff >= 0 : measured from the END of the file
|
|
302
|
-
* - cutoff < 0 : region length from offset = |cutoff|
|
|
303
|
-
*
|
|
304
|
-
* After trimming, sample 0 == oto offset, so pre/overlap/consonant carry over
|
|
305
|
-
* unchanged (just converted to samples), and the slice length is the region end.
|
|
306
|
-
*/
|
|
307
|
-
declare function trimToOto(pcm: Int16Array, oto: OtoEntry, recordedPitch?: number): TrimmedPhoneme;
|
|
308
|
-
/**
|
|
309
|
-
* Pack normalized PCM phonemes into voice.bin + manifest.json.
|
|
310
|
-
* Each phoneme is trimmed to its oto region first.
|
|
311
|
-
* Duplicate aliases are silently overwritten by the later entry.
|
|
312
|
-
*/
|
|
313
|
-
declare function pack(inputs: PackInput[], referencePitch?: number): PackOutput;
|
|
314
|
-
|
|
315
|
-
/** Parse a note name like "E4", "G#4", "Db5" → frequency in Hz (null if invalid). */
|
|
316
|
-
declare function noteNameToHz(name: string): number | null;
|
|
317
|
-
/** Recorded pitch encoded in a multi-pitch alias suffix: "a い_E4" → 329.63 Hz. */
|
|
318
|
-
declare function pitchFromAliasSuffix(alias: string): number | null;
|
|
319
|
-
/**
|
|
320
|
-
* Estimate the fundamental frequency (Hz) of a voiced region by normalized
|
|
321
|
-
* autocorrelation. Returns 0 when no clear pitch is found (unvoiced consonant,
|
|
322
|
-
* silence, or a region too short to analyse).
|
|
323
|
-
*
|
|
324
|
-
* The signal is decimated to a lower analysis rate for speed; f0 below ~700 Hz
|
|
325
|
-
* is well within the resulting Nyquist limit. A parabolic interpolation around
|
|
326
|
-
* the best lag gives sub-sample (sub-semitone) accuracy.
|
|
327
|
-
*/
|
|
328
|
-
declare function detectF0(pcm: Int16Array, start: number, end: number): number;
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* UTAU `.frq` frequency-analysis files (FREQ0003). The same format OpenUtau
|
|
332
|
-
* reads: an 8-byte header, hop size, then the average fundamental frequency of
|
|
333
|
-
* the recording — exactly the reference pitch we need for correct resampling.
|
|
334
|
-
*
|
|
335
|
-
* Layout:
|
|
336
|
-
* char[8] "FREQ0003"
|
|
337
|
-
* int32 hopSize
|
|
338
|
-
* float64 averageF0 ← the recorded pitch in Hz
|
|
339
|
-
* byte[16] (blank)
|
|
340
|
-
* int32 length
|
|
341
|
-
* { float64 f0, float64 amp } × length
|
|
342
|
-
*/
|
|
343
|
-
declare function parseFrqAverageF0(buffer: ArrayBuffer): number | null;
|
|
344
|
-
/** Map a WAV filename to its sibling frq filename: "あ.wav" → "あ_wav.frq". */
|
|
345
|
-
declare function frqFileName(wavName: string): string;
|
|
346
|
-
|
|
347
360
|
/**
|
|
348
361
|
* Koe Archive Format (.koe)
|
|
349
362
|
* [4B] magic 'KOE\0' (big-endian)
|