@onjmin/koe 1.0.0 → 1.0.3
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 +258 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +81 -21
- package/dist/index.js.map +1 -1
- package/dist/koe-convert.js +24 -6
- package/package.json +21 -13
package/README.md
CHANGED
|
@@ -1,2 +1,258 @@
|
|
|
1
|
-
# koe
|
|
2
|
-
|
|
1
|
+
# koe
|
|
2
|
+
|
|
3
|
+
ブラウザ上でUTAU音源をラグ無しで再生するnpmモジュール。
|
|
4
|
+
|
|
5
|
+
UTAU の oto.ini で定義された音源を `.koe` アーカイブに変換し、WebAssembly + AudioWorklet でリアルタイム再生・高品質再合成を行う。
|
|
6
|
+
|
|
7
|
+
- [DEMO](https://onjmin.github.io/koe/demo) koeフォーマット作成もこちらで
|
|
8
|
+
- [npm](https://www.npmjs.com/package/@onjmin/koe)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## インストール
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @onjmin/koe
|
|
16
|
+
# or
|
|
17
|
+
pnpm add @onjmin/koe
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 設計概要
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
UTAU音源 (wav + oto.ini + frq)
|
|
26
|
+
↓ 変換 (pack / koe-convert CLI)
|
|
27
|
+
.koe アーカイブ
|
|
28
|
+
├── 8byte ヘッダ (magic + JSON長)
|
|
29
|
+
├── manifest JSON (音素テーブル・ピッチ情報)
|
|
30
|
+
└── PCM バイナリ (Int16 / 48kHz / mono)
|
|
31
|
+
↓
|
|
32
|
+
VoiceBank ──── Blob slice / HTTP Range で音素を必要時のみ取得
|
|
33
|
+
├── KoeEngine ── AudioWorklet で連接合成・リアルタイム再生
|
|
34
|
+
└── Worldline ── WORLD ボコーダ (WASM) で高品質ノート合成
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **VoiceBank** : `.koe` ファイルの読み取り専用ビュー。AudioContext 不要。音素 PCM をオンデマンドで取得する。
|
|
38
|
+
- **KoeEngine** : メインスレッド API。`VoiceBank` の上に AudioWorklet を組み合わせた連接合成エンジン。
|
|
39
|
+
- **Worldline** : OpenUtau の worldline WASM を使った高品質ノート合成。PCM を入力して Float32 PCM を返す純粋な合成器。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 使い方
|
|
44
|
+
|
|
45
|
+
### 1. 音源の変換 (oto.ini → .koe)
|
|
46
|
+
|
|
47
|
+
CLIコマンド `koe-convert` で UTAU 音源を `.koe` アーカイブに変換する。
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx koe-convert <音源フォルダ> -o voice.koe
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
あるいはコードから変換する:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { parseOto, parseWav, toMono, resample, normalizePcm, pack, packKoe, parseFrqAverageF0, frqFileName, pitchFromAliasSuffix } from "@onjmin/koe";
|
|
57
|
+
|
|
58
|
+
// oto.ini をパース
|
|
59
|
+
const otoText = await fs.readFile("voice/oto.ini", "utf8");
|
|
60
|
+
const entries = parseOto(otoText);
|
|
61
|
+
|
|
62
|
+
// 各エントリを PackInput に変換
|
|
63
|
+
const inputs = await Promise.all(entries.map(async (oto) => {
|
|
64
|
+
const wavBuf = await fs.readFile(`voice/${oto.file}`);
|
|
65
|
+
const wav = parseWav(wavBuf.buffer);
|
|
66
|
+
const mono = toMono(wav);
|
|
67
|
+
const resampled = resample(mono, wav.sampleRate, 48000);
|
|
68
|
+
const pcm = normalizePcm(resampled); // Int16Array
|
|
69
|
+
|
|
70
|
+
// .frq ファイルからピッチ取得 (任意)
|
|
71
|
+
let recordedPitch = pitchFromAliasSuffix(oto.alias); // エイリアス末尾から推定
|
|
72
|
+
if (!recordedPitch) {
|
|
73
|
+
try {
|
|
74
|
+
const frqBuf = await fs.readFile(`voice/${frqFileName(oto.file)}`);
|
|
75
|
+
recordedPitch = parseFrqAverageF0(frqBuf.buffer);
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { oto, pcm, recordedPitch };
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
// 変換してパック
|
|
83
|
+
const { manifest, bin } = pack(inputs, 220 /* referencePitch Hz */);
|
|
84
|
+
const blob = packKoe(manifest, [bin]);
|
|
85
|
+
|
|
86
|
+
// blob を voice.koe として保存 (Node.js)
|
|
87
|
+
const buf = Buffer.from(await blob.arrayBuffer());
|
|
88
|
+
await fs.writeFile("voice.koe", buf);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### 2. KoeEngine — リアルタイム再生 (ブラウザ)
|
|
94
|
+
|
|
95
|
+
AudioWorklet を使った連接合成エンジン。`koe-worklet.js` を同じオリジンから配信する必要がある。
|
|
96
|
+
|
|
97
|
+
GitHub Pages にホストされているファイルをそのまま使える:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { KoeEngine } from "@onjmin/koe";
|
|
101
|
+
|
|
102
|
+
const engine = new KoeEngine({
|
|
103
|
+
workletUrl: "https://onjmin.github.io/koe/demo/koe-worklet.js",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// .koe ファイルをロード (Blob でも URL でも可)
|
|
107
|
+
await engine.load("/voice.koe");
|
|
108
|
+
|
|
109
|
+
// ノートシーケンスを再生
|
|
110
|
+
await engine.play([
|
|
111
|
+
{ phoneme: "a", pitch: 440, duration: 48000 }, // 1秒 / A4
|
|
112
|
+
{ phoneme: "i", pitch: 494, duration: 24000 }, // 0.5秒 / B4
|
|
113
|
+
{ phoneme: "u", pitch: 392, duration: 48000 }, // 1秒 / G4
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
// 停止
|
|
117
|
+
engine.stop();
|
|
118
|
+
|
|
119
|
+
// ブラウザの autoplay ポリシーで停止した場合
|
|
120
|
+
await engine.resume();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**NoteEvent の型:**
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
interface NoteEvent {
|
|
127
|
+
phoneme: string; // 音素エイリアス (oto.ini の alias)
|
|
128
|
+
pitch: number; // 出力ピッチ (Hz)
|
|
129
|
+
duration: number; // 出力長 (サンプル数 @ 48kHz)
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### 3. VoiceBank — 音素 PCM の直接取得
|
|
136
|
+
|
|
137
|
+
AudioContext 不要。WORLD ボコーダや独自の合成処理に PCM を渡したい場合に使う。
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { VoiceBank } from "@onjmin/koe";
|
|
141
|
+
|
|
142
|
+
// URL からロード (HTTP Range リクエストを使用)
|
|
143
|
+
const bank = await VoiceBank.load("/voice.koe");
|
|
144
|
+
|
|
145
|
+
// Blob からロード
|
|
146
|
+
const blob = await fetch("/voice.koe").then(r => r.blob());
|
|
147
|
+
const bank2 = await VoiceBank.load(blob);
|
|
148
|
+
|
|
149
|
+
// マニフェスト参照
|
|
150
|
+
console.log(bank.manifest.phonemes); // 全音素のオフセット・ピッチ情報
|
|
151
|
+
|
|
152
|
+
// Float64 PCM 取得 ([-1, 1] 正規化済み)
|
|
153
|
+
const pcm = await bank.getPcm("a");
|
|
154
|
+
|
|
155
|
+
// Int16 PCM バイト列を取得 (AudioWorklet への転送用)
|
|
156
|
+
const buf = await bank.readPcmBytes("a");
|
|
157
|
+
|
|
158
|
+
// 音素の存在確認
|
|
159
|
+
bank.has("a"); // boolean
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### 4. Worldline — 高品質ノート合成 (WORLD ボコーダ)
|
|
165
|
+
|
|
166
|
+
OpenUtau の worldline WASM で F0 分析・再合成を行う。`worldline.js` と `worldline.wasm` を配信する必要がある。
|
|
167
|
+
|
|
168
|
+
GitHub Pages にホストされているファイルをそのまま使える:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { VoiceBank, Worldline, leadInFromEntry } from "@onjmin/koe";
|
|
172
|
+
|
|
173
|
+
const bank = await VoiceBank.load("/voice.koe");
|
|
174
|
+
const wl = await Worldline.load({
|
|
175
|
+
scriptUrl: "https://onjmin.github.io/koe/demo/world/worldline.js",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const alias = "a";
|
|
179
|
+
const entry = bank.manifest.phonemes[alias];
|
|
180
|
+
const pcm = await bank.getPcm(alias);
|
|
181
|
+
|
|
182
|
+
// ノートをレンダリング → Float32 PCM @ 48kHz
|
|
183
|
+
const audio = wl.renderNote({
|
|
184
|
+
pcm,
|
|
185
|
+
pitch: 440, // Hz
|
|
186
|
+
durationMs: 500, // 母音部分の長さ (ms)
|
|
187
|
+
...leadInFromEntry(entry), // preMs / consonantMs を entry から自動計算
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (audio) {
|
|
191
|
+
// audio: Float32Array (レイアウト: [子音 ≈ preMs][母音 ≈ durationMs])
|
|
192
|
+
const ctx = new AudioContext({ sampleRate: 48000 });
|
|
193
|
+
const buf = ctx.createBuffer(1, audio.length, 48000);
|
|
194
|
+
buf.copyToChannel(audio, 0);
|
|
195
|
+
const src = ctx.createBufferSource();
|
|
196
|
+
src.buffer = buf;
|
|
197
|
+
src.connect(ctx.destination);
|
|
198
|
+
src.start();
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### 5. .koe アーカイブ形式
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
[4B] magic 'KOE\0' (big-endian)
|
|
208
|
+
[4B] JSON 長 (little-endian)
|
|
209
|
+
[N ] manifest JSON (UTF-8)
|
|
210
|
+
[M ] PCM バイナリ (Int16 / 48kHz / mono)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
パース・生成ユーティリティ:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { packKoe, parseKoeHeader, pcmBase } from "@onjmin/koe";
|
|
217
|
+
|
|
218
|
+
// 生成
|
|
219
|
+
const blob = packKoe(manifest, [pcmArrayBuffer]);
|
|
220
|
+
|
|
221
|
+
// ヘッダ解析
|
|
222
|
+
const headerBuf = await blob.slice(0, 8).arrayBuffer();
|
|
223
|
+
const { jsonLength } = parseKoeHeader(headerBuf);
|
|
224
|
+
const pcmOffset = pcmBase(jsonLength); // PCM データの開始バイト位置
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## API リファレンス
|
|
230
|
+
|
|
231
|
+
| エクスポート | 説明 |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `KoeEngine` | AudioWorklet ベースの連接合成エンジン |
|
|
234
|
+
| `VoiceBank` | .koe から音素 PCM をオンデマンド取得 |
|
|
235
|
+
| `Worldline` | WORLD ボコーダによる高品質ノート合成 |
|
|
236
|
+
| `parseOto` | oto.ini テキストをパース |
|
|
237
|
+
| `parseWav` | WAV バイナリをパース |
|
|
238
|
+
| `toMono` | ステレオ → モノラル変換 |
|
|
239
|
+
| `resample` | サンプルレート変換 |
|
|
240
|
+
| `normalizePcm` | Float → Int16 正規化 |
|
|
241
|
+
| `pack` | 音素リスト → manifest + PCM バイナリ |
|
|
242
|
+
| `trimToOto` | WAV を oto リージョンにトリミング |
|
|
243
|
+
| `packKoe` | manifest + PCM → .koe Blob |
|
|
244
|
+
| `parseKoeHeader` | .koe ヘッダ解析 |
|
|
245
|
+
| `pcmBase` | JSON長 → PCM 開始バイト位置 |
|
|
246
|
+
| `detectF0` | PCM からピッチ自動検出 |
|
|
247
|
+
| `noteNameToHz` | 音名 → Hz 変換 (例: `"A4"` → `440`) |
|
|
248
|
+
| `pitchFromAliasSuffix` | エイリアス末尾の音名からピッチ推定 |
|
|
249
|
+
| `parseFrqAverageF0` | .frq ファイルから平均 F0 取得 |
|
|
250
|
+
| `frqFileName` | WAV ファイル名 → .frq ファイル名 |
|
|
251
|
+
| `leadInFromEntry` | PhonemeEntry → preMs / consonantMs 変換 |
|
|
252
|
+
| `samplesToMs` | サンプル数 → ミリ秒変換 |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## ライセンス
|
|
257
|
+
|
|
258
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -155,6 +155,7 @@ type WorldlineFactory = (opts?: {
|
|
|
155
155
|
}) => Promise<WorldlineWasm>;
|
|
156
156
|
declare global {
|
|
157
157
|
var WorldlineModule: WorldlineFactory | undefined;
|
|
158
|
+
var importScripts: ((...urls: string[]) => void) | undefined;
|
|
158
159
|
}
|
|
159
160
|
interface WorldlineLoadOptions {
|
|
160
161
|
/**
|
|
@@ -217,7 +218,13 @@ declare class Worldline {
|
|
|
217
218
|
private wasm;
|
|
218
219
|
readonly sampleRate = 48000;
|
|
219
220
|
private constructor();
|
|
220
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* Load + instantiate the worldline WASM module (deduped per scriptUrl).
|
|
223
|
+
*
|
|
224
|
+
* Works on the main thread (loads via `<script>`) and inside a classic Web
|
|
225
|
+
* Worker (loads via `importScripts`), so the heavy synthesis can run
|
|
226
|
+
* off-thread. The matching `worldline.wasm` is fetched next to scriptUrl.
|
|
227
|
+
*/
|
|
221
228
|
static load(options: WorldlineLoadOptions): Promise<Worldline>;
|
|
222
229
|
/**
|
|
223
230
|
* Render one note to Float32 PCM at 48 kHz.
|
|
@@ -290,7 +297,7 @@ interface TrimmedPhoneme {
|
|
|
290
297
|
/** PCM trimmed to the oto region [offset, cutoff] */
|
|
291
298
|
pcm: Int16Array;
|
|
292
299
|
/** Manifest params relative to the trimmed start (sample 0 = oto offset) */
|
|
293
|
-
entry: Omit<PhonemeEntry,
|
|
300
|
+
entry: Omit<PhonemeEntry, "offset">;
|
|
294
301
|
}
|
|
295
302
|
/**
|
|
296
303
|
* Cut the full WAV PCM down to its usable oto region and recompute parameters
|
package/dist/index.js
CHANGED
|
@@ -49,8 +49,11 @@ var RangeVoiceSource = class {
|
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
51
|
async function rangeFetch(url, start, length) {
|
|
52
|
-
const res = await fetch(url, {
|
|
53
|
-
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
headers: { Range: `bytes=${start}-${start + length - 1}` }
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok && res.status !== 206)
|
|
56
|
+
throw new Error(`.koe fetch failed: ${res.status}`);
|
|
54
57
|
return res.arrayBuffer();
|
|
55
58
|
}
|
|
56
59
|
var VoiceBank = class _VoiceBank {
|
|
@@ -70,13 +73,19 @@ var VoiceBank = class _VoiceBank {
|
|
|
70
73
|
const { jsonLength: jsonLength2 } = parseKoeHeader(header2);
|
|
71
74
|
const json2 = await rangeFetch(koe, 8, jsonLength2);
|
|
72
75
|
const manifest2 = JSON.parse(new TextDecoder().decode(json2));
|
|
73
|
-
return new _VoiceBank(
|
|
76
|
+
return new _VoiceBank(
|
|
77
|
+
manifest2,
|
|
78
|
+
new RangeVoiceSource(koe, pcmBase(jsonLength2))
|
|
79
|
+
);
|
|
74
80
|
}
|
|
75
81
|
const header = await koe.slice(0, 8).arrayBuffer();
|
|
76
82
|
const { jsonLength } = parseKoeHeader(header);
|
|
77
83
|
const json = await koe.slice(8, 8 + jsonLength).arrayBuffer();
|
|
78
84
|
const manifest = JSON.parse(new TextDecoder().decode(json));
|
|
79
|
-
return new _VoiceBank(
|
|
85
|
+
return new _VoiceBank(
|
|
86
|
+
manifest,
|
|
87
|
+
new BlobVoiceSource(koe, pcmBase(jsonLength))
|
|
88
|
+
);
|
|
80
89
|
}
|
|
81
90
|
/** True if the bank contains a phoneme under this alias. */
|
|
82
91
|
has(phoneme) {
|
|
@@ -158,7 +167,9 @@ var KoeEngine = class {
|
|
|
158
167
|
if (!this.bank || !this.node) return Promise.resolve();
|
|
159
168
|
const load = this.bank.readPcmBytes(name).then((buf) => {
|
|
160
169
|
if (!buf) return;
|
|
161
|
-
this.node.port.postMessage({ type: "phoneme", name, buffer: buf }, [
|
|
170
|
+
this.node.port.postMessage({ type: "phoneme", name, buffer: buf }, [
|
|
171
|
+
buf
|
|
172
|
+
]);
|
|
162
173
|
this.delivered.add(name);
|
|
163
174
|
this.pending.delete(name);
|
|
164
175
|
});
|
|
@@ -198,12 +209,17 @@ var SYNTH_REQ_SIZE = 120;
|
|
|
198
209
|
var WL_FRAME_MS = 10;
|
|
199
210
|
var samplesToMs = (samples) => samples / WORLDLINE_SAMPLE_RATE * 1e3;
|
|
200
211
|
function leadInFromEntry(entry) {
|
|
201
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
preMs: samplesToMs(entry.pre || 0),
|
|
214
|
+
consonantMs: samplesToMs(entry.consonant || 0)
|
|
215
|
+
};
|
|
202
216
|
}
|
|
203
217
|
var moduleCache = /* @__PURE__ */ new Map();
|
|
204
218
|
function injectScript(src) {
|
|
205
219
|
return new Promise((resolve, reject) => {
|
|
206
|
-
const existing = document.querySelector(
|
|
220
|
+
const existing = document.querySelector(
|
|
221
|
+
`script[data-koe-worldline="${src}"]`
|
|
222
|
+
);
|
|
207
223
|
if (existing) {
|
|
208
224
|
resolve();
|
|
209
225
|
return;
|
|
@@ -219,17 +235,30 @@ function injectScript(src) {
|
|
|
219
235
|
function loadWasm(scriptUrl) {
|
|
220
236
|
const cached = moduleCache.get(scriptUrl);
|
|
221
237
|
if (cached) return cached;
|
|
222
|
-
if (typeof document === "undefined") {
|
|
223
|
-
return Promise.reject(
|
|
224
|
-
new Error("Worldline.load requires a DOM (browser) environment to load worldline.js")
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
238
|
const baseUrl = scriptUrl.slice(0, scriptUrl.lastIndexOf("/") + 1);
|
|
228
|
-
const
|
|
239
|
+
const instantiate = () => {
|
|
229
240
|
const factory = globalThis.WorldlineModule;
|
|
230
|
-
if (!factory)
|
|
241
|
+
if (!factory)
|
|
242
|
+
throw new Error(
|
|
243
|
+
"worldline: WorldlineModule global was not defined by the script"
|
|
244
|
+
);
|
|
231
245
|
return factory({ locateFile: (f) => baseUrl + f });
|
|
232
|
-
}
|
|
246
|
+
};
|
|
247
|
+
let promise;
|
|
248
|
+
if (typeof document !== "undefined") {
|
|
249
|
+
promise = injectScript(scriptUrl).then(instantiate);
|
|
250
|
+
} else if (typeof globalThis.importScripts === "function") {
|
|
251
|
+
promise = Promise.resolve().then(() => {
|
|
252
|
+
globalThis.importScripts(scriptUrl);
|
|
253
|
+
return instantiate();
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
return Promise.reject(
|
|
257
|
+
new Error(
|
|
258
|
+
"Worldline.load requires a DOM or a classic Web Worker (importScripts) to load worldline.js"
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
233
262
|
moduleCache.set(scriptUrl, promise);
|
|
234
263
|
return promise;
|
|
235
264
|
}
|
|
@@ -239,7 +268,13 @@ var Worldline = class _Worldline {
|
|
|
239
268
|
}
|
|
240
269
|
wasm;
|
|
241
270
|
sampleRate = WORLDLINE_SAMPLE_RATE;
|
|
242
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* Load + instantiate the worldline WASM module (deduped per scriptUrl).
|
|
273
|
+
*
|
|
274
|
+
* Works on the main thread (loads via `<script>`) and inside a classic Web
|
|
275
|
+
* Worker (loads via `importScripts`), so the heavy synthesis can run
|
|
276
|
+
* off-thread. The matching `worldline.wasm` is fetched next to scriptUrl.
|
|
277
|
+
*/
|
|
243
278
|
static async load(options) {
|
|
244
279
|
return new _Worldline(await loadWasm(options.scriptUrl));
|
|
245
280
|
}
|
|
@@ -331,7 +366,16 @@ var Worldline = class _Worldline {
|
|
|
331
366
|
WL.HEAPF64.set(tArr, tPtr >> 3);
|
|
332
367
|
WL.HEAPF64.set(bArr, bPtr >> 3);
|
|
333
368
|
WL.HEAPF64.set(vArr, vPtr >> 3);
|
|
334
|
-
WL._PhraseSynthSetCurves(
|
|
369
|
+
WL._PhraseSynthSetCurves(
|
|
370
|
+
ps,
|
|
371
|
+
f0Ptr,
|
|
372
|
+
gPtr,
|
|
373
|
+
tPtr,
|
|
374
|
+
bPtr,
|
|
375
|
+
vPtr,
|
|
376
|
+
nFrames,
|
|
377
|
+
WL_FRAME_MS
|
|
378
|
+
);
|
|
335
379
|
WL._free(f0Ptr);
|
|
336
380
|
WL._free(gPtr);
|
|
337
381
|
WL._free(tPtr);
|
|
@@ -435,7 +479,8 @@ function toMono(wav) {
|
|
|
435
479
|
const out = new Float32Array(len);
|
|
436
480
|
for (let i = 0; i < len; i++) {
|
|
437
481
|
let sum = 0;
|
|
438
|
-
for (let c = 0; c < wav.channels; c++)
|
|
482
|
+
for (let c = 0; c < wav.channels; c++)
|
|
483
|
+
sum += wav.samples[i * wav.channels + c];
|
|
439
484
|
out[i] = sum / wav.channels;
|
|
440
485
|
}
|
|
441
486
|
return { sampleRate: wav.sampleRate, channels: 1, samples: out };
|
|
@@ -478,7 +523,15 @@ function readFourCC(view, pos) {
|
|
|
478
523
|
|
|
479
524
|
// src/converter/pitch.ts
|
|
480
525
|
var SAMPLE_RATE = 48e3;
|
|
481
|
-
var NAME_SEMITONE = {
|
|
526
|
+
var NAME_SEMITONE = {
|
|
527
|
+
c: 0,
|
|
528
|
+
d: 2,
|
|
529
|
+
e: 4,
|
|
530
|
+
f: 5,
|
|
531
|
+
g: 7,
|
|
532
|
+
a: 9,
|
|
533
|
+
b: 11
|
|
534
|
+
};
|
|
482
535
|
function noteNameToHz(name) {
|
|
483
536
|
const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec(name);
|
|
484
537
|
if (!m) return null;
|
|
@@ -556,7 +609,11 @@ function trimToOto(pcm, oto, recordedPitch = 0) {
|
|
|
556
609
|
const pre = clamp(msToSamples(oto.pre), 0, length);
|
|
557
610
|
const overlap = clamp(msToSamples(oto.overlap), 0, length);
|
|
558
611
|
const consonant = clamp(msToSamples(oto.consonant), 0, length);
|
|
559
|
-
const pitch = recordedPitch > 0 ? recordedPitch : detectF0(
|
|
612
|
+
const pitch = recordedPitch > 0 ? recordedPitch : detectF0(
|
|
613
|
+
slice,
|
|
614
|
+
Math.min(Math.max(pre, consonant), Math.max(0, length - 1)),
|
|
615
|
+
length
|
|
616
|
+
);
|
|
560
617
|
return {
|
|
561
618
|
pcm: slice,
|
|
562
619
|
entry: { length, pre, overlap, consonant, pitch }
|
|
@@ -577,7 +634,10 @@ function pack(inputs, referencePitch = 220) {
|
|
|
577
634
|
const view = new Uint8Array(bin);
|
|
578
635
|
let pos = 0;
|
|
579
636
|
for (const chunk of chunks) {
|
|
580
|
-
view.set(
|
|
637
|
+
view.set(
|
|
638
|
+
new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
|
|
639
|
+
pos
|
|
640
|
+
);
|
|
581
641
|
pos += chunk.byteLength;
|
|
582
642
|
}
|
|
583
643
|
const manifest = {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/koe.ts","../src/engine/voice-bank.ts","../src/engine/index.ts","../src/engine/worldline.ts","../src/converter/parse-oto.ts","../src/converter/wav.ts","../src/converter/pitch.ts","../src/converter/pack.ts","../src/converter/frq.ts"],"sourcesContent":["import type { Manifest } from './types.js';\n\n/** Magic number 'KOE\\0' (stored big-endian as 0x4B 0x4F 0x45 0x00). */\nconst MAGIC = 0x4b4f4500;\n\n/**\n * Koe Archive Format (.koe)\n * [4B] magic 'KOE\\0' (big-endian)\n * [4B] JSON length (little-endian)\n * [N ] manifest JSON (UTF-8)\n * [M ] raw PCM (Int16 / 48kHz / mono); phoneme offsets are relative to here\n */\nexport function packKoe(manifest: Manifest, pcmParts: BlobPart[]): Blob {\n const json = new TextEncoder().encode(JSON.stringify(manifest));\n const header = new ArrayBuffer(8);\n const view = new DataView(header);\n view.setUint32(0, MAGIC, false); // magic, big-endian\n view.setUint32(4, json.byteLength, true); // json length, little-endian\n return new Blob([header, json, ...pcmParts]);\n}\n\n/** Read the 8-byte header → JSON length. Throws on bad magic. */\nexport function parseKoeHeader(headerBytes: ArrayBuffer): { jsonLength: number } {\n const view = new DataView(headerBytes);\n if (view.byteLength < 8 || view.getUint32(0, false) !== MAGIC) {\n throw new Error('Not a .koe file (bad magic)');\n }\n return { jsonLength: view.getUint32(4, true) };\n}\n\n/** Byte offset where PCM data begins, given the JSON length. */\nexport const pcmBase = (jsonLength: number): number => 8 + jsonLength;\n","import type { Manifest } from '../types.js';\nimport { parseKoeHeader, pcmBase } from '../koe.js';\n\n/**\n * Supplies raw bytes from the PCM section of a .koe archive on demand.\n * - BlobVoiceSource: slices an in-memory Blob / File (no full-buffer copy)\n * - RangeVoiceSource: HTTP Range requests against a URL (mobile-friendly)\n * `base` is the byte offset where PCM data starts (8 + jsonLength).\n */\ninterface VoiceSource {\n readBytes(offset: number, length: number): Promise<ArrayBuffer>;\n}\n\nclass BlobVoiceSource implements VoiceSource {\n constructor(\n private blob: Blob,\n private base: number,\n ) {}\n readBytes(offset: number, length: number): Promise<ArrayBuffer> {\n const start = this.base + offset;\n return this.blob.slice(start, start + length).arrayBuffer();\n }\n}\n\nclass RangeVoiceSource implements VoiceSource {\n constructor(\n private url: string,\n private base: number,\n ) {}\n async readBytes(offset: number, length: number): Promise<ArrayBuffer> {\n const start = this.base + offset;\n const res = await fetch(this.url, {\n headers: { Range: `bytes=${start}-${start + length - 1}` },\n });\n if (!res.ok && res.status !== 206) {\n throw new Error(`.koe range request failed: ${res.status}`);\n }\n return res.arrayBuffer();\n }\n}\n\nasync function rangeFetch(url: string, start: number, length: number): Promise<ArrayBuffer> {\n const res = await fetch(url, { headers: { Range: `bytes=${start}-${start + length - 1}` } });\n if (!res.ok && res.status !== 206) throw new Error(`.koe fetch failed: ${res.status}`);\n return res.arrayBuffer();\n}\n\n/**\n * Read-only access to a .koe voice bank: its manifest plus per-phoneme PCM,\n * fetched on demand (Blob slice or HTTP Range). The full bank is never held in\n * memory.\n *\n * Pure data — no AudioContext, no AudioWorklet, no DOM. Use this when you only\n * need the source samples (e.g. to feed the {@link Worldline} renderer or any\n * other vocoder). {@link KoeEngine} builds its concatenative playback on top of\n * this same class.\n *\n * const bank = await VoiceBank.load(koeBlobOrUrl);\n * const pcm = await bank.getPcm('a'); // Float64 [-1, 1]\n */\nexport class VoiceBank {\n private constructor(\n /** The voice bank manifest (sample rate, reference pitch, phoneme table). */\n readonly manifest: Manifest,\n private source: VoiceSource,\n ) {}\n\n /**\n * Parse a .koe archive header + manifest and bind a lazy PCM source.\n * @param koe a Blob/File of the .koe archive, or a URL (served with Range support)\n */\n static async load(koe: Blob | string): Promise<VoiceBank> {\n if (typeof koe === 'string') {\n const header = await rangeFetch(koe, 0, 8);\n const { jsonLength } = parseKoeHeader(header);\n const json = await rangeFetch(koe, 8, jsonLength);\n const manifest = JSON.parse(new TextDecoder().decode(json)) as Manifest;\n return new VoiceBank(manifest, new RangeVoiceSource(koe, pcmBase(jsonLength)));\n }\n const header = await koe.slice(0, 8).arrayBuffer();\n const { jsonLength } = parseKoeHeader(header);\n const json = await koe.slice(8, 8 + jsonLength).arrayBuffer();\n const manifest = JSON.parse(new TextDecoder().decode(json)) as Manifest;\n return new VoiceBank(manifest, new BlobVoiceSource(koe, pcmBase(jsonLength)));\n }\n\n /** True if the bank contains a phoneme under this alias. */\n has(phoneme: string): boolean {\n return this.manifest.phonemes[phoneme] !== undefined;\n }\n\n /**\n * Raw Int16 PCM bytes (48 kHz / mono) for a phoneme, or null if unknown.\n * The returned ArrayBuffer is freshly allocated and safe to transfer to a\n * worker / AudioWorklet.\n */\n async readPcmBytes(phoneme: string): Promise<ArrayBuffer | null> {\n const entry = this.manifest.phonemes[phoneme];\n if (!entry) return null;\n return this.source.readBytes(entry.offset, entry.length * 2); // Int16 = 2 bytes/sample\n }\n\n /**\n * A phoneme's PCM as a Float64Array normalised to [-1, 1], or null if unknown.\n * Intended for external analysis / resynthesis such as the WORLD vocoder.\n */\n async getPcm(phoneme: string): Promise<Float64Array | null> {\n const buf = await this.readPcmBytes(phoneme);\n if (!buf) return null;\n const int16 = new Int16Array(buf);\n const f64 = new Float64Array(int16.length);\n for (let i = 0; i < int16.length; i++) f64[i] = int16[i] / 32768;\n return f64;\n }\n}\n","import type { Manifest, NoteEvent } from '../types.js';\nimport { VoiceBank } from './voice-bank.js';\n\nexport type { NoteEvent };\n\nexport interface KoeEngineOptions {\n /** URL to koe-worklet.js. Defaults to './koe-worklet.js'. */\n workletUrl?: string;\n}\n\n/**\n * Main-thread API for the koe concatenative synthesis engine.\n *\n * Loads a single .koe archive. The full voice bank is never held in memory:\n * phonemes are fetched on demand (via {@link VoiceBank}) and cached in the\n * audio thread.\n *\n * const engine = new KoeEngine();\n * await engine.load(koeBlobOrUrl);\n * await engine.play([{ phoneme: 'a', pitch: 440, duration: 48000 }]);\n *\n * For analysis / resynthesis (e.g. {@link Worldline}) you usually only need the\n * raw samples — use {@link VoiceBank} directly instead, which needs no\n * AudioContext or worklet.\n */\nexport class KoeEngine {\n private ctx: AudioContext;\n private workletUrl: string;\n private node: AudioWorkletNode | null = null;\n private bank: VoiceBank | null = null;\n private delivered = new Set<string>();\n private pending = new Map<string, Promise<void>>();\n\n constructor(options: KoeEngineOptions = {}) {\n this.ctx = new AudioContext({ sampleRate: 48000 });\n this.workletUrl = options.workletUrl ?? './koe-worklet.js';\n }\n\n get audioContext(): AudioContext {\n return this.ctx;\n }\n\n get manifest(): Manifest | null {\n return this.bank?.manifest ?? null;\n }\n\n /** The underlying voice bank (manifest + on-demand PCM), or null before load(). */\n get voiceBank(): VoiceBank | null {\n return this.bank;\n }\n\n /**\n * Register the worklet and bind a .koe voice bank.\n * @param koe a Blob/File of the .koe archive, or a URL (served with Range support)\n */\n async load(koe: Blob | string): Promise<void> {\n await this.ctx.audioWorklet.addModule(this.workletUrl);\n\n this.bank = await VoiceBank.load(koe);\n this.delivered.clear();\n this.pending.clear();\n\n this.node = new AudioWorkletNode(this.ctx, 'koe-processor', {\n numberOfInputs: 0,\n numberOfOutputs: 1,\n outputChannelCount: [1],\n });\n this.node.port.postMessage({ type: 'init', manifest: this.bank.manifest });\n this.node.connect(this.ctx.destination);\n\n console.log(\n '[koe] ready —',\n Object.keys(this.bank.manifest.phonemes).length,\n 'phonemes (on-demand)',\n );\n }\n\n /** Fetch one phoneme's PCM and deliver it to the worklet (deduped, cached). */\n private ensurePhoneme(name: string): Promise<void> {\n if (this.delivered.has(name)) return Promise.resolve();\n const existing = this.pending.get(name);\n if (existing) return existing;\n if (!this.bank || !this.node) return Promise.resolve();\n\n const load = this.bank.readPcmBytes(name).then(buf => {\n if (!buf) return;\n this.node!.port.postMessage({ type: 'phoneme', name, buffer: buf }, [buf]);\n this.delivered.add(name);\n this.pending.delete(name);\n });\n\n this.pending.set(name, load);\n return load;\n }\n\n /** Stop current playback, preload the phonemes for `notes`, then queue them. */\n async play(notes: NoteEvent[]): Promise<void> {\n if (!this.node) throw new Error('KoeEngine: call load() before play()');\n this.node.port.postMessage({ type: 'stop' });\n const names = [...new Set(notes.map(n => n.phoneme))].filter(Boolean);\n await Promise.all(names.map(n => this.ensurePhoneme(n)));\n this.node.port.postMessage({ type: 'play', notes });\n }\n\n /** Stop playback and clear the queue. */\n stop(): void {\n this.node?.port.postMessage({ type: 'stop' });\n }\n\n /** Resume the AudioContext if suspended (e.g. after autoplay block). */\n async resume(): Promise<void> {\n if (this.ctx.state === 'suspended') await this.ctx.resume();\n }\n\n /**\n * Read a phoneme's raw PCM and return it as a Float64Array normalised to\n * [-1, 1]. Convenience that forwards to the underlying {@link VoiceBank}.\n * Intended for external analysis such as the WORLD vocoder.\n */\n async getPcm(phoneme: string): Promise<Float64Array | null> {\n return this.bank?.getPcm(phoneme) ?? null;\n }\n}\n","import type { PhonemeEntry } from '../types.js';\n\n/** Output sample rate of the worldline synthesizer. */\nexport const WORLDLINE_SAMPLE_RATE = 48000;\n\n/**\n * WORLD needs roughly 85 ms of audio for stable F0 analysis. Phonemes with\n * fewer samples than this are rejected (renderNote returns null).\n */\nexport const MIN_WORLDLINE_SAMPLES = 4096;\n\n/** SynthRequest struct layout — 120 bytes, WASM32. */\nconst SYNTH_REQ_SIZE = 120;\n/** Frame period hardcoded in worldline's phrase_synth.cpp. */\nconst WL_FRAME_MS = 10;\n\n/**\n * The subset of the Emscripten module surface that we call. worldline.js is an\n * MODULARIZE=1 / EXPORT_NAME=WorldlineModule build of OpenUtau's worldline.\n */\ninterface WorldlineWasm {\n _PhraseSynthNew(): number;\n _PhraseSynthDelete(ps: number): void;\n _PhraseSynthAddRequest(\n ps: number,\n req: number,\n posMs: number,\n skipMs: number,\n lengthMs: number,\n fadeInMs: number,\n fadeOutMs: number,\n flag: number,\n ): void;\n _PhraseSynthSetCurves(\n ps: number,\n f0: number,\n gender: number,\n tension: number,\n breathiness: number,\n voicing: number,\n length: number,\n frameMs: number,\n ): void;\n _PhraseSynthSynth(ps: number, yPtrPtr: number, flag: number): number;\n _malloc(size: number): number;\n _free(ptr: number): void;\n setValue(ptr: number, value: number, type: string): void;\n getValue(ptr: number, type: string): number;\n HEAPF32: Float32Array;\n HEAPF64: Float64Array;\n}\n\ntype WorldlineFactory = (opts?: {\n locateFile?: (path: string) => string;\n}) => Promise<WorldlineWasm>;\n\ndeclare global {\n // Injected by worldline.js when loaded as a classic <script>.\n // eslint-disable-next-line no-var\n var WorldlineModule: WorldlineFactory | undefined;\n}\n\nexport interface WorldlineLoadOptions {\n /**\n * URL of `worldline.js` (the Emscripten loader). The matching\n * `worldline.wasm` must sit next to it — it is resolved relative to this URL.\n *\n * When consuming koe from npm + a CDN this is typically e.g.\n * `https://cdn.jsdelivr.net/npm/@onjmin/koe/dist/world/worldline.js`, or your\n * own hosted copy of `dist/world/`.\n */\n scriptUrl: string;\n}\n\nexport interface RenderNoteParams {\n /**\n * Source phoneme PCM normalised to [-1, 1] (e.g. from\n * `VoiceBank.getPcm()` / `KoeEngine.getPcm()`).\n */\n pcm: Float64Array;\n /** Target output pitch in Hz. */\n pitch: number;\n /** Sustain / vowel duration in ms (the lead-in below is rendered on top). */\n durationMs: number;\n /** Preutterance / lead-in in ms — convert from {@link PhonemeEntry.pre}. */\n preMs: number;\n /** Consonant length in ms — convert from {@link PhonemeEntry.consonant}. */\n consonantMs: number;\n /** Reference tempo in BPM for worldline's internal timing. Default 120. */\n tempo?: number;\n}\n\n/** Convert a sample count at 48 kHz to milliseconds. */\nexport const samplesToMs = (samples: number): number => (samples / WORLDLINE_SAMPLE_RATE) * 1000;\n\n/**\n * Derive {@link RenderNoteParams} lead-in / consonant fields from a manifest\n * entry, so callers don't repeat the sample→ms conversion.\n *\n * const params = { pcm, pitch, durationMs, ...leadInFromEntry(entry) };\n */\nexport function leadInFromEntry(entry: PhonemeEntry): { preMs: number; consonantMs: number } {\n return { preMs: samplesToMs(entry.pre || 0), consonantMs: samplesToMs(entry.consonant || 0) };\n}\n\n// Cache module factories by script URL so repeated loads share one WASM instance.\nconst moduleCache = new Map<string, Promise<WorldlineWasm>>();\n\nfunction injectScript(src: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const existing = document.querySelector<HTMLScriptElement>(`script[data-koe-worldline=\"${src}\"]`);\n if (existing) {\n resolve();\n return;\n }\n const s = document.createElement('script');\n s.src = src;\n s.dataset.koeWorldline = src;\n s.onload = () => resolve();\n s.onerror = () => reject(new Error(`worldline: failed to load ${src}`));\n document.head.appendChild(s);\n });\n}\n\nfunction loadWasm(scriptUrl: string): Promise<WorldlineWasm> {\n const cached = moduleCache.get(scriptUrl);\n if (cached) return cached;\n\n if (typeof document === 'undefined') {\n return Promise.reject(\n new Error('Worldline.load requires a DOM (browser) environment to load worldline.js'),\n );\n }\n\n const baseUrl = scriptUrl.slice(0, scriptUrl.lastIndexOf('/') + 1);\n const promise = injectScript(scriptUrl).then(() => {\n const factory = globalThis.WorldlineModule;\n if (!factory) throw new Error('worldline: WorldlineModule global was not defined by the script');\n return factory({ locateFile: (f: string) => baseUrl + f });\n });\n moduleCache.set(scriptUrl, promise);\n return promise;\n}\n\n/**\n * High-quality WORLD-vocoder note renderer (OpenUtau's worldline via WASM).\n *\n * Pure synthesis: PCM in → PCM out. No AudioContext, no scheduling — the caller\n * owns playback (schedule the returned buffer on its own timeline). Pair it with\n * {@link VoiceBank} for the source samples:\n *\n * const bank = await VoiceBank.load(koeUrl);\n * const wl = await Worldline.load({ scriptUrl: '.../world/worldline.js' });\n *\n * const entry = bank.manifest.phonemes[alias];\n * const pcm = await bank.getPcm(alias);\n * const audio = wl.renderNote({\n * pcm, pitch: 440, durationMs: 500, ...leadInFromEntry(entry),\n * });\n * // audio: Float32 @ 48 kHz, layout [lead-in/consonant ≈ preMs][vowel ≈ durationMs]\n */\nexport class Worldline {\n readonly sampleRate = WORLDLINE_SAMPLE_RATE;\n\n private constructor(private wasm: WorldlineWasm) {}\n\n /** Load + instantiate the worldline WASM module (deduped per scriptUrl). */\n static async load(options: WorldlineLoadOptions): Promise<Worldline> {\n return new Worldline(await loadWasm(options.scriptUrl));\n }\n\n /**\n * Render one note to Float32 PCM at 48 kHz.\n *\n * The output buffer is laid out as [lead-in/consonant ≈ preMs][vowel ≈\n * durationMs], rendered from sample offset 0 (no leading silence). The vowel\n * onset (the \"beat\") sits at ≈ preMs into the buffer, so a sequencer should\n * place the buffer at `beatTime − preMs` and may trim/crossfade the lead-in.\n *\n * No internal crossfade is applied — apply fades externally.\n *\n * @returns Float32 PCM, or null when `pcm` is shorter than\n * {@link MIN_WORLDLINE_SAMPLES} (too short for stable F0 analysis).\n */\n renderNote(params: RenderNoteParams): Float32Array | null {\n const { pcm, pitch, durationMs, preMs, consonantMs, tempo = 120 } = params;\n if (!pcm || pcm.length < MIN_WORLDLINE_SAMPLES) return null;\n\n const WL = this.wasm;\n const FS = WORLDLINE_SAMPLE_RATE;\n const midiNote = Math.round(69 + 12 * Math.log2(pitch / 440));\n const posMs = 0; // no leading silence\n const reqLen = preMs + durationMs; // render lead-in/consonant + vowel\n\n // cut_off must trim at least ~2 frames off the tail. PhraseSynth::AddRequest\n // computes the trim frame count as ceil(in_length_ms)/frame_ms, but\n // PyinEstimator yields floor(N/nhop)−1 frames — one fewer. With cut_off=0 the\n // requested frame count exceeds f0_.size() and Model::Trim's f0_.erase() runs\n // past end(), causing \"memory access out of bounds\". Trimming 2 frames keeps\n // it within bounds.\n const cutMs = WL_FRAME_MS * 2;\n\n const ps = WL._PhraseSynthNew();\n if (!ps) return null;\n const reqPtr = WL._malloc(SYNTH_REQ_SIZE);\n if (!reqPtr) {\n WL._PhraseSynthDelete(ps);\n return null;\n }\n const samplePtr = WL._malloc(pcm.length * 8);\n if (!samplePtr) {\n WL._free(reqPtr);\n WL._PhraseSynthDelete(ps);\n return null;\n }\n WL.HEAPF64.set(pcm, samplePtr >> 3);\n\n const sv = (off: number, val: number, type: string) => WL.setValue(reqPtr + off, val, type);\n sv(0, FS, 'i32');\n sv(4, pcm.length, 'i32');\n sv(8, samplePtr, '*');\n sv(12, 0, 'i32');\n sv(16, 0, '*');\n sv(20, midiNote, 'i32');\n sv(24, 100.0, 'double');\n sv(32, 0.0, 'double');\n sv(40, reqLen, 'double');\n sv(48, consonantMs, 'double');\n sv(56, cutMs, 'double');\n sv(64, 100.0, 'double');\n sv(72, 0.0, 'double');\n sv(80, tempo, 'double');\n sv(88, 0, 'i32');\n sv(92, 0, '*');\n sv(96, 0, 'i32');\n sv(100, 0, 'i32');\n sv(104, 100, 'i32');\n sv(108, 0, 'i32');\n sv(112, 0, 'i32');\n sv(116, 100, 'i32');\n\n // skip/fadeIn/fadeOut all 0 — crossfade is handled externally.\n WL._PhraseSynthAddRequest(ps, reqPtr, posMs, 0.0, reqLen, 0.0, 0.0, 0);\n WL._free(samplePtr);\n WL._free(reqPtr);\n\n const totalMs = posMs + reqLen + WL_FRAME_MS * 2;\n const nFrames = Math.ceil(totalMs / WL_FRAME_MS) + 4;\n const f0Arr = new Float64Array(nFrames).fill(pitch);\n const gArr = new Float64Array(nFrames).fill(0.5);\n const tArr = new Float64Array(nFrames).fill(0.5);\n const bArr = new Float64Array(nFrames).fill(0.5);\n const vArr = new Float64Array(nFrames).fill(1.0);\n\n const f0Ptr = WL._malloc(nFrames * 8);\n const gPtr = WL._malloc(nFrames * 8);\n const tPtr = WL._malloc(nFrames * 8);\n const bPtr = WL._malloc(nFrames * 8);\n const vPtr = WL._malloc(nFrames * 8);\n if (!f0Ptr || !gPtr || !tPtr || !bPtr || !vPtr) {\n if (f0Ptr) WL._free(f0Ptr);\n if (gPtr) WL._free(gPtr);\n if (tPtr) WL._free(tPtr);\n if (bPtr) WL._free(bPtr);\n if (vPtr) WL._free(vPtr);\n WL._PhraseSynthDelete(ps);\n return null;\n }\n WL.HEAPF64.set(f0Arr, f0Ptr >> 3);\n WL.HEAPF64.set(gArr, gPtr >> 3);\n WL.HEAPF64.set(tArr, tPtr >> 3);\n WL.HEAPF64.set(bArr, bPtr >> 3);\n WL.HEAPF64.set(vArr, vPtr >> 3);\n WL._PhraseSynthSetCurves(ps, f0Ptr, gPtr, tPtr, bPtr, vPtr, nFrames, WL_FRAME_MS);\n WL._free(f0Ptr);\n WL._free(gPtr);\n WL._free(tPtr);\n WL._free(bPtr);\n WL._free(vPtr);\n\n const yPtrPtr = WL._malloc(4);\n if (!yPtrPtr) {\n WL._PhraseSynthDelete(ps);\n return null;\n }\n const outLen = WL._PhraseSynthSynth(ps, yPtrPtr, 0);\n const yPtr = WL.getValue(yPtrPtr, '*');\n const audio =\n outLen > 0 ? new Float32Array(WL.HEAPF32.buffer, yPtr, outLen).slice() : null;\n WL._free(yPtrPtr);\n WL._PhraseSynthDelete(ps);\n return audio;\n }\n}\n","export interface OtoEntry {\n /** Source WAV filename */\n wav: string;\n /** Phoneme alias */\n alias: string;\n /** Left blank — offset from WAV start (ms) */\n offset: number;\n /** Consonant portion end from offset (ms) */\n consonant: number;\n /** Right blank — negative = from WAV end, positive = from offset (ms) */\n cutoff: number;\n /** Preutterance from offset (ms) */\n pre: number;\n /** Overlap / crossfade region (ms) */\n overlap: number;\n}\n\n/**\n * Parse oto.ini content (already decoded to UTF-8 string).\n * Silently skips malformed lines.\n */\nexport function parseOto(content: string): OtoEntry[] {\n const entries: OtoEntry[] = [];\n\n for (const raw of content.split(/\\r?\\n/)) {\n const line = raw.trim();\n if (!line || line.startsWith('#')) continue;\n\n const eq = line.indexOf('=');\n if (eq === -1) continue;\n\n const wav = line.slice(0, eq).trim();\n const parts = line.slice(eq + 1).split(',');\n if (parts.length < 6) continue;\n\n const [alias, offsetStr, consonantStr, cutoffStr, preStr, overlapStr] = parts;\n // Empty alias → use filename without extension (UTAU spec default)\n const aliasStr = alias.trim() || wav.replace(/\\.[^.]+$/, '');\n const entry: OtoEntry = {\n wav,\n alias: aliasStr,\n offset: parseFloat(offsetStr) || 0,\n consonant: parseFloat(consonantStr) || 0,\n cutoff: parseFloat(cutoffStr) || 0,\n pre: parseFloat(preStr) || 0,\n overlap: parseFloat(overlapStr) || 0,\n };\n\n if (!entry.alias) continue;\n entries.push(entry);\n }\n\n return entries;\n}\n","export interface WavData {\n sampleRate: number;\n channels: number;\n /** Normalized samples in [-1, 1], interleaved if multi-channel */\n samples: Float32Array;\n}\n\n/** Parse a WAV file from an ArrayBuffer. Supports PCM 8/16/24-bit and IEEE float 32-bit. */\nexport function parseWav(buf: ArrayBuffer): WavData {\n const view = new DataView(buf);\n\n const riff = readFourCC(view, 0);\n if (riff !== 'RIFF') throw new Error(`Not a RIFF file (got \"${riff}\")`);\n\n let sampleRate = 0;\n let channels = 0;\n let bitsPerSample = 0;\n let audioFormat = 1;\n let dataOffset = 0;\n let dataLength = 0;\n\n let pos = 12;\n while (pos < view.byteLength - 8) {\n const id = readFourCC(view, pos);\n const size = view.getUint32(pos + 4, true);\n pos += 8;\n\n if (id === 'fmt ') {\n audioFormat = view.getUint16(pos, true);\n channels = view.getUint16(pos + 2, true);\n sampleRate = view.getUint32(pos + 4, true);\n bitsPerSample = view.getUint16(pos + 14, true);\n } else if (id === 'data') {\n dataOffset = pos;\n dataLength = size;\n break;\n }\n\n pos += size + (size & 1);\n }\n\n if (!dataOffset) throw new Error('WAV has no data chunk');\n if (!channels || !sampleRate) throw new Error('WAV fmt chunk missing');\n\n const bytesPerSample = bitsPerSample >> 3;\n const totalSamples = Math.floor(dataLength / bytesPerSample);\n const samples = new Float32Array(totalSamples);\n\n for (let i = 0; i < totalSamples; i++) {\n const p = dataOffset + i * bytesPerSample;\n if (audioFormat === 3) {\n samples[i] = view.getFloat32(p, true);\n } else if (bitsPerSample === 8) {\n samples[i] = (view.getUint8(p) - 128) / 128;\n } else if (bitsPerSample === 16) {\n samples[i] = view.getInt16(p, true) / 32768;\n } else if (bitsPerSample === 24) {\n const lo = view.getUint8(p) | (view.getUint8(p + 1) << 8);\n let hi = view.getUint8(p + 2);\n if (hi & 0x80) hi = hi | 0xffffff00;\n samples[i] = ((hi << 16) | lo) / 8388608;\n }\n }\n\n return { sampleRate, channels, samples };\n}\n\n/** Mix down to mono by averaging all channels. */\nexport function toMono(wav: WavData): WavData {\n if (wav.channels === 1) return wav;\n const len = wav.samples.length / wav.channels;\n const out = new Float32Array(len);\n for (let i = 0; i < len; i++) {\n let sum = 0;\n for (let c = 0; c < wav.channels; c++) sum += wav.samples[i * wav.channels + c];\n out[i] = sum / wav.channels;\n }\n return { sampleRate: wav.sampleRate, channels: 1, samples: out };\n}\n\n/** Linear interpolation resample to targetRate. Expects mono input. */\nexport function resample(wav: WavData, targetRate: number): WavData {\n if (wav.sampleRate === targetRate) return wav;\n const ratio = wav.sampleRate / targetRate;\n const outLen = Math.floor(wav.samples.length / ratio);\n const out = new Float32Array(outLen);\n const src = wav.samples;\n for (let i = 0; i < outLen; i++) {\n const x = i * ratio;\n const xi = Math.floor(x);\n const frac = x - xi;\n out[i] = (src[xi] ?? 0) + ((src[xi + 1] ?? 0) - (src[xi] ?? 0)) * frac;\n }\n return { sampleRate: targetRate, channels: 1, samples: out };\n}\n\n/** Convert Float32 [-1,1] samples to Int16 PCM. */\nexport function toInt16(samples: Float32Array): Int16Array {\n const out = new Int16Array(samples.length);\n for (let i = 0; i < samples.length; i++) {\n out[i] = Math.round(Math.max(-1, Math.min(1, samples[i])) * 32767);\n }\n return out;\n}\n\n/** Normalize then convert a WAV to 48kHz/16bit/mono Int16 PCM. */\nexport function normalizePcm(buf: ArrayBuffer): Int16Array {\n const wav = parseWav(buf);\n const mono = toMono(wav);\n const resampled = resample(mono, 48000);\n return toInt16(resampled.samples);\n}\n\nfunction readFourCC(view: DataView, pos: number): string {\n return String.fromCharCode(\n view.getUint8(pos),\n view.getUint8(pos + 1),\n view.getUint8(pos + 2),\n view.getUint8(pos + 3),\n );\n}\n","const SAMPLE_RATE = 48000;\n\nconst NAME_SEMITONE: Record<string, number> = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };\n\n/** Parse a note name like \"E4\", \"G#4\", \"Db5\" → frequency in Hz (null if invalid). */\nexport function noteNameToHz(name: string): number | null {\n const m = /^([A-Ga-g])([#b]?)(-?\\d+)$/.exec(name);\n if (!m) return null;\n let semi = NAME_SEMITONE[m[1].toLowerCase()];\n if (m[2] === '#') semi++;\n else if (m[2] === 'b') semi--;\n const midi = (parseInt(m[3], 10) + 1) * 12 + semi;\n return 440 * 2 ** ((midi - 69) / 12);\n}\n\n/** Recorded pitch encoded in a multi-pitch alias suffix: \"a い_E4\" → 329.63 Hz. */\nexport function pitchFromAliasSuffix(alias: string): number | null {\n const m = /_([A-Ga-g][#b]?-?\\d+)$/.exec(alias);\n return m ? noteNameToHz(m[1]) : null;\n}\n\n/**\n * Estimate the fundamental frequency (Hz) of a voiced region by normalized\n * autocorrelation. Returns 0 when no clear pitch is found (unvoiced consonant,\n * silence, or a region too short to analyse).\n *\n * The signal is decimated to a lower analysis rate for speed; f0 below ~700 Hz\n * is well within the resulting Nyquist limit. A parabolic interpolation around\n * the best lag gives sub-sample (sub-semitone) accuracy.\n */\nexport function detectF0(pcm: Int16Array, start: number, end: number): number {\n const DECIM = 4;\n const sr = SAMPLE_RATE / DECIM; // 12 kHz analysis rate\n const minLag = Math.floor(sr / 700); // highest f0 we look for\n const maxLag = Math.floor(sr / 70); // lowest f0 we look for\n\n const outLen = Math.floor((end - start) / DECIM);\n if (outLen < maxLag + 2) return 0;\n const win = Math.min(outLen, 1500); // up to ~0.125 s of vowel\n\n // Decimate (box-average) + remove DC, and build a prefix sum of squares so\n // each lag's window energy is O(1).\n const buf = new Float32Array(win);\n let mean = 0;\n for (let i = 0; i < win; i++) {\n let s = 0;\n const base = start + i * DECIM;\n for (let j = 0; j < DECIM; j++) s += pcm[base + j];\n buf[i] = s;\n mean += s;\n }\n mean /= win;\n\n const sq = new Float64Array(win + 1);\n for (let i = 0; i < win; i++) {\n buf[i] -= mean;\n sq[i + 1] = sq[i] + buf[i] * buf[i];\n }\n if (sq[win] < 1) return 0; // effectively silent\n\n // Normalized autocorrelation: 2·r(lag) / (E[0..n] + E[lag..lag+n]) ∈ [-1, 1].\n const norm = (lag: number): number => {\n const n = win - lag;\n let r = 0;\n for (let i = 0; i < n; i++) r += buf[i] * buf[i + lag];\n const e = sq[n] + (sq[lag + n] - sq[lag]);\n return e > 0 ? (2 * r) / e : 0;\n };\n\n let bestLag = -1;\n let best = 0;\n for (let lag = minLag; lag <= maxLag; lag++) {\n const v = norm(lag);\n if (v > best) {\n best = v;\n bestLag = lag;\n }\n }\n if (bestLag < 1 || best < 0.4) return 0; // unvoiced / no clear period\n\n // Parabolic interpolation around the peak\n const y0 = norm(bestLag - 1);\n const y1 = best;\n const y2 = norm(bestLag + 1);\n const denom = y0 - 2 * y1 + y2;\n const shift = denom !== 0 ? (0.5 * (y0 - y2)) / denom : 0;\n\n return sr / (bestLag + shift);\n}\n","import type { Manifest, PhonemeEntry } from '../types.js';\nimport type { OtoEntry } from './parse-oto.js';\nimport { detectF0 } from './pitch.js';\n\nconst TARGET_RATE = 48000;\n\nfunction msToSamples(ms: number): number {\n return Math.round((ms / 1000) * TARGET_RATE);\n}\n\nconst clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));\n\nexport interface PackInput {\n oto: OtoEntry;\n /** Full normalized PCM of the source WAV (48kHz / 16bit / mono) */\n pcm: Int16Array;\n /** Known recorded pitch in Hz (e.g. from the .frq file). 0/undefined → auto-detect. */\n recordedPitch?: number;\n}\n\nexport interface PackOutput {\n manifest: Manifest;\n /** Raw PCM blob — Int16 / 48kHz / mono */\n bin: ArrayBuffer;\n}\n\nexport interface TrimmedPhoneme {\n /** PCM trimmed to the oto region [offset, cutoff] */\n pcm: Int16Array;\n /** Manifest params relative to the trimmed start (sample 0 = oto offset) */\n entry: Omit<PhonemeEntry, 'offset'>;\n}\n\n/**\n * Cut the full WAV PCM down to its usable oto region and recompute parameters\n * relative to the trimmed start.\n *\n * UTAU oto.ini values are all in ms and measured from `offset` (the left blank),\n * except `cutoff` (right blank):\n * - cutoff >= 0 : measured from the END of the file\n * - cutoff < 0 : region length from offset = |cutoff|\n *\n * After trimming, sample 0 == oto offset, so pre/overlap/consonant carry over\n * unchanged (just converted to samples), and the slice length is the region end.\n */\nexport function trimToOto(pcm: Int16Array, oto: OtoEntry, recordedPitch = 0): TrimmedPhoneme {\n const full = pcm.length;\n\n const start = clamp(msToSamples(oto.offset), 0, full);\n const end = oto.cutoff < 0\n ? clamp(start + msToSamples(-oto.cutoff), start, full)\n : clamp(full - msToSamples(oto.cutoff), start, full);\n\n const slice = pcm.subarray(start, end);\n const length = slice.length;\n\n const pre = clamp(msToSamples(oto.pre), 0, length);\n const overlap = clamp(msToSamples(oto.overlap), 0, length);\n const consonant = clamp(msToSamples(oto.consonant), 0, length);\n\n // Prefer the known recorded pitch (from the .frq file or alias suffix); only\n // fall back to autocorrelation on the sustained vowel when none is available.\n const pitch = recordedPitch > 0\n ? recordedPitch\n : detectF0(slice, Math.min(Math.max(pre, consonant), Math.max(0, length - 1)), length);\n\n return {\n pcm: slice,\n entry: { length, pre, overlap, consonant, pitch },\n };\n}\n\n/**\n * Pack normalized PCM phonemes into voice.bin + manifest.json.\n * Each phoneme is trimmed to its oto region first.\n * Duplicate aliases are silently overwritten by the later entry.\n */\nexport function pack(inputs: PackInput[], referencePitch = 220): PackOutput {\n const phonemes: Record<string, PhonemeEntry> = {};\n const chunks: Int16Array[] = [];\n let byteOffset = 0;\n\n for (const { oto, pcm, recordedPitch } of inputs) {\n const { pcm: slice, entry } = trimToOto(pcm, oto, recordedPitch);\n if (slice.length === 0) continue;\n\n phonemes[oto.alias] = { offset: byteOffset, ...entry };\n byteOffset += slice.byteLength;\n chunks.push(slice);\n }\n\n const bin = new ArrayBuffer(byteOffset);\n const view = new Uint8Array(bin);\n let pos = 0;\n for (const chunk of chunks) {\n view.set(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), pos);\n pos += chunk.byteLength;\n }\n\n const manifest: Manifest = {\n sampleRate: 48000,\n referencePitch,\n phonemes,\n };\n\n return { manifest, bin };\n}\n","/**\n * UTAU `.frq` frequency-analysis files (FREQ0003). The same format OpenUtau\n * reads: an 8-byte header, hop size, then the average fundamental frequency of\n * the recording — exactly the reference pitch we need for correct resampling.\n *\n * Layout:\n * char[8] \"FREQ0003\"\n * int32 hopSize\n * float64 averageF0 ← the recorded pitch in Hz\n * byte[16] (blank)\n * int32 length\n * { float64 f0, float64 amp } × length\n */\nexport function parseFrqAverageF0(buffer: ArrayBuffer): number | null {\n if (buffer.byteLength < 20) return null;\n const view = new DataView(buffer);\n\n let header = '';\n for (let i = 0; i < 8; i++) header += String.fromCharCode(view.getUint8(i));\n if (header !== 'FREQ0003') return null;\n\n const avg = view.getFloat64(12, true); // little-endian\n return Number.isFinite(avg) && avg > 0 ? avg : null;\n}\n\n/** Map a WAV filename to its sibling frq filename: \"あ.wav\" → \"あ_wav.frq\". */\nexport function frqFileName(wavName: string): string {\n const dot = wavName.lastIndexOf('.');\n const base = dot >= 0 ? wavName.slice(0, dot) : wavName;\n const ext = dot >= 0 ? wavName.slice(dot + 1) : 'wav';\n return `${base}_${ext}.frq`;\n}\n"],"mappings":";AAGA,IAAM,QAAQ;AASP,SAAS,QAAQ,UAAoB,UAA4B;AACtE,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,QAAQ,CAAC;AAC9D,QAAM,SAAS,IAAI,YAAY,CAAC;AAChC,QAAM,OAAO,IAAI,SAAS,MAAM;AAChC,OAAK,UAAU,GAAG,OAAO,KAAK;AAC9B,OAAK,UAAU,GAAG,KAAK,YAAY,IAAI;AACvC,SAAO,IAAI,KAAK,CAAC,QAAQ,MAAM,GAAG,QAAQ,CAAC;AAC7C;AAGO,SAAS,eAAe,aAAkD;AAC/E,QAAM,OAAO,IAAI,SAAS,WAAW;AACrC,MAAI,KAAK,aAAa,KAAK,KAAK,UAAU,GAAG,KAAK,MAAM,OAAO;AAC7D,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AACA,SAAO,EAAE,YAAY,KAAK,UAAU,GAAG,IAAI,EAAE;AAC/C;AAGO,IAAM,UAAU,CAAC,eAA+B,IAAI;;;AClB3D,IAAM,kBAAN,MAA6C;AAAA,EAC3C,YACU,MACA,MACR;AAFQ;AACA;AAAA,EACP;AAAA,EAFO;AAAA,EACA;AAAA,EAEV,UAAU,QAAgB,QAAsC;AAC9D,UAAM,QAAQ,KAAK,OAAO;AAC1B,WAAO,KAAK,KAAK,MAAM,OAAO,QAAQ,MAAM,EAAE,YAAY;AAAA,EAC5D;AACF;AAEA,IAAM,mBAAN,MAA8C;AAAA,EAC5C,YACU,KACA,MACR;AAFQ;AACA;AAAA,EACP;AAAA,EAFO;AAAA,EACA;AAAA,EAEV,MAAM,UAAU,QAAgB,QAAsC;AACpE,UAAM,QAAQ,KAAK,OAAO;AAC1B,UAAM,MAAM,MAAM,MAAM,KAAK,KAAK;AAAA,MAChC,SAAS,EAAE,OAAO,SAAS,KAAK,IAAI,QAAQ,SAAS,CAAC,GAAG;AAAA,IAC3D,CAAC;AACD,QAAI,CAAC,IAAI,MAAM,IAAI,WAAW,KAAK;AACjC,YAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,EAAE;AAAA,IAC5D;AACA,WAAO,IAAI,YAAY;AAAA,EACzB;AACF;AAEA,eAAe,WAAW,KAAa,OAAe,QAAsC;AAC1F,QAAM,MAAM,MAAM,MAAM,KAAK,EAAE,SAAS,EAAE,OAAO,SAAS,KAAK,IAAI,QAAQ,SAAS,CAAC,GAAG,EAAE,CAAC;AAC3F,MAAI,CAAC,IAAI,MAAM,IAAI,WAAW,IAAK,OAAM,IAAI,MAAM,sBAAsB,IAAI,MAAM,EAAE;AACrF,SAAO,IAAI,YAAY;AACzB;AAeO,IAAM,YAAN,MAAM,WAAU;AAAA,EACb,YAEG,UACD,QACR;AAFS;AACD;AAAA,EACP;AAAA,EAFQ;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAOV,aAAa,KAAK,KAAwC;AACxD,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAMA,UAAS,MAAM,WAAW,KAAK,GAAG,CAAC;AACzC,YAAM,EAAE,YAAAC,YAAW,IAAI,eAAeD,OAAM;AAC5C,YAAME,QAAO,MAAM,WAAW,KAAK,GAAGD,WAAU;AAChD,YAAME,YAAW,KAAK,MAAM,IAAI,YAAY,EAAE,OAAOD,KAAI,CAAC;AAC1D,aAAO,IAAI,WAAUC,WAAU,IAAI,iBAAiB,KAAK,QAAQF,WAAU,CAAC,CAAC;AAAA,IAC/E;AACA,UAAM,SAAS,MAAM,IAAI,MAAM,GAAG,CAAC,EAAE,YAAY;AACjD,UAAM,EAAE,WAAW,IAAI,eAAe,MAAM;AAC5C,UAAM,OAAO,MAAM,IAAI,MAAM,GAAG,IAAI,UAAU,EAAE,YAAY;AAC5D,UAAM,WAAW,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,IAAI,CAAC;AAC1D,WAAO,IAAI,WAAU,UAAU,IAAI,gBAAgB,KAAK,QAAQ,UAAU,CAAC,CAAC;AAAA,EAC9E;AAAA;AAAA,EAGA,IAAI,SAA0B;AAC5B,WAAO,KAAK,SAAS,SAAS,OAAO,MAAM;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,SAA8C;AAC/D,UAAM,QAAQ,KAAK,SAAS,SAAS,OAAO;AAC5C,QAAI,CAAC,MAAO,QAAO;AACnB,WAAO,KAAK,OAAO,UAAU,MAAM,QAAQ,MAAM,SAAS,CAAC;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,SAA+C;AAC1D,UAAM,MAAM,MAAM,KAAK,aAAa,OAAO;AAC3C,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,UAAM,MAAM,IAAI,aAAa,MAAM,MAAM;AACzC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,KAAI,CAAC,IAAI,MAAM,CAAC,IAAI;AAC3D,WAAO;AAAA,EACT;AACF;;;ACzFO,IAAM,YAAN,MAAgB;AAAA,EACb;AAAA,EACA;AAAA,EACA,OAAgC;AAAA,EAChC,OAAyB;AAAA,EACzB,YAAY,oBAAI,IAAY;AAAA,EAC5B,UAAU,oBAAI,IAA2B;AAAA,EAEjD,YAAY,UAA4B,CAAC,GAAG;AAC1C,SAAK,MAAM,IAAI,aAAa,EAAE,YAAY,KAAM,CAAC;AACjD,SAAK,aAAa,QAAQ,cAAc;AAAA,EAC1C;AAAA,EAEA,IAAI,eAA6B;AAC/B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,WAA4B;AAC9B,WAAO,KAAK,MAAM,YAAY;AAAA,EAChC;AAAA;AAAA,EAGA,IAAI,YAA8B;AAChC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,KAAK,KAAmC;AAC5C,UAAM,KAAK,IAAI,aAAa,UAAU,KAAK,UAAU;AAErD,SAAK,OAAO,MAAM,UAAU,KAAK,GAAG;AACpC,SAAK,UAAU,MAAM;AACrB,SAAK,QAAQ,MAAM;AAEnB,SAAK,OAAO,IAAI,iBAAiB,KAAK,KAAK,iBAAiB;AAAA,MAC1D,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,oBAAoB,CAAC,CAAC;AAAA,IACxB,CAAC;AACD,SAAK,KAAK,KAAK,YAAY,EAAE,MAAM,QAAQ,UAAU,KAAK,KAAK,SAAS,CAAC;AACzE,SAAK,KAAK,QAAQ,KAAK,IAAI,WAAW;AAEtC,YAAQ;AAAA,MACN;AAAA,MACA,OAAO,KAAK,KAAK,KAAK,SAAS,QAAQ,EAAE;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGQ,cAAc,MAA6B;AACjD,QAAI,KAAK,UAAU,IAAI,IAAI,EAAG,QAAO,QAAQ,QAAQ;AACrD,UAAM,WAAW,KAAK,QAAQ,IAAI,IAAI;AACtC,QAAI,SAAU,QAAO;AACrB,QAAI,CAAC,KAAK,QAAQ,CAAC,KAAK,KAAM,QAAO,QAAQ,QAAQ;AAErD,UAAM,OAAO,KAAK,KAAK,aAAa,IAAI,EAAE,KAAK,SAAO;AACpD,UAAI,CAAC,IAAK;AACV,WAAK,KAAM,KAAK,YAAY,EAAE,MAAM,WAAW,MAAM,QAAQ,IAAI,GAAG,CAAC,GAAG,CAAC;AACzE,WAAK,UAAU,IAAI,IAAI;AACvB,WAAK,QAAQ,OAAO,IAAI;AAAA,IAC1B,CAAC;AAED,SAAK,QAAQ,IAAI,MAAM,IAAI;AAC3B,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,KAAK,OAAmC;AAC5C,QAAI,CAAC,KAAK,KAAM,OAAM,IAAI,MAAM,sCAAsC;AACtE,SAAK,KAAK,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAC3C,UAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,OAAO,CAAC,CAAC,EAAE,OAAO,OAAO;AACpE,UAAM,QAAQ,IAAI,MAAM,IAAI,OAAK,KAAK,cAAc,CAAC,CAAC,CAAC;AACvD,SAAK,KAAK,KAAK,YAAY,EAAE,MAAM,QAAQ,MAAM,CAAC;AAAA,EACpD;AAAA;AAAA,EAGA,OAAa;AACX,SAAK,MAAM,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAAA,EAC9C;AAAA;AAAA,EAGA,MAAM,SAAwB;AAC5B,QAAI,KAAK,IAAI,UAAU,YAAa,OAAM,KAAK,IAAI,OAAO;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,OAAO,SAA+C;AAC1D,WAAO,KAAK,MAAM,OAAO,OAAO,KAAK;AAAA,EACvC;AACF;;;ACvHO,IAAM,wBAAwB;AAM9B,IAAM,wBAAwB;AAGrC,IAAM,iBAAiB;AAEvB,IAAM,cAAc;AA+Eb,IAAM,cAAc,CAAC,YAA6B,UAAU,wBAAyB;AAQrF,SAAS,gBAAgB,OAA6D;AAC3F,SAAO,EAAE,OAAO,YAAY,MAAM,OAAO,CAAC,GAAG,aAAa,YAAY,MAAM,aAAa,CAAC,EAAE;AAC9F;AAGA,IAAM,cAAc,oBAAI,IAAoC;AAE5D,SAAS,aAAa,KAA4B;AAChD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,WAAW,SAAS,cAAiC,8BAA8B,GAAG,IAAI;AAChG,QAAI,UAAU;AACZ,cAAQ;AACR;AAAA,IACF;AACA,UAAM,IAAI,SAAS,cAAc,QAAQ;AACzC,MAAE,MAAM;AACR,MAAE,QAAQ,eAAe;AACzB,MAAE,SAAS,MAAM,QAAQ;AACzB,MAAE,UAAU,MAAM,OAAO,IAAI,MAAM,6BAA6B,GAAG,EAAE,CAAC;AACtE,aAAS,KAAK,YAAY,CAAC;AAAA,EAC7B,CAAC;AACH;AAEA,SAAS,SAAS,WAA2C;AAC3D,QAAM,SAAS,YAAY,IAAI,SAAS;AACxC,MAAI,OAAQ,QAAO;AAEnB,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO,QAAQ;AAAA,MACb,IAAI,MAAM,0EAA0E;AAAA,IACtF;AAAA,EACF;AAEA,QAAM,UAAU,UAAU,MAAM,GAAG,UAAU,YAAY,GAAG,IAAI,CAAC;AACjE,QAAM,UAAU,aAAa,SAAS,EAAE,KAAK,MAAM;AACjD,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS,OAAM,IAAI,MAAM,iEAAiE;AAC/F,WAAO,QAAQ,EAAE,YAAY,CAAC,MAAc,UAAU,EAAE,CAAC;AAAA,EAC3D,CAAC;AACD,cAAY,IAAI,WAAW,OAAO;AAClC,SAAO;AACT;AAmBO,IAAM,YAAN,MAAM,WAAU;AAAA,EAGb,YAAoB,MAAqB;AAArB;AAAA,EAAsB;AAAA,EAAtB;AAAA,EAFnB,aAAa;AAAA;AAAA,EAKtB,aAAa,KAAK,SAAmD;AACnE,WAAO,IAAI,WAAU,MAAM,SAAS,QAAQ,SAAS,CAAC;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,WAAW,QAA+C;AACxD,UAAM,EAAE,KAAK,OAAO,YAAY,OAAO,aAAa,QAAQ,IAAI,IAAI;AACpE,QAAI,CAAC,OAAO,IAAI,SAAS,sBAAuB,QAAO;AAEvD,UAAM,KAAK,KAAK;AAChB,UAAM,KAAK;AACX,UAAM,WAAW,KAAK,MAAM,KAAK,KAAK,KAAK,KAAK,QAAQ,GAAG,CAAC;AAC5D,UAAM,QAAQ;AACd,UAAM,SAAS,QAAQ;AAQvB,UAAM,QAAQ,cAAc;AAE5B,UAAM,KAAK,GAAG,gBAAgB;AAC9B,QAAI,CAAC,GAAI,QAAO;AAChB,UAAM,SAAS,GAAG,QAAQ,cAAc;AACxC,QAAI,CAAC,QAAQ;AACX,SAAG,mBAAmB,EAAE;AACxB,aAAO;AAAA,IACT;AACA,UAAM,YAAY,GAAG,QAAQ,IAAI,SAAS,CAAC;AAC3C,QAAI,CAAC,WAAW;AACd,SAAG,MAAM,MAAM;AACf,SAAG,mBAAmB,EAAE;AACxB,aAAO;AAAA,IACT;AACA,OAAG,QAAQ,IAAI,KAAK,aAAa,CAAC;AAElC,UAAM,KAAK,CAAC,KAAa,KAAa,SAAiB,GAAG,SAAS,SAAS,KAAK,KAAK,IAAI;AAC1F,OAAG,GAAG,IAAI,KAAK;AACf,OAAG,GAAG,IAAI,QAAQ,KAAK;AACvB,OAAG,GAAG,WAAW,GAAG;AACpB,OAAG,IAAI,GAAG,KAAK;AACf,OAAG,IAAI,GAAG,GAAG;AACb,OAAG,IAAI,UAAU,KAAK;AACtB,OAAG,IAAI,KAAO,QAAQ;AACtB,OAAG,IAAI,GAAK,QAAQ;AACpB,OAAG,IAAI,QAAQ,QAAQ;AACvB,OAAG,IAAI,aAAa,QAAQ;AAC5B,OAAG,IAAI,OAAO,QAAQ;AACtB,OAAG,IAAI,KAAO,QAAQ;AACtB,OAAG,IAAI,GAAK,QAAQ;AACpB,OAAG,IAAI,OAAO,QAAQ;AACtB,OAAG,IAAI,GAAG,KAAK;AACf,OAAG,IAAI,GAAG,GAAG;AACb,OAAG,IAAI,GAAG,KAAK;AACf,OAAG,KAAK,GAAG,KAAK;AAChB,OAAG,KAAK,KAAK,KAAK;AAClB,OAAG,KAAK,GAAG,KAAK;AAChB,OAAG,KAAK,GAAG,KAAK;AAChB,OAAG,KAAK,KAAK,KAAK;AAGlB,OAAG,uBAAuB,IAAI,QAAQ,OAAO,GAAK,QAAQ,GAAK,GAAK,CAAC;AACrE,OAAG,MAAM,SAAS;AAClB,OAAG,MAAM,MAAM;AAEf,UAAM,UAAU,QAAQ,SAAS,cAAc;AAC/C,UAAM,UAAU,KAAK,KAAK,UAAU,WAAW,IAAI;AACnD,UAAM,QAAQ,IAAI,aAAa,OAAO,EAAE,KAAK,KAAK;AAClD,UAAM,OAAO,IAAI,aAAa,OAAO,EAAE,KAAK,GAAG;AAC/C,UAAM,OAAO,IAAI,aAAa,OAAO,EAAE,KAAK,GAAG;AAC/C,UAAM,OAAO,IAAI,aAAa,OAAO,EAAE,KAAK,GAAG;AAC/C,UAAM,OAAO,IAAI,aAAa,OAAO,EAAE,KAAK,CAAG;AAE/C,UAAM,QAAQ,GAAG,QAAQ,UAAU,CAAC;AACpC,UAAM,OAAO,GAAG,QAAQ,UAAU,CAAC;AACnC,UAAM,OAAO,GAAG,QAAQ,UAAU,CAAC;AACnC,UAAM,OAAO,GAAG,QAAQ,UAAU,CAAC;AACnC,UAAM,OAAO,GAAG,QAAQ,UAAU,CAAC;AACnC,QAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM;AAC9C,UAAI,MAAO,IAAG,MAAM,KAAK;AACzB,UAAI,KAAM,IAAG,MAAM,IAAI;AACvB,UAAI,KAAM,IAAG,MAAM,IAAI;AACvB,UAAI,KAAM,IAAG,MAAM,IAAI;AACvB,UAAI,KAAM,IAAG,MAAM,IAAI;AACvB,SAAG,mBAAmB,EAAE;AACxB,aAAO;AAAA,IACT;AACA,OAAG,QAAQ,IAAI,OAAO,SAAS,CAAC;AAChC,OAAG,QAAQ,IAAI,MAAM,QAAQ,CAAC;AAC9B,OAAG,QAAQ,IAAI,MAAM,QAAQ,CAAC;AAC9B,OAAG,QAAQ,IAAI,MAAM,QAAQ,CAAC;AAC9B,OAAG,QAAQ,IAAI,MAAM,QAAQ,CAAC;AAC9B,OAAG,sBAAsB,IAAI,OAAO,MAAM,MAAM,MAAM,MAAM,SAAS,WAAW;AAChF,OAAG,MAAM,KAAK;AACd,OAAG,MAAM,IAAI;AACb,OAAG,MAAM,IAAI;AACb,OAAG,MAAM,IAAI;AACb,OAAG,MAAM,IAAI;AAEb,UAAM,UAAU,GAAG,QAAQ,CAAC;AAC5B,QAAI,CAAC,SAAS;AACZ,SAAG,mBAAmB,EAAE;AACxB,aAAO;AAAA,IACT;AACA,UAAM,SAAS,GAAG,kBAAkB,IAAI,SAAS,CAAC;AAClD,UAAM,OAAO,GAAG,SAAS,SAAS,GAAG;AACrC,UAAM,QACJ,SAAS,IAAI,IAAI,aAAa,GAAG,QAAQ,QAAQ,MAAM,MAAM,EAAE,MAAM,IAAI;AAC3E,OAAG,MAAM,OAAO;AAChB,OAAG,mBAAmB,EAAE;AACxB,WAAO;AAAA,EACT;AACF;;;AChRO,SAAS,SAAS,SAA6B;AACpD,QAAM,UAAsB,CAAC;AAE7B,aAAW,OAAO,QAAQ,MAAM,OAAO,GAAG;AACxC,UAAM,OAAO,IAAI,KAAK;AACtB,QAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,EAAG;AAEnC,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAI,OAAO,GAAI;AAEf,UAAM,MAAM,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;AACnC,UAAM,QAAQ,KAAK,MAAM,KAAK,CAAC,EAAE,MAAM,GAAG;AAC1C,QAAI,MAAM,SAAS,EAAG;AAEtB,UAAM,CAAC,OAAO,WAAW,cAAc,WAAW,QAAQ,UAAU,IAAI;AAExE,UAAM,WAAW,MAAM,KAAK,KAAK,IAAI,QAAQ,YAAY,EAAE;AAC3D,UAAM,QAAkB;AAAA,MACtB;AAAA,MACA,OAAO;AAAA,MACP,QAAQ,WAAW,SAAS,KAAK;AAAA,MACjC,WAAW,WAAW,YAAY,KAAK;AAAA,MACvC,QAAQ,WAAW,SAAS,KAAK;AAAA,MACjC,KAAK,WAAW,MAAM,KAAK;AAAA,MAC3B,SAAS,WAAW,UAAU,KAAK;AAAA,IACrC;AAEA,QAAI,CAAC,MAAM,MAAO;AAClB,YAAQ,KAAK,KAAK;AAAA,EACpB;AAEA,SAAO;AACT;;;AC7CO,SAAS,SAAS,KAA2B;AAClD,QAAM,OAAO,IAAI,SAAS,GAAG;AAE7B,QAAM,OAAO,WAAW,MAAM,CAAC;AAC/B,MAAI,SAAS,OAAQ,OAAM,IAAI,MAAM,yBAAyB,IAAI,IAAI;AAEtE,MAAI,aAAa;AACjB,MAAI,WAAW;AACf,MAAI,gBAAgB;AACpB,MAAI,cAAc;AAClB,MAAI,aAAa;AACjB,MAAI,aAAa;AAEjB,MAAI,MAAM;AACV,SAAO,MAAM,KAAK,aAAa,GAAG;AAChC,UAAM,KAAK,WAAW,MAAM,GAAG;AAC/B,UAAM,OAAO,KAAK,UAAU,MAAM,GAAG,IAAI;AACzC,WAAO;AAEP,QAAI,OAAO,QAAQ;AACjB,oBAAc,KAAK,UAAU,KAAK,IAAI;AACtC,iBAAW,KAAK,UAAU,MAAM,GAAG,IAAI;AACvC,mBAAa,KAAK,UAAU,MAAM,GAAG,IAAI;AACzC,sBAAgB,KAAK,UAAU,MAAM,IAAI,IAAI;AAAA,IAC/C,WAAW,OAAO,QAAQ;AACxB,mBAAa;AACb,mBAAa;AACb;AAAA,IACF;AAEA,WAAO,QAAQ,OAAO;AAAA,EACxB;AAEA,MAAI,CAAC,WAAY,OAAM,IAAI,MAAM,uBAAuB;AACxD,MAAI,CAAC,YAAY,CAAC,WAAY,OAAM,IAAI,MAAM,uBAAuB;AAErE,QAAM,iBAAiB,iBAAiB;AACxC,QAAM,eAAe,KAAK,MAAM,aAAa,cAAc;AAC3D,QAAM,UAAU,IAAI,aAAa,YAAY;AAE7C,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACrC,UAAM,IAAI,aAAa,IAAI;AAC3B,QAAI,gBAAgB,GAAG;AACrB,cAAQ,CAAC,IAAI,KAAK,WAAW,GAAG,IAAI;AAAA,IACtC,WAAW,kBAAkB,GAAG;AAC9B,cAAQ,CAAC,KAAK,KAAK,SAAS,CAAC,IAAI,OAAO;AAAA,IAC1C,WAAW,kBAAkB,IAAI;AAC/B,cAAQ,CAAC,IAAI,KAAK,SAAS,GAAG,IAAI,IAAI;AAAA,IACxC,WAAW,kBAAkB,IAAI;AAC/B,YAAM,KAAK,KAAK,SAAS,CAAC,IAAK,KAAK,SAAS,IAAI,CAAC,KAAK;AACvD,UAAI,KAAK,KAAK,SAAS,IAAI,CAAC;AAC5B,UAAI,KAAK,IAAM,MAAK,KAAK;AACzB,cAAQ,CAAC,KAAM,MAAM,KAAM,MAAM;AAAA,IACnC;AAAA,EACF;AAEA,SAAO,EAAE,YAAY,UAAU,QAAQ;AACzC;AAGO,SAAS,OAAO,KAAuB;AAC5C,MAAI,IAAI,aAAa,EAAG,QAAO;AAC/B,QAAM,MAAM,IAAI,QAAQ,SAAS,IAAI;AACrC,QAAM,MAAM,IAAI,aAAa,GAAG;AAChC,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,QAAI,MAAM;AACV,aAAS,IAAI,GAAG,IAAI,IAAI,UAAU,IAAK,QAAO,IAAI,QAAQ,IAAI,IAAI,WAAW,CAAC;AAC9E,QAAI,CAAC,IAAI,MAAM,IAAI;AAAA,EACrB;AACA,SAAO,EAAE,YAAY,IAAI,YAAY,UAAU,GAAG,SAAS,IAAI;AACjE;AAGO,SAAS,SAAS,KAAc,YAA6B;AAClE,MAAI,IAAI,eAAe,WAAY,QAAO;AAC1C,QAAM,QAAQ,IAAI,aAAa;AAC/B,QAAM,SAAS,KAAK,MAAM,IAAI,QAAQ,SAAS,KAAK;AACpD,QAAM,MAAM,IAAI,aAAa,MAAM;AACnC,QAAM,MAAM,IAAI;AAChB,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,UAAM,IAAI,IAAI;AACd,UAAM,KAAK,KAAK,MAAM,CAAC;AACvB,UAAM,OAAO,IAAI;AACjB,QAAI,CAAC,KAAK,IAAI,EAAE,KAAK,OAAO,IAAI,KAAK,CAAC,KAAK,MAAM,IAAI,EAAE,KAAK,MAAM;AAAA,EACpE;AACA,SAAO,EAAE,YAAY,YAAY,UAAU,GAAG,SAAS,IAAI;AAC7D;AAGO,SAAS,QAAQ,SAAmC;AACzD,QAAM,MAAM,IAAI,WAAW,QAAQ,MAAM;AACzC,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,QAAI,CAAC,IAAI,KAAK,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,KAAK;AAAA,EACnE;AACA,SAAO;AACT;AAGO,SAAS,aAAa,KAA8B;AACzD,QAAM,MAAM,SAAS,GAAG;AACxB,QAAM,OAAO,OAAO,GAAG;AACvB,QAAM,YAAY,SAAS,MAAM,IAAK;AACtC,SAAO,QAAQ,UAAU,OAAO;AAClC;AAEA,SAAS,WAAW,MAAgB,KAAqB;AACvD,SAAO,OAAO;AAAA,IACZ,KAAK,SAAS,GAAG;AAAA,IACjB,KAAK,SAAS,MAAM,CAAC;AAAA,IACrB,KAAK,SAAS,MAAM,CAAC;AAAA,IACrB,KAAK,SAAS,MAAM,CAAC;AAAA,EACvB;AACF;;;ACxHA,IAAM,cAAc;AAEpB,IAAM,gBAAwC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG;AAGnF,SAAS,aAAa,MAA6B;AACxD,QAAM,IAAI,6BAA6B,KAAK,IAAI;AAChD,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,OAAO,cAAc,EAAE,CAAC,EAAE,YAAY,CAAC;AAC3C,MAAI,EAAE,CAAC,MAAM,IAAK;AAAA,WACT,EAAE,CAAC,MAAM,IAAK;AACvB,QAAM,QAAQ,SAAS,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,KAAK;AAC7C,SAAO,MAAM,OAAO,OAAO,MAAM;AACnC;AAGO,SAAS,qBAAqB,OAA8B;AACjE,QAAM,IAAI,yBAAyB,KAAK,KAAK;AAC7C,SAAO,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI;AAClC;AAWO,SAAS,SAAS,KAAiB,OAAe,KAAqB;AAC5E,QAAM,QAAQ;AACd,QAAM,KAAK,cAAc;AACzB,QAAM,SAAS,KAAK,MAAM,KAAK,GAAG;AAClC,QAAM,SAAS,KAAK,MAAM,KAAK,EAAE;AAEjC,QAAM,SAAS,KAAK,OAAO,MAAM,SAAS,KAAK;AAC/C,MAAI,SAAS,SAAS,EAAG,QAAO;AAChC,QAAM,MAAM,KAAK,IAAI,QAAQ,IAAI;AAIjC,QAAM,MAAM,IAAI,aAAa,GAAG;AAChC,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,QAAI,IAAI;AACR,UAAM,OAAO,QAAQ,IAAI;AACzB,aAAS,IAAI,GAAG,IAAI,OAAO,IAAK,MAAK,IAAI,OAAO,CAAC;AACjD,QAAI,CAAC,IAAI;AACT,YAAQ;AAAA,EACV;AACA,UAAQ;AAER,QAAM,KAAK,IAAI,aAAa,MAAM,CAAC;AACnC,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,QAAI,CAAC,KAAK;AACV,OAAG,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC;AAAA,EACpC;AACA,MAAI,GAAG,GAAG,IAAI,EAAG,QAAO;AAGxB,QAAM,OAAO,CAAC,QAAwB;AACpC,UAAM,IAAI,MAAM;AAChB,QAAI,IAAI;AACR,aAAS,IAAI,GAAG,IAAI,GAAG,IAAK,MAAK,IAAI,CAAC,IAAI,IAAI,IAAI,GAAG;AACrD,UAAM,IAAI,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,GAAG,GAAG;AACvC,WAAO,IAAI,IAAK,IAAI,IAAK,IAAI;AAAA,EAC/B;AAEA,MAAI,UAAU;AACd,MAAI,OAAO;AACX,WAAS,MAAM,QAAQ,OAAO,QAAQ,OAAO;AAC3C,UAAM,IAAI,KAAK,GAAG;AAClB,QAAI,IAAI,MAAM;AACZ,aAAO;AACP,gBAAU;AAAA,IACZ;AAAA,EACF;AACA,MAAI,UAAU,KAAK,OAAO,IAAK,QAAO;AAGtC,QAAM,KAAK,KAAK,UAAU,CAAC;AAC3B,QAAM,KAAK;AACX,QAAM,KAAK,KAAK,UAAU,CAAC;AAC3B,QAAM,QAAQ,KAAK,IAAI,KAAK;AAC5B,QAAM,QAAQ,UAAU,IAAK,OAAO,KAAK,MAAO,QAAQ;AAExD,SAAO,MAAM,UAAU;AACzB;;;ACpFA,IAAM,cAAc;AAEpB,SAAS,YAAY,IAAoB;AACvC,SAAO,KAAK,MAAO,KAAK,MAAQ,WAAW;AAC7C;AAEA,IAAM,QAAQ,CAAC,GAAW,IAAY,OAAe,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC;AAmC1E,SAAS,UAAU,KAAiB,KAAe,gBAAgB,GAAmB;AAC3F,QAAM,OAAO,IAAI;AAEjB,QAAM,QAAQ,MAAM,YAAY,IAAI,MAAM,GAAG,GAAG,IAAI;AACpD,QAAM,MAAM,IAAI,SAAS,IACrB,MAAM,QAAQ,YAAY,CAAC,IAAI,MAAM,GAAG,OAAO,IAAI,IACnD,MAAM,OAAO,YAAY,IAAI,MAAM,GAAG,OAAO,IAAI;AAErD,QAAM,QAAQ,IAAI,SAAS,OAAO,GAAG;AACrC,QAAM,SAAS,MAAM;AAErB,QAAM,MAAM,MAAM,YAAY,IAAI,GAAG,GAAG,GAAG,MAAM;AACjD,QAAM,UAAU,MAAM,YAAY,IAAI,OAAO,GAAG,GAAG,MAAM;AACzD,QAAM,YAAY,MAAM,YAAY,IAAI,SAAS,GAAG,GAAG,MAAM;AAI7D,QAAM,QAAQ,gBAAgB,IAC1B,gBACA,SAAS,OAAO,KAAK,IAAI,KAAK,IAAI,KAAK,SAAS,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC,CAAC,GAAG,MAAM;AAEvF,SAAO;AAAA,IACL,KAAK;AAAA,IACL,OAAO,EAAE,QAAQ,KAAK,SAAS,WAAW,MAAM;AAAA,EAClD;AACF;AAOO,SAAS,KAAK,QAAqB,iBAAiB,KAAiB;AAC1E,QAAM,WAAyC,CAAC;AAChD,QAAM,SAAuB,CAAC;AAC9B,MAAI,aAAa;AAEjB,aAAW,EAAE,KAAK,KAAK,cAAc,KAAK,QAAQ;AAChD,UAAM,EAAE,KAAK,OAAO,MAAM,IAAI,UAAU,KAAK,KAAK,aAAa;AAC/D,QAAI,MAAM,WAAW,EAAG;AAExB,aAAS,IAAI,KAAK,IAAI,EAAE,QAAQ,YAAY,GAAG,MAAM;AACrD,kBAAc,MAAM;AACpB,WAAO,KAAK,KAAK;AAAA,EACnB;AAEA,QAAM,MAAM,IAAI,YAAY,UAAU;AACtC,QAAM,OAAO,IAAI,WAAW,GAAG;AAC/B,MAAI,MAAM;AACV,aAAW,SAAS,QAAQ;AAC1B,SAAK,IAAI,IAAI,WAAW,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU,GAAG,GAAG;AAC9E,WAAO,MAAM;AAAA,EACf;AAEA,QAAM,WAAqB;AAAA,IACzB,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,IAAI;AACzB;;;AC7FO,SAAS,kBAAkB,QAAoC;AACpE,MAAI,OAAO,aAAa,GAAI,QAAO;AACnC,QAAM,OAAO,IAAI,SAAS,MAAM;AAEhC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,GAAG,IAAK,WAAU,OAAO,aAAa,KAAK,SAAS,CAAC,CAAC;AAC1E,MAAI,WAAW,WAAY,QAAO;AAElC,QAAM,MAAM,KAAK,WAAW,IAAI,IAAI;AACpC,SAAO,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM;AACjD;AAGO,SAAS,YAAY,SAAyB;AACnD,QAAM,MAAM,QAAQ,YAAY,GAAG;AACnC,QAAM,OAAO,OAAO,IAAI,QAAQ,MAAM,GAAG,GAAG,IAAI;AAChD,QAAM,MAAM,OAAO,IAAI,QAAQ,MAAM,MAAM,CAAC,IAAI;AAChD,SAAO,GAAG,IAAI,IAAI,GAAG;AACvB;","names":["header","jsonLength","json","manifest"]}
|
|
1
|
+
{"version":3,"sources":["../src/koe.ts","../src/engine/voice-bank.ts","../src/engine/index.ts","../src/engine/worldline.ts","../src/converter/parse-oto.ts","../src/converter/wav.ts","../src/converter/pitch.ts","../src/converter/pack.ts","../src/converter/frq.ts"],"sourcesContent":["import type { Manifest } from \"./types.js\";\n\n/** Magic number 'KOE\\0' (stored big-endian as 0x4B 0x4F 0x45 0x00). */\nconst MAGIC = 0x4b4f4500;\n\n/**\n * Koe Archive Format (.koe)\n * [4B] magic 'KOE\\0' (big-endian)\n * [4B] JSON length (little-endian)\n * [N ] manifest JSON (UTF-8)\n * [M ] raw PCM (Int16 / 48kHz / mono); phoneme offsets are relative to here\n */\nexport function packKoe(manifest: Manifest, pcmParts: BlobPart[]): Blob {\n\tconst json = new TextEncoder().encode(JSON.stringify(manifest));\n\tconst header = new ArrayBuffer(8);\n\tconst view = new DataView(header);\n\tview.setUint32(0, MAGIC, false); // magic, big-endian\n\tview.setUint32(4, json.byteLength, true); // json length, little-endian\n\treturn new Blob([header, json, ...pcmParts]);\n}\n\n/** Read the 8-byte header → JSON length. Throws on bad magic. */\nexport function parseKoeHeader(headerBytes: ArrayBuffer): {\n\tjsonLength: number;\n} {\n\tconst view = new DataView(headerBytes);\n\tif (view.byteLength < 8 || view.getUint32(0, false) !== MAGIC) {\n\t\tthrow new Error(\"Not a .koe file (bad magic)\");\n\t}\n\treturn { jsonLength: view.getUint32(4, true) };\n}\n\n/** Byte offset where PCM data begins, given the JSON length. */\nexport const pcmBase = (jsonLength: number): number => 8 + jsonLength;\n","import type { Manifest } from \"../types.js\";\nimport { parseKoeHeader, pcmBase } from \"../koe.js\";\n\n/**\n * Supplies raw bytes from the PCM section of a .koe archive on demand.\n * - BlobVoiceSource: slices an in-memory Blob / File (no full-buffer copy)\n * - RangeVoiceSource: HTTP Range requests against a URL (mobile-friendly)\n * `base` is the byte offset where PCM data starts (8 + jsonLength).\n */\ninterface VoiceSource {\n\treadBytes(offset: number, length: number): Promise<ArrayBuffer>;\n}\n\nclass BlobVoiceSource implements VoiceSource {\n\tconstructor(\n\t\tprivate blob: Blob,\n\t\tprivate base: number,\n\t) {}\n\treadBytes(offset: number, length: number): Promise<ArrayBuffer> {\n\t\tconst start = this.base + offset;\n\t\treturn this.blob.slice(start, start + length).arrayBuffer();\n\t}\n}\n\nclass RangeVoiceSource implements VoiceSource {\n\tconstructor(\n\t\tprivate url: string,\n\t\tprivate base: number,\n\t) {}\n\tasync readBytes(offset: number, length: number): Promise<ArrayBuffer> {\n\t\tconst start = this.base + offset;\n\t\tconst res = await fetch(this.url, {\n\t\t\theaders: { Range: `bytes=${start}-${start + length - 1}` },\n\t\t});\n\t\tif (!res.ok && res.status !== 206) {\n\t\t\tthrow new Error(`.koe range request failed: ${res.status}`);\n\t\t}\n\t\treturn res.arrayBuffer();\n\t}\n}\n\nasync function rangeFetch(\n\turl: string,\n\tstart: number,\n\tlength: number,\n): Promise<ArrayBuffer> {\n\tconst res = await fetch(url, {\n\t\theaders: { Range: `bytes=${start}-${start + length - 1}` },\n\t});\n\tif (!res.ok && res.status !== 206)\n\t\tthrow new Error(`.koe fetch failed: ${res.status}`);\n\treturn res.arrayBuffer();\n}\n\n/**\n * Read-only access to a .koe voice bank: its manifest plus per-phoneme PCM,\n * fetched on demand (Blob slice or HTTP Range). The full bank is never held in\n * memory.\n *\n * Pure data — no AudioContext, no AudioWorklet, no DOM. Use this when you only\n * need the source samples (e.g. to feed the {@link Worldline} renderer or any\n * other vocoder). {@link KoeEngine} builds its concatenative playback on top of\n * this same class.\n *\n * const bank = await VoiceBank.load(koeBlobOrUrl);\n * const pcm = await bank.getPcm('a'); // Float64 [-1, 1]\n */\nexport class VoiceBank {\n\tprivate constructor(\n\t\t/** The voice bank manifest (sample rate, reference pitch, phoneme table). */\n\t\treadonly manifest: Manifest,\n\t\tprivate source: VoiceSource,\n\t) {}\n\n\t/**\n\t * Parse a .koe archive header + manifest and bind a lazy PCM source.\n\t * @param koe a Blob/File of the .koe archive, or a URL (served with Range support)\n\t */\n\tstatic async load(koe: Blob | string): Promise<VoiceBank> {\n\t\tif (typeof koe === \"string\") {\n\t\t\tconst header = await rangeFetch(koe, 0, 8);\n\t\t\tconst { jsonLength } = parseKoeHeader(header);\n\t\t\tconst json = await rangeFetch(koe, 8, jsonLength);\n\t\t\tconst manifest = JSON.parse(new TextDecoder().decode(json)) as Manifest;\n\t\t\treturn new VoiceBank(\n\t\t\t\tmanifest,\n\t\t\t\tnew RangeVoiceSource(koe, pcmBase(jsonLength)),\n\t\t\t);\n\t\t}\n\t\tconst header = await koe.slice(0, 8).arrayBuffer();\n\t\tconst { jsonLength } = parseKoeHeader(header);\n\t\tconst json = await koe.slice(8, 8 + jsonLength).arrayBuffer();\n\t\tconst manifest = JSON.parse(new TextDecoder().decode(json)) as Manifest;\n\t\treturn new VoiceBank(\n\t\t\tmanifest,\n\t\t\tnew BlobVoiceSource(koe, pcmBase(jsonLength)),\n\t\t);\n\t}\n\n\t/** True if the bank contains a phoneme under this alias. */\n\thas(phoneme: string): boolean {\n\t\treturn this.manifest.phonemes[phoneme] !== undefined;\n\t}\n\n\t/**\n\t * Raw Int16 PCM bytes (48 kHz / mono) for a phoneme, or null if unknown.\n\t * The returned ArrayBuffer is freshly allocated and safe to transfer to a\n\t * worker / AudioWorklet.\n\t */\n\tasync readPcmBytes(phoneme: string): Promise<ArrayBuffer | null> {\n\t\tconst entry = this.manifest.phonemes[phoneme];\n\t\tif (!entry) return null;\n\t\treturn this.source.readBytes(entry.offset, entry.length * 2); // Int16 = 2 bytes/sample\n\t}\n\n\t/**\n\t * A phoneme's PCM as a Float64Array normalised to [-1, 1], or null if unknown.\n\t * Intended for external analysis / resynthesis such as the WORLD vocoder.\n\t */\n\tasync getPcm(phoneme: string): Promise<Float64Array | null> {\n\t\tconst buf = await this.readPcmBytes(phoneme);\n\t\tif (!buf) return null;\n\t\tconst int16 = new Int16Array(buf);\n\t\tconst f64 = new Float64Array(int16.length);\n\t\tfor (let i = 0; i < int16.length; i++) f64[i] = int16[i] / 32768;\n\t\treturn f64;\n\t}\n}\n","import type { Manifest, NoteEvent } from \"../types.js\";\nimport { VoiceBank } from \"./voice-bank.js\";\n\nexport type { NoteEvent };\n\nexport interface KoeEngineOptions {\n\t/** URL to koe-worklet.js. Defaults to './koe-worklet.js'. */\n\tworkletUrl?: string;\n}\n\n/**\n * Main-thread API for the koe concatenative synthesis engine.\n *\n * Loads a single .koe archive. The full voice bank is never held in memory:\n * phonemes are fetched on demand (via {@link VoiceBank}) and cached in the\n * audio thread.\n *\n * const engine = new KoeEngine();\n * await engine.load(koeBlobOrUrl);\n * await engine.play([{ phoneme: 'a', pitch: 440, duration: 48000 }]);\n *\n * For analysis / resynthesis (e.g. {@link Worldline}) you usually only need the\n * raw samples — use {@link VoiceBank} directly instead, which needs no\n * AudioContext or worklet.\n */\nexport class KoeEngine {\n\tprivate ctx: AudioContext;\n\tprivate workletUrl: string;\n\tprivate node: AudioWorkletNode | null = null;\n\tprivate bank: VoiceBank | null = null;\n\tprivate delivered = new Set<string>();\n\tprivate pending = new Map<string, Promise<void>>();\n\n\tconstructor(options: KoeEngineOptions = {}) {\n\t\tthis.ctx = new AudioContext({ sampleRate: 48000 });\n\t\tthis.workletUrl = options.workletUrl ?? \"./koe-worklet.js\";\n\t}\n\n\tget audioContext(): AudioContext {\n\t\treturn this.ctx;\n\t}\n\n\tget manifest(): Manifest | null {\n\t\treturn this.bank?.manifest ?? null;\n\t}\n\n\t/** The underlying voice bank (manifest + on-demand PCM), or null before load(). */\n\tget voiceBank(): VoiceBank | null {\n\t\treturn this.bank;\n\t}\n\n\t/**\n\t * Register the worklet and bind a .koe voice bank.\n\t * @param koe a Blob/File of the .koe archive, or a URL (served with Range support)\n\t */\n\tasync load(koe: Blob | string): Promise<void> {\n\t\tawait this.ctx.audioWorklet.addModule(this.workletUrl);\n\n\t\tthis.bank = await VoiceBank.load(koe);\n\t\tthis.delivered.clear();\n\t\tthis.pending.clear();\n\n\t\tthis.node = new AudioWorkletNode(this.ctx, \"koe-processor\", {\n\t\t\tnumberOfInputs: 0,\n\t\t\tnumberOfOutputs: 1,\n\t\t\toutputChannelCount: [1],\n\t\t});\n\t\tthis.node.port.postMessage({ type: \"init\", manifest: this.bank.manifest });\n\t\tthis.node.connect(this.ctx.destination);\n\n\t\tconsole.log(\n\t\t\t\"[koe] ready —\",\n\t\t\tObject.keys(this.bank.manifest.phonemes).length,\n\t\t\t\"phonemes (on-demand)\",\n\t\t);\n\t}\n\n\t/** Fetch one phoneme's PCM and deliver it to the worklet (deduped, cached). */\n\tprivate ensurePhoneme(name: string): Promise<void> {\n\t\tif (this.delivered.has(name)) return Promise.resolve();\n\t\tconst existing = this.pending.get(name);\n\t\tif (existing) return existing;\n\t\tif (!this.bank || !this.node) return Promise.resolve();\n\n\t\tconst load = this.bank.readPcmBytes(name).then((buf) => {\n\t\t\tif (!buf) return;\n\t\t\tthis.node!.port.postMessage({ type: \"phoneme\", name, buffer: buf }, [\n\t\t\t\tbuf,\n\t\t\t]);\n\t\t\tthis.delivered.add(name);\n\t\t\tthis.pending.delete(name);\n\t\t});\n\n\t\tthis.pending.set(name, load);\n\t\treturn load;\n\t}\n\n\t/** Stop current playback, preload the phonemes for `notes`, then queue them. */\n\tasync play(notes: NoteEvent[]): Promise<void> {\n\t\tif (!this.node) throw new Error(\"KoeEngine: call load() before play()\");\n\t\tthis.node.port.postMessage({ type: \"stop\" });\n\t\tconst names = [...new Set(notes.map((n) => n.phoneme))].filter(Boolean);\n\t\tawait Promise.all(names.map((n) => this.ensurePhoneme(n)));\n\t\tthis.node.port.postMessage({ type: \"play\", notes });\n\t}\n\n\t/** Stop playback and clear the queue. */\n\tstop(): void {\n\t\tthis.node?.port.postMessage({ type: \"stop\" });\n\t}\n\n\t/** Resume the AudioContext if suspended (e.g. after autoplay block). */\n\tasync resume(): Promise<void> {\n\t\tif (this.ctx.state === \"suspended\") await this.ctx.resume();\n\t}\n\n\t/**\n\t * Read a phoneme's raw PCM and return it as a Float64Array normalised to\n\t * [-1, 1]. Convenience that forwards to the underlying {@link VoiceBank}.\n\t * Intended for external analysis such as the WORLD vocoder.\n\t */\n\tasync getPcm(phoneme: string): Promise<Float64Array | null> {\n\t\treturn this.bank?.getPcm(phoneme) ?? null;\n\t}\n}\n","import type { PhonemeEntry } from \"../types.js\";\n\n/** Output sample rate of the worldline synthesizer. */\nexport const WORLDLINE_SAMPLE_RATE = 48000;\n\n/**\n * WORLD needs roughly 85 ms of audio for stable F0 analysis. Phonemes with\n * fewer samples than this are rejected (renderNote returns null).\n */\nexport const MIN_WORLDLINE_SAMPLES = 4096;\n\n/** SynthRequest struct layout — 120 bytes, WASM32. */\nconst SYNTH_REQ_SIZE = 120;\n/** Frame period hardcoded in worldline's phrase_synth.cpp. */\nconst WL_FRAME_MS = 10;\n\n/**\n * The subset of the Emscripten module surface that we call. worldline.js is an\n * MODULARIZE=1 / EXPORT_NAME=WorldlineModule build of OpenUtau's worldline.\n */\ninterface WorldlineWasm {\n\t_PhraseSynthNew(): number;\n\t_PhraseSynthDelete(ps: number): void;\n\t_PhraseSynthAddRequest(\n\t\tps: number,\n\t\treq: number,\n\t\tposMs: number,\n\t\tskipMs: number,\n\t\tlengthMs: number,\n\t\tfadeInMs: number,\n\t\tfadeOutMs: number,\n\t\tflag: number,\n\t): void;\n\t_PhraseSynthSetCurves(\n\t\tps: number,\n\t\tf0: number,\n\t\tgender: number,\n\t\ttension: number,\n\t\tbreathiness: number,\n\t\tvoicing: number,\n\t\tlength: number,\n\t\tframeMs: number,\n\t): void;\n\t_PhraseSynthSynth(ps: number, yPtrPtr: number, flag: number): number;\n\t_malloc(size: number): number;\n\t_free(ptr: number): void;\n\tsetValue(ptr: number, value: number, type: string): void;\n\tgetValue(ptr: number, type: string): number;\n\tHEAPF32: Float32Array;\n\tHEAPF64: Float64Array;\n}\n\ntype WorldlineFactory = (opts?: {\n\tlocateFile?: (path: string) => string;\n}) => Promise<WorldlineWasm>;\n\ndeclare global {\n\t// Injected by worldline.js when loaded as a classic <script> or via importScripts.\n\t// eslint-disable-next-line no-var\n\tvar WorldlineModule: WorldlineFactory | undefined;\n\t// Present in classic Web Workers (used to load worldline.js off the main thread).\n\t// eslint-disable-next-line no-var\n\tvar importScripts: ((...urls: string[]) => void) | undefined;\n}\n\nexport interface WorldlineLoadOptions {\n\t/**\n\t * URL of `worldline.js` (the Emscripten loader). The matching\n\t * `worldline.wasm` must sit next to it — it is resolved relative to this URL.\n\t *\n\t * When consuming koe from npm + a CDN this is typically e.g.\n\t * `https://cdn.jsdelivr.net/npm/@onjmin/koe/dist/world/worldline.js`, or your\n\t * own hosted copy of `dist/world/`.\n\t */\n\tscriptUrl: string;\n}\n\nexport interface RenderNoteParams {\n\t/**\n\t * Source phoneme PCM normalised to [-1, 1] (e.g. from\n\t * `VoiceBank.getPcm()` / `KoeEngine.getPcm()`).\n\t */\n\tpcm: Float64Array;\n\t/** Target output pitch in Hz. */\n\tpitch: number;\n\t/** Sustain / vowel duration in ms (the lead-in below is rendered on top). */\n\tdurationMs: number;\n\t/** Preutterance / lead-in in ms — convert from {@link PhonemeEntry.pre}. */\n\tpreMs: number;\n\t/** Consonant length in ms — convert from {@link PhonemeEntry.consonant}. */\n\tconsonantMs: number;\n\t/** Reference tempo in BPM for worldline's internal timing. Default 120. */\n\ttempo?: number;\n}\n\n/** Convert a sample count at 48 kHz to milliseconds. */\nexport const samplesToMs = (samples: number): number =>\n\t(samples / WORLDLINE_SAMPLE_RATE) * 1000;\n\n/**\n * Derive {@link RenderNoteParams} lead-in / consonant fields from a manifest\n * entry, so callers don't repeat the sample→ms conversion.\n *\n * const params = { pcm, pitch, durationMs, ...leadInFromEntry(entry) };\n */\nexport function leadInFromEntry(entry: PhonemeEntry): {\n\tpreMs: number;\n\tconsonantMs: number;\n} {\n\treturn {\n\t\tpreMs: samplesToMs(entry.pre || 0),\n\t\tconsonantMs: samplesToMs(entry.consonant || 0),\n\t};\n}\n\n// Cache module factories by script URL so repeated loads share one WASM instance.\nconst moduleCache = new Map<string, Promise<WorldlineWasm>>();\n\nfunction injectScript(src: string): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst existing = document.querySelector<HTMLScriptElement>(\n\t\t\t`script[data-koe-worldline=\"${src}\"]`,\n\t\t);\n\t\tif (existing) {\n\t\t\tresolve();\n\t\t\treturn;\n\t\t}\n\t\tconst s = document.createElement(\"script\");\n\t\ts.src = src;\n\t\ts.dataset.koeWorldline = src;\n\t\ts.onload = () => resolve();\n\t\ts.onerror = () => reject(new Error(`worldline: failed to load ${src}`));\n\t\tdocument.head.appendChild(s);\n\t});\n}\n\nfunction loadWasm(scriptUrl: string): Promise<WorldlineWasm> {\n\tconst cached = moduleCache.get(scriptUrl);\n\tif (cached) return cached;\n\n\tconst baseUrl = scriptUrl.slice(0, scriptUrl.lastIndexOf(\"/\") + 1);\n\tconst instantiate = (): Promise<WorldlineWasm> => {\n\t\tconst factory = globalThis.WorldlineModule;\n\t\tif (!factory)\n\t\t\tthrow new Error(\n\t\t\t\t\"worldline: WorldlineModule global was not defined by the script\",\n\t\t\t);\n\t\t// locateFile resolves worldline.wasm next to the script; fetch works in both\n\t\t// the DOM and Web Workers, so the WASM instantiates the same way off-thread.\n\t\treturn factory({ locateFile: (f: string) => baseUrl + f });\n\t};\n\n\tlet promise: Promise<WorldlineWasm>;\n\tif (typeof document !== \"undefined\") {\n\t\t// Main thread / DOM: load worldline.js via a <script> tag.\n\t\tpromise = injectScript(scriptUrl).then(instantiate);\n\t} else if (typeof globalThis.importScripts === \"function\") {\n\t\t// Classic Web Worker: importScripts runs worldline.js synchronously in the\n\t\t// worker's global scope, defining globalThis.WorldlineModule. This lets the\n\t\t// heavy WORLD synthesis run off the main thread.\n\t\tpromise = Promise.resolve().then(() => {\n\t\t\t(globalThis.importScripts as (...urls: string[]) => void)(scriptUrl);\n\t\t\treturn instantiate();\n\t\t});\n\t} else {\n\t\treturn Promise.reject(\n\t\t\tnew Error(\n\t\t\t\t\"Worldline.load requires a DOM or a classic Web Worker (importScripts) to load worldline.js\",\n\t\t\t),\n\t\t);\n\t}\n\tmoduleCache.set(scriptUrl, promise);\n\treturn promise;\n}\n\n/**\n * High-quality WORLD-vocoder note renderer (OpenUtau's worldline via WASM).\n *\n * Pure synthesis: PCM in → PCM out. No AudioContext, no scheduling — the caller\n * owns playback (schedule the returned buffer on its own timeline). Pair it with\n * {@link VoiceBank} for the source samples:\n *\n * const bank = await VoiceBank.load(koeUrl);\n * const wl = await Worldline.load({ scriptUrl: '.../world/worldline.js' });\n *\n * const entry = bank.manifest.phonemes[alias];\n * const pcm = await bank.getPcm(alias);\n * const audio = wl.renderNote({\n * pcm, pitch: 440, durationMs: 500, ...leadInFromEntry(entry),\n * });\n * // audio: Float32 @ 48 kHz, layout [lead-in/consonant ≈ preMs][vowel ≈ durationMs]\n */\nexport class Worldline {\n\treadonly sampleRate = WORLDLINE_SAMPLE_RATE;\n\n\tprivate constructor(private wasm: WorldlineWasm) {}\n\n\t/**\n\t * Load + instantiate the worldline WASM module (deduped per scriptUrl).\n\t *\n\t * Works on the main thread (loads via `<script>`) and inside a classic Web\n\t * Worker (loads via `importScripts`), so the heavy synthesis can run\n\t * off-thread. The matching `worldline.wasm` is fetched next to scriptUrl.\n\t */\n\tstatic async load(options: WorldlineLoadOptions): Promise<Worldline> {\n\t\treturn new Worldline(await loadWasm(options.scriptUrl));\n\t}\n\n\t/**\n\t * Render one note to Float32 PCM at 48 kHz.\n\t *\n\t * The output buffer is laid out as [lead-in/consonant ≈ preMs][vowel ≈\n\t * durationMs], rendered from sample offset 0 (no leading silence). The vowel\n\t * onset (the \"beat\") sits at ≈ preMs into the buffer, so a sequencer should\n\t * place the buffer at `beatTime − preMs` and may trim/crossfade the lead-in.\n\t *\n\t * No internal crossfade is applied — apply fades externally.\n\t *\n\t * @returns Float32 PCM, or null when `pcm` is shorter than\n\t * {@link MIN_WORLDLINE_SAMPLES} (too short for stable F0 analysis).\n\t */\n\trenderNote(params: RenderNoteParams): Float32Array | null {\n\t\tconst { pcm, pitch, durationMs, preMs, consonantMs, tempo = 120 } = params;\n\t\tif (!pcm || pcm.length < MIN_WORLDLINE_SAMPLES) return null;\n\n\t\tconst WL = this.wasm;\n\t\tconst FS = WORLDLINE_SAMPLE_RATE;\n\t\tconst midiNote = Math.round(69 + 12 * Math.log2(pitch / 440));\n\t\tconst posMs = 0; // no leading silence\n\t\tconst reqLen = preMs + durationMs; // render lead-in/consonant + vowel\n\n\t\t// cut_off must trim at least ~2 frames off the tail. PhraseSynth::AddRequest\n\t\t// computes the trim frame count as ceil(in_length_ms)/frame_ms, but\n\t\t// PyinEstimator yields floor(N/nhop)−1 frames — one fewer. With cut_off=0 the\n\t\t// requested frame count exceeds f0_.size() and Model::Trim's f0_.erase() runs\n\t\t// past end(), causing \"memory access out of bounds\". Trimming 2 frames keeps\n\t\t// it within bounds.\n\t\tconst cutMs = WL_FRAME_MS * 2;\n\n\t\tconst ps = WL._PhraseSynthNew();\n\t\tif (!ps) return null;\n\t\tconst reqPtr = WL._malloc(SYNTH_REQ_SIZE);\n\t\tif (!reqPtr) {\n\t\t\tWL._PhraseSynthDelete(ps);\n\t\t\treturn null;\n\t\t}\n\t\tconst samplePtr = WL._malloc(pcm.length * 8);\n\t\tif (!samplePtr) {\n\t\t\tWL._free(reqPtr);\n\t\t\tWL._PhraseSynthDelete(ps);\n\t\t\treturn null;\n\t\t}\n\t\tWL.HEAPF64.set(pcm, samplePtr >> 3);\n\n\t\tconst sv = (off: number, val: number, type: string) =>\n\t\t\tWL.setValue(reqPtr + off, val, type);\n\t\tsv(0, FS, \"i32\");\n\t\tsv(4, pcm.length, \"i32\");\n\t\tsv(8, samplePtr, \"*\");\n\t\tsv(12, 0, \"i32\");\n\t\tsv(16, 0, \"*\");\n\t\tsv(20, midiNote, \"i32\");\n\t\tsv(24, 100.0, \"double\");\n\t\tsv(32, 0.0, \"double\");\n\t\tsv(40, reqLen, \"double\");\n\t\tsv(48, consonantMs, \"double\");\n\t\tsv(56, cutMs, \"double\");\n\t\tsv(64, 100.0, \"double\");\n\t\tsv(72, 0.0, \"double\");\n\t\tsv(80, tempo, \"double\");\n\t\tsv(88, 0, \"i32\");\n\t\tsv(92, 0, \"*\");\n\t\tsv(96, 0, \"i32\");\n\t\tsv(100, 0, \"i32\");\n\t\tsv(104, 100, \"i32\");\n\t\tsv(108, 0, \"i32\");\n\t\tsv(112, 0, \"i32\");\n\t\tsv(116, 100, \"i32\");\n\n\t\t// skip/fadeIn/fadeOut all 0 — crossfade is handled externally.\n\t\tWL._PhraseSynthAddRequest(ps, reqPtr, posMs, 0.0, reqLen, 0.0, 0.0, 0);\n\t\tWL._free(samplePtr);\n\t\tWL._free(reqPtr);\n\n\t\tconst totalMs = posMs + reqLen + WL_FRAME_MS * 2;\n\t\tconst nFrames = Math.ceil(totalMs / WL_FRAME_MS) + 4;\n\t\tconst f0Arr = new Float64Array(nFrames).fill(pitch);\n\t\tconst gArr = new Float64Array(nFrames).fill(0.5);\n\t\tconst tArr = new Float64Array(nFrames).fill(0.5);\n\t\tconst bArr = new Float64Array(nFrames).fill(0.5);\n\t\tconst vArr = new Float64Array(nFrames).fill(1.0);\n\n\t\tconst f0Ptr = WL._malloc(nFrames * 8);\n\t\tconst gPtr = WL._malloc(nFrames * 8);\n\t\tconst tPtr = WL._malloc(nFrames * 8);\n\t\tconst bPtr = WL._malloc(nFrames * 8);\n\t\tconst vPtr = WL._malloc(nFrames * 8);\n\t\tif (!f0Ptr || !gPtr || !tPtr || !bPtr || !vPtr) {\n\t\t\tif (f0Ptr) WL._free(f0Ptr);\n\t\t\tif (gPtr) WL._free(gPtr);\n\t\t\tif (tPtr) WL._free(tPtr);\n\t\t\tif (bPtr) WL._free(bPtr);\n\t\t\tif (vPtr) WL._free(vPtr);\n\t\t\tWL._PhraseSynthDelete(ps);\n\t\t\treturn null;\n\t\t}\n\t\tWL.HEAPF64.set(f0Arr, f0Ptr >> 3);\n\t\tWL.HEAPF64.set(gArr, gPtr >> 3);\n\t\tWL.HEAPF64.set(tArr, tPtr >> 3);\n\t\tWL.HEAPF64.set(bArr, bPtr >> 3);\n\t\tWL.HEAPF64.set(vArr, vPtr >> 3);\n\t\tWL._PhraseSynthSetCurves(\n\t\t\tps,\n\t\t\tf0Ptr,\n\t\t\tgPtr,\n\t\t\ttPtr,\n\t\t\tbPtr,\n\t\t\tvPtr,\n\t\t\tnFrames,\n\t\t\tWL_FRAME_MS,\n\t\t);\n\t\tWL._free(f0Ptr);\n\t\tWL._free(gPtr);\n\t\tWL._free(tPtr);\n\t\tWL._free(bPtr);\n\t\tWL._free(vPtr);\n\n\t\tconst yPtrPtr = WL._malloc(4);\n\t\tif (!yPtrPtr) {\n\t\t\tWL._PhraseSynthDelete(ps);\n\t\t\treturn null;\n\t\t}\n\t\tconst outLen = WL._PhraseSynthSynth(ps, yPtrPtr, 0);\n\t\tconst yPtr = WL.getValue(yPtrPtr, \"*\");\n\t\tconst audio =\n\t\t\toutLen > 0\n\t\t\t\t? new Float32Array(WL.HEAPF32.buffer, yPtr, outLen).slice()\n\t\t\t\t: null;\n\t\tWL._free(yPtrPtr);\n\t\tWL._PhraseSynthDelete(ps);\n\t\treturn audio;\n\t}\n}\n","export interface OtoEntry {\n\t/** Source WAV filename */\n\twav: string;\n\t/** Phoneme alias */\n\talias: string;\n\t/** Left blank — offset from WAV start (ms) */\n\toffset: number;\n\t/** Consonant portion end from offset (ms) */\n\tconsonant: number;\n\t/** Right blank — negative = from WAV end, positive = from offset (ms) */\n\tcutoff: number;\n\t/** Preutterance from offset (ms) */\n\tpre: number;\n\t/** Overlap / crossfade region (ms) */\n\toverlap: number;\n}\n\n/**\n * Parse oto.ini content (already decoded to UTF-8 string).\n * Silently skips malformed lines.\n */\nexport function parseOto(content: string): OtoEntry[] {\n\tconst entries: OtoEntry[] = [];\n\n\tfor (const raw of content.split(/\\r?\\n/)) {\n\t\tconst line = raw.trim();\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst eq = line.indexOf(\"=\");\n\t\tif (eq === -1) continue;\n\n\t\tconst wav = line.slice(0, eq).trim();\n\t\tconst parts = line.slice(eq + 1).split(\",\");\n\t\tif (parts.length < 6) continue;\n\n\t\tconst [alias, offsetStr, consonantStr, cutoffStr, preStr, overlapStr] =\n\t\t\tparts;\n\t\t// Empty alias → use filename without extension (UTAU spec default)\n\t\tconst aliasStr = alias.trim() || wav.replace(/\\.[^.]+$/, \"\");\n\t\tconst entry: OtoEntry = {\n\t\t\twav,\n\t\t\talias: aliasStr,\n\t\t\toffset: parseFloat(offsetStr) || 0,\n\t\t\tconsonant: parseFloat(consonantStr) || 0,\n\t\t\tcutoff: parseFloat(cutoffStr) || 0,\n\t\t\tpre: parseFloat(preStr) || 0,\n\t\t\toverlap: parseFloat(overlapStr) || 0,\n\t\t};\n\n\t\tif (!entry.alias) continue;\n\t\tentries.push(entry);\n\t}\n\n\treturn entries;\n}\n","export interface WavData {\n\tsampleRate: number;\n\tchannels: number;\n\t/** Normalized samples in [-1, 1], interleaved if multi-channel */\n\tsamples: Float32Array;\n}\n\n/** Parse a WAV file from an ArrayBuffer. Supports PCM 8/16/24-bit and IEEE float 32-bit. */\nexport function parseWav(buf: ArrayBuffer): WavData {\n\tconst view = new DataView(buf);\n\n\tconst riff = readFourCC(view, 0);\n\tif (riff !== \"RIFF\") throw new Error(`Not a RIFF file (got \"${riff}\")`);\n\n\tlet sampleRate = 0;\n\tlet channels = 0;\n\tlet bitsPerSample = 0;\n\tlet audioFormat = 1;\n\tlet dataOffset = 0;\n\tlet dataLength = 0;\n\n\tlet pos = 12;\n\twhile (pos < view.byteLength - 8) {\n\t\tconst id = readFourCC(view, pos);\n\t\tconst size = view.getUint32(pos + 4, true);\n\t\tpos += 8;\n\n\t\tif (id === \"fmt \") {\n\t\t\taudioFormat = view.getUint16(pos, true);\n\t\t\tchannels = view.getUint16(pos + 2, true);\n\t\t\tsampleRate = view.getUint32(pos + 4, true);\n\t\t\tbitsPerSample = view.getUint16(pos + 14, true);\n\t\t} else if (id === \"data\") {\n\t\t\tdataOffset = pos;\n\t\t\tdataLength = size;\n\t\t\tbreak;\n\t\t}\n\n\t\tpos += size + (size & 1);\n\t}\n\n\tif (!dataOffset) throw new Error(\"WAV has no data chunk\");\n\tif (!channels || !sampleRate) throw new Error(\"WAV fmt chunk missing\");\n\n\tconst bytesPerSample = bitsPerSample >> 3;\n\tconst totalSamples = Math.floor(dataLength / bytesPerSample);\n\tconst samples = new Float32Array(totalSamples);\n\n\tfor (let i = 0; i < totalSamples; i++) {\n\t\tconst p = dataOffset + i * bytesPerSample;\n\t\tif (audioFormat === 3) {\n\t\t\tsamples[i] = view.getFloat32(p, true);\n\t\t} else if (bitsPerSample === 8) {\n\t\t\tsamples[i] = (view.getUint8(p) - 128) / 128;\n\t\t} else if (bitsPerSample === 16) {\n\t\t\tsamples[i] = view.getInt16(p, true) / 32768;\n\t\t} else if (bitsPerSample === 24) {\n\t\t\tconst lo = view.getUint8(p) | (view.getUint8(p + 1) << 8);\n\t\t\tlet hi = view.getUint8(p + 2);\n\t\t\tif (hi & 0x80) hi = hi | 0xffffff00;\n\t\t\tsamples[i] = ((hi << 16) | lo) / 8388608;\n\t\t}\n\t}\n\n\treturn { sampleRate, channels, samples };\n}\n\n/** Mix down to mono by averaging all channels. */\nexport function toMono(wav: WavData): WavData {\n\tif (wav.channels === 1) return wav;\n\tconst len = wav.samples.length / wav.channels;\n\tconst out = new Float32Array(len);\n\tfor (let i = 0; i < len; i++) {\n\t\tlet sum = 0;\n\t\tfor (let c = 0; c < wav.channels; c++)\n\t\t\tsum += wav.samples[i * wav.channels + c];\n\t\tout[i] = sum / wav.channels;\n\t}\n\treturn { sampleRate: wav.sampleRate, channels: 1, samples: out };\n}\n\n/** Linear interpolation resample to targetRate. Expects mono input. */\nexport function resample(wav: WavData, targetRate: number): WavData {\n\tif (wav.sampleRate === targetRate) return wav;\n\tconst ratio = wav.sampleRate / targetRate;\n\tconst outLen = Math.floor(wav.samples.length / ratio);\n\tconst out = new Float32Array(outLen);\n\tconst src = wav.samples;\n\tfor (let i = 0; i < outLen; i++) {\n\t\tconst x = i * ratio;\n\t\tconst xi = Math.floor(x);\n\t\tconst frac = x - xi;\n\t\tout[i] = (src[xi] ?? 0) + ((src[xi + 1] ?? 0) - (src[xi] ?? 0)) * frac;\n\t}\n\treturn { sampleRate: targetRate, channels: 1, samples: out };\n}\n\n/** Convert Float32 [-1,1] samples to Int16 PCM. */\nexport function toInt16(samples: Float32Array): Int16Array {\n\tconst out = new Int16Array(samples.length);\n\tfor (let i = 0; i < samples.length; i++) {\n\t\tout[i] = Math.round(Math.max(-1, Math.min(1, samples[i])) * 32767);\n\t}\n\treturn out;\n}\n\n/** Normalize then convert a WAV to 48kHz/16bit/mono Int16 PCM. */\nexport function normalizePcm(buf: ArrayBuffer): Int16Array {\n\tconst wav = parseWav(buf);\n\tconst mono = toMono(wav);\n\tconst resampled = resample(mono, 48000);\n\treturn toInt16(resampled.samples);\n}\n\nfunction readFourCC(view: DataView, pos: number): string {\n\treturn String.fromCharCode(\n\t\tview.getUint8(pos),\n\t\tview.getUint8(pos + 1),\n\t\tview.getUint8(pos + 2),\n\t\tview.getUint8(pos + 3),\n\t);\n}\n","const SAMPLE_RATE = 48000;\n\nconst NAME_SEMITONE: Record<string, number> = {\n\tc: 0,\n\td: 2,\n\te: 4,\n\tf: 5,\n\tg: 7,\n\ta: 9,\n\tb: 11,\n};\n\n/** Parse a note name like \"E4\", \"G#4\", \"Db5\" → frequency in Hz (null if invalid). */\nexport function noteNameToHz(name: string): number | null {\n\tconst m = /^([A-Ga-g])([#b]?)(-?\\d+)$/.exec(name);\n\tif (!m) return null;\n\tlet semi = NAME_SEMITONE[m[1].toLowerCase()];\n\tif (m[2] === \"#\") semi++;\n\telse if (m[2] === \"b\") semi--;\n\tconst midi = (parseInt(m[3], 10) + 1) * 12 + semi;\n\treturn 440 * 2 ** ((midi - 69) / 12);\n}\n\n/** Recorded pitch encoded in a multi-pitch alias suffix: \"a い_E4\" → 329.63 Hz. */\nexport function pitchFromAliasSuffix(alias: string): number | null {\n\tconst m = /_([A-Ga-g][#b]?-?\\d+)$/.exec(alias);\n\treturn m ? noteNameToHz(m[1]) : null;\n}\n\n/**\n * Estimate the fundamental frequency (Hz) of a voiced region by normalized\n * autocorrelation. Returns 0 when no clear pitch is found (unvoiced consonant,\n * silence, or a region too short to analyse).\n *\n * The signal is decimated to a lower analysis rate for speed; f0 below ~700 Hz\n * is well within the resulting Nyquist limit. A parabolic interpolation around\n * the best lag gives sub-sample (sub-semitone) accuracy.\n */\nexport function detectF0(pcm: Int16Array, start: number, end: number): number {\n\tconst DECIM = 4;\n\tconst sr = SAMPLE_RATE / DECIM; // 12 kHz analysis rate\n\tconst minLag = Math.floor(sr / 700); // highest f0 we look for\n\tconst maxLag = Math.floor(sr / 70); // lowest f0 we look for\n\n\tconst outLen = Math.floor((end - start) / DECIM);\n\tif (outLen < maxLag + 2) return 0;\n\tconst win = Math.min(outLen, 1500); // up to ~0.125 s of vowel\n\n\t// Decimate (box-average) + remove DC, and build a prefix sum of squares so\n\t// each lag's window energy is O(1).\n\tconst buf = new Float32Array(win);\n\tlet mean = 0;\n\tfor (let i = 0; i < win; i++) {\n\t\tlet s = 0;\n\t\tconst base = start + i * DECIM;\n\t\tfor (let j = 0; j < DECIM; j++) s += pcm[base + j];\n\t\tbuf[i] = s;\n\t\tmean += s;\n\t}\n\tmean /= win;\n\n\tconst sq = new Float64Array(win + 1);\n\tfor (let i = 0; i < win; i++) {\n\t\tbuf[i] -= mean;\n\t\tsq[i + 1] = sq[i] + buf[i] * buf[i];\n\t}\n\tif (sq[win] < 1) return 0; // effectively silent\n\n\t// Normalized autocorrelation: 2·r(lag) / (E[0..n] + E[lag..lag+n]) ∈ [-1, 1].\n\tconst norm = (lag: number): number => {\n\t\tconst n = win - lag;\n\t\tlet r = 0;\n\t\tfor (let i = 0; i < n; i++) r += buf[i] * buf[i + lag];\n\t\tconst e = sq[n] + (sq[lag + n] - sq[lag]);\n\t\treturn e > 0 ? (2 * r) / e : 0;\n\t};\n\n\tlet bestLag = -1;\n\tlet best = 0;\n\tfor (let lag = minLag; lag <= maxLag; lag++) {\n\t\tconst v = norm(lag);\n\t\tif (v > best) {\n\t\t\tbest = v;\n\t\t\tbestLag = lag;\n\t\t}\n\t}\n\tif (bestLag < 1 || best < 0.4) return 0; // unvoiced / no clear period\n\n\t// Parabolic interpolation around the peak\n\tconst y0 = norm(bestLag - 1);\n\tconst y1 = best;\n\tconst y2 = norm(bestLag + 1);\n\tconst denom = y0 - 2 * y1 + y2;\n\tconst shift = denom !== 0 ? (0.5 * (y0 - y2)) / denom : 0;\n\n\treturn sr / (bestLag + shift);\n}\n","import type { Manifest, PhonemeEntry } from \"../types.js\";\nimport type { OtoEntry } from \"./parse-oto.js\";\nimport { detectF0 } from \"./pitch.js\";\n\nconst TARGET_RATE = 48000;\n\nfunction msToSamples(ms: number): number {\n\treturn Math.round((ms / 1000) * TARGET_RATE);\n}\n\nconst clamp = (v: number, lo: number, hi: number) =>\n\tMath.max(lo, Math.min(hi, v));\n\nexport interface PackInput {\n\toto: OtoEntry;\n\t/** Full normalized PCM of the source WAV (48kHz / 16bit / mono) */\n\tpcm: Int16Array;\n\t/** Known recorded pitch in Hz (e.g. from the .frq file). 0/undefined → auto-detect. */\n\trecordedPitch?: number;\n}\n\nexport interface PackOutput {\n\tmanifest: Manifest;\n\t/** Raw PCM blob — Int16 / 48kHz / mono */\n\tbin: ArrayBuffer;\n}\n\nexport interface TrimmedPhoneme {\n\t/** PCM trimmed to the oto region [offset, cutoff] */\n\tpcm: Int16Array;\n\t/** Manifest params relative to the trimmed start (sample 0 = oto offset) */\n\tentry: Omit<PhonemeEntry, \"offset\">;\n}\n\n/**\n * Cut the full WAV PCM down to its usable oto region and recompute parameters\n * relative to the trimmed start.\n *\n * UTAU oto.ini values are all in ms and measured from `offset` (the left blank),\n * except `cutoff` (right blank):\n * - cutoff >= 0 : measured from the END of the file\n * - cutoff < 0 : region length from offset = |cutoff|\n *\n * After trimming, sample 0 == oto offset, so pre/overlap/consonant carry over\n * unchanged (just converted to samples), and the slice length is the region end.\n */\nexport function trimToOto(\n\tpcm: Int16Array,\n\toto: OtoEntry,\n\trecordedPitch = 0,\n): TrimmedPhoneme {\n\tconst full = pcm.length;\n\n\tconst start = clamp(msToSamples(oto.offset), 0, full);\n\tconst end =\n\t\toto.cutoff < 0\n\t\t\t? clamp(start + msToSamples(-oto.cutoff), start, full)\n\t\t\t: clamp(full - msToSamples(oto.cutoff), start, full);\n\n\tconst slice = pcm.subarray(start, end);\n\tconst length = slice.length;\n\n\tconst pre = clamp(msToSamples(oto.pre), 0, length);\n\tconst overlap = clamp(msToSamples(oto.overlap), 0, length);\n\tconst consonant = clamp(msToSamples(oto.consonant), 0, length);\n\n\t// Prefer the known recorded pitch (from the .frq file or alias suffix); only\n\t// fall back to autocorrelation on the sustained vowel when none is available.\n\tconst pitch =\n\t\trecordedPitch > 0\n\t\t\t? recordedPitch\n\t\t\t: detectF0(\n\t\t\t\t\tslice,\n\t\t\t\t\tMath.min(Math.max(pre, consonant), Math.max(0, length - 1)),\n\t\t\t\t\tlength,\n\t\t\t\t);\n\n\treturn {\n\t\tpcm: slice,\n\t\tentry: { length, pre, overlap, consonant, pitch },\n\t};\n}\n\n/**\n * Pack normalized PCM phonemes into voice.bin + manifest.json.\n * Each phoneme is trimmed to its oto region first.\n * Duplicate aliases are silently overwritten by the later entry.\n */\nexport function pack(inputs: PackInput[], referencePitch = 220): PackOutput {\n\tconst phonemes: Record<string, PhonemeEntry> = {};\n\tconst chunks: Int16Array[] = [];\n\tlet byteOffset = 0;\n\n\tfor (const { oto, pcm, recordedPitch } of inputs) {\n\t\tconst { pcm: slice, entry } = trimToOto(pcm, oto, recordedPitch);\n\t\tif (slice.length === 0) continue;\n\n\t\tphonemes[oto.alias] = { offset: byteOffset, ...entry };\n\t\tbyteOffset += slice.byteLength;\n\t\tchunks.push(slice);\n\t}\n\n\tconst bin = new ArrayBuffer(byteOffset);\n\tconst view = new Uint8Array(bin);\n\tlet pos = 0;\n\tfor (const chunk of chunks) {\n\t\tview.set(\n\t\t\tnew Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),\n\t\t\tpos,\n\t\t);\n\t\tpos += chunk.byteLength;\n\t}\n\n\tconst manifest: Manifest = {\n\t\tsampleRate: 48000,\n\t\treferencePitch,\n\t\tphonemes,\n\t};\n\n\treturn { manifest, bin };\n}\n","/**\n * UTAU `.frq` frequency-analysis files (FREQ0003). The same format OpenUtau\n * reads: an 8-byte header, hop size, then the average fundamental frequency of\n * the recording — exactly the reference pitch we need for correct resampling.\n *\n * Layout:\n * char[8] \"FREQ0003\"\n * int32 hopSize\n * float64 averageF0 ← the recorded pitch in Hz\n * byte[16] (blank)\n * int32 length\n * { float64 f0, float64 amp } × length\n */\nexport function parseFrqAverageF0(buffer: ArrayBuffer): number | null {\n\tif (buffer.byteLength < 20) return null;\n\tconst view = new DataView(buffer);\n\n\tlet header = \"\";\n\tfor (let i = 0; i < 8; i++) header += String.fromCharCode(view.getUint8(i));\n\tif (header !== \"FREQ0003\") return null;\n\n\tconst avg = view.getFloat64(12, true); // little-endian\n\treturn Number.isFinite(avg) && avg > 0 ? avg : null;\n}\n\n/** Map a WAV filename to its sibling frq filename: \"あ.wav\" → \"あ_wav.frq\". */\nexport function frqFileName(wavName: string): string {\n\tconst dot = wavName.lastIndexOf(\".\");\n\tconst base = dot >= 0 ? wavName.slice(0, dot) : wavName;\n\tconst ext = dot >= 0 ? wavName.slice(dot + 1) : \"wav\";\n\treturn `${base}_${ext}.frq`;\n}\n"],"mappings":"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","names":["header","jsonLength","json","manifest"]}
|
package/dist/koe-convert.js
CHANGED
|
@@ -89,7 +89,8 @@ function toMono(wav) {
|
|
|
89
89
|
const out = new Float32Array(len);
|
|
90
90
|
for (let i = 0; i < len; i++) {
|
|
91
91
|
let sum = 0;
|
|
92
|
-
for (let c = 0; c < wav.channels; c++)
|
|
92
|
+
for (let c = 0; c < wav.channels; c++)
|
|
93
|
+
sum += wav.samples[i * wav.channels + c];
|
|
93
94
|
out[i] = sum / wav.channels;
|
|
94
95
|
}
|
|
95
96
|
return { sampleRate: wav.sampleRate, channels: 1, samples: out };
|
|
@@ -132,7 +133,15 @@ function readFourCC(view, pos) {
|
|
|
132
133
|
|
|
133
134
|
// src/converter/pitch.ts
|
|
134
135
|
var SAMPLE_RATE = 48e3;
|
|
135
|
-
var NAME_SEMITONE = {
|
|
136
|
+
var NAME_SEMITONE = {
|
|
137
|
+
c: 0,
|
|
138
|
+
d: 2,
|
|
139
|
+
e: 4,
|
|
140
|
+
f: 5,
|
|
141
|
+
g: 7,
|
|
142
|
+
a: 9,
|
|
143
|
+
b: 11
|
|
144
|
+
};
|
|
136
145
|
function noteNameToHz(name) {
|
|
137
146
|
const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec(name);
|
|
138
147
|
if (!m) return null;
|
|
@@ -210,7 +219,11 @@ function trimToOto(pcm, oto, recordedPitch = 0) {
|
|
|
210
219
|
const pre = clamp(msToSamples(oto.pre), 0, length);
|
|
211
220
|
const overlap = clamp(msToSamples(oto.overlap), 0, length);
|
|
212
221
|
const consonant = clamp(msToSamples(oto.consonant), 0, length);
|
|
213
|
-
const pitch = recordedPitch > 0 ? recordedPitch : detectF0(
|
|
222
|
+
const pitch = recordedPitch > 0 ? recordedPitch : detectF0(
|
|
223
|
+
slice,
|
|
224
|
+
Math.min(Math.max(pre, consonant), Math.max(0, length - 1)),
|
|
225
|
+
length
|
|
226
|
+
);
|
|
214
227
|
return {
|
|
215
228
|
pcm: slice,
|
|
216
229
|
entry: { length, pre, overlap, consonant, pitch }
|
|
@@ -231,7 +244,10 @@ function pack(inputs, referencePitch = 220) {
|
|
|
231
244
|
const view = new Uint8Array(bin);
|
|
232
245
|
let pos = 0;
|
|
233
246
|
for (const chunk of chunks) {
|
|
234
|
-
view.set(
|
|
247
|
+
view.set(
|
|
248
|
+
new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
|
|
249
|
+
pos
|
|
250
|
+
);
|
|
235
251
|
pos += chunk.byteLength;
|
|
236
252
|
}
|
|
237
253
|
const manifest = {
|
|
@@ -315,8 +331,10 @@ async function main() {
|
|
|
315
331
|
inputs.push({ oto, pcm, recordedPitch });
|
|
316
332
|
} catch (err) {
|
|
317
333
|
const msg = err instanceof Error ? err.message : String(err);
|
|
318
|
-
process.stderr.write(
|
|
319
|
-
`)
|
|
334
|
+
process.stderr.write(
|
|
335
|
+
`[skip] ${oto.alias} (${basename(wavPath)}): ${msg}
|
|
336
|
+
`
|
|
337
|
+
);
|
|
320
338
|
skipped++;
|
|
321
339
|
}
|
|
322
340
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "onjmin",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "@onjmin/koe",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.3",
|
|
6
6
|
"description": "UTAU concatenative synthesis engine optimized for browser / mobile via WebAssembly + AudioWorklet",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./dist/index.js",
|
|
@@ -20,17 +20,12 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"lint": "biome check src",
|
|
30
|
-
"clean": "rimraf dist"
|
|
31
|
-
},
|
|
32
|
-
"keywords": ["utau", "singing-synthesis", "audioworklet", "wasm"],
|
|
33
|
-
"packageManager": "pnpm@10.17.1",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"utau",
|
|
25
|
+
"singing-synthesis",
|
|
26
|
+
"audioworklet",
|
|
27
|
+
"wasm"
|
|
28
|
+
],
|
|
34
29
|
"devDependencies": {
|
|
35
30
|
"@biomejs/biome": "2.5.0",
|
|
36
31
|
"@hono/node-server": "2.0.5",
|
|
@@ -39,10 +34,23 @@
|
|
|
39
34
|
"rimraf": "6.1.3",
|
|
40
35
|
"tsup": "8.5.1",
|
|
41
36
|
"tsx": "4.22.4",
|
|
37
|
+
"typedoc": "0.28.19",
|
|
42
38
|
"typescript": "6.0.3"
|
|
43
39
|
},
|
|
44
40
|
"volta": {
|
|
45
41
|
"node": "24.16.0",
|
|
46
42
|
"pnpm": "11.7.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"dev": "tsx src/dev-server.ts",
|
|
47
|
+
"watch": "tsup --watch",
|
|
48
|
+
"convert": "tsx src/converter/cli.ts",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"lint": "biome check src",
|
|
51
|
+
"lint2": "biome format src --write",
|
|
52
|
+
"clean": "rimraf dist",
|
|
53
|
+
"patch": "pnpm version patch",
|
|
54
|
+
"publish-dangerous-dont-use-this-command": "pnpm publish --access public"
|
|
47
55
|
}
|
|
48
|
-
}
|
|
56
|
+
}
|