Hezu06 commited on
Commit
1373a35
·
1 Parent(s): e5a55b2

Refactor audio chain for enhanced dynamics and responsiveness to BPM changes

Browse files
Files changed (1) hide show
  1. index.html +131 -284
index.html CHANGED
@@ -871,348 +871,195 @@
871
  btnDisconnect.addEventListener('click', doDisconnect);
872
  btnReconnect.addEventListener('click', doReconnect);
873
 
874
- const masterGain = new Tone.Gain(1.15).toDestination();
875
-
876
- // Low cut để bỏ rumble gây rợn
877
- const lowCut = new Tone.Filter({
878
- type: "highpass",
879
- frequency: 140,
880
- rolloff: -24
881
- }).connect(masterGain);
882
-
883
- // Soft compressor
884
  const compressor = new Tone.Compressor({
885
- threshold: -18,
886
- ratio: 2,
887
- attack: 0.03,
888
- release: 0.25
889
- }).connect(lowCut);
890
-
891
- // EQ dịu hơn
892
- const eq = new Tone.EQ3({
893
- low: -10,
894
- mid: -2,
895
- high: 2
896
- }).connect(compressor);
897
-
898
- // Stereo nhẹ
899
- const stereo = new Tone.StereoWidener(0.35).connect(eq);
900
-
901
- // Reverb rất nhẹ
902
- const reverb = new Tone.Reverb({
903
- decay: 2.2,
904
- wet: 0.18
905
- }).connect(stereo);
906
-
907
- // Delay subtle
908
- const delay = new Tone.FeedbackDelay({
909
- delayTime: "8n",
910
- feedback: 0.12,
911
- wet: 0.08
912
- }).connect(reverb);
913
-
914
- // =========================================================
915
- // PIANO
916
- // =========================================================
917
-
918
  const piano = new Tone.Sampler({
919
- volume: -4,
920
-
921
  urls: {
922
- A1:"A1.mp3",
923
- C2:"C2.mp3",
924
- "D#2":"Ds2.mp3",
925
- "F#2":"Fs2.mp3",
926
-
927
- A2:"A2.mp3",
928
- C3:"C3.mp3",
929
- "D#3":"Ds3.mp3",
930
- "F#3":"Fs3.mp3",
931
-
932
- A3:"A3.mp3",
933
- C4:"C4.mp3",
934
- "D#4":"Ds4.mp3",
935
- "F#4":"Fs4.mp3",
936
-
937
- A4:"A4.mp3",
938
- C5:"C5.mp3"
939
  },
940
-
941
- baseUrl:
942
- "https://raw.githubusercontent.com/Tonejs/audio/master/salamander/",
943
-
944
- release: 0.8
945
-
946
- }).connect(delay);
947
-
948
- // =========================================================
949
- // SOFT PAD
950
- // =========================================================
951
-
952
- const pad = new Tone.PolySynth(
953
- Tone.Synth,
954
- {
955
- volume: -20,
956
-
957
- oscillator: {
958
- type: "sine"
959
- },
960
-
961
- envelope: {
962
- attack: 2.5,
963
- decay: 0.3,
964
- sustain: 0.7,
965
- release: 4
966
- }
967
- }
968
- ).connect(reverb);
969
-
970
- // =========================================================
971
- // VERY SOFT NOISE
972
- // =========================================================
973
-
974
- const noise = new Tone.Noise("pink");
975
-
976
- const noiseFilter = new Tone.Filter({
977
- type: "lowpass",
978
- frequency: 4000
979
- });
980
-
981
- const noiseGain = new Tone.Gain(0.012);
982
-
983
- noise
984
- .connect(noiseFilter)
985
- .connect(noiseGain)
986
- .connect(stereo);
987
-
988
- noise.start();
989
-
990
- // =========================================================
991
- // BPM TEXTURE
992
- // =========================================================
993
-
994
  function applyBpmTexture(bpm) {
995
-
996
- // BPM cao -> calm hơn
997
- const t = Math.min(1, Math.max(0, (bpm - 50) / 100));
998
-
999
- // Reverb
1000
- reverb.wet.rampTo(
1001
- 0.14 + t * 0.08,
1002
- 3
1003
- );
1004
-
1005
- // Delay
1006
- delay.feedback.rampTo(
1007
- 0.08 + t * 0.08,
1008
- 3
1009
- );
1010
-
1011
- delay.wet.rampTo(
1012
- 0.05 + t * 0.05,
1013
- 3
1014
- );
1015
-
1016
- // Pad volume
1017
- pad.volume.rampTo(
1018
- -22 + t * 6,
1019
- 3
1020
- );
1021
-
1022
- // Piano volume
1023
- piano.volume.rampTo(
1024
- -5 + t * 2,
1025
- 2
1026
- );
1027
-
1028
- // Noise
1029
- noiseGain.gain.rampTo(
1030
- 0.008 + t * 0.008,
1031
- 3
1032
- );
1033
-
1034
- // Tempo inverse entrainment
1035
- const musicTempo =
1036
- 78 - t * 18;
1037
-
1038
- Tone.getTransport().bpm.rampTo(
1039
- musicTempo,
1040
- 4
1041
- );
1042
-
1043
- // UI color
1044
- const color =
1045
- bpm < 70
1046
- ? '#4ecca3'
1047
- : bpm < 95
1048
- ? '#8ecae6'
1049
- : '#f9c74f';
1050
-
1051
- bpmDisplay.style.color = color;
1052
- bpmDisplay.style.textShadow =
1053
- `0 0 18px ${color}`;
1054
  }
1055
 
1056
- // =========================================================
1057
- // PLAY AI MUSIC
1058
- // =========================================================
 
 
 
 
 
 
1059
 
 
 
 
1060
  btnGenerate.addEventListener('click', async () => {
1061
-
1062
  if (currentBpm === 0) {
1063
- alert('Chưa có BPM!');
1064
  return;
1065
  }
1066
 
1067
  await Tone.start();
1068
  await Tone.loaded();
1069
-
1070
- masterGain.gain.value = 1.15;
1071
-
1072
  if (ws) ws.close();
1073
 
1074
- setStatus(
1075
- `Đang kết nối AI với ${currentBpm} BPM`,
1076
- 'info'
1077
- );
1078
-
1079
- btnGenerate.disabled = true;
1080
- btnStop.disabled = false;
1081
-
1082
- bpmDisplay.style.animation =
1083
- 'pulse 1.2s infinite';
1084
 
 
1085
  applyBpmTexture(currentBpm);
1086
 
1087
- const protocol =
1088
- window.location.protocol === 'https:'
1089
- ? 'wss:'
1090
- : 'ws:';
1091
-
1092
- ws = new WebSocket(
1093
- `${protocol}//${window.location.host}/ws/vibe`
1094
- );
1095
 
1096
  ws.onopen = () => {
1097
-
1098
- setStatus(
1099
- '🎵 Đang phát nhạc relaxing…',
1100
- 'ok'
1101
- );
1102
-
1103
- ws.send(JSON.stringify({
1104
- bpm: currentBpm
1105
- }));
1106
  };
1107
 
1108
- let startTime = Tone.now() + 0.12;
1109
 
1110
  ws.onmessage = (event) => {
1111
-
1112
  const data = JSON.parse(event.data);
1113
 
1114
- data.notes.forEach(n => {
1115
-
1116
- const noteStr =
1117
- Tone.Frequency(
1118
- n.note,
1119
- "midi"
1120
- ).toNote();
1121
 
 
 
1122
  const t = startTime + n.time;
1123
 
1124
- // =====================================================
1125
- // MELODY
1126
- // =====================================================
1127
-
1128
  if (n.type === "melody") {
1129
-
1130
- // softer velocity
1131
- const vel =
1132
- Math.min(
1133
- 0.72,
1134
- (n.velocity / 127) * 0.72
1135
- );
1136
-
1137
- piano.triggerAttackRelease(
1138
- noteStr,
1139
- n.duration * 0.95,
1140
- t,
1141
- vel
1142
- );
1143
  }
1144
-
1145
- // =====================================================
1146
- // CHORDS
1147
- // =====================================================
1148
-
1149
  else if (n.type === "chord") {
1150
-
1151
- // giảm note thấp gây rợn
1152
- if (n.note < 58) return;
1153
-
1154
- pad.triggerAttackRelease(
1155
- noteStr,
1156
- n.duration * 0.7,
1157
- t,
1158
- 0.22
1159
- );
1160
  }
 
1161
  });
1162
 
1163
  startTime += data.batch_duration;
1164
  };
1165
 
1166
- ws.onerror = (err) => {
1167
-
1168
- console.error(err);
 
 
 
 
 
 
 
 
1169
 
1170
- setStatus(
1171
- 'Lỗi kết nối AI!',
1172
- 'err'
1173
- );
1174
  };
1175
 
1176
  ws.onclose = () => {
1177
-
1178
- btnGenerate.disabled = false;
1179
- btnStop.disabled = true;
1180
-
1181
  bpmDisplay.style.animation = 'none';
1182
  };
1183
  });
