pub mod effects;
pub mod enhancement;
pub mod format;
pub mod manager;
pub mod registry;
use crate::{audio::AudioBuffer, error::Result, VoirsError};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::{
any::Any,
collections::HashMap,
path::PathBuf,
sync::{Arc, RwLock},
};
pub trait VoirsPlugin: Send + Sync + Any {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn description(&self) -> &str;
fn author(&self) -> &str;
fn initialize(&self, config: &PluginConfig) -> Result<()> {
let _ = config; Ok(()) }
fn shutdown(&self) -> Result<()> {
Ok(()) }
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
name: self.name().to_string(),
version: self.version().to_string(),
description: self.description().to_string(),
author: self.author().to_string(),
plugin_type: PluginType::Effect,
supported_formats: vec!["wav".to_string()],
capabilities: vec![],
dependencies: vec![],
supported_platforms: Some(vec!["any".to_string()]),
min_voirs_version: Some("0.1.0".to_string()),
}
}
fn supports_capability(&self, capability: &str) -> bool {
self.metadata()
.capabilities
.contains(&capability.to_string())
}
fn as_any(&self) -> &dyn Any;
}
#[async_trait]
pub trait AudioEffect: VoirsPlugin {
async fn process_audio(&self, audio: &AudioBuffer) -> Result<AudioBuffer>;
fn get_parameters(&self) -> HashMap<String, ParameterValue>;
fn set_parameter(&self, name: &str, value: ParameterValue) -> Result<()>;
fn get_parameter_definition(&self, name: &str) -> Option<ParameterDefinition>;
fn list_parameters(&self) -> Vec<String> {
self.get_parameters().keys().cloned().collect()
}
fn reset_parameters(&self) -> Result<()> {
Ok(())
}
fn get_latency_samples(&self) -> usize {
0 }
fn is_realtime_capable(&self) -> bool {
true }
}
#[async_trait]
pub trait VoiceEffect: VoirsPlugin {
async fn process_voice_synthesis(
&self,
phonemes: &[crate::types::Phoneme],
mel: &crate::types::MelSpectrogram,
audio: &AudioBuffer,
) -> Result<VoiceSynthesisResult>;
fn get_effect_type(&self) -> VoiceEffectType;
fn modifies_stage(&self, stage: SynthesisStage) -> bool;
}
#[async_trait]
pub trait TextProcessor: VoirsPlugin {
async fn process_text(
&self,
text: &str,
language: crate::types::LanguageCode,
) -> Result<String>;
fn get_processor_type(&self) -> TextProcessorType;
fn supports_language(&self, language: crate::types::LanguageCode) -> bool;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfig {
pub parameters: HashMap<String, ParameterValue>,
pub enabled: bool,
pub priority: i32,
pub config_file: Option<PathBuf>,
pub data_dir: Option<PathBuf>,
pub cache_dir: Option<PathBuf>,
}
impl Default for PluginConfig {
fn default() -> Self {
Self {
parameters: HashMap::new(),
enabled: true,
priority: 100,
config_file: None,
data_dir: None,
cache_dir: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginMetadata {
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub plugin_type: PluginType,
pub supported_formats: Vec<String>,
pub capabilities: Vec<String>,
pub dependencies: Vec<String>,
pub supported_platforms: Option<Vec<String>>,
pub min_voirs_version: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginType {
Effect,
VoiceEffect,
TextProcessor,
ModelEnhancer,
OutputProcessor,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ParameterValue {
Float(f32),
Int(i32),
Integer(i64), Bool(bool),
String(String),
FloatArray(Vec<f32>),
IntArray(Vec<i32>),
}
impl ParameterValue {
pub fn as_f32(&self) -> Option<f32> {
match self {
Self::Float(v) => Some(*v),
Self::Int(v) => Some(*v as f32),
Self::Integer(v) => Some(*v as f32),
_ => None,
}
}
pub fn as_i32(&self) -> Option<i32> {
match self {
Self::Int(v) => Some(*v),
Self::Integer(v) => Some(*v as i32),
Self::Float(v) => Some(*v as i32),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Self::Integer(v) => Some(*v),
Self::Int(v) => Some(*v as i64),
Self::Float(v) => Some(*v as i64),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(v) => Some(*v),
Self::Int(v) => Some(*v != 0),
Self::Integer(v) => Some(*v != 0),
Self::Float(v) => Some(*v != 0.0),
_ => None,
}
}
pub fn as_string(&self) -> String {
match self {
Self::String(v) => v.clone(),
Self::Float(v) => v.to_string(),
Self::Int(v) => v.to_string(),
Self::Integer(v) => v.to_string(),
Self::Bool(v) => v.to_string(),
Self::FloatArray(v) => format!("{v:?}"),
Self::IntArray(v) => format!("{v:?}"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterDefinition {
pub name: String,
pub description: String,
pub parameter_type: ParameterType,
pub default_value: ParameterValue,
pub min_value: Option<ParameterValue>,
pub max_value: Option<ParameterValue>,
pub step_size: Option<f32>,
pub realtime_safe: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ParameterType {
Float,
Int,
Integer, Bool,
String,
FloatArray,
IntArray,
Enum,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VoiceEffectType {
PhonemeModifier,
MelModifier,
AudioModifier,
MultiStage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SynthesisStage {
TextPreprocessing,
PhonemeGeneration,
MelSynthesis,
AudioGeneration,
PostProcessing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TextProcessorType {
Normalizer,
Expander,
NumberConverter,
SsmlProcessor,
EmotionAnalyzer,
}
#[derive(Debug, Clone)]
pub struct VoiceSynthesisResult {
pub phonemes: Option<Vec<crate::types::Phoneme>>,
pub mel_spectrogram: Option<crate::types::MelSpectrogram>,
pub audio_buffer: Option<AudioBuffer>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, thiserror::Error)]
pub enum PluginError {
#[error("Plugin not found: {name}")]
NotFound { name: String },
#[error("Plugin initialization failed: {message}")]
InitializationFailed { message: String },
#[error("Plugin parameter error: {message}")]
ParameterError { message: String },
#[error("Plugin version incompatible: required {required}, found {found}")]
VersionIncompatible { required: String, found: String },
#[error("Plugin dependency missing: {dependency}")]
DependencyMissing { dependency: String },
#[error("Plugin processing error: {message}")]
ProcessingError { message: String },
}
pub struct PluginHost {
plugins: Arc<RwLock<HashMap<String, Arc<dyn VoirsPlugin>>>>,
configs: Arc<RwLock<HashMap<String, PluginConfig>>>,
load_order: Vec<String>,
#[allow(dead_code)]
plugin_dir: PathBuf,
}
impl PluginHost {
pub fn new(plugin_dir: impl Into<PathBuf>) -> Self {
Self {
plugins: Arc::new(RwLock::new(HashMap::new())),
configs: Arc::new(RwLock::new(HashMap::new())),
load_order: Vec::new(),
plugin_dir: plugin_dir.into(),
}
}
pub fn load_plugin(&mut self, name: &str, plugin: Arc<dyn VoirsPlugin>) -> Result<()> {
let default_config = PluginConfig::default();
plugin.initialize(&default_config).map_err(|e| {
VoirsError::internal("plugins", format!("Plugin initialization failed: {e}"))
})?;
{
let mut plugins = self
.plugins
.write()
.map_err(|e| VoirsError::internal("plugins", format!("Lock poisoned: {e}")))?;
let mut configs = self
.configs
.write()
.map_err(|e| VoirsError::internal("plugins", format!("Lock poisoned: {e}")))?;
plugins.insert(name.to_string(), plugin);
configs.insert(name.to_string(), default_config);
}
self.load_order.push(name.to_string());
tracing::info!("Loaded plugin: {}", name);
Ok(())
}
pub fn unload_plugin(&mut self, name: &str) -> Result<()> {
{
let mut plugins = self
.plugins
.write()
.map_err(|e| VoirsError::internal("plugins", format!("Lock poisoned: {e}")))?;
let mut configs = self
.configs
.write()
.map_err(|e| VoirsError::internal("plugins", format!("Lock poisoned: {e}")))?;
if let Some(plugin) = plugins.remove(name) {
plugin.shutdown().map_err(|e| {
VoirsError::internal("plugins", format!("Plugin shutdown failed: {e}"))
})?;
}
configs.remove(name);
}
self.load_order.retain(|n| n != name);
tracing::info!("Unloaded plugin: {}", name);
Ok(())
}
pub fn get_plugin(&self, name: &str) -> Option<Arc<dyn VoirsPlugin>> {
let plugins = self.plugins.read().ok()?;
plugins.get(name).cloned()
}
pub fn list_plugins(&self) -> Vec<String> {
self.plugins
.read()
.map(|plugins| plugins.keys().cloned().collect())
.unwrap_or_default()
}
pub fn get_plugin_metadata(&self, name: &str) -> Option<PluginMetadata> {
self.get_plugin(name).map(|p| p.metadata())
}
pub fn configure_plugin(&self, name: &str, config: PluginConfig) -> Result<()> {
let mut configs = self
.configs
.write()
.map_err(|e| VoirsError::internal("plugins", format!("Lock poisoned: {e}")))?;
configs.insert(name.to_string(), config);
Ok(())
}
pub fn get_plugin_config(&self, name: &str) -> Option<PluginConfig> {
let configs = self.configs.read().ok()?;
configs.get(name).cloned()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestAudioEffect {
name: String,
gain: std::sync::RwLock<f32>,
}
impl TestAudioEffect {
fn new() -> Self {
Self {
name: "TestEffect".to_string(),
gain: std::sync::RwLock::new(1.0),
}
}
}
impl VoirsPlugin for TestAudioEffect {
fn name(&self) -> &str {
&self.name
}
fn version(&self) -> &str {
"1.0.0"
}
fn description(&self) -> &str {
"Test audio effect plugin"
}
fn author(&self) -> &str {
"VoiRS Team"
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[async_trait]
impl AudioEffect for TestAudioEffect {
async fn process_audio(&self, audio: &AudioBuffer) -> Result<AudioBuffer> {
let mut processed = audio.clone();
let gain = *self.gain.read().expect("lock should not be poisoned");
for sample in processed.samples_mut() {
*sample *= gain;
}
Ok(processed)
}
fn get_parameters(&self) -> HashMap<String, ParameterValue> {
let mut params = HashMap::new();
params.insert(
"gain".to_string(),
ParameterValue::Float(*self.gain.read().expect("lock should not be poisoned")),
);
params
}
fn set_parameter(&self, name: &str, value: ParameterValue) -> Result<()> {
match name {
"gain" => {
if let Some(gain) = value.as_f32() {
*self.gain.write().expect("lock should not be poisoned") = gain;
Ok(())
} else {
Err(VoirsError::internal(
"plugins",
"Invalid gain parameter type",
))
}
}
_ => Err(VoirsError::internal(
"plugins",
format!("Unknown parameter: {name}"),
)),
}
}
fn get_parameter_definition(&self, name: &str) -> Option<ParameterDefinition> {
match name {
"gain" => Some(ParameterDefinition {
name: "gain".to_string(),
description: "Audio gain multiplier".to_string(),
parameter_type: ParameterType::Float,
default_value: ParameterValue::Float(1.0),
min_value: Some(ParameterValue::Float(0.0)),
max_value: Some(ParameterValue::Float(10.0)),
step_size: Some(0.1),
realtime_safe: true,
}),
_ => None,
}
}
}
#[test]
fn test_parameter_value_conversions() {
let float_val = ParameterValue::Float(3.5);
assert_eq!(float_val.as_f32(), Some(3.5));
assert_eq!(float_val.as_i32(), Some(3));
let int_val = ParameterValue::Int(42);
assert_eq!(int_val.as_i32(), Some(42));
assert_eq!(int_val.as_f32(), Some(42.0));
let bool_val = ParameterValue::Bool(true);
assert_eq!(bool_val.as_bool(), Some(true));
let string_val = ParameterValue::String("test".to_string());
assert_eq!(string_val.as_string(), "test");
}
#[test]
fn test_plugin_host() {
let mut host = PluginHost::new("/tmp/plugins");
let plugin = Arc::new(TestAudioEffect::new());
host.load_plugin("test_effect", plugin).unwrap();
assert!(host.get_plugin("test_effect").is_some());
assert_eq!(host.list_plugins().len(), 1);
let metadata = host.get_plugin_metadata("test_effect").unwrap();
assert_eq!(metadata.name, "TestEffect");
assert_eq!(metadata.version, "1.0.0");
host.unload_plugin("test_effect").unwrap();
assert!(host.get_plugin("test_effect").is_none());
assert_eq!(host.list_plugins().len(), 0);
}
#[tokio::test]
async fn test_audio_effect_processing() {
let effect = TestAudioEffect::new();
effect
.set_parameter("gain", ParameterValue::Float(2.0))
.unwrap();
let audio = crate::AudioBuffer::sine_wave(440.0, 1.0, 44100, 0.5);
let processed = effect.process_audio(&audio).await.unwrap();
assert!(processed.samples()[100].abs() > audio.samples()[100].abs());
}
#[test]
fn test_plugin_metadata() {
let effect = TestAudioEffect::new();
let metadata = effect.metadata();
assert_eq!(metadata.name, "TestEffect");
assert_eq!(metadata.version, "1.0.0");
assert_eq!(metadata.author, "VoiRS Team");
assert_eq!(metadata.plugin_type, PluginType::Effect);
}
}