@onjmin/koe 1.0.8 → 1.0.10

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 CHANGED
@@ -1,375 +1,440 @@
1
- # koe
2
-
3
- ブラウザ上でUTAU音源をラグ無しで再生するnpmモジュール。
4
-
5
- UTAU の oto.ini で定義された音源を `.koe` アーカイブに変換し、WebAssembly + AudioWorklet でリアルタイム再生・高品質再合成を行う。
6
- oto.ini が無い収録済み wav フォルダからは、原音設定そのものを自動生成できる。
7
-
8
- - [DEMO](https://onjmin.github.io/koe/demo) koeフォーマット作成もこちらで
9
- - [npm](https://www.npmjs.com/package/@onjmin/koe)
10
-
11
- ---
12
-
13
- ## インストール
14
-
15
- ```bash
16
- npm install @onjmin/koe
17
- # or
18
- pnpm add @onjmin/koe
19
- ```
20
-
21
- ---
22
-
23
- ## 設計概要
24
-
25
- ```
26
- UTAU音源 (wav + oto.ini + frq)
27
- ↓ 変換 (pack / koe-convert CLI)
28
- .koe アーカイブ
29
- ├── 8byte ヘッダ (magic + JSON長)
30
- ├── manifest JSON (音素テーブル・ピッチ情報)
31
- └── PCM バイナリ (Int16 / 48kHz / mono)
32
-
33
- VoiceBank ──── Blob slice / HTTP Range で音素を必要時のみ取得
34
- ├── KoeEngine ── AudioWorklet で連接合成・リアルタイム再生
35
- └── Worldline ── WORLD ボコーダ (WASM) で高品質ノート合成
36
- ```
37
-
38
- - **VoiceBank** : `.koe` ファイルの読み取り専用ビュー。AudioContext 不要。音素 PCM をオンデマンドで取得する。
39
- - **KoeEngine** : メインスレッド API。`VoiceBank` の上に AudioWorklet を組み合わせた連接合成エンジン。
40
- - **Worldline** : OpenUtau の worldline WASM を使った高品質ノート合成。PCM を入力して Float32 PCM を返す純粋な合成器。
41
-
42
- ---
43
-
44
- ## 使い方
45
-
46
- ### 1. 原音設定の自動生成 (wav → oto.ini)
47
-
48
- 収録済みの wav フォルダから oto.ini を生成する。CLI と [DEMO ページ](https://onjmin.github.io/koe/demo) の両方から使える。
49
-
50
- #### GUI から
51
-
52
- DEMO ページの「原音設定 — oto.ini を作る」カードで、wav フォルダ(または zip)を選んでボタンを押すだけ。wav が入っているフォルダごとに oto.ini ができ、そのまま保存(複数フォルダなら zip)できる。「変換へ」を押すと、作った oto.ini をそのまま使って `.koe` を生成し、その場で歌わせて確認できる。
53
-
54
- エイリアス接尾辞と、`- か` / `* あ` エイリアスを作るかどうかはカード内で切り替えられる。処理はすべてブラウザ内で完結し、wav はどこにも送信されない。
55
-
56
- #### CLI から
57
-
58
- ```bash
59
- npx koe-oto <音源フォルダ>
60
- ```
61
-
62
- フォルダを渡すだけで、その下の「wav が置かれている全フォルダ」それぞれに oto.ini が書き出される。
63
-
64
- | オプション | 説明 |
65
- | --- | --- |
66
- | `-n`, `--dry-run` | 書き込まず、生成結果の件数だけ表示する |
67
- | `-f`, `--force` | 既存の oto.ini を上書きする (`oto.ini.bak` を残す) |
68
- | `--suffix <s>` | 全エイリアスの末尾に `<s>` を付ける (既定: フォルダ名が音階名なら `_G4` 等) |
69
- | `-q`, `--quiet` | 集計行だけ出力する |
70
-
71
- 既存の oto.ini があるフォルダは既定でスキップする。上書きしたい場合のみ `--force` を付ける。
72
-
73
- #### 収録方式の判定
74
-
75
- ファイル名がそのまま音素の書き起こしになっているので、方式はファイル名から決まる。
76
-
77
- | ファイル名 | 判定 | 生成されるエイリアス |
78
- | --- | --- | --- |
79
- | `か.wav` / `_きゃ.wav` | 単独音 | `か`, `- か` |
80
- | `_あ.wav` | 単独音 (母音) | `あ`, `- あ`, `* あ` |
81
- | `_ああいあうえあ.wav` | 連続音 | `- あ`, `a あ`, `a い`, `i あ`, … |
82
- | `_ああR.wav` | 連続音 + 語尾 | `- あ`, `a あ`, `a R` |
83
- | `_あb.wav` / `_ううわ↑.wav` | テイク違い | `あb`, `- あb` / `- う↑`, `u う↑` … |
84
- | `_xか.wav` | 語頭記号付き | `xか`, `- xか` |
85
- | `カラオケ.wav` | 仮名として読めない → スキップ | — |
86
-
87
- カタカナ・拗音・外来音 (`ヴぁ`, `てぃ`, `つぉ` 等) も解決する。仮名の前後に付いた `x` / `b` / `2` / `↑↓` のようなテイク記号はエイリアスに引き継ぐ。仮名として読めないファイルは音声ではないものとして飛ばすので、伴奏やサンプル曲が混ざったフォルダでもそのまま渡せる。
88
-
89
- フォルダ名が音階そのもの (`G4`) か、音階タグを含む (`多音階03:_G4(連続音)`) 場合は、多音階音源としてエイリアス末尾に `_G4` を付ける。音源全体で oto.ini のエイリアスは1つの名前空間に混ざるため、これが無いと音階ごとの `- あ` が互いを上書きしてしまう。
90
-
91
- #### 推定のしかた
92
-
93
- [UTAU音源制作wiki の原音設定記事](https://w.atwiki.jp/vbmaker/pages/17.html) のセオリーをそのまま実装している。
94
-
95
- - **オフセット** — 子音の立ち上がりの少し手前。さ行・は行のような摩擦音は 4kHz 以上の帯域で先に立ち上がるので、その帯域を見て検出する。
96
- - **先行発声** — 母音の開始点。無声子音 (か・さ・た・は・ぱ行) は声帯が鳴り出した瞬間、な・ま・ら行は鼻音/はじき音が開放された瞬間、や・わ行は渡りの中間。
97
- - **子音部 (固定範囲)** — 母音に入ってスペクトルが落ち着くまで。伸縮されるのが定常部の母音だけになる。
98
- - **オーバーラップ** — 先行発声に対する比で決める。共鳴音 (な・ま・ら・や・わ行) はおよそ 0.6 倍、摩擦音・破擦音は 0.3 倍、下限 12ms・上限 40ms。か・た・ぱ行は破裂前の無音を再現するため負値 (−10ms)。母音単体は 20ms 固定。
99
- - **右ブランク** — 減衰が始まる手前。負値 (オフセットからの相対値) で書き出す。
100
-
101
- 連続音はガイドBGMに合わせて収録されるため、モーラが等間隔に並ぶ。オンセット検出の自己相関からテンポを求め、グリッドを当ててから各モーラを最寄りのオンセットに吸着させる。テンポが求まれば先行発声 = 間隔の 1/2、オーバーラップ = その 1/3、固定範囲 = その 1.5 倍、右ブランク = ノートの 2/3 間隔先、という既存音源が共通して使っているテンプレートを当てる。
102
-
103
- #### 精度
104
-
105
- 手作業の音源との一致度 (絶対時刻でのずれ)。`重音テト単独音` は人力精度が高い音源として比較対象にした。
106
-
107
- | 音源 | 方式 | 先行発声 ≤20ms | ≤40ms | 中央絶対誤差 |
108
- | --- | --- | --- | --- | --- |
109
- | 重音テト単独音 | 単独音 | 72% | 85% | 11ms |
110
- | 欲音ルコ♀ A3 | 連続音 | 53% | 81% | 18ms |
111
- | つくよみちゃん _G4 | 連続音 | 42% | 71% | 25ms |
112
- | 束音ロゼ G4 | 連続音 | 43% | 61% | 28ms |
113
- | 欲音ルコ♂ | 連続音 | 30% | 55% | 37ms |
114
-
115
- テトでは他のパラメータも、オフセット ≤20ms 89% / 中央絶対誤差 8ms、オーバーラップ ≤20ms 79% / 10.6ms、右ブランク ≤20ms 85% / 8ms。一番緩いのは子音部 (固定範囲) で中央絶対誤差 34ms だが、これは伸縮の開始位置を決めるだけなのでリズムには効かない。
116
-
117
- 連続音は音源ごとのテンプレート方針の差がそのまま誤差に出るため、単独音より一致率が落ちる (欲音ルコ♂ は人力でモーラごとに詰めてある音源)。
118
-
119
- setParam の自動推定と同程度で、そのまま歌わせられる水準ではあるが、商用配布するなら人の手で詰める前提の出力。
120
-
121
- なお `息.wav` `咳払い.wav` のような非言語音は、歌詞が仮名で書かれていない以上どんなエイリアスを振るべきか決めようがないのでスキップする (スキップしたファイルは `GenerateResult.skipped` と CLI の集計に出る)。必要ならその数行だけ手で足すことになる。
122
-
123
- #### コードから使う
124
-
125
- ブラウザでもそのまま動く (`fs` に依存しない)。
126
-
127
- ```ts
128
- import { generateOto, formatOto, encodeOto } from "@onjmin/koe";
129
-
130
- const files = [{ name: "_あ.wav", data: arrayBuffer }, /* ... */];
131
- const { entries, skipped, style } = generateOto(files, { suffix: "_G4" });
132
-
133
- console.log(style); // "solo" | "sequence" | "mixed"
134
- console.log(formatOto(entries)); // oto.ini のテキスト
135
- const bytes = encodeOto(entries); // Shift-JIS の Uint8Array
136
- ```
137
-
138
- `generateOto` は同期処理なので、フォルダが大きいとブラウザのUIが固まる。進捗を出したい場合は1ファイルずつ回す:
139
-
140
- ```ts
141
- import { generateOtoForFile, summarise } from "@onjmin/koe";
142
-
143
- for (const file of files) {
144
- const { entries, skipped, style } = generateOtoForFile(file, { suffix: "_G4" });
145
- // …集計して、ここでイベントループに制御を返す
146
- }
147
- ```
148
-
149
- `generateOto` が返す `entries` は `parseOto` と同じ `OtoEntry[]` なので、そのまま `pack()` に渡して `.koe` 化できる。
150
-
151
- ---
152
-
153
- ### 2. 音源の変換 (oto.ini → .koe)
154
-
155
- CLIコマンド `koe-convert` で UTAU 音源を `.koe` アーカイブに変換する。
156
-
157
- ```bash
158
- npx koe-convert <音源フォルダ> -o voice.koe
159
- ```
160
-
161
- あるいはコードから変換する:
162
-
163
- ```ts
164
- import { parseOto, parseWav, toMono, resample, normalizePcm, pack, packKoe, parseFrqAverageF0, frqFileName, pitchFromAliasSuffix } from "@onjmin/koe";
165
-
166
- // oto.ini をパース
167
- const otoText = await fs.readFile("voice/oto.ini", "utf8");
168
- const entries = parseOto(otoText);
169
-
170
- // 各エントリを PackInput に変換
171
- const inputs = await Promise.all(entries.map(async (oto) => {
172
- const wavBuf = await fs.readFile(`voice/${oto.file}`);
173
- const wav = parseWav(wavBuf.buffer);
174
- const mono = toMono(wav);
175
- const resampled = resample(mono, wav.sampleRate, 48000);
176
- const pcm = normalizePcm(resampled); // Int16Array
177
-
178
- // .frq ファイルからピッチ取得 (任意)
179
- let recordedPitch = pitchFromAliasSuffix(oto.alias); // エイリアス末尾から推定
180
- if (!recordedPitch) {
181
- try {
182
- const frqBuf = await fs.readFile(`voice/${frqFileName(oto.file)}`);
183
- recordedPitch = parseFrqAverageF0(frqBuf.buffer);
184
- } catch {}
185
- }
186
-
187
- return { oto, pcm, recordedPitch };
188
- }));
189
-
190
- // 変換してパック
191
- const { manifest, bin } = pack(inputs, 220 /* referencePitch Hz */);
192
- const blob = packKoe(manifest, [bin]);
193
-
194
- // blob を voice.koe として保存 (Node.js)
195
- const buf = Buffer.from(await blob.arrayBuffer());
196
- await fs.writeFile("voice.koe", buf);
197
- ```
198
-
199
- ---
200
-
201
- ### 3. KoeEngine — リアルタイム再生 (ブラウザ)
202
-
203
- AudioWorklet を使った連接合成エンジン。`koe-worklet.js` を同じオリジンから配信する必要がある。
204
-
205
- GitHub Pages にホストされているファイルをそのまま使える:
206
-
207
- ```ts
208
- import { KoeEngine } from "@onjmin/koe";
209
-
210
- const engine = new KoeEngine({
211
- workletUrl: "https://onjmin.github.io/koe/demo/koe-worklet.js",
212
- });
213
-
214
- // .koe ファイルをロード (Blob でも URL でも可)
215
- await engine.load("/voice.koe");
216
-
217
- // ノートシーケンスを再生
218
- await engine.play([
219
- { phoneme: "a", pitch: 440, duration: 48000 }, // 1秒 / A4
220
- { phoneme: "i", pitch: 494, duration: 24000 }, // 0.5秒 / B4
221
- { phoneme: "u", pitch: 392, duration: 48000 }, // 1秒 / G4
222
- ]);
223
-
224
- // 停止
225
- engine.stop();
226
-
227
- // ブラウザの autoplay ポリシーで停止した場合
228
- await engine.resume();
229
- ```
230
-
231
- **NoteEvent の型:**
232
-
233
- ```ts
234
- interface NoteEvent {
235
- phoneme: string; // 音素エイリアス (oto.ini の alias)
236
- pitch: number; // 出力ピッチ (Hz)
237
- duration: number; // 出力長 (サンプル数 @ 48kHz)
238
- }
239
- ```
240
-
241
- ---
242
-
243
- ### 4. VoiceBank — 音素 PCM の直接取得
244
-
245
- AudioContext 不要。WORLD ボコーダや独自の合成処理に PCM を渡したい場合に使う。
246
-
247
- ```ts
248
- import { VoiceBank } from "@onjmin/koe";
249
-
250
- // URL からロード (HTTP Range リクエストを使用)
251
- const bank = await VoiceBank.load("/voice.koe");
252
-
253
- // Blob からロード
254
- const blob = await fetch("/voice.koe").then(r => r.blob());
255
- const bank2 = await VoiceBank.load(blob);
256
-
257
- // マニフェスト参照
258
- console.log(bank.manifest.phonemes); // 全音素のオフセット・ピッチ情報
259
-
260
- // Float64 PCM 取得 ([-1, 1] 正規化済み)
261
- const pcm = await bank.getPcm("a");
262
-
263
- // Int16 PCM バイト列を取得 (AudioWorklet への転送用)
264
- const buf = await bank.readPcmBytes("a");
265
-
266
- // 音素の存在確認
267
- bank.has("a"); // boolean
268
- ```
269
-
270
- ---
271
-
272
- ### 5. Worldline — 高品質ノート合成 (WORLD ボコーダ)
273
-
274
- OpenUtau の worldline WASM で F0 分析・再合成を行う。`worldline.js` と `worldline.wasm` を配信する必要がある。
275
-
276
- GitHub Pages にホストされているファイルをそのまま使える:
277
-
278
- ```ts
279
- import { VoiceBank, Worldline, leadInFromEntry } from "@onjmin/koe";
280
-
281
- const bank = await VoiceBank.load("/voice.koe");
282
- const wl = await Worldline.load({
283
- scriptUrl: "https://onjmin.github.io/koe/demo/world/worldline.js",
284
- });
285
-
286
- const alias = "a";
287
- const entry = bank.manifest.phonemes[alias];
288
- const pcm = await bank.getPcm(alias);
289
-
290
- // ノートをレンダリング → Float32 PCM @ 48kHz
291
- const audio = wl.renderNote({
292
- pcm,
293
- pitch: 440, // Hz
294
- durationMs: 500, // 母音部分の長さ (ms)
295
- ...leadInFromEntry(entry), // preMs / consonantMs を entry から自動計算
296
- });
297
-
298
- if (audio) {
299
- // audio: Float32Array (レイアウト: [子音 ≈ preMs][母音 ≈ durationMs])
300
- const ctx = new AudioContext({ sampleRate: 48000 });
301
- const buf = ctx.createBuffer(1, audio.length, 48000);
302
- buf.copyToChannel(audio, 0);
303
- const src = ctx.createBufferSource();
304
- src.buffer = buf;
305
- src.connect(ctx.destination);
306
- src.start();
307
- }
308
- ```
309
-
310
- ---
311
-
312
- ### 6. .koe アーカイブ形式
313
-
314
- ```
315
- [4B] magic 'KOE\0' (big-endian)
316
- [4B] JSON (little-endian)
317
- [N ] manifest JSON (UTF-8)
318
- [M ] PCM バイナリ (Int16 / 48kHz / mono)
319
- ```
320
-
321
- パース・生成ユーティリティ:
322
-
323
- ```ts
324
- import { packKoe, parseKoeHeader, pcmBase } from "@onjmin/koe";
325
-
326
- // 生成
327
- const blob = packKoe(manifest, [pcmArrayBuffer]);
328
-
329
- // ヘッダ解析
330
- const headerBuf = await blob.slice(0, 8).arrayBuffer();
331
- const { jsonLength } = parseKoeHeader(headerBuf);
332
- const pcmOffset = pcmBase(jsonLength); // PCM データの開始バイト位置
333
- ```
334
-
335
- ---
336
-
337
- ## API リファレンス
338
-
339
- | エクスポート | 説明 |
340
- |---|---|
341
- | `KoeEngine` | AudioWorklet ベースの連接合成エンジン |
342
- | `VoiceBank` | .koe から音素 PCM をオンデマンド取得 |
343
- | `Worldline` | WORLD ボコーダによる高品質ノート合成 |
344
- | `generateOto` | wav oto.ini エントリを推定 (原音設定) |
345
- | `generateOtoForFile` | wav 1本ぶんの推定 (進捗表示したいとき用) |
346
- | `formatOto` / `encodeOto` | エントリ → oto.ini テキスト / Shift-JIS バイト列 |
347
- | `analyze` / `analyzeWav` | WAV フレーム特徴量 (RMS・有声度・スペクトル) |
348
- | `estimateSolo` / `estimateSequence` | 単独音 / 連続音 1ファイル分のパラメータ推定 |
349
- | `detectGrid` | 連続音のモーラ位置とテンポを検出 |
350
- | `splitKana` | 仮名文字列 モーラ (子音・母音・調音種別) |
351
- | `transcribe` | wav ファイル名 モーラ列 |
352
- | `parseOto` | oto.ini テキストをパース |
353
- | `parseWav` | WAV バイナリをパース |
354
- | `toMono` | ステレオ モノラル変換 |
355
- | `resample` | サンプルレート変換 |
356
- | `normalizePcm` | Float Int16 正規化 |
357
- | `pack` | 音素リスト manifest + PCM バイナリ |
358
- | `trimToOto` | WAV を oto リージョンにトリミング |
359
- | `packKoe` | manifest + PCM → .koe Blob |
360
- | `parseKoeHeader` | .koe ヘッダ解析 |
361
- | `unzipToFileMap` / `zipFiles` | zip の展開 / 作成 |
362
- | `pcmBase` | JSON長 PCM 開始バイト位置 |
363
- | `detectF0` | PCM からピッチ自動検出 |
364
- | `noteNameToHz` | 音名 → Hz 変換 (例: `"A4"` → `440`) |
365
- | `pitchFromAliasSuffix` | エイリアス末尾の音名からピッチ推定 |
366
- | `parseFrqAverageF0` | .frq ファイルから平均 F0 取得 |
367
- | `frqFileName` | WAV ファイル名 → .frq ファイル名 |
368
- | `leadInFromEntry` | PhonemeEntry → preMs / consonantMs 変換 |
369
- | `samplesToMs` | サンプル数 ミリ秒変換 |
370
-
371
- ---
372
-
373
- ## ライセンス
374
-
375
- MIT
1
+ # koe
2
+
3
+ ブラウザ上でUTAU音源をラグ無しで再生するnpmモジュール。
4
+
5
+ UTAU の oto.ini で定義された音源を `.koe` アーカイブに変換し、WebAssembly + AudioWorklet でリアルタイム再生・高品質再合成を行う。
6
+ oto.ini が無い収録済み wav フォルダからは、原音設定そのものを自動生成できる。
7
+
8
+ - [DEMO](https://onjmin.github.io/koe/demo) koeフォーマット作成もこちらで
9
+ - [npm](https://www.npmjs.com/package/@onjmin/koe)
10
+
11
+ ---
12
+
13
+ ## インストール
14
+
15
+ ```bash
16
+ npm install @onjmin/koe
17
+ # or
18
+ pnpm add @onjmin/koe
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 設計概要
24
+
25
+ ```
26
+ UTAU音源 (wav + oto.ini + frq)
27
+ ↓ 変換 (pack / koe-convert CLI)
28
+ .koe アーカイブ
29
+ ├── 8byte ヘッダ (magic + JSON長)
30
+ ├── manifest JSON (音素テーブル・ピッチ情報)
31
+ └── PCM バイナリ (Int16 / 48kHz / mono)
32
+
33
+ VoiceBank ──── Blob slice / HTTP Range で音素を必要時のみ取得
34
+ ├── KoeEngine ── AudioWorklet で連接合成・リアルタイム再生
35
+ └── Worldline ── WORLD ボコーダ (WASM) で高品質ノート合成
36
+ ```
37
+
38
+ - **VoiceBank** : `.koe` ファイルの読み取り専用ビュー。AudioContext 不要。音素 PCM をオンデマンドで取得する。
39
+ - **KoeEngine** : メインスレッド API。`VoiceBank` の上に AudioWorklet を組み合わせた連接合成エンジン。
40
+ - **Worldline** : OpenUtau の worldline WASM を使った高品質ノート合成。PCM を入力して Float32 PCM を返す純粋な合成器。
41
+
42
+ ---
43
+
44
+ ## 使い方
45
+
46
+ ### 1. 原音設定の自動生成 (wav → oto.ini)
47
+
48
+ 収録済みの wav フォルダから oto.ini を生成する。CLI と [DEMO ページ](https://onjmin.github.io/koe/demo) の両方から使える。
49
+
50
+ #### GUI から
51
+
52
+ DEMO ページの「原音設定 — oto.ini を作る」カードで、wav フォルダ(または zip)を選んでボタンを押すだけ。wav が入っているフォルダごとに oto.ini ができ、そのまま保存(複数フォルダなら zip)できる。「変換へ」を押すと、作った oto.ini をそのまま使って `.koe` を生成し、その場で歌わせて確認できる。
53
+
54
+ エイリアス接尾辞と、`- か` / `* あ` エイリアスを作るかどうかはカード内で切り替えられる。処理はすべてブラウザ内で完結し、wav はどこにも送信されない。
55
+
56
+ #### CLI から
57
+
58
+ ```bash
59
+ npx koe-oto <音源フォルダ>
60
+ ```
61
+
62
+ フォルダを渡すだけで、その下の「wav が置かれている全フォルダ」それぞれに oto.ini が書き出される。
63
+
64
+ | オプション | 説明 |
65
+ | --- | --- |
66
+ | `-n`, `--dry-run` | 書き込まず、生成結果の件数だけ表示する |
67
+ | `-f`, `--force` | 既存の oto.ini を上書きする (`oto.ini.bak` を残す) |
68
+ | `--suffix <s>` | 全エイリアスの末尾に `<s>` を付ける (既定: フォルダ名が音階名なら `_G4` 等) |
69
+ | `-q`, `--quiet` | 集計行だけ出力する |
70
+
71
+ 既存の oto.ini があるフォルダは既定でスキップする。上書きしたい場合のみ `--force` を付ける。
72
+
73
+ #### 収録方式の判定
74
+
75
+ ファイル名がそのまま音素の書き起こしになっているので、方式はファイル名から決まる。
76
+
77
+ | ファイル名 | 判定 | 生成されるエイリアス |
78
+ | --- | --- | --- |
79
+ | `か.wav` / `_きゃ.wav` | 単独音 | `か`, `- か` |
80
+ | `_あ.wav` | 単独音 (母音) | `あ`, `- あ`, `* あ` |
81
+ | `_ああいあうえあ.wav` | 連続音 | `- あ`, `a あ`, `a い`, `i あ`, … |
82
+ | `_ああR.wav` | 連続音 + 語尾 | `- あ`, `a あ`, `a R` |
83
+ | `_あb.wav` / `_ううわ↑.wav` | テイク違い | `あb`, `- あb` / `- う↑`, `u う↑` … |
84
+ | `_xか.wav` | 語頭記号付き | `xか`, `- xか` |
85
+ | `カラオケ.wav` | 仮名として読めない → スキップ | — |
86
+
87
+ カタカナ・拗音・外来音 (`ヴぁ`, `てぃ`, `つぉ` 等) も解決する。仮名の前後に付いた `x` / `b` / `2` / `↑↓` のようなテイク記号はエイリアスに引き継ぐ。仮名として読めないファイルは音声ではないものとして飛ばすので、伴奏やサンプル曲が混ざったフォルダでもそのまま渡せる。
88
+
89
+ フォルダ名が音階そのもの (`G4`) か、音階タグを含む (`多音階03:_G4(連続音)`) 場合は、多音階音源としてエイリアス末尾に `_G4` を付ける。音源全体で oto.ini のエイリアスは1つの名前空間に混ざるため、これが無いと音階ごとの `- あ` が互いを上書きしてしまう。
90
+
91
+ #### 推定のしかた
92
+
93
+ [UTAU音源制作wiki の原音設定記事](https://w.atwiki.jp/vbmaker/pages/17.html) のセオリーをそのまま実装している。
94
+
95
+ - **オフセット** — 子音の立ち上がりの少し手前。さ行・は行のような摩擦音は 4kHz 以上の帯域で先に立ち上がるので、その帯域を見て検出する。
96
+ - **先行発声** — 母音の開始点。無声子音 (か・さ・た・は・ぱ行) は声帯が鳴り出した瞬間、な・ま・ら行は鼻音/はじき音が開放された瞬間、や・わ行は渡りの中間。
97
+ - **子音部 (固定範囲)** — 母音に入ってスペクトルが落ち着くまで。伸縮されるのが定常部の母音だけになる。
98
+ - **オーバーラップ** — 先行発声に対する比で決める。共鳴音 (な・ま・ら・や・わ行) はおよそ 0.6 倍、摩擦音・破擦音は 0.3 倍、下限 12ms・上限 40ms。か・た・ぱ行は破裂前の無音を再現するため負値 (−10ms)。母音単体は 20ms 固定。
99
+ - **右ブランク** — 減衰が始まる手前。負値 (オフセットからの相対値) で書き出す。
100
+
101
+ 連続音はガイドBGMに合わせて収録されるため、モーラが等間隔に並ぶ。オンセット検出の自己相関からテンポを求め、グリッドを当ててから各モーラを最寄りのオンセットに吸着させる。テンポが求まれば先行発声 = 間隔の 1/2、オーバーラップ = その 1/3、固定範囲 = その 1.5 倍、右ブランク = ノートの 2/3 間隔先、という既存音源が共通して使っているテンプレートを当てる。
102
+
103
+ #### 精度
104
+
105
+ 手作業の音源との一致度 (絶対時刻でのずれ)。`重音テト単独音` は人力精度が高い音源として比較対象にした。
106
+
107
+ | 音源 | 方式 | 先行発声 ≤20ms | ≤40ms | 中央絶対誤差 |
108
+ | --- | --- | --- | --- | --- |
109
+ | 重音テト単独音 | 単独音 | 72% | 85% | 11ms |
110
+ | 欲音ルコ♀ A3 | 連続音 | 53% | 81% | 18ms |
111
+ | つくよみちゃん _G4 | 連続音 | 42% | 71% | 25ms |
112
+ | 束音ロゼ G4 | 連続音 | 43% | 61% | 28ms |
113
+ | 欲音ルコ♂ | 連続音 | 30% | 55% | 37ms |
114
+
115
+ テトでは他のパラメータも、オフセット ≤20ms 89% / 中央絶対誤差 8ms、オーバーラップ ≤20ms 79% / 10.6ms、右ブランク ≤20ms 85% / 8ms。一番緩いのは子音部 (固定範囲) で中央絶対誤差 34ms だが、これは伸縮の開始位置を決めるだけなのでリズムには効かない。
116
+
117
+ 連続音は音源ごとのテンプレート方針の差がそのまま誤差に出るため、単独音より一致率が落ちる (欲音ルコ♂ は人力でモーラごとに詰めてある音源)。
118
+
119
+ setParam の自動推定と同程度で、そのまま歌わせられる水準ではあるが、商用配布するなら人の手で詰める前提の出力。
120
+
121
+ なお `息.wav` `咳払い.wav` のような非言語音は、歌詞が仮名で書かれていない以上どんなエイリアスを振るべきか決めようがないのでスキップする (スキップしたファイルは `GenerateResult.skipped` と CLI の集計に出る)。必要ならその数行だけ手で足すことになる。
122
+
123
+ #### コードから使う
124
+
125
+ ブラウザでもそのまま動く (`fs` に依存しない)。
126
+
127
+ ```ts
128
+ import { generateOto, formatOto, encodeOto } from "@onjmin/koe";
129
+
130
+ const files = [{ name: "_あ.wav", data: arrayBuffer }, /* ... */];
131
+ const { entries, skipped, style } = generateOto(files, { suffix: "_G4" });
132
+
133
+ console.log(style); // "solo" | "sequence" | "mixed"
134
+ console.log(formatOto(entries)); // oto.ini のテキスト
135
+ const bytes = encodeOto(entries); // Shift-JIS の Uint8Array
136
+ ```
137
+
138
+ `generateOto` は同期処理なので、フォルダが大きいとブラウザのUIが固まる。進捗を出したい場合は1ファイルずつ回す:
139
+
140
+ ```ts
141
+ import { generateOtoForFile, summarise } from "@onjmin/koe";
142
+
143
+ for (const file of files) {
144
+ const { entries, skipped, style } = generateOtoForFile(file, { suffix: "_G4" });
145
+ // …集計して、ここでイベントループに制御を返す
146
+ }
147
+ ```
148
+
149
+ `generateOto` が返す `entries` は `parseOto` と同じ `OtoEntry[]` なので、そのまま `pack()` に渡して `.koe` 化できる。
150
+
151
+ ---
152
+
153
+ ### 2. 音源の変換 (oto.ini → .koe)
154
+
155
+ CLIコマンド `koe-convert` で UTAU 音源を `.koe` アーカイブに変換する。
156
+
157
+ ```bash
158
+ npx koe-convert <音源フォルダ> -o voice.koe
159
+ ```
160
+
161
+ あるいはコードから変換する:
162
+
163
+ ```ts
164
+ import { parseOto, parseWav, toMono, resample, normalizePcm, pack, packKoe, parseFrqAverageF0, frqFileName, pitchFromAliasSuffix } from "@onjmin/koe";
165
+
166
+ // oto.ini をパース
167
+ const otoText = await fs.readFile("voice/oto.ini", "utf8");
168
+ const entries = parseOto(otoText);
169
+
170
+ // 各エントリを PackInput に変換
171
+ const inputs = await Promise.all(entries.map(async (oto) => {
172
+ const wavBuf = await fs.readFile(`voice/${oto.file}`);
173
+ const wav = parseWav(wavBuf.buffer);
174
+ const mono = toMono(wav);
175
+ const resampled = resample(mono, wav.sampleRate, 48000);
176
+ const pcm = normalizePcm(resampled); // Int16Array
177
+
178
+ // .frq ファイルからピッチ取得 (任意)
179
+ let recordedPitch = pitchFromAliasSuffix(oto.alias); // エイリアス末尾から推定
180
+ if (!recordedPitch) {
181
+ try {
182
+ const frqBuf = await fs.readFile(`voice/${frqFileName(oto.file)}`);
183
+ recordedPitch = parseFrqAverageF0(frqBuf.buffer);
184
+ } catch {}
185
+ }
186
+
187
+ return { oto, pcm, recordedPitch };
188
+ }));
189
+
190
+ // 変換してパック
191
+ const { manifest, bin } = pack(inputs, 220 /* referencePitch Hz */);
192
+ const blob = packKoe(manifest, [bin]);
193
+
194
+ // blob を voice.koe として保存 (Node.js)
195
+ const buf = Buffer.from(await blob.arrayBuffer());
196
+ await fs.writeFile("voice.koe", buf);
197
+ ```
198
+
199
+ ---
200
+
201
+ ### 3. KoeEngine — リアルタイム再生 (ブラウザ)
202
+
203
+ AudioWorklet を使った連接合成エンジン。`koe-worklet.js` を同じオリジンから配信する必要がある。
204
+
205
+ GitHub Pages にホストされているファイルをそのまま使える:
206
+
207
+ ```ts
208
+ import { KoeEngine } from "@onjmin/koe";
209
+
210
+ const engine = new KoeEngine({
211
+ workletUrl: "https://onjmin.github.io/koe/demo/koe-worklet.js",
212
+ });
213
+
214
+ // .koe ファイルをロード (Blob でも URL でも可)
215
+ await engine.load("/voice.koe");
216
+
217
+ // ノートシーケンスを再生
218
+ await engine.play([
219
+ { phoneme: "a", pitch: 440, duration: 48000 }, // 1秒 / A4
220
+ { phoneme: "i", pitch: 494, duration: 24000 }, // 0.5秒 / B4
221
+ { phoneme: "u", pitch: 392, duration: 48000 }, // 1秒 / G4
222
+ ]);
223
+
224
+ // 停止
225
+ engine.stop();
226
+
227
+ // ブラウザの autoplay ポリシーで停止した場合
228
+ await engine.resume();
229
+ ```
230
+
231
+ **NoteEvent の型:**
232
+
233
+ ```ts
234
+ interface NoteEvent {
235
+ phoneme: string; // 音素エイリアス (oto.ini の alias)
236
+ pitch: number; // 出力ピッチ (Hz)
237
+ duration: number; // 出力長 (サンプル数 @ 48kHz)
238
+ }
239
+ ```
240
+
241
+ ---
242
+
243
+ ### 4. VoiceBank — 音素 PCM の直接取得
244
+
245
+ AudioContext 不要。WORLD ボコーダや独自の合成処理に PCM を渡したい場合に使う。
246
+
247
+ ```ts
248
+ import { VoiceBank } from "@onjmin/koe";
249
+
250
+ // URL からロード (HTTP Range リクエストを使用)
251
+ const bank = await VoiceBank.load("/voice.koe");
252
+
253
+ // Blob からロード
254
+ const blob = await fetch("/voice.koe").then(r => r.blob());
255
+ const bank2 = await VoiceBank.load(blob);
256
+
257
+ // マニフェスト参照
258
+ console.log(bank.manifest.phonemes); // 全音素のオフセット・ピッチ情報
259
+
260
+ // Float64 PCM 取得 ([-1, 1] 正規化済み)
261
+ const pcm = await bank.getPcm("a");
262
+
263
+ // Int16 PCM バイト列を取得 (AudioWorklet への転送用)
264
+ const buf = await bank.readPcmBytes("a");
265
+
266
+ // 音素の存在確認
267
+ bank.has("a"); // boolean
268
+ ```
269
+
270
+ ---
271
+
272
+ ### 5. Worldline — 高品質ノート合成 (WORLD ボコーダ)
273
+
274
+ OpenUtau の worldline WASM で F0 分析・再合成を行う。`worldline.js` と `worldline.wasm` を配信する必要がある。
275
+
276
+ GitHub Pages にホストされているファイルをそのまま使える:
277
+
278
+ ```ts
279
+ import { VoiceBank, Worldline, leadInFromEntry } from "@onjmin/koe";
280
+
281
+ const bank = await VoiceBank.load("/voice.koe");
282
+ const wl = await Worldline.load({
283
+ scriptUrl: "https://onjmin.github.io/koe/demo/world/worldline.js",
284
+ });
285
+
286
+ const alias = "a";
287
+ const entry = bank.manifest.phonemes[alias];
288
+ const pcm = await bank.getPcm(alias);
289
+
290
+ // ノートをレンダリング → Float32 PCM @ 48kHz
291
+ const audio = wl.renderNote({
292
+ pcm,
293
+ pitch: 440, // Hz
294
+ durationMs: 500, // 母音部分の長さ (ms)
295
+ ...leadInFromEntry(entry), // preMs / consonantMs を entry から自動計算
296
+ });
297
+
298
+ if (audio) {
299
+ // audio: Float32Array (レイアウト: [子音 ≈ preMs][母音 ≈ durationMs])
300
+ const ctx = new AudioContext({ sampleRate: 48000 });
301
+ const buf = ctx.createBuffer(1, audio.length, 48000);
302
+ buf.copyToChannel(audio, 0);
303
+ const src = ctx.createBufferSource();
304
+ src.buffer = buf;
305
+ src.connect(ctx.destination);
306
+ src.start();
307
+ }
308
+ ```
309
+
310
+ ---
311
+
312
+ ### 6. UtauTTS 読み上げ (日本語 TTS)
313
+
314
+ [UtauTTS](https://github.com/onjmin/UtauTTS) の前半(読み・アクセント解析 → ユニット選択 → 時間計画 → TCN イントネーション → worldline 配置)を Wasm で実行し、波形は `Worldline` で合成する。必要なアセットは 3 つで、いずれも DEMO と同じ配置で配信する(`dist/utautts/` にも同梱):
315
+
316
+ | アセット | サイズ | 役割 |
317
+ |---|---|---|
318
+ | `utautts/utautts.wasm` + `wasm_exec.js` | 約 10MB (gzip 3.5MB) | UtauTTS プランナー (Go) |
319
+ | `utautts/jpreprocess_wasm/` + `naist-jdic/*.gz` | 1.4MB + 約 29MB | OpenJTalk 互換の読み・アクセント解析と辞書 |
320
+ | `utautts/frame-intonation-v8.json` | 1MB | TCN イントネーションモデル |
321
+ | `utautts/hts/tohoku-f01-neutral.htsvoice` | 2MB | HTS 音声モデル(東北大学 伊藤・能勢研究室 tohoku-f01, CC BY 4.0)。音素長と F0 だけを取り出して UTAU 音源に移植する |
322
+
323
+ ```ts
324
+ import {
325
+ VoiceBank, Worldline, UtauTTSAdapter, openjtalkAnalyze, alignHtsProsody, shapeProsody, isQuestion,
326
+ fetchAsset, fetchAssetBytes, fetchAssetText, loadNaistJdic, initJpreprocessDictionary,
327
+ } from "@onjmin/koe";
328
+ import initJpreprocess, * as jpreprocess from "./utautts/jpreprocess_wasm/jpreprocess_wasm.js";
329
+ // <script src="./utautts/wasm_exec.js"></script> を先に読み込んでおく
330
+
331
+ // 初回のみ。fetchAsset Cache API に保存するので 2 回目以降はネットワークを使わない。
332
+ await initJpreprocess({ module_or_path: fetchAsset("./utautts/jpreprocess_wasm/jpreprocess_wasm_bg.wasm") });
333
+ initJpreprocessDictionary(jpreprocess, await loadNaistJdic("./utautts/jpreprocess_wasm/naist-jdic"));
334
+ await UtauTTSAdapter.initializeWasm("./utautts/utautts.wasm", { fetch: fetchAsset });
335
+ UtauTTSAdapter.setModel(await fetchAssetText("./utautts/frame-intonation-v8.json"));
336
+
337
+ const bank = await VoiceBank.load("/voice.koe");
338
+ const wl = await Worldline.load({ scriptUrl: "./world/worldline.js" });
339
+ const tts = new UtauTTSAdapter(wl);
340
+
341
+ const text = "こんにちは、私の名前はテトです。";
342
+ const { features } = openjtalkAnalyze(JSON.parse(jpreprocess.analyze_text(text)));
343
+
344
+ // 韻律は 2 通り。HTS 音声モデルの音素長と F0 を移植する(推奨、アクセントの起伏が大きい)か、
345
+ // UtauTTS TCN モデルに任せる(prosody を渡さない)。
346
+ jpreprocess.init_voice(await fetchAssetBytes("./utautts/hts/tohoku-f01-neutral.htsvoice"));
347
+ const frames = JSON.parse(jpreprocess.analyze_prosody(text, 1.0)); // 音素ごとの長さ + 5ms 刻みの F0(第 3 引数は F0 の GV 重み。GV を持つ音声モデルでのみ有効、tohoku-f01 には無い)
348
+ let prosody = alignHtsProsody(frames, features, { intonationStrength: 1 }); // モーラに整列(失敗時 null)
349
+ // 規則による残差: 「?」で終わる文の語尾上げ、F0 に連動した音量の抑揚、無声化母音(です・ます)の減音
350
+ if (prosody) prosody = shapeProsody(prosody, features, { question: isQuestion(text) });
351
+ const plan = tts.plan(bank, text, features, { prosody: prosody ?? undefined }); // 選択・タイミング・F0 曲線・worldline 配置
352
+
353
+ // チャンクごとに合成 届いた順にスケジュールすると数モーラ分で再生が始まる
354
+ const ctx = new AudioContext({ sampleRate: 48000 });
355
+ const t0 = ctx.currentTime + 0.1;
356
+ for await (const chunk of tts.renderChunks(bank, plan)) {
357
+ const buf = ctx.createBuffer(1, chunk.pcm.length, 48000);
358
+ buf.copyToChannel(chunk.pcm, 0);
359
+ const src = ctx.createBufferSource();
360
+ src.buffer = buf;
361
+ src.connect(ctx.destination);
362
+ src.start(t0 + chunk.startMs / 1000); // 重なる部分は等パワーのクロスフェード済み
363
+ }
364
+
365
+ // 一括で 1 本の Float32Array が欲しいとき
366
+ const pcm = await tts.synthesizeText(bank, text, features);
367
+ ```
368
+
369
+ `plan()` のオプション(`tone`, `moraDurationMs`, `pauseDurationMs`, `releaseMs`, `applyPitch`, `intonationStrength`, `speechTiming`)は `utautts-cli` と同じ意味・既定値。ピッチの基準は各音素の収録ピッチ(manifest の `pitch`)で、TCN の輪郭はそこからの相対値として掛かる。
370
+
371
+ ---
372
+
373
+ ### 7. .koe アーカイブ形式
374
+
375
+ ```
376
+ [4B] magic 'KOE\0' (big-endian)
377
+ [4B] JSON 長 (little-endian)
378
+ [N ] manifest JSON (UTF-8)
379
+ [M ] PCM バイナリ (Int16 / 48kHz / mono)
380
+ ```
381
+
382
+ パース・生成ユーティリティ:
383
+
384
+ ```ts
385
+ import { packKoe, parseKoeHeader, pcmBase } from "@onjmin/koe";
386
+
387
+ // 生成
388
+ const blob = packKoe(manifest, [pcmArrayBuffer]);
389
+
390
+ // ヘッダ解析
391
+ const headerBuf = await blob.slice(0, 8).arrayBuffer();
392
+ const { jsonLength } = parseKoeHeader(headerBuf);
393
+ const pcmOffset = pcmBase(jsonLength); // PCM データの開始バイト位置
394
+ ```
395
+
396
+ ---
397
+
398
+ ## API リファレンス
399
+
400
+ | エクスポート | 説明 |
401
+ |---|---|
402
+ | `KoeEngine` | AudioWorklet ベースの連接合成エンジン |
403
+ | `VoiceBank` | .koe から音素 PCM をオンデマンド取得 |
404
+ | `Worldline` | WORLD ボコーダによる高品質ノート合成 |
405
+ | `UtauTTSAdapter` | UtauTTS プランナー (Wasm) + worldline による日本語読み上げ。`plan` / `renderChunks` / `synthesizeText` |
406
+ | `openjtalkAnalyze`, `sparse_features`, `readingFromFeatures` | jpreprocess の NJD 出力 → モーラ特徴量・読み |
407
+ | `alignHtsProsody`, `shapeProsody`, `isQuestion` | HTS の音素長・F0 をモーラに整列し、疑問の語尾上げ・音量包絡・無声化の規則を重ねる |
408
+ | `fetchAsset`, `loadNaistJdic`, `initJpreprocessDictionary` | Cache API 付きアセット取得と naist-jdic 辞書の読み込み |
409
+ | `generateOto` | wav 群 → oto.ini エントリを推定 (原音設定) |
410
+ | `generateOtoForFile` | wav 1本ぶんの推定 (進捗表示したいとき用) |
411
+ | `formatOto` / `encodeOto` | エントリ → oto.ini テキスト / Shift-JIS バイト列 |
412
+ | `analyze` / `analyzeWav` | WAV → フレーム特徴量 (RMS・有声度・スペクトル) |
413
+ | `estimateSolo` / `estimateSequence` | 単独音 / 連続音 1ファイル分のパラメータ推定 |
414
+ | `detectGrid` | 連続音のモーラ位置とテンポを検出 |
415
+ | `splitKana` | 仮名文字列 → モーラ (子音・母音・調音種別) |
416
+ | `transcribe` | wav ファイル名 → モーラ列 |
417
+ | `parseOto` | oto.ini テキストをパース |
418
+ | `parseWav` | WAV バイナリをパース |
419
+ | `toMono` | ステレオ → モノラル変換 |
420
+ | `resample` | サンプルレート変換 |
421
+ | `normalizePcm` | Float → Int16 正規化 |
422
+ | `pack` | 音素リスト → manifest + PCM バイナリ |
423
+ | `trimToOto` | WAV を oto リージョンにトリミング |
424
+ | `packKoe` | manifest + PCM → .koe Blob |
425
+ | `parseKoeHeader` | .koe ヘッダ解析 |
426
+ | `unzipToFileMap` / `zipFiles` | zip の展開 / 作成 |
427
+ | `pcmBase` | JSON長 → PCM 開始バイト位置 |
428
+ | `detectF0` | PCM からピッチ自動検出 |
429
+ | `noteNameToHz` | 音名 → Hz 変換 (例: `"A4"` → `440`) |
430
+ | `pitchFromAliasSuffix` | エイリアス末尾の音名からピッチ推定 |
431
+ | `parseFrqAverageF0` | .frq ファイルから平均 F0 取得 |
432
+ | `frqFileName` | WAV ファイル名 → .frq ファイル名 |
433
+ | `leadInFromEntry` | PhonemeEntry → preMs / consonantMs 変換 |
434
+ | `samplesToMs` | サンプル数 → ミリ秒変換 |
435
+
436
+ ---
437
+
438
+ ## ライセンス
439
+
440
+ MIT