audioplayer/src/audio/player.jai
2026-05-07 20:21:29 +03:00

145 lines
4.7 KiB
Plaintext

//
// Audio playback. Downloads the track as OGG Vorbis (Jellyfin transcodes
// everything server-side), decodes locally to s16 PCM via stb_vorbis, and
// hands the LINEAR_SAMPLE_ARRAY to Sound_Player. The visualizer reads from
// the same sd.samples buffer — single source of truth, no parallel decoder,
// no cursor desync.
//
// We own the Sound_Data struct, the stb_vorbis-malloc'd PCM buffer (held in
// sd.samples / sd.buffer), and the Track copy. All freed in
// sound_release_callback when Sound_Player is finished with the stream.
//
audio_play_track :: (track: Track) {
if !app.audio_inited return;
pending := New(Track);
pending.* = clone_track(track);
// Ask Jellyfin to transcode everything to OGG Vorbis 192 kbps.
// We pin audioSampleRate to Sound_Player's actual output device rate so
// the OGG file's sample rate matches what Sound_Player expects when
// calculating desired_rate (= sound_data.sampling_rate / output_rate).
// Without this, Jellyfin defaults to 48000 Hz on many systems while
// sound_data.sampling_rate stays at 44100 → plays at 91.9% speed.
output_hz := Sound.get_audio_sampling_rate();
if output_hz <= 0 output_hz = 44100;
path := tprint(
"/Audio/%/universal?container=ogg&audioCodec=vorbis&maxStreamingBitrate=192000&audioSampleRate=%&audioChannels=2&userId=%&deviceId=%&api_key=%",
track.id, output_hz, app.jellyfin.user_id, app.jellyfin.device_id, app.jellyfin.auth_token,
);
log_info("audio: downloading '%' (% ticks)", track.name, track.duration_ticks);
http_submit("GET", path, on_done=on_track_downloaded, user_data=pending);
}
audio_toggle_pause :: () {
if !app.current_stream return;
s := app.current_stream;
app.paused = !app.paused;
if app.paused {
s.current_rate = 0;
s.desired_rate = 0;
s.inaudible = true;
} else {
s.current_rate = 1;
s.desired_rate = 1;
s.inaudible = false;
}
media_controls_notify_status();
}
audio_is_paused :: () -> bool {
return app.paused;
}
audio_seek_seconds :: (seconds: float) {
if !app.current_stream || !app.current_stream.sound_data return;
rate := cast(float64) app.current_stream.sound_data.sampling_rate;
target := cast(float64) seconds * rate;
if target < 0 target = 0;
app.current_stream.play_cursor = target;
}
stop_current_stream :: () {
if !app.current_stream return;
Sound.stop_stream_abruptly(app.current_stream.entity_id);
app.current_stream = null;
}
// Called by Sound_Player when it is finished with a stream (natural end or
// stop_stream_abruptly). We own the Sound_Data struct and the OGG bytes in
// sound_data.buffer; free both here.
sound_release_callback :: (stream: *Sound.Sound_Stream, data: *Sound.Sound_Data) {
free(data.buffer);
free(data.name);
free(data);
if stream && stream.entity_id == app.track_entity_id {
app.track_finished = true;
app.current_stream = null;
}
}
clone_track :: (t: Track) -> Track {
out: Track;
out.id = copy_string(t.id);
out.name = copy_string(t.name);
out.album = copy_string(t.album);
out.album_id = copy_string(t.album_id);
out.artist = copy_string(t.artist);
out.artist_id = copy_string(t.artist_id);
out.duration_ticks = t.duration_ticks;
out.index_number = t.index_number;
out.is_favourite = t.is_favourite;
return out;
}
free_track :: (t: *Track) {
free(t.id);
free(t.name);
free(t.album);
free(t.album_id);
free(t.artist);
free(t.artist_id);
t.* = .{};
}
#scope_file
on_track_downloaded :: (task: *Http_Task) {
pending := cast(*Track) task.user_data;
defer { free_track(pending); free(pending); }
if !task.response.ok {
log_error("audio download '%' failed: status=%", pending.name, task.response.status_code);
return;
}
bytes := task.response.body;
task.response.body = "";
defer free(bytes); // stb_vorbis_decode_memory copies what it needs
sd, ok := decode_ogg(bytes, pending.name);
if !ok return;
data := New(Sound.Sound_Data);
data.* = sd;
stop_current_stream();
free_track(*app.current_track);
app.current_track = clone_track(pending.*);
app.paused = false;
app.track_entity_id += 1;
stream := Sound.make_stream(data, .MUSIC);
stream.entity_id = app.track_entity_id;
Sound.start_playing(stream);
app.current_stream = stream;
app.track_finished = false;
media_controls_notify_track();
lyrics_request(app.current_track.id);
log_info("audio: playing '% — %' artist_id=%",
app.current_track.artist, app.current_track.name, app.current_track.artist_id);
}