diff --git a/Cargo.lock b/Cargo.lock index 7029d7f..34c689a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "itoa" version = "1.0.15" @@ -221,6 +227,7 @@ name = "looper" version = "0.1.0" dependencies = [ "dirs", + "hound", "jack", "kanal", "log", diff --git a/Cargo.toml b/Cargo.toml index e3bfabb..c7e7bf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ path = "src/sendmidi.rs" [dependencies] dirs = "5" +hound = "3.5" jack = "0.13" kanal = "0.1" log = "0.4" diff --git a/src/audio_chunk.rs b/src/audio_chunk.rs index 9b735f1..b889bd6 100644 --- a/src/audio_chunk.rs +++ b/src/audio_chunk.rs @@ -85,6 +85,19 @@ impl AudioChunk { } } + /// Get total sample count across entire chunk chain + pub fn len(&self) -> usize { + let mut total = self.sample_count; + let mut current = &self.next; + + while let Some(chunk) = current { + total += chunk.sample_count; + current = &chunk.next; + } + + total + } + pub fn copy_samples(self: &Arc, dest: &mut [f32], start: usize) -> Result<()> { if start < self.sample_count { // Copy from this chunk @@ -113,6 +126,123 @@ impl AudioChunk { mod tests { use super::*; + #[test] + fn test_len_single_chunk_with_samples() { + let chunk = Arc::new(AudioChunk { + samples: vec![1.0, 2.0, 3.0, 4.0, 5.0].into_boxed_slice(), + sample_count: 5, + next: None, + }); + + assert_eq!(chunk.len(), 5); + } + + #[test] + fn test_len_single_chunk_empty() { + let chunk = Arc::new(AudioChunk { + samples: vec![0.0, 0.0, 0.0].into_boxed_slice(), + sample_count: 0, + next: None, + }); + + assert_eq!(chunk.len(), 0); + } + + #[test] + fn test_len_single_chunk_partial() { + let chunk = Arc::new(AudioChunk { + samples: vec![1.0, 2.0, 3.0, 0.0, 0.0].into_boxed_slice(), + sample_count: 3, // Only first 3 samples are valid + next: None, + }); + + assert_eq!(chunk.len(), 3); + } + + #[test] + fn test_len_linked_chunks() { + let chunk3 = Arc::new(AudioChunk { + samples: vec![7.0, 8.0, 9.0].into_boxed_slice(), + sample_count: 3, + next: None, + }); + + let chunk2 = Arc::new(AudioChunk { + samples: vec![4.0, 5.0].into_boxed_slice(), + sample_count: 2, + next: Some(chunk3), + }); + + let chunk1 = Arc::new(AudioChunk { + samples: vec![1.0, 2.0, 3.0].into_boxed_slice(), + sample_count: 3, + next: Some(chunk2), + }); + + assert_eq!(chunk1.len(), 8); // 3 + 2 + 3 + } + + #[test] + fn test_len_chain_with_empty_chunks() { + let chunk3 = Arc::new(AudioChunk { + samples: vec![4.0, 5.0].into_boxed_slice(), + sample_count: 2, + next: None, + }); + + let chunk2 = Arc::new(AudioChunk { + samples: vec![0.0, 0.0].into_boxed_slice(), + sample_count: 0, // Empty chunk in middle + next: Some(chunk3), + }); + + let chunk1 = Arc::new(AudioChunk { + samples: vec![1.0, 2.0, 3.0].into_boxed_slice(), + sample_count: 3, + next: Some(chunk2), + }); + + assert_eq!(chunk1.len(), 5); // 3 + 0 + 2 + } + + #[test] + fn test_len_all_empty_chunks() { + let chunk2 = Arc::new(AudioChunk { + samples: vec![0.0, 0.0].into_boxed_slice(), + sample_count: 0, + next: None, + }); + + let chunk1 = Arc::new(AudioChunk { + samples: vec![0.0, 0.0, 0.0].into_boxed_slice(), + sample_count: 0, + next: Some(chunk2), + }); + + assert_eq!(chunk1.len(), 0); + } + + #[test] + fn test_len_long_chain() { + // Create a longer chain to test iteration + let mut current_chunk = Arc::new(AudioChunk { + samples: vec![10.0].into_boxed_slice(), + sample_count: 1, + next: None, + }); + + // Build chain backwards: 9 -> 8 -> ... -> 1 -> 0 + for i in (0..10).rev() { + current_chunk = Arc::new(AudioChunk { + samples: vec![i as f32].into_boxed_slice(), + sample_count: 1, + next: Some(current_chunk), + }); + } + + assert_eq!(current_chunk.len(), 11); // 11 chunks, each with 1 sample + } + #[test] fn test_allocate_creates_empty_chunk() { let chunk = AudioChunk::allocate(1024); diff --git a/src/audio_engine.rs b/src/audio_engine.rs index 11df979..07cea1a 100644 --- a/src/audio_engine.rs +++ b/src/audio_engine.rs @@ -6,9 +6,10 @@ mod looper_error; mod metronome; mod midi; mod notification_handler; +mod post_record_handler; mod process_handler; mod state; -mod state_manager; +mod persistence_manager; mod track; use std::sync::Arc; @@ -19,14 +20,18 @@ use chunk_factory::ChunkFactory; use connection_manager::ConnectionManager; use looper_error::LooperError; use looper_error::Result; +use metronome::Metronome; use metronome::generate_beep; use notification_handler::JackNotification; use notification_handler::NotificationHandler; +use post_record_handler::PostRecordHandler; +use post_record_handler::PostRecordController; use process_handler::ProcessHandler; use state::State; -use state_manager::StateManager; +use persistence_manager::PersistenceManager; use track::Track; use track::TrackState; +use track::TrackTiming; pub struct JackPorts { pub audio_in: jack::Port, @@ -51,20 +56,25 @@ async fn main() { let notification_handler = NotificationHandler::new(); let mut notification_channel = notification_handler.subscribe(); - let (mut state_manager, state_rx) = StateManager::new(notification_handler.subscribe()); + let (mut persistence_manager, state_watch) = PersistenceManager::new(notification_handler.subscribe()); // Load state values for metronome configuration - let initial_state = state_rx.borrow().clone(); + let initial_state = state_watch.borrow().clone(); + + // Create post-record handler and get controller for ProcessHandler + let (mut post_record_handler, post_record_controller) = PostRecordHandler::new() + .expect("Could not create post-record handler"); let process_handler = ProcessHandler::new( ports, allocator, beep_samples, &initial_state, + post_record_controller, ).expect("Could not create process handler"); let mut connection_manager = ConnectionManager::new( - state_rx, + state_watch, notification_handler.subscribe(), jack_client.name().to_string(), ) @@ -82,7 +92,7 @@ async fn main() { break; } } - result = state_manager.run() => { + result = persistence_manager.run() => { if let Err(e) = result { log::error!("StateManager task failed: {}", e); } @@ -94,6 +104,12 @@ async fn main() { } break; } + result = post_record_handler.run() => { + if let Err(e) = result { + log::error!("PostRecordHandler task failed: {}", e); + } + break; + } _ = tokio::signal::ctrl_c() => { log::info!("Stopping"); break; diff --git a/src/connection_manager.rs b/src/connection_manager.rs index 6c947d0..aefc783 100644 --- a/src/connection_manager.rs +++ b/src/connection_manager.rs @@ -2,7 +2,7 @@ use crate::*; use tokio::sync::{broadcast, watch}; pub struct ConnectionManager { - state_rx: watch::Receiver, + state_watch: watch::Receiver, notification_rx: broadcast::Receiver, jack_client: jack::Client, jack_client_name: String, @@ -10,7 +10,7 @@ pub struct ConnectionManager { impl ConnectionManager { pub fn new( - state_rx: watch::Receiver, + state_watch: watch::Receiver, notification_rx: broadcast::Receiver, jack_client_name: String, ) -> Result { @@ -21,7 +21,7 @@ impl ConnectionManager { .map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?; Ok(Self { - state_rx, + state_watch, notification_rx, jack_client, jack_client_name, @@ -39,7 +39,7 @@ impl ConnectionManager { } async fn restore_connections(&self) { - let state = self.state_rx.borrow(); + let state = self.state_watch.borrow(); for external_port in &state.connections.midi_in { let our_port = format!("{}:midi_in", self.jack_client_name); diff --git a/src/notification_handler.rs b/src/notification_handler.rs index badbe11..6059242 100644 --- a/src/notification_handler.rs +++ b/src/notification_handler.rs @@ -63,14 +63,7 @@ impl jack::NotificationHandler for NotificationHandler { .expect("Could not send port connection notification"); } - fn port_registration(&mut self, client: &jack::Client, port_id: jack::PortId, register: bool) { - let Some(port_name) = client.port_by_id(port_id) else { - return; - }; - let Ok(port_name) = port_name.name() else { - return; - }; - + fn port_registration(&mut self, _client: &jack::Client, _port_id: jack::PortId, register: bool) { if register { let notification = JackNotification::PortRegistered {}; self.channel diff --git a/src/state_manager.rs b/src/persistence_manager.rs similarity index 98% rename from src/state_manager.rs rename to src/persistence_manager.rs index 6c96d34..6d2496b 100644 --- a/src/state_manager.rs +++ b/src/persistence_manager.rs @@ -2,14 +2,14 @@ use crate::*; use std::path::PathBuf; use tokio::sync::{broadcast, watch}; -pub struct StateManager { +pub struct PersistenceManager { state: State, state_tx: watch::Sender, notification_rx: broadcast::Receiver, state_file_path: PathBuf, } -impl StateManager { +impl PersistenceManager { pub fn new( notification_rx: broadcast::Receiver, ) -> (Self, watch::Receiver) { diff --git a/src/post_record_handler.rs b/src/post_record_handler.rs new file mode 100644 index 0000000..91d3b6c --- /dev/null +++ b/src/post_record_handler.rs @@ -0,0 +1,173 @@ +use crate::*; +use std::path::PathBuf; + +/// Request to process a recorded chunk chain +#[derive(Debug)] +pub struct PostRecordRequest { + pub chunk_chain: Arc, + pub sample_rate: u32, +} + +/// Response containing the consolidated chunk +#[derive(Debug)] +pub struct PostRecordResponse { + pub consolidated_chunk: Arc, +} + +/// RT-side interface for post-record operations +#[derive(Debug)] +pub struct PostRecordController { + request_sender: kanal::Sender, + response_receiver: kanal::Receiver, +} + +impl PostRecordController { + /// Send a post-record processing request (RT-safe) + pub fn send_request(&self, chunk_chain: Arc, sample_rate: u32) -> Result<()> { + let request = PostRecordRequest { chunk_chain, sample_rate }; + + match self.request_sender.try_send(request) { + Ok(true) => Ok(()), // Successfully sent + Ok(false) => Err(LooperError::ChunkAllocation(std::panic::Location::caller())), // Channel full + Err(_) => Err(LooperError::ChunkAllocation(std::panic::Location::caller())), // Channel closed + } + } + + /// Try to receive a processing response (RT-safe) + pub fn try_recv_response(&self) -> Option { + match self.response_receiver.try_recv() { + Ok(Some(response)) => Some(response), + Ok(None) | Err(_) => None, // No message or channel closed = None + } + } +} + +/// Handles post-record processing: consolidation and saving +pub struct PostRecordHandler { + request_receiver: kanal::AsyncReceiver, + response_sender: kanal::AsyncSender, + directory: PathBuf, +} + +impl PostRecordHandler { + /// Create new handler and return RT controller + pub fn new() -> Result<(Self, PostRecordController)> { + // Create channels for bidirectional communication + let (request_sender, request_receiver) = kanal::bounded(16); + let (response_sender, response_receiver) = kanal::bounded(16); + + let request_receiver = request_receiver.to_async(); + let response_sender = response_sender.to_async(); + + let controller = PostRecordController { + request_sender, + response_receiver, + }; + + let handler = Self { + request_receiver, + response_sender, + directory: Self::create_directory()?, + }; + + Ok((handler, controller)) + } + + /// Run the async processing task + pub async fn run(&mut self) -> Result<()> { + while let Ok(request) = self.request_receiver.recv().await { + if let Err(e) = self.process_request(request).await { + log::error!("Failed to process post-record request: {}", e); + } + } + + Ok(()) + } + + /// Process a single post-record request + async fn process_request( + &self, + request: PostRecordRequest, + ) -> Result<()> { + log::debug!("Processing post-record request for {} samples", + request.chunk_chain.len()); + + // Step 1: Consolidate chunk chain (CPU intensive) + let consolidated_chunk = AudioChunk::consolidate(&request.chunk_chain); + + log::debug!("Consolidated {} samples", consolidated_chunk.sample_count); + + // Step 2: Send consolidated chunk back to RT thread immediately + let response = PostRecordResponse { + consolidated_chunk: consolidated_chunk.clone(), + }; + + if let Err(_) = self.response_sender.send(response).await { + log::warn!("Failed to send consolidated chunk to RT thread"); + } + + // Step 3: Save WAV file in background (I/O intensive) + let file_path = self.get_file_path(); + + match self.save_wav_file(&consolidated_chunk, request.sample_rate, &file_path).await { + Ok(_) => log::info!("Saved recording to {:?}", file_path), + Err(e) => log::error!("Failed to save recording to {:?}: {}", file_path, e), + } + + Ok(()) + } + + /// Save consolidated chunk as WAV file + async fn save_wav_file( + &self, + chunk: &AudioChunk, + sample_rate: u32, + file_path: &PathBuf, + ) -> Result<()> { + // Run WAV writing in blocking task to avoid blocking async runtime + let chunk_samples: Vec = chunk.samples[..chunk.sample_count].to_vec(); + let file_path_clone = file_path.clone(); + + tokio::task::spawn_blocking(move || { + let spec = hound::WavSpec { + channels: 1, + sample_rate, + bits_per_sample: 32, + sample_format: hound::SampleFormat::Float, + }; + + let mut writer = hound::WavWriter::create(&file_path_clone, spec) + .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; + + // Write all samples from the chunk + for sample in chunk_samples { + writer.write_sample(sample) + .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; + } + + writer.finalize() + .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; + + Ok::<(), LooperError>(()) + }) + .await + .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))? + } + + /// Create save directory and return path + fn create_directory() -> Result { + let mut path = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")); + path.push(".fcb_looper"); + + std::fs::create_dir_all(&path) + .map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?; + + Ok(path) + } + + /// Get file path for track recording + fn get_file_path(&self) -> PathBuf { + self.directory.join("track.wav") + } +} \ No newline at end of file diff --git a/src/process_handler.rs b/src/process_handler.rs index f2d9622..98a23bb 100644 --- a/src/process_handler.rs +++ b/src/process_handler.rs @@ -1,6 +1,4 @@ use crate::*; -use crate::metronome::Metronome; -use crate::track::{TrackTiming, TrackState}; pub struct ProcessHandler { track: Track, @@ -8,6 +6,7 @@ pub struct ProcessHandler { pub ports: JackPorts, chunk_factory: F, metronome: Metronome, + post_record_controller: PostRecordController, } impl ProcessHandler { @@ -16,6 +15,7 @@ impl ProcessHandler { mut chunk_factory: F, beep_samples: Arc, state: &State, + post_record_controller: PostRecordController, ) -> Result { Ok(Self { track: Track::new(&mut chunk_factory)?, @@ -23,6 +23,7 @@ impl ProcessHandler { ports, chunk_factory, metronome: Metronome::new(beep_samples, state), + post_record_controller, }) } @@ -66,16 +67,25 @@ impl jack::ProcessHandler for ProcessHandler { let state_before = self.track.current_state().clone(); // Calculate timing information for track processing - let timing = self.calculate_track_timing(beat_sample_index, buffer_size, &state_before); + let timing = self.calculate_track_timing(beat_sample_index, &state_before); // Process track audio with calculated timing - if let Err(e) = self.track.process( + let should_consolidate = match self.track.process( ps, &mut self.ports, timing, &mut self.chunk_factory ) { - log::error!("Error processing track: {}", e); + Ok(consolidate) => consolidate, + Err(e) => { + log::error!("Error processing track: {}", e); + return jack::Control::Quit; + } + }; + + // Handle post-record processing + if let Err(e) = self.handle_post_record_processing(should_consolidate, client.sample_rate() as u32) { + log::error!("Error handling post-record processing: {}", e); return jack::Control::Quit; } @@ -87,11 +97,35 @@ impl jack::ProcessHandler for ProcessHandler { } impl ProcessHandler { + /// Handle post-record processing: send requests and swap chunks + fn handle_post_record_processing(&mut self, should_consolidate: bool, sample_rate: u32) -> Result<()> { + // Send chunk chain for processing if track indicates consolidation needed + if should_consolidate { + let chunk_chain = self.track.get_audio_chunk(); + log::debug!("Sending chunk chain with {} samples for post-record processing", + chunk_chain.len()); + + if let Err(e) = self.post_record_controller.send_request(chunk_chain, sample_rate) { + log::warn!("Failed to send post-record request: {}", e); + } + } + + // Check for consolidation response + if let Some(response) = self.post_record_controller.try_recv_response() { + log::debug!("Received consolidated chunk with {} samples, swapping in track", + response.consolidated_chunk.len()); + + // Swap the consolidated chunk into the track + self.track.set_audio_chunk(response.consolidated_chunk)?; + } + + Ok(()) + } + /// Calculate timing information for track processing fn calculate_track_timing( &self, beat_sample_index: Option, - buffer_size: usize, state_before: &TrackState, ) -> TrackTiming { match beat_sample_index { diff --git a/src/track.rs b/src/track.rs index 9214acc..4bfe0c5 100644 --- a/src/track.rs +++ b/src/track.rs @@ -21,7 +21,7 @@ pub enum TrackTiming { } pub struct Track { - audio_chunks: Option>, + audio_chunk: Arc, length: usize, current_state: TrackState, next_state: TrackState, @@ -31,7 +31,7 @@ pub struct Track { impl Track { pub fn new(chunk_factory: &mut F) -> Result { Ok(Self { - audio_chunks: None, + audio_chunk: chunk_factory.create_chunk()?, length: 0, current_state: TrackState::Empty, next_state: TrackState::Empty, @@ -40,18 +40,19 @@ impl Track { } /// Main audio processing method called from ProcessHandler + /// Returns true if track should be consolidated and saved pub fn process( &mut self, ps: &jack::ProcessScope, ports: &mut JackPorts, timing: TrackTiming, chunk_factory: &mut F, - ) -> Result<()> { + ) -> Result { let input_buffer = ports.audio_in.as_slice(ps); let output_buffer = ports.audio_out.as_mut_slice(ps); let buffer_size = output_buffer.len(); - match timing { + let should_consolidate = match timing { TrackTiming::NoBeat { position } => { // No beat in this buffer - process entire buffer with current state self.process_audio_range( @@ -62,6 +63,7 @@ impl Track { position, chunk_factory, )?; + false // No state transition possible without beat } TrackTiming::Beat { pre_beat_position, @@ -80,8 +82,8 @@ impl Track { )?; } - // Apply state transition at beat boundary - self.apply_state_transition(chunk_factory)?; + // Apply state transition at beat boundary and check if consolidation needed + let should_consolidate = self.apply_state_transition(chunk_factory)?; if beat_sample_index < buffer_size { // Process samples after beat with new current state @@ -94,10 +96,12 @@ impl Track { chunk_factory, )?; } - } - } - Ok(()) + should_consolidate + } + }; + + Ok(should_consolidate) } /// Process audio for a specific range within the buffer @@ -142,19 +146,24 @@ impl Track { } /// Apply state transition from next_state to current_state + /// Returns true if track should be consolidated and saved fn apply_state_transition( &mut self, chunk_factory: &mut F - ) -> Result<()> { + ) -> Result { + // Check if this is a Recording → Playing transition (consolidation trigger) + let should_consolidate = self.current_state == TrackState::Recording + && self.next_state == TrackState::Playing; + // Handle transitions that require setup match (&self.current_state, &self.next_state) { - (_, TrackState::Recording) => { + (current_state, TrackState::Recording) if *current_state != TrackState::Recording => { // Starting to record - clear previous data and create new chunk self.clear_audio_data(chunk_factory)?; } (_, TrackState::Playing) => { // Starting playback - check if we have audio data - if self.audio_chunks.is_none() || self.length == 0 { + if self.length == 0 { // No audio data - transition to Idle instead self.next_state = TrackState::Idle; } @@ -162,7 +171,7 @@ impl Track { (_, TrackState::Empty) => { // Clear operation - remove audio data // Note: Actual deallocation will happen later via IO thread - self.audio_chunks = None; + self.audio_chunk = chunk_factory.create_chunk()?; self.length = 0; } _ => { @@ -173,7 +182,7 @@ impl Track { // Apply the state transition self.current_state = self.next_state.clone(); - Ok(()) + Ok(should_consolidate) } /// Copy samples from track audio to output buffer with looping @@ -183,42 +192,37 @@ impl Track { sample_count: usize, mut playback_position: usize, ) -> Result<()> { - if let Some(ref audio_chunks) = self.audio_chunks { - if self.length == 0 { - output_slice.fill(0.0); - return Ok(()); - } - - let mut samples_written = 0; - - while samples_written < sample_count { - let samples_remaining = sample_count - samples_written; - let samples_until_loop = self.length - playback_position; - let samples_to_copy = samples_remaining.min(samples_until_loop); - - // Copy directly from audio chunks to output slice - audio_chunks.copy_samples( - &mut output_slice[samples_written..samples_written + samples_to_copy], - playback_position, - )?; - - // Apply volume scaling in-place - for sample in &mut output_slice[samples_written..samples_written + samples_to_copy] { - *sample *= self.volume; - } - - samples_written += samples_to_copy; - playback_position += samples_to_copy; - - // Handle looping - if playback_position >= self.length { - playback_position = 0; - } - } - } else { + if self.length == 0 { output_slice.fill(0.0); + return Ok(()); } + let mut samples_written = 0; + + while samples_written < sample_count { + let samples_remaining = sample_count - samples_written; + let samples_until_loop = self.length - playback_position; + let samples_to_copy = samples_remaining.min(samples_until_loop); + + // Copy directly from audio chunk to output slice + self.audio_chunk.copy_samples( + &mut output_slice[samples_written..samples_written + samples_to_copy], + playback_position, + )?; + + // Apply volume scaling in-place + for sample in &mut output_slice[samples_written..samples_written + samples_to_copy] { + *sample *= self.volume; + } + + samples_written += samples_to_copy; + playback_position += samples_to_copy; + + // Handle looping + if playback_position >= self.length { + playback_position = 0; + } + } Ok(()) } @@ -228,15 +232,8 @@ impl Track { samples: &[f32], chunk_factory: &mut F, ) -> Result<()> { - if self.audio_chunks.is_none() { - self.audio_chunks = Some(chunk_factory.create_chunk()?); - } - - if let Some(ref mut chunks) = self.audio_chunks { - chunks.append_samples(samples, chunk_factory)?; - self.length += samples.len(); - } - + self.audio_chunk.append_samples(samples, chunk_factory)?; + self.length += samples.len(); Ok(()) } @@ -245,11 +242,25 @@ impl Track { &mut self, chunk_factory: &mut F, ) -> Result<()> { - self.audio_chunks = Some(chunk_factory.create_chunk()?); + self.audio_chunk = chunk_factory.create_chunk()?; self.length = 0; Ok(()) } + /// Get audio chunk for post-record processing (read-only access) + pub fn get_audio_chunk(&self) -> Arc { + self.audio_chunk.clone() + } + + /// Set audio chunk (for swapping in consolidated chunk) + pub fn set_audio_chunk(&mut self, chunk: Arc) -> Result<()> { + if chunk.len() != self.length { + return Err(LooperError::OutOfBounds(std::panic::Location::caller())); + } + self.audio_chunk = chunk; + Ok(()) + } + // Public accessors and commands for MIDI handling pub fn current_state(&self) -> &TrackState { &self.current_state @@ -259,10 +270,6 @@ impl Track { &self.next_state } - pub fn set_next_state(&mut self, state: TrackState) { - self.next_state = state; - } - pub fn len(&self) -> usize { self.length }