// // 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); }