Browse Source

Added FFT sound visualization

Thomas Buck 8 years ago
parent
commit
66aba82c33
6 changed files with 219 additions and 28 deletions
  1. 7
    0
      CaseLights/AppDelegate.h
  2. 20
    26
      CaseLights/AppDelegate.m
  3. 186
    0
      CaseLights/AudioVisualizer.m
  4. 1
    1
      CaseLights/Info.plist
  5. 3
    1
      CaseLights/Serial.m
  6. 2
    0
      README.md

+ 7
- 0
CaseLights/AppDelegate.h View File

@@ -15,10 +15,17 @@
15 15
 
16 16
 @interface AppDelegate : NSObject <NSApplicationDelegate, EZMicrophoneDelegate>
17 17
 
18
+@property (weak) IBOutlet NSApplication *application;
19
+
20
+@property (strong) EZMicrophone *microphone;
21
+
18 22
 - (void)clearDisplayUI;
19 23
 - (void)updateDisplayUI:(NSArray *)displayIDs;
20 24
 
21 25
 - (void)setLightsR:(unsigned char)r G:(unsigned char)g B:(unsigned char)b;
22 26
 
27
++ (double)map:(double)val FromMin:(double)fmin FromMax:(double)fmax ToMin:(double)tmin ToMax:(double)tmax;
28
++ (void)convertH:(double)h S:(double)s V:(double)v toR:(unsigned char *)r G:(unsigned char *)g B:(unsigned char *)b;
29
+
23 30
 @end
24 31
 

+ 20
- 26
CaseLights/AppDelegate.m View File

@@ -64,7 +64,6 @@
64 64
 @interface AppDelegate ()
65 65
 
66 66
 @property (weak) IBOutlet NSMenu *statusMenu;
67
-@property (weak) IBOutlet NSApplication *application;
68 67
 
69 68
 @property (weak) IBOutlet NSMenu *menuColors;
70 69
 @property (weak) IBOutlet NSMenu *menuAnimations;
@@ -89,7 +88,6 @@
89 88
 @property (strong) NSTimer *animation;
90 89
 @property (strong) Serial *serial;
91 90
 @property (strong) NSMenuItem *lastLEDMode;
92
-@property (strong) EZMicrophone *microphone;
93 91
 
94 92
 @end
95 93
 
@@ -880,14 +878,6 @@
880 878
     [application orderFrontStandardAboutPanel:self];
881 879
 }
882 880
 
883
-- (void)updateBuffer:(float *)buffer withBufferSize:(UInt32)bufferSize {
884
-    if (microphone == nil) {
885
-        return; // Old buffer from before we changed mode
886
-    }
887
-    
888
-    [AudioVisualizer updateBuffer:buffer withBufferSize:bufferSize];
889
-}
890
-
891 881
 // ------------------------------------------------------
892 882
 // ----------------- Microphone Delegate ----------------
893 883
 // ------------------------------------------------------
@@ -903,8 +893,12 @@
903 893
     // EZAudioPlot, EZAudioPlotGL, or whatever visualization you would like to do with
904 894
     // the microphone data.
905 895
     dispatch_async(dispatch_get_main_queue(),^{
896
+        if (weakSelf.microphone == nil) {
897
+            return;
898
+        }
899
+        
906 900
         // buffer[0] = left channel, buffer[1] = right channel
907
-        [weakSelf updateBuffer:buffer[0] withBufferSize:bufferSize];
901
+        [AudioVisualizer updateBuffer:buffer[0] withBufferSize:bufferSize];
908 902
     });
909 903
 }
910 904
 
@@ -956,14 +950,14 @@
956 950
     if ([GPUStats getGPUUsage:&usage freeVRAM:&freeVRAM usedVRAM:&usedVRAM] != 0) {
957 951
         NSLog(@"Error reading GPU information\n");
958 952
     } else {
959
-        double h = [self map:[usage doubleValue] FromMin:0.0 FromMax:100.0 ToMin:GPU_COLOR_MIN ToMax:GPU_COLOR_MAX];
953
+        double h = [AppDelegate map:[usage doubleValue] FromMin:0.0 FromMax:100.0 ToMin:GPU_COLOR_MIN ToMax:GPU_COLOR_MAX];
960 954
         
961 955
 #ifdef DEBUG
962 956
         NSLog(@"GPU Usage: %.3f%%\n", [usage doubleValue]);
963 957
 #endif
964 958
         
965 959
         unsigned char r, g, b;
966
-        [self convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
960
+        [AppDelegate convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
967 961
         [self setLightsR:r G:g B:b];
968 962
     }
969 963
 }
@@ -975,14 +969,14 @@
975 969
     if ([GPUStats getGPUUsage:&usage freeVRAM:&freeVRAM usedVRAM:&usedVRAM] != 0) {
976 970
         NSLog(@"Error reading GPU information\n");
977 971
     } else {
978
-        double h = [self map:[freeVRAM doubleValue] FromMin:0.0 FromMax:([freeVRAM doubleValue] + [usedVRAM doubleValue]) ToMin:RAM_COLOR_MIN ToMax:RAM_COLOR_MAX];
972
+        double h = [AppDelegate map:[freeVRAM doubleValue] FromMin:0.0 FromMax:([freeVRAM doubleValue] + [usedVRAM doubleValue]) ToMin:RAM_COLOR_MIN ToMax:RAM_COLOR_MAX];
979 973
         
980 974
 #ifdef DEBUG
981 975
         NSLog(@"VRAM %.2fGB Free + %.2fGB Used = %.2fGB mapped to color %.2f!\n", [freeVRAM doubleValue] / (1024.0 * 1024.0 * 1024.0), [usedVRAM doubleValue] / (1024.0 * 1024.0 * 1024.0), ([freeVRAM doubleValue] + [usedVRAM doubleValue]) / (1024.0 * 1024.0 * 1024.0), h);
982 976
 #endif
983 977
         
984 978
         unsigned char r, g, b;
985
-        [self convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
979
+        [AppDelegate convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
986 980
         [self setLightsR:r G:g B:b];
987 981
     }
988 982
 }
@@ -990,28 +984,28 @@
990 984
 - (void)visualizeCPUUsage:(NSTimer *)timer {
991 985
     JSKMCPUUsageInfo cpuUsageInfo = [JSKSystemMonitor systemMonitor].cpuUsageInfo;
992 986
     
993
-    double h = [self map:cpuUsageInfo.usage FromMin:0.0 FromMax:100.0 ToMin:CPU_COLOR_MIN ToMax:CPU_COLOR_MAX];
987
+    double h = [AppDelegate map:cpuUsageInfo.usage FromMin:0.0 FromMax:100.0 ToMin:CPU_COLOR_MIN ToMax:CPU_COLOR_MAX];
994 988
     
995 989
 #ifdef DEBUG
996 990
     NSLog(@"CPU Usage: %.3f%%\n", cpuUsageInfo.usage);
997 991
 #endif
998 992
     
999 993
     unsigned char r, g, b;
1000
-    [self convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
994
+    [AppDelegate convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1001 995
     [self setLightsR:r G:g B:b];
1002 996
 }
1003 997
 
1004 998
 - (void)visualizeRAMUsage:(NSTimer *)timer {
1005 999
     JSKMMemoryUsageInfo memoryUsageInfo = [JSKSystemMonitor systemMonitor].memoryUsageInfo;
1006 1000
     
1007
-    double h = [self map:memoryUsageInfo.freeMemory FromMin:0.0 FromMax:(memoryUsageInfo.usedMemory + memoryUsageInfo.freeMemory) ToMin:RAM_COLOR_MIN ToMax:RAM_COLOR_MAX];
1001
+    double h = [AppDelegate map:memoryUsageInfo.freeMemory FromMin:0.0 FromMax:(memoryUsageInfo.usedMemory + memoryUsageInfo.freeMemory) ToMin:RAM_COLOR_MIN ToMax:RAM_COLOR_MAX];
1008 1002
     
1009 1003
 #ifdef DEBUG
1010 1004
     NSLog(@"RAM %.2fGB Free + %.2fGB Used = %.2fGB mapped to color %.2f!\n", memoryUsageInfo.freeMemory / (1024.0 * 1024.0 * 1024.0), memoryUsageInfo.usedMemory / (1024.0 * 1024.0 * 1024.0), (memoryUsageInfo.freeMemory + memoryUsageInfo.usedMemory) / (1024.0 * 1024.0 * 1024.0), h);
1011 1005
 #endif
1012 1006
     
1013 1007
     unsigned char r, g, b;
1014
-    [self convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1008
+    [AppDelegate convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1015 1009
     [self setLightsR:r G:g B:b];
1016 1010
 }
1017 1011
 
@@ -1031,14 +1025,14 @@
1031 1025
         temp = GPU_TEMP_MIN;
1032 1026
     }
1033 1027
     
1034
-    double h = [self map:temp FromMin:GPU_TEMP_MIN FromMax:GPU_TEMP_MAX ToMin:GPU_COLOR_MIN ToMax:GPU_COLOR_MAX];
1028
+    double h = [AppDelegate map:temp FromMin:GPU_TEMP_MIN FromMax:GPU_TEMP_MAX ToMin:GPU_COLOR_MIN ToMax:GPU_COLOR_MAX];
1035 1029
     
1036 1030
 #ifdef DEBUG
1037 1031
     NSLog(@"GPU Temp %.2f mapped to color %.2f!\n", temp, h);
1038 1032
 #endif
1039 1033
     
1040 1034
     unsigned char r, g, b;
1041
-    [self convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1035
+    [AppDelegate convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1042 1036
     [self setLightsR:r G:g B:b];
1043 1037
 }
1044 1038
 
@@ -1058,14 +1052,14 @@
1058 1052
         temp = CPU_TEMP_MIN;
1059 1053
     }
1060 1054
     
1061
-    double h = [self map:temp FromMin:CPU_TEMP_MIN FromMax:CPU_TEMP_MAX ToMin:CPU_COLOR_MIN ToMax:CPU_COLOR_MAX];
1055
+    double h = [AppDelegate map:temp FromMin:CPU_TEMP_MIN FromMax:CPU_TEMP_MAX ToMin:CPU_COLOR_MIN ToMax:CPU_COLOR_MAX];
1062 1056
     
1063 1057
 #ifdef DEBUG
1064 1058
     NSLog(@"CPU Temp %.2f mapped to color %.2f!\n", temp, h);
1065 1059
 #endif
1066 1060
     
1067 1061
     unsigned char r, g, b;
1068
-    [self convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1062
+    [AppDelegate convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1069 1063
     [self setLightsR:r G:g B:b];
1070 1064
 }
1071 1065
 
@@ -1103,7 +1097,7 @@
1103 1097
     }
1104 1098
     
1105 1099
     unsigned char r, g, b;
1106
-    [self convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1100
+    [AppDelegate convertH:h S:1.0 V:1.0 toR:&r G:&g B:&b];
1107 1101
     [self setLightsR:r G:g B:b];
1108 1102
 }
1109 1103
 
@@ -1115,12 +1109,12 @@
1115 1109
 // --------------------- Utilities ---------------------
1116 1110
 // -----------------------------------------------------
1117 1111
 
1118
-- (double)map:(double)val FromMin:(double)fmin FromMax:(double)fmax ToMin:(double)tmin ToMax:(double)tmax {
1112
++ (double)map:(double)val FromMin:(double)fmin FromMax:(double)fmax ToMin:(double)tmin ToMax:(double)tmax {
1119 1113
     double norm = (val - fmin) / (fmax - fmin);
1120 1114
     return (norm * (tmax - tmin)) + tmin;
1121 1115
 }
1122 1116
 
1123
-- (void)convertH:(double)h S:(double)s V:(double)v toR:(unsigned char *)r G:(unsigned char *)g B:(unsigned char *)b {
1117
++ (void)convertH:(double)h S:(double)s V:(double)v toR:(unsigned char *)r G:(unsigned char *)g B:(unsigned char *)b {
1124 1118
     // Adapted from:
1125 1119
     // https://gist.github.com/hdznrrd/656996
1126 1120
     

+ 186
- 0
CaseLights/AudioVisualizer.m View File

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

+ 1
- 1
CaseLights/Info.plist View File

@@ -21,7 +21,7 @@
21 21
 	<key>CFBundleSignature</key>
22 22
 	<string>????</string>
23 23
 	<key>CFBundleVersion</key>
24
-	<string>182</string>
24
+	<string>359</string>
25 25
 	<key>LSApplicationCategoryType</key>
26 26
 	<string>public.app-category.utilities</string>
27 27
 	<key>LSMinimumSystemVersion</key>

+ 3
- 1
CaseLights/Serial.m View File

@@ -9,6 +9,8 @@
9 9
 //  Copyright © 2015 xythobuz. All rights reserved.
10 10
 //
11 11
 
12
+//#define DEBUG_TEXT
13
+
12 14
 #import <Cocoa/Cocoa.h>
13 15
 #import <IOKit/IOKitLib.h>
14 16
 #import <IOKit/serial/IOSerialKeys.h>
@@ -155,7 +157,7 @@
155 157
     const char *data = [string UTF8String];
156 158
     size_t length = strlen(data);
157 159
     
158
-#ifdef DEBUG
160
+#ifdef DEBUG_TEXT
159 161
     NSLog(@"Sending string %s", data);
160 162
 #endif
161 163
     

+ 2
- 0
README.md View File

@@ -22,6 +22,8 @@ CaseLights is only visible in the system menu bar. You can enable or disable the
22 22
 
23 23
 You can also select one of the displays connected to the Host machine. The CaseLights App will then create a Screenshot of this display 10-times per second and calculate the average color to display it on the RGB LEDs.
24 24
 
25
+CaseLights is also able to visualize sound coming from a system audio input. To be able to directly visualize the system sound output, install [Soundflower](https://github.com/mattingalls/Soundflower) and create a Multi-Output-Device in the Mac `Audio Midi Setup.app` consisting of `Soundflower (2ch)` and your normally used output device. Set this device as audio output. Then, in CaseLights, select `Soundflower (2ch)` as input.
26
+
25 27
 ## Working with Git Submodules
26 28
 
27 29
 To clone this repository, enter the following:

Loading…
Cancel
Save