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

Refactor audio processing and LSTM model for improved sound dynamics and melody generation

Browse files
Files changed (2) hide show
  1. index.html +47 -29
  2. 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: -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",
@@ -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
- 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
  // ================================================================
@@ -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.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
 
@@ -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
- 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');
@@ -977,7 +995,7 @@
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:';
@@ -993,9 +1011,9 @@
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 => {
@@ -1011,7 +1029,9 @@
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;
@@ -1043,24 +1063,22 @@
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>
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("module.") else k: v
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("notes_corpus.pkl", "rb") as f:
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
- # Scale cα»±c kα»³ an toΓ n, nghe chill/anime/lofi
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
- [60, 64, 67, 71], # Cmaj7
85
- [62, 65, 69, 72], # Dm7
86
- [64, 67, 71, 74], # Em7
87
- [65, 69, 72, 76], # Fmaj7
88
  ],
89
-
90
- "groove": [
91
- [60, 64, 67], # C
92
- [67, 71, 74], # G
93
- [69, 72, 76], # Am
94
- [65, 69, 72], # F
95
  ],
96
  }
97
 
98
  # ==========================================
99
- # MUSIC HELPERS
100
  # ==========================================
 
101
  def get_music_beat(bpm: int) -> float:
102
  """
103
- BPM cao -> nhαΊ‘c hΖ‘i nhanh hΖ‘n chΓΊt
104
- nhΖ°ng vαΊ«n giα»― relaxing.
105
  """
106
- music_tempo = 60.0 + (bpm - 40) * (18.0 / 140.0)
107
- music_tempo = max(58.0, min(82.0, music_tempo))
108
  return 60.0 / music_tempo
109
 
110
 
111
  def get_temperature(bpm: int) -> float:
112
  """
113
- Temperature thαΊ₯p hΖ‘n để:
114
- - Γ­t random
115
- - Γ­t uncanny
116
- - α»•n Δ‘α»‹nh hΖ‘n
117
  """
118
  t = (bpm - 40) / (180 - 40)
119
- return round(0.72 - t * 0.18, 3)
120
 
121
 
122
  def get_melody_params(bpm: int, music_beat: float) -> dict:
 
 
 
 
123
  t = (bpm - 40) / (180 - 40)
124
 
125
- vel_low = int(58 - t * 12)
126
- vel_high = int(72 - t * 10)
127
 
128
- dur_factor = 0.55 + t * 0.22
 
129
 
130
- duration = round(music_beat * dur_factor, 4)
131
-
132
- midi_low = 60
133
- midi_high = 79
134
 
135
  return {
136
- "vel_low": max(42, vel_low),
137
- "vel_high": max(56, vel_high),
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
- use_groove = bpm < 82
 
 
 
 
 
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 = True
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
- # SEED
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
- bar_dur = music_beat * 4
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
- last_midi = None
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
- # trΓ‘nh quΓ‘ dΓ y note
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
- probs = torch.softmax(
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 = float(tok.split("_")[1])
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 = mel_params["midi_low"]
301
- hi = mel_params["midi_high"]
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
- step = random.choice([
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
- velocity = random.randint(
346
- mel_params["vel_low"],
347
- mel_params["vel_high"]
348
- )
349
-
350
  melody_notes.append({
351
- "note": midi,
352
  "velocity": velocity,
353
  "duration": mel_params["duration"],
354
- "type": "melody",
355
- "time": round(current_time, 4),
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": random.choice(scale),
383
- "velocity": random.randint(
384
- mel_params["vel_low"],
385
- mel_params["vel_high"]
386
- ),
387
  "duration": mel_params["duration"],
388
- "type": "melody",
389
- "time": round(t, 4),
390
  })
 
391
 
392
- t += music_beat * random.choice([
393
- 0.5,
394
- 1.0
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": c,
438
  "velocity": chord_vel,
439
- "duration": round(bar_dur * 1.8, 4),
440
- "type": "chord",
441
- "time": round(t, 4),
442
  })
443
 
444
- # ==========================================
445
- # SEND
446
- # ==========================================
447
  await websocket.send_json({
448
- "notes": melody_notes + chord_notes,
449
  "batch_duration": round(batch_dur, 4),
450
- "music_bpm": round(60.0 / music_beat, 1),
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: