Skip to content

Commit

Permalink
Enable full effect control via LfSources
Browse files Browse the repository at this point in the history
  • Loading branch information
Woyten committed Nov 9, 2022
1 parent cc10c0e commit 85df73d
Show file tree
Hide file tree
Showing 11 changed files with 474 additions and 267 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
dist
target
*_????????_??????.wav
waveforms.yml
microwave.yml
8 changes: 8 additions & 0 deletions magnetron/src/automation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ impl<T> AutomatedValue<T> for () {
fn use_context(&mut self, _context: &AutomationContext<T>) -> Self::Value {}
}

impl<T, A: AutomatedValue<T>> AutomatedValue<T> for &mut A {
type Value = A::Value;

fn use_context(&mut self, context: &AutomationContext<T>) -> Self::Value {
A::use_context(self, context)
}
}

impl<T, A1: AutomatedValue<T>, A2: AutomatedValue<T>> AutomatedValue<T> for (A1, A2) {
type Value = (A1::Value, A2::Value);

Expand Down
153 changes: 106 additions & 47 deletions microwave/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,71 +100,130 @@ The command-line enables you to set set up sample rates, buffer sizes and many o
microwave run help
```

## Modular Synth &ndash; Create Your Own Waveforms
## Modular Synth &ndash; Create Your Own Waveforms and Effects

On startup, `microwave` tries to locate a waveforms file specified by the `--wv-loc` parameter or the `MICROWAVE_WV_LOC` environment variable. If no such file is found `microwave` will create a default waveforms file for you.
On startup, `microwave` tries to locate a config file specified by the `--cfg-loc` parameter or the `MICROWAVE_CFG_LOC` environment variable. If no such file is found `microwave` will create a default config file with predefined waveforms and effects for you.

Let's have a look at an example clavinettish sounding waveform that I discovered by accident:
### `waveforms` section

The `waveforms` section of the config file defines waveform render stages to be applied sequentially when a key is pressed.

You can combine those stages to create the tailored sound you wish for. The following example config defines a clavinettish sounding waveform that I discovered by accident:

```yml
name: Funky Clavinet
envelope: Piano
stages:
- Oscillator:
kind: Sin
frequency: WaveformPitch
modulation: None
out_buffer: 0
out_level: 440.0
- Oscillator:
kind: Triangle
frequency: WaveformPitch
modulation: ByFrequency
mod_buffer: 0
out_buffer: 1
out_level: 1.0
- Filter:
kind: HighPass2
resonance:
Mul:
- WaveformPitch
- Time:
start: 0.0
end: 0.1
from: 2.0
to: 4.0
quality: 5.0
in_buffer: 1
out_buffer: AudioOut
out_level: 1.0
waveforms:
- name: Funky Clavinet
envelope: Piano
stages:
- Oscillator:
kind: Sin
frequency: WaveformPitch
modulation: None
out_buffer: 0
out_level: 440.0
- Oscillator:
kind: Triangle
frequency: WaveformPitch
modulation: ByFrequency
mod_buffer: 0
out_buffer: 1
out_level: 1.0
- Filter:
kind: HighPass2
resonance:
Mul:
- WaveformPitch
- Time:
start: 0.0
end: 0.1
from: 2.0
to: 4.0
quality: 5.0
in_buffer: 1
out_buffer: AudioOut
out_level: 1.0
```
This waveform has three stages:
While rendering the sound three stages are applied:
1. Generate a sine wave with the waveform's nominal frequency *F* and an amplitude of 440. Write this waveform to buffer 0.
1. Generate a triangle wave with frequency *F* and an amplitude of 1.0. Modulate the waveform's frequency (in Hz) sample-wise by the amount stored in buffer 0. Write the modulated waveform to buffer 1.
1. Apply a second-order high-pass filter to the samples stored in buffer 1. The high-pass's resonance frequency rises from 2*F* to 4*F* within 0.1 seconds. Write the result to `AudioOut`.

To create your own waveforms use the default waveforms file as a starting point and try editing it by trial-and-error. Let `microwave`'s error messages guide you to find valid configurations.
To create your own waveforms use the default config file as a starting point and try editing it by trial-and-error. Let `microwave`'s error messages guide you to find valid configurations.

### `effects` section

The `effects` section of the config file defines the effects to be applied sequentially after the waveforms have been rendered.

Use the following config as an example to add a rotary-speaker effect to your sound.

```yml
effects:
- RotarySpeaker:
buffer_size: 100000
gain:
Controller:
kind: Sound9
from: 0.0
to: 0.5
rotation_radius: 20.0
speed:
Controller:
kind: Sound10
from: 1.0
to: 7.0
acceleration: 7.0
deceleration: 14.0
```

The given config defines the following properties:

- A fixed delay buffer size of 100000 samples
- An input gain ranging from 0.0 to 0.5. The input gain can be controlled via the F9 key or MIDI CCN 78.
- A rotation radius of 20 cm
- A target rotation speed ranging from 1 Hz to 7 Hz. The speed can be controlled via the F10 key or MIDI CCN 79.
- The speaker accelerates (decelerates) at 7 (14) Hz/s.

## Live Interactions

You can live-control your waveforms with your mouse pointer or any MIDI Control Change messages source.
You can live-control your waveforms with your mouse pointer, touch pad or any MIDI Control Change messages source.

The following example stage defines a resonating low-pass filter whose resonance frequency can be controlled with a MIDI modulation wheel/lever from 0 to 10,000 Hz.

```yml
Filter:
kind: LowPass2
resonance:
Controller:
kind: Modulation
from: 0.0
to: 10000.0
quality: 5.0
in_buffer: 0
out_buffer: AudioOut
out_level: 1.0
stages:
- Filter:
kind: LowPass2
resonance:
Controller:
kind: Modulation
from: 0.0
to: 10000.0
quality: 5.0
in_buffer: 0
out_buffer: AudioOut
out_level: 1.0
```

If you want to use the mouse's vertical axis for sound control use the Breath controller.

```yml
resonance:
Controller:
kind: Breath
from: 0.0
to: 10000.0
```

If you want to use the touchpad for polyphonic sound control use the KeyPressure property.

```yml
resonance:
Linear:
value: KeyPressure
from: 0.0
to: 10000.0
```

# Feature List
Expand Down
83 changes: 57 additions & 26 deletions microwave/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ use crate::{
signal::{SignalKind, SignalSpec},
source::{LfSource, LfSourceExpr},
waveguide::{Reflectance, WaveguideSpec},
EnvelopeSpec, InBufferSpec, OutBufferSpec, OutSpec, StageSpec, WaveformProperty,
WaveformSpec, WaveformsSpec,
AudioSpec, EnvelopeSpec, InBufferSpec, OutBufferSpec, OutSpec, StageSpec, WaveformProperty,
WaveformSpec,
},
};

pub fn load_waveforms(
location: &Path,
) -> CliResult<WaveformsSpec<LfSource<WaveformProperty, LiveParameter>>> {
pub fn load_waveforms(location: &Path) -> CliResult<AudioSpec> {
if location.exists() {
println!("[INFO] Loading waveforms file `{}`", location.display());
let file = File::open(location)?;
Expand All @@ -37,7 +35,7 @@ pub fn load_waveforms(
}
}

pub fn get_builtin_waveforms() -> WaveformsSpec<LfSource<WaveformProperty, LiveParameter>> {
pub fn get_builtin_waveforms() -> AudioSpec {
let envelopes = vec![
EnvelopeSpec {
name: "Organ".to_owned(),
Expand Down Expand Up @@ -1278,33 +1276,66 @@ pub fn get_builtin_waveforms() -> WaveformsSpec<LfSource<WaveformProperty, LiveP

let effects = vec![
EffectSpec::Echo(EchoSpec {
gain_controller: LiveParameter::Sound7,
delay_time: 0.5,
feedback: 0.6,
feedback_rotation: 135.0,
buffer_size: 100000,
gain: LfSourceExpr::Controller {
kind: LiveParameter::Sound7,
from: LfSource::Value(0.0),
to: LfSource::Value(1.0),
}
.wrap(),
delay_time: LfSource::Value(0.5),
feedback: LfSource::Value(0.6),
feedback_rotation: LfSource::Value(135.0),
}),
EffectSpec::SchroederReverb(SchroederReverbSpec {
gain_controller: LiveParameter::Sound8,
allpasses: vec![5.10, 7.73, 10.00, 12.61],
allpass_feedback: 0.5,
combs: vec![25.31, 26.94, 28.96, 30.75, 32.24, 33.81, 35.31, 36.67],
comb_feedback: 0.95,
cutoff: 5600.0,
stereo: 0.52,
max_gain: 0.5,
buffer_size: 100000,
gain: LfSourceExpr::Controller {
kind: LiveParameter::Sound8,
from: LfSource::Value(0.0),
to: LfSource::Value(0.5),
}
.wrap(),
allpasses: vec![
LfSource::Value(5.10),
LfSource::Value(7.73),
LfSource::Value(10.00),
LfSource::Value(12.61),
],
allpass_feedback: LfSource::Value(0.5),
combs: vec![
(LfSource::Value(25.31), LfSource::Value(25.83)),
(LfSource::Value(26.94), LfSource::Value(27.46)),
(LfSource::Value(28.96), LfSource::Value(29.48)),
(LfSource::Value(30.75), LfSource::Value(31.27)),
(LfSource::Value(32.24), LfSource::Value(32.76)),
(LfSource::Value(33.81), LfSource::Value(34.33)),
(LfSource::Value(35.31), LfSource::Value(35.83)),
(LfSource::Value(36.67), LfSource::Value(37.19)),
],
comb_feedback: LfSource::Value(0.95),
cutoff: LfSource::Value(5600.0),
}),
EffectSpec::RotarySpeaker(RotarySpeakerSpec {
gain_controller: LiveParameter::Sound9,
motor_controller: LiveParameter::Sound10,
rotation_radius: 20.0,
min_speed: 1.0,
max_speed: 7.0,
acceleration: 1.0,
deceleration: 0.5,
buffer_size: 100000,
gain: LfSourceExpr::Controller {
kind: LiveParameter::Sound9,
from: LfSource::Value(0.0),
to: LfSource::Value(0.5),
}
.wrap(),
rotation_radius: LfSource::Value(20.0),
speed: LfSourceExpr::Controller {
kind: LiveParameter::Sound10,
from: LfSource::Value(1.0),
to: LfSource::Value(7.0),
}
.wrap(),
acceleration: LfSource::Value(7.0),
deceleration: LfSource::Value(14.0),
}),
];

WaveformsSpec {
AudioSpec {
envelopes,
waveforms,
effects,
Expand Down
16 changes: 11 additions & 5 deletions microwave/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use cpal::{
SupportedBufferSize, SupportedStreamConfig,
};
use hound::{WavSpec, WavWriter};
use magnetron::automation::AutomationContext;
use ringbuf::Producer;

use crate::control::{LiveParameter, LiveParameterStorage};
Expand Down Expand Up @@ -56,7 +57,7 @@ pub struct AudioModel {

impl AudioModel {
pub fn new(
audio_stages: Vec<Box<dyn AudioStage>>,
audio_stages: Vec<Box<dyn AudioStage<((), LiveParameterStorage)>>>,
output_stream_params: (Device, StreamConfig, SampleFormat),
options: AudioOptions,
storage_updates: Receiver<LiveParameterStorage>,
Expand Down Expand Up @@ -129,7 +130,7 @@ impl AudioOut {

struct AudioRenderer {
buffer: Vec<f64>,
audio_stages: Vec<Box<dyn AudioStage>>,
audio_stages: Vec<Box<dyn AudioStage<((), LiveParameterStorage)>>>,
storage: LiveParameterStorage,
storage_updates: Receiver<LiveParameterStorage>,
current_wav_writer: Option<WavWriter<BufWriter<File>>>,
Expand All @@ -154,8 +155,13 @@ impl AudioRenderer {
for sample in &mut *buffer_f64 {
*sample = 0.0;
}

let context = AutomationContext {
render_window_secs: buffer.len() as f64 / self.sample_rate_hz as f64,
payload: &((), self.storage),
};
for audio_stage in &mut self.audio_stages {
audio_stage.render(buffer_f64, &self.storage);
audio_stage.render(buffer_f64, &context);
}

for (src, dst) in buffer_f64.iter().zip(buffer.iter_mut()) {
Expand Down Expand Up @@ -277,8 +283,8 @@ fn send_update(

type UpdateFn = Box<dyn FnOnce(&mut AudioRenderer) + Send>;

pub trait AudioStage: Send {
fn render(&mut self, buffer: &mut [f64], storage: &LiveParameterStorage);
pub trait AudioStage<T>: Send {
fn render(&mut self, buffer: &mut [f64], context: &AutomationContext<T>);

fn mute(&mut self);
}
9 changes: 4 additions & 5 deletions microwave/src/fluid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ use fluid_xenth::{
oxisynth::{MidiEvent, SoundFont, SynthDescriptor},
TunableFluid, Xenth,
};
use magnetron::automation::AutomationContext;
use tune::{
pitch::Pitch,
scala::{KbmRoot, Scl},
};
use tune_cli::CliResult;

use crate::{
audio::AudioStage, control::LiveParameterStorage, piano::Backend, tunable::TunableBackend,
};
use crate::{audio::AudioStage, piano::Backend, tunable::TunableBackend};

pub struct FluidBackend<I, S> {
backend: TunableBackend<S, TunableFluid>,
Expand Down Expand Up @@ -167,8 +166,8 @@ pub struct FluidSynth {
xenth: Xenth,
}

impl AudioStage for FluidSynth {
fn render(&mut self, buffer: &mut [f64], _storage: &LiveParameterStorage) {
impl<T> AudioStage<T> for FluidSynth {
fn render(&mut self, buffer: &mut [f64], _context: &AutomationContext<T>) {
let mut index = 0;
self.xenth
.write(buffer.len() / 2, |(l, r)| {
Expand Down
Loading

0 comments on commit 85df73d

Please sign in to comment.