subsonic-vita/src/lib/audio.cpp
2025-11-16 15:41:37 +02:00

526 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <psp2/kernel/processmgr.h>
#include <psp2/audioout.h>
#include <psp2/kernel/threadmgr.h>
#include <curl/curl.h>
#include <string.h>
#define DR_FLAC_IMPLEMENTATION
#include "dr_flac.h"
#define DR_MP3_IMPLEMENTATION
#include "dr_mp3.h"
#include "audio.hpp"
// ==============================
// Internal State
// ==============================
static volatile bool gRunning = false;
static volatile bool gPaused = false;
static volatile bool gStopRequest = false;
static volatile bool stream_eof = false;
static SceUID gCurlThread = -1;
static SceUID gDecodeThread = -1;
static SceUID gAudioThread = -1;
static int audio_port = -1;
// ==============================
// Ringbuffer
// ==============================
#define STREAMBUF_SIZE (1024 * 1024 * 2)
#define FRAMES_PER_CHUNK 1024
static uint8_t streambuf[STREAMBUF_SIZE];
static volatile size_t wb = 0, rb = 0;
// ==============================
// Double-buffer PCM
// ==============================
static int16_t gAudioBuf[2][FRAMES_PER_CHUNK * 2];
static volatile int gBufReady[2] = {0,0};
static volatile int gDecodeIndex = 0;
static volatile int gPlayIndex = 0;
static volatile bool gDecodeDone = false;
static volatile uint64_t gDecodedFrames = 0;
static volatile uint64_t gTotalFrames = 0;
static volatile uint32_t gSampleRate = 44100; // default
static volatile bool gHasLength = false; // FLAC streaming may not have this
// =======================================================================
// CURL callback
// =======================================================================
static size_t curl_cb(void *ptr, size_t size, size_t nmemb, void *userdata)
{
if (gStopRequest) return 0;
size_t total = size * nmemb;
const uint8_t *in = (const uint8_t*)ptr;
size_t written = 0;
while (written < total && !gStopRequest)
{
size_t used = (wb + STREAMBUF_SIZE - rb) % STREAMBUF_SIZE;
size_t free = STREAMBUF_SIZE - used - 1;
if (free == 0) {
sceKernelDelayThread(1000);
continue;
}
size_t chunk = total - written;
if (chunk > free) chunk = free;
size_t tillEnd = STREAMBUF_SIZE - wb;
size_t first = (chunk < tillEnd) ? chunk : tillEnd;
size_t second = chunk - first;
memcpy(&streambuf[wb], in + written, first);
wb = (wb + first) % STREAMBUF_SIZE;
written += first;
if (second > 0) {
memcpy(&streambuf[wb], in + written, second);
wb = (wb + second) % STREAMBUF_SIZE;
written += second;
}
}
return total;
}
// =======================================================================
// Streaming read callback for both FLAC and MP3
// =======================================================================
static unsigned int stream_read(void *user, void *out, unsigned int want)
{
(void)user;
if (gStopRequest) return 0;
uint8_t *o = (uint8_t*)out;
size_t read = 0;
while (read < want && !gStopRequest)
{
size_t available = (wb + STREAMBUF_SIZE - rb) % STREAMBUF_SIZE;
if (available == 0) {
if (stream_eof) break;
sceKernelDelayThread(1000);
continue;
}
size_t chunk = want - read;
if (chunk > available) chunk = available;
size_t tillEnd = STREAMBUF_SIZE - rb;
size_t first = (chunk < tillEnd) ? chunk : tillEnd;
size_t second = chunk - first;
memcpy(o + read, &streambuf[rb], first);
rb = (rb + first) % STREAMBUF_SIZE;
read += first;
if (second > 0) {
memcpy(o + read, &streambuf[rb], second);
rb = (rb + second) % STREAMBUF_SIZE;
read += second;
}
}
return read;
}
static drflac_bool32 flac_seek(void*, int, drflac_seek_origin)
{
return DRFLAC_FALSE;
}
static drmp3_bool32 mp3_seek(void* user, int offset, drmp3_seek_origin origin)
{
(void)user;
(void)offset;
(void)origin;
// Streaming over HTTP no real seeking.
return DRMP3_FALSE;
}
static drflac_bool32 flac_tell(void*, drflac_int64 *pos)
{
*pos = 0;
return DRFLAC_TRUE;
}
// =======================================================================
// Threads
// =======================================================================
static int curl_thread(unsigned int, void *url_)
{
CURL *c = curl_easy_init();
curl_easy_setopt(c, CURLOPT_URL, (char*)url_);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, curl_cb);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_BUFFERSIZE, 64 * 1024);
curl_easy_perform(c);
stream_eof = true;
curl_easy_cleanup(c);
return 0;
}
// -----------------------------------------------------------
// AUDIO THREAD
// -----------------------------------------------------------
static int audio_thread(unsigned int, void*)
{
for (;;)
{
if (gStopRequest) break;
if (gPaused) {
sceKernelDelayThread(10000);
continue;
}
if (!gBufReady[gPlayIndex]) {
if (gDecodeDone && !gBufReady[gPlayIndex ^ 1])
break;
sceKernelDelayThread(1000);
continue;
}
sceAudioOutOutput(audio_port, gAudioBuf[gPlayIndex]);
gBufReady[gPlayIndex] = 0;
gPlayIndex ^= 1;
}
return 0;
}
static void mp3_meta(void* user, const drmp3_metadata* meta)
{
// do nothing
}
// MP3 DECODE THREAD
// -----------------------------------------------------------
static int decode_mp3_thread(unsigned int, void*)
{
// Wait until some header/data is buffered
while (((wb + STREAMBUF_SIZE - rb) % STREAMBUF_SIZE) < 32 && !gStopRequest)
sceKernelDelayThread(10000);
if (gStopRequest) return 0;
drmp3 mp3;
if (!drmp3_init(&mp3, stream_read, mp3_seek, NULL, mp3_meta, NULL, NULL))
return 0;
gSampleRate = mp3.sampleRate;
gTotalFrames = 0; // unknown for streaming MP3
gHasLength = false;
audio_port = sceAudioOutOpenPort(
SCE_AUDIO_OUT_PORT_TYPE_BGM,
FRAMES_PER_CHUNK,
mp3.sampleRate,
SCE_AUDIO_OUT_MODE_STEREO
);
if (audio_port < 0) {
drmp3_uninit(&mp3);
return 0;
}
int vol[2] = {0x8000, 0x8000};
sceAudioOutSetVolume(
audio_port,
(SceAudioOutChannelFlag)(SCE_AUDIO_VOLUME_FLAG_L_CH | SCE_AUDIO_VOLUME_FLAG_R_CH),
vol
);
gDecodeIndex = 0;
gPlayIndex = 0;
gDecodeDone = false;
gBufReady[0] = 0;
gBufReady[1] = 0;
// Start audio thread (same as FLAC)
gAudioThread = sceKernelCreateThread(
"audio_out_thread",
audio_thread,
0x40,
0x10000,
0, 0, NULL
);
sceKernelStartThread(gAudioThread, 0, NULL);
int16_t inBuf[FRAMES_PER_CHUNK * 8]; // extra space, channels <= 2 anyway
while (!gStopRequest)
{
int idx = gDecodeIndex;
// Wait for playback to consume this buffer
while (gBufReady[idx] && !gStopRequest)
sceKernelDelayThread(1000);
if (gStopRequest) break;
drmp3_uint64 frames64 = drmp3_read_pcm_frames_s16(&mp3, FRAMES_PER_CHUNK, inBuf);
size_t frames = (size_t)frames64;
gDecodedFrames += frames;
if (frames == 0) {
gDecodeDone = true;
break;
}
int16_t *outBuf = gAudioBuf[idx];
if (mp3.channels == 1) {
// Mono -> Stereo
for (size_t i = 0; i < frames; i++) {
outBuf[i*2 + 0] = inBuf[i];
outBuf[i*2 + 1] = inBuf[i];
}
} else {
// Interleaved stereo (or more, but we only keep first 2 channels)
for (size_t i = 0; i < frames; i++) {
outBuf[i*2 + 0] = inBuf[i*mp3.channels + 0];
outBuf[i*2 + 1] = inBuf[i*mp3.channels + 1];
}
}
// Zero-pad last chunk if short
if (frames < FRAMES_PER_CHUNK) {
memset(&outBuf[frames * 2], 0, (FRAMES_PER_CHUNK - frames) * 4);
}
gBufReady[idx] = 1;
gDecodeIndex ^= 1;
}
drmp3_uninit(&mp3);
return 0;
}
// -----------------------------------------------------------
// DECODE THREAD
// -----------------------------------------------------------
static int decode_flac_thread(unsigned int, void*)
{
// Wait FLAC header
while (((wb + STREAMBUF_SIZE - rb) % STREAMBUF_SIZE) < 32 && !gStopRequest)
sceKernelDelayThread(10000);
if (gStopRequest) return 0;
drflac *flac = drflac_open(stream_read, flac_seek, flac_tell, NULL, NULL);
if (!flac) return 0;
gSampleRate = flac->sampleRate;
gTotalFrames = flac->totalPCMFrameCount;
gHasLength = (gTotalFrames > 0);
audio_port = sceAudioOutOpenPort(
SCE_AUDIO_OUT_PORT_TYPE_BGM,
FRAMES_PER_CHUNK,
flac->sampleRate,
SCE_AUDIO_OUT_MODE_STEREO
);
int vol[2] = {0x8000,0x8000};
sceAudioOutSetVolume(audio_port, (SceAudioOutChannelFlag)(SCE_AUDIO_VOLUME_FLAG_L_CH | SCE_AUDIO_VOLUME_FLAG_R_CH), vol);
gDecodeIndex = 0;
gPlayIndex = 0;
gDecodeDone = false;
gBufReady[0] = gBufReady[1] = 0;
int16_t inBuf[FRAMES_PER_CHUNK * 8];
// Start audio thread
gAudioThread = sceKernelCreateThread("audio_out_thread", audio_thread, 0x40, 0x10000, 0, 0, NULL);
sceKernelStartThread(gAudioThread, 0, NULL);
// Decode loop
while (!gStopRequest)
{
int idx = gDecodeIndex;
while (gBufReady[idx] && !gStopRequest)
sceKernelDelayThread(1000);
if (gStopRequest) break;
size_t frames = drflac_read_pcm_frames_s16(flac, FRAMES_PER_CHUNK, inBuf);
gDecodedFrames += frames;
if (frames == 0) {
gDecodeDone = true;
break;
}
int16_t *outBuf = gAudioBuf[idx];
if (flac->channels == 1) {
for (size_t i = 0; i < frames; i++) {
outBuf[i*2+0] = inBuf[i];
outBuf[i*2+1] = inBuf[i];
}
} else {
for (size_t i = 0; i < frames; i++) {
outBuf[i*2+0] = inBuf[i*flac->channels + 0];
outBuf[i*2+1] = inBuf[i*flac->channels + 1];
}
}
if (frames < FRAMES_PER_CHUNK) {
memset(&outBuf[frames * 2], 0, (FRAMES_PER_CHUNK - frames) * 4);
}
gBufReady[idx] = 1;
gDecodeIndex ^= 1;
}
drflac_close(flac);
return 0;
}
// =======================================================================
// Public API
// =======================================================================
float Audio_GetLengthSeconds()
{
if (!gHasLength) return 0.0f;
return (float)gTotalFrames / (float)gSampleRate;
}
float Audio_GetProgress()
{
if (!gHasLength || gTotalFrames == 0) return 0.0f;
return (float)gDecodedFrames / (float)gTotalFrames;
}
bool Audio_IsPlaying()
{
if(gStopRequest) {
return false;
} else {
return !gDecodeDone && gRunning;
}
}
float Audio_SetProgress(float p)
{
if (p < 0.0f) p = 0.0f;
if (p > 1.0f) p = 1.0f;
if (!gHasLength || gTotalFrames == 0)
return 0.0f;
return ((float)gTotalFrames / (float)gSampleRate) * p;
}
float Audio_GetPositionSeconds()
{
return (float)gDecodedFrames / (float)gSampleRate;
}
void Audio_Init()
{
curl_global_init(CURL_GLOBAL_DEFAULT);
}
void Audio_Play(const char *url)
{
if (gRunning) Audio_Stop();
gDecodedFrames = 0;
gTotalFrames = 0;
gHasLength = false;
gSampleRate = 44100;
gStopRequest = false;
gRunning = true;
gPaused = false;
stream_eof = false;
wb = rb = 0;
gCurlThread = sceKernelCreateThread("curl_stream", curl_thread, 0x40, 0x10000, 0, 0, NULL);
sceKernelStartThread(gCurlThread, strlen(url)+1, (void*)url);
gDecodeThread = sceKernelCreateThread("decode_thread_flac", decode_flac_thread, 0x40, 0x20000, 0, 0, NULL);
sceKernelStartThread(gDecodeThread, 0, NULL);
}
void Audio_PlayMP3(const char *url)
{
if (gRunning) Audio_Stop();
gDecodedFrames = 0;
gTotalFrames = 0;
gHasLength = false;
gSampleRate = 44100;
gStopRequest = false;
gRunning = true;
gPaused = false;
stream_eof = false;
wb = rb = 0;
gCurlThread = sceKernelCreateThread("curl_stream_mp3", curl_thread, 0x40, 0x10000, 0, 0, NULL);
sceKernelStartThread(gCurlThread, strlen(url)+1, (void*)url);
gDecodeThread = sceKernelCreateThread("decode_thread_mp3", decode_mp3_thread, 0x40, 0x20000, 0, 0, NULL);
sceKernelStartThread(gDecodeThread, 0, NULL);
}
bool Audio_IsPaused() {
return gPaused;
}
void Audio_Pause()
{
gPaused = true;
}
void Audio_Resume()
{
gPaused = false;
}
void Audio_Stop()
{
if (!gRunning) return;
gStopRequest = true;
sceKernelDelayThread(50000);
if (audio_port >= 0)
sceAudioOutReleasePort(audio_port);
audio_port = -1;
gRunning = false;
}
void Audio_Shutdown()
{
Audio_Stop();
curl_global_cleanup();
}