]> Shamusworld >> Repos - apple2/blob - src/sound.cpp
5e92f48332051853aa767b2fe64bf1daa8f4db7f
[apple2] / src / sound.cpp
1 //
2 // Sound Interface
3 //
4 // by James L. Hammons
5 // (C) 2005 Underground Software
6 //
7 // JLH = James L. Hammons <jlhamm@acm.org>
8 //
9 // WHO  WHEN        WHAT
10 // ---  ----------  ------------------------------------------------------------
11 // JLH  12/02/2005  Fixed a problem with sound callback thread signaling the
12 //                  main thread
13 // JLH  12/03/2005  Fixed sound callback dropping samples when the sample buffer
14 //                  is shorter than the callback sample buffer
15 //
16
17 // STILL TO DO:
18 //
19 // - Figure out why it's losing samples (Bard's Tale) [DONE]
20 //
21
22 #include "sound.h"
23
24 #include <string.h>                                                             // For memset, memcpy
25 #include <SDL.h>
26 #include "log.h"
27
28
29 #define SAMPLE_RATE                     (44100.0)
30 #define SAMPLES_PER_FRAME       (SAMPLE_RATE / 60.0)
31 #define CYCLES_PER_SAMPLE       (1024000.0 / SAMPLE_RATE)
32 #define SOUND_BUFFER_SIZE       (8192)
33 #define AMPLITUDE                       (32)                            // -32 - +32 seems to be plenty loud!
34
35 // Global variables
36
37
38 // Local variables
39
40 static SDL_AudioSpec desired;
41 static bool soundInitialized = false;
42 static bool speakerState = false;
43 static uint8 soundBuffer[SOUND_BUFFER_SIZE];
44 static uint32 soundBufferPos;
45 static uint32 sampleBase;
46 static SDL_cond * conditional = NULL;
47 static SDL_mutex * mutex = NULL;
48 static SDL_mutex * mutex2 = NULL;
49
50 // Private function prototypes
51
52 static void SDLSoundCallback(void * userdata, Uint8 * buffer, int length);
53
54 //
55 // Initialize the SDL sound system
56 //
57 void SoundInit(void)
58 {
59 // To weed out problems for now...
60 #if 0
61 return;
62 #endif
63
64         desired.freq = SAMPLE_RATE;                                     // SDL will do conversion on the fly, if it can't get the exact rate. Nice!
65         desired.format = AUDIO_S8;                                      // This uses the native endian (for portability)...
66 //      desired.format = AUDIO_S16SYS;                          // This uses the native endian (for portability)...
67         desired.channels = 1;
68 //      desired.samples = 4096;                                         // Let's try a 4K buffer (can always go lower)
69 //      desired.samples = 2048;                                         // Let's try a 2K buffer (can always go lower)
70         desired.samples = 1024;                                         // Let's try a 1K buffer (can always go lower)
71         desired.callback = SDLSoundCallback;
72
73         if (SDL_OpenAudio(&desired, NULL) < 0)          // NULL means SDL guarantees what we want
74         {
75                 WriteLog("Sound: Failed to initialize SDL sound.\n");
76                 return;
77         }
78
79         conditional = SDL_CreateCond();
80         mutex = SDL_CreateMutex();
81         mutex2 = SDL_CreateMutex();// Let's try real signalling...
82         SDL_mutexP(mutex);                                                      // Must lock the mutex for the cond to work properly...
83         soundBufferPos = 0;
84         sampleBase = 0;
85
86         SDL_PauseAudio(false);                                          // Start playback!
87         soundInitialized = true;
88         WriteLog("Sound: Successfully initialized.\n");
89 }
90
91 //
92 // Close down the SDL sound subsystem
93 //
94 void SoundDone(void)
95 {
96         if (soundInitialized)
97         {
98                 SDL_PauseAudio(true);
99                 SDL_CloseAudio();
100                 SDL_DestroyCond(conditional);
101                 SDL_DestroyMutex(mutex);
102                 SDL_DestroyMutex(mutex2);
103                 WriteLog("Sound: Done.\n");
104         }
105 }
106
107 //
108 // Sound card callback handler
109 //
110 static void SDLSoundCallback(void * userdata, Uint8 * buffer, int length)
111 {
112         // The sound buffer should only starve when starting which will cause it to
113         // lag behind the emulation at most by around 1 frame...
114         // (Actually, this should never happen since we fill the buffer beforehand.)
115         // (But, then again, if the sound hasn't been toggled for a while, then this
116         //  makes perfect sense as the buffer won't have been filled at all!)
117
118         // Let's try using a mutex for shared resource consumption...
119         SDL_mutexP(mutex2);
120
121         if (soundBufferPos < (uint32)length)            // The sound buffer is starved...
122         {
123 //printf("Sound buffer starved!\n");
124 //fflush(stdout);
125                 for(uint32 i=0; i<soundBufferPos; i++)
126                         buffer[i] = soundBuffer[i];
127
128                 // Fill buffer with last value
129                 memset(buffer + soundBufferPos, (uint8)(speakerState ? AMPLITUDE : -AMPLITUDE), length - soundBufferPos);
130                 soundBufferPos = 0;                                             // Reset soundBufferPos to start of buffer...
131                 sampleBase = 0;                                                 // & sampleBase...
132 //Ick. This should never happen!
133 //Actually, this probably happens a lot. (?)
134 //              SDL_CondSignal(conditional);                    // Wake up any threads waiting for the buffer to drain...
135 //              return;                                                                 // & bail!
136         }
137         else
138         {
139                 // Fill sound buffer with frame buffered sound
140                 memcpy(buffer, soundBuffer, length);
141                 soundBufferPos -= length;
142                 sampleBase -= length;
143
144                 // Move current buffer down to start
145                 for(uint32 i=0; i<soundBufferPos; i++)
146                         soundBuffer[i] = soundBuffer[length + i];
147         }
148
149         // Free the mutex...
150         SDL_mutexV(mutex2);
151         // Wake up any threads waiting for the buffer to drain...
152 //      SDL_CondSignal(conditional);
153 }
154
155 // Need some interface functions here to take care of flipping the
156 // waveform at the correct time in the sound stream...
157
158 /*
159 Maybe set up a buffer 1 frame long (44100 / 60 = 735 bytes per frame)
160
161 Hmm. That's smaller than the sound buffer 2048 bytes... (About 2.75 frames needed to fill)
162
163 So... I guess what we could do is this:
164
165 -- Execute V65C02 for one frame. The read/writes at I/O address $C030 fill up the buffer
166    to the current time position.
167 -- The sound callback function copies the pertinent area out of the buffer, resets
168    the time position back (or copies data down from what it took out)
169 */
170
171 void ToggleSpeaker(uint32 time)
172 {
173         if (!soundInitialized)
174                 return;
175
176 #if 0
177 if (time > 95085)//(time & 0x80000000)
178 {
179         WriteLog("ToggleSpeaker() given bad time value: %08X (%u)!\n", time, time);
180 //      fflush(stdout);
181 }
182 #endif
183
184         // 1.024 MHz / 60 = 17066.6... cycles (23.2199 cycles per sample)
185         // Need the last frame position in order to calculate correctly...
186         // (or do we?)
187
188 //      SDL_LockAudio();
189         SDL_mutexP(mutex2);
190         uint32 currentPos = sampleBase + (uint32)((double)time / CYCLES_PER_SAMPLE);
191
192         if (currentPos > SOUND_BUFFER_SIZE - 1)
193         {
194 #if 0
195 WriteLog("ToggleSpeaker() about to go into spinlock at time: %08X (%u) (sampleBase=%u)!\n", time, time, sampleBase);
196 #endif
197 // Still hanging on this spinlock...
198 // That could be because the "time" value is too high and so the buffer will NEVER be
199 // empty enough...
200 // Now that we're using a conditional, it seems to be working OK--though not perfectly...
201 /*
202 ToggleSpeaker() about to go into spinlock at time: 00004011 (16401) (sampleBase=3504)!
203 16401 -> 706 samples, 3504 + 706 = 4210
204
205 And it still thrashed the sound even though it didn't run into a spinlock...
206
207 Seems like it's OK now that I've fixed the buffer-less-than-length bug...
208 */
209 //              SDL_UnlockAudio();
210 //              SDL_CondWait(conditional, mutex);
211 //              SDL_LockAudio();
212                 currentPos = sampleBase + (uint32)((double)time / CYCLES_PER_SAMPLE);
213 #if 0
214 WriteLog("--> after spinlock (sampleBase=%u)...\n", sampleBase);
215 #endif
216         }
217
218         int8 sample = (speakerState ? AMPLITUDE : -AMPLITUDE);
219
220         while (soundBufferPos < currentPos)
221                 soundBuffer[soundBufferPos++] = (uint8)sample;
222
223         // This is done *after* in case the buffer had a long dead spot (I think...)
224         speakerState = !speakerState;
225         SDL_mutexV(mutex2);
226 //      SDL_UnlockAudio();
227 }
228
229 void AddToSoundTimeBase(uint32 cycles)
230 {
231         if (!soundInitialized)
232                 return;
233
234 //      SDL_LockAudio();
235         SDL_mutexP(mutex2);
236         sampleBase += (uint32)((double)cycles / CYCLES_PER_SAMPLE);
237         SDL_mutexV(mutex2);
238 //      SDL_UnlockAudio();
239 }
240
241 /*
242 HOW IT WORKS
243
244 the main thread adds the amount of cpu time elapsed to samplebase. togglespeaker uses
245 samplebase + current cpu time to find appropriate spot in buffer. it then fills the
246 buffer up to the current time with the old toggle value before flipping it. the sound
247 irq takes what it needs from the sound buffer and then adjusts both the buffer and
248 samplebase back the appropriate amount.
249
250
251 A better way might be as follows:
252
253 Keep timestamp array of speaker toggle times. In the sound routine, unpack as many as will
254 fit into the given buffer and keep going. Have the toggle function check to see if the
255 buffer is full, and if it is, way for a signal from the interrupt that there's room for
256 more. Can keep a circular buffer. Also, would need a timestamp buffer on the order of 2096
257 samples *in theory* could toggle each sample
258
259 Instead of a timestamp, just keep a delta. That way, don't need to deal with wrapping and
260 all that (though the timestamp could wrap--need to check into that)
261
262 Need to consider corner cases where a sound IRQ happens but no speaker toggle happened.
263
264 If (delta > SAMPLES_PER_FRAME) then
265
266 Here's the relevant cases:
267
268 delta < SAMPLES_PER_FRAME -> Change happened within this time frame, so change buffer
269 frame came and went, no change -> fill buffer with last value
270 How to detect: Have bool bufferWasTouched = true when ToggleSpeaker() is called.
271 Clear bufferWasTouched each frame.
272
273 Two major cases here:
274
275  o  Buffer is touched on current frame
276  o  Buffer is untouched on current frame
277
278 In the first case, it doesn't matter too much if the previous frame was touched or not,
279 we don't really care except in finding the correct spot in the buffer to put our change
280 in. In the second case, we need to tell the IRQ that nothing happened and to continue
281 to output the same value.
282
283 SO: How to synchronize the regular frame buffer with the IRQ buffer?
284
285 What happens:
286   Sound IRQ --> Every 1024 sample period (@ 44.1 KHz = 0.0232s)
287   Emulation --> Render a frame --> 1/60 sec --> 735 samples
288     --> sound buffer is filled
289
290 Since the emulation is faster than the SIRQ the sound buffer should fill up
291 prior to dumping it to the sound card.
292
293 Problem is this: If silence happens for a long time then ToggleSpeaker is never
294 called and the sound buffer has stale data; at least until soundBufferPos goes to
295 zero and stays there...
296
297 BUT this should be handled correctly by toggling the speaker value *after* filling
298 the sound buffer...
299
300 Still getting random clicks when running...
301 (This may be due to the lock/unlock sound happening in ToggleSpeaker()...)
302 */
303
304
305
306
307
308
309