Refactor audio chain and drum engine for improved performance and responsiveness
Browse files- index.html +231 -128
- main.py +1 -53
index.html
CHANGED
|
@@ -871,180 +871,283 @@
|
|
| 871 |
btnDisconnect.addEventListener('click', doDisconnect);
|
| 872 |
btnReconnect.addEventListener('click', doReconnect);
|
| 873 |
|
| 874 |
-
// ===============================
|
| 875 |
-
//
|
| 876 |
-
|
| 877 |
-
const
|
| 878 |
-
|
| 879 |
-
// 2. Compressor (giữ âm ổn định)
|
| 880 |
const compressor = new Tone.Compressor({
|
| 881 |
-
threshold: -
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
});
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 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:
|
| 921 |
-
|
| 922 |
-
"D#
|
| 923 |
-
"
|
| 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
|
| 940 |
const pad = new Tone.PolySynth(Tone.Synth, {
|
| 941 |
-
volume: -
|
| 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: -
|
| 954 |
-
pitchDecay: 0.
|
| 955 |
-
|
| 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: -
|
| 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: -
|
| 977 |
frequency: 400,
|
| 978 |
-
envelope: { attack: 0.001, decay: 0.
|
| 979 |
-
harmonicity: 5.1,
|
| 980 |
-
modulationIndex: 32,
|
| 981 |
-
resonance: 4000,
|
| 982 |
-
octaves: 1.5
|
| 983 |
}).connect(compressor);
|
| 984 |
|
| 985 |
-
|
| 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 |
-
//
|
| 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 |
-
|
| 1002 |
|
| 1003 |
-
if (ws) ws.close();
|
| 1004 |
|
| 1005 |
-
setStatus(`Đang kết nối
|
| 1006 |
btnGenerate.textContent = 'AI đang truyền nốt…';
|
| 1007 |
-
btnGenerate.disabled
|
| 1008 |
-
btnStop.disabled
|
| 1009 |
bpmDisplay.style.animation = 'pulse 1s infinite';
|
| 1010 |
|
| 1011 |
-
//
|
| 1012 |
-
|
|
|
|
|
|
|
| 1013 |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 1014 |
-
|
| 1015 |
-
ws = new WebSocket(wsUrl);
|
| 1016 |
|
| 1017 |
ws.onopen = () => {
|
| 1018 |
-
setStatus('🎵 Đang phát nhạc Real-time
|
| 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.
|
| 1024 |
|
| 1025 |
ws.onmessage = (event) => {
|
| 1026 |
const data = JSON.parse(event.data);
|
| 1027 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1028 |
data.notes.forEach(n => {
|
| 1029 |
-
const
|
| 1030 |
-
const t = startTime + n.time;
|
| 1031 |
|
| 1032 |
if (n.type === "melody") {
|
| 1033 |
-
piano
|
|
|
|
|
|
|
|
|
|
| 1034 |
}
|
| 1035 |
-
|
| 1036 |
else if (n.type === "chord") {
|
| 1037 |
-
pad.triggerAttackRelease(
|
| 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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1048 |
};
|
| 1049 |
|
| 1050 |
ws.onerror = (err) => {
|
|
@@ -1053,22 +1156,22 @@
|
|
| 1053 |
};
|
| 1054 |
|
| 1055 |
ws.onclose = () => {
|
| 1056 |
-
btnGenerate.textContent
|
| 1057 |
-
btnGenerate.disabled
|
| 1058 |
-
btnStop.disabled
|
| 1059 |
bpmDisplay.style.animation = 'none';
|
|
|
|
| 1060 |
};
|
| 1061 |
});
|
| 1062 |
|
| 1063 |
-
// ===============================
|
| 1064 |
-
// STOP
|
| 1065 |
-
// ===============================
|
| 1066 |
btnStop.addEventListener('click', () => {
|
| 1067 |
-
if (ws) {
|
| 1068 |
-
|
| 1069 |
-
|
| 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
|
| 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 |
})
|