Check and Windows preview / check (anvil) (push) Successful in 2m5s
Check and Windows preview / check (crucible) (push) Successful in 2m11s
Check and Windows preview / check (lathe) (push) Successful in 2m22s
Check and Windows preview / check (patina) (push) Successful in 2m50s
Check and Windows preview / check (rivet) (push) Successful in 46s
Check and Windows preview / check (temper) (push) Successful in 41s
Check and Windows preview / check (weld) (push) Successful in 39s
Check and Windows preview / previews (push) Failing after 24s
Check and Windows preview / windows (push) Skipped
Set the CLAP vendor of all seven plugins to "Bass Foundry" and name the combined preview ZIP bass-foundry-windows-x86_64.zip. CLAP IDs and saved state stay unchanged. Bring development.md and releases.md up to date with all seven products, the shared crates, the test types, the steps to add a product and the published releases. Add texture problems and seven texture plugin ideas to ideas.md.
201 lines
6.9 KiB
Rust
201 lines
6.9 KiB
Rust
//! CLAP audio effect. Audio passes through unchanged; the input is recorded continuously.
|
|
use crucible_core::{Player, Queues, Recorder, Ring, Settings, Transport, HISTORY};
|
|
use nih_plug::prelude::*;
|
|
use nih_plug_egui::{create_egui_editor, egui, EguiState};
|
|
use std::sync::{Arc, Mutex, RwLock};
|
|
|
|
#[derive(Params)]
|
|
pub struct CrucibleParams {
|
|
#[persist = "editor"]
|
|
pub editor: Arc<EguiState>,
|
|
#[persist = "crucible-settings"]
|
|
pub settings: RwLock<Settings>,
|
|
#[id = "bypass"]
|
|
pub bypass: BoolParam,
|
|
}
|
|
impl Default for CrucibleParams {
|
|
fn default() -> Self {
|
|
Self {
|
|
editor: EguiState::from_size(crucible_ui::WIDTH, crucible_ui::HEIGHT),
|
|
settings: RwLock::new(Settings::default()),
|
|
bypass: BoolParam::new("Bypass", false).make_bypass(),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Shared {
|
|
/// Replaced on activation; the editor clones the Arc briefly, never the audio thread.
|
|
ring: Mutex<Option<Arc<Ring>>>,
|
|
queues: Arc<Queues>,
|
|
}
|
|
pub struct Crucible {
|
|
params: Arc<CrucibleParams>,
|
|
shared: Arc<Shared>,
|
|
ring: Option<Arc<Ring>>,
|
|
recorder: Recorder,
|
|
player: Player,
|
|
}
|
|
impl Default for Crucible {
|
|
fn default() -> Self {
|
|
Self {
|
|
params: Arc::new(CrucibleParams::default()),
|
|
shared: Arc::new(Shared {
|
|
ring: Mutex::new(None),
|
|
queues: Arc::new(Queues::default()),
|
|
}),
|
|
ring: None,
|
|
recorder: Recorder::new(),
|
|
player: Player::default(),
|
|
}
|
|
}
|
|
}
|
|
impl Plugin for Crucible {
|
|
const NAME: &'static str = "Crucible";
|
|
const VENDOR: &'static str = "Bass Foundry";
|
|
const URL: &'static str = "";
|
|
const EMAIL: &'static str = "";
|
|
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
|
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
|
|
AudioIOLayout {
|
|
main_input_channels: NonZeroU32::new(2),
|
|
main_output_channels: NonZeroU32::new(2),
|
|
..AudioIOLayout::const_default()
|
|
},
|
|
AudioIOLayout {
|
|
main_input_channels: NonZeroU32::new(1),
|
|
main_output_channels: NonZeroU32::new(1),
|
|
..AudioIOLayout::const_default()
|
|
},
|
|
];
|
|
const SAMPLE_ACCURATE_AUTOMATION: bool = false;
|
|
type SysExMessage = ();
|
|
type BackgroundTask = ();
|
|
fn params(&self) -> Arc<dyn Params> {
|
|
self.params.clone()
|
|
}
|
|
fn editor(&mut self, _: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
|
let params = self.params.clone();
|
|
let shared = self.shared.clone();
|
|
create_egui_editor(
|
|
params.editor.clone(),
|
|
crucible_ui::State::default(),
|
|
|ctx, _| crucible_ui::style(ctx),
|
|
move |ctx, _setter, state| {
|
|
let link = crucible_ui::Link {
|
|
ring: shared.ring.lock().ok().and_then(|r| r.clone()),
|
|
queues: shared.queues.clone(),
|
|
};
|
|
let mut settings = params
|
|
.settings
|
|
.read()
|
|
.map(|s| s.clone())
|
|
.unwrap_or_default();
|
|
let before = settings.clone();
|
|
nih_plug_egui::resizable_window::ResizableWindow::new("crucible-editor")
|
|
.min_size(egui::vec2(
|
|
crucible_ui::WIDTH as f32 - 80.,
|
|
crucible_ui::HEIGHT as f32 - 40.,
|
|
))
|
|
.show(ctx, ¶ms.editor, |ui| {
|
|
crucible_ui::show(ui, &mut settings, state, &link)
|
|
});
|
|
if settings != before {
|
|
if let Ok(mut s) = params.settings.write() {
|
|
*s = settings;
|
|
}
|
|
}
|
|
// Meters and the transport readout follow the audio.
|
|
ctx.request_repaint_after(std::time::Duration::from_millis(50));
|
|
},
|
|
)
|
|
}
|
|
fn initialize(
|
|
&mut self,
|
|
_: &AudioIOLayout,
|
|
config: &BufferConfig,
|
|
_: &mut impl InitContext<Self>,
|
|
) -> bool {
|
|
if let Ok(mut s) = self.params.settings.write() {
|
|
s.sanitize();
|
|
}
|
|
let ring = Arc::new(Ring::new(config.sample_rate, HISTORY));
|
|
self.ring = Some(ring.clone());
|
|
self.recorder = Recorder::new();
|
|
self.player = Player::default();
|
|
if let Ok(mut slot) = self.shared.ring.lock() {
|
|
*slot = Some(ring);
|
|
}
|
|
true
|
|
}
|
|
fn reset(&mut self) {
|
|
self.player = Player::default();
|
|
}
|
|
fn process(
|
|
&mut self,
|
|
buffer: &mut Buffer,
|
|
_: &mut AuxiliaryBuffers,
|
|
context: &mut impl ProcessContext<Self>,
|
|
) -> ProcessStatus {
|
|
let Some(ring) = self.ring.as_deref() else {
|
|
return ProcessStatus::Normal;
|
|
};
|
|
let bypass = self.params.bypass.value();
|
|
let t = context.transport();
|
|
let transport = match (t.tempo, t.pos_beats()) {
|
|
(Some(tempo), Some(beats)) => Some(Transport {
|
|
playing: t.playing,
|
|
tempo,
|
|
beats,
|
|
numerator: t.time_sig_numerator.unwrap_or(4).clamp(1, 64) as u32,
|
|
denominator: t.time_sig_denominator.unwrap_or(4).clamp(1, 64) as u32,
|
|
}),
|
|
_ => None,
|
|
};
|
|
if !bypass {
|
|
self.recorder.block(ring, transport);
|
|
}
|
|
self.player.block(&self.shared.queues);
|
|
let channels = buffer.channels();
|
|
for mut sample in buffer.iter_samples() {
|
|
let left = *sample.get_mut(0).unwrap();
|
|
let right = if channels > 1 {
|
|
*sample.get_mut(1).unwrap()
|
|
} else {
|
|
left
|
|
};
|
|
if bypass {
|
|
continue;
|
|
}
|
|
self.recorder.push(ring, [left, right]);
|
|
// Auditioned slices are added to the output, never recorded.
|
|
let a = self.player.next(&self.shared.queues);
|
|
if self.player.playing() || a != [0.; 2] {
|
|
*sample.get_mut(0).unwrap() += if channels > 1 {
|
|
a[0]
|
|
} else {
|
|
0.5 * (a[0] + a[1])
|
|
};
|
|
if channels > 1 {
|
|
*sample.get_mut(1).unwrap() += a[1];
|
|
}
|
|
}
|
|
}
|
|
self.recorder.publish(ring);
|
|
ProcessStatus::Normal
|
|
}
|
|
}
|
|
impl ClapPlugin for Crucible {
|
|
const CLAP_ID: &'static str = "io.storyboard.resampler";
|
|
const CLAP_DESCRIPTION: Option<&'static str> =
|
|
Some("Capture the last bars, slice them and export clean, key-labelled samples");
|
|
const CLAP_MANUAL_URL: Option<&'static str> = None;
|
|
const CLAP_SUPPORT_URL: Option<&'static str> = None;
|
|
const CLAP_FEATURES: &'static [ClapFeature] = &[
|
|
ClapFeature::AudioEffect,
|
|
ClapFeature::Utility,
|
|
ClapFeature::Stereo,
|
|
ClapFeature::Mono,
|
|
];
|
|
}
|
|
nih_export_clap!(Crucible);
|