mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-13 23:08:25 +00:00
Merge #328
This commit is contained in:
commit
b4f7c4af6d
5 files changed
+94
-11
No files matched your search
@@ -301,6 +301,11 @@ pub fn plugin_info(id: &str) -> Option<&'static PluginInfo> {
|
||||
plugins::REGISTRY.iter().copied().find(|i| i.id == id)
|
||||
}
|
||||
|
||||
/// Whether a plugin makes sound instead of processing its input (it ignores the input).
|
||||
pub fn is_generator(id: &str) -> bool {
|
||||
id == "signal_generator"
|
||||
}
|
||||
|
||||
/// Silence floor used by the dB helpers.
|
||||
pub const MIN_DB: f32 = -144.0;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
use soundcraft_dsp::{Plugin, Unit, create, db_to_gain, gain_to_db, plugin_info, plugins};
|
||||
use soundcraft_dsp::{Plugin, Unit, create, db_to_gain, gain_to_db, is_generator, plugin_info, plugins};
|
||||
|
||||
const SR: f32 = 48_000.0;
|
||||
const BLOCK: usize = 512;
|
||||
@@ -45,7 +45,7 @@ fn prepared(id: &str) -> Box<dyn Plugin> {
|
||||
}
|
||||
|
||||
fn is_source(id: &str) -> bool {
|
||||
plugin_info(id).unwrap().is_instrument || id == "signal_generator"
|
||||
plugin_info(id).unwrap().is_instrument || is_generator(id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::*;
|
||||
use crate::cmd;
|
||||
use serde_json::json;
|
||||
use soundcraft_audio_io::{AudioBuffer, FileFormat};
|
||||
use soundcraft_model::{Clip, ClipContent};
|
||||
use soundcraft_model::{Clip, ClipContent, TrackKind};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
@@ -12,7 +12,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"AudioSuite Process",
|
||||
[],
|
||||
None,
|
||||
"{process: <plugin id> | normalize | reverse | invert | gain | duplicate | time_stretch | pitch_shift | varispeed | signal_generator, params?: {..}, clips?}",
|
||||
"{process: <plugin id> | normalize | reverse | invert | gain | duplicate | time_stretch | pitch_shift | varispeed | signal_generator, params?: {..}, clips?, tracks?, start?, end?} — signal_generator fills start..end (default: the selection) with a new clip on each audio track in `tracks` (default: the selected tracks)",
|
||||
has_selection,
|
||||
process
|
||||
)]
|
||||
@@ -35,6 +35,12 @@ fn process(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
if !known {
|
||||
return Err(bad("audiosuite.process", format!("unknown process `{name}`")));
|
||||
}
|
||||
if soundcraft_dsp::is_generator(&name) && p.get("clips").is_none() && p.get("clip").is_none() {
|
||||
let r = range_param(e, "audiosuite.process", p)?;
|
||||
if !r.is_empty() {
|
||||
return generate(e, p, &name, ¶ms, r);
|
||||
}
|
||||
}
|
||||
let ids = clip_ids_param(e, p);
|
||||
if ids.is_empty() {
|
||||
return Err(bad("audiosuite.process", "select audio clips first"));
|
||||
@@ -95,12 +101,7 @@ fn process(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
"pitch_shift" => soundcraft_dsp::offline::pitch_shift(&audio, semitones, srf),
|
||||
"varispeed" => soundcraft_dsp::offline::resample(&audio, sr.hz(), to),
|
||||
plugin => {
|
||||
let Some(mut pl) = soundcraft_dsp::create(plugin) else { return Err(bad("audiosuite.process", format!("cannot create `{plugin}`"))) };
|
||||
pl.prepare(srf, 1024, audio.len().max(1));
|
||||
for (k, v) in ¶ms {
|
||||
pl.set_param(k, *v);
|
||||
}
|
||||
soundcraft_dsp::offline::apply_plugin(&mut audio, pl.as_mut(), srf);
|
||||
apply(plugin, ¶ms, &mut audio, srf)?;
|
||||
audio
|
||||
}
|
||||
};
|
||||
@@ -131,6 +132,46 @@ fn process(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"processed": done, "process": name}))
|
||||
}
|
||||
|
||||
/// Fill `r` on each selected audio track with a new clip from the generator `name`, over
|
||||
/// whatever was there, like Pro Tools' Signal Generator.
|
||||
fn generate(e: &mut Engine, p: &Value, name: &str, params: &[(String, f32)], r: Range) -> Result<Value> {
|
||||
let sr = e.session().sample_rate;
|
||||
let tracks = tracks_param(e, "audiosuite.process", p)?;
|
||||
// Refuse before generating anything, rather than cut the clip short.
|
||||
for t in tracks.iter().filter_map(|t| e.session().track(*t)).filter(|t| t.kind == TrackKind::Audio) {
|
||||
crate::io::refuse_longer(e.session(), "audiosuite.process", &t.name, "generate on", r.len(), crate::io::render_limit(t.channels()))?;
|
||||
}
|
||||
let mut clips = Vec::new();
|
||||
for track in tracks {
|
||||
let Some(t) = e.session().track(track).filter(|t| t.kind == TrackKind::Audio) else { continue };
|
||||
let channels = t.channels().max(1);
|
||||
let frames = usize::try_from(r.len()).unwrap_or(0).min(crate::io::render_limit(channels));
|
||||
let clip_name = format!("{}-{}", t.name, short(name));
|
||||
let mut audio = vec![vec![0.0; frames]; channels];
|
||||
apply(name, params, &mut audio, sr.as_f64() as f32)?;
|
||||
let s = e.session_mut();
|
||||
let src = crate::io::add_source(s, &clip_name, AudioBuffer { sample_rate: sr.hz(), channels: audio }, None, FileFormat::Wav);
|
||||
let id = s.new_clip_id();
|
||||
crate::edit::place_clip(s, track, Clip::audio(id, &clip_name, src, 0, r.start, i64::try_from(frames).unwrap_or(0)));
|
||||
clips.push(id.0);
|
||||
}
|
||||
if clips.is_empty() {
|
||||
return Err(bad("audiosuite.process", "select an audio track to generate on"));
|
||||
}
|
||||
Ok(json!({"processed": clips.len(), "process": name, "clips": clips}))
|
||||
}
|
||||
|
||||
/// Run plugin `id` with `params` over `audio`, in place.
|
||||
fn apply(id: &str, params: &[(String, f32)], audio: &mut [Vec<f32>], srf: f32) -> Result<()> {
|
||||
let Some(mut pl) = soundcraft_dsp::create(id) else { return Err(bad("audiosuite.process", format!("cannot create `{id}`"))) };
|
||||
pl.prepare(srf, 1024, audio.len().max(1));
|
||||
for (k, v) in params {
|
||||
pl.set_param(k, *v);
|
||||
}
|
||||
soundcraft_dsp::offline::apply_plugin(audio, pl.as_mut(), srf);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn short(n: &str) -> String {
|
||||
let s: String = n.split('_').map(|w| w.chars().take(4).collect::<String>()).collect::<Vec<_>>().join("");
|
||||
s.chars().take(8).collect()
|
||||
@@ -174,4 +215,28 @@ mod tests {
|
||||
let sample = e.session().pool.get(c.source().unwrap()).unwrap().buffer.channels[0][0];
|
||||
assert!((sample - 0.5).abs() < 1e-3, "duplicate keeps the file level and the clip gain, got {sample}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_generator_fills_the_selection_with_a_clip() {
|
||||
let (mut e, _) = gained_clip();
|
||||
let tone = e.session().tracks[0].id;
|
||||
let t = e.session_mut().add_track(TrackKind::Audio, ChannelFormat::Stereo, Some("Empty"));
|
||||
e.execute("edit.select", &json!({"tracks": [t.0], "start": 48_000, "end": 96_000})).unwrap();
|
||||
assert!(matches!(e.execute("audiosuite.process", &json!({"process": "normalize"})), Err(crate::EngineError::BadParams(..))));
|
||||
|
||||
let generator = json!({"process": "signal_generator", "params": {"freq": 440.0, "level": -6.0}});
|
||||
assert_eq!(e.execute("audiosuite.process", &generator).unwrap()["processed"], 1);
|
||||
let c = e.session().tracks[1].clips()[0].clone();
|
||||
assert_eq!((c.start, c.length), (48_000, 48_000));
|
||||
let buf = &e.session().pool.get(c.source().unwrap()).unwrap().buffer;
|
||||
assert_eq!(buf.channels.len(), 2);
|
||||
let peak = buf.channels[1].iter().fold(0.0f32, |m, x| m.max(x.abs()));
|
||||
assert!((peak - 0.5).abs() < 0.01, "-6 dB sine peaks near 0.5, got {peak}");
|
||||
|
||||
// Over existing audio it replaces just the selection.
|
||||
e.execute("edit.select", &json!({"tracks": [tone.0], "start": 32, "end": 96})).unwrap();
|
||||
e.execute("audiosuite.process", &generator).unwrap();
|
||||
let ranges: Vec<_> = e.session().tracks[0].clips().iter().map(|c| (c.start, c.length)).collect();
|
||||
assert_eq!(ranges, vec![(0, 32), (32, 64)]);
|
||||
}
|
||||
}
|
||||
@@ -47,3 +47,11 @@ fn audiosuite_refuses_results_longer_than_one_render() {
|
||||
refused(&mut e, "audiosuite.process", &json!({"process": process, "params": params, "clips": [id.0]}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_generator_refuses_a_selection_longer_than_one_render() {
|
||||
let (mut e, _) = long_clip(48_000);
|
||||
let end = (MAX_RENDER_SAMPLES / 2 + 1) as i64;
|
||||
e.execute("edit.select", &json!({"tracks": ["Talk"], "start": 0, "end": end})).unwrap();
|
||||
refused(&mut e, "audiosuite.process", &json!({"process": "signal_generator"}));
|
||||
}
|
||||
@@ -688,7 +688,12 @@ fn audiosuite_window(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
ui.ctx().memory_mut(|m| m.data.insert_temp(key, params.clone()));
|
||||
ui.label(egui::RichText::new("Processes the selected clips and replaces them with rendered audio.").small());
|
||||
let hint = if soundcraft_dsp::is_generator(&process) {
|
||||
"Fills the selection on the selected tracks with a new clip."
|
||||
} else {
|
||||
"Processes the selected clips and replaces them with rendered audio."
|
||||
};
|
||||
ui.label(egui::RichText::new(hint).small());
|
||||
if ui.button(egui::RichText::new("Render").strong()).clicked() {
|
||||
let p: serde_json::Map<String, Value> = params.iter().map(|(k, v)| (k.clone(), json!(v))).collect();
|
||||
let _ = app.run("audiosuite.process", json!({"process": process, "params": p}));
|
||||
|
||||
Reference in new issue
Block a user