+#include <stdlib.h>
+#include <stdio.h>
+#include <getopt.h>
+#include <math.h>
+
+#include <pulse/pulseaudio.h>
+#include <pulse/mainloop.h>
+
+#include "config.h"
+
+
+enum commands {
+ CMD_CHANNELS = 1,
+ CMD_FORMAT,
+};
+
+typedef struct properties {
+ void* play_buffer;
+ size_t play_buffer_pos;
+ size_t play_buffer_length;
+ FILE* rec_file;
+ size_t count;
+ pa_sample_spec sample_spec;
+ pa_mainloop_api* api;
+} properties;
+
+
+static void quit_mainloop(properties* prop, int ret) {
+ assert(prop);
+ prop->api->quit(prop->api, ret);
+}
+
+static void context_drain_complete(pa_context* c, void* userdata) {
+ (void) userdata;
+ assert(c);
+
+ pa_context_disconnect(c);
+}
+
+static void stream_drain_complete(pa_stream* s, int success, void* userdata) {
+ (void) userdata;
+ assert(s);
+
+ if (!success) {
+ fprintf(stderr, "pa_stream_drain() not successful.\n");
+ quit_mainloop((properties*) userdata, EXIT_FAILURE);
+ }
+
+ pa_stream_disconnect(s);
+ pa_stream_unref(s);
+
+ pa_context* context = pa_stream_get_context(s);
+ pa_operation *o;
+ if (!(o = pa_context_drain(context, context_drain_complete, NULL)))
+ pa_context_disconnect(context);
+ else {
+ pa_operation_unref(o);
+ }
+}
+
+static void drain_rec_stream(pa_stream* s, properties* prop) {
+ assert(s);
+
+ pa_stream_set_write_callback(s, NULL, NULL);
+
+ pa_operation* o;
+ if (!(o = pa_stream_drain(s, stream_drain_complete, NULL))) {
+ fprintf(stderr, "pa_stream_drain() failed.\n");
+ quit_mainloop(prop, EXIT_FAILURE);
+ }
+
+ pa_operation_unref(o);
+}
+
+static size_t stream_write_partial(pa_stream* s, size_t length, properties* prop) {
+ assert(s);
+ assert(length > 0);
+ assert(prop->count > 0);
+
+ // Shorten length to not exceed buffer length
+ const size_t rem = prop->play_buffer_length - prop->play_buffer_pos;
+ if (length > rem)
+ length = rem;
+
+ fprintf(stderr, "Playing %lu bytes...\n", length);
+
+ uint8_t* data = (uint8_t*) prop->play_buffer + prop->play_buffer_pos;
+ if (pa_stream_write(s, data, length, NULL, 0, PA_SEEK_RELATIVE) < 0) {
+ fprintf(stderr, "pa_stream_write() failed.\n");
+ quit_mainloop(prop, EXIT_FAILURE);
+ }
+
+ prop->play_buffer_pos += length;
+ assert(prop->play_buffer_pos <= prop->play_buffer_length);
+
+ // Reached end of buffer
+ if (prop->play_buffer_pos == prop->play_buffer_length) {
+ prop->play_buffer_pos = 0;
+ --prop->count;
+ }
+
+ return length;
+}
+
+static void stream_write_callback(pa_stream *s, size_t length, void *userdata) {
+ assert(s);
+ assert(length > 0);
+
+ properties* prop = (properties*) userdata;
+ assert(prop);
+
+ // We still have length many bytes to play
+ while (length > 0) {
+ // Play up to length many bytes
+ const size_t ret = stream_write_partial(s, length, prop);
+ length -= ret;
+
+ // No playback anymore
+ if (prop->count == 0) {
+ drain_rec_stream(s, prop);
+ break;
+ }
+ }
+}
+
+static void stream_read_callback(pa_stream *s, size_t length, void *userdata) {
+ assert(s);
+ assert(length > 0);
+
+ properties* prop = (properties*) userdata;
+ assert(prop);
+ assert(prop->rec_file);
+
+ const void* data;
+ if (pa_stream_peek(s, &data, &length) < 0) {
+ fprintf(stderr, "pa_stream_peek() failed.\n");
+ quit_mainloop(prop, EXIT_FAILURE);
+ }
+
+ fprintf(stderr, "Recorded %lu bytes...\n", length);
+
+ assert(data);
+ fwrite(data, length, 1, prop->rec_file);
+ fflush(prop->rec_file);
+
+ pa_stream_drop(s);
+}
+
+static void context_state_callback(pa_context *c, void *userdata) {
+ pa_stream* play_stream;
+ pa_stream* rec_stream;
+ properties* prop = (properties*) userdata;
+ assert(prop);
+
+ switch (pa_context_get_state(c)) {
+
+ case PA_CONTEXT_READY:
+ if (!(play_stream = pa_stream_new(c, PACKAGE_NAME, &prop->sample_spec, NULL))) {
+ fprintf(stderr, "pa_stream_new() failed.\n");
+ quit_mainloop(prop, EXIT_FAILURE);
+ }
+
+ pa_stream_set_write_callback(play_stream, stream_write_callback, userdata);
+
+ if (pa_stream_connect_playback(play_stream, NULL, NULL, 0, NULL, NULL) < 0) {
+ fprintf(stderr, "pa_stream_connect_playback() failed.\n");
+ quit_mainloop(prop, EXIT_FAILURE);
+ }
+
+ if (!(rec_stream = pa_stream_new(c, PACKAGE_NAME, &prop->sample_spec, NULL))) {
+ fprintf(stderr, "pa_stream_new() failed.\n");
+ quit_mainloop(prop, EXIT_FAILURE);
+ }
+
+ pa_stream_set_read_callback(rec_stream, stream_read_callback, userdata);
+
+ if (pa_stream_connect_record(rec_stream, NULL, NULL, 0) < 0) {
+ fprintf(stderr, "pa_stream_connect_record() failed.\n");
+ quit_mainloop(prop, EXIT_FAILURE);
+ }
+ break;
+
+ case PA_CONTEXT_TERMINATED:
+ quit_mainloop(prop, EXIT_SUCCESS);
+ break;
+
+ case PA_CONTEXT_FAILED:
+ fprintf(stderr, "Connection failed.\n");
+ quit_mainloop(prop, EXIT_FAILURE);
+
+ default:
+ break;
+ }
+}
+
+static void cmd_help(const char* argv0) {
+ printf("%s [options]\n"
+ "\n"
+ "Plays repeatedly a given sound file while continuously\n"
+ "recording to another file the acoustic feedback.\n"
+ "\n"
+ "Usage:\n"
+ " %s --help\n"
+ " %s --play=FILENAME\n"
+ "\n"
+ "Options:\n"
+ "\n"
+ " --channels=NUMBER\n"
+ " The number of channels for the play and rec files.\n"
+ " Defaults to 2.\n"
+ "\n"
+ " -c, --count=NUMBER\n"
+ " Stop after playing for the given number of iterations.\n"
+ " Defaults to 0 which means infinity.\n"
+ "\n"
+ " --format=SAMPLEFORMAT\n"
+ " The sample format of the play and rec files. See\n"
+ " pacat --list-file-formats. Defaults to s16le.\n"
+ "\n"
+ " -h, --help\n"
+ " Prints this usage text and exits.\n"
+ "\n"
+ " -p, --play=FILENAME\n"
+ " Plays the raw samples in the given file. If not specified\n"
+ " then stdin is used.\n"
+ "\n"
+ " -r, --rec=FILENAME\n"
+ " The file where the recording is written to. If not\n"
+ " specified then stdout is used.\n"
+ "\n"
+ " -s, --rate=SAMPLERATE\n"
+ " The sample rate of the play and rec files. Defaults\n"
+ " to 44100.\n"
+ "\n"
+ " -V, --version\n"
+ " Prints version information and exits.\n", argv0, argv0, argv0);
+}
+
+static void cmd_version() {
+ printf(PACKAGE_STRING "\n");
+}
+
+static void read_file(FILE* f, void** data, size_t* length) {
+ assert(f);
+
+ *length = 0L;
+ *data = NULL;
+
+ size_t cap = 0;
+ while (*length == cap) {
+ // Double capacity of buffer, start with 4096 bytes
+ cap = (cap == 0) ? 4096 : 2 * cap;
+ *data = realloc(*data, cap);
+
+ // Could not allocate
+ if (!*data)
+ return;
+
+ // Read into remainder of the buffer
+ uint8_t* rem_data = (uint8_t*) *data + *length;
+ const size_t rem_cap = cap - *length;
+ const size_t ret = fread(rem_data, 1, rem_cap, f);
+ *length += ret;
+
+ // Note that ret < rem_cap iff *length < cap, which
+ // means that there is no more data and we are done.
+ }
+}
+
+int main(int argc, char* const argv[]) {
+ int ret = EXIT_FAILURE;
+ pa_mainloop* m = NULL;
+ pa_context* context = NULL;
+ const char* play_fn = NULL;
+ const char* rec_fn = NULL;
+
+ properties prop = {
+ .play_buffer = NULL,
+ .play_buffer_pos = 0L,
+ .play_buffer_length = 0L,
+ .rec_file = stdout,
+ .count = -1,
+ .sample_spec = {
+ .format = PA_SAMPLE_S16LE,
+ .rate = 44100,
+ .channels = 2,
+ },
+ .api = NULL
+ };
+
+ static const struct option long_options[] = {
+ {"channels", 0, NULL, CMD_CHANNELS},
+ {"count", 1, NULL, 'c'},
+ {"format", 1, NULL, CMD_FORMAT},
+ {"help", 0, NULL, 'h'},
+ {"play", 1, NULL, 'p'},
+ {"rec", 1, NULL, 'r'},
+ {"rate", 1, NULL, 's'},
+ {"version", 0, NULL, 'V'},
+ {NULL, 0, NULL, 0}
+ };
+
+ int c;
+ while ((c = getopt_long(argc, argv, "c:f:hp:r:s:V", long_options, NULL)) != -1) {
+ switch (c) {
+ case CMD_CHANNELS:
+ prop.sample_spec.channels = atoi(optarg);
+ break;
+
+ case 'c':
+ prop.count = atoi(optarg);
+ break;
+
+ case CMD_FORMAT:
+ prop.sample_spec.format = pa_parse_sample_format(optarg);
+ break;
+
+ case 'h':
+ cmd_help(argv[0]);
+ return EXIT_SUCCESS;
+
+ case 'p':
+ play_fn = optarg;
+ break;
+
+ case 'r':
+ rec_fn = optarg;
+ break;
+
+ case 's':
+ prop.sample_spec.rate = atoi(optarg);
+ break;
+
+ case 'V':
+ cmd_version();
+ return EXIT_SUCCESS;
+
+ default:
+ goto quit;
+ }
+ }
+
+ FILE* play_file = stdin;
+ if (play_fn)
+ play_file = fopen(play_fn, "r");
+
+ if (!play_file) {
+ perror("Error opening play file.");
+ goto quit;
+ }
+
+ read_file(play_file, &prop.play_buffer, &prop.play_buffer_length);
+
+ if (play_fn)
+ fclose(play_file);
+
+ const size_t samples = prop.play_buffer_length / pa_sample_size(&prop.sample_spec);
+ fprintf(stderr, "Play buffer populated with %lu samples.\n", samples);
+
+ if (samples == 0) {
+ fprintf(stderr, "Could not gather samples from play file.\n");
+ goto quit;
+ }
+
+ if (rec_fn) {
+ prop.rec_file = fopen(rec_fn, "w");
+ if (!prop.rec_file) {
+ perror("Error opening rec file.");
+ goto quit;
+ }
+ }
+
+ if (!(m = pa_mainloop_new())) {
+ fprintf(stderr, "pa_mainloop_new() failed.\n");
+ goto quit;
+ }
+
+ prop.api = pa_mainloop_get_api(m);
+ assert(prop.api);
+
+ if (!(context = pa_context_new(prop.api, PACKAGE_NAME))) {
+ fprintf(stderr, "pa_context_new() failed.\n");
+ goto quit;
+ }
+
+ pa_context_set_state_callback(context, context_state_callback, &prop);
+
+ if (pa_context_connect(context, NULL, 0, NULL) < 0) {
+ fprintf(stderr, "pa_context_connect() failed: %s\n", pa_strerror(pa_context_errno(context)));
+ goto quit;
+ }
+
+ if (pa_mainloop_run(m, &ret) < 0) {
+ fprintf(stderr, "pa_mainloop_run() failed.\n");
+ goto quit;
+ }
+
+ fprintf(stderr, "Done.\n");
+
+quit:
+ if (context)
+ pa_context_unref(context);
+
+ if (m) {
+ pa_signal_done();
+ pa_mainloop_free(m);
+ }
+
+ if (rec_fn)
+ fclose(prop.rec_file);
+
+ if (prop.play_buffer)
+ free(prop.play_buffer);
+
+ return ret;
+}