Compare commits

...

21 Commits

Author SHA1 Message Date
Jade Rowland 1bae027aaa Merge branch 'tidalcycles:main' into ableton_link_integration 2023-10-05 23:07:41 -04:00
Felix Roos 3a69a2561c half working cps link 2023-10-02 21:21:11 +02:00
Felix Roos 30a0059ef3 remove todo 2023-10-02 20:59:35 +02:00
Felix Roos b1fcf6ca34 cleanup 2023-10-02 20:59:10 +02:00
Felix Roos f2534310e8 fix: default cyclist 2023-10-02 20:57:25 +02:00
Felix Roos 25be8fca0d encapsulate cyclist 2023-10-02 20:56:45 +02:00
Felix Roos f089037f18 add cmake instructions 2023-10-02 08:35:41 +02:00
Jade Rowland e818ca9f03 Merge branch 'tidalcycles:main' into ableton_link_integration 2023-10-01 20:44:09 -04:00
Jade Rowland 0efcf84c46 cleaning up 2023-10-01 20:30:47 -04:00
Jade Rowland 5db39b8e9e fix clock being set to 0 2023-10-01 19:45:44 -04:00
Jade Rowland 2127a8f642 cleaning things up 2023-10-01 19:19:49 -04:00
Jade Rowland 1d85d60b11 cleaning up 2023-10-01 00:46:40 -04:00
Jade Rowland d5c6cb85ea use cps instead of bpm 2023-09-30 22:18:31 -04:00
Jade Rowland fe8c4e2bcd cyclist bridge 2023-09-29 23:27:43 -04:00
Jade Rowland 6eda7af14b play sync now accurate 2023-09-29 01:01:01 -04:00
Jade Rowland 6719c6127b figuring out backend integration 2023-09-27 15:28:23 -04:00
Jade Rowland 172006a3b3 improving listener 2023-09-25 22:32:31 -04:00
Jade Rowland 209122b8f7 start offset 2023-09-25 19:29:14 -04:00
Jade Rowland d7fe491c84 play stop working 2023-09-22 14:27:03 -04:00
Jade Rowland db36636ab2 play working kinda 2023-09-21 23:08:30 -04:00
Jade Rowland 789cad3e97 figuring things out 2023-09-21 22:57:05 -04:00
11 changed files with 402 additions and 8 deletions
+5 -2
View File
@@ -16,14 +16,17 @@ export function repl({
transpiler,
onToggle,
editPattern,
createCyclist,
}) {
const scheduler = new Cyclist({
const cyclistParams = {
interval,
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
});
};
const scheduler = createCyclist?.(cyclistParams) || new Cyclist(cyclistParams);
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
+2 -1
View File
@@ -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;
+77
View File
@@ -0,0 +1,77 @@
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 { started, cps, phase, timestamp } = payload;
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
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) {
// 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
logger('[cyclist] start');
this.clock.start();
this.setStarted(true);
}, timestamp - Date.now() - evaluationTime);
} else {
this.stop();
}
}
});
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
const linkmsg = {
cps: this.cps,
started: true,
timestamp: Date.now(),
phase: this.clock.getPhase(),
};
Invoke('sendabelinkmsg', { linkmsg });
}
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
const linkmsg = {
// TODO: change this to value of "main" clock cps
cps: 0,
started: false,
timestamp: Date.now(),
phase: this.clock.getPhase(),
};
Invoke('sendabelinkmsg', { linkmsg });
}
}
+1
View File
@@ -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';
+2
View File
@@ -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,
+113
View File
@@ -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"
+1
View File
@@ -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.
+2 -1
View File
@@ -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:
+175
View File
@@ -0,0 +1,175 @@
use std::time::{ Duration, SystemTime, UNIX_EPOCH };
use rusty_link::{ AblLink, SessionState };
use std::sync::Arc;
use tokio::sync::Mutex;
use serde::Deserialize;
use std::thread::sleep;
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,
}
pub struct AbeLinkStateContainer {
pub abelink: Arc<Mutex<AbeLinkState>>,
}
pub struct AbeLinkState {
pub link: AblLink,
pub session_state: SessionState,
pub running: bool,
pub quantum: f64,
pub window: Arc<Window>,
}
impl AbeLinkState {
pub fn new(window: Arc<Window>) -> 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();
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);
}
pub fn commit_app_state(&mut self) {
self.link.commit_app_session_state(&self.session_state);
}
pub fn send(&self, payload: LinkMsg) {
let _ = self.window.emit("abelink-event", payload);
}
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 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);
}
pub fn send_phase(&self) {
self.send_started();
}
}
pub fn init(_logger: Logger, abelink: Arc<Mutex<AbeLinkState>>) {
tauri::async_runtime::spawn(async move {
let mut prev_is_started = false;
let mut prev_cps = 0.0;
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;
state.capture_app_state();
if state.link.is_enabled() == false {
state.link.enable(true);
state.link.enable_start_stop_sync(true);
}
let started = state.session_state.is_playing();
if started != prev_is_started {
state.send_started();
prev_is_started = started;
} 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
} else if time_since_last_phase_send > 30000 {
state.send_phase();
time_since_last_phase_send = 0;
}
drop(state);
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<'_, 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;
let linkmsg_bpm = cps_to_bpm(linkmsg.cps);
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() && linkmsg_bpm != 0.0 {
abelink.session_state.set_tempo(linkmsg_bpm, time_stamp);
}
abelink.commit_app_state();
drop(abelink);
Ok(())
}
+20 -3
View File
@@ -4,8 +4,10 @@
mod midibridge;
mod oscbridge;
mod loggerbridge;
mod ablelinkbridge;
use std::sync::Arc;
use ablelinkbridge::AbeLinkState;
use loggerbridge::Logger;
use tauri::Manager;
use tokio::sync::mpsc;
@@ -21,6 +23,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);
tauri::Builder
::default()
.manage(midibridge::AsyncInputTransmit {
@@ -29,17 +32,31 @@ fn main() {
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc])
.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 };
let logger = Logger { window: window.clone() };
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
);
// 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(())
})
.run(tauri::generate_context!())
+4 -1
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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();
@@ -51,6 +52,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 = modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
@@ -163,6 +165,7 @@ export function Repl({ embedded = false }) {
drawContext,
// drawTime: [0, 6],
paintOptions,
createCyclist: (p) => (isTauri() ? new CyclistBridge(p) : new Cyclist(p)),
});
// init code