use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use serde::Deserialize;
use std::sync::Arc;
use vibelang_core::{
traits::{FadeConfig, FadeCurve, FadeTarget},
types::Duration,
EffectId, FadeMessage, GroupId, VoiceId,
};
use crate::{
models::{ErrorResponse, FadeCreate, FadeTargetType},
AppState,
};
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum CurveSpec {
Name(String),
Exponential { exp: f64 },
Spline { spline: Vec<[f64; 2]> },
}
impl Default for CurveSpec {
fn default() -> Self {
CurveSpec::Name("linear".to_string())
}
}
#[derive(Debug, Deserialize)]
pub struct VoiceFadeRequest {
pub param: String,
pub to: f32,
pub duration_beats: f64,
#[serde(default)]
pub from: Option<f32>,
#[serde(default)]
pub curve: CurveSpec,
}
#[derive(Debug, Deserialize)]
pub struct GroupFadeRequest {
pub param: String,
pub to: f32,
pub duration_beats: f64,
#[serde(default)]
pub from: Option<f32>,
#[serde(default)]
pub curve: CurveSpec,
}
#[derive(Debug, Deserialize)]
pub struct EffectFadeRequest {
pub param: String,
pub to: f32,
pub duration_beats: f64,
#[serde(default)]
pub from: Option<f32>,
#[serde(default)]
pub curve: CurveSpec,
}
fn parse_curve_name(curve: &str) -> FadeCurve {
match curve.to_lowercase().as_str() {
"ease_in" | "easein" | "ease-in" => FadeCurve::EaseIn,
"ease_out" | "easeout" | "ease-out" => FadeCurve::EaseOut,
"ease_in_out" | "easeinout" | "ease-in-out" | "ease" => FadeCurve::EaseInOut,
"sine_in" | "sinein" | "sine-in" => FadeCurve::SineIn,
"sine_out" | "sineout" | "sine-out" => FadeCurve::SineOut,
"sine" | "sine_in_out" | "sineinout" | "sin" | "smooth" => FadeCurve::SineInOut,
"cubic_in" | "cubicin" | "cubic-in" => FadeCurve::CubicIn,
"cubic_out" | "cubicout" | "cubic-out" => FadeCurve::CubicOut,
"cubic_in_out" | "cubicinout" | "cubic-in-out" | "cubic" => FadeCurve::CubicInOut,
"exponential" | "exp" => FadeCurve::Exponential { exponent: 2.0 },
"log" | "logarithmic" => FadeCurve::Logarithmic,
"step" | "instant" => FadeCurve::Step,
_ => FadeCurve::Linear,
}
}
fn parse_curve_spec(spec: &CurveSpec) -> FadeCurve {
match spec {
CurveSpec::Name(name) => parse_curve_name(name),
CurveSpec::Exponential { exp } => FadeCurve::Exponential {
exponent: *exp as f32,
},
CurveSpec::Spline { spline } => FadeCurve::CubicSpline {
points: spline.iter().map(|[t, v]| (*t as f32, *v as f32)).collect(),
},
}
}
pub async fn fade_voice(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
Json(req): Json<VoiceFadeRequest>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = state
.with_state(|s| {
s.voices
.iter()
.find(|(_, v)| v.id.raw().to_string() == name || v.config.name == name)
.map(|(id, _)| *id)
})
.await;
let voice_id = match voice_id {
Some(id) => id,
None => {
match name.parse::<u32>() {
Ok(n) => VoiceId::new(n),
Err(_) => {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse::not_found(&format!(
"Voice '{}' not found",
name
))),
));
}
}
}
};
let mut config = FadeConfig::new(
FadeTarget::Voice(voice_id),
&req.param,
req.to,
Duration::from_beats(req.duration_beats),
);
config.from = req.from;
config.curve = parse_curve_spec(&req.curve);
state
.send(FadeMessage::Start { config }.into())
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&e.to_string())),
)
})?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn fade_group(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
Json(req): Json<GroupFadeRequest>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let group_id = state
.with_state(|s| {
s.groups
.iter()
.find(|(_, g)| g.id.raw().to_string() == path || format!("{}", g.id) == path)
.map(|(id, _)| *id)
})
.await;
let group_id = match group_id {
Some(id) => id,
None => {
match path.parse::<u32>() {
Ok(n) => GroupId::new(n),
Err(_) => {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse::not_found(&format!(
"Group '{}' not found",
path
))),
));
}
}
}
};
let mut config = FadeConfig::new(
FadeTarget::Group(group_id),
&req.param,
req.to,
Duration::from_beats(req.duration_beats),
);
config.from = req.from;
config.curve = parse_curve_spec(&req.curve);
state
.send(FadeMessage::Start { config }.into())
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&e.to_string())),
)
})?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn fade_effect(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(req): Json<EffectFadeRequest>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let effect_id = id.parse::<u32>().map(EffectId::new).map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request(&format!(
"Invalid effect ID '{}': must be a number",
id
))),
)
})?;
let mut config = FadeConfig::new(
FadeTarget::Effect(effect_id),
&req.param,
req.to,
Duration::from_beats(req.duration_beats),
);
config.from = req.from;
config.curve = parse_curve_spec(&req.curve);
state
.send(FadeMessage::Start { config }.into())
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&e.to_string())),
)
})?;
Ok(StatusCode::NO_CONTENT)
}
#[derive(Debug, Deserialize)]
pub struct CancelFadeRequest {
pub target_type: FadeTargetType,
pub target_name: String,
pub param: String,
}
pub async fn cancel_fade(
State(state): State<Arc<AppState>>,
Json(req): Json<CancelFadeRequest>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let target = match req.target_type {
FadeTargetType::Group => {
let id = req
.target_name
.parse::<u32>()
.map(GroupId::new)
.map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request("Invalid group ID")),
)
})?;
FadeTarget::Group(id)
}
FadeTargetType::Voice => {
let id = req
.target_name
.parse::<u32>()
.map(VoiceId::new)
.map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request("Invalid voice ID")),
)
})?;
FadeTarget::Voice(id)
}
FadeTargetType::Effect => {
let id = req
.target_name
.parse::<u32>()
.map(EffectId::new)
.map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request("Invalid effect ID")),
)
})?;
FadeTarget::Effect(id)
}
};
state
.send(
FadeMessage::Cancel {
target,
param: req.param,
}
.into(),
)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&e.to_string())),
)
})?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn start_fade(
State(state): State<Arc<AppState>>,
Json(req): Json<FadeCreate>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let target = match req.target_type {
FadeTargetType::Group => {
let id = req
.target_name
.parse::<u32>()
.map(GroupId::new)
.map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request("Invalid group ID")),
)
})?;
FadeTarget::Group(id)
}
FadeTargetType::Voice => {
let id = req
.target_name
.parse::<u32>()
.map(VoiceId::new)
.map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request("Invalid voice ID")),
)
})?;
FadeTarget::Voice(id)
}
FadeTargetType::Effect => {
let id = req
.target_name
.parse::<u32>()
.map(EffectId::new)
.map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request("Invalid effect ID")),
)
})?;
FadeTarget::Effect(id)
}
};
let mut config = FadeConfig::new(
target,
&req.param_name,
req.target_value,
Duration::from_beats(req.duration_beats),
);
config.from = req.start_value;
state
.send(FadeMessage::Start { config }.into())
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&e.to_string())),
)
})?;
Ok(StatusCode::CREATED)
}