Refactor audio processing and LSTM model for improved sound dynamics and melody generation
Browse files- index.html +47 -29
- main.py +89 -267
index.html
CHANGED
|
@@ -879,14 +879,14 @@
|
|
| 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:
|
| 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:
|
| 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",
|
|
@@ -895,14 +895,25 @@
|
|
| 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 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
// ================================================================
|
|
@@ -914,16 +925,17 @@
|
|
| 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.
|
| 918 |
-
bpm < 90 ? 0.
|
| 919 |
-
bpm < 110 ? 0.
|
| 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 |
-
|
| 926 |
-
//
|
|
|
|
| 927 |
const padVol = bpm < 75 ? -12 : bpm < 100 ? -8 : -4;
|
| 928 |
pad.volume.rampTo(padVol, 2.5);
|
| 929 |
|
|
@@ -931,6 +943,8 @@
|
|
| 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 |
|
|
@@ -968,7 +982,11 @@
|
|
| 968 |
await Tone.start();
|
| 969 |
await Tone.loaded();
|
| 970 |
log("β
Piano samples loaded");
|
| 971 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
if (ws) ws.close();
|
| 973 |
|
| 974 |
setStatus(`Δang kαΊΏt nα»i AI vα»i ${currentBpm} BPMβ¦`, 'info');
|
|
@@ -977,7 +995,7 @@
|
|
| 977 |
btnStop.disabled = false;
|
| 978 |
bpmDisplay.style.animation = 'pulse 1s infinite';
|
| 979 |
|
| 980 |
-
// Khα»i Δα»ng texture
|
| 981 |
applyBpmTexture(currentBpm);
|
| 982 |
|
| 983 |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
@@ -993,9 +1011,9 @@
|
|
| 993 |
ws.onmessage = (event) => {
|
| 994 |
const data = JSON.parse(event.data);
|
| 995 |
|
| 996 |
-
//
|
| 997 |
-
if (data.
|
| 998 |
-
|
| 999 |
}
|
| 1000 |
|
| 1001 |
data.notes.forEach(n => {
|
|
@@ -1011,7 +1029,9 @@
|
|
| 1011 |
else if (n.type === "chord") {
|
| 1012 |
pad.triggerAttackRelease(noteStr, n.duration, t, n.velocity / 127);
|
| 1013 |
}
|
| 1014 |
-
|
|
|
|
|
|
|
| 1015 |
});
|
| 1016 |
|
| 1017 |
startTime += data.batch_duration;
|
|
@@ -1043,24 +1063,22 @@
|
|
| 1043 |
});
|
| 1044 |
|
| 1045 |
// ================================================================
|
| 1046 |
-
// STOP β
|
| 1047 |
// ================================================================
|
| 1048 |
btnStop.addEventListener('click', () => {
|
| 1049 |
if (ws) { ws.close(); ws = null; }
|
| 1050 |
|
| 1051 |
-
//
|
| 1052 |
-
|
|
|
|
|
|
|
|
|
|
| 1053 |
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 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>
|
| 1066 |
</body>
|
|
|
|
| 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",
|
|
|
|
| 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 |
+
// πΈ BASS LINE β theo root cα»§a chord, luΓ΄n ΔΓΊng tΓ΄ng
|
| 908 |
+
const bass = new Tone.Synth({
|
| 909 |
+
volume: -4,
|
| 910 |
+
oscillator: { type: "triangle" },
|
| 911 |
+
envelope: { attack: 0.04, decay: 0.3, sustain: 0.6, release: 1.2 }
|
| 912 |
+
}).connect(compressor);
|
| 913 |
+
|
| 914 |
log("πΉ ΔΓ£ khα»i tαΊ‘o audio chain. Volume boosted.");
|
| 915 |
setStatus("SαΊ΅n sΓ ng sinh nhαΊ‘c AI", "ok");
|
| 916 |
+
|
| 917 |
// ================================================================
|
| 918 |
// BPM-REACTIVE: thay Δα»i texture theo nhα»p tim
|
| 919 |
// ================================================================
|
|
|
|
| 925 |
|
| 926 |
// Reverb: BPM CAO = nhiα»u reverb hΖ‘n (mΖ‘ mΓ ng, bay bα»ng, dα»u)
|
| 927 |
// BPM thαΊ₯p = Γt reverb (rΓ΅ rΓ ng, cΓ³ chiα»u sΓ’u nhαΊΉ)
|
| 928 |
+
const reverbWet = bpm < 70 ? 0.28 :
|
| 929 |
+
bpm < 90 ? 0.35 :
|
| 930 |
+
bpm < 110 ? 0.46 : 0.58;
|
| 931 |
reverb.wet.rampTo(reverbWet, 2.5);
|
| 932 |
|
| 933 |
// Delay feedback: BPM cao = echo dΓ i hΖ‘n (hypnotic, calming)
|
| 934 |
const delayFb = bpm < 80 ? 0.16 : bpm < 110 ? 0.24 : 0.34;
|
| 935 |
delay.feedback.rampTo(delayFb, 2.5);
|
| 936 |
+
|
| 937 |
+
// Pad volume: BPM cao = pad nα»i hΖ‘n (ambient nhαΊ₯n chΓ¬m, che melody)
|
| 938 |
+
// BPM thαΊ₯p = pad lΓΉi, melody groove rΓ΅ hΖ‘n
|
| 939 |
const padVol = bpm < 75 ? -12 : bpm < 100 ? -8 : -4;
|
| 940 |
pad.volume.rampTo(padVol, 2.5);
|
| 941 |
|
|
|
|
| 943 |
const padAttack = bpm < 75 ? 0.6 : bpm < 100 ? 1.2 : 2.2;
|
| 944 |
pad.set({ envelope: { attack: padAttack } });
|
| 945 |
|
| 946 |
+
// NhαΊ‘c tempo: BPM cao β tempo chαΊm hΖ‘n Δα» kΓ©o nhα»p tim xuα»ng
|
| 947 |
+
// map BPM [40,180] β music tempo [85, 55] BPM (inverse linear)
|
| 948 |
const musicTempo = Math.round(85 - (bpm - 40) * (30 / 140));
|
| 949 |
Tone.getTransport().bpm.rampTo(Math.max(52, Math.min(88, musicTempo)), 3);
|
| 950 |
|
|
|
|
| 982 |
await Tone.start();
|
| 983 |
await Tone.loaded();
|
| 984 |
log("β
Piano samples loaded");
|
| 985 |
+
|
| 986 |
+
// KhΓ΄i phα»₯c gain (cΓ³ thα» Δang = 0 nαΊΏu vα»«a Stop)
|
| 987 |
+
masterGain.gain.cancelScheduledValues(Tone.now());
|
| 988 |
+
masterGain.gain.setValueAtTime(1.5, Tone.now());
|
| 989 |
+
|
| 990 |
if (ws) ws.close();
|
| 991 |
|
| 992 |
setStatus(`Δang kαΊΏt nα»i AI vα»i ${currentBpm} BPMβ¦`, 'info');
|
|
|
|
| 995 |
btnStop.disabled = false;
|
| 996 |
bpmDisplay.style.animation = 'pulse 1s infinite';
|
| 997 |
|
| 998 |
+
// Khα»i Δα»ng texture ngay
|
| 999 |
applyBpmTexture(currentBpm);
|
| 1000 |
|
| 1001 |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
|
| 1011 |
ws.onmessage = (event) => {
|
| 1012 |
const data = JSON.parse(event.data);
|
| 1013 |
|
| 1014 |
+
// Sync Transport BPM vα»i music_bpm server trαΊ£ vα» (entrainment tempo)
|
| 1015 |
+
if (data.music_bpm) {
|
| 1016 |
+
Tone.getTransport().bpm.rampTo(data.music_bpm, 2);
|
| 1017 |
}
|
| 1018 |
|
| 1019 |
data.notes.forEach(n => {
|
|
|
|
| 1029 |
else if (n.type === "chord") {
|
| 1030 |
pad.triggerAttackRelease(noteStr, n.duration, t, n.velocity / 127);
|
| 1031 |
}
|
| 1032 |
+
else if (n.type === "bass") {
|
| 1033 |
+
bass.triggerAttackRelease(noteStr, n.duration, t, n.velocity / 127);
|
| 1034 |
+
}
|
| 1035 |
});
|
| 1036 |
|
| 1037 |
startTime += data.batch_duration;
|
|
|
|
| 1063 |
});
|
| 1064 |
|
| 1065 |
// ================================================================
|
| 1066 |
+
// STOP β tαΊ―t ngay lαΊp tα»©c, khΓ΄ng chα» note ΔΓ£ schedule trong buffer
|
| 1067 |
// ================================================================
|
| 1068 |
btnStop.addEventListener('click', () => {
|
| 1069 |
if (ws) { ws.close(); ws = null; }
|
| 1070 |
|
| 1071 |
+
// cancelScheduledValues triα»t tiΓͺu mα»i ramp Δang chα»,
|
| 1072 |
+
// setValueAtTime(0) tαΊ―t gain TỨC THα»I β kα» cαΊ£ note ΔΓ£ queue sαΊ΅n.
|
| 1073 |
+
// Gain KHΓNG Δược restore α» ΔΓ’y β chα» restore khi nhαΊ₯n Generate lαΊ‘i.
|
| 1074 |
+
masterGain.gain.cancelScheduledValues(Tone.now());
|
| 1075 |
+
masterGain.gain.setValueAtTime(0, Tone.now());
|
| 1076 |
|
| 1077 |
+
piano.releaseAll();
|
| 1078 |
+
pad.releaseAll();
|
| 1079 |
+
bass.triggerRelease();
|
|
|
|
| 1080 |
|
| 1081 |
setStatus('ΔΓ£ dα»«ng nhαΊ‘c.', 'info');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1082 |
});
|
| 1083 |
</script>
|
| 1084 |
</body>
|
main.py
CHANGED
|
@@ -24,13 +24,7 @@ class BioVibeAI(nn.Module):
|
|
| 24 |
def __init__(self, vocab_size):
|
| 25 |
super().__init__()
|
| 26 |
self.embedding = nn.Embedding(vocab_size, 256)
|
| 27 |
-
self.lstm = nn.LSTM(
|
| 28 |
-
256,
|
| 29 |
-
512,
|
| 30 |
-
num_layers=3,
|
| 31 |
-
batch_first=True,
|
| 32 |
-
dropout=0.3
|
| 33 |
-
)
|
| 34 |
self.norm = nn.LayerNorm(512)
|
| 35 |
self.fc = nn.Linear(512, vocab_size)
|
| 36 |
|
|
@@ -41,30 +35,22 @@ class BioVibeAI(nn.Module):
|
|
| 41 |
out = self.norm(out)
|
| 42 |
return self.fc(out), hidden
|
| 43 |
|
| 44 |
-
|
| 45 |
-
# LOAD MODEL
|
| 46 |
-
# ==========================================
|
| 47 |
-
with open("mapping_dict.pkl", "rb") as f:
|
| 48 |
int_to_note = pickle.load(f)
|
| 49 |
-
|
| 50 |
note_to_int = {n: i for i, n in int_to_note.items()}
|
| 51 |
vocab_size = len(int_to_note)
|
| 52 |
|
| 53 |
device = torch.device("cpu")
|
| 54 |
-
|
| 55 |
model = BioVibeAI(vocab_size=vocab_size)
|
| 56 |
-
|
| 57 |
state_dict = torch.load("model.pth", map_location=device)
|
| 58 |
-
|
| 59 |
clean_state_dict = {
|
| 60 |
-
k[7:] if k.startswith(
|
| 61 |
for k, v in state_dict.items()
|
| 62 |
}
|
| 63 |
-
|
| 64 |
model.load_state_dict(clean_state_dict)
|
| 65 |
model.eval()
|
| 66 |
|
| 67 |
-
with open(
|
| 68 |
corpus = pickle.load(f)
|
| 69 |
|
| 70 |
# ==========================================
|
|
@@ -73,68 +59,65 @@ with open("notes_corpus.pkl", "rb") as f:
|
|
| 73 |
SEQ_LEN = 128
|
| 74 |
BARS_PER_BATCH = 2
|
| 75 |
|
| 76 |
-
#
|
| 77 |
-
SAFE_SCALE = {
|
| 78 |
-
60, 62, 64, 65, 67, 69, 71,
|
| 79 |
-
72, 74, 76, 77, 79
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
CHORD_SETS = {
|
| 83 |
-
"calm": [
|
| 84 |
-
[
|
| 85 |
-
[
|
| 86 |
-
[
|
| 87 |
-
[
|
| 88 |
],
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
[
|
| 92 |
-
[
|
| 93 |
-
[
|
| 94 |
-
[65, 69, 72], # F
|
| 95 |
],
|
| 96 |
}
|
| 97 |
|
| 98 |
# ==========================================
|
| 99 |
-
#
|
| 100 |
# ==========================================
|
|
|
|
| 101 |
def get_music_beat(bpm: int) -> float:
|
| 102 |
"""
|
| 103 |
-
|
| 104 |
-
|
| 105 |
"""
|
| 106 |
-
music_tempo =
|
| 107 |
-
music_tempo = max(
|
| 108 |
return 60.0 / music_tempo
|
| 109 |
|
| 110 |
|
| 111 |
def get_temperature(bpm: int) -> float:
|
| 112 |
"""
|
| 113 |
-
|
| 114 |
-
-
|
| 115 |
-
|
| 116 |
-
- α»n Δα»nh hΖ‘n
|
| 117 |
"""
|
| 118 |
t = (bpm - 40) / (180 - 40)
|
| 119 |
-
return round(0.
|
| 120 |
|
| 121 |
|
| 122 |
def get_melody_params(bpm: int, music_beat: float) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
t = (bpm - 40) / (180 - 40)
|
| 124 |
|
| 125 |
-
vel_low = int(
|
| 126 |
-
vel_high = int(
|
| 127 |
|
| 128 |
-
dur_factor = 0.
|
|
|
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
midi_low = 60
|
| 133 |
-
midi_high = 79
|
| 134 |
|
| 135 |
return {
|
| 136 |
-
"vel_low": max(
|
| 137 |
-
"vel_high": max(
|
| 138 |
"duration": duration,
|
| 139 |
"midi_low": midi_low,
|
| 140 |
"midi_high": midi_high,
|
|
@@ -142,316 +125,155 @@ def get_melody_params(bpm: int, music_beat: float) -> dict:
|
|
| 142 |
|
| 143 |
|
| 144 |
def get_chord_set(bpm: int, bar_count: int) -> list:
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
return CHORD_SETS["groove"] if use_groove else CHORD_SETS["calm"]
|
| 147 |
|
| 148 |
|
| 149 |
# ==========================================
|
| 150 |
-
# WEBSOCKET
|
| 151 |
# ==========================================
|
| 152 |
@app.websocket("/ws/vibe")
|
| 153 |
async def websocket_vibe(websocket: WebSocket):
|
| 154 |
-
|
| 155 |
await websocket.accept()
|
| 156 |
|
| 157 |
current_bpm = 75
|
| 158 |
-
is_playing
|
| 159 |
|
| 160 |
async def listen():
|
| 161 |
nonlocal current_bpm, is_playing
|
| 162 |
-
|
| 163 |
try:
|
| 164 |
while True:
|
| 165 |
data = await websocket.receive_json()
|
| 166 |
-
|
| 167 |
bpm = int(data.get("bpm", current_bpm))
|
| 168 |
-
|
| 169 |
if 40 <= bpm <= 180:
|
| 170 |
current_bpm = bpm
|
| 171 |
-
|
| 172 |
except Exception:
|
| 173 |
is_playing = False
|
| 174 |
|
| 175 |
asyncio.create_task(listen())
|
| 176 |
|
| 177 |
-
#
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
lofi_songs = [
|
| 181 |
-
s for s in corpus
|
| 182 |
-
if s and len(s) >= SEQ_LEN and s[0] == "STYLE_LOFI"
|
| 183 |
-
]
|
| 184 |
-
|
| 185 |
-
pool = lofi_songs if lofi_songs else [
|
| 186 |
-
s for s in corpus if len(s) >= SEQ_LEN
|
| 187 |
-
]
|
| 188 |
-
|
| 189 |
song = random.choice(pool)
|
| 190 |
|
| 191 |
seed = list(song[:SEQ_LEN])
|
| 192 |
seed[0] = "STYLE_LOFI"
|
| 193 |
-
|
| 194 |
pattern = [note_to_int.get(tok, 0) for tok in seed]
|
| 195 |
|
|
|
|
| 196 |
with torch.no_grad():
|
| 197 |
seed_tensor = torch.tensor([pattern], dtype=torch.long)
|
| 198 |
_, hidden = model(seed_tensor)
|
| 199 |
|
| 200 |
bar_count = 0
|
| 201 |
|
| 202 |
-
# ==========================================
|
| 203 |
-
# MAIN LOOP
|
| 204 |
-
# ==========================================
|
| 205 |
try:
|
| 206 |
-
|
| 207 |
while is_playing:
|
| 208 |
-
|
| 209 |
bpm = current_bpm
|
| 210 |
|
| 211 |
music_beat = get_music_beat(bpm)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
batch_dur = bar_dur * BARS_PER_BATCH
|
| 216 |
-
|
| 217 |
-
temp = get_temperature(bpm)
|
| 218 |
-
|
| 219 |
-
mel_params = get_melody_params(
|
| 220 |
-
bpm,
|
| 221 |
-
music_beat
|
| 222 |
-
)
|
| 223 |
-
|
| 224 |
-
chords = get_chord_set(
|
| 225 |
-
bpm,
|
| 226 |
-
bar_count
|
| 227 |
-
)
|
| 228 |
-
|
| 229 |
-
# ==========================================
|
| 230 |
-
# GENERATE MELODY
|
| 231 |
-
# ==========================================
|
| 232 |
melody_notes = []
|
| 233 |
-
|
| 234 |
current_time = 0.0
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
MAX_TOKENS = 180
|
| 239 |
-
|
| 240 |
-
note_density = 0
|
| 241 |
|
| 242 |
for _ in range(MAX_TOKENS):
|
| 243 |
-
|
| 244 |
if current_time >= batch_dur:
|
| 245 |
break
|
| 246 |
|
| 247 |
-
|
| 248 |
-
if note_density > 24 and random.random() < 0.50:
|
| 249 |
-
current_time += music_beat * 0.5
|
| 250 |
-
continue
|
| 251 |
-
|
| 252 |
-
new_tok = torch.tensor(
|
| 253 |
-
[[pattern[-1]]],
|
| 254 |
-
dtype=torch.long
|
| 255 |
-
)
|
| 256 |
-
|
| 257 |
with torch.no_grad():
|
| 258 |
logits, hidden = model(new_tok, hidden)
|
|
|
|
|
|
|
| 259 |
|
| 260 |
-
|
| 261 |
-
logits / temp,
|
| 262 |
-
dim=1
|
| 263 |
-
).numpy()[0]
|
| 264 |
-
|
| 265 |
-
idx = int(
|
| 266 |
-
np.random.choice(
|
| 267 |
-
len(probs),
|
| 268 |
-
p=probs
|
| 269 |
-
)
|
| 270 |
-
)
|
| 271 |
-
|
| 272 |
-
tok = int_to_note[idx]
|
| 273 |
-
|
| 274 |
pattern = pattern[1:] + [idx]
|
| 275 |
|
| 276 |
-
# ==========================================
|
| 277 |
-
# TIME TOKEN
|
| 278 |
-
# ==========================================
|
| 279 |
if tok.startswith("TIME_"):
|
| 280 |
-
|
| 281 |
try:
|
| 282 |
-
gap_beats
|
| 283 |
-
|
| 284 |
-
# hαΊ‘n chαΊΏ gap quΓ‘ dΓ i
|
| 285 |
-
gap_beats = min(gap_beats, 1.5)
|
| 286 |
-
|
| 287 |
current_time += gap_beats * music_beat
|
| 288 |
-
|
| 289 |
except ValueError:
|
| 290 |
pass
|
| 291 |
|
| 292 |
-
# ==========================================
|
| 293 |
-
# NOTE TOKEN
|
| 294 |
-
# ==========================================
|
| 295 |
elif tok.startswith("NOTE_"):
|
| 296 |
-
|
| 297 |
try:
|
| 298 |
midi = int(tok.split("_")[1])
|
|
|
|
| 299 |
|
| 300 |
-
lo =
|
| 301 |
-
hi =
|
| 302 |
-
|
| 303 |
-
while midi < lo:
|
| 304 |
-
midi += 12
|
| 305 |
-
|
| 306 |
-
while midi > hi:
|
| 307 |
-
midi -= 12
|
| 308 |
-
|
| 309 |
-
# snap scale
|
| 310 |
-
midi = min(
|
| 311 |
-
SAFE_SCALE,
|
| 312 |
-
key=lambda x: abs(x - midi)
|
| 313 |
-
)
|
| 314 |
|
| 315 |
-
# trΓ‘nh lαΊ·p note
|
| 316 |
if midi == last_midi and random.random() < 0.40:
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
-2, -1, 1, 2, 3
|
| 320 |
-
])
|
| 321 |
-
|
| 322 |
-
midi += step
|
| 323 |
-
|
| 324 |
-
midi = min(
|
| 325 |
-
SAFE_SCALE,
|
| 326 |
-
key=lambda x: abs(x - midi)
|
| 327 |
-
)
|
| 328 |
-
|
| 329 |
-
# trΓ‘nh jump lα»n
|
| 330 |
-
if last_midi is not None:
|
| 331 |
-
|
| 332 |
-
if abs(midi - last_midi) > 5:
|
| 333 |
-
|
| 334 |
-
midi = last_midi + random.choice([
|
| 335 |
-
-2, -1, 1, 2
|
| 336 |
-
])
|
| 337 |
-
|
| 338 |
-
midi = min(
|
| 339 |
-
SAFE_SCALE,
|
| 340 |
-
key=lambda x: abs(x - midi)
|
| 341 |
-
)
|
| 342 |
|
| 343 |
last_midi = midi
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
mel_params["vel_low"],
|
| 347 |
-
mel_params["vel_high"]
|
| 348 |
-
)
|
| 349 |
-
|
| 350 |
melody_notes.append({
|
| 351 |
-
"note":
|
| 352 |
"velocity": velocity,
|
| 353 |
"duration": mel_params["duration"],
|
| 354 |
-
"type":
|
| 355 |
-
"time":
|
| 356 |
})
|
| 357 |
-
|
| 358 |
-
note_density += 1
|
| 359 |
-
|
| 360 |
-
# breathing space
|
| 361 |
-
if random.random() < 0.18:
|
| 362 |
-
current_time += (
|
| 363 |
-
music_beat *
|
| 364 |
-
random.choice([0.25, 0.5])
|
| 365 |
-
)
|
| 366 |
-
|
| 367 |
except ValueError:
|
| 368 |
pass
|
| 369 |
|
| 370 |
-
#
|
| 371 |
-
# FALLBACK
|
| 372 |
-
# ==========================================
|
| 373 |
if len(melody_notes) < 3:
|
| 374 |
-
|
| 375 |
scale = [60, 62, 64, 67, 69, 72]
|
| 376 |
-
|
| 377 |
t = 0.0
|
| 378 |
-
|
| 379 |
while t < batch_dur:
|
| 380 |
-
|
| 381 |
melody_notes.append({
|
| 382 |
-
"note":
|
| 383 |
-
"velocity": random.randint(
|
| 384 |
-
|
| 385 |
-
mel_params["vel_high"]
|
| 386 |
-
),
|
| 387 |
"duration": mel_params["duration"],
|
| 388 |
-
"type":
|
| 389 |
-
"time":
|
| 390 |
})
|
|
|
|
| 391 |
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
])
|
| 396 |
-
|
| 397 |
-
# ==========================================
|
| 398 |
-
# FILTER OVERDENSITY
|
| 399 |
-
# ==========================================
|
| 400 |
-
melody_notes.sort(key=lambda x: x["time"])
|
| 401 |
-
|
| 402 |
-
filtered = []
|
| 403 |
-
|
| 404 |
-
last_t = -999
|
| 405 |
-
|
| 406 |
-
for n in melody_notes:
|
| 407 |
-
|
| 408 |
-
if n["time"] - last_t > 0.12:
|
| 409 |
-
filtered.append(n)
|
| 410 |
-
last_t = n["time"]
|
| 411 |
-
|
| 412 |
-
melody_notes = filtered
|
| 413 |
-
|
| 414 |
-
# ==========================================
|
| 415 |
-
# CHORD PADS
|
| 416 |
-
# ==========================================
|
| 417 |
-
chord_vel = max(
|
| 418 |
-
26,
|
| 419 |
-
int(42 - (bpm - 40) * 0.04)
|
| 420 |
-
)
|
| 421 |
|
| 422 |
chord_notes = []
|
| 423 |
-
|
| 424 |
for bar_i in range(BARS_PER_BATCH):
|
| 425 |
-
|
| 426 |
-
# chord giα»― lΓ’u hΖ‘n
|
| 427 |
-
chord = chords[
|
| 428 |
-
((bar_count + bar_i) // 2)
|
| 429 |
-
% len(chords)
|
| 430 |
-
]
|
| 431 |
-
|
| 432 |
t = bar_i * bar_dur
|
| 433 |
-
|
| 434 |
for c in chord:
|
| 435 |
-
|
| 436 |
chord_notes.append({
|
| 437 |
-
"note":
|
| 438 |
"velocity": chord_vel,
|
| 439 |
-
"duration": round(bar_dur *
|
| 440 |
-
"type":
|
| 441 |
-
"time":
|
| 442 |
})
|
| 443 |
|
| 444 |
-
# ==========================================
|
| 445 |
-
# SEND
|
| 446 |
-
# ==========================================
|
| 447 |
await websocket.send_json({
|
| 448 |
-
"notes":
|
| 449 |
"batch_duration": round(batch_dur, 4),
|
| 450 |
-
"music_bpm":
|
| 451 |
})
|
| 452 |
|
| 453 |
bar_count += BARS_PER_BATCH
|
| 454 |
-
|
| 455 |
await asyncio.sleep(batch_dur * 0.80)
|
| 456 |
|
| 457 |
except Exception:
|
|
|
|
| 24 |
def __init__(self, vocab_size):
|
| 25 |
super().__init__()
|
| 26 |
self.embedding = nn.Embedding(vocab_size, 256)
|
| 27 |
+
self.lstm = nn.LSTM(256, 512, num_layers=3, batch_first=True, dropout=0.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
self.norm = nn.LayerNorm(512)
|
| 29 |
self.fc = nn.Linear(512, vocab_size)
|
| 30 |
|
|
|
|
| 35 |
out = self.norm(out)
|
| 36 |
return self.fc(out), hidden
|
| 37 |
|
| 38 |
+
with open('mapping_dict.pkl', 'rb') as f:
|
|
|
|
|
|
|
|
|
|
| 39 |
int_to_note = pickle.load(f)
|
|
|
|
| 40 |
note_to_int = {n: i for i, n in int_to_note.items()}
|
| 41 |
vocab_size = len(int_to_note)
|
| 42 |
|
| 43 |
device = torch.device("cpu")
|
|
|
|
| 44 |
model = BioVibeAI(vocab_size=vocab_size)
|
|
|
|
| 45 |
state_dict = torch.load("model.pth", map_location=device)
|
|
|
|
| 46 |
clean_state_dict = {
|
| 47 |
+
k[7:] if k.startswith('module.') else k: v
|
| 48 |
for k, v in state_dict.items()
|
| 49 |
}
|
|
|
|
| 50 |
model.load_state_dict(clean_state_dict)
|
| 51 |
model.eval()
|
| 52 |
|
| 53 |
+
with open('notes_corpus.pkl', 'rb') as f:
|
| 54 |
corpus = pickle.load(f)
|
| 55 |
|
| 56 |
# ==========================================
|
|
|
|
| 59 |
SEQ_LEN = 128
|
| 60 |
BARS_PER_BATCH = 2
|
| 61 |
|
| 62 |
+
# Chord progressions β 2 mΓ u sαΊ―c luΓ’n phiΓͺn Δα» trΓ‘nh lαΊ·p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
CHORD_SETS = {
|
| 64 |
+
"calm": [ # BPM cao β tΓ΄ng mα»m, dreamy
|
| 65 |
+
[48, 52, 55, 59], # Cmaj7
|
| 66 |
+
[45, 48, 52, 55], # Am7
|
| 67 |
+
[41, 45, 48, 52], # Fmaj7
|
| 68 |
+
[43, 47, 50, 55], # G9
|
| 69 |
],
|
| 70 |
+
"groove": [ # BPM thαΊ₯p β tΓ΄ng rΓ΅ hΖ‘n, cΓ³ chiα»u sΓ’u
|
| 71 |
+
[48, 52, 55, 59], # Cmaj7
|
| 72 |
+
[43, 47, 50, 53], # G7
|
| 73 |
+
[45, 48, 52, 57], # Am9
|
| 74 |
+
[41, 44, 48, 52], # Fm7
|
|
|
|
| 75 |
],
|
| 76 |
}
|
| 77 |
|
| 78 |
# ==========================================
|
| 79 |
+
# ENTRAINMENT HELPERS
|
| 80 |
# ==========================================
|
| 81 |
+
|
| 82 |
def get_music_beat(bpm: int) -> float:
|
| 83 |
"""
|
| 84 |
+
Tim ΔαΊp nhanh -> nhac cham lai de keo nhip tim xuong (entrainment).
|
| 85 |
+
BPM [40->180] => music_tempo [85->55] BPM (nghich chieu).
|
| 86 |
"""
|
| 87 |
+
music_tempo = 85.0 - (bpm - 40) * (30.0 / 140.0)
|
| 88 |
+
music_tempo = max(52.0, min(88.0, music_tempo))
|
| 89 |
return 60.0 / music_tempo
|
| 90 |
|
| 91 |
|
| 92 |
def get_temperature(bpm: int) -> float:
|
| 93 |
"""
|
| 94 |
+
BPM cao -> temp THAP -> melody on dinh, du doan duoc, calming.
|
| 95 |
+
BPM thap -> temp CAO -> melody da dang, groovy hon.
|
| 96 |
+
Range: [0.60, 0.92]
|
|
|
|
| 97 |
"""
|
| 98 |
t = (bpm - 40) / (180 - 40)
|
| 99 |
+
return round(0.92 - t * 0.32, 3)
|
| 100 |
|
| 101 |
|
| 102 |
def get_melody_params(bpm: int, music_beat: float) -> dict:
|
| 103 |
+
"""
|
| 104 |
+
BPM cao -> not nhe hon, dai hon (legato), range cao (airy).
|
| 105 |
+
BPM thap -> not vua, ngan hon (rhythmic), range mid.
|
| 106 |
+
"""
|
| 107 |
t = (bpm - 40) / (180 - 40)
|
| 108 |
|
| 109 |
+
vel_low = int(75 - t * 28) # 75 (bpm=40) -> 47 (bpm=180)
|
| 110 |
+
vel_high = int(90 - t * 22) # 90 -> 68
|
| 111 |
|
| 112 |
+
dur_factor = 0.72 + t * 0.48 # 0.72 -> 1.20 (legato khi BPM cao)
|
| 113 |
+
duration = round(music_beat * dur_factor, 4)
|
| 114 |
|
| 115 |
+
midi_low = int(59 + t * 5) # 59 -> 64
|
| 116 |
+
midi_high = int(71 + t * 5) # 71 -> 76
|
|
|
|
|
|
|
| 117 |
|
| 118 |
return {
|
| 119 |
+
"vel_low": max(40, vel_low),
|
| 120 |
+
"vel_high": max(60, vel_high),
|
| 121 |
"duration": duration,
|
| 122 |
"midi_low": midi_low,
|
| 123 |
"midi_high": midi_high,
|
|
|
|
| 125 |
|
| 126 |
|
| 127 |
def get_chord_set(bpm: int, bar_count: int) -> list:
|
| 128 |
+
"""
|
| 129 |
+
BPM cao -> chord calm (Cmaj7/Am7/Fmaj7/G9).
|
| 130 |
+
BPM thap -> chord groove (co Fm7 them mau).
|
| 131 |
+
Doi chord set moi 16 bar.
|
| 132 |
+
"""
|
| 133 |
+
use_groove = (bpm < 85) or (bar_count % 16 >= 8 and bpm < 110)
|
| 134 |
return CHORD_SETS["groove"] if use_groove else CHORD_SETS["calm"]
|
| 135 |
|
| 136 |
|
| 137 |
# ==========================================
|
| 138 |
+
# WEBSOCKET ENDPOINT
|
| 139 |
# ==========================================
|
| 140 |
@app.websocket("/ws/vibe")
|
| 141 |
async def websocket_vibe(websocket: WebSocket):
|
|
|
|
| 142 |
await websocket.accept()
|
| 143 |
|
| 144 |
current_bpm = 75
|
| 145 |
+
is_playing = True
|
| 146 |
|
| 147 |
async def listen():
|
| 148 |
nonlocal current_bpm, is_playing
|
|
|
|
| 149 |
try:
|
| 150 |
while True:
|
| 151 |
data = await websocket.receive_json()
|
|
|
|
| 152 |
bpm = int(data.get("bpm", current_bpm))
|
|
|
|
| 153 |
if 40 <= bpm <= 180:
|
| 154 |
current_bpm = bpm
|
|
|
|
| 155 |
except Exception:
|
| 156 |
is_playing = False
|
| 157 |
|
| 158 |
asyncio.create_task(listen())
|
| 159 |
|
| 160 |
+
# Chon seed uu tien LOFI
|
| 161 |
+
lofi_songs = [s for s in corpus if s and len(s) >= SEQ_LEN and s[0] == "STYLE_LOFI"]
|
| 162 |
+
pool = lofi_songs if lofi_songs else [s for s in corpus if len(s) >= SEQ_LEN]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
song = random.choice(pool)
|
| 164 |
|
| 165 |
seed = list(song[:SEQ_LEN])
|
| 166 |
seed[0] = "STYLE_LOFI"
|
|
|
|
| 167 |
pattern = [note_to_int.get(tok, 0) for tok in seed]
|
| 168 |
|
| 169 |
+
# Warm-up LSTM mot lan duy nhat voi toan bo seed
|
| 170 |
with torch.no_grad():
|
| 171 |
seed_tensor = torch.tensor([pattern], dtype=torch.long)
|
| 172 |
_, hidden = model(seed_tensor)
|
| 173 |
|
| 174 |
bar_count = 0
|
| 175 |
|
|
|
|
|
|
|
|
|
|
| 176 |
try:
|
|
|
|
| 177 |
while is_playing:
|
|
|
|
| 178 |
bpm = current_bpm
|
| 179 |
|
| 180 |
music_beat = get_music_beat(bpm)
|
| 181 |
+
bar_dur = music_beat * 4
|
| 182 |
+
batch_dur = bar_dur * BARS_PER_BATCH
|
| 183 |
+
temp = get_temperature(bpm)
|
| 184 |
+
mel_params = get_melody_params(bpm, music_beat)
|
| 185 |
+
chords = get_chord_set(bpm, bar_count)
|
| 186 |
|
| 187 |
+
# ββ SINH MELODY ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
melody_notes = []
|
|
|
|
| 189 |
current_time = 0.0
|
| 190 |
+
last_midi = None
|
| 191 |
+
MAX_TOKENS = 180
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
for _ in range(MAX_TOKENS):
|
|
|
|
| 194 |
if current_time >= batch_dur:
|
| 195 |
break
|
| 196 |
|
| 197 |
+
new_tok = torch.tensor([[pattern[-1]]], dtype=torch.long)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
with torch.no_grad():
|
| 199 |
logits, hidden = model(new_tok, hidden)
|
| 200 |
+
probs = torch.softmax(logits / temp, dim=1).numpy()[0]
|
| 201 |
+
idx = int(np.random.choice(len(probs), p=probs))
|
| 202 |
|
| 203 |
+
tok = int_to_note[idx]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
pattern = pattern[1:] + [idx]
|
| 205 |
|
|
|
|
|
|
|
|
|
|
| 206 |
if tok.startswith("TIME_"):
|
|
|
|
| 207 |
try:
|
| 208 |
+
gap_beats = float(tok.split("_")[1])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
current_time += gap_beats * music_beat
|
|
|
|
| 210 |
except ValueError:
|
| 211 |
pass
|
| 212 |
|
|
|
|
|
|
|
|
|
|
| 213 |
elif tok.startswith("NOTE_"):
|
|
|
|
| 214 |
try:
|
| 215 |
midi = int(tok.split("_")[1])
|
| 216 |
+
lo, hi = mel_params["midi_low"], mel_params["midi_high"]
|
| 217 |
|
| 218 |
+
while midi < lo: midi += 12
|
| 219 |
+
while midi > hi: midi -= 12
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
|
|
|
| 221 |
if midi == last_midi and random.random() < 0.40:
|
| 222 |
+
step = random.choice([-2, 2, 3, -3, 5])
|
| 223 |
+
midi = max(lo - 2, min(hi + 2, midi + step))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
last_midi = midi
|
| 226 |
+
velocity = random.randint(mel_params["vel_low"],
|
| 227 |
+
mel_params["vel_high"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
melody_notes.append({
|
| 229 |
+
"note": midi,
|
| 230 |
"velocity": velocity,
|
| 231 |
"duration": mel_params["duration"],
|
| 232 |
+
"type": "melody",
|
| 233 |
+
"time": round(current_time, 4),
|
| 234 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
except ValueError:
|
| 236 |
pass
|
| 237 |
|
| 238 |
+
# Fallback pentatonic
|
|
|
|
|
|
|
| 239 |
if len(melody_notes) < 3:
|
|
|
|
| 240 |
scale = [60, 62, 64, 67, 69, 72]
|
|
|
|
| 241 |
t = 0.0
|
|
|
|
| 242 |
while t < batch_dur:
|
|
|
|
| 243 |
melody_notes.append({
|
| 244 |
+
"note": random.choice(scale),
|
| 245 |
+
"velocity": random.randint(mel_params["vel_low"],
|
| 246 |
+
mel_params["vel_high"]),
|
|
|
|
|
|
|
| 247 |
"duration": mel_params["duration"],
|
| 248 |
+
"type": "melody",
|
| 249 |
+
"time": round(t, 4),
|
| 250 |
})
|
| 251 |
+
t += music_beat * random.choice([0.5, 0.5, 1.0])
|
| 252 |
|
| 253 |
+
# ββ CHORD PADS βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 254 |
+
# BPM cao -> pad nhe, am thanh (ambient)
|
| 255 |
+
chord_vel = max(32, int(50 - (bpm - 40) * 0.06))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
chord_notes = []
|
|
|
|
| 258 |
for bar_i in range(BARS_PER_BATCH):
|
| 259 |
+
chord = chords[(bar_count + bar_i) % len(chords)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
t = bar_i * bar_dur
|
|
|
|
| 261 |
for c in chord:
|
|
|
|
| 262 |
chord_notes.append({
|
| 263 |
+
"note": c,
|
| 264 |
"velocity": chord_vel,
|
| 265 |
+
"duration": round(bar_dur * 0.95, 4),
|
| 266 |
+
"type": "chord",
|
| 267 |
+
"time": round(t, 4),
|
| 268 |
})
|
| 269 |
|
|
|
|
|
|
|
|
|
|
| 270 |
await websocket.send_json({
|
| 271 |
+
"notes": melody_notes + chord_notes,
|
| 272 |
"batch_duration": round(batch_dur, 4),
|
| 273 |
+
"music_bpm": round(60.0 / music_beat, 1),
|
| 274 |
})
|
| 275 |
|
| 276 |
bar_count += BARS_PER_BATCH
|
|
|
|
| 277 |
await asyncio.sleep(batch_dur * 0.80)
|
| 278 |
|
| 279 |
except Exception:
|