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
Christopher Snowhill 2022-02-12 23:04:03 -08:00
parent 1309672adc
commit 417687600b
11 changed files with 471 additions and 4 deletions

View File

@ -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 */,

View File

@ -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;

View File

@ -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 {

40
Audio/ThirdParty/deadbeef/fft.h vendored Normal file
View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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"/>

View File

@ -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 */,

View File

@ -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

View File

@ -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