526 lines
13 KiB
C++
526 lines
13 KiB
C++
|
||
#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();
|
||
}
|