Refactor audio chain for enhanced dynamics and responsiveness to BPM changes
Browse files- index.html +131 -284
index.html
CHANGED
|
@@ -871,348 +871,195 @@
|
|
| 871 |
btnDisconnect.addEventListener('click', doDisconnect);
|
| 872 |
btnReconnect.addEventListener('click', doReconnect);
|
| 873 |
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
//
|
| 877 |
-
const
|
| 878 |
-
|
| 879 |
-
frequency: 140,
|
| 880 |
-
rolloff: -24
|
| 881 |
-
}).connect(masterGain);
|
| 882 |
-
|
| 883 |
-
// Soft compressor
|
| 884 |
const compressor = new Tone.Compressor({
|
| 885 |
-
threshold: -
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
}).connect(
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 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:
|
| 920 |
-
|
| 921 |
urls: {
|
| 922 |
-
A1:"A1.mp3",
|
| 923 |
-
|
| 924 |
-
"D#
|
| 925 |
-
"
|
| 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 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
// =========================================================
|
| 951 |
-
|
| 952 |
-
|
| 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 |
-
//
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
reverb
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
);
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
pad.
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
);
|
| 1021 |
-
|
| 1022 |
-
//
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 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 |
-
|
| 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.
|
| 1071 |
-
|
| 1072 |
if (ws) ws.close();
|
| 1073 |
|
| 1074 |
-
setStatus(
|
| 1075 |
-
|
| 1076 |
-
|
| 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 |
-
|
| 1089 |
-
? 'wss:'
|
| 1090 |
-
: 'ws:';
|
| 1091 |
-
|
| 1092 |
-
ws = new WebSocket(
|
| 1093 |
-
`${protocol}//${window.location.host}/ws/vibe`
|
| 1094 |
-
);
|
| 1095 |
|
| 1096 |
ws.onopen = () => {
|
| 1097 |
-
|
| 1098 |
-
|
| 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.
|
| 1109 |
|
| 1110 |
ws.onmessage = (event) => {
|
| 1111 |
-
|
| 1112 |
const data = JSON.parse(event.data);
|
| 1113 |
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 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 |
-
|
| 1131 |
-
const vel =
|
| 1132 |
-
|
| 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 |
-
|
| 1167 |
-
|
| 1168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
);
|
| 1174 |
};
|
| 1175 |
|
| 1176 |
ws.onclose = () => {
|
| 1177 |
-
|
| 1178 |
-
btnGenerate.disabled
|
| 1179 |
-
btnStop.disabled
|
| 1180 |
-
|
| 1181 |
bpmDisplay.style.animation = 'none';
|
| 1182 |
};
|
| 1183 |
});
|
| 1184 |
|
| 1185 |
-
// =========================================================
|
| 1186 |
-
// STOP
|
| 1187 |
-
// =========================================================
|
| 1188 |
-
|
| 1189 |
btnStop.addEventListener('click', () => {
|
|
|
|
| 1190 |
|
| 1191 |
-
|
| 1192 |
-
|
| 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 |
-
|
| 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 = cơ thể đang stress → nhạc kéo ngược lại: chậm,
|
| 912 |
+
// mơ 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>
|