#include #include #include #include // goddamn apple headers #undef TCP_NODELAY #undef TCP_MAXSEG #include #include "MacMain.h" #include "SoundMangler.h" // allocate the SndChannel statically here rather than using NewPtr static SndChannel mySndChannel; static SndChannelPtr mySndChannelPtr = 0; static SndCallBackUPP myUPP; // each SoundHeader has an attached BLOCKSIZE buffer. if a single // sound block is larger than that, it will be divided without any // promise of being played atomically... enum { BLOCKSIZE = 1024 }; enum { MAXHEADERS = 8 }; // SoundHeader buffers are now used strictly round-robin fashion, // and when a buffer is queued, we do not return until the buffer // starts playing (approximately, anyway; what happens is that the // caller passes in an MPSemaphoreID which is to be tickled when // some previous buffer completes; previously this means the current // buffer minus one...). // A buffer is marked busy by writing 1 into the corresponding gSemaphore // pointer, and marked free by writing 0. If you can change the semaphore // pointer atomically from 1 to a nonzero value, the Sound Manager Fairy // will awaken you when the corresponding sound completes, otherwise it's // your problem (it's an error if the value is nonzero already). When // marking a buffer free, if you can atomically turn 1 into 0, you're off // the hook for signalling, otherwise it contains a semaphore pointer to ring. static ExtSoundHeaderPtr mySoundHeaders[MAXHEADERS]; static MPSemaphoreID *gSemaphores[MAXHEADERS]; static int gNextBuffer, gNumBuffers = 3; static volatile UInt32 gHeaderMask; static short gNumChannels; static short gSampleSize; static Fixed gSampleRate; #define debugcrap #ifdef debugcrap static int bcount; static Nanoseconds gStartTime; static Nanoseconds gMostRecentTime; static int playcount; static Nanoseconds gPlayStartTime, gPlayMostRecentTime; static int misses; #endif static pascal void theCallbackFunction(SndChannelPtr chan, SndCommand *cmd) { // negative param1 is special (means check gCallBackWhenDone) if (cmd->param1 >= 0) { // Try to give the buffer back by changing the semaphore pointer // from 1 to 0. if (!OTCompareAndSwapPtr((void*)1, (void*)0, (void **)&gSemaphores[cmd->param1])) { (void)MPSignalSemaphore(*gSemaphores[cmd->param1]); gSemaphores[cmd->param1] = (MPSemaphoreID *)0; } #ifdef debugcrap if (bcount == 0) gStartTime = AbsoluteToNanoseconds(UpTime()); else gMostRecentTime = AbsoluteToNanoseconds(UpTime()); if (++bcount == 200) fprintf(stderr,"200 buffers played\n"); #endif } else if (cmd->param1 == (short)-1) { // "thunk!" callbackThunk *thunk = (callbackThunk *)cmd->param2; thunk->func((void *)thunk); // thunk you very much. } } // Not much to do during the Initialize phase. I allocate the memory for // the SndChannel statically, and we don't set up the parameters until // Open time. OSStatus SoundManagerInitialize() { OSErr err = noErr; // first create the UPP for the callback function if ((myUPP = NewSndCallBackUPP(theCallbackFunction)) == 0) { err = memFullErr; goto bail; } gNextBuffer = 0; for (int i = 0; i < MAXHEADERS; i++) { ExtSoundHeaderPtr p; if ((p = (ExtSoundHeaderPtr)NewPtrClear( sizeof(ExtSoundHeader) - 1 + BLOCKSIZE )) == 0) { err = memFullErr; break; } mySoundHeaders[i] = p; gSemaphores[i] = 0; // channels, rate, and size get set at play time p->encode = extSH; p->baseFrequency = 60; // ?? // fill in numFrames at the appropriate time // according to the CarbonSndPlayDB.c code snippet, AIFFSampleRate // is unused. well, that's a logical place to document that fact! } bail: return err; } OSStatus SoundManagerOpenPlayer(const char *name, const soundParams *sp, unsigned long *handlep) { OSStatus err = noErr; if (mySndChannelPtr != 0) return fBsyErr; mySndChannel.qLength = 128; mySndChannel.userInfo = (long)0; // Actually create the channel // XXX To open a sound channel which is not the default device, // XXX obtain the component id of the output device, then pass // XXX kUseOptionalOutputDevice as the synth parameter and the // XXX ComponentInstance as the init parameter; I guess you'd then // XXX do a reInitCmd to set stereo or mono, and nothing works but // XXX sampledSynth nowadays anyway... mySndChannelPtr = &mySndChannel; err = SndNewChannel(&mySndChannelPtr, sampledSynth, sp->sp_channels == 2 ? initStereo : initMono, myUPP); if (err == 0) { gNumChannels = sp->sp_channels; gSampleSize = sp->sp_samplesize; gSampleRate = Long2Fix(sp->sp_samplerate); *handlep = (unsigned long)mySndChannelPtr; } return err; } OSStatus SoundManagerSetPlayerFormat(unsigned long /*h*/, const soundParams *sp) { OSStatus err = noErr; // These values just get copied into each bufferCmd. Clever Sound Manager. gNumChannels = sp->sp_channels; gSampleSize = sp->sp_samplesize; gSampleRate = Long2Fix(sp->sp_samplerate); // Interesting question; I wonder if I should "correct" integral sample // rates which are near standard non-integral sample rates? Bah; I'm // only doing this for a task that uses 8KHz sampling, anyway... // I'm not sure if a stereo-capable channel needs to be warned that mono // data is coming (or vice versa?), but I suppose that a component-selected // output channel should be told of the channel choice sometime... SndCommand theCommand; theCommand.cmd = reInitCmd; theCommand.param1 = 0; theCommand.param2 = gNumChannels == 2 ? initStereo : initMono; err = SndDoCommand(&mySndChannel, &theCommand, FALSE /* wait */); return err; } OSStatus SoundManagerPlaySample(unsigned long /*h*/, const unsigned char *buf, size_t len, MPSemaphoreID *awaken_me) { #ifdef debugcrap if (playcount == 0) { playcount = 1; gPlayStartTime = AbsoluteToNanoseconds(UpTime()); } else { if (playcount == 1) playcount = len; else playcount += len; if ((playcount - len) < 240000 && playcount >= 240000) fprintf(stderr,"~240000 bytes played\n"); gPlayMostRecentTime = AbsoluteToNanoseconds(UpTime()); } // if (len != 480) fprintf(stderr,"wierd len %d\n", len); #endif OSErr err = noErr; // the insufficiently abstract API we present loses the size of the // sample frames, so we have to reinvent that here. int frameSize = gNumChannels * (gSampleSize == 8 ? 1 : 2); assert(len%frameSize == 0); if (len > BLOCKSIZE) return notEnoughBufferSpace; // busy-wait until the sound header we want is free // Remember -- only one thread queues requests! while (!OTCompareAndSwap32(0, 1, (UInt32*)&gSemaphores[gNextBuffer])) { /* the assumption is we're a Blue thread, so play nice */ YieldToAnyThread(); #if P_MACOSX usleep(1000); #endif // P_MACOSX #ifdef debugcrap #define DEBUGME #ifdef DEBUGME if ((misses & 127) == 0) fprintf(stderr,"missed scoring a buffer!\n"); misses++; #endif // DEBUGME #endif // debugcrap } // OK, we've made our reservation, copy the data ExtSoundHeaderPtr p; p = mySoundHeaders[gNextBuffer]; // someone set up us the buffer! memcpy(&p->sampleArea[0], buf, len); p->numFrames = len / frameSize; p->numChannels = gNumChannels; p->sampleRate = gSampleRate; p->sampleSize = gSampleSize; // Queue the commands (release the hounds!) do { // break-scope SndCommand theCommand; theCommand.cmd = bufferCmd; theCommand.param1 = 0; theCommand.param2 = (long)p; err = SndDoCommand(&mySndChannel, &theCommand, FALSE); if (err) break; // And arrange for it to be picked up theCommand.cmd = callBackCmd; theCommand.param1 = gNextBuffer; // we're single threaded, right??? theCommand.param2 = 0; err = SndDoCommand(&mySndChannel, &theCommand, FALSE); if (err) break; // now, attempt to reserve callback if (awaken_me) { int prevBuffer = gNextBuffer-1; if (prevBuffer == -1) prevBuffer = gNumBuffers-1; if (!OTCompareAndSwapPtr((void *)1, (void *)awaken_me, (void **)&gSemaphores[prevBuffer])) { PAssert(gSemaphores[prevBuffer] == 0, "screwup in buffer handling"); MPSignalSemaphore(*awaken_me); } } // OK, advance buffer pointer if (++gNextBuffer == gNumBuffers) gNextBuffer = 0; } while (0); // end break scope if (err) { gSemaphores[gNextBuffer] = 0; // give it back goto bail; } bail: if (err) { fprintf(stderr,"SndDoCommand failed with err %d\n", err); } return err; } OSStatus SoundManagerClosePlayer(unsigned long /*h*/) { if (mySndChannelPtr) { // XXX what about all of the waiting semaphores? SndDisposeChannel(&mySndChannel, FALSE); } mySndChannelPtr = 0; return 0; } OSStatus SoundManagerStopPlayer(unsigned long /*h*/) { OSErr err; SndCommand theCommand; theCommand.cmd = flushCmd; theCommand.param1 = 0; theCommand.param2 = 0; err = SndDoImmediate(&mySndChannel, &theCommand); // XXX what about all of the waiting semaphores? if (err == 0) { theCommand.cmd = quietCmd; err = SndDoImmediate(&mySndChannel, &theCommand); } return err; } OSStatus SoundManagerIsPlaying(unsigned long /*h*/, unsigned long *answer) { // If the "previous" buffer is free, it's not busy. int prevBuffer = gNextBuffer-1; if (prevBuffer == -1) prevBuffer = gNumBuffers-1; *answer = (gSemaphores[prevBuffer] != 0); return 0; } OSStatus SoundManagerWaitForPlayCompletion(unsigned long, const callbackThunk *thunk) { // Register a callback which will be called when the sounds are done // playing. It will be inserted into the SM queue, so if you queue // sounds while it is waiting, it may return "early". SndCommand theCommand; theCommand.cmd = callBackCmd; theCommand.param1 = -1; // special index value theCommand.param2 = (unsigned long)thunk; OSStatus err = SndDoCommand(&mySndChannel, &theCommand, FALSE); return err; } OSStatus SoundManagerTeardown() { if (myUPP) { // better not be running! DisposeSndCallBackUPP(myUPP); myUPP = 0; } for (int i = 0; i < MAXHEADERS; i++) { if (mySoundHeaders[i]) { DisposePtr((char *)mySoundHeaders[i]); mySoundHeaders[i] = 0; } } return 0; }