1184
 
1185
- // =========================================================
1186
- // STOP
1187
- // =========================================================
1188
-
1189
  btnStop.addEventListener('click', () => {
 
1190
 
1191
- if (ws) {
1192
- ws.close();
1193
- ws = null;
1194
- }
1195
-
1196
- masterGain.gain.rampTo(
1197
- 0,
1198
- 0.4
1199
- );
1200
 
1201
  setTimeout(() => {
1202
-
1203
  piano.releaseAll();
1204
  pad.releaseAll();
 
1205
 
1206
- }, 450);
1207
-
1208
- setStatus(
1209
- 'Đã dừng nhạc.',
1210
- 'info'
1211
- );
1212
 
1213
  btnGenerate.disabled = false;
1214
  btnStop.disabled = true;
1215
-
1216
  bpmDisplay.style.animation = 'none';
1217
  });
1218
  </script>
 
871
  btnDisconnect.addEventListener('click', doDisconnect);
872
  btnReconnect.addEventListener('click', doReconnect);
873
 
874
+ // ================================================================
875
+ // AUDIO CHAIN — volume boosted, gain staging cẩn thận
876
+ // ================================================================
877
+ const masterGain = new Tone.Gain(1.5).toDestination(); // +3.5dB tổng thể
878
+ const limiter = new Tone.Limiter(-0.3).connect(masterGain); // limiter thoáng hơn
 
 
 
 
 
