Mina Emadi commited on
Commit
35fde27
·
1 Parent(s): 6643ac9

some minor UI changes: adding the playback functionality for the chosen section with no key and bpm change and replacing the upload new with choose a different song

Browse files
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
backend/.DS_Store CHANGED
Binary files a/backend/.DS_Store and b/backend/.DS_Store differ
 
backend/models/__pycache__/session.cpython-310.pyc CHANGED
Binary files a/backend/models/__pycache__/session.cpython-310.pyc and b/backend/models/__pycache__/session.cpython-310.pyc differ
 
backend/presets/sample-track/MIDI.mid ADDED
Binary file (24.1 kB). View file
 
backend/presets/sample-track/bass.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34d0bf1999390c1846ec635d7d13f719f12235dd0c3b43aff37c978defd20166
3
+ size 70893820
backend/presets/sample-track/click_record.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:436b475a472014349fe08f7acee5916bf17d1ff475471b6f04b45e1cf3f67233
3
+ size 70893820
backend/presets/sample-track/drums.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:536d1b91df21e93ca9f1f1352da5b70f64e6b0b8ac884e7ef794122f63183cfd
3
+ size 70893820
backend/presets/sample-track/guitar.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ed112603fd68bed8a61c290b9ca36fcecee998b085eb283a21e127df0b2bc9e
3
+ size 70893820
backend/presets/sample-track/synth.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba8a34d85b65a69e09ba0a319136bba8c9d92a6e0fa10e9dc3770d6e26d25a49
3
+ size 70893820
backend/routers/__pycache__/detection.cpython-310.pyc CHANGED
Binary files a/backend/routers/__pycache__/detection.cpython-310.pyc and b/backend/routers/__pycache__/detection.cpython-310.pyc differ
 
backend/services/__pycache__/midi_analyzer.cpython-310.pyc CHANGED
Binary files a/backend/services/__pycache__/midi_analyzer.cpython-310.pyc and b/backend/services/__pycache__/midi_analyzer.cpython-310.pyc differ
 
