mod models;
mod routes;
mod websocket;
use axum::{
routing::{delete, get, patch, post, put},
Router,
};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use tower_http::cors::{Any, CorsLayer};
use vibelang_core::{Message, RuntimeHandle, State};
pub use models::*;
pub use routes::eval::{EvalJob, EvalResult};
pub use websocket::WebSocketEvent;
pub type EvalSender = std::sync::mpsc::Sender<EvalJob>;
pub struct AppState {
pub handle: RuntimeHandle,
pub state: Arc<RwLock<State>>,
pub ws_tx: broadcast::Sender<WebSocketEvent>,
pub eval_tx: Option<EvalSender>,
}
impl AppState {
pub async fn with_state<F, R>(&self, f: F) -> R
where
F: FnOnce(&State) -> R,
{
let guard = self.state.read().await;
f(&guard)
}
pub async fn send(&self, msg: Message) -> Result<(), vibelang_core::Error> {
self.handle.send(msg).await
}
}
pub async fn start_server(
handle: RuntimeHandle,
state: Arc<RwLock<State>>,
port: u16,
eval_tx: Option<EvalSender>,
) {
let (ws_tx, _) = broadcast::channel::<WebSocketEvent>(1024);
let app_state = Arc::new(AppState {
handle: handle.clone(),
state: state.clone(),
ws_tx: ws_tx.clone(),
eval_tx,
});
let broadcast_state = state.clone();
let broadcast_tx = ws_tx.clone();
tokio::spawn(async move {
websocket::run_event_broadcaster(broadcast_state, broadcast_tx).await;
});
let app = Router::new()
.route("/transport", get(routes::transport::get_transport))
.route("/transport", patch(routes::transport::update_transport))
.route("/transport/start", post(routes::transport::start_transport))
.route("/transport/stop", post(routes::transport::stop_transport))
.route("/transport/seek", post(routes::transport::seek_transport))
.route("/groups", get(routes::groups::list_groups))
.route("/groups/{id}", get(routes::groups::get_group))
.route("/groups/{id}", patch(routes::groups::update_group))
.route("/groups/{id}/mute", post(routes::groups::mute_group))
.route("/groups/{id}/unmute", post(routes::groups::unmute_group))
.route("/groups/{id}/solo", post(routes::groups::solo_group))
.route("/groups/{id}/unsolo", post(routes::groups::unsolo_group))
.route(
"/groups/{id}/params/{param}",
put(routes::groups::set_group_param),
)
.route("/voices", get(routes::voices::list_voices))
.route("/voices", post(routes::voices::create_voice))
.route("/voices/{id}", get(routes::voices::get_voice))
.route("/voices/{id}", patch(routes::voices::update_voice))
.route("/voices/{id}", delete(routes::voices::delete_voice))
.route("/voices/{id}/trigger", post(routes::voices::trigger_voice))
.route("/voices/{id}/stop", post(routes::voices::stop_voice))
.route("/voices/{id}/note-on", post(routes::voices::note_on))
.route("/voices/{id}/note-off", post(routes::voices::note_off))
.route(
"/voices/{id}/params/{param}",
put(routes::voices::set_voice_param),
)
.route("/voices/{id}/mute", post(routes::voices::mute_voice))
.route("/voices/{id}/unmute", post(routes::voices::unmute_voice))
.route("/patterns", get(routes::patterns::list_patterns))
.route("/patterns", post(routes::patterns::create_pattern))
.route("/patterns/{id}", get(routes::patterns::get_pattern))
.route("/patterns/{id}", patch(routes::patterns::update_pattern))
.route("/patterns/{id}", delete(routes::patterns::delete_pattern))
.route(
"/patterns/{id}/start",
post(routes::patterns::start_pattern),
)
.route("/patterns/{id}/stop", post(routes::patterns::stop_pattern))
.route("/melodies", get(routes::melodies::list_melodies))
.route("/melodies", post(routes::melodies::create_melody))
.route("/melodies/{id}", get(routes::melodies::get_melody))
.route("/melodies/{id}", patch(routes::melodies::update_melody))
.route("/melodies/{id}", delete(routes::melodies::delete_melody))
.route("/melodies/{id}/start", post(routes::melodies::start_melody))
.route("/melodies/{id}/stop", post(routes::melodies::stop_melody))
.route("/sequences", get(routes::sequences::list_sequences))
.route("/sequences", post(routes::sequences::create_sequence))
.route("/sequences/{id}", get(routes::sequences::get_sequence))
.route("/sequences/{id}", patch(routes::sequences::update_sequence))
.route(
"/sequences/{id}",
delete(routes::sequences::delete_sequence),
)
.route(
"/sequences/{id}/start",
post(routes::sequences::start_sequence),
)
.route(
"/sequences/{id}/stop",
post(routes::sequences::stop_sequence),
)
.route(
"/sequences/{id}/pause",
post(routes::sequences::pause_sequence),
)
.route("/effects", get(routes::effects::list_effects))
.route("/effects/{id}", get(routes::effects::get_effect))
.route("/effects/{id}", patch(routes::effects::update_effect))
.route("/effects/{id}", delete(routes::effects::delete_effect))
.route(
"/effects/{id}/params/{param}",
put(routes::effects::set_effect_param),
)
.route("/samples", get(routes::samples::list_samples))
.route("/samples", post(routes::samples::load_sample))
.route("/samples/{id}", get(routes::samples::get_sample))
.route("/samples/{id}", delete(routes::samples::delete_sample))
.route("/synthdefs", get(routes::synthdefs::list_synthdefs))
.route("/synthdefs/{name}", get(routes::synthdefs::get_synthdef))
.route("/eval", post(routes::eval::eval_code))
.route("/live", get(routes::live::get_live_state))
.route("/live/transport", get(routes::live::get_transport_state))
.route("/live/fades", get(routes::live::get_active_fades))
.route("/live/meters", get(routes::live::get_meters))
.route("/fades", get(routes::live::get_active_fades))
.route("/fades", post(routes::fades::start_fade))
.route("/fades", delete(routes::fades::cancel_fade))
.route("/fades/voice/{name}", post(routes::fades::fade_voice))
.route("/fades/group/{path}", post(routes::fades::fade_group))
.route("/fades/effect/{id}", post(routes::fades::fade_effect))
.route("/ws", get(websocket::ws_handler));
#[cfg(feature = "midi")]
let app = app
.route("/midi/devices", get(routes::midi::list_devices))
.route("/midi/input/open", post(routes::midi::open_input))
.route("/midi/output/open", post(routes::midi::open_output))
.route("/midi/close", post(routes::midi::close_device))
.route("/midi/note/on", post(routes::midi::send_note_on))
.route("/midi/note/off", post(routes::midi::send_note_off))
.route("/midi/cc", post(routes::midi::send_cc))
.route("/midi/record/start", post(routes::midi::start_recording))
.route("/midi/record/stop", post(routes::midi::stop_recording))
.route(
"/midi/clock/enable",
post(routes::midi::enable_clock_output),
)
.route(
"/midi/clock/disable",
post(routes::midi::disable_clock_output),
)
.route("/midi/transport/start", post(routes::midi::send_midi_start))
.route("/midi/transport/stop", post(routes::midi::send_midi_stop))
.route(
"/midi/transport/continue",
post(routes::midi::send_midi_continue),
)
.route("/midi/routes", get(routes::midi::list_routes))
.route("/midi/routes", delete(routes::midi::clear_routes))
.route(
"/midi/route/keyboard",
post(routes::midi::add_keyboard_route),
)
.route(
"/midi/route/{index}",
delete(routes::midi::remove_keyboard_route),
);
let app = app.with_state(app_state).layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any),
);
let addr = SocketAddr::from(([0, 0, 0, 0], port));
tracing::info!(
"HTTP API server starting on http://{}:{}",
addr.ip(),
addr.port()
);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}