PlaySoundConvertBuffer.c

/*
    File:       PlaySoundConvertBuffer.c
    
    Description: Play Sound shows how to use the Sound Description Extention atom information with the
                 SoundConverter APIs to play non VBR MP3 files as well as other types of sound files
                 using various encoding methods.
 
    Author:     mc, era
 
    Copyright:  © Copyright 2000 Apple Computer, Inc. All rights reserved.
    
    Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple Computer, Inc.
                ("Apple") in consideration of your agreement to the following terms, and your
                use, installation, modification or redistribution of this Apple software
                constitutes acceptance of these terms.  If you do not agree with these terms,
                please do not use, install, modify or redistribute this Apple software.
 
                In consideration of your agreement to abide by the following terms, and subject
                to these terms, Apple grants you a personal, non-exclusive license, under AppleÕs
                copyrights in this original Apple software (the "Apple Software"), to use,
                reproduce, modify and redistribute the Apple Software, with or without
                modifications, in source and/or binary forms; provided that if you redistribute
                the Apple Software in its entirety and without modifications, you must retain
                this notice and the following text and disclaimers in all such redistributions of
                the Apple Software.  Neither the name, trademarks, service marks or logos of
                Apple Computer, Inc. may be used to endorse or promote products derived from the
                Apple Software without specific prior written permission from Apple.  Except as
                expressly stated in this notice, no other rights or licenses, express or implied,
                are granted by Apple herein, including but not limited to any patent rights that
                may be infringed by your derivative works or by other works in which the Apple
                Software may be incorporated.
 
                The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO
                WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
                WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
                PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
                COMBINATION WITH YOUR PRODUCTS.
 
                IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
                CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
                GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
                OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
                (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
                ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
                
    Change History (most recent first): <2> 7/26/00 sans hickup and carbonized for CarbonLib 1.04
                                        <1> 4/01/00 initial release
*/
 
#include "MP3Player.h"
 
// globals
Boolean gBufferDone = false;
 
// * ----------------------------
// MySoundCallBackFunction
//
// used to signal when a buffer is done playing
static pascal void MySoundCallBackFunction(SndChannelPtr theChannel, SndCommand *theCmd);
static pascal void MySoundCallBackFunction(SndChannelPtr theChannel, SndCommand *theCmd)
{
#pragma unused(theChannel)
    
    #ifndef TARGET_API_MAC_CARBON
        #if !GENERATINGCFM
            long oldA5;
            oldA5 = SetA5(theCmd->param2);
        #else
            #pragma unused(theCmd)
        #endif
    #else
        #pragma unused(theCmd)
    #endif // TARGET_API_MAC_CARBON
 
    gBufferDone = true;
 
    #ifndef TARGET_API_MAC_CARBON
        #if !GENERATINGCFM
            oldA5 = SetA5(oldA5);
        #endif
    #endif // TARGET_API_MAC_CARBON
}
 
// * ----------------------------
// MyGetSoundDescriptionExtension
//
// this function will extract the information needed to decompress the sound file, this includes retrieving the sample description,
// the decompression atom, setting up the sound header, copying the sample data into a sample buffer and calculating it's length
static OSErr MyGetSoundDescriptionExtension(const FSSpec *inMP3file, AudioFormatAtomPtr *outAudioAtom, CmpSoundHeaderPtr outSoundHeader, Handle outSampleBuf, UInt32 *outLength);
static OSErr MyGetSoundDescriptionExtension(const FSSpec *inMP3file, AudioFormatAtomPtr *outAudioAtom, CmpSoundHeaderPtr outSoundHeader, Handle outSampleBuf, UInt32 *outLength)
{
    Movie theMovie;
    Track theTrack;
    Media theMedia;
    short theRefNum;
    short theResID = 0; // we want the first movie
    Boolean wasChanged;
    
    OSErr err = noErr;
    
    // open the movie file
    err = OpenMovieFile(inMP3file, &theRefNum, fsRdPerm);
    BailErr(err);
 
    // instantiate the movie
    err = NewMovieFromFile(&theMovie, theRefNum, &theResID, NULL, newMovieActive, &wasChanged);
    BailErr(err);
    CloseMovieFile(theRefNum);
    theRefNum = 0;
        
    // get the first sound track
    theTrack = GetMovieIndTrackType(theMovie, 1, SoundMediaType, movieTrackMediaType);
    if (theTrack != NULL) {
    
        // get the sound track media
        theMedia = GetTrackMedia(theTrack);
        if (theMedia != NULL) {         
            Size size;
            Handle extension;
            
            // Version 1 of this record includes four extra fields to store information about compression ratios. It also defines
            // how other extensions are added to the SoundDescription record.
            // All other additions to the SoundDescription record are made using QT atoms. That means one or more
            // atoms can be appended to the end of the SoundDescription record using the standard [size, type]
            // mechanism used throughout the QuickTime movie resource architecture.
            // http://developer.apple.com/techpubs/quicktime/qtdevdocs/RM/frameset.htm
            SoundDescriptionV1Handle sourceSoundDescription = (SoundDescriptionV1Handle)NewHandle(0);
            
            // get the description of the sample data
            GetMediaSampleDescription(theMedia, 1, (SampleDescriptionHandle)sourceSoundDescription);
            err = GetMoviesError();
 
            extension = NewHandle(0);
            
            // get the "magic" decompression atom
            // This extension to the SoundDescription information stores data specific to a given audio decompressor.
            // Some audio decompression algorithms require a set of out-of-stream values to configure the decompressor
            // which are stored in a siDecompressionParams atom. The contents of the siDecompressionParams atom are dependent
            // on the audio decompressor.
            err = GetSoundDescriptionExtension((SoundDescriptionHandle)sourceSoundDescription, &extension, siDecompressionParams);
            
            if (noErr == err) {
                size = GetHandleSize(extension);
                HLock(extension);
                *outAudioAtom = (AudioFormatAtom*)NewPtr(size);
                err = MemError();
                // copy the atom data to our buffer...
                BlockMoveData(*extension, *outAudioAtom, size);
                HUnlock(extension);
            } else {
                // if it doesn't have an atom, that's ok
                err = noErr;
            }
            
            // set up our sound header
            outSoundHeader->format = (*sourceSoundDescription)->desc.dataFormat;
            outSoundHeader->numChannels = (*sourceSoundDescription)->desc.numChannels;
            outSoundHeader->sampleSize = (*sourceSoundDescription)->desc.sampleSize;
            outSoundHeader->sampleRate = (*sourceSoundDescription)->desc.sampleRate;
            
            if (noErr == err) {
                // copy the sample data into our buffer
                TimeValue trackDuration = GetTrackDuration(theTrack);   
                TimeValue startTime = 0;
                long numMediaSamples = GetMediaSampleCount(theMedia);
                long numberOfSamples;
                Handle hTemp = NewHandle(0);
                
                do {
                    err = GetMediaSample(theMedia, hTemp, 0L, NULL, startTime, NULL, 0, NULL, NULL, numMediaSamples, &numberOfSamples, NULL);
                    startTime += numberOfSamples;
                    HandAndHand(hTemp, outSampleBuf);
                } while (startTime < numMediaSamples && noErr == err );
                DisposeHandle(hTemp);
                
                // calculate the duration of the data
                
                // although we should be able to use the samplesPerPacket value returned in the SoundDescriptionV1 record to see if we're dealing with multiple samples per packet
                // as per the documentation, and I quote "If these fields are not used, they are set to 0. File readers only need to check to see if samplesPerPacket is 0."
                // in the case when a sound track is exported as a .mov using for example µLaw 2:1 compression these fields are filed with 'spore' aka 'grunge' so...
                // use GetCompressionInfo and check samplesPerPacket NOTE: Although better than nothing this will not always work, for example a .mov file with a SoundTrack
                // encoded using MACE where samples per packet will be greater than 1 but the information in the SoundDescriptionV1 record will be wrong
                CompressionInfo compInfo;
                err = GetCompressionInfo(fixedCompression, (*sourceSoundDescription)->desc.dataFormat, (*sourceSoundDescription)->desc.numChannels, (*sourceSoundDescription)->desc.sampleSize, &compInfo);
                
                // numFrames = samples / samplesPerPacket
                // duration = numFrames * bytesPerFrame
                if (compInfo.samplesPerPacket == 1) {
                    *outLength = (((numMediaSamples / compInfo.samplesPerPacket) * compInfo.bytesPerFrame));
                } else {
                    *outLength = (((numMediaSamples / (*sourceSoundDescription)->samplesPerPacket) * (*sourceSoundDescription)->bytesPerFrame));
                }
            }
 
            DisposeHandle(extension);
            DisposeHandle((Handle)sourceSoundDescription);
        }
    }
    
bail:
    return err;
}
 
// * ----------------------------
// PlaySound
//
// this function does the actual work of playing the sound file, it sets up the sound converter environment, allocates play buffers,
// creates the sound channel and sends the appropriate sound commands to play the converted sound data
OSErr PlaySound(const FSSpec *inFileToPlay)
{
    Handle                  hSoundData = NULL;
    UInt32                  theLength = 0;
    
    AudioCompressionAtomPtr theDecompressionAtom;
    SoundComponentData      theInputFormat,
                            theOutputFormat;
    SoundConverter          mySoundConverter = NULL;
    CmpSoundHeader          mySndHeader0,
                            mySndHeader1;
 
    Ptr                     pSourceBuffer = NULL;
    Ptr                     pDecomBuffer0 = NULL,
                            pDecomBuffer1 = NULL;
                        
    Boolean                 isSoundDone = false;
    FInfo                   fndrInfo;
    
    OSErr                   err = noErr;
 
    err = FSpGetFInfo(inFileToPlay, &fndrInfo);
    BailErr(err);
    
    hSoundData = NewHandle(0);
    if (hSoundData == NULL || MemError()) goto bail;
    
    // get what we need to do what we need to do
    err = MyGetSoundDescriptionExtension(inFileToPlay, (AudioFormatAtomPtr *)&theDecompressionAtom, &mySndHeader0, hSoundData, &theLength);
    if (noErr == err) {
        HLock(hSoundData);
        pSourceBuffer = *hSoundData;    // source buffer
 
        // setup input/output format for sound converter
        theInputFormat.flags = 0;
        theInputFormat.format = mySndHeader0.format;
        theInputFormat.numChannels = mySndHeader0.numChannels;
        theInputFormat.sampleSize = mySndHeader0.sampleSize;
        theInputFormat.sampleRate = mySndHeader0. sampleRate;
        theInputFormat.sampleCount = 0;
        theInputFormat.buffer = NULL;
        theInputFormat.reserved = 0;
 
        theOutputFormat.flags = 0;
        theOutputFormat.format = kSoundNotCompressed;
        theOutputFormat.numChannels = theInputFormat.numChannels;
        theOutputFormat.sampleSize = theInputFormat.sampleSize;
        theOutputFormat.sampleRate = theInputFormat.sampleRate;
        theOutputFormat.sampleCount = 0;
        theOutputFormat.buffer = NULL;
        theOutputFormat.reserved = 0;
 
        err = SoundConverterOpen(&theInputFormat, &theOutputFormat, &mySoundConverter);
        BailErr(err);
 
        // set up the sound converters decompresson 'environment' by passing in the 'magic' decompression atom
        err = SoundConverterSetInfo(mySoundConverter, siDecompressionParams, theDecompressionAtom);
        if (siUnknownInfoType == err) {
            // clear this error, the decompressor didn't
            // need the decompression atom and that's OK
            err = noErr;
        } else BailErr(err);
        
        UInt32  targetBytes = 32768,
                inputFrames = 0,
                inputBytes = 0,
                outputFrames = 0,
                outputBytes = 0,
                actualOutputBytes = 0,
                bytesConverted = 0,
                bytesPerFrame = 0;
            
        // find out how much buffer space to alocate for our output buffers
        do {
            targetBytes *= 2;
            err = SoundConverterGetBufferSizes(mySoundConverter, targetBytes, &inputFrames, &inputBytes, &outputBytes);
        } while (notEnoughBufferSpace == err  && targetBytes < (MaxBlock() / 4));
        
        bytesPerFrame = inputBytes / inputFrames;
 
        pDecomBuffer0 = NewPtr(outputBytes + kOverflowRoom);
        BailErr(MemError(););
        
        pDecomBuffer1 = NewPtr(outputBytes + kOverflowRoom);
        BailErr(MemError(););
 
        // convert two buffers of sound before we begin to do anything else
        err = SoundConverterBeginConversion(mySoundConverter);
        BailErr(err);
 
        // setup first header
        mySndHeader0.samplePtr = pDecomBuffer0;
        mySndHeader0.numChannels = theOutputFormat.numChannels;
        mySndHeader0.sampleRate = theOutputFormat.sampleRate;
        mySndHeader0.loopStart = 0;
        mySndHeader0.loopEnd = 0;
        mySndHeader0.encode = cmpSH;                    // compressed sound header encode value
        mySndHeader0.baseFrequency = kMiddleC;
        // mySndHeader0.AIFFSampleRate;                 // this is not used
        mySndHeader0.markerChunk = NULL;
        mySndHeader0.format = theOutputFormat.format;
        mySndHeader0.futureUse2 = 0;
        mySndHeader0.stateVars = NULL;
        mySndHeader0.leftOverSamples = NULL;
        mySndHeader0.compressionID = fixedCompression;  // compression ID for fixed-sized compression, even uncompressed sounds use fixedCompression
        mySndHeader0.packetSize = 0;                    // the Sound Manager will figure this out for us
        mySndHeader0.snthID = 0;
        mySndHeader0.sampleSize = theOutputFormat.sampleSize;
        mySndHeader0.sampleArea[0] = 0;                 // no samples here because we use samplePtr to point to our buffer instead
 
        // setup second header, only the buffer ptr is different
        BlockMoveData(&mySndHeader0, &mySndHeader1, sizeof(mySndHeader0));
        mySndHeader1.samplePtr = pDecomBuffer1;
 
        bytesConverted = 0;
        err = SoundConverterConvertBuffer(mySoundConverter, pSourceBuffer, inputFrames, pDecomBuffer0, &outputFrames, &actualOutputBytes);
        BailErr(err);
        if (actualOutputBytes > outputBytes) DebugStr("\pYikes! Overflowed the outputbuffer");
        bytesConverted += inputBytes;
        mySndHeader0.numFrames = outputFrames;
 
        if (bytesConverted > theLength) {
            isSoundDone = true;
            inputBytes = 0;
            inputFrames = 0;
        } else if (bytesConverted + inputBytes > theLength) {
            isSoundDone = true;
            inputBytes = theLength - bytesConverted;
            inputFrames = inputBytes / bytesPerFrame;
        }
        
        err = SoundConverterConvertBuffer(mySoundConverter, pSourceBuffer + bytesConverted, inputFrames, pDecomBuffer1, &outputFrames, &actualOutputBytes);
        BailErr(err);
        if (actualOutputBytes > outputBytes) DebugStr("\pYikes! Overflowed the outputbuffer");
        bytesConverted += inputBytes;
        mySndHeader1.numFrames = outputFrames;
 
        // setup the callback, create the sound channel and play the sound
        // we will continue to convert the sound data into the free (non playing) buffer
        SndCallBackUPP theSoundCallBackUPP = NewSndCallBackUPP(MySoundCallBackFunction);
        SndChannelPtr pSoundChannel = NULL;
        
        err = SndNewChannel(&pSoundChannel, sampledSynth, 0, theSoundCallBackUPP);
 
        if (err == noErr) {
            
            SndCommand      thePlayCmd0,
                            thePlayCmd1,
                            theCallBackCmd;
            SndCommand      *pPlayCmd;
            eBufferNumber   whichBuffer = kFirstBuffer;
            
            thePlayCmd0.cmd = bufferCmd;
            thePlayCmd0.param1 = 0;                     // not used, but clear it out anyway just to be safe
            thePlayCmd0.param2 = (long)&mySndHeader0;
 
            thePlayCmd1.cmd = bufferCmd;
            thePlayCmd1.param1 = 0;                     // not used, but clear it out anyway just to be safe
            thePlayCmd1.param2 = (long)&mySndHeader1;
                        
            whichBuffer = kFirstBuffer;             // buffer 1 will be free when callback runs
            theCallBackCmd.cmd = callBackCmd;
            theCallBackCmd.param2 = SetCurrentA5();
 
            gBufferDone = false;
            err = SndDoCommand(pSoundChannel, &thePlayCmd0, true);
 
            if (noErr == err) {
                err = SndDoCommand(pSoundChannel, &theCallBackCmd, true);
            }
 
            if (noErr == err) {
                err = SndDoCommand(pSoundChannel, &thePlayCmd1, true);
            }
            
            Ptr pDecomBuffer = NULL;
            CmpSoundHeaderPtr pSndHeader = NULL;
            
            if (noErr == err) {
                while (!isSoundDone && !Button()) {                     
                    if (gBufferDone == true) {
                        if (kFirstBuffer == whichBuffer) {
                            pPlayCmd = &thePlayCmd0;
                            pDecomBuffer = pDecomBuffer0;
                            pSndHeader = &mySndHeader0;
                            whichBuffer = kSecondBuffer;
                        } else {
                            pPlayCmd = &thePlayCmd1;
                            pDecomBuffer = pDecomBuffer1;
                            pSndHeader = &mySndHeader1;
                            whichBuffer = kFirstBuffer;
                        }
 
                        if (bytesConverted < theLength) {
                            if (bytesConverted + inputBytes >= theLength) {
                                isSoundDone = true;
                                inputBytes = theLength - bytesConverted;
                                inputFrames = inputBytes / bytesPerFrame;
                            }
 
                            err = SoundConverterConvertBuffer(mySoundConverter, pSourceBuffer + bytesConverted, inputFrames, pDecomBuffer, &outputFrames, &actualOutputBytes);
                            if (err) break;
                            if (actualOutputBytes > outputBytes) DebugStr("\pYikes! Overflowed the outputbuffer");
                            bytesConverted += inputBytes;
                            pSndHeader->numFrames = outputFrames;
 
                            gBufferDone = false;
                            if (!isSoundDone) {
                                SndDoCommand(pSoundChannel, &theCallBackCmd, true); // reuse callBackCmd
                            }
                            
                            SndDoCommand(pSoundChannel, pPlayCmd, true);            // play the next buffer
                        }
                    }
                } // while
            }
            
            SoundConverterEndConversion(mySoundConverter, pDecomBuffer, &outputFrames, &outputBytes);
 
            if (noErr == err && outputFrames) {
                pSndHeader->numFrames = outputFrames;
                SndDoCommand(pSoundChannel, pPlayCmd, true);    // play the last buffer.
            }
        }
 
        if (pSoundChannel)
            err = SndDisposeChannel(pSoundChannel, false);      // wait until sounds stops playing before disposing of channel
                
        if (theSoundCallBackUPP)
            DisposeSndCallBackUPP(theSoundCallBackUPP);
    }
            
bail:
    if (mySoundConverter)
        SoundConverterClose(mySoundConverter);
        
    if (pDecomBuffer0)
        DisposePtr(pDecomBuffer0);
        
    if (pDecomBuffer1)
        DisposePtr(pDecomBuffer1);
        
    if (hSoundData)
        DisposeHandle(hSoundData);
 
    return err;
}