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 }