@onjmin/koe 1.0.5 → 1.0.7

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,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
 
4
3
  // src/converter/cli.ts
5
4
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
@@ -17,14 +16,48 @@ function packKoe(manifest, pcmParts) {
17
16
  }
18
17
 
19
18
  // src/converter/frq.ts
20
- function parseFrqAverageF0(buffer) {
21
- if (buffer.byteLength < 20) return null;
19
+ var HEADER_SIZE = 40;
20
+ function parseFrq(buffer) {
21
+ if (buffer.byteLength < HEADER_SIZE) return null;
22
22
  const view = new DataView(buffer);
23
23
  let header = "";
24
24
  for (let i = 0; i < 8; i++) header += String.fromCharCode(view.getUint8(i));
25
25
  if (header !== "FREQ0003") return null;
26
- const avg = view.getFloat64(12, true);
27
- return Number.isFinite(avg) && avg > 0 ? avg : null;
26
+ const hopSize = view.getInt32(8, true);
27
+ const averageF0 = view.getFloat64(12, true);
28
+ const declared = view.getInt32(36, true);
29
+ const available = Math.floor((buffer.byteLength - HEADER_SIZE) / 16);
30
+ const length = Math.max(0, Math.min(declared, available));
31
+ const f0 = new Float64Array(length);
32
+ const amp = new Float64Array(length);
33
+ for (let i = 0; i < length; i++) {
34
+ const p = HEADER_SIZE + i * 16;
35
+ f0[i] = view.getFloat64(p, true);
36
+ amp[i] = view.getFloat64(p + 8, true);
37
+ }
38
+ return {
39
+ hopSize: hopSize > 0 ? hopSize : 256,
40
+ averageF0: Number.isFinite(averageF0) && averageF0 > 0 ? averageF0 : 0,
41
+ f0,
42
+ amp
43
+ };
44
+ }
45
+ function frqAverageF0InRange(frq, startMs, endMs, sourceRate) {
46
+ if (!(sourceRate > 0) || frq.f0.length === 0) return 0;
47
+ const perFrameMs = frq.hopSize / sourceRate * 1e3;
48
+ if (!(perFrameMs > 0)) return 0;
49
+ const first = Math.max(0, Math.floor(startMs / perFrameMs));
50
+ const last = Math.min(frq.f0.length - 1, Math.ceil(endMs / perFrameMs));
51
+ let num = 0;
52
+ let den = 0;
53
+ for (let i = first; i <= last; i++) {
54
+ const hz = frq.f0[i];
55
+ if (!(hz > 0)) continue;
56
+ const w = frq.amp[i] > 0 ? frq.amp[i] : 1;
57
+ num += hz * w;
58
+ den += w;
59
+ }
60
+ return den > 0 ? num / den : 0;
28
61
  }
29
62
  function frqFileName(wavName) {
30
63
  const dot = wavName.lastIndexOf(".");
@@ -112,11 +145,25 @@ function msToSamples(ms) {
112
145
  return Math.round(ms / 1e3 * TARGET_RATE);
113
146
  }
114
147
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
148
+ function otoRegion(pcmLength, oto) {
149
+ const start = clamp(msToSamples(oto.offset), 0, pcmLength);
150
+ const end = oto.cutoff < 0 ? clamp(start + msToSamples(-oto.cutoff), start, pcmLength) : clamp(pcmLength - msToSamples(oto.cutoff), start, pcmLength);
151
+ return { start, end };
152
+ }
153
+ function centerSlice(slice) {
154
+ const out = new Int16Array(slice.length);
155
+ if (slice.length === 0) return out;
156
+ let sum = 0;
157
+ for (let i = 0; i < slice.length; i++) sum += slice[i];
158
+ const dc = Math.round(sum / slice.length);
159
+ for (let i = 0; i < slice.length; i++) {
160
+ out[i] = clamp(slice[i] - dc, -32768, 32767);
161
+ }
162
+ return out;
163
+ }
115
164
  function trimToOto(pcm, oto, recordedPitch = 0) {
116
- const full = pcm.length;
117
- const start = clamp(msToSamples(oto.offset), 0, full);
118
- const end = oto.cutoff < 0 ? clamp(start + msToSamples(-oto.cutoff), start, full) : clamp(full - msToSamples(oto.cutoff), start, full);
119
- const slice = pcm.subarray(start, end);
165
+ const { start, end } = otoRegion(pcm.length, oto);
166
+ const slice = centerSlice(pcm.subarray(start, end));
120
167
  const length = slice.length;
121
168
  const pre = clamp(msToSamples(oto.pre), 0, length);
122
169
  const overlap = clamp(msToSamples(oto.overlap), 0, length);
@@ -280,11 +327,11 @@ function toInt16(samples) {
280
327
  }
281
328
  return out;
282
329
  }
283
- function normalizePcm(buf) {
330
+ function readWavPcm48k(buf) {
284
331
  const wav = parseWav(buf);
285
332
  const mono = toMono(wav);
286
333
  const resampled = resample(mono, 48e3);
287
- return toInt16(resampled.samples);
334
+ return { pcm: toInt16(resampled.samples), sourceRate: wav.sampleRate };
288
335
  }
289
336
  function readFourCC(view, pos) {
290
337
  return String.fromCharCode(
@@ -346,14 +393,24 @@ async function main() {
346
393
  try {
347
394
  const wavPath = resolveInside(otoDir, oto.wav);
348
395
  const wavBytes = await readFile(wavPath);
349
- const pcm = normalizePcm(toArrayBuffer(wavBytes));
396
+ const { pcm, sourceRate } = readWavPcm48k(toArrayBuffer(wavBytes));
350
397
  let recordedPitch = pitchFromAliasSuffix(oto.alias) ?? 0;
351
398
  try {
352
399
  const frqBytes = await readFile(
353
400
  resolveInside(otoDir, frqFileName(oto.wav))
354
401
  );
355
- const avg = parseFrqAverageF0(toArrayBuffer(frqBytes));
356
- if (avg) recordedPitch = avg;
402
+ const frq = parseFrq(toArrayBuffer(frqBytes));
403
+ if (frq) {
404
+ const { start, end } = otoRegion(pcm.length, oto);
405
+ const toMs = (samples) => samples / 48e3 * 1e3;
406
+ const local = frqAverageF0InRange(
407
+ frq,
408
+ toMs(start) + oto.pre,
409
+ toMs(end),
410
+ sourceRate
411
+ ) || frqAverageF0InRange(frq, toMs(start), toMs(end), sourceRate) || frq.averageF0;
412
+ if (local > 0) recordedPitch = local;
413
+ }
357
414
  } catch {
358
415
  }
359
416
  inputs.push({ oto, pcm, recordedPitch });