use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use std::sync::Arc;
use vibelang_core::{
traits::{MelodyConfig, NoteEvent},
types::Beat,
MelodyId, MelodyMessage, VoiceId,
};
use crate::{
models::{
ErrorResponse, LoopState, LoopStatus, Melody, MelodyCreate, MelodyEvent, MelodyUpdate,
StartRequest, StopRequest,
},
AppState,
};
async fn resolve_melody_id(
state: &Arc<AppState>,
identifier: &str,
) -> Result<MelodyId, (StatusCode, Json<ErrorResponse>)> {
if let Ok(num_id) = identifier.parse::<u32>() {
let melody_id = MelodyId::new(num_id);
let exists = state
.with_state(|s| s.melodies.contains_key(&melody_id))
.await;
if exists {
return Ok(melody_id);
}
}
let found = state
.with_state(|s| {
s.melodies
.iter()
.find(|(_, ms)| ms.config.name == identifier)
.map(|(id, _)| *id)
})
.await;
match found {
Some(id) => Ok(id),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse::not_found(&format!(
"Melody '{}' not found",
identifier
))),
)),
}
}
fn melody_to_api(
_id: &MelodyId,
state: &vibelang_core::MelodyState,
voices: &std::collections::HashMap<vibelang_core::VoiceId, vibelang_core::VoiceState>,
) -> Melody {
let name = state.config.name.clone();
let voice_name = state
.config
.voice
.and_then(|vid| voices.get(&vid))
.map(|vs| vs.config.name.clone())
.unwrap_or_default();
let group_path = state
.config
.voice
.and_then(|vid| voices.get(&vid))
.map(|vs| vs.config.group.raw().to_string())
.unwrap_or_else(|| "0".to_string());
let status = LoopStatus {
state: if state.playing {
LoopState::Playing
} else {
LoopState::Stopped
},
start_beat: None,
stop_beat: None,
};
Melody {
name,
voice_name,
group_path,
loop_beats: state.config.length.to_f64(),
events: state
.config
.notes
.iter()
.map(|n| MelodyEvent {
beat: n.beat.to_f64(),
note: format!("{}", n.note), frequency: None, duration: Some(n.duration.to_f64()),
velocity: Some(n.velocity),
params: None,
})
.collect(),
params: None,
status,
is_looping: true, source_location: None,
notes_patterns: None,
}
}
pub async fn list_melodies(State(state): State<Arc<AppState>>) -> Json<Vec<Melody>> {
let melodies = state
.with_state(|s| {
s.melodies
.iter()
.map(|(id, ms)| melody_to_api(id, ms, &s.voices))
.collect::<Vec<_>>()
})
.await;
Json(melodies)
}
pub async fn get_melody(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Melody>, (StatusCode, Json<ErrorResponse>)> {
let melody_id = resolve_melody_id(&state, &id).await?;
let melody = state
.with_state(|s| {
s.melodies
.get(&melody_id)
.map(|ms| melody_to_api(&melody_id, ms, &s.voices))
})
.await;
match melody {
Some(m) => Ok(Json(m)),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse::not_found(&format!(
"Melody '{}' not found",
id
))),
)),
}
}
pub async fn update_melody(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(update): Json<MelodyUpdate>,
) -> Result<Json<Melody>, (StatusCode, Json<ErrorResponse>)> {
let _melody_id = resolve_melody_id(&state, &id).await?;
if update.events.is_some() || update.loop_beats.is_some() {
tracing::warn!(
"Melody {} update requested events or loop_beats change, which requires script reload",
id
);
}
get_melody(State(state), Path(id)).await
}
pub async fn start_melody(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(_req): Json<Option<StartRequest>>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let melody_id = resolve_melody_id(&state, &id).await?;
if let Err(e) = state
.send(MelodyMessage::Start { id: melody_id }.into())
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to start melody: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
pub async fn stop_melody(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Json(_req): Json<Option<StopRequest>>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let melody_id = resolve_melody_id(&state, &id).await?;
if let Err(e) = state
.send(MelodyMessage::Stop { id: melody_id }.into())
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to stop melody: {}",
e
))),
));
}
Ok(StatusCode::OK)
}
fn parse_note(note: &str) -> u8 {
if let Ok(n) = note.parse::<u8>() {
return n;
}
let note_upper = note.to_uppercase();
let mut chars = note_upper.chars().peekable();
let base = match chars.next() {
Some('C') => 0,
Some('D') => 2,
Some('E') => 4,
Some('F') => 5,
Some('G') => 7,
Some('A') => 9,
Some('B') => 11,
_ => return 60, };
let mut offset = 0i8;
while let Some(&c) = chars.peek() {
match c {
'#' => {
offset += 1;
chars.next();
}
'B' if chars.clone().count() > 1 => {
offset -= 1;
chars.next();
} _ => break,
}
}
let octave: i8 = chars.collect::<String>().parse().unwrap_or(4);
((octave + 1) * 12 + base as i8 + offset).clamp(0, 127) as u8
}
pub async fn create_melody(
State(state): State<Arc<AppState>>,
Json(req): Json<MelodyCreate>,
) -> Result<(StatusCode, Json<Melody>), (StatusCode, Json<ErrorResponse>)> {
let voice_id = state
.with_state(|s| {
s.voices
.iter()
.find(|(_, v)| v.config.name == req.voice_name)
.map(|(id, _)| *id)
})
.await;
let voice_id = match voice_id {
Some(id) => id,
None => {
match req.voice_name.parse::<u32>() {
Ok(n) => VoiceId::new(n),
Err(_) => {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse::bad_request(&format!(
"Voice '{}' not found",
req.voice_name
))),
));
}
}
}
};
let melody_id = state
.with_state(|s| {
let id = req
.name
.bytes()
.fold(1u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
let mut id = id % 10000 + 1;
while s.melodies.contains_key(&MelodyId::new(id)) {
id += 1;
}
MelodyId::new(id)
})
.await;
let notes: Vec<NoteEvent> = req
.events
.iter()
.map(|e| {
NoteEvent::new(
e.beat,
parse_note(&e.note),
e.velocity.unwrap_or(0.8),
e.duration.unwrap_or(1.0),
)
})
.collect();
let config = MelodyConfig {
name: req.name.clone(),
voice: Some(voice_id),
notes,
length: Beat::from_f64(req.loop_beats),
swing: 0.0,
};
if let Err(e) = state
.send(
MelodyMessage::Create {
id: melody_id,
config,
}
.into(),
)
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to create melody: {}",
e
))),
));
}
let melody = state
.with_state(|s| {
s.melodies
.get(&melody_id)
.map(|ms| melody_to_api(&melody_id, ms, &s.voices))
})
.await;
match melody {
Some(m) => Ok((StatusCode::CREATED, Json(m))),
None => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(
"Melody created but not found in state",
)),
)),
}
}
pub async fn delete_melody(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let melody_id = resolve_melody_id(&state, &id).await?;
if let Err(e) = state
.send(MelodyMessage::Delete { id: melody_id }.into())
.await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal(&format!(
"Failed to delete melody: {}",
e
))),
));
}
Ok(StatusCode::NO_CONTENT)
}