commit 32d81cfa154f20bf14e0bdda38f6fefdb9a3e4a7 Author: Scan Date: Wed Aug 28 23:32:05 2024 -0400 Initial re-upload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2de2978 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/bin/ +/build/ +/local/ + diff --git a/.gitmodules b/.gitmodules new file mode 100755 index 0000000..253f986 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,16 @@ +[submodule "lib/cmake-various"] + path = lib/cmake-various + url = https://codeberg.org/lilium/cmake-various.git + branch = master +[submodule "lib/make-various"] + path = lib/make-various + url = https://codeberg.org/lilium/make-various.git + branch = master +[submodule "lib/exec_u001"] + path = lib/exec_u001 + url = https://codeberg.org/lilium/exec_u001.git + branch = master +[submodule "lib/yyjson"] + path = lib/yyjson + url = https://github.com/ibireme/yyjson.git + branch = master diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..399d6cd --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Extensions +markdown-navigator*.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..9299273 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,86 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e650cf6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Clean.xml b/.idea/runConfigurations/Clean.xml new file mode 100644 index 0000000..d950de2 --- /dev/null +++ b/.idea/runConfigurations/Clean.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Debug_x64.xml b/.idea/runConfigurations/Debug_x64.xml new file mode 100644 index 0000000..20cfa6a --- /dev/null +++ b/.idea/runConfigurations/Debug_x64.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Debug_x86.xml b/.idea/runConfigurations/Debug_x86.xml new file mode 100644 index 0000000..06a401d --- /dev/null +++ b/.idea/runConfigurations/Debug_x86.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Release_x64.xml b/.idea/runConfigurations/Release_x64.xml new file mode 100644 index 0000000..b87f6b8 --- /dev/null +++ b/.idea/runConfigurations/Release_x64.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Release_x86.xml b/.idea/runConfigurations/Release_x86.xml new file mode 100644 index 0000000..a8afd7a --- /dev/null +++ b/.idea/runConfigurations/Release_x86.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/passthrough.xml b/.idea/runConfigurations/passthrough.xml new file mode 100644 index 0000000..fce8221 --- /dev/null +++ b/.idea/runConfigurations/passthrough.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ef45d3d --- /dev/null +++ b/Makefile @@ -0,0 +1,67 @@ +#!/usr/bin/make -s -f +# Usage: make -Rs TARGET_ARCH= TARGET_TYPE= +# Example: make -Rs build TARGET_ARCH=x64 TARGET_TYPE=Release + +## Available user recipes ## +.PHONY: tests distribution build clean build_all clean_all +distribution: + $(error "Unimplemented") +tests: + $(error "Unimplemented") +passthrough: + true +build: sanity_checks yyjson@post bmsound-pw@post bmsound-wine@post test-client@post + echo "Build completed: [$(TARGET_TYPE)_$(TARGET_ARCH)]" + echo +clean: sanity_checks + echo "Purging build files at : 'build/$(TARGET_TYPE)/$(TARGET_ARCH)'" + rm -rf build/$(TARGET_TYPE)/$(TARGET_ARCH) + echo +build_all: + $(MAKE) build TARGET_ARCH=x64 TARGET_TYPE=Release + $(MAKE) build TARGET_ARCH=x86 TARGET_TYPE=Release + $(MAKE) build TARGET_ARCH=x64 TARGET_TYPE=Debug + $(MAKE) build TARGET_ARCH=x86 TARGET_TYPE=Debug +clean_all: + $(MAKE) clean TARGET_ARCH=x64 TARGET_TYPE=Release + $(MAKE) clean TARGET_ARCH=x86 TARGET_TYPE=Release + $(MAKE) clean TARGET_ARCH=x64 TARGET_TYPE=Debug + $(MAKE) clean TARGET_ARCH=x86 TARGET_TYPE=Debug + +## Load extensions ## +CC_STANDARD = 99 +CXX_STANDARD = 20 +CC_FLAGS += -fms-extensions -Wno-microsoft-anon-tag -Wno-narrowing -Wno-conversion +include ./lib/make-various/extension/MakeEx.mk + +## Proxy targets ## +ifneq (,$(findstring all,$(MAKECMDGOALS))) +else ifneq (,$(findstring passthrough,$(MAKECMDGOALS))) +else ifneq (,$(findstring clean,$(MAKECMDGOALS))) +else + +## Add projects ## +# libyyjson.a +$(call cmake_target,lib/yyjson,YYJSON_DISABLE_WRITER=ON) +$(target): $(target)@pre + $(call cmake_build,$@) + +# bmsound-pw.so +include $(SRC_DIR)/bmsound-pw/Makefile.mk + +# bmsound-wine.{dll:so} +include $(SRC_DIR)/bmsound-wine/Makefile.mk + +# test-client.bin +include $(SRC_DIR)/test-client/Makefile.mk + +## Post-setup Info ## +$(info +----+) +$(info Build revision: $(VERSION)) +$(info Working directory: $(shell echo "$$PWD")) +$(info Configuration name: [$(TARGET_TYPE)_$(TARGET_ARCH)]) +$(info Target executed: [$(MAKECMDGOALS)]) +$(info CC_FLAGS: [$(CC_FLAGS)]) +$(info CXX_FLAGS: [$(CXX_FLAGS)]) +$(info +----+) +endif diff --git a/README.md b/README.md new file mode 100644 index 0000000..4babca6 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Library structure + +## bmsound-wine +This library acts as a bridge between windows<->unix libraries running inside wine environment. +Makefile environment specific to wine applies (see [winebuild](https://www.winehq.org/docs/winebuild)). + +## bmsound-pw +This library runs fully in unix userspace and is not directly aware of wine environment. +This library implements various handlers for translating bemani audio interface callbacks into ones compatible with pipewire's native API. +Makefile environment native to unix applies. + +# Supported formats +Audio formats tested. Listed by lookup priority where applicable. + +## IIDX +| Format | Support | +|:--------------------|:---------| +| 44.1kHz@32bit 7.1ch | ✘ | +| 44.1kHz@24bit 7.1ch | ✘ | +| 44.1kHz@16bit 7.1ch | ✘ | +| 44.1kHz@32bit 5.1ch | ✘ | +| 44.1kHz@16bit 7.1ch | ✘ | +| 44.1kHz@32bit 3.1ch | ✘ | +| 44.1kHz@16bit 3.1ch | ✘ | +| 44.1kHz@16bit 2ch | ✔ | + +# Building notes +* define build-wide `BMSW_NOEXPERIMENTAL` to build without additional test code/unstable APIs +* define build-wide `BMSW_NODBGEXEC` to disable compiling of any additional issue tracing code +* changes to `translation_unit.frag.c` will not trigger `translation_unit.o` rebuild, applying any change directly to `translation_unit.c` will diff --git a/src/bmsound-pw/Client/bmswpw_callbacks.c b/src/bmsound-pw/Client/bmswpw_callbacks.c new file mode 100644 index 0000000..a5a9778 --- /dev/null +++ b/src/bmsound-pw/Client/bmswpw_callbacks.c @@ -0,0 +1,98 @@ +#include "bmswpw_callbacks.h" +#include "bmswpw_defines.h" +#include "bmswpw_util.h" +#include "bmsound_experimental.h" +#include +#include +#include + + +/* Internally reused */ +#define buffer_set(__b, __val, __size) memset((__b)->buffer->datas[0].data, (__val), (__size)) +#define buffer_cpy(__b, __val, __size) memcpy((__b)->buffer->datas[0].data, (__val), (__size)); +static inline int process_beg(bmsw_pwdata_t *data, pw_buffer_t **b) +{ + //pw_time_t time; + pw_buffer_t *buffer = pw_stream_dequeue_buffer(data->stream); + if (!buffer) + { + DBGEXEC(printf("out of buffers: %s\n", strerror(errno));) + return 0; + } + + int nframes = buffer->buffer->datas[0].maxsize / data->format->stride; +#if PW_CHECK_VERSION(0, 3, 49) + if (buffer->requested != 0) + nframes = SPA_MIN(buffer->requested, nframes); +#endif + //if (!(*b)->buffer->datas[0].data) return; + *b = buffer; + return nframes; +} +static inline void process_end(bmsw_pwdata_t *data, pw_buffer_t *b, int nframes) +{ + b->buffer->datas[0].chunk->size = nframes * data->format->stride; + b->buffer->datas[0].chunk->offset = 0; + b->buffer->datas[0].chunk->stride = data->format->stride; + pw_stream_queue_buffer(data->stream, b); +} + +/* Handlers */ +// Communicates with caller through ambiguous notification callback _INFO: in chunk size may have to be bigger/smaller than pipewire internal +void process_notif_callback(void *rdata) +{ + // Init + pw_buffer_t *buffer; + bmsw_pwdata_t *data = rdata; + int nframes = process_beg(data, &buffer); + if (!nframes) return; + + // Process + pthread_mutex_lock(&data->node->lock); + DBGEXEC(if (data->node->in.cursor > 5000) printf("BUFFER UNDERRUN\n");) + if (data->node->in.cursor) + { + nframes = data->node->in.cursor / data->format->stride; + buffer_cpy(buffer, data->node->in.u8, data->node->in.cursor); + data->node->in.cursor = 0; + pthread_mutex_unlock(&data->node->lock); + } + else + { + pthread_mutex_unlock(&data->node->lock); + DBGEXEC(printf("BUFFER OVERRUN\n");) + buffer_set(buffer, 0, nframes * data->format->stride); + } + data->node->event_cb(data->node->event_cb_arg); + + // Finish + process_end(data, buffer, nframes); +} +// Playbacks buffer according to internal timer without communicating with caller, assuming buffer is always available _INFO: critical syncing part code is missing _BUG: may need mutex at least for syncing +void process_twin_cursor(void *rdata) +{ + // Init + pw_buffer_t *buffer; + bmsw_pwdata_t *data = rdata; + int nframes = process_beg(data, &buffer); + if (!nframes) return; + + // Process + //int framemax=data->node->in.size/data->format->stride; + int nbeg = data->node->out.cursor; + int nend = nbeg + nframes * data->format->stride; + if (nend > data->node->in.size) + { + buffer_cpy(buffer, data->node->in.u8 + nbeg, data->node->in.size - nbeg); + nbeg = 0; + nend -= data->node->in.size; + } + buffer_cpy(buffer, data->node->in.u8 + nbeg, nend - nbeg); + data->node->out.cursor = nend; + + // Finish + process_end(data, buffer, nframes);//_BUG: should set amount of copied frames (so in case of insufficient node.in less than requested) +} + +/* Experimental */ +#include "bmswexp_callbacks.frag.c" diff --git a/src/bmsound-pw/Client/bmswpw_callbacks.h b/src/bmsound-pw/Client/bmswpw_callbacks.h new file mode 100644 index 0000000..3ab8204 --- /dev/null +++ b/src/bmsound-pw/Client/bmswpw_callbacks.h @@ -0,0 +1,8 @@ +#ifndef _BMSWPW_CALLBACKS_H +#define _BMSWPW_CALLBACKS_H + + +void process_notif_callback(void *rdata); +void process_twin_cursor(void *rdata); + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/Client/bmswpw_defines.h b/src/bmsound-pw/Client/bmswpw_defines.h new file mode 100644 index 0000000..c367ef8 --- /dev/null +++ b/src/bmsound-pw/Client/bmswpw_defines.h @@ -0,0 +1,72 @@ +#ifndef _BMSWPW_DEFINES_H +#define _BMSWPW_DEFINES_H +#include +#include +#include + + +typedef struct pw_stream_events pw_stream_events_t; +typedef struct pw_thread_loop pw_thread_loop_t; +typedef struct pw_main_loop pw_main_loop_t; +typedef struct pw_stream pw_stream_t; +typedef struct pw_properties pw_properties_t; +typedef struct pw_buffer pw_buffer_t; +typedef struct pw_time pw_time_t; +typedef struct spa_buffer spa_buffer_t; +typedef struct spa_pod_builder spa_pod_builder_t; +typedef struct spa_pod spa_pod_t; +typedef struct spa_audio_info_raw spa_audio_info_raw_t; +typedef struct bmsw_audio_info_raw bmsw_audio_info_raw_t; +typedef struct bmsw_pwout bmsw_pwout_t; +typedef struct bmsw_pwdata bmsw_pwdata_t; +typedef void (*bmsw_audio_callback_t)(void *); + +struct bmsw_pwdata /* callback only data */ +{ + pw_stream_t *stream; // buffer stream *managed by pipewire + pw_properties_t *properties; // buffer stream properties *managed by pipewire + bmsw_audio_info_raw_t *format; // stream audio format(s) *managed by bmswpw + + // Specialized payload (placeholder) + bmsw_pwout_t *node; // master recursion +}; +struct bmsw_pwout /* output only pipewire node */ +{ + struct + { + union + { + uint8_t *u8; + int16_t *s16; + }; // raw buffer *managed by bmswpw_buffer + int cursor; // position in raw buffer + uint32_t size; // raw buffer max size in bytes + } in, out;//in: iidx incoming out: used by pipewire stream + + union + { + pw_thread_loop_t *concurrent; + pw_main_loop_t *sequential; + } event_loop; // stream event loop + pw_stream_events_t event_handler; // events called on data (operates mostly on event_data) + bmsw_pwdata_t event_data; + bmsw_audio_callback_t event_cb; // optional function called each time buffer has been processed + void *event_cb_arg; + + //_REM: experimental, possible target to removal + pw_buffer_t *q_buf; + pthread_mutex_t lock; +}; + +/* Extensions */ +struct bmsw_audio_info_raw +{ + struct spa_audio_info_raw; + int stride; + int frames; +}; + +/* Common audio formats *///_REV: refactor as lookup table if necessary for multi-format support +#define BMSPWM_SPA_FORMAT SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_S16, .channels = bmsw_config->audio_channels, .rate = bmsw_config->audio_rate) + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/Client/bmswpw_util.c b/src/bmsound-pw/Client/bmswpw_util.c new file mode 100644 index 0000000..cb3d580 --- /dev/null +++ b/src/bmsound-pw/Client/bmswpw_util.c @@ -0,0 +1,9 @@ +#include +#include "bmswpw_util.h" +#include "bmsound_experimental.h" + + +void cb_empty(void *arg) +{ + //DBGEXEC(printf("cb_empty\n")); +} diff --git a/src/bmsound-pw/Client/bmswpw_util.h b/src/bmsound-pw/Client/bmswpw_util.h new file mode 100644 index 0000000..e363838 --- /dev/null +++ b/src/bmsound-pw/Client/bmswpw_util.h @@ -0,0 +1,10 @@ +#ifndef _BMSWPW_UTIL_H +#define _BMSWPW_UTIL_H + + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define MIN(a, b) ((a) > (b) ? (b) : (a)) + +void cb_empty(void *arg); + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/Core/bmsound_config.c b/src/bmsound-pw/Core/bmsound_config.c new file mode 100644 index 0000000..c495acd --- /dev/null +++ b/src/bmsound-pw/Core/bmsound_config.c @@ -0,0 +1,66 @@ +#include "bmsound_config.h" +#include "bmsound_experimental.h" +#include +#include + + +const bmsw_config_t *bmsw_config = NULL; +static bmsw_config_t bmsw_config_; + +int parse_json(const char *path) +{ + union + { + int64_t sint; + uint64_t uint; + double num; + const char *str; + } yy; + yyjson_doc *cfgjson = yyjson_read_file(path, 0, NULL, NULL); + yyjson_val *cfgroot = yyjson_doc_get_root(cfgjson); + if (!cfgroot) return 0; + + //_INFO: *get_sint() fails for valid positive sint values (so it's get negative, more than get signed long) + if (yyjson_ptr_get_str(cfgroot, "/audio/profile", &yy.str)) + { + DBGEXEC(printf("Updating profile: '%s' -> '%s'\n", bmswexp_name_by_profile(*bmsw_config_.exp_profile), yy.str)); + *bmsw_config_.exp_profile = bmswexp_profile_by_name(yy.str); + } + if (yyjson_ptr_get_uint(cfgroot, "/audio/fpc", &yy.uint)) + { + DBGEXEC(printf("Updating audio fpc: '%d' -> '%ld'\n", bmsw_config_.audio_fpc, yy.uint)); + bmsw_config_.audio_fpc = yy.sint; + } + if (yyjson_ptr_get_uint(cfgroot, "/audio/channels", &yy.uint)) + { + DBGEXEC(printf("Updating audio channels: '%d' -> '%ld'\n", bmsw_config_.audio_channels, yy.uint)); + bmsw_config_.audio_channels = yy.sint; + } + if (yyjson_ptr_get_uint(cfgroot, "/audio/depth", &yy.uint)) + { + DBGEXEC(printf("Updating audio depth: '%d' -> '%ld'\n", bmsw_config_.audio_depth, yy.uint)); + bmsw_config_.audio_depth = yy.sint; + } + + yyjson_doc_free(cfgjson); + return 1; +} + +void bmsw_config_init(const char *path) +{ + bmsw_config = &bmsw_config_; + bmsw_config_.exp_profile = &bmswexp_profile; + + // Defaults as per config specification in vault + *bmsw_config_.exp_profile = T_NOTIF_SPICE;//_REV: T_NONE <- stable(should be default) + bmsw_config_.audio_fpc = 64; + bmsw_config_.audio_channels = 2; + bmsw_config_.audio_depth = 16; + bmsw_config_.audio_rate = 44100; + + // User config + if (!parse_json(path)) + { + if (path) DBGEXEC(printf("Invalid config: '%s'\n", path)); + } +} diff --git a/src/bmsound-pw/Core/bmsound_config.h b/src/bmsound-pw/Core/bmsound_config.h new file mode 100644 index 0000000..197530e --- /dev/null +++ b/src/bmsound-pw/Core/bmsound_config.h @@ -0,0 +1,20 @@ +#ifndef _BMSOUND_CONFIG_H +#define _BMSOUND_CONFIG_H +#include "bmsound_experimental.h" + + +typedef struct bmsw_config bmsw_config_t; +struct bmsw_config +{ + profile_exp_t *exp_profile; + int audio_fpc; // frames requested per each chunk/process call + int audio_channels; + int audio_depth; // depth in bits + int audio_rate; // always 44.1kHz +}; + +extern const bmsw_config_t *bmsw_config; + +void bmsw_config_init(const char *path); // reinitializes bmsw_config, can be called multiple times, path=NULL for reset to default values + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/Experimental/bmsound_experimental.c b/src/bmsound-pw/Experimental/bmsound_experimental.c new file mode 100644 index 0000000..aa6680d --- /dev/null +++ b/src/bmsound-pw/Experimental/bmsound_experimental.c @@ -0,0 +1,32 @@ +#include +#include +#include "bmsound_experimental.h" + + +void *bmsw_experiment_dispatcher[T_LAST][FPTR_MAX]; +profile_exp_t bmswexp_profile = T_NONE; +const char *const bmswexp_profile_name[T_LAST] = { + [T_NONE] = "stable", + [T_AUDIO_FORMAT] = "audio_format", + [T_NOTIF_CALLBACK] = "notif_callback", + [T_NOTIF_SPICE] = "notif_spice", + [T_SIGNAL_SPICE] = "signal_spice", + [T_TWIN_CURSOR] = "twin_cursor" +}; + +profile_exp_t bmswexp_profile_by_name(const char *name) +{ + for (profile_exp_t profile = T_NONE; profile < T_LAST; profile++) + { + if (bmswexp_profile_name[profile] && !strcmp(name, bmswexp_profile_name[profile])) + return profile; + } + DBGEXEC(printf("Invalid profile '%s'\n", name)); + return bmswexp_profile; +} +const char *bmswexp_name_by_profile(profile_exp_t profile) +{ + static const char *profile_invalid = "invalid"; + if (profile >= T_NONE && profile < T_LAST && bmswexp_profile_name[profile]) return bmswexp_profile_name[profile]; + return profile_invalid; +} diff --git a/src/bmsound-pw/Experimental/bmsound_experimental.h b/src/bmsound-pw/Experimental/bmsound_experimental.h new file mode 100644 index 0000000..99788df --- /dev/null +++ b/src/bmsound-pw/Experimental/bmsound_experimental.h @@ -0,0 +1,60 @@ +#ifndef _BMSOUND_EXPERIMENTAL_H +#define _BMSOUND_EXPERIMENTAL_H +#include + + +#define FPTR_MAX 16 +/* Experimental function forward declarations */ +// Client scope +enum bmswpw_exp_i +{ + bmswpw_get_sbuf_i, + bmswpw_send_sbuf_i, + bmswpw_process_i, + bmswpw_callback_i, + bmswpw_exp_i_LAST +}; +typedef unsigned char *(*bmswpw_get_sbuf_t)(void *, uint32_t); +typedef int (*bmswpw_send_sbuf_t)(void *, uint32_t); +typedef void (*bmswpw_process_t)(void *); +typedef void (*bmswpw_callback_t)(void *); +// Common/shared scope +enum none_exp_i +{ + bmswpw_update_process_i, + bmswpw_replace_buffer_i, + none_exp_i_LAST +}; +typedef int (*bmswpw_update_process_t)(void *, bmswpw_process_t); +typedef void *(*bmswpw_replace_buffer_t)(void *, void *, int); + +/* Experimental API wrappers */ +enum profile_exp +{ + T_NONE = 0, // doubles as profile independent storage + + /* audio backend tests */ + T_AUDIO_FORMAT, + T_NOTIF_CALLBACK, + T_NOTIF_SPICE, // utilizes notif_callback implementation, but syncs server to spice(client) instead of client to server + T_SIGNAL_SPICE, // utilizes notif_spice implementation, but without busy lock _INFO: working, stable candidate + T_TWIN_CURSOR, + T_SPOOFED_LOOP, + T_STATIC_SINE, + + T_LAST +}; +typedef enum profile_exp profile_exp_t; +extern void *bmsw_experiment_dispatcher[T_LAST][FPTR_MAX]; +extern profile_exp_t bmswexp_profile; +#define EXPERIMENTAL(__expid, __func) (((__func##_t)bmsw_experiment_dispatcher[__expid][__func##_i])) +#ifndef BMSW_NODBGEXEC +#define DBGEXEC(__body) __body +#else +#define DBGEXEC(__body) +#endif + +profile_exp_t bmswexp_profile_by_name(const char *name); +const char *bmswexp_name_by_profile(profile_exp_t profile); + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/Experimental/bmsound_test.h b/src/bmsound-pw/Experimental/bmsound_test.h new file mode 100644 index 0000000..4254512 --- /dev/null +++ b/src/bmsound-pw/Experimental/bmsound_test.h @@ -0,0 +1,14 @@ +#ifndef _BMSOUND_TEST_H +#define _BMSOUND_TEST_H +/*_INFO: + * Forwards declaration of experimental functions that can be used as self-contained test units + * Exists mainly to ensure API consistency + * + * */ + + +/* Simple audio output */ +int bmswtest_client_concurrent(const char *title); // threaded sine +int bmswtest_client_sequential(const char *title); // blocking sine + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/Experimental/bmswexp_callbacks.frag.c b/src/bmsound-pw/Experimental/bmswexp_callbacks.frag.c new file mode 100644 index 0000000..06dab80 --- /dev/null +++ b/src/bmsound-pw/Experimental/bmswexp_callbacks.frag.c @@ -0,0 +1,78 @@ +#ifndef BMSW_NOEXPERIMENTAL + + +/* Tests */ +// Playback static sine sound +void process_sine(void *userdata) +{ +#define process_sine_CHANNELS 2 +#define process_sine_RATE 44100 +#define process_sine_VOLUME 0.7 + static double accumulator; + bmsw_pwdata_t *data = userdata; + pw_buffer_t *b; + spa_buffer_t *buf; + int i, c, n_frames, stride; + int16_t *dst, val; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) + { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + if ((dst = buf->datas[0].data) == NULL) + return; + + stride = sizeof(int16_t) * process_sine_CHANNELS; + n_frames = buf->datas[0].maxsize / stride; + + for (i = 0; i < n_frames; i++) + { + accumulator += (M_PI + M_PI) * 440 / process_sine_RATE; + if (accumulator >= (M_PI + M_PI)) + accumulator -= (M_PI + M_PI); + + val = sin(accumulator) * process_sine_VOLUME * 16767.f; + for (c = 0; c < process_sine_CHANNELS; c++) + *dst++ = val; + } + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = stride; + buf->datas[0].chunk->size = n_frames * stride; + + pw_stream_queue_buffer(data->stream, b); +} +// Playback buffer directly as it is, for testing stream format only +void process_audio_format(void *rdata) +{ + // Init + pw_buffer_t *buffer; + bmsw_pwdata_t *data = rdata; + int nframes = process_beg(data, &buffer); + if (!nframes) return; + + // Process + int nbeg = data->node->out.cursor; + int nend = nbeg + nframes * data->format->stride; + buffer_cpy(buffer, data->node->in.u8 + nbeg, nend - nbeg); + data->node->out.cursor = nend; + + // Finish + process_end(data, buffer, nframes);//_BUG: should set amount of copied frames (so in case of insufficient node.in less than required) +} + +/* Init */ +void bmswexp_callbacks() +{ + bmsw_experiment_dispatcher[T_NOTIF_CALLBACK][bmswpw_process_i] = process_notif_callback; + bmsw_experiment_dispatcher[T_NOTIF_SPICE][bmswpw_process_i] = process_notif_callback; //_INFO: no change + bmsw_experiment_dispatcher[T_SIGNAL_SPICE][bmswpw_process_i] = process_notif_callback; + bmsw_experiment_dispatcher[T_TWIN_CURSOR][bmswpw_process_i] = process_twin_cursor; + bmsw_experiment_dispatcher[T_AUDIO_FORMAT][bmswpw_process_i] = process_audio_format; + bmsw_experiment_dispatcher[T_STATIC_SINE][bmswpw_process_i] = process_sine; +} + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/Experimental/bmswexp_pw.frag.c b/src/bmsound-pw/Experimental/bmswexp_pw.frag.c new file mode 100644 index 0000000..34b9d62 --- /dev/null +++ b/src/bmsound-pw/Experimental/bmswexp_pw.frag.c @@ -0,0 +1,236 @@ +#ifndef BMSW_NOEXPERIMENTAL + + +/* Wrappers () *///_REV: This is pretty ugly structure-wise right now and probably should be moved elsewhere (separate unit) +volatile int buffer_ready = 0; +pthread_cond_t buffer_signal = PTHREAD_COND_INITIALIZER; +void callback_notif_spice(void *arg) +{ + /*_INFO: Block until spicetools is done + * stream: process()->buffer_ready:->buffer_ready?->process() + * client: get_framesize()->get_buffer()->buffer_ready?:->release_buffer()->buffer_ready->SetEvent()->get_framesize() + * _TODO: alternative, try moving callback to before buffer_cpy() not after, this would require different pipe than above and use nanotime on spice side + * */ + buffer_ready = 0; + while (!buffer_ready); +} +void callback_signal_spice(void *arg) +{ + bmsw_pwout_t *this = (bmsw_pwout_t *) arg; + + pthread_mutex_lock(&this->lock); + buffer_ready = 0; + pthread_cond_signal(&buffer_signal); + while (!buffer_ready) pthread_cond_wait(&buffer_signal, &this->lock); //_INFO: mutex relocked on return + pthread_mutex_unlock(&this->lock); +} + +/* Experimental::bmswpw_buffer_t */ +unsigned char *bmswpw_get_sbuf_signal_spice(bmsw_pwout_t *this, uint32_t n) +{ + pthread_mutex_lock(&this->lock); + while (buffer_ready) pthread_cond_wait(&buffer_signal, &this->lock);//_INFO: mutex relocked on return + return this->in.u8 + this->in.cursor; +} +int bmswpw_send_sbuf_signal_spice(bmsw_pwout_t *this, uint32_t n) +{ + this->in.cursor += n * this->event_data.format->stride; + if (this->in.cursor > 100000) + { + DBGEXEC(printf("EXTREME BUFFER UNDERRUN DETECTED\n")); + } + + buffer_ready = 1; + pthread_cond_signal(&buffer_signal); + pthread_mutex_unlock(&this->lock); + return 0; +} +unsigned char *bmswpw_get_sbuf_notif_spice(bmsw_pwout_t *this, uint32_t n) +{ + while (buffer_ready); + pthread_mutex_lock(&this->lock); + return this->in.u8 + this->in.cursor; +} +int bmswpw_send_sbuf_notif_spice(bmsw_pwout_t *this, uint32_t n) +{ + this->in.cursor += n * this->event_data.format->stride; + if (this->in.cursor > 100000) + { + DBGEXEC(printf("EXTREME BUFFER UNDERRUN DETECTED\n")); + } + buffer_ready = 1; + pthread_mutex_unlock(&this->lock); + return 0; +} +unsigned char *bmswpw_get_sbuf_twin_cursor(bmsw_pwout_t *this, uint32_t n) +{ + //pw_thread_loop_lock(this->event_loop.concurrent); + return this->in.u8 + this->in.cursor; +} +int bmswpw_send_sbuf_twin_cursor(bmsw_pwout_t *this, uint32_t n) +{ + this->in.cursor += n * this->event_data.format->stride; + if (this->in.cursor > this->in.size) + { + int overbuf = this->in.cursor - this->in.size; + memcpy(this->in.u8, this->in.u8 + this->in.size, overbuf); // let's hope out.cursor is not too ahead/behind and do this + this->in.cursor = overbuf; + } + //pw_thread_loop_unlock(this->event_loop.concurrent); + + return 0; +} +unsigned char *bmswpw_get_sbuf_notif_callback(bmsw_pwout_t *this, uint32_t n) +{ + pthread_mutex_lock(&this->lock); + return this->in.u8 + this->in.cursor; +} +int bmswpw_send_sbuf_notif_callback(bmsw_pwout_t *this, uint32_t n) +{ + this->in.cursor += n * this->event_data.format->stride; + if (this->in.cursor > 100000) + { + DBGEXEC(printf("EXTREME BUFFER UNDERRUN DETECTED\n")); + } + pthread_mutex_unlock(&this->lock); + return 0; +} +unsigned char *bmswpw_get_sbuf_spoofed_loop(bmsw_pwout_t *this, uint32_t n) +{ + if ((this->q_buf = pw_stream_dequeue_buffer(this->event_data.stream)) == NULL || this->q_buf->buffer->datas[0].data == NULL) + { + return NULL; + } + + uint32_t n_max = this->q_buf->buffer->datas[0].maxsize / this->event_data.format->stride; + if (!n || n > n_max) + { + n = n_max; + DBGEXEC(printf("BUFFER UNDERRUN WAS ABOUT TO RESULT IN HEAP CORRUPTION\n")); + return NULL; + } + return (unsigned char *) this->q_buf->buffer->datas[0].data; +} +int bmswpw_send_sbuf_spoofed_loop(bmsw_pwout_t *this, uint32_t n) +{ + this->q_buf->buffer->datas[0].chunk->offset = 0; + this->q_buf->buffer->datas[0].chunk->stride = this->event_data.format->stride; + this->q_buf->buffer->datas[0].chunk->size = n * this->event_data.format->stride; + pw_stream_queue_buffer(this->event_data.stream, this->q_buf); + return 0; +} +int bmswpw_update_process(bmsw_pwout_t *this, bmswpw_process_t func) +{ + this->event_handler.process = func; +} +void *bmswpw_replace_buffer(void *client, void *newbuf, int newsize) +{ + bmsw_pwout_t *client_ = (bmsw_pwout_t *) client; + void *rv = client_->in.u8; + client_->in.u8 = newbuf; + client_->in.size = newsize; + return rv; +} + +/* Tests */ +int bmswtest_client_concurrent(const char *title) +{ + pw_init(NULL, NULL); + + bmsw_pwout_t client; + bmswpw_init_buffer(&client, BMSPWM_SPA_FORMAT); + + bmswpw_init_events(&client, (const pw_stream_events_t) {.process = EXPERIMENTAL(T_STATIC_SINE, bmswpw_process)}, NULL, NULL); + + bmswpw_init_stream(&client, pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Game", + PW_KEY_MEDIA_NAME, "待機", + PW_KEY_APP_NAME, title, + NULL + )); + + bmswpw_poll_start(&client); + + // Stop after 10s + sleep(10); + + bmswpw_poll_stop(&client); + + bmswpw_destroy_buffer(&client); + + return 0; +} +int bmswtest_client_sequential(const char *title) +{ + // pw basic init code + bmsw_pwout_t client = {0,}; + pw_init(NULL, NULL); + + // create a loop object + client.event_loop.sequential = pw_main_loop_new(NULL); + + // create a stream object is this on_get_buffer?? + //_INFO: pw_stream_new() is an alternative with more control, but harder setup + client.event_handler = (pw_stream_events_t) { + PW_VERSION_STREAM_EVENTS, + .process = EXPERIMENTAL(T_STATIC_SINE, bmswpw_process) + }; + client.event_data.stream = pw_stream_new_simple( + pw_main_loop_get_loop(client.event_loop.sequential), + "bmsw-stream", + pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Game", + PW_KEY_MEDIA_NAME, "待機", + PW_KEY_APP_NAME, title, + NULL + ), + &client.event_handler, + &client.event_data + ); + + // This is only format of stream (in sync with on_is_format_supported), example for 2ch@44100hz:16bit depth stream + const spa_pod_t *params[1]; + uint8_t buffer[1024]; + spa_pod_builder_t b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); // builder can go out of scope + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_S16, + .channels = 2, + .rate = 44100)); + + // Connects stream to main loop + pw_stream_connect(client.event_data.stream, + PW_DIRECTION_OUTPUT, // specifies data.stream should be run in output mode + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, // stream flags, see limitations on PW_STREAM_FLAG_RT_PROCESS + params, 1 // params are supported formats + ); + + // Runs polling loop in THIS THREAD + pw_main_loop_run(client.event_loop.sequential); // results in stream_events.process polling and block + + return 0; +} + +/* Init */ +void bmswexp_pw() +{ + bmsw_experiment_dispatcher[T_NOTIF_CALLBACK][bmswpw_get_sbuf_i] = bmswpw_get_sbuf_notif_callback; + bmsw_experiment_dispatcher[T_NOTIF_SPICE][bmswpw_get_sbuf_i] = bmswpw_get_sbuf_notif_spice; + bmsw_experiment_dispatcher[T_SIGNAL_SPICE][bmswpw_get_sbuf_i] = bmswpw_get_sbuf_signal_spice; + bmsw_experiment_dispatcher[T_TWIN_CURSOR][bmswpw_get_sbuf_i] = bmswpw_get_sbuf_twin_cursor; + bmsw_experiment_dispatcher[T_NOTIF_CALLBACK][bmswpw_send_sbuf_i] = bmswpw_send_sbuf_notif_callback; + bmsw_experiment_dispatcher[T_NOTIF_SPICE][bmswpw_send_sbuf_i] = bmswpw_send_sbuf_notif_spice; + bmsw_experiment_dispatcher[T_SIGNAL_SPICE][bmswpw_send_sbuf_i] = bmswpw_send_sbuf_signal_spice; + bmsw_experiment_dispatcher[T_TWIN_CURSOR][bmswpw_send_sbuf_i] = bmswpw_send_sbuf_twin_cursor; + bmsw_experiment_dispatcher[T_NOTIF_SPICE][bmswpw_callback_i] = callback_notif_spice; + bmsw_experiment_dispatcher[T_SIGNAL_SPICE][bmswpw_callback_i] = callback_signal_spice; + + bmsw_experiment_dispatcher[T_NONE][bmswpw_update_process_i] = bmswpw_update_process; + bmsw_experiment_dispatcher[T_NONE][bmswpw_replace_buffer_i] = bmswpw_replace_buffer; +} + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/Makefile.mk b/src/bmsound-pw/Makefile.mk new file mode 100644 index 0000000..da677f2 --- /dev/null +++ b/src/bmsound-pw/Makefile.mk @@ -0,0 +1,25 @@ +## bmsound-pw.so ## +$(call generic_target,bmsound-pw,c) + +# Local #define +$(call incl_define,$(target),) + +# Local includes (#include "") +$(call incl_target_dirs,$(target)) +$(call incl_quoted,$(target),) + +# External includes (#include <>) +$(call incl_angled,$(target),lib/yyjson/src) + +# Local dynamic linker (compile-able libraries) +$(call link_reference_s,$(target),libyyjson) + +# External dynamic linker (binary libraries) - external/system libraries +$(call link_package,$(target),libspa-0.2 libpipewire-0.3) +$(call link_external,$(target),) + +$($(target)_build)/%.o: $(target)@pre $($(target)_src) + $(call gcc_object,bmsound-pw,$*) + +$(target): $($(target)_obj) yyjson@post + $(call gcc_library,bmsound-pw) diff --git a/src/bmsound-pw/bmsound.c b/src/bmsound-pw/bmsound.c new file mode 100644 index 0000000..c69b3b5 --- /dev/null +++ b/src/bmsound-pw/bmsound.c @@ -0,0 +1,22 @@ +#include "bmsound_config.h" +#include + + +// Subunit's private init function listing +void bmswexp_callbacks(); +void bmswexp_pw(); +static void init() __attribute__((constructor)); + +void init() +{ + printf("Initializing bmsound::pipewire\n"); + +#ifndef BMSW_NOEXPERIMENTAL + // Experimental APIs + bmswexp_pw(); + bmswexp_callbacks(); +#endif + + // Built-in config + bmsw_config_init(NULL); +} diff --git a/src/bmsound-pw/bmsound_fx.h b/src/bmsound-pw/bmsound_fx.h new file mode 100644 index 0000000..b51ebb2 --- /dev/null +++ b/src/bmsound-pw/bmsound_fx.h @@ -0,0 +1,3 @@ +#ifndef _BMSOUND_FX_H +#define _BMSOUND_FX_H +#endif \ No newline at end of file diff --git a/src/bmsound-pw/bmsound_pw.c b/src/bmsound-pw/bmsound_pw.c new file mode 100644 index 0000000..8d22a22 --- /dev/null +++ b/src/bmsound-pw/bmsound_pw.c @@ -0,0 +1,217 @@ +#include "bmsound_pw.h" +#include "bmswpw_defines.h" +#include "bmswpw_callbacks.h" +#include "bmswpw_util.h" +#include "bmsound_experimental.h" +#include "bmsound_config.h" +#include +#include +#include +#include +#include + + +/* bmswpw_buffer_t */ +void bmswpw_init_buffer(bmsw_pwout_t *this, const spa_audio_info_raw_t format) +{ + this->event_data.format = malloc(sizeof(bmsw_audio_info_raw_t)); + *(spa_audio_info_raw_t *) this->event_data.format = format; + this->event_data.format->stride = sizeof(int16_t) * this->event_data.format->channels; + this->event_data.format->frames = bmsw_config->audio_fpc; + + //_BUG: size of this buffer matters a lot depending on chosen callback, 10sec may seem overkill, but for twin cursor -O3 makes no difference, size of this does (the bigger, the higher possible in<->out cursor latency distance) + this->in.size = 1764000; + this->in.u8 = calloc(this->in.size + this->event_data.format->rate * this->event_data.format->stride, 1);//+1s out-of-boundaries + this->in.cursor = 0; + + //_REV: out.size should be calculated from format values + this->out.size = 4096; + this->out.u8 = calloc(this->out.size, 1); + this->out.cursor = this->in.size - (this->event_data.format->frames * this->event_data.format->stride);//single chunk latency for twin cursor + + this->event_data.node = this; + pthread_mutex_init(&this->lock, NULL); +} +void bmswpw_init_events(bmsw_pwout_t *this, const pw_stream_events_t handler, void *event_cb, void *event_cb_arg) +{ + this->event_handler = handler; + this->event_handler.version = PW_VERSION_STREAM_EVENTS; + if (!this->event_cb) this->event_cb = cb_empty; + if (event_cb) + { + this->event_cb = event_cb; + this->event_cb_arg = event_cb_arg; + } +} +void bmswpw_init_stream(bmsw_pwout_t *this, pw_properties_t *prop) +{ + if (!this->event_handler.version || !this->event_data.node) return; + this->event_data.properties = prop; + + // Create event loop + this->event_loop.concurrent = pw_thread_loop_new("bmsw", NULL); + + // Create buffer stream + this->event_data.stream = pw_stream_new_simple( + pw_thread_loop_get_loop(this->event_loop.concurrent), + "bmsw-stream", + this->event_data.properties, + &this->event_handler, + &this->event_data + ); + + // Configure format of buffer stream (e.g. 2ch@44100hz:16bit depth stream) + spa_pod_builder_t pod = SPA_POD_BUILDER_INIT(this->out.u8, this->out.size); + const spa_pod_t *params[1]; //_INFO: allowed to go out of scope + params[0] = spa_format_audio_raw_build(&pod, SPA_PARAM_EnumFormat, (spa_audio_info_raw_t *) this->event_data.format); + + // Connect buffer stream with event loop + pw_stream_connect(this->event_data.stream, + PW_DIRECTION_OUTPUT, // specifies buffer stream as output only + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, // buffer stream flags, see limitations arising with PW_STREAM_FLAG_RT_PROCESS + params, 1 + ); +} +void bmswpw_poll_start(bmsw_pwout_t *this) +{ + this->in.cursor = 0; + this->out.cursor = this->in.size - (this->event_data.format->frames * this->event_data.format->stride); + pw_thread_loop_start(this->event_loop.concurrent); +} +void bmswpw_poll_stop(bmsw_pwout_t *this) +{ + pw_thread_loop_stop(this->event_loop.concurrent); +} +void bmswpw_destroy_buffer(bmsw_pwout_t *this) +{ + // Cleanup event loop + if (this->event_loop.concurrent) + { + bmswpw_poll_stop(this); + pw_thread_loop_destroy(this->event_loop.concurrent); + } + + // Cleanup managed memory + free(this->event_data.format); + free(this->out.u8); + free(this->in.u8); + this->event_data.format = NULL; + this->out.u8 = NULL; + this->in.u8 = NULL; + pthread_mutex_destroy(&this->lock); +} + +/* Public API */ +void *bmswpw_create(const char *title, void *event_cb, void *event_cb_arg) +{ + pw_init(NULL, NULL); //_REV: move to on library load if thread safe + + static bmsw_pwout_t client; + bmswpw_init_buffer(&client, BMSPWM_SPA_FORMAT); + +#ifndef BMSW_NOEXPERIMENTAL + if (bmswexp_profile) + { + DBGEXEC(printf("Experimental API enabled. Initializing profile '%d' (%s)\n", bmswexp_profile, bmswexp_name_by_profile(bmswexp_profile))); + } + else + { + DBGEXEC(printf("Stable API unimplemented\n")); + return NULL; + //_TODO: Default API (so API for T_NONE, most likely Stable API in experimental builds) + } + if (EXPERIMENTAL(bmswexp_profile, bmswpw_callback) && !event_cb) bmswpw_update_callback(&client, EXPERIMENTAL(bmswexp_profile, bmswpw_callback), &client); + bmswpw_init_events(&client, (const pw_stream_events_t) {.process = EXPERIMENTAL(bmswexp_profile, bmswpw_process)}, event_cb, event_cb_arg); +#else + //_TODO: Stable API +#endif + + char latency[32]; + snprintf(latency, 32, "%d/%d", client.event_data.format->frames, client.event_data.format->rate); + bmswpw_init_stream(&client, pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Game", + PW_KEY_MEDIA_NAME, "待機", + PW_KEY_APP_NAME, title, + PW_KEY_NODE_LATENCY, latency, + NULL + )); + + return &client; +} +int bmswpw_start(void *client) +{ + bmswpw_poll_start(client); + return 0; +} +int bmswpw_stop(void *client) +{ + bmswpw_poll_stop(client); + return 0; +} +int bmswpw_destroy(void *client) +{ + bmswpw_destroy_buffer(client); + return 0; +} +unsigned char *bmswpw_get_buffer(void *client, uint32_t n) +{ +#ifndef BMSW_NOEXPERIMENTAL + return EXPERIMENTAL(bmswexp_profile, bmswpw_get_sbuf)(client, n); +#endif + //_TODO: Stable API +} +int bmswpw_release_buffer(void *client, uint32_t n) +{ +#ifndef BMSW_NOEXPERIMENTAL + return EXPERIMENTAL(bmswexp_profile, bmswpw_send_sbuf)(client, n); +#endif + //_TODO: Stable API +} +void bmswpw_update_callback(void *client, void *event_cb, void *event_cb_arg) +{ + bmsw_pwout_t *client_ = (bmsw_pwout_t *) client; + client_->event_cb = event_cb; + client_->event_cb_arg = event_cb_arg; +} +int bmswpw_format_is_supported(int rate, int channel, int depth, void *client) +{ + + int rate_ = bmsw_config->audio_rate; + int channel_ = bmsw_config->audio_channels; + int depth_ = bmsw_config->audio_depth; + if (client) + { + bmsw_pwout_t *client_ = client; + rate_ = client_->event_data.format->rate; + channel_ = client_->event_data.format->channels; + //depth_ = client_->event_data.format->format; //_BUG: spa_format to bit value lookup table + } + + if (channel == channel_ && rate == rate_ && depth == depth_) return 0; + return -1; +} +int bmswpw_format_period_fpc(void *client) +{ + if (client) + return ((bmsw_pwout_t *) client)->event_data.format->frames; + return bmsw_config->audio_fpc; +} +long long bmswpw_format_period_wrt(void *client) +{ + int frames = bmsw_config->audio_fpc; + int rate = bmsw_config->audio_rate; + if (client) + { + bmsw_pwout_t *client_ = client; + frames = client_->event_data.format->frames; + rate = client_->event_data.format->rate; + } + + return (long long) ceil(1e7 * frames / rate); +} + +/* Experimental */ +#include "bmswexp_pw.frag.c" diff --git a/src/bmsound-pw/bmsound_pw.h b/src/bmsound-pw/bmsound_pw.h new file mode 100644 index 0000000..3511058 --- /dev/null +++ b/src/bmsound-pw/bmsound_pw.h @@ -0,0 +1,22 @@ +#ifndef _BMSOUND_PW_H +#define _BMSOUND_PW_H +#include + + +/* Utils (client=NULL for library-scoped queries) */ +int bmswpw_format_is_supported(int rate, int channel, int depth, void *client); // format availability for current endpoint +int bmswpw_format_period_fpc(void *client); // frames per chunk +long long bmswpw_format_period_wrt(void *client); // wrt per chunk _INFO: wasapi decided to invent a new time unit, so now we have to translate one more thing (WASAPI Reference Time) + +/* Core */ +void *bmswpw_create(const char *title, void *event_cb, void *event_cb_arg); +int bmswpw_start(void *client); +int bmswpw_stop(void *client); +int bmswpw_destroy(void *client); +unsigned char *bmswpw_get_buffer(void *client, unsigned int n); +int bmswpw_release_buffer(void *client, unsigned int n); + +/* Advanced */ +void bmswpw_update_callback(void *client, void *event_cb, void *event_cb_arg); + +#endif \ No newline at end of file diff --git a/src/bmsound-pw/on_compiled.exiv b/src/bmsound-pw/on_compiled.exiv new file mode 100755 index 0000000..06737a1 --- /dev/null +++ b/src/bmsound-pw/on_compiled.exiv @@ -0,0 +1,5 @@ +# Includes +copy "$(ProjectDir)bmsound_pw.h" "$(TargetDir)include/" +copy "$(ProjectDir)Core/bmsound_config.h" "$(TargetDir)include/" +copy "$(ProjectDir)Experimental/bmsound_experimental.h" "$(TargetDir)include/" +copy "$(ProjectDir)Experimental/bmsound_test.h" "$(TargetDir)include/" diff --git a/src/bmsound-wine/Makefile.mk b/src/bmsound-wine/Makefile.mk new file mode 100644 index 0000000..0261ab9 --- /dev/null +++ b/src/bmsound-wine/Makefile.mk @@ -0,0 +1,25 @@ +## bmsound-wine.{dll:so} ## +$(call generic_target,bmsound-wine,c) + +# Local #define +$(call incl_define,$(target),) + +# Local includes (#include "") +$(call incl_target_dirs,$(target)) +$(call incl_quoted,$(target),) + +# External includes (#include <>) +$(call incl_angled,$(target),$(OUTPUT_DIR)/include) + +# Local dynamic linker (compile-able libraries) +$(call link_reference,$(target),bmsound-pw) + +# External dynamic linker (binary libraries) - external/system libraries +$(call link_package,$(target),) +$(call link_external,$(target),) + +$($(target)_build)/%.o: $(target)@pre $($(target)_src) + $(call wine_object,bmsound-wine,$*) + +$(target): $($(target)_obj) + $(call wine_library,bmsound-wine) diff --git a/src/bmsound-wine/bmsound-wine.dll.spec b/src/bmsound-wine/bmsound-wine.dll.spec new file mode 100644 index 0000000..ad51276 --- /dev/null +++ b/src/bmsound-wine/bmsound-wine.dll.spec @@ -0,0 +1,12 @@ +@ stdcall BmswClientFormatIsSupported(long long long ptr) BmswClientFormatIsSupported +@ stdcall BmswClientFormatPeriodFPC(ptr) BmswClientFormatPeriodFPC +@ stdcall BmswClientFormatPeriodWRT(ptr) BmswClientFormatPeriodWRT +@ stdcall BmswClientCreate(ptr ptr ptr) BmswClientCreate +@ stdcall BmswClientStart(ptr) BmswClientStart +@ stdcall BmswClientStop(ptr) BmswClientStop +@ stdcall BmswClientDestroy(ptr) BmswClientDestroy +@ stdcall BmswClientGetBuffer(ptr long) BmswClientGetBuffer +@ stdcall BmswClientReleaseBuffer(ptr long) BmswClientReleaseBuffer +@ stdcall BmswClientUpdateCallback(ptr ptr ptr) BmswClientUpdateCallback +@ stdcall BmswExperimentalForceProfile(ptr) BmswExperimentalForceProfile +@ stdcall BmswConfigInit(ptr) BmswConfigInit diff --git a/src/bmsound-wine/bmsw.c b/src/bmsound-wine/bmsw.c new file mode 100644 index 0000000..1aa4e87 --- /dev/null +++ b/src/bmsound-wine/bmsw.c @@ -0,0 +1,61 @@ +// Unix includes +#include "bmsound_pw.h" +#include "bmsound_config.h" +#include +//#include +#include +// Wine includes +#include +#include + + +typedef void (*BmswCallback_t)(void *); + +void WINAPI BmswConfigInit(const char *path) +{ + bmsw_config_init(path); +} +void WINAPI BmswExperimentalForceProfile(const char *name) +{ + bmswexp_profile = bmswexp_profile_by_name(name); +} +void WINAPI BmswClientUpdateCallback(void *client, BmswCallback_t cb, void *arg) +{ + bmswpw_update_callback(client, cb, arg); +} +void* WINAPI BmswClientCreate(const char *title, BmswCallback_t cb, void *arg) +{ + return bmswpw_create(title, cb, arg); +} +int WINAPI BmswClientStart(void *client) +{ + return bmswpw_start(client); +} +int WINAPI BmswClientStop(void *client) +{ + return bmswpw_stop(client); +} +int WINAPI BmswClientDestroy(void *client) +{ + return bmswpw_destroy(client); +} +unsigned char* WINAPI BmswClientGetBuffer(void *client, unsigned int n) +{ + return bmswpw_get_buffer(client, n); +} +int WINAPI BmswClientReleaseBuffer(void *client, unsigned int n) +{ + return bmswpw_release_buffer(client, n); +} +int WINAPI BmswClientFormatIsSupported(int rate, int channel, int depth, void *client) +{ + return bmswpw_format_is_supported(rate, channel, depth, client); +} +int WINAPI BmswClientFormatPeriodFPC(void *client) +{ + return bmswpw_format_period_fpc(client); +} +LONGLONG WINAPI BmswClientFormatPeriodWRT(void *client) +{ + return bmswpw_format_period_wrt(client); +} diff --git a/src/test-client/Makefile.mk b/src/test-client/Makefile.mk new file mode 100644 index 0000000..dd10dfd --- /dev/null +++ b/src/test-client/Makefile.mk @@ -0,0 +1,25 @@ +## test-client.bin ## +$(call generic_target,test-client,cpp) + +# Local #define +$(call incl_define,$(target),) + +# Local includes (#include "") +$(call incl_target_dirs,$(target)) +$(call incl_quoted,$(target),) + +# External includes (#include <>) +$(call incl_angled,$(target),$(OUTPUT_DIR)/include) + +# Local dynamic linker (compile-able libraries) +$(call link_reference,$(target),bmsound-pw) + +# External dynamic linker (binary libraries) - external/system libraries +$(call link_package,$(target),sndfile) +$(call link_external,$(target),) + +$($(target)_build)/%.o: $(target)@pre $($(target)_src) + $(call gcc_object,test-client,$*) + +$(target): $($(target)_obj) + $(call gcc_executable,test-client) diff --git a/src/test-client/client-config.json b/src/test-client/client-config.json new file mode 100644 index 0000000..bc785bf --- /dev/null +++ b/src/test-client/client-config.json @@ -0,0 +1,17 @@ +{ + "video": { + "display": 0, + "refresh_rate": 0, + "fsr": -1 + }, + "audio": { + "profile": "notif_callback", + "fpc": 64, + "channels": 2, + "depth": 16 + }, + "network": { + "url": null, + "pcbid": null + } +} \ No newline at end of file diff --git a/src/test-client/pw-client.c b/src/test-client/pw-client.c new file mode 100644 index 0000000..eed830e --- /dev/null +++ b/src/test-client/pw-client.c @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include +#ifdef __cplusplus +extern "C" { +#endif +#include "bmsound_pw.h" +#include "bmsound_config.h" +#include "bmsound_test.h" +#ifdef __cplusplus +} +#endif + + +static const profile_exp_t profile = T_NOTIF_CALLBACK; +static void *(*test[T_LAST])(void *); +volatile sig_atomic_t sig_ = 0; + +/* Utils */ +void sig_handler(int sig) +{ + sig_ = sig; +} +int16_t *get_sndbuf(const char *fname) +{ + SF_INFO info; + SNDFILE *file = sf_open(fname, SFM_READ, &info); + if (file == NULL) + { + printf("File error: '%s'\n", fname ? fname : ""); + return NULL; + } + sf_command(file, SFC_GET_CURRENT_SF_INFO, &info, sizeof(info)); + + // Load into the buffer + int16_t *buffer = (signed short *) malloc(info.frames * info.channels * sizeof(int16_t)); + int numFramesRead = sf_readf_short(file, buffer, info.frames); + + sf_close(file); + return buffer; +} + +/* Tests */ +// Just for testing pipewire server connection +void *test_static_sine(void *sndbuf_) +{ + bmswtest_client_concurrent("pw-client"); + bmswtest_client_sequential("pw-client"); + return NULL; +} +// Just for testing audio buffers for format misconfigurations +void *test_audio_format(void *sndbuf_) +{ + char *sndbuf = (char *) sndbuf_; + void *client = bmswpw_create("pw-client", NULL, NULL); + if(!client) return NULL; + EXPERIMENTAL(T_NONE, bmswpw_update_process)(client, EXPERIMENTAL(T_AUDIO_FORMAT, bmswpw_process)); + free(EXPERIMENTAL(T_NONE, bmswpw_replace_buffer)(client, sndbuf, 0)); + + bmswpw_start(client); + + while (!sig_) sleep(1); + EXPERIMENTAL(T_NONE, bmswpw_replace_buffer)(client, malloc(50000), 0);// just a workaround for double free + + return client; +} +// Unsafe buffer mirroring with latency defined by cursors distance delta (proof-of-concept) +void *test_twin_cursor(void *sndbuf_) +{ + char *sndbuf = (char *) sndbuf_; + void *client = bmswpw_create("pw-client", NULL, NULL); + if(!client) return NULL; + EXPERIMENTAL(T_NONE, bmswpw_update_process)(client, EXPERIMENTAL(T_TWIN_CURSOR, bmswpw_process)); + + bmswpw_start(client); + //_INFO should be 441 for 10msec, since usleep is not exactly known for its interruption precision, we use slightly more than backend to prevent buffer underruns during test as single underrun and audio desync increases to whole buffer size (loops back to be precise) + while (!sig_) + { + memcpy(EXPERIMENTAL(T_TWIN_CURSOR, bmswpw_get_sbuf)(client, 446), sndbuf, 446 * 4); + EXPERIMENTAL(T_TWIN_CURSOR, bmswpw_send_sbuf)(client, 446); + sndbuf += 446 * 4; + usleep(10000); + } + //_REV: could probably measure latency here after sndbuf flush (by comparing in/out cursors) + + return client; +} +// Event driven audio buffer fetching from ambiguous audio interface +struct tntcb_data +{ + void *client; + char *sndbuf; +}; +typedef struct tntcb_data tntcb_data_t; +void tntcb_cb(tntcb_data_t *ntcb_data) +{ + //_INFO: notify event handler from spicetools would be here (if callbacks to executable worked..), first process will be silent if get/release part wasn't called beforehand + memcpy(EXPERIMENTAL(T_NOTIF_CALLBACK, bmswpw_get_sbuf)(ntcb_data->client, bmsw_config->audio_fpc), ntcb_data->sndbuf, bmsw_config->audio_fpc * 4);// amount here defines audio latency, seems to scale fine so far + EXPERIMENTAL(T_NOTIF_CALLBACK, bmswpw_send_sbuf)(ntcb_data->client, bmsw_config->audio_fpc); + ntcb_data->sndbuf += bmsw_config->audio_fpc * 4; +} +void *test_notify_cb(void *sndbuf) +{ + static tntcb_data_t forward; // must be in scope for client duration, managed by caller + void *client = bmswpw_create("pw-client", (void *) tntcb_cb, &forward); + if(!client) return NULL; + EXPERIMENTAL(T_NONE, bmswpw_update_process)(client, EXPERIMENTAL(T_NOTIF_CALLBACK, bmswpw_process)); + forward.client = client; + forward.sndbuf = (char *) sndbuf; + + bmswpw_start(client); + return client; +} + +int main(void) +{ + test[T_AUDIO_FORMAT] = test_audio_format; + test[T_TWIN_CURSOR] = test_twin_cursor; + test[T_NOTIF_CALLBACK] = test_notify_cb; + test[T_STATIC_SINE] = test_static_sine; + signal(SIGTERM, sig_handler); + signal(SIGINT, sig_handler); + bmsw_config_init("src/test-client/client-config.json"); + char *sndbuf = (char *) get_sndbuf("local/test/sousoushi.wav"); + + /* simulated loop on iidx side */ + void *client = NULL; + if (test[profile]) client = test[profile](sndbuf); + + while (!sig_) sleep(1); + if (client) bmswpw_destroy(client); + free(sndbuf); + return 0; +} diff --git a/src/test-client/pw-client.cpp b/src/test-client/pw-client.cpp new file mode 100644 index 0000000..fee71fa --- /dev/null +++ b/src/test-client/pw-client.cpp @@ -0,0 +1 @@ +#include "pw-client.c"