Hezu06 commited on
Commit
76b32b8
·
1 Parent(s): 2bee9cb

Refactor audio chain and drum engine for improved performance and responsiveness

Browse files
Files changed (2) hide show
  1. index.html +231 -128
  2. main.py +1 -53
index.html CHANGED
@@ -871,180 +871,283 @@
871
  btnDisconnect.addEventListener('click', doDisconnect);
872
  btnReconnect.addEventListener('click', doReconnect);
873
 
874
- // ===============================
875
- // GENERATE MUSIC
876
-
877
- const limiter = new Tone.Limiter(-1);
878
-
879
- // 2. Compressor (giữ âm ổn định)
880
  const compressor = new Tone.Compressor({
881
- threshold: -18,
882
- ratio: 3,
883
- attack: 0.01,
884
- release: 0.2
885
- });
886
-
887
- // 3. EQ (làm ấm piano)
888
- const eq = new Tone.EQ3({
889
- low: 3, // tăng bass nhẹ
890
- mid: -1, // giảm mid đục
891
- high: 2 // tăng sáng nhẹ
892
- });
893
-
894
- // 4. Stereo widen (cho không gian rộng)
895
- const stereo = new Tone.StereoWidener(0.6);
896
-
897
- // 5. Reverb (không gian)
898
- const reverb = new Tone.Reverb({
899
- decay: 4,
900
- wet: 0.3
901
- });
902
-
903
- // 6. Delay (lofi feel)
904
- const delay = new Tone.FeedbackDelay({
905
- delayTime: "8n",
906
- feedback: 0.25,
907
- wet: 0.15
908
- });
909
-
910
- // Connect chain
911
- delay.connect(reverb);
912
- reverb.connect(stereo);
913
- stereo.connect(eq);
914
- eq.connect(compressor);
915
- compressor.connect(limiter);
916
- limiter.toDestination();
917
-
918
  const piano = new Tone.Sampler({
 
919
  urls: {
920
- A1: "A1.mp3",
921
- C2: "C2.mp3",
922
- "D#2": "Ds2.mp3",
923
- "F#2": "Fs2.mp3",
924
- A2: "A2.mp3",
925
- C3: "C3.mp3",
926
- "D#3": "Ds3.mp3",
927
- "F#3": "Fs3.mp3",
928
- A3: "A3.mp3",
929
- C4: "C4.mp3",
930
- "D#4": "Ds4.mp3",
931
- "F#4": "Fs4.mp3",
932
- A4: "A4.mp3",
933
- C5: "C5.mp3"
934
  },
935
- baseUrl: "https://raw.githubusercontent.com/Tonejs/audio/master/salamander/",
936
  release: 1
937
  }).connect(delay);
938
 
939
- // 🌊 PAD (chord)
940
  const pad = new Tone.PolySynth(Tone.Synth, {
941
- volume: -12,
942
  oscillator: { type: "sine" },
943
- envelope: {
944
- attack: 1,
945
- decay: 0.5,
946
- sustain: 0.8,
947
- release: 3
948
- }
949
  }).connect(reverb);
950
 
951
- // 🥁 KICK
952
  const kick = new Tone.MembraneSynth({
953
- volume: -7,
954
- pitchDecay: 0.05,
955
- octaves: 6,
956
- envelope: {
957
- attack: 0.001,
958
- decay: 0.3,
959
- sustain: 0
960
- }
961
  }).connect(compressor);
962
 
963
  // 🥁 SNARE
964
  const snare = new Tone.NoiseSynth({
965
- volume: -11,
966
  noise: { type: "white" },
967
- envelope: {
968
- attack: 0.001,
969
- decay: 0.15,
970
- sustain: 0
971
- }
972
  }).connect(compressor);
973
 
974
  // 🥁 HIHAT
975
  const hihat = new Tone.MetalSynth({
976
- volume: -20,
977
  frequency: 400,
978
- envelope: { attack: 0.001, decay: 0.06, release: 0.01 },
979
- harmonicity: 5.1,
980
- modulationIndex: 32,
981
- resonance: 4000,
982
- octaves: 1.5
983
  }).connect(compressor);
984
 
985
- // Gọi Tone.start() sẵn khi load trang nếu được
986
- log("🎹 Đã khởi tạo Lo-fi Synthesizer (Có Limiter chống rè).");
987
  setStatus("Sẵn sàng sinh nhạc AI", "ok");
988
 
