Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
Relevant replacement documents include:
Source/CocoaUI/AppleDemoFilter_GraphView.m
/* |
File: AppleDemoFilter_GraphView.m |
Abstract: AppleDemoFilter_GraphView.m |
Version: 1.01 |
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) 2012 Apple Inc. All Rights Reserved. |
*/ |
#import "AppleDemoFilter_GraphView.h" |
@implementation AppleDemoFilter_GraphView |
#define kDefaultMinHertz 12 |
#define kDefaultMaxHertz 22050 |
#define kLogBase 2 |
#define kNumGridLines 11 |
#define kNumDBLines 4 |
#define kDefaultGain 20 |
#define kDBAxisGap 35 |
#define kFreqAxisGap 17 |
#define kRightMargin 10 |
#define kTopMargin 5 |
NSString *kGraphViewDataChangedNotification = @"AppleDemoFilter_GraphViewDataChangedNotification"; |
NSString *kGraphViewBeginGestureNotification= @"AppleDemoFilter_GraphViewBeginGestureNotification"; |
NSString *kGraphViewEndGestureNotification= @"AppleDemoFilter_GraphViewEndGestureNotification"; |
- (id)initWithFrame:(NSRect)frameRect |
{ |
if ((self = [super initWithFrame:frameRect]) != nil) { |
// Add initialization code here |
mGraphFrame = NSMakeRect(kDBAxisGap, // Initialize frame that is used for drawing the graph content |
kFreqAxisGap, |
frameRect.size.width - kDBAxisGap - kRightMargin, |
frameRect.size.height - kFreqAxisGap - kTopMargin); |
// Initialize the text attributes for the db and frequency axis |
NSMutableParagraphStyle *dbLabelStyle = [[NSMutableParagraphStyle alloc] init]; |
[dbLabelStyle setParagraphStyle: [NSParagraphStyle defaultParagraphStyle]]; |
[dbLabelStyle setAlignment:NSRightTextAlignment]; |
mDBAxisStringAttributes = [[NSDictionary dictionaryWithObjectsAndKeys: [NSFont systemFontOfSize: 9], NSFontAttributeName, |
[dbLabelStyle autorelease], NSParagraphStyleAttributeName, |
[NSColor colorWithDeviceWhite: .1 alpha: 1], NSForegroundColorAttributeName, nil] retain]; |
NSMutableParagraphStyle *freqLabelStyle = [[NSMutableParagraphStyle alloc] init]; |
[freqLabelStyle setParagraphStyle: [NSParagraphStyle defaultParagraphStyle]]; |
[freqLabelStyle setAlignment:NSCenterTextAlignment]; |
mFreqAxisStringAttributes = [[NSDictionary dictionaryWithObjectsAndKeys: [NSFont systemFontOfSize: 9], NSFontAttributeName, |
[freqLabelStyle autorelease], NSParagraphStyleAttributeName, |
[NSColor colorWithDeviceWhite: .1 alpha: 1], NSForegroundColorAttributeName, nil] retain]; |
mEditPoint = NSZeroPoint; |
mActiveWidth = [self locationForFrequencyValue: kDefaultMaxHertz] - mGraphFrame.origin.x - .5; |
[self setPostsFrameChangedNotifications: YES]; // send notifications when the frame changes |
} |
return self; |
} |
-(void) dealloc { |
[mDBAxisStringAttributes release]; |
[mFreqAxisStringAttributes release]; |
[mCurvePath release]; |
[curveColor release]; |
[mBackgroundCache release]; |
[super dealloc]; |
} |
/* Compute the pixel location on the y axis within the graph frame for the decibel value argument */ |
- (double) locationForDBValue: (double) value { |
double step = mGraphFrame.size.height / (kDefaultGain * 2); |
double location = (value + kDefaultGain) * step; |
return mGraphFrame.origin.y + location; |
} |
/* Compute the logarithm of a number with an arbitrary base */ |
static inline double logValueForNumber(double number, double base) { |
return log (number) / log(base); |
} |
/* Compute the pixel location on the x axis within the graph frame for the frequency value argument */ |
- (double) locationForFrequencyValue: (double) value { |
// how many pixels are in one base power increment? |
double pixelIncrement = mGraphFrame.size.width / kNumGridLines; |
double location = logValueForNumber(value/kDefaultMinHertz, kLogBase) * pixelIncrement; |
location = floor(location + mGraphFrame.origin.x) + .5; |
return location; |
} |
/* Compute the decibel value at a specific y coordinate in the graph */ |
- (double) dbValueForLocation: (float) location { |
double step = mGraphFrame.size.height / (kDefaultGain * 2);// number of pixels per db |
return ((location - mGraphFrame.origin.y)/ step) - kDefaultGain; |
} |
/* Compute the pixel value of a specific grid line */ |
static inline double valueAtGridIndex(double index) { |
return kDefaultMinHertz * pow(kLogBase, index); |
} |
/* Compute the frequency value of a specific pixel location in the graph */ |
- (double) freqValueForLocation: (float) location { |
double pixelIncrement = mGraphFrame.size.width / kNumGridLines; |
return valueAtGridIndex((location - mGraphFrame.origin.x - .5)/pixelIncrement); |
} |
/* returns a string for a specific double value (for displaying axis labels) */ |
- (NSString *) stringForValue:(double) value { |
NSString * theString; |
double temp = value; |
if (value >= 1000) |
temp = temp / 1000; |
temp = (floor(temp *100))/100; // chop everything after 2 decimal places |
// we don't want trailing 0's |
//if we do not have trailing zeros |
if (floor(temp) == temp) |
theString = [NSString localizedStringWithFormat: @"%.0f", temp]; |
else // if we have only one digit |
theString = [NSString localizedStringWithFormat: @"%.1f", temp]; |
return theString; |
} |
/* draws the DB grid lines and axis labels */ |
- (void)drawDBScale { |
NSPoint startPoint, endPoint; |
int index, value; |
[[NSColor whiteColor] set]; |
// figure out how many grid divisions to use for the gain axis |
for (index = -kNumDBLines; index <= kNumDBLines; index ++) { |
value = index * (kDefaultGain / kNumDBLines); |
startPoint = NSMakePoint(mGraphFrame.origin.x, floor([self locationForDBValue: index * (kDefaultGain/kNumDBLines)]) + .5); |
endPoint = NSMakePoint(mGraphFrame.origin.x + mActiveWidth, startPoint.y); |
if (index > -kNumDBLines && index < kNumDBLines) { |
if (index == 0) { |
[[NSColor colorWithDeviceWhite: .2 alpha: .3] set]; |
[NSBezierPath strokeLineFromPoint: startPoint toPoint: endPoint]; |
[[NSColor whiteColor] set]; |
} else |
[NSBezierPath strokeLineFromPoint: startPoint toPoint: endPoint]; |
} |
[[NSString localizedStringWithFormat: @"%d db", value] drawInRect: NSMakeRect(0, startPoint.y - 4, mGraphFrame.origin.x - 4, 11) withAttributes: mDBAxisStringAttributes]; |
} |
} |
/* Draws the frequency grid lines on a logarithmic scale */ |
- (void) drawMajorGridLines { |
int index; |
double location, value; |
float labelWidth = mGraphFrame.origin.x - 2; |
NSColor *gridColor = [[NSColor redColor] colorWithAlphaComponent: .15]; |
BOOL firstK = YES; // we only want a 'K' label the first time a value is over 1000 |
for (index = 0; index <= kNumGridLines; index++) { |
value = valueAtGridIndex(index); |
location = [self locationForFrequencyValue: value]; |
if (index > 0 && index < kNumGridLines) { |
[gridColor set]; |
[NSBezierPath strokeLineFromPoint: NSMakePoint(location, mGraphFrame.origin.y) |
toPoint: NSMakePoint(location, floor(mGraphFrame.origin.y + mGraphFrame.size.height - 2) +.5)]; |
NSString *s = [self stringForValue: value]; |
if (value >= 1000 && firstK) { |
s = [s stringByAppendingString: @"K"]; |
firstK = NO; |
} |
[s drawInRect: NSMakeRect(location - 3 - labelWidth/2, 0, labelWidth, 12) withAttributes: mFreqAxisStringAttributes]; |
} else if (index == 0) { // append hertz label to first frequency |
[[[self stringForValue: value] stringByAppendingString: @"Hz"] drawInRect: NSMakeRect(location - labelWidth/2, 0, labelWidth, 12) withAttributes: mFreqAxisStringAttributes]; |
} else { // always label the last grid marker the maximum hertz value |
[[[self stringForValue: kDefaultMaxHertz] stringByAppendingString: @"K"] drawInRect: NSMakeRect(location - labelWidth/2 - 12, 0, labelWidth + kRightMargin, 12) withAttributes: mFreqAxisStringAttributes]; |
} |
} |
} |
/* Draw the control point that modifies the curve */ |
-(void) drawControlPoint { |
NSRect controlPointRect = NSIntegralRect(NSMakeRect(mEditPoint.x - 3, mEditPoint.y - 3, 7, 7)); |
[[NSColor blueColor] set]; |
NSFrameRect(controlPointRect); |
if (!mMouseDown) // if the mouse isn't down, draw in a more muted gray color |
[[NSColor grayColor] set]; |
NSRect hLine = NSIntegralRect(NSMakeRect(mGraphFrame.origin.x, mEditPoint.y, (mEditPoint.x - 3) - mGraphFrame.origin.x, 1)); |
NSFrameRect(hLine); |
hLine.origin.x = mEditPoint.x + 4; |
hLine.size.width = mActiveWidth - (hLine.origin.x - mGraphFrame.origin.x)-1; |
NSFrameRect(hLine); |
NSRect vLine = NSIntegralRect(NSMakeRect(mEditPoint.x, mGraphFrame.origin.y, 1, (mEditPoint.y - 3) - mGraphFrame.origin.y)); |
NSFrameRect(vLine); |
vLine.origin.y = mEditPoint.y + 4; |
vLine.size.height = mGraphFrame.size.height - (vLine.origin.y - mGraphFrame.origin.y)-1; |
NSFrameRect(vLine); |
} |
/* ------- NOTES ON DRAWING ------- |
For the purposes of this sample, we do only the most basic performance optimizations in the interest |
of keeping this sample reasonably simple such as caching the background graph and labels in an image. |
There are several additional optimizations that could be performed in order to enhance graphic speed |
1) This is a non-opaque view. Every time it draws, it is necessary to redraw the window background before |
drawing the view contents. Drawing a solid fill color in the background of the view and overriding -(void) isOpaque |
to return YES would result in some speedup |
2) The view is drawn anti-aliased. It is probably unneccesary to draw anti-aliased when the mouse is being dragged. |
Calling setShouldAntialias: NO on the NSGraphicsContext when the mouse is dragging begins, and setting it back to |
YES when dragging ends could result in a substatial speed increase. Likewise, the control point could always be drawn |
with anti-aliasing off because it is drawn aligned to pixel boundaries |
3) Drawing the curve could be done with Quartz to avoid the overhead of NSBezierPath |
4) The curve should be drawn with increased resolution around the control point and less resolution farther away. The fewer |
points in the curve plot, the faster it will draw |
5) Drawing the curve without transparency may increase the render time |
Remember that before doing any optimization, make sure that your code is working correctly first, and then profile with Shark |
to determine which areas could benefit from the most optimization. Premature optimization can actually result in slower code. |
*/ |
- (void)drawRect:(NSRect)rect |
{ |
if (!mBackgroundCache) { |
mBackgroundCache = [[NSImage alloc] initWithSize: [self frame].size]; |
[mBackgroundCache lockFocus]; |
// fill the graph area |
[[NSColor colorWithDeviceWhite: .90 alpha: 1.0] set]; |
NSRectFill(NSIntersectionRect(rect, NSMakeRect(mGraphFrame.origin.x, mGraphFrame.origin.y, mActiveWidth, mGraphFrame.size.height))); |
// draw the graph border |
[[NSColor whiteColor] set]; |
NSRect lineRect = NSMakeRect(mGraphFrame.origin.x, mGraphFrame.origin.y-1, mActiveWidth, 1); |
NSRectFill(NSIntersectionRect(rect, lineRect)); |
[[NSColor colorWithDeviceWhite: .46 alpha: 1] set]; |
lineRect.origin.y = mGraphFrame.origin.y + mGraphFrame.size.height -1; |
NSRectFill(NSIntersectionRect(rect, lineRect)); |
[[NSColor colorWithDeviceWhite: .75 alpha: 1] set]; |
lineRect.origin.y -= 1; |
NSRectFill(NSIntersectionRect(rect, lineRect)); |
[self drawDBScale]; |
[self drawMajorGridLines]; |
[mBackgroundCache unlockFocus]; |
} |
[mBackgroundCache drawInRect: rect fromRect: rect operation: NSCompositeSourceOver fraction: 1.0]; |
// draw the curve |
// [[NSColor colorWithDeviceRed: .31 green: .37 blue: .73 alpha: .8] set]; |
if (curveColor) { |
[curveColor set]; |
[mCurvePath fill]; |
} |
// draw the controlPoint |
[self drawControlPoint]; |
} |
/* Respond to mouse events */ |
-(void) handleMouseEventAtLocation:(NSPoint) location { |
NSRect activeFrame = mGraphFrame; |
activeFrame.size.width = mActiveWidth + 2.5; // take into account crosshair box |
BOOL isInside = [self mouse:location inRect: activeFrame]; |
if (isInside) { |
mEditPoint = location; |
mFreq = [self freqValueForLocation: mEditPoint.x]; |
if (mFreq > kDefaultMaxHertz) |
mFreq = kDefaultMaxHertz; |
if (mFreq < kDefaultMinHertz) |
mFreq = kDefaultMinHertz; |
if (mEditPoint.y < mGraphFrame.origin.y+1) |
mRes = -kDefaultGain; |
else if (mEditPoint.y == mGraphFrame.origin.y + mGraphFrame.size.height) |
mRes = kDefaultGain; |
else |
mRes = [self dbValueForLocation: mEditPoint.y]; |
[[NSNotificationCenter defaultCenter] postNotificationName: kGraphViewDataChangedNotification object:self]; |
} |
} |
-(void) mouseDown:(NSEvent *) theEvent { |
NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; |
mMouseDown = YES; |
[[NSNotificationCenter defaultCenter] postNotificationName: kGraphViewBeginGestureNotification object:self]; |
[self handleMouseEventAtLocation: mouseLoc]; |
[self setNeedsDisplayInRect: mGraphFrame]; // update the display of the crosshairs |
} |
- (void)mouseDragged:(NSEvent *)theEvent { |
NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; |
mMouseDown = YES; |
[self handleMouseEventAtLocation: mouseLoc]; |
} |
- (void)mouseUp:(NSEvent *)theEvent { |
mMouseDown = NO; |
[[NSNotificationCenter defaultCenter] postNotificationName: kGraphViewEndGestureNotification object:self]; |
[self setNeedsDisplayInRect: mGraphFrame]; |
} |
/* Update the edit point based on the new resonance value */ |
-(void) setRes: (float) res { |
mRes = res; |
if (mRes > kDefaultGain) |
mRes = kDefaultGain; |
if (mRes < -kDefaultGain) |
mRes = -kDefaultGain; |
mEditPoint.y = floor([self locationForDBValue: mRes]); |
} |
/* Update the edit point based on the new frequency value */ |
-(void) setFreq: (float) freq { |
mFreq = freq; |
if (mFreq > kDefaultMaxHertz) |
mFreq = kDefaultMaxHertz; |
if (mFreq < kDefaultMinHertz) |
mFreq = kDefaultMinHertz; |
mEditPoint.x = floor([self locationForFrequencyValue: mFreq]); |
} |
/* Get the resonance value */ |
-(float)getRes { |
return mRes; |
} |
/* Get the frequency value */ |
-(float)getFreq { |
return mFreq; |
} |
/* update the graph frame and edit point when the view frame size changes */ |
-(void) setFrameSize: (NSSize) newSize { |
mGraphFrame.size.width = newSize.width - kDBAxisGap - kRightMargin; |
mGraphFrame.size.height= newSize.height - kFreqAxisGap - kTopMargin; |
mEditPoint.y = floor([self locationForDBValue: mRes]); |
mEditPoint.x = floor([self locationForFrequencyValue: mFreq]); |
mActiveWidth = [self locationForFrequencyValue: kDefaultMaxHertz] - mGraphFrame.origin.x - .5; |
[mBackgroundCache release]; |
mBackgroundCache = nil; |
[super setFrameSize: newSize]; |
} |
/* update the graph frame and edit point when the view frame changes */ |
-(void) setFrame: (NSRect) frameRect { |
mGraphFrame.size.width = frameRect.size.width - kDBAxisGap - kRightMargin; |
mGraphFrame.size.height= frameRect.size.height - kFreqAxisGap - kTopMargin; |
mEditPoint.y = floor([self locationForDBValue: mRes]); |
mEditPoint.x = floor([self locationForFrequencyValue: mFreq]); |
mActiveWidth = [self locationForFrequencyValue: kDefaultMaxHertz] - mGraphFrame.origin.x - .5; |
[mBackgroundCache release]; |
mBackgroundCache = nil; |
[super setFrame: frameRect]; |
} |
/* This method is called to set the frequency response values in the data that correspond to the points that we will will later be drawing |
The data argument is an array of FrequencyResponse structures. The number of items in this array is a fixed size, so we have a finite amount |
of resolution. We compute a pixelRatio which specifies how many pixels separate each frequency value |
*/ |
-(FrequencyResponse *) prepareDataForDrawing: (FrequencyResponse *) data { |
float width = mActiveWidth; |
float rightEdge = width + mGraphFrame.origin.x; |
int i, pixelRatio = (int) ceil(width/kNumberOfResponseFrequencies); |
float location = mGraphFrame.origin.x; // location is the x coordinate in the graph |
for (i = 0; i < kNumberOfResponseFrequencies; i++) { |
if (location > rightEdge) // if we have exceeded the right edge of our graph, just store the max hertz value |
data[i].mFrequency = kDefaultMaxHertz; |
else { |
float freq = [self freqValueForLocation: location]; // compute the frequency value for our location |
if (freq > kDefaultMaxHertz) // check to make sure our computed value does not exceed our maximum hertz value |
freq = kDefaultMaxHertz; |
data[i].mFrequency = freq; |
} |
location += pixelRatio; // increment our location counter |
} |
return data; |
} |
/* Draw the curve from the data */ |
-(void) plotData: (FrequencyResponse *) data { |
// NOTE that much of this data could be cached since it will be the same every time we draw as long as our frame size has not changed |
// We do not do this optimization in the interest of simplicity. |
float width = mActiveWidth; |
float rightEdge = width + mGraphFrame.origin.x; |
int i, pixelRatio = (int) ceil(width/kNumberOfResponseFrequencies); // compute how many pixels separate each db value |
float location = mGraphFrame.origin.x; |
if (!curveColor) |
curveColor = [[NSColor colorWithDeviceRed: .31 green: .37 blue: .73 alpha: .8] retain]; |
[mCurvePath release]; // release previous bezier path |
mCurvePath = [[NSBezierPath bezierPath] retain]; // create a new default empty path |
[mCurvePath moveToPoint: mGraphFrame.origin]; // start the bezier path at the bottom left corner of the graph |
float lastDBPos = 0; // cache the previous decibel pixel value |
for (i = 0; i < kNumberOfResponseFrequencies; i++) { |
float dbValue = 20.0*log10(data[i].mMagnitude); // compute the current decibel value |
float dbPos = 0; |
if (dbValue < -kDefaultGain) // constrain the current db value to our min and max gain interval |
dbPos = mGraphFrame.origin.y; |
else if (dbValue > kDefaultGain) |
dbPos = mGraphFrame.origin.y + mGraphFrame.size.height; |
else |
dbPos = [self locationForDBValue: dbValue]; // if the current db value is within our range, compute the location |
if (fabsf(lastDBPos - dbPos) >= .1) // only create a new point in our bezier path if the current db pixel value |
[mCurvePath lineToPoint: NSMakePoint(location, dbPos)]; // differs from our previous value by .1 pixels or more |
lastDBPos = dbPos; // cache current value |
location += pixelRatio; // increment our location |
if (location > rightEdge) { // if we get to the right edge of our graph, bail |
location = rightEdge; |
break; |
} |
} |
[mCurvePath lineToPoint: NSMakePoint(location, mGraphFrame.origin.y)]; // set the final point to the lower right hand corner of the graph |
[mCurvePath closePath]; |
[self setNeedsDisplay: YES]; // mark the graph as needing to be updated |
} |
-(void) handleBeginGesture { // called when parameter automation started |
mMouseDown = YES; // simulate physical mouse press in the view |
[self setNeedsDisplay: YES]; |
} |
-(void) handleEndGesture { // called when parameter automation finished |
mMouseDown = NO; // simulate mouse up in the view |
[self setNeedsDisplay: YES]; |
} |
-(void) disableGraphCurve { // update the view, but don't draw the curve (used when the AU is not initialized and the curve can not be retrieved) |
if (curveColor) { |
[curveColor release]; |
curveColor = nil; |
} |
[self setNeedsDisplay: YES]; |
} |
@end |
Copyright © 2012 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2012-08-28