Initial re-upload

This commit is contained in:
Hazel 2024-08-28 23:32:05 -04:00
commit 32d81cfa15
39 changed files with 1579 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/bin/
/build/
/local/

16
.gitmodules vendored Executable file
View File

@ -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

10
.idea/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,86 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ClangTidy" enabled="true" level="WARNING" enabled_by_default="true">
<option name="clangTidyChecks" value="-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bad-signal-to-kill-thread,bugprone-branch-clone,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-dynamic-static-initializers,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-inaccurate-erase,bugprone-incorrect-roundings,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-parentheses,bugprone-macro-repeated-side-effects,bugprone-misplaced-operator-in-strlen-in-alloc,bugprone-misplaced-pointer-arithmetic-in-alloc,bugprone-misplaced-widening-cast,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-no-escape,bugprone-not-null-terminated-result,bugprone-parent-virtual-call,bugprone-posix-return,bugprone-reserved-identifier,bugprone-sizeof-container,bugprone-sizeof-expression,bugprone-spuriously-wake-up-functions,bugprone-string-constructor,bugprone-string-integer-assignment,bugprone-string-literal-with-embedded-nul,bugprone-suspicious-enum-usage,bugprone-suspicious-include,bugprone-suspicious-memory-comparison,bugprone-suspicious-memset-usage,bugprone-suspicious-missing-comma,bugprone-suspicious-semicolon,bugprone-suspicious-string-compare,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-throw-keyword-missing,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-self-assignment,bugprone-unused-raii,bugprone-unused-return-value,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl21-cpp,cert-dcl58-cpp,cert-err34-c,cert-err52-cpp,cert-err60-cpp,cert-flp30-c,cert-msc50-cpp,cert-msc51-cpp,cert-str34-c,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-pro-type-static-cast-downcast,cppcoreguidelines-slicing,google-default-arguments,google-explicit-constructor,google-runtime-operator,hicpp-exception-baseclass,hicpp-multiway-paths-covered,misc-misplaced-const,misc-new-delete-overloads,misc-no-recursion,misc-non-copyable-objects,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,misc-uniqueptr-reset-release,modernize-avoid-bind,modernize-concat-nested-namespaces,modernize-deprecated-headers,modernize-deprecated-ios-base-aliases,modernize-loop-convert,modernize-make-shared,modernize-make-unique,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-auto-ptr,modernize-replace-disallow-copy-and-assign-macro,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-emplace,modernize-use-equals-default,modernize-use-equals-delete,modernize-use-nodiscard,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,mpi-buffer-deref,mpi-type-mismatch,openmp-use-default-none,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-algorithm,performance-inefficient-string-concatenation,performance-inefficient-vector-operation,performance-move-const-arg,performance-move-constructor-init,performance-no-automatic-move,performance-noexcept-move-constructor,performance-trivially-destructible,performance-type-promotion-in-math-fn,performance-unnecessary-copy-initialization,performance-unnecessary-value-param,portability-simd-intrinsics,readability-avoid-const-params-in-decls,readability-const-return-type,readability-container-size-empty,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-deleted-default,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-smartptr-get,readability-redundant-string-cstr,readability-redundant-string-init,readability-simplify-subscript-expr,readability-static-accessed-through-instance,readability-static-definition-in-anonymous-namespace,readability-string-compare,readability-uniqueptr-delete-release,readability-use-anyofallof" />
</inspection_tool>
<inspection_tool class="ConstantConditionsOC" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ConstantFunctionResult" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedConcept" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedGlobalDeclaration" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedIncludeDirective" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedMacro" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedStruct" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedTemplateParameter" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedTypeAlias" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyBroadExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ourVersions">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="2.7" />
<item index="1" class="java.lang.String" itemvalue="3.7" />
<item index="2" class="java.lang.String" itemvalue="3.8" />
<item index="3" class="java.lang.String" itemvalue="3.9" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyDefaultArgumentInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E266" />
<option value="E701" />
<option value="E265" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N806" />
<option value="N803" />
<option value="N802" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyTypeCheckerInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnusedLocalInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ReassignedToPlainText" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ShellCheck" enabled="true" level="ERROR" enabled_by_default="true">
<shellcheck_settings value="SC2164" />
</inspection_tool>
<inspection_tool class="Simplify" enabled="false" level="WARNING" enabled_by_default="false">
<option name="clangTidyCheckOptions">
<list>
<ClangTidyCheckOption>
<option name="optionName" value="clion-simplify.SimplifyConstantConditions" />
<option name="optionValue" value="1" />
</ClangTidyCheckOption>
<ClangTidyCheckOption>
<option name="optionName" value="clion-simplify.SimplifyIfWithReturn" />
<option name="optionValue" value="0" />
</ClangTidyCheckOption>
<ClangTidyCheckOption>
<option name="optionName" value="clion-simplify.SimplifyEqualToTrueFalse" />
<option name="optionValue" value="1" />
</ClangTidyCheckOption>
<ClangTidyCheckOption>
<option name="optionName" value="clion-simplify.SimplifyTernaryWithConstantBranch" />
<option name="optionValue" value="1" />
</ClangTidyCheckOption>
</list>
</option>
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="true" level="TYPO" enabled_by_default="true">
<option name="processCode" value="false" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="UnusedLocalVariable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedValue" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

