diff --git a/Audio/Output/OutputCoreAudio.m b/Audio/Output/OutputCoreAudio.m index 4db89c763..3e9819d4c 100644 --- a/Audio/Output/OutputCoreAudio.m +++ b/Audio/Output/OutputCoreAudio.m @@ -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; diff --git a/Audio/Visualization/VisualizationController.h b/Audio/Visualization/VisualizationController.h index d31a695b8..2feb6c0e8 100644 --- a/Audio/Visualization/VisualizationController.h +++ b/Audio/Visualization/VisualizationController.h @@ -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 diff --git a/Audio/Visualization/VisualizationController.m b/Audio/Visualization/VisualizationController.m index 2dca802e7..c6178a90e 100644 --- a/Audio/Visualization/VisualizationController.m +++ b/Audio/Visualization/VisualizationController.m @@ -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); } } diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index a24a33a01..90e5102ea 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -876,7 +876,7 @@ - + @@ -1132,7 +1132,7 @@ - + diff --git a/Cog.xcodeproj/project.pbxproj b/Cog.xcodeproj/project.pbxproj index 30dd28234..efba33bf1 100644 --- a/Cog.xcodeproj/project.pbxproj +++ b/Cog.xcodeproj/project.pbxproj @@ -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 = ""; }; 830C37A327B95EB300E02BB0 /* EqualizerWindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = EqualizerWindowController.h; path = Equalizer/EqualizerWindowController.h; sourceTree = ""; }; 830C37A427B95EB300E02BB0 /* EqualizerWindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = EqualizerWindowController.m; path = Equalizer/EqualizerWindowController.m; sourceTree = ""; }; + 830C37F127B9956C00E02BB0 /* analyzer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = analyzer.h; sourceTree = ""; }; + 830C37F227B9956C00E02BB0 /* analyzer.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = analyzer.c; sourceTree = ""; }; 8314A46527A28C28000EBE7E /* equalizerTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = equalizerTemplate.pdf; path = Images/equalizerTemplate.pdf; sourceTree = ""; }; 8314D63B1A354DFE00EEE8E6 /* sidplay.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = sidplay.xcodeproj; path = Plugins/sidplay/sidplay.xcodeproj; sourceTree = ""; }; 832923AE279FAC400048201E /* Cog.q1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Cog.q1.json; sourceTree = ""; }; @@ -1631,6 +1634,24 @@ name = Equalizer; sourceTree = ""; }; + 830C37EF27B9956C00E02BB0 /* ThirdParty */ = { + isa = PBXGroup; + children = ( + 830C37F027B9956C00E02BB0 /* deadbeef */, + ); + name = ThirdParty; + path = Visualization/ThirdParty; + sourceTree = ""; + }; + 830C37F027B9956C00E02BB0 /* deadbeef */ = { + isa = PBXGroup; + children = ( + 830C37F127B9956C00E02BB0 /* analyzer.h */, + 830C37F227B9956C00E02BB0 /* analyzer.c */, + ); + path = deadbeef; + sourceTree = ""; + }; 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 */, diff --git a/Visualization/SpectrumView.h b/Visualization/SpectrumView.h index 8394b6b99..c788622eb 100644 --- a/Visualization/SpectrumView.h +++ b/Visualization/SpectrumView.h @@ -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 diff --git a/Visualization/SpectrumView.m b/Visualization/SpectrumView.m index afa238ecf..1f6cd03a5 100644 --- a/Visualization/SpectrumView.m +++ b/Visualization/SpectrumView.m @@ -7,13 +7,31 @@ #import "SpectrumView.h" -#import +#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 diff --git a/Visualization/ThirdParty/deadbeef/analyzer.c b/Visualization/ThirdParty/deadbeef/analyzer.c new file mode 100644 index 000000000..67a2586cf --- /dev/null +++ b/Visualization/ThirdParty/deadbeef/analyzer.c @@ -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 +#include +#include +#include +#include + +#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; +} diff --git a/Visualization/ThirdParty/deadbeef/analyzer.h b/Visualization/ThirdParty/deadbeef/analyzer.h new file mode 100644 index 000000000..a80cde3c5 --- /dev/null +++ b/Visualization/ThirdParty/deadbeef/analyzer.h @@ -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 */