Skip to content

Commit

Permalink
Add zoom and gain to auscope
Browse files Browse the repository at this point in the history
  • Loading branch information
Nico Chatzi committed Nov 21, 2023
1 parent de74405 commit de458fe
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 79 deletions.
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ scriptable <code>aud</code>io terminal tools

🌶️ `Scriptable`: in [Lua](https://www.lua.org/start.html), with `hooks`, `hot-reloading` and `sandboxed panics`

🔨 `Install`: `just install <INSTALL_DIR>`: build `aud` and install it on your system
🔨 `Install`: `just install <INSTALL_DIR>`: build and install `aud` on your system

💻 `Contribute`: `just setup`: setup development environment for this project

📚 `Learn`: [Docs](./doc/readme.md) for all commands

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

After installing, you can generate and install terminal auto-completions scripts.
Expand All @@ -28,24 +26,22 @@ After installing, you can generate and install terminal auto-completions scripts

### `midimon`

MIDI Monitor:
- Select a MIDI input device to open the stream
- Hit `spacebar` to pause.
Scriptable MIDI Monitor.

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

### `auscope`

Audio Oscilloscope:
Scriptable Audio Oscilloscope.

- Select an audio source to open the stream.
- Use the core library in Rust (or through C-FFI API) to produce sources.
- Sources can be sent over UDP.
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)

### `derlink`

Ableton Link Client
Simple Ableton Link Client.

![derlink](./res/out/derlink.gif)
30 changes: 17 additions & 13 deletions aud/cli/src/auscope/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
mod ui;

use std::net::UdpSocket;

use aud::{apps::auscope::*, audio::*, comms::Sockets};
use ratatui::prelude::*;
use std::net::UdpSocket;

struct TerminalApp {
app: App,
Expand All @@ -21,15 +20,13 @@ impl TerminalApp {

fn try_connect_to_audio_input(&mut self, index: usize) -> anyhow::Result<()> {
let Some(device) = self.app.devices().get(index) else {
log::warn!(
"Invalid device index selection {index}, with {} devices",
self.app.devices().len()
);
let num_devices = self.app.devices().len();
log::warn!("Invalid device index selection {index}, with {num_devices} devices",);
return Ok(());
};

self.app
.connect_to_audio_input(&device.clone(), AudioChannelSelection::Mono(0))
let channels = AudioChannelSelection::Mono(0);
self.app.connect_to_audio_input(&device.clone(), channels)
}
}

Expand All @@ -50,11 +47,21 @@ 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)?;
Ok(crate::app::Flow::Continue)
}
}
}

fn render(&mut self, f: &mut Frame) {
self.ui.render(f, &mut self.app, self.fps);
if let Some(alert) = self.app.take_alert() {
self.ui.show_alert_message(&alert);
}

self.ui.render(f, &self.app);
self.ui
.remove_offscreen_samples(&mut self.app, f.size().width as usize, self.fps);
}
}

Expand Down Expand Up @@ -99,10 +106,7 @@ fn create_remote_audio_provider(address: String, ports: String) -> Box<dyn Audio
target: format!("{address}:{in_port}").parse().unwrap(),
};

let provider =
RemoteAudioProvider::new(sockets).expect("failed to create remote audio receiver");

Box::new(provider)
Box::new(RemoteAudioProvider::new(sockets).unwrap())
}

pub fn run(
Expand Down
131 changes: 99 additions & 32 deletions aud/cli/src/auscope/ui.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
use crate::ui::{components, widgets};
use aud::{
apps::auscope::App,
audio::AudioDevice,
files,
lua::imported::auscope::{API, DOCS},
};
use aud::{apps::auscope::App, audio::AudioDevice, files};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::prelude::*;

const USAGE: &str = r#"
? : display help
a : display API
s : display script
d : display docs
K : increase gain
J : decrease gain
H : zoom out
L : zoom in
<UP>, k : scroll up
<DOWN>, j : scroll down
<LEFT>, h : cycle panes left
<RIGHT>, l : cycle panes right
Enter : confirm selection
<ESC>, q : quit or hide help
<C-c> : force quit
Expand All @@ -34,15 +38,19 @@ pub enum Selector {

pub enum UiEvent<Id> {
Continue,
Exit,
Select { id: Id, index: usize },
LoadScript(usize),
Exit,
}

pub struct Ui {
popups: components::Popups<Popup>,
selectors: components::Selectors<Selector>,
script_dir: Option<std::path::PathBuf>,
script_names: Vec<String>,
alert_message: Option<String>,
downsample: usize,
gain: f32,
}

impl Default for Ui {
Expand All @@ -58,11 +66,16 @@ impl Default for Ui {
selectors: components::Selectors::new(&[Selector::Device, Selector::Script]),
script_dir: None,
script_names: vec![],
alert_message: None,
downsample: 16,
gain: 1.,
}
}
}

impl Ui {
const SAMPLE_RATE: usize = 48000;

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 All @@ -79,24 +92,42 @@ impl Ui {
};
}

fn adjust_gain(&mut self, amount: f32) {
self.gain = (self.gain + amount).clamp(0., 16.);
}

fn adjust_downsample(&mut self, amount: isize) {
self.downsample = (self.downsample as isize + amount).clamp(16, 4096) as usize;
}

pub fn on_keypress(&mut self, key: KeyEvent) -> UiEvent<Selector> {
match key.code {
KeyCode::Char('?') => self.popups.toggle_visible(Popup::Usage),
KeyCode::Char('a') => self.popups.toggle_visible(Popup::Api),
KeyCode::Char('s') => self.popups.toggle_visible(Popup::Script),
KeyCode::Char('d') => self.popups.toggle_visible(Popup::Docs),
KeyCode::Char('q') | KeyCode::Esc => {
if !self.popups.any_visible() {
return UiEvent::Exit;
}
self.popups.hide()
}
KeyCode::Down | KeyCode::Char('j') => self.selectors.next_item(),
KeyCode::Char('K') => self.adjust_gain(0.1),
KeyCode::Char('J') => self.adjust_gain(-0.1),
KeyCode::Char('H') => self.adjust_downsample(-16),
KeyCode::Char('L') => self.adjust_downsample(16),
KeyCode::Up | KeyCode::Char('k') => self.selectors.previous_item(),
KeyCode::Down | KeyCode::Char('j') => self.selectors.next_item(),
KeyCode::Left | KeyCode::Char('h') => self.selectors.previous_selector(),
KeyCode::Right | KeyCode::Char('l') => self.selectors.next_selector(),
KeyCode::Enter => {
if let Some(selection) = self.selectors.select() {
return UiEvent::Select {
id: selection.selector,
index: selection.index,
return match selection.selector {
Selector::Script => UiEvent::LoadScript(selection.index),
Selector::Device => UiEvent::Select {
id: selection.selector,
index: selection.index,
},
};
}
}
Expand All @@ -106,7 +137,12 @@ impl Ui {
UiEvent::Continue
}

pub fn render(&mut self, f: &mut Frame, app: &mut App, fps: f32) {
pub fn show_alert_message(&mut self, alert_message: &str) {
self.popups.show(Popup::Alert);
self.alert_message = Some(alert_message.into());
}

pub fn render(&mut self, f: &mut Frame, app: &App) {
let sections = Layout::default()
.direction(Direction::Horizontal)
.margin(1)
Expand Down Expand Up @@ -147,33 +183,64 @@ impl Ui {
);
}

self.popups.render(f, Popup::Api, crate::title!("api"), API);
self.popups
.render(f, Popup::Docs, crate::title!("docs"), DOCS);
let selected_device_name = self
.selectors
.get(Selector::Device)
.and_then(|s| s.selected().and_then(|index| app.devices().get(index)))
.map(|device| device.name.clone())
.unwrap_or_default();

let scope_tile = format!(
"{}───{}─{}",
crate::title!("{}", selected_device_name),
crate::title!("zoom : {}", self.downsample),
crate::title!("gain : {:.2}", self.gain),
);

widgets::scope::render(
f,
sections[1],
&scope_tile,
app.audio(),
self.downsample,
self.gain,
);

self.popups.render(
f,
Popup::Api,
crate::title!("api"),
aud::lua::imported::auscope::API,
);

self.popups.render(
f,
Popup::Docs,
crate::title!("docs"),
aud::lua::imported::auscope::DOCS,
);

self.popups
.render(f, Popup::Usage, crate::title!("usage"), USAGE);

// self.popups.render(f, Popup::Script, );
// self.popups.render(f, Popup::Aler, );

let selected_device_name = match self.selectors.get(Selector::Device) {
Some(s) => s
.selected()
.and_then(|index| app.devices().get(index))
.map(|device| crate::title!("{}", device.name))
.unwrap_or_else(|| "".to_owned()),
None => "".to_owned(),
};
self.popups.render(
f,
Popup::Alert,
crate::title!("alert!"),
self.alert_message.as_ref().unwrap_or(&"".to_owned()),
);

const DOWNSAMPLE: usize = 128;
const SAMPLE_RATE: usize = 48000;
if !self.popups.is_visible(Popup::Script) {
self.popups
.render(f, Popup::Script, crate::title!(""), "No script loaded");
}
}

pub fn remove_offscreen_samples(&mut self, app: &mut App, screen_width: usize, fps: f32) {
let audio = app.audio_mut();
widgets::scope::render(f, sections[1], &selected_device_name, audio, 128);

let num_renderable_samples = f.size().width as usize * DOWNSAMPLE;
let num_renderable_samples = screen_width * self.downsample;
let num_samples_to_purge =
((SAMPLE_RATE as f32 / fps) * audio.num_channels as f32) as usize;
((Self::SAMPLE_RATE as f32 / fps) * audio.num_channels as f32) as usize;

if audio.data.len() > num_renderable_samples {
let num_samples_to_purge =
Expand Down
25 changes: 16 additions & 9 deletions aud/cli/src/midimon/ui.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
use crate::ui::{components, widgets};
use aud::{
apps::midimon::App,
files,
lua::imported::midimon::{API, DOCS},
};
use aud::{apps::midimon::App, files};
use crossterm::event::KeyCode;
use ratatui::prelude::*;
use std::path::Path;
Expand All @@ -18,7 +14,6 @@ const USAGE: &str = r#"
<DOWN>, j : scroll down
<LEFT>, h : cycle panes left
<RIGHT>, l : cycle panes right
<DOWN>, j : scroll down
Enter : confirm selection
<ESC>, q : quit or hide popup
<C-c> : force quit
Expand Down Expand Up @@ -213,11 +208,23 @@ impl Ui {
sections[1],
);

self.popups.render(f, Popup::Api, crate::title!("api"), API);
self.popups
.render(f, Popup::Docs, crate::title!("docs"), DOCS);
self.popups.render(
f,
Popup::Api,
crate::title!("api"),
aud::lua::imported::midimon::API,
);

self.popups.render(
f,
Popup::Docs,
crate::title!("docs"),
aud::lua::imported::midimon::DOCS,
);

self.popups
.render(f, Popup::Usage, crate::title!("usage"), USAGE);

self.popups.render(
f,
Popup::Alert,
Expand Down
Loading

0 comments on commit de458fe

Please sign in to comment.