32
.idea/misc.xml Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CidrRootsConfiguration">
<sourceRoots>
<file path="$PROJECT_DIR$/src" />
</sourceRoots>
<excludeRoots>
<file path="$PROJECT_DIR$/bin" />
<file path="$PROJECT_DIR$/build" />
<file path="$PROJECT_DIR$/lib" />
<file path="$PROJECT_DIR$/local" />
</excludeRoots>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MakefileSettings">
<option name="linkedExternalProjectsSettings">
<MakefileProjectSettings>
<option name="buildTarget" value="build" />
<option name="cleanTarget" value="clean_all" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
<option name="switches" value="-Rs TARGET_ARCH=x64 TARGET_TYPE=Debug" />
<option name="version" value="2" />
</MakefileProjectSettings>
</option>
</component>
<component name="MakefileWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
</project>

View File

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clean" type="MAKEFILE_TARGET_RUN_CONFIGURATION" factoryName="Makefile">
<makefile filename="$PROJECT_DIR$/Makefile" target="clean_all" workingDirectory="" arguments="-Rs">
<envs />
</makefile>
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Debug_x64" type="MAKEFILE_TARGET_RUN_CONFIGURATION" factoryName="Makefile">
<makefile filename="$PROJECT_DIR$/Makefile" target="build" workingDirectory="" arguments="-Rs TARGET_ARCH=x64 TARGET_TYPE=Debug">
<envs />
</makefile>
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Debug_x86" type="MAKEFILE_TARGET_RUN_CONFIGURATION" factoryName="Makefile">
<makefile filename="$PROJECT_DIR$/Makefile" target="build" workingDirectory="" arguments="-Rs TARGET_ARCH=x86 TARGET_TYPE=Debug">
<envs />
</makefile>
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,10 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Release_x64" type="MAKEFILE_TARGET_RUN_CONFIGURATION" factoryName="Makefile">
<makefile filename="$PROJECT_DIR$/Makefile" target="build" workingDirectory="" arguments="-Rs TARGET_ARCH=x64 TARGET_TYPE=Release">
<envs>
<env name="LC_ALL" value="C" />
</envs>
</makefile>
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Release_x86" type="MAKEFILE_TARGET_RUN_CONFIGURATION" factoryName="Makefile">
<makefile filename="$PROJECT_DIR$/Makefile" target="build" workingDirectory="" arguments="-Rs TARGET_ARCH=x86 TARGET_TYPE=Release">
<envs />
</makefile>
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="passthrough" type="CLionNativeAppRunConfigurationType" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="bmsound_wine" TARGET_NAME="passthrough" CONFIG_NAME="passthrough" version="1" RUN_PATH="$PROJECT_DIR$/bin/Debug/x64/test-client">
<envs>
<env name="LD_LIBRARY_PATH" value="bin/Debug/x64" />
</envs>
<method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Debug_x64" run_configuration_type="MAKEFILE_TARGET_RUN_CONFIGURATION" />
<option name="CLION.COMPOUND.BUILD" enabled="true" />
</method>
</configuration>
</component>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

