├── .gitignore ├── LICENSE ├── Makefile ├── README.md └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # External libraries 2 | lib 3 | 4 | # Executable 5 | cppaudiocapture 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chris Rouck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Name of the executable 2 | EXEC = cppaudiocapture 3 | 4 | # Include PortAudio and FFTW headers and library archives 5 | CLIB = -I./lib/portaudio/include ./lib/portaudio/lib/.libs/libportaudio.a \ 6 | -lrt -lasound -ljack -pthread -I./lib/fftw-3.3.10/api -lfftw3 7 | 8 | $(EXEC): main.cpp 9 | g++ -o $@ $^ $(CLIB) 10 | 11 | install-deps: install-portaudio install-fftw 12 | .PHONY: install-deps 13 | 14 | uninstall-deps: uninstall-portaudio uninstall-fftw 15 | .PHONY: uninstall-deps 16 | 17 | install-portaudio: 18 | mkdir -p lib 19 | 20 | curl https://files.portaudio.com/archives/pa_stable_v190700_20210406.tgz | tar -zx -C lib 21 | cd lib/portaudio && ./configure && $(MAKE) -j 22 | .PHONY: install-portaudio 23 | 24 | uninstall-portaudio: 25 | cd lib/portaudio && $(MAKE) uninstall 26 | rm -rf lib/portaudio 27 | .PHONY: uninstall-portaudio 28 | 29 | install-fftw: 30 | mkdir -p lib 31 | 32 | curl http://www.fftw.org/fftw-3.3.10.tar.gz | tar -zx -C lib 33 | cd lib/fftw-3.3.10 && ./configure && $(MAKE) -j && sudo $(MAKE) install 34 | .PHONY: install-fftw 35 | 36 | uninstall-fftw: 37 | cd lib/fftw-3.3.10 && $(MAKE) uninstall 38 | rm -rf lib/fftw-3.3.10 39 | .PHONY: uninstall-fftw 40 | 41 | clean: 42 | rm -f $(EXEC) 43 | .PHONY: clean 44 | 45 | clean-all: clean 46 | rm -rf lib 47 | .PHONY: clean-all 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Capture Audio in C++ 2 | 3 | This repository stores the code that was written for the associated YouTube 4 | tutorial series on my [YouTube channel](https://www.youtube.com/@chrisrouck). 5 | 6 | ## Release structure 7 | 8 | The YouTube tutorial is split into two parts: 9 | 10 | 1. Capture realtime audio from the computer and display its left and right 11 | channels as volume bars in the terminal 12 | - **Video**: https://youtu.be/jpsJCji71Ec 13 | - **Source code**: https://github.com/chrisrouck/tutorial-cpp-audio-capture/releases/tag/v1.0.0 14 | 2. Gather frequency data from the audio capture and display it as a spectrogram 15 | in the terminal 16 | - **Video**: https://youtu.be/yt7i4zPbVDs 17 | - **Source code**: https://github.com/chrisrouck/tutorial-cpp-audio-capture/releases/tag/v2.0.0 18 | 19 | ## Feedback 20 | 21 | I welcome constructive feedback on this repository! It can help me and the rest 22 | of the community to become better programmers. 23 | 24 | If you would like to ask **questions** or provide other **comments**, feel free to post 25 | to this repository's [Discussions 26 | page](https://github.com/chrisrouck/tutorial-cpp-audio-capture/discussions). 27 | 28 | If you would like to **report any bugs** or **suggest changes to the code**, you 29 | can [create an 30 | issue](https://github.com/chrisrouck/tutorial-cpp-audio-capture/issues/new/choose). 31 | Current and past issues can be found 32 | [here](https://github.com/chrisrouck/tutorial-cpp-audio-capture/issues). 33 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include // PortAudio: Used for audio capture 7 | #include // FFTW: Provides a discrete FFT algorithm to get 8 | // frequency data from captured audio 9 | 10 | #define SAMPLE_RATE 44100.0 // How many audio samples to capture every second (44100 Hz is standard) 11 | #define FRAMES_PER_BUFFER 512 // How many audio samples to send to our callback function for each channel 12 | #define NUM_CHANNELS 2 // Number of audio channels to capture 13 | 14 | #define SPECTRO_FREQ_START 20 // Lower bound of the displayed spectrogram (Hz) 15 | #define SPECTRO_FREQ_END 20000 // Upper bound of the displayed spectrogram (Hz) 16 | 17 | // Define our callback data (data that is passed to every callback function call) 18 | typedef struct { 19 | double* in; // Input buffer, will contain our audio sample 20 | double* out; // Output buffer, FFTW will write to this based on the input buffer's contents 21 | fftw_plan p; // Created by FFTW to facilitate FFT calculation 22 | int startIndex; // First index of our FFT output to display in the spectrogram 23 | int spectroSize; // Number of elements in our FFT output to display from the start index 24 | } streamCallbackData; 25 | 26 | // Callback data, persisted between calls. Allows us to access the data it 27 | // contains from within the callback function. 28 | static streamCallbackData* spectroData; 29 | 30 | // Checks the return value of a PortAudio function. Logs the message and exits 31 | // if there was an error 32 | static void checkErr(PaError err) { 33 | if (err != paNoError) { 34 | printf("PortAudio error: %s\n", Pa_GetErrorText(err)); 35 | exit(EXIT_FAILURE); 36 | } 37 | } 38 | 39 | // Returns the lowest of the two given numbers 40 | static inline float min(float a, float b) { 41 | return a < b ? a : b; 42 | } 43 | 44 | // PortAudio stream callback function. Will be called after every 45 | // `FRAMES_PER_BUFFER` audio samples PortAudio captures. Used to process the 46 | // resulting audio sample. 47 | static int streamCallback( 48 | const void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer, 49 | const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, 50 | void* userData 51 | ) { 52 | // Cast our input buffer to a float pointer (since our sample format is `paFloat32`) 53 | float* in = (float*)inputBuffer; 54 | 55 | // We will not be modifying the output buffer. This line is a no-op. 56 | (void)outputBuffer; 57 | 58 | // Cast our user data to streamCallbackData* so we can access its struct members 59 | streamCallbackData* callbackData = (streamCallbackData*)userData; 60 | 61 | // Set our spectrogram size in the terminal to 100 characters, and move the 62 | // cursor to the beginning of the line 63 | int dispSize = 100; 64 | printf("\r"); 65 | 66 | // Copy audio sample to FFTW's input buffer 67 | for (unsigned long i = 0; i < framesPerBuffer; i++) { 68 | callbackData->in[i] = in[i * NUM_CHANNELS]; 69 | } 70 | 71 | // Perform FFT on callbackData->in (results will be stored in callbackData->out) 72 | fftw_execute(callbackData->p); 73 | 74 | // Draw the spectrogram 75 | for (int i = 0; i < dispSize; i++) { 76 | // Sample frequency data logarithmically 77 | double proportion = std::pow(i / (double)dispSize, 4); 78 | double freq = callbackData->out[(int)(callbackData->startIndex 79 | + proportion * callbackData->spectroSize)]; 80 | 81 | // Display full block characters with heights based on frequency intensity 82 | if (freq < 0.125) { 83 | printf("▁"); 84 | } else if (freq < 0.25) { 85 | printf("▂"); 86 | } else if (freq < 0.375) { 87 | printf("▃"); 88 | } else if (freq < 0.5) { 89 | printf("▄"); 90 | } else if (freq < 0.625) { 91 | printf("▅"); 92 | } else if (freq < 0.75) { 93 | printf("▆"); 94 | } else if (freq < 0.875) { 95 | printf("▇"); 96 | } else { 97 | printf("█"); 98 | } 99 | } 100 | 101 | // Display the buffered changes to stdout in the terminal 102 | fflush(stdout); 103 | 104 | return 0; 105 | } 106 | 107 | int main() { 108 | // Initialize PortAudio 109 | PaError err; 110 | err = Pa_Initialize(); 111 | checkErr(err); 112 | 113 | // Allocate and define the callback data used to calculate/display the spectrogram 114 | spectroData = (streamCallbackData*)malloc(sizeof(streamCallbackData)); 115 | spectroData->in = (double*)malloc(sizeof(double) * FRAMES_PER_BUFFER); 116 | spectroData->out = (double*)malloc(sizeof(double) * FRAMES_PER_BUFFER); 117 | if (spectroData->in == NULL || spectroData->out == NULL) { 118 | printf("Could not allocate spectro data\n"); 119 | exit(EXIT_FAILURE); 120 | } 121 | spectroData->p = fftw_plan_r2r_1d( 122 | FRAMES_PER_BUFFER, spectroData->in, spectroData->out, 123 | FFTW_R2HC, FFTW_ESTIMATE 124 | ); 125 | double sampleRatio = FRAMES_PER_BUFFER / SAMPLE_RATE; 126 | spectroData->startIndex = std::ceil(sampleRatio * SPECTRO_FREQ_START); 127 | spectroData->spectroSize = min( 128 | std::ceil(sampleRatio * SPECTRO_FREQ_END), 129 | FRAMES_PER_BUFFER / 2.0 130 | ) - spectroData->startIndex; 131 | 132 | // Get and display the number of audio devices accessible to PortAudio 133 | int numDevices = Pa_GetDeviceCount(); 134 | printf("Number of devices: %d\n", numDevices); 135 | 136 | if (numDevices < 0) { 137 | printf("Error getting device count.\n"); 138 | exit(EXIT_FAILURE); 139 | } else if (numDevices == 0) { 140 | printf("There are no available audio devices on this machine.\n"); 141 | exit(EXIT_SUCCESS); 142 | } 143 | 144 | // Display audio device information for each device accessible to PortAudio 145 | const PaDeviceInfo* deviceInfo; 146 | for (int i = 0; i < numDevices; i++) { 147 | deviceInfo = Pa_GetDeviceInfo(i); 148 | printf("Device %d:\n", i); 149 | printf(" name: %s\n", deviceInfo->name); 150 | printf(" maxInputChannels: %d\n", deviceInfo->maxInputChannels); 151 | printf(" maxOutputChannels: %d\n", deviceInfo->maxOutputChannels); 152 | printf(" defaultSampleRate: %f\n", deviceInfo->defaultSampleRate); 153 | } 154 | 155 | // Use device 0 (for a programmatic solution for choosing a device, 156 | // `numDevices - 1` is typically the 'default' device 157 | int device = 0; 158 | 159 | // Define stream capture specifications 160 | PaStreamParameters inputParameters; 161 | memset(&inputParameters, 0, sizeof(inputParameters)); 162 | inputParameters.channelCount = NUM_CHANNELS; 163 | inputParameters.device = device; 164 | inputParameters.hostApiSpecificStreamInfo = NULL; 165 | inputParameters.sampleFormat = paFloat32; 166 | inputParameters.suggestedLatency = Pa_GetDeviceInfo(device)->defaultLowInputLatency; 167 | 168 | // Open the PortAudio stream 169 | PaStream* stream; 170 | err = Pa_OpenStream( 171 | &stream, 172 | &inputParameters, 173 | NULL, 174 | SAMPLE_RATE, 175 | FRAMES_PER_BUFFER, 176 | paNoFlag, 177 | streamCallback, 178 | spectroData 179 | ); 180 | checkErr(err); 181 | 182 | // Begin capturing audio 183 | err = Pa_StartStream(stream); 184 | checkErr(err); 185 | 186 | // Wait 30 seconds (PortAudio will continue to capture audio) 187 | Pa_Sleep(30 * 1000); 188 | 189 | // Stop capturing audio 190 | err = Pa_StopStream(stream); 191 | checkErr(err); 192 | 193 | // Close the PortAudio stream 194 | err = Pa_CloseStream(stream); 195 | checkErr(err); 196 | 197 | // Terminate PortAudio 198 | err = Pa_Terminate(); 199 | checkErr(err); 200 | 201 | // Free allocated resources used for FFT calculation 202 | fftw_destroy_plan(spectroData->p); 203 | fftw_free(spectroData->in); 204 | fftw_free(spectroData->out); 205 | free(spectroData); 206 | 207 | printf("\n"); 208 | 209 | return EXIT_SUCCESS; 210 | } 211 | --------------------------------------------------------------------------------