Implement visualization support and a spectrum
Borrowing some DFT code from deadbeef, this implements a simple spectrum visualization into the main toolbar of the app. Signed-off-by: Christopher Snowhill <kode54@gmail.com>CQTexperiment
parent
1309672adc
commit
417687600b
|
@ -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 = "<group>"; };
|
||||
83725A8A27AA0DBF0003F694 /* libsoxr.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libsoxr.0.dylib; sourceTree = "<group>"; };
|
||||
8377C64B27B8C51500E8BC0F /* fft_accelerate.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = fft_accelerate.c; sourceTree = "<group>"; };
|
||||
8377C64D27B8C54400E8BC0F /* fft.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fft.h; sourceTree = "<group>"; };
|
||||
8377C65027B8CAD100E8BC0F /* VisualizationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VisualizationController.h; sourceTree = "<group>"; };
|
||||
8377C65127B8CAD100E8BC0F /* VisualizationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VisualizationController.m; sourceTree = "<group>"; };
|
||||
8384912618080FF100E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
|
||||
839366651815923C006DD712 /* CogPluginMulti.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CogPluginMulti.h; sourceTree = "<group>"; };
|
||||
839366661815923C006DD712 /* CogPluginMulti.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CogPluginMulti.m; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
};
|
||||
8377C64A27B8C51500E8BC0F /* deadbeef */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8377C64D27B8C54400E8BC0F /* fft.h */,
|
||||
8377C64B27B8C51500E8BC0F /* fft_accelerate.c */,
|
||||
);
|
||||
path = deadbeef;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8377C64F27B8CAAB00E8BC0F /* Visualization */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8377C65027B8CAD100E8BC0F /* VisualizationController.h */,
|
||||
8377C65127B8CAD100E8BC0F /* VisualizationController.m */,
|
||||
);
|
||||
path = Visualization;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 */,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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 <Accelerate/Accelerate.h>
|
||||
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// VisualizationController.h
|
||||
// CogAudio Framework
|
||||
//
|
||||
// Created by Christopher Snowhill on 2/12/22.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
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
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// VisualizationController.m
|
||||
// CogAudio Framework
|
||||
//
|
||||
// Created by Christopher Snowhill on 2/12/22.
|
||||
//
|
||||
|
||||
#import "VisualizationController.h"
|
||||
#import <Accelerate/Accelerate.h>
|
||||
|
||||
#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
|
|
@ -32,7 +32,7 @@
|
|||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="KWC-Ti-8KY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1000" height="378"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" alternatingRowBackgroundColors="YES" autosaveName="Playlist" rowHeight="18" headerView="1517" viewBased="YES" id="207" customClass="PlaylistView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1000" height="361"/>
|
||||
|
@ -130,11 +130,11 @@
|
|||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView id="ZCP-Dx-UBV">
|
||||
<rect key="frame" x="106" y="3" width="125.5" height="18"/>
|
||||
<rect key="frame" x="106" y="3" width="126" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="exY-Bg-Mjm">
|
||||
<rect key="frame" x="0.0" y="1" width="125.5" height="16"/>
|
||||
<rect key="frame" x="0.0" y="1" width="126" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="sdo-Sm-KPH">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
|
@ -876,6 +876,14 @@
|
|||
</connections>
|
||||
</button>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="5E0A643B-09FC-45CF-A562-C56F1E388E62" label="Spectrum" paletteLabel="Spectrum" sizingBehavior="auto" id="gui-fC-qTP">
|
||||
<nil key="toolTip"/>
|
||||
<imageView key="view" horizontalHuggingPriority="251" verticalHuggingPriority="251" id="Zaq-sS-aTV" customClass="SpectrumView">
|
||||
<rect key="frame" x="0.0" y="14" width="64" height="26"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="VDu-2p-2bJ"/>
|
||||
</imageView>
|
||||
</toolbarItem>
|
||||
</allowedToolbarItems>
|
||||
<defaultToolbarItems>
|
||||
<toolbarItem reference="ZH9-ZU-skw"/>
|
||||
|
@ -888,6 +896,7 @@
|
|||
<toolbarItem reference="1630"/>
|
||||
<toolbarItem reference="1629"/>
|
||||
<toolbarItem reference="1529"/>
|
||||
<toolbarItem reference="gui-fC-qTP"/>
|
||||
<toolbarItem reference="1568"/>
|
||||
<toolbarItem reference="1551"/>
|
||||
<toolbarItem reference="1610"/>
|
||||
|
@ -1122,6 +1131,14 @@
|
|||
</connections>
|
||||
</button>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="1A931C99-28FC-4209-BD5C-38E5CF96F7AF" label="Spectrum" paletteLabel="Spectrum" sizingBehavior="auto" id="K64-ro-2GI">
|
||||
<nil key="toolTip"/>
|
||||
<imageView key="view" horizontalHuggingPriority="251" verticalHuggingPriority="251" id="F4M-9A-fZv" customClass="SpectrumView">
|
||||
<rect key="frame" x="0.0" y="14" width="64" height="26"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="br1-X9-N6H"/>
|
||||
</imageView>
|
||||
</toolbarItem>
|
||||
</allowedToolbarItems>
|
||||
<defaultToolbarItems>
|
||||
<toolbarItem reference="qfu-F9-bOZ"/>
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
8377C66127B8CF6300E8BC0F /* SpectrumView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SpectrumView.m; path = Visualization/SpectrumView.m; sourceTree = "<group>"; };
|
||||
8377C66227B8CF6300E8BC0F /* SpectrumView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SpectrumView.h; path = Visualization/SpectrumView.h; sourceTree = "<group>"; };
|
||||
8377C66427B8CF7A00E8BC0F /* VisualizationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VisualizationController.h; path = Audio/Visualization/VisualizationController.h; sourceTree = "<group>"; };
|
||||
8384912518080F2D00E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Logging.h; sourceTree = "<group>"; };
|
||||
8384913D18083E4E00E7332D /* filetype.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = filetype.icns; sourceTree = "<group>"; };
|
||||
8384914318083EAB00E7332D /* infoTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = infoTemplate.pdf; path = Images/infoTemplate.pdf; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
};
|
||||
8377C66027B8CF2300E8BC0F /* Visualization */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8377C66427B8CF7A00E8BC0F /* VisualizationController.h */,
|
||||
8377C66227B8CF6300E8BC0F /* SpectrumView.h */,
|
||||
8377C66127B8CF6300E8BC0F /* SpectrumView.m */,
|
||||
);
|
||||
name = Visualization;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// SpectrumView.h
|
||||
// Cog
|
||||
//
|
||||
// Created by Christopher Snowhill on 2/12/22.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "VisualizationController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SpectrumView : NSImageView {
|
||||
VisualizationController *visController;
|
||||
NSTimer *timer;
|
||||
NSImage *theImage;
|
||||
BOOL stopped;
|
||||
}
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -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
|
Loading…
Reference in New Issue