67
Makefile Normal file
View File

@ -0,0 +1,67 @@
#!/usr/bin/make -s -f
# Usage: make -Rs <action> TARGET_ARCH=<x64,x86> TARGET_TYPE=<Release,Debug>
# 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

30
README.md Normal file
View File

@ -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 | &#10008; |
| 44.1kHz@24bit 7.1ch | &#10008; |
| 44.1kHz@16bit 7.1ch | &#10008; |
| 44.1kHz@32bit 5.1ch | &#10008; |
| 44.1kHz@16bit 7.1ch | &#10008; |
| 44.1kHz@32bit 3.1ch | &#10008; |
| 44.1kHz@16bit 3.1ch | &#10008; |
| 44.1kHz@16bit 2ch | &#10004; |
# 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

View File

@ -0,0 +1,98 @@
#include "bmswpw_callbacks.h"
#include "bmswpw_defines.h"
#include "bmswpw_util.h"
#include "bmsound_experimental.h"
#include <spa/param/audio/format-utils.h>
#include <pipewire/pipewire.h>
#include <math.h>
/* 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"

View File

@ -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

View File

@ -0,0 +1,72 @@
#ifndef _BMSWPW_DEFINES_H
#define _BMSWPW_DEFINES_H
#include <spa/param/audio/format-utils.h>
#include <pipewire/stream.h>
#include <pthread.h>
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

View File

@ -0,0 +1,9 @@
#include <stdio.h>
#include "bmswpw_util.h"
#include "bmsound_experimental.h"
void cb_empty(void *arg)
{
//DBGEXEC(printf("cb_empty\n"));
}

View File

@ -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

View File

@ -0,0 +1,66 @@
#include "bmsound_config.h"
#include "bmsound_experimental.h"
#include <yyjson.h>
#include <stddef.h>
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));
}
}

View File

@ -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

View File

@ -0,0 +1,32 @@
#include <string.h>
#include <stdio.h>
#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;
}

View File

@ -0,0 +1,60 @@
#ifndef _BMSOUND_EXPERIMENTAL_H
#define _BMSOUND_EXPERIMENTAL_H
#include <stdint.h>
#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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

22
src/bmsound-pw/bmsound.c Normal file
View File

@ -0,0 +1,22 @@
#include "bmsound_config.h"
#include <stdio.h>
// 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);
}

View File

@ -0,0 +1,3 @@
#ifndef _BMSOUND_FX_H
#define _BMSOUND_FX_H
#endif

217
src/bmsound-pw/bmsound_pw.c Normal file
View File

@ -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 <spa/param/audio/format-utils.h>
#include <pipewire/pipewire.h>
#include <unistd.h>
#include <math.h>
#include <stdint.h>
/* 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"

View File

@ -0,0 +1,22 @@
#ifndef _BMSOUND_PW_H
#define _BMSOUND_PW_H
#include <stdint.h>
/* 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

View File

@ -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/"

View File

@ -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)

View File

@ -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

61
src/bmsound-wine/bmsw.c Normal file
View File

@ -0,0 +1,61 @@
// Unix includes
#include "bmsound_pw.h"
#include "bmsound_config.h"
#include <unistd.h>
//#include <pthread.h>
#include <stdio.h>
// Wine includes
#include <windef.h>
#include <windows.h>
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);
}

View File

@ -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)

View File

@ -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
}
}

137
src/test-client/pw-client.c Normal file
View File

@ -0,0 +1,137 @@
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sndfile.h>
#include <signal.h>
#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;
}

View File

@ -0,0 +1 @@
#include "pw-client.c"