// // SpectrumViewCG.m // Cog // // Created by Christopher Snowhill on 2/12/22. // #import "SpectrumViewCG.h" #import "NSView+Visibility.h" #import "analyzer.h" #define LOWER_BOUND -80 static void *kSpectrumViewCGContext = &kSpectrumViewCGContext; extern NSString *CogPlaybackDidBeginNotficiation; extern NSString *CogPlaybackDidPauseNotficiation; extern NSString *CogPlaybackDidResumeNotficiation; extern NSString *CogPlaybackDidStopNotficiation; @interface SpectrumViewCG () { VisualizationController *visController; NSTimer *timer; float saLowerBound; BOOL paused; BOOL stopped; BOOL isListening; BOOL observersAdded; BOOL isFullView; BOOL isOccluded; NSRect initFrame; NSDictionary *textAttrs; NSColor *baseColor; NSColor *peakColor; NSColor *backgroundColor; NSColor *borderColor; ddb_analyzer_t _analyzer; ddb_analyzer_draw_data_t _draw_data; float visAudio[4096], visFFT[2048]; } @end @implementation SpectrumViewCG @synthesize isListening; - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if(self) { initFrame = frame; [self setup]; } return self; } - (void)updateVisListening { if(self.isListening && (![self visibleInWindow] || isOccluded || paused || stopped)) { [self stopTimer]; self.isListening = NO; } else if(!self.isListening && ([self visibleInWindow] && !isOccluded && !stopped && !paused)) { [self startTimer]; self.isListening = YES; } } - (void)setOccluded:(BOOL)occluded { isOccluded = occluded; [self updateVisListening]; } - (void)windowChangedOcclusionState:(NSNotification *)notification { if([notification object] == self.window) { BOOL curOccluded = !self.window; if(!curOccluded) { curOccluded = !(self.window.occlusionState & NSWindowOcclusionStateVisible); } if(curOccluded != isOccluded) { [self setOccluded:curOccluded]; } } } - (void)setup { visController = [NSClassFromString(@"VisualizationController") sharedController]; timer = nil; stopped = YES; paused = NO; isListening = NO; saLowerBound = LOWER_BOUND; [self colorsDidChange:nil]; BOOL freqMode = [[NSUserDefaults standardUserDefaults] boolForKey:@"spectrumFreqMode"]; ddb_analyzer_init(&_analyzer); _analyzer.db_lower_bound = LOWER_BOUND; _analyzer.min_freq = 10; _analyzer.max_freq = 22000; _analyzer.peak_hold = 10; _analyzer.view_width = 64; _analyzer.fractional_bars = 1; _analyzer.octave_bars_step = 2; _analyzer.max_of_stereo_data = 1; _analyzer.freq_is_log = 0; _analyzer.mode = freqMode ? DDB_ANALYZER_MODE_FREQUENCIES : DDB_ANALYZER_MODE_OCTAVE_NOTE_BANDS; [self addObservers]; } - (void)enableFullView { isFullView = YES; _analyzer.freq_is_log = 1; [self repaint]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if(context == kSpectrumViewCGContext) { if([keyPath isEqualToString:@"self.window.visible"]) { [self updateVisListening]; } else { [self colorsDidChange:nil]; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)addObservers { if(!observersAdded) { NSUserDefaultsController *sharedUserDefaultsController = [NSUserDefaultsController sharedUserDefaultsController]; [sharedUserDefaultsController addObserver:self forKeyPath:@"values.spectrumBarColor" options:0 context:kSpectrumViewCGContext]; [sharedUserDefaultsController addObserver:self forKeyPath:@"values.spectrumDotColor" options:0 context:kSpectrumViewCGContext]; [self addObserver:self forKeyPath:@"self.window.visible" options:0 context:kSpectrumViewCGContext]; NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; [defaultCenter addObserver:self selector:@selector(colorsDidChange:) name:NSSystemColorsDidChangeNotification object:nil]; [defaultCenter addObserver:self selector:@selector(playbackDidBegin:) name:CogPlaybackDidBeginNotficiation object:nil]; [defaultCenter addObserver:self selector:@selector(playbackDidPause:) name:CogPlaybackDidPauseNotficiation object:nil]; [defaultCenter addObserver:self selector:@selector(playbackDidResume:) name:CogPlaybackDidResumeNotficiation object:nil]; [defaultCenter addObserver:self selector:@selector(playbackDidStop:) name:CogPlaybackDidStopNotficiation object:nil]; [defaultCenter addObserver:self selector:@selector(windowChangedOcclusionState:) name:NSWindowDidChangeOcclusionStateNotification object:nil]; observersAdded = YES; } } - (void)dealloc { ddb_analyzer_dealloc(&_analyzer); ddb_analyzer_draw_data_dealloc(&_draw_data); [self removeObservers]; } - (void)removeObservers { if(observersAdded) { NSUserDefaultsController *sharedUserDefaultsController = [NSUserDefaultsController sharedUserDefaultsController]; [sharedUserDefaultsController removeObserver:self forKeyPath:@"values.spectrumBarColor" context:kSpectrumViewCGContext]; [sharedUserDefaultsController removeObserver:self forKeyPath:@"values.spectrumDotColor" context:kSpectrumViewCGContext]; [self removeObserver:self forKeyPath:@"self.window.visible" context:kSpectrumViewCGContext]; NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; [defaultCenter removeObserver:self name:NSSystemColorsDidChangeNotification object:nil]; [defaultCenter removeObserver:self name:CogPlaybackDidBeginNotficiation object:nil]; [defaultCenter removeObserver:self name:CogPlaybackDidPauseNotficiation object:nil]; [defaultCenter removeObserver:self name:CogPlaybackDidResumeNotficiation object:nil]; [defaultCenter removeObserver:self name:CogPlaybackDidStopNotficiation object:nil]; [defaultCenter removeObserver:self name:NSWindowDidChangeOcclusionStateNotification object:nil]; observersAdded = NO; } } - (void)repaint { self.needsDisplay = YES; } - (void)startTimer { [self stopTimer]; timer = [NSTimer timerWithTimeInterval:1.0 / 60.0 target:self selector:@selector(timerRun:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } - (void)stopTimer { [timer invalidate]; timer = nil; } - (void)timerRun:(NSTimer *)timer { [self repaint]; } - (void)colorsDidChange:(NSNotification *)notification { backgroundColor = [NSColor textBackgroundColor]; backgroundColor = [backgroundColor colorWithAlphaComponent:0.0]; borderColor = [NSColor systemGrayColor]; NSValueTransformer *colorToValueTransformer = [NSValueTransformer valueTransformerForName:@"ColorToValueTransformer"]; baseColor = [colorToValueTransformer transformedValue:[[NSUserDefaults standardUserDefaults] dataForKey:@"spectrumBarColor"]]; peakColor = [colorToValueTransformer transformedValue:[[NSUserDefaults standardUserDefaults] dataForKey:@"spectrumDotColor"]]; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.alignment = NSTextAlignmentLeft; textAttrs = @{ NSFontAttributeName: [NSFont fontWithName:@"HelveticaNeue" size:10], NSParagraphStyleAttributeName: paragraphStyle, NSForegroundColorAttributeName: borderColor }; [self repaint]; } - (void)startPlayback { [self playbackDidBegin:nil]; } - (void)playbackDidBegin:(NSNotification *)notification { stopped = NO; paused = NO; [self updateVisListening]; } - (void)playbackDidPause:(NSNotification *)notification { stopped = NO; paused = YES; [self updateVisListening]; } - (void)playbackDidResume:(NSNotification *)notification { stopped = NO; paused = NO; [self updateVisListening]; } - (void)playbackDidStop:(NSNotification *)notification { stopped = YES; paused = NO; [self updateVisListening]; [self repaint]; } - (void)drawSaGrid { CGContextRef context = NSGraphicsContext.currentContext.CGContext; // horz lines, db scale CGContextSetStrokeColorWithColor(context, borderColor.CGColor); CGFloat lower = -floor(saLowerBound); for(int db = 10; db < lower; db += 10) { CGFloat y = (CGFloat)(db / lower) * NSHeight(self.bounds); if(y >= NSHeight(self.bounds)) { break; } CGPoint points[] = { CGPointMake(0, y), CGPointMake(NSWidth(self.bounds) - 1, y) }; CGContextAddLines(context, points, 2); } CGFloat dash[2] = { 1, 2 }; CGContextSetLineDash(context, 0, dash, 2); CGContextStrokePath(context); CGContextSetLineDash(context, 0, NULL, 0); // db text for(int db = 10; db < lower; db += 10) { CGFloat y = (CGFloat)(db / lower) * NSHeight(self.bounds); if(y >= NSHeight(self.bounds)) { break; } NSString *string = [NSString stringWithFormat:@"%d dB", -db]; [string drawAtPoint:NSMakePoint(0, NSHeight(self.bounds) - y - 12) withAttributes:textAttrs]; } } - (void)drawFrequencyLabels { // octaves text for(int i = 0; i < _draw_data.label_freq_count; i++) { if(_draw_data.label_freq_positions < 0) { continue; } NSString *string = [NSString stringWithUTF8String:_draw_data.label_freq_texts[i]]; CGFloat x = _draw_data.label_freq_positions[i]; [string drawAtPoint:NSMakePoint(x, NSHeight(self.bounds) - 12) withAttributes:textAttrs]; } } - (void)drawAnalyzerDescreteFrequencies { CGContextRef context = NSGraphicsContext.currentContext.CGContext; ddb_analyzer_draw_bar_t *bar = _draw_data.bars; for(int i = 0; i < _draw_data.bar_count; i++, bar++) { CGContextMoveToPoint(context, bar->xpos, 0); CGContextAddLineToPoint(context, bar->xpos, bar->bar_height); } CGContextSetStrokeColorWithColor(context, baseColor.CGColor); CGContextStrokePath(context); bar = _draw_data.bars; for(int i = 0; i < _draw_data.bar_count; i++, bar++) { CGContextMoveToPoint(context, bar->xpos - 0.5, bar->peak_ypos); CGContextAddLineToPoint(context, bar->xpos + 0.5, bar->peak_ypos); } CGContextSetStrokeColorWithColor(context, peakColor.CGColor); CGContextStrokePath(context); } - (void)drawAnalyzerOctaveBands { CGContextRef context = NSGraphicsContext.currentContext.CGContext; ddb_analyzer_draw_bar_t *bar = _draw_data.bars; for(int i = 0; i < _draw_data.bar_count; i++, bar++) { CGContextAddRect(context, CGRectMake(bar->xpos, 0, _draw_data.bar_width, bar->bar_height)); } CGContextSetFillColorWithColor(context, baseColor.CGColor); CGContextFillPath(context); bar = _draw_data.bars; for(int i = 0; i < _draw_data.bar_count; i++, bar++) { CGContextAddRect(context, CGRectMake(bar->xpos, bar->peak_ypos, _draw_data.bar_width, 1.0)); } CGContextSetFillColorWithColor(context, peakColor.CGColor); CGContextFillPath(context); } - (void)drawAnalyzer { if(_analyzer.mode == DDB_ANALYZER_MODE_FREQUENCIES) { [self drawAnalyzerDescreteFrequencies]; } else { [self drawAnalyzerOctaveBands]; } } - (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; [self updateVisListening]; [backgroundColor setFill]; NSRectFill(dirtyRect); if(!isFullView) { CGContextRef context = NSGraphicsContext.currentContext.CGContext; CGContextMoveToPoint(context, 0.0, 0.0); CGContextAddLineToPoint(context, initFrame.size.width, 0.0); CGContextAddLineToPoint(context, initFrame.size.width, initFrame.size.height); CGContextAddLineToPoint(context, 0.0, initFrame.size.height); CGContextAddLineToPoint(context, 0.0, 0.0); CGContextSetStrokeColorWithColor(context, borderColor.CGColor); CGContextStrokePath(context); } if(stopped) return; if(isFullView) { _analyzer.view_width = self.bounds.size.width; } [self->visController copyVisPCM:&visAudio[0] visFFT:&visFFT[0] latencyOffset:0]; ddb_analyzer_process(&_analyzer, [self->visController readSampleRate] / 2.0, 1, visFFT, 2048); ddb_analyzer_tick(&_analyzer); ddb_analyzer_get_draw_data(&_analyzer, self.bounds.size.width, self.bounds.size.height, &_draw_data); if(isFullView) { [self drawSaGrid]; [self drawFrequencyLabels]; } [self drawAnalyzer]; } - (void)mouseDown:(NSEvent *)event { BOOL freqMode = ![[NSUserDefaults standardUserDefaults] boolForKey:@"spectrumFreqMode"]; [[NSUserDefaults standardUserDefaults] setBool:freqMode forKey:@"spectrumFreqMode"]; _analyzer.mode = freqMode ? DDB_ANALYZER_MODE_FREQUENCIES : DDB_ANALYZER_MODE_OCTAVE_NOTE_BANDS; _analyzer.mode_did_change = 1; [self repaint]; } @end