diff --git a/taiko-game/Cargo.toml b/taiko-game/Cargo.toml index a08a3ac..bd6590c 100644 --- a/taiko-game/Cargo.toml +++ b/taiko-game/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "taiko-game" description = "A taiko game written in Rust." -version = "0.0.2" +version = "0.0.3" license = "MIT" authors = ["JacobLinCool "] homepage = "https://github.com/JacobLinCool/rhythm-rs" @@ -40,10 +40,9 @@ tokio-util = "0.7.9" tracing = "0.1.37" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] } -rodio = "0.17.3" -cpal = "0.15.3" anyhow = "1.0.82" encoding_rs = "0.8.34" +kira = "0.8.7" [build-dependencies] vergen = { version = "8.3.1", features = [ "build", "git", "gitoxide", "cargo" ]} diff --git a/taiko-game/src/app.rs b/taiko-game/src/app.rs index be7f3f3..5f84431 100644 --- a/taiko-game/src/app.rs +++ b/taiko-game/src/app.rs @@ -1,15 +1,21 @@ use color_eyre::eyre::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use kira::{ + manager::{backend::DefaultBackend, AudioManager, AudioManagerSettings}, + sound::static_sound::{StaticSoundData, StaticSoundHandle, StaticSoundSettings}, + track::{TrackBuilder, TrackHandle}, + tween::Tween, +}; use ratatui::prelude::Rect; use ratatui::widgets::canvas::{Canvas, Rectangle}; use ratatui::{prelude::*, widgets::*}; use rhythm_core::Rhythm; -use rodio::{source::Source, Decoder, OutputStream, Sink}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use serde::{de, Deserialize, Serialize}; +use std::{collections::HashMap, io::Cursor, time::Duration}; use std::{fs, io, path::PathBuf, time::Instant}; use taiko_core::constant::{COURSE_TYPE, GUAGE_FULL_THRESHOLD, GUAGE_PASS_THRESHOLD, RANGE_GREAT}; use tokio::sync::mpsc; +use tracing::instrument::WithSubscriber; use rhythm_core::Note; use taiko_core::{ @@ -19,9 +25,8 @@ use tja::{TJACourse, TJAParser, TaikoNote, TaikoNoteType, TaikoNoteVariant, TJA} use crate::assets::{DON_WAV, KAT_WAV}; use crate::cli::AppArgs; -use crate::sound::{SoundData, SoundPlayer}; use crate::utils::read_utf8_or_shiftjis; -use crate::{action::Action, sound, tui}; +use crate::{action::Action, tui}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Page { @@ -37,9 +42,10 @@ pub struct App { song_selector: ListState, course: Option, course_selector: ListState, - player: sound::RodioSoundPlayer, - sounds: HashMap, - music: Option, + player: AudioManager, + sounds: HashMap, + effect_track: TrackHandle, + playing: Option, pending_quit: bool, pending_suspend: bool, ticks: Vec, @@ -69,14 +75,23 @@ impl App { return Err(io::Error::new(io::ErrorKind::NotFound, "No songs found").into()); } + let mut player = AudioManager::::new(AudioManagerSettings::default())?; + let effect_track = player.add_sub_track(TrackBuilder::new())?; + let mut sounds = HashMap::new(); sounds.insert( "don".to_owned(), - SoundData::load_from_buffer(DON_WAV.to_vec())?, + StaticSoundData::from_cursor( + Cursor::new(DON_WAV.to_vec()), + StaticSoundSettings::default(), + )?, ); sounds.insert( "kat".to_owned(), - SoundData::load_from_buffer(KAT_WAV.to_vec())?, + StaticSoundData::from_cursor( + Cursor::new(KAT_WAV.to_vec()), + StaticSoundSettings::default(), + )?, ); Ok(Self { @@ -86,9 +101,10 @@ impl App { song_selector, course: None, course_selector, - player: sound::RodioSoundPlayer::new().unwrap(), + player, + effect_track, sounds, - music: None, + playing: None, pending_quit: false, pending_suspend: false, ticks: Vec::new(), @@ -124,6 +140,33 @@ impl App { } } + fn music_path(&self) -> Result { + if let Some(song) = &self.song { + let fallback_ogg = self.songs[self.song_selector.selected().unwrap()] + .1 + .with_extension("ogg"); + let rel = song + .header + .wave + .clone() + .filter(|s| !s.is_empty()) + .unwrap_or( + fallback_ogg + .file_name() + .unwrap() + .to_string_lossy() + .to_string(), + ); + Ok(self.songs[self.song_selector.selected().unwrap()] + .1 + .parent() + .unwrap() + .join(rel)) + } else { + Err(io::Error::new(io::ErrorKind::NotFound, "No song is available").into()) + } + } + async fn enter_course_menu(&mut self) -> Result<()> { let selected = self.song_selector.selected().unwrap_or(0); let content = read_utf8_or_shiftjis(&self.songs[selected].1).unwrap(); @@ -132,44 +175,27 @@ impl App { .parse(content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; song.courses.sort_by_key(|course| course.course); - - let fallback_ogg = self.songs[selected].1.with_extension("ogg"); - let rel = song - .header - .wave - .clone() - .filter(|s| !s.is_empty()) - .unwrap_or( - fallback_ogg - .file_name() - .unwrap() - .to_string_lossy() - .to_string(), - ); - let path = self.songs[selected].1.parent().unwrap().join(rel); - - self.music.replace(sound::SoundData::load_from_path(path)?); self.song.replace(song); - self.player - .play_music_from( - self.music.as_ref().unwrap(), - self.song - .clone() - .unwrap() - .header - .demostart - .unwrap_or(0.0) - .into(), - ) - .await; + let path = self.music_path()?; + + let demostart = self.song.clone().unwrap().header.demostart.unwrap_or(0.0) as f64; + let music = + StaticSoundData::from_file(path, StaticSoundSettings::new().loop_region(demostart..))?; + self.playing.replace(self.player.play(music)?); + self.player.resume(Tween::default())?; Ok(()) } - async fn leave_course_menu(&mut self) { + async fn leave_course_menu(&mut self) -> Result<()> { self.song.take(); - self.music.take(); - self.player.stop_music().await; + if let Some(mut playing) = self.playing.take() { + playing.stop(Tween { + duration: Duration::from_secs_f32(0.5), + ..Default::default() + })?; + } + Ok(()) } async fn enter_game(&mut self) -> Result<()> { @@ -205,16 +231,28 @@ impl App { self.course.replace(course); self.taiko.replace(DefaultTaikoEngine::new(source)); - self.player.stop_music().await; - self.player.load_music(self.music.as_ref().unwrap()).await; + if let Some(mut playing) = self.playing.take() { + playing.stop(Tween::default())?; + } + let music = StaticSoundData::from_file( + self.songs[self.song_selector.selected().unwrap()] + .1 + .with_extension("ogg"), + StaticSoundSettings::default(), + )?; + self.player.pause(Tween::default())?; + self.playing.replace(self.player.play(music)?); Ok(()) } - async fn leave_game(&mut self) { + async fn leave_game(&mut self) -> Result<()> { self.taiko.take(); self.course.take(); - self.player.stop_music().await; + if let Some(mut playing) = self.playing.take() { + playing.stop(Tween::default())?; + } + Ok(()) } async fn handle_key_event( @@ -233,10 +271,10 @@ impl App { } => match self.page() { Page::SongSelect => action_tx.send(Action::Quit)?, Page::CourseSelect => { - self.leave_course_menu().await; + self.leave_course_menu().await?; } Page::Game => { - self.leave_game().await; + self.leave_game().await?; self.enter_course_menu().await?; } }, @@ -330,7 +368,7 @@ impl App { .. } => { // don - self.player.play_effect(&self.sounds["don"]).await; + self.player.play(self.sounds["don"].clone())?; if self.taiko.is_some() { self.hit.replace(Hit::Don); self.last_hit_type.replace(Hit::Don); @@ -374,7 +412,7 @@ impl App { .. } => { // kat - self.player.play_effect(&self.sounds["kat"]).await; + self.player.play(self.sounds["kat"].clone())?; if self.taiko.is_some() { self.hit.replace(Hit::Kat); self.last_hit_type.replace(Hit::Kat); @@ -395,7 +433,13 @@ impl App { tui.enter()?; loop { - let player_time = self.player.get_music_time().await; + let player_time = if self.enter_countdown <= 0 { + self.enter_countdown as f64 / self.args.tps as f64 + } else if let Some(music) = &self.playing { + music.position() + } else { + 0.0 + }; if let Some(e) = tui.next().await { match e { tui::Event::Quit => action_tx.send(Action::Quit)?, @@ -421,15 +465,10 @@ impl App { if self.enter_countdown < 0 { self.enter_countdown += 1; } else if self.enter_countdown == 0 { - self.player.play_loaded_music().await; + self.player.resume(Tween::default())?; self.enter_countdown = 1; } - let player_time = if self.enter_countdown <= 0 { - self.enter_countdown as f64 / self.args.tps as f64 - } else { - self.player.get_music_time().await - }; if self.taiko.is_some() { let taiko = self.taiko.as_mut().unwrap(); @@ -442,7 +481,7 @@ impl App { if note.variant == TaikoNoteVariant::Don { if (note.start - player_time).abs() < RANGE_GREAT { - self.player.play_effect(&self.sounds["don"]).await; + self.player.play(self.sounds["don"].clone())?; self.hit.replace(Hit::Don); self.last_hit_type.replace(Hit::Don); self.hit_show = self.args.tps as i32 / 40; @@ -452,7 +491,7 @@ impl App { } } else if note.variant == TaikoNoteVariant::Kat { if (note.start - player_time).abs() < RANGE_GREAT { - self.player.play_effect(&self.sounds["kat"]).await; + self.player.play(self.sounds["kat"].clone())?; self.hit.replace(Hit::Kat); self.last_hit_type.replace(Hit::Kat); self.hit_show = self.args.tps as i32 / 40; @@ -463,7 +502,7 @@ impl App { } else if note.variant == TaikoNoteVariant::Both { if player_time > note.start { if self.auto_play_combo_sleep == 0 { - self.player.play_effect(&self.sounds["don"]).await; + self.player.play(self.sounds["don"].clone())?; self.hit.replace(Hit::Don); self.last_hit_type.replace(Hit::Don); self.hit_show = self.args.tps as i32 / 40; @@ -509,11 +548,6 @@ impl App { } else { 0.0 }; - let player_time = if self.enter_countdown <= 0 { - self.enter_countdown as f64 / self.args.tps as f64 - } else { - self.player.get_music_time().await - }; let song_name: Option = if self.song.is_none() { None diff --git a/taiko-game/src/main.rs b/taiko-game/src/main.rs index fd46424..941fc07 100644 --- a/taiko-game/src/main.rs +++ b/taiko-game/src/main.rs @@ -6,7 +6,6 @@ pub mod action; pub mod app; pub mod assets; pub mod cli; -pub mod sound; pub mod tui; pub mod utils; diff --git a/taiko-game/src/sound.rs b/taiko-game/src/sound.rs deleted file mode 100644 index 387c9f3..0000000 --- a/taiko-game/src/sound.rs +++ /dev/null @@ -1,218 +0,0 @@ -use rodio::{ - self, - dynamic_mixer::{mixer, DynamicMixer, DynamicMixerController}, - Decoder, Source, -}; -use std::{fs::File, io::BufReader, path::Path}; - -pub struct SoundData { - buffer: Vec, - sample_rate: u32, - channels: u16, -} - -impl SoundData { - pub fn load_from_path( - file_path: impl AsRef, - ) -> Result { - let file = File::open(&file_path) - .expect(format!("Failed to open file: {:?}", file_path.as_ref()).as_str()); - Self::load_from_file(file) - } - - pub fn load_from_file(file: File) -> Result { - let decoder = Decoder::new(BufReader::new(file))?; - let decoder = decoder.convert_samples::(); - let channels = decoder.channels(); - let sample_rate = decoder.sample_rate(); - let buffer: Vec = decoder.collect(); - Ok(Self::load(buffer, sample_rate, channels)) - } - - pub fn load_from_buffer(buffer: Vec) -> Result { - let decoder = Decoder::new(std::io::Cursor::new(buffer))?; - let decoder = decoder.convert_samples::(); - let channels = decoder.channels(); - let sample_rate = decoder.sample_rate(); - let buffer: Vec = decoder.collect(); - Ok(Self::load(buffer, sample_rate, channels)) - } - - pub fn load(data: Vec, sample_rate: impl Into, channels: impl Into) -> Self { - Self { - buffer: data, - sample_rate: sample_rate.into(), - channels: channels.into(), - } - } -} - -/// A sound player that plays sound effects and background music. -/// Which supports playing multiple sounds at the same time. -pub(crate) trait SoundPlayer { - /// Plays a sound effect. - async fn play_effect(&mut self, effect: &SoundData); - - /// Loads a background music. - async fn load_music(&mut self, music: &SoundData); - - /// Plays a background music after loading it. - async fn play_loaded_music(&mut self); - - /// Plays a background music. - async fn play_music(&mut self, music: &SoundData); - - /// Plays a background music from a specific time. - async fn play_music_from(&mut self, music: &SoundData, time: f64); - - /// Get the paused state of the background music. - async fn is_music_paused(&self) -> bool; - - /// Stops the background music. - async fn stop_music(&mut self); - - /// Pauses the background music. - async fn pause_music(&mut self); - - /// Resumes the background music. - async fn resume_music(&mut self); - - /// Gets the current playing time of the background music. - async fn get_music_time(&self) -> f64; - - /// Sets the volume of the sound player. - async fn set_volume(&mut self, volume: f32); - - /// Gets the volume of the sound player. - async fn get_volume(&self) -> f32; -} - -use rodio::{OutputStream, Sink}; -use std::sync::{Arc, Mutex}; -use tokio::{sync::Mutex as AsyncMutex, time::Instant}; - -pub struct RodioSoundPlayer { - sink: Arc>, - controller: Arc>, - output_stream: OutputStream, - music_start: Option, - music_time: f64, -} - -impl RodioSoundPlayer { - pub fn new() -> anyhow::Result { - let (output_stream, stream_handle) = OutputStream::try_default()?; - let sink = Sink::try_new(&stream_handle)?; - let (controller, mixer) = mixer(2, 44100); - Ok(Self { - sink: Arc::new(AsyncMutex::new(sink)), - controller, - output_stream, - music_start: None, - music_time: 0.0, - }) - } -} - -impl SoundPlayer for RodioSoundPlayer { - async fn play_effect(&mut self, effect: &SoundData) { - let source = rodio::buffer::SamplesBuffer::new( - effect.channels, - effect.sample_rate, - effect.buffer.clone(), - ); - self.controller.add(source); - } - - async fn load_music(&mut self, music: &SoundData) { - let sink = self.sink.lock().await; - - let source = rodio::buffer::SamplesBuffer::new( - music.channels, - music.sample_rate, - music.buffer.clone(), - ); - - let (controller, mixer) = mixer::(2, 44100); - sink.append(mixer); - self.controller = controller; - self.controller.add(source); - sink.pause(); - self.music_start = Some(Instant::now()); - self.music_time = 0.0; - } - - async fn play_loaded_music(&mut self) { - self.resume_music().await - } - - async fn play_music(&mut self, music: &SoundData) { - self.load_music(music).await; - self.play_loaded_music().await; - } - - async fn play_music_from(&mut self, music: &SoundData, time: f64) { - let sink = self.sink.lock().await; - let data = music - .buffer - .clone() - .into_iter() - .skip((time * music.sample_rate as f64 * music.channels as f64) as usize) - .collect::>(); - - let source = rodio::buffer::SamplesBuffer::new(music.channels, music.sample_rate, data); - - let (controller, mixer) = mixer::(2, 44100); - sink.append(mixer); - self.controller = controller; - self.controller.add(source); - - sink.play(); - self.music_start = Some(Instant::now()); - self.music_time = time; - } - - async fn is_music_paused(&self) -> bool { - let sink = self.sink.lock().await; - sink.is_paused() - } - - async fn stop_music(&mut self) { - let sink = self.sink.lock().await; - sink.stop(); - self.music_start = None; - } - - async fn pause_music(&mut self) { - let sink = self.sink.lock().await; - sink.pause(); - if let Some(start) = self.music_start { - self.music_time += start.elapsed().as_secs_f64(); - } - } - - async fn resume_music(&mut self) { - let sink = self.sink.lock().await; - sink.play(); - self.music_start = Some(Instant::now()); - } - - async fn get_music_time(&self) -> f64 { - let current = if let Some(start) = self.music_start { - start.elapsed().as_secs_f64() - } else { - 0.0 - }; - self.music_time + current - } - - async fn set_volume(&mut self, volume: f32) { - let sink = self.sink.lock().await; - sink.set_volume(volume); - } - - async fn get_volume(&self) -> f32 { - let sink = self.sink.lock().await; - sink.volume() - } -}