@onjmin/dtm 0.1.47 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +55 -2
- package/dist/index.d.ts +55 -2
- package/dist/index.js +408 -51
- package/dist/index.mjs +405 -51
- package/dist/voice-worker.js +90 -25
- package/package.json +3 -3
package/dist/voice-worker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
-
// node_modules/.pnpm/@onjmin+koe@1.0.
|
|
3
|
+
// node_modules/.pnpm/@onjmin+koe@1.0.4/node_modules/@onjmin/koe/dist/index.js
|
|
4
4
|
var MAGIC = 1263486208;
|
|
5
5
|
function parseKoeHeader(headerBytes) {
|
|
6
6
|
const view = new DataView(headerBytes);
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
return { jsonLength: view.getUint32(4, true) };
|
|
11
11
|
}
|
|
12
12
|
var pcmBase = (jsonLength) => 8 + jsonLength;
|
|
13
|
+
var MAX_PHONEME_SAMPLES = 5242880;
|
|
14
|
+
var MAX_JSON_LENGTH = 50 * 1024 * 1024;
|
|
13
15
|
var BlobVoiceSource = class {
|
|
14
16
|
constructor(blob, base) {
|
|
15
17
|
this.blob = blob;
|
|
@@ -31,22 +33,60 @@
|
|
|
31
33
|
base;
|
|
32
34
|
async readBytes(offset, length) {
|
|
33
35
|
const start = this.base + offset;
|
|
34
|
-
|
|
35
|
-
headers: { Range: `bytes=${start}-${start + length - 1}` }
|
|
36
|
-
});
|
|
37
|
-
if (!res.ok && res.status !== 206) {
|
|
38
|
-
throw new Error(`.koe range request failed: ${res.status}`);
|
|
39
|
-
}
|
|
40
|
-
return res.arrayBuffer();
|
|
36
|
+
return rangeFetch(this.url, start, length);
|
|
41
37
|
}
|
|
42
38
|
};
|
|
43
39
|
async function rangeFetch(url, start, length) {
|
|
44
40
|
const res = await fetch(url, {
|
|
45
|
-
headers: { Range: `bytes=${start}-${start + length - 1}` }
|
|
41
|
+
headers: { Range: `bytes=${start}-${start + length - 1}` },
|
|
42
|
+
credentials: "omit"
|
|
43
|
+
// never leak cookies / auth to a MML-supplied URL
|
|
46
44
|
});
|
|
47
|
-
if (
|
|
48
|
-
throw new Error(
|
|
49
|
-
|
|
45
|
+
if (res.status !== 206) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`.koe fetch failed: expected 206 Partial Content, got ${res.status}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return readCapped(res, length);
|
|
51
|
+
}
|
|
52
|
+
async function readCapped(res, length) {
|
|
53
|
+
const reader = res.body?.getReader();
|
|
54
|
+
if (!reader) {
|
|
55
|
+
const buf = await res.arrayBuffer();
|
|
56
|
+
if (buf.byteLength > length) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`.koe fetch failed: response exceeds requested ${length} bytes`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return buf;
|
|
62
|
+
}
|
|
63
|
+
const out = new Uint8Array(length);
|
|
64
|
+
let received = 0;
|
|
65
|
+
for (; ; ) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done) break;
|
|
68
|
+
if (received + value.byteLength > length) {
|
|
69
|
+
await reader.cancel();
|
|
70
|
+
throw new Error(
|
|
71
|
+
`.koe fetch failed: response exceeds requested ${length} bytes`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
out.set(value, received);
|
|
75
|
+
received += value.byteLength;
|
|
76
|
+
}
|
|
77
|
+
return received === length ? out.buffer : out.buffer.slice(0, received);
|
|
78
|
+
}
|
|
79
|
+
function validateJsonLength(jsonLength) {
|
|
80
|
+
if (!Number.isInteger(jsonLength) || jsonLength < 0 || jsonLength > MAX_JSON_LENGTH) {
|
|
81
|
+
throw new Error(`manifest JSON length out of bounds: ${jsonLength}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function parseManifest(json) {
|
|
85
|
+
const manifest = JSON.parse(new TextDecoder().decode(json));
|
|
86
|
+
if (!manifest || typeof manifest !== "object" || typeof manifest.phonemes !== "object" || manifest.phonemes === null) {
|
|
87
|
+
throw new Error("invalid manifest: missing phonemes table");
|
|
88
|
+
}
|
|
89
|
+
return manifest;
|
|
50
90
|
}
|
|
51
91
|
var VoiceBank = class _VoiceBank {
|
|
52
92
|
constructor(manifest, source) {
|
|
@@ -60,20 +100,41 @@
|
|
|
60
100
|
* @param koe a Blob/File of the .koe archive, or a URL (served with Range support)
|
|
61
101
|
*/
|
|
62
102
|
static async load(koe) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
try {
|
|
104
|
+
if (typeof koe === "string") {
|
|
105
|
+
if (/^blob:/i.test(koe)) {
|
|
106
|
+
const res = await fetch(koe);
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
throw new Error(`blob: URL fetch failed: ${res.status}`);
|
|
109
|
+
}
|
|
110
|
+
return await _VoiceBank.fromBlob(await res.blob());
|
|
111
|
+
}
|
|
112
|
+
if (!/^https?:/i.test(koe)) {
|
|
113
|
+
throw new Error(`unsupported URL protocol: ${koe}`);
|
|
114
|
+
}
|
|
115
|
+
const header = await rangeFetch(koe, 0, 8);
|
|
116
|
+
const { jsonLength } = parseKoeHeader(header);
|
|
117
|
+
validateJsonLength(jsonLength);
|
|
118
|
+
const json = await rangeFetch(koe, 8, jsonLength);
|
|
119
|
+
const manifest = parseManifest(json);
|
|
120
|
+
return new _VoiceBank(
|
|
121
|
+
manifest,
|
|
122
|
+
new RangeVoiceSource(koe, pcmBase(jsonLength))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return await _VoiceBank.fromBlob(koe);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Failed to load .koe voice bank: ${error instanceof Error ? error.message : String(error)}`
|
|
71
129
|
);
|
|
72
130
|
}
|
|
131
|
+
}
|
|
132
|
+
static async fromBlob(koe) {
|
|
73
133
|
const header = await koe.slice(0, 8).arrayBuffer();
|
|
74
134
|
const { jsonLength } = parseKoeHeader(header);
|
|
135
|
+
validateJsonLength(jsonLength);
|
|
75
136
|
const json = await koe.slice(8, 8 + jsonLength).arrayBuffer();
|
|
76
|
-
const manifest =
|
|
137
|
+
const manifest = parseManifest(json);
|
|
77
138
|
return new _VoiceBank(
|
|
78
139
|
manifest,
|
|
79
140
|
new BlobVoiceSource(koe, pcmBase(jsonLength))
|
|
@@ -81,7 +142,7 @@
|
|
|
81
142
|
}
|
|
82
143
|
/** True if the bank contains a phoneme under this alias. */
|
|
83
144
|
has(phoneme) {
|
|
84
|
-
return this.manifest.phonemes
|
|
145
|
+
return Object.hasOwn(this.manifest.phonemes, phoneme);
|
|
85
146
|
}
|
|
86
147
|
/**
|
|
87
148
|
* Raw Int16 PCM bytes (48 kHz / mono) for a phoneme, or null if unknown.
|
|
@@ -89,8 +150,11 @@
|
|
|
89
150
|
* worker / AudioWorklet.
|
|
90
151
|
*/
|
|
91
152
|
async readPcmBytes(phoneme) {
|
|
153
|
+
if (!Object.hasOwn(this.manifest.phonemes, phoneme)) return null;
|
|
92
154
|
const entry = this.manifest.phonemes[phoneme];
|
|
93
|
-
if (!entry)
|
|
155
|
+
if (!Number.isInteger(entry.offset) || !Number.isInteger(entry.length) || entry.offset < 0 || entry.length < 0 || entry.length > MAX_PHONEME_SAMPLES) {
|
|
156
|
+
throw new Error(`manifest entry out of bounds for phoneme: ${phoneme}`);
|
|
157
|
+
}
|
|
94
158
|
return this.source.readBytes(entry.offset, entry.length * 2);
|
|
95
159
|
}
|
|
96
160
|
/**
|
|
@@ -100,7 +164,7 @@
|
|
|
100
164
|
async getPcm(phoneme) {
|
|
101
165
|
const buf = await this.readPcmBytes(phoneme);
|
|
102
166
|
if (!buf) return null;
|
|
103
|
-
const int16 = new Int16Array(buf);
|
|
167
|
+
const int16 = new Int16Array(buf, 0, Math.floor(buf.byteLength / 2));
|
|
104
168
|
const f64 = new Float64Array(int16.length);
|
|
105
169
|
for (let i = 0; i < int16.length; i++) f64[i] = int16[i] / 32768;
|
|
106
170
|
return f64;
|
|
@@ -291,7 +355,8 @@
|
|
|
291
355
|
}
|
|
292
356
|
const outLen = WL._PhraseSynthSynth(ps, yPtrPtr, 0);
|
|
293
357
|
const yPtr = WL.getValue(yPtrPtr, "*");
|
|
294
|
-
const audio = outLen > 0 ? new Float32Array(WL.HEAPF32.buffer, yPtr, outLen).slice() : null;
|
|
358
|
+
const audio = outLen > 0 && yPtr ? new Float32Array(WL.HEAPF32.buffer, yPtr, outLen).slice() : null;
|
|
359
|
+
if (yPtr) WL._free(yPtr);
|
|
295
360
|
WL._free(yPtrPtr);
|
|
296
361
|
WL._PhraseSynthDelete(ps);
|
|
297
362
|
return audio;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "onjmin",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "@onjmin/dtm",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.49",
|
|
6
6
|
"description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
|
|
7
7
|
"homepage": "https://onjmin.github.io/dtm",
|
|
8
8
|
"repository": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"esbuild": "0.28.1",
|
|
49
49
|
"hono": "4.10.1",
|
|
50
50
|
"rimraf": "6.0.1",
|
|
51
|
-
"tsup": "8.5.
|
|
51
|
+
"tsup": "8.5.1",
|
|
52
52
|
"tsx": "4.20.6",
|
|
53
53
|
"typedoc": "0.28.19",
|
|
54
54
|
"typescript": "5.8.3"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@onjmin/chord-parser": "1.0.2",
|
|
58
58
|
"@onjmin/dtm": "0.1.14",
|
|
59
|
-
"@onjmin/koe": "1.0.
|
|
59
|
+
"@onjmin/koe": "1.0.4",
|
|
60
60
|
"midi-json-parser": "8.1.74"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|