Apparently, Xcode 15, at the point of the first beta, is not capable of producing nib files that will load on macOS 10.13 or 10.14 without crashing on startup. So this is the prep for now, raise the minimum deploy target to 10.15, where it currently works. This may change later. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
434 lines
13 KiB
Objective-C
434 lines
13 KiB
Objective-C
//
|
|
// SpectrumViewSK.m
|
|
// Cog
|
|
//
|
|
// Created by Christopher Snowhill on 2/12/22.
|
|
//
|
|
|
|
#import "SpectrumViewSK.h"
|
|
|
|
#import "NSView+Visibility.h"
|
|
|
|
#import <Metal/Metal.h>
|
|
|
|
#import "analyzer.h"
|
|
|
|
#import "Logging.h"
|
|
|
|
#define LOWER_BOUND -80
|
|
|
|
static void *kSpectrumViewSKContext = &kSpectrumViewSKContext;
|
|
|
|
extern NSString *CogPlaybackDidBeginNotficiation;
|
|
extern NSString *CogPlaybackDidPauseNotficiation;
|
|
extern NSString *CogPlaybackDidResumeNotficiation;
|
|
extern NSString *CogPlaybackDidStopNotficiation;
|
|
|
|
@interface SpectrumViewSK () {
|
|
VisualizationController *visController;
|
|
NSTimer *timer;
|
|
BOOL paused;
|
|
BOOL stopped;
|
|
BOOL isListening;
|
|
BOOL bandsReset;
|
|
BOOL cameraControlEnabled;
|
|
BOOL observersAdded;
|
|
BOOL isOccluded;
|
|
|
|
NSColor *backgroundColor;
|
|
ddb_analyzer_t _analyzer;
|
|
ddb_analyzer_draw_data_t _draw_data;
|
|
|
|
SCNVector3 cameraPosition2d;
|
|
SCNVector3 cameraEulerAngles2d;
|
|
SCNVector3 cameraPosition3d;
|
|
SCNVector3 cameraEulerAngles3d;
|
|
|
|
float visAudio[4096], visFFT[2048];
|
|
}
|
|
@end
|
|
|
|
@implementation SpectrumViewSK
|
|
|
|
+ (SpectrumViewSK *)createGuardWithFrame:(NSRect)frame {
|
|
if (![NSUserDefaults.standardUserDefaults boolForKey:@"spectrumSceneKit"])
|
|
return nil;
|
|
|
|
return [[SpectrumViewSK alloc] initWithFrame:frame];
|
|
}
|
|
|
|
@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] || 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)observeValueForKeyPath:(NSString *)keyPath
|
|
ofObject:(id)object
|
|
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
|
|
context:(void *)context {
|
|
if(context == kSpectrumViewSKContext) {
|
|
if([keyPath isEqualToString:@"self.window.visible"]) {
|
|
[self updateVisListening];
|
|
} else {
|
|
[self updateControls];
|
|
}
|
|
} else {
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
}
|
|
}
|
|
|
|
- (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 addObservers];
|
|
}
|
|
|
|
- (void)addObservers {
|
|
if(!observersAdded) {
|
|
NSUserDefaultsController *sharedUserDefaultsController = [NSUserDefaultsController sharedUserDefaultsController];
|
|
[sharedUserDefaultsController addObserver:self forKeyPath:@"values.spectrumProjectionMode" options:0 context:kSpectrumViewSKContext];
|
|
[sharedUserDefaultsController addObserver:self forKeyPath:@"values.spectrumBarColor" options:0 context:kSpectrumViewSKContext];
|
|
[sharedUserDefaultsController addObserver:self forKeyPath:@"values.spectrumDotColor" options:0 context:kSpectrumViewSKContext];
|
|
|
|
[self addObserver:self forKeyPath:@"self.window.visible" options:0 context:kSpectrumViewSKContext];
|
|
|
|
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
|
|
[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.spectrumProjectionMode" context:kSpectrumViewSKContext];
|
|
[sharedUserDefaultsController removeObserver:self forKeyPath:@"values.spectrumBarColor" context:kSpectrumViewSKContext];
|
|
[sharedUserDefaultsController removeObserver:self forKeyPath:@"values.spectrumDotColor" context:kSpectrumViewSKContext];
|
|
|
|
[self removeObserver:self forKeyPath:@"self.window.visible" context:kSpectrumViewSKContext];
|
|
|
|
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
|
|
[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 updateVisListening];
|
|
|
|
if(stopped) {
|
|
[self drawBaseBands];
|
|
return;
|
|
}
|
|
|
|
[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, 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;
|
|
bandsReset = 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
|