Skip to content

Commit

Permalink
Implemented background noise. Fixes #1.
Browse files Browse the repository at this point in the history
Continuously samples background noise in chunks of 1023 samples. If a
chunk is interrupted, it will be discarded. 20 last chunks are stored
and when needed, they are played back by the background looper one by
one, by choosing from the ten quietest clips.

Seems to work surprisingly fine; no cross fading between samples is
implemented yet, and the choise mechasnim for samples could better
than just its RMS (e.g. start/end RMS difference?).
  • Loading branch information
eras committed May 23, 2021
1 parent 7e35413 commit 50f6c6d
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ thiserror = "1.0.22"
anyhow = "1.0.34"
atomicwrites = "0.3.0"
hound = "3.4.0"
rand = "0.8.3"

# [lib]
# name = "mute_keyboard_plugin"
Expand Down
106 changes: 106 additions & 0 deletions src/background_sampler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use crate::sampler::Sampler;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use std::collections::BTreeMap;

#[derive(Clone)]
pub struct Clip {
pub sample_a: Sampler,
pub sample_b: Sampler,
pub rms: f32,
}

type ClipId = usize;

pub struct BackgroundSampler {
current_clip: Option<Clip>,
clip_length: usize,
num_clips: usize,
clip_id_gen: ClipId,
clips: BTreeMap<ClipId, Clip>,
rng: StdRng,
}

pub type StereoSample = (f32, f32);

// BackgroundSampler periodically (or maybe randomly) samples short samples, keeping at most num_clips latest
// ones. Then it orders the samples by their volume and the user may pick n quietest samples from them to use as
// background noise. If sampling a sample is interrupted by a pause, then that sample is discarded.
impl BackgroundSampler {
pub fn new(num_clips: usize, clip_length: usize) -> BackgroundSampler {
let mut bg_sampler = BackgroundSampler {
current_clip: None,
clip_length,
num_clips,
clip_id_gen: 0,
clips: BTreeMap::new(),
rng: StdRng::from_entropy(),
};
bg_sampler.resume();
bg_sampler
}

fn new_clip_id(&mut self) -> usize {
let id = self.clip_id_gen;
self.clip_id_gen += 1;
id
}

pub fn sample(&mut self, sample: StereoSample) {
let full = match self.current_clip {
None => false,
Some(ref mut clip) => {
clip.sample_a.sample(sample.0);
clip.sample_b.sample(sample.1);
clip.sample_a.is_full()
}
};
if full {
let id = self.new_clip_id();
let mut clip = self.current_clip.take().unwrap();
clip.rms = clip.sample_a.rms().max(clip.sample_b.rms());
self.clips.insert(id, clip);
if self.clips.len() > self.num_clips {
// we could do: self.clips.pop_first();
// but let's avoid unstable features for now
if let Some((&key, _)) = self.clips.iter().next() {
self.clips.remove(&key);
}
}
self.resume();
}
}

// pick a random clip from the n least-rms clips
pub fn choose_clip(&mut self, limit: usize) -> Option<Clip> {
if self.clips.is_empty() {
None
} else {
let mut clips: Vec<&Clip> = self.clips.iter().map(|(_, v)| v).collect();
clips.sort_unstable_by(|a, b| {
if a.rms < b.rms {
std::cmp::Ordering::Less
} else if a.rms > b.rms {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
});
Some(clips[self.rng.gen_range(0..limit.min(clips.len()))].clone())
}
}

pub fn pause(&mut self) {
self.current_clip = None;
}

pub fn resume(&mut self) {
if self.current_clip.is_none() {
self.current_clip = Some(Clip {
sample_a: Sampler::new(self.clip_length, true),
sample_b: Sampler::new(self.clip_length, true),
rms: 0.0,
});
}
}
}
2 changes: 2 additions & 0 deletions src/click_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub struct ClickInfo {
pub click_sampler: Sampler,
pub mute_enabled: bool,
pub invert_mute: bool,
pub background_noise: bool,
pub num_clicks: usize,
}

Expand All @@ -20,6 +21,7 @@ impl ClickInfo {
},
mute_enabled: true,
invert_mute: false,
background_noise: true,
num_clicks: 0,
}
}
Expand Down
37 changes: 35 additions & 2 deletions src/click_mute.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::{clicky_events::ClickyEvents, delay::Delay, fader::Fader};
use crate::{clicky_events::ClickyEvents, cross_fader::CrossFader, delay::Delay, fader::Fader};
// use crossbeam_channel::bounded;
use crate::background_sampler::BackgroundSampler;
use crate::click_info::ClickInfo;
use crate::click_mute_control;
use crate::config::Config;
use crate::level_event::LevelEvent;
use crate::looper::Looper;
use crate::save::Save;
use std::sync::{Arc, Mutex};

Expand All @@ -25,6 +27,9 @@ struct ClickMute {
fader_a: Fader,
fader_b: Fader,

cross_fader_a: CrossFader,
cross_fader_b: CrossFader,

clicky_events: ClickyEvents,

sample_index: usize,
Expand All @@ -36,6 +41,9 @@ struct ClickMute {
control: click_mute_control::Receiver,

save: Option<(Save, Save, Save, Save, bool)>,

background_sampler: BackgroundSampler,
background_looper: Looper,
}

impl ClickMute {
Expand Down Expand Up @@ -67,9 +75,13 @@ impl ClickMute {
let delay_samples = (delay_seconds * sample_rate as f64) as usize;
let fade_samples = (fade_seconds * sample_rate as f64) as usize;

let mut cross_fader_a = CrossFader::new(0.0);
let mut cross_fader_b = CrossFader::new(0.0);
let mut fader_a = Fader::new(0.0);
let mut fader_b = Fader::new(0.0);

cross_fader_a.fade_in(fade_samples);
cross_fader_b.fade_in(fade_samples);
fader_a.fade_in(fade_samples);
fader_b.fade_in(fade_samples);

Expand All @@ -89,6 +101,8 @@ impl ClickMute {

fader_a,
fader_b,
cross_fader_a,
cross_fader_b,

clicky_events: ClickyEvents::new(),

Expand All @@ -107,6 +121,8 @@ impl ClickMute {
// Save::new(1, "3.wav"),
// false,
// )),
background_sampler: BackgroundSampler::new(10, 1024),
background_looper: Looper::new(),
}
}

Expand Down Expand Up @@ -185,13 +201,17 @@ impl ClickMute {
if click_info.invert_mute {
self.fader_a.fade_in(self.fade_samples);
self.fader_b.fade_in(self.fade_samples);
} else if click_info.background_noise {
self.cross_fader_a.fade_out(self.fade_samples);
self.cross_fader_b.fade_out(self.fade_samples);
} else {
self.fader_a.fade_out(self.fade_samples);
self.fader_b.fade_out(self.fade_samples);
}
self.mute_t0_index = None;
click_info.click_sampler.trigger();
self.save.iter_mut().for_each(|x| x.4 = !x.4);
self.background_sampler.pause();
}

if let Some(ref mut save) = self.save {
Expand All @@ -200,14 +220,23 @@ impl ClickMute {

let a = self.delay_a.process(*in_a);
let b = self.delay_b.process(*in_b);
self.background_sampler.sample((a, b));

self.save.iter_mut().for_each(|x| x.1.process(a));

click_info.live_sampler.sample(*in_a); // undelayed sample
click_info.click_sampler.sample(a); // delayed sample

let (bg_a, bg_b) = self.background_looper.produce(&mut self.background_sampler);
let (a, b) = if click_info.mute_enabled {
(self.fader_a.process(a), self.fader_b.process(b))
if click_info.invert_mute || !click_info.background_noise {
(self.fader_a.process(a), self.fader_b.process(b))
} else {
(
self.cross_fader_a.process(a, bg_a),
self.cross_fader_b.process(b, bg_b),
)
}
} else {
(a, b)
};
Expand All @@ -228,13 +257,17 @@ impl ClickMute {
if click_info.invert_mute {
self.fader_a.fade_out(self.fade_samples);
self.fader_b.fade_out(self.fade_samples);
} else if click_info.background_noise {
self.cross_fader_a.fade_in(self.fade_samples);
self.cross_fader_b.fade_in(self.fade_samples);
} else {
self.fader_a.fade_in(self.fade_samples);
self.fader_b.fade_in(self.fade_samples);
}
if !click_info.click_sampler.is_empty() {
click_info.click_sampler.hold_or_auto_hold();
}
self.background_sampler.resume();
}

self.sample_index += 1
Expand Down
28 changes: 28 additions & 0 deletions src/cross_fader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
pub struct CrossFader {
value: f32,
step_per_sample: f32,
}

impl CrossFader {
pub fn new(value: f32) -> CrossFader {
CrossFader {
value,
step_per_sample: 0.0,
}
}

// fades to sample_a
pub fn fade_in(&mut self, samples: usize) {
self.step_per_sample = 1.0 / (samples as f32);
}

// fades to sample_b
pub fn fade_out(&mut self, samples: usize) {
self.step_per_sample = -1.0 / (samples as f32);
}

pub fn process(&mut self, sample_a: f32, sample_b: f32) -> f32 {
self.value = f32::clamp(self.value + self.step_per_sample, 0.0, 1.0);
sample_a * self.value + sample_b * (1.0 - self.value)
}
}
16 changes: 10 additions & 6 deletions src/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,15 @@ impl Stage {

egui::CentralPanel::default().show(egui_ctx, |ui| {
ui.horizontal(|ui| {
ui.columns(4, |columns| {
ui.columns(5, |columns| {
let mut click_info = click_info.lock().unwrap();
columns[0].checkbox(&mut click_info.mute_enabled, "Automatic muting enabled");
columns[1].checkbox(&mut click_info.invert_mute, "Invert muting");
if columns[2]
columns[0].checkbox(&mut click_info.mute_enabled, "Automatic\nmuting");
columns[1].checkbox(&mut click_info.invert_mute, "Invert\nmuting");
if click_info.invert_mute {
click_info.background_noise = false;
}
columns[2].checkbox(&mut click_info.background_noise, "Background\nnoise");
if columns[3]
.add_sized((0.0, 40.0), egui::Button::new("Save"))
.clicked()
{
Expand All @@ -113,8 +117,8 @@ impl Stage {
}
}
}
columns[3].with_layout(egui::Layout::right_to_left(), |ui| {
ui.label(format!("Number of clicks is {}", click_info.num_clicks))
columns[4].with_layout(egui::Layout::right_to_left(), |ui| {
ui.label(format!("#{}", click_info.num_clicks))
});
});
});
Expand Down
45 changes: 45 additions & 0 deletions src/looper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::background_sampler;
use crate::background_sampler::BackgroundSampler;

struct PlayClip {
sample_a: Vec<f32>,
sample_b: Vec<f32>,
playhead: usize,
}

pub struct Looper {
current_clip: Option<PlayClip>,
}

impl Looper {
pub fn new() -> Looper {
Looper { current_clip: None }
}

pub fn produce(
&mut self,
background_sampler: &mut BackgroundSampler,
) -> background_sampler::StereoSample {
if self.current_clip.is_none() {
if let Some(clip) = background_sampler.choose_clip(10) {
self.current_clip = Some(PlayClip {
sample_a: clip.sample_a.get(),
sample_b: clip.sample_b.get(),
playhead: 0,
})
}
}
if let Some(ref mut clip) = self.current_clip {
if clip.playhead >= clip.sample_a.len() {
self.current_clip = None;
(0.0, 0.0)
} else {
let sample = (clip.sample_a[clip.playhead], clip.sample_b[clip.playhead]);
clip.playhead += 1;
sample
}
} else {
(0.0, 0.0)
}
}
}
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
mod background_sampler;
mod click_info;
mod click_mute;
mod click_mute_control;
mod clicky_events;
mod config;
mod cross_fader;
mod delay;
mod error;
mod fader;
mod gui;
mod level_event;
mod looper;
mod sampler;
mod save;

Expand Down
Loading

0 comments on commit 50f6c6d

Please sign in to comment.