Skip to content

Commit

Permalink
organelle: Use JACK
Browse files Browse the repository at this point in the history
CPAL cannot be used on Organelle.

  * Using default audio device gives a EPIPE error in snd_pcm_start()
  * Specifying device `default=CARD=Codec` fails because of
    RustAudio/cpal#615
  * Using JACK backend fails for reasons I didn't investigate - the
    `feedback` example fails with 'unsupported configuration' even
    though it's clearly using a supported configuration.

JACK is available on Organelle and can play sound, we just need to start
it in the run.sh script.

I don't have time to fix any of the CPAL problems so let's switch to
JACK for everything.

This can also be tested locally on desktop, but see:
RustAudio/rust-jack#142
  • Loading branch information
ssssam committed Nov 21, 2021
1 parent d00215a commit 4b6d675
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 86 deletions.
8 changes: 3 additions & 5 deletions organelle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ include = ["data/*"]

[dependencies]
boucle = { path = "../boucle" }
clap = "2"
# We pin cpal version to work around https://github.com/RustAudio/cpal/issues/606
cpal = "=0.13.3"
env_logger = "^0.9.0"
jack = { version = "^0.8.1", optional = true }
log = "^0.4"
nannou_osc = { version = "0.17.0", optional = true }
nannou_osc = { version = "^0.17.0", optional = true }

[features]
default = ["organelle"]
organelle = ["nannou_osc"]
organelle = ["jack", "nannou_osc"]

[[bin]]
name = "boucle_organelle"
Expand Down
39 changes: 37 additions & 2 deletions organelle/data/run.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
#!/bin/sh
#!/bin/bash

./boucle_organelle
set -em

echo "boucle run.sh"

FW_SCRIPTS_DIR=/root/fw_dir/scripts

$FW_SCRIPTS_DIR/start-jack.sh
echo "Started jackd as $(cat /tmp/pids/jack.pid)"

jack_wait --wait

export RUST_LOG=warn
./boucle_organelle &
echo $! > /tmp/pids/boucle_organelle.pid
echo "Started boucle_organelle as $(cat /tmp/pids/boucle_organelle.pid)"

trap 'echo "Received SIGINT"; $FW_SCRIPTS_DIR/killpatch.sh; jack_wait --quit' SIGINT
trap 'echo "Received EXIT/ERR/TERM"; $FW_SCRIPTS_DIR/killpatch.sh; jack_wait --quit' EXIT ERR SIGTERM

while ! jack_lsp boucle | grep --silent boucle:boucle_in; do
echo "Waiting for patch audio ports..."
jack_lsp boucle
sleep 1
done
# Sleep after ports appear so they are active: otherwise you see errors:
#
# cannot connect ports owned by inactive clients; "boucle" is not active
sleep 1

echo "Connecting ports"
jack_connect boucle:boucle_in system:capture_1
jack_connect boucle:boucle_in system:capture_2
jack_connect boucle:boucle_out system:playback_1
jack_connect boucle:boucle_out system:playback_2

fg %1
253 changes: 189 additions & 64 deletions organelle/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};

use clap::{Arg, App};
use cpal::traits::{DeviceTrait, HostTrait};
use log::*;
use nannou_osc as osc;

use boucle::BeatFraction;
use boucle::Boucle;
use boucle::buffers::LoopBuffers;
use boucle::buffers::{Buffer, InputBuffer, LoopBuffers};
use boucle::event::StateChange;
use boucle::Operation;
use crate::patch_error::PatchError;
Expand Down Expand Up @@ -83,6 +81,93 @@ fn map_key(key: i32) -> Operation {
}


struct JackNotifications;

