commit 9900659b323b51cf9aa53257147967a303e429a4
Author: Louis Burda <quent.burda@gmail.com>
Date: Sun, 10 Mar 2024 14:20:29 +0100
Initial version
Diffstat:
6 files changed, 771 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+.cache
+compile_commands.json
+mplay.flac
diff --git a/.gitmodules b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "lib/mplay"]
+ path = lib/mplay
+ url = git@sinitax.com:sinitax/mplay
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Louis Burda
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,28 @@
+PREFIX ?= /usr/local
+BINDIR ?= /bin
+
+CFLAGS = -I lib/mplay
+CFLAGS += -Wunused-variable -Wunused-function -Wconversion
+LDFLAGS = -lpulse -lpulse-simple -lFLAC
+
+ifeq ($(DEBUG),1)
+CFLAGS += -Og -g
+else
+CFLAGS += -O2
+endif
+
+all: mplay.flac
+
+clean:
+ rm -f mplay.flac
+
+mplay.flac: mplay.flac.c lib/mplay/mplay.h
+ $(CC) -o $@ $< $(CFLAGS) $(LDFLAGS)
+
+install:
+ install -m755 mplay.flac -t "$(DESTDIR)$(PREFIX)$(BINDIR)"
+
+uninstall:
+ rm -f "$(DESTDIR)$(PREFIX)$(BINDIR)/mplay.flac"
+
+.PHONY: all clean install uninstall
diff --git a/lib/mplay b/lib/mplay
@@ -0,0 +1 @@
+Subproject commit c1d226be9672047fd66c20fde4bb2b3d197aa260
diff --git a/mplay.flac.c b/mplay.flac.c
@@ -0,0 +1,715 @@
+#include "mplay.h"
+
+#include <FLAC/format.h>
+#include <FLAC/stream_decoder.h>
+#include <pulse/sample.h>
+#include <pulse/introspect.h>
+#include <pulse/operation.h>
+#include <pulse/stream.h>
+#include <pulse/thread-mainloop.h>
+#include <pulse/volume.h>
+#include <pulse/pulseaudio.h>
+#include <pulse/def.h>
+
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <pthread.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+struct decoder {
+ FLAC__StreamDecoder *flac;
+ uint8_t *samples;
+ size_t sample_cnt;
+ size_t sample_cap;
+ size_t sample_size;
+ size_t sample_max;
+
+ size_t sample_next;
+
+ bool seek;
+
+ uint32_t rate;
+ uint8_t channels;
+ pa_sample_format_t format;
+
+ bool init;
+ bool pause;
+ bool done;
+ bool end;
+};
+
+static pa_buffer_attr pa_buf = {
+ .fragsize = UINT32_MAX,
+ .maxlength = UINT32_MAX,
+ .tlength = UINT32_MAX,
+ .prebuf = UINT32_MAX,
+ .minreq = UINT32_MAX,
+};
+
+static pa_stream_flags_t pa_stream_flags = PA_STREAM_START_CORKED
+ | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY;
+
+static pa_threaded_mainloop *pa_mloop;
+static pa_context *pa_ctx;
+static pa_stream *pa_strm;
+static pa_sink_input_info pa_strm_sink;
+static bool pa_strm_sink_update;
+
+static struct decoder decoder;
+
+static pthread_t input_worker_thread;
+static bool input_worker_alive = false;
+
+static struct termios term_old;
+static struct termios term_new;
+static bool term_set = false;
+
+static bool headless = false;
+static bool verbose = false;
+static bool istty = false;
+
+static void
+__attribute__((noreturn))
+__attribute__((format(printf, 1, 2)))
+die(const char *fmt, ...)
+{
+ va_list ap;
+
+ fputs("mplay.flac: ", stderr);
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ if (*fmt && fmt[strlen(fmt)-1] == ':') {
+ perror(NULL);
+ } else {
+ fputc('\n', stderr);
+ }
+
+ exit(1);
+}
+
+static void
+term_set_canonical(void)
+{
+ term_new.c_lflag |= ICANON;
+ term_new.c_lflag |= ECHO;
+ if (tcsetattr(0, TCSANOW, &term_new))
+ die("tcsetattr:");
+}
+
+static void
+term_set_raw(void)
+{
+ term_new.c_lflag &= ~(0U | ICANON);
+ term_new.c_lflag &= ~(0U | ECHO);
+ if (tcsetattr(0, TCSANOW, &term_new))
+ die("tcsetattr:");
+}
+
+static void
+get_line(char *buf, int size)
+{
+ char *c;
+
+ if (istty) putc('>', stderr);
+ if (istty) term_set_canonical();
+ fgets(buf, size, stdin);
+ if ((c = strchr(buf, '\n'))) *c = '\0';
+ if (istty) term_set_raw();
+}
+
+static FLAC__StreamDecoderWriteStatus
+decoder_write_callback(const FLAC__StreamDecoder *flac,
+ const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *user)
+{
+ uint32_t si, ch;
+
+ assert(decoder.sample_next == decoder.sample_cnt);
+
+ if (decoder.sample_cnt + frame->header.blocksize > decoder.sample_cap) {
+ decoder.sample_cap = MAX(decoder.sample_cap * 2,
+ decoder.sample_cnt + frame->header.blocksize);
+ decoder.samples = realloc(decoder.samples, decoder.sample_cap
+ * decoder.channels * decoder.sample_size);
+ if (!decoder.samples) die("realloc:");
+ }
+
+ if (decoder.format == PA_SAMPLE_S16LE) {
+ uint16_t *sample = (uint16_t *)decoder.samples
+ + decoder.sample_cnt * decoder.channels;
+ for (si = 0; si < frame->header.blocksize; si++) {
+ for (ch = 0; ch < frame->header.channels; ch++) {
+ *sample++ = (uint16_t)htole32((uint32_t)buffer[ch][si]);
+ }
+ }
+ } else if (decoder.format == PA_SAMPLE_S32LE) {
+ uint32_t *sample = (uint32_t *)decoder.samples
+ + decoder.sample_cnt * decoder.channels;
+ for (si = 0; si < frame->header.blocksize; si++) {
+ for (ch = 0; ch < frame->header.channels; ch++) {
+ *sample++ = htole32((uint32_t)buffer[ch][si]);
+ }
+ }
+ }
+
+ decoder.sample_cnt += frame->header.blocksize;
+ decoder.sample_max = MAX(decoder.sample_max, decoder.sample_cnt);
+
+ return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+static void
+decoder_metadata_callback(const FLAC__StreamDecoder *flac,
+ const FLAC__StreamMetadata *metadata, void *user)
+{
+ if (!decoder.init && metadata->type == FLAC__METADATA_TYPE_STREAMINFO) {
+ decoder.sample_cnt = 0;
+ decoder.rate = metadata->data.stream_info.sample_rate;
+ decoder.channels = (uint8_t) metadata->data.stream_info.channels;
+ decoder.sample_size = metadata->data.stream_info.bits_per_sample / 8;
+ decoder.sample_max = metadata->data.stream_info.total_samples;
+ if (decoder.sample_size == 2) {
+ decoder.format = PA_SAMPLE_S16LE;
+ } else if (decoder.sample_size == 4) {
+ decoder.format = PA_SAMPLE_S32LE;
+ } else {
+ die("unsupported sample size");
+ }
+ decoder.init = true;
+ }
+}
+
+static void
+decoder_error_callback(const FLAC__StreamDecoder *flac,
+ FLAC__StreamDecoderErrorStatus status, void *user)
+{
+ die("flac decoding error: %s", FLAC__StreamDecoderErrorStatusString[status]);
+}
+
+static void
+decoder_flac_decode_next(void)
+{
+ bool ok;
+
+ ok = FLAC__stream_decoder_process_single(decoder.flac);
+ FLAC__StreamDecoderState state =
+ FLAC__stream_decoder_get_state(decoder.flac);
+ if (!ok) {
+ die("decode process failed: %s",
+ FLAC__StreamDecoderStateString[state]);
+ }
+ decoder.done = state == FLAC__STREAM_DECODER_END_OF_STREAM;
+}
+
+static uint8_t *
+decoder_process_frame(size_t *cnt)
+{
+ size_t _cnt;
+
+ if (decoder.sample_next < decoder.sample_cnt) {
+ _cnt = decoder.sample_cnt - decoder.sample_next;
+ } else {
+ if (decoder.done) {
+ decoder.end = true;
+ _cnt = 0;
+ } else {
+ decoder_flac_decode_next();
+ _cnt = decoder.sample_cnt - decoder.sample_next;
+ }
+ }
+
+ if (cnt) *cnt = _cnt;
+
+ return decoder.samples + decoder.sample_next \
+ * decoder.channels * decoder.sample_size;
+}
+
+static void
+decoder_init(const char *file)
+{
+ FLAC__StreamDecoderInitStatus init_status;
+
+ decoder.sample_cnt = 0;
+ decoder.sample_next = 0;
+ decoder.sample_cap = 0;
+ decoder.sample_size = 0;
+ decoder.samples = NULL;
+
+ decoder.seek = false;
+ decoder.pause = false;
+ decoder.done = false;
+ decoder.end = false;
+
+ decoder.flac = FLAC__stream_decoder_new();
+ init_status = FLAC__stream_decoder_init_file(decoder.flac, file,
+ decoder_write_callback, decoder_metadata_callback,
+ decoder_error_callback, NULL);
+ if (init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
+ die("flac decoder init failed: %s",
+ FLAC__StreamDecoderInitStatusString[init_status]);
+ }
+
+ /* decode channel specs */
+ decoder.init = false;
+ decoder_process_frame(NULL);
+ assert(decoder.init);
+}
+
+static void
+decoder_seek(size_t sample_pos)
+{
+ while (decoder.sample_next < sample_pos) {
+ decoder_process_frame(NULL);
+ if (decoder.end) {
+ mplay_status(MPLAY_INFO_EXIT, "from seek");
+ exit(0);
+ }
+ }
+ decoder.sample_next = sample_pos;
+}
+
+
+static void
+pa_stream_drain_callback(pa_stream *stream, int success, void *data)
+{
+ mplay_status(MPLAY_INFO_EXIT, NULL);
+ exit(0);
+}
+
+static void
+pa_stream_write_callback(pa_stream *stream, size_t requested, void *data)
+{
+ uint8_t *samples;
+ pa_operation *op;
+ ssize_t remaining;
+ size_t cnt, size;
+
+ remaining = (ssize_t) requested;
+ while (remaining > 0) {
+ samples = decoder_process_frame(&cnt);
+ if (decoder.end) {
+ op = pa_stream_drain(pa_strm,
+ pa_stream_drain_callback, NULL);
+ pa_operation_unref(op);
+ return;
+ }
+ if (!cnt) continue;
+
+ cnt = MIN(cnt, (size_t) remaining);
+ size = cnt * decoder.channels * decoder.sample_size;
+ pa_stream_write(stream, samples, size, NULL, 0,
+ decoder.seek ? PA_SEEK_RELATIVE_ON_READ : PA_SEEK_RELATIVE);
+ decoder.sample_next += cnt;
+
+ decoder.seek = false;
+ remaining -= (ssize_t) size;
+ }
+}
+
+static void
+pa_stream_underflow_callback(struct pa_stream *stream, void *data)
+{
+ fprintf(stderr, "pulseaudio underflow!\n");
+}
+
+static void
+pa_stream_overflow_callback(struct pa_stream *stream, void *data)
+{
+ fprintf(stderr, "pulseaudio overflow!\n");
+}
+
+static void
+update_sink_input_info_callback(struct pa_context *ctx,
+ const pa_sink_input_info *info, int eol, void *data)
+{
+ if (eol) return;
+ memcpy(&pa_strm_sink, info, sizeof(pa_sink_input_info));
+ pa_strm_sink_update = true;
+ pa_threaded_mainloop_signal(pa_mloop, true);
+}
+
+static void
+update_sink_input_info(void)
+{
+ pa_operation *op;
+
+ pa_strm_sink_update = false;
+ op = pa_context_get_sink_input_info(pa_ctx,
+ pa_stream_get_index(pa_strm),
+ update_sink_input_info_callback, NULL);
+ if (!op) die("pa_context_get_sink_input_info failed");
+
+ while (!pa_strm_sink_update) {
+ pa_threaded_mainloop_unlock(pa_mloop);
+ pa_threaded_mainloop_wait(pa_mloop);
+ pa_threaded_mainloop_lock(pa_mloop);
+ }
+
+ pa_threaded_mainloop_accept(pa_mloop);
+
+ pa_operation_unref(op);
+}
+
+static size_t
+stream_samples_buffered(void)
+{
+ static size_t last_read_index = 0, last_write_index = 0;
+ const pa_timing_info *info;
+ size_t buffered;
+
+ info = pa_stream_get_timing_info(pa_strm);
+ if (!info || info->write_index_corrupt || info->read_index_corrupt)
+ return 0;
+
+ if (info->read_index != last_read_index)
+ last_write_index = (size_t) MAX(0, info->write_index);
+
+ buffered = (last_write_index - last_read_index)
+ / (decoder.sample_size * decoder.channels);
+
+ last_read_index = (size_t) MAX(0, info->read_index);
+
+ return buffered;
+}
+
+static double
+user_vol(void)
+{
+ pa_volume_t vol;
+
+ update_sink_input_info();
+ if (!pa_strm_sink.has_volume)
+ return -1;
+
+ vol = pa_cvolume_avg(&pa_strm_sink.volume);
+
+ return (double) vol * 100.F / (double) PA_VOLUME_NORM;
+}
+
+static double
+user_pos(void)
+{
+ size_t sample_pos;
+
+ sample_pos = MAX(0, decoder.sample_next - stream_samples_buffered());
+
+ return (double) sample_pos * 1.F / (double) decoder.rate;
+}
+
+static double
+user_end(void)
+{
+ return (double) decoder.sample_max * 1.F / (double) decoder.rate;
+}
+
+static void
+cmd_setvol(double vol, double delta)
+{
+ pa_operation *op;
+
+ pa_threaded_mainloop_lock(pa_mloop);
+
+ if (delta != 0) {
+ vol = user_vol() + delta;
+ }
+
+ update_sink_input_info();
+ pa_cvolume_set(&pa_strm_sink.volume, 2,
+ (pa_volume_t) (vol * PA_VOLUME_NORM / 100.F));
+ if (pa_cvolume_avg(&pa_strm_sink.volume) > 2 * PA_VOLUME_NORM)
+ pa_cvolume_set(&pa_strm_sink.volume, 2, 2 * PA_VOLUME_NORM);
+
+ op = pa_context_set_sink_input_volume(pa_ctx,
+ pa_stream_get_index(pa_strm),
+ &pa_strm_sink.volume, NULL, NULL);
+ pa_operation_unref(op);
+
+ mplay_status(MPLAY_INFO_VOLUME, "%02.2f", (double) pa_cvolume_avg(&pa_strm_sink.volume)
+ * 100.f / (double) PA_VOLUME_NORM);
+
+ pa_threaded_mainloop_unlock(pa_mloop);
+}
+
+static void
+cmd_seek(double time_sec, double delta)
+{
+ size_t sample_pos;
+ pa_operation *op;
+
+ pa_threaded_mainloop_lock(pa_mloop);
+
+ if (delta != 0) {
+ time_sec = user_pos() + delta;
+ }
+
+ if (time_sec < 0) time_sec = 0;
+ sample_pos = (size_t) (time_sec * decoder.rate);
+ decoder_seek(sample_pos);
+
+ op = pa_stream_flush(pa_strm, NULL, NULL);
+ pa_operation_unref(op);
+ decoder.seek = true;
+
+ mplay_status(MPLAY_INFO_SEEK, "%02.2f", user_pos());
+
+ pa_threaded_mainloop_unlock(pa_mloop);
+}
+
+static void
+cmd_playpause(void)
+{
+ pa_operation *op;
+
+ pa_threaded_mainloop_lock(pa_mloop);
+
+ decoder.pause ^= 1;
+ op = pa_stream_cork(pa_strm, decoder.pause, NULL, NULL);
+ pa_operation_unref(op);
+
+ mplay_status(MPLAY_INFO_PAUSE, "%i", decoder.pause);
+
+ pa_threaded_mainloop_unlock(pa_mloop);
+}
+
+static void
+cmd_status(void)
+{
+ pa_threaded_mainloop_lock(pa_mloop);
+
+ update_sink_input_info();
+
+ mplay_status(MPLAY_INFO_STATUS,
+ "volume=%02.2f,pos=%02.2f,end=%02.2f,pause=%i",
+ user_vol(), user_pos(), user_end(), decoder.pause);
+
+ pa_threaded_mainloop_unlock(pa_mloop);
+}
+
+static bool
+key_input(void)
+{
+ char linebuf[16], *end;
+ float fval;
+ int key;
+
+ while ((key = mplay_getkey()) == MPLAY_KEY_EOF);
+
+ switch (mplay_action(key)) {
+ case MPLAY_ACTION_VOLUME:
+ get_line(linebuf, sizeof(linebuf));
+ fval = strtof(linebuf, &end);
+ if (!end || *end) {
+ mplay_status(MPLAY_INFO_VOLUME, "fail:bad float");
+ break;
+ }
+ cmd_setvol(fval, 0);
+ break;
+ case MPLAY_ACTION_VOLUME_UP:
+ cmd_setvol(0, 5);
+ break;
+ case MPLAY_ACTION_VOLUME_DOWN:
+ cmd_setvol(0, -5);
+ break;
+ case MPLAY_ACTION_SEEK:
+ get_line(linebuf, sizeof(linebuf));
+ fval = strtof(linebuf, &end);
+ if (!end || *end) {
+ mplay_status(MPLAY_INFO_SEEK, "fail:bad float");
+ break;
+ }
+ cmd_seek(fval, 0);
+ break;
+ case MPLAY_ACTION_SEEK_BWD:
+ cmd_seek(0, -5);
+ break;
+ case MPLAY_ACTION_SEEK_FWD:
+ cmd_seek(0, 5);
+ break;
+ case MPLAY_ACTION_PAUSE:
+ cmd_playpause();
+ break;
+ case MPLAY_ACTION_STATUS:
+ cmd_status();
+ break;
+ default:
+ mplay_status(MPLAY_INFO_INPUT, "fail");
+ break;
+ }
+
+ return true;
+}
+
+static void *
+input_worker(void *arg)
+{
+ input_worker_alive = true;
+
+ setvbuf(stdin, NULL, _IONBF, 0);
+ setvbuf(stdout, NULL, _IONBF, 0);
+ setvbuf(stderr, NULL, _IONBF, 0);
+
+ if (istty) {
+ if (tcgetattr(0, &term_old))
+ die("tcgetattr:");
+ term_new = term_old;
+ term_new.c_iflag |= BRKINT;
+ term_new.c_iflag &= ~(0U | IGNBRK);
+ term_set = true;
+
+ term_set_raw();
+ }
+
+ mplay_status(MPLAY_INFO_READY, NULL);
+
+ while (key_input());
+
+ return NULL;
+}
+
+static void
+pulse_context_init(void)
+{
+ pa_mainloop_api *pa_mloop_api;
+ int ret;
+
+ pa_mloop_api = pa_threaded_mainloop_get_api(pa_mloop);
+ if (!pa_mloop_api) die("pa_threaded_mainloop_get_api");
+
+ pa_ctx = pa_context_new(pa_mloop_api, "mplay.flac");
+ if (!pa_ctx) die("pa_context_new");
+
+ ret = pa_context_connect(pa_ctx, NULL, 0, NULL);
+ if (ret) die("pa_context_connect: %s",
+ pa_strerror(pa_context_errno(pa_ctx)));
+
+ while (pa_context_get_state(pa_ctx) != PA_CONTEXT_READY) {
+ pa_threaded_mainloop_unlock(pa_mloop);
+ pa_threaded_mainloop_wait(pa_mloop);
+ pa_threaded_mainloop_lock(pa_mloop);
+ }
+}
+
+static void
+pulse_stream_init(void)
+{
+ struct pa_sample_spec pa_spec;
+ pa_channel_map pa_chmap;
+ int ret;
+
+ pa_channel_map_init_stereo(&pa_chmap);
+
+ pa_spec.channels = decoder.channels;
+ pa_spec.rate = decoder.rate;
+ pa_spec.format = decoder.format;
+
+ pa_strm = pa_stream_new(pa_ctx, "mplay.flac", &pa_spec, &pa_chmap);
+ if (!pa_strm) die("pa_stream_new: %s",
+ pa_strerror(pa_context_errno(pa_ctx)));
+
+ pa_stream_set_write_callback(pa_strm, pa_stream_write_callback, NULL);
+
+ if (verbose) {
+ pa_stream_set_underflow_callback(pa_strm,
+ pa_stream_underflow_callback, NULL);
+ pa_stream_set_overflow_callback(pa_strm,
+ pa_stream_overflow_callback, NULL);
+ }
+
+ ret = pa_stream_connect_playback(pa_strm, NULL,
+ &pa_buf, pa_stream_flags, NULL, NULL);
+ if (ret) die("pa_stream_connect_playback failed");
+
+ while (pa_stream_get_state(pa_strm) != PA_STREAM_READY) {
+ pa_threaded_mainloop_unlock(pa_mloop);
+ pa_threaded_mainloop_wait(pa_mloop);
+ pa_threaded_mainloop_lock(pa_mloop);
+ }
+}
+
+static void
+sigint_handler(int sig)
+{
+ exit(0);
+}
+
+static void
+cleanup(void)
+{
+ if (term_set) tcsetattr(0, TCSANOW, &term_old);
+
+ pa_stream_disconnect(pa_strm);
+ pa_stream_unref(pa_strm);
+
+ pa_context_disconnect(pa_ctx);
+ pa_context_unref(pa_ctx);
+
+ if (input_worker_alive)
+ pthread_kill(input_worker_thread, SIGINT);
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "Usage: mplay.flac [-v] [" MPLAY_FLAG_HEADLESS "] FILE\n");
+ exit(1);
+}
+
+int
+main(int argc, const char **argv)
+{
+ pthread_mutex_t lock;
+ const char **arg;
+ const char *file;
+
+ file = NULL;
+ for (arg = argv + 1; *arg; arg++) {
+ if (!strcmp(*arg, MPLAY_FLAG_HEADLESS)) {
+ headless = true;
+ } else if (!strcmp(*arg, "-v")) {
+ verbose = true;
+ } else {
+ if (file) usage();
+ file = *arg;
+ }
+ }
+ if (!file) usage();
+
+ istty = isatty(0);
+
+ decoder_init(file);
+
+ pa_mloop = pa_threaded_mainloop_new();
+ if (!pa_mloop) die("pa_threaded_mainloop_new");
+
+ pa_threaded_mainloop_start(pa_mloop);
+
+ pa_threaded_mainloop_lock(pa_mloop);
+ pulse_context_init();
+ pulse_stream_init();
+ pa_stream_cork(pa_strm, 0, NULL, NULL);
+ pa_threaded_mainloop_unlock(pa_mloop);
+
+ atexit(cleanup);
+ signal(SIGINT, sigint_handler);
+
+ if (!headless) {
+ if (pthread_create(&input_worker_thread, NULL, input_worker, NULL))
+ die("pthread_create");
+ pthread_join(input_worker_thread, NULL);
+ } else {
+ pthread_mutex_init(&lock, NULL);
+ pthread_mutex_lock(&lock);
+ pthread_mutex_lock(&lock);
+ }
+
+ return 0;
+}