Simple RGB LED controller for Mac OS X
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AudioVisualizer.m 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. //
  2. // AudioVisualizer.m
  3. // CaseLights
  4. //
  5. // Based on the ideas in:
  6. // http://archive.gamedev.net/archive/reference/programming/features/beatdetection/
  7. //
  8. // The detected sound frequency of beats will be mapped to the hue of the resulting color,
  9. // the variance of the beat is mapped to the brightness of the color. The colors
  10. // of all detected beats will be added together to form the final displayed color.
  11. //
  12. // Created by Thomas Buck on 01.01.16.
  13. // Copyright © 2016 xythobuz. All rights reserved.
  14. //
  15. // Enabling this will cause crashes when changing audio input
  16. // devices while the app is running. Select it before enabling.
  17. #define DEBUG_PLOT_FFT
  18. //#define DEBUG_PLOT_FFT_RAW
  19. #define DEBUG_LOG_BEATS
  20. #import "AudioVisualizer.h"
  21. #import "AppDelegate.h"
  22. #import "EZAudioFFT.h"
  23. #ifdef DEBUG_PLOT_FFT
  24. #import "EZAudioPlot.h"
  25. #endif
  26. // Parameters for fine-tuning beat detection
  27. #define FFT_BUCKET_COUNT 64
  28. #define FFT_BUCKET_HISTORY 43
  29. #define FFT_C_FACTOR 4.2
  30. #define FFT_V0_FACTOR 0.000015
  31. #define FFT_MAX_V0_COLOR 0.0002
  32. #define FFT_COLOR_DECAY 0.99
  33. // Factors for nicer debug display
  34. #define FFT_DEBUG_RAW_FACTOR 42.0
  35. #define FFT_DEBUG_FACTOR 230.0
  36. static AppDelegate *appDelegate = nil;
  37. static EZAudioFFT *fft = nil;
  38. static int maxBufferSize = 0;
  39. @implementation AudioVisualizer
  40. + (void)setDelegate:(AppDelegate *)delegate {
  41. appDelegate = delegate;
  42. }
  43. + (void)updateBuffer:(float *)buffer withBufferSize:(UInt32)bufferSize {
  44. // Create Fast Fourier Transformation object
  45. if (fft == nil) {
  46. maxBufferSize = bufferSize;
  47. fft = [EZAudioFFT fftWithMaximumBufferSize:maxBufferSize sampleRate:appDelegate.microphone.audioStreamBasicDescription.mSampleRate];
  48. #ifdef DEBUG
  49. NSLog(@"Created FFT with max. freq.: %.2f\n", appDelegate.microphone.audioStreamBasicDescription.mSampleRate / 2);
  50. #endif
  51. }
  52. // Check for changing buffer sizes
  53. if (bufferSize > maxBufferSize) {
  54. NSLog(@"Buffer Size changed?! %d != %d\n", maxBufferSize, bufferSize);
  55. maxBufferSize = bufferSize;
  56. fft = [EZAudioFFT fftWithMaximumBufferSize:maxBufferSize sampleRate:appDelegate.microphone.audioStreamBasicDescription.mSampleRate];
  57. }
  58. [fft computeFFTWithBuffer:buffer withBufferSize:bufferSize];
  59. static float history[FFT_BUCKET_COUNT][FFT_BUCKET_HISTORY];
  60. static int nextHistory = 0;
  61. static int samplesPerBucket = 0;
  62. // Initialize static variables
  63. if (samplesPerBucket == 0) {
  64. samplesPerBucket = bufferSize / FFT_BUCKET_COUNT;
  65. for (int i = 0; i < FFT_BUCKET_COUNT; i++) {
  66. for (int j = 0; j < FFT_BUCKET_HISTORY; j++) {
  67. history[i][j] = 0.5f;
  68. }
  69. }
  70. }
  71. // Split FFT output into a small number of 'buckets' or 'bins' and add to circular history buffer
  72. for (int i = 0; i < FFT_BUCKET_COUNT; i++) {
  73. float sum = 0.0f;
  74. for (int j = 0; j < samplesPerBucket; j++) {
  75. sum += fft.fftData[(i + samplesPerBucket) + j];
  76. }
  77. history[i][nextHistory] = sum / samplesPerBucket;
  78. }
  79. #ifdef DEBUG_PLOT_FFT
  80. int beatCount = 0;
  81. #endif
  82. static unsigned char lastRed = 0, lastGreen = 0, lastBlue = 0;
  83. lastRed = lastRed * FFT_COLOR_DECAY;
  84. lastGreen = lastGreen * FFT_COLOR_DECAY;
  85. lastBlue = lastBlue * FFT_COLOR_DECAY;
  86. // Check for any beats
  87. for (int i = 0; i < FFT_BUCKET_COUNT; i++) {
  88. float average = 0.0f;
  89. for (int j = 0; j < FFT_BUCKET_HISTORY; j++) {
  90. average += history[i][j];
  91. }
  92. average /= FFT_BUCKET_HISTORY;
  93. float v = 0.0f;
  94. for (int j = 0; j < FFT_BUCKET_HISTORY; j++) {
  95. float tmp = history[i][j] - average;
  96. tmp *= tmp;
  97. v += tmp;
  98. }
  99. v /= FFT_BUCKET_HISTORY;
  100. if ((history[i][nextHistory] > (FFT_C_FACTOR * average)) && (v > FFT_V0_FACTOR)) {
  101. // Found a beat on this frequency band, map to a single color
  102. if (v < FFT_V0_FACTOR) v = FFT_V0_FACTOR;
  103. if (v > FFT_MAX_V0_COLOR) v = FFT_MAX_V0_COLOR;
  104. float bright = [AppDelegate map:v FromMin:FFT_V0_FACTOR FromMax:FFT_MAX_V0_COLOR ToMin:0.0 ToMax:100.0];
  105. float hue = [AppDelegate map:i FromMin:0.0 FromMax:FFT_BUCKET_COUNT ToMin:0.0 ToMax:360.0];
  106. unsigned char r, g, b;
  107. [AppDelegate convertH:hue S:1.0 V:bright toR:&r G:&g B:&b];
  108. // Blend with last color using averaging
  109. int tmpR = (lastRed + r) / 2;
  110. int tmpG = (lastGreen + g) / 2;
  111. int tmpB = (lastBlue + b) / 2;
  112. lastRed = tmpR;
  113. lastGreen = tmpG;
  114. lastBlue = tmpB;
  115. #ifdef DEBUG_LOG_BEATS
  116. NSLog(@"Beat in %d with c: %f v: %f", i, (history[i][nextHistory] / average), v);
  117. #endif
  118. #ifdef DEBUG_PLOT_FFT
  119. beatCount++;
  120. #endif
  121. }
  122. }
  123. [appDelegate setLightsR:lastRed G:lastGreen B:lastBlue];
  124. #ifdef DEBUG_PLOT_FFT
  125. static NSWindow *window = nil;
  126. static EZAudioPlot *plot = nil;
  127. static NSTextField *label = nil;
  128. if ((window == nil) || (plot == nil) || (label == nil)) {
  129. NSRect frame = NSMakeRect(450, 300, 600, 400);
  130. window = [[NSWindow alloc] initWithContentRect:frame
  131. styleMask:NSClosableWindowMask | NSTitledWindowMask | NSBorderlessWindowMask
  132. backing:NSBackingStoreBuffered
  133. defer:NO];
  134. [window setTitle:@"Debug FFT"];
  135. plot = [[EZAudioPlot alloc] initWithFrame:window.contentView.frame];
  136. plot.color = [NSColor whiteColor];
  137. plot.shouldOptimizeForRealtimePlot = NO; // Not working with 'YES' here?!
  138. plot.shouldFill = YES;
  139. plot.shouldCenterYAxis = NO;
  140. plot.shouldMirror = NO;
  141. plot.plotType = EZPlotTypeBuffer;
  142. [window.contentView addSubview:plot];
  143. label = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 380, 600, 20)];
  144. [label setTextColor:[NSColor whiteColor]];
  145. [label setEditable:NO];
  146. [label setBezeled:NO];
  147. [label setDrawsBackground:NO];
  148. [label setSelectable:NO];
  149. [label setStringValue:@"-"];
  150. [window.contentView addSubview:label];
  151. [window makeKeyAndOrderFront:appDelegate.application];
  152. NSLog(@"Created debugging FFT Plot window...\n");
  153. }
  154. // Scale so we can see something
  155. # ifdef DEBUG_PLOT_FFT_RAW
  156. memcpy(buffer, fft.fftData, bufferSize * sizeof(float));
  157. for (UInt32 i = 0; i < bufferSize; i++) {
  158. buffer[i] *= FFT_DEBUG_RAW_FACTOR;
  159. # else
  160. for (int i = 0; i < FFT_BUCKET_COUNT; i++) {
  161. buffer[i] = history[i][nextHistory];
  162. }
  163. for (UInt32 i = 0; i < FFT_BUCKET_COUNT; i++) {
  164. buffer[i] *= FFT_DEBUG_FACTOR;
  165. # endif
  166. if (buffer[i] > 1.0f) buffer[i] = 1.0f;
  167. if (buffer[i] < -1.0f) buffer[i] = -1.0f;
  168. }
  169. [plot updateBuffer:buffer withBufferSize:bufferSize];
  170. [window setBackgroundColor:[NSColor colorWithCalibratedRed:lastRed / 255.0 green:lastGreen / 255.0 blue:lastBlue / 255.0 alpha:1.0]];
  171. [label setStringValue:[NSString stringWithFormat:@"Beats: %d", beatCount]];
  172. #endif
  173. // Point to next history buffer
  174. nextHistory++;
  175. if (nextHistory >= FFT_BUCKET_HISTORY) {
  176. nextHistory = 0;
  177. }
  178. }
  179. @end