diff --git a/Cargo.lock b/Cargo.lock index c0f4701..0e3d48c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,6 +559,7 @@ dependencies = [ "libosu", "log", "num", + "ordered-float 2.0.1", "stderrlog", ] @@ -891,7 +892,7 @@ checksum = "5fca6f9d679bff1322c76c9a1ad4b8553b30a94f3f75bea6936e19032c2f2ec3" dependencies = [ "glyph_brush_layout", "log", - "ordered-float", + "ordered-float 1.1.1", "rustc-hash", "rusttype 0.8.3", "twox-hash", @@ -1485,6 +1486,15 @@ dependencies = [ "num-traits 0.2.14", ] +[[package]] +name = "ordered-float" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dacdec97876ef3ede8c50efc429220641a0b11ba0048b4b0c357bccbc47c5204" +dependencies = [ + "num-traits 0.2.14", +] + [[package]] name = "osmesa-sys" version = "0.1.2" @@ -1909,7 +1919,7 @@ dependencies = [ "crossbeam-utils 0.7.2", "linked-hash-map", "num_cpus", - "ordered-float", + "ordered-float 1.1.1", "rustc-hash", "stb_truetype", ] diff --git a/Cargo.toml b/Cargo.toml index 9fc0de1..fe48933 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ ggez = "0.5.1" log = "0.4.11" stderrlog = "0.5.0" num = "0.3.1" +ordered-float = "2.0.1" [dependencies.libosu] git = "https://github.com/iptq/libosu" diff --git a/bass-sys/src/constants.rs b/bass-sys/src/constants.rs index e2a680f..90d8ace 100644 --- a/bass-sys/src/constants.rs +++ b/bass-sys/src/constants.rs @@ -52,3 +52,27 @@ pub const BASS_POS_INEXACT: DWORD = 0x8000000; pub const BASS_POS_DECODE: DWORD = 0x10000000; pub const BASS_POS_DECODETO: DWORD = 0x20000000; pub const BASS_POS_SCAN: DWORD = 0x40000000; + +pub const BASS_ATTRIB_FREQ: DWORD = 1; +pub const BASS_ATTRIB_VOL: DWORD = 2; +pub const BASS_ATTRIB_PAN: DWORD = 3; +pub const BASS_ATTRIB_EAXMIX: DWORD = 4; +pub const BASS_ATTRIB_NOBUFFER: DWORD = 5; +pub const BASS_ATTRIB_VBR: DWORD = 6; +pub const BASS_ATTRIB_CPU: DWORD = 7; +pub const BASS_ATTRIB_SRC: DWORD = 8; +pub const BASS_ATTRIB_NET_RESUME: DWORD = 9; +pub const BASS_ATTRIB_SCANINFO: DWORD = 10; +pub const BASS_ATTRIB_NORAMP: DWORD = 11; +pub const BASS_ATTRIB_BITRATE: DWORD = 12; +pub const BASS_ATTRIB_BUFFER: DWORD = 13; +pub const BASS_ATTRIB_GRANULE: DWORD = 14; +pub const BASS_ATTRIB_MUSIC_AMPLIFY: DWORD = 0x100; +pub const BASS_ATTRIB_MUSIC_PANSEP: DWORD = 0x101; +pub const BASS_ATTRIB_MUSIC_PSCALER: DWORD = 0x102; +pub const BASS_ATTRIB_MUSIC_BPM: DWORD = 0x103; +pub const BASS_ATTRIB_MUSIC_SPEED: DWORD = 0x104; +pub const BASS_ATTRIB_MUSIC_VOL_GLOBAL: DWORD = 0x105; +pub const BASS_ATTRIB_MUSIC_ACTIVE: DWORD = 0x106; +pub const BASS_ATTRIB_MUSIC_VOL_CHAN: DWORD = 0x200; +pub const BASS_ATTRIB_MUSIC_VOL_INST: DWORD = 0x300; diff --git a/bass-sys/src/lib.rs b/bass-sys/src/lib.rs index abe8bbc..359a304 100644 --- a/bass-sys/src/lib.rs +++ b/bass-sys/src/lib.rs @@ -12,14 +12,16 @@ extern_log! { pub fn BASS_GetConfig(option: DWORD) -> DWORD; pub fn BASS_ChannelGetDevice(handle: DWORD) -> DWORD; + pub fn BASS_ChannelGetAttribute(handle:DWORD, attrib:DWORD, value:*mut c_float); + pub fn BASS_ChannelSetAttribute(handle:DWORD, attrib:DWORD, value:c_float); pub fn BASS_ChannelSetDevice(handle: DWORD, device: DWORD) -> BOOL; pub fn BASS_ChannelPlay(handle: DWORD, restart: BOOL) -> BOOL; pub fn BASS_ChannelPause(handle: DWORD) -> BOOL; pub fn BASS_ChannelGetLength(handle: DWORD, mode: DWORD) -> QWORD; pub fn BASS_ChannelGetPosition(handle: DWORD, mode: DWORD) -> QWORD; pub fn BASS_ChannelSetPosition(handle: DWORD, pos: QWORD, mode: DWORD); - pub fn BASS_ChannelBytes2Seconds(handle: DWORD, pos: QWORD) -> f64; - pub fn BASS_ChannelSeconds2Bytes(handle: DWORD, pos: f64) -> QWORD; + pub fn BASS_ChannelBytes2Seconds(handle: DWORD, pos: QWORD) -> c_double; + pub fn BASS_ChannelSeconds2Bytes(handle: DWORD, pos: c_double) -> QWORD; pub fn BASS_GetDevice() -> DWORD; pub fn BASS_GetDeviceInfo(device: DWORD, info: *mut BASS_DEVICEINFO) -> BOOL; diff --git a/bass-sys/src/types.rs b/bass-sys/src/types.rs index 3e9ec62..03986f2 100644 --- a/bass-sys/src/types.rs +++ b/bass-sys/src/types.rs @@ -1,4 +1,4 @@ -pub use std::os::raw::{c_char, c_float, c_int, c_uint, c_ulong, c_void}; +pub use std::os::raw::*; pub type DWORD = c_uint; pub type QWORD = c_ulong; diff --git a/run.sh b/run.sh index feef0b8..8ac11f8 100755 --- a/run.sh +++ b/run.sh @@ -1,4 +1,4 @@ #!/bin/bash export LD_LIBRARY_PATH=$(pwd)/bass-sys/linux/bass24/x64 echo $LD_LIBRARY_PATH -exec cargo run "$@" +exec cargo run --release "$@" diff --git a/src/audio.rs b/src/audio.rs index 70b088c..c942652 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -84,4 +84,12 @@ impl Sound { } Ok(()) } + + pub fn set_playback_rate(&self, rate: f64) { + unsafe { + let mut val = 0.0f32; + bass::BASS_ChannelGetAttribute(self.handle, BASS_ATTRIB_FREQ, &mut val as *mut _); + bass::BASS_ChannelSetAttribute(self.handle, BASS_ATTRIB_FREQ, val * rate as f32); + } + } } diff --git a/src/game.rs b/src/game.rs index 90c183b..5aa32b9 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::Path; @@ -11,10 +12,12 @@ use ggez::{ }, Context, GameError, GameResult, }; -use libosu::{Beatmap, HitObject, HitObjectKind, SpinnerInfo}; +use libosu::{Beatmap, HitObject, HitObjectKind, Point, SpinnerInfo}; use crate::audio::{AudioEngine, Sound}; -use crate::slider_render::render_slider; +use crate::slider_render::{render_slider, Spline}; + +pub type SliderCache = HashMap>, Spline>; pub struct Game { is_playing: bool, @@ -22,6 +25,8 @@ pub struct Game { song: Option, beatmap: Beatmap, hit_objects: Vec, + + slider_cache: SliderCache, } impl Game { @@ -36,6 +41,7 @@ impl Game { beatmap, hit_objects, song: None, + slider_cache: SliderCache::default(), }) } @@ -51,6 +57,7 @@ impl Game { let song = Sound::create(dir.join(&self.beatmap.audio_filename))?; song.set_position(113.0)?; + song.set_playback_rate(0.6); self.song = Some(song); Ok(()) @@ -136,9 +143,44 @@ impl Game { ]; let color = graphics::Color::new(1.0, 1.0, 1.0, draw_info.opacity as f32); - if let HitObjectKind::Slider(_) = ho.kind { + if let HitObjectKind::Slider(info) = &ho.kind { let color = graphics::Color::new(1.0, 1.0, 1.0, 0.6 * draw_info.opacity as f32); - render_slider(ctx, EDITOR_SCREEN, &self.beatmap, ho, color)?; + let spline = render_slider( + &mut self.slider_cache, + ctx, + EDITOR_SCREEN, + &self.beatmap, + ho, + color, + )?; + + if time > ho_time && time < draw_info.end_time { + let elapsed_time = time - ho_time; + let total_duration = draw_info.end_time - ho_time; + let single_duration = total_duration / info.num_repeats as f64; + let finished_repeats = (elapsed_time / single_duration).floor(); + let this_repeat_time = elapsed_time - finished_repeats * single_duration; + let mut travel_percent = this_repeat_time / single_duration; + // reverse direction on odd trips + if finished_repeats as u32 % 2 == 1 { + travel_percent = 1.0 - travel_percent; + } + let travel_length = travel_percent * info.pixel_length; + let pos = spline.position_at_length(travel_length); + let ball_pos = [ + EDITOR_SCREEN.x + osupx_scale_x * pos.0 as f32, + EDITOR_SCREEN.y + osupx_scale_y * pos.1 as f32, + ]; + let ball = Mesh::new_circle( + ctx, + DrawMode::Fill(FillOptions::default()), + ball_pos, + cs_real, + 1.0, + graphics::WHITE, + )?; + graphics::draw(ctx, &ball, DrawParam::default())?; + } } let circ = Mesh::new_circle( diff --git a/src/main.rs b/src/main.rs index 38c64a5..0fc91e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ extern crate bass_sys as bass; mod audio; mod game; mod math; +mod skin; mod slider_render; use std::env; diff --git a/src/math.rs b/src/math.rs index 422e883..cc51bc0 100644 --- a/src/math.rs +++ b/src/math.rs @@ -24,12 +24,6 @@ impl Math { / d; let center = Point(ux, uy); - (center, Self::distance(center, p1)) - } - - pub fn distance(p1: Point, p2: Point) -> T { - let dx = p2.0 - p1.0; - let dy = p2.1 - p1.1; - (dx * dx + dy * dy).sqrt() + (center, center.distance(p1)) } } diff --git a/src/skin.rs b/src/skin.rs new file mode 100644 index 0000000..21a6e5b --- /dev/null +++ b/src/skin.rs @@ -0,0 +1 @@ +pub struct Skin {} diff --git a/src/slider_render.rs b/src/slider_render.rs index f5fcdcd..6a6ff9a 100644 --- a/src/slider_render.rs +++ b/src/slider_render.rs @@ -9,16 +9,19 @@ use ggez::{ Context, }; use libosu::{Beatmap, HitObject, HitObjectKind, Point, SliderSplineKind}; +use ordered_float::NotNan; +use crate::game::SliderCache; use crate::math::Math; -pub fn render_slider( +pub fn render_slider<'a>( + slider_cache: &'a mut SliderCache, ctx: &mut Context, rect: Rect, beatmap: &Beatmap, slider: &HitObject, color: Color, -) -> Result<()> { +) -> Result<&'a Spline> { let mut control_points = vec![slider.pos]; let slider_info = match &slider.kind { HitObjectKind::Slider(info) => info, @@ -26,7 +29,14 @@ pub fn render_slider( }; control_points.extend(&slider_info.control); - let spline = get_spline(&slider_info.kind, &control_points, slider_info.pixel_length); + let spline = if slider_cache.contains_key(&control_points) { + slider_cache.get(&control_points).unwrap() + } else { + let new_spline = + Spline::from_control(&slider_info.kind, &control_points, slider_info.pixel_length); + slider_cache.insert(control_points.clone(), new_spline); + slider_cache.get(&control_points).unwrap() + }; let osupx_scale_x = rect.w as f64 / 512.0; let osupx_scale_y = rect.h as f64 / 384.0; @@ -45,6 +55,7 @@ pub fn render_slider( let (mut boundx, mut boundy, mut boundw, mut boundh) = (0.0f64, 0.0f64, 0.0f64, 0.0f64); let spline_mapped = spline + .spline_points .iter() .map(|point| { let (x, y) = (point.0, point.1); @@ -85,86 +96,131 @@ pub fn render_slider( graphics::draw(ctx, &rect, DrawParam::default())?; } - Ok(()) + Ok(spline) } -fn get_spline( - kind: &SliderSplineKind, - control_points: &[Point], - pixel_length: f64, -) -> Vec> { - // no matter what, if there's 2 control points, it's linear - let mut kind = kind.clone(); - if control_points.len() == 2 { - kind = SliderSplineKind::Linear; +pub struct Spline { + spline_points: Vec

, + cumulative_lengths: Vec>, +} + +impl Spline { + fn from_control( + kind: &SliderSplineKind, + control_points: &[Point], + pixel_length: f64, + ) -> Self { + // no matter what, if there's 2 control points, it's linear + let mut kind = kind.clone(); + if control_points.len() == 2 { + kind = SliderSplineKind::Linear; + } + + let points = control_points + .iter() + .map(|p| Point(p.0 as f64, p.1 as f64)) + .collect::>(); + let spline_points = match kind { + SliderSplineKind::Linear => { + let start = points[0]; + let unit = (points[1] - points[0]).norm(); + let end = points[0] + unit * pixel_length; + vec![start, end] + } + SliderSplineKind::Perfect => { + let (p1, p2, p3) = (points[0], points[1], points[2]); + let (center, radius) = Math::circumcircle(p1, p2, p3); + + // find the t-values of the start and end of the slider + let t0 = (center.1 - p1.1).atan2(p1.0 - center.0); + let mut mid = (center.1 - p2.1).atan2(p2.0 - center.0); + let mut t1 = (center.1 - p3.1).atan2(p3.0 - center.0); + + // make sure t0 is less than t1 + while mid < t0 { + mid += std::f64::consts::TAU; + } + while t1 < t0 { + t1 += std::f64::consts::TAU; + } + if mid > t1 { + t1 -= std::f64::consts::TAU; + } + + // circumference is 2 * pi * r, slider length over circumference is length/(2 * pi * r) + let direction_unit = (t1 - t0) / (t1 - t0).abs(); + let new_t1 = t0 + direction_unit * (pixel_length / radius); + + let mut t = t0; + let mut c = Vec::new(); + loop { + if !((new_t1 >= t0 && t < new_t1) || (new_t1 < t0 && t > new_t1)) { + break; + } + + let rel = Point(t.cos() * radius, -t.sin() * radius); + c.push(center + rel); + + t += (new_t1 - t0) / pixel_length; + } + c + } + SliderSplineKind::Bezier => { + // split the curve by red-anchors + let mut idx = 0; + let mut whole = Vec::new(); + for i in 1..points.len() { + if points[i].0 == points[i - 1].0 && points[i].1 == points[i - 1].1 { + let spline = calculate_bezier(&points[idx..i]); + whole.extend(spline); + idx = i; + continue; + } + } + let spline = calculate_bezier(&points[idx..]); + whole.extend(spline); + whole + } + _ => todo!(), + }; + + let mut cumulative_lengths = Vec::with_capacity(spline_points.len()); + let mut curr = 0.0; + cumulative_lengths.push(unsafe { NotNan::unchecked_new(curr) }); + for points in spline_points.windows(2) { + let dist = points[0].distance(points[1]); + curr += dist; + cumulative_lengths.push(unsafe { NotNan::unchecked_new(curr) }); + } + + Spline { + spline_points, + cumulative_lengths, + } } - let points = control_points - .iter() - .map(|p| Point(p.0 as f64, p.1 as f64)) - .collect::>(); - match kind { - SliderSplineKind::Linear => { - let start = points[0]; - let unit = (points[1] - points[0]).norm(); - let end = points[0] + unit * pixel_length; - vec![start, end] - } - SliderSplineKind::Perfect => { - let (p1, p2, p3) = (points[0], points[1], points[2]); - let (center, radius) = Math::circumcircle(p1, p2, p3); - - // find the t-values of the start and end of the slider - let t0 = (center.1 - p1.1).atan2(p1.0 - center.0); - let mut mid = (center.1 - p2.1).atan2(p2.0 - center.0); - let mut t1 = (center.1 - p3.1).atan2(p3.0 - center.0); - - // make sure t0 is less than t1 - while mid < t0 { - mid += std::f64::consts::TAU; - } - while t1 < t0 { - t1 += std::f64::consts::TAU; - } - if mid > t1 { - t1 -= std::f64::consts::TAU; - } - - // circumference is 2 * pi * r, slider length over circumference is length/(2 * pi * r) - let direction_unit = (t1 - t0) / (t1 - t0).abs(); - let new_t1 = t0 + direction_unit * (pixel_length / radius); - - let mut t = t0; - let mut c = Vec::new(); - loop { - if !((new_t1 >= t0 && t < new_t1) || (new_t1 < t0 && t > new_t1)) { - break; + pub fn position_at_length(&self, length: f64) -> P { + let length_notnan = unsafe { NotNan::unchecked_new(length) }; + match self.cumulative_lengths.binary_search(&length_notnan) { + Ok(idx) => self.spline_points[idx], + Err(idx) => { + let n = self.spline_points.len() - 1; + if idx == 0 && self.spline_points.len() > 2 { + return self.spline_points[0]; + } else if idx >= n { + return self.spline_points[n]; } - let rel = Point(t.cos() * radius, -t.sin() * radius); - c.push(center + rel); + let (len1, len2) = ( + self.cumulative_lengths[idx].into_inner(), + self.cumulative_lengths[idx + 1].into_inner(), + ); + let proportion = (length - len1) / (len2 - len1); - t += (new_t1 - t0) / pixel_length; + let (p1, p2) = (self.spline_points[idx], self.spline_points[idx + 1]); + (p2 - p1) * proportion + p1 } - c } - SliderSplineKind::Bezier => { - // split the curve by red-anchors - let mut idx = 0; - let mut whole = Vec::new(); - for i in 1..points.len() { - if points[i].0 == points[i - 1].0 && points[i].1 == points[i - 1].1 { - let spline = calculate_bezier(&points[idx..i]); - whole.extend(spline); - idx = i; - continue; - } - } - let spline = calculate_bezier(&points[idx..]); - whole.extend(spline); - whole - } - _ => todo!(), } } diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..26ad4fe --- /dev/null +++ b/todo.txt @@ -0,0 +1,4 @@ +- stacking +- fix the overlaps in notch hell slider (circle size wrong?) +- don't draw overlaps between slider segments +- use skin components