impl jack::NotificationHandler for JackNotifications {
fn thread_init(&self, _: &jack::Client) {
info!("JACK: thread init");
}

fn shutdown(&mut self, status: jack::ClientStatus, reason: &str) {
info!(
"JACK: shutdown with status {:?} because \"{}\"",
status, reason
);
}

fn freewheel(&mut self, _: &jack::Client, is_enabled: bool) {
info!(
"JACK: freewheel mode is {}",
if is_enabled { "on" } else { "off" }
);
}

fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control {
// FIXME: We need to handle this.
info!("JACK: sample rate changed to {}", srate);
jack::Control::Continue
}

fn client_registration(&mut self, _: &jack::Client, name: &str, is_reg: bool) {
info!(
"JACK: {} client with name \"{}\"",
if is_reg { "registered" } else { "unregistered" },
name
);
}

fn port_registration(&mut self, _: &jack::Client, port_id: jack::PortId, is_reg: bool) {
info!(
"JACK: {} port with id {}",
if is_reg { "registered" } else { "unregistered" },
port_id
);
}

fn port_rename(
&mut self,
_: &jack::Client,
port_id: jack::PortId,
old_name: &str,
new_name: &str,
) -> jack::Control {
info!(
"JACK: port with id {} renamed from {} to {}",
port_id, old_name, new_name
);
jack::Control::Continue
}

fn ports_connected(
&mut self,
_: &jack::Client,
port_id_a: jack::PortId,
port_id_b: jack::PortId,
are_connected: bool,
) {
info!(
"JACK: ports with id {} and {} are {}",
port_id_a,
port_id_b,
if are_connected {
"connected"
} else {
"disconnected"
}
);
}

fn graph_reorder(&mut self, _: &jack::Client) -> jack::Control {
info!("JACK: graph reordered");
jack::Control::Continue
}

fn xrun(&mut self, _: &jack::Client) -> jack::Control {
info!("JACK: xrun occurred");
jack::Control::Continue
}
}

impl Patch {
pub fn new() -> Result<Self, PatchError> {
let boucle_config = boucle::Config {
Expand Down Expand Up @@ -111,53 +196,112 @@ impl Patch {
});
}

pub fn run(self: &mut Self,
input_device_name: Option<&str>,
output_device_name: Option<&str>) -> Result<(), PatchError> {
let audio_host = cpal::default_host();
info!("cpal host: {:?}, input: {:?}, output: {:?}", audio_host.id(), input_device_name, output_device_name);

let audio_out = match output_device_name {
Some(name) => audio_host.output_devices()?
.find(|d| {
info!("matching {} with {:?}", name, d.name());
name == d.name().unwrap_or("".to_string())
})
.expect(format!("no output device found matching '{}'", name).as_str()),
None => audio_host.default_output_device()
.expect("no output device available"),
};
pub fn run(self: &mut Self) -> Result<(), PatchError> {
let (client, _status) =
jack::Client::new("boucle", jack::ClientOptions::NO_START_SERVER).unwrap();

let audio_in = match input_device_name {
Some(name) => audio_host.input_devices()?
.find(|d| name == d.name().unwrap_or("".to_string()))
.expect(format!("no input device found matching '{}'", name).as_str()),
None => audio_host.default_input_device()
.expect("no input device available"),
};
let in_port = client
.register_port("boucle_in", jack::AudioIn::default())
.unwrap();
let mut out_port = client
.register_port("boucle_out", jack::AudioOut::default())
.unwrap();

self.signal_loaded();
self.update_screen();

let boucle_rc = self.boucle_rc.clone();
let buffers_rc = self.buffers_rc.clone();
let process_callback = move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control {
let mut boucle = boucle_rc.lock().unwrap();
let mut buffers = buffers_rc.lock().unwrap();
let mut play_clock = buffers.play_clock.clone();
let mut record_pos = buffers.record_pos.clone();
let loop_length = boucle.loop_length();

// Read input into buffer
{
let mut record_buffer: &mut Buffer = match buffers.current_input {
InputBuffer::A => &mut buffers.input_a,
InputBuffer::B => &mut buffers.input_b,
};

for &s in in_port.as_slice(ps) {
record_buffer[record_pos] = s;
record_pos += 1;
if record_pos >= loop_length {
if buffers.current_input == InputBuffer::A {
buffers.current_input = InputBuffer::B;
record_buffer = &mut buffers.input_b;
} else {
buffers.current_input = InputBuffer::A;
record_buffer = &mut buffers.input_a;
}
debug!("Record buffer flip");
record_pos = 0;
}
}
}

let supported_audio_config = boucle::cpal_helpers::get_audio_config(&self.boucle_rc.lock().unwrap(), &audio_out);
buffers.record_pos = record_pos;

let out_buf = out_port.as_mut_slice(ps);
{
let mut in_buffer = match buffers.current_output {
InputBuffer::A => &buffers.input_a,
InputBuffer::B => &buffers.input_b,
};

let mut out_pos = 0;
let play_pos = play_clock % loop_length;
let span = std::cmp::min(loop_length - play_pos, out_buf.len());
debug!("Play clock {} pos {}/{} span {} (total data {})", play_clock, play_pos, loop_length, span, out_buf.len());

let ops = boucle.event_recorder.ops_for_period(play_clock, span);
boucle.process_buffer(&in_buffer, play_clock, span,
&ops, &mut |s| {
out_buf[out_pos] = s;
out_pos += 1;
});
play_clock += span;

if out_pos < out_buf.len() {
// Flip buffer and continue
let span_2 = out_buf.len() - span;
debug!("play buffer flip");

if buffers.current_output == InputBuffer::A {
buffers.current_output = InputBuffer::B;
in_buffer = &mut buffers.input_b;
} else {
buffers.current_output = InputBuffer::A;
in_buffer = &mut buffers.input_a;
}

let ops = boucle.event_recorder.ops_for_period(play_clock, span_2);
boucle.process_buffer(&in_buffer, play_clock, span_2,
&ops, &mut |s| {
out_buf[out_pos] = s;
out_pos += 1;
});
play_clock += span_2;
}
}

let sample_format = supported_audio_config.sample_format();
info!("Sample format: {:?}", sample_format);
buffers.play_clock = play_clock;

let input_audio_config: cpal::StreamConfig = supported_audio_config.clone().into();
let _audio_in_stream = match sample_format {
// FIXME: do we need to support all these? Organelle should give us same each time.
cpal::SampleFormat::F32 => boucle::cpal_helpers::open_in_stream::<f32>(audio_in, input_audio_config, self.buffers_rc.clone()),
cpal::SampleFormat::I16 => boucle::cpal_helpers::open_in_stream::<i16>(audio_in, input_audio_config, self.buffers_rc.clone()),
cpal::SampleFormat::U16 => boucle::cpal_helpers::open_in_stream::<u16>(audio_in, input_audio_config, self.buffers_rc.clone()),
// Performer responds to what they hear.
// The MIDI events we receive are therefore treated as relative
// to the last thing the performer heard.
boucle.event_recorder.set_event_sync_point(Instant::now(), play_clock);
jack::Control::Continue
};

let output_audio_config: cpal::StreamConfig = supported_audio_config.into();
let _audio_out_stream = match sample_format {
cpal::SampleFormat::F32 => boucle::cpal_helpers::open_out_stream::<f32>(audio_out, output_audio_config, self.boucle_rc.clone(), self.buffers_rc.clone()),
cpal::SampleFormat::I16 => boucle::cpal_helpers::open_out_stream::<i16>(audio_out, output_audio_config, self.boucle_rc.clone(), self.buffers_rc.clone()),
cpal::SampleFormat::U16 => boucle::cpal_helpers::open_out_stream::<u16>(audio_out, output_audio_config, self.boucle_rc.clone(), self.buffers_rc.clone()),
};
let _active_client = client.activate_async(
JackNotifications,
jack::ClosureProcessHandler::new(process_callback),
).unwrap();

self.signal_loaded();
self.update_screen();
loop {
let update_screen = self.process_events();

Expand Down Expand Up @@ -218,7 +362,7 @@ impl Patch {
}

match message.addr.as_str() {
"/keys" => {
"/key" => {
if let [osc::Type::Int(key), osc::Type::Int(pressed)] = args(message) {
if *key >= 0 && *key <= 24 {
return self.handle_key(*key, *pressed >= 1);
Expand Down Expand Up @@ -283,29 +427,10 @@ impl Patch {
pub fn main() {
env_logger::init();

let app_m = App::new("Boucle looper for Organelle")
.version("1.0")
.arg(Arg::with_name("input-device")
.long("input-device")
.short("i")
.help("Record audio from device")
.takes_value(true)
.value_name("NAME"))
.arg(Arg::with_name("output-device")
.long("output-device")
.short("o")
.help("Play audio to device")
.takes_value(true)
.value_name("NAME"))
.get_matches();

let audio_in = app_m.value_of("input-device");
let audio_out = app_m.value_of("output-device");

let mut patch = Patch::new()
.map_err(|e| panic!("{}", e.message))
.unwrap();
patch.run(audio_in, audio_out)
patch.run()
.map_err(|e| panic!("{}", e.message))
.unwrap();
}
15 changes: 0 additions & 15 deletions organelle/src/patch_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,6 @@ impl fmt::Display for PatchError {
}
}

impl From<cpal::DevicesError> for PatchError {
fn from(error: cpal::DevicesError) -> Self {
PatchError {
message: error.to_string(),
}
}
}

impl From<cpal::DeviceNameError> for PatchError {
fn from(error: cpal::DeviceNameError) -> Self {
PatchError {
message: error.to_string(),
}
}
}
impl From<io::Error> for PatchError {
fn from(error: io::Error) -> Self {
PatchError {
Expand Down

0 comments on commit 4b6d675

Please sign in to comment.