From b71669f55383660ed4fa21d7a5769954d0cfde27 Mon Sep 17 00:00:00 2001 From: aprzn Date: Wed, 1 Mar 2023 17:01:38 -0500 Subject: [PATCH] cleaning up, working on audio playback, screw chrono duration, it sucks --- Cargo.lock | 10 ++++ Cargo.toml | 1 + src/gd.rs | 11 ++-- src/main.rs | 144 ++++++++++++++++++++++++++++++--------------------- src/music.rs | 4 +- 5 files changed, 101 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20164d4..71b4544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1624,6 +1624,15 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "mp3-duration" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348bdc7300502f0801e5b57c448815713cd843b744ef9bda252a2698fdf90a0f" +dependencies = [ + "thiserror", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -2069,6 +2078,7 @@ dependencies = [ "itertools", "log", "md5", + "mp3-duration", "ordered-float", "reqwest", "rodio", diff --git a/Cargo.toml b/Cargo.toml index 5020889..e6bf489 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ itertools = "0.10.5" directories = "4.0.1" simplelog = "0.12.0" log = "0.4.17" +mp3-duration = "0.1.10" diff --git a/src/gd.rs b/src/gd.rs index 55e7c0b..8242cd2 100644 --- a/src/gd.rs +++ b/src/gd.rs @@ -1,6 +1,6 @@ use crate::music::Lines; use base64::engine::{general_purpose::URL_SAFE, Engine}; -use chrono::Duration; +use std::time::Duration; use eframe::egui::TextFormat; use eframe::epaint::text::LayoutJob; use flate2::read::GzDecoder; @@ -79,7 +79,7 @@ impl InnerLevel { log::info!("{} could not be parsed", color_code); return; }; - let Ok(duration) = timestamp.parse::().map(|t| Duration::nanoseconds((t * 1_000_000_000f64) as i64)) else { + let Ok(duration) = timestamp.parse::().map(|t| Duration::from_secs_f64(t)) else { log::info!("{} could not be parsed", timestamp); return; }; @@ -103,16 +103,13 @@ impl Song { match self { Self::Newgrounds { id } => { let mut out = SongResponse(Default::default()); - dbg!(req::ClientBuilder::new() + req::ClientBuilder::new() .user_agent("") .build()? .post("http://www.boomlings.com/database/getGJSongInfo.php") - // .body(dbg!(format!( - // r#" {{ "secret": "Wmfd2893gb7", "songID": {} }} "#, id - // ))) .form(&[("songID", id.to_string().as_ref()), ("secret", "Wmfd2893gb7")]) .send()? - .text()?) + .text()? .split("~|~") .array_chunks() .try_for_each(|[id, value]| -> Result<(), SongRequestError> { diff --git a/src/main.rs b/src/main.rs index c9ccb65..c3fcfc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,8 @@ use std::fs::File; use std::io::Write; use std::mem; use thiserror::Error; +use std::time; +use rodio::source::Source; fn project_dirs() -> directories::ProjectDirs { directories::ProjectDirs::from("xyz", "interestingzinc", "pipedash").expect("Home dir missing?") @@ -60,9 +62,9 @@ struct WizardEditor { } struct EditorState { - scroll_pos: f32, - pts_per_second: f32, // zoom level - subdivisions: f32, + scroll_pos: f64, + pts_per_second: f64, // zoom level + subdivisions: u32, } struct GdlData { @@ -74,9 +76,9 @@ struct GdlData { } struct WizardData { - green_lines: music::Lines, - orange_lines: music::Lines, - yellow_lines: music::Lines, + green_lines: music::Lines, + orange_lines: music::Lines, + yellow_lines: music::Lines, beat_rate: Option, time_signatures: Option, } @@ -84,7 +86,10 @@ struct WizardData { struct Song { name: String, id: i64, - decoder: rodio::Decoder, + buffer: rodio::buffer::SamplesBuffer, + length: time::Duration, + stream: rodio::OutputStream, + sink: rodio::Sink, } #[derive(Error, Debug)] @@ -101,26 +106,31 @@ enum SongError { NgServerError(#[from] reqwest::Error), #[error("Missing download link")] MissingLink, + #[error("Unable to create audio stream")] + StreamError(#[from] rodio::StreamError), + #[error("Unable to create audio sink")] + SinkError(#[from] rodio::PlayError), } struct BeatRateWidget<'a> { state: &'a mut EditorState, beat_rate: Option<&'a mut music::BeatRate>, + song: &'a Song, // for waveform } + struct TimeSignatureWidget<'a> { state: &'a mut EditorState, time_signatures: Option<&'a mut music::TimeSignature>, + song: &'a Song, // for waveform } + struct LinesWidget<'a, T = music::BeatPosition> where T: Ord { state: &'a mut EditorState, lines: &'a mut music::Lines, color: Color, -} -struct WaveformWidget<'a> { - state: &'a mut EditorState, - song: &'a Song, + song: &'a Song, // for waveform } fn allocate_editor_space(ui: &mut egui::Ui) -> (egui::Rect, egui::Response) { @@ -141,25 +151,30 @@ impl From for eframe::epaint::Color32 { impl EditorMode { pub fn display(&mut self, ui: &mut egui::Ui) { + let ctx = ui.ctx(); match self { EditorMode::RhythmWizard { editor, song } => { + ui.label("Rhythm Wizard"); }, EditorMode::Full { editor, song } => { - ui.add(editor.time_signature_widget()); - ui.add(editor.beat_rate_widget()); - ui.add(editor.lines_widget(Color::Green)); - ui.add(editor.lines_widget(Color::Orange)); - ui.add(editor.lines_widget(Color::Yellow)); - ui.add(editor.waveform_widget(song)); + editor.handle_keyboard_input(ctx, song); + ui.label("Editor"); + ui.add(editor.time_signature_widget(song)); + ui.add(editor.beat_rate_widget(song)); + ui.add(editor.lines_widget(Color::Green, song)); + ui.add(editor.lines_widget(Color::Orange, song)); + ui.add(editor.lines_widget(Color::Yellow, song)); + }, + EditorMode::NoSong => { + ui.label("No song to edit"); }, - EditorMode::NoSong => {}, } } } impl Default for EditorState { fn default() -> Self { - EditorState { scroll_pos: 0f32, pts_per_second: 10f32, subdivisions: 4f32 } + EditorState { scroll_pos: 0.0, pts_per_second: 10.0, subdivisions: 4 } } } @@ -169,7 +184,7 @@ impl Default for GdlData { green_lines: Default::default(), orange_lines: Default::default(), yellow_lines: Default::default(), - beat_rate: music::StaticBeatRate::from_bpm(120f32).into(), + beat_rate: music::StaticBeatRate::from_bpm(120.0).into(), time_signatures: music::StaticTimeSignature::new(4, 4).into(), } } @@ -203,10 +218,16 @@ impl Song { } (Err(err), Err(_)) => return Err(err.into()), }; + + let length = mp3_duration::from_file(&file).unwrap(); let decoder = rodio::Decoder::new_mp3(file)?; + let buffer = rodio::buffer::SamplesBuffer::new(decoder.channels(), decoder.sample_rate(), decoder.collect::>()); + + let (stream, stream_handle) = rodio::OutputStream::try_default()?; + let sink = rodio::Sink::try_new(&stream_handle)?; - Ok(Self { name, id, decoder }) + Ok(Self { name, id, buffer, length, stream, sink }) } else { Err(SongError::NotNewgrounds) } @@ -214,21 +235,23 @@ impl Song { } impl Editor { - pub fn beat_rate_widget(&mut self) -> BeatRateWidget { + pub fn beat_rate_widget<'a>(&'a mut self, song: & 'a mut Song) -> BeatRateWidget { BeatRateWidget { state: &mut self.state, beat_rate: Some(&mut self.data.beat_rate), + song, } } - pub fn time_signature_widget(&mut self) -> TimeSignatureWidget { + pub fn time_signature_widget<'a>(&'a mut self, song: &'a mut Song) -> TimeSignatureWidget { TimeSignatureWidget { state: &mut self.state, time_signatures: Some(&mut self.data.time_signatures), + song, } } - pub fn lines_widget(&mut self, col: Color) -> LinesWidget { + pub fn lines_widget<'a>(&'a mut self, col: Color, song: &'a mut Song) -> LinesWidget { LinesWidget { state: &mut self.state, lines: match col { @@ -237,33 +260,57 @@ impl Editor { Color::Orange => &mut self.data.orange_lines, }, color: col, + song, } } - pub fn waveform_widget<'a>(&'a mut self, song: &'a Song) -> WaveformWidget { - WaveformWidget { - state: &mut self.state, - song, - } + /// points in width of entire song + fn song_width(&self, song: &Song) -> f64 { + song.length.as_secs_f64() * self.state.pts_per_second + } + + fn play_pause(&self, song: &Song) { + todo!("toggle song playback") + } + + fn handle_keyboard_input(&mut self, ctx: &egui::Context, song: &Song) { + use egui::Key; + use egui::Event; + ctx.input().events + .iter() + .for_each(|ev| match ev { + Event::Key { key: Key::ArrowLeft, pressed: true, modifiers } => self.scroll(-5.0, song), + Event::Key { key: Key::ArrowRight, pressed: true, modifiers } => self.scroll(5.0, song), + Event::Key { key: Key::Space, pressed: true, modifiers } => self.play_pause(song), + _ => (), + }); + } + + fn scroll(&mut self, pts: f64, song: &Song) { + self.state.scroll_pos += pts; + self.state.scroll_pos.clamp(0f64, self.song_width(song)); } + } impl WizardEditor { - pub fn beat_rate_widget(&mut self) -> BeatRateWidget { + pub fn beat_rate_widget<'a>(&'a mut self, song: &'a mut Song) -> BeatRateWidget { BeatRateWidget { state: &mut self.state, beat_rate: self.data.beat_rate.as_mut(), + song, } } - pub fn time_signature_widget(&mut self) -> TimeSignatureWidget { + pub fn time_signature_widget<'a>(&'a mut self, song: &'a mut Song) -> TimeSignatureWidget { TimeSignatureWidget { state: &mut self.state, time_signatures: self.data.time_signatures.as_mut(), + song, } } - pub fn lines_widget(&mut self, col: Color) -> LinesWidget { + pub fn lines_widget<'a>(&'a mut self, col: Color, song: &'a mut Song) -> LinesWidget { LinesWidget { state: &mut self.state, lines: match col { @@ -272,12 +319,6 @@ impl WizardEditor { Color::Orange => &mut self.data.orange_lines, }, color: col, - } - } - - pub fn waveform_widget<'a>(&'a mut self, song: &'a Song) -> WaveformWidget { - WaveformWidget { - state: &mut self.state, song, } } @@ -308,7 +349,7 @@ impl<'a> egui::Widget for BeatRateWidget<'a> { // draw widget if ui.is_rect_visible(rect) { ui.painter() - .rect_filled(rect, 0f32, eframe::epaint::Color32::from_gray(0)); + .rect_filled(rect, 0.0, eframe::epaint::Color32::from_gray(0)); } res } @@ -325,7 +366,7 @@ impl<'a> egui::Widget for TimeSignatureWidget<'a> { // 4. draw widget if ui.is_rect_visible(rect) { ui.painter() - .rect_filled(rect, 0f32, eframe::epaint::Color32::from_gray(0)); + .rect_filled(rect, 0.0, eframe::epaint::Color32::from_gray(0)); } res } @@ -342,24 +383,7 @@ impl<'a> egui::Widget for LinesWidget<'a> { // 4. draw widget if ui.is_rect_visible(rect) { ui.painter() - .rect_filled(rect, 0f32, eframe::epaint::Color32::from_gray(0)); - } - res - } -} - -impl<'a> egui::Widget for WaveformWidget<'a> { - fn ui(self, ui: &mut egui::Ui) -> egui::Response { - // 1. choose size - let max_rect = ui.max_rect(); - let preferred_size = egui::Vec2::new(max_rect.size().x, 60.0); - // 2. allocate space - let (rect, res) = ui.allocate_exact_size(preferred_size, egui::Sense::click_and_drag()); - // 3. handle interactions - // 4. draw widget - if ui.is_rect_visible(rect) { - ui.painter() - .rect_filled(rect, 0f32, eframe::epaint::Color32::from_gray(0)); + .rect_filled(rect, 0.0, eframe::epaint::Color32::from_gray(0)); } res } @@ -379,7 +403,7 @@ impl PipeDash { fn side_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { egui::SidePanel::left("level_picker") - .default_width(100f32) + .default_width(100.0) .show(ctx, |ui| { ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { if ui @@ -484,7 +508,7 @@ impl PipeDash { impl eframe::App for PipeDash { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - ctx.set_pixels_per_point(2f32); + ctx.set_pixels_per_point(2.0); if let Some(boxed_err) = &self.errors.front() { egui::CentralPanel::default().show(ctx, |ui| { diff --git a/src/music.rs b/src/music.rs index 388e01d..b7683ac 100644 --- a/src/music.rs +++ b/src/music.rs @@ -1,4 +1,4 @@ -use chrono::Duration; +use std::time::Duration; use ordered_float::OrderedFloat as Float; use std::collections::{BTreeMap, BTreeSet}; @@ -35,7 +35,7 @@ where impl StaticBeatRate { pub fn from_bpm(bpm: f32) -> Self { - Self(Duration::microseconds(60_000_000 / bpm as i64)) + Self(Duration::from_secs_f32(60.0 / bpm)) } }