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.9KB

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