236 lines
8.1 KiB
Rust
236 lines
8.1 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
|
use std::sync::Arc;
|
|
|
|
use crossbeam_channel::Receiver;
|
|
use doux::EngineMetrics;
|
|
|
|
use crate::app::App;
|
|
use crate::engine::{
|
|
build_stream, preload_sample_heads, spawn_sequencer, AnalysisHandle, AudioStreamConfig,
|
|
LinkState, MidiCommand, PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle,
|
|
SpectrumBuffer,
|
|
};
|
|
use crate::midi;
|
|
use crate::model;
|
|
use crate::settings::Settings;
|
|
use crate::state::audio::RefreshRate;
|
|
use crate::state::StagedChange;
|
|
use crate::theme;
|
|
|
|
pub struct InitArgs {
|
|
pub samples: Vec<PathBuf>,
|
|
pub output: Option<String>,
|
|
pub input: Option<String>,
|
|
pub channels: Option<u16>,
|
|
pub buffer: Option<u32>,
|
|
}
|
|
|
|
// Fields destructured in main.rs (cli) and plugin crate — all are used.
|
|
#[allow(dead_code)]
|
|
pub struct Init {
|
|
pub app: App,
|
|
pub link: Arc<LinkState>,
|
|
pub sequencer: SequencerHandle,
|
|
pub playing: Arc<AtomicBool>,
|
|
pub nudge_us: Arc<AtomicI64>,
|
|
pub metrics: Arc<EngineMetrics>,
|
|
pub scope_buffer: Arc<ScopeBuffer>,
|
|
pub spectrum_buffer: Arc<SpectrumBuffer>,
|
|
pub audio_sample_pos: Arc<AtomicU64>,
|
|
pub sample_rate_shared: Arc<AtomicU32>,
|
|
pub stream: Option<cpal::Stream>,
|
|
pub analysis_handle: Option<AnalysisHandle>,
|
|
pub midi_rx: Receiver<MidiCommand>,
|
|
#[cfg(feature = "desktop")]
|
|
pub settings: Settings,
|
|
#[cfg(feature = "desktop")]
|
|
pub mouse_x: Arc<AtomicU32>,
|
|
#[cfg(feature = "desktop")]
|
|
pub mouse_y: Arc<AtomicU32>,
|
|
#[cfg(feature = "desktop")]
|
|
pub mouse_down: Arc<AtomicU32>,
|
|
}
|
|
|
|
pub fn init(args: InitArgs) -> Init {
|
|
let settings = Settings::load();
|
|
|
|
let link = Arc::new(LinkState::new(settings.link.tempo, settings.link.quantum));
|
|
if settings.link.enabled {
|
|
link.enable();
|
|
}
|
|
|
|
let playing = Arc::new(AtomicBool::new(true));
|
|
let nudge_us = Arc::new(AtomicI64::new(0));
|
|
|
|
let mut app = App::new();
|
|
|
|
app.playback.queued_changes.push(StagedChange {
|
|
change: PatternChange::Start {
|
|
bank: 0,
|
|
pattern: 0,
|
|
},
|
|
quantization: model::LaunchQuantization::Immediate,
|
|
sync_mode: model::SyncMode::PhaseLock,
|
|
});
|
|
|
|
app.audio.config.output_device = args.output.or(settings.audio.output_device.clone());
|
|
app.audio.config.input_device = args.input.or(settings.audio.input_device.clone());
|
|
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
|
|
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
|
|
app.audio.config.max_voices = settings.audio.max_voices;
|
|
app.audio.config.sample_paths = args.samples;
|
|
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
|
|
app.ui.runtime_highlight = settings.display.runtime_highlight;
|
|
app.audio.config.show_scope = settings.display.show_scope;
|
|
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
|
app.audio.config.show_preview = settings.display.show_preview;
|
|
app.ui.show_completion = settings.display.show_completion;
|
|
app.ui.performance_mode = settings.display.performance_mode;
|
|
app.ui.color_scheme = settings.display.color_scheme;
|
|
app.ui.hue_rotation = settings.display.hue_rotation;
|
|
app.audio.config.layout = settings.display.layout;
|
|
app.ui.onboarding_dismissed = settings.display.onboarding_dismissed.clone();
|
|
app.ui.font = settings.display.font.clone();
|
|
app.ui.zoom_factor = settings.display.zoom_factor;
|
|
|
|
let palette = settings.display.color_scheme.to_palette();
|
|
let rotated =
|
|
cagire_ratatui::theme::transform::rotate_palette(&palette, settings.display.hue_rotation);
|
|
theme::set(rotated);
|
|
|
|
// MIDI connections
|
|
let outputs = midi::list_midi_outputs();
|
|
let inputs = midi::list_midi_inputs();
|
|
for (slot, name) in settings.midi.output_devices.iter().enumerate() {
|
|
if !name.is_empty() {
|
|
if let Some(idx) = outputs.iter().position(|d| &d.name == name) {
|
|
let _ = app.midi.connect_output(slot, idx);
|
|
}
|
|
}
|
|
}
|
|
for (slot, name) in settings.midi.input_devices.iter().enumerate() {
|
|
if !name.is_empty() {
|
|
if let Some(idx) = inputs.iter().position(|d| &d.name == name) {
|
|
let _ = app.midi.connect_input(slot, idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
let metrics = Arc::new(EngineMetrics::default());
|
|
let scope_buffer = Arc::new(ScopeBuffer::new());
|
|
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
|
|
|
|
let audio_sample_pos = Arc::new(AtomicU64::new(0));
|
|
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
|
|
let mut initial_samples = Vec::new();
|
|
for path in &app.audio.config.sample_paths {
|
|
let index = doux::sampling::scan_samples_dir(path);
|
|
app.audio.config.sample_count += index.len();
|
|
initial_samples.extend(index);
|
|
}
|
|
let preload_entries: Vec<(String, std::path::PathBuf)> = initial_samples
|
|
.iter()
|
|
.map(|e| (e.name.clone(), e.path.clone()))
|
|
.collect();
|
|
|
|
#[cfg(feature = "desktop")]
|
|
let mouse_x = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
|
|
#[cfg(feature = "desktop")]
|
|
let mouse_y = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
|
|
#[cfg(feature = "desktop")]
|
|
let mouse_down = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
|
|
|
|
let seq_config = SequencerConfig {
|
|
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
|
sample_rate: Arc::clone(&sample_rate_shared),
|
|
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn model::CcAccess>),
|
|
variables: Arc::clone(&app.variables),
|
|
dict: Arc::clone(&app.dict),
|
|
#[cfg(feature = "desktop")]
|
|
mouse_x: Arc::clone(&mouse_x),
|
|
#[cfg(feature = "desktop")]
|
|
mouse_y: Arc::clone(&mouse_y),
|
|
#[cfg(feature = "desktop")]
|
|
mouse_down: Arc::clone(&mouse_down),
|
|
};
|
|
|
|
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
|
|
Arc::clone(&link),
|
|
Arc::clone(&playing),
|
|
settings.link.quantum,
|
|
Arc::clone(&app.live_keys),
|
|
Arc::clone(&nudge_us),
|
|
seq_config,
|
|
);
|
|
|
|
let stream_config = AudioStreamConfig {
|
|
output_device: app.audio.config.output_device.clone(),
|
|
channels: app.audio.config.channels,
|
|
buffer_size: app.audio.config.buffer_size,
|
|
max_voices: app.audio.config.max_voices,
|
|
};
|
|
|
|
let (stream, analysis_handle) = match build_stream(
|
|
&stream_config,
|
|
initial_audio_rx,
|
|
Arc::clone(&scope_buffer),
|
|
Arc::clone(&spectrum_buffer),
|
|
Arc::clone(&metrics),
|
|
initial_samples,
|
|
Arc::clone(&audio_sample_pos),
|
|
) {
|
|
Ok((s, info, analysis, registry)) => {
|
|
app.audio.config.sample_rate = info.sample_rate;
|
|
app.audio.config.host_name = info.host_name;
|
|
app.audio.config.channels = info.channels;
|
|
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
|
app.audio.sample_registry = Some(Arc::clone(®istry));
|
|
|
|
if !preload_entries.is_empty() {
|
|
let sr = info.sample_rate;
|
|
std::thread::Builder::new()
|
|
.name("sample-preload".into())
|
|
.spawn(move || {
|
|
preload_sample_heads(preload_entries, sr, ®istry);
|
|
})
|
|
.expect("failed to spawn preload thread");
|
|
}
|
|
|
|
(Some(s), Some(analysis))
|
|
}
|
|
Err(e) => {
|
|
app.ui.set_status(format!("Audio failed: {e}"));
|
|
app.audio.error = Some(e);
|
|
(None, None)
|
|
}
|
|
};
|
|
|
|
app.mark_all_patterns_dirty();
|
|
|
|
Init {
|
|
app,
|
|
link,
|
|
sequencer,
|
|
playing,
|
|
nudge_us,
|
|
metrics,
|
|
scope_buffer,
|
|
spectrum_buffer,
|
|
audio_sample_pos,
|
|
sample_rate_shared,
|
|
stream,
|
|
analysis_handle,
|
|
midi_rx,
|
|
#[cfg(feature = "desktop")]
|
|
settings,
|
|
#[cfg(feature = "desktop")]
|
|
mouse_x,
|
|
#[cfg(feature = "desktop")]
|
|
mouse_y,
|
|
#[cfg(feature = "desktop")]
|
|
mouse_down,
|
|
}
|
|
}
|
|
|