989
- // ===============================
990
- // GENERATE MUSIC (CẬP NHẬT WEBSOCKET)
991
- // ===============================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
992
  btnGenerate.addEventListener('click', async () => {
993
  if (currentBpm === 0) {
994
  alert('Chưa có BPM! Kết nối đồng hồ hoặc nhập thủ công.');
995
  return;
996
  }
997
 
998
- // Bắt buộc trình duyệt cho phép phát âm thanh
999
  await Tone.start();
1000
  await Tone.loaded();
1001
- console.log("✅ Piano samples loaded");
1002
 
1003
- if (ws) ws.close(); // Đóng kết nối cũ nếu có
1004
 
1005
- setStatus(`Đang kết nối não AI với ${currentBpm} BPM…`, 'info');
1006
  btnGenerate.textContent = 'AI đang truyền nốt…';
1007
- btnGenerate.disabled = true;
1008
- btnStop.disabled = false;
1009
  bpmDisplay.style.animation = 'pulse 1s infinite';
1010
 
1011
- // Mở kết nối WebSocket tới Server
1012
- // ĐOẠN CODE MỚI ĐÃ FIX LỖI:
 
 
1013
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1014
- const wsUrl = `${protocol}//${window.location.host}/ws/vibe`;
1015
- ws = new WebSocket(wsUrl);
1016
 
1017
  ws.onopen = () => {
1018
- setStatus('🎵 Đang phát nhạc Real-time vô tận…', 'ok');
1019
- // Gửi BPM lên server để AI lấy tempo
1020
  ws.send(JSON.stringify({ bpm: currentBpm }));
1021
  };
1022
 
1023
- let startTime = Tone.now() + 0.1; // Thời gian bắt đầu chơi nhạc, cách hiện tại 100ms để đảm bảo đồng bộ
1024
 
1025
  ws.onmessage = (event) => {
1026
  const data = JSON.parse(event.data);
1027
 
 
 
 
 
 
1028
  data.notes.forEach(n => {
1029
- const note = Tone.Frequency(n.note, "midi").toNote();
1030
- const t = startTime + n.time; // 🔥 timeline chuẩn
1031
 
1032
  if (n.type === "melody") {
1033
- piano.triggerAttackRelease(note, n.duration, t);
 
 
 
1034
  }
1035
-
1036
  else if (n.type === "chord") {
1037
- pad.triggerAttackRelease(note, n.duration, t);
1038
- }
1039
-
1040
- else if (n.type === "drum") {
1041
- if (n.note === 36) kick.triggerAttackRelease("C1", "8n", t);
1042
- if (n.note === 38) snare.triggerAttackRelease("16n", t);
1043
- if (n.note === 42) hihat.triggerAttackRelease("32n", t); // hihat
1044
  }
 
1045
  });
1046
 
1047
- startTime += data.batch_duration; // FIX: dùng batch_duration thực tế từ server
 
 
 
 
 
 
 
 
 
 
 
 
1048
  };
1049
 
1050
  ws.onerror = (err) => {
@@ -1053,22 +1156,22 @@
1053
  };
1054
 
1055
  ws.onclose = () => {
1056
- btnGenerate.textContent = '2. Sinh nhạc AI';
1057
- btnGenerate.disabled = false;
1058
- btnStop.disabled = true;
1059
  bpmDisplay.style.animation = 'none';
 
1060
  };
1061
  });
1062
 
1063
- // ===============================
1064
- // STOP MUSIC (CẬP NHẬT WEBSOCKET)
1065
- // ===============================
1066
  btnStop.addEventListener('click', () => {
1067
- if (ws) {
1068
- ws.close(); // Cắt kết nối, Server sẽ tự động dừng suy luận
1069
- ws = null;
1070
- }
1071
- piano.releaseAll(); // Ngắt ngay lập tức các âm vang còn sót lại
1072
  setStatus('Đã dừng nhạc.', 'info');
1073
  });
1074
  </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: 5, mid: -1, high: 3 }).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: 4, // +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
 
900
+ // 🌊 PAD — volume tăng, attack thay đổi theo BPM
901
  const pad = new Tone.PolySynth(Tone.Synth, {
902
+ volume: -6, // -6dB (trước là -12dB)
903
  oscillator: { type: "sine" },
904
+ envelope: { attack: 1.2, decay: 0.5, sustain: 0.8, release: 3 }
 
 
 
 
 
905
  }).connect(reverb);
906
 
