diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 03f8b4104..798870f03 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -91,6 +91,9 @@ export const midi2note = (n) => { // modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does) export const _mod = (n, m) => ((n % m) + m) % m; +// average numbers in an array +export const averageArray = (arr) => arr.reduce((a, b) => a + b) / arr.length; + export function nanFallback(value, fallback = 0) { if (isNaN(Number(value))) { logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning'); diff --git a/packages/desktopbridge/oscbridge.mjs b/packages/desktopbridge/oscbridge.mjs index 81366367b..42a9a6d4a 100644 --- a/packages/desktopbridge/oscbridge.mjs +++ b/packages/desktopbridge/oscbridge.mjs @@ -1,6 +1,10 @@ -import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core'; +import { parseNumeral, Pattern, averageArray } from '@strudel/core'; import { Invoke } from './utils.mjs'; +let offsetTime; +let timeAtPrevOffsetSample; +let prevOffsetTimes = []; + Pattern.prototype.osc = function () { return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => { hap.ensureObjectValue(); @@ -13,7 +17,27 @@ Pattern.prototype.osc = function () { const params = []; - const timestamp = Math.round(Date.now() + getEventOffsetMs(targetTime, currentTime)); + const unixTimeSecs = Date.now() / 1000; + const newOffsetTime = unixTimeSecs - currentTime; + if (offsetTime == null) { + offsetTime = newOffsetTime; + } + prevOffsetTimes.push(newOffsetTime); + if (prevOffsetTimes.length > 8) { + prevOffsetTimes.shift(); + } + // every two seconds, the average of the previous 8 offset times is calculated and used as a stable reference + // for calculating the timestamp that will be sent to the backend + if (timeAtPrevOffsetSample == null || unixTimeSecs - timeAtPrevOffsetSample > 2) { + timeAtPrevOffsetSample = unixTimeSecs; + const rollingOffsetTime = averageArray(prevOffsetTimes); + //account for the js clock freezing or resets set the new offset + if (Math.abs(rollingOffsetTime - offsetTime) > 0.01) { + offsetTime = rollingOffsetTime; + } + } + + const timestamp = offsetTime + targetTime; Object.keys(controls).forEach((key) => { const val = controls[key]; @@ -29,15 +53,12 @@ Pattern.prototype.osc = function () { }); }); - const messagesfromjs = []; - if (params.length) { - messagesfromjs.push({ target: '/dirt/play', timestamp, params }); - } - - if (messagesfromjs.length) { - setTimeout(() => { - Invoke('sendosc', { messagesfromjs }); - }); + if (params.length === 0) { + return; } + const message = { target: '/dirt/play', timestamp, params }; + setTimeout(() => { + Invoke('sendosc', { messagesfromjs: [message] }); + }); }); }; diff --git a/src-tauri/src/oscbridge.rs b/src-tauri/src/oscbridge.rs index c8dcf03af..f6ac84a73 100644 --- a/src-tauri/src/oscbridge.rs +++ b/src-tauri/src/oscbridge.rs @@ -6,13 +6,13 @@ use std::net::UdpSocket; use serde::Deserialize; use std::sync::Arc; use std::thread::sleep; -use std::time::Duration; +use std::time::{Duration}; use tokio::sync::{mpsc, Mutex}; use crate::loggerbridge::Logger; pub struct OscMsg { pub msg_buf: Vec, - pub timestamp: u64, + pub timestamp: f64, } pub struct AsyncInputTransmit { @@ -106,7 +106,7 @@ pub struct Param { #[derive(Deserialize)] pub struct MessageFromJS { params: Vec, - timestamp: u64, + timestamp: f64, target: String, } // Called from JS @@ -127,9 +127,11 @@ pub async fn sendosc( args.push(OscType::String(p.value)); } } + // let start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let time_delay = Duration::from_secs_f64(m.timestamp); let duration_since_epoch = - Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0); + time_delay + Duration::new(UNIX_OFFSET, 0); let seconds = u32::try_from(duration_since_epoch.as_secs()) .map_err(|_| "bit conversion failed for osc message timetag")?; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 41aa8d84e..11b27c7a6 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -5234,6 +5234,25 @@ exports[`runs examples > example "pickF" example index 1 1`] = ` ] `; +exports[`runs examples > example "pickmodRestart" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:C3 s:piano ]", + "[ 1/4 → 1/2 | note:D3 s:piano ]", + "[ 1/2 → 3/4 | note:E3 s:piano ]", + "[ 3/4 → 1/1 | note:C3 s:piano ]", + "[ 1/1 → 5/4 | note:C3 s:piano ]", + "[ 5/4 → 3/2 | note:D3 s:piano ]", + "[ 3/2 → 7/4 | note:E3 s:piano ]", + "[ 7/4 → 2/1 | note:C3 s:piano ]", + "[ 2/1 → 9/4 | note:E3 s:piano ]", + "[ 9/4 → 5/2 | note:F3 s:piano ]", + "[ 5/2 → 11/4 | note:G3 s:piano ]", + "[ 3/1 → 13/4 | note:E3 s:piano ]", + "[ 13/4 → 7/2 | note:F3 s:piano ]", + "[ 7/2 → 15/4 | note:G3 s:piano ]", +] +`; + exports[`runs examples > example "pitchwheel" example index 0 1`] = ` [ "[ 0/1 → 1/13 | note:C3 s:sawtooth cutoff:500 ]",