Update vgmstream

This commit is contained in:
Zachary Hall 2024-08-08 13:12:37 -07:00
parent 204adf1f3e
commit 73686be925
76 changed files with 14894 additions and 380 deletions

2
.clangd Normal file
View file

@ -0,0 +1,2 @@
CompileFlags:
CompilationDatabase: "./build/compile_commands.json"

1
.gitignore vendored
View file

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

@ -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
View 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"]
}
]

View file

@ -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()
@ -153,7 +155,7 @@ else()
set(TAG "unknown") set(TAG "unknown")
endif() endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DEBUG ON) set(DEBUG ON)
endif() endif()
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
@ -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
View 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.

View file

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

View 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})

View file

@ -0,0 +1,4 @@
{
"class_name": "SDLMixerXBackend",
"include_path": "sdl_mixer_x.hpp"
}

View 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;
}

View 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 { }
};

View 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})

View file

@ -0,0 +1,4 @@
{
"class_name": "VgmStreamBackend",
"include_path": "vgmstream.hpp"
}

View 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;
}

View 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 { }
};

View 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})

View file

@ -0,0 +1,4 @@
{
"class_name": "ZsmBackend",
"include_path": "zsm_backend.hpp"
}

View 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);
}

View 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);

View 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

View 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);
}
}

View 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);

View 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;
}
}

View 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);

View 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.

View 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

View 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);
}
}

View 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 &regs() { 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 &regs() { 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

View 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 &regs() 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 &regs() 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 &regs() { 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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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

View 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();
}
}
}

View 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

View 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();
}
}

View 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 &regs() { 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

View 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;
}
}

View 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

View 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;
}

View 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 { }
};

View file

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

View file

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

View 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" ]

View 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
View 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 ..

View 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
View 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
View 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})

View file

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

View file

@ -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:]:
ui_backend_metafiles += [ui_backend_dir + "/" + backend + "/ui.json"] 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"]
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
View 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
View 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
View 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
View 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
View file

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

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

View file

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

View file

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

View file

@ -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; SDL_FreeAudioStream(sdl_stream);
stream.length = 0;
stream.name = "";
if (!sf) {
streams.push_back(stream);
continue;
} }
sf->stream_index = i; sdl_stream = SDL_NewAudioStream(backend_spec.format, backend_spec.channels, backend_spec.freq, spec.format, spec.channels, spec.freq);
auto *tmp = init_vgmstream_from_STREAMFILE(sf); if (sdl_stream == nullptr) {
if (!tmp) { ERROR.writefln("SDL_NewAudioStream: %s", SDL_GetError());
streams.push_back(stream); 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");
continue; set_error("Failed to create SDL audio stream");
set_signal(PlaybackSignalErrorOccurred);
} }
reset_vgmstream(tmp); size_t required_size = sample_fmt.size * backend_spec.samples;
st->setChannels(spec.channels);
stream.length = (double)tmp->num_samples / (double)tmp->sample_rate; st->setRate(spec.freq);
char *buf = (char*)malloc(STREAM_NAME_SIZE + 1); st->flush();
memset(buf, 0, STREAM_NAME_SIZE + 1); backend_spec.size = required_size;
strncpy(buf, tmp->stream_name, STREAM_NAME_SIZE); bufsize = backend_spec.size;
if (buf[0] == '\0') { buf = (Uint8*)malloc(bufsize);
free(buf); if (buf == nullptr) {
buf = strdup("Unknown"); ERROR.writeln("Failed to allocate memory for playback!");
set_error("Failed to allocate memory for playback!");
set_signal(PlaybackSignalErrorOccurred);
bufsize = 0;
} }
if (i == 0) { delete backend_spec_proxy;
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 { } else {
current_title = std::string(title_tag); 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_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); SDL_UnlockAudioDevice(device);
load_finished.store(true);
return output;
} }
void PlaybackInstance::UnloadMix(Mix_Music *music) { void PlaybackInstance::Unload() {
stream_list_mutex.lock(); if (process == nullptr) return;
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); SDL_LockAudioDevice(device);
close_vgmstream(stream); delete process;
process = nullptr;
SDL_FreeAudioStream(sdl_stream); SDL_FreeAudioStream(sdl_stream);
this->stream = nullptr;
sdl_stream = nullptr; 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 (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);
@ -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();

View file

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

@ -0,0 +1 @@
Subproject commit 0379bf3a5d52d8542aec1874677c9df5ff9ba5f9

1
subprojects/grpc Submodule

@ -0,0 +1 @@
Subproject commit e821cdc231bda9ee93139a6daab6311dd8953832

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

File diff suppressed because it is too large Load diff

View file

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

View file

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