Skip to content

Commit

Permalink
Hook up scripting to audscope app
Browse files Browse the repository at this point in the history
  • Loading branch information
Nico Chatzi committed Nov 21, 2023
1 parent 849fb6a commit 92d11d3
Show file tree
Hide file tree
Showing 29 changed files with 339 additions and 203 deletions.
8 changes: 4 additions & 4 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ run CMD:

# run all tests - requires `cargo-nextest`
test:
cargo nextest run --all-targets --all-features
cargo nextest run --all-targets --all-features -j{{num_cpus()}}
cargo test --doc

# run a command every time source files change - requires `cargo-watch`
dev CMD='just b':
cargo watch -cs 'reset; {{CMD}}' -i 'res/*' -i 'out/*' -i 'lua/api/examples/*'
cargo watch -cs 'reset; {{CMD}}' -i 'vhs/*' -i 'out/*' -i 'lua/api/examples/*'

# run the benchmarks
bench:
Expand All @@ -43,12 +43,12 @@ install DIR='./out': build
cp target/release/aud {{DIR}}/aud
_tape CMD:
vhs res/vhs/{{CMD}}.tape
vhs vhs/{{CMD}}.tape

# create CLI recordings - requires `vhs` & `parallel`
tape:
#!/usr/bin/env bash
parallel --ungroup vhs ::: res/vhs/*
parallel --ungroup vhs ::: vhs/*
# tail a log file - request `bat`
log FILE='./out/aud.log':
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ scriptable <code>aud</code>io terminal tools
</p>

<p align="center">
<img src="./res/out/scope_loop.gif">
<img src="./vhs/out/scope_loop.gif">
</p>

🧱 `Requires`: [Rust](https://www.rust-lang.org/tools/install) and [Just](https://github.com/casey/just)
Expand All @@ -20,15 +20,15 @@ scriptable <code>aud</code>io terminal tools

After installing, you can generate and install terminal auto-completions scripts.

![aud](./res/out/aud.gif)
![aud](./vhs/out/aud.gif)

<h2 align="center"><code>commands</code></h2>

### `midimon`

Scriptable MIDI Monitor.

![midimon](./res/out/midimon.gif)
![midimon](./vhs/out/midimon.gif)

### `auscope`

Expand All @@ -38,10 +38,10 @@ By default `auscope` lists the host machine's audio devices.
`aud_lib` can integrated in other applications (Rust or through C-FFI)
to generate sources and send them over UDP to an `auscope` instance.

![auscope](./res/out/auscope.gif)
![auscope](./vhs/out/auscope.gif)

### `derlink`

Simple Ableton Link Client.

![derlink](./res/out/derlink.gif)
![derlink](./vhs/out/derlink.gif)
15 changes: 14 additions & 1 deletion aud/cli/src/auscope/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ impl TerminalApp {
impl crate::app::Base for TerminalApp {
fn update(&mut self) -> anyhow::Result<crate::app::Flow> {
self.app.fetch_audio()?;
self.app.process_engine_events()?;

if self.app.process_script_events()? == AppEvent::Stopping {
return Ok(crate::app::Flow::Exit);
}

if self.app.process_file_events()? == AppEvent::ScriptLoaded {
self.ui.clear_script_cache();
}

Ok(crate::app::Flow::Continue)
}

Expand All @@ -48,7 +58,10 @@ impl crate::app::Base for TerminalApp {
ui::Selector::Script => Ok(crate::app::Flow::Continue),
},
ui::UiEvent::LoadScript(index) => {
self.try_connect_to_audio_input(index)?;
if let Some(script_name) = &self.ui.scripts().get(index) {
let script = self.ui.script_dir().unwrap().join(script_name);
self.app.load_script(script)?;
};
Ok(crate::app::Flow::Continue)
}
}
Expand Down
39 changes: 38 additions & 1 deletion aud/cli/src/auscope/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub struct Ui {
script_dir: Option<std::path::PathBuf>,
script_names: Vec<String>,
alert_message: Option<String>,
cached_script: Option<String>,
downsample: usize,
gain: f32,
}
Expand All @@ -67,6 +68,7 @@ impl Default for Ui {
script_dir: None,
script_names: vec![],
alert_message: None,
cached_script: None,
downsample: 16,
gain: 1.,
}
Expand All @@ -76,6 +78,18 @@ impl Default for Ui {
impl Ui {
const SAMPLE_RATE: usize = 48000;

pub fn scripts(&self) -> &[String] {
self.script_names.as_slice()
}

pub fn script_dir(&self) -> Option<&std::path::PathBuf> {
self.script_dir.as_ref()
}

pub fn clear_script_cache(&mut self) {
self.cached_script = None;
}

pub fn update_script_dir(&mut self, dir: impl AsRef<std::path::Path>) -> anyhow::Result<()> {
let dir = dir.as_ref();
self.script_names = files::list_with_extension(dir, "lua")?;
Expand Down Expand Up @@ -123,7 +137,10 @@ impl Ui {
KeyCode::Enter => {
if let Some(selection) = self.selectors.select() {
return match selection.selector {
Selector::Script => UiEvent::LoadScript(selection.index),
Selector::Script => {
self.cached_script = None;
UiEvent::LoadScript(selection.index)
}
Selector::Device => UiEvent::Select {
id: selection.selector,
index: selection.index,
Expand Down Expand Up @@ -234,6 +251,26 @@ impl Ui {
self.popups
.render(f, Popup::Script, crate::title!(""), "No script loaded");
}

let selected_script_name = match app.selected_script() {
Some(name) => crate::title!("script : {}", name),
None => "".to_owned(),
};

if self.cached_script.is_none() {
self.cached_script = Some(
app.loaded_script_path()
.and_then(|path| std::fs::read_to_string(path).ok())
.unwrap_or_else(|| "No script loaded".to_owned()),
);
}

self.popups.render(
f,
Popup::Script,
&selected_script_name,
self.cached_script.as_ref().unwrap(),
);
}

pub fn remove_offscreen_samples(&mut self, app: &mut App, screen_width: usize, fps: f32) {
Expand Down
2 changes: 1 addition & 1 deletion aud/lib/benches/host_audio_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn bench_dequeue(c: &mut Criterion) {
total_channels,
selected_channels,
);
std::vec::from_elem(buffer_size * total_channels, 0.0);
std::vec::from_elem(buffer_size * total_channels, 0);
let mut audio_buffer: Vec<f32> = vec![0.0; buffer_size * total_channels];

b.iter(|| {
Expand Down
74 changes: 72 additions & 2 deletions aud/lib/src/apps/auscope/app.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use std::path::Path;
use std::path::{Path, PathBuf};

use crate::{
audio::*,
comms::{SocketInterface, Sockets},
lua::{imported, HostEvent, ScriptController},
lua::{
imported,
traits::api::{ConnectionApiEvent, LogApiEvent},
HostEvent, LuaEngineEvent, ScriptController, ScriptEvent,
},
};
use crossbeam::channel;

Expand Down Expand Up @@ -50,6 +54,14 @@ impl App {
self.alert_message.take()
}

pub fn selected_script(&self) -> Option<&str> {
self.selected_script_name.as_deref()
}

pub fn loaded_script_path(&self) -> Option<&PathBuf> {
self.script.path()
}

pub fn audio(&self) -> &AudioBuffer {
&self.buffer
}
Expand Down Expand Up @@ -133,6 +145,64 @@ impl App {

Ok(AppEvent::Continue)
}

pub fn process_script_events(&mut self) -> anyhow::Result<AppEvent> {
while let Ok(script_event) = self.script.try_recv() {
match script_event {
ScriptEvent::Loaded => return Ok(AppEvent::ScriptLoaded),
ScriptEvent::Log(request) => self.handle_lua_log_request(request),
ScriptEvent::Connect(request) => self.handle_lua_connect_request(request)?,
_ => (),
}
}

Ok(AppEvent::Continue)
}

pub fn process_file_events(&mut self) -> anyhow::Result<AppEvent> {
if self.script.was_script_modified()? && self.script.path().is_some() {
self.load_script(self.script.path().unwrap().clone())
} else {
Ok(AppEvent::Continue)
}
}

pub fn process_engine_events(&mut self) -> anyhow::Result<AppEvent> {
while let Ok(event) = self.script.try_recv_engine_events() {
match event {
LuaEngineEvent::Panicked => return Ok(AppEvent::ScriptCrash),
LuaEngineEvent::Terminated => log::info!("Lua Engine terminated"),
}
}
Ok(AppEvent::Continue)
}

fn handle_lua_connect_request(&mut self, request: ConnectionApiEvent) -> anyhow::Result<()> {
let ConnectionApiEvent { ref device } = request;

let device = self
.devices()
.iter()
.find(|dev| dev.name == *device)
.cloned();

if let Some(device) = device {
let channels = self
.selected_channels
.clone()
.unwrap_or(AudioChannelSelection::Mono(0));
self.connect_to_audio_input(&device, channels)?;
}

Ok(())
}

fn handle_lua_log_request(&mut self, request: LogApiEvent) {
match request {
LogApiEvent::Log(msg) => log::info!("{msg}"),
LogApiEvent::Alert(msg) => self.alert_message = Some(msg),
}
}
}

// Pipes audio received from the remote into the provider.
Expand Down
2 changes: 1 addition & 1 deletion doc/midimon.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<h1 align="center"><code>midimon</code></h1>
<p align="center">Scriptable MIDI Monitor</p>

![midimon](../res/out/midimon.gif)
![midimon](../vhs/out/midimon.gif)

## Usage

Expand Down
1 change: 0 additions & 1 deletion lua/api/sysexio/docs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,3 @@ function resume() end

-- Request to stop the application
function stop() end

42 changes: 21 additions & 21 deletions lua/aud/log.lua
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
local M = {}

function M.print_table_or_user_data(t, indent)
indent = indent or 0
local t_type = type(t)
indent = indent or 0
local t_type = type(t)

if t_type == "table" then
for k, v in pairs(t) do
if type(v) == "table" or type(v) == "userdata" then
print(string.rep(" ", indent) .. k .. ":")
M.print_table_or_user_data(v, indent + 1)
else
print(string.rep(" ", indent) .. k .. ": " .. tostring(v))
end
end
elseif t_type == "userdata" then
local meta = getmetatable(t)
if meta and meta.__tostring then
print(string.rep(" ", indent) .. tostring(t))
else
print(string.rep(" ", indent) .. "userdata")
end
else
print(string.rep(" ", indent) .. tostring(t))
end
if t_type == "table" then
for k, v in pairs(t) do
if type(v) == "table" or type(v) == "userdata" then
print(string.rep(" ", indent) .. k .. ":")
M.print_table_or_user_data(v, indent + 1)
else
print(string.rep(" ", indent) .. k .. ": " .. tostring(v))
end
end
elseif t_type == "userdata" then
local meta = getmetatable(t)
if meta and meta.__tostring then
print(string.rep(" ", indent) .. tostring(t))
else
print(string.rep(" ", indent) .. "userdata")
end
else
print(string.rep(" ", indent) .. tostring(t))
end
end

return M
30 changes: 15 additions & 15 deletions lua/aud/midi.lua
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
local M = {}

M.headers = {
sysex_start = 0xF0,
sysex_end = 0xF7,
note_on = 0x90,
note_off = 0x80,
poly_pressure = 0xA0,
controller = 0xB0,
program_change = 0xC0,
channel_pressure = 0xD0,
pitch_bend = 0xE0,
sysex_start = 0xF0,
sysex_end = 0xF7,
note_on = 0x90,
note_off = 0x80,
poly_pressure = 0xA0,
controller = 0xB0,
program_change = 0xC0,
channel_pressure = 0xD0,
pitch_bend = 0xE0,
}

function M.parse_header(bytes)
for header in M.headers do
if bytes[0] & header ~= 0 then
return header
end
end
for header in M.headers do
if bytes[0] & header ~= 0 then
return header
end
end

return nil
return nil
end

return M
Loading

0 comments on commit 92d11d3

Please sign in to comment.