From d959d0b944587b31bc51daf265070f8fc6af951b Mon Sep 17 00:00:00 2001 From: Noah Bennett Date: Mon, 26 Feb 2024 12:33:46 -0500 Subject: [PATCH 1/3] updating playback to run all samples instead of slices --- engine/src/daw/daw.rs | 162 +++++++++++++------------- engine/src/daw/daw_core/audiograph.rs | 142 ++++++++++++++++++---- engine/src/main.rs | 2 +- src/state/slices/playlistSlice.ts | 4 +- src/views/playlist/sketch/Cursor.ts | 2 +- 5 files changed, 206 insertions(+), 106 deletions(-) diff --git a/engine/src/daw/daw.rs b/engine/src/daw/daw.rs index 2b874e4..2ba6151 100644 --- a/engine/src/daw/daw.rs +++ b/engine/src/daw/daw.rs @@ -152,88 +152,90 @@ pub fn run_playlist(state_ref: &Arc) { .unwrap() .interval_of_subdivision(daw_core::timing::WholeNote::new()); - pool.exec(move || { - thread::spawn(move || loop { - println!("tick: {}ms", &state.playlist.audiograph.lock().unwrap().current_offset.unwrap().as_millis()); - - // play metronome if enabled - // if state.metronome_enabled.load(Ordering::SeqCst) - // && state.playlist.playing() { - // play_metronome(&state); - // } - - // run ahead n milliseconds and schedule the next - // samples in the audio graph to be played - let mut audiograph_ref = state.playlist.audiograph.lock().unwrap(); - - audiograph_ref.run_for(tempo_interval); - // let m = audiograph_ref.buffer_for(tempo_interval); - // match m { - // Some(x) => { - // futures::executor::block_on(play_buffer(x.take_duration(tempo_interval).buffered())); - // } - // None => {} - // } - - // sleep this thread for the length of a single beat - thread::sleep(tempo_interval); + let graph = &state.playlist.audiograph.lock().unwrap(); + graph.run(); + // pool.exec(move || { + // thread::spawn(move || loop { + // println!("tick: {}ms", &state.playlist.audiograph.lock().unwrap().current_offset.unwrap().as_millis()); + + // // play metronome if enabled + // // if state.metronome_enabled.load(Ordering::SeqCst) + // // && state.playlist.playing() { + // // play_metronome(&state); + // // } + + // // run ahead n milliseconds and schedule the next + // // samples in the audio graph to be played + // let mut audiograph_ref = state.playlist.audiograph.lock().unwrap(); + + // audiograph_ref.run_for(tempo_interval); + // // let m = audiograph_ref.buffer_for(tempo_interval); + // // match m { + // // Some(x) => { + // // futures::executor::block_on(play_buffer(x.take_duration(tempo_interval).buffered())); + // // } + // // None => {} + // // } + + // // sleep this thread for the length of a single beat + // thread::sleep(tempo_interval); - let curr = audiograph_ref.current_offset.unwrap(); - - if state.playlist.playing() { - audiograph_ref.set_current_offset(Some(curr + tempo_interval)); - } else { - audiograph_ref.set_current_offset(Some(curr)); - } - - // increment the beat counter - let current_time_signature = - state.playlist.time_signature.lock().unwrap(); - let current_beat = - state.playlist.current_beat.load(Ordering::SeqCst); - let next_beat = (current_beat + 1) % current_time_signature.numerator; + // let curr = audiograph_ref.current_offset.unwrap(); + + // if state.playlist.playing() { + // audiograph_ref.set_current_offset(Some(curr + tempo_interval)); + // } else { + // audiograph_ref.set_current_offset(Some(curr)); + // } + + // // increment the beat counter + // let current_time_signature = + // state.playlist.time_signature.lock().unwrap(); + // let current_beat = + // state.playlist.current_beat.load(Ordering::SeqCst); + // let next_beat = (current_beat + 1) % current_time_signature.numerator; - if state.playlist.playing() { - // loop the playlist by setting the current offset to zero, - // if looping is enabled, otherwise, add and store the next - // current beat - if state.playlist.loop_enabled.load(Ordering::SeqCst) - && ((state.playlist.total_beats.load(Ordering::SeqCst) + 1) as u64 - >= state.playlist.max_beats.load(Ordering::SeqCst)) { - audiograph_ref.set_current_offset(Some(Duration::ZERO)); - - state - .playlist - .current_beat - .store(next_beat, Ordering::SeqCst); - state - .playlist - .total_beats - .store(0, Ordering::SeqCst); - } else { - state - .playlist.current_beat - .store(next_beat, Ordering::SeqCst); - state - .playlist.total_beats - .fetch_add(1, Ordering::SeqCst); - } - } else { - state.playlist.current_beat.store(0, Ordering::SeqCst); - } - - println!( - "current beat: {}, total beats played: {}, looping: {}", - state.playlist.current_beat.load(Ordering::SeqCst), - state.playlist.total_beats.load(Ordering::SeqCst), - state.playlist.loop_enabled.load(Ordering::SeqCst) - ); - - if !state.playlist.playing() { - break; - } - }); - }); + // if state.playlist.playing() { + // // loop the playlist by setting the current offset to zero, + // // if looping is enabled, otherwise, add and store the next + // // current beat + // if state.playlist.loop_enabled.load(Ordering::SeqCst) + // && ((state.playlist.total_beats.load(Ordering::SeqCst) + 1) as u64 + // >= state.playlist.max_beats.load(Ordering::SeqCst)) { + // audiograph_ref.set_current_offset(Some(Duration::ZERO)); + + // state + // .playlist + // .current_beat + // .store(next_beat, Ordering::SeqCst); + // state + // .playlist + // .total_beats + // .store(0, Ordering::SeqCst); + // } else { + // state + // .playlist.current_beat + // .store(next_beat, Ordering::SeqCst); + // state + // .playlist.total_beats + // .fetch_add(1, Ordering::SeqCst); + // } + // } else { + // state.playlist.current_beat.store(0, Ordering::SeqCst); + // } + + // println!( + // "current beat: {}, total beats played: {}, looping: {}", + // state.playlist.current_beat.load(Ordering::SeqCst), + // state.playlist.total_beats.load(Ordering::SeqCst), + // state.playlist.loop_enabled.load(Ordering::SeqCst) + // ); + + // if !state.playlist.playing() { + // break; + // } + // }); + // }); println!("continuing"); } diff --git a/engine/src/daw/daw_core/audiograph.rs b/engine/src/daw/daw_core/audiograph.rs index b0be210..27e987c 100644 --- a/engine/src/daw/daw_core/audiograph.rs +++ b/engine/src/daw/daw_core/audiograph.rs @@ -35,11 +35,13 @@ use rodio::dynamic_mixer::{ DynamicMixerController, }; use rodio::buffer::{SamplesBuffer}; +use futures; #[cfg(target_os = "linux")] use psimple; #[cfg(target_os = "linux")] use pulse; +use serde::__private::de; #[cfg(target_os = "linux")] #[derive(Clone)] @@ -211,7 +213,7 @@ pub struct AudioNode { // audio node implementation for Mac/Windows #[cfg(not(target_os = "linux"))] -impl AudioNode { +impl AudioNode { pub fn new( id: u64, sample_path: String, @@ -456,9 +458,27 @@ impl AudioGraph<'static> { self.running = true; } - // pub fn play_node(&self, node: &AudioNode) { - // self.controller.add(node.samples.lock().unwrap()); - // } + pub fn run(&self) { + let (_controller, mixer) = self.buffer_graph().unwrap(); + + println!("running full graph:"); + let running = self.running; + thread::spawn(move || { + if running { + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let sink = Sink::try_new(&stream_handle).unwrap(); + sink.pause(); + sink.append(mixer); + println!("System time (PB): {:?}", std::time::Instant::now()); + sink.play(); + sink.sleep_until_end(); + + // sink.detach(); + // stream_handle.play_raw(mixer).unwrap(); + // thread::sleep(dur); + } + }); + } // run graph and schedule nodes to be played // n milliseconds in advance @@ -488,14 +508,48 @@ impl AudioGraph<'static> { }); } - pub fn buffer_slice(&self, dur: Duration) -> Option<(Arc>, DynamicMixer)> { + pub fn buffer_graph(&self) -> Option<(Arc>, DynamicMixer)> { + let (controller, mixer) = rodio::dynamic_mixer::mixer::(2, self.sample_rate); + + // find total play duration + if self.nodes.len() == 0 { + return Some((controller, mixer)); + } + + // let dur = self.duration(); + if self.nodes.len() == 0 { return Some((controller, mixer)); } + let last: &AudioNode = self.nodes.last().unwrap(); + + let dur = last.start_offset + last.duration(); + + // buffer silence let silence = rodio::source::Zero::::new(2, self.sample_rate).take_duration(dur); + controller.add(silence); + + // buffer nodes + let self_arc = Arc::new(Mutex::new(self)); + let slice = self_arc.lock().unwrap().nodes[0..self.nodes.len()].to_vec(); + + for node in slice { + let start_offset = Arc::new(Mutex::new(node.start_offset)); + let delay: Duration = *start_offset.lock().unwrap(); + println!("delay: {}ms",delay.as_millis()); + + let source = node.buffer.decoder().delay(*start_offset.lock().unwrap()).convert_samples(); + controller.add(source); + } + + Some((controller, mixer)) + } + + pub fn buffer_slice(&self, dur: Duration) -> Option<(Arc>, DynamicMixer)> { let (controller, mixer) = rodio::dynamic_mixer::mixer::(2, self.sample_rate); + let silence = rodio::source::Zero::::new(2, self.sample_rate).take_duration(dur); controller.add(silence); // todo: add conditional for metronome state - // controller.add(daw::METRONOME_TICK_SOURCE.convert_samples()); + controller.add(daw::METRONOME_TICK_SOURCE.convert_samples()); // get starting index of nodes within this time slice let idx_start_opt = self.nodes.iter() @@ -666,6 +720,7 @@ impl AudioGraph<'static> { track_number, self.sample_rate, ); + println!("start offset is: {}ms", start_offset.as_millis()); self.add_node(node); @@ -921,7 +976,12 @@ impl AudioGraph<'static> { mod tests { use super::*; use crate::daw::{self}; - use futures_test::{self}; + + macro_rules! aw { + ($e:expr) => { + futures::executor::block_on($e) + }; + } #[test] fn test_node_initialization() { @@ -941,8 +1001,8 @@ mod tests { assert_eq!(node.track_number, track_number); } - #[futures_test::test] - async fn test_node_play() { + #[test] + fn test_node_play() { let id = 1; let sample_path = daw::METRONOME_TICK_PATH.to_string(); let start_offset = Duration::from_millis(0); @@ -953,7 +1013,7 @@ mod tests { let now = Instant::now(); - node.play().await; + aw!(node.play()); thread::sleep(Duration::from_millis(100)); @@ -987,8 +1047,8 @@ mod tests { assert_eq!(audiograph.duration(), start_offset + node_dur); } - #[futures_test::test] - async fn test_audiograph_run() { + #[test] + fn test_audiograph_run() { let id = 1; let sample_path = daw::METRONOME_TICK_PATH.to_string(); let start_offset = Duration::from_millis(650); @@ -1021,8 +1081,8 @@ mod tests { assert_eq!(result, true); } - #[futures_test::test] - async fn test_audiograph_buffer_slice_with_silence() { + #[test] + fn test_audiograph_buffer_slice_with_silence() { let sample_rate = 44_100; let tempo = 120f32; let max_beats: u64 = 8; @@ -1030,7 +1090,7 @@ mod tests { let audiograph = AudioGraph::new(sample_rate, tempo, max_beats); let runtime = Duration::from_secs(1); - let (_controller, mixer) = audiograph.buffer_slice(runtime).unwrap(); + let mixer = audiograph.buffer_for(runtime).unwrap(); let mut zero_samples = Vec::::new(); let mut nonzero_samples = Vec::::new(); @@ -1046,8 +1106,46 @@ mod tests { assert_eq!(nonzero_samples.len(), 0); } - #[futures_test::test] - async fn test_audiograph_buffer_slice_with_node() { + fn test_audiograph_buffer_graph() { + let id = 1; + let sample_path = daw::METRONOME_TICK_PATH.to_string(); + let start_offset = Duration::from_millis(650); + let track_number = 0; + let sample_rate = 44_100; + + let node = AudioNode::new(id, sample_path, start_offset, track_number, sample_rate); + + let tempo = 120f32; + let max_beats: u64 = 8; + + let mut audiograph = AudioGraph::new(sample_rate, tempo, max_beats); + let buf = node.buffer.clone(); + + audiograph.add_node(node); + + let runtime = Duration::from_secs(1); + + let (_controller, mixer) = audiograph.buffer_graph().unwrap(); + + let mut nonzero_samples = Vec::::new(); + mixer.for_each(|x| { + if x != 0f32 { + nonzero_samples.push(x); + } + }); + + let mut node_samples = Vec::::new(); + buf.convert_samples().for_each(|x| { + if x != 0f32 { + node_samples.push(x); + } + }); + + assert_eq!(node_samples.len() * 4, nonzero_samples.len()); + } + + #[test] + fn test_audiograph_buffer_slice_with_node() { let id = 1; let sample_path = daw::METRONOME_TICK_PATH.to_string(); let start_offset = Duration::from_millis(650); @@ -1082,11 +1180,11 @@ mod tests { } }); - assert_eq!(node_samples.len() * 2, nonzero_samples.len()); + assert_eq!(node_samples.len() * 4, nonzero_samples.len()); } - #[futures_test::test] - async fn test_audiograph_buffer_set_functions() { + #[test] + fn test_audiograph_buffer_set_functions() { let sample_rate = 44_100; let tempo = 120f32; let max_beats: u64 = 8; @@ -1106,8 +1204,8 @@ mod tests { assert_eq!(audiograph.tempo(), 75f32); } - #[futures_test::test] - async fn test_audiograph_node_functions() { + #[test] + fn test_audiograph_node_functions() { let id = 100; let sample_path = daw::METRONOME_TICK_PATH.to_string(); let start_offset = Duration::from_millis(350); diff --git a/engine/src/main.rs b/engine/src/main.rs index f04ecf8..0a4b824 100644 --- a/engine/src/main.rs +++ b/engine/src/main.rs @@ -337,11 +337,11 @@ fn get_node_data( .lock() .unwrap(); let node = audiograph.get_mut_node_by_id(id).unwrap(); + println!("offset dur: {:?}", node.start_offset.as_millis()); let waveform = node.get_waveform().clone(); let dur = node.duration().as_secs_f32(); let ratio = dur / audiograph.max_beats() as f32; - println!("waveform dur: {:?}", dur); // return waveform data Ok((waveform, dur)) diff --git a/src/state/slices/playlistSlice.ts b/src/state/slices/playlistSlice.ts index 55daa15..3b12f18 100644 --- a/src/state/slices/playlistSlice.ts +++ b/src/state/slices/playlistSlice.ts @@ -190,8 +190,8 @@ export const addToPlaylist = ( trackNumber: number, pixelOffset: Omit, ) => async (dispatch: Dispatch) => { - const dropX = pixelOffset.x - const dropY = pixelOffset.y + const dropX = pixelOffset.x - pixelOffset.left + const dropY = pixelOffset.y - pixelOffset.top const id = await invoke('add_audiograph_node', { samplePath: path, trackNumber, diff --git a/src/views/playlist/sketch/Cursor.ts b/src/views/playlist/sketch/Cursor.ts index c23c74f..358df25 100644 --- a/src/views/playlist/sketch/Cursor.ts +++ b/src/views/playlist/sketch/Cursor.ts @@ -37,7 +37,7 @@ class Cursor extends PlaylistComponent { if(!playlistStart) return null const delta = now - playlistStart - const ratio = delta / (((this.renderer as Renderer).width - (this.renderer as Renderer).maxPlaylistBeats) / (this.renderer as Renderer).maxPlaylistBeats) * 4 + const ratio = delta / (((this.renderer as Renderer).width - (this.renderer as Renderer).maxPlaylistBeats * 4) / (this.renderer as Renderer).maxPlaylistBeats) * 2 return ratio * this.currentScale } From 95aa9d2e154769f62b7186c92b9f21401f645fd8 Mon Sep 17 00:00:00 2001 From: Noah Bennett Date: Wed, 27 Mar 2024 11:23:22 -0400 Subject: [PATCH 2/3] wip refactiring --- engine/src/app/workspaces/playlist.rs | 2 +- engine/src/mod.rs | 7 +++++++ src/views/playlist/sketch/Cursor.ts | 2 +- src/views/playlist/sketch/Timeline.ts | 6 ++++-- src/views/playlist/sketch/index.ts | 4 ++-- 5 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 engine/src/mod.rs diff --git a/engine/src/app/workspaces/playlist.rs b/engine/src/app/workspaces/playlist.rs index 379970d..6973266 100644 --- a/engine/src/app/workspaces/playlist.rs +++ b/engine/src/app/workspaces/playlist.rs @@ -12,7 +12,7 @@ pub fn calc_sample_offset( let width = max_bound_x - min_bound_x; // let adjusted_min_x = 0. as f32; let adjusted_max_x = width; - let adjusted_drop_x = drop_x - min_bound_x; + let adjusted_drop_x = drop_x - min_bound_x - 100.; println!("drop_x: {}, min_bound_x: {}, max_bound_x: {}, adjusted_drop_x: {}", drop_x, min_bound_x, max_bound_x, adjusted_drop_x); let ratio = adjusted_drop_x / max_bound_x; diff --git a/engine/src/mod.rs b/engine/src/mod.rs new file mode 100644 index 0000000..a047d49 --- /dev/null +++ b/engine/src/mod.rs @@ -0,0 +1,7 @@ +mod app; +mod daw; +mod util; + +pub use app::*; +pub use daw::*; +pub use util::*; diff --git a/src/views/playlist/sketch/Cursor.ts b/src/views/playlist/sketch/Cursor.ts index 358df25..9ea0b75 100644 --- a/src/views/playlist/sketch/Cursor.ts +++ b/src/views/playlist/sketch/Cursor.ts @@ -37,7 +37,7 @@ class Cursor extends PlaylistComponent { if(!playlistStart) return null const delta = now - playlistStart - const ratio = delta / (((this.renderer as Renderer).width - (this.renderer as Renderer).maxPlaylistBeats * 4) / (this.renderer as Renderer).maxPlaylistBeats) * 2 + const ratio = delta * 16 / ((this.renderer as Renderer).width / 16) return ratio * this.currentScale } diff --git a/src/views/playlist/sketch/Timeline.ts b/src/views/playlist/sketch/Timeline.ts index 641ed04..1ed030d 100644 --- a/src/views/playlist/sketch/Timeline.ts +++ b/src/views/playlist/sketch/Timeline.ts @@ -35,12 +35,14 @@ class Timeline extends PlaylistComponent { this.p.fill('#222') this.p.textFont('Helvetica') - for(let i = 0; i < 32; i++) { + const limit = 16 + + for(let i = 0; i < limit; i++) { this.p.push() this.p.scale(1 / this.currentScale, 1) this.p.text( i+1, - (i * (this.timelineWidth * this.currentScale / 32) + 2), + (i * (this.timelineWidth * this.currentScale / limit) + 2), this.timelineHeight - 2 ) this.p.pop() diff --git a/src/views/playlist/sketch/index.ts b/src/views/playlist/sketch/index.ts index f422593..481fa24 100644 --- a/src/views/playlist/sketch/index.ts +++ b/src/views/playlist/sketch/index.ts @@ -134,7 +134,7 @@ export class Renderer extends RendererBase { for(let i = 0; i < this.playlistTracks.length; i++) { const track = this.playlistTracks[i] const [min, max] = [track.minHeight, track.maxHeight] - console.log("min, max, dropy", min, max, dropY) + console.log("track data: min, max, dropy", min, max, dropY) if(min < dropY && dropY < max) { trackNumber = track.trackNumber @@ -432,7 +432,7 @@ export class Renderer extends RendererBase { const gridStroke = .2 * (1 - currentScale) > 0 ? .2 * (1 - currentScale) : .4 p.strokeWeight(gridStroke) p.stroke(100, 100, 100) - for(let i = 0; i < width; i += width/maxPlaylistBeats) { + for(let i = 0; i < width; i += width/16) { // p.line(i, 0, i, height) p.line(i, 0, i, trackCount * staticDefaults.trackHeight + staticDefaults.timelineHeight + 1) } From 738cbf13fd4088ef6a56d08ac67a71ba7b1500df Mon Sep 17 00:00:00 2001 From: Noah Bennett Date: Tue, 2 Apr 2024 15:47:17 -0400 Subject: [PATCH 3/3] add debug mode, separate daw actions into dispatched events, add basic interpreter --- engine/src/daw/daw.rs | 2 +- engine/src/daw/state/state.rs | 2 + engine/src/event_dispatch.rs | 494 ++++++++++++++++++++++++++++ engine/src/lang/chunk.rs | 120 +++++++ engine/src/lang/function.rs | 28 ++ engine/src/lang/mod.rs | 11 + engine/src/lang/opcode.rs | 70 ++++ engine/src/lang/operator.rs | 34 ++ engine/src/lang/parse_rule.rs | 8 + engine/src/lang/parser.rs | 491 ++++++++++++++++++++++++++++ engine/src/lang/precedence.rs | 33 ++ engine/src/lang/scanner.rs | 386 ++++++++++++++++++++++ engine/src/lang/token.rs | 359 ++++++++++++++++++++ engine/src/lang/value.rs | 126 +++++++ engine/src/lang/vm.rs | 359 ++++++++++++++++++++ engine/src/main.rs | 597 ++++++---------------------------- engine/src/mod.rs | 7 - package.json | 1 + 18 files changed, 2624 insertions(+), 504 deletions(-) create mode 100644 engine/src/event_dispatch.rs create mode 100644 engine/src/lang/chunk.rs create mode 100644 engine/src/lang/function.rs create mode 100644 engine/src/lang/mod.rs create mode 100644 engine/src/lang/opcode.rs create mode 100644 engine/src/lang/operator.rs create mode 100644 engine/src/lang/parse_rule.rs create mode 100644 engine/src/lang/parser.rs create mode 100644 engine/src/lang/precedence.rs create mode 100644 engine/src/lang/scanner.rs create mode 100644 engine/src/lang/token.rs create mode 100644 engine/src/lang/value.rs create mode 100644 engine/src/lang/vm.rs delete mode 100644 engine/src/mod.rs diff --git a/engine/src/daw/daw.rs b/engine/src/daw/daw.rs index 2ba6151..b91e414 100644 --- a/engine/src/daw/daw.rs +++ b/engine/src/daw/daw.rs @@ -15,7 +15,7 @@ use std::sync::{ use std::sync::atomic::{Ordering}; use std::thread; use std::time::{ - Duration, + Duration, Instant, }; use rodio::{Source}; diff --git a/engine/src/daw/state/state.rs b/engine/src/daw/state/state.rs index b116074..79c5bc6 100644 --- a/engine/src/daw/state/state.rs +++ b/engine/src/daw/state/state.rs @@ -46,3 +46,5 @@ impl InnerState { .set_tempo(tempo); } } + +pub type TState<'a> = tauri::State<'a, Arc>; diff --git a/engine/src/event_dispatch.rs b/engine/src/event_dispatch.rs new file mode 100644 index 0000000..48bf29d --- /dev/null +++ b/engine/src/event_dispatch.rs @@ -0,0 +1,494 @@ +use std::sync::atomic::{Ordering}; +use std::thread; +use std::time::{SystemTime}; +use tauri; + +use crate::{app, daw, util}; + +#[tauri::command] +pub fn toggle_playlist(state: daw::TState<'_>) { + if state.playlist.playing() { + daw::pause_playlist(state); + } else { + // start playlist + daw::start_playlist(state); + } +} + +#[tauri::command] +pub fn get_playlist_playing( + state: daw::TState<'_> +) -> Result { + Ok(state.playlist.playing()) +} + +#[cfg(target_os = "windows")] +#[tauri::command] +pub fn get_playlist_start_time( + state: tauri::State<'_, Arc> +) -> Result { + let elapsed = state + .playlist + .started_time + .lock() + .unwrap() + .unwrap() + .elapsed(); + let now = SystemTime::now() + .duration_since(SystemTime::Windows) + .unwrap(); + let start_time = now - elapsed; + Ok(start_time.as_millis()) +} + +#[cfg(not(target_os = "windows"))] +#[tauri::command] +pub fn get_playlist_start_time( + state: daw::TState<'_> +) -> Result { + let elapsed = state + .playlist + .started_time + .lock() + .unwrap() + .unwrap() + .elapsed(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let start_time = now - elapsed; + Ok(start_time.as_millis()) +} + +#[tauri::command] +pub fn get_playlist_tempo( + state: daw::TState<'_> +) -> Result { + Ok(state.tempo()) +} + +#[tauri::command] +pub fn set_playlist_tempo( + state: daw::TState<'_>, + val: f32, +) { + println!("playlist tempo updated: {}", val); + daw::set_playlist_tempo(state, val); +} + +#[tauri::command] +pub fn toggle_metronome_enabled( + state: daw::TState<'_> +) { + let val = !state.metronome_enabled.load(Ordering::SeqCst); + state.metronome_enabled.store(val, Ordering::SeqCst); +} + +#[tauri::command] +pub fn get_metronome_enabled( + state: daw::TState<'_> +) -> Result { + Ok(state.metronome_enabled.load(Ordering::SeqCst)) +} + +#[tauri::command] +pub fn get_playlist_runtime_formatted( + state: daw::TState<'_> +) -> Result { + let start_time = state.playlist.started_time.lock().unwrap().unwrap(); + let res = util::format_playlist_runtime(start_time); + Ok(res) +} + +#[tauri::command] +pub fn get_playlist_beat_count( + state: daw::TState<'_> +) -> Result { + let beat_count = state.playlist.total_beats.load(Ordering::SeqCst); + Ok(beat_count) +} + +#[tauri::command] +pub fn get_playlist_time_signature( + state: daw::TState<'_> +) -> Result<(u16, u16), String> { + let res = state.playlist.time_signature.lock().unwrap(); + Ok((res.numerator, res.denominator)) +} + +#[tauri::command] +pub fn set_playlist_time_signature( + state: daw::TState<'_>, + numerator: u16, + denominator: u16, +) { + let updated: daw::timing::TimeSignature = daw::timing::TimeSignature { + numerator, + denominator, + }; + + *state.playlist.time_signature.lock().unwrap() = updated; +} + +#[tauri::command] +pub fn get_sidebar_samples() -> Result<( + Vec, + Vec, + Vec, + Vec +), String> { + let (samples, samples_paths, dirs, dirs_paths) = + app::env::get_sample_browser_root(); + Ok((samples, samples_paths, dirs, dirs_paths)) +} + +#[tauri::command] +pub fn enumerate_directory(dir_path: String) -> Result<( + Vec, + Vec, + Vec, + Vec +), String> { + let (samples, samples_paths, dirs, dirs_paths) = + app::env::enumerate_files_in_dir(dir_path); + Ok((samples, samples_paths, dirs, dirs_paths)) +} + +#[tauri::command] +pub fn preview_sample( + _state: daw::TState<'_>, + path: String, +) { + thread::spawn(move || { + futures::executor::block_on(daw::play_sample(&path)); + }); +} + +#[tauri::command] +pub fn get_audio_drivers() -> Result, String> { + Ok(daw::drivers::get_sound_host_names()) +} + +#[tauri::command] +pub fn add_audiograph_node( + state: daw::TState<'_>, + sample_path: String, + track_number: u32, + drop_x: Option, + drop_y: Option, +) -> Result { + let min_bound_x = state.playlist.ui.lock().unwrap().viewport.min_bound_x.unwrap(); + let min_bound_y = state.playlist.ui.lock().unwrap().viewport.min_bound_y.unwrap(); + let max_bound_x = state.playlist.ui.lock().unwrap().viewport.max_bound_x.unwrap(); + let max_bound_y = state.playlist.ui.lock().unwrap().viewport.max_bound_y.unwrap(); + let tempo = state.playlist.audiograph.lock().unwrap().tempo(); + let max_beats_displayed = state.playlist.ui.lock().unwrap().max_beats_displayed; + println!("max_beats_displayed: {}", max_beats_displayed); + let max_playlist_dur = daw::timing::n_subdivision_duration_from_tempo( + tempo, + max_beats_displayed as u32, + daw::timing::QuarterNote::new() + ); + + println!("max playlist dur: {}ms", max_playlist_dur.as_millis()); + + let start_offset = app::workspaces::playlist::calc_sample_offset( + drop_x.unwrap_or(min_bound_x), + drop_y.unwrap_or(min_bound_y), + min_bound_x, + min_bound_y, + max_bound_x, + max_bound_y, + max_playlist_dur + ); + + println!("added sample: {}, with offset of: {}ms", sample_path, start_offset.as_millis()); + + let id: u64; + if state.playlist.ui.lock().unwrap().snap_enabled { + println!("snapping the sample"); + // snap to nearest snap subdivision + let subdivision = state + .playlist + .ui + .lock() + .unwrap() + .snap_subdivision; + + id = state + .playlist + .audiograph + .lock() + .unwrap() + .construct_and_add_node_with_snap( + sample_path, + start_offset, + track_number, + daw::timing::SixteenthNote::new()); + } else { + // don't snap + id = state + .playlist + .audiograph + .lock() + .unwrap() + .construct_and_add_node(sample_path, start_offset, track_number); + } + + // returns the id of the new node, offset + Ok(id) +} + +#[tauri::command] +pub fn move_audiograph_node( + state: daw::TState<'_>, + id: u64, + track_number: u32, + drop_x: Option, + drop_y: Option, +) { + let min_bound_x = state.playlist.ui.lock().unwrap().viewport.min_bound_x.unwrap(); + let min_bound_y = state.playlist.ui.lock().unwrap().viewport.min_bound_y.unwrap(); + let max_bound_x = state.playlist.ui.lock().unwrap().viewport.max_bound_x.unwrap(); + let max_bound_y = state.playlist.ui.lock().unwrap().viewport.max_bound_y.unwrap(); + let tempo = state.playlist.audiograph.lock().unwrap().tempo(); + let max_beats_displayed = state.playlist.ui.lock().unwrap().max_beats_displayed; + println!("max_beats_displayed: {}", max_beats_displayed); + let max_playlist_dur = daw::timing::n_subdivision_duration_from_tempo( + tempo, + max_beats_displayed as u32, + daw::timing::QuarterNote::new() + ); + + let start_offset = app::workspaces::playlist::calc_sample_offset( + drop_x.unwrap_or(min_bound_x), + drop_y.unwrap_or(min_bound_y), + min_bound_x, + min_bound_y, + max_bound_x, + max_bound_y, + max_playlist_dur + ); + + + println!("moved sample with id: {}, with offset of: {}ms", id, start_offset.as_millis()); + + if state.playlist.ui.lock().unwrap().snap_enabled { + println!("snapping the sample"); + // snap to nearest snap subdivision + let subdivision = state + .playlist + .ui + .lock() + .unwrap() + .snap_subdivision; + + state + .playlist + .audiograph + .lock() + .unwrap() + .move_node_with_snap( + id, + start_offset, + track_number, + daw::timing::SixteenthNote::new()); + } else { + // don't snap + state + .playlist + .audiograph + .lock() + .unwrap() + .move_node(id, start_offset, track_number); + } +} + +#[tauri::command] +pub fn remove_audiograph_node( + state: daw::TState<'_>, + id: u64, +) { + state + .playlist + .audiograph + .lock() + .unwrap() + .remove_node(id); +} + +#[tauri::command] +pub fn get_node_data( + state: daw::TState<'_>, + id: u64, +) -> Result<(Vec, f32), String> { + let playlist = &state.playlist; + let mut audiograph = playlist + .audiograph + .lock() + .unwrap(); + let node = audiograph.get_mut_node_by_id(id).unwrap(); + println!("offset dur: {:?}", node.start_offset.as_millis()); + let waveform = node.get_waveform().clone(); + let dur = node.duration().as_secs_f32(); + let ratio = dur / audiograph.max_beats() as f32; + + + // return waveform data + Ok((waveform, dur)) +} + +#[tauri::command] +pub fn get_playlist_sample_offset( + _state: daw::TState<'_>, + drop_x: f32, + drop_y: f32, + min_bound_x: f32, + min_bound_y: f32, + max_bound_x: f32, + max_bound_y: f32, +) -> Result { + // todo: choose a number that isn't arbitrary + let max_sample_offset = (max_bound_x - min_bound_x).round() as u64 * 5; + println!("max sample offset: {}", max_sample_offset); + let res = util::calc_playlist_sample_offset( + drop_x, + drop_y, + min_bound_x, + min_bound_y, + max_bound_x, + max_bound_y, + max_sample_offset, + ); + + Ok(res) +} + +#[tauri::command] +pub fn get_playlist_data( + state: daw::TState<'_>, +) -> Result<(u64, u64, f32, u32), String> { + let audiograph = state + .playlist + .audiograph + .lock() + .unwrap(); + let max_playlist_beats = state + .playlist + .max_beats + .load(Ordering::SeqCst); + let max_beats_displayed = state + .playlist + .ui + .lock() + .unwrap() + .max_beats_displayed; + let track_count = audiograph.track_count(); + let max_playlist_duration = audiograph + .duration_max() + .as_secs_f32(); + + println!("max dur: {}s, track count: {}", max_playlist_duration, track_count); + + Ok(( + max_playlist_beats, + max_beats_displayed, + max_playlist_duration, + track_count + )) +} + +#[tauri::command] +pub fn toggle_loop_enabled( + state: daw::TState<'_> +) { + let val = !state + .playlist + .loop_enabled + .load(Ordering::SeqCst); + + state + .playlist + .loop_enabled + .store(val, Ordering::SeqCst); +} + +#[tauri::command] +pub fn get_loop_enabled( + state: daw::TState<'_> +) -> Result { + Ok(state.playlist.loop_enabled.load(Ordering::SeqCst)) +} + +#[tauri::command] +pub fn toggle_snap_enabled( + state: daw::TState<'_> +) { + state + .playlist + .ui + .lock() + .unwrap() + .toggle_snap_enabled(); +} + +#[tauri::command] +pub fn get_snap_enabled( + state: daw::TState<'_> +) -> Result { + Ok(state.playlist.ui.lock().unwrap().snap_enabled) +} + +#[tauri::command] +pub fn get_playlist_max_length ( + state: daw::TState<'_> +) -> Result<(u64, u64), String> { + let dur = state + .playlist + .audiograph + .lock() + .unwrap() + .duration_max(); + let max_beats = state + .playlist + .audiograph + .lock() + .unwrap() + .max_beats(); + Ok((dur.as_millis().try_into().unwrap(), max_beats)) +} + +#[tauri::command] +pub fn init_playlist_workspace( + state: daw::TState<'_>, + min_bound_x: f32, + min_bound_y: f32, + max_bound_x: f32, + max_bound_y: f32, +) { + println!("min_bound_x: {}, max_bound_x: {}", min_bound_x, max_bound_x); + // update bounding box + state + .playlist + .ui + .lock() + .unwrap() + .viewport + .set_bounding_box( + min_bound_x, + min_bound_y, + max_bound_x, + max_bound_y); +} + +#[tauri::command] +pub fn toggle_record_input( + state: daw::TState<'_>, +) { + println!("toggling input recording"); + + let recording = &state + .playlist + .recording(); + daw::input::record_input(*recording); +} diff --git a/engine/src/lang/chunk.rs b/engine/src/lang/chunk.rs new file mode 100644 index 0000000..2f3a9a9 --- /dev/null +++ b/engine/src/lang/chunk.rs @@ -0,0 +1,120 @@ +use crate::lang::opcode::Opcode; +use crate::lang::value::Value; + +#[derive(Debug, Clone)] +pub struct Chunk { + pub code: Vec, + pub constants: Vec, + pub lines: Vec, +} + +impl Chunk { + pub fn new() -> Chunk { + Chunk { + code: Vec::new(), + constants: Vec::new(), + lines: Vec::new(), + } + } + + pub fn write(&mut self, byte: u8, line: usize) { + self.code.push(byte); + self.lines.push(line); + } + + pub fn add_constant(&mut self, value: Value) -> usize { + self.constants.push(value); + self.constants.len() - 1 + } + + pub fn disassemble(&self, name: &str) { + // only disassemble in debug mode + if !cfg!(debug_assertions) { + return; + } + + println!("== {} ==", name); + + let mut offset = 0; + while offset < self.code.len() { + offset = self.disassemble_instruction(offset); + } + } + + fn disassemble_instruction(&self, offset: usize) -> usize { + print!("{:04} ", offset); + if offset > 0 && self.lines[offset] == self.lines[offset - 1] { + print!(" | "); + } else { + print!("{:4} ", self.lines[offset]); + } + + let instruction = self.code[offset]; + match Opcode::from(instruction) { + Opcode::Return => self.simple_instruction("Return", offset), + Opcode::Constant => self.constant_instruction("Constant", offset), + Opcode::Negate => self.simple_instruction("Negate", offset), + Opcode::Add => self.simple_instruction("Add", offset), + Opcode::Subtract => self.simple_instruction("Subtract", offset), + Opcode::Multiply => self.simple_instruction("Multiply", offset), + Opcode::Divide => self.simple_instruction("Divide", offset), + Opcode::Mod => self.simple_instruction("Mod", offset), + Opcode::Nil => self.simple_instruction("Nil", offset), + Opcode::True => self.simple_instruction("True", offset), + Opcode::False => self.simple_instruction("False", offset), + Opcode::Not => self.simple_instruction("Not", offset), + Opcode::Equal => self.simple_instruction("Equal", offset), + Opcode::Greater => self.simple_instruction("Greater", offset), + Opcode::Less => self.simple_instruction("Less", offset), + Opcode::LogicalAnd => self.simple_instruction("LogicalAnd", offset), + Opcode::LogicalOr => self.simple_instruction("LogicalOr", offset), + Opcode::BitwiseAnd => self.simple_instruction("BitwiseAnd", offset), + Opcode::BitwiseOr => self.simple_instruction("BitwiseOr", offset), + Opcode::Print => self.simple_instruction("Print", offset), + Opcode::Pop => self.simple_instruction("Pop", offset), + Opcode::GetGlobal => self.constant_instruction("GetGlobal", offset), + Opcode::SetGlobal => self.constant_instruction("SetGlobal", offset), + Opcode::GetLocal => self.byte_instruction("GetLocal", offset), + Opcode::SetLocal => self.byte_instruction("SetLocal", offset), + Opcode::JumpIfFalse => self.jump_instruction("JumpIfFalse", 1, offset), + Opcode::Jump => self.jump_instruction("Jump", 1, offset), + Opcode::Loop => self.jump_instruction("Loop", -1, offset), + Opcode::Call => self.byte_instruction("Call", offset), + _ => { + println!("Unknown opcode: {}", instruction); + offset + 1 + } + } + } + + fn simple_instruction(&self, name: &str, offset: usize) -> usize { + println!("{}", name); + offset + 1 + } + + fn constant_instruction(&self, name: &str, offset: usize) -> usize { + let constant = self.code[offset + 1] as usize; + print!("{} {} ", name, constant); + println!("{}", self.constants[constant]); + offset + 2 + } + + fn byte_instruction(&self, name: &str, offset: usize) -> usize { + let slot = self.code[offset + 1]; + println!("{} {}", name, slot); + offset + 2 + } + + fn jump_instruction(&self, name: &str, sign: i32, offset: usize) -> usize { + let mut jump = (self.code[offset + 1] as u16) << 8; + jump |= self.code[offset + 2] as u16; + let to = sign * (jump as i32); + println!( + "{} {} -> {}", + name, + offset, + ((offset as i64) + 3 + to as i64) as i64 + ); + offset + 3 + } +} diff --git a/engine/src/lang/function.rs b/engine/src/lang/function.rs new file mode 100644 index 0000000..fb5b2d5 --- /dev/null +++ b/engine/src/lang/function.rs @@ -0,0 +1,28 @@ +use crate::lang::chunk::Chunk; + +#[derive(Debug, Clone)] +pub enum FunctionType { + Fn, + Script, +} + +#[derive(Debug, Clone)] +pub struct Function { + pub num_params: usize, + pub chunk: Chunk, + pub name: String, + pub native: bool, + pub function_type: FunctionType, +} + +impl Function { + pub fn new(name: String, function_type: FunctionType) -> Function { + Function { + num_params: 0, + chunk: Chunk::new(), + name, + native: false, + function_type, + } + } +} diff --git a/engine/src/lang/mod.rs b/engine/src/lang/mod.rs new file mode 100644 index 0000000..d29cf57 --- /dev/null +++ b/engine/src/lang/mod.rs @@ -0,0 +1,11 @@ +pub mod chunk; +pub mod function; +pub mod opcode; +pub mod operator; +pub mod parse_rule; +pub mod parser; +pub mod precedence; +pub mod scanner; +pub mod token; +pub mod value; +pub mod vm; diff --git a/engine/src/lang/opcode.rs b/engine/src/lang/opcode.rs new file mode 100644 index 0000000..69546f0 --- /dev/null +++ b/engine/src/lang/opcode.rs @@ -0,0 +1,70 @@ +pub enum Opcode { + Return = 0, + Constant, + Negate, + Add, + Subtract, + Multiply, + Divide, + Mod, + Nil, + True, + False, + Not, + Equal, + Greater, + Less, + LogicalAnd, + LogicalOr, + BitwiseAnd, + BitwiseOr, + Print, + Pop, + GetGlobal, + SetGlobal, + GetLocal, + SetLocal, + JumpIfFalse, + Jump, + Loop, + Call, + + Unknown, +} + +impl Opcode { + pub fn from(x: u8) -> Opcode { + match x { + 0 => Opcode::Return, + 1 => Opcode::Constant, + 2 => Opcode::Negate, + 3 => Opcode::Add, + 4 => Opcode::Subtract, + 5 => Opcode::Multiply, + 6 => Opcode::Divide, + 7 => Opcode::Mod, + 8 => Opcode::Nil, + 9 => Opcode::True, + 10 => Opcode::False, + 11 => Opcode::Not, + 12 => Opcode::Equal, + 13 => Opcode::Greater, + 14 => Opcode::Less, + 15 => Opcode::LogicalAnd, + 16 => Opcode::LogicalOr, + 17 => Opcode::BitwiseAnd, + 18 => Opcode::BitwiseOr, + 19 => Opcode::Print, + 20 => Opcode::Pop, + 21 => Opcode::GetGlobal, + 22 => Opcode::SetGlobal, + 23 => Opcode::GetLocal, + 24 => Opcode::SetLocal, + 25 => Opcode::JumpIfFalse, + 26 => Opcode::Jump, + 27 => Opcode::Loop, + 28 => Opcode::Call, + _ => Opcode::Unknown, + } + } +} diff --git a/engine/src/lang/operator.rs b/engine/src/lang/operator.rs new file mode 100644 index 0000000..d0ba9aa --- /dev/null +++ b/engine/src/lang/operator.rs @@ -0,0 +1,34 @@ +use std::fmt::{self, Display, Formatter}; + +#[derive(Debug)] +pub enum Operator { + Plus, + Minus, + Star, + Slash, + Mod, + LessThan, + GreaterThan, + Amp, + AmpAmp, + Pipe, + PipePipe, +} + +impl Display for Operator { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Operator::Plus => write!(f, "+"), + Operator::Minus => write!(f, "-"), + Operator::Star => write!(f, "*"), + Operator::Slash => write!(f, "/"), + Operator::Mod => write!(f, "%"), + Operator::LessThan => write!(f, "<"), + Operator::GreaterThan => write!(f, ">"), + Operator::Amp => write!(f, "&"), + Operator::AmpAmp => write!(f, "&&"), + Operator::Pipe => write!(f, "|"), + Operator::PipePipe => write!(f, "||"), + } + } +} diff --git a/engine/src/lang/parse_rule.rs b/engine/src/lang/parse_rule.rs new file mode 100644 index 0000000..258010d --- /dev/null +++ b/engine/src/lang/parse_rule.rs @@ -0,0 +1,8 @@ +use crate::lang::precedence::Precedence; +use crate::lang::parser::Parser; + +pub struct ParseRule { + pub prefix: Option, + pub infix: Option, + pub precedence: Precedence, +} diff --git a/engine/src/lang/parser.rs b/engine/src/lang/parser.rs new file mode 100644 index 0000000..96d5a43 --- /dev/null +++ b/engine/src/lang/parser.rs @@ -0,0 +1,491 @@ +use crate::lang::function::{Function, FunctionType}; +use crate::lang::opcode::Opcode; +use crate::lang::precedence::Precedence; +use crate::lang::token::{Token, TokenType}; +use crate::lang::value::Value; +use crate::lang::vm::InterpretError; +use crate::lang::scanner::Scanner; + +pub struct Parser { + current: Token, + previous: Token, + scanner: Scanner, + functions: Vec, + locals: Vec, + had_error: bool, + end_flag: bool, + local_count: usize, + scope_depth: usize, +} + +/// represents a local variable +struct Local { + name: String, + depth: usize, +} + +impl Parser { + pub fn new(source: String) -> Parser { + Parser { + current: Token::new(TokenType::Error(String::from("current token")), 0, 0, 0), + previous: Token::new(TokenType::Error(String::from("current token")), 0, 0, 0), + scanner: Scanner::new(source), + functions: Vec::new(), + locals: Vec::new(), + had_error: false, + end_flag: false, + local_count: 0, + scope_depth: 0, + } + } + + pub fn compile(mut self) -> Result { + // add top level functions to function stack + self.functions + .push(Function::new(String::new(), FunctionType::Script)); + + self.advance(); + + while !self.end_flag { + self.declaration(); + } + + self.emit_op(Opcode::Return); + Ok(self.functions[0].clone()) + } + + fn expression(&mut self) { + self.parse_precedence(Precedence::Assignment); + } + + fn declaration(&mut self) { + self.statement(); + } + + fn statement(&mut self) { + match self.current.token_type.clone() { + TokenType::Print => { + self.advance(); + self.print_statement(); + } + TokenType::LeftBrace => { + self.advance(); + self.begin_scope(); + self.block(); + self.end_scope(); + } + TokenType::If => { + self.advance(); + self.if_statement(); + } + TokenType::While => { + self.advance(); + self.while_statement(); + } + TokenType::Fn => { + self.advance(); + self.function_definition(); + } + TokenType::Return => { + self.advance(); + self.return_statement(); + } + _ => self.expression_statement(), + } + } + + fn function_definition(&mut self) { + let function_name = match self.current.token_type.clone() { + TokenType::Identifier(name) => name, + _ => unreachable!("Not given an identifier in function_definition"), + }; + + let f = Function::new(function_name, FunctionType::Fn); + self.functions.push(f); + + self.advance(); + + self.begin_scope(); + self.consume(TokenType::LeftParen, "Expect '(' after function name"); + + let mut num_params = 0; + while !self.matches(TokenType::RightParen) { + num_params += 1; + match self.current.token_type.clone() { + TokenType::Identifier(name) => self.add_local(name), + _ => panic!("Expect identifier"), + } + self.advance(); + } + + let mut f = self.functions.pop().unwrap(); + f.num_params = num_params; + self.functions.push(f); + + self.consume(TokenType::LeftBrace, "Expect '{' before function body"); + + self.block(); + + // add implicit nil for empty functions + if self.functions.last_mut().unwrap().chunk.code.is_empty() { + self.emit_constant(Value::Nil); + } + + self.emit_op(Opcode::Return); + + let f = self.functions.pop().unwrap(); + f.chunk.disassemble(&f.name); + let global = self.make_constant(Value::Function(f)); + self.emit_op(Opcode::SetGlobal); + self.emit_byte(global as u8); + } + + fn expression_statement(&mut self) { + self.expression(); + } + + fn block(&mut self) { + while self.current.token_type != TokenType::RightBrace { + self.declaration() + } + + self.consume(TokenType::RightBrace, "Expect '}' after block"); + } + + fn if_statement(&mut self) { + self.expression(); + let if_offset = self.emit_jump(Opcode::JumpIfFalse); + self.emit_op(Opcode::Pop); + self.statement(); + + let else_offset = self.emit_jump(Opcode::Jump); + self.emit_op(Opcode::Pop); + + self.patch_jump(if_offset); + + // compile optional else clause + if self.matches(TokenType::Else) { + self.statement(); + } + + self.patch_jump(else_offset); + } + + fn while_statement(&mut self) { + let loop_start = self.functions.last_mut().unwrap().chunk.code.len(); + self.expression(); + let exit_offset = self.emit_jump(Opcode::JumpIfFalse); + self.emit_op(Opcode::Pop); + self.statement(); + self.emit_loop(loop_start); + + self.patch_jump(exit_offset); + self.emit_op(Opcode::Pop); + } + + fn return_statement(&mut self) { + // TODO: don't parse expression if return is followed immediately by \n + self.expression(); + self.emit_op(Opcode::Return); + } + + pub fn advance(&mut self) { + self.previous = self.current.clone(); + + if let Some(tok) = self.scanner.next() { + self.current = tok; + } else { + self.end_flag = true; + } + } + + fn consume(&mut self, token_type: TokenType, msg: &str) { + if !(self.current.token_type == token_type) { + self.error_at_current(msg); + } + + self.advance(); + } + + pub fn string(&mut self, _can_assign: bool) { + match &self.previous.token_type { + TokenType::String(s) => self.emit_constant(Value::String(s.to_string())), + _ => unreachable!("No string"), + } + } + + fn error(&mut self, msg: &str) { + self.error_at(self.previous.clone(), msg); + } + + fn error_at_current(&mut self, msg: &str) { + self.error_at(self.current.clone(), msg); + } + + fn error_at(&mut self, tok: Token, msg: &str) { + println!("[line {}] Error: {}", tok.line, msg); + self.had_error = true; + } + + pub fn number(&mut self, _can_assign: bool) { + if let TokenType::Number(num) = self.previous.token_type { + self.emit_constant(Value::Number(num)); + } + } + + fn emit_byte(&mut self, byte: u8) { + self.functions + .last_mut() + .unwrap() + .chunk + .write(byte, self.previous.line); + } + + fn emit_bytes(&mut self, a: u8, b: u8) { + self.emit_byte(a); + self.emit_byte(b); + } + + fn emit_op(&mut self, op: Opcode) { + self.emit_byte(op as u8); + } + + fn emit_ops(&mut self, op1: Opcode, op2: Opcode) { + self.emit_byte(op1 as u8); + self.emit_byte(op2 as u8); + } + + fn emit_constant(&mut self, value: Value) { + let constant = self.make_constant(value) as u8; + self.emit_bytes(Opcode::Constant as u8, constant); + } + + fn emit_jump(&mut self, op: Opcode) -> usize { + self.emit_byte(op as u8); + self.emit_bytes(0xff, 0xff); + self.functions.last_mut().unwrap().chunk.code.len() - 2 + } + + fn patch_jump(&mut self, offset: usize) { + let jump = self.functions.last_mut().unwrap().chunk.code.len() - offset - 2; + + if jump > std::i16::MAX as usize { + self.error("Jump is out of bounds"); + } + + self.functions.last_mut().unwrap().chunk.code[offset] = ((jump >> 8) & 0xff) as u8; + self.functions.last_mut().unwrap().chunk.code[offset + 1] = (jump & 0xff) as u8; + } + + fn emit_loop(&mut self, loop_start: usize) { + self.emit_op(Opcode::Loop); + + let offset = self.functions.last_mut().unwrap().chunk.code.len() - loop_start + 2; + if offset as u16 > std::u16::MAX { + self.error("Loop offset is out of bounds"); + } + + self.emit_byte(((offset >> 8) & 0xff) as u8); + self.emit_byte((offset & 0xff) as u8); + } + + fn make_constant(&mut self, value: Value) -> usize { + let constant = self.functions.last_mut().unwrap().chunk.add_constant(value); + if constant > std::u8::MAX as usize { + self.error("Too many constants in this chunk"); + 0 + } else { + constant + } + } + + pub fn grouping(&mut self, _can_assign: bool) { + self.expression(); + self.consume(TokenType::RightParen, "Expect ')' after expression"); + } + + pub fn unary(&mut self, _can_assign: bool) { + let operator = self.previous.token_type.clone(); + self.parse_precedence(Precedence::Unary); + + match operator { + TokenType::Minus => self.emit_op(Opcode::Negate), + TokenType::Bang => self.emit_op(Opcode::Not), + _ => unreachable!("Impossible unary operator"), + } + } + + pub fn call(&mut self, _can_assign: bool) { + let mut num_params = 0; + + if self.current.token_type.clone() != TokenType::RightParen { + while { + num_params += 1; + self.expression(); + + self.matches(TokenType::Comma) + } {} + } + + self.consume(TokenType::RightParen, "Expected ) after arguments"); + + self.emit_op(Opcode::Call); + self.emit_byte(num_params); + } + + pub fn binary(&mut self, _can_assign: bool) { + let operator = self.previous.token_type.clone(); + let rule = operator.rule(); + let precedence = Precedence::from(rule.precedence as usize + 1); + self.parse_precedence(precedence); + + match operator { + TokenType::Plus => self.emit_op(Opcode::Add), + TokenType::Minus => self.emit_op(Opcode::Subtract), + TokenType::Star => self.emit_op(Opcode::Multiply), + TokenType::Slash => self.emit_op(Opcode::Divide), + TokenType::Mod => self.emit_op(Opcode::Mod), + TokenType::BangEqual => self.emit_ops(Opcode::Equal, Opcode::Not), + TokenType::EqualEqual => self.emit_op(Opcode::Equal), + TokenType::Greater => self.emit_op(Opcode::Greater), + TokenType::GreaterEqual => self.emit_ops(Opcode::Less, Opcode::Not), + TokenType::Less => self.emit_op(Opcode::Less), + TokenType::LessEqual => self.emit_ops(Opcode::Greater, Opcode::Not), + TokenType::BitwiseAnd => self.emit_op(Opcode::BitwiseAnd), + TokenType::BitwiseOr => self.emit_op(Opcode::BitwiseOr), + TokenType::LogicalAnd => self.emit_op(Opcode::LogicalAnd), + TokenType::LogicalOr => self.emit_op(Opcode::LogicalOr), + TokenType::And => self.emit_op(Opcode::LogicalAnd), + _ => (), + } + } + + fn parse_precedence(&mut self, precedence: Precedence) { + self.advance(); + let rule = self.previous.token_type.rule(); + + if let Some(prefix_rule) = rule.prefix { + let can_assign = precedence as usize <= Precedence::Assignment as usize; + prefix_rule(self, can_assign); + + let prec_u8 = precedence as u8; + while prec_u8 <= self.current.token_type.rule().precedence as u8 { + self.advance(); + if let Some(infix_rule) = self.previous.token_type.rule().infix { + infix_rule(self, can_assign); + } + } + + return; + } + + self.error("Expected expression"); + } + + pub fn literal(&mut self, _can_assign: bool) { + let token_type = self.previous.token_type.clone(); + match token_type { + TokenType::False => self.emit_op(Opcode::False), + TokenType::Nil => self.emit_op(Opcode::Nil), + TokenType::True => self.emit_op(Opcode::True), + _ => unreachable!("Impossible TokenType in literal"), + } + } + + fn print_statement(&mut self) { + self.expression(); + self.emit_op(Opcode::Print); + } + + fn matches(&mut self, token_type: TokenType) -> bool { + if self.current.token_type == token_type { + self.advance(); + true + } else { + false + } + } + + pub fn variable(&mut self, can_assign: bool) { + let identifier = self.previous.clone(); + let name = match identifier.token_type.clone() { + TokenType::Identifier(name) => name, + _ => unreachable!("In variable() without name"), + }; + + let (get_op, set_op, constant) = match self.resolve_local(&identifier) { + Ok(id) => (Opcode::GetLocal, Opcode::SetLocal, id), + Err(_) => ( + Opcode::GetGlobal, + Opcode::SetGlobal, + self.make_constant(Value::String(name)), + ), + }; + + if can_assign && self.matches(TokenType::Equal) { + self.expression(); + self.emit_op(set_op); + self.emit_byte(constant as u8); + } else { + self.emit_op(get_op); + self.emit_byte(constant as u8); + } + } + + fn add_local(&mut self, name: String) { + // no more than 255 local variables + if self.local_count == u8::MAX as usize { + self.error("Too many local variables"); + return; + } + + let local = Local { + name, + depth: self.scope_depth, + }; + + self.local_count += 1; + self.locals.push(local); + } + + fn begin_scope(&mut self) { + self.scope_depth += 1; + } + + fn end_scope(&mut self) { + self.scope_depth -= 1; + + // pop local variables introduced in this scope off the stack + while self.local_count > 0 && self.locals[self.local_count - 1].depth > self.scope_depth { + self.emit_op(Opcode::Pop); + self.local_count -= 1; + } + } + + fn resolve_local(&self, name: &Token) -> Result { + if self.locals.is_empty() { + return Err(()); + } + + let mut local_count = self.local_count - 1; + let identifier = match &name.token_type { + TokenType::Identifier(id) => id, + _ => unreachable!("Was not given an identifier to resolve_local"), + }; + + loop { + let local = &self.locals[local_count]; + if local.name == *identifier { + return Ok(local_count); + } + + if local_count == 0 { + break; + } + + local_count -= 1; + } + + Err(()) + } +} diff --git a/engine/src/lang/precedence.rs b/engine/src/lang/precedence.rs new file mode 100644 index 0000000..f1398d2 --- /dev/null +++ b/engine/src/lang/precedence.rs @@ -0,0 +1,33 @@ +#[derive(Clone, Copy)] +pub enum Precedence { + None, + Assignment, // = + Or, // || + And, // && + Equality, // == != + Comparison, // < > <= >= + Term, // + - + Factor, // * / + Unary, // ! - + Call, // . () + Primary, +} + +impl Precedence { + pub fn from(x: usize) -> Precedence { + match x { + 0 => Precedence::None, + 1 => Precedence::Assignment, + 2 => Precedence::Or, + 3 => Precedence::And, + 4 => Precedence::Equality, + 5 => Precedence::Comparison, + 6 => Precedence::Term, + 7 => Precedence::Factor, + 8 => Precedence::Unary, + 9 => Precedence::Call, + 10 => Precedence::Primary, + _ => Precedence::None, + } + } +} diff --git a/engine/src/lang/scanner.rs b/engine/src/lang/scanner.rs new file mode 100644 index 0000000..66c5262 --- /dev/null +++ b/engine/src/lang/scanner.rs @@ -0,0 +1,386 @@ +use crate::lang::token::{Token, TokenType}; + +fn is_digit(c: char) -> bool { + matches!(c, '0'..='9') +} + +fn is_alpha(c: char) -> bool { + matches!(c, 'a'..='z' | 'A'..='Z' | '_') +} + +pub struct Scanner { + source: String, + start: usize, // index of beginning of lexeme being scanned + pos: usize, // current character being looked at + line: usize, +} + +impl Scanner { + pub fn new(source: String) -> Scanner { + Scanner { + source, + start: 0, + pos: 0, + line: 1, + } + } + + // used only in tests + #[cfg(test)] + fn scan_all(&mut self) -> Vec { + let mut tokens = Vec::new(); + while let Some(tok) = self.next() { + tokens.push(tok); + } + tokens + } + + pub fn next(&mut self) -> Option { + // ignores whitespace between tokens + self.skip_whitespace(); + + self.start = self.pos; + + if self.eof() { + return None; + } + + let c = self.advance(); + match c { + '(' => Some(self.make_token(TokenType::LeftParen)), + ')' => Some(self.make_token(TokenType::RightParen)), + '{' => Some(self.make_token(TokenType::LeftBrace)), + '}' => Some(self.make_token(TokenType::RightBrace)), + ';' => Some(self.make_token(TokenType::Semicolon)), + ',' => Some(self.make_token(TokenType::Comma)), + '.' => Some(self.make_token(TokenType::Dot)), + '-' => Some(self.make_token(TokenType::Minus)), + '+' => Some(self.make_token(TokenType::Plus)), + '/' => Some(self.make_token(TokenType::Slash)), + '*' => Some(self.make_token(TokenType::Star)), + '%' => Some(self.make_token(TokenType::Mod)), + '&' => { + let token_type = if self.matches('&') { + TokenType::LogicalAnd + } else { + TokenType::BitwiseAnd + }; + Some(self.make_token(token_type)) + } + '|' => { + let token_type = if self.matches('|') { + TokenType::LogicalOr + } else { + TokenType::BitwiseOr + }; + Some(self.make_token(token_type)) + } + '!' => { + let token_type = if self.matches('=') { + TokenType::BangEqual + } else { + TokenType::Bang + }; + Some(self.make_token(token_type)) + } + '=' => { + let token_type = if self.matches('=') { + TokenType::EqualEqual + } else { + TokenType::Equal + }; + Some(self.make_token(token_type)) + } + '<' => { + let token_type = if self.matches('=') { + TokenType::LessEqual + } else { + TokenType::Less + }; + Some(self.make_token(token_type)) + } + '>' => { + let token_type = if self.matches('=') { + TokenType::GreaterEqual + } else { + TokenType::Greater + }; + Some(self.make_token(token_type)) + } + '\'' | '\"' => Some(self.string(c)), + '0'..='9' => Some(self.number()), + 'a'..='z' | 'A'..='Z' | '_' => Some(self.identifier()), + _ => None, + } + } + + fn make_token(&self, token_type: TokenType) -> Token { + let len = self.pos - self.start; + Token::new(token_type, self.line, self.start, len) + } + + /// emits a syntax error token + fn error(&self, msg: &str) -> Token { + Token::new( + TokenType::Error(String::from(msg)), + self.line, + self.start, + msg.len(), + ) + } + + fn peek(&self) -> char { + if self.eof() { + '\0' + } else { + self.source[self.pos..].chars().next().unwrap() + } + } + + /// advances the scanner's position and returns the consumed character + fn advance(&mut self) -> char { + let c = self.peek(); + self.pos += 1; + c + } + + /// returns true if next character matches the expected character, consuming it + fn matches(&mut self, expected: char) -> bool { + if self.eof() || self.peek() != expected { + false + } else { + self.pos += 1; + true + } + } + + fn string(&mut self, delimiter: char) -> Token { + assert!(delimiter == '\'' || delimiter == '\"'); + self.advance(); + while self.peek() != delimiter && !self.eof() { + if self.peek() == '\n' { + self.line += 1; + } + + self.advance(); + } + + if self.eof() { + self.error("Unterminated string") + } else { + // consume closing ' + self.advance(); + + let string = &self.source[self.start + 1..self.pos - 1]; + self.make_token(TokenType::String(String::from(string))) + } + } + + fn number(&mut self) -> Token { + while is_digit(self.peek()) { + self.advance(); + } + + if self.peek() == '.' && is_digit(self.peek_next()) { + // consume the '.' + self.advance(); + + while is_digit(self.peek()) { + self.advance(); + } + } + + let string = &self.source[self.start..self.pos]; + let num: f64 = string.parse().unwrap(); + self.make_token(TokenType::Number(num)) + } + + fn identifier(&mut self) -> Token { + while is_alpha(self.peek()) || is_digit(self.peek()) { + self.advance(); + } + + let tok = self.identifier_type(); + self.make_token(tok) + } + + fn skip_whitespace(&mut self) { + loop { + match self.peek() { + ' ' | '\r' | '\t' => { + self.advance(); + } + '\n' => { + self.line += 1; + self.advance(); + } + '#' => { + while self.peek() != '\n' && !self.eof() { + self.advance(); + } + } + _ => break, + } + } + } + + fn peek_next(&self) -> char { + let next = self.pos + 1; + if self.eof() { + '\0' + } else { + self.source[next..].chars().next().unwrap() + } + } + + fn identifier_type(&mut self) -> TokenType { + let c = self.source[self.start..].chars().next().unwrap(); + let t = match c { + 'a' => self.check_keyword(1, 2, "nd", TokenType::And), + 'c' => self.check_keyword(1, 4, "lass", TokenType::Class), + 'e' => self.check_keyword(1, 3, "lse", TokenType::Else), + 'i' => self.check_keyword(1, 1, "f", TokenType::If), + 'f' => { + if self.pos - self.start > 1 { + match self.source.chars().nth(self.start + 1).unwrap() { + 'a' => self.check_keyword(2, 3, "lse", TokenType::False), + 'o' => self.check_keyword(2, 1, "r", TokenType::For), + _ => None, + } + } else { + None + } + } + 'n' => self.check_keyword(1, 2, "il", TokenType::Nil), + 'o' => self.check_keyword(1, 1, "r", TokenType::Or), + 'p' => self.check_keyword(1, 4, "rint", TokenType::Print), + 'r' => self.check_keyword(1, 5, "eturn", TokenType::Return), + 's' => self.check_keyword(1, 4, "uper", TokenType::Super), + 't' => { + if self.pos - self.start > 1 { + match self.source.chars().nth(self.start + 1).unwrap() { + 'h' => self.check_keyword(2, 2, "is", TokenType::This), + 'r' => self.check_keyword(2, 2, "ue", TokenType::True), + _ => None, + } + } else { + None + } + } + 'v' => self.check_keyword(1, 2, "ar", TokenType::Var), + 'w' => self.check_keyword(1, 4, "hile", TokenType::While), + _ => None, + }; + + if let Some(token_type) = t { + token_type + } else { + let ident = &self.source[self.start..self.pos]; + if ident == "fn" { + TokenType::Fn + } else { + TokenType::Identifier(String::from(ident)) + } + } + } + + fn check_keyword( + &self, + start: usize, + len: usize, + rest: &str, + token_type: TokenType, + ) -> Option { + if (self.pos - self.start == start + len) + && &self.source[self.start + start..self.start + start + len] == rest + { + Some(token_type) + } else { + None + } + } + + fn eof(&self) -> bool { + self.pos >= self.source.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_source_returns_empty_vector_of_tokens() { + let mut scanner = Scanner::new(String::from("")); + assert_eq!(scanner.scan_all().len(), 0); + } + + #[test] + fn scans_keyword_return() { + let mut scanner = Scanner::new(String::from("return")); + let tokens = scanner.scan_all(); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0], Token::new(TokenType::Return, 1, 0, 6)); + } + + #[test] + fn scans_keyword_false() { + let mut scanner = Scanner::new(String::from("false")); + let tokens = scanner.scan_all(); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0], Token::new(TokenType::False, 1, 0, 5)); + } + + #[test] + fn scans_keyword_fn() { + let mut scanner = Scanner::new(String::from("fn f fna")); + let tokens = scanner.scan_all(); + assert_eq!(tokens.len(), 3); + assert_eq!(tokens[0], Token::new(TokenType::Fn, 1, 0, 2)); + assert_eq!( + tokens[1], + Token::new(TokenType::Identifier(String::from("f")), 1, 3, 1) + ); + assert_eq!( + tokens[2], + Token::new(TokenType::Identifier(String::from("fna")), 1, 5, 3) + ); + } + + #[test] + fn scans_list_of_keywords() { + let mut scanner = Scanner::new(String::from("nil if fn")); + let tokens = scanner.scan_all(); + assert_eq!(tokens.len(), 3); + assert_eq!(tokens[0], Token::new(TokenType::Nil, 1, 0, 3)); + assert_eq!(tokens[1], Token::new(TokenType::If, 1, 4, 2)); + assert_eq!(tokens[2], Token::new(TokenType::Fn, 1, 7, 2)); + } + + #[test] + fn ignores_whitespace() { + let mut scanner = Scanner::new(String::from(" \tnil")); + let tokens = scanner.scan_all(); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0], Token::new(TokenType::Nil, 1, 3, 3)); + } + + #[test] + fn finds_string() { + let mut scanner = Scanner::new(String::from("'hello world'")); + let tokens = scanner.scan_all(); + assert_eq!(tokens.len(), 1); + assert_eq!( + tokens[0], + Token::new(TokenType::String(String::from("hello world")), 1, 0, 13) + ); + } + + #[test] + fn finds_number() { + let mut scanner = Scanner::new(String::from("12.34")); + let tokens = scanner.scan_all(); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0], Token::new(TokenType::Number(12.34), 1, 0, 5)); + } +} diff --git a/engine/src/lang/token.rs b/engine/src/lang/token.rs new file mode 100644 index 0000000..2ecbd94 --- /dev/null +++ b/engine/src/lang/token.rs @@ -0,0 +1,359 @@ +use crate::lang::parse_rule::ParseRule; +use crate::lang::parser::Parser; +use crate::lang::precedence::Precedence; + +use std::fmt::{self, Display, Formatter}; + +#[derive(Debug, PartialEq, Clone)] +pub enum TokenType { + // 1 character tokens + LeftParen, + RightParen, + LeftBrace, + RightBrace, + Comma, + Dot, + Minus, + Plus, + Semicolon, + Slash, + Star, + Mod, + BitwiseAnd, + BitwiseOr, + + // 1 or 2 character tokens + Bang, + BangEqual, + Equal, + EqualEqual, + Greater, + GreaterEqual, + Less, + LessEqual, + LogicalAnd, + LogicalOr, + + // Literals + Identifier(String), + String(String), + Number(f64), + + // Keywords + And, + Class, + Else, + False, + For, + Fn, + If, + Nil, + Or, + Print, + Return, + Super, + This, + True, + Var, + While, + + // Misc tokens + Error(String), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Token { + pub token_type: TokenType, + pub line: usize, + pub col: usize, + pub len: usize, +} + +impl Token { + pub fn new(token_type: TokenType, line: usize, col: usize, len: usize) -> Token { + Token { + token_type, + line, + col, + len, + } + } +} + +impl TokenType { + pub fn rule(&self) -> &'static ParseRule { + match self { + TokenType::LeftParen => &ParseRule { + prefix: Some(Parser::grouping), + infix: Some(Parser::call), + precedence: Precedence::Call, + }, + TokenType::RightParen => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::LeftBrace => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::RightBrace => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Comma => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Dot => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Minus => &ParseRule { + prefix: Some(Parser::unary), + infix: Some(Parser::binary), + precedence: Precedence::Term, + }, + TokenType::Plus => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Term, + }, + TokenType::Semicolon => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Slash => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Factor, + }, + TokenType::Star => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Factor, + }, + TokenType::Mod => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Factor, + }, + TokenType::BitwiseAnd => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Term, + }, + TokenType::BitwiseOr => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Term, + }, + TokenType::LogicalAnd => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::And, + }, + TokenType::LogicalOr => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Or, + }, + TokenType::Bang => &ParseRule { + prefix: Some(Parser::unary), + infix: None, + precedence: Precedence::None, + }, + TokenType::BangEqual => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Equality, + }, + TokenType::Equal => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::EqualEqual => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Comparison, + }, + TokenType::Greater => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Comparison, + }, + TokenType::GreaterEqual => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Comparison, + }, + TokenType::Less => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Comparison, + }, + TokenType::LessEqual => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::Comparison, + }, + TokenType::Identifier(_) => &ParseRule { + prefix: Some(Parser::variable), + infix: None, + precedence: Precedence::None, + }, + TokenType::String(_) => &ParseRule { + prefix: Some(Parser::string), + infix: None, + precedence: Precedence::None, + }, + TokenType::Number(_) => &ParseRule { + prefix: Some(Parser::number), + infix: None, + precedence: Precedence::None, + }, + TokenType::And => &ParseRule { + prefix: None, + infix: Some(Parser::binary), + precedence: Precedence::And, + }, + TokenType::Class => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Else => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::False => &ParseRule { + prefix: Some(Parser::literal), + infix: None, + precedence: Precedence::None, + }, + TokenType::For => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Fn => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::If => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Nil => &ParseRule { + prefix: Some(Parser::literal), + infix: None, + precedence: Precedence::None, + }, + TokenType::Or => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Print => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Return => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Super => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::This => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::True => &ParseRule { + prefix: Some(Parser::literal), + infix: None, + precedence: Precedence::None, + }, + TokenType::Var => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::While => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + TokenType::Error(_) => &ParseRule { + prefix: None, + infix: None, + precedence: Precedence::None, + }, + } + } +} + +impl Display for TokenType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + TokenType::LeftParen => write!(f, "LeftParen"), + TokenType::RightParen => write!(f, "RightParen"), + TokenType::LeftBrace => write!(f, "LeftBrace"), + TokenType::RightBrace => write!(f, "RightBrace"), + TokenType::Comma => write!(f, "Comma"), + TokenType::Dot => write!(f, "Dot"), + TokenType::Minus => write!(f, "Minus"), + TokenType::Plus => write!(f, "Plus"), + TokenType::Semicolon => write!(f, "Semicolon"), + TokenType::Slash => write!(f, "Slash"), + TokenType::Star => write!(f, "Star"), + TokenType::Mod => write!(f, "Mod"), + TokenType::BitwiseAnd => write!(f, "BitwiseAnd"), + TokenType::BitwiseOr => write!(f, "BitwiseOr"), + TokenType::LogicalAnd => write!(f, "LogicalAnd"), + TokenType::LogicalOr => write!(f, "LogicalOr"), + TokenType::Bang => write!(f, "Bang"), + TokenType::BangEqual => write!(f, "BangEqual"), + TokenType::Equal => write!(f, "Equal"), + TokenType::EqualEqual => write!(f, "EqualEqual"), + TokenType::Greater => write!(f, "Greater"), + TokenType::GreaterEqual => write!(f, "GreaterEqual"), + TokenType::Less => write!(f, "Less"), + TokenType::LessEqual => write!(f, "LessEqual"), + TokenType::Identifier(_) => write!(f, "Identifier"), + TokenType::String(_) => write!(f, "String"), + TokenType::Number(_) => write!(f, "Number"), + TokenType::And => write!(f, "And"), + TokenType::Class => write!(f, "Class"), + TokenType::Else => write!(f, "Else"), + TokenType::False => write!(f, "False"), + TokenType::For => write!(f, "For"), + TokenType::Fn => write!(f, "Fn"), + TokenType::If => write!(f, "If"), + TokenType::Nil => write!(f, "Nil"), + TokenType::Or => write!(f, "Or"), + TokenType::Print => write!(f, "Print"), + TokenType::Return => write!(f, "Return"), + TokenType::Super => write!(f, "Super"), + TokenType::This => write!(f, "This"), + TokenType::True => write!(f, "True"), + TokenType::Var => write!(f, "Var"), + TokenType::While => write!(f, "While"), + TokenType::Error(_) => write!(f, "Error"), + } + } +} diff --git a/engine/src/lang/value.rs b/engine/src/lang/value.rs new file mode 100644 index 0000000..7edbb2f --- /dev/null +++ b/engine/src/lang/value.rs @@ -0,0 +1,126 @@ +use crate::lang::function::Function; +use std::fmt::{self, Display, Formatter}; + +#[derive(Debug, Clone)] +pub enum Value { + Bool(bool), + Nil, + Number(f64), + String(String), + Function(Function), +} + +impl From for Value { + fn from(n: i64) -> Self { + Value::Number(n as f64) + } +} + +impl Display for Value { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Value::Bool(x) => write!(f, "{}", x), + Value::Number(x) => write!(f, "{}", x), + Value::Nil => write!(f, "nil"), + Value::String(s) => write!(f, "{}", s), + Value::Function(func) => { + if func.native { + write!(f, "") + } else { + write!(f, "", func.name) + } + } + } + } +} + +impl Value { + pub fn is_falsey(&self) -> bool { + match self { + Value::Nil => true, + Value::Bool(b) => !b, + _ => false, + } + } + + pub fn eq(&self, other: &Value) -> bool { + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return false; + } + + match (self, other) { + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Number(a), Value::Number(b)) => a == b, + (Value::String(a), Value::String(b)) => a == b, + (Value::Nil, _) => true, + _ => unreachable!("Unrecognized value equality comparison"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn true_is_not_falsey() { + assert_eq!(Value::Bool(true).is_falsey(), false); + } + + #[test] + fn false_is_falsey() { + assert_eq!(Value::Bool(false).is_falsey(), true); + } + + #[test] + fn nil_is_falsey() { + assert_eq!(Value::Nil.is_falsey(), true); + } + + #[test] + fn numbers_are_not_falsey() { + assert_eq!(Value::Number(3.14).is_falsey(), false); + } + + #[test] + fn nil_equals_nil() { + let a = Value::Nil; + let b = Value::Nil; + assert_eq!(a.eq(&b), true); + } + + #[test] + fn equal_numbers_are_equal() { + let a = Value::Number(25.9); + let b = Value::Number(25.9); + assert_eq!(a.eq(&b), true); + } + + #[test] + fn different_numbers_are_not_equal() { + let a = Value::Number(0.0); + let b = Value::Number(25.9); + assert_eq!(a.eq(&b), false); + } + + #[test] + fn different_types_are_not_equal() { + let a = Value::Number(0.0); + let b = Value::Bool(false); + assert_eq!(a.eq(&b), false); + } + + #[test] + fn different_strings_are_not_equal() { + let a = Value::String(String::from("star wars")); + let b = Value::String(String::from("star trek")); + assert_eq!(a.eq(&b), false); + } + + #[test] + fn equal_strings_are_equal() { + let a = Value::String(String::from("topaz is neat!")); + let b = Value::String(String::from("topaz is neat!")); + assert_eq!(a.eq(&b), true); + } +} diff --git a/engine/src/lang/vm.rs b/engine/src/lang/vm.rs new file mode 100644 index 0000000..7889fd7 --- /dev/null +++ b/engine/src/lang/vm.rs @@ -0,0 +1,359 @@ +use crate::lang::function::Function; +use crate::lang::opcode::Opcode; +use crate::lang::operator::Operator; +use crate::lang::value::Value; + +use std::collections::HashMap; + +pub struct Vm { + stack: Vec, + globals: HashMap, + frames: Vec, +} + +pub enum InterpretError { + CompileError, + RuntimeError, +} + +struct CallFrame { + function: Function, + ip: usize, // ip of caller to return to + base: usize, // index of base of stack +} + +impl CallFrame { + pub fn new(function: Function, base: usize) -> CallFrame { + CallFrame { + function, + ip: 0, + base, + } + } +} + +impl Vm { + pub fn new() -> Vm { + Vm { + stack: Vec::new(), + globals: HashMap::new(), + frames: Vec::new(), + } + } + + pub fn run(&mut self, function: Function) -> Result { + // push "stack frame" of top level script onto stack + let cf = CallFrame::new(function, 0); + self.frames.push(cf); + + loop { + // debug information + if cfg!(debug_assertions) { + print!("stack: "); + print!("[ "); + for value in &mut self.stack { + print!("{} ", value); + } + print!("]"); + println!(); + } + + let instruction = self.read_byte(); + match Opcode::from(instruction) { + Opcode::Return => { + let result = self.pop(); + let frame = self.frames.pop().unwrap(); + + if self.frames.is_empty() { + return Ok(result); + } + + // return caller's stack to how it was before function call + let diff = self.stack.len() - frame.base + 1; + for _ in 0..diff { + self.pop(); + } + + self.push(result); + } + Opcode::Constant => { + let constant = self.read_constant(); + self.push(constant); + } + Opcode::Negate => { + let value = self.pop(); + let negated_value = match value { + Value::Number(num) => Value::Number(-num), + _ => return Err(self.runtime_error("Operand must be a number")), + }; + self.push(negated_value) + } + Opcode::Add => self.binary_op(Operator::Plus), + Opcode::Subtract => self.binary_op(Operator::Minus), + Opcode::Multiply => self.binary_op(Operator::Star), + Opcode::Divide => self.binary_op(Operator::Slash), + Opcode::Mod => self.binary_op(Operator::Mod), + Opcode::Nil => self.push(Value::Nil), + Opcode::True => self.push(Value::Bool(true)), + Opcode::False => self.push(Value::Bool(false)), + Opcode::Not => { + let value = self.pop().is_falsey(); + self.push(Value::Bool(value)) + } + Opcode::Equal => { + let b = self.pop(); + let a = self.pop(); + self.push(Value::Bool(a.eq(&b))); + } + Opcode::Greater => self.binary_op(Operator::GreaterThan), + Opcode::Less => self.binary_op(Operator::LessThan), + Opcode::LogicalAnd => self.binary_op(Operator::AmpAmp), + Opcode::LogicalOr => self.binary_op(Operator::PipePipe), + Opcode::BitwiseAnd => self.binary_op(Operator::Amp), + Opcode::BitwiseOr => self.binary_op(Operator::Pipe), + Opcode::Print => { + println!("{}", self.peek(0)); + } + Opcode::Pop => { + self.pop(); + } + Opcode::GetGlobal => { + let constant = self.read_constant(); + if let Value::String(name) = constant { + match self.globals.get(&name) { + Some(val) => self.push(val.clone()), + None => { + self.runtime_error( + format!("Undefined variable {}", &name).as_str(), + ); + return Err(InterpretError::RuntimeError); + } + } + } else { + unreachable!("Did not receive a String in GetGlobal") + } + } + Opcode::SetGlobal => { + let constant = self.read_constant(); + let (name, value) = match constant { + Value::String(name) => (name, self.peek(0).clone()), + Value::Function(f) => (f.name.clone(), Value::Function(f)), + _ => unreachable!("Unknown value in SetGlobal"), + }; + + self.globals.insert(name, value); + } + Opcode::GetLocal => { + let base = self.frames.last_mut().unwrap().base; + let slot = self.read_byte() as usize; + self.push(self.stack[base + slot].clone()); + } + Opcode::SetLocal => { + let base = self.frames.last_mut().unwrap().base; + let slot = self.read_byte() as usize; + self.stack[base + slot] = self.peek(0).clone(); + } + Opcode::JumpIfFalse => { + let offset = self.read_short() as usize; + if self.peek(0).is_falsey() { + self.frames.last_mut().unwrap().ip += offset; + } + } + Opcode::Jump => { + let offset = self.read_short() as usize; + self.frames.last_mut().unwrap().ip += offset; + } + Opcode::Loop => { + let offset = self.read_short() as usize; + self.frames.last_mut().unwrap().ip -= offset; + } + Opcode::Call => { + let num_args = self.read_byte() as usize; + let function = self.peek(num_args); + let f = match function { + Value::Function(f) => f, + _ => { + return Err(InterpretError::RuntimeError); + } + }; + + let cf = CallFrame::new(f.clone(), self.stack.len() - num_args); + self.frames.push(cf); + } + _ => return Err(InterpretError::CompileError), + }; + } + } + + fn runtime_error(&mut self, msg: &str) -> InterpretError { + let ip = self.frames.last_mut().unwrap().ip; + let line = self.frames.last_mut().unwrap().function.chunk.lines[ip - 1]; + println!("{} [line {}]", msg, line); + InterpretError::RuntimeError + } + + fn read_byte(&mut self) -> u8 { + let ip = self.frames.last_mut().unwrap().ip; + let byte = self.frames.last_mut().unwrap().function.chunk.code[ip]; + self.frames.last_mut().unwrap().ip += 1; + byte + } + + fn read_short(&mut self) -> u16 { + let ip = self.frames.last_mut().unwrap().ip; + let rs = &self.frames.last_mut().unwrap().function.chunk.code[ip..=ip + 1]; + let short: u16 = ((rs[0] as u16) << 8) | rs[1] as u16; + self.frames.last_mut().unwrap().ip += 2; + short + } + + fn read_constant(&mut self) -> Value { + let byte = self.read_byte(); + self.frames.last_mut().unwrap().function.chunk.constants[byte as usize].clone() + } + + fn push(&mut self, value: Value) { + self.stack.push(value) + } + + fn pop(&mut self) -> Value { + self.stack.pop().unwrap() + } + + fn peek(&self, offset: usize) -> &Value { + let len = self.stack.len(); + &self.stack[len - 1 - offset] + } + + fn binary_op(&mut self, op: Operator) { + let val2 = self.pop(); + let val1 = self.pop(); + + match (val1, val2) { + (Value::Number(a), Value::Number(b)) => { + let result = match op { + Operator::Plus => Value::Number(a + b), + Operator::Minus => Value::Number(a - b), + Operator::Star => Value::Number(a * b), + Operator::Slash => Value::Number(a / b), + Operator::Mod => Value::Number(a % b), + Operator::GreaterThan => Value::Bool(a > b), + Operator::LessThan => Value::Bool(a < b), + Operator::Amp => { + let a_diff = (a - a.round()).abs(); + let b_diff = (b - b.round()).abs(); + + if a_diff > 0f64 || b_diff > 0f64 { + self.runtime_error("Cannot use fp operands for & operator"); + } + + Value::Number((a.round() as i64 & b.round() as i64) as f64) + } + Operator::Pipe => { + let a_diff = (a - a.round()).abs(); + let b_diff = (b - b.round()).abs(); + + if a_diff > 0f64 || b_diff > 0f64 { + self.runtime_error("Cannot use fp operands for | operator"); + } + + Value::Number((a.round() as i64 | b.round() as i64) as f64) + } + Operator::AmpAmp => Value::Bool(a != 0f64 && b != 0f64), + Operator::PipePipe => Value::Bool(a != 0f64 || b != 0f64), + }; + + self.push(result) + } + (Value::Bool(n), Value::Number(m)) => { + let (a, b) = (1f64, m); + + let result = match op { + Operator::Plus + | Operator::Minus + | Operator::Star + | Operator::Slash + | Operator::Mod + | Operator::GreaterThan + | Operator::LessThan => { + self.runtime_error("operands must be numbers"); + Value::Nil + } + Operator::Amp => Value::Number((a as i64 & b.round() as i64) as f64), + Operator::Pipe => Value::Number((a as i64 | b.round() as i64) as f64), + Operator::AmpAmp => Value::Bool(n && b != 0f64), + Operator::PipePipe => Value::Bool(n || b != 0f64), + }; + + self.push(result) + } + (Value::Number(n), Value::Bool(m)) => { + let (a, b) = (n, 1f64); + + let result = match op { + Operator::Plus + | Operator::Minus + | Operator::Star + | Operator::Slash + | Operator::Mod + | Operator::GreaterThan + | Operator::LessThan => { + self.runtime_error("operands must be numbers"); + Value::Nil + } + Operator::Amp => Value::Number((a.round() as i64 & b as i64) as f64), + Operator::Pipe => Value::Number((a.round() as i64 | b as i64) as f64), + Operator::AmpAmp => Value::Bool(a != 0f64 && m), + Operator::PipePipe => Value::Bool(a != 0f64 || m), + }; + + self.push(result) + } + (Value::Bool(n), Value::Bool(m)) => { + let (a, b) = (1f64, 1f64); + + let result = match op { + Operator::Plus + | Operator::Minus + | Operator::Star + | Operator::Slash + | Operator::Mod + | Operator::GreaterThan + | Operator::LessThan => { + self.runtime_error("operands must be numbers"); + Value::Nil + } + Operator::Amp => Value::Number((a as i64 & b as i64) as f64), + Operator::Pipe => Value::Number((a as i64 | b as i64) as f64), + Operator::AmpAmp => Value::Bool(n && m), + Operator::PipePipe => Value::Bool(n || m), + }; + + self.push(result) + } + (Value::String(a), Value::String(b)) => { + let result: Value = match op { + Operator::Plus => Value::String(format!("{}{}", a, b)), + Operator::Minus + | Operator::Star + | Operator::Slash + | Operator::Mod + | Operator::GreaterThan + | Operator::LessThan => { + let msg = format!("no {} operation on string '{}' and '{}'", op, a, b); + self.runtime_error(&msg); + Value::Nil + } + Operator::AmpAmp => Value::Bool(!a.is_empty() && !b.is_empty()), + Operator::PipePipe => Value::Bool(!a.is_empty() && !b.is_empty()), + _ => unreachable!("binary_op: invalid op {}", op), + }; + + self.push(result) + } + _ => { + unreachable!("binary_op: invalid op {}", op); + } + } + } +} diff --git a/engine/src/main.rs b/engine/src/main.rs index 0a4b824..0fcd6a5 100644 --- a/engine/src/main.rs +++ b/engine/src/main.rs @@ -1,508 +1,90 @@ -use std::sync::atomic::{Ordering}; -use std::sync::{Arc}; -use std::thread; -use std::time::{ - Duration, - SystemTime -}; -use tauri; - -#[macro_use(lazy_static)] -extern crate lazy_static; - mod app; mod daw; mod util; +mod lang; +mod event_dispatch; -#[tauri::command] -fn toggle_playlist(state: tauri::State<'_, Arc>) { - if state.playlist.playing() { - daw::pause_playlist(state); - } else { - // start playlist - daw::start_playlist(state); - } -} - -#[tauri::command] -fn get_playlist_playing( - state: tauri::State<'_, Arc> -) -> Result { - Ok(state.playlist.playing()) -} - -#[cfg(target_os = "windows")] -#[tauri::command] -fn get_playlist_start_time( - state: tauri::State<'_, Arc> -) -> Result { - let elapsed = state - .playlist - .started_time - .lock() - .unwrap() - .unwrap() - .elapsed(); - let now = SystemTime::now() - .duration_since(SystemTime::Windows) - .unwrap(); - let start_time = now - elapsed; - Ok(start_time.as_millis()) -} - -#[cfg(not(target_os = "windows"))] -#[tauri::command] -fn get_playlist_start_time( - state: tauri::State<'_, Arc> -) -> Result { - let elapsed = state - .playlist - .started_time - .lock() - .unwrap() - .unwrap() - .elapsed(); - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); - let start_time = now - elapsed; - Ok(start_time.as_millis()) -} - -#[tauri::command] -fn get_playlist_tempo( - state: tauri::State<'_, Arc> -) -> Result { - Ok(state.tempo()) -} - -#[tauri::command] -fn set_playlist_tempo( - state: tauri::State<'_, Arc>, - val: f32, -) { - println!("playlist tempo updated: {}", val); - daw::set_playlist_tempo(state, val); -} - -#[tauri::command] -fn toggle_metronome_enabled( - state: tauri::State<'_, Arc> -) { - let val = !state.metronome_enabled.load(Ordering::SeqCst); - state.metronome_enabled.store(val, Ordering::SeqCst); -} - -#[tauri::command] -fn get_metronome_enabled( - state: tauri::State<'_, Arc> -) -> Result { - Ok(state.metronome_enabled.load(Ordering::SeqCst)) -} - -#[tauri::command] -fn get_playlist_runtime_formatted( - state: tauri::State<'_, Arc> -) -> Result { - let start_time = state.playlist.started_time.lock().unwrap().unwrap(); - let res = util::format_playlist_runtime(start_time); - Ok(res) -} - -#[tauri::command] -fn get_playlist_beat_count( - state: tauri::State<'_, Arc> -) -> Result { - let beat_count = state.playlist.total_beats.load(Ordering::SeqCst); - Ok(beat_count) -} - -#[tauri::command] -fn get_playlist_time_signature( - state: tauri::State<'_, Arc> -) -> Result<(u16, u16), String> { - let res = state.playlist.time_signature.lock().unwrap(); - Ok((res.numerator, res.denominator)) -} - -#[tauri::command] -fn set_playlist_time_signature( - state: tauri::State<'_, Arc>, - numerator: u16, - denominator: u16, -) { - let updated: daw::timing::TimeSignature = daw::timing::TimeSignature { - numerator, - denominator, - }; - - *state.playlist.time_signature.lock().unwrap() = updated; -} - -#[tauri::command] -fn get_sidebar_samples() -> Result<( - Vec, - Vec, - Vec, - Vec -), String> { - let (samples, samples_paths, dirs, dirs_paths) = - app::env::get_sample_browser_root(); - Ok((samples, samples_paths, dirs, dirs_paths)) -} - -#[tauri::command] -fn enumerate_directory(dir_path: String) -> Result<( - Vec, - Vec, - Vec, - Vec -), String> { - let (samples, samples_paths, dirs, dirs_paths) = - app::env::enumerate_files_in_dir(dir_path); - Ok((samples, samples_paths, dirs, dirs_paths)) -} - -#[tauri::command] -fn preview_sample( - _state: tauri::State<'_, Arc>, - path: String, -) { - thread::spawn(move || { - futures::executor::block_on(daw::play_sample(&path)); - }); -} - -#[tauri::command] -fn get_audio_drivers() -> Result, String> { - Ok(daw::drivers::get_sound_host_names()) -} - -#[tauri::command] -fn add_audiograph_node( - state: tauri::State<'_, Arc>, - sample_path: String, - track_number: u32, - drop_x: Option, - drop_y: Option, -) -> Result { - let min_bound_x = state.playlist.ui.lock().unwrap().viewport.min_bound_x.unwrap(); - let min_bound_y = state.playlist.ui.lock().unwrap().viewport.min_bound_y.unwrap(); - let max_bound_x = state.playlist.ui.lock().unwrap().viewport.max_bound_x.unwrap(); - let max_bound_y = state.playlist.ui.lock().unwrap().viewport.max_bound_y.unwrap(); - let tempo = state.playlist.audiograph.lock().unwrap().tempo(); - let max_beats_displayed = state.playlist.ui.lock().unwrap().max_beats_displayed; - println!("max_beats_displayed: {}", max_beats_displayed); - let max_playlist_dur = daw::timing::n_subdivision_duration_from_tempo( - tempo, - max_beats_displayed as u32, - daw::timing::QuarterNote::new() - ); - - println!("max playlist dur: {}ms", max_playlist_dur.as_millis()); - - let start_offset = app::workspaces::playlist::calc_sample_offset( - drop_x.unwrap_or(min_bound_x), - drop_y.unwrap_or(min_bound_y), - min_bound_x, - min_bound_y, - max_bound_x, - max_bound_y, - max_playlist_dur - ); - - println!("added sample: {}, with offset of: {}ms", sample_path, start_offset.as_millis()); - - let id: u64; - if state.playlist.ui.lock().unwrap().snap_enabled { - println!("snapping the sample"); - // snap to nearest snap subdivision - let subdivision = state - .playlist - .ui - .lock() - .unwrap() - .snap_subdivision; - - id = state - .playlist - .audiograph - .lock() - .unwrap() - .construct_and_add_node_with_snap( - sample_path, - start_offset, - track_number, - daw::timing::SixteenthNote::new()); - } else { - // don't snap - id = state - .playlist - .audiograph - .lock() - .unwrap() - .construct_and_add_node(sample_path, start_offset, track_number); - } - - // returns the id of the new node, offset - Ok(id) -} - -#[tauri::command] -fn move_audiograph_node( - state: tauri::State<'_, Arc>, - id: u64, - track_number: u32, - drop_x: Option, - drop_y: Option, -) { - let min_bound_x = state.playlist.ui.lock().unwrap().viewport.min_bound_x.unwrap(); - let min_bound_y = state.playlist.ui.lock().unwrap().viewport.min_bound_y.unwrap(); - let max_bound_x = state.playlist.ui.lock().unwrap().viewport.max_bound_x.unwrap(); - let max_bound_y = state.playlist.ui.lock().unwrap().viewport.max_bound_y.unwrap(); - let tempo = state.playlist.audiograph.lock().unwrap().tempo(); - let max_beats_displayed = state.playlist.ui.lock().unwrap().max_beats_displayed; - println!("max_beats_displayed: {}", max_beats_displayed); - let max_playlist_dur = daw::timing::n_subdivision_duration_from_tempo( - tempo, - max_beats_displayed as u32, - daw::timing::QuarterNote::new() - ); - - let start_offset = app::workspaces::playlist::calc_sample_offset( - drop_x.unwrap_or(min_bound_x), - drop_y.unwrap_or(min_bound_y), - min_bound_x, - min_bound_y, - max_bound_x, - max_bound_y, - max_playlist_dur - ); - - - println!("moved sample with id: {}, with offset of: {}ms", id, start_offset.as_millis()); +use std::env; +use std::sync::{Arc}; +use std::io::{self, stdout, Write}; +use tauri; - if state.playlist.ui.lock().unwrap().snap_enabled { - println!("snapping the sample"); - // snap to nearest snap subdivision - let subdivision = state - .playlist - .ui - .lock() - .unwrap() - .snap_subdivision; +#[macro_use(lazy_static)] +extern crate lazy_static; - state - .playlist - .audiograph - .lock() - .unwrap() - .move_node_with_snap( - id, - start_offset, - track_number, - daw::timing::SixteenthNote::new()); - } else { - // don't snap - state - .playlist - .audiograph - .lock() - .unwrap() - .move_node(id, start_offset, track_number); +fn debug(){ + println!("debug mode:"); + + let mut vm = lang::vm::Vm::new(); + let mut line_num = 1; + let mut input = String::new(); + + loop { + print!("> "); + io::stdout().flush().ok(); + + let mut line = String::new(); + io::stdin().read_line(&mut line).ok(); + + if line == "exit\n" { + break; + } + + let res = lang::parser::Parser::new(line).compile(); + match res { + Ok(func) => { + func.chunk + .disassemble(format!("repl line {}", line_num).as_str()); + let res = vm.run(func); + match res { + Ok(value) => println!("{}", value), + _ => todo!("Handle runtime error"), + } + } + Err(_) => println!("Compile error"), + } + + line_num += 1; } -} - -#[tauri::command] -fn remove_audiograph_node( - state: tauri::State<'_, Arc>, - id: u64, -) { - state - .playlist - .audiograph - .lock() - .unwrap() - .remove_node(id); -} - -#[tauri::command] -fn get_node_data( - state: tauri::State<'_, Arc>, - id: u64, -) -> Result<(Vec, f32), String> { - let playlist = &state.playlist; - let mut audiograph = playlist - .audiograph - .lock() - .unwrap(); - let node = audiograph.get_mut_node_by_id(id).unwrap(); - println!("offset dur: {:?}", node.start_offset.as_millis()); - let waveform = node.get_waveform().clone(); - let dur = node.duration().as_secs_f32(); - let ratio = dur / audiograph.max_beats() as f32; - - - // return waveform data - Ok((waveform, dur)) -} - -#[tauri::command] -fn get_playlist_sample_offset( - state: tauri::State<'_, Arc>, - drop_x: f32, - drop_y: f32, - min_bound_x: f32, - min_bound_y: f32, - max_bound_x: f32, - max_bound_y: f32, -) -> Result { - // todo: choose a number that isn't arbitrary - let max_sample_offset = (max_bound_x - min_bound_x).round() as u64 * 5; - println!("max sample offset: {}", max_sample_offset); - let res = util::calc_playlist_sample_offset( - drop_x, - drop_y, - min_bound_x, - min_bound_y, - max_bound_x, - max_bound_y, - max_sample_offset, - ); - - Ok(res) -} - -#[tauri::command] -fn get_playlist_data( - state: tauri::State<'_, Arc> -) -> Result<(u64, u64, f32, u32), String> { - let audiograph = state - .playlist - .audiograph - .lock() - .unwrap(); - let max_playlist_beats = state - .playlist - .max_beats - .load(Ordering::SeqCst); - let max_beats_displayed = state - .playlist - .ui - .lock() - .unwrap() - .max_beats_displayed; - let track_count = audiograph.track_count(); - let max_playlist_duration = audiograph - .duration_max() - .as_secs_f32(); - - println!("max dur: {}s, track count: {}", max_playlist_duration, track_count); - - Ok(( - max_playlist_beats, - max_beats_displayed, - max_playlist_duration, - track_count - )) -} -#[tauri::command] -fn toggle_loop_enabled( - state: tauri::State<'_, Arc> -) { - let val = !state - .playlist - .loop_enabled - .load(Ordering::SeqCst); + // while input != "exit" { + // match io::stdin().read_line(&mut input) { + // Ok(_n) => { + // match &*input { + // "exit\n" => { + // break; + // } + // _ => { + // println!("exec: \"{}\"", input.trim()); + // let res = lang::parser::Parser::new(input).compile(); + + // match res { + // Ok(func) => { + // func.chunk + // .disassemble(format!("repl line {}", line_num).as_str()); + // let res = vm.run(func); + // match res { + // Ok(value) => println!("{}", value), + // _ => todo!("Handle runtime error"), + // } + // } + // Err(_) => println!("Compile error"), + // } + + // line_num += 1; + // } + // } + // } + // Err(error) => println!("error: {error}"), + // } + + // // input = "".to_string(); + // } +} + +fn init_tauri() { + use event_dispatch::*; - state - .playlist - .loop_enabled - .store(val, Ordering::SeqCst); -} - -#[tauri::command] -fn get_loop_enabled( - state: tauri::State<'_, Arc> -) -> Result { - Ok(state.playlist.loop_enabled.load(Ordering::SeqCst)) -} - -#[tauri::command] -fn toggle_snap_enabled( - state: tauri::State<'_, Arc> -) { - state - .playlist - .ui - .lock() - .unwrap() - .toggle_snap_enabled(); -} - -#[tauri::command] -fn get_snap_enabled( - state: tauri::State<'_, Arc> -) -> Result { - Ok(state.playlist.ui.lock().unwrap().snap_enabled) -} - -#[tauri::command] -fn get_playlist_max_length ( - state: tauri::State<'_, Arc> -) -> Result<(u64, u64), String> { - let dur = state - .playlist - .audiograph - .lock() - .unwrap() - .duration_max(); - let max_beats = state - .playlist - .audiograph - .lock() - .unwrap() - .max_beats(); - Ok((dur.as_millis().try_into().unwrap(), max_beats)) -} - -#[tauri::command] -fn init_playlist_workspace( - state: tauri::State<'_, Arc>, - min_bound_x: f32, - min_bound_y: f32, - max_bound_x: f32, - max_bound_y: f32, -) { - println!("min_bound_x: {}, max_bound_x: {}", min_bound_x, max_bound_x); - // update bounding box - state - .playlist - .ui - .lock() - .unwrap() - .viewport - .set_bounding_box( - min_bound_x, - min_bound_y, - max_bound_x, - max_bound_y); -} - -#[tauri::command] -fn toggle_record_input( - state: tauri::State<'_, Arc>, -) { - println!("toggling input recording"); - - let recording = &state - .playlist - .recording(); - daw::input::record_input(*recording); -} - -fn main() { tauri::Builder::default() .setup(app::window::setup) .menu(app::window::build_menu()) @@ -540,3 +122,26 @@ fn main() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +/** + * args: + * -d | --debug => debug mode + */ +fn main() { + let args: Vec = env::args().skip(1).collect(); + + if args.is_empty() { + init_tauri(); + } + + for arg in args { + match &arg[..] { + "-d" | "--debug" => debug(), + _ => { + if arg.starts_with('-') { + println!("Unkown argument: {}", arg); + } + } + } + } +} diff --git a/engine/src/mod.rs b/engine/src/mod.rs deleted file mode 100644 index a047d49..0000000 --- a/engine/src/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod app; -mod daw; -mod util; - -pub use app::*; -pub use daw::*; -pub use util::*; diff --git a/package.json b/package.json index e03821b..16a72f7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build": "tauri build", "build:assets:link": "ln -s engine/assets/ engine/target/assets", "dev": "tauri dev", + "debug": "cd engine && cargo run -- --debug", "lint": "eslint . --ignore-path .gitignore && prettier --check .", "lint:fix": "eslint . --fix --ignore-path .gitignore && pnpm format", "format": "prettier -w src/"