rhythm-open-exchange 0.6.2

A try to create the ffmpeg of vsrg
Documentation
use crate::model::RoxChart;

/// 2D boolean grid representing note presence.
///
/// # Why a Grid?
/// Raw note lists are hard to analyze spatially. By rasterizing notes into a grid:
/// 1. We normalize time (quantizing to slices).
/// 2. We allow O(1) lookup for neighborhood checks.
/// 3. We decouple the analysis from the specific note format (Osu, SM, etc.).
///
/// X-axis: columns (matching key count)
/// Y-axis: time slices
#[derive(Debug, Clone)]
pub struct PatternGrid {
    pub columns: usize,
    pub time_slices: usize,
    pub time_start_us: i64,
    pub time_end_us: i64,
    grid: Vec<bool>,
}

impl PatternGrid {
    pub fn new(columns: usize, time_slices: usize) -> Self {
        Self {
            columns,
            time_slices,
            time_start_us: 0,
            time_end_us: 0,
            grid: vec![false; columns * time_slices],
        }
    }

    pub fn set_note(&mut self, time_index: usize, column: usize, value: bool) {
        if time_index < self.time_slices && column < self.columns {
            self.grid[time_index * self.columns + column] = value;
        }
    }

    pub fn get_note(&self, time_index: usize, column: usize) -> bool {
        if time_index < self.time_slices && column < self.columns {
            self.grid[time_index * self.columns + column]
        } else {
            false
        }
    }

    pub fn note_count(&self) -> usize {
        self.grid.iter().filter(|&&b| b).count()
    }

    /// Create PatternGrid segments from a chart.
    pub fn from_chart(
        chart: &RoxChart,
        max_time_slices: usize,
        ignore_holds: bool,
    ) -> (Vec<Self>, Vec<Vec<i64>>) {
        let mut notes: Vec<_> = chart
            .notes
            .iter()
            .filter(|n| !ignore_holds || !n.is_hold())
            .collect();

        notes.sort_by_key(|n| n.time_us);

        if notes.is_empty() {
            return (vec![], vec![]);
        }

        // Group notes by time
        let mut time_groups: std::collections::BTreeMap<i64, Vec<u8>> =
            std::collections::BTreeMap::new();
        for note in notes {
            time_groups
                .entry(note.time_us)
                .or_default()
                .push(note.column);
        }

        let sorted_times: Vec<i64> = time_groups.keys().cloned().collect();
        let total_time_slots = sorted_times.len();

        let mut grids = Vec::new();
        let mut all_timestamps = Vec::new();
        let mut segment_start = 0;

        let key_count = chart.key_count() as usize;

        while segment_start < total_time_slots {
            let segment_end = (segment_start + max_time_slices).min(total_time_slots);
            let segment_times = &sorted_times[segment_start..segment_end];

            let mut grid = Self::new(key_count, segment_times.len());
            grid.time_start_us = segment_times[0];
            grid.time_end_us = segment_times[segment_times.len() - 1];

            for (i, &time_val) in segment_times.iter().enumerate() {
                if let Some(columns) = time_groups.get(&time_val) {
                    for &col in columns {
                        grid.set_note(i, col as usize, true);
                    }
                }
            }

            grids.push(grid);
            all_timestamps.push(segment_times.to_vec());
            segment_start = segment_end;
        }

        (grids, all_timestamps)
    }
}