per track volume

This commit is contained in:
2025-06-21 11:00:04 +02:00
parent 70b2f92a8d
commit 7b7ebb8c0e
29 changed files with 4989 additions and 309 deletions

View File

@@ -0,0 +1,58 @@
use crate::*;
#[derive(Debug)]
pub struct OscController {
sender: kanal::Sender<osc::Message>,
}
impl OscController {
pub(crate) fn new(sender: kanal::Sender<osc::Message>) -> Self {
Self { sender }
}
pub fn track_state_changed(
&self,
column: usize,
row: usize,
state: TrackState,
) -> Result<()> {
let message = osc::Message::TrackStateChanged { column, row, state: state.into() };
match self.sender.try_send(message) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
}
}
pub fn track_volume_changed(
&self,
column: usize,
row: usize,
volume: f32,
) -> Result<()> {
let message = osc::Message::TrackVolumeChanged { column, row, volume };
match self.sender.try_send(message) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
}
}
pub fn selected_column_changed(&self, column: usize) -> Result<()> {
let message = osc::Message::SelectedColumnChanged { column };
match self.sender.try_send(message) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
}
}
pub fn selected_row_changed(&self, row: usize) -> Result<()> {
let message = osc::Message::SelectedRowChanged { row };
match self.sender.try_send(message) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
}
}
}

View File

@@ -0,0 +1,7 @@
mod controller;
mod platform;
mod server;
// Re-export public interface - same as before
pub use controller::OscController;
pub use server::Osc;

View File

@@ -0,0 +1,23 @@
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
#[cfg(unix)]
pub(crate) use unix::{PlatformListener, PlatformStream};
#[cfg(windows)]
pub(crate) use windows::{PlatformListener, PlatformStream};
use crate::Result;
pub(crate) async fn create_listener(socket_path: &str) -> Result<PlatformListener> {
#[cfg(unix)]
{
unix::create_listener(socket_path).await
}
#[cfg(windows)]
{
windows::create_listener(socket_path).await
}
}

View File

@@ -0,0 +1,26 @@
use crate::{LooperError, Result};
use tokio::net::{UnixListener, UnixStream};
pub(crate) type PlatformStream = UnixStream;
pub(crate) struct PlatformListener {
listener: UnixListener,
}
impl PlatformListener {
pub async fn accept(&mut self) -> Result<PlatformStream> {
let (stream, _) = self.listener.accept().await
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
Ok(stream)
}
}
pub(crate) async fn create_listener(socket_path: &str) -> Result<PlatformListener> {
// Remove existing socket file if it exists
let _ = std::fs::remove_file(socket_path);
let listener = UnixListener::bind(socket_path)
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
Ok(PlatformListener { listener })
}

View File

@@ -0,0 +1,45 @@
use crate::{LooperError, Result};
use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions};
pub(crate) type PlatformStream = NamedPipeServer;
pub(crate) struct PlatformListener {
pipe_name: String,
current_server: Option<NamedPipeServer>,
}
impl PlatformListener {
pub async fn accept(&mut self) -> Result<PlatformStream> {
// Create new pipe instance if needed
if self.current_server.is_none() {
self.current_server = Some(create_pipe_server(&self.pipe_name)?);
}
let server = self.current_server.take().unwrap();
server.connect().await
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
Ok(server)
}
}
pub(crate) async fn create_listener(pipe_name: &str) -> Result<PlatformListener> {
// Convert pipe name to Windows format if needed
let pipe_path = if pipe_name.starts_with(r"\\.\pipe\") {
pipe_name.to_string()
} else {
format!(r"\\.\pipe\{}", pipe_name)
};
Ok(PlatformListener {
pipe_name: pipe_path,
current_server: None,
})
}
fn create_pipe_server(pipe_name: &str) -> Result<NamedPipeServer> {
ServerOptions::new()
.first_pipe_instance(false)
.create(pipe_name)
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))
}

View File

@@ -0,0 +1,139 @@
use crate::*;
use futures::SinkExt;
const CLIENT_BUFFER_SIZE: usize = 32;
pub struct Osc {
receiver: kanal::AsyncReceiver<osc::Message>,
listener: osc_server::platform::PlatformListener,
broadcaster: tokio::sync::broadcast::Sender<osc::Message>,
shadow_state: osc::State,
}
impl Osc {
/// Create new OSC server and controller with same interface as before
pub async fn new(socket_path: &str) -> Result<(Self, OscController)> {
// Create platform listener (server)
let listener = osc_server::platform::create_listener(socket_path).await?;
// Create communication channels
let (sender, receiver) = kanal::bounded(64);
let receiver = receiver.to_async();
let (broadcaster, _) = tokio::sync::broadcast::channel(CLIENT_BUFFER_SIZE);
let controller = OscController::new(sender);
let server = Self {
receiver,
listener,
broadcaster,
shadow_state: osc::State::new(5, 5), // Default 5x5 matrix
};
Ok((server, controller))
}
/// Main server loop - same interface as before
pub async fn run(&mut self) -> Result<()> {
loop {
tokio::select! {
// Handle messages from OscController
msg = self.receiver.recv() => {
if let Ok(msg) = msg {
self.shadow_state.update(&msg);
// Broadcast to all connected clients (ignore if no clients)
let _ = self.broadcaster.send(msg);
} else {
// Channel closed, exit
break;
}
}
// Accept new client connections
connection = self.listener.accept() => {
if let Ok(stream) = connection {
self.spawn_client_task(stream);
}
}
}
}
Ok(())
}
fn spawn_client_task(&self, stream: osc_server::platform::PlatformStream) {
let mut receiver = self.broadcaster.subscribe();
let state_dump = self.shadow_state.create_state_dump();
tokio::spawn(async move {
let mut framed = tokio_util::codec::Framed::new(stream, tokio_util::codec::LengthDelimitedCodec::new());
// Send current state dump immediately
for msg in state_dump {
if let Err(_) = Self::send_osc_message(&mut framed, msg).await {
return; // Client disconnected during state dump
}
}
// Forward broadcast messages until connection dies
while let Ok(msg) = receiver.recv().await {
if let Err(_) = Self::send_osc_message(&mut framed, msg).await {
break; // Client disconnected
}
}
});
}
async fn send_osc_message(
framed: &mut tokio_util::codec::Framed<osc_server::platform::PlatformStream, tokio_util::codec::LengthDelimitedCodec>,
message: osc::Message,
) -> Result<()> {
let osc_packet = Self::message_to_osc_packet(message)?;
let osc_bytes = rosc::encoder::encode(&osc_packet)
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
framed
.send(bytes::Bytes::from(osc_bytes))
.await
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
Ok(())
}
fn message_to_osc_packet(message: osc::Message) -> Result<rosc::OscPacket> {
match message {
osc::Message::TrackStateChanged { column, row, state } => {
let address = format!("/looper/cell/{}/{}/state", column + 1, row + 1); // 1-based indexing
Ok(rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::String(state.to_string())],
}))
}
osc::Message::TrackVolumeChanged { column, row, volume } => {
let address = format!("/looper/cell/{}/{}/volume", column + 1, row + 1); // 1-based indexing
Ok(rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Float(volume)],
}))
}
osc::Message::SelectedColumnChanged { column } => {
let address = "/looper/selected/column".to_string();
Ok(rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing
}))
}
osc::Message::SelectedRowChanged { row } => {
let address = "/looper/selected/row".to_string();
Ok(rosc::OscPacket::Message(rosc::OscMessage {
addr: address,
args: vec![rosc::OscType::Int((row + 1) as i32)], // 1-based indexing
}))
}
}
}
}