Run as 'vois': install via make, config file, Auto-Tune, more presets, custom chains
- 'vois' installs to PATH (make install) and works with no flags: auto device selection (pipewire/pulse/default) and 48 kHz default - ~/.config/vois/config.toml for defaults + custom preset chains (serde) - Auto-Tune pitch correction (snap to nearest semitone, smoothed) - 5 new presets: Chipmunk, Helium, Vader, Telephone, Growl (17 total) - --pitch-correct flag, --no-virtual-mic to disable the default virtual mic - no dead-code allowances; clippy/fmt/tests clean
This commit is contained in:
parent
b0d685a110
commit
aacaf7d9cf
14 changed files with 753 additions and 285 deletions
103
src/config.rs
103
src/config.rs
|
|
@ -1,5 +1,65 @@
|
|||
use crate::dsp::chain::Control;
|
||||
use crate::dsp::presets::PRESETS;
|
||||
use crate::dsp::presets::{presets, CustomPresetCfg, Preset};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Settings loaded from `~/.config/vois/config.toml`.
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct FileConfig {
|
||||
pub input: Option<String>,
|
||||
pub output: Option<String>,
|
||||
pub sample_rate: Option<u32>,
|
||||
pub preset: Option<String>,
|
||||
pub record: Option<String>,
|
||||
pub pitch: Option<f32>,
|
||||
pub formant: Option<f32>,
|
||||
pub gate: Option<f32>,
|
||||
pub gain: Option<f32>,
|
||||
pub tone: Option<f32>,
|
||||
pub virtual_mic: Option<bool>,
|
||||
pub pitch_correct: Option<bool>,
|
||||
pub custom_presets: Option<HashMap<String, CustomPresetCfg>>,
|
||||
}
|
||||
|
||||
impl FileConfig {
|
||||
pub fn config_path() -> PathBuf {
|
||||
let base = std::env::var("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
std::env::var("HOME")
|
||||
.map(|h| PathBuf::from(h).join(".config"))
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
});
|
||||
base.join("vois").join("config.toml")
|
||||
}
|
||||
|
||||
/// Load the config file if it exists; any parse error is ignored (treated
|
||||
/// as an empty config so the app still starts).
|
||||
pub fn load() -> Self {
|
||||
match std::fs::read_to_string(Self::config_path()) {
|
||||
Ok(text) => toml::from_str(&text).unwrap_or_default(),
|
||||
Err(_) => Self::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert custom presets from the file into `Preset` objects.
|
||||
pub fn to_presets(&self) -> Vec<Preset> {
|
||||
self.custom_presets
|
||||
.as_ref()
|
||||
.map(|map| {
|
||||
map.iter()
|
||||
.map(|(name, c)| Preset {
|
||||
name: name.clone(),
|
||||
pitch_semitones: c.pitch.unwrap_or(0.0),
|
||||
formant_ratio: c.formant.unwrap_or(1.0),
|
||||
effects: c.effects.clone().unwrap_or_default(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppConfig {
|
||||
pub input: Option<String>,
|
||||
|
|
@ -14,29 +74,41 @@ pub struct AppConfig {
|
|||
pub run_seconds: Option<u32>,
|
||||
pub tone: f32,
|
||||
pub virtual_mic: bool,
|
||||
pub pitch_correct: bool,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Merge CLI options over the config file, over the app defaults.
|
||||
pub fn from_cli(cli: &crate::Cli) -> Self {
|
||||
let file = FileConfig::load();
|
||||
Self {
|
||||
input: cli.input.clone(),
|
||||
output: cli.output.clone(),
|
||||
sample_rate: cli.sample_rate,
|
||||
preset: cli.preset.clone(),
|
||||
record: cli.record.clone(),
|
||||
pitch: cli.pitch,
|
||||
formant: cli.formant,
|
||||
gate: cli.gate,
|
||||
gain: cli.gain,
|
||||
input: cli.input.clone().or(file.input),
|
||||
output: cli.output.clone().or(file.output),
|
||||
sample_rate: cli.sample_rate.or(file.sample_rate),
|
||||
preset: cli
|
||||
.preset
|
||||
.clone()
|
||||
.or(file.preset)
|
||||
.unwrap_or_else(|| "Clean".to_string()),
|
||||
record: cli.record.clone().or(file.record),
|
||||
pitch: cli.pitch.or(file.pitch).unwrap_or(0.0),
|
||||
formant: cli.formant.or(file.formant).unwrap_or(0.0),
|
||||
gate: cli.gate.or(file.gate).unwrap_or(-100.0),
|
||||
gain: cli.gain.or(file.gain).unwrap_or(1.0),
|
||||
run_seconds: cli.run_seconds,
|
||||
tone: cli.tone,
|
||||
virtual_mic: !cli.no_virtual_mic,
|
||||
tone: cli.tone.or(file.tone).unwrap_or(0.0),
|
||||
virtual_mic: if cli.no_virtual_mic {
|
||||
false
|
||||
} else {
|
||||
file.virtual_mic.unwrap_or(true)
|
||||
},
|
||||
pitch_correct: cli.pitch_correct.or(file.pitch_correct).unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the preset name into an index, defaulting to "Clean" on error.
|
||||
fn preset_idx(&self) -> usize {
|
||||
crate::dsp::presets::Preset::find(&self.preset).unwrap_or(0)
|
||||
Preset::find(&self.preset).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn to_control(&self) -> Control {
|
||||
|
|
@ -50,14 +122,15 @@ impl AppConfig {
|
|||
record_path: self.record.clone(),
|
||||
run_seconds: self.run_seconds,
|
||||
tone_hz: self.tone,
|
||||
pitch_correct: self.pitch_correct,
|
||||
..Control::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preset_names() -> String {
|
||||
PRESETS
|
||||
presets()
|
||||
.iter()
|
||||
.map(|p| p.name)
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue