--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+
+#import "CocosDenshion.h"
+#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 30000
+ #import <AVFoundation/AVFoundation.h>
+#else
+ #import "CDXMacOSXSupport.h"
+#endif
+
+/** Different modes of the engine */
+typedef enum {
+ kAMM_FxOnly, //!Other apps will be able to play audio
+ kAMM_FxPlusMusic, //!Only this app will play audio
+ kAMM_FxPlusMusicIfNoOtherAudio, //!If another app is playing audio at start up then allow it to continue and don't play music
+ kAMM_MediaPlayback, //!This app takes over audio e.g music player app
+ kAMM_PlayAndRecord //!App takes over audio and has input and output
+} tAudioManagerMode;
+
+/** Possible states of the engine */
+typedef enum {
+ kAMStateUninitialised, //!Audio manager has not been initialised - do not use
+ kAMStateInitialising, //!Audio manager is in the process of initialising - do not use
+ kAMStateInitialised //!Audio manager is initialised - safe to use
+} tAudioManagerState;
+
+typedef enum {
+ kAMRBDoNothing, //Audio manager will not do anything on resign or becoming active
+ kAMRBStopPlay, //Background music is stopped on resign and resumed on become active
+ kAMRBStop //Background music is stopped on resign but not resumed - maybe because you want to do this from within your game
+} tAudioManagerResignBehavior;
+
+/** Notifications */
+extern NSString * const kCDN_AudioManagerInitialised;
+
+@interface CDAsynchInitialiser : NSOperation {}
+@end
+
+/** CDAudioManager supports two long audio source channels called left and right*/
+typedef enum {
+ kASC_Left = 0,
+ kASC_Right = 1
+} tAudioSourceChannel;
+
+typedef enum {
+ kLAS_Init,
+ kLAS_Loaded,
+ kLAS_Playing,
+ kLAS_Paused,
+ kLAS_Stopped,
+} tLongAudioSourceState;
+
+@class CDLongAudioSource;
+@protocol CDLongAudioSourceDelegate <NSObject>
+@optional
+/** The audio source completed playing */
+- (void) cdAudioSourceDidFinishPlaying:(CDLongAudioSource *) audioSource;
+/** The file used to load the audio source has changed */
+- (void) cdAudioSourceFileDidChange:(CDLongAudioSource *) audioSource;
+@end
+
+/**
+ CDLongAudioSource represents an audio source that has a long duration which makes
+ it costly to load into memory for playback as an effect using CDSoundEngine. Examples
+ include background music and narration tracks. The audio file may or may not be compressed.
+ Bear in mind that current iDevices can only use hardware to decode a single compressed
+ audio file at a time and playing multiple compressed files will result in a performance drop
+ as software decompression will take place.
+ @since v0.99
+ */
+@interface CDLongAudioSource : NSObject <AVAudioPlayerDelegate, CDAudioInterruptProtocol>{
+ AVAudioPlayer *audioSourcePlayer;
+ NSString *audioSourceFilePath;
+ NSInteger numberOfLoops;
+ float volume;
+ id<CDLongAudioSourceDelegate> delegate;
+ BOOL mute;
+ BOOL enabled_;
+ BOOL backgroundMusic;
+@public
+ BOOL systemPaused;//Used for auto resign handling
+ NSTimeInterval systemPauseLocation;//Used for auto resign handling
+@protected
+ tLongAudioSourceState state;
+}
+@property (readonly) AVAudioPlayer *audioSourcePlayer;
+@property (readonly) NSString *audioSourceFilePath;
+@property (readwrite, nonatomic) NSInteger numberOfLoops;
+@property (readwrite, nonatomic) float volume;
+@property (assign) id<CDLongAudioSourceDelegate> delegate;
+/* This long audio source functions as background music */
+@property (readwrite, nonatomic) BOOL backgroundMusic;
+
+/** Loads the file into the audio source */
+-(void) load:(NSString*) filePath;
+/** Plays the audio source */
+-(void) play;
+/** Stops playing the audio soruce */
+-(void) stop;
+/** Pauses the audio source */
+-(void) pause;
+/** Rewinds the audio source */
+-(void) rewind;
+/** Resumes playing the audio source if it was paused */
+-(void) resume;
+/** Returns whether or not the audio source is playing */
+-(BOOL) isPlaying;
+
+@end
+
+/**
+ CDAudioManager manages audio requirements for a game. It provides access to a CDSoundEngine object
+ for playing sound effects. It provides access to two CDLongAudioSource object (left and right channel)
+ for playing long duration audio such as background music and narration tracks. Additionally it manages
+ the audio session to take care of things like audio session interruption and interacting with the audio
+ of other apps that are running on the device.
+
+ Requirements:
+ - Firmware: OS 2.2 or greater
+ - Files: CDAudioManager.*, CocosDenshion.*
+ - Frameworks: OpenAL, AudioToolbox, AVFoundation
+ @since v0.8
+ */
+@interface CDAudioManager : NSObject <CDLongAudioSourceDelegate, CDAudioInterruptProtocol, AVAudioSessionDelegate> {
+ CDSoundEngine *soundEngine;
+ CDLongAudioSource *backgroundMusic;
+ NSMutableArray *audioSourceChannels;
+ NSString* _audioSessionCategory;
+ BOOL _audioWasPlayingAtStartup;
+ tAudioManagerMode _mode;
+ SEL backgroundMusicCompletionSelector;
+ id backgroundMusicCompletionListener;
+ BOOL willPlayBackgroundMusic;
+ BOOL _mute;
+ BOOL _resigned;
+ BOOL _interrupted;
+ BOOL _audioSessionActive;
+ BOOL enabled_;
+
+ //For handling resign/become active
+ BOOL _isObservingAppEvents;
+ tAudioManagerResignBehavior _resignBehavior;
+}
+
+@property (readonly) CDSoundEngine *soundEngine;
+@property (readonly) CDLongAudioSource *backgroundMusic;
+@property (readonly) BOOL willPlayBackgroundMusic;
+
+/** Returns the shared singleton */
++ (CDAudioManager *) sharedManager;
++ (tAudioManagerState) sharedManagerState;
+/** Configures the shared singleton with a mode*/
++ (void) configure: (tAudioManagerMode) mode;
+/** Initializes the engine asynchronously with a mode */
++ (void) initAsynchronously: (tAudioManagerMode) mode;
+/** Initializes the engine synchronously with a mode, channel definition and a total number of channels */
+- (id) init: (tAudioManagerMode) mode;
+-(void) audioSessionInterrupted;
+-(void) audioSessionResumed;
+-(void) setResignBehavior:(tAudioManagerResignBehavior) resignBehavior autoHandle:(BOOL) autoHandle;
+/** Returns true is audio is muted at a hardware level e.g user has ringer switch set to off */
+-(BOOL) isDeviceMuted;
+/** Returns true if another app is playing audio such as the iPod music player */
+-(BOOL) isOtherAudioPlaying;
+/** Sets the way the audio manager interacts with the operating system such as whether it shares output with other apps or obeys the mute switch */
+-(void) setMode:(tAudioManagerMode) mode;
+/** Shuts down the shared audio manager instance so that it can be reinitialised */
++(void) end;
+
+/** Call if you want to use built in resign behavior but need to do some additional audio processing on resign active. */
+- (void) applicationWillResignActive;
+/** Call if you want to use built in resign behavior but need to do some additional audio processing on become active. */
+- (void) applicationDidBecomeActive;
+
+//New AVAudioPlayer API
+/** Loads the data from the specified file path to the channel's audio source */
+-(CDLongAudioSource*) audioSourceLoad:(NSString*) filePath channel:(tAudioSourceChannel) channel;
+/** Retrieves the audio source for the specified channel */
+-(CDLongAudioSource*) audioSourceForChannel:(tAudioSourceChannel) channel;
+
+//Legacy AVAudioPlayer API
+/** Plays music in background. The music can be looped or not
+ It is recommended to use .aac files as background music since they are decoded by the device (hardware).
+ */
+-(void) playBackgroundMusic:(NSString*) filePath loop:(BOOL) loop;
+/** Preloads a background music */
+-(void) preloadBackgroundMusic:(NSString*) filePath;
+/** Stops playing the background music */
+-(void) stopBackgroundMusic;
+/** Pauses the background music */
+-(void) pauseBackgroundMusic;
+/** Rewinds the background music */
+-(void) rewindBackgroundMusic;
+/** Resumes playing the background music */
+-(void) resumeBackgroundMusic;
+/** Returns whether or not the background music is playing */
+-(BOOL) isBackgroundMusicPlaying;
+
+-(void) setBackgroundMusicCompletionListener:(id) listener selector:(SEL) selector;
+
+@end
+
+/** Fader for long audio source objects */
+@interface CDLongAudioSourceFader : CDPropertyModifier{}
+@end
+
+static const int kCDNoBuffer = -1;
+
+/** Allows buffers to be associated with file names */
+@interface CDBufferManager:NSObject{
+ NSMutableDictionary* loadedBuffers;
+ NSMutableArray *freedBuffers;
+ CDSoundEngine *soundEngine;
+ int nextBufferId;
+}
+
+-(id) initWithEngine:(CDSoundEngine *) theSoundEngine;
+-(int) bufferForFile:(NSString*) filePath create:(BOOL) create;
+-(void) releaseBufferForFile:(NSString *) filePath;
+
+@end
+
--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+
+
+#import "CDAudioManager.h"
+
+NSString * const kCDN_AudioManagerInitialised = @"kCDN_AudioManagerInitialised";
+
+//NSOperation object used to asynchronously initialise
+@implementation CDAsynchInitialiser
+
+-(void) main {
+ [super main];
+ [CDAudioManager sharedManager];
+}
+
+@end
+
+@implementation CDLongAudioSource
+
+@synthesize audioSourcePlayer, audioSourceFilePath, delegate, backgroundMusic;
+
+-(id) init {
+ if ((self = [super init])) {
+ state = kLAS_Init;
+ volume = 1.0f;
+ mute = NO;
+ enabled_ = YES;
+ }
+ return self;
+}
+
+-(void) dealloc {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - deallocating %@", self);
+ [audioSourcePlayer release];
+ [audioSourceFilePath release];
+ [super dealloc];
+}
+
+-(void) load:(NSString*) filePath {
+ //We have alread loaded a file previously, check if we are being asked to load the same file
+ if (state == kLAS_Init || ![filePath isEqualToString:audioSourceFilePath]) {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - Loading new audio source %@",filePath);
+ //New file
+ if (state != kLAS_Init) {
+ [audioSourceFilePath release];//Release old file path
+ [audioSourcePlayer release];//Release old AVAudioPlayer, they can't be reused
+ }
+ audioSourceFilePath = [filePath copy];
+ NSError *error = nil;
+ NSString *path = [CDUtilities fullPathFromRelativePath:audioSourceFilePath];
+ audioSourcePlayer = [(AVAudioPlayer*)[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:&error];
+ if (error == nil) {
+ [audioSourcePlayer prepareToPlay];
+ audioSourcePlayer.delegate = self;
+ if (delegate && [delegate respondsToSelector:@selector(cdAudioSourceFileDidChange:)]) {
+ //Tell our delegate the file has changed
+ [delegate cdAudioSourceFileDidChange:self];
+ }
+ } else {
+ CDLOG(@"Denshion::CDLongAudioSource - Error initialising audio player: %@",error);
+ }
+ } else {
+ //Same file - just return it to a consistent state
+ [self pause];
+ [self rewind];
+ }
+ audioSourcePlayer.volume = volume;
+ audioSourcePlayer.numberOfLoops = numberOfLoops;
+ state = kLAS_Loaded;
+}
+
+-(void) play {
+ if (enabled_) {
+ self->systemPaused = NO;
+ [audioSourcePlayer play];
+ } else {
+ CDLOGINFO(@"Denshion::CDLongAudioSource long audio source didn't play because it is disabled");
+ }
+}
+
+-(void) stop {
+ [audioSourcePlayer stop];
+}
+
+-(void) pause {
+ [audioSourcePlayer pause];
+}
+
+-(void) rewind {
+ [audioSourcePlayer setCurrentTime:0];
+}
+
+-(void) resume {
+ [audioSourcePlayer play];
+}
+
+-(BOOL) isPlaying {
+ if (state != kLAS_Init) {
+ return [audioSourcePlayer isPlaying];
+ } else {
+ return NO;
+ }
+}
+
+-(void) setVolume:(float) newVolume
+{
+ volume = newVolume;
+ if (state != kLAS_Init && !mute) {
+ audioSourcePlayer.volume = newVolume;
+ }
+}
+
+-(float) volume
+{
+ return volume;
+}
+
+#pragma mark Audio Interrupt Protocol
+-(BOOL) mute
+{
+ return mute;
+}
+
+-(void) setMute:(BOOL) muteValue
+{
+ if (mute != muteValue) {
+ if (mute) {
+ //Turn sound back on
+ audioSourcePlayer.volume = volume;
+ } else {
+ audioSourcePlayer.volume = 0.0f;
+ }
+ mute = muteValue;
+ }
+}
+
+-(BOOL) enabled
+{
+ return enabled_;
+}
+
+-(void) setEnabled:(BOOL)enabledValue
+{
+ if (enabledValue != enabled_) {
+ enabled_ = enabledValue;
+ if (!enabled_) {
+ //"Stop" the sounds
+ [self pause];
+ [self rewind];
+ }
+ }
+}
+
+-(NSInteger) numberOfLoops {
+ return numberOfLoops;
+}
+
+-(void) setNumberOfLoops:(NSInteger) loopCount
+{
+ audioSourcePlayer.numberOfLoops = loopCount;
+ numberOfLoops = loopCount;
+}
+
+- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - audio player finished");
+#if TARGET_IPHONE_SIMULATOR
+ CDLOGINFO(@"Denshion::CDLongAudioSource - workaround for OpenAL clobbered audio issue");
+ //This is a workaround for an issue in all simulators (tested to 3.1.2). Problem is
+ //that OpenAL audio playback is clobbered when an AVAudioPlayer stops. Workaround
+ //is to keep the player playing on an endless loop with 0 volume and then when
+ //it is played again reset the volume and set loop count appropriately.
+ //NB: this workaround is not foolproof but it is good enough for most situations.
+ player.numberOfLoops = -1;
+ player.volume = 0;
+ [player play];
+#endif
+ if (delegate && [delegate respondsToSelector:@selector(cdAudioSourceDidFinishPlaying:)]) {
+ [delegate cdAudioSourceDidFinishPlaying:self];
+ }
+}
+
+-(void)audioPlayerBeginInterruption:(AVAudioPlayer *)player {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - audio player interrupted");
+}
+
+-(void)audioPlayerEndInterruption:(AVAudioPlayer *)player {
+ CDLOGINFO(@"Denshion::CDLongAudioSource - audio player resumed");
+ if (self.backgroundMusic) {
+ //Check if background music can play as rules may have changed during
+ //the interruption. This is to address a specific issue in 4.x when
+ //fast task switching
+ if([CDAudioManager sharedManager].willPlayBackgroundMusic) {
+ [player play];
+ }
+ } else {
+ [player play];
+ }
+}
+
+@end
+
+
+@interface CDAudioManager (PrivateMethods)
+-(BOOL) audioSessionSetActive:(BOOL) active;
+-(BOOL) audioSessionSetCategory:(NSString*) category;
+-(void) badAlContextHandler;
+@end
+
+
+@implementation CDAudioManager
+#define BACKGROUND_MUSIC_CHANNEL kASC_Left
+
+@synthesize soundEngine, willPlayBackgroundMusic;
+static CDAudioManager *sharedManager;
+static tAudioManagerState _sharedManagerState = kAMStateUninitialised;
+static tAudioManagerMode configuredMode;
+static BOOL configured = FALSE;
+
+-(BOOL) audioSessionSetActive:(BOOL) active {
+ NSError *activationError = nil;
+ if ([[AVAudioSession sharedInstance] setActive:active error:&activationError]) {
+ _audioSessionActive = active;
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio session set active %i succeeded", active);
+ return YES;
+ } else {
+ //Failed
+ CDLOG(@"Denshion::CDAudioManager - Audio session set active %i failed with error %@", active, activationError);
+ return NO;
+ }
+}
+
+-(BOOL) audioSessionSetCategory:(NSString*) category {
+ NSError *categoryError = nil;
+ if ([[AVAudioSession sharedInstance] setCategory:category error:&categoryError]) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio session set category %@ succeeded", category);
+ return YES;
+ } else {
+ //Failed
+ CDLOG(@"Denshion::CDAudioManager - Audio session set category %@ failed with error %@", category, categoryError);
+ return NO;
+ }
+}
+
+// Init
++ (CDAudioManager *) sharedManager
+{
+ @synchronized(self) {
+ if (!sharedManager) {
+ if (!configured) {
+ //Set defaults here
+ configuredMode = kAMM_FxPlusMusicIfNoOtherAudio;
+ }
+ sharedManager = [[CDAudioManager alloc] init:configuredMode];
+ _sharedManagerState = kAMStateInitialised;//This is only really relevant when using asynchronous initialisation
+ [[NSNotificationCenter defaultCenter] postNotificationName:kCDN_AudioManagerInitialised object:nil];
+ }
+ }
+ return sharedManager;
+}
+
++ (tAudioManagerState) sharedManagerState {
+ return _sharedManagerState;
+}
+
+/**
+ * Call this to set up audio manager asynchronously. Initialisation is finished when sharedManagerState == kAMStateInitialised
+ */
++ (void) initAsynchronously: (tAudioManagerMode) mode {
+ @synchronized(self) {
+ if (_sharedManagerState == kAMStateUninitialised) {
+ _sharedManagerState = kAMStateInitialising;
+ [CDAudioManager configure:mode];
+ CDAsynchInitialiser *initOp = [[[CDAsynchInitialiser alloc] init] autorelease];
+ NSOperationQueue *opQ = [[[NSOperationQueue alloc] init] autorelease];
+ [opQ addOperation:initOp];
+ }
+ }
+}
+
++ (id) alloc
+{
+ @synchronized(self) {
+ NSAssert(sharedManager == nil, @"Attempted to allocate a second instance of a singleton.");
+ return [super alloc];
+ }
+ return nil;
+}
+
+/*
+ * Call this method before accessing the shared manager in order to configure the shared audio manager
+ */
++ (void) configure: (tAudioManagerMode) mode {
+ configuredMode = mode;
+ configured = TRUE;
+}
+
+-(BOOL) isOtherAudioPlaying {
+ UInt32 isPlaying = 0;
+ UInt32 varSize = sizeof(isPlaying);
+ AudioSessionGetProperty (kAudioSessionProperty_OtherAudioIsPlaying, &varSize, &isPlaying);
+ return (isPlaying != 0);
+}
+
+-(void) setMode:(tAudioManagerMode) mode {
+
+ _mode = mode;
+ switch (_mode) {
+
+ case kAMM_FxOnly:
+ //Share audio with other app
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio will be shared");
+ //_audioSessionCategory = kAudioSessionCategory_AmbientSound;
+ _audioSessionCategory = AVAudioSessionCategoryAmbient;
+ willPlayBackgroundMusic = NO;
+ break;
+
+ case kAMM_FxPlusMusic:
+ //Use audio exclusively - if other audio is playing it will be stopped
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio will be exclusive");
+ //_audioSessionCategory = kAudioSessionCategory_SoloAmbientSound;
+ _audioSessionCategory = AVAudioSessionCategorySoloAmbient;
+ willPlayBackgroundMusic = YES;
+ break;
+
+ case kAMM_MediaPlayback:
+ //Use audio exclusively, ignore mute switch and sleep
+ CDLOGINFO(@"Denshion::CDAudioManager - Media playback mode, audio will be exclusive");
+ //_audioSessionCategory = kAudioSessionCategory_MediaPlayback;
+ _audioSessionCategory = AVAudioSessionCategoryPlayback;
+ willPlayBackgroundMusic = YES;
+ break;
+
+ case kAMM_PlayAndRecord:
+ //Use audio exclusively, ignore mute switch and sleep, has inputs and outputs
+ CDLOGINFO(@"Denshion::CDAudioManager - Play and record mode, audio will be exclusive");
+ //_audioSessionCategory = kAudioSessionCategory_PlayAndRecord;
+ _audioSessionCategory = AVAudioSessionCategoryPlayAndRecord;
+ willPlayBackgroundMusic = YES;
+ break;
+
+ default:
+ //kAudioManagerFxPlusMusicIfNoOtherAudio
+ if ([self isOtherAudioPlaying]) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Other audio is playing audio will be shared");
+ //_audioSessionCategory = kAudioSessionCategory_AmbientSound;
+ _audioSessionCategory = AVAudioSessionCategoryAmbient;
+ willPlayBackgroundMusic = NO;
+ } else {
+ CDLOGINFO(@"Denshion::CDAudioManager - Other audio is not playing audio will be exclusive");
+ //_audioSessionCategory = kAudioSessionCategory_SoloAmbientSound;
+ _audioSessionCategory = AVAudioSessionCategorySoloAmbient;
+ willPlayBackgroundMusic = YES;
+ }
+
+ break;
+ }
+
+ [self audioSessionSetCategory:_audioSessionCategory];
+
+}
+
+/**
+ * This method is used to work around various bugs introduced in 4.x OS versions. In some circumstances the
+ * audio session is interrupted but never resumed, this results in the loss of OpenAL audio when following
+ * standard practices. If we detect this situation then we will attempt to resume the audio session ourselves.
+ * Known triggers: lock the device then unlock it (iOS 4.2 gm), playback a song using MPMediaPlayer (iOS 4.0)
+ */
+- (void) badAlContextHandler {
+ if (_interrupted && alcGetCurrentContext() == NULL) {
+ CDLOG(@"Denshion::CDAudioManager - bad OpenAL context detected, attempting to resume audio session");
+ [self audioSessionResumed];
+ }
+}
+
+- (id) init: (tAudioManagerMode) mode {
+ if ((self = [super init])) {
+
+ //Initialise the audio session
+ AVAudioSession* session = [AVAudioSession sharedInstance];
+ session.delegate = self;
+
+ _mode = mode;
+ backgroundMusicCompletionSelector = nil;
+ _isObservingAppEvents = FALSE;
+ _mute = NO;
+ _resigned = NO;
+ _interrupted = NO;
+ enabled_ = YES;
+ _audioSessionActive = NO;
+ [self setMode:mode];
+ soundEngine = [[CDSoundEngine alloc] init];
+
+ //Set up audioSource channels
+ audioSourceChannels = [[NSMutableArray alloc] init];
+ CDLongAudioSource *leftChannel = [[CDLongAudioSource alloc] init];
+ leftChannel.backgroundMusic = YES;
+ CDLongAudioSource *rightChannel = [[CDLongAudioSource alloc] init];
+ rightChannel.backgroundMusic = NO;
+ [audioSourceChannels insertObject:leftChannel atIndex:kASC_Left];
+ [audioSourceChannels insertObject:rightChannel atIndex:kASC_Right];
+ [leftChannel release];
+ [rightChannel release];
+ //Used to support legacy APIs
+ backgroundMusic = [self audioSourceForChannel:BACKGROUND_MUSIC_CHANNEL];
+ backgroundMusic.delegate = self;
+
+ //Add handler for bad al context messages, these are posted by the sound engine.
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(badAlContextHandler) name:kCDN_BadAlContext object:nil];
+
+ }
+ return self;
+}
+
+-(void) dealloc {
+ CDLOGINFO(@"Denshion::CDAudioManager - deallocating");
+ [self stopBackgroundMusic];
+ [soundEngine release];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self audioSessionSetActive:NO];
+ [audioSourceChannels release];
+ [super dealloc];
+}
+
+/** Retrieves the audio source for the specified channel */
+-(CDLongAudioSource*) audioSourceForChannel:(tAudioSourceChannel) channel
+{
+ return (CDLongAudioSource*)[audioSourceChannels objectAtIndex:channel];
+}
+
+/** Loads the data from the specified file path to the channel's audio source */
+-(CDLongAudioSource*) audioSourceLoad:(NSString*) filePath channel:(tAudioSourceChannel) channel
+{
+ CDLongAudioSource *audioSource = [self audioSourceForChannel:channel];
+ if (audioSource) {
+ [audioSource load:filePath];
+ }
+ return audioSource;
+}
+
+-(BOOL) isBackgroundMusicPlaying {
+ return [self.backgroundMusic isPlaying];
+}
+
+//NB: originally I tried using a route change listener and intended to store the current route,
+//however, on a 3gs running 3.1.2 no route change is generated when the user switches the
+//ringer mute switch to off (i.e. enables sound) therefore polling is the only reliable way to
+//determine ringer switch state
+-(BOOL) isDeviceMuted {
+
+#if TARGET_IPHONE_SIMULATOR
+ //Calling audio route stuff on the simulator causes problems
+ return NO;
+#else
+ CFStringRef newAudioRoute;
+ UInt32 propertySize = sizeof (CFStringRef);
+
+ AudioSessionGetProperty (
+ kAudioSessionProperty_AudioRoute,
+ &propertySize,
+ &newAudioRoute
+ );
+
+ if (newAudioRoute == NULL) {
+ //Don't expect this to happen but playing safe otherwise a null in the CFStringCompare will cause a crash
+ return YES;
+ } else {
+ CFComparisonResult newDeviceIsMuted = CFStringCompare (
+ newAudioRoute,
+ (CFStringRef) @"",
+ 0
+ );
+
+ return (newDeviceIsMuted == kCFCompareEqualTo);
+ }
+#endif
+}
+
+#pragma mark Audio Interrupt Protocol
+
+-(BOOL) mute {
+ return _mute;
+}
+
+-(void) setMute:(BOOL) muteValue {
+ if (muteValue != _mute) {
+ _mute = muteValue;
+ [soundEngine setMute:muteValue];
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ audioSource.mute = muteValue;
+ }
+ }
+}
+
+-(BOOL) enabled {
+ return enabled_;
+}
+
+-(void) setEnabled:(BOOL) enabledValue {
+ if (enabledValue != enabled_) {
+ enabled_ = enabledValue;
+ [soundEngine setEnabled:enabled_];
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ audioSource.enabled = enabled_;
+ }
+ }
+}
+
+-(CDLongAudioSource*) backgroundMusic
+{
+ return backgroundMusic;
+}
+
+//Load background music ready for playing
+-(void) preloadBackgroundMusic:(NSString*) filePath
+{
+ [self.backgroundMusic load:filePath];
+}
+
+-(void) playBackgroundMusic:(NSString*) filePath loop:(BOOL) loop
+{
+ [self.backgroundMusic load:filePath];
+
+ if (!willPlayBackgroundMusic || _mute) {
+ CDLOGINFO(@"Denshion::CDAudioManager - play bgm aborted because audio is not exclusive or sound is muted");
+ return;
+ }
+
+ if (loop) {
+ [self.backgroundMusic setNumberOfLoops:-1];
+ } else {
+ [self.backgroundMusic setNumberOfLoops:0];
+ }
+ [self.backgroundMusic play];
+}
+
+-(void) stopBackgroundMusic
+{
+ [self.backgroundMusic stop];
+}
+
+-(void) pauseBackgroundMusic
+{
+ [self.backgroundMusic pause];
+}
+
+-(void) resumeBackgroundMusic
+{
+ if (!willPlayBackgroundMusic || _mute) {
+ CDLOGINFO(@"Denshion::CDAudioManager - resume bgm aborted because audio is not exclusive or sound is muted");
+ return;
+ }
+
+ [self.backgroundMusic resume];
+}
+
+-(void) rewindBackgroundMusic
+{
+ [self.backgroundMusic rewind];
+}
+
+-(void) setBackgroundMusicCompletionListener:(id) listener selector:(SEL) selector {
+ backgroundMusicCompletionListener = listener;
+ backgroundMusicCompletionSelector = selector;
+}
+
+/*
+ * Call this method to have the audio manager automatically handle application resign and
+ * become active. Pass a tAudioManagerResignBehavior to indicate the desired behavior
+ * for resigning and becoming active again.
+ *
+ * If autohandle is YES then the applicationWillResignActive and applicationDidBecomActive
+ * methods are automatically called, otherwise you must call them yourself at the appropriate time.
+ *
+ * Based on idea of Dominique Bongard
+ */
+-(void) setResignBehavior:(tAudioManagerResignBehavior) resignBehavior autoHandle:(BOOL) autoHandle {
+
+ if (!_isObservingAppEvents && autoHandle) {
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:@"UIApplicationWillResignActiveNotification" object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:@"UIApplicationDidBecomeActiveNotification" object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:@"UIApplicationWillTerminateNotification" object:nil];
+ _isObservingAppEvents = TRUE;
+ }
+ _resignBehavior = resignBehavior;
+}
+
+- (void) applicationWillResignActive {
+ self->_resigned = YES;
+
+ //Set the audio sesssion to one that allows sharing so that other audio won't be clobbered on resume
+ [self audioSessionSetCategory:AVAudioSessionCategoryAmbient];
+
+ switch (_resignBehavior) {
+
+ case kAMRBStopPlay:
+
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ if (audioSource.isPlaying) {
+ audioSource->systemPaused = YES;
+ audioSource->systemPauseLocation = audioSource.audioSourcePlayer.currentTime;
+ [audioSource stop];
+ } else {
+ //Music is either paused or stopped, if it is paused it will be restarted
+ //by OS so we will stop it.
+ audioSource->systemPaused = NO;
+ [audioSource stop];
+ }
+ }
+ break;
+
+ case kAMRBStop:
+ //Stop music regardless of whether it is playing or not because if it was paused
+ //then the OS would resume it
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ [audioSource stop];
+ }
+
+ default:
+ break;
+
+ }
+ CDLOGINFO(@"Denshion::CDAudioManager - handled resign active");
+}
+
+//Called when application resigns active only if setResignBehavior has been called
+- (void) applicationWillResignActive:(NSNotification *) notification
+{
+ [self applicationWillResignActive];
+}
+
+- (void) applicationDidBecomeActive {
+
+ if (self->_resigned) {
+ _resigned = NO;
+ //Reset the mode incase something changed with audio while we were inactive
+ [self setMode:_mode];
+ switch (_resignBehavior) {
+
+ case kAMRBStopPlay:
+
+ //Music had been stopped but stop maintains current time
+ //so playing again will continue from where music was before resign active.
+ //We check if music can be played because while we were inactive the user might have
+ //done something that should force music to not play such as starting a track in the iPod
+ if (self.willPlayBackgroundMusic) {
+ for( CDLongAudioSource *audioSource in audioSourceChannels) {
+ if (audioSource->systemPaused) {
+ [audioSource resume];
+ audioSource->systemPaused = NO;
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+
+ }
+ CDLOGINFO(@"Denshion::CDAudioManager - audio manager handled become active");
+ }
+}
+
+//Called when application becomes active only if setResignBehavior has been called
+- (void) applicationDidBecomeActive:(NSNotification *) notification
+{
+ [self applicationDidBecomeActive];
+}
+
+//Called when application terminates only if setResignBehavior has been called
+- (void) applicationWillTerminate:(NSNotification *) notification
+{
+ CDLOGINFO(@"Denshion::CDAudioManager - audio manager handling terminate");
+ [self stopBackgroundMusic];
+}
+
+/** The audio source completed playing */
+- (void) cdAudioSourceDidFinishPlaying:(CDLongAudioSource *) audioSource {
+ CDLOGINFO(@"Denshion::CDAudioManager - audio manager got told background music finished");
+ if (backgroundMusicCompletionSelector != nil) {
+ [backgroundMusicCompletionListener performSelector:backgroundMusicCompletionSelector];
+ }
+}
+
+-(void) beginInterruption {
+ CDLOGINFO(@"Denshion::CDAudioManager - begin interruption");
+ [self audioSessionInterrupted];
+}
+
+-(void) endInterruption {
+ CDLOGINFO(@"Denshion::CDAudioManager - end interruption");
+ [self audioSessionResumed];
+}
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000
+-(void) endInterruptionWithFlags:(NSUInteger)flags {
+ CDLOGINFO(@"Denshion::CDAudioManager - interruption ended with flags %i",flags);
+ if (flags == AVAudioSessionInterruptionFlags_ShouldResume) {
+ [self audioSessionResumed];
+ }
+}
+#endif
+
+-(void)audioSessionInterrupted
+{
+ if (!_interrupted) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio session interrupted");
+ _interrupted = YES;
+
+ // Deactivate the current audio session
+ [self audioSessionSetActive:NO];
+
+ if (alcGetCurrentContext() != NULL) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Setting OpenAL context to NULL");
+
+ ALenum error = AL_NO_ERROR;
+
+ // set the current context to NULL will 'shutdown' openAL
+ alcMakeContextCurrent(NULL);
+
+ if((error = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDAudioManager - Error making context current %x\n", error);
+ }
+ #pragma unused(error)
+ }
+ }
+}
+
+-(void)audioSessionResumed
+{
+ if (_interrupted) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Audio session resumed");
+ _interrupted = NO;
+
+ BOOL activationResult = NO;
+ // Reactivate the current audio session
+ activationResult = [self audioSessionSetActive:YES];
+
+ //This code is to handle a problem with iOS 4.0 and 4.01 where reactivating the session can fail if
+ //task switching is performed too rapidly. A test case that reliably reproduces the issue is to call the
+ //iPhone and then hang up after two rings (timing may vary ;))
+ //Basically we keep waiting and trying to let the OS catch up with itself but the number of tries is
+ //limited.
+ if (!activationResult) {
+ CDLOG(@"Denshion::CDAudioManager - Failure reactivating audio session, will try wait-try cycle");
+ int activateCount = 0;
+ while (!activationResult && activateCount < 10) {
+ [NSThread sleepForTimeInterval:0.5];
+ activationResult = [self audioSessionSetActive:YES];
+ activateCount++;
+ CDLOGINFO(@"Denshion::CDAudioManager - Reactivation attempt %i status = %i",activateCount,activationResult);
+ }
+ }
+
+ if (alcGetCurrentContext() == NULL) {
+ CDLOGINFO(@"Denshion::CDAudioManager - Restoring OpenAL context");
+ ALenum error = AL_NO_ERROR;
+ // Restore open al context
+ alcMakeContextCurrent([soundEngine openALContext]);
+ if((error = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDAudioManager - Error making context current%x\n", error);
+ }
+ #pragma unused(error)
+ }
+ }
+}
+
++(void) end {
+ [sharedManager release];
+ sharedManager = nil;
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+@implementation CDLongAudioSourceFader
+
+-(void) _setTargetProperty:(float) newVal {
+ ((CDLongAudioSource*)target).volume = newVal;
+}
+
+-(float) _getTargetProperty {
+ return ((CDLongAudioSource*)target).volume;
+}
+
+-(void) _stopTarget {
+ //Pause instead of stop as stop releases resources and causes problems in the simulator
+ [((CDLongAudioSource*)target) pause];
+}
+
+-(Class) _allowableType {
+ return [CDLongAudioSource class];
+}
+
+@end
+///////////////////////////////////////////////////////////////////////////////////////
+@implementation CDBufferManager
+
+-(id) initWithEngine:(CDSoundEngine *) theSoundEngine {
+ if ((self = [super init])) {
+ soundEngine = theSoundEngine;
+ loadedBuffers = [[NSMutableDictionary alloc] initWithCapacity:CD_BUFFERS_START];
+ freedBuffers = [[NSMutableArray alloc] init];
+ nextBufferId = 0;
+ }
+ return self;
+}
+
+-(void) dealloc {
+ [loadedBuffers release];
+ [freedBuffers release];
+ [super dealloc];
+}
+
+-(int) bufferForFile:(NSString*) filePath create:(BOOL) create {
+
+ NSNumber* soundId = (NSNumber*)[loadedBuffers objectForKey:filePath];
+ if(soundId == nil)
+ {
+ if (create) {
+ NSNumber* bufferId = nil;
+ //First try to get a buffer from the free buffers
+ if ([freedBuffers count] > 0) {
+ bufferId = [[[freedBuffers lastObject] retain] autorelease];
+ [freedBuffers removeLastObject];
+ CDLOGINFO(@"Denshion::CDBufferManager reusing buffer id %i",[bufferId intValue]);
+ } else {
+ bufferId = [[NSNumber alloc] initWithInt:nextBufferId];
+ [bufferId autorelease];
+ CDLOGINFO(@"Denshion::CDBufferManager generating new buffer id %i",[bufferId intValue]);
+ nextBufferId++;
+ }
+
+ if ([soundEngine loadBuffer:[bufferId intValue] filePath:filePath]) {
+ //File successfully loaded
+ CDLOGINFO(@"Denshion::CDBufferManager buffer loaded %@ %@",bufferId,filePath);
+ [loadedBuffers setObject:bufferId forKey:filePath];
+ return [bufferId intValue];
+ } else {
+ //File didn't load, put buffer id on free list
+ [freedBuffers addObject:bufferId];
+ return kCDNoBuffer;
+ }
+ } else {
+ //No matching buffer was found
+ return kCDNoBuffer;
+ }
+ } else {
+ return [soundId intValue];
+ }
+}
+
+-(void) releaseBufferForFile:(NSString *) filePath {
+ int bufferId = [self bufferForFile:filePath create:NO];
+ if (bufferId != kCDNoBuffer) {
+ [soundEngine unloadBuffer:bufferId];
+ [loadedBuffers removeObjectForKey:filePath];
+ NSNumber *freedBufferId = [[NSNumber alloc] initWithInt:bufferId];
+ [freedBufferId autorelease];
+ [freedBuffers addObject:freedBufferId];
+ }
+}
+@end
+
+
+
--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+#define COCOSDENSHION_VERSION "Aphex.rc"
+
+
+/**
+ If enabled code useful for debugging such as parameter check assertions will be performed.
+ If you experience any problems you should enable this and test your code with a debug build.
+ */
+//#define CD_DEBUG 1
+
+/**
+ The total number of sounds/buffers that can be loaded assuming memory is sufficient
+ */
+//Number of buffers slots that will be initially created
+#define CD_BUFFERS_START 64
+//Number of buffers that will be added
+#define CD_BUFFERS_INCREMENT 16
+
+/**
+ If enabled, OpenAL code will use static buffers. When static buffers are used the audio
+ data is managed outside of OpenAL, this eliminates a memcpy operation which leads to
+ higher performance when loading sounds.
+
+ However, the downside is that when the audio data is freed you must
+ be certain that it is no longer being accessed otherwise your app will crash. Testing on OS 2.2.1
+ and 3.1.2 has shown that this may occur if a buffer is being used by a source with state = AL_PLAYING
+ when the buffer is deleted. If the data is freed too quickly after the source is stopped then
+ a crash will occur. The implemented workaround is that when static buffers are used the unloadBuffer code will wait for
+ any playing sources to finish playing before the associated buffer and data are deleted, however, this delay may negate any
+ performance gains that are achieved during loading.
+
+ Performance tests on a 1st gen iPod running OS 2.2.1 loading the CocosDenshionDemo sounds were ~0.14 seconds without
+ static buffers and ~0.12 seconds when using static buffers.
+
+ */
+//#define CD_USE_STATIC_BUFFERS 1
+
+
--- /dev/null
+/*
+
+ 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.
+
+ Copyright (C) 2009 Apple Inc. All Rights Reserved.
+
+ $Id$
+ */
+
+/*
+ This file contains code from version 1.1 and 1.4 of MyOpenALSupport.h taken from Apple's oalTouch version.
+ The 1.4 version code is used for loading IMA4 files, however, this code causes very noticeable clicking
+ when used to load wave files that are looped so the 1.1 version code is used specifically for loading
+ wav files.
+ */
+
+#ifndef __CD_OPENAL_H
+#define __CD_OPENAL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+#import <OpenAL/al.h>
+#import <OpenAL/alc.h>
+#import <CoreFoundation/CFURL.h>
+
+
+//Taken from oalTouch MyOpenALSupport 1.1
+void* CDloadWaveAudioData(CFURLRef inFileURL, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei* outSampleRate);
+void* CDloadCafAudioData(CFURLRef inFileURL, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei* outSampleRate);
+void* CDGetOpenALAudioData(CFURLRef inFileURL, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei* outSampleRate);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
+
--- /dev/null
+/*
+
+ 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.
+
+ Copyright (C) 2009 Apple Inc. All Rights Reserved.
+
+ $Id: CDOpenALSupport.h 16 2010-03-11 06:22:10Z steveoldmeadow $
+ */
+
+#import "CDOpenALSupport.h"
+#import "CocosDenshion.h"
+#import <AudioToolbox/AudioToolbox.h>
+#import <AudioToolbox/ExtendedAudioFile.h>
+
+//Taken from oalTouch MyOpenALSupport 1.1
+void* CDloadWaveAudioData(CFURLRef inFileURL, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei* outSampleRate)
+{
+ OSStatus err = noErr;
+ UInt64 fileDataSize = 0;
+ AudioStreamBasicDescription theFileFormat;
+ UInt32 thePropertySize = sizeof(theFileFormat);
+ AudioFileID afid = 0;
+ void* theData = NULL;
+
+ // Open a file with ExtAudioFileOpen()
+ err = AudioFileOpenURL(inFileURL, kAudioFileReadPermission, 0, &afid);
+ if(err) { CDLOG(@"MyGetOpenALAudioData: AudioFileOpenURL FAILED, Error = %ld\n", err); goto Exit; }
+
+ // Get the audio data format
+ err = AudioFileGetProperty(afid, kAudioFilePropertyDataFormat, &thePropertySize, &theFileFormat);
+ if(err) { CDLOG(@"MyGetOpenALAudioData: AudioFileGetProperty(kAudioFileProperty_DataFormat) FAILED, Error = %ld\n", err); goto Exit; }
+
+ if (theFileFormat.mChannelsPerFrame > 2) {
+ CDLOG(@"MyGetOpenALAudioData - Unsupported Format, channel count is greater than stereo\n"); goto Exit;
+ }
+
+ if ((theFileFormat.mFormatID != kAudioFormatLinearPCM) || (!TestAudioFormatNativeEndian(theFileFormat))) {
+ CDLOG(@"MyGetOpenALAudioData - Unsupported Format, must be little-endian PCM\n"); goto Exit;
+ }
+
+ if ((theFileFormat.mBitsPerChannel != 8) && (theFileFormat.mBitsPerChannel != 16)) {
+ CDLOG(@"MyGetOpenALAudioData - Unsupported Format, must be 8 or 16 bit PCM\n"); goto Exit;
+ }
+
+
+ thePropertySize = sizeof(fileDataSize);
+ err = AudioFileGetProperty(afid, kAudioFilePropertyAudioDataByteCount, &thePropertySize, &fileDataSize);
+ if(err) { CDLOG(@"MyGetOpenALAudioData: AudioFileGetProperty(kAudioFilePropertyAudioDataByteCount) FAILED, Error = %ld\n", err); goto Exit; }
+
+ // Read all the data into memory
+ UInt32 dataSize = (UInt32)fileDataSize;
+ theData = malloc(dataSize);
+ if (theData)
+ {
+ AudioFileReadBytes(afid, false, 0, &dataSize, theData);
+ if(err == noErr)
+ {
+ // success
+ *outDataSize = (ALsizei)dataSize;
+ //This fix was added by me, however, 8 bit sounds have a clipping sound at the end so aren't really usable (SO)
+ if (theFileFormat.mBitsPerChannel == 16) {
+ *outDataFormat = (theFileFormat.mChannelsPerFrame > 1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
+ } else {
+ *outDataFormat = (theFileFormat.mChannelsPerFrame > 1) ? AL_FORMAT_STEREO8 : AL_FORMAT_MONO8;
+ }
+ *outSampleRate = (ALsizei)theFileFormat.mSampleRate;
+ }
+ else
+ {
+ // failure
+ free (theData);
+ theData = NULL; // make sure to return NULL
+ CDLOG(@"MyGetOpenALAudioData: ExtAudioFileRead FAILED, Error = %ld\n", err); goto Exit;
+ }
+ }
+
+Exit:
+ // Dispose the ExtAudioFileRef, it is no longer needed
+ if (afid) AudioFileClose(afid);
+ return theData;
+}
+
+//Taken from oalTouch MyOpenALSupport 1.4
+void* CDloadCafAudioData(CFURLRef inFileURL, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei* outSampleRate)
+{
+ OSStatus status = noErr;
+ BOOL abort = NO;
+ SInt64 theFileLengthInFrames = 0;
+ AudioStreamBasicDescription theFileFormat;
+ UInt32 thePropertySize = sizeof(theFileFormat);
+ ExtAudioFileRef extRef = NULL;
+ void* theData = NULL;
+ AudioStreamBasicDescription theOutputFormat;
+ UInt32 dataSize = 0;
+
+ // Open a file with ExtAudioFileOpen()
+ status = ExtAudioFileOpenURL(inFileURL, &extRef);
+ if (status != noErr)
+ {
+ CDLOG(@"MyGetOpenALAudioData: ExtAudioFileOpenURL FAILED, Error = %ld\n", status);
+ abort = YES;
+ }
+ if (abort)
+ goto Exit;
+
+ // Get the audio data format
+ status = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileDataFormat, &thePropertySize, &theFileFormat);
+ if (status != noErr)
+ {
+ CDLOG(@"MyGetOpenALAudioData: ExtAudioFileGetProperty(kExtAudioFileProperty_FileDataFormat) FAILED, Error = %ld\n", status);
+ abort = YES;
+ }
+ if (abort)
+ goto Exit;
+
+ if (theFileFormat.mChannelsPerFrame > 2)
+ {
+ CDLOG(@"MyGetOpenALAudioData - Unsupported Format, channel count is greater than stereo\n");
+ abort = YES;
+ }
+ if (abort)
+ goto Exit;
+
+ // Set the client format to 16 bit signed integer (native-endian) data
+ // Maintain the channel count and sample rate of the original source format
+ theOutputFormat.mSampleRate = theFileFormat.mSampleRate;
+ theOutputFormat.mChannelsPerFrame = theFileFormat.mChannelsPerFrame;
+
+ theOutputFormat.mFormatID = kAudioFormatLinearPCM;
+ theOutputFormat.mBytesPerPacket = 2 * theOutputFormat.mChannelsPerFrame;
+ theOutputFormat.mFramesPerPacket = 1;
+ theOutputFormat.mBytesPerFrame = 2 * theOutputFormat.mChannelsPerFrame;
+ theOutputFormat.mBitsPerChannel = 16;
+ theOutputFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
+
+ // Set the desired client (output) data format
+ status = ExtAudioFileSetProperty(extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(theOutputFormat), &theOutputFormat);
+ if (status != noErr)
+ {
+ CDLOG(@"MyGetOpenALAudioData: ExtAudioFileSetProperty(kExtAudioFileProperty_ClientDataFormat) FAILED, Error = %ld\n", status);
+ abort = YES;
+ }
+ if (abort)
+ goto Exit;
+
+ // Get the total frame count
+ thePropertySize = sizeof(theFileLengthInFrames);
+ status = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileLengthFrames, &thePropertySize, &theFileLengthInFrames);
+ if (status != noErr)
+ {
+ CDLOG(@"MyGetOpenALAudioData: ExtAudioFileGetProperty(kExtAudioFileProperty_FileLengthFrames) FAILED, Error = %ld\n", status);
+ abort = YES;
+ }
+ if (abort)
+ goto Exit;
+
+ // Read all the data into memory
+ dataSize = (UInt32) theFileLengthInFrames * theOutputFormat.mBytesPerFrame;
+ theData = malloc(dataSize);
+ if (theData)
+ {
+ AudioBufferList theDataBuffer;
+ theDataBuffer.mNumberBuffers = 1;
+ theDataBuffer.mBuffers[0].mDataByteSize = dataSize;
+ theDataBuffer.mBuffers[0].mNumberChannels = theOutputFormat.mChannelsPerFrame;
+ theDataBuffer.mBuffers[0].mData = theData;
+
+ // Read the data into an AudioBufferList
+ status = ExtAudioFileRead(extRef, (UInt32*)&theFileLengthInFrames, &theDataBuffer);
+ if(status == noErr)
+ {
+ // success
+ *outDataSize = (ALsizei)dataSize;
+ *outDataFormat = (theOutputFormat.mChannelsPerFrame > 1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
+ *outSampleRate = (ALsizei)theOutputFormat.mSampleRate;
+ }
+ else
+ {
+ // failure
+ free (theData);
+ theData = NULL; // make sure to return NULL
+ CDLOG(@"MyGetOpenALAudioData: ExtAudioFileRead FAILED, Error = %ld\n", status);
+ abort = YES;
+ }
+ }
+ if (abort)
+ goto Exit;
+
+Exit:
+ // Dispose the ExtAudioFileRef, it is no longer needed
+ if (extRef) ExtAudioFileDispose(extRef);
+ return theData;
+}
+
+void* CDGetOpenALAudioData(CFURLRef inFileURL, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei* outSampleRate) {
+
+ CFStringRef extension = CFURLCopyPathExtension(inFileURL);
+ CFComparisonResult isWavFile = 0;
+ if (extension != NULL) {
+ isWavFile = CFStringCompare (extension,(CFStringRef)@"wav", kCFCompareCaseInsensitive);
+ CFRelease(extension);
+ }
+
+ if (isWavFile == kCFCompareEqualTo) {
+ return CDloadWaveAudioData(inFileURL, outDataSize, outDataFormat, outSampleRate);
+ } else {
+ return CDloadCafAudioData(inFileURL, outDataSize, outDataFormat, outSampleRate);
+ }
+}
+
--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+
+
+
+/**
+@file
+@b IMPORTANT
+There are 3 different ways of using CocosDenshion. Depending on which you choose you
+will need to include different files and frameworks.
+
+@par SimpleAudioEngine
+This is recommended for basic audio requirements. If you just want to play some sound fx
+and some background music and have no interest in learning the lower level workings then
+this is the interface to use.
+
+Requirements:
+ - Firmware: OS 2.2 or greater
+ - Files: SimpleAudioEngine.*, CocosDenshion.*
+ - Frameworks: OpenAL, AudioToolbox, AVFoundation
+
+@par CDAudioManager
+CDAudioManager is basically a thin wrapper around an AVAudioPlayer object used for playing
+background music and a CDSoundEngine object used for playing sound effects. It manages the
+audio session for you deals with audio session interruption. It is fairly low level and it
+is expected you have some understanding of the underlying technologies. For example, for
+many use cases regarding background music it is expected you will work directly with the
+backgroundMusic AVAudioPlayer which is exposed as a property.
+
+Requirements:
+ - Firmware: OS 2.2 or greater
+ - Files: CDAudioManager.*, CocosDenshion.*
+ - Frameworks: OpenAL, AudioToolbox, AVFoundation
+
+@par CDSoundEngine
+CDSoundEngine is a sound engine built upon OpenAL and derived from Apple's oalTouch
+example. It can playback up to 32 sounds simultaneously with control over pitch, pan
+and gain. It can be set up to handle audio session interruption automatically. You
+may decide to use CDSoundEngine directly instead of CDAudioManager or SimpleAudioEngine
+because you require OS 2.0 compatibility.
+
+Requirements:
+ - Firmware: OS 2.0 or greater
+ - Files: CocosDenshion.*
+ - Frameworks: OpenAL, AudioToolbox
+
+*/
+
+#import <OpenAL/al.h>
+#import <OpenAL/alc.h>
+#import <AudioToolbox/AudioToolbox.h>
+#import <Foundation/Foundation.h>
+#import "CDConfig.h"
+
+
+#if !defined(CD_DEBUG) || CD_DEBUG == 0
+#define CDLOG(...) do {} while (0)
+#define CDLOGINFO(...) do {} while (0)
+
+#elif CD_DEBUG == 1
+#define CDLOG(...) NSLog(__VA_ARGS__)
+#define CDLOGINFO(...) do {} while (0)
+
+#elif CD_DEBUG > 1
+#define CDLOG(...) NSLog(__VA_ARGS__)
+#define CDLOGINFO(...) NSLog(__VA_ARGS__)
+#endif // CD_DEBUG
+
+
+#import "CDOpenALSupport.h"
+
+//Tested source limit on 2.2.1 and 3.1.2 with up to 128 sources and appears to work. Older OS versions e.g 2.2 may support only 32
+#define CD_SOURCE_LIMIT 32 //Total number of sources we will ever want, may actually get less
+#define CD_NO_SOURCE 0xFEEDFAC //Return value indicating playback failed i.e. no source
+#define CD_IGNORE_AUDIO_SESSION 0xBEEFBEE //Used internally to indicate audio session will not be handled
+#define CD_MUTE 0xFEEDBAB //Return value indicating sound engine is muted or non functioning
+#define CD_NO_SOUND = -1;
+
+#define CD_SAMPLE_RATE_HIGH 44100
+#define CD_SAMPLE_RATE_MID 22050
+#define CD_SAMPLE_RATE_LOW 16000
+#define CD_SAMPLE_RATE_BASIC 8000
+#define CD_SAMPLE_RATE_DEFAULT 44100
+
+extern NSString * const kCDN_BadAlContext;
+extern NSString * const kCDN_AsynchLoadComplete;
+
+extern float const kCD_PitchDefault;
+extern float const kCD_PitchLowerOneOctave;
+extern float const kCD_PitchHigherOneOctave;
+extern float const kCD_PanDefault;
+extern float const kCD_PanFullLeft;
+extern float const kCD_PanFullRight;
+extern float const kCD_GainDefault;
+
+enum bufferState {
+ CD_BS_EMPTY = 0,
+ CD_BS_LOADED = 1,
+ CD_BS_FAILED = 2
+};
+
+typedef struct _sourceGroup {
+ int startIndex;
+ int currentIndex;
+ int totalSources;
+ bool enabled;
+ bool nonInterruptible;
+ int *sourceStatuses;//pointer into array of source status information
+} sourceGroup;
+
+typedef struct _bufferInfo {
+ ALuint bufferId;
+ int bufferState;
+ void* bufferData;
+ ALenum format;
+ ALsizei sizeInBytes;
+ ALsizei frequencyInHertz;
+} bufferInfo;
+
+typedef struct _sourceInfo {
+ bool usable;
+ ALuint sourceId;
+ ALuint attachedBufferId;
+} sourceInfo;
+
+#pragma mark CDAudioTransportProtocol
+
+@protocol CDAudioTransportProtocol <NSObject>
+/** Play the audio */
+-(BOOL) play;
+/** Pause the audio, retain resources */
+-(BOOL) pause;
+/** Stop the audio, release resources */
+-(BOOL) stop;
+/** Return playback to beginning */
+-(BOOL) rewind;
+@end
+
+#pragma mark CDAudioInterruptProtocol
+
+@protocol CDAudioInterruptProtocol <NSObject>
+/** Is audio mute */
+-(BOOL) mute;
+/** If YES then audio is silenced but not stopped, calls to start new audio will proceed but silently */
+-(void) setMute:(BOOL) muteValue;
+/** Is audio enabled */
+-(BOOL) enabled;
+/** If NO then all audio is stopped and any calls to start new audio will be ignored */
+-(void) setEnabled:(BOOL) enabledValue;
+@end
+
+#pragma mark CDUtilities
+/**
+ Collection of utilities required by CocosDenshion
+ */
+@interface CDUtilities : NSObject
+{
+}
+
+/** Fundamentally the same as the corresponding method is CCFileUtils but added to break binding to cocos2d */
++(NSString*) fullPathFromRelativePath:(NSString*) relPath;
+
+@end
+
+
+#pragma mark CDSoundEngine
+
+/** CDSoundEngine is built upon OpenAL and works with SDK 2.0.
+ CDSoundEngine is a sound engine built upon OpenAL and derived from Apple's oalTouch
+ example. It can playback up to 32 sounds simultaneously with control over pitch, pan
+ and gain. It can be set up to handle audio session interruption automatically. You
+ may decide to use CDSoundEngine directly instead of CDAudioManager or SimpleAudioEngine
+ because you require OS 2.0 compatibility.
+
+ Requirements:
+ - Firmware: OS 2.0 or greater
+ - Files: CocosDenshion.*
+ - Frameworks: OpenAL, AudioToolbox
+
+ @since v0.8
+ */
+@class CDSoundSource;
+@interface CDSoundEngine : NSObject <CDAudioInterruptProtocol> {
+
+ bufferInfo *_buffers;
+ sourceInfo *_sources;
+ sourceGroup *_sourceGroups;
+ ALCcontext *context;
+ NSUInteger _sourceGroupTotal;
+ UInt32 _audioSessionCategory;
+ BOOL _handleAudioSession;
+ ALfloat _preMuteGain;
+ NSObject *_mutexBufferLoad;
+ BOOL mute_;
+ BOOL enabled_;
+
+ ALenum lastErrorCode_;
+ BOOL functioning_;
+ float asynchLoadProgress_;
+ BOOL getGainWorks_;
+
+ //For managing dynamic allocation of sources and buffers
+ int sourceTotal_;
+ int bufferTotal;
+
+}
+
+@property (readwrite, nonatomic) ALfloat masterGain;
+@property (readonly) ALenum lastErrorCode;//Last OpenAL error code that was generated
+@property (readonly) BOOL functioning;//Is the sound engine functioning
+@property (readwrite) float asynchLoadProgress;
+@property (readonly) BOOL getGainWorks;//Does getting the gain for a source work
+/** Total number of sources available */
+@property (readonly) int sourceTotal;
+/** Total number of source groups that have been defined */
+@property (readonly) NSUInteger sourceGroupTotal;
+
+/** Sets the sample rate for the audio mixer. For best performance this should match the sample rate of your audio content */
++(void) setMixerSampleRate:(Float32) sampleRate;
+
+/** Initializes the engine with a group definition and a total number of groups */
+-(id)init;
+
+/** Plays a sound in a channel group with a pitch, pan and gain. The sound could played looped or not */
+-(ALuint) playSound:(int) soundId sourceGroupId:(int)sourceGroupId pitch:(float) pitch pan:(float) pan gain:(float) gain loop:(BOOL) loop;
+
+/** Creates and returns a sound source object for the specified sound within the specified source group.
+ */
+-(CDSoundSource *) soundSourceForSound:(int) soundId sourceGroupId:(int) sourceGroupId;
+
+/** Stops playing a sound */
+- (void) stopSound:(ALuint) sourceId;
+/** Stops playing a source group */
+- (void) stopSourceGroup:(int) sourceGroupId;
+/** Stops all playing sounds */
+-(void) stopAllSounds;
+-(void) defineSourceGroups:(NSArray*) sourceGroupDefinitions;
+-(void) defineSourceGroups:(int[]) sourceGroupDefinitions total:(NSUInteger) total;
+-(void) setSourceGroupNonInterruptible:(int) sourceGroupId isNonInterruptible:(BOOL) isNonInterruptible;
+-(void) setSourceGroupEnabled:(int) sourceGroupId enabled:(BOOL) enabled;
+-(BOOL) sourceGroupEnabled:(int) sourceGroupId;
+-(BOOL) loadBufferFromData:(int) soundId soundData:(ALvoid*) soundData format:(ALenum) format size:(ALsizei) size freq:(ALsizei) freq;
+-(BOOL) loadBuffer:(int) soundId filePath:(NSString*) filePath;
+-(void) loadBuffersAsynchronously:(NSArray *) loadRequests;
+-(BOOL) unloadBuffer:(int) soundId;
+-(ALCcontext *) openALContext;
+
+/** Returns the duration of the buffer in seconds or a negative value if the buffer id is invalid */
+-(float) bufferDurationInSeconds:(int) soundId;
+/** Returns the size of the buffer in bytes or a negative value if the buffer id is invalid */
+-(ALsizei) bufferSizeInBytes:(int) soundId;
+/** Returns the sampling frequency of the buffer in hertz or a negative value if the buffer id is invalid */
+-(ALsizei) bufferFrequencyInHertz:(int) soundId;
+
+/** Used internally, never call unless you know what you are doing */
+-(void) _soundSourcePreRelease:(CDSoundSource *) soundSource;
+
+@end
+
+#pragma mark CDSoundSource
+/** CDSoundSource is a wrapper around an OpenAL sound source.
+ It allows you to manipulate properties such as pitch, gain, pan and looping while the
+ sound is playing. CDSoundSource is based on the old CDSourceWrapper class but with much
+ added functionality.
+
+ @since v1.0
+ */
+@interface CDSoundSource : NSObject <CDAudioTransportProtocol, CDAudioInterruptProtocol> {
+ ALenum lastError;
+@public
+ ALuint _sourceId;
+ ALuint _sourceIndex;
+ CDSoundEngine* _engine;
+ int _soundId;
+ float _preMuteGain;
+ BOOL enabled_;
+ BOOL mute_;
+}
+@property (readwrite, nonatomic) float pitch;
+@property (readwrite, nonatomic) float gain;
+@property (readwrite, nonatomic) float pan;
+@property (readwrite, nonatomic) BOOL looping;
+@property (readonly) BOOL isPlaying;
+@property (readwrite, nonatomic) int soundId;
+/** Returns the duration of the attached buffer in seconds or a negative value if the buffer is invalid */
+@property (readonly) float durationInSeconds;
+
+/** Stores the last error code that occurred. Check against AL_NO_ERROR */
+@property (readonly) ALenum lastError;
+/** Do not init yourself, get an instance from the sourceForSound factory method on CDSoundEngine */
+-(id)init:(ALuint) theSourceId sourceIndex:(int) index soundEngine:(CDSoundEngine*) engine;
+
+@end
+
+#pragma mark CDAudioInterruptTargetGroup
+
+/** Container for objects that implement audio interrupt protocol i.e. they can be muted and enabled.
+ Setting mute and enabled for the group propagates to all children.
+ Designed to be used with your CDSoundSource objects to get them to comply with global enabled and mute settings
+ if that is what you want to do.*/
+@interface CDAudioInterruptTargetGroup : NSObject <CDAudioInterruptProtocol> {
+ BOOL mute_;
+ BOOL enabled_;
+ NSMutableArray *children_;
+}
+-(void) addAudioInterruptTarget:(NSObject<CDAudioInterruptProtocol>*) interruptibleTarget;
+@end
+
+#pragma mark CDAsynchBufferLoader
+
+/** CDAsynchBufferLoader
+ TODO
+ */
+@interface CDAsynchBufferLoader : NSOperation {
+ NSArray *_loadRequests;
+ CDSoundEngine *_soundEngine;
+}
+
+-(id) init:(NSArray *)loadRequests soundEngine:(CDSoundEngine *) theSoundEngine;
+
+@end
+
+#pragma mark CDBufferLoadRequest
+
+/** CDBufferLoadRequest */
+@interface CDBufferLoadRequest: NSObject
+{
+ NSString *filePath;
+ int soundId;
+ //id loader;
+}
+
+@property (readonly) NSString *filePath;
+@property (readonly) int soundId;
+
+- (id)init:(int) theSoundId filePath:(const NSString *) theFilePath;
+@end
+
+/** Interpolation type */
+typedef enum {
+ kIT_Linear, //!Straight linear interpolation fade
+ kIT_SCurve, //!S curved interpolation
+ kIT_Exponential //!Exponential interpolation
+} tCDInterpolationType;
+
+#pragma mark CDFloatInterpolator
+@interface CDFloatInterpolator: NSObject
+{
+ float start;
+ float end;
+ float lastValue;
+ tCDInterpolationType interpolationType;
+}
+@property (readwrite, nonatomic) float start;
+@property (readwrite, nonatomic) float end;
+@property (readwrite, nonatomic) tCDInterpolationType interpolationType;
+
+/** Return a value between min and max based on t which represents fractional progress where 0 is the start
+ and 1 is the end */
+-(float) interpolate:(float) t;
+-(id) init:(tCDInterpolationType) type startVal:(float) startVal endVal:(float) endVal;
+
+@end
+
+#pragma mark CDPropertyModifier
+
+/** Base class for classes that modify properties such as pitch, pan and gain */
+@interface CDPropertyModifier: NSObject
+{
+ CDFloatInterpolator *interpolator;
+ float startValue;
+ float endValue;
+ id target;
+ BOOL stopTargetWhenComplete;
+
+}
+@property (readwrite, nonatomic) BOOL stopTargetWhenComplete;
+@property (readwrite, nonatomic) float startValue;
+@property (readwrite, nonatomic) float endValue;
+@property (readwrite, nonatomic) tCDInterpolationType interpolationType;
+
+-(id) init:(id) theTarget interpolationType:(tCDInterpolationType) type startVal:(float) startVal endVal:(float) endVal;
+/** Set to a fractional value between 0 and 1 where 0 equals the start and 1 equals the end*/
+-(void) modify:(float) t;
+
+-(void) _setTargetProperty:(float) newVal;
+-(float) _getTargetProperty;
+-(void) _stopTarget;
+-(Class) _allowableType;
+
+@end
+
+#pragma mark CDSoundSourceFader
+
+/** Fader for CDSoundSource objects */
+@interface CDSoundSourceFader : CDPropertyModifier{}
+@end
+
+#pragma mark CDSoundSourcePanner
+
+/** Panner for CDSoundSource objects */
+@interface CDSoundSourcePanner : CDPropertyModifier{}
+@end
+
+#pragma mark CDSoundSourcePitchBender
+
+/** Pitch bender for CDSoundSource objects */
+@interface CDSoundSourcePitchBender : CDPropertyModifier{}
+@end
+
+#pragma mark CDSoundEngineFader
+
+/** Fader for CDSoundEngine objects */
+@interface CDSoundEngineFader : CDPropertyModifier{}
+@end
+
+
+
+
--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+
+#import "CocosDenshion.h"
+
+typedef ALvoid AL_APIENTRY (*alBufferDataStaticProcPtr) (const ALint bid, ALenum format, ALvoid* data, ALsizei size, ALsizei freq);
+ALvoid alBufferDataStaticProc(const ALint bid, ALenum format, ALvoid* data, ALsizei size, ALsizei freq)
+{
+ static alBufferDataStaticProcPtr proc = NULL;
+
+ if (proc == NULL) {
+ proc = (alBufferDataStaticProcPtr) alcGetProcAddress(NULL, (const ALCchar*) "alBufferDataStatic");
+ }
+
+ if (proc)
+ proc(bid, format, data, size, freq);
+
+ return;
+}
+
+typedef ALvoid AL_APIENTRY (*alcMacOSXMixerOutputRateProcPtr) (const ALdouble value);
+ALvoid alcMacOSXMixerOutputRateProc(const ALdouble value)
+{
+ static alcMacOSXMixerOutputRateProcPtr proc = NULL;
+
+ if (proc == NULL) {
+ proc = (alcMacOSXMixerOutputRateProcPtr) alcGetProcAddress(NULL, (const ALCchar*) "alcMacOSXMixerOutputRate");
+ }
+
+ if (proc)
+ proc(value);
+
+ return;
+}
+
+NSString * const kCDN_BadAlContext = @"kCDN_BadAlContext";
+NSString * const kCDN_AsynchLoadComplete = @"kCDN_AsynchLoadComplete";
+float const kCD_PitchDefault = 1.0f;
+float const kCD_PitchLowerOneOctave = 0.5f;
+float const kCD_PitchHigherOneOctave = 2.0f;
+float const kCD_PanDefault = 0.0f;
+float const kCD_PanFullLeft = -1.0f;
+float const kCD_PanFullRight = 1.0f;
+float const kCD_GainDefault = 1.0f;
+
+@interface CDSoundEngine (PrivateMethods)
+-(BOOL) _initOpenAL;
+-(void) _testGetGain;
+-(void) _dumpSourceGroupsInfo;
+-(void) _getSourceIndexForSourceGroup;
+-(void) _freeSourceGroups;
+-(BOOL) _setUpSourceGroups:(int[]) definitions total:(NSUInteger) total;
+@end
+
+#pragma mark -
+#pragma mark CDUtilities
+
+@implementation CDUtilities
+
++(NSString*) fullPathFromRelativePath:(NSString*) relPath
+{
+ // do not convert an absolute path (starting with '/')
+ if(([relPath length] > 0) && ([relPath characterAtIndex:0] == '/'))
+ {
+ return relPath;
+ }
+
+ NSMutableArray *imagePathComponents = [NSMutableArray arrayWithArray:[relPath pathComponents]];
+ NSString *file = [imagePathComponents lastObject];
+
+ [imagePathComponents removeLastObject];
+ NSString *imageDirectory = [NSString pathWithComponents:imagePathComponents];
+
+ NSString *fullpath = [[NSBundle mainBundle] pathForResource:file ofType:nil inDirectory:imageDirectory];
+ if (fullpath == nil)
+ fullpath = relPath;
+
+ return fullpath;
+}
+
+@end
+
+#pragma mark -
+#pragma mark CDSoundEngine
+
+@implementation CDSoundEngine
+
+static Float32 _mixerSampleRate;
+static BOOL _mixerRateSet = NO;
+
+@synthesize lastErrorCode = lastErrorCode_;
+@synthesize functioning = functioning_;
+@synthesize asynchLoadProgress = asynchLoadProgress_;
+@synthesize getGainWorks = getGainWorks_;
+@synthesize sourceTotal = sourceTotal_;
+
++ (void) setMixerSampleRate:(Float32) sampleRate {
+ _mixerRateSet = YES;
+ _mixerSampleRate = sampleRate;
+}
+
+- (void) _testGetGain {
+ float testValue = 0.7f;
+ ALuint testSourceId = _sources[0].sourceId;
+ alSourcef(testSourceId, AL_GAIN, 0.0f);//Start from know value
+ alSourcef(testSourceId, AL_GAIN, testValue);
+ ALfloat gainVal;
+ alGetSourcef(testSourceId, AL_GAIN, &gainVal);
+ getGainWorks_ = (gainVal == testValue);
+}
+
+//Generate sources one at a time until we fail
+-(void) _generateSources {
+
+ _sources = (sourceInfo*)malloc( sizeof(_sources[0]) * CD_SOURCE_LIMIT);
+ BOOL hasFailed = NO;
+ sourceTotal_ = 0;
+ alGetError();//Clear error
+ while (!hasFailed && sourceTotal_ < CD_SOURCE_LIMIT) {
+ alGenSources(1, &(_sources[sourceTotal_].sourceId));
+ if (alGetError() == AL_NO_ERROR) {
+ //Now try attaching source to null buffer
+ alSourcei(_sources[sourceTotal_].sourceId, AL_BUFFER, 0);
+ if (alGetError() == AL_NO_ERROR) {
+ _sources[sourceTotal_].usable = true;
+ sourceTotal_++;
+ } else {
+ hasFailed = YES;
+ }
+ } else {
+ _sources[sourceTotal_].usable = false;
+ hasFailed = YES;
+ }
+ }
+ //Mark the rest of the sources as not usable
+ for (int i=sourceTotal_; i < CD_SOURCE_LIMIT; i++) {
+ _sources[i].usable = false;
+ }
+}
+
+-(void) _generateBuffers:(int) startIndex endIndex:(int) endIndex {
+ if (_buffers) {
+ alGetError();
+ for (int i=startIndex; i <= endIndex; i++) {
+ alGenBuffers(1, &_buffers[i].bufferId);
+ _buffers[i].bufferData = NULL;
+ if (alGetError() == AL_NO_ERROR) {
+ _buffers[i].bufferState = CD_BS_EMPTY;
+ } else {
+ _buffers[i].bufferState = CD_BS_FAILED;
+ CDLOG(@"Denshion::CDSoundEngine - buffer creation failed %i",i);
+ }
+ }
+ }
+}
+
+/**
+ * Internal method called during init
+ */
+- (BOOL) _initOpenAL
+{
+ //ALenum error;
+ context = NULL;
+ ALCdevice *newDevice = NULL;
+
+ //Set the mixer rate for the audio mixer
+ if (!_mixerRateSet) {
+ _mixerSampleRate = CD_SAMPLE_RATE_DEFAULT;
+ }
+ alcMacOSXMixerOutputRateProc(_mixerSampleRate);
+ CDLOGINFO(@"Denshion::CDSoundEngine - mixer output rate set to %0.2f",_mixerSampleRate);
+
+ // Create a new OpenAL Device
+ // Pass NULL to specify the system's default output device
+ newDevice = alcOpenDevice(NULL);
+ if (newDevice != NULL)
+ {
+ // Create a new OpenAL Context
+ // The new context will render to the OpenAL Device just created
+ context = alcCreateContext(newDevice, 0);
+ if (context != NULL)
+ {
+ // Make the new context the Current OpenAL Context
+ alcMakeContextCurrent(context);
+
+ // Create some OpenAL Buffer Objects
+ [self _generateBuffers:0 endIndex:bufferTotal-1];
+
+ // Create some OpenAL Source Objects
+ [self _generateSources];
+
+ }
+ } else {
+ return FALSE;//No device
+ }
+ alGetError();//Clear error
+ return TRUE;
+}
+
+- (void) dealloc {
+
+ ALCcontext *currentContext = NULL;
+ ALCdevice *device = NULL;
+
+ [self stopAllSounds];
+
+ CDLOGINFO(@"Denshion::CDSoundEngine - Deallocing sound engine.");
+ [self _freeSourceGroups];
+
+ // Delete the Sources
+ CDLOGINFO(@"Denshion::CDSoundEngine - deleting sources.");
+ for (int i=0; i < sourceTotal_; i++) {
+ alSourcei(_sources[i].sourceId, AL_BUFFER, 0);//Detach from current buffer
+ alDeleteSources(1, &(_sources[i].sourceId));
+ if((lastErrorCode_ = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDSoundEngine - Error deleting source! %x\n", lastErrorCode_);
+ }
+ }
+
+ // Delete the Buffers
+ CDLOGINFO(@"Denshion::CDSoundEngine - deleting buffers.");
+ for (int i=0; i < bufferTotal; i++) {
+ alDeleteBuffers(1, &_buffers[i].bufferId);
+#ifdef CD_USE_STATIC_BUFFERS
+ if (_buffers[i].bufferData) {
+ free(_buffers[i].bufferData);
+ }
+#endif
+ }
+ CDLOGINFO(@"Denshion::CDSoundEngine - free buffers.");
+ free(_buffers);
+ currentContext = alcGetCurrentContext();
+ //Get device for active context
+ device = alcGetContextsDevice(currentContext);
+ //Release context
+ CDLOGINFO(@"Denshion::CDSoundEngine - destroy context.");
+ alcDestroyContext(currentContext);
+ //Close device
+ CDLOGINFO(@"Denshion::CDSoundEngine - close device.");
+ alcCloseDevice(device);
+ CDLOGINFO(@"Denshion::CDSoundEngine - free sources.");
+ free(_sources);
+
+ //Release mutexes
+ [_mutexBufferLoad release];
+
+ [super dealloc];
+}
+
+-(NSUInteger) sourceGroupTotal {
+ return _sourceGroupTotal;
+}
+
+-(void) _freeSourceGroups
+{
+ CDLOGINFO(@"Denshion::CDSoundEngine freeing source groups");
+ if(_sourceGroups) {
+ for (int i=0; i < _sourceGroupTotal; i++) {
+ if (_sourceGroups[i].sourceStatuses) {
+ free(_sourceGroups[i].sourceStatuses);
+ CDLOGINFO(@"Denshion::CDSoundEngine freed source statuses %i",i);
+ }
+ }
+ free(_sourceGroups);
+ }
+}
+
+-(BOOL) _redefineSourceGroups:(int[]) definitions total:(NSUInteger) total
+{
+ if (_sourceGroups) {
+ //Stop all sounds
+ [self stopAllSounds];
+ //Need to free source groups
+ [self _freeSourceGroups];
+ }
+ return [self _setUpSourceGroups:definitions total:total];
+}
+
+-(BOOL) _setUpSourceGroups:(int[]) definitions total:(NSUInteger) total
+{
+ _sourceGroups = (sourceGroup *)malloc( sizeof(_sourceGroups[0]) * total);
+ if(!_sourceGroups) {
+ CDLOG(@"Denshion::CDSoundEngine - source groups memory allocation failed");
+ return NO;
+ }
+
+ _sourceGroupTotal = total;
+ int sourceCount = 0;
+ for (int i=0; i < _sourceGroupTotal; i++) {
+
+ _sourceGroups[i].startIndex = 0;
+ _sourceGroups[i].currentIndex = _sourceGroups[i].startIndex;
+ _sourceGroups[i].enabled = false;
+ _sourceGroups[i].nonInterruptible = false;
+ _sourceGroups[i].totalSources = definitions[i];
+ _sourceGroups[i].sourceStatuses = malloc(sizeof(_sourceGroups[i].sourceStatuses[0]) * _sourceGroups[i].totalSources);
+ if (_sourceGroups[i].sourceStatuses) {
+ for (int j=0; j < _sourceGroups[i].totalSources; j++) {
+ //First bit is used to indicate whether source is locked, index is shifted back 1 bit
+ _sourceGroups[i].sourceStatuses[j] = (sourceCount + j) << 1;
+ }
+ }
+ sourceCount += definitions[i];
+ }
+ return YES;
+}
+
+-(void) defineSourceGroups:(int[]) sourceGroupDefinitions total:(NSUInteger) total {
+ [self _redefineSourceGroups:sourceGroupDefinitions total:total];
+}
+
+-(void) defineSourceGroups:(NSArray*) sourceGroupDefinitions {
+ CDLOGINFO(@"Denshion::CDSoundEngine - source groups defined by NSArray.");
+ NSUInteger totalDefs = [sourceGroupDefinitions count];
+ int* defs = (int *)malloc( sizeof(int) * totalDefs);
+ int currentIndex = 0;
+ for (id currentDef in sourceGroupDefinitions) {
+ if ([currentDef isKindOfClass:[NSNumber class]]) {
+ defs[currentIndex] = (int)[(NSNumber*)currentDef integerValue];
+ CDLOGINFO(@"Denshion::CDSoundEngine - found definition %i.",defs[currentIndex]);
+ } else {
+ CDLOG(@"Denshion::CDSoundEngine - warning, did not understand source definition.");
+ defs[currentIndex] = 0;
+ }
+ currentIndex++;
+ }
+ [self _redefineSourceGroups:defs total:totalDefs];
+ free(defs);
+}
+
+- (id)init
+{
+ if ((self = [super init])) {
+
+ //Create mutexes
+ _mutexBufferLoad = [[NSObject alloc] init];
+
+ asynchLoadProgress_ = 0.0f;
+
+ bufferTotal = CD_BUFFERS_START;
+ _buffers = (bufferInfo *)malloc( sizeof(_buffers[0]) * bufferTotal);
+
+ // Initialize our OpenAL environment
+ if ([self _initOpenAL]) {
+ //Set up the default source group - a single group that contains all the sources
+ int sourceDefs[1];
+ sourceDefs[0] = self.sourceTotal;
+ [self _setUpSourceGroups:sourceDefs total:1];
+
+ functioning_ = YES;
+ //Synchronize premute gain
+ _preMuteGain = self.masterGain;
+ mute_ = NO;
+ enabled_ = YES;
+ //Test whether get gain works for sources
+ [self _testGetGain];
+ } else {
+ //Something went wrong with OpenAL
+ functioning_ = NO;
+ }
+ }
+
+ return self;
+}
+
+/**
+ * Delete the buffer identified by soundId
+ * @return true if buffer deleted successfully, otherwise false
+ */
+- (BOOL) unloadBuffer:(int) soundId
+{
+ //Ensure soundId is within array bounds otherwise memory corruption will occur
+ if (soundId < 0 || soundId >= bufferTotal) {
+ CDLOG(@"Denshion::CDSoundEngine - soundId is outside array bounds, maybe you need to increase CD_MAX_BUFFERS");
+ return FALSE;
+ }
+
+ //Before a buffer can be deleted any sources that are attached to it must be stopped
+ for (int i=0; i < sourceTotal_; i++) {
+ //Note: tried getting the AL_BUFFER attribute of the source instead but doesn't
+ //appear to work on a device - just returned zero.
+ if (_buffers[soundId].bufferId == _sources[i].attachedBufferId) {
+
+ CDLOG(@"Denshion::CDSoundEngine - Found attached source %i %i %i",i,_buffers[soundId].bufferId,_sources[i].sourceId);
+#ifdef CD_USE_STATIC_BUFFERS
+ //When using static buffers a crash may occur if a source is playing with a buffer that is about
+ //to be deleted even though we stop the source and successfully delete the buffer. Crash is confirmed
+ //on 2.2.1 and 3.1.2, however, it will only occur if a source is used rapidly after having its prior
+ //data deleted. To avoid any possibility of the crash we wait for the source to finish playing.
+ ALint state;
+
+ alGetSourcei(_sources[i].sourceId, AL_SOURCE_STATE, &state);
+
+ if (state == AL_PLAYING) {
+ CDLOG(@"Denshion::CDSoundEngine - waiting for source to complete playing before removing buffer data");
+ alSourcei(_sources[i].sourceId, AL_LOOPING, FALSE);//Turn off looping otherwise loops will never end
+ while (state == AL_PLAYING) {
+ alGetSourcei(_sources[i].sourceId, AL_SOURCE_STATE, &state);
+ usleep(10000);
+ }
+ }
+#endif
+ //Stop source and detach
+ alSourceStop(_sources[i].sourceId);
+ if((lastErrorCode_ = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDSoundEngine - error stopping source: %x\n", lastErrorCode_);
+ }
+
+ alSourcei(_sources[i].sourceId, AL_BUFFER, 0);//Attach to "NULL" buffer to detach
+ if((lastErrorCode_ = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDSoundEngine - error detaching buffer: %x\n", lastErrorCode_);
+ } else {
+ //Record that source is now attached to nothing
+ _sources[i].attachedBufferId = 0;
+ }
+ }
+ }
+
+ alDeleteBuffers(1, &_buffers[soundId].bufferId);
+ if((lastErrorCode_ = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDSoundEngine - error deleting buffer: %x\n", lastErrorCode_);
+ _buffers[soundId].bufferState = CD_BS_FAILED;
+ return FALSE;
+ } else {
+#ifdef CD_USE_STATIC_BUFFERS
+ //Free previous data, if alDeleteBuffer has returned without error then no
+ if (_buffers[soundId].bufferData) {
+ CDLOGINFO(@"Denshion::CDSoundEngine - freeing static data for soundId %i @ %i",soundId,_buffers[soundId].bufferData);
+ free(_buffers[soundId].bufferData);//Free the old data
+ _buffers[soundId].bufferData = NULL;
+ }
+#endif
+ }
+
+ alGenBuffers(1, &_buffers[soundId].bufferId);
+ if((lastErrorCode_ = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDSoundEngine - error regenerating buffer: %x\n", lastErrorCode_);
+ _buffers[soundId].bufferState = CD_BS_FAILED;
+ return FALSE;
+ } else {
+ //We now have an empty buffer
+ _buffers[soundId].bufferState = CD_BS_EMPTY;
+ CDLOGINFO(@"Denshion::CDSoundEngine - buffer %i successfully unloaded\n",soundId);
+ return TRUE;
+ }
+}
+
+/**
+ * Load buffers asynchronously
+ * Check asynchLoadProgress for progress. asynchLoadProgress represents fraction of completion. When it equals 1.0 loading
+ * is complete. NB: asynchLoadProgress is simply based on the number of load requests, it does not take into account
+ * file sizes.
+ * @param An array of CDBufferLoadRequest objects
+ */
+- (void) loadBuffersAsynchronously:(NSArray *) loadRequests {
+ @synchronized(self) {
+ asynchLoadProgress_ = 0.0f;
+ CDAsynchBufferLoader *loaderOp = [[[CDAsynchBufferLoader alloc] init:loadRequests soundEngine:self] autorelease];
+ NSOperationQueue *opQ = [[[NSOperationQueue alloc] init] autorelease];
+ [opQ addOperation:loaderOp];
+ }
+}
+
+-(BOOL) _resizeBuffers:(int) increment {
+
+ void * tmpBufferInfos = realloc( _buffers, sizeof(_buffers[0]) * (bufferTotal + increment) );
+
+ if(!tmpBufferInfos) {
+ free(tmpBufferInfos);
+ return NO;
+ } else {
+ _buffers = tmpBufferInfos;
+ int oldBufferTotal = bufferTotal;
+ bufferTotal = bufferTotal + increment;
+ [self _generateBuffers:oldBufferTotal endIndex:bufferTotal-1];
+ return YES;
+ }
+}
+
+-(BOOL) loadBufferFromData:(int) soundId soundData:(ALvoid*) soundData format:(ALenum) format size:(ALsizei) size freq:(ALsizei) freq {
+
+ @synchronized(_mutexBufferLoad) {
+
+ if (!functioning_) {
+ //OpenAL initialisation has previously failed
+ CDLOG(@"Denshion::CDSoundEngine - Loading buffer failed because sound engine state != functioning");
+ return FALSE;
+ }
+
+ //Ensure soundId is within array bounds otherwise memory corruption will occur
+ if (soundId < 0) {
+ CDLOG(@"Denshion::CDSoundEngine - soundId is negative");
+ return FALSE;
+ }
+
+ if (soundId >= bufferTotal) {
+ //Need to resize the buffers
+ int requiredIncrement = CD_BUFFERS_INCREMENT;
+ while (bufferTotal + requiredIncrement < soundId) {
+ requiredIncrement += CD_BUFFERS_INCREMENT;
+ }
+ CDLOGINFO(@"Denshion::CDSoundEngine - attempting to resize buffers by %i for sound %i",requiredIncrement,soundId);
+ if (![self _resizeBuffers:requiredIncrement]) {
+ CDLOG(@"Denshion::CDSoundEngine - buffer resize failed");
+ return FALSE;
+ }
+ }
+
+ if (soundData)
+ {
+ if (_buffers[soundId].bufferState != CD_BS_EMPTY) {
+ CDLOGINFO(@"Denshion::CDSoundEngine - non empty buffer, regenerating");
+ if (![self unloadBuffer:soundId]) {
+ //Deletion of buffer failed, delete buffer routine has set buffer state and lastErrorCode
+ return NO;
+ }
+ }
+
+#ifdef CD_DEBUG
+ //Check that sample rate matches mixer rate and warn if they do not
+ if (freq != (int)_mixerSampleRate) {
+ CDLOGINFO(@"Denshion::CDSoundEngine - WARNING sample rate does not match mixer sample rate performance may not be optimal.");
+ }
+#endif
+
+#ifdef CD_USE_STATIC_BUFFERS
+ alBufferDataStaticProc(_buffers[soundId].bufferId, format, soundData, size, freq);
+ _buffers[soundId].bufferData = data;//Save the pointer to the new data
+#else
+ alBufferData(_buffers[soundId].bufferId, format, soundData, size, freq);
+#endif
+ if((lastErrorCode_ = alGetError()) != AL_NO_ERROR) {
+ CDLOG(@"Denshion::CDSoundEngine - error attaching audio to buffer: %x", lastErrorCode_);
+ _buffers[soundId].bufferState = CD_BS_FAILED;
+ return FALSE;
+ }
+ } else {
+ CDLOG(@"Denshion::CDSoundEngine Buffer data is null!");
+ _buffers[soundId].bufferState = CD_BS_FAILED;
+ return FALSE;
+ }
+
+ _buffers[soundId].format = format;
+ _buffers[soundId].sizeInBytes = size;
+ _buffers[soundId].frequencyInHertz = freq;
+ _buffers[soundId].bufferState = CD_BS_LOADED;
+ CDLOGINFO(@"Denshion::CDSoundEngine Buffer %i loaded format:%i freq:%i size:%i",soundId,format,freq,size);
+ return TRUE;
+ }//end mutex
+}
+
+/**
+ * Load sound data for later play back.
+ * @return TRUE if buffer loaded okay for play back otherwise false
+ */
+- (BOOL) loadBuffer:(int) soundId filePath:(NSString*) filePath
+{
+
+ ALvoid* data;
+ ALenum format;
+ ALsizei size;
+ ALsizei freq;
+
+ CDLOGINFO(@"Denshion::CDSoundEngine - Loading openAL buffer %i %@", soundId, filePath);
+
+ CFURLRef fileURL = nil;
+ NSString *path = [CDUtilities fullPathFromRelativePath:filePath];
+ if (path) {
+ fileURL = (CFURLRef)[[NSURL fileURLWithPath:path] retain];
+ }
+
+ if (fileURL)
+ {
+ data = CDGetOpenALAudioData(fileURL, &size, &format, &freq);
+ CFRelease(fileURL);
+ BOOL result = [self loadBufferFromData:soundId soundData:data format:format size:size freq:freq];
+#ifndef CD_USE_STATIC_BUFFERS
+ free(data);//Data can be freed here because alBufferData performs a memcpy
+#endif
+ return result;
+ } else {
+ CDLOG(@"Denshion::CDSoundEngine Could not find file!\n");
+ //Don't change buffer state here as it will be the same as before method was called
+ return FALSE;
+ }
+}
+
+-(BOOL) validateBufferId:(int) soundId {
+ if (soundId < 0 || soundId >= bufferTotal) {
+ CDLOGINFO(@"Denshion::CDSoundEngine - validateBufferId buffer outside range %i",soundId);
+ return NO;
+ } else if (_buffers[soundId].bufferState != CD_BS_LOADED) {
+ CDLOGINFO(@"Denshion::CDSoundEngine - validateBufferId invalide buffer state %i",soundId);
+ return NO;
+ } else {
+ return YES;
+ }
+}
+
+-(float) bufferDurationInSeconds:(int) soundId {
+ if ([self validateBufferId:soundId]) {
+ float factor = 0.0f;
+ switch (_buffers[soundId].format) {
+ case AL_FORMAT_MONO8:
+ factor = 1.0f;
+ break;
+ case AL_FORMAT_MONO16:
+ factor = 0.5f;
+ break;
+ case AL_FORMAT_STEREO8:
+ factor = 0.5f;
+ break;
+ case AL_FORMAT_STEREO16:
+ factor = 0.25f;
+ break;
+ }
+ return (float)_buffers[soundId].sizeInBytes/(float)_buffers[soundId].frequencyInHertz * factor;
+ } else {
+ return -1.0f;
+ }
+}
+
+-(ALsizei) bufferSizeInBytes:(int) soundId {
+ if ([self validateBufferId:soundId]) {
+ return _buffers[soundId].sizeInBytes;
+ } else {
+ return -1.0f;
+ }
+}
+
+-(ALsizei) bufferFrequencyInHertz:(int) soundId {
+ if ([self validateBufferId:soundId]) {
+ return _buffers[soundId].frequencyInHertz;
+ } else {
+ return -1.0f;
+ }
+}
+
+- (ALfloat) masterGain {
+ if (mute_) {
+ //When mute the real gain will always be 0 therefore return the preMuteGain value
+ return _preMuteGain;
+ } else {
+ ALfloat gain;
+ alGetListenerf(AL_GAIN, &gain);
+ return gain;
+ }
+}
+
+/**
+ * Overall gain setting multiplier. e.g 0.5 is half the gain.
+ */
+- (void) setMasterGain:(ALfloat) newGainValue {
+ if (mute_) {
+ _preMuteGain = newGainValue;
+ } else {
+ alListenerf(AL_GAIN, newGainValue);
+ }
+}
+
+#pragma mark CDSoundEngine AudioInterrupt protocol
+- (BOOL) mute {
+ return mute_;
+}
+
+/**
+ * Setting mute silences all sounds but playing sounds continue to advance playback
+ */
+- (void) setMute:(BOOL) newMuteValue {
+
+ if (newMuteValue == mute_) {
+ return;
+ }
+
+ mute_ = newMuteValue;
+ if (mute_) {
+ //Remember what the gain was
+ _preMuteGain = self.masterGain;
+ //Set gain to 0 - do not use the property as this will adjust preMuteGain when muted
+ alListenerf(AL_GAIN, 0.0f);
+ } else {
+ //Restore gain to what it was before being muted
+ self.masterGain = _preMuteGain;
+ }
+}
+
+- (BOOL) enabled {
+ return enabled_;
+}
+
+- (void) setEnabled:(BOOL)enabledValue
+{
+ if (enabled_ == enabledValue) {
+ return;
+ }
+ enabled_ = enabledValue;
+ if (enabled_ == NO) {
+ [self stopAllSounds];
+ }
+}
+
+-(void) _lockSource:(int) sourceIndex lock:(BOOL) lock {
+ BOOL found = NO;
+ for (int i=0; i < _sourceGroupTotal && !found; i++) {
+ if (_sourceGroups[i].sourceStatuses) {
+ for (int j=0; j < _sourceGroups[i].totalSources && !found; j++) {
+ //First bit is used to indicate whether source is locked, index is shifted back 1 bit
+ if((_sourceGroups[i].sourceStatuses[j] >> 1)==sourceIndex) {
+ if (lock) {
+ //Set first bit to lock this source
+ _sourceGroups[i].sourceStatuses[j] |= 1;
+ } else {
+ //Unset first bit to unlock this source
+ _sourceGroups[i].sourceStatuses[j] &= ~1;
+ }
+ found = YES;
+ }
+ }
+ }
+ }
+}
+
+-(int) _getSourceIndexForSourceGroup:(int)sourceGroupId
+{
+ //Ensure source group id is valid to prevent memory corruption
+ if (sourceGroupId < 0 || sourceGroupId >= _sourceGroupTotal) {
+ CDLOG(@"Denshion::CDSoundEngine invalid source group id %i",sourceGroupId);
+ return CD_NO_SOURCE;
+ }
+
+ int sourceIndex = -1;//Using -1 to indicate no source found
+ BOOL complete = NO;
+ ALint sourceState = 0;
+ sourceGroup *thisSourceGroup = &_sourceGroups[sourceGroupId];
+ thisSourceGroup->currentIndex = thisSourceGroup->startIndex;
+ while (!complete) {
+ //Iterate over sources looking for one that is not locked, first bit indicates if source is locked
+ if ((thisSourceGroup->sourceStatuses[thisSourceGroup->currentIndex] & 1) == 0) {
+ //This source is not locked
+ sourceIndex = thisSourceGroup->sourceStatuses[thisSourceGroup->currentIndex] >> 1;//shift back to get the index
+ if (thisSourceGroup->nonInterruptible) {
+ //Check if this source is playing, if so it can't be interrupted
+ alGetSourcei(_sources[sourceIndex].sourceId, AL_SOURCE_STATE, &sourceState);
+ if (sourceState != AL_PLAYING) {
+ //complete = YES;
+ //Set start index so next search starts at the next position
+ thisSourceGroup->startIndex = thisSourceGroup->currentIndex + 1;
+ break;
+ } else {
+ sourceIndex = -1;//The source index was no good because the source was playing
+ }
+ } else {
+ //complete = YES;
+ //Set start index so next search starts at the next position
+ thisSourceGroup->startIndex = thisSourceGroup->currentIndex + 1;
+ break;
+ }
+ }
+ thisSourceGroup->currentIndex++;
+ if (thisSourceGroup->currentIndex >= thisSourceGroup->totalSources) {
+ //Reset to the beginning
+ thisSourceGroup->currentIndex = 0;
+ }
+ if (thisSourceGroup->currentIndex == thisSourceGroup->startIndex) {
+ //We have looped around and got back to the start
+ complete = YES;
+ }
+ }
+
+ //Reset start index to beginning if beyond bounds
+ if (thisSourceGroup->startIndex >= thisSourceGroup->totalSources) {
+ thisSourceGroup->startIndex = 0;
+ }
+
+ if (sourceIndex >= 0) {
+ return sourceIndex;
+ } else {
+ return CD_NO_SOURCE;
+ }
+
+}
+
+/**
+ * Play a sound.
+ * @param soundId the id of the sound to play (buffer id).
+ * @param SourceGroupId the source group that will be used to play the sound.
+ * @param pitch pitch multiplier. e.g 1.0 is unaltered, 0.5 is 1 octave lower.
+ * @param pan stereo position. -1 is fully left, 0 is centre and 1 is fully right.
+ * @param gain gain multiplier. e.g. 1.0 is unaltered, 0.5 is half the gain
+ * @param loop should the sound be looped or one shot.
+ * @return the id of the source being used to play the sound or CD_MUTE if the sound engine is muted or non functioning
+ * or CD_NO_SOURCE if a problem occurs setting up the source
+ *
+ */
+- (ALuint)playSound:(int) soundId sourceGroupId:(int)sourceGroupId pitch:(float) pitch pan:(float) pan gain:(float) gain loop:(BOOL) loop {
+
+#ifdef CD_DEBUG
+ //Sanity check parameters - only in DEBUG
+ NSAssert(soundId >= 0, @"soundId can not be negative");
+ NSAssert(soundId < bufferTotal, @"soundId exceeds limit");
+ NSAssert(sourceGroupId >= 0, @"sourceGroupId can not be negative");
+ NSAssert(sourceGroupId < _sourceGroupTotal, @"sourceGroupId exceeds limit");
+ NSAssert(pitch > 0, @"pitch must be greater than zero");
+ NSAssert(pan >= -1 && pan <= 1, @"pan must be between -1 and 1");
+ NSAssert(gain >= 0, @"gain can not be negative");
+#endif
+ //If mute or initialisation has failed or buffer is not loaded then do nothing
+ if (!enabled_ || !functioning_ || _buffers[soundId].bufferState != CD_BS_LOADED || _sourceGroups[sourceGroupId].enabled) {
+#ifdef CD_DEBUG
+ if (!functioning_) {
+ CDLOGINFO(@"Denshion::CDSoundEngine - sound playback aborted because sound engine is not functioning");
+ } else if (_buffers[soundId].bufferState != CD_BS_LOADED) {
+ CDLOGINFO(@"Denshion::CDSoundEngine - sound playback aborted because buffer %i is not loaded", soundId);
+ }
+#endif
+ return CD_MUTE;
+ }
+
+ int sourceIndex = [self _getSourceIndexForSourceGroup:sourceGroupId];//This method ensures sourceIndex is valid
+
+ if (sourceIndex != CD_NO_SOURCE) {
+ ALint state;
+ ALuint source = _sources[sourceIndex].sourceId;
+ ALuint buffer = _buffers[soundId].bufferId;
+ alGetError();//Clear the error code
+ alGetSourcei(source, AL_SOURCE_STATE, &state);
+ if (state == AL_PLAYING) {
+ alSourceStop(source);
+ }
+ alSourcei(source, AL_BUFFER, buffer);//Attach to sound
+ alSourcef(source, AL_PITCH, pitch);//Set pitch
+ alSourcei(source, AL_LOOPING, loop);//Set looping
+ alSourcef(source, AL_GAIN, gain);//Set gain/volume
+ float sourcePosAL[] = {pan, 0.0f, 0.0f};//Set position - just using left and right panning
+ alSourcefv(source, AL_POSITION, sourcePosAL);
+ alGetError();//Clear the error code
+ alSourcePlay(source);
+ if((lastErrorCode_ = alGetError()) == AL_NO_ERROR) {
+ //Everything was okay
+ _sources[sourceIndex].attachedBufferId = buffer;
+ return source;
+ } else {
+ if (alcGetCurrentContext() == NULL) {
+ CDLOGINFO(@"Denshion::CDSoundEngine - posting bad OpenAL context message");
+ [[NSNotificationCenter defaultCenter] postNotificationName:kCDN_BadAlContext object:nil];
+ }
+ return CD_NO_SOURCE;
+ }
+ } else {
+ return CD_NO_SOURCE;
+ }
+}
+
+-(BOOL) _soundSourceAttachToBuffer:(CDSoundSource*) soundSource soundId:(int) soundId {
+ //Attach the source to the buffer
+ ALint state;
+ ALuint source = soundSource->_sourceId;
+ ALuint buffer = _buffers[soundId].bufferId;
+ alGetSourcei(source, AL_SOURCE_STATE, &state);
+ if (state == AL_PLAYING) {
+ alSourceStop(source);
+ }
+ alGetError();//Clear the error code
+ alSourcei(source, AL_BUFFER, buffer);//Attach to sound data
+ if((lastErrorCode_ = alGetError()) == AL_NO_ERROR) {
+ _sources[soundSource->_sourceIndex].attachedBufferId = buffer;
+ //_sourceBufferAttachments[soundSource->_sourceIndex] = buffer;//Keep track of which
+ soundSource->_soundId = soundId;
+ return YES;
+ } else {
+ return NO;
+ }
+}
+
+/**
+ * Get a sound source for the specified sound in the specified source group
+ */
+-(CDSoundSource *) soundSourceForSound:(int) soundId sourceGroupId:(int) sourceGroupId
+{
+ if (!functioning_) {
+ return nil;
+ }
+ //Check if a source is available
+ int sourceIndex = [self _getSourceIndexForSourceGroup:sourceGroupId];
+ if (sourceIndex != CD_NO_SOURCE) {
+ CDSoundSource *result = [[CDSoundSource alloc] init:_sources[sourceIndex].sourceId sourceIndex:sourceIndex soundEngine:self];
+ [self _lockSource:sourceIndex lock:YES];
+ //Try to attach to the buffer
+ if ([self _soundSourceAttachToBuffer:result soundId:soundId]) {
+ //Set to a known state
+ result.pitch = 1.0f;
+ result.pan = 0.0f;
+ result.gain = 1.0f;
+ result.looping = NO;
+ return [result autorelease];
+ } else {
+ //Release the sound source we just created, this will also unlock the source
+ [result release];
+ return nil;
+ }
+ } else {
+ //No available source within that source group
+ return nil;
+ }
+}
+
+-(void) _soundSourcePreRelease:(CDSoundSource *) soundSource {
+ CDLOGINFO(@"Denshion::CDSoundEngine _soundSourcePreRelease %i",soundSource->_sourceIndex);
+ //Unlock the sound source's source
+ [self _lockSource:soundSource->_sourceIndex lock:NO];
+}
+
+/**
+ * Stop all sounds playing within a source group
+ */
+- (void) stopSourceGroup:(int) sourceGroupId {
+
+ if (!functioning_ || sourceGroupId >= _sourceGroupTotal || sourceGroupId < 0) {
+ return;
+ }
+ int sourceCount = _sourceGroups[sourceGroupId].totalSources;
+ for (int i=0; i < sourceCount; i++) {
+ int sourceIndex = _sourceGroups[sourceGroupId].sourceStatuses[i] >> 1;
+ alSourceStop(_sources[sourceIndex].sourceId);
+ }
+ alGetError();//Clear error in case we stopped any sounds that couldn't be stopped
+}
+
+/**
+ * Stop a sound playing.
+ * @param sourceId an OpenAL source identifier i.e. the return value of playSound
+ */
+- (void)stopSound:(ALuint) sourceId {
+ if (!functioning_) {
+ return;
+ }
+ alSourceStop(sourceId);
+ alGetError();//Clear error in case we stopped any sounds that couldn't be stopped
+}
+
+- (void) stopAllSounds {
+ for (int i=0; i < sourceTotal_; i++) {
+ alSourceStop(_sources[i].sourceId);
+ }
+ alGetError();//Clear error in case we stopped any sounds that couldn't be stopped
+}
+
+/**
+ * Set a source group as non interruptible. Default is that source groups are interruptible.
+ * Non interruptible means that if a request to play a sound is made for a source group and there are
+ * no free sources available then the play request will be ignored and CD_NO_SOURCE will be returned.
+ */
+- (void) setSourceGroupNonInterruptible:(int) sourceGroupId isNonInterruptible:(BOOL) isNonInterruptible {
+ //Ensure source group id is valid to prevent memory corruption
+ if (sourceGroupId < 0 || sourceGroupId >= _sourceGroupTotal) {
+ CDLOG(@"Denshion::CDSoundEngine setSourceGroupNonInterruptible invalid source group id %i",sourceGroupId);
+ return;
+ }
+
+ if (isNonInterruptible) {
+ _sourceGroups[sourceGroupId].nonInterruptible = true;
+ } else {
+ _sourceGroups[sourceGroupId].nonInterruptible = false;
+ }
+}
+
+/**
+ * Set the mute property for a source group. If mute is turned on any sounds in that source group
+ * will be stopped and further sounds in that source group will play. However, turning mute off
+ * will not restart any sounds that were playing when mute was turned on. Also the mute setting
+ * for the sound engine must be taken into account. If the sound engine is mute no sounds will play
+ * no matter what the source group mute setting is.
+ */
+- (void) setSourceGroupEnabled:(int) sourceGroupId enabled:(BOOL) enabled {
+ //Ensure source group id is valid to prevent memory corruption
+ if (sourceGroupId < 0 || sourceGroupId >= _sourceGroupTotal) {
+ CDLOG(@"Denshion::CDSoundEngine setSourceGroupEnabled invalid source group id %i",sourceGroupId);
+ return;
+ }
+
+ if (enabled) {
+ _sourceGroups[sourceGroupId].enabled = true;
+ [self stopSourceGroup:sourceGroupId];
+ } else {
+ _sourceGroups[sourceGroupId].enabled = false;
+ }
+}
+
+/**
+ * Return the mute property for the source group identified by sourceGroupId
+ */
+- (BOOL) sourceGroupEnabled:(int) sourceGroupId {
+ return _sourceGroups[sourceGroupId].enabled;
+}
+
+-(ALCcontext *) openALContext {
+ return context;
+}
+
+- (void) _dumpSourceGroupsInfo {
+#ifdef CD_DEBUG
+ CDLOGINFO(@"-------------- source Group Info --------------");
+ for (int i=0; i < _sourceGroupTotal; i++) {
+ CDLOGINFO(@"Group: %i start:%i total:%i",i,_sourceGroups[i].startIndex, _sourceGroups[i].totalSources);
+ CDLOGINFO(@"----- mute:%i nonInterruptible:%i",_sourceGroups[i].enabled, _sourceGroups[i].nonInterruptible);
+ CDLOGINFO(@"----- Source statuses ----");
+ for (int j=0; j < _sourceGroups[i].totalSources; j++) {
+ CDLOGINFO(@"Source status:%i index=%i locked=%i",j,_sourceGroups[i].sourceStatuses[j] >> 1, _sourceGroups[i].sourceStatuses[j] & 1);
+ }
+ }
+#endif
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+@implementation CDSoundSource
+
+@synthesize lastError;
+
+//Macro for handling the al error code
+#define CDSOUNDSOURCE_UPDATE_LAST_ERROR (lastError = alGetError())
+#define CDSOUNDSOURCE_ERROR_HANDLER ( CDSOUNDSOURCE_UPDATE_LAST_ERROR == AL_NO_ERROR)
+
+-(id)init:(ALuint) theSourceId sourceIndex:(int) index soundEngine:(CDSoundEngine*) engine {
+ if ((self = [super init])) {
+ _sourceId = theSourceId;
+ _engine = engine;
+ _sourceIndex = index;
+ enabled_ = YES;
+ mute_ = NO;
+ _preMuteGain = self.gain;
+ }
+ return self;
+}
+
+-(void) dealloc
+{
+ CDLOGINFO(@"Denshion::CDSoundSource deallocated %i",self->_sourceIndex);
+
+ //Notify sound engine we are about to release
+ [_engine _soundSourcePreRelease:self];
+ [super dealloc];
+}
+
+- (void) setPitch:(float) newPitchValue {
+ alSourcef(_sourceId, AL_PITCH, newPitchValue);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+}
+
+- (void) setGain:(float) newGainValue {
+ if (!mute_) {
+ alSourcef(_sourceId, AL_GAIN, newGainValue);
+ } else {
+ _preMuteGain = newGainValue;
+ }
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+}
+
+- (void) setPan:(float) newPanValue {
+ float sourcePosAL[] = {newPanValue, 0.0f, 0.0f};//Set position - just using left and right panning
+ alSourcefv(_sourceId, AL_POSITION, sourcePosAL);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+
+}
+
+- (void) setLooping:(BOOL) newLoopingValue {
+ alSourcei(_sourceId, AL_LOOPING, newLoopingValue);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+
+}
+
+- (BOOL) isPlaying {
+ ALint state;
+ alGetSourcei(_sourceId, AL_SOURCE_STATE, &state);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+ return (state == AL_PLAYING);
+}
+
+- (float) pitch {
+ ALfloat pitchVal;
+ alGetSourcef(_sourceId, AL_PITCH, &pitchVal);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+ return pitchVal;
+}
+
+- (float) pan {
+ ALfloat sourcePosAL[] = {0.0f,0.0f,0.0f};
+ alGetSourcefv(_sourceId, AL_POSITION, sourcePosAL);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+ return sourcePosAL[0];
+}
+
+- (float) gain {
+ if (!mute_) {
+ ALfloat val;
+ alGetSourcef(_sourceId, AL_GAIN, &val);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+ return val;
+ } else {
+ return _preMuteGain;
+ }
+}
+
+- (BOOL) looping {
+ ALfloat val;
+ alGetSourcef(_sourceId, AL_LOOPING, &val);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+ return val;
+}
+
+-(BOOL) stop {
+ alSourceStop(_sourceId);
+ return CDSOUNDSOURCE_ERROR_HANDLER;
+}
+
+-(BOOL) play {
+ if (enabled_) {
+ alSourcePlay(_sourceId);
+ CDSOUNDSOURCE_UPDATE_LAST_ERROR;
+ if (lastError != AL_NO_ERROR) {
+ if (alcGetCurrentContext() == NULL) {
+ CDLOGINFO(@"Denshion::CDSoundSource - posting bad OpenAL context message");
+ [[NSNotificationCenter defaultCenter] postNotificationName:kCDN_BadAlContext object:nil];
+ }
+ return NO;
+ } else {
+ return YES;
+ }
+ } else {
+ return NO;
+ }
+}
+
+-(BOOL) pause {
+ alSourcePause(_sourceId);
+ return CDSOUNDSOURCE_ERROR_HANDLER;
+}
+
+-(BOOL) rewind {
+ alSourceRewind(_sourceId);
+ return CDSOUNDSOURCE_ERROR_HANDLER;
+}
+
+-(void) setSoundId:(int) soundId {
+ [_engine _soundSourceAttachToBuffer:self soundId:soundId];
+}
+
+-(int) soundId {
+ return _soundId;
+}
+
+-(float) durationInSeconds {
+ return [_engine bufferDurationInSeconds:_soundId];
+}
+
+#pragma mark CDSoundSource AudioInterrupt protocol
+- (BOOL) mute {
+ return mute_;
+}
+
+/**
+ * Setting mute silences all sounds but playing sounds continue to advance playback
+ */
+- (void) setMute:(BOOL) newMuteValue {
+
+ if (newMuteValue == mute_) {
+ return;
+ }
+
+ if (newMuteValue) {
+ //Remember what the gain was
+ _preMuteGain = self.gain;
+ self.gain = 0.0f;
+ mute_ = newMuteValue;//Make sure this is done after setting the gain property as the setter behaves differently depending on mute value
+ } else {
+ //Restore gain to what it was before being muted
+ mute_ = newMuteValue;
+ self.gain = _preMuteGain;
+ }
+}
+
+- (BOOL) enabled {
+ return enabled_;
+}
+
+- (void) setEnabled:(BOOL)enabledValue
+{
+ if (enabled_ == enabledValue) {
+ return;
+ }
+ enabled_ = enabledValue;
+ if (enabled_ == NO) {
+ [self stop];
+ }
+}
+
+@end
+
+////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+#pragma mark CDAudioInterruptTargetGroup
+
+@implementation CDAudioInterruptTargetGroup
+
+-(id) init {
+ if ((self = [super init])) {
+ children_ = [[NSMutableArray alloc] initWithCapacity:32];
+ enabled_ = YES;
+ mute_ = NO;
+ }
+ return self;
+}
+
+-(void) addAudioInterruptTarget:(NSObject<CDAudioInterruptProtocol>*) interruptibleTarget {
+ //Synchronize child with group settings;
+ [interruptibleTarget setMute:mute_];
+ [interruptibleTarget setEnabled:enabled_];
+ [children_ addObject:interruptibleTarget];
+}
+
+-(void) removeAudioInterruptTarget:(NSObject<CDAudioInterruptProtocol>*) interruptibleTarget {
+ [children_ removeObjectIdenticalTo:interruptibleTarget];
+}
+
+- (BOOL) mute {
+ return mute_;
+}
+
+/**
+ * Setting mute silences all sounds but playing sounds continue to advance playback
+ */
+- (void) setMute:(BOOL) newMuteValue {
+
+ if (newMuteValue == mute_) {
+ return;
+ }
+
+ for (NSObject<CDAudioInterruptProtocol>* target in children_) {
+ [target setMute:newMuteValue];
+ }
+}
+
+- (BOOL) enabled {
+ return enabled_;
+}
+
+- (void) setEnabled:(BOOL)enabledValue
+{
+ if (enabledValue == enabled_) {
+ return;
+ }
+
+ for (NSObject<CDAudioInterruptProtocol>* target in children_) {
+ [target setEnabled:enabledValue];
+ }
+}
+
+@end
+
+
+
+////////////////////////////////////////////////////////////////////////////
+
+#pragma mark -
+#pragma mark CDAsynchBufferLoader
+
+@implementation CDAsynchBufferLoader
+
+-(id) init:(NSArray *)loadRequests soundEngine:(CDSoundEngine *) theSoundEngine {
+ if ((self = [super init])) {
+ _loadRequests = loadRequests;
+ [_loadRequests retain];
+ _soundEngine = theSoundEngine;
+ [_soundEngine retain];
+ }
+ return self;
+}
+
+-(void) main {
+ CDLOGINFO(@"Denshion::CDAsynchBufferLoader - loading buffers");
+ [super main];
+ _soundEngine.asynchLoadProgress = 0.0f;
+
+ if ([_loadRequests count] > 0) {
+ float increment = 1.0f / [_loadRequests count];
+ //Iterate over load request and load
+ for (CDBufferLoadRequest *loadRequest in _loadRequests) {
+ [_soundEngine loadBuffer:loadRequest.soundId filePath:loadRequest.filePath];
+ _soundEngine.asynchLoadProgress += increment;
+ }
+ }
+
+ //Completed
+ _soundEngine.asynchLoadProgress = 1.0f;
+ [[NSNotificationCenter defaultCenter] postNotificationName:kCDN_AsynchLoadComplete object:nil];
+
+}
+
+-(void) dealloc {
+ [_loadRequests release];
+ [_soundEngine release];
+ [super dealloc];
+}
+
+@end
+
+
+///////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+#pragma mark CDBufferLoadRequest
+
+@implementation CDBufferLoadRequest
+
+@synthesize filePath, soundId;
+
+-(id) init:(int) theSoundId filePath:(const NSString *) theFilePath {
+ if ((self = [super init])) {
+ soundId = theSoundId;
+ filePath = [theFilePath copy];//TODO: is retain necessary or does copy set retain count
+ [filePath retain];
+ }
+ return self;
+}
+
+-(void) dealloc {
+ [filePath release];
+ [super dealloc];
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+#pragma mark CDFloatInterpolator
+
+@implementation CDFloatInterpolator
+@synthesize start,end,interpolationType;
+
+-(float) interpolate:(float) t {
+
+ if (t < 1.0f) {
+ switch (interpolationType) {
+ case kIT_Linear:
+ //Linear interpolation
+ return ((end - start) * t) + start;
+
+ case kIT_SCurve:
+ //Cubic s curve t^2 * (3 - 2t)
+ return ((float)(t * t * (3.0 - (2.0 * t))) * (end - start)) + start;
+
+ case kIT_Exponential:
+ //Formulas taken from EaseAction
+ if (end > start) {
+ //Fade in
+ float logDelta = (t==0) ? 0 : powf(2, 10 * (t/1 - 1)) - 1 * 0.001f;
+ return ((end - start) * logDelta) + start;
+ } else {
+ //Fade Out
+ float logDelta = (-powf(2, -10 * t/1) + 1);
+ return ((end - start) * logDelta) + start;
+ }
+ default:
+ return 0.0f;
+ }
+ } else {
+ return end;
+ }
+}
+
+-(id) init:(tCDInterpolationType) type startVal:(float) startVal endVal:(float) endVal {
+ if ((self = [super init])) {
+ start = startVal;
+ end = endVal;
+ interpolationType = type;
+ }
+ return self;
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+#pragma mark CDPropertyModifier
+
+@implementation CDPropertyModifier
+
+@synthesize stopTargetWhenComplete;
+
+-(id) init:(id) theTarget interpolationType:(tCDInterpolationType) type startVal:(float) startVal endVal:(float) endVal {
+ if ((self = [super init])) {
+ if (target) {
+ //Release the previous target if there is one
+ [target release];
+ }
+ target = theTarget;
+#if CD_DEBUG
+ //Check target is of the required type
+ if (![theTarget isMemberOfClass:[self _allowableType]] ) {
+ CDLOG(@"Denshion::CDPropertyModifier target is not of type %@",[self _allowableType]);
+ NSAssert([theTarget isKindOfClass:[CDSoundEngine class]], @"CDPropertyModifier target not of required type");
+ }
+#endif
+ [target retain];
+ startValue = startVal;
+ endValue = endVal;
+ if (interpolator) {
+ //Release previous interpolator if there is one
+ [interpolator release];
+ }
+ interpolator = [[CDFloatInterpolator alloc] init:type startVal:startVal endVal:endVal];
+ stopTargetWhenComplete = NO;
+ }
+ return self;
+}
+
+-(void) dealloc {
+ CDLOGINFO(@"Denshion::CDPropertyModifier deallocated %@",self);
+ [target release];
+ [interpolator release];
+ [super dealloc];
+}
+
+-(void) modify:(float) t {
+ if (t < 1.0) {
+ [self _setTargetProperty:[interpolator interpolate:t]];
+ } else {
+ //At the end
+ [self _setTargetProperty:endValue];
+ if (stopTargetWhenComplete) {
+ [self _stopTarget];
+ }
+ }
+}
+
+-(float) startValue {
+ return startValue;
+}
+
+-(void) setStartValue:(float) startVal
+{
+ startValue = startVal;
+ interpolator.start = startVal;
+}
+
+-(float) endValue {
+ return startValue;
+}
+
+-(void) setEndValue:(float) endVal
+{
+ endValue = endVal;
+ interpolator.end = endVal;
+}
+
+-(tCDInterpolationType) interpolationType {
+ return interpolator.interpolationType;
+}
+
+-(void) setInterpolationType:(tCDInterpolationType) interpolationType {
+ interpolator.interpolationType = interpolationType;
+}
+
+-(void) _setTargetProperty:(float) newVal {
+
+}
+
+-(float) _getTargetProperty {
+ return 0.0f;
+}
+
+-(void) _stopTarget {
+
+}
+
+-(Class) _allowableType {
+ return [NSObject class];
+}
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+#pragma mark CDSoundSourceFader
+
+@implementation CDSoundSourceFader
+
+-(void) _setTargetProperty:(float) newVal {
+ ((CDSoundSource*)target).gain = newVal;
+}
+
+-(float) _getTargetProperty {
+ return ((CDSoundSource*)target).gain;
+}
+
+-(void) _stopTarget {
+ [((CDSoundSource*)target) stop];
+}
+
+-(Class) _allowableType {
+ return [CDSoundSource class];
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+#pragma mark CDSoundSourcePanner
+
+@implementation CDSoundSourcePanner
+
+-(void) _setTargetProperty:(float) newVal {
+ ((CDSoundSource*)target).pan = newVal;
+}
+
+-(float) _getTargetProperty {
+ return ((CDSoundSource*)target).pan;
+}
+
+-(void) _stopTarget {
+ [((CDSoundSource*)target) stop];
+}
+
+-(Class) _allowableType {
+ return [CDSoundSource class];
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+#pragma mark CDSoundSourcePitchBender
+
+@implementation CDSoundSourcePitchBender
+
+-(void) _setTargetProperty:(float) newVal {
+ ((CDSoundSource*)target).pitch = newVal;
+}
+
+-(float) _getTargetProperty {
+ return ((CDSoundSource*)target).pitch;
+}
+
+-(void) _stopTarget {
+ [((CDSoundSource*)target) stop];
+}
+
+-(Class) _allowableType {
+ return [CDSoundSource class];
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+#pragma mark CDSoundEngineFader
+
+@implementation CDSoundEngineFader
+
+-(void) _setTargetProperty:(float) newVal {
+ ((CDSoundEngine*)target).masterGain = newVal;
+}
+
+-(float) _getTargetProperty {
+ return ((CDSoundEngine*)target).masterGain;
+}
+
+-(void) _stopTarget {
+ [((CDSoundEngine*)target) stopAllSounds];
+}
+
+-(Class) _allowableType {
+ return [CDSoundEngine class];
+}
+
+@end
+
+
--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+
+
+#import "CDAudioManager.h"
+
+/**
+ A wrapper to the CDAudioManager object.
+ This is recommended for basic audio requirements. If you just want to play some sound fx
+ and some background music and have no interest in learning the lower level workings then
+ this is the interface to use.
+
+ Requirements:
+ - Firmware: OS 2.2 or greater
+ - Files: SimpleAudioEngine.*, CocosDenshion.*
+ - Frameworks: OpenAL, AudioToolbox, AVFoundation
+ @since v0.8
+ */
+@interface SimpleAudioEngine : NSObject <CDAudioInterruptProtocol> {
+
+ BOOL mute_;
+ BOOL enabled_;
+}
+
+/** Background music volume. Range is 0.0f to 1.0f. This will only have an effect if willPlayBackgroundMusic returns YES */
+@property (readwrite) float backgroundMusicVolume;
+/** Effects volume. Range is 0.0f to 1.0f */
+@property (readwrite) float effectsVolume;
+/** If NO it indicates background music will not be played either because no background music is loaded or the audio session does not permit it.*/
+@property (readonly) BOOL willPlayBackgroundMusic;
+
+/** returns the shared instance of the SimpleAudioEngine object */
++ (SimpleAudioEngine*) sharedEngine;
+
+/** Preloads a music file so it will be ready to play as background music */
+-(void) preloadBackgroundMusic:(NSString*) filePath;
+
+/** plays background music in a loop*/
+-(void) playBackgroundMusic:(NSString*) filePath;
+/** plays background music, if loop is true the music will repeat otherwise it will be played once */
+-(void) playBackgroundMusic:(NSString*) filePath loop:(BOOL) loop;
+/** stops playing background music */
+-(void) stopBackgroundMusic;
+/** pauses the background music */
+-(void) pauseBackgroundMusic;
+/** resume background music that has been paused */
+-(void) resumeBackgroundMusic;
+/** rewind the background music */
+-(void) rewindBackgroundMusic;
+/** returns whether or not the background music is playing */
+-(BOOL) isBackgroundMusicPlaying;
+
+/** plays an audio effect with a file path*/
+-(ALuint) playEffect:(NSString*) filePath;
+/** stop a sound that is playing, note you must pass in the soundId that is returned when you started playing the sound with playEffect */
+-(void) stopEffect:(ALuint) soundId;
+/** plays an audio effect with a file path, pitch, pan and gain */
+-(ALuint) playEffect:(NSString*) filePath pitch:(Float32) pitch pan:(Float32) pan gain:(Float32) gain;
+/** preloads an audio effect */
+-(void) preloadEffect:(NSString*) filePath;
+/** unloads an audio effect from memory */
+-(void) unloadEffect:(NSString*) filePath;
+/** Gets a CDSoundSource object set up to play the specified file. */
+-(CDSoundSource *) soundSourceForFile:(NSString*) filePath;
+
+/** Shuts down the shared audio engine instance so that it can be reinitialised */
++(void) end;
+
+@end
--- /dev/null
+/*
+ Copyright (c) 2010 Steve Oldmeadow
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+ $Id$
+ */
+
+#import "SimpleAudioEngine.h"
+
+@implementation SimpleAudioEngine
+
+static SimpleAudioEngine *sharedEngine = nil;
+static CDSoundEngine* soundEngine = nil;
+static CDAudioManager *am = nil;
+static CDBufferManager *bufferManager = nil;
+
+// Init
++ (SimpleAudioEngine *) sharedEngine
+{
+ @synchronized(self) {
+ if (!sharedEngine)
+ sharedEngine = [[SimpleAudioEngine alloc] init];
+ }
+ return sharedEngine;
+}
+
++ (id) alloc
+{
+ @synchronized(self) {
+ NSAssert(sharedEngine == nil, @"Attempted to allocate a second instance of a singleton.");
+ return [super alloc];
+ }
+ return nil;
+}
+
+-(id) init
+{
+ if((self=[super init])) {
+ am = [CDAudioManager sharedManager];
+ soundEngine = am.soundEngine;
+ bufferManager = [[CDBufferManager alloc] initWithEngine:soundEngine];
+ mute_ = NO;
+ enabled_ = YES;
+ }
+ return self;
+}
+
+// Memory
+- (void) dealloc
+{
+ am = nil;
+ soundEngine = nil;
+ bufferManager = nil;
+ [super dealloc];
+}
+
++(void) end
+{
+ am = nil;
+ [CDAudioManager end];
+ [bufferManager release];
+ [sharedEngine release];
+ sharedEngine = nil;
+}
+
+#pragma mark SimpleAudioEngine - background music
+
+-(void) preloadBackgroundMusic:(NSString*) filePath {
+ [am preloadBackgroundMusic:filePath];
+}
+
+-(void) playBackgroundMusic:(NSString*) filePath
+{
+ [am playBackgroundMusic:filePath loop:TRUE];
+}
+
+-(void) playBackgroundMusic:(NSString*) filePath loop:(BOOL) loop
+{
+ [am playBackgroundMusic:filePath loop:loop];
+}
+
+-(void) stopBackgroundMusic
+{
+ [am stopBackgroundMusic];
+}
+
+-(void) pauseBackgroundMusic {
+ [am pauseBackgroundMusic];
+}
+
+-(void) resumeBackgroundMusic {
+ [am resumeBackgroundMusic];
+}
+
+-(void) rewindBackgroundMusic {
+ [am rewindBackgroundMusic];
+}
+
+-(BOOL) isBackgroundMusicPlaying {
+ return [am isBackgroundMusicPlaying];
+}
+
+-(BOOL) willPlayBackgroundMusic {
+ return [am willPlayBackgroundMusic];
+}
+
+#pragma mark SimpleAudioEngine - sound effects
+
+-(ALuint) playEffect:(NSString*) filePath
+{
+ return [self playEffect:filePath pitch:1.0f pan:0.0f gain:1.0f];
+}
+
+-(ALuint) playEffect:(NSString*) filePath pitch:(Float32) pitch pan:(Float32) pan gain:(Float32) gain
+{
+ int soundId = [bufferManager bufferForFile:filePath create:YES];
+ if (soundId != kCDNoBuffer) {
+ return [soundEngine playSound:soundId sourceGroupId:0 pitch:pitch pan:pan gain:gain loop:false];
+ } else {
+ return CD_MUTE;
+ }
+}
+
+-(void) stopEffect:(ALuint) soundId {
+ [soundEngine stopSound:soundId];
+}
+
+-(void) preloadEffect:(NSString*) filePath
+{
+ int soundId = [bufferManager bufferForFile:filePath create:YES];
+ if (soundId == kCDNoBuffer) {
+ CDLOG(@"Denshion::SimpleAudioEngine sound failed to preload %@",filePath);
+ }
+}
+
+-(void) unloadEffect:(NSString*) filePath
+{
+ CDLOGINFO(@"Denshion::SimpleAudioEngine unloadedEffect %@",filePath);
+ [bufferManager releaseBufferForFile:filePath];
+}
+
+#pragma mark Audio Interrupt Protocol
+-(BOOL) mute
+{
+ return mute_;
+}
+
+-(void) setMute:(BOOL) muteValue
+{
+ if (mute_ != muteValue) {
+ mute_ = muteValue;
+ am.mute = mute_;
+ }
+}
+
+-(BOOL) enabled
+{
+ return enabled_;
+}
+
+-(void) setEnabled:(BOOL) enabledValue
+{
+ if (enabled_ != enabledValue) {
+ enabled_ = enabledValue;
+ am.enabled = enabled_;
+ }
+}
+
+
+#pragma mark SimpleAudioEngine - BackgroundMusicVolume
+-(float) backgroundMusicVolume
+{
+ return am.backgroundMusic.volume;
+}
+
+-(void) setBackgroundMusicVolume:(float) volume
+{
+ am.backgroundMusic.volume = volume;
+}
+
+#pragma mark SimpleAudioEngine - EffectsVolume
+-(float) effectsVolume
+{
+ return am.soundEngine.masterGain;
+}
+
+-(void) setEffectsVolume:(float) volume
+{
+ am.soundEngine.masterGain = volume;
+}
+
+-(CDSoundSource *) soundSourceForFile:(NSString*) filePath {
+ int soundId = [bufferManager bufferForFile:filePath create:YES];
+ if (soundId != kCDNoBuffer) {
+ CDSoundSource *result = [soundEngine soundSourceForSound:soundId sourceGroupId:0];
+ CDLOGINFO(@"Denshion::SimpleAudioEngine sound source created for %@",filePath);
+ return result;
+ } else {
+ return nil;
+ }
+}
+
+@end
--- /dev/null
+//
+// FontLabel.h
+// FontLabel
+//
+// Created by Kevin Ballard on 5/8/09.
+// Copyright © 2009 Zynga Game Networks
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@class ZFont;
+@class ZAttributedString;
+
+@interface FontLabel : UILabel {
+ void *reserved; // works around a bug in UILabel
+ ZFont *zFont;
+ ZAttributedString *zAttributedText;
+}
+@property (nonatomic, setter=setCGFont:) CGFontRef cgFont __AVAILABILITY_INTERNAL_DEPRECATED;
+@property (nonatomic, assign) CGFloat pointSize __AVAILABILITY_INTERNAL_DEPRECATED;
+@property (nonatomic, retain, setter=setZFont:) ZFont *zFont;
+// if attributedText is nil, fall back on using the inherited UILabel properties
+// if attributedText is non-nil, the font/text/textColor
+// in addition, adjustsFontSizeToFitWidth does not work with attributed text
+@property (nonatomic, copy) ZAttributedString *zAttributedText;
+// -initWithFrame:fontName:pointSize: uses FontManager to look up the font name
+- (id)initWithFrame:(CGRect)frame fontName:(NSString *)fontName pointSize:(CGFloat)pointSize;
+- (id)initWithFrame:(CGRect)frame zFont:(ZFont *)font;
+- (id)initWithFrame:(CGRect)frame font:(CGFontRef)font pointSize:(CGFloat)pointSize __AVAILABILITY_INTERNAL_DEPRECATED;
+@end
--- /dev/null
+//
+// FontLabel.m
+// FontLabel
+//
+// Created by Kevin Ballard on 5/8/09.
+// Copyright © 2009 Zynga Game Networks
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "FontLabel.h"
+#import "FontManager.h"
+#import "FontLabelStringDrawing.h"
+#import "ZFont.h"
+
+@interface ZFont (ZFontPrivate)
+@property (nonatomic, readonly) CGFloat ratio;
+@end
+
+@implementation FontLabel
+@synthesize zFont;
+@synthesize zAttributedText;
+
+- (id)initWithFrame:(CGRect)frame fontName:(NSString *)fontName pointSize:(CGFloat)pointSize {
+ return [self initWithFrame:frame zFont:[[FontManager sharedManager] zFontWithName:fontName pointSize:pointSize]];
+}
+
+- (id)initWithFrame:(CGRect)frame zFont:(ZFont *)font {
+ if ((self = [super initWithFrame:frame])) {
+ zFont = [font retain];
+ }
+ return self;
+}
+
+- (id)initWithFrame:(CGRect)frame font:(CGFontRef)font pointSize:(CGFloat)pointSize {
+ return [self initWithFrame:frame zFont:[ZFont fontWithCGFont:font size:pointSize]];
+}
+
+- (CGFontRef)cgFont {
+ return self.zFont.cgFont;
+}
+
+- (void)setCGFont:(CGFontRef)font {
+ if (self.zFont.cgFont != font) {
+ self.zFont = [ZFont fontWithCGFont:font size:self.zFont.pointSize];
+ }
+}
+
+- (CGFloat)pointSize {
+ return self.zFont.pointSize;
+}
+
+- (void)setPointSize:(CGFloat)pointSize {
+ if (self.zFont.pointSize != pointSize) {
+ self.zFont = [ZFont fontWithCGFont:self.zFont.cgFont size:pointSize];
+ }
+}
+
+- (void)setZAttributedText:(ZAttributedString *)attStr {
+ if (zAttributedText != attStr) {
+ [zAttributedText release];
+ zAttributedText = [attStr copy];
+ [self setNeedsDisplay];
+ }
+}
+
+- (void)drawTextInRect:(CGRect)rect {
+ if (self.zFont == NULL && self.zAttributedText == nil) {
+ [super drawTextInRect:rect];
+ return;
+ }
+
+ if (self.zAttributedText == nil) {
+ // this method is documented as setting the text color for us, but that doesn't appear to be the case
+ if (self.highlighted) {
+ [(self.highlightedTextColor ?: [UIColor whiteColor]) setFill];
+ } else {
+ [(self.textColor ?: [UIColor blackColor]) setFill];
+ }
+
+ ZFont *actualFont = self.zFont;
+ CGSize origSize = rect.size;
+ if (self.numberOfLines == 1) {
+ origSize.height = actualFont.leading;
+ CGPoint point = CGPointMake(rect.origin.x,
+ rect.origin.y + roundf(((rect.size.height - actualFont.leading) / 2.0f)));
+ CGSize size = [self.text sizeWithZFont:actualFont];
+ if (self.adjustsFontSizeToFitWidth && self.minimumFontSize < actualFont.pointSize) {
+ if (size.width > origSize.width) {
+ CGFloat desiredRatio = (origSize.width * actualFont.ratio) / size.width;
+ CGFloat desiredPointSize = desiredRatio * actualFont.pointSize / actualFont.ratio;
+ actualFont = [actualFont fontWithSize:MAX(MAX(desiredPointSize, self.minimumFontSize), 1.0f)];
+ size = [self.text sizeWithZFont:actualFont];
+ }
+ if (!CGSizeEqualToSize(origSize, size)) {
+ switch (self.baselineAdjustment) {
+ case UIBaselineAdjustmentAlignCenters:
+ point.y += roundf((origSize.height - size.height) / 2.0f);
+ break;
+ case UIBaselineAdjustmentAlignBaselines:
+ point.y += (self.zFont.ascender - actualFont.ascender);
+ break;
+ case UIBaselineAdjustmentNone:
+ break;
+ }
+ }
+ }
+ size.width = MIN(size.width, origSize.width);
+ // adjust the point for alignment
+ switch (self.textAlignment) {
+ case UITextAlignmentLeft:
+ break;
+ case UITextAlignmentCenter:
+ point.x += (origSize.width - size.width) / 2.0f;
+ break;
+ case UITextAlignmentRight:
+ point.x += origSize.width - size.width;
+ break;
+ }
+ [self.text drawAtPoint:point forWidth:size.width withZFont:actualFont lineBreakMode:self.lineBreakMode];
+ } else {
+ CGSize size = [self.text sizeWithZFont:actualFont constrainedToSize:origSize lineBreakMode:self.lineBreakMode numberOfLines:self.numberOfLines];
+ CGPoint point = rect.origin;
+ point.y += roundf((rect.size.height - size.height) / 2.0f);
+ rect = (CGRect){point, CGSizeMake(rect.size.width, size.height)};
+ [self.text drawInRect:rect withZFont:actualFont lineBreakMode:self.lineBreakMode alignment:self.textAlignment numberOfLines:self.numberOfLines];
+ }
+ } else {
+ ZAttributedString *attStr = self.zAttributedText;
+ if (self.highlighted) {
+ // modify the string to change the base color
+ ZMutableAttributedString *mutStr = [[attStr mutableCopy] autorelease];
+ NSRange activeRange = NSMakeRange(0, attStr.length);
+ while (activeRange.length > 0) {
+ NSRange effective;
+ UIColor *color = [attStr attribute:ZForegroundColorAttributeName atIndex:activeRange.location
+ longestEffectiveRange:&effective inRange:activeRange];
+ if (color == nil) {
+ [mutStr addAttribute:ZForegroundColorAttributeName value:[UIColor whiteColor] range:effective];
+ }
+ activeRange.location += effective.length, activeRange.length -= effective.length;
+ }
+ attStr = mutStr;
+ }
+ CGSize size = [attStr sizeConstrainedToSize:rect.size lineBreakMode:self.lineBreakMode numberOfLines:self.numberOfLines];
+ CGPoint point = rect.origin;
+ point.y += roundf((rect.size.height - size.height) / 2.0f);
+ rect = (CGRect){point, CGSizeMake(rect.size.width, size.height)};
+ [attStr drawInRect:rect withLineBreakMode:self.lineBreakMode alignment:self.textAlignment numberOfLines:self.numberOfLines];
+ }
+}
+
+- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines {
+ if (self.zFont == NULL && self.zAttributedText == nil) {
+ return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
+ }
+
+ if (numberOfLines == 1) {
+ // if numberOfLines == 1 we need to use the version that converts spaces
+ CGSize size;
+ if (self.zAttributedText == nil) {
+ size = [self.text sizeWithZFont:self.zFont];
+ } else {
+ size = [self.zAttributedText size];
+ }
+ bounds.size.width = MIN(bounds.size.width, size.width);
+ bounds.size.height = MIN(bounds.size.height, size.height);
+ } else {
+ if (numberOfLines > 0) bounds.size.height = MIN(bounds.size.height, self.zFont.leading * numberOfLines);
+ if (self.zAttributedText == nil) {
+ bounds.size = [self.text sizeWithZFont:self.zFont constrainedToSize:bounds.size lineBreakMode:self.lineBreakMode];
+ } else {
+ bounds.size = [self.zAttributedText sizeConstrainedToSize:bounds.size lineBreakMode:self.lineBreakMode];
+ }
+ }
+ return bounds;
+}
+
+- (void)dealloc {
+ [zFont release];
+ [zAttributedText release];
+ [super dealloc];
+}
+@end
--- /dev/null
+//
+// FontLabelStringDrawing.h
+// FontLabel
+//
+// Created by Kevin Ballard on 5/5/09.
+// Copyright © 2009 Zynga Game Networks
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <UIKit/UIKit.h>
+#import "ZAttributedString.h"
+
+@class ZFont;
+
+@interface NSString (FontLabelStringDrawing)
+// CGFontRef-based methods
+- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize __AVAILABILITY_INTERNAL_DEPRECATED;
+- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size __AVAILABILITY_INTERNAL_DEPRECATED;
+- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size
+ lineBreakMode:(UILineBreakMode)lineBreakMode __AVAILABILITY_INTERNAL_DEPRECATED;
+- (CGSize)drawAtPoint:(CGPoint)point withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize __AVAILABILITY_INTERNAL_DEPRECATED;
+- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize __AVAILABILITY_INTERNAL_DEPRECATED;
+- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize
+ lineBreakMode:(UILineBreakMode)lineBreakMode __AVAILABILITY_INTERNAL_DEPRECATED;
+- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize
+ lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment __AVAILABILITY_INTERNAL_DEPRECATED;
+
+// ZFont-based methods
+- (CGSize)sizeWithZFont:(ZFont *)font;
+- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size;
+- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode;
+- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
+ numberOfLines:(NSUInteger)numberOfLines;
+- (CGSize)drawAtPoint:(CGPoint)point withZFont:(ZFont *)font;
+- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode;
+- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font;
+- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode;
+- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
+ alignment:(UITextAlignment)alignment;
+- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
+ alignment:(UITextAlignment)alignment numberOfLines:(NSUInteger)numberOfLines;
+@end
+
+@interface ZAttributedString (ZAttributedStringDrawing)
+- (CGSize)size;
+- (CGSize)sizeConstrainedToSize:(CGSize)size;
+- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode;
+- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
+ numberOfLines:(NSUInteger)numberOfLines;
+- (CGSize)drawAtPoint:(CGPoint)point;
+- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode;
+- (CGSize)drawInRect:(CGRect)rect;
+- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode;
+- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment;
+- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment
+ numberOfLines:(NSUInteger)numberOfLines;
+@end
--- /dev/null
+//
+// FontLabelStringDrawing.m
+// FontLabel
+//
+// Created by Kevin Ballard on 5/5/09.
+// Copyright © 2009 Zynga Game Networks
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "FontLabelStringDrawing.h"
+#import "ZFont.h"
+#import "ZAttributedStringPrivate.h"
+
+@interface ZFont (ZFontPrivate)
+@property (nonatomic, readonly) CGFloat ratio;
+@end
+
+#define kUnicodeHighSurrogateStart 0xD800
+#define kUnicodeHighSurrogateEnd 0xDBFF
+#define kUnicodeHighSurrogateMask kUnicodeHighSurrogateStart
+#define kUnicodeLowSurrogateStart 0xDC00
+#define kUnicodeLowSurrogateEnd 0xDFFF
+#define kUnicodeLowSurrogateMask kUnicodeLowSurrogateStart
+#define kUnicodeSurrogateTypeMask 0xFC00
+#define UnicharIsHighSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeHighSurrogateMask)
+#define UnicharIsLowSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeLowSurrogateMask)
+#define ConvertSurrogatePairToUTF32(high, low) ((UInt32)((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000))
+
+typedef enum {
+ kFontTableFormat4 = 4,
+ kFontTableFormat12 = 12,
+} FontTableFormat;
+
+typedef struct fontTable {
+ NSUInteger retainCount;
+ CFDataRef cmapTable;
+ FontTableFormat format;
+ union {
+ struct {
+ UInt16 segCountX2;
+ UInt16 *endCodes;
+ UInt16 *startCodes;
+ UInt16 *idDeltas;
+ UInt16 *idRangeOffsets;
+ } format4;
+ struct {
+ UInt32 nGroups;
+ struct {
+ UInt32 startCharCode;
+ UInt32 endCharCode;
+ UInt32 startGlyphCode;
+ } *groups;
+ } format12;
+ } cmap;
+} fontTable;
+
+static FontTableFormat supportedFormats[] = { kFontTableFormat4, kFontTableFormat12 };
+static size_t supportedFormatsCount = sizeof(supportedFormats) / sizeof(FontTableFormat);
+
+static fontTable *newFontTable(CFDataRef cmapTable, FontTableFormat format) {
+ fontTable *table = (struct fontTable *)malloc(sizeof(struct fontTable));
+ table->retainCount = 1;
+ table->cmapTable = CFRetain(cmapTable);
+ table->format = format;
+ return table;
+}
+
+static fontTable *retainFontTable(fontTable *table) {
+ if (table != NULL) {
+ table->retainCount++;
+ }
+ return table;
+}
+
+static void releaseFontTable(fontTable *table) {
+ if (table != NULL) {
+ if (table->retainCount <= 1) {
+ CFRelease(table->cmapTable);
+ free(table);
+ } else {
+ table->retainCount--;
+ }
+ }
+}
+
+static const void *fontTableRetainCallback(CFAllocatorRef allocator, const void *value) {
+ return retainFontTable((fontTable *)value);
+}
+
+static void fontTableReleaseCallback(CFAllocatorRef allocator, const void *value) {
+ releaseFontTable((fontTable *)value);
+}
+
+static const CFDictionaryValueCallBacks kFontTableDictionaryValueCallBacks = {
+ .version = 0,
+ .retain = &fontTableRetainCallback,
+ .release = &fontTableReleaseCallback,
+ .copyDescription = NULL,
+ .equal = NULL
+};
+
+// read the cmap table from the font
+// we only know how to understand some of the table formats at the moment
+static fontTable *readFontTableFromCGFont(CGFontRef font) {
+ CFDataRef cmapTable = CGFontCopyTableForTag(font, 'cmap');
+ NSCAssert1(cmapTable != NULL, @"CGFontCopyTableForTag returned NULL for 'cmap' tag in font %@",
+ (font ? [(id)CFCopyDescription(font) autorelease] : @"(null)"));
+ const UInt8 * const bytes = CFDataGetBytePtr(cmapTable);
+ NSCAssert1(OSReadBigInt16(bytes, 0) == 0, @"cmap table for font %@ has bad version number",
+ (font ? [(id)CFCopyDescription(font) autorelease] : @"(null)"));
+ UInt16 numberOfSubtables = OSReadBigInt16(bytes, 2);
+ const UInt8 *unicodeSubtable = NULL;
+ //UInt16 unicodeSubtablePlatformID;
+ UInt16 unicodeSubtablePlatformSpecificID;
+ FontTableFormat unicodeSubtableFormat;
+ const UInt8 * const encodingSubtables = &bytes[4];
+ for (UInt16 i = 0; i < numberOfSubtables; i++) {
+ const UInt8 * const encodingSubtable = &encodingSubtables[8 * i];
+ UInt16 platformID = OSReadBigInt16(encodingSubtable, 0);
+ UInt16 platformSpecificID = OSReadBigInt16(encodingSubtable, 2);
+ // find the best subtable
+ // best is defined by a combination of encoding and format
+ // At the moment we only support format 4, so ignore all other format tables
+ // We prefer platformID == 0, but we will also accept Microsoft's unicode format
+ if (platformID == 0 || (platformID == 3 && platformSpecificID == 1)) {
+ BOOL preferred = NO;
+ if (unicodeSubtable == NULL) {
+ preferred = YES;
+ } else if (platformID == 0 && platformSpecificID > unicodeSubtablePlatformSpecificID) {
+ preferred = YES;
+ }
+ if (preferred) {
+ UInt32 offset = OSReadBigInt32(encodingSubtable, 4);
+ const UInt8 *subtable = &bytes[offset];
+ UInt16 format = OSReadBigInt16(subtable, 0);
+ for (size_t i = 0; i < supportedFormatsCount; i++) {
+ if (format == supportedFormats[i]) {
+ if (format >= 8) {
+ // the version is a fixed-point
+ UInt16 formatFrac = OSReadBigInt16(subtable, 2);
+ if (formatFrac != 0) {
+ // all the current formats with a Fixed version are always *.0
+ continue;
+ }
+ }
+ unicodeSubtable = subtable;
+ //unicodeSubtablePlatformID = platformID;
+ unicodeSubtablePlatformSpecificID = platformSpecificID;
+ unicodeSubtableFormat = format;
+ break;
+ }
+ }
+ }
+ }
+ }
+ fontTable *table = NULL;
+ if (unicodeSubtable != NULL) {
+ table = newFontTable(cmapTable, unicodeSubtableFormat);
+ switch (unicodeSubtableFormat) {
+ case kFontTableFormat4:
+ // subtable format 4
+ //UInt16 length = OSReadBigInt16(unicodeSubtable, 2);
+ //UInt16 language = OSReadBigInt16(unicodeSubtable, 4);
+ table->cmap.format4.segCountX2 = OSReadBigInt16(unicodeSubtable, 6);
+ //UInt16 searchRange = OSReadBigInt16(unicodeSubtable, 8);
+ //UInt16 entrySelector = OSReadBigInt16(unicodeSubtable, 10);
+ //UInt16 rangeShift = OSReadBigInt16(unicodeSubtable, 12);
+ table->cmap.format4.endCodes = (UInt16*)&unicodeSubtable[14];
+ table->cmap.format4.startCodes = (UInt16*)&((UInt8*)table->cmap.format4.endCodes)[table->cmap.format4.segCountX2+2];
+ table->cmap.format4.idDeltas = (UInt16*)&((UInt8*)table->cmap.format4.startCodes)[table->cmap.format4.segCountX2];
+ table->cmap.format4.idRangeOffsets = (UInt16*)&((UInt8*)table->cmap.format4.idDeltas)[table->cmap.format4.segCountX2];
+ //UInt16 *glyphIndexArray = &idRangeOffsets[segCountX2];
+ break;
+ case kFontTableFormat12:
+ table->cmap.format12.nGroups = OSReadBigInt32(unicodeSubtable, 12);
+ table->cmap.format12.groups = (void *)&unicodeSubtable[16];
+ break;
+ default:
+ releaseFontTable(table);
+ table = NULL;
+ }
+ }
+ CFRelease(cmapTable);
+ return table;
+}
+
+// outGlyphs must be at least size n
+static void mapCharactersToGlyphsInFont(const fontTable *table, unichar characters[], size_t charLen, CGGlyph outGlyphs[], size_t *outGlyphLen) {
+ if (table != NULL) {
+ NSUInteger j = 0;
+ switch (table->format) {
+ case kFontTableFormat4: {
+ for (NSUInteger i = 0; i < charLen; i++, j++) {
+ unichar c = characters[i];
+ UInt16 segOffset;
+ BOOL foundSegment = NO;
+ for (segOffset = 0; segOffset < table->cmap.format4.segCountX2; segOffset += 2) {
+ UInt16 endCode = OSReadBigInt16(table->cmap.format4.endCodes, segOffset);
+ if (endCode >= c) {
+ foundSegment = YES;
+ break;
+ }
+ }
+ if (!foundSegment) {
+ // no segment
+ // this is an invalid font
+ outGlyphs[j] = 0;
+ } else {
+ UInt16 startCode = OSReadBigInt16(table->cmap.format4.startCodes, segOffset);
+ if (!(startCode <= c)) {
+ // the code falls in a hole between segments
+ outGlyphs[j] = 0;
+ } else {
+ UInt16 idRangeOffset = OSReadBigInt16(table->cmap.format4.idRangeOffsets, segOffset);
+ if (idRangeOffset == 0) {
+ UInt16 idDelta = OSReadBigInt16(table->cmap.format4.idDeltas, segOffset);
+ outGlyphs[j] = (c + idDelta) % 65536;
+ } else {
+ // use the glyphIndexArray
+ UInt16 glyphOffset = idRangeOffset + 2 * (c - startCode);
+ outGlyphs[j] = OSReadBigInt16(&((UInt8*)table->cmap.format4.idRangeOffsets)[segOffset], glyphOffset);
+ }
+ }
+ }
+ }
+ break;
+ }
+ case kFontTableFormat12: {
+ UInt32 lastSegment = UINT32_MAX;
+ for (NSUInteger i = 0; i < charLen; i++, j++) {
+ unichar c = characters[i];
+ UInt32 c32 = c;
+ if (UnicharIsHighSurrogate(c)) {
+ if (i+1 < charLen) { // do we have another character after this one?
+ unichar cc = characters[i+1];
+ if (UnicharIsLowSurrogate(cc)) {
+ c32 = ConvertSurrogatePairToUTF32(c, cc);
+ i++;
+ }
+ }
+ }
+ // Start the heuristic search
+ // If this is an ASCII char, just do a linear search
+ // Otherwise do a hinted, modified binary search
+ // Start the first pivot at the last range found
+ // And when moving the pivot, limit the movement by increasing
+ // powers of two. This should help with locality
+ __typeof__(table->cmap.format12.groups[0]) *foundGroup = NULL;
+ if (c32 <= 0x7F) {
+ // ASCII
+ for (UInt32 idx = 0; idx < table->cmap.format12.nGroups; idx++) {
+ __typeof__(table->cmap.format12.groups[idx]) *group = &table->cmap.format12.groups[idx];
+ if (c32 < OSSwapBigToHostInt32(group->startCharCode)) {
+ // we've fallen into a hole
+ break;
+ } else if (c32 <= OSSwapBigToHostInt32(group->endCharCode)) {
+ // this is the range
+ foundGroup = group;
+ break;
+ }
+ }
+ } else {
+ // heuristic search
+ UInt32 maxJump = (lastSegment == UINT32_MAX ? UINT32_MAX / 2 : 8);
+ UInt32 lowIdx = 0, highIdx = table->cmap.format12.nGroups; // highIdx is the first invalid idx
+ UInt32 pivot = (lastSegment == UINT32_MAX ? lowIdx + (highIdx - lowIdx) / 2 : lastSegment);
+ while (highIdx > lowIdx) {
+ __typeof__(table->cmap.format12.groups[pivot]) *group = &table->cmap.format12.groups[pivot];
+ if (c32 < OSSwapBigToHostInt32(group->startCharCode)) {
+ highIdx = pivot;
+ } else if (c32 > OSSwapBigToHostInt32(group->endCharCode)) {
+ lowIdx = pivot + 1;
+ } else {
+ // we've hit the range
+ foundGroup = group;
+ break;
+ }
+ if (highIdx - lowIdx > maxJump * 2) {
+ if (highIdx == pivot) {
+ pivot -= maxJump;
+ } else {
+ pivot += maxJump;
+ }
+ maxJump *= 2;
+ } else {
+ pivot = lowIdx + (highIdx - lowIdx) / 2;
+ }
+ }
+ if (foundGroup != NULL) lastSegment = pivot;
+ }
+ if (foundGroup == NULL) {
+ outGlyphs[j] = 0;
+ } else {
+ outGlyphs[j] = (CGGlyph)(OSSwapBigToHostInt32(foundGroup->startGlyphCode) +
+ (c32 - OSSwapBigToHostInt32(foundGroup->startCharCode)));
+ }
+ }
+ break;
+ }
+ }
+ if (outGlyphLen != NULL) *outGlyphLen = j;
+ } else {
+ // we have no table, so just null out the glyphs
+ bzero(outGlyphs, charLen*sizeof(CGGlyph));
+ if (outGlyphLen != NULL) *outGlyphLen = 0;
+ }
+}
+
+static BOOL mapGlyphsToAdvancesInFont(ZFont *font, size_t n, CGGlyph glyphs[], CGFloat outAdvances[]) {
+ int advances[n];
+ if (CGFontGetGlyphAdvances(font.cgFont, glyphs, n, advances)) {
+ CGFloat ratio = font.ratio;
+
+ for (size_t i = 0; i < n; i++) {
+ outAdvances[i] = advances[i]*ratio;
+ }
+ return YES;
+ } else {
+ bzero(outAdvances, n*sizeof(CGFloat));
+ }
+ return NO;
+}
+
+static id getValueOrDefaultForRun(ZAttributeRun *run, NSString *key) {
+ id value = [run.attributes objectForKey:key];
+ if (value == nil) {
+ static NSDictionary *defaultValues = nil;
+ if (defaultValues == nil) {
+ defaultValues = [[NSDictionary alloc] initWithObjectsAndKeys:
+ [ZFont fontWithUIFont:[UIFont systemFontOfSize:12]], ZFontAttributeName,
+ [UIColor blackColor], ZForegroundColorAttributeName,
+ [UIColor clearColor], ZBackgroundColorAttributeName,
+ [NSNumber numberWithInt:ZUnderlineStyleNone], ZUnderlineStyleAttributeName,
+ nil];
+ }
+ value = [defaultValues objectForKey:key];
+ }
+ return value;
+}
+
+static void readRunInformation(NSArray *attributes, NSUInteger len, CFMutableDictionaryRef fontTableMap,
+ NSUInteger index, ZAttributeRun **currentRun, NSUInteger *nextRunStart,
+ ZFont **currentFont, fontTable **currentTable) {
+ *currentRun = [attributes objectAtIndex:index];
+ *nextRunStart = ([attributes count] > index+1 ? [[attributes objectAtIndex:index+1] index] : len);
+ *currentFont = getValueOrDefaultForRun(*currentRun, ZFontAttributeName);
+ if (!CFDictionaryGetValueIfPresent(fontTableMap, (*currentFont).cgFont, (const void **)currentTable)) {
+ *currentTable = readFontTableFromCGFont((*currentFont).cgFont);
+ CFDictionarySetValue(fontTableMap, (*currentFont).cgFont, *currentTable);
+ releaseFontTable(*currentTable);
+ }
+}
+
+static CGSize drawOrSizeTextConstrainedToSize(BOOL performDraw, NSString *string, NSArray *attributes, CGSize constrainedSize, NSUInteger maxLines,
+ UILineBreakMode lineBreakMode, UITextAlignment alignment, BOOL ignoreColor) {
+ NSUInteger len = [string length];
+ NSUInteger idx = 0;
+ CGPoint drawPoint = CGPointZero;
+ CGSize retValue = CGSizeZero;
+ CGContextRef ctx = (performDraw ? UIGraphicsGetCurrentContext() : NULL);
+
+ BOOL convertNewlines = (maxLines == 1);
+
+ // Extract the characters from the string
+ // Convert newlines to spaces if necessary
+ unichar *characters = (unichar *)malloc(sizeof(unichar) * len);
+ if (convertNewlines) {
+ NSCharacterSet *charset = [NSCharacterSet newlineCharacterSet];
+ NSRange range = NSMakeRange(0, len);
+ size_t cIdx = 0;
+ while (range.length > 0) {
+ NSRange newlineRange = [string rangeOfCharacterFromSet:charset options:0 range:range];
+ if (newlineRange.location == NSNotFound) {
+ [string getCharacters:&characters[cIdx] range:range];
+ cIdx += range.length;
+ break;
+ } else {
+ NSUInteger delta = newlineRange.location - range.location;
+ if (newlineRange.location > range.location) {
+ [string getCharacters:&characters[cIdx] range:NSMakeRange(range.location, delta)];
+ }
+ cIdx += delta;
+ characters[cIdx] = (unichar)' ';
+ cIdx++;
+ delta += newlineRange.length;
+ range.location += delta, range.length -= delta;
+ if (newlineRange.length == 1 && range.length >= 1 &&
+ [string characterAtIndex:newlineRange.location] == (unichar)'\r' &&
+ [string characterAtIndex:range.location] == (unichar)'\n') {
+ // CRLF sequence, skip the LF
+ range.location += 1, range.length -= 1;
+ }
+ }
+ }
+ len = cIdx;
+ } else {
+ [string getCharacters:characters range:NSMakeRange(0, len)];
+ }
+
+ // Create storage for glyphs and advances
+ CGGlyph *glyphs;
+ CGFloat *advances;
+ {
+ NSUInteger maxRunLength = 0;
+ ZAttributeRun *a = [attributes objectAtIndex:0];
+ for (NSUInteger i = 1; i < [attributes count]; i++) {
+ ZAttributeRun *b = [attributes objectAtIndex:i];
+ maxRunLength = MAX(maxRunLength, b.index - a.index);
+ a = b;
+ }
+ maxRunLength = MAX(maxRunLength, len - a.index);
+ maxRunLength++; // for a potential ellipsis
+ glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * maxRunLength);
+ advances = (CGFloat *)malloc(sizeof(CGFloat) * maxRunLength);
+ }
+
+ // Use this table to cache all fontTable objects
+ CFMutableDictionaryRef fontTableMap = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks,
+ &kFontTableDictionaryValueCallBacks);
+
+ // Fetch initial style values
+ NSUInteger currentRunIdx = 0;
+ ZAttributeRun *currentRun;
+ NSUInteger nextRunStart;
+ ZFont *currentFont;
+ fontTable *currentTable;
+
+#define READ_RUN() readRunInformation(attributes, len, fontTableMap, \
+ currentRunIdx, ¤tRun, &nextRunStart, \
+ ¤tFont, ¤tTable)
+
+ READ_RUN();
+
+ // fetch the glyphs for the first run
+ size_t glyphCount;
+ NSUInteger glyphIdx;
+
+#define READ_GLYPHS() do { \
+ mapCharactersToGlyphsInFont(currentTable, &characters[currentRun.index], (nextRunStart - currentRun.index), glyphs, &glyphCount); \
+ mapGlyphsToAdvancesInFont(currentFont, (nextRunStart - currentRun.index), glyphs, advances); \
+ glyphIdx = 0; \
+ } while (0)
+
+ READ_GLYPHS();
+
+ NSMutableCharacterSet *alphaCharset = [NSMutableCharacterSet alphanumericCharacterSet];
+ [alphaCharset addCharactersInString:@"([{'\"\u2019\u02BC"];
+
+ // scan left-to-right looking for newlines or until we hit the width constraint
+ // When we hit a wrapping point, calculate truncation as follows:
+ // If we have room to draw at least one more character on the next line, no truncation
+ // Otherwise apply the truncation algorithm to the current line.
+ // After calculating any truncation, draw.
+ // Each time we hit the end of an attribute run, calculate the new font and make sure
+ // it fits (vertically) within the size constraint. If not, truncate this line.
+ // When we draw, iterate over the attribute runs for this line and draw each run separately
+ BOOL lastLine = NO; // used to indicate truncation and to stop the iterating
+ NSUInteger lineCount = 1;
+ while (idx < len && !lastLine) {
+ if (maxLines > 0 && lineCount == maxLines) {
+ lastLine = YES;
+ }
+ // scan left-to-right
+ struct {
+ NSUInteger index;
+ NSUInteger glyphIndex;
+ NSUInteger currentRunIdx;
+ } indexCache = { idx, glyphIdx, currentRunIdx };
+ CGSize lineSize = CGSizeMake(0, currentFont.leading);
+ CGFloat lineAscender = currentFont.ascender;
+ struct {
+ NSUInteger index;
+ NSUInteger glyphIndex;
+ NSUInteger currentRunIdx;
+ CGSize lineSize;
+ } lastWrapCache = {0, 0, 0, CGSizeZero};
+ BOOL inAlpha = NO; // used for calculating wrap points
+
+ BOOL finishLine = NO;
+ for (;idx <= len && !finishLine;) {
+ NSUInteger skipCount = 0;
+ if (idx == len) {
+ finishLine = YES;
+ lastLine = YES;
+ } else {
+ if (idx >= nextRunStart) {
+ // cycle the font and table and grab the next set of glyphs
+ do {
+ currentRunIdx++;
+ READ_RUN();
+ } while (idx >= nextRunStart);
+ READ_GLYPHS();
+ // re-scan the characters to synchronize the glyph index
+ for (NSUInteger j = currentRun.index; j < idx; j++) {
+ if (UnicharIsHighSurrogate(characters[j]) && j+1<len && UnicharIsLowSurrogate(characters[j+1])) {
+ j++;
+ }
+ glyphIdx++;
+ }
+ if (currentFont.leading > lineSize.height) {
+ lineSize.height = currentFont.leading;
+ if (retValue.height + currentFont.ascender > constrainedSize.height) {
+ lastLine = YES;
+ finishLine = YES;
+ }
+ }
+ lineAscender = MAX(lineAscender, currentFont.ascender);
+ }
+ unichar c = characters[idx];
+ // Mark a wrap point before spaces and after any stretch of non-alpha characters
+ BOOL markWrap = NO;
+ if (c == (unichar)' ') {
+ markWrap = YES;
+ } else if ([alphaCharset characterIsMember:c]) {
+ if (!inAlpha) {
+ markWrap = YES;
+ inAlpha = YES;
+ }
+ } else {
+ inAlpha = NO;
+ }
+ if (markWrap) {
+ lastWrapCache = (__typeof__(lastWrapCache)){
+ .index = idx,
+ .glyphIndex = glyphIdx,
+ .currentRunIdx = currentRunIdx,
+ .lineSize = lineSize
+ };
+ }
+ // process the line
+ if (c == (unichar)'\n' || c == 0x0085) { // U+0085 is the NEXT_LINE unicode character
+ finishLine = YES;
+ skipCount = 1;
+ } else if (c == (unichar)'\r') {
+ finishLine = YES;
+ // check for CRLF
+ if (idx+1 < len && characters[idx+1] == (unichar)'\n') {
+ skipCount = 2;
+ } else {
+ skipCount = 1;
+ }
+ } else if (lineSize.width + advances[glyphIdx] > constrainedSize.width) {
+ finishLine = YES;
+ if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) {
+ lastLine = YES;
+ }
+ // walk backwards if wrapping is necessary
+ if (lastWrapCache.index > indexCache.index && lineBreakMode != UILineBreakModeCharacterWrap &&
+ (!lastLine || lineBreakMode != UILineBreakModeClip)) {
+ // we're doing some sort of word wrapping
+ idx = lastWrapCache.index;
+ lineSize = lastWrapCache.lineSize;
+ if (!lastLine) {
+ // re-check if this is the last line
+ if (lastWrapCache.currentRunIdx != currentRunIdx) {
+ currentRunIdx = lastWrapCache.currentRunIdx;
+ READ_RUN();
+ READ_GLYPHS();
+ }
+ if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) {
+ lastLine = YES;
+ }
+ }
+ glyphIdx = lastWrapCache.glyphIndex;
+ // skip any spaces
+ for (NSUInteger j = idx; j < len && characters[j] == (unichar)' '; j++) {
+ skipCount++;
+ }
+ }
+ }
+ }
+ if (finishLine) {
+ // TODO: support head/middle truncation
+ if (lastLine && idx < len && lineBreakMode == UILineBreakModeTailTruncation) {
+ // truncate
+ unichar ellipsis = 0x2026; // ellipsis (…)
+ CGGlyph ellipsisGlyph;
+ mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL);
+ CGFloat ellipsisWidth;
+ mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth);
+ while ((idx - indexCache.index) > 1 && lineSize.width + ellipsisWidth > constrainedSize.width) {
+ // we have more than 1 character and we're too wide, so back up
+ idx--;
+ if (UnicharIsHighSurrogate(characters[idx]) && UnicharIsLowSurrogate(characters[idx+1])) {
+ idx--;
+ }
+ if (idx < currentRun.index) {
+ ZFont *oldFont = currentFont;
+ do {
+ currentRunIdx--;
+ READ_RUN();
+ } while (idx < currentRun.index);
+ READ_GLYPHS();
+ glyphIdx = glyphCount-1;
+ if (oldFont != currentFont) {
+ mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL);
+ mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth);
+ }
+ } else {
+ glyphIdx--;
+ }
+ lineSize.width -= advances[glyphIdx];
+ }
+ // skip any spaces before truncating
+ while ((idx - indexCache.index) > 1 && characters[idx-1] == (unichar)' ') {
+ idx--;
+ if (idx < currentRun.index) {
+ currentRunIdx--;
+ READ_RUN();
+ READ_GLYPHS();
+ glyphIdx = glyphCount-1;
+ } else {
+ glyphIdx--;
+ }
+ lineSize.width -= advances[glyphIdx];
+ }
+ lineSize.width += ellipsisWidth;
+ glyphs[glyphIdx] = ellipsisGlyph;
+ idx++;
+ glyphIdx++;
+ }
+ retValue.width = MAX(retValue.width, lineSize.width);
+ retValue.height += lineSize.height;
+
+ // draw
+ if (performDraw) {
+ switch (alignment) {
+ case UITextAlignmentLeft:
+ drawPoint.x = 0;
+ break;
+ case UITextAlignmentCenter:
+ drawPoint.x = (constrainedSize.width - lineSize.width) / 2.0f;
+ break;
+ case UITextAlignmentRight:
+ drawPoint.x = constrainedSize.width - lineSize.width;
+ break;
+ }
+ NSUInteger stopGlyphIdx = glyphIdx;
+ NSUInteger lastRunIdx = currentRunIdx;
+ NSUInteger stopCharIdx = idx;
+ idx = indexCache.index;
+ if (currentRunIdx != indexCache.currentRunIdx) {
+ currentRunIdx = indexCache.currentRunIdx;
+ READ_RUN();
+ READ_GLYPHS();
+ }
+ glyphIdx = indexCache.glyphIndex;
+ for (NSUInteger drawIdx = currentRunIdx; drawIdx <= lastRunIdx; drawIdx++) {
+ if (drawIdx != currentRunIdx) {
+ currentRunIdx = drawIdx;
+ READ_RUN();
+ READ_GLYPHS();
+ }
+ NSUInteger numGlyphs;
+ if (drawIdx == lastRunIdx) {
+ numGlyphs = stopGlyphIdx - glyphIdx;
+ idx = stopCharIdx;
+ } else {
+ numGlyphs = glyphCount - glyphIdx;
+ idx = nextRunStart;
+ }
+ CGContextSetFont(ctx, currentFont.cgFont);
+ CGContextSetFontSize(ctx, currentFont.pointSize);
+ // calculate the fragment size
+ CGFloat fragmentWidth = 0;
+ for (NSUInteger g = 0; g < numGlyphs; g++) {
+ fragmentWidth += advances[glyphIdx + g];
+ }
+
+ if (!ignoreColor) {
+ UIColor *foregroundColor = getValueOrDefaultForRun(currentRun, ZForegroundColorAttributeName);
+ UIColor *backgroundColor = getValueOrDefaultForRun(currentRun, ZBackgroundColorAttributeName);
+ if (backgroundColor != nil && ![backgroundColor isEqual:[UIColor clearColor]]) {
+ [backgroundColor setFill];
+ UIRectFillUsingBlendMode((CGRect){ drawPoint, { fragmentWidth, lineSize.height } }, kCGBlendModeNormal);
+ }
+ [foregroundColor setFill];
+ }
+
+ CGContextShowGlyphsAtPoint(ctx, drawPoint.x, drawPoint.y + lineAscender, &glyphs[glyphIdx], numGlyphs);
+ NSNumber *underlineStyle = getValueOrDefaultForRun(currentRun, ZUnderlineStyleAttributeName);
+ if ([underlineStyle integerValue] & ZUnderlineStyleMask) {
+ // we only support single for the time being
+ UIRectFill(CGRectMake(drawPoint.x, drawPoint.y + lineAscender, fragmentWidth, 1));
+ }
+ drawPoint.x += fragmentWidth;
+ glyphIdx += numGlyphs;
+ }
+ drawPoint.y += lineSize.height;
+ }
+ idx += skipCount;
+ glyphIdx += skipCount;
+ lineCount++;
+ } else {
+ lineSize.width += advances[glyphIdx];
+ glyphIdx++;
+ idx++;
+ if (idx < len && UnicharIsHighSurrogate(characters[idx-1]) && UnicharIsLowSurrogate(characters[idx])) {
+ // skip the second half of the surrogate pair
+ idx++;
+ }
+ }
+ }
+ }
+ CFRelease(fontTableMap);
+ free(glyphs);
+ free(advances);
+ free(characters);
+
+#undef READ_GLYPHS
+#undef READ_RUN
+
+ return retValue;
+}
+
+static NSArray *attributeRunForFont(ZFont *font) {
+ return [NSArray arrayWithObject:[ZAttributeRun attributeRunWithIndex:0
+ attributes:[NSDictionary dictionaryWithObject:font
+ forKey:ZFontAttributeName]]];
+}
+
+static CGSize drawTextInRect(CGRect rect, NSString *text, NSArray *attributes, UILineBreakMode lineBreakMode,
+ UITextAlignment alignment, NSUInteger numberOfLines, BOOL ignoreColor) {
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
+
+ CGContextSaveGState(ctx);
+
+ // flip it upside-down because our 0,0 is upper-left, whereas ttfs are for screens where 0,0 is lower-left
+ CGAffineTransform textTransform = CGAffineTransformMake(1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f);
+ CGContextSetTextMatrix(ctx, textTransform);
+
+ CGContextTranslateCTM(ctx, rect.origin.x, rect.origin.y);
+
+ CGContextSetTextDrawingMode(ctx, kCGTextFill);
+ CGSize size = drawOrSizeTextConstrainedToSize(YES, text, attributes, rect.size, numberOfLines, lineBreakMode, alignment, ignoreColor);
+
+ CGContextRestoreGState(ctx);
+
+ return size;
+}
+
+@implementation NSString (FontLabelStringDrawing)
+// CGFontRef-based methods
+- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
+ return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize]];
+}
+
+- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size {
+ return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size];
+}
+
+- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size
+ lineBreakMode:(UILineBreakMode)lineBreakMode {
+ return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size lineBreakMode:lineBreakMode];
+}
+
+- (CGSize)drawAtPoint:(CGPoint)point withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
+ return [self drawAtPoint:point withZFont:[ZFont fontWithCGFont:font size:pointSize]];
+}
+
+- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
+ return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize]];
+}
+
+- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize lineBreakMode:(UILineBreakMode)lineBreakMode {
+ return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode];
+}
+
+- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize
+ lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment {
+ return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode alignment:alignment];
+}
+
+// ZFont-based methods
+- (CGSize)sizeWithZFont:(ZFont *)font {
+ CGSize size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
+ UILineBreakModeClip, UITextAlignmentLeft, YES);
+ return CGSizeMake(ceilf(size.width), ceilf(size.height));
+}
+
+- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size {
+ return [self sizeWithZFont:font constrainedToSize:size lineBreakMode:UILineBreakModeWordWrap];
+}
+
+/*
+ According to experimentation with UIStringDrawing, this can actually return a CGSize whose height is greater
+ than the one passed in. The two cases are as follows:
+ 1. If the given size parameter's height is smaller than a single line, the returned value will
+ be the height of one line.
+ 2. If the given size parameter's height falls between multiples of a line height, and the wrapped string
+ actually extends past the size.height, and the difference between size.height and the previous multiple
+ of a line height is >= the font's ascender, then the returned size's height is extended to the next line.
+ To put it simply, if the baseline point of a given line falls in the given size, the entire line will
+ be present in the output size.
+ */
+- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode {
+ size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, 0, lineBreakMode, UITextAlignmentLeft, YES);
+ return CGSizeMake(ceilf(size.width), ceilf(size.height));
+}
+
+- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
+ numberOfLines:(NSUInteger)numberOfLines {
+ size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, numberOfLines, lineBreakMode, UITextAlignmentLeft, YES);
+ return CGSizeMake(ceilf(size.width), ceilf(size.height));
+}
+
+- (CGSize)drawAtPoint:(CGPoint)point withZFont:(ZFont *)font {
+ return [self drawAtPoint:point forWidth:CGFLOAT_MAX withZFont:font lineBreakMode:UILineBreakModeClip];
+}
+
+- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode {
+ return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self, attributeRunForFont(font), lineBreakMode, UITextAlignmentLeft, 1, YES);
+}
+
+- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font {
+ return [self drawInRect:rect withZFont:font lineBreakMode:UILineBreakModeWordWrap];
+}
+
+- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode {
+ return [self drawInRect:rect withZFont:font lineBreakMode:lineBreakMode alignment:UITextAlignmentLeft];
+}
+
+- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
+ alignment:(UITextAlignment)alignment {
+ return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, 0, YES);
+}
+
+- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
+ alignment:(UITextAlignment)alignment numberOfLines:(NSUInteger)numberOfLines {
+ return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, numberOfLines, YES);
+}
+@end
+
+@implementation ZAttributedString (ZAttributedStringDrawing)
+- (CGSize)size {
+ CGSize size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
+ UILineBreakModeClip, UITextAlignmentLeft, NO);
+ return CGSizeMake(ceilf(size.width), ceilf(size.height));
+}
+
+- (CGSize)sizeConstrainedToSize:(CGSize)size {
+ return [self sizeConstrainedToSize:size lineBreakMode:UILineBreakModeWordWrap];
+}
+
+- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode {
+ size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, 0, lineBreakMode, UITextAlignmentLeft, NO);
+ return CGSizeMake(ceilf(size.width), ceilf(size.height));
+}
+
+- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
+ numberOfLines:(NSUInteger)numberOfLines {
+ size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, numberOfLines, lineBreakMode, UITextAlignmentLeft, NO);
+ return CGSizeMake(ceilf(size.width), ceilf(size.height));
+}
+
+- (CGSize)drawAtPoint:(CGPoint)point {
+ return [self drawAtPoint:point forWidth:CGFLOAT_MAX lineBreakMode:UILineBreakModeClip];
+}
+
+- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode {
+ return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self.string, self.attributes, lineBreakMode, UITextAlignmentLeft, 1, NO);
+}
+
+- (CGSize)drawInRect:(CGRect)rect {
+ return [self drawInRect:rect withLineBreakMode:UILineBreakModeWordWrap];
+}
+
+- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode {
+ return [self drawInRect:rect withLineBreakMode:lineBreakMode alignment:UITextAlignmentLeft];
+}
+
+- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment {
+ return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, 0, NO);
+}
+
+- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment
+ numberOfLines:(NSUInteger)numberOfLines {
+ return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, numberOfLines, NO);
+}
+@end
--- /dev/null
+//
+// FontManager.h
+// FontLabel
+//
+// Created by Kevin Ballard on 5/5/09.
+// Copyright © 2009 Zynga Game Networks
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <Foundation/Foundation.h>
+#import <CoreGraphics/CoreGraphics.h>
+
+@class ZFont;
+
+@interface FontManager : NSObject {
+ CFMutableDictionaryRef fonts;
+ NSMutableDictionary *urls;
+}
++ (FontManager *)sharedManager;
+/*!
+ @method
+ @abstract Loads a TTF font from the main bundle
+ @param filename The name of the font file to load (with or without extension).
+ @return YES if the font was loaded, NO if an error occurred
+ @discussion If the font has already been loaded, this method does nothing and returns YES.
+ This method first attempts to load the font by appending .ttf to the filename.
+ If that file does not exist, it tries the filename exactly as given.
+*/
+- (BOOL)loadFont:(NSString *)filename;
+/*!
+ @method
+ @abstract Loads a font from the given file URL
+ @param url A file URL that points to a font file
+ @return YES if the font was loaded, NO if an error occurred
+ @discussion If the font has already been loaded, this method does nothing and returns YES.
+*/
+- (BOOL)loadFontURL:(NSURL *)url;
+/*!
+ @method
+ @abstract Returns the loaded font with the given filename
+ @param filename The name of the font file that was given to -loadFont:
+ @return A CGFontRef, or NULL if the specified font cannot be found
+ @discussion If the font has not been loaded yet, -loadFont: will be
+ called with the given name first.
+*/
+- (CGFontRef)fontWithName:(NSString *)filename __AVAILABILITY_INTERNAL_DEPRECATED;
+/*!
+ @method
+ @abstract Returns a ZFont object corresponding to the loaded font with the given filename and point size
+ @param filename The name of the font file that was given to -loadFont:
+ @param pointSize The point size of the font
+ @return A ZFont, or NULL if the specified font cannot be found
+ @discussion If the font has not been loaded yet, -loadFont: will be
+ called with the given name first.
+*/
+- (ZFont *)zFontWithName:(NSString *)filename pointSize:(CGFloat)pointSize;
+/*!
+ @method
+ @abstract Returns a ZFont object corresponding to the loaded font with the given file URL and point size
+ @param url A file URL that points to a font file
+ @param pointSize The point size of the font
+ @return A ZFont, or NULL if the specified font cannot be loaded
+ @discussion If the font has not been loaded yet, -loadFontURL: will be called with the given URL first.
+*/
+- (ZFont *)zFontWithURL:(NSURL *)url pointSize:(CGFloat)pointSize;
+/*!
+ @method
+ @abstract Returns a CFArrayRef of all loaded CGFont objects
+ @return A CFArrayRef of all loaded CGFont objects
+ @description You are responsible for releasing the CFArrayRef
+*/
+- (CFArrayRef)copyAllFonts;
+@end
--- /dev/null
+//
+// FontManager.m
+// FontLabel
+//
+// Created by Kevin Ballard on 5/5/09.
+// Copyright © 2009 Zynga Game Networks
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "FontManager.h"
+#import "ZFont.h"
+
+static FontManager *sharedFontManager = nil;
+
+@implementation FontManager
++ (FontManager *)sharedManager {
+ @synchronized(self) {
+ if (sharedFontManager == nil) {
+ sharedFontManager = [[self alloc] init];
+ }
+ }
+ return sharedFontManager;
+}
+
+- (id)init {
+ if ((self = [super init])) {
+ fonts = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ urls = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (BOOL)loadFont:(NSString *)filename {
+ NSString *fontPath = [[NSBundle mainBundle] pathForResource:filename ofType:@"ttf"];
+ if (fontPath == nil) {
+ fontPath = [[NSBundle mainBundle] pathForResource:filename ofType:nil];
+ }
+ if (fontPath == nil) return NO;
+
+ NSURL *url = [NSURL fileURLWithPath:fontPath];
+ if ([self loadFontURL:url]) {
+ [urls setObject:url forKey:filename];
+ return YES;
+ }
+ return NO;
+}
+
+- (BOOL)loadFontURL:(NSURL *)url {
+ CGDataProviderRef fontDataProvider = CGDataProviderCreateWithURL((CFURLRef)url);
+ if (fontDataProvider == NULL) return NO;
+ CGFontRef newFont = CGFontCreateWithDataProvider(fontDataProvider);
+ CGDataProviderRelease(fontDataProvider);
+ if (newFont == NULL) return NO;
+
+ CFDictionarySetValue(fonts, url, newFont);
+ CGFontRelease(newFont);
+ return YES;
+}
+
+- (CGFontRef)fontWithName:(NSString *)filename {
+ CGFontRef font = NULL;
+ NSURL *url = [urls objectForKey:filename];
+ if (url == nil && [self loadFont:filename]) {
+ url = [urls objectForKey:filename];
+ }
+ if (url != nil) {
+ font = (CGFontRef)CFDictionaryGetValue(fonts, url);
+ }
+ return font;
+}
+
+- (ZFont *)zFontWithName:(NSString *)filename pointSize:(CGFloat)pointSize {
+ NSURL *url = [urls objectForKey:filename];
+ if (url == nil && [self loadFont:filename]) {
+ url = [urls objectForKey:filename];
+ }
+ if (url != nil) {
+ CGFontRef cgFont = (CGFontRef)CFDictionaryGetValue(fonts, url);
+ if (cgFont != NULL) {
+ return [ZFont fontWithCGFont:cgFont size:pointSize];
+ }
+ }
+ return nil;
+}
+
+- (ZFont *)zFontWithURL:(NSURL *)url pointSize:(CGFloat)pointSize {
+ CGFontRef cgFont = (CGFontRef)CFDictionaryGetValue(fonts, url);
+ if (cgFont == NULL && [self loadFontURL:url]) {
+ cgFont = (CGFontRef)CFDictionaryGetValue(fonts, url);
+ }
+ if (cgFont != NULL) {
+ return [ZFont fontWithCGFont:cgFont size:pointSize];
+ }
+ return nil;
+}
+
+- (CFArrayRef)copyAllFonts {
+ CFIndex count = CFDictionaryGetCount(fonts);
+ CGFontRef *values = (CGFontRef *)malloc(sizeof(CGFontRef) * count);
+ CFDictionaryGetKeysAndValues(fonts, NULL, (const void **)values);
+ CFArrayRef array = CFArrayCreate(NULL, (const void **)values, count, &kCFTypeArrayCallBacks);
+ free(values);
+ return array;
+}
+
+- (void)dealloc {
+ CFRelease(fonts);
+ [urls release];
+ [super dealloc];
+}
+@end
--- /dev/null
+//
+// ZAttributedString.h
+// FontLabel
+//
+// Created by Kevin Ballard on 9/22/09.
+// Copyright 2009 Zynga Game Networks. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+#if NS_BLOCKS_AVAILABLE
+#define Z_BLOCKS 1
+#else
+// set this to 1 if you are using PLBlocks
+#define Z_BLOCKS 0
+#endif
+
+#if Z_BLOCKS
+enum {
+ ZAttributedStringEnumerationReverse = (1UL << 1),
+ ZAttributedStringEnumerationLongestEffectiveRangeNotRequired = (1UL << 20)
+};
+typedef NSUInteger ZAttributedStringEnumerationOptions;
+#endif
+
+@interface ZAttributedString : NSObject <NSCoding, NSCopying, NSMutableCopying> {
+ NSMutableString *_buffer;
+ NSMutableArray *_attributes;
+}
+@property (nonatomic, readonly) NSUInteger length;
+@property (nonatomic, readonly) NSString *string;
+- (id)initWithAttributedString:(ZAttributedString *)attr;
+- (id)initWithString:(NSString *)str;
+- (id)initWithString:(NSString *)str attributes:(NSDictionary *)attributes;
+- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange;
+- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange inRange:(NSRange)rangeLimit;
+- (ZAttributedString *)attributedSubstringFromRange:(NSRange)aRange;
+- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange;
+- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange inRange:(NSRange)rangeLimit;
+#if Z_BLOCKS
+- (void)enumerateAttribute:(NSString *)attrName inRange:(NSRange)enumerationRange options:(ZAttributedStringEnumerationOptions)opts
+ usingBlock:(void (^)(id value, NSRange range, BOOL *stop))block;
+- (void)enumerateAttributesInRange:(NSRange)enumerationRange options:(ZAttributedStringEnumerationOptions)opts
+ usingBlock:(void (^)(NSDictionary *attrs, NSRange range, BOOL *stop))block;
+#endif
+- (BOOL)isEqualToAttributedString:(ZAttributedString *)otherString;
+@end
+
+@interface ZMutableAttributedString : ZAttributedString {
+}
+- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range;
+- (void)addAttributes:(NSDictionary *)attributes range:(NSRange)range;
+- (void)appendAttributedString:(ZAttributedString *)str;
+- (void)deleteCharactersInRange:(NSRange)range;
+- (void)insertAttributedString:(ZAttributedString *)str atIndex:(NSUInteger)idx;
+- (void)removeAttribute:(NSString *)name range:(NSRange)range;
+- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(ZAttributedString *)str;
+- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str;
+- (void)setAttributedString:(ZAttributedString *)str;
+- (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range;
+@end
+
+extern NSString * const ZFontAttributeName;
+extern NSString * const ZForegroundColorAttributeName;
+extern NSString * const ZBackgroundColorAttributeName;
+extern NSString * const ZUnderlineStyleAttributeName;
+
+enum {
+ ZUnderlineStyleNone = 0x00,
+ ZUnderlineStyleSingle = 0x01
+};
+#define ZUnderlineStyleMask 0x00FF
+
+enum {
+ ZUnderlinePatternSolid = 0x0000
+};
+#define ZUnderlinePatternMask 0xFF00
--- /dev/null
+//
+// ZAttributedString.m
+// FontLabel
+//
+// Created by Kevin Ballard on 9/22/09.
+// Copyright 2009 Zynga Game Networks. All rights reserved.
+//
+
+#import "ZAttributedString.h"
+#import "ZAttributedStringPrivate.h"
+
+@interface ZAttributedString ()
+- (NSUInteger)indexOfEffectiveAttributeRunForIndex:(NSUInteger)index;
+- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange uniquingOnName:(NSString *)attributeName;
+- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange
+ inRange:(NSRange)rangeLimit uniquingOnName:(NSString *)attributeName;
+@end
+
+@interface ZAttributedString ()
+@property (nonatomic, readonly) NSArray *attributes;
+@end
+
+@implementation ZAttributedString
+@synthesize string = _buffer;
+@synthesize attributes = _attributes;
+
+- (id)initWithAttributedString:(ZAttributedString *)attr {
+ NSParameterAssert(attr != nil);
+ if ((self = [super init])) {
+ _buffer = [attr->_buffer mutableCopy];
+ _attributes = [[NSMutableArray alloc] initWithArray:attr->_attributes copyItems:YES];
+ }
+ return self;
+}
+
+- (id)initWithString:(NSString *)str {
+ return [self initWithString:str attributes:nil];
+}
+
+- (id)initWithString:(NSString *)str attributes:(NSDictionary *)attributes {
+ if ((self = [super init])) {
+ _buffer = [str mutableCopy];
+ _attributes = [[NSMutableArray alloc] initWithObjects:[ZAttributeRun attributeRunWithIndex:0 attributes:attributes], nil];
+ }
+ return self;
+}
+
+- (id)init {
+ return [self initWithString:@"" attributes:nil];
+}
+
+- (id)initWithCoder:(NSCoder *)decoder {
+ if ((self = [super init])) {
+ _buffer = [[decoder decodeObjectForKey:@"buffer"] mutableCopy];
+ _attributes = [[decoder decodeObjectForKey:@"attributes"] mutableCopy];
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:_buffer forKey:@"buffer"];
+ [aCoder encodeObject:_attributes forKey:@"attributes"];
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ return [self retain];
+}
+
+- (id)mutableCopyWithZone:(NSZone *)zone {
+ return [(ZMutableAttributedString *)[ZMutableAttributedString allocWithZone:zone] initWithAttributedString:self];
+}
+
+- (NSUInteger)length {
+ return [_buffer length];
+}
+
+- (NSString *)description {
+ NSMutableArray *components = [NSMutableArray arrayWithCapacity:[_attributes count]*2];
+ NSRange range = NSMakeRange(0, 0);
+ for (NSUInteger i = 0; i <= [_attributes count]; i++) {
+ range.location = NSMaxRange(range);
+ ZAttributeRun *run;
+ if (i < [_attributes count]) {
+ run = [_attributes objectAtIndex:i];
+ range.length = run.index - range.location;
+ } else {
+ run = nil;
+ range.length = [_buffer length] - range.location;
+ }
+ if (range.length > 0) {
+ [components addObject:[NSString stringWithFormat:@"\"%@\"", [_buffer substringWithRange:range]]];
+ }
+ if (run != nil) {
+ NSMutableArray *attrDesc = [NSMutableArray arrayWithCapacity:[run.attributes count]];
+ for (id key in run.attributes) {
+ [attrDesc addObject:[NSString stringWithFormat:@"%@: %@", key, [run.attributes objectForKey:key]]];
+ }
+ [components addObject:[NSString stringWithFormat:@"{%@}", [attrDesc componentsJoinedByString:@", "]]];
+ }
+ }
+ return [NSString stringWithFormat:@"%@", [components componentsJoinedByString:@" "]];
+}
+
+- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange {
+ NSParameterAssert(attributeName != nil);
+ return [[self attributesAtIndex:index effectiveRange:aRange uniquingOnName:attributeName] objectForKey:attributeName];
+}
+
+- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange inRange:(NSRange)rangeLimit {
+ NSParameterAssert(attributeName != nil);
+ return [[self attributesAtIndex:index longestEffectiveRange:aRange inRange:rangeLimit uniquingOnName:attributeName] objectForKey:attributeName];
+}
+
+- (ZAttributedString *)attributedSubstringFromRange:(NSRange)aRange {
+ if (NSMaxRange(aRange) > [_buffer length]) {
+ @throw [NSException exceptionWithName:NSRangeException reason:@"range was outisde of the attributed string" userInfo:nil];
+ }
+ ZMutableAttributedString *newStr = [self mutableCopy];
+ if (aRange.location > 0) {
+ [newStr deleteCharactersInRange:NSMakeRange(0, aRange.location)];
+ }
+ if (NSMaxRange(aRange) < [_buffer length]) {
+ [newStr deleteCharactersInRange:NSMakeRange(aRange.length, [_buffer length] - NSMaxRange(aRange))];
+ }
+ return [newStr autorelease];
+}
+
+- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange {
+ return [NSDictionary dictionaryWithDictionary:[self attributesAtIndex:index effectiveRange:aRange uniquingOnName:nil]];
+}
+
+- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange inRange:(NSRange)rangeLimit {
+ return [NSDictionary dictionaryWithDictionary:[self attributesAtIndex:index longestEffectiveRange:aRange inRange:rangeLimit uniquingOnName:nil]];
+}
+
+#if Z_BLOCKS
+// Warning: this code has not been tested. The only guarantee is that it compiles.
+- (void)enumerateAttribute:(NSString *)attrName inRange:(NSRange)enumerationRange options:(ZAttributedStringEnumerationOptions)opts
+ usingBlock:(void (^)(id, NSRange, BOOL*))block {
+ if (opts & ZAttributedStringEnumerationLongestEffectiveRangeNotRequired) {
+ [self enumerateAttributesInRange:enumerationRange options:opts usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
+ id value = [attrs objectForKey:attrName];
+ if (value != nil) {
+ block(value, range, stop);
+ }
+ }];
+ } else {
+ __block id oldValue = nil;
+ __block NSRange effectiveRange = NSMakeRange(0, 0);
+ [self enumerateAttributesInRange:enumerationRange options:opts usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
+ id value = [attrs objectForKey:attrName];
+ if (oldValue == nil) {
+ oldValue = value;
+ effectiveRange = range;
+ } else if (value != nil && [oldValue isEqual:value]) {
+ // combine the attributes
+ effectiveRange = NSUnionRange(effectiveRange, range);
+ } else {
+ BOOL innerStop = NO;
+ block(oldValue, effectiveRange, &innerStop);
+ if (innerStop) {
+ *stop = YES;
+ oldValue = nil;
+ } else {
+ oldValue = value;
+ }
+ }
+ }];
+ if (oldValue != nil) {
+ BOOL innerStop = NO; // necessary for the block, but unused
+ block(oldValue, effectiveRange, &innerStop);
+ }
+ }
+}
+
+- (void)enumerateAttributesInRange:(NSRange)enumerationRange options:(ZAttributedStringEnumerationOptions)opts
+ usingBlock:(void (^)(NSDictionary*, NSRange, BOOL*))block {
+ // copy the attributes so we can mutate the string if necessary during enumeration
+ // also clip the array during copy to only the subarray of attributes that cover the requested range
+ NSArray *attrs;
+ if (NSEqualRanges(enumerationRange, NSMakeRange(0, 0))) {
+ attrs = [NSArray arrayWithArray:_attributes];
+ } else {
+ // in this binary search, last is the first run after the range
+ NSUInteger first = 0, last = [_attributes count];
+ while (last > first+1) {
+ NSUInteger pivot = (last + first) / 2;
+ ZAttributeRun *run = [_attributes objectAtIndex:pivot];
+ if (run.index < enumerationRange.location) {
+ first = pivot;
+ } else if (run.index >= NSMaxRange(enumerationRange)) {
+ last = pivot;
+ }
+ }
+ attrs = [_attributes subarrayWithRange:NSMakeRange(first, last-first)];
+ }
+ if (opts & ZAttributedStringEnumerationReverse) {
+ NSUInteger end = [_buffer length];
+ for (ZAttributeRun *run in [attrs reverseObjectEnumerator]) {
+ BOOL stop = NO;
+ NSUInteger start = run.index;
+ // clip to enumerationRange
+ start = MAX(start, enumerationRange.location);
+ end = MIN(end, NSMaxRange(enumerationRange));
+ block(run.attributes, NSMakeRange(start, end - start), &stop);
+ if (stop) break;
+ end = run.index;
+ }
+ } else {
+ NSUInteger start = 0;
+ ZAttributeRun *run = [attrs objectAtIndex:0];
+ NSInteger offset = 0;
+ NSInteger oldLength = [_buffer length];
+ for (NSUInteger i = 1;;i++) {
+ NSUInteger end;
+ if (i >= [attrs count]) {
+ end = oldLength;
+ } else {
+ end = [[attrs objectAtIndex:i] index];
+ }
+ BOOL stop = NO;
+ NSUInteger clippedStart = MAX(start, enumerationRange.location);
+ NSUInteger clippedEnd = MIN(end, NSMaxRange(enumerationRange));
+ block(run.attributes, NSMakeRange(clippedStart + offset, clippedEnd - start), &stop);
+ if (stop || i >= [attrs count]) break;
+ start = end;
+ NSUInteger newLength = [_buffer length];
+ offset += (newLength - oldLength);
+ oldLength = newLength;
+ }
+ }
+}
+#endif
+
+- (BOOL)isEqualToAttributedString:(ZAttributedString *)otherString {
+ return ([_buffer isEqualToString:otherString->_buffer] && [_attributes isEqualToArray:otherString->_attributes]);
+}
+
+- (BOOL)isEqual:(id)object {
+ return [object isKindOfClass:[ZAttributedString class]] && [self isEqualToAttributedString:(ZAttributedString *)object];
+}
+
+#pragma mark -
+
+- (NSUInteger)indexOfEffectiveAttributeRunForIndex:(NSUInteger)index {
+ NSUInteger first = 0, last = [_attributes count];
+ while (last > first + 1) {
+ NSUInteger pivot = (last + first) / 2;
+ ZAttributeRun *run = [_attributes objectAtIndex:pivot];
+ if (run.index > index) {
+ last = pivot;
+ } else if (run.index < index) {
+ first = pivot;
+ } else {
+ first = pivot;
+ break;
+ }
+ }
+ return first;
+}
+
+- (NSDictionary *)attributesAtIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange uniquingOnName:(NSString *)attributeName {
+ if (index >= [_buffer length]) {
+ @throw [NSException exceptionWithName:NSRangeException reason:@"index beyond range of attributed string" userInfo:nil];
+ }
+ NSUInteger runIndex = [self indexOfEffectiveAttributeRunForIndex:index];
+ ZAttributeRun *run = [_attributes objectAtIndex:runIndex];
+ if (aRange != NULL) {
+ aRange->location = run.index;
+ runIndex++;
+ if (runIndex < [_attributes count]) {
+ aRange->length = [[_attributes objectAtIndex:runIndex] index] - aRange->location;
+ } else {
+ aRange->length = [_buffer length] - aRange->location;
+ }
+ }
+ return run.attributes;
+}
+- (NSDictionary *)attributesAtIndex:(NSUInteger)index longestEffectiveRange:(NSRangePointer)aRange
+ inRange:(NSRange)rangeLimit uniquingOnName:(NSString *)attributeName {
+ if (index >= [_buffer length]) {
+ @throw [NSException exceptionWithName:NSRangeException reason:@"index beyond range of attributed string" userInfo:nil];
+ } else if (NSMaxRange(rangeLimit) > [_buffer length]) {
+ @throw [NSException exceptionWithName:NSRangeException reason:@"rangeLimit beyond range of attributed string" userInfo:nil];
+ }
+ NSUInteger runIndex = [self indexOfEffectiveAttributeRunForIndex:index];
+ ZAttributeRun *run = [_attributes objectAtIndex:runIndex];
+ if (aRange != NULL) {
+ if (attributeName != nil) {
+ id value = [run.attributes objectForKey:attributeName];
+ NSUInteger endRunIndex = runIndex+1;
+ runIndex--;
+ // search backwards
+ while (1) {
+ if (run.index <= rangeLimit.location) {
+ break;
+ }
+ ZAttributeRun *prevRun = [_attributes objectAtIndex:runIndex];
+ id prevValue = [prevRun.attributes objectForKey:attributeName];
+ if (prevValue == value || (value != nil && [prevValue isEqual:value])) {
+ runIndex--;
+ run = prevRun;
+ } else {
+ break;
+ }
+ }
+ // search forwards
+ ZAttributeRun *endRun = nil;
+ while (endRunIndex < [_attributes count]) {
+ ZAttributeRun *nextRun = [_attributes objectAtIndex:endRunIndex];
+ if (nextRun.index >= NSMaxRange(rangeLimit)) {
+ endRun = nextRun;
+ break;
+ }
+ id nextValue = [nextRun.attributes objectForKey:attributeName];
+ if (nextValue == value || (value != nil && [nextValue isEqual:value])) {
+ endRunIndex++;
+ } else {
+ endRun = nextRun;
+ break;
+ }
+ }
+ aRange->location = MAX(run.index, rangeLimit.location);
+ aRange->length = MIN((endRun ? endRun.index : [_buffer length]), NSMaxRange(rangeLimit)) - aRange->location;
+ } else {
+ // with no attribute name, we don't need to do any real searching,
+ // as we already guarantee each run has unique attributes.
+ // just make sure to clip the range to the rangeLimit
+ aRange->location = MAX(run.index, rangeLimit.location);
+ ZAttributeRun *endRun = (runIndex+1 < [_attributes count] ? [_attributes objectAtIndex:runIndex+1] : nil);
+ aRange->length = MIN((endRun ? endRun.index : [_buffer length]), NSMaxRange(rangeLimit)) - aRange->location;
+ }
+ }
+ return run.attributes;
+}
+
+- (void)dealloc {
+ [_buffer release];
+ [_attributes release];
+ [super dealloc];
+}
+@end
+
+@interface ZMutableAttributedString ()
+- (void)cleanupAttributesInRange:(NSRange)range;
+- (NSRange)rangeOfAttributeRunsForRange:(NSRange)range;
+- (void)offsetRunsInRange:(NSRange )range byOffset:(NSInteger)offset;
+@end
+
+@implementation ZMutableAttributedString
+- (id)copyWithZone:(NSZone *)zone {
+ return [(ZAttributedString *)[ZAttributedString allocWithZone:zone] initWithAttributedString:self];
+}
+
+- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range {
+ range = [self rangeOfAttributeRunsForRange:range];
+ for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) {
+ [run.attributes setObject:value forKey:name];
+ }
+ [self cleanupAttributesInRange:range];
+}
+
+- (void)addAttributes:(NSDictionary *)attributes range:(NSRange)range {
+ range = [self rangeOfAttributeRunsForRange:range];
+ for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) {
+ [run.attributes addEntriesFromDictionary:attributes];
+ }
+ [self cleanupAttributesInRange:range];
+}
+
+- (void)appendAttributedString:(ZAttributedString *)str {
+ [self insertAttributedString:str atIndex:[_buffer length]];
+}
+
+- (void)deleteCharactersInRange:(NSRange)range {
+ NSRange runRange = [self rangeOfAttributeRunsForRange:range];
+ [_buffer replaceCharactersInRange:range withString:@""];
+ [_attributes removeObjectsInRange:runRange];
+ for (NSUInteger i = runRange.location; i < [_attributes count]; i++) {
+ ZAttributeRun *run = [_attributes objectAtIndex:i];
+ ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:(run.index - range.length) attributes:run.attributes];
+ [_attributes replaceObjectAtIndex:i withObject:newRun];
+ [newRun release];
+ }
+ [self cleanupAttributesInRange:NSMakeRange(runRange.location, 0)];
+}
+
+- (void)insertAttributedString:(ZAttributedString *)str atIndex:(NSUInteger)idx {
+ [self replaceCharactersInRange:NSMakeRange(idx, 0) withAttributedString:str];
+}
+
+- (void)removeAttribute:(NSString *)name range:(NSRange)range {
+ range = [self rangeOfAttributeRunsForRange:range];
+ for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) {
+ [run.attributes removeObjectForKey:name];
+ }
+ [self cleanupAttributesInRange:range];
+}
+
+- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(ZAttributedString *)str {
+ NSRange replaceRange = [self rangeOfAttributeRunsForRange:range];
+ NSInteger offset = [str->_buffer length] - range.length;
+ [_buffer replaceCharactersInRange:range withString:str->_buffer];
+ [_attributes replaceObjectsInRange:replaceRange withObjectsFromArray:str->_attributes];
+ NSRange newRange = NSMakeRange(replaceRange.location, [str->_attributes count]);
+ [self offsetRunsInRange:newRange byOffset:range.location];
+ [self offsetRunsInRange:NSMakeRange(NSMaxRange(newRange), [_attributes count] - NSMaxRange(newRange)) byOffset:offset];
+ [self cleanupAttributesInRange:NSMakeRange(newRange.location, 0)];
+ [self cleanupAttributesInRange:NSMakeRange(NSMaxRange(newRange), 0)];
+}
+
+- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str {
+ [self replaceCharactersInRange:range withAttributedString:[[[ZAttributedString alloc] initWithString:str] autorelease]];
+}
+
+- (void)setAttributedString:(ZAttributedString *)str {
+ [_buffer release], _buffer = [str->_buffer mutableCopy];
+ [_attributes release], _attributes = [str->_attributes mutableCopy];
+}
+
+- (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range {
+ range = [self rangeOfAttributeRunsForRange:range];
+ for (ZAttributeRun *run in [_attributes subarrayWithRange:range]) {
+ [run.attributes setDictionary:attributes];
+ }
+ [self cleanupAttributesInRange:range];
+}
+
+#pragma mark -
+
+// splits the existing runs to provide one or more new runs for the given range
+- (NSRange)rangeOfAttributeRunsForRange:(NSRange)range {
+ NSParameterAssert(NSMaxRange(range) <= [_buffer length]);
+
+ // find (or create) the first run
+ NSUInteger first = 0;
+ ZAttributeRun *lastRun = nil;
+ for (;;first++) {
+ if (first >= [_attributes count]) {
+ // we didn't find a run
+ first = [_attributes count];
+ ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:range.location attributes:lastRun.attributes];
+ [_attributes addObject:newRun];
+ [newRun release];
+ break;
+ }
+ ZAttributeRun *run = [_attributes objectAtIndex:first];
+ if (run.index == range.location) {
+ break;
+ } else if (run.index > range.location) {
+ ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:range.location attributes:lastRun.attributes];
+ [_attributes insertObject:newRun atIndex:first];
+ [newRun release];
+ break;
+ }
+ lastRun = run;
+ }
+
+ if (((ZAttributeRun *)[_attributes lastObject]).index < NSMaxRange(range)) {
+ NSRange subrange = NSMakeRange(first, [_attributes count] - first);
+ if (NSMaxRange(range) < [_buffer length]) {
+ ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:NSMaxRange(range) attributes:[[_attributes lastObject] attributes]];
+ [_attributes addObject:newRun];
+ [newRun release];
+ }
+ return subrange;
+ } else {
+ // find the last run within and the first run after the range
+ NSUInteger lastIn = first, firstAfter = [_attributes count]-1;
+ while (firstAfter > lastIn + 1) {
+ NSUInteger idx = (firstAfter + lastIn) / 2;
+ ZAttributeRun *run = [_attributes objectAtIndex:idx];
+ if (run.index < range.location) {
+ lastIn = idx;
+ } else if (run.index > range.location) {
+ firstAfter = idx;
+ } else {
+ // this is definitively the first run after the range
+ firstAfter = idx;
+ break;
+ }
+ }
+ if ([[_attributes objectAtIndex:firstAfter] index] > NSMaxRange(range)) {
+ // the first after is too far after, insert another run!
+ ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:NSMaxRange(range)
+ attributes:[[_attributes objectAtIndex:firstAfter-1] attributes]];
+ [_attributes insertObject:newRun atIndex:firstAfter];
+ [newRun release];
+ }
+ return NSMakeRange(lastIn, firstAfter - lastIn);
+ }
+}
+
+- (void)cleanupAttributesInRange:(NSRange)range {
+ // expand the range to include one surrounding attribute on each side
+ if (range.location > 0) {
+ range.location -= 1;
+ range.length += 1;
+ }
+ if (NSMaxRange(range) < [_attributes count]) {
+ range.length += 1;
+ } else {
+ // make sure the range is capped to the attributes count
+ range.length = [_attributes count] - range.location;
+ }
+ if (range.length == 0) return;
+ ZAttributeRun *lastRun = [_attributes objectAtIndex:range.location];
+ for (NSUInteger i = range.location+1; i < NSMaxRange(range);) {
+ ZAttributeRun *run = [_attributes objectAtIndex:i];
+ if ([lastRun.attributes isEqualToDictionary:run.attributes]) {
+ [_attributes removeObjectAtIndex:i];
+ range.length -= 1;
+ } else {
+ lastRun = run;
+ i++;
+ }
+ }
+}
+
+- (void)offsetRunsInRange:(NSRange)range byOffset:(NSInteger)offset {
+ for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
+ ZAttributeRun *run = [_attributes objectAtIndex:i];
+ ZAttributeRun *newRun = [[ZAttributeRun alloc] initWithIndex:run.index + offset attributes:run.attributes];
+ [_attributes replaceObjectAtIndex:i withObject:newRun];
+ [newRun release];
+ }
+}
+@end
+
+@implementation ZAttributeRun
+@synthesize index = _index;
+@synthesize attributes = _attributes;
+
++ (id)attributeRunWithIndex:(NSUInteger)idx attributes:(NSDictionary *)attrs {
+ return [[[self alloc] initWithIndex:idx attributes:attrs] autorelease];
+}
+
+- (id)initWithIndex:(NSUInteger)idx attributes:(NSDictionary *)attrs {
+ NSParameterAssert(idx >= 0);
+ if ((self = [super init])) {
+ _index = idx;
+ if (attrs == nil) {
+ _attributes = [[NSMutableDictionary alloc] init];
+ } else {
+ _attributes = [attrs mutableCopy];
+ }
+ }
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder *)decoder {
+ if ((self = [super init])) {
+ _index = [[decoder decodeObjectForKey:@"index"] unsignedIntegerValue];
+ _attributes = [[decoder decodeObjectForKey:@"attributes"] mutableCopy];
+ }
+ return self;
+}
+
+- (id)init {
+ return [self initWithIndex:0 attributes:[NSDictionary dictionary]];
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ return [[ZAttributeRun allocWithZone:zone] initWithIndex:_index attributes:_attributes];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:_index] forKey:@"index"];
+ [aCoder encodeObject:_attributes forKey:@"attributes"];
+}
+
+- (NSString *)description {
+ NSMutableArray *components = [NSMutableArray arrayWithCapacity:[_attributes count]];
+ for (id key in _attributes) {
+ [components addObject:[NSString stringWithFormat:@"%@=%@", key, [_attributes objectForKey:key]]];
+ }
+ return [NSString stringWithFormat:@"<%@: %p index=%lu attributes={%@}>",
+ NSStringFromClass([self class]), self, (unsigned long)_index, [components componentsJoinedByString:@" "]];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[ZAttributeRun class]]) return NO;
+ ZAttributeRun *other = (ZAttributeRun *)object;
+ return _index == other->_index && [_attributes isEqualToDictionary:other->_attributes];
+}
+
+- (void)dealloc {
+ [_attributes release];
+ [super dealloc];
+}
+@end
+
+NSString * const ZFontAttributeName = @"ZFontAttributeName";
+NSString * const ZForegroundColorAttributeName = @"ZForegroundColorAttributeName";
+NSString * const ZBackgroundColorAttributeName = @"ZBackgroundColorAttributeName";
+NSString * const ZUnderlineStyleAttributeName = @"ZUnderlineStyleAttributeName";
--- /dev/null
+//
+// ZAttributedStringPrivate.h
+// FontLabel
+//
+// Created by Kevin Ballard on 9/23/09.
+// Copyright 2009 Zynga Game Networks. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "ZAttributedString.h"
+
+@interface ZAttributeRun : NSObject <NSCopying, NSCoding> {
+ NSUInteger _index;
+ NSMutableDictionary *_attributes;
+}
+@property (nonatomic, readonly) NSUInteger index;
+@property (nonatomic, readonly) NSMutableDictionary *attributes;
++ (id)attributeRunWithIndex:(NSUInteger)idx attributes:(NSDictionary *)attrs;
+- (id)initWithIndex:(NSUInteger)idx attributes:(NSDictionary *)attrs;
+@end
+
+@interface ZAttributedString (ZAttributedStringPrivate)
+@property (nonatomic, readonly) NSArray *attributes;
+@end
--- /dev/null
+//
+// ZFont.h
+// FontLabel
+//
+// Created by Kevin Ballard on 7/2/09.
+// Copyright © 2009 Zynga Game Networks
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@interface ZFont : NSObject {
+ CGFontRef _cgFont;
+ CGFloat _pointSize;
+ CGFloat _ratio;
+ NSString *_familyName;
+ NSString *_fontName;
+ NSString *_postScriptName;
+}
+@property (nonatomic, readonly) CGFontRef cgFont;
+@property (nonatomic, readonly) CGFloat pointSize;
+@property (nonatomic, readonly) CGFloat ascender;
+@property (nonatomic, readonly) CGFloat descender;
+@property (nonatomic, readonly) CGFloat leading;
+@property (nonatomic, readonly) CGFloat xHeight;
+@property (nonatomic, readonly) CGFloat capHeight;
+@property (nonatomic, readonly) NSString *familyName;
+@property (nonatomic, readonly) NSString *fontName;
+@property (nonatomic, readonly) NSString *postScriptName;
++ (ZFont *)fontWithCGFont:(CGFontRef)cgFont size:(CGFloat)fontSize;
++ (ZFont *)fontWithUIFont:(UIFont *)uiFont;
+- (id)initWithCGFont:(CGFontRef)cgFont size:(CGFloat)fontSize;
+- (ZFont *)fontWithSize:(CGFloat)fontSize;
+@end
--- /dev/null
+//
+// ZFont.m
+// FontLabel
+//
+// Created by Kevin Ballard on 7/2/09.
+// Copyright © 2009 Zynga Game Networks
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "ZFont.h"
+
+@interface ZFont ()
+@property (nonatomic, readonly) CGFloat ratio;
+- (NSString *)copyNameTableEntryForID:(UInt16)nameID;
+@end
+
+@implementation ZFont
+@synthesize cgFont=_cgFont, pointSize=_pointSize, ratio=_ratio;
+
++ (ZFont *)fontWithCGFont:(CGFontRef)cgFont size:(CGFloat)fontSize {
+ return [[[self alloc] initWithCGFont:cgFont size:fontSize] autorelease];
+}
+
++ (ZFont *)fontWithUIFont:(UIFont *)uiFont {
+ NSParameterAssert(uiFont != nil);
+ CGFontRef cgFont = CGFontCreateWithFontName((CFStringRef)uiFont.fontName);
+ ZFont *zFont = [[self alloc] initWithCGFont:cgFont size:uiFont.pointSize];
+ CGFontRelease(cgFont);
+ return [zFont autorelease];
+}
+
+- (id)initWithCGFont:(CGFontRef)cgFont size:(CGFloat)fontSize {
+ if ((self = [super init])) {
+ _cgFont = CGFontRetain(cgFont);
+ _pointSize = fontSize;
+ _ratio = fontSize/CGFontGetUnitsPerEm(cgFont);
+ }
+ return self;
+}
+
+- (id)init {
+ NSAssert(NO, @"-init is not valid for ZFont");
+ return nil;
+}
+
+- (CGFloat)ascender {
+ return ceilf(self.ratio * CGFontGetAscent(self.cgFont));
+}
+
+- (CGFloat)descender {
+ return floorf(self.ratio * CGFontGetDescent(self.cgFont));
+}
+
+- (CGFloat)leading {
+ return (self.ascender - self.descender);
+}
+
+- (CGFloat)capHeight {
+ return ceilf(self.ratio * CGFontGetCapHeight(self.cgFont));
+}
+
+- (CGFloat)xHeight {
+ return ceilf(self.ratio * CGFontGetXHeight(self.cgFont));
+}
+
+- (NSString *)familyName {
+ if (_familyName == nil) {
+ _familyName = [self copyNameTableEntryForID:1];
+ }
+ return _familyName;
+}
+
+- (NSString *)fontName {
+ if (_fontName == nil) {
+ _fontName = [self copyNameTableEntryForID:4];
+ }
+ return _fontName;
+}
+
+- (NSString *)postScriptName {
+ if (_postScriptName == nil) {
+ _postScriptName = [self copyNameTableEntryForID:6];
+ }
+ return _postScriptName;
+}
+
+- (ZFont *)fontWithSize:(CGFloat)fontSize {
+ if (fontSize == self.pointSize) return self;
+ NSParameterAssert(fontSize > 0.0);
+ return [[[ZFont alloc] initWithCGFont:self.cgFont size:fontSize] autorelease];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[ZFont class]]) return NO;
+ ZFont *font = (ZFont *)object;
+ return (font.cgFont == self.cgFont && font.pointSize == self.pointSize);
+}
+
+- (NSString *)copyNameTableEntryForID:(UInt16)aNameID {
+ CFDataRef nameTable = CGFontCopyTableForTag(self.cgFont, 'name');
+ NSAssert1(nameTable != NULL, @"CGFontCopyTableForTag returned NULL for 'name' tag in font %@",
+ [(id)CFCopyDescription(self.cgFont) autorelease]);
+ const UInt8 * const bytes = CFDataGetBytePtr(nameTable);
+ NSAssert1(OSReadBigInt16(bytes, 0) == 0, @"name table for font %@ has bad version number",
+ [(id)CFCopyDescription(self.cgFont) autorelease]);
+ const UInt16 count = OSReadBigInt16(bytes, 2);
+ const UInt16 stringOffset = OSReadBigInt16(bytes, 4);
+ const UInt8 * const nameRecords = &bytes[6];
+ UInt16 nameLength = 0;
+ UInt16 nameOffset = 0;
+ NSStringEncoding encoding = 0;
+ for (UInt16 idx = 0; idx < count; idx++) {
+ const uintptr_t recordOffset = 12 * idx;
+ const UInt16 nameID = OSReadBigInt16(nameRecords, recordOffset + 6);
+ if (nameID != aNameID) continue;
+ const UInt16 platformID = OSReadBigInt16(nameRecords, recordOffset + 0);
+ const UInt16 platformSpecificID = OSReadBigInt16(nameRecords, recordOffset + 2);
+ encoding = 0;
+ // for now, we only support a subset of encodings
+ switch (platformID) {
+ case 0: // Unicode
+ encoding = NSUTF16StringEncoding;
+ break;
+ case 1: // Macintosh
+ switch (platformSpecificID) {
+ case 0:
+ encoding = NSMacOSRomanStringEncoding;
+ break;
+ }
+ case 3: // Microsoft
+ switch (platformSpecificID) {
+ case 1:
+ encoding = NSUTF16StringEncoding;
+ break;
+ }
+ }
+ if (encoding == 0) continue;
+ nameLength = OSReadBigInt16(nameRecords, recordOffset + 8);
+ nameOffset = OSReadBigInt16(nameRecords, recordOffset + 10);
+ break;
+ }
+ NSString *result = nil;
+ if (nameOffset > 0) {
+ const UInt8 *nameBytes = &bytes[stringOffset + nameOffset];
+ result = [[NSString alloc] initWithBytes:nameBytes length:nameLength encoding:encoding];
+ }
+ CFRelease(nameTable);
+ return result;
+}
+
+- (void)dealloc {
+ CGFontRelease(_cgFont);
+ [_familyName release];
+ [_fontName release];
+ [_postScriptName release];
+ [super dealloc];
+}
+@end
--- /dev/null
+//
+// CDataScanner.h
+// TouchCode
+//
+// Created by Jonathan Wight on 04/16/08.
+// Copyright 2008 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import <Foundation/Foundation.h>
+
+// NSScanner
+
+@interface CDataScanner : NSObject {
+ NSData *data;
+
+ u_int8_t *start;
+ u_int8_t *end;
+ u_int8_t *current;
+ NSUInteger length;
+}
+
+@property (readwrite, nonatomic, retain) NSData *data;
+@property (readwrite, nonatomic, assign) NSUInteger scanLocation;
+@property (readonly, nonatomic, assign) NSUInteger bytesRemaining;
+@property (readonly, nonatomic, assign) BOOL isAtEnd;
+
+- (id)initWithData:(NSData *)inData;
+
+- (unichar)currentCharacter;
+- (unichar)scanCharacter;
+- (BOOL)scanCharacter:(unichar)inCharacter;
+
+- (BOOL)scanUTF8String:(const char *)inString intoString:(NSString **)outValue;
+- (BOOL)scanString:(NSString *)inString intoString:(NSString **)outValue;
+- (BOOL)scanCharactersFromSet:(NSCharacterSet *)inSet intoString:(NSString **)outValue; // inSet must only contain 7-bit ASCII characters
+
+- (BOOL)scanUpToString:(NSString *)string intoString:(NSString **)outValue;
+- (BOOL)scanUpToCharactersFromSet:(NSCharacterSet *)set intoString:(NSString **)outValue; // inSet must only contain 7-bit ASCII characters
+
+- (BOOL)scanNumber:(NSNumber **)outValue;
+- (BOOL)scanDecimalNumber:(NSDecimalNumber **)outValue;
+
+- (BOOL)scanDataOfLength:(NSUInteger)inLength intoData:(NSData **)outData;
+
+- (void)skipWhitespace;
+
+- (NSString *)remainingString;
+- (NSData *)remainingData;
+
+@end
--- /dev/null
+//
+// CDataScanner.m
+// TouchCode
+//
+// Created by Jonathan Wight on 04/16/08.
+// Copyright 2008 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import "CDataScanner.h"
+
+#import "CDataScanner_Extensions.h"
+
+@interface CDataScanner ()
+@end
+
+#pragma mark -
+
+inline static unichar CharacterAtPointer(void *start, void *end)
+ {
+ #pragma unused(end)
+
+ const u_int8_t theByte = *(u_int8_t *)start;
+ if (theByte & 0x80)
+ {
+ // TODO -- UNICODE!!!! (well in theory nothing todo here)
+ }
+ const unichar theCharacter = theByte;
+ return(theCharacter);
+ }
+
+ static NSCharacterSet *sDoubleCharacters = NULL;
+
+ @implementation CDataScanner
+
+- (id)init
+ {
+ if ((self = [super init]) != NULL)
+ {
+ }
+ return(self);
+ }
+
+- (id)initWithData:(NSData *)inData;
+ {
+ if ((self = [self init]) != NULL)
+ {
+ [self setData:inData];
+ }
+ return(self);
+ }
+
+ + (void)initialize
+ {
+ if (sDoubleCharacters == NULL)
+ {
+ sDoubleCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789eE-+."] retain];
+ }
+ }
+
+- (void)dealloc
+ {
+ [data release];
+ data = NULL;
+ //
+ [super dealloc];
+ }
+
+- (NSUInteger)scanLocation
+ {
+ return(current - start);
+ }
+
+- (NSUInteger)bytesRemaining
+ {
+ return(end - current);
+ }
+
+- (NSData *)data
+ {
+ return(data);
+ }
+
+- (void)setData:(NSData *)inData
+ {
+ if (data != inData)
+ {
+ [data release];
+ data = [inData retain];
+ }
+
+ if (data)
+ {
+ start = (u_int8_t *)data.bytes;
+ end = start + data.length;
+ current = start;
+ length = data.length;
+ }
+ else
+ {
+ start = NULL;
+ end = NULL;
+ current = NULL;
+ length = 0;
+ }
+ }
+
+- (void)setScanLocation:(NSUInteger)inScanLocation
+ {
+ current = start + inScanLocation;
+ }
+
+- (BOOL)isAtEnd
+ {
+ return(self.scanLocation >= length);
+ }
+
+- (unichar)currentCharacter
+ {
+ return(CharacterAtPointer(current, end));
+ }
+
+#pragma mark -
+
+- (unichar)scanCharacter
+ {
+ const unichar theCharacter = CharacterAtPointer(current++, end);
+ return(theCharacter);
+ }
+
+- (BOOL)scanCharacter:(unichar)inCharacter
+ {
+ unichar theCharacter = CharacterAtPointer(current, end);
+ if (theCharacter == inCharacter)
+ {
+ ++current;
+ return(YES);
+ }
+ else
+ return(NO);
+ }
+
+- (BOOL)scanUTF8String:(const char *)inString intoString:(NSString **)outValue
+ {
+ const size_t theLength = strlen(inString);
+ if ((size_t)(end - current) < theLength)
+ return(NO);
+ if (strncmp((char *)current, inString, theLength) == 0)
+ {
+ current += theLength;
+ if (outValue)
+ *outValue = [NSString stringWithUTF8String:inString];
+ return(YES);
+ }
+ return(NO);
+ }
+
+- (BOOL)scanString:(NSString *)inString intoString:(NSString **)outValue
+ {
+ if ((size_t)(end - current) < inString.length)
+ return(NO);
+ if (strncmp((char *)current, [inString UTF8String], inString.length) == 0)
+ {
+ current += inString.length;
+ if (outValue)
+ *outValue = inString;
+ return(YES);
+ }
+ return(NO);
+ }
+
+- (BOOL)scanCharactersFromSet:(NSCharacterSet *)inSet intoString:(NSString **)outValue
+ {
+ u_int8_t *P;
+ for (P = current; P < end && [inSet characterIsMember:*P] == YES; ++P)
+ ;
+
+ if (P == current)
+ {
+ return(NO);
+ }
+
+ if (outValue)
+ {
+ *outValue = [[[NSString alloc] initWithBytes:current length:P - current encoding:NSUTF8StringEncoding] autorelease];
+ }
+
+ current = P;
+
+ return(YES);
+ }
+
+- (BOOL)scanUpToString:(NSString *)inString intoString:(NSString **)outValue
+ {
+ const char *theToken = [inString UTF8String];
+ const char *theResult = strnstr((char *)current, theToken, end - current);
+ if (theResult == NULL)
+ {
+ return(NO);
+ }
+
+ if (outValue)
+ {
+ *outValue = [[[NSString alloc] initWithBytes:current length:theResult - (char *)current encoding:NSUTF8StringEncoding] autorelease];
+ }
+
+ current = (u_int8_t *)theResult;
+
+ return(YES);
+ }
+
+- (BOOL)scanUpToCharactersFromSet:(NSCharacterSet *)inSet intoString:(NSString **)outValue
+ {
+ u_int8_t *P;
+ for (P = current; P < end && [inSet characterIsMember:*P] == NO; ++P)
+ ;
+
+ if (P == current)
+ {
+ return(NO);
+ }
+
+ if (outValue)
+ {
+ *outValue = [[[NSString alloc] initWithBytes:current length:P - current encoding:NSUTF8StringEncoding] autorelease];
+ }
+
+ current = P;
+
+ return(YES);
+ }
+
+- (BOOL)scanNumber:(NSNumber **)outValue
+ {
+ NSString *theString = NULL;
+ if ([self scanCharactersFromSet:sDoubleCharacters intoString:&theString])
+ {
+ if ([theString rangeOfString:@"."].location != NSNotFound)
+ {
+ if (outValue)
+ {
+ *outValue = [NSDecimalNumber decimalNumberWithString:theString];
+ }
+ return(YES);
+ }
+ else if ([theString rangeOfString:@"-"].location != NSNotFound)
+ {
+ if (outValue != NULL)
+ {
+ *outValue = [NSNumber numberWithLongLong:[theString longLongValue]];
+ }
+ return(YES);
+ }
+ else
+ {
+ if (outValue != NULL)
+ {
+ *outValue = [NSNumber numberWithUnsignedLongLong:strtoull([theString UTF8String], NULL, 0)];
+ }
+ return(YES);
+ }
+
+ }
+ return(NO);
+ }
+
+- (BOOL)scanDecimalNumber:(NSDecimalNumber **)outValue;
+ {
+ NSString *theString = NULL;
+ if ([self scanCharactersFromSet:sDoubleCharacters intoString:&theString])
+ {
+ if (outValue)
+ {
+ *outValue = [NSDecimalNumber decimalNumberWithString:theString];
+ }
+ return(YES);
+ }
+ return(NO);
+ }
+
+- (BOOL)scanDataOfLength:(NSUInteger)inLength intoData:(NSData **)outData;
+ {
+ if (self.bytesRemaining < inLength)
+ {
+ return(NO);
+ }
+
+ if (outData)
+ {
+ *outData = [NSData dataWithBytes:current length:inLength];
+ }
+
+ current += inLength;
+ return(YES);
+ }
+
+
+- (void)skipWhitespace
+ {
+ u_int8_t *P;
+ for (P = current; P < end && (isspace(*P)); ++P)
+ ;
+
+ current = P;
+ }
+
+- (NSString *)remainingString
+ {
+ NSData *theRemainingData = [NSData dataWithBytes:current length:end - current];
+ NSString *theString = [[[NSString alloc] initWithData:theRemainingData encoding:NSUTF8StringEncoding] autorelease];
+ return(theString);
+ }
+
+- (NSData *)remainingData;
+ {
+ NSData *theRemainingData = [NSData dataWithBytes:current length:end - current];
+ return(theRemainingData);
+ }
+
+ @end
--- /dev/null
+//
+// CDataScanner_Extensions.h
+// TouchCode
+//
+// Created by Jonathan Wight on 12/08/2005.
+// Copyright 2005 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import "CDataScanner.h"
+
+@interface CDataScanner (CDataScanner_Extensions)
+
+- (BOOL)scanCStyleComment:(NSString **)outComment;
+- (BOOL)scanCPlusPlusStyleComment:(NSString **)outComment;
+
+- (NSUInteger)lineOfScanLocation;
+- (NSDictionary *)userInfoForScanLocation;
+
+@end
--- /dev/null
+//
+// CDataScanner_Extensions.m
+// TouchCode
+//
+// Created by Jonathan Wight on 12/08/2005.
+// Copyright 2005 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import "CDataScanner_Extensions.h"
+
+#define LF 0x000a // Line Feed
+#define FF 0x000c // Form Feed
+#define CR 0x000d // Carriage Return
+#define NEL 0x0085 // Next Line
+#define LS 0x2028 // Line Separator
+#define PS 0x2029 // Paragraph Separator
+
+@implementation CDataScanner (CDataScanner_Extensions)
+
+- (BOOL)scanCStyleComment:(NSString **)outComment
+{
+if ([self scanString:@"/*" intoString:NULL] == YES)
+ {
+ NSString *theComment = NULL;
+ if ([self scanUpToString:@"*/" intoString:&theComment] == NO)
+ [NSException raise:NSGenericException format:@"Started to scan a C style comment but it wasn't terminated."];
+
+ if ([theComment rangeOfString:@"/*"].location != NSNotFound)
+ [NSException raise:NSGenericException format:@"C style comments should not be nested."];
+
+ if ([self scanString:@"*/" intoString:NULL] == NO)
+ [NSException raise:NSGenericException format:@"C style comment did not end correctly."];
+
+ if (outComment != NULL)
+ *outComment = theComment;
+
+ return(YES);
+ }
+else
+ {
+ return(NO);
+ }
+}
+
+- (BOOL)scanCPlusPlusStyleComment:(NSString **)outComment
+ {
+ if ([self scanString:@"//" intoString:NULL] == YES)
+ {
+ unichar theCharacters[] = { LF, FF, CR, NEL, LS, PS, };
+ NSCharacterSet *theLineBreaksCharacterSet = [NSCharacterSet characterSetWithCharactersInString:[NSString stringWithCharacters:theCharacters length:sizeof(theCharacters) / sizeof(*theCharacters)]];
+
+ NSString *theComment = NULL;
+ [self scanUpToCharactersFromSet:theLineBreaksCharacterSet intoString:&theComment];
+ [self scanCharactersFromSet:theLineBreaksCharacterSet intoString:NULL];
+
+ if (outComment != NULL)
+ *outComment = theComment;
+
+ return(YES);
+ }
+ else
+ {
+ return(NO);
+ }
+ }
+
+- (NSUInteger)lineOfScanLocation
+ {
+ NSUInteger theLine = 0;
+ for (const u_int8_t *C = start; C < current; ++C)
+ {
+ // TODO: JIW What about MS-DOS line endings you bastard! (Also other unicode line endings)
+ if (*C == '\n' || *C == '\r')
+ {
+ ++theLine;
+ }
+ }
+ return(theLine);
+ }
+
+- (NSDictionary *)userInfoForScanLocation
+ {
+ NSUInteger theLine = 0;
+ const u_int8_t *theLineStart = start;
+ for (const u_int8_t *C = start; C < current; ++C)
+ {
+ if (*C == '\n' || *C == '\r')
+ {
+ theLineStart = C - 1;
+ ++theLine;
+ }
+ }
+
+ NSUInteger theCharacter = current - theLineStart;
+
+ NSRange theStartRange = NSIntersectionRange((NSRange){ .location = MAX((NSInteger)self.scanLocation - 20, 0), .length = 20 + (NSInteger)self.scanLocation - 20 }, (NSRange){ .location = 0, .length = self.data.length });
+ NSRange theEndRange = NSIntersectionRange((NSRange){ .location = self.scanLocation, .length = 20 }, (NSRange){ .location = 0, .length = self.data.length });
+
+
+ NSString *theSnippet = [NSString stringWithFormat:@"%@!HERE>!%@",
+ [[[NSString alloc] initWithData:[self.data subdataWithRange:theStartRange] encoding:NSUTF8StringEncoding] autorelease],
+ [[[NSString alloc] initWithData:[self.data subdataWithRange:theEndRange] encoding:NSUTF8StringEncoding] autorelease]
+ ];
+
+ NSDictionary *theUserInfo = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithUnsignedInteger:theLine], @"line",
+ [NSNumber numberWithUnsignedInteger:theCharacter], @"character",
+ [NSNumber numberWithUnsignedInteger:self.scanLocation], @"location",
+ theSnippet, @"snippet",
+ NULL];
+ return(theUserInfo);
+ }
+
+@end
--- /dev/null
+//
+// NSDictionary_JSONExtensions.h
+// TouchCode
+//
+// Created by Jonathan Wight on 04/17/08.
+// Copyright 2008 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface NSDictionary (NSDictionary_JSONExtensions)
+
++ (id)dictionaryWithJSONData:(NSData *)inData error:(NSError **)outError;
++ (id)dictionaryWithJSONString:(NSString *)inJSON error:(NSError **)outError;
+
+@end
--- /dev/null
+//
+// NSDictionary_JSONExtensions.m
+// TouchCode
+//
+// Created by Jonathan Wight on 04/17/08.
+// Copyright 2008 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import "NSDictionary_JSONExtensions.h"
+
+#import "CJSONDeserializer.h"
+
+@implementation NSDictionary (NSDictionary_JSONExtensions)
+
++ (id)dictionaryWithJSONData:(NSData *)inData error:(NSError **)outError
+ {
+ return([[CJSONDeserializer deserializer] deserialize:inData error:outError]);
+ }
+
++ (id)dictionaryWithJSONString:(NSString *)inJSON error:(NSError **)outError;
+ {
+ NSData *theData = [inJSON dataUsingEncoding:NSUTF8StringEncoding];
+ return([self dictionaryWithJSONData:theData error:outError]);
+ }
+
+@end
--- /dev/null
+//
+// CJSONDeserializer.h
+// TouchCode
+//
+// Created by Jonathan Wight on 12/15/2005.
+// Copyright 2005 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import <Foundation/Foundation.h>
+
+#import "CJSONScanner.h"
+
+extern NSString *const kJSONDeserializerErrorDomain /* = @"CJSONDeserializerErrorDomain" */;
+
+enum {
+ kJSONDeserializationOptions_MutableContainers = kJSONScannerOptions_MutableContainers,
+ kJSONDeserializationOptions_MutableLeaves = kJSONScannerOptions_MutableLeaves,
+};
+typedef NSUInteger EJSONDeserializationOptions;
+
+@class CJSONScanner;
+
+@interface CJSONDeserializer : NSObject {
+ CJSONScanner *scanner;
+ EJSONDeserializationOptions options;
+}
+
+@property (readwrite, nonatomic, retain) CJSONScanner *scanner;
+/// Object to return instead when a null encountered in the JSON. Defaults to NSNull. Setting to null causes the scanner to skip null values.
+@property (readwrite, nonatomic, retain) id nullObject;
+/// JSON must be encoded in Unicode (UTF-8, UTF-16 or UTF-32). Use this if you expect to get the JSON in another encoding.
+@property (readwrite, nonatomic, assign) NSStringEncoding allowedEncoding;
+@property (readwrite, nonatomic, assign) EJSONDeserializationOptions options;
+
++ (id)deserializer;
+
+- (id)deserialize:(NSData *)inData error:(NSError **)outError;
+
+- (id)deserializeAsDictionary:(NSData *)inData error:(NSError **)outError;
+- (id)deserializeAsArray:(NSData *)inData error:(NSError **)outError;
+
+@end
--- /dev/null
+//
+// CJSONDeserializer.m
+// TouchCode
+//
+// Created by Jonathan Wight on 12/15/2005.
+// Copyright 2005 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import "CJSONDeserializer.h"
+
+#import "CJSONScanner.h"
+#import "CDataScanner.h"
+
+NSString *const kJSONDeserializerErrorDomain = @"CJSONDeserializerErrorDomain";
+
+@interface CJSONDeserializer ()
+@end
+
+@implementation CJSONDeserializer
+
+@synthesize scanner;
+@synthesize options;
+
++ (id)deserializer
+ {
+ return([[[self alloc] init] autorelease]);
+ }
+
+- (id)init
+ {
+ if ((self = [super init]) != NULL)
+ {
+ }
+ return(self);
+ }
+
+- (void)dealloc
+ {
+ [scanner release];
+ scanner = NULL;
+ //
+ [super dealloc];
+ }
+
+#pragma mark -
+
+- (CJSONScanner *)scanner
+ {
+ if (scanner == NULL)
+ {
+ scanner = [[CJSONScanner alloc] init];
+ }
+ return(scanner);
+ }
+
+- (id)nullObject
+ {
+ return(self.scanner.nullObject);
+ }
+
+- (void)setNullObject:(id)inNullObject
+ {
+ self.scanner.nullObject = inNullObject;
+ }
+
+#pragma mark -
+
+- (NSStringEncoding)allowedEncoding
+ {
+ return(self.scanner.allowedEncoding);
+ }
+
+- (void)setAllowedEncoding:(NSStringEncoding)inAllowedEncoding
+ {
+ self.scanner.allowedEncoding = inAllowedEncoding;
+ }
+
+#pragma mark -
+
+- (id)deserialize:(NSData *)inData error:(NSError **)outError
+ {
+ if (inData == NULL || [inData length] == 0)
+ {
+ if (outError)
+ *outError = [NSError errorWithDomain:kJSONDeserializerErrorDomain code:kJSONScannerErrorCode_NothingToScan userInfo:NULL];
+
+ return(NULL);
+ }
+ if ([self.scanner setData:inData error:outError] == NO)
+ {
+ return(NULL);
+ }
+ id theObject = NULL;
+ if ([self.scanner scanJSONObject:&theObject error:outError] == YES)
+ return(theObject);
+ else
+ return(NULL);
+ }
+
+- (id)deserializeAsDictionary:(NSData *)inData error:(NSError **)outError
+ {
+ if (inData == NULL || [inData length] == 0)
+ {
+ if (outError)
+ *outError = [NSError errorWithDomain:kJSONDeserializerErrorDomain code:kJSONScannerErrorCode_NothingToScan userInfo:NULL];
+
+ return(NULL);
+ }
+ if ([self.scanner setData:inData error:outError] == NO)
+ {
+ return(NULL);
+ }
+ NSDictionary *theDictionary = NULL;
+ if ([self.scanner scanJSONDictionary:&theDictionary error:outError] == YES)
+ return(theDictionary);
+ else
+ return(NULL);
+ }
+
+- (id)deserializeAsArray:(NSData *)inData error:(NSError **)outError
+ {
+ if (inData == NULL || [inData length] == 0)
+ {
+ if (outError)
+ *outError = [NSError errorWithDomain:kJSONDeserializerErrorDomain code:kJSONScannerErrorCode_NothingToScan userInfo:NULL];
+
+ return(NULL);
+ }
+ if ([self.scanner setData:inData error:outError] == NO)
+ {
+ return(NULL);
+ }
+ NSArray *theArray = NULL;
+ if ([self.scanner scanJSONArray:&theArray error:outError] == YES)
+ return(theArray);
+ else
+ return(NULL);
+ }
+
+@end
--- /dev/null
+//
+// CJSONScanner.h
+// TouchCode
+//
+// Created by Jonathan Wight on 12/07/2005.
+// Copyright 2005 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import "CDataScanner.h"
+
+enum {
+ kJSONScannerOptions_MutableContainers = 0x1,
+ kJSONScannerOptions_MutableLeaves = 0x2,
+};
+typedef NSUInteger EJSONScannerOptions;
+
+/// CDataScanner subclass that understands JSON syntax natively. You should generally use CJSONDeserializer instead of this class. (TODO - this could have been a category?)
+@interface CJSONScanner : CDataScanner {
+ BOOL strictEscapeCodes;
+ id nullObject;
+ NSStringEncoding allowedEncoding;
+ EJSONScannerOptions options;
+}
+
+@property (readwrite, nonatomic, assign) BOOL strictEscapeCodes;
+@property (readwrite, nonatomic, retain) id nullObject;
+@property (readwrite, nonatomic, assign) NSStringEncoding allowedEncoding;
+@property (readwrite, nonatomic, assign) EJSONScannerOptions options;
+
+- (BOOL)setData:(NSData *)inData error:(NSError **)outError;
+
+- (BOOL)scanJSONObject:(id *)outObject error:(NSError **)outError;
+- (BOOL)scanJSONDictionary:(NSDictionary **)outDictionary error:(NSError **)outError;
+- (BOOL)scanJSONArray:(NSArray **)outArray error:(NSError **)outError;
+- (BOOL)scanJSONStringConstant:(NSString **)outStringConstant error:(NSError **)outError;
+- (BOOL)scanJSONNumberConstant:(NSNumber **)outNumberConstant error:(NSError **)outError;
+
+@end
+
+extern NSString *const kJSONScannerErrorDomain /* = @"kJSONScannerErrorDomain" */;
+
+typedef enum {
+
+ // Fundamental scanning errors
+ kJSONScannerErrorCode_NothingToScan = -11,
+ kJSONScannerErrorCode_CouldNotDecodeData = -12,
+ kJSONScannerErrorCode_CouldNotSerializeData = -13,
+ kJSONScannerErrorCode_CouldNotSerializeObject = -14,
+ kJSONScannerErrorCode_CouldNotScanObject = -15,
+
+ // Dictionary scanning
+ kJSONScannerErrorCode_DictionaryStartCharacterMissing = -101,
+ kJSONScannerErrorCode_DictionaryKeyScanFailed = -102,
+ kJSONScannerErrorCode_DictionaryKeyNotTerminated = -103,
+ kJSONScannerErrorCode_DictionaryValueScanFailed = -104,
+ kJSONScannerErrorCode_DictionaryKeyValuePairNoDelimiter = -105,
+ kJSONScannerErrorCode_DictionaryNotTerminated = -106,
+
+ // Array scanning
+ kJSONScannerErrorCode_ArrayStartCharacterMissing = -201,
+ kJSONScannerErrorCode_ArrayValueScanFailed = -202,
+ kJSONScannerErrorCode_ArrayValueIsNull = -203,
+ kJSONScannerErrorCode_ArrayNotTerminated = -204,
+
+ // String scanning
+ kJSONScannerErrorCode_StringNotStartedWithBackslash = -301,
+ kJSONScannerErrorCode_StringUnicodeNotDecoded = -302,
+ kJSONScannerErrorCode_StringUnknownEscapeCode = -303,
+ kJSONScannerErrorCode_StringNotTerminated = -304,
+
+ // Number scanning
+ kJSONScannerErrorCode_NumberNotScannable = -401
+
+} EJSONScannerErrorCode;
--- /dev/null
+//
+// CJSONScanner.m
+// TouchCode
+//
+// Created by Jonathan Wight on 12/07/2005.
+// Copyright 2005 toxicsoftware.com. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#import "CJSONScanner.h"
+
+#import "CDataScanner_Extensions.h"
+
+#if !defined(TREAT_COMMENTS_AS_WHITESPACE)
+#define TREAT_COMMENTS_AS_WHITESPACE 0
+#endif // !defined(TREAT_COMMENTS_AS_WHITESPACE)
+
+NSString *const kJSONScannerErrorDomain = @"kJSONScannerErrorDomain";
+
+inline static int HexToInt(char inCharacter)
+ {
+ int theValues[] = { 0x0 /* 48 '0' */, 0x1 /* 49 '1' */, 0x2 /* 50 '2' */, 0x3 /* 51 '3' */, 0x4 /* 52 '4' */, 0x5 /* 53 '5' */, 0x6 /* 54 '6' */, 0x7 /* 55 '7' */, 0x8 /* 56 '8' */, 0x9 /* 57 '9' */, -1 /* 58 ':' */, -1 /* 59 ';' */, -1 /* 60 '<' */, -1 /* 61 '=' */, -1 /* 62 '>' */, -1 /* 63 '?' */, -1 /* 64 '@' */, 0xa /* 65 'A' */, 0xb /* 66 'B' */, 0xc /* 67 'C' */, 0xd /* 68 'D' */, 0xe /* 69 'E' */, 0xf /* 70 'F' */, -1 /* 71 'G' */, -1 /* 72 'H' */, -1 /* 73 'I' */, -1 /* 74 'J' */, -1 /* 75 'K' */, -1 /* 76 'L' */, -1 /* 77 'M' */, -1 /* 78 'N' */, -1 /* 79 'O' */, -1 /* 80 'P' */, -1 /* 81 'Q' */, -1 /* 82 'R' */, -1 /* 83 'S' */, -1 /* 84 'T' */, -1 /* 85 'U' */, -1 /* 86 'V' */, -1 /* 87 'W' */, -1 /* 88 'X' */, -1 /* 89 'Y' */, -1 /* 90 'Z' */, -1 /* 91 '[' */, -1 /* 92 '\' */, -1 /* 93 ']' */, -1 /* 94 '^' */, -1 /* 95 '_' */, -1 /* 96 '`' */, 0xa /* 97 'a' */, 0xb /* 98 'b' */, 0xc /* 99 'c' */, 0xd /* 100 'd' */, 0xe /* 101 'e' */, 0xf /* 102 'f' */, };
+ if (inCharacter >= '0' && inCharacter <= 'f')
+ return(theValues[inCharacter - '0']);
+ else
+ return(-1);
+ }
+
+@interface CJSONScanner ()
+- (BOOL)scanNotQuoteCharactersIntoString:(NSString **)outValue;
+@end
+
+#pragma mark -
+
+@implementation CJSONScanner
+
+@synthesize strictEscapeCodes;
+@synthesize nullObject;
+@synthesize allowedEncoding;
+@synthesize options;
+
+- (id)init
+ {
+ if ((self = [super init]) != NULL)
+ {
+ strictEscapeCodes = NO;
+ nullObject = [[NSNull null] retain];
+ }
+ return(self);
+ }
+
+- (void)dealloc
+ {
+ [nullObject release];
+ nullObject = NULL;
+ //
+ [super dealloc];
+ }
+
+#pragma mark -
+
+- (BOOL)setData:(NSData *)inData error:(NSError **)outError;
+ {
+ NSData *theData = inData;
+ if (theData && theData.length >= 4)
+ {
+ // This code is lame, but it works. Because the first character of any JSON string will always be a (ascii) control character we can work out the Unicode encoding by the bit pattern. See section 3 of http://www.ietf.org/rfc/rfc4627.txt
+ const char *theChars = theData.bytes;
+ NSStringEncoding theEncoding = NSUTF8StringEncoding;
+ if (theChars[0] != 0 && theChars[1] == 0)
+ {
+ if (theChars[2] != 0 && theChars[3] == 0)
+ theEncoding = NSUTF16LittleEndianStringEncoding;
+ else if (theChars[2] == 0 && theChars[3] == 0)
+ theEncoding = NSUTF32LittleEndianStringEncoding;
+ }
+ else if (theChars[0] == 0 && theChars[2] == 0 && theChars[3] != 0)
+ {
+ if (theChars[1] == 0)
+ theEncoding = NSUTF32BigEndianStringEncoding;
+ else if (theChars[1] != 0)
+ theEncoding = NSUTF16BigEndianStringEncoding;
+ }
+
+ NSString *theString = [[NSString alloc] initWithData:theData encoding:theEncoding];
+ if (theString == NULL && self.allowedEncoding != 0)
+ {
+ theString = [[NSString alloc] initWithData:theData encoding:self.allowedEncoding];
+ }
+ theData = [theString dataUsingEncoding:NSUTF8StringEncoding];
+ [theString release];
+ }
+
+ if (theData)
+ {
+ [super setData:theData];
+ return(YES);
+ }
+ else
+ {
+ if (outError)
+ {
+ NSMutableDictionary *theUserInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ @"Could not scan data. Data wasn't encoded properly?", NSLocalizedDescriptionKey,
+ NULL];
+ [theUserInfo addEntriesFromDictionary:self.userInfoForScanLocation];
+ *outError = [NSError errorWithDomain:kJSONScannerErrorDomain code:kJSONScannerErrorCode_CouldNotDecodeData userInfo:theUserInfo];
+ }
+ return(NO);
+ }
+ }
+
+- (void)setData:(NSData *)inData
+ {
+ [self setData:inData error:NULL];
+ }
+
+#pragma mark -
+
+- (BOOL)scanJSONObject:(id *)outObject error:(NSError **)outError
+ {
+ BOOL theResult = YES;
+
+ [self skipWhitespace];
+
+ id theObject = NULL;
+
+ const unichar C = [self currentCharacter];
+ switch (C)
+ {
+ case 't':
+ if ([self scanUTF8String:"true" intoString:NULL])
+ {
+ theObject = [NSNumber numberWithBool:YES];
+ }
+ break;
+ case 'f':
+ if ([self scanUTF8String:"false" intoString:NULL])
+ {
+ theObject = [NSNumber numberWithBool:NO];
+ }
+ break;
+ case 'n':
+ if ([self scanUTF8String:"null" intoString:NULL])
+ {
+ theObject = self.nullObject;
+ }
+ break;
+ case '\"':
+ case '\'':
+ theResult = [self scanJSONStringConstant:&theObject error:outError];
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '-':
+ theResult = [self scanJSONNumberConstant:&theObject error:outError];
+ break;
+ case '{':
+ theResult = [self scanJSONDictionary:&theObject error:outError];
+ break;
+ case '[':
+ theResult = [self scanJSONArray:&theObject error:outError];
+ break;
+ default:
+ theResult = NO;
+ if (outError)
+ {
+ NSMutableDictionary *theUserInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ @"Could not scan object. Character not a valid JSON character.", NSLocalizedDescriptionKey,
+ NULL];
+ [theUserInfo addEntriesFromDictionary:self.userInfoForScanLocation];
+ *outError = [NSError errorWithDomain:kJSONScannerErrorDomain code:kJSONScannerErrorCode_CouldNotScanObject userInfo:theUserInfo];
+ }
+ break;
+ }
+
+ if (outObject != NULL)
+ *outObject = theObject;
+
+ return(theResult);
+ }
+
+- (BOOL)scanJSONDictionary:(NSDictionary **)outDictionary error:(NSError **)outError
+ {
+ NSUInteger theScanLocation = [self scanLocation];
+
+ [self skipWhitespace];
+
+ if ([self scanCharacter:'{'] == NO)
+ {
+ if (outError)
+ {
+ NSMutableDictionary *theUserInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ @"Could not scan dictionary. Dictionary that does not start with '{' character.", NSLocalizedDescriptionKey,
+ NULL];
+ [theUserInfo addEntriesFromDictionary:self.userInfoForScanLocation];
+ *outError = [NSError errorWithDomain:kJSONScannerErrorDomain code:kJSONScannerErrorCode_DictionaryStartCharacterMissing userInfo:theUserInfo];
+ }
+ return(NO);
+ }
+
+ NSMutableDictionary *theDictionary = [[NSMutableDictionary alloc] init];
+
+ while ([self currentCharacter] != '}')
+ {
+ [self skipWhitespace];
+
+ if ([self currentCharacter] == '}')
+ break;
+
+ NSString *theKey = NULL;
+ if ([self scanJSONStringConstant:&theKey error:outError] == NO)
+ {
+ [self setScanLocation:theScanLocation];
+ if (outError)
+ {
+ NSMutableDictionary *theUserInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ @"Could not scan dictionary. Failed to scan a key.", NSLocalizedDescriptionKey,
+ NULL];
+ [theUserInfo addEntriesFromDictionary:self.userInfoForScanLocation];
+ *outError = [NSError errorWithDomain:kJSONScannerErrorDomain code:kJSONScannerErrorCode_DictionaryKeyScanFailed userInfo:theUserInfo];
+ }
+ [theDictionary release];
+ return(NO);
+ }
+
+ [self skipWhitespace];
+
+ if ([self scanCharacter:':'] == NO)
+ {
+ [self setScanLocation:theScanLocation];
+ if (outError)
+ {
+ NSMutableDictionary *theUserInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ @"Could not scan dictionary. Key was not terminated with a ':' character.", NSLocalizedDescriptionKey,
+ NULL];
+ [theUserInfo addEntriesFromDictionary:self.userInfoForScanLocation];
+ *outError = [NSError errorWithDomain:kJSONScannerErrorDomain code:kJSONScannerErrorCode_DictionaryKeyNotTerminated userInfo:theUserInfo];
+ }
+ [theDictionary release];
+ return(NO);
+ }
+
+ id theValue = NULL;
+ if ([self scanJSONObject:&theValue error:outError] == NO)
+ {
+ [self setScanLocation:theScanLocation];
+ if (outError)
+ {
+ NSMutableDictionary *theUserInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ @"Could not scan dictionary. Failed to scan a value.", NSLocalizedDescriptionKey,
+ NULL];
+
+ [theUserInfo addEntriesFromDictionary:self.userInfoForScanLocation];
+ *outError = [NSError errorWithDomain:kJSONScannerEr