view python/src/audio.rs @ 792:11bc22bad1bf default tip

python: Replace the image crate with png We weren’t using any of its features anyway, so the png crate is exactly what we need, without the many heavy dependencies of image. https://github.com/image-rs/image-png/pull/670 will eventually make it even faster to build.
author Link Mauve <linkmauve@linkmauve.fr>
date Sat, 17 Jan 2026 22:22:25 +0100
parents ec1e06402a97
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();
    }
}