improving

This commit is contained in:
Yonokid
2025-06-24 11:48:21 -04:00
parent c1081d255b
commit 9c8a51118e
12 changed files with 373 additions and 194 deletions

View File

@@ -4,7 +4,7 @@ from libs.utils import get_current_ms
class BaseAnimation():
def __init__(self, duration: float, delay: float = 0.0):
def __init__(self, duration: float, delay: float = 0.0) -> None:
"""
Initialize a base animation.
@@ -57,7 +57,7 @@ class FadeAnimation(BaseAnimation):
def __init__(self, duration: float, initial_opacity: float = 1.0,
final_opacity: float = 0.0, delay: float = 0.0,
ease_in: Optional[str] = None, ease_out: Optional[str] = None,
reverse_delay: Optional[float] = None):
reverse_delay: Optional[float] = None) -> None:
super().__init__(duration, delay)
self.initial_opacity = initial_opacity
self.final_opacity = final_opacity
@@ -68,13 +68,13 @@ class FadeAnimation(BaseAnimation):
self.reverse_delay = reverse_delay
self.reverse_delay_saved = reverse_delay
def restart(self):
def restart(self) -> None:
super().restart()
self.reverse_delay = self.reverse_delay_saved
self.initial_opacity = self.initial_opacity_saved
self.final_opacity = self.final_opacity_saved
def update(self, current_time_ms: float):
def update(self, current_time_ms: float) -> None:
elapsed_time = current_time_ms - self.start_ms
if elapsed_time <= self.delay:
@@ -100,7 +100,7 @@ class MoveAnimation(BaseAnimation):
def __init__(self, duration: float, total_distance: int = 0,
start_position: int = 0, delay: float = 0.0,
reverse_delay: Optional[float] = None,
ease_in: Optional[str] = None, ease_out: Optional[str] = None):
ease_in: Optional[str] = None, ease_out: Optional[str] = None) -> None:
super().__init__(duration, delay)
self.reverse_delay = reverse_delay
self.reverse_delay_saved = reverse_delay
@@ -111,13 +111,13 @@ class MoveAnimation(BaseAnimation):
self.ease_in = ease_in
self.ease_out = ease_out
def restart(self):
def restart(self) -> None:
super().restart()
self.reverse_delay = self.reverse_delay_saved
self.total_distance = self.total_distance_saved
self.start_position = self.start_position_saved
def update(self, current_time_ms: float):
def update(self, current_time_ms: float) -> None:
elapsed_time = current_time_ms - self.start_ms
if elapsed_time < self.delay:
self.attribute = self.start_position
@@ -138,12 +138,12 @@ class MoveAnimation(BaseAnimation):
self.attribute = self.start_position + (self.total_distance * progress)
class TextureChangeAnimation(BaseAnimation):
def __init__(self, duration: float, textures: list[tuple[float, float, int]], delay: float = 0.0):
def __init__(self, duration: float, textures: list[tuple[float, float, int]], delay: float = 0.0) -> None:
super().__init__(duration)
self.textures = textures
self.delay = delay
def update(self, current_time_ms: float):
def update(self, current_time_ms: float) -> None:
elapsed_time = current_time_ms - self.start_ms - self.delay
if elapsed_time <= self.duration:
for start, end, index in self.textures:
@@ -153,9 +153,9 @@ class TextureChangeAnimation(BaseAnimation):
self.is_finished = True
class TextStretchAnimation(BaseAnimation):
def __init__(self, duration: float):
def __init__(self, duration: float) -> None:
super().__init__(duration)
def update(self, current_time_ms: float):
def update(self, current_time_ms: float) -> None:
elapsed_time = current_time_ms - self.start_ms
if elapsed_time <= self.duration:
self.attribute = 2 + 5 * (elapsed_time // 25)
@@ -169,7 +169,7 @@ class TextStretchAnimation(BaseAnimation):
class TextureResizeAnimation(BaseAnimation):
def __init__(self, duration: float, initial_size: float = 1.0,
final_size: float = 0.0, delay: float = 0.0,
reverse_delay: Optional[float] = None):
reverse_delay: Optional[float] = None) -> None:
super().__init__(duration, delay)
self.initial_size = initial_size
self.final_size = final_size
@@ -178,14 +178,14 @@ class TextureResizeAnimation(BaseAnimation):
self.final_size_saved = final_size
self.reverse_delay_saved = reverse_delay
def restart(self):
def restart(self) -> None:
super().restart()
self.reverse_delay = self.reverse_delay_saved
self.initial_size = self.initial_size_saved
self.final_size = self.final_size_saved
def update(self, current_time_ms: float):
def update(self, current_time_ms: float) -> None:
elapsed_time = current_time_ms - self.start_ms
if elapsed_time <= self.delay:

View File

@@ -91,7 +91,7 @@ def get_average_volume_rms(data):
return rms
class Sound:
def __init__(self, file_path: Path, data=None, target_sample_rate=44100):
def __init__(self, file_path: Path, data: Optional[ndarray]=None, target_sample_rate: int=44100):
self.file_path = file_path
self.data = data
self.channels = 0
@@ -103,10 +103,10 @@ class Sound:
self.pan = 0.5 # 0.0 = left, 0.5 = center, 1.0 = right
self.normalize: Optional[float] = None
if file_path:
if file_path.exists():
self.load()
def load(self):
def load(self) -> None:
"""Load and prepare the sound file data"""
data, original_sample_rate = sf.read(str(self.file_path))
@@ -129,33 +129,33 @@ class Sound:
self.data = data
def play(self):
def play(self) -> None:
self.position = 0
self.is_playing = True
self.is_paused = False
def stop(self):
def stop(self) -> None:
self.is_playing = False
self.is_paused = False
self.position = 0
def pause(self):
def pause(self) -> None:
if self.is_playing:
self.is_paused = True
self.is_playing = False
def resume(self):
def resume(self) -> None:
if self.is_paused:
self.is_playing = True
self.is_paused = False
def normalize_vol(self, rms: float):
def normalize_vol(self, rms: float) -> None:
self.normalize = rms
if self.data is not None:
self.data = None
self.load()
def get_frames(self, num_frames):
def get_frames(self, num_frames: int) -> Optional[ndarray]:
"""Get the next num_frames of audio data, applying volume, pitch, and pan"""
if self.data is None:
return
@@ -203,21 +203,22 @@ class Sound:
return output
class Music:
def __init__(self, file_path: Path, data=None, file_type=None, target_sample_rate=44100):
def __init__(self, file_path: Path, data: Optional[ndarray]=None, target_sample_rate: int=44100, sample_rate: int =44100, preview: Optional[float]=None, normalize: Optional[float]=None):
self.file_path = file_path
self.file_type = file_type
self.data = data
self.target_sample_rate = target_sample_rate
self.sample_rate = target_sample_rate
self.sample_rate = sample_rate
self.channels = 0
self.position = 0 # In frames
self.position = 0 # In frames (original sample rate)
self.is_playing = False
self.is_paused = False
self.volume = 0.75
self.pan = 0.5 # Center
self.total_frames = 0
self.valid = False
self.normalize = None
self.normalize = normalize
self.preview = preview # Preview start time in seconds
self.is_preview_mode = preview is not None
self.file_buffer_size = int(target_sample_rate * 5) # 5 seconds buffer
self.buffer = None
@@ -225,25 +226,97 @@ class Music:
# Thread-safe updates
self.lock = Lock()
self.sound_file = None
if self.file_path.exists():
self.load_from_file()
else:
self.load_from_memory()
self.load_from_file()
def load_from_memory(self) -> None:
"""Load music from in-memory numpy array"""
try:
if self.data is None:
raise Exception("No data provided for memory loading")
def load_from_file(self):
# Convert to float32 if needed
if self.data.dtype != float32:
self.data = self.data.astype(float32)
if self.sample_rate != self.target_sample_rate:
print(f"Resampling {self.file_path} from {self.sample_rate}Hz to {self.target_sample_rate}Hz")
self.data = resample(self.data, self.sample_rate, self.target_sample_rate)
if self.normalize is not None:
current_rms = get_average_volume_rms(self.data)
if current_rms > 0: # Avoid division by zero
target_rms = self.normalize
rms_scale_factor = target_rms / current_rms
self.data *= rms_scale_factor
# Determine channels and total frames
if self.data.ndim == 1:
self.channels = 1
self.total_frames = len(self.data)
# Reshape for consistency
self.data = self.data.reshape(-1, 1)
else:
self.channels = self.data.shape[1]
self.total_frames = self.data.shape[0]
self.sample_width = 4 # float32
self._fill_buffer()
self.valid = True
print(f"Music loaded from memory: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames")
except Exception as e:
print(f"Error loading music from memory: {e}")
self.valid = False
def load_from_file(self) -> None:
"""Load music from file"""
try:
# soundfile handles OGG, WAV, FLAC, etc. natively
self.sound_file = sf.SoundFile(str(self.file_path))
# Get file properties
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_rate = self.sound_file.samplerate
self.total_frames = len(self.sound_file)
original_total_frames = self.sound_file.frames
# Initialize buffer with some initial data
self._fill_buffer()
if self.is_preview_mode:
# Calculate preview start and end frames
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)
# Ensure preview start is within bounds
if preview_start_frame >= original_total_frames:
preview_start_frame = max(0, original_total_frames - preview_duration_frames)
preview_end_frame = original_total_frames
# Seek to preview start position
self.sound_file.seek(preview_start_frame)
# Read only the preview segment
frames_to_read = preview_end_frame - preview_start_frame
self.data = self.sound_file.read(frames_to_read)
# Update total frames to reflect the preview segment
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:
# Load entire file
self.data = self.sound_file.read()
self.total_frames = original_total_frames
self.load_from_memory()
self.valid = True
print(f"Music loaded: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames")
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)")
else:
print(f"Music loaded: {self.channels} channels, {self.sample_rate}Hz, {self.total_frames} frames")
except Exception as e:
print(f"Error loading music file: {e}")
@@ -252,50 +325,31 @@ class Music:
self.sound_file = None
self.valid = False
def _fill_buffer(self):
"""Fill the streaming buffer from file"""
if not self.sound_file:
return False
# Read a chunk of frames from file
def _fill_buffer(self) -> bool:
"""Fill buffer from in-memory data"""
try:
frames_to_read = min(self.file_buffer_size, self.total_frames - self.position)
if frames_to_read <= 0:
if self.data is None:
return False
# Read data directly as numpy array (float64 by default)
data = self.sound_file.read(frames_to_read)
start_frame = self.position + self.buffer_position
end_frame = min(start_frame + self.file_buffer_size, self.total_frames)
# Convert to float32 if needed (soundfile returns float64 by default)
if data.dtype != float32:
data = data.astype(float32)
if start_frame >= self.total_frames:
return False
# Ensure proper shape for mono audio
if self.channels == 1 and data.ndim == 1:
data = data.reshape(-1, 1)
elif self.channels == 1 and data.ndim == 2:
data = data[:, 0].reshape(-1, 1) # Take first channel if stereo file but expecting mono
# Extract the chunk of data
data_chunk = self.data[start_frame:end_frame]
# Resample if needed
if self.sample_rate != self.target_sample_rate:
print(f"Resampling {self.file_path} from {self.sample_rate}Hz to {self.target_sample_rate}Hz")
data = resample(data, self.sample_rate, self.target_sample_rate)
if self.normalize is not None:
current_rms = get_average_volume_rms(data)
if current_rms > 0: # Avoid division by zero
target_rms = self.normalize
rms_scale_factor = target_rms / current_rms
data *= rms_scale_factor
self.buffer = data
self.buffer = data_chunk
self.position += self.buffer_position
self.buffer_position = 0
return True
except Exception as e:
print(f"Error filling buffer: {e}")
print(f"Error filling buffer from memory: {e}")
return False
def update(self):
def update(self) -> None:
"""Update music stream buffers"""
if not self.is_playing or self.is_paused:
return
@@ -303,25 +357,27 @@ class Music:
with self.lock:
# Check if we need to refill the buffer
if self.buffer is None:
raise Exception("buffer is None")
if self.sound_file and self.buffer_position >= len(self.buffer):
if not self._fill_buffer():
self.is_playing = False
return
if self.buffer_position >= len(self.buffer):
self.is_playing = self._fill_buffer()
def play(self):
def play(self) -> None:
"""Start playing the music stream"""
with self.lock:
# Reset position if at the end
if self.sound_file and self.position >= self.total_frames:
self.sound_file.seek(0) # Reset to beginning
if self.position >= self.total_frames:
self.position = 0
self.buffer_position = 0
self._fill_buffer()
if self.sound_file:
# For preview mode, seek to the preview start position
seek_pos = int(self.preview * self.sample_rate) if self.is_preview_mode else 0
self.sound_file.seek(seek_pos)
self._fill_buffer()
self.is_playing = True
self.is_paused = False
def stop(self):
def stop(self) -> None:
"""Stop playing the music stream"""
with self.lock:
self.is_playing = False
@@ -329,49 +385,62 @@ class Music:
self.position = 0
self.buffer_position = 0
if self.sound_file:
self.sound_file.seek(0) # Reset to beginning
# For preview mode, seek to the preview start position
seek_pos = int(self.preview * self.sample_rate) if self.is_preview_mode else 0
self.sound_file.seek(seek_pos)
self._fill_buffer()
def pause(self):
def pause(self) -> None:
"""Pause the music playback"""
with self.lock:
if self.is_playing:
self.is_paused = True
self.is_playing = False
def resume(self):
def resume(self) -> None:
"""Resume the music playback"""
with self.lock:
if self.is_paused:
self.is_playing = True
self.is_paused = False
def seek(self, position_seconds):
"""Seek to a specific position in seconds"""
def seek(self, position_seconds) -> None:
"""Seek to a specific position in seconds (relative to preview start if in preview mode)"""
with self.lock:
# Convert seconds to frames
frame_position = int(position_seconds * self.sample_rate)
frame_position = int(position_seconds * self.target_sample_rate)
# Clamp position to valid range
frame_position = max(0, min(frame_position, self.total_frames - 1))
# Update file position if streaming from file
if self.sound_file:
self.sound_file.seek(frame_position)
self._fill_buffer()
# For preview mode, add the preview offset
actual_file_position = frame_position
if self.is_preview_mode:
actual_file_position += int(self.preview * self.sample_rate)
self.sound_file.seek(actual_file_position)
self.position = frame_position
self.buffer_position = 0
self._fill_buffer()
def get_time_length(self):
"""Get the total length of the music in seconds"""
return self.total_frames / self.sample_rate
def get_time_length(self) -> float:
"""Get the total length of the music in seconds (preview length if in preview mode)"""
return self.total_frames / self.target_sample_rate
def get_time_played(self):
"""Get the current playback position in seconds"""
return (self.position + self.buffer_position) / self.sample_rate
def get_time_played(self) -> float:
"""Get the current playback position in seconds (relative to preview start if in preview mode)"""
return (self.position + self.buffer_position) / self.target_sample_rate
def get_frames(self, num_frames):
def get_actual_time_played(self) -> float:
"""Get the actual playback position in the original file (including preview offset)"""
base_time = (self.position + self.buffer_position) / self.target_sample_rate
if self.is_preview_mode:
return base_time + self.preview
return base_time
def get_frames(self, num_frames) -> ndarray:
"""Get the next num_frames of music data, applying volume, pitch, and pan"""
if not self.is_playing:
# Return silence if not playing
@@ -382,11 +451,12 @@ class Music:
with self.lock:
if self.buffer is None:
raise Exception("buffer is None")
return zeros(num_frames, dtype=float32)
# Check if we need more data
if self.buffer_position >= len(self.buffer):
# If no more data available and streaming from file
if self.sound_file and not self._fill_buffer():
# Try to fill buffer again
if not self._fill_buffer():
self.is_playing = False
if self.channels == 1:
return zeros(num_frames, dtype=float32)
@@ -409,7 +479,6 @@ class Music:
# Update buffer position
self.buffer_position += frames_to_get
self.position += frames_to_get
# Apply volume
output *= self.volume
@@ -425,7 +494,7 @@ class Music:
return output
def __del__(self):
def __del__(self) -> None:
"""Cleanup when the music object is deleted"""
if hasattr(self, 'sound_file') and self.sound_file:
try:
@@ -434,7 +503,7 @@ class Music:
raise Exception("unable to close music stream")
class AudioEngine:
def __init__(self, type: str):
def __init__(self, type: str) -> None:
self.target_sample_rate = 44100
self.buffer_size = 10
self.sounds: dict[str, Sound] = {}
@@ -453,20 +522,20 @@ class AudioEngine:
self.update_thread_running = False
self.type = type
def _initialize_asio(self):
"""Set up ASIO device"""
# Find ASIO API and use its default device
def _initialize_api(self) -> bool:
"""Set up API device"""
# Find API and use its default device
hostapis = sd.query_hostapis()
asio_api_index = -1
api_index = -1
for i, api in enumerate(hostapis):
if isinstance(api, dict) and 'name' in api and api['name'] == self.type:
asio_api_index = i
api_index = i
break
if isinstance(hostapis, tuple):
asio_api = hostapis[asio_api_index]
if isinstance(asio_api, dict) and 'default_output_device' in asio_api:
default_asio_device = asio_api['default_output_device']
api = hostapis[api_index]
if isinstance(api, dict) and 'default_output_device' in api:
default_asio_device = api['default_output_device']
else:
raise Exception("Warning: 'default_output_device' key not found in ASIO API info.")
if default_asio_device >= 0:
@@ -500,7 +569,7 @@ class AudioEngine:
self.output_channels = min(2, device_info['max_output_channels'])
return True
def _audio_callback(self, outdata, frames, time, status):
def _audio_callback(self, outdata: ndarray, frames: int, time: int, status: str) -> None:
"""Callback function for the sounddevice stream"""
if status:
print(f"Status: {status}")
@@ -589,31 +658,36 @@ class AudioEngine:
outdata[:] = output
def _start_update_thread(self):
def _start_update_thread(self) -> None:
"""Start a thread to update music streams"""
self.update_thread_running = True
self.update_thread = Thread(target=self._update_music_thread)
self.update_thread.daemon = True
self.update_thread.start()
def _update_music_thread(self):
def _update_music_thread(self) -> None:
"""Thread function to update all music streams"""
while self.update_thread_running:
# Update all active music streams
for music_name, music in self.music_streams.items():
if music.is_playing:
music.update()
active_streams = [music for music in self.music_streams.values() if music.is_playing]
# Sleep to not consume too much CPU
time.sleep(0.1)
if not active_streams:
# Sleep longer when no streams are active
time.sleep(0.5)
continue
for music in active_streams:
music.update()
# Adjust sleep based on number of active streams
sleep_time = max(0.05, 0.1 / len(active_streams))
time.sleep(sleep_time)
def init_audio_device(self):
if self.audio_device_ready:
return True
try:
# Try to use ASIO if available
self._initialize_asio()
self._initialize_api()
# Set up and start the stream
extra_settings = None
@@ -630,7 +704,6 @@ class AudioEngine:
self.stream.start()
self.running = True
self.audio_device_ready = True
print(self.stream.samplerate, self.stream.blocksize, self.stream.latency*1000)
# Start update thread for music streams
self._start_update_thread()
@@ -642,7 +715,7 @@ class AudioEngine:
self.audio_device_ready = False
return False
def close_audio_device(self):
def close_audio_device(self) -> None:
self.update_thread_running = False
if self.update_thread:
self.update_thread.join(timeout=1.0)
@@ -673,27 +746,27 @@ class AudioEngine:
print(f"Loaded sound from {fileName} as {sound_id}")
return sound_id
def play_sound(self, sound):
def play_sound(self, sound) -> None:
if sound in self.sounds:
self.sound_queue.put(sound)
def stop_sound(self, sound):
def stop_sound(self, sound) -> None:
if sound in self.sounds:
self.sounds[sound].stop()
def pause_sound(self, sound: str):
def pause_sound(self, sound: str) -> None:
if sound in self.sounds:
self.sounds[sound].pause()
def resume_sound(self, sound: str):
def resume_sound(self, sound: str) -> None:
if sound in self.sounds:
self.sounds[sound].resume()
def unload_sound(self, sound: str):
def unload_sound(self, sound: str) -> None:
if sound in self.sounds:
del self.sounds[sound]
def normalize_sound(self, sound: str, rms: float):
def normalize_sound(self, sound: str, rms: float) -> None:
if sound in self.sounds:
self.sounds[sound].normalize_vol(rms)
@@ -705,31 +778,41 @@ class AudioEngine:
return self.sounds[sound].is_playing
return False
def set_sound_volume(self, sound: str, volume: float):
def set_sound_volume(self, sound: str, volume: float) -> None:
if sound in self.sounds:
self.sounds[sound].volume = max(0.0, min(1.0, volume))
def set_sound_pan(self, sound: str, pan: float):
def set_sound_pan(self, sound: str, pan: float) -> None:
if sound in self.sounds:
self.sounds[sound].pan = max(0.0, min(1.0, pan))
def load_music_stream(self, fileName: Path) -> str:
music = Music(file_path=fileName, target_sample_rate=self.target_sample_rate)
def load_music_stream(self, fileName: Path, preview: float=0, normalize: Optional[float] = None) -> str:
music = Music(file_path=fileName, target_sample_rate=self.target_sample_rate, preview=preview, normalize=normalize)
music_id = f"music_{len(self.music_streams)}"
self.music_streams[music_id] = music
print(f"Loaded music stream from {fileName} as {music_id}")
return music_id
def load_music_stream_from_data(self, audio_array: ndarray, sample_rate: int=44100) -> str:
"""Load music stream from numpy array data"""
# Create a dummy path since Music class expects one
dummy_path = Path("memory_audio")
music = Music(file_path=dummy_path, data=audio_array, target_sample_rate=self.target_sample_rate, sample_rate=sample_rate)
music_id = f"music_{len(self.music_streams)}"
self.music_streams[music_id] = music
print(f"Loaded music stream from memory data as {music_id}")
return music_id
def is_music_valid(self, music: str) -> bool:
if music in self.music_streams:
return self.music_streams[music].valid
return False
def unload_music_stream(self, music: str):
def unload_music_stream(self, music: str) -> None:
if music in self.music_streams:
del self.music_streams[music]
def play_music_stream(self, music: str):
def play_music_stream(self, music: str) -> None:
if music in self.music_streams:
self.music_queue.put((music, 'play'))
@@ -738,35 +821,35 @@ class AudioEngine:
return self.music_streams[music].is_playing
return False
def update_music_stream(self, music: str):
def update_music_stream(self, music: str) -> None:
if music in self.music_streams:
self.music_streams[music].update()
def stop_music_stream(self, music: str):
def stop_music_stream(self, music: str) -> None:
if music in self.music_streams:
self.music_queue.put((music, 'stop'))
def pause_music_stream(self, music: str):
def pause_music_stream(self, music: str) -> None:
if music in self.music_streams:
self.music_queue.put((music, 'pause'))
def resume_music_stream(self, music: str):
def resume_music_stream(self, music: str) -> None:
if music in self.music_streams:
self.music_queue.put((music, 'resume'))
def seek_music_stream(self, music: str, position: float):
def seek_music_stream(self, music: str, position: float) -> None:
if music in self.music_streams:
self.music_queue.put((music, 'seek', position))
def set_music_volume(self, music: str, volume: float):
def set_music_volume(self, music: str, volume: float) -> None:
if music in self.music_streams:
self.music_streams[music].volume = max(0.0, min(1.0, volume))
def set_music_pan(self, music: str, pan: float):
def set_music_pan(self, music: str, pan: float) -> None:
if music in self.music_streams:
self.music_streams[music].pan = max(0.0, min(1.0, pan))
def normalize_music_stream(self, music: str, rms: float):
def normalize_music_stream(self, music: str, rms: float) -> None:
if music in self.music_streams:
self.music_streams[music].normalize = rms

View File

@@ -9,14 +9,11 @@ from libs.utils import get_current_ms
class VideoPlayer:
def __init__(self, path: Path):
"""Initialize a video player instance. Audio must have the same name and an ogg extension.
Todo: extract audio from video directly
"""
"""Initialize a video player instance"""
self.is_finished_list = [False, False]
self.video_path = path
self.video = VideoFileClip(path)
audio_path = path.with_suffix('.ogg')
self.audio = audio.load_music_stream(audio_path)
if self.video.audio is not None:
self.audio = audio.load_music_stream_from_data(self.video.audio.to_soundarray(), sample_rate=self.video.audio.fps)
self.buffer_size = 10 # Number of frames to keep in memory
self.frame_buffer = {} # Dictionary to store frames {timestamp: texture}
@@ -27,15 +24,18 @@ class VideoPlayer:
self.current_frame = None
self.fps = self.video.fps
self.frame_duration = 1000 / self.fps
self.audio_played = False
def _audio_manager(self):
if not audio.is_music_stream_playing(self.audio):
if self.audio is None:
return
if self.is_finished_list[1]:
return
if not audio.is_music_stream_playing(self.audio) and not self.audio_played:
audio.play_music_stream(self.audio)
self.audio_played = True
audio.update_music_stream(self.audio)
time_played = audio.get_music_time_played(self.audio) / audio.get_music_time_length(self.audio)
ending_lenience = 0.95
if time_played > ending_lenience:
self.is_finished_list[1] = True
self.is_finished_list[1] = not audio.is_music_stream_playing(self.audio)
def _load_frame(self, index: int):
"""Load a specific frame into the buffer"""
@@ -102,7 +102,7 @@ class VideoPlayer:
"""Updates video playback, advancing frames and audio"""
self._audio_manager()
if self.frame_index >= len(self.frame_timestamps) - 1:
if self.frame_index >= len(self.frame_timestamps):
self.is_finished_list[0] = True
return
@@ -130,11 +130,11 @@ class VideoPlayer:
def stop(self):
"""Stops the video, audio, and clears its buffer"""
self.video.close()
for timestamp, texture in self.frame_buffer.items():
ray.unload_texture(texture)
self.frame_buffer.clear()
if audio.is_music_stream_playing(self.audio):
audio.stop_music_stream(self.audio)
self.video.close()
audio.unload_music_stream(self.audio)