use crate::io::AtomicF64;
use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub enum OscValue {
Int(i32),
Float(f32),
String(String),
Blob(Vec<u8>),
True,
False,
Nil,
Infinitum,
Long(i64),
Double(f64),
}
impl OscValue {
pub fn to_f64(&self) -> Option<f64> {
match self {
OscValue::Int(v) => Some(*v as f64),
OscValue::Float(v) => Some(*v as f64),
OscValue::Long(v) => Some(*v as f64),
OscValue::Double(v) => Some(*v),
OscValue::True => Some(1.0),
OscValue::False => Some(0.0),
_ => None,
}
}
pub fn to_bool(&self) -> Option<bool> {
match self {
OscValue::Int(v) => Some(*v != 0),
OscValue::Float(v) => Some(*v != 0.0),
OscValue::True => Some(true),
OscValue::False => Some(false),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct OscMessage {
pub address: String,
pub args: Vec<OscValue>,
}
impl OscMessage {
pub fn new(address: impl Into<String>) -> Self {
Self {
address: address.into(),
args: Vec::new(),
}
}
pub fn with_arg(mut self, arg: OscValue) -> Self {
self.args.push(arg);
self
}
pub fn with_float(self, value: f32) -> Self {
self.with_arg(OscValue::Float(value))
}
pub fn with_int(self, value: i32) -> Self {
self.with_arg(OscValue::Int(value))
}
pub fn first_f64(&self) -> Option<f64> {
self.args.first().and_then(|v| v.to_f64())
}
}
pub struct OscPattern {
components: Vec<String>,
}
impl OscPattern {
pub fn new(pattern: &str) -> Self {
let components = pattern
.split('/')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
Self { components }
}
pub fn matches(&self, address: &str) -> bool {
let parts: Vec<&str> = address.split('/').filter(|s| !s.is_empty()).collect();
if parts.len() != self.components.len() {
return false;
}
self.components
.iter()
.zip(parts.iter())
.all(|(pat, part)| component_matches(pat, part))
}
}
fn component_matches(pattern: &str, text: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = text.chars().collect();
glob_match(&pat, &txt)
}
fn glob_match(pat: &[char], text: &[char]) -> bool {
let Some((&c, rest)) = pat.split_first() else {
return text.is_empty();
};
match c {
'*' => {
(0..=text.len()).any(|i| glob_match(rest, &text[i..]))
}
'?' => !text.is_empty() && glob_match(rest, &text[1..]),
'[' => match parse_class(pat) {
Some((class, consumed)) => {
!text.is_empty()
&& class_matches(&class, text[0])
&& glob_match(&pat[consumed..], &text[1..])
}
None => false,
},
'{' => match parse_alternation(pat) {
Some((alts, consumed)) => alts.iter().any(|alt| {
strip_prefix(alt, text).is_some_and(|rem| glob_match(&pat[consumed..], rem))
}),
None => false,
},
_ => !text.is_empty() && text[0] == c && glob_match(rest, &text[1..]),
}
}
enum ClassItem {
Char(char),
Range(char, char),
}
struct CharClass {
negated: bool,
items: Vec<ClassItem>,
}
fn parse_class(pat: &[char]) -> Option<(CharClass, usize)> {
debug_assert_eq!(pat.first(), Some(&'['));
let mut i = 1;
let mut negated = false;
if matches!(pat.get(i), Some('!') | Some('^')) {
negated = true;
i += 1;
}
let mut items = Vec::new();
let mut closed = false;
while i < pat.len() {
if pat[i] == ']' {
closed = true;
i += 1;
break;
}
if i + 2 < pat.len() && pat[i + 1] == '-' && pat[i + 2] != ']' {
items.push(ClassItem::Range(pat[i], pat[i + 2]));
i += 3;
} else {
items.push(ClassItem::Char(pat[i]));
i += 1;
}
}
if !closed || items.is_empty() {
return None;
}
Some((CharClass { negated, items }, i))
}
fn class_matches(class: &CharClass, ch: char) -> bool {
let mut hit = false;
for item in &class.items {
match item {
ClassItem::Char(c) => {
if *c == ch {
hit = true;
break;
}
}
ClassItem::Range(a, b) => {
let (lo, hi) = if a <= b { (*a, *b) } else { (*b, *a) };
if ch >= lo && ch <= hi {
hit = true;
break;
}
}
}
}
hit ^ class.negated
}
fn parse_alternation(pat: &[char]) -> Option<(Vec<Vec<char>>, usize)> {
debug_assert_eq!(pat.first(), Some(&'{'));
let mut i = 1;
let mut alts = Vec::new();
let mut current = Vec::new();
let mut closed = false;
while i < pat.len() {
match pat[i] {
'}' => {
alts.push(current);
closed = true;
i += 1;
break;
}
',' => {
alts.push(core::mem::take(&mut current));
i += 1;
}
c => {
current.push(c);
i += 1;
}
}
}
if !closed {
return None;
}
Some((alts, i))
}
fn strip_prefix<'a>(prefix: &[char], text: &'a [char]) -> Option<&'a [char]> {
if text.len() >= prefix.len() && text[..prefix.len()] == *prefix {
Some(&text[prefix.len()..])
} else {
None
}
}
pub struct OscBinding {
pub pattern: OscPattern,
pub value: Arc<AtomicF64>,
pub scale: f64,
pub offset: f64,
}
impl OscBinding {
pub fn new(pattern: &str, value: Arc<AtomicF64>) -> Self {
Self {
pattern: OscPattern::new(pattern),
value,
scale: 1.0,
offset: 0.0,
}
}
pub fn with_scale(mut self, scale: f64) -> Self {
self.scale = scale;
self
}
pub fn with_offset(mut self, offset: f64) -> Self {
self.offset = offset;
self
}
pub fn apply(&self, msg: &OscMessage) -> bool {
if !self.pattern.matches(&msg.address) {
return false;
}
if let Some(v) = msg.first_f64() {
self.value.set(v * self.scale + self.offset);
return true;
}
false
}
}
pub struct OscReceiver {
bindings: Vec<OscBinding>,
message_count: AtomicU32,
matched_count: AtomicU32,
}
impl OscReceiver {
pub fn new() -> Self {
Self {
bindings: Vec::new(),
message_count: AtomicU32::new(0),
matched_count: AtomicU32::new(0),
}
}
pub fn add_binding(&mut self, binding: OscBinding) {
self.bindings.push(binding);
}
pub fn bind(&mut self, pattern: &str, value: Arc<AtomicF64>) {
self.add_binding(OscBinding::new(pattern, value));
}
pub fn bind_scaled(&mut self, pattern: &str, value: Arc<AtomicF64>, scale: f64, offset: f64) {
self.add_binding(
OscBinding::new(pattern, value)
.with_scale(scale)
.with_offset(offset),
);
}
pub fn handle_message(&self, msg: &OscMessage) -> bool {
self.message_count.fetch_add(1, Ordering::Relaxed);
let mut handled = false;
for binding in &self.bindings {
if binding.apply(msg) {
handled = true;
}
}
if handled {
self.matched_count.fetch_add(1, Ordering::Relaxed);
}
handled
}
pub fn binding_count(&self) -> usize {
self.bindings.len()
}
pub fn message_count(&self) -> u32 {
self.message_count.load(Ordering::Relaxed)
}
pub fn matched_count(&self) -> u32 {
self.matched_count.load(Ordering::Relaxed)
}
pub fn reset_counters(&self) {
self.message_count.store(0, Ordering::Relaxed);
self.matched_count.store(0, Ordering::Relaxed);
}
}
impl Default for OscReceiver {
fn default() -> Self {
Self::new()
}
}
pub struct OscInput {
value: Arc<AtomicF64>,
spec: PortSpec,
address: String,
}
impl OscInput {
pub fn new(address: impl Into<String>, value: Arc<AtomicF64>, kind: SignalKind) -> Self {
Self {
value,
spec: PortSpec {
inputs: vec![],
outputs: vec![PortDef::new(0, "out", kind)],
},
address: address.into(),
}
}
pub fn address(&self) -> &str {
&self.address
}
pub fn value_ref(&self) -> &Arc<AtomicF64> {
&self.value
}
}
impl GraphModule for OscInput {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, _inputs: &PortValues, outputs: &mut PortValues) {
outputs.set(0, self.value.get());
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"osc_input"
}
}
#[derive(Debug, Clone)]
pub struct PluginParameter {
pub id: u32,
pub name: String,
pub short_name: String,
pub min: f64,
pub max: f64,
pub default: f64,
pub unit: String,
pub steps: u32,
}
impl PluginParameter {
pub fn new(id: u32, name: &str, min: f64, max: f64, default: f64) -> Self {
Self {
id,
name: name.to_string(),
short_name: name.chars().take(8).collect(),
min,
max,
default,
unit: String::new(),
steps: 0,
}
}
pub fn with_unit(mut self, unit: &str) -> Self {
self.unit = unit.to_string();
self
}
pub fn with_steps(mut self, steps: u32) -> Self {
self.steps = steps;
self
}
pub fn with_short_name(mut self, short_name: &str) -> Self {
self.short_name = short_name.to_string();
self
}
pub fn normalize(&self, value: f64) -> f64 {
(value - self.min) / (self.max - self.min)
}
pub fn denormalize(&self, normalized: f64) -> f64 {
self.min + normalized * (self.max - self.min)
}
pub fn quantize(&self, value: f64) -> f64 {
if self.steps == 0 {
return value;
}
let step_size = (self.max - self.min) / self.steps as f64;
let steps = ((value - self.min) / step_size).round();
self.min + steps * step_size
}
}
#[derive(Debug, Clone)]
pub struct AudioBusConfig {
pub inputs: u32,
pub outputs: u32,
pub name: String,
}
impl AudioBusConfig {
pub fn stereo_out() -> Self {
Self {
inputs: 0,
outputs: 2,
name: "Main".to_string(),
}
}
pub fn stereo_io() -> Self {
Self {
inputs: 2,
outputs: 2,
name: "Main".to_string(),
}
}
pub fn mono_out() -> Self {
Self {
inputs: 0,
outputs: 1,
name: "Main".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct PluginInfo {
pub id: String,
pub name: String,
pub vendor: String,
pub version: String,
pub category: PluginCategory,
pub is_synth: bool,
pub sample_rates: Vec<f64>,
pub max_block_size: usize,
pub latency: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginCategory {
Effect,
Instrument,
Analyzer,
Spatial,
Generator,
Other,
}
impl PluginInfo {
pub fn synth(id: &str, name: &str, vendor: &str) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
vendor: vendor.to_string(),
version: "1.0.0".to_string(),
category: PluginCategory::Instrument,
is_synth: true,
sample_rates: vec![],
max_block_size: 0,
latency: 0,
}
}
pub fn effect(id: &str, name: &str, vendor: &str) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
vendor: vendor.to_string(),
version: "1.0.0".to_string(),
category: PluginCategory::Effect,
is_synth: false,
sample_rates: vec![],
max_block_size: 0,
latency: 0,
}
}
}
pub struct PluginWrapper {
pub info: PluginInfo,
pub bus_config: AudioBusConfig,
pub parameters: Vec<PluginParameter>,
pub param_values: Vec<Arc<AtomicF64>>,
pub sample_rate: f64,
pub is_processing: AtomicBool,
}
impl PluginWrapper {
pub fn new(info: PluginInfo, bus_config: AudioBusConfig) -> Self {
Self {
info,
bus_config,
parameters: Vec::new(),
param_values: Vec::new(),
sample_rate: 44100.0,
is_processing: AtomicBool::new(false),
}
}
pub fn add_parameter(&mut self, param: PluginParameter) -> Arc<AtomicF64> {
let value = Arc::new(AtomicF64::new(param.default));
self.param_values.push(value.clone());
self.parameters.push(param);
value
}
pub fn parameter_count(&self) -> usize {
self.parameters.len()
}
pub fn get_parameter(&self, index: usize) -> Option<f64> {
self.param_values.get(index).map(|v| v.get())
}
pub fn set_parameter_normalized(&self, index: usize, normalized: f64) {
if let (Some(param), Some(value)) =
(self.parameters.get(index), self.param_values.get(index))
{
let denormalized = param.denormalize(normalized.clamp(0.0, 1.0));
let quantized = param.quantize(denormalized);
value.set(quantized);
}
}
pub fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
pub fn start_processing(&self) {
self.is_processing.store(true, Ordering::SeqCst);
}
pub fn stop_processing(&self) {
self.is_processing.store(false, Ordering::SeqCst);
}
pub fn is_processing(&self) -> bool {
self.is_processing.load(Ordering::SeqCst)
}
pub fn latency(&self) -> u32 {
self.info.latency
}
pub fn set_latency(&mut self, samples: u32) {
self.info.latency = samples;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MidiStatus {
NoteOff(u8),
NoteOn(u8),
PolyPressure(u8),
ControlChange(u8),
ProgramChange(u8),
ChannelPressure(u8),
PitchBend(u8),
System(u8),
}
impl MidiStatus {
pub fn from_byte(byte: u8) -> Option<Self> {
let status = byte & 0xF0;
let channel = byte & 0x0F;
match status {
0x80 => Some(MidiStatus::NoteOff(channel)),
0x90 => Some(MidiStatus::NoteOn(channel)),
0xA0 => Some(MidiStatus::PolyPressure(channel)),
0xB0 => Some(MidiStatus::ControlChange(channel)),
0xC0 => Some(MidiStatus::ProgramChange(channel)),
0xD0 => Some(MidiStatus::ChannelPressure(channel)),
0xE0 => Some(MidiStatus::PitchBend(channel)),
0xF0..=0xFF => Some(MidiStatus::System(byte)),
_ => None,
}
}
pub fn channel(&self) -> Option<u8> {
match self {
MidiStatus::NoteOff(ch)
| MidiStatus::NoteOn(ch)
| MidiStatus::PolyPressure(ch)
| MidiStatus::ControlChange(ch)
| MidiStatus::ProgramChange(ch)
| MidiStatus::ChannelPressure(ch)
| MidiStatus::PitchBend(ch) => Some(*ch),
MidiStatus::System(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub struct MidiMessage {
pub sample_offset: u32,
pub status: MidiStatus,
pub data1: u8,
pub data2: u8,
}
impl MidiMessage {
pub fn note_on(channel: u8, note: u8, velocity: u8) -> Self {
Self {
sample_offset: 0,
status: MidiStatus::NoteOn(channel & 0x0F),
data1: note & 0x7F,
data2: velocity & 0x7F,
}
}
pub fn note_off(channel: u8, note: u8, velocity: u8) -> Self {
Self {
sample_offset: 0,
status: MidiStatus::NoteOff(channel & 0x0F),
data1: note & 0x7F,
data2: velocity & 0x7F,
}
}
pub fn control_change(channel: u8, cc: u8, value: u8) -> Self {
Self {
sample_offset: 0,
status: MidiStatus::ControlChange(channel & 0x0F),
data1: cc & 0x7F,
data2: value & 0x7F,
}
}
pub fn pitch_bend(channel: u8, value: i16) -> Self {
let unsigned = (value + 8192).clamp(0, 16383) as u16;
Self {
sample_offset: 0,
status: MidiStatus::PitchBend(channel & 0x0F),
data1: (unsigned & 0x7F) as u8,
data2: ((unsigned >> 7) & 0x7F) as u8,
}
}
pub fn at_sample(mut self, offset: u32) -> Self {
self.sample_offset = offset;
self
}
pub fn is_note_on(&self) -> bool {
matches!(self.status, MidiStatus::NoteOn(_)) && self.data2 > 0
}
pub fn is_note_off(&self) -> bool {
matches!(self.status, MidiStatus::NoteOff(_))
|| (matches!(self.status, MidiStatus::NoteOn(_)) && self.data2 == 0)
}
pub fn note(&self) -> u8 {
self.data1
}
pub fn velocity(&self) -> u8 {
self.data2
}
pub fn note_to_frequency(&self) -> f64 {
440.0 * 2.0_f64.powf((self.data1 as f64 - 69.0) / 12.0)
}
pub fn note_to_volt_per_octave(&self) -> f64 {
(self.data1 as f64 - 60.0) / 12.0
}
pub fn pitch_bend_normalized(&self) -> f64 {
if !matches!(self.status, MidiStatus::PitchBend(_)) {
return 0.0;
}
let value = (self.data1 as i32) | ((self.data2 as i32) << 7);
(value - 8192) as f64 / 8192.0
}
}
pub struct MidiBuffer {
events: Vec<MidiMessage>,
}
impl MidiBuffer {
pub fn new() -> Self {
Self { events: Vec::new() }
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
events: Vec::with_capacity(capacity),
}
}
pub fn push(&mut self, event: MidiMessage) {
self.events.push(event);
}
pub fn clear(&mut self) {
self.events.clear();
}
pub fn sort(&mut self) {
self.events.sort_by_key(|e| e.sample_offset);
}
pub fn iter(&self) -> impl Iterator<Item = &MidiMessage> {
self.events.iter()
}
pub fn events_at(&self, sample: u32) -> impl Iterator<Item = &MidiMessage> {
self.events
.iter()
.filter(move |e| e.sample_offset == sample)
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
}
impl Default for MidiBuffer {
fn default() -> Self {
Self::new()
}
}
pub struct ProcessContext<'a> {
pub sample_rate: f64,
pub num_samples: usize,
pub transport_position: Option<u64>,
pub tempo: Option<f64>,
pub is_playing: bool,
pub midi_in: &'a MidiBuffer,
pub midi_out: &'a mut MidiBuffer,
}
pub trait PluginProcessor: Send {
fn initialize(&mut self, sample_rate: f64, max_block_size: usize);
fn process(
&mut self,
inputs: &[&[f32]],
outputs: &mut [&mut [f32]],
context: &mut ProcessContext,
);
fn reset(&mut self);
fn set_parameter(&mut self, id: u32, value: f64);
fn get_parameter(&self, id: u32) -> f64;
fn parameter_count(&self) -> usize {
0
}
fn parameter_info(&self, _id: u32) -> Option<PluginParameter> {
None
}
fn tail_samples(&self) -> u32 {
0
}
fn latency_samples(&self) -> u32 {
0
}
fn save_state(&self) -> Vec<u8> {
Vec::new()
}
fn load_state(&mut self, _data: &[u8]) -> bool {
false
}
}
#[derive(Debug, Clone)]
pub struct WebAudioConfig {
pub input_channels: u32,
pub output_channels: u32,
pub sample_rate: f64,
pub block_size: usize,
}
impl Default for WebAudioConfig {
fn default() -> Self {
Self {
input_channels: 0,
output_channels: 2,
sample_rate: 44100.0,
block_size: 128,
}
}
}
pub trait WebAudioProcessor: Send {
fn initialize(&mut self, config: &WebAudioConfig);
fn process(&mut self, inputs: &[f32], outputs: &mut [f32]) -> bool;
fn set_parameter(&mut self, name: &str, value: f64);
fn get_parameter(&self, name: &str) -> Option<f64>;
fn parameter_names(&self) -> Vec<String>;
fn handle_message(&mut self, _data: &[u8]) {}
}
pub struct WebAudioWorklet {
config: WebAudioConfig,
parameters: HashMap<String, Arc<AtomicF64>>,
active: bool,
}
impl WebAudioWorklet {
pub fn new() -> Self {
Self {
config: WebAudioConfig::default(),
parameters: HashMap::new(),
active: false,
}
}
pub fn add_parameter(&mut self, name: &str, initial: f64) -> Arc<AtomicF64> {
let value = Arc::new(AtomicF64::new(initial));
self.parameters.insert(name.to_string(), value.clone());
value
}
pub fn initialize(&mut self, config: WebAudioConfig) {
self.config = config;
self.active = true;
}
pub fn config(&self) -> &WebAudioConfig {
&self.config
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn get_parameter(&self, name: &str) -> Option<f64> {
self.parameters.get(name).map(|v| v.get())
}
pub fn set_parameter(&mut self, name: &str, value: f64) {
if let Some(param) = self.parameters.get(name) {
param.set(value);
}
}
}
impl Default for WebAudioWorklet {
fn default() -> Self {
Self::new()
}
}
pub struct WebAudioBlockProcessor {
config: WebAudioConfig,
left_buffer: Vec<f64>,
right_buffer: Vec<f64>,
interleaved_buffer: Vec<f32>,
parameters: HashMap<String, Arc<AtomicF64>>,
active: bool,
}
impl WebAudioBlockProcessor {
pub fn new() -> Self {
Self::with_config(WebAudioConfig::default())
}
pub fn with_config(config: WebAudioConfig) -> Self {
let block_size = config.block_size;
Self {
config,
left_buffer: vec![0.0; block_size],
right_buffer: vec![0.0; block_size],
interleaved_buffer: vec![0.0; block_size * 2],
parameters: HashMap::new(),
active: false,
}
}
pub fn add_parameter(&mut self, name: &str, initial: f64) -> Arc<AtomicF64> {
let value = Arc::new(AtomicF64::new(initial));
self.parameters.insert(name.to_string(), value.clone());
value
}
pub fn activate(&mut self) {
self.active = true;
}
pub fn deactivate(&mut self) {
self.active = false;
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn config(&self) -> &WebAudioConfig {
&self.config
}
pub fn block_size(&self) -> usize {
self.config.block_size
}
pub fn sample_rate(&self) -> f64 {
self.config.sample_rate
}
pub fn get_parameter(&self, name: &str) -> Option<f64> {
self.parameters.get(name).map(|v| v.get())
}
pub fn set_parameter(&mut self, name: &str, value: f64) {
if let Some(param) = self.parameters.get(name) {
param.set(value);
}
}
pub fn parameter_names(&self) -> Vec<String> {
self.parameters.keys().cloned().collect()
}
pub fn process_with<F>(&mut self, mut generator: F) -> &[f32]
where
F: FnMut(usize) -> (f64, f64),
{
for i in 0..self.config.block_size {
let (left, right) = generator(i);
self.left_buffer[i] = left;
self.right_buffer[i] = right;
}
interleave_stereo(
&self.left_buffer,
&self.right_buffer,
&mut self.interleaved_buffer,
);
&self.interleaved_buffer
}
pub fn left_buffer_mut(&mut self) -> &mut [f64] {
&mut self.left_buffer
}
pub fn right_buffer_mut(&mut self) -> &mut [f64] {
&mut self.right_buffer
}
pub fn finalize(&mut self) -> &[f32] {
interleave_stereo(
&self.left_buffer,
&self.right_buffer,
&mut self.interleaved_buffer,
);
&self.interleaved_buffer
}
pub fn clear(&mut self) {
self.left_buffer.fill(0.0);
self.right_buffer.fill(0.0);
self.interleaved_buffer.fill(0.0);
}
}
impl Default for WebAudioBlockProcessor {
fn default() -> Self {
Self::new()
}
}
#[inline]
pub fn f64_to_f32_block(src: &[f64], dst: &mut [f32]) {
let len = src.len().min(dst.len());
for i in 0..len {
dst[i] = src[i] as f32;
}
}
#[inline]
pub fn f32_to_f64_block(src: &[f32], dst: &mut [f64]) {
let len = src.len().min(dst.len());
for i in 0..len {
dst[i] = src[i] as f64;
}
}
#[inline]
pub fn interleave_stereo(left: &[f64], right: &[f64], output: &mut [f32]) {
let frames = left.len().min(right.len()).min(output.len() / 2);
for i in 0..frames {
output[i * 2] = left[i] as f32;
output[i * 2 + 1] = right[i] as f32;
}
}
#[inline]
pub fn deinterleave_stereo(input: &[f32], left: &mut [f64], right: &mut [f64]) {
let frames = (input.len() / 2).min(left.len()).min(right.len());
for i in 0..frames {
left[i] = input[i * 2] as f64;
right[i] = input[i * 2 + 1] as f64;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_osc_message() {
let msg = OscMessage::new("/synth/filter/cutoff").with_float(0.75);
assert_eq!(msg.address, "/synth/filter/cutoff");
assert!((msg.first_f64().unwrap() - 0.75).abs() < 0.001);
}
#[test]
fn test_osc_pattern_literal() {
let pattern = OscPattern::new("/synth/osc/pitch");
assert!(pattern.matches("/synth/osc/pitch"));
assert!(!pattern.matches("/synth/osc/volume"));
assert!(!pattern.matches("/synth/osc"));
}
#[test]
fn test_osc_pattern_wildcard() {
let pattern = OscPattern::new("/synth/*");
assert!(pattern.matches("/synth/osc"));
assert!(pattern.matches("/synth/filter"));
assert!(!pattern.matches("/synth/filter/cutoff"));
assert!(!pattern.matches("/synth"));
}
#[test]
fn test_osc_pattern_wildcard_partial_component() {
let pattern = OscPattern::new("/synth/osc*");
assert!(pattern.matches("/synth/osc"));
assert!(pattern.matches("/synth/osc1"));
assert!(pattern.matches("/synth/oscillator"));
assert!(!pattern.matches("/synth/lfo"));
let mid = OscPattern::new("/a/f*r");
assert!(mid.matches("/a/filter"));
assert!(mid.matches("/a/fr"));
assert!(!mid.matches("/a/filik"));
}
#[test]
fn test_osc_pattern_char_class_range() {
let pattern = OscPattern::new("/ch[0-9]");
assert!(pattern.matches("/ch0"));
assert!(pattern.matches("/ch7"));
assert!(!pattern.matches("/chx"));
assert!(!pattern.matches("/ch-"));
let alpha = OscPattern::new("/[a-c]");
assert!(alpha.matches("/a"));
assert!(alpha.matches("/c"));
assert!(!alpha.matches("/d"));
}
#[test]
fn test_osc_pattern_char_class_negation() {
let pattern = OscPattern::new("/ch[!0-9]");
assert!(pattern.matches("/chx"));
assert!(!pattern.matches("/ch5"));
let neg = OscPattern::new("/[!abc]");
assert!(neg.matches("/d"));
assert!(!neg.matches("/a"));
}
#[test]
fn test_osc_pattern_alternation() {
let pattern = OscPattern::new("/synth/{osc,lfo}");
assert!(pattern.matches("/synth/osc"));
assert!(pattern.matches("/synth/lfo"));
assert!(!pattern.matches("/synth/vcf"));
let mixed = OscPattern::new("/v{1,2}/gain");
assert!(mixed.matches("/v1/gain"));
assert!(mixed.matches("/v2/gain"));
assert!(!mixed.matches("/v3/gain"));
}
#[test]
fn test_osc_pattern_malformed_fails() {
let unterminated_class = OscPattern::new("/ch[0-9");
assert!(!unterminated_class.matches("/ch5"));
assert!(!unterminated_class.matches("/ch"));
let unterminated_alt = OscPattern::new("/synth/{osc,lfo");
assert!(!unterminated_alt.matches("/synth/osc"));
let empty_class = OscPattern::new("/ch[]");
assert!(!empty_class.matches("/ch"));
}
#[test]
fn test_osc_binding() {
let value = Arc::new(AtomicF64::new(0.0));
let binding = OscBinding::new("/test/param", value.clone()).with_scale(10.0);
let msg = OscMessage::new("/test/param").with_float(0.5);
assert!(binding.apply(&msg));
assert!((value.get() - 5.0).abs() < 0.001);
}
#[test]
fn test_osc_receiver() {
let mut receiver = OscReceiver::new();
let value = Arc::new(AtomicF64::new(0.0));
receiver.bind("/synth/volume", value.clone());
let msg = OscMessage::new("/synth/volume").with_float(0.8);
assert!(receiver.handle_message(&msg));
assert!((value.get() - 0.8).abs() < 0.001);
let msg2 = OscMessage::new("/synth/pitch").with_float(0.5);
assert!(!receiver.handle_message(&msg2));
}
#[test]
fn test_plugin_parameter() {
let param = PluginParameter::new(0, "Cutoff", 20.0, 20000.0, 1000.0).with_unit("Hz");
assert!((param.normalize(20.0) - 0.0).abs() < 0.001);
assert!((param.normalize(20000.0) - 1.0).abs() < 0.001);
assert!((param.denormalize(0.5) - 10010.0).abs() < 1.0);
}
#[test]
fn test_plugin_parameter_quantize() {
let param = PluginParameter::new(0, "Steps", 0.0, 10.0, 5.0).with_steps(10);
assert!((param.quantize(0.4) - 0.0).abs() < 0.1);
assert!((param.quantize(0.6) - 1.0).abs() < 0.1);
assert!((param.quantize(4.7) - 5.0).abs() < 0.1);
assert!((param.quantize(9.9) - 10.0).abs() < 0.1);
}
#[test]
fn test_plugin_wrapper() {
let info = PluginInfo::synth("com.quiver.test", "Test Synth", "Quiver");
let bus = AudioBusConfig::stereo_out();
let mut wrapper = PluginWrapper::new(info, bus);
let cutoff =
wrapper.add_parameter(PluginParameter::new(0, "Cutoff", 20.0, 20000.0, 1000.0));
assert_eq!(wrapper.parameter_count(), 1);
assert!((wrapper.get_parameter(0).unwrap() - 1000.0).abs() < 0.001);
wrapper.set_parameter_normalized(0, 0.5);
assert!((cutoff.get() - 10010.0).abs() < 1.0);
}
#[test]
fn test_web_audio_config() {
let config = WebAudioConfig::default();
assert_eq!(config.output_channels, 2);
assert_eq!(config.block_size, 128);
}
#[test]
fn test_web_audio_worklet() {
let mut worklet = WebAudioWorklet::new();
let freq = worklet.add_parameter("frequency", 440.0);
assert!((worklet.get_parameter("frequency").unwrap() - 440.0).abs() < 0.001);
worklet.set_parameter("frequency", 880.0);
assert!((freq.get() - 880.0).abs() < 0.001);
}
#[test]
fn test_interleave_stereo() {
let left = vec![1.0, 2.0, 3.0];
let right = vec![4.0, 5.0, 6.0];
let mut output = vec![0.0f32; 6];
interleave_stereo(&left, &right, &mut output);
assert!((output[0] - 1.0).abs() < 0.001);
assert!((output[1] - 4.0).abs() < 0.001);
assert!((output[2] - 2.0).abs() < 0.001);
assert!((output[3] - 5.0).abs() < 0.001);
}
#[test]
fn test_deinterleave_stereo() {
let input = vec![1.0f32, 4.0, 2.0, 5.0, 3.0, 6.0];
let mut left = vec![0.0; 3];
let mut right = vec![0.0; 3];
deinterleave_stereo(&input, &mut left, &mut right);
assert!((left[0] - 1.0).abs() < 0.001);
assert!((left[1] - 2.0).abs() < 0.001);
assert!((left[2] - 3.0).abs() < 0.001);
assert!((right[0] - 4.0).abs() < 0.001);
assert!((right[1] - 5.0).abs() < 0.001);
assert!((right[2] - 6.0).abs() < 0.001);
}
#[test]
fn test_osc_value_to_f64() {
assert!((OscValue::Int(42).to_f64().unwrap() - 42.0).abs() < 0.001);
assert!((OscValue::Float(2.5).to_f64().unwrap() - 2.5).abs() < 0.01);
assert!((OscValue::Long(100).to_f64().unwrap() - 100.0).abs() < 0.001);
assert!((OscValue::Double(2.71).to_f64().unwrap() - 2.71).abs() < 0.001);
assert!((OscValue::True.to_f64().unwrap() - 1.0).abs() < 0.001);
assert!((OscValue::False.to_f64().unwrap() - 0.0).abs() < 0.001);
assert!(OscValue::Nil.to_f64().is_none());
}
#[test]
fn test_osc_value_to_bool() {
assert_eq!(OscValue::Int(1).to_bool(), Some(true));
assert_eq!(OscValue::Int(0).to_bool(), Some(false));
assert_eq!(OscValue::Float(1.0).to_bool(), Some(true));
assert_eq!(OscValue::Float(0.0).to_bool(), Some(false));
assert_eq!(OscValue::True.to_bool(), Some(true));
assert_eq!(OscValue::False.to_bool(), Some(false));
assert_eq!(OscValue::Nil.to_bool(), None);
}
#[test]
fn test_osc_message_with_int() {
let msg = OscMessage::new("/test").with_int(42);
assert_eq!(msg.args.len(), 1);
}
#[test]
fn test_osc_pattern_single_char() {
let pattern = OscPattern::new("/a/?");
assert!(pattern.matches("/a/b"));
assert!(!pattern.matches("/a/bb"));
}
#[test]
fn test_osc_pattern_char_class() {
let pattern = OscPattern::new("/[abc]");
assert!(pattern.matches("/a"));
assert!(pattern.matches("/b"));
assert!(!pattern.matches("/d"));
}
#[test]
fn test_osc_binding_with_offset() {
let value = Arc::new(AtomicF64::new(0.0));
let binding = OscBinding::new("/test", value.clone())
.with_scale(2.0)
.with_offset(10.0);
let msg = OscMessage::new("/test").with_float(5.0);
binding.apply(&msg);
assert!((value.get() - 20.0).abs() < 0.001);
}
#[test]
fn test_osc_binding_non_matching() {
let value = Arc::new(AtomicF64::new(0.0));
let binding = OscBinding::new("/test", value.clone());
let msg = OscMessage::new("/other").with_float(5.0);
assert!(!binding.apply(&msg));
}
#[test]
fn test_osc_receiver_bind_scaled() {
let mut receiver = OscReceiver::new();
let value = Arc::new(AtomicF64::new(0.0));
receiver.bind_scaled("/test", value.clone(), 10.0, 5.0);
let msg = OscMessage::new("/test").with_float(1.0);
receiver.handle_message(&msg);
assert!((value.get() - 15.0).abs() < 0.001);
}
#[test]
fn test_osc_receiver_counters() {
let mut receiver = OscReceiver::new();
let value = Arc::new(AtomicF64::new(0.0));
receiver.bind("/test", value.clone());
let msg = OscMessage::new("/test").with_float(1.0);
receiver.handle_message(&msg);
assert_eq!(receiver.message_count(), 1);
assert_eq!(receiver.matched_count(), 1);
assert_eq!(receiver.binding_count(), 1);
receiver.reset_counters();
assert_eq!(receiver.message_count(), 0);
}
#[test]
fn test_osc_receiver_default() {
let receiver = OscReceiver::default();
assert_eq!(receiver.binding_count(), 0);
}
#[test]
fn test_osc_input_module() {
let value = Arc::new(AtomicF64::new(5.0));
let mut input = OscInput::new("/test/param", value.clone(), SignalKind::CvUnipolar);
assert_eq!(input.address(), "/test/param");
assert!((input.value_ref().get() - 5.0).abs() < 0.001);
let inputs = PortValues::new();
let mut outputs = PortValues::new();
input.tick(&inputs, &mut outputs);
assert!((outputs.get(0).unwrap() - 5.0).abs() < 0.001);
input.reset();
input.set_sample_rate(48000.0);
assert_eq!(input.type_id(), "osc_input");
}
#[test]
fn test_midi_status_parsing() {
assert_eq!(MidiStatus::from_byte(0x90), Some(MidiStatus::NoteOn(0)));
assert_eq!(MidiStatus::from_byte(0x95), Some(MidiStatus::NoteOn(5)));
assert_eq!(MidiStatus::from_byte(0x80), Some(MidiStatus::NoteOff(0)));
assert_eq!(
MidiStatus::from_byte(0xB0),
Some(MidiStatus::ControlChange(0))
);
assert_eq!(MidiStatus::from_byte(0xE0), Some(MidiStatus::PitchBend(0)));
assert_eq!(MidiStatus::from_byte(0xF0), Some(MidiStatus::System(0xF0)));
}
#[test]
fn test_midi_status_channel() {
let note_on = MidiStatus::NoteOn(5);
assert_eq!(note_on.channel(), Some(5));
let system = MidiStatus::System(0xF0);
assert_eq!(system.channel(), None);
}
#[test]
fn test_midi_note_on() {
let msg = MidiMessage::note_on(0, 60, 100);
assert!(msg.is_note_on());
assert!(!msg.is_note_off());
assert_eq!(msg.note(), 60);
assert_eq!(msg.velocity(), 100);
}
#[test]
fn test_midi_note_off() {
let msg = MidiMessage::note_off(0, 60, 0);
assert!(!msg.is_note_on());
assert!(msg.is_note_off());
}
#[test]
fn test_midi_note_on_zero_velocity() {
let msg = MidiMessage::note_on(0, 60, 0);
assert!(!msg.is_note_on());
assert!(msg.is_note_off());
}
#[test]
fn test_midi_control_change() {
let msg = MidiMessage::control_change(0, 1, 64);
assert_eq!(msg.data1, 1); assert_eq!(msg.data2, 64); }
#[test]
fn test_midi_pitch_bend() {
let msg = MidiMessage::pitch_bend(0, 0);
let normalized = msg.pitch_bend_normalized();
assert!(normalized.abs() < 0.001);
let msg = MidiMessage::pitch_bend(0, 8191);
let normalized = msg.pitch_bend_normalized();
assert!((normalized - 1.0).abs() < 0.01);
let msg = MidiMessage::pitch_bend(0, -8192);
let normalized = msg.pitch_bend_normalized();
assert!((normalized + 1.0).abs() < 0.01);
}
#[test]
fn test_midi_note_to_frequency() {
let msg = MidiMessage::note_on(0, 69, 100); assert!((msg.note_to_frequency() - 440.0).abs() < 0.01);
let msg = MidiMessage::note_on(0, 60, 100); assert!((msg.note_to_frequency() - 261.63).abs() < 0.1);
}
#[test]
fn test_midi_note_to_volt_per_octave() {
let msg = MidiMessage::note_on(0, 60, 100); assert!(msg.note_to_volt_per_octave().abs() < 0.001);
let msg = MidiMessage::note_on(0, 72, 100); assert!((msg.note_to_volt_per_octave() - 1.0).abs() < 0.001);
}
#[test]
fn test_midi_at_sample() {
let msg = MidiMessage::note_on(0, 60, 100).at_sample(64);
assert_eq!(msg.sample_offset, 64);
}
#[test]
fn test_midi_buffer() {
let mut buffer = MidiBuffer::new();
assert!(buffer.is_empty());
assert_eq!(buffer.len(), 0);
buffer.push(MidiMessage::note_on(0, 60, 100).at_sample(0));
buffer.push(MidiMessage::note_on(0, 64, 100).at_sample(32));
buffer.push(MidiMessage::note_on(0, 67, 100).at_sample(64));
assert_eq!(buffer.len(), 3);
assert!(!buffer.is_empty());
let at_0: Vec<_> = buffer.events_at(0).collect();
assert_eq!(at_0.len(), 1);
assert_eq!(at_0[0].note(), 60);
buffer.clear();
assert!(buffer.is_empty());
}
#[test]
fn test_midi_buffer_sort() {
let mut buffer = MidiBuffer::with_capacity(10);
buffer.push(MidiMessage::note_on(0, 60, 100).at_sample(64));
buffer.push(MidiMessage::note_on(0, 64, 100).at_sample(0));
buffer.push(MidiMessage::note_on(0, 67, 100).at_sample(32));
buffer.sort();
let events: Vec<_> = buffer.iter().collect();
assert_eq!(events[0].sample_offset, 0);
assert_eq!(events[1].sample_offset, 32);
assert_eq!(events[2].sample_offset, 64);
}
#[test]
fn test_midi_buffer_default() {
let buffer = MidiBuffer::default();
assert!(buffer.is_empty());
}
#[test]
fn test_plugin_wrapper_latency() {
let info = PluginInfo::effect("com.quiver.test", "Test Effect", "Quiver");
let bus = AudioBusConfig::stereo_io();
let mut wrapper = PluginWrapper::new(info, bus);
assert_eq!(wrapper.latency(), 0);
wrapper.set_latency(256);
assert_eq!(wrapper.latency(), 256);
}
#[test]
fn test_plugin_wrapper_processing_state() {
let info = PluginInfo::synth("com.quiver.test", "Test Synth", "Quiver");
let bus = AudioBusConfig::stereo_out();
let wrapper = PluginWrapper::new(info, bus);
assert!(!wrapper.is_processing());
wrapper.start_processing();
assert!(wrapper.is_processing());
wrapper.stop_processing();
assert!(!wrapper.is_processing());
}
#[test]
fn test_audio_bus_config() {
let stereo_out = AudioBusConfig::stereo_out();
assert_eq!(stereo_out.inputs, 0);
assert_eq!(stereo_out.outputs, 2);
let stereo_io = AudioBusConfig::stereo_io();
assert_eq!(stereo_io.inputs, 2);
assert_eq!(stereo_io.outputs, 2);
let mono_out = AudioBusConfig::mono_out();
assert_eq!(mono_out.inputs, 0);
assert_eq!(mono_out.outputs, 1);
}
#[test]
fn test_web_audio_block_processor_new() {
let processor = WebAudioBlockProcessor::new();
assert_eq!(processor.block_size(), 128);
assert!((processor.sample_rate() - 44100.0).abs() < 0.001);
assert!(!processor.is_active());
}
#[test]
fn test_web_audio_block_processor_with_config() {
let config = WebAudioConfig {
input_channels: 0,
output_channels: 2,
sample_rate: 48000.0,
block_size: 256,
};
let processor = WebAudioBlockProcessor::with_config(config);
assert_eq!(processor.block_size(), 256);
assert!((processor.sample_rate() - 48000.0).abs() < 0.001);
}
#[test]
fn test_web_audio_block_processor_activate() {
let mut processor = WebAudioBlockProcessor::new();
assert!(!processor.is_active());
processor.activate();
assert!(processor.is_active());
processor.deactivate();
assert!(!processor.is_active());
}
#[test]
fn test_web_audio_block_processor_parameters() {
let mut processor = WebAudioBlockProcessor::new();
let freq = processor.add_parameter("frequency", 440.0);
assert!((processor.get_parameter("frequency").unwrap() - 440.0).abs() < 0.001);
processor.set_parameter("frequency", 880.0);
assert!((freq.get() - 880.0).abs() < 0.001);
let names = processor.parameter_names();
assert!(names.contains(&"frequency".to_string()));
}
#[test]
fn test_web_audio_block_processor_process_with() {
let mut processor = WebAudioBlockProcessor::new();
let mut phase = 0.0;
let output = processor.process_with(|_i| {
let sample = (phase * std::f64::consts::TAU).sin();
phase += 440.0 / 44100.0;
(sample, sample)
});
assert_eq!(output.len(), 256);
assert!(output[0].abs() < 0.1);
}
#[test]
fn test_web_audio_block_processor_direct_buffer() {
let mut processor = WebAudioBlockProcessor::new();
{
let left = processor.left_buffer_mut();
for (i, slot) in left.iter_mut().enumerate().take(128) {
*slot = (i as f64) / 128.0;
}
}
{
let right = processor.right_buffer_mut();
for (i, slot) in right.iter_mut().enumerate().take(128) {
*slot = 1.0 - (i as f64) / 128.0;
}
}
let output = processor.finalize();
assert!(output[0].abs() < 0.01); assert!((output[1] - 1.0).abs() < 0.01);
assert!((output[254] - 127.0 / 128.0).abs() < 0.01);
assert!((output[255] - 1.0 / 128.0).abs() < 0.01);
}
#[test]
fn test_web_audio_block_processor_clear() {
let mut processor = WebAudioBlockProcessor::new();
processor.process_with(|_| (1.0, 1.0));
processor.clear();
let output = processor.finalize();
for sample in output {
assert!(*sample < 0.001);
}
}
#[test]
fn test_web_audio_block_processor_default() {
let processor = WebAudioBlockProcessor::default();
assert_eq!(processor.block_size(), 128);
}
#[test]
fn test_f64_to_f32_block() {
let src = vec![0.5_f64, -0.5, 1.0, -1.0];
let mut dst = vec![0.0_f32; 4];
f64_to_f32_block(&src, &mut dst);
assert!((dst[0] - 0.5).abs() < 0.001);
assert!((dst[1] + 0.5).abs() < 0.001);
assert!((dst[2] - 1.0).abs() < 0.001);
assert!((dst[3] + 1.0).abs() < 0.001);
}
#[test]
fn test_f32_to_f64_block() {
let src = vec![0.5_f32, -0.5, 1.0, -1.0];
let mut dst = vec![0.0_f64; 4];
f32_to_f64_block(&src, &mut dst);
assert!((dst[0] - 0.5).abs() < 0.001);
assert!((dst[1] + 0.5).abs() < 0.001);
assert!((dst[2] - 1.0).abs() < 0.001);
assert!((dst[3] + 1.0).abs() < 0.001);
}
}