411 lines
12 KiB
Objective-C
411 lines
12 KiB
Objective-C
//
|
|
// SpectrumView.m
|
|
// Cog
|
|
//
|
|
// Created by Christopher Snowhill on 2/12/22.
|
|
//
|
|
|
|
#import "SpectrumView.h"
|
|
|
|
#import <Metal/Metal.h>
|
|
|
|
#import "analyzer.h"
|
|
|
|
#define LOWER_BOUND -80
|
|
|
|
void *kSpectrumViewContext = &kSpectrumViewContext;
|
|
|
|
extern NSString *CogPlaybackDidBeginNotficiation;
|
|
extern NSString *CogPlaybackDidPauseNotficiation;
|
|
extern NSString *CogPlaybackDidResumeNotficiation;
|
|
extern NSString *CogPlaybackDidStopNotficiation;
|
|
|
|
@interface NSView (Visibility)
|
|
- (BOOL)visibleInWindow;
|
|
@end
|
|
|
|
@implementation NSView (Visibility)
|
|
|
|
- (BOOL)visibleInWindow
|
|
{
|
|
if(self.window == nil) {
|
|
return NO;
|
|
}
|
|
|
|
// Might have zero opacity.
|
|
if(self.alphaValue == 0 || self.hiddenOrHasHiddenAncestor) {
|
|
return NO;
|
|
}
|
|
|
|
// Might be clipped by an ancestor.
|
|
return !NSIsEmptyRect(self.visibleRect);
|
|
}
|
|
|
|
@end
|
|
|
|
@interface SpectrumView () {
|
|
VisualizationController *visController;
|
|
NSTimer *timer;
|
|
BOOL paused;
|
|
BOOL stopped;
|
|
BOOL isListening;
|
|
BOOL bandsReset;
|
|
BOOL cameraControlEnabled;
|
|
BOOL observersRegistered;
|
|
|
|
NSColor *backgroundColor;
|
|
ddb_analyzer_t _analyzer;
|
|
ddb_analyzer_draw_data_t _draw_data;
|
|
|
|
SCNVector3 cameraPosition2d;
|
|
SCNVector3 cameraEulerAngles2d;
|
|
SCNVector3 cameraPosition3d;
|
|
SCNVector3 cameraEulerAngles3d;
|
|
}
|
|
@end
|
|
|
|
@implementation SpectrumView
|
|
|
|
@synthesize isListening;
|
|
|
|
- (id)initWithFrame:(NSRect)frame {
|
|
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
|
|
|
if(!device) return nil;
|
|
|
|
NSDictionary *sceneOptions = @{
|
|
SCNPreferredRenderingAPIKey: @(SCNRenderingAPIMetal),
|
|
SCNPreferredDeviceKey: device,
|
|
SCNPreferLowPowerDeviceKey: @(NO)
|
|
};
|
|
|
|
self = [super initWithFrame:frame options:sceneOptions];
|
|
if(self) {
|
|
[self setup];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)updateVisListening {
|
|
if(self.isListening && (![self visibleInWindow] || paused || stopped)) {
|
|
[self stopTimer];
|
|
self.isListening = NO;
|
|
} else if(!self.isListening && ([self visibleInWindow] && !stopped && !paused)) {
|
|
[self startTimer];
|
|
self.isListening = YES;
|
|
}
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
ofObject:(id)object
|
|
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
|
|
context:(void *)context {
|
|
if(context == kSpectrumViewContext) {
|
|
[self updateControls];
|
|
} else if([keyPath isEqualToString:@"self.window.visible"]) {
|
|
[self updateVisListening];
|
|
}
|
|
}
|
|
|
|
- (void)updateControls {
|
|
BOOL projectionMode = cameraControlEnabled ? NO : [[NSUserDefaults standardUserDefaults] boolForKey:@"spectrumProjectionMode"];
|
|
SCNNode *rootNode = [[self scene] rootNode];
|
|
SCNNode *cameraNode = [rootNode childNodeWithName:@"camera" recursively:NO];
|
|
SCNCamera *camera = [cameraNode camera];
|
|
if (projectionMode) {
|
|
cameraNode.eulerAngles = cameraEulerAngles2d;
|
|
cameraNode.position = cameraPosition2d;
|
|
camera.usesOrthographicProjection = YES;
|
|
camera.orthographicScale = 0.6;
|
|
} else {
|
|
cameraNode.eulerAngles = cameraEulerAngles3d;
|
|
cameraNode.position = cameraPosition3d;
|
|
camera.usesOrthographicProjection = NO;
|
|
camera.orthographicScale = 1.0;
|
|
}
|
|
|
|
NSValueTransformer *colorToValueTransformer = [NSValueTransformer valueTransformerForName:@"ColorToValueTransformer"];
|
|
|
|
NSColor *barColor = [colorToValueTransformer transformedValue:[[NSUserDefaults standardUserDefaults] dataForKey:@"spectrumBarColor"]];
|
|
NSColor *dotColor = [colorToValueTransformer transformedValue:[[NSUserDefaults standardUserDefaults] dataForKey:@"spectrumDotColor"]];
|
|
|
|
{
|
|
SCNNode *barNode = [rootNode childNodeWithName:@"cylinder0" recursively:NO];
|
|
SCNGeometry *geometry = [barNode geometry];
|
|
NSArray<SCNMaterial *> *materials = [geometry materials];
|
|
SCNMaterial *material = materials[0];
|
|
material.diffuse.contents = barColor;
|
|
material.emission.contents = barColor;
|
|
}
|
|
|
|
{
|
|
SCNNode *dotNode = [rootNode childNodeWithName:@"sphere0" recursively:NO];
|
|
SCNGeometry *geometry = [dotNode geometry];
|
|
NSArray<SCNMaterial *> *materials = [geometry materials];
|
|
SCNMaterial *material = materials[0];
|
|
material.diffuse.contents = dotColor;
|
|
material.emission.contents = dotColor;
|
|
}
|
|
}
|
|
|
|
- (void)enableCameraControl {
|
|
[self setAllowsCameraControl:YES];
|
|
cameraControlEnabled = YES;
|
|
[self updateControls];
|
|
}
|
|
|
|
- (void)setup {
|
|
visController = [NSClassFromString(@"VisualizationController") sharedController];
|
|
timer = nil;
|
|
stopped = YES;
|
|
paused = NO;
|
|
isListening = NO;
|
|
cameraControlEnabled = NO;
|
|
|
|
[self setBackgroundColor:[NSColor clearColor]];
|
|
|
|
SCNScene *theScene = [SCNScene sceneNamed:@"Scenes.scnassets/Spectrum.scn"];
|
|
[self setScene:theScene];
|
|
|
|
[self setAntialiasingMode:SCNAntialiasingModeMultisampling8X];
|
|
|
|
SCNNode *rootNode = [[self scene] rootNode];
|
|
SCNNode *cameraNode = [rootNode childNodeWithName:@"camera" recursively:NO];
|
|
cameraPosition2d = SCNVector3Make(0.0, 0.5, 1.0);
|
|
cameraEulerAngles2d = SCNVector3Zero;
|
|
// Save initial camera position from SceneKit file.
|
|
cameraPosition3d = cameraNode.position;
|
|
cameraEulerAngles3d = cameraNode.eulerAngles;
|
|
[self updateControls];
|
|
|
|
bandsReset = NO;
|
|
[self drawBaseBands];
|
|
|
|
//[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 = 1000;
|
|
_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 registerObservers];
|
|
}
|
|
|
|
- (void)registerObservers {
|
|
if(!observersRegistered) {
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.spectrumProjectionMode" options:0 context:kSpectrumViewContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.spectrumBarColor" options:0 context:kSpectrumViewContext];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.spectrumDotColor" options:0 context:kSpectrumViewContext];
|
|
|
|
[self addObserver:self forKeyPath:@"self.window.visible" options:0 context:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(playbackDidBegin:)
|
|
name:CogPlaybackDidBeginNotficiation
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(playbackDidPause:)
|
|
name:CogPlaybackDidPauseNotficiation
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(playbackDidResume:)
|
|
name:CogPlaybackDidResumeNotficiation
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(playbackDidStop:)
|
|
name:CogPlaybackDidStopNotficiation
|
|
object:nil];
|
|
|
|
observersRegistered = YES;
|
|
}
|
|
}
|
|
|
|
- (void)dealloc {
|
|
ddb_analyzer_dealloc(&_analyzer);
|
|
ddb_analyzer_draw_data_dealloc(&_draw_data);
|
|
|
|
[self removeObservers];
|
|
}
|
|
|
|
- (void)removeObservers {
|
|
if(observersRegistered) {
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.spectrumProjectionMode"];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.spectrumBarColor"];
|
|
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.spectrumDotColor"];
|
|
|
|
[self removeObserver:self forKeyPath:@"self.window.visible" context:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
name:CogPlaybackDidBeginNotficiation
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
name:CogPlaybackDidPauseNotficiation
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
name:CogPlaybackDidResumeNotficiation
|
|
object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
name:CogPlaybackDidStopNotficiation
|
|
object:nil];
|
|
|
|
observersRegistered = NO;
|
|
}
|
|
}
|
|
|
|
- (void)repaint {
|
|
[self updateVisListening];
|
|
|
|
if(stopped) {
|
|
[self drawBaseBands];
|
|
return;
|
|
}
|
|
|
|
float visAudio[4096], visFFT[2048];
|
|
|
|
[self->visController copyVisPCM:&visAudio[0] visFFT:&visFFT[0]];
|
|
|
|
ddb_analyzer_process(&_analyzer, [self->visController readSampleRate] / 2.0, 1, visFFT, 2048);
|
|
ddb_analyzer_tick(&_analyzer);
|
|
ddb_analyzer_get_draw_data(&_analyzer, 11.0, 1.0, &_draw_data);
|
|
|
|
[self drawAnalyzer];
|
|
}
|
|
|
|
- (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)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)drawBaseBands {
|
|
if(bandsReset) return;
|
|
|
|
SCNScene *scene = [self scene];
|
|
SCNNode *rootNode = [scene rootNode];
|
|
NSArray<SCNNode *> *nodes = [rootNode childNodes];
|
|
|
|
for(int i = 0; i < 11; ++i) {
|
|
SCNNode *node = nodes[i + 1];
|
|
SCNNode *dotNode = nodes[i + 1 + 11];
|
|
SCNVector3 position = node.position;
|
|
position.y = 0.0;
|
|
node.scale = SCNVector3Make(1.0, 0.0, 1.0);
|
|
node.position = position;
|
|
|
|
position = dotNode.position;
|
|
position.y = 0.072;
|
|
dotNode.position = position;
|
|
}
|
|
|
|
bandsReset = YES;
|
|
}
|
|
|
|
- (void)drawAnalyzerOctaveBands {
|
|
const int maxBars = (int)(ceilf((float)(_draw_data.bar_count) / 11.0));
|
|
const int barStep = (int)(floorf((float)(_draw_data.bar_count) / 11.0));
|
|
|
|
ddb_analyzer_draw_bar_t *bar = _draw_data.bars;
|
|
|
|
SCNScene *scene = [self scene];
|
|
SCNNode *rootNode = [scene rootNode];
|
|
NSArray<SCNNode *> *nodes = [rootNode childNodes];
|
|
|
|
for(int i = 0; i < 11; ++i) {
|
|
float maxValue = 0.0;
|
|
float maxMax = 0.0;
|
|
for(int j = 0; j < maxBars; ++j) {
|
|
const int barBase = i * barStep;
|
|
const int barIndex = barBase + j;
|
|
if(barIndex < _draw_data.bar_count) {
|
|
if(bar[barIndex].bar_height > maxValue) {
|
|
maxValue = bar[barIndex].bar_height;
|
|
}
|
|
if(bar[barIndex].peak_ypos > maxMax) {
|
|
maxMax = bar[barIndex].peak_ypos;
|
|
}
|
|
}
|
|
}
|
|
SCNNode *node = nodes[i + 1];
|
|
SCNNode *dotNode = nodes[i + 1 + 11];
|
|
SCNVector3 position = node.position;
|
|
position.y = maxValue * 0.5;
|
|
node.scale = SCNVector3Make(1.0, maxValue, 1.0);
|
|
node.position = position;
|
|
|
|
position = dotNode.position;
|
|
position.y = maxMax + 0.072;
|
|
dotNode.position = position;
|
|
}
|
|
|
|
bandsReset = NO;
|
|
}
|
|
|
|
- (void)drawAnalyzer {
|
|
[self drawAnalyzerOctaveBands];
|
|
}
|
|
|
|
- (void)mouseDown:(NSEvent *)event {
|
|
if(cameraControlEnabled) return;
|
|
|
|
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
|