907
+ // 🥁 KICK — volume tăng đáng kể
908
  const kick = new Tone.MembraneSynth({
909
+ volume: -2, // -2dB (trước là -8dB)
910
+ pitchDecay: 0.06, octaves: 7,
911
+ envelope: { attack: 0.001, decay: 0.35, sustain: 0 }
 
 
 
 
 
912
  }).connect(compressor);
913
 
914
  // 🥁 SNARE
915
  const snare = new Tone.NoiseSynth({
916
+ volume: -4, // -4dB (trước là -9dB)
917
  noise: { type: "white" },
918
+ envelope: { attack: 0.001, decay: 0.13, sustain: 0 }
 
 
 
 
919
  }).connect(compressor);
920
 
921
  // 🥁 HIHAT
922
  const hihat = new Tone.MetalSynth({
923
+ volume: -16, // -16dB (trước là -22dB)
924
  frequency: 400,
925
+ envelope: { attack: 0.001, decay: 0.055, release: 0.01 },
926
+ harmonicity: 5.1, modulationIndex: 32, resonance: 4000, octaves: 1.5
 
 
 
927
  }).connect(compressor);
928
 
929
+ log("🎹 Đã khởi tạo audio chain. Volume boosted.");
 
930
  setStatus("Sẵn sàng sinh nhạc AI", "ok");
931
 
