rustyray 0.2.1

High-level wrapper on top of the low-level rustyray-sys bindings
Documentation
use std::ffi::CString;
use std::path::Path;
use thiserror::Error;

use rustyray_sys::{
    audio::Sound as RaySound,
    ffi::{
        self, is_audio_device_ready, is_music_valid, is_sound_valid, is_wave_valid,
        load_music_stream_from_memory, load_sound_alias, load_sound_from_wave,
        load_wave_from_memory, unload_wave,
    },
};

use super::assets::{Asset, AssetLoader, AssetManager, Handle};

fn file_type_from_path(path: &str) -> Option<String> {
    let ext = Path::new(path).extension()?.to_str()?;
    let ext = ext.trim_start_matches('.').to_ascii_lowercase();
    Some(format!(".{ext}"))
}

#[derive(Debug, Error)]
pub enum SoundLoadError {
    #[error("file not found: {0}")]
    FileNotFound(String),
    #[error("audio device is not ready")]
    AudioDeviceNotReady,
    #[error("failed to decode sound")]
    DecodeFailed,
}

#[derive(Debug, Error)]
pub enum MusicLoadError {
    #[error("file not found: {0}")]
    FileNotFound(String),
    #[error("audio device is not ready")]
    AudioDeviceNotReady,
    #[error("failed to decode music")]
    DecodeFailed,
}

#[derive(Debug)]
pub struct Sound {
    inner: RaySound,
    is_alias: bool,
}

#[derive(Debug)]
pub struct Music {
    inner: rustyray_sys::audio::Music,
    paused: bool,
    _buffer: Vec<u8>,
}

impl Sound {
    pub(crate) fn from_wave(wave: rustyray_sys::audio::Wave) -> Result<Self, SoundLoadError> {
        let inner = unsafe { load_sound_from_wave(wave.clone()) };
        unsafe { unload_wave(wave) };
        if !unsafe { is_sound_valid(inner.clone()) } {
            return Err(SoundLoadError::DecodeFailed);
        }
        Ok(Self {
            inner,
            is_alias: false,
        })
    }

    pub(crate) fn from_alias(source: &Sound) -> Self {
        Self {
            inner: unsafe { load_sound_alias(source.inner.clone()) },
            is_alias: true,
        }
    }

    pub fn is_alias(&self) -> bool {
        self.is_alias
    }

    pub fn play(&self) {
        unsafe {
            ffi::play_sound(self.inner.clone());
        }
    }

    pub fn alias(manager: &mut AssetManager, source: &Handle<Sound>) -> Option<Handle<Sound>> {
        let source = manager.get(source)?;
        if source.is_alias() {
            return None;
        }
        Some(manager.insert(Self::from_alias(source)))
    }
}

impl Music {
    pub fn play(&mut self) {
        self.paused = false;
        unsafe {
            ffi::play_music_stream(self.inner.clone());
        }
    }

    #[inline]
    pub fn is_playing(&self) -> bool {
        unsafe { ffi::is_music_stream_playing(self.inner.clone()) }
    }

    #[inline]
    pub fn is_paused(&self) -> bool {
        self.paused
    }

    #[inline]
    pub fn toggle(&mut self) {
        if self.is_paused() {
            self.resume();
        } else {
            self.pause();
        }
    }

    pub fn pause(&mut self) {
        self.paused = true;
        unsafe {
            ffi::pause_music_stream(self.inner.clone());
        }
    }

    pub fn stop(&mut self) {
        self.paused = false;
        unsafe {
            ffi::stop_music_stream(self.inner.clone());
        }
    }

    pub fn resume(&mut self) {
        self.paused = false;
        unsafe {
            ffi::resume_music_stream(self.inner.clone());
        }
    }

    #[inline]
    pub fn restart(&mut self) {
        self.stop();
        self.play();
    }

    #[inline]
    pub fn played(&self) -> f32 {
        unsafe { ffi::get_music_time_played(self.inner.clone()) }
    }

    #[inline]
    pub fn length(&self) -> f32 {
        unsafe { ffi::get_music_time_length(self.inner.clone()) }
    }

    #[inline]
    pub fn update(&self) {
        unsafe {
            ffi::update_music_stream(self.inner.clone());
        }
    }

    #[inline]
    pub fn pitch(&self, pitch: f32) {
        unsafe {
            ffi::set_music_pitch(self.inner.clone(), pitch);
        }
    }

    #[inline]
    pub fn is_looping(&self) -> bool {
        self.inner.looping
    }

    pub fn looping(&mut self, looping: bool) {
        self.inner.looping = looping;
    }
}

impl Drop for Sound {
    fn drop(&mut self) {
        if self.is_alias {
            self.inner.clone().unload_alias();
        } else {
            self.inner.clone().unload();
        }
    }
}

impl Drop for Music {
    fn drop(&mut self) {
        self.inner.clone().unload();
    }
}

impl Asset for Sound {}

impl AssetLoader for Sound {
    type Key = String;
    type Error = SoundLoadError;

    async fn load(path: Self::Key) -> Result<Self, Self::Error> {
        if !unsafe { is_audio_device_ready() } {
            return Err(SoundLoadError::AudioDeviceNotReady);
        }

        let file_type =
            file_type_from_path(&path).ok_or(SoundLoadError::FileNotFound(path.clone()))?;

        let bytes = async_fs::read(&path)
            .await
            .map_err(|_| SoundLoadError::FileNotFound(path.clone()))?;

        let file_type = CString::new(file_type).map_err(|_| SoundLoadError::DecodeFailed)?;
        let wave = unsafe {
            load_wave_from_memory(file_type.as_ptr(), bytes.as_ptr(), bytes.len() as i32)
        };

        if !unsafe { is_wave_valid(wave.clone()) } {
            return Err(SoundLoadError::DecodeFailed);
        }

        Self::from_wave(wave)
    }
}

impl Asset for Music {}

impl AssetLoader for Music {
    type Key = String;
    type Error = MusicLoadError;

    async fn load(path: Self::Key) -> Result<Self, Self::Error> {
        if !unsafe { is_audio_device_ready() } {
            return Err(MusicLoadError::AudioDeviceNotReady);
        }

        let file_type =
            file_type_from_path(&path).ok_or(MusicLoadError::FileNotFound(path.clone()))?;

        let bytes = async_fs::read(&path)
            .await
            .map_err(|_| MusicLoadError::FileNotFound(path.clone()))?;

        let file_type = CString::new(file_type).map_err(|_| MusicLoadError::DecodeFailed)?;
        let inner = unsafe {
            load_music_stream_from_memory(file_type.as_ptr(), bytes.as_ptr(), bytes.len() as i32)
        };

        if !unsafe { is_music_valid(inner.clone()) } {
            return Err(MusicLoadError::DecodeFailed);
        }

        Ok(Self {
            inner,
            paused: false,
            _buffer: bytes,
        })
    }
}