Voice Recognition / Speech Recognition and speech for the iPhone

I’m happy to announce that OpenEars is launched! OpenEars is an open-source iOS library for implementing continuous, multithreaded, and low-overhead speech recognition and text-to-speech, using CMU Pocketsphinx and CMU Flite, for the iPhone and iPad. It uses nice Cocoa-standard cross-project-referenced static libraries and it offers all the easy-to-use Objective-C methods you’ll need to bring round-trip speech functions to your app. The launch version is 0.9.0 and I hope that there will be some cool projects using it.

4 Comments

Ducklings weren’t on the balcony

They were in Kladow:
Swimming Duckling

Standing Duckling

Mom Duck and Two Ducklings

Mom Duck and Two Ducklings On Cobblestones

Close Shot of Mom Duck and Two Ducklings

0 Comments

Who is on the balcony, flying swan edition

A swan flies over, head downwards.

Swan Flies Over Berlin Balcony 1

Swan Flies Over Berlin Balcony 2

0 Comments

Decibel metering from an iPhone audio unit

[Take me right to the code please!] There are three levels of abstraction for audio on the iPhone, with the AVAudioPlayer as the easiest to use (great for 75% of cases) but with the least fine control and highest latency, then Audio Queue Services as the middle step, with less latency and a callback where you can do a lot of useful stuff, and then at the lowest level there are two types of Audio Unit: Remote I/O (or remoteio) and the Voice Processing Audio Unit subtype.

Audio Units are a little bit less forgiving than Audio Queues in their setup, and they have a few more low-level settings that need to be accounted for, and they are a little less documented than Audio Queues, and their sample code on the developer site (Auriotouch) is a little less transparent than the one for Audio Queues (SpeakHere), all of which has led to the impression that they are ultra-difficult and should be approached with caution, although in practice the code is almost identical to that for Audio Queues if you aren’t mixing sounds and have a single callback. At least, I’ve spent as much time being mystified by a non-working Audio Queue as by a non-working Audio Unit on the iPhone. But it needs to be said that the main reason that Audio Units aren’t much harder than Audio Queues at this point is because a lot of independent developers have put a lot of time into experimenting, asking questions, and publishing their results. A year ago they were much more of a black box.

The decision process on which technology to use is something like:

Q. Are any of the following statements true: “I need the lowest possible latency”, “I need to work with network streams of audio or audio in memory”, “I need to do signal processing”, “I need to record voice with maximum clarity”
A. If yes, Audio Units are probably best. If no,
Q. With the answers to the previous questions being no, do you still need to be able to work with sound at the buffer level?
A: If yes, use Audio Queues or Audio Units, whichever is more comfortable. If no, use AVAudioPlayer/AVAudioRecorder.

In my experience there is just one big downside to the Audio Unit on the iPhone, which is that there is no metering property for it. There is a metering property which you can see in the audio unit properties header and in the iPhone Audio Units docs, but it isn’t really turned on, and you can lose a lot of time discovering this via experimentation. So, if you’ve chosen to use Audio Units and your implementation is working, you have a render callback function. This is where you can meter your samples. I have only written/tested this for 16-bit mono PCM data so if you are using something else, adaptations might be required.

To meter the samples in the render callback requires six steps.

Step 1: get an array of your samples that you can loop through. Each sample contains the amplitude.
Step 2: for each sample, get its amplitude’s absolute value.
Step 3: for each sample’s absolute value, run it through a simple low-pass filter,
Step 4: for each sample’s filtered absolute value, convert it into decibels,
Step 5: for each sample’s filtered absolute value in decibels, add an offset value that normalizes the clipping point of the device to zero.
Step 6: keep the highest value you find.

That end value will be more or less the same thing you’d get when using the metering property for an Audio Queue or AVAudioRecorder/AVAudioPlayer.
Now, the actual code:

	static OSStatus	AudioUnitRenderCallback (void *inRefCon,
											 AudioUnitRenderActionFlags *ioActionFlags,
											 const AudioTimeStamp *inTimeStamp,
											 UInt32 inBusNumber,
											 UInt32 inNumberFrames,
											 AudioBufferList *ioData) {

		OSStatus err = AudioUnitRender(audioUnitWrapper->audioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, ioData);

		if(err != 0) NSLog(@"AudioUnitRender status is %d", err);
		// These values should be in a more conventional location for a bunch of preprocessor defines in your real code
#define DBOFFSET -74.0
		// DBOFFSET is An offset that will be used to normalize the decibels to a maximum of zero.
		// This is an estimate, you can do your own or construct an experiment to find the right value
#define LOWPASSFILTERTIMESLICE .001
		// LOWPASSFILTERTIMESLICE is part of the low pass filter and should be a small positive value

		SInt16* samples = (SInt16*)(ioData->mBuffers[0].mData); // Step 1: get an array of your samples that you can loop through. Each sample contains the amplitude.

		Float32 decibels = DBOFFSET; // When we have no signal we'll leave this on the lowest setting
		Float32 currentFilteredValueOfSampleAmplitude, previousFilteredValueOfSampleAmplitude; // We'll need these in the low-pass filter
		Float32 peakValue = DBOFFSET; // We'll end up storing the peak value here

		for (int i=0; i < inNumberFrames; i++) { 

			Float32 absoluteValueOfSampleAmplitude = abs(samples[i]); //Step 2: for each sample, get its amplitude's absolute value.

			// Step 3: for each sample's absolute value, run it through a simple low-pass filter
			// Begin low-pass filter
			currentFilteredValueOfSampleAmplitude = LOWPASSFILTERTIMESLICE * absoluteValueOfSampleAmplitude + (1.0 - LOWPASSFILTERTIMESLICE) * previousFilteredValueOfSampleAmplitude;
			previousFilteredValueOfSampleAmplitude = currentFilteredValueOfSampleAmplitude;
			Float32 amplitudeToConvertToDB = currentFilteredValueOfSampleAmplitude;
			// End low-pass filter

			Float32 sampleDB = 20.0*log10(amplitudeToConvertToDB) + DBOFFSET;
			// Step 4: for each sample's filtered absolute value, convert it into decibels
			// Step 5: for each sample's filtered absolute value in decibels, add an offset value that normalizes the clipping point of the device to zero.

			if((sampleDB == sampleDB) && (sampleDB <= DBL_MAX && sampleDB >= -DBL_MAX)) { // if it's a rational number and isn't infinite

				if(sampleDB > peakValue) peakValue = sampleDB; // Step 6: keep the highest value you find.
				decibels = peakValue; // final value
			}
		}

		NSLog(@"decibel level is %f", decibels);

		for (UInt32 i=0; i < ioData->mNumberBuffers; i++) { // This is only if you need to silence the output of the audio unit
			memset(ioData->mBuffers[i].mData, 0, ioData->mBuffers[i].mDataByteSize); // Delete if you need audio output as well as input
		}

		return err;
	}
}

That should give you a metered decibel value which is analogous to the output of the metering property for an Audio Queue. If anyone has any corrections to this or comments I hope they’ll get in touch.

My starting point for learning this technique was a helpful response email from iWillApps’ Will to a silly question I had which got me on track analyzing the actual samples, and this page where the math behind displaying DB is broken down pretty thoroughly, and this post on Stack Overflow which explains that the process needs to be done on a rectified signal and has the low-pass filter code example.

46 Comments

What happens when an iPhone app runs out of disk space for a recorded sound file?

I recently asked on the Apple iPhone Developer Forums (developer program subscribers only) what would happen if my app hit a disk full error while recording sound using an Audio Queue. There wasn’t an answer so I set up a test for it and learned a little bit (nothing absolutely definitive though), which I’m sharing here so it will be helpful to others. I suspect it will apply to some extent to file types other than audio as well.

1. How I set up my device in order to make sure there was as little space available to my app as possible:

I loaded a full library of music on my device, then took photos until I got a general “Warning: you are running out of disk space. Please delete some photos or videos.” message, then continued taking photos until I got a follow-up Camera-specific message “Low Disk Space: There is not enough disk space to continue taking photos. Make room by deleting existing videos or photos.”. Note: this wasn’t actually easy to achieve, meaning that the iPhone has a lot of buffer space in reserve at the time that it starts reporting low disk space. This is actually good for all concerned since the potentially error-causing situation is called out well in advance; it’s only bad if you want to get the iPhone full enough to really use up every bit of space and discover it can take more than a half-hour of snapping photos of your living room.

2. The test:

I downloaded the SpeakHere example (again) and added a background thread that would update the overall file size of the recorded file to the fileDescription UILabel every second (on the main thread) so I could see if the file was still growing. I wanted this feedback because if I need to repeat the test, it’s really good to have even a vague idea at what point in time significant things will start happening, especially since I have no idea how long of a test it’s going to end up being.

If you are interested in seeing the same feedback as I did in the SpeakHere sample app, download it from the developer site and add a pointer to an NSThread called *recordThread and the matching @property to the interface of SpeakHereController, and in the implementation synthesize/deallocate recordThread and replace the entire method -(IBAction)record:(id)sender with this block of code (which I have put no effort into making efficient or correct, it’s just supposed to update my label without tying up my time troubleshooting):

- (void) updateLabelWithFileSize:(NSString *)fileSize {
	fileDescription.text = fileSize;
}

- (void) startRecordSizeLoop {

	NSFileManager *fileManager = [[NSFileManager alloc] init];
	NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent: @"recordedFile.caf"];

	NSError *error = nil;

	NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:filePath error:&error];

	NSString *fileSize = nil;

	if(error) {
		NSLog(@"Error getting file size of audio file: %@", [error description]);
	} else {
		NSString *fileSizeAttribute = [fileAttributes objectForKey:@"NSFileSize"];

		if([fileSizeAttribute intValue] < 1) {
			fileSize = @"error";
		} else if([fileSizeAttribute floatValue] / 1024.0 < 1) {
			fileSize = @"1k";
		} else {
			fileSize = [NSString stringWithFormat:@"%dk",[fileSizeAttribute intValue] / 1024];
		}
	}

	[self performSelectorOnMainThread:@selector(updateLabelWithFileSize:) withObject:fileSize waitUntilDone:NO];

	[fileManager release];

	[NSThread sleepForTimeInterval:1];
	if(self.recordThread.isCancelled == FALSE)[self startRecordSizeLoop];
}

- (void) startRecordThreadAutoreleasePool {

	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	[self startRecordSizeLoop];
	[pool drain];
}

- (void)startRecordThread {

    NSThread *recThread = [[NSThread alloc] initWithTarget:self selector:@selector(startRecordThreadAutoreleasePool) object:nil];
    self.recordThread = recThread;
    [recThread release];
    [self.recordThread start];
}

- (void)waitForRecordThreadToFinish {
    while (recordThread && ![recordThread isFinished]) {
		[NSThread sleepForTimeInterval:0.1];
    }
}

- (void)stopRecordThread {
    [self.recordThread cancel];
	[self waitForRecordThreadToFinish];
    self.recordThread = nil;
}

- (IBAction)record:(id)sender
{
	if (recorder->IsRunning())
	{
		[self stopRecord];
		[self stopRecordThread];
	}
	else
	{
		btn_play.enabled = NO;	

		btn_record.title = @"Stop";

		recorder->StartRecord(CFSTR("recordedFile.caf"));

		[self setFileDescriptionForFormat:recorder->DataFormat() withName:@"Recorded File"];

		[self startRecordThread];
		[lvlMeter_in setAq: recorder->Queue()];
	}
}

The other thing I did to save time was to go into the AQRecorder implementation and increase the sample rate for the queue so the space would get consumed faster by adding the line “mRecordFormat.mSampleRate = 96000.0;” to the end of this format definition in void AQRecorder::SetupAudioFormat(UInt32 inFormatID):

	mRecordFormat.mBitsPerChannel = 16;
	mRecordFormat.mBytesPerPacket = mRecordFormat.mBytesPerFrame = (mRecordFormat.mBitsPerChannel / 8) * mRecordFormat.mChannelsPerFrame;
	mRecordFormat.mFramesPerPacket = 1;

Back to my test: I tapped “record” and waited. around 56000k, a warning popped up: “Warning: you are running out of disk space. Please delete some photos or videos.” I dismissed it and let the app continue recording. Receiving this warning wasn’t accompanied by any other changes or restrictions. This seems to be an OS-level thing and not specific to Core Audio, since it’s the identical warning I received in Camera.

At 121764k the number stopped increasing. I recorded myself saying “is this still recording?” so I could listen to the recording and see if it was or wasn’t still recording at the time that the numbers stopped increasing (I expected it wasn’t but wanted to verify). I checked the debugger and none of the error logging that is in the AQ’s input buffer handler had gone off when the file size had stopped increasing. I played back the recording and the last words I recorded were not played back, i.e. when the numbers stopped going up, the recording did apparently stop as expected.

Next step was to see what was going on inside of void AQRecorder::MyInputBufferHandler() when the file stopped increasing so I changed it in the following ways (adding an int callsToMyInputBufferHandler to the beginning of the class implementation):

void AQRecorder::MyInputBufferHandler(	void *								inUserData,
										AudioQueueRef						inAQ,
										AudioQueueBufferRef					inBuffer,
										const AudioTimeStamp *				inStartTime,
										UInt32								inNumPackets,
										const AudioStreamPacketDescription*	inPacketDesc)
{

	AQRecorder *aqr = (AQRecorder *)inUserData;

	callsToMyInputBufferHandler++;
	printf("MyInputBufferHandler() was called on iteration %d\n", callsToMyInputBufferHandler);
	printf("inNumPackets = %d\n", inNumPackets);
	printf("inBuffer->mAudioDataByteSize = %d\n", inBuffer->mAudioDataByteSize);
	printf("inBuffer->mAudioData = %d\n", inBuffer->mAudioData);
	printf("aqr->mIsRunning = %d\n", aqr->mIsRunning);

	try {
		if (inNumPackets > 0) {
			// write packets to file
			XThrowIfError(AudioFileWritePackets(aqr->mRecordFile, FALSE, inBuffer->mAudioDataByteSize,
											 inPacketDesc, aqr->mRecordPacket, &inNumPackets, inBuffer->mAudioData),
					   "AudioFileWritePackets failed");
			aqr->mRecordPacket += inNumPackets;
		} else {
			NSLog(@"There are no packets available, do not write them to file");
		}

		// if we're not stopping, re-enqueue the buffer so that it gets filled again
		if (aqr->IsRunning()) {
			XThrowIfError(AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL), "AudioQueueEnqueueBuffer failed");
		} else {
			NSLog(@"The audioqueue isn't running, don't re-enqueue the buffer");
		}
	} catch (CAXException e) {
		char buf[256];
		fprintf(stderr, "Error: %s (%s)\n", e.mOperation, e.FormatError(buf));
	}
	NSLog(@"MyInputBufferHandler end iteration %d", callsToMyInputBufferHandler);
}

Some of the stuff being logged here is uninformative, I just went with a kitchen sink approach since I didn’t want to do this too many times. The first thing I noticed as a result of doing this was that what I thought were permanent stops in the recorder were not – they were really long lags between rounds of MyInputBufferHandler() being called. Lags of more than minutes in some cases, about 4 or 5 times before a definitive stop at 125104k. Finally, after a lag of more than 4 minutes, I got an error returned with a complete iteration:

Error: AudioFileWritePackets failed (-40)

The command line utility macerror tells me that -40 means “Mac OS error -40 (posErr): tried to position to before start of file (r/w)” which seems somewhat related to the situation at hand (I guess it’s something that happens with an EOF under low-disk-space conditions).

This recurred through a couple more very laggy iterations and then nothing more happened. This is all the testing I have time for on this issue but I feel like I learned a bit here and can make some decisions about addressing the case in my app.

Conclusions: users will get a warning from the OS that there isn’t enough room some minutes in advance of running out of room. This might be “good enough” since if they decide to proceed they won’t be surprised by weird behavior. When there is really no more room, AudioFileWritePackets will start returning an exception -40, but as far as I can see here it might be returning some time after the exception occured, which means there could be an unexpected loss of something the user was expecting to be recorded, as there was when I spoke to SpeakHere after the buffer calls started to lag.

The -40 is something to go on, but I don’t know that disk full is the only circumstance under which is could occur, which means that using it as a specific indicator of being out of space is probably a bad idea. But, I suspect that any time it appears it probably denotes a significant error, so one strategy that could be used would be to deliver an error alert to the user when -40 is returned and mention that an error occurred and it could be due to lack of disk space. Since this was time-consuming to set up and observe, I hope it helps others avoid having to replicate the same steps.

0 Comments

Want to put a UI widget into a UITableViewCell? Subclass them for approximately 50% less angst as measured by scientists.

Sometimes you want to put a UI widget like a UISwitch or a UIButton into a UITableViewCell – depending on the task it might be better practice to let users of your software do some stuff without descending through a table hierarchy. Sometimes it’s ideal to do a task while focused on a single part of a whole, and sometimes it’s ideal to do it in the context of the whole. That might be orthogonal to whether the table works better grouped or ungrouped.

The SDK lets you make a widget such as a UISwitch or UIButton become the accessoryView of a table cell, which is helpful because it manages the widget recycling, but also forces a particular placement of the widget in the cell which might not be a good placement (put a big-hitzone UISwitch right next to the section index in a non-grouped table and you will be able to hear people on other continents cursing your name). If you are using something two-dimensional like a NSDictionary or an NSArray of NSArrays as your table data source, it also leaves you out in the cold if you need to tell some other part of the UITableViewController which section the widget-hosting cell was in, even if you use the tag to store the row.

There are hacks that involve looking up the parent view hierarchy to find out which cell contains the thing that sent the widget event, but these are always a bit dependent on Apple not changing that view hierarchy, or you can future-proof them a little by looping through multiple levels of the view hierarchy checking for your widget, or…let’s stop before the workaround’s workaround has a workaround. No one is free of the sin of hacks, but this is a case in which your soul can remain fluffy and clean.

Subclass the widget and add a row and section instance variable to your class. Use naming that is unlikely to ever end up unintentionally overriding instance variables that might be added to the superclass in the future (i.e. I wouldn’t call the row “row”, since doing things with rows is a pretty common activity in the iPhone SDK and you never know; the UIButton of the future might have a “row” instance variable that is in some tiny yet important way different from the one you’re making below, and then it’s Katy bar the door. Or, anyway, you’ll have to fix something). Note that we aren’t overriding any methods in the implementation – the whole point of this is that it should act identically to a UIButton with the exception of having a couple of added instance variables for our use.

UITableCellButton.h:

#import 

@interface UITableCellButton : UIButton {

	int cellButtonSection;
	int cellButtonRow;
}

@property (nonatomic, assign) int cellButtonSection;
@property (nonatomic, assign) int cellButtonRow;

@end

UITableCellButton.m:

#import "UITableCellButton.h"

@implementation UITableCellButton

@synthesize cellButtonSection;
@synthesize cellButtonRow;

@end

When you use it in your table cell, just add it to the cell.contentView so you can put it wherever you feel like:

UITableCellButton *myButton = [[UITableCellButton alloc] initWithFrame:CGRectMake(250,10,30,30)];
[myButton setImage:[UIImage imageNamed:@"NormalStateButton.png"] forState:UIControlStateNormal];
[myButton setImage:[UIImage imageNamed:@"HighlightedStateButton.png"] forState:UIControlStateHighlighted];
[myButton setImage:[UIImage imageNamed:@"SelectedStateButton.png"] forState:UIControlStateSelected];
[myButton addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
myButton.tag = kTagForWidgetRecycling;
myButton.cellButtonSection = indexPath.section;
myButton.cellButtonRow = indexPath.row;
[cell.contentView addSubview:myButton];

[myButton release];

Somewhere before that, deal with the view recycling like this:

UIView *viewToRemove = nil;
viewToRemove = [cell.contentView viewWithTag:kTagForWidgetRecycling];
if (viewToRemove){
	NSLog(@"I found a %@ and I'm removing it", [viewToRemove class]);
	[viewToRemove removeFromSuperview];
}

I made my own custom button here because the button types (UIButtonTypeRoundedRect et al) are different subclasses and I wasn’t sure how to subclass them. Maybe someone will post below and let me know.

That kTagForWidgetRecycling thing is a constant that I defined at the start of the UITableViewController implementation – it’s just an integer that all cell subviews needing recycling will be tagged with, given a name for clarity so I know why I tagged them when I’m looking at this again some morning in a pre-coffee haze:

#define kTagForWidgetRecycling 1

N.b.: if you use tags to flag cell view recycling, don’t use them for anything else on any views that are hosted by a cell unless you have reason to believe that there will be no possible overlap between your official “recycle this view” tag number and some other number that a view gets tagged with for some other purpose, or it’s Katy again.

That buttonPressed: selector is going to this method:

- (void) buttonPressed:(id)sender {

    UITableCellButton *button = (UITableCellButton *)sender;

    NSLog(@"I can get any properties back out of my button here, for instance the button that was pressed is in section %d", button.cellButtonSection);

}

A way to make this approach even simpler is to ask yourself what it is you’re doing with the section and row info. Are you using it to get a string out of the right place? Is it for doing a read or write from Core Data? Instead of sending the coordinates of the index path, why not just send the data you need? So the subclass interface might instead look like this instead:

#import 

@interface UITableCellButton : UIButton {

    NSManagedObjectID *managedObjectID;
}

@property (nonatomic, retain) NSManagedObjectID *managedObjectID;

@end

and the implementation like this:

#import "UITableCellButton.h"

@implementation UITableCellButton

@synthesize managedObjectID;

- (void)dealloc //now that we are using a pointer, we need to deallocate it
{
    [managedObjectID release];
    [super dealloc];
}

@end

and in the table cell, you’ll just forget about the whole section/row thing and tell the subclassed button which Core Data NSManagedObjectID it’s attached to:

UITableCellButton *myButton = [[UITableCellButton alloc] initWithFrame:CGRectMake(250,10,30,30)];
[myButton setImage:[UIImage imageNamed:@"NormalStateButton.png"] forState:UIControlStateNormal];
[myButton setImage:[UIImage imageNamed:@"HighlightedStateButton.png"] forState:UIControlStateHighlighted];
[myButton setImage:[UIImage imageNamed:@"SelectedStateButton.png"] forState:UIControlStateSelected];
[myButton addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
myButton.tag = kTagForWidgetRecycling;
myButton.managedObjectID = theIDofThisCellsManagedObject;
[cell.contentView addSubview:myButton];

[myButton release];


This is just an example; Core Data is very well integrated with UITableViewController and it’s unlikely that you’d need to go too far off the beaten path to do useful things with it. But if you have more modest data structures that don’t justify the full-on database treatment and yet still have some dimensionality, this can keep things nice and simple.

2 Comments

Who is on the balcony, springtime addendum

Mallards are on the balcony, sitting heavily in flowerpots that E. just planted yesterday, in the rain, having a chat.
Two Big Mallards Hanging Out In Our Flowerpots And Chatting

0 Comments

Life without NIBs

This is not an attack on NIBs, or XIBs, or any other IBs. They are great for laying out a UI. There is just one circumstance under which I really don’t like them, and that is when I’m looking at some Apple sample code for something pretty gnarly like Keychain Services or AudioQueues, and the first thing I always do when I’m looking at a new example that is a little confusing is to get rid of as much presentation stuff as possible so there is less to dig through. But, in these samples there are often important functions and methods distributed throughout the app delegate, a view controller, and one or more models, and what I like to do when possible is to consolidate anything important into the root view controller so I can look at it next to the model and that’s that. This is much simpler for me if I can get rid of all the NIBs including MainWindow.xib. So, if you want to get rid of MainWindow.xib, here is how to do it:

1. Open the info.plist of the app and remove the value for the key “Main nib file base name”.
2. In main.m, change the line

int retVal = UIApplicationMain(argc, argv, nil, nil);

to

int retVal = UIApplicationMain(argc, argv, nil, @"YourAppDelegateClassName");

3. In

- (void)applicationDidFinishLaunching:(UIApplication *)application

of your app delegate class, create the main window for the app:

UIWindow *myWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window = myWindow;
[myWindow release];

4. Then set up the root view controller and the navigation controller and call it a day:

myViewController = [[MyViewController alloc] init];
navigationController = [[UINavigationController alloc] initWithRootViewController:myViewController];

This should all go before the lines:

 [window addSubview:[navigationController view]];
 [window makeKeyAndVisible];

I got this info from this post on iPhone Dev SDK originally, thanks mcurtis.

0 Comments

Nothing in Recent Projects or Recent Files? Here’s why, and a fix.

I only recently upgraded to Snow Leopard; I’d been working on a long project when Snow Leopard hit and I didn’t want to make changes to my system until it was over. Well, ever since I finally upgraded, I’ve had nothing in my “Recent Projects” submenu in Xcode. But, I misattributed it to using the Xcode 3.2 betas, which I started using at the same time. It’s been annoying but in a peripheral way that never quite made it to the front burner.

Today I finally got it in gear and searched for an answer, and learned that the reason that I have no Recent Projects is that in Snow Leopard, the settings in System Settings->Appearance->Recent Documents are propagated to every Apple application rather than only applying to the global Recent Items menu under the blue apple.

Just a teensy bit of griping about this: I only use Recent Items for launching apps, so allowing a big list of recent documents in there as well just makes that menu take longer to appear. I always felt confident setting Recent Documents to zero because I knew I would never want to invoke them from that menu, and it wasn’t necessary because…they’d be in the Recent Documents submenu of whatever app I was using. Anyway.

I can see the UI issue that this addresses though. The options are either to a) allow the number of recent items to be set in one place (this is what they are currently doing) or b) let it be settable in a sporadic/ad hoc way from app to app, or c) require it to be settable in all of their apps. They all have their upsides and downsides as approaches and the heart of my semi-objection to the change is only that I have a learned behavior based on the old way going back 8 years now, so it could be a positive change but it also makes a demand that I adapt out of an old adaptation and I don’t feel like it.

Which is why I’m happy that you can override this on a per-app basis. If you are also picky/stubborn and want to keep your System Settings set to zero recent documents and Xcode set to more, paste this in terminal with the relevant number replacing 20:

defaults write com.apple.Xcode NSRecentDocumentsLimit 20

Ta-da.

0 Comments

Can we call it yet?

I hope it isn’t tempting fate to call this remarkable winter finished and share some images from it.

A grapevine runs along our walls; its little grapes were encased in ice before the last starlings in town could harvest them.

Grapevine With Grapes Frozen In Icicle

Hooded crow in snow.

A Hooded Crow With Snow On Its Beak

Swans walk along a frozen waterway, searching for a break in the ice that isn’t there.

Swans Walk Along A Frozen Waterway

But for a dog it’s just more room to frolic.

A Dog Runs On A Frozen Waterway

0 Comments