From 789cad3e97dfd3217ae670c5d4cdf14fa9465296 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Thu, 21 Sep 2023 22:57:05 -0400 Subject: [PATCH 01/19] figuring things out --- src-tauri/Cargo.lock | 113 +++++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/ablelinkbridge.rs | 170 ++++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 36 ++++++- 4 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 src-tauri/src/ablelinkbridge.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 38fce20bf..9eb9f1057 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -99,6 +99,7 @@ version = "0.1.0" dependencies = [ "midir", "rosc", + "rusty_link", "serde", "serde_json", "tauri", @@ -163,6 +164,28 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -285,6 +308,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.7.3" @@ -334,6 +366,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "cocoa" version = "0.24.1" @@ -637,6 +689,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "embed-resource" version = "2.1.1" @@ -1431,12 +1489,28 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "line-wrap" version = "0.1.1" @@ -1814,6 +1888,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.3.0" @@ -2206,6 +2286,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2235,6 +2321,16 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +[[package]] +name = "rusty_link" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212e275351a46badd88b67ab8a4a8e17580317c728c361299b245cb51debe81c" +dependencies = [ + "bindgen", + "cmake", +] + [[package]] name = "ryu" version = "1.0.13" @@ -2428,6 +2524,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3375,6 +3477,17 @@ dependencies = [ "windows-metadata", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 35aab489e..4c517cfba 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri = { version = "1.4.0", features = ["fs-all"] } midir = "0.9.1" tokio = { version = "1.29.0", features = ["full"] } rosc = "0.10.1" +rusty_link = "0.3.6" [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs new file mode 100644 index 000000000..daa75ff8b --- /dev/null +++ b/src-tauri/src/ablelinkbridge.rs @@ -0,0 +1,170 @@ +use std::time::Duration; +use rusty_link::{ AblLink, SessionState }; +use std::sync::Arc; +use tokio::sync::{ mpsc, Mutex }; +use serde::Deserialize; +use std::thread::sleep; + +use crate::loggerbridge::Logger; + +use tauri::{ Window, App, Manager }; + +#[derive(Deserialize, Clone, serde::Serialize)] +pub struct LinkMsg { + pub play: bool, + pub bpm: f64, +} + +#[derive(Clone)] +pub struct AbeLinkToJs { + pub window: Arc, +} + +impl AbeLinkToJs { + pub fn send(&self, payload: LinkMsg) { + let _ = self.window.emit("abelink-event", payload); + } +} + +pub struct State2 { + pub inner: Mutex>, + pub ablelink_state: Mutex, +} +// #[derive(Sync)] +pub struct AbleLinkState { + pub link: AblLink, + pub session_state: SessionState, + pub running: bool, + pub quantum: f64, +} + +impl AbleLinkState { + pub fn new() -> Self { + Self { + link: AblLink::new(120.0), + session_state: SessionState::new(), + running: true, + quantum: 4.0, + } + } + + pub fn capture_app_state(&mut self) { + self.link.capture_app_session_state(&mut self.session_state); + } + + pub fn commit_app_state(&mut self) { + self.link.commit_app_session_state(&self.session_state); + } +} + +pub async fn init( + abelink_to_js: AbeLinkToJs, + _logger: Logger, + async_input_receiver: mpsc::Receiver, + mut async_output_receiver: mpsc::Receiver, + async_output_transmitter: mpsc::Sender +) { + println!("init"); + tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await }); + + let message_queue: Arc>> = Arc::new(Mutex::new(Vec::new())); + //let s = app.state::().ablelink_state.lock().await; + // tauri::async_runtime::spawn(async move { + // abelink_to_js.send(LinkMsg { play: true, bpm: 100.0 }); + + // //let mut s = a.state::().ablelink_state.lock().await; + // // s.capture_app_state(); + // }); + + // let state = unlocked_state.ablelink_state.try_lock().unwrap(); + + /* ........................................................... + Listen For incoming messages and add to queue + ............................................................*/ + let message_queue_clone = Arc::clone(&message_queue); + tauri::async_runtime::spawn(async move { + loop { + if let Some(message) = async_output_receiver.recv().await { + let mut message_queue = message_queue_clone.lock().await; + (*message_queue).push(message); + } + } + }); + + let message_queue_clone = Arc::clone(&message_queue); + // tauri::async_runtime::spawn(async move { + // let a = app.lock().await; + // let mut state = a.state::().ablelink_state.lock().await; + + // /* ........................................................... + // Initialize Ableton link + // ............................................................*/ + // //let mut state = State::new(); + // state.link.enable(true); + // state.link.enable_start_stop_sync(true); + + // let mut prev_is_playing = state.session_state.is_playing(); + // let mut prev_bpm = state.session_state.tempo(); + + // /* ........................................................... + // Process queued messages + // ............................................................*/ + + // loop { + // let mut message_queue = message_queue_clone.lock().await; + + // state.capture_app_state(); + // let time_stamp = state.link.clock_micros(); + // let bpm = state.session_state.tempo(); + // let play = state.session_state.is_playing(); + // let quantum = state.quantum; + // println!("quant: {}", quantum); + + // if bpm != prev_bpm || play != prev_is_playing { + // //let cycle = state.session_state.time_at_beat(beat, quantum) + // let payload = LinkMsg { + // bpm, + // play, + // }; + // abelink_to_js.send(payload); + // prev_is_playing = play; + // prev_bpm = bpm; + // } + + // message_queue.retain(|message| { + // let is_playing = message.play; + // println!("is playing {}", is_playing); + // if is_playing != prev_is_playing { + // if is_playing == false { + // state.session_state.set_is_playing(false, time_stamp as u64); + // } else { + // state.session_state.set_is_playing_and_request_beat_at_time(true, time_stamp as u64, 0.0, quantum); + // } + // state.commit_app_state(); + // } + // return false; + // }); + + // sleep(Duration::from_millis(10)); + // } + // }); +} + +pub async fn async_process_model( + mut input_reciever: mpsc::Receiver, + output_transmitter: mpsc::Sender +) -> Result<(), Box> { + while let Some(input) = input_reciever.recv().await { + let output = input; + output_transmitter.send(output).await?; + } + Ok(()) +} + +// Called from JS +#[tauri::command] +pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, State2>) -> Result<(), String> { + println!("bpm {} play {}", linkmsg.bpm, linkmsg.play); + let async_proc_input_tx = state.inner.lock().await; + async_proc_input_tx.send(linkmsg).await.map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6f97b8c98..c866fbdc6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,8 +4,11 @@ mod midibridge; mod oscbridge; mod loggerbridge; +mod ablelinkbridge; use std::sync::Arc; +use ablelinkbridge::AbeLinkToJs; +use ablelinkbridge::State2; use loggerbridge::Logger; use tauri::Manager; use tokio::sync::mpsc; @@ -21,6 +24,8 @@ fn main() { let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1); let (async_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1); let (async_output_transmitter_osc, async_output_receiver_osc) = mpsc::channel(1); + let (async_input_transmitter_abelink, async_input_receiver_abelink) = mpsc::channel(1); + let (async_output_transmitter_abelink, async_output_receiver_abelink) = mpsc::channel(1); tauri::Builder ::default() .manage(midibridge::AsyncInputTransmit { @@ -29,17 +34,42 @@ fn main() { .manage(oscbridge::AsyncInputTransmit { inner: Mutex::new(async_input_transmitter_osc), }) - .invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc]) + .manage(ablelinkbridge::State2 { + inner: Mutex::new(async_input_transmitter_abelink), + ablelink_state: Mutex::new(ablelinkbridge::AbleLinkState::new()), + }) + .invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, ablelinkbridge::sendabelinkmsg]) .setup(|app| { + // let mut able_link_state = Arc::new( + // Mutex::new(ablelinkbridge::State::new(Mutex::new(async_input_transmitter_abelink))) + // ); + // app.manage(able_link_state.clone()); let window = Arc::new(app.get_window("main").unwrap()); - let logger = Logger { window }; + let logger = Logger { window: window.clone() }; + // let state_mutex = app.state::>(); + // state_mutex.lock(); + midibridge::init( logger.clone(), async_input_receiver_midi, async_output_receiver_midi, async_output_transmitter_midi ); - oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc); + oscbridge::init( + logger.clone(), + async_input_receiver_osc, + async_output_receiver_osc, + async_output_transmitter_osc + ); + + ablelinkbridge::init( + AbeLinkToJs { window }, + logger.clone(), + async_input_receiver_abelink, + async_output_receiver_abelink, + async_output_transmitter_abelink + ); + Ok(()) }) .run(tauri::generate_context!()) From db36636ab23e8ca2932217a974622268b12fbfb3 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Thu, 21 Sep 2023 23:08:30 -0400 Subject: [PATCH 02/19] play working kinda --- src-tauri/src/ablelinkbridge.rs | 135 ++++++++++++++------------------ src-tauri/src/main.rs | 12 +-- 2 files changed, 60 insertions(+), 87 deletions(-) diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs index daa75ff8b..5728cc914 100644 --- a/src-tauri/src/ablelinkbridge.rs +++ b/src-tauri/src/ablelinkbridge.rs @@ -7,7 +7,7 @@ use std::thread::sleep; use crate::loggerbridge::Logger; -use tauri::{ Window, App, Manager }; +use tauri::Window; #[derive(Deserialize, Clone, serde::Serialize)] pub struct LinkMsg { @@ -26,19 +26,17 @@ impl AbeLinkToJs { } } -pub struct State2 { +pub struct AsyncInputTransmit { pub inner: Mutex>, - pub ablelink_state: Mutex, } -// #[derive(Sync)] -pub struct AbleLinkState { +pub struct State { pub link: AblLink, pub session_state: SessionState, pub running: bool, pub quantum: f64, } -impl AbleLinkState { +impl State { pub fn new() -> Self { Self { link: AblLink::new(120.0), @@ -57,27 +55,15 @@ impl AbleLinkState { } } -pub async fn init( - abelink_to_js: AbeLinkToJs, +pub fn init( _logger: Logger, + abelink_to_js: AbeLinkToJs, async_input_receiver: mpsc::Receiver, mut async_output_receiver: mpsc::Receiver, async_output_transmitter: mpsc::Sender ) { - println!("init"); tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await }); - let message_queue: Arc>> = Arc::new(Mutex::new(Vec::new())); - //let s = app.state::().ablelink_state.lock().await; - // tauri::async_runtime::spawn(async move { - // abelink_to_js.send(LinkMsg { play: true, bpm: 100.0 }); - - // //let mut s = a.state::().ablelink_state.lock().await; - // // s.capture_app_state(); - // }); - - // let state = unlocked_state.ablelink_state.try_lock().unwrap(); - /* ........................................................... Listen For incoming messages and add to queue ............................................................*/ @@ -92,62 +78,57 @@ pub async fn init( }); let message_queue_clone = Arc::clone(&message_queue); - // tauri::async_runtime::spawn(async move { - // let a = app.lock().await; - // let mut state = a.state::().ablelink_state.lock().await; - - // /* ........................................................... - // Initialize Ableton link - // ............................................................*/ - // //let mut state = State::new(); - // state.link.enable(true); - // state.link.enable_start_stop_sync(true); - - // let mut prev_is_playing = state.session_state.is_playing(); - // let mut prev_bpm = state.session_state.tempo(); - - // /* ........................................................... - // Process queued messages - // ............................................................*/ - - // loop { - // let mut message_queue = message_queue_clone.lock().await; - - // state.capture_app_state(); - // let time_stamp = state.link.clock_micros(); - // let bpm = state.session_state.tempo(); - // let play = state.session_state.is_playing(); - // let quantum = state.quantum; - // println!("quant: {}", quantum); - - // if bpm != prev_bpm || play != prev_is_playing { - // //let cycle = state.session_state.time_at_beat(beat, quantum) - // let payload = LinkMsg { - // bpm, - // play, - // }; - // abelink_to_js.send(payload); - // prev_is_playing = play; - // prev_bpm = bpm; - // } - - // message_queue.retain(|message| { - // let is_playing = message.play; - // println!("is playing {}", is_playing); - // if is_playing != prev_is_playing { - // if is_playing == false { - // state.session_state.set_is_playing(false, time_stamp as u64); - // } else { - // state.session_state.set_is_playing_and_request_beat_at_time(true, time_stamp as u64, 0.0, quantum); - // } - // state.commit_app_state(); - // } - // return false; - // }); - - // sleep(Duration::from_millis(10)); - // } - // }); + tauri::async_runtime::spawn(async move { + /* ........................................................... + Initialize Ableton link + ............................................................*/ + let mut state = State::new(); + state.link.enable(true); + state.link.enable_start_stop_sync(true); + + let mut prev_is_playing = state.session_state.is_playing(); + let mut prev_bpm = state.session_state.tempo(); + + /* ........................................................... + Process queued messages + ............................................................*/ + + loop { + let mut message_queue = message_queue_clone.lock().await; + + state.capture_app_state(); + let time_stamp = state.link.clock_micros(); + let bpm = state.session_state.tempo(); + let play = state.session_state.is_playing(); + + if bpm != prev_bpm || play != prev_is_playing { + //let cycle = state.session_state.time_at_beat(beat, quantum) + let payload = LinkMsg { + bpm, + play, + }; + abelink_to_js.send(payload); + prev_is_playing = play; + prev_bpm = bpm; + } + + message_queue.retain(|message| { + let is_playing = message.play; + println!("is playing {}", is_playing); + if is_playing != prev_is_playing { + if is_playing == false { + state.session_state.set_is_playing(false, time_stamp as u64); + } else { + state.session_state.set_is_playing_and_request_beat_at_time(true, time_stamp as u64, 0.0, state.quantum); + } + state.commit_app_state(); + } + return false; + }); + + sleep(Duration::from_millis(10)); + } + }); } pub async fn async_process_model( @@ -163,7 +144,7 @@ pub async fn async_process_model( // Called from JS #[tauri::command] -pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, State2>) -> Result<(), String> { +pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AsyncInputTransmit>) -> Result<(), String> { println!("bpm {} play {}", linkmsg.bpm, linkmsg.play); let async_proc_input_tx = state.inner.lock().await; async_proc_input_tx.send(linkmsg).await.map_err(|e| e.to_string()) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c866fbdc6..ca1db1ef1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,7 +8,6 @@ mod ablelinkbridge; use std::sync::Arc; use ablelinkbridge::AbeLinkToJs; -use ablelinkbridge::State2; use loggerbridge::Logger; use tauri::Manager; use tokio::sync::mpsc; @@ -34,20 +33,13 @@ fn main() { .manage(oscbridge::AsyncInputTransmit { inner: Mutex::new(async_input_transmitter_osc), }) - .manage(ablelinkbridge::State2 { + .manage(ablelinkbridge::AsyncInputTransmit { inner: Mutex::new(async_input_transmitter_abelink), - ablelink_state: Mutex::new(ablelinkbridge::AbleLinkState::new()), }) .invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, ablelinkbridge::sendabelinkmsg]) .setup(|app| { - // let mut able_link_state = Arc::new( - // Mutex::new(ablelinkbridge::State::new(Mutex::new(async_input_transmitter_abelink))) - // ); - // app.manage(able_link_state.clone()); let window = Arc::new(app.get_window("main").unwrap()); let logger = Logger { window: window.clone() }; - // let state_mutex = app.state::>(); - // state_mutex.lock(); midibridge::init( logger.clone(), @@ -63,8 +55,8 @@ fn main() { ); ablelinkbridge::init( - AbeLinkToJs { window }, logger.clone(), + AbeLinkToJs { window }, async_input_receiver_abelink, async_output_receiver_abelink, async_output_transmitter_abelink From d7fe491c840f8d908ab5dc7c9a780576768a9eac Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Fri, 22 Sep 2023 14:27:03 -0400 Subject: [PATCH 03/19] play stop working --- src-tauri/src/ablelinkbridge.rs | 31 ++++++++++++++++++++----------- src-tauri/src/main.rs | 11 ++++++++--- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs index 5728cc914..e035709cf 100644 --- a/src-tauri/src/ablelinkbridge.rs +++ b/src-tauri/src/ablelinkbridge.rs @@ -28,15 +28,16 @@ impl AbeLinkToJs { pub struct AsyncInputTransmit { pub inner: Mutex>, + pub abelink: Arc>, } -pub struct State { +pub struct AbeLinkState { pub link: AblLink, pub session_state: SessionState, pub running: bool, pub quantum: f64, } -impl State { +impl AbeLinkState { pub fn new() -> Self { Self { link: AblLink::new(120.0), @@ -58,6 +59,7 @@ impl State { pub fn init( _logger: Logger, abelink_to_js: AbeLinkToJs, + abelink: Arc>, async_input_receiver: mpsc::Receiver, mut async_output_receiver: mpsc::Receiver, async_output_transmitter: mpsc::Sender @@ -82,24 +84,28 @@ pub fn init( /* ........................................................... Initialize Ableton link ............................................................*/ - let mut state = State::new(); - state.link.enable(true); - state.link.enable_start_stop_sync(true); + //let mut state = AbeLinkState::new(); - let mut prev_is_playing = state.session_state.is_playing(); - let mut prev_bpm = state.session_state.tempo(); + let mut prev_is_playing = false; + let mut prev_bpm = 120.0; /* ........................................................... Process queued messages ............................................................*/ loop { - let mut message_queue = message_queue_clone.lock().await; + let mut state = abelink.lock().await; + if state.link.is_enabled() == false { + state.link.enable(true); + state.link.enable_start_stop_sync(true); + } - state.capture_app_state(); + let mut message_queue = message_queue_clone.lock().await; let time_stamp = state.link.clock_micros(); let bpm = state.session_state.tempo(); let play = state.session_state.is_playing(); + let quantum = state.quantum; + state.capture_app_state(); if bpm != prev_bpm || play != prev_is_playing { //let cycle = state.session_state.time_at_beat(beat, quantum) @@ -119,13 +125,13 @@ pub fn init( if is_playing == false { state.session_state.set_is_playing(false, time_stamp as u64); } else { - state.session_state.set_is_playing_and_request_beat_at_time(true, time_stamp as u64, 0.0, state.quantum); + state.session_state.set_is_playing_and_request_beat_at_time(true, time_stamp as u64, 0.0, quantum); } state.commit_app_state(); } return false; }); - + drop(state); sleep(Duration::from_millis(10)); } }); @@ -147,5 +153,8 @@ pub async fn async_process_model( pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AsyncInputTransmit>) -> Result<(), String> { println!("bpm {} play {}", linkmsg.bpm, linkmsg.play); let async_proc_input_tx = state.inner.lock().await; + // let abelink = state.abelink.lock().await; + // drop(abelink); + async_proc_input_tx.send(linkmsg).await.map_err(|e| e.to_string()) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ca1db1ef1..aa3e7a12f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,6 +7,7 @@ mod loggerbridge; mod ablelinkbridge; use std::sync::Arc; +use ablelinkbridge::AbeLinkState; use ablelinkbridge::AbeLinkToJs; use loggerbridge::Logger; use tauri::Manager; @@ -33,14 +34,17 @@ fn main() { .manage(oscbridge::AsyncInputTransmit { inner: Mutex::new(async_input_transmitter_osc), }) - .manage(ablelinkbridge::AsyncInputTransmit { - inner: Mutex::new(async_input_transmitter_abelink), - }) .invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, ablelinkbridge::sendabelinkmsg]) .setup(|app| { let window = Arc::new(app.get_window("main").unwrap()); let logger = Logger { window: window.clone() }; + let abelink = Arc::new(Mutex::new(AbeLinkState::new())); + app.manage(ablelinkbridge::AsyncInputTransmit { + inner: Mutex::new(async_input_transmitter_abelink), + abelink: abelink.clone(), + }); + midibridge::init( logger.clone(), async_input_receiver_midi, @@ -57,6 +61,7 @@ fn main() { ablelinkbridge::init( logger.clone(), AbeLinkToJs { window }, + abelink, async_input_receiver_abelink, async_output_receiver_abelink, async_output_transmitter_abelink From 209122b8f7b3cbab1e11bea1e1c350fc64d57f5b Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Mon, 25 Sep 2023 19:29:14 -0400 Subject: [PATCH 04/19] start offset --- src-tauri/src/ablelinkbridge.rs | 47 ++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs index e035709cf..16daada29 100644 --- a/src-tauri/src/ablelinkbridge.rs +++ b/src-tauri/src/ablelinkbridge.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::time::{ Duration, SystemTime, UNIX_EPOCH }; use rusty_link::{ AblLink, SessionState }; use std::sync::Arc; use tokio::sync::{ mpsc, Mutex }; @@ -13,6 +13,7 @@ use tauri::Window; pub struct LinkMsg { pub play: bool, pub bpm: f64, + pub timestamp: u64, } #[derive(Clone)] @@ -112,6 +113,7 @@ pub fn init( let payload = LinkMsg { bpm, play, + timestamp: 1, }; abelink_to_js.send(payload); prev_is_playing = play; @@ -150,11 +152,44 @@ pub async fn async_process_model( // Called from JS #[tauri::command] -pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AsyncInputTransmit>) -> Result<(), String> { +pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AsyncInputTransmit>) -> Result { println!("bpm {} play {}", linkmsg.bpm, linkmsg.play); let async_proc_input_tx = state.inner.lock().await; - // let abelink = state.abelink.lock().await; - // drop(abelink); - - async_proc_input_tx.send(linkmsg).await.map_err(|e| e.to_string()) + let mut abelink = state.abelink.lock().await; + let quantum = abelink.quantum; + let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + // abelink.session_state.phase_at_time(time, quantum) + //let x = abelink.session_state.request_beat_at_start_playing_time(1.0, quantum); + + let x = Duration::from_millis(abelink.session_state.time_at_beat(0.0, quantum) as u64); + let link_clock = Duration::from_millis(abelink.link.clock_micros() as u64); + + let timestamp = current_time.as_millis(); + let phase = abelink.session_state.phase_at_time(abelink.link.clock_micros(), quantum); + let bpm = abelink.session_state.tempo(); + let beat = abelink.session_state.beat_at_time(abelink.link.clock_micros(), quantum); + + let bps = 60.0 / bpm; + let phase_offset = quantum - phase; + let seconds_offset = phase_offset * bps; + let milliseconds_offset = seconds_offset * 1000.0; + let transport_offset = (milliseconds_offset as u64) + (current_time.as_millis() as u64); + + // let d = Duration::from_millis(x as u64); + // let d2 = Duration::from_secs(0); + // let d3 = d + d2; + + let x2 = Duration::from_millis(abelink.session_state.time_for_is_playing()); + + println!("quantum {}, phase {}, bpm {}, beat {}, seconds offset {}", quantum, phase, bpm, beat, seconds_offset); + let ret_message = LinkMsg { + bpm: linkmsg.bpm.clone(), + play: linkmsg.play.clone(), + timestamp: transport_offset, + }; + + async_proc_input_tx.send(linkmsg).await.unwrap(); + + drop(abelink); + Ok(ret_message) } From 172006a3b3b3aaeff83a5a7d4d970da40a502aff Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Mon, 25 Sep 2023 22:32:31 -0400 Subject: [PATCH 05/19] improving listener --- packages/core/cyclist.mjs | 26 +++++++++++++-- packages/react/src/hooks/useStrudel.mjs | 42 ++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 303525a25..e07722b77 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; +import { Invoke } from '../../website/src/tauri.mjs'; export class Cyclist { constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { @@ -62,9 +63,22 @@ export class Cyclist { if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } - logger('[cyclist] start'); - this.clock.start(); - this.setStarted(true); + + const linkmsg = { + bpm: this.cps * 60, + play: true, + timestamp: Date.now(), + }; + Invoke('sendabelinkmsg', { linkmsg }).then((res) => { + const timeoffset = res.timestamp - Date.now(); + + console.log({ res, timeoffset }); + window.setTimeout(() => { + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); + }, timeoffset); + }); } pause() { logger('[cyclist] pause'); @@ -76,6 +90,12 @@ export class Cyclist { this.clock.stop(); this.lastEnd = 0; this.setStarted(false); + const linkmsg = { + bpm: this.clock.interval, + play: false, + timestamp: Date.now(), + }; + Invoke('sendabelinkmsg', { linkmsg }); } setPattern(pat, autostart = false) { this.pattern = pat; diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index a10998e73..5c9ca374d 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -3,7 +3,7 @@ import { repl } from '@strudel.cycles/core'; import { transpiler } from '@strudel.cycles/transpiler'; import usePatternFrame from './usePatternFrame'; import usePostMessage from './usePostMessage.mjs'; - +import { listen } from '@tauri-apps/api/event'; function useStrudel({ defaultOutput, interval, @@ -67,6 +67,7 @@ function useStrudel({ }), [defaultOutput, interval, getTime], ); + const broadcast = usePostMessage(({ data: { from, type } }) => { if (type === 'start' && from !== id) { // console.log('message', from, type); @@ -126,6 +127,45 @@ function useStrudel({ await activateCode(); } }; + + // useEffect(() => { + // async function listenForAbelinkMessage() { + // await listen('abelink-event', async (e) => { + // const payload = e?.payload; + // if (payload == null) { + // return; + // } + // const { play, bpm, timestamp } = payload; + // console.log(scheduler.started); + + // if (scheduler.started !== play && play != null) { + // togglePlay(); + // } + + // const { message, message_type } = e.payload; + // }); + // } + // listenForAbelinkMessage(); + // }, []); + + listen('abelink-event', async (e) => { + const payload = e?.payload; + if (payload == null) { + return; + } + const { play, bpm, timestamp } = payload; + + if (started !== play && play != null) { + if (play) { + start(); + } else { + stop(); + } + } + + const { message, message_type } = e.payload; + }); + const error = schedulerError || evalError; usePatternFrame({ From 6719c6127b3752576646ca5ab630f0551ac72972 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Wed, 27 Sep 2023 15:28:23 -0400 Subject: [PATCH 06/19] figuring out backend integration --- packages/core/cyclist.mjs | 19 +++++++++++++++++++ packages/react/src/hooks/useStrudel.mjs | 21 +-------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index e07722b77..9cabceb45 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -7,6 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; import { Invoke } from '../../website/src/tauri.mjs'; +import { listen } from '@tauri-apps/api/event'; export class Cyclist { constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { @@ -17,6 +18,24 @@ export class Cyclist { this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time this.onToggle = onToggle; + // this.abeLinkListener = listen('abelink-event', async (e) => { + // const payload = e?.payload; + // if (payload == null) { + // return; + // } + // const { play, bpm, timestamp } = payload; + // console.log('play'); + + // if (this.started !== play && play != null) { + // if (play) { + // this.start(); + // } else { + // this.stop(); + // } + // } + + // const { message, message_type } = e.payload; + // }); this.latency = latency; // fixed trigger time offset const round = (x) => Math.round(x * 1000) / 1000; this.clock = createClock( diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 5c9ca374d..93674af5a 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -128,26 +128,6 @@ function useStrudel({ } }; - // useEffect(() => { - // async function listenForAbelinkMessage() { - // await listen('abelink-event', async (e) => { - // const payload = e?.payload; - // if (payload == null) { - // return; - // } - // const { play, bpm, timestamp } = payload; - // console.log(scheduler.started); - - // if (scheduler.started !== play && play != null) { - // togglePlay(); - // } - - // const { message, message_type } = e.payload; - // }); - // } - // listenForAbelinkMessage(); - // }, []); - listen('abelink-event', async (e) => { const payload = e?.payload; if (payload == null) { @@ -157,6 +137,7 @@ function useStrudel({ if (started !== play && play != null) { if (play) { + // activateCode(); start(); } else { stop(); From 6eda7af14bc006ea30daee873abc71267c8d4bb1 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Fri, 29 Sep 2023 01:01:01 -0400 Subject: [PATCH 07/19] play sync now accurate --- packages/core/cyclist.mjs | 56 +++++------ packages/react/src/hooks/useStrudel.mjs | 32 +++--- src-tauri/src/ablelinkbridge.rs | 126 ++++++------------------ src-tauri/src/main.rs | 13 +-- 4 files changed, 73 insertions(+), 154 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 9cabceb45..7304558a8 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -18,24 +18,30 @@ export class Cyclist { this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time this.onToggle = onToggle; - // this.abeLinkListener = listen('abelink-event', async (e) => { - // const payload = e?.payload; - // if (payload == null) { - // return; - // } - // const { play, bpm, timestamp } = payload; - // console.log('play'); - - // if (this.started !== play && play != null) { - // if (play) { - // this.start(); - // } else { - // this.stop(); - // } - // } + this.start_timer; + this.abeLinkListener = listen('abelink-event', async (e) => { + const payload = e?.payload; + if (payload == null) { + return; + } + const { play, bpm, timestamp } = payload; + // (if bpm !== prev_bpm) { + //update the clock + // } + if (this.started !== play && play != null) { + if (play) { + this.start_timer = window.setTimeout(() => { + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); + }, timestamp - Date.now()); + } else { + this.stop(); + } + } - // const { message, message_type } = e.payload; - // }); + const { message, message_type } = e.payload; + }); this.latency = latency; // fixed trigger time offset const round = (x) => Math.round(x * 1000) / 1000; this.clock = createClock( @@ -78,26 +84,18 @@ export class Cyclist { this.started = v; this.onToggle?.(v); } + startClock() {} start() { if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } const linkmsg = { - bpm: this.cps * 60, + bpm: 110, play: true, timestamp: Date.now(), }; - Invoke('sendabelinkmsg', { linkmsg }).then((res) => { - const timeoffset = res.timestamp - Date.now(); - - console.log({ res, timeoffset }); - window.setTimeout(() => { - logger('[cyclist] start'); - this.clock.start(); - this.setStarted(true); - }, timeoffset); - }); + Invoke('sendabelinkmsg', { linkmsg }); } pause() { logger('[cyclist] pause'); @@ -110,7 +108,7 @@ export class Cyclist { this.lastEnd = 0; this.setStarted(false); const linkmsg = { - bpm: this.clock.interval, + bpm: 110, play: false, timestamp: Date.now(), }; diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 93674af5a..051b2a170 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -128,24 +128,24 @@ function useStrudel({ } }; - listen('abelink-event', async (e) => { - const payload = e?.payload; - if (payload == null) { - return; - } - const { play, bpm, timestamp } = payload; + // listen('abelink-event', async (e) => { + // const payload = e?.payload; + // if (payload == null) { + // return; + // } + // const { play, bpm, timestamp } = payload; - if (started !== play && play != null) { - if (play) { - // activateCode(); - start(); - } else { - stop(); - } - } + // if (started !== play && play != null) { + // if (play) { + // // activateCode(); + // start(); + // } else { + // stop(); + // } + // } - const { message, message_type } = e.payload; - }); + // const { message, message_type } = e.payload; + // }); const error = schedulerError || evalError; diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs index 16daada29..d1bf7d652 100644 --- a/src-tauri/src/ablelinkbridge.rs +++ b/src-tauri/src/ablelinkbridge.rs @@ -1,7 +1,7 @@ use std::time::{ Duration, SystemTime, UNIX_EPOCH }; use rusty_link::{ AblLink, SessionState }; use std::sync::Arc; -use tokio::sync::{ mpsc, Mutex }; +use tokio::sync::Mutex; use serde::Deserialize; use std::thread::sleep; @@ -28,7 +28,6 @@ impl AbeLinkToJs { } pub struct AsyncInputTransmit { - pub inner: Mutex>, pub abelink: Arc>, } pub struct AbeLinkState { @@ -57,43 +56,15 @@ impl AbeLinkState { } } -pub fn init( - _logger: Logger, - abelink_to_js: AbeLinkToJs, - abelink: Arc>, - async_input_receiver: mpsc::Receiver, - mut async_output_receiver: mpsc::Receiver, - async_output_transmitter: mpsc::Sender -) { - tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await }); - let message_queue: Arc>> = Arc::new(Mutex::new(Vec::new())); - /* ........................................................... - Listen For incoming messages and add to queue - ............................................................*/ - let message_queue_clone = Arc::clone(&message_queue); - tauri::async_runtime::spawn(async move { - loop { - if let Some(message) = async_output_receiver.recv().await { - let mut message_queue = message_queue_clone.lock().await; - (*message_queue).push(message); - } - } - }); - - let message_queue_clone = Arc::clone(&message_queue); +pub fn init(_logger: Logger, abelink_to_js: AbeLinkToJs, abelink: Arc>) { tauri::async_runtime::spawn(async move { /* ........................................................... Initialize Ableton link ............................................................*/ - //let mut state = AbeLinkState::new(); let mut prev_is_playing = false; let mut prev_bpm = 120.0; - /* ........................................................... - Process queued messages - ............................................................*/ - loop { let mut state = abelink.lock().await; if state.link.is_enabled() == false { @@ -101,95 +72,54 @@ pub fn init( state.link.enable_start_stop_sync(true); } - let mut message_queue = message_queue_clone.lock().await; - let time_stamp = state.link.clock_micros(); + let link_time_stamp = state.link.clock_micros(); let bpm = state.session_state.tempo(); - let play = state.session_state.is_playing(); + let started = state.session_state.is_playing(); let quantum = state.quantum; + let beat = state.session_state.beat_at_time(link_time_stamp, quantum); + let phase = state.session_state.phase_at_time(link_time_stamp, quantum); + + let time_at_next_cycle = state.session_state.time_at_beat(beat + (quantum - phase), quantum); + + let time_offset = Duration::from_micros((time_at_next_cycle - link_time_stamp) as u64); + let current_unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let message_timestamp = (current_unix_time + time_offset).as_millis() - 140; + state.capture_app_state(); - if bpm != prev_bpm || play != prev_is_playing { - //let cycle = state.session_state.time_at_beat(beat, quantum) + if bpm != prev_bpm || started != prev_is_playing { let payload = LinkMsg { bpm, - play, - timestamp: 1, + play: started, + timestamp: message_timestamp as u64, }; abelink_to_js.send(payload); - prev_is_playing = play; + prev_is_playing = started; prev_bpm = bpm; } - message_queue.retain(|message| { - let is_playing = message.play; - println!("is playing {}", is_playing); - if is_playing != prev_is_playing { - if is_playing == false { - state.session_state.set_is_playing(false, time_stamp as u64); - } else { - state.session_state.set_is_playing_and_request_beat_at_time(true, time_stamp as u64, 0.0, quantum); - } - state.commit_app_state(); - } - return false; - }); drop(state); sleep(Duration::from_millis(10)); } }); } -pub async fn async_process_model( - mut input_reciever: mpsc::Receiver, - output_transmitter: mpsc::Sender -) -> Result<(), Box> { - while let Some(input) = input_reciever.recv().await { - let output = input; - output_transmitter.send(output).await?; - } - Ok(()) -} - // Called from JS #[tauri::command] -pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AsyncInputTransmit>) -> Result { +pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AsyncInputTransmit>) -> Result<(), String> { println!("bpm {} play {}", linkmsg.bpm, linkmsg.play); - let async_proc_input_tx = state.inner.lock().await; let mut abelink = state.abelink.lock().await; + let started = abelink.session_state.is_playing(); + let time_stamp = abelink.link.clock_micros(); let quantum = abelink.quantum; - let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - // abelink.session_state.phase_at_time(time, quantum) - //let x = abelink.session_state.request_beat_at_start_playing_time(1.0, quantum); - - let x = Duration::from_millis(abelink.session_state.time_at_beat(0.0, quantum) as u64); - let link_clock = Duration::from_millis(abelink.link.clock_micros() as u64); - - let timestamp = current_time.as_millis(); - let phase = abelink.session_state.phase_at_time(abelink.link.clock_micros(), quantum); - let bpm = abelink.session_state.tempo(); - let beat = abelink.session_state.beat_at_time(abelink.link.clock_micros(), quantum); - - let bps = 60.0 / bpm; - let phase_offset = quantum - phase; - let seconds_offset = phase_offset * bps; - let milliseconds_offset = seconds_offset * 1000.0; - let transport_offset = (milliseconds_offset as u64) + (current_time.as_millis() as u64); - - // let d = Duration::from_millis(x as u64); - // let d2 = Duration::from_secs(0); - // let d3 = d + d2; - - let x2 = Duration::from_millis(abelink.session_state.time_for_is_playing()); - - println!("quantum {}, phase {}, bpm {}, beat {}, seconds offset {}", quantum, phase, bpm, beat, seconds_offset); - let ret_message = LinkMsg { - bpm: linkmsg.bpm.clone(), - play: linkmsg.play.clone(), - timestamp: transport_offset, - }; - - async_proc_input_tx.send(linkmsg).await.unwrap(); + if linkmsg.play != started { + abelink.session_state.set_is_playing_and_request_beat_at_time(linkmsg.play, time_stamp as u64, 0.0, quantum); + } + if linkmsg.bpm != abelink.session_state.tempo() { + abelink.session_state.set_tempo(linkmsg.bpm, time_stamp); + } + abelink.commit_app_state(); drop(abelink); - Ok(ret_message) + Ok(()) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index aa3e7a12f..6e3e47ac5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -24,8 +24,7 @@ fn main() { let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1); let (async_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1); let (async_output_transmitter_osc, async_output_receiver_osc) = mpsc::channel(1); - let (async_input_transmitter_abelink, async_input_receiver_abelink) = mpsc::channel(1); - let (async_output_transmitter_abelink, async_output_receiver_abelink) = mpsc::channel(1); + tauri::Builder ::default() .manage(midibridge::AsyncInputTransmit { @@ -41,7 +40,6 @@ fn main() { let abelink = Arc::new(Mutex::new(AbeLinkState::new())); app.manage(ablelinkbridge::AsyncInputTransmit { - inner: Mutex::new(async_input_transmitter_abelink), abelink: abelink.clone(), }); @@ -58,14 +56,7 @@ fn main() { async_output_transmitter_osc ); - ablelinkbridge::init( - logger.clone(), - AbeLinkToJs { window }, - abelink, - async_input_receiver_abelink, - async_output_receiver_abelink, - async_output_transmitter_abelink - ); + ablelinkbridge::init(logger.clone(), AbeLinkToJs { window }, abelink); Ok(()) }) From fe8c4e2bcd65d7e2c15598717f0983b5da25c99b Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Fri, 29 Sep 2023 23:27:43 -0400 Subject: [PATCH 08/19] cyclist bridge --- packages/core/cyclist.mjs | 43 ++--------------- packages/core/repl.mjs | 9 +++- packages/desktopbridge/cyclistbridge.mjs | 61 ++++++++++++++++++++++++ packages/desktopbridge/index.mjs | 1 + website/src/repl/Repl.jsx | 1 + 5 files changed, 73 insertions(+), 42 deletions(-) create mode 100644 packages/desktopbridge/cyclistbridge.mjs diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 7304558a8..303525a25 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,8 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; -import { Invoke } from '../../website/src/tauri.mjs'; -import { listen } from '@tauri-apps/api/event'; export class Cyclist { constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { @@ -18,30 +16,6 @@ export class Cyclist { this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time this.onToggle = onToggle; - this.start_timer; - this.abeLinkListener = listen('abelink-event', async (e) => { - const payload = e?.payload; - if (payload == null) { - return; - } - const { play, bpm, timestamp } = payload; - // (if bpm !== prev_bpm) { - //update the clock - // } - if (this.started !== play && play != null) { - if (play) { - this.start_timer = window.setTimeout(() => { - logger('[cyclist] start'); - this.clock.start(); - this.setStarted(true); - }, timestamp - Date.now()); - } else { - this.stop(); - } - } - - const { message, message_type } = e.payload; - }); this.latency = latency; // fixed trigger time offset const round = (x) => Math.round(x * 1000) / 1000; this.clock = createClock( @@ -84,18 +58,13 @@ export class Cyclist { this.started = v; this.onToggle?.(v); } - startClock() {} start() { if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } - - const linkmsg = { - bpm: 110, - play: true, - timestamp: Date.now(), - }; - Invoke('sendabelinkmsg', { linkmsg }); + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); } pause() { logger('[cyclist] pause'); @@ -107,12 +76,6 @@ export class Cyclist { this.clock.stop(); this.lastEnd = 0; this.setStarted(false); - const linkmsg = { - bpm: 110, - play: false, - timestamp: Date.now(), - }; - Invoke('sendabelinkmsg', { linkmsg }); } setPattern(pat, autostart = false) { this.pattern = pat; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index b6866b874..2dc62a728 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -4,6 +4,8 @@ import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register } from './pattern.mjs'; +import { isTauri } from '../../website/src/tauri.mjs'; +import { CyclistBridge } from '../desktopbridge/cyclistbridge.mjs'; export function repl({ interval, @@ -17,13 +19,16 @@ export function repl({ onToggle, editPattern, }) { - const scheduler = new Cyclist({ + const abelinkEnabled = isTauri(); + const cyclistParams = { interval, onTrigger: getTrigger({ defaultOutput, getTime }), onError: onSchedulerError, getTime, onToggle, - }); + }; + const scheduler = abelinkEnabled ? new CyclistBridge(cyclistParams) : new Cyclist(cyclistParams); + const setPattern = (pattern, autostart = true) => { pattern = editPattern?.(pattern) || pattern; scheduler.setPattern(pattern, autostart); diff --git a/packages/desktopbridge/cyclistbridge.mjs b/packages/desktopbridge/cyclistbridge.mjs new file mode 100644 index 000000000..9bcf32cd6 --- /dev/null +++ b/packages/desktopbridge/cyclistbridge.mjs @@ -0,0 +1,61 @@ +import { Cyclist } from '@strudel.cycles/core'; +import { logger } from '../core/logger.mjs'; +import { Invoke } from '../../website/src/tauri.mjs'; +import { listen } from '@tauri-apps/api/event'; + +export class CyclistBridge extends Cyclist { + constructor(params) { + super(params); + + this.start_timer; + this.abeLinkListener = listen('abelink-event', async (e) => { + const payload = e?.payload; + if (payload == null) { + return; + } + const { play, bpm, timestamp } = payload; + // (if bpm !== prev_bpm) { + //update the clock + // } + if (this.started !== play && play != null) { + if (play) { + this.start_timer = window.setTimeout(() => { + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); + }, timestamp - Date.now()); + } else { + this.stop(); + } + } + + const { message, message_type } = e.payload; + }); + } + + start() { + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } + + const linkmsg = { + bpm: 110, + play: true, + timestamp: Date.now(), + }; + Invoke('sendabelinkmsg', { linkmsg }); + } + + stop() { + logger('[cyclist] stop'); + this.clock.stop(); + this.lastEnd = 0; + this.setStarted(false); + const linkmsg = { + bpm: 110, + play: false, + timestamp: Date.now(), + }; + Invoke('sendabelinkmsg', { linkmsg }); + } +} diff --git a/packages/desktopbridge/index.mjs b/packages/desktopbridge/index.mjs index 591bbe34f..6190558b0 100644 --- a/packages/desktopbridge/index.mjs +++ b/packages/desktopbridge/index.mjs @@ -7,3 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th export * from './midibridge.mjs'; export * from './utils.mjs'; export * from './oscbridge.mjs'; +export * from './cyclistbridge.mjs'; diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 173bb4556..eb792664b 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -49,6 +49,7 @@ if (isTauri()) { import('@strudel/desktopbridge/loggerbridge.mjs'), import('@strudel/desktopbridge/midibridge.mjs'), import('@strudel/desktopbridge/oscbridge.mjs'), + import('@strudel/desktopbridge/cyclistbridge.mjs'), ]); } else { modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]); From d5c6cb85ea4a8bc45d00c6312c8a5f4906d25dad Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sat, 30 Sep 2023 22:18:31 -0400 Subject: [PATCH 09/19] use cps instead of bpm --- packages/desktopbridge/cyclistbridge.mjs | 14 ++++++------ src-tauri/src/ablelinkbridge.rs | 29 ++++++++++++++++-------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/desktopbridge/cyclistbridge.mjs b/packages/desktopbridge/cyclistbridge.mjs index 9bcf32cd6..c2017dd49 100644 --- a/packages/desktopbridge/cyclistbridge.mjs +++ b/packages/desktopbridge/cyclistbridge.mjs @@ -13,12 +13,12 @@ export class CyclistBridge extends Cyclist { if (payload == null) { return; } - const { play, bpm, timestamp } = payload; + const { started, cps, timestamp } = payload; // (if bpm !== prev_bpm) { //update the clock // } - if (this.started !== play && play != null) { - if (play) { + if (this.started !== started && started != null) { + if (started) { this.start_timer = window.setTimeout(() => { logger('[cyclist] start'); this.clock.start(); @@ -39,8 +39,8 @@ export class CyclistBridge extends Cyclist { } const linkmsg = { - bpm: 110, - play: true, + cps: 0.5, + started: true, timestamp: Date.now(), }; Invoke('sendabelinkmsg', { linkmsg }); @@ -52,8 +52,8 @@ export class CyclistBridge extends Cyclist { this.lastEnd = 0; this.setStarted(false); const linkmsg = { - bpm: 110, - play: false, + cps: 0.5, + started: false, timestamp: Date.now(), }; Invoke('sendabelinkmsg', { linkmsg }); diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs index d1bf7d652..79654bba7 100644 --- a/src-tauri/src/ablelinkbridge.rs +++ b/src-tauri/src/ablelinkbridge.rs @@ -11,8 +11,8 @@ use tauri::Window; #[derive(Deserialize, Clone, serde::Serialize)] pub struct LinkMsg { - pub play: bool, - pub bpm: f64, + pub started: bool, + pub cps: f64, pub timestamp: u64, } @@ -56,6 +56,16 @@ impl AbeLinkState { } } +fn bpm_to_cps(bpm: f64) -> f64 { + let cpm = bpm / 4.0; + return cpm / 60.0; +} + +fn cps_to_bpm(cps: f64) -> f64 { + let cpm = cps * 60.0; + return cpm * 4.0; +} + pub fn init(_logger: Logger, abelink_to_js: AbeLinkToJs, abelink: Arc>) { tauri::async_runtime::spawn(async move { /* ........................................................... @@ -74,6 +84,7 @@ pub fn init(_logger: Logger, abelink_to_js: AbeLinkToJs, abelink: Arc) -> Result<(), String> { - println!("bpm {} play {}", linkmsg.bpm, linkmsg.play); let mut abelink = state.abelink.lock().await; let started = abelink.session_state.is_playing(); let time_stamp = abelink.link.clock_micros(); let quantum = abelink.quantum; + let linkmsg_bpm = cps_to_bpm(linkmsg.cps); - if linkmsg.play != started { - abelink.session_state.set_is_playing_and_request_beat_at_time(linkmsg.play, time_stamp as u64, 0.0, quantum); + if linkmsg.started != started { + abelink.session_state.set_is_playing_and_request_beat_at_time(linkmsg.started, time_stamp as u64, 0.0, quantum); } - if linkmsg.bpm != abelink.session_state.tempo() { - abelink.session_state.set_tempo(linkmsg.bpm, time_stamp); + if linkmsg_bpm != abelink.session_state.tempo() { + abelink.session_state.set_tempo(linkmsg_bpm, time_stamp); } abelink.commit_app_state(); drop(abelink); From 1d85d60b11b8651d91e7144969d4a70633a5ec52 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 1 Oct 2023 00:46:40 -0400 Subject: [PATCH 10/19] cleaning up --- packages/desktopbridge/cyclistbridge.mjs | 26 +++-- packages/react/src/hooks/useStrudel.mjs | 23 +--- src-tauri/src/ablelinkbridge.rs | 143 ++++++++++++++--------- src-tauri/src/main.rs | 13 +-- 4 files changed, 117 insertions(+), 88 deletions(-) diff --git a/packages/desktopbridge/cyclistbridge.mjs b/packages/desktopbridge/cyclistbridge.mjs index c2017dd49..8fd6558f2 100644 --- a/packages/desktopbridge/cyclistbridge.mjs +++ b/packages/desktopbridge/cyclistbridge.mjs @@ -6,20 +6,31 @@ import { listen } from '@tauri-apps/api/event'; export class CyclistBridge extends Cyclist { constructor(params) { super(params); - this.start_timer; this.abeLinkListener = listen('abelink-event', async (e) => { const payload = e?.payload; if (payload == null) { return; } - const { started, cps, timestamp } = payload; - // (if bpm !== prev_bpm) { - //update the clock + const { started, cps, phase, timestamp } = payload; + + // TODO: I'm not sure how to hook this up this cps adjustment in Strudel + // (if cps !== clock_cps) { + // updateClock(cps) // } + + // TODO: I'm not sure how to hook this up this phase adjustment in Strudel + // a phase adjustment message is sent every 30 seconds from backend to keep clocks in sync + // if (Math.abs(phase - this.clock.getPhase()) > someDelta) { + // setCyclistPhase(phase) + // } + if (this.started !== started && started != null) { if (started) { + // when start message comes from abelink, delay starting cyclist clock until the start of the next abelink phase this.start_timer = window.setTimeout(() => { + // TODO: evaluate the code so if another source triggers the play there will not be an error + logger('[cyclist] start'); this.clock.start(); this.setStarted(true); @@ -28,8 +39,6 @@ export class CyclistBridge extends Cyclist { this.stop(); } } - - const { message, message_type } = e.payload; }); } @@ -37,11 +46,12 @@ export class CyclistBridge extends Cyclist { if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } - const linkmsg = { + // TODO: change this to value of "main" clock cps cps: 0.5, started: true, timestamp: Date.now(), + phase: this.clock.getPhase(), }; Invoke('sendabelinkmsg', { linkmsg }); } @@ -52,9 +62,11 @@ export class CyclistBridge extends Cyclist { this.lastEnd = 0; this.setStarted(false); const linkmsg = { + // TODO: change this to value of "main" clock cps cps: 0.5, started: false, timestamp: Date.now(), + phase: this.clock.getPhase(), }; Invoke('sendabelinkmsg', { linkmsg }); } diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 051b2a170..a10998e73 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -3,7 +3,7 @@ import { repl } from '@strudel.cycles/core'; import { transpiler } from '@strudel.cycles/transpiler'; import usePatternFrame from './usePatternFrame'; import usePostMessage from './usePostMessage.mjs'; -import { listen } from '@tauri-apps/api/event'; + function useStrudel({ defaultOutput, interval, @@ -67,7 +67,6 @@ function useStrudel({ }), [defaultOutput, interval, getTime], ); - const broadcast = usePostMessage(({ data: { from, type } }) => { if (type === 'start' && from !== id) { // console.log('message', from, type); @@ -127,26 +126,6 @@ function useStrudel({ await activateCode(); } }; - - // listen('abelink-event', async (e) => { - // const payload = e?.payload; - // if (payload == null) { - // return; - // } - // const { play, bpm, timestamp } = payload; - - // if (started !== play && play != null) { - // if (play) { - // // activateCode(); - // start(); - // } else { - // stop(); - // } - // } - - // const { message, message_type } = e.payload; - // }); - const error = schedulerError || evalError; usePatternFrame({ diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs index 79654bba7..8e31ba937 100644 --- a/src-tauri/src/ablelinkbridge.rs +++ b/src-tauri/src/ablelinkbridge.rs @@ -9,25 +9,30 @@ use crate::loggerbridge::Logger; use tauri::Window; +fn bpm_to_cps(bpm: f64) -> f64 { + let cpm = bpm / 4.0; + return cpm / 60.0; +} + +fn cps_to_bpm(cps: f64) -> f64 { + let cpm = cps * 60.0; + return cpm * 4.0; +} + +fn current_unix_time() -> Duration { + let current_unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + return current_unix_time; +} + #[derive(Deserialize, Clone, serde::Serialize)] pub struct LinkMsg { pub started: bool, pub cps: f64, pub timestamp: u64, + pub phase: f64, } -#[derive(Clone)] -pub struct AbeLinkToJs { - pub window: Arc, -} - -impl AbeLinkToJs { - pub fn send(&self, payload: LinkMsg) { - let _ = self.window.emit("abelink-event", payload); - } -} - -pub struct AsyncInputTransmit { +pub struct AbeLinkStateContainer { pub abelink: Arc>, } pub struct AbeLinkState { @@ -35,18 +40,38 @@ pub struct AbeLinkState { pub session_state: SessionState, pub running: bool, pub quantum: f64, + pub window: Arc, } impl AbeLinkState { - pub fn new() -> Self { + pub fn new(window: Arc) -> Self { Self { link: AblLink::new(120.0), session_state: SessionState::new(), running: true, quantum: 4.0, + window, } } + pub fn unix_time_at_next_phase(&self) -> u64 { + let link_time_stamp = self.link.clock_micros(); + let quantum = self.quantum; + let beat = self.session_state.beat_at_time(link_time_stamp, quantum); + let phase = self.session_state.phase_at_time(link_time_stamp, quantum); + let internal_time_at_next_phase = self.session_state.time_at_beat(beat + (quantum - phase), quantum); + let time_offset = Duration::from_micros((internal_time_at_next_phase - link_time_stamp) as u64); + let current_unix_time = current_unix_time(); + let unix_time_at_next_phase = (current_unix_time + time_offset).as_millis() - 140; + return unix_time_at_next_phase as u64; + } + + pub fn cps(&self) -> f64 { + let bpm = self.session_state.tempo(); + let cps = bpm_to_cps(bpm); + return cps; + } + pub fn capture_app_state(&mut self) { self.link.capture_app_session_state(&mut self.session_state); } @@ -54,27 +79,51 @@ impl AbeLinkState { pub fn commit_app_state(&mut self) { self.link.commit_app_session_state(&self.session_state); } -} -fn bpm_to_cps(bpm: f64) -> f64 { - let cpm = bpm / 4.0; - return cpm / 60.0; -} + pub fn send(&self, payload: LinkMsg) { + let _ = self.window.emit("abelink-event", payload); + } -fn cps_to_bpm(cps: f64) -> f64 { - let cpm = cps * 60.0; - return cpm * 4.0; -} + pub fn send_started(&self) { + let cps = self.cps(); + let started = self.session_state.is_playing(); + let payload = LinkMsg { + cps, + started, + timestamp: self.unix_time_at_next_phase(), + phase: 0.0, + }; + self.send(payload); + } -pub fn init(_logger: Logger, abelink_to_js: AbeLinkToJs, abelink: Arc>) { - tauri::async_runtime::spawn(async move { - /* ........................................................... - Initialize Ableton link - ............................................................*/ + pub fn send_cps(&self) { + let cps = self.cps(); + let started = self.session_state.is_playing(); + let phase = self.session_state.phase_at_time(self.link.clock_micros(), self.quantum); + let payload = LinkMsg { + cps, + started, + timestamp: current_unix_time().as_millis() as u64, + phase, + }; + self.send(payload); + } - let mut prev_is_playing = false; - let mut prev_bpm = 120.0; + pub fn send_phase(&self) { + self.send_started(); + } +} +pub fn init(_logger: Logger, abelink: Arc>) { + tauri::async_runtime::spawn(async move { + let mut prev_is_started = false; + let mut prev_cps = 0.5; + + let mut time_since_last_phase_send = 0; + let sleep_time = 10; + /* ....................................................................... + Evaluate Abelink State and send messages back to JS side when needed. + ........................................................................*/ loop { let mut state = abelink.lock().await; if state.link.is_enabled() == false { @@ -82,42 +131,32 @@ pub fn init(_logger: Logger, abelink_to_js: AbeLinkToJs, abelink: Arc 30000 { + state.send_phase(); + time_since_last_phase_send = 0; } drop(state); - sleep(Duration::from_millis(10)); + sleep(Duration::from_millis(sleep_time)); + time_since_last_phase_send = time_since_last_phase_send + sleep_time; } }); } // Called from JS #[tauri::command] -pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AsyncInputTransmit>) -> Result<(), String> { +pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AbeLinkStateContainer>) -> Result<(), String> { let mut abelink = state.abelink.lock().await; let started = abelink.session_state.is_playing(); let time_stamp = abelink.link.clock_micros(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6e3e47ac5..e4288b83b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,7 +8,6 @@ mod ablelinkbridge; use std::sync::Arc; use ablelinkbridge::AbeLinkState; -use ablelinkbridge::AbeLinkToJs; use loggerbridge::Logger; use tauri::Manager; use tokio::sync::mpsc; @@ -38,11 +37,6 @@ fn main() { let window = Arc::new(app.get_window("main").unwrap()); let logger = Logger { window: window.clone() }; - let abelink = Arc::new(Mutex::new(AbeLinkState::new())); - app.manage(ablelinkbridge::AsyncInputTransmit { - abelink: abelink.clone(), - }); - midibridge::init( logger.clone(), async_input_receiver_midi, @@ -56,7 +50,12 @@ fn main() { async_output_transmitter_osc ); - ablelinkbridge::init(logger.clone(), AbeLinkToJs { window }, abelink); + // This state must be declared in the setup so it can be shared between invoked commands and the initialized function + let abelink = Arc::new(Mutex::new(AbeLinkState::new(window))); + app.manage(ablelinkbridge::AbeLinkStateContainer { + abelink: abelink.clone(), + }); + ablelinkbridge::init(logger.clone(), abelink); Ok(()) }) From 2127a8f6422f62828035b96f4db55ba58dd4b39e Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 1 Oct 2023 19:19:49 -0400 Subject: [PATCH 11/19] cleaning things up --- packages/desktopbridge/cyclistbridge.mjs | 10 ++++++++-- src-tauri/src/ablelinkbridge.rs | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/desktopbridge/cyclistbridge.mjs b/packages/desktopbridge/cyclistbridge.mjs index 8fd6558f2..288fab435 100644 --- a/packages/desktopbridge/cyclistbridge.mjs +++ b/packages/desktopbridge/cyclistbridge.mjs @@ -27,6 +27,9 @@ export class CyclistBridge extends Cyclist { if (this.started !== started && started != null) { if (started) { + // the time delay in ms that seems to occur when starting the clock. Unsure if this is standard across all clients + const evaluationTime = 140; + // when start message comes from abelink, delay starting cyclist clock until the start of the next abelink phase this.start_timer = window.setTimeout(() => { // TODO: evaluate the code so if another source triggers the play there will not be an error @@ -34,7 +37,7 @@ export class CyclistBridge extends Cyclist { logger('[cyclist] start'); this.clock.start(); this.setStarted(true); - }, timestamp - Date.now()); + }, timestamp - Date.now() - evaluationTime); } else { this.stop(); } @@ -46,6 +49,7 @@ export class CyclistBridge extends Cyclist { if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } + let x = Date.now(); const linkmsg = { // TODO: change this to value of "main" clock cps cps: 0.5, @@ -53,7 +57,9 @@ export class CyclistBridge extends Cyclist { timestamp: Date.now(), phase: this.clock.getPhase(), }; - Invoke('sendabelinkmsg', { linkmsg }); + Invoke('sendabelinkmsg', { linkmsg }).then(() => { + logger(`${Date.now() - x}`); + }); } stop() { diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs index 8e31ba937..18809b176 100644 --- a/src-tauri/src/ablelinkbridge.rs +++ b/src-tauri/src/ablelinkbridge.rs @@ -62,7 +62,7 @@ impl AbeLinkState { let internal_time_at_next_phase = self.session_state.time_at_beat(beat + (quantum - phase), quantum); let time_offset = Duration::from_micros((internal_time_at_next_phase - link_time_stamp) as u64); let current_unix_time = current_unix_time(); - let unix_time_at_next_phase = (current_unix_time + time_offset).as_millis() - 140; + let unix_time_at_next_phase = (current_unix_time + time_offset).as_millis(); return unix_time_at_next_phase as u64; } @@ -117,7 +117,7 @@ impl AbeLinkState { pub fn init(_logger: Logger, abelink: Arc>) { tauri::async_runtime::spawn(async move { let mut prev_is_started = false; - let mut prev_cps = 0.5; + let mut prev_cps = 0.0; let mut time_since_last_phase_send = 0; let sleep_time = 10; @@ -126,6 +126,7 @@ pub fn init(_logger: Logger, abelink: Arc>) { ........................................................................*/ loop { let mut state = abelink.lock().await; + state.capture_app_state(); if state.link.is_enabled() == false { state.link.enable(true); state.link.enable_start_stop_sync(true); @@ -133,8 +134,6 @@ pub fn init(_logger: Logger, abelink: Arc>) { let started = state.session_state.is_playing(); - state.capture_app_state(); - if started != prev_is_started { state.send_started(); prev_is_started = started; @@ -158,6 +157,7 @@ pub fn init(_logger: Logger, abelink: Arc>) { #[tauri::command] pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AbeLinkStateContainer>) -> Result<(), String> { let mut abelink = state.abelink.lock().await; + abelink.capture_app_state(); let started = abelink.session_state.is_playing(); let time_stamp = abelink.link.clock_micros(); let quantum = abelink.quantum; From 5db39b8e9e5084d57ab10a4683c17c92dc153da4 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 1 Oct 2023 19:45:44 -0400 Subject: [PATCH 12/19] fix clock being set to 0 --- packages/desktopbridge/cyclistbridge.mjs | 8 +++++--- src-tauri/src/ablelinkbridge.rs | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/desktopbridge/cyclistbridge.mjs b/packages/desktopbridge/cyclistbridge.mjs index 288fab435..3e8a1e222 100644 --- a/packages/desktopbridge/cyclistbridge.mjs +++ b/packages/desktopbridge/cyclistbridge.mjs @@ -36,7 +36,9 @@ export class CyclistBridge extends Cyclist { logger('[cyclist] start'); this.clock.start(); + this.setStarted(true); + console.log('clock duration: ', this.clock.interval); }, timestamp - Date.now() - evaluationTime); } else { this.stop(); @@ -52,13 +54,13 @@ export class CyclistBridge extends Cyclist { let x = Date.now(); const linkmsg = { // TODO: change this to value of "main" clock cps - cps: 0.5, + cps: 0, started: true, timestamp: Date.now(), phase: this.clock.getPhase(), }; Invoke('sendabelinkmsg', { linkmsg }).then(() => { - logger(`${Date.now() - x}`); + logger(`invoke delay: ${Date.now() - x}`); }); } @@ -69,7 +71,7 @@ export class CyclistBridge extends Cyclist { this.setStarted(false); const linkmsg = { // TODO: change this to value of "main" clock cps - cps: 0.5, + cps: 0, started: false, timestamp: Date.now(), phase: this.clock.getPhase(), diff --git a/src-tauri/src/ablelinkbridge.rs b/src-tauri/src/ablelinkbridge.rs index 18809b176..41f409042 100644 --- a/src-tauri/src/ablelinkbridge.rs +++ b/src-tauri/src/ablelinkbridge.rs @@ -137,7 +137,7 @@ pub fn init(_logger: Logger, abelink: Arc>) { if started != prev_is_started { state.send_started(); prev_is_started = started; - } else if state.cps() != prev_cps { + } else if state.cps() != prev_cps && state.cps() != 0.0 { state.send_cps(); prev_cps = state.cps(); // a phase sync message needs to be sent to strudel every 30 seconds to keep clock drift at bay @@ -166,7 +166,7 @@ pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AbeLinkSta if linkmsg.started != started { abelink.session_state.set_is_playing_and_request_beat_at_time(linkmsg.started, time_stamp as u64, 0.0, quantum); } - if linkmsg_bpm != abelink.session_state.tempo() { + if linkmsg_bpm != abelink.session_state.tempo() && linkmsg_bpm != 0.0 { abelink.session_state.set_tempo(linkmsg_bpm, time_stamp); } abelink.commit_app_state(); From 0efcf84c46c15e00032663dc2c5bb652a4110cbb Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 1 Oct 2023 20:30:47 -0400 Subject: [PATCH 13/19] cleaning up --- packages/core/repl.mjs | 1 + packages/desktopbridge/cyclistbridge.mjs | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 2dc62a728..83d7d409a 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -19,6 +19,7 @@ export function repl({ onToggle, editPattern, }) { + //TODO: could maybe be enabled with a command or a switch in settings? const abelinkEnabled = isTauri(); const cyclistParams = { interval, diff --git a/packages/desktopbridge/cyclistbridge.mjs b/packages/desktopbridge/cyclistbridge.mjs index 3e8a1e222..b4071403a 100644 --- a/packages/desktopbridge/cyclistbridge.mjs +++ b/packages/desktopbridge/cyclistbridge.mjs @@ -36,9 +36,7 @@ export class CyclistBridge extends Cyclist { logger('[cyclist] start'); this.clock.start(); - this.setStarted(true); - console.log('clock duration: ', this.clock.interval); }, timestamp - Date.now() - evaluationTime); } else { this.stop(); @@ -59,9 +57,7 @@ export class CyclistBridge extends Cyclist { timestamp: Date.now(), phase: this.clock.getPhase(), }; - Invoke('sendabelinkmsg', { linkmsg }).then(() => { - logger(`invoke delay: ${Date.now() - x}`); - }); + Invoke('sendabelinkmsg', { linkmsg }); } stop() { From f089037f1822ee29d80c610ff70cf800840a331b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 2 Oct 2023 08:35:41 +0200 Subject: [PATCH 14/19] add cmake instructions --- src-tauri/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/README.md b/src-tauri/README.md index 4045e1904..ed7bee4de 100644 --- a/src-tauri/README.md +++ b/src-tauri/README.md @@ -4,7 +4,8 @@ Rust source files for building native desktop apps using Tauri ## Usage -Install [Rust](https://rustup.rs/) on your system. +- Install [Rust](https://rustup.rs/) on your system. +- Install `cmake` on your system. OSX: `brew install cmake`, Linux: `sudo apt-get install cmake` From the project root: From 25be8fca0dcb17806892bdd51f56e33917a1f29c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 2 Oct 2023 20:56:45 +0200 Subject: [PATCH 15/19] encapsulate cyclist --- packages/core/repl.mjs | 4 ++-- packages/react/src/hooks/useStrudel.mjs | 2 ++ website/src/repl/Repl.jsx | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 83d7d409a..56a26bb52 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -5,7 +5,6 @@ import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register } from './pattern.mjs'; import { isTauri } from '../../website/src/tauri.mjs'; -import { CyclistBridge } from '../desktopbridge/cyclistbridge.mjs'; export function repl({ interval, @@ -18,6 +17,7 @@ export function repl({ transpiler, onToggle, editPattern, + createCyclist, }) { //TODO: could maybe be enabled with a command or a switch in settings? const abelinkEnabled = isTauri(); @@ -28,7 +28,7 @@ export function repl({ getTime, onToggle, }; - const scheduler = abelinkEnabled ? new CyclistBridge(cyclistParams) : new Cyclist(cyclistParams); + const scheduler = createCyclist?.(cyclistParams) || new Cyclist(p); const setPattern = (pattern, autostart = true) => { pattern = editPattern?.(pattern) || pattern; diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index a10998e73..cb154d80b 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -19,6 +19,7 @@ function useStrudel({ drawContext, drawTime = [-2, 2], paintOptions = {}, + createCyclist, // (params) => Cyclist }) { const id = useMemo(() => s4(), []); canvasId = canvasId || `canvas-${id}`; @@ -45,6 +46,7 @@ function useStrudel({ onEvalError?.(err); }, getTime, + createCyclist, drawContext, transpiler, editPattern, diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 456f53c60..04f5002c2 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core'; +import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger, Cyclist } from '@strudel.cycles/core'; import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react'; import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio'; import { createClient } from '@supabase/supabase-js'; @@ -23,6 +23,7 @@ import { settingPatterns } from '../settings.mjs'; import { code2hash, hash2code } from './helpers.mjs'; import { isTauri } from '../tauri.mjs'; import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs'; +import { CyclistBridge } from '@strudel/desktopbridge/cyclistbridge.mjs'; const { latestCode } = settingsMap.get(); @@ -164,6 +165,7 @@ export function Repl({ embedded = false }) { drawContext, // drawTime: [0, 6], paintOptions, + createCyclist: (p) => (isTauri() ? new CyclistBridge(p) : new Cyclist(p)), }); // init code From f2534310e85025d39918c59fc8a76e4133546386 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 2 Oct 2023 20:57:25 +0200 Subject: [PATCH 16/19] fix: default cyclist --- packages/core/repl.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 56a26bb52..837eb9b23 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -28,7 +28,7 @@ export function repl({ getTime, onToggle, }; - const scheduler = createCyclist?.(cyclistParams) || new Cyclist(p); + const scheduler = createCyclist?.(cyclistParams) || new Cyclist(cyclistParams); const setPattern = (pattern, autostart = true) => { pattern = editPattern?.(pattern) || pattern; From b1fcf6ca34e49f32d0fafefc3a753cd93ed5c3c9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 2 Oct 2023 20:59:10 +0200 Subject: [PATCH 17/19] cleanup --- packages/core/repl.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 837eb9b23..af787727c 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -4,7 +4,6 @@ import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register } from './pattern.mjs'; -import { isTauri } from '../../website/src/tauri.mjs'; export function repl({ interval, @@ -20,7 +19,6 @@ export function repl({ createCyclist, }) { //TODO: could maybe be enabled with a command or a switch in settings? - const abelinkEnabled = isTauri(); const cyclistParams = { interval, onTrigger: getTrigger({ defaultOutput, getTime }), From 30a0059ef31605fe66001394afae8861611f0e8d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 2 Oct 2023 20:59:35 +0200 Subject: [PATCH 18/19] remove todo --- packages/core/repl.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index af787727c..75d2ea950 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -18,7 +18,6 @@ export function repl({ editPattern, createCyclist, }) { - //TODO: could maybe be enabled with a command or a switch in settings? const cyclistParams = { interval, onTrigger: getTrigger({ defaultOutput, getTime }), From 3a69a2561cb7d3ee0d3578ffb598993744d5a8ea Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 2 Oct 2023 21:21:11 +0200 Subject: [PATCH 19/19] half working cps link --- packages/core/zyklus.mjs | 3 ++- packages/desktopbridge/cyclistbridge.mjs | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs index 3d25b0544..6d6610898 100644 --- a/packages/core/zyklus.mjs +++ b/packages/core/zyklus.mjs @@ -43,7 +43,8 @@ function createClock( clear(); }; const getPhase = () => phase; + const setPhase = (_phase) => (phase = _phase); // setCallback - return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; + return { setDuration, start, stop, pause, duration, interval, getPhase, setPhase, minLatency }; } export default createClock; diff --git a/packages/desktopbridge/cyclistbridge.mjs b/packages/desktopbridge/cyclistbridge.mjs index b4071403a..bb6acf24c 100644 --- a/packages/desktopbridge/cyclistbridge.mjs +++ b/packages/desktopbridge/cyclistbridge.mjs @@ -14,16 +14,18 @@ export class CyclistBridge extends Cyclist { } const { started, cps, phase, timestamp } = payload; - // TODO: I'm not sure how to hook this up this cps adjustment in Strudel - // (if cps !== clock_cps) { - // updateClock(cps) - // } + if (cps !== this.cps) { + this.setCps(cps); + } // TODO: I'm not sure how to hook this up this phase adjustment in Strudel // a phase adjustment message is sent every 30 seconds from backend to keep clocks in sync - // if (Math.abs(phase - this.clock.getPhase()) > someDelta) { - // setCyclistPhase(phase) - // } + const phaseDiff = Math.abs(phase - this.clock.getPhase()); + if (phaseDiff > 0.1) { + console.log('set phase from', this.clock.getPhase(), 'to', phase); + // hmmm this seems wrong... + //this.clock.setPhase(phase); + } if (this.started !== started && started != null) { if (started) { @@ -49,10 +51,8 @@ export class CyclistBridge extends Cyclist { if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } - let x = Date.now(); const linkmsg = { - // TODO: change this to value of "main" clock cps - cps: 0, + cps: this.cps, started: true, timestamp: Date.now(), phase: this.clock.getPhase(),