From 74c3414ccdb4b1cb394b3c9844443c82e56856ba Mon Sep 17 00:00:00 2001 From: Anthony Samms Date: Mon, 15 Sep 2025 10:58:44 -0400 Subject: [PATCH] new audio system? --- .github/workflows/python-app.yml | 216 ++++-- config.toml | 4 +- libs/audio.py | 1199 ++++++++---------------------- libs/audio/Makefile | 51 ++ libs/audio/audio.c | 1085 +++++++++++++++++++++++++++ libs/audio/audio.h | 490 ++++++++++++ libs/video.py | 8 +- scenes/game.py | 9 +- scenes/settings.py | 3 +- scenes/song_select.py | 3 +- 10 files changed, 2093 insertions(+), 975 deletions(-) create mode 100644 libs/audio/Makefile create mode 100644 libs/audio/audio.c create mode 100644 libs/audio/audio.h diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index d4a61b7..8bced00 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -1,70 +1,154 @@ name: PyTaiko on: - push: - branches: ["main"] - pull_request: - branches: ["main"] + push: + branches: ["main"] + pull_request: + branches: ["main"] permissions: - contents: write - pull-requests: write - issues: write - repository-projects: write + contents: write + pull-requests: write + issues: write + repository-projects: write jobs: - build: - strategy: - matrix: - os: [ubuntu-22.04, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - name: Check-out repository - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v4 - - name: Setup Python - run: uv python install - - name: Install Dependencies - run: | - uv sync - - name: Install Nuitka - run: | - uv add nuitka - - name: Build Executable - run: | - uv run nuitka --mode=app --noinclude-setuptools-mode=nofollow --noinclude-IPython-mode=nofollow --assume-yes-for-downloads PyTaiko.py - - name: Create Release Directory - run: | - mkdir -p release - cp -r Graphics Sounds Videos Songs config.toml shader model release/ - # Copy executable based on OS - if [ "${{ runner.os }}" == "Windows" ]; then - cp *.exe release/ 2>/dev/null || true - elif [ "${{ runner.os }}" == "macOS" ]; then - cp -r *.app release/ 2>/dev/null || true - else - cp *.bin release/ 2>/dev/null || true - fi - shell: bash - - name: Create Zip Archive - run: | - cd release - if [ "${{ runner.os }}" == "Windows" ]; then - 7z a ../PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip * - else - zip -r ../PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip * - fi - shell: bash - - name: Upload Artifacts - uses: actions/upload-artifact@v4 - with: - name: PyTaiko-${{ runner.os }}-${{ runner.arch }} - path: release/ - - name: Upload Release - uses: softprops/action-gh-release@v2 - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - with: - files: PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip - name: "PyTaiko [Rolling Release]" - tag_name: "latest" - make_latest: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build: + strategy: + matrix: + os: [ubuntu-22.04, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Check-out repository + uses: actions/checkout@v4 + + # Install audio library dependencies + - name: Install Audio Dependencies (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + pkg-config \ + libportaudio2 \ + libportaudio-dev \ + libsndfile1 \ + libsndfile1-dev \ + libsamplerate0 \ + libsamplerate0-dev + + - name: Install Audio Dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew update + brew install portaudio libsndfile libsamplerate pkg-config + + - name: Install Audio Dependencies (Windows) + if: runner.os == 'Windows' + run: | + # Using vcpkg for Windows dependencies + vcpkg install portaudio:x64-windows libsndfile:x64-windows libsamplerate:x64-windows + # Set environment variables for linking + echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" >> $env:GITHUB_ENV + echo "PKG_CONFIG_PATH=$env:VCPKG_INSTALLATION_ROOT/installed/x64-windows/lib/pkgconfig" >> $env:GITHUB_ENV + shell: powershell + + # Compile the audio library + - name: Check Audio Library Dependencies + run: | + cd libs/audio + make check-deps + shell: bash + continue-on-error: true + + - name: Compile Audio Library + run: | + cd libs/audio + make clean + make all + shell: bash + + - name: Install Audio Library (Unix) + if: runner.os != 'Windows' + run: | + cd libs/audio + sudo make install + shell: bash + + - name: Copy Audio Library to System Path (Windows) + if: runner.os == 'Windows' + run: | + cd libs/audio + # Copy DLL to a location where Python can find it + mkdir -p ../../build/lib + cp libaudio.dll ../../build/lib/ + # Add to PATH + echo "${{ github.workspace }}/build/lib" >> $env:GITHUB_PATH + shell: powershell + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Setup Python + run: uv python install + + - name: Install Dependencies + run: | + uv sync + + - name: Install Nuitka + run: | + uv add nuitka + + - name: Build Executable + run: | + uv run nuitka --mode=app --noinclude-setuptools-mode=nofollow --noinclude-IPython-mode=nofollow --assume-yes-for-downloads PyTaiko.py + + - name: Create Release Directory + run: | + mkdir -p release + cp -r Graphics Sounds Videos Songs config.toml shader model release/ + + # Copy the compiled audio library to release + if [ "${{ runner.os }}" == "Windows" ]; then + cp libs/audio/libaudio.dll release/ 2>/dev/null || true + cp build/lib/libaudio.dll release/ 2>/dev/null || true + elif [ "${{ runner.os }}" == "macOS" ]; then + cp libs/audio/libaudio.dylib release/ 2>/dev/null || true + else + cp libs/audio/libaudio.so release/ 2>/dev/null || true + fi + + # Copy executable based on OS + if [ "${{ runner.os }}" == "Windows" ]; then + cp *.exe release/ 2>/dev/null || true + elif [ "${{ runner.os }}" == "macOS" ]; then + cp -r *.app release/ 2>/dev/null || true + else + cp *.bin release/ 2>/dev/null || true + fi + shell: bash + + - name: Create Zip Archive + run: | + cd release + if [ "${{ runner.os }}" == "Windows" ]; then + 7z a ../PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip * + else + zip -r ../PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip * + fi + shell: bash + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: PyTaiko-${{ runner.os }}-${{ runner.arch }} + path: release/ + + - name: Upload Release + uses: softprops/action-gh-release@v2 + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + with: + files: PyTaiko-${{ runner.os }}-${{ runner.arch }}.zip + name: "PyTaiko [Rolling Release]" + tag_name: "latest" + make_latest: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/config.toml b/config.toml index fadfaf6..5b979b6 100644 --- a/config.toml +++ b/config.toml @@ -29,8 +29,8 @@ right_don = [17] right_kat = [12] [audio] -device_type = "ALSA" -buffer_size = 64 +device_type = 0 +buffer_size = 16 sample_rate = -1 exclusive = false diff --git a/libs/audio.py b/libs/audio.py index 46b9bf9..87d3481 100644 --- a/libs/audio.py +++ b/libs/audio.py @@ -1,944 +1,347 @@ -import os -import queue -import time +import cffi from pathlib import Path -from threading import Lock, Thread -from concurrent.futures import ThreadPoolExecutor from typing import Optional - -import soundfile as sf -from numpy import ( - arange, - clip, - column_stack, - float32, - frombuffer, - int16, - int32, - interp, - mean, - multiply, - ndarray, - sqrt, - uint8, - zeros, -) - -os.environ["SD_ENABLE_ASIO"] = "1" -import sounddevice as sd - -from libs.utils import get_config, rounded - - -def resample(data, orig_sr, target_sr): - # Return original data if no resampling needed - ratio = target_sr / orig_sr - if ratio == 1.0: - return data - - # Handle both mono and multi-channel audio - if len(data.shape) == 1: # Mono audio - return _resample_channel(data, orig_sr, target_sr) - else: # Multi-channel audio - num_channels = data.shape[1] - resampled_channels = [] - - for ch in range(num_channels): - channel_data = data[:, ch] - resampled_channel = _resample_channel(channel_data, orig_sr, target_sr) - resampled_channels.append(resampled_channel) - - return column_stack(resampled_channels) - -def _resample_channel(channel_data, orig_sr, target_sr): - # Calculate number of samples in resampled audio - orig_length = len(channel_data) - new_length = int(orig_length * target_sr / orig_sr) - - # Create time points for original and new sample rates - orig_time = arange(orig_length) / orig_sr - new_time = arange(new_length) / target_sr - - # Perform linear interpolation - resampled_data = interp(new_time, orig_time, channel_data) - - return resampled_data - -def get_np_array(sample_width, raw_data): - if sample_width == 1: - # 8-bit samples are unsigned - data = frombuffer(raw_data, dtype=uint8) - return (data.astype(float32) - 128) / 128.0 - elif sample_width == 2: - # 16-bit samples are signed - data = frombuffer(raw_data, dtype=int16) - return data.astype(float32) / 32768.0 - elif sample_width == 3: - # 24-bit samples handling - data = zeros(len(raw_data) // 3, dtype=int32) - for i in range(len(data)): - data[i] = int.from_bytes(raw_data[i*3:i*3+3], byteorder='little', signed=True) - return data.astype(float32) / (2**23) - elif sample_width == 4: - # 32-bit samples are signed - data = frombuffer(raw_data, dtype=int32) - return data.astype(float32) / (2**31) - else: - raise ValueError(f"Unsupported sample width: {sample_width}") - -def get_average_volume_rms(data): - """Calculate average volume using RMS method""" - rms = sqrt(mean(data ** 2)) - return rms - -class Sound: - 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 - self.sample_rate = target_sample_rate - self.position = 0 - self.is_playing = False - self.is_paused = False - self.volume = 1.0 - self.pan = 0.5 # 0.0 = left, 0.5 = center, 1.0 = right - self.normalize: Optional[float] = None - - if file_path.exists(): - self.load() - - def load(self) -> None: - """Load and prepare the sound file data""" - data, original_sample_rate = sf.read(str(self.file_path)) - - if data.ndim == 1: - self.channels = 1 - data = data.reshape(-1, 1) - else: - self.channels = data.shape[1] - - if original_sample_rate != self.sample_rate: - print(f"Resampling {self.file_path} from {original_sample_rate}Hz to {self.sample_rate}Hz") - data = resample(data, original_sample_rate, self.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.data = data - - def play(self) -> None: - self.position = 0 - self.is_playing = True - self.is_paused = False - - def stop(self) -> None: - self.is_playing = False - self.is_paused = False - self.position = 0 - - def pause(self) -> None: - if self.is_playing: - self.is_paused = True - self.is_playing = False - - def resume(self) -> None: - if self.is_paused: - self.is_playing = True - self.is_paused = False - - 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: int) -> Optional[ndarray]: - """Get the next num_frames of audio data, applying volume, pitch, and pan""" - if self.data is None: - return - if not self.is_playing: - # Return silence if not playing - if self.channels == 1: - return zeros(num_frames, dtype=float32) - else: - return zeros((num_frames, self.channels), dtype=float32) - - # Calculate how many frames we have left - frames_left = len(self.data) - self.position - if self.channels > 1: - frames_left = self.data.shape[0] - self.position - - if frames_left <= 0: - # We've reached the end of the sound - self.is_playing = False - if self.channels == 1: - return zeros(num_frames, dtype=float32) - else: - return zeros((num_frames, self.channels), dtype=float32) - - # Get the actual frames to return - frames_to_get = min(num_frames, frames_left) - - if self.channels == 1: - output = zeros(num_frames, dtype=float32) - output[:frames_to_get] = self.data[self.position:self.position+frames_to_get].flatten() - else: - output = zeros((num_frames, self.channels), dtype=float32) - output[:frames_to_get] = self.data[self.position:self.position+frames_to_get] - - self.position += frames_to_get - - output *= self.volume - - # Apply pan for stereo output - if self.channels == 2 and self.pan != 0.5: - # pan=0: full left, pan=0.5: center, pan=1: full right - left_vol = min(1.0, 2.0 * (1.0 - self.pan)) - right_vol = min(1.0, 2.0 * self.pan) - output[:, 0] *= left_vol - output[:, 1] *= right_vol - return output - - -class Music: - BUFFER_DURATION = 5.0 # seconds - 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): - # Core properties - self.file_path = file_path - self.data = data - self.target_sample_rate = target_sample_rate - self.sample_rate = sample_rate - self.normalize = normalize - self.preview = preview - - # Playback state - self.position = 0 - self.is_playing = False - self.is_paused = False - self.volume = 0.75 - self.pan = 0.5 - - # Audio properties (set during loading) - self.channels = 0 - self.total_frames = 0 - self.valid = False - - # Buffering - self.buffer_size = int(target_sample_rate * self.BUFFER_DURATION) - self.buffer = None - self.buffer_position = 0 - - # File streaming - self.sound_file = None - self.uses_file_streaming = False - self.preview_start_frame = 0 - self.preview_end_frame = 0 - - # Thread safety - self.lock = Lock() - self.buffer_executor = ThreadPoolExecutor(max_workers=1) - self.buffer_future = None - - # Load audio - self._load_audio() - - def _load_audio(self): - """Load audio from file or memory""" - try: - if self.file_path.exists(): - self._load_from_file() - else: - self._load_from_memory() - except Exception as e: - print(f"Error loading audio: {e}") - self.valid = False - - def _load_from_file(self): - """Load and setup file streaming""" - self.sound_file = sf.SoundFile(str(self.file_path)) - self.channels = self.sound_file.channels - self.sample_rate = self.sound_file.samplerate - original_frames = self.sound_file.frames - - self.preview_start_frame, self.preview_end_frame = self._calculate_preview_bounds(original_frames) - self.total_frames = self.preview_end_frame - self.preview_start_frame - - if self.preview is not None: - self.uses_file_streaming = True - self.sound_file.seek(self.preview_start_frame) - self.data = None - print(f"Preview mode: Streaming {self.total_frames} frames starting at {self.preview:.2f}s") - else: - self.data = self.sound_file.read() - self.data = self._process_audio_data(self.data) - self.uses_file_streaming = False - - self._fill_buffer() - self.valid = True - print(f"Music loaded: {self.channels} channels, {self.sample_rate}Hz, {self.time_length:.2f}s") - - def _load_from_memory(self): - """Load from in-memory numpy array""" - if self.data is None: - raise ValueError("No data provided for memory loading") - - self.data = self._process_audio_data(self.data) - self.channels = 1 if self.data.ndim == 1 else self.data.shape[1] - self.total_frames = len(self.data) if self.data.ndim == 1 else self.data.shape[0] - self.uses_file_streaming = False - - self._fill_buffer() - self.valid = True - print(f"Music loaded from memory: {self.channels} channels, {self.total_frames} frames") - - def _process_audio_data(self, data): - """Apply resampling, normalization, and ensure correct format""" - if data.dtype != float32: - data = data.astype(float32) - - if self.sample_rate != self.target_sample_rate: - print(f"Resampling 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: - data *= self.normalize / current_rms - - if data.ndim == 1: - data = data.reshape(-1, 1) - - return data - - def _calculate_preview_bounds(self, total_frames): - """Calculate start and end frames for preview mode""" - if self.preview is None: - return 0, total_frames - - start_frame = int(self.preview * self.sample_rate) - start_frame = max(0, min(start_frame, total_frames - 1)) - end_frame = total_frames - - return start_frame, end_frame - - def _fill_buffer(self): - """Fill the audio buffer from appropriate source""" - if self.buffer_future and not self.buffer_future.done(): - return True - if self.uses_file_streaming: - self.buffer_future = self.buffer_executor.submit(self._fill_buffer_from_file) - return True - else: - self.buffer_future = self.buffer_executor.submit(self._fill_buffer_from_memory) - return True - - def _fill_buffer_from_file(self): - """Stream buffer from file""" - if not self.sound_file: - return False - - current_pos = self.preview_start_frame + self.position - if current_pos >= self.preview_end_frame: - return False - - frames_to_read = min(self.buffer_size, self.preview_end_frame - current_pos) - if frames_to_read <= 0: - return False - - self.sound_file.seek(current_pos) - data_chunk = self.sound_file.read(frames_to_read) - - if len(data_chunk) == 0: - return False - - original_frames = len(data_chunk) if data_chunk.ndim == 1 else data_chunk.shape[0] - - data_chunk = self._process_audio_data(data_chunk) - - self.buffer = data_chunk - self.buffer_position = 0 - self.position += original_frames - - return True - - def _fill_buffer_from_memory(self): - """Fill buffer from in-memory data""" - if self.data is None: - return False - - start_frame = self.position + self.buffer_position - end_frame = min(start_frame + self.buffer_size, self.total_frames) - - if start_frame >= self.total_frames: - return False - - self.buffer = self.data[start_frame:end_frame] - self.position += self.buffer_position - self.buffer_position = 0 - - return True - - @property - def time_length(self): - """Total length in seconds""" - return self.total_frames / self.target_sample_rate - - @property - def time_played(self): - """Current position in seconds (relative to preview start)""" - return (self.position + self.buffer_position) / self.target_sample_rate - - @property - def actual_time_played(self): - """Actual position in original file (including preview offset)""" - base_time = self.time_played - if self.preview is not None: - return base_time + self.preview - return base_time - - def update(self): - """Update music stream buffers""" - if not self.is_playing or self.is_paused: - return - - with self.lock: - if self.buffer is None: - return - if self.buffer_position >= len(self.buffer): - self.is_playing = self._fill_buffer() - - def play(self): - """Start playing""" - with self.lock: - if self.position >= self.total_frames: - self._reset_position() - - self.is_playing = True - self.is_paused = False - - def stop(self): - """Stop playing""" - with self.lock: - self.is_playing = False - self.is_paused = False - self._reset_position() - - def pause(self): - """Pause playback""" - with self.lock: - if self.is_playing: - self.is_paused = True - self.is_playing = False - - def resume(self): - """Resume playback""" - with self.lock: - if self.is_paused: - self.is_playing = True - self.is_paused = False - - def seek(self, position_seconds): - """Seek to position in seconds""" - with self.lock: - frame_position = int(position_seconds * self.target_sample_rate) - frame_position = max(0, min(frame_position, self.total_frames - 1)) - - if self.sound_file: - actual_pos = frame_position - if self.preview is not None: - actual_pos += int(self.preview * self.sample_rate) - self.sound_file.seek(actual_pos) - - self.position = frame_position - self.buffer_position = 0 - self._fill_buffer() - - def _reset_position(self): - """Reset to beginning""" - self.position = 0 - self.buffer_position = 0 - if self.sound_file: - seek_pos = self.preview_start_frame if self.preview is not None else 0 - self.sound_file.seek(seek_pos) - self._fill_buffer() - - def get_frames(self, num_frames): - """Get next audio frames with volume and panning applied""" - if not self.is_playing: - return self._get_silence(num_frames) - - with self.lock: - if self.buffer is None: - return self._get_silence(num_frames) - - if self.buffer_position >= len(self.buffer): - if not self._fill_buffer(): - self.is_playing = False - return self._get_silence(num_frames) - - frames_available = len(self.buffer) - self.buffer_position - if self.channels > 1: - frames_available = self.buffer.shape[0] - self.buffer_position - - frames_to_get = min(num_frames, frames_available) - - output = self._get_silence(num_frames) - - if self.channels == 1: - output[:frames_to_get] = self.buffer[ - self.buffer_position:self.buffer_position + frames_to_get - ].flatten() - else: - output[:frames_to_get] = self.buffer[ - self.buffer_position:self.buffer_position + frames_to_get - ] - - self.buffer_position += frames_to_get - return self._apply_effects(output) - - def _get_silence(self, num_frames): - """Return silence buffer""" - if self.channels == 1: - return zeros(num_frames, dtype=float32) - else: - return zeros((num_frames, self.channels), dtype=float32) - - def _apply_effects(self, output): - """Apply volume and panning effects""" - output *= self.volume - - if self.channels == 2 and self.pan != 0.5: - left_vol = min(1.0, 2.0 * (1.0 - self.pan)) - right_vol = min(1.0, 2.0 * self.pan) - output[:, 0] *= left_vol - output[:, 1] *= right_vol - - return output - - def __del__(self): - """Cleanup""" - if hasattr(self, 'sound_file') and self.sound_file: - try: - self.sound_file.close() - except Exception: - pass +import numpy as np + +from libs.utils import get_config + +# Initialize CFFI +ffi = cffi.FFI() + +# Define the C interface +ffi.cdef(""" + typedef int PaHostApiIndex; + // Forward declarations + struct audio_buffer; + + // Type definitions + typedef struct wave { + unsigned int frameCount; + unsigned int sampleRate; + unsigned int sampleSize; + unsigned int channels; + void *data; + } wave; + + typedef struct audio_stream { + struct audio_buffer *buffer; + unsigned int sampleRate; + unsigned int sampleSize; + unsigned int channels; + } audio_stream; + + typedef struct sound { + audio_stream stream; + unsigned int frameCount; + } sound; + + typedef struct music { + audio_stream stream; + unsigned int frameCount; + void *ctxData; + } music; + + // Device management + void list_host_apis(void); + void init_audio_device(PaHostApiIndex host_api); + void close_audio_device(void); + bool is_audio_device_ready(void); + void set_master_volume(float volume); + float get_master_volume(void); + + // Wave management + wave load_wave(const char* filename); + bool is_wave_valid(wave wave); + void unload_wave(wave wave); + + // Sound management + sound load_sound_from_wave(wave wave); + sound load_sound(const char* filename); + bool is_sound_valid(sound sound); + void unload_sound(sound sound); + void play_sound(sound sound); + void pause_sound(sound sound); + void resume_sound(sound sound); + void stop_sound(sound sound); + bool is_sound_playing(sound sound); + void set_sound_volume(sound sound, float volume); + void set_sound_pitch(sound sound, float pitch); + void set_sound_pan(sound sound, float pan); + + // Audio stream management + audio_stream load_audio_stream(unsigned int sample_rate, unsigned int sample_size, unsigned int channels); + void unload_audio_stream(audio_stream stream); + void play_audio_stream(audio_stream stream); + void pause_audio_stream(audio_stream stream); + void resume_audio_stream(audio_stream stream); + bool is_audio_stream_playing(audio_stream stream); + void stop_audio_stream(audio_stream stream); + void set_audio_stream_volume(audio_stream stream, float volume); + void set_audio_stream_pitch(audio_stream stream, float pitch); + void set_audio_stream_pan(audio_stream stream, float pan); + void update_audio_stream(audio_stream stream, const void *data, int frame_count); + + // Music management + music load_music_stream(const char* filename); + bool is_music_valid(music music); + void unload_music_stream(music music); + void play_music_stream(music music); + void pause_music_stream(music music); + void resume_music_stream(music music); + void stop_music_stream(music music); + void seek_music_stream(music music, float position); + void update_music_stream(music music); + bool is_music_stream_playing(music music); + void set_music_volume(music music, float volume); + void set_music_pitch(music music, float pitch); + void set_music_pan(music music, float pan); + float get_music_time_length(music music); + float get_music_time_played(music music); + + // Memory management + void free(void *ptr); +""") + +# Load the compiled C library +# You'll need to compile your C code into a shared library first +# gcc -shared -fPIC -o libaudio.so audio.c -lportaudio -lsndfile -lpthread +try: + lib = ffi.dlopen("libs/audio_c/libaudio.so") # Adjust path as needed +except OSError as e: + print(f"Failed to load libaudio.so: {e}") + print("Make sure to compile your C code first:") + print("gcc -shared -fPIC -o libaudio.so audio.c -lportaudio -lsndfile -lpthread") + raise class AudioEngine: - def __init__(self, type: str) -> None: + def __init__(self, device_type: int): + self.device_type = device_type self.target_sample_rate = 44100 - self.buffer_size = 10 - self.sounds: dict[str, Sound] = {} - self.music_streams = {} - self.stream = None - self.device_id = None - self.running = False - self.sound_queue: queue.Queue[str] = queue.Queue() - self.music_queue = queue.Queue() - self.master_volume = 0.70 - self.output_channels = 2 # Default to stereo + self.sounds = {} # sound_id -> sound struct + self.music_streams = {} # music_id -> music struct + self.sound_counter = 0 + self.music_counter = 0 self.audio_device_ready = False - # Threading for music stream updates - self.update_thread = None - self.update_thread_running = False - self.type = type + def list_host_apis(self): + lib.list_host_apis() - self._output_buffer = None - self._channel_conversion_buffer = None - self._expected_frames = None - - def _initialize_api(self) -> bool: - """Set up API device""" - # Find API and use its default device - hostapis = sd.query_hostapis() - api_index = -1 - for i, api in enumerate(hostapis): - if isinstance(api, dict) and 'name' in api and api['name'] == self.type: - api_index = i - break - - if isinstance(hostapis, tuple): - 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: - self.device_id = default_asio_device - device_info = sd.query_devices(self.device_id) - if isinstance(device_info, sd.DeviceList): - raise Exception("Invalid ASIO Device") - print(f"Using default audio device: {device_info['name']}") - self.buffer_size = rounded(device_info['default_low_output_latency']*1000) - if 'buffer_size' in get_config()['audio']: - self.buffer_size = get_config()['audio']['buffer_size'] - self.target_sample_rate = device_info['default_samplerate'] - if 'sample_rate' in get_config()['audio']: - self.target_sample_rate = get_config()['audio']['sample_rate'] - if self.target_sample_rate == -1: - self.target_sample_rate = device_info['default_samplerate'] - # Set output channels based on device capabilities - self.output_channels = device_info['max_output_channels'] - if self.output_channels > 2: - # Limit to stereo for simplicity - self.output_channels = 2 - return True - else: - print("ASIO API not found, using system default device.") - - # If we get here, use default system device - self.device_id = None - device_info = sd.query_devices(sd.default.device[1]) - if isinstance(device_info, sd.DeviceList): - raise Exception("Invalid ASIO Device") - self.output_channels = min(2, device_info['max_output_channels']) - return True - - def _audio_callback(self, outdata: ndarray, frames: int, time: int, status: str) -> None: - """callback function for the sounddevice stream""" - - if self._output_buffer is None: - raise Exception("output buffer was not allocated") - if status: - print(f"Status: {status}") - - self._process_sound_queue() - self._process_music_queue() - - self._output_buffer.fill(0.0) - - self._mix_sounds(self._output_buffer, frames) - - self._mix_music(self._output_buffer, frames) - - if self.master_volume != 1.0: - multiply(self._output_buffer, self.master_volume, out=self._output_buffer) - - clip(self._output_buffer, -0.95, 0.95, out=self._output_buffer) - - outdata[:] = self._output_buffer - - def _process_sound_queue(self) -> None: - """Process sound queue""" - sounds_to_play = [] + def init_audio_device(self) -> bool: + """Initialize the audio device""" try: - while True: - sounds_to_play.append(self.sound_queue.get_nowait()) - except queue.Empty: - pass - - for sound_name in sounds_to_play: - if sound_name in self.sounds: - self.sounds[sound_name].play() - - def _process_music_queue(self) -> None: - """Process music queue""" - music_commands = [] - try: - while True: - music_commands.append(self.music_queue.get_nowait()) - except queue.Empty: - pass - - for command in music_commands: - music_name, action, *args = command - if music_name in self.music_streams: - music = self.music_streams[music_name] - if action == 'play': - music.play() - elif action == 'stop': - music.stop() - elif action == 'pause': - music.pause() - elif action == 'resume': - music.resume() - elif action == 'seek' and args: - music.seek(args[0]) - - def _mix_sounds(self, output: ndarray, frames: int) -> None: - """sound mixing""" - for sound in self.sounds.values(): - if not sound.is_playing: - continue - - sound_data = sound.get_frames(frames) - if sound_data is None or not isinstance(sound_data, ndarray): - continue - - # Handle channel mismatch - if sound.channels != self.output_channels: - sound_data = self._convert_channels(sound_data, sound.channels) - - output += sound_data - - def _mix_music(self, output: ndarray, frames: int) -> None: - """music mixing""" - for music in self.music_streams.values(): - if not music.is_playing: - continue - - music_data = music.get_frames(frames) - if music_data is None: - continue - - if music.channels != self.output_channels: - music_data = self._convert_channels(music_data, music.channels) - - output += music_data - - def _convert_channels(self, data: ndarray, input_channels: int) -> ndarray: - """Channel conversion using single pre-allocated buffer""" - if data.ndim == 1: - data = data.reshape(-1, 1) - input_channels = 1 - - frames = data.shape[0] - - if input_channels == self.output_channels: - return data - - if self._channel_conversion_buffer is None: - raise Exception("channel conversion buffer was not allocated") - - self._channel_conversion_buffer[:frames, :self.output_channels].fill(0.0) - - if input_channels == 1 and self.output_channels > 1: - # Mono to stereo/multi: broadcast to all channels - for ch in range(self.output_channels): - self._channel_conversion_buffer[:frames, ch] = data[:frames, 0] - - elif input_channels > self.output_channels: - if self.output_channels == 1: - # Multi to mono: average channels - self._channel_conversion_buffer[:frames, 0] = mean(data[:frames, :input_channels], axis=1) - else: - # Multi to fewer channels: take first N channels - self._channel_conversion_buffer[:frames, :self.output_channels] = data[:frames, :self.output_channels] - - # Return a view of the converted data - return self._channel_conversion_buffer[:frames, :self.output_channels] - - 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) -> None: - """Thread function to update all music streams""" - while self.update_thread_running: - active_streams = [music for music in self.music_streams.values() if music.is_playing] - - if not active_streams: - time.sleep(0.5) - continue - - for music in active_streams: - music.update() - - time.sleep(0.1) - - def init_audio_device(self): - if self.audio_device_ready: - return True - - try: - self._initialize_api() - - self._expected_frames = self.buffer_size - self._output_buffer = zeros((self._expected_frames, self.output_channels), dtype=float32) - self._channel_conversion_buffer = zeros((self._expected_frames, max(8, self.output_channels)), dtype=float32) - # Set up and start the stream - extra_settings = None - buffer_size = self.buffer_size - self.stream = sd.OutputStream( - samplerate=self.target_sample_rate, - channels=self.output_channels, - callback=self._audio_callback, - blocksize=buffer_size, - device=self.device_id, - latency='low', - extra_settings=extra_settings - ) - self.stream.start() - self.running = True - self.audio_device_ready = True - - # Start update thread for music streams - self._start_update_thread() - - print(f"Audio device initialized with {self.output_channels} channels at {self.target_sample_rate}Hz") - return True + lib.init_audio_device(self.device_type) + self.audio_device_ready = lib.is_audio_device_ready() + if self.audio_device_ready: + print("Audio device initialized successfully") + return self.audio_device_ready except Exception as e: - print(f"Error initializing audio device: {e}") - self.audio_device_ready = False + print(f"Failed to initialize audio device: {e}") return False def close_audio_device(self) -> None: - self.update_thread_running = False - if self.update_thread: - self.update_thread.join(timeout=1.0) + """Close the audio device""" + try: + # Clean up all sounds and music + for sound_id in list(self.sounds.keys()): + self.unload_sound(sound_id) + for music_id in list(self.music_streams.keys()): + self.unload_music_stream(music_id) - if self.stream: - self.stream.stop() - self.stream.close() - self.stream = None - - self.running = False - self.audio_device_ready = False - self.sounds = {} - self.music_streams = {} - self.sound_queue = queue.Queue() - self.music_queue = queue.Queue() - self._output_buffer = None - self._channel_conversion_buffer = None - print("Audio device closed") - return + lib.close_audio_device() + self.audio_device_ready = False + print("Audio device closed") + except Exception as e: + print(f"Error closing audio device: {e}") def is_audio_device_ready(self) -> bool: - return self.audio_device_ready + """Check if audio device is ready""" + return lib.is_audio_device_ready() - def set_master_volume(self, volume: float): - self.master_volume = max(0.0, min(1.0, volume)) + def set_master_volume(self, volume: float) -> None: + """Set master volume (0.0 to 1.0)""" + lib.set_master_volume(max(0.0, min(1.0, volume))) def get_master_volume(self) -> float: - return self.master_volume + """Get master volume""" + return lib.get_master_volume() - def load_sound(self, fileName: Path) -> str: - sound = Sound(fileName, target_sample_rate=self.target_sample_rate) - sound_id = f"sound_{len(self.sounds)}" - self.sounds[sound_id] = sound - print(f"Loaded sound from {fileName} as {sound_id}") - return sound_id + # Sound management + def load_sound(self, file_path: Path) -> str: + """Load a sound file and return sound ID""" + try: + file_path_str = str(file_path).encode('utf-8') + sound = lib.load_sound(file_path_str) - def play_sound(self, sound) -> None: - if sound in self.sounds: - self.sound_queue.put(sound) + if lib.is_sound_valid(sound): + sound_id = f"sound_{self.sound_counter}" + self.sounds[sound_id] = sound + self.sound_counter += 1 + print(f"Loaded sound from {file_path} as {sound_id}") + return sound_id + else: + print(f"Failed to load sound: {file_path}") + return "" + except Exception as e: + print(f"Error loading sound {file_path}: {e}") + return "" - def stop_sound(self, sound) -> None: - if sound in self.sounds: - self.sounds[sound].stop() - - def pause_sound(self, sound: str) -> None: - if sound in self.sounds: - self.sounds[sound].pause() - - def resume_sound(self, sound: str) -> None: - if sound in self.sounds: - self.sounds[sound].resume() - - def unload_sound(self, sound: str) -> None: - if sound in self.sounds: - del self.sounds[sound] + def unload_sound(self, sound_id: str) -> None: + """Unload a sound""" + if sound_id in self.sounds: + lib.unload_sound(self.sounds[sound_id]) + del self.sounds[sound_id] def unload_all_sounds(self) -> None: - sounds_to_clear = list(self.sounds.keys()) - for key in sounds_to_clear: - if key in self.sounds: - del self.sounds[key] + """Unload all sounds""" + for sound_id in list(self.sounds.keys()): + self.unload_sound(sound_id) - def normalize_sound(self, sound: str, rms: float) -> None: - if sound in self.sounds: - self.sounds[sound].normalize_vol(rms) + def play_sound(self, sound_id: str) -> None: + """Play a sound""" + if sound_id in self.sounds: + lib.play_sound(self.sounds[sound_id]) - def is_sound_valid(self, sound: str) -> bool: - return sound in self.music_streams + def stop_sound(self, sound_id: str) -> None: + """Stop a sound""" + if sound_id in self.sounds: + lib.stop_sound(self.sounds[sound_id]) - def is_sound_playing(self, sound: str) -> bool: - if sound in self.sounds: - return self.sounds[sound].is_playing + def pause_sound(self, sound_id: str) -> None: + """Pause a sound""" + if sound_id in self.sounds: + lib.pause_sound(self.sounds[sound_id]) + + def resume_sound(self, sound_id: str) -> None: + """Resume a sound""" + if sound_id in self.sounds: + lib.resume_sound(self.sounds[sound_id]) + + def is_sound_valid(self, sound_id: str) -> bool: + """Check if sound is valid""" + if sound_id in self.sounds: + return lib.is_sound_valid(self.sounds[sound_id]) return False - def set_sound_volume(self, sound: str, volume: float) -> None: - if sound in self.sounds: - self.sounds[sound].volume = max(0.0, volume) - - 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, preview: Optional[float] = None, 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 + def is_sound_playing(self, sound_id: str) -> bool: + """Check if sound is playing""" + if sound_id in self.sounds: + return lib.is_sound_playing(self.sounds[sound_id]) return False - def unload_music_stream(self, music: str) -> None: - if music in self.music_streams: - del self.music_streams[music] + def set_sound_volume(self, sound_id: str, volume: float) -> None: + """Set sound volume""" + if sound_id in self.sounds: + lib.set_sound_volume(self.sounds[sound_id], max(0.0, volume)) - def play_music_stream(self, music: str) -> None: - if music in self.music_streams: - self.music_queue.put((music, 'play')) + def set_sound_pan(self, sound_id: str, pan: float) -> None: + """Set sound pan (0.0 = left, 0.5 = center, 1.0 = right)""" + if sound_id in self.sounds: + lib.set_sound_pan(self.sounds[sound_id], max(0.0, min(1.0, pan))) - def is_music_stream_playing(self, music: str) -> bool: - if music in self.music_streams: - return self.music_streams[music].is_playing + def normalize_sound(self, sound_id: str, rms: float) -> None: + """Normalize sound - Note: This would need to be implemented in C""" + # The C implementation doesn't have normalize function yet + # You'd need to add this to your C code + print(f"Warning: normalize_sound not implemented in C backend") + + # Music management + def load_music_stream(self, file_path: Path, normalize: Optional[float] = None) -> str: + """Load a music stream and return music ID""" + try: + file_path_str = str(file_path).encode('utf-8') + music = lib.load_music_stream(file_path_str) + + if lib.is_music_valid(music): + music_id = f"music_{self.music_counter}" + self.music_streams[music_id] = music + self.music_counter += 1 + print(f"Loaded music stream from {file_path} as {music_id}") + return music_id + else: + print(f"Failed to load music: {file_path}") + return "" + except Exception as e: + print(f"Error loading music {file_path}: {e}") + return "" + + def load_music_stream_from_data(self, audio_array: np.ndarray, sample_rate: int = 44100) -> str: + """Load music from numpy array - would need C implementation""" + print("Warning: load_music_stream_from_data not implemented in C backend") + return "" + + def unload_music_stream(self, music_id: str) -> None: + """Unload a music stream""" + if music_id in self.music_streams: + lib.unload_music_stream(self.music_streams[music_id]) + del self.music_streams[music_id] + + def is_music_valid(self, music_id: str) -> bool: + """Check if music is valid""" + if music_id in self.music_streams: + return lib.is_music_valid(self.music_streams[music_id]) return False - def update_music_stream(self, music: str) -> None: - if music in self.music_streams: - self.music_streams[music].update() + def play_music_stream(self, music_id: str) -> None: + """Play a music stream""" + if music_id in self.music_streams: + lib.play_music_stream(self.music_streams[music_id]) - def stop_music_stream(self, music: str) -> None: - if music in self.music_streams: - self.music_queue.put((music, 'stop')) + def stop_music_stream(self, music_id: str) -> None: + """Stop a music stream""" + if music_id in self.music_streams: + lib.stop_music_stream(self.music_streams[music_id]) - def pause_music_stream(self, music: str) -> None: - if music in self.music_streams: - self.music_queue.put((music, 'pause')) + def pause_music_stream(self, music_id: str) -> None: + """Pause a music stream""" + if music_id in self.music_streams: + lib.pause_music_stream(self.music_streams[music_id]) - def resume_music_stream(self, music: str) -> None: - if music in self.music_streams: - self.music_queue.put((music, 'resume')) + def resume_music_stream(self, music_id: str) -> None: + """Resume a music stream""" + if music_id in self.music_streams: + lib.resume_music_stream(self.music_streams[music_id]) - def seek_music_stream(self, music: str, position: float) -> None: - if music in self.music_streams: - self.music_queue.put((music, 'seek', position)) + def is_music_stream_playing(self, music_id: str) -> bool: + """Check if music stream is playing""" + if music_id in self.music_streams: + return lib.is_music_stream_playing(self.music_streams[music_id]) + return False - 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 seek_music_stream(self, music_id: str, position: float) -> None: + """Seek music stream to position in seconds""" + if music_id in self.music_streams: + lib.seek_music_stream(self.music_streams[music_id], position) - 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 update_music_stream(self, music_id: str) -> None: + """Update music stream (fill buffers)""" + if music_id in self.music_streams: + lib.update_music_stream(self.music_streams[music_id]) - def normalize_music_stream(self, music: str, rms: float) -> None: - if music in self.music_streams: - self.music_streams[music].normalize = rms + def set_music_volume(self, music_id: str, volume: float) -> None: + """Set music volume""" + if music_id in self.music_streams: + lib.set_music_volume(self.music_streams[music_id], max(0.0, min(1.0, volume))) - def get_music_time_length(self, music: str) -> float: - if music in self.music_streams: - return self.music_streams[music].get_time_length() - raise ValueError(f"Music stream {music} not initialized") + def set_music_pan(self, music_id: str, pan: float) -> None: + """Set music pan""" + if music_id in self.music_streams: + lib.set_music_pan(self.music_streams[music_id], max(0.0, min(1.0, pan))) - def get_music_time_played(self, music: str) -> float: - if music in self.music_streams: - return self.music_streams[music].get_time_played() - raise ValueError(f"Music stream {music} not initialized") + def normalize_music_stream(self, music_id: str, rms: float) -> None: + """Normalize music stream - would need C implementation""" + print("Warning: normalize_music_stream not implemented in C backend") + def get_music_time_length(self, music_id: str) -> float: + """Get total music length in seconds""" + if music_id in self.music_streams: + return lib.get_music_time_length(self.music_streams[music_id]) + return 0.0 + + def get_music_time_played(self, music_id: str) -> float: + """Get current music position in seconds""" + if music_id in self.music_streams: + return lib.get_music_time_played(self.music_streams[music_id]) + return 0.0 + +# Create the global audio instance audio = AudioEngine(get_config()["audio"]["device_type"]) diff --git a/libs/audio/Makefile b/libs/audio/Makefile new file mode 100644 index 0000000..b4bc1e9 --- /dev/null +++ b/libs/audio/Makefile @@ -0,0 +1,51 @@ +# Makefile for audio library +CC = gcc +CFLAGS = -Wall -Wextra -O3 -fPIC -std=c99 +LDFLAGS = -shared -Wl,--export-dynamic +LIBS = -lportaudio -lsndfile -lsamplerate -lpthread -lm + +# Detect OS for library naming +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + LIBNAME = libaudio.dylib + CFLAGS += -I/usr/local/include -I/opt/homebrew/include + LDFLAGS = -shared -undefined dynamic_lookup + LDFLAGS += -L/usr/local/lib -L/opt/homebrew/lib +else ifeq ($(UNAME_S),Linux) + LIBNAME = libaudio.so +else + LIBNAME = libaudio.dll + LIBS += -lole32 -lwinmm + LDFLAGS = -shared +endif + +SOURCES = $(wildcard *.c) +OBJECTS = $(SOURCES:.c=.o) + +.PHONY: all clean install + +all: $(LIBNAME) + +$(LIBNAME): $(OBJECTS) + $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJECTS) $(LIBNAME) + +install: $(LIBNAME) + sudo cp $(LIBNAME) /usr/local/lib/ + sudo ldconfig 2>/dev/null || true + +# Development target with debug symbols +debug: CFLAGS += -g -DDEBUG +debug: $(LIBNAME) + +# Check dependencies +check-deps: + @echo "Checking dependencies..." + @pkg-config --exists portaudio-2.0 || (echo "PortAudio not found!" && false) + @pkg-config --exists sndfile || (echo "libsndfile not found!" && false) + @echo "All dependencies found!" diff --git a/libs/audio/audio.c b/libs/audio/audio.c new file mode 100644 index 0000000..9625baa --- /dev/null +++ b/libs/audio/audio.c @@ -0,0 +1,1085 @@ + +//WARNING: This is ported code from raylib's rAudio. It's also been mangled +// with claude. I need a professional review. Thank you raysan for most of the code +// https://github.com/raysan5/raylib/blob/master/src/raudio.c + +#include "portaudio.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Logging macros +#define LOG_INFO 0 +#define LOG_WARNING 1 +#define LOG_ERROR 2 + +#define TRACELOG(level, ...) do { \ + const char* level_str = (level == LOG_INFO) ? "INFO" : \ + (level == LOG_WARNING) ? "WARNING" : "ERROR"; \ + printf("[%s] AUDIO: ", level_str); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + fflush(stdout); \ +} while(0) + +// Memory management macros +#define FREE(ptr) do { if (ptr) { free(ptr); (ptr) = NULL; } } while(0) + +#define AUDIO_DEVICE_CHANNELS 2 // Device output channels: stereo +#define AUDIO_DEVICE_SAMPLE_RATE 44100 // Device output sample rate + +// Forward declarations of structures +struct audio_buffer; + +// Type definitions +typedef struct wave { + unsigned int frameCount; // Total number of frames (considering channels) + unsigned int sampleRate; // Frequency (samples per second) + unsigned int sampleSize; // Bit depth (bits per sample): 8, 16, 32 (24 not supported) + unsigned int channels; // Number of channels (1-mono, 2-stereo, ...) + void *data; // Buffer data pointer +} wave; + +// AudioStream, custom audio stream +typedef struct audio_stream { + struct audio_buffer *buffer; // Pointer to internal data used by the audio system + + unsigned int sampleRate; // Frequency (samples per second) + unsigned int sampleSize; // Bit depth (bits per sample): 8, 16, 32 (24 not supported) + unsigned int channels; // Number of channels (1-mono, 2-stereo, ...) +} audio_stream; + +// Sound +typedef struct sound { + audio_stream stream; // Audio stream + unsigned int frameCount; // Total number of frames (considering channels) +} sound; + +// Music, audio stream, anything longer than ~10 seconds should be streamed +typedef struct music { + audio_stream stream; // Audio stream + unsigned int frameCount; // Total number of frames (considering channels) + void *ctxData; +} music; + +// Music context data, required for music streaming +typedef struct music_ctx { + SNDFILE *snd_file; + SRC_STATE *resampler; + double src_ratio; +} music_ctx; + +// Audio buffer structure +struct audio_buffer { + float volume; // Audio buffer volume + float pitch; // Audio buffer pitch + float pan; // Audio buffer pan (0.0f to 1.0f) + bool playing; // Audio buffer state: AUDIO_PLAYING + bool paused; + bool isStreaming; // Audio buffer state: AUDIO_PAUSED + bool isSubBufferProcessed[2]; // SubBuffer processed (virtual double buffer) + unsigned int sizeInFrames; // Total buffer size in frames + unsigned int frameCursorPos; // Frame cursor position + unsigned int framesProcessed; // Total frames processed in this buffer (required for play timing) + unsigned char *data; // Data buffer, on music stream keeps filling + struct audio_buffer *next; // Next audio buffer on the list + struct audio_buffer *prev; // Previous audio buffer on the list +}; + + + +// Audio data context +typedef struct AudioData { + struct { + PaStream *stream; // PortAudio stream + PaStreamParameters outputParameters; // Output stream parameters + pthread_mutex_t lock; // Mutex lock for thread synchronization + bool isReady; // Check if audio device is ready + size_t pcmBufferSize; // Pre-allocated buffer size + void *pcmBuffer; // Pre-allocated buffer to read audio data from file/memory + float masterVolume; // Master volume control + } System; + struct { + struct audio_buffer *first; // Pointer to first audio_buffer in the list + struct audio_buffer *last; // Pointer to last audio_buffer in the list + } Buffer; +} AudioData; + +// Function declarations +// Device management +void list_host_apis(void); +void init_audio_device(PaHostApiIndex host_api); +void close_audio_device(void); +bool is_audio_device_ready(void); +void set_master_volume(float volume); +float get_master_volume(void); + +// Audio buffer management +struct audio_buffer *load_audio_buffer(uint32_t channels, uint32_t size_in_frames, int usage); +void unload_audio_buffer(struct audio_buffer *buffer); +bool is_audio_buffer_playing(struct audio_buffer *buffer); +void play_audio_buffer(struct audio_buffer *buffer); +void stop_audio_buffer(struct audio_buffer *buffer); +void pause_audio_buffer(struct audio_buffer *buffer); +void resume_audio_buffer(struct audio_buffer *buffer); +void set_audio_buffer_volume(struct audio_buffer *buffer, float volume); +void set_audio_buffer_pitch(struct audio_buffer *buffer, float pitch); +void set_audio_buffer_pan(struct audio_buffer *buffer, float pan); +void track_audio_buffer(struct audio_buffer *buffer); +void untrack_audio_buffer(struct audio_buffer *buffer); + +// Wave management +wave load_wave(const char* filename); +bool is_wave_valid(wave wave); +void unload_wave(wave wave); + +// Sound management +sound load_sound_from_wave(wave wave); +sound load_sound(const char* filename); +bool is_sound_valid(sound sound); +void unload_sound(sound sound); +void play_sound(sound sound); +void pause_sound(sound sound); +void resume_sound(sound sound); +void stop_sound(sound sound); +bool is_sound_playing(sound sound); +void set_sound_volume(sound sound, float volume); +void set_sound_pitch(sound sound, float pitch); +void set_sound_pan(sound sound, float pan); + +// Audio stream management +audio_stream load_audio_stream(unsigned int sample_rate, unsigned int sample_size, unsigned int channels); +void unload_audio_stream(audio_stream stream); +void play_audio_stream(audio_stream stream); +void pause_audio_stream(audio_stream stream); +void resume_audio_stream(audio_stream stream); +bool is_audio_stream_playing(audio_stream stream); +void stop_audio_stream(audio_stream stream); +void set_audio_stream_volume(audio_stream stream, float volume); +void set_audio_stream_pitch(audio_stream stream, float pitch); +void set_audio_stream_pan(audio_stream stream, float pan); +void update_audio_stream(audio_stream stream, const void *data, int frame_count); + +// Music management +music load_music_stream(const char* filename); +bool is_music_valid(music music); +void unload_music_stream(music music); +void play_music_stream(music music); +void pause_music_stream(music music); +void resume_music_stream(music music); +void stop_music_stream(music music); +void seek_music_stream(music music, float position); +void update_music_stream(music music); +bool is_music_stream_playing(music music); +void set_music_volume(music music, float volume); +void set_music_pitch(music music, float pitch); +void set_music_pan(music music, float pan); +float get_music_time_length(music music); +float get_music_time_played(music music); + +// Internal callback +static int port_audio_callback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData); + +// Global audio data +static AudioData AUDIO = { + .System.masterVolume = 1.0f +}; + +// PortAudio callback implementation +static int port_audio_callback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData) +{ + (void) inputBuffer; + (void) timeInfo; + (void) statusFlags; + (void) userData; + + float *out = (float*)outputBuffer; + + pthread_mutex_lock(&AUDIO.System.lock); + + // Initialize output buffer with silence + for (unsigned long i = 0; i < framesPerBuffer * AUDIO_DEVICE_CHANNELS; i++) { + out[i] = 0.0f; + } + + struct audio_buffer *audio_buffer = AUDIO.Buffer.first; + int active_buffers = 0; + while (audio_buffer != NULL) { + if (audio_buffer->playing && !audio_buffer->paused && audio_buffer->data != NULL) { + active_buffers++; + unsigned int subBufferSizeFrames = audio_buffer->sizeInFrames / 2; + unsigned long framesToMix = framesPerBuffer; + float *buffer_data = (float *)audio_buffer->data; + + while (framesToMix > 0) { + unsigned int currentSubBufferIndex = (audio_buffer->frameCursorPos / subBufferSizeFrames) % 2; + unsigned int frameOffsetInSubBuffer = audio_buffer->frameCursorPos % subBufferSizeFrames; + unsigned int framesLeftInSubBuffer = subBufferSizeFrames - frameOffsetInSubBuffer; + unsigned int framesThisPass = (framesToMix < framesLeftInSubBuffer) ? framesToMix : framesLeftInSubBuffer; + + if (audio_buffer->isSubBufferProcessed[currentSubBufferIndex]) { + // This part of the buffer is not ready, output silence + } else { + // Mix the audio data + for (unsigned long i = 0; i < framesThisPass; i++) { + unsigned long buffer_pos = ((audio_buffer->frameCursorPos + i) % audio_buffer->sizeInFrames) * AUDIO_DEVICE_CHANNELS; + unsigned long output_pos = (framesPerBuffer - framesToMix + i) * AUDIO_DEVICE_CHANNELS; + + for (int ch = 0; ch < AUDIO_DEVICE_CHANNELS; ch++) { + float sample = buffer_data[buffer_pos + ch] * audio_buffer->volume; + out[output_pos + ch] += sample; + } + } + } + + audio_buffer->frameCursorPos += framesThisPass; + audio_buffer->framesProcessed += framesThisPass; + framesToMix -= framesThisPass; + + unsigned int newSubBufferIndex = (audio_buffer->frameCursorPos / subBufferSizeFrames) % 2; + if (newSubBufferIndex != currentSubBufferIndex) { + audio_buffer->isSubBufferProcessed[currentSubBufferIndex] = true; + } + + // FIXED: Only stop non-streaming buffers when they reach the end + if (!audio_buffer->isStreaming && audio_buffer->frameCursorPos >= audio_buffer->sizeInFrames) { + audio_buffer->playing = false; + break; + } + + // For streaming buffers, frameCursorPos can exceed sizeInFrames and that's OK + // The modulo operation handles the circular buffer access + } + } + audio_buffer = audio_buffer->next; + } + + // Apply master volume + for (unsigned long i = 0; i < framesPerBuffer * AUDIO_DEVICE_CHANNELS; i++) { + out[i] *= AUDIO.System.masterVolume; + } + + pthread_mutex_unlock(&AUDIO.System.lock); + + return paContinue; +} + +void list_host_apis(void) +{ + PaHostApiIndex hostApiCount = Pa_GetHostApiCount(); + if (hostApiCount < 0) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to get host API count: %s", Pa_GetErrorText(hostApiCount)); + return; + } + + TRACELOG(LOG_INFO, "AUDIO: Available host APIs:"); + for (PaHostApiIndex i = 0; i < hostApiCount; i++) { + const PaHostApiInfo *info = Pa_GetHostApiInfo(i); + if (info) { + TRACELOG(LOG_INFO, " [%d] %s (%d devices)", i, info->name, info->deviceCount); + } + } +} + +PaDeviceIndex get_best_output_device_for_host_api(PaHostApiIndex hostApi) +{ + const PaHostApiInfo *hostApiInfo = Pa_GetHostApiInfo(hostApi); + if (!hostApiInfo) { + return paNoDevice; + } + + // First try the default output device for this host API + if (hostApiInfo->defaultOutputDevice != paNoDevice) { + return hostApiInfo->defaultOutputDevice; + } + + // If no default, find the first available output device for this host API + for (int i = 0; i < hostApiInfo->deviceCount; i++) { + PaDeviceIndex deviceIndex = Pa_HostApiDeviceIndexToDeviceIndex(hostApi, i); + if (deviceIndex >= 0) { + const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(deviceIndex); + if (deviceInfo && deviceInfo->maxOutputChannels > 0) { + return deviceIndex; + } + } + } + + return paNoDevice; +} + +// Device management implementations +void init_audio_device(PaHostApiIndex host_api) +{ + // Initialize PortAudio + PaError err = Pa_Initialize(); + if (err != paNoError) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to initialize PortAudio: %s", Pa_GetErrorText(err)); + return; + } + + // Initialize mutex for thread synchronization + if (pthread_mutex_init(&AUDIO.System.lock, NULL) != 0) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to create mutex for mixing"); + Pa_Terminate(); + return; + } + + // Set up output parameters + AUDIO.System.outputParameters.device = get_best_output_device_for_host_api(host_api); + if (AUDIO.System.outputParameters.device == paNoDevice) { + TRACELOG(LOG_WARNING, "AUDIO: No usable output device found"); + pthread_mutex_destroy(&AUDIO.System.lock); + Pa_Terminate(); + return; + } + + AUDIO.System.outputParameters.channelCount = AUDIO_DEVICE_CHANNELS; + AUDIO.System.outputParameters.sampleFormat = paFloat32; // Using float format like miniaudio version + AUDIO.System.outputParameters.suggestedLatency = Pa_GetDeviceInfo(AUDIO.System.outputParameters.device)->defaultLowOutputLatency; + AUDIO.System.outputParameters.hostApiSpecificStreamInfo = NULL; + + // Open the audio stream + err = Pa_OpenStream(&AUDIO.System.stream, + NULL, // No input + &AUDIO.System.outputParameters, // Output parameters + AUDIO_DEVICE_SAMPLE_RATE, // Sample rate + paFramesPerBufferUnspecified, // Frames per buffer (let PortAudio decide) + paClipOff, // No clipping + port_audio_callback, // Callback function + NULL); // User data + + if (err != paNoError) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to open audio stream: %s", Pa_GetErrorText(err)); + pthread_mutex_destroy(&AUDIO.System.lock); + Pa_Terminate(); + return; + } + + // Start the audio stream + err = Pa_StartStream(AUDIO.System.stream); + if (err != paNoError) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to start audio stream: %s", Pa_GetErrorText(err)); + Pa_CloseStream(AUDIO.System.stream); + pthread_mutex_destroy(&AUDIO.System.lock); + Pa_Terminate(); + return; + } + + AUDIO.System.isReady = true; + + // Log device information + const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(AUDIO.System.outputParameters.device); + const PaHostApiInfo *hostApiInfo = Pa_GetHostApiInfo(deviceInfo->hostApi); + + TRACELOG(LOG_INFO, "AUDIO: Device initialized successfully"); + TRACELOG(LOG_INFO, " > Backend: PortAudio | %s", hostApiInfo->name); + TRACELOG(LOG_INFO, " > Device: %s", deviceInfo->name); + TRACELOG(LOG_INFO, " > Format: %s", "Float32"); + TRACELOG(LOG_INFO, " > Channels: %d", AUDIO_DEVICE_CHANNELS); + TRACELOG(LOG_INFO, " > Sample rate: %d", AUDIO_DEVICE_SAMPLE_RATE); + TRACELOG(LOG_INFO, " > Latency: %f ms", AUDIO.System.outputParameters.suggestedLatency * 1000.0); +} + +void close_audio_device(void) +{ + if (AUDIO.System.isReady) { + // Stop the stream + PaError err = Pa_StopStream(AUDIO.System.stream); + if (err != paNoError) { + TRACELOG(LOG_WARNING, "AUDIO: Error stopping stream: %s", Pa_GetErrorText(err)); + } + + // Close the stream + err = Pa_CloseStream(AUDIO.System.stream); + if (err != paNoError) { + TRACELOG(LOG_WARNING, "AUDIO: Error closing stream: %s", Pa_GetErrorText(err)); + } + + // Cleanup + pthread_mutex_destroy(&AUDIO.System.lock); + Pa_Terminate(); + + AUDIO.System.isReady = false; + FREE(AUDIO.System.pcmBuffer); + AUDIO.System.pcmBuffer = NULL; + AUDIO.System.pcmBufferSize = 0; + + TRACELOG(LOG_INFO, "AUDIO: Device closed successfully"); + } + else { + TRACELOG(LOG_WARNING, "AUDIO: Device could not be closed, not currently initialized"); + } +} + +bool is_audio_device_ready(void) +{ + return AUDIO.System.isReady; +} + +void set_master_volume(float volume) +{ + pthread_mutex_lock(&AUDIO.System.lock); + AUDIO.System.masterVolume = volume; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +float get_master_volume(void) +{ + pthread_mutex_lock(&AUDIO.System.lock); + float volume = AUDIO.System.masterVolume; + pthread_mutex_unlock(&AUDIO.System.lock); + return volume; +} + +// Audio buffer management implementations +struct audio_buffer *load_audio_buffer(uint32_t channels, uint32_t size_in_frames, int usage) +{ + struct audio_buffer *buffer = (struct audio_buffer*)calloc(1, sizeof(struct audio_buffer)); + + if (buffer == NULL) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to allocate memory for buffer"); + return NULL; + } + + buffer->data = calloc(size_in_frames*channels*sizeof(float), 1); + if (buffer->data == NULL) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to allocate memory for buffer data"); + FREE(buffer); + return NULL; + } + + buffer->volume = 1.0f; + buffer->pitch = 1.0f; + buffer->pan = 0.5f; + buffer->playing = false; + buffer->paused = false; + buffer->frameCursorPos = 0; + buffer->framesProcessed = 0; + buffer->sizeInFrames = size_in_frames; + if (usage == 0) { // Static buffer + buffer->isSubBufferProcessed[0] = false; + buffer->isSubBufferProcessed[1] = false; + } else { // Streaming buffer + buffer->isSubBufferProcessed[0] = true; + buffer->isSubBufferProcessed[1] = true; + } + + // Set streaming flag based on usage parameter + // You can define constants like: #define AUDIO_BUFFER_USAGE_STATIC 0, #define AUDIO_BUFFER_USAGE_STREAM 1 + buffer->isStreaming = (usage == 1); // Assuming 1 means streaming + + track_audio_buffer(buffer); + + return buffer; +} + +void unload_audio_buffer(struct audio_buffer *buffer) +{ + if (buffer == NULL) return; + + untrack_audio_buffer(buffer); + + FREE(buffer->data); + FREE(buffer); +} + +bool is_audio_buffer_playing(struct audio_buffer *buffer) +{ + if (buffer == NULL) return false; + + pthread_mutex_lock(&AUDIO.System.lock); + bool result = (buffer->playing && !buffer->paused); + pthread_mutex_unlock(&AUDIO.System.lock); + return result; +} + +void play_audio_buffer(struct audio_buffer *buffer) { + if (buffer == NULL) return; + + pthread_mutex_lock(&AUDIO.System.lock); + buffer->playing = true; + buffer->paused = false; + buffer->frameCursorPos = 0; + buffer->framesProcessed = 0; + if (!buffer->isStreaming) { + buffer->isSubBufferProcessed[0] = false; + buffer->isSubBufferProcessed[1] = false; + } + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void stop_audio_buffer(struct audio_buffer* buffer) { + if (buffer == NULL) return; + + pthread_mutex_lock(&AUDIO.System.lock); + buffer->playing = false; + buffer->paused = false; + buffer->frameCursorPos = 0; + buffer->framesProcessed = 0; + buffer->isSubBufferProcessed[0] = true; + buffer->isSubBufferProcessed[1] = true; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void pause_audio_buffer(struct audio_buffer* buffer) { + if (buffer == NULL) return; + + pthread_mutex_lock(&AUDIO.System.lock); + buffer->paused = true; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void resume_audio_buffer(struct audio_buffer* buffer) { + if (buffer == NULL) return; + + pthread_mutex_lock(&AUDIO.System.lock); + buffer->paused = false; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void set_audio_buffer_volume(struct audio_buffer* buffer, float volume) { + if (buffer == NULL) return; + + pthread_mutex_lock(&AUDIO.System.lock); + buffer->volume = volume; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void set_audio_buffer_pitch(struct audio_buffer* buffer, float pitch) { + if ((buffer == NULL) || (pitch < 0.0f)) return; + + pthread_mutex_lock(&AUDIO.System.lock); + buffer->pitch = pitch; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void set_audio_buffer_pan(struct audio_buffer* buffer, float pan) { + if (buffer == NULL) return; + if (pan < 0.0f) pan = 0.0f; + else if (pan > 1.0f) pan = 1.0f; + + pthread_mutex_lock(&AUDIO.System.lock); + buffer->pan = pan; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void track_audio_buffer(struct audio_buffer* buffer) { + if (buffer == NULL) return; + + pthread_mutex_lock(&AUDIO.System.lock); + if (AUDIO.Buffer.first == NULL) AUDIO.Buffer.first = buffer; + else { + AUDIO.Buffer.last->next = buffer; + buffer->prev = AUDIO.Buffer.last; + } + AUDIO.Buffer.last = buffer; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void untrack_audio_buffer(struct audio_buffer* buffer) { + if (buffer == NULL) return; + + pthread_mutex_lock(&AUDIO.System.lock); + if (buffer->prev == NULL) AUDIO.Buffer.first = buffer->next; + else buffer->prev->next = buffer->next; + + if (buffer->next == NULL) AUDIO.Buffer.last = buffer->prev; + else buffer->next->prev = buffer->prev; + + buffer->prev = NULL; + buffer->next = NULL; + pthread_mutex_unlock(&AUDIO.System.lock); +} + +// Wave management implementations +wave load_wave(const char* filename) { + wave wave = { 0 }; + SNDFILE *snd_file; + SF_INFO sf_info; + memset(&sf_info, 0, sizeof(sf_info)); + + snd_file = sf_open(filename, SFM_READ, &sf_info); + if (snd_file == NULL) { + TRACELOG(LOG_ERROR, "AUDIO: Failed to open file '%s'\n", filename); + return wave; + } + wave.frameCount = (unsigned int)sf_info.frames; + wave.sampleRate = (unsigned int)sf_info.samplerate; + wave.channels = (unsigned int)sf_info.channels; + wave.sampleSize = 32; // Using 32-bit float samples + + size_t total_samples = sf_info.frames * sf_info.channels; + wave.data = malloc(total_samples * sizeof(float)); + if (wave.data == NULL) { + TRACELOG(LOG_ERROR, "AUDIO: Failed to allocate memory for wave data"); + sf_close(snd_file); + return wave; + } + sf_readf_float(snd_file, wave.data, sf_info.frames); + sf_close(snd_file); + return wave; +} + +bool is_wave_valid(wave wave) { + bool result = false; + if ((wave.data != NULL) && // Validate wave data available + (wave.frameCount > 0) && // Validate frame count + (wave.sampleRate > 0) && // Validate sample rate is supported + (wave.sampleSize > 0) && // Validate sample size is supported + (wave.channels > 0)) result = true; // Validate number of channels supported + + return result; +} + +void unload_wave(wave wave) { + FREE(wave.data); +} + +// Sound management implementations +sound load_sound_from_wave(wave wave) { + sound sound = { 0 }; + if (wave.data == NULL) return sound; + + struct wave resampled_wave = { 0 }; + bool is_resampled = false; + + if (wave.sampleRate != AUDIO_DEVICE_SAMPLE_RATE) { + TRACELOG(LOG_INFO, "AUDIO: Resampling wave from %d Hz to %d Hz", wave.sampleRate, AUDIO_DEVICE_SAMPLE_RATE); + + SRC_DATA src_data; + src_data.data_in = wave.data; + src_data.input_frames = wave.frameCount; + src_data.src_ratio = (double)AUDIO_DEVICE_SAMPLE_RATE / wave.sampleRate; + src_data.output_frames = (sf_count_t)(wave.frameCount * src_data.src_ratio); + + resampled_wave.data = calloc(src_data.output_frames * wave.channels, sizeof(float)); + if (resampled_wave.data == NULL) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to allocate memory for resampling"); + return sound; + } + src_data.data_out = resampled_wave.data; + + int error = src_simple(&src_data, SRC_SINC_BEST_QUALITY, wave.channels); + if (error) { + TRACELOG(LOG_WARNING, "AUDIO: Resampling failed: %s", src_strerror(error)); + FREE(resampled_wave.data); + return sound; + } + + resampled_wave.frameCount = src_data.output_frames_gen; + resampled_wave.sampleRate = AUDIO_DEVICE_SAMPLE_RATE; + resampled_wave.channels = wave.channels; + resampled_wave.sampleSize = wave.sampleSize; + is_resampled = true; + } + + struct wave *wave_to_load = is_resampled ? &resampled_wave : &wave; + + struct audio_buffer *buffer = load_audio_buffer(AUDIO_DEVICE_CHANNELS, wave_to_load->frameCount, 0); + + if (buffer != NULL && buffer->data != NULL) { + size_t samples_to_copy = wave_to_load->frameCount * wave_to_load->channels; + size_t buffer_samples = wave_to_load->frameCount * AUDIO_DEVICE_CHANNELS; + + float *wave_data = (float *)wave_to_load->data; + float *buffer_data = (float *)buffer->data; + + if (wave_to_load->channels == 1 && AUDIO_DEVICE_CHANNELS == 2) { + for (unsigned int i = 0; i < wave_to_load->frameCount; i++) { + buffer_data[i * 2] = wave_data[i]; // Left channel + buffer_data[i * 2 + 1] = wave_data[i]; // Right channel + } + } else if (wave_to_load->channels == 2 && AUDIO_DEVICE_CHANNELS == 2) { + memcpy(buffer_data, wave_data, samples_to_copy * sizeof(float)); + } else { + size_t min_samples = (samples_to_copy < buffer_samples) ? samples_to_copy : buffer_samples; + memcpy(buffer_data, wave_data, min_samples * sizeof(float)); + } + } + + sound.frameCount = wave_to_load->frameCount; + sound.stream.sampleRate = wave_to_load->sampleRate; + sound.stream.sampleSize = wave_to_load->sampleSize; + sound.stream.channels = wave_to_load->channels; + sound.stream.buffer = buffer; + + if (is_resampled) { + FREE(resampled_wave.data); + } + + return sound; +} + +sound load_sound(const char* filename) { + wave wave = load_wave(filename); + + sound sound = load_sound_from_wave(wave); + + unload_wave(wave); + return sound; +} + +bool is_sound_valid(sound sound) { + bool result = false; + if ((sound.stream.buffer != NULL) && // Validate wave data available + (sound.frameCount > 0) && // Validate frame count + (sound.stream.sampleRate > 0) && // Validate sample rate is supported + (sound.stream.sampleSize > 0) && // Validate sample size is supported + (sound.stream.channels > 0)) result = true; // Validate number of channels supported + + return result; +} + +void unload_sound(sound sound) { + unload_audio_buffer(sound.stream.buffer); +} + +void play_sound(sound sound) { + play_audio_buffer(sound.stream.buffer); +} + +void pause_sound(sound sound) { + pause_audio_buffer(sound.stream.buffer); +} + +void resume_sound(sound sound) { + resume_audio_buffer(sound.stream.buffer); +} + +void stop_sound(sound sound) { + stop_audio_buffer(sound.stream.buffer); +} + +bool is_sound_playing(sound sound) { + return is_audio_buffer_playing(sound.stream.buffer); +} + +void set_sound_volume(sound sound, float volume) { + set_audio_buffer_volume(sound.stream.buffer, volume); +} + +void set_sound_pitch(sound sound, float pitch) { + set_audio_buffer_pitch(sound.stream.buffer, pitch); +} + +void set_sound_pan(sound sound, float pan) { + set_audio_buffer_pan(sound.stream.buffer, pan); +} + +// Audio stream management implementations +audio_stream load_audio_stream(unsigned int sample_rate, unsigned int sample_size, unsigned int channels) +{ + audio_stream stream = { 0 }; + + stream.sampleRate = sample_rate; + stream.sampleSize = sample_size; + stream.channels = channels; + + // Pass 1 to indicate this is a streaming buffer + stream.buffer = load_audio_buffer(AUDIO_DEVICE_CHANNELS, AUDIO_DEVICE_SAMPLE_RATE, 1); + return stream; +} + +void unload_audio_stream(audio_stream stream) +{ + unload_audio_buffer(stream.buffer); +} + +void play_audio_stream(audio_stream stream) +{ + play_audio_buffer(stream.buffer); +} + +void pause_audio_stream(audio_stream stream) +{ + pause_audio_buffer(stream.buffer); +} + +void resume_audio_stream(audio_stream stream) +{ + resume_audio_buffer(stream.buffer); +} + +bool is_audio_stream_playing(audio_stream stream) +{ + return is_audio_buffer_playing(stream.buffer); +} + +void stop_audio_stream(audio_stream stream) +{ + stop_audio_buffer(stream.buffer); +} + +void set_audio_stream_volume(audio_stream stream, float volume) +{ + set_audio_buffer_volume(stream.buffer, volume); +} + +void set_audio_stream_pitch(audio_stream stream, float pitch) +{ + set_audio_buffer_pitch(stream.buffer, pitch); +} + +void set_audio_stream_pan(audio_stream stream, float pan) +{ + set_audio_buffer_pan(stream.buffer, pan); +} + +void update_audio_stream(audio_stream stream, const void *data, int frame_count) +{ + if (stream.buffer == NULL || data == NULL) return; + + pthread_mutex_lock(&AUDIO.System.lock); + + // For streaming, we directly update the buffer data + if (stream.buffer->data != NULL) { + float *buffer_data = (float *)stream.buffer->data; + const float *input_data = (const float *)data; + + unsigned int samples_to_copy = frame_count * AUDIO_DEVICE_CHANNELS; + unsigned int max_samples = stream.buffer->sizeInFrames * AUDIO_DEVICE_CHANNELS; + + if (samples_to_copy > max_samples) { + samples_to_copy = max_samples; + } + + // Copy the data to the buffer + memcpy(buffer_data, input_data, samples_to_copy * sizeof(float)); + + // Update the buffer size to match actual data + stream.buffer->sizeInFrames = frame_count; + // Don't reset cursor - let the callback manage it + } + + pthread_mutex_unlock(&AUDIO.System.lock); +} + +// Music management implementations +music load_music_stream(const char* filename) { + music music = { 0 }; + bool music_loaded = false; + + SF_INFO sf_info = { 0 }; + SNDFILE *snd_file = sf_open(filename, SFM_READ, &sf_info); + if (snd_file != NULL) { + music_ctx *ctx = calloc(1, sizeof(music_ctx)); + if (ctx == NULL) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to allocate memory for music context"); + sf_close(snd_file); + return music; + } + ctx->snd_file = snd_file; + + if (sf_info.samplerate != AUDIO_DEVICE_SAMPLE_RATE) { + TRACELOG(LOG_INFO, "AUDIO: Resampling music from %d Hz to %d Hz", sf_info.samplerate, AUDIO_DEVICE_SAMPLE_RATE); + int error; + ctx->resampler = src_new(SRC_SINC_FASTEST, sf_info.channels, &error); + if (ctx->resampler == NULL) { + TRACELOG(LOG_WARNING, "AUDIO: Failed to create resampler: %s", src_strerror(error)); + free(ctx); + sf_close(snd_file); + return music; + } + ctx->src_ratio = (double)AUDIO_DEVICE_SAMPLE_RATE / sf_info.samplerate; + } else { + ctx->resampler = NULL; + ctx->src_ratio = 1.0; + } + + music.ctxData = ctx; + int sample_size = 32; // We will work with floats internally + music.stream = load_audio_stream(AUDIO_DEVICE_SAMPLE_RATE, sample_size, sf_info.channels); + music.frameCount = (unsigned int)(sf_info.frames * ctx->src_ratio); + music_loaded = true; + } + if (!music_loaded) + { + TRACELOG(LOG_WARNING, "FILEIO: [%s] Music file could not be opened", filename); + if (snd_file) sf_close(snd_file); + } + else + { + TRACELOG(LOG_INFO, "FILEIO: [%s] Music file loaded successfully", filename); + TRACELOG(LOG_INFO, " > Sample rate: %i Hz", music.stream.sampleRate); + TRACELOG(LOG_INFO, " > Sample size: %i bits", music.stream.sampleSize); + TRACELOG(LOG_INFO, " > Channels: %i (%s)", music.stream.channels, + (music.stream.channels == 1) ? "Mono" : + (music.stream.channels == 2) ? "Stereo" : "Multi"); + TRACELOG(LOG_INFO, " > Total frames: %i", music.frameCount); + } + return music; +} + +bool is_music_valid(music music) +{ + return ((music.frameCount > 0) && // Validate audio frame count + (music.stream.sampleRate > 0) && // Validate sample rate is supported + (music.stream.sampleSize > 0) && // Validate sample size is supported + (music.stream.channels > 0)); // Validate number of channels supported +} + +void unload_music_stream(music music) { + if (music.ctxData) { + music_ctx *ctx = (music_ctx *)music.ctxData; + if (ctx->snd_file) sf_close(ctx->snd_file); + if (ctx->resampler) src_delete(ctx->resampler); + free(ctx); + } + unload_audio_stream(music.stream); +} + +void play_music_stream(music music) { + play_audio_stream(music.stream); +} + +void pause_music_stream(music music) { + pause_audio_stream(music.stream); +} + +void resume_music_stream(music music) { + resume_audio_stream(music.stream); +} + +void stop_music_stream(music music) { + stop_audio_stream(music.stream); +} + +void seek_music_stream(music music, float position) { + if (music.stream.buffer == NULL || music.ctxData == NULL) return; + + music_ctx *ctx = (music_ctx *)music.ctxData; + SNDFILE *sndFile = ctx->snd_file; + // Position is in output samples, so we need to convert back to input samples for seeking + unsigned int position_in_frames = (unsigned int)(position * music.stream.sampleRate / ctx->src_ratio); + + // Seek the file to the new position + sf_count_t seek_result = sf_seek(sndFile, position_in_frames, SEEK_SET); + if (seek_result < 0) return; // Seek failed + + pthread_mutex_lock(&AUDIO.System.lock); + music.stream.buffer->framesProcessed = position_in_frames; + music.stream.buffer->frameCursorPos = 0; // Reset cursor + music.stream.buffer->isSubBufferProcessed[0] = true; // Force reload + music.stream.buffer->isSubBufferProcessed[1] = true; // Force reload + pthread_mutex_unlock(&AUDIO.System.lock); +} + +void update_music_stream(music music) { + if (music.stream.buffer == NULL || music.ctxData == NULL) return; + + music_ctx *ctx = (music_ctx *)music.ctxData; + SNDFILE *sndFile = ctx->snd_file; + if (sndFile == NULL) return; + + for (int i = 0; i < 2; i++) { + pthread_mutex_lock(&AUDIO.System.lock); + bool needs_refill = music.stream.buffer->isSubBufferProcessed[i]; + pthread_mutex_unlock(&AUDIO.System.lock); + + if (needs_refill) { + unsigned int subBufferSizeFrames = music.stream.buffer->sizeInFrames / 2; + + unsigned int frames_to_read = subBufferSizeFrames; + if (ctx->resampler) { + frames_to_read = (unsigned int)(subBufferSizeFrames / ctx->src_ratio) + 1; + } + + if (AUDIO.System.pcmBufferSize < frames_to_read * music.stream.channels * sizeof(float)) { + FREE(AUDIO.System.pcmBuffer); + AUDIO.System.pcmBuffer = calloc(1, frames_to_read * music.stream.channels * sizeof(float)); + AUDIO.System.pcmBufferSize = frames_to_read * music.stream.channels * sizeof(float); + } + + sf_count_t frames_read = sf_readf_float(sndFile, (float*)AUDIO.System.pcmBuffer, frames_to_read); + + unsigned int subBufferOffset = i * subBufferSizeFrames * AUDIO_DEVICE_CHANNELS; + float *buffer_data = (float *)music.stream.buffer->data; + float *input_ptr = (float *)AUDIO.System.pcmBuffer; + sf_count_t frames_written = 0; + + if (ctx->resampler) { + SRC_DATA src_data; + src_data.data_in = input_ptr; + src_data.input_frames = frames_read; + src_data.data_out = buffer_data + subBufferOffset; + src_data.output_frames = subBufferSizeFrames; + src_data.src_ratio = ctx->src_ratio; + src_data.end_of_input = (frames_read < frames_to_read); + + int error = src_process(ctx->resampler, &src_data); + if (error) { + TRACELOG(LOG_WARNING, "AUDIO: Resampling failed: %s", src_strerror(error)); + } + frames_written = src_data.output_frames_gen; + } else { + if (music.stream.channels == 1 && AUDIO_DEVICE_CHANNELS == 2) { + for (int j = 0; j < frames_read; j++) { + buffer_data[subBufferOffset + j*2] = input_ptr[j]; + buffer_data[subBufferOffset + j*2 + 1] = input_ptr[j]; + } + } else { + memcpy(buffer_data + subBufferOffset, input_ptr, frames_read * music.stream.channels * sizeof(float)); + } + frames_written = frames_read; + } + + if (frames_written < subBufferSizeFrames) { + unsigned int offset = subBufferOffset + (frames_written * AUDIO_DEVICE_CHANNELS); + unsigned int size = (subBufferSizeFrames - frames_written) * AUDIO_DEVICE_CHANNELS * sizeof(float); + memset(buffer_data + offset, 0, size); + } + + pthread_mutex_lock(&AUDIO.System.lock); + music.stream.buffer->isSubBufferProcessed[i] = false; + pthread_mutex_unlock(&AUDIO.System.lock); + } + } +} + +bool is_music_stream_playing(music music) { + return is_audio_stream_playing(music.stream); +} + +void set_music_volume(music music, float volume) { + set_audio_stream_volume(music.stream, volume); +} + +void set_music_pitch(music music, float pitch) { + set_audio_buffer_pitch(music.stream.buffer, pitch); +} + +void set_music_pan(music music, float pan) { + set_audio_buffer_pan(music.stream.buffer, pan); +} + +float get_music_time_length(music music) { + float total_seconds = 0.0f; + + total_seconds = (float)music.frameCount/music.stream.sampleRate; + + return total_seconds; +} + +float get_music_time_played(music music) { + float seconds_played = 0.0f; + if (music.stream.buffer != NULL) { + pthread_mutex_lock(&AUDIO.System.lock); + seconds_played = (float)music.stream.buffer->framesProcessed / music.stream.sampleRate; + pthread_mutex_unlock(&AUDIO.System.lock); + } + return seconds_played; +} diff --git a/libs/audio/audio.h b/libs/audio/audio.h new file mode 100644 index 0000000..4494406 --- /dev/null +++ b/libs/audio/audio.h @@ -0,0 +1,490 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================================================================= +// CONSTANTS AND CONFIGURATION +// ============================================================================= + +#define AUDIO_DEVICE_CHANNELS 2 // Device output channels: stereo +#define AUDIO_DEVICE_SAMPLE_RATE 44100 // Device output sample rate + +// Audio buffer usage types +#define AUDIO_BUFFER_USAGE_STATIC 0 // Static audio buffer (for sounds) +#define AUDIO_BUFFER_USAGE_STREAM 1 // Streaming audio buffer (for music/streams) + +// ============================================================================= +// TYPE DEFINITIONS +// ============================================================================= + +// Forward declaration of internal audio buffer structure +struct audio_buffer; + +/** + * Wave structure - represents audio data loaded from a file + */ +typedef struct wave { + unsigned int frameCount; // Total number of frames (considering channels) + unsigned int sampleRate; // Frequency (samples per second) + unsigned int sampleSize; // Bit depth (bits per sample): 8, 16, 32 (24 not supported) + unsigned int channels; // Number of channels (1-mono, 2-stereo, ...) + void *data; // Buffer data pointer +} wave; + +/** + * AudioStream - custom audio stream for real-time audio processing + */ +typedef struct audio_stream { + struct audio_buffer *buffer; // Pointer to internal data used by the audio system + unsigned int sampleRate; // Frequency (samples per second) + unsigned int sampleSize; // Bit depth (bits per sample): 8, 16, 32 (24 not supported) + unsigned int channels; // Number of channels (1-mono, 2-stereo, ...) +} audio_stream; + +/** + * Sound - represents a short audio clip loaded into memory + * Suitable for sound effects and short audio clips (~10 seconds or less) + */ +typedef struct sound { + audio_stream stream; // Audio stream + unsigned int frameCount; // Total number of frames (considering channels) +} sound; + +/** + * Music - represents a streaming audio source + * Suitable for background music and longer audio files + */ +typedef struct music { + audio_stream stream; // Audio stream + unsigned int frameCount; // Total number of frames (considering channels) + void *ctxData; // Internal context data (file handle, decoder state, etc.) +} music; + +// ============================================================================= +// DEVICE MANAGEMENT +// ============================================================================= + +/** + * Print available host APIs to the console + */ +void list_host_apis(void); +/** + * Initialize the audio device and system + * Must be called before using any other audio functions + */ +void init_audio_device(void); + +/** + * Close the audio device and cleanup resources + * Should be called when done using audio functionality + */ +void close_audio_device(void); + +/** + * Check if the audio device is ready and initialized + * @return true if audio device is ready, false otherwise + */ +bool is_audio_device_ready(void); + +/** + * Set the master volume for all audio output + * @param volume Volume level (0.0f = silent, 1.0f = full volume) + */ +void set_master_volume(float volume); + +/** + * Get the current master volume + * @return Current master volume (0.0f to 1.0f) + */ +float get_master_volume(void); + +// ============================================================================= +// AUDIO BUFFER MANAGEMENT (Internal/Advanced) +// ============================================================================= + +/** + * Load an audio buffer with specified parameters + * @param channels Number of channels + * @param sampleRate Sample rate in Hz + * @param size_in_frames Size of buffer in frames + * @param usage Buffer usage type (AUDIO_BUFFER_USAGE_STATIC or AUDIO_BUFFER_USAGE_STREAM) + * @return Pointer to audio buffer, or NULL on failure + */ +struct audio_buffer *load_audio_buffer(uint32_t channels, uint32_t sampleRate, uint32_t size_in_frames, int usage); + +/** + * Unload and free an audio buffer + * @param buffer Pointer to audio buffer to unload + */ +void unload_audio_buffer(struct audio_buffer *buffer); + +/** + * Check if an audio buffer is currently playing + * @param buffer Pointer to audio buffer + * @return true if playing, false otherwise + */ +bool is_audio_buffer_playing(struct audio_buffer *buffer); + +/** + * Start playing an audio buffer + * @param buffer Pointer to audio buffer + */ +void play_audio_buffer(struct audio_buffer *buffer); + +/** + * Stop playing an audio buffer + * @param buffer Pointer to audio buffer + */ +void stop_audio_buffer(struct audio_buffer *buffer); + +/** + * Pause an audio buffer + * @param buffer Pointer to audio buffer + */ +void pause_audio_buffer(struct audio_buffer *buffer); + +/** + * Resume a paused audio buffer + * @param buffer Pointer to audio buffer + */ +void resume_audio_buffer(struct audio_buffer *buffer); + +/** + * Set the volume of an audio buffer + * @param buffer Pointer to audio buffer + * @param volume Volume level (0.0f = silent, 1.0f = full volume) + */ +void set_audio_buffer_volume(struct audio_buffer *buffer, float volume); + +/** + * Set the pitch of an audio buffer + * @param buffer Pointer to audio buffer + * @param pitch Pitch multiplier (1.0f = normal, 2.0f = double speed/octave higher) + */ +void set_audio_buffer_pitch(struct audio_buffer *buffer, float pitch); + +/** + * Set the pan (stereo positioning) of an audio buffer + * @param buffer Pointer to audio buffer + * @param pan Pan position (0.0f = full left, 0.5f = center, 1.0f = full right) + */ +void set_audio_buffer_pan(struct audio_buffer *buffer, float pan); + +/** + * Add an audio buffer to the internal tracking system + * @param buffer Pointer to audio buffer + */ +void track_audio_buffer(struct audio_buffer *buffer); + +/** + * Remove an audio buffer from the internal tracking system + * @param buffer Pointer to audio buffer + */ +void untrack_audio_buffer(struct audio_buffer *buffer); + +// ============================================================================= +// WAVE MANAGEMENT +// ============================================================================= + +/** + * Load wave data from file + * Supports WAV, OGG, FLAC and other formats supported by libsndfile + * @param filename Path to audio file + * @return Wave structure containing audio data + */ +wave load_wave(const char* filename); + +/** + * Check if a wave structure contains valid audio data + * @param wave Wave structure to validate + * @return true if wave is valid, false otherwise + */ +bool is_wave_valid(wave wave); + +/** + * Unload wave data and free memory + * @param wave Wave structure to unload + */ +void unload_wave(wave wave); + +// ============================================================================= +// SOUND MANAGEMENT +// ============================================================================= + +/** + * Create a sound from existing wave data + * @param wave Wave data to create sound from + * @return Sound structure + */ +sound load_sound_from_wave(wave wave); + +/** + * Load a sound directly from file + * Suitable for sound effects and short audio clips + * @param filename Path to audio file + * @return Sound structure + */ +sound load_sound(const char* filename); + +/** + * Check if a sound structure is valid + * @param sound Sound structure to validate + * @return true if sound is valid, false otherwise + */ +bool is_sound_valid(sound sound); + +/** + * Unload sound and free resources + * @param sound Sound structure to unload + */ +void unload_sound(sound sound); + +/** + * Play a sound + * @param sound Sound to play + */ +void play_sound(sound sound); + +/** + * Pause a sound + * @param sound Sound to pause + */ +void pause_sound(sound sound); + +/** + * Resume a paused sound + * @param sound Sound to resume + */ +void resume_sound(sound sound); + +/** + * Stop a sound + * @param sound Sound to stop + */ +void stop_sound(sound sound); + +/** + * Check if a sound is currently playing + * @param sound Sound to check + * @return true if playing, false otherwise + */ +bool is_sound_playing(sound sound); + +/** + * Set the volume of a sound + * @param sound Sound to modify + * @param volume Volume level (0.0f = silent, 1.0f = full volume) + */ +void set_sound_volume(sound sound, float volume); + +/** + * Set the pitch of a sound + * @param sound Sound to modify + * @param pitch Pitch multiplier (1.0f = normal, 2.0f = double speed/octave higher) + */ +void set_sound_pitch(sound sound, float pitch); + +/** + * Set the pan (stereo positioning) of a sound + * @param sound Sound to modify + * @param pan Pan position (0.0f = full left, 0.5f = center, 1.0f = full right) + */ +void set_sound_pan(sound sound, float pan); + +// ============================================================================= +// AUDIO STREAM MANAGEMENT +// ============================================================================= + +/** + * Create an audio stream for real-time audio processing + * @param sample_rate Sample rate in Hz + * @param sample_size Sample size in bits (8, 16, or 32) + * @param channels Number of channels (1 = mono, 2 = stereo) + * @return Audio stream structure + */ +audio_stream load_audio_stream(unsigned int sample_rate, unsigned int sample_size, unsigned int channels); + +/** + * Unload an audio stream and free resources + * @param stream Audio stream to unload + */ +void unload_audio_stream(audio_stream stream); + +/** + * Start playing an audio stream + * @param stream Audio stream to play + */ +void play_audio_stream(audio_stream stream); + +/** + * Pause an audio stream + * @param stream Audio stream to pause + */ +void pause_audio_stream(audio_stream stream); + +/** + * Resume a paused audio stream + * @param stream Audio stream to resume + */ +void resume_audio_stream(audio_stream stream); + +/** + * Check if an audio stream is currently playing + * @param stream Audio stream to check + * @return true if playing, false otherwise + */ +bool is_audio_stream_playing(audio_stream stream); + +/** + * Stop an audio stream + * @param stream Audio stream to stop + */ +void stop_audio_stream(audio_stream stream); + +/** + * Set the volume of an audio stream + * @param stream Audio stream to modify + * @param volume Volume level (0.0f = silent, 1.0f = full volume) + */ +void set_audio_stream_volume(audio_stream stream, float volume); + +/** + * Set the pitch of an audio stream + * @param stream Audio stream to modify + * @param pitch Pitch multiplier (1.0f = normal, 2.0f = double speed/octave higher) + */ +void set_audio_stream_pitch(audio_stream stream, float pitch); + +/** + * Set the pan (stereo positioning) of an audio stream + * @param stream Audio stream to modify + * @param pan Pan position (0.0f = full left, 0.5f = center, 1.0f = full right) + */ +void set_audio_stream_pan(audio_stream stream, float pan); + +/** + * Update an audio stream with new audio data + * Used for real-time audio processing and procedural audio + * @param stream Audio stream to update + * @param data Pointer to audio data (format should match stream parameters) + * @param frame_count Number of frames to update + */ +void update_audio_stream(audio_stream stream, const void *data, int frame_count); + +// ============================================================================= +// MUSIC MANAGEMENT +// ============================================================================= + +/** + * Load a music stream from file + * Suitable for background music and longer audio files + * Music is streamed from disk to save memory + * @param filename Path to audio file + * @return Music structure + */ +music load_music_stream(const char* filename); + +/** + * Check if a music structure is valid + * @param music Music structure to validate + * @return true if music is valid, false otherwise + */ +bool is_music_valid(music music); + +/** + * Unload music stream and free resources + * @param music Music structure to unload + */ +void unload_music_stream(music music); + +/** + * Start playing music + * @param music Music to play + */ +void play_music_stream(music music); + +/** + * Pause music playback + * @param music Music to pause + */ +void pause_music_stream(music music); + +/** + * Resume paused music + * @param music Music to resume + */ +void resume_music_stream(music music); + +/** + * Stop music playback + * @param music Music to stop + */ +void stop_music_stream(music music); + +/** + * Seek to a specific position in music + * @param music Music to seek + * @param position Position in seconds to seek to + */ +void seek_music_stream(music music, float position); + +/** + * Update music stream buffers + * Must be called regularly when playing music to maintain continuous playback + * @param music Music stream to update + */ +void update_music_stream(music music); + +/** + * Check if music is currently playing + * @param music Music to check + * @return true if playing, false otherwise + */ +bool is_music_stream_playing(music music); + +/** + * Set the volume of music + * @param music Music to modify + * @param volume Volume level (0.0f = silent, 1.0f = full volume) + */ +void set_music_volume(music music, float volume); + +/** + * Set the pitch of music + * @param music Music to modify + * @param pitch Pitch multiplier (1.0f = normal, 2.0f = double speed/octave higher) + */ +void set_music_pitch(music music, float pitch); + +/** + * Set the pan (stereo positioning) of music + * @param music Music to modify + * @param pan Pan position (0.0f = full left, 0.5f = center, 1.0f = full right) + */ +void set_music_pan(music music, float pan); + +/** + * Get the total length of music in seconds + * @param music Music to query + * @return Total length in seconds + */ +float get_music_time_length(music music); + +/** + * Get the current playback position in seconds + * @param music Music to query + * @return Current position in seconds + */ +float get_music_time_played(music music); + +#ifdef __cplusplus +} +#endif + +#endif // AUDIO_H diff --git a/libs/video.py b/libs/video.py index 20a5847..a104f5d 100644 --- a/libs/video.py +++ b/libs/video.py @@ -13,7 +13,8 @@ class VideoPlayer: self.is_finished_list = [False, False] self.video = VideoFileClip(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.video.audio.write_audiofile("cache/temp_audio.wav") + self.audio = audio.load_music_stream(Path("cache/temp_audio.wav")) self.buffer_size = 10 # Number of frames to keep in memory self.frame_buffer: dict[float, ray.Texture] = dict() # Dictionary to store frames {timestamp: texture} @@ -31,11 +32,11 @@ class VideoPlayer: return if self.is_finished_list[1]: return - if not audio.is_music_stream_playing(self.audio) and not self.audio_played: + if not self.audio_played: audio.play_music_stream(self.audio) self.audio_played = True audio.update_music_stream(self.audio) - self.is_finished_list[1] = not audio.is_music_stream_playing(self.audio) + self.is_finished_list[1] = audio.get_music_time_length(self.audio) <= audio.get_music_time_played(self.audio) def _load_frame(self, index: int): """Load a specific frame into the buffer""" @@ -138,3 +139,4 @@ class VideoPlayer: if audio.is_music_stream_playing(self.audio): audio.stop_music_stream(self.audio) audio.unload_music_stream(self.audio) + Path("cache/temp_audio.wav").unlink() diff --git a/scenes/game.py b/scenes/game.py index 179a541..bcf115b 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -111,7 +111,6 @@ class GameScreen: tex.unload_textures() if self.song_music is not None: audio.unload_music_stream(self.song_music) - self.song_music = None self.song_started = False self.end_ms = 0 self.movie = None @@ -156,9 +155,8 @@ class GameScreen: if self.tja is not None: if (self.current_ms >= self.tja.metadata.offset*1000 + self.start_delay - global_data.config["general"]["judge_offset"]) and not self.song_started: if self.song_music is not None: - if not audio.is_music_stream_playing(self.song_music): - audio.play_music_stream(self.song_music) - print(f"Song started at {self.current_ms}") + audio.play_music_stream(self.song_music) + print(f"Song started at {self.current_ms}") if self.movie is not None: self.movie.start(current_time) self.song_started = True @@ -170,6 +168,9 @@ class GameScreen: if self.background is not None: self.background.update(current_time, self.bpm, self.player_1.gauge) + if self.song_music is not None: + audio.update_music_stream(self.song_music) + self.player_1.update(self, current_time) self.song_info.update(current_time) self.result_transition.update(current_time) diff --git a/scenes/settings.py b/scenes/settings.py index a02e5db..2d7da14 100644 --- a/scenes/settings.py +++ b/scenes/settings.py @@ -26,6 +26,7 @@ class SettingsScreen: def on_screen_start(self): if not self.screen_init: + audio.list_host_apis() self.screen_init = True def on_screen_end(self): @@ -33,7 +34,7 @@ class SettingsScreen: save_config(self.config) global_data.config = self.config audio.close_audio_device() - audio.type = global_data.config["audio"]["device_type"] + audio.device_type = global_data.config["audio"]["device_type"] audio.init_audio_device() return "ENTRY" diff --git a/scenes/song_select.py b/scenes/song_select.py index 9e71222..c478722 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -407,8 +407,9 @@ class SongSelectScreen: if self.demo_song is None and get_current_ms() >= song.box.wait + (83.33*3): song.box.get_scores() 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) + self.demo_song = audio.load_music_stream(song.tja.metadata.wave) audio.play_music_stream(self.demo_song) + audio.seek_music_stream(self.demo_song, song.tja.metadata.demostart) audio.stop_sound(self.sound_bgm) if song.box.is_open: current_box = song.box