@onjmin/koe 1.0.9 → 1.0.11

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.js CHANGED
@@ -2100,7 +2100,7 @@ function analyze(wav) {
2100
2100
  const mono = resample(toMono(wav), RATE);
2101
2101
  const x = mono.samples;
2102
2102
  const n = Math.max(1, Math.floor((x.length - FFT_SIZE) / HOP) + 1);
2103
- const rmsDb = new Float32Array(n);
2103
+ const rmsDb2 = new Float32Array(n);
2104
2104
  const highRatio = new Float32Array(n);
2105
2105
  const highDb = new Float32Array(n);
2106
2106
  const flux = new Float32Array(n);
@@ -2123,7 +2123,7 @@ function analyze(wav) {
2123
2123
  re[i] = s * window[i];
2124
2124
  im[i] = 0;
2125
2125
  }
2126
- rmsDb[t] = 10 * Math.log10(energy / FFT_SIZE + 1e-12);
2126
+ rmsDb2[t] = 10 * Math.log10(energy / FFT_SIZE + 1e-12);
2127
2127
  fft(re, im);
2128
2128
  let lowSum = 0;
2129
2129
  let highSum = 0;
@@ -2141,7 +2141,7 @@ function analyze(wav) {
2141
2141
  highDb[t] = 20 * Math.log10(highSum / BINS + 1e-9);
2142
2142
  flux[t] = t === 0 ? 0 : fluxSum;
2143
2143
  }
2144
- const smoothDb = smooth(rmsDb, Math.round(30 / HOP_MS));
2144
+ const smoothDb = smooth(rmsDb2, Math.round(30 / HOP_MS));
2145
2145
  const smoothHighDb = smooth(highDb, Math.round(20 / HOP_MS));
2146
2146
  const highFloorDb = quietestWindow(smoothHighDb);
2147
2147
  const floorDb = percentile(smoothDb, 0.1);
@@ -2197,7 +2197,7 @@ function analyze(wav) {
2197
2197
  return {
2198
2198
  n,
2199
2199
  durationMs: x.length / RATE * 1e3,
2200
- rmsDb,
2200
+ rmsDb: rmsDb2,
2201
2201
  smoothDb,
2202
2202
  voiced,
2203
2203
  highRatio,
@@ -3338,9 +3338,25 @@ function softLimit(value, knee, max2) {
3338
3338
  return Math.sign(value) * Math.min(max2, limited);
3339
3339
  }
3340
3340
  var DEVOICED = /* @__PURE__ */ new Set(["A", "I", "U", "E", "O"]);
3341
+ var UNVOICED_ONSET = /* @__PURE__ */ new Set([
3342
+ "k",
3343
+ "ky",
3344
+ "s",
3345
+ "sh",
3346
+ "t",
3347
+ "ty",
3348
+ "ch",
3349
+ "ts",
3350
+ "h",
3351
+ "hy",
3352
+ "f",
3353
+ "p",
3354
+ "py"
3355
+ ]);
3341
3356
  function segmentPhonemes(phonemes) {
3342
3357
  const segments = [];
3343
3358
  let openStart = null;
3359
+ let openOnset = "";
3344
3360
  for (let i = 0; i < phonemes.length; i++) {
3345
3361
  const { phone, start_ms, duration_ms } = phonemes[i];
3346
3362
  if (phone === "sil" || phone === "pau") {
@@ -3348,8 +3364,10 @@ function segmentPhonemes(phonemes) {
3348
3364
  segments.push({
3349
3365
  pause: false,
3350
3366
  startMs: openStart,
3351
- durationMs: start_ms - openStart,
3352
- devoiced: false
3367
+ nucleusMs: openStart,
3368
+ endMs: start_ms,
3369
+ devoiced: false,
3370
+ onset: openOnset
3353
3371
  });
3354
3372
  openStart = null;
3355
3373
  }
@@ -3357,18 +3375,25 @@ function segmentPhonemes(phonemes) {
3357
3375
  segments.push({
3358
3376
  pause: true,
3359
3377
  startMs: start_ms,
3360
- durationMs: duration_ms,
3361
- devoiced: false
3378
+ nucleusMs: start_ms,
3379
+ endMs: start_ms + duration_ms,
3380
+ devoiced: false,
3381
+ onset: ""
3362
3382
  });
3363
3383
  continue;
3364
3384
  }
3365
- if (openStart === null) openStart = start_ms;
3385
+ if (openStart === null) {
3386
+ openStart = start_ms;
3387
+ openOnset = NUCLEUS.has(phone) ? "" : phone;
3388
+ }
3366
3389
  if (NUCLEUS.has(phone)) {
3367
3390
  segments.push({
3368
3391
  pause: false,
3369
3392
  startMs: openStart,
3370
- durationMs: start_ms + duration_ms - openStart,
3371
- devoiced: DEVOICED.has(phone)
3393
+ nucleusMs: start_ms,
3394
+ endMs: start_ms + duration_ms,
3395
+ devoiced: DEVOICED.has(phone),
3396
+ onset: openOnset
3372
3397
  });
3373
3398
  openStart = null;
3374
3399
  }
@@ -3378,47 +3403,79 @@ function segmentPhonemes(phonemes) {
3378
3403
  segments.push({
3379
3404
  pause: false,
3380
3405
  startMs: openStart,
3381
- durationMs: last.start_ms + last.duration_ms - openStart,
3406
+ nucleusMs: openStart,
3407
+ endMs: last.start_ms + last.duration_ms,
3408
+ onset: openOnset,
3382
3409
  devoiced: false
3383
3410
  });
3384
3411
  }
3385
3412
  return segments;
3386
3413
  }
3414
+ function anchorMs(segment) {
3415
+ return segment.pause ? segment.startMs : segment.nucleusMs;
3416
+ }
3387
3417
  function alignHtsProsody(frames, features, options = {}) {
3388
3418
  const {
3389
3419
  intonationStrength = 1,
3390
3420
  frameMs = 10,
3391
3421
  fallbackPauseMs = 150,
3392
3422
  kneeCents = 400,
3393
- maxCents = 700
3423
+ maxCents = 700,
3424
+ unvoicedBridgeMs = 120
3394
3425
  } = options;
3395
3426
  const segments = segmentPhonemes(frames.phonemes);
3396
- const durations = new Array(features.length).fill(0);
3397
- const htsStart = new Array(features.length).fill(null);
3427
+ const matched = new Array(features.length).fill(null);
3398
3428
  const devoiced = new Array(features.length).fill(false);
3429
+ const consonantMs = new Array(features.length).fill(0);
3430
+ const unvoicedOnset = new Array(features.length).fill(false);
3399
3431
  let s = 0;
3400
3432
  for (let f2 = 0; f2 < features.length; f2++) {
3401
3433
  const feature = features[f2];
3402
3434
  while (s < segments.length && segments[s].pause && !feature.pause) s++;
3403
3435
  const segment = segments[s];
3404
3436
  if (feature.pause) {
3405
- if (segment?.pause) {
3406
- durations[f2] = segment.durationMs;
3407
- htsStart[f2] = segment.startMs;
3408
- s++;
3409
- } else {
3410
- durations[f2] = fallbackPauseMs;
3411
- }
3437
+ if (segment?.pause) matched[f2] = s++;
3412
3438
  continue;
3413
3439
  }
3414
3440
  if (!segment) return null;
3415
- durations[f2] = segment.durationMs;
3416
- htsStart[f2] = segment.startMs;
3441
+ matched[f2] = s++;
3417
3442
  devoiced[f2] = segment.devoiced;
3418
- s++;
3443
+ consonantMs[f2] = Math.max(0, segment.nucleusMs - segment.startMs);
3444
+ unvoicedOnset[f2] = UNVOICED_ONSET.has(segment.onset);
3419
3445
  }
3420
3446
  if (segments.slice(s).some((segment) => !segment.pause)) return null;
3421
- const f0 = frames.f0_hz;
3447
+ const durations = new Array(features.length).fill(0);
3448
+ const htsStart = new Array(features.length).fill(null);
3449
+ for (let f2 = 0; f2 < features.length; f2++) {
3450
+ const index = matched[f2];
3451
+ if (index === null) {
3452
+ durations[f2] = fallbackPauseMs;
3453
+ continue;
3454
+ }
3455
+ const here = anchorMs(segments[index]);
3456
+ let next = f2 + 1;
3457
+ while (next < features.length && matched[next] === null) next++;
3458
+ let end = segments[index].endMs;
3459
+ if (next < features.length) {
3460
+ const nextIndex = matched[next];
3461
+ end = anchorMs(segments[nextIndex]);
3462
+ for (let k = index + 1; k < nextIndex; k++) {
3463
+ if (segments[k].pause) end -= segments[k].endMs - segments[k].startMs;
3464
+ }
3465
+ }
3466
+ durations[f2] = Math.max(0, end - here);
3467
+ htsStart[f2] = here;
3468
+ }
3469
+ const f0 = frames.f0_hz.slice();
3470
+ for (const phoneme of frames.phonemes) {
3471
+ if (!UNVOICED_ONSET.has(phoneme.phone) && phoneme.phone !== "cl") continue;
3472
+ const from = Math.max(0, Math.round(phoneme.start_ms / frames.frame_ms));
3473
+ const to = Math.min(
3474
+ f0.length,
3475
+ Math.round((phoneme.start_ms + phoneme.duration_ms) / frames.frame_ms)
3476
+ );
3477
+ for (let i = from; i < to; i++) f0[i] = 0;
3478
+ }
3422
3479
  const logF0 = new Float64Array(f0.length);
3423
3480
  const voiced = [];
3424
3481
  for (let i = 0; i < f0.length; i++) if (f0[i] > 0) voiced.push(i);
@@ -3436,7 +3493,18 @@ function alignHtsProsody(frames, features, options = {}) {
3436
3493
  if (left === void 0) logF0[i] = Math.log2(f0[right]);
3437
3494
  else if (right === void 0) logF0[i] = Math.log2(f0[left]);
3438
3495
  else {
3439
- const t = (i - left) / (right - left);
3496
+ if ((right - left) * frames.frame_ms > unvoicedBridgeMs) {
3497
+ logF0[i] = Math.log2(
3498
+ i - left <= (right - left) / 2 ? f0[left] : f0[right]
3499
+ );
3500
+ continue;
3501
+ }
3502
+ const rampStart = Math.max(left, right - Math.ceil(20 / frames.frame_ms));
3503
+ if (i <= rampStart) {
3504
+ logF0[i] = Math.log2(f0[left]);
3505
+ continue;
3506
+ }
3507
+ const t = (i - rampStart) / (right - rampStart);
3440
3508
  logF0[i] = Math.log2(f0[left]) * (1 - t) + Math.log2(f0[right]) * t;
3441
3509
  }
3442
3510
  }
@@ -3479,7 +3547,9 @@ function alignHtsProsody(frames, features, options = {}) {
3479
3547
  pitchCurve: { frame_ms: frameMs, cents },
3480
3548
  medianHz,
3481
3549
  durationMs,
3482
- devoiced
3550
+ devoiced,
3551
+ consonantMs,
3552
+ unvoicedOnset
3483
3553
  };
3484
3554
  }
3485
3555
 
@@ -3517,6 +3587,25 @@ function isHigh(position, accent) {
3517
3587
  if (accent > 1) return 2 <= position && position <= accent;
3518
3588
  return position >= 2;
3519
3589
  }
3590
+ var SENTENCE_END = /[。.!?!?.]/u;
3591
+ var CLAUSE = /[、,,]/u;
3592
+ function pauseKind(punctuation) {
3593
+ if (punctuation === void 0) return "clause";
3594
+ if (SENTENCE_END.test(punctuation)) return "sentence";
3595
+ if (CLAUSE.test(punctuation)) return "clause";
3596
+ return "space";
3597
+ }
3598
+ function pauseRank(punctuation) {
3599
+ if (punctuation === void 0) return 0;
3600
+ switch (pauseKind(punctuation)) {
3601
+ case "sentence":
3602
+ return 3;
3603
+ case "clause":
3604
+ return 2;
3605
+ default:
3606
+ return 1;
3607
+ }
3608
+ }
3520
3609
  function analyze2(nodes) {
3521
3610
  const reading_parts = [];
3522
3611
  const result = [];
@@ -3526,8 +3615,11 @@ function analyze2(nodes) {
3526
3615
  const pronunciation = (node.pron || "").replace(/'/g, "").replace(/’/g, "");
3527
3616
  if (node.mora_size === 0 || PUNCTUATION.has(node.string)) {
3528
3617
  reading_parts.push(node.string || "\u3001");
3529
- if (result.length > 0 && !result[result.length - 1].pause) {
3530
- result.push({ mora: "", pause: true });
3618
+ const last = result[result.length - 1];
3619
+ if (result.length > 0 && !last.pause) {
3620
+ result.push({ mora: "", pause: true, punctuation: node.string || "\u3001" });
3621
+ } else if (last?.pause && pauseRank(node.string) > pauseRank(last.punctuation)) {
3622
+ last.punctuation = node.string;
3531
3623
  }
3532
3624
  index++;
3533
3625
  continue;
@@ -3612,26 +3704,113 @@ function smoothstep(t) {
3612
3704
  const x = Math.min(1, Math.max(0, t));
3613
3705
  return x * x * (3 - 2 * x);
3614
3706
  }
3707
+ var DEFAULT_PAUSE_MS = {
3708
+ sentence: 650,
3709
+ clause: 380,
3710
+ space: 500
3711
+ };
3712
+ function shapeDurations(durations, features, options = {}) {
3713
+ const {
3714
+ pauseMs = DEFAULT_PAUSE_MS,
3715
+ durationContrast = 1.3,
3716
+ minMoraMs = 60,
3717
+ minShrink = 0.85
3718
+ } = options;
3719
+ const count = Math.min(features.length, durations.length);
3720
+ let sum = 0;
3721
+ let spoken = 0;
3722
+ for (let f = 0; f < count; f++) {
3723
+ if (features[f].pause) continue;
3724
+ sum += durations[f];
3725
+ spoken++;
3726
+ }
3727
+ const mean = spoken > 0 ? sum / spoken : 0;
3728
+ const result = durations.slice();
3729
+ let changed = false;
3730
+ for (let f = 0; f < count; f++) {
3731
+ const feature = features[f];
3732
+ let next = durations[f];
3733
+ if (feature.pause) {
3734
+ if (pauseMs && f < count - 1) {
3735
+ const kind = pauseKind(feature.punctuation);
3736
+ next = Math.max(next, pauseMs[kind] ?? DEFAULT_PAUSE_MS[kind]);
3737
+ }
3738
+ } else if (durationContrast !== 1 && next > 0) {
3739
+ next = Math.max(
3740
+ Math.min(next, minMoraMs),
3741
+ next * minShrink,
3742
+ mean + (next - mean) * durationContrast
3743
+ );
3744
+ }
3745
+ if (next !== durations[f]) {
3746
+ result[f] = next;
3747
+ changed = true;
3748
+ }
3749
+ }
3750
+ return changed ? result : durations;
3751
+ }
3752
+ function warpPitchCurve(cents, frameMs, from, to) {
3753
+ const count = Math.min(from.length, to.length);
3754
+ const fromStart = [];
3755
+ const toStart = [];
3756
+ let a = 0;
3757
+ let b = 0;
3758
+ for (let f2 = 0; f2 < count; f2++) {
3759
+ fromStart.push(a);
3760
+ toStart.push(b);
3761
+ a += from[f2];
3762
+ b += to[f2];
3763
+ }
3764
+ const frames = Math.max(2, Math.ceil(b / frameMs) + 2);
3765
+ const result = new Array(frames);
3766
+ const last = cents.length - 1;
3767
+ if (last < 0) return result.fill(0);
3768
+ let f = 0;
3769
+ for (let frame = 0; frame < frames; frame++) {
3770
+ const tMs = frame * frameMs;
3771
+ while (f + 1 < count && tMs >= toStart[f + 1]) f++;
3772
+ const progress = to[f] > 0 ? (tMs - toStart[f]) / to[f] : 0;
3773
+ const source = (fromStart[f] + progress * from[f]) / frameMs;
3774
+ const left = Math.min(last, Math.max(0, Math.floor(source)));
3775
+ const right = Math.min(last, left + 1);
3776
+ const t = Math.min(1, Math.max(0, source - Math.floor(source)));
3777
+ result[frame] = cents[left] * (1 - t) + cents[right] * t;
3778
+ }
3779
+ return result;
3780
+ }
3615
3781
  function shapeProsody(prosody, features, options = {}) {
3616
3782
  const {
3617
3783
  question = false,
3618
3784
  questionRiseCents = 350,
3619
- energyDbPerSemitone = 0.5,
3620
- devoicedGain = 0.5,
3785
+ energyDbPerSemitone = 0,
3786
+ devoicedGain = 1,
3621
3787
  minGain = 0.35,
3622
- maxGain = 1.8
3788
+ maxGain = 1.8,
3789
+ pitchShiftCents = 0
3623
3790
  } = options;
3624
3791
  const { frame_ms: frameMs } = prosody.pitchCurve;
3625
- const cents = prosody.pitchCurve.cents.slice();
3626
- const count = Math.min(features.length, prosody.moraDurationsMs.length);
3792
+ const durations = shapeDurations(prosody.moraDurationsMs, features, options);
3793
+ const cents = durations === prosody.moraDurationsMs ? prosody.pitchCurve.cents.slice() : warpPitchCurve(
3794
+ prosody.pitchCurve.cents,
3795
+ frameMs,
3796
+ prosody.moraDurationsMs,
3797
+ durations
3798
+ );
3799
+ const count = Math.min(features.length, durations.length);
3627
3800
  const startFrame = [];
3628
3801
  const endFrame = [];
3629
3802
  let cursor = 0;
3630
3803
  for (let f = 0; f < count; f++) {
3631
3804
  startFrame.push(cursor / frameMs);
3632
- cursor += prosody.moraDurationsMs[f];
3805
+ cursor += durations[f];
3633
3806
  endFrame.push(cursor / frameMs);
3634
3807
  }
3808
+ for (let f = count; f < durations.length; f++) cursor += durations[f];
3809
+ const durationMs = cursor;
3810
+ if (pitchShiftCents !== 0) {
3811
+ for (let frame = 0; frame < cents.length; frame++)
3812
+ cents[frame] += pitchShiftCents;
3813
+ }
3635
3814
  const frameRange = (f) => [
3636
3815
  Math.max(0, Math.min(cents.length, Math.round(startFrame[f]))),
3637
3816
  Math.max(0, Math.min(cents.length, Math.round(endFrame[f])))
@@ -3676,17 +3855,100 @@ function shapeProsody(prosody, features, options = {}) {
3676
3855
  }
3677
3856
  return {
3678
3857
  ...prosody,
3858
+ moraDurationsMs: durations,
3859
+ durationMs,
3679
3860
  pitchCurve: { frame_ms: frameMs, cents },
3680
3861
  moraGains
3681
3862
  };
3682
3863
  }
3683
3864
 
3865
+ // src/utautts/style.ts
3866
+ var SPEAKING_STYLES = {
3867
+ neutral: {
3868
+ speed: 1,
3869
+ intonation: 1,
3870
+ pitchShiftCents: 0,
3871
+ pauseScale: 1,
3872
+ durationContrast: 1.3,
3873
+ energyDbPerSemitone: 0.8
3874
+ },
3875
+ calm: {
3876
+ speed: 0.9,
3877
+ intonation: 0.9,
3878
+ pitchShiftCents: -100,
3879
+ pauseScale: 1.4,
3880
+ durationContrast: 1.2,
3881
+ energyDbPerSemitone: 0.6
3882
+ },
3883
+ lively: {
3884
+ speed: 1.08,
3885
+ intonation: 1.3,
3886
+ pitchShiftCents: 100,
3887
+ pauseScale: 0.85,
3888
+ durationContrast: 1.4,
3889
+ energyDbPerSemitone: 1
3890
+ }
3891
+ };
3892
+ var BASE_PAUSE_MS = { sentence: 650, clause: 380, space: 500 };
3893
+ function resolveSpeakingStyle(input = "neutral") {
3894
+ if (typeof input === "string") return { ...SPEAKING_STYLES[input] };
3895
+ const { preset = "neutral", ...overrides } = input;
3896
+ return { ...SPEAKING_STYLES[preset], ...overrides };
3897
+ }
3898
+ function styleAlignOptions(style) {
3899
+ return { intonationStrength: style.intonation };
3900
+ }
3901
+ function styleShapeOptions(style) {
3902
+ return {
3903
+ pauseMs: {
3904
+ sentence: BASE_PAUSE_MS.sentence * style.pauseScale,
3905
+ clause: BASE_PAUSE_MS.clause * style.pauseScale,
3906
+ space: BASE_PAUSE_MS.space * style.pauseScale
3907
+ },
3908
+ durationContrast: style.durationContrast,
3909
+ pitchShiftCents: style.pitchShiftCents
3910
+ };
3911
+ }
3912
+ function styleRenderOptions(style) {
3913
+ return { energyDbPerSemitone: style.energyDbPerSemitone };
3914
+ }
3915
+
3684
3916
  // src/utautts/UtauTTSAdapter.ts
3685
3917
  function readingFromFeatures(features) {
3686
3918
  return features.map((frame) => frame.pause ? "\u3001" : frame.mora).join("");
3687
3919
  }
3688
3920
  var FS = WORLDLINE_SAMPLE_RATE;
3689
3921
  var msToSamples2 = (ms) => Math.round(ms / 1e3 * FS);
3922
+ function rmsDb(pcm, from, to) {
3923
+ const a = Math.max(0, from);
3924
+ const b = Math.min(pcm.length, to);
3925
+ if (b <= a) return Number.NaN;
3926
+ let sum = 0;
3927
+ for (let k = a; k < b; k++) sum += pcm[k] * pcm[k];
3928
+ return 10 * Math.log10(sum / (b - a) + 1e-12);
3929
+ }
3930
+ function applyGainFrames(pcm, framesDb, frameSamples) {
3931
+ const last = framesDb.length - 1;
3932
+ if (last < 0) return;
3933
+ for (let k = 0; k < pcm.length; k++) {
3934
+ const position = k / frameSamples - 0.5;
3935
+ const left = Math.min(last, Math.max(0, Math.floor(position)));
3936
+ const right = Math.min(last, left + 1);
3937
+ const t = Math.min(1, Math.max(0, position - left));
3938
+ pcm[k] *= 10 ** ((framesDb[left] * (1 - t) + framesDb[right] * t) / 20);
3939
+ }
3940
+ }
3941
+ function softLimit2(pcm, threshold) {
3942
+ const range = 1 - threshold;
3943
+ if (range <= 0) return;
3944
+ for (let k = 0; k < pcm.length; k++) {
3945
+ const x = pcm[k];
3946
+ const a = Math.abs(x);
3947
+ if (a <= threshold) continue;
3948
+ const y = threshold + range * Math.tanh((a - threshold) / range);
3949
+ pcm[k] = x < 0 ? -y : y;
3950
+ }
3951
+ }
3690
3952
  function f0At(timeline, tMs) {
3691
3953
  const curve = timeline.f0_curve;
3692
3954
  if (curve.length === 0) return timeline.reference_hz || 220;
@@ -3716,6 +3978,30 @@ function applyMoraGains(plan, gains) {
3716
3978
  unit.volume *= gain;
3717
3979
  }
3718
3980
  }
3981
+ function applyConsonantGaps(plan, prosody) {
3982
+ const { consonantMs, unvoicedOnset } = prosody;
3983
+ if (!consonantMs || !unvoicedOnset) return;
3984
+ if (consonantMs.length !== (plan.morae?.length ?? -1)) return;
3985
+ const leading = plan.timeline.leading_ms;
3986
+ const units = plan.timeline.units;
3987
+ for (let i = 0; i + 1 < units.length; i++) {
3988
+ const unit = units[i];
3989
+ const next = units[i + 1];
3990
+ if (next.position !== unit.position + 1) continue;
3991
+ if (!unvoicedOnset[next.position]) continue;
3992
+ const consonant = consonantMs[next.position];
3993
+ if (!(consonant > 0)) continue;
3994
+ const gapStart = next.note_start_ms + leading - consonant;
3995
+ const currentEnd = unit.position_ms + unit.length_ms;
3996
+ if (gapStart >= currentEnd) continue;
3997
+ const minimumLength = Math.max(
3998
+ 40,
3999
+ unit.fade_in_ms + unit.fade_out_ms + 10,
4000
+ unit.note_start_ms + leading - unit.position_ms + 30
4001
+ );
4002
+ unit.length_ms = Math.max(minimumLength, gapStart - unit.position_ms);
4003
+ }
4004
+ }
3719
4005
  function planChunks(units, firstChunkUnits, chunkUnits) {
3720
4006
  const ranges = [];
3721
4007
  if (units.length === 0) return ranges;
@@ -3758,6 +4044,93 @@ function applyFade(pcm, fromSample, samples, fadeIn) {
3758
4044
  pcm[fromSample + k] *= gain;
3759
4045
  }
3760
4046
  }
4047
+ function median(values) {
4048
+ const sorted = values.slice().sort((a, b) => a - b);
4049
+ return sorted[Math.floor(sorted.length / 2)];
4050
+ }
4051
+ function shapeLoudness(audio, units, timeline, baseMs, options) {
4052
+ const frameMs = timeline.frame_ms > 0 ? timeline.frame_ms : 10;
4053
+ const frameSamples = Math.max(1, msToSamples2(frameMs));
4054
+ const frames = Math.ceil(audio.length / frameSamples) + 1;
4055
+ const curve = new Float64Array(frames);
4056
+ if (options.equalize) {
4057
+ const anchors = [];
4058
+ const measured = [];
4059
+ for (const unit of units) {
4060
+ if (!(unit.volume > 0) || unit.duration_ms < 30) continue;
4061
+ const start = unit.note_start_ms + timeline.leading_ms - baseMs;
4062
+ const soundEnd = unit.position_ms - baseMs + unit.length_ms - unit.fade_out_ms;
4063
+ const from = msToSamples2(start + unit.duration_ms * 0.35);
4064
+ const to = msToSamples2(
4065
+ Math.min(start + unit.duration_ms * 0.9, soundEnd)
4066
+ );
4067
+ if (to - from < msToSamples2(20)) continue;
4068
+ const db = rmsDb(audio, from, to);
4069
+ if (!Number.isFinite(db) || db < -70) continue;
4070
+ measured.push({
4071
+ frame: (start + unit.duration_ms * 0.6) / frameMs,
4072
+ intrinsicDb: db - 20 * Math.log10(unit.volume / 100)
4073
+ });
4074
+ }
4075
+ if (measured.length > 0) {
4076
+ const reference = options.targetDb;
4077
+ for (const m of measured) {
4078
+ anchors.push({
4079
+ frame: m.frame,
4080
+ db: Math.max(
4081
+ -options.maxDb,
4082
+ Math.min(options.maxDb, reference - m.intrinsicDb)
4083
+ )
4084
+ });
4085
+ }
4086
+ let a = 0;
4087
+ for (let frame = 0; frame < frames; frame++) {
4088
+ while (a + 1 < anchors.length && frame >= anchors[a + 1].frame) a++;
4089
+ const left = anchors[a];
4090
+ const right = anchors[Math.min(anchors.length - 1, a + 1)];
4091
+ if (frame <= left.frame || right === left) curve[frame] = left.db;
4092
+ else if (frame >= right.frame) curve[frame] = right.db;
4093
+ else {
4094
+ const t = (frame - left.frame) / (right.frame - left.frame);
4095
+ curve[frame] = left.db * (1 - t) + right.db * t;
4096
+ }
4097
+ }
4098
+ }
4099
+ }
4100
+ if (options.energyDbPerSemitone !== 0 && options.referenceHz > 0) {
4101
+ for (let frame = 0; frame < frames; frame++) {
4102
+ const f0 = f0At(timeline, baseMs + frame * frameMs);
4103
+ if (!(f0 > 0)) continue;
4104
+ const semitones = 12 * Math.log2(f0 / options.referenceHz);
4105
+ curve[frame] += Math.max(
4106
+ -options.energyMaxDb,
4107
+ Math.min(options.energyMaxDb, semitones * options.energyDbPerSemitone)
4108
+ );
4109
+ }
4110
+ }
4111
+ if (options.devoicedDb < 0) {
4112
+ const rampFrames = Math.max(1, Math.round(20 / frameMs));
4113
+ for (let i = 0; i < units.length; i++) {
4114
+ const unit = units[i];
4115
+ if (!unit.devoiced) continue;
4116
+ const start = unit.note_start_ms + timeline.leading_ms - baseMs;
4117
+ let endMs = start + unit.duration_ms;
4118
+ const next = units[i + 1];
4119
+ if (next) endMs = Math.min(endMs, next.position_ms - baseMs);
4120
+ const from = start / frameMs;
4121
+ const to = endMs / frameMs;
4122
+ for (let frame = Math.max(0, Math.floor(from)); frame < frames && frame < to + rampFrames; frame++) {
4123
+ const fadeIn = Math.min(1, Math.max(0, (frame - from) / rampFrames));
4124
+ const fadeOut = Math.min(
4125
+ 1,
4126
+ Math.max(0, (to + rampFrames - frame) / rampFrames)
4127
+ );
4128
+ curve[frame] += options.devoicedDb * Math.min(fadeIn, fadeOut);
4129
+ }
4130
+ }
4131
+ }
4132
+ applyGainFrames(audio, curve, frameSamples);
4133
+ }
3761
4134
  var UtauTTSAdapter = class _UtauTTSAdapter {
3762
4135
  constructor(worldline) {
3763
4136
  this.worldline = worldline;
@@ -3884,6 +4257,13 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
3884
4257
  const plan = JSON.parse(response.plan);
3885
4258
  const gains = options.prosody?.moraGains;
3886
4259
  if (gains) applyMoraGains(plan, gains);
4260
+ const devoiced = options.prosody?.devoiced;
4261
+ if (devoiced && devoiced.length === (plan.morae?.length ?? -1)) {
4262
+ for (const unit of plan.timeline.units) {
4263
+ if (devoiced[unit.position]) unit.devoiced = true;
4264
+ }
4265
+ }
4266
+ if (options.prosody) applyConsonantGaps(plan, options.prosody);
3887
4267
  return plan;
3888
4268
  }
3889
4269
  getPcm(bank, alias) {
@@ -3908,10 +4288,19 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
3908
4288
  firstChunkUnits = 3,
3909
4289
  chunkUnits = 6,
3910
4290
  seamCrossfadeMs = 20,
4291
+ normalizeUnitLoudness = true,
4292
+ unitLoudnessDb = -16,
4293
+ unitLoudnessMaxDb = 6,
4294
+ energyDbPerSemitone = 0.8,
4295
+ energyMaxDb = 6,
4296
+ devoicedDb = -9,
4297
+ outputLimit = 0.8,
3911
4298
  signal
3912
4299
  } = options;
3913
4300
  const timeline = plan.timeline;
3914
4301
  const units = timeline.units.filter((unit) => unit.length_ms > 0);
4302
+ const voicedF0 = timeline.f0_curve.filter((hz) => hz > 0);
4303
+ const referenceHz = voicedF0.length > 0 ? median(voicedF0) : 0;
3915
4304
  const ranges = planChunks(
3916
4305
  units,
3917
4306
  Math.max(1, firstChunkUnits),
@@ -3958,6 +4347,17 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
3958
4347
  voicing: options.voicing
3959
4348
  });
3960
4349
  if (!audio || audio.length === 0) continue;
4350
+ if (normalizeUnitLoudness || energyDbPerSemitone !== 0 || devoicedDb < 0) {
4351
+ shapeLoudness(audio, rendered, timeline, baseMs, {
4352
+ equalize: normalizeUnitLoudness,
4353
+ targetDb: unitLoudnessDb,
4354
+ maxDb: unitLoudnessMaxDb,
4355
+ energyDbPerSemitone,
4356
+ energyMaxDb,
4357
+ devoicedDb,
4358
+ referenceHz
4359
+ });
4360
+ }
3961
4361
  let fromSample = 0;
3962
4362
  let toSample = audio.length;
3963
4363
  let startMs = baseMs;
@@ -3978,6 +4378,7 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
3978
4378
  }
3979
4379
  if (toSample <= fromSample) continue;
3980
4380
  const pcm = audio.slice(fromSample, toSample);
4381
+ if (outputLimit > 0) softLimit2(pcm, outputLimit);
3981
4382
  if (range.headSeam) applyFade(pcm, 0, crossfadeSamples, true);
3982
4383
  if (range.tailSeam)
3983
4384
  applyFade(
@@ -4005,6 +4406,8 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
4005
4406
  const n = Math.min(chunk.pcm.length, out.length - offset);
4006
4407
  for (let k = 0; k < n; k++) out[offset + k] += chunk.pcm[k];
4007
4408
  }
4409
+ const limit = options.outputLimit ?? 0.8;
4410
+ if (any && limit > 0) softLimit2(out, limit);
4008
4411
  return any ? out : null;
4009
4412
  }
4010
4413
  };
@@ -4012,6 +4415,7 @@ export {
4012
4415
  KoeEngine,
4013
4416
  MIN_WORLDLINE_SAMPLES,
4014
4417
  NAIST_JDIC_FILES,
4418
+ SPEAKING_STYLES,
4015
4419
  UtauTTSAdapter,
4016
4420
  VoiceBank,
4017
4421
  WORLDLINE_SAMPLE_RATE,
@@ -4050,15 +4454,21 @@ export {
4050
4454
  parseKoeHeader,
4051
4455
  parseOto,
4052
4456
  parseWav,
4457
+ pauseKind,
4053
4458
  pcmBase,
4054
4459
  pitchFromAliasSuffix,
4055
4460
  readWavPcm48k,
4056
4461
  readingFromFeatures,
4057
4462
  resample,
4463
+ resolveSpeakingStyle,
4058
4464
  samplesToMs,
4465
+ shapeDurations,
4059
4466
  shapeProsody,
4060
4467
  sparse_features,
4061
4468
  splitKana,
4469
+ styleAlignOptions,
4470
+ styleRenderOptions,
4471
+ styleShapeOptions,
4062
4472
  suffixFromFolderName,
4063
4473
  summarise,
4064
4474
  toHiragana,
@@ -4067,6 +4477,7 @@ export {
4067
4477
  transcribe,
4068
4478
  trimToOto,
4069
4479
  unzipToFileMap,
4480
+ warpPitchCurve,
4070
4481
  zipFiles
4071
4482
  };
4072
4483
  //# sourceMappingURL=index.js.map