libgui 2.01
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
WutAudioDriver.cpp
1/****************************************************************************
2 * Platform Abstraction Layer (WUT driver)
3 * Daryl Borth 2026
4 * WutAudioDriver.cpp
5 ***************************************************************************/
6#include <coreinit/cache.h>
7#include <string.h>
8#include <unistd.h>
9#include <proc_ui/procui.h>
10
11#include "../Platform.h"
12#include "WutAudioDriver.h"
13
14static WutAudioDriver *instance = nullptr;
15
16// Once the OS has taken the foreground away from us (HOME menu overlay,
17// forced exit, etc.) we stop feeding/starting audio rather than continuing
18// to drive AX in the background.
19static inline bool isForeground() { return platform->getStatus() == Status::Running; }
20
21static void wut_frame_callback() {
22 if (instance)
23 instance->handleStreamCallback();
24}
25
26template <int N>
27static void buildChannelMix(AXVoiceDeviceMixData (&mix)[N], bool left, bool right) {
28 memset(mix, 0, sizeof(mix));
29 if (left)
30 mix[0].bus[0].volume = 0x8000;
31 if (right && N > 1)
32 mix[1].bus[0].volume = 0x8000;
33}
34
35static constexpr int AX_TV_CHANNELS = 6;
36static constexpr int AX_DRC_CHANNELS = 4;
37
39 instance = this;
40 AXInitParams params = {};
41 params.renderer = AX_INIT_RENDERER_48KHZ;
42 params.pipeline = AX_INIT_PIPELINE_SINGLE;
43 AXInitWithParams(&params);
44 AXRegisterAppFrameCallback(wut_frame_callback);
45
46 nextVoiceSlot = 0;
47
48 // Pre-allocate the 16 hardware SFX voices - mono, centered equally on
49 // both output channels of both devices.
50 AXVoiceDeviceMixData tvMixCentered[AX_TV_CHANNELS];
51 AXVoiceDeviceMixData drcMixCentered[AX_DRC_CHANNELS];
52 buildChannelMix(tvMixCentered, true, true);
53 buildChannelMix(drcMixCentered, true, true);
54
55 for (int i = 0; i < 16; i++) {
56 voices[i].voice = AXAcquireVoice(31, 0, 0);
57 voices[i].active = false;
58 if (voices[i].voice) {
59 AXVoiceBegin(voices[i].voice);
60 AXSetVoiceType(voices[i].voice, 0);
61
62 AXSetVoiceDeviceMix(voices[i].voice, AX_DEVICE_TYPE_TV, 0, tvMixCentered);
63 AXSetVoiceDeviceMix(voices[i].voice, AX_DEVICE_TYPE_DRC, 0, drcMixCentered);
64 AXVoiceEnd(voices[i].voice);
65 }
66 }
67
68 streamVoiceL = AXAcquireVoice(31, 0, 0);
69 streamVoiceR = AXAcquireVoice(31, 0, 0);
70
71 if (streamVoiceL && streamVoiceR) {
72 AXVoiceDeviceMixData tvMixL[AX_TV_CHANNELS], drcMixL[AX_DRC_CHANNELS];
73 AXVoiceDeviceMixData tvMixR[AX_TV_CHANNELS], drcMixR[AX_DRC_CHANNELS];
74 buildChannelMix(tvMixL, true, false); // Hard-pan Left
75 buildChannelMix(drcMixL, true, false);
76 buildChannelMix(tvMixR, false, true); // Hard-pan Right
77 buildChannelMix(drcMixR, false, true);
78
79 AXVoiceBegin(streamVoiceL);
80 AXSetVoiceType(streamVoiceL, 0);
81 AXSetVoiceDeviceMix(streamVoiceL, AX_DEVICE_TYPE_TV, 0, tvMixL);
82 AXSetVoiceDeviceMix(streamVoiceL, AX_DEVICE_TYPE_DRC, 0, drcMixL);
83 AXVoiceEnd(streamVoiceL);
84
85 AXVoiceBegin(streamVoiceR);
86 AXSetVoiceType(streamVoiceR, 0);
87 AXSetVoiceDeviceMix(streamVoiceR, AX_DEVICE_TYPE_TV, 0, tvMixR);
88 AXSetVoiceDeviceMix(streamVoiceR, AX_DEVICE_TYPE_DRC, 0, drcMixR);
89 AXVoiceEnd(streamVoiceR);
90 }
91
92 streamVolume = 255;
93 writeOffset = 0;
94 eofSilenceWritten = false;
95}
96
98
99}
100
102
103}
104
106 stopStream();
107 AXDeregisterAppFrameCallback(wut_frame_callback);
108
109 for (int i = 0; i < 16; i++) {
110 if (voices[i].voice) {
111 AXFreeVoice(voices[i].voice);
112 voices[i].voice = nullptr;
113 }
114 }
115
116 if (streamVoiceL) { AXFreeVoice(streamVoiceL); streamVoiceL = nullptr; }
117 if (streamVoiceR) { AXFreeVoice(streamVoiceR); streamVoiceR = nullptr; }
118
119 AXQuit();
120 instance = nullptr;
121}
122
123int32_t WutAudioDriver::playVoice(const uint8_t *data, int32_t length, int volume) {
124 if (!isForeground())
125 return -1;
126
127 int voiceIdx = nextVoiceSlot;
128 nextVoiceSlot = (nextVoiceSlot + 1) % 16;
129
130 WutVoiceSlot &slot = voices[voiceIdx];
131 if (!slot.voice)
132 return -1;
133
134 AXSetVoiceState(slot.voice, 0); // Stop current if active
135 slot.active = true;
136
137 AXVoiceOffsets offsets;
138 memset(&offsets, 0, sizeof(offsets));
139 offsets.data = data;
140 offsets.dataType = AX_VOICE_FORMAT_LPCM16;
141 offsets.loopingEnabled = AX_VOICE_LOOP_DISABLED;
142 offsets.endOffset = length / 2; // Conversion: bytes to 16-bit samples
143 AXSetVoiceOffsets(slot.voice, &offsets);
144
145 AXVoiceSrc src;
146 memset(&src, 0, sizeof(src));
147 uint32_t samplesPerSec = AXGetInputSamplesPerSec();
148 src.ratio = (uint32_t)(0x00010000 * ((float) 48000 / (float) samplesPerSec));
149 AXSetVoiceSrc(slot.voice, &src);
150 AXSetVoiceSrcType(slot.voice, (src.ratio == 0x00010000) ? 0 : 1); // SRC bypass check
151
152 AXVoiceVeData veData;
153 veData.volume = (volume << 7); // Remap 0-255 to ~0-0x8000
154 veData.delta = 0;
155 AXSetVoiceVe(slot.voice, &veData);
156
157 AXSetVoiceState(slot.voice, 1);
158 return voiceIdx;
159}
160
161void WutAudioDriver::stopVoice(int32_t voice) {
162 if (voice >= 0 && voice < 16 && voices[voice].voice) {
163 AXSetVoiceState(voices[voice].voice, 0);
164 voices[voice].active = false;
165 }
166}
167
168void WutAudioDriver::pauseVoice(int32_t voice) {
169 if (voice >= 0 && voice < 16 && voices[voice].voice)
170 AXSetVoiceState(voices[voice].voice, 0);
171}
172
173void WutAudioDriver::resumeVoice(int32_t voice) {
174 if (voice >= 0 && voice < 16 && voices[voice].voice)
175 AXSetVoiceState(voices[voice].voice, 1);
176}
177
179 if (voice >= 0 && voice < 16 && voices[voice].voice)
180 return voices[voice].active && (voices[voice].voice->state == AX_VOICE_STATE_PLAYING);
181 return false;
182}
183
184void WutAudioDriver::setVoiceVolume(int32_t voice, int volume) {
185 if (voice >= 0 && voice < 16 && voices[voice].voice) {
186 AXVoiceVeData veData;
187 veData.volume = (volume << 7);
188 veData.delta = 0;
189 AXSetVoiceVe(voices[voice].voice, &veData);
190 }
191}
192
193void WutAudioDriver::playStream(const uint8_t *data, int32_t length, bool loop, int volume) {
194 if (!isForeground())
195 return;
196
197 stopStream();
198 streamVolume = volume;
199
200 memset(streamBufL, 0, sizeof(streamBufL));
201 memset(streamBufR, 0, sizeof(streamBufR));
202 DCFlushRange(streamBufL, sizeof(streamBufL));
203 DCFlushRange(streamBufR, sizeof(streamBufR));
204
205 writeOffset = 0;
206 eofSilenceWritten = false;
207
208 if (oggPlayer.play(data, length, 0, loop)) {
209 int32_t readySize = 0;
210 const uint8_t *pcm = nullptr;
211
212 while (oggPlayer.isPlaying() && !(pcm = oggPlayer.getReadyBuffer(&readySize))) {
213 usleep(1000);
214 }
215
216 if (pcm && readySize > 0) {
217 int samples = readySize / (oggPlayer.getChannels() == 2 ? 4 : 2);
218 int16_t *pcm16 = (int16_t*) pcm;
219
220 if (oggPlayer.getChannels() == 2) {
221 for (int i = 0; i < samples; i++) {
222 streamBufL[i] = pcm16[i * 2];
223 streamBufR[i] = pcm16[i * 2 + 1];
224 }
225 } else {
226 for (int i = 0; i < samples; i++) {
227 streamBufL[i] = pcm16[i];
228 streamBufR[i] = pcm16[i];
229 }
230 }
231
232 DCFlushRange(streamBufL, samples * 2);
233 DCFlushRange(streamBufR, samples * 2);
234 writeOffset = samples;
235 oggPlayer.consumeBuffer();
236
237 AXSetVoiceState(streamVoiceL, 0);
238 AXSetVoiceState(streamVoiceR, 0);
239
240 AXVoiceOffsets offsetsL, offsetsR;
241 memset(&offsetsL, 0, sizeof(offsetsL));
242 offsetsL.data = streamBufL;
243 offsetsL.dataType = AX_VOICE_FORMAT_LPCM16;
244 offsetsL.loopingEnabled = AX_VOICE_LOOP_ENABLED;
245 offsetsL.loopOffset = 0;
246 offsetsL.endOffset = STREAM_BUFFER_SAMPLES - 1;
247 AXSetVoiceOffsets(streamVoiceL, &offsetsL);
248
249 offsetsR = offsetsL;
250 offsetsR.data = streamBufR;
251 AXSetVoiceOffsets(streamVoiceR, &offsetsR);
252
253 AXVoiceSrc src;
254 memset(&src, 0, sizeof(src));
255 uint32_t samplesPerSec = AXGetInputSamplesPerSec();
256 src.ratio = (uint32_t)(0x00010000 * ((float) oggPlayer.getSampleRate() / (float) samplesPerSec));
257 AXSetVoiceSrc(streamVoiceL, &src);
258 AXSetVoiceSrc(streamVoiceR, &src);
259
260 uint16_t srcType = (src.ratio == 0x00010000) ? 0 : 1;
261 AXSetVoiceSrcType(streamVoiceL, srcType);
262 AXSetVoiceSrcType(streamVoiceR, srcType);
263
264 AXVoiceVeData veData;
265 veData.volume = (streamVolume << 7);
266 veData.delta = 0;
267 AXSetVoiceVe(streamVoiceL, &veData);
268 AXSetVoiceVe(streamVoiceR, &veData);
269
270 AXSetVoiceState(streamVoiceL, 1);
271 AXSetVoiceState(streamVoiceR, 1);
272 }
273 }
274}
275
277 if (!isForeground()) {
278 // Lost the foreground - stop driving the stream voices rather than
279 // continuing to feed/play audio in the background.
280 pauseStream();
281 return;
282 }
283
284 if (!streamVoiceL || !streamVoiceR || oggPlayer.isPaused() || streamVoiceL->state != AX_VOICE_STATE_PLAYING)
285 return;
286
287 // Query exactly where the hardware is right now. streamBuf is the required base pointer.
288 uint32_t currentOffset = AXGetVoiceCurrentOffsetEx(streamVoiceL, streamBufL);
289
290 // Calculate how many valid, unplayed samples are queued ahead of the hardware playhead
291 uint32_t queuedSamples = (writeOffset - currentOffset + STREAM_BUFFER_SAMPLES) % STREAM_BUFFER_SAMPLES;
292
293 if (queuedSamples < (STREAM_BUFFER_SAMPLES / 2)) {
294 int32_t readySize = 0;
295 const uint8_t *pcm = oggPlayer.getReadyBuffer(&readySize);
296
297 if (pcm && readySize > 0) {
298 int samples = readySize / (oggPlayer.getChannels() == 2 ? 4 : 2);
299
300 // Handle physical wrap-around if the new chunk crosses the end of streamBuf
301 int samplesToEnd = STREAM_BUFFER_SAMPLES - writeOffset;
302 int write1 = (samples < samplesToEnd) ? samples : samplesToEnd;
303 int write2 = samples - write1;
304 int16_t *pcm16 = (int16_t*) pcm;
305
306 auto copySamples = [&](int dest, int src, int count) {
307 if (oggPlayer.getChannels() == 2) {
308 for (int i = 0; i < count; i++) {
309 streamBufL[dest + i] = pcm16[(src + i) * 2];
310 streamBufR[dest + i] = pcm16[(src + i) * 2 + 1];
311 }
312 } else {
313 for (int i = 0; i < count; i++) {
314 streamBufL[dest + i] = pcm16[src + i];
315 streamBufR[dest + i] = pcm16[src + i];
316 }
317 }
318 };
319
320 if (write1 > 0) copySamples(writeOffset, 0, write1);
321 if (write2 > 0) copySamples(0, write1, write2);
322
323 if (write1 > 0) {
324 DCFlushRange(&streamBufL[writeOffset], write1 * 2);
325 DCFlushRange(&streamBufR[writeOffset], write1 * 2);
326 }
327 if (write2 > 0) {
328 DCFlushRange(&streamBufL[0], write2 * 2);
329 DCFlushRange(&streamBufR[0], write2 * 2);
330 }
331
332 writeOffset = (writeOffset + samples) % STREAM_BUFFER_SAMPLES;
333 oggPlayer.consumeBuffer();
334
335 } else if (!oggPlayer.isPlaying()) {
336
337 // Natural End-Of-File reached. Scrub the unwritten remainder of the buffer with silence
338 // so the DSP doesn't play old loop garbage before halting.
339 if (!eofSilenceWritten) {
340 uint32_t clearCount = STREAM_BUFFER_SAMPLES - queuedSamples;
341 if (clearCount > 0) {
342 uint32_t clear1 = (clearCount < (STREAM_BUFFER_SAMPLES - writeOffset)) ? clearCount : (STREAM_BUFFER_SAMPLES - writeOffset);
343 uint32_t clear2 = clearCount - clear1;
344
345 memset(&streamBufL[writeOffset], 0, clear1 * 2);
346 memset(&streamBufR[writeOffset], 0, clear1 * 2);
347 DCFlushRange(&streamBufL[writeOffset], clear1 * 2);
348 DCFlushRange(&streamBufR[writeOffset], clear1 * 2);
349
350 if (clear2 > 0) {
351 memset(&streamBufL[0], 0, clear2 * 2);
352 memset(&streamBufR[0], 0, clear2 * 2);
353 DCFlushRange(&streamBufL[0], clear2 * 2);
354 DCFlushRange(&streamBufR[0], clear2 * 2);
355 }
356 }
357 eofSilenceWritten = true;
358 }
359
360 // Once the hardware finishes playing the very last valid samples, halt it.
361 if (queuedSamples < 128) {
362 AXSetVoiceState(streamVoiceL, 0);
363 AXSetVoiceState(streamVoiceR, 0);
364 }
365 }
366 }
367}
368
370 oggPlayer.stop();
371 if (streamVoiceL) AXSetVoiceState(streamVoiceL, 0);
372 if (streamVoiceR) AXSetVoiceState(streamVoiceR, 0);
373}
374
376 oggPlayer.pause(true);
377 if (streamVoiceL) AXSetVoiceState(streamVoiceL, 0);
378 if (streamVoiceR) AXSetVoiceState(streamVoiceR, 0);
379}
380
382 if (streamVoiceL) AXSetVoiceState(streamVoiceL, 1);
383 if (streamVoiceR) AXSetVoiceState(streamVoiceR, 1);
384 oggPlayer.pause(false);
385}
386
388 return oggPlayer.isPlaying();
389}
390
392 streamVolume = volume;
393 if (streamVoiceL && streamVoiceR) {
394 AXVoiceVeData veData;
395 veData.volume = (volume << 7);
396 veData.delta = 0;
397 AXSetVoiceVe(streamVoiceL, &veData);
398 AXSetVoiceVe(streamVoiceR, &veData);
399 }
400}
Wii U AudioDriver: AX (sndcore2), 16 fixed AXVoice slots for one-shots plus a dedicated stereo stream...
void pauseStream() override
Pauses the stream, keeping its position.
void init() override
Allocates the backend's audio resources. Does not start playback; see start().
void resumeVoice(int32_t voice) override
Resumes a voice paused with pauseVoice().
void stopVoice(int32_t voice) override
Stops a voice returned by playVoice() and frees its slot.
void resumeStream() override
Resumes a stream paused with pauseStream().
void shutdown() override
Releases everything init() allocated. Call stop() first.
void stopStream() override
Stops the stream started by playStream().
void start() override
Starts the audio backend running (eg. registers the DSP/AX callback).
void setVoiceVolume(int32_t voice, int volume) override
Changes the volume of a playing voice.
int32_t playVoice(const uint8_t *data, int32_t length, int volume) override
Start a one-shot/short PCM voice.
void stop() override
Stops the audio backend.
void setStreamVolume(int volume) override
Changes the stream volume.
bool isStreamPlaying() override
bool isVoicePlaying(int32_t voice) override
void handleStreamCallback()
AX frame callback hook that refills the stream ring buffers.
void pauseVoice(int32_t voice) override
Pauses a playing voice, keeping its position and slot.
void playStream(const uint8_t *data, int32_t length, bool loop, int volume) override
Streamed (OGG) playback.