From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Julian=20Offenh=C3=A4user?= <offenhaeuser@protonmail.com>
Date: Fri, 23 Feb 2024 21:17:23 +0100
Subject: [PATCH] Add support for Minerva LibAudio

---
 src/Makefile.am        |   4 ++
 src/sound.c            |   1 +
 src/sound.h            |   1 +
 src/sound_minerva.cpp | 157 +++++++++++++++++++++++++++++++++++++++++
 src/xmp.conf           |   2 +-
 5 files changed, 164 insertions(+), 1 deletion(-)
 create mode 100644 src/sound_minerva.cpp

diff --git a/src/Makefile.am b/src/Makefile.am
index b65eb85244acec722b2f240976f184f06db11a27..6745ed1d04cde08b9811781c2bfe552d488a6a82 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -2,6 +2,7 @@
 
 AM_CPPFLAGS = -DSYSCONFDIR=\"${sysconfdir}/${PACKAGE_NAME}\" ${LIBXMP_CFLAGS} \
               ${alsa_CFLAGS} ${pulseaudio_CFLAGS}
+AM_CXXFLAGS = -std=c++23 -fno-exceptions
 AM_CFLAGS   = -Wall
 
 bin_PROGRAMS = xmp
@@ -93,6 +94,9 @@ xmp_SOURCES += sound_dart.c
 xmp_LDADD   += -lmmpm2
 endif
 
+xmp_SOURCES += sound_minerva.cpp
+xmp_LDADD   += -lcore -lcoreminimal -lcorebasic -lipc -laudio -lstdc++
+
 man_MANS = xmp.1
 
 pkgsysconfdir   = ${sysconfdir}/${PACKAGE_NAME}
diff --git a/src/sound.c b/src/sound.c
index b6d0c971876a555e27a628b0517a8dd653f24dff..04935412baf8a17558dcc62914d4a5cc1a72c49d 100644
--- a/src/sound.c
+++ b/src/sound.c
@@ -73,6 +73,7 @@ void init_sound_drivers(void)
 #ifdef SOUND_SB
 	register_sound_driver(&sound_sb);
 #endif
+    register_sound_driver(&sound_minerva);
 	register_sound_driver(&sound_wav);
 	register_sound_driver(&sound_aiff);
 	register_sound_driver(&sound_file);
diff --git a/src/sound.h b/src/sound.h
index 153a6c7cb6f6f1431e00b5a10a8a36d3fe7ab7db..beebb83fa3cccdb53bdd2f6893307e476460373f 100644
--- a/src/sound.h
+++ b/src/sound.h
@@ -41,6 +41,7 @@ extern struct sound_driver sound_beos;
 extern struct sound_driver sound_aix;
 extern struct sound_driver sound_ahi;
 extern struct sound_driver sound_sb;
+extern struct sound_driver sound_minerva;
 
 #define parm_init(p) { char *token; for (; *(p); (p)++) { \
 	char s[80]; strncpy(s, *(p), 79); s[79] = 0; \
diff --git a/src/sound_minerva.cpp b/src/sound_minerva.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..94e5f70953599a8292d1cd0980009bd83363ab06
--- /dev/null
+++ b/src/sound_minerva.cpp
@@ -0,0 +1,157 @@
+#include "sound.h"
+
+#include <AK/OwnPtr.h>
+#include <AK/RefPtr.h>
+#include <LibAudio/ConnectionToServer.h>
+#include <LibAudio/Sample.h>
+#include <LibAudio/SampleFormats.h>
+
+static RefPtr<Audio::ConnectionToServer> client;
+static OwnPtr<Core::EventLoop> event_loop;
+
+static Array<Audio::Sample, Audio::AUDIO_BUFFER_SIZE> output_buffer {};
+static size_t output_buffer_samples_remaining { 0 };
+
+static int format;
+static int channels;
+static int bytes_per_sample;
+
+static int init(struct options* options)
+{
+    event_loop = make<Core::EventLoop>();
+    client = MUST(Audio::ConnectionToServer::try_create());
+
+    format = options->format;
+    channels = format & XMP_FORMAT_MONO ? 1 : 2;
+    bytes_per_sample = format & XMP_FORMAT_8BIT ? 1 : 2;
+
+    u32 sample_rate = options->rate;
+    client->set_self_sample_rate(sample_rate);
+
+    client->async_start_playback();
+
+    return 0;
+}
+
+static void deinit(void)
+{
+    client->die();
+}
+
+template<typename T>
+static double read_sample(void*& b)
+{
+    T sample = *(T*)b;
+    b = bit_cast<void*>(bit_cast<FlatPtr>(b) + sizeof(T));
+
+    // Remap integer samples to normalized floating-point range of -1 to 1.
+    if constexpr (IsIntegral<T>) {
+        if constexpr (NumericLimits<T>::is_signed()) {
+            // Signed integer samples are centered around zero, so this division is enough.
+            return static_cast<double>(AK::convert_between_host_and_little_endian(sample)) / static_cast<double>(NumericLimits<T>::max());
+        } else {
+            // Unsigned integer samples, on the other hand, need to be shifted to center them around zero.
+            // The first division therefore remaps to the range 0 to 2.
+            return static_cast<double>(AK::convert_between_host_and_little_endian(sample)) / (static_cast<double>(NumericLimits<T>::max()) / 2.0) - 1.0;
+        }
+    } else {
+        return static_cast<double>(AK::convert_between_host_and_little_endian(sample));
+    }
+}
+
+/* Build and write one tick (one PAL frame or 1/50 s in standard vblank
+ * timed mods) of audio data to the output device.
+ */
+static void play(void* b, int i)
+{
+    int samples_per_channel = i / (channels * bytes_per_sample);
+    int unsigned_samples = format & XMP_FORMAT_UNSIGNED;
+
+    auto const sleep_spec = Duration::from_nanoseconds(100).to_timespec();
+
+    while (samples_per_channel > 0) {
+        // Fill up the output buffer
+        auto const samples_to_process = min(samples_per_channel, Audio::AUDIO_BUFFER_SIZE - output_buffer_samples_remaining);
+
+        for (int i = 0; i < samples_to_process; ++i) {
+            float left, right;
+
+            auto convert_sample = [&]() -> float {
+                if (unsigned_samples) {
+                    if (format & XMP_FORMAT_8BIT)
+                        return read_sample<u8>(b);
+                    else
+                        return read_sample<u16>(b);
+                } else {
+                    if (format & XMP_FORMAT_8BIT)
+                        return read_sample<i8>(b);
+                    else
+                        return read_sample<i16>(b);
+                }
+                return 0.0f;
+            };
+
+            left = convert_sample();
+            if (format & XMP_FORMAT_MONO) {
+                right = left;
+            } else {
+                right = convert_sample();
+            }
+
+            output_buffer[output_buffer_samples_remaining + i] = Audio::Sample { left, right };
+        }
+        output_buffer_samples_remaining += samples_to_process;
+        samples_per_channel -= samples_to_process;
+
+        // Stop if we don't have enough samples to fill a buffer
+        if (output_buffer_samples_remaining < Audio::AUDIO_BUFFER_SIZE)
+            break;
+
+        // Try to enqueue our output buffer
+        for (;;) {
+            auto enqueue_result = client->realtime_enqueue(output_buffer);
+            if (!enqueue_result.is_error())
+                break;
+            if (enqueue_result.error() != Audio::AudioQueue::QueueStatus::Full)
+                return;
+
+            nanosleep(&sleep_spec, nullptr);
+        }
+        output_buffer_samples_remaining = 0;
+    }
+
+    // Pump our event loop - should just be the IPC call to start playback
+    for (;;) {
+        auto number_of_events_pumped = event_loop->pump(Core::EventLoop::WaitMode::PollForEvents);
+        if (number_of_events_pumped == 0)
+            break;
+    }
+}
+
+static void flush(void)
+{
+}
+
+static void onpause(void)
+{
+}
+
+static void onresume(void)
+{
+}
+
+static char const* const help[] = {
+    NULL
+};
+
+struct sound_driver sound_minerva = {
+    "minerva",
+    "Minerva AudioServer",
+    help,
+    init,
+    deinit,
+    play,
+    flush,
+    onpause,
+    onresume
+};
diff --git a/src/xmp.conf b/src/xmp.conf
index 08166a0e7e5f2cefa69fc1c843ca27c5388cbdf1..fa9ace68299cb0c503276629b809b83edad5cf31 100644
--- a/src/xmp.conf
+++ b/src/xmp.conf
@@ -19,7 +19,7 @@
 # driver = <name>
 # Output device to be used
 #
-#driver = alsa
+driver = minerva
 
 
 # ALSA driver
