diff --git a/.gitignore b/.gitignore index 9036b65b..f1701abc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,16 @@ +# rust target/ +Cargo.lock + +# python *.pyc + +# musescore +*.mscz~ +*.mscz.autosave + +# self matrix.html assets/local -Cargo.lock *.i16le -*.mscz~ +out.flac diff --git a/assets/midis/audiobro5.mid b/assets/midis/audiobro5.mid new file mode 100644 index 00000000..d4d8646a Binary files /dev/null and b/assets/midis/audiobro5.mid differ diff --git a/assets/musescore/audiobro5.mscz b/assets/musescore/audiobro5.mscz new file mode 100644 index 00000000..a4878269 Binary files /dev/null and b/assets/musescore/audiobro5.mscz differ diff --git a/assets/sounds/drum/cuica-open.wav b/assets/sounds/drum/cuica-open.wav new file mode 100644 index 00000000..ad130a0b Binary files /dev/null and b/assets/sounds/drum/cuica-open.wav differ diff --git a/assets/sounds/drum/high-tom.wav b/assets/sounds/drum/high-tom.wav new file mode 100644 index 00000000..c7363ee0 Binary files /dev/null and b/assets/sounds/drum/high-tom.wav differ diff --git a/assets/sounds/drum/mid-tom.wav b/assets/sounds/drum/mid-tom.wav new file mode 100644 index 00000000..3aa2d69b Binary files /dev/null and b/assets/sounds/drum/mid-tom.wav differ diff --git a/assets/systems/audiobro1.py b/assets/systems/audiobro1.py index e555b08c..6b7fb8ae 100644 --- a/assets/systems/audiobro1.py +++ b/assets/systems/audiobro1.py @@ -5,7 +5,6 @@ import argparse parser = argparse.ArgumentParser() -parser.add_argument('--live', '-l', action='store_true') parser.add_argument('--start', '-s') parser.add_argument('--run-size', type=int) args = parser.parse_args() @@ -127,9 +126,8 @@ def connect(self, other): drum.buf.mul(38, 1) # ride drum.buf.load('assets/sounds/drum/ride-bell.wav', 46) -drum.buf.resample(0.465, 53) -drum.buf.amplify(0.3, 53) -drum.buf.mul(53, 1) +drum.buf.resample(0.455, 46) +drum.buf.amplify(0.3, 46) bass.sonic.from_json({ "0": { @@ -155,14 +153,14 @@ def connect(self, other): piano.sonic.from_json({ "0": { "a": 4e-3, "d": 5e-5, "s": 0.2, "r": 2e-4, "m": 1, - "i0": 0, "i1": 0.06, "i2": 0, "i3": 0, "o": 0.25, + "i0": 0, "i1": 0.06, "i2": 0.02, "i3": 0, "o": 0.25, }, "1": { "a": 0.025, "d": 6e-5, "s": 0.2, "r": 3e-5, "m": 1, "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, }, "2": { - "a": 0, "d": 0, "s": 0, "r": 0, "m": 0, + "a": 0.025, "d": 0.01, "s": 1.0, "r": 0.01, "m": 4, "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, }, "3": { diff --git a/assets/systems/audiobro3.py b/assets/systems/audiobro3.py index 98c7a26c..2b161d44 100644 --- a/assets/systems/audiobro3.py +++ b/assets/systems/audiobro3.py @@ -9,7 +9,6 @@ import sys parser = argparse.ArgumentParser() -parser.add_argument('--live', '-l', action='store_true') parser.add_argument('--start', '-s') parser.add_argument('--run-size', type=int) args = parser.parse_args() diff --git a/assets/systems/audiobro4.py b/assets/systems/audiobro4.py index ffa940d5..9bfeac0b 100644 --- a/assets/systems/audiobro4.py +++ b/assets/systems/audiobro4.py @@ -15,7 +15,7 @@ def init(self, name, vol=0): self, ('osc', ['saw'], {'stay_on': True}), vol=1.0, - randomize_phase=lambda osc: osc.phase(random.random()), + per_voice_init=lambda osc, i: osc.phase(random.random()), name=name, ) dlal.subsystem.Subsystem.init( diff --git a/assets/systems/audiobro5.py b/assets/systems/audiobro5.py new file mode 100644 index 00000000..e57f66e4 --- /dev/null +++ b/assets/systems/audiobro5.py @@ -0,0 +1,316 @@ +import dlal +import midi + +import argparse +import hashlib + +parser = argparse.ArgumentParser() +parser.add_argument('--start', '-s', type=float) +parser.add_argument('--run-size', type=int) +args = parser.parse_args() + +class Piano(dlal.subsystem.Voices): + def init(self, name=None): + super().init( + ('digitar', [], {'lowness': 0.1, 'feedback': 0.9999, 'release': 0.2}), + cents=0.3, + vol=0.5, + per_voice_init=lambda voice, i: voice.hammer(offset=1 + (3 + i) / 10), + effects={ + 'lim': ('lim', [0.9, 0.8, 0.2]), + }, + name=name, + ) + +class Drums(dlal.subsystem.Subsystem): + def init(self, name=None): + dlal.subsystem.Subsystem.init( + self, + { + 'drums': 'buf', + 'lim': ('lim', [1.0, 0.9, 0.1]), + 'buf': 'buf', + }, + ['drums'], + ['buf'], + name=name, + ) + dlal.connect( + self.drums, + [self.buf, '<+', self.lim], + ) + +class Ghost(dlal.subsystem.Subsystem): + def init(self, name=None): + x = hashlib.sha256(name.encode()).digest() + r1 = int.from_bytes(x[0:4], byteorder='big') / (1 << 32) + r2 = int.from_bytes(x[4:8], byteorder='big') / (1 << 32) + dlal.subsystem.Subsystem.init( + self, + { + 'midman': 'midman', + 'rhymel': 'rhymel', + 'lpf': 'lpf', + 'lfo': 'lfo', + 'oracle': 'oracle', + 'sonic': 'sonic', + 'lim': 'lim', + 'buf': 'buf', + }, + ['rhymel'], + ['buf'], + name=name, + ) + dlal.connect( + self.midman, + [self.rhymel, '+>', self.sonic], + [self.oracle, '<+', self.lpf, '<+', self.lfo], + self.sonic, + [self.buf, '<+', self.lim], + ) + self.midman.directive([{'nibble': 0x90}], 0, 'midi', [0x90, '%1', 0]) + self.lpf.set(0.9992) + self.lfo.freq(5) + self.lfo.amp(1 / 128) + self.oracle.mode('pitch_wheel') + self.oracle.m(0x4000) + self.oracle.format('midi', [0xe0, '%l', '%h']) + self.sonic.from_json({ + "0": { + "a": 1e-3, "d": 5e-3, "s": 1, "r": 6e-5, "m": 1 + (r1 - 0.5)/20, + "i0": 0, "i1": 0.15 + r1/20, "i2": 0, "i3": 0, "o": 0.95/2, + }, + "1": { + "a": 1, "d": 2e-5, "s": 0, "r": 1, "m": 1 + (r2 - 0.5)/20, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + "2": { + "a": 1e-5, "d": 3e-5, "s": 1, "r": 6e-5, "m": 1, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + "3": { + "a": 4e-6, "d": 1e-5, "s": 1, "r": 6e-5, "m": 2, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + }) + self.sonic.midi(midi.Msg.pitch_bend_range(64)) + self.lim.hard(0.25/2) + self.lim.soft(0.15/2) + +class TalkingBassoon(dlal.subsystem.Subsystem): + def init(self, name=None): + dlal.subsystem.Subsystem.init( + self, + { + 'bassoon': ('buf', ['bassoon']), + 'afr2': ('afr', ['assets/local/bassindaface2.flac']), + 'afr2_gain': ('gain', [2/3]), + 'afr1': ('afr', ['assets/local/bassindaface1.flac']), + 'talk': 'buf', + 'vocoder': 'vocoder', + 'gain': ('gain', [8]), + 'lim': ('lim', [1.0, 0.8, 0.1]), + 'buf': 'buf', + }, + ['bassoon'], + ['buf'], + name=name, + ) + dlal.connect( + [self.bassoon, self.gain, self.lim], + self.buf, + ) + dlal.connect( + [self.afr2, self.afr2_gain, self.afr1], + self.talk, + self.vocoder, + self.buf, + ) + +class Choirist(dlal.subsystem.Subsystem): + def init(self, phonetic_samples, name=None): + self.phonetic_samples = phonetic_samples + x = hashlib.sha256(name.encode()).digest() + r1 = int.from_bytes(x[0:4], byteorder='big') / (1 << 32) + r2 = int.from_bytes(x[4:8], byteorder='big') / (1 << 32) + dlal.subsystem.Subsystem.init( + self, + { + 'midman': 'midman', + 'rhymel': 'rhymel', + 'lpf': ('lpf', [0.9992]), + 'lfo': ('lfo', [2 + 2 * r1, (1+r2)/128/16]), + 'oracle': 'oracle', + 'sonic': 'sonic', + 'vocoder': 'vocoder', + 'lim': ('lim', [0.25, 0.15]), + 'buf': 'buf', + }, + ['rhymel'], + ['buf'], + name=name, + ) + dlal.connect( + self.midman, + [self.rhymel, '+>', self.sonic], + [self.oracle, '<+', self.lpf, '<+', self.lfo], + self.sonic, + [self.buf, '<+', self.lim], + ) + dlal.connect(self.vocoder, self.buf) + self.midman.directive([{'nibble': 0x90}], 0, 'midi', [0x90, '%1', 0]) + self.oracle.mode('pitch_wheel') + self.oracle.m(0x4000) + self.oracle.format('midi', [0xe0, '%l', '%h']) + self.sonic.from_json({ + "0": { + "a": 1e-4, "d": 0, "s": 1, "r": 1e-4, "m": 1, + "i0": 0, "i1": 0.3, "i2": 0.2, "i3": 0.1, "o": 0.125, + }, + "1": { + "a": 1, "d": 0, "s": 1, "r": 1e-5, "m": 1, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + "2": { + "a": 1, "d": 0, "s": 1, "r": 1e-5, "m": 3, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + "3": { + "a": 1, "d": 0, "s": 1, "r": 1e-5, "m": 5, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + }) + self.sonic.midi(midi.Msg.pitch_bend_range(64)) + + def post_add_init(self): + self.vocoder.freeze_with(self.phonetic_samples) + +#===== init =====# +a_m = dlal.sound.read('assets/phonetics/a.flac').samples[44100:44100+64*1024] +a_f = dlal.sound.read('assets/phonetics/a.flac').samples[44100:44100+64*1024] + +audio = dlal.Audio(driver=True, run_size=args.run_size) +liner = dlal.Liner('assets/midis/audiobro5.mid') + +ghost1 = Ghost(name='ghost1') +ghost2 = Ghost(name='ghost2') +piano = Piano() +bass = dlal.Sonic(name='bass') +crow = dlal.Buf(name='crow') +drums = Drums() +talking_bassoon = TalkingBassoon() +bell = dlal.Addsyn().tubular_bells() +choir_s = Choirist(a_f, name='choir_s') +choir_a = Choirist(a_f, name='choir_a') +choir_t = Choirist(a_m, name='choir_t') +choir_b = Choirist(a_m, name='choir_b') + +reverb = dlal.Reverb(0.3) +lim = dlal.Lim(hard=1, soft=0.95, soft_gain=0.1) +buf = dlal.Buf() +tape = dlal.Tape() + +#===== commands =====# +if args.start: + liner.advance(args.start) + +bass.from_json({ + "0": { + "a": 5e-3, "d": 3e-4, "s": 0.5, "r": 0.01, "m": 1, + "i0": 0.3, "i1": 0.5, "i2": 0.4, "i3": 0.3, "o": 0.5, + }, + "1": { + "a": 5e-3, "d": 1e-4, "s": 0.5, "r": 0.01, "m": 1.99, + "i0": 0.01, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + "2": { + "a": 4e-3, "d": 3e-4, "s": 0.5, "r": 0.01, "m": 3.00013, + "i0": 0.01, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + "3": { + "a": 3e-3, "d": 1e-4, "s": 0.5, "r": 0.01, "m": 4.0001, + "i0": 0.01, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, +}) + +#----- drums -----# +drums.drums.load_drums() +drums.drums.load('assets/sounds/drum/kick.wav', dlal.Buf.Drum.bass) +drums.drums.amplify(1.5) +drums.drums.amplify(0.5, dlal.Buf.Drum.mute_cuica) +drums.drums.amplify(0.5, dlal.Buf.Drum.open_cuica) + +# burgers ride +drums.drums.resample(0.455, dlal.Buf.Drum.ride_bell) +drums.drums.amplify(0.3, dlal.Buf.Drum.ride_bell) + +# math kick +class Kick(dlal.maths.Generator): + def init(self): + self.duration = 0.2 + self.phase = self.Phase() + self.phase2 = self.Phase() + + def amp(self, t): + if t > self.duration: return + self.phase += self.ramp(120, 0, t / self.duration) + self.phase2 += 60 + return 0.5 * self.sqr(self.phase) * self.ramp(1, 0, t / self.duration) * self.sin(self.phase2) + +drums.drums.set( + Kick(audio.sample_rate()).generate(), + dlal.Buf.Drum.bass_1, +) + +#----- crow -----# +crow.load_asset('animal/crow.wav', 78) + +#===== connect =====# +dlal.connect( + liner, + [ + ghost1, + ghost2, + drums, + crow, + piano, + piano, + bass, + drums, + drums, + drums, + talking_bassoon, + bell, + choir_s, + choir_a, + choir_t, + choir_b, + ], +) +dlal.connect( + [ + ghost1, + ghost2, + drums, + crow, + piano, + bass, + talking_bassoon, + bell, + choir_s, + choir_a, + choir_t, + choir_b, + ], + [buf, + '<+', lim, + '<+', reverb, + ], + [audio, tape], +) + +#===== start =====# +print(dlal.system_diagram()) +for i in audio.addee_order(): print(i) +print() +dlal.typical_setup(duration=240) diff --git a/cat.mp3 b/cat.mp3 deleted file mode 100644 index 100090ce..00000000 Binary files a/cat.mp3 and /dev/null differ diff --git a/components/Cargo.toml b/components/Cargo.toml index 36389661..2555562b 100644 --- a/components/Cargo.toml +++ b/components/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "addsyn", "adsr", "arp", "audio", diff --git a/components/addsyn/Cargo.toml b/components/addsyn/Cargo.toml new file mode 100644 index 00000000..97a2f64c --- /dev/null +++ b/components/addsyn/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "addsyn" +version = "1.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +dlal-component-base = { path = "../base" } +serde = "1.0.204" diff --git a/components/addsyn/README.md b/components/addsyn/README.md new file mode 100644 index 00000000..9f8e882a --- /dev/null +++ b/components/addsyn/README.md @@ -0,0 +1 @@ +additive synth diff --git a/components/addsyn/src/lib.rs b/components/addsyn/src/lib.rs new file mode 100644 index 00000000..9242f4fb --- /dev/null +++ b/components/addsyn/src/lib.rs @@ -0,0 +1,310 @@ +use dlal_component_base::{arg, Body, CmdResult, component, json, serde_json}; +use serde::{Serialize, Deserialize}; + +//===== partial =====// +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Partial { + v: f32, // volume + a: f32, // attack + d: f32, // decay + s: f32, // sustain + r: f32, // release + m: f32, // freq multiplier + b: f32, // freq offset +} + +arg!(Partial); + +//===== decay mode =====// +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +enum DecayMode { + Linear, + Exponential, +} + +impl Default for DecayMode { + fn default() -> Self { + DecayMode::Linear + } +} + +arg!(DecayMode); + +//===== runner =====// +#[derive(Debug, PartialEq)] +enum Stage { + A, //attack + D, //decay + S, //sustain + R, //release +} + +#[derive(Debug)] +struct Runner { + phase: f32, + stage: Stage, + vol: f32, // envelope amplitude + partial: Partial, + decay_mode: DecayMode, +} + +impl Runner { + fn new(partial: &Partial, decay_mode: DecayMode, sample_rate: f32) -> Self { + Self { + phase: 0.0, + stage: Stage::R, + vol: 0.0, + partial: Partial { + v: partial.v, + a: partial.a / sample_rate, + d: match decay_mode { + DecayMode::Linear => partial.d / sample_rate, + DecayMode::Exponential => 10.0_f32.powf(-partial.d / sample_rate / 20.0), + }, + s: partial.s, + r: partial.r / sample_rate, + m: partial.m, + b: partial.b / sample_rate, + }, + decay_mode, + } + } + + fn advance_envelope(&mut self) { + match self.stage { + Stage::A => { + self.vol += self.partial.a; + if self.vol > 1.0 { + self.vol = 1.0; + self.stage = Stage::D; + } + } + Stage::D => { + match self.decay_mode { + DecayMode::Linear => { + self.vol -= self.partial.d; + } + DecayMode::Exponential => { + self.vol *= self.partial.d; + } + } + if self.vol < self.partial.s { + self.vol = self.partial.s; + self.stage = Stage::S; + } + } + Stage::S => (), + Stage::R => { + self.vol -= self.partial.r; + if self.vol < 0.0 { + self.vol = 0.0; + } + } + } + } +} + +//===== note =====// +struct Note { + runners: Vec, + step: f32, + vel: f32, +} + +impl Note { + fn new(freq: f32, sample_rate: u32) -> Self { + Note { + runners: Vec::new(), + step: freq / sample_rate as f32, + vel: 0.0, + } + } + + fn on(&mut self, vel: f32) { + self.vel = vel; + for runner in self.runners.iter_mut() { + runner.phase = 0.0; + runner.stage = Stage::A; + } + } + + fn off(&mut self, _vel: f32) { + for runner in self.runners.iter_mut() { + runner.stage = Stage::R; + } + } + + fn advance(&mut self, bend: f32) -> f32 { + let mut x = 0.0; + for runner in self.runners.iter_mut() { + runner.advance_envelope(); + x += self.vel * runner.partial.v * runner.vol * (runner.phase * std::f32::consts::TAU).sin(); + runner.phase += self.step * runner.partial.m * bend + runner.partial.b; + if runner.phase > 1.0 { + runner.phase -= 1.0; + } + } + x + } + + fn done(&self) -> bool { + for runner in self.runners.iter() { + if runner.stage != Stage::R || runner.vol > 1e-6{ + return false; + } + } + true + } +} + +//===== component =====// +component!( + {"in": ["midi"], "out": ["audio"]}, + [ + "run_size", + "sample_rate", + "uni", + "check_audio", + {"name": "field_helpers", "fields": ["partials", "decay_mode"], "kinds": ["json"]}, + "midi_rpn", + "midi_bend", + "notes", + ], + { + partials: Vec, + decay_mode: DecayMode, + }, + { + "partials": { + "args": [ + { + "name": "partials", + "type": "array", + "element": { + "name": "partial", + "type": "dict", + "keys": [ + { + "name": "v", + "desc": "volume", + "range": "[0, 1]", + }, + { + "name": "a", + "desc": "attack rate", + "units": "amplitude per second", + "range": "(0, 1]", + }, + { + "name": "d", + "desc": "decay rate", + "units": "amplitude per second or dB/s", + "range": "(0, 1] or [0, inf)", + }, + { + "name": "s", + "desc": "sustain level", + "range": "[0, 1]", + }, + { + "name": "r", + "desc": "release rate", + "units": "amplitude per second", + "range": "(0, 1]", + }, + { + "name": "m", + "desc": "frequency multiplier", + }, + { + "name": "b", + "units": "Hz", + "desc": "frequency offset", + }, + ], + }, + }, + ], + }, + "decay_mode": { + "args": [ + { + "name": "mode", + "options": [ + "Linear", + "Exponential", + ], + }, + ], + }, + }, +); + +impl ComponentTrait for Component { + fn init(&mut self) { + self.partials.push(Partial { + v: 1.0, + a: 1e-3, + d: 1e-5, + s: 0.5, + r: 1e-4, + m: 1.0, + b: 0.0, + }); + } + + fn join(&mut self, _body: serde_json::Value) -> CmdResult { + self.update_note_partials(); + Ok(None) + } + + fn run(&mut self) { + self.note_run_uni(); + } + + fn midi(&mut self, msg: &[u8]) { + if msg.len() < 3 { + return; + } + match msg[0] & 0xf0 { + 0x80 => self.note_off(msg), + 0x90 => self.note_on(msg), + 0xb0 => self.midi_rpn(msg), + 0xe0 => self.midi_bend(msg), + _ => {} + } + } +} + +impl Component { + fn partials_cmd(&mut self, body: serde_json::Value) -> CmdResult { + match body.arg(0) { + Ok(v) => self.partials = v, + e => if body.has_arg(0) { + e?; + } + } + self.update_note_partials(); + Ok(Some(json!(self.partials))) + } + + fn decay_mode_cmd(&mut self, body: serde_json::Value) -> CmdResult { + match body.arg(0) { + Ok(v) => self.decay_mode = v, + e => if body.has_arg(0) { + e?; + } + } + self.update_note_partials(); + Ok(Some(json!(self.decay_mode))) + } + + fn update_note_partials(&mut self) { + for note in self.notes.iter_mut() { + note.runners = self.partials + .iter() + .map(|i| Runner::new(i, self.decay_mode, self.sample_rate as f32)) + .collect(); + } + } +} diff --git a/components/afr/src/lib.rs b/components/afr/src/lib.rs index 4c18470c..3935324e 100644 --- a/components/afr/src/lib.rs +++ b/components/afr/src/lib.rs @@ -34,6 +34,7 @@ component!( reader: Reader, reader_sample_rate: f32, block: Vec, + block_sample: u64, sample_i: usize, sample_f64: f64, sample: f32, @@ -47,6 +48,10 @@ component!( "args": [], "return": "number of samples", }, + "elapsed": { + "args": [], + "return": "seconds of file played", + }, }, ); @@ -63,6 +68,7 @@ impl ComponentTrait for Component { if self.sample_i >= self.block.len() { self.block = match reader.blocks().read_next_or_eof(vec![]) { Ok(Some(block)) => { + self.block_sample = block.time(); (0..block.duration()) .map(|i| block.sample(0, i) as f32 / 0x8000 as f32) .collect() @@ -140,4 +146,10 @@ impl Component { Reader::Wav(_, duration) => Ok(Some(json!(duration))), } } + + fn elapsed_cmd(&mut self, _body: serde_json::Value) -> CmdResult { + Ok(Some(json!( + (self.block_sample as f32 + self.sample_f64 as f32) / self.reader_sample_rate + ))) + } } diff --git a/components/audio/Cargo.toml b/components/audio/Cargo.toml index 39958f6b..d0452833 100644 --- a/components/audio/Cargo.toml +++ b/components/audio/Cargo.toml @@ -11,4 +11,5 @@ crate-type = ["cdylib"] colored = "1.9.3" dlal-component-base = { path = "../base" } portaudio = { git = "https://github.com/dansgithubuser/rust-portaudio" } +serde = "1.0.204" serde_json = "1.0.48" diff --git a/components/audio/src/lib.rs b/components/audio/src/lib.rs index 89f088ce..30c74d40 100644 --- a/components/audio/src/lib.rs +++ b/components/audio/src/lib.rs @@ -2,10 +2,38 @@ use dlal_component_base::{component, err, json, serde_json, Body, CmdResult, Vie use colored::*; use portaudio as pa; +use serde::Serialize; use std::env; use std::ptr::{null, null_mut}; use std::slice::{from_raw_parts, from_raw_parts_mut}; +use std::time::{Duration, Instant}; + +fn default_input_latency(info: pa::DeviceInfo) -> pa::Time { + let mut ret = info.default_low_input_latency; + match env::var("DLAL_DEFAULT_LATENCY") { + Ok(s) => match s.as_str() { + "low" => ret = info.default_low_input_latency, + "high" => ret = info.default_high_input_latency, + _ => {} + } + _ => {} + } + ret +} + +fn default_output_latency(info: pa::DeviceInfo) -> pa::Time { + let mut ret = info.default_low_output_latency; + match env::var("DLAL_DEFAULT_LATENCY") { + Ok(s) => match s.as_str() { + "low" => ret = info.default_low_output_latency, + "high" => ret = info.default_high_output_latency, + _ => {} + } + _ => {} + } + ret +} struct Audio { i: *const f32, @@ -31,6 +59,12 @@ impl Default for Stream { fn default() -> Self { Stream::None } } +#[derive(Serialize)] +struct Profile { + name: String, + duration: Duration, +} + component!( {"in": ["audio**"], "out": ["audio**"]}, [ @@ -39,11 +73,13 @@ component!( "run_size", "sample_rate", {"name": "field_helpers", "fields": ["run_size", "sample_rate"], "kinds": ["rw", "json"]}, + {"name": "field_helpers", "fields": ["profiles"], "kinds": ["r"]}, ], { addees: Vec>, stream: Stream, audio: Audio, + profiles: Vec>, }, { "add": {"args": ["component", "command", "audio", "midi", "run"]}, @@ -56,12 +92,20 @@ component!( "addee_order": {}, "version": {}, "list_devices": {}, + "profile": {}, }, ); impl ComponentTrait for Component { fn init(&mut self) { self.run_size = 64; + match env::var("DLAL_RUN_SIZE") { + Ok(s) => match s.parse() { + Ok(run_size) => self.run_size = run_size, + _ => {}, + } + _ => {} + } self.sample_rate = 44100; } @@ -97,6 +141,21 @@ impl Component { } } + fn run_addees_profile(&mut self) { + let mut profiles = Vec::::new(); + for slot in self.addees.iter().rev() { + for i in slot { + let start = Instant::now(); + i.run(); + profiles.push(Profile { + name: i.name(), + duration: Instant::now() - start, + }); + } + } + self.profiles.push(profiles); + } + fn explain(&self) { for slot in self.addees.iter().rev() { for i in slot { @@ -162,7 +221,7 @@ impl Component { input_device, CHANNELS, INTERLEAVED, - pa.device_info(input_device)?.default_low_input_latency, + default_input_latency(pa.device_info(input_device)?), ); let output_device = match env::var("DLAL_AUDIO_OUTPUT_DEVICE") { Ok(v) => pa::DeviceIndex(v.parse()?), @@ -172,7 +231,7 @@ impl Component { output_device, CHANNELS, INTERLEAVED, - pa.device_info(output_device)?.default_low_output_latency, + default_output_latency(pa.device_info(output_device)?), ); pa.is_duplex_format_supported(input_params, output_params, self.sample_rate.into())?; let self_scoped = unsafe { std::mem::transmute::<&mut Component, &mut Component>(self) }; @@ -201,6 +260,57 @@ impl Component { Ok(None) } + fn profile_cmd(&mut self, _body: serde_json::Value) -> CmdResult { + const CHANNELS: i32 = 1; + const INTERLEAVED: bool = true; + let pa = pa::PortAudio::new()?; + let input_device = match env::var("DLAL_AUDIO_INPUT_DEVICE") { + Ok(v) => pa::DeviceIndex(v.parse()?), + Err(_) => pa.default_input_device()?, + }; + let input_params = pa::StreamParameters::::new( + input_device, + CHANNELS, + INTERLEAVED, + default_input_latency(pa.device_info(input_device)?), + ); + let output_device = match env::var("DLAL_AUDIO_OUTPUT_DEVICE") { + Ok(v) => pa::DeviceIndex(v.parse()?), + Err(_) => pa.default_output_device()?, + }; + let output_params = pa::StreamParameters::::new( + output_device, + CHANNELS, + INTERLEAVED, + default_output_latency(pa.device_info(output_device)?), + ); + pa.is_duplex_format_supported(input_params, output_params, self.sample_rate.into())?; + let self_scoped = unsafe { std::mem::transmute::<&mut Component, &mut Component>(self) }; + self.stream = Stream::Duplex({ + let mut stream = pa.open_non_blocking_stream( + pa::DuplexStreamSettings::new( + input_params, + output_params, + self.sample_rate.into(), + self.run_size as u32, + ), + move |args| { + assert!(args.frames == self_scoped.run_size); + for output_sample in args.out_buffer.iter_mut() { + *output_sample = 0.0; + } + self_scoped.audio.i = args.in_buffer.as_ptr(); + self_scoped.audio.o = args.out_buffer.as_mut_ptr(); + self_scoped.run_addees_profile(); + pa::Continue + }, + )?; + stream.start()?; + stream + }); + Ok(None) + } + fn start_input_only_cmd(&mut self, _body: serde_json::Value) -> CmdResult { const CHANNELS: i32 = 1; const INTERLEAVED: bool = true; @@ -213,7 +323,7 @@ impl Component { input_device, CHANNELS, INTERLEAVED, - pa.device_info(input_device)?.default_low_input_latency, + default_input_latency(pa.device_info(input_device)?), ); let self_scoped = unsafe { std::mem::transmute::<&mut Component, &mut Component>(self) }; self.stream = Stream::Input({ diff --git a/components/base/macro/component.hbs.rs b/components/base/macro/component.hbs.rs index d820dea4..8635f772 100644 --- a/components/base/macro/component.hbs.rs +++ b/components/base/macro/component.hbs.rs @@ -4,7 +4,7 @@ pub trait ComponentTrait { fn run(&mut self) {} fn command(&mut self, body: &dlal_component_base::serde_json::Value) {} fn midi(&mut self, _msg: &[u8]) {} - fn audio(&mut self) -> Option<&mut[f32]> { None } + fn audio(&mut self) -> Option<&mut [f32]> { None } // standard command extensions fn join(&mut self, body: dlal_component_base::serde_json::Value) -> dlal_component_base::CmdResult { Ok(None) } @@ -35,6 +35,17 @@ pub struct Component { {{#if features.multi}} outputs: Vec, {{/if}} + {{#if features.midi_rpn}} + midi_rpn: u16, + {{/if}} + {{#if features.midi_bend}} + midi_pitch_bend_range: f32, + midi_bend: f32, + {{/if}} + {{#if features.notes}} + notes: Vec, + notes_playing: Vec, + {{/if}} {{fields}} } @@ -51,10 +62,23 @@ impl Component { {{/if}} {{#if features.sample_rate}} self.sample_rate = body.kwarg("sample_rate")?; + {{#if features.notes}} + self.notes = (0..128) + .map(|i| Note::new( + 440.0 * (2.0 as f32).powf((i as f32 - 69.0) / 12.0), + self.sample_rate, + )) + .collect(); + self.notes_playing.reserve(128); + {{/if}} {{/if}} {{#if features.audio}} self.audio.resize(self.run_size, 0.0); {{/if}} + {{#if features.midi_bend}} + self.midi_pitch_bend_range = 2.0; + self.midi_bend = 1.0; + {{/if}} self.join(body) } @@ -147,11 +171,81 @@ impl Component { {{/if}} {{#if features.audio}} - fn audio(&mut self) -> Option<&mut[f32]> { + fn audio(&mut self) -> Option<&mut [f32]> { Some(self.audio.as_mut_slice()) } {{/if}} + {{#if features.midi_rpn}} + fn midi_rpn(&mut self, msg: &[u8]) { + match msg[1] { + 0x65 => self.midi_rpn = (msg[2] << 7) as u16, + 0x64 => self.midi_rpn += msg[2] as u16, + {{#if features.midi_bend}} + 0x06 => match self.midi_rpn { + 0x0000 => self.midi_pitch_bend_range = msg[2] as f32, + _ => (), + }, + 0x26 => match self.midi_rpn { + 0x0000 => self.midi_pitch_bend_range += msg[2] as f32 / 100.0, + _ => (), + }, + {{/if}} + _ => (), + } + } + {{/if}} + + {{#if features.midi_bend}} + fn midi_bend(&mut self, msg: &[u8]) { + const CENTER: f32 = 0x2000 as f32; + let value = (msg[1] as u16 + ((msg[2] as u16) << 7)) as f32; + let octaves = self.midi_pitch_bend_range * (value - CENTER) / (CENTER * 12.0); + self.midi_bend = (2.0 as f32).powf(octaves); + } + {{/if}} + + {{#if features.notes}} + fn note_off(&mut self, msg: &[u8]) { + self.notes[msg[1] as usize].off(msg[2] as f32 / 127.0); + } + + fn note_on(&mut self, msg: &[u8]) { + if msg[2] == 0 { + self.note_off(msg); + } else { + let note_num = msg[1] as usize; + self.notes[note_num].on(msg[2] as f32 / 127.0); + if !self.notes_playing.contains(¬e_num) { + self.notes_playing.push(note_num); + } + } + } + + {{#if features.uni}} + fn note_run_uni(&mut self) { + let audio = match &self.output { + Some(output) => output.audio(self.run_size).unwrap(), + None => return, + }; + self.notes_playing.retain(|note_num| { + let note = &mut self.notes[*note_num]; + if note.done() { + return false; + } + for i in audio.iter_mut() { + *i += note.advance( + {{#if features.midi_bend}} + self.midi_bend, + {{/if}} + ); + } + true + }); + } + {{/if}} + {{/if}} + {{#if features.field_helpers.json}} fn to_json_cmd(&mut self, _body: dlal_component_base::serde_json::Value) -> dlal_component_base::CmdResult { Ok(Some(dlal_component_base::json!({ diff --git a/components/base/src/lib.rs b/components/base/src/lib.rs index fc7df307..d4f7a051 100644 --- a/components/base/src/lib.rs +++ b/components/base/src/lib.rs @@ -164,6 +164,17 @@ impl Arg for serde_json::Value { } } +#[macro_export] +macro_rules! arg { + ($t:ty) => { + impl $crate::Arg for $t { + fn from_value(value: &$crate::serde_json::Value) -> Option { + $crate::serde_json::from_str(&value.to_string()).ok() + } + } + } +} + // ----- Body ----- // pub trait Body { fn has_arg(&self, index: usize) -> bool; diff --git a/components/buf/src/lib.rs b/components/buf/src/lib.rs index b3089356..6368214d 100644 --- a/components/buf/src/lib.rs +++ b/components/buf/src/lib.rs @@ -2,10 +2,12 @@ use dlal_component_base::{component, err, json, serde_json, Body, CmdResult}; use std::cmp::min; use std::collections::HashMap; +use std::path::Path; //===== Sound =====// #[derive(Clone)] struct Sound { + path: String, samples: Vec, sample_rate: u32, cresc: f32, @@ -19,6 +21,7 @@ struct Sound { impl Default for Sound { fn default() -> Self { Self { + path: String::new(), samples: Vec::new(), sample_rate: 0, cresc: 1.0, @@ -75,7 +78,16 @@ component!( "sample_rate", "run_size", "multi", - {"name": "field_helpers", "fields": ["repeat", "repeat_start", "repeat_end"], "kinds": []}, + { + "name": "field_helpers", + "fields": ["repeat_start", "repeat_end"], + "kinds": ["rw", "json"] + }, + { + "name": "field_helpers", + "fields": ["repeat"], + "kinds": ["json"] + }, ], { audio: Vec, @@ -85,8 +97,17 @@ component!( repeat_end: f32, }, { - "set": {"args": ["samples"]}, + "set": { + "args": [ + "samples", + { + "name": "note", + "optional": true, + }, + ], + }, "load": {"args": ["file_path", "note"]}, + "notes": {}, "sound_params": {"args": ["note"], "kwargs": ["cresc", "accel", "repeat"]}, "resample": {"args": ["ratio", "note"]}, "crop": {"args": ["start", "end", "note"]}, @@ -172,6 +193,7 @@ impl ComponentTrait for Component { sounds.insert( note.to_string(), json!({ + "path": sound.path, "samples": sound.samples, "sample_rate": sound.sample_rate, "cresc": sound.cresc, @@ -180,7 +202,6 @@ impl ComponentTrait for Component { }), ); } - Ok(Some(field_helper_to_json!(self, { "sounds": sounds, }))) @@ -193,6 +214,7 @@ impl ComponentTrait for Component { self.ensure_sounds(); for (note, sound) in sounds.iter() { self.sounds[note.parse::()?] = Sound { + path: sound.at("path")?, sample_rate: sound.at("sample_rate")?, samples: sound.at("samples")?, cresc: sound.at("cresc")?, @@ -219,8 +241,14 @@ impl Component { impl Component { fn set_cmd(&mut self, body: serde_json::Value) -> CmdResult { let samples = body.arg::>(0)?; - for i in 0..min(samples.len(), self.audio.len()) { - self.audio[i] = samples[i]; + let note: Option = body.arg(1).ok(); + if let Some(note) = note { + self.sounds[note].samples = samples; + self.sounds[note].sample_rate = self.sample_rate; + } else { + for i in 0..min(samples.len(), self.audio.len()) { + self.audio[i] = samples[i]; + } } Ok(None) } @@ -229,10 +257,16 @@ impl Component { self.ensure_sounds(); let file_path: String = body.arg(0)?; let note: usize = body.arg(1)?; + if !Path::new(&file_path).exists() { + return Err(err!("no such file {}", file_path).into()); + } if note >= 128 { return Err(err!("invalid note").into()); } - let mut sound = Sound { ..Default::default() }; + let mut sound = Sound { + path: file_path.clone(), + ..Default::default() + }; if file_path.ends_with(".wav") { let mut reader = hound::WavReader::open(file_path)?; let spec = reader.spec(); @@ -265,6 +299,21 @@ impl Component { Ok(None) } + fn notes_cmd(&mut self, _body: serde_json::Value) -> CmdResult { + Ok(Some(json!(self + .sounds + .iter() + .enumerate() + .filter_map(|(note, sound)| { + if sound.samples.is_empty() { + None + } else { + Some((note, sound.path.clone())) + } + }) + .collect::>()))) + } + fn sound_params_cmd(&mut self, body: serde_json::Value) -> CmdResult { let note: usize = body.arg(0)?; let sound = &mut self.sounds.get_mut(note).ok_or("invalid note")?; @@ -366,7 +415,8 @@ impl Component { if note_multiplier >= self.sounds.len() { return Err(err!("invalid note_multiplicand").into()); } - if self.sounds[note_multiplier].samples.len() < self.sounds[note_multiplicand].samples.len() { + if self.sounds[note_multiplier].samples.len() < self.sounds[note_multiplicand].samples.len() + { let len = self.sounds[note_multiplier].samples.len(); self.sounds[note_multiplicand].samples.resize(len, 0.0); } @@ -389,7 +439,7 @@ impl Component { let len = (duration * self.sample_rate as f32) as usize; let mut sin = vec![0.0; len]; for i in 0..len { - let phase = i as f32 / self.sample_rate as f32 * freq * std::f32::consts::TAU; + let phase = i as f32 / self.sample_rate as f32 * freq * std::f32::consts::TAU; sin[i] = amp * phase.sin() + offset; } self.sounds[note] = Sound::default(); @@ -404,7 +454,7 @@ impl Component { self.repeat_start = body.kwarg("start").unwrap_or(0.2) * self.sample_rate as f32; self.repeat_end = body.kwarg("end").unwrap_or(0.2) * self.sample_rate as f32; } - Ok(None) + Ok(Some(json!(self.repeat))) } fn normalize_cmd(&mut self, body: serde_json::Value) -> CmdResult { diff --git a/components/comm/Cargo.toml b/components/comm/Cargo.toml index 19f64aad..142c74a6 100644 --- a/components/comm/Cargo.toml +++ b/components/comm/Cargo.toml @@ -9,4 +9,3 @@ crate-type = ["cdylib"] [dependencies] dlal-component-base = { path = "../base" } -multiqueue2 = "0.1.6" diff --git a/components/comm/src/lib.rs b/components/comm/src/lib.rs index e1991fd8..1f47ecec 100644 --- a/components/comm/src/lib.rs +++ b/components/comm/src/lib.rs @@ -1,6 +1,6 @@ use dlal_component_base::{component, err, serde_json, Body, CmdResult, View}; -use multiqueue2::{MPMCSender, MPMCUniReceiver}; +use std::sync::mpsc::{Receiver, sync_channel, SyncSender}; #[derive(Debug)] enum QueuedItem { @@ -13,21 +13,21 @@ enum QueuedItem { } struct Queues { - to_audio_send: MPMCSender, - to_audio_recv: MPMCUniReceiver, - fro_audio_send: MPMCSender>>, - fro_audio_recv: MPMCUniReceiver>>, + to_audio_send: SyncSender, + to_audio_recv: Receiver, + fro_audio_send: SyncSender>>, + fro_audio_recv: Receiver>>, } impl Queues { - fn new(size: u64) -> Self { - let (to_audio_send, to_audio_recv) = multiqueue2::mpmc_queue(size); - let (fro_audio_send, fro_audio_recv) = multiqueue2::mpmc_queue(size); + fn new(size: usize) -> Self { + let (to_audio_send, to_audio_recv) = sync_channel(size); + let (fro_audio_send, fro_audio_recv) = sync_channel(size); Self { to_audio_send, - to_audio_recv: to_audio_recv.into_single().unwrap(), + to_audio_recv, fro_audio_send, - fro_audio_recv: fro_audio_recv.into_single().unwrap(), + fro_audio_recv, } } } @@ -121,8 +121,8 @@ impl Component { if detach { return Ok(None); } - std::thread::sleep(std::time::Duration::from_millis(body.arg(6)?)); - Ok(*self.queues.fro_audio_recv.try_recv()?) + let t = std::time::Duration::from_millis(body.arg(6)?); + Ok(*self.queues.fro_audio_recv.recv_timeout(t)?) } fn wait_cmd(&mut self, body: serde_json::Value) -> CmdResult { diff --git a/components/digitar/Cargo.toml b/components/digitar/Cargo.toml index 91786b40..e299fefa 100644 --- a/components/digitar/Cargo.toml +++ b/components/digitar/Cargo.toml @@ -9,3 +9,4 @@ crate-type = ["cdylib"] [dependencies] dlal-component-base = { path = "../base" } +serde = "1.0.203" diff --git a/components/digitar/src/lib.rs b/components/digitar/src/lib.rs index ad8ef3b0..3159fa12 100644 --- a/components/digitar/src/lib.rs +++ b/components/digitar/src/lib.rs @@ -1,15 +1,43 @@ -use dlal_component_base::{component, serde_json, CmdResult}; +use dlal_component_base::{component, serde_json, Arg, Body, CmdResult}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +enum Excitation { + Saw, + Hammer { contact: f32, offset: f32 }, +} + +impl Default for Excitation { + fn default() -> Self { + Excitation::Saw + } +} + +impl Arg for Excitation { + fn from_value(value: &serde_json::Value) -> Option { + serde_json::from_str(&value.to_string()).ok() + } +} #[derive(Default)] struct Note { on: bool, + done: bool, wavetable: Vec, + x: f32, index: f32, step: f32, lowness: f32, feedback: f32, + release: f32, freq: f32, + sample_rate: u32, sum: f32, + hammer_contact: u32, + hammer_offset: usize, + hammer_vol: f32, + hammer_displacement: u32, + bend: f32, } impl Note { @@ -20,31 +48,61 @@ impl Note { wavetable: vec![0.0; size], step: size as f32 / period, freq, + sample_rate, ..Default::default() } } - fn on(&mut self, vol: f32, lowness: f32, feedback: f32) { + fn on(&mut self, vol: f32, lowness: f32, feedback: f32, excitation: &Excitation, bend: f32) { self.on = true; + self.done = false; let size = self.wavetable.len(); - for i in 0..size { - self.wavetable[i] = vol * (2 * i) as f32 / size as f32; - if i > size / 2 { - self.wavetable[i] -= 2.0 * vol; + match excitation { + Excitation::Saw => { + for i in 0..size { + self.wavetable[i] = vol * (2 * i) as f32 / size as f32; + if i > size / 2 { + self.wavetable[i] -= 2.0 * vol; + } + } + } + Excitation::Hammer { contact, offset } => { + let length = 2093.00 / self.freq; + self.hammer_offset = ((offset / length * size as f32) as usize).min(size); + self.hammer_contact = (contact * self.sample_rate as f32) as u32; + self.hammer_vol = vol; + self.hammer_displacement = 0; } } self.index = 0.0; self.lowness = lowness.powf(self.freq / 440.0); // high freq, low lowness self.feedback = feedback.powf(440.0 / self.freq); // high freq, high feedback + self.bend = bend; } - fn off(&mut self) { + fn off(&mut self, release: f32) { self.on = false; + self.release = release; } fn advance(&mut self) -> f32 { let index = self.index as usize; let size = self.wavetable.len(); + // hammer + if self.hammer_contact != 0 { + if self.hammer_displacement < self.hammer_contact { + self.hammer_displacement += 1; + let d = self.hammer_vol / self.hammer_contact as f32; + for i in 0..self.hammer_offset { + self.wavetable[i] += d * (2.0 * i as f32 / self.hammer_offset as f32 - 1.0); + } + for i in self.hammer_offset..size { + self.wavetable[i] += d * (2.0 * (1.0 - (i - self.hammer_offset) as f32 / (size - self.hammer_offset) as f32) - 1.0); + } + } else { + self.hammer_contact = 0; + } + } // sample wavetable let sample = { let a = self.wavetable[(index + 0) % size]; @@ -53,19 +111,22 @@ impl Note { (1.0 - t) * a + t * b }; // update index and wavetable - self.index += self.step; + self.index += self.step * self.bend; let next = self.index as usize; for i in index..next { - let b = self.wavetable[(i + size - 1) % size]; - let f = self.wavetable[(i + size + 1) % size]; let i = i % size; - self.wavetable[i] = (1.0 - self.lowness) * self.wavetable[i] + self.lowness * (b + f) / 2.0; - self.wavetable[i] *= self.feedback; + self.x = self.lowness * self.x + (1.0 - self.lowness) * self.wavetable[i]; + self.wavetable[i] = self.x; + self.wavetable[i] *= if self.on { + self.feedback + } else { + 1.0 - self.release + }; self.sum += self.wavetable[i].abs(); } if next >= size { if self.sum < 0.0001 { - self.on = false; + self.done = true; } self.sum = 0.0; self.index -= size as f32; @@ -73,6 +134,10 @@ impl Note { // return sample sample } + + fn done(&self) -> bool { + self.done + } } component!( @@ -84,15 +149,25 @@ component!( "check_audio", { "name": "field_helpers", - "fields": ["lowness", "feedback", "stay_on"], + "fields": ["lowness", "feedback", "release", "stay_on"], "kinds": ["rw", "json"] }, + { + "name": "field_helpers", + "fields": ["excitation"], + "kinds": ["json"] + }, ], { lowness: f32, feedback: f32, stay_on: bool, notes: Vec, + excitation: Excitation, + release: f32, + rpn: u16, //registered parameter number + pitch_bend_range: f32, //MIDI RPN 0x0000 + bend: f32, }, { "lowness": { @@ -115,13 +190,48 @@ component!( }, ], }, + "saw": { + "desc": "Excite each string like a saw wave.", + }, + "hammer": { + "desc": "Hammer each string like a piano.", + "kwargs": [ + { + "name": "contact", + "default": 0.01, + "desc": "How long hammer contacts string for, in seconds.", + }, + { + "name": "offset", + "default": 0.5, + "desc": "Where the hammer strikes, in highest piano key (MIDI 96, 2093.00 Hz) string-lengths.", + }, + ], + }, }, ); +impl Component { + fn saw_cmd(&mut self, _body: serde_json::Value) -> CmdResult { + self.excitation = Excitation::Saw; + Ok(None) + } + + fn hammer_cmd(&mut self, body: serde_json::Value) -> CmdResult { + let contact = body.kwarg("contact").unwrap_or(0.01); + let offset = body.kwarg("offset").unwrap_or(0.5); + self.excitation = Excitation::Hammer { contact, offset }; + Ok(None) + } +} + impl ComponentTrait for Component { fn init(&mut self) { self.lowness = 0.5; self.feedback = 0.98; + self.release = 1.0; + self.bend = 1.0; + self.pitch_bend_range = 2.0; } fn join(&mut self, _body: serde_json::Value) -> CmdResult { @@ -142,7 +252,7 @@ impl ComponentTrait for Component { None => return, }; for note in &mut self.notes { - if !note.on { + if note.done() { continue; } for i in audio.iter_mut() { @@ -160,18 +270,45 @@ impl ComponentTrait for Component { if self.stay_on { return; } - self.notes[msg[1] as usize].off(); + self.notes[msg[1] as usize].off(self.release); } 0x90 => { if msg[2] == 0 && self.stay_on { return; } if msg[2] == 0 { - self.notes[msg[1] as usize].off(); + self.notes[msg[1] as usize].off(self.release); } else { - self.notes[msg[1] as usize].on(msg[2] as f32 / 127.0, self.lowness, self.feedback); + self.notes[msg[1] as usize].on( + msg[2] as f32 / 127.0, + self.lowness, + self.feedback, + &self.excitation, + self.bend, + ); + } + } + 0xb0 => { + match msg[1] { + 0x65 => self.rpn = (msg[2] << 7) as u16, + 0x64 => self.rpn += msg[2] as u16, + 0x06 => match self.rpn { + 0x0000 => self.pitch_bend_range = msg[2] as f32, + _ => (), + }, + 0x26 => match self.rpn { + 0x0000 => self.pitch_bend_range += msg[2] as f32 / 100.0, + _ => (), + }, + _ => (), } } + 0xe0 => { + const CENTER: f32 = 0x2000 as f32; + let value = (msg[1] as u16 + ((msg[2] as u16) << 7)) as f32; + let octaves = self.pitch_bend_range * (value - CENTER) / (CENTER * 12.0); + self.bend = (2.0 as f32).powf(octaves); + } _ => {} } } diff --git a/components/lim/src/lib.rs b/components/lim/src/lib.rs index 8f90ac07..8ae53fff 100644 --- a/components/lim/src/lib.rs +++ b/components/lim/src/lib.rs @@ -51,14 +51,14 @@ impl ComponentTrait for Component { for i in audio { if *i > self.soft { *i = self.soft + (*i - self.soft) * self.soft_gain; - if *i > self.hard { - *i = self.hard; - } } else if *i < -self.soft { *i = -self.soft + (*i + self.soft) * self.soft_gain; - if *i < -self.hard { - *i = -self.hard; - } + } + if *i > self.hard { + *i = self.hard; + } + else if *i < -self.hard { + *i = -self.hard; } } } diff --git a/components/vocoder/src/lib.rs b/components/vocoder/src/lib.rs index c5faa24a..4190e14c 100644 --- a/components/vocoder/src/lib.rs +++ b/components/vocoder/src/lib.rs @@ -1,24 +1,15 @@ -use dlal_component_base::{component, serde_json, CmdResult}; +use dlal_component_base::{Body, component, json, serde_json, CmdResult}; use biquad::frequency::ToHertz; use biquad::Biquad; -use std::time; - -fn noise() -> f32 { - let t = time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) - .unwrap(); - let r1 = t.as_secs(); - let r2 = t.subsec_nanos() as u64; - const PERIOD: u64 = 77777; - let rf = (r1 ^ r2) % PERIOD; - 2.0 * rf as f32 / PERIOD as f32 - 1.0 -} +const RUNS_TO_ZERO: u32 = 1024; struct Band { modulator_biquads: Vec>, carrier_biquads: Vec>, + gain: f64, + attack: f32, sustain: f32, amp: f32, } @@ -28,7 +19,9 @@ impl Default for Band { Self { modulator_biquads: vec![], carrier_biquads: vec![], - sustain: 0.99, + gain: 1.0, + attack: 0.01, + sustain: 0.999, amp: 0.0, } } @@ -39,7 +32,7 @@ impl Band { Self::default() } - fn narrow(mut self, f: f64, q: f64, sample_rate: f64) -> Self { + fn narrow(mut self, f: f64, q: f64, gain: f64, sample_rate: f64) -> Self { let bq = biquad::DirectForm2Transposed::::new( biquad::Coefficients::::from_params( biquad::Type::BandPass, @@ -49,33 +42,26 @@ impl Band { ) .unwrap(), ); - self.modulator_biquads.push(bq.clone()); - self.carrier_biquads.push(bq.clone()); - self - } - - fn high_pass(mut self, f: f64, sample_rate: f64) -> Self { - let bq = biquad::DirectForm2Transposed::::new( - biquad::Coefficients::::from_params( - biquad::Type::HighPass, - sample_rate.hz(), - f.hz(), - biquad::Q_BUTTERWORTH_F64, - ) - .unwrap(), - ); - self.modulator_biquads.push(bq.clone()); - self.carrier_biquads.push(bq.clone()); + for _ in 0..2 { + self.modulator_biquads.push(bq.clone()); + self.carrier_biquads.push(bq.clone()); + } + self.gain = gain; self } fn filter_modulator(&mut self, x: f32) { - self.amp *= self.sustain; let mut x = x as f64; for biquad in &mut self.modulator_biquads { x = biquad.run(x); } - self.amp = f32::max(x as f32, self.amp); + let x = self.gain * x.abs(); + let x = x as f32; + if x > self.amp { + self.amp = self.attack * x + (1.0 - self.attack) * self.amp; + } else { + self.amp *= self.sustain; + } } fn filter_carrier(&mut self, x: f32) -> f32 { @@ -98,49 +84,66 @@ component!( { "name": "field_helpers", "fields": [ - "tone_order", - "tone_cutoff", - "noise_cutoff" + "order", + "cutoff" ], "kinds": ["rw", "json"] }, + { + "name": "field_helpers", + "fields": [ + "freeze" + ], + "kinds": ["rw"] + }, ], { - tone_order: usize, - tone_cutoff: u32, - noise_cutoff: u32, + order: usize, + cutoff: u32, bands: Vec, + freeze: bool, + zero: u32, }, { "commit": { "args": [] }, + "read_band_amps": {}, + "freeze_with": { + "args": ["samples"], + }, }, ); impl Component { fn commit(&mut self) { let s = self.sample_rate as f64; - let t_o = self.tone_order as f64; - let t_c = self.tone_cutoff as f64; - let n_c = self.noise_cutoff as f64; - self.bands = (0..self.tone_order) + let t_o = self.order as f64; + let t_c = self.cutoff as f64; + self.bands = (0..self.order) .map(|i| { let i = i as f64; Band::new().narrow( (i + 1.0) / t_o * t_c, - 25.0 + i / 2.0, + /* + q and gains found empirically with the following goals: + - intelligible when vocoding + - relatively flat response, vocoding a chirp (carrier) with white noise (modulator) + note that bands need to get thinner (higher q) as they increase in frequency because we're using linear bands + I think this only works with order=40 and cutoff=8000 + */ + 5.0 + i / 2.0, + 8.0 / (256.0 + 0.0 * i + 8.0 * i * i + 1.0 * i * i * i), s, ) }) .collect(); - self.bands.push(Band::new().high_pass(n_c, s)); } } impl ComponentTrait for Component { fn init(&mut self) { - self.tone_order = 20; - self.tone_cutoff = 8000; - self.noise_cutoff = 8000; + self.order = 40; + self.cutoff = 8000; + self.zero = RUNS_TO_ZERO; } fn join(&mut self, _body: serde_json::Value) -> CmdResult { @@ -154,26 +157,51 @@ impl ComponentTrait for Component { Some(output) => output.audio(self.run_size).unwrap(), None => return, }; - // find band amplitudes from modulator and apply to carrier; add noise - let mut total_amp = 0.0; + // optimizations + if !self.freeze { + // if modulator is zero, and bands are zero, then all we need to do is zero the carrier + let zero = match self.audio.iter().max_by(|a, b| a.total_cmp(b)) { + Some(x) => *x < 1.0e-6, + None => true, + }; + if zero { + if self.zero >= RUNS_TO_ZERO { + for i in carrier.iter_mut() { + *i = 0.0; + } + return; + } + self.zero += 1; + } else { + self.zero = 0; + } + } else { + // if carrier is zero, and filters are zero, then we don't need to run filters + let zero = match carrier.iter().max_by(|a, b| a.total_cmp(b)) { + Some(x) => *x < 1.0e-6, + None => true, + }; + if zero { + if self.zero >= RUNS_TO_ZERO { + return; + } + self.zero += 1; + } else { + self.zero = 0; + } + } + // find band amplitudes from modulator and apply to carrier for (i_mod, i_car) in self.audio.iter().zip(carrier.iter_mut()) { let mut y = 0.0; for band in &mut self.bands { - band.filter_modulator(*i_mod); + if !self.freeze { + band.filter_modulator(*i_mod); + } y += band.filter_carrier(*i_car); - total_amp += band.amp; } *i_car = y; - // add noise based on noise band - *i_car += noise() * self.bands.last().unwrap().amp; } - // apply overall amplitude to carrier for silencier silences - total_amp /= self.run_size as f32; - total_amp = f32::min(1.0, total_amp); - for i in carrier.iter_mut() { - *i *= total_amp; - } - // reset audio + // reset modulator for i in &mut self.audio { *i = 0.0; } @@ -195,4 +223,23 @@ impl Component { self.commit(); Ok(None) } + + fn read_band_amps_cmd(&mut self, _body: serde_json::Value) -> CmdResult { + Ok(Some(json!(self.bands + .iter() + .map(|band| band.amp) + .collect::>() + ))) + } + + fn freeze_with_cmd(&mut self, body: serde_json::Value) -> CmdResult { + let x: Vec = body.arg(0)?; + for i_mod in x { + for band in &mut self.bands { + band.filter_modulator(i_mod); + } + } + self.freeze = true; + Ok(None) + } } diff --git a/out.wav b/out.wav deleted file mode 100644 index eb142ff3..00000000 Binary files a/out.wav and /dev/null differ diff --git a/output.wav b/output.wav deleted file mode 100644 index dc563a8d..00000000 Binary files a/output.wav and /dev/null differ diff --git a/skeleton/dlal/__init__.py b/skeleton/dlal/__init__.py index 1ca113dd..94741e28 100644 --- a/skeleton/dlal/__init__.py +++ b/skeleton/dlal/__init__.py @@ -13,6 +13,7 @@ if _os.environ.get('DLAL_LOG_LEVEL'): set_logger_level('dlal', _os.environ['DLAL_LOG_LEVEL']) +from . import _maths as maths from . import _sound as sound from . import _speech as speech from . import _subsystem as subsystem diff --git a/skeleton/dlal/_component.py b/skeleton/dlal/_component.py index 82cb3d38..95c586c2 100644 --- a/skeleton/dlal/_component.py +++ b/skeleton/dlal/_component.py @@ -85,7 +85,7 @@ def __del__(self): def __repr__(self): return self.name - def command(self, name, args=[], kwargs={}, timeout_ms=20): + def command(self, name, args=[], kwargs={}, timeout_ms=40): if Component._comm: log('debug', f'{self.name} queue {name} {args} {kwargs}') return Component._comm.queue(self, name, args, kwargs, timeout_ms=timeout_ms, detach=self._detach) @@ -134,6 +134,7 @@ def py(command): for k, v in inspect.getmembers(self): if k.startswith('_') and k != '__init__': continue if not callable(v): continue + if type(v) == type: continue if k in covered: continue if v.__func__ == getattr(Component, k, None): continue py_only.append({ diff --git a/skeleton/dlal/_maths.py b/skeleton/dlal/_maths.py new file mode 100644 index 00000000..324cf99d --- /dev/null +++ b/skeleton/dlal/_maths.py @@ -0,0 +1,42 @@ +import math as _math + +def define_phase(sample_rate): + class Phase: + def __init__(self): + self.value = 0 + def __iadd__(self, freq): + self.value += freq / sample_rate + self.value %= 1 + return self + return Phase + +class Generator: + def __init__(self, sample_rate): + self.sample_rate = sample_rate + self.Phase = define_phase(sample_rate) + self.init() + + def generate(self): + samples = [] + while True: + a = self.amp(len(samples) / self.sample_rate) + if a == None: break + samples.append(a) + return samples + + def sqr(self, phase): + if phase.value < 0.5: + return -1 + else: + return +1 + + def saw(self, phase): + return 2 * phase.value - 1 + + def sin(self, phase): + return _math.sin(_math.tau * phase.value) + + def ramp(self, a, b, t): + if t < 0: return a + if t > 1: return b + return (1 - t) * a + t * b diff --git a/skeleton/dlal/_skeleton.py b/skeleton/dlal/_skeleton.py index 9685fbe1..23e97f44 100644 --- a/skeleton/dlal/_skeleton.py +++ b/skeleton/dlal/_skeleton.py @@ -6,6 +6,7 @@ from ._component import Component as _Component, component_kinds from ._server import audio_broadcast_start, serve +from . import _sound from ._utils import ( snake_to_upper_camel_case as _snake_to_upper_camel_case, iterable as _iterable, @@ -264,9 +265,10 @@ def disconnect(*args): Useful for agnostic disconnection more than for disconnecting complex structures.''' connect(*args, _dis=True) -def typical_setup(*, duration=None, out_path='out.i16le'): +def typical_setup(*, duration=None, out_path='out.i16le', flac_path=True): import atexit import os + from pathlib import Path import sys import time audio = component('audio', None) @@ -280,17 +282,23 @@ def typical_setup(*, duration=None, out_path='out.i16le'): comm_set(comm) serve() else: - assert tape + assert tape, 'No tape. For live audio, run Python interactively.' assert audio - assert duration + assert duration, 'No duration specified. For live audio, run Python interactively.' runs = int(duration * audio.sample_rate() / audio.run_size()) n = tape.size() // audio.run_size() with open(out_path, 'wb') as file: + print('running') for i in range(runs): audio.run() if i % n == n - 1 or i == runs - 1: tape.to_file_i16le(file) print(f'{100*(i+1)/runs:5.1f} %', end='\r') print() + if flac_path: + if flac_path == True: + flac_path = Path(sys.argv[0]).with_suffix('.flac').name + print('converting to FLAC') + _sound.i16le_to_flac(out_path, flac_path) def system_info(): return { diff --git a/skeleton/dlal/_sound.py b/skeleton/dlal/_sound.py index f742c4ff..9251c3aa 100644 --- a/skeleton/dlal/_sound.py +++ b/skeleton/dlal/_sound.py @@ -1,13 +1,23 @@ import soundfile as sf import re as _re +import struct as _struct +import subprocess as _subprocess class Sound: - def __init__(self, samples, sample_rate): + def __init__(self, samples, sample_rate=44100): self.samples = samples self.sample_rate = sample_rate - def to_flac(self, file_path): + def to_i16le(self, file_path='out.i16le'): + with open(file_path, 'wb') as file: + for sample in self.samples: + i = int(sample * 0x7fff) + if i > 0x7fff: i = 0x7fff + elif i < -0x8000: i = -0x8000 + file.write(_struct.pack('> 7 - synth.midi([0xe1, l, h]) + voice = self.components[f'voice{i}'] + if per_voice_init: per_voice_init(voice, i) + if n > 1: + bend = 0x2000 + int(0x500 * cents * (2*i/(n-1) - 1)) + l = bend & 0x7f + h = bend >> 7 + voice.midi([0xe1, l, h]) diff --git a/skeleton/dlal/addsyn.py b/skeleton/dlal/addsyn.py new file mode 100644 index 00000000..6a3e0543 --- /dev/null +++ b/skeleton/dlal/addsyn.py @@ -0,0 +1,36 @@ +from ._component import Component + +class Addsyn(Component): + def __init__(self, preset=None, **kwargs): + Component.__init__(self, 'addsyn', **kwargs) + + def rissets_bells(self, duration=4): + d = 1 / duration + self.partials([ + {'v': 0.10, 'a': 1e4, 'd': 1.0*d, 's': 0, 'r': 1e4, 'm': 0.56, 'b': 0.0}, + {'v': 0.07, 'a': 1e4, 'd': 1.1*d, 's': 0, 'r': 1e4, 'm': 0.56, 'b': 1.0}, + {'v': 0.10, 'a': 1e4, 'd': 1.5*d, 's': 0, 'r': 1e4, 'm': 0.92, 'b': 0.0}, + {'v': 0.18, 'a': 1e4, 'd': 1.8*d, 's': 0, 'r': 1e4, 'm': 0.92, 'b': 1.7}, + {'v': 0.27, 'a': 1e4, 'd': 3.1*d, 's': 0, 'r': 1e4, 'm': 1.19, 'b': 0.0}, + {'v': 0.17, 'a': 1e4, 'd': 2.8*d, 's': 0, 'r': 1e4, 'm': 1.70, 'b': 0.0}, + {'v': 0.15, 'a': 1e4, 'd': 4.0*d, 's': 0, 'r': 1e4, 'm': 2.00, 'b': 0.0}, + {'v': 0.13, 'a': 1e4, 'd': 5.0*d, 's': 0, 'r': 1e4, 'm': 2.74, 'b': 0.0}, + {'v': 0.13, 'a': 1e4, 'd': 6.6*d, 's': 0, 'r': 1e4, 'm': 3.00, 'b': 0.0}, + {'v': 0.10, 'a': 1e4, 'd': 9.8*d, 's': 0, 'r': 1e4, 'm': 3.76, 'b': 0.0}, + {'v': 0.13, 'a': 1e4, 'd': 14.3*d, 's': 0, 'r': 1e4, 'm': 4.07, 'b': 0.0}, + ]) + return self + + def tubular_bells(self): + self.partials([ + {'v': 0.02, 'a': 1e4, 'd': 20, 's': 0, 'r': 1e4, 'm': 0.63, 'b': 0.0}, + {'v': 0.46, 'a': 1e4, 'd': 20, 's': 0, 'r': 1e4, 'm': 1.24, 'b': 0.0}, + {'v': 0.08, 'a': 1e4, 'd': 20, 's': 0, 'r': 1e4, 'm': 2.00, 'b': 0.0}, + {'v': 0.50, 'a': 1e4, 'd': 20, 's': 0, 'r': 1e4, 'm': 2.94, 'b': 0.0}, + {'v': 0.05, 'a': 1e4, 'd': 20, 's': 0, 'r': 1e4, 'm': 3.17, 'b': 0.0}, + {'v': 0.09, 'a': 1e4, 'd': 25, 's': 0, 'r': 1e4, 'm': 4.01, 'b': 0.0}, + {'v': 4e-3, 'a': 1e4, 'd': 35, 's': 0, 'r': 1e4, 'm': 5.21, 'b': 0.0}, + {'v': 8e-4, 'a': 1e4, 'd': 25, 's': 0, 'r': 1e4, 'm': 6.37, 'b': 0.0}, + ]) + self.decay_mode('Exponential') + return self diff --git a/skeleton/dlal/audio.py b/skeleton/dlal/audio.py index e52d9fa2..eb57048a 100644 --- a/skeleton/dlal/audio.py +++ b/skeleton/dlal/audio.py @@ -69,3 +69,17 @@ def set_cross_state(self, state): from ._skeleton import component for name in state['components']: self.add(component(name), state['slots'][name]) + + def profile_print(self): + profiles = self.profiles() + for profile in profiles: + total = 0 + durs = [] + for i in profile: + dur = i['duration']['secs'] * 1e3 + i['duration']['nanos'] / 1e6 + durs.append((i['name'], dur)) + total += dur + durs.sort(key=lambda i: -i[1]) + for name, dur in durs: + print(f'{name:7} {dur:7.3f}', end=', ') + print(f'total {total: 7.3f} ms') diff --git a/skeleton/dlal/buf.py b/skeleton/dlal/buf.py index abffd10c..1d40c24d 100644 --- a/skeleton/dlal/buf.py +++ b/skeleton/dlal/buf.py @@ -4,7 +4,98 @@ import glob import os +midi_drums = { + #27: 'high-q.wav', + #28: 'slap.wav', + #29: 'scratch-push.wav', + #30: 'scratch-pull.wav', + #31: 'sticks.wav', + #32: 'square-click.wav', + #33: 'metronome-click.wav', + #34: 'metronome-bell.wav', + 35: 'bass.wav', + 36: 'bass.wav', + 37: 'side-stick.wav', + 38: 'snare.wav', + 39: 'clap.wav', + 40: 'snare.wav', + 41: 'floor-tom.wav', + 42: 'hat.wav', + 43: 'floor-tom.wav', + #44: 'pedal-hat.wav', + 45: 'low-tom.wav', + #46: 'open-hat.wav', + 47: 'mid-tom.wav', + 48: 'mid-tom.wav', + 49: 'crash.wav', + 50: 'high-tom.wav', + 51: 'ride.wav', + #52: 'chinese-cymbal.wav', + 53: 'ride-bell.wav', + #54: 'tambourine.wav', + #55: 'splash-cymbal.wav', + 56: 'cowbell.wav', + 57: 'crash.wav', + #58: 'Vibra Slap', + 59: 'ride.wav', + 60: 'bongo-hi.wav', + 61: 'bongo-lo.wav', + #62: 'Mute High Conga', + #63: 'Open High Conga', + #64: 'Low Conga', + #65: 'High Timbale', + #66: 'Low Timbale', + #67: 'High Agogo', + #68: 'Low Agogo', + #69: 'Cabasa', + #70: 'Maracas', + #71: 'Short Whistle', + #72: 'Long Whistle', + #73: 'Short Guiro', + 74: 'guiro.wav', + #75: 'Claves', + #76: 'High Wood Block', + #77: 'Low Wood Block', + 78: 'cuica.wav', + 79: 'cuica-open.wav', + #80: 'Mute Triangle', + #81: 'Open Triangle', + 82: 'shaker1.wav', + #83: 'Jingle Bell', + #84: 'Belltree', + #85: 'Castanets', + #86: 'Mute Surdo', + #87: 'Open Surdo', +} + class Buf(Component): + class Drum: + bass = 35 + bass_1 = 36 + side_stick = 37 + snare = 38 + clap = 39 + electric_snare = 40 + low_floor_tom = 41 + closed_hat = 42 + high_floor_tom = 43 + low_tom = 45 + low_mid_tom = 47 + high_mid_tom = 48 + crash = 49 + high_tom = 50 + ride = 51 + ride_bell = 53 + cowbell = 56 + crash_2 = 57 + ride_2 = 59 + high_bongo = 60 + low_bongo = 61 + long_guiro = 74 + mute_cuica = 78 + open_cuica = 79 + shaker = 82 + def __init__(self, instrument=None, **kwargs): Component.__init__(self, 'buf', **kwargs) if instrument: @@ -13,6 +104,9 @@ def __init__(self, instrument=None, **kwargs): def load(self, file_path, note): return self.command_immediate('load', [file_path, note]) + def load_asset(self, asset_path, note): + return self.load(os.path.join(ASSETS_DIR, 'sounds', asset_path), note) + def load_all(self): pattern = os.path.join(ASSETS_DIR, 'sounds', '*', '*.wav') for i, file_path in enumerate(glob.glob(pattern)): @@ -26,6 +120,17 @@ def load_pitched(self, instrument='?'): for i in os.listdir(path): self.load(os.path.join(path, i), int(i.split('.')[0])) + def load_drums(self): + for note, file_path in midi_drums.items(): + self.load(os.path.join(ASSETS_DIR, 'sounds', 'drum', file_path), note) + + def amplify(self, amount, note=None): + if note != None: + return self.command('amplify', [amount, note]) + else: + for note in range(128): + self.command('amplify', [amount, note]) + def plot(self, note): samples = self.to_json()['_extra']['sounds'][str(note)]['samples'] import dansplotcore as dpc diff --git a/skeleton/dlal/digitar.py b/skeleton/dlal/digitar.py index 5c4933b5..7b21da44 100644 --- a/skeleton/dlal/digitar.py +++ b/skeleton/dlal/digitar.py @@ -1,7 +1,8 @@ from ._component import Component class Digitar(Component): - def __init__(self, lowness=None, feedback=None, **kwargs): + def __init__(self, lowness=None, feedback=None, release=None, **kwargs): Component.__init__(self, 'digitar', **kwargs) if lowness != None: self.lowness(lowness) if feedback != None: self.feedback(feedback) + if release != None: self.release(release) diff --git a/smoke.mp3 b/smoke.mp3 deleted file mode 100644 index 366a2e95..00000000 Binary files a/smoke.mp3 and /dev/null differ diff --git a/smoke.wav b/smoke.wav deleted file mode 100644 index 316c72f2..00000000 Binary files a/smoke.wav and /dev/null differ diff --git a/systems/choirist.py b/systems/choirist.py new file mode 100644 index 00000000..ba77c666 --- /dev/null +++ b/systems/choirist.py @@ -0,0 +1,67 @@ +import dlal +import midi + +class Choirist(dlal.subsystem.Subsystem): + def init(self, name=None): + dlal.subsystem.Subsystem.init( + self, + { + 'midman': 'midman', + 'rhymel': 'rhymel', + 'lpf': ('lpf', [0.9992]), + 'oracle': 'oracle', + 'sonic': 'sonic', + 'vocoder': 'vocoder', + 'lim': ('lim', [0.25, 0.15]), + 'buf': 'buf', + }, + ['rhymel'], + ['buf'], + name=name, + ) + dlal.connect( + self.midman, + [self.rhymel, '+>', self.sonic], + [self.oracle, '<+', self.lpf], + self.sonic, + [self.buf, '<+', self.lim], + ) + dlal.connect(self.vocoder, self.buf) + self.midman.directive([{'nibble': 0x90}], 0, 'midi', [0x90, '%1', 0]) + self.oracle.mode('pitch_wheel') + self.oracle.m(0x4000) + self.oracle.format('midi', [0xe0, '%l', '%h']) + self.sonic.from_json({ + "0": { + "a": 1e-4, "d": 0, "s": 1, "r": 1e-4, "m": 1, + "i0": 0, "i1": 0.3, "i2": 0.2, "i3": 0.1, "o": 0.125, + }, + "1": { + "a": 1, "d": 0, "s": 1, "r": 1e-5, "m": 1, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + "2": { + "a": 1, "d": 0, "s": 1, "r": 1e-5, "m": 3, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + "3": { + "a": 1, "d": 0, "s": 1, "r": 1e-5, "m": 5, + "i0": 0, "i1": 0, "i2": 0, "i3": 0, "o": 0, + }, + }) + self.sonic.midi(midi.Msg.pitch_bend_range(64)) + + def post_add_init(self): + self.vocoder.freeze_with(dlal.sound.read('assets/phonetics/a.flac').samples[44100:44100+64*1024]) + +audio = dlal.Audio(driver=True) +midi_ = dlal.Midi() +choirist = Choirist() + +dlal.connect( + midi_, + choirist, + audio, +) + +dlal.typical_setup() diff --git a/systems/vocoder.py b/systems/vocoder.py index 23acb47c..bcd07dfb 100644 --- a/systems/vocoder.py +++ b/systems/vocoder.py @@ -2,37 +2,34 @@ import dlal -import time +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('carrier_path', help='The sound to be vocoded. Usually a rich sound.') +parser.add_argument('modulator_path', help='A sound whose spectrogram will be used to filter the carrier. Usually speech.') +args = parser.parse_args() audio = dlal.Audio(driver=True) -comm = dlal.Comm() -midi = dlal.Midi() -osc = dlal.Osc('saw') -audio.add(audio) +carrier = dlal.Afr(args.carrier_path) +modulator = dlal.Afr(args.modulator_path) vocoder = dlal.Vocoder() buf = dlal.Buf() -tape = dlal.Tape(1 << 17) +tape = dlal.Tape() dlal.connect( - midi, - osc, - [buf, '<+', vocoder, audio], - [audio, tape], + carrier, + [buf, '<+', vocoder, modulator], + tape, ) -midi.midi([0x90, 41, 0x40]) - -dlal.typical_setup() - -def rec(duration=5, pause=3): - print('recording in') - for i in range(pause, 0, -1): - print(i) - time.sleep(1) - print('recording') - tape.to_file_i16le_start() - for i in range(duration): - print(f'{i} / {duration}') - time.sleep(1) - tape.to_file_i16le_stop() - print('done') +duration = carrier.duration() +file = open('out.i16le', 'wb') +while carrier.playing(): + audio.run() + tape.to_file_i16le(file) + print(f'{carrier.elapsed():9.3f} s', end=' ') + for amp in vocoder.read_band_amps(): + print(f'{amp:6.2f}', end=' ') + print() +file.close() +dlal.sound.i16le_to_flac('out.i16le')