├── .gitignore ├── teensy_fft_hardware.fzz ├── Spectrogram.py ├── README.md ├── SpectrogramDevice.py ├── SerialPortDevice.py ├── toneinput └── toneinput.ino ├── spectrum └── spectrum.ino └── SpectrogramUI.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /teensy_fft_hardware.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdicola/adafruit_guide_fft/HEAD/teensy_fft_hardware.fzz -------------------------------------------------------------------------------- /Spectrogram.py: -------------------------------------------------------------------------------- 1 | # Spectrogram.py 2 | # Copyright 2013 Tony DiCola (tony@tonydicola.com) 3 | 4 | # Program to display a real-time spectrogram of data read from a device on the serial port. 5 | # Depends on the following python libraries: 6 | # - PySides (and Qt4) 7 | # - Matplotlib 8 | # - Numpy 9 | # - PySerial 10 | # For PySides, Matplotlib, and Numpy it is _highly_ recommended to install a prebuilt 11 | # python distribution for scientific computing such as Anaconda or Enthought Canopy! 12 | 13 | import sys 14 | 15 | from PySide import QtGui 16 | 17 | import SpectrogramUI 18 | import SerialPortDevice 19 | 20 | 21 | app = QtGui.QApplication(sys.argv) 22 | devices = SerialPortDevice.enumerate_devices() 23 | ui = SpectrogramUI.MainWindow(devices) 24 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FFT: Fun with Fourier Transforms 2 | 3 | Code in support of the ['FFT: Fun with Fourier Transforms' guide](http://learn.adafruit.com/fft-fun-with-fourier-transforms) on the [Adafruit learning system](http://learn.adafruit.com/). 4 | 5 | ## License 6 | 7 | The MIT License (MIT) 8 | 9 | Copyright (c) 2013 Tony DiCola 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /SpectrogramDevice.py: -------------------------------------------------------------------------------- 1 | # SpectrogramDevice.py 2 | # Copyright 2013 Tony DiCola (tony@tonydicola.com) 3 | 4 | # An abstract base class to implement for communication with a spectrogram device. 5 | # This is meant for extending the spectrogram viewer UI so it can communicate with 6 | # other devices in the future. 7 | 8 | 9 | class SpectrogramDevice(object): 10 | def get_name(self): 11 | """Return name of the device.""" 12 | raise NotImplementedError() 13 | 14 | def get_fftsize(self): 15 | """Return device's FFT size.""" 16 | raise NotImplementedError() 17 | 18 | def set_fftsize(self, size): 19 | """Set the device's FFT size to the specified number of bins.""" 20 | raise NotImplementedError() 21 | 22 | def get_samplerate(self): 23 | """Return device's sample rate in hertz.""" 24 | raise NotImplementedError() 25 | 26 | def set_samplerate(self, frequency): 27 | """Set the device's sample rate to the specified frequency in hertz.""" 28 | raise NotImplementedError() 29 | 30 | def get_magnitudes(self): 31 | """Return a list of magnitudes from an FFT run on the device. 32 | The size of the returned magnitude list should be the sample 33 | as the device's FFT size. 34 | """ 35 | raise NotImplementedError() 36 | 37 | def open(self): 38 | """Open communication with the device.""" 39 | raise NotImplementedError() 40 | 41 | def close(self): 42 | """Close communication with the device.""" 43 | raise NotImplementedError() -------------------------------------------------------------------------------- /SerialPortDevice.py: -------------------------------------------------------------------------------- 1 | # SerialPortDevice.py 2 | # Copyright 2013 Tony DiCola (tony@tonydicola.com) 3 | 4 | # This is a concrete implementation of SpectrogramDevice to define communication 5 | # with a serial port-based spectrogram device. 6 | 7 | import serial 8 | import serial.tools.list_ports 9 | 10 | import SpectrogramDevice 11 | 12 | 13 | TIMEOUT_SECONDS = 5 14 | PORT_BAUD_RATE = 38400 15 | 16 | 17 | class SerialPortDevice(SpectrogramDevice.SpectrogramDevice): 18 | def __init__(self, path, name): 19 | """Create a serial port device with a path to the serial port and name of the device. 20 | Path and name should be values returned from calling enumerate_devices. 21 | """ 22 | self.path = path 23 | self.name = name 24 | 25 | def get_name(self): 26 | """Return device name based on serial port name.""" 27 | return self.name 28 | 29 | def get_fftsize(self): 30 | """Return device FFT size.""" 31 | return self.fftSize 32 | 33 | def get_samplerate(self): 34 | """Return device sample rate in hertz.""" 35 | return self.sampleRate 36 | 37 | def set_samplerate(self, samplerate): 38 | self.port.write('SET SAMPLE_RATE_HZ %d;' % samplerate) 39 | self.sampleRate = samplerate 40 | 41 | def get_magnitudes(self): 42 | """Return an array of FFT magnitudes. The number of values returned is the same as the FFT size.""" 43 | self.port.write('GET MAGNITUDES;') 44 | return [float(self._readline()) for i in range(self.fftSize)] 45 | 46 | def open(self): 47 | """Start communication with the device. Must be done before any other calls are made to the device.""" 48 | self.port = serial.Serial(self.path, PORT_BAUD_RATE, timeout=TIMEOUT_SECONDS, writeTimeout=TIMEOUT_SECONDS) 49 | # Read the initial state of the device 50 | self.port.write('GET FFT_SIZE;') 51 | self.fftSize = int(self._readline()) 52 | self.port.write('GET SAMPLE_RATE_HZ;') 53 | self.sampleRate = int(self._readline()) 54 | 55 | def close(self): 56 | """Close communication with the device.""" 57 | self.port.close() 58 | 59 | def _readline(self): 60 | value = self.port.readline() 61 | if value == None or value == '': 62 | raise IOError('Timeout exceeded while waiting for device to respond.') 63 | return value 64 | 65 | 66 | def enumerate_devices(): 67 | """Enumerate all the serial ports.""" 68 | return [SerialPortDevice(port[0], port[1]) for port in serial.tools.list_ports.comports() if port[2] != 'n/a'] -------------------------------------------------------------------------------- /toneinput/toneinput.ino: -------------------------------------------------------------------------------- 1 | // Audio Tone Input 2 | // Copyright 2013 Tony DiCola (tony@tonydicola.com) 3 | 4 | // This code is part of the guide at http://learn.adafruit.com/fft-fun-with-fourier-transforms/ 5 | 6 | #define ARM_MATH_CM4 7 | #include 8 | #include 9 | 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | // CONIFIGURATION 13 | // These values can be changed to alter the behavior of the spectrum display. 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | int SAMPLE_RATE_HZ = 9000; // Sample rate of the audio in hertz. 17 | const int TONE_LOWS[] = { // Lower bound (in hz) of each tone in the input sequence. 18 | 1723, 1934, 1512, 738, 1125 19 | }; 20 | const int TONE_HIGHS[] = { // Upper bound (in hz) of each tone in the input sequence. 21 | 1758, 1969, 1546, 773, 1160 22 | }; 23 | int TONE_ERROR_MARGIN_HZ = 50; // Allowed fudge factor above and below the bounds for each tone input. 24 | int TONE_WINDOW_MS = 4000; // Maximum amount of milliseconds allowed to enter the full sequence. 25 | float TONE_THRESHOLD_DB = 10.0; // Threshold (in decibels) each tone must be above other frequencies to count. 26 | const int FFT_SIZE = 256; // Size of the FFT. Realistically can only be at most 256 27 | // without running out of memory for buffers and other state. 28 | const int AUDIO_INPUT_PIN = 14; // Input ADC pin for audio data. 29 | const int ANALOG_READ_RESOLUTION = 10; // Bits of resolution for the ADC. 30 | const int ANALOG_READ_AVERAGING = 16; // Number of samples to average with each ADC reading. 31 | const int POWER_LED_PIN = 13; // Output pin for power LED (pin 13 to use Teensy 3.0's onboard LED). 32 | const int NEO_PIXEL_PIN = 3; // Output pin for neo pixels. 33 | const int NEO_PIXEL_COUNT = 4; // Number of neo pixels. You should be able to increase this without 34 | // any other changes to the program. 35 | const int MAX_CHARS = 65; // Max size of the input command buffer 36 | 37 | 38 | //////////////////////////////////////////////////////////////////////////////// 39 | // INTERNAL STATE 40 | // These shouldn't be modified unless you know what you're doing. 41 | //////////////////////////////////////////////////////////////////////////////// 42 | 43 | IntervalTimer samplingTimer; 44 | float samples[FFT_SIZE*2]; 45 | float magnitudes[FFT_SIZE]; 46 | int sampleCounter = 0; 47 | Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEO_PIXEL_COUNT, NEO_PIXEL_PIN, NEO_GRB + NEO_KHZ800); 48 | char commandBuffer[MAX_CHARS]; 49 | int tonePosition = 0; 50 | unsigned long toneStart = 0; 51 | 52 | 53 | //////////////////////////////////////////////////////////////////////////////// 54 | // MAIN SKETCH FUNCTIONS 55 | //////////////////////////////////////////////////////////////////////////////// 56 | 57 | void setup() { 58 | // Set up serial port. 59 | Serial.begin(38400); 60 | 61 | // Set up ADC and audio input. 62 | pinMode(AUDIO_INPUT_PIN, INPUT); 63 | analogReadResolution(ANALOG_READ_RESOLUTION); 64 | analogReadAveraging(ANALOG_READ_AVERAGING); 65 | 66 | // Turn on the power indicator LED. 67 | pinMode(POWER_LED_PIN, OUTPUT); 68 | digitalWrite(POWER_LED_PIN, HIGH); 69 | 70 | // Initialize neo pixel library and turn off the LEDs 71 | pixels.begin(); 72 | pixels.show(); 73 | 74 | // Clear the input command buffer 75 | memset(commandBuffer, 0, sizeof(commandBuffer)); 76 | 77 | // Begin sampling audio 78 | samplingBegin(); 79 | } 80 | 81 | void loop() { 82 | // Calculate FFT if a full sample is available. 83 | if (samplingIsDone()) { 84 | // Run FFT on sample data. 85 | arm_cfft_radix4_instance_f32 fft_inst; 86 | arm_cfft_radix4_init_f32(&fft_inst, FFT_SIZE, 0, 1); 87 | arm_cfft_radix4_f32(&fft_inst, samples); 88 | // Calculate magnitude of complex numbers output by the FFT. 89 | arm_cmplx_mag_f32(samples, magnitudes, FFT_SIZE); 90 | 91 | // Detect tone sequence. 92 | toneLoop(); 93 | 94 | // Restart audio sampling. 95 | samplingBegin(); 96 | } 97 | 98 | // Parse any pending commands. 99 | parserLoop(); 100 | } 101 | 102 | 103 | //////////////////////////////////////////////////////////////////////////////// 104 | // UTILITY FUNCTIONS 105 | //////////////////////////////////////////////////////////////////////////////// 106 | 107 | // Compute the average magnitude of a target frequency window vs. all other frequencies. 108 | void windowMean(float* magnitudes, int lowBin, int highBin, float* windowMean, float* otherMean) { 109 | *windowMean = 0; 110 | *otherMean = 0; 111 | // Notice the first magnitude bin is skipped because it represents the 112 | // average power of the signal. 113 | for (int i = 1; i < FFT_SIZE/2; ++i) { 114 | if (i >= lowBin && i <= highBin) { 115 | *windowMean += magnitudes[i]; 116 | } 117 | else { 118 | *otherMean += magnitudes[i]; 119 | } 120 | } 121 | *windowMean /= (highBin - lowBin) + 1; 122 | *otherMean /= (FFT_SIZE / 2 - (highBin - lowBin)); 123 | } 124 | 125 | // Convert a frequency to the appropriate FFT bin it will fall within. 126 | int frequencyToBin(float frequency) { 127 | float binFrequency = float(SAMPLE_RATE_HZ) / float(FFT_SIZE); 128 | return int(frequency / binFrequency); 129 | } 130 | 131 | // Convert intensity to decibels 132 | float intensityDb(float intensity) { 133 | return 20.0*log10(intensity); 134 | } 135 | 136 | 137 | //////////////////////////////////////////////////////////////////////////////// 138 | // SPECTRUM DISPLAY FUNCTIONS 139 | /////////////////////////////////////////////////////////////////////////////// 140 | 141 | void toneLoop() { 142 | // Calculate the low and high frequency bins for the currently expected tone. 143 | int lowBin = frequencyToBin(TONE_LOWS[tonePosition] - TONE_ERROR_MARGIN_HZ); 144 | int highBin = frequencyToBin(TONE_HIGHS[tonePosition] + TONE_ERROR_MARGIN_HZ); 145 | // Get the average intensity of frequencies inside and outside the tone window. 146 | float window, other; 147 | windowMean(magnitudes, lowBin, highBin, &window, &other); 148 | window = intensityDb(window); 149 | other = intensityDb(other); 150 | // Check if tone intensity is above the threshold to detect a step in the sequence. 151 | if ((window - other) >= TONE_THRESHOLD_DB) { 152 | // Start timing the window if this is the first in the sequence. 153 | unsigned long time = millis(); 154 | if (tonePosition == 0) { 155 | toneStart = time; 156 | } 157 | // Increment key position if still within the window of key input time. 158 | if (toneStart + TONE_WINDOW_MS > time) { 159 | tonePosition += 1; 160 | } 161 | else { 162 | // Outside the window of key input time, reset back to the beginning key. 163 | tonePosition = 0; 164 | } 165 | } 166 | // Check if the entire sequence was passed through. 167 | if (tonePosition >= sizeof(TONE_LOWS)/sizeof(int)) { 168 | toneDetected(); 169 | tonePosition = 0; 170 | } 171 | } 172 | 173 | void toneDetected() { 174 | // Flash the LEDs four times. 175 | int pause = 250; 176 | for (int i = 0; i < 4; ++i) { 177 | for (int j = 0; j < NEO_PIXEL_COUNT; ++j) { 178 | pixels.setPixelColor(j, pixels.Color(255, 0, 0)); 179 | } 180 | pixels.show(); 181 | delay(pause); 182 | for (int j = 0; j < NEO_PIXEL_COUNT; ++j) { 183 | pixels.setPixelColor(j, 0); 184 | } 185 | pixels.show(); 186 | delay(pause); 187 | } 188 | } 189 | 190 | //////////////////////////////////////////////////////////////////////////////// 191 | // SAMPLING FUNCTIONS 192 | //////////////////////////////////////////////////////////////////////////////// 193 | 194 | void samplingCallback() { 195 | // Read from the ADC and store the sample data 196 | samples[sampleCounter] = (float32_t)analogRead(AUDIO_INPUT_PIN); 197 | // Complex FFT functions require a coefficient for the imaginary part of the input. 198 | // Since we only have real data, set this coefficient to zero. 199 | samples[sampleCounter+1] = 0.0; 200 | // Update sample buffer position and stop after the buffer is filled 201 | sampleCounter += 2; 202 | if (sampleCounter >= FFT_SIZE*2) { 203 | samplingTimer.end(); 204 | } 205 | } 206 | 207 | void samplingBegin() { 208 | // Reset sample buffer position and start callback at necessary rate. 209 | sampleCounter = 0; 210 | samplingTimer.begin(samplingCallback, 1000000/SAMPLE_RATE_HZ); 211 | } 212 | 213 | boolean samplingIsDone() { 214 | return sampleCounter >= FFT_SIZE*2; 215 | } 216 | 217 | 218 | //////////////////////////////////////////////////////////////////////////////// 219 | // COMMAND PARSING FUNCTIONS 220 | // These functions allow parsing simple commands input on the serial port. 221 | // Commands allow reading and writing variables that control the device. 222 | // 223 | // All commands must end with a semicolon character. 224 | // 225 | // Example commands are: 226 | // GET SAMPLE_RATE_HZ; 227 | // - Get the sample rate of the device. 228 | // SET SAMPLE_RATE_HZ 400; 229 | // - Set the sample rate of the device to 400 hertz. 230 | // 231 | //////////////////////////////////////////////////////////////////////////////// 232 | 233 | void parserLoop() { 234 | // Process any incoming characters from the serial port 235 | while (Serial.available() > 0) { 236 | char c = Serial.read(); 237 | // Add any characters that aren't the end of a command (semicolon) to the input buffer. 238 | if (c != ';') { 239 | c = toupper(c); 240 | strncat(commandBuffer, &c, 1); 241 | } 242 | else 243 | { 244 | // Parse the command because an end of command token was encountered. 245 | parseCommand(commandBuffer); 246 | // Clear the input buffer 247 | memset(commandBuffer, 0, sizeof(commandBuffer)); 248 | } 249 | } 250 | } 251 | 252 | // Macro used in parseCommand function to simplify parsing get and set commands for a variable 253 | #define GET_AND_SET(variableName) \ 254 | else if (strcmp(command, "GET " #variableName) == 0) { \ 255 | Serial.println(variableName); \ 256 | } \ 257 | else if (strstr(command, "SET " #variableName " ") != NULL) { \ 258 | variableName = (typeof(variableName)) atof(command+(sizeof("SET " #variableName " ")-1)); \ 259 | } 260 | 261 | void parseCommand(char* command) { 262 | if (strcmp(command, "GET MAGNITUDES") == 0) { 263 | for (int i = 0; i < FFT_SIZE; ++i) { 264 | Serial.println(magnitudes[i]); 265 | } 266 | } 267 | else if (strcmp(command, "GET SAMPLES") == 0) { 268 | for (int i = 0; i < FFT_SIZE*2; i+=2) { 269 | Serial.println(samples[i]); 270 | } 271 | } 272 | else if (strcmp(command, "GET FFT_SIZE") == 0) { 273 | Serial.println(FFT_SIZE); 274 | } 275 | GET_AND_SET(SAMPLE_RATE_HZ) 276 | GET_AND_SET(TONE_ERROR_MARGIN_HZ) 277 | GET_AND_SET(TONE_WINDOW_MS) 278 | GET_AND_SET(TONE_THRESHOLD_DB) 279 | } 280 | -------------------------------------------------------------------------------- /spectrum/spectrum.ino: -------------------------------------------------------------------------------- 1 | // Audio Spectrum Display 2 | // Copyright 2013 Tony DiCola (tony@tonydicola.com) 3 | 4 | // This code is part of the guide at http://learn.adafruit.com/fft-fun-with-fourier-transforms/ 5 | 6 | #define ARM_MATH_CM4 7 | #include 8 | #include 9 | 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | // CONIFIGURATION 13 | // These values can be changed to alter the behavior of the spectrum display. 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | int SAMPLE_RATE_HZ = 9000; // Sample rate of the audio in hertz. 17 | float SPECTRUM_MIN_DB = 30.0; // Audio intensity (in decibels) that maps to low LED brightness. 18 | float SPECTRUM_MAX_DB = 60.0; // Audio intensity (in decibels) that maps to high LED brightness. 19 | int LEDS_ENABLED = 1; // Control if the LED's should display the spectrum or not. 1 is true, 0 is false. 20 | // Useful for turning the LED display on and off with commands from the serial port. 21 | const int FFT_SIZE = 256; // Size of the FFT. Realistically can only be at most 256 22 | // without running out of memory for buffers and other state. 23 | const int AUDIO_INPUT_PIN = 14; // Input ADC pin for audio data. 24 | const int ANALOG_READ_RESOLUTION = 10; // Bits of resolution for the ADC. 25 | const int ANALOG_READ_AVERAGING = 16; // Number of samples to average with each ADC reading. 26 | const int POWER_LED_PIN = 13; // Output pin for power LED (pin 13 to use Teensy 3.0's onboard LED). 27 | const int NEO_PIXEL_PIN = 3; // Output pin for neo pixels. 28 | const int NEO_PIXEL_COUNT = 4; // Number of neo pixels. You should be able to increase this without 29 | // any other changes to the program. 30 | const int MAX_CHARS = 65; // Max size of the input command buffer 31 | 32 | 33 | //////////////////////////////////////////////////////////////////////////////// 34 | // INTERNAL STATE 35 | // These shouldn't be modified unless you know what you're doing. 36 | //////////////////////////////////////////////////////////////////////////////// 37 | 38 | IntervalTimer samplingTimer; 39 | float samples[FFT_SIZE*2]; 40 | float magnitudes[FFT_SIZE]; 41 | int sampleCounter = 0; 42 | Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEO_PIXEL_COUNT, NEO_PIXEL_PIN, NEO_GRB + NEO_KHZ800); 43 | char commandBuffer[MAX_CHARS]; 44 | float frequencyWindow[NEO_PIXEL_COUNT+1]; 45 | float hues[NEO_PIXEL_COUNT]; 46 | 47 | 48 | //////////////////////////////////////////////////////////////////////////////// 49 | // MAIN SKETCH FUNCTIONS 50 | //////////////////////////////////////////////////////////////////////////////// 51 | 52 | void setup() { 53 | // Set up serial port. 54 | Serial.begin(38400); 55 | 56 | // Set up ADC and audio input. 57 | pinMode(AUDIO_INPUT_PIN, INPUT); 58 | analogReadResolution(ANALOG_READ_RESOLUTION); 59 | analogReadAveraging(ANALOG_READ_AVERAGING); 60 | 61 | // Turn on the power indicator LED. 62 | pinMode(POWER_LED_PIN, OUTPUT); 63 | digitalWrite(POWER_LED_PIN, HIGH); 64 | 65 | // Initialize neo pixel library and turn off the LEDs 66 | pixels.begin(); 67 | pixels.show(); 68 | 69 | // Clear the input command buffer 70 | memset(commandBuffer, 0, sizeof(commandBuffer)); 71 | 72 | // Initialize spectrum display 73 | spectrumSetup(); 74 | 75 | // Begin sampling audio 76 | samplingBegin(); 77 | } 78 | 79 | void loop() { 80 | // Calculate FFT if a full sample is available. 81 | if (samplingIsDone()) { 82 | // Run FFT on sample data. 83 | arm_cfft_radix4_instance_f32 fft_inst; 84 | arm_cfft_radix4_init_f32(&fft_inst, FFT_SIZE, 0, 1); 85 | arm_cfft_radix4_f32(&fft_inst, samples); 86 | // Calculate magnitude of complex numbers output by the FFT. 87 | arm_cmplx_mag_f32(samples, magnitudes, FFT_SIZE); 88 | 89 | if (LEDS_ENABLED == 1) 90 | { 91 | spectrumLoop(); 92 | } 93 | 94 | // Restart audio sampling. 95 | samplingBegin(); 96 | } 97 | 98 | // Parse any pending commands. 99 | parserLoop(); 100 | } 101 | 102 | 103 | //////////////////////////////////////////////////////////////////////////////// 104 | // UTILITY FUNCTIONS 105 | //////////////////////////////////////////////////////////////////////////////// 106 | 107 | // Compute the average magnitude of a target frequency window vs. all other frequencies. 108 | void windowMean(float* magnitudes, int lowBin, int highBin, float* windowMean, float* otherMean) { 109 | *windowMean = 0; 110 | *otherMean = 0; 111 | // Notice the first magnitude bin is skipped because it represents the 112 | // average power of the signal. 113 | for (int i = 1; i < FFT_SIZE/2; ++i) { 114 | if (i >= lowBin && i <= highBin) { 115 | *windowMean += magnitudes[i]; 116 | } 117 | else { 118 | *otherMean += magnitudes[i]; 119 | } 120 | } 121 | *windowMean /= (highBin - lowBin) + 1; 122 | *otherMean /= (FFT_SIZE / 2 - (highBin - lowBin)); 123 | } 124 | 125 | // Convert a frequency to the appropriate FFT bin it will fall within. 126 | int frequencyToBin(float frequency) { 127 | float binFrequency = float(SAMPLE_RATE_HZ) / float(FFT_SIZE); 128 | return int(frequency / binFrequency); 129 | } 130 | 131 | // Convert from HSV values (in floating point 0 to 1.0) to RGB colors usable 132 | // by neo pixel functions. 133 | uint32_t pixelHSVtoRGBColor(float hue, float saturation, float value) { 134 | // Implemented from algorithm at http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV 135 | float chroma = value * saturation; 136 | float h1 = float(hue)/60.0; 137 | float x = chroma*(1.0-fabs(fmod(h1, 2.0)-1.0)); 138 | float r = 0; 139 | float g = 0; 140 | float b = 0; 141 | if (h1 < 1.0) { 142 | r = chroma; 143 | g = x; 144 | } 145 | else if (h1 < 2.0) { 146 | r = x; 147 | g = chroma; 148 | } 149 | else if (h1 < 3.0) { 150 | g = chroma; 151 | b = x; 152 | } 153 | else if (h1 < 4.0) { 154 | g = x; 155 | b = chroma; 156 | } 157 | else if (h1 < 5.0) { 158 | r = x; 159 | b = chroma; 160 | } 161 | else // h1 <= 6.0 162 | { 163 | r = chroma; 164 | b = x; 165 | } 166 | float m = value - chroma; 167 | r += m; 168 | g += m; 169 | b += m; 170 | return pixels.Color(int(255*r), int(255*g), int(255*b)); 171 | } 172 | 173 | 174 | //////////////////////////////////////////////////////////////////////////////// 175 | // SPECTRUM DISPLAY FUNCTIONS 176 | /////////////////////////////////////////////////////////////////////////////// 177 | 178 | void spectrumSetup() { 179 | // Set the frequency window values by evenly dividing the possible frequency 180 | // spectrum across the number of neo pixels. 181 | float windowSize = (SAMPLE_RATE_HZ / 2.0) / float(NEO_PIXEL_COUNT); 182 | for (int i = 0; i < NEO_PIXEL_COUNT+1; ++i) { 183 | frequencyWindow[i] = i*windowSize; 184 | } 185 | // Evenly spread hues across all pixels. 186 | for (int i = 0; i < NEO_PIXEL_COUNT; ++i) { 187 | hues[i] = 360.0*(float(i)/float(NEO_PIXEL_COUNT-1)); 188 | } 189 | } 190 | 191 | void spectrumLoop() { 192 | // Update each LED based on the intensity of the audio 193 | // in the associated frequency window. 194 | float intensity, otherMean; 195 | for (int i = 0; i < NEO_PIXEL_COUNT; ++i) { 196 | windowMean(magnitudes, 197 | frequencyToBin(frequencyWindow[i]), 198 | frequencyToBin(frequencyWindow[i+1]), 199 | &intensity, 200 | &otherMean); 201 | // Convert intensity to decibels. 202 | intensity = 20.0*log10(intensity); 203 | // Scale the intensity and clamp between 0 and 1.0. 204 | intensity -= SPECTRUM_MIN_DB; 205 | intensity = intensity < 0.0 ? 0.0 : intensity; 206 | intensity /= (SPECTRUM_MAX_DB-SPECTRUM_MIN_DB); 207 | intensity = intensity > 1.0 ? 1.0 : intensity; 208 | pixels.setPixelColor(i, pixelHSVtoRGBColor(hues[i], 1.0, intensity)); 209 | } 210 | pixels.show(); 211 | } 212 | 213 | 214 | //////////////////////////////////////////////////////////////////////////////// 215 | // SAMPLING FUNCTIONS 216 | //////////////////////////////////////////////////////////////////////////////// 217 | 218 | void samplingCallback() { 219 | // Read from the ADC and store the sample data 220 | samples[sampleCounter] = (float32_t)analogRead(AUDIO_INPUT_PIN); 221 | // Complex FFT functions require a coefficient for the imaginary part of the input. 222 | // Since we only have real data, set this coefficient to zero. 223 | samples[sampleCounter+1] = 0.0; 224 | // Update sample buffer position and stop after the buffer is filled 225 | sampleCounter += 2; 226 | if (sampleCounter >= FFT_SIZE*2) { 227 | samplingTimer.end(); 228 | } 229 | } 230 | 231 | void samplingBegin() { 232 | // Reset sample buffer position and start callback at necessary rate. 233 | sampleCounter = 0; 234 | samplingTimer.begin(samplingCallback, 1000000/SAMPLE_RATE_HZ); 235 | } 236 | 237 | boolean samplingIsDone() { 238 | return sampleCounter >= FFT_SIZE*2; 239 | } 240 | 241 | 242 | //////////////////////////////////////////////////////////////////////////////// 243 | // COMMAND PARSING FUNCTIONS 244 | // These functions allow parsing simple commands input on the serial port. 245 | // Commands allow reading and writing variables that control the device. 246 | // 247 | // All commands must end with a semicolon character. 248 | // 249 | // Example commands are: 250 | // GET SAMPLE_RATE_HZ; 251 | // - Get the sample rate of the device. 252 | // SET SAMPLE_RATE_HZ 400; 253 | // - Set the sample rate of the device to 400 hertz. 254 | // 255 | //////////////////////////////////////////////////////////////////////////////// 256 | 257 | void parserLoop() { 258 | // Process any incoming characters from the serial port 259 | while (Serial.available() > 0) { 260 | char c = Serial.read(); 261 | // Add any characters that aren't the end of a command (semicolon) to the input buffer. 262 | if (c != ';') { 263 | c = toupper(c); 264 | strncat(commandBuffer, &c, 1); 265 | } 266 | else 267 | { 268 | // Parse the command because an end of command token was encountered. 269 | parseCommand(commandBuffer); 270 | // Clear the input buffer 271 | memset(commandBuffer, 0, sizeof(commandBuffer)); 272 | } 273 | } 274 | } 275 | 276 | // Macro used in parseCommand function to simplify parsing get and set commands for a variable 277 | #define GET_AND_SET(variableName) \ 278 | else if (strcmp(command, "GET " #variableName) == 0) { \ 279 | Serial.println(variableName); \ 280 | } \ 281 | else if (strstr(command, "SET " #variableName " ") != NULL) { \ 282 | variableName = (typeof(variableName)) atof(command+(sizeof("SET " #variableName " ")-1)); \ 283 | } 284 | 285 | void parseCommand(char* command) { 286 | if (strcmp(command, "GET MAGNITUDES") == 0) { 287 | for (int i = 0; i < FFT_SIZE; ++i) { 288 | Serial.println(magnitudes[i]); 289 | } 290 | } 291 | else if (strcmp(command, "GET SAMPLES") == 0) { 292 | for (int i = 0; i < FFT_SIZE*2; i+=2) { 293 | Serial.println(samples[i]); 294 | } 295 | } 296 | else if (strcmp(command, "GET FFT_SIZE") == 0) { 297 | Serial.println(FFT_SIZE); 298 | } 299 | GET_AND_SET(SAMPLE_RATE_HZ) 300 | GET_AND_SET(LEDS_ENABLED) 301 | GET_AND_SET(SPECTRUM_MIN_DB) 302 | GET_AND_SET(SPECTRUM_MAX_DB) 303 | 304 | // Update spectrum display values if sample rate was changed. 305 | if (strstr(command, "SET SAMPLE_RATE_HZ ") != NULL) { 306 | spectrumSetup(); 307 | } 308 | 309 | // Turn off the LEDs if the state changed. 310 | if (LEDS_ENABLED == 0) { 311 | for (int i = 0; i < NEO_PIXEL_COUNT; ++i) { 312 | pixels.setPixelColor(i, 0); 313 | } 314 | pixels.show(); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /SpectrogramUI.py: -------------------------------------------------------------------------------- 1 | # SpectrogramUI.py 2 | # Copyright 2013 Tony DiCola (tony@tonydicola.com) 3 | 4 | # User interface for Spectrogram program. 5 | 6 | import matplotlib 7 | matplotlib.use('Qt4Agg') 8 | matplotlib.rcParams['backend.qt4']='PySide' 9 | from matplotlib.animation import FuncAnimation 10 | from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas 11 | from matplotlib.cm import get_cmap 12 | from matplotlib.figure import Figure 13 | from matplotlib.gridspec import GridSpec 14 | from matplotlib.ticker import MultipleLocator, FuncFormatter 15 | import numpy as np 16 | from PySide import QtCore, QtGui 17 | 18 | VERSION = 'Spectrogram v1.0' 19 | 20 | # Add global version number/name 21 | 22 | class SpectrogramCanvas(FigureCanvas): 23 | def __init__(self, window): 24 | """Initialize spectrogram canvas graphs.""" 25 | # Initialize variables to default values. 26 | self.window = window 27 | self.samples = 100 # Number of samples to store 28 | self.fftSize = 256 # Initial FFT size just to render something in the charts 29 | self.sampleRate = 0 30 | self.binFreq = 0 31 | self.binCount = self.fftSize/2 32 | self.graphUpdateHz = 10 # Update rate of the animation 33 | self.coloredBin = None 34 | self.magnitudes = np.zeros((self.samples, self.binCount)) 35 | # Tell numpy to ignore errors like taking the log of 0 36 | np.seterr(all='ignore') 37 | # Set up figure to hold plots 38 | self.figure = Figure(figsize=(1024,768), dpi=72, facecolor=(1,1,1), edgecolor=(0,0,0)) 39 | # Set up 4x4 grid to hold 2 plots and colorbar 40 | gs = GridSpec(2, 2, height_ratios=[1,2], width_ratios=[9.5, 0.5]) 41 | gs.update(left=0.075, right=0.925, bottom=0.05, top=0.95, wspace=0.05) 42 | # Set up frequency histogram bar plot 43 | self.histAx = self.figure.add_subplot(gs[0]) 44 | self.histAx.set_title('Frequency Histogram') 45 | self.histAx.set_ylabel('Intensity (decibels)') 46 | self.histAx.set_xlabel('Frequency Bin (hz)') 47 | self.histAx.set_xticks([]) 48 | self.histPlot = self.histAx.bar(np.arange(self.binCount), np.zeros(self.binCount), width=1.0, linewidth=0.0, facecolor='blue') 49 | # Set up spectrogram waterfall plot 50 | self.spectAx = self.figure.add_subplot(gs[2]) 51 | self.spectAx.set_title('Spectrogram') 52 | self.spectAx.set_ylabel('Sample Age (seconds)') 53 | self.spectAx.set_xlabel('Frequency Bin (hz)') 54 | self.spectAx.set_xticks([]) 55 | self.spectPlot = self.spectAx.imshow(self.magnitudes, aspect='auto', cmap=get_cmap('jet')) 56 | # Add formatter to translate position to age in seconds 57 | self.spectAx.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: '%d' % (x*(1.0/self.graphUpdateHz)))) 58 | # Set up spectrogram color bar 59 | cbAx = self.figure.add_subplot(gs[3]) 60 | self.figure.colorbar(self.spectPlot, cax=cbAx, use_gridspec=True, format=FuncFormatter(lambda x, pos: '%d' % (x*100.0))) 61 | cbAx.set_ylabel('Intensity (decibels)') 62 | # Initialize canvas 63 | super(SpectrogramCanvas, self).__init__(self.figure) 64 | # Hook up mouse and animation events 65 | self.mpl_connect('motion_notify_event', self._mouseMove) 66 | self.ani = FuncAnimation(self.figure, self._update, interval=1000.0/self.graphUpdateHz, blit=False) 67 | 68 | def updateParameters(self, fftSize, sampleRate): 69 | """Update the FFT size and sample rate parameters to redraw the charts appropriately.""" 70 | # Update variables to new values. 71 | self.fftSize = fftSize 72 | self.sampleRate = sampleRate 73 | self.binCount = self.fftSize/2 74 | self.binFreq = self.sampleRate/float(self.fftSize) 75 | # Remove old bar plot. 76 | for bar in self.histPlot: 77 | bar.remove() 78 | # Update data for charts. 79 | self.histPlot = self.histAx.bar(np.arange(self.binCount), np.zeros(self.binCount), width=1.0, linewidth=0.0, facecolor='blue') 80 | self.magnitudes = np.zeros((self.samples, self.binCount)) 81 | # Update frequency x axis to have 5 evenly spaced ticks from 0 to sampleRate/2. 82 | ticks = np.floor(np.linspace(0, self.binCount, 5)) 83 | labels = ['%d hz' % i for i in np.linspace(0, self.sampleRate/2.0, 5)] 84 | self.histAx.set_xticks(ticks) 85 | self.histAx.set_xticklabels(labels) 86 | self.spectAx.set_xticks(ticks) 87 | self.spectAx.set_xticklabels(labels) 88 | 89 | def updateIntensityRange(self, low, high): 90 | """Adjust low and high intensity limits for histogram and spectrum axes.""" 91 | self.histAx.set_ylim(bottom=low, top=high) 92 | self.spectPlot.set_clim(low/100.0, high/100.0) 93 | 94 | def _mouseMove(self, event): 95 | # Update the selected frequency bin if the mouse is over a plot. 96 | # Check if sampleRate is not 0 so the status bar isn't updated if the spectrogram hasn't ever been started. 97 | if self.sampleRate != 0 and (event.inaxes == self.histAx or event.inaxes == self.spectAx): 98 | bin = int(event.xdata) 99 | self.window.updateStatus('Frequency bin %d: %.0f hz to %.0f hz' % (bin, bin*self.binFreq, (bin+1)*self.binFreq)) 100 | # Highlight selected frequency in red 101 | if self.coloredBin != None: 102 | self.histPlot[self.coloredBin].set_facecolor('blue') 103 | self.histPlot[bin].set_facecolor('red') 104 | self.coloredBin = bin 105 | else: 106 | if self.coloredBin != None: 107 | self.histPlot[self.coloredBin].set_facecolor('blue') 108 | self.window.updateStatus() 109 | 110 | def _update(self, *fargs): 111 | # Animation function called 10 times a second to update graphs with recent data. 112 | # Get a list of recent magnitudes from the open device. 113 | mags = self.window.getMagnitudes() 114 | if mags != None: 115 | # Convert magnitudes to decibels. Also skip the first value because it's 116 | # the average power of the signal, and only grab the first half of values 117 | # because the second half is for negative frequencies (which don't apply 118 | # to an FFT run on real data). 119 | mags = 20.0*np.log10(mags[1:len(mags)/2+1]) 120 | # Update histogram bar heights based on magnitudes. 121 | for bin, mag in zip(self.histPlot, mags): 122 | bin.set_height(mag) 123 | # Roll samples forward and save the most recent sample. Note that image 124 | # samples are scaled to 0 to 1. 125 | self.magnitudes = np.roll(self.magnitudes, 1, axis=0) 126 | self.magnitudes[0] = mags/100.0 127 | # Update spectrogram image data. 128 | self.spectPlot.set_array(self.magnitudes) 129 | return (self.histPlot, self.spectPlot) 130 | else: 131 | return () 132 | 133 | 134 | class MainWindow(QtGui.QMainWindow): 135 | def __init__(self, devices): 136 | """Set up the main window. 137 | Devices should be a list of items that implement the SpectrogramDevice interface. 138 | """ 139 | super(MainWindow, self).__init__() 140 | self.devices = devices 141 | self.openDevice = None 142 | main = QtGui.QWidget() 143 | main.setLayout(self._setupMainLayout()) 144 | self.setCentralWidget(main) 145 | self.status = self.statusBar() 146 | self.setGeometry(10,10,1024,768) 147 | self.setWindowTitle(VERSION) 148 | self._sliderChanged(0) # Force graphs to update their limits with initial values. 149 | self.show() 150 | 151 | def closeEvent(self, event): 152 | # Close the serial port before exiting. 153 | if self.isDeviceOpen(): 154 | self.openDevice.close() 155 | event.accept() 156 | 157 | def updateStatus(self, message=''): 158 | """Update the status bar of the widnow with the provided message text.""" 159 | self.status.showMessage(message) 160 | 161 | def getMagnitudes(self): 162 | """Get a list of magnitudes if the device is open, or None if the device is not open. 163 | There are FFT size number of magnitudes. 164 | """ 165 | try: 166 | if self.isDeviceOpen(): 167 | return self.openDevice.get_magnitudes() 168 | except IOError as e: 169 | self._communicationError(e) 170 | return None 171 | 172 | def isDeviceOpen(self): 173 | """Return True if device is open.""" 174 | return self.openDevice != None 175 | 176 | def _communicationError(self, error): 177 | # Error communicating with device, shut it down if possible. 178 | mb = QtGui.QMessageBox() 179 | mb.setText('Error communicating with device! %s' % error) 180 | mb.exec_() 181 | self._closeDevice() 182 | 183 | def _setupMainLayout(self): 184 | controls = QtGui.QVBoxLayout() 185 | controls.addWidget(QtGui.QLabel('

%s

' % VERSION)) 186 | author = QtGui.QLabel('by Tony DiCola') 187 | author.setOpenExternalLinks(True) 188 | controls.addWidget(author) 189 | controls.addSpacing(10) 190 | for control in self._setupControls(): 191 | controls.addWidget(control) 192 | controls.addStretch(1) 193 | layout = QtGui.QHBoxLayout() 194 | layout.addLayout(controls) 195 | self.spectrogram = SpectrogramCanvas(self) 196 | layout.addWidget(self.spectrogram) 197 | return layout 198 | 199 | def _setupControls(self): 200 | # Set up device group 201 | deviceCombo = QtGui.QComboBox() 202 | for device in sorted(self.devices, lambda a, b: cmp(a.get_name(), b.get_name())): 203 | deviceCombo.addItem(device.get_name(), userData=device) 204 | deviceBtn = QtGui.QPushButton('Open') 205 | deviceBtn.clicked.connect(self._deviceButton) 206 | self.deviceCombo = deviceCombo 207 | self.deviceBtn = deviceBtn 208 | device = QtGui.QGroupBox('Device') 209 | device.setLayout(QtGui.QGridLayout()) 210 | device.layout().addWidget(QtGui.QLabel('Serial Port:'), 0, 0) 211 | device.layout().addWidget(deviceCombo, 0, 1) 212 | device.layout().addWidget(deviceBtn, 1, 1) 213 | # Set up device parameters group 214 | fftSize = QtGui.QLabel() 215 | sampleRate = QtGui.QLabel() 216 | modifyBtn = QtGui.QPushButton('Modify') 217 | modifyBtn.clicked.connect(self._modifyButton) 218 | self.fftSize = fftSize 219 | self.sampleRate = sampleRate 220 | self.modifyBtn = modifyBtn 221 | parameters = QtGui.QGroupBox('Device Parameters') 222 | parameters.setLayout(QtGui.QGridLayout()) 223 | parameters.layout().addWidget(QtGui.QLabel('FFT Size:'), 0, 0) 224 | parameters.layout().addWidget(fftSize, 0, 1) 225 | parameters.layout().addWidget(QtGui.QLabel('Sample Rate:'), 1, 0) 226 | parameters.layout().addWidget(sampleRate, 1, 1) 227 | parameters.layout().addWidget(modifyBtn, 2, 1) 228 | parameters.setDisabled(True) 229 | self.parameters = parameters 230 | # Set up graph values group 231 | lowSlider = QtGui.QSlider(QtCore.Qt.Orientation.Horizontal) 232 | highSlider = QtGui.QSlider(QtCore.Qt.Orientation.Horizontal) 233 | lowSlider.setRange(0, 100) 234 | lowSlider.setValue(20) 235 | lowSlider.valueChanged.connect(self._sliderChanged) 236 | highSlider.setRange(0, 100) 237 | highSlider.setValue(60) 238 | highSlider.valueChanged.connect(self._sliderChanged) 239 | self.lowSlider = lowSlider 240 | self.highSlider = highSlider 241 | self.lowValue = QtGui.QLabel() 242 | self.highValue = QtGui.QLabel() 243 | graphs = QtGui.QGroupBox('Graphs') 244 | graphs.setLayout(QtGui.QGridLayout()) 245 | graphs.layout().addWidget(QtGui.QLabel('Intensity Min:'), 0, 0) 246 | graphs.layout().addWidget(self.lowValue, 0, 1) 247 | graphs.layout().addWidget(lowSlider, 1, 0, 1, 2) 248 | graphs.layout().addWidget(QtGui.QLabel('Intensity Max:'), 2, 0) 249 | graphs.layout().addWidget(self.highValue, 2, 1) 250 | graphs.layout().addWidget(highSlider, 3, 0, 1, 2) 251 | return (device, parameters, graphs) 252 | 253 | def _deviceButton(self): 254 | # Toggle between opening and closing the device. 255 | if not self.isDeviceOpen(): 256 | self._openDevice() 257 | else: 258 | self._closeDevice() 259 | 260 | def _modifyButton(self): 261 | # Create dialog 262 | dialog = QtGui.QDialog(self) 263 | dialog.setModal(True) 264 | sampleRate = QtGui.QSpinBox() 265 | sampleRate.setRange(1,9000) 266 | sampleRate.setValue(self.openDevice.get_samplerate()) 267 | buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) 268 | buttons.accepted.connect(dialog.accept) 269 | buttons.rejected.connect(dialog.reject) 270 | dialog.setLayout(QtGui.QVBoxLayout()) 271 | dialog.layout().addWidget(QtGui.QLabel('Sample Rate (hz):')) 272 | dialog.layout().addWidget(sampleRate) 273 | dialog.layout().addWidget(buttons) 274 | dialog.setWindowTitle('Modify Device') 275 | # Show dialog and update device & UI based on results. 276 | try: 277 | if dialog.exec_() == QtGui.QDialog.Accepted: 278 | self.openDevice.set_samplerate(sampleRate.value()) 279 | self._updateDeviceUI() 280 | except IOError as e: 281 | self._communicationError(e) 282 | 283 | def _updateDeviceUI(self): 284 | # Update UI to reflect current state of device. 285 | sampleRate = self.openDevice.get_samplerate() 286 | fftSize = self.openDevice.get_fftsize() 287 | self.fftSize.setText('%d' % fftSize) 288 | self.sampleRate.setText('%d hz' % sampleRate) 289 | self.spectrogram.updateParameters(fftSize, sampleRate) 290 | 291 | def _openDevice(self): 292 | try: 293 | # Open communication with selected device. 294 | device = self.deviceCombo.itemData(self.deviceCombo.currentIndex()) 295 | self.updateStatus('Opening device %s...' % device.get_name()) 296 | device.open() 297 | self.openDevice = device 298 | self.updateStatus('Communication with device %s established.' % device.get_name()) 299 | # Update UI with data from open device. 300 | self._updateDeviceUI() 301 | self.parameters.setDisabled(False) 302 | self.deviceCombo.setDisabled(True) 303 | self.deviceBtn.setText('Close') 304 | except IOError as e: 305 | self.updateStatus() # Clear status bar 306 | self._communicationError(e) 307 | 308 | def _closeDevice(self): 309 | try: 310 | # Close the device if it's open. 311 | if self.isDeviceOpen(): 312 | self.openDevice.close() 313 | self.openDevice = None 314 | # Update UI to show device is closed. 315 | self.parameters.setDisabled(True) 316 | self.deviceCombo.setDisabled(False) 317 | self.deviceBtn.setText('Open') 318 | except IOError as e: 319 | self._communicationError(e) 320 | 321 | def _sliderChanged(self, value): 322 | low = self.lowSlider.value() 323 | high = self.highSlider.value() 324 | # Adjust slider ranges to allowed values. 325 | self.lowSlider.setRange(0, high) 326 | self.highSlider.setRange(low, 100) 327 | # Update UI to represent new slider values 328 | self.lowValue.setText('%d dB' % low) 329 | self.highValue.setText('%d dB' % high) 330 | # Adjust chart UI with new slider values. 331 | self.spectrogram.updateIntensityRange(low, high) 332 | --------------------------------------------------------------------------------