Update vgmstream
This commit is contained in:
parent
204adf1f3e
commit
73686be925
76 changed files with 14894 additions and 380 deletions
2
.clangd
Normal file
2
.clangd
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
CompileFlags:
|
||||||
|
CompilationDatabase: "./build/compile_commands.json"
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
assets/*.h
|
assets/*.h
|
||||||
build*
|
build*
|
||||||
|
!build-env
|
||||||
.vscode
|
.vscode
|
||||||
.cache
|
.cache
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
|
9
.gitmodules
vendored
9
.gitmodules
vendored
|
@ -28,3 +28,12 @@
|
||||||
[submodule "subprojects/oboe"]
|
[submodule "subprojects/oboe"]
|
||||||
path = subprojects/oboe
|
path = subprojects/oboe
|
||||||
url = https://github.com/google/oboe.git
|
url = https://github.com/google/oboe.git
|
||||||
|
[submodule "subprojects/protobuf"]
|
||||||
|
path = subprojects/protobuf
|
||||||
|
url = /subprojects/protobuf
|
||||||
|
[submodule "subprojects/fmt"]
|
||||||
|
path = subprojects/fmt
|
||||||
|
url = https://github.com/fmtlib/fmt.git
|
||||||
|
[submodule "subprojects/grpc"]
|
||||||
|
path = subprojects/grpc
|
||||||
|
url = https://github.com/grpc/grpc
|
||||||
|
|
28
.zed/tasks.json
Normal file
28
.zed/tasks.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "Build (System)",
|
||||||
|
"command": "./build.sh",
|
||||||
|
"args": ["-j$(nproc)"],
|
||||||
|
"shell": "system",
|
||||||
|
"reveal": "always",
|
||||||
|
"hide": "never"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build (Docker)",
|
||||||
|
"command": "./build-docker.sh",
|
||||||
|
"args": ["-j$(nproc)"],
|
||||||
|
"shell": "system",
|
||||||
|
"reveal": "always",
|
||||||
|
"hide": "never"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Debug (Docker-built, gdb)",
|
||||||
|
"command": "./start-memory-limited.sh",
|
||||||
|
"args": ["2048M", "gdb", "--args", "./build-docker/looper", "-m"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Debug (System-built, gdb)",
|
||||||
|
"command": "./start-memory-limited.sh",
|
||||||
|
"args": ["2048M", "gdb", "--args", "./build/looper", "-m"]
|
||||||
|
}
|
||||||
|
]
|
|
@ -5,6 +5,9 @@ if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten")
|
||||||
set(EMSCRIPTEN ON)
|
set(EMSCRIPTEN ON)
|
||||||
message("Building for WASM.")
|
message("Building for WASM.")
|
||||||
endif()
|
endif()
|
||||||
|
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
||||||
|
include(SanitizerBuildTypes)
|
||||||
|
sanitizer_build_types(ASAN AddressSanitizer LSAN LeakSanitizer MSAN MemorySanitizer TSAN ThreadSanitizer UBSAN UndefinedBehaviorSanitizer)
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(SDL_MIXER_X_STATIC ON CACHE BOOL "")
|
set(SDL_MIXER_X_STATIC ON CACHE BOOL "")
|
||||||
|
@ -132,7 +135,6 @@ find_package(PkgConfig)
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
add_subdirectory(subprojects/SDL-Mixer-X)
|
add_subdirectory(subprojects/SDL-Mixer-X)
|
||||||
add_subdirectory(subprojects/vgmstream)
|
add_subdirectory(subprojects/vgmstream)
|
||||||
|
|
||||||
if (DEFINED EMSCRIPTEN)
|
if (DEFINED EMSCRIPTEN)
|
||||||
set(EXTRA_LIBS )
|
set(EXTRA_LIBS )
|
||||||
else()
|
else()
|
||||||
|
@ -161,7 +163,7 @@ include(log)
|
||||||
#execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/meson2cmake_cfg.py ${CMAKE_CURRENT_SOURCE_DIR}/config.meson.h.in ${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.h.in)
|
#execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/meson2cmake_cfg.py ${CMAKE_CURRENT_SOURCE_DIR}/config.meson.h.in ${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.h.in)
|
||||||
set(DEBUG_MODE_VALUE ${DEBUG})
|
set(DEBUG_MODE_VALUE ${DEBUG})
|
||||||
configure_file(config.cmake.h.in config.h)
|
configure_file(config.cmake.h.in config.h)
|
||||||
|
add_subdirectory(subprojects/fmt)
|
||||||
macro(target_pkgconfig)
|
macro(target_pkgconfig)
|
||||||
push_fnstack("target_pkgconfig")
|
push_fnstack("target_pkgconfig")
|
||||||
cmake_parse_arguments(PARSED_ARGS "OPTIONAL;PRIVATE;PUBLIC;INTERFACE" "PREFIX" "TARGETS;LIBRARIES" ${ARGN})
|
cmake_parse_arguments(PARSED_ARGS "OPTIONAL;PRIVATE;PUBLIC;INTERFACE" "PREFIX" "TARGETS;LIBRARIES" ${ARGN})
|
||||||
|
@ -203,6 +205,48 @@ macro(prefix_all)
|
||||||
list(APPEND ${OUT_VAR} ${PREFIX}${ARG})
|
list(APPEND ${OUT_VAR} ${PREFIX}${ARG})
|
||||||
endforeach()
|
endforeach()
|
||||||
endmacro()
|
endmacro()
|
||||||
|
find_package(Protobuf REQUIRED)
|
||||||
|
find_package(gRPC CONFIG REQUIRED)
|
||||||
|
|
||||||
|
set(_PROTOBUF_LIBPROTOBUF ${Protobuf_LIBRARY_RELEASE})
|
||||||
|
set(_REFLECTION gRPC::grpc++_reflection)
|
||||||
|
set(_GRPC_GRPCPP gRPC::grpc++)
|
||||||
|
find_program(_GRPC_CPP_PLUGIN_EXE grpc_cpp_plugin)
|
||||||
|
find_program(_PROTOBUF_PROTOC protoc)
|
||||||
|
function(grpc_proto)
|
||||||
|
cmake_parse_arguments(GRPC_PROTO "" "TARGET"
|
||||||
|
"SOURCES" ${ARGV})
|
||||||
|
foreach (GRPC_PROTO_SOURCE ${GRPC_PROTO_SOURCES})
|
||||||
|
set(GRPC_PROTO_SRCS)
|
||||||
|
set(GRPC_PROTO_HDRS)
|
||||||
|
set(GRPC_GRPC_SRCS)
|
||||||
|
set(GRPC_GRPC_HDRS)
|
||||||
|
get_filename_component(src_stem "${GRPC_PROTO_SOURCE}" NAME_WE)
|
||||||
|
get_filename_component(src_path "${GRPC_PROTO_SOURCE}" DIRECTORY)
|
||||||
|
cmake_path(RELATIVE_PATH src_path BASE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE src_rel_path)
|
||||||
|
set(src_out ${CMAKE_CURRENT_BINARY_DIR}/${src_rel_path})
|
||||||
|
message("Output directory: ${src_out}")
|
||||||
|
list(APPEND GRPC_PROTO_SRCS ${src_out}/${src_stem}.pb.cc)
|
||||||
|
list(APPEND GRPC_PROTO_HDRS ${src_out}/${src_stem}.pb.h)
|
||||||
|
list(APPEND GRPC_GRPC_SRCS ${src_out}/${src_stem}.grpc.pb.cc)
|
||||||
|
list(APPEND GRPC_GRPC_HDRS ${src_out}/${src_stem}.grpc.pb.h)
|
||||||
|
make_directory(${src_out})
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${GRPC_PROTO_SRCS} ${GRPC_PROTO_HDRS} ${GRPC_GRPC_SRCS} ${GRPC_GRPC_HDRS}
|
||||||
|
COMMAND ${_PROTOBUF_PROTOC}
|
||||||
|
ARGS --grpc_out "${src_out}"
|
||||||
|
--cpp_out "${src_out}"
|
||||||
|
-I "${src_path}"
|
||||||
|
--plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXE}"
|
||||||
|
"${GRPC_PROTO_SOURCE}"
|
||||||
|
DEPENDS "${GRPC_PROTO_SOURCE}"
|
||||||
|
)
|
||||||
|
target_sources(${GRPC_PROTO_TARGET} PRIVATE ${GRPC_PROTO_SRCS} ${GRPC_GRPC_SRCS})
|
||||||
|
target_include_directories(${GRPC_PROTO_TARGET} PRIVATE ${src_out})
|
||||||
|
endforeach()
|
||||||
|
target_include_directories(${GRPC_PROTO_TARGET} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_link_libraries(${GRPC_PROTO_TARGET} PUBLIC ${_REFLECTION} ${_GRPC_GRPCPP} ${_PROTOBUF_LIBPROTOBUF})
|
||||||
|
endfunction()
|
||||||
prefix_all(LIBRARY_SOURCES
|
prefix_all(LIBRARY_SOURCES
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/
|
${CMAKE_CURRENT_SOURCE_DIR}/
|
||||||
backend.cpp
|
backend.cpp
|
||||||
|
@ -211,10 +255,16 @@ prefix_all(LIBRARY_SOURCES
|
||||||
util.cpp
|
util.cpp
|
||||||
log.cpp
|
log.cpp
|
||||||
dbus.cpp
|
dbus.cpp
|
||||||
|
file_backend.cpp
|
||||||
|
playback_backend.cpp
|
||||||
translation.cpp
|
translation.cpp
|
||||||
|
playback_process.cpp
|
||||||
)
|
)
|
||||||
add_library(liblooper STATIC ${LIBRARY_SOURCES})
|
add_library(liblooper STATIC ${LIBRARY_SOURCES})
|
||||||
|
grpc_proto(TARGET liblooper SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ipc/internal.proto ${CMAKE_CURRENT_SOURCE_DIR}/ipc/common.proto)
|
||||||
target_include_directories(liblooper PUBLIC ${INC})
|
target_include_directories(liblooper PUBLIC ${INC})
|
||||||
|
target_link_libraries(liblooper PUBLIC grpc++)
|
||||||
|
target_include_directories(liblooper PUBLIC)
|
||||||
set(JSONCPP_TARGET PkgConfig::jsoncpp)
|
set(JSONCPP_TARGET PkgConfig::jsoncpp)
|
||||||
set(SOUNDTOUCH_TARGET PkgConfig::SoundTouch)
|
set(SOUNDTOUCH_TARGET PkgConfig::SoundTouch)
|
||||||
function(set_to_value_of_condition var)
|
function(set_to_value_of_condition var)
|
||||||
|
@ -249,11 +299,12 @@ else()
|
||||||
set(LIBINTL_LIBRARY Intl::Intl CACHE INTERNAL "")
|
set(LIBINTL_LIBRARY Intl::Intl CACHE INTERNAL "")
|
||||||
set(LIBINTL_INCDIRS ${Intl_INCLUDE_DIRS} CACHE INTERNAL "")
|
set(LIBINTL_INCDIRS ${Intl_INCLUDE_DIRS} CACHE INTERNAL "")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (DEFINED ANDROID_NDK)
|
if (DEFINED ANDROID_NDK)
|
||||||
add_subdirectory(subprojects/oboe)
|
add_subdirectory(subprojects/oboe)
|
||||||
target_link_libraries(liblooper PUBLIC oboe)
|
target_link_libraries(liblooper PUBLIC oboe)
|
||||||
endif()
|
endif()
|
||||||
|
find_package(absl CONFIG REQUIRED)
|
||||||
|
target_link_libraries(liblooper PUBLIC ${Protobuf_LIBRARY_RELEASE} fmt::fmt)
|
||||||
target_include_directories(liblooper PUBLIC ${LIBINTL_INCDIRS})
|
target_include_directories(liblooper PUBLIC ${LIBINTL_INCDIRS})
|
||||||
target_link_libraries(liblooper PUBLIC ${LIBINTL_LIBRARY})
|
target_link_libraries(liblooper PUBLIC ${LIBINTL_LIBRARY})
|
||||||
if(DEFINED EMSCRIPTEN)
|
if(DEFINED EMSCRIPTEN)
|
||||||
|
@ -321,12 +372,42 @@ macro(ui_backend_subdir)
|
||||||
message("Disabled backend ${UI_OPTS_READABLE_NAME}")
|
message("Disabled backend ${UI_OPTS_READABLE_NAME}")
|
||||||
endif()
|
endif()
|
||||||
endmacro()
|
endmacro()
|
||||||
|
macro(add_playback_backend)
|
||||||
|
set(ARGS ${ARGV})
|
||||||
|
list(POP_FRONT ARGS target)
|
||||||
|
add_library(${target} STATIC ${ARGS})
|
||||||
|
message("Enabling playback backend '" ${target} "'...")
|
||||||
|
list(APPEND PLAYBACK_BACKENDS ${target})
|
||||||
|
set(PLAYBACK_BACKENDS ${PLAYBACK_BACKENDS} PARENT_SCOPE)
|
||||||
|
add_dependencies(${target} looper_assets)
|
||||||
|
target_include_directories(${target} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${INC})
|
||||||
|
target_link_libraries(${target} PRIVATE liblooper)
|
||||||
|
endmacro()
|
||||||
|
macro(playback_backend_subdir)
|
||||||
|
cmake_parse_arguments(PLAYBACK_OPTS "" "SUBDIR;NAME;READABLE_NAME" "" ${ARGN} )
|
||||||
|
message("Backend ${PLAYBACK_OPTS_READABLE_NAME} defined...")
|
||||||
|
set(PLAYBACK_DISABLE_OPT DISABLE_${PLAYBACK_OPTS_NAME}_PLAYBACK_BACKEND)
|
||||||
|
option(${PLAYBACK_DISABLE_OPT} "Disables the ${PLAYBACK_OPTS_READABLE_NAME} playback backend" OFF)
|
||||||
|
if (NOT ${${PLAYBACK_DISABLE_OPT}})
|
||||||
|
cmake_path(GET PLAYBACK_OPTS_SUBDIR STEM PLAYBACK_OPTS_DIRNAME)
|
||||||
|
set(BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${PLAYBACK_OPTS_SUBDIR})
|
||||||
|
add_subdirectory(${PLAYBACK_OPTS_SUBDIR})
|
||||||
|
list(APPEND ENABLED_PLAYBACK_BACKENDS "${PLAYBACK_OPTS_DIRNAME}")
|
||||||
|
message("Enabled backend ${PLAYBACK_OPTS_READABLE_NAME}.")
|
||||||
|
else()
|
||||||
|
message("Disabled backend ${PLAYBACK_OPTS_READABLE_NAME}")
|
||||||
|
endif()
|
||||||
|
endmacro()
|
||||||
set(ENABLED_UIS )
|
set(ENABLED_UIS )
|
||||||
|
set(ENABLED_PLAYBACK_BACKENDS )
|
||||||
ui_backend_subdir(NAME "IMGUI" READABLE_NAME "Dear ImGui" SUBDIR backends/ui/imgui)
|
ui_backend_subdir(NAME "IMGUI" READABLE_NAME "Dear ImGui" SUBDIR backends/ui/imgui)
|
||||||
if (NOT (DEFINED EMSCRIPTEN OR DEFINED ANDROID_NDK))
|
if (NOT (DEFINED EMSCRIPTEN OR DEFINED ANDROID_NDK))
|
||||||
ui_backend_subdir(NAME "GTK" READABLE_NAME "GTK4" SUBDIR backends/ui/gtk)
|
ui_backend_subdir(NAME "GTK" READABLE_NAME "GTK4" SUBDIR backends/ui/gtk)
|
||||||
endif()
|
endif()
|
||||||
execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen_ui_backend_inc.py ${CMAKE_CURRENT_BINARY_DIR} ${ENABLED_UIS})
|
playback_backend_subdir(NAME "VGMSTREAM" READABLE_NAME "VgmStream" SUBDIR backends/playback/vgmstream)
|
||||||
|
playback_backend_subdir(NAME "SDL_MIXER_X" READABLE_NAME "SDL Mixer X" SUBDIR backends/playback/sdl_mixer_x)
|
||||||
|
playback_backend_subdir(NAME "ZSM" READABLE_NAME "ZSM" SUBDIR backends/playback/zsm)
|
||||||
|
execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen_ui_backend_inc.py ${CMAKE_CURRENT_BINARY_DIR} --ui ${ENABLED_UIS} --playback ${ENABLED_PLAYBACK_BACKENDS})
|
||||||
prefix_all(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ main.cpp daemon_backend.cpp proxy_backend.cpp)
|
prefix_all(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ main.cpp daemon_backend.cpp proxy_backend.cpp)
|
||||||
list(APPEND SOURCES ${CMAKE_CURRENT_BINARY_DIR}/backend_glue.cpp)
|
list(APPEND SOURCES ${CMAKE_CURRENT_BINARY_DIR}/backend_glue.cpp)
|
||||||
if(DEFINED EMSCRIPTEN)
|
if(DEFINED EMSCRIPTEN)
|
||||||
|
@ -364,7 +445,7 @@ if(${ASCLI_EXE} STREQUAL "ASCLIEXE-NOTFOUND")
|
||||||
else()
|
else()
|
||||||
add_test(NAME "verify appstream metadata" COMMAND ${ASCLI_EXE} validate --no-net --pedantic "assets/com.complecwaft.Looper.metainfo.xml")
|
add_test(NAME "verify appstream metadata" COMMAND ${ASCLI_EXE} validate --no-net --pedantic "assets/com.complecwaft.Looper.metainfo.xml")
|
||||||
endif()
|
endif()
|
||||||
target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS})
|
target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS} ${PLAYBACK_BACKENDS})
|
||||||
install(TARGETS ${TARGET_NAME} ${EXTRA_LIBS})
|
install(TARGETS ${TARGET_NAME} ${EXTRA_LIBS})
|
||||||
if (${BUILD_SDL2})
|
if (${BUILD_SDL2})
|
||||||
install(EXPORT SDL2-static SDL2main)
|
install(EXPORT SDL2-static SDL2main)
|
||||||
|
|
28
assets/licenses/CRCpp.txt
Normal file
28
assets/licenses/CRCpp.txt
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
CRC++
|
||||||
|
Copyright (c) 2022, Daniel Bahr
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of CRC++ nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -62,7 +62,7 @@ def add_dbus(file: str, output_basename: str, adaptor: str|None = None, proxy: s
|
||||||
if which('sdbus-c++-xml2cpp') is not None:
|
if which('sdbus-c++-xml2cpp') is not None:
|
||||||
subprocess.call(['sdbus-c++-xml2cpp', file, '--adaptor=' + adaptor, '--proxy=' + proxy])
|
subprocess.call(['sdbus-c++-xml2cpp', file, '--adaptor=' + adaptor, '--proxy=' + proxy])
|
||||||
else:
|
else:
|
||||||
warn("Not generating DBus API stubs.")
|
warn("dbus-nogen", "Not generating DBus API stubs.")
|
||||||
compile_program("../backends/ui/imgui/imgui/misc/fonts/binary_to_compressed_c.cpp", "btcc")
|
compile_program("../backends/ui/imgui/imgui/misc/fonts/binary_to_compressed_c.cpp", "btcc")
|
||||||
for i in glob("Noto_Sans/*.ttf"):
|
for i in glob("Noto_Sans/*.ttf"):
|
||||||
add_font(i)
|
add_font(i)
|
||||||
|
|
3
backends/playback/sdl_mixer_x/CMakeLists.txt
Normal file
3
backends/playback/sdl_mixer_x/CMakeLists.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
set(BACKEND_SDL_MIXER_X_SRC ${CMAKE_CURRENT_SOURCE_DIR}/sdl_mixer_x.cpp)
|
||||||
|
set(BACKEND_SDL_MIXER_X_INC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
add_playback_backend(sdl_mixer_x_backend ${BACKEND_SDL_MIXER_X_SRC})
|
4
backends/playback/sdl_mixer_x/backend.json
Normal file
4
backends/playback/sdl_mixer_x/backend.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"class_name": "SDLMixerXBackend",
|
||||||
|
"include_path": "sdl_mixer_x.hpp"
|
||||||
|
}
|
103
backends/playback/sdl_mixer_x/sdl_mixer_x.cpp
Normal file
103
backends/playback/sdl_mixer_x/sdl_mixer_x.cpp
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
#include "sdl_mixer_x.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
#include "file_backend.hpp"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
std::optional<uint64_t> SDLMixerXBackend::get_max_samples() {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
void SDLMixerXBackend::load(const char *filename) {
|
||||||
|
Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK);
|
||||||
|
memset(&spec, 0, sizeof(spec));
|
||||||
|
spec.callback = NULL;
|
||||||
|
spec.channels = 2;
|
||||||
|
spec.format = AUDIO_F32SYS;
|
||||||
|
spec.freq = 48000;
|
||||||
|
spec.samples = get_max_samples().value();
|
||||||
|
spec.size = spec.samples * sizeof(float);
|
||||||
|
Mix_InitMixer(&spec, SDL_FALSE);
|
||||||
|
mixer = Mix_GetGeneralMixer();
|
||||||
|
{
|
||||||
|
std::filesystem::path fpath = std::string(filename);
|
||||||
|
fpath = fpath.parent_path() / fpath.stem();
|
||||||
|
|
||||||
|
std::string fpath_str = fpath.string();
|
||||||
|
std::vector<std::string> soundfonts = {fpath_str + ".sf2", fpath_str + ".dls"};
|
||||||
|
std::string sf_path_str = "";
|
||||||
|
bool any_path_exists = false;
|
||||||
|
for (auto sf_path : soundfonts) {
|
||||||
|
if (std::filesystem::exists(sf_path)) {
|
||||||
|
any_path_exists = true;
|
||||||
|
sf_path_str += ";" + sf_path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (any_path_exists) {
|
||||||
|
sf_path_str = sf_path_str.substr(1);
|
||||||
|
Mix_SetSoundFonts(sf_path_str.c_str());
|
||||||
|
} else {
|
||||||
|
Mix_SetSoundFonts(NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file = open_file(filename);
|
||||||
|
Mix_Music *output = Mix_LoadMUS_RW(get_sdl_file(this->file), 0);
|
||||||
|
if (output == nullptr) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
Mix_PlayMusicStream(output, -1);
|
||||||
|
length = Mix_MusicDuration(output);
|
||||||
|
current_file = std::string(filename);
|
||||||
|
const char *title_tag = Mix_GetMusicTitleTag(output);
|
||||||
|
// Check for an empty string, which indicates there's no title tag.
|
||||||
|
if (title_tag[0] == '\0') {
|
||||||
|
std::filesystem::path path(current_file);
|
||||||
|
current_title = path.stem().string();
|
||||||
|
} else {
|
||||||
|
current_title = std::string(title_tag);
|
||||||
|
}
|
||||||
|
this->music = output;
|
||||||
|
streams.clear();
|
||||||
|
PlaybackStream stream;
|
||||||
|
stream.id = 0;
|
||||||
|
stream.length = Mix_MusicDuration(output);
|
||||||
|
stream.name = current_title;
|
||||||
|
streams.push_back(stream);
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
void SDLMixerXBackend::switch_stream(int idx) { }
|
||||||
|
void SDLMixerXBackend::seek(double position) {
|
||||||
|
if (music == nullptr || file == nullptr) {
|
||||||
|
open = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Mix_SetMusicPositionStream(music, position);
|
||||||
|
}
|
||||||
|
size_t SDLMixerXBackend::render(void *buf, size_t maxlen) {
|
||||||
|
if (music == nullptr || file == nullptr) {
|
||||||
|
open = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (maxlen > spec.size) {
|
||||||
|
maxlen = spec.size;
|
||||||
|
}
|
||||||
|
// Remove partial sample frames.
|
||||||
|
maxlen /= sizeof(float);
|
||||||
|
maxlen *= sizeof(float);
|
||||||
|
mixer(NULL, (Uint8*)buf, maxlen);
|
||||||
|
return maxlen;
|
||||||
|
}
|
||||||
|
double SDLMixerXBackend::get_position() {
|
||||||
|
if (music == nullptr || file == nullptr) {
|
||||||
|
open = false;
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return Mix_GetMusicPosition(music);
|
||||||
|
}
|
||||||
|
void SDLMixerXBackend::cleanup() {
|
||||||
|
streams.clear();
|
||||||
|
Mix_HaltMusicStream(music);
|
||||||
|
Mix_FreeMusic(music);
|
||||||
|
delete file;
|
||||||
|
file = nullptr;
|
||||||
|
Mix_Quit();
|
||||||
|
open = false;
|
||||||
|
}
|
24
backends/playback/sdl_mixer_x/sdl_mixer_x.hpp
Normal file
24
backends/playback/sdl_mixer_x/sdl_mixer_x.hpp
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
#include "playback_backend.hpp"
|
||||||
|
#include <SDL_mixer.h>
|
||||||
|
#include "file_backend.hpp"
|
||||||
|
class SDLMixerXBackend : public PlaybackBackend {
|
||||||
|
Mix_Music *music;
|
||||||
|
Mix_CommonMixer_t mixer;
|
||||||
|
File *file;
|
||||||
|
public:
|
||||||
|
inline std::string get_id() override {
|
||||||
|
return "sdl_mixer_x";
|
||||||
|
}
|
||||||
|
inline std::string get_name() override {
|
||||||
|
return "SDL Mixer X";
|
||||||
|
}
|
||||||
|
std::optional<uint64_t> get_max_samples() override;
|
||||||
|
void seek(double position) override;
|
||||||
|
double get_position() override;
|
||||||
|
void load(const char *filename) override;
|
||||||
|
void switch_stream(int idx) override;
|
||||||
|
void cleanup() override;
|
||||||
|
size_t render(void *buf, size_t maxlen) override;
|
||||||
|
inline ~SDLMixerXBackend() override { }
|
||||||
|
};
|
3
backends/playback/vgmstream/CMakeLists.txt
Normal file
3
backends/playback/vgmstream/CMakeLists.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
set(BACKEND_VGMSTREAM_SRC ${CMAKE_CURRENT_SOURCE_DIR}/vgmstream.cpp)
|
||||||
|
set(BACKEND_VGMSTREAM_INC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
add_playback_backend(vgmstream_backend ${BACKEND_VGMSTREAM_SRC})
|
4
backends/playback/vgmstream/backend.json
Normal file
4
backends/playback/vgmstream/backend.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"class_name": "VgmStreamBackend",
|
||||||
|
"include_path": "vgmstream.hpp"
|
||||||
|
}
|
139
backends/playback/vgmstream/vgmstream.cpp
Normal file
139
backends/playback/vgmstream/vgmstream.cpp
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
#include "vgmstream.hpp"
|
||||||
|
extern "C" {
|
||||||
|
#include <vgmstream.h>
|
||||||
|
#include <util.h>
|
||||||
|
#include <plugins.h>
|
||||||
|
}
|
||||||
|
#include <filesystem>
|
||||||
|
#include "file_backend.hpp"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
void VgmStreamBackend::load(const char *filename) {
|
||||||
|
spec.format = AUDIO_S16SYS;
|
||||||
|
file = open_file(filename);
|
||||||
|
sf = get_sf_from_file(file);
|
||||||
|
if (!sf) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
auto *output = init_vgmstream_from_STREAMFILE(sf);
|
||||||
|
if (!output) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
int stream_count = output->num_streams;
|
||||||
|
close_vgmstream(output);
|
||||||
|
streams.clear();
|
||||||
|
for (int i = 0; i < stream_count; i++) {
|
||||||
|
PlaybackStream stream;
|
||||||
|
stream.id = i;
|
||||||
|
stream.length = 0;
|
||||||
|
stream.name = "";
|
||||||
|
if (!sf) {
|
||||||
|
streams.push_back(stream);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sf->stream_index = i;
|
||||||
|
auto *tmp = init_vgmstream_from_STREAMFILE(sf);
|
||||||
|
if (!tmp) {
|
||||||
|
streams.push_back(stream);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reset_vgmstream(tmp);
|
||||||
|
stream.length = (double)tmp->num_samples / (double)tmp->sample_rate;
|
||||||
|
char *buf = (char*)calloc(STREAM_NAME_SIZE + 1, 1);
|
||||||
|
strncpy(buf, tmp->stream_name, STREAM_NAME_SIZE);
|
||||||
|
if (buf[0] == '\0') {
|
||||||
|
free(buf);
|
||||||
|
buf = strdup("Unknown");
|
||||||
|
}
|
||||||
|
if (i == 0) {
|
||||||
|
char *buf2 = NULL;
|
||||||
|
const char *const DEFAULT_FORMAT_STR = "Default (%s)";
|
||||||
|
size_t buflen = snprintf(NULL, 0, DEFAULT_FORMAT_STR, buf) + 1;
|
||||||
|
buf2 = (char*)calloc(buflen, 1);
|
||||||
|
snprintf(buf2, buflen, DEFAULT_FORMAT_STR, buf);
|
||||||
|
stream.name = buf2;
|
||||||
|
free(buf2);
|
||||||
|
} else {
|
||||||
|
stream.name = buf;
|
||||||
|
}
|
||||||
|
streams.push_back(stream);
|
||||||
|
free(buf);
|
||||||
|
close_vgmstream(tmp);
|
||||||
|
}
|
||||||
|
switch_stream(0);
|
||||||
|
}
|
||||||
|
void VgmStreamBackend::switch_stream(int idx) {
|
||||||
|
if (vf == nullptr || sf == nullptr || file == nullptr) {
|
||||||
|
open = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sf->stream_index = idx;
|
||||||
|
if (idx >= streams.size()) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
vf = init_vgmstream_from_STREAMFILE(sf);
|
||||||
|
if (!vf) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
vgmstream_cfg_t cfg = {0};
|
||||||
|
cfg.allow_play_forever = 1;
|
||||||
|
cfg.play_forever = 1;
|
||||||
|
vgmstream_apply_config(vf, &cfg);
|
||||||
|
spec.channels = vf->channels;
|
||||||
|
spec.freq = vf->sample_rate;
|
||||||
|
length = streams[idx].length;
|
||||||
|
const char *title_tag = vf->stream_name;
|
||||||
|
if (title_tag[0] == '\0') {
|
||||||
|
std::filesystem::path path(current_file);
|
||||||
|
current_title = path.stem().string();
|
||||||
|
} else {
|
||||||
|
char *buf = (char*)calloc(STREAM_NAME_SIZE + 1, 1);
|
||||||
|
strncpy(buf, title_tag, STREAM_NAME_SIZE);
|
||||||
|
current_title = buf;
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
open = true;
|
||||||
|
spec.channels = vf->channels;
|
||||||
|
spec.freq = vf->sample_rate;
|
||||||
|
}
|
||||||
|
void VgmStreamBackend::cleanup() {
|
||||||
|
streams.clear();
|
||||||
|
close_vgmstream(vf);
|
||||||
|
close_streamfile(sf);
|
||||||
|
vf = nullptr;
|
||||||
|
sf = nullptr;
|
||||||
|
delete file;
|
||||||
|
file = nullptr;
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
size_t VgmStreamBackend::render(void *buf, size_t maxlen) {
|
||||||
|
if (vf == nullptr || sf == nullptr || file == nullptr) {
|
||||||
|
open = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
maxlen /= sizeof(sample_t);
|
||||||
|
size_t new_samples = render_vgmstream((sample_t*)(buf), (int)maxlen, vf);
|
||||||
|
if (maxlen > new_samples) {
|
||||||
|
reset_vgmstream(vf);
|
||||||
|
position = 0.0;
|
||||||
|
} else {
|
||||||
|
position += (double)new_samples / (double)vf->sample_rate;
|
||||||
|
}
|
||||||
|
return new_samples * sizeof(sample_t);
|
||||||
|
}
|
||||||
|
void VgmStreamBackend::seek(double position) {
|
||||||
|
|
||||||
|
if (vf == nullptr || sf == nullptr || file == nullptr) {
|
||||||
|
open = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto pos32 = (int32_t)(position * vf->sample_rate);
|
||||||
|
seek_vgmstream(vf, pos32);
|
||||||
|
this->position = position;
|
||||||
|
}
|
||||||
|
double VgmStreamBackend::get_position() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
int VgmStreamBackend::get_stream_idx() {
|
||||||
|
return vf->stream_index;
|
||||||
|
}
|
28
backends/playback/vgmstream/vgmstream.hpp
Normal file
28
backends/playback/vgmstream/vgmstream.hpp
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#pragma once
|
||||||
|
#include "playback_backend.hpp"
|
||||||
|
extern "C" {
|
||||||
|
#include "vgmstream.h"
|
||||||
|
}
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "file_backend.hpp"
|
||||||
|
class VgmStreamBackend : public PlaybackBackend {
|
||||||
|
STREAMFILE *sf;
|
||||||
|
VGMSTREAM *vf;
|
||||||
|
File *file;
|
||||||
|
public:
|
||||||
|
inline std::string get_id() override {
|
||||||
|
return "vgmstream";
|
||||||
|
}
|
||||||
|
inline std::string get_name() override {
|
||||||
|
return "VGMStream";
|
||||||
|
}
|
||||||
|
void seek(double position) override;
|
||||||
|
void load(const char *filename) override;
|
||||||
|
void switch_stream(int idx) override;
|
||||||
|
void cleanup() override;
|
||||||
|
int get_stream_idx() override;
|
||||||
|
size_t render(void *buf, size_t maxlen) override;
|
||||||
|
double get_position() override;
|
||||||
|
inline ~VgmStreamBackend() override { }
|
||||||
|
};
|
6
backends/playback/zsm/CMakeLists.txt
Normal file
6
backends/playback/zsm/CMakeLists.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
set(X16_DIR ${CMAKE_CURRENT_SOURCE_DIR}/x16emu)
|
||||||
|
set(YMFM_DIR ${X16_DIR}/ymfm/src)
|
||||||
|
set(BACKEND_ZSM_SRC ${CMAKE_CURRENT_SOURCE_DIR}/zsm_backend.cpp ${X16_DIR}/audio.c ${X16_DIR}/vera_pcm.c ${X16_DIR}/vera_psg.c ${X16_DIR}/ymglue.cpp ${YMFM_DIR}/ymfm_adpcm.cpp ${YMFM_DIR}/ymfm_opl.cpp ${YMFM_DIR}/ymfm_opm.cpp ${YMFM_DIR}/ymfm_pcm.cpp)
|
||||||
|
set(BACKEND_ZSM_INC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${YMFM_DIR} ${X16_DIR})
|
||||||
|
add_playback_backend(zsm_backend ${BACKEND_ZSM_SRC})
|
||||||
|
target_include_directories(zsm_backend PRIVATE ${BACKEND_ZSM_INC})
|
4
backends/playback/zsm/backend.json
Normal file
4
backends/playback/zsm/backend.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"class_name": "ZsmBackend",
|
||||||
|
"include_path": "zsm_backend.hpp"
|
||||||
|
}
|
354
backends/playback/zsm/x16emu/audio.c
Normal file
354
backends/playback/zsm/x16emu/audio.c
Normal file
|
@ -0,0 +1,354 @@
|
||||||
|
// Commander X16 Emulator
|
||||||
|
// Copyright (c) 2020 Frank van den Hoef
|
||||||
|
// All rights reserved. License: 2-clause BSD
|
||||||
|
|
||||||
|
#include "audio.h"
|
||||||
|
#include "glue.h"
|
||||||
|
#include "vera_psg.h"
|
||||||
|
#include "vera_pcm.h"
|
||||||
|
#include "ymglue.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#define SAMPLES_PER_BUFFER (1024)
|
||||||
|
#define SAMP_POS_FRAC_BITS (22)
|
||||||
|
#else
|
||||||
|
#define SAMPLES_PER_BUFFER (256)
|
||||||
|
#define SAMP_POS_FRAC_BITS (24)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define VERA_SAMP_CLKS_PER_CPU_CLK ((25000000ULL << SAMP_POS_FRAC_BITS) / 512 / MHZ / 1000000)
|
||||||
|
#define YM_SAMP_CLKS_PER_CPU_CLK ((3579545ULL << SAMP_POS_FRAC_BITS) / 64 / MHZ / 1000000)
|
||||||
|
#define SAMPLE_BYTES (2 * sizeof(int16_t))
|
||||||
|
#define SAMP_POS_MASK (SAMPLES_PER_BUFFER - 1)
|
||||||
|
#define SAMP_POS_MASK_FRAC (((uint32_t)SAMPLES_PER_BUFFER << SAMP_POS_FRAC_BITS) - 1)
|
||||||
|
|
||||||
|
// windowed sinc
|
||||||
|
static const int16_t filter[512] = {
|
||||||
|
32767,32765,32761,32755,32746,32736,32723,32707,32690,32670,32649,32625,32598,32570,32539,32507,
|
||||||
|
32472,32435,32395,32354,32310,32265,32217,32167,32115,32061,32004,31946,31885,31823,31758,31691,
|
||||||
|
31623,31552,31479,31404,31327,31248,31168,31085,31000,30913,30825,30734,30642,30547,30451,30353,
|
||||||
|
30253,30151,30048,29943,29835,29726,29616,29503,29389,29273,29156,29037,28916,28793,28669,28544,
|
||||||
|
28416,28288,28157,28025,27892,27757,27621,27483,27344,27204,27062,26918,26774,26628,26481,26332,
|
||||||
|
26182,26031,25879,25726,25571,25416,25259,25101,24942,24782,24621,24459,24296,24132,23967,23801,
|
||||||
|
23634,23466,23298,23129,22959,22788,22616,22444,22271,22097,21923,21748,21572,21396,21219,21042,
|
||||||
|
20864,20686,20507,20328,20148,19968,19788,19607,19426,19245,19063,18881,18699,18517,18334,18152,
|
||||||
|
17969,17786,17603,17420,17237,17054,16871,16688,16505,16322,16139,15957,15774,15592,15409,15227,
|
||||||
|
15046,14864,14683,14502,14321,14141,13961,13781,13602,13423,13245,13067,12890,12713,12536,12360,
|
||||||
|
12185,12010,11836,11663,11490,11317,11146,10975,10804,10635,10466,10298,10131, 9964, 9799, 9634,
|
||||||
|
9470, 9306, 9144, 8983, 8822, 8662, 8504, 8346, 8189, 8033, 7878, 7724, 7571, 7419, 7268, 7118,
|
||||||
|
6969, 6822, 6675, 6529, 6385, 6241, 6099, 5958, 5818, 5679, 5541, 5405, 5269, 5135, 5002, 4870,
|
||||||
|
4739, 4610, 4482, 4355, 4229, 4104, 3981, 3859, 3738, 3619, 3500, 3383, 3268, 3153, 3040, 2928,
|
||||||
|
2817, 2708, 2600, 2493, 2388, 2284, 2181, 2079, 1979, 1880, 1783, 1686, 1591, 1498, 1405, 1314,
|
||||||
|
1225, 1136, 1049, 963, 879, 795, 714, 633, 554, 476, 399, 323, 249, 176, 105, 34,
|
||||||
|
-34, -102, -168, -234, -298, -361, -422, -482, -542, -599, -656, -712, -766, -819, -871, -922,
|
||||||
|
-971,-1020,-1067,-1113,-1158,-1202,-1244,-1286,-1326,-1366,-1404,-1441,-1477,-1512,-1546,-1579,
|
||||||
|
-1611,-1642,-1671,-1700,-1728,-1755,-1781,-1806,-1830,-1852,-1874,-1896,-1916,-1935,-1953,-1971,
|
||||||
|
-1987,-2003,-2018,-2032,-2045,-2058,-2069,-2080,-2090,-2099,-2108,-2116,-2123,-2129,-2134,-2139,
|
||||||
|
-2143,-2147,-2150,-2152,-2153,-2154,-2154,-2154,-2153,-2151,-2149,-2146,-2143,-2139,-2135,-2130,
|
||||||
|
-2124,-2118,-2112,-2105,-2098,-2090,-2082,-2073,-2064,-2054,-2045,-2034,-2024,-2012,-2001,-1989,
|
||||||
|
-1977,-1965,-1952,-1939,-1926,-1912,-1898,-1884,-1870,-1855,-1840,-1825,-1810,-1794,-1778,-1762,
|
||||||
|
-1746,-1730,-1714,-1697,-1680,-1663,-1646,-1629,-1612,-1595,-1577,-1560,-1542,-1525,-1507,-1489,
|
||||||
|
-1471,-1453,-1435,-1418,-1400,-1382,-1364,-1346,-1328,-1310,-1292,-1274,-1256,-1238,-1220,-1203,
|
||||||
|
-1185,-1167,-1150,-1132,-1115,-1097,-1080,-1063,-1046,-1029,-1012, -995, -978, -962, -945, -929,
|
||||||
|
-912, -896, -880, -864, -849, -833, -817, -802, -787, -772, -757, -742, -727, -713, -699, -684,
|
||||||
|
-670, -656, -643, -629, -616, -603, -589, -577, -564, -551, -539, -526, -514, -502, -491, -479,
|
||||||
|
-468, -456, -445, -434, -423, -413, -402, -392, -381, -371, -361, -352, -342, -333, -323, -314,
|
||||||
|
-305, -296, -288, -279, -270, -262, -254, -246, -238, -230, -222, -215, -207, -200, -193, -186,
|
||||||
|
-179, -172, -165, -158, -152, -145, -139, -133, -127, -120, -114, -108, -103, -97, -91, -85,
|
||||||
|
-80, -74, -69, -63, -58, -53, -47, -42, -37, -32, -27, -22, -17, -12, -7, -2
|
||||||
|
};
|
||||||
|
|
||||||
|
static SDL_AudioDeviceID audio_dev;
|
||||||
|
static int16_t * buffer;
|
||||||
|
static uint32_t buffer_size = 0;
|
||||||
|
static uint32_t rdidx = 0;
|
||||||
|
static uint32_t wridx = 0;
|
||||||
|
static uint32_t buffer_written = 0;
|
||||||
|
static uint32_t vera_samp_pos_rd = 0;
|
||||||
|
static uint32_t vera_samp_pos_wr = 0;
|
||||||
|
static uint32_t vera_samp_pos_hd = 0;
|
||||||
|
static uint32_t ym_samp_pos_rd = 0;
|
||||||
|
static uint32_t ym_samp_pos_wr = 0;
|
||||||
|
static uint32_t ym_samp_pos_hd = 0;
|
||||||
|
static uint32_t vera_samps_per_host_samps = 0;
|
||||||
|
static uint32_t ym_samps_per_host_samps = 0;
|
||||||
|
static uint32_t limiter_amp = 0;
|
||||||
|
|
||||||
|
static int16_t psg_buf[2 * SAMPLES_PER_BUFFER];
|
||||||
|
static int16_t pcm_buf[2 * SAMPLES_PER_BUFFER];
|
||||||
|
static int16_t ym_buf[2 * SAMPLES_PER_BUFFER];
|
||||||
|
|
||||||
|
uint32_t host_sample_rate = 0;
|
||||||
|
|
||||||
|
void
|
||||||
|
audio_callback(void *userdata, Uint8 *stream, int len)
|
||||||
|
{
|
||||||
|
int expected = SAMPLES_PER_BUFFER * SAMPLE_BYTES;
|
||||||
|
if (len != expected) {
|
||||||
|
printf("Audio buffer size mismatch! (expected: %d, got: %d)\n", expected, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t spos = 0;
|
||||||
|
if (rdidx > wridx) {
|
||||||
|
uint32_t actual_len = SDL_min(len / SAMPLE_BYTES, (buffer_size - rdidx) / 2);
|
||||||
|
if (actual_len > 0) {
|
||||||
|
memcpy(&stream[spos], &buffer[rdidx], actual_len * SAMPLE_BYTES);
|
||||||
|
spos += actual_len * SAMPLE_BYTES;
|
||||||
|
len -= actual_len * SAMPLE_BYTES;
|
||||||
|
rdidx = (rdidx + actual_len * 2) % buffer_size;
|
||||||
|
buffer_written -= actual_len * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint32_t actual_len = SDL_min(len / SAMPLE_BYTES, (wridx - rdidx) / 2);
|
||||||
|
if (actual_len > 0) {
|
||||||
|
memcpy(&stream[spos], &buffer[rdidx], actual_len * SAMPLE_BYTES);
|
||||||
|
spos += actual_len * SAMPLE_BYTES;
|
||||||
|
len -= actual_len * SAMPLE_BYTES;
|
||||||
|
rdidx = (rdidx + actual_len * 2) % buffer_size;
|
||||||
|
buffer_written -= actual_len * 2;
|
||||||
|
}
|
||||||
|
if (len > 0) memset(&stream[spos], 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AudioSpec obtained;
|
||||||
|
void
|
||||||
|
audio_init(const char *dev_name, int num_audio_buffers)
|
||||||
|
{
|
||||||
|
if (audio_dev > 0) {
|
||||||
|
audio_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev_name) {
|
||||||
|
if (!strcmp("none", dev_name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set number of buffers
|
||||||
|
int num_bufs = num_audio_buffers;
|
||||||
|
if (num_bufs < 3) {
|
||||||
|
num_bufs = 3;
|
||||||
|
}
|
||||||
|
if (num_bufs > 1024) {
|
||||||
|
num_bufs = 1024;
|
||||||
|
}
|
||||||
|
buffer_size = SAMPLES_PER_BUFFER * num_bufs * 2;
|
||||||
|
|
||||||
|
// Allocate audio buffer
|
||||||
|
buffer = malloc(buffer_size * sizeof(int16_t));
|
||||||
|
rdidx = 0;
|
||||||
|
wridx = 0;
|
||||||
|
buffer_written = 0;
|
||||||
|
|
||||||
|
SDL_AudioSpec desired;
|
||||||
|
|
||||||
|
// Setup SDL audio
|
||||||
|
memset(&desired, 0, sizeof(desired));
|
||||||
|
desired.freq = AUDIO_SAMPLERATE;
|
||||||
|
desired.format = AUDIO_S16SYS;
|
||||||
|
desired.samples = SAMPLES_PER_BUFFER;
|
||||||
|
desired.channels = 2;
|
||||||
|
desired.callback = audio_callback;
|
||||||
|
|
||||||
|
obtained = desired;
|
||||||
|
if (obtained.freq <= 0 || (AUDIO_SAMPLERATE / obtained.freq) > SAMPLES_PER_BUFFER) {
|
||||||
|
fprintf(stderr, "Obtained sample rate is too low");
|
||||||
|
audio_close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init YM2151 emulation. 3.579545 MHz clock
|
||||||
|
YM_Create(3579545);
|
||||||
|
YM_init(3579545/64, 60);
|
||||||
|
|
||||||
|
host_sample_rate = obtained.freq;
|
||||||
|
vera_samps_per_host_samps = ((25000000ULL << SAMP_POS_FRAC_BITS) / 512 / host_sample_rate);
|
||||||
|
ym_samps_per_host_samps = ((3579545ULL << SAMP_POS_FRAC_BITS) / 64 / host_sample_rate);
|
||||||
|
vera_samp_pos_rd = 0;
|
||||||
|
vera_samp_pos_wr = 0;
|
||||||
|
vera_samp_pos_hd = 0;
|
||||||
|
ym_samp_pos_rd = 0;
|
||||||
|
ym_samp_pos_wr = 0;
|
||||||
|
ym_samp_pos_hd = 0;
|
||||||
|
limiter_amp = (1 << 16);
|
||||||
|
|
||||||
|
psg_buf[0] = psg_buf[1] = 0;
|
||||||
|
pcm_buf[0] = pcm_buf[1] = 0;
|
||||||
|
ym_buf[0] = ym_buf[1] = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
audio_close(void)
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
// Free audio buffers
|
||||||
|
if (buffer != NULL) {
|
||||||
|
free(buffer);
|
||||||
|
buffer = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
audio_step(int cpu_clocks)
|
||||||
|
{
|
||||||
|
|
||||||
|
while (cpu_clocks > 0) {
|
||||||
|
// Only the source with the higest sample rate (YM2151) is needed for calculation
|
||||||
|
uint32_t max_cpu_clks_ym = ((ym_samp_pos_rd - ym_samp_pos_hd - (1 << SAMP_POS_FRAC_BITS)) & SAMP_POS_MASK_FRAC) / YM_SAMP_CLKS_PER_CPU_CLK;
|
||||||
|
uint32_t max_cpu_clks = SDL_min(cpu_clocks, max_cpu_clks_ym);
|
||||||
|
vera_samp_pos_hd = (vera_samp_pos_hd + max_cpu_clks * VERA_SAMP_CLKS_PER_CPU_CLK) & SAMP_POS_MASK_FRAC;
|
||||||
|
ym_samp_pos_hd = (ym_samp_pos_hd + max_cpu_clks * YM_SAMP_CLKS_PER_CPU_CLK) & SAMP_POS_MASK_FRAC;
|
||||||
|
cpu_clocks -= max_cpu_clks;
|
||||||
|
if (cpu_clocks > 0) audio_render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
audio_render()
|
||||||
|
{
|
||||||
|
// Render all audio sources until read and write positions catch up
|
||||||
|
// This happens when there's a change to sound registers or one of the
|
||||||
|
// sources' sample buffer head position is too far
|
||||||
|
|
||||||
|
uint32_t pos, len;
|
||||||
|
|
||||||
|
pos = (vera_samp_pos_wr + 1) & SAMP_POS_MASK;
|
||||||
|
len = ((vera_samp_pos_hd >> SAMP_POS_FRAC_BITS) - vera_samp_pos_wr) & SAMP_POS_MASK;
|
||||||
|
vera_samp_pos_wr = vera_samp_pos_hd >> SAMP_POS_FRAC_BITS;
|
||||||
|
if (pos + len > SAMPLES_PER_BUFFER) {
|
||||||
|
psg_render(&psg_buf[pos * 2], SAMPLES_PER_BUFFER - pos);
|
||||||
|
pcm_render(&pcm_buf[pos * 2], SAMPLES_PER_BUFFER - pos);
|
||||||
|
len -= SAMPLES_PER_BUFFER - pos;
|
||||||
|
pos = 0;
|
||||||
|
}
|
||||||
|
if (len > 0) {
|
||||||
|
psg_render(&psg_buf[pos * 2], len);
|
||||||
|
pcm_render(&pcm_buf[pos * 2], len);
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = (ym_samp_pos_wr + 1) & SAMP_POS_MASK;
|
||||||
|
len = ((ym_samp_pos_hd >> SAMP_POS_FRAC_BITS) - ym_samp_pos_wr) & SAMP_POS_MASK;
|
||||||
|
ym_samp_pos_wr = ym_samp_pos_hd >> SAMP_POS_FRAC_BITS;
|
||||||
|
if ((pos + len) > SAMPLES_PER_BUFFER) {
|
||||||
|
YM_stream_update((uint16_t *)&ym_buf[pos * 2], SAMPLES_PER_BUFFER - pos);
|
||||||
|
len -= SAMPLES_PER_BUFFER - pos;
|
||||||
|
pos = 0;
|
||||||
|
}
|
||||||
|
if (len > 0) {
|
||||||
|
YM_stream_update((uint16_t *)&ym_buf[pos * 2], len);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t wridx_old = wridx;
|
||||||
|
uint32_t len_vera = (vera_samp_pos_hd - vera_samp_pos_rd) & SAMP_POS_MASK_FRAC;
|
||||||
|
uint32_t len_ym = (ym_samp_pos_hd - ym_samp_pos_rd) & SAMP_POS_MASK_FRAC;
|
||||||
|
if (len_vera < (4 << SAMP_POS_FRAC_BITS) || len_ym < (4 << SAMP_POS_FRAC_BITS)) {
|
||||||
|
// not enough samples yet, at least 4 are needed for the filter
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
len_vera = (len_vera - (4 << SAMP_POS_FRAC_BITS)) / vera_samps_per_host_samps;
|
||||||
|
len_ym = (len_ym - (4 << SAMP_POS_FRAC_BITS)) / ym_samps_per_host_samps;
|
||||||
|
len = SDL_min(len_vera, len_ym);
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
int32_t samp[8];
|
||||||
|
int32_t filter_idx = 0;
|
||||||
|
int32_t vera_out_l = 0;
|
||||||
|
int32_t vera_out_r = 0;
|
||||||
|
int32_t ym_out_l = 0;
|
||||||
|
int32_t ym_out_r = 0;
|
||||||
|
// Don't resample VERA outputs if the host sample rate is as desired
|
||||||
|
if (host_sample_rate == AUDIO_SAMPLERATE) {
|
||||||
|
pos = (vera_samp_pos_rd >> SAMP_POS_FRAC_BITS) * 2;
|
||||||
|
vera_out_l = ((int32_t)psg_buf[pos] + pcm_buf[pos]) << 14;
|
||||||
|
vera_out_r = ((int32_t)psg_buf[pos + 1] + pcm_buf[pos + 1]) << 14;
|
||||||
|
} else {
|
||||||
|
filter_idx = (vera_samp_pos_rd >> (SAMP_POS_FRAC_BITS - 8)) & 0xff;
|
||||||
|
pos = (vera_samp_pos_rd >> SAMP_POS_FRAC_BITS) * 2;
|
||||||
|
for (int j = 0; j < 8; j += 2) {
|
||||||
|
samp[j] = (int32_t)psg_buf[pos] + pcm_buf[pos];
|
||||||
|
samp[j + 1] = (int32_t)psg_buf[pos + 1] + pcm_buf[pos + 1];
|
||||||
|
pos = (pos + 2) & (SAMP_POS_MASK * 2);
|
||||||
|
}
|
||||||
|
vera_out_l += samp[0] * filter[256 + filter_idx];
|
||||||
|
vera_out_r += samp[1] * filter[256 + filter_idx];
|
||||||
|
vera_out_l += samp[2] * filter[ 0 + filter_idx];
|
||||||
|
vera_out_r += samp[3] * filter[ 0 + filter_idx];
|
||||||
|
vera_out_l += samp[4] * filter[255 - filter_idx];
|
||||||
|
vera_out_r += samp[5] * filter[255 - filter_idx];
|
||||||
|
vera_out_l += samp[6] * filter[511 - filter_idx];
|
||||||
|
vera_out_r += samp[7] * filter[511 - filter_idx];
|
||||||
|
}
|
||||||
|
filter_idx = (ym_samp_pos_rd >> (SAMP_POS_FRAC_BITS - 8)) & 0xff;
|
||||||
|
pos = (ym_samp_pos_rd >> SAMP_POS_FRAC_BITS) * 2;
|
||||||
|
for (int j = 0; j < 8; j += 2) {
|
||||||
|
samp[j] = ym_buf[pos];
|
||||||
|
samp[j + 1] = ym_buf[pos + 1];
|
||||||
|
pos = (pos + 2) & (SAMP_POS_MASK * 2);
|
||||||
|
}
|
||||||
|
ym_out_l += samp[0] * filter[256 + filter_idx];
|
||||||
|
ym_out_r += samp[1] * filter[256 + filter_idx];
|
||||||
|
ym_out_l += samp[2] * filter[ 0 + filter_idx];
|
||||||
|
ym_out_r += samp[3] * filter[ 0 + filter_idx];
|
||||||
|
ym_out_l += samp[4] * filter[255 - filter_idx];
|
||||||
|
ym_out_r += samp[5] * filter[255 - filter_idx];
|
||||||
|
ym_out_l += samp[6] * filter[511 - filter_idx];
|
||||||
|
ym_out_r += samp[7] * filter[511 - filter_idx];
|
||||||
|
// Mixing is according to the Developer Board
|
||||||
|
// Loudest single PSG channel is 1/8 times the max output
|
||||||
|
// mix = (psg + pcm) * 2 + ym
|
||||||
|
int32_t mix_l = (vera_out_l >> 13) + (ym_out_l >> 15);
|
||||||
|
int32_t mix_r = (vera_out_r >> 13) + (ym_out_r >> 15);
|
||||||
|
uint32_t amp = SDL_max(SDL_abs(mix_l), SDL_abs(mix_r));
|
||||||
|
if (amp > 32767) {
|
||||||
|
uint32_t limiter_amp_new = (32767 << 16) / amp;
|
||||||
|
limiter_amp = SDL_min(limiter_amp_new, limiter_amp);
|
||||||
|
}
|
||||||
|
buffer[wridx++] = (int16_t)((mix_l * limiter_amp) >> 16);
|
||||||
|
buffer[wridx++] = (int16_t)((mix_r * limiter_amp) >> 16);
|
||||||
|
if (limiter_amp < (1 << 16)) limiter_amp++;
|
||||||
|
vera_samp_pos_rd = (vera_samp_pos_rd + vera_samps_per_host_samps) & SAMP_POS_MASK_FRAC;
|
||||||
|
ym_samp_pos_rd = (ym_samp_pos_rd + ym_samps_per_host_samps) & SAMP_POS_MASK_FRAC;
|
||||||
|
}
|
||||||
|
buffer_written += len * 2;
|
||||||
|
if (buffer_written > buffer_size) {
|
||||||
|
// Prevent the buffer from overflowing by skipping the read pointer ahead.
|
||||||
|
uint32_t buffer_skip_amount = (buffer_written / buffer_size) * SAMPLES_PER_BUFFER * 2;
|
||||||
|
rdidx = (rdidx + buffer_skip_amount) % buffer_size;
|
||||||
|
buffer_written -= buffer_skip_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// catch up all buffers if they are too far behind
|
||||||
|
uint32_t skip = len_vera - len;
|
||||||
|
if (skip > 1) {
|
||||||
|
vera_samp_pos_rd = (vera_samp_pos_rd + vera_samps_per_host_samps) & SAMP_POS_MASK_FRAC;
|
||||||
|
}
|
||||||
|
skip = len_ym - len;
|
||||||
|
if (skip > 1) {
|
||||||
|
ym_samp_pos_rd = (ym_samp_pos_rd + ym_samps_per_host_samps) & SAMP_POS_MASK_FRAC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
audio_usage(void)
|
||||||
|
{
|
||||||
|
// SDL_GetAudioDeviceName doesn't work if audio isn't initialized.
|
||||||
|
// Since argument parsing happens before initializing SDL, ensure the
|
||||||
|
// audio subsystem is initialized before printing audio device names.
|
||||||
|
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
|
||||||
|
// List all available sound devices
|
||||||
|
printf("The following sound output devices are available:\n");
|
||||||
|
const int sounds = SDL_GetNumAudioDevices(0);
|
||||||
|
for (int i = 0; i < sounds; ++i) {
|
||||||
|
printf("\t%s\n", SDL_GetAudioDeviceName(i, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Quit();
|
||||||
|
exit(1);
|
||||||
|
}
|
16
backends/playback/zsm/x16emu/audio.h
Normal file
16
backends/playback/zsm/x16emu/audio.h
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Commander X16 Emulator
|
||||||
|
// Copyright (c) 2020 Frank van den Hoef
|
||||||
|
// All rights reserved. License: 2-clause BSD
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#define AUDIO_SAMPLERATE (25000000 / 512)
|
||||||
|
void audio_callback(void *userdata, Uint8 *stream, int len);
|
||||||
|
void audio_init(const char *dev_name, int num_audio_buffers);
|
||||||
|
void audio_close(void);
|
||||||
|
void audio_step(int cpu_clocks);
|
||||||
|
void audio_render();
|
||||||
|
|
||||||
|
void audio_usage(void);
|
20
backends/playback/zsm/x16emu/glue.h
Normal file
20
backends/playback/zsm/x16emu/glue.h
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Commander X16 Emulator
|
||||||
|
// Copyright (c) 2019 Michael Steil
|
||||||
|
// All rights reserved. License: 2-clause BSD
|
||||||
|
|
||||||
|
#ifndef _GLUE_H_
|
||||||
|
#define _GLUE_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
|
||||||
|
//#define TRACE
|
||||||
|
//#define PERFSTAT
|
||||||
|
|
||||||
|
|
||||||
|
extern void init_audio();
|
||||||
|
#define MHZ 8
|
||||||
|
|
||||||
|
#endif
|
182
backends/playback/zsm/x16emu/vera_pcm.c
Normal file
182
backends/playback/zsm/x16emu/vera_pcm.c
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
// Commander X16 Emulator
|
||||||
|
// Copyright (c) 2020 Frank van den Hoef
|
||||||
|
// All rights reserved. License: 2-clause BSD
|
||||||
|
|
||||||
|
#include "vera_pcm.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static uint8_t fifo[4096];
|
||||||
|
static unsigned fifo_wridx;
|
||||||
|
static unsigned fifo_rdidx;
|
||||||
|
static unsigned fifo_cnt;
|
||||||
|
|
||||||
|
static uint8_t ctrl;
|
||||||
|
static uint8_t rate;
|
||||||
|
static uint8_t loop;
|
||||||
|
|
||||||
|
static uint8_t volume_lut[16] = {0, 1, 2, 3, 4, 5, 6, 8, 11, 14, 18, 23, 30, 38, 49, 64};
|
||||||
|
|
||||||
|
static int16_t cur_l, cur_r;
|
||||||
|
static uint8_t phase;
|
||||||
|
|
||||||
|
static void
|
||||||
|
fifo_reset(void)
|
||||||
|
{
|
||||||
|
fifo_wridx = 0;
|
||||||
|
fifo_rdidx = 0;
|
||||||
|
fifo_cnt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
fifo_restart(void)
|
||||||
|
{
|
||||||
|
fifo_rdidx = 0;
|
||||||
|
fifo_cnt = fifo_wridx;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
pcm_reset(void)
|
||||||
|
{
|
||||||
|
fifo_reset();
|
||||||
|
ctrl = 0;
|
||||||
|
rate = 0;
|
||||||
|
cur_l = 0;
|
||||||
|
cur_r = 0;
|
||||||
|
phase = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
pcm_write_ctrl(uint8_t val)
|
||||||
|
{
|
||||||
|
if ((val & 0xc0) == 0xc0) {
|
||||||
|
loop = true;
|
||||||
|
} else {
|
||||||
|
loop = false;
|
||||||
|
if (val & 0x80) {
|
||||||
|
fifo_reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (val & 0x40) {
|
||||||
|
fifo_restart();
|
||||||
|
}
|
||||||
|
ctrl = val & 0x3F;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t
|
||||||
|
pcm_read_ctrl(void)
|
||||||
|
{
|
||||||
|
uint8_t result = ctrl;
|
||||||
|
if (fifo_cnt == sizeof(fifo) - 1) {
|
||||||
|
result |= 0x80;
|
||||||
|
}
|
||||||
|
if (fifo_cnt == 0) {
|
||||||
|
result |= 0x40;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
pcm_write_rate(uint8_t val)
|
||||||
|
{
|
||||||
|
rate = (val > 128) ? (256 - val) : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t
|
||||||
|
pcm_read_rate(void)
|
||||||
|
{
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
pcm_write_fifo(uint8_t val)
|
||||||
|
{
|
||||||
|
if (fifo_cnt < sizeof(fifo) - 1) {
|
||||||
|
fifo[fifo_wridx++] = val;
|
||||||
|
if (fifo_wridx == sizeof(fifo)) {
|
||||||
|
fifo_wridx = 0;
|
||||||
|
}
|
||||||
|
fifo_cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t
|
||||||
|
read_fifo()
|
||||||
|
{
|
||||||
|
static uint8_t result = 0;
|
||||||
|
if (fifo_cnt == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
result = fifo[fifo_rdidx++];
|
||||||
|
if (fifo_rdidx == sizeof(fifo)) {
|
||||||
|
fifo_rdidx = 0;
|
||||||
|
}
|
||||||
|
fifo_cnt--;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
pcm_is_fifo_almost_empty(void)
|
||||||
|
{
|
||||||
|
return fifo_cnt < 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
pcm_render(int16_t *buf, unsigned num_samples)
|
||||||
|
{
|
||||||
|
while (num_samples--) {
|
||||||
|
uint8_t old_phase = phase;
|
||||||
|
phase += rate;
|
||||||
|
if ((old_phase & 0x80) != (phase & 0x80)) {
|
||||||
|
if (fifo_cnt == 0) {
|
||||||
|
cur_l = 0;
|
||||||
|
cur_r = 0;
|
||||||
|
} else {
|
||||||
|
switch ((ctrl >> 4) & 3) {
|
||||||
|
case 0: { // mono 8-bit
|
||||||
|
cur_l = (int16_t)read_fifo() << 8;
|
||||||
|
cur_r = cur_l;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: { // stereo 8-bit
|
||||||
|
if (fifo_cnt < 2) {
|
||||||
|
fifo_cnt = 0;
|
||||||
|
fifo_rdidx = fifo_wridx;
|
||||||
|
} else {
|
||||||
|
cur_l = read_fifo() << 8;
|
||||||
|
cur_r = read_fifo() << 8;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: { // mono 16-bit
|
||||||
|
if (fifo_cnt < 2) {
|
||||||
|
fifo_cnt = 0;
|
||||||
|
fifo_rdidx = fifo_wridx;
|
||||||
|
} else {
|
||||||
|
cur_l = read_fifo();
|
||||||
|
cur_l |= read_fifo() << 8;
|
||||||
|
cur_r = cur_l;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: { // stereo 16-bit
|
||||||
|
if (fifo_cnt < 4) {
|
||||||
|
fifo_cnt = 0;
|
||||||
|
fifo_rdidx = fifo_wridx;
|
||||||
|
} else {
|
||||||
|
cur_l = read_fifo();
|
||||||
|
cur_l |= read_fifo() << 8;
|
||||||
|
cur_r = read_fifo();
|
||||||
|
cur_r |= read_fifo() << 8;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loop && fifo_cnt == 0) {
|
||||||
|
fifo_restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*(buf++) = (int16_t)((int32_t)cur_l * volume_lut[ctrl & 0xF] / 64);
|
||||||
|
*(buf++) = (int16_t)((int32_t)cur_r * volume_lut[ctrl & 0xF] / 64);
|
||||||
|
}
|
||||||
|
}
|
17
backends/playback/zsm/x16emu/vera_pcm.h
Normal file
17
backends/playback/zsm/x16emu/vera_pcm.h
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Commander X16 Emulator
|
||||||
|
// Copyright (c) 2020 Frank van den Hoef
|
||||||
|
// All rights reserved. License: 2-clause BSD
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
void pcm_reset(void);
|
||||||
|
void pcm_write_ctrl(uint8_t val);
|
||||||
|
uint8_t pcm_read_ctrl(void);
|
||||||
|
void pcm_write_rate(uint8_t val);
|
||||||
|
uint8_t pcm_read_rate(void);
|
||||||
|
void pcm_write_fifo(uint8_t val);
|
||||||
|
void pcm_render(int16_t *buf, unsigned num_samples);
|
||||||
|
bool pcm_is_fifo_almost_empty(void);
|
126
backends/playback/zsm/x16emu/vera_psg.c
Normal file
126
backends/playback/zsm/x16emu/vera_psg.c
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// Commander X16 Emulator
|
||||||
|
// Copyright (c) 2020 Frank van den Hoef
|
||||||
|
// All rights reserved. License: 2-clause BSD
|
||||||
|
|
||||||
|
#include "vera_psg.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
enum waveform {
|
||||||
|
WF_PULSE = 0,
|
||||||
|
WF_SAWTOOTH,
|
||||||
|
WF_TRIANGLE,
|
||||||
|
WF_NOISE,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct channel {
|
||||||
|
uint16_t freq;
|
||||||
|
uint16_t volume;
|
||||||
|
bool left, right;
|
||||||
|
uint8_t pw;
|
||||||
|
uint8_t waveform;
|
||||||
|
|
||||||
|
uint16_t noiseval;
|
||||||
|
uint32_t phase;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct channel channels[16];
|
||||||
|
|
||||||
|
static uint16_t volume_lut[64] = {
|
||||||
|
0, 4, 8, 12,
|
||||||
|
16, 17, 18, 20, 21, 22, 23, 25, 26, 28, 30, 31,
|
||||||
|
33, 35, 37, 40, 42, 45, 47, 50, 53, 56, 60, 63,
|
||||||
|
67, 71, 75, 80, 85, 90, 95, 101, 107, 113, 120, 127,
|
||||||
|
135, 143, 151, 160, 170, 180, 191, 202, 214, 227, 241, 255,
|
||||||
|
270, 286, 303, 321, 341, 361, 382, 405, 429, 455, 482, 511
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint16_t noise_state;
|
||||||
|
|
||||||
|
void
|
||||||
|
psg_reset(void)
|
||||||
|
{
|
||||||
|
memset(channels, 0, sizeof(channels));
|
||||||
|
noise_state = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
psg_writereg(uint8_t reg, uint8_t val)
|
||||||
|
{
|
||||||
|
reg &= 0x3f;
|
||||||
|
|
||||||
|
int ch = reg / 4;
|
||||||
|
int idx = reg & 3;
|
||||||
|
|
||||||
|
switch (idx) {
|
||||||
|
case 0: channels[ch].freq = (channels[ch].freq & 0xFF00) | val; break;
|
||||||
|
case 1: channels[ch].freq = (channels[ch].freq & 0x00FF) | (val << 8); break;
|
||||||
|
case 2: {
|
||||||
|
channels[ch].right = (val & 0x80) != 0;
|
||||||
|
channels[ch].left = (val & 0x40) != 0;
|
||||||
|
channels[ch].volume = volume_lut[val & 0x3F];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
channels[ch].pw = val & 0x3F;
|
||||||
|
channels[ch].waveform = val >> 6;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
render(int16_t *left, int16_t *right)
|
||||||
|
{
|
||||||
|
int16_t l = 0;
|
||||||
|
int16_t r = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
// In FPGA implementation, noise values are generated every system clock and
|
||||||
|
// the channel update is run sequentially. So, even if both two channels are
|
||||||
|
// fetching a noise value in the same sample, they should have different values
|
||||||
|
noise_state = (noise_state << 1) | (((noise_state >> 1) ^ (noise_state >> 2) ^ (noise_state >> 4) ^ (noise_state >> 15)) & 1);
|
||||||
|
|
||||||
|
struct channel *ch = &channels[i];
|
||||||
|
|
||||||
|
uint32_t new_phase = (ch->left || ch->right) ? ((ch->phase + ch->freq) & 0x1FFFF) : 0;
|
||||||
|
if ((ch->phase & 0x10000) != (new_phase & 0x10000)) {
|
||||||
|
ch->noiseval = (noise_state >> 1) & 0x3F;
|
||||||
|
}
|
||||||
|
ch->phase = new_phase;
|
||||||
|
|
||||||
|
uint32_t v = 0;
|
||||||
|
switch (ch->waveform) {
|
||||||
|
case WF_PULSE: v = ((ch->phase >> 10) > ch->pw) ? 0 : 0x3F; break;
|
||||||
|
case WF_SAWTOOTH: v = (ch->phase >> 11) ^ ((ch->pw ^ 0x3f) & 0x3f); break;
|
||||||
|
case WF_TRIANGLE: v = ((ch->phase & 0x10000) ? (~(ch->phase >> 10) & 0x3F) : ((ch->phase >> 10) & 0x3F)) ^ ((ch->pw ^ 0x3f) & 0x3f); break;
|
||||||
|
case WF_NOISE: v = ch->noiseval; break;
|
||||||
|
}
|
||||||
|
int16_t sv = (v ^ 0x20);
|
||||||
|
if (sv & 0x20) {
|
||||||
|
sv |= 0xFFC0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t val = sv * ch->volume;
|
||||||
|
|
||||||
|
if (ch->left) {
|
||||||
|
l += val >> 3;
|
||||||
|
}
|
||||||
|
if (ch->right) {
|
||||||
|
r += val >> 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*left = l;
|
||||||
|
*right = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
psg_render(int16_t *buf, unsigned num_samples)
|
||||||
|
{
|
||||||
|
while (num_samples--) {
|
||||||
|
render(&buf[0], &buf[1]);
|
||||||
|
buf += 2;
|
||||||
|
}
|
||||||
|
}
|
11
backends/playback/zsm/x16emu/vera_psg.h
Normal file
11
backends/playback/zsm/x16emu/vera_psg.h
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Commander X16 Emulator
|
||||||
|
// Copyright (c) 2020 Frank van den Hoef
|
||||||
|
// All rights reserved. License: 2-clause BSD
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
void psg_reset(void);
|
||||||
|
void psg_writereg(uint8_t reg, uint8_t val);
|
||||||
|
void psg_render(int16_t *buf, unsigned num_samples);
|
29
backends/playback/zsm/x16emu/ymfm/LICENSE
Normal file
29
backends/playback/zsm/x16emu/ymfm/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2021, Aaron Giles
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
575
backends/playback/zsm/x16emu/ymfm/src/ymfm.h
Normal file
575
backends/playback/zsm/x16emu/ymfm/src/ymfm.h
Normal file
|
@ -0,0 +1,575 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#ifndef YMFM_H
|
||||||
|
#define YMFM_H
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
|
||||||
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(__clang__)
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// DEBUGGING
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
class debug
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// masks to help isolate specific channels
|
||||||
|
static constexpr uint32_t GLOBAL_FM_CHANNEL_MASK = 0xffffffff;
|
||||||
|
static constexpr uint32_t GLOBAL_ADPCM_A_CHANNEL_MASK = 0xffffffff;
|
||||||
|
static constexpr uint32_t GLOBAL_ADPCM_B_CHANNEL_MASK = 0xffffffff;
|
||||||
|
static constexpr uint32_t GLOBAL_PCM_CHANNEL_MASK = 0xffffffff;
|
||||||
|
|
||||||
|
// types of logging
|
||||||
|
static constexpr bool LOG_FM_WRITES = false;
|
||||||
|
static constexpr bool LOG_KEYON_EVENTS = false;
|
||||||
|
static constexpr bool LOG_UNEXPECTED_READ_WRITES = false;
|
||||||
|
|
||||||
|
// helpers to write based on the log type
|
||||||
|
template<typename... Params> static void log_fm_write(Params &&... args) { if (LOG_FM_WRITES) log(args...); }
|
||||||
|
template<typename... Params> static void log_keyon(Params &&... args) { if (LOG_KEYON_EVENTS) log(args...); }
|
||||||
|
template<typename... Params> static void log_unexpected_read_write(Params &&... args) { if (LOG_UNEXPECTED_READ_WRITES) log(args...); }
|
||||||
|
|
||||||
|
// downstream helper to output log data; defaults to printf
|
||||||
|
template<typename... Params> static void log(Params &&... args) { printf(args...); }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// GLOBAL HELPERS
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// bitfield - extract a bitfield from the given
|
||||||
|
// value, starting at bit 'start' for a length of
|
||||||
|
// 'length' bits
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
inline uint32_t bitfield(uint32_t value, int start, int length = 1)
|
||||||
|
{
|
||||||
|
return (value >> start) & ((1 << length) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clamp - clamp between the minimum and maximum
|
||||||
|
// values provided
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
inline int32_t clamp(int32_t value, int32_t minval, int32_t maxval)
|
||||||
|
{
|
||||||
|
if (value < minval)
|
||||||
|
return minval;
|
||||||
|
if (value > maxval)
|
||||||
|
return maxval;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// array_size - return the size of an array
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
template<typename ArrayType, int ArraySize>
|
||||||
|
constexpr uint32_t array_size(ArrayType (&array)[ArraySize])
|
||||||
|
{
|
||||||
|
return ArraySize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// count_leading_zeros - return the number of
|
||||||
|
// leading zeros in a 32-bit value; CPU-optimized
|
||||||
|
// versions for various architectures are included
|
||||||
|
// below
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
#if defined(__GNUC__)
|
||||||
|
|
||||||
|
inline uint8_t count_leading_zeros(uint32_t value)
|
||||||
|
{
|
||||||
|
if (value == 0)
|
||||||
|
return 32;
|
||||||
|
return __builtin_clz(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(_MSC_VER)
|
||||||
|
|
||||||
|
inline uint8_t count_leading_zeros(uint32_t value)
|
||||||
|
{
|
||||||
|
unsigned long index;
|
||||||
|
return _BitScanReverse(&index, value) ? uint8_t(31U - index) : 32U;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
inline uint8_t count_leading_zeros(uint32_t value)
|
||||||
|
{
|
||||||
|
if (value == 0)
|
||||||
|
return 32;
|
||||||
|
uint8_t count;
|
||||||
|
for (count = 0; int32_t(value) >= 0; count++)
|
||||||
|
value <<= 1;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// Many of the Yamaha FM chips emit a floating-point value, which is sent to
|
||||||
|
// a DAC for processing. The exact format of this floating-point value is
|
||||||
|
// documented below. This description only makes sense if the "internal"
|
||||||
|
// format treats sign as 1=positive and 0=negative, so the helpers below
|
||||||
|
// presume that.
|
||||||
|
//
|
||||||
|
// Internal OPx data 16-bit signed data Exp Sign Mantissa
|
||||||
|
// ================= ================= === ==== ========
|
||||||
|
// 1 1xxxxxxxx------ -> 0 1xxxxxxxx------ -> 111 1 1xxxxxxx
|
||||||
|
// 1 01xxxxxxxx----- -> 0 01xxxxxxxx----- -> 110 1 1xxxxxxx
|
||||||
|
// 1 001xxxxxxxx---- -> 0 001xxxxxxxx---- -> 101 1 1xxxxxxx
|
||||||
|
// 1 0001xxxxxxxx--- -> 0 0001xxxxxxxx--- -> 100 1 1xxxxxxx
|
||||||
|
// 1 00001xxxxxxxx-- -> 0 00001xxxxxxxx-- -> 011 1 1xxxxxxx
|
||||||
|
// 1 000001xxxxxxxx- -> 0 000001xxxxxxxx- -> 010 1 1xxxxxxx
|
||||||
|
// 1 000000xxxxxxxxx -> 0 000000xxxxxxxxx -> 001 1 xxxxxxxx
|
||||||
|
// 0 111111xxxxxxxxx -> 1 111111xxxxxxxxx -> 001 0 xxxxxxxx
|
||||||
|
// 0 111110xxxxxxxx- -> 1 111110xxxxxxxx- -> 010 0 0xxxxxxx
|
||||||
|
// 0 11110xxxxxxxx-- -> 1 11110xxxxxxxx-- -> 011 0 0xxxxxxx
|
||||||
|
// 0 1110xxxxxxxx--- -> 1 1110xxxxxxxx--- -> 100 0 0xxxxxxx
|
||||||
|
// 0 110xxxxxxxx---- -> 1 110xxxxxxxx---- -> 101 0 0xxxxxxx
|
||||||
|
// 0 10xxxxxxxx----- -> 1 10xxxxxxxx----- -> 110 0 0xxxxxxx
|
||||||
|
// 0 0xxxxxxxx------ -> 1 0xxxxxxxx------ -> 111 0 0xxxxxxx
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// encode_fp - given a 32-bit signed input value
|
||||||
|
// convert it to a signed 3.10 floating-point
|
||||||
|
// value
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
inline int16_t encode_fp(int32_t value)
|
||||||
|
{
|
||||||
|
// handle overflows first
|
||||||
|
if (value < -32768)
|
||||||
|
return (7 << 10) | 0x000;
|
||||||
|
if (value > 32767)
|
||||||
|
return (7 << 10) | 0x3ff;
|
||||||
|
|
||||||
|
// we need to count the number of leading sign bits after the sign
|
||||||
|
// we can use count_leading_zeros if we invert negative values
|
||||||
|
int32_t scanvalue = value ^ (int32_t(value) >> 31);
|
||||||
|
|
||||||
|
// exponent is related to the number of leading bits starting from bit 14
|
||||||
|
int exponent = 7 - count_leading_zeros(scanvalue << 17);
|
||||||
|
|
||||||
|
// smallest exponent value allowed is 1
|
||||||
|
exponent = std::max(exponent, 1);
|
||||||
|
|
||||||
|
// mantissa
|
||||||
|
int32_t mantissa = value >> (exponent - 1);
|
||||||
|
|
||||||
|
// assemble into final form, inverting the sign
|
||||||
|
return ((exponent << 10) | (mantissa & 0x3ff)) ^ 0x200;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// decode_fp - given a 3.10 floating-point value,
|
||||||
|
// convert it to a signed 16-bit value
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
inline int16_t decode_fp(int16_t value)
|
||||||
|
{
|
||||||
|
// invert the sign and the exponent
|
||||||
|
value ^= 0x1e00;
|
||||||
|
|
||||||
|
// shift mantissa up to 16 bits then apply inverted exponent
|
||||||
|
return int16_t(value << 6) >> bitfield(value, 10, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// roundtrip_fp - compute the result of a round
|
||||||
|
// trip through the encode/decode process above
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
inline int16_t roundtrip_fp(int32_t value)
|
||||||
|
{
|
||||||
|
// handle overflows first
|
||||||
|
if (value < -32768)
|
||||||
|
return -32768;
|
||||||
|
if (value > 32767)
|
||||||
|
return 32767;
|
||||||
|
|
||||||
|
// we need to count the number of leading sign bits after the sign
|
||||||
|
// we can use count_leading_zeros if we invert negative values
|
||||||
|
int32_t scanvalue = value ^ (int32_t(value) >> 31);
|
||||||
|
|
||||||
|
// exponent is related to the number of leading bits starting from bit 14
|
||||||
|
int exponent = 7 - count_leading_zeros(scanvalue << 17);
|
||||||
|
|
||||||
|
// smallest exponent value allowed is 1
|
||||||
|
exponent = std::max(exponent, 1);
|
||||||
|
|
||||||
|
// apply the shift back and forth to zero out bits that are lost
|
||||||
|
exponent -= 1;
|
||||||
|
return (value >> exponent) << exponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// HELPER CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// various envelope states
|
||||||
|
enum envelope_state : uint32_t
|
||||||
|
{
|
||||||
|
EG_DEPRESS = 0, // OPLL only; set EG_HAS_DEPRESS to enable
|
||||||
|
EG_ATTACK = 1,
|
||||||
|
EG_DECAY = 2,
|
||||||
|
EG_SUSTAIN = 3,
|
||||||
|
EG_RELEASE = 4,
|
||||||
|
EG_REVERB = 5, // OPQ/OPZ only; set EG_HAS_REVERB to enable
|
||||||
|
EG_STATES = 6
|
||||||
|
};
|
||||||
|
|
||||||
|
// external I/O access classes
|
||||||
|
enum access_class : uint32_t
|
||||||
|
{
|
||||||
|
ACCESS_IO = 0,
|
||||||
|
ACCESS_ADPCM_A,
|
||||||
|
ACCESS_ADPCM_B,
|
||||||
|
ACCESS_PCM,
|
||||||
|
ACCESS_CLASSES
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// HELPER CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> ymfm_output
|
||||||
|
|
||||||
|
// struct containing an array of output values
|
||||||
|
template<int NumOutputs>
|
||||||
|
struct ymfm_output
|
||||||
|
{
|
||||||
|
// clear all outputs to 0
|
||||||
|
ymfm_output &clear()
|
||||||
|
{
|
||||||
|
for (uint32_t index = 0; index < NumOutputs; index++)
|
||||||
|
data[index] = 0;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clamp all outputs to a 16-bit signed value
|
||||||
|
ymfm_output &clamp16()
|
||||||
|
{
|
||||||
|
for (uint32_t index = 0; index < NumOutputs; index++)
|
||||||
|
data[index] = clamp(data[index], -32768, 32767);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// run each output value through the floating-point processor
|
||||||
|
ymfm_output &roundtrip_fp()
|
||||||
|
{
|
||||||
|
for (uint32_t index = 0; index < NumOutputs; index++)
|
||||||
|
data[index] = ymfm::roundtrip_fp(data[index]);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
int32_t data[NumOutputs];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> ymfm_wavfile
|
||||||
|
|
||||||
|
// this class is a debugging helper that accumulates data and writes it to wav files
|
||||||
|
template<int Channels>
|
||||||
|
class ymfm_wavfile
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// construction
|
||||||
|
ymfm_wavfile(uint32_t samplerate = 44100) :
|
||||||
|
m_samplerate(samplerate)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// configuration
|
||||||
|
ymfm_wavfile &set_index(uint32_t index) { m_index = index; return *this; }
|
||||||
|
ymfm_wavfile &set_samplerate(uint32_t samplerate) { m_samplerate = samplerate; return *this; }
|
||||||
|
|
||||||
|
// destruction
|
||||||
|
~ymfm_wavfile()
|
||||||
|
{
|
||||||
|
if (!m_buffer.empty())
|
||||||
|
{
|
||||||
|
// create file
|
||||||
|
char name[20];
|
||||||
|
sprintf(name, "wavlog-%02d.wav", m_index);
|
||||||
|
FILE *out = fopen(name, "wb");
|
||||||
|
|
||||||
|
// make the wav file header
|
||||||
|
uint8_t header[44];
|
||||||
|
memcpy(&header[0], "RIFF", 4);
|
||||||
|
*(uint32_t *)&header[4] = m_buffer.size() * 2 + 44 - 8;
|
||||||
|
memcpy(&header[8], "WAVE", 4);
|
||||||
|
memcpy(&header[12], "fmt ", 4);
|
||||||
|
*(uint32_t *)&header[16] = 16;
|
||||||
|
*(uint16_t *)&header[20] = 1;
|
||||||
|
*(uint16_t *)&header[22] = Channels;
|
||||||
|
*(uint32_t *)&header[24] = m_samplerate;
|
||||||
|
*(uint32_t *)&header[28] = m_samplerate * 2 * Channels;
|
||||||
|
*(uint16_t *)&header[32] = 2 * Channels;
|
||||||
|
*(uint16_t *)&header[34] = 16;
|
||||||
|
memcpy(&header[36], "data", 4);
|
||||||
|
*(uint32_t *)&header[40] = m_buffer.size() * 2 + 44 - 44;
|
||||||
|
|
||||||
|
// write header then data
|
||||||
|
fwrite(&header[0], 1, sizeof(header), out);
|
||||||
|
fwrite(&m_buffer[0], 2, m_buffer.size(), out);
|
||||||
|
fclose(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add data to the file
|
||||||
|
template<int Outputs>
|
||||||
|
void add(ymfm_output<Outputs> output)
|
||||||
|
{
|
||||||
|
int16_t sum[Channels] = { 0 };
|
||||||
|
for (int index = 0; index < Outputs; index++)
|
||||||
|
sum[index % Channels] += output.data[index];
|
||||||
|
for (int index = 0; index < Channels; index++)
|
||||||
|
m_buffer.push_back(sum[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add data to the file, using a reference
|
||||||
|
template<int Outputs>
|
||||||
|
void add(ymfm_output<Outputs> output, ymfm_output<Outputs> const &ref)
|
||||||
|
{
|
||||||
|
int16_t sum[Channels] = { 0 };
|
||||||
|
for (int index = 0; index < Outputs; index++)
|
||||||
|
sum[index % Channels] += output.data[index] - ref.data[index];
|
||||||
|
for (int index = 0; index < Channels; index++)
|
||||||
|
m_buffer.push_back(sum[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
uint32_t m_index;
|
||||||
|
uint32_t m_samplerate;
|
||||||
|
std::vector<int16_t> m_buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> ymfm_saved_state
|
||||||
|
|
||||||
|
// this class contains a managed vector of bytes that is used to save and
|
||||||
|
// restore state
|
||||||
|
class ymfm_saved_state
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// construction
|
||||||
|
ymfm_saved_state(std::vector<uint8_t> &buffer, bool saving) :
|
||||||
|
m_buffer(buffer),
|
||||||
|
m_offset(saving ? -1 : 0)
|
||||||
|
{
|
||||||
|
if (saving)
|
||||||
|
buffer.resize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// are we saving or restoring?
|
||||||
|
bool saving() const { return (m_offset < 0); }
|
||||||
|
|
||||||
|
// generic save/restore
|
||||||
|
template<typename DataType>
|
||||||
|
void save_restore(DataType &data)
|
||||||
|
{
|
||||||
|
if (saving())
|
||||||
|
save(data);
|
||||||
|
else
|
||||||
|
restore(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// save data to the buffer
|
||||||
|
void save(bool &data) { write(data ? 1 : 0); }
|
||||||
|
void save(int8_t &data) { write(data); }
|
||||||
|
void save(uint8_t &data) { write(data); }
|
||||||
|
void save(int16_t &data) { write(uint8_t(data)).write(data >> 8); }
|
||||||
|
void save(uint16_t &data) { write(uint8_t(data)).write(data >> 8); }
|
||||||
|
void save(int32_t &data) { write(data).write(data >> 8).write(data >> 16).write(data >> 24); }
|
||||||
|
void save(uint32_t &data) { write(data).write(data >> 8).write(data >> 16).write(data >> 24); }
|
||||||
|
void save(envelope_state &data) { write(uint8_t(data)); }
|
||||||
|
template<typename DataType, int Count>
|
||||||
|
void save(DataType (&data)[Count]) { for (uint32_t index = 0; index < Count; index++) save(data[index]); }
|
||||||
|
|
||||||
|
// restore data from the buffer
|
||||||
|
void restore(bool &data) { data = read() ? true : false; }
|
||||||
|
void restore(int8_t &data) { data = read(); }
|
||||||
|
void restore(uint8_t &data) { data = read(); }
|
||||||
|
void restore(int16_t &data) { data = read(); data |= read() << 8; }
|
||||||
|
void restore(uint16_t &data) { data = read(); data |= read() << 8; }
|
||||||
|
void restore(int32_t &data) { data = read(); data |= read() << 8; data |= read() << 16; data |= read() << 24; }
|
||||||
|
void restore(uint32_t &data) { data = read(); data |= read() << 8; data |= read() << 16; data |= read() << 24; }
|
||||||
|
void restore(envelope_state &data) { data = envelope_state(read()); }
|
||||||
|
template<typename DataType, int Count>
|
||||||
|
void restore(DataType (&data)[Count]) { for (uint32_t index = 0; index < Count; index++) restore(data[index]); }
|
||||||
|
|
||||||
|
// internal helper
|
||||||
|
ymfm_saved_state &write(uint8_t data) { m_buffer.push_back(data); return *this; }
|
||||||
|
uint8_t read() { return (m_offset < int32_t(m_buffer.size())) ? m_buffer[m_offset++] : 0; }
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
std::vector<uint8_t> &m_buffer;
|
||||||
|
int32_t m_offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// INTERFACE CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> ymfm_engine_callbacks
|
||||||
|
|
||||||
|
// this class represents functions in the engine that the ymfm_interface
|
||||||
|
// needs to be able to call; it is represented here as a separate interface
|
||||||
|
// that is independent of the actual engine implementation
|
||||||
|
class ymfm_engine_callbacks
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// timer callback; called by the interface when a timer fires
|
||||||
|
virtual void engine_timer_expired(uint32_t tnum) = 0;
|
||||||
|
|
||||||
|
// check interrupts; called by the interface after synchronization
|
||||||
|
virtual void engine_check_interrupts() = 0;
|
||||||
|
|
||||||
|
// mode register write; called by the interface after synchronization
|
||||||
|
virtual void engine_mode_write(uint8_t data) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> ymfm_interface
|
||||||
|
|
||||||
|
// this class represents the interface between the fm_engine and the outside
|
||||||
|
// world; it provides hooks for timers, synchronization, and I/O
|
||||||
|
class ymfm_interface
|
||||||
|
{
|
||||||
|
// the engine is our friend
|
||||||
|
template<typename RegisterType> friend class fm_engine_base;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// the following functions must be implemented by any derived classes; the
|
||||||
|
// default implementations are sufficient for some minimal operation, but will
|
||||||
|
// likely need to be overridden to integrate with the outside world; they are
|
||||||
|
// all prefixed with ymfm_ to reduce the likelihood of namespace collisions
|
||||||
|
|
||||||
|
//
|
||||||
|
// timing and synchronizaton
|
||||||
|
//
|
||||||
|
|
||||||
|
// the chip implementation calls this when a write happens to the mode
|
||||||
|
// register, which could affect timers and interrupts; our responsibility
|
||||||
|
// is to ensure the system is up to date before calling the engine's
|
||||||
|
// engine_mode_write() method
|
||||||
|
virtual void ymfm_sync_mode_write(uint8_t data) { m_engine->engine_mode_write(data); }
|
||||||
|
|
||||||
|
// the chip implementation calls this when the chip's status has changed,
|
||||||
|
// which may affect the interrupt state; our responsibility is to ensure
|
||||||
|
// the system is up to date before calling the engine's
|
||||||
|
// engine_check_interrupts() method
|
||||||
|
virtual void ymfm_sync_check_interrupts() { m_engine->engine_check_interrupts(); }
|
||||||
|
|
||||||
|
// the chip implementation calls this when one of the two internal timers
|
||||||
|
// has changed state; our responsibility is to arrange to call the engine's
|
||||||
|
// engine_timer_expired() method after the provided number of clocks; if
|
||||||
|
// duration_in_clocks is negative, we should cancel any outstanding timers
|
||||||
|
virtual void ymfm_set_timer(uint32_t tnum, int32_t duration_in_clocks) { }
|
||||||
|
|
||||||
|
// the chip implementation calls this to indicate that the chip should be
|
||||||
|
// considered in a busy state until the given number of clocks has passed;
|
||||||
|
// our responsibility is to compute and remember the ending time based on
|
||||||
|
// the chip's clock for later checking
|
||||||
|
virtual void ymfm_set_busy_end(uint32_t clocks) { }
|
||||||
|
|
||||||
|
// the chip implementation calls this to see if the chip is still currently
|
||||||
|
// is a busy state, as specified by a previous call to ymfm_set_busy_end();
|
||||||
|
// our responsibility is to compare the current time against the previously
|
||||||
|
// noted busy end time and return true if we haven't yet passed it
|
||||||
|
virtual bool ymfm_is_busy() { return false; }
|
||||||
|
|
||||||
|
//
|
||||||
|
// I/O functions
|
||||||
|
//
|
||||||
|
|
||||||
|
// the chip implementation calls this when the state of the IRQ signal has
|
||||||
|
// changed due to a status change; our responsibility is to respond as
|
||||||
|
// needed to the change in IRQ state, signaling any consumers
|
||||||
|
virtual void ymfm_update_irq(bool asserted) { }
|
||||||
|
|
||||||
|
// the chip implementation calls this whenever data is read from outside
|
||||||
|
// of the chip; our responsibility is to provide the data requested
|
||||||
|
virtual uint8_t ymfm_external_read(access_class type, uint32_t address) { return 0; }
|
||||||
|
|
||||||
|
// the chip implementation calls this whenever data is written outside
|
||||||
|
// of the chip; our responsibility is to pass the written data on to any consumers
|
||||||
|
virtual void ymfm_external_write(access_class type, uint32_t address, uint8_t data) { }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// pointer to engine callbacks -- this is set directly by the engine at
|
||||||
|
// construction time
|
||||||
|
ymfm_engine_callbacks *m_engine;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // YMFM_H
|
807
backends/playback/zsm/x16emu/ymfm/src/ymfm_adpcm.cpp
Normal file
807
backends/playback/zsm/x16emu/ymfm/src/ymfm_adpcm.cpp
Normal file
|
@ -0,0 +1,807 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#include "ymfm_adpcm.h"
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// ADPCM "A" REGISTERS
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the register state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_a_registers::reset()
|
||||||
|
{
|
||||||
|
std::fill_n(&m_regdata[0], REGISTERS, 0);
|
||||||
|
|
||||||
|
// initialize the pans to on by default, and max instrument volume;
|
||||||
|
// some neogeo homebrews (for example ffeast) rely on this
|
||||||
|
m_regdata[0x08] = m_regdata[0x09] = m_regdata[0x0a] =
|
||||||
|
m_regdata[0x0b] = m_regdata[0x0c] = m_regdata[0x0d] = 0xdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_a_registers::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
state.save_restore(m_regdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// ADPCM "A" CHANNEL
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// adpcm_a_channel - constructor
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
adpcm_a_channel::adpcm_a_channel(adpcm_a_engine &owner, uint32_t choffs, uint32_t addrshift) :
|
||||||
|
m_choffs(choffs),
|
||||||
|
m_address_shift(addrshift),
|
||||||
|
m_playing(0),
|
||||||
|
m_curnibble(0),
|
||||||
|
m_curbyte(0),
|
||||||
|
m_curaddress(0),
|
||||||
|
m_accumulator(0),
|
||||||
|
m_step_index(0),
|
||||||
|
m_regs(owner.regs()),
|
||||||
|
m_owner(owner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the channel state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_a_channel::reset()
|
||||||
|
{
|
||||||
|
m_playing = 0;
|
||||||
|
m_curnibble = 0;
|
||||||
|
m_curbyte = 0;
|
||||||
|
m_curaddress = 0;
|
||||||
|
m_accumulator = 0;
|
||||||
|
m_step_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_a_channel::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
state.save_restore(m_playing);
|
||||||
|
state.save_restore(m_curnibble);
|
||||||
|
state.save_restore(m_curbyte);
|
||||||
|
state.save_restore(m_curaddress);
|
||||||
|
state.save_restore(m_accumulator);
|
||||||
|
state.save_restore(m_step_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// keyonoff - signal key on/off
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_a_channel::keyonoff(bool on)
|
||||||
|
{
|
||||||
|
// QUESTION: repeated key ons restart the sample?
|
||||||
|
m_playing = on;
|
||||||
|
if (m_playing)
|
||||||
|
{
|
||||||
|
m_curaddress = m_regs.ch_start(m_choffs) << m_address_shift;
|
||||||
|
m_curnibble = 0;
|
||||||
|
m_curbyte = 0;
|
||||||
|
m_accumulator = 0;
|
||||||
|
m_step_index = 0;
|
||||||
|
|
||||||
|
// don't log masked channels
|
||||||
|
if (((debug::GLOBAL_ADPCM_A_CHANNEL_MASK >> m_choffs) & 1) != 0)
|
||||||
|
debug::log_keyon("KeyOn ADPCM-A%d: pan=%d%d start=%04X end=%04X level=%02X\n",
|
||||||
|
m_choffs,
|
||||||
|
m_regs.ch_pan_left(m_choffs),
|
||||||
|
m_regs.ch_pan_right(m_choffs),
|
||||||
|
m_regs.ch_start(m_choffs),
|
||||||
|
m_regs.ch_end(m_choffs),
|
||||||
|
m_regs.ch_instrument_level(m_choffs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clock - master clocking function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
bool adpcm_a_channel::clock()
|
||||||
|
{
|
||||||
|
// if not playing, just output 0
|
||||||
|
if (m_playing == 0)
|
||||||
|
{
|
||||||
|
m_accumulator = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're about to read nibble 0, fetch the data
|
||||||
|
uint8_t data;
|
||||||
|
if (m_curnibble == 0)
|
||||||
|
{
|
||||||
|
// stop when we hit the end address; apparently only low 20 bits are used for
|
||||||
|
// comparison on the YM2610: this affects sample playback in some games, for
|
||||||
|
// example twinspri character select screen music will skip some samples if
|
||||||
|
// this is not correct
|
||||||
|
//
|
||||||
|
// note also: end address is inclusive, so wait until we are about to fetch
|
||||||
|
// the sample just after the end before stopping; this is needed for nitd's
|
||||||
|
// jump sound, for example
|
||||||
|
uint32_t end = (m_regs.ch_end(m_choffs) + 1) << m_address_shift;
|
||||||
|
if (((m_curaddress ^ end) & 0xfffff) == 0)
|
||||||
|
{
|
||||||
|
m_playing = m_accumulator = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_curbyte = m_owner.intf().ymfm_external_read(ACCESS_ADPCM_A, m_curaddress++);
|
||||||
|
data = m_curbyte >> 4;
|
||||||
|
m_curnibble = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise just extract from the previosuly-fetched byte
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = m_curbyte & 0xf;
|
||||||
|
m_curnibble = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute the ADPCM delta
|
||||||
|
static uint16_t const s_steps[49] =
|
||||||
|
{
|
||||||
|
16, 17, 19, 21, 23, 25, 28,
|
||||||
|
31, 34, 37, 41, 45, 50, 55,
|
||||||
|
60, 66, 73, 80, 88, 97, 107,
|
||||||
|
118, 130, 143, 157, 173, 190, 209,
|
||||||
|
230, 253, 279, 307, 337, 371, 408,
|
||||||
|
449, 494, 544, 598, 658, 724, 796,
|
||||||
|
876, 963, 1060, 1166, 1282, 1411, 1552
|
||||||
|
};
|
||||||
|
int32_t delta = (2 * bitfield(data, 0, 3) + 1) * s_steps[m_step_index] / 8;
|
||||||
|
if (bitfield(data, 3))
|
||||||
|
delta = -delta;
|
||||||
|
|
||||||
|
// the 12-bit accumulator wraps on the ym2610 and ym2608 (like the msm5205)
|
||||||
|
m_accumulator = (m_accumulator + delta) & 0xfff;
|
||||||
|
|
||||||
|
// adjust ADPCM step
|
||||||
|
static int8_t const s_step_inc[8] = { -1, -1, -1, -1, 2, 5, 7, 9 };
|
||||||
|
m_step_index = clamp(m_step_index + s_step_inc[bitfield(data, 0, 3)], 0, 48);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// output - return the computed output value, with
|
||||||
|
// panning applied
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
template<int NumOutputs>
|
||||||
|
void adpcm_a_channel::output(ymfm_output<NumOutputs> &output) const
|
||||||
|
{
|
||||||
|
// volume combines instrument and total levels
|
||||||
|
int vol = (m_regs.ch_instrument_level(m_choffs) ^ 0x1f) + (m_regs.total_level() ^ 0x3f);
|
||||||
|
|
||||||
|
// if combined is maximum, don't add to outputs
|
||||||
|
if (vol >= 63)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// convert into a shift and a multiplier
|
||||||
|
// QUESTION: verify this from other sources
|
||||||
|
int8_t mul = 15 - (vol & 7);
|
||||||
|
uint8_t shift = 4 + 1 + (vol >> 3);
|
||||||
|
|
||||||
|
// m_accumulator is a 12-bit value; shift up to sign-extend;
|
||||||
|
// the downshift is incorporated into 'shift'
|
||||||
|
int16_t value = ((int16_t(m_accumulator << 4) * mul) >> shift) & ~3;
|
||||||
|
|
||||||
|
// apply to left/right as appropriate
|
||||||
|
if (NumOutputs == 1 || m_regs.ch_pan_left(m_choffs))
|
||||||
|
output.data[0] += value;
|
||||||
|
if (NumOutputs > 1 && m_regs.ch_pan_right(m_choffs))
|
||||||
|
output.data[1] += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// ADPCM "A" ENGINE
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// adpcm_a_engine - constructor
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
adpcm_a_engine::adpcm_a_engine(ymfm_interface &intf, uint32_t addrshift) :
|
||||||
|
m_intf(intf)
|
||||||
|
{
|
||||||
|
// create the channels
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
m_channel[chnum] = std::make_unique<adpcm_a_channel>(*this, chnum, addrshift);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the engine state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_a_engine::reset()
|
||||||
|
{
|
||||||
|
// reset register state
|
||||||
|
m_regs.reset();
|
||||||
|
|
||||||
|
// reset each channel
|
||||||
|
for (auto &chan : m_channel)
|
||||||
|
chan->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_a_engine::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
// save register state
|
||||||
|
m_regs.save_restore(state);
|
||||||
|
|
||||||
|
// save channel state
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
m_channel[chnum]->save_restore(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clock - master clocking function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint32_t adpcm_a_engine::clock(uint32_t chanmask)
|
||||||
|
{
|
||||||
|
// clock each channel, setting a bit in result if it finished
|
||||||
|
uint32_t result = 0;
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
if (bitfield(chanmask, chnum))
|
||||||
|
if (m_channel[chnum]->clock())
|
||||||
|
result |= 1 << chnum;
|
||||||
|
|
||||||
|
// return the bitmask of completed samples
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// update - master update function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
template<int NumOutputs>
|
||||||
|
void adpcm_a_engine::output(ymfm_output<NumOutputs> &output, uint32_t chanmask)
|
||||||
|
{
|
||||||
|
// mask out some channels for debug purposes
|
||||||
|
chanmask &= debug::GLOBAL_ADPCM_A_CHANNEL_MASK;
|
||||||
|
|
||||||
|
// compute the output of each channel
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
if (bitfield(chanmask, chnum))
|
||||||
|
m_channel[chnum]->output(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
template void adpcm_a_engine::output<1>(ymfm_output<1> &output, uint32_t chanmask);
|
||||||
|
template void adpcm_a_engine::output<2>(ymfm_output<2> &output, uint32_t chanmask);
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// write - handle writes to the ADPCM-A registers
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_a_engine::write(uint32_t regnum, uint8_t data)
|
||||||
|
{
|
||||||
|
// store the raw value to the register array;
|
||||||
|
// most writes are passive, consumed only when needed
|
||||||
|
m_regs.write(regnum, data);
|
||||||
|
|
||||||
|
// actively handle writes to the control register
|
||||||
|
if (regnum == 0x00)
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
if (bitfield(data, chnum))
|
||||||
|
m_channel[chnum]->keyonoff(bitfield(~data, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// ADPCM "B" REGISTERS
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the register state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_registers::reset()
|
||||||
|
{
|
||||||
|
std::fill_n(&m_regdata[0], REGISTERS, 0);
|
||||||
|
|
||||||
|
// default limit to wide open
|
||||||
|
m_regdata[0x0c] = m_regdata[0x0d] = 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_registers::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
state.save_restore(m_regdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// ADPCM "B" CHANNEL
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// adpcm_b_channel - constructor
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
adpcm_b_channel::adpcm_b_channel(adpcm_b_engine &owner, uint32_t addrshift) :
|
||||||
|
m_address_shift(addrshift),
|
||||||
|
m_status(STATUS_BRDY),
|
||||||
|
m_curnibble(0),
|
||||||
|
m_curbyte(0),
|
||||||
|
m_dummy_read(0),
|
||||||
|
m_position(0),
|
||||||
|
m_curaddress(0),
|
||||||
|
m_accumulator(0),
|
||||||
|
m_prev_accum(0),
|
||||||
|
m_adpcm_step(STEP_MIN),
|
||||||
|
m_regs(owner.regs()),
|
||||||
|
m_owner(owner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the channel state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_channel::reset()
|
||||||
|
{
|
||||||
|
m_status = STATUS_BRDY;
|
||||||
|
m_curnibble = 0;
|
||||||
|
m_curbyte = 0;
|
||||||
|
m_dummy_read = 0;
|
||||||
|
m_position = 0;
|
||||||
|
m_curaddress = 0;
|
||||||
|
m_accumulator = 0;
|
||||||
|
m_prev_accum = 0;
|
||||||
|
m_adpcm_step = STEP_MIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_channel::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
state.save_restore(m_status);
|
||||||
|
state.save_restore(m_curnibble);
|
||||||
|
state.save_restore(m_curbyte);
|
||||||
|
state.save_restore(m_dummy_read);
|
||||||
|
state.save_restore(m_position);
|
||||||
|
state.save_restore(m_curaddress);
|
||||||
|
state.save_restore(m_accumulator);
|
||||||
|
state.save_restore(m_prev_accum);
|
||||||
|
state.save_restore(m_adpcm_step);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clock - master clocking function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_channel::clock()
|
||||||
|
{
|
||||||
|
// only process if active and not recording (which we don't support)
|
||||||
|
if (!m_regs.execute() || m_regs.record() || (m_status & STATUS_PLAYING) == 0)
|
||||||
|
{
|
||||||
|
m_status &= ~STATUS_PLAYING;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, advance the step
|
||||||
|
uint32_t position = m_position + m_regs.delta_n();
|
||||||
|
m_position = uint16_t(position);
|
||||||
|
if (position < 0x10000)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// if we're about to process nibble 0, fetch sample
|
||||||
|
if (m_curnibble == 0)
|
||||||
|
{
|
||||||
|
// playing from RAM/ROM
|
||||||
|
if (m_regs.external())
|
||||||
|
m_curbyte = m_owner.intf().ymfm_external_read(ACCESS_ADPCM_B, m_curaddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the nibble from our current byte
|
||||||
|
uint8_t data = uint8_t(m_curbyte << (4 * m_curnibble)) >> 4;
|
||||||
|
m_curnibble ^= 1;
|
||||||
|
|
||||||
|
// we just processed the last nibble
|
||||||
|
if (m_curnibble == 0)
|
||||||
|
{
|
||||||
|
// if playing from RAM/ROM, check the end/limit address or advance
|
||||||
|
if (m_regs.external())
|
||||||
|
{
|
||||||
|
// handle the sample end, either repeating or stopping
|
||||||
|
if (at_end())
|
||||||
|
{
|
||||||
|
// if repeating, go back to the start
|
||||||
|
if (m_regs.repeat())
|
||||||
|
load_start();
|
||||||
|
|
||||||
|
// otherwise, done; set the EOS bit
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_accumulator = 0;
|
||||||
|
m_prev_accum = 0;
|
||||||
|
m_status = (m_status & ~STATUS_PLAYING) | STATUS_EOS;
|
||||||
|
debug::log_keyon("%s\n", "ADPCM EOS");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap at the limit address
|
||||||
|
else if (at_limit())
|
||||||
|
m_curaddress = 0;
|
||||||
|
|
||||||
|
// otherwise, advance the current address
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_curaddress++;
|
||||||
|
m_curaddress &= 0xffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if CPU-driven, copy the next byte and request more
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_curbyte = m_regs.cpudata();
|
||||||
|
m_status |= STATUS_BRDY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remember previous value for interpolation
|
||||||
|
m_prev_accum = m_accumulator;
|
||||||
|
|
||||||
|
// forecast to next forecast: 1/8, 3/8, 5/8, 7/8, 9/8, 11/8, 13/8, 15/8
|
||||||
|
int32_t delta = (2 * bitfield(data, 0, 3) + 1) * m_adpcm_step / 8;
|
||||||
|
if (bitfield(data, 3))
|
||||||
|
delta = -delta;
|
||||||
|
|
||||||
|
// add and clamp to 16 bits
|
||||||
|
m_accumulator = clamp(m_accumulator + delta, -32768, 32767);
|
||||||
|
|
||||||
|
// scale the ADPCM step: 0.9, 0.9, 0.9, 0.9, 1.2, 1.6, 2.0, 2.4
|
||||||
|
static uint8_t const s_step_scale[8] = { 57, 57, 57, 57, 77, 102, 128, 153 };
|
||||||
|
m_adpcm_step = clamp((m_adpcm_step * s_step_scale[bitfield(data, 0, 3)]) / 64, STEP_MIN, STEP_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// output - return the computed output value, with
|
||||||
|
// panning applied
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
template<int NumOutputs>
|
||||||
|
void adpcm_b_channel::output(ymfm_output<NumOutputs> &output, uint32_t rshift) const
|
||||||
|
{
|
||||||
|
// mask out some channels for debug purposes
|
||||||
|
if ((debug::GLOBAL_ADPCM_B_CHANNEL_MASK & 1) == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// do a linear interpolation between samples
|
||||||
|
int32_t result = (m_prev_accum * int32_t((m_position ^ 0xffff) + 1) + m_accumulator * int32_t(m_position)) >> 16;
|
||||||
|
|
||||||
|
// apply volume (level) in a linear fashion and reduce
|
||||||
|
result = (result * int32_t(m_regs.level())) >> (8 + rshift);
|
||||||
|
|
||||||
|
// apply to left/right
|
||||||
|
if (NumOutputs == 1 || m_regs.pan_left())
|
||||||
|
output.data[0] += result;
|
||||||
|
if (NumOutputs > 1 && m_regs.pan_right())
|
||||||
|
output.data[1] += result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// read - handle special register reads
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint8_t adpcm_b_channel::read(uint32_t regnum)
|
||||||
|
{
|
||||||
|
uint8_t result = 0;
|
||||||
|
|
||||||
|
// register 8 reads over the bus under some conditions
|
||||||
|
if (regnum == 0x08 && !m_regs.execute() && !m_regs.record() && m_regs.external())
|
||||||
|
{
|
||||||
|
// two dummy reads are consumed first
|
||||||
|
if (m_dummy_read != 0)
|
||||||
|
{
|
||||||
|
load_start();
|
||||||
|
m_dummy_read--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the data
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// read from outside of the chip
|
||||||
|
result = m_owner.intf().ymfm_external_read(ACCESS_ADPCM_B, m_curaddress++);
|
||||||
|
|
||||||
|
// did we hit the end? if so, signal EOS
|
||||||
|
if (at_end())
|
||||||
|
{
|
||||||
|
m_status = STATUS_EOS | STATUS_BRDY;
|
||||||
|
debug::log_keyon("%s\n", "ADPCM EOS");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// signal ready
|
||||||
|
m_status = STATUS_BRDY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap at the limit address
|
||||||
|
if (at_limit())
|
||||||
|
m_curaddress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// write - handle special register writes
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_channel::write(uint32_t regnum, uint8_t value)
|
||||||
|
{
|
||||||
|
// register 0 can do a reset; also use writes here to reset the
|
||||||
|
// dummy read counter
|
||||||
|
if (regnum == 0x00)
|
||||||
|
{
|
||||||
|
if (m_regs.execute())
|
||||||
|
{
|
||||||
|
load_start();
|
||||||
|
|
||||||
|
// don't log masked channels
|
||||||
|
if ((debug::GLOBAL_ADPCM_B_CHANNEL_MASK & 1) != 0)
|
||||||
|
debug::log_keyon("KeyOn ADPCM-B: rep=%d spk=%d pan=%d%d dac=%d 8b=%d rom=%d ext=%d rec=%d start=%04X end=%04X pre=%04X dn=%04X lvl=%02X lim=%04X\n",
|
||||||
|
m_regs.repeat(),
|
||||||
|
m_regs.speaker(),
|
||||||
|
m_regs.pan_left(),
|
||||||
|
m_regs.pan_right(),
|
||||||
|
m_regs.dac_enable(),
|
||||||
|
m_regs.dram_8bit(),
|
||||||
|
m_regs.rom_ram(),
|
||||||
|
m_regs.external(),
|
||||||
|
m_regs.record(),
|
||||||
|
m_regs.start(),
|
||||||
|
m_regs.end(),
|
||||||
|
m_regs.prescale(),
|
||||||
|
m_regs.delta_n(),
|
||||||
|
m_regs.level(),
|
||||||
|
m_regs.limit());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
m_status &= ~STATUS_EOS;
|
||||||
|
if (m_regs.resetflag())
|
||||||
|
reset();
|
||||||
|
if (m_regs.external())
|
||||||
|
m_dummy_read = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// register 8 writes over the bus under some conditions
|
||||||
|
else if (regnum == 0x08)
|
||||||
|
{
|
||||||
|
// if writing from the CPU during execute, clear the ready flag
|
||||||
|
if (m_regs.execute() && !m_regs.record() && !m_regs.external())
|
||||||
|
m_status &= ~STATUS_BRDY;
|
||||||
|
|
||||||
|
// if writing during "record", pass through as data
|
||||||
|
else if (!m_regs.execute() && m_regs.record() && m_regs.external())
|
||||||
|
{
|
||||||
|
// clear out dummy reads and set start address
|
||||||
|
if (m_dummy_read != 0)
|
||||||
|
{
|
||||||
|
load_start();
|
||||||
|
m_dummy_read = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// did we hit the end? if so, signal EOS
|
||||||
|
if (at_end())
|
||||||
|
{
|
||||||
|
debug::log_keyon("%s\n", "ADPCM EOS");
|
||||||
|
m_status = STATUS_EOS | STATUS_BRDY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, write the data and signal ready
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_owner.intf().ymfm_external_write(ACCESS_ADPCM_B, m_curaddress++, value);
|
||||||
|
m_status = STATUS_BRDY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// address_shift - compute the current address
|
||||||
|
// shift amount based on register settings
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint32_t adpcm_b_channel::address_shift() const
|
||||||
|
{
|
||||||
|
// if a constant address shift, just provide that
|
||||||
|
if (m_address_shift != 0)
|
||||||
|
return m_address_shift;
|
||||||
|
|
||||||
|
// if ROM or 8-bit DRAM, shift is 5 bits
|
||||||
|
if (m_regs.rom_ram())
|
||||||
|
return 5;
|
||||||
|
if (m_regs.dram_8bit())
|
||||||
|
return 5;
|
||||||
|
|
||||||
|
// otherwise, shift is 2 bits
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// load_start - load the start address and
|
||||||
|
// initialize the state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_channel::load_start()
|
||||||
|
{
|
||||||
|
m_status = (m_status & ~STATUS_EOS) | STATUS_PLAYING;
|
||||||
|
m_curaddress = m_regs.external() ? (m_regs.start() << address_shift()) : 0;
|
||||||
|
m_curnibble = 0;
|
||||||
|
m_curbyte = 0;
|
||||||
|
m_position = 0;
|
||||||
|
m_accumulator = 0;
|
||||||
|
m_prev_accum = 0;
|
||||||
|
m_adpcm_step = STEP_MIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// ADPCM "B" ENGINE
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// adpcm_b_engine - constructor
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
adpcm_b_engine::adpcm_b_engine(ymfm_interface &intf, uint32_t addrshift) :
|
||||||
|
m_intf(intf)
|
||||||
|
{
|
||||||
|
// create the channel (only one supported for now, but leaving possibilities open)
|
||||||
|
m_channel = std::make_unique<adpcm_b_channel>(*this, addrshift);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the engine state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_engine::reset()
|
||||||
|
{
|
||||||
|
// reset registers
|
||||||
|
m_regs.reset();
|
||||||
|
|
||||||
|
// reset each channel
|
||||||
|
m_channel->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_engine::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
// save our state
|
||||||
|
m_regs.save_restore(state);
|
||||||
|
|
||||||
|
// save channel state
|
||||||
|
m_channel->save_restore(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clock - master clocking function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_engine::clock()
|
||||||
|
{
|
||||||
|
// clock each channel, setting a bit in result if it finished
|
||||||
|
m_channel->clock();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// output - master output function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
template<int NumOutputs>
|
||||||
|
void adpcm_b_engine::output(ymfm_output<NumOutputs> &output, uint32_t rshift)
|
||||||
|
{
|
||||||
|
// compute the output of each channel
|
||||||
|
m_channel->output(output, rshift);
|
||||||
|
}
|
||||||
|
|
||||||
|
template void adpcm_b_engine::output<1>(ymfm_output<1> &output, uint32_t rshift);
|
||||||
|
template void adpcm_b_engine::output<2>(ymfm_output<2> &output, uint32_t rshift);
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// write - handle writes to the ADPCM-B registers
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void adpcm_b_engine::write(uint32_t regnum, uint8_t data)
|
||||||
|
{
|
||||||
|
// store the raw value to the register array;
|
||||||
|
// most writes are passive, consumed only when needed
|
||||||
|
m_regs.write(regnum, data);
|
||||||
|
|
||||||
|
// let the channel handle any special writes
|
||||||
|
m_channel->write(regnum, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
411
backends/playback/zsm/x16emu/ymfm/src/ymfm_adpcm.h
Normal file
411
backends/playback/zsm/x16emu/ymfm/src/ymfm_adpcm.h
Normal file
|
@ -0,0 +1,411 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#ifndef YMFM_ADPCM_H
|
||||||
|
#define YMFM_ADPCM_H
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ymfm.h"
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// INTERFACE CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// forward declarations
|
||||||
|
class adpcm_a_engine;
|
||||||
|
class adpcm_b_engine;
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> adpcm_a_registers
|
||||||
|
|
||||||
|
//
|
||||||
|
// ADPCM-A register map:
|
||||||
|
//
|
||||||
|
// System-wide registers:
|
||||||
|
// 00 x------- Dump (disable=1) or keyon (0) control
|
||||||
|
// --xxxxxx Mask of channels to dump or keyon
|
||||||
|
// 01 --xxxxxx Total level
|
||||||
|
// 02 xxxxxxxx Test register
|
||||||
|
// 08-0D x------- Pan left
|
||||||
|
// -x------ Pan right
|
||||||
|
// ---xxxxx Instrument level
|
||||||
|
// 10-15 xxxxxxxx Start address (low)
|
||||||
|
// 18-1D xxxxxxxx Start address (high)
|
||||||
|
// 20-25 xxxxxxxx End address (low)
|
||||||
|
// 28-2D xxxxxxxx End address (high)
|
||||||
|
//
|
||||||
|
class adpcm_a_registers
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constants
|
||||||
|
static constexpr uint32_t OUTPUTS = 2;
|
||||||
|
static constexpr uint32_t CHANNELS = 6;
|
||||||
|
static constexpr uint32_t REGISTERS = 0x30;
|
||||||
|
static constexpr uint32_t ALL_CHANNELS = (1 << CHANNELS) - 1;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
adpcm_a_registers() { }
|
||||||
|
|
||||||
|
// reset to initial state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// map channel number to register offset
|
||||||
|
static constexpr uint32_t channel_offset(uint32_t chnum)
|
||||||
|
{
|
||||||
|
assert(chnum < CHANNELS);
|
||||||
|
return chnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// direct read/write access
|
||||||
|
void write(uint32_t index, uint8_t data) { m_regdata[index] = data; }
|
||||||
|
|
||||||
|
// system-wide registers
|
||||||
|
uint32_t dump() const { return bitfield(m_regdata[0x00], 7); }
|
||||||
|
uint32_t dump_mask() const { return bitfield(m_regdata[0x00], 0, 6); }
|
||||||
|
uint32_t total_level() const { return bitfield(m_regdata[0x01], 0, 6); }
|
||||||
|
uint32_t test() const { return m_regdata[0x02]; }
|
||||||
|
|
||||||
|
// per-channel registers
|
||||||
|
uint32_t ch_pan_left(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x08], 7); }
|
||||||
|
uint32_t ch_pan_right(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x08], 6); }
|
||||||
|
uint32_t ch_instrument_level(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x08], 0, 5); }
|
||||||
|
uint32_t ch_start(uint32_t choffs) const { return m_regdata[choffs + 0x10] | (m_regdata[choffs + 0x18] << 8); }
|
||||||
|
uint32_t ch_end(uint32_t choffs) const { return m_regdata[choffs + 0x20] | (m_regdata[choffs + 0x28] << 8); }
|
||||||
|
|
||||||
|
// per-channel writes
|
||||||
|
void write_start(uint32_t choffs, uint32_t address)
|
||||||
|
{
|
||||||
|
write(choffs + 0x10, address);
|
||||||
|
write(choffs + 0x18, address >> 8);
|
||||||
|
}
|
||||||
|
void write_end(uint32_t choffs, uint32_t address)
|
||||||
|
{
|
||||||
|
write(choffs + 0x20, address);
|
||||||
|
write(choffs + 0x28, address >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
uint8_t m_regdata[REGISTERS]; // register data
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> adpcm_a_channel
|
||||||
|
|
||||||
|
class adpcm_a_channel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
adpcm_a_channel(adpcm_a_engine &owner, uint32_t choffs, uint32_t addrshift);
|
||||||
|
|
||||||
|
// reset the channel state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// signal key on/off
|
||||||
|
void keyonoff(bool on);
|
||||||
|
|
||||||
|
// master clockingfunction
|
||||||
|
bool clock();
|
||||||
|
|
||||||
|
// return the computed output value, with panning applied
|
||||||
|
template<int NumOutputs>
|
||||||
|
void output(ymfm_output<NumOutputs> &output) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
uint32_t const m_choffs; // channel offset
|
||||||
|
uint32_t const m_address_shift; // address bits shift-left
|
||||||
|
uint32_t m_playing; // currently playing?
|
||||||
|
uint32_t m_curnibble; // index of the current nibble
|
||||||
|
uint32_t m_curbyte; // current byte of data
|
||||||
|
uint32_t m_curaddress; // current address
|
||||||
|
int32_t m_accumulator; // accumulator
|
||||||
|
int32_t m_step_index; // index in the stepping table
|
||||||
|
adpcm_a_registers &m_regs; // reference to registers
|
||||||
|
adpcm_a_engine &m_owner; // reference to our owner
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> adpcm_a_engine
|
||||||
|
|
||||||
|
class adpcm_a_engine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr int CHANNELS = adpcm_a_registers::CHANNELS;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
adpcm_a_engine(ymfm_interface &intf, uint32_t addrshift);
|
||||||
|
|
||||||
|
// reset our status
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// master clocking function
|
||||||
|
uint32_t clock(uint32_t chanmask);
|
||||||
|
|
||||||
|
// compute sum of channel outputs
|
||||||
|
template<int NumOutputs>
|
||||||
|
void output(ymfm_output<NumOutputs> &output, uint32_t chanmask);
|
||||||
|
|
||||||
|
// write to the ADPCM-A registers
|
||||||
|
void write(uint32_t regnum, uint8_t data);
|
||||||
|
|
||||||
|
// set the start/end address for a channel (for hardcoded YM2608 percussion)
|
||||||
|
void set_start_end(uint8_t chnum, uint16_t start, uint16_t end)
|
||||||
|
{
|
||||||
|
uint32_t choffs = adpcm_a_registers::channel_offset(chnum);
|
||||||
|
m_regs.write_start(choffs, start);
|
||||||
|
m_regs.write_end(choffs, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a reference to our interface
|
||||||
|
ymfm_interface &intf() { return m_intf; }
|
||||||
|
|
||||||
|
// return a reference to our registers
|
||||||
|
adpcm_a_registers ®s() { return m_regs; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
ymfm_interface &m_intf; // reference to the interface
|
||||||
|
std::unique_ptr<adpcm_a_channel> m_channel[CHANNELS]; // array of channels
|
||||||
|
adpcm_a_registers m_regs; // registers
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> adpcm_b_registers
|
||||||
|
|
||||||
|
//
|
||||||
|
// ADPCM-B register map:
|
||||||
|
//
|
||||||
|
// System-wide registers:
|
||||||
|
// 00 x------- Start of synthesis/analysis
|
||||||
|
// -x------ Record
|
||||||
|
// --x----- External/manual driving
|
||||||
|
// ---x---- Repeat playback
|
||||||
|
// ----x--- Speaker off
|
||||||
|
// -------x Reset
|
||||||
|
// 01 x------- Pan left
|
||||||
|
// -x------ Pan right
|
||||||
|
// ----x--- Start conversion
|
||||||
|
// -----x-- DAC enable
|
||||||
|
// ------x- DRAM access (1=8-bit granularity; 0=1-bit)
|
||||||
|
// -------x RAM/ROM (1=ROM, 0=RAM)
|
||||||
|
// 02 xxxxxxxx Start address (low)
|
||||||
|
// 03 xxxxxxxx Start address (high)
|
||||||
|
// 04 xxxxxxxx End address (low)
|
||||||
|
// 05 xxxxxxxx End address (high)
|
||||||
|
// 06 xxxxxxxx Prescale value (low)
|
||||||
|
// 07 -----xxx Prescale value (high)
|
||||||
|
// 08 xxxxxxxx CPU data/buffer
|
||||||
|
// 09 xxxxxxxx Delta-N frequency scale (low)
|
||||||
|
// 0a xxxxxxxx Delta-N frequency scale (high)
|
||||||
|
// 0b xxxxxxxx Level control
|
||||||
|
// 0c xxxxxxxx Limit address (low)
|
||||||
|
// 0d xxxxxxxx Limit address (high)
|
||||||
|
// 0e xxxxxxxx DAC data [YM2608/10]
|
||||||
|
// 0f xxxxxxxx PCM data [YM2608/10]
|
||||||
|
// 0e xxxxxxxx DAC data high [Y8950]
|
||||||
|
// 0f xx------ DAC data low [Y8950]
|
||||||
|
// 10 -----xxx DAC data exponent [Y8950]
|
||||||
|
//
|
||||||
|
class adpcm_b_registers
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constants
|
||||||
|
static constexpr uint32_t REGISTERS = 0x11;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
adpcm_b_registers() { }
|
||||||
|
|
||||||
|
// reset to initial state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// direct read/write access
|
||||||
|
void write(uint32_t index, uint8_t data) { m_regdata[index] = data; }
|
||||||
|
|
||||||
|
// system-wide registers
|
||||||
|
uint32_t execute() const { return bitfield(m_regdata[0x00], 7); }
|
||||||
|
uint32_t record() const { return bitfield(m_regdata[0x00], 6); }
|
||||||
|
uint32_t external() const { return bitfield(m_regdata[0x00], 5); }
|
||||||
|
uint32_t repeat() const { return bitfield(m_regdata[0x00], 4); }
|
||||||
|
uint32_t speaker() const { return bitfield(m_regdata[0x00], 3); }
|
||||||
|
uint32_t resetflag() const { return bitfield(m_regdata[0x00], 0); }
|
||||||
|
uint32_t pan_left() const { return bitfield(m_regdata[0x01], 7); }
|
||||||
|
uint32_t pan_right() const { return bitfield(m_regdata[0x01], 6); }
|
||||||
|
uint32_t start_conversion() const { return bitfield(m_regdata[0x01], 3); }
|
||||||
|
uint32_t dac_enable() const { return bitfield(m_regdata[0x01], 2); }
|
||||||
|
uint32_t dram_8bit() const { return bitfield(m_regdata[0x01], 1); }
|
||||||
|
uint32_t rom_ram() const { return bitfield(m_regdata[0x01], 0); }
|
||||||
|
uint32_t start() const { return m_regdata[0x02] | (m_regdata[0x03] << 8); }
|
||||||
|
uint32_t end() const { return m_regdata[0x04] | (m_regdata[0x05] << 8); }
|
||||||
|
uint32_t prescale() const { return m_regdata[0x06] | (bitfield(m_regdata[0x07], 0, 3) << 8); }
|
||||||
|
uint32_t cpudata() const { return m_regdata[0x08]; }
|
||||||
|
uint32_t delta_n() const { return m_regdata[0x09] | (m_regdata[0x0a] << 8); }
|
||||||
|
uint32_t level() const { return m_regdata[0x0b]; }
|
||||||
|
uint32_t limit() const { return m_regdata[0x0c] | (m_regdata[0x0d] << 8); }
|
||||||
|
uint32_t dac() const { return m_regdata[0x0e]; }
|
||||||
|
uint32_t pcm() const { return m_regdata[0x0f]; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
uint8_t m_regdata[REGISTERS]; // register data
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> adpcm_b_channel
|
||||||
|
|
||||||
|
class adpcm_b_channel
|
||||||
|
{
|
||||||
|
static constexpr int32_t STEP_MIN = 127;
|
||||||
|
static constexpr int32_t STEP_MAX = 24576;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t STATUS_EOS = 0x01;
|
||||||
|
static constexpr uint8_t STATUS_BRDY = 0x02;
|
||||||
|
static constexpr uint8_t STATUS_PLAYING = 0x04;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
adpcm_b_channel(adpcm_b_engine &owner, uint32_t addrshift);
|
||||||
|
|
||||||
|
// reset the channel state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// signal key on/off
|
||||||
|
void keyonoff(bool on);
|
||||||
|
|
||||||
|
// master clocking function
|
||||||
|
void clock();
|
||||||
|
|
||||||
|
// return the computed output value, with panning applied
|
||||||
|
template<int NumOutputs>
|
||||||
|
void output(ymfm_output<NumOutputs> &output, uint32_t rshift) const;
|
||||||
|
|
||||||
|
// return the status register
|
||||||
|
uint8_t status() const { return m_status; }
|
||||||
|
|
||||||
|
// handle special register reads
|
||||||
|
uint8_t read(uint32_t regnum);
|
||||||
|
|
||||||
|
// handle special register writes
|
||||||
|
void write(uint32_t regnum, uint8_t value);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// helper - return the current address shift
|
||||||
|
uint32_t address_shift() const;
|
||||||
|
|
||||||
|
// load the start address
|
||||||
|
void load_start();
|
||||||
|
|
||||||
|
// limit checker; stops at the last byte of the chunk described by address_shift()
|
||||||
|
bool at_limit() const { return (m_curaddress == (((m_regs.limit() + 1) << address_shift()) - 1)); }
|
||||||
|
|
||||||
|
// end checker; stops at the last byte of the chunk described by address_shift()
|
||||||
|
bool at_end() const { return (m_curaddress == (((m_regs.end() + 1) << address_shift()) - 1)); }
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint32_t const m_address_shift; // address bits shift-left
|
||||||
|
uint32_t m_status; // currently playing?
|
||||||
|
uint32_t m_curnibble; // index of the current nibble
|
||||||
|
uint32_t m_curbyte; // current byte of data
|
||||||
|
uint32_t m_dummy_read; // dummy read tracker
|
||||||
|
uint32_t m_position; // current fractional position
|
||||||
|
uint32_t m_curaddress; // current address
|
||||||
|
int32_t m_accumulator; // accumulator
|
||||||
|
int32_t m_prev_accum; // previous accumulator (for linear interp)
|
||||||
|
int32_t m_adpcm_step; // next forecast
|
||||||
|
adpcm_b_registers &m_regs; // reference to registers
|
||||||
|
adpcm_b_engine &m_owner; // reference to our owner
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> adpcm_b_engine
|
||||||
|
|
||||||
|
class adpcm_b_engine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
adpcm_b_engine(ymfm_interface &intf, uint32_t addrshift = 0);
|
||||||
|
|
||||||
|
// reset our status
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// master clocking function
|
||||||
|
void clock();
|
||||||
|
|
||||||
|
// compute sum of channel outputs
|
||||||
|
template<int NumOutputs>
|
||||||
|
void output(ymfm_output<NumOutputs> &output, uint32_t rshift);
|
||||||
|
|
||||||
|
// read from the ADPCM-B registers
|
||||||
|
uint32_t read(uint32_t regnum) { return m_channel->read(regnum); }
|
||||||
|
|
||||||
|
// write to the ADPCM-B registers
|
||||||
|
void write(uint32_t regnum, uint8_t data);
|
||||||
|
|
||||||
|
// status
|
||||||
|
uint8_t status() const { return m_channel->status(); }
|
||||||
|
|
||||||
|
// return a reference to our interface
|
||||||
|
ymfm_interface &intf() { return m_intf; }
|
||||||
|
|
||||||
|
// return a reference to our registers
|
||||||
|
adpcm_b_registers ®s() { return m_regs; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
ymfm_interface &m_intf; // reference to our interface
|
||||||
|
std::unique_ptr<adpcm_b_channel> m_channel; // channel pointer
|
||||||
|
adpcm_b_registers m_regs; // registers
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // YMFM_ADPCM_H
|
463
backends/playback/zsm/x16emu/ymfm/src/ymfm_fm.h
Normal file
463
backends/playback/zsm/x16emu/ymfm/src/ymfm_fm.h
Normal file
|
@ -0,0 +1,463 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#ifndef YMFM_FM_H
|
||||||
|
#define YMFM_FM_H
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define YMFM_DEBUG_LOG_WAVFILES (0)
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// GLOBAL ENUMERATORS
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// three different keyon sources; actual keyon is an OR over all of these
|
||||||
|
enum keyon_type : uint32_t
|
||||||
|
{
|
||||||
|
KEYON_NORMAL = 0,
|
||||||
|
KEYON_RHYTHM = 1,
|
||||||
|
KEYON_CSM = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// CORE IMPLEMENTATION
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> opdata_cache
|
||||||
|
|
||||||
|
// this class holds data that is computed once at the start of clocking
|
||||||
|
// and remains static during subsequent sound generation
|
||||||
|
struct opdata_cache
|
||||||
|
{
|
||||||
|
// set phase_step to this value to recalculate it each sample; needed
|
||||||
|
// in the case of PM LFO changes
|
||||||
|
static constexpr uint32_t PHASE_STEP_DYNAMIC = 1;
|
||||||
|
|
||||||
|
uint16_t const *waveform; // base of sine table
|
||||||
|
uint32_t phase_step; // phase step, or PHASE_STEP_DYNAMIC if PM is active
|
||||||
|
uint32_t total_level; // total level * 8 + KSL
|
||||||
|
uint32_t block_freq; // raw block frequency value (used to compute phase_step)
|
||||||
|
int32_t detune; // detuning value (used to compute phase_step)
|
||||||
|
uint32_t multiple; // multiple value (x.1, used to compute phase_step)
|
||||||
|
uint32_t eg_sustain; // sustain level, shifted up to envelope values
|
||||||
|
uint8_t eg_rate[EG_STATES]; // envelope rate, including KSR
|
||||||
|
uint8_t eg_shift = 0; // envelope shift amount
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> fm_registers_base
|
||||||
|
|
||||||
|
// base class for family-specific register classes; this provides a few
|
||||||
|
// constants, common defaults, and helpers, but mostly each derived class is
|
||||||
|
// responsible for defining all commonly-called methods
|
||||||
|
class fm_registers_base
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// this value is returned from the write() function for rhythm channels
|
||||||
|
static constexpr uint32_t RHYTHM_CHANNEL = 0xff;
|
||||||
|
|
||||||
|
// this is the size of a full sin waveform
|
||||||
|
static constexpr uint32_t WAVEFORM_LENGTH = 0x400;
|
||||||
|
|
||||||
|
//
|
||||||
|
// the following constants need to be defined per family:
|
||||||
|
// uint32_t OUTPUTS: The number of outputs exposed (1-4)
|
||||||
|
// uint32_t CHANNELS: The number of channels on the chip
|
||||||
|
// uint32_t ALL_CHANNELS: A bitmask of all channels
|
||||||
|
// uint32_t OPERATORS: The number of operators on the chip
|
||||||
|
// uint32_t WAVEFORMS: The number of waveforms offered
|
||||||
|
// uint32_t REGISTERS: The number of 8-bit registers allocated
|
||||||
|
// uint32_t DEFAULT_PRESCALE: The starting clock prescale
|
||||||
|
// uint32_t EG_CLOCK_DIVIDER: The clock divider of the envelope generator
|
||||||
|
// uint32_t CSM_TRIGGER_MASK: Mask of channels to trigger in CSM mode
|
||||||
|
// uint32_t REG_MODE: The address of the "mode" register controlling timers
|
||||||
|
// uint8_t STATUS_TIMERA: Status bit to set when timer A fires
|
||||||
|
// uint8_t STATUS_TIMERB: Status bit to set when tiemr B fires
|
||||||
|
// uint8_t STATUS_BUSY: Status bit to set when the chip is busy
|
||||||
|
// uint8_t STATUS_IRQ: Status bit to set when an IRQ is signalled
|
||||||
|
//
|
||||||
|
// the following constants are uncommon:
|
||||||
|
// bool DYNAMIC_OPS: True if ops/channel can be changed at runtime (OPL3+)
|
||||||
|
// bool EG_HAS_DEPRESS: True if the chip has a DP ("depress"?) envelope stage (OPLL)
|
||||||
|
// bool EG_HAS_REVERB: True if the chip has a faux reverb envelope stage (OPQ/OPZ)
|
||||||
|
// bool EG_HAS_SSG: True if the chip has SSG envelope support (OPN)
|
||||||
|
// bool MODULATOR_DELAY: True if the modulator is delayed by 1 sample (OPL pre-OPL3)
|
||||||
|
//
|
||||||
|
static constexpr bool DYNAMIC_OPS = false;
|
||||||
|
static constexpr bool EG_HAS_DEPRESS = false;
|
||||||
|
static constexpr bool EG_HAS_REVERB = false;
|
||||||
|
static constexpr bool EG_HAS_SSG = false;
|
||||||
|
static constexpr bool MODULATOR_DELAY = false;
|
||||||
|
|
||||||
|
// system-wide register defaults
|
||||||
|
uint32_t status_mask() const { return 0; } // OPL only
|
||||||
|
uint32_t irq_reset() const { return 0; } // OPL only
|
||||||
|
uint32_t noise_enable() const { return 0; } // OPM only
|
||||||
|
uint32_t rhythm_enable() const { return 0; } // OPL only
|
||||||
|
|
||||||
|
// per-operator register defaults
|
||||||
|
uint32_t op_ssg_eg_enable(uint32_t opoffs) const { return 0; } // OPN(A) only
|
||||||
|
uint32_t op_ssg_eg_mode(uint32_t opoffs) const { return 0; } // OPN(A) only
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// helper to encode four operator numbers into a 32-bit value in the
|
||||||
|
// operator maps for each register class
|
||||||
|
static constexpr uint32_t operator_list(uint8_t o1 = 0xff, uint8_t o2 = 0xff, uint8_t o3 = 0xff, uint8_t o4 = 0xff)
|
||||||
|
{
|
||||||
|
return o1 | (o2 << 8) | (o3 << 16) | (o4 << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to apply KSR to the raw ADSR rate, ignoring ksr if the
|
||||||
|
// raw value is 0, and clamping to 63
|
||||||
|
static constexpr uint32_t effective_rate(uint32_t rawrate, uint32_t ksr)
|
||||||
|
{
|
||||||
|
return (rawrate == 0) ? 0 : std::min<uint32_t>(rawrate + ksr, 63);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// CORE ENGINE CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// forward declarations
|
||||||
|
template<class RegisterType> class fm_engine_base;
|
||||||
|
|
||||||
|
// ======================> fm_operator
|
||||||
|
|
||||||
|
// fm_operator represents an FM operator (or "slot" in FM parlance), which
|
||||||
|
// produces an output sine wave modulated by an envelope
|
||||||
|
template<class RegisterType>
|
||||||
|
class fm_operator
|
||||||
|
{
|
||||||
|
// "quiet" value, used to optimize when we can skip doing work
|
||||||
|
static constexpr uint32_t EG_QUIET = 0x380;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
fm_operator(fm_engine_base<RegisterType> &owner, uint32_t opoffs);
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// reset the operator state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// return the operator/channel offset
|
||||||
|
uint32_t opoffs() const { return m_opoffs; }
|
||||||
|
uint32_t choffs() const { return m_choffs; }
|
||||||
|
|
||||||
|
// set the current channel
|
||||||
|
void set_choffs(uint32_t choffs) { m_choffs = choffs; }
|
||||||
|
|
||||||
|
// prepare prior to clocking
|
||||||
|
bool prepare();
|
||||||
|
|
||||||
|
// master clocking function
|
||||||
|
void clock(uint32_t env_counter, int32_t lfo_raw_pm);
|
||||||
|
|
||||||
|
// return the current phase value
|
||||||
|
uint32_t phase() const { return m_phase >> 10; }
|
||||||
|
|
||||||
|
// compute operator volume
|
||||||
|
int32_t compute_volume(uint32_t phase, uint32_t am_offset) const;
|
||||||
|
|
||||||
|
// compute volume for the OPM noise channel
|
||||||
|
int32_t compute_noise_volume(uint32_t am_offset) const;
|
||||||
|
|
||||||
|
// key state control
|
||||||
|
void keyonoff(uint32_t on, keyon_type type);
|
||||||
|
|
||||||
|
// return a reference to our registers
|
||||||
|
RegisterType ®s() const { return m_regs; }
|
||||||
|
|
||||||
|
// simple getters for debugging
|
||||||
|
envelope_state debug_eg_state() const { return m_env_state; }
|
||||||
|
uint16_t debug_eg_attenuation() const { return m_env_attenuation; }
|
||||||
|
uint8_t debug_ssg_inverted() const { return m_ssg_inverted; }
|
||||||
|
opdata_cache &debug_cache() { return m_cache; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// start the attack phase
|
||||||
|
void start_attack(bool is_restart = false);
|
||||||
|
|
||||||
|
// start the release phase
|
||||||
|
void start_release();
|
||||||
|
|
||||||
|
// clock phases
|
||||||
|
void clock_keystate(uint32_t keystate);
|
||||||
|
void clock_ssg_eg_state();
|
||||||
|
void clock_envelope(uint32_t env_counter);
|
||||||
|
void clock_phase(int32_t lfo_raw_pm);
|
||||||
|
|
||||||
|
// return effective attenuation of the envelope
|
||||||
|
uint32_t envelope_attenuation(uint32_t am_offset) const;
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint32_t m_choffs; // channel offset in registers
|
||||||
|
uint32_t m_opoffs; // operator offset in registers
|
||||||
|
uint32_t m_phase; // current phase value (10.10 format)
|
||||||
|
uint16_t m_env_attenuation; // computed envelope attenuation (4.6 format)
|
||||||
|
envelope_state m_env_state; // current envelope state
|
||||||
|
uint8_t m_ssg_inverted; // non-zero if the output should be inverted (bit 0)
|
||||||
|
uint8_t m_key_state; // current key state: on or off (bit 0)
|
||||||
|
uint8_t m_keyon_live; // live key on state (bit 0 = direct, bit 1 = rhythm, bit 2 = CSM)
|
||||||
|
opdata_cache m_cache; // cached values for performance
|
||||||
|
RegisterType &m_regs; // direct reference to registers
|
||||||
|
fm_engine_base<RegisterType> &m_owner; // reference to the owning engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> fm_channel
|
||||||
|
|
||||||
|
// fm_channel represents an FM channel which combines the output of 2 or 4
|
||||||
|
// operators into a final result
|
||||||
|
template<class RegisterType>
|
||||||
|
class fm_channel
|
||||||
|
{
|
||||||
|
using output_data = ymfm_output<RegisterType::OUTPUTS>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
fm_channel(fm_engine_base<RegisterType> &owner, uint32_t choffs);
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// reset the channel state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// return the channel offset
|
||||||
|
uint32_t choffs() const { return m_choffs; }
|
||||||
|
|
||||||
|
// assign operators
|
||||||
|
void assign(uint32_t index, fm_operator<RegisterType> *op)
|
||||||
|
{
|
||||||
|
assert(index < array_size(m_op));
|
||||||
|
m_op[index] = op;
|
||||||
|
if (op != nullptr)
|
||||||
|
op->set_choffs(m_choffs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// signal key on/off to our operators
|
||||||
|
void keyonoff(uint32_t states, keyon_type type, uint32_t chnum);
|
||||||
|
|
||||||
|
// prepare prior to clocking
|
||||||
|
bool prepare();
|
||||||
|
|
||||||
|
// master clocking function
|
||||||
|
void clock(uint32_t env_counter, int32_t lfo_raw_pm);
|
||||||
|
|
||||||
|
// specific 2-operator and 4-operator output handlers
|
||||||
|
void output_2op(output_data &output, uint32_t rshift, int32_t clipmax) const;
|
||||||
|
void output_4op(output_data &output, uint32_t rshift, int32_t clipmax) const;
|
||||||
|
|
||||||
|
// compute the special OPL rhythm channel outputs
|
||||||
|
void output_rhythm_ch6(output_data &output, uint32_t rshift, int32_t clipmax) const;
|
||||||
|
void output_rhythm_ch7(uint32_t phase_select, output_data &output, uint32_t rshift, int32_t clipmax) const;
|
||||||
|
void output_rhythm_ch8(uint32_t phase_select, output_data &output, uint32_t rshift, int32_t clipmax) const;
|
||||||
|
|
||||||
|
// are we a 4-operator channel or a 2-operator one?
|
||||||
|
bool is4op() const
|
||||||
|
{
|
||||||
|
if (RegisterType::DYNAMIC_OPS)
|
||||||
|
return (m_op[2] != nullptr);
|
||||||
|
return (RegisterType::OPERATORS / RegisterType::CHANNELS == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a reference to our registers
|
||||||
|
RegisterType ®s() const { return m_regs; }
|
||||||
|
|
||||||
|
// simple getters for debugging
|
||||||
|
fm_operator<RegisterType> *debug_operator(uint32_t index) const { return m_op[index]; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// helper to add values to the outputs based on channel enables
|
||||||
|
void add_to_output(uint32_t choffs, output_data &output, int32_t value) const
|
||||||
|
{
|
||||||
|
// create these constants to appease overzealous compilers checking array
|
||||||
|
// bounds in unreachable code (looking at you, clang)
|
||||||
|
constexpr int out0_index = 0;
|
||||||
|
constexpr int out1_index = 1 % RegisterType::OUTPUTS;
|
||||||
|
constexpr int out2_index = 2 % RegisterType::OUTPUTS;
|
||||||
|
constexpr int out3_index = 3 % RegisterType::OUTPUTS;
|
||||||
|
|
||||||
|
if (RegisterType::OUTPUTS == 1 || m_regs.ch_output_0(choffs))
|
||||||
|
output.data[out0_index] += value;
|
||||||
|
if (RegisterType::OUTPUTS >= 2 && m_regs.ch_output_1(choffs))
|
||||||
|
output.data[out1_index] += value;
|
||||||
|
if (RegisterType::OUTPUTS >= 3 && m_regs.ch_output_2(choffs))
|
||||||
|
output.data[out2_index] += value;
|
||||||
|
if (RegisterType::OUTPUTS >= 4 && m_regs.ch_output_3(choffs))
|
||||||
|
output.data[out3_index] += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint32_t m_choffs; // channel offset in registers
|
||||||
|
int16_t m_feedback[2]; // feedback memory for operator 1
|
||||||
|
mutable int16_t m_feedback_in; // next input value for op 1 feedback (set in output)
|
||||||
|
fm_operator<RegisterType> *m_op[4]; // up to 4 operators
|
||||||
|
RegisterType &m_regs; // direct reference to registers
|
||||||
|
fm_engine_base<RegisterType> &m_owner; // reference to the owning engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> fm_engine_base
|
||||||
|
|
||||||
|
// fm_engine_base represents a set of operators and channels which together
|
||||||
|
// form a Yamaha FM core; chips that implement other engines (ADPCM, wavetable,
|
||||||
|
// etc) take this output and combine it with the others externally
|
||||||
|
template<class RegisterType>
|
||||||
|
class fm_engine_base : public ymfm_engine_callbacks
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// expose some constants from the registers
|
||||||
|
static constexpr uint32_t OUTPUTS = RegisterType::OUTPUTS;
|
||||||
|
static constexpr uint32_t CHANNELS = RegisterType::CHANNELS;
|
||||||
|
static constexpr uint32_t ALL_CHANNELS = RegisterType::ALL_CHANNELS;
|
||||||
|
static constexpr uint32_t OPERATORS = RegisterType::OPERATORS;
|
||||||
|
|
||||||
|
// also expose status flags for consumers that inject additional bits
|
||||||
|
static constexpr uint8_t STATUS_TIMERA = RegisterType::STATUS_TIMERA;
|
||||||
|
static constexpr uint8_t STATUS_TIMERB = RegisterType::STATUS_TIMERB;
|
||||||
|
static constexpr uint8_t STATUS_BUSY = RegisterType::STATUS_BUSY;
|
||||||
|
static constexpr uint8_t STATUS_IRQ = RegisterType::STATUS_IRQ;
|
||||||
|
|
||||||
|
// expose the correct output class
|
||||||
|
using output_data = ymfm_output<OUTPUTS>;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
fm_engine_base(ymfm_interface &intf);
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// reset the overall state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// master clocking function
|
||||||
|
uint32_t clock(uint32_t chanmask);
|
||||||
|
|
||||||
|
// compute sum of channel outputs
|
||||||
|
void output(output_data &output, uint32_t rshift, int32_t clipmax, uint32_t chanmask) const;
|
||||||
|
|
||||||
|
// write to the OPN registers
|
||||||
|
void write(uint16_t regnum, uint8_t data);
|
||||||
|
|
||||||
|
// return the current status
|
||||||
|
uint8_t status() const;
|
||||||
|
|
||||||
|
// set/reset bits in the status register, updating the IRQ status
|
||||||
|
uint8_t set_reset_status(uint8_t set, uint8_t reset)
|
||||||
|
{
|
||||||
|
m_status = (m_status | set) & ~(reset | STATUS_BUSY);
|
||||||
|
m_intf.ymfm_sync_check_interrupts();
|
||||||
|
return m_status & ~m_regs.status_mask();
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the IRQ mask
|
||||||
|
void set_irq_mask(uint8_t mask) { m_irq_mask = mask; m_intf.ymfm_sync_check_interrupts(); }
|
||||||
|
|
||||||
|
// return the current clock prescale
|
||||||
|
uint32_t clock_prescale() const { return m_clock_prescale; }
|
||||||
|
|
||||||
|
// set prescale factor (2/3/6)
|
||||||
|
void set_clock_prescale(uint32_t prescale) { m_clock_prescale = prescale; }
|
||||||
|
|
||||||
|
// compute sample rate
|
||||||
|
uint32_t sample_rate(uint32_t baseclock) const
|
||||||
|
{
|
||||||
|
#if (YMFM_DEBUG_LOG_WAVFILES)
|
||||||
|
for (uint32_t chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
m_wavfile[chnum].set_samplerate(baseclock / (m_clock_prescale * OPERATORS));
|
||||||
|
#endif
|
||||||
|
return baseclock / (m_clock_prescale * OPERATORS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the owning device
|
||||||
|
ymfm_interface &intf() const { return m_intf; }
|
||||||
|
|
||||||
|
// return a reference to our registers
|
||||||
|
RegisterType ®s() { return m_regs; }
|
||||||
|
|
||||||
|
// invalidate any caches
|
||||||
|
void invalidate_caches() { m_modified_channels = RegisterType::ALL_CHANNELS; }
|
||||||
|
|
||||||
|
// simple getters for debugging
|
||||||
|
fm_channel<RegisterType> *debug_channel(uint32_t index) const { return m_channel[index].get(); }
|
||||||
|
fm_operator<RegisterType> *debug_operator(uint32_t index) const { return m_operator[index].get(); }
|
||||||
|
|
||||||
|
public:
|
||||||
|
// timer callback; called by the interface when a timer fires
|
||||||
|
virtual void engine_timer_expired(uint32_t tnum) override;
|
||||||
|
|
||||||
|
// check interrupts; called by the interface after synchronization
|
||||||
|
virtual void engine_check_interrupts() override;
|
||||||
|
|
||||||
|
// mode register write; called by the interface after synchronization
|
||||||
|
virtual void engine_mode_write(uint8_t data) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// assign the current set of operators to channels
|
||||||
|
void assign_operators();
|
||||||
|
|
||||||
|
// update the state of the given timer
|
||||||
|
void update_timer(uint32_t which, uint32_t enable, int32_t delta_clocks);
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
ymfm_interface &m_intf; // reference to the system interface
|
||||||
|
uint32_t m_env_counter; // envelope counter; low 2 bits are sub-counter
|
||||||
|
uint8_t m_status; // current status register
|
||||||
|
uint8_t m_clock_prescale; // prescale factor (2/3/6)
|
||||||
|
uint8_t m_irq_mask; // mask of which bits signal IRQs
|
||||||
|
uint8_t m_irq_state; // current IRQ state
|
||||||
|
uint8_t m_timer_running[2]; // current timer running state
|
||||||
|
uint8_t m_total_clocks; // low 8 bits of the total number of clocks processed
|
||||||
|
uint32_t m_active_channels; // mask of active channels (computed by prepare)
|
||||||
|
uint32_t m_modified_channels; // mask of channels that have been modified
|
||||||
|
uint32_t m_prepare_count; // counter to do periodic prepare sweeps
|
||||||
|
RegisterType m_regs; // register accessor
|
||||||
|
std::unique_ptr<fm_channel<RegisterType>> m_channel[CHANNELS]; // channel pointers
|
||||||
|
std::unique_ptr<fm_operator<RegisterType>> m_operator[OPERATORS]; // operator pointers
|
||||||
|
#if (YMFM_DEBUG_LOG_WAVFILES)
|
||||||
|
mutable ymfm_wavfile<1> m_wavfile[CHANNELS]; // for debugging
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // YMFM_FM_H
|
1590
backends/playback/zsm/x16emu/ymfm/src/ymfm_fm.ipp
Normal file
1590
backends/playback/zsm/x16emu/ymfm/src/ymfm_fm.ipp
Normal file
File diff suppressed because it is too large
Load diff
2220
backends/playback/zsm/x16emu/ymfm/src/ymfm_opl.cpp
Normal file
2220
backends/playback/zsm/x16emu/ymfm/src/ymfm_opl.cpp
Normal file
File diff suppressed because it is too large
Load diff
902
backends/playback/zsm/x16emu/ymfm/src/ymfm_opl.h
Normal file
902
backends/playback/zsm/x16emu/ymfm/src/ymfm_opl.h
Normal file
|
@ -0,0 +1,902 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#ifndef YMFM_OPL_H
|
||||||
|
#define YMFM_OPL_H
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ymfm.h"
|
||||||
|
#include "ymfm_adpcm.h"
|
||||||
|
#include "ymfm_fm.h"
|
||||||
|
#include "ymfm_pcm.h"
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// REGISTER CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> opl_registers_base
|
||||||
|
|
||||||
|
//
|
||||||
|
// OPL/OPL2/OPL3/OPL4 register map:
|
||||||
|
//
|
||||||
|
// System-wide registers:
|
||||||
|
// 01 xxxxxxxx Test register
|
||||||
|
// --x----- Enable OPL compatibility mode [OPL2 only] (1 = enable)
|
||||||
|
// 02 xxxxxxxx Timer A value (4 * OPN)
|
||||||
|
// 03 xxxxxxxx Timer B value
|
||||||
|
// 04 x------- RST
|
||||||
|
// -x------ Mask timer A
|
||||||
|
// --x----- Mask timer B
|
||||||
|
// ------x- Load timer B
|
||||||
|
// -------x Load timer A
|
||||||
|
// 08 x------- CSM mode [OPL/OPL2 only]
|
||||||
|
// -x------ Note select
|
||||||
|
// BD x------- AM depth
|
||||||
|
// -x------ PM depth
|
||||||
|
// --x----- Rhythm enable
|
||||||
|
// ---x---- Bass drum key on
|
||||||
|
// ----x--- Snare drum key on
|
||||||
|
// -----x-- Tom key on
|
||||||
|
// ------x- Top cymbal key on
|
||||||
|
// -------x High hat key on
|
||||||
|
// 101 --xxxxxx Test register 2 [OPL3 only]
|
||||||
|
// 104 --x----- Channel 6 4-operator mode [OPL3 only]
|
||||||
|
// ---x---- Channel 5 4-operator mode [OPL3 only]
|
||||||
|
// ----x--- Channel 4 4-operator mode [OPL3 only]
|
||||||
|
// -----x-- Channel 3 4-operator mode [OPL3 only]
|
||||||
|
// ------x- Channel 2 4-operator mode [OPL3 only]
|
||||||
|
// -------x Channel 1 4-operator mode [OPL3 only]
|
||||||
|
// 105 -------x New [OPL3 only]
|
||||||
|
// ------x- New2 [OPL4 only]
|
||||||
|
//
|
||||||
|
// Per-channel registers (channel in address bits 0-3)
|
||||||
|
// Note that all these apply to address+100 as well on OPL3+
|
||||||
|
// A0-A8 xxxxxxxx F-number (low 8 bits)
|
||||||
|
// B0-B8 --x----- Key on
|
||||||
|
// ---xxx-- Block (octvate, 0-7)
|
||||||
|
// ------xx F-number (high two bits)
|
||||||
|
// C0-C8 x------- CHD output (to DO0 pin) [OPL3+ only]
|
||||||
|
// -x------ CHC output (to DO0 pin) [OPL3+ only]
|
||||||
|
// --x----- CHB output (mixed right, to DO2 pin) [OPL3+ only]
|
||||||
|
// ---x---- CHA output (mixed left, to DO2 pin) [OPL3+ only]
|
||||||
|
// ----xxx- Feedback level for operator 1 (0-7)
|
||||||
|
// -------x Operator connection algorithm
|
||||||
|
//
|
||||||
|
// Per-operator registers (operator in bits 0-5)
|
||||||
|
// Note that all these apply to address+100 as well on OPL3+
|
||||||
|
// 20-35 x------- AM enable
|
||||||
|
// -x------ PM enable (VIB)
|
||||||
|
// --x----- EG type
|
||||||
|
// ---x---- Key scale rate
|
||||||
|
// ----xxxx Multiple value (0-15)
|
||||||
|
// 40-55 xx------ Key scale level (0-3)
|
||||||
|
// --xxxxxx Total level (0-63)
|
||||||
|
// 60-75 xxxx---- Attack rate (0-15)
|
||||||
|
// ----xxxx Decay rate (0-15)
|
||||||
|
// 80-95 xxxx---- Sustain level (0-15)
|
||||||
|
// ----xxxx Release rate (0-15)
|
||||||
|
// E0-F5 ------xx Wave select (0-3) [OPL2 only]
|
||||||
|
// -----xxx Wave select (0-7) [OPL3+ only]
|
||||||
|
//
|
||||||
|
|
||||||
|
template<int Revision>
|
||||||
|
class opl_registers_base : public fm_registers_base
|
||||||
|
{
|
||||||
|
static constexpr bool IsOpl2 = (Revision == 2);
|
||||||
|
static constexpr bool IsOpl2Plus = (Revision >= 2);
|
||||||
|
static constexpr bool IsOpl3Plus = (Revision >= 3);
|
||||||
|
static constexpr bool IsOpl4Plus = (Revision >= 4);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// constants
|
||||||
|
static constexpr uint32_t OUTPUTS = IsOpl3Plus ? 4 : 1;
|
||||||
|
static constexpr uint32_t CHANNELS = IsOpl3Plus ? 18 : 9;
|
||||||
|
static constexpr uint32_t ALL_CHANNELS = (1 << CHANNELS) - 1;
|
||||||
|
static constexpr uint32_t OPERATORS = CHANNELS * 2;
|
||||||
|
static constexpr uint32_t WAVEFORMS = IsOpl3Plus ? 8 : (IsOpl2Plus ? 4 : 1);
|
||||||
|
static constexpr uint32_t REGISTERS = IsOpl3Plus ? 0x200 : 0x100;
|
||||||
|
static constexpr uint32_t REG_MODE = 0x04;
|
||||||
|
static constexpr uint32_t DEFAULT_PRESCALE = IsOpl4Plus ? 19 : (IsOpl3Plus ? 8 : 4);
|
||||||
|
static constexpr uint32_t EG_CLOCK_DIVIDER = 1;
|
||||||
|
static constexpr uint32_t CSM_TRIGGER_MASK = ALL_CHANNELS;
|
||||||
|
static constexpr bool DYNAMIC_OPS = IsOpl3Plus;
|
||||||
|
static constexpr bool MODULATOR_DELAY = !IsOpl3Plus;
|
||||||
|
static constexpr uint8_t STATUS_TIMERA = 0x40;
|
||||||
|
static constexpr uint8_t STATUS_TIMERB = 0x20;
|
||||||
|
static constexpr uint8_t STATUS_BUSY = 0;
|
||||||
|
static constexpr uint8_t STATUS_IRQ = 0x80;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
opl_registers_base();
|
||||||
|
|
||||||
|
// reset to initial state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// map channel number to register offset
|
||||||
|
static constexpr uint32_t channel_offset(uint32_t chnum)
|
||||||
|
{
|
||||||
|
assert(chnum < CHANNELS);
|
||||||
|
if (!IsOpl3Plus)
|
||||||
|
return chnum;
|
||||||
|
else
|
||||||
|
return (chnum % 9) + 0x100 * (chnum / 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// map operator number to register offset
|
||||||
|
static constexpr uint32_t operator_offset(uint32_t opnum)
|
||||||
|
{
|
||||||
|
assert(opnum < OPERATORS);
|
||||||
|
if (!IsOpl3Plus)
|
||||||
|
return opnum + 2 * (opnum / 6);
|
||||||
|
else
|
||||||
|
return (opnum % 18) + 2 * ((opnum % 18) / 6) + 0x100 * (opnum / 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an array of operator indices for each channel
|
||||||
|
struct operator_mapping { uint32_t chan[CHANNELS]; };
|
||||||
|
void operator_map(operator_mapping &dest) const;
|
||||||
|
|
||||||
|
// OPL4 apparently can read back FM registers?
|
||||||
|
uint8_t read(uint16_t index) const { return m_regdata[index]; }
|
||||||
|
|
||||||
|
// handle writes to the register array
|
||||||
|
bool write(uint16_t index, uint8_t data, uint32_t &chan, uint32_t &opmask);
|
||||||
|
|
||||||
|
// clock the noise and LFO, if present, returning LFO PM value
|
||||||
|
int32_t clock_noise_and_lfo();
|
||||||
|
|
||||||
|
// reset the LFO
|
||||||
|
void reset_lfo() { m_lfo_am_counter = m_lfo_pm_counter = 0; }
|
||||||
|
|
||||||
|
// return the AM offset from LFO for the given channel
|
||||||
|
// on OPL this is just a fixed value
|
||||||
|
uint32_t lfo_am_offset(uint32_t choffs) const { return m_lfo_am; }
|
||||||
|
|
||||||
|
// return LFO/noise states
|
||||||
|
uint32_t noise_state() const { return m_noise_lfsr >> 23; }
|
||||||
|
|
||||||
|
// caching helpers
|
||||||
|
void cache_operator_data(uint32_t choffs, uint32_t opoffs, opdata_cache &cache);
|
||||||
|
|
||||||
|
// compute the phase step, given a PM value
|
||||||
|
uint32_t compute_phase_step(uint32_t choffs, uint32_t opoffs, opdata_cache const &cache, int32_t lfo_raw_pm);
|
||||||
|
|
||||||
|
// log a key-on event
|
||||||
|
std::string log_keyon(uint32_t choffs, uint32_t opoffs);
|
||||||
|
|
||||||
|
// system-wide registers
|
||||||
|
uint32_t test() const { return byte(0x01, 0, 8); }
|
||||||
|
uint32_t waveform_enable() const { return IsOpl2 ? byte(0x01, 5, 1) : (IsOpl3Plus ? 1 : 0); }
|
||||||
|
uint32_t timer_a_value() const { return byte(0x02, 0, 8) * 4; } // 8->10 bits
|
||||||
|
uint32_t timer_b_value() const { return byte(0x03, 0, 8); }
|
||||||
|
uint32_t status_mask() const { return byte(0x04, 0, 8) & 0x78; }
|
||||||
|
uint32_t irq_reset() const { return byte(0x04, 7, 1); }
|
||||||
|
uint32_t reset_timer_b() const { return byte(0x04, 7, 1) | byte(0x04, 5, 1); }
|
||||||
|
uint32_t reset_timer_a() const { return byte(0x04, 7, 1) | byte(0x04, 6, 1); }
|
||||||
|
uint32_t enable_timer_b() const { return 1; }
|
||||||
|
uint32_t enable_timer_a() const { return 1; }
|
||||||
|
uint32_t load_timer_b() const { return byte(0x04, 1, 1); }
|
||||||
|
uint32_t load_timer_a() const { return byte(0x04, 0, 1); }
|
||||||
|
uint32_t csm() const { return IsOpl3Plus ? 0 : byte(0x08, 7, 1); }
|
||||||
|
uint32_t note_select() const { return byte(0x08, 6, 1); }
|
||||||
|
uint32_t lfo_am_depth() const { return byte(0xbd, 7, 1); }
|
||||||
|
uint32_t lfo_pm_depth() const { return byte(0xbd, 6, 1); }
|
||||||
|
uint32_t rhythm_enable() const { return byte(0xbd, 5, 1); }
|
||||||
|
uint32_t rhythm_keyon() const { return byte(0xbd, 4, 0); }
|
||||||
|
uint32_t newflag() const { return IsOpl3Plus ? byte(0x105, 0, 1) : 0; }
|
||||||
|
uint32_t new2flag() const { return IsOpl4Plus ? byte(0x105, 1, 1) : 0; }
|
||||||
|
uint32_t fourop_enable() const { return IsOpl3Plus ? byte(0x104, 0, 6) : 0; }
|
||||||
|
|
||||||
|
// per-channel registers
|
||||||
|
uint32_t ch_block_freq(uint32_t choffs) const { return word(0xb0, 0, 5, 0xa0, 0, 8, choffs); }
|
||||||
|
uint32_t ch_feedback(uint32_t choffs) const { return byte(0xc0, 1, 3, choffs); }
|
||||||
|
uint32_t ch_algorithm(uint32_t choffs) const { return byte(0xc0, 0, 1, choffs) | (IsOpl3Plus ? (8 | (byte(0xc3, 0, 1, choffs) << 1)) : 0); }
|
||||||
|
uint32_t ch_output_any(uint32_t choffs) const { return newflag() ? byte(0xc0 + choffs, 4, 4) : 1; }
|
||||||
|
uint32_t ch_output_0(uint32_t choffs) const { return newflag() ? byte(0xc0 + choffs, 4, 1) : 1; }
|
||||||
|
uint32_t ch_output_1(uint32_t choffs) const { return newflag() ? byte(0xc0 + choffs, 5, 1) : (IsOpl3Plus ? 1 : 0); }
|
||||||
|
uint32_t ch_output_2(uint32_t choffs) const { return newflag() ? byte(0xc0 + choffs, 6, 1) : 0; }
|
||||||
|
uint32_t ch_output_3(uint32_t choffs) const { return newflag() ? byte(0xc0 + choffs, 7, 1) : 0; }
|
||||||
|
|
||||||
|
// per-operator registers
|
||||||
|
uint32_t op_lfo_am_enable(uint32_t opoffs) const { return byte(0x20, 7, 1, opoffs); }
|
||||||
|
uint32_t op_lfo_pm_enable(uint32_t opoffs) const { return byte(0x20, 6, 1, opoffs); }
|
||||||
|
uint32_t op_eg_sustain(uint32_t opoffs) const { return byte(0x20, 5, 1, opoffs); }
|
||||||
|
uint32_t op_ksr(uint32_t opoffs) const { return byte(0x20, 4, 1, opoffs); }
|
||||||
|
uint32_t op_multiple(uint32_t opoffs) const { return byte(0x20, 0, 4, opoffs); }
|
||||||
|
uint32_t op_ksl(uint32_t opoffs) const { uint32_t temp = byte(0x40, 6, 2, opoffs); return bitfield(temp, 1) | (bitfield(temp, 0) << 1); }
|
||||||
|
uint32_t op_total_level(uint32_t opoffs) const { return byte(0x40, 0, 6, opoffs); }
|
||||||
|
uint32_t op_attack_rate(uint32_t opoffs) const { return byte(0x60, 4, 4, opoffs); }
|
||||||
|
uint32_t op_decay_rate(uint32_t opoffs) const { return byte(0x60, 0, 4, opoffs); }
|
||||||
|
uint32_t op_sustain_level(uint32_t opoffs) const { return byte(0x80, 4, 4, opoffs); }
|
||||||
|
uint32_t op_release_rate(uint32_t opoffs) const { return byte(0x80, 0, 4, opoffs); }
|
||||||
|
uint32_t op_waveform(uint32_t opoffs) const { return IsOpl2Plus ? byte(0xe0, 0, newflag() ? 3 : 2, opoffs) : 0; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// return a bitfield extracted from a byte
|
||||||
|
uint32_t byte(uint32_t offset, uint32_t start, uint32_t count, uint32_t extra_offset = 0) const
|
||||||
|
{
|
||||||
|
return bitfield(m_regdata[offset + extra_offset], start, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a bitfield extracted from a pair of bytes, MSBs listed first
|
||||||
|
uint32_t word(uint32_t offset1, uint32_t start1, uint32_t count1, uint32_t offset2, uint32_t start2, uint32_t count2, uint32_t extra_offset = 0) const
|
||||||
|
{
|
||||||
|
return (byte(offset1, start1, count1, extra_offset) << count2) | byte(offset2, start2, count2, extra_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to determine if the this channel is an active rhythm channel
|
||||||
|
bool is_rhythm(uint32_t choffs) const
|
||||||
|
{
|
||||||
|
return rhythm_enable() && (choffs >= 6 && choffs <= 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint16_t m_lfo_am_counter; // LFO AM counter
|
||||||
|
uint16_t m_lfo_pm_counter; // LFO PM counter
|
||||||
|
uint32_t m_noise_lfsr; // noise LFSR state
|
||||||
|
uint8_t m_lfo_am; // current LFO AM value
|
||||||
|
uint8_t m_regdata[REGISTERS]; // register data
|
||||||
|
uint16_t m_waveform[WAVEFORMS][WAVEFORM_LENGTH]; // waveforms
|
||||||
|
};
|
||||||
|
|
||||||
|
using opl_registers = opl_registers_base<1>;
|
||||||
|
using opl2_registers = opl_registers_base<2>;
|
||||||
|
using opl3_registers = opl_registers_base<3>;
|
||||||
|
using opl4_registers = opl_registers_base<4>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> opll_registers
|
||||||
|
|
||||||
|
//
|
||||||
|
// OPLL register map:
|
||||||
|
//
|
||||||
|
// System-wide registers:
|
||||||
|
// 0E --x----- Rhythm enable
|
||||||
|
// ---x---- Bass drum key on
|
||||||
|
// ----x--- Snare drum key on
|
||||||
|
// -----x-- Tom key on
|
||||||
|
// ------x- Top cymbal key on
|
||||||
|
// -------x High hat key on
|
||||||
|
// 0F xxxxxxxx Test register
|
||||||
|
//
|
||||||
|
// Per-channel registers (channel in address bits 0-3)
|
||||||
|
// 10-18 xxxxxxxx F-number (low 8 bits)
|
||||||
|
// 20-28 --x----- Sustain on
|
||||||
|
// ---x---- Key on
|
||||||
|
// --- xxx- Block (octvate, 0-7)
|
||||||
|
// -------x F-number (high bit)
|
||||||
|
// 30-38 xxxx---- Instrument selection
|
||||||
|
// ----xxxx Volume
|
||||||
|
//
|
||||||
|
// User instrument registers (for carrier, modulator operators)
|
||||||
|
// 00-01 x------- AM enable
|
||||||
|
// -x------ PM enable (VIB)
|
||||||
|
// --x----- EG type
|
||||||
|
// ---x---- Key scale rate
|
||||||
|
// ----xxxx Multiple value (0-15)
|
||||||
|
// 02 xx------ Key scale level (carrier, 0-3)
|
||||||
|
// --xxxxxx Total level (modulator, 0-63)
|
||||||
|
// 03 xx------ Key scale level (modulator, 0-3)
|
||||||
|
// ---x---- Rectified wave (carrier)
|
||||||
|
// ----x--- Rectified wave (modulator)
|
||||||
|
// -----xxx Feedback level for operator 1 (0-7)
|
||||||
|
// 04-05 xxxx---- Attack rate (0-15)
|
||||||
|
// ----xxxx Decay rate (0-15)
|
||||||
|
// 06-07 xxxx---- Sustain level (0-15)
|
||||||
|
// ----xxxx Release rate (0-15)
|
||||||
|
//
|
||||||
|
// Internal (fake) registers:
|
||||||
|
// 40-48 xxxxxxxx Current instrument base address
|
||||||
|
// 4E-5F xxxxxxxx Current instrument base address + operator slot (0/1)
|
||||||
|
// 70-FF xxxxxxxx Data for instruments (1-16 plus 3 drums)
|
||||||
|
//
|
||||||
|
|
||||||
|
class opll_registers : public fm_registers_base
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr uint32_t OUTPUTS = 2;
|
||||||
|
static constexpr uint32_t CHANNELS = 9;
|
||||||
|
static constexpr uint32_t ALL_CHANNELS = (1 << CHANNELS) - 1;
|
||||||
|
static constexpr uint32_t OPERATORS = CHANNELS * 2;
|
||||||
|
static constexpr uint32_t WAVEFORMS = 2;
|
||||||
|
static constexpr uint32_t REGISTERS = 0x40;
|
||||||
|
static constexpr uint32_t REG_MODE = 0x3f;
|
||||||
|
static constexpr uint32_t DEFAULT_PRESCALE = 4;
|
||||||
|
static constexpr uint32_t EG_CLOCK_DIVIDER = 1;
|
||||||
|
static constexpr uint32_t CSM_TRIGGER_MASK = 0;
|
||||||
|
static constexpr bool EG_HAS_DEPRESS = true;
|
||||||
|
static constexpr bool MODULATOR_DELAY = true;
|
||||||
|
static constexpr uint8_t STATUS_TIMERA = 0;
|
||||||
|
static constexpr uint8_t STATUS_TIMERB = 0;
|
||||||
|
static constexpr uint8_t STATUS_BUSY = 0;
|
||||||
|
static constexpr uint8_t STATUS_IRQ = 0;
|
||||||
|
|
||||||
|
// OPLL-specific constants
|
||||||
|
static constexpr uint32_t INSTDATA_SIZE = 0x90;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
opll_registers();
|
||||||
|
|
||||||
|
// reset to initial state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// map channel number to register offset
|
||||||
|
static constexpr uint32_t channel_offset(uint32_t chnum)
|
||||||
|
{
|
||||||
|
assert(chnum < CHANNELS);
|
||||||
|
return chnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// map operator number to register offset
|
||||||
|
static constexpr uint32_t operator_offset(uint32_t opnum)
|
||||||
|
{
|
||||||
|
assert(opnum < OPERATORS);
|
||||||
|
return opnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an array of operator indices for each channel
|
||||||
|
struct operator_mapping { uint32_t chan[CHANNELS]; };
|
||||||
|
void operator_map(operator_mapping &dest) const;
|
||||||
|
|
||||||
|
// read a register value
|
||||||
|
uint8_t read(uint16_t index) const { return m_regdata[index]; }
|
||||||
|
|
||||||
|
// handle writes to the register array
|
||||||
|
bool write(uint16_t index, uint8_t data, uint32_t &chan, uint32_t &opmask);
|
||||||
|
|
||||||
|
// clock the noise and LFO, if present, returning LFO PM value
|
||||||
|
int32_t clock_noise_and_lfo();
|
||||||
|
|
||||||
|
// reset the LFO
|
||||||
|
void reset_lfo() { m_lfo_am_counter = m_lfo_pm_counter = 0; }
|
||||||
|
|
||||||
|
// return the AM offset from LFO for the given channel
|
||||||
|
// on OPL this is just a fixed value
|
||||||
|
uint32_t lfo_am_offset(uint32_t choffs) const { return m_lfo_am; }
|
||||||
|
|
||||||
|
// return LFO/noise states
|
||||||
|
uint32_t noise_state() const { return m_noise_lfsr >> 23; }
|
||||||
|
|
||||||
|
// caching helpers
|
||||||
|
void cache_operator_data(uint32_t choffs, uint32_t opoffs, opdata_cache &cache);
|
||||||
|
|
||||||
|
// compute the phase step, given a PM value
|
||||||
|
uint32_t compute_phase_step(uint32_t choffs, uint32_t opoffs, opdata_cache const &cache, int32_t lfo_raw_pm);
|
||||||
|
|
||||||
|
// log a key-on event
|
||||||
|
std::string log_keyon(uint32_t choffs, uint32_t opoffs);
|
||||||
|
|
||||||
|
// set the instrument data
|
||||||
|
void set_instrument_data(uint8_t const *data)
|
||||||
|
{
|
||||||
|
std::copy_n(data, INSTDATA_SIZE, &m_instdata[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// system-wide registers
|
||||||
|
uint32_t rhythm_enable() const { return byte(0x0e, 5, 1); }
|
||||||
|
uint32_t rhythm_keyon() const { return byte(0x0e, 4, 0); }
|
||||||
|
uint32_t test() const { return byte(0x0f, 0, 8); }
|
||||||
|
uint32_t waveform_enable() const { return 1; }
|
||||||
|
uint32_t timer_a_value() const { return 0; }
|
||||||
|
uint32_t timer_b_value() const { return 0; }
|
||||||
|
uint32_t status_mask() const { return 0; }
|
||||||
|
uint32_t irq_reset() const { return 0; }
|
||||||
|
uint32_t reset_timer_b() const { return 0; }
|
||||||
|
uint32_t reset_timer_a() const { return 0; }
|
||||||
|
uint32_t enable_timer_b() const { return 0; }
|
||||||
|
uint32_t enable_timer_a() const { return 0; }
|
||||||
|
uint32_t load_timer_b() const { return 0; }
|
||||||
|
uint32_t load_timer_a() const { return 0; }
|
||||||
|
uint32_t csm() const { return 0; }
|
||||||
|
|
||||||
|
// per-channel registers
|
||||||
|
uint32_t ch_block_freq(uint32_t choffs) const { return word(0x20, 0, 4, 0x10, 0, 8, choffs); }
|
||||||
|
uint32_t ch_sustain(uint32_t choffs) const { return byte(0x20, 5, 1, choffs); }
|
||||||
|
uint32_t ch_total_level(uint32_t choffs) const { return instchbyte(0x02, 0, 6, choffs); }
|
||||||
|
uint32_t ch_feedback(uint32_t choffs) const { return instchbyte(0x03, 0, 3, choffs); }
|
||||||
|
uint32_t ch_algorithm(uint32_t choffs) const { return 0; }
|
||||||
|
uint32_t ch_instrument(uint32_t choffs) const { return byte(0x30, 4, 4, choffs); }
|
||||||
|
uint32_t ch_output_any(uint32_t choffs) const { return 1; }
|
||||||
|
uint32_t ch_output_0(uint32_t choffs) const { return !is_rhythm(choffs); }
|
||||||
|
uint32_t ch_output_1(uint32_t choffs) const { return is_rhythm(choffs); }
|
||||||
|
uint32_t ch_output_2(uint32_t choffs) const { return 0; }
|
||||||
|
uint32_t ch_output_3(uint32_t choffs) const { return 0; }
|
||||||
|
|
||||||
|
// per-operator registers
|
||||||
|
uint32_t op_lfo_am_enable(uint32_t opoffs) const { return instopbyte(0x00, 7, 1, opoffs); }
|
||||||
|
uint32_t op_lfo_pm_enable(uint32_t opoffs) const { return instopbyte(0x00, 6, 1, opoffs); }
|
||||||
|
uint32_t op_eg_sustain(uint32_t opoffs) const { return instopbyte(0x00, 5, 1, opoffs); }
|
||||||
|
uint32_t op_ksr(uint32_t opoffs) const { return instopbyte(0x00, 4, 1, opoffs); }
|
||||||
|
uint32_t op_multiple(uint32_t opoffs) const { return instopbyte(0x00, 0, 4, opoffs); }
|
||||||
|
uint32_t op_ksl(uint32_t opoffs) const { return instopbyte(0x02, 6, 2, opoffs); }
|
||||||
|
uint32_t op_waveform(uint32_t opoffs) const { return instchbyte(0x03, 3 + bitfield(opoffs, 0), 1, opoffs >> 1); }
|
||||||
|
uint32_t op_attack_rate(uint32_t opoffs) const { return instopbyte(0x04, 4, 4, opoffs); }
|
||||||
|
uint32_t op_decay_rate(uint32_t opoffs) const { return instopbyte(0x04, 0, 4, opoffs); }
|
||||||
|
uint32_t op_sustain_level(uint32_t opoffs) const { return instopbyte(0x06, 4, 4, opoffs); }
|
||||||
|
uint32_t op_release_rate(uint32_t opoffs) const { return instopbyte(0x06, 0, 4, opoffs); }
|
||||||
|
uint32_t op_volume(uint32_t opoffs) const { return byte(0x30, 4 * bitfield(~opoffs, 0), 4, opoffs >> 1); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// return a bitfield extracted from a byte
|
||||||
|
uint32_t byte(uint32_t offset, uint32_t start, uint32_t count, uint32_t extra_offset = 0) const
|
||||||
|
{
|
||||||
|
return bitfield(m_regdata[offset + extra_offset], start, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a bitfield extracted from a pair of bytes, MSBs listed first
|
||||||
|
uint32_t word(uint32_t offset1, uint32_t start1, uint32_t count1, uint32_t offset2, uint32_t start2, uint32_t count2, uint32_t extra_offset = 0) const
|
||||||
|
{
|
||||||
|
return (byte(offset1, start1, count1, extra_offset) << count2) | byte(offset2, start2, count2, extra_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpers to read from instrument channel/operator data
|
||||||
|
uint32_t instchbyte(uint32_t offset, uint32_t start, uint32_t count, uint32_t choffs) const { return bitfield(m_chinst[choffs][offset], start, count); }
|
||||||
|
uint32_t instopbyte(uint32_t offset, uint32_t start, uint32_t count, uint32_t opoffs) const { return bitfield(m_opinst[opoffs][offset], start, count); }
|
||||||
|
|
||||||
|
// helper to determine if the this channel is an active rhythm channel
|
||||||
|
bool is_rhythm(uint32_t choffs) const
|
||||||
|
{
|
||||||
|
return rhythm_enable() && choffs >= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint16_t m_lfo_am_counter; // LFO AM counter
|
||||||
|
uint16_t m_lfo_pm_counter; // LFO PM counter
|
||||||
|
uint32_t m_noise_lfsr; // noise LFSR state
|
||||||
|
uint8_t m_lfo_am; // current LFO AM value
|
||||||
|
uint8_t const *m_chinst[CHANNELS]; // pointer to instrument data for each channel
|
||||||
|
uint8_t const *m_opinst[OPERATORS]; // pointer to instrument data for each operator
|
||||||
|
uint8_t m_regdata[REGISTERS]; // register data
|
||||||
|
uint8_t m_instdata[INSTDATA_SIZE]; // instrument data
|
||||||
|
uint16_t m_waveform[WAVEFORMS][WAVEFORM_LENGTH]; // waveforms
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// OPL IMPLEMENTATION CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> ym3526
|
||||||
|
|
||||||
|
class ym3526
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using fm_engine = fm_engine_base<opl_registers>;
|
||||||
|
using output_data = fm_engine::output_data;
|
||||||
|
static constexpr uint32_t OUTPUTS = fm_engine::OUTPUTS;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
ym3526(ymfm_interface &intf);
|
||||||
|
|
||||||
|
// reset
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// pass-through helpers
|
||||||
|
uint32_t sample_rate(uint32_t input_clock) const { return m_fm.sample_rate(input_clock); }
|
||||||
|
void invalidate_caches() { m_fm.invalidate_caches(); }
|
||||||
|
|
||||||
|
// read access
|
||||||
|
uint8_t read_status();
|
||||||
|
uint8_t read(uint32_t offset);
|
||||||
|
|
||||||
|
// write access
|
||||||
|
void write_address(uint8_t data);
|
||||||
|
void write_data(uint8_t data);
|
||||||
|
void write(uint32_t offset, uint8_t data);
|
||||||
|
|
||||||
|
// generate samples of sound
|
||||||
|
void generate(output_data *output, uint32_t numsamples = 1);
|
||||||
|
protected:
|
||||||
|
// internal state
|
||||||
|
uint8_t m_address; // address register
|
||||||
|
fm_engine m_fm; // core FM engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> y8950
|
||||||
|
|
||||||
|
class y8950
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using fm_engine = fm_engine_base<opl_registers>;
|
||||||
|
using output_data = fm_engine::output_data;
|
||||||
|
static constexpr uint32_t OUTPUTS = fm_engine::OUTPUTS;
|
||||||
|
|
||||||
|
static constexpr uint8_t STATUS_ADPCM_B_PLAYING = 0x01;
|
||||||
|
static constexpr uint8_t STATUS_ADPCM_B_BRDY = 0x08;
|
||||||
|
static constexpr uint8_t STATUS_ADPCM_B_EOS = 0x10;
|
||||||
|
static constexpr uint8_t ALL_IRQS = STATUS_ADPCM_B_BRDY | STATUS_ADPCM_B_EOS | fm_engine::STATUS_TIMERA | fm_engine::STATUS_TIMERB;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
y8950(ymfm_interface &intf);
|
||||||
|
|
||||||
|
// reset
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// pass-through helpers
|
||||||
|
uint32_t sample_rate(uint32_t input_clock) const { return m_fm.sample_rate(input_clock); }
|
||||||
|
void invalidate_caches() { m_fm.invalidate_caches(); }
|
||||||
|
|
||||||
|
// read access
|
||||||
|
uint8_t read_status();
|
||||||
|
uint8_t read_data();
|
||||||
|
uint8_t read(uint32_t offset);
|
||||||
|
|
||||||
|
// write access
|
||||||
|
void write_address(uint8_t data);
|
||||||
|
void write_data(uint8_t data);
|
||||||
|
void write(uint32_t offset, uint8_t data);
|
||||||
|
|
||||||
|
// generate samples of sound
|
||||||
|
void generate(output_data *output, uint32_t numsamples = 1);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// internal state
|
||||||
|
uint8_t m_address; // address register
|
||||||
|
uint8_t m_io_ddr; // data direction register for I/O
|
||||||
|
fm_engine m_fm; // core FM engine
|
||||||
|
adpcm_b_engine m_adpcm_b; // ADPCM-B engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// OPL2 IMPLEMENTATION CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> ym3812
|
||||||
|
|
||||||
|
class ym3812
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using fm_engine = fm_engine_base<opl2_registers>;
|
||||||
|
using output_data = fm_engine::output_data;
|
||||||
|
static constexpr uint32_t OUTPUTS = fm_engine::OUTPUTS;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
ym3812(ymfm_interface &intf);
|
||||||
|
|
||||||
|
// reset
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// pass-through helpers
|
||||||
|
uint32_t sample_rate(uint32_t input_clock) const { return m_fm.sample_rate(input_clock); }
|
||||||
|
void invalidate_caches() { m_fm.invalidate_caches(); }
|
||||||
|
|
||||||
|
// read access
|
||||||
|
uint8_t read_status();
|
||||||
|
uint8_t read(uint32_t offset);
|
||||||
|
|
||||||
|
// write access
|
||||||
|
void write_address(uint8_t data);
|
||||||
|
void write_data(uint8_t data);
|
||||||
|
void write(uint32_t offset, uint8_t data);
|
||||||
|
|
||||||
|
// generate samples of sound
|
||||||
|
void generate(output_data *output, uint32_t numsamples = 1);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// internal state
|
||||||
|
uint8_t m_address; // address register
|
||||||
|
fm_engine m_fm; // core FM engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// OPL3 IMPLEMENTATION CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> ymf262
|
||||||
|
|
||||||
|
class ymf262
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using fm_engine = fm_engine_base<opl3_registers>;
|
||||||
|
using output_data = fm_engine::output_data;
|
||||||
|
static constexpr uint32_t OUTPUTS = fm_engine::OUTPUTS;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
ymf262(ymfm_interface &intf);
|
||||||
|
|
||||||
|
// reset
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// pass-through helpers
|
||||||
|
uint32_t sample_rate(uint32_t input_clock) const { return m_fm.sample_rate(input_clock); }
|
||||||
|
void invalidate_caches() { m_fm.invalidate_caches(); }
|
||||||
|
|
||||||
|
// read access
|
||||||
|
uint8_t read_status();
|
||||||
|
uint8_t read(uint32_t offset);
|
||||||
|
|
||||||
|
// write access
|
||||||
|
void write_address(uint8_t data);
|
||||||
|
void write_data(uint8_t data);
|
||||||
|
void write_address_hi(uint8_t data);
|
||||||
|
void write(uint32_t offset, uint8_t data);
|
||||||
|
|
||||||
|
// generate samples of sound
|
||||||
|
void generate(output_data *output, uint32_t numsamples = 1);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// internal state
|
||||||
|
uint16_t m_address; // address register
|
||||||
|
fm_engine m_fm; // core FM engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> ymf289b
|
||||||
|
|
||||||
|
class ymf289b
|
||||||
|
{
|
||||||
|
static constexpr uint8_t STATUS_BUSY_FLAGS = 0x05;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using fm_engine = fm_engine_base<opl3_registers>;
|
||||||
|
using output_data = fm_engine::output_data;
|
||||||
|
static constexpr uint32_t OUTPUTS = 2;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
ymf289b(ymfm_interface &intf);
|
||||||
|
|
||||||
|
// reset
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// pass-through helpers
|
||||||
|
uint32_t sample_rate(uint32_t input_clock) const { return m_fm.sample_rate(input_clock); }
|
||||||
|
void invalidate_caches() { m_fm.invalidate_caches(); }
|
||||||
|
|
||||||
|
// read access
|
||||||
|
uint8_t read_status();
|
||||||
|
uint8_t read_data();
|
||||||
|
uint8_t read(uint32_t offset);
|
||||||
|
|
||||||
|
// write access
|
||||||
|
void write_address(uint8_t data);
|
||||||
|
void write_data(uint8_t data);
|
||||||
|
void write_address_hi(uint8_t data);
|
||||||
|
void write(uint32_t offset, uint8_t data);
|
||||||
|
|
||||||
|
// generate samples of sound
|
||||||
|
void generate(output_data *output, uint32_t numsamples = 1);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// internal helpers
|
||||||
|
bool ymf289b_mode() { return ((m_fm.regs().read(0x105) & 0x04) != 0); }
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint16_t m_address; // address register
|
||||||
|
fm_engine m_fm; // core FM engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// OPL4 IMPLEMENTATION CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> ymf278b
|
||||||
|
|
||||||
|
class ymf278b
|
||||||
|
{
|
||||||
|
// Using the nominal datasheet frequency of 33.868MHz, the output of the
|
||||||
|
// chip will be clock/768 = 44.1kHz. However, the FM engine is clocked
|
||||||
|
// internally at clock/(19*36), or 49.515kHz, so the FM output needs to
|
||||||
|
// be downsampled. We treat this as needing to clock the FM engine an
|
||||||
|
// extra tick every few samples. The exact ratio is 768/(19*36) or
|
||||||
|
// 768/684 = 192/171. So if we always clock the FM once, we'll have
|
||||||
|
// 192/171 - 1 = 21/171 left. Thus we count 21 for each sample and when
|
||||||
|
// it gets above 171, we tick an extra time.
|
||||||
|
static constexpr uint32_t FM_EXTRA_SAMPLE_THRESH = 171;
|
||||||
|
static constexpr uint32_t FM_EXTRA_SAMPLE_STEP = 192 - FM_EXTRA_SAMPLE_THRESH;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using fm_engine = fm_engine_base<opl4_registers>;
|
||||||
|
static constexpr uint32_t OUTPUTS = 6;
|
||||||
|
using output_data = ymfm_output<OUTPUTS>;
|
||||||
|
|
||||||
|
static constexpr uint8_t STATUS_BUSY = 0x01;
|
||||||
|
static constexpr uint8_t STATUS_LD = 0x02;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
ymf278b(ymfm_interface &intf);
|
||||||
|
|
||||||
|
// reset
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// pass-through helpers
|
||||||
|
uint32_t sample_rate(uint32_t input_clock) const { return input_clock / 768; }
|
||||||
|
void invalidate_caches() { m_fm.invalidate_caches(); }
|
||||||
|
|
||||||
|
// read access
|
||||||
|
uint8_t read_status();
|
||||||
|
uint8_t read_data_pcm();
|
||||||
|
uint8_t read(uint32_t offset);
|
||||||
|
|
||||||
|
// write access
|
||||||
|
void write_address(uint8_t data);
|
||||||
|
void write_data(uint8_t data);
|
||||||
|
void write_address_hi(uint8_t data);
|
||||||
|
void write_address_pcm(uint8_t data);
|
||||||
|
void write_data_pcm(uint8_t data);
|
||||||
|
void write(uint32_t offset, uint8_t data);
|
||||||
|
|
||||||
|
// generate samples of sound
|
||||||
|
void generate(output_data *output, uint32_t numsamples = 1);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// internal state
|
||||||
|
uint16_t m_address; // address register
|
||||||
|
uint32_t m_fm_pos; // FM resampling position
|
||||||
|
uint32_t m_load_remaining; // how many more samples until LD flag clears
|
||||||
|
bool m_next_status_id; // flag to track which status ID to return
|
||||||
|
fm_engine m_fm; // core FM engine
|
||||||
|
pcm_engine m_pcm; // core PCM engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// OPLL IMPLEMENTATION CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> opll_base
|
||||||
|
|
||||||
|
class opll_base
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using fm_engine = fm_engine_base<opll_registers>;
|
||||||
|
using output_data = fm_engine::output_data;
|
||||||
|
static constexpr uint32_t OUTPUTS = fm_engine::OUTPUTS;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
opll_base(ymfm_interface &intf, uint8_t const *data);
|
||||||
|
|
||||||
|
// configuration
|
||||||
|
void set_instrument_data(uint8_t const *data) { m_fm.regs().set_instrument_data(data); }
|
||||||
|
|
||||||
|
// reset
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// pass-through helpers
|
||||||
|
uint32_t sample_rate(uint32_t input_clock) const { return m_fm.sample_rate(input_clock); }
|
||||||
|
void invalidate_caches() { m_fm.invalidate_caches(); }
|
||||||
|
|
||||||
|
// read access -- doesn't really have any, but provide these for consistency
|
||||||
|
uint8_t read_status() { return 0x00; }
|
||||||
|
uint8_t read(uint32_t offset) { return 0x00; }
|
||||||
|
|
||||||
|
// write access
|
||||||
|
void write_address(uint8_t data);
|
||||||
|
void write_data(uint8_t data);
|
||||||
|
void write(uint32_t offset, uint8_t data);
|
||||||
|
|
||||||
|
// generate samples of sound
|
||||||
|
void generate(output_data *output, uint32_t numsamples = 1);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// internal state
|
||||||
|
uint8_t m_address; // address register
|
||||||
|
fm_engine m_fm; // core FM engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> ym2413
|
||||||
|
|
||||||
|
class ym2413 : public opll_base
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
ym2413(ymfm_interface &intf, uint8_t const *instrument_data = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
static uint8_t const s_default_instruments[];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> ym2413
|
||||||
|
|
||||||
|
class ym2423 : public opll_base
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
ym2423(ymfm_interface &intf, uint8_t const *instrument_data = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
static uint8_t const s_default_instruments[];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> ymf281
|
||||||
|
|
||||||
|
class ymf281 : public opll_base
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
ymf281(ymfm_interface &intf, uint8_t const *instrument_data = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
static uint8_t const s_default_instruments[];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> ds1001
|
||||||
|
|
||||||
|
class ds1001 : public opll_base
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
ds1001(ymfm_interface &intf, uint8_t const *instrument_data = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
static uint8_t const s_default_instruments[];
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // YMFM_OPL_H
|
539
backends/playback/zsm/x16emu/ymfm/src/ymfm_opm.cpp
Normal file
539
backends/playback/zsm/x16emu/ymfm/src/ymfm_opm.cpp
Normal file
|
@ -0,0 +1,539 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#include "ymfm_opm.h"
|
||||||
|
#include "ymfm_fm.ipp"
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// OPM REGISTERS
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// opm_registers - constructor
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
opm_registers::opm_registers() :
|
||||||
|
m_lfo_counter(0),
|
||||||
|
m_noise_lfsr(1),
|
||||||
|
m_noise_counter(0),
|
||||||
|
m_noise_state(0),
|
||||||
|
m_noise_lfo(0),
|
||||||
|
m_lfo_am(0)
|
||||||
|
{
|
||||||
|
// create the waveforms
|
||||||
|
for (uint32_t index = 0; index < WAVEFORM_LENGTH; index++)
|
||||||
|
m_waveform[0][index] = abs_sin_attenuation(index) | (bitfield(index, 9) << 15);
|
||||||
|
|
||||||
|
// create the LFO waveforms; AM in the low 8 bits, PM in the upper 8
|
||||||
|
// waveforms are adjusted to match the pictures in the application manual
|
||||||
|
for (uint32_t index = 0; index < LFO_WAVEFORM_LENGTH; index++)
|
||||||
|
{
|
||||||
|
// waveform 0 is a sawtooth
|
||||||
|
uint8_t am = index ^ 0xff;
|
||||||
|
int8_t pm = int8_t(index);
|
||||||
|
m_lfo_waveform[0][index] = am | (pm << 8);
|
||||||
|
|
||||||
|
// waveform 1 is a square wave
|
||||||
|
am = bitfield(index, 7) ? 0 : 0xff;
|
||||||
|
pm = int8_t(am ^ 0x80);
|
||||||
|
m_lfo_waveform[1][index] = am | (pm << 8);
|
||||||
|
|
||||||
|
// waveform 2 is a triangle wave
|
||||||
|
am = bitfield(index, 7) ? (index << 1) : ((index ^ 0xff) << 1);
|
||||||
|
pm = int8_t(bitfield(index, 6) ? am : ~am);
|
||||||
|
m_lfo_waveform[2][index] = am | (pm << 8);
|
||||||
|
|
||||||
|
// waveform 3 is noise; it is filled in dynamically
|
||||||
|
m_lfo_waveform[3][index] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset to initial state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void opm_registers::reset()
|
||||||
|
{
|
||||||
|
std::fill_n(&m_regdata[0], REGISTERS, 0);
|
||||||
|
|
||||||
|
// enable output on both channels by default
|
||||||
|
m_regdata[0x20] = m_regdata[0x21] = m_regdata[0x22] = m_regdata[0x23] = 0xc0;
|
||||||
|
m_regdata[0x24] = m_regdata[0x25] = m_regdata[0x26] = m_regdata[0x27] = 0xc0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void opm_registers::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
state.save_restore(m_lfo_counter);
|
||||||
|
state.save_restore(m_lfo_am);
|
||||||
|
state.save_restore(m_noise_lfsr);
|
||||||
|
state.save_restore(m_noise_counter);
|
||||||
|
state.save_restore(m_noise_state);
|
||||||
|
state.save_restore(m_noise_lfo);
|
||||||
|
state.save_restore(m_regdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// operator_map - return an array of operator
|
||||||
|
// indices for each channel; for OPM this is fixed
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void opm_registers::operator_map(operator_mapping &dest) const
|
||||||
|
{
|
||||||
|
// Note that the channel index order is 0,2,1,3, so we bitswap the index.
|
||||||
|
//
|
||||||
|
// This is because the order in the map is:
|
||||||
|
// carrier 1, carrier 2, modulator 1, modulator 2
|
||||||
|
//
|
||||||
|
// But when wiring up the connections, the more natural order is:
|
||||||
|
// carrier 1, modulator 1, carrier 2, modulator 2
|
||||||
|
static const operator_mapping s_fixed_map =
|
||||||
|
{ {
|
||||||
|
operator_list( 0, 16, 8, 24 ), // Channel 0 operators
|
||||||
|
operator_list( 1, 17, 9, 25 ), // Channel 1 operators
|
||||||
|
operator_list( 2, 18, 10, 26 ), // Channel 2 operators
|
||||||
|
operator_list( 3, 19, 11, 27 ), // Channel 3 operators
|
||||||
|
operator_list( 4, 20, 12, 28 ), // Channel 4 operators
|
||||||
|
operator_list( 5, 21, 13, 29 ), // Channel 5 operators
|
||||||
|
operator_list( 6, 22, 14, 30 ), // Channel 6 operators
|
||||||
|
operator_list( 7, 23, 15, 31 ), // Channel 7 operators
|
||||||
|
} };
|
||||||
|
dest = s_fixed_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// write - handle writes to the register array
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
bool opm_registers::write(uint16_t index, uint8_t data, uint32_t &channel, uint32_t &opmask)
|
||||||
|
{
|
||||||
|
assert(index < REGISTERS);
|
||||||
|
|
||||||
|
// LFO AM/PM depth are written to the same register (0x19);
|
||||||
|
// redirect the PM depth to an unused neighbor (0x1a)
|
||||||
|
if (index == 0x19)
|
||||||
|
m_regdata[index + bitfield(data, 7)] = data;
|
||||||
|
else if (index != 0x1a)
|
||||||
|
m_regdata[index] = data;
|
||||||
|
|
||||||
|
// handle writes to the key on index
|
||||||
|
if (index == 0x08)
|
||||||
|
{
|
||||||
|
channel = bitfield(data, 0, 3);
|
||||||
|
opmask = bitfield(data, 3, 4);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clock_noise_and_lfo - clock the noise and LFO,
|
||||||
|
// handling clock division, depth, and waveform
|
||||||
|
// computations
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
int32_t opm_registers::clock_noise_and_lfo()
|
||||||
|
{
|
||||||
|
// base noise frequency is measured at 2x 1/2 FM frequency; this
|
||||||
|
// means each tick counts as two steps against the noise counter
|
||||||
|
uint32_t freq = noise_frequency();
|
||||||
|
for (int rep = 0; rep < 2; rep++)
|
||||||
|
{
|
||||||
|
// evidence seems to suggest the LFSR is clocked continually and just
|
||||||
|
// sampled at the noise frequency for output purposes; note that the
|
||||||
|
// low 8 bits are the most recent 8 bits of history while bits 8-24
|
||||||
|
// contain the 17 bit LFSR state
|
||||||
|
m_noise_lfsr <<= 1;
|
||||||
|
m_noise_lfsr |= bitfield(m_noise_lfsr, 17) ^ bitfield(m_noise_lfsr, 14) ^ 1;
|
||||||
|
|
||||||
|
// compare against the frequency and latch when we exceed it
|
||||||
|
if (m_noise_counter++ >= freq)
|
||||||
|
{
|
||||||
|
m_noise_counter = 0;
|
||||||
|
m_noise_state = bitfield(m_noise_lfsr, 17);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// treat the rate as a 4.4 floating-point step value with implied
|
||||||
|
// leading 1; this matches exactly the frequencies in the application
|
||||||
|
// manual, though it might not be implemented exactly this way on chip
|
||||||
|
uint32_t rate = lfo_rate();
|
||||||
|
m_lfo_counter += (0x10 | bitfield(rate, 0, 4)) << bitfield(rate, 4, 4);
|
||||||
|
|
||||||
|
// bit 1 of the test register is officially undocumented but has been
|
||||||
|
// discovered to hold the LFO in reset while active
|
||||||
|
if (lfo_reset())
|
||||||
|
m_lfo_counter = 0;
|
||||||
|
|
||||||
|
// now pull out the non-fractional LFO value
|
||||||
|
uint32_t lfo = bitfield(m_lfo_counter, 22, 8);
|
||||||
|
|
||||||
|
// fill in the noise entry 1 ahead of our current position; this
|
||||||
|
// ensures the current value remains stable for a full LFO clock
|
||||||
|
// and effectively latches the running value when the LFO advances
|
||||||
|
uint32_t lfo_noise = bitfield(m_noise_lfsr, 17, 8);
|
||||||
|
m_lfo_waveform[3][(lfo + 1) & 0xff] = lfo_noise | (lfo_noise << 8);
|
||||||
|
|
||||||
|
// fetch the AM/PM values based on the waveform; AM is unsigned and
|
||||||
|
// encoded in the low 8 bits, while PM signed and encoded in the upper
|
||||||
|
// 8 bits
|
||||||
|
int32_t ampm = m_lfo_waveform[lfo_waveform()][lfo];
|
||||||
|
|
||||||
|
// apply depth to the AM value and store for later
|
||||||
|
m_lfo_am = ((ampm & 0xff) * lfo_am_depth()) >> 7;
|
||||||
|
|
||||||
|
// apply depth to the PM value and return it
|
||||||
|
return ((ampm >> 8) * int32_t(lfo_pm_depth())) >> 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// lfo_am_offset - return the AM offset from LFO
|
||||||
|
// for the given channel
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint32_t opm_registers::lfo_am_offset(uint32_t choffs) const
|
||||||
|
{
|
||||||
|
// OPM maps AM quite differently from OPN
|
||||||
|
|
||||||
|
// shift value for AM sensitivity is [*, 0, 1, 2],
|
||||||
|
// mapping to values of [0, 23.9, 47.8, and 95.6dB]
|
||||||
|
uint32_t am_sensitivity = ch_lfo_am_sens(choffs);
|
||||||
|
if (am_sensitivity == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// QUESTION: see OPN note below for the dB range mapping; it applies
|
||||||
|
// here as well
|
||||||
|
|
||||||
|
// raw LFO AM value on OPM is 0-FF, which is already a factor of 2
|
||||||
|
// larger than the OPN below, putting our staring point at 2x theirs;
|
||||||
|
// this works out since our minimum is 2x their maximum
|
||||||
|
return m_lfo_am << (am_sensitivity - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// cache_operator_data - fill the operator cache
|
||||||
|
// with prefetched data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void opm_registers::cache_operator_data(uint32_t choffs, uint32_t opoffs, opdata_cache &cache)
|
||||||
|
{
|
||||||
|
// set up the easy stuff
|
||||||
|
cache.waveform = &m_waveform[0][0];
|
||||||
|
|
||||||
|
// get frequency from the channel
|
||||||
|
uint32_t block_freq = cache.block_freq = ch_block_freq(choffs);
|
||||||
|
|
||||||
|
// compute the keycode: block_freq is:
|
||||||
|
//
|
||||||
|
// BBBCCCCFFFFFF
|
||||||
|
// ^^^^^
|
||||||
|
//
|
||||||
|
// the 5-bit keycode is just the top 5 bits (block + top 2 bits
|
||||||
|
// of the key code)
|
||||||
|
uint32_t keycode = bitfield(block_freq, 8, 5);
|
||||||
|
|
||||||
|
// detune adjustment
|
||||||
|
cache.detune = detune_adjustment(op_detune(opoffs), keycode);
|
||||||
|
|
||||||
|
// multiple value, as an x.1 value (0 means 0.5)
|
||||||
|
cache.multiple = op_multiple(opoffs) * 2;
|
||||||
|
if (cache.multiple == 0)
|
||||||
|
cache.multiple = 1;
|
||||||
|
|
||||||
|
// phase step, or PHASE_STEP_DYNAMIC if PM is active; this depends on
|
||||||
|
// block_freq, detune, and multiple, so compute it after we've done those
|
||||||
|
if (lfo_pm_depth() == 0 || ch_lfo_pm_sens(choffs) == 0)
|
||||||
|
cache.phase_step = compute_phase_step(choffs, opoffs, cache, 0);
|
||||||
|
else
|
||||||
|
cache.phase_step = opdata_cache::PHASE_STEP_DYNAMIC;
|
||||||
|
|
||||||
|
// total level, scaled by 8
|
||||||
|
cache.total_level = op_total_level(opoffs) << 3;
|
||||||
|
|
||||||
|
// 4-bit sustain level, but 15 means 31 so effectively 5 bits
|
||||||
|
cache.eg_sustain = op_sustain_level(opoffs);
|
||||||
|
cache.eg_sustain |= (cache.eg_sustain + 1) & 0x10;
|
||||||
|
cache.eg_sustain <<= 5;
|
||||||
|
|
||||||
|
// determine KSR adjustment for enevlope rates
|
||||||
|
uint32_t ksrval = keycode >> (op_ksr(opoffs) ^ 3);
|
||||||
|
cache.eg_rate[EG_ATTACK] = effective_rate(op_attack_rate(opoffs) * 2, ksrval);
|
||||||
|
cache.eg_rate[EG_DECAY] = effective_rate(op_decay_rate(opoffs) * 2, ksrval);
|
||||||
|
cache.eg_rate[EG_SUSTAIN] = effective_rate(op_sustain_rate(opoffs) * 2, ksrval);
|
||||||
|
cache.eg_rate[EG_RELEASE] = effective_rate(op_release_rate(opoffs) * 4 + 2, ksrval);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// compute_phase_step - compute the phase step
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint32_t opm_registers::compute_phase_step(uint32_t choffs, uint32_t opoffs, opdata_cache const &cache, int32_t lfo_raw_pm)
|
||||||
|
{
|
||||||
|
// OPM logic is rather unique here, due to extra detune
|
||||||
|
// and the use of key codes (not to be confused with keycode)
|
||||||
|
|
||||||
|
// start with coarse detune delta; table uses cents value from
|
||||||
|
// manual, converted into 1/64ths
|
||||||
|
static const int16_t s_detune2_delta[4] = { 0, (600*64+50)/100, (781*64+50)/100, (950*64+50)/100 };
|
||||||
|
int32_t delta = s_detune2_delta[op_detune2(opoffs)];
|
||||||
|
|
||||||
|
// add in the PM delta
|
||||||
|
uint32_t pm_sensitivity = ch_lfo_pm_sens(choffs);
|
||||||
|
if (pm_sensitivity != 0)
|
||||||
|
{
|
||||||
|
// raw PM value is -127..128 which is +/- 200 cents
|
||||||
|
// manual gives these magnitudes in cents:
|
||||||
|
// 0, +/-5, +/-10, +/-20, +/-50, +/-100, +/-400, +/-700
|
||||||
|
// this roughly corresponds to shifting the 200-cent value:
|
||||||
|
// 0 >> 5, >> 4, >> 3, >> 2, >> 1, << 1, << 2
|
||||||
|
if (pm_sensitivity < 6)
|
||||||
|
delta += lfo_raw_pm >> (6 - pm_sensitivity);
|
||||||
|
else
|
||||||
|
delta += lfo_raw_pm << (pm_sensitivity - 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply delta and convert to a frequency number
|
||||||
|
uint32_t phase_step = opm_key_code_to_phase_step(cache.block_freq, delta);
|
||||||
|
|
||||||
|
// apply detune based on the keycode
|
||||||
|
phase_step += cache.detune;
|
||||||
|
|
||||||
|
// apply frequency multiplier (which is cached as an x.1 value)
|
||||||
|
return (phase_step * cache.multiple) >> 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// log_keyon - log a key-on event
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
std::string opm_registers::log_keyon(uint32_t choffs, uint32_t opoffs)
|
||||||
|
{
|
||||||
|
uint32_t chnum = choffs;
|
||||||
|
uint32_t opnum = opoffs;
|
||||||
|
|
||||||
|
char buffer[256];
|
||||||
|
char *end = &buffer[0];
|
||||||
|
|
||||||
|
end += sprintf(end, "%u.%02u freq=%04X dt2=%u dt=%u fb=%u alg=%X mul=%X tl=%02X ksr=%u adsr=%02X/%02X/%02X/%X sl=%X out=%c%c",
|
||||||
|
chnum, opnum,
|
||||||
|
ch_block_freq(choffs),
|
||||||
|
op_detune2(opoffs),
|
||||||
|
op_detune(opoffs),
|
||||||
|
ch_feedback(choffs),
|
||||||
|
ch_algorithm(choffs),
|
||||||
|
op_multiple(opoffs),
|
||||||
|
op_total_level(opoffs),
|
||||||
|
op_ksr(opoffs),
|
||||||
|
op_attack_rate(opoffs),
|
||||||
|
op_decay_rate(opoffs),
|
||||||
|
op_sustain_rate(opoffs),
|
||||||
|
op_release_rate(opoffs),
|
||||||
|
op_sustain_level(opoffs),
|
||||||
|
ch_output_0(choffs) ? 'L' : '-',
|
||||||
|
ch_output_1(choffs) ? 'R' : '-');
|
||||||
|
|
||||||
|
bool am = (lfo_am_depth() != 0 && ch_lfo_am_sens(choffs) != 0 && op_lfo_am_enable(opoffs) != 0);
|
||||||
|
if (am)
|
||||||
|
end += sprintf(end, " am=%u/%02X", ch_lfo_am_sens(choffs), lfo_am_depth());
|
||||||
|
bool pm = (lfo_pm_depth() != 0 && ch_lfo_pm_sens(choffs) != 0);
|
||||||
|
if (pm)
|
||||||
|
end += sprintf(end, " pm=%u/%02X", ch_lfo_pm_sens(choffs), lfo_pm_depth());
|
||||||
|
if (am || pm)
|
||||||
|
end += sprintf(end, " lfo=%02X/%c", lfo_rate(), "WQTN"[lfo_waveform()]);
|
||||||
|
if (noise_enable() && opoffs == 31)
|
||||||
|
end += sprintf(end, " noise=1");
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// YM2151
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// ym2151 - constructor
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
ym2151::ym2151(ymfm_interface &intf, opm_variant variant) :
|
||||||
|
m_variant(variant),
|
||||||
|
m_address(0),
|
||||||
|
m_fm(intf)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the system
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void ym2151::reset()
|
||||||
|
{
|
||||||
|
// reset the engines
|
||||||
|
m_fm.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void ym2151::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
m_fm.save_restore(state);
|
||||||
|
state.save_restore(m_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// read_status - read the status register
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint8_t ym2151::read_status()
|
||||||
|
{
|
||||||
|
uint8_t result = m_fm.status();
|
||||||
|
if (m_fm.intf().ymfm_is_busy())
|
||||||
|
result |= fm_engine::STATUS_BUSY;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// read - handle a read from the device
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint8_t ym2151::read(uint32_t offset)
|
||||||
|
{
|
||||||
|
uint8_t result = 0xff;
|
||||||
|
switch (offset & 1)
|
||||||
|
{
|
||||||
|
case 0: // data port (unused)
|
||||||
|
debug::log_unexpected_read_write("Unexpected read from YM2151 offset %d\n", offset & 3);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // status port, YM2203 compatible
|
||||||
|
result = read_status();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// write_address - handle a write to the address
|
||||||
|
// register
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void ym2151::write_address(uint8_t data)
|
||||||
|
{
|
||||||
|
// just set the address
|
||||||
|
m_address = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// write - handle a write to the register
|
||||||
|
// interface
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void ym2151::write_data(uint8_t data)
|
||||||
|
{
|
||||||
|
// write the FM register
|
||||||
|
m_fm.write(m_address, data);
|
||||||
|
|
||||||
|
// special cases
|
||||||
|
if (m_address == 0x1b)
|
||||||
|
{
|
||||||
|
// writes to register 0x1B send the upper 2 bits to the output lines
|
||||||
|
m_fm.intf().ymfm_external_write(ACCESS_IO, 0, data >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark busy for a bit
|
||||||
|
m_fm.intf().ymfm_set_busy_end(32 * m_fm.clock_prescale());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// write - handle a write to the register
|
||||||
|
// interface
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void ym2151::write(uint32_t offset, uint8_t data)
|
||||||
|
{
|
||||||
|
switch (offset & 1)
|
||||||
|
{
|
||||||
|
case 0: // address port
|
||||||
|
write_address(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // data port
|
||||||
|
write_data(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// generate - generate one sample of sound
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void ym2151::generate(output_data *output, uint32_t numsamples)
|
||||||
|
{
|
||||||
|
for (uint32_t samp = 0; samp < numsamples; samp++, output++)
|
||||||
|
{
|
||||||
|
// clock the system
|
||||||
|
m_fm.clock(fm_engine::ALL_CHANNELS);
|
||||||
|
|
||||||
|
// update the FM content; OPM is full 14-bit with no intermediate clipping
|
||||||
|
m_fm.output(output->clear(), 0, 32767, fm_engine::ALL_CHANNELS);
|
||||||
|
|
||||||
|
// YM2151 uses an external DAC (YM3012) with mantissa/exponent format
|
||||||
|
// convert to 10.3 floating point value and back to simulate truncation
|
||||||
|
output->roundtrip_fp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
322
backends/playback/zsm/x16emu/ymfm/src/ymfm_opm.h
Normal file
322
backends/playback/zsm/x16emu/ymfm/src/ymfm_opm.h
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#ifndef YMFM_OPM_H
|
||||||
|
#define YMFM_OPM_H
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ymfm.h"
|
||||||
|
#include "ymfm_fm.h"
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// REGISTER CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> opm_registers
|
||||||
|
|
||||||
|
//
|
||||||
|
// OPM register map:
|
||||||
|
//
|
||||||
|
// System-wide registers:
|
||||||
|
// 01 xxxxxx-x Test register
|
||||||
|
// ------x- LFO reset
|
||||||
|
// 08 -x------ Key on/off operator 4
|
||||||
|
// --x----- Key on/off operator 3
|
||||||
|
// ---x---- Key on/off operator 2
|
||||||
|
// ----x--- Key on/off operator 1
|
||||||
|
// -----xxx Channel select
|
||||||
|
// 0F x------- Noise enable
|
||||||
|
// ---xxxxx Noise frequency
|
||||||
|
// 10 xxxxxxxx Timer A value (upper 8 bits)
|
||||||
|
// 11 ------xx Timer A value (lower 2 bits)
|
||||||
|
// 12 xxxxxxxx Timer B value
|
||||||
|
// 14 x------- CSM mode
|
||||||
|
// --x----- Reset timer B
|
||||||
|
// ---x---- Reset timer A
|
||||||
|
// ----x--- Enable timer B
|
||||||
|
// -----x-- Enable timer A
|
||||||
|
// ------x- Load timer B
|
||||||
|
// -------x Load timer A
|
||||||
|
// 18 xxxxxxxx LFO frequency
|
||||||
|
// 19 0xxxxxxx AM LFO depth
|
||||||
|
// 1xxxxxxx PM LFO depth
|
||||||
|
// 1B xx------ CT (2 output data lines)
|
||||||
|
// ------xx LFO waveform
|
||||||
|
//
|
||||||
|
// Per-channel registers (channel in address bits 0-2)
|
||||||
|
// 20-27 x------- Pan right
|
||||||
|
// -x------ Pan left
|
||||||
|
// --xxx--- Feedback level for operator 1 (0-7)
|
||||||
|
// -----xxx Operator connection algorithm (0-7)
|
||||||
|
// 28-2F -xxxxxxx Key code
|
||||||
|
// 30-37 xxxxxx-- Key fraction
|
||||||
|
// 38-3F -xxx---- LFO PM sensitivity
|
||||||
|
// ------xx LFO AM shift
|
||||||
|
//
|
||||||
|
// Per-operator registers (channel in address bits 0-2, operator in bits 3-4)
|
||||||
|
// 40-5F -xxx---- Detune value (0-7)
|
||||||
|
// ----xxxx Multiple value (0-15)
|
||||||
|
// 60-7F -xxxxxxx Total level (0-127)
|
||||||
|
// 80-9F xx------ Key scale rate (0-3)
|
||||||
|
// ---xxxxx Attack rate (0-31)
|
||||||
|
// A0-BF x------- LFO AM enable
|
||||||
|
// ---xxxxx Decay rate (0-31)
|
||||||
|
// C0-DF xx------ Detune 2 value (0-3)
|
||||||
|
// ---xxxxx Sustain rate (0-31)
|
||||||
|
// E0-FF xxxx---- Sustain level (0-15)
|
||||||
|
// ----xxxx Release rate (0-15)
|
||||||
|
//
|
||||||
|
// Internal (fake) registers:
|
||||||
|
// 1A -xxxxxxx PM depth
|
||||||
|
//
|
||||||
|
|
||||||
|
class opm_registers : public fm_registers_base
|
||||||
|
{
|
||||||
|
// LFO waveforms are 256 entries long
|
||||||
|
static constexpr uint32_t LFO_WAVEFORM_LENGTH = 256;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// constants
|
||||||
|
static constexpr uint32_t OUTPUTS = 2;
|
||||||
|
static constexpr uint32_t CHANNELS = 8;
|
||||||
|
static constexpr uint32_t ALL_CHANNELS = (1 << CHANNELS) - 1;
|
||||||
|
static constexpr uint32_t OPERATORS = CHANNELS * 4;
|
||||||
|
static constexpr uint32_t WAVEFORMS = 1;
|
||||||
|
static constexpr uint32_t REGISTERS = 0x100;
|
||||||
|
static constexpr uint32_t DEFAULT_PRESCALE = 2;
|
||||||
|
static constexpr uint32_t EG_CLOCK_DIVIDER = 3;
|
||||||
|
static constexpr uint32_t CSM_TRIGGER_MASK = ALL_CHANNELS;
|
||||||
|
static constexpr uint32_t REG_MODE = 0x14;
|
||||||
|
static constexpr uint8_t STATUS_TIMERA = 0x01;
|
||||||
|
static constexpr uint8_t STATUS_TIMERB = 0x02;
|
||||||
|
static constexpr uint8_t STATUS_BUSY = 0x80;
|
||||||
|
static constexpr uint8_t STATUS_IRQ = 0;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
opm_registers();
|
||||||
|
|
||||||
|
// reset to initial state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// map channel number to register offset
|
||||||
|
static constexpr uint32_t channel_offset(uint32_t chnum)
|
||||||
|
{
|
||||||
|
assert(chnum < CHANNELS);
|
||||||
|
return chnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// map operator number to register offset
|
||||||
|
static constexpr uint32_t operator_offset(uint32_t opnum)
|
||||||
|
{
|
||||||
|
assert(opnum < OPERATORS);
|
||||||
|
return opnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an array of operator indices for each channel
|
||||||
|
struct operator_mapping { uint32_t chan[CHANNELS]; };
|
||||||
|
void operator_map(operator_mapping &dest) const;
|
||||||
|
|
||||||
|
// handle writes to the register array
|
||||||
|
bool write(uint16_t index, uint8_t data, uint32_t &chan, uint32_t &opmask);
|
||||||
|
|
||||||
|
// clock the noise and LFO, if present, returning LFO PM value
|
||||||
|
int32_t clock_noise_and_lfo();
|
||||||
|
|
||||||
|
// return the AM offset from LFO for the given channel
|
||||||
|
uint32_t lfo_am_offset(uint32_t choffs) const;
|
||||||
|
|
||||||
|
// return the current noise state, gated by the noise clock
|
||||||
|
uint32_t noise_state() const { return m_noise_state; }
|
||||||
|
|
||||||
|
// caching helpers
|
||||||
|
void cache_operator_data(uint32_t choffs, uint32_t opoffs, opdata_cache &cache);
|
||||||
|
|
||||||
|
// compute the phase step, given a PM value
|
||||||
|
uint32_t compute_phase_step(uint32_t choffs, uint32_t opoffs, opdata_cache const &cache, int32_t lfo_raw_pm);
|
||||||
|
|
||||||
|
// log a key-on event
|
||||||
|
std::string log_keyon(uint32_t choffs, uint32_t opoffs);
|
||||||
|
|
||||||
|
// system-wide registers
|
||||||
|
uint32_t test() const { return byte(0x01, 0, 8); }
|
||||||
|
uint32_t lfo_reset() const { return byte(0x01, 1, 1); }
|
||||||
|
uint32_t noise_frequency() const { return byte(0x0f, 0, 5) ^ 0x1f; }
|
||||||
|
uint32_t noise_enable() const { return byte(0x0f, 7, 1); }
|
||||||
|
uint32_t timer_a_value() const { return word(0x10, 0, 8, 0x11, 0, 2); }
|
||||||
|
uint32_t timer_b_value() const { return byte(0x12, 0, 8); }
|
||||||
|
uint32_t csm() const { return byte(0x14, 7, 1); }
|
||||||
|
uint32_t reset_timer_b() const { return byte(0x14, 5, 1); }
|
||||||
|
uint32_t reset_timer_a() const { return byte(0x14, 4, 1); }
|
||||||
|
uint32_t enable_timer_b() const { return byte(0x14, 3, 1); }
|
||||||
|
uint32_t enable_timer_a() const { return byte(0x14, 2, 1); }
|
||||||
|
uint32_t load_timer_b() const { return byte(0x14, 1, 1); }
|
||||||
|
uint32_t load_timer_a() const { return byte(0x14, 0, 1); }
|
||||||
|
uint32_t lfo_rate() const { return byte(0x18, 0, 8); }
|
||||||
|
uint32_t lfo_am_depth() const { return byte(0x19, 0, 7); }
|
||||||
|
uint32_t lfo_pm_depth() const { return byte(0x1a, 0, 7); }
|
||||||
|
uint32_t output_bits() const { return byte(0x1b, 6, 2); }
|
||||||
|
uint32_t lfo_waveform() const { return byte(0x1b, 0, 2); }
|
||||||
|
|
||||||
|
// per-channel registers
|
||||||
|
uint32_t ch_output_any(uint32_t choffs) const { return byte(0x20, 6, 2, choffs); }
|
||||||
|
uint32_t ch_output_0(uint32_t choffs) const { return byte(0x20, 6, 1, choffs); }
|
||||||
|
uint32_t ch_output_1(uint32_t choffs) const { return byte(0x20, 7, 1, choffs); }
|
||||||
|
uint32_t ch_output_2(uint32_t choffs) const { return 0; }
|
||||||
|
uint32_t ch_output_3(uint32_t choffs) const { return 0; }
|
||||||
|
uint32_t ch_feedback(uint32_t choffs) const { return byte(0x20, 3, 3, choffs); }
|
||||||
|
uint32_t ch_algorithm(uint32_t choffs) const { return byte(0x20, 0, 3, choffs); }
|
||||||
|
uint32_t ch_block_freq(uint32_t choffs) const { return word(0x28, 0, 7, 0x30, 2, 6, choffs); }
|
||||||
|
uint32_t ch_lfo_pm_sens(uint32_t choffs) const { return byte(0x38, 4, 3, choffs); }
|
||||||
|
uint32_t ch_lfo_am_sens(uint32_t choffs) const { return byte(0x38, 0, 2, choffs); }
|
||||||
|
|
||||||
|
// per-operator registers
|
||||||
|
uint32_t op_detune(uint32_t opoffs) const { return byte(0x40, 4, 3, opoffs); }
|
||||||
|
uint32_t op_multiple(uint32_t opoffs) const { return byte(0x40, 0, 4, opoffs); }
|
||||||
|
uint32_t op_total_level(uint32_t opoffs) const { return byte(0x60, 0, 7, opoffs); }
|
||||||
|
uint32_t op_ksr(uint32_t opoffs) const { return byte(0x80, 6, 2, opoffs); }
|
||||||
|
uint32_t op_attack_rate(uint32_t opoffs) const { return byte(0x80, 0, 5, opoffs); }
|
||||||
|
uint32_t op_lfo_am_enable(uint32_t opoffs) const { return byte(0xa0, 7, 1, opoffs); }
|
||||||
|
uint32_t op_decay_rate(uint32_t opoffs) const { return byte(0xa0, 0, 5, opoffs); }
|
||||||
|
uint32_t op_detune2(uint32_t opoffs) const { return byte(0xc0, 6, 2, opoffs); }
|
||||||
|
uint32_t op_sustain_rate(uint32_t opoffs) const { return byte(0xc0, 0, 5, opoffs); }
|
||||||
|
uint32_t op_sustain_level(uint32_t opoffs) const { return byte(0xe0, 4, 4, opoffs); }
|
||||||
|
uint32_t op_release_rate(uint32_t opoffs) const { return byte(0xe0, 0, 4, opoffs); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// return a bitfield extracted from a byte
|
||||||
|
uint32_t byte(uint32_t offset, uint32_t start, uint32_t count, uint32_t extra_offset = 0) const
|
||||||
|
{
|
||||||
|
return bitfield(m_regdata[offset + extra_offset], start, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a bitfield extracted from a pair of bytes, MSBs listed first
|
||||||
|
uint32_t word(uint32_t offset1, uint32_t start1, uint32_t count1, uint32_t offset2, uint32_t start2, uint32_t count2, uint32_t extra_offset = 0) const
|
||||||
|
{
|
||||||
|
return (byte(offset1, start1, count1, extra_offset) << count2) | byte(offset2, start2, count2, extra_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint32_t m_lfo_counter; // LFO counter
|
||||||
|
uint32_t m_noise_lfsr; // noise LFSR state
|
||||||
|
uint8_t m_noise_counter; // noise counter
|
||||||
|
uint8_t m_noise_state; // latched noise state
|
||||||
|
uint8_t m_noise_lfo; // latched LFO noise value
|
||||||
|
uint8_t m_lfo_am; // current LFO AM value
|
||||||
|
uint8_t m_regdata[REGISTERS]; // register data
|
||||||
|
int16_t m_lfo_waveform[4][LFO_WAVEFORM_LENGTH]; // LFO waveforms; AM in low 8, PM in upper 8
|
||||||
|
uint16_t m_waveform[WAVEFORMS][WAVEFORM_LENGTH]; // waveforms
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// OPM IMPLEMENTATION CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> ym2151
|
||||||
|
|
||||||
|
class ym2151
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using fm_engine = fm_engine_base<opm_registers>;
|
||||||
|
using output_data = fm_engine::output_data;
|
||||||
|
static constexpr uint32_t OUTPUTS = fm_engine::OUTPUTS;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
ym2151(ymfm_interface &intf) : ym2151(intf, VARIANT_YM2151) { }
|
||||||
|
|
||||||
|
// reset
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// pass-through helpers
|
||||||
|
uint32_t sample_rate(uint32_t input_clock) const { return m_fm.sample_rate(input_clock); }
|
||||||
|
void invalidate_caches() { m_fm.invalidate_caches(); }
|
||||||
|
|
||||||
|
// read access
|
||||||
|
uint8_t read_status();
|
||||||
|
uint8_t read(uint32_t offset);
|
||||||
|
|
||||||
|
// write access
|
||||||
|
void write_address(uint8_t data);
|
||||||
|
void write_data(uint8_t data);
|
||||||
|
void write(uint32_t offset, uint8_t data);
|
||||||
|
|
||||||
|
// generate one sample of sound
|
||||||
|
void generate(output_data *output, uint32_t numsamples = 1);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// variants
|
||||||
|
enum opm_variant
|
||||||
|
{
|
||||||
|
VARIANT_YM2151,
|
||||||
|
VARIANT_YM2164
|
||||||
|
};
|
||||||
|
|
||||||
|
// internal constructor
|
||||||
|
ym2151(ymfm_interface &intf, opm_variant variant);
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
opm_variant m_variant; // chip variant
|
||||||
|
uint8_t m_address; // address register
|
||||||
|
fm_engine m_fm; // core FM engine
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// OPP IMPLEMENTATION CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
// ======================> ym2164
|
||||||
|
|
||||||
|
// the YM2164 is almost 100% functionally identical to the YM2151, except
|
||||||
|
// it apparently has some mystery registers in the 00-07 range, and timer
|
||||||
|
// B's frequency is half that of the 2151
|
||||||
|
class ym2164 : public ym2151
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constructor
|
||||||
|
ym2164(ymfm_interface &intf) : ym2151(intf, VARIANT_YM2164) { }
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif // YMFM_OPM_H
|
714
backends/playback/zsm/x16emu/ymfm/src/ymfm_pcm.cpp
Normal file
714
backends/playback/zsm/x16emu/ymfm/src/ymfm_pcm.cpp
Normal file
|
@ -0,0 +1,714 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#include "ymfm_pcm.h"
|
||||||
|
#include "ymfm_fm.h"
|
||||||
|
#include "ymfm_fm.ipp"
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// PCM REGISTERS
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the register state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_registers::reset()
|
||||||
|
{
|
||||||
|
std::fill_n(&m_regdata[0], REGISTERS, 0);
|
||||||
|
m_regdata[0xf8] = 0x1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_registers::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
state.save_restore(m_regdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// cache_channel_data - update the cache with
|
||||||
|
// data from the registers
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_registers::cache_channel_data(uint32_t choffs, pcm_cache &cache)
|
||||||
|
{
|
||||||
|
// compute step from octave and fnumber; the math here implies
|
||||||
|
// a .18 fraction but .16 should be perfectly fine
|
||||||
|
int32_t octave = int8_t(ch_octave(choffs) << 4) >> 4;
|
||||||
|
uint32_t fnum = ch_fnumber(choffs);
|
||||||
|
cache.step = ((0x400 | fnum) << (octave + 7)) >> 2;
|
||||||
|
|
||||||
|
// total level is computed as a .10 value for interpolation
|
||||||
|
cache.total_level = ch_total_level(choffs) << 10;
|
||||||
|
|
||||||
|
// compute panning values in terms of envelope attenuation
|
||||||
|
int32_t panpot = int8_t(ch_panpot(choffs) << 4) >> 4;
|
||||||
|
if (panpot >= 0)
|
||||||
|
{
|
||||||
|
cache.pan_left = (panpot == 7) ? 0x3ff : 0x20 * panpot;
|
||||||
|
cache.pan_right = 0;
|
||||||
|
}
|
||||||
|
else if (panpot >= -7)
|
||||||
|
{
|
||||||
|
cache.pan_left = 0;
|
||||||
|
cache.pan_right = (panpot == -7) ? 0x3ff : -0x20 * panpot;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
cache.pan_left = cache.pan_right = 0x3ff;
|
||||||
|
|
||||||
|
// determine the LFO stepping value; this how much to add to a running
|
||||||
|
// x.18 value for the LFO; steps were derived from frequencies in the
|
||||||
|
// manual and come out very close with these values
|
||||||
|
static const uint8_t s_lfo_steps[8] = { 1, 12, 19, 25, 31, 35, 37, 42 };
|
||||||
|
cache.lfo_step = s_lfo_steps[ch_lfo_speed(choffs)];
|
||||||
|
|
||||||
|
// AM LFO depth values, derived from the manual; note each has at most
|
||||||
|
// 2 bits to make the "multiply" easy in hardware
|
||||||
|
static const uint8_t s_am_depth[8] = { 0, 0x14, 0x20, 0x28, 0x30, 0x40, 0x50, 0x80 };
|
||||||
|
cache.am_depth = s_am_depth[ch_am_depth(choffs)];
|
||||||
|
|
||||||
|
// PM LFO depth values; these are converted from the manual's cents values
|
||||||
|
// into f-numbers; the computations come out quite cleanly so pretty sure
|
||||||
|
// these are correct
|
||||||
|
static const uint8_t s_pm_depth[8] = { 0, 2, 3, 4, 6, 12, 24, 48 };
|
||||||
|
cache.pm_depth = s_pm_depth[ch_vibrato(choffs)];
|
||||||
|
|
||||||
|
// 4-bit sustain level, but 15 means 31 so effectively 5 bits
|
||||||
|
cache.eg_sustain = ch_sustain_level(choffs);
|
||||||
|
cache.eg_sustain |= (cache.eg_sustain + 1) & 0x10;
|
||||||
|
cache.eg_sustain <<= 5;
|
||||||
|
|
||||||
|
// compute the key scaling correction factor; 15 means don't do any correction
|
||||||
|
int32_t correction = ch_rate_correction(choffs);
|
||||||
|
if (correction == 15)
|
||||||
|
correction = 0;
|
||||||
|
else
|
||||||
|
correction = (octave + correction) * 2 + bitfield(fnum, 9);
|
||||||
|
|
||||||
|
// compute the envelope generator rates
|
||||||
|
cache.eg_rate[EG_ATTACK] = effective_rate(ch_attack_rate(choffs), correction);
|
||||||
|
cache.eg_rate[EG_DECAY] = effective_rate(ch_decay_rate(choffs), correction);
|
||||||
|
cache.eg_rate[EG_SUSTAIN] = effective_rate(ch_sustain_rate(choffs), correction);
|
||||||
|
cache.eg_rate[EG_RELEASE] = effective_rate(ch_release_rate(choffs), correction);
|
||||||
|
cache.eg_rate[EG_REVERB] = 5;
|
||||||
|
|
||||||
|
// if damping is on, override some things; essentially decay at a hardcoded
|
||||||
|
// rate of 48 until -12db (0x80), then at maximum rate for the rest
|
||||||
|
if (ch_damp(choffs) != 0)
|
||||||
|
{
|
||||||
|
cache.eg_rate[EG_DECAY] = 48;
|
||||||
|
cache.eg_rate[EG_SUSTAIN] = 63;
|
||||||
|
cache.eg_rate[EG_RELEASE] = 63;
|
||||||
|
cache.eg_sustain = 0x80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// effective_rate - return the effective rate,
|
||||||
|
// clamping and applying corrections as needed
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint32_t pcm_registers::effective_rate(uint32_t raw, uint32_t correction)
|
||||||
|
{
|
||||||
|
// raw rates of 0 and 15 just pin to min/max
|
||||||
|
if (raw == 0)
|
||||||
|
return 0;
|
||||||
|
if (raw == 15)
|
||||||
|
return 63;
|
||||||
|
|
||||||
|
// otherwise add the correction and clamp to range
|
||||||
|
return clamp(raw * 4 + correction, 0, 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// PCM CHANNEL
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// pcm_channel - constructor
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
pcm_channel::pcm_channel(pcm_engine &owner, uint32_t choffs) :
|
||||||
|
m_choffs(choffs),
|
||||||
|
m_baseaddr(0),
|
||||||
|
m_endpos(0),
|
||||||
|
m_looppos(0),
|
||||||
|
m_curpos(0),
|
||||||
|
m_nextpos(0),
|
||||||
|
m_lfo_counter(0),
|
||||||
|
m_eg_state(EG_RELEASE),
|
||||||
|
m_env_attenuation(0x3ff),
|
||||||
|
m_total_level(0x7f << 10),
|
||||||
|
m_format(0),
|
||||||
|
m_key_state(0),
|
||||||
|
m_regs(owner.regs()),
|
||||||
|
m_owner(owner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the channel state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::reset()
|
||||||
|
{
|
||||||
|
m_baseaddr = 0;
|
||||||
|
m_endpos = 0;
|
||||||
|
m_looppos = 0;
|
||||||
|
m_curpos = 0;
|
||||||
|
m_nextpos = 0;
|
||||||
|
m_lfo_counter = 0;
|
||||||
|
m_eg_state = EG_RELEASE;
|
||||||
|
m_env_attenuation = 0x3ff;
|
||||||
|
m_total_level = 0x7f << 10;
|
||||||
|
m_format = 0;
|
||||||
|
m_key_state = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
state.save_restore(m_baseaddr);
|
||||||
|
state.save_restore(m_endpos);
|
||||||
|
state.save_restore(m_looppos);
|
||||||
|
state.save_restore(m_curpos);
|
||||||
|
state.save_restore(m_nextpos);
|
||||||
|
state.save_restore(m_lfo_counter);
|
||||||
|
state.save_restore(m_eg_state);
|
||||||
|
state.save_restore(m_env_attenuation);
|
||||||
|
state.save_restore(m_total_level);
|
||||||
|
state.save_restore(m_format);
|
||||||
|
state.save_restore(m_key_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// prepare - prepare for clocking
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
bool pcm_channel::prepare()
|
||||||
|
{
|
||||||
|
// cache the data
|
||||||
|
m_regs.cache_channel_data(m_choffs, m_cache);
|
||||||
|
|
||||||
|
// clock the key state
|
||||||
|
if ((m_key_state & KEY_PENDING) != 0)
|
||||||
|
{
|
||||||
|
uint8_t oldstate = m_key_state;
|
||||||
|
m_key_state = (m_key_state >> 1) & KEY_ON;
|
||||||
|
if (((oldstate ^ m_key_state) & KEY_ON) != 0)
|
||||||
|
{
|
||||||
|
if ((m_key_state & KEY_ON) != 0)
|
||||||
|
start_attack();
|
||||||
|
else
|
||||||
|
start_release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the total level directly if not interpolating
|
||||||
|
if (m_regs.ch_level_direct(m_choffs))
|
||||||
|
m_total_level = m_cache.total_level;
|
||||||
|
|
||||||
|
// we're active until we're quiet after the release
|
||||||
|
return (m_eg_state < EG_RELEASE || m_env_attenuation < EG_QUIET);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clock - master clocking function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::clock(uint32_t env_counter)
|
||||||
|
{
|
||||||
|
// clock the LFO, which is an x.18 value incremented based on the
|
||||||
|
// LFO speed value
|
||||||
|
m_lfo_counter += m_cache.lfo_step;
|
||||||
|
|
||||||
|
// clock the envelope
|
||||||
|
clock_envelope(env_counter);
|
||||||
|
|
||||||
|
// determine the step after applying vibrato
|
||||||
|
uint32_t step = m_cache.step;
|
||||||
|
if (m_cache.pm_depth != 0)
|
||||||
|
{
|
||||||
|
// shift the LFO by 1/4 cycle for PM so that it starts at 0
|
||||||
|
uint32_t lfo_shifted = m_lfo_counter + (1 << 16);
|
||||||
|
int32_t lfo_value = bitfield(lfo_shifted, 10, 7);
|
||||||
|
if (bitfield(lfo_shifted, 17) != 0)
|
||||||
|
lfo_value ^= 0x7f;
|
||||||
|
lfo_value -= 0x40;
|
||||||
|
step += (lfo_value * int32_t(m_cache.pm_depth)) >> 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// advance the sample step and loop as needed
|
||||||
|
m_curpos = m_nextpos;
|
||||||
|
m_nextpos = m_curpos + step;
|
||||||
|
if (m_nextpos >= m_endpos)
|
||||||
|
m_nextpos += m_looppos - m_endpos;
|
||||||
|
|
||||||
|
// interpolate total level if needed
|
||||||
|
if (m_total_level != m_cache.total_level)
|
||||||
|
{
|
||||||
|
// max->min volume takes 156.4ms, or pretty close to 19/1024 per 44.1kHz sample
|
||||||
|
// min->max volume is half that, so advance by 38/1024 per sample
|
||||||
|
if (m_total_level < m_cache.total_level)
|
||||||
|
m_total_level = std::min<int32_t>(m_total_level + 19, m_cache.total_level);
|
||||||
|
else
|
||||||
|
m_total_level = std::max<int32_t>(m_total_level - 38, m_cache.total_level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// output - return the computed output value, with
|
||||||
|
// panning applied
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::output(output_data &output) const
|
||||||
|
{
|
||||||
|
// early out if the envelope is effectively off
|
||||||
|
uint32_t envelope = m_env_attenuation;
|
||||||
|
if (envelope > EG_QUIET)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// add in LFO AM modulation
|
||||||
|
if (m_cache.am_depth != 0)
|
||||||
|
{
|
||||||
|
uint32_t lfo_value = bitfield(m_lfo_counter, 10, 7);
|
||||||
|
if (bitfield(m_lfo_counter, 17) != 0)
|
||||||
|
lfo_value ^= 0x7f;
|
||||||
|
envelope += (lfo_value * m_cache.am_depth) >> 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add in the current interpolated total level value, which is a .10
|
||||||
|
// value shifted left by 2
|
||||||
|
envelope += m_total_level >> 8;
|
||||||
|
|
||||||
|
// add in panning effect and clamp
|
||||||
|
uint32_t lenv = std::min<uint32_t>(envelope + m_cache.pan_left, 0x3ff);
|
||||||
|
uint32_t renv = std::min<uint32_t>(envelope + m_cache.pan_right, 0x3ff);
|
||||||
|
|
||||||
|
// convert to volume as a .11 fraction
|
||||||
|
int32_t lvol = attenuation_to_volume(lenv << 2);
|
||||||
|
int32_t rvol = attenuation_to_volume(renv << 2);
|
||||||
|
|
||||||
|
// fetch current sample and add
|
||||||
|
int16_t sample = fetch_sample();
|
||||||
|
uint32_t outnum = m_regs.ch_output_channel(m_choffs) * 2;
|
||||||
|
output.data[outnum + 0] += (lvol * sample) >> 15;
|
||||||
|
output.data[outnum + 1] += (rvol * sample) >> 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// keyonoff - signal key on/off
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::keyonoff(bool on)
|
||||||
|
{
|
||||||
|
// mark the key state as pending
|
||||||
|
m_key_state |= KEY_PENDING | (on ? KEY_PENDING_ON : 0);
|
||||||
|
|
||||||
|
// don't log masked channels
|
||||||
|
if ((m_key_state & (KEY_PENDING_ON | KEY_ON)) == KEY_PENDING_ON && ((debug::GLOBAL_PCM_CHANNEL_MASK >> m_choffs) & 1) != 0)
|
||||||
|
{
|
||||||
|
debug::log_keyon("KeyOn PCM-%02d: num=%3d oct=%2d fnum=%03X level=%02X%c ADSR=%X/%X/%X/%X SL=%X",
|
||||||
|
m_choffs,
|
||||||
|
m_regs.ch_wave_table_num(m_choffs),
|
||||||
|
int8_t(m_regs.ch_octave(m_choffs) << 4) >> 4,
|
||||||
|
m_regs.ch_fnumber(m_choffs),
|
||||||
|
m_regs.ch_total_level(m_choffs),
|
||||||
|
m_regs.ch_level_direct(m_choffs) ? '!' : '/',
|
||||||
|
m_regs.ch_attack_rate(m_choffs),
|
||||||
|
m_regs.ch_decay_rate(m_choffs),
|
||||||
|
m_regs.ch_sustain_rate(m_choffs),
|
||||||
|
m_regs.ch_release_rate(m_choffs),
|
||||||
|
m_regs.ch_sustain_level(m_choffs));
|
||||||
|
|
||||||
|
if (m_regs.ch_rate_correction(m_choffs) != 15)
|
||||||
|
debug::log_keyon(" RC=%X", m_regs.ch_rate_correction(m_choffs));
|
||||||
|
|
||||||
|
if (m_regs.ch_pseudo_reverb(m_choffs) != 0)
|
||||||
|
debug::log_keyon(" %s", "REV");
|
||||||
|
if (m_regs.ch_damp(m_choffs) != 0)
|
||||||
|
debug::log_keyon(" %s", "DAMP");
|
||||||
|
|
||||||
|
if (m_regs.ch_vibrato(m_choffs) != 0 || m_regs.ch_am_depth(m_choffs) != 0)
|
||||||
|
{
|
||||||
|
if (m_regs.ch_vibrato(m_choffs) != 0)
|
||||||
|
debug::log_keyon(" VIB=%d", m_regs.ch_vibrato(m_choffs));
|
||||||
|
if (m_regs.ch_am_depth(m_choffs) != 0)
|
||||||
|
debug::log_keyon(" AM=%d", m_regs.ch_am_depth(m_choffs));
|
||||||
|
debug::log_keyon(" LFO=%d", m_regs.ch_lfo_speed(m_choffs));
|
||||||
|
}
|
||||||
|
debug::log_keyon("%s", "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// load_wavetable - load a wavetable by fetching
|
||||||
|
// its data from external memory
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::load_wavetable()
|
||||||
|
{
|
||||||
|
// determine the address of the wave table header
|
||||||
|
uint32_t wavnum = m_regs.ch_wave_table_num(m_choffs);
|
||||||
|
uint32_t wavheader = 12 * wavnum;
|
||||||
|
|
||||||
|
// above 384 it may be in a different bank
|
||||||
|
if (wavnum >= 384)
|
||||||
|
{
|
||||||
|
uint32_t bank = m_regs.wave_table_header();
|
||||||
|
if (bank != 0)
|
||||||
|
wavheader = 512*1024 * bank + (wavnum - 384) * 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the 22-bit base address and 2-bit format
|
||||||
|
uint8_t byte = read_pcm(wavheader + 0);
|
||||||
|
m_format = bitfield(byte, 6, 2);
|
||||||
|
m_baseaddr = bitfield(byte, 0, 6) << 16;
|
||||||
|
m_baseaddr |= read_pcm(wavheader + 1) << 8;
|
||||||
|
m_baseaddr |= read_pcm(wavheader + 2) << 0;
|
||||||
|
|
||||||
|
// fetch the 16-bit loop position
|
||||||
|
m_looppos = read_pcm(wavheader + 3) << 8;
|
||||||
|
m_looppos |= read_pcm(wavheader + 4);
|
||||||
|
m_looppos <<= 16;
|
||||||
|
|
||||||
|
// fetch the 16-bit end position, which is stored as a negative value
|
||||||
|
// for some reason that is unclear
|
||||||
|
m_endpos = read_pcm(wavheader + 5) << 8;
|
||||||
|
m_endpos |= read_pcm(wavheader + 6);
|
||||||
|
m_endpos = -int32_t(m_endpos) << 16;
|
||||||
|
|
||||||
|
// remaining data values set registers
|
||||||
|
m_owner.write(0x80 + m_choffs, read_pcm(wavheader + 7));
|
||||||
|
m_owner.write(0x98 + m_choffs, read_pcm(wavheader + 8));
|
||||||
|
m_owner.write(0xb0 + m_choffs, read_pcm(wavheader + 9));
|
||||||
|
m_owner.write(0xc8 + m_choffs, read_pcm(wavheader + 10));
|
||||||
|
m_owner.write(0xe0 + m_choffs, read_pcm(wavheader + 11));
|
||||||
|
|
||||||
|
// reset the envelope so we don't continue playing mid-sample from previous key ons
|
||||||
|
m_env_attenuation = 0x3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// read_pcm - read a byte from the external PCM
|
||||||
|
// memory interface
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint8_t pcm_channel::read_pcm(uint32_t address) const
|
||||||
|
{
|
||||||
|
return m_owner.intf().ymfm_external_read(ACCESS_PCM, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// start_attack - start the attack phase
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::start_attack()
|
||||||
|
{
|
||||||
|
// don't change anything if already in attack state
|
||||||
|
if (m_eg_state == EG_ATTACK)
|
||||||
|
return;
|
||||||
|
m_eg_state = EG_ATTACK;
|
||||||
|
|
||||||
|
// reset the LFO if requested
|
||||||
|
if (m_regs.ch_lfo_reset(m_choffs))
|
||||||
|
m_lfo_counter = 0;
|
||||||
|
|
||||||
|
// if the attack rate == 63 then immediately go to max attenuation
|
||||||
|
if (m_cache.eg_rate[EG_ATTACK] == 63)
|
||||||
|
m_env_attenuation = 0;
|
||||||
|
|
||||||
|
// reset the positions
|
||||||
|
m_curpos = m_nextpos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// start_release - start the release phase
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::start_release()
|
||||||
|
{
|
||||||
|
// don't change anything if already in release or reverb state
|
||||||
|
if (m_eg_state >= EG_RELEASE)
|
||||||
|
return;
|
||||||
|
m_eg_state = EG_RELEASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clock_envelope - clock the envelope generator
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_channel::clock_envelope(uint32_t env_counter)
|
||||||
|
{
|
||||||
|
// handle attack->decay transitions
|
||||||
|
if (m_eg_state == EG_ATTACK && m_env_attenuation == 0)
|
||||||
|
m_eg_state = EG_DECAY;
|
||||||
|
|
||||||
|
// handle decay->sustain transitions
|
||||||
|
if (m_eg_state == EG_DECAY && m_env_attenuation >= m_cache.eg_sustain)
|
||||||
|
m_eg_state = EG_SUSTAIN;
|
||||||
|
|
||||||
|
// fetch the appropriate 6-bit rate value from the cache
|
||||||
|
uint32_t rate = m_cache.eg_rate[m_eg_state];
|
||||||
|
|
||||||
|
// compute the rate shift value; this is the shift needed to
|
||||||
|
// apply to the env_counter such that it becomes a 5.11 fixed
|
||||||
|
// point number
|
||||||
|
uint32_t rate_shift = rate >> 2;
|
||||||
|
env_counter <<= rate_shift;
|
||||||
|
|
||||||
|
// see if the fractional part is 0; if not, it's not time to clock
|
||||||
|
if (bitfield(env_counter, 0, 11) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// determine the increment based on the non-fractional part of env_counter
|
||||||
|
uint32_t relevant_bits = bitfield(env_counter, (rate_shift <= 11) ? 11 : rate_shift, 3);
|
||||||
|
uint32_t increment = attenuation_increment(rate, relevant_bits);
|
||||||
|
|
||||||
|
// attack is the only one that increases
|
||||||
|
if (m_eg_state == EG_ATTACK)
|
||||||
|
m_env_attenuation += (~m_env_attenuation * increment) >> 4;
|
||||||
|
|
||||||
|
// all other cases are similar
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// apply the increment
|
||||||
|
m_env_attenuation += increment;
|
||||||
|
|
||||||
|
// clamp the final attenuation
|
||||||
|
if (m_env_attenuation >= 0x400)
|
||||||
|
m_env_attenuation = 0x3ff;
|
||||||
|
|
||||||
|
// transition to reverb at -18dB if enabled
|
||||||
|
if (m_env_attenuation >= 0xc0 && m_eg_state < EG_REVERB && m_regs.ch_pseudo_reverb(m_choffs))
|
||||||
|
m_eg_state = EG_REVERB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// fetch_sample - fetch a sample at the current
|
||||||
|
// position
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
int16_t pcm_channel::fetch_sample() const
|
||||||
|
{
|
||||||
|
uint32_t addr = m_baseaddr;
|
||||||
|
uint32_t pos = m_curpos >> 16;
|
||||||
|
|
||||||
|
// 8-bit PCM: shift up by 8
|
||||||
|
if (m_format == 0)
|
||||||
|
return read_pcm(addr + pos) << 8;
|
||||||
|
|
||||||
|
// 16-bit PCM: assemble from 2 halves
|
||||||
|
if (m_format == 2)
|
||||||
|
{
|
||||||
|
addr += pos * 2;
|
||||||
|
return (read_pcm(addr) << 8) | read_pcm(addr + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12-bit PCM: assemble out of half of 3 bytes
|
||||||
|
addr += (pos / 2) * 3;
|
||||||
|
if ((pos & 1) == 0)
|
||||||
|
return (read_pcm(addr + 0) << 8) | ((read_pcm(addr + 1) << 4) & 0xf0);
|
||||||
|
else
|
||||||
|
return (read_pcm(addr + 2) << 8) | ((read_pcm(addr + 1) << 0) & 0xf0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// PCM ENGINE
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// pcm_engine - constructor
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
pcm_engine::pcm_engine(ymfm_interface &intf) :
|
||||||
|
m_intf(intf),
|
||||||
|
m_env_counter(0),
|
||||||
|
m_modified_channels(ALL_CHANNELS),
|
||||||
|
m_active_channels(ALL_CHANNELS)
|
||||||
|
{
|
||||||
|
// create the channels
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
m_channel[chnum] = std::make_unique<pcm_channel>(*this, chnum);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// reset - reset the engine state
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_engine::reset()
|
||||||
|
{
|
||||||
|
// reset register state
|
||||||
|
m_regs.reset();
|
||||||
|
|
||||||
|
// reset each channel
|
||||||
|
for (auto &chan : m_channel)
|
||||||
|
chan->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// save_restore - save or restore the data
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_engine::save_restore(ymfm_saved_state &state)
|
||||||
|
{
|
||||||
|
// save our data
|
||||||
|
state.save_restore(m_env_counter);
|
||||||
|
|
||||||
|
// save channel state
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
m_channel[chnum]->save_restore(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// clock - master clocking function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_engine::clock(uint32_t chanmask)
|
||||||
|
{
|
||||||
|
// if something was modified, prepare
|
||||||
|
// also prepare every 4k samples to catch ending notes
|
||||||
|
if (m_modified_channels != 0 || m_prepare_count++ >= 4096)
|
||||||
|
{
|
||||||
|
// call each channel to prepare
|
||||||
|
m_active_channels = 0;
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
if (bitfield(chanmask, chnum))
|
||||||
|
if (m_channel[chnum]->prepare())
|
||||||
|
m_active_channels |= 1 << chnum;
|
||||||
|
|
||||||
|
// reset the modified channels and prepare count
|
||||||
|
m_modified_channels = m_prepare_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment the envelope counter; the envelope generator
|
||||||
|
// only clocks every other sample in order to make the PCM
|
||||||
|
// envelopes line up with the FM envelopes (after taking into
|
||||||
|
// account the different FM sampling rate)
|
||||||
|
m_env_counter++;
|
||||||
|
|
||||||
|
// now update the state of all the channels and operators
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
if (bitfield(chanmask, chnum))
|
||||||
|
m_channel[chnum]->clock(m_env_counter >> 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// update - master update function
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_engine::output(output_data &output, uint32_t chanmask)
|
||||||
|
{
|
||||||
|
// mask out some channels for debug purposes
|
||||||
|
chanmask &= debug::GLOBAL_PCM_CHANNEL_MASK;
|
||||||
|
|
||||||
|
// compute the output of each channel
|
||||||
|
for (int chnum = 0; chnum < CHANNELS; chnum++)
|
||||||
|
if (bitfield(chanmask, chnum))
|
||||||
|
m_channel[chnum]->output(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// read - handle reads from the PCM registers
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
uint8_t pcm_engine::read(uint32_t regnum)
|
||||||
|
{
|
||||||
|
// handle reads from the data register
|
||||||
|
if (regnum == 0x06 && m_regs.memory_access_mode() != 0)
|
||||||
|
return m_intf.ymfm_external_read(ACCESS_PCM, m_regs.memory_address_autoinc());
|
||||||
|
|
||||||
|
return m_regs.read(regnum);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------------------------------------
|
||||||
|
// write - handle writes to the PCM registers
|
||||||
|
//-------------------------------------------------
|
||||||
|
|
||||||
|
void pcm_engine::write(uint32_t regnum, uint8_t data)
|
||||||
|
{
|
||||||
|
// handle reads to the data register
|
||||||
|
if (regnum == 0x06 && m_regs.memory_access_mode() != 0)
|
||||||
|
{
|
||||||
|
m_intf.ymfm_external_write(ACCESS_PCM, m_regs.memory_address_autoinc(), data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for now just mark all channels as modified
|
||||||
|
m_modified_channels = ALL_CHANNELS;
|
||||||
|
|
||||||
|
// most writes are passive, consumed only when needed
|
||||||
|
m_regs.write(regnum, data);
|
||||||
|
|
||||||
|
// however, process keyons immediately
|
||||||
|
if (regnum >= 0x68 && regnum <= 0x7f)
|
||||||
|
m_channel[regnum - 0x68]->keyonoff(bitfield(data, 7));
|
||||||
|
|
||||||
|
// and also wavetable writes
|
||||||
|
else if (regnum >= 0x08 && regnum <= 0x1f)
|
||||||
|
m_channel[regnum - 0x08]->load_wavetable();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
347
backends/playback/zsm/x16emu/ymfm/src/ymfm_pcm.h
Normal file
347
backends/playback/zsm/x16emu/ymfm/src/ymfm_pcm.h
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
// BSD 3-Clause License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2021, Aaron Giles
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
// list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
// this list of conditions and the following disclaimer in the documentation
|
||||||
|
// and/or other materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
#ifndef YMFM_PCM_H
|
||||||
|
#define YMFM_PCM_H
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ymfm.h"
|
||||||
|
|
||||||
|
namespace ymfm
|
||||||
|
{
|
||||||
|
|
||||||
|
/*
|
||||||
|
Note to self: Sega "Multi-PCM" is almost identical to this
|
||||||
|
|
||||||
|
28 channels
|
||||||
|
|
||||||
|
Writes:
|
||||||
|
00 = data reg, causes write
|
||||||
|
01 = target slot = data - (data / 8)
|
||||||
|
02 = address (clamped to 7)
|
||||||
|
|
||||||
|
Slot data (registers with ADSR/KSR seem to be inaccessible):
|
||||||
|
0: xxxx---- panpot
|
||||||
|
1: xxxxxxxx wavetable low
|
||||||
|
2: xxxxxx-- pitch low
|
||||||
|
-------x wavetable high
|
||||||
|
3: xxxx---- octave
|
||||||
|
----xxxx pitch hi
|
||||||
|
4: x------- key on
|
||||||
|
5: xxxxxxx- total level
|
||||||
|
-------x level direct (0=interpolate)
|
||||||
|
6: --xxx--- LFO frequency
|
||||||
|
-----xxx PM sensitivity
|
||||||
|
7: -----xxx AM sensitivity
|
||||||
|
|
||||||
|
Sample data:
|
||||||
|
+00: start hi
|
||||||
|
+01: start mid
|
||||||
|
+02: start low
|
||||||
|
+03: loop hi
|
||||||
|
+04: loop low
|
||||||
|
+05: -end hi
|
||||||
|
+06: -end low
|
||||||
|
+07: vibrato (reg 6)
|
||||||
|
+08: attack/decay
|
||||||
|
+09: sustain level/rate
|
||||||
|
+0A: ksr/release
|
||||||
|
+0B: LFO amplitude (reg 7)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
//*********************************************************
|
||||||
|
// INTERFACE CLASSES
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
class pcm_engine;
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> pcm_cache
|
||||||
|
|
||||||
|
// this class holds data that is computed once at the start of clocking
|
||||||
|
// and remains static during subsequent sound generation
|
||||||
|
struct pcm_cache
|
||||||
|
{
|
||||||
|
uint32_t step; // sample position step, as a .16 value
|
||||||
|
uint32_t total_level; // target total level, as a .10 value
|
||||||
|
uint32_t pan_left; // left panning attenuation
|
||||||
|
uint32_t pan_right; // right panning attenuation
|
||||||
|
uint32_t eg_sustain; // sustain level, shifted up to envelope values
|
||||||
|
uint8_t eg_rate[EG_STATES]; // envelope rate, including KSR
|
||||||
|
uint8_t lfo_step; // stepping value for LFO
|
||||||
|
uint8_t am_depth; // scale value for AM LFO
|
||||||
|
uint8_t pm_depth; // scale value for PM LFO
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> pcm_registers
|
||||||
|
|
||||||
|
//
|
||||||
|
// PCM register map:
|
||||||
|
//
|
||||||
|
// System-wide registers:
|
||||||
|
// 00-01 xxxxxxxx LSI Test
|
||||||
|
// 02 -------x Memory access mode (0=sound gen, 1=read/write)
|
||||||
|
// ------x- Memory type (0=ROM, 1=ROM+SRAM)
|
||||||
|
// ---xxx-- Wave table header
|
||||||
|
// xxx----- Device ID (=1 for YMF278B)
|
||||||
|
// 03 --xxxxxx Memory address high
|
||||||
|
// 04 xxxxxxxx Memory address mid
|
||||||
|
// 05 xxxxxxxx Memory address low
|
||||||
|
// 06 xxxxxxxx Memory data
|
||||||
|
// F8 --xxx--- Mix control (FM_R)
|
||||||
|
// -----xxx Mix control (FM_L)
|
||||||
|
// F9 --xxx--- Mix control (PCM_R)
|
||||||
|
// -----xxx Mix control (PCM_L)
|
||||||
|
//
|
||||||
|
// Channel-specific registers:
|
||||||
|
// 08-1F xxxxxxxx Wave table number low
|
||||||
|
// 20-37 -------x Wave table number high
|
||||||
|
// xxxxxxx- F-number low
|
||||||
|
// 38-4F -----xxx F-number high
|
||||||
|
// ----x--- Pseudo-reverb
|
||||||
|
// xxxx---- Octave
|
||||||
|
// 50-67 xxxxxxx- Total level
|
||||||
|
// -------x Level direct
|
||||||
|
// 68-7F x------- Key on
|
||||||
|
// -x------ Damp
|
||||||
|
// --x----- LFO reset
|
||||||
|
// ---x---- Output channel
|
||||||
|
// ----xxxx Panpot
|
||||||
|
// 80-97 --xxx--- LFO speed
|
||||||
|
// -----xxx Vibrato
|
||||||
|
// 98-AF xxxx---- Attack rate
|
||||||
|
// ----xxxx Decay rate
|
||||||
|
// B0-C7 xxxx---- Sustain level
|
||||||
|
// ----xxxx Sustain rate
|
||||||
|
// C8-DF xxxx---- Rate correction
|
||||||
|
// ----xxxx Release rate
|
||||||
|
// E0-F7 -----xxx AM depth
|
||||||
|
|
||||||
|
class pcm_registers
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// constants
|
||||||
|
static constexpr uint32_t OUTPUTS = 4;
|
||||||
|
static constexpr uint32_t CHANNELS = 24;
|
||||||
|
static constexpr uint32_t REGISTERS = 0x100;
|
||||||
|
static constexpr uint32_t ALL_CHANNELS = (1 << CHANNELS) - 1;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
pcm_registers() { }
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// reset to initial state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// update cache information
|
||||||
|
void cache_channel_data(uint32_t choffs, pcm_cache &cache);
|
||||||
|
|
||||||
|
// direct read/write access
|
||||||
|
uint8_t read(uint32_t index ) { return m_regdata[index]; }
|
||||||
|
void write(uint32_t index, uint8_t data) { m_regdata[index] = data; }
|
||||||
|
|
||||||
|
// system-wide registers
|
||||||
|
uint32_t memory_access_mode() const { return bitfield(m_regdata[0x02], 0); }
|
||||||
|
uint32_t memory_type() const { return bitfield(m_regdata[0x02], 1); }
|
||||||
|
uint32_t wave_table_header() const { return bitfield(m_regdata[0x02], 2, 3); }
|
||||||
|
uint32_t device_id() const { return bitfield(m_regdata[0x02], 5, 3); }
|
||||||
|
uint32_t memory_address() const { return (bitfield(m_regdata[0x03], 0, 6) << 16) | (m_regdata[0x04] << 8) | m_regdata[0x05]; }
|
||||||
|
uint32_t memory_data() const { return m_regdata[0x06]; }
|
||||||
|
uint32_t mix_fm_r() const { return bitfield(m_regdata[0xf8], 3, 3); }
|
||||||
|
uint32_t mix_fm_l() const { return bitfield(m_regdata[0xf8], 0, 3); }
|
||||||
|
uint32_t mix_pcm_r() const { return bitfield(m_regdata[0xf9], 3, 3); }
|
||||||
|
uint32_t mix_pcm_l() const { return bitfield(m_regdata[0xf9], 0, 3); }
|
||||||
|
|
||||||
|
// per-channel registers
|
||||||
|
uint32_t ch_wave_table_num(uint32_t choffs) const { return m_regdata[choffs + 0x08] | (bitfield(m_regdata[choffs + 0x20], 0) << 8); }
|
||||||
|
uint32_t ch_fnumber(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x20], 1, 7) | (bitfield(m_regdata[choffs + 0x38], 0, 3) << 7); }
|
||||||
|
uint32_t ch_pseudo_reverb(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x38], 3); }
|
||||||
|
uint32_t ch_octave(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x38], 4, 4); }
|
||||||
|
uint32_t ch_total_level(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x50], 1, 7); }
|
||||||
|
uint32_t ch_level_direct(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x50], 0); }
|
||||||
|
uint32_t ch_keyon(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x68], 7); }
|
||||||
|
uint32_t ch_damp(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x68], 6); }
|
||||||
|
uint32_t ch_lfo_reset(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x68], 5); }
|
||||||
|
uint32_t ch_output_channel(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x68], 4); }
|
||||||
|
uint32_t ch_panpot(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x68], 0, 4); }
|
||||||
|
uint32_t ch_lfo_speed(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x80], 3, 3); }
|
||||||
|
uint32_t ch_vibrato(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x80], 0, 3); }
|
||||||
|
uint32_t ch_attack_rate(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x98], 4, 4); }
|
||||||
|
uint32_t ch_decay_rate(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0x98], 0, 4); }
|
||||||
|
uint32_t ch_sustain_level(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0xb0], 4, 4); }
|
||||||
|
uint32_t ch_sustain_rate(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0xb0], 0, 4); }
|
||||||
|
uint32_t ch_rate_correction(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0xc8], 4, 4); }
|
||||||
|
uint32_t ch_release_rate(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0xc8], 0, 4); }
|
||||||
|
uint32_t ch_am_depth(uint32_t choffs) const { return bitfield(m_regdata[choffs + 0xe0], 0, 3); }
|
||||||
|
|
||||||
|
// return the memory address and increment it
|
||||||
|
uint32_t memory_address_autoinc()
|
||||||
|
{
|
||||||
|
uint32_t result = memory_address();
|
||||||
|
uint32_t newval = result + 1;
|
||||||
|
m_regdata[0x05] = newval >> 0;
|
||||||
|
m_regdata[0x04] = newval >> 8;
|
||||||
|
m_regdata[0x03] = (newval >> 16) & 0x3f;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal helpers
|
||||||
|
uint32_t effective_rate(uint32_t raw, uint32_t correction);
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint8_t m_regdata[REGISTERS]; // register data
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> pcm_channel
|
||||||
|
|
||||||
|
class pcm_channel
|
||||||
|
{
|
||||||
|
static constexpr uint8_t KEY_ON = 0x01;
|
||||||
|
static constexpr uint8_t KEY_PENDING_ON = 0x02;
|
||||||
|
static constexpr uint8_t KEY_PENDING = 0x04;
|
||||||
|
|
||||||
|
// "quiet" value, used to optimize when we can skip doing working
|
||||||
|
static constexpr uint32_t EG_QUIET = 0x200;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using output_data = ymfm_output<pcm_registers::OUTPUTS>;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
pcm_channel(pcm_engine &owner, uint32_t choffs);
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// reset the channel state
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// return the channel offset
|
||||||
|
uint32_t choffs() const { return m_choffs; }
|
||||||
|
|
||||||
|
// prepare prior to clocking
|
||||||
|
bool prepare();
|
||||||
|
|
||||||
|
// master clocking function
|
||||||
|
void clock(uint32_t env_counter);
|
||||||
|
|
||||||
|
// return the computed output value, with panning applied
|
||||||
|
void output(output_data &output) const;
|
||||||
|
|
||||||
|
// signal key on/off
|
||||||
|
void keyonoff(bool on);
|
||||||
|
|
||||||
|
// load a new wavetable entry
|
||||||
|
void load_wavetable();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal helpers
|
||||||
|
void start_attack();
|
||||||
|
void start_release();
|
||||||
|
void clock_envelope(uint32_t env_counter);
|
||||||
|
int16_t fetch_sample() const;
|
||||||
|
uint8_t read_pcm(uint32_t address) const;
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
uint32_t const m_choffs; // channel offset
|
||||||
|
uint32_t m_baseaddr; // base address
|
||||||
|
uint32_t m_endpos; // ending position
|
||||||
|
uint32_t m_looppos; // loop position
|
||||||
|
uint32_t m_curpos; // current position
|
||||||
|
uint32_t m_nextpos; // next position
|
||||||
|
uint32_t m_lfo_counter; // LFO counter
|
||||||
|
envelope_state m_eg_state; // envelope state
|
||||||
|
uint16_t m_env_attenuation; // computed envelope attenuation
|
||||||
|
uint32_t m_total_level; // total level with as 7.10 for interp
|
||||||
|
uint8_t m_format; // sample format
|
||||||
|
uint8_t m_key_state; // current key state
|
||||||
|
pcm_cache m_cache; // cached data
|
||||||
|
pcm_registers &m_regs; // reference to registers
|
||||||
|
pcm_engine &m_owner; // reference to our owner
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ======================> pcm_engine
|
||||||
|
|
||||||
|
class pcm_engine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr int OUTPUTS = pcm_registers::OUTPUTS;
|
||||||
|
static constexpr int CHANNELS = pcm_registers::CHANNELS;
|
||||||
|
static constexpr uint32_t ALL_CHANNELS = pcm_registers::ALL_CHANNELS;
|
||||||
|
using output_data = pcm_channel::output_data;
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
pcm_engine(ymfm_interface &intf);
|
||||||
|
|
||||||
|
// reset our status
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// save/restore
|
||||||
|
void save_restore(ymfm_saved_state &state);
|
||||||
|
|
||||||
|
// master clocking function
|
||||||
|
void clock(uint32_t chanmask);
|
||||||
|
|
||||||
|
// compute sum of channel outputs
|
||||||
|
void output(output_data &output, uint32_t chanmask);
|
||||||
|
|
||||||
|
// read from the PCM registers
|
||||||
|
uint8_t read(uint32_t regnum);
|
||||||
|
|
||||||
|
// write to the PCM registers
|
||||||
|
void write(uint32_t regnum, uint8_t data);
|
||||||
|
|
||||||
|
// return a reference to our interface
|
||||||
|
ymfm_interface &intf() { return m_intf; }
|
||||||
|
|
||||||
|
// return a reference to our registers
|
||||||
|
pcm_registers ®s() { return m_regs; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// internal state
|
||||||
|
ymfm_interface &m_intf; // reference to the interface
|
||||||
|
uint32_t m_env_counter; // envelope counter
|
||||||
|
uint32_t m_modified_channels; // bitmask of modified channels
|
||||||
|
uint32_t m_active_channels; // bitmask of active channels
|
||||||
|
uint32_t m_prepare_count; // counter to do periodic prepare sweeps
|
||||||
|
std::unique_ptr<pcm_channel> m_channel[CHANNELS]; // array of channels
|
||||||
|
pcm_registers m_regs; // registers
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // YMFM_PCM_H
|
131
backends/playback/zsm/x16emu/ymglue.cpp
Normal file
131
backends/playback/zsm/x16emu/ymglue.cpp
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
#include "ymglue.h"
|
||||||
|
#include "ymfm_opm.h"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class ym2151_interface : public ymfm::ymfm_interface {
|
||||||
|
public:
|
||||||
|
ym2151_interface():
|
||||||
|
m_chip(*this),
|
||||||
|
m_timers{0, 0},
|
||||||
|
m_busy_timer{ 0 },
|
||||||
|
m_irq_status{ false }
|
||||||
|
{ }
|
||||||
|
~ym2151_interface() { }
|
||||||
|
|
||||||
|
virtual void ymfm_sync_mode_write(uint8_t data) override {
|
||||||
|
m_engine->engine_mode_write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void ymfm_sync_check_interrupts() override {
|
||||||
|
m_engine->engine_check_interrupts();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void ymfm_set_timer(uint32_t tnum, int32_t duration_in_clocks) override {
|
||||||
|
if (tnum >= 2) return;
|
||||||
|
m_timers[tnum] = duration_in_clocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void ymfm_set_busy_end(uint32_t clocks) override {
|
||||||
|
m_busy_timer = clocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool ymfm_is_busy() override {
|
||||||
|
return m_busy_timer > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void ymfm_update_irq(bool asserted) override {
|
||||||
|
m_irq_status = asserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_clocks(int cycles) {
|
||||||
|
m_busy_timer = std::max(0, m_busy_timer - (64 * cycles));
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
if (m_timers[i] > 0) {
|
||||||
|
m_timers[i] = std::max(0, m_timers[i] - (64 * cycles));
|
||||||
|
if (m_timers[i] <= 0) {
|
||||||
|
m_engine->engine_timer_expired(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(uint8_t addr, uint8_t value) {
|
||||||
|
if (!ymfm_is_busy()) {
|
||||||
|
m_chip.write_address(addr);
|
||||||
|
m_chip.write_data(value);
|
||||||
|
} else {
|
||||||
|
printf("YM2151 write received while busy.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void generate(int16_t* output, uint32_t numsamples) {
|
||||||
|
int s = 0;
|
||||||
|
int ls, rs;
|
||||||
|
update_clocks(numsamples);
|
||||||
|
for (uint32_t i = 0; i < numsamples; i++) {
|
||||||
|
m_chip.generate(&opm_out);
|
||||||
|
ls = opm_out.data[0];
|
||||||
|
rs = opm_out.data[1];
|
||||||
|
if (ls < -32768) ls = -32768;
|
||||||
|
if (ls > 32767) ls = 32767;
|
||||||
|
if (rs < -32768) rs = -32768;
|
||||||
|
if (rs > 32767) rs = 32767;
|
||||||
|
output[s++] = ls;
|
||||||
|
output[s++] = rs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t read_status() {
|
||||||
|
return m_chip.read_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool irq() {
|
||||||
|
return m_irq_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
ymfm::ym2151 m_chip;
|
||||||
|
int32_t m_timers[2];
|
||||||
|
int32_t m_busy_timer;
|
||||||
|
bool m_irq_status;
|
||||||
|
|
||||||
|
ymfm::ym2151::output_data opm_out;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
ym2151_interface opm_iface;
|
||||||
|
bool initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void YM_Create(int clock) {
|
||||||
|
// clock is fixed at 3.579545MHz
|
||||||
|
}
|
||||||
|
|
||||||
|
void YM_init(int sample_rate, int frame_rate) {
|
||||||
|
// args are ignored
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void YM_stream_update(uint16_t* output, uint32_t numsamples) {
|
||||||
|
if (initialized) opm_iface.generate((int16_t*)output, numsamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
void YM_write_reg(uint8_t reg, uint8_t val) {
|
||||||
|
if (initialized) opm_iface.write(reg, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t YM_read_status() {
|
||||||
|
if (initialized)
|
||||||
|
return opm_iface.read_status();
|
||||||
|
else
|
||||||
|
return 0x00; // prevent programs that wait for the busy flag to clear from locking up (emulator compromise)
|
||||||
|
}
|
||||||
|
|
||||||
|
bool YM_irq() {
|
||||||
|
if (initialized)
|
||||||
|
return opm_iface.irq();
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
20
backends/playback/zsm/x16emu/ymglue.h
Normal file
20
backends/playback/zsm/x16emu/ymglue.h
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#ifndef YMGLUE_H
|
||||||
|
#define YMGLUE_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
uint8_t YM_read_status(void);
|
||||||
|
void YM_Create(int clock);
|
||||||
|
void YM_init(int sample_rate, int frame_rate);
|
||||||
|
void YM_stream_update(uint16_t* output, uint32_t numsamples);
|
||||||
|
void YM_write_reg(uint8_t reg, uint8_t val);
|
||||||
|
bool YM_irq(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
219
backends/playback/zsm/zsm_backend.cpp
Normal file
219
backends/playback/zsm/zsm_backend.cpp
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
#include "zsm_backend.hpp"
|
||||||
|
extern "C" {
|
||||||
|
#include "x16emu/audio.h"
|
||||||
|
#include "x16emu/vera_pcm.h"
|
||||||
|
#include "x16emu/vera_psg.h"
|
||||||
|
#include "x16emu/ymglue.h"
|
||||||
|
}
|
||||||
|
#include <exception>
|
||||||
|
#include <filesystem>
|
||||||
|
#include "file_backend.hpp"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <file_backend.hpp>
|
||||||
|
void ZsmBackend::load(const char *filename) {
|
||||||
|
spec.format = AUDIO_S16SYS;
|
||||||
|
file = open_file(filename);
|
||||||
|
char magic[2];
|
||||||
|
file->read(magic, 2);
|
||||||
|
if (magic[0] != 0x7a || magic[1] != 0x6d) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
uint8_t version;
|
||||||
|
file->read(&version, 1);
|
||||||
|
uint8_t loop_point[3];
|
||||||
|
file->read(loop_point, 3);
|
||||||
|
this->loop_point = loop_point[0] | ((uint32_t)(loop_point[1]) << 8) | ((uint32_t)(loop_point[2]) << 16);
|
||||||
|
file->read(loop_point, 3);
|
||||||
|
this->pcm_offset = loop_point[0] | ((uint32_t)(loop_point[1]) << 8) | ((uint32_t)(loop_point[2]) << 16);
|
||||||
|
file->read(&fm_mask, 1);
|
||||||
|
file->read(loop_point, 2);
|
||||||
|
this->psg_channel_mask = loop_point[0] | ((uint16_t)(loop_point[1]) << 8);
|
||||||
|
file->read(loop_point, 2);
|
||||||
|
this->tick_rate = loop_point[0] | ((uint16_t)(loop_point[1]) << 8);
|
||||||
|
file->read(loop_point, 2); // Reserved.
|
||||||
|
music_data_start = file->get_pos();
|
||||||
|
while (true) {
|
||||||
|
ZsmCommand cmd = get_command();
|
||||||
|
if (cmd.id == ZsmEOF) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
music_data_len = file->get_pos();
|
||||||
|
switch_stream(0);
|
||||||
|
}
|
||||||
|
extern SDL_AudioSpec obtained;
|
||||||
|
void ZsmBackend::switch_stream(int idx) {
|
||||||
|
psg_reset();
|
||||||
|
audio_close();
|
||||||
|
audio_init(NULL, 16);
|
||||||
|
spec = obtained;
|
||||||
|
}
|
||||||
|
void ZsmBackend::cleanup() {
|
||||||
|
audio_close();
|
||||||
|
delete file;
|
||||||
|
file = nullptr;
|
||||||
|
}
|
||||||
|
size_t ZsmBackend::render(void *buf, size_t maxlen) {
|
||||||
|
size_t sample_type_len = size_of_sample_type(spec.format);
|
||||||
|
maxlen /= sample_type_len;
|
||||||
|
double prevTicks = ticks;
|
||||||
|
double delta = (double)(maxlen) / (double)(spec.freq);
|
||||||
|
double deltaTicks = delta / (double)(tick_rate);
|
||||||
|
ticks += deltaTicks;
|
||||||
|
double clocks = delta * (double)(8000000);
|
||||||
|
for (size_t i = 0; i < (size_t)(ticks - prevTicks); i++) {
|
||||||
|
double deltaClocks = (double)(tick_rate) * (double)(8000000);
|
||||||
|
clocks -= deltaClocks;
|
||||||
|
if (clocks < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
ZsmCommand cmd = get_command();
|
||||||
|
bool tick_end = false;
|
||||||
|
if (delayTicks > 0.0) {
|
||||||
|
delayTicks -= 1.0 / double(tick_rate);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (cmd.id) {
|
||||||
|
case ZsmEOF: {
|
||||||
|
seek_internal((double)(loop_point) / (double)(tick_rate));
|
||||||
|
} break;
|
||||||
|
case PsgWrite: {
|
||||||
|
psg_writereg(cmd.psg_write.reg, cmd.psg_write.val);
|
||||||
|
} break;
|
||||||
|
case FmWrite: {
|
||||||
|
for (uint8_t i = 0; i < cmd.fm_write.len; i++) {
|
||||||
|
YM_write_reg(cmd.fm_write.regs[i].reg, cmd.fm_write.regs[i].val);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case Delay: {
|
||||||
|
delayTicks = cmd.delay;
|
||||||
|
tick_end = true;
|
||||||
|
} break;
|
||||||
|
case ExtCmd: {
|
||||||
|
// Nothing handled yet.
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
if (tick_end) break;
|
||||||
|
}
|
||||||
|
audio_step((int)(clocks));
|
||||||
|
}
|
||||||
|
audio_render();
|
||||||
|
audio_callback(nullptr, (Uint8*)(buf), sample_type_len * maxlen);
|
||||||
|
return maxlen * sample_type_len;
|
||||||
|
}
|
||||||
|
ZsmCommand ZsmBackend::get_command() {
|
||||||
|
ZsmCommandId cmdid;
|
||||||
|
uint8_t cmd_byte;
|
||||||
|
file->read(&cmd_byte, 1);
|
||||||
|
if (cmd_byte == 0x80) {
|
||||||
|
cmdid = ZsmEOF;
|
||||||
|
} else {
|
||||||
|
if ((cmd_byte & 0b11000000) == 0) {
|
||||||
|
cmdid = PsgWrite;
|
||||||
|
} else if ((cmd_byte & 0b11000000) == 0b01) {
|
||||||
|
if (cmd_byte == 0b01000000) {
|
||||||
|
cmdid = ExtCmd;
|
||||||
|
} else {
|
||||||
|
cmdid = FmWrite;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cmdid = Delay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ZsmCommand output;
|
||||||
|
output.id = cmdid;
|
||||||
|
if (cmdid == ZsmEOF) {
|
||||||
|
return output;
|
||||||
|
} else if (cmdid == PsgWrite) {
|
||||||
|
uint8_t value;
|
||||||
|
file->read(&value, 1);
|
||||||
|
output.psg_write.reg = cmd_byte & 0x3F;
|
||||||
|
output.psg_write.val = value;
|
||||||
|
} else if (cmdid == FmWrite) {
|
||||||
|
uint8_t value[2];
|
||||||
|
uint8_t pairs = cmd_byte & 0b111111;
|
||||||
|
output.fm_write.len = pairs;
|
||||||
|
output.fm_write.regs = (reg_pair*)malloc((sizeof(uint8_t)*2)*pairs);
|
||||||
|
for (uint8_t i = 0; i < pairs; i++) {
|
||||||
|
output.fm_write.regs[i].reg = value[0];
|
||||||
|
output.fm_write.regs[i].val = value[1];
|
||||||
|
file->read(value, 2);
|
||||||
|
}
|
||||||
|
} else if (cmdid == ExtCmd) {
|
||||||
|
uint8_t ext_cmd_byte;
|
||||||
|
file->read(&ext_cmd_byte, 1);
|
||||||
|
uint8_t bytes = ext_cmd_byte & 0x3F;
|
||||||
|
uint8_t ch = ext_cmd_byte >> 6;
|
||||||
|
output.extcmd.channel = ch;
|
||||||
|
output.extcmd.bytes = bytes;
|
||||||
|
if (ch == 1) {
|
||||||
|
output.extcmd.expansion.write_bytes = (uint8_t*)malloc(bytes - 2);
|
||||||
|
} else {
|
||||||
|
output.extcmd.pcm = (uint8_t*)malloc(bytes); // Handles all other cases due to them being in a union, and each having the same type.
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < bytes; i++) {
|
||||||
|
uint8_t byte;
|
||||||
|
file->read(&byte, 1);
|
||||||
|
switch (ch) {
|
||||||
|
case 0: {
|
||||||
|
output.extcmd.pcm[i] = byte;
|
||||||
|
} break;
|
||||||
|
case 1: {
|
||||||
|
if (i == 0) {
|
||||||
|
output.extcmd.expansion.chip_id = byte;
|
||||||
|
} else if (i == 1) {
|
||||||
|
output.extcmd.expansion.writes = byte;
|
||||||
|
} else {
|
||||||
|
output.extcmd.expansion.write_bytes[i - 2] = byte;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case 2: {
|
||||||
|
output.extcmd.sync[i] = byte;
|
||||||
|
} break;
|
||||||
|
case 3: {
|
||||||
|
output.extcmd.custom[i] = byte;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
ZsmCommand::~ZsmCommand() {
|
||||||
|
switch (id) {
|
||||||
|
case ExtCmd: {
|
||||||
|
if (extcmd.channel == 1) {
|
||||||
|
free(extcmd.expansion.write_bytes);
|
||||||
|
} else {
|
||||||
|
free(extcmd.pcm);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case FmWrite: {
|
||||||
|
free(fm_write.regs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void ZsmBackend::seek_internal(double position) {
|
||||||
|
double ticks = 0;
|
||||||
|
file->seek(music_data_start, SeekType::SET);
|
||||||
|
while (ticks < position) {
|
||||||
|
ZsmCommand cmd = get_command();
|
||||||
|
if (cmd.id == ZsmEOF) {
|
||||||
|
file->seek(music_data_start, SeekType::SET);
|
||||||
|
break;
|
||||||
|
} else if (cmd.id == Delay) {
|
||||||
|
ticks += (double)(cmd.delay) / (double)(tick_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->position = position;
|
||||||
|
}
|
||||||
|
void ZsmBackend::seek(double position) {
|
||||||
|
seek_internal(position);
|
||||||
|
}
|
||||||
|
double ZsmBackend::get_position() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
int ZsmBackend::get_stream_idx() {
|
||||||
|
return 0;
|
||||||
|
}
|
76
backends/playback/zsm/zsm_backend.hpp
Normal file
76
backends/playback/zsm/zsm_backend.hpp
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#pragma once
|
||||||
|
#include "playback_backend.hpp"
|
||||||
|
#include <cstdint>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "file_backend.hpp"
|
||||||
|
enum ZsmCommandId {
|
||||||
|
PsgWrite,
|
||||||
|
ExtCmd,
|
||||||
|
FmWrite,
|
||||||
|
ZsmEOF,
|
||||||
|
Delay
|
||||||
|
};
|
||||||
|
struct reg_pair {
|
||||||
|
uint8_t reg;
|
||||||
|
uint8_t val;
|
||||||
|
};
|
||||||
|
struct ZsmCommand {
|
||||||
|
ZsmCommandId id;
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
uint8_t reg;
|
||||||
|
uint8_t val;
|
||||||
|
} psg_write;
|
||||||
|
struct {
|
||||||
|
uint8_t channel;
|
||||||
|
uint8_t bytes;
|
||||||
|
union {
|
||||||
|
uint8_t *pcm;
|
||||||
|
struct {
|
||||||
|
uint8_t chip_id;
|
||||||
|
uint8_t writes;
|
||||||
|
uint8_t *write_bytes;
|
||||||
|
} expansion;
|
||||||
|
uint8_t *sync;
|
||||||
|
uint8_t *custom;
|
||||||
|
};
|
||||||
|
} extcmd;
|
||||||
|
struct {
|
||||||
|
uint8_t len;
|
||||||
|
reg_pair *regs;
|
||||||
|
} fm_write;
|
||||||
|
uint8_t delay;
|
||||||
|
};
|
||||||
|
~ZsmCommand();
|
||||||
|
};
|
||||||
|
class ZsmBackend : public PlaybackBackend {
|
||||||
|
File *file;
|
||||||
|
uint32_t loop_point;
|
||||||
|
uint32_t pcm_offset;
|
||||||
|
uint8_t fm_mask;
|
||||||
|
uint16_t psg_channel_mask;
|
||||||
|
uint16_t tick_rate;
|
||||||
|
size_t music_data_start;
|
||||||
|
size_t music_data_len;
|
||||||
|
double ticks;
|
||||||
|
double delayTicks = 0.0;
|
||||||
|
double position;
|
||||||
|
void seek_internal(double position);
|
||||||
|
ZsmCommand get_command();
|
||||||
|
public:
|
||||||
|
inline std::string get_id() override {
|
||||||
|
return "zsm";
|
||||||
|
}
|
||||||
|
inline std::string get_name() override {
|
||||||
|
return "ZSM player";
|
||||||
|
}
|
||||||
|
void seek(double position) override;
|
||||||
|
void load(const char *filename) override;
|
||||||
|
void switch_stream(int idx) override;
|
||||||
|
void cleanup() override;
|
||||||
|
int get_stream_idx() override;
|
||||||
|
size_t render(void *buf, size_t maxlen) override;
|
||||||
|
double get_position() override;
|
||||||
|
inline ~ZsmBackend() override { }
|
||||||
|
};
|
|
@ -75,6 +75,7 @@ struct FontData {
|
||||||
};
|
};
|
||||||
void RendererBackend::BackendDeinit() {
|
void RendererBackend::BackendDeinit() {
|
||||||
ImGuiIO& io = ImGui::GetIO(); (void)io;
|
ImGuiIO& io = ImGui::GetIO(); (void)io;
|
||||||
|
void *IniFilename = (void*)io.IniFilename;
|
||||||
// Cleanup
|
// Cleanup
|
||||||
ImGui_ImplSDLRenderer2_Shutdown();
|
ImGui_ImplSDLRenderer2_Shutdown();
|
||||||
ImGui_ImplSDL2_Shutdown();
|
ImGui_ImplSDL2_Shutdown();
|
||||||
|
@ -84,7 +85,7 @@ void RendererBackend::BackendDeinit() {
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
IMG_Quit();
|
IMG_Quit();
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
free((void*)io.IniFilename);
|
free(IniFilename);
|
||||||
Deinit();
|
Deinit();
|
||||||
renderer_backend = nullptr;
|
renderer_backend = nullptr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -339,5 +339,7 @@ FileBrowser::~FileBrowser() {
|
||||||
g_variant_type_free(inner_filter_type);
|
g_variant_type_free(inner_filter_type);
|
||||||
}
|
}
|
||||||
g_main_loop_quit(main_loop);
|
g_main_loop_quit(main_loop);
|
||||||
|
g_main_loop_unref(main_loop);
|
||||||
|
g_object_unref(portal);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
60
build-docker.sh
Executable file
60
build-docker.sh
Executable file
|
@ -0,0 +1,60 @@
|
||||||
|
#!/bin/bash
|
||||||
|
on_err() {
|
||||||
|
code=$?
|
||||||
|
if echo -n "$BASH_COMMAND" | grep "^docker" >/dev/null 2>/dev/null; then
|
||||||
|
exit $code
|
||||||
|
fi
|
||||||
|
echo "Error: Command exited with non-zero status ($code)" >&2
|
||||||
|
echo "Command: $BASH_COMMAND"
|
||||||
|
exit $code
|
||||||
|
}
|
||||||
|
have_image() {
|
||||||
|
docker images --format json | jq '.[].Names' --raw-output | grep -v null | jq '.[]' --raw-output | sed 's/:.*$//' | sed 's@^.*/@@' | grep -Fx "$1" >/dev/null 2>/dev/null
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
trap on_err ERR
|
||||||
|
export first_arg="$1"
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
export first_arg="build"
|
||||||
|
fi
|
||||||
|
shift || true
|
||||||
|
build_cmake() {
|
||||||
|
pushd "$1"
|
||||||
|
shift
|
||||||
|
git submodule update --init --recursive
|
||||||
|
mkdir -p "build-docker" && cd "build-docker"
|
||||||
|
cmake ..
|
||||||
|
cmake --build . "$@"
|
||||||
|
cmake --install .
|
||||||
|
popd
|
||||||
|
}
|
||||||
|
if [ "$first_arg" = "in-docker" ]; then
|
||||||
|
ARGS=( "$@" )
|
||||||
|
cd /src
|
||||||
|
build_cmake subprojects/protobuf "${ARGS[@]}"
|
||||||
|
pushd subprojects/grpc
|
||||||
|
git apply ../../grpc.patch || true
|
||||||
|
popd
|
||||||
|
build_cmake subprojects/grpc "${ARGS[@]}"
|
||||||
|
mkdir -p /src/build-docker && cd /src/build-docker
|
||||||
|
export CMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||||
|
cmake -DDISABLE_GTK_UI=ON -DCMAKE_BUILD_TYPE=Debug ..
|
||||||
|
cmake --build . "${ARGS[@]}"
|
||||||
|
else
|
||||||
|
export second_arg="$1"
|
||||||
|
shift || true
|
||||||
|
if [ -z "$second_arg" ]; then
|
||||||
|
export second_arg="debian"
|
||||||
|
fi
|
||||||
|
export tag="looper-build-$second_arg"
|
||||||
|
if [ "$first_arg" = "env" ]; then
|
||||||
|
if [ -d "build-env/$second_arg" ]; then
|
||||||
|
docker build "build-env/$second_arg" -t "$tag"
|
||||||
|
fi
|
||||||
|
elif [ "$first_arg" = "build" ]; then
|
||||||
|
if ! have_image "$tag"; then
|
||||||
|
"$0" env "$second_arg"
|
||||||
|
fi
|
||||||
|
docker run -v $PWD:/src "$tag" "/src/build-docker.sh" "in-docker" "$@"
|
||||||
|
fi
|
||||||
|
fi
|
6
build-env/arch/Dockerfile
Normal file
6
build-env/arch/Dockerfile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
FROM docker.io/library/archlinux
|
||||||
|
ADD http://worldclockapi.com/api/json/utc/now /etc/builddate
|
||||||
|
RUN --mount=type=cache,target=/var/cache/pacman pacman -Syu --noconfirm
|
||||||
|
RUN --mount=type=cache,target=/var/cache/pacman pacman -S gcc cmake git make ninja abseil-cpp soundtouch sdl2 sdl2_image libogg libvorbis flac ffmpeg mpg123 wavpack libmodplug fluidsynth libgme zlib libpng gtkmm-4.0 jsoncpp sdbus-cpp libportal automake autoconf --noconfirm --needed
|
||||||
|
VOLUME [ "/src" ]
|
||||||
|
CMD [ "/src/build-docker.sh", "in-docker" ]
|
8
build-env/debian/Dockerfile
Normal file
8
build-env/debian/Dockerfile
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
ARG DEBIAN_CODENAME=bookworm
|
||||||
|
FROM docker.io/library/debian:${DEBIAN_CODENAME}
|
||||||
|
ADD http://worldclockapi.com/api/json/utc/now /etc/builddate
|
||||||
|
RUN apt-get update
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt apt-get upgrade -y
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt apt-get install -y build-essential cmake git libsoundtouch-dev libsdl2-dev libsdl2-image-dev libogg-dev libvorbis-dev libflac-dev libavcodec-dev libswresample-dev libavutil-dev libavformat-dev libavfilter-dev libavdevice-dev libmpg123-dev libwavpack-dev libmodplug-dev libfluidsynth-dev libgme-dev libxmp-dev zlib1g-dev libpng-dev libgtkmm-4.0-dev libjsoncpp-dev libsdbus-c++-dev libportal-dev automake autoconf libsdbus-c++-bin autotools-dev libtool-bin libtool libssl-dev
|
||||||
|
VOLUME [ "/src" ]
|
||||||
|
CMD [ "/src/build-docker.sh", "in-docker" ]
|
31
build.sh
Executable file
31
build.sh
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
#!/bin/bash
|
||||||
|
export OLD_DIR="$(pwd)"
|
||||||
|
cmd_err() {
|
||||||
|
CUR_DIR="$(pwd)"
|
||||||
|
code=$1
|
||||||
|
echo "Error: Command exited with non-zero status ($code)" >&2
|
||||||
|
echo "Directory: $CUR_DIR"
|
||||||
|
echo "Command: $2" >&2
|
||||||
|
cd $OLD_DIR
|
||||||
|
exit $code
|
||||||
|
}
|
||||||
|
on_err() {
|
||||||
|
echo "Script error."
|
||||||
|
cmd_err "$?" "$BASH_COMMAND"
|
||||||
|
}
|
||||||
|
run_command() {
|
||||||
|
trap ERR
|
||||||
|
echo "$@" >&2
|
||||||
|
"$@"
|
||||||
|
code=$?
|
||||||
|
if [ $code -ne 0 ]; then
|
||||||
|
cmd_err "$code" "$*"
|
||||||
|
fi
|
||||||
|
trap on_err ERR
|
||||||
|
}
|
||||||
|
trap on_err ERR
|
||||||
|
mkdir -p build
|
||||||
|
cd build
|
||||||
|
run_command cmake .. -DDISABLE_GTK_UI=ON -DCMAKE_BUILD_TYPE=Debug
|
||||||
|
run_command cmake --build . "$@"
|
||||||
|
cd ..
|
123
cmake/SanitizerBuildTypes.cmake
Normal file
123
cmake/SanitizerBuildTypes.cmake
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
#[=======================================================================[.rst:
|
||||||
|
SanitizerBuildTypes
|
||||||
|
--------
|
||||||
|
|
||||||
|
Add the compiler and linker flags for the sanitizer build types.
|
||||||
|
Currently, only Clang and GCC are supported.
|
||||||
|
Support for MSVC has yet to be added.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Provide the names for the build types through the following variables:
|
||||||
|
|
||||||
|
ASAN
|
||||||
|
LSAN
|
||||||
|
MSAN
|
||||||
|
TSAN
|
||||||
|
UBSAN
|
||||||
|
|
||||||
|
It is possible to only provide a subset of these variables to only create build types for specific sanitizers.
|
||||||
|
|
||||||
|
#]=======================================================================]
|
||||||
|
function(sanitizer_build_types)
|
||||||
|
|
||||||
|
if(ARGC EQUAL 0)
|
||||||
|
message(FATAL_ERROR "No arguments were given.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(prefix ARG)
|
||||||
|
set(no_values "")
|
||||||
|
set(single_values ASAN LSAN MSAN TSAN UBSAN)
|
||||||
|
set(multi_values "")
|
||||||
|
|
||||||
|
include(CMakeParseArguments)
|
||||||
|
cmake_parse_arguments(${prefix}
|
||||||
|
"${no_values}"
|
||||||
|
"${single_values}"
|
||||||
|
"${multi_values}"
|
||||||
|
${ARGN})
|
||||||
|
|
||||||
|
if(ARG_UNPARSED_ARGUMENTS)
|
||||||
|
message(FATAL_ERROR "Unexpected arguments: ${ARG_UNPARSED_ARGUMENTS}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ARG_KEYWORDS_MISSING_VALUES)
|
||||||
|
message(FATAL_ERROR "Keywords missing values: ${ARG_KEYWORDS_MISSING_VALUES}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_bitness 32)
|
||||||
|
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||||
|
set(_bitness 64)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# todo Add support for the AddressSanitizer for MSVC / 32-bit builds.
|
||||||
|
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
|
foreach(arg IN LISTS single_values)
|
||||||
|
if(${prefix}_${arg})
|
||||||
|
set_property(GLOBAL APPEND PROPERTY DEBUG_CONFIGURATIONS ${${prefix}_${arg}})
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set(_asan_platforms Darwin FreeBSD Linux NetBSD Windows)
|
||||||
|
if(ARG_ASAN)
|
||||||
|
if(CMAKE_SYSTEM_NAME IN_LIST _asan_platforms)
|
||||||
|
set(CMAKE_C_FLAGS_${ARG_ASAN} "${CMAKE_C_FLAGS_RELWITHDEBINFO} -fsanitize=address -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the C compiler during Address Sanitizer builds.")
|
||||||
|
set(CMAKE_CXX_FLAGS_${ARG_ASAN} "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -fsanitize=address -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the C++ compiler during Address Sanitizer builds.")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_${ARG_ASAN} "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} -fsanitize=address -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the linker during Address Sanitizer builds.")
|
||||||
|
else()
|
||||||
|
message("The address sanitizer is only available on ${_asan_platforms}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_lsan_platforms Darwin Linux)
|
||||||
|
if(ARG_LSAN)
|
||||||
|
if(_bitness EQUAL 32)
|
||||||
|
message("The leak sanitizer is only available for 64-bit systems")
|
||||||
|
elseif(CMAKE_SYSTEM_NAME IN_LIST _lsan_platforms)
|
||||||
|
set(CMAKE_C_FLAGS_${ARG_LSAN} "${CMAKE_C_FLAGS_RELWITHDEBINFO} -fsanitize=leak -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the C compiler during Leak Sanitizer builds.")
|
||||||
|
set(CMAKE_CXX_FLAGS_${ARG_LSAN} "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -fsanitize=leak -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the C++ compiler during Leak Sanitizer builds.")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_${ARG_LSAN} "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} -fsanitize=leak -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the linker during Leak Sanitizer builds.")
|
||||||
|
else()
|
||||||
|
message("The leak sanitizer is only available on ${_lsan_platforms}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_msan_platforms FreeBSD Linux NetBSD)
|
||||||
|
if(ARG_MSAN)
|
||||||
|
if(CMAKE_SYSTEM_NAME IN_LIST _msan_platforms)
|
||||||
|
set(CMAKE_C_FLAGS_${ARG_MSAN} "${CMAKE_C_FLAGS_RELWITHDEBINFO} -fsanitize=memory -fno-optimize-sibling-calls -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the C compiler during Memory Sanitizer builds.")
|
||||||
|
set(CMAKE_CXX_FLAGS_${ARG_MSAN} "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -fsanitize=memory -fno-optimize-sibling-calls -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the C++ compiler during Memory Sanitizer builds.")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_${ARG_MSAN} "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} -fsanitize=memory -fno-optimize-sibling-calls -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -g" CACHE STRING "Flags used by the linker during Memory Sanitizer builds.")
|
||||||
|
else()
|
||||||
|
message("The memory sanitizer is only available on ${_msan_platforms}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_tsan_platforms Darwin FreeBSD Linux NetBSD)
|
||||||
|
if(ARG_TSAN)
|
||||||
|
if(_bitness EQUAL 32)
|
||||||
|
message("The thread sanitizer is only available for 64-bit systems")
|
||||||
|
elseif(CMAKE_SYSTEM_NAME IN_LIST _tsan_platforms)
|
||||||
|
set(CMAKE_C_FLAGS_${ARG_TSAN} "${CMAKE_C_FLAGS_RELWITHDEBINFO} -fsanitize=thread -g" CACHE STRING "Flags used by the C compiler during Thread Sanitizer builds.")
|
||||||
|
set(CMAKE_CXX_FLAGS_${ARG_TSAN} "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -fsanitize=thread -g" CACHE STRING "Flags used by the C++ compiler during Thread Sanitizer builds.")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_${ARG_TSAN} "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} -fsanitize=thread -g" CACHE STRING "Flags used by the linker during Thread Sanitizer builds.")
|
||||||
|
else()
|
||||||
|
message("The thread sanitizer is only available on ${_tsan_platforms}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_ubsan_platforms Darwin FreeBSD Linux NetBSD OpenBSD Windows)
|
||||||
|
if(ARG_UBSAN)
|
||||||
|
if(CMAKE_SYSTEM_NAME IN_LIST _ubsan_platforms)
|
||||||
|
set(CMAKE_C_FLAGS_${UBSAN} "${CMAKE_C_FLAGS_RELWITHDEBINFO} -fsanitize=undefined -g" CACHE STRING "Flags used by the C compiler during Undefined Behaviour Sanitizer builds.")
|
||||||
|
set(CMAKE_CXX_FLAGS_${UBSAN} "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -fsanitize=undefined -g" CACHE STRING "Flags used by the C++ compiler during Undefined Behaviour Sanitizer builds.")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_${UBSAN} "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} -fsanitize=undefined -g" CACHE STRING "Flags used by the linker during Undefined Behavior Sanitizer builds.")
|
||||||
|
else()
|
||||||
|
message("The undefined behavior sanitizer is only available on ${_ubsan_platforms}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
message("Sanitizers not supported for the current compiler: ${CMAKE_CXX_COMPILER_ID}.")
|
||||||
|
endif()
|
||||||
|
endfunction()
|
37
cmake/libprotoc.cmake
Normal file
37
cmake/libprotoc.cmake
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# CMake definitions for libprotoc (the protobuf compiler library).
|
||||||
|
|
||||||
|
include(${protobuf_SOURCE_DIR}/src/file_lists.cmake)
|
||||||
|
include(${protobuf_SOURCE_DIR}/cmake/protobuf-configure-target.cmake)
|
||||||
|
|
||||||
|
add_library(libprotoc ${protobuf_SHARED_OR_STATIC}
|
||||||
|
${libprotoc_srcs}
|
||||||
|
${libprotoc_hdrs}
|
||||||
|
${protobuf_version_rc_file})
|
||||||
|
if(protobuf_HAVE_LD_VERSION_SCRIPT)
|
||||||
|
if(${CMAKE_VERSION} VERSION_GREATER 3.13 OR ${CMAKE_VERSION} VERSION_EQUAL 3.13)
|
||||||
|
target_link_options(libprotoc PRIVATE -Wl,--version-script=${protobuf_SOURCE_DIR}/src/libprotoc.map)
|
||||||
|
elseif(protobuf_BUILD_SHARED_LIBS)
|
||||||
|
target_link_libraries(libprotoc PRIVATE -Wl,--version-script=${protobuf_SOURCE_DIR}/src/libprotoc.map)
|
||||||
|
endif()
|
||||||
|
set_target_properties(libprotoc PROPERTIES
|
||||||
|
LINK_DEPENDS ${protobuf_SOURCE_DIR}/src/libprotoc.map)
|
||||||
|
endif()
|
||||||
|
target_link_libraries(libprotoc PRIVATE libprotobuf)
|
||||||
|
target_link_libraries(libprotoc PUBLIC ${protobuf_ABSL_USED_TARGETS})
|
||||||
|
protobuf_configure_target(libprotoc)
|
||||||
|
if(protobuf_BUILD_SHARED_LIBS)
|
||||||
|
target_compile_definitions(libprotoc
|
||||||
|
PUBLIC PROTOBUF_USE_DLLS
|
||||||
|
PRIVATE LIBPROTOC_EXPORTS)
|
||||||
|
endif()
|
||||||
|
set_target_properties(libprotoc PROPERTIES
|
||||||
|
COMPILE_DEFINITIONS LIBPROTOC_EXPORTS
|
||||||
|
VERSION ${protobuf_VERSION}
|
||||||
|
OUTPUT_NAME ${LIB_PREFIX}protoc
|
||||||
|
DEBUG_POSTFIX "${protobuf_DEBUG_POSTFIX}"
|
||||||
|
# For -fvisibility=hidden and -fvisibility-inlines-hidden
|
||||||
|
C_VISIBILITY_PRESET hidden
|
||||||
|
CXX_VISIBILITY_PRESET hidden
|
||||||
|
VISIBILITY_INLINES_HIDDEN ON
|
||||||
|
)
|
||||||
|
add_library(protobuf::libprotoc ALIAS libprotoc)
|
14
cmake/protoc.cmake
Normal file
14
cmake/protoc.cmake
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
set(protoc_files
|
||||||
|
${protobuf_SOURCE_DIR}/src/google/protobuf/compiler/main.cc
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(protoc ${protoc_files} ${protobuf_version_rc_file})
|
||||||
|
target_link_libraries(protoc
|
||||||
|
libprotoc
|
||||||
|
libprotobuf
|
||||||
|
${protobuf_ABSL_USED_TARGETS}
|
||||||
|
)
|
||||||
|
add_executable(protobuf::protoc ALIAS protoc)
|
||||||
|
|
||||||
|
set_target_properties(protoc PROPERTIES
|
||||||
|
VERSION ${protobuf_VERSION})
|
8
dbus.cpp
8
dbus.cpp
|
@ -2,6 +2,8 @@
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
#include "backend.hpp"
|
#include "backend.hpp"
|
||||||
#include <random>
|
#include <random>
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <fmt/format.h>
|
||||||
#ifdef DBUS_ENABLED
|
#ifdef DBUS_ENABLED
|
||||||
MprisAPI::MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api)
|
MprisAPI::MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api)
|
||||||
: AdaptorInterfaces(connection, std::move(objectPath))
|
: AdaptorInterfaces(connection, std::move(objectPath))
|
||||||
|
@ -91,7 +93,7 @@ std::vector<meta_t> MprisAPI::GetTracksMetadata(const std::vector<track_id_t> &T
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (auto stream : streams) {
|
for (auto stream : streams) {
|
||||||
std::map<std::string, sdbus::Variant> meta;
|
std::map<std::string, sdbus::Variant> meta;
|
||||||
meta["mpris:trackid"] = std::format("{}{}", streamPrefix, i);
|
meta["mpris:trackid"] = fmt::format("{}{}", streamPrefix, i);
|
||||||
meta["xesam:title"] = stream.name;
|
meta["xesam:title"] = stream.name;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
@ -121,7 +123,7 @@ std::vector<track_id_t> MprisAPI::Tracks() {
|
||||||
auto streams = dbus_api->playback->get_streams();
|
auto streams = dbus_api->playback->get_streams();
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (auto stream : streams) {
|
for (auto stream : streams) {
|
||||||
output.push_back(std::format("{}{}", streamPrefix, i));
|
output.push_back(fmt::format("{}{}", streamPrefix, i));
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
|
@ -317,7 +319,7 @@ std::string DBusAPI::CreateHandle() {
|
||||||
size_t hash = std::hash<size_t>()(idx);
|
size_t hash = std::hash<size_t>()(idx);
|
||||||
size_t rand = (size_t)rand_engine();
|
size_t rand = (size_t)rand_engine();
|
||||||
size_t rand_hash = std::hash<size_t>()(rand);
|
size_t rand_hash = std::hash<size_t>()(rand);
|
||||||
std::string value = std::format("{}:{}:{}", idx, hash, rand_hash);
|
std::string value = fmt::format("{}:{}:{}", idx, hash, rand_hash);
|
||||||
handles[value] = (void*)&value;
|
handles[value] = (void*)&value;
|
||||||
playback->register_handle(handles[value]);
|
playback->register_handle(handles[value]);
|
||||||
return value;
|
return value;
|
||||||
|
|
153
file_backend.cpp
Normal file
153
file_backend.cpp
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
#include "file_backend.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
File::File(const char *fname) {
|
||||||
|
open(fname);
|
||||||
|
}
|
||||||
|
size_t File::get_len() {
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
CFile::CFile(const char *fname) : File(fname) {
|
||||||
|
open(fname);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CFile::open(const char *fname) {
|
||||||
|
name = fname;
|
||||||
|
file = fopen(fname, "rb");
|
||||||
|
if (file == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fseek(file, 0, SEEK_END);
|
||||||
|
len = ftell(file);
|
||||||
|
fseek(file, 0, SEEK_SET);
|
||||||
|
}
|
||||||
|
void CFile::close() {
|
||||||
|
if (file == NULL) return;
|
||||||
|
fclose(file);
|
||||||
|
file = NULL;
|
||||||
|
}
|
||||||
|
size_t CFile::read(void *ptr, size_t len) {
|
||||||
|
if (file == NULL) return 0;
|
||||||
|
return fread(ptr, 1, len, file);
|
||||||
|
}
|
||||||
|
void CFile::seek(size_t pos, SeekType seek_type) {
|
||||||
|
int whence;
|
||||||
|
switch (seek_type) {
|
||||||
|
case SeekType::SET: {
|
||||||
|
whence = SEEK_SET;
|
||||||
|
} break;
|
||||||
|
case SeekType::CUR: {
|
||||||
|
whence = SEEK_CUR;
|
||||||
|
} break;
|
||||||
|
case SeekType::END: {
|
||||||
|
whence = SEEK_END;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
if (file == NULL) return;
|
||||||
|
fseek(file, pos, whence);
|
||||||
|
}
|
||||||
|
size_t CFile::get_pos() {
|
||||||
|
if (file == NULL) return 0;
|
||||||
|
return ftell(file);
|
||||||
|
}
|
||||||
|
bool CFile::is_open() {
|
||||||
|
return file != NULL;
|
||||||
|
}
|
||||||
|
size_t rwops_read(SDL_RWops *rwops, void *ptr, size_t size, size_t maxnum) {
|
||||||
|
File *file = (File*)rwops->hidden.unknown.data1;
|
||||||
|
uint8_t *ptr8 = (uint8_t*)ptr;
|
||||||
|
size_t out = 0;
|
||||||
|
size_t tmp;
|
||||||
|
for (size_t i = 0; i < maxnum; i++) {
|
||||||
|
tmp = file->read(ptr8 + (i * size), size);
|
||||||
|
out++;
|
||||||
|
if (tmp == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
int rwops_close(SDL_RWops *rwops) {
|
||||||
|
File *file = (File*)rwops->hidden.unknown.data1;
|
||||||
|
file->close();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Sint64 rwops_seek(SDL_RWops *rwops, Sint64 offset, int whence) {
|
||||||
|
SeekType type;
|
||||||
|
switch (whence) {
|
||||||
|
case RW_SEEK_CUR: {
|
||||||
|
type = SeekType::CUR;
|
||||||
|
} break;
|
||||||
|
case RW_SEEK_SET: {
|
||||||
|
type = SeekType::SET;
|
||||||
|
} break;
|
||||||
|
case RW_SEEK_END: {
|
||||||
|
type = SeekType::END;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
File *file = (File*)rwops->hidden.unknown.data1;
|
||||||
|
file->seek((size_t)offset, type);
|
||||||
|
return file->get_pos();
|
||||||
|
}
|
||||||
|
Sint64 rwops_size(SDL_RWops *rwops) {
|
||||||
|
FILE_TYPE *file = (FILE_TYPE*)rwops->hidden.unknown.data1;
|
||||||
|
return file->get_len();
|
||||||
|
}
|
||||||
|
SDL_RWops *get_sdl_file(File *file) {
|
||||||
|
SDL_RWops *rwops = new SDL_RWops();
|
||||||
|
rwops->read = &rwops_read;
|
||||||
|
rwops->close = &rwops_close;
|
||||||
|
rwops->write = NULL;
|
||||||
|
rwops->seek = &rwops_seek;
|
||||||
|
rwops->size = &rwops_size;
|
||||||
|
rwops->type = SDL_RWOPS_UNKNOWN;
|
||||||
|
rwops->hidden.unknown.data1 = file;
|
||||||
|
return rwops;
|
||||||
|
}
|
||||||
|
struct file_streamfile {
|
||||||
|
STREAMFILE vt;
|
||||||
|
File *file;
|
||||||
|
};
|
||||||
|
static file_streamfile *sf_open(file_streamfile *sf, const char *const filename, size_t buf_size) {
|
||||||
|
sf->file->open(filename);
|
||||||
|
return sf;
|
||||||
|
}
|
||||||
|
static size_t sf_read(file_streamfile *sf, uint8_t *dst, offv_t offset, size_t length) {
|
||||||
|
File *file = sf->file;
|
||||||
|
file->seek(offset, SeekType::SET);
|
||||||
|
return file->read(dst, length);
|
||||||
|
}
|
||||||
|
static size_t sf_size(file_streamfile *sf) {
|
||||||
|
File *file = sf->file;
|
||||||
|
return file->get_len();
|
||||||
|
}
|
||||||
|
static void sf_close(file_streamfile *sf) {
|
||||||
|
File *file = sf->file;
|
||||||
|
file->close();
|
||||||
|
}
|
||||||
|
static void sf_get_name(file_streamfile *sf, char *name, size_t name_size) {
|
||||||
|
File *file = sf->file;
|
||||||
|
int copy_size = strlen(file->name) + 1;
|
||||||
|
if (copy_size > name_size) copy_size = name_size;
|
||||||
|
memcpy(name, file->name, copy_size);
|
||||||
|
name[copy_size - 1] = '\0';
|
||||||
|
}
|
||||||
|
static offv_t sf_get_offset(file_streamfile *sf) {
|
||||||
|
return (offv_t)sf->file->get_pos();
|
||||||
|
}
|
||||||
|
STREAMFILE *get_sf_from_file(File *file) {
|
||||||
|
file_streamfile *sf = new file_streamfile();
|
||||||
|
sf->vt.read = (size_t(*)(STREAMFILE*, uint8_t*, offv_t, size_t))(void*)sf_read;
|
||||||
|
sf->vt.get_size = (size_t(*)(STREAMFILE*))(void*)(sf_size);
|
||||||
|
sf->vt.get_offset = (offv_t(*)(STREAMFILE*))(void*)(sf_get_offset);
|
||||||
|
sf->vt.get_name = (void(*)(STREAMFILE*,char*,size_t))(void*)(sf_get_name);
|
||||||
|
sf->vt.open = (STREAMFILE*(*)(STREAMFILE*,const char *const, size_t))(sf_open);
|
||||||
|
sf->vt.close = (void(*)(STREAMFILE*))(void*)(sf_close);
|
||||||
|
sf->vt.stream_index = 0;
|
||||||
|
sf->file = file;
|
||||||
|
return &sf->vt;
|
||||||
|
}
|
||||||
|
File *open_file(const char *name) {
|
||||||
|
return new FILE_TYPE(name);
|
||||||
|
}
|
75
file_backend.hpp
Normal file
75
file_backend.hpp
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <vector>
|
||||||
|
extern "C" {
|
||||||
|
#include <vgmstream.h>
|
||||||
|
}
|
||||||
|
enum class SeekType {
|
||||||
|
SET,
|
||||||
|
CUR,
|
||||||
|
END
|
||||||
|
};
|
||||||
|
class File {
|
||||||
|
protected:
|
||||||
|
size_t len;
|
||||||
|
public:
|
||||||
|
const char *name;
|
||||||
|
File(const char *fname);
|
||||||
|
inline virtual void open(const char *fname) { name = fname; }
|
||||||
|
virtual void close() = 0;
|
||||||
|
virtual size_t read(void *ptr, size_t len) = 0;
|
||||||
|
virtual void seek(size_t pos, SeekType seek_type) = 0;
|
||||||
|
virtual size_t get_len();
|
||||||
|
virtual size_t get_pos() = 0;
|
||||||
|
virtual bool is_open() = 0;
|
||||||
|
};
|
||||||
|
class CFile : public File {
|
||||||
|
protected:
|
||||||
|
FILE *file;
|
||||||
|
public:
|
||||||
|
CFile(const char *fname);
|
||||||
|
void open(const char *fname) override;
|
||||||
|
void close() override;
|
||||||
|
size_t read(void *ptr, size_t len) override;
|
||||||
|
void seek(size_t pos, SeekType seek_type) override;
|
||||||
|
size_t get_pos() override;
|
||||||
|
bool is_open() override;
|
||||||
|
};
|
||||||
|
class HttpFile : public File {
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
public:
|
||||||
|
HttpFile(const char *url);
|
||||||
|
void open(const char *url) override;
|
||||||
|
void close() override;
|
||||||
|
size_t read(void *ptr, size_t len) override;
|
||||||
|
void seek(size_t pos, SeekType type) override;
|
||||||
|
size_t get_pos();
|
||||||
|
bool is_open();
|
||||||
|
};
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
class AndroidFile : public File {
|
||||||
|
protected:
|
||||||
|
void *handle;
|
||||||
|
public:
|
||||||
|
AndroidFile(const char *fname);
|
||||||
|
void open(const char *fname) override;
|
||||||
|
void close() override;
|
||||||
|
size_t read(void *ptr, size_t len) override;
|
||||||
|
void seek(size_t pos, SeekType seek_type) override;
|
||||||
|
size_t get_pos() override;
|
||||||
|
bool is_open() override;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#define FILE_TYPE AndroidFile
|
||||||
|
#else
|
||||||
|
#define FILE_TYPE CFile
|
||||||
|
#endif
|
||||||
|
STREAMFILE *get_sf_from_file(File *file);
|
||||||
|
SDL_RWops *get_sdl_file(File *file);
|
||||||
|
File *open_file(const char *fname);
|
||||||
|
File *open_url(const char *url);
|
|
@ -4,15 +4,29 @@ import os.path as path
|
||||||
import sys
|
import sys
|
||||||
from glob import glob
|
from glob import glob
|
||||||
import json
|
import json
|
||||||
|
|
||||||
olddir = os.curdir
|
olddir = os.curdir
|
||||||
scriptdir = path.realpath(path.dirname(__file__))
|
scriptdir = path.realpath(path.dirname(__file__))
|
||||||
os.chdir(scriptdir)
|
os.chdir(scriptdir)
|
||||||
outpath = path.join(sys.argv[1], "backend_glue.cpp")
|
outpath = path.join(sys.argv[1], "backend_glue.cpp")
|
||||||
ui_backend_dir = path.join(scriptdir, "backends", "ui")
|
ui_backend_dir = path.join(scriptdir, "backends", "ui")
|
||||||
ui_backend_metafiles = []
|
ui_backend_metafiles = []
|
||||||
|
playback_backend_dir = path.join(scriptdir, "backends", "playback")
|
||||||
|
playback_backend_metafiles = []
|
||||||
|
adding_uis = True
|
||||||
for backend in sys.argv[2:]:
|
for backend in sys.argv[2:]:
|
||||||
|
if backend == "--ui":
|
||||||
|
adding_uis = True
|
||||||
|
continue
|
||||||
|
elif backend == "--playback":
|
||||||
|
adding_uis = False
|
||||||
|
continue
|
||||||
|
if adding_uis:
|
||||||
ui_backend_metafiles += [ui_backend_dir + "/" + backend + "/ui.json"]
|
ui_backend_metafiles += [ui_backend_dir + "/" + backend + "/ui.json"]
|
||||||
|
else:
|
||||||
|
playback_backend_metafiles += [playback_backend_dir + "/" + backend + "/backend.json"]
|
||||||
ui_backends = []
|
ui_backends = []
|
||||||
|
playback_backends = []
|
||||||
for metafile_path in ui_backend_metafiles:
|
for metafile_path in ui_backend_metafiles:
|
||||||
with open(metafile_path, "rt") as metafile:
|
with open(metafile_path, "rt") as metafile:
|
||||||
metajson = json.load(metafile)
|
metajson = json.load(metafile)
|
||||||
|
@ -20,14 +34,30 @@ for metafile_path in ui_backend_metafiles:
|
||||||
incpath = path.relpath(incpath, scriptdir)
|
incpath = path.relpath(incpath, scriptdir)
|
||||||
ui_backend = {"class_name": metajson["class_name"], "include_path": incpath}
|
ui_backend = {"class_name": metajson["class_name"], "include_path": incpath}
|
||||||
ui_backends.append(ui_backend)
|
ui_backends.append(ui_backend)
|
||||||
|
for metafile_path in playback_backend_metafiles:
|
||||||
|
with open(metafile_path, "rt") as metafile:
|
||||||
|
metajson = json.load(metafile)
|
||||||
|
incpath = path.join(path.dirname(metafile_path), metajson["include_path"])
|
||||||
|
incpath = path.relpath(incpath, scriptdir)
|
||||||
|
playback_backend = {"class_name": metajson["class_name"], "include_path": incpath}
|
||||||
|
playback_backends.append(playback_backend)
|
||||||
with open(outpath, "wt+") as of:
|
with open(outpath, "wt+") as of:
|
||||||
|
of.write("#include \"playback_backend.hpp\"\n")
|
||||||
of.write("#include \"backend.hpp\"\n")
|
of.write("#include \"backend.hpp\"\n")
|
||||||
for ui_backend in ui_backends:
|
for ui_backend in ui_backends:
|
||||||
of.write("#include \"%s\"\n" % ui_backend["include_path"])
|
of.write("#include \"%s\"\n" % ui_backend["include_path"])
|
||||||
|
for playback_backend in playback_backends:
|
||||||
|
of.write("#include \"%s\"\n" % playback_backend["include_path"])
|
||||||
of.write("""
|
of.write("""
|
||||||
void init_backends() {
|
void init_backends() {
|
||||||
""")
|
""")
|
||||||
for ui_backend in ui_backends:
|
for ui_backend in ui_backends:
|
||||||
of.write("\tUIBackend::register_backend<%s>();\n" % ui_backend["class_name"])
|
of.write("\tUIBackend::register_backend<%s>();\n" % ui_backend["class_name"])
|
||||||
of.write("}\n")
|
of.write("}\n")
|
||||||
|
of.write("""
|
||||||
|
void init_playback_backends() {
|
||||||
|
""")
|
||||||
|
for playback_backend in playback_backends:
|
||||||
|
of.write("\tPlaybackBackend::register_backend<%s>();\n" % playback_backend["class_name"])
|
||||||
|
of.write("}\n")
|
||||||
os.chdir(olddir)
|
os.chdir(olddir)
|
30
grpc.patch
Normal file
30
grpc.patch
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
diff --git a/.gitmodules b/.gitmodules
|
||||||
|
index 9b16a02..614613f 100644
|
||||||
|
--- a/.gitmodules
|
||||||
|
+++ b/.gitmodules
|
||||||
|
@@ -7,9 +7,9 @@
|
||||||
|
[submodule "third_party/bloaty"]
|
||||||
|
path = third_party/bloaty
|
||||||
|
url = https://github.com/google/bloaty.git
|
||||||
|
-[submodule "third_party/boringssl-with-bazel"]
|
||||||
|
- path = third_party/boringssl-with-bazel
|
||||||
|
- url = https://github.com/google/boringssl.git
|
||||||
|
+#[submodule "third_party/boringssl-with-bazel"]
|
||||||
|
+# path = third_party/boringssl-with-bazel
|
||||||
|
+# url = https://github.com/google/boringssl.git
|
||||||
|
[submodule "third_party/cares/cares"]
|
||||||
|
path = third_party/cares/cares
|
||||||
|
url = https://github.com/c-ares/c-ares.git
|
||||||
|
diff --git a/include/grpcpp/impl/proto_utils.h b/include/grpcpp/impl/proto_utils.h
|
||||||
|
index 20d1435..21cd206 100644
|
||||||
|
--- a/include/grpcpp/impl/proto_utils.h
|
||||||
|
+++ b/include/grpcpp/impl/proto_utils.h
|
||||||
|
@@ -20,7 +20,7 @@
|
||||||
|
#define GRPCPP_IMPL_PROTO_UTILS_H
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
-
|
||||||
|
+#include <google/protobuf/stubs/status.h>
|
||||||
|
#include "absl/log/absl_check.h"
|
||||||
|
|
||||||
|
#include <grpc/byte_buffer_reader.h>
|
89
ipc/common.proto
Normal file
89
ipc/common.proto
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
|
||||||
|
enum PropertyId {
|
||||||
|
BackendSpecific = 0;
|
||||||
|
StreamIdProperty = 1;
|
||||||
|
StreamsProperty = 2;
|
||||||
|
TitleProperty = 3;
|
||||||
|
FilenameProperty = 4;
|
||||||
|
FilePathProperty = 5;
|
||||||
|
PositionProperty = 6;
|
||||||
|
SpecProperty = 7;
|
||||||
|
BackendId = 8;
|
||||||
|
BackendName = 9;
|
||||||
|
};
|
||||||
|
message StringProperty {
|
||||||
|
string value = 506;
|
||||||
|
}
|
||||||
|
message StreamId {
|
||||||
|
uint64 id = 504;
|
||||||
|
};
|
||||||
|
message Stream {
|
||||||
|
uint64 id = 501;
|
||||||
|
double len = 502;
|
||||||
|
string title = 503;
|
||||||
|
};
|
||||||
|
message StreamList {
|
||||||
|
repeated Stream streams = 500;
|
||||||
|
}
|
||||||
|
message Position {
|
||||||
|
double pos = 508;
|
||||||
|
};
|
||||||
|
enum EndianID {
|
||||||
|
LITTLE = 0;
|
||||||
|
BIG = 1;
|
||||||
|
};
|
||||||
|
enum FormatType {
|
||||||
|
UNSIGNED = 0;
|
||||||
|
SIGNED = 1;
|
||||||
|
FLOAT = 2;
|
||||||
|
};
|
||||||
|
message AudioSpec {
|
||||||
|
uint32 channel_count = 509;
|
||||||
|
EndianID endian = 510;
|
||||||
|
FormatType format_type = 511;
|
||||||
|
uint32 bits = 512;
|
||||||
|
uint64 sample_rate = 513;
|
||||||
|
optional uint64 min_samples = 514;
|
||||||
|
optional uint64 max_samples = 515;
|
||||||
|
}
|
||||||
|
message PropertyData {
|
||||||
|
PropertyId id = 100;
|
||||||
|
optional uint64 idx = 108;
|
||||||
|
google.protobuf.Any value = 101;
|
||||||
|
};
|
||||||
|
message GetProperty {
|
||||||
|
PropertyId id = 102;
|
||||||
|
optional string path = 107;
|
||||||
|
optional uint64 idx = 103;
|
||||||
|
};
|
||||||
|
message SetProperty {
|
||||||
|
PropertyId id = 104;
|
||||||
|
optional string path = 109;
|
||||||
|
optional uint64 idx = 105;
|
||||||
|
google.protobuf.Any value = 106;
|
||||||
|
};
|
||||||
|
message ResetProperty {
|
||||||
|
string path = 110;
|
||||||
|
optional uint64 idx = 111;
|
||||||
|
};
|
||||||
|
|
||||||
|
message ErrorResponse {
|
||||||
|
string id = 5000;
|
||||||
|
optional string desc = 5001;
|
||||||
|
bool fatal = 5002;
|
||||||
|
};
|
||||||
|
|
||||||
|
message PropertyDataOrError {
|
||||||
|
oneof data {
|
||||||
|
PropertyData output = 1;
|
||||||
|
ErrorResponse err = 2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
message ResetResponse {
|
||||||
|
optional PropertyDataOrError value = 1;
|
||||||
|
}
|
||||||
|
message MaybeError {
|
||||||
|
optional ErrorResponse err = 1;
|
||||||
|
};
|
20
ipc/controller.proto
Normal file
20
ipc/controller.proto
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
import "common.proto";
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
message PlayCommand {
|
||||||
|
string filename = 1;
|
||||||
|
optional uint64 idx = 2;
|
||||||
|
};
|
||||||
|
message SetStreamCommand {
|
||||||
|
uint64 idx = 1;
|
||||||
|
}
|
||||||
|
service PlaybackControlService {
|
||||||
|
rpc Get(GetProperty) returns (PropertyDataOrError) {}
|
||||||
|
rpc Set(SetProperty) returns (MaybeError) {}
|
||||||
|
rpc Reset(ResetProperty) returns (PropertyDataOrError) {}
|
||||||
|
rpc Quit() returns (MaybeError) {}
|
||||||
|
rpc Play(PlayCommand) returns (MaybeError) {}
|
||||||
|
rpc Stop() returns (MaybeError) {}
|
||||||
|
rpc Pause() returns (MaybeError) {}
|
||||||
|
rpc SetStream(SetStreamCommand) returns (MaybeError) {}
|
||||||
|
}
|
46
ipc/internal.proto
Normal file
46
ipc/internal.proto
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
import "common.proto";
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
message RenderCommand {
|
||||||
|
uint64 len = 201;
|
||||||
|
};
|
||||||
|
message InitResponse {
|
||||||
|
};
|
||||||
|
message QuitCmd {
|
||||||
|
|
||||||
|
};
|
||||||
|
message RenderResponse {
|
||||||
|
uint64 len = 302;
|
||||||
|
bytes data = 303;
|
||||||
|
};
|
||||||
|
message LogMessage {
|
||||||
|
uint64 timespec = 6000;
|
||||||
|
uint32 level = 6001;
|
||||||
|
string msg = 6002;
|
||||||
|
};
|
||||||
|
message RenderResponseOrError {
|
||||||
|
oneof data {
|
||||||
|
RenderResponse output = 1;
|
||||||
|
ErrorResponse err = 2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
message SimpleAckResponse {
|
||||||
|
|
||||||
|
};
|
||||||
|
message InitCommand {
|
||||||
|
string filename = 1;
|
||||||
|
uint64 idx = 2;
|
||||||
|
};
|
||||||
|
service PlaybackProcessService {
|
||||||
|
rpc Render(RenderCommand) returns (RenderResponseOrError) {}
|
||||||
|
rpc Get(GetProperty) returns (PropertyDataOrError) {}
|
||||||
|
rpc Set(SetProperty) returns (MaybeError) {}
|
||||||
|
rpc Reset(ResetProperty) returns (ResetResponse) {}
|
||||||
|
rpc Quit(QuitCmd) returns (MaybeError) {}
|
||||||
|
rpc Init(InitCommand) returns (MaybeError) {}
|
||||||
|
};
|
||||||
|
service HostProcess {
|
||||||
|
rpc WriteLog(LogMessage) returns (SimpleAckResponse) {}
|
||||||
|
rpc SetAddress(StringProperty) returns (MaybeError) {}
|
||||||
|
};
|
123
log.cpp
123
log.cpp
|
@ -6,6 +6,7 @@
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#endif
|
#endif
|
||||||
namespace Looper::Log {
|
namespace Looper::Log {
|
||||||
|
|
||||||
std::set<FILE*> LogStream::global_outputs;
|
std::set<FILE*> LogStream::global_outputs;
|
||||||
int LogStream::log_level =
|
int LogStream::log_level =
|
||||||
#ifdef DEBUG_MODE
|
#ifdef DEBUG_MODE
|
||||||
|
@ -23,8 +24,9 @@ namespace Looper::Log {
|
||||||
}
|
}
|
||||||
return used_outputs;
|
return used_outputs;
|
||||||
}
|
}
|
||||||
std::string line;
|
std::recursive_mutex log_mutex;
|
||||||
void LogStream::writec(const char chr) {
|
void LogStream::writec(const char chr) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
bool is_newline = (chr == '\n' || chr == '\r');
|
bool is_newline = (chr == '\n' || chr == '\r');
|
||||||
if (my_log_level < log_level) {
|
if (my_log_level < log_level) {
|
||||||
return;
|
return;
|
||||||
|
@ -54,6 +56,7 @@ namespace Looper::Log {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void LogStream::writeprefix() {
|
void LogStream::writeprefix() {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
if (need_prefix) {
|
if (need_prefix) {
|
||||||
need_prefix = false;
|
need_prefix = false;
|
||||||
for (auto name : names) {
|
for (auto name : names) {
|
||||||
|
@ -64,11 +67,13 @@ namespace Looper::Log {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void LogStream::writes(const char *msg) {
|
void LogStream::writes(const char *msg) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
while (*msg != '\0') {
|
while (*msg != '\0') {
|
||||||
writec(*(msg++));
|
writec(*(msg++));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void LogStream::writesn(const char *msg, size_t n) {
|
void LogStream::writesn(const char *msg, size_t n) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
for (size_t i = 0; i < n && msg[i] != '\0'; i++) {
|
for (size_t i = 0; i < n && msg[i] != '\0'; i++) {
|
||||||
writec(msg[i]);
|
writec(msg[i]);
|
||||||
}
|
}
|
||||||
|
@ -77,24 +82,29 @@ namespace Looper::Log {
|
||||||
LogStream::writesn(msg.c_str(), msg.size());
|
LogStream::writesn(msg.c_str(), msg.size());
|
||||||
}
|
}
|
||||||
void LogStream::writeln(const char *msg) {
|
void LogStream::writeln(const char *msg) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
LogStream::writes(msg);
|
LogStream::writes(msg);
|
||||||
LogStream::writec('\n');
|
LogStream::writec('\n');
|
||||||
}
|
}
|
||||||
void LogStream::writeln_n(const char *msg, size_t n) {
|
void LogStream::writeln_n(const char *msg, size_t n) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
LogStream::writesn(msg, n);
|
LogStream::writesn(msg, n);
|
||||||
LogStream::writec('\n');
|
LogStream::writec('\n');
|
||||||
}
|
}
|
||||||
void LogStream::writeln(std::string msg) {
|
void LogStream::writeln(std::string msg) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
LogStream::writes(msg);
|
LogStream::writes(msg);
|
||||||
LogStream::writec('\n');
|
LogStream::writec('\n');
|
||||||
}
|
}
|
||||||
void LogStream::writef(const char *fmt, ...) {
|
void LogStream::writef(const char *fmt, ...) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, fmt);
|
va_start(args, fmt);
|
||||||
vwritef(fmt, args);
|
vwritef(fmt, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
}
|
}
|
||||||
void LogStream::vwritef(const char *fmt, va_list args) {
|
void LogStream::vwritef(const char *fmt, va_list args) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
const char *buf = vcformat(fmt, args);
|
const char *buf = vcformat(fmt, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
if (buf == NULL) {
|
if (buf == NULL) {
|
||||||
|
@ -104,15 +114,45 @@ namespace Looper::Log {
|
||||||
free((void*)buf);
|
free((void*)buf);
|
||||||
}
|
}
|
||||||
void LogStream::vwritefln(const char *fmt, va_list args) {
|
void LogStream::vwritefln(const char *fmt, va_list args) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
vwritef(fmt, args);
|
vwritef(fmt, args);
|
||||||
writec('\n');
|
writec('\n');
|
||||||
}
|
}
|
||||||
void LogStream::writefln(const char *fmt, ...) {
|
void LogStream::writefln(const char *fmt, ...) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, fmt);
|
va_start(args, fmt);
|
||||||
vwritefln(fmt, args);
|
vwritefln(fmt, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
}
|
}
|
||||||
|
void LogStream::write_level_prefix(size_t level) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
|
for (size_t i = 0; i < level; i++) {
|
||||||
|
writes(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void LogStream::write_level(size_t level, const char *msg) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
|
write_level_prefix(level);
|
||||||
|
writeln(msg);
|
||||||
|
}
|
||||||
|
void LogStream::write_level(size_t level, std::string msg) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
|
write_level_prefix(level);
|
||||||
|
writeln(msg);
|
||||||
|
}
|
||||||
|
void LogStream::vwritef_level(size_t level, const char *fmt, va_list args) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
|
write_level_prefix(level);
|
||||||
|
vwritefln(fmt, args);
|
||||||
|
}
|
||||||
|
void LogStream::writef_level(size_t level, const char *fmt, ...) {
|
||||||
|
log_mutex_guard guard(log_mutex);
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vwritef_level(level, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
LogStream::LogStream(std::initializer_list<std::string> names, int log_level, bool nested, void *discriminator)
|
LogStream::LogStream(std::initializer_list<std::string> names, int log_level, bool nested, void *discriminator)
|
||||||
: names(names),
|
: names(names),
|
||||||
need_prefix(true),
|
need_prefix(true),
|
||||||
|
@ -152,41 +192,62 @@ namespace Looper::Log {
|
||||||
this->outputs = std::set(outputs);
|
this->outputs = std::set(outputs);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
static LogStream *debug_stream;
|
LogStreamMap log_streams;
|
||||||
static LogStream *info_stream;
|
void make_log_stream(int stream_id, log_stream_creation_function_t fn) {
|
||||||
static LogStream *warning_stream;
|
if (!log_streams.contains(stream_id)) {
|
||||||
static LogStream *error_stream;
|
log_streams[stream_id] = std::stack<LogStream*>();
|
||||||
void init_logging() {
|
}
|
||||||
|
LogStream *new_stream = fn(stream_id);
|
||||||
|
log_streams[stream_id].push(new_stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::recursive_mutex log_stream_mutex;
|
||||||
|
void init_logging_custom(log_stream_creation_function_t fn) {
|
||||||
|
std::lock_guard<std::recursive_mutex> guard(log_stream_mutex);
|
||||||
|
make_log_stream(-1, fn);
|
||||||
|
make_log_stream(0, fn);
|
||||||
|
make_log_stream(1, fn);
|
||||||
|
make_log_stream(2, fn);
|
||||||
|
}
|
||||||
|
void pop_log_stream(int i) {
|
||||||
|
log_streams[i].pop();
|
||||||
|
}
|
||||||
|
void pop_log_streams() {
|
||||||
|
std::lock_guard<std::recursive_mutex> guard(log_stream_mutex);
|
||||||
|
pop_log_stream(-1);
|
||||||
|
pop_log_stream(0);
|
||||||
|
pop_log_stream(1);
|
||||||
|
pop_log_stream(2);
|
||||||
|
}
|
||||||
|
LogStream *_init_logging_normal(int id) {
|
||||||
|
std::map<int, std::string> stream_names = {
|
||||||
|
{-1, "DEBUG"},
|
||||||
|
{0, "INFO"},
|
||||||
|
{1, "WARNING"},
|
||||||
|
{2, "ERROR"}
|
||||||
|
};
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
debug_stream = new LogStream({"DEBUG"}, {ANDROID_LOG_DEBUG}, -1);
|
std::map<int, int> stream_files = {
|
||||||
info_stream = new LogStream({"INFO"}, {ANDROID_LOG_INFO}, 0);
|
{-1, ANDROID_LOG_DEBUG},
|
||||||
warning_stream = new LogStream({"WARNING"}, {ANDROID_LOG_WARN}, 1);
|
{0, ANDROID_LOG_INFO},
|
||||||
error_stream = new LogStream({"ERROR"}, {ANDROID_LOG_ERROR}, 2);
|
{1, ANDROID_LOG_WARN},
|
||||||
|
{2, ANDROID_LOG_ERROR}
|
||||||
|
};
|
||||||
#else
|
#else
|
||||||
debug_stream = new LogStream({"DEBUG"}, {stderr}, -1);
|
std::map<int, FILE*> stream_files = {
|
||||||
info_stream = new LogStream({"INFO"}, {stdout}, 0);
|
{-1, stderr},
|
||||||
warning_stream = new LogStream({"WARNING"}, {stderr}, 1);
|
{0, stdout},
|
||||||
error_stream = new LogStream({"ERROR"}, {stderr}, 2);
|
{1, stderr},
|
||||||
|
{2, stderr}
|
||||||
|
};
|
||||||
#endif
|
#endif
|
||||||
|
return new LogStream({stream_names[id]}, {stream_files[id]}, id);
|
||||||
|
}
|
||||||
|
void init_logging() {
|
||||||
|
init_logging_custom(_init_logging_normal);
|
||||||
}
|
}
|
||||||
LogStream &get_log_stream_by_level(int level) {
|
LogStream &get_log_stream_by_level(int level) {
|
||||||
switch (level) {
|
return *log_streams[level].top();
|
||||||
case -1: {
|
|
||||||
return *debug_stream;
|
|
||||||
} break;
|
|
||||||
case 0: {
|
|
||||||
return *info_stream;
|
|
||||||
} break;
|
|
||||||
case 1: {
|
|
||||||
return *warning_stream;
|
|
||||||
} break;
|
|
||||||
case 2: {
|
|
||||||
return *error_stream;
|
|
||||||
} break;
|
|
||||||
default: {
|
|
||||||
return *info_stream;
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
37
log.hpp
37
log.hpp
|
@ -6,9 +6,13 @@
|
||||||
#include <config.h>
|
#include <config.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <stack>
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include <functional>
|
||||||
namespace Looper::Log {
|
namespace Looper::Log {
|
||||||
struct LogStream {
|
struct LogStream {
|
||||||
std::set<FILE *> outputs;
|
std::set<FILE *> outputs;
|
||||||
|
@ -21,21 +25,27 @@ namespace Looper::Log {
|
||||||
std::set<FILE*> get_used_outputs();
|
std::set<FILE*> get_used_outputs();
|
||||||
|
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
std::string line;
|
|
||||||
std::set<android_LogPriority> android_outputs;
|
std::set<android_LogPriority> android_outputs;
|
||||||
#endif
|
#endif
|
||||||
|
std::string line;
|
||||||
LogStream(std::initializer_list<std::string> names, int log_level, bool nested, void* discriminator);
|
LogStream(std::initializer_list<std::string> names, int log_level, bool nested, void* discriminator);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
typedef std::lock_guard<std::recursive_mutex> log_mutex_guard;
|
||||||
static int log_level;
|
static int log_level;
|
||||||
void writeprefix();
|
void writeprefix();
|
||||||
|
void write_level_prefix(size_t level);
|
||||||
|
void write_level(size_t level, const char *msg);
|
||||||
|
void write_level(size_t level, std::string msg);
|
||||||
|
void writef_level(size_t level, const char *msg, ...);
|
||||||
|
void vwritef_level(size_t level, const char *msg, va_list args);
|
||||||
void writeln(const char *msg);
|
void writeln(const char *msg);
|
||||||
void writeln_n(const char *msg, size_t n);
|
void writeln_n(const char *msg, size_t n);
|
||||||
void writeln(std::string msg);
|
void writeln(std::string msg);
|
||||||
void writes(const char *msg);
|
void writes(const char *msg);
|
||||||
void writesn(const char *msg, size_t n);
|
void writesn(const char *msg, size_t n);
|
||||||
void writes(std::string msg);
|
void writes(std::string msg);
|
||||||
void writec(const char chr);
|
virtual void writec(const char chr);
|
||||||
void vwritef(const char *fmt, va_list args);
|
void vwritef(const char *fmt, va_list args);
|
||||||
void writef(const char *fmt, ...);
|
void writef(const char *fmt, ...);
|
||||||
void vwritefln(const char *fmt, va_list args);
|
void vwritefln(const char *fmt, va_list args);
|
||||||
|
@ -47,14 +57,37 @@ namespace Looper::Log {
|
||||||
#else
|
#else
|
||||||
LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level = 0);
|
LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level = 0);
|
||||||
#endif
|
#endif
|
||||||
|
protected:
|
||||||
};
|
};
|
||||||
void init_logging();
|
void init_logging();
|
||||||
|
void init_logging_subprocess();
|
||||||
|
typedef std::function<LogStream*(int)> log_stream_creation_function_t;
|
||||||
|
void init_logging_custom(log_stream_creation_function_t fn);
|
||||||
LogStream &get_log_stream_by_level(int level);
|
LogStream &get_log_stream_by_level(int level);
|
||||||
#define LOG(level) (Looper::Log::get_log_stream_by_level(level))
|
#define LOG(level) (Looper::Log::get_log_stream_by_level(level))
|
||||||
#define DEBUG LOG(-1)
|
#define DEBUG LOG(-1)
|
||||||
#define INFO LOG(0)
|
#define INFO LOG(0)
|
||||||
#define WARNING LOG(1)
|
#define WARNING LOG(1)
|
||||||
#define ERROR LOG(2)
|
#define ERROR LOG(2)
|
||||||
|
void pop_log_streams();
|
||||||
|
class Context {
|
||||||
|
std::function<void()> end;
|
||||||
|
public:
|
||||||
|
inline Context(std::function<void()> start, std::function<void()> end) {
|
||||||
|
start();
|
||||||
|
this->end = end;
|
||||||
|
}
|
||||||
|
inline ~Context() {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
class LogContext : public Context {
|
||||||
|
public:
|
||||||
|
inline LogContext(std::function<void()> push_log_stream_fn) : Context(push_log_stream_fn, pop_log_streams) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
typedef std::map<int, std::stack<LogStream*>> LogStreamMap;
|
||||||
}
|
}
|
||||||
extern "C" {
|
extern "C" {
|
||||||
void write_log(int level, const char *log);
|
void write_log(int level, const char *log);
|
||||||
|
|
35
main.cpp
35
main.cpp
|
@ -23,6 +23,7 @@ std::unordered_set<LicenseData> license_data;
|
||||||
std::unordered_set<LicenseData> &get_license_data() {
|
std::unordered_set<LicenseData> &get_license_data() {
|
||||||
return license_data;
|
return license_data;
|
||||||
}
|
}
|
||||||
|
char *executable_path;
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
@ -48,11 +49,7 @@ void initNative() {
|
||||||
mainActivity = reinterpret_cast<jobject>(env->NewGlobalRef(env->GetStaticObjectField(MainActivity, singleton)));
|
mainActivity = reinterpret_cast<jobject>(env->NewGlobalRef(env->GetStaticObjectField(MainActivity, singleton)));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef LIBRARY_MODE
|
extern "C" int looper_run_as_executable(std::vector<std::string> args) {
|
||||||
extern "C" int looper_run_as_executable(int argc, char **argv) {
|
|
||||||
#else
|
|
||||||
int main(int argc, char **argv) {
|
|
||||||
#endif
|
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
env = (JNIEnv*)SDL_AndroidGetJNIEnv();
|
||||||
initNative();
|
initNative();
|
||||||
|
@ -60,10 +57,6 @@ int main(int argc, char **argv) {
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
EM_ASM({ Module.wasmTable = wasmTable; });
|
EM_ASM({ Module.wasmTable = wasmTable; });
|
||||||
#endif
|
#endif
|
||||||
std::vector<std::string> args;
|
|
||||||
for (int i = 1; i < argc; i++) {
|
|
||||||
args.push_back(std::string(argv[i]));
|
|
||||||
}
|
|
||||||
CLI::App app{DESCRIPTION};
|
CLI::App app{DESCRIPTION};
|
||||||
std::string filename = "";
|
std::string filename = "";
|
||||||
app.allow_extras();
|
app.allow_extras();
|
||||||
|
@ -147,6 +140,8 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
DEBUG.writeln("Initializing frontends...");
|
DEBUG.writeln("Initializing frontends...");
|
||||||
init_backends();
|
init_backends();
|
||||||
|
DEBUG.writeln("Initializing playback backends...");
|
||||||
|
init_playback_backends();
|
||||||
#ifdef DBUS_ENABLED
|
#ifdef DBUS_ENABLED
|
||||||
ProxyGlueBackend *proxy_backend = nullptr;
|
ProxyGlueBackend *proxy_backend = nullptr;
|
||||||
if ((disable_gui && !daemonize) || quit) {
|
if ((disable_gui && !daemonize) || quit) {
|
||||||
|
@ -171,6 +166,10 @@ int main(int argc, char **argv) {
|
||||||
for (auto kv : UIBackend::backends) {
|
for (auto kv : UIBackend::backends) {
|
||||||
DEBUG.writefln(" - '%s'", kv.first.c_str());
|
DEBUG.writefln(" - '%s'", kv.first.c_str());
|
||||||
}
|
}
|
||||||
|
DEBUG.writeln("Loaded playback backends: ");
|
||||||
|
for (auto kv : PlaybackBackendHelper()) {
|
||||||
|
DEBUG.writefln(" - '%s'", kv.first.c_str());
|
||||||
|
}
|
||||||
DEBUG.writeln("Loading options file...");
|
DEBUG.writeln("Loading options file...");
|
||||||
load_options();
|
load_options();
|
||||||
std::string backend_id = get_option<std::string>("ui.frontend", "imgui");
|
std::string backend_id = get_option<std::string>("ui.frontend", "imgui");
|
||||||
|
@ -231,3 +230,21 @@ int main(int argc, char **argv) {
|
||||||
#endif
|
#endif
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
extern int looper_run_playback_process(std::vector<std::string> args);
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
CLI::App app{DESCRIPTION};
|
||||||
|
std::string process_type = "normal";
|
||||||
|
app.add_option<std::string, std::string>("--process-type", process_type);
|
||||||
|
app.allow_extras();
|
||||||
|
app.parse(argc, argv);
|
||||||
|
executable_path = argv[0];
|
||||||
|
if (process_type == "playback") {
|
||||||
|
return looper_run_playback_process(app.remaining(false));
|
||||||
|
} else if (process_type == "daemon") {
|
||||||
|
std::vector<std::string> opts = app.remaining(false);
|
||||||
|
opts.push_back("-d");
|
||||||
|
return looper_run_as_executable(opts);
|
||||||
|
} else {
|
||||||
|
return looper_run_as_executable(app.remaining(false));
|
||||||
|
}
|
||||||
|
}
|
14
options.hpp
14
options.hpp
|
@ -109,7 +109,19 @@ namespace Looper::Options {
|
||||||
if (initial_value.has_value()) {
|
if (initial_value.has_value()) {
|
||||||
init_option<T>(name, initial_value.value());
|
init_option<T>(name, initial_value.value());
|
||||||
}
|
}
|
||||||
return (**(OPTIONS.at_path(name).as<T>()));
|
auto option = OPTIONS.at_path(name);
|
||||||
|
return (**(option).as<T>());
|
||||||
|
}
|
||||||
|
template<>
|
||||||
|
inline bool get_option<bool>(std::string name, std::optional<bool> initial_value) {
|
||||||
|
if (initial_value.has_value()) {
|
||||||
|
init_option<bool>(name, initial_value.value());
|
||||||
|
}
|
||||||
|
auto option = OPTIONS.at_path(name).as<bool>();
|
||||||
|
if (option == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (option->get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#define UI_BACKEND() (Looper::Options::get_option<std::string>("ui.frontend", "imgui"))
|
#define UI_BACKEND() (Looper::Options::get_option<std::string>("ui.frontend", "imgui"))
|
395
playback.cpp
395
playback.cpp
|
@ -19,6 +19,7 @@ extern "C" {
|
||||||
#include "dbus.hpp"
|
#include "dbus.hpp"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "util.hpp"
|
#include "util.hpp"
|
||||||
|
#include "file_backend.hpp"
|
||||||
using namespace std::chrono;
|
using namespace std::chrono;
|
||||||
|
|
||||||
int NotSDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data, int src_len,
|
int NotSDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data, int src_len,
|
||||||
|
@ -81,39 +82,35 @@ void PlaybackInstance::SDLCallbackInner(Uint8 *stream, int len) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
size_t unit = sizeof(SAMPLETYPE) * spec.channels;
|
size_t unit = sizeof(SAMPLETYPE) * spec.channels;
|
||||||
size_t bytes_per_iter = std::min(((bufsize / unit)) * unit, (size_t)fakespec.size);
|
size_t bytes_per_iter = ((bufsize / unit)) * unit;
|
||||||
while (st->numSamples() < (size_t)len) {
|
while (st->numSamples() < (size_t)len) {
|
||||||
if (general_mixer == nullptr && stream == nullptr) {
|
if (process == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Uint8 *new_buf;
|
Uint8 *new_buf;
|
||||||
int new_bufsize;
|
int new_bufsize;
|
||||||
new_buf = buf;
|
new_buf = buf;
|
||||||
|
bytes_per_iter = backend_spec.size;
|
||||||
new_bufsize = bytes_per_iter;
|
new_bufsize = bytes_per_iter;
|
||||||
if (this->stream != nullptr) {
|
if (this->process != nullptr) {
|
||||||
size_t samples = (int)bytes_per_iter / sizeof(SAMPLETYPE) / this->stream->channels;
|
size_t new_bytes = this->process->render(buf, bytes_per_iter);
|
||||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
|
if (SDL_AudioStreamPut(sdl_stream, buf, new_bytes) < 0) {
|
||||||
size_t new_samples = render_vgmstream((sample_t*)(buf), (int)samples, this->stream);
|
ERROR.writefln("SDL_AudioStreamPut: %s", SDL_GetError());
|
||||||
#else
|
DEBUG.writefln("Current audio backend: %s", process->get_backend_id().c_str());
|
||||||
size_t new_samples = render_vgmstream((sample_t*)(buf), (int)samples, this->stream);
|
DEBUG.writefln("SDL_AudioStream *sdl_stream = %0lx", (size_t)sdl_stream);
|
||||||
vgmstream_spec.samples = new_samples;
|
return;
|
||||||
SDL_AudioStreamPut(sdl_stream, buf, new_samples * sizeof(sample_t) * this->stream->channels);
|
}
|
||||||
new_bufsize = SDL_AudioStreamGet(sdl_stream, buf, bufsize);
|
new_bufsize = SDL_AudioStreamGet(sdl_stream, buf, bufsize);
|
||||||
#endif
|
if (new_bufsize < 0) {
|
||||||
if (samples > new_samples) {
|
ERROR.writefln("SDL_AudioStreamGet: %s", SDL_GetError());
|
||||||
reset_vgmstream(this->stream);
|
DEBUG.writefln("Current audio backend: %s", process->get_backend_id().c_str());
|
||||||
position = 0.0;
|
return;
|
||||||
} else {
|
|
||||||
position += samples / this->stream->sample_rate;
|
|
||||||
}
|
}
|
||||||
samples = new_samples;
|
SAMPLETYPE *sbuf = (SAMPLETYPE*)buf;
|
||||||
for (int i = 0; i < new_bufsize / sizeof(SAMPLETYPE); i++) {
|
for (size_t i = 0; i < (new_bufsize / unit) * spec.channels; i++) {
|
||||||
((SAMPLETYPE*)new_buf)[i] *= real_volume;
|
sbuf[i] *= real_volume;
|
||||||
}
|
}
|
||||||
st->putSamples((SAMPLETYPE*)new_buf, new_bufsize / unit);
|
st->putSamples(sbuf, new_bufsize / unit);
|
||||||
} else {
|
|
||||||
general_mixer(nullptr, buf, (int)bytes_per_iter);
|
|
||||||
st->putSamples((SAMPLETYPE*)new_buf, new_bufsize / unit);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
st->receiveSamples((SAMPLETYPE*)stream, len / unit);
|
st->receiveSamples((SAMPLETYPE*)stream, len / unit);
|
||||||
|
@ -130,182 +127,71 @@ oboe::DataCallbackResult PlaybackInstance::onAudioReady(
|
||||||
void PlaybackInstance::SDLCallback(void *userdata, Uint8 *stream, int len) {
|
void PlaybackInstance::SDLCallback(void *userdata, Uint8 *stream, int len) {
|
||||||
((PlaybackInstance*)userdata)->SDLCallbackInner(stream, len);
|
((PlaybackInstance*)userdata)->SDLCallbackInner(stream, len);
|
||||||
}
|
}
|
||||||
Mix_Music *PlaybackInstance::LoadMix(const char *file) {
|
void PlaybackInstance::Load(const char *file, int idx) {
|
||||||
stream_list_mutex.lock();
|
|
||||||
streams.clear();
|
|
||||||
stream_list_mutex.unlock();
|
|
||||||
{
|
|
||||||
std::filesystem::path fpath = std::string(file);
|
|
||||||
fpath = fpath.parent_path() / fpath.stem();
|
|
||||||
|
|
||||||
std::string fpath_str = fpath.string();
|
|
||||||
std::vector<std::string> soundfonts = {fpath_str + ".sf2", fpath_str + ".dls"};
|
|
||||||
std::string sf_path_str = "";
|
|
||||||
bool any_path_exists = false;
|
|
||||||
for (auto sf_path : soundfonts) {
|
|
||||||
if (std::filesystem::exists(sf_path)) {
|
|
||||||
any_path_exists = true;
|
|
||||||
sf_path_str += ";" + sf_path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (any_path_exists) {
|
|
||||||
sf_path_str = sf_path_str.substr(1);
|
|
||||||
Mix_SetSoundFonts(sf_path_str.c_str());
|
|
||||||
} else {
|
|
||||||
Mix_SetSoundFonts(NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Mix_Music *output = Mix_LoadMUS(file);
|
|
||||||
if (output == nullptr) {
|
|
||||||
ERROR.writefln("Error loading music '%s': %s", file, Mix_GetError());
|
|
||||||
set_error("Error loading music!");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
Mix_PlayMusicStream(output, -1);
|
|
||||||
length = Mix_MusicDuration(output);
|
|
||||||
update.store(true);
|
|
||||||
current_file_mutex.lock();
|
|
||||||
current_file = std::string(file);
|
|
||||||
const char *title_tag = Mix_GetMusicTitleTag(output);
|
|
||||||
// Check for an empty string, which indicates there's no title tag.
|
|
||||||
if (title_tag[0] == '\0') {
|
|
||||||
std::filesystem::path path(current_file.value());
|
|
||||||
current_title = path.stem().string();
|
|
||||||
} else {
|
|
||||||
current_title = std::string(title_tag);
|
|
||||||
}
|
|
||||||
current_file_mutex.unlock();
|
|
||||||
load_finished.store(true);
|
|
||||||
set_signal(PlaybackSignalFileChanged);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
VGMSTREAM *PlaybackInstance::LoadVgm(const char *file, int idx) {
|
|
||||||
SDL_LockAudioDevice(device);
|
SDL_LockAudioDevice(device);
|
||||||
STREAMFILE *sf = open_stdio_streamfile(file);
|
playback_ready.store(false);
|
||||||
if (!sf) {
|
if (process != nullptr) delete process;
|
||||||
ERROR.writefln("Could not find file '%s'", file);
|
process = new PlaybackProcess(file, idx);
|
||||||
SDL_UnlockAudioDevice(device);
|
if (process->process_running()) {
|
||||||
return nullptr;
|
auto backend_spec_proxy = process->get_audio_spec();
|
||||||
}
|
memset(&backend_spec, 0, sizeof(backend_spec));
|
||||||
auto *output = init_vgmstream_from_STREAMFILE(sf);
|
backend_spec.channels = backend_spec_proxy->channel_count();
|
||||||
if (!output) {
|
audio_data_t sample_fmt;
|
||||||
DEBUG.writeln("VGMStream init failed.");
|
sample_fmt.size = backend_spec_proxy->bits() / 8;
|
||||||
SDL_UnlockAudioDevice(device);
|
sample_fmt.endian = backend_spec_proxy->endian() == EndianID::BIG;
|
||||||
return nullptr;
|
sample_fmt.is_float = backend_spec_proxy->format_type() == FormatType::FLOAT;
|
||||||
}
|
sample_fmt.is_signed = backend_spec_proxy->format_type() != FormatType::UNSIGNED;
|
||||||
int stream_count = output->num_streams;
|
backend_spec.format = sample_spec_to_sdl(sample_fmt);
|
||||||
close_vgmstream(output);
|
backend_spec.freq = backend_spec_proxy->sample_rate();
|
||||||
stream_list_mutex.lock();
|
backend_spec.samples = backend_spec_proxy->has_max_samples() ? backend_spec_proxy->max_samples() : backend_spec_proxy->has_min_samples() ? backend_spec_proxy->min_samples() : 0;
|
||||||
streams.clear();
|
if (backend_spec.samples == 0) backend_spec.samples = 100;
|
||||||
//PlaybackStream defaultStream;
|
DEBUG.writeln("Backend audio specification:");
|
||||||
//defaultStream.id = 0;
|
DEBUG.writefln("\tFormat: %s", sdl_to_str(backend_spec.format).c_str());
|
||||||
//defaultStream.name = "Default";
|
DEBUG.writefln("\tChannels: %d", backend_spec.channels);
|
||||||
//streams.push_back(defaultStream);
|
DEBUG.writefln("\tSample rate: %d", backend_spec.freq);
|
||||||
for (int i = 0; i <= stream_count; i++) {
|
DEBUG.writefln("\tSamples: %d", backend_spec.samples);
|
||||||
PlaybackStream stream;
|
if (sdl_stream != nullptr) {
|
||||||
stream.id = i;
|
|
||||||
stream.length = 0;
|
|
||||||
stream.name = "";
|
|
||||||
if (!sf) {
|
|
||||||
streams.push_back(stream);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sf->stream_index = i;
|
|
||||||
auto *tmp = init_vgmstream_from_STREAMFILE(sf);
|
|
||||||
if (!tmp) {
|
|
||||||
streams.push_back(stream);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
reset_vgmstream(tmp);
|
|
||||||
|
|
||||||
stream.length = (double)tmp->num_samples / (double)tmp->sample_rate;
|
|
||||||
char *buf = (char*)malloc(STREAM_NAME_SIZE + 1);
|
|
||||||
memset(buf, 0, STREAM_NAME_SIZE + 1);
|
|
||||||
strncpy(buf, tmp->stream_name, STREAM_NAME_SIZE);
|
|
||||||
if (buf[0] == '\0') {
|
|
||||||
free(buf);
|
|
||||||
buf = strdup("Unknown");
|
|
||||||
}
|
|
||||||
if (i == 0) {
|
|
||||||
char *buf2 = NULL;
|
|
||||||
#define DEFAULT_FORMAT_STR "Default (%s)"
|
|
||||||
size_t buflen = snprintf(NULL, 0, DEFAULT_FORMAT_STR, buf) + 1;
|
|
||||||
buf2= (char*)malloc(buflen);
|
|
||||||
memset(buf2, 0, buflen);
|
|
||||||
snprintf(buf2, buflen, DEFAULT_FORMAT_STR, buf);
|
|
||||||
stream.name = buf2;
|
|
||||||
free(buf);
|
|
||||||
} else {
|
|
||||||
stream.name = buf;
|
|
||||||
}
|
|
||||||
DEBUG.writefln("Stream %d: '%s' (Length: %s)", stream.id, stream.name.c_str(), TimeToString(stream.length).c_str());
|
|
||||||
streams.push_back(stream);
|
|
||||||
free(buf);
|
|
||||||
close_vgmstream(tmp);
|
|
||||||
}
|
|
||||||
sf->stream_index = idx;
|
|
||||||
output = init_vgmstream_from_STREAMFILE(sf);
|
|
||||||
close_streamfile(sf);
|
|
||||||
if (!output) {
|
|
||||||
DEBUG.writeln("VGMStream init failed.");
|
|
||||||
SDL_UnlockAudioDevice(device);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
stream_list_mutex.unlock();
|
|
||||||
|
|
||||||
//vgmstream_set_loop_target(stream, -1);
|
|
||||||
vgmstream_cfg_t cfg = {0};
|
|
||||||
cfg.allow_play_forever = 1;
|
|
||||||
cfg.play_forever = 1;
|
|
||||||
//cfg.disable_config_override = 1;
|
|
||||||
vgmstream_apply_config(output, &cfg);
|
|
||||||
vgmstream_spec.channels = output->channels;
|
|
||||||
vgmstream_spec.freq = output->sample_rate;
|
|
||||||
|
|
||||||
length = streams[idx].length;
|
|
||||||
update.store(true);
|
|
||||||
current_file_mutex.lock();
|
|
||||||
current_file = std::string(file);
|
|
||||||
const char *title_tag = output->stream_name;
|
|
||||||
// Check for an empty string, which indicates there's no title tag.
|
|
||||||
if (title_tag[0] == '\0') {
|
|
||||||
std::filesystem::path path(current_file.value());
|
|
||||||
current_title = path.stem().string();
|
|
||||||
} else {
|
|
||||||
current_title = std::string(title_tag);
|
|
||||||
}
|
|
||||||
sdl_stream = SDL_NewAudioStream(vgmstream_spec.format, vgmstream_spec.channels, vgmstream_spec.freq, spec.format, spec.channels, spec.freq);
|
|
||||||
current_file_mutex.unlock();
|
|
||||||
set_signal(PlaybackSignalFileChanged);
|
|
||||||
SDL_UnlockAudioDevice(device);
|
|
||||||
load_finished.store(true);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
void PlaybackInstance::UnloadMix(Mix_Music *music) {
|
|
||||||
stream_list_mutex.lock();
|
|
||||||
streams.clear();
|
|
||||||
stream_list_mutex.unlock();
|
|
||||||
Mix_HaltMusicStream(music);
|
|
||||||
Mix_FreeMusic(music);
|
|
||||||
current_file_mutex.lock();
|
|
||||||
current_file = {};
|
|
||||||
current_title = {};
|
|
||||||
current_file_mutex.unlock();
|
|
||||||
}
|
|
||||||
void PlaybackInstance::UnloadVgm(VGMSTREAM *stream) {
|
|
||||||
stream_list_mutex.lock();
|
|
||||||
streams.clear();
|
|
||||||
stream_list_mutex.unlock();
|
|
||||||
SDL_LockAudioDevice(device);
|
|
||||||
close_vgmstream(stream);
|
|
||||||
SDL_FreeAudioStream(sdl_stream);
|
SDL_FreeAudioStream(sdl_stream);
|
||||||
this->stream = nullptr;
|
}
|
||||||
sdl_stream = nullptr;
|
sdl_stream = SDL_NewAudioStream(backend_spec.format, backend_spec.channels, backend_spec.freq, spec.format, spec.channels, spec.freq);
|
||||||
|
if (sdl_stream == nullptr) {
|
||||||
|
ERROR.writefln("SDL_NewAudioStream: %s", SDL_GetError());
|
||||||
|
DEBUG.writefln("format: AUDIO_%s%d%s", sample_fmt.is_float ? "F" : sample_fmt.is_signed ? "S" : "U", sample_fmt.size, sample_fmt.endian ? "MSB" : "LSB");
|
||||||
|
set_error("Failed to create SDL audio stream");
|
||||||
|
set_signal(PlaybackSignalErrorOccurred);
|
||||||
|
}
|
||||||
|
size_t required_size = sample_fmt.size * backend_spec.samples;
|
||||||
|
st->setChannels(spec.channels);
|
||||||
|
st->setRate(spec.freq);
|
||||||
|
st->flush();
|
||||||
|
backend_spec.size = required_size;
|
||||||
|
bufsize = backend_spec.size;
|
||||||
|
buf = (Uint8*)malloc(bufsize);
|
||||||
|
if (buf == nullptr) {
|
||||||
|
ERROR.writeln("Failed to allocate memory for playback!");
|
||||||
|
set_error("Failed to allocate memory for playback!");
|
||||||
|
set_signal(PlaybackSignalErrorOccurred);
|
||||||
|
bufsize = 0;
|
||||||
|
}
|
||||||
|
delete backend_spec_proxy;
|
||||||
|
} else {
|
||||||
|
ERROR.writeln("Failed to detect valid playback backend for file!");
|
||||||
|
set_error("Failed to detect valid backend for file.");
|
||||||
|
set_signal(PlaybackSignalErrorOccurred);
|
||||||
|
delete process;
|
||||||
|
process = nullptr;
|
||||||
|
}
|
||||||
|
SDL_UnlockAudioDevice(device);
|
||||||
|
}
|
||||||
|
void PlaybackInstance::Unload() {
|
||||||
|
if (process == nullptr) return;
|
||||||
|
SDL_LockAudioDevice(device);
|
||||||
|
delete process;
|
||||||
|
process = nullptr;
|
||||||
|
SDL_FreeAudioStream(sdl_stream);
|
||||||
|
sdl_stream = nullptr;
|
||||||
|
free(buf);
|
||||||
SDL_UnlockAudioDevice(device);
|
SDL_UnlockAudioDevice(device);
|
||||||
current_file_mutex.lock();
|
|
||||||
current_file = {};
|
|
||||||
current_title = {};
|
|
||||||
current_file_mutex.unlock();
|
|
||||||
}
|
}
|
||||||
void PlaybackInstance::UpdateST() {
|
void PlaybackInstance::UpdateST() {
|
||||||
if (speed > 0.0f && speed_changed.exchange(false)) {
|
if (speed > 0.0f && speed_changed.exchange(false)) {
|
||||||
|
@ -326,6 +212,7 @@ double PlaybackInstance::GetMaxSeconds() {
|
||||||
}
|
}
|
||||||
void PlaybackInstance::InitLoopFunction() {
|
void PlaybackInstance::InitLoopFunction() {
|
||||||
bool reload = false;
|
bool reload = false;
|
||||||
|
init_audio_data();
|
||||||
speed_changed.store(true);
|
speed_changed.store(true);
|
||||||
tempo_changed.store(true);
|
tempo_changed.store(true);
|
||||||
pitch_changed.store(true);
|
pitch_changed.store(true);
|
||||||
|
@ -351,7 +238,6 @@ void PlaybackInstance::InitLoopFunction() {
|
||||||
desired.callback = PlaybackInstance::SDLCallback;
|
desired.callback = PlaybackInstance::SDLCallback;
|
||||||
desired.userdata = this;
|
desired.userdata = this;
|
||||||
st = new SoundTouch();
|
st = new SoundTouch();
|
||||||
Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK);
|
|
||||||
#ifndef __ANDROID__
|
#ifndef __ANDROID__
|
||||||
if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) {
|
if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) {
|
||||||
ERROR.writefln("Error opening audio device: '%s'", SDL_GetError());
|
ERROR.writefln("Error opening audio device: '%s'", SDL_GetError());
|
||||||
|
@ -414,35 +300,11 @@ Total samples: %u"
|
||||||
st->setSampleRate(spec.freq);
|
st->setSampleRate(spec.freq);
|
||||||
st->setChannels(spec.channels);
|
st->setChannels(spec.channels);
|
||||||
UpdateST();
|
UpdateST();
|
||||||
bufsize = 0;
|
|
||||||
fakespec = spec;
|
|
||||||
double maxSeconds = GetMaxSeconds();
|
|
||||||
fakespec.size *= maxSeconds;
|
|
||||||
fakespec.samples *= maxSeconds;
|
|
||||||
vgmstream_spec = fakespec;
|
|
||||||
vgmstream_spec.format = AUDIO_S16;
|
|
||||||
size_t new_bufsize = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds);
|
|
||||||
buf = (Uint8*)malloc(new_bufsize);
|
|
||||||
if (buf == nullptr) {
|
|
||||||
ERROR.writeln("Failed to allocate memory for playback!");
|
|
||||||
set_error("Failed to allocate memory for playback!");
|
|
||||||
set_signal(PlaybackSignalErrorOccurred);
|
|
||||||
running = false;
|
|
||||||
loop_started = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bufsize = new_bufsize;
|
|
||||||
general_mixer = Mix_GetGeneralMixer();
|
|
||||||
Mix_InitMixer(&fakespec, SDL_FALSE);
|
|
||||||
SDL_PauseAudioDevice(device, 0);
|
SDL_PauseAudioDevice(device, 0);
|
||||||
music = LoadMix(filePath.c_str());
|
Load(filePath.c_str(), 0);
|
||||||
stream = nullptr;
|
|
||||||
if (music == nullptr) {
|
|
||||||
stream = LoadVgm(filePath.c_str(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
reload = false;
|
reload = false;
|
||||||
if (music || stream) {
|
if (process && process->process_running()) {
|
||||||
playback_ready.store(true);
|
playback_ready.store(true);
|
||||||
} else {
|
} else {
|
||||||
playback_ready.store(false);
|
playback_ready.store(false);
|
||||||
|
@ -453,18 +315,9 @@ Total samples: %u"
|
||||||
void PlaybackInstance::LoopFunction() {
|
void PlaybackInstance::LoopFunction() {
|
||||||
|
|
||||||
if (file_changed.exchange(false) || load_requested.exchange(false)) {
|
if (file_changed.exchange(false) || load_requested.exchange(false)) {
|
||||||
if (stream != nullptr) {
|
Unload();
|
||||||
UnloadVgm(stream);
|
Load(filePath.c_str(), 0);
|
||||||
}
|
if (process && process->process_running()) {
|
||||||
if (music != nullptr) {
|
|
||||||
UnloadMix(music);
|
|
||||||
}
|
|
||||||
music = LoadMix(filePath.c_str());
|
|
||||||
stream = nullptr;
|
|
||||||
if (music == nullptr) {
|
|
||||||
stream = LoadVgm(filePath.c_str(), 0);
|
|
||||||
}
|
|
||||||
if (music || stream) {
|
|
||||||
playback_ready.store(true);
|
playback_ready.store(true);
|
||||||
} else {
|
} else {
|
||||||
playback_ready.store(false);
|
playback_ready.store(false);
|
||||||
|
@ -482,15 +335,11 @@ void PlaybackInstance::LoopFunction() {
|
||||||
current_stream = 0;
|
current_stream = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (stream != nullptr) {
|
SDL_LockAudioDevice(device);
|
||||||
UnloadVgm(stream);
|
if (process && process->process_running()) process->set_stream_idx(current_stream);
|
||||||
stream = LoadVgm(file.c_str(), current_stream);
|
SDL_UnlockAudioDevice(device);
|
||||||
} else if (music != nullptr) {
|
|
||||||
UnloadMix(music);
|
|
||||||
music = LoadMix(file.c_str());
|
|
||||||
}
|
}
|
||||||
}
|
if (process && process->process_running()) {
|
||||||
if (music || stream) {
|
|
||||||
playback_ready.store(true);
|
playback_ready.store(true);
|
||||||
} else {
|
} else {
|
||||||
playback_ready.store(false);
|
playback_ready.store(false);
|
||||||
|
@ -500,81 +349,31 @@ void PlaybackInstance::LoopFunction() {
|
||||||
}
|
}
|
||||||
if (flag_mutex.try_lock()) {
|
if (flag_mutex.try_lock()) {
|
||||||
if (seeking.exchange(false)) {
|
if (seeking.exchange(false)) {
|
||||||
if (stream != nullptr) {
|
if (process && process->process_running()) process->set_position(position);
|
||||||
SDL_LockAudioDevice(device);
|
|
||||||
seek_vgmstream(stream, (int32_t)((double)stream->sample_rate * position));
|
|
||||||
st->flush();
|
|
||||||
SDL_UnlockAudioDevice(device);
|
|
||||||
} else {
|
|
||||||
Mix_SetMusicPositionStream(music, position);
|
|
||||||
}
|
|
||||||
set_signal(PlaybackSignalSeeked);
|
set_signal(PlaybackSignalSeeked);
|
||||||
}
|
}
|
||||||
if (pause_changed.exchange(false)) {
|
if (pause_changed.exchange(false)) {
|
||||||
if (stream != nullptr) {
|
|
||||||
SDL_PauseAudioDevice(device, paused ? 1 : 0);
|
SDL_PauseAudioDevice(device, paused ? 1 : 0);
|
||||||
}
|
|
||||||
if (paused) {
|
if (paused) {
|
||||||
if (music != nullptr) {
|
|
||||||
Mix_PauseMusicStream(music);
|
|
||||||
}
|
|
||||||
set_signal(PlaybackSignalPaused);
|
set_signal(PlaybackSignalPaused);
|
||||||
} else {
|
} else {
|
||||||
if (music != nullptr) {
|
|
||||||
Mix_ResumeMusicStream(music);
|
|
||||||
}
|
|
||||||
set_signal(PlaybackSignalResumed);
|
set_signal(PlaybackSignalResumed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (update.exchange(false)) {
|
if (update.exchange(false)) {
|
||||||
SDL_LockAudioDevice(device);
|
SDL_LockAudioDevice(device);
|
||||||
real_volume = volume / 100.0;
|
real_volume = volume / 100.0;
|
||||||
if (stream == nullptr) {
|
|
||||||
Mix_VolumeMusicStream(music, (int)(volume / 100.0 * MIX_MAX_VOLUME));
|
|
||||||
}
|
|
||||||
UpdateST();
|
UpdateST();
|
||||||
size_t correct_buf_size = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds);
|
|
||||||
size_t max_buf_size = correct_buf_size * 10;
|
|
||||||
bool too_large = max_buf_size < bufsize;
|
|
||||||
bool too_small = correct_buf_size > bufsize;
|
|
||||||
if (too_large) {
|
|
||||||
ERROR.writes("Bufsize is too large - ");
|
|
||||||
} else if (too_small) {
|
|
||||||
ERROR.writes("Bufsize is too small - ");
|
|
||||||
}
|
|
||||||
if (too_large || too_small) {
|
|
||||||
ERROR.writeln("Resizing buffer...");
|
|
||||||
general_mixer = nullptr;
|
|
||||||
bufsize = 0;
|
|
||||||
buf = (Uint8*)realloc((void*)buf, correct_buf_size);
|
|
||||||
if (buf == nullptr) {
|
|
||||||
ERROR.writes("Failed to allocate memory for playback!");
|
|
||||||
set_error("Failed to allocate memory for playback!");
|
|
||||||
set_signal(PlaybackSignalErrorOccurred);
|
|
||||||
running = false;
|
|
||||||
stop_loop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bufsize = correct_buf_size;
|
|
||||||
}
|
|
||||||
SDL_UnlockAudioDevice(device);
|
SDL_UnlockAudioDevice(device);
|
||||||
}
|
}
|
||||||
flag_mutex.unlock();
|
flag_mutex.unlock();
|
||||||
}
|
}
|
||||||
if (music != nullptr) {
|
|
||||||
position = Mix_GetMusicPosition(music);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
void PlaybackInstance::DeinitLoopFunction() {
|
void PlaybackInstance::DeinitLoopFunction() {
|
||||||
playback_ready.store(false);
|
playback_ready.store(false);
|
||||||
// ====
|
// ====
|
||||||
if (music != nullptr) {
|
Unload();
|
||||||
UnloadMix(music);
|
|
||||||
}
|
|
||||||
if (stream != nullptr) {
|
|
||||||
UnloadVgm(stream);
|
|
||||||
}
|
|
||||||
#ifndef __ANDROID__
|
#ifndef __ANDROID__
|
||||||
SDL_CloseAudioDevice(device);
|
SDL_CloseAudioDevice(device);
|
||||||
#else
|
#else
|
||||||
|
@ -584,8 +383,6 @@ void PlaybackInstance::DeinitLoopFunction() {
|
||||||
}
|
}
|
||||||
ostream.reset();
|
ostream.reset();
|
||||||
#endif
|
#endif
|
||||||
Mix_CloseAudio();
|
|
||||||
Mix_Quit();
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
delete st;
|
delete st;
|
||||||
free(buf);
|
free(buf);
|
||||||
|
@ -615,12 +412,16 @@ PlaybackInstance::PlaybackInstance() {
|
||||||
speed = 1.0;
|
speed = 1.0;
|
||||||
pitch = 1.0;
|
pitch = 1.0;
|
||||||
tempo = 1.0;
|
tempo = 1.0;
|
||||||
|
prev_speed = -1.0;
|
||||||
|
prev_pitch = -1.0;
|
||||||
|
prev_tempo = -1.0;
|
||||||
tempo_changed.store(true);
|
tempo_changed.store(true);
|
||||||
speed_changed.store(true);
|
speed_changed.store(true);
|
||||||
pitch_changed.store(true);
|
pitch_changed.store(true);
|
||||||
current_file = {};
|
current_file = {};
|
||||||
playback_ready = false;
|
playback_ready = false;
|
||||||
bufsize = 0;
|
bufsize = 0;
|
||||||
|
process = nullptr;
|
||||||
}
|
}
|
||||||
std::optional<std::string> PlaybackInstance::get_current_file() {
|
std::optional<std::string> PlaybackInstance::get_current_file() {
|
||||||
current_file_mutex.lock();
|
current_file_mutex.lock();
|
||||||
|
|
23
playback.h
23
playback.h
|
@ -19,6 +19,9 @@ extern "C" {
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include "file_backend.hpp"
|
||||||
|
#include "playback_backend.hpp"
|
||||||
|
#include "playback_process.hpp"
|
||||||
using namespace soundtouch;
|
using namespace soundtouch;
|
||||||
using std::span;
|
using std::span;
|
||||||
using std::optional;
|
using std::optional;
|
||||||
|
@ -49,11 +52,6 @@ enum {
|
||||||
/// @brief Playback has started. If @ref PlaybackSignalStopped has also been signalled, call @ref Playback::IsStopped to find out if playback is currently playing.
|
/// @brief Playback has started. If @ref PlaybackSignalStopped has also been signalled, call @ref Playback::IsStopped to find out if playback is currently playing.
|
||||||
PlaybackSignalStarted = 1 << 9
|
PlaybackSignalStarted = 1 << 9
|
||||||
};
|
};
|
||||||
struct PlaybackStream {
|
|
||||||
double length;
|
|
||||||
std::string name;
|
|
||||||
int id;
|
|
||||||
};
|
|
||||||
/// @brief Playback handler base class.
|
/// @brief Playback handler base class.
|
||||||
class Playback {
|
class Playback {
|
||||||
protected:
|
protected:
|
||||||
|
@ -230,6 +228,7 @@ public:
|
||||||
int32_t numFrames) override;
|
int32_t numFrames) override;
|
||||||
private:
|
private:
|
||||||
#endif
|
#endif
|
||||||
|
PlaybackProcess *process;
|
||||||
std::string filePath;
|
std::string filePath;
|
||||||
std::atomic_bool running;
|
std::atomic_bool running;
|
||||||
std::atomic_bool file_changed;
|
std::atomic_bool file_changed;
|
||||||
|
@ -252,20 +251,15 @@ private:
|
||||||
bool paused;
|
bool paused;
|
||||||
Uint8* buf;
|
Uint8* buf;
|
||||||
size_t bufsize;
|
size_t bufsize;
|
||||||
Mix_CommonMixer_t general_mixer;
|
|
||||||
SDL_AudioDeviceID device;
|
SDL_AudioDeviceID device;
|
||||||
SoundTouch *st;
|
SoundTouch *st;
|
||||||
SDL_AudioSpec spec;
|
SDL_AudioSpec spec;
|
||||||
SDL_AudioStream *sdl_stream;
|
SDL_AudioSpec backend_spec;
|
||||||
/// @brief A fake SDL_AudioSpec used to trick SDL Mixer X into allocating a bigger buffer.
|
SDL_AudioStream *sdl_stream = nullptr;
|
||||||
SDL_AudioSpec fakespec;
|
|
||||||
SDL_AudioSpec vgmstream_spec;
|
|
||||||
void SDLCallbackInner(Uint8 *stream, int len);
|
void SDLCallbackInner(Uint8 *stream, int len);
|
||||||
static void SDLCallback(void *userdata, Uint8 *stream, int len);
|
static void SDLCallback(void *userdata, Uint8 *stream, int len);
|
||||||
Mix_Music *LoadMix(const char* file);
|
void Load(const char *file, int idx);
|
||||||
VGMSTREAM *LoadVgm(const char *file, int idx = 0);
|
void Unload();
|
||||||
void UnloadMix(Mix_Music* music);
|
|
||||||
void UnloadVgm(VGMSTREAM *stream);
|
|
||||||
VGMSTREAM *stream;
|
VGMSTREAM *stream;
|
||||||
Mix_Music *music;
|
Mix_Music *music;
|
||||||
std::vector<PlaybackStream> streams;
|
std::vector<PlaybackStream> streams;
|
||||||
|
@ -280,6 +274,7 @@ private:
|
||||||
std::optional<std::string> current_file;
|
std::optional<std::string> current_file;
|
||||||
std::optional<std::string> current_title;
|
std::optional<std::string> current_title;
|
||||||
float prev_pitch, prev_speed, prev_tempo;
|
float prev_pitch, prev_speed, prev_tempo;
|
||||||
|
FILE_TYPE *file;
|
||||||
public:
|
public:
|
||||||
PlaybackInstance();
|
PlaybackInstance();
|
||||||
~PlaybackInstance() override;
|
~PlaybackInstance() override;
|
||||||
|
|
108
playback_backend.cpp
Normal file
108
playback_backend.cpp
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
#include "playback_backend.hpp"
|
||||||
|
void PlaybackBackend::init(const char *filename, int idx) {
|
||||||
|
load(filename);
|
||||||
|
switch_stream(idx);
|
||||||
|
}
|
||||||
|
void PlaybackBackend::load(const char *filename) {}
|
||||||
|
void PlaybackBackend::switch_stream(int idx) {}
|
||||||
|
void PlaybackBackend::cleanup() { }
|
||||||
|
size_t PlaybackBackend::render(void *buf, size_t maxlen) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
PlaybackBackend::map PlaybackBackend::backends = PlaybackBackend::map();
|
||||||
|
std::map<int, audio_data_t> data;
|
||||||
|
void init_audio_data() {
|
||||||
|
audio_data_t tmp;
|
||||||
|
tmp.size = sizeof(float);
|
||||||
|
tmp.endian = false;
|
||||||
|
tmp.is_float = true;
|
||||||
|
tmp.is_signed = true;
|
||||||
|
data[AUDIO_F32LSB] = tmp;
|
||||||
|
tmp.endian = true;
|
||||||
|
data[AUDIO_F32MSB] = tmp;
|
||||||
|
tmp.size = sizeof(int32_t);
|
||||||
|
tmp.is_float = false;
|
||||||
|
data[AUDIO_S32MSB] = tmp;
|
||||||
|
tmp.endian = false;
|
||||||
|
data[AUDIO_S32LSB] = tmp;
|
||||||
|
tmp.size = sizeof(int16_t);
|
||||||
|
data[AUDIO_S16LSB] = tmp;
|
||||||
|
tmp.endian = true;
|
||||||
|
data[AUDIO_S16MSB] = tmp;
|
||||||
|
tmp.is_signed = false;
|
||||||
|
data[AUDIO_U16MSB] = tmp;
|
||||||
|
tmp.endian = false;
|
||||||
|
data[AUDIO_U16LSB] = tmp;
|
||||||
|
tmp.size = sizeof(uint8_t);
|
||||||
|
data[AUDIO_U8] = tmp;
|
||||||
|
tmp.is_signed = true;
|
||||||
|
data[AUDIO_S8] = tmp;
|
||||||
|
}
|
||||||
|
size_t size_of_sample_type(int type) {
|
||||||
|
return data[type].size;
|
||||||
|
}
|
||||||
|
#define SAMPLE_TYPE_HAS_ENDIAN(type) (data[type].size > 1)
|
||||||
|
bool sample_type_has_endian(int type) {
|
||||||
|
return SAMPLE_TYPE_HAS_ENDIAN(type);
|
||||||
|
}
|
||||||
|
bool sample_type_endian_msb(int type) {
|
||||||
|
return SAMPLE_TYPE_HAS_ENDIAN(type) && data[type].endian;
|
||||||
|
}
|
||||||
|
bool sample_type_endian_lsb(int type) {
|
||||||
|
return SAMPLE_TYPE_HAS_ENDIAN(type) && (!data[type].endian);
|
||||||
|
}
|
||||||
|
bool sample_type_is_float(int type) {
|
||||||
|
return data[type].is_float;
|
||||||
|
}
|
||||||
|
bool sample_type_is_integer(int type) {
|
||||||
|
return !data[type].is_float;
|
||||||
|
}
|
||||||
|
bool sample_type_is_signed(int type) {
|
||||||
|
return data[type].is_signed;
|
||||||
|
}
|
||||||
|
bool sample_type_is_unsigned(int type) {
|
||||||
|
return !data[type].is_signed;
|
||||||
|
}
|
||||||
|
int sample_spec_to_sdl(audio_data_t spec) {
|
||||||
|
if (spec.is_float) {
|
||||||
|
return spec.endian ? AUDIO_F32MSB : AUDIO_F32LSB;
|
||||||
|
} else {
|
||||||
|
switch (spec.size) {
|
||||||
|
case 1:
|
||||||
|
return spec.is_signed ? AUDIO_S8 : AUDIO_U8;
|
||||||
|
case 2:
|
||||||
|
if (spec.is_signed) {
|
||||||
|
return spec.endian ? AUDIO_S16MSB : AUDIO_S16LSB;
|
||||||
|
} else {
|
||||||
|
return spec.endian ? AUDIO_U16MSB : AUDIO_U16LSB;
|
||||||
|
}
|
||||||
|
case 4:
|
||||||
|
return spec.endian ? AUDIO_S32MSB : AUDIO_S32LSB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
audio_data_t sdl_to_sample_spec(int type) {
|
||||||
|
return data[type];
|
||||||
|
}
|
||||||
|
std::string sdl_to_str(int type) {
|
||||||
|
audio_data_t data = sdl_to_sample_spec(type);
|
||||||
|
std::string signed_text[3] = {"Unsigned", "Signed", "Float"};
|
||||||
|
char *bit_text_buf = (char*)calloc(8, 1);
|
||||||
|
snprintf(bit_text_buf, 8, " %d-bit", (int)(data.size * 8));
|
||||||
|
std::string bit_text = bit_text_buf;
|
||||||
|
free(bit_text_buf);
|
||||||
|
std::string endian_text[2] = {" (Little Endian", " (Big Endian"};
|
||||||
|
if (AUDIO_U16SYS == AUDIO_U16LSB) {
|
||||||
|
endian_text[0] += ", System";
|
||||||
|
} else {
|
||||||
|
endian_text[1] += ", System";
|
||||||
|
}
|
||||||
|
endian_text[0] += ")";
|
||||||
|
endian_text[1] += ")";
|
||||||
|
if (data.size == 1) {
|
||||||
|
return signed_text[data.is_signed ? 1 : 0] + bit_text;
|
||||||
|
} else {
|
||||||
|
return signed_text[data.is_float ? 2 : data.is_signed ? 1 : 0] + bit_text + endian_text[data.endian ? 1 : 0];
|
||||||
|
}
|
||||||
|
}
|
191
playback_backend.hpp
Normal file
191
playback_backend.hpp
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <iterator>
|
||||||
|
#include "ipc/internal.pb.h"
|
||||||
|
struct PlaybackStream {
|
||||||
|
double length;
|
||||||
|
std::string name;
|
||||||
|
int id;
|
||||||
|
};
|
||||||
|
class PlaybackBackendHelper;
|
||||||
|
class PlaybackBackend {
|
||||||
|
protected:
|
||||||
|
double length;
|
||||||
|
std::vector<PlaybackStream> streams;
|
||||||
|
std::string current_file;
|
||||||
|
std::string current_title;
|
||||||
|
bool open;
|
||||||
|
SDL_AudioSpec spec;
|
||||||
|
double position;
|
||||||
|
std::map<std::string, google::protobuf::Any> property_defaults;
|
||||||
|
std::map<std::string, google::protobuf::Any> properties;
|
||||||
|
public:
|
||||||
|
using map = std::map<std::string, PlaybackBackend*>;
|
||||||
|
using iterator = map::iterator;
|
||||||
|
using const_iterator = map::const_iterator;
|
||||||
|
using reverse_iterator = map::reverse_iterator;
|
||||||
|
using const_reverse_iterator = map::const_reverse_iterator;
|
||||||
|
inline virtual std::string get_id() {return "";}
|
||||||
|
inline virtual std::string get_name() {return "";}
|
||||||
|
inline virtual void seek(double position) { }
|
||||||
|
inline virtual double get_position() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
inline virtual SDL_AudioSpec get_spec() {
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
inline virtual bool set(std::string path, google::protobuf::Any value) {properties[path] = value; return true;}
|
||||||
|
inline virtual std::optional<google::protobuf::Any> get(std::string path) {return properties.contains(path) ? properties[path] : std::optional<google::protobuf::Any>();}
|
||||||
|
inline virtual std::optional<google::protobuf::Any> reset(std::string path) {
|
||||||
|
if (property_defaults.contains(path)) {
|
||||||
|
properties[path] = property_defaults[path];
|
||||||
|
} else {
|
||||||
|
properties.erase(path);
|
||||||
|
}
|
||||||
|
return get(path);
|
||||||
|
}
|
||||||
|
virtual void load(const char *filename);
|
||||||
|
virtual void init(const char *filename, int idx = 0);
|
||||||
|
virtual void switch_stream(int idx);
|
||||||
|
inline virtual std::vector<PlaybackStream> get_streams() {
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
inline virtual uint64_t get_min_samples() {return 0;}
|
||||||
|
inline virtual std::optional<uint64_t> get_max_samples() {return {};}
|
||||||
|
virtual void cleanup();
|
||||||
|
virtual size_t render(void *buf, size_t maxlen);
|
||||||
|
inline virtual double get_length() {
|
||||||
|
return open ? length : 0.0;
|
||||||
|
}
|
||||||
|
inline virtual std::optional<std::string> get_current_file() {
|
||||||
|
return open ? current_file : std::optional<std::string>();
|
||||||
|
}
|
||||||
|
inline virtual std::optional<std::string> get_title() {
|
||||||
|
return open ? current_title : std::optional<std::string>();
|
||||||
|
}
|
||||||
|
inline virtual int get_stream_idx() {return 0;}
|
||||||
|
inline virtual ~PlaybackBackend() { }
|
||||||
|
static map backends;
|
||||||
|
template<class T>
|
||||||
|
static inline std::string get_backend_id() {
|
||||||
|
PlaybackBackend *backend = new T();
|
||||||
|
auto output = backend->get_id();
|
||||||
|
delete backend;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
static inline PlaybackBackend* get_first_backend() {
|
||||||
|
if (backends.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return (*backends.begin()).second;
|
||||||
|
}
|
||||||
|
static void register_backend(PlaybackBackend *backend) {
|
||||||
|
std::string backend_id = backend->get_id();
|
||||||
|
if (backends.contains(backend_id)) { // Guard against potential memory leak due to reassigning a new pointer without deallocating the previous one
|
||||||
|
delete backend;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
backends[backend_id] = backend;
|
||||||
|
}
|
||||||
|
template<class T>
|
||||||
|
static void register_backend() {
|
||||||
|
PlaybackBackend *backend = new T();
|
||||||
|
backend->register_self();
|
||||||
|
}
|
||||||
|
static inline void unregister_backend(std::string id) {
|
||||||
|
if (backends.contains(id)) {
|
||||||
|
auto backend = backends[id];
|
||||||
|
delete backend;
|
||||||
|
backends.erase(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline void unregister_self() {
|
||||||
|
PlaybackBackend::unregister_backend(get_id());
|
||||||
|
}
|
||||||
|
inline void register_self() {
|
||||||
|
PlaybackBackend::register_backend(this);
|
||||||
|
}
|
||||||
|
template<class T>
|
||||||
|
static void unregister_backend() {
|
||||||
|
unregister_backend(get_backend_id<T>());
|
||||||
|
}
|
||||||
|
static void deinit_backends();
|
||||||
|
static inline std::optional<PlaybackBackend*> get_backend(std::string id) {
|
||||||
|
if (backends.contains(id)) {
|
||||||
|
return backends[id];
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template<class T>
|
||||||
|
static inline std::optional<T*> get_backend() {
|
||||||
|
auto id = get_backend_id<T>();
|
||||||
|
auto output = get_backend(id);
|
||||||
|
if (output.has_value()) {
|
||||||
|
return (T*)output.value();
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
class PlaybackBackendHelper {
|
||||||
|
using iterator = PlaybackBackend::iterator;
|
||||||
|
using const_iterator = PlaybackBackend::const_iterator;
|
||||||
|
using reverse_iterator = PlaybackBackend::reverse_iterator;
|
||||||
|
using const_reverse_iterator = PlaybackBackend::const_reverse_iterator;
|
||||||
|
public:
|
||||||
|
inline iterator begin() {
|
||||||
|
return PlaybackBackend::backends.begin();
|
||||||
|
}
|
||||||
|
inline const_iterator cbegin() const {
|
||||||
|
return PlaybackBackend::backends.cbegin();
|
||||||
|
}
|
||||||
|
inline iterator end() {
|
||||||
|
return PlaybackBackend::backends.end();
|
||||||
|
}
|
||||||
|
inline const_iterator cend() const {
|
||||||
|
return PlaybackBackend::backends.cend();
|
||||||
|
}
|
||||||
|
inline reverse_iterator rbegin() {
|
||||||
|
return PlaybackBackend::backends.rbegin();
|
||||||
|
}
|
||||||
|
inline const_reverse_iterator crbegin() const {
|
||||||
|
return PlaybackBackend::backends.crbegin();
|
||||||
|
}
|
||||||
|
inline reverse_iterator rend() {
|
||||||
|
return PlaybackBackend::backends.rend();
|
||||||
|
}
|
||||||
|
inline const_reverse_iterator crend() const {
|
||||||
|
return PlaybackBackend::backends.crend();
|
||||||
|
}
|
||||||
|
inline size_t size() {
|
||||||
|
return PlaybackBackend::backends.size();
|
||||||
|
}
|
||||||
|
inline bool empty() {
|
||||||
|
return PlaybackBackend::backends.empty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void init_playback_backends();
|
||||||
|
struct audio_data_t {
|
||||||
|
size_t size;
|
||||||
|
bool endian;
|
||||||
|
bool is_signed;
|
||||||
|
bool is_float;
|
||||||
|
};
|
||||||
|
void init_audio_data();
|
||||||
|
size_t size_of_sample_type(int type);
|
||||||
|
bool sample_type_has_endian(int type);
|
||||||
|
bool sample_type_endian_msb(int type);
|
||||||
|
bool sample_type_endian_lsb(int type);
|
||||||
|
bool sample_type_is_float(int type);
|
||||||
|
bool smaple_type_is_integer(int type);
|
||||||
|
bool sample_type_is_signed(int type);
|
||||||
|
bool sample_type_is_unsigned(int type);
|
||||||
|
int sample_spec_to_sdl(audio_data_t spec);
|
||||||
|
audio_data_t sdl_to_sample_spec(int type);
|
||||||
|
std::string sdl_to_str(int type);
|
521
playback_process.cpp
Normal file
521
playback_process.cpp
Normal file
|
@ -0,0 +1,521 @@
|
||||||
|
|
||||||
|
#include <google/protobuf/descriptor.h>
|
||||||
|
#include <google/protobuf/wire_format_lite.h>
|
||||||
|
#include <grpcpp/client_context.h>
|
||||||
|
#include <grpcpp/impl/codegen/server_interface.h>
|
||||||
|
#include <grpcpp/server_builder.h>
|
||||||
|
#include <grpcpp/support/status.h>
|
||||||
|
#include "playback_process.hpp"
|
||||||
|
#include "ipc/common.pb.h"
|
||||||
|
#include "playback_backend.hpp"
|
||||||
|
#include <functional>
|
||||||
|
#include "rpc.hpp"
|
||||||
|
#include "thirdparty/CRC.hpp"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <exception>
|
||||||
|
#include <thread>
|
||||||
|
#include <config.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include "backend.hpp"
|
||||||
|
#include "util.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include <chrono>
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#ifdef __WINDOWS__
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
#include <google/protobuf/message.h>
|
||||||
|
using namespace google::protobuf;
|
||||||
|
int sndfd;
|
||||||
|
int rcvfd;
|
||||||
|
extern char *executable_path;
|
||||||
|
using grpc::ClientContext;
|
||||||
|
void print_field_descriptor(const google::protobuf::FieldDescriptor *fdesc, const google::protobuf::Message &msg, size_t level) {
|
||||||
|
auto desc = msg.GetDescriptor();
|
||||||
|
auto reflection = msg.GetReflection();
|
||||||
|
static std::map<google::protobuf::FieldDescriptor::Label, const char*> labelNames = {
|
||||||
|
{(google::protobuf::FieldDescriptor::Label)0, "(Error)"},
|
||||||
|
{google::protobuf::FieldDescriptor::LABEL_OPTIONAL, "Optional"},
|
||||||
|
{google::protobuf::FieldDescriptor::LABEL_REPEATED, "Repeated"},
|
||||||
|
{google::protobuf::FieldDescriptor::LABEL_REQUIRED, "Required"}
|
||||||
|
};
|
||||||
|
DEBUG.writef_level(level, "- %s = %d", fdesc->full_name().c_str(), fdesc->number());
|
||||||
|
level++;
|
||||||
|
DEBUG.writef_level(level, "Type: %s (C++: %s)", fdesc->type_name(), fdesc->cpp_type_name());
|
||||||
|
std::string value = "(None)";
|
||||||
|
if (fdesc->has_default_value()) {
|
||||||
|
FORMAT_CPP_TYPE_LOWERCASE(fdesc, value, fdesc->default_value_);
|
||||||
|
DEBUG.writef_level(level, "Default value: %s", value.c_str());
|
||||||
|
}
|
||||||
|
DEBUG.writef_level(level, "Label: %s", labelNames.contains(fdesc->label()) ? labelNames[fdesc->label()] : labelNames[(google::protobuf::FieldDescriptor::Label)0]);
|
||||||
|
DEBUG.writef_level(level, "Index: %d", fdesc->index());
|
||||||
|
if (fdesc->type() == google::protobuf::FieldDescriptor::TYPE_MESSAGE || fdesc->type() == google::protobuf::FieldDescriptor::TYPE_GROUP) {
|
||||||
|
const google::protobuf::Descriptor *desc = fdesc->message_type();
|
||||||
|
DEBUG.writef_level(level, "Value: (Message)");
|
||||||
|
const Message &out_msg = reflection->GetMessage(msg, fdesc);
|
||||||
|
print_ipc_message(out_msg, level + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fdesc->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) {
|
||||||
|
value = "(bytes)";
|
||||||
|
} else {
|
||||||
|
FORMAT_CPP_TYPE_TITLECASE(fdesc, value, reflection->Get, msg, fdesc);
|
||||||
|
}
|
||||||
|
DEBUG.writef_level(level, "Value: %s", value.c_str());
|
||||||
|
}
|
||||||
|
void print_ipc_message(const google::protobuf::Message &msg, size_t level) {
|
||||||
|
const google::protobuf::Descriptor *msgdesc = msg.GetDescriptor();
|
||||||
|
const google::protobuf::Reflection *reflect = msg.GetReflection();
|
||||||
|
DEBUG.writef_level(level, "Message type: %s", msg.GetTypeName().c_str());
|
||||||
|
std::vector<const FieldDescriptor *> descriptors;
|
||||||
|
reflect->ListFields(msg, &descriptors);
|
||||||
|
DEBUG.writef_level(level, "Message field count: %d", descriptors.size());
|
||||||
|
for (auto field : descriptors) {
|
||||||
|
print_field_descriptor(field, msg, level);
|
||||||
|
}
|
||||||
|
auto &unknown_fields = reflect->GetUnknownFields(msg);
|
||||||
|
DEBUG.writef_level(level, "Unknown fields: %d", unknown_fields.field_count());
|
||||||
|
}
|
||||||
|
grpc::Status PlaybackProcessServiceImpl::Render(grpc::ServerContext *ctx, const RenderCommand *cmd, RenderResponseOrError *response) {
|
||||||
|
size_t maxlen = cmd->len();
|
||||||
|
void *ptr = malloc(maxlen);
|
||||||
|
size_t len = cur_backend->render(ptr, maxlen);
|
||||||
|
std::string buf = std::string(len, ' ', std::allocator<char>());
|
||||||
|
memcpy(buf.data(), ptr, len);
|
||||||
|
free(ptr);
|
||||||
|
auto output = new RenderResponse();
|
||||||
|
output->set_data(buf);
|
||||||
|
output->set_len(len);
|
||||||
|
response->set_allocated_output(output);
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
grpc::Status PlaybackProcessServiceImpl::Get(grpc::ServerContext *ctx, const GetProperty *request, PropertyDataOrError *response) {
|
||||||
|
PropertyData *data = new PropertyData();
|
||||||
|
ErrorResponse *err_maybe = nullptr;
|
||||||
|
switch (request->id()) {
|
||||||
|
case PropertyId::StreamIdProperty: {
|
||||||
|
StreamId id;
|
||||||
|
id.set_id(cur_backend->get_stream_idx());
|
||||||
|
data->mutable_value()->PackFrom(id);
|
||||||
|
} break;
|
||||||
|
case PropertyId::FilenameProperty: {
|
||||||
|
StringProperty prop;
|
||||||
|
prop.set_value(std::filesystem::path(cur_backend->get_current_file().value_or("None")).filename().string());
|
||||||
|
data->mutable_value()->PackFrom(prop);
|
||||||
|
} break;
|
||||||
|
case PropertyId::BackendId: {
|
||||||
|
StringProperty prop;
|
||||||
|
prop.set_value(cur_backend->get_id());
|
||||||
|
data->mutable_value()->PackFrom(prop);
|
||||||
|
} break;
|
||||||
|
case PropertyId::BackendName: {
|
||||||
|
StringProperty prop;
|
||||||
|
prop.set_value(cur_backend->get_name());
|
||||||
|
data->mutable_value()->PackFrom(prop);
|
||||||
|
}
|
||||||
|
case PropertyId::FilePathProperty: {
|
||||||
|
StringProperty path;
|
||||||
|
path.set_value(cur_backend->get_current_file().value_or(""));
|
||||||
|
data->mutable_value()->PackFrom(path);
|
||||||
|
} break;
|
||||||
|
case PropertyId::SpecProperty: {
|
||||||
|
AudioSpec spec;
|
||||||
|
SDL_AudioSpec sdl_spec = cur_backend->get_spec();
|
||||||
|
audio_data_t sample_spec = sdl_to_sample_spec(sdl_spec.format);
|
||||||
|
DEBUG.writefln("Sample_spec.size: %d", sample_spec.size);
|
||||||
|
spec.set_bits(sample_spec.size * 8);
|
||||||
|
spec.set_channel_count(sdl_spec.channels);
|
||||||
|
spec.set_endian(sample_spec.endian ? EndianID::BIG : EndianID::LITTLE);
|
||||||
|
spec.set_format_type(sample_spec.is_float ? FormatType::FLOAT : sample_spec.is_signed ? FormatType::SIGNED : FormatType::UNSIGNED);
|
||||||
|
spec.set_sample_rate(sdl_spec.freq);
|
||||||
|
data->mutable_value()->PackFrom(spec);
|
||||||
|
} break;
|
||||||
|
case PropertyId::StreamsProperty: {
|
||||||
|
auto full_streams = cur_backend->get_streams();
|
||||||
|
if (request->has_idx()) {
|
||||||
|
size_t idx = (size_t)request->idx();
|
||||||
|
Stream stream;
|
||||||
|
if (idx >= full_streams.size()) {
|
||||||
|
ErrorResponse *err = new ErrorResponse();
|
||||||
|
err->set_id("invalid_index");
|
||||||
|
err->set_desc("An attempt to access an array element outside the boundaries of that array was detected.");
|
||||||
|
err->set_fatal(false);
|
||||||
|
err_maybe = err;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto backend_stream = full_streams[idx];
|
||||||
|
stream.set_id(backend_stream.id);
|
||||||
|
stream.set_len(backend_stream.length);
|
||||||
|
stream.set_title(backend_stream.name);
|
||||||
|
data->mutable_value()->PackFrom(stream);
|
||||||
|
} else {
|
||||||
|
StreamList out_streams;
|
||||||
|
for (auto backend_stream : full_streams) {
|
||||||
|
Stream stream = *out_streams.add_streams();
|
||||||
|
stream.set_id(backend_stream.id);
|
||||||
|
stream.set_len(backend_stream.length);
|
||||||
|
stream.set_title(backend_stream.name);
|
||||||
|
}
|
||||||
|
data->mutable_value()->PackFrom(out_streams);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case PropertyId::TitleProperty: {
|
||||||
|
StringProperty title;
|
||||||
|
title.set_value(cur_backend->get_title().value_or(""));
|
||||||
|
data->mutable_value()->PackFrom(title);
|
||||||
|
} break;
|
||||||
|
case PropertyId::PositionProperty: {
|
||||||
|
Position pos;
|
||||||
|
pos.set_pos(cur_backend->get_position());
|
||||||
|
data->mutable_value()->PackFrom(pos);
|
||||||
|
} break;
|
||||||
|
case PropertyId::BackendSpecific: {
|
||||||
|
if (!request->has_path()) {
|
||||||
|
ErrorResponse *err = new ErrorResponse();
|
||||||
|
err->set_id("param_missing");
|
||||||
|
err->set_fatal(false);
|
||||||
|
err->set_desc("Backend specific parameters require a path.");
|
||||||
|
err_maybe = err;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto output_maybe = cur_backend->get(request->path());
|
||||||
|
if (output_maybe.has_value()) {
|
||||||
|
data->mutable_value()->PackFrom(output_maybe.value());
|
||||||
|
} else {
|
||||||
|
ErrorResponse *err = new ErrorResponse;
|
||||||
|
err->set_id("not_found");
|
||||||
|
err->set_fatal(false);
|
||||||
|
err->set_desc("The backend reported that the property was not found.");
|
||||||
|
err_maybe = err;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
default: {
|
||||||
|
ErrorResponse *err = new ErrorResponse();
|
||||||
|
err->set_id("invalid_property");
|
||||||
|
err->set_fatal(false);
|
||||||
|
err->set_desc("The property requested was invalid or write-only");
|
||||||
|
err_maybe = err;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
if (err_maybe != nullptr) {
|
||||||
|
delete data;
|
||||||
|
response->set_allocated_err(err_maybe);
|
||||||
|
} else {
|
||||||
|
response->set_allocated_output(data);
|
||||||
|
}
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
grpc::Status HostProcessImpl::WriteLog(grpc::ServerContext *ctx, const LogMessage *msg, SimpleAckResponse *response) {
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
grpc::Status HostProcessImpl::SetAddress(grpc::ServerContext *ctx, const StringProperty *value, MaybeError *response) {
|
||||||
|
process->host_channel.value().construct_client(value->value());
|
||||||
|
process->done = true;
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
grpc::Status PlaybackProcessServiceImpl::Set(grpc::ServerContext *ctx, const SetProperty *request, MaybeError *response) {
|
||||||
|
ErrorResponse *err_maybe = nullptr;
|
||||||
|
switch (request->id()) {
|
||||||
|
case PropertyId::PositionProperty: {
|
||||||
|
cur_backend->seek(resolve_any<Position>(request->value())->pos());
|
||||||
|
} break;
|
||||||
|
case PropertyId::StreamIdProperty: {
|
||||||
|
cur_backend->switch_stream(resolve_any<StreamId>(request->value())->id());
|
||||||
|
} break;
|
||||||
|
case PropertyId::BackendSpecific: {
|
||||||
|
if (!request->has_path()) {
|
||||||
|
ErrorResponse *err = new ErrorResponse();
|
||||||
|
err->set_id("param_missing");
|
||||||
|
err->set_fatal(false);
|
||||||
|
err->set_desc("Backend specific parameters require a path.");
|
||||||
|
err_maybe = err;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!cur_backend->set(request->path(), request->value())) {
|
||||||
|
ErrorResponse *err = new ErrorResponse();
|
||||||
|
err->set_id("not_found");
|
||||||
|
err->set_fatal(false);
|
||||||
|
err->set_desc("The backend reported that the property being set was invalid for it.");
|
||||||
|
err_maybe = err;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
default: {
|
||||||
|
ErrorResponse *err = new ErrorResponse();
|
||||||
|
err->set_id("invalid_property");
|
||||||
|
err->set_fatal(false);
|
||||||
|
err->set_desc("The property requested was invalid or read-only");
|
||||||
|
err_maybe = err;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
if (err_maybe != nullptr) {
|
||||||
|
response->set_allocated_err(err_maybe);
|
||||||
|
}
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
grpc::Status PlaybackProcessServiceImpl::Reset(grpc::ServerContext *ctx, const ResetProperty *request, ResetResponse *response) {
|
||||||
|
auto path = request->path();
|
||||||
|
std::optional<uint64_t> idx = {};
|
||||||
|
if (request->has_idx()) {
|
||||||
|
idx = request->idx();
|
||||||
|
}
|
||||||
|
auto output = cur_backend->reset(path);
|
||||||
|
if (output.has_value()) {
|
||||||
|
PropertyDataOrError *real_output = new PropertyDataOrError();
|
||||||
|
PropertyData *data = new PropertyData();
|
||||||
|
data->mutable_value()->PackFrom(output.value());
|
||||||
|
data->set_id(PropertyId::BackendSpecific);
|
||||||
|
data->clear_idx();
|
||||||
|
real_output->set_allocated_output(data);
|
||||||
|
response->set_allocated_value(real_output);
|
||||||
|
}
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
grpc::Status PlaybackProcessServiceImpl::Quit(grpc::ServerContext *ctx, const QuitCmd *request, MaybeError *response) {
|
||||||
|
process->done = true;
|
||||||
|
process->playback_process_channel.value().get_server()->get_server()->Shutdown();
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
grpc::Status PlaybackProcessServiceImpl::Init(grpc::ServerContext *ctx, const InitCommand *cmd, MaybeError *response) {
|
||||||
|
cur_backend = nullptr;
|
||||||
|
auto filename = cmd->filename();
|
||||||
|
auto idx = cmd->idx();
|
||||||
|
for (auto &backend : PlaybackBackendHelper()) {
|
||||||
|
try {
|
||||||
|
backend.second->init(filename.c_str(), idx);
|
||||||
|
} catch (std::exception e) {
|
||||||
|
backend.second->cleanup();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur_backend = backend.second;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (cur_backend == nullptr) {
|
||||||
|
ErrorResponse *maybe_error = new ErrorResponse();
|
||||||
|
maybe_error->set_desc("Couldn't find a backend.");
|
||||||
|
maybe_error->set_id("no_backend_for_file");
|
||||||
|
maybe_error->set_fatal(true);
|
||||||
|
response->set_allocated_err(maybe_error);
|
||||||
|
process->done = true;
|
||||||
|
process->playback_process_channel.value().get_server()->get_server()->Shutdown();
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
return grpc::Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_logging_subprocess(PlaybackProcess *proc) {
|
||||||
|
Looper::Log::init_logging();
|
||||||
|
}
|
||||||
|
PlaybackStream deserialize_stream(Stream serialized) {
|
||||||
|
PlaybackStream stream;
|
||||||
|
stream.id = serialized.id();
|
||||||
|
stream.length = serialized.len();
|
||||||
|
stream.name = serialized.title();
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
Stream serialize_stream(PlaybackStream stream, std::optional<Stream*> stream_ptr = {}) {
|
||||||
|
Stream serialized;
|
||||||
|
serialized.set_id(stream.id);
|
||||||
|
serialized.set_len(stream.length);
|
||||||
|
serialized.set_title(stream.name);
|
||||||
|
if (stream_ptr.has_value()) {
|
||||||
|
*(stream_ptr.value()) = serialized;
|
||||||
|
}
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
std::vector<PlaybackStream> deserialize_stream_list(StreamList list) {
|
||||||
|
int len = list.streams_size();
|
||||||
|
std::vector<PlaybackStream> output;
|
||||||
|
output.reserve(len);
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
Stream stream = list.streams(i);
|
||||||
|
output.push_back(deserialize_stream(stream));
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
StreamList serialize_stream_list(std::vector<PlaybackStream> streams) {
|
||||||
|
StreamList list;
|
||||||
|
for (auto input : streams) {
|
||||||
|
Stream *stream = list.add_streams();
|
||||||
|
serialize_stream(input, stream);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
PlaybackProcess::PlaybackProcess(std::vector<std::string> args) {
|
||||||
|
done = false;
|
||||||
|
is_playback_process = true;
|
||||||
|
init_playback_backends();
|
||||||
|
init_audio_data();
|
||||||
|
std::string address = args[0];
|
||||||
|
auto mk_service = [this]() -> grpc::Service* {
|
||||||
|
PlaybackProcessServiceImpl *output = new PlaybackProcessServiceImpl();
|
||||||
|
output->process = this;
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
playback_process_channel = IPCChannel<PlaybackProcessService, HostProcess>(mk_service);
|
||||||
|
playback_process_channel.value().construct_client(address);
|
||||||
|
{
|
||||||
|
StringProperty property;
|
||||||
|
property.set_value(playback_process_channel.value().get_server()->get_address());
|
||||||
|
ClientContext ctx;
|
||||||
|
MaybeError response;
|
||||||
|
playback_process_channel.value().get_stub()->SetAddress(&ctx, property, &response);
|
||||||
|
}
|
||||||
|
Looper::Log::init_logging();
|
||||||
|
}
|
||||||
|
PlaybackProcess::PlaybackProcess(std::string filename, int idx) {
|
||||||
|
done = false;
|
||||||
|
|
||||||
|
auto mk_service = [this]() -> grpc::Service* {
|
||||||
|
HostProcessImpl *output = new HostProcessImpl();
|
||||||
|
output->process = this;
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
this->done = false;
|
||||||
|
host_channel = IPCChannel<HostProcess, PlaybackProcessService>(mk_service);
|
||||||
|
std::string address = host_channel.value().get_server()->get_address();
|
||||||
|
std::vector<std::string> new_args;
|
||||||
|
new_args.push_back(executable_path);
|
||||||
|
new_args.push_back("--process-type");
|
||||||
|
new_args.push_back("playback");
|
||||||
|
new_args.push_back(address);
|
||||||
|
pid = launch(new_args);
|
||||||
|
while (!done) {
|
||||||
|
std::this_thread::yield();
|
||||||
|
}
|
||||||
|
ClientContext ctx;
|
||||||
|
InitCommand cmd;
|
||||||
|
cmd.set_filename(filename);
|
||||||
|
cmd.set_idx(idx);
|
||||||
|
MaybeError output;
|
||||||
|
get_stub()->Init(&ctx, cmd, &output);
|
||||||
|
if (output.has_err()) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool PlaybackProcess::process_running() {
|
||||||
|
if (is_playback_process) return true;
|
||||||
|
return kill(pid, 0) == 0;
|
||||||
|
}
|
||||||
|
void PlaybackProcess::run_playback_process() {
|
||||||
|
playback_process_channel.value().get_server()->get_server()->Wait();
|
||||||
|
}
|
||||||
|
int looper_run_playback_process(std::vector<std::string> args) {
|
||||||
|
auto proc = PlaybackProcess(args);
|
||||||
|
proc.run_playback_process();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
PropertyData PlaybackProcess::get_property(PropertyId property, std::optional<uint64_t> idx) {
|
||||||
|
GetProperty get_property;
|
||||||
|
get_property.set_id(property);
|
||||||
|
if (idx.has_value()) {
|
||||||
|
get_property.set_idx(idx.value());
|
||||||
|
} else {
|
||||||
|
get_property.clear_idx();
|
||||||
|
}
|
||||||
|
ClientContext context;
|
||||||
|
PropertyDataOrError output;
|
||||||
|
get_stub()->Get(&context, get_property, &output);
|
||||||
|
if (output.has_err()) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
return output.output();
|
||||||
|
}
|
||||||
|
void PlaybackProcess::set_property(PropertyId id, PropertyData data, std::optional<uint64_t> idx) {
|
||||||
|
SetProperty set_property;
|
||||||
|
set_property.set_id(id);
|
||||||
|
set_property.mutable_value()->PackFrom(data);
|
||||||
|
if (idx.has_value()) {
|
||||||
|
set_property.set_idx(idx.value());
|
||||||
|
} else {
|
||||||
|
set_property.clear_idx();
|
||||||
|
}
|
||||||
|
ClientContext context;
|
||||||
|
MaybeError output;
|
||||||
|
get_stub()->Set(&context, set_property, &output);
|
||||||
|
}
|
||||||
|
std::string PlaybackProcess::get_version_code() {
|
||||||
|
return std::string(TAG) +
|
||||||
|
#ifdef DEBUG_MODE
|
||||||
|
std::string("-debugmode") +
|
||||||
|
#endif
|
||||||
|
std::string("-at") + std::string(__TIME__);
|
||||||
|
}
|
||||||
|
double PlaybackProcess::get_position() {
|
||||||
|
auto *tmp = get_property_value<Position>(PropertyId::PositionProperty);
|
||||||
|
double output = tmp->pos();
|
||||||
|
delete tmp;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
void PlaybackProcess::set_position(double value) {
|
||||||
|
Position *pos = new Position();
|
||||||
|
pos->set_pos(value);
|
||||||
|
set_property_value<Position>(PropertyId::PositionProperty, pos);
|
||||||
|
delete pos;
|
||||||
|
}
|
||||||
|
size_t PlaybackProcess::get_stream_idx() {
|
||||||
|
StreamId *id = get_property_value<StreamId>(PropertyId::StreamIdProperty);
|
||||||
|
size_t output = id->id();
|
||||||
|
delete id;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
void PlaybackProcess::set_stream_idx(size_t idx) {
|
||||||
|
StreamId *id = new StreamId();
|
||||||
|
id->set_id(idx);
|
||||||
|
set_property_value<StreamId>(PropertyId::StreamIdProperty, id);
|
||||||
|
delete id;
|
||||||
|
}
|
||||||
|
std::string PlaybackProcess::get_title() {
|
||||||
|
return get_property_string(PropertyId::TitleProperty);
|
||||||
|
}
|
||||||
|
std::string PlaybackProcess::get_file_path() {
|
||||||
|
return get_property_string(PropertyId::FilePathProperty);
|
||||||
|
}
|
||||||
|
std::string PlaybackProcess::get_file_name() {
|
||||||
|
return get_property_string(PropertyId::FilenameProperty);
|
||||||
|
}
|
||||||
|
PlaybackStream PlaybackProcess::get_playback_stream(size_t idx) {
|
||||||
|
auto *stream = get_property_value<Stream>(PropertyId::StreamsProperty, idx);
|
||||||
|
PlaybackStream output = deserialize_stream(*stream);
|
||||||
|
delete stream;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
std::string PlaybackProcess::get_backend_id() {
|
||||||
|
return get_property_string(PropertyId::BackendId);
|
||||||
|
}
|
||||||
|
std::string PlaybackProcess::get_backend_name() {
|
||||||
|
return get_property_string(PropertyId::BackendName);
|
||||||
|
}
|
||||||
|
std::vector<PlaybackStream> PlaybackProcess::get_playback_streams() {
|
||||||
|
auto *list = get_property_value<StreamList>(PropertyId::StreamsProperty);
|
||||||
|
auto output = deserialize_stream_list(*list);
|
||||||
|
delete list;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
size_t PlaybackProcess::render(void *buf, size_t maxlen) {
|
||||||
|
RenderCommand rend_cmd = RenderCommand();
|
||||||
|
rend_cmd.set_len(maxlen);
|
||||||
|
ClientContext ctx;
|
||||||
|
RenderResponseOrError response;
|
||||||
|
get_stub()->Render(&ctx, rend_cmd, &response);
|
||||||
|
if (response.has_err()) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
std::string data = response.output().data();
|
||||||
|
memcpy(buf, data.data(), data.length());
|
||||||
|
return data.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AudioSpec *PlaybackProcess::get_audio_spec() {
|
||||||
|
return get_property_value<AudioSpec>(PropertyId::SpecProperty);
|
||||||
|
}
|
||||||
|
PlaybackProcess::~PlaybackProcess() {
|
||||||
|
ClientContext ctx;
|
||||||
|
QuitCmd quit_cmd;
|
||||||
|
MaybeError output;
|
||||||
|
get_stub()->Quit(&ctx, quit_cmd, &output);
|
||||||
|
done = true;
|
||||||
|
}
|
114
playback_process.hpp
Normal file
114
playback_process.hpp
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
#pragma once
|
||||||
|
#include <any>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "thirdparty/CRC.hpp"
|
||||||
|
#include "playback_backend.hpp"
|
||||||
|
#include <grpc/grpc.h>
|
||||||
|
#include <grpcpp/security/server_credentials.h>
|
||||||
|
#include <grpcpp/server.h>
|
||||||
|
#include <grpcpp/server_builder.h>
|
||||||
|
#include <grpcpp/server_context.h>
|
||||||
|
#include "rpc.hpp"
|
||||||
|
#include <google/protobuf/stubs/status.h>
|
||||||
|
#include "ipc/common.pb.h"
|
||||||
|
#include "ipc/internal.pb.h"
|
||||||
|
#include "ipc/internal.grpc.pb.h"
|
||||||
|
#include <log.hpp>
|
||||||
|
class PlaybackProcess;
|
||||||
|
class PlaybackProcessServiceImpl : public PlaybackProcessService::Service {
|
||||||
|
PlaybackBackend *cur_backend;
|
||||||
|
public:
|
||||||
|
PlaybackProcess *process;
|
||||||
|
grpc::Status Init(grpc::ServerContext *ctx, const InitCommand *cmd, MaybeError *response) override;
|
||||||
|
grpc::Status Render(grpc::ServerContext *context, const RenderCommand *cmd, RenderResponseOrError *response) override;
|
||||||
|
grpc::Status Get(grpc::ServerContext *ctx, const GetProperty *request, PropertyDataOrError *response) override;
|
||||||
|
grpc::Status Set(grpc::ServerContext *ctx, const SetProperty *request, MaybeError *err) override;
|
||||||
|
grpc::Status Reset(grpc::ServerContext *ctx, const ResetProperty *request, ResetResponse *response) override;
|
||||||
|
grpc::Status Quit(grpc::ServerContext *ctx, const QuitCmd *request, MaybeError *response) override;
|
||||||
|
};
|
||||||
|
class HostProcessImpl : public HostProcess::Service {
|
||||||
|
public:
|
||||||
|
PlaybackProcess *process;
|
||||||
|
grpc::Status WriteLog(grpc::ServerContext *ctx, const LogMessage *msg, SimpleAckResponse *response) override;
|
||||||
|
grpc::Status SetAddress(grpc::ServerContext *ctx, const StringProperty *data, MaybeError *response) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
inline T *resolve_any(google::protobuf::Any value) {
|
||||||
|
T *output = new T();
|
||||||
|
value.UnpackTo(output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
//#define DEBUG_PRINT_IPC
|
||||||
|
void print_ipc_message(const google::protobuf::Message &msg, size_t level = 0);
|
||||||
|
class PlaybackProcess {
|
||||||
|
friend class HostProcessImpl;
|
||||||
|
friend class PlaybackProcessServiceImpl;
|
||||||
|
void threadfunc();
|
||||||
|
int pid;
|
||||||
|
bool is_playback_process = false;
|
||||||
|
bool done;
|
||||||
|
std::optional<IPCChannel<HostProcess, PlaybackProcessService>> host_channel;
|
||||||
|
std::optional<IPCChannel<PlaybackProcessService, HostProcess>> playback_process_channel;
|
||||||
|
inline std::unique_ptr<PlaybackProcessService::Stub> get_stub() {
|
||||||
|
return host_channel.value().get_stub();
|
||||||
|
}
|
||||||
|
std::string get_version_code();
|
||||||
|
PropertyData get_property(PropertyId id, std::optional<uint64_t> idx = {});
|
||||||
|
inline google::protobuf::Any get_property_value_any(PropertyId id, std::optional<uint64_t> idx = {}) {
|
||||||
|
PropertyData data = get_property(id, idx);
|
||||||
|
return data.value();
|
||||||
|
}
|
||||||
|
template<class T>
|
||||||
|
inline T *get_property_value(PropertyId id, std::optional<uint64_t> idx = {}) {
|
||||||
|
if constexpr (std::is_same_v<T, std::string>) {
|
||||||
|
auto *property = get_property_value<StringProperty>(id, idx);
|
||||||
|
std::string *tmp = new std::string(property->value());
|
||||||
|
delete property;
|
||||||
|
return tmp;
|
||||||
|
} else {
|
||||||
|
return resolve_any<T>(get_property_value_any(id, idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline std::string get_property_string(PropertyId id, std::optional<uint64_t> idx = {}) {
|
||||||
|
auto *property = get_property_value<StringProperty>(id, idx);
|
||||||
|
std::string output = std::string(property->value());
|
||||||
|
delete property;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
void set_property(PropertyId id, PropertyData data, std::optional<uint64_t> idx = {});
|
||||||
|
template<class T>
|
||||||
|
inline void set_property_value(PropertyId id, T *value, std::optional<uint64_t> idx = {}) {
|
||||||
|
PropertyData data;
|
||||||
|
data.mutable_value()->PackFrom(*value);
|
||||||
|
set_property(id, data, idx);
|
||||||
|
}
|
||||||
|
friend int looper_run_playback_process(std::vector<std::string> args);
|
||||||
|
PlaybackProcess(std::vector<std::string> args);
|
||||||
|
void run_playback_process();
|
||||||
|
public:
|
||||||
|
bool process_running();
|
||||||
|
double get_position();
|
||||||
|
void set_position(double pos);
|
||||||
|
size_t get_stream_idx();
|
||||||
|
void set_stream_idx(size_t idx);
|
||||||
|
std::string get_title();
|
||||||
|
std::string get_file_path();
|
||||||
|
std::string get_file_name();
|
||||||
|
std::string get_backend_id();
|
||||||
|
std::string get_backend_name();
|
||||||
|
PlaybackStream get_playback_stream(size_t idx);
|
||||||
|
std::vector<PlaybackStream> get_playback_streams();
|
||||||
|
AudioSpec *get_audio_spec();
|
||||||
|
size_t render(void *buf, size_t maxlen);
|
||||||
|
|
||||||
|
std::optional<google::protobuf::Any> get_property(std::string path);
|
||||||
|
bool set_property(std::string path, google::protobuf::Any value);
|
||||||
|
PlaybackProcess(std::string filename, int idx = 0);
|
||||||
|
~PlaybackProcess();
|
||||||
|
};
|
222
rpc.hpp
Normal file
222
rpc.hpp
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
#pragma once
|
||||||
|
#include <algorithm>
|
||||||
|
#include <any>
|
||||||
|
#include <grpcpp/security/server_credentials.h>
|
||||||
|
#include <grpcpp/support/byte_buffer.h>
|
||||||
|
#include <jsoncpp/json/json.h>
|
||||||
|
#include <google/protobuf/descriptor.h>
|
||||||
|
#include <google/protobuf/generated_message_reflection.h>
|
||||||
|
#include <google/protobuf/message.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include "thirdparty/CRC.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string.h>
|
||||||
|
#include <map>
|
||||||
|
#include <grpc++/grpc++.h>
|
||||||
|
#include <grpc++/server.h>
|
||||||
|
#include <variant>
|
||||||
|
#include <filesystem>
|
||||||
|
#define _FORMAT_CPP_TYPE(fdesc, value, prefix, int32, int64, uint32, uint64, float, double, bool, string, enum, message, ...) \
|
||||||
|
switch (fdesc->cpp_type()) { \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: \
|
||||||
|
value = prefix##bool(__VA_ARGS__) ? "true" : "false"; \
|
||||||
|
break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: \
|
||||||
|
value = fmt::format("{0}", prefix##double(__VA_ARGS__)); \
|
||||||
|
break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: \
|
||||||
|
value = fmt::format("{0}", prefix##float(__VA_ARGS__)); \
|
||||||
|
break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_INT32: \
|
||||||
|
value = fmt::format("{0}", prefix##int32(__VA_ARGS__)); \
|
||||||
|
break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: \
|
||||||
|
value = fmt::format("{0}", prefix##uint32(__VA_ARGS__)); \
|
||||||
|
break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_INT64: \
|
||||||
|
value = fmt::format("{0}", prefix##int64(__VA_ARGS__)); \
|
||||||
|
break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: \
|
||||||
|
value = fmt::format("{0}", prefix##uint64(__VA_ARGS__)); \
|
||||||
|
break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_STRING: \
|
||||||
|
value = prefix##string(__VA_ARGS__); \
|
||||||
|
break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: {\
|
||||||
|
const auto *enum_desc = prefix##enum(__VA_ARGS__); \
|
||||||
|
value = fmt::format("{0} = {1}", enum_desc->full_name(), enum_desc->index()); \
|
||||||
|
} break; \
|
||||||
|
case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: \
|
||||||
|
value = "(Message)"; \
|
||||||
|
break; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define FORMAT_CPP_TYPE_LOWERCASE(fdesc, value, prefix, ...) _FORMAT_CPP_TYPE(fdesc, value, prefix, int32, int64, uint32, uint64, float, double, bool, string, enum, message __VA_OPT__(,) __VA_ARGS__)
|
||||||
|
|
||||||
|
#define FORMAT_CPP_TYPE_TITLECASE(fdesc, value, prefix, ...) _FORMAT_CPP_TYPE(fdesc, value, prefix, Int32, Int64, UInt32, UInt64, Float, Double, Bool, String, Enum, Message __VA_OPT__(,) __VA_ARGS__)
|
||||||
|
|
||||||
|
inline std::string generate_address() {
|
||||||
|
std::filesystem::path tmpdir = std::filesystem::temp_directory_path();
|
||||||
|
std::filesystem::path sockpath = tmpdir / "looper_playback.";
|
||||||
|
const char *chars = "abcdefghijklmnopqrtsuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890._";
|
||||||
|
for (size_t i = 0; i < 64; i++) {
|
||||||
|
char chr = chars[rand() % strlen(chars)];
|
||||||
|
sockpath += chr;
|
||||||
|
}
|
||||||
|
sockpath += ".sock";
|
||||||
|
return "unix:" + std::string(sockpath);
|
||||||
|
}
|
||||||
|
template<class C>
|
||||||
|
class IPCClient {
|
||||||
|
public:
|
||||||
|
using Stub = std::unique_ptr<typename C::Stub>;
|
||||||
|
std::shared_ptr<grpc::ChannelInterface> channel;
|
||||||
|
IPCClient(std::string address) {
|
||||||
|
channel = grpc::CreateChannel(address, grpc::InsecureChannelCredentials());
|
||||||
|
}
|
||||||
|
|
||||||
|
Stub client_stub() {
|
||||||
|
return C::NewStub(channel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
template<class S>
|
||||||
|
class IPCServer {
|
||||||
|
std::optional<std::shared_ptr<grpc::Server>> server;
|
||||||
|
std::string address;
|
||||||
|
using Service = S;
|
||||||
|
public:
|
||||||
|
using ServiceConstructor = std::function<grpc::Service*()>;
|
||||||
|
private:
|
||||||
|
void init_address(std::string address) {
|
||||||
|
this->address = address;
|
||||||
|
}
|
||||||
|
void init_address() {
|
||||||
|
init_address(generate_address());
|
||||||
|
}
|
||||||
|
void init(ServiceConstructor custom_service_constructor) {
|
||||||
|
this->address = address;
|
||||||
|
grpc::ServerBuilder builder;
|
||||||
|
grpc::Service *service = nullptr;
|
||||||
|
service = custom_service_constructor();
|
||||||
|
builder.AddListeningPort(address, grpc::InsecureServerCredentials());
|
||||||
|
builder.RegisterService(service);
|
||||||
|
server = std::shared_ptr<grpc::Server>(builder.BuildAndStart().release());
|
||||||
|
}
|
||||||
|
template<class SI>
|
||||||
|
void init() {
|
||||||
|
return init([]() {
|
||||||
|
return new SI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
inline std::shared_ptr<grpc::Server> get_server() {
|
||||||
|
return server.value();
|
||||||
|
}
|
||||||
|
std::string get_address() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
template<class SI>
|
||||||
|
IPCServer(std::string address) {
|
||||||
|
init_address(address);
|
||||||
|
init<SI>();
|
||||||
|
}
|
||||||
|
template<class SI>
|
||||||
|
IPCServer() {
|
||||||
|
init_address();
|
||||||
|
init<SI>();
|
||||||
|
}
|
||||||
|
IPCServer(ServiceConstructor custom_service_constructor) {
|
||||||
|
init_address();
|
||||||
|
init(custom_service_constructor);
|
||||||
|
}
|
||||||
|
IPCServer(std::string address, ServiceConstructor custom_service_constructor) {
|
||||||
|
init_address(address);
|
||||||
|
init(custom_service_constructor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
template<class S, class C>
|
||||||
|
class IPCChannel {
|
||||||
|
using Client = IPCClient<C>;
|
||||||
|
using Server = IPCServer<S>;
|
||||||
|
using ClientPtr = std::shared_ptr<Client>;
|
||||||
|
using ServerPtr = std::shared_ptr<Server>;
|
||||||
|
using ServiceConstructor = typename Server::ServiceConstructor;
|
||||||
|
std::optional<ClientPtr> client = {};
|
||||||
|
ServerPtr server;
|
||||||
|
void init_client(ClientPtr client) {
|
||||||
|
this->client = client;
|
||||||
|
}
|
||||||
|
void init_client(Client *client) {
|
||||||
|
init_client(ClientPtr(client));
|
||||||
|
}
|
||||||
|
void init_server(ServerPtr server) {
|
||||||
|
this->server = server;
|
||||||
|
}
|
||||||
|
void init_server(Server *server) {
|
||||||
|
init_server(ServerPtr(server));
|
||||||
|
}
|
||||||
|
void init_client(std::string client_address) {
|
||||||
|
init_client(new Client(client_address));
|
||||||
|
}
|
||||||
|
void init_server(std::string server_address, typename Server::ServiceConstructor constructor) {
|
||||||
|
init_server(new Server(server_address, constructor));
|
||||||
|
}
|
||||||
|
void init_server(typename Server::ServiceConstructor constructor) {
|
||||||
|
init_server(new Server(constructor));
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
using Stub = typename Client::Stub;
|
||||||
|
bool has_client() const {
|
||||||
|
return client.has_value();
|
||||||
|
}
|
||||||
|
Stub get_stub() {
|
||||||
|
return get_client()->client_stub();
|
||||||
|
}
|
||||||
|
ServerPtr get_server() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
const ServerPtr get_server() const {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
ClientPtr get_client() {
|
||||||
|
return client.value();
|
||||||
|
}
|
||||||
|
const ClientPtr get_client() const {
|
||||||
|
return client.value();
|
||||||
|
}
|
||||||
|
void set_client(ClientPtr client) {
|
||||||
|
if (this->client.has_value()) return;
|
||||||
|
init_client(client);
|
||||||
|
}
|
||||||
|
void construct_client(std::string client_address) {
|
||||||
|
if (this->client.has_value()) return;
|
||||||
|
Client *client = new Client(client_address);
|
||||||
|
init_client(client);
|
||||||
|
}
|
||||||
|
/// @brief Constructs an IP channel with a newly-created server with a generated address.
|
||||||
|
IPCChannel(ServiceConstructor custom_service_constructor) {
|
||||||
|
init_server(custom_service_constructor);
|
||||||
|
}
|
||||||
|
/// @brief Constructs an IPC channel with a specific address for the server, creating a new server in the process
|
||||||
|
/// @param address The address to use
|
||||||
|
IPCChannel(std::string address, ServiceConstructor custom_service_constructor = {}) {
|
||||||
|
init_server(address, custom_service_constructor);
|
||||||
|
}
|
||||||
|
/// @brief Constructs an IPC channel with only an already-existing server.
|
||||||
|
/// @param server_init The existing server to use
|
||||||
|
IPCChannel(ServerPtr server_init) {
|
||||||
|
init_server(server_init);
|
||||||
|
}
|
||||||
|
};
|
4
start-memory-limited.sh
Executable file
4
start-memory-limited.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
MEMORY_MAX="$1"
|
||||||
|
shift
|
||||||
|
systemd-run --scope -p MemoryMax="$MEMORY_MAX" -p MemorySwapMax=0 --user "$@"
|
1
subprojects/fmt
Submodule
1
subprojects/fmt
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 0379bf3a5d52d8542aec1874677c9df5ff9ba5f9
|
1
subprojects/grpc
Submodule
1
subprojects/grpc
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit e821cdc231bda9ee93139a6daab6311dd8953832
|
1
subprojects/protobuf
Submodule
1
subprojects/protobuf
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 439c42c735ae1efed57ab7771986f2a3c0b99319
|
|
@ -1 +1 @@
|
||||||
Subproject commit 416ac26510b4e243760d9c3a1fdb81a885762a8b
|
Subproject commit 4b2dc01ccdcd3eeccb7b2ca0d7a32692dfdec947
|
2114
thirdparty/CRC.hpp
vendored
Normal file
2114
thirdparty/CRC.hpp
vendored
Normal file
File diff suppressed because it is too large
Load diff
25
util.cpp
25
util.cpp
|
@ -2,6 +2,9 @@
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stddef.h>
|
||||||
std::string PadZeros(std::string input, size_t required_length) {
|
std::string PadZeros(std::string input, size_t required_length) {
|
||||||
return std::string(required_length - std::min(required_length, input.length()), '0') + input;
|
return std::string(required_length - std::min(required_length, input.length()), '0') + input;
|
||||||
}
|
}
|
||||||
|
@ -71,3 +74,25 @@ std::string get_prefs_path() {
|
||||||
#endif
|
#endif
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
int launch(std::vector<std::string> args) {
|
||||||
|
char **argv;
|
||||||
|
bool err = false;
|
||||||
|
argv = (char**)calloc(args.size() + 1, sizeof(char*));
|
||||||
|
for (size_t i = 0; i < args.size(); i++) {
|
||||||
|
argv[i] = strdup(args[i].c_str());
|
||||||
|
}
|
||||||
|
int pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
err = true;
|
||||||
|
} else if (pid == 0) {
|
||||||
|
execvp(argv[0], argv);
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < args.size(); i++) {
|
||||||
|
free(argv[i]);
|
||||||
|
}
|
||||||
|
free(argv);
|
||||||
|
if (err) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
8
util.hpp
8
util.hpp
|
@ -4,6 +4,7 @@
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
std::string PadZeros(std::string input, size_t required_length);
|
std::string PadZeros(std::string input, size_t required_length);
|
||||||
uint8_t TimeToComponentCount(double time_code);
|
uint8_t TimeToComponentCount(double time_code);
|
||||||
|
@ -77,5 +78,12 @@ inline size_t combine_hashes(std::initializer_list<size_t> hashes) {
|
||||||
}
|
}
|
||||||
return std::hash<std::string>()(values);
|
return std::hash<std::string>()(values);
|
||||||
}
|
}
|
||||||
|
int launch(std::vector<std::string> args);
|
||||||
|
#ifdef MIN
|
||||||
|
#undef MIN
|
||||||
|
#endif
|
||||||
#define MIN(x, y) ((x < y) ? x : y)
|
#define MIN(x, y) ((x < y) ? x : y)
|
||||||
|
#ifdef MAX
|
||||||
|
#undef MAX
|
||||||
|
#endif
|
||||||
#define MAX(x, y) ((x > y) ? x : y)
|
#define MAX(x, y) ((x > y) ? x : y)
|
Loading…
Reference in a new issue