use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use std::sync::Arc;
use vibelang_core::{GroupId, ParamMap, VoiceConfig, VoiceId, VoiceMessage};
use crate::{
models::{
ErrorResponse, NoteOffRequest, NoteOnRequest, ParamSet, TriggerRequest, Voice, VoiceCreate,
VoiceUpdate,
},
AppState,
};
async fn resolve_voice_id(
state: &Arc<AppState>,
identifier: &str,
) -> Result<VoiceId, (StatusCode, Json<ErrorResponse>)> {
if let Ok(num_id) = identifier.parse::<u32>() {
let voice_id = VoiceId::new(num_id);
let exists = state.with_state(|s| s.voices.contains_key(&voice_id)).await;
if exists {
return Ok(voice_id);
}
}
let found = state
.with_state(|s| {
s.voices
.iter()
.find(|(_, vs)| vs.config.name == identifier)
.map(|(id, _)| *id)
})
.await;
match found {
Some(id) => Ok(id),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse::not_found(&format!(
"Voice '{}' not found",
identifier
))),
)),
}
}
fn voice_to_api(_id: &VoiceId, state: &vibelang_core::VoiceState) -> Voice {
let name = state.config.name.clone();
let group_path = state.config.group.raw().to_string();
Voice {
name,
synth_name: state.config.synthdef.clone(),
polyphony: state.config.polyphony,
gain: state.config.params.get("amp").copied().unwrap_or(1.0),
group_path: group_path.clone(),
group_name: Some(group_path),
output_bus: None, muted: state.config.muted,
soloed: false, params: state
.config
.params
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect(),
sfz_instrument: state.config.sfz_instrument.map(|s| s.raw().to_string()),
vst_instrument: None, active_notes: Some(state.note_nodes.keys().copied().collect()),
sustained_notes: None, running: !state.active_nodes.is_empty(),
running_node_id: state.active_nodes.first().map(|n| n.raw() as i32),
source_location: None, }
}
pub async fn list_voices(State(state): State<Arc<AppState>>) -> Json<Vec<Voice>> {
let voices = state
.with_state(|s| {
s.voices
.iter()
.map(|(id, vs)| voice_to_api(id, vs))
.collect::<Vec<_>>()
})
.await;
Json(voices)
}
pub async fn get_voice(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Voice>, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
let voice = state
.with_state(|s| {
s.voices
.get(&voice_id)
.map(|vs| voice_to_api(&voice_id, vs))
})
.await;
match voice {
Some(v) => Ok(Json(v)),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse::not_found(&format!(
"Voice '{}' not found",
id
))),
)),
}
}
pub async fn update_voice(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(update): Json<VoiceUpdate>,
) -> Result<Json<Voice>, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
for (param, value) in update.params {
if let Err(e) = state
.send(
VoiceMessage::SetParam {
id: voice_id,
param,
value,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to update voice params: {}",
e
))),
));
}
}
get_voice(State(state), Path(id)).await
}
pub async fn trigger_voice(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(req): Json<Option<TriggerRequest>>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
let mut params = ParamMap::new();
if let Some(r) = req {
for (k, v) in r.params {
params.insert(k, v);
}
}
if let Err(e) = state
.send(
VoiceMessage::Trigger {
id: voice_id,
params,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to trigger voice: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn stop_voice(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
if let Err(e) = state.send(VoiceMessage::Stop { id: voice_id }.into()).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to stop voice: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn note_on(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(req): Json<NoteOnRequest>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
if let Err(e) = state
.send(
VoiceMessage::NoteOn {
voice: voice_id,
note: req.note,
velocity: req.velocity,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to send note-on: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn note_off(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(req): Json<NoteOffRequest>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
if let Err(e) = state
.send(
VoiceMessage::NoteOff {
voice: voice_id,
note: req.note,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to send note-off: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn set_voice_param(
State(state): State<Arc<AppState>>,
Path((id, param)): Path<(String, String)>,
Json(req): Json<ParamSet>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
if let Err(e) = state
.send(
VoiceMessage::SetParam {
id: voice_id,
param,
value: req.value,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to set voice param: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn mute_voice(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
if let Err(e) = state
.send(
VoiceMessage::Mute {
id: voice_id,
muted: true,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to mute voice: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn unmute_voice(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
if let Err(e) = state
.send(
VoiceMessage::Mute {
id: voice_id,
muted: false,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to unmute voice: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn create_voice(
State(state): State<Arc<AppState>>,
Json(req): Json<VoiceCreate>,
) -> Result<(StatusCode, Json<Voice>), (StatusCode, Json<ErrorResponse>)> {
let voice_id = state
.with_state(|s| {
let max_id = s.voices.keys().map(|id| id.raw()).max().unwrap_or(0);
VoiceId::new(max_id + 1)
})
.await;
let group_id = if let Some(gid) = &req.group_path {
gid.parse::<u32>().map(GroupId::new).map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request(&format!(
"Invalid group path '{}': must be a number",
gid
))),
)
})?
} else {
GroupId::new(0)
};
let voice_name = req
.name
.clone()
.unwrap_or_else(|| voice_id.raw().to_string());
let synth_name = req
.synth_name
.clone()
.unwrap_or_else(|| "default".to_string());
let mut config = VoiceConfig::new(&voice_name, &synth_name, group_id);
if let Some(polyphony) = req.polyphony {
config = config.with_polyphony(polyphony);
}
for (param, value) in req.params {
config = config.with_param(param, value);
}
if let Err(e) = state
.send(
VoiceMessage::Create {
id: voice_id,
config,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to create voice: {}",
e
))),
));
}
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let voice = state
.with_state(|s| {
s.voices
.get(&voice_id)
.map(|vs| voice_to_api(&voice_id, vs))
})
.await;
match voice {
Some(v) => Ok((StatusCode::CREATED, Json(v))),
None => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(
"Voice created but not found in state",
)),
)),
}
}
pub async fn delete_voice(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let voice_id = resolve_voice_id(&state, &id).await?;
if let Err(e) = state
.send(VoiceMessage::Delete { id: voice_id }.into())
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to delete voice: {}",
e
))),
));
}
Ok(StatusCode::NO_CONTENT)
}