view python/src/audio.rs @ 783:ec1e06402a97

Replace SDL2_mixer with the kira crate
author Link Mauve <linkmauve@linkmauve.fr>
date Fri, 21 Nov 2025 10:21:59 +0100
parents
children
line wrap: on
line source

use kira::sound::static_sound::StaticSoundData;
use kira::sound::streaming::{StreamingSoundData, StreamingSoundHandle};
use kira::sound::FromFileError;
use kira::{AudioManager, AudioManagerSettings, Tween};
use pyo3::prelude::*;
use std::collections::HashMap;
use std::io::Cursor;
use std::path::PathBuf;

#[pyclass(module = "libtouhou")]
pub struct Audio {
    loader: Py<super::Loader>,
    manager: AudioManager,
    cache: HashMap<String, StaticSoundData>,
    bgms: [Option<(String, String)>; 4],
    current_music: Option<StreamingSoundHandle<FromFileError>>,
}

#[pymethods]
impl Audio {
    #[new]
    fn new(loader: Py<super::Loader>, bgms: [Option<(String, String)>; 4]) -> Audio {
        let manager =
            AudioManager::<kira::DefaultBackend>::new(AudioManagerSettings::default()).unwrap();
        let cache = HashMap::new();
        Audio {
            loader,
            manager,
            cache,
            bgms,
            current_music: None,
        }
    }

    fn play_bgm(&mut self, py: Python, number: usize) {
        let Some((_name, filename)) = &self.bgms[number] else {
            eprintln!("Unspecified bgm number {number}");
            return;
        };

        // Load the loop points corresponding to this bgm.
        let mut filename = PathBuf::from(filename);
        filename.set_extension("pos");
        let loader = self.loader.borrow(py);
        let loop_points = loader
            .get_loop_points(filename.file_name().unwrap().to_str().unwrap())
            .unwrap();

        // Then try to open the music file.
        filename.set_extension("*");
        let path = loader
            .game_dir
            .clone()
            .and_then(|dir| Some(dir.join(&filename)))
            .unwrap_or(filename);
        let mut music = None;
        for path in glob::glob(path.to_str().unwrap())
            .unwrap()
            .map(Result::unwrap)
            .map(PathBuf::from)
        {
            match StreamingSoundData::from_file(&path) {
                Ok(sound) => {
                    music = Some(sound);
                    break;
                }
                Err(err) => {
                    eprintln!("Error while opening {path:?} as a music file: {err:?}");
                    continue;
                }
            }
        }
        let Some(music) = music else {
            eprintln!("Unable to find bgm, let’s keep the previous one playing…");
            return;
        };

        // Stop the previous playing one.
        if let Some(current_music) = &mut self.current_music {
            current_music.stop(Tween::default());
        }

        // And now we can start playing the new one!
        let mut current_music = self.manager.play(music).unwrap();
        // TODO: Fetch the sample rate from the file, instead of hardcoding it.
        current_music.set_loop_region(
            (loop_points.start as f64 / 44100.0)..(loop_points.end as f64 / 44100.0),
        );
        self.current_music = Some(current_music);
    }

    fn play(&mut self, py: Python, name: &str) {
        let sound = self.cache.entry(name.to_string()).or_insert_with(|| {
            let loader = self.loader.borrow(py);
            let bytes = loader.get_file_internal(name).unwrap();
            let cursor = Cursor::new(bytes);
            StaticSoundData::from_cursor(cursor).unwrap()
        });
        self.manager.play(sound.clone()).unwrap();
    }
}