first commit
This commit is contained in:
commit
745ae2fff2
22 changed files with 21283 additions and 0 deletions
526
src/lib/audio.cpp
Normal file
526
src/lib/audio.cpp
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
|
||||
#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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue