AppleiSubEngine.cpp [plain text]
#include "AppleiSubEngine.h"
#include "AppleUSBAudioCommon.h"
#include "AppleUSBAudioLevelControl.h"
#include "AppleUSBAudioMuteControl.h"
#include "USBAudioObject.h"

#include <IOKit/audio/IOAudioStream.h>
#include <IOKit/audio/IOAudioEngine.h>

#define kiSubFeatureUnitID					2
#define kiSubMuteControlChannelNum			0
#define kiSubVolumeControlLeftChannelNum	1
#define kiSubVolumeControlRightChannelNum	2

// Has to be true or else the iSub looses the beginning of sounds if it has auto powered down
#define CONTINUOUS_STREAMING				TRUE
#define DEBUGTIMESTAMPS						FALSE

#define super IOService

OSDefineMetaClassAndStructors (AppleiSubEngine, super)

#pragma mark -IOKit Routines-

// This has to be called on the audio engine's work loop so that we don't interrupt the IOAudioFamily
void AppleiSubEngine::close (IOService * forClient, IOOptionBits options) {
	debug4IOLog ("+AppleiSubEngine[%p]::close (%p, 0x%lx)\n", this, forClient, options);

	if (NULL != audioEngine) {
		debugIOLog ("removing iSub audio controls\n");
		audioEngine->pauseAudioEngine ();
		audioEngine->beginConfigurationChange ();
		(void)audioEngine->removeDefaultAudioControl (leftVolumeControl);
		(void)audioEngine->removeDefaultAudioControl (rightVolumeControl);
		(void)audioEngine->removeDefaultAudioControl (muteControl);
		audioEngine->completeConfigurationChange ();
		audioEngine->resumeAudioEngine ();
		audioEngine = NULL;
	}

	super::close (forClient, options);

	debug4IOLog ("-AppleiSubEngine[%p]::close (%p, 0x%lx)\n", this, forClient, options);
}

bool AppleiSubEngine::finalize (IOOptionBits options) {
	Boolean							resultCode;

	debug4IOLog ("+AppleiSubEngine[%p]::finalize (%p), rc=%d\n", this, options, getRetainCount ());

	resultCode = super::finalize (options);

	debug5IOLog ("-AppleiSubEngine[%p]::finalize (%p) = %d, rc=%d\n", this, options, resultCode, getRetainCount ());
	return resultCode;
}

void AppleiSubEngine::free (void) {
	UInt32							frameIndex;

	debug2IOLog ("+AppleiSubEngine[%p]::free ()\n", this);

	if (NULL != thePipe) {
		thePipe->release ();
		thePipe = NULL;
	}

	if (NULL != sampleBufferDescriptor) {
		sampleBufferDescriptor->release ();
		sampleBufferDescriptor = NULL;
	}

	if (NULL != sampleBuffer) {
		IOFree (sampleBuffer, bufferSize);
		sampleBuffer = NULL;
	}

	for (frameIndex = 0; frameIndex < NUM_ISUB_FRAME_LISTS; frameIndex++) {
		if (NULL != soundBuffer[frameIndex]) {
			soundBuffer[frameIndex]->release ();
			soundBuffer[frameIndex] = NULL;
		}
	}

	super::free ();

	debug2IOLog ("-AppleiSubEngine[%p]::free ()\n", this);
	return;
}

// This has to be called on the audio engine's work loop so that we don't interrupt the IOAudioFamily
bool AppleiSubEngine::handleOpen (IOService * forClient, IOOptionBits options, void * arg) {
	IOUSBFindInterfaceRequest		findRequest;
	IOUSBFindEndpointRequest		audioIsochEndpoint;
	IOReturn						resultIOReturn;
	UInt32							i;
#if CONTINUOUS_STREAMING
    AbsoluteTime					currentTime;
	AbsoluteTime					initialTimestampOffset;
	UInt32							frameListNum;
	UInt16							numQueued;
#endif
	bool							resultCode;

	debug5IOLog ("+AppleiSubEngine[%p]::handleOpen (%p, 0x%lx, %p)\n", this, forClient, options, arg);

	resultCode = FALSE;

	FailIf (FALSE == super::handleOpen (forClient, options, arg), Exit);
	FailIf (NULL == forClient, Exit);
	audioEngine = (IOAudioEngine *)forClient;

	frameListSize = CalculateNumSamplesPerBuffer (44100, NUM_ISUB_FRAMES_PER_LIST) * 4;
	debug2IOLog ("frameListSize = %d\n", frameListSize);
	bufferSize = CalculateNumSamplesPerBuffer (44100, NUM_ISUB_FRAMES_PER_LIST, NUM_ISUB_FRAME_LISTS) * 4;
	debug2IOLog ("bufferSize = %d\n", bufferSize);
	sampleBuffer = IOMallocAligned (round_page (bufferSize), PAGE_SIZE);
	FailIf (NULL == sampleBuffer, Exit);
	for (i = 0; i < bufferSize / 4; i++) {
		((UInt32*)sampleBuffer)[i] = 0;
	}
	sampleBufferDescriptor = IOMemoryDescriptor::withAddress (sampleBuffer, bufferSize, kIODirectionNone);
	FailIf (NULL == sampleBufferDescriptor, Exit);

	FailIf (kIOReturnSuccess != PrepareFrameLists (frameListSize), Exit);
	FailIf (FALSE == streamInterface->open (this), Exit);

	resultIOReturn = streamInterface->SetAlternateInterface (this, kRootAlternateSetting);
	FailIf (kIOReturnSuccess != resultIOReturn, Exit);

	resultIOReturn = streamInterface->SetAlternateInterface (this, 4);		// This is the 16-bit stereo interface
	FailIf (kIOReturnSuccess != resultIOReturn, Exit);

	// Acquire a PIPE for the isochronous stream.
	audioIsochEndpoint.type = kUSBIsoc;
	audioIsochEndpoint.direction = kUSBOut;

	thePipe = streamInterface->FindNextPipe (NULL, &audioIsochEndpoint);
	FailIf (NULL == thePipe, Exit);
	thePipe->retain ();

#if CONTINUOUS_STREAMING
	// Start the iSub playing silence now
	loopCount = 0;
	nanoseconds_to_absolutetime ((kMinimumiSubFrameOffset) * 1000 * 1000, &initialTimestampOffset);
	clock_get_uptime (&currentTime);
	ADD_ABSOLUTETIME (&currentTime, &initialTimestampOffset);
	lastLoopTime.hi = currentTime.hi;
	lastLoopTime.lo = currentTime.lo;

	shouldStop = 0;
	numQueued = 0;
	currentFrameList = 0;
	currentByteOffset = 0;
	theFirstFrame = streamInterface->GetDevice()->GetBus()->GetFrameNumber () + kMinimumiSubFrameOffset;
	for (frameListNum = currentFrameList; numQueued < NUM_ISUB_FRAME_LISTS_TO_QUEUE; frameListNum++) {
		resultIOReturn = WriteFrameList (frameListNum);
		FailIf (kIOReturnSuccess != resultIOReturn, Exit);
		numQueued++;
	}
#endif

	// Find control interface on iSub device
	findRequest.bInterfaceClass = 1;
	findRequest.bInterfaceSubClass = 1;
	findRequest.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
	findRequest.bAlternateSetting = kIOUSBFindInterfaceDontCare;
	controlInterface = streamInterface->GetDevice()->FindNextInterface (NULL, &findRequest);
	FailIf (NULL == controlInterface, Exit);

	audioEngine->pauseAudioEngine ();
	audioEngine->beginConfigurationChange ();
	leftVolumeControl = AppleUSBAudioLevelControl::create (kiSubFeatureUnitID,
													controlInterface->GetInterfaceNumber (),
													VOLUME_CONTROL,
													kiSubVolumeControlLeftChannelNum,
													(USBDeviceRequest)&deviceRequest,
													this,
													'subL',
													kIOAudioControlUsageOutput);
	FailIf (NULL == leftVolumeControl, Exit);

	audioEngine->addDefaultAudioControl (leftVolumeControl);
	leftVolumeControl->release ();

	rightVolumeControl = AppleUSBAudioLevelControl::create (kiSubFeatureUnitID,
													controlInterface->GetInterfaceNumber (),
													VOLUME_CONTROL,
													kiSubVolumeControlRightChannelNum,
													(USBDeviceRequest)&deviceRequest,
													this,
													'subR',
													kIOAudioControlUsageOutput);
	FailIf (NULL == rightVolumeControl, Exit);

	audioEngine->addDefaultAudioControl (rightVolumeControl);
	rightVolumeControl->release ();

	muteControl = AppleUSBAudioMuteControl::create (kiSubFeatureUnitID,
													controlInterface->GetInterfaceNumber (),
													0,
													(USBDeviceRequest)&deviceRequest,
													this,
													kIOAudioControlUsageOutput,
													'subM');
	FailIf (NULL == muteControl, Exit);

	audioEngine->addDefaultAudioControl (muteControl);
	muteControl->release ();

	audioEngine->completeConfigurationChange ();
	audioEngine->resumeAudioEngine ();

	resultCode = TRUE;

Exit:
	debug5IOLog ("-AppleiSubEngine[%p]::handleOpen (%p, 0x%lx, %p)\n", this, forClient, options, arg);
	return resultCode;
}

bool AppleiSubEngine::init (OSDictionary * properties) {
	Boolean							resultCode;

	debug3IOLog ("+AppleiSubEngine[%p]::init (%p)\n", this, properties);

	resultCode = FALSE;
	FailIf (FALSE == super::init (properties), Exit);

	resultCode = TRUE;

Exit:
	debug4IOLog ("-AppleiSubEngine[%p]::init (%p) = %d\n", this, properties, resultCode);
	return resultCode;
}

IOReturn AppleiSubEngine::message (UInt32 type, IOService * provider, void * arg) {
    IOReturn						resultCode;

	debug4IOLog ("+AppleiSubEngine[%p]::message (0x%x, %p)\n", this, type, provider);

	resultCode = kIOReturnSuccess;

	switch (type) {
		case kIOMessageServiceIsRequestingClose:
			debugIOLog ("kIOMessageServiceIsRequestingClose\n");
		case kIOMessageServiceIsTerminated:
			if (kIOMessageServiceIsRequestingClose != type) debugIOLog ("kIOMessageServiceIsTerminated\n");
			if ((NULL != streamInterface) && (streamInterface == provider)) {
				debugIOLog ("stopping iSub\n");
				shouldStop = 1;
				streamInterface->close (this);
				streamInterface = NULL;
			}
			break;
		default:
			;
	}

	debug4IOLog ("-AppleiSubEngine[%p]::message (0x%x, %p)\n", this, type, provider);
	return resultCode;
}

bool AppleiSubEngine::start (IOService * provider) {
	Boolean							resultBool;

	debug3IOLog ("+AppleiSubEngine[%p]::start (%p)\n", this, provider);

	resultBool = FALSE;
	FailIf (FALSE == super::start (provider), Exit);

	streamInterface = OSDynamicCast (IOUSBInterface, provider);
	FailIf (NULL == streamInterface, Exit);

	ourInterfaceNumber = streamInterface->GetInterfaceNumber ();
	debug2IOLog ("AppleiSubEngine->ourInterfaceNumber = %d\n", ourInterfaceNumber);
	alternateInterfaceID = 4;

	interfaceLock = IORecursiveLockAlloc ();
	FailIf (NULL == interfaceLock, Exit);

	registerService ();
	resultBool = TRUE;

Exit:
	debug4IOLog ("-AppleiSubEngine[%p]::start (%p) = %d\n", this, provider, resultBool);
	return resultBool;
}

void AppleiSubEngine::stop (IOService * provider) {
	debug2IOLog("+AppleiSubEngine[%p]::stop ()\n", this);

	// We've been asked to go away, so we should really stop now
	shouldStop = 1;

	super::stop (provider);

	debug2IOLog("-AppleiSubEngine[%p]::stop ()\n", this);
	return;
}

bool AppleiSubEngine::terminate (IOOptionBits options) {
	Boolean							resultCode;

	debug4IOLog ("+AppleiSubEngine[%p]::terminate (0x%x), rc=%d\n", this, options, getRetainCount ());

	resultCode = super::terminate (options);

	debug5IOLog ("-AppleiSubEngine[%p]::terminate (0x%x) = %d, rc=%d\n", this, options, resultCode, getRetainCount ());
	return resultCode;
}

#pragma mark -iSub Routines-

