Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved PlayAlong logic #175

Merged
merged 4 commits into from
Jun 6, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 96 additions & 30 deletions neothesia/src/scene/playing_scene/midi_player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
song::{PlayerConfig, Song},
};
use std::{
collections::{HashSet, VecDeque},
collections::{HashMap, HashSet},
time::{Duration, Instant},
};

Expand Down Expand Up @@ -194,21 +194,64 @@ pub enum MidiEventSource {
User,
}

type NoteId = u8;

#[derive(Debug, Default)]
struct PlayerStats {
/// User notes that expired, or were simply wrong
wrong_notes: usize,
/// List of deltas of notes played early
played_early: Vec<Duration>,
/// List of deltas of notes played late
played_late: Vec<Duration>,
}

impl PlayerStats {
#[allow(unused)]
fn timing_acurracy(&self) -> f64 {
let all = self.played_early.len() + self.played_late.len();
let early_count = self.count_too_early();
let late_count = self.count_too_late();
(early_count + late_count) as f64 / all as f64
}

fn count_too_early(&self) -> usize {
// 500 is the same as expire time, so this does not make much sense, but we can chooses
// better threshold later down the line
Self::count_with_threshold(&self.played_early, Duration::from_millis(500))
}

fn count_too_late(&self) -> usize {
// 160 to forgive touching the bottom
Self::count_with_threshold(&self.played_late, Duration::from_millis(160))
}

fn count_with_threshold(events: &[Duration], threshold: Duration) -> usize {
events
.iter()
.filter(|delta| **delta > threshold)
.fold(0, |n, _| n + 1)
}
}

#[derive(Debug)]
struct UserPress {
struct NotePress {
timestamp: Instant,
note_id: u8,
}

#[derive(Debug)]
pub struct PlayAlong {
user_keyboard_range: piano_math::KeyboardRange,

required_notes: HashSet<u8>,
/// Notes required to proggres further in the song
required_notes: HashMap<NoteId, NotePress>,
/// List of user key press events that happened in last 500ms,
/// used for play along leeway logic
user_pressed_recently: HashMap<NoteId, NotePress>,
/// File notes that had NoteOn event, but no NoteOff yet
in_proggres_file_notes: HashSet<NoteId>,

// List of user key press events that happened in last 500ms,
// used for play along leeway logic
user_pressed_recently: VecDeque<UserPress>,
stats: PlayerStats,
}

impl PlayAlong {
Expand All @@ -217,50 +260,71 @@ impl PlayAlong {
user_keyboard_range,
required_notes: Default::default(),
user_pressed_recently: Default::default(),
in_proggres_file_notes: Default::default(),
stats: PlayerStats::default(),
}
}

fn update(&mut self) {
// Instead of calling .elapsed() per item let's fetch `now` once, and subtract it ourselves
let now = Instant::now();
let threshold = Duration::from_millis(500);

while let Some(item) = self.user_pressed_recently.front_mut() {
let elapsed = now - item.timestamp;
// Track the count of items before retain
let count_before = self.user_pressed_recently.len();

// If older than 500ms
if elapsed.as_millis() > 500 {
self.user_pressed_recently.pop_front();
} else {
// All subsequent items will by younger than front item, so we can break
break;
}
}
// Retain only the items that are within the threshold
self.user_pressed_recently
.retain(|_, item| now.duration_since(item.timestamp) <= threshold);

self.stats.wrong_notes += count_before - self.user_pressed_recently.len();
}

fn user_press_key(&mut self, note_id: u8, active: bool) {
let timestamp = Instant::now();

if active {
self.user_pressed_recently
.push_back(UserPress { timestamp, note_id });
self.required_notes.remove(&note_id);
// Check if note has already been played by a file
if let Some(required_press) = self.required_notes.remove(&note_id) {
self.stats
.played_late
.push(timestamp.duration_since(required_press.timestamp));
} else {
// This note was not played by file yet, place it in recents
let got_replaced = self
.user_pressed_recently
.insert(note_id, NotePress { timestamp })
.is_some();

if got_replaced {
self.stats.wrong_notes += 1
}
}
}
}

fn file_press_key(&mut self, note_id: u8, active: bool) {
let timestamp = Instant::now();
if active {
if let Some((id, _)) = self
.user_pressed_recently
.iter()
.enumerate()
.find(|(_, item)| item.note_id == note_id)
{
self.user_pressed_recently.remove(id);
// Check if note got pressed earlier 500ms (user_pressed_recently)
if let Some(press) = self.user_pressed_recently.remove(&note_id) {
self.stats
.played_early
.push(timestamp.duration_since(press.timestamp));
} else {
self.required_notes.insert(note_id);
// Player never pressed that note, let it reach required_notes

// Ignore overlapping notes
if self.in_proggres_file_notes.contains(&note_id) {
return;
}

self.required_notes.insert(note_id, NotePress { timestamp });
}

self.in_proggres_file_notes.insert(note_id);
} else {
self.required_notes.remove(&note_id);
self.in_proggres_file_notes.remove(&note_id);
}
}

Expand All @@ -284,7 +348,9 @@ impl PlayAlong {
}

pub fn clear(&mut self) {
self.required_notes.clear()
self.required_notes.clear();
self.user_pressed_recently.clear();
self.in_proggres_file_notes.clear();
}

pub fn are_required_keys_pressed(&self) -> bool {
Expand Down
Loading