convert_to_ogg.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Convert a single preset's WAV stems to OGG Vorbis at quality 9.
3
+ Verifies sample count matches original WAV after conversion.
4
+ Usage: python convert_to_ogg.py <preset_folder_name>
5
+ """
6
+
7
+ import subprocess
8
+ import sys
9
+ import soundfile as sf
10
+ from pathlib import Path
11
+
12
+ PRESETS_DIR = Path(__file__).parent / "backend" / "presets"
13
+
14
+ preset_name = sys.argv[1] if len(sys.argv) > 1 else "274-SUNSPILL"
15
+ preset_dir = PRESETS_DIR / preset_name
16
+
17
+ wav_files = sorted(preset_dir.glob("*.wav"))
18
+ if not wav_files:
19
+ print(f"No WAV files found in {preset_dir}")
20
+ sys.exit(1)
21
+
22
+ print(f"Converting {preset_name} ({len(wav_files)} stems) at OGG quality 9\n")
23
+
24
+ all_ok = True
25
+ for wav_path in wav_files:
26
+ ogg_path = wav_path.with_suffix(".ogg")
27
+
28
+ # Get original sample count
29
+ info = sf.info(str(wav_path))
30
+ original_frames = info.frames
31
+ sr = info.samplerate
32
+
33
+ # Convert: pipe exact sample count via atrim so encoder sees correct length
34
+ result = subprocess.run(
35
+ [
36
+ "ffmpeg", "-y", "-i", str(wav_path),
37
+ "-af", f"atrim=end_sample={original_frames}",
38
+ "-c:a", "vorbis", "-strict", "experimental",
39
+ "-q:a", "9",
40
+ str(ogg_path)
41
+ ],
42
+ capture_output=True
43
+ )
44
+
45
+ if result.returncode != 0:
46
+ print(f" ERROR converting {wav_path.name}")
47
+ print(result.stderr.decode()[-300:])
48
+ all_ok = False
49
+ continue
50
+
51
+ # Verify decoded length
52
+ ogg_info = sf.info(str(ogg_path))
53
+ decoded_frames = ogg_info.frames
54
+ delta = decoded_frames - original_frames
55
+
56
+ wav_mb = wav_path.stat().st_size / 1024 / 1024
57
+ ogg_mb = ogg_path.stat().st_size / 1024 / 1024
58
+ length_ok = "✓" if delta == 0 else f"✗ delta={delta:+d} samples ({delta/sr*1000:.1f}ms)"
59
+
60
+ print(f" {wav_path.name}")
61
+ print(f" Size: {wav_mb:.1f} MB → {ogg_mb:.1f} MB ({ogg_mb/wav_mb*100:.0f}%)")
62
+ print(f" Length: {original_frames} → {decoded_frames} samples {length_ok}")
63
+
64
+ if all_ok:
65
+ print("\nAll conversions successful. WAV files kept for now — delete manually after listening test.")
66
+ else:
67
+ print("\nSome conversions failed.")
frontend/src/App.jsx CHANGED
@@ -51,6 +51,8 @@ function App() {
51
  setPan,
52
  resetVolumes,
53
  setLoop,
 
 
54
  clearBufferCache
55
  } = useAudioEngine()
56
 
@@ -84,6 +86,24 @@ function App() {
84
  await loadPreset(presetName)
85
  }, [loadPreset])
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  const handleProcess = useCallback(async (semitones, targetBpm) => {
88
  setIsProcessing(true)
89
  resetProgress()
@@ -100,6 +120,7 @@ function App() {
100
  // Clear stale region cache (new processing produced new audio)
101
  clearBufferCache('region')
102
  await loadStems(sessionId, stems, { region: true })
 
103
  setPlaybackMode('region')
104
  setLoop(true)
105
  } else {
@@ -113,7 +134,7 @@ function App() {
113
  } finally {
114
  setIsProcessing(false)
115
  }
116
- }, [process, sessionId, stems, loadStems, resetProgress, hasRegion, regionStart, regionEnd, setLoop, clearBufferCache])
117
 
118
  const handleGenerate = useCallback(async (prompt) => {
119
  if (!hasRegion) return
@@ -134,6 +155,7 @@ function App() {
134
  const handlePlayFullSong = useCallback(async () => {
135
  // Stop current playback
136
  stop()
 
137
  setLoop(false)
138
  setPlaybackMode('full')
139
 
@@ -142,7 +164,7 @@ function App() {
142
  // Setting processed=true will get processed if available, else original
143
  await loadStems(sessionId, stems)
144
  }
145
- }, [stop, setLoop, sessionId, stems, loadStems])
146
 
147
  const handleStemsReady = useCallback(async () => {
148
  if (sessionId && stems.length > 0) {
@@ -210,7 +232,7 @@ function App() {
210
  onClick={() => window.location.reload()}
211
  className="text-sm text-gray-400 hover:text-white transition-colors"
212
  >
213
- Upload New
214
  </button>
215
  </header>
216
 
@@ -246,7 +268,9 @@ function App() {
246
  isPlaying={isPlaying}
247
  currentTime={currentTime}
248
  duration={duration}
249
- onPlay={play}
 
 
250
  onPause={pause}
251
  onStop={stop}
252
  onSeek={seek}
@@ -255,10 +279,16 @@ function App() {
255
  onRegionChange={(start, end) => {
256
  setRegionStart(start)
257
  setRegionEnd(end)
 
 
 
 
258
  }}
259
  onClearRegion={() => {
260
  setRegionStart(null)
261
  setRegionEnd(null)
 
 
262
  if (playbackMode === 'region') {
263
  handlePlayFullSong()
264
  }
 
51
  setPan,
52
  resetVolumes,
53
  setLoop,
54
+ setRawRegion,
55
+ isRawRegionActive,
56
  clearBufferCache
57
  } = useAudioEngine()
58
 
 
86
  await loadPreset(presetName)
87
  }, [loadPreset])
88
 
89
+ // Main play button: always plays full song — clears any active raw-region loop first
90
+ const handlePlay = useCallback(() => {
91
+ if (isRawRegionActive) {
92
+ setRawRegion(null)
93
+ setLoop(false)
94
+ }
95
+ play()
96
+ }, [isRawRegionActive, setRawRegion, setLoop, play])
97
+
98
+ // Play Section: loops the selected region using full-song buffers (no processing needed)
99
+ const handlePlaySection = useCallback(() => {
100
+ if (!hasRegion) return
101
+ stop()
102
+ setRawRegion(regionStart, regionEnd)
103
+ setLoop(true)
104
+ play()
105
+ }, [hasRegion, stop, setRawRegion, regionStart, regionEnd, setLoop, play])
106
+
107
  const handleProcess = useCallback(async (semitones, targetBpm) => {
108
  setIsProcessing(true)
109
  resetProgress()
 
120
  // Clear stale region cache (new processing produced new audio)
121
  clearBufferCache('region')
122
  await loadStems(sessionId, stems, { region: true })
123
+ setRawRegion(null) // processed region takes over; raw region no longer needed
124
  setPlaybackMode('region')
125
  setLoop(true)
126
  } else {
 
134
  } finally {
135
  setIsProcessing(false)
136
  }
137
+ }, [process, sessionId, stems, loadStems, resetProgress, hasRegion, regionStart, regionEnd, setRawRegion, setLoop, clearBufferCache])
138
 
139
  const handleGenerate = useCallback(async (prompt) => {
140
  if (!hasRegion) return
 
155
  const handlePlayFullSong = useCallback(async () => {
156
  // Stop current playback
157
  stop()
158
+ setRawRegion(null)
159
  setLoop(false)
160
  setPlaybackMode('full')
161
 
 
164
  // Setting processed=true will get processed if available, else original
165
  await loadStems(sessionId, stems)
166
  }
167
+ }, [stop, setRawRegion, setLoop, sessionId, stems, loadStems])
168
 
169
  const handleStemsReady = useCallback(async () => {
170
  if (sessionId && stems.length > 0) {
 
232
  onClick={() => window.location.reload()}
233
  className="text-sm text-gray-400 hover:text-white transition-colors"
234
  >
235
+ Choose a different song
236
  </button>
237
  </header>
238
 
 
268
  isPlaying={isPlaying}
269
  currentTime={currentTime}
270
  duration={duration}
271
+ onPlay={handlePlay}
272
+ onPlaySection={handlePlaySection}
273
+ isRawRegionActive={isRawRegionActive}
274
  onPause={pause}
275
  onStop={stop}
276
  onSeek={seek}
 
279
  onRegionChange={(start, end) => {
280
  setRegionStart(start)
281
  setRegionEnd(end)
282
+ if (playbackMode !== 'region') {
283
+ setRawRegion(start, end)
284
+ setLoop(true)
285
+ }
286
  }}
287
  onClearRegion={() => {
288
  setRegionStart(null)
289
  setRegionEnd(null)
290
+ setRawRegion(null)
291
+ setLoop(false)
292
  if (playbackMode === 'region') {
293
  handlePlayFullSong()
294
  }
frontend/src/components/TransportBar.jsx CHANGED
@@ -42,6 +42,8 @@ function TransportBar({
42
  onClearRegion,
43
  playbackMode,
44
  onPlayFullSong,
 
 
45
  fullSongDuration
46
  }) {
47
  // In region mode, map the playhead position into the region's span on the full bar
@@ -264,6 +266,7 @@ function TransportBar({
264
  </svg>
265
  </button>
266
 
 
267
  {/* Progress bar with region selection */}
268
  <div className="flex-1 flex items-center gap-3">
269
  <span className="text-sm text-gray-400 w-12 text-right">
@@ -379,6 +382,19 @@ function TransportBar({
379
  </span>
380
  </div>
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  {/* Clear region button */}
383
  <button
384
  onClick={onClearRegion}
@@ -387,8 +403,8 @@ function TransportBar({
387
  Clear Selection
388
  </button>
389
 
390
- {/* Looping indicator + Play Full Song button (when in region mode) */}
391
- {playbackMode === 'region' && (
392
  <>
393
  <span className="text-xs text-yellow-400 flex items-center gap-1">
394
  <svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -397,7 +413,7 @@ function TransportBar({
397
  <path d="M7 22l-4-4 4-4" />
398
  <path d="M21 13v2a4 4 0 01-4 4H3" />
399
  </svg>
400
- Looping region
401
  </span>
402
 
403
  <button
 
42
  onClearRegion,
43
  playbackMode,
44
  onPlayFullSong,
45
+ onPlaySection,
46
+ isRawRegionActive,
47
  fullSongDuration
48
  }) {
49
  // In region mode, map the playhead position into the region's span on the full bar
 
266
  </svg>
267
  </button>
268
 
269
+
270
  {/* Progress bar with region selection */}
271
  <div className="flex-1 flex items-center gap-3">
272
  <span className="text-sm text-gray-400 w-12 text-right">
 
382
  </span>
383
  </div>
384
 
385
+ {/* Play Section button */}
386
+ {playbackMode !== 'region' && (
387
+ <button
388
+ onClick={onPlaySection}
389
+ className="flex items-center gap-1.5 px-3 py-1 rounded-lg bg-yellow-500/20 hover:bg-yellow-500/30 border border-yellow-500/40 text-yellow-300 text-xs font-medium transition-colors"
390
+ >
391
+ <svg className="w-3 h-3 fill-current" viewBox="0 0 24 24">
392
+ <polygon points="5,3 19,12 5,21" />
393
+ </svg>
394
+ Play Section
395
+ </button>
396
+ )}
397
+
398
  {/* Clear region button */}
399
  <button
400
  onClick={onClearRegion}
 
403
  Clear Selection
404
  </button>
405
 
406
+ {/* Looping indicator + Play Full Song button — shown when section is looping */}
407
+ {(playbackMode === 'region' || isRawRegionActive) && (
408
  <>
409
  <span className="text-xs text-yellow-400 flex items-center gap-1">
410
  <svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
 
413
  <path d="M7 22l-4-4 4-4" />
414
  <path d="M21 13v2a4 4 0 01-4 4H3" />
415
  </svg>
416
+ Looping section
417
  </span>
418
 
419
  <button
frontend/src/hooks/useAudioEngine.js CHANGED
@@ -40,6 +40,8 @@ export function useAudioEngine() {
40
  const animationRef = useRef(null)
41
  const lastTimeUpdateRef = useRef(0) // Last time we updated currentTime state (for throttling)
42
  const loopRef = useRef(false) // Whether to loop playback
 
 
43
  // Persistent AudioBuffer cache: keys like "drums_full", "drums_region" → AudioBuffer
44
  // Survives across loadStems calls so we skip fetch+decode on replay
45
  const bufferCacheRef = useRef({})
@@ -385,8 +387,10 @@ export function useAudioEngine() {
385
  // Update current time - throttled to every 100ms for performance
386
  if (audioContextRef.current && isPlayingRef.current) {
387
  const elapsed = audioContextRef.current.currentTime - startTimeRef.current + pauseTimeRef.current
388
- const dur = durationRef.current
389
- const newTime = Math.min(elapsed, dur)
 
 
390
 
391
  // Only update state every 100ms (10fps) to reduce re-renders
392
  const timeSinceLastUpdate = timestamp - lastTimeUpdateRef.current
@@ -396,17 +400,20 @@ export function useAudioEngine() {
396
  }
397
 
398
  // Check if playback ended
399
- if (elapsed >= dur && dur > 0) {
400
  if (loopRef.current) {
401
- // Loop: restart from beginning
402
  Object.values(sourcesRef.current).forEach(source => {
403
  try { source.stop() } catch (e) {}
404
  })
405
  sourcesRef.current = {}
406
- pauseTimeRef.current = 0
407
- setCurrentTime(0)
408
 
409
- // Recreate sources and start from 0
 
 
 
 
 
410
  const ctx = audioContextRef.current
411
  Object.entries(buffersRef.current).forEach(([stem, buffer]) => {
412
  if (!buffer || !gainsRef.current[stem]) return
@@ -414,7 +421,7 @@ export function useAudioEngine() {
414
  source.buffer = buffer
415
  source.connect(gainsRef.current[stem])
416
  sourcesRef.current[stem] = source
417
- source.start(0, 0)
418
  })
419
  startTimeRef.current = ctx.currentTime
420
  } else {
@@ -509,6 +516,7 @@ export function useAudioEngine() {
509
  sourcesRef.current = {}
510
 
511
  // Create new source nodes for each stem (AudioBufferSourceNode is one-shot)
 
512
  Object.entries(buffersRef.current).forEach(([stem, buffer]) => {
513
  if (!buffer || !gainsRef.current[stem]) return
514
 
@@ -517,8 +525,13 @@ export function useAudioEngine() {
517
  source.connect(gainsRef.current[stem])
518
  sourcesRef.current[stem] = source
519
 
520
- // Start from paused position: source.start(when, offset)
521
- source.start(0, pauseTimeRef.current)
 
 
 
 
 
522
  })
523
 
524
  startTimeRef.current = ctx.currentTime
@@ -611,6 +624,23 @@ export function useAudioEngine() {
611
  loopRef.current = enabled
612
  }, [])
613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  // Clear cached AudioBuffers for a specific tag ('full', 'region', or all)
615
  const clearBufferCache = useCallback((tag = null) => {
616
  if (tag) {
@@ -642,6 +672,7 @@ export function useAudioEngine() {
642
  // State
643
  isPlaying,
644
  isLoaded,
 
645
  currentTime,
646
  duration,
647
  volumes,
@@ -665,6 +696,7 @@ export function useAudioEngine() {
665
  setPan,
666
  resetVolumes,
667
  setLoop,
 
668
  clearBufferCache,
669
  getAnalyserData
670
  }
 
40
  const animationRef = useRef(null)
41
  const lastTimeUpdateRef = useRef(0) // Last time we updated currentTime state (for throttling)
42
  const loopRef = useRef(false) // Whether to loop playback
43
+ const rawRegionRef = useRef(null) // { start, end } in seconds for raw-region loop (no processing)
44
+ const [isRawRegionActive, setIsRawRegionActive] = useState(false)
45
  // Persistent AudioBuffer cache: keys like "drums_full", "drums_region" → AudioBuffer
46
  // Survives across loadStems calls so we skip fetch+decode on replay
47
  const bufferCacheRef = useRef({})
 
387
  // Update current time - throttled to every 100ms for performance
388
  if (audioContextRef.current && isPlayingRef.current) {
389
  const elapsed = audioContextRef.current.currentTime - startTimeRef.current + pauseTimeRef.current
390
+ const rawRegion = rawRegionRef.current
391
+ // For raw region loops, treat region end as the effective song end
392
+ const effectiveDur = rawRegion ? rawRegion.end : durationRef.current
393
+ const newTime = Math.min(elapsed, effectiveDur)
394
 
395
  // Only update state every 100ms (10fps) to reduce re-renders
396
  const timeSinceLastUpdate = timestamp - lastTimeUpdateRef.current
 
400
  }
401
 
402
  // Check if playback ended
403
+ if (elapsed >= effectiveDur && effectiveDur > 0) {
404
  if (loopRef.current) {
405
+ // Loop: restart from region start (or song beginning)
406
  Object.values(sourcesRef.current).forEach(source => {
407
  try { source.stop() } catch (e) {}
408
  })
409
  sourcesRef.current = {}
 
 
410
 
411
+ const loopStart = rawRegion ? rawRegion.start : 0
412
+ const loopDur = rawRegion ? (rawRegion.end - rawRegion.start) : undefined
413
+
414
+ pauseTimeRef.current = loopStart
415
+ setCurrentTime(loopStart)
416
+
417
  const ctx = audioContextRef.current
418
  Object.entries(buffersRef.current).forEach(([stem, buffer]) => {
419
  if (!buffer || !gainsRef.current[stem]) return
 
421
  source.buffer = buffer
422
  source.connect(gainsRef.current[stem])
423
  sourcesRef.current[stem] = source
424
+ source.start(0, loopStart, loopDur)
425
  })
426
  startTimeRef.current = ctx.currentTime
427
  } else {
 
516
  sourcesRef.current = {}
517
 
518
  // Create new source nodes for each stem (AudioBufferSourceNode is one-shot)
519
+ const rawRegion = rawRegionRef.current
520
  Object.entries(buffersRef.current).forEach(([stem, buffer]) => {
521
  if (!buffer || !gainsRef.current[stem]) return
522
 
 
525
  source.connect(gainsRef.current[stem])
526
  sourcesRef.current[stem] = source
527
 
528
+ if (rawRegion) {
529
+ // Confine playback to the selected region
530
+ const remaining = Math.max(0, rawRegion.end - pauseTimeRef.current)
531
+ source.start(0, pauseTimeRef.current, remaining)
532
+ } else {
533
+ source.start(0, pauseTimeRef.current)
534
+ }
535
  })
536
 
537
  startTimeRef.current = ctx.currentTime
 
624
  loopRef.current = enabled
625
  }, [])
626
 
627
+ // Set raw region for looping a selection without processing.
628
+ // When active, play() confines audio to [start, end] and loops there.
629
+ const setRawRegion = useCallback((start, end) => {
630
+ if (start !== null && end !== null) {
631
+ rawRegionRef.current = { start, end }
632
+ setIsRawRegionActive(true)
633
+ // Snap playhead to region start if it's currently outside the region
634
+ if (pauseTimeRef.current < start || pauseTimeRef.current >= end) {
635
+ pauseTimeRef.current = start
636
+ setCurrentTime(start)
637
+ }
638
+ } else {
639
+ rawRegionRef.current = null
640
+ setIsRawRegionActive(false)
641
+ }
642
+ }, [])
643
+
644
  // Clear cached AudioBuffers for a specific tag ('full', 'region', or all)
645
  const clearBufferCache = useCallback((tag = null) => {
646
  if (tag) {
 
672
  // State
673
  isPlaying,
674
  isLoaded,
675
+ isRawRegionActive,
676
  currentTime,
677
  duration,
678
  volumes,
 
696
  setPan,
697
  resetVolumes,
698
  setLoop,
699
+ setRawRegion,
700
  clearBufferCache,
701
  getAnalyserData
702
  }