UInt32 AppleiSubEngine::CalculateNumSamplesPerBuffer (UInt32 sampleRate, UInt32 theNumFramesPerList, UInt32 theNumFrameLists) {
	UInt32							numSamplesPerFrameList;
	UInt32 							totalFrames;
	UInt32							numAlternateFrames;
	UInt32							numAverageFrames;
	UInt16							averageSamplesPerFrame;
	UInt16							additionalSampleFrameFreq;

	CalculateSamplesPerFrame (sampleRate, &averageSamplesPerFrame, &additionalSampleFrameFreq);
	if (0 == additionalSampleFrameFreq) {
		numSamplesPerFrameList = averageSamplesPerFrame * theNumFramesPerList * theNumFrameLists;
	} else {
		totalFrames = theNumFramesPerList * theNumFrameLists;

		numAlternateFrames = totalFrames / additionalSampleFrameFreq;
		numAverageFrames = totalFrames - numAlternateFrames;
		numSamplesPerFrameList = (numAverageFrames * averageSamplesPerFrame) + (numAlternateFrames * (averageSamplesPerFrame + 1));
	}

	return numSamplesPerFrameList;
}

void AppleiSubEngine::CalculateSamplesPerFrame (UInt32 sampleRate, UInt16 * averageSamplesPerFrame, UInt16 * additionalSampleFrameFreq) {
	UInt32							divisor;

	*averageSamplesPerFrame = sampleRate / 1000;

	divisor = (sampleRate % 1000);

	if (divisor)
		*additionalSampleFrameFreq = 1000 / divisor;
	else
		*additionalSampleFrameFreq = 0;
}

IOReturn AppleiSubEngine::deviceRequest (IOUSBDevRequest * request, AppleiSubEngine * self, IOUSBCompletion * completion) {
	IOReturn						result;

	debug4IOLog ("+AppleiSubEngine[%p]::deviceRequest (%p, %p)\n", self, request, completion);
	result = kIOReturnSuccess;
	if (self->streamInterface) {
		FailIf (NULL == self->interfaceLock, Exit);

        IORecursiveLockLock (self->interfaceLock);
        result = self->streamInterface->DeviceRequest (request, completion);
        IORecursiveLockUnlock (self->interfaceLock);
    }

	debug4IOLog ("-AppleiSubEngine[%p]::deviceRequest (%p, %p)\n", self, request, completion);

Exit:
	return result;
}

UInt32 AppleiSubEngine::GetCurrentByteCount (void) {
	return currentByteOffset;
}

UInt16 AppleiSubEngine::GetCurrentFrameList (void) {
	return currentFrameList;
}

UInt32 AppleiSubEngine::GetCurrentLoopCount (void) {
	return loopCount;
}

IOMemoryDescriptor * AppleiSubEngine::GetSampleBuffer (void) {
	return sampleBufferDescriptor;
}

volatile AbsoluteTime * AppleiSubEngine::GetLoopTime (void) {
	return &lastLoopTime;
}

IOReturn AppleiSubEngine::PrepareFrameLists (UInt32 frameListSize) {
	IOReturn						result;
	UInt32							frameIndex;

	result = kIOReturnError;

	for (frameIndex = 0; frameIndex < NUM_ISUB_FRAME_LISTS; frameIndex++) {
		usbCompletion[frameIndex].target = (void *)this;
		usbCompletion[frameIndex].parameter = (void *)((UInt8 *)sampleBuffer + (frameIndex * frameListSize));	// pointer into the buffer that is the start of this frame list
		usbCompletion[frameIndex].action = WriteHandler;

		soundBuffer[frameIndex] = IOMemoryDescriptor::withAddress ((UInt8 *)usbCompletion[frameIndex].parameter, frameListSize, kIODirectionNone);
		FailIf (NULL == soundBuffer[frameIndex], Exit);
	}

	result = kIOReturnSuccess;

Exit:
	return result;
}

IOReturn AppleiSubEngine::StartiSub (void) {
    AbsoluteTime					currentTime;
	AbsoluteTime					initialTimestampOffset;
	UInt32							frameListNum;
	UInt16							numQueued;
    IOReturn						resultCode;

	debug2IOLog ("+AppleiSubEngine[%p]::StartiSub ()\n", this);

	resultCode = kIOReturnError;
#if !CONTINUOUS_STREAMING
	loopCount = 0;
	nanoseconds_to_absolutetime ((kMinimumiSubFrameOffset) * 1000 * 1000, &initialTimestampOffset);
	clock_get_uptime (&currentTime);
	ADD_ABSOLUTETIME (&currentTime, &initialTimestampOffset);
	lastLoopTime.hi = currentTime.hi;
	lastLoopTime.lo = currentTime.lo;

	shouldStop = 0;
	numQueued = 0;
	currentFrameList = 0;
	currentByteOffset = 0;
	theFirstFrame = streamInterface->GetDevice()->GetBus()->GetFrameNumber () + kMinimumiSubFrameOffset;
	for (frameListNum = currentFrameList; numQueued < NUM_ISUB_FRAME_LISTS_TO_QUEUE; frameListNum++) {
		resultCode = WriteFrameList (frameListNum);
		FailIf (kIOReturnSuccess != resultCode, Exit);
		numQueued++;
	}
#else
	iSubRunning = TRUE;
	loopCount = 0xFFFFFFFF;			// so that it will go to 0 in the write completion routine when the frames are aborted
	clock_get_uptime (&currentTime);
	lastLoopTime.hi = currentTime.hi;
	lastLoopTime.lo = currentTime.lo;
	currentFrameList = NUM_ISUB_FRAME_LISTS - 1;
//	theFirstFrame = 0;		// force the completion routine to calculate the correct starting frame
//	FailIf (NULL == thePipe, Exit);
//	thePipe->Abort ();		// let's kill all outstanding IO and start right back at the beginning
	currentByteOffset = 0;
	resultCode = kIOReturnSuccess;
	if (FALSE == iSubUSBRunning) {
		// Why aren't the USB writes running?  They should be always running...
#if DEBUGLOG
		IOLog ("!!!iSub USB transport isn't running!!!\n");
#endif

		loopCount = 0;
		nanoseconds_to_absolutetime ((kMinimumiSubFrameOffset) * 1000 * 1000, &initialTimestampOffset);
		clock_get_uptime (&currentTime);
		ADD_ABSOLUTETIME (&currentTime, &initialTimestampOffset);
		lastLoopTime.hi = currentTime.hi;
		lastLoopTime.lo = currentTime.lo;

		shouldStop = 0;
		numQueued = 0;
		currentFrameList = 0;
		currentByteOffset = 0;
		theFirstFrame = streamInterface->GetDevice()->GetBus()->GetFrameNumber () + kMinimumiSubFrameOffset;
		for (frameListNum = currentFrameList; numQueued < NUM_ISUB_FRAME_LISTS_TO_QUEUE; frameListNum++) {
			resultCode = WriteFrameList (frameListNum);
			FailIf (kIOReturnSuccess != resultCode, Exit);
			numQueued++;
		}
	}
#endif

Exit:
	debug3IOLog ("-AppleiSubEngine[%p]::StartiSub (), result = %d\n", this, resultCode);

	return resultCode;
}

IOReturn AppleiSubEngine::StopiSub (void) {

	debug2IOLog ("+AppleiSubEngine[%p]::StopiSub ()\n", this);

	iSubRunning = FALSE;
#if !CONTINUOUS_STREAMING
	shouldStop = 1;
#endif

	debug2IOLog ("-AppleiSubEngine[%p]::StopiSub ()\n", this);

	return kIOReturnSuccess;
}

IOReturn AppleiSubEngine::WriteFrameList (UInt32 frameListNum) {
    UInt32							frameIndex;
	UInt32							firstFrame;
	UInt16							averageFrameSamples;
	UInt16							averageFrameSize;
	UInt16							alternateFrameSize;
	UInt16							additionalSampleFrameFreq;
    IOReturn						result;

	result = kIOReturnError;

	firstFrame = (frameListNum % NUM_ISUB_FRAME_LISTS_TO_QUEUE) * NUM_ISUB_FRAMES_PER_LIST;

	CalculateSamplesPerFrame (44100, &averageFrameSamples, &additionalSampleFrameFreq);
	averageFrameSize = averageFrameSamples * 4;
	alternateFrameSize = (averageFrameSamples + 1) * 4;

	if (additionalSampleFrameFreq) {
		for (frameIndex = 0; frameIndex < NUM_ISUB_FRAMES_PER_LIST; frameIndex++) {
			theFrames[firstFrame + frameIndex].frStatus = -1;
			if ((frameIndex % additionalSampleFrameFreq) == (UInt16)(additionalSampleFrameFreq - 1)) {
				theFrames[firstFrame + frameIndex].frReqCount = alternateFrameSize;
			} else {
				theFrames[firstFrame + frameIndex].frReqCount = averageFrameSize;
			}
			theFrames[firstFrame + frameIndex].frActCount = 0;
		}
	} else {
		for (frameIndex = 0; frameIndex < NUM_ISUB_FRAMES_PER_LIST; frameIndex++) {
			theFrames[firstFrame + frameIndex].frStatus = -1;
			theFrames[firstFrame + frameIndex].frReqCount = averageFrameSize;
			theFrames[firstFrame + frameIndex].frActCount = 0;
		}
	}

	retain ();		// Don't want the driver being terminated until our completion routine runs.
	result = thePipe->Write (soundBuffer[frameListNum], theFirstFrame, NUM_ISUB_FRAMES_PER_LIST, &theFrames[firstFrame], &usbCompletion[frameListNum]);

	if (result != kIOReturnSuccess) {
		debug6IOLog ("++AppleiSubEngine[%p]::WriteFrameList (%d) - error writing to pipe at frame %lu - current = %lu: 0x%x\n", this, frameListNum, (UInt32)theFirstFrame, (UInt32)streamInterface->GetDevice()->GetBus()->GetFrameNumber(), result);
		iSubUSBRunning = FALSE;
	} else {
		theFirstFrame += NUM_ISUB_FRAMES_PER_LIST;
		iSubUSBRunning = TRUE;
	}

	return result;
}

void AppleiSubEngine::WriteHandler (AppleiSubEngine * self, UInt32 * buffer, IOReturn result, IOUSBIsocFrame * pFrames) {
    AbsoluteTime					currentTime;
	UInt64							currentUSBFrame;
//	static UInt64					lastScheduledUSBFrame = 0;
	UInt32							frameListToWrite;
	UInt32							i;
#if DEBUGTIMESTAMPS
	static AbsoluteTime				lastLoopTime = {0, 0};
	AbsoluteTime					diff;
	UInt64							nanos;
#endif

	// Zero the data in the buffer so that this buffer just contains silence
	for (i = 0; i < self->frameListSize / 4; i++) {
		buffer[i] = 0;
	}

	if (result != kIOReturnSuccess) {
#if DEBUGLOG
		if (result != kIOReturnOverrun) {
			IOLog ("++AppleiSubEngine::WriteHandler () - error 0x%x\n", result);
		}
#endif
		FailIf (NULL == self->streamInterface, Exit);
		currentUSBFrame = self->streamInterface->GetDevice()->GetBus()->GetFrameNumber ();
		switch (result) {
			case kIOReturnOverrun:
			case kIOReturnAborted:
			default:
				// skip ahead and see if that helps
				if (self->theFirstFrame <= currentUSBFrame) {
					self->theFirstFrame = currentUSBFrame + kMinimumiSubFrameOffset;
#if DEBUGLOG
					IOLog ("+++AppleiSubEngine::skipping ahead to frame %ld\n", self->theFirstFrame);
#endif
				}
				break;
		}
	}

	if ((NUM_ISUB_FRAME_LISTS - 1) == self->currentFrameList) {
		clock_get_uptime (&currentTime);
#if DEBUGTIMESTAMPS
		diff.hi = currentTime.hi;
		diff.lo = currentTime.lo;
		SUB_ABSOLUTETIME (&diff, &lastLoopTime);
		lastLoopTime.hi = currentTime.hi;
		lastLoopTime.lo = currentTime.lo;
		absolutetime_to_nanoseconds (diff, &nanos);
		IOLog ("delta = %ld\n", (UInt32)(nanos / (1000 * 1000)));
#endif
		self->lastLoopTime.hi = currentTime.hi;
		self->lastLoopTime.lo = currentTime.lo;
		if (TRUE == self->iSubRunning) {
			self->loopCount++;
			self->currentByteOffset = 0;
		}
		self->currentFrameList = 0;
	} else {
		self->currentFrameList++;
		if (TRUE == self->iSubRunning) {
			self->currentByteOffset = self->currentFrameList * self->frameListSize;
		}
	}

	if (self->shouldStop > 0) {
		debug3IOLog ("++AppleiSubEngine[%p]::WriteHandler () - stopping: %d\n", self, self->shouldStop);
		self->shouldStop++;
	} else {
		frameListToWrite = self->currentFrameList + NUM_ISUB_FRAME_LISTS_TO_QUEUE - 1;
		if (frameListToWrite >= NUM_ISUB_FRAME_LISTS) {
			frameListToWrite -= NUM_ISUB_FRAME_LISTS;
		}
		self->WriteFrameList (frameListToWrite);
	}

Exit:
	self->release ();
	return;
}

Generated by GNU enscript 1.6.4.