Compare commits

...

10 commits

Author SHA1 Message Date
Josh Jeppson
ae6ead454e small change
Some checks failed
Rust / build (push) Has been cancelled
2025-09-24 08:38:48 -06:00
Josh Jeppson
6d150eae77 Added some extra options: max gain, and reset buttons 2024-11-20 13:43:09 -07:00
Josh Jeppson
64f0f6bed6 yoink 2024-11-15 11:33:20 -07:00
Josh Jeppson
5c5a7942c5 fixed small bug 2024-11-15 11:30:34 -07:00
Josh Jeppson
3f9cbacf5b Merge branch 'main' of github.com:ifndefJOSH/fslcmix 2024-11-15 11:20:17 -07:00
Josh Jeppson
c728c54c29 nice error message now 2024-11-15 11:20:00 -07:00
Josh Jeppson
c0011f43a3
Create rust.yml 2024-11-15 17:15:45 +00:00
Josh Jeppson
03d7286eca still render the bar if muted or other tracks soloing 2024-11-15 10:11:15 -07:00
Josh Jeppson
d714bf6c72 featuressssss 2024-11-14 22:27:30 -07:00
Josh Jeppson
27c80e901b mixer. not eq 2024-11-14 22:25:13 -07:00
4 changed files with 184 additions and 48 deletions

22
.github/workflows/rust.yml vendored Normal file
View file

@ -0,0 +1,22 @@
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,3 +9,4 @@ eframe = "0.29.1"
egui = "0.29.1"
egui_flex = "0.1.1"
jack = "0.13.0"
native-dialog = "0.7.0"

View file

@ -1,9 +1,15 @@
# FSLCMix (Simple Jack Equalizer)
# FSLCMix (Simple Jack Mixer)
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)
## Features
- RMS and Peak Tracking
- Per-track Mute, Solo and Gain
- Per-track hard limiting
To build:
```

View file

@ -6,7 +6,7 @@ use eframe::egui::*;
const PEAK_HOLD_TIME: usize = 4000;
const DECAY_FACTOR: f32 = 0.9999;
use std::sync::{Arc, Mutex};
use std::{process::exit, sync::{Arc, Mutex}};
fn main() -> eframe::Result {
let args = Args::parse();
@ -14,32 +14,54 @@ fn main() -> eframe::Result {
let app = MixApp {
mix : shared_mix.clone(),
};
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([500.0, 350.0])
.with_min_inner_size([300.0, 350.0])
.with_max_inner_size([5000.0, 350.0]),
..Default::default()
};
let (client, _status) = jack::Client::new("fslcmix", jack::ClientOptions::default()).unwrap();
let process_callback = register_jack_callback(&client, shared_mix);
// Create process and activate the client
let process = jack::contrib::ClosureProcessHandler::new(process_callback);
let active_client = client.activate_async((), process).unwrap();
let result = eframe::run_native(
"FSLCMix",
options,
Box::new(|cc| {
// Dark theme
cc.egui_ctx.set_theme(egui::Theme::Dark);
// egui_extras::install_image_loaders(&cc.egui_ctx);
Ok(Box::new(app))
}),
);
if let Err(err) = active_client.deactivate() {
eprintln!("JACK exited with error: {err}");
if let Ok((client, _status)) = jack::Client::new("fslcmix", jack::ClientOptions::default()) {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([500.0, 350.0])
.with_min_inner_size([300.0, 350.0])
.with_max_inner_size([5000.0, 350.0]),
..Default::default()
};
let process_callback = register_jack_callback(&client, shared_mix);
// Create process and activate the client
let process = jack::contrib::ClosureProcessHandler::new(process_callback);
let active_client = client.activate_async((), process).unwrap();
let result = eframe::run_native(
"FSLCMix",
options,
Box::new(|cc| {
// Dark theme
cc.egui_ctx.set_theme(egui::Theme::Dark);
// egui_extras::install_image_loaders(&cc.egui_ctx);
Ok(Box::new(app))
}),
);
if let Err(err) = active_client.deactivate() {
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 {
@ -63,13 +85,40 @@ fn register_jack_callback(client: &jack::Client, mixer: Arc<Mutex<FslcMix>>) ->
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 {
mix: Arc<Mutex<FslcMix>>,
}
impl eframe::App for MixApp {
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);
}
}
@ -88,6 +137,7 @@ struct FslcMix {
master: MixChannel,
normalize: bool,
ui_size: egui::Vec2,
max_gain: f32,
}
impl FslcMix {
@ -106,6 +156,7 @@ impl FslcMix {
normalize: false,
ui_size: egui::Vec2::new(400.0, 330.0), // This size doesn't matter since it's
// overritten
max_gain: 1.25,
}
}
@ -164,7 +215,27 @@ impl FslcMix {
ui.vertical(|ui| {
ui.horizontal(|ui| {
// ui.label("Licensed under the GPLv3.");
ui.toggle_value(&mut self.normalize, "Normalize")
ui.label("Max Gain:");
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| {
@ -183,6 +254,15 @@ impl FslcMix {
// frame.set_window_size(window_size);
// 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 {
@ -196,16 +276,19 @@ struct MixChannel {
limit: bool,
mute: bool,
solo: bool,
others_solo: bool,
show_rms: bool,
max_gain: f32,
}
impl MixChannel {
fn mix(&mut self, input : &[f32], output : &mut [f32], any_solo : bool) {
if self.mute || (any_solo && !self.solo) {
self.last = 0.0;
return;
}
// if self.mute || (any_solo && !self.solo) {
// self.last = 0.0;
// return;
// }
self.others_solo = any_solo;
// Sanity check
assert!(input.len() == output.len());
self.rms(input);
@ -221,7 +304,10 @@ impl MixChannel {
if out_sample > self.max {
self.max = out_sample;
}
output[i] += 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;
}
self.last = out_sample;
self.update_smoothed(self.last);
}
@ -231,20 +317,29 @@ impl MixChannel {
ui.vertical(|ui| {
ui.vertical(|ui| {
let wrap_mode = TextWrapMode::Extend;
let pb = ui.add(egui::Button::new(format!("Peak: {:.2} dB", self.max.log10())).wrap_mode(wrap_mode));
let pb = ui.add(egui::Button::new(
format!("Peak: {:+2.2} dB", db_peak(self.max)))
.frame(false)
.small()
.wrap_mode(wrap_mode));
if pb.clicked() {
self.max = 0.0;
}
let rb = ui.add(egui::Button::new(format!("RMS: {:.2}", self.last_rms)).wrap_mode(wrap_mode));
let rb = ui.add(egui::Button::new(
format!("RMS: {:+2.2} dB", db_rms(self.last_rms)))
.frame(false)
.small()
.wrap_mode(wrap_mode));
if rb.clicked() {
self.last_rms = 0.0;
}
});
ui.horizontal(|ui| {
ui.spacing_mut().slider_width = 175.0;
ui.add(egui::Slider::new(&mut self.gain, 0.0..=1.2)
ui.add(egui::Slider::new(&mut self.gain, 0.0..=self.max_gain)
//.text("Gain")
.vertical());
.vertical()
.max_decimals(2));
// ui.add(egui::ProgressBar::new(self.last));
self.levels_bar(ui);
});
@ -261,7 +356,7 @@ impl MixChannel {
ui.toggle_value(&mut self.solo, "S");
ui.toggle_value(&mut self.limit, "Lim");
});
ui.add(egui::TextEdit::singleline(&mut self.channel_name).desired_width(75.0));
ui.add(egui::TextEdit::singleline(&mut self.channel_name).desired_width(85.0));
});
}
@ -272,29 +367,39 @@ impl MixChannel {
} else {
self.last_smoothed
};
let (rect, response) = ui.allocate_exact_size(vec2(15.0, 190.0), egui::Sense::hover());
let val_db = if self.show_rms {
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 filled_height = (rect.height() * val / 1.2).min(rect.height()); // Show a bit over max amplitude
let filled_height = (rect.height() * val / self.max_gain).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 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 remaining_rect = Rect::from_min_max(rect.min, filled_rect.max);
// 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 {
Color32::from_rgb(0, 200, 0)
} else if val < 1.2 {
Color32::from_rgb(200, 200, 0)
Color32::from_rgb(0, color_saturation, 0)
} else if val < self.max_gain {
Color32::from_rgb(color_saturation, color_saturation, 0)
} else {
Color32::from_rgb(200, 0, 0)
Color32::from_rgb(color_saturation, 0, 0)
};
painter.rect_filled(filled_rect, 0.0, color);
painter.rect_stroke(rect, 0.0, (1.0, Color32::DARK_GRAY));
// Draw scale numbers
let num_steps = 12;
let num_steps = (self.max_gain * 10.0) as u16;
let step_size = rect.height() / num_steps as f32;
for i in 0..=num_steps {
let y_pos = rect.top() + i as f32 * step_size;
let number = (num_steps - i) as f32 / 10.0;
let number = if self.show_rms {
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
let text_pos = Pos2::new(rect.right() + 5.0, y_pos);
painter.text(text_pos,
@ -304,7 +409,7 @@ impl MixChannel {
Color32::DARK_GRAY);
}
response.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(format!("{:.1} db", self.last.log10()));
.on_hover_text(format!("{:.3} dB", val_db));
}
fn declare_jack_port(&self, client : &jack::Client) -> jack::Port<jack::AudioIn> {
@ -340,7 +445,9 @@ impl Default for MixChannel {
limit: false,
mute: false,
solo: false,
others_solo: false,
show_rms: false,
max_gain: 1.25,
}
}
}