@onjmin/koe 1.0.5 → 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.
- package/dist/index.d.ts +94 -11
- package/dist/index.js +93 -13
- package/dist/index.js.map +1 -1
- package/dist/koe-convert.js +71 -13
- package/dist/koe-worklet.js +1 -1
- package/package.json +1 -1
package/dist/koe-convert.js
CHANGED
|
@@ -17,14 +17,48 @@ function packKoe(manifest, pcmParts) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// src/converter/frq.ts
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
27
|
-
|
|
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
|
|
117
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
356
|
-
if (
|
|
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 });
|
package/dist/koe-worklet.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";(()=>{var
|
|
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.
|
|
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",
|