mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
Merge pull request #694 from daslyfe/tauri_osc_support
Direct OSC Support in Tauri
This commit is contained in:
commit
133a1d2e8f
@ -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
packages/desktopbridge/oscbridge.mjs
Normal file
43
packages/desktopbridge/oscbridge.mjs
Normal file
@ -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 });
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -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": {
|
||||
|
||||
27
src-tauri/Cargo.lock
generated
27
src-tauri/Cargo.lock
generated
@ -98,6 +98,7 @@ name = "app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"midir",
|
||||
"rosc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@ -1557,6 +1558,12 @@ dependencies = [
|
||||
"windows 0.43.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
@ -1629,6 +1636,16 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@ -2173,6 +2190,16 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||
|
||||
[[package]]
|
||||
name = "rosc"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2e63d9e6b0d090be1485cf159b1e04c3973d2d3e1614963544ea2ff47a4a981"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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!())
|
||||
|
||||
150
src-tauri/src/oscbridge.rs
Normal file
150
src-tauri/src/oscbridge.rs
Normal file
@ -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())
|
||||
}
|
||||
@ -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'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user