make preview songs stream directly from the file

This commit is contained in:
Yonokid
2025-08-27 11:29:03 -04:00
parent 7c97ebb310
commit d5caf735fa
2 changed files with 143 additions and 54 deletions

View File

@@ -220,13 +220,20 @@ class Music:
self.preview = preview # Preview start time in seconds self.preview = preview # Preview start time in seconds
self.is_preview_mode = preview is not None self.is_preview_mode = preview is not None
# Add preview-specific attributes
self.preview_start_frame = 0
self.preview_end_frame = 0
self.uses_file_streaming = False # Flag to indicate streaming vs memory loading
self.file_buffer_size = int(target_sample_rate * 5) # 5 seconds buffer self.file_buffer_size = int(target_sample_rate * 5) # 5 seconds buffer
self.buffer = None self.buffer = None
self.buffer_position = 0 self.buffer_position = 0
self.buffer_original_frames = 0 # Track original frames for resampling
# Thread-safe updates # Thread-safe updates
self.lock = Lock() self.lock = Lock()
self.sound_file = None self.sound_file = None
if self.file_path.exists(): if self.file_path.exists():
self.load_from_file() self.load_from_file()
else: else:
@@ -264,6 +271,7 @@ class Music:
self.total_frames = self.data.shape[0] self.total_frames = self.data.shape[0]
self.sample_width = 4 # float32 self.sample_width = 4 # float32
self.uses_file_streaming = False
self._fill_buffer() self._fill_buffer()
self.valid = True self.valid = True
print(f"Music loaded from memory: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames") print(f"Music loaded from memory: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames")
@@ -279,42 +287,51 @@ class Music:
# Get file properties # Get file properties
self.channels = self.sound_file.channels self.channels = self.sound_file.channels
self.sample_width = 2 if self.sound_file.subtype in ['PCM_16', 'VORBIS'] else 4 # Most common self.sample_width = 2 if self.sound_file.subtype in ['PCM_16', 'VORBIS'] else 4
self.sample_rate = self.sound_file.samplerate self.sample_rate = self.sound_file.samplerate
original_total_frames = self.sound_file.frames original_total_frames = self.sound_file.frames
if self.is_preview_mode: if self.is_preview_mode:
# Calculate preview start and end frames # Calculate preview boundaries but don't load data into memory
preview_start_frame = int(self.preview * self.sample_rate) self.preview_start_frame = int(self.preview * self.sample_rate)
preview_duration_frames = original_total_frames - preview_start_frame
preview_end_frame = min(preview_start_frame + preview_duration_frames, original_total_frames) # For preview, we'll calculate the available frames from the start position
available_frames = original_total_frames - self.preview_start_frame
self.preview_end_frame = min(self.preview_start_frame + available_frames, original_total_frames)
# Ensure preview start is within bounds # Ensure preview start is within bounds
if preview_start_frame >= original_total_frames: if self.preview_start_frame >= original_total_frames:
preview_start_frame = max(0, original_total_frames - preview_duration_frames) self.preview_start_frame = max(0, original_total_frames - available_frames)
preview_end_frame = original_total_frames self.preview_end_frame = original_total_frames
# Set total frames to the preview length
self.total_frames = self.preview_end_frame - self.preview_start_frame
# Seek to preview start position # Seek to preview start position
self.sound_file.seek(preview_start_frame) self.sound_file.seek(self.preview_start_frame)
# Read only the preview segment # Enable file streaming mode (don't load into memory)
frames_to_read = preview_end_frame - preview_start_frame self.uses_file_streaming = True
self.data = self.sound_file.read(frames_to_read) self.data = None # Don't store full data in memory
# Update total frames to reflect the preview segment print(f"Preview mode: Streaming {self.total_frames} frames ({self.total_frames/self.sample_rate:.2f}s) starting at {self.preview:.2f}s")
self.total_frames = len(self.data) if self.data.ndim == 1 else self.data.shape[0]
print(f"Preview mode: Loading {frames_to_read} frames ({frames_to_read/self.sample_rate:.2f}s) starting at {self.preview:.2f}s")
else: else:
# Load entire file # For non-preview mode, load entire file into memory as before
self.data = self.sound_file.read() self.data = self.sound_file.read()
self.total_frames = original_total_frames self.total_frames = original_total_frames
self.uses_file_streaming = False
# Process the loaded data
self.load_from_memory() self.load_from_memory()
return # Early return to avoid duplicate processing
# Initialize buffer for streaming mode
self._fill_buffer()
self.valid = True self.valid = True
if self.is_preview_mode: if self.is_preview_mode:
print(f"Music preview loaded: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames ({self.get_time_length():.2f}s)") print(f"Music preview streaming: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames ({self.get_time_length():.2f}s)")
else: else:
print(f"Music loaded: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames") print(f"Music loaded: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames")
@@ -326,12 +343,22 @@ class Music:
self.valid = False self.valid = False
def _fill_buffer(self) -> bool: def _fill_buffer(self) -> bool:
"""Fill buffer from in-memory data""" """Fill buffer from either memory or file stream"""
try: try:
if self.uses_file_streaming:
return self._fill_buffer_from_file()
else:
return self._fill_buffer_from_memory()
except Exception as e:
print(f"Error filling buffer: {e}")
return False
def _fill_buffer_from_memory(self) -> bool:
"""Fill buffer from in-memory data"""
if self.data is None: if self.data is None:
return False return False
start_frame = self.position + self.buffer_position start_frame = self.position
end_frame = min(start_frame + self.file_buffer_size, self.total_frames) end_frame = min(start_frame + self.file_buffer_size, self.total_frames)
if start_frame >= self.total_frames: if start_frame >= self.total_frames:
@@ -341,14 +368,69 @@ class Music:
data_chunk = self.data[start_frame:end_frame] data_chunk = self.data[start_frame:end_frame]
self.buffer = data_chunk self.buffer = data_chunk
self.position += self.buffer_position
self.buffer_position = 0 self.buffer_position = 0
# For memory mode, original frames equals buffer frames since no resampling happens here
self.buffer_original_frames = len(data_chunk) if data_chunk.ndim == 1 else data_chunk.shape[0]
return True return True
except Exception as e: def _fill_buffer_from_file(self) -> bool:
print(f"Error filling buffer from memory: {e}") """Fill buffer by streaming from file"""
if not self.sound_file:
return False return False
# Calculate current absolute position in file (in original sample rate)
current_absolute_position = self.preview_start_frame + self.position
# Check if we've reached the end of the preview
if current_absolute_position >= self.preview_end_frame:
return False
# Calculate how many frames to read (respecting preview boundaries)
# This is in the original sample rate
frames_to_read = min(self.file_buffer_size, self.preview_end_frame - current_absolute_position)
if frames_to_read <= 0:
return False
# Seek to the correct position
self.sound_file.seek(current_absolute_position)
# Read the data chunk
data_chunk = self.sound_file.read(frames_to_read)
if len(data_chunk) == 0:
return False
# Store the original length before any processing
original_frames = len(data_chunk) if data_chunk.ndim == 1 else data_chunk.shape[0]
# Apply resampling if needed
if self.sample_rate != self.target_sample_rate:
data_chunk = resample(data_chunk, self.sample_rate, self.target_sample_rate)
# Apply normalization if needed
if self.normalize is not None:
current_rms = get_average_volume_rms(data_chunk)
if current_rms > 0:
target_rms = self.normalize
rms_scale_factor = target_rms / current_rms
data_chunk *= rms_scale_factor
# Ensure proper shape
if data_chunk.ndim == 1 and self.channels > 1:
data_chunk = data_chunk.reshape(-1, self.channels)
elif data_chunk.ndim == 2 and self.channels == 1:
data_chunk = data_chunk.flatten().reshape(-1, 1)
self.buffer = data_chunk
self.buffer_position = 0
# Store how many original frames this buffer represents
# This is crucial for position tracking when resampling occurs
self.buffer_original_frames = original_frames
return True
def update(self) -> None: def update(self) -> None:
"""Update music stream buffers""" """Update music stream buffers"""
if not self.is_playing or self.is_paused: if not self.is_playing or self.is_paused:
@@ -359,6 +441,14 @@ class Music:
if self.buffer is None: if self.buffer is None:
return return
if self.buffer_position >= len(self.buffer): if self.buffer_position >= len(self.buffer):
# Update position based on original frames consumed
if self.uses_file_streaming:
# For file streaming, we need to track position in original sample rate
self.position += self.buffer_original_frames
else:
# For memory mode, position is already in target sample rate
self.position += self.buffer_position
self.buffer_position = 0
self.is_playing = self._fill_buffer() self.is_playing = self._fill_buffer()
def play(self) -> None: def play(self) -> None:
@@ -368,10 +458,8 @@ class Music:
if self.position >= self.total_frames: if self.position >= self.total_frames:
self.position = 0 self.position = 0
self.buffer_position = 0 self.buffer_position = 0
if self.sound_file: if self.sound_file and self.uses_file_streaming:
# For preview mode, seek to the preview start position self.sound_file.seek(self.preview_start_frame)
seek_pos = int(self.preview * self.sample_rate) if self.preview is not None else 0
self.sound_file.seek(seek_pos)
self._fill_buffer() self._fill_buffer()
self.is_playing = True self.is_playing = True
@@ -384,10 +472,8 @@ class Music:
self.is_paused = False self.is_paused = False
self.position = 0 self.position = 0
self.buffer_position = 0 self.buffer_position = 0
if self.sound_file: if self.sound_file and self.uses_file_streaming:
# For preview mode, seek to the preview start position self.sound_file.seek(self.preview_start_frame)
seek_pos = int(self.preview * self.sample_rate) if self.preview is not None else 0
self.sound_file.seek(seek_pos)
self._fill_buffer() self._fill_buffer()
def pause(self) -> None: def pause(self) -> None:
@@ -413,16 +499,10 @@ class Music:
# Clamp position to valid range # Clamp position to valid range
frame_position = max(0, min(frame_position, self.total_frames - 1)) frame_position = max(0, min(frame_position, self.total_frames - 1))
# Update file position if streaming from file
if self.sound_file:
# For preview mode, add the preview offset
actual_file_position = frame_position
if self.is_preview_mode and self.preview is not None:
actual_file_position += int(self.preview * self.sample_rate)
self.sound_file.seek(actual_file_position)
self.position = frame_position self.position = frame_position
self.buffer_position = 0 self.buffer_position = 0
# For file streaming, we'll let _fill_buffer handle the seeking
self._fill_buffer() self._fill_buffer()
def get_time_length(self) -> float: def get_time_length(self) -> float:
@@ -455,6 +535,15 @@ class Music:
# Check if we need more data # Check if we need more data
if self.buffer_position >= len(self.buffer): if self.buffer_position >= len(self.buffer):
# Update position based on what we actually consumed from the original file
if self.uses_file_streaming:
# For file streaming, advance by original frames (before resampling)
self.position += self.buffer_original_frames
else:
# For memory mode, advance by resampled frames
self.position += self.buffer_position
self.buffer_position = 0
# Try to fill buffer again # Try to fill buffer again
if not self._fill_buffer(): if not self._fill_buffer():
self.is_playing = False self.is_playing = False

View File

@@ -404,7 +404,7 @@ class SongSelectScreen:
if self.demo_song is None and get_current_ms() >= song.box.wait + (83.33*3): if self.demo_song is None and get_current_ms() >= song.box.wait + (83.33*3):
song.box.get_scores() song.box.get_scores()
if song.tja.metadata.wave.exists() and song.tja.metadata.wave.is_file(): if song.tja.metadata.wave.exists() and song.tja.metadata.wave.is_file():
self.demo_song = audio.load_music_stream(song.tja.metadata.wave, preview=song.tja.metadata.demostart, normalize=0.1935) self.demo_song = audio.load_music_stream(song.tja.metadata.wave, preview=song.tja.metadata.demostart)
audio.play_music_stream(self.demo_song) audio.play_music_stream(self.demo_song)
audio.stop_sound(self.sound_bgm) audio.stop_sound(self.sound_bgm)
if song.box.is_open: if song.box.is_open: