Feat: fix sequencer precision regression
Some checks failed
Deploy Website / deploy (push) Failing after 4s
Some checks failed
Deploy Website / deploy (push) Failing after 4s
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1825,7 +1825,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "doux"
|
||||
version = "0.0.15"
|
||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.15#1f11f795b877d9c15f65d88eb5576e8149092b17"
|
||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.15#29d8f055612f6141d7546d72b91e60026937b0fd"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clap",
|
||||
|
||||
@@ -292,7 +292,7 @@ impl Plugin for CagirePlugin {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
audio_sample_pos: self.sample_pos,
|
||||
corrected_audio_pos: self.sample_pos as f64,
|
||||
sr: self.sample_rate as f64,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use clap::Parser;
|
||||
use doux::EngineMetrics;
|
||||
use eframe::NativeOptions;
|
||||
|
||||
use cagire::engine::{
|
||||
build_stream, AnalysisHandle, AudioStreamConfig, LinkState, ScopeBuffer,
|
||||
build_stream, AnalysisHandle, AudioRef, AudioStreamConfig, LinkState, ScopeBuffer,
|
||||
SequencerHandle, SpectrumBuffer,
|
||||
};
|
||||
use cagire::terminal::{create_terminal, FontChoice, TerminalType};
|
||||
@@ -48,7 +49,7 @@ struct CagireDesktop {
|
||||
metrics: Arc<EngineMetrics>,
|
||||
scope_buffer: Arc<ScopeBuffer>,
|
||||
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
sample_rate_shared: Arc<AtomicU32>,
|
||||
_stream: Option<cpal::Stream>,
|
||||
_input_stream: Option<cpal::Stream>,
|
||||
@@ -94,7 +95,7 @@ impl CagireDesktop {
|
||||
metrics: b.metrics,
|
||||
scope_buffer: b.scope_buffer,
|
||||
spectrum_buffer: b.spectrum_buffer,
|
||||
audio_sample_pos: b.audio_sample_pos,
|
||||
audio_ref: b.audio_ref,
|
||||
sample_rate_shared: b.sample_rate_shared,
|
||||
_stream: b.stream,
|
||||
_input_stream: b.input_stream,
|
||||
@@ -152,7 +153,11 @@ impl CagireDesktop {
|
||||
}
|
||||
}
|
||||
|
||||
self.audio_sample_pos.store(0, Ordering::Release);
|
||||
self.audio_ref.store(Arc::new(AudioRef {
|
||||
sample_pos: 0,
|
||||
timestamp: std::time::Instant::now(),
|
||||
sample_rate: 44100.0,
|
||||
}));
|
||||
|
||||
let preload_entries: Vec<(String, std::path::PathBuf)> = restart_samples
|
||||
.iter()
|
||||
@@ -166,7 +171,7 @@ impl CagireDesktop {
|
||||
Arc::clone(&self.spectrum_buffer),
|
||||
Arc::clone(&self.metrics),
|
||||
restart_samples,
|
||||
Arc::clone(&self.audio_sample_pos),
|
||||
Arc::clone(&self.audio_ref),
|
||||
new_error_tx,
|
||||
&self.app.audio.config.sample_paths,
|
||||
Arc::clone(&self.device_lost),
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
//! Audio output stream (cpal) and FFT spectrum analysis.
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use ringbuf::{traits::*, HeapRb};
|
||||
use rustfft::{num_complex::Complex, FftPlanner};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use std::sync::atomic::AtomicU64;
|
||||
/// Timestamped audio position reference for jitter-free tick interpolation.
|
||||
/// Published by the audio callback after each `process_block`, read by the
|
||||
/// sequencer to compute the correct sample position at any instant.
|
||||
#[derive(Clone)]
|
||||
pub struct AudioRef {
|
||||
pub sample_pos: u64,
|
||||
pub timestamp: Instant,
|
||||
pub sample_rate: f64,
|
||||
}
|
||||
|
||||
pub struct ScopeBuffer {
|
||||
pub samples: [AtomicU32; 256],
|
||||
@@ -303,7 +312,7 @@ pub fn build_stream(
|
||||
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
metrics: Arc<EngineMetrics>,
|
||||
initial_samples: Vec<doux::sampling::SampleEntry>,
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
error_tx: Sender<String>,
|
||||
sample_paths: &[std::path::PathBuf],
|
||||
device_lost: Arc<AtomicBool>,
|
||||
@@ -438,6 +447,7 @@ pub fn build_stream(
|
||||
let mut rt_set = false;
|
||||
let mut live_scratch = vec![0.0f32; 4096];
|
||||
let mut input_consumer = input_consumer;
|
||||
let mut current_pos: u64 = 0;
|
||||
|
||||
let stream = device
|
||||
.build_output_stream(
|
||||
@@ -454,8 +464,6 @@ pub fn build_stream(
|
||||
let buffer_samples = data.len() / channels;
|
||||
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
|
||||
|
||||
audio_sample_pos.fetch_add(buffer_samples as u64, Ordering::Release);
|
||||
|
||||
while let Ok(cmd) = audio_rx.try_recv() {
|
||||
match cmd {
|
||||
AudioCommand::Evaluate { cmd, tick } => {
|
||||
@@ -497,6 +505,16 @@ pub fn build_stream(
|
||||
|
||||
engine.metrics.load.set_buffer_time(buffer_time_ns);
|
||||
engine.process_block(data, &[], &live_scratch[..raw_len]);
|
||||
|
||||
// Publish accurate audio reference AFTER process_block
|
||||
// so sample_pos matches doux's internal tick exactly.
|
||||
current_pos += buffer_samples as u64;
|
||||
audio_ref.store(Arc::new(AudioRef {
|
||||
sample_pos: current_pos,
|
||||
timestamp: Instant::now(),
|
||||
sample_rate: sr as f64,
|
||||
}));
|
||||
|
||||
scope_buffer.write(data);
|
||||
|
||||
// Feed mono mix to analysis thread via ring buffer (non-blocking)
|
||||
|
||||
@@ -7,7 +7,7 @@ mod timing;
|
||||
|
||||
pub use timing::{next_boundary, substeps_in_window, SyncTime};
|
||||
|
||||
pub use audio::{preload_sample_heads, AnalysisHandle, ScopeBuffer, SpectrumBuffer};
|
||||
pub use audio::{preload_sample_heads, AnalysisHandle, AudioRef, ScopeBuffer, SpectrumBuffer};
|
||||
|
||||
// Re-exported for the plugin crate (not used by the terminal binary).
|
||||
#[allow(unused_imports)]
|
||||
|
||||
@@ -8,10 +8,12 @@ use rand::SeedableRng;
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "desktop")]
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::{AtomicI64, AtomicU64};
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use super::audio::AudioRef;
|
||||
|
||||
use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand};
|
||||
use super::realtime::set_realtime_priority;
|
||||
use super::{next_boundary, substeps_in_window, LinkState, SyncTime};
|
||||
@@ -329,7 +331,7 @@ impl AudioState {
|
||||
}
|
||||
|
||||
pub struct SequencerConfig {
|
||||
pub audio_sample_pos: Arc<AtomicU64>,
|
||||
pub audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
pub cc_access: Option<Arc<dyn CcAccess>>,
|
||||
pub variables: Variables,
|
||||
@@ -393,7 +395,7 @@ pub fn spawn_sequencer(
|
||||
shared_state_clone,
|
||||
live_keys,
|
||||
nudge_us,
|
||||
config.audio_sample_pos,
|
||||
config.audio_ref,
|
||||
config.sample_rate,
|
||||
config.cc_access,
|
||||
variables,
|
||||
@@ -502,7 +504,7 @@ pub struct TickInput {
|
||||
pub fill: bool,
|
||||
pub nudge_secs: f64,
|
||||
pub current_time_us: SyncTime,
|
||||
pub audio_sample_pos: u64,
|
||||
pub corrected_audio_pos: f64,
|
||||
pub sr: f64,
|
||||
pub mouse_x: f64,
|
||||
pub mouse_y: f64,
|
||||
@@ -765,7 +767,7 @@ impl SequencerState {
|
||||
input.fill,
|
||||
input.nudge_secs,
|
||||
input.current_time_us,
|
||||
input.audio_sample_pos,
|
||||
input.corrected_audio_pos,
|
||||
input.sr,
|
||||
input.mouse_x,
|
||||
input.mouse_y,
|
||||
@@ -780,7 +782,7 @@ impl SequencerState {
|
||||
input.quantum,
|
||||
input.fill,
|
||||
input.nudge_secs,
|
||||
input.audio_sample_pos,
|
||||
input.corrected_audio_pos,
|
||||
input.sr,
|
||||
input.mouse_x,
|
||||
input.mouse_y,
|
||||
@@ -934,7 +936,7 @@ impl SequencerState {
|
||||
fill: bool,
|
||||
nudge_secs: f64,
|
||||
_current_time_us: SyncTime,
|
||||
audio_sample_pos: u64,
|
||||
corrected_audio_pos: f64,
|
||||
sr: f64,
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
@@ -983,7 +985,7 @@ impl SequencerState {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let event_tick = Some(audio_sample_pos + (time_delta * sr).round() as u64);
|
||||
let event_tick = Some((corrected_audio_pos + time_delta * sr).round() as u64);
|
||||
|
||||
if let Some(step) = pattern.steps.get(step_idx) {
|
||||
let resolved_script = pattern.resolve_script(step_idx);
|
||||
@@ -1095,7 +1097,7 @@ impl SequencerState {
|
||||
quantum: f64,
|
||||
fill: bool,
|
||||
nudge_secs: f64,
|
||||
audio_sample_pos: u64,
|
||||
corrected_audio_pos: f64,
|
||||
sr: f64,
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
@@ -1117,7 +1119,7 @@ impl SequencerState {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let event_tick = Some(audio_sample_pos + (time_delta * sr).round() as u64);
|
||||
let event_tick = Some((corrected_audio_pos + time_delta * sr).round() as u64);
|
||||
|
||||
let step_in_cycle = self.script_step % self.script_length;
|
||||
|
||||
@@ -1263,7 +1265,7 @@ fn sequencer_loop(
|
||||
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
nudge_us: Arc<AtomicI64>,
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
cc_access: Option<Arc<dyn CcAccess>>,
|
||||
variables: Variables,
|
||||
@@ -1330,7 +1332,9 @@ fn sequencer_loop(
|
||||
let lookahead_end = beat + lookahead_beats;
|
||||
|
||||
let sr = sample_rate.load(Ordering::Relaxed) as f64;
|
||||
let audio_samples = audio_sample_pos.load(Ordering::Acquire);
|
||||
let ref_snapshot = audio_ref.load();
|
||||
let elapsed_secs = ref_snapshot.timestamp.elapsed().as_secs_f64();
|
||||
let corrected_pos = ref_snapshot.sample_pos as f64 + elapsed_secs * ref_snapshot.sample_rate;
|
||||
let input = TickInput {
|
||||
commands,
|
||||
playing: is_playing,
|
||||
@@ -1341,7 +1345,7 @@ fn sequencer_loop(
|
||||
fill: live_keys.fill(),
|
||||
nudge_secs: nudge_us.load(Ordering::Relaxed) as f64 / 1_000_000.0,
|
||||
current_time_us,
|
||||
audio_sample_pos: audio_samples,
|
||||
corrected_audio_pos: corrected_pos,
|
||||
sr,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: f32::from_bits(mouse_x.load(Ordering::Relaxed)) as f64,
|
||||
@@ -1563,7 +1567,7 @@ mod tests {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
audio_sample_pos: 0,
|
||||
corrected_audio_pos: 0.0,
|
||||
sr: 48000.0,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
@@ -1582,7 +1586,7 @@ mod tests {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
audio_sample_pos: 0,
|
||||
corrected_audio_pos: 0.0,
|
||||
sr: 48000.0,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
|
||||
24
src/init.rs
24
src/init.rs
@@ -1,14 +1,16 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use doux::EngineMetrics;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::{
|
||||
build_stream, preload_sample_heads, spawn_sequencer, AnalysisHandle, AudioStreamConfig,
|
||||
LinkState, PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle,
|
||||
SpectrumBuffer,
|
||||
build_stream, preload_sample_heads, spawn_sequencer, AnalysisHandle, AudioRef,
|
||||
AudioStreamConfig, LinkState, PatternChange, ScopeBuffer, SequencerConfig,
|
||||
SequencerHandle, SpectrumBuffer,
|
||||
};
|
||||
use crate::midi;
|
||||
use crate::model;
|
||||
@@ -36,7 +38,7 @@ pub struct Init {
|
||||
pub metrics: Arc<EngineMetrics>,
|
||||
pub scope_buffer: Arc<ScopeBuffer>,
|
||||
pub spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
pub audio_sample_pos: Arc<AtomicU64>,
|
||||
pub audio_ref: Arc<ArcSwap<AudioRef>>,
|
||||
pub sample_rate_shared: Arc<AtomicU32>,
|
||||
pub stream: Option<cpal::Stream>,
|
||||
pub input_stream: Option<cpal::Stream>,
|
||||
@@ -151,7 +153,11 @@ pub fn init(args: InitArgs) -> Init {
|
||||
let scope_buffer = Arc::new(ScopeBuffer::new());
|
||||
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
|
||||
|
||||
let audio_sample_pos = Arc::new(AtomicU64::new(0));
|
||||
let audio_ref = Arc::new(ArcSwap::from_pointee(AudioRef {
|
||||
sample_pos: 0,
|
||||
timestamp: Instant::now(),
|
||||
sample_rate: 44100.0,
|
||||
}));
|
||||
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
|
||||
let mut initial_samples = Vec::new();
|
||||
for path in &app.audio.config.sample_paths {
|
||||
@@ -177,7 +183,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
let mouse_down = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
|
||||
|
||||
let seq_config = SequencerConfig {
|
||||
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||
audio_ref: Arc::clone(&audio_ref),
|
||||
sample_rate: Arc::clone(&sample_rate_shared),
|
||||
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn model::CcAccess>),
|
||||
variables: Arc::clone(&app.variables),
|
||||
@@ -218,7 +224,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
initial_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
Arc::clone(&audio_ref),
|
||||
stream_error_tx,
|
||||
&app.audio.config.sample_paths,
|
||||
Arc::clone(&device_lost),
|
||||
@@ -262,7 +268,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
metrics,
|
||||
scope_buffer,
|
||||
spectrum_buffer,
|
||||
audio_sample_pos,
|
||||
audio_ref,
|
||||
sample_rate_shared,
|
||||
stream,
|
||||
input_stream,
|
||||
|
||||
10
src/main.rs
10
src/main.rs
@@ -96,7 +96,7 @@ fn main() -> io::Result<()> {
|
||||
let metrics = b.metrics;
|
||||
let scope_buffer = b.scope_buffer;
|
||||
let spectrum_buffer = b.spectrum_buffer;
|
||||
let audio_sample_pos = b.audio_sample_pos;
|
||||
let audio_ref = b.audio_ref;
|
||||
let sample_rate_shared = b.sample_rate_shared;
|
||||
let mut _stream = b.stream;
|
||||
let mut _input_stream = b.input_stream;
|
||||
@@ -148,7 +148,11 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
audio_sample_pos.store(0, Ordering::Relaxed);
|
||||
audio_ref.store(Arc::new(engine::AudioRef {
|
||||
sample_pos: 0,
|
||||
timestamp: std::time::Instant::now(),
|
||||
sample_rate: 44100.0,
|
||||
}));
|
||||
|
||||
let preload_entries: Vec<(String, std::path::PathBuf)> = restart_samples
|
||||
.iter()
|
||||
@@ -162,7 +166,7 @@ fn main() -> io::Result<()> {
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
restart_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
Arc::clone(&audio_ref),
|
||||
new_error_tx,
|
||||
&app.audio.config.sample_paths,
|
||||
Arc::clone(&device_lost),
|
||||
|
||||
Reference in New Issue
Block a user