Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
MixMash.mm
/* Copyright © 2007 Apple Inc. All Rights Reserved. |
Disclaimer: IMPORTANT: This Apple software is supplied to you by |
Apple 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 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. |
*/ |
#import "MixMash.h" |
#include <AudioUnit/AudioUnit.h> |
#include <vector> |
#include "CAComponentDescription.h" |
#include "CAXException.h" |
#include "CAStreamBasicDescription.h" |
#define kAudioPlaybackSampleRate 44100 |
#define kDefaultTotalTime 30 |
#define kDefaultMinSliceLength 0.050 |
#define kDefaultMaxSliceLength 0.250 |
OSStatus FilePlayerRenderCallback( |
void * inRefCon, |
AudioUnitRenderActionFlags * ioActionFlags, |
const AudioTimeStamp * inTimeStamp, |
UInt32 inBusNumber, |
UInt32 inNumberFrames, |
AudioBufferList * ioData) |
{ |
MixMash *This = (MixMash *)inRefCon; |
if (!This->mHaveStartTime) { |
AudioTimeStamp ts; |
UInt32 propertySize = sizeof(ts); |
OSStatus err = AudioUnitGetProperty(This->mPlayerUnit, kAudioUnitProperty_ScheduleStartTimeStamp, kAudioUnitScope_Global, 0, &ts, &propertySize); |
if (!err && (ts.mFlags & kAudioTimeStampSampleTimeValid) && ts.mSampleTime >= 0.) { |
This->mStartSample = ts.mSampleTime; |
This->mHaveStartTime = YES; |
} |
} |
This->mCurrentTime = inTimeStamp->mSampleTime; |
return noErr; |
} |
@implementation MixMash |
- (id)init |
{ |
self = [super init]; |
if (self) { |
mTotalLength = kDefaultTotalTime; |
mMinSliceLength = kDefaultMinSliceLength; |
mMaxSliceLength = kDefaultMaxSliceLength; |
mAudioFiles = new AudioFileList; |
mRegionList = new RegionList; |
mPlayRow = -1; |
AUNode theOutputNode, thePlayerNode; |
AudioUnit theOutputUnit; |
CAStreamBasicDescription theAudioFormat; |
// create and open the graph |
XThrowIfError( |
NewAUGraph(&mGraph), |
"NewAUGraph failed"); |
XThrowIfError( |
AUGraphOpen(mGraph), |
"couldn't open graph"); |
// add an output unit |
CAComponentDescription outputDesc(kAudioUnitType_Output, kAudioUnitSubType_DefaultOutput, kAudioUnitManufacturer_Apple); |
XThrowIfError( |
AUGraphAddNode(mGraph, &outputDesc, &theOutputNode), |
"couldn't create node for output unit"); |
XThrowIfError( |
AUGraphNodeInfo(mGraph, theOutputNode, NULL, &theOutputUnit), |
"couldn't get output unit from node"); |
// add a file player |
CAComponentDescription playerDesc(kAudioUnitType_Generator, kAudioUnitSubType_AudioFilePlayer, kAudioUnitManufacturer_Apple); |
XThrowIfError( |
AUGraphAddNode(mGraph, &playerDesc, &thePlayerNode), |
"couldn't create node for file player"); |
XThrowIfError( |
AUGraphNodeInfo(mGraph, thePlayerNode, NULL, &mPlayerUnit), |
"couldn't get player unit from node"); |
//set the render callback |
XThrowIfError( |
AudioUnitAddRenderNotify(mPlayerUnit, FilePlayerRenderCallback, self), |
"couldn't set file player render callback"); |
// Make the player's output format -- in terms of sample rate and number of channels. |
// The player will convert to deinterleaved float. |
theAudioFormat.mSampleRate = kAudioPlaybackSampleRate; |
theAudioFormat.SetCanonical(2, false /* deinterleaved */); |
XThrowIfError( |
AudioUnitSetProperty(mPlayerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &theAudioFormat, sizeof(CAStreamBasicDescription)), |
"couldn't set player's output stream format"); |
// connect the player to the output unit (stream format will propagate) |
XThrowIfError( |
AUGraphConnectNodeInput(mGraph, thePlayerNode, 0, theOutputNode, 0), |
"couldn't connect mixer to output unit"); |
// initialize the AUGraph |
XThrowIfError( |
AUGraphInitialize(mGraph), |
"couldn't initialize graph"); |
XThrowIfError(AUGraphStart(mGraph), "couldn't start graph"); |
// set the image to display the current file being played |
mSpeakerImage = [NSImage imageNamed: @"Speaker.tiff"]; |
// set a default path for sources |
mSourcePath = @"/System/Library/Sounds"; |
} |
return self; |
} |
- (void)awakeFromNib |
{ |
[mFolderName setStringValue: [mSourcePath lastPathComponent]]; |
} |
- (void)closeFiles |
{ |
for (AudioFileList::iterator it = mAudioFiles->begin(); it != mAudioFiles->end(); ++it) { |
AudioFileClose((*it).fileid); |
[(*it).description release]; |
} |
mAudioFiles->clear(); |
} |
- (void)dealloc |
{ |
[self closeFiles]; |
delete mAudioFiles; |
delete mRegionList; |
[super dealloc]; |
} |
class CmpRgnTime { |
public: |
bool operator () (Float64 t, ScheduledAudioFileRegion &a) |
{ |
return a.mTimeStamp.mSampleTime > t; |
} |
}; |
- (void)updatePlayLoc |
{ |
if (!mHaveStartTime) |
return; |
Float64 pos = mCurrentTime - mStartSample; |
int row; |
if (pos >= mTotalLengthSamples) |
row = -1; |
else { |
RegionList::iterator i = std::upper_bound(mRegionList->begin(), mRegionList->end(), pos, CmpRgnTime()); |
row = i - mRegionList->begin() - 1; |
} |
if (row != mPlayRow) { |
mPlayRow = row; |
if (mPlayRow >= 0) |
[mPlaylistTable scrollRowToVisible: mPlayRow]; |
[mPlaylistTable reloadData]; |
if (row == -1) |
[self stop]; |
} |
} |
- (void)play |
{ |
if (mPlaying) return; |
OSStatus err; |
AudioUnitReset(mPlayerUnit, kAudioUnitScope_Global, 0); |
// give the player the array of AudioFileIDs it will be playing |
AudioFileID *fileids = new AudioFileID[mAudioFiles->size()], *pf = fileids; |
for (AudioFileList::iterator fi = mAudioFiles->begin(); fi != mAudioFiles->end(); ++fi) |
*pf++ = (*fi).fileid; |
XThrowIfError( |
AudioUnitSetProperty(mPlayerUnit, kAudioUnitProperty_ScheduledFileIDs, kAudioUnitScope_Global, 0, fileids, mAudioFiles->size() * sizeof(AudioFileID)), |
"couldn't set the player's list of AudioFileID's"); |
delete[] fileids; |
// schedule events |
for (RegionList::iterator it = mRegionList->begin(); it != mRegionList->end(); ++it) { |
err = AudioUnitSetProperty(mPlayerUnit, kAudioUnitProperty_ScheduledFileRegion, kAudioUnitScope_Global, 0, &(*it), sizeof(ScheduledAudioFileRegion)); |
if (err) |
printf("schedule region: error %ld\n", (long int)err); |
} |
UInt32 primeFrames = 0; // default |
AudioUnitSetProperty(mPlayerUnit, kAudioUnitProperty_ScheduledFilePrime, kAudioUnitScope_Global, 0, &primeFrames, sizeof(primeFrames)); |
AudioTimeStamp startTime; |
startTime.mFlags = kAudioTimeStampSampleTimeValid; |
startTime.mSampleTime = -1; |
AudioUnitSetProperty(mPlayerUnit, kAudioUnitProperty_ScheduleStartTimeStamp, kAudioUnitScope_Global, 0, &startTime, sizeof(startTime)); |
mPlaying = YES; |
mHaveStartTime = NO; |
[mPlayButton setTitle: @"Stop"]; |
mPlayTimer = [NSTimer scheduledTimerWithTimeInterval:0.040 target:self selector:@selector(updatePlayLoc) |
userInfo:nil repeats:YES]; |
} |
- (void)stop |
{ |
if (!mPlaying) return; |
AudioUnitReset(mPlayerUnit, kAudioUnitScope_Global, 0); |
mPlaying = NO; |
[mPlayButton setTitle: @"Play"]; |
mPlayRow = -1; |
[mPlayTimer invalidate]; |
mPlayTimer = nil; |
} |
static int randChoice(int n) |
{ |
return random() % n; |
} |
static double frand() |
{ |
return (double)random() / (double)RAND_MAX; |
} |
- (void)findAudioFilesInFolder: (NSString *)folder fileList: (NSMutableArray *)files |
{ |
NSFileManager *fm = [NSFileManager defaultManager]; |
NSArray *subs = [fm directoryContentsAtPath: folder]; |
NSEnumerator *e = [subs objectEnumerator]; |
NSString *s; |
while ((s = [e nextObject]) != nil) { |
NSString *path = [folder stringByAppendingPathComponent: s]; |
BOOL isDir; |
if ([fm fileExistsAtPath: path isDirectory: &isDir]) { |
if (isDir) |
[self findAudioFilesInFolder: path fileList: files]; |
else { |
const char *ext = [[s pathExtension] cStringUsingEncoding: NSASCIIStringEncoding]; |
if (!strcmp(ext, "aif") || !strcmp(ext, "wav") || !strcmp(ext, "mp3") |
|| !strcmp(ext, "aiff") || !strcmp(ext, "sd2") || !strcmp(ext, "aac")) { |
[files addObject: path]; |
} |
} |
} |
} |
} |
- (NSMutableArray *)audioFilesInFolder: (NSString *)folder |
{ |
NSMutableArray *files = [[[NSMutableArray alloc] init] autorelease]; |
[self findAudioFilesInFolder: folder fileList: files]; |
return files; |
} |
- (IBAction)generatePlaylist: (id)sender |
{ |
[self stop]; |
[self closeFiles]; |
mRegionList->clear(); |
Float64 playTime = 0; |
Float64 endTime = mTotalLength * 44100; |
NSMutableArray *paths; |
paths = [self audioFilesInFolder: mSourcePath]; |
if ([paths count] == 0) |
return; |
bool gotTooManyFilesOpen = false; |
mTotalLengthSamples = 0; |
while (playTime < endTime) { |
// pick a random file |
AudioFile af; |
if (!gotTooManyFilesOpen && [paths count]) { |
// from a file we haven't played from |
int i = randChoice([paths count]); |
NSString *filePath = [paths objectAtIndex: i]; |
// remove it from the list so we don't pick again |
[paths removeObjectAtIndex: i]; |
printf("file: %s\n", [filePath cStringUsingEncoding: NSASCIIStringEncoding]); |
// try to open the audio file |
OSStatus err; |
FSRef fsref; |
err = FSPathMakeRef((Byte *)[filePath UTF8String], &fsref, NULL); |
if (err) { printf("FSPathMakeRef: %ld\n", (long int)err); continue; } |
err = AudioFileOpen(&fsref, fsRdPerm, 0, &af.fileid); |
if (err) { |
if (err == tmfoErr) { |
gotTooManyFilesOpen = true; |
continue; |
} |
printf("AudioFileOpen: %ld\n", (long int)err); continue; |
} |
// get its sample rate and duration |
UInt32 propsize = sizeof(af.filefmt); |
err = AudioFileGetProperty(af.fileid, kAudioFilePropertyDataFormat, &propsize, &af.filefmt); |
if (err || af.filefmt.mFramesPerPacket == 0 /* no VBR */) { |
printf("get data format: err=%ld framesperpacket=%ld\n", (long int)err, (long int)af.filefmt.mFramesPerPacket); |
AudioFileClose(af.fileid); |
continue; |
} |
propsize = sizeof(UInt64); |
err = AudioFileGetProperty(af.fileid, kAudioFilePropertyAudioDataPacketCount, &propsize, &af.npackets); |
if (err || af.npackets == 0) { |
printf("get data packet count: err=%ld npackets=%qd\n", (long int)err, af.npackets); |
AudioFileClose(af.fileid); |
continue; |
} |
// generate description |
char buf[64]; |
if (af.filefmt.mChannelsPerFrame > 2) |
sprintf(buf, "%ld ch", (long int)af.filefmt.mChannelsPerFrame); |
else |
strcpy(buf, af.filefmt.mChannelsPerFrame == 2 ? "stereo" : "mono"); |
if (af.filefmt.mFormatID == kAudioFormatLinearPCM) { |
sprintf(buf + strlen(buf), ", %ld-bit %s", (long int)af.filefmt.mBitsPerChannel, (af.filefmt.mFormatFlags & kLinearPCMFormatFlagIsFloat) ? "float" : "integer"); |
} |
if (af.filefmt.mSampleRate != 0) |
sprintf(buf + strlen(buf), ", %.f Hz", af.filefmt.mSampleRate); |
af.description = [[NSString stringWithFormat: @"%@\n%s", [filePath lastPathComponent], buf] retain]; |
mAudioFiles->push_back(af); |
} else if (mAudioFiles->size() == 0) { |
break; |
} else { |
// from a file we've already played from |
int i = randChoice(mAudioFiles->size()); |
af = (*mAudioFiles)[i]; |
} |
Float64 fileDuration = (af.npackets * af.filefmt.mFramesPerPacket) / af.filefmt.mSampleRate; |
Float64 sliceDuration = std::min(frand() * (mMaxSliceLength - mMinSliceLength) + mMinSliceLength, fileDuration); |
Float64 sliceStart = frand() * (fileDuration - sliceDuration); |
ScheduledAudioFileRegion rgn; |
rgn.mTimeStamp.mFlags = kAudioTimeStampSampleTimeValid; |
rgn.mTimeStamp.mSampleTime = playTime; |
rgn.mCompletionProc = NULL; |
rgn.mCompletionProcUserData = NULL; |
rgn.mAudioFile = af.fileid; |
rgn.mLoopCount = 0; |
rgn.mStartFrame = SInt64(sliceStart * af.filefmt.mSampleRate); |
rgn.mFramesToPlay = UInt32(sliceDuration * af.filefmt.mSampleRate); |
printf(" t: %.3f : %ld frames @ frame %qd\n", playTime / 44100., (long int)rgn.mFramesToPlay, rgn.mStartFrame); |
mRegionList->push_back(rgn); |
playTime += ceil(sliceDuration * kAudioPlaybackSampleRate); |
[mPlaylistTable reloadData]; |
[mPlaylistTable scrollRowToVisible: mRegionList->size() - 1]; |
[mPlaylistTable displayIfNeeded]; |
} |
mTotalLengthSamples = playTime; |
[mPlaylistTable scrollRowToVisible: 0]; |
} |
- (IBAction)togglePlay: (id)sender |
{ |
if (mPlaying) { |
[self stop]; |
} else { |
[self play]; |
} |
} |
- (int)numberOfRowsInTableView:(NSTableView *)tableView |
{ |
return mRegionList->size(); |
} |
NSString *formatSample(UInt64 sample, Float64 rate, bool millis) |
{ |
char buf[32]; |
double seconds = double(sample) / rate; |
int milliseconds = int(seconds * 1000 + 0.5); |
int minutes = milliseconds / 60000; |
milliseconds %= 60000; |
int isec = milliseconds / 1000; |
milliseconds %= 1000; |
if (minutes) |
sprintf(buf, "%2d:%02d.%03d", minutes, isec, milliseconds); |
else |
sprintf(buf, " %2d.%03d", isec, milliseconds); |
return [NSString stringWithUTF8String: buf]; |
} |
- (AudioFile *)FileForRegion: (ScheduledAudioFileRegion *)rgn |
{ |
AudioFile *af = NULL; |
for (AudioFileList::iterator i = mAudioFiles->begin(); i != mAudioFiles->end(); ++i) |
if ((*i).fileid == rgn->mAudioFile) { |
af = &(*i); |
break; |
} |
return af; |
} |
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row |
{ |
ScheduledAudioFileRegion *rgn = &(*mRegionList)[row]; |
AudioFile *af = [self FileForRegion: rgn]; |
if (af == NULL) |
return nil; |
if (tableColumn == mPlayLocColumn) { |
return (row == mPlayRow) ? mSpeakerImage : nil; |
} else if (tableColumn == mFileColumn) { |
return af->description; |
} else if (tableColumn == mStartColumn) { |
int seconds = int(rgn->mStartFrame / af->filefmt.mSampleRate + 0.5); |
int minutes = seconds / 60; |
seconds %= 60; |
return [NSString stringWithFormat: @"%d:%02d", minutes, seconds]; |
} else if (tableColumn == mLengthColumn) { |
double s = rgn->mFramesToPlay / af->filefmt.mSampleRate; |
return [NSString stringWithFormat: @"%.3f", s]; |
} |
return nil; |
} |
// optional |
- (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(int)row |
{ |
ScheduledAudioFileRegion *rgn = &(*mRegionList)[row]; |
AudioFile *af = [self FileForRegion: rgn]; |
if (af == NULL) |
return; |
if (tableColumn == mLengthColumn) { |
double s = [object doubleValue]; |
rgn->mFramesToPlay = UInt32(s * af->filefmt.mSampleRate); |
} |
} |
- (IBAction)setFolder: (id)sender |
{ |
NSOpenPanel *panel = [NSOpenPanel openPanel]; |
[panel setCanChooseDirectories: YES]; |
[panel setCanChooseFiles: NO]; |
if ([panel runModalForDirectory: mSourcePath file: nil types: nil] == NSOKButton) { |
[mSourcePath release]; |
mSourcePath = [[panel filename] retain]; |
[mFolderName setStringValue: [mSourcePath lastPathComponent]]; |
} |
} |
@end |
Copyright © 2009 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2009-04-15