@onjmin/dtm 0.1.90 → 0.1.91

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.mts CHANGED
@@ -297,6 +297,8 @@ type VoiceExpression = {
297
297
  gender?: number;
298
298
  /** ブレシネス(息成分)。大きいほど息っぽく(ささやき寄り)。 */
299
299
  breathiness?: number;
300
+ /** テンション(張り/力強さ)。大きいほど張った・押した声(こぶし寄り、力強く歌う)。 */
301
+ tension?: number;
300
302
  };
301
303
  /** 歌唱合成モデルの実装シグネチャ */
302
304
  type VoiceModel = {
@@ -456,6 +458,11 @@ type StreamVoiceTrack = {
456
458
  * — klattフォールバックには効かない。
457
459
  */
458
460
  breathiness?: number;
461
+ /**
462
+ * テンション(張り/力強さ、"こぶし"寄り)0-1。既定0.5(無変化)。koe音源(Worldline)限定
463
+ * — klattフォールバックには効かない。
464
+ */
465
+ tension?: number;
459
466
  /**
460
467
  * オクターブユニゾン。各音節をもう1声、1オクターブ上/下(控えめな音量)で重ねて発音し、
461
468
  * 声に厚み(下)または煌びやかさ(上)を足す。既定 "none"(重ねない)。
@@ -680,6 +687,7 @@ type LyricSyncData = {
680
687
  vocalDelay?: number;
681
688
  vocalGender?: number;
682
689
  vocalBreathiness?: number;
690
+ vocalTension?: number;
683
691
  vocalOctaveUnison?: OctaveUnisonMode;
684
692
  };
685
693
  /** パッチ送受信用ノートデータ(ローカルIDを持たない)。 */
@@ -854,6 +862,12 @@ type LyricTrack = {
854
862
  * MMLでは `@@n klatt h70 …` のように h トークンで付与する。
855
863
  */
856
864
  breathiness?: number;
865
+ /**
866
+ * テンション(張り/力強さ)0-100。既定50(無変化)。大きいほど張った・押した声
867
+ * (こぶし寄り、力強く歌わせる)、小さいほど脱力・リラックス(ブレシネス寄り)になる。
868
+ * MMLでは `@@n klatt t70 …` のように t トークンで付与する。
869
+ */
870
+ tension?: number;
857
871
  /**
858
872
  * オクターブユニゾン。既定"none"。
859
873
  * "down"/"up"/"both" にすると各音節をもう1オクターブ上/下/両方(控えめな音量)で
package/dist/index.d.ts CHANGED
@@ -297,6 +297,8 @@ type VoiceExpression = {
297
297
  gender?: number;
298
298
  /** ブレシネス(息成分)。大きいほど息っぽく(ささやき寄り)。 */
299
299
  breathiness?: number;
300
+ /** テンション(張り/力強さ)。大きいほど張った・押した声(こぶし寄り、力強く歌う)。 */
301
+ tension?: number;
300
302
  };
301
303
  /** 歌唱合成モデルの実装シグネチャ */
302
304
  type VoiceModel = {
@@ -456,6 +458,11 @@ type StreamVoiceTrack = {
456
458
  * — klattフォールバックには効かない。
457
459
  */
458
460
  breathiness?: number;
461
+ /**
462
+ * テンション(張り/力強さ、"こぶし"寄り)0-1。既定0.5(無変化)。koe音源(Worldline)限定
463
+ * — klattフォールバックには効かない。
464
+ */
465
+ tension?: number;
459
466
  /**
460
467
  * オクターブユニゾン。各音節をもう1声、1オクターブ上/下(控えめな音量)で重ねて発音し、
461
468
  * 声に厚み(下)または煌びやかさ(上)を足す。既定 "none"(重ねない)。
@@ -680,6 +687,7 @@ type LyricSyncData = {
680
687
  vocalDelay?: number;
681
688
  vocalGender?: number;
682
689
  vocalBreathiness?: number;
690
+ vocalTension?: number;
683
691
  vocalOctaveUnison?: OctaveUnisonMode;
684
692
  };
685
693
  /** パッチ送受信用ノートデータ(ローカルIDを持たない)。 */
@@ -854,6 +862,12 @@ type LyricTrack = {
854
862
  * MMLでは `@@n klatt h70 …` のように h トークンで付与する。
855
863
  */
856
864
  breathiness?: number;
865
+ /**
866
+ * テンション(張り/力強さ)0-100。既定50(無変化)。大きいほど張った・押した声
867
+ * (こぶし寄り、力強く歌わせる)、小さいほど脱力・リラックス(ブレシネス寄り)になる。
868
+ * MMLでは `@@n klatt t70 …` のように t トークンで付与する。
869
+ */
870
+ tension?: number;
857
871
  /**
858
872
  * オクターブユニゾン。既定"none"。
859
873
  * "down"/"up"/"both" にすると各音節をもう1オクターブ上/下/両方(控えめな音量)で
package/dist/index.js CHANGED
@@ -2285,6 +2285,15 @@ var vibratoPitchCurve = (baseHz, preMs) => (tMs) => {
2285
2285
  return baseHz * 2 ** (cents / 1200);
2286
2286
  };
2287
2287
 
2288
+ // src/voice-worker-types.ts
2289
+ var COMPOSITE_ALIAS_SEP = "\0";
2290
+ var packCompositeAlias = (consonantAlias, vowelAlias) => `${COMPOSITE_ALIAS_SEP}${consonantAlias}${COMPOSITE_ALIAS_SEP}${vowelAlias}`;
2291
+ var unpackCompositeAlias = (alias) => {
2292
+ if (!alias.startsWith(COMPOSITE_ALIAS_SEP)) return null;
2293
+ const parts = alias.slice(COMPOSITE_ALIAS_SEP.length).split(COMPOSITE_ALIAS_SEP);
2294
+ return parts.length === 2 ? [parts[0], parts[1]] : null;
2295
+ };
2296
+
2288
2297
  // src/lyrics.ts
2289
2298
  var kanaTable = {
2290
2299
  \u3042: ["", "a"],
@@ -2463,9 +2472,10 @@ var parseLyrics = (mml) => {
2463
2472
  let delay = 0;
2464
2473
  let gender = 50;
2465
2474
  let breathiness = 50;
2475
+ let tension = 50;
2466
2476
  let octaveUnison = "none";
2467
2477
  const modelMatch = rest.match(
2468
- /^([a-z_][a-z0-9_]*?)(?=(?:[vqpobrghew]-?\d)|[^a-z0-9_]|$)(?::(\d+))?/i
2478
+ /^([a-z_][a-z0-9_]*?)(?=(?:[vqpobrghewt]-?\d)|[^a-z0-9_]|$)(?::(\d+))?/i
2469
2479
  );
2470
2480
  let model = "";
2471
2481
  const metaTokens = [];
@@ -2534,6 +2544,13 @@ var parseLyrics = (mml) => {
2534
2544
  rest = rest.substring(hMatch[0].length).trim();
2535
2545
  continue;
2536
2546
  }
2547
+ const tMatch = rest.match(/^t(\d+)/i);
2548
+ if (tMatch) {
2549
+ tension = clamp2(Number.parseInt(tMatch[1], 10), 0, 100);
2550
+ metaTokens.push(tMatch[0]);
2551
+ rest = rest.substring(tMatch[0].length).trim();
2552
+ continue;
2553
+ }
2537
2554
  const eMatch = rest.match(/^e(\d+)/i);
2538
2555
  if (eMatch) {
2539
2556
  delay = clamp2(Number.parseInt(eMatch[1], 10), 0, 100);
@@ -2567,6 +2584,7 @@ var parseLyrics = (mml) => {
2567
2584
  delay,
2568
2585
  gender,
2569
2586
  breathiness,
2587
+ tension,
2570
2588
  octaveUnison,
2571
2589
  syllables,
2572
2590
  metaText: metaTokens.join(" "),
@@ -2888,8 +2906,33 @@ var resolveKoeAlias = (hasAlias, pitchTokens, syl, prevVowel, noteNum) => {
2888
2906
  const hit = tryAlias(base);
2889
2907
  if (hit) return hit;
2890
2908
  }
2909
+ if (cons && vow) {
2910
+ const vowelAlias = tryAlias(vow) ?? tryAlias(`${pv} ${vow}`);
2911
+ const consAlias = vowelAlias ? tryAlias(cons) : null;
2912
+ if (vowelAlias && consAlias)
2913
+ return packCompositeAlias(consAlias, vowelAlias);
2914
+ }
2891
2915
  return null;
2892
2916
  };
2917
+ var COMPOSITE_SPLICE_XFADE_SEC = 5e-3;
2918
+ var spliceCompositePcm = (consonantPcm, vowelPcm, sampleRate) => {
2919
+ if (consonantPcm.length === 0 || vowelPcm.length === 0) return null;
2920
+ const xfade = Math.min(
2921
+ Math.floor(sampleRate * COMPOSITE_SPLICE_XFADE_SEC),
2922
+ consonantPcm.length,
2923
+ vowelPcm.length
2924
+ );
2925
+ const pcm = new Float64Array(consonantPcm.length + vowelPcm.length - xfade);
2926
+ pcm.set(consonantPcm, 0);
2927
+ for (let i2 = 0; i2 < xfade; i2++) {
2928
+ const t = (i2 + 1) / (xfade + 1);
2929
+ const idx = consonantPcm.length - xfade + i2;
2930
+ pcm[idx] = consonantPcm[idx] * (1 - t) + vowelPcm[i2] * t;
2931
+ }
2932
+ pcm.set(vowelPcm.subarray(xfade), consonantPcm.length);
2933
+ const consonantMs = (consonantPcm.length - xfade / 2) / sampleRate * 1e3;
2934
+ return { pcm, preMs: consonantMs, consonantMs };
2935
+ };
2893
2936
  var createLocalBackend = async (options) => {
2894
2937
  const bank = await VoiceBank.load(options.koe);
2895
2938
  const worldline = options.lightweight ? null : await Worldline.load({
@@ -2904,7 +2947,40 @@ var createLocalBackend = async (options) => {
2904
2947
  }
2905
2948
  return p;
2906
2949
  };
2950
+ const renderComposite = async (consonantAlias, vowelAlias, pitch, durationMs, vibrato, expr) => {
2951
+ if (!worldline) return null;
2952
+ const [consonantPcm, vowelPcm] = await Promise.all([
2953
+ getPcm(consonantAlias),
2954
+ getPcm(vowelAlias)
2955
+ ]);
2956
+ if (!consonantPcm || !vowelPcm) return null;
2957
+ const spliced = spliceCompositePcm(consonantPcm, vowelPcm, KOE_SAMPLE_RATE);
2958
+ if (!spliced) return null;
2959
+ const targetHz = midiToFreq(pitch);
2960
+ const audio = worldline.renderNote({
2961
+ pcm: spliced.pcm,
2962
+ pitch: vibrato ? vibratoPitchCurve(targetHz, spliced.preMs) : targetHz,
2963
+ durationMs,
2964
+ preMs: spliced.preMs,
2965
+ consonantMs: spliced.consonantMs,
2966
+ gender: expr?.gender,
2967
+ breathiness: expr?.breathiness,
2968
+ tension: expr?.tension
2969
+ });
2970
+ return audio ? { pcm: audio, preSec: spliced.preMs / 1e3, rate: 1 } : null;
2971
+ };
2907
2972
  const renderAlias = async (alias, pitch, durationMs, vibrato, expr) => {
2973
+ const composite = unpackCompositeAlias(alias);
2974
+ if (composite) {
2975
+ return renderComposite(
2976
+ composite[0],
2977
+ composite[1],
2978
+ pitch,
2979
+ durationMs,
2980
+ vibrato,
2981
+ expr
2982
+ );
2983
+ }
2908
2984
  const pcm = await getPcm(alias);
2909
2985
  if (!pcm || pcm.length === 0) return null;
2910
2986
  const entry = bank.manifest.phonemes[alias];
@@ -2917,7 +2993,8 @@ var createLocalBackend = async (options) => {
2917
2993
  durationMs,
2918
2994
  ...lead,
2919
2995
  gender: expr?.gender,
2920
- breathiness: expr?.breathiness
2996
+ breathiness: expr?.breathiness,
2997
+ tension: expr?.tension
2921
2998
  });
2922
2999
  if (audio) return { pcm: audio, preSec: lead.preMs / 1e3, rate: 1 };
2923
3000
  }
@@ -2998,7 +3075,8 @@ var createWorkerBackend = async (workerUrl, options) => {
2998
3075
  durationMs,
2999
3076
  vibrato,
3000
3077
  gender: expr?.gender,
3001
- breathiness: expr?.breathiness
3078
+ breathiness: expr?.breathiness,
3079
+ tension: expr?.tension
3002
3080
  });
3003
3081
  });
3004
3082
  return {
@@ -3027,7 +3105,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3027
3105
  const inflight = /* @__PURE__ */ new Map();
3028
3106
  const active = /* @__PURE__ */ new Set();
3029
3107
  let prevVowel = "";
3030
- const keyOf = (alias, pitch, durationMs, vibrato, expr) => `${alias}|${pitch}|${Math.round(durationMs / 10) * 10}${vibrato ? "|vib" : ""}${expr?.gender !== void 0 ? `|g${Math.round(expr.gender * 100)}` : ""}${expr?.breathiness !== void 0 ? `|h${Math.round(expr.breathiness * 100)}` : ""}`;
3108
+ const keyOf = (alias, pitch, durationMs, vibrato, expr) => `${alias}|${pitch}|${Math.round(durationMs / 10) * 10}${vibrato ? "|vib" : ""}${expr?.gender !== void 0 ? `|g${Math.round(expr.gender * 100)}` : ""}${expr?.breathiness !== void 0 ? `|h${Math.round(expr.breathiness * 100)}` : ""}${expr?.tension !== void 0 ? `|t${Math.round(expr.tension * 100)}` : ""}`;
3031
3109
  const renderInto = (alias, pitch, durationMs, vibrato, expr) => {
3032
3110
  const key = keyOf(alias, pitch, durationMs, vibrato, expr);
3033
3111
  const existing = renderCache.get(key);
@@ -3258,7 +3336,8 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3258
3336
  let n = 0;
3259
3337
  const expr = {
3260
3338
  gender: track.gender,
3261
- breathiness: track.breathiness
3339
+ breathiness: track.breathiness,
3340
+ tension: track.tension
3262
3341
  };
3263
3342
  forEachSungNote(track, (note, prevVowel) => {
3264
3343
  if (n >= count && note.startSec >= STREAM_LOOKAHEAD_SEC) return;
@@ -3345,7 +3424,11 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3345
3424
  pitch,
3346
3425
  note.durationSec * 1e3,
3347
3426
  track.vibrato,
3348
- { gender: track.gender, breathiness: track.breathiness }
3427
+ {
3428
+ gender: track.gender,
3429
+ breathiness: track.breathiness,
3430
+ tension: track.tension
3431
+ }
3349
3432
  );
3350
3433
  if (session !== streamSession) return;
3351
3434
  if (key) {
@@ -9572,6 +9655,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9572
9655
  delaySend: (lt.delay ?? 0) / 100,
9573
9656
  gender: (lt.gender ?? 50) / 100,
9574
9657
  breathiness: (lt.breathiness ?? 50) / 100,
9658
+ tension: (lt.tension ?? 50) / 100,
9575
9659
  octaveUnison: lt.octaveUnison,
9576
9660
  notes
9577
9661
  };
@@ -13422,6 +13506,16 @@ var BREATHINESS_INFO_HTML = `
13422
13506
  <p>\u81EA\u52D5\u30D3\u30D6\u30E9\u30FC\u30C8\u3068\u7D44\u307F\u5408\u308F\u305B\u308B\u3068\u3001\u63FA\u308C\u306A\u304C\u3089\u606F\u3063\u307D\u3044\u3001\u3088\u308A\u30A8\u30E2\u30FC\u30B7\u30E7\u30CA\u30EB\u306A\u8868\u73FE\u306B\u306A\u308A\u3084\u3059\u3044\u3067\u3059\u3002koe\u97F3\u6E90\uFF08UTAU\u7531\u6765\u306E.koe\u97F3\u6E90\uFF09\u9650\u5B9A\u306E\u52B9\u679C\u3067\u3001klatt\uFF08\u5185\u8535\u306E\u7C21\u6613\u5408\u6210\uFF09\u3067\u306F\u5909\u5316\u3057\u307E\u305B\u3093\u3002</p>
13423
13507
  </div>
13424
13508
  `;
13509
+ var TENSION_INFO_HTML = `
13510
+ <div class="dtm-modal-body-content">
13511
+ <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
13512
+ <p>\u58F0\u306E\u5F35\u308A\uFF0F\u62BC\u3057\u51FA\u3057\u306E\u5F37\u3055\u3067\u3059\u300250\u304C\u7121\u5909\u5316\u3001\u5927\u304D\u3044\u307B\u3069\u5F35\u3063\u305F\u30FB\u62BC\u3057\u305F\u58F0\uFF08\u300C\u3053\u3076\u3057\u300D\u5BC4\u308A\u3001\u529B\u5F37\u304F\u6B4C\u308F\u305B\u308B\uFF09\u306B\u306A\u308A\u3001\u5C0F\u3055\u3044\u307B\u3069\u8131\u529B\u3057\u305F\u30EA\u30E9\u30C3\u30AF\u30B9\u3057\u305F\u58F0\u306B\u306A\u308A\u307E\u3059\u3002</p>
13513
+ <h4>\u30D6\u30EC\u30B7\u30CD\u30B9\u3068\u306E\u9055\u3044</h4>
13514
+ <p>\u30D6\u30EC\u30B7\u30CD\u30B9\u306F\u606F\u6210\u5206\u306E\u91CF\uFF08\u82AF\u306E\u3042\u308B\u58F0\u21D4\u606F\u3063\u307D\u3044\u58F0\uFF09\u3092\u52D5\u304B\u3057\u307E\u3059\u304C\u3001\u30C6\u30F3\u30B7\u30E7\u30F3\u306F\u58F0\u5E2F\u306E\u7DE0\u307E\u308A\u30FB\u62BC\u3057\u306E\u5F37\u3055\uFF08\u8131\u529B\u21D4\u5F35\u3063\u305F\u58F0\uFF09\u3092\u52D5\u304B\u3057\u307E\u3059\u3002\u30C6\u30F3\u30B7\u30E7\u30F3\u3092\u4E0A\u3052\u308B\u3068\u30B5\u30D3\u306E\u529B\u5F37\u3055\u3001\u4E0B\u3052\u308B\u3068\u7A4F\u3084\u304B\u306A\u56C1\u304D\u306B\u5BC4\u305B\u305F\u6B4C\u308F\u305B\u65B9\u306B\u306A\u308A\u307E\u3059\u3002</p>
13515
+ <h4>\u4ED6\u306E\u8A2D\u5B9A\u3068\u306E\u517C\u306D\u5408\u3044</h4>
13516
+ <p>\u30D6\u30EC\u30B7\u30CD\u30B9\u3092\u4E0B\u3052\u3064\u3064\u30C6\u30F3\u30B7\u30E7\u30F3\u3092\u4E0A\u3052\u308B\u3068\u3001\u3088\u308A\u82AF\u304C\u3042\u308A\u529B\u5F37\u3044\u6B4C\u58F0\u306B\u306A\u308A\u307E\u3059\u3002\u9006\u306B\u30D6\u30EC\u30B7\u30CD\u30B9\u3092\u4E0A\u3052\u3064\u3064\u30C6\u30F3\u30B7\u30E7\u30F3\u3092\u4E0B\u3052\u308B\u3068\u3001\u3088\u308A\u8131\u529B\u3057\u305F\u606F\u3063\u307D\u3044\u6B4C\u58F0\u306B\u306A\u308A\u307E\u3059\u3002koe\u97F3\u6E90\uFF08UTAU\u7531\u6765\u306E.koe\u97F3\u6E90\uFF09\u9650\u5B9A\u306E\u52B9\u679C\u3067\u3001klatt\uFF08\u5185\u8535\u306E\u7C21\u6613\u5408\u6210\uFF09\u3067\u306F\u5909\u5316\u3057\u307E\u305B\u3093\u3002</p>
13517
+ </div>
13518
+ `;
13425
13519
  var LYRIC_REVERB_INFO_HTML = `
13426
13520
  <div class="dtm-modal-body-content">
13427
13521
  <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
@@ -13946,6 +14040,7 @@ var mountDAW = (target, options = {}) => {
13946
14040
  vocalDelay: t.vocalDelay,
13947
14041
  vocalGender: t.vocalGender,
13948
14042
  vocalBreathiness: t.vocalBreathiness,
14043
+ vocalTension: t.vocalTension,
13949
14044
  vocalOctaveUnison: t.vocalOctaveUnison
13950
14045
  };
13951
14046
  if (lyricsDebounceTimer) clearTimeout(lyricsDebounceTimer);
@@ -14011,6 +14106,7 @@ var mountDAW = (target, options = {}) => {
14011
14106
  vocalDelay: 0,
14012
14107
  vocalGender: 50,
14013
14108
  vocalBreathiness: 50,
14109
+ vocalTension: 50,
14014
14110
  vocalOctaveUnison: "none",
14015
14111
  trackInstrument: "",
14016
14112
  trackCompression: 0,
@@ -14042,6 +14138,7 @@ var mountDAW = (target, options = {}) => {
14042
14138
  delay: t.vocalDelay,
14043
14139
  gender: t.vocalGender,
14044
14140
  breathiness: t.vocalBreathiness,
14141
+ tension: t.vocalTension,
14045
14142
  octaveUnison: t.vocalOctaveUnison,
14046
14143
  syllables
14047
14144
  });
@@ -14852,6 +14949,7 @@ var mountDAW = (target, options = {}) => {
14852
14949
  delaySend: (lt.delay ?? 0) / 100,
14853
14950
  gender: (lt.gender ?? 50) / 100,
14854
14951
  breathiness: (lt.breathiness ?? 50) / 100,
14952
+ tension: (lt.tension ?? 50) / 100,
14855
14953
  octaveUnison: lt.octaveUnison,
14856
14954
  notes
14857
14955
  };
@@ -15280,6 +15378,12 @@ var mountDAW = (target, options = {}) => {
15280
15378
  <span class="dtm-label" data-dtm="lyric-breathiness-label"></span>
15281
15379
  <button class="dtm-infobtn" data-dtm="lyric-breathiness-info" title="\u30D6\u30EC\u30B7\u30CD\u30B9\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
15282
15380
  </div>
15381
+ <div class="dtm-row">
15382
+ <span class="dtm-label">\u30C6\u30F3\u30B7\u30E7\u30F3</span>
15383
+ <input type="range" class="dtm-range dtm-grow" data-dtm="lyric-tension" min="0" max="100" aria-label="\u30C6\u30F3\u30B7\u30E7\u30F3\uFF08\u5F35\u308A/\u529B\u5F37\u3055\u3001koe\u97F3\u6E90\u9650\u5B9A\uFF09">
15384
+ <span class="dtm-label" data-dtm="lyric-tension-label"></span>
15385
+ <button class="dtm-infobtn" data-dtm="lyric-tension-info" title="\u30C6\u30F3\u30B7\u30E7\u30F3\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
15386
+ </div>
15283
15387
  <div class="dtm-row">
15284
15388
  <span class="dtm-label" style="display:inline-flex;align-items:center;gap:2px">
15285
15389
  <input type="checkbox" data-dtm="lyric-vibrato" aria-label="\u81EA\u52D5\u30D3\u30D6\u30E9\u30FC\u30C8">\u30D3\u30D6\u30E9\u30FC\u30C8
@@ -15383,6 +15487,18 @@ var mountDAW = (target, options = {}) => {
15383
15487
  lyricBreathinessInfo.addEventListener("click", () => {
15384
15488
  showModal("\u30D6\u30EC\u30B7\u30CD\u30B9\u306E\u89E3\u8AAC", BREATHINESS_INFO_HTML);
15385
15489
  });
15490
+ const lyricTension = lyricDiv.querySelector(
15491
+ '[data-dtm="lyric-tension"]'
15492
+ );
15493
+ const lyricTensionLabel = lyricDiv.querySelector(
15494
+ '[data-dtm="lyric-tension-label"]'
15495
+ );
15496
+ const lyricTensionInfo = lyricDiv.querySelector(
15497
+ '[data-dtm="lyric-tension-info"]'
15498
+ );
15499
+ lyricTensionInfo.addEventListener("click", () => {
15500
+ showModal("\u30C6\u30F3\u30B7\u30E7\u30F3\u306E\u89E3\u8AAC", TENSION_INFO_HTML);
15501
+ });
15386
15502
  const lyricVibrato = lyricDiv.querySelector(
15387
15503
  '[data-dtm="lyric-vibrato"]'
15388
15504
  );
@@ -15470,6 +15586,8 @@ var mountDAW = (target, options = {}) => {
15470
15586
  lyricGenderLabel.textContent = `${active.vocalGender}`;
15471
15587
  lyricBreathiness.value = String(active.vocalBreathiness);
15472
15588
  lyricBreathinessLabel.textContent = `${active.vocalBreathiness}`;
15589
+ lyricTension.value = String(active.vocalTension);
15590
+ lyricTensionLabel.textContent = `${active.vocalTension}`;
15473
15591
  lyricVibrato.checked = active.vocalVibrato;
15474
15592
  lyricOctaveUnison.value = active.vocalOctaveUnison;
15475
15593
  const updateLyricCount = () => {
@@ -15640,6 +15758,11 @@ var mountDAW = (target, options = {}) => {
15640
15758
  lyricBreathinessLabel.textContent = `${active.vocalBreathiness}`;
15641
15759
  fireLyricsChange(active);
15642
15760
  });
15761
+ lyricTension.addEventListener("input", () => {
15762
+ active.vocalTension = Number.parseInt(lyricTension.value, 10);
15763
+ lyricTensionLabel.textContent = `${active.vocalTension}`;
15764
+ fireLyricsChange(active);
15765
+ });
15643
15766
  }
15644
15767
  if (active.config.id === "chord" && showChord) {
15645
15768
  const div = document.createElement("div");
@@ -15844,6 +15967,7 @@ var mountDAW = (target, options = {}) => {
15844
15967
  del: t.vocalDelay,
15845
15968
  gen: t.vocalGender,
15846
15969
  bre: t.vocalBreathiness,
15970
+ ten: t.vocalTension,
15847
15971
  uni: t.vocalOctaveUnison
15848
15972
  })).filter(
15849
15973
  (x2) => x2.model.length > 0 && x2.text.length > 0 && x2.notes.length > 0
@@ -15858,6 +15982,7 @@ var mountDAW = (target, options = {}) => {
15858
15982
  x2.del === 0 ? "" : `e${x2.del}`,
15859
15983
  x2.gen === 50 ? "" : `g${x2.gen}`,
15860
15984
  x2.bre === 50 ? "" : `h${x2.bre}`,
15985
+ x2.ten === 50 ? "" : `t${x2.ten}`,
15861
15986
  OCTAVE_UNISON_TOKEN[x2.uni]
15862
15987
  ].filter((s) => s.length > 0).join(" ");
15863
15988
  const head = params ? `${x2.model} ${params}` : x2.model;
@@ -16100,6 +16225,7 @@ var mountDAW = (target, options = {}) => {
16100
16225
  active.vocalDelay = 0;
16101
16226
  active.vocalGender = 50;
16102
16227
  active.vocalBreathiness = 50;
16228
+ active.vocalTension = 50;
16103
16229
  active.vocalOctaveUnison = "none";
16104
16230
  } else {
16105
16231
  for (const t of trackStates) {
@@ -16114,6 +16240,7 @@ var mountDAW = (target, options = {}) => {
16114
16240
  t.vocalDelay = 0;
16115
16241
  t.vocalGender = 50;
16116
16242
  t.vocalBreathiness = 50;
16243
+ t.vocalTension = 50;
16117
16244
  t.vocalOctaveUnison = "none";
16118
16245
  }
16119
16246
  }
@@ -16132,6 +16259,8 @@ var mountDAW = (target, options = {}) => {
16132
16259
  t.vocalDelay = lt.delay ?? 0;
16133
16260
  t.vocalGender = lt.gender ?? 50;
16134
16261
  t.vocalBreathiness = lt.breathiness ?? 50;
16262
+ t.vocalTension = lt.tension ?? 50;
16263
+ t.vocalTension = lt.tension ?? 50;
16135
16264
  t.vocalOctaveUnison = lt.octaveUnison ?? "none";
16136
16265
  });
16137
16266
  for (const p of placements) {
@@ -16216,6 +16345,7 @@ var mountDAW = (target, options = {}) => {
16216
16345
  active.vocalDelay = 0;
16217
16346
  active.vocalGender = 50;
16218
16347
  active.vocalBreathiness = 50;
16348
+ active.vocalTension = 50;
16219
16349
  active.vocalOctaveUnison = "none";
16220
16350
  } else {
16221
16351
  clearAll();
@@ -16232,6 +16362,7 @@ var mountDAW = (target, options = {}) => {
16232
16362
  t.vocalDelay = 0;
16233
16363
  t.vocalGender = 50;
16234
16364
  t.vocalBreathiness = 50;
16365
+ t.vocalTension = 50;
16235
16366
  t.vocalOctaveUnison = "none";
16236
16367
  }
16237
16368
  }
@@ -17596,6 +17727,7 @@ var mountDAW = (target, options = {}) => {
17596
17727
  t.vocalDelay = data.vocalDelay ?? 0;
17597
17728
  t.vocalGender = data.vocalGender ?? 50;
17598
17729
  t.vocalBreathiness = data.vocalBreathiness ?? 50;
17730
+ t.vocalTension = data.vocalTension ?? 50;
17599
17731
  t.vocalOctaveUnison = data.vocalOctaveUnison ?? "none";
17600
17732
  },
17601
17733
  applyTrackInstrument: (trackIndex, instrumentName) => {
@@ -17718,6 +17850,7 @@ var playSingingMML = async (mml, options = {}) => {
17718
17850
  delaySend: (lt.delay ?? 0) / 100,
17719
17851
  gender: (lt.gender ?? 50) / 100,
17720
17852
  breathiness: (lt.breathiness ?? 50) / 100,
17853
+ tension: (lt.tension ?? 50) / 100,
17721
17854
  octaveUnison: lt.octaveUnison,
17722
17855
  notes
17723
17856
  };
package/dist/index.mjs CHANGED
@@ -2154,6 +2154,15 @@ var vibratoPitchCurve = (baseHz, preMs) => (tMs) => {
2154
2154
  return baseHz * 2 ** (cents / 1200);
2155
2155
  };
2156
2156
 
2157
+ // src/voice-worker-types.ts
2158
+ var COMPOSITE_ALIAS_SEP = "\0";
2159
+ var packCompositeAlias = (consonantAlias, vowelAlias) => `${COMPOSITE_ALIAS_SEP}${consonantAlias}${COMPOSITE_ALIAS_SEP}${vowelAlias}`;
2160
+ var unpackCompositeAlias = (alias) => {
2161
+ if (!alias.startsWith(COMPOSITE_ALIAS_SEP)) return null;
2162
+ const parts = alias.slice(COMPOSITE_ALIAS_SEP.length).split(COMPOSITE_ALIAS_SEP);
2163
+ return parts.length === 2 ? [parts[0], parts[1]] : null;
2164
+ };
2165
+
2157
2166
  // src/lyrics.ts
2158
2167
  var kanaTable = {
2159
2168
  \u3042: ["", "a"],
@@ -2332,9 +2341,10 @@ var parseLyrics = (mml) => {
2332
2341
  let delay = 0;
2333
2342
  let gender = 50;
2334
2343
  let breathiness = 50;
2344
+ let tension = 50;
2335
2345
  let octaveUnison = "none";
2336
2346
  const modelMatch = rest.match(
2337
- /^([a-z_][a-z0-9_]*?)(?=(?:[vqpobrghew]-?\d)|[^a-z0-9_]|$)(?::(\d+))?/i
2347
+ /^([a-z_][a-z0-9_]*?)(?=(?:[vqpobrghewt]-?\d)|[^a-z0-9_]|$)(?::(\d+))?/i
2338
2348
  );
2339
2349
  let model = "";
2340
2350
  const metaTokens = [];
@@ -2403,6 +2413,13 @@ var parseLyrics = (mml) => {
2403
2413
  rest = rest.substring(hMatch[0].length).trim();
2404
2414
  continue;
2405
2415
  }
2416
+ const tMatch = rest.match(/^t(\d+)/i);
2417
+ if (tMatch) {
2418
+ tension = clamp2(Number.parseInt(tMatch[1], 10), 0, 100);
2419
+ metaTokens.push(tMatch[0]);
2420
+ rest = rest.substring(tMatch[0].length).trim();
2421
+ continue;
2422
+ }
2406
2423
  const eMatch = rest.match(/^e(\d+)/i);
2407
2424
  if (eMatch) {
2408
2425
  delay = clamp2(Number.parseInt(eMatch[1], 10), 0, 100);
@@ -2436,6 +2453,7 @@ var parseLyrics = (mml) => {
2436
2453
  delay,
2437
2454
  gender,
2438
2455
  breathiness,
2456
+ tension,
2439
2457
  octaveUnison,
2440
2458
  syllables,
2441
2459
  metaText: metaTokens.join(" "),
@@ -2757,8 +2775,33 @@ var resolveKoeAlias = (hasAlias, pitchTokens, syl, prevVowel, noteNum) => {
2757
2775
  const hit = tryAlias(base);
2758
2776
  if (hit) return hit;
2759
2777
  }
2778
+ if (cons && vow) {
2779
+ const vowelAlias = tryAlias(vow) ?? tryAlias(`${pv} ${vow}`);
2780
+ const consAlias = vowelAlias ? tryAlias(cons) : null;
2781
+ if (vowelAlias && consAlias)
2782
+ return packCompositeAlias(consAlias, vowelAlias);
2783
+ }
2760
2784
  return null;
2761
2785
  };
2786
+ var COMPOSITE_SPLICE_XFADE_SEC = 5e-3;
2787
+ var spliceCompositePcm = (consonantPcm, vowelPcm, sampleRate) => {
2788
+ if (consonantPcm.length === 0 || vowelPcm.length === 0) return null;
2789
+ const xfade = Math.min(
2790
+ Math.floor(sampleRate * COMPOSITE_SPLICE_XFADE_SEC),
2791
+ consonantPcm.length,
2792
+ vowelPcm.length
2793
+ );
2794
+ const pcm = new Float64Array(consonantPcm.length + vowelPcm.length - xfade);
2795
+ pcm.set(consonantPcm, 0);
2796
+ for (let i2 = 0; i2 < xfade; i2++) {
2797
+ const t = (i2 + 1) / (xfade + 1);
2798
+ const idx = consonantPcm.length - xfade + i2;
2799
+ pcm[idx] = consonantPcm[idx] * (1 - t) + vowelPcm[i2] * t;
2800
+ }
2801
+ pcm.set(vowelPcm.subarray(xfade), consonantPcm.length);
2802
+ const consonantMs = (consonantPcm.length - xfade / 2) / sampleRate * 1e3;
2803
+ return { pcm, preMs: consonantMs, consonantMs };
2804
+ };
2762
2805
  var createLocalBackend = async (options) => {
2763
2806
  const bank = await VoiceBank.load(options.koe);
2764
2807
  const worldline = options.lightweight ? null : await Worldline.load({
@@ -2773,7 +2816,40 @@ var createLocalBackend = async (options) => {
2773
2816
  }
2774
2817
  return p;
2775
2818
  };
2819
+ const renderComposite = async (consonantAlias, vowelAlias, pitch, durationMs, vibrato, expr) => {
2820
+ if (!worldline) return null;
2821
+ const [consonantPcm, vowelPcm] = await Promise.all([
2822
+ getPcm(consonantAlias),
2823
+ getPcm(vowelAlias)
2824
+ ]);
2825
+ if (!consonantPcm || !vowelPcm) return null;
2826
+ const spliced = spliceCompositePcm(consonantPcm, vowelPcm, KOE_SAMPLE_RATE);
2827
+ if (!spliced) return null;
2828
+ const targetHz = midiToFreq(pitch);
2829
+ const audio = worldline.renderNote({
2830
+ pcm: spliced.pcm,
2831
+ pitch: vibrato ? vibratoPitchCurve(targetHz, spliced.preMs) : targetHz,
2832
+ durationMs,
2833
+ preMs: spliced.preMs,
2834
+ consonantMs: spliced.consonantMs,
2835
+ gender: expr?.gender,
2836
+ breathiness: expr?.breathiness,
2837
+ tension: expr?.tension
2838
+ });
2839
+ return audio ? { pcm: audio, preSec: spliced.preMs / 1e3, rate: 1 } : null;
2840
+ };
2776
2841
  const renderAlias = async (alias, pitch, durationMs, vibrato, expr) => {
2842
+ const composite = unpackCompositeAlias(alias);
2843
+ if (composite) {
2844
+ return renderComposite(
2845
+ composite[0],
2846
+ composite[1],
2847
+ pitch,
2848
+ durationMs,
2849
+ vibrato,
2850
+ expr
2851
+ );
2852
+ }
2777
2853
  const pcm = await getPcm(alias);
2778
2854
  if (!pcm || pcm.length === 0) return null;
2779
2855
  const entry = bank.manifest.phonemes[alias];
@@ -2786,7 +2862,8 @@ var createLocalBackend = async (options) => {
2786
2862
  durationMs,
2787
2863
  ...lead,
2788
2864
  gender: expr?.gender,
2789
- breathiness: expr?.breathiness
2865
+ breathiness: expr?.breathiness,
2866
+ tension: expr?.tension
2790
2867
  });
2791
2868
  if (audio) return { pcm: audio, preSec: lead.preMs / 1e3, rate: 1 };
2792
2869
  }
@@ -2867,7 +2944,8 @@ var createWorkerBackend = async (workerUrl, options) => {
2867
2944
  durationMs,
2868
2945
  vibrato,
2869
2946
  gender: expr?.gender,
2870
- breathiness: expr?.breathiness
2947
+ breathiness: expr?.breathiness,
2948
+ tension: expr?.tension
2871
2949
  });
2872
2950
  });
2873
2951
  return {
@@ -2896,7 +2974,7 @@ var createKoeVoice = async (ctx, destination, options) => {
2896
2974
  const inflight = /* @__PURE__ */ new Map();
2897
2975
  const active = /* @__PURE__ */ new Set();
2898
2976
  let prevVowel = "";
2899
- const keyOf = (alias, pitch, durationMs, vibrato, expr) => `${alias}|${pitch}|${Math.round(durationMs / 10) * 10}${vibrato ? "|vib" : ""}${expr?.gender !== void 0 ? `|g${Math.round(expr.gender * 100)}` : ""}${expr?.breathiness !== void 0 ? `|h${Math.round(expr.breathiness * 100)}` : ""}`;
2977
+ const keyOf = (alias, pitch, durationMs, vibrato, expr) => `${alias}|${pitch}|${Math.round(durationMs / 10) * 10}${vibrato ? "|vib" : ""}${expr?.gender !== void 0 ? `|g${Math.round(expr.gender * 100)}` : ""}${expr?.breathiness !== void 0 ? `|h${Math.round(expr.breathiness * 100)}` : ""}${expr?.tension !== void 0 ? `|t${Math.round(expr.tension * 100)}` : ""}`;
2900
2978
  const renderInto = (alias, pitch, durationMs, vibrato, expr) => {
2901
2979
  const key = keyOf(alias, pitch, durationMs, vibrato, expr);
2902
2980
  const existing = renderCache.get(key);
@@ -3127,7 +3205,8 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3127
3205
  let n = 0;
3128
3206
  const expr = {
3129
3207
  gender: track.gender,
3130
- breathiness: track.breathiness
3208
+ breathiness: track.breathiness,
3209
+ tension: track.tension
3131
3210
  };
3132
3211
  forEachSungNote(track, (note, prevVowel) => {
3133
3212
  if (n >= count && note.startSec >= STREAM_LOOKAHEAD_SEC) return;
@@ -3214,7 +3293,11 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3214
3293
  pitch,
3215
3294
  note.durationSec * 1e3,
3216
3295
  track.vibrato,
3217
- { gender: track.gender, breathiness: track.breathiness }
3296
+ {
3297
+ gender: track.gender,
3298
+ breathiness: track.breathiness,
3299
+ tension: track.tension
3300
+ }
3218
3301
  );
3219
3302
  if (session !== streamSession) return;
3220
3303
  if (key) {
@@ -9441,6 +9524,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9441
9524
  delaySend: (lt.delay ?? 0) / 100,
9442
9525
  gender: (lt.gender ?? 50) / 100,
9443
9526
  breathiness: (lt.breathiness ?? 50) / 100,
9527
+ tension: (lt.tension ?? 50) / 100,
9444
9528
  octaveUnison: lt.octaveUnison,
9445
9529
  notes
9446
9530
  };
@@ -13291,6 +13375,16 @@ var BREATHINESS_INFO_HTML = `
13291
13375
  <p>\u81EA\u52D5\u30D3\u30D6\u30E9\u30FC\u30C8\u3068\u7D44\u307F\u5408\u308F\u305B\u308B\u3068\u3001\u63FA\u308C\u306A\u304C\u3089\u606F\u3063\u307D\u3044\u3001\u3088\u308A\u30A8\u30E2\u30FC\u30B7\u30E7\u30CA\u30EB\u306A\u8868\u73FE\u306B\u306A\u308A\u3084\u3059\u3044\u3067\u3059\u3002koe\u97F3\u6E90\uFF08UTAU\u7531\u6765\u306E.koe\u97F3\u6E90\uFF09\u9650\u5B9A\u306E\u52B9\u679C\u3067\u3001klatt\uFF08\u5185\u8535\u306E\u7C21\u6613\u5408\u6210\uFF09\u3067\u306F\u5909\u5316\u3057\u307E\u305B\u3093\u3002</p>
13292
13376
  </div>
13293
13377
  `;
13378
+ var TENSION_INFO_HTML = `
13379
+ <div class="dtm-modal-body-content">
13380
+ <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
13381
+ <p>\u58F0\u306E\u5F35\u308A\uFF0F\u62BC\u3057\u51FA\u3057\u306E\u5F37\u3055\u3067\u3059\u300250\u304C\u7121\u5909\u5316\u3001\u5927\u304D\u3044\u307B\u3069\u5F35\u3063\u305F\u30FB\u62BC\u3057\u305F\u58F0\uFF08\u300C\u3053\u3076\u3057\u300D\u5BC4\u308A\u3001\u529B\u5F37\u304F\u6B4C\u308F\u305B\u308B\uFF09\u306B\u306A\u308A\u3001\u5C0F\u3055\u3044\u307B\u3069\u8131\u529B\u3057\u305F\u30EA\u30E9\u30C3\u30AF\u30B9\u3057\u305F\u58F0\u306B\u306A\u308A\u307E\u3059\u3002</p>
13382
+ <h4>\u30D6\u30EC\u30B7\u30CD\u30B9\u3068\u306E\u9055\u3044</h4>
13383
+ <p>\u30D6\u30EC\u30B7\u30CD\u30B9\u306F\u606F\u6210\u5206\u306E\u91CF\uFF08\u82AF\u306E\u3042\u308B\u58F0\u21D4\u606F\u3063\u307D\u3044\u58F0\uFF09\u3092\u52D5\u304B\u3057\u307E\u3059\u304C\u3001\u30C6\u30F3\u30B7\u30E7\u30F3\u306F\u58F0\u5E2F\u306E\u7DE0\u307E\u308A\u30FB\u62BC\u3057\u306E\u5F37\u3055\uFF08\u8131\u529B\u21D4\u5F35\u3063\u305F\u58F0\uFF09\u3092\u52D5\u304B\u3057\u307E\u3059\u3002\u30C6\u30F3\u30B7\u30E7\u30F3\u3092\u4E0A\u3052\u308B\u3068\u30B5\u30D3\u306E\u529B\u5F37\u3055\u3001\u4E0B\u3052\u308B\u3068\u7A4F\u3084\u304B\u306A\u56C1\u304D\u306B\u5BC4\u305B\u305F\u6B4C\u308F\u305B\u65B9\u306B\u306A\u308A\u307E\u3059\u3002</p>
13384
+ <h4>\u4ED6\u306E\u8A2D\u5B9A\u3068\u306E\u517C\u306D\u5408\u3044</h4>
13385
+ <p>\u30D6\u30EC\u30B7\u30CD\u30B9\u3092\u4E0B\u3052\u3064\u3064\u30C6\u30F3\u30B7\u30E7\u30F3\u3092\u4E0A\u3052\u308B\u3068\u3001\u3088\u308A\u82AF\u304C\u3042\u308A\u529B\u5F37\u3044\u6B4C\u58F0\u306B\u306A\u308A\u307E\u3059\u3002\u9006\u306B\u30D6\u30EC\u30B7\u30CD\u30B9\u3092\u4E0A\u3052\u3064\u3064\u30C6\u30F3\u30B7\u30E7\u30F3\u3092\u4E0B\u3052\u308B\u3068\u3001\u3088\u308A\u8131\u529B\u3057\u305F\u606F\u3063\u307D\u3044\u6B4C\u58F0\u306B\u306A\u308A\u307E\u3059\u3002koe\u97F3\u6E90\uFF08UTAU\u7531\u6765\u306E.koe\u97F3\u6E90\uFF09\u9650\u5B9A\u306E\u52B9\u679C\u3067\u3001klatt\uFF08\u5185\u8535\u306E\u7C21\u6613\u5408\u6210\uFF09\u3067\u306F\u5909\u5316\u3057\u307E\u305B\u3093\u3002</p>
13386
+ </div>
13387
+ `;
13294
13388
  var LYRIC_REVERB_INFO_HTML = `
13295
13389
  <div class="dtm-modal-body-content">
13296
13390
  <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
@@ -13815,6 +13909,7 @@ var mountDAW = (target, options = {}) => {
13815
13909
  vocalDelay: t.vocalDelay,
13816
13910
  vocalGender: t.vocalGender,
13817
13911
  vocalBreathiness: t.vocalBreathiness,
13912
+ vocalTension: t.vocalTension,
13818
13913
  vocalOctaveUnison: t.vocalOctaveUnison
13819
13914
  };
13820
13915
  if (lyricsDebounceTimer) clearTimeout(lyricsDebounceTimer);
@@ -13880,6 +13975,7 @@ var mountDAW = (target, options = {}) => {
13880
13975
  vocalDelay: 0,
13881
13976
  vocalGender: 50,
13882
13977
  vocalBreathiness: 50,
13978
+ vocalTension: 50,
13883
13979
  vocalOctaveUnison: "none",
13884
13980
  trackInstrument: "",
13885
13981
  trackCompression: 0,
@@ -13911,6 +14007,7 @@ var mountDAW = (target, options = {}) => {
13911
14007
  delay: t.vocalDelay,
13912
14008
  gender: t.vocalGender,
13913
14009
  breathiness: t.vocalBreathiness,
14010
+ tension: t.vocalTension,
13914
14011
  octaveUnison: t.vocalOctaveUnison,
13915
14012
  syllables
13916
14013
  });
@@ -14721,6 +14818,7 @@ var mountDAW = (target, options = {}) => {
14721
14818
  delaySend: (lt.delay ?? 0) / 100,
14722
14819
  gender: (lt.gender ?? 50) / 100,
14723
14820
  breathiness: (lt.breathiness ?? 50) / 100,
14821
+ tension: (lt.tension ?? 50) / 100,
14724
14822
  octaveUnison: lt.octaveUnison,
14725
14823
  notes
14726
14824
  };
@@ -15149,6 +15247,12 @@ var mountDAW = (target, options = {}) => {
15149
15247
  <span class="dtm-label" data-dtm="lyric-breathiness-label"></span>
15150
15248
  <button class="dtm-infobtn" data-dtm="lyric-breathiness-info" title="\u30D6\u30EC\u30B7\u30CD\u30B9\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
15151
15249
  </div>
15250
+ <div class="dtm-row">
15251
+ <span class="dtm-label">\u30C6\u30F3\u30B7\u30E7\u30F3</span>
15252
+ <input type="range" class="dtm-range dtm-grow" data-dtm="lyric-tension" min="0" max="100" aria-label="\u30C6\u30F3\u30B7\u30E7\u30F3\uFF08\u5F35\u308A/\u529B\u5F37\u3055\u3001koe\u97F3\u6E90\u9650\u5B9A\uFF09">
15253
+ <span class="dtm-label" data-dtm="lyric-tension-label"></span>
15254
+ <button class="dtm-infobtn" data-dtm="lyric-tension-info" title="\u30C6\u30F3\u30B7\u30E7\u30F3\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
15255
+ </div>
15152
15256
  <div class="dtm-row">
15153
15257
  <span class="dtm-label" style="display:inline-flex;align-items:center;gap:2px">
15154
15258
  <input type="checkbox" data-dtm="lyric-vibrato" aria-label="\u81EA\u52D5\u30D3\u30D6\u30E9\u30FC\u30C8">\u30D3\u30D6\u30E9\u30FC\u30C8
@@ -15252,6 +15356,18 @@ var mountDAW = (target, options = {}) => {
15252
15356
  lyricBreathinessInfo.addEventListener("click", () => {
15253
15357
  showModal("\u30D6\u30EC\u30B7\u30CD\u30B9\u306E\u89E3\u8AAC", BREATHINESS_INFO_HTML);
15254
15358
  });
15359
+ const lyricTension = lyricDiv.querySelector(
15360
+ '[data-dtm="lyric-tension"]'
15361
+ );
15362
+ const lyricTensionLabel = lyricDiv.querySelector(
15363
+ '[data-dtm="lyric-tension-label"]'
15364
+ );
15365
+ const lyricTensionInfo = lyricDiv.querySelector(
15366
+ '[data-dtm="lyric-tension-info"]'
15367
+ );
15368
+ lyricTensionInfo.addEventListener("click", () => {
15369
+ showModal("\u30C6\u30F3\u30B7\u30E7\u30F3\u306E\u89E3\u8AAC", TENSION_INFO_HTML);
15370
+ });
15255
15371
  const lyricVibrato = lyricDiv.querySelector(
15256
15372
  '[data-dtm="lyric-vibrato"]'
15257
15373
  );
@@ -15339,6 +15455,8 @@ var mountDAW = (target, options = {}) => {
15339
15455
  lyricGenderLabel.textContent = `${active.vocalGender}`;
15340
15456
  lyricBreathiness.value = String(active.vocalBreathiness);
15341
15457
  lyricBreathinessLabel.textContent = `${active.vocalBreathiness}`;
15458
+ lyricTension.value = String(active.vocalTension);
15459
+ lyricTensionLabel.textContent = `${active.vocalTension}`;
15342
15460
  lyricVibrato.checked = active.vocalVibrato;
15343
15461
  lyricOctaveUnison.value = active.vocalOctaveUnison;
15344
15462
  const updateLyricCount = () => {
@@ -15509,6 +15627,11 @@ var mountDAW = (target, options = {}) => {
15509
15627
  lyricBreathinessLabel.textContent = `${active.vocalBreathiness}`;
15510
15628
  fireLyricsChange(active);
15511
15629
  });
15630
+ lyricTension.addEventListener("input", () => {
15631
+ active.vocalTension = Number.parseInt(lyricTension.value, 10);
15632
+ lyricTensionLabel.textContent = `${active.vocalTension}`;
15633
+ fireLyricsChange(active);
15634
+ });
15512
15635
  }
15513
15636
  if (active.config.id === "chord" && showChord) {
15514
15637
  const div = document.createElement("div");
@@ -15713,6 +15836,7 @@ var mountDAW = (target, options = {}) => {
15713
15836
  del: t.vocalDelay,
15714
15837
  gen: t.vocalGender,
15715
15838
  bre: t.vocalBreathiness,
15839
+ ten: t.vocalTension,
15716
15840
  uni: t.vocalOctaveUnison
15717
15841
  })).filter(
15718
15842
  (x2) => x2.model.length > 0 && x2.text.length > 0 && x2.notes.length > 0
@@ -15727,6 +15851,7 @@ var mountDAW = (target, options = {}) => {
15727
15851
  x2.del === 0 ? "" : `e${x2.del}`,
15728
15852
  x2.gen === 50 ? "" : `g${x2.gen}`,
15729
15853
  x2.bre === 50 ? "" : `h${x2.bre}`,
15854
+ x2.ten === 50 ? "" : `t${x2.ten}`,
15730
15855
  OCTAVE_UNISON_TOKEN[x2.uni]
15731
15856
  ].filter((s) => s.length > 0).join(" ");
15732
15857
  const head = params ? `${x2.model} ${params}` : x2.model;
@@ -15969,6 +16094,7 @@ var mountDAW = (target, options = {}) => {
15969
16094
  active.vocalDelay = 0;
15970
16095
  active.vocalGender = 50;
15971
16096
  active.vocalBreathiness = 50;
16097
+ active.vocalTension = 50;
15972
16098
  active.vocalOctaveUnison = "none";
15973
16099
  } else {
15974
16100
  for (const t of trackStates) {
@@ -15983,6 +16109,7 @@ var mountDAW = (target, options = {}) => {
15983
16109
  t.vocalDelay = 0;
15984
16110
  t.vocalGender = 50;
15985
16111
  t.vocalBreathiness = 50;
16112
+ t.vocalTension = 50;
15986
16113
  t.vocalOctaveUnison = "none";
15987
16114
  }
15988
16115
  }
@@ -16001,6 +16128,8 @@ var mountDAW = (target, options = {}) => {
16001
16128
  t.vocalDelay = lt.delay ?? 0;
16002
16129
  t.vocalGender = lt.gender ?? 50;
16003
16130
  t.vocalBreathiness = lt.breathiness ?? 50;
16131
+ t.vocalTension = lt.tension ?? 50;
16132
+ t.vocalTension = lt.tension ?? 50;
16004
16133
  t.vocalOctaveUnison = lt.octaveUnison ?? "none";
16005
16134
  });
16006
16135
  for (const p of placements) {
@@ -16085,6 +16214,7 @@ var mountDAW = (target, options = {}) => {
16085
16214
  active.vocalDelay = 0;
16086
16215
  active.vocalGender = 50;
16087
16216
  active.vocalBreathiness = 50;
16217
+ active.vocalTension = 50;
16088
16218
  active.vocalOctaveUnison = "none";
16089
16219
  } else {
16090
16220
  clearAll();
@@ -16101,6 +16231,7 @@ var mountDAW = (target, options = {}) => {
16101
16231
  t.vocalDelay = 0;
16102
16232
  t.vocalGender = 50;
16103
16233
  t.vocalBreathiness = 50;
16234
+ t.vocalTension = 50;
16104
16235
  t.vocalOctaveUnison = "none";
16105
16236
  }
16106
16237
  }
@@ -17465,6 +17596,7 @@ var mountDAW = (target, options = {}) => {
17465
17596
  t.vocalDelay = data.vocalDelay ?? 0;
17466
17597
  t.vocalGender = data.vocalGender ?? 50;
17467
17598
  t.vocalBreathiness = data.vocalBreathiness ?? 50;
17599
+ t.vocalTension = data.vocalTension ?? 50;
17468
17600
  t.vocalOctaveUnison = data.vocalOctaveUnison ?? "none";
17469
17601
  },
17470
17602
  applyTrackInstrument: (trackIndex, instrumentName) => {
@@ -17587,6 +17719,7 @@ var playSingingMML = async (mml, options = {}) => {
17587
17719
  delaySend: (lt.delay ?? 0) / 100,
17588
17720
  gender: (lt.gender ?? 50) / 100,
17589
17721
  breathiness: (lt.breathiness ?? 50) / 100,
17722
+ tension: (lt.tension ?? 50) / 100,
17590
17723
  octaveUnison: lt.octaveUnison,
17591
17724
  notes
17592
17725
  };
@@ -525,6 +525,14 @@
525
525
  return baseHz * 2 ** (cents / 1200);
526
526
  };
527
527
 
528
+ // src/voice-worker-types.ts
529
+ var COMPOSITE_ALIAS_SEP = "\0";
530
+ var unpackCompositeAlias = (alias) => {
531
+ if (!alias.startsWith(COMPOSITE_ALIAS_SEP)) return null;
532
+ const parts = alias.slice(COMPOSITE_ALIAS_SEP.length).split(COMPOSITE_ALIAS_SEP);
533
+ return parts.length === 2 ? [parts[0], parts[1]] : null;
534
+ };
535
+
528
536
  // src/voice-worker.ts
529
537
  var KOE_SAMPLE_RATE = 48e3;
530
538
  var midiToFreq = (m) => 440 * 2 ** ((m - 69) / 12);
@@ -540,8 +548,62 @@
540
548
  }
541
549
  return p;
542
550
  };
543
- var renderAlias = async (alias, pitch, durationMs, vibrato, gender, breathiness) => {
551
+ var COMPOSITE_SPLICE_XFADE_SEC = 5e-3;
552
+ var spliceCompositePcm = (consonantPcm, vowelPcm, sampleRate) => {
553
+ if (consonantPcm.length === 0 || vowelPcm.length === 0) return null;
554
+ const xfade = Math.min(
555
+ Math.floor(sampleRate * COMPOSITE_SPLICE_XFADE_SEC),
556
+ consonantPcm.length,
557
+ vowelPcm.length
558
+ );
559
+ const pcm = new Float64Array(consonantPcm.length + vowelPcm.length - xfade);
560
+ pcm.set(consonantPcm, 0);
561
+ for (let i2 = 0; i2 < xfade; i2++) {
562
+ const t = (i2 + 1) / (xfade + 1);
563
+ const idx = consonantPcm.length - xfade + i2;
564
+ pcm[idx] = consonantPcm[idx] * (1 - t) + vowelPcm[i2] * t;
565
+ }
566
+ pcm.set(vowelPcm.subarray(xfade), consonantPcm.length);
567
+ const consonantMs = (consonantPcm.length - xfade / 2) / sampleRate * 1e3;
568
+ return { pcm, preMs: consonantMs, consonantMs };
569
+ };
570
+ var renderComposite = async (consonantAlias, vowelAlias, pitch, durationMs, vibrato, gender, breathiness, tension) => {
571
+ if (!worldline) return null;
572
+ const [consonantPcm, vowelPcm] = await Promise.all([
573
+ getPcm(consonantAlias),
574
+ getPcm(vowelAlias)
575
+ ]);
576
+ if (!consonantPcm || !vowelPcm) return null;
577
+ const spliced = spliceCompositePcm(consonantPcm, vowelPcm, KOE_SAMPLE_RATE);
578
+ if (!spliced) return null;
579
+ const targetHz = midiToFreq(pitch);
580
+ const audio = worldline.renderNote({
581
+ pcm: spliced.pcm,
582
+ pitch: vibrato ? vibratoPitchCurve(targetHz, spliced.preMs) : targetHz,
583
+ durationMs,
584
+ preMs: spliced.preMs,
585
+ consonantMs: spliced.consonantMs,
586
+ gender,
587
+ breathiness,
588
+ tension
589
+ });
590
+ return audio ? { pcm: audio, preSec: spliced.preMs / 1e3, rate: 1 } : null;
591
+ };
592
+ var renderAlias = async (alias, pitch, durationMs, vibrato, gender, breathiness, tension) => {
544
593
  if (!bank) return null;
594
+ const composite = unpackCompositeAlias(alias);
595
+ if (composite) {
596
+ return renderComposite(
597
+ composite[0],
598
+ composite[1],
599
+ pitch,
600
+ durationMs,
601
+ vibrato,
602
+ gender,
603
+ breathiness,
604
+ tension
605
+ );
606
+ }
545
607
  const pcm = await getPcm(alias);
546
608
  if (!pcm || pcm.length === 0) return null;
547
609
  const entry = bank.manifest.phonemes[alias];
@@ -554,7 +616,8 @@
554
616
  durationMs,
555
617
  ...lead,
556
618
  gender,
557
- breathiness
619
+ breathiness,
620
+ tension
558
621
  });
559
622
  if (audio) return { pcm: audio, preSec: lead.preMs / 1e3, rate: 1 };
560
623
  }
@@ -586,7 +649,16 @@
586
649
  return;
587
650
  }
588
651
  if (msg.type === "render") {
589
- const { id, alias, pitch, durationMs, vibrato, gender, breathiness } = msg;
652
+ const {
653
+ id,
654
+ alias,
655
+ pitch,
656
+ durationMs,
657
+ vibrato,
658
+ gender,
659
+ breathiness,
660
+ tension
661
+ } = msg;
590
662
  try {
591
663
  const out = await renderAlias(
592
664
  alias,
@@ -594,7 +666,8 @@
594
666
  durationMs,
595
667
  vibrato,
596
668
  gender,
597
- breathiness
669
+ breathiness,
670
+ tension
598
671
  );
599
672
  if (out) {
600
673
  wself.postMessage(
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.90",
5
+ "version": "0.1.91",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {