From 2324d5ad9ece58905818035dfa91fad8288c255b Mon Sep 17 00:00:00 2001 From: David Reid Date: Mon, 16 Feb 2026 19:07:35 +1000 Subject: [PATCH] Add Dreamcast backend. --- .gitignore | 5 + miniaudio.h | 596 +++++++++++++++++++++ tests/dreamcast/Makefile | 22 + tests/dreamcast/kos_environ.sh | 4 + tests/dreamcast/kos_make.sh | 8 + tests/dreamcast/miniaudio_dreamcast_test.c | 186 +++++++ 6 files changed, 821 insertions(+) create mode 100644 tests/dreamcast/Makefile create mode 100644 tests/dreamcast/kos_environ.sh create mode 100644 tests/dreamcast/kos_make.sh create mode 100644 tests/dreamcast/miniaudio_dreamcast_test.c diff --git a/.gitignore b/.gitignore index 85cf9cf7..f26e0740 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,11 @@ /tests/_build/a.out /tests/_build/a.exe /tests/debugging/archive/ +/tests/dreamcast/miniaudio.elf +/tests/dreamcast/miniaudio_dreamcast_test.o +/tests/dreamcast/romdisk.img +/tests/dreamcast/romdisk.o +/tests/dreamcast/romdisk/ /tests/*.c /tests/*.cpp /website/docs/ diff --git a/miniaudio.h b/miniaudio.h index eb0fbabf..755f0584 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -3734,6 +3734,7 @@ example, ALSA, which is specific to Linux, will not be included in the Windows b | AAudio | ma_device_backend_aaudio | Android 8+ | | OpenSL|ES | ma_device_backend_opensl | Android (API level 16+) | | Web Audio | ma_device_backend_webaudio | Web (via Emscripten) | + | Dreamcast | ma_device_backend_dreamcast | KallistiOS | | Null | ma_device_backend_null | Cross Platform (not used on Web) | +-------------+------------------------------+--------------------------------------------------------+ @@ -3800,6 +3801,62 @@ Some backends have some nuance details you may want to be aware of. https://developers.google.com/web/updates/2017/09/autoplay-policy-changes. Starting the device may fail if you try to start playback without first handling some kind of user input. +15.6. Dreamcast +--------------- +The Dreamcast backend uses KallistiOS. + +miniaudio will call `snd_init()` automatically unless you disable it via the context config: + +```c +contextConfig.dreamcast.noInit = MA_TRUE; +``` + +The following device configuration is recommended. + + Format: ma_format_u8 or ma_format_s16 + Channels: 1 or 2 + Sample Rate: 44100, 22050 or 11025 + Period Size: 1024 to 16384, multiple of 32. Recommended you keep it a power of two. + noFixedSizedCallback: True (This will tell miniaudio to bypass one of its internal buffers.) + +If you follow the above rules, miniaudio will skip it's data conversion pipeline and will pass +through a direct pointer to an internal buffer to the data callback without any extra buffering or +data conversion. You are still free to use other formats, but it'll just have a bit more overhead. +The miniaudio resampler will have enough overhead that it's strongly recommended you keep the +sample rate set to one of the recommended values and pre-process your audio assets to match. + +You can initialize multiple devices and specify which voice channel you want to use. There is a +maximum of 64 voices. If you request two channels when initializing the device it will use two of +them. You can specify the index of the voice channel you want to use in the dreamcast device +config. You can use multiple devices to have the hardware do mixing for you, but if you do this +make sure you manage your voice channel properly. To configure the voice channel, set it in the +`dreamcast` config: + +```c +deviceConfig.dreamcast.voiceChannel = 2; // 0 if the default. The left channel will use `voiceChannel + 0`, the right channel will use `voiceChannel + 1`. +``` + +If you initialize more than one `ma_device` object, self manage a single `ma_context` object. It is +a single context to many devices: + +```c +ma_context context; +ma_context_init(...); + +deviceConfig.dreamcast.voiceChannel = 0; +ma_device_init(&context, &deviceConfig, &device0); + +deviceConfig.dreamcast.voiceChannel = 2; // <-- Make sure the voice channel is different between devices. Remember that a stereo device consumes two channels. +ma_device_init(&context, &deviceConfig, &device1); +``` + +Note that the `ma_device` struct itself has memory overhead just from its own members. + +By default audio will be processed on a background thread. You can disable threading and run your +device in single threaded mode by disabling it at compiling with the `MA_NO_THREADING` define. By +disabling threading at compile time you will also reduce memory usage of the `ma_device` struct. +When running in single threaded mode you need to manually step the device periodically with +`ma_device_step()`, which you would normally do each frame. 16. Optimization Tips @@ -7343,6 +7400,28 @@ MA_API ma_device_backend_vtable* ma_webaudio_get_vtable(void); /* END WEBAUDIO */ +/* BEG miniaudio_dreamcast.h */ +typedef struct +{ + ma_bool32 noInit; /* When set to true, will *not* call snd_init() when the context is initialized. */ +} ma_context_config_dreamcast; + +MA_API ma_context_config_dreamcast ma_context_config_dreamcast_init(void); + + +typedef struct +{ + int voiceChannel; /* For a stereo stream, the left channel will be voiceChannel and the right channel will be voiceChannel+1. */ +} ma_device_config_dreamcast; + +MA_API ma_device_config_dreamcast ma_device_config_dreamcast_init(int voiceChannel); + + +extern ma_device_backend_vtable* ma_device_backend_dreamcast; +MA_API ma_device_backend_vtable* ma_dreamcast_get_vtable(void); +/* END miniaudio_dreamcast.h */ + + /* BEG NULL */ typedef struct ma_context_config_null { @@ -7773,6 +7852,7 @@ struct ma_device_config ma_device_config_aaudio aaudio; ma_device_config_opensl opensl; ma_device_config_webaudio webaudio; + ma_device_config_dreamcast dreamcast; ma_device_config_null null_backend; }; @@ -7912,6 +7992,7 @@ struct ma_context_config ma_context_config_aaudio aaudio; ma_context_config_opensl opensl; ma_context_config_webaudio webaudio; + ma_context_config_dreamcast dreamcast; ma_context_config_null null_backend; }; @@ -19861,6 +19942,9 @@ BACKENDS #if defined(MA_EMSCRIPTEN) #define MA_SUPPORT_WEBAUDIO #endif +#if defined(MA_DREAMCAST) + #define MA_SUPPORT_DREAMCAST +#endif /* All platforms should support custom backends. */ #define MA_SUPPORT_CUSTOM @@ -19913,6 +19997,9 @@ BACKENDS #if defined(MA_SUPPORT_WEBAUDIO) && !defined(MA_NO_WEBAUDIO) && (!defined(MA_ENABLE_ONLY_SPECIFIC_BACKENDS) || defined(MA_ENABLE_WEBAUDIO)) #define MA_HAS_WEBAUDIO #endif +#if defined(MA_SUPPORT_DREAMCAST) && !defined(MA_NO_DREAMCAST) && (!defined(MA_ENABLE_ONLY_SPECIFIC_BACKENDS) || defined(MA_ENABLE_DREAMCAST)) + #define MA_HAS_DREAMCAST +#endif #if defined(MA_SUPPORT_NULL) && !defined(MA_NO_NULL) && (!defined(MA_ENABLE_ONLY_SPECIFIC_BACKENDS) || defined(MA_ENABLE_NULL)) #define MA_HAS_NULL #endif @@ -47405,6 +47492,508 @@ MA_API ma_device_backend_vtable* ma_webaudio_get_vtable(void) + +/* BEG miniaudio_dreamcast.c */ +#if defined(MA_HAS_DREAMCAST) +#include +#include +#include + +typedef struct +{ + int _unused; +} ma_context_state_dreamcast; + +typedef struct +{ + ma_int8 voiceChannel; + ma_int8 subBufferIndex; /* Flip-flops between 0 and 1 and is used to determine which half of the buffer needs to be updated with fresh data. */ + ma_int8 padding[2]; + ma_uint32 buffer; /* Memory allocation on the SPU. Returned by snd_mem_malloc(). */ + void* pIntermediaryBuffer; /* Big enough for one period (one half of the buffer). Used for transferring to the SPU buffer. */ +} ma_device_state_dreamcast; + + +static ma_context_state_dreamcast* ma_context_get_backend_state__dreamcast(ma_context* pContext) +{ + return (ma_context_state_dreamcast*)ma_context_get_backend_state(pContext); +} + +static ma_device_state_dreamcast* ma_device_get_backend_state__dreamcast(ma_device* pDevice) +{ + return (ma_device_state_dreamcast*)ma_device_get_backend_state(pDevice); +} + + +static ma_uint32 ma_format_to_dreamcast(ma_format format) +{ + if (format == ma_format_u8) { + return AICA_SM_8BIT; + } else { + return AICA_SM_16BIT; + } +} + + +static void ma_backend_info__dreamcast(ma_device_backend_info* pBackendInfo) +{ + MA_ASSERT(pBackendInfo != NULL); + pBackendInfo->pName = "Dreamcast"; +} + +static ma_result ma_context_init__dreamcast(ma_context* pContext, const void* pContextBackendConfig, void** ppContextState) +{ + ma_context_state_dreamcast* pContextStateDreamcast; + const ma_context_config_dreamcast* pContextConfigDreamcast = (ma_context_config_dreamcast*)pContextBackendConfig; + ma_log* pLog = ma_context_get_log(pContext); + + /* The context config is not currently being used for this backend. */ + (void)pContextConfigDreamcast; + (void)pLog; + + if (!pContextConfigDreamcast->noInit) { + snd_init(); + } + + /* We don't actually do anything with context state so there's no need wasting time and memory allocating anything. */ + #if 0 + { + pContextStateDreamcast = (ma_context_state_dreamcast*)ma_calloc(sizeof(*pContextStateDreamcast), ma_context_get_allocation_callbacks(pContext)); + if (pContextStateDreamcast == NULL) { + return MA_OUT_OF_MEMORY; + } + } + #else + { + pContextStateDreamcast = NULL; + } + #endif + + *ppContextState = pContextStateDreamcast; + + return MA_SUCCESS; +} + +static void ma_context_uninit__dreamcast(ma_context* pContext) +{ + ma_context_state_dreamcast* pContextStateDreamcast = ma_context_get_backend_state__dreamcast(pContext); + + #if 0 + ma_free(pContextStateDreamcast, ma_context_get_allocation_callbacks(pContext)); + #else + (void)pContextStateDreamcast; + #endif +} + +static ma_result ma_context_enumerate_devices__dreamcast(ma_context* pContext, ma_enum_devices_callback_proc callback, void* pCallbackUserData) +{ + ma_context_state_dreamcast* pContextStateDreamcast = ma_context_get_backend_state__dreamcast(pContext); + ma_device_info deviceInfo; + + (void)pContextStateDreamcast; + + /* We're only outputting a single default playback device. */ + MA_ZERO_OBJECT(&deviceInfo); + deviceInfo.isDefault = MA_TRUE; + deviceInfo.id.custom.i = 0; + ma_strncpy_s(deviceInfo.name, sizeof(deviceInfo.name), "Default Playback Device", (size_t)-1); + + /* We can do u8 and s16, mono or stereo, and 11025, 22050 or 44100. */ + ma_device_info_add_native_data_format(&deviceInfo, ma_format_s16, 1, 2, 11025, 44100); + ma_device_info_add_native_data_format(&deviceInfo, ma_format_u8, 1, 2, 11025, 44100); + + return MA_SUCCESS; +} + +static ma_result ma_device_init__dreamcast(ma_device* pDevice, const void* pDeviceBackendConfig, ma_device_descriptor* pDescriptorPlayback, ma_device_descriptor* pDescriptorCapture, void** ppDeviceState) +{ + ma_device_state_dreamcast* pDeviceStateDreamcast; + ma_device_config_dreamcast* pDeviceConfigDreamcast = (ma_device_config_dreamcast*)pDeviceBackendConfig; + ma_context_state_dreamcast* pContextStateDreamcast = ma_context_get_backend_state__dreamcast(ma_device_get_context(pDevice)); + ma_device_config_dreamcast defaultConfig; + ma_device_type deviceType = ma_device_get_type(pDevice); + ma_uint32 bufferSizeInBytes; + ma_uint32 bpf; + + (void)pContextStateDreamcast; + (void)pDescriptorCapture; + + if (pDeviceConfigDreamcast == NULL) { + defaultConfig = ma_device_config_dreamcast_init(0); + pDeviceConfigDreamcast = &defaultConfig; + } + + /* Can only support playback. */ + if (deviceType != ma_device_type_playback) { + return MA_DEVICE_TYPE_NOT_SUPPORTED; + } + + pDeviceStateDreamcast = (ma_device_state_dreamcast*)ma_calloc(sizeof(*pDeviceStateDreamcast), ma_device_get_allocation_callbacks(pDevice)); + if (pDeviceStateDreamcast == NULL) { + return MA_OUT_OF_MEMORY; + } + + pDeviceStateDreamcast->subBufferIndex = 1; /* Should start at one. */ + + /* Default to s16 for the format. Can support u8 and s16. */ + if (pDescriptorPlayback->format != ma_format_u8 /*&& pDescriptorPlayback->format != ma_format_s16*/) { + pDescriptorPlayback->format = ma_format_s16; + } + + /* The channels needs to be mono or stereo. */ + if (pDescriptorPlayback->channels == 0 || pDescriptorPlayback->channels > 2) { + pDescriptorPlayback->channels = 2; + } + + /* Now that we know our channel count we need to verify our voice channel index. There is a maximum of 64. Stereo uses two of them. */ + if (pDeviceConfigDreamcast->voiceChannel + pDescriptorPlayback->channels > 64) { + return MA_INVALID_ARGS; /* Voice channel exceeds limit. */ + } + + pDeviceStateDreamcast->voiceChannel = (ma_uint8)pDeviceConfigDreamcast->voiceChannel; + + /* The sample rate is always 44100, 22050 or 11025. */ + if (pDescriptorPlayback->sampleRate != 44100 && pDescriptorPlayback->sampleRate != 22050 && pDescriptorPlayback->sampleRate != 11025) { + pDescriptorPlayback->sampleRate = 44100; + } + + /* Channel map. */ + ma_channel_map_init_standard(ma_standard_channel_map_default, pDescriptorPlayback->channelMap, ma_countof(pDescriptorPlayback->channelMap), pDescriptorPlayback->channels); + + /* The period count is always 2 (double buffered). */ + pDescriptorPlayback->periodCount = 2; + + /* + The buffer cannot exceed 65534. Since we'll always be using 2 periods (double buffering), the maximum period + size in frames is 32767 = 65534/2. We're also going to align this to 32 bytes. + */ + pDescriptorPlayback->periodSizeInFrames = ma_calculate_buffer_size_in_frames_from_descriptor(pDescriptorPlayback, pDescriptorPlayback->sampleRate); + if (pDescriptorPlayback->periodSizeInFrames > 32767) { + pDescriptorPlayback->periodSizeInFrames = 32767; + } + if (pDescriptorPlayback->periodSizeInFrames < 1024) { + pDescriptorPlayback->periodSizeInFrames = 1024; + } + + bpf = ma_get_bytes_per_frame(pDescriptorPlayback->format, pDescriptorPlayback->channels); + bufferSizeInBytes = (pDescriptorPlayback->periodSizeInFrames & ~31) * bpf * pDescriptorPlayback->periodCount; + pDescriptorPlayback->periodSizeInFrames = bufferSizeInBytes / bpf / pDescriptorPlayback->periodCount; + + /* We need an intermediary buffer for transferring to the SPU buffer. */ + pDeviceStateDreamcast->pIntermediaryBuffer = ma_aligned_malloc(pDescriptorPlayback->periodSizeInFrames * bpf, 32, ma_device_get_allocation_callbacks(pDevice)); + if (pDeviceStateDreamcast->pIntermediaryBuffer == NULL) { + ma_free(pDeviceStateDreamcast, ma_device_get_allocation_callbacks(pDevice)); + return MA_OUT_OF_MEMORY; + } + + MA_ZERO_MEMORY(pDeviceStateDreamcast->pIntermediaryBuffer, pDescriptorPlayback->periodSizeInFrames * bpf); + + /* Now we can allocate our memory for the buffer on the SPU side. */ + pDeviceStateDreamcast->buffer = snd_mem_malloc(bufferSizeInBytes); + if (pDeviceStateDreamcast->buffer == 0) { + ma_free(pDeviceStateDreamcast->pIntermediaryBuffer, ma_device_get_allocation_callbacks(pDevice)); + ma_free(pDeviceStateDreamcast, ma_device_get_allocation_callbacks(pDevice)); + return MA_OUT_OF_MEMORY; + } + + /* We will fill the buffer with initial silence. */ + spu_memset_sq(pDeviceStateDreamcast->buffer, 0, bufferSizeInBytes); /* Can we safely assume the returned value from snd_mem_malloc() is aligned to 32 bytes? */ + + + /* Done. */ + *ppDeviceState = pDeviceStateDreamcast; + + return MA_SUCCESS; +} + +static void ma_device_uninit__dreamcast(ma_device* pDevice) +{ + ma_device_state_dreamcast* pDeviceStateDreamcast = ma_device_get_backend_state__dreamcast(pDevice); + + snd_mem_free(pDeviceStateDreamcast->buffer); + ma_aligned_free(pDeviceStateDreamcast->pIntermediaryBuffer, ma_device_get_allocation_callbacks(pDevice)); + ma_free(pDeviceStateDreamcast, ma_device_get_allocation_callbacks(pDevice)); +} + +static void ma_device_fill_sub_buffer__dreamcast(ma_device* pDevice, int subBufferIndex) +{ + ma_device_state_dreamcast* pDeviceStateDreamcast = ma_device_get_backend_state__dreamcast(pDevice); + ma_uint32 offsetInSamples = 0; + ma_uint32 bpf = ma_get_bytes_per_frame(pDevice->playback.internalFormat, pDevice->playback.internalChannels); + + /* The offset is in samples, not frames. */ + offsetInSamples = subBufferIndex * pDevice->playback.internalPeriodSizeInFrames; + if (pDevice->playback.internalFormat == ma_format_s16) { + offsetInSamples *= sizeof(ma_int16); + } + + /* Do the data callback. */ + ma_device_handle_backend_data_callback(pDevice, pDeviceStateDreamcast->pIntermediaryBuffer, NULL, pDevice->playback.internalPeriodSizeInFrames); + + /* If our format is u8 we want to convert this to signed. */ + if (pDevice->playback.internalFormat == ma_format_u8) { + ma_uint32 sampleCount = pDevice->playback.internalPeriodSizeInFrames * pDevice->playback.internalChannels; + ma_uint32 iSample; + + for (iSample = 0; iSample < sampleCount; iSample += 1) { + ((ma_int8*)pDeviceStateDreamcast->pIntermediaryBuffer)[iSample] = (ma_int8)128 - ((ma_uint8*)pDeviceStateDreamcast->pIntermediaryBuffer)[iSample]; + } + + if (pDevice->playback.internalChannels == 1) { + spu_memload_sq(pDeviceStateDreamcast->buffer + offsetInSamples, pDeviceStateDreamcast->pIntermediaryBuffer, pDevice->playback.internalPeriodSizeInFrames * bpf); + } else { + /* There is no snd_pcm8_split_sq() from what I can tell. Might need to figure that out for myself. */ + ma_uint32 l = pDeviceStateDreamcast->buffer + offsetInSamples; + ma_uint32 r = pDeviceStateDreamcast->buffer + (pDevice->playback.internalPeriodSizeInFrames * pDevice->playback.internalPeriods * bpf) + offsetInSamples; + ma_uint32 framesWritten = 0; + while (framesWritten < pDevice->playback.internalPeriodSizeInFrames) { + ma_uint32 framesToWrite; + ma_int8 tmpl[256] __attribute__((aligned(32))); + ma_int8 tmpr[256] __attribute__((aligned(32))); + void* tmp[2]; + + framesToWrite = pDevice->playback.internalPeriodSizeInFrames - framesWritten; + if (framesToWrite > ma_countof(tmpl)) { + framesToWrite = ma_countof(tmpl); + } + + tmp[0] = tmpl; + tmp[1] = tmpr; + ma_deinterleave_pcm_frames(ma_format_u8, 2, framesToWrite, ma_offset_ptr(pDeviceStateDreamcast->pIntermediaryBuffer, framesWritten * bpf), tmp); + spu_memload_sq(l + (framesWritten * sizeof(ma_int8)), tmpl, framesToWrite * sizeof(ma_int8)); + spu_memload_sq(r + (framesWritten * sizeof(ma_int8)), tmpr, framesToWrite * sizeof(ma_int8)); + + framesWritten += framesToWrite; + } + } + } else { + if (pDevice->playback.internalChannels == 1) { + spu_memload_sq(pDeviceStateDreamcast->buffer + offsetInSamples, pDeviceStateDreamcast->pIntermediaryBuffer, pDevice->playback.internalPeriodSizeInFrames * bpf); + } else { + ma_uint32 l = pDeviceStateDreamcast->buffer + offsetInSamples; + ma_uint32 r = pDeviceStateDreamcast->buffer + (pDevice->playback.internalPeriodSizeInFrames * pDevice->playback.internalPeriods * bpf) + offsetInSamples; + + /* + There is an endian bug in snd_pcm16_split_sq(). It merges two signed 16-bit samples into a single 32-bit variable in preparation + for transfer, but the ordering is the wrong way around. This results in a low quality sound. I'm working around this by splitting + this in one pass and then doing the transfer in a second pass. Later on I would like to update this to do the splitting and copy + in a single pass to save on some data movement. + */ + #if 0 + snd_pcm16_split_sq((uint32_t*)pDeviceStateDreamcast->pIntermediaryBuffer, l, r, pDevice->playback.internalPeriodSizeInFrames * bpf); + #else + ma_uint32 framesWritten = 0; + while (framesWritten < pDevice->playback.internalPeriodSizeInFrames) { + ma_uint32 framesToWrite; + ma_int16 tmpl[256] __attribute__((aligned(32))); + ma_int16 tmpr[256] __attribute__((aligned(32))); + void* tmp[2]; + + framesToWrite = pDevice->playback.internalPeriodSizeInFrames - framesWritten; + if (framesToWrite > ma_countof(tmpl)) { + framesToWrite = ma_countof(tmpl); + } + + tmp[0] = tmpl; + tmp[1] = tmpr; + ma_deinterleave_pcm_frames(ma_format_s16, 2, framesToWrite, ma_offset_ptr(pDeviceStateDreamcast->pIntermediaryBuffer, framesWritten * bpf), tmp); + spu_memload_sq(l + (framesWritten * sizeof(ma_int16)), tmpl, framesToWrite * sizeof(ma_int16)); + spu_memload_sq(r + (framesWritten * sizeof(ma_int16)), tmpr, framesToWrite * sizeof(ma_int16)); + + framesWritten += framesToWrite; + } + #endif + } + } +} + +static ma_result ma_device_start__dreamcast(ma_device* pDevice) +{ + ma_device_state_dreamcast* pDeviceStateDreamcast = ma_device_get_backend_state__dreamcast(pDevice); + AICA_CMDSTR_CHANNEL(tmp, cmd, chan); + + /* Make sure our sub-buffer index is reset or else we'll get a glitch when starting. */ + pDeviceStateDreamcast->subBufferIndex = 1; + + /* + We'll want to fire the data callback and fill our buffer with an initial chunk of data. If we don't do this we'll end + up with one period of silence before hearing any audio. + */ + ma_device_fill_sub_buffer__dreamcast(pDevice, 0); + + /* With the buffer pre-filled we can now start the voices. */ + memset(tmp, 0, sizeof(tmp)); + cmd->cmd = AICA_CMD_CHAN; + cmd->timestamp = 0; + cmd->cmd_id = (uint32_t)pDeviceStateDreamcast->voiceChannel; + cmd->size = AICA_CMDSTR_CHANNEL_SIZE; + chan->cmd = AICA_CH_CMD_START; + chan->base = pDeviceStateDreamcast->buffer; + chan->type = ma_format_to_dreamcast(pDevice->playback.internalFormat); + chan->length = pDevice->playback.internalPeriodSizeInFrames * pDevice->playback.internalPeriods; + chan->loop = 1; + chan->loopstart = 0; + chan->loopend = pDevice->playback.internalPeriodSizeInFrames * pDevice->playback.internalPeriods; + chan->freq = pDevice->playback.internalSampleRate; + chan->vol = 255; /* 0..255 */ + chan->pan = 128; /* 0..255 (0=left, 128=center, 255=right) */ + chan->pos = 0; + + if (pDevice->playback.internalChannels == 1) { + snd_sh4_to_aica(tmp, cmd->size); + } else { + snd_sh4_to_aica_stop(); + { + /* Left. Hard pan to the left. */ + cmd->cmd_id = (uint32_t)pDeviceStateDreamcast->voiceChannel + 0; + chan->base = pDeviceStateDreamcast->buffer + 0; + chan->pan = 0; + snd_sh4_to_aica(tmp, cmd->size); + + /* Right. Hard pan to the right. */ + cmd->cmd_id = (uint32_t)pDeviceStateDreamcast->voiceChannel + 1; + chan->base = pDeviceStateDreamcast->buffer + (pDevice->playback.internalPeriodSizeInFrames * pDevice->playback.internalPeriods * ma_get_bytes_per_frame(pDevice->playback.internalFormat, pDevice->playback.internalChannels)); + chan->pan = 255; + snd_sh4_to_aica(tmp, cmd->size); + } + snd_sh4_to_aica_start(); + } + + return MA_SUCCESS; +} + +static ma_result ma_device_stop__dreamcast(ma_device* pDevice) +{ + ma_device_state_dreamcast* pDeviceStateDreamcast = ma_device_get_backend_state__dreamcast(pDevice); + AICA_CMDSTR_CHANNEL(tmp, cmd, chan); + + memset(tmp, 0, sizeof(tmp)); + + cmd->cmd = AICA_CMD_CHAN; + cmd->timestamp = 0; + cmd->cmd_id = (uint32_t)pDeviceStateDreamcast->voiceChannel; + cmd->size = AICA_CMDSTR_CHANNEL_SIZE; + chan->cmd = AICA_CH_CMD_STOP; + + if (pDevice->playback.internalChannels == 1) { + snd_sh4_to_aica(tmp, cmd->size); + } else { + snd_sh4_to_aica_stop(); + { + /* Left. */ + cmd->cmd_id = (uint32_t)pDeviceStateDreamcast->voiceChannel + 0; + snd_sh4_to_aica(tmp, cmd->size); + + /* Right. */ + cmd->cmd_id = (uint32_t)pDeviceStateDreamcast->voiceChannel + 1; + snd_sh4_to_aica(tmp, cmd->size); + } + snd_sh4_to_aica_start(); + } + + return MA_SUCCESS; +} + +static ma_result ma_device_step__dreamcast(ma_device* pDevice, ma_blocking_mode blockingMode) +{ + ma_device_state_dreamcast* pDeviceStateDreamcast = ma_device_get_backend_state__dreamcast(pDevice); + + /* Keep looping until we have processed some data, or we're in non-blocking mode. */ + for (;;) { + int pos; + ma_int8 currentSubBufferIndex; + ma_bool32 wasDataProcessed = MA_FALSE; + + if (!ma_device_is_started(pDevice)) { + return MA_DEVICE_NOT_STARTED; + } + + /* We're currently using a poll/sleep pattern. Should try figuring out a better way to do this later. */ + pos = (int)snd_get_pos(pDeviceStateDreamcast->voiceChannel); + if (pos < pDevice->playback.internalPeriodSizeInFrames) { + currentSubBufferIndex = 0; + } else { + currentSubBufferIndex = 1; + } + + if (pDeviceStateDreamcast->subBufferIndex != currentSubBufferIndex) { + ma_device_fill_sub_buffer__dreamcast(pDevice, pDeviceStateDreamcast->subBufferIndex); + pDeviceStateDreamcast->subBufferIndex = currentSubBufferIndex; + + wasDataProcessed = MA_TRUE; + } + + if (wasDataProcessed) { + break; + } + + /* Getting here means there is no data to process. In non blocking mode we just bomb out. In blocking mode we wait a bit. */ + if (blockingMode == MA_BLOCKING_MODE_NON_BLOCKING) { + break; + } + + /* Getting here means we're in blocking mode. */ + ma_sleep(1); + } + + return MA_SUCCESS; +} + +static void ma_device_wakeup__dreamcast(ma_device* pDevice) +{ + ma_device_state_dreamcast* pDeviceStateDreamcast = ma_device_get_backend_state__dreamcast(pDevice); + + /* Nothing to do. */ + (void)pDeviceStateDreamcast; +} + +static ma_device_backend_vtable ma_gDeviceBackendVTable_Dreamcast = +{ + ma_backend_info__dreamcast, + ma_context_init__dreamcast, + ma_context_uninit__dreamcast, + ma_context_enumerate_devices__dreamcast, + ma_device_init__dreamcast, + ma_device_uninit__dreamcast, + ma_device_start__dreamcast, + ma_device_stop__dreamcast, + ma_device_step__dreamcast, + ma_device_wakeup__dreamcast +}; + +ma_device_backend_vtable* ma_device_backend_dreamcast = &ma_gDeviceBackendVTable_Dreamcast; +#else +ma_device_backend_vtable* ma_device_backend_dreamcast = NULL; +#endif /* MA_HAS_DREAMCAST */ + + +MA_API ma_device_backend_vtable* ma_dreamcast_get_vtable(void) +{ + return ma_device_backend_dreamcast; +} + +MA_API ma_context_config_dreamcast ma_context_config_dreamcast_init(void) +{ + ma_context_config_dreamcast config; + + MA_ZERO_OBJECT(&config); + + return config; +} + +MA_API ma_device_config_dreamcast ma_device_config_dreamcast_init(int voiceChannel) +{ + ma_device_config_dreamcast config; + + MA_ZERO_OBJECT(&config); + config.voiceChannel = voiceChannel; + + return config; +} +/* END miniaudio_dreamcast.c */ + + + MA_API void ma_get_device_backend_info(ma_device_backend_vtable* pBackendVTable, ma_device_backend_info* pBackendInfo) { if (pBackendVTable == NULL || pBackendVTable->onBackendInfo == NULL || pBackendInfo == NULL) { @@ -48432,6 +49021,9 @@ static const void* ma_context_config_find_backend_config(const ma_context_config if (pVTable == ma_device_backend_webaudio) { return &pConfig->webaudio; } + if (pVTable == ma_device_backend_dreamcast) { + return &pConfig->dreamcast; + } if (pVTable == ma_device_backend_null) { return &pConfig->null_backend; } @@ -48461,6 +49053,7 @@ MA_API ma_uint32 ma_get_stock_device_backends(ma_device_backend_config* pBackend if (backendsCap > count) { pBackends[count++] = ma_device_backend_config_init(ma_device_backend_aaudio, NULL); } if (backendsCap > count) { pBackends[count++] = ma_device_backend_config_init(ma_device_backend_opensl, NULL); } if (backendsCap > count) { pBackends[count++] = ma_device_backend_config_init(ma_device_backend_webaudio, NULL); } + if (backendsCap > count) { pBackends[count++] = ma_device_backend_config_init(ma_device_backend_dreamcast, NULL); } if (backendsCap > count) { pBackends[count++] = ma_device_backend_config_init(ma_device_backend_null, NULL); } return count; @@ -48926,6 +49519,9 @@ static const void* ma_device_config_find_backend_config(const ma_device_config* if (pVTable == ma_device_backend_webaudio) { return &pConfig->webaudio; } + if (pVTable == ma_device_backend_dreamcast) { + return &pConfig->dreamcast; + } if (pVTable == ma_device_backend_null) { return &pConfig->null_backend; } diff --git a/tests/dreamcast/Makefile b/tests/dreamcast/Makefile new file mode 100644 index 00000000..9fa00141 --- /dev/null +++ b/tests/dreamcast/Makefile @@ -0,0 +1,22 @@ +TARGET = miniaudio.elf +OBJS = miniaudio_dreamcast_test.o romdisk.o +KOS_ROMDISK_DIR = romdisk + +all: clean $(TARGET) + +include $(KOS_BASE)/Makefile.rules + +clean: + -rm -f $(TARGET) $(OBJS) + -rm -f romdisk.o romdisk.img + +$(TARGET): $(OBJS) + kos-cc -o $(TARGET) $(OBJS) -lpthread + +run: $(TARGET) + $(KOS_LOADER) $(TARGET) + +dist: $(TARGET) + rm -f $(OBJS) romdisk.o romdisk.img + $(KOS_STRIP) $(TARGET) + diff --git a/tests/dreamcast/kos_environ.sh b/tests/dreamcast/kos_environ.sh new file mode 100644 index 00000000..08fa2c3f --- /dev/null +++ b/tests/dreamcast/kos_environ.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +#set the KOS environtment variables +source /opt/toolchains/dc/kos/environ.sh \ No newline at end of file diff --git a/tests/dreamcast/kos_make.sh b/tests/dreamcast/kos_make.sh new file mode 100644 index 00000000..983cc98a --- /dev/null +++ b/tests/dreamcast/kos_make.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +#set the KOS environtment variables +source /opt/toolchains/dc/kos/environ.sh + +make + +exit \ No newline at end of file diff --git a/tests/dreamcast/miniaudio_dreamcast_test.c b/tests/dreamcast/miniaudio_dreamcast_test.c new file mode 100644 index 00000000..40ea1086 --- /dev/null +++ b/tests/dreamcast/miniaudio_dreamcast_test.c @@ -0,0 +1,186 @@ +//#define MA_NO_THREADING +#include "../../miniaudio.c" + +ma_context context; +ma_device device; +ma_waveform waveform; +ma_decoder decoder; + +typedef struct allocation_callbacks_data +{ + ma_uint32 allocationCount; + ma_uint32 totalAllocationSizeInBytes; +} allocation_callbacks_data; + +static void* test_malloc(size_t sz, void* pUserData) +{ + allocation_callbacks_data* pData = (allocation_callbacks_data*)pUserData; + + printf("malloc(%d)\n", (int)sz); + + pData->allocationCount += 1; + pData->totalAllocationSizeInBytes += sz; + return malloc(sz); +} + +static void* test_realloc(void* p, size_t sz, void* pUserData) +{ + allocation_callbacks_data* pData = (allocation_callbacks_data*)pUserData; + + printf("realloc(%d)\n", (int)sz); + + pData->totalAllocationSizeInBytes += sz; /* Don't actually know how to properly track this. Might need to keep a list of allocations. */ + return realloc(p, sz); +} + +static void test_free(void* p, void* pUserData) +{ + allocation_callbacks_data* pData = (allocation_callbacks_data*)pUserData; + + printf("free()\n"); + + pData->allocationCount -= 1; + free(p); +} + +allocation_callbacks_data allocationCallbacksData; + + +static void data_callback(ma_device* pDevice, void* pFramesOut, const void* pFramesIn, ma_uint32 frameCount) +{ + (void)pDevice; + (void)pFramesOut; + (void)pFramesIn; + (void)frameCount; + + //printf("data_callback(%d)\n", (int)frameCount); + //printf("internalePeriodSizeInFrames = %d\n", device.playback.internalPeriodSizeInFrames); + + //ma_waveform_read_pcm_frames(&waveform, pFramesOut, frameCount, NULL); + ma_decoder_read_pcm_frames(&decoder, pFramesOut, frameCount, NULL); + + // Just applying a volume factor to make testing a bit more bearable... + ma_apply_volume_factor_pcm_frames(pFramesOut, frameCount, ma_device_get_format(pDevice, ma_device_type_playback), ma_device_get_channels(pDevice, ma_device_type_playback), 0.2f); +} + +int main() +{ + ma_result result; + ma_context_config contextConfig; + ma_device_config deviceConfig; + uint32 prevButtons = 0; + + dbgio_init(); + dbgio_dev_select("fb"); + + + vid_init(DEFAULT_VID_MODE, PM_RGB565); + vid_clear(64, 192, 64); + + bfont_draw_str(vram_s + 20 * 640 + 20, 640, 0, "MINIAUDIO"); + + printf("sizeof(ma_context) = %d\n", (int)sizeof(ma_context)); + printf("sizeof(ma_device) = %d\n", (int)sizeof(ma_device)); + printf("sizeof(ma_device.playback) = %d\n", (int)sizeof(device.playback)); + printf("sizeof(ma_data_converter) = %d\n", (int)sizeof(ma_data_converter)); + + + memset(&allocationCallbacksData, 0, sizeof(allocationCallbacksData)); + + contextConfig = ma_context_config_init(); + contextConfig.allocationCallbacks.onMalloc = test_malloc; + contextConfig.allocationCallbacks.onRealloc = test_realloc; + contextConfig.allocationCallbacks.onFree = test_free; + contextConfig.allocationCallbacks.pUserData = &allocationCallbacksData; + + result = ma_context_init(NULL, 0, &contextConfig, &context); + if (result != MA_SUCCESS) { + printf("ma_context_init() failed."); + } + + deviceConfig = ma_device_config_init(ma_device_type_playback); + deviceConfig.threadingMode = MA_THREADING_MODE_SINGLE_THREADED; + deviceConfig.playback.format = ma_format_s16; + deviceConfig.playback.channels = 2; + deviceConfig.sampleRate = 44100; + deviceConfig.periodSizeInFrames = 2048; + deviceConfig.noFixedSizedCallback = MA_TRUE; /* With the period size between 2048 and 16384 we can be guaranteed a fixed sized callback. */ + deviceConfig.dataCallback = data_callback; + deviceConfig.pUserData = NULL; + result = ma_device_init(&context, &deviceConfig, &device); + if (result != MA_SUCCESS) { + printf("ma_device_init() failed.\n"); + } + + printf("internalPeriodSizeInFrames = %d\n", device.playback.internalPeriodSizeInFrames); + printf("format: %d %d\n", device.playback.format, device.playback.internalFormat); + printf("channels: %d %d\n", device.playback.channels, device.playback.internalChannels); + printf("rate: %d %d\n", device.sampleRate, device.playback.internalSampleRate); + + /* Waveform. */ + { + ma_waveform_config waveformConfig = ma_waveform_config_init(device.playback.format, device.playback.channels, device.sampleRate, ma_waveform_type_sine, 0.1f, 400); + ma_waveform_init(&waveformConfig, &waveform); + } + + /* Decoder. */ + { + ma_decoder_config decoderConfig = ma_decoder_config_init(device.playback.format, device.playback.channels, 0); + ma_decoder_init_file("/rd/test.mp3", &decoderConfig, &decoder); + } + + printf("Allocation Count = %d\n", (int)allocationCallbacksData.allocationCount); + printf("Allocation Size = %d\n", (int)allocationCallbacksData.totalAllocationSizeInBytes); + + result = ma_device_start(&device); + if (result != MA_SUCCESS) { + printf("ma_device_start() failed.\n"); + } + + thd_sleep(1); + + for (;;) { + maple_device_t* dev; + cont_state_t* st; + + dev = maple_enum_type(0, MAPLE_FUNC_CONTROLLER); /* 0 = first controller found */ + if (dev) { + st = (cont_state_t*)maple_dev_status(dev); + if (st) { + uint32 buttons; + uint32 pressed; + uint32 released; + + buttons = st->buttons; /* bitmask of CONT_* */ + pressed = (buttons ^ prevButtons) & buttons; + released = (buttons ^ prevButtons) & prevButtons; + prevButtons = buttons; + + (void)released; + + if (pressed & CONT_START) { + break; + } + + if (buttons & CONT_DPAD_LEFT) { + /* Held */ + } + } + } + + ma_device_step(&device, MA_BLOCKING_MODE_NON_BLOCKING); + + thd_pass(); /* yield */ + //thd_sleep(1); + } + + result = ma_device_stop(&device); + if (result != MA_SUCCESS) { + printf("ma_device_start() failed.\n"); + } + + ma_device_uninit(&device); + ma_context_uninit(&context); + + return 0; +}