Compare commits

..

No commits in common. "ae6ead454ebda6a85048f477af0703dce2b6a45f" and "ae76e39d2ee060fd1032ed5d09325e445d4ea5da" have entirely different histories.

4 changed files with 48 additions and 184 deletions

View file

@ -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

View file

@ -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"

View file

@ -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.
![Screenshot](./screenshots/screenshot.png) ![Screenshot](./screenshots/screenshot.png)
## Features
- RMS and Peak Tracking
- Per-track Mute, Solo and Gain
- Per-track hard limiting
To build: To build:
``` ```

View file

@ -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,7 +14,6 @@ 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])
@ -22,7 +21,7 @@ fn main() -> eframe::Result {
.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);
@ -42,27 +41,6 @@ fn main() -> eframe::Result {
} }
result 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
}
}
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 {
let unlocked_mixer = mixer.lock().unwrap(); let unlocked_mixer = mixer.lock().unwrap();
@ -85,33 +63,6 @@ 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>>,
} }
@ -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
if !(self.mute || (any_solo && !self.solo)) {
output[i] += out_sample; 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,
} }
} }
} }