@onjmin/dtm 1.0.1 → 1.0.2

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
@@ -9916,6 +9916,36 @@ var SoundFont = class _SoundFont {
9916
9916
  static afterTime = 0.5;
9917
9917
  static fonts = /* @__PURE__ */ new Map();
9918
9918
  static ch = -1;
9919
+ // ── ヒューマナイズ(疑似ラウンドロビン)──
9920
+ // サンプルを複数持たない代わりに、発音のたびピッチと音量をごく僅かにランダムへ
9921
+ // ブレさせて「毎回寸分違わず同じ波形」という機械的な均一さを崩す。
9922
+ /** 発音ごとのピッチ微揺らぎ幅(セント、±この値の範囲で一様乱数)。 */
9923
+ static humanizeDetuneCents = 4;
9924
+ /** 発音ごとの音量微揺らぎ幅(比率、0.06 = ±6%の範囲で一様乱数)。 */
9925
+ static humanizeGainRatio = 0.06;
9926
+ // ── ベロシティ→明るさ連動フィルタ ──
9927
+ // volume は音量にしか効かず音色(明るさ)が変わらないのを補うため、
9928
+ // ローパスのカットオフを volume に連動させる(弱いほど丸く、強いほど明るく)。
9929
+ /** volume=0 のときのローパスカットオフ(Hz)。 */
9930
+ static brightnessMinHz = 1200;
9931
+ /** volume=1 のときのローパスカットオフ(Hz)。サンプルの帯域を実質塞がない値。 */
9932
+ static brightnessMaxHz = 18e3;
9933
+ /** ベロシティ→明るさフィルタのQ値。共鳴が付かないButterworth特性。 */
9934
+ static brightnessQ = Math.SQRT1_2;
9935
+ // ── ロングトーン用ビブラート(ピッチLFO) ──
9936
+ // 伸ばす音が完全に静止していると不自然に聞こえるため、一定長以上持続する
9937
+ // 音符にだけ、発音直後フェードインしつつ穏やかなピッチ揺れを掛ける。
9938
+ // 閾値・速さは歌声合成側の vibrato.ts (VIBRATO_MIN_SEC/VIBRATO_RATE_HZ) と
9939
+ // 同じ考え方だが、MIDI楽器はここでは独立した定数として持つ
9940
+ // (koeのCurveInput契約に縛られない汎用のWeb Audio LFOのため)。
9941
+ /** ロングトーン判定の最短秒数。これ未満の音符にはビブラートを掛けない。 */
9942
+ static longToneThresholdSec = 0.35;
9943
+ /** ロングトーンビブラートの速さ(Hz)。 */
9944
+ static vibratoRateHz = 5.5;
9945
+ /** ロングトーンビブラートの深さ(セント)。歌声用(±35セント)より控えめ。 */
9946
+ static vibratoDepthCents = 12;
9947
+ /** ロングトーンビブラートのフェードイン秒数。発音直後は素直な音程を保つ。 */
9948
+ static vibratoFadeSec = 0.2;
9919
9949
  static toURL(fontName) {
9920
9950
  return `https://surikov.github.io/webaudiofontdata/sound/${fontName}.js`;
9921
9951
  }
@@ -9966,6 +9996,7 @@ var SoundFont = class _SoundFont {
9966
9996
  const zone = zones.get(pitch);
9967
9997
  if (!zone) return;
9968
9998
  const src = ctx.createBufferSource();
9999
+ const filter = ctx.createBiquadFilter();
9969
10000
  const g = ctx.createGain();
9970
10001
  const _when = when + ctx.currentTime;
9971
10002
  const { buffer, _param } = zone;
@@ -9973,23 +10004,53 @@ var SoundFont = class _SoundFont {
9973
10004
  src.buffer = buffer;
9974
10005
  src.playbackRate.setValueAtTime(_param.playbackRate, 0);
9975
10006
  Object.assign(src, _param.src);
10007
+ const humanizeCents = (Math.random() * 2 - 1) * _SoundFont.humanizeDetuneCents;
10008
+ const humanizeGainMul = 1 + (Math.random() * 2 - 1) * _SoundFont.humanizeGainRatio;
10009
+ src.detune.setValueAtTime(humanizeCents, 0);
10010
+ const effectiveVolume = volume * humanizeGainMul;
10011
+ filter.type = "lowpass";
10012
+ const brightness = Math.max(0, Math.min(1, volume));
10013
+ filter.frequency.setValueAtTime(
10014
+ _SoundFont.brightnessMinHz + (_SoundFont.brightnessMaxHz - _SoundFont.brightnessMinHz) * brightness,
10015
+ 0
10016
+ );
10017
+ filter.Q.setValueAtTime(_SoundFont.brightnessQ, 0);
9976
10018
  g.gain.setValueAtTime(0, ctx.currentTime);
9977
10019
  const attackTime = 5e-3;
9978
10020
  const startGainTime = Math.max(ctx.currentTime, _when);
9979
10021
  g.gain.setValueAtTime(0, startGainTime);
9980
- g.gain.linearRampToValueAtTime(volume, startGainTime + attackTime);
10022
+ g.gain.linearRampToValueAtTime(effectiveVolume, startGainTime + attackTime);
9981
10023
  const _duration = duration + _SoundFont.afterTime;
9982
10024
  const end = _when + (isDrum ? buffer.duration : src.loop ? _duration : Math.min(_duration, _param.max));
9983
10025
  if (!isDrum) {
9984
- g.gain.setValueAtTime(volume, startGainTime + attackTime);
10026
+ g.gain.setValueAtTime(effectiveVolume, startGainTime + attackTime);
9985
10027
  g.gain.linearRampToValueAtTime(0, end);
9986
10028
  }
9987
- src.connect(g).connect(destination);
10029
+ src.connect(filter).connect(g).connect(destination);
10030
+ let vibratoOsc;
10031
+ let vibratoDepth;
10032
+ if (!isDrum && duration >= _SoundFont.longToneThresholdSec) {
10033
+ vibratoOsc = ctx.createOscillator();
10034
+ vibratoOsc.type = "sine";
10035
+ vibratoOsc.frequency.setValueAtTime(_SoundFont.vibratoRateHz, 0);
10036
+ vibratoDepth = ctx.createGain();
10037
+ vibratoDepth.gain.setValueAtTime(0, startGainTime);
10038
+ vibratoDepth.gain.linearRampToValueAtTime(
10039
+ _SoundFont.vibratoDepthCents,
10040
+ startGainTime + _SoundFont.vibratoFadeSec
10041
+ );
10042
+ vibratoOsc.connect(vibratoDepth).connect(src.detune);
10043
+ vibratoOsc.start(_when);
10044
+ vibratoOsc.stop(end);
10045
+ }
9988
10046
  src.start(_when);
9989
10047
  src.stop(end);
9990
10048
  src.onended = () => {
9991
10049
  src.disconnect();
10050
+ filter.disconnect();
9992
10051
  g.disconnect();
10052
+ vibratoOsc?.disconnect();
10053
+ vibratoDepth?.disconnect();
9993
10054
  };
9994
10055
  }
9995
10056
  };
package/dist/index.mjs CHANGED
@@ -9783,6 +9783,36 @@ var SoundFont = class _SoundFont {
9783
9783
  static afterTime = 0.5;
9784
9784
  static fonts = /* @__PURE__ */ new Map();
9785
9785
  static ch = -1;
9786
+ // ── ヒューマナイズ(疑似ラウンドロビン)──
9787
+ // サンプルを複数持たない代わりに、発音のたびピッチと音量をごく僅かにランダムへ
9788
+ // ブレさせて「毎回寸分違わず同じ波形」という機械的な均一さを崩す。
9789
+ /** 発音ごとのピッチ微揺らぎ幅(セント、±この値の範囲で一様乱数)。 */
9790
+ static humanizeDetuneCents = 4;
9791
+ /** 発音ごとの音量微揺らぎ幅(比率、0.06 = ±6%の範囲で一様乱数)。 */
9792
+ static humanizeGainRatio = 0.06;
9793
+ // ── ベロシティ→明るさ連動フィルタ ──
9794
+ // volume は音量にしか効かず音色(明るさ)が変わらないのを補うため、
9795
+ // ローパスのカットオフを volume に連動させる(弱いほど丸く、強いほど明るく)。
9796
+ /** volume=0 のときのローパスカットオフ(Hz)。 */
9797
+ static brightnessMinHz = 1200;
9798
+ /** volume=1 のときのローパスカットオフ(Hz)。サンプルの帯域を実質塞がない値。 */
9799
+ static brightnessMaxHz = 18e3;
9800
+ /** ベロシティ→明るさフィルタのQ値。共鳴が付かないButterworth特性。 */
9801
+ static brightnessQ = Math.SQRT1_2;
9802
+ // ── ロングトーン用ビブラート(ピッチLFO) ──
9803
+ // 伸ばす音が完全に静止していると不自然に聞こえるため、一定長以上持続する
9804
+ // 音符にだけ、発音直後フェードインしつつ穏やかなピッチ揺れを掛ける。
9805
+ // 閾値・速さは歌声合成側の vibrato.ts (VIBRATO_MIN_SEC/VIBRATO_RATE_HZ) と
9806
+ // 同じ考え方だが、MIDI楽器はここでは独立した定数として持つ
9807
+ // (koeのCurveInput契約に縛られない汎用のWeb Audio LFOのため)。
9808
+ /** ロングトーン判定の最短秒数。これ未満の音符にはビブラートを掛けない。 */
9809
+ static longToneThresholdSec = 0.35;
9810
+ /** ロングトーンビブラートの速さ(Hz)。 */
9811
+ static vibratoRateHz = 5.5;
9812
+ /** ロングトーンビブラートの深さ(セント)。歌声用(±35セント)より控えめ。 */
9813
+ static vibratoDepthCents = 12;
9814
+ /** ロングトーンビブラートのフェードイン秒数。発音直後は素直な音程を保つ。 */
9815
+ static vibratoFadeSec = 0.2;
9786
9816
  static toURL(fontName) {
9787
9817
  return `https://surikov.github.io/webaudiofontdata/sound/${fontName}.js`;
9788
9818
  }
@@ -9833,6 +9863,7 @@ var SoundFont = class _SoundFont {
9833
9863
  const zone = zones.get(pitch);
9834
9864
  if (!zone) return;
9835
9865
  const src = ctx.createBufferSource();
9866
+ const filter = ctx.createBiquadFilter();
9836
9867
  const g = ctx.createGain();
9837
9868
  const _when = when + ctx.currentTime;
9838
9869
  const { buffer, _param } = zone;
@@ -9840,23 +9871,53 @@ var SoundFont = class _SoundFont {
9840
9871
  src.buffer = buffer;
9841
9872
  src.playbackRate.setValueAtTime(_param.playbackRate, 0);
9842
9873
  Object.assign(src, _param.src);
9874
+ const humanizeCents = (Math.random() * 2 - 1) * _SoundFont.humanizeDetuneCents;
9875
+ const humanizeGainMul = 1 + (Math.random() * 2 - 1) * _SoundFont.humanizeGainRatio;
9876
+ src.detune.setValueAtTime(humanizeCents, 0);
9877
+ const effectiveVolume = volume * humanizeGainMul;
9878
+ filter.type = "lowpass";
9879
+ const brightness = Math.max(0, Math.min(1, volume));
9880
+ filter.frequency.setValueAtTime(
9881
+ _SoundFont.brightnessMinHz + (_SoundFont.brightnessMaxHz - _SoundFont.brightnessMinHz) * brightness,
9882
+ 0
9883
+ );
9884
+ filter.Q.setValueAtTime(_SoundFont.brightnessQ, 0);
9843
9885
  g.gain.setValueAtTime(0, ctx.currentTime);
9844
9886
  const attackTime = 5e-3;
9845
9887
  const startGainTime = Math.max(ctx.currentTime, _when);
9846
9888
  g.gain.setValueAtTime(0, startGainTime);
9847
- g.gain.linearRampToValueAtTime(volume, startGainTime + attackTime);
9889
+ g.gain.linearRampToValueAtTime(effectiveVolume, startGainTime + attackTime);
9848
9890
  const _duration = duration + _SoundFont.afterTime;
9849
9891
  const end = _when + (isDrum ? buffer.duration : src.loop ? _duration : Math.min(_duration, _param.max));
9850
9892
  if (!isDrum) {
9851
- g.gain.setValueAtTime(volume, startGainTime + attackTime);
9893
+ g.gain.setValueAtTime(effectiveVolume, startGainTime + attackTime);
9852
9894
  g.gain.linearRampToValueAtTime(0, end);
9853
9895
  }
9854
- src.connect(g).connect(destination);
9896
+ src.connect(filter).connect(g).connect(destination);
9897
+ let vibratoOsc;
9898
+ let vibratoDepth;
9899
+ if (!isDrum && duration >= _SoundFont.longToneThresholdSec) {
9900
+ vibratoOsc = ctx.createOscillator();
9901
+ vibratoOsc.type = "sine";
9902
+ vibratoOsc.frequency.setValueAtTime(_SoundFont.vibratoRateHz, 0);
9903
+ vibratoDepth = ctx.createGain();
9904
+ vibratoDepth.gain.setValueAtTime(0, startGainTime);
9905
+ vibratoDepth.gain.linearRampToValueAtTime(
9906
+ _SoundFont.vibratoDepthCents,
9907
+ startGainTime + _SoundFont.vibratoFadeSec
9908
+ );
9909
+ vibratoOsc.connect(vibratoDepth).connect(src.detune);
9910
+ vibratoOsc.start(_when);
9911
+ vibratoOsc.stop(end);
9912
+ }
9855
9913
  src.start(_when);
9856
9914
  src.stop(end);
9857
9915
  src.onended = () => {
9858
9916
  src.disconnect();
9917
+ filter.disconnect();
9859
9918
  g.disconnect();
9919
+ vibratoOsc?.disconnect();
9920
+ vibratoDepth?.disconnect();
9860
9921
  };
9861
9922
  }
9862
9923
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "onjmin",
3
3
  "license": "MIT",
4
4
  "name": "@onjmin/dtm",
5
- "version": "1.0.1",
5
+ "version": "1.0.2",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {