diff --git a/Audio/CogAudio.xcodeproj/project.pbxproj b/Audio/CogAudio.xcodeproj/project.pbxproj index f5eaef1b8..af67c20ea 100644 --- a/Audio/CogAudio.xcodeproj/project.pbxproj +++ b/Audio/CogAudio.xcodeproj/project.pbxproj @@ -63,6 +63,10 @@ 83725A8E27AA0DE60003F694 /* libsoxr.0.dylib in CopyFiles */ = {isa = PBXBuildFile; fileRef = 83725A8A27AA0DBF0003F694 /* libsoxr.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 83725A9027AA16C90003F694 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83725A7B27AA0D8A0003F694 /* Accelerate.framework */; }; 83725A9127AA16D50003F694 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83725A7C27AA0D8E0003F694 /* AVFoundation.framework */; }; + 8377C64C27B8C51500E8BC0F /* fft_accelerate.c in Sources */ = {isa = PBXBuildFile; fileRef = 8377C64B27B8C51500E8BC0F /* fft_accelerate.c */; }; + 8377C64E27B8C54400E8BC0F /* fft.h in Headers */ = {isa = PBXBuildFile; fileRef = 8377C64D27B8C54400E8BC0F /* fft.h */; }; + 8377C65227B8CAD100E8BC0F /* VisualizationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8377C65027B8CAD100E8BC0F /* VisualizationController.h */; }; + 8377C65327B8CAD100E8BC0F /* VisualizationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8377C65127B8CAD100E8BC0F /* VisualizationController.m */; }; 8384912718080FF100E7332D /* Logging.h in Headers */ = {isa = PBXBuildFile; fileRef = 8384912618080FF100E7332D /* Logging.h */; }; 839366671815923C006DD712 /* CogPluginMulti.h in Headers */ = {isa = PBXBuildFile; fileRef = 839366651815923C006DD712 /* CogPluginMulti.h */; }; 839366681815923C006DD712 /* CogPluginMulti.m in Sources */ = {isa = PBXBuildFile; fileRef = 839366661815923C006DD712 /* CogPluginMulti.m */; }; @@ -159,6 +163,10 @@ 83725A7C27AA0D8E0003F694 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; 83725A8827AA0DBF0003F694 /* soxr.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = soxr.h; sourceTree = ""; }; 83725A8A27AA0DBF0003F694 /* libsoxr.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libsoxr.0.dylib; sourceTree = ""; }; + 8377C64B27B8C51500E8BC0F /* fft_accelerate.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = fft_accelerate.c; sourceTree = ""; }; + 8377C64D27B8C54400E8BC0F /* fft.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fft.h; sourceTree = ""; }; + 8377C65027B8CAD100E8BC0F /* VisualizationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VisualizationController.h; sourceTree = ""; }; + 8377C65127B8CAD100E8BC0F /* VisualizationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VisualizationController.m; sourceTree = ""; }; 8384912618080FF100E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = ""; }; 839366651815923C006DD712 /* CogPluginMulti.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CogPluginMulti.h; sourceTree = ""; }; 839366661815923C006DD712 /* CogPluginMulti.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CogPluginMulti.m; sourceTree = ""; }; @@ -235,6 +243,7 @@ 08FB77AEFE84172EC02AAC07 /* Classes */ = { isa = PBXGroup; children = ( + 8377C64F27B8CAAB00E8BC0F /* Visualization */, 17F94DDC0B8D101100A34E87 /* Plugin.h */, 17D21EBB0B8BF44000D1EBDE /* AudioPlayer.h */, 17D21EBC0B8BF44000D1EBDE /* AudioPlayer.m */, @@ -326,6 +335,7 @@ 17D21CD80B8BE5B400D1EBDE /* ThirdParty */ = { isa = PBXGroup; children = ( + 8377C64A27B8C51500E8BC0F /* deadbeef */, 83725A8627AA0DBF0003F694 /* libsoxr */, 835C88AE279811A500E28EAE /* hdcd */, 835C88A22797D4D400E28EAE /* lvqcl */, @@ -426,6 +436,24 @@ name = Frameworks; sourceTree = ""; }; + 8377C64A27B8C51500E8BC0F /* deadbeef */ = { + isa = PBXGroup; + children = ( + 8377C64D27B8C54400E8BC0F /* fft.h */, + 8377C64B27B8C51500E8BC0F /* fft_accelerate.c */, + ); + path = deadbeef; + sourceTree = ""; + }; + 8377C64F27B8CAAB00E8BC0F /* Visualization */ = { + isa = PBXGroup; + children = ( + 8377C65027B8CAD100E8BC0F /* VisualizationController.h */, + 8377C65127B8CAD100E8BC0F /* VisualizationController.m */, + ); + path = Visualization; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -445,6 +473,7 @@ 17D21CF30B8BE5EF00D1EBDE /* Semaphore.h in Headers */, 17D21DC70B8BE79700D1EBDE /* CoreAudioUtils.h in Headers */, 17D21EBD0B8BF44000D1EBDE /* AudioPlayer.h in Headers */, + 8377C65227B8CAD100E8BC0F /* VisualizationController.h in Headers */, 834FD4F027AF93680063BC83 /* ChunkList.h in Headers */, 17F94DD50B8D0F7000A34E87 /* PluginController.h in Headers */, 17F94DDD0B8D101100A34E87 /* Plugin.h in Headers */, @@ -460,6 +489,7 @@ 835C88B1279811A500E28EAE /* hdcd_decode2.h in Headers */, 8EC1225F0B993BD500C5B3AD /* ConverterNode.h in Headers */, 8384912718080FF100E7332D /* Logging.h in Headers */, + 8377C64E27B8C54400E8BC0F /* fft.h in Headers */, 8E8D3D2F0CBAEE6E00135C1B /* AudioContainer.h in Headers */, B0575F2D0D687A0800411D77 /* Helper.h in Headers */, 835C88AD2797DA5800E28EAE /* util.h in Headers */, @@ -546,6 +576,7 @@ 8399CF2D27B5D1D5008751F1 /* NSDictionary+Merge.m in Sources */, 17D21CA80B8BE4BA00D1EBDE /* Node.m in Sources */, 17D21CAA0B8BE4BA00D1EBDE /* OutputNode.m in Sources */, + 8377C65327B8CAD100E8BC0F /* VisualizationController.m in Sources */, 834FD4F527AFA2150063BC83 /* Downmix.m in Sources */, 17D21CC60B8BE4BA00D1EBDE /* OutputCoreAudio.m in Sources */, 835C88B2279811A500E28EAE /* hdcd_decode2.c in Sources */, @@ -553,6 +584,7 @@ 17D21CF40B8BE5EF00D1EBDE /* Semaphore.m in Sources */, 8347C7422796C58800FA8A7D /* NSFileHandle+CreateFile.m in Sources */, 17D21DC80B8BE79700D1EBDE /* CoreAudioUtils.m in Sources */, + 8377C64C27B8C51500E8BC0F /* fft_accelerate.c in Sources */, 839366681815923C006DD712 /* CogPluginMulti.m in Sources */, 835C88AA2797D4D400E28EAE /* lpc.c in Sources */, 17D21EBE0B8BF44000D1EBDE /* AudioPlayer.m in Sources */, diff --git a/Audio/Output/OutputCoreAudio.h b/Audio/Output/OutputCoreAudio.h index cc4f44961..75ea43935 100644 --- a/Audio/Output/OutputCoreAudio.h +++ b/Audio/Output/OutputCoreAudio.h @@ -19,6 +19,8 @@ #import "Downmix.h" +#import "VisualizationController.h" + #import "Semaphore.h" //#define OUTPUT_LOG @@ -61,6 +63,8 @@ AudioStreamBasicDescription deviceFormat; // info about the default device AudioStreamBasicDescription streamFormat; // stream format last seen in render callback + AudioStreamBasicDescription visFormat; // Mono format for vis + uint32_t deviceChannelConfig; uint32_t streamChannelConfig; @@ -70,6 +74,9 @@ AudioUnit _eq; DownmixProcessor *downmixer; + DownmixProcessor *downmixerForVis; + + VisualizationController *visController; #ifdef OUTPUT_LOG FILE *_logFile; diff --git a/Audio/Output/OutputCoreAudio.m b/Audio/Output/OutputCoreAudio.m index d8e7a4c75..88c394b05 100644 --- a/Audio/Output/OutputCoreAudio.m +++ b/Audio/Output/OutputCoreAudio.m @@ -58,6 +58,9 @@ static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioAct amountToRead = inNumberFrames * bytesPerPacket; + int visTabulated = 0; + float visAudio[512]; // Chunk size + if(_self->stopping == YES || [_self->outputController shouldContinue] == NO) { // Chain is dead, fill out the serial number pointer forever with silence clearBuffers(ioData, amountToRead / bytesPerPacket, 0); @@ -85,6 +88,7 @@ static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioAct _self->streamChannelConfig = config; _self->streamFormatStarted = YES; _self->downmixer = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:_self->deviceFormat outputConfig:_self->deviceChannelConfig]; + _self->downmixerForVis = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:_self->visFormat outputConfig:AudioConfigMono]; } double chunkDuration = [chunk duration]; @@ -94,6 +98,9 @@ static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioAct float downmixedData[frameCount * channels]; [_self->downmixer process:[samples bytes] frameCount:frameCount output:downmixedData]; + [_self->downmixerForVis process:[samples bytes] frameCount:frameCount output:visAudio]; + visTabulated += frameCount; + fillBuffers(ioData, downmixedData, frameCount, 0); amountRead = frameCount * bytesPerPacket; [_self->outputController incrementAmountPlayed:chunkDuration]; @@ -113,6 +120,7 @@ static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioAct _self->streamFormat = format; _self->streamFormatStarted = YES; _self->downmixer = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:_self->deviceFormat outputConfig:_self->deviceChannelConfig]; + _self->downmixerForVis = [[DownmixProcessor alloc] initWithInputFormat:format inputConfig:config andOutputFormat:_self->visFormat outputConfig:AudioConfigMono]; } atomic_fetch_add(&_self->bytesRendered, frameCount * bytesPerPacket); double chunkDuration = [chunk duration]; @@ -121,6 +129,9 @@ static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioAct [_self->downmixer process:[samples bytes] frameCount:frameCount output:downmixedData]; fillBuffers(ioData, downmixedData, frameCount, amountRead / bytesPerPacket); + [_self->downmixerForVis process:[samples bytes] frameCount:frameCount output:visAudio + visTabulated]; + visTabulated += frameCount; + [_self->outputController incrementAmountPlayed:chunkDuration]; amountRead += frameCount * bytesPerPacket; @@ -148,8 +159,12 @@ static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioAct // buffer loop if it doesn't get anything, so always produce a full // buffer, and silence anything we couldn't supply. clearBuffers(ioData, (amountToRead - amountRead) / bytesPerPacket, amountRead / bytesPerPacket); + + vDSP_vclr(visAudio + visTabulated, 1, 512 - visTabulated); } + [_self->visController postVisPCM:visAudio]; + return 0; } }; @@ -469,7 +484,7 @@ default_device_changed(AudioObjectID inObjectID, UInt32 inNumberAddresses, const - (void)resetIfOutputChanged { AVAudioFormat *format = _au.outputBusses[0].format; - if(!restarted && !_deviceFormat || ![_deviceFormat isEqual:format]) { + if(!restarted && (!_deviceFormat || ![_deviceFormat isEqual:format])) { [outputController restartPlaybackAtCurrentPosition]; restarted = YES; } @@ -501,6 +516,11 @@ default_device_changed(AudioObjectID inObjectID, UInt32 inNumberAddresses, const deviceFormat.mBytesPerFrame = deviceFormat.mChannelsPerFrame * (deviceFormat.mBitsPerChannel / 8); deviceFormat.mBytesPerPacket = deviceFormat.mBytesPerFrame * deviceFormat.mFramesPerPacket; + visFormat = deviceFormat; + visFormat.mChannelsPerFrame = 1; + visFormat.mBytesPerFrame = visFormat.mChannelsPerFrame * (visFormat.mBitsPerChannel / 8); + visFormat.mBytesPerPacket = visFormat.mBytesPerFrame * visFormat.mFramesPerPacket; + /* Set the channel layout for the audio queue */ AudioChannelLayoutTag tag = 0; switch(deviceFormat.mChannelsPerFrame) { @@ -579,6 +599,7 @@ default_device_changed(AudioObjectID inObjectID, UInt32 inNumberAddresses, const restarted = NO; downmixer = nil; + downmixerForVis = nil; AudioComponentDescription desc; NSError *err; @@ -720,6 +741,8 @@ default_device_changed(AudioObjectID inObjectID, UInt32 inNumberAddresses, const [_au allocateRenderResourcesAndReturnError:&err]; + visController = [VisualizationController sharedController]; + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.outputDevice" options:0 context:NULL]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.GraphicEQenable" options:0 context:NULL]; observersapplied = YES; @@ -797,6 +820,9 @@ default_device_changed(AudioObjectID inObjectID, UInt32 inNumberAddresses, const if(downmixer) { downmixer = nil; } + if(downmixerForVis) { + downmixerForVis = nil; + } #ifdef OUTPUT_LOG if(_logFile) { fclose(_logFile); @@ -804,6 +830,7 @@ default_device_changed(AudioObjectID inObjectID, UInt32 inNumberAddresses, const } #endif outputController = nil; + visController = nil; } - (void)dealloc { diff --git a/Audio/ThirdParty/deadbeef/fft.h b/Audio/ThirdParty/deadbeef/fft.h new file mode 100644 index 000000000..dba7f8f4d --- /dev/null +++ b/Audio/ThirdParty/deadbeef/fft.h @@ -0,0 +1,40 @@ +/* + DeaDBeeF -- the music player + Copyright (C) 2009-2021 Alexey Yakovenko and other contributors + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef FFT_H +#define FFT_H + +#ifdef __cplusplus +extern "C" { +} +#endif + +void fft_calculate(const float *data, float *freq, int fft_size); + +void fft_free(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Audio/ThirdParty/deadbeef/fft_accelerate.c b/Audio/ThirdParty/deadbeef/fft_accelerate.c new file mode 100644 index 000000000..320731c1f --- /dev/null +++ b/Audio/ThirdParty/deadbeef/fft_accelerate.c @@ -0,0 +1,95 @@ +/* + DeaDBeeF -- the music player + Copyright (C) 2009-2021 Alexey Yakovenko and other contributors + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "fft.h" +#include + +static int _fft_size; +static float *_input_real; +static float *_input_imaginary; +static float *_output_real; +static float *_output_imaginary; +static float *_hamming; +static float *_sq_mags; + +static vDSP_DFT_Setup _dft_setup; + +static void +_init_buffers(int fft_size) { + if(fft_size != _fft_size) { + fft_free(); + + _input_real = calloc(fft_size * 2, sizeof(float)); + _input_imaginary = calloc(fft_size * 2, sizeof(float)); + _hamming = calloc(fft_size * 2, sizeof(float)); + _sq_mags = calloc(fft_size, sizeof(float)); + _output_real = calloc(fft_size * 2, sizeof(float)); + _output_imaginary = calloc(fft_size * 2, sizeof(float)); + + _dft_setup = vDSP_DFT_zop_CreateSetup(NULL, fft_size * 2, FFT_FORWARD); + vDSP_hamm_window(_hamming, fft_size * 2, 0); + + _fft_size = fft_size; + } +} + +void fft_calculate(const float *data, float *freq, int fft_size) { + int dft_size = fft_size * 2; + + _init_buffers(fft_size); + + vDSP_vmul(data, 1, _hamming, 1, _input_real, 1, dft_size); + + vDSP_DFT_Execute(_dft_setup, _input_real, _input_imaginary, _output_real, _output_imaginary); + + DSPSplitComplex split_complex = { + .realp = _output_real, + .imagp = _output_imaginary + }; + vDSP_zvmags(&split_complex, 1, _sq_mags, 1, fft_size); + + int sq_count = fft_size; + vvsqrtf(_sq_mags, _sq_mags, &sq_count); + + float mult = 2.f / fft_size; + vDSP_vsmul(_sq_mags, 1, &mult, freq, 1, fft_size); +} + +void fft_free(void) { + free(_input_real); + free(_input_imaginary); + free(_hamming); + free(_sq_mags); + free(_output_real); + free(_output_imaginary); + if(_dft_setup != NULL) { + vDSP_DFT_DestroySetup(_dft_setup); + } + _input_real = NULL; + _input_imaginary = NULL; + _hamming = NULL; + _sq_mags = NULL; + _dft_setup = NULL; + _output_real = NULL; + _output_imaginary = NULL; +} diff --git a/Audio/Visualization/VisualizationController.h b/Audio/Visualization/VisualizationController.h new file mode 100644 index 000000000..d31a695b8 --- /dev/null +++ b/Audio/Visualization/VisualizationController.h @@ -0,0 +1,23 @@ +// +// VisualizationController.h +// CogAudio Framework +// +// Created by Christopher Snowhill on 2/12/22. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface VisualizationController : NSObject { + float visAudio[512]; +} + ++ (VisualizationController *)sharedController; + +- (void)postVisPCM:(const float *)inPCM; +- (void)copyVisPCM:(float *)outPCM visFFT:(float *)outFFT; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Audio/Visualization/VisualizationController.m b/Audio/Visualization/VisualizationController.m new file mode 100644 index 000000000..2dca802e7 --- /dev/null +++ b/Audio/Visualization/VisualizationController.m @@ -0,0 +1,51 @@ +// +// VisualizationController.m +// CogAudio Framework +// +// Created by Christopher Snowhill on 2/12/22. +// + +#import "VisualizationController.h" +#import + +#import "fft.h" + +@implementation VisualizationController + +static VisualizationController *_sharedController = nil; + ++ (VisualizationController *)sharedController { + @synchronized(self) { + if(!_sharedController) { + _sharedController = [[VisualizationController alloc] init]; + } + } + return _sharedController; +} + +- (id)init { + self = [super init]; + if(self) { + vDSP_vclr(visAudio, 1, 512); + } + return self; +} + +- (void)dealloc { + fft_free(); +} + +- (void)postVisPCM:(const float *)inPCM { + @synchronized(self) { + cblas_scopy(512, inPCM, 1, visAudio, 1); + } +} + +- (void)copyVisPCM:(float *)outPCM visFFT:(float *)outFFT { + @synchronized(self) { + cblas_scopy(512, visAudio, 1, outPCM, 1); + fft_calculate(visAudio, outFFT, 256); + } +} + +@end diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index 562a59177..b229e08dc 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -32,7 +32,7 @@ - + @@ -130,11 +130,11 @@ - + - + @@ -876,6 +876,14 @@ + + + + + + + + @@ -888,6 +896,7 @@ + @@ -1122,6 +1131,14 @@ + + + + + + + + diff --git a/Cog.xcodeproj/project.pbxproj b/Cog.xcodeproj/project.pbxproj index a3f566e35..9c24add20 100644 --- a/Cog.xcodeproj/project.pbxproj +++ b/Cog.xcodeproj/project.pbxproj @@ -124,6 +124,7 @@ 836FB5A718206F2500B3AD2D /* Hively.bundle in CopyFiles */ = {isa = PBXBuildFile; fileRef = 836FB5471820538800B3AD2D /* Hively.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 8370D73D277419F700245CE0 /* SQLiteStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8370D73C277419F700245CE0 /* SQLiteStore.m */; }; 8370D73F2775AE1300245CE0 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8370D73E2775AE1300245CE0 /* libsqlite3.tbd */; }; + 8377C66327B8CF6300E8BC0F /* SpectrumView.m in Sources */ = {isa = PBXBuildFile; fileRef = 8377C66127B8CF6300E8BC0F /* SpectrumView.m */; }; 8384914018083E4E00E7332D /* filetype.icns in Resources */ = {isa = PBXBuildFile; fileRef = 8384913D18083E4E00E7332D /* filetype.icns */; }; 8384915918083EAB00E7332D /* infoTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 8384914318083EAB00E7332D /* infoTemplate.pdf */; }; 8384915A18083EAB00E7332D /* missingArt@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8384914418083EAB00E7332D /* missingArt@2x.png */; }; @@ -942,6 +943,9 @@ 8370D73C277419F700245CE0 /* SQLiteStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SQLiteStore.m; sourceTree = ""; }; 8370D73E2775AE1300245CE0 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 8375B05117FFEA400092A79F /* OpusPlugin.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OpusPlugin.xcodeproj; path = Plugins/Opus/OpusPlugin.xcodeproj; sourceTree = ""; }; + 8377C66127B8CF6300E8BC0F /* SpectrumView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SpectrumView.m; path = Visualization/SpectrumView.m; sourceTree = ""; }; + 8377C66227B8CF6300E8BC0F /* SpectrumView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SpectrumView.h; path = Visualization/SpectrumView.h; sourceTree = ""; }; + 8377C66427B8CF7A00E8BC0F /* VisualizationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VisualizationController.h; path = Audio/Visualization/VisualizationController.h; sourceTree = ""; }; 8384912518080F2D00E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Logging.h; sourceTree = ""; }; 8384913D18083E4E00E7332D /* filetype.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = filetype.icns; sourceTree = ""; }; 8384914318083EAB00E7332D /* infoTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = infoTemplate.pdf; path = Images/infoTemplate.pdf; sourceTree = ""; }; @@ -1073,6 +1077,7 @@ 835F00B3279BD1CD00055FCF /* Formatters */, 177042960B8BC53600B86321 /* Application */, 17E0D5D20F520E75005B6FED /* Window */, + 8377C66027B8CF2300E8BC0F /* Visualization */, 8E75752A09F31D5A0080F1EE /* Playlist */, 8E07AAEA0AAC90DC00A4B32F /* Preferences */, 17DDF6400E0CB6F100A2E4AD /* FileTree */, @@ -1686,6 +1691,16 @@ name = Products; sourceTree = ""; }; + 8377C66027B8CF2300E8BC0F /* Visualization */ = { + isa = PBXGroup; + children = ( + 8377C66427B8CF7A00E8BC0F /* VisualizationController.h */, + 8377C66227B8CF6300E8BC0F /* SpectrumView.h */, + 8377C66127B8CF6300E8BC0F /* SpectrumView.m */, + ); + name = Visualization; + sourceTree = ""; + }; 83B0669D180D5668008E3612 /* Products */ = { isa = PBXGroup; children = ( @@ -2451,6 +2466,7 @@ 1770429E0B8BC53600B86321 /* PlaybackController.m in Sources */, 1766C6930B911DF1004A7AE4 /* AudioScrobbler.m in Sources */, 8355D6B6180612F300D05687 /* NSData+MD5.m in Sources */, + 8377C66327B8CF6300E8BC0F /* SpectrumView.m in Sources */, 1766C6950B911DF1004A7AE4 /* AudioScrobblerClient.m in Sources */, 1755E1F90BA0D2B600CA3560 /* PlaylistLoader.m in Sources */, 8E9A30160BA792DC0091081B /* NSFileHandle+CreateFile.m in Sources */, diff --git a/Visualization/SpectrumView.h b/Visualization/SpectrumView.h new file mode 100644 index 000000000..045a32762 --- /dev/null +++ b/Visualization/SpectrumView.h @@ -0,0 +1,22 @@ +// +// SpectrumView.h +// Cog +// +// Created by Christopher Snowhill on 2/12/22. +// + +#import + +#import "VisualizationController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SpectrumView : NSImageView { + VisualizationController *visController; + NSTimer *timer; + NSImage *theImage; + BOOL stopped; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Visualization/SpectrumView.m b/Visualization/SpectrumView.m new file mode 100644 index 000000000..afc6d9a96 --- /dev/null +++ b/Visualization/SpectrumView.m @@ -0,0 +1,137 @@ +// +// SpectrumView.m +// Cog +// +// Created by Christopher Snowhill on 2/12/22. +// + +#import "SpectrumView.h" + +extern NSString *CogPlaybackDidBeginNotficiation; +extern NSString *CogPlaybackDidPauseNotficiation; +extern NSString *CogPlaybackDidResumeNotficiation; +extern NSString *CogPlaybackDidStopNotficiation; + +@implementation SpectrumView + +- (void)awakeFromNib { + visController = [NSClassFromString(@"VisualizationController") sharedController]; + timer = nil; + theImage = [NSImage imageWithSize:NSMakeSize(64, 26) + flipped:NO + drawingHandler:^BOOL(NSRect dstRect) { + NSColor *backColor = [NSColor textBackgroundColor]; + [backColor drawSwatchInRect:dstRect]; + return YES; + }]; + + stopped = YES; + + [self setImage:theImage]; + [self setImageScaling:NSImageScaleAxesIndependently]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(colorsDidChange:) + name:NSSystemColorsDidChangeNotification + object: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]; +} + +- (void)repaint { + { + [theImage lockFocus]; + + NSColor *backColor = [NSColor textBackgroundColor]; + [backColor drawSwatchInRect:NSMakeRect(0, 0, 64, 26)]; + + NSBezierPath *bezierPath = [[NSBezierPath alloc] init]; + + float visAudio[512], visFFT[256]; + + if(!self->stopped) { + [self->visController copyVisPCM:&visAudio[0] visFFT:&visFFT[0]]; + } else { + memset(visFFT, 0, sizeof(visFFT)); + } + + for(int i = 0; i < 60; ++i) { + CGFloat y = MAX(MIN(visFFT[i], 0.25), 0.0) * 4.0 * 22.0 + 2.0; + [bezierPath moveToPoint:NSMakePoint(2 + i, 2)]; + [bezierPath lineToPoint:NSMakePoint(2 + i, y)]; + } + + NSColor *lineColor = [NSColor textColor]; + [lineColor setStroke]; + + [bezierPath stroke]; + + [theImage unlockFocus]; + } + + [self setNeedsDisplay]; +} + +- (void)startTimer { + [self stopTimer]; + timer = [NSTimer timerWithTimeInterval:0.02 + 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 { + [self repaint]; +} + +- (void)playbackDidBegin:(NSNotification *)notification { + stopped = NO; + [self startTimer]; +} + +- (void)playbackDidPause:(NSNotification *)notification { + stopped = NO; + [self stopTimer]; +} + +- (void)playbackDidResume:(NSNotification *)notification { + stopped = NO; + [self startTimer]; +} + +- (void)playbackDidStop:(NSNotification *)notification { + [self stopTimer]; + stopped = YES; + [self repaint]; +} + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; +} + +@end