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 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. #ifdef DEBUG
  16. #define DEBUG_PLOT_FFT
  17. //#define DEBUG_PLOT_FFT_RAW
  18. #endif
  19. #ifdef DEBUG_PLOT_FFT
  20. #define DEBUG_LOG_BEATS
  21. #endif
  22. #import "AudioVisualizer.h"
  23. #import "AppDelegate.h"
  24. #import "EZAudioFFT.h"
  25. #ifdef DEBUG_PLOT_FFT
  26. #import "EZAudioPlot.h"
  27. #endif
  28. // Parameters for fine-tuning beat detection
  29. #define FFT_BUCKET_COUNT 64
  30. #define FFT_BUCKET_HISTORY 45
  31. #define FFT_C_FACTOR 3.3
  32. #define FFT_V0_FACTOR 0.00001
  33. #define FFT_MAX_V0_COLOR 0.00025
  34. #define FFT_COLOR_DECAY 0.98
  35. // Use this to skip specific frequencies
  36. // Only check bass frequencies
  37. //#define FFT_BUCKET_SKIP_CONDITION (i > (FFT_BUCKET_COUNT / 4))
  38. // Only check mid frequencies
  39. //#define FFT_BUCKET_SKIP_CONDITION ((i < (FFT_BUCKET_COUNT / 4)) || (i > (FFT_BUCKET_COUNT * 3 / 4)))
  40. // Only check high frequencies
  41. //#define FFT_BUCKET_SKIP_CONDITION (i < (FFT_BUCKET_COUNT * 3 / 4))
  42. // Factors for nicer debug display
  43. #define FFT_DEBUG_RAW_FACTOR 42.0
  44. #define FFT_DEBUG_FACTOR 230.0
  45. static AppDelegate *appDelegate = nil;
  46. static EZAudioFFT *fft = nil;
  47. static int maxBufferSize = 0;
  48. @implementation AudioVisualizer
  49. + (void)setDelegate:(AppDelegate *)delegate {
  50. appDelegate = delegate;
  51. }
  52. + (void)updateBuffer:(float *)buffer withBufferSize:(UInt32)bufferSize {
  53. // Create Fast Fourier Transformation object
  54. if (fft == nil) {
  55. maxBufferSize = bufferSize;
  56. fft = [EZAudioFFT fftWithMaximumBufferSize:maxBufferSize sampleRate:appDelegate.microphone.audioStreamBasicDescription.mSampleRate];
  57. #ifdef DEBUG
  58. NSLog(@"Created FFT with max. freq.: %.2f\n", appDelegate.microphone.audioStreamBasicDescription.mSampleRate / 2);
  59. #endif
  60. }
  61. // Check for changing buffer sizes
  62. if (bufferSize > maxBufferSize) {
  63. NSLog(@"Buffer Size changed?! %d != %d\n", maxBufferSize, bufferSize);
  64. maxBufferSize = bufferSize;
  65. fft = [EZAudioFFT fftWithMaximumBufferSize:maxBufferSize sampleRate:appDelegate.microphone.audioStreamBasicDescription.mSampleRate];
  66. }
  67. [fft computeFFTWithBuffer:buffer withBufferSize:bufferSize];
  68. static float history[FFT_BUCKET_COUNT][FFT_BUCKET_HISTORY];
  69. static int nextHistory = 0;
  70. static int samplesPerBucket = 0;
  71. // Initialize static variables
  72. if (samplesPerBucket == 0) {
  73. samplesPerBucket = bufferSize / FFT_BUCKET_COUNT;
  74. for (int i = 0; i < FFT_BUCKET_COUNT; i++) {
  75. for (int j = 0; j < FFT_BUCKET_HISTORY; j++) {
  76. history[i][j] = 0.5f;
  77. }
  78. }
  79. }
  80. // Split FFT output into a small number of 'buckets' or 'bins' and add to circular history buffer
  81. for (int i = 0; i < FFT_BUCKET_COUNT; i++) {
  82. float sum = 0.0f;
  83. for (int j = 0; j < samplesPerBucket; j++) {
  84. sum += fft.fftData[(i + samplesPerBucket) + j];
  85. }
  86. history[i][nextHistory] = sum / samplesPerBucket;
  87. }
  88. #ifdef DEBUG_PLOT_FFT
  89. int beatCount = 0;
  90. #endif
  91. // Slowly fade old colors to black
  92. static unsigned char lastRed = 0, lastGreen = 0, lastBlue = 0;
  93. lastRed = lastRed * FFT_COLOR_DECAY;
  94. lastGreen = lastGreen * FFT_COLOR_DECAY;
  95. lastBlue = lastBlue * FFT_COLOR_DECAY;
  96. // Check for any beats
  97. for (int i = 0; i < FFT_BUCKET_COUNT; i++) {
  98. // Skip frequency bands, if required
  99. #ifdef FFT_BUCKET_SKIP_CONDITION
  100. if (FFT_BUCKET_SKIP_CONDITION) continue;
  101. #endif
  102. // Calculate average of history of this frequency
  103. float average = 0.0f;
  104. for (int j = 0; j < FFT_BUCKET_HISTORY; j++) {
  105. average += history[i][j];
  106. }
  107. average /= FFT_BUCKET_HISTORY;
  108. // Calculate variance of current bucket in history
  109. float v = 0.0f;
  110. for (int j = 0; j < FFT_BUCKET_HISTORY; j++) {
  111. float tmp = history[i][j] - average;
  112. tmp *= tmp;
  113. v += tmp;
  114. }
  115. v /= FFT_BUCKET_HISTORY;
  116. // Check for beat conditions
  117. if ((history[i][nextHistory] > (FFT_C_FACTOR * average)) && (v > FFT_V0_FACTOR)) {
  118. // Found a beat on this frequency band, map to a single color
  119. if (v < FFT_V0_FACTOR) v = FFT_V0_FACTOR;
  120. if (v > FFT_MAX_V0_COLOR) v = FFT_MAX_V0_COLOR;
  121. float bright = [AppDelegate map:v FromMin:FFT_V0_FACTOR FromMax:FFT_MAX_V0_COLOR ToMin:0.0 ToMax:100.0];
  122. float hue = [AppDelegate map:i FromMin:0.0 FromMax:FFT_BUCKET_COUNT ToMin:0.0 ToMax:360.0];
  123. unsigned char r, g, b;
  124. [AppDelegate convertH:hue S:1.0 V:bright toR:&r G:&g B:&b];
  125. // Blend with last color using averaging
  126. int tmpR = (lastRed + r) / 2;
  127. int tmpG = (lastGreen + g) / 2;
  128. int tmpB = (lastBlue + b) / 2;
  129. lastRed = tmpR;
  130. lastGreen = tmpG;
  131. lastBlue = tmpB;
  132. #ifdef DEBUG_LOG_BEATS
  133. NSLog(@"Beat in %d with c: %f v: %f", i, (history[i][nextHistory] / average), v);
  134. #endif
  135. #ifdef DEBUG_PLOT_FFT
  136. beatCount++;
  137. #endif
  138. }
  139. }
  140. // Send new RGB value to lights, if it has changed
  141. static unsigned char lastSentRed = 42, lastSentGreen = 23, lastSentBlue = 99;
  142. if ((lastSentRed != lastRed) || (lastSentGreen != lastGreen) || (lastSentBlue != lastBlue)) {
  143. [appDelegate setLightsR:lastRed G:lastGreen B:lastBlue];
  144. lastSentRed = lastRed;
  145. lastSentGreen = lastGreen;
  146. lastSentBlue = lastBlue;
  147. }
  148. // Display debug FFT plot, if required
  149. #ifdef DEBUG_PLOT_FFT
  150. static NSWindow *window = nil;
  151. static EZAudioPlot *plot = nil;
  152. static NSTextField *label = nil;
  153. if ((window == nil) || (plot == nil) || (label == nil)) {
  154. // Create window
  155. NSRect frame = NSMakeRect(450, 300, 600, 400);
  156. window = [[NSWindow alloc] initWithContentRect:frame
  157. styleMask:NSClosableWindowMask | NSTitledWindowMask | NSBorderlessWindowMask
  158. backing:NSBackingStoreBuffered
  159. defer:NO];
  160. [window setTitle:@"Debug FFT"];
  161. // Create FFT Plot and add to window
  162. plot = [[EZAudioPlot alloc] initWithFrame:window.contentView.frame];
  163. plot.color = [NSColor whiteColor];
  164. plot.shouldOptimizeForRealtimePlot = NO; // Not working with 'YES' here?!
  165. plot.shouldFill = YES;
  166. plot.shouldCenterYAxis = NO;
  167. plot.shouldMirror = NO;
  168. plot.plotType = EZPlotTypeBuffer;
  169. [window.contentView addSubview:plot];
  170. // Create beat count label
  171. label = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 380, 600, 20)];
  172. [label setTextColor:[NSColor whiteColor]];
  173. [label setEditable:NO];
  174. [label setBezeled:NO];
  175. [label setDrawsBackground:NO];
  176. [label setSelectable:NO];
  177. [label setStringValue:@"-"];
  178. [window.contentView addSubview:label];
  179. // Make window visible
  180. [window makeKeyAndOrderFront:appDelegate.application];
  181. NSLog(@"Created debugging FFT Plot window...\n");
  182. }
  183. // Copy output to input buffer (a bit ugly, but is always big enough)
  184. // Scale so user can see something
  185. # ifdef DEBUG_PLOT_FFT_RAW
  186. memcpy(buffer, fft.fftData, bufferSize * sizeof(float));
  187. for (UInt32 i = 0; i < bufferSize; i++) {
  188. buffer[i] *= FFT_DEBUG_RAW_FACTOR;
  189. # else
  190. for (int i = 0; i < FFT_BUCKET_COUNT; i++) {
  191. buffer[i] = history[i][nextHistory];
  192. }
  193. for (UInt32 i = 0; i < FFT_BUCKET_COUNT; i++) {
  194. buffer[i] *= FFT_DEBUG_FACTOR;
  195. # endif
  196. if (buffer[i] > 1.0f) buffer[i] = 1.0f;
  197. if (buffer[i] < -1.0f) buffer[i] = -1.0f;
  198. }
  199. [plot updateBuffer:buffer withBufferSize:bufferSize];
  200. // Change background color to match color output and show beat counter
  201. [window setBackgroundColor:[NSColor colorWithCalibratedRed:lastRed / 255.0 green:lastGreen / 255.0 blue:lastBlue / 255.0 alpha:1.0]];
  202. [label setStringValue:[NSString stringWithFormat:@"Beats: %d", beatCount]];
  203. #endif
  204. // Point to next history buffer
  205. nextHistory++;
  206. if (nextHistory >= FFT_BUCKET_HISTORY) {
  207. nextHistory = 0;
  208. }
  209. }
  210. @end