879
  const compressor = new Tone.Compressor({
880
+ threshold: -14, ratio: 3, attack: 0.008, release: 0.18
881
+ }).connect(limiter);
882
+ const eq = new Tone.EQ3({ low: -8, mid: 1, high: 4 }).connect(compressor);
883
+ const stereo = new Tone.StereoWidener(0.65).connect(eq);
884
+ const reverb = new Tone.Reverb({ decay: 3.5, wet: 0.38 }).connect(stereo); // wet sẽ thay đổi theo BPM
885
+ const delay = new Tone.FeedbackDelay({ delayTime: "8n", feedback: 0.28, wet: 0.18 }).connect(reverb);
886
+
887
+ // 🎹 PIANO volume tăng lên, kết nối thẳng vào delay
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
888
  const piano = new Tone.Sampler({
889
+ volume: 2, // +4dB so với trước (trước là 0)
 
890
  urls: {
891
+ A1:"A1.mp3", C2:"C2.mp3", "D#2":"Ds2.mp3", "F#2":"Fs2.mp3",
892
+ A2:"A2.mp3", C3:"C3.mp3", "D#3":"Ds3.mp3", "F#3":"Fs3.mp3",
893
+ A3:"A3.mp3", C4:"C4.mp3", "D#4":"Ds4.mp3", "F#4":"Fs4.mp3",
894
+ A4:"A4.mp3", C5:"C5.mp3"
 
 
 
 
 
 
 
 
 
 
 
 
 
895
  },
896
+ baseUrl: "https://raw.githubusercontent.com/Tonejs/audio/master/salamander/",
897
+ release: 1
898
+ }).connect(delay)
899
+ const pad = new Tone.PolySynth(Tone.Synth, {
900
+ volume: -12,
901
+ oscillator: { type: "triangle" },
902
+ envelope: { attack: 1.2, decay: 0.5, sustain: 0.8, release: 3 }
903
+ }).connect(reverb);
904
+ log("🎹 Đã khởi tạo audio chain. Volume boosted.");
905
+ setStatus("Sẵn sàng sinh nhạc AI", "ok");
906
+ // ================================================================
907
+ // BPM-REACTIVE: thay đổi texture theo nhịp tim
908
+ // ================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909
  function applyBpmTexture(bpm) {
910
+ // ── ENTRAINMENT LOGIC ──────────────────────────────────────────
911
+ // Nhịp tim cao = thể đang stress → nhạc kéo ngược lại: chậm,
912
+ // màng, ambient hơn để hạ nhiệt.
913
+ // Nhịp tim thấp = nghỉ ngơi → nhạc có thể nhẹ nhàng groove hơn.
914
+
915
+ // Reverb: BPM CAO = nhiều reverb hơn (mơ màng, bay bổng, dịu)
916
+ // BPM thấp = ít reverb (rõ ràng, có chiều sâu nhẹ)
917
+ const reverbWet = bpm < 70 ? 0.2 :
918
+ bpm < 90 ? 0.3 :
919
+ bpm < 110 ? 0.4 : 0.5;
920
+ reverb.wet.rampTo(reverbWet, 2.5);
921
+
922
+ // Delay feedback: BPM cao = echo dài hơn (hypnotic, calming)
923
+ const delayFb = bpm < 80 ? 0.16 : bpm < 110 ? 0.24 : 0.34;
924
+ delay.feedback.rampTo(delayFb, 2.5);
925
+ // Nhạc tempo: BPM cao → tempo chậm hơn để kéo nhịp tim xuống
926
+ // map BPM [40,180] → music tempo [85, 55] BPM (inverse linear)
927
+ const padVol = bpm < 75 ? -12 : bpm < 100 ? -8 : -4;
928
+ pad.volume.rampTo(padVol, 2.5);
929
+
930
+ // Pad attack: BPM cao = attack chậm (drifting), thấp = attack vừa
931
+ const padAttack = bpm < 75 ? 0.6 : bpm < 100 ? 1.2 : 2.2;
932
+ pad.set({ envelope: { attack: padAttack } });
933
+
934
+ const musicTempo = Math.round(85 - (bpm - 40) * (30 / 140));
935
+ Tone.getTransport().bpm.rampTo(Math.max(52, Math.min(88, musicTempo)), 3);
936
+
937
+ // BPM display color theo mức độ stress
938
+ // (màu vẫn phản ánh nhịp tim thực, không phải tempo nhạc)
939
+ const color = bpm < 70 ? '#4ecca3' : // xanh — bình tĩnh
940
+ bpm < 90 ? '#a0d8ef' : // xanh dương — nhẹ nhàng
941
+ bpm < 110 ? '#f9c74f' : // vàng — đang tăng
942
+ '#e94560'; // đỏ — cần hạ nhiệt
943
+ bpmDisplay.style.color = color;
944
+ bpmDisplay.style.textShadow = `0 0 20px ${color}`;
945
+
946
+ log(`🎚 Texture (entrainment) → reverb:${reverbWet} delayFb:${delayFb} padVol:${padVol} musicTempo:${musicTempo}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
947
  }
948
 
949
+ // Override applyBpm để gọi texture update
950
+ const _origApplyBpm = applyBpm;
951
+ // Không thể override function declaration, thêm hook vào ws.send thay thế
952
+ function sendBpmToServer(bpm) {
953
+ if (ws && ws.readyState === WebSocket.OPEN) {
954
+ ws.send(JSON.stringify({ bpm }));
955
+ }
956
+ applyBpmTexture(bpm);
957
+ }
958
 
959
+ // ================================================================
960
+ // WEBSOCKET — sinh nhạc
961
+ // ================================================================
962
  btnGenerate.addEventListener('click', async () => {
 
963
  if (currentBpm === 0) {
964
+ alert('Chưa có BPM! Kết nối đồng hồ hoặc nhập thủ công.');
965
  return;
966
  }
967
 
968
  await Tone.start();
969
  await Tone.loaded();
970
+ log("✅ Piano samples loaded");
971
+ masterGain.gain.rampTo(1.5, 0.1);
 
972
  if (ws) ws.close();
973
 
974
+ setStatus(`Đang kết nối AI với ${currentBpm} BPM…`, 'info');
975
+ btnGenerate.textContent = 'AI đang truyền nốt';
976
+ btnGenerate.disabled = true;
977
+ btnStop.disabled = false;
978
+ bpmDisplay.style.animation = 'pulse 1s infinite';
 
 
 
 
 
979
 
980
+ // Khởi động texture + drum engine ngay
981
  applyBpmTexture(currentBpm);
982
 
983
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
984
+ ws = new WebSocket(`${protocol}//${window.location.host}/ws/vibe`);
 
 
 
 
 
 
985
 
986
  ws.onopen = () => {
987
+ setStatus('🎵 Đang phát nhạc Real-time…', 'ok');
988
+ ws.send(JSON.stringify({ bpm: currentBpm }));
 
 
 
 
 
 
 
989
  };
990
 
991
+ let startTime = Tone.now() + 0.15;
992
 
993
  ws.onmessage = (event) => {
 
994
  const data = JSON.parse(event.data);
995
 
996
+ // Nếu BPM thay đổi (server gửi về), cập nhật texture
997
+ if (data.bpm && data.bpm !== currentBpm) {
998
+ applyBpmTexture(data.bpm);
999
+ }
 
 
 
1000
 
1001
+ data.notes.forEach(n => {
1002
+ const noteStr = Tone.Frequency(n.note, "midi").toNote();
1003
  const t = startTime + n.time;
1004
 
 
 
 
 
1005
  if (n.type === "melody") {
1006
+ // Velocity melody cũng phản ánh BPM: cao → piano mạnh hơn
1007
+ const velBoost = currentBpm > 100 ? 1.15 : 1.0;
1008
+ const vel = Math.min(1, (n.velocity / 127) * velBoost);
1009
+ piano.triggerAttackRelease(noteStr, n.duration, t, vel);
 
 
 
 
 
 
 
 
 
 
1010
  }
 
 
 
 
 
1011
  else if (n.type === "chord") {
1012
+ pad.triggerAttackRelease(noteStr, n.duration, t, n.velocity / 127);
 
 
 
 
 
 
 
 
 
1013
  }
1014
+ // drum events từ server bị bỏ qua — drum engine client đã xử lý
1015
  });
1016
 
1017
  startTime += data.batch_duration;
1018
  };
1019
 
1020
+ // Mỗi khi BPM thay đổi từ đồng hồ/manual → cập nhật texture + drum
1021
+ const origApply = applyBpm;
1022
+ // Patch ws.send để bắt BPM change
1023
+ const _origSend = ws.send.bind(ws);
1024
+ ws.send = function(data) {
1025
+ try {
1026
+ const parsed = JSON.parse(data);
1027
+ if (parsed.bpm) applyBpmTexture(parsed.bpm);
1028
+ } catch {}
1029
+ return _origSend(data);
1030
+ };
1031
 
1032
+ ws.onerror = (err) => {
1033
+ log('WebSocket Error: ' + err);
1034
+ setStatus('Lỗi kết nối tới AI!', 'err');
 
1035
  };
1036
 
1037
  ws.onclose = () => {
1038
+ btnGenerate.textContent = '2. Sinh nhạc AI';
1039
+ btnGenerate.disabled = false;
1040
+ btnStop.disabled = true;
 
1041
  bpmDisplay.style.animation = 'none';
1042
  };
1043
  });
1044
 
1045
+ // ================================================================
1046
+ // STOP — fade masterGain ngay lập tức, không chờ note scheduled
1047
+ // ================================================================
 
1048
  btnStop.addEventListener('click', () => {
1049
+ if (ws) { ws.close(); ws = null; }
1050
 
1051
+ // Fade out nhanh (0.25s) để tắt ngay cả note đã schedule trước
1052
+ masterGain.gain.rampTo(0, 0.1);
 
 
 
 
 
 
 
1053
 
1054
  setTimeout(() => {
 
1055
  piano.releaseAll();
1056
  pad.releaseAll();
1057
+ }, 200);
1058
 
1059
+ setStatus('Đã dừng nhạc.', 'info');
 
 
 
 
 
1060
 
1061
  btnGenerate.disabled = false;
1062
  btnStop.disabled = true;
 
1063
  bpmDisplay.style.animation = 'none';
1064
  });
1065
  </script>