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

@@ -79,6 +79,7 @@ dependencies = [
"jack",
"kanal",
"log",
"osc",
"rosc",
"serde",
"serde_json",
@@ -519,6 +520,14 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "osc"
version = "0.1.0"
dependencies = [
"rosc",
"thiserror",
]
[[package]]
name = "parking_lot"
version = "0.12.4"

View File

@@ -4,19 +4,21 @@ version = "0.1.0"
edition = "2021"
[dependencies]
bytes = "1.0"
clap = { version = "4", features = ["derive", "env", "string"] }
dirs = "5"
futures = "0.3"
hound = "3.5"
jack = "0.13"
kanal = "0.1"
log = "0.4"
rosc = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
simple_logger = "5"
thiserror = "1"
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["codec"] }
wmidi = "4"
wmidi = "4"
bytes.workspace = true
futures.workspace = true
log.workspace = true
osc.workspace = true
rosc.workspace = true
thiserror.workspace = true
tokio.workspace = true
tokio-util.workspace = true

View File

@@ -11,7 +11,7 @@ mod looper_error;
mod metronome;
mod midi;
mod notification_handler;
mod osc;
mod osc_server;
mod persistence_manager;
mod post_record_handler;
mod process_handler;
@@ -38,8 +38,8 @@ use metronome::BufferTiming;
use metronome::Metronome;
use notification_handler::JackNotification;
use notification_handler::NotificationHandler;
use osc::OscController;
use osc::Osc;
use osc_server::OscController;
use osc_server::Osc;
use persistence_manager::PersistenceManager;
use persistence_manager::PersistenceManagerController;
use post_record_handler::PostRecordController;

View File

@@ -1,107 +0,0 @@
use crate::TrackState;
#[derive(Debug, Clone)]
pub(crate) enum OscMessage {
TrackStateChanged {
column: usize,
row: usize,
state: TrackState,
},
TrackVolumeChanged {
column: usize,
row: usize,
volume: f32,
},
SelectedColumnChanged {
column: usize,
},
SelectedRowChanged {
row: usize,
},
}
#[derive(Debug, Clone)]
pub(crate) struct Track {
pub state: TrackState,
pub volume: f32,
}
#[derive(Debug, Clone)]
pub(crate) struct ShadowState {
pub selected_column: usize, // 0-based (converted to 1-based for OSC)
pub selected_row: usize, // 0-based (converted to 1-based for OSC)
pub cells: Vec<Vec<Track>>,
pub columns: usize,
pub rows: usize,
}
impl ShadowState {
pub fn new(columns: usize, rows: usize) -> Self {
let cells = (0..columns)
.map(|_| vec![Track { state: TrackState::Empty, volume: 1.0 }; rows])
.collect();
Self {
selected_column: 0,
selected_row: 0,
cells,
columns,
rows,
}
}
pub fn update(&mut self, message: &OscMessage) {
match message {
OscMessage::TrackStateChanged { column, row, state } => {
if *column < self.columns && *row < self.rows {
self.cells[*column][*row].state = state.clone();
}
}
OscMessage::TrackVolumeChanged { column, row, volume } => {
if *column < self.columns && *row < self.rows {
self.cells[*column][*row].volume = *volume;
}
}
OscMessage::SelectedColumnChanged { column } => {
if *column < self.columns {
self.selected_column = *column;
}
}
OscMessage::SelectedRowChanged { row } => {
if *row < self.rows {
self.selected_row = *row;
}
}
}
}
pub fn create_state_dump(&self) -> Vec<OscMessage> {
let mut messages = Vec::new();
// Send current selections
messages.push(OscMessage::SelectedColumnChanged {
column: self.selected_column,
});
messages.push(OscMessage::SelectedRowChanged {
row: self.selected_row,
});
// Send all cell states and volumes
for (column, column_cells) in self.cells.iter().enumerate() {
for (row, track) in column_cells.iter().enumerate() {
messages.push(OscMessage::TrackStateChanged {
column,
row,
state: track.state.clone(),
});
messages.push(OscMessage::TrackVolumeChanged {
column,
row,
volume: track.volume,
});
}
}
messages
}
}

View File

@@ -1,13 +1,12 @@
use crate::{LooperError, Result, TrackState};
use super::message::OscMessage;
use crate::*;
#[derive(Debug)]
pub struct OscController {
sender: kanal::Sender<OscMessage>,
sender: kanal::Sender<osc::Message>,
}
impl OscController {
pub(crate) fn new(sender: kanal::Sender<OscMessage>) -> Self {
pub(crate) fn new(sender: kanal::Sender<osc::Message>) -> Self {
Self { sender }
}
@@ -17,7 +16,7 @@ impl OscController {
row: usize,
state: TrackState,
) -> Result<()> {
let message = OscMessage::TrackStateChanged { column, row, state };
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
@@ -31,7 +30,7 @@ impl OscController {
row: usize,
volume: f32,
) -> Result<()> {
let message = OscMessage::TrackVolumeChanged { column, row, volume };
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
@@ -40,7 +39,7 @@ impl OscController {
}
pub fn selected_column_changed(&self, column: usize) -> Result<()> {
let message = OscMessage::SelectedColumnChanged { column };
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
@@ -49,7 +48,7 @@ impl OscController {
}
pub fn selected_row_changed(&self, row: usize) -> Result<()> {
let message = OscMessage::SelectedRowChanged { row };
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

View File

@@ -1,5 +1,4 @@
mod controller;
mod message;
mod platform;
mod server;

View File

@@ -1,33 +1,25 @@
use crate::Result;
use super::{
controller::OscController,
message::{OscMessage, ShadowState},
platform::{create_listener, PlatformListener, PlatformStream},
};
use bytes::Bytes;
use crate::*;
use futures::SinkExt;
use tokio::sync::broadcast;
use tokio_util::codec::{Framed, LengthDelimitedCodec};
const CLIENT_BUFFER_SIZE: usize = 32;
pub struct Osc {
receiver: kanal::AsyncReceiver<OscMessage>,
listener: PlatformListener,
broadcaster: broadcast::Sender<OscMessage>,
shadow_state: ShadowState,
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 = create_listener(socket_path).await?;
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, _) = broadcast::channel(CLIENT_BUFFER_SIZE);
let (broadcaster, _) = tokio::sync::broadcast::channel(CLIENT_BUFFER_SIZE);
let controller = OscController::new(sender);
@@ -35,7 +27,7 @@ impl Osc {
receiver,
listener,
broadcaster,
shadow_state: ShadowState::new(5, 5), // Default 5x5 matrix
shadow_state: osc::State::new(5, 5), // Default 5x5 matrix
};
Ok((server, controller))
@@ -68,12 +60,12 @@ impl Osc {
Ok(())
}
fn spawn_client_task(&self, stream: PlatformStream) {
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 = Framed::new(stream, LengthDelimitedCodec::new());
let mut framed = tokio_util::codec::Framed::new(stream, tokio_util::codec::LengthDelimitedCodec::new());
// Send current state dump immediately
for msg in state_dump {
@@ -92,8 +84,8 @@ impl Osc {
}
async fn send_osc_message(
framed: &mut Framed<PlatformStream, LengthDelimitedCodec>,
message: OscMessage,
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)?;
@@ -101,25 +93,24 @@ impl Osc {
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
framed
.send(Bytes::from(osc_bytes))
.send(bytes::Bytes::from(osc_bytes))
.await
.map_err(|_| crate::LooperError::StateSave(std::panic::Location::caller()))?;
Ok(())
}
fn message_to_osc_packet(message: OscMessage) -> Result<rosc::OscPacket> {
fn message_to_osc_packet(message: osc::Message) -> Result<rosc::OscPacket> {
match message {
OscMessage::TrackStateChanged { column, row, state } => {
let osc_state = Self::track_state_to_osc_string(state);
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(osc_state)],
args: vec![rosc::OscType::String(state.to_string())],
}))
}
OscMessage::TrackVolumeChanged { column, row, volume } => {
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 {
@@ -127,7 +118,7 @@ impl Osc {
args: vec![rosc::OscType::Float(volume)],
}))
}
OscMessage::SelectedColumnChanged { column } => {
osc::Message::SelectedColumnChanged { column } => {
let address = "/looper/selected/column".to_string();
Ok(rosc::OscPacket::Message(rosc::OscMessage {
@@ -135,7 +126,7 @@ impl Osc {
args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing
}))
}
OscMessage::SelectedRowChanged { row } => {
osc::Message::SelectedRowChanged { row } => {
let address = "/looper/selected/row".to_string();
Ok(rosc::OscPacket::Message(rosc::OscMessage {
@@ -145,14 +136,4 @@ impl Osc {
}
}
}
fn track_state_to_osc_string(state: crate::TrackState) -> String {
match state {
crate::TrackState::Empty => "empty".to_string(),
crate::TrackState::Idle => "ready".to_string(),
crate::TrackState::Recording { .. } => "recording".to_string(),
crate::TrackState::RecordingAutoStop { .. } => "recording".to_string(),
crate::TrackState::Playing => "playing".to_string(),
}
}
}

View File

@@ -327,4 +327,16 @@ impl Track {
Ok(())
}
}
impl Into<osc::TrackState> for TrackState {
fn into(self) -> osc::TrackState {
match self {
TrackState::Empty => osc::TrackState::Empty,
TrackState::Idle => osc::TrackState::Idle,
TrackState::Recording { .. } => osc::TrackState::Recording,
TrackState::RecordingAutoStop { .. } => osc::TrackState::Recording,
TrackState::Playing => osc::TrackState::Playing,
}
}
}