Skip to content

Commit

Permalink
Merge pull request #694 from daslyfe/tauri_osc_support
Browse files Browse the repository at this point in the history
Direct OSC Support in Tauri
  • Loading branch information
felixroos committed Sep 4, 2023
2 parents abaeb52 + 15c15a9 commit 133a1d2
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/desktopbridge/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,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';
43 changes: 43 additions & 0 deletions packages/desktopbridge/oscbridge.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { parseNumeral, Pattern } from '@strudel.cycles/core';
import { Invoke } from './utils.mjs';

Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n));
controls.note && (controls.note = parseNumeral(controls.note));

const params = [];

const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);

Object.keys(controls).forEach((key) => {
const val = controls[key];
const value = typeof val === 'number' ? val.toString() : val;

if (value == null) {
return;
}
params.push({
name: key,
value,
valueisnumber: typeof val === 'number',
});
});

const messagesfromjs = [];
if (params.length) {
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
}

if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendosc', { messagesfromjs });
});
}
});
};
2 changes: 1 addition & 1 deletion packages/desktopbridge/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@strudel/desktopbridge",
"version": "0.1.0",
"description": "send midi messages between the JS and Tauri (Rust) sides of the Studel desktop app",
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
"main": "index.mjs",
"type": "module",
"repository": {
Expand Down
27 changes: 27 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.0", features = ["fs-all"] }
midir = "0.9.1"
tokio = { version = "1.29.0", features = ["full"] }
rosc = "0.10.1"

[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
Expand Down
17 changes: 12 additions & 5 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod midibridge;
mod oscbridge;
use tokio::sync::mpsc;
use tokio::sync::Mutex;

fn main() {
let (async_input_transmitter, async_input_receiver) = mpsc::channel(1);
let (async_output_transmitter, async_output_receiver) = mpsc::channel(1);
let (async_input_transmitter_midi, async_input_receiver_midi) = mpsc::channel(1);
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 {
inner: Mutex::new(async_input_transmitter),
inner: Mutex::new(async_input_transmitter_midi),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi])
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc])
.setup(|_app| {
midibridge::init(async_input_receiver, async_output_receiver, async_output_transmitter);
midibridge::init(async_input_receiver_midi, async_output_receiver_midi, async_output_transmitter_midi);
oscbridge::init(async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
Ok(())
})
.run(tauri::generate_context!())
Expand Down
150 changes: 150 additions & 0 deletions src-tauri/src/oscbridge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use rosc::{ encoder, OscTime };
use rosc::{ OscMessage, OscPacket, OscType, OscBundle };
use std::net::UdpSocket;

use std::time::Duration;
use std::sync::Arc;
use tokio::sync::{ mpsc, Mutex };
use serde::Deserialize;
use std::thread::sleep;
pub struct OscMsg {
pub msg_buf: Vec<u8>,
pub timestamp: u64,
}

pub struct AsyncInputTransmit {
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
}

const UNIX_OFFSET: u64 = 2_208_988_800; // 70 years in seconds
const TWO_POW_32: f64 = (u32::MAX as f64) + 1.0; // Number of bits in a `u32`
const NANOS_PER_SECOND: f64 = 1.0e9;
const SECONDS_PER_NANO: f64 = 1.0 / NANOS_PER_SECOND;

pub fn init(
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>
) {
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let message_queue: Arc<Mutex<Vec<OscMsg>>> = 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(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});

let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Open OSC Ports
............................................................*/
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
let to_addr = String::from("127.0.0.1:57120");
sock.set_nonblocking(true).unwrap();
sock.connect(to_addr).expect("could not connect to OSC address");

/* ...........................................................
Process queued messages
............................................................*/

loop {
let mut message_queue = message_queue_clone.lock().await;

message_queue.retain(|message| {
let result = sock.send(&message.msg_buf);
if result.is_err() {
println!("OSC Message failed to send, the server might no longer be available");
}
return false;
});

sleep(Duration::from_millis(1));
}
});
}

pub async fn async_process_model(
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
output_transmitter: mpsc::Sender<Vec<OscMsg>>
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}

#[derive(Deserialize)]
pub struct Param {
name: String,
value: String,
valueisnumber: bool,
}
#[derive(Deserialize)]
pub struct MessageFromJS {
params: Vec<Param>,
timestamp: u64,
target: String,
}
// Called from JS
#[tauri::command]
pub async fn sendosc(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<OscMsg> = Vec::new();
for m in messagesfromjs {
let mut args = Vec::new();
for p in m.params {
args.push(OscType::String(p.name));
if p.valueisnumber {
args.push(OscType::Float(p.value.parse().unwrap()));
} else {
args.push(OscType::String(p.value));
}
}

let duration_since_epoch = Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);

let seconds = u32
::try_from(duration_since_epoch.as_secs())
.map_err(|_| "bit conversion failed for osc message timetag")?;

let nanos = duration_since_epoch.subsec_nanos() as f64;
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;

let timetag = OscTime::from((seconds, fractional));

let packet = OscPacket::Message(OscMessage {
addr: m.target,
args,
});

let bundle = OscBundle {
content: vec![packet],
timetag,
};

let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();

let message_to_process = OscMsg {
msg_buf,
timestamp: m.timestamp,
};
messages_to_process.push(message_to_process);
}

async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}
4 changes: 2 additions & 2 deletions website/src/repl/Repl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ const modules = [
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
isTauri() ? import('@strudel/desktopbridge') : import('@strudel.cycles/midi'),
isTauri() ? import('@strudel/desktopbridge/midibridge.mjs') : import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
isTauri() ? import('@strudel/desktopbridge/oscbridge.mjs') : import('@strudel.cycles/osc'),
import('@strudel.cycles/serial'),
import('@strudel.cycles/soundfonts'),
import('@strudel.cycles/csound'),
Expand Down

0 comments on commit 133a1d2

Please sign in to comment.