932
+ // ================================================================
933
+ // DRUM ENGINE client-side, Tone.Transport grid, không bao giờ lệch
934
+ // ================================================================
935
+
936
+ // 16-step patterns (mỗi step = 1 sixteenth note)
937
+ // vel: 0 = tắt, 1–127 = velocity
938
+ const DRUM_PATTERNS = {
939
+ // BPM thấp (< 75): half-time lofi, thưa, mơ màng
940
+ calm: {
941
+ kick: [90, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0],
942
+ snare: [ 0, 0, 0, 0, 76, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0],
943
+ hihat: [55, 0,45, 0, 52, 0,42, 0, 55, 0,45, 0, 52, 0,42, 0],
944
+ },
945
+ // Variation của calm (bar 5–6 trong chu kỳ 8 bar)
946
+ calm_var: {
947
+ kick: [88, 0, 0, 0, 0, 0,62, 0, 75, 0, 0, 0, 0, 0, 0, 0],
948
+ snare: [ 0, 0, 0, 0, 74, 0, 0, 0, 0, 0,30, 0, 70, 0, 0, 0],
949
+ hihat: [55, 0,45, 0, 50, 0,42, 0, 55,35,45, 0, 50, 0,40,30],
950
+ },
951
+
952
+ // BPM vừa (75–105): groove lofi 4/4 với syncopation nhẹ
953
+ groove: {
954
+ kick: [88, 0, 0, 0, 0, 0,68, 0, 85, 0, 0,58, 0, 0, 0, 0],
955
+ snare: [ 0, 0, 0, 0, 80, 0, 0, 0, 0, 0, 0, 0, 75, 0, 0, 0],
956
+ hihat: [62, 0,50,42, 58, 0,52, 0, 60,38,52, 0, 58, 0,48,38],
957
+ },
958
+ groove_var: {
959
+ kick: [88, 0, 0, 0, 65, 0,68, 0, 85, 0, 0, 0, 58, 0,72, 0],
960
+ snare: [ 0, 0, 0,28, 80, 0, 0, 0, 0, 0,32, 0, 75, 0, 0,40],
961
+ hihat: [62,42,50, 0, 58,40,52,35, 60, 0,52,42, 58,40,48, 0],
962
+ },
963
+
964
+ // BPM cao (> 105): active, nhiều hihat 16th, kick syncopated
965
+ active: {
966
+ kick: [90, 0,68, 0, 0, 0,75, 0, 88, 0, 0, 0, 78, 0,65, 0],
967
+ snare: [ 0, 0, 0, 0, 82, 0, 0,55, 0, 0, 0, 0, 78, 0, 0,48],
968
+ hihat: [65,48,55,40, 60,45,55,35, 62,42,52,38, 60, 0,52,48],
969
+ },
970
+ active_var: {
971
+ kick: [90, 0,68, 0, 62, 0,75, 0, 88, 0,55, 0, 78, 0,65,45],
972
+ snare: [ 0, 0, 0,35, 82, 0, 0,55, 0,28, 0, 0, 78, 0,38, 0],
973
+ hihat: [65,48,55, 0, 60,45, 0,35, 62,42,52,38, 0,40,52,48],
974
+ },
975
+
976
+ // Breakdown: mỗi 8 bar, bar thứ 7 — tối giản, tạo cảm giác "breath"
977
+ breakdown: {
978
+ kick: [80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
979
+ snare: [ 0, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0],
980
+ hihat: [45, 0, 0, 0, 42, 0, 0, 0, 45, 0, 0, 0, 42, 0, 0, 0],
981
+ },
982
+ };
983
+
984
+ function choosePattern(bpm, barIdx) {
985
+ const cycle = barIdx % 8;
986
+ if (cycle === 6) return 'breakdown'; // bar 7 → breakdown
987
+ const isVar = cycle >= 4; // bar 5–6 → variation
988
+ if (bpm < 75) return isVar ? 'calm_var' : 'calm';
989
+ if (bpm < 105) return isVar ? 'groove_var' : 'groove';
990
+ return isVar ? 'active_var' : 'active';
991
+ }
992
+
993
+ let drumRepeatId = null;
994
+ let drumBarCount = 0;
995
+
996
+ function startDrumEngine(bpm) {
997
+ stopDrumEngine();
998
+ Tone.getTransport().bpm.value = bpm;
999
+
1000
+ const step16 = 60 / bpm / 4; // duration của 1 sixteenth note (giây)
1001
+
1002
+ drumRepeatId = Tone.getTransport().scheduleRepeat((barTime) => {
1003
+ const patName = choosePattern(currentBpm, drumBarCount);
1004
+ const pat = DRUM_PATTERNS[patName];
1005
+ const s16 = 60 / currentBpm / 4; // re-read mỗi bar để phản ánh BPM mới
1006
+
1007
+ for (let i = 0; i < 16; i++) {
1008
+ const t = barTime + i * s16;
1009
+ const hu = () => (Math.random() - 0.5) * 0.012; // humanize ±6ms
1010
+
1011
+ if (pat.kick[i]) kick .triggerAttackRelease('C1', '16n', t + hu(), pat.kick[i] / 127);
1012
+ if (pat.snare[i]) snare.triggerAttackRelease('16n', t + hu(), pat.snare[i] / 127);
1013
+ if (pat.hihat[i]) hihat.triggerAttackRelease('32n', t + Math.abs(hu()), pat.hihat[i] / 127);
1014
+ }
1015
+ drumBarCount++;
1016
+ }, '1m'); // repeat mỗi 1 measure
1017
+
1018
+ Tone.getTransport().start();
1019
+ log(`🥁 Drum engine started — pattern: ${choosePattern(bpm, 0)}`);
1020
+ }
1021
+
1022
+ function stopDrumEngine() {
1023
+ if (drumRepeatId !== null) {
1024
+ Tone.getTransport().clear(drumRepeatId);
1025
+ drumRepeatId = null;
1026
+ }
1027
+ Tone.getTransport().stop();
1028
+ Tone.getTransport().cancel();
1029
+ drumBarCount = 0;
1030
+ }
1031
+
1032
+ // ================================================================
1033
+ // BPM-REACTIVE: thay đổi texture theo nhịp tim
1034
+ // ================================================================
1035
+ function applyBpmTexture(bpm) {
1036
+ // Reverb: BPM thấp = nhiều không gian (dreamy), cao = khô hơn (energetic)
1037
+ const reverbWet = bpm < 70 ? 0.50 :
1038
+ bpm < 90 ? 0.40 :
1039
+ bpm < 110 ? 0.30 : 0.20;
1040
+ reverb.wet.rampTo(reverbWet, 2);
1041
+
1042
+ // Delay feedback: BPM thấp = echo dài hơn
1043
+ const delayFb = bpm < 80 ? 0.32 : bpm < 110 ? 0.22 : 0.14;
1044
+ delay.feedback.rampTo(delayFb, 2);
1045
+
1046
+ // Pad volume: BPM thấp = pad nổi hơn (ambient), cao = nhường chỗ cho melody
1047
+ const padVol = bpm < 75 ? -4 : bpm < 100 ? -8 : -13;
1048
+ pad.volume.rampTo(padVol, 2);
1049
+
1050
+ // Pad attack: BPM thấp = chậm mơ màng, cao = nhanh rõ ràng
1051
+ // (không thể rampTo envelope, cập nhật ngay)
1052
+ const padAttack = bpm < 75 ? 1.8 : bpm < 100 ? 1.0 : 0.5;
1053
+ pad.set({ envelope: { attack: padAttack } });
1054
+
1055
+ // BPM display color: mát → ấm theo nhịp tim
1056
+ const color = bpm < 70 ? '#4ecca3' : // xanh lá — nghỉ ngơi
1057
+ bpm < 90 ? '#a0d8ef' : // xanh dương — nhẹ nhàng
1058
+ bpm < 110 ? '#f9c74f' : // vàng — vận động
1059
+ '#e94560'; // đỏ — cường độ cao
1060
+ bpmDisplay.style.color = color;
1061
+ bpmDisplay.style.textShadow = `0 0 20px ${color}`;
1062
+
1063
+ // Cập nhật Tone.Transport BPM (drum engine tự pick up)
1064
+ Tone.getTransport().bpm.rampTo(bpm, 1);
1065
+
1066
+ log(`🎚 Texture → reverb:${reverbWet} delayFb:${delayFb} padVol:${padVol} bpm:${bpm}`);
1067
+ }
1068
+
1069
+ // Override applyBpm để gọi texture update
1070
+ const _origApplyBpm = applyBpm;
1071
+ // Không thể override function declaration, thêm hook vào ws.send thay thế
1072
+ function sendBpmToServer(bpm) {
1073
+ if (ws && ws.readyState === WebSocket.OPEN) {
1074
+ ws.send(JSON.stringify({ bpm }));
1075
+ }
1076
+ applyBpmTexture(bpm);
1077
+ }
1078
+
1079
+ // ================================================================
1080
+ // WEBSOCKET — sinh nhạc
1081
+ // ================================================================
1082
  btnGenerate.addEventListener('click', async () => {
1083
  if (currentBpm === 0) {
1084
  alert('Chưa có BPM! Kết nối đồng hồ hoặc nhập thủ công.');
1085
  return;
1086
  }
1087
 
 
1088
  await Tone.start();
1089
  await Tone.loaded();
1090
+ log("✅ Piano samples loaded");
1091
 
1092
+ if (ws) ws.close();
1093
 
1094
+ setStatus(`Đang kết nối AI với ${currentBpm} BPM…`, 'info');
1095
  btnGenerate.textContent = 'AI đang truyền nốt…';
1096
+ btnGenerate.disabled = true;
1097
+ btnStop.disabled = false;
1098
  bpmDisplay.style.animation = 'pulse 1s infinite';
1099
 
1100
+ // Khởi động texture + drum engine ngay
1101
+ applyBpmTexture(currentBpm);
1102
+ startDrumEngine(currentBpm);
1103
+
1104
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1105
+ ws = new WebSocket(`${protocol}//${window.location.host}/ws/vibe`);
 
1106
 
1107
  ws.onopen = () => {
1108
+ setStatus('🎵 Đang phát nhạc Real-time…', 'ok');
 
1109
  ws.send(JSON.stringify({ bpm: currentBpm }));
1110
  };
1111
 
1112
+ let startTime = Tone.now() + 0.15;
1113
 
1114
  ws.onmessage = (event) => {
1115
  const data = JSON.parse(event.data);
1116
 
1117
+ // Nếu BPM thay đổi (server gửi về), cập nhật texture
1118
+ if (data.bpm && data.bpm !== currentBpm) {
1119
+ applyBpmTexture(data.bpm);
1120
+ }
1121
+
1122
  data.notes.forEach(n => {
1123
+ const noteStr = Tone.Frequency(n.note, "midi").toNote();
1124
+ const t = startTime + n.time;
1125
 
1126
  if (n.type === "melody") {
1127
+ // Velocity melody cũng phản ánh BPM: cao → piano mạnh hơn
1128
+ const velBoost = currentBpm > 100 ? 1.15 : 1.0;
1129
+ const vel = Math.min(1, (n.velocity / 127) * velBoost);
1130
+ piano.triggerAttackRelease(noteStr, n.duration, t, vel);
1131
  }
 
1132
  else if (n.type === "chord") {
1133
+ pad.triggerAttackRelease(noteStr, n.duration, t, n.velocity / 127);
 
 
 
 
 
 
1134
  }
1135
+ // drum events từ server bị bỏ qua — drum engine client đã xử lý
1136
  });
1137
 
1138
+ startTime += data.batch_duration;
1139
+ };
1140
+
1141
+ // Mỗi khi BPM thay đổi từ đồng hồ/manual → cập nhật texture + drum
1142
+ const origApply = applyBpm;
1143
+ // Patch ws.send để bắt BPM change
1144
+ const _origSend = ws.send.bind(ws);
1145
+ ws.send = function(data) {
1146
+ try {
1147
+ const parsed = JSON.parse(data);
1148
+ if (parsed.bpm) applyBpmTexture(parsed.bpm);
1149
+ } catch {}
1150
+ return _origSend(data);
1151
  };
1152
 
1153
  ws.onerror = (err) => {
 
1156
  };
1157
 
1158
  ws.onclose = () => {
1159
+ btnGenerate.textContent = '2. Sinh nhạc AI';
1160
+ btnGenerate.disabled = false;
1161
+ btnStop.disabled = true;
1162
  bpmDisplay.style.animation = 'none';
1163
+ stopDrumEngine();
1164
  };
1165
  });
1166
 
1167
+ // ================================================================
1168
+ // STOP
1169
+ // ================================================================
1170
  btnStop.addEventListener('click', () => {
1171
+ if (ws) { ws.close(); ws = null; }
1172
+ stopDrumEngine();
1173
+ piano.releaseAll();
1174
+ pad.releaseAll();
 
1175
  setStatus('Đã dừng nhạc.', 'info');
1176
  });
1177
  </script>
main.py CHANGED
@@ -80,53 +80,6 @@ def get_temperature(bpm: int) -> float:
80
  t = (bpm - 40) / (180 - 40)
81
  return round(0.65 + t * 0.40, 3)
82
 
83
- def generate_drum_pattern(beat: float, bars: int = 2) -> list:
84
- """
85
- Lofi drum pattern:
86
- - Kick: beats 1 và 3
87
- - Snare: beats 2 và 4 (có humanize nhỏ)
88
- - Hihat: 8th notes, bỏ ngẫu nhiên 15% để tạo groove
89
- """
90
- drums = []
91
- bar_dur = beat * 4
92
-
93
- for bar in range(bars):
94
- base = bar * bar_dur
95
-
96
- # Kick (note 36)
97
- drums.append({
98
- "note": 36, "velocity": 82, "duration": 0.05,
99
- "type": "drum", "time": round(base, 4)
100
- })
101
- # Kick beat 3 (nhẹ hơn một chút)
102
- drums.append({
103
- "note": 36, "velocity": 70, "duration": 0.05,
104
- "type": "drum", "time": round(base + beat * 2, 4)
105
- })
106
-
107
- # Snare (note 38) — humanize ±15ms
108
- drums.append({
109
- "note": 38, "velocity": 78, "duration": 0.05,
110
- "type": "drum",
111
- "time": round(base + beat + random.uniform(-0.015, 0.015), 4)
112
- })
113
- drums.append({
114
- "note": 38, "velocity": 72, "duration": 0.05,
115
- "type": "drum",
116
- "time": round(base + beat * 3 + random.uniform(-0.015, 0.015), 4)
117
- })
118
-
119
- # Hihat (note 42) — 8th notes, bỏ ngẫu nhiên
120
- for i in range(8):
121
- if random.random() > 0.15: # 85% chance mỗi hit
122
- vel = random.randint(38, 58)
123
- drums.append({
124
- "note": 42, "velocity": vel, "duration": 0.03,
125
- "type": "drum",
126
- "time": round(base + i * (beat * 0.5), 4)
127
- })
128
-
129
- return drums
130
 
131
  # ==========================================
132
  # WEBSOCKET ENDPOINT
@@ -267,14 +220,9 @@ async def websocket_vibe(websocket: WebSocket):
267
  "time": round(t, 4)
268
  })
269
 
270
- # ============================================
271
- # DRUM PATTERN
272
- # ============================================
273
- drums = generate_drum_pattern(beat, bars=BARS_PER_BATCH)
274
-
275
  # Gửi toàn bộ batch về client
276
  await websocket.send_json({
277
- "notes": melody_notes + chord_notes + drums,
278
  # FIX: gửi batch_duration thực tế thay vì step cứng
279
  "batch_duration": round(batch_dur, 4)
280
  })
 
80
  t = (bpm - 40) / (180 - 40)
81
  return round(0.65 + t * 0.40, 3)
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  # ==========================================
85
  # WEBSOCKET ENDPOINT
 
220
  "time": round(t, 4)
221
  })
222
 
 
 
 
 
 
223
  # Gửi toàn bộ batch về client
224
  await websocket.send_json({
225
+ "notes": melody_notes + chord_notes, # drums handled client-side
226
  # FIX: gửi batch_duration thực tế thay vì step cứng
227
  "batch_duration": round(batch_dur, 4)
228
  })