// // FastCamController.m // FastCam // // Created by Simon Urbanek on 11/30/07. // Copyright 2007 Simon Urbanek. All rights reserved. // #import "FastCamController.h" #import "Camera.h" #include // version suffix #ifdef FAST #ifdef DEBUG #define versionKind @"FAST (debug)" #else #define versionKind @"FAST" #endif #else #ifdef DEBUG #define versionKind @"debug" #else #define versionKind @"std" #endif #endif // disable NSLog for non-debug versions #ifndef DEBUG #undef NSLog #define NSLog(...) #endif #pragma mark ---- BubbleRecording ---- #define bbs 2048 typedef struct bb_s { unsigned int sz[bbs]; unsigned int fr[bbs]; unsigned int es; struct bb_s *next; } bb_t; @interface BubbleRecording : NSObject { bb_t *head, *tail; struct timeval start, stop; unsigned long sum; unsigned int frames, count, lastSize; double pxFactor; } @end @implementation BubbleRecording - (id) init { self = [super init]; if (self) { head = tail = (bb_t*) malloc(sizeof(bb_t)); head->next = 0; head->es = 0; frames = 0; count = 0; pxFactor = 1.0; } return self; } - (void) setFactor: (double) aFactor { pxFactor = aFactor; } - (void) start { frames = 0; count = 0; sum = 0; lastSize = 0; head->es = 0; tail = head; bb_t *a = head->next; while (a) { /* remove all subsequent blocks */ bb_t *a_next = a->next; free(a); a = a_next; } head->next = 0; gettimeofday(&start, 0); } - (void) stopWithFramesCount: (unsigned int) frameCount { gettimeofday(&stop, 0); frames = frameCount; } - (double) elapsedTime { struct timeval now; gettimeofday(&now, 0); return ((double) now.tv_sec) - ((double)start.tv_sec) + (((double) now.tv_usec) - ((double) start.tv_usec))/1000000.0; } - (unsigned int) count { return count; } - (NSString*) infoString { double avg = ((double)sum)/((double)count); return [NSString stringWithFormat:@"%d bubbles, avg. %.1fpx/%.1fµm (last %dpx/%.1fµm)", count, avg, avg*pxFactor, lastSize, ((double)lastSize)*pxFactor]; } - (BOOL) writeToFile: (NSString*) fileName atomically: (BOOL) atom { NSMutableData *data = [[NSMutableData alloc] initWithCapacity: 1024]; char *c = ctime(&start.tv_sec); NSString *s = [NSString stringWithFormat:@"Date:\t%sLength:\t%.3f s\nFrames:\t%d\nFactor:\t%.3f\nBubbles:\t%d\n\nSize distribution:\n", c, ((double) stop.tv_sec) - ((double)start.tv_sec) + (((double) stop.tv_usec) - ((double) start.tv_usec))/1000000.0, frames, pxFactor, count]; c = (char*) [s UTF8String]; [data appendBytes:c length:strlen(c)]; NSMutableData *log = [[NSMutableData alloc] initWithCapacity:1024]; bb_t * a = head; unsigned int smin = a->sz[0], smax = smin; while (a) { int i = 0; while (i < a->es) { char cc[32]; snprintf(cc,32,"%d\t%d\t%.1f\n", a->fr[i], a->sz[i], ((double)a->sz[i])*pxFactor); if (a->sz[i] > smax) smax = a->sz[i]; if (a->sz[i] < smin) smin = a->sz[i]; [log appendBytes:cc length:strlen(cc)]; i++; } a = a->next; } unsigned int *hist = (unsigned int*) calloc(smax-smin+1, sizeof(int)); a = head; while (a) { int i = 0; while (i < a->es) { hist[a->sz[i] - smin]++; i++; } a = a->next; } int i = smin; while (i <= smax) { char cc[32]; snprintf(cc, 32, "%d\t%.1f\t%d\n", i, ((double)i)*pxFactor, hist[i - smin]); [data appendBytes:cc length:strlen(cc)]; i++; } free(hist); [data appendBytes:"\nIndividual bubbles:\n" length:strlen("\nIndividual bubbles:\n")]; [data appendData:log]; [log release]; BOOL res = [data writeToFile:fileName atomically:atom]; [data release]; return res; } - (void) addBubbleAtFrame: (unsigned int) frame size: (unsigned int) size { if (tail->es >= bbs) { tail->next = (bb_t*) malloc(sizeof(bb_t)); tail = tail->next; tail->es = 0; tail->next = 0; } int i = tail->es; sum += (lastSize = tail->sz[i] = size); count++; tail->fr[i] = frame; tail->es++; } @end #pragma mark ---- FastCamController ---- @implementation FastCamController - (id) init { self = [super init]; if (self) { frameNo = 0; aoiY1 = 0; aoiY2 = 491; shutter = 405; brightness = 800; gain = 192; status = @"Looking for cameras ..."; timeInfo = @""; cam = nil; isRecording = ready = saveFrames = NO; frames = [[NSMutableArray alloc] init]; anaFrameNo = 0; anaMax = 1.0; anaMin = 255.0; blindOff = 656 / 4; minBubSize = 10; lastBubPos = 999; pxFactor = 1.0; analyze = NO; lastFrame = [[NSMutableData alloc] initWithCapacity: 656 * 491]; currentFrame = [[NSMutableData alloc] initWithCapacity: 656 * 491]; refFrame = [[NSMutableData alloc] initWithCapacity: 656 * 491]; // make sure the temporary bytes for analysis have at least one additional line which is set, acting as a stopper tempBytes = (unsigned char*) malloc(656 * (491 + 2)); memset(tempBytes + (656 * 491), 1, 656); bubbles = [[BubbleRecording alloc] init]; memset(calibFrame, 0, sizeof(calibFrame)); memset(calibFrameSize, 0, sizeof(calibFrameSize)); // load user settings NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; int i = [prefs integerForKey: @"camera.gain"]; if (i > 0) gain = i; i = [prefs integerForKey:@"camera.shutter"]; if (i > 0) shutter = i; i = [prefs integerForKey:@"camera.brightness"]; if (i > 0) brightness = i; i = [prefs integerForKey:@"minBubSize"]; if (i != 0) minBubSize = i; i = [prefs integerForKey:@"blind.offset"]; if (i != 0) blindOff = (i > 0)?i:0; lastFilePath = [prefs stringForKey:@"last.path"]; double d = [prefs floatForKey:@"px.factor"]; if (d > 0) pxFactor = d; [bubbles setFactor:pxFactor]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminateNotification:) name:NSApplicationWillTerminateNotification object:nil]; } return self; } #pragma mark synthesized properties @synthesize shutter, gain, brightness, isRecording, saveFrames, status, analyze, timeInfo, bpsInfo, aoiY1, aoiY2, frameNo, maxFrame, minBubSize, blindOff, calibrated, calibrating, pxFactor; - (void) dealloc { if (cam) { [cam release]; cam=nil; } [frames release]; NSLog(@"FastCamController.dealloc"); [super dealloc]; } - (void) applicationWillTerminateNotification: (NSNotification*) notification { NSLog(@" - application will terminate, saving preferences"); NSUserDefaults *pref = [NSUserDefaults standardUserDefaults]; [pref setInteger:gain forKey:@"camera.gain"]; [pref setInteger:brightness forKey:@"camera.brightness"]; [pref setInteger:shutter forKey:@"camera.shutter"]; [pref setInteger:blindOff forKey:@"blind.offset"]; [pref setFloat:pxFactor forKey:@"px.factor"]; if (lastFilePath) [pref setObject:lastFilePath forKey:@"last.path"]; [pref synchronize]; } - (void) findCamera { cam = [[DC1394 sharedInstance] cameraAt:0]; if (cam) { self.status = @"Camera found."; iSight = NO; [cam retain]; if ([cam setFormat7: 0] && [cam setAOI: (AOI) { 0, 491 - (aoiY1>aoiY2)?aoiY1:aoiY2, 656, ((aoiY1 0.0) { if (bpsBubbles > cc) bpsBubbles = 0; // there was a reset that we didn't notice double bps = (double) (cc - bpsBubbles); bps /= (t - bpsTime); self.bpsInfo = [NSString stringWithFormat:@"%.1f bub/s", bps]; } bpsBubbles = cc; bpsTime = t; } } - (void) setShutter: (int) newValue { NSLog(@"setShutter: %d (is %d)", newValue, shutter); shutter = newValue; if (cam) [cam setShutter:shutter]; } - (void) setGain: (int) newValue { NSLog(@"setGain: %d (is %d)", newValue, gain); gain = newValue; if (cam) [cam setGain:((double)gain)/255.0]; } - (void) setBlindOff: (unsigned int) newValue { NSLog(@"setBlindOff: %d (is %d)", newValue, blindOff); blindOff = newValue; [view setBubbleArea:MakeBubArea(blindOff, 0, 656, 491)]; } - (void) setPxFactor: (double) newValue { NSLog(@"setPxFactor: %.3f (is %.3f)", newValue, pxFactor); pxFactor = newValue; if (bubbles) [bubbles setFactor:pxFactor]; } - (void) setAoiY1: (int) newValue { NSLog(@"setAoiY1: %d (is %d)", newValue, aoiY1); aoiY1 = newValue; if (cam) [cam setAOI: (AOI) { 0, 491 - ((aoiY1>aoiY2)?aoiY1:aoiY2), 656, ((aoiY1aoiY2)?aoiY1:aoiY2), 656, ((aoiY1aoiY2)?aoiY1:aoiY2))*656]; /* skip frames in display to make sure we're fast enough */ } else if (showThis) [view updateMono: (const char*)frame_data offset: (491 - ((aoiY1>aoiY2)?aoiY1:aoiY2))*656 length:frame_size]; [cam enqueue:frame]; fc++; } NSEvent *event; while ((event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES])) [NSApp sendEvent:event]; } [view setAllowDrag:NO]; self.status = @"Camera is ready"; self.maxFrame = maxFrame; // to update UI } } - (void) setFrameNo: (int) newValue { int i = [frames count]; if (newValue < i) { NSData *data = [frames objectAtIndex:newValue]; frameNo = newValue; #if 0 { int h = [data length] / 656; unsigned char *d = (unsigned char*) [data bytes], *e = d + [data length]; unsigned char dmin = 255, dmax = 0; unsigned int fsum = 0; while (d < e) { fsum+=*d; if (dmin > *d) dmin = *d; if (dmax < *d) dmax = *d; d++; } self.status = [NSString stringWithFormat:@"sum=%d, range=(%d,%d)", fsum, dmin, dmax]; } #endif [view updateMonoWithData: data]; } } - (IBAction)showReferenceFrame:(id)sender { if (refFrame && [refFrame length] > 0) { self.status=@"Reference frame"; if ([refFrame length] < 656*491) [view updateMonoWithData:refFrame atOffset: (491 - ((aoiY1>aoiY2)?aoiY1:aoiY2))*656]; else [view updateMonoWithData:refFrame atOffset:0]; } } - (IBAction)calibrate:(id)sender { self.status=@"Calibrating ..."; [self resetCalibration]; self.calibrating = YES; } - (IBAction)calibrateAndStartAnalysis:(id)sender { self.status=@"Calibrating for analysis ..."; [self resetAnalysis]; [self resetCalibration]; self.calibrating = YES; self.analyze = YES; [view setAllowDrag:NO]; } - (IBAction)startAnalysis:(id)sender { if (calibrated) [self resetAnalysis]; if (!analyze) self.status = @"Measuring bubbles..."; self.analyze = YES; [view setAllowDrag:NO]; } - (IBAction)stopAnalysis:(id)sender { if (analyze) { self.analyze = NO; // not sure if this will work since the end may be anynchronous [self finishAnalysis]; if ([status isEqualToString:@"Measuring bubbles..."]) self.status=@""; [view setAllowDrag:YES]; [self saveDocumentAs:self]; } } - (IBAction)stop:(id)sender { if (ready) [cam stop]; self.isRecording = NO; } - (IBAction)reset:(id)sender { [frames removeAllObjects]; if (!saveFrames) self.maxFrame = 0; } - (void)windowWillClose:(NSNotification *)notification { if (cam) { [cam stop]; [cam disableCapture]; [cam release]; cam=nil; } } #pragma mark ---- analysis ---- static int spot(const unsigned char *r, const unsigned char *c, const int i, const int calibDelta) { int d = ((int) r[i]) - ((int) c[i]); return d > calibDelta; } // FIXME: this is highly recursive - for very large bubbles we may run out of stack space ... static void bounds(const unsigned char *r, const unsigned char *c, unsigned char *t, const int i, const int x, const int w, const int calibDelta, int *ymin, int *ymax, int *xmin, int *xmax) { t[i] = 1; if (i > 0 && t[i - 1] == 0 && spot(r, c, i - 1, calibDelta)) { if (x - 1 < *xmin) *xmin = x - 1; bounds(r, c, t, i - 1, x - 1, w, calibDelta, ymin, ymax, xmin, xmax); } if (t[i + 1] == 0 && x < w && spot(r, c, i + 1, calibDelta)) { if (x + 1 > *xmax) *xmax = x + 1; bounds(r, c, t, i + 1, x + 1, w, calibDelta, ymin, ymax, xmin, xmax); } if (i > w && t[i - w] == 0 && spot(r, c, i - w, calibDelta)) { if (i - w < *ymin) *ymin = i - w; bounds(r, c, t, i - w, x, w, calibDelta, ymin, ymax, xmin, xmax); } if (t[i + w] == 0 && spot(r, c, i + w, calibDelta)) { if (i + w > *ymax) *ymax = i + w; bounds(r, c, t, i + w, x, w, calibDelta, ymin, ymax, xmin, xmax); } } - (void) signalError: (NSString*) errorText { self.status = errorText; } - (void) resetCalibration { calibSum = 0; anaFrameNo = 0; } - (void) calibrate: (unsigned char*) data width: (int) w height: (int) h { int len = w * h; unsigned char *c = data; calibWidth = (unsigned int) w; calibHeight = (unsigned int) h; if (anaFrameNo >= ANA_CALIBRATE) { NSLog(@"calibrate: skipping frame %d since out of ranage (%d)", anaFrameNo, ANA_CALIBRATE); anaFrameNo++; return; } if (calibFrameSize[anaFrameNo] < len) { if (calibFrame[anaFrameNo]) free(calibFrame[anaFrameNo]); calibFrame[anaFrameNo] = (unsigned char*) malloc(len + 4); } memcpy(calibFrame[anaFrameNo], c, len); #if 0 /* old calibration */ // calibration: assume that bubbles/particles are shadows, hence // the brightest calibration frame is the "clean" base // Note: for robustness it would be beneficial to use the pixelwise // maximum of two consecutive frames in case we have fast moving bubbles // all the time unsigned int dsum = 0; unsigned char dmin = 255, dmax = 0; for (;c < e; c++) { dsum += *c; if (*c > dmax) dmax = *c; if (*c < dmin) dmin = *c; } if (dsum > calibSum) { [refFrame setLength: len]; memcpy([refFrame mutableBytes], data, len); calibSum = dsum; calibDelta = (dmax - dmin)/4; // threshold - 25% } #endif anaFrameNo++; } /* fast median search in 9 elements */ #define PIX_SWAP(a,b) { register unsigned char temp=(a);(a)=(b);(b)=temp; } #define PIX_SORT(a,b) { if ((a)>(b)) PIX_SWAP((a),(b)); } #if (ANA_CALIBRATE < 9) #error "ANA_CALIBRATE must be ate least 9" #endif - (void) calibrationFinished { unsigned int len = calibWidth * calibHeight; NSLog(@" - collected calibation data, computing reference frame (9-median)"); [refFrame setLength: len]; unsigned char *rf = [refFrame mutableBytes]; unsigned int dsum = 0; unsigned char dmin = 255, dmax = 0; unsigned int i; for(i = 0; i < len; i++) { unsigned char p0=calibFrame[0][i], p1=calibFrame[1][i], p2=calibFrame[2][i], p3=calibFrame[3][i]; unsigned char p4=calibFrame[4][i], p5=calibFrame[5][i], p6=calibFrame[6][i], p7=calibFrame[7][i]; unsigned char p8=calibFrame[8][i]; PIX_SORT(p1, p2); PIX_SORT(p4, p5); PIX_SORT(p7, p8); PIX_SORT(p0, p1); PIX_SORT(p3, p4); PIX_SORT(p6, p7); PIX_SORT(p1, p2); PIX_SORT(p4, p5); PIX_SORT(p7, p8); PIX_SORT(p0, p3); PIX_SORT(p5, p8); PIX_SORT(p4, p7); PIX_SORT(p3, p6); PIX_SORT(p1, p4); PIX_SORT(p2, p5); PIX_SORT(p4, p7); PIX_SORT(p4, p2); PIX_SORT(p6, p4); PIX_SORT(p4, p2); rf[i] = p4; if (p4 > dmax) dmax = p4; if (p4 < dmin) dmin = p4; dsum += p4; } /* the calib values are based on the reference frame */ calibSum = dsum; calibDelta = (dmax - dmin)/4; // threshold - 25% NSLog(@" - calibration finished, sum=%d, delta=%d", calibSum, (int) calibDelta); } - (void) resetAnalysis { anaFrameNo = 0; bpsBubbles = 0; bpsTime = 0.0; [bubbles start]; } - (void) finishAnalysis { [bubbles stopWithFramesCount: anaFrameNo]; } - (BOOL) analyze: (unsigned char*) data width: (int) w height: (int) h { int len = w * h; unsigned char *c = data; unsigned char *r = (unsigned char*) [refFrame bytes]; anaFrameNo++; if ([refFrame length] != len) { [self signalError: @"Frame size changed, re-calibration is necessary!"]; self.analyze = NO; self.calibrated = NO; return NO; } unsigned int x = blindOff; while (x < w) { unsigned int i = x; while (i < len) { if (spot(r, c, i, calibDelta)) { NSLog(@"frame %d: found spot at %d (%dx%d)", anaFrameNo, i, i%656, i/656); memset(tempBytes, 0, len); int ymin = i, ymax = i, xmin = x, xmax = x; bounds(r, c, tempBytes, i, x, w, calibDelta, &ymin, &ymax, &xmin, &xmax); int bsize = xmax - xmin + 1; NSLog(@" - bounds: %d x %d - %d x %d (size %d x %d)", xmin, ymin/656, xmax, ymax/656, xmax-xmin+1, ymax/656 - ymin/656 + 1); #ifndef FAST { /* create analysis box */ int y1 = ymin/656, y2 = ymax/656; int a = y1*656+xmin; data[a]=255; data[a+1]=255; data[a+2]=255; data[a+w]=255; data[a+w+w]=255; a = y1*656+xmax; data[a]=255; data[a-1]=255; data[a-2]=255; data[a+w]=255; data[a+w+w]=255; a = y2*656+xmax; data[a]=255; data[a-1]=255; data[a-2]=255; data[a-w]=255; data[a-w-w]=255; a = y2*656+xmin; data[a]=255; data[a+1]=255; data[a+2]=255; data[a-w]=255; data[a-w-w]=255; } #endif x = xmax; // skip the whole blob if (bsize >= minBubSize) { int bheight = ymax/656 - ymin/656 + 1; if (bheight > bsize/2 && bheight/2 < bsize) { // aspect ratio should be between 2:1 and 1:2 NSLog(@" - bubble: %d pixels, finishing frame", bsize); if (lastBubPos > xmin) { NSLog(@" - unique bubble"); [bubbles addBubbleAtFrame:anaFrameNo size:bsize]; self.status = [bubbles infoString]; #ifndef FAST int y1 = ymin/656, y2 = ymax/656; int a = ((y1+y2)/2)*656; { int xx =0; while (xx<656) data[a+xx++]/=2; } a = (xmax+xmin)/2; while (a /* RVF1 (Raw Video Frame format) [ ] ... id = 0x52564631 - defines endianness of width and height 4 MSB of width and height are reserved: width 4 MSB: bpp = avg bytes per pixel in a frame (1+x/2 => 0=1, 1=1.5, 2=2, 3=2.5, 4=3, 6=4) height 4 MSB: ; (format: 0=mono, 1=RGB, 2=RGBA) - if the origin bit (MSB) is set then x and y are included */ - (BOOL) readFramesFromFile: (NSString*) fn { unsigned int i[3]; BOOL swap = NO; NSMutableData *md; FILE *f = fopen([fn UTF8String], "rb"); NSLog(@"open f=%p", f); if (!f) return NO; NSLog(@"- read header"); if (fread(i, sizeof(int), 1, f) != 1) { fclose(f); return NO; } NSLog(@"- check magic"); if (i[0] != 0x52564631 && i[0] != 0x31465652) { fclose(f); return NO; } if (i[0] == 0x31465652) swap = YES; anaFrameNo = 0; anaMax = 1.0; anaMin = 255.0; md = [[NSMutableData alloc] initWithCapacity: 656*491]; unsigned int fileFrame = 0; [self resetCalibration]; while (!feof(f)) { int n; NSLog(@"- read frame header (swap=%d)", swap); n = fread(i, sizeof(int), 2, f); if (n == 0 || feof(f)) break; if (n != 2) { fclose(f); [md release]; return NO; } if (swap) { i[0] = (i[0] << 24) | ((i[0]&0xff00) << 8) | ((i[0]&0xff0000) >> 8) | (i[0] >> 24); i[1] = (i[1] << 24) | ((i[1]&0xff00) << 8) | ((i[1]&0xff0000) >> 8) | (i[1] >> 24); } NSLog(@"- w=%d, h=%d", i[0], i[1]); if (((i[0]&0xf000)>0)||((i[1]&0xf000)>0)) { fclose(f); [md release]; return NO; } /* we handle plain 8-bit mono without origin only */ { unsigned int len = i[0]*i[1]; [md setLength:len]; if (fread([md mutableBytes], 1, len, f) != len) { fclose(f); [md release]; self.maxFrame=[frames count]-1; return NO; } fileFrame++; if (fileFrame > ANA_SKIP) { if (fileFrame - ANA_SKIP <= ANA_CALIBRATE) { [self calibrate: (unsigned char *) [md mutableBytes] width: i[0] height: i[1]]; if (fileFrame - ANA_SKIP == ANA_CALIBRATE) [self calibrationFinished]; } else [self analyze: (unsigned char *) [md mutableBytes] width: i[0] height: i[1]]; } [frames addObject:[NSData dataWithData:md]]; } } NSLog(@"- done"); fclose(f); [md release]; self.maxFrame=[frames count]-1; return YES; } - (BOOL) writeFramesToFile: (NSString*) fn { unsigned int i[3] = { 656, 491, 0x52564631 }, j = 0, k = [frames count]; FILE *f = fopen([fn UTF8String], "wb"); if (!f) return NO; if (fwrite(i+2, sizeof(int), 1, f) != 1) { fclose(f); return NO; } while (j < k) { NSData *data = (NSData*) [frames objectAtIndex:j]; if (data) { i[1] = [data length] / i[0]; // get the height based on the width if (fwrite(i, sizeof(int), 2, f) != 2) { fclose(f); return NO; } if (fwrite([data bytes], 1, i[1]*i[0], f) != i[1]*i[0]) { fclose(f); return NO; } } j++; } fclose(f); return YES; } - (IBAction) openDocument: (id)sender { NSOpenPanel *op; int runResult; op = [NSOpenPanel openPanel]; [op setRequiredFileType:@"rvf"]; [op setCanChooseDirectories: NO]; [op setAllowsMultipleSelection: NO]; if (!lastFilePath) lastFilePath = NSHomeDirectory(); runResult = [op runModalForDirectory:lastFilePath file:@"video"]; if (runResult == NSOKButton) { lastFilePath = [[op directory] copy]; if (![self readFramesFromFile:lastFileName=[op filename]]) NSBeep(); } } - (IBAction) saveDocumentAs: (id)sender { NSSavePanel *sp; int runResult; sp = [NSSavePanel savePanel]; //[sp setAccessoryView:nil]; [sp setRequiredFileType:@"txt"]; if (!lastFilePath) lastFilePath = NSHomeDirectory(); runResult = [sp runModalForDirectory:lastFilePath file:@"bubbles"]; if (runResult == NSOKButton) { lastFilePath = [[sp directory] copy]; if (![bubbles writeToFile:lastFileName=[sp filename] atomically:YES]) NSBeep(); } } - (IBAction) saveVideoAs: (id)sender { NSSavePanel *sp; int runResult; sp = [NSSavePanel savePanel]; //[sp setAccessoryView:nil]; [sp setRequiredFileType:@"rvf"]; if (!lastFilePath) lastFilePath = NSHomeDirectory(); runResult = [sp runModalForDirectory:lastFilePath file:@"video"]; if (runResult == NSOKButton) { lastFilePath = [[sp directory] copy]; if (![self writeFramesToFile:[sp filename]]) NSBeep(); } } - (IBAction) saveDocument: (id)sender { // if (!lastFileName) [self saveDocumentAs:sender]; /* else if (![self writeFramesToFile:lastFileName]) NSBeep(); */ } @end