From 039e24ec4496cccade2f7b1366bbcbfac44b2486 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 29 Jan 2020 23:26:20 +0100 Subject: [PATCH] Optionally use PortAudio as an audio back-end --- .travis.yml | 23 +++ CMakeLists.txt | 15 +- README.md | 8 +- cmake/FindMp3Lame.cmake | 2 + config.h.in | 15 +- src/main.c | 322 +++++++++++++++++++++++++++------------- 6 files changed, 277 insertions(+), 108 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6e3e61e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +sudo: false +dist: xenial + +language: c + +addons: + apt: + packages: + - libasound2-dev + - libmp3lame-dev + - libsndfile1-dev + - libvorbis-dev + - portaudio19-dev + +before_script: + - mkdir build && cd build + +script: + - cmake .. && make + - cmake .. -DCMAKE_BUILD_TYPE=Debug && make + - cmake .. -DCMAKE_BUILD_TYPE=Release && make + - cmake .. -DENABLE_SNDFILE=ON -DENABLE_MP3LAME=ON -DENABLE_VORBIS=ON && make + - cmake .. -DENABLE_PORTAUDIO=ON && make diff --git a/CMakeLists.txt b/CMakeLists.txt index a0942ed..e98cfa4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,12 @@ if(CMAKE_BUILD_TYPE MATCHES Debug) set(DEBUG TRUE) endif() +if(CMAKE_SYSTEM_NAME MATCHES Linux) + option(ENABLE_PORTAUDIO "Use PortAudio as an audio back-end.") +else() + set(ENABLE_PORTAUDIO ON) +endif() + option(ENABLE_MP3LAME "Enable MP3 support.") option(ENABLE_SNDFILE "Enable WAV support.") option(ENABLE_VORBIS "Enable OGG support.") @@ -40,8 +46,13 @@ target_link_libraries(svar m) find_package(Threads REQUIRED) target_link_libraries(svar Threads::Threads) -pkg_check_modules(ALSA REQUIRED IMPORTED_TARGET alsa) -target_link_libraries(svar PkgConfig::ALSA) +if(ENABLE_PORTAUDIO) + pkg_check_modules(PortAudio REQUIRED IMPORTED_TARGET portaudio-2.0) + target_link_libraries(svar PkgConfig::PortAudio) +else() + pkg_check_modules(ALSA REQUIRED IMPORTED_TARGET alsa) + target_link_libraries(svar PkgConfig::ALSA) +endif() if(ENABLE_SNDFILE) pkg_check_modules(SNDFile REQUIRED IMPORTED_TARGET sndfile) diff --git a/README.md b/README.md index d430495..dd56c39 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,12 @@ SVAR - Simple Voice Activated Recorder It is a simple console application (low memory footprint and CPU usage) designed for recording audio when a specified signal level is exceeded. It is commonly known solution called Voice Operated Recording (VOR). When the signal level is low for longer than the fadeout time, audio -recording is paused. Capturing the audio signal is based on the -[ALSA](http://www.alsa-project.org/) technology, so it should work on all modern Linux systems. +recording is paused. + +On Linux systems, capturing the audio signal is based on the [ALSA](http://www.alsa-project.org/) +technology. For all other systems, [PortAudio](http://www.portaudio.com/) library will be used. +Alternatively, it is possible to force PortAudio back-end on Linux systems by adding +`-DENABLE_PORTAUDIO=ON` to the CMake configuration step. Currently this application supports four output formats: - RAW (PCM 16bit interleaved) diff --git a/cmake/FindMp3Lame.cmake b/cmake/FindMp3Lame.cmake index f12a346..3678de7 100644 --- a/cmake/FindMp3Lame.cmake +++ b/cmake/FindMp3Lame.cmake @@ -3,6 +3,8 @@ find_path(LAME_INCLUDE_DIR lame/lame.h) find_library(LAME_LIBRARIES NAMES mp3lame) +mark_as_advanced(LAME_INCLUDE_DIR) + if(LAME_INCLUDE_DIR AND LAME_LIBRARIES) add_library(Mp3Lame::Mp3Lame INTERFACE IMPORTED) diff --git a/config.h.in b/config.h.in index 2acdae5..ece5e07 100644 --- a/config.h.in +++ b/config.h.in @@ -6,11 +6,20 @@ /* Define to 1 if debugging is enabled. */ #cmakedefine DEBUG 1 -/* Define to 1 if MP3LAME is enabled. */ +/* Define to 1 if PortAudio is enabled. */ +#cmakedefine ENABLE_PORTAUDIO 1 + +/* Define to 1 if Mp3Lame is enabled. */ #cmakedefine ENABLE_MP3LAME 1 -/* Define to 1 if SNDFILE is enabled. */ +/* Define to 1 if SNDFile is enabled. */ #cmakedefine ENABLE_SNDFILE 1 -/* Define to 1 if OGG is enabled. */ +/* Define to 1 if Ogg Vorbis is enabled. */ #cmakedefine ENABLE_VORBIS 1 + +/* Define to the name of this project. */ +#define PROJECT_NAME "@PROJECT_NAME@" + +/* Define to the version of this project. */ +#define PROJECT_VERSION "@PROJECT_VERSION@" diff --git a/src/main.c b/src/main.c index 604b655..6ae6fe0 100644 --- a/src/main.c +++ b/src/main.c @@ -26,7 +26,12 @@ #include #include -#include +#if ENABLE_PORTAUDIO +# include +#else +# include +#endif + #if ENABLE_MP3LAME # include "writer_mp3lame.h" #endif @@ -73,8 +78,8 @@ static struct appconfig_t { char *banner; /* capturing PCM device */ - snd_pcm_t *pcm; char pcm_device[25]; + int pcm_device_id; unsigned int pcm_channels; unsigned int pcm_rate; @@ -109,6 +114,7 @@ static struct appconfig_t { .banner = "SVAR - Simple Voice Activated Recorder", .pcm_device = "default", + .pcm_device_id = 0, .pcm_channels = 1, .pcm_rate = 44100, @@ -149,8 +155,9 @@ static void main_loop_stop(int sig) { main_loop_on = false; } -/* Set hardware parameters. */ -static int set_hw_params(snd_pcm_t *pcm, char **msg) { +#if !ENABLE_PORTAUDIO +/* Set ALSA hardware parameters. */ +static int pcm_set_hw_params(snd_pcm_t *pcm, char **msg) { snd_pcm_hw_params_t *params; char buf[256]; @@ -193,6 +200,7 @@ static int set_hw_params(snd_pcm_t *pcm, char **msg) { *msg = strdup(buf); return err; } +#endif /* Return the name of a given output format. */ static const char *get_output_format_name(enum output_format format) { @@ -206,10 +214,9 @@ static const char *get_output_format_name(enum output_format format) { /* Print some information about the audio device and its configuration. */ static void print_audio_info(void) { printf("Selected PCM device: %s\n" - "Hardware parameters: %iHz, %s, %i channel%s\n", + "Hardware parameters: %d Hz, S16LE, %d channel%s\n", appconfig.pcm_device, appconfig.pcm_rate, - snd_pcm_format_name(SND_PCM_FORMAT_S16_LE), appconfig.pcm_channels, appconfig.pcm_channels > 1 ? "s" : ""); if (!appconfig.signal_meter) printf("Output file format: %s\n", @@ -225,7 +232,26 @@ static void print_audio_info(void) { appconfig.bitrate_max / 1000); } -/* Calculate max peak and amplitude RMSD (based on all channels). */ +#if ENABLE_PORTAUDIO +static void pa_list_devices(void) { + + PaDeviceIndex count = Pa_GetDeviceCount(); + unsigned int len = ceil(log10(count)); + const PaDeviceInfo *info; + PaDeviceIndex i; + + printf(" %*s:\t%s\n", 1 + len, "ID", "Name"); + for (i = 0; i < count; i++) { + if ((info = Pa_GetDeviceInfo(i))->maxInputChannels > 0) + printf(" %c%*d:\t%s\n", + i == appconfig.pcm_device_id ? '*' : ' ', + len, i, info->name); + } + +} +#endif + +/* Calculate max peak and amplitude RMS (based on all channels). */ static void peak_check_S16_LE(const int16_t *buffer, size_t frames, int channels, int16_t *peak, int16_t *rms) { @@ -241,87 +267,111 @@ static void peak_check_S16_LE(const int16_t *buffer, size_t frames, int channels *peak = abslvl; sum2 += abslvl * abslvl; } - *rms = sqrt(sum2 / frames); + + *rms = sqrt((double)sum2 / frames); } -/* Audio signal data reader thread. */ -static void *reader_thread(void *arg) { - (void)arg; +/* Process incoming audio frames. */ +static void process_audio_S16_LE(const int16_t *buffer, size_t frames, int channels) { + + static struct timespec peak_time = { 0 }; + struct timespec current_time; - int16_t *buffer = malloc(sizeof(int16_t) * appconfig.pcm_channels * READER_FRAMES); - snd_pcm_sframes_t rd_len; int16_t signal_peak; int16_t signal_rms; - struct timespec current_time; - struct timespec peak_time = { 0 }; + peak_check_S16_LE(buffer, frames, channels, &signal_peak, &signal_rms); - while (main_loop_on) { - rd_len = snd_pcm_readi(appconfig.pcm, buffer, READER_FRAMES); + if (appconfig.signal_meter) { + /* dump current peak and RMS values to the stdout */ + printf("\rsignal peak [%%]: %3u, signal RMS [%%]: %3u\r", + signal_peak * 100 / 0x7ffe, signal_rms * 100 / 0x7ffe); + fflush(stdout); + return; + } - /* buffer overrun (this should not happen) */ - if (rd_len == -EPIPE) { - snd_pcm_recover(appconfig.pcm, rd_len, 1); - if (appconfig.verbose) - warn("PCM buffer overrun"); - continue; - } + /* if the max peak in the buffer is greater than the threshold, update + * the last peak time */ + if ((int)signal_peak * 100 / 0x7ffe > appconfig.threshold) + clock_gettime(CLOCK_MONOTONIC_RAW, &peak_time); + + clock_gettime(CLOCK_MONOTONIC_RAW, ¤t_time); + if ((current_time.tv_sec - peak_time.tv_sec) * 1000 + + (current_time.tv_nsec - peak_time.tv_nsec) / 1000000 < appconfig.fadeout_time) { - peak_check_S16_LE(buffer, rd_len, appconfig.pcm_channels, &signal_peak, &signal_rms); + pthread_mutex_lock(&appconfig.mutex); - if (appconfig.signal_meter) { - /* dump current peak and RMS values to the stdout */ - printf("\rsignal peak [%%]: %3u, signal RMS [%%]: %3u\r", - signal_peak * 100 / 0x7ffe, signal_rms * 100 / 0x7ffe); - fflush(stdout); - continue; + /* if this will happen, nothing is going to save us... */ + if (appconfig.current == appconfig.size) { + appconfig.current = 0; + if (appconfig.verbose) + warn("Reader buffer overrun"); } - /* if the max peak in the buffer is greater than the threshold, update - * the last peak time */ - if ((int)signal_peak * 100 / 0x7ffe > appconfig.threshold) - clock_gettime(CLOCK_MONOTONIC_RAW, &peak_time); + /* NOTE: The size of data returned by the pcm_read in the blocking mode is + * always equal to the requested size. So, if the reader buffer (the + * external one) is an integer multiplication of our internal buffer, + * there is no need for any fancy boundary check. However, this might + * not be true if someone is using CPU profiling tool, like cpulimit. */ + memcpy(&appconfig.buffer[appconfig.current], buffer, + sizeof(int16_t) * frames * appconfig.pcm_channels); + appconfig.current += frames * appconfig.pcm_channels; - clock_gettime(CLOCK_MONOTONIC_RAW, ¤t_time); - if ((current_time.tv_sec - peak_time.tv_sec) * 1000 + - (current_time.tv_nsec - peak_time.tv_nsec) / 1000000 < appconfig.fadeout_time) { + /* dump reader buffer usage */ + debug("Buffer usage: %zd out of %zd", appconfig.current, appconfig.size); - pthread_mutex_lock(&appconfig.mutex); + pthread_cond_signal(&appconfig.ready); + pthread_mutex_unlock(&appconfig.mutex); - /* if this will happen, nothing is going to save us... */ - if (appconfig.current == appconfig.size) { - appconfig.current = 0; - if (appconfig.verbose) - warn("Reader buffer overrun"); - } + } - /* NOTE: The size of data returned by the pcm_read in the blocking mode is - * always equal to the requested size. So, if the reader buffer (the - * external one) is an integer multiplication of our internal buffer, - * there is no need for any fancy boundary check. However, this might - * not be true if someone is using CPU profiling tool, like cpulimit. */ - memcpy(&appconfig.buffer[appconfig.current], buffer, sizeof(int16_t) * rd_len * appconfig.pcm_channels); - appconfig.current += rd_len * appconfig.pcm_channels; +} - /* dump reader buffer usage */ - debug("Buffer usage: %d out of %d", appconfig.current, appconfig.size); +#if ENABLE_PORTAUDIO + +/* Callback function for PortAudio capture. */ +static int pa_capture_callback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, void *userData) { + (void)outputBuffer; + (void)timeInfo; + (void)statusFlags; + (void)userData; + process_audio_S16_LE(inputBuffer, framesPerBuffer, appconfig.pcm_channels); + return main_loop_on ? paContinue : paComplete; +} - pthread_cond_broadcast(&appconfig.ready); - pthread_mutex_unlock(&appconfig.mutex); +#else - } - } +/* Thread function for ALSA capture. */ +static void *alsa_capture_thread(void *arg) { - if (appconfig.signal_meter) - printf("\n"); + int16_t *buffer = malloc(sizeof(int16_t) * appconfig.pcm_channels * READER_FRAMES); + snd_pcm_sframes_t frames; + snd_pcm_t *pcm = arg; - /* avoid dead-lock on the condition wait during the exit */ - pthread_cond_broadcast(&appconfig.ready); + while (main_loop_on) { + if ((frames = snd_pcm_readi(pcm, buffer, READER_FRAMES)) < 0) + switch (frames) { + case -EPIPE: + case -ESTRPIPE: + snd_pcm_recover(pcm, frames, 1); + if (appconfig.verbose) + warn("PCM buffer overrun: %s", snd_strerror(frames)); + continue; + default: + error("PCM read error: %s", snd_strerror(frames)); + continue; + } + process_audio_S16_LE(buffer, frames, appconfig.pcm_channels); + } free(buffer); - return 0; + return NULL; } +#endif + /* Audio signal data processing thread. */ static void *processing_thread(void *arg) { (void)arg; @@ -402,7 +452,7 @@ static void *processing_thread(void *arg) { tmp_t_time = time(NULL); localtime_r(&tmp_t_time, &tmp_tm_time); - sprintf(file_name, "%s-%02d-%02d:%02d:%02d.%s", + snprintf(file_name, sizeof(file_name), "%s-%02d-%02d:%02d:%02d.%s", appconfig.output_prefix, tmp_tm_time.tm_mday, tmp_tm_time.tm_hour, tmp_tm_time.tm_min, tmp_tm_time.tm_sec, @@ -500,10 +550,12 @@ int main(int argc, char *argv[]) { int opt; size_t i; - const char *opts = "hvmD:R:C:l:f:o:p:s:"; + const char *opts = "hVvLD:R:C:l:f:o:p:s:m"; const struct option longopts[] = { {"help", no_argument, NULL, 'h'}, + {"version", no_argument, NULL, 'V'}, {"verbose", no_argument, NULL, 'v'}, + {"list-devices", no_argument, NULL, 'L'}, {"device", required_argument, NULL, 'D'}, {"channels", required_argument, NULL, 'C'}, {"rate", required_argument, NULL, 'R'}, @@ -516,8 +568,15 @@ int main(int argc, char *argv[]) { {0, 0, 0, 0}, }; - /* print application banner */ - printf("%s\n", appconfig.banner); +#if ENABLE_PORTAUDIO + PaError pa_err; + if ((pa_err = Pa_Initialize()) != paNoError) { + error("Couldn't initialize PortAudio: %s", Pa_GetErrorText(pa_err)); + return EXIT_FAILURE; + } + if ((appconfig.pcm_device_id = Pa_GetDefaultInputDevice()) == paNoDevice) + warn("Couldn't get default input PortAudio device"); +#endif /* arguments parser */ while ((opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1) @@ -525,7 +584,14 @@ int main(int argc, char *argv[]) { case 'h' /* --help */ : printf("usage: %s [options]\n" " -h, --help\t\t\tprint recipe for a delicious cake\n" + " -V, --version\t\t\tprint version number and exit\n" + " -v, --verbose\t\t\tprint some extra information\n" +#if ENABLE_PORTAUDIO + " -L, --list-devices\t\tlist available audio input devices\n" + " -D ID, --device=ID\t\tselect audio input device (current: %d)\n" +#else " -D DEV, --device=DEV\t\tselect audio input device (current: %s)\n" +#endif " -R NN, --rate=NN\t\tset sample rate (current: %u)\n" " -C NN, --channels=NN\t\tspecify number of channels (current: %u)\n" " -l NN, --sig-level=NN\t\tactivation signal threshold (current: %u)\n" @@ -533,15 +599,23 @@ int main(int argc, char *argv[]) { " -s NN, --split-time=NN\tsplit output file time in s (current: %d)\n" " -p STR, --out-prefix=STR\toutput file prefix (current: %s)\n" " -o FMT, --out-format=FMT\toutput file format (current: %s)\n" - " -m, --sig-meter\t\taudio signal level meter\n" - " -v, --verbose\t\t\tprint some extra information\n", + " -m, --sig-meter\t\taudio signal level meter\n", argv[0], - appconfig.pcm_device, appconfig.pcm_rate, appconfig.pcm_channels, +#if ENABLE_PORTAUDIO + appconfig.pcm_device_id, +#else + appconfig.pcm_device, +#endif + appconfig.pcm_rate, appconfig.pcm_channels, appconfig.threshold, appconfig.fadeout_time, appconfig.split_time, appconfig.output_prefix, get_output_format_name(appconfig.output_format)); return EXIT_SUCCESS; + case 'V' /* --version */ : + printf("%s\n", PROJECT_VERSION); + return EXIT_SUCCESS; + case 'm' /* --sig-meter */ : appconfig.signal_meter = true; break; @@ -549,9 +623,19 @@ int main(int argc, char *argv[]) { appconfig.verbose++; break; - case 'D' /* --device */ : +#if ENABLE_PORTAUDIO + case 'L' /* --list-devices */ : + pa_list_devices(); + return EXIT_SUCCESS; + case 'D' /* --device=ID */ : + appconfig.pcm_device_id = atoi(optarg); + break; +#else + case 'D' /* --device=DEV */ : strncpy(appconfig.pcm_device, optarg, sizeof(appconfig.pcm_device) - 1); break; +#endif + case 'C' /* --channels */ : appconfig.pcm_channels = abs(atoi(optarg)); break; @@ -604,10 +688,13 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } - int status = EXIT_SUCCESS; - pthread_t thread_read_id; + /* print application banner */ + printf("%s\n", appconfig.banner); + +#if !ENABLE_PORTAUDIO + pthread_t thread_alsa_capture_id; +#endif pthread_t thread_process_id; - char *msg = NULL; int err; /* initialize reader data */ @@ -619,24 +706,48 @@ int main(int argc, char *argv[]) { if (appconfig.buffer == NULL) { error("Failed to allocate memory for read buffer"); - goto fail; + return EXIT_FAILURE; + } + +#if ENABLE_PORTAUDIO + + PaStream *pa_stream = NULL; + PaStreamParameters pa_params = { + .sampleFormat = paInt16, + .device = appconfig.pcm_device_id, + .channelCount = appconfig.pcm_channels, + .suggestedLatency = Pa_GetDeviceInfo(appconfig.pcm_device_id)->defaultLowInputLatency, + .hostApiSpecificStreamInfo = NULL, + }; + + if ((pa_err = Pa_OpenStream(&pa_stream, &pa_params, NULL, appconfig.pcm_rate, + READER_FRAMES, paClipOff, pa_capture_callback, NULL)) != paNoError) { + error("Couldn't open PortAudio stream: %s", Pa_GetErrorText(pa_err)); + return EXIT_FAILURE; } - if ((err = snd_pcm_open(&appconfig.pcm, appconfig.pcm_device, SND_PCM_STREAM_CAPTURE, 0)) != 0) { +#else + + snd_pcm_t *pcm; + char *msg; + + if ((err = snd_pcm_open(&pcm, appconfig.pcm_device, SND_PCM_STREAM_CAPTURE, 0)) != 0) { error("Couldn't open PCM device: %s", snd_strerror(err)); - goto fail; + return EXIT_FAILURE; } - if ((err = set_hw_params(appconfig.pcm, &msg)) != 0) { + if ((err = pcm_set_hw_params(pcm, &msg)) != 0) { error("Couldn't set HW parameters: %s", msg); - goto fail; + return EXIT_FAILURE; } - if ((err = snd_pcm_prepare(appconfig.pcm)) != 0) { + if ((err = snd_pcm_prepare(pcm)) != 0) { error("Couldn't prepare PCM: %s", snd_strerror(err)); - goto fail; + return EXIT_FAILURE; } +#endif + if (appconfig.verbose) print_audio_info(); @@ -644,32 +755,41 @@ int main(int argc, char *argv[]) { sigaction(SIGTERM, &sigact, NULL); sigaction(SIGINT, &sigact, NULL); - /* initialize thread for audio capturing */ - if (pthread_create(&thread_read_id, NULL, &reader_thread, NULL) != 0) { - error("Couldn't create input thread"); - goto fail; +#if ENABLE_PORTAUDIO + if ((pa_err = Pa_StartStream(pa_stream)) != paNoError) { + error("Couldn't start PortAudio stream: %s", Pa_GetErrorText(pa_err)); + return EXIT_FAILURE; } +#else + if ((err = pthread_create(&thread_alsa_capture_id, NULL, &alsa_capture_thread, pcm)) != 0) { + error("Couldn't create ALSA capture thread: %s", strerror(-err)); + return EXIT_FAILURE; + } +#endif /* initialize thread for data processing */ - if (pthread_create(&thread_process_id, NULL, &processing_thread, NULL) != 0) { - error("Couldn't create processing thread"); - goto fail; + if ((err = pthread_create(&thread_process_id, NULL, &processing_thread, NULL)) != 0) { + error("Couldn't create processing thread: %s", strerror(-err)); + return EXIT_FAILURE; } - pthread_join(thread_read_id, NULL); +#if ENABLE_PORTAUDIO + while ((pa_err = Pa_IsStreamActive(pa_stream)) == 1) + Pa_Sleep(1000); + if (pa_err < 0) { + error("Couldn't check PortAudio activity: %s", Pa_GetErrorText(pa_err)); + return EXIT_FAILURE; + } +#else + pthread_join(thread_alsa_capture_id, NULL); +#endif + + /* avoid dead-lock on the condition wait */ + pthread_cond_signal(&appconfig.ready); pthread_join(thread_process_id, NULL); - status = EXIT_SUCCESS; - goto success; + if (appconfig.signal_meter) + printf("\n"); -fail: - status = EXIT_FAILURE; - -success: - if (appconfig.pcm != NULL) - snd_pcm_close(appconfig.pcm); - pthread_mutex_destroy(&appconfig.mutex); - pthread_cond_destroy(&appconfig.ready); - free(appconfig.buffer); - return status; + return EXIT_SUCCESS; }