Visualization: Increased fft size, imported code
Boy, I just be outright stealing code now. But it looks nicer now. Fixes #234 Signed-off-by: Christopher Snowhill <kode54@gmail.com>CQTexperiment
parent
cad09b8912
commit
344ceb173d
|
@ -163,6 +163,7 @@ static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioAct
|
|||
vDSP_vclr(visAudio + visTabulated, 1, 512 - visTabulated);
|
||||
}
|
||||
|
||||
[_self->visController postSampleRate:_self->deviceFormat.mSampleRate];
|
||||
[_self->visController postVisPCM:visAudio];
|
||||
|
||||
return 0;
|
||||
|
|
|
@ -10,12 +10,15 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface VisualizationController : NSObject {
|
||||
float visAudio[512];
|
||||
double sampleRate;
|
||||
float visAudio[8192];
|
||||
}
|
||||
|
||||
+ (VisualizationController *)sharedController;
|
||||
|
||||
- (void)postSampleRate:(double)sampleRate;
|
||||
- (void)postVisPCM:(const float *)inPCM;
|
||||
- (double)readSampleRate;
|
||||
- (void)copyVisPCM:(float *)outPCM visFFT:(float *)outFFT;
|
||||
|
||||
@end
|
||||
|
|
|
@ -26,7 +26,7 @@ static VisualizationController *_sharedController = nil;
|
|||
- (id)init {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
vDSP_vclr(visAudio, 1, 512);
|
||||
vDSP_vclr(visAudio, 1, 8192);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -35,16 +35,29 @@ static VisualizationController *_sharedController = nil;
|
|||
fft_free();
|
||||
}
|
||||
|
||||
- (void)postSampleRate:(double)sampleRate {
|
||||
@synchronized(self) {
|
||||
self->sampleRate = sampleRate;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)postVisPCM:(const float *)inPCM {
|
||||
@synchronized(self) {
|
||||
cblas_scopy(512, inPCM, 1, visAudio, 1);
|
||||
cblas_scopy(8192 - 512, visAudio + 512, 1, visAudio, 1);
|
||||
cblas_scopy(512, inPCM, 1, visAudio + 8192 - 512, 1);
|
||||
}
|
||||
}
|
||||
|
||||
- (double)readSampleRate {
|
||||
@synchronized(self) {
|
||||
return sampleRate;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)copyVisPCM:(float *)outPCM visFFT:(float *)outFFT {
|
||||
@synchronized(self) {
|
||||
cblas_scopy(512, visAudio, 1, outPCM, 1);
|
||||
fft_calculate(visAudio, outFFT, 256);
|
||||
cblas_scopy(8192, visAudio, 1, outPCM, 1);
|
||||
fft_calculate(visAudio, outFFT, 4096);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -876,7 +876,7 @@
|
|||
</connections>
|
||||
</button>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="80D8CDBD-A6E9-47A6-B9C0-213C450E45FA" label="Spectrum" paletteLabel="Spectrum" tag="-1" id="NtB-XF-g07" customClass="SpectrumItem">
|
||||
<toolbarItem implicitItemIdentifier="80D8CDBD-A6E9-47A6-B9C0-213C450E45FA" label="Spectrum" paletteLabel="Spectrum" tag="-1" bordered="YES" id="NtB-XF-g07" customClass="SpectrumItem">
|
||||
<nil key="toolTip"/>
|
||||
<size key="minSize" width="64" height="26"/>
|
||||
<size key="maxSize" width="64" height="26"/>
|
||||
|
@ -1132,7 +1132,7 @@
|
|||
</connections>
|
||||
</button>
|
||||
</toolbarItem>
|
||||
<toolbarItem implicitItemIdentifier="09140C29-FF34-4BF2-92C8-54C7D9AA4A27" label="Spectrum" paletteLabel="Spectrum" tag="-1" id="sf3-l1-fJw" customClass="SpectrumItem">
|
||||
<toolbarItem implicitItemIdentifier="09140C29-FF34-4BF2-92C8-54C7D9AA4A27" label="Spectrum" paletteLabel="Spectrum" tag="-1" bordered="YES" id="sf3-l1-fJw" customClass="SpectrumItem">
|
||||
<nil key="toolTip"/>
|
||||
<size key="minSize" width="64" height="26"/>
|
||||
<size key="maxSize" width="64" height="26"/>
|
||||
|
|
|
@ -99,6 +99,7 @@
|
|||
830596EE277F05EE00EBFAAE /* Vorbis.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 830596E7277F05E200EBFAAE /* Vorbis.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
830C37A127B95E3000E02BB0 /* Equalizer.xib in Resources */ = {isa = PBXBuildFile; fileRef = 830C379F27B95E3000E02BB0 /* Equalizer.xib */; };
|
||||
830C37A527B95EB300E02BB0 /* EqualizerWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C37A427B95EB300E02BB0 /* EqualizerWindowController.m */; };
|
||||
830C37FC27B9956C00E02BB0 /* analyzer.c in Sources */ = {isa = PBXBuildFile; fileRef = 830C37F227B9956C00E02BB0 /* analyzer.c */; };
|
||||
8314A46F27A28C29000EBE7E /* equalizerTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 8314A46527A28C28000EBE7E /* equalizerTemplate.pdf */; };
|
||||
832923AF279FAC400048201E /* Cog.q1.json in Resources */ = {isa = PBXBuildFile; fileRef = 832923AE279FAC400048201E /* Cog.q1.json */; };
|
||||
83293070277886250010C07E /* OpenMPTOld.bundle in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8329306D277885790010C07E /* OpenMPTOld.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -916,6 +917,8 @@
|
|||
830C37A027B95E3000E02BB0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/Equalizer.xib; sourceTree = "<group>"; };
|
||||
830C37A327B95EB300E02BB0 /* EqualizerWindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = EqualizerWindowController.h; path = Equalizer/EqualizerWindowController.h; sourceTree = "<group>"; };
|
||||
830C37A427B95EB300E02BB0 /* EqualizerWindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = EqualizerWindowController.m; path = Equalizer/EqualizerWindowController.m; sourceTree = "<group>"; };
|
||||
830C37F127B9956C00E02BB0 /* analyzer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = analyzer.h; sourceTree = "<group>"; };
|
||||
830C37F227B9956C00E02BB0 /* analyzer.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = analyzer.c; sourceTree = "<group>"; };
|
||||
8314A46527A28C28000EBE7E /* equalizerTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = equalizerTemplate.pdf; path = Images/equalizerTemplate.pdf; sourceTree = "<group>"; };
|
||||
8314D63B1A354DFE00EEE8E6 /* sidplay.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = sidplay.xcodeproj; path = Plugins/sidplay/sidplay.xcodeproj; sourceTree = "<group>"; };
|
||||
832923AE279FAC400048201E /* Cog.q1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Cog.q1.json; sourceTree = "<group>"; };
|
||||
|
@ -1631,6 +1634,24 @@
|
|||
name = Equalizer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
830C37EF27B9956C00E02BB0 /* ThirdParty */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
830C37F027B9956C00E02BB0 /* deadbeef */,
|
||||
);
|
||||
name = ThirdParty;
|
||||
path = Visualization/ThirdParty;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
830C37F027B9956C00E02BB0 /* deadbeef */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
830C37F127B9956C00E02BB0 /* analyzer.h */,
|
||||
830C37F227B9956C00E02BB0 /* analyzer.c */,
|
||||
);
|
||||
path = deadbeef;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8314D63C1A354DFE00EEE8E6 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1708,6 +1729,7 @@
|
|||
8377C66027B8CF2300E8BC0F /* Visualization */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
830C37EF27B9956C00E02BB0 /* ThirdParty */,
|
||||
8377C66427B8CF7A00E8BC0F /* VisualizationController.h */,
|
||||
8377C66227B8CF6300E8BC0F /* SpectrumView.h */,
|
||||
8377C66127B8CF6300E8BC0F /* SpectrumView.m */,
|
||||
|
@ -2501,6 +2523,7 @@
|
|||
56DB084C0D6717DC00453B6A /* NSNumber+CogSort.m in Sources */,
|
||||
56DB08550D67185300453B6A /* NSArray+CogSort.m in Sources */,
|
||||
170B55940D6E5E7B006B9E92 /* StatusImageTransformer.m in Sources */,
|
||||
830C37FC27B9956C00E02BB0 /* analyzer.c in Sources */,
|
||||
17249F0F0D82E17700F33392 /* ToggleQueueTitleTransformer.m in Sources */,
|
||||
179D031E0E0CB2500064A77A /* ContainedNode.m in Sources */,
|
||||
179D031F0E0CB2500064A77A /* ContainerNode.m in Sources */,
|
||||
|
|
|
@ -11,19 +11,7 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SpectrumView : NSView {
|
||||
VisualizationController *visController;
|
||||
NSTimer *timer;
|
||||
BOOL paused;
|
||||
BOOL stopped;
|
||||
BOOL isListening;
|
||||
|
||||
float FFTMax[256];
|
||||
|
||||
NSColor *baseColor;
|
||||
NSColor *peakColor;
|
||||
NSColor *backgroundColor;
|
||||
}
|
||||
@interface SpectrumView : NSView
|
||||
@property(nonatomic) BOOL isListening;
|
||||
@end
|
||||
|
||||
|
|
|
@ -7,13 +7,31 @@
|
|||
|
||||
#import "SpectrumView.h"
|
||||
|
||||
#import <Accelerate/Accelerate.h>
|
||||
#import "analyzer.h"
|
||||
|
||||
#define LOWER_BOUND -80
|
||||
|
||||
extern NSString *CogPlaybackDidBeginNotficiation;
|
||||
extern NSString *CogPlaybackDidPauseNotficiation;
|
||||
extern NSString *CogPlaybackDidResumeNotficiation;
|
||||
extern NSString *CogPlaybackDidStopNotficiation;
|
||||
|
||||
@interface SpectrumView () {
|
||||
VisualizationController *visController;
|
||||
NSTimer *timer;
|
||||
BOOL paused;
|
||||
BOOL stopped;
|
||||
BOOL isListening;
|
||||
|
||||
NSColor *baseColor;
|
||||
NSColor *peakColor;
|
||||
NSColor *backgroundColor;
|
||||
NSColor *borderColor;
|
||||
ddb_analyzer_t _analyzer;
|
||||
ddb_analyzer_draw_data_t _draw_data;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation SpectrumView
|
||||
|
||||
@synthesize isListening;
|
||||
|
@ -45,7 +63,14 @@ extern NSString *CogPlaybackDidStopNotficiation;
|
|||
|
||||
[self colorsDidChange:nil];
|
||||
|
||||
vDSP_vclr(&FFTMax[0], 1, 256);
|
||||
ddb_analyzer_init(&_analyzer);
|
||||
_analyzer.db_lower_bound = LOWER_BOUND;
|
||||
_analyzer.peak_hold = 10;
|
||||
_analyzer.view_width = 1000;
|
||||
_analyzer.fractional_bars = 1;
|
||||
_analyzer.octave_bars_step = 2;
|
||||
_analyzer.max_of_stereo_data = 1;
|
||||
_analyzer.mode = DDB_ANALYZER_MODE_OCTAVE_NOTE_BANDS;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(colorsDidChange:)
|
||||
|
@ -70,6 +95,9 @@ extern NSString *CogPlaybackDidStopNotficiation;
|
|||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ddb_analyzer_dealloc(&_analyzer);
|
||||
ddb_analyzer_draw_data_dealloc(&_draw_data);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||
name:NSSystemColorsDidChangeNotification
|
||||
object:nil];
|
||||
|
@ -112,6 +140,8 @@ extern NSString *CogPlaybackDidStopNotficiation;
|
|||
|
||||
- (void)colorsDidChange:(NSNotification *)notification {
|
||||
backgroundColor = [NSColor textBackgroundColor];
|
||||
backgroundColor = [backgroundColor colorWithAlphaComponent:0.0];
|
||||
borderColor = [NSColor systemGrayColor];
|
||||
|
||||
if(@available(macOS 10.14, *)) {
|
||||
baseColor = [NSColor textColor];
|
||||
|
@ -147,10 +177,53 @@ extern NSString *CogPlaybackDidStopNotficiation;
|
|||
stopped = YES;
|
||||
paused = NO;
|
||||
[self updateVisListening];
|
||||
vDSP_vclr(&FFTMax[0], 1, 256);
|
||||
[self repaint];
|
||||
}
|
||||
|
||||
- (void)drawAnalyzerDescreteFrequencies {
|
||||
CGContextRef context = NSGraphicsContext.currentContext.CGContext;
|
||||
ddb_analyzer_draw_bar_t *bar = _draw_data.bars;
|
||||
for(int i = 0; i < _draw_data.bar_count; i++, bar++) {
|
||||
CGContextMoveToPoint(context, bar->xpos, 0);
|
||||
CGContextAddLineToPoint(context, bar->xpos, bar->bar_height);
|
||||
}
|
||||
CGContextSetStrokeColorWithColor(context, baseColor.CGColor);
|
||||
CGContextStrokePath(context);
|
||||
|
||||
bar = _draw_data.bars;
|
||||
for(int i = 0; i < _draw_data.bar_count; i++, bar++) {
|
||||
CGContextMoveToPoint(context, bar->xpos - 0.5, bar->peak_ypos);
|
||||
CGContextAddLineToPoint(context, bar->xpos + 0.5, bar->peak_ypos);
|
||||
}
|
||||
CGContextSetStrokeColorWithColor(context, peakColor.CGColor);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
- (void)drawAnalyzerOctaveBands {
|
||||
CGContextRef context = NSGraphicsContext.currentContext.CGContext;
|
||||
ddb_analyzer_draw_bar_t *bar = _draw_data.bars;
|
||||
for(int i = 0; i < _draw_data.bar_count; i++, bar++) {
|
||||
CGContextAddRect(context, CGRectMake(bar->xpos, 0, _draw_data.bar_width, bar->bar_height));
|
||||
}
|
||||
CGContextSetFillColorWithColor(context, baseColor.CGColor);
|
||||
CGContextFillPath(context);
|
||||
|
||||
bar = _draw_data.bars;
|
||||
for(int i = 0; i < _draw_data.bar_count; i++, bar++) {
|
||||
CGContextAddRect(context, CGRectMake(bar->xpos, bar->peak_ypos, _draw_data.bar_width, 1.0));
|
||||
}
|
||||
CGContextSetFillColorWithColor(context, peakColor.CGColor);
|
||||
CGContextFillPath(context);
|
||||
}
|
||||
|
||||
- (void)drawAnalyzer {
|
||||
if(_analyzer.mode == DDB_ANALYZER_MODE_FREQUENCIES) {
|
||||
[self drawAnalyzerDescreteFrequencies];
|
||||
} else {
|
||||
[self drawAnalyzerOctaveBands];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
[super drawRect:dirtyRect];
|
||||
|
||||
|
@ -159,35 +232,26 @@ extern NSString *CogPlaybackDidStopNotficiation;
|
|||
[backgroundColor setFill];
|
||||
NSRectFill(dirtyRect);
|
||||
|
||||
float visAudio[512], visFFT[256];
|
||||
|
||||
if(!self->stopped) {
|
||||
[self->visController copyVisPCM:&visAudio[0] visFFT:&visFFT[0]];
|
||||
} else {
|
||||
memset(visFFT, 0, sizeof(visFFT));
|
||||
}
|
||||
|
||||
float scale = 0.95;
|
||||
vDSP_vsmul(&FFTMax[0], 1, &scale, &FFTMax[0], 1, 256);
|
||||
vDSP_vmax(&visFFT[0], 1, &FFTMax[0], 1, &FFTMax[0], 1, 256);
|
||||
|
||||
CGContextRef context = NSGraphicsContext.currentContext.CGContext;
|
||||
|
||||
for(int i = 0; i < 60; ++i) {
|
||||
CGFloat y = MAX(MIN(visFFT[i], 0.25), 0.0) * 4.0 * 22.0;
|
||||
CGContextMoveToPoint(context, 2.0 + i, 2.0);
|
||||
CGContextAddLineToPoint(context, 2.0 + i, 2.0 + y);
|
||||
}
|
||||
CGContextSetStrokeColorWithColor(context, baseColor.CGColor);
|
||||
CGContextMoveToPoint(context, 0.0, 0.0);
|
||||
CGContextAddLineToPoint(context, 63.0, 0.0);
|
||||
CGContextAddLineToPoint(context, 63.0, 25.0);
|
||||
CGContextAddLineToPoint(context, 0.0, 25.0);
|
||||
CGContextAddLineToPoint(context, 0.0, 0.0);
|
||||
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
|
||||
CGContextStrokePath(context);
|
||||
|
||||
for(int i = 0; i < 60; ++i) {
|
||||
CGFloat y = MAX(MIN(FFTMax[i], 0.25), 0.0) * 4.0 * 22.0;
|
||||
CGContextMoveToPoint(context, 2.0 + i, 1.5 + y);
|
||||
CGContextAddLineToPoint(context, 2.0 + i, 2.5 + y);
|
||||
}
|
||||
CGContextSetStrokeColorWithColor(context, peakColor.CGColor);
|
||||
CGContextStrokePath(context);
|
||||
if(stopped) return;
|
||||
|
||||
float visAudio[8192], visFFT[4096];
|
||||
|
||||
[self->visController copyVisPCM:&visAudio[0] visFFT:&visFFT[0]];
|
||||
|
||||
ddb_analyzer_process(&_analyzer, [self->visController readSampleRate], 1, visFFT, 4096);
|
||||
ddb_analyzer_tick(&_analyzer);
|
||||
ddb_analyzer_get_draw_data(&_analyzer, self.bounds.size.width, self.bounds.size.height, &_draw_data);
|
||||
|
||||
[self drawAnalyzer];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -0,0 +1,416 @@
|
|||
/*
|
||||
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 "analyzer.h"
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define OCTAVES 11
|
||||
#define STEPS 24
|
||||
#define ROOT24 1.0293022366 // pow(2, 1.0 / STEPS)
|
||||
#define C0 16.3515978313 // 440 * pow(ROOT24, -114);
|
||||
|
||||
#pragma mark - Forward declarations
|
||||
|
||||
static float
|
||||
_get_bar_height(ddb_analyzer_t *analyzer, float normalized_height, int view_height);
|
||||
|
||||
static void
|
||||
_generate_frequency_labels(ddb_analyzer_t *analyzer);
|
||||
|
||||
static void
|
||||
_generate_frequency_bars(ddb_analyzer_t *analyzer);
|
||||
|
||||
static void
|
||||
_generate_octave_note_bars(ddb_analyzer_t *analyzer);
|
||||
|
||||
static void
|
||||
_tempered_scale_bands_precalc(ddb_analyzer_t *analyzer);
|
||||
|
||||
static float _bin_for_freq_floor(ddb_analyzer_t *analyzer, float freq);
|
||||
|
||||
static float _bin_for_freq_round(ddb_analyzer_t *analyzer, float freq);
|
||||
|
||||
static float _freq_for_bin(ddb_analyzer_t *analyzer, int bin);
|
||||
|
||||
static float
|
||||
_interpolate_bin_with_ratio(float *fft_data, int bin, float ratio);
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
ddb_analyzer_t *
|
||||
ddb_analyzer_alloc(void) {
|
||||
return calloc(1, sizeof(ddb_analyzer_t));
|
||||
}
|
||||
|
||||
ddb_analyzer_t *
|
||||
ddb_analyzer_init(ddb_analyzer_t *analyzer) {
|
||||
analyzer->mode = DDB_ANALYZER_MODE_FREQUENCIES;
|
||||
analyzer->min_freq = 50;
|
||||
analyzer->max_freq = 22000;
|
||||
analyzer->view_width = 1000;
|
||||
analyzer->peak_hold = 10;
|
||||
analyzer->peak_speed_scale = 1000.f;
|
||||
analyzer->db_lower_bound = -80;
|
||||
analyzer->octave_bars_step = 1;
|
||||
analyzer->bar_gap_denominator = 3;
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
void ddb_analyzer_dealloc(ddb_analyzer_t *analyzer) {
|
||||
free(analyzer->tempered_scale_bands);
|
||||
free(analyzer->fft_data);
|
||||
memset(analyzer, 0, sizeof(ddb_analyzer_t));
|
||||
}
|
||||
|
||||
void ddb_analyzer_free(ddb_analyzer_t *analyzer) {
|
||||
free(analyzer);
|
||||
}
|
||||
|
||||
void ddb_analyzer_process(ddb_analyzer_t *analyzer, int samplerate, int channels, const float *fft_data, int fft_size) {
|
||||
int need_regenerate = 0;
|
||||
|
||||
if(channels > 2) {
|
||||
channels = 2;
|
||||
}
|
||||
|
||||
if(!analyzer->max_of_stereo_data) {
|
||||
channels = 1;
|
||||
}
|
||||
|
||||
if(analyzer->mode_did_change || channels != analyzer->channels || fft_size != analyzer->fft_size || samplerate != analyzer->samplerate) {
|
||||
analyzer->channels = channels;
|
||||
analyzer->fft_size = fft_size;
|
||||
analyzer->samplerate = samplerate;
|
||||
free(analyzer->fft_data);
|
||||
analyzer->fft_data = malloc(fft_size * channels * sizeof(float));
|
||||
need_regenerate = 1;
|
||||
analyzer->mode_did_change = 0;
|
||||
}
|
||||
|
||||
memcpy(analyzer->fft_data, fft_data, fft_size * channels * sizeof(float));
|
||||
|
||||
if(need_regenerate) {
|
||||
switch(analyzer->mode) {
|
||||
case DDB_ANALYZER_MODE_FREQUENCIES:
|
||||
_generate_frequency_bars(analyzer);
|
||||
break;
|
||||
case DDB_ANALYZER_MODE_OCTAVE_NOTE_BANDS:
|
||||
_generate_octave_note_bars(analyzer);
|
||||
break;
|
||||
}
|
||||
|
||||
_generate_frequency_labels(analyzer);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update bars and peaks for the next frame
|
||||
void ddb_analyzer_tick(ddb_analyzer_t *analyzer) {
|
||||
if(analyzer->mode_did_change) {
|
||||
return; // avoid ticks until the next data update
|
||||
}
|
||||
// frequency lines
|
||||
for(int ch = 0; ch < analyzer->channels; ch++) {
|
||||
float *fft_data = analyzer->fft_data + ch * analyzer->fft_size;
|
||||
ddb_analyzer_bar_t *bar = analyzer->bars;
|
||||
for(int i = 0; i < analyzer->bar_count; i++, bar++) {
|
||||
float norm_h = _interpolate_bin_with_ratio(fft_data, bar->bin, bar->ratio);
|
||||
|
||||
// if the bar spans more than one bin, find the max value
|
||||
for(int b = bar->bin + 1; b <= bar->last_bin; b++) {
|
||||
float val = analyzer->fft_data[b];
|
||||
if(val > norm_h) {
|
||||
norm_h = val;
|
||||
}
|
||||
}
|
||||
|
||||
float bound = -analyzer->db_lower_bound;
|
||||
float height = (20 * log10(norm_h) + bound) / bound;
|
||||
|
||||
if(ch == 0) {
|
||||
bar->height = height;
|
||||
} else if(height > bar->height) {
|
||||
bar->height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// peaks
|
||||
ddb_analyzer_bar_t *bar = analyzer->bars;
|
||||
for(int i = 0; i < analyzer->bar_count; i++, bar++) {
|
||||
if(bar->peak < bar->height) {
|
||||
bar->peak = bar->height;
|
||||
bar->peak_speed = analyzer->peak_hold;
|
||||
}
|
||||
|
||||
if(bar->peak_speed-- < 0) {
|
||||
bar->peak += bar->peak_speed / analyzer->peak_speed_scale;
|
||||
if(bar->peak < bar->height) {
|
||||
bar->peak = bar->height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ddb_analyzer_get_draw_data(ddb_analyzer_t *analyzer, int view_width, int view_height, ddb_analyzer_draw_data_t *draw_data) {
|
||||
if(draw_data->bar_count != analyzer->bar_count) {
|
||||
free(draw_data->bars);
|
||||
draw_data->bars = calloc(analyzer->bar_count, sizeof(ddb_analyzer_draw_bar_t));
|
||||
draw_data->bar_count = analyzer->bar_count;
|
||||
}
|
||||
|
||||
if(analyzer->mode == DDB_ANALYZER_MODE_FREQUENCIES) {
|
||||
draw_data->bar_width = 1;
|
||||
} else if(analyzer->mode == DDB_ANALYZER_MODE_OCTAVE_NOTE_BANDS) {
|
||||
if(analyzer->fractional_bars) {
|
||||
float width = (float)view_width / analyzer->bar_count;
|
||||
float gap = analyzer->bar_gap_denominator > 0 ? width / analyzer->bar_gap_denominator : 0;
|
||||
draw_data->bar_width = width - gap;
|
||||
} else {
|
||||
int width = view_width / analyzer->bar_count;
|
||||
int gap = analyzer->bar_gap_denominator > 0 ? width / analyzer->bar_gap_denominator : 0;
|
||||
if(gap < 1) {
|
||||
gap = 1;
|
||||
}
|
||||
if(width <= 1) {
|
||||
width = 1;
|
||||
gap = 0;
|
||||
}
|
||||
draw_data->bar_width = width - gap;
|
||||
}
|
||||
}
|
||||
|
||||
ddb_analyzer_bar_t *bar = analyzer->bars;
|
||||
ddb_analyzer_draw_bar_t *draw_bar = draw_data->bars;
|
||||
for(int i = 0; i < analyzer->bar_count; i++, bar++, draw_bar++) {
|
||||
float height = bar->height;
|
||||
|
||||
draw_bar->bar_height = _get_bar_height(analyzer, height, view_height);
|
||||
draw_bar->xpos = bar->xpos * view_width;
|
||||
draw_bar->peak_ypos = _get_bar_height(analyzer, bar->peak, view_height);
|
||||
}
|
||||
|
||||
memcpy(draw_data->label_freq_texts, analyzer->label_freq_texts, sizeof(analyzer->label_freq_texts));
|
||||
for(int i = 0; i < analyzer->label_freq_count; i++) {
|
||||
draw_data->label_freq_positions[i] = analyzer->label_freq_positions[i] * view_width;
|
||||
}
|
||||
draw_data->label_freq_count = analyzer->label_freq_count;
|
||||
}
|
||||
|
||||
void ddb_analyzer_draw_data_dealloc(ddb_analyzer_draw_data_t *draw_data) {
|
||||
free(draw_data->bars);
|
||||
memset(draw_data, 0, sizeof(ddb_analyzer_draw_data_t));
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
static float
|
||||
_get_bar_height(ddb_analyzer_t *analyzer, float normalized_height, int view_height) {
|
||||
float height = normalized_height;
|
||||
if(height < 0) {
|
||||
height = 0;
|
||||
} else if(height > 1) {
|
||||
height = 1;
|
||||
}
|
||||
height *= view_height;
|
||||
return height;
|
||||
}
|
||||
|
||||
static void
|
||||
_generate_frequency_labels(ddb_analyzer_t *analyzer) {
|
||||
float min_freq_log = log10(analyzer->min_freq);
|
||||
float max_freq_log = log10(analyzer->max_freq);
|
||||
float view_width = analyzer->view_width;
|
||||
float width_log = view_width / (max_freq_log - min_freq_log);
|
||||
|
||||
// calculate the distance between any 2 neighbour labels
|
||||
float freq = 64000;
|
||||
float freq2 = 32000;
|
||||
float pos = width_log * (log10(freq) - min_freq_log) / view_width;
|
||||
float pos2 = width_log * (log10(freq2) - min_freq_log) / view_width;
|
||||
float dist = pos - pos2;
|
||||
|
||||
// generate position and text for each label
|
||||
int index = 0;
|
||||
while(freq > 30 && index < DDB_ANALYZER_MAX_LABEL_FREQS) {
|
||||
analyzer->label_freq_positions[index] = pos;
|
||||
|
||||
if(freq < 1000) {
|
||||
snprintf(analyzer->label_freq_texts[index], sizeof(analyzer->label_freq_texts[index]), "%d", (int)round(freq));
|
||||
} else {
|
||||
snprintf(analyzer->label_freq_texts[index], sizeof(analyzer->label_freq_texts[index]), "%dk", ((int)freq) / 1000);
|
||||
}
|
||||
|
||||
pos -= dist;
|
||||
freq /= 2;
|
||||
index += 1;
|
||||
}
|
||||
analyzer->label_freq_count = index;
|
||||
}
|
||||
|
||||
static void
|
||||
_generate_frequency_bars(ddb_analyzer_t *analyzer) {
|
||||
float min_freq_log = log10(analyzer->min_freq);
|
||||
float max_freq_log = log10(analyzer->max_freq);
|
||||
float view_width = analyzer->view_width;
|
||||
float width_log = view_width / (max_freq_log - min_freq_log);
|
||||
|
||||
float minIndex = _bin_for_freq_floor(analyzer, analyzer->min_freq);
|
||||
float maxIndex = _bin_for_freq_round(analyzer, analyzer->max_freq);
|
||||
|
||||
int prev = -1;
|
||||
|
||||
analyzer->bar_count = 0;
|
||||
|
||||
if(analyzer->bar_count_max != analyzer->view_width) {
|
||||
free(analyzer->bars);
|
||||
analyzer->bars = calloc(analyzer->view_width, sizeof(ddb_analyzer_bar_t));
|
||||
analyzer->bar_count_max = analyzer->view_width;
|
||||
}
|
||||
|
||||
for(int i = minIndex; i <= maxIndex; i++) {
|
||||
float freq = _freq_for_bin(analyzer, i);
|
||||
|
||||
// FIXME: only int position!
|
||||
int pos = width_log * (log10(freq) - min_freq_log);
|
||||
|
||||
if(pos > prev && pos >= 0) {
|
||||
// start accumulating frequencies for the new band
|
||||
ddb_analyzer_bar_t *bar = analyzer->bars + analyzer->bar_count;
|
||||
|
||||
bar->xpos = pos / view_width; // normalized position
|
||||
bar->bin = i;
|
||||
bar->ratio = 0;
|
||||
analyzer->bar_count += 1;
|
||||
|
||||
prev = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_generate_octave_note_bars(ddb_analyzer_t *analyzer) {
|
||||
analyzer->bar_count = 0;
|
||||
|
||||
_tempered_scale_bands_precalc(analyzer);
|
||||
|
||||
if(analyzer->bar_count_max != OCTAVES * STEPS) {
|
||||
free(analyzer->bars);
|
||||
analyzer->bars = calloc(OCTAVES * STEPS, sizeof(ddb_analyzer_bar_t));
|
||||
analyzer->bar_count_max = OCTAVES * STEPS;
|
||||
}
|
||||
|
||||
int minBand = -1;
|
||||
int maxBand = -1;
|
||||
|
||||
ddb_analyzer_bar_t *prev_bar = NULL;
|
||||
for(int i = 0; i < OCTAVES * STEPS; i += analyzer->octave_bars_step) {
|
||||
ddb_analyzer_band_t *band = &analyzer->tempered_scale_bands[i];
|
||||
|
||||
if(band->freq < analyzer->min_freq || band->freq > analyzer->max_freq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(minBand == -1) {
|
||||
minBand = i;
|
||||
}
|
||||
|
||||
maxBand = i;
|
||||
|
||||
ddb_analyzer_bar_t *bar = analyzer->bars + analyzer->bar_count;
|
||||
|
||||
int bin = _bin_for_freq_floor(analyzer, band->freq);
|
||||
|
||||
bar->bin = bin;
|
||||
bar->last_bin = 0;
|
||||
bar->ratio = 0;
|
||||
|
||||
// interpolation ratio of next bin of previous bar to the first bin of this bar
|
||||
if(prev_bar && bin - 1 > prev_bar->bin) {
|
||||
prev_bar->last_bin = bin - 1;
|
||||
}
|
||||
|
||||
analyzer->bar_count += 1;
|
||||
|
||||
// get interpolation ratio to the next bin
|
||||
int bin2 = bin + 1;
|
||||
if(bin2 < analyzer->fft_size) {
|
||||
float p = log10(band->freq);
|
||||
float p1 = log10(_freq_for_bin(analyzer, bin));
|
||||
float p2 = log10(_freq_for_bin(analyzer, bin2));
|
||||
float d = p2 - p1;
|
||||
bar->ratio = (p - p1) / d;
|
||||
}
|
||||
|
||||
prev_bar = bar;
|
||||
}
|
||||
|
||||
for(int i = 0; i < analyzer->bar_count; i++) {
|
||||
analyzer->bars[i].xpos = (float)i / analyzer->bar_count;
|
||||
}
|
||||
}
|
||||
|
||||
static float _bin_for_freq_floor(ddb_analyzer_t *analyzer, float freq) {
|
||||
float max = analyzer->fft_size - 1;
|
||||
float bin = floor(freq * analyzer->fft_size / analyzer->samplerate);
|
||||
return bin < max ? bin : max;
|
||||
}
|
||||
|
||||
static float _bin_for_freq_round(ddb_analyzer_t *analyzer, float freq) {
|
||||
float max = analyzer->fft_size - 1;
|
||||
float bin = round(freq * analyzer->fft_size / analyzer->samplerate);
|
||||
return bin < max ? bin : max;
|
||||
}
|
||||
|
||||
static float _freq_for_bin(ddb_analyzer_t *analyzer, int bin) {
|
||||
return (int64_t)bin * analyzer->samplerate / analyzer->fft_size;
|
||||
}
|
||||
|
||||
// Precalculate data for tempered scale
|
||||
static void
|
||||
_tempered_scale_bands_precalc(ddb_analyzer_t *analyzer) {
|
||||
if(analyzer->tempered_scale_bands != NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
analyzer->tempered_scale_bands = calloc(OCTAVES * STEPS, sizeof(ddb_analyzer_band_t));
|
||||
|
||||
for(int i = 0; i < OCTAVES * STEPS; i++) {
|
||||
float f = C0 * pow(ROOT24, i);
|
||||
float bin = _bin_for_freq_floor(analyzer, f);
|
||||
float binf = _freq_for_bin(analyzer, bin);
|
||||
float fn = _freq_for_bin(analyzer, bin + 1);
|
||||
float ratio = (f - binf) / (fn - binf);
|
||||
|
||||
analyzer->tempered_scale_bands[i].bin = bin;
|
||||
analyzer->tempered_scale_bands[i].freq = f;
|
||||
analyzer->tempered_scale_bands[i].ratio = ratio;
|
||||
}
|
||||
}
|
||||
|
||||
static float
|
||||
_interpolate_bin_with_ratio(float *fft_data, int bin, float ratio) {
|
||||
return fft_data[bin] + (fft_data[bin + 1] - fft_data[bin]) * ratio;
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
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 analyzer_h
|
||||
#define analyzer_h
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define DDB_ANALYZER_MAX_LABEL_FREQS 20
|
||||
|
||||
typedef struct {
|
||||
float freq;
|
||||
float ratio;
|
||||
int bin;
|
||||
} ddb_analyzer_band_t;
|
||||
|
||||
typedef struct {
|
||||
// interpolation data
|
||||
int bin;
|
||||
int last_bin;
|
||||
float ratio;
|
||||
|
||||
// normalized position
|
||||
float xpos;
|
||||
float height;
|
||||
float peak;
|
||||
float peak_speed;
|
||||
} ddb_analyzer_bar_t;
|
||||
|
||||
typedef struct {
|
||||
float xpos;
|
||||
float peak_ypos;
|
||||
float bar_height;
|
||||
} ddb_analyzer_draw_bar_t;
|
||||
|
||||
typedef struct {
|
||||
int bar_count;
|
||||
ddb_analyzer_draw_bar_t *bars;
|
||||
float bar_width;
|
||||
|
||||
// freq label drawing positions in view space
|
||||
float label_freq_positions[DDB_ANALYZER_MAX_LABEL_FREQS];
|
||||
char label_freq_texts[DDB_ANALYZER_MAX_LABEL_FREQS][4];
|
||||
int label_freq_count;
|
||||
} ddb_analyzer_draw_data_t;
|
||||
|
||||
typedef enum {
|
||||
DDB_ANALYZER_MODE_FREQUENCIES,
|
||||
DDB_ANALYZER_MODE_OCTAVE_NOTE_BANDS,
|
||||
} ddb_analyzer_mode_t;
|
||||
|
||||
typedef struct ddb_analyzer_s {
|
||||
// Settings
|
||||
|
||||
float min_freq;
|
||||
float max_freq;
|
||||
|
||||
ddb_analyzer_mode_t mode;
|
||||
|
||||
/// Set to 1 after changing the @c mode or @c octave_bars_step at runtime, to refresh the internal state
|
||||
int mode_did_change;
|
||||
|
||||
/// Generate fractional bar positions and width. Default is 0.
|
||||
int fractional_bars;
|
||||
|
||||
/// Process 2 channels, if available, and use the max value. Default is 0.
|
||||
int max_of_stereo_data;
|
||||
|
||||
/// How to calculate the gap between bars. E.g. 10 means bar_width/10. Default is 3.
|
||||
/// If this value is 0, no gap will be created.
|
||||
/// If the gap is >=1, a gap of at least 1px will be created, unless the bar width itself is 1px.
|
||||
int bar_gap_denominator;
|
||||
|
||||
/// Can be either a real or virtual view width.
|
||||
/// The calculated values are normalized,
|
||||
/// and are going to be converted to the real view size at the draw time.
|
||||
int view_width;
|
||||
float peak_hold; // how many frames to hold the peak in place
|
||||
float peak_speed_scale;
|
||||
float db_upper_bound; // dB value corresponding to the top of the view
|
||||
float db_lower_bound; // dB value corresponding to the bottom of the view
|
||||
int octave_bars_step; // how many notes in one bar (default 1)
|
||||
|
||||
/// The bars get created / updated by calling @c ddb_analyzer_process.
|
||||
/// The same bars get updated on every call to @c ddb_analyzer_tick.
|
||||
ddb_analyzer_bar_t *bars;
|
||||
int bar_count;
|
||||
int bar_count_max;
|
||||
int samplerate;
|
||||
int channels;
|
||||
int fft_size;
|
||||
float *fft_data;
|
||||
|
||||
/// Calculated label texts and frequencies
|
||||
float label_freq_positions[DDB_ANALYZER_MAX_LABEL_FREQS];
|
||||
char label_freq_texts[DDB_ANALYZER_MAX_LABEL_FREQS][4];
|
||||
int label_freq_count;
|
||||
|
||||
/// Tempered scale data, precalculated from fft pins
|
||||
ddb_analyzer_band_t *tempered_scale_bands;
|
||||
|
||||
} ddb_analyzer_t;
|
||||
|
||||
ddb_analyzer_t *ddb_analyzer_alloc(void);
|
||||
|
||||
ddb_analyzer_t *ddb_analyzer_init(ddb_analyzer_t *analyzer);
|
||||
|
||||
void ddb_analyzer_dealloc(ddb_analyzer_t *analyzer);
|
||||
|
||||
void ddb_analyzer_free(ddb_analyzer_t *analyzer);
|
||||
|
||||
/// Process fft data into bars and peaks
|
||||
void ddb_analyzer_process(ddb_analyzer_t *analyzer, int samplerate, int channels, const float *fft_data, int fft_size);
|
||||
|
||||
/// Update bars and peaks for the next frame
|
||||
void ddb_analyzer_tick(ddb_analyzer_t *analyzer);
|
||||
|
||||
void ddb_analyzer_get_draw_data(ddb_analyzer_t *analyzer, int view_width, int view_height, ddb_analyzer_draw_data_t *draw_data);
|
||||
|
||||
void ddb_analyzer_draw_data_dealloc(ddb_analyzer_draw_data_t *draw_data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* analyzer_h */
|
Loading…
Reference in New Issue