Compare commits
No commits in common. "ae6ead454ebda6a85048f477af0703dce2b6a45f" and "ae76e39d2ee060fd1032ed5d09325e445d4ea5da" have entirely different histories.
ae6ead454e
...
ae76e39d2e
4 changed files with 48 additions and 184 deletions
22
.github/workflows/rust.yml
vendored
22
.github/workflows/rust.yml
vendored
|
|
@ -1,22 +0,0 @@
|
||||||
name: Rust
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --verbose
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --verbose
|
|
||||||
|
|
@ -9,4 +9,3 @@ eframe = "0.29.1"
|
||||||
egui = "0.29.1"
|
egui = "0.29.1"
|
||||||
egui_flex = "0.1.1"
|
egui_flex = "0.1.1"
|
||||||
jack = "0.13.0"
|
jack = "0.13.0"
|
||||||
native-dialog = "0.7.0"
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
# FSLCMix (Simple Jack Mixer)
|
# FSLCMix (Simple Jack Equalizer)
|
||||||
|
|
||||||
Made for my Linux Audio presentation for the USU Free Software and Linux Club (FSLC). Supports an arbitrary number of channels (default 5), but can be specified on the args via the `-c`/`--channels` argument. The exercise is to check out the `jackless` branch and re-add the JACK functionality without peeking at the `main` branch.
|
Made for my Linux Audio presentation for the USU Free Software and Linux Club (FSLC). Supports an arbitrary number of channels (default 5), but can be specified on the args via the `-c`/`--channels` argument. The exercise is to check out the `jackless` branch and re-add the JACK functionality without peeking at the `main` branch.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- RMS and Peak Tracking
|
|
||||||
- Per-track Mute, Solo and Gain
|
|
||||||
- Per-track hard limiting
|
|
||||||
|
|
||||||
To build:
|
To build:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
201
src/main.rs
201
src/main.rs
|
|
@ -6,7 +6,7 @@ use eframe::egui::*;
|
||||||
const PEAK_HOLD_TIME: usize = 4000;
|
const PEAK_HOLD_TIME: usize = 4000;
|
||||||
const DECAY_FACTOR: f32 = 0.9999;
|
const DECAY_FACTOR: f32 = 0.9999;
|
||||||
|
|
||||||
use std::{process::exit, sync::{Arc, Mutex}};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
@ -14,54 +14,32 @@ fn main() -> eframe::Result {
|
||||||
let app = MixApp {
|
let app = MixApp {
|
||||||
mix : shared_mix.clone(),
|
mix : shared_mix.clone(),
|
||||||
};
|
};
|
||||||
if let Ok((client, _status)) = jack::Client::new("fslcmix", jack::ClientOptions::default()) {
|
let options = eframe::NativeOptions {
|
||||||
let options = eframe::NativeOptions {
|
viewport: egui::ViewportBuilder::default()
|
||||||
viewport: egui::ViewportBuilder::default()
|
.with_inner_size([500.0, 350.0])
|
||||||
.with_inner_size([500.0, 350.0])
|
.with_min_inner_size([300.0, 350.0])
|
||||||
.with_min_inner_size([300.0, 350.0])
|
.with_max_inner_size([5000.0, 350.0]),
|
||||||
.with_max_inner_size([5000.0, 350.0]),
|
..Default::default()
|
||||||
..Default::default()
|
};
|
||||||
};
|
let (client, _status) = jack::Client::new("fslcmix", jack::ClientOptions::default()).unwrap();
|
||||||
|
let process_callback = register_jack_callback(&client, shared_mix);
|
||||||
let process_callback = register_jack_callback(&client, shared_mix);
|
// Create process and activate the client
|
||||||
// Create process and activate the client
|
let process = jack::contrib::ClosureProcessHandler::new(process_callback);
|
||||||
let process = jack::contrib::ClosureProcessHandler::new(process_callback);
|
let active_client = client.activate_async((), process).unwrap();
|
||||||
let active_client = client.activate_async((), process).unwrap();
|
let result = eframe::run_native(
|
||||||
let result = eframe::run_native(
|
"FSLCMix",
|
||||||
"FSLCMix",
|
options,
|
||||||
options,
|
Box::new(|cc| {
|
||||||
Box::new(|cc| {
|
// Dark theme
|
||||||
// Dark theme
|
cc.egui_ctx.set_theme(egui::Theme::Dark);
|
||||||
cc.egui_ctx.set_theme(egui::Theme::Dark);
|
// egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
// egui_extras::install_image_loaders(&cc.egui_ctx);
|
Ok(Box::new(app))
|
||||||
Ok(Box::new(app))
|
}),
|
||||||
}),
|
);
|
||||||
);
|
if let Err(err) = active_client.deactivate() {
|
||||||
if let Err(err) = active_client.deactivate() {
|
eprintln!("JACK exited with error: {err}");
|
||||||
eprintln!("JACK exited with error: {err}");
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
eprintln!("Could not connect to JACK Audio Server.");
|
|
||||||
let options = eframe::NativeOptions {
|
|
||||||
viewport: egui::ViewportBuilder::default()
|
|
||||||
.with_inner_size([150.0, 80.0])
|
|
||||||
.with_min_inner_size([150.0, 80.0])
|
|
||||||
.with_max_inner_size([150.0, 80.0]),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = eframe::run_native(
|
|
||||||
"Error - FSLCMix",
|
|
||||||
options,
|
|
||||||
Box::new(|cc| {
|
|
||||||
cc.egui_ctx.set_theme(egui::Theme::Dark);
|
|
||||||
Ok(Box::new(ErrorBox { text: "Could not connect to JACK Audio Server".to_owned(), }))
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_jack_callback(client: &jack::Client, mixer: Arc<Mutex<FslcMix>>) -> impl FnMut(&jack::Client, &jack::ProcessScope) -> jack::Control {
|
fn register_jack_callback(client: &jack::Client, mixer: Arc<Mutex<FslcMix>>) -> impl FnMut(&jack::Client, &jack::ProcessScope) -> jack::Control {
|
||||||
|
|
@ -85,40 +63,13 @@ fn register_jack_callback(client: &jack::Client, mixer: Arc<Mutex<FslcMix>>) ->
|
||||||
process_callback
|
process_callback
|
||||||
}
|
}
|
||||||
|
|
||||||
fn db_peak(val : f32) -> f32 {
|
|
||||||
20.0 * val.log10()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_rms(val : f32) -> f32 {
|
|
||||||
10.0 * val.log10()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ErrorBox {
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for ErrorBox {
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.label(&self.text);
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("Okay").clicked() {
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MixApp {
|
struct MixApp {
|
||||||
mix: Arc<Mutex<FslcMix>>,
|
mix: Arc<Mutex<FslcMix>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for MixApp {
|
impl eframe::App for MixApp {
|
||||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
let mut owned_mix = self.mix.lock().unwrap();
|
let mut owned_mix = self.mix.lock().unwrap();
|
||||||
owned_mix.update(ctx, frame);
|
owned_mix.update(ctx, frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +88,6 @@ struct FslcMix {
|
||||||
master: MixChannel,
|
master: MixChannel,
|
||||||
normalize: bool,
|
normalize: bool,
|
||||||
ui_size: egui::Vec2,
|
ui_size: egui::Vec2,
|
||||||
max_gain: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FslcMix {
|
impl FslcMix {
|
||||||
|
|
@ -156,7 +106,6 @@ impl FslcMix {
|
||||||
normalize: false,
|
normalize: false,
|
||||||
ui_size: egui::Vec2::new(400.0, 330.0), // This size doesn't matter since it's
|
ui_size: egui::Vec2::new(400.0, 330.0), // This size doesn't matter since it's
|
||||||
// overritten
|
// overritten
|
||||||
max_gain: 1.25,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,27 +164,7 @@ impl FslcMix {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
// ui.label("Licensed under the GPLv3.");
|
// ui.label("Licensed under the GPLv3.");
|
||||||
ui.label("Max Gain:");
|
ui.toggle_value(&mut self.normalize, "Normalize")
|
||||||
let slider = ui.add(egui::DragValue::new(&mut self.max_gain).range(1.1..=2.0));
|
|
||||||
if slider.changed() {
|
|
||||||
self.update_max_gain();
|
|
||||||
}
|
|
||||||
let btn = ui.button("Reset");
|
|
||||||
if btn.clicked() {
|
|
||||||
self.max_gain = 1.25;
|
|
||||||
self.update_max_gain();
|
|
||||||
}
|
|
||||||
ui.toggle_value(&mut self.normalize, "Normalize");
|
|
||||||
if ui.add(egui::Button::new("All Unmute").frame(false)).clicked() {
|
|
||||||
for channel in &mut self.channels {
|
|
||||||
channel.mute = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ui.add(egui::Button::new("All Unsolo").frame(false)).clicked() {
|
|
||||||
for channel in &mut self.channels {
|
|
||||||
channel.solo = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|
@ -254,15 +183,6 @@ impl FslcMix {
|
||||||
// frame.set_window_size(window_size);
|
// frame.set_window_size(window_size);
|
||||||
// frame.request_repaint();
|
// frame.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_max_gain(&mut self) {
|
|
||||||
// update max gain for all channels
|
|
||||||
for channel in &mut self.channels {
|
|
||||||
channel.max_gain = self.max_gain;
|
|
||||||
}
|
|
||||||
self.master.max_gain = self.max_gain;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MixChannel {
|
struct MixChannel {
|
||||||
|
|
@ -276,19 +196,16 @@ struct MixChannel {
|
||||||
limit: bool,
|
limit: bool,
|
||||||
mute: bool,
|
mute: bool,
|
||||||
solo: bool,
|
solo: bool,
|
||||||
others_solo: bool,
|
|
||||||
show_rms: bool,
|
show_rms: bool,
|
||||||
max_gain: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MixChannel {
|
impl MixChannel {
|
||||||
|
|
||||||
fn mix(&mut self, input : &[f32], output : &mut [f32], any_solo : bool) {
|
fn mix(&mut self, input : &[f32], output : &mut [f32], any_solo : bool) {
|
||||||
// if self.mute || (any_solo && !self.solo) {
|
if self.mute || (any_solo && !self.solo) {
|
||||||
// self.last = 0.0;
|
self.last = 0.0;
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
self.others_solo = any_solo;
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
assert!(input.len() == output.len());
|
assert!(input.len() == output.len());
|
||||||
self.rms(input);
|
self.rms(input);
|
||||||
|
|
@ -304,10 +221,7 @@ impl MixChannel {
|
||||||
if out_sample > self.max {
|
if out_sample > self.max {
|
||||||
self.max = out_sample;
|
self.max = out_sample;
|
||||||
}
|
}
|
||||||
// Only mix into the output if we're not muted or no other tracks have solo
|
output[i] += out_sample;
|
||||||
if !(self.mute || (any_solo && !self.solo)) {
|
|
||||||
output[i] += out_sample;
|
|
||||||
}
|
|
||||||
self.last = out_sample;
|
self.last = out_sample;
|
||||||
self.update_smoothed(self.last);
|
self.update_smoothed(self.last);
|
||||||
}
|
}
|
||||||
|
|
@ -317,29 +231,20 @@ impl MixChannel {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
let wrap_mode = TextWrapMode::Extend;
|
let wrap_mode = TextWrapMode::Extend;
|
||||||
let pb = ui.add(egui::Button::new(
|
let pb = ui.add(egui::Button::new(format!("Peak: {:.2} dB", self.max.log10())).wrap_mode(wrap_mode));
|
||||||
format!("Peak: {:+2.2} dB", db_peak(self.max)))
|
|
||||||
.frame(false)
|
|
||||||
.small()
|
|
||||||
.wrap_mode(wrap_mode));
|
|
||||||
if pb.clicked() {
|
if pb.clicked() {
|
||||||
self.max = 0.0;
|
self.max = 0.0;
|
||||||
}
|
}
|
||||||
let rb = ui.add(egui::Button::new(
|
let rb = ui.add(egui::Button::new(format!("RMS: {:.2}", self.last_rms)).wrap_mode(wrap_mode));
|
||||||
format!("RMS: {:+2.2} dB", db_rms(self.last_rms)))
|
|
||||||
.frame(false)
|
|
||||||
.small()
|
|
||||||
.wrap_mode(wrap_mode));
|
|
||||||
if rb.clicked() {
|
if rb.clicked() {
|
||||||
self.last_rms = 0.0;
|
self.last_rms = 0.0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().slider_width = 175.0;
|
ui.spacing_mut().slider_width = 175.0;
|
||||||
ui.add(egui::Slider::new(&mut self.gain, 0.0..=self.max_gain)
|
ui.add(egui::Slider::new(&mut self.gain, 0.0..=1.2)
|
||||||
//.text("Gain")
|
//.text("Gain")
|
||||||
.vertical()
|
.vertical());
|
||||||
.max_decimals(2));
|
|
||||||
// ui.add(egui::ProgressBar::new(self.last));
|
// ui.add(egui::ProgressBar::new(self.last));
|
||||||
self.levels_bar(ui);
|
self.levels_bar(ui);
|
||||||
});
|
});
|
||||||
|
|
@ -356,7 +261,7 @@ impl MixChannel {
|
||||||
ui.toggle_value(&mut self.solo, "S");
|
ui.toggle_value(&mut self.solo, "S");
|
||||||
ui.toggle_value(&mut self.limit, "Lim");
|
ui.toggle_value(&mut self.limit, "Lim");
|
||||||
});
|
});
|
||||||
ui.add(egui::TextEdit::singleline(&mut self.channel_name).desired_width(85.0));
|
ui.add(egui::TextEdit::singleline(&mut self.channel_name).desired_width(75.0));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,39 +272,29 @@ impl MixChannel {
|
||||||
} else {
|
} else {
|
||||||
self.last_smoothed
|
self.last_smoothed
|
||||||
};
|
};
|
||||||
let val_db = if self.show_rms {
|
let (rect, response) = ui.allocate_exact_size(vec2(15.0, 190.0), egui::Sense::hover());
|
||||||
db_rms(val)
|
|
||||||
} else {
|
|
||||||
db_peak(val)
|
|
||||||
};
|
|
||||||
let (rect, response) = ui.allocate_exact_size(vec2(10.0, 190.0), egui::Sense::hover());
|
|
||||||
let painter = ui.painter();
|
let painter = ui.painter();
|
||||||
let filled_height = (rect.height() * val / self.max_gain).min(rect.height()); // Show a bit over max amplitude
|
let filled_height = (rect.height() * val / 1.2).min(rect.height()); // Show a bit over max amplitude
|
||||||
// let filled_rect = Rect::from_min_max(rect.min, rect.min + vec2(rect.width(), filled_height));
|
// let filled_rect = Rect::from_min_max(rect.min, rect.min + vec2(rect.width(), filled_height));
|
||||||
// let remaining_rect = Rect::from_min_max(filled_rect.max, rect.max);
|
// let remaining_rect = Rect::from_min_max(filled_rect.max, rect.max);
|
||||||
let filled_rect = Rect::from_min_max(rect.max - vec2(rect.width(), filled_height), rect.max);
|
let filled_rect = Rect::from_min_max(rect.max - vec2(rect.width(), filled_height), rect.max);
|
||||||
// let remaining_rect = Rect::from_min_max(rect.min, filled_rect.max);
|
// let remaining_rect = Rect::from_min_max(rect.min, filled_rect.max);
|
||||||
// painter.rect_filled(remaining_rect, 0.0, Color32::from_rgb(200, 0, 0));
|
// painter.rect_filled(remaining_rect, 0.0, Color32::from_rgb(200, 0, 0));
|
||||||
let color_saturation = if self.mute || (!self.solo && self.others_solo) { 50 } else { 200 };
|
|
||||||
let color = if val < 1.0 {
|
let color = if val < 1.0 {
|
||||||
Color32::from_rgb(0, color_saturation, 0)
|
Color32::from_rgb(0, 200, 0)
|
||||||
} else if val < self.max_gain {
|
} else if val < 1.2 {
|
||||||
Color32::from_rgb(color_saturation, color_saturation, 0)
|
Color32::from_rgb(200, 200, 0)
|
||||||
} else {
|
} else {
|
||||||
Color32::from_rgb(color_saturation, 0, 0)
|
Color32::from_rgb(200, 0, 0)
|
||||||
};
|
};
|
||||||
painter.rect_filled(filled_rect, 0.0, color);
|
painter.rect_filled(filled_rect, 0.0, color);
|
||||||
painter.rect_stroke(rect, 0.0, (1.0, Color32::DARK_GRAY));
|
painter.rect_stroke(rect, 0.0, (1.0, Color32::DARK_GRAY));
|
||||||
// Draw scale numbers
|
// Draw scale numbers
|
||||||
let num_steps = (self.max_gain * 10.0) as u16;
|
let num_steps = 12;
|
||||||
let step_size = rect.height() / num_steps as f32;
|
let step_size = rect.height() / num_steps as f32;
|
||||||
for i in 0..=num_steps {
|
for i in 0..=num_steps {
|
||||||
let y_pos = rect.top() + i as f32 * step_size;
|
let y_pos = rect.top() + i as f32 * step_size;
|
||||||
let number = if self.show_rms {
|
let number = (num_steps - i) as f32 / 10.0;
|
||||||
db_rms((num_steps - i) as f32 / 10.0)
|
|
||||||
} else {
|
|
||||||
db_peak((num_steps - i) as f32 / 10.0)
|
|
||||||
};
|
|
||||||
// Invert the order if you want 0 at the bottom
|
// Invert the order if you want 0 at the bottom
|
||||||
let text_pos = Pos2::new(rect.right() + 5.0, y_pos);
|
let text_pos = Pos2::new(rect.right() + 5.0, y_pos);
|
||||||
painter.text(text_pos,
|
painter.text(text_pos,
|
||||||
|
|
@ -409,7 +304,7 @@ impl MixChannel {
|
||||||
Color32::DARK_GRAY);
|
Color32::DARK_GRAY);
|
||||||
}
|
}
|
||||||
response.on_hover_cursor(egui::CursorIcon::PointingHand)
|
response.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||||
.on_hover_text(format!("{:.3} dB", val_db));
|
.on_hover_text(format!("{:.1} db", self.last.log10()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn declare_jack_port(&self, client : &jack::Client) -> jack::Port<jack::AudioIn> {
|
fn declare_jack_port(&self, client : &jack::Client) -> jack::Port<jack::AudioIn> {
|
||||||
|
|
@ -445,9 +340,7 @@ impl Default for MixChannel {
|
||||||
limit: false,
|
limit: false,
|
||||||
mute: false,
|
mute: false,
|
||||||
solo: false,
|
solo: false,
|
||||||
others_solo: false,
|
|
||||||
show_rms: false,
|
show_rms: false,
|
||||||
max_gain: 1.25,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue