@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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  (() => {
3
- // node_modules/.pnpm/@onjmin+koe@1.0.3/node_modules/@onjmin/koe/dist/index.js
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
- const res = await fetch(this.url, {
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 (!res.ok && res.status !== 206)
48
- throw new Error(`.koe fetch failed: ${res.status}`);
49
- return res.arrayBuffer();
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
- if (typeof koe === "string") {
64
- const header2 = await rangeFetch(koe, 0, 8);
65
- const { jsonLength: jsonLength2 } = parseKoeHeader(header2);
66
- const json2 = await rangeFetch(koe, 8, jsonLength2);
67
- const manifest2 = JSON.parse(new TextDecoder().decode(json2));
68
- return new _VoiceBank(
69
- manifest2,
70
- new RangeVoiceSource(koe, pcmBase(jsonLength2))
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 = JSON.parse(new TextDecoder().decode(json));
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[phoneme] !== void 0;
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) return null;
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.47",
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.0",
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.3",
59
+ "@onjmin/koe": "1.0.4",
60
60
  "midi-json-parser": "8.1.74"
61
61
  },
62
62
  "scripts": {