Compare commits
10 commits
ae76e39d2e
...
ae6ead454e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae6ead454e | ||
|
|
6d150eae77 | ||
|
|
64f0f6bed6 | ||
|
|
5c5a7942c5 | ||
|
|
3f9cbacf5b | ||
|
|
c728c54c29 | ||
|
|
c0011f43a3 | ||
|
|
03d7286eca | ||
|
|
d714bf6c72 | ||
|
|
27c80e901b |
4 changed files with 184 additions and 48 deletions
22
.github/workflows/rust.yml
vendored
Normal file
22
.github/workflows/rust.yml
vendored
Normal 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- RMS and Peak Tracking
|
||||
- Per-track Mute, Solo and Gain
|
||||
- Per-track hard limiting
|
||||
|
||||
To build:
|
||||
|
||||
```
|
||||
|
|
|
|||
149
src/main.rs
149
src/main.rs
|
|
@ -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,6 +14,7 @@ fn main() -> eframe::Result {
|
|||
let app = MixApp {
|
||||
mix : shared_mix.clone(),
|
||||
};
|
||||
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])
|
||||
|
|
@ -21,7 +22,7 @@ fn main() -> eframe::Result {
|
|||
.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);
|
||||
|
|
@ -41,6 +42,27 @@ fn main() -> eframe::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 {
|
||||
let unlocked_mixer = mixer.lock().unwrap();
|
||||
|
|
@ -63,6 +85,33 @@ 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>>,
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue