@onjmin/koe 1.0.4 → 1.0.6

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.
@@ -17,14 +17,48 @@ function packKoe(manifest, pcmParts) {
17
17
  }
18
18
 
19
19
  // src/converter/frq.ts
20
- function parseFrqAverageF0(buffer) {
21
- if (buffer.byteLength < 20) return null;
20
+ var HEADER_SIZE = 40;
21
+ function parseFrq(buffer) {
22
+ if (buffer.byteLength < HEADER_SIZE) return null;
22
23
  const view = new DataView(buffer);
23
24
  let header = "";
24
25
  for (let i = 0; i < 8; i++) header += String.fromCharCode(view.getUint8(i));
25
26
  if (header !== "FREQ0003") return null;
26
- const avg = view.getFloat64(12, true);
27
- return Number.isFinite(avg) && avg > 0 ? avg : null;
27
+ const hopSize = view.getInt32(8, true);
28
+ const averageF0 = view.getFloat64(12, true);
29
+ const declared = view.getInt32(36, true);
30
+ const available = Math.floor((buffer.byteLength - HEADER_SIZE) / 16);
31
+ const length = Math.max(0, Math.min(declared, available));
32
+ const f0 = new Float64Array(length);
33
+ const amp = new Float64Array(length);
34
+ for (let i = 0; i < length; i++) {
35
+ const p = HEADER_SIZE + i * 16;
36
+ f0[i] = view.getFloat64(p, true);
37
+ amp[i] = view.getFloat64(p + 8, true);
38
+ }
39
+ return {
40
+ hopSize: hopSize > 0 ? hopSize : 256,
41
+ averageF0: Number.isFinite(averageF0) && averageF0 > 0 ? averageF0 : 0,
42
+ f0,
43
+ amp
44
+ };
45
+ }
46
+ function frqAverageF0InRange(frq, startMs, endMs, sourceRate) {
47
+ if (!(sourceRate > 0) || frq.f0.length === 0) return 0;
48
+ const perFrameMs = frq.hopSize / sourceRate * 1e3;
49
+ if (!(perFrameMs > 0)) return 0;
50
+ const first = Math.max(0, Math.floor(startMs / perFrameMs));
51
+ const last = Math.min(frq.f0.length - 1, Math.ceil(endMs / perFrameMs));
52
+ let num = 0;
53
+ let den = 0;
54
+ for (let i = first; i <= last; i++) {
55
+ const hz = frq.f0[i];
56
+ if (!(hz > 0)) continue;
57
+ const w = frq.amp[i] > 0 ? frq.amp[i] : 1;
58
+ num += hz * w;
59
+ den += w;
60
+ }
61
+ return den > 0 ? num / den : 0;
28
62
  }
29
63
  function frqFileName(wavName) {
30
64
  const dot = wavName.lastIndexOf(".");
@@ -112,11 +146,25 @@ function msToSamples(ms) {
112
146
  return Math.round(ms / 1e3 * TARGET_RATE);
113
147
  }
114
148
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
149
+ function otoRegion(pcmLength, oto) {
150
+ const start = clamp(msToSamples(oto.offset), 0, pcmLength);
151
+ const end = oto.cutoff < 0 ? clamp(start + msToSamples(-oto.cutoff), start, pcmLength) : clamp(pcmLength - msToSamples(oto.cutoff), start, pcmLength);
152
+ return { start, end };
153
+ }
154
+ function centerSlice(slice) {
155
+ const out = new Int16Array(slice.length);
156
+ if (slice.length === 0) return out;
157
+ let sum = 0;
158
+ for (let i = 0; i < slice.length; i++) sum += slice[i];
159
+ const dc = Math.round(sum / slice.length);
160
+ for (let i = 0; i < slice.length; i++) {
161
+ out[i] = clamp(slice[i] - dc, -32768, 32767);
162
+ }
163
+ return out;
164
+ }
115
165
  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);
166
+ const { start, end } = otoRegion(pcm.length, oto);
167
+ const slice = centerSlice(pcm.subarray(start, end));
120
168
  const length = slice.length;
121
169
  const pre = clamp(msToSamples(oto.pre), 0, length);
122
170
  const overlap = clamp(msToSamples(oto.overlap), 0, length);
@@ -280,11 +328,11 @@ function toInt16(samples) {
280
328
  }
281
329
  return out;
282
330
  }
283
- function normalizePcm(buf) {
331
+ function readWavPcm48k(buf) {
284
332
  const wav = parseWav(buf);
285
333
  const mono = toMono(wav);
286
334
  const resampled = resample(mono, 48e3);
287
- return toInt16(resampled.samples);
335
+ return { pcm: toInt16(resampled.samples), sourceRate: wav.sampleRate };
288
336
  }
289
337
  function readFourCC(view, pos) {
290
338
  return String.fromCharCode(
@@ -346,14 +394,24 @@ async function main() {
346
394
  try {
347
395
  const wavPath = resolveInside(otoDir, oto.wav);
348
396
  const wavBytes = await readFile(wavPath);
349
- const pcm = normalizePcm(toArrayBuffer(wavBytes));
397
+ const { pcm, sourceRate } = readWavPcm48k(toArrayBuffer(wavBytes));
350
398
  let recordedPitch = pitchFromAliasSuffix(oto.alias) ?? 0;
351
399
  try {
352
400
  const frqBytes = await readFile(
353
401
  resolveInside(otoDir, frqFileName(oto.wav))
354
402
  );
355
- const avg = parseFrqAverageF0(toArrayBuffer(frqBytes));
356
- if (avg) recordedPitch = avg;
403
+ const frq = parseFrq(toArrayBuffer(frqBytes));
404
+ if (frq) {
405
+ const { start, end } = otoRegion(pcm.length, oto);
406
+ const toMs = (samples) => samples / 48e3 * 1e3;
407
+ const local = frqAverageF0InRange(
408
+ frq,
409
+ toMs(start) + oto.pre,
410
+ toMs(end),
411
+ sourceRate
412
+ ) || frqAverageF0InRange(frq, toMs(start), toMs(end), sourceRate) || frq.averageF0;
413
+ if (local > 0) recordedPitch = local;
414
+ }
357
415
  } catch {
358
416
  }
359
417
  inputs.push({ oto, pcm, recordedPitch });
@@ -1 +1 @@
1
- "use strict";(()=>{var f=new Int16Array(0),u=class extends AudioWorkletProcessor{manifest=null;phonemes=new Map;queue=[];current=null;next=null;constructor(e){super(e),this.port.onmessage=t=>this.onMessage(t.data)}onMessage(e){e.type==="init"&&e.manifest?this.manifest=e.manifest:e.type==="phoneme"&&e.name&&e.buffer?this.phonemes.set(e.name,new Int16Array(e.buffer,0,Math.floor(e.buffer.byteLength/2))):e.type==="play"&&e.notes?this.queue.push(...e.notes):e.type==="stop"&&(this.queue=[],this.current=null,this.next=null)}dequeue(){let e=this.queue.shift();if(!e||!this.manifest)return null;if(e.phoneme==="")return{data:f,length:0,consonant:0,pre:0,overlap:0,readPos:0,stepRate:1,remaining:e.duration};let t=Object.hasOwn(this.manifest.phonemes,e.phoneme)?this.manifest.phonemes[e.phoneme]:void 0,n=this.phonemes.get(e.phoneme);if(!t||!n)return null;let i=t.pitch||this.manifest.referencePitch;return{data:n,length:t.length,consonant:t.consonant,pre:t.pre,overlap:t.overlap,readPos:0,stepRate:e.pitch/i,remaining:e.duration}}readSample(e,t){let n=t|0,i=t-n;return((e[n]??0)+((e[n+1]??0)-(e[n]??0))*i)/32768}advance(e){if(!(e.length<=0)&&(e.readPos+=e.stepRate,e.readPos>=e.length)){let t=e.length-e.consonant;e.readPos=t>0?e.consonant+(e.readPos-e.consonant)%t:e.length-1}}process(e,t){let n=t[0]?.[0];if(!n)return!0;for(let i=0;i<n.length;i++){if(!this.current||this.current.remaining<=0){let s=this.next!==null;if(this.current=this.next??this.dequeue(),this.next=null,!this.current){n[i]=0;continue}s||(this.current.readPos=this.current.pre)}let r=this.current;!this.next&&this.queue.length>0&&(this.next=this.dequeue());let o=this.readSample(r.data,r.readPos);if(!this.next&&this.queue.length===0&&r.remaining<=480&&r.length>0&&(o*=r.remaining/480),this.next){let s=this.next,a=Math.max(s.pre/s.stepRate,480);if(r.remaining<=a){let c=Math.min(Math.max(s.overlap/s.stepRate,480),a),l=a-r.remaining,h=Math.min(1,l/c),p=this.readSample(s.data,s.readPos);o=o*(1-h)+p*h,this.advance(s)}}n[i]=o,this.advance(r),r.remaining--}return!0}};registerProcessor("koe-processor",u);})();
1
+ "use strict";(()=>{var P=new Int16Array(0),M=class extends AudioWorkletProcessor{manifest=null;phonemes=new Map;loops=new Map;queue=[];current=null;next=null;leadIn=!1;scratch=new Float32Array(256);constructor(e){super(e),this.port.onmessage=t=>this.onMessage(t.data)}onMessage(e){e.type==="init"&&e.manifest?this.manifest=e.manifest:e.type==="phoneme"&&e.name&&e.buffer?(this.phonemes.set(e.name,new Int16Array(e.buffer,0,Math.floor(e.buffer.byteLength/2))),this.loops.delete(e.name)):e.type==="play"&&e.notes?(this.leadIn=e.leadIn===!0,this.queue.push(...e.notes)):e.type==="stop"&&(this.queue=[],this.current=null,this.next=null)}dequeue(){let e=this.queue.shift();if(!e||!this.manifest)return null;if(e.phoneme==="")return{data:P,length:0,consonant:0,pre:0,overlap:0,readPos:0,stepRate:1,remaining:e.duration,duration:e.duration,period:0,lead:0,xfade:0,loop:{start:0,fade:0},fadeIn:0,aligned:!0};let t=Object.hasOwn(this.manifest.phonemes,e.phoneme)?this.manifest.phonemes[e.phoneme]:void 0,a=this.phonemes.get(e.phoneme);if(!t||!a)return null;let r=t.pitch||this.manifest.referencePitch||e.pitch||1;return{data:a,length:t.length,consonant:t.consonant,pre:t.pre,overlap:t.overlap,readPos:0,stepRate:e.pitch/r,remaining:e.duration,duration:e.duration,period:e.pitch>0?sampleRate/e.pitch:0,lead:0,xfade:0,loop:this.loopPoints(e.phoneme,a,t.length,t.consonant,sampleRate/r),fadeIn:0,aligned:!1}}readSample(e,t){let a=t|0,r=t-a;return((e[a]??0)+((e[a+1]??0)-(e[a]??0))*r)/32768}read(e){let t=this.readSample(e.data,e.readPos),a=e.loop.fade;if(a<=0)return t;let r=e.length-a;if(e.readPos<r)return t;let n=(e.readPos-r)/a,i=this.readSample(e.data,e.loop.start+(e.readPos-r));return t*(1-n)+i*n}wrap(e,t){let a=e.loop.fade,r=a>0?e.length-a-e.loop.start:e.length-e.consonant;return r<=0?e.length-1:(a>0?e.loop.start+a:e.consonant)+(t-e.length)%r}advance(e){e.length<=0||(e.readPos+=e.stepRate,e.readPos>=e.length&&(e.readPos=this.wrap(e,e.readPos)))}bestLag(e,t,a,r,n,i,s,o,h,d){let p=this.scratch,c=0;for(let l=0;l<s;l++){let m=this.readSample(e,t+l*a);p[l]=m,c+=m*m}if(c<=0)return 0;let f=0,u=-1/0;for(let l=o;l<=h;l+=d){let m=0,A=0;for(let b=0;b<s;b++){let v=this.readSample(r,n+l+b*i);m+=p[b]*v,A+=v*v}if(A<=0)continue;let g=m/Math.sqrt(A);(g>u||g===u&&Math.abs(l)<Math.abs(f))&&(u=g,f=l)}return f}loopPoints(e,t,a,r,n){let i=this.loops.get(e);if(i)return i;let s={start:r,fade:0},o=a-r;if(o>8&&n>=4&&Number.isFinite(n)){let h=Math.min(480,Math.floor(o/4)),d=a-h,p=Math.min(Math.round(n),Math.max(0,o-h*2-1)),c=r;if(p>0){let f=Math.min(h,256),u=Math.max(1,Math.round(p/64)),l=this.bestLag(t,d,1,t,r,1,f,0,p,u);u>1&&(l=this.bestLag(t,d,1,t,r,1,f,Math.max(0,l-u),Math.min(p,l+u),1)),c=r+l}c+h<a&&(s={start:c,fade:h})}return this.loops.set(e,s),s}prepareTransition(e,t){let a=Math.max(0,e.remaining*.5),r=Math.min(480,a),n=Math.max(t.pre/t.stepRate,r),i=e.length>0?Math.max(t.overlap/t.stepRate,r):r,s=Math.min(i,n);if(n>a&&n>0){let o=a/n;n*=o,s*=o}t.lead=n,t.xfade=s,t.readPos=Math.max(0,t.pre-n*t.stepRate),t.aligned=!1}alignPhase(e,t){if(t.aligned=!0,e.length<=0||t.length<=0||t.period<4||t.xfade<32)return;let a=Math.min(Math.round(t.period),Math.round(t.xfade),256);if(a<8)return;let r=t.period/2*t.stepRate,n=Math.max(1,Math.round(2*r/64)),i=this.bestLag(e.data,e.readPos,e.stepRate,t.data,t.readPos,t.stepRate,a,-r,r,n);n>1&&(i=this.bestLag(e.data,e.readPos,e.stepRate,t.data,t.readPos,t.stepRate,a,i-n,i+n,1));let s=t.readPos+i;t.readPos=s>=0?s:s+t.period*t.stepRate,t.readPos<0&&(t.readPos=0)}process(e,t){let a=t[0]?.[0];if(!a)return!0;for(let r=0;r<a.length;r++){if(!this.current||this.current.remaining<=0){let s=this.next!==null;if(this.current=this.next??this.dequeue(),this.next=null,!this.current){a[r]=0;continue}if(!s){let o=this.current;if(this.leadIn&&o.length>0){let h=o.pre/o.stepRate;o.readPos=0,o.remaining+=h,o.duration+=h}else o.readPos=o.pre;o.fadeIn=Math.min(480,o.duration)}}let n=this.current;!this.next&&this.queue.length>0&&(this.next=this.dequeue(),this.next&&this.prepareTransition(n,this.next));let i=this.read(n);if(n.fadeIn>0){let s=n.duration-n.remaining;s<n.fadeIn?i*=s/n.fadeIn:n.fadeIn=0}if(!this.next&&this.queue.length===0&&n.remaining<=480&&n.length>0&&(i*=n.remaining/480),this.next){let s=this.next;if(n.remaining<=s.lead){s.aligned||this.alignPhase(n,s);let o=s.lead-n.remaining,h=s.xfade>0?Math.min(1,o/s.xfade):1,d=this.read(s);i=i*(1-h)+d*h,this.advance(s)}}a[r]=i,this.advance(n),n.remaining--}return!0}};registerProcessor("koe-processor",M);})();
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.4",
5
+ "version": "1.0.6",
6
6
  "description": "UTAU concatenative synthesis engine optimized for browser / mobile via WebAssembly + AudioWorklet",
7
7
  "type": "module",
8
8
  "main": "./dist/index.js",
@@ -41,6 +41,9 @@
41
41
  "node": "24.16.0",
42
42
  "pnpm": "11.7.0"
43
43
  },
44
+ "dependencies": {
45
+ "fflate": "^0.8.3"
46
+ },
44
47
  "scripts": {
45
48
  "build": "tsup",
46
49
  "dev": "tsx src/dev-server.ts",