diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 6ffa4fe..2308b3b 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -25,7 +25,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install libasound2-dev libpulse-dev libaudio-dev libxcb1-dev libxcb-icccm4-dev libcairo2-dev libsdl2-dev + sudo apt-get install libasound2-dev libpulse-dev libaudio-dev libxcb1-dev libxcb-icccm4-dev libcairo2-dev libsdl2-dev libpipewire-0.3-dev - name: Install valgrind if: ${{ matrix.flags == '' }} run: | @@ -39,7 +39,7 @@ jobs: make - name: Check activated plugouts run: | - ./check_plugouts.sh alsa pulse devdsp nas sdl + ./check_plugouts.sh alsa pulse devdsp nas sdl pipewire - name: Check WAV plugout run: | ./check_plugout_wav.sh diff --git a/COPYRIGHT b/COPYRIGHT index 9535eb8..0b867e6 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,7 +1,7 @@ gbsplay - A Gameboy sound player -(C) 2003-2022 by Tobias Diedrich +(C) 2003-2024 by Tobias Diedrich Christian Garbs Maximilian Rehkopf Vegard Nossum diff --git a/HISTORY b/HISTORY index ced6f8e..e3eaf64 100644 --- a/HISTORY +++ b/HISTORY @@ -12,6 +12,9 @@ Bugfixes: Enhancements: +- gbsplay: + - add PipeWire plugout + - build process: - add automated build pipeline for FreeBSD (x86-64 and arm64) diff --git a/Makefile b/Makefile index a8f09ad..f00f51e 100644 --- a/Makefile +++ b/Makefile @@ -147,6 +147,10 @@ endif ifeq ($(plugout_altmidi),yes) plugout_objs += plugout_altmidi.o midifile.o filewriter.o endif +ifeq ($(plugout_pipewire),yes) +plugout_objs += plugout_pipewire.o +plugout_ldflags += $(libpipewire_0_3_flags) +endif ifeq ($(plugout_pulse),yes) plugout_objs += plugout_pulse.o plugout_ldflags += -lpulse-simple -lpulse diff --git a/README.md b/README.md index 29a1005..722f4e1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ the following parts: ## License ``` -(C) 2003-2022 by Tobias Diedrich +(C) 2003-2024 by Tobias Diedrich Christian Garbs Maximilian Rehkopf Vegard Nossum diff --git a/configure b/configure index 2773b3f..fde2ae9 100755 --- a/configure +++ b/configure @@ -4,7 +4,7 @@ # # generate config.mk for gbsplay Makefile # -# 2003-2022 (C) by Christian Garbs +# 2003-2024 (C) by Christian Garbs # Tobias Diedrich # # Licensed under GNU GPL v1 or, at your option, any later version. @@ -186,7 +186,7 @@ check_libs() local INFILE="$TEMPDIR/cl.c" local OUTFILE="$TEMPDIR/cl" local checklib="$1" - local libname="$(echo "$checklib" | sed -e 's@[/\.-]@_@g')" + local libname="$(echo "$checklib" | sed -e 's@[/\.-]@_@g' -e 's/ //g')" local extralibs="${checklib} ${2-}" local extralibdirs="${3-}" local name="${4-}" @@ -194,6 +194,9 @@ check_libs() local msg="${6-$checklib}" local cflags="$CFLAGS $LDFLAGS" + # don't let -pedantic mess with our checks + cflags="$(sed -E 's/-pedantic ?//g'<<<"$cflags")" + eval "lib${libname}_flags=" cat > "$INFILE" @@ -221,6 +224,7 @@ check_libs() done if [ "$minerrs" -ne 0 ]; then + echo "...but it still won't compile cleanly" # log the default result to config.err ( echo "minerrs: $minerrs > 0" @@ -542,6 +546,7 @@ Output Plugins: --disable-midi omit MIDI file writer plugin --disable-altmidi omit alternative MIDI file writer plugin --disable-nas omit NAS sound output plugin + --disable-pipewire omit PipeWire sound output plugin --disable-pulse omit PulseAudio sound output plugin --disable-sdl omit SDL sound output plugin --disable-stdout omit stdout file writer plugin @@ -565,6 +570,7 @@ OPTS="${OPTS} use_iodumper" OPTS="${OPTS} use_midi" OPTS="${OPTS} use_altmidi" OPTS="${OPTS} use_nas" +OPTS="${OPTS} use_pipewire" OPTS="${OPTS} use_pulse" OPTS="${OPTS} use_sdl" OPTS="${OPTS} use_sharedlibgbs" @@ -698,6 +704,7 @@ if [ "$windows_build" = yes ]; then # don't look for audio libs that are unavailable on Windows setdefault use_alsa no setdefault use_nas no + setdefault use_pipewire no setdefault use_pulse no if cc_check "" "" "" </dev/null && pkg-config ${pipewire_version}; then + pipewire_have_pkg_config=yes + else + pipewire_have_pkg_config=no + fi + + if [ "$pipewire_have_pkg_config" = yes ]; then + pipewire_include_flags="$(pkg-config --cflags-only-I ${pipewire_version})" + pipewire_cflags="$(pkg-config --cflags-only-other ${pipewire_version})" + else + pipewire_include_flags='-I/usr/include/pipewire-0.3 -I/usr/include/spa-0.2' + pipewire_cflags='' + fi + pipewire_include_path="$(sed -e 's/^-I//' -e 's/ -I/ /g' <<<"${pipewire_include_flags}")" + + check_include spa/support/plugin.h "$pipewire_include_path" + if [ "$have_spa_support_plugin_h" = yes ]; then + append_nodupe CFLAGS "-I${include_spa_support_plugin_h_path}" + check_include pipewire/pipewire.h "$pipewire_include_path" + if [ "$have_pipewire_pipewire_h" = yes ]; then + append_nodupe CFLAGS "-I${include_pipewire_pipewire_h_path} ${pipewire_cflags}" + + if [ "$pipewire_have_pkg_config" = yes ]; then + pipewire_ldpath_flags="$(pkg-config --libs-only-L ${pipewire_version})" + pipewire_libname_flags="$(pkg-config --libs-only-l ${pipewire_version})" + pipewire_ldflags="$(pkg-config --libs-only-other ${pipewire_version})" + else + pipewire_ldpath_flags='' + pipewire_libname_flags='-lpipewire-0.3' + pipewire_ldflags='' + fi + pipewire_ldpath="$(sed -e 's/^-L//' -e 's/ -L/ /g' <<<"${pipewire_ldpath_flags}")" + pipewire_libname="${pipewire_libname_flags#*-l}" + + check_libs "${pipewire_libname}" "" "${pipewire_ldpath}" "" "${pipewire_ldflags}" < +int main(int argc, char **argv) { + pw_init(0, NULL); + pw_deinit(); + return 0; +} +EOF + if [ "$?" -eq 0 ]; then + use_pipewire=yes + fi + fi + fi + + recheck_use pipewire +fi + if [ "$use_pulse" != no ]; then remember_use pulse check_include pulse/simple.h @@ -1230,6 +1292,7 @@ use_verbosebuild windows_build windows_libprefix libaudio_flags +libpipewire_0_3_flags libSDL2_flags __EOF__ echo plugout_alsa := $use_alsa @@ -1239,6 +1302,7 @@ __EOF__ echo plugout_midi := $use_midi echo plugout_altmidi := $use_altmidi echo plugout_nas := $use_nas + echo plugout_pipewire := $use_pipewire echo plugout_pulse := $use_pulse echo plugout_sdl := $use_sdl echo plugout_stdout := $use_stdout @@ -1260,6 +1324,7 @@ __EOF__ plugout_x MIDI plugout_x ALTMIDI plugout_x NAS + plugout_x PIPEWIRE plugout_x PULSE plugout_x STDOUT plugout_x SDL diff --git a/man/gbsplay.in.1 b/man/gbsplay.in.1 index da50634..249f5b5 100644 --- a/man/gbsplay.in.1 +++ b/man/gbsplay.in.1 @@ -1,4 +1,4 @@ -.\" This manpage 2003-2022 (C) by Christian Garbs +.\" This manpage 2003-2024 (C) by Christian Garbs .\" Licensed under GNU GPL v1 or, at your option, any later version. .TH "GBSPLAY" "1" "%%%VERSION%%%" "Tobias Diedrich" "Gameboy sound player" .SH "NAME" @@ -254,6 +254,9 @@ using tricks and hacks will not be converted properly. .B nas Use the NAS sound driver for sound output to a Network Audio Server. .TP +.B pipewire +Use the PipeWire sound driver for sound output. +.TP .B pulse Use the Pulseaudio sound driver for sound output. .TP diff --git a/man/xgbsplay.in.1 b/man/xgbsplay.in.1 index a71ec65..08aeeef 100644 --- a/man/xgbsplay.in.1 +++ b/man/xgbsplay.in.1 @@ -1,4 +1,4 @@ -.\" This manpage 2003-2022 (C) by Christian Garbs +.\" This manpage 2003-2024 (C) by Christian Garbs .\" Licensed under GNU GPL v1 or, at your option, any later version. .TH "GBSPLAY" "1" "%%%VERSION%%%" "Tobias Diedrich" "Gameboy sound player" .SH "NAME" @@ -174,6 +174,9 @@ using tricks and hacks will not be converted properly. .B nas Use the NAS sound driver for sound output to a Network Audio Server. .TP +.B pipewire +Use the PipeWire sound driver for sound output. +.TP .B pulse Use the Pulseaudio sound driver for sound output. .TP diff --git a/plugout.c b/plugout.c index 4bd2aa3..d93ce36 100644 --- a/plugout.c +++ b/plugout.c @@ -35,6 +35,9 @@ extern const struct output_plugin plugout_altmidi; #ifdef PLUGOUT_NAS extern const struct output_plugin plugout_nas; #endif +#ifdef PLUGOUT_PIPEWIRE +extern const struct output_plugin plugout_pipewire; +#endif #ifdef PLUGOUT_PULSE extern const struct output_plugin plugout_pulse; #endif @@ -70,6 +73,9 @@ static output_plugin_const_t plugouts[] = { #ifdef PLUGOUT_STDOUT &plugout_stdout, #endif +#ifdef PLUGOUT_PIPEWIRE + &plugout_pipewire, +#endif #ifdef PLUGOUT_NAS &plugout_nas, #endif diff --git a/plugout.h b/plugout.h index 3bedab7..8e482a8 100644 --- a/plugout.h +++ b/plugout.h @@ -18,6 +18,8 @@ #if PLUGOUT_DSOUND == 1 # define PLUGOUT_DEFAULT "dsound" +#elif PLUGOUT_PIPEWIRE == 1 +# define PLUGOUT_DEFAULT "pipewire" #elif PLUGOUT_PULSE == 1 # define PLUGOUT_DEFAULT "pulse" #elif PLUGOUT_ALSA == 1 diff --git a/plugout_pipewire.c b/plugout_pipewire.c new file mode 100644 index 0000000..fa08030 --- /dev/null +++ b/plugout_pipewire.c @@ -0,0 +1,190 @@ +/* + * gbsplay is a Gameboy sound player + * + * 2024 (C) by Christian Garbs + * + * Licensed under GNU GPL v1 or, at your option, any later version. + */ + +#include +#include + +#include +#include +#include + +#include "common.h" +#include "plugout.h" + +#if GBS_BYTE_ORDER == GBS_ORDER_LITTLE_ENDIAN +#define SPA_AUDIO_FORMAT_S16_NE SPA_AUDIO_FORMAT_S16_LE +#else +#define SPA_AUDIO_FORMAT_S16_NE SPA_AUDIO_FORMAT_S16_BE +#endif + +static const int BYTES_PER_SAMPLE = 2; +static const int CHANNELS = 2; +static const int STRIDE = BYTES_PER_SAMPLE * CHANNELS; + +static struct pipewire_data { + struct pw_thread_loop *loop; + struct pw_stream *stream; + struct timespec buffer_fill_wait_time; +} pipewire_data; + +static const struct pw_stream_events pipewire_stream_events = { + PW_VERSION_STREAM_EVENTS, +}; + +static long pipewire_open(enum plugout_endian *endian, long rate, long *buffer_bytes) +{ + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct pw_properties *props; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + enum spa_audio_format fmt; + int err; + + // determine endianess + switch (*endian) { + case PLUGOUT_ENDIAN_BIG: fmt = SPA_AUDIO_FORMAT_S16_BE; break; + case PLUGOUT_ENDIAN_LITTLE: fmt = SPA_AUDIO_FORMAT_S16_LE; break; + default: fmt = SPA_AUDIO_FORMAT_S16_NE; break; + } + + // determine buffer wait time - use 25% gbsplay buffer length (~2 wait cycles on my machine) + pipewire_data.buffer_fill_wait_time.tv_sec = 0; + pipewire_data.buffer_fill_wait_time.tv_nsec = + 1000000000 // nanoseconds per second + / rate // sample rate + * (*buffer_bytes / STRIDE) // samples in buffer + / 4; // 25% of that + + // init pipewire + pw_init(0, NULL); + + // set main loop + pipewire_data.loop = pw_thread_loop_new("gbsplay", NULL); + + // set stream metadata + props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + NULL); + + // TODO: we could add a PW_KEY_TARGET_OBJECT to select an audio target, + // but we can't yet pass parameters to plugouts. + + // set audio format + params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT( + .format = fmt, + .channels = CHANNELS, + .rate = rate)); + + // create stream + pipewire_data.stream = pw_stream_new_simple(pw_thread_loop_get_loop(pipewire_data.loop), + "gbsplay", + props, + &pipewire_stream_events, + &pipewire_data); + + // connect the stream + // TODO: do we need the realtime flag? + if ((err = pw_stream_connect(pipewire_data.stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, 1))) { + fprintf(stderr, _("pw_stream_connect failed: %s\n"), spa_strerror(err)); + return -1; + } + + // run the loop + pw_thread_loop_start(pipewire_data.loop); + + return 0; +} + +static void pipewire_pause(int pause) +{ + int err; + if ((err = pw_stream_set_active(pipewire_data.stream, !pause))) { + fprintf(stderr, _("pw_stream_set_active failed: %s\n"), spa_strerror(err)); + return; + } +} + +static ssize_t pipewire_write(const void *buf, size_t count) +{ + struct pw_buffer *b; + struct spa_buffer *spa_buf; + int n_frames; + uint8_t *p; + size_t buf_sent = 0; + size_t bytes_to_send; + + // repeat until the whole buffer is sent + while (buf_sent < count) { + + const int frames_to_send = (count - buf_sent) / STRIDE; + + // wait until data can be sent by us + while ((b = pw_stream_dequeue_buffer(pipewire_data.stream)) == NULL) { + nanosleep(&pipewire_data.buffer_fill_wait_time, NULL); + } + + // get send buffer + spa_buf = b->buffer; + if ((p = spa_buf->datas[0].data) == NULL) { + nanosleep(&pipewire_data.buffer_fill_wait_time, NULL); + break; + } + + // check how much we can send + n_frames = spa_buf->datas[0].maxsize / STRIDE; +#if PW_CHECK_VERSION(0,3,49) + // unfortunately our CI pipeline runs Ubuntu 22.04LTS + // which is on 0.3.48, so we have to do this version check + if (b->requested) + n_frames = SPA_MIN((int)b->requested, n_frames); +#endif + n_frames = SPA_MIN(n_frames, frames_to_send); + bytes_to_send = n_frames * STRIDE; + + // send audio data + memcpy(p, ((uint8_t *) buf) + buf_sent, bytes_to_send); + + spa_buf->datas[0].chunk->offset = 0; + spa_buf->datas[0].chunk->stride = STRIDE; + spa_buf->datas[0].chunk->size = bytes_to_send; + + pw_stream_queue_buffer(pipewire_data.stream, b); + + // remember what was sent + buf_sent += bytes_to_send; + } + + return 0; +} + +static void pipewire_close() +{ + pw_thread_loop_wait(pipewire_data.loop); + pw_thread_loop_unlock(pipewire_data.loop); + pw_thread_loop_stop(pipewire_data.loop); + pw_stream_destroy(pipewire_data.stream); + pw_thread_loop_destroy(pipewire_data.loop); + pw_deinit(); +} + +const struct output_plugin plugout_pipewire = { + .name = "pipewire", + .description = "PipeWire sound driver", + .open = pipewire_open, + .pause = pipewire_pause, + .write = pipewire_write, + .close = pipewire_close, +}; diff --git a/po/de.po b/po/de.po index 336102b..3c267c8 100644 --- a/po/de.po +++ b/po/de.po @@ -1,7 +1,7 @@ # German translations for gbsplay. # -# Copyright (C) 2003-2020,2023 Tobias Diedrich -# Christian Garbs +# Copyright (C) 2003-2024 Tobias Diedrich +# Christian Garbs # # This file is distributed under GNU GPL v1 or, at your option, any later version. # @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.4\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-19 15:29+0100\n" -"PO-Revision-Date: 2023-11-19 16:03+0100\n" +"POT-Creation-Date: 2024-02-02 22:58+0100\n" +"PO-Revision-Date: 2024-02-02 23:02+0100\n" "Last-Translator: Christian Garbs \n" "Language-Team: none\n" "Language: de\n" @@ -57,9 +57,11 @@ msgstr "" msgid "" "\"%s\" is not a valid endian.\n" "\n" -msgstr "\"%s\" ist keine gültige Byte-Reihenfolge.\n\n" +msgstr "" +"\"%s\" ist keine gültige Byte-Reihenfolge.\n" +"\n" -#, c-format, fuzzy +#, fuzzy, c-format msgid "%s: Can't create audio flow\n" msgstr "%s: Kann keinen Audio-Flow erzeugen\n" @@ -71,11 +73,11 @@ msgstr "%s: Kann kein Ger msgid "%s: Can't open server: %s\n" msgstr "%s: Kann den Server nicht öffnen: %s\n" -#, c-format, fuzzy +#, fuzzy, c-format msgid "%s: Can't set audio elements: %s\n" msgstr "%s: Kann Audio-Elemente nicht setzen: %s\n" -#, c-format, fuzzy +#, fuzzy, c-format msgid "%s: Can't start audio flow: %s\n" msgstr "%s: Kann den Audio-Flow nicht starten: %s\n" @@ -89,7 +91,9 @@ msgstr "59.7Hz VBlank\n" msgid "" "Available output plugins:\n" "\n" -msgstr "Verfügbare Ausgabemethoden:\n\n" +msgstr "" +"Verfügbare Ausgabemethoden:\n" +"\n" #, c-format msgid "Bad GD3 offset: %08lx\n" @@ -210,7 +214,7 @@ msgstr "" msgid "Invalid filter type \"%s\"\n" msgstr "Ungültiger Filtertyp \"%s\"\n" -#, c-format, fuzzy +#, fuzzy, c-format msgid "Load address %04x overlaps with replayer end %04x.\n" msgstr "Ladeaddresse %04x überlappt mit Replayer-Ende %04x.\n" @@ -411,6 +415,14 @@ msgstr "ioctl(fd, SNDCTL_DSP_SPEED, %ld) fehlgeschlagen: %s\n" msgid "ioctl(fd, SNDCTL_DSP_STEREO, %d) failed: %s\n" msgstr "ioctl(fd, SNDCTL_DSP_STEREO, %d) fehlgeschlagen: %s\n" +#, c-format +msgid "pw_stream_connect failed: %s\n" +msgstr "pw_stream_connect fehlgeschlagen: %s\n" + +#, c-format +msgid "pw_stream_set_active failed: %s\n" +msgstr "pw_stream_set_active fehlgeschlagen: %s\n" + #, c-format msgid "snd_pcm_hw_params failed: %s\n" msgstr "snd_pcm_hw_params fehlgeschlagen: %s\n" diff --git a/po/en.po b/po/en.po index 91a2e9e..b1f3044 100644 --- a/po/en.po +++ b/po/en.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.4\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-19 15:29+0100\n" +"POT-Creation-Date: 2024-02-02 22:58+0100\n" "PO-Revision-Date: 2023-11-19 15:29+0100\n" "Last-Translator: Tobias Diedrich \n" "Language-Team: none\n" @@ -413,6 +413,14 @@ msgstr "ioctl(fd, SNDCTL_DSP_SPEED, %ld) failed: %s\n" msgid "ioctl(fd, SNDCTL_DSP_STEREO, %d) failed: %s\n" msgstr "ioctl(fd, SNDCTL_DSP_STEREO, %d) failed: %s\n" +#, c-format +msgid "pw_stream_connect failed: %s\n" +msgstr "pw_stream_connect failed: %s\n" + +#, c-format +msgid "pw_stream_set_active failed: %s\n" +msgstr "pw_stream_set_active failed: %s\n" + #, c-format msgid "snd_pcm_hw_params failed: %s\n" msgstr "snd_pcm_hw_params failed: %s\n"