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

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