1 module gbaid.audio; 2 3 import core.sync.mutex : Mutex; 4 import core.sync.condition : Condition; 5 6 import std.meta : aliasSeqOf; 7 import std.range : iota; 8 import std.algorithm.comparison : min; 9 10 import derelict.sdl2.sdl; 11 12 import gbaid.util; 13 14 public class AudioQueue(uint channelCount) { 15 private enum uint DEVICE_SAMPLES = 1024 * channelCount; 16 private enum size_t SAMPLE_BUFFER_LENGTH = DEVICE_SAMPLES * 4; 17 private SDL_AudioDeviceID device = 0; 18 private short[SAMPLE_BUFFER_LENGTH] samples; 19 private size_t sampleIndex = 0; 20 private size_t sampleCount = 0; 21 mixin declareFields!(LowPassFilter, true, "filter", LowPassFilter.init, channelCount); 22 private uint frequency; 23 private bool filterEnabled; 24 private Condition sampleSignal; 25 26 public this(uint frequency, bool filterEnabled = false) { 27 this.frequency = frequency; 28 this.filterEnabled = filterEnabled; 29 sampleSignal = new Condition(new Mutex()); 30 } 31 32 public void create() { 33 if (device != 0) { 34 return; 35 } 36 37 if (!SDL_WasInit(SDL_INIT_AUDIO)) { 38 if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { 39 throw new Exception("Failed to initialize SDL audio sytem: " ~ toDString(SDL_GetError())); 40 } 41 } 42 43 SDL_AudioSpec spec; 44 spec.freq = frequency; 45 spec.format = AUDIO_S16; 46 spec.channels = channelCount; 47 spec.samples = DEVICE_SAMPLES; 48 spec.callback = &callback!channelCount; 49 spec.userdata = cast(void*) this; 50 device = SDL_OpenAudioDevice(null, 0, &spec, null, 0); 51 if (!device) { 52 throw new Exception("Failed to open audio device: " ~ toDString(SDL_GetError())); 53 } 54 } 55 56 public void destroy() { 57 if (device == 0) { 58 return; 59 } 60 SDL_CloseAudioDevice(device); 61 } 62 63 public void pause() { 64 SDL_PauseAudioDevice(device, true); 65 } 66 67 public void resume() { 68 SDL_PauseAudioDevice(device, false); 69 } 70 71 public void queueAudio(short[] newSamples) { 72 if (newSamples.length <= 0) { 73 return; 74 } 75 synchronized (sampleSignal.mutex) { 76 // Limit the length to copy to the free space 77 auto length = min(SAMPLE_BUFFER_LENGTH - sampleCount, newSamples.length); 78 if (length <= 0) { 79 return; 80 } 81 // Filter the samples 82 if (filterEnabled) { 83 for (size_t i = 0; i < length; i += channelCount) { 84 // Each channel has its own filter 85 foreach (j; aliasSeqOf!(iota(0, channelCount))) { 86 newSamples[i + j] = filter!j.next(newSamples[i + j]); 87 } 88 } 89 } 90 // Copy the first part to the circular buffer 91 auto start = (sampleIndex + sampleCount) % SAMPLE_BUFFER_LENGTH; 92 auto end = min(start + length, SAMPLE_BUFFER_LENGTH); 93 auto copyLength = end - start; 94 samples[start .. end] = newSamples[0 .. copyLength]; 95 // Copy the wrapped around part 96 start = 0; 97 end = length - copyLength; 98 samples[start .. end] = newSamples[copyLength .. length]; 99 // Increment the sample count by the copied length 100 sampleCount += length; 101 } 102 } 103 104 public size_t nextRequiredSamples() { 105 synchronized (sampleSignal.mutex) { 106 size_t requiredSamples = void; 107 while ((requiredSamples = SAMPLE_BUFFER_LENGTH - sampleCount) <= 0) { 108 sampleSignal.wait(); 109 } 110 return requiredSamples / channelCount; 111 } 112 } 113 } 114 115 private struct LowPassFilter { 116 private static enum GAIN = 4.675473023e1f; 117 private float x0 = 0, x1 = 0, x2 = 0, x3 = 0; 118 private float y0 = 0, y1 = 0, y2 = 0, y3 = 0; 119 120 private short next(short sample) { 121 // Low-pass third-order Butterworth filter with a 7kHz cutoff 122 // Generated with: http://www-users.cs.york.ac.uk/~fisher/mkfilter/trad.html 123 x0 = x1; x1 = x2; x2 = x3; 124 x3 = sample / GAIN; 125 y0 = y1; y1 = y2; y2 = y3; 126 y3 = x0 + x3 + 3 * (x1 + x2) + 0.2538063624f * y0 127 - 1.1025360056f * y1 + 1.6776239613f * y2; 128 return cast(short) (y3 + 0.5f); 129 } 130 } 131 132 private extern(C) void callback(uint channelCount)(void* instance, ubyte* stream, int length) nothrow { 133 alias Audio = AudioQueue!channelCount; 134 auto audio = cast(Audio) instance; 135 auto sampleBytes = cast(ubyte*) audio.samples.ptr; 136 try { 137 synchronized (audio.sampleSignal.mutex) { 138 // Limit the length to copy to the available samples 139 length = min(length, audio.sampleCount * short.sizeof); 140 if (length <= 0) { 141 return; 142 } 143 // Copy the first part of the circular buffer 144 auto start = audio.sampleIndex * short.sizeof; 145 auto end = min(start + length, Audio.SAMPLE_BUFFER_LENGTH * short.sizeof); 146 auto copyLength = end - start; 147 stream[0 .. copyLength] = sampleBytes[start .. end]; 148 // Copy the wrapped around part 149 start = 0; 150 end = length - copyLength; 151 stream[copyLength .. length] = sampleBytes[start .. end]; 152 // Increment the index past what as consumed, with wrapping 153 audio.sampleIndex = (audio.sampleIndex + length / short.sizeof) % Audio.SAMPLE_BUFFER_LENGTH; 154 // Decrement the sample count by the copied length 155 audio.sampleCount -= length / short.sizeof; 156 // If the sample count is half of the buffer length, request more 157 if (audio.sampleCount <= Audio.SAMPLE_BUFFER_LENGTH / 2) { 158 audio.sampleSignal.notify(); 159 } 160 } 161 } catch (Throwable throwable) { 162 import core.stdc.stdio : printf; 163 import std..string : toStringz; 164 printf("Error in audio callback: %s\n", throwable.msg.toStringz()); 165 } 166 }