2025-11-28 15:33:12 -06:00
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
2025-09-24 00:34:54 -05:00
|
|
|
use rodio::OutputStream;
|
2025-11-28 15:33:12 -06:00
|
|
|
use tinyrand::{Seeded, StdRand};
|
2025-09-24 00:34:54 -05:00
|
|
|
use crate::grid::Grid;
|
2025-10-04 00:43:45 -05:00
|
|
|
use crate::dialog::Dialog;
|
2025-10-10 23:37:35 -05:00
|
|
|
use crate::noise::Note;
|
2025-09-20 20:36:57 -05:00
|
|
|
|
|
|
|
|
pub const LOOP_LENGTH: u32 = 16;
|
|
|
|
|
|
2025-09-24 00:34:54 -05:00
|
|
|
pub struct Tenori {
|
2025-09-20 20:36:57 -05:00
|
|
|
/// Tempo in beats per minute
|
|
|
|
|
pub tempo: u32,
|
|
|
|
|
|
|
|
|
|
// A count _in beats_ of where we are in the loop
|
2025-09-27 01:13:01 -05:00
|
|
|
pub timer: f32,
|
2025-09-20 20:36:57 -05:00
|
|
|
|
|
|
|
|
/// Whether or not we're playing; false == paused
|
|
|
|
|
pub playing: bool,
|
|
|
|
|
|
|
|
|
|
// The instant of the last time we called tick()
|
|
|
|
|
last_tick: Option<Instant>,
|
|
|
|
|
|
|
|
|
|
/// The grids that we currently have going
|
|
|
|
|
pub grids: Vec<Grid>,
|
|
|
|
|
|
2025-10-04 13:24:53 -05:00
|
|
|
/// Running count of windows created (for ids)
|
|
|
|
|
pub window_counter: usize,
|
2025-09-24 00:34:54 -05:00
|
|
|
|
|
|
|
|
// The audio output stream to which we will play notes
|
2025-09-30 17:53:00 -05:00
|
|
|
output_stream: OutputStream,
|
|
|
|
|
|
2025-10-04 00:43:45 -05:00
|
|
|
/// Error dialogs we're currently showing
|
|
|
|
|
pub dialogs: Vec<Dialog>,
|
|
|
|
|
|
|
|
|
|
/// If present, we can save to this file without asking the
|
|
|
|
|
/// user to select a file first.
|
2025-11-28 15:33:12 -06:00
|
|
|
pub default_filename: Option<String>,
|
|
|
|
|
|
|
|
|
|
/// A random number generator
|
|
|
|
|
pub rand: Arc<Mutex<StdRand>>,
|
2025-09-20 20:36:57 -05:00
|
|
|
}
|
|
|
|
|
|
2025-09-24 00:34:54 -05:00
|
|
|
impl Default for Tenori {
|
2025-09-20 20:36:57 -05:00
|
|
|
fn default() -> Self {
|
2025-09-24 00:34:54 -05:00
|
|
|
let output_stream = rodio::OutputStreamBuilder::open_default_stream()
|
|
|
|
|
.expect("Open audio output stream");
|
|
|
|
|
|
2025-11-28 15:33:12 -06:00
|
|
|
let rand = StdRand::seed(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs());
|
|
|
|
|
|
2025-09-20 20:36:57 -05:00
|
|
|
Self {
|
|
|
|
|
tempo: 90,
|
|
|
|
|
timer: 0.0,
|
|
|
|
|
playing: true,
|
|
|
|
|
last_tick: None,
|
|
|
|
|
grids: vec![],
|
2025-09-24 00:34:54 -05:00
|
|
|
window_counter: 0,
|
2025-09-30 17:53:00 -05:00
|
|
|
dialogs: vec![],
|
2025-10-04 00:43:45 -05:00
|
|
|
default_filename: None,
|
2025-11-28 15:33:12 -06:00
|
|
|
rand: Arc::new(Mutex::new(rand)),
|
2025-09-24 00:34:54 -05:00
|
|
|
output_stream
|
2025-09-20 20:36:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-24 00:34:54 -05:00
|
|
|
impl Tenori {
|
2025-09-20 20:36:57 -05:00
|
|
|
/// Call this every frame to update the timer / last tick based on the current instant
|
|
|
|
|
/// and the tempo
|
|
|
|
|
pub fn tick(&mut self) -> bool {
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
let old_beat = self.beat();
|
|
|
|
|
if let Some(last) = self.last_tick && self.playing {
|
|
|
|
|
let dt = (now - last).as_secs_f32();
|
|
|
|
|
let bps = (self.tempo as f32) / 60.0;
|
|
|
|
|
// Timer is an amount of time _in beats_ and some of those beats might have been for a
|
|
|
|
|
// different tempo.
|
|
|
|
|
// We know now a time delta in seconds and a conversion factor to turn that to beats, so:
|
|
|
|
|
self.timer += dt * bps;
|
|
|
|
|
// 16 beats in the loop, so timer should never be over 16:
|
|
|
|
|
while self.timer > LOOP_LENGTH as f32 { self.timer -= LOOP_LENGTH as f32 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playing or not, update last_tick so that the next frame adds the correct duration to timer
|
|
|
|
|
self.last_tick = Some(now);
|
|
|
|
|
|
|
|
|
|
// Did we enter a new beat?
|
|
|
|
|
self.beat() != old_beat
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Which beat (0..loop_length) we're on
|
|
|
|
|
pub fn beat(&self) -> u32 {
|
|
|
|
|
self.timer.floor() as u32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What fraction we are (0.0..1.0) through the loop
|
|
|
|
|
/// (multiply by window width to find the x coord to draw the cursor line)
|
|
|
|
|
pub fn ratio(&self) -> f32 {
|
|
|
|
|
self.timer / LOOP_LENGTH as f32
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-24 00:34:54 -05:00
|
|
|
pub fn notes_for_beat(&self) -> Vec<Note> {
|
2025-09-20 20:36:57 -05:00
|
|
|
let mut notes = vec![];
|
|
|
|
|
let beat = self.beat();
|
|
|
|
|
|
|
|
|
|
for grid in self.grids.iter() {
|
2025-09-26 01:06:33 -05:00
|
|
|
for tone in grid.notes(beat).into_iter() {
|
2025-09-24 00:34:54 -05:00
|
|
|
|
|
|
|
|
notes.push(Note {
|
|
|
|
|
tone,
|
|
|
|
|
volume: grid.volume,
|
2025-10-05 13:57:18 -05:00
|
|
|
timbre: grid.timbre
|
2025-09-24 00:34:54 -05:00
|
|
|
})
|
2025-09-20 20:36:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
notes
|
|
|
|
|
}
|
2025-09-24 00:34:54 -05:00
|
|
|
|
|
|
|
|
pub fn play(&mut self, note: Note) {
|
|
|
|
|
note.play(self.output_stream.mixer())
|
|
|
|
|
}
|
2025-09-20 20:36:57 -05:00
|
|
|
}
|