Merge branch 'master' into ymf289b

This commit is contained in:
tildearrow 2023-05-11 16:59:38 -05:00
commit 25eb720631
230 changed files with 69242 additions and 87659 deletions

View File

@ -24,7 +24,7 @@ jobs:
#- { name: 'Windows MinGW x86_64', os: ubuntu-20.04, compiler: mingw, arch: x86_64 }
- { name: 'macOS x86_64', os: macos-latest, arch: x86_64 }
- { name: 'macOS ARM', os: macos-latest, arch: arm64 }
- { name: 'Linux x86_64', os: ubuntu-18.04, arch: x86_64 }
- { name: 'Linux x86_64', os: ubuntu-20.04, arch: x86_64 }
#- { name: 'Linux ARM', os: ubuntu-18.04, arch: armhf }
fail-fast: false

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ android/app/.cxx/
CMakeSettings.json
CMakePresets.json
extern/imgui_patched/examples/
src/asm/68k/amigatest/*.bin
src/asm/68k/amigatest/player

View File

@ -459,6 +459,8 @@ src/engine/platform/sound/ga20/iremga20.cpp
src/engine/platform/sound/sm8521.c
src/engine/platform/sound/d65modified.c
src/engine/platform/oplAInterface.cpp
src/engine/platform/ym2608Interface.cpp
src/engine/platform/ym2610Interface.cpp
@ -467,10 +469,12 @@ src/engine/blip_buf.c
src/engine/brrUtils.c
src/engine/safeReader.cpp
src/engine/safeWriter.cpp
src/engine/cmdStream.cpp
src/engine/config.cpp
src/engine/configEngine.cpp
src/engine/dispatchContainer.cpp
src/engine/engine.cpp
src/engine/export.cpp
src/engine/fileOps.cpp
src/engine/fileOpsIns.cpp
src/engine/filter.cpp
@ -486,6 +490,7 @@ src/engine/waveSynth.cpp
src/engine/vgmOps.cpp
src/engine/zsmOps.cpp
src/engine/zsm.cpp
src/engine/platform/abstract.cpp
src/engine/platform/genesis.cpp
src/engine/platform/genesisext.cpp
@ -528,6 +533,7 @@ src/engine/platform/t6w28.cpp
src/engine/platform/vb.cpp
src/engine/platform/vera.cpp
src/engine/platform/zxbeeper.cpp
src/engine/platform/zxbeeperquadtone.cpp
src/engine/platform/bubsyswsg.cpp
src/engine/platform/n163.cpp
src/engine/platform/pet.cpp
@ -544,8 +550,12 @@ src/engine/platform/snes.cpp
src/engine/platform/k007232.cpp
src/engine/platform/ga20.cpp
src/engine/platform/sm8521.cpp
src/engine/platform/pv1000.cpp
src/engine/platform/pcmdac.cpp
src/engine/platform/dummy.cpp
src/engine/export/abstract.cpp
src/engine/export/amigaValidation.cpp
)
if (USE_SNDFILE)
@ -615,6 +625,7 @@ src/gui/editing.cpp
src/gui/editControls.cpp
src/gui/effectList.cpp
src/gui/findReplace.cpp
src/gui/fmPreview.cpp
src/gui/gradient.cpp
src/gui/grooves.cpp
src/gui/insEdit.cpp
@ -780,6 +791,8 @@ endif()
if(ANDROID AND NOT TERMUX)
add_library(furnace SHARED ${USED_SOURCES})
elseif(WIN32)
add_executable(furnace WIN32 ${USED_SOURCES})
else()
add_executable(furnace ${USED_SOURCES})
endif()

View File

@ -47,6 +47,7 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a
- OKI MSM6258 and MSM6295
- Konami K007232
- Irem GA20
- Ensoniq ES5506
- wavetable chips:
- HuC6280 used in PC Engine
- Konami Bubble System WSG
@ -54,6 +55,7 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a
- Namco arcade chips (WSG/C15/C30)
- WonderSwan
- Seta/Allumer X1-010
- Sharp SM8521 used in Tiger Game.com
- NES (Ricoh 2A03/2A07), with additional expansion sound support:
- Konami VRC6
- Konami VRC7
@ -78,7 +80,7 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a
- over 200 ready to use presets from computers, game consoles and arcade boards...
- ...or create your own - up to 32 of them or a total of 128 channels!
- DefleMask compatibility
- loads .dmf modules from all versions (beta 1 to 1.1.5)
- loads .dmf modules from all versions (beta 1 to 1.1.7)
- saves .dmf modules - both modern and legacy
- Furnace doubles as a module downgrader
- loads/saves .dmp instruments and .dmw wavetables as well

16
TODO.md Normal file
View File

@ -0,0 +1,16 @@
# to-do for 0.6pre5
- tutorial
- ease-of-use improvements... ideas:
- preset compat flags
- setting to toggle the Choose a System screen on new project
- maybe reduced set of presets for the sake of simplicity
- a more preferable highlight/drag system
- some speed/intuitive workflow improvements that go a long way
- Had a hard time finding the docs on github and in Furnace's folder.
- make .pdf manual out of papers/doc/
- you're going too fast; please slow down
- break compatibility if it relieves complexity
- ins/wave/sample organization (folders and all)
- multi-key binds
- bug fixes

View File

@ -15,8 +15,8 @@ android {
}
minSdkVersion 21
targetSdkVersion 26
versionCode 136
versionName "dev136"
versionCode 143
versionName "0.6pre4"
externalNativeBuild {
cmake {
arguments "-DANDROID_APP_PLATFORM=android-21", "-DANDROID_STL=c++_static"

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.tildearrow.furnace"
android:versionCode="133"
android:versionName="0.6pre3"
android:versionCode="143"
android:versionName="0.6pre4"
android:installLocation="auto">
<!-- OpenGL ES 2.0 -->

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
demos/ay8930/joyful_.fur Normal file

Binary file not shown.

BIN
demos/c64/yeah!.fur Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
demos/multichip/collab.fur Normal file

Binary file not shown.

BIN
demos/multichip/one.fur Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
demos/pc98/CT_maintheme.fur Normal file

Binary file not shown.

BIN
demos/snes/ManbowSMW.fur Normal file

Binary file not shown.

BIN
demos/x16/TFV_Rise.fur Normal file

Binary file not shown.

BIN
demos/x16/richca.fur Normal file

Binary file not shown.

View File

@ -26,6 +26,7 @@ SOFTWARE.
*/
#include "ImGuiFileDialog.h"
#include "../../src/ta-log.h"
#ifdef __cplusplus
@ -132,8 +133,11 @@ namespace IGFD
#define resetButtonString ICON_FA_REPEAT
#endif // resetButtonString
#ifndef drivesButtonString
#define drivesButtonString "Drives"
#define drivesButtonString ICON_FA_HDD_O
#endif // drivesButtonString
#ifndef parentDirString
#define parentDirString ICON_FA_CHEVRON_UP
#endif // parentDirString
#ifndef editPathButtonString
#define editPathButtonString ICON_FA_PENCIL
#endif // editPathButtonString
@ -167,6 +171,9 @@ namespace IGFD
#ifndef buttonResetPathString
#define buttonResetPathString "Reset to current directory"
#endif // buttonResetPathString
#ifndef buttonParentDirString
#define buttonParentDirString "Go to parent directory"
#endif
#ifndef buttonCreateDirString
#define buttonCreateDirString "Create Directory"
#endif // buttonCreateDirString
@ -602,6 +609,10 @@ namespace IGFD
res.ext = pfn.substr(lastPoint + 1);
IGFD::Utils::ReplaceString(res.name, "." + res.ext, "");
}
if (res.path.empty()) {
res.path=separator;
}
if (!res.isOk)
{
@ -1191,12 +1202,14 @@ namespace IGFD
if (puDLGDirectoryMode) // directory mode
SetDefaultFileName(".");
else
SetDefaultFileName(puDLGDefaultFileName);
SetDefaultFileName("");
logV("IGFD: OpenCurrentPath()");
ScanDir(vFileDialogInternal, GetCurrentPath());
}
void IGFD::FileManager::SortFields(const FileDialogInternal& vFileDialogInternal, const SortingFieldEnum& vSortingField, const bool vCanChangeOrder)
{
logV("IGFD: SortFields()");
if (vSortingField != SortingFieldEnum::FIELD_NONE)
{
puHeaderFileName = tableHeaderFileNameString;
@ -1206,10 +1219,17 @@ namespace IGFD
#ifdef USE_THUMBNAILS
puHeaderFileThumbnails = tableHeaderFileThumbnailsString;
#endif // #ifdef USE_THUMBNAILS
}
} else {
logV("IGFD: sorting by NONE!");
}
if (prFileList.empty()) {
logV("IGFD: with an empty file list?");
}
if (vSortingField == SortingFieldEnum::FIELD_FILENAME)
{
logV("IGFD: sorting by name");
if (vCanChangeOrder && puSortingField == vSortingField) {
//printf("Change the sorting\n");
puSortingDirection[0] = true;//!puSortingDirection[0];
@ -1279,6 +1299,7 @@ namespace IGFD
}
else if (vSortingField == SortingFieldEnum::FIELD_TYPE)
{
logV("IGFD: sorting by type");
if (vCanChangeOrder && puSortingField == vSortingField)
puSortingDirection[1] = !puSortingDirection[1];
@ -1317,6 +1338,7 @@ namespace IGFD
}
else if (vSortingField == SortingFieldEnum::FIELD_SIZE)
{
logV("IGFD: sorting by size");
if (vCanChangeOrder && puSortingField == vSortingField)
puSortingDirection[2] = !puSortingDirection[2];
@ -1357,6 +1379,7 @@ namespace IGFD
}
else if (vSortingField == SortingFieldEnum::FIELD_DATE)
{
logV("IGFD: sorting by date");
if (vCanChangeOrder && puSortingField == vSortingField)
puSortingDirection[3] = !puSortingDirection[3];
@ -1448,6 +1471,8 @@ namespace IGFD
puSortingField = vSortingField;
}
logV("IGFD: applying filtering on file list");
ApplyFilteringOnFileList(vFileDialogInternal);
}
@ -1475,7 +1500,7 @@ namespace IGFD
infos->fileNameExt_optimized = prOptimizeFilenameForSearchOperations(infos->fileNameExt);
infos->fileType = vFileType;
if (infos->fileNameExt.empty() || (infos->fileNameExt == "." && !vFileDialogInternal.puFilterManager.puDLGFilters.empty())) return; // filename empty or filename is the current dir '.' //-V807
if (infos->fileNameExt.empty() || ((infos->fileNameExt == "." || infos->fileNameExt == "..") && !vFileDialogInternal.puFilterManager.puDLGFilters.empty())) return; // filename empty or filename is the current dir '.' //-V807
if (infos->fileNameExt != ".." && (vFileDialogInternal.puDLGflags & ImGuiFileDialogFlags_DontShowHiddenFiles) && infos->fileNameExt[0] == '.') // dont show hidden files
if (!vFileDialogInternal.puFilterManager.puDLGFilters.empty() || (vFileDialogInternal.puFilterManager.puDLGFilters.empty() && infos->fileNameExt != ".")) // except "." if in directory mode //-V728
return;
@ -1508,14 +1533,17 @@ namespace IGFD
void IGFD::FileManager::ScanDir(const FileDialogInternal& vFileDialogInternal, const std::string& vPath)
{
std::string path = vPath;
logV("IGFD: ScanDir(%s)",vPath);
if (prCurrentPathDecomposition.empty())
{
logV("IGFD: the current path decomposition is empty. setting.");
SetCurrentDir(path);
}
if (!prCurrentPathDecomposition.empty())
{
logV("IGFD: the current path decomposition is not empty. trying.");
#ifdef WIN32
if (path == puFsRoot)
path += std::string(1u, PATH_SEP);
@ -1543,6 +1571,7 @@ namespace IGFD
#else // dirent
struct dirent** files = nullptr;
int n = scandir(path.c_str(), &files, nullptr, inAlphaSort);
logV("IGFD: %d entries in directory",n);
if (n>0)
{
int i;
@ -1610,11 +1639,18 @@ namespace IGFD
}
free(files);
}
} else {
logV("IGFD: it's empty");
}
#endif // USE_STD_FILESYSTEM
logV("IGFD: sorting fields...");
SortFields(vFileDialogInternal, puSortingField, false);
}
} else {
logE("IGFD: current path decomposition is empty!");
}
fileListActuallyEmpty=prFileList.empty();
}
bool IGFD::FileManager::GetDrives()
@ -2217,6 +2253,16 @@ namespace IGFD
if (ImGui::IsItemHovered())
ImGui::SetTooltip(buttonResetPathString);
ImGui::SameLine();
if (IMGUI_BUTTON(parentDirString))
{
if (SetPathOnParentDirectoryIfAny()) {
OpenCurrentPath(vFileDialogInternal);
}
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip(buttonParentDirString);
#ifdef WIN32
ImGui::SameLine();
@ -2416,7 +2462,7 @@ namespace IGFD
void IGFD::FileDialogInternal::ResetForNewDialog()
{
puFileManager.fileListActuallyEmpty=false;
}
/////////////////////////////////////////////////////////////////////////////////////
@ -3401,7 +3447,7 @@ namespace IGFD
if (ps.isOk)
{
prFileDialogInternal.puFileManager.puDLGpath = ps.path;
prFileDialogInternal.puFileManager.SetDefaultFileName(vFilePathName);
prFileDialogInternal.puFileManager.SetDefaultFileName("");
prFileDialogInternal.puFilterManager.puDLGdefaultExt = "." + ps.ext;
}
else
@ -3717,16 +3763,17 @@ namespace IGFD
fdFilter.SetDefaultFilterIfNotDefined();
// init list of files
if (fdFile.IsFileListEmpty() && !fdFile.puShowDrives)
if (fdFile.IsFileListEmpty() && !fdFile.puShowDrives && !fdFile.fileListActuallyEmpty)
{
IGFD::Utils::ReplaceString(fdFile.puDLGDefaultFileName, fdFile.puDLGpath, ""); // local path
if (!fdFile.puDLGDefaultFileName.empty())
{
fdFile.SetDefaultFileName(fdFile.puDLGDefaultFileName);
fdFilter.SetSelectedFilterWithExt(fdFilter.puDLGdefaultExt);
}
else if (fdFile.puDLGDirectoryMode) // directory mode
} else if (fdFile.puDLGDirectoryMode) { // directory mode
fdFile.SetDefaultFileName(".");
}
logV("IGFD: fdFile.IsFileListEmpty() and !fdFile.puShowDrives");
fdFile.ScanDir(prFileDialogInternal, fdFile.puDLGpath);
}

View File

@ -875,6 +875,7 @@ namespace IGFD
#endif
SortingFieldEnum puSortingField = SortingFieldEnum::FIELD_FILENAME; // detail view sorting column
bool puShowDrives = false; // drives are shown (only on os windows)
bool fileListActuallyEmpty = false;
std::string puDLGpath; // base path set by user when OpenDialog/OpenModal was called
std::string puDLGDefaultFileName; // base default file path name set by user when OpenDialog/OpenModal was called

View File

@ -94,13 +94,14 @@ static void ImGui_ImplSDLRenderer_SetupRenderState()
SDL_RenderSetClipRect(bd->SDLRenderer, NULL);
}
void ImGui_ImplSDLRenderer_NewFrame()
bool ImGui_ImplSDLRenderer_NewFrame()
{
ImGui_ImplSDLRenderer_Data* bd = ImGui_ImplSDLRenderer_GetBackendData();
IM_ASSERT(bd != NULL && "Did you call ImGui_ImplSDLRenderer_Init()?");
if (!bd->FontTexture)
ImGui_ImplSDLRenderer_CreateDeviceObjects();
return ImGui_ImplSDLRenderer_CreateDeviceObjects();
return true;
}
void ImGui_ImplSDLRenderer_RenderDrawData(ImDrawData* draw_data)

View File

@ -21,7 +21,7 @@ struct SDL_Renderer;
IMGUI_IMPL_API bool ImGui_ImplSDLRenderer_Init(SDL_Renderer* renderer);
IMGUI_IMPL_API void ImGui_ImplSDLRenderer_Shutdown();
IMGUI_IMPL_API void ImGui_ImplSDLRenderer_NewFrame();
IMGUI_IMPL_API bool ImGui_ImplSDLRenderer_NewFrame();
IMGUI_IMPL_API void ImGui_ImplSDLRenderer_RenderDrawData(ImDrawData* draw_data);
// Called by Init/NewFrame/Shutdown

View File

@ -837,6 +837,8 @@ CODE
#include <stdint.h> // intptr_t
#endif
#include "../../src/fileutils.h"
// [Windows] On non-Visual Studio compilers, we default to IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS unless explicitly enabled
#if defined(_WIN32) && !defined(_MSC_VER) && !defined(IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS)
#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS
@ -12457,14 +12459,47 @@ void ImGui::ClearIniSettings()
g.SettingsHandlers[handler_n].ClearAllFn(&g, &g.SettingsHandlers[handler_n]);
}
void ImGui::LoadIniSettingsFromDisk(const char* ini_filename)
bool ImGui::LoadIniSettingsFromDisk(const char* ini_filename, bool redundancy)
{
size_t file_data_size = 0;
char* file_data = (char*)ImFileLoadToMemory(ini_filename, "rb", &file_data_size);
char* file_data = NULL;
if (redundancy) {
char fileName[4096];
for (int i=0; i<5; i++) {
bool viable=false;
if (i>0) {
snprintf(fileName,4095,"%s.%d",ini_filename,i);
} else {
strncpy(fileName,ini_filename,4095);
}
file_data=(char*)ImFileLoadToMemory(fileName, "rb", &file_data_size);
if (!file_data) continue;
for (size_t j=0; j<file_data_size; j++) {
if (file_data[j]!='\r' && file_data[j]!='\n' && file_data[j]!=' ') {
viable=true;
break;
}
}
if (!viable) {
IM_FREE(file_data);
file_data=NULL;
file_data_size=0;
} else {
break;
}
}
} else {
file_data=(char*)ImFileLoadToMemory(ini_filename, "rb", &file_data_size);
}
if (!file_data)
return;
return false;
LoadIniSettingsFromMemory(file_data, (size_t)file_data_size);
IM_FREE(file_data);
return true;
}
// Zero-tolerance, no error reporting, cheap .ini parsing
@ -12538,20 +12573,45 @@ void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size)
g.SettingsHandlers[handler_n].ApplyAllFn(&g, &g.SettingsHandlers[handler_n]);
}
void ImGui::SaveIniSettingsToDisk(const char* ini_filename)
bool ImGui::SaveIniSettingsToDisk(const char* ini_filename, bool redundancy)
{
ImGuiContext& g = *GImGui;
g.SettingsDirtyTimer = 0.0f;
if (!ini_filename)
return;
return false;
if (redundancy) {
char oldPath[4096];
char newPath[4096];
if (fileExists(ini_filename)==1) {
for (int i=4; i>=0; i--) {
if (i>0) {
snprintf(oldPath,4095,"%s.%d",ini_filename,i);
} else {
strncpy(oldPath,ini_filename,4095);
}
snprintf(newPath,4095,"%s.%d",ini_filename,i+1);
if (i>=4) {
deleteFile(oldPath);
} else {
moveFiles(oldPath,newPath);
}
}
}
}
size_t ini_data_size = 0;
const char* ini_data = SaveIniSettingsToMemory(&ini_data_size);
ImFileHandle f = ImFileOpen(ini_filename, "wt");
if (!f)
return;
ImFileWrite(ini_data, sizeof(char), ini_data_size, f);
return false;
bool areEqual=ImFileWrite(ini_data, sizeof(char), ini_data_size, f)==ini_data_size;
IM_ASSERT_USER_ERROR(areEqual, "ImFileWrite failed to write file!");
ImFileClose(f);
if (!areEqual && redundancy) deleteFile(ini_filename);
return areEqual;
}
// Call registered handlers (e.g. SettingsHandlerWindow_WriteAll() + custom handlers) to write their stuff into a text buffer

View File

@ -950,9 +950,9 @@ namespace ImGui
// - The disk functions are automatically called if io.IniFilename != NULL (default is "imgui.ini").
// - Set io.IniFilename to NULL to load/save manually. Read io.WantSaveIniSettings description about handling .ini saving manually.
// - Important: default value "imgui.ini" is relative to current working dir! Most apps will want to lock this to an absolute path (e.g. same path as executables).
IMGUI_API void LoadIniSettingsFromDisk(const char* ini_filename); // call after CreateContext() and before the first call to NewFrame(). NewFrame() automatically calls LoadIniSettingsFromDisk(io.IniFilename).
IMGUI_API bool LoadIniSettingsFromDisk(const char* ini_filename, bool redundancy=false); // call after CreateContext() and before the first call to NewFrame(). NewFrame() automatically calls LoadIniSettingsFromDisk(io.IniFilename).
IMGUI_API void LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size=0); // call after CreateContext() and before the first call to NewFrame() to provide .ini data from your own data source.
IMGUI_API void SaveIniSettingsToDisk(const char* ini_filename); // this is automatically called (if io.IniFilename is not empty) a few seconds after any modification that should be reflected in the .ini file (and also by DestroyContext).
IMGUI_API bool SaveIniSettingsToDisk(const char* ini_filename, bool redundancy=false); // this is automatically called (if io.IniFilename is not empty) a few seconds after any modification that should be reflected in the .ini file (and also by DestroyContext).
IMGUI_API const char* SaveIniSettingsToMemory(size_t* out_ini_size = NULL); // return a zero-terminated string with the .ini data which you can save by your own mean. call when io.WantSaveIniSettings is set, then save data by your own mean and clear io.WantSaveIniSettings.
// Debug Utilities

View File

@ -95,13 +95,15 @@ static void ImGui_ImplSDLRenderer_SetupRenderState()
SDL_RenderSetClipRect(bd->SDLRenderer, NULL);
}
void ImGui_ImplSDLRenderer_NewFrame()
bool ImGui_ImplSDLRenderer_NewFrame()
{
ImGui_ImplSDLRenderer_Data* bd = ImGui_ImplSDLRenderer_GetBackendData();
IM_ASSERT(bd != NULL && "Did you call ImGui_ImplSDLRenderer_Init()?");
if (!bd->FontTexture)
ImGui_ImplSDLRenderer_CreateDeviceObjects();
return ImGui_ImplSDLRenderer_CreateDeviceObjects();
return true;
}
void ImGui_ImplSDLRenderer_RenderDrawData(ImDrawData* draw_data)

View File

@ -21,7 +21,7 @@ struct SDL_Renderer;
IMGUI_IMPL_API bool ImGui_ImplSDLRenderer_Init(SDL_Renderer* renderer);
IMGUI_IMPL_API void ImGui_ImplSDLRenderer_Shutdown();
IMGUI_IMPL_API void ImGui_ImplSDLRenderer_NewFrame();
IMGUI_IMPL_API bool ImGui_ImplSDLRenderer_NewFrame();
IMGUI_IMPL_API void ImGui_ImplSDLRenderer_RenderDrawData(ImDrawData* draw_data);
// Called by Init/NewFrame/Shutdown

View File

@ -3112,8 +3112,9 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
ItemSize(bb, style.FramePadding.y);
if (!ItemAdd(frame_bb, id, NULL, ImGuiItemFlags_NoInertialScroll))
if (!ItemAdd(frame_bb, id, NULL, (temp_input_allowed ? ImGuiItemFlags_Inputable : 0) | ImGuiItemFlags_NoInertialScroll))
return false;
// Default format string when passing NULL
@ -3122,13 +3123,29 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.)
format = PatchFormatStringFloatToInt(format);
// Tabbing or CTRL-clicking on Slider turns it into an input box
const bool hovered = ItemHoverable(frame_bb, id);
if ((hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || g.NavActivateInputId == id)
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
if (!temp_input_is_active)
{
SetActiveID(id, window);
SetFocusID(id, window);
FocusWindow(window);
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0;
const bool clicked = (hovered && g.IO.MouseClicked[0]);
if (input_requested_by_tabbing || clicked || g.NavActivateId == id || g.NavActivateInputId == id)
{
SetActiveID(id, window);
SetFocusID(id, window);
FocusWindow(window);
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
if (temp_input_allowed && (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || g.NavActivateInputId == id))
temp_input_is_active = true;
}
}
if (temp_input_is_active)
{
// Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0;
return TempInputScalar(frame_bb, id, label, data_type, p_data, format, is_clamp_input ? p_min : NULL, is_clamp_input ? p_max : NULL);
}
// Draw frame
@ -3154,6 +3171,7 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d
if (label_size.x > 0.0f)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
return value_changed;
}

View File

@ -204,7 +204,9 @@ static void CopyNFDCharToWChar( const nfdchar_t *inStr, wchar_t **outStr )
#ifdef _DEBUG
int inStrCharacterCount = static_cast<int>(NFDi_UTF8_Strlen(inStr));
assert( ret == inStrCharacterCount );
if (ret!=inStrCharacterCount) {
logW("length does not match! %d != %d",ret,inStrCharacterCount);
}
#else
_NFD_UNUSED(ret);
#endif

View File

@ -78,10 +78,10 @@ C2: 31 9 0 11 15 0 0 1 3 0 0
// vgm offset = 00001d22, channels used = 1-------
@:8 Synth Bass 2
LFO: 0 0 0 0 0
CH: 64 5 4 0 0 120 0
M1: 27 10 5 11 7 21 1 1 3 0 0
C1: 31 0 15 11 7 3 0 1 3 0 0
M2: 22 13 13 8 6 21 3 12 3 0 0
C2: 31 15 16 11 8 13 0 2 3 0 0
CH: 64 0 0 0 0 120 0
M1: 22 12 20 8 15 39 3 12 3 0 0
C1: 31 13 11 11 4 25 3 4 3 0 0
M2: 27 0 5 11 7 23 1 1 3 0 0
C2: 31 0 15 11 7 3 0 1 3 0 0

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
instruments/OPLL/Chime.fui Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -121,8 +121,8 @@ ID | macro
33 | KSR
---|-----------------------------
40 | operator 2 macros
60 | operator 2 macros
80 | operator 2 macros
60 | operator 3 macros
80 | operator 4 macros
the interpretation of duty, wave and extra macros depends on chip/instrument type:

View File

@ -1,8 +1,14 @@
# wavetable editor
Wavetable synthesizers, in context of Furnace, are sound sources that operate on extremely short n-bit PCM streams. By extremely short, no more than 256 bytes. This amount of space is nowhere near enough to store an actual sampled sound, it allows certain amount of freedom to define a waveform shape. As of Furnace 0.5.8, wavetable editor affects PC Engine, WonderSwan and channel 3 of Game Boy.
Wavetable synthesizers, in context of Furnace, are sound sources that operate on extremely short n-bit PCM streams. By extremely short, no more than 256 bytes. This amount of space is nowhere near enough to store an actual sampled sound, it allows certain amount of freedom to define a waveform shape. As of Furnace 0.6pre4, wavetable editor affects PC Engine, WonderSwan, Namco WSGs, Virtual Boy, Game.com, SCC, FDS, Seta X1-010, Konami Bubble System WSG, SNES, Amiga and channel 3 of Game Boy.
Furnace's wavetable editor is rather simple, you can draw the waveform using mouse or by pasting an MML bit stream in the input field. Maximum wave width (length) is 256 bytes, and maximum wave height (depth) is 256. NOTE: Game Boy, PCE, WonderSwan and Bubble System can handle max 32 byte waveforms, X1-010 can handle max 128 byte waveforms as of now, with 16-level height for GB, X1-010 Envelope, WS, Bubble System and N163, and 32-level height for PCE. If a larger wave is defined for these chips, it will be squashed to fit within the constraints of the chips.
Furnace's wavetable editor is rather simple, you can draw the waveform using mouse or by pasting an MML bit stream in the input field. Maximum wave width (length) is 256 bytes, and maximum wave height (depth) is 256. NOTE: Game Boy, PCE, WonderSwan, Namco WSG, N163, Game.com, Virtual Boy and Bubble System can handle max 32 byte waveforms, X1-010 can handle max 128 byte waveforms as of now, with 16-level height for GB, X1-010 Envelope, WS, Bubble System, SNES, Namco WSG and N163, 32-level height for PCE and 64-level height for Virtual Boy. If a larger wave is defined for these chips, it will be squashed to fit within the constraints of the chips.
Furnace's wavetable editor features multiple ways of creating desired waveform shape:
- Shape tab allows you to select a few predefined basic shapes and indirectly edit it via "Duty", "Exponent" and "XOR Point" sliders TODO: what the last two are doing? What is amplitude/phase for?)
- FM is for creating the waveform with frequency modulation synthesis principles: One can set carrier/modulation levels, frquency multiplier, connection between operators and FM waveforms of these operators.
- WaveTools allows user to fine-tune the waveform: scale said waveform in both X and Y axes, smoothen, amplify, normalize, convert to signed/unisgned, invert or even randomize the wavetable.
## wavetable synthesizer

View File

@ -0,0 +1,41 @@
# Ensoniq ES5506 (OTTO)
Sample-based synthesis chip used in a bunch of Taito arcade machines and PC sound cards like Soundscape Elite. A variant of it was the heart of the well-known Gravis Ultrasound.
it supports a whooping 32 channels of 16-bit PCM and:
- Real time digital filters
- Frequency interpolation
- Loop start and stop positions for each voice (bidirectional and reverse looping)
- Internal volume multiplication and stereo panning
- Hardware support for envelopes
# effects
- `10xx`: set waveform.
- `11xx`: set filter mode (0-3)
- `120x`: set pause (bit 0). Pauses the sample until the bit is unset, where it will then resume where it left off.
- `14xx`: set filter coefficient K1 low byte.
- `15xx`: set filter coefficient K1 high byte.
- `16xx`: set filter coefficient K2 low byte.
- `17xx`: set filter coefficient K2 high byte.
- `18xx`: set filter coefficient K1 slide up.
- `19xx`: set filter coefficient K1 slide down.
- `1Axx`: set filter coefficient K2 slide up.
- `1Bxx`: set filter coefficient K2 slide down.
- `20xx`: set envelope count.
- `22xx`: set envelope left volume ramp.
- `23xx`: set envelope right volume ramp.
- `24xx`: set envelope filter coefficient K1 ramp.
- `25xx`: set envelope filter coefficient K1 ramp (slower).
- `26xx`: set envelope filter coefficient K2 ramp.
- `27xx`: set envelope filter coefficient K2 ramp (slower).
- `3xxx`: set coarse filter coefficient K1.
- `4xxx`: set coarse filter coefficient K2.
- `81xx`: set panning (left channel).
- `82xx`: set panning (right channel).
- `88xx`: set panning (rear channels).
- `89xx`: set panning (rear left channel).
- `8Axx`: set panning (rear right channel).
- `9xxx`: set sample offset (x256).
- `DFxx`: set sample playback direction.

View File

@ -42,6 +42,33 @@ also known as Famicom. it is a five-channel sound generator: first two channels
- `00`: PCM (software).
- `01`: DPCM (hardware).
- when in DPCM mode, samples will sound muffled (due to its nature), availables pitches are limited and loop point is ignored.
- `19xx`: set triangle linear counter.
- `00` to `7F` set the counter.
- `80` and higher halt it.
- `20xx`: set DPCM frequency.
- only works in DPCM mode.
- see table below for possible values.
# DPCM frequency table
val | NTSC | PAL
----|-----------|-----------
00 | 4181.7Hz | 4177.4Hz
01 | 4709.9Hz | 4696.6Hz
02 | 5264.0Hz | 5261.4Hz
03 | 5593.0Hz | 5579.2Hz
04 | 6257.9Hz | 6023.9Hz
05 | 7046.3Hz | 7044.9Hz
06 | 7919.3Hz | 7917.2Hz
07 | 8363.4Hz | 8397.0Hz
08 | 9419.9Hz | 9446.6Hz
09 | 11186.1Hz | 11233.8Hz
0A | 12604.0Hz | 12595.5Hz
0B | 13982.6Hz | 14089.9Hz
0C | 16884.6Hz | 16965.4Hz
0D | 21306.8Hz | 21315.5Hz
0E | 24858.0Hz | 25191.0Hz
0F | 33143.9Hz | 33252.1Hz
# length counter table

View File

@ -8,7 +8,7 @@ because the chip lacks sample interpolation, it is recommended that you try to p
the QSound chip also has a small echo buffer, somewhat similar to the SNES, although with a very basic (and non-adjustable) filter. it is however possible to adjust the feedback and length of the echo buffer (the initial values can be set in the "configure chip" option in the file menu or the chip manager).
there are also 3 ADPCM channels, however they cannot be used in Furnace yet. they have been reserved in case this feature is added later. ADPCM samples are limited to 8012 Hz.
there are also 3 ADPCM channels. ADPCM samples are fixed to 8012 Hz.
# effects

View File

@ -0,0 +1,23 @@
# Sharp SM8521
The SM8521 is the CPU and sound chip of the Game.com, a handheld console released in 1997 as a competitor to the infamous Nintendo Virtual Boy.
Ultimately, most of the games for the Game.com ended up being failiures in the eyes of reviewers, thus giving the Game.com a pretty bad reputation. This was one of the reasons that the Game.com only ended up selling at least 300,000 units. For these reasons and more, the Game.com ended up being discontinued in 2000.
However, for its time, it was a pretty competitively priced system. The Gameboy Color was to be released in a year for $79.95, while the Game.com was released for $69.99, and its later model, the Pocket Pro, was released in mid-1999 for $29.99 due to the Game.com's apparent significant decrease in value.
In fact, most games never used the wavetable/noise mode of the chip. Sonic Jam, for example, uses a sine wave with a software-controlled volume envelope on the DAC channel (see below for more information on the DAC channel).
The sound-related features and quirks of the SM8521 are as follows:
- 2 4-bit wavetable channels
- a noise channel (which can go up to a very high pitch, creating an almost periodic noise sound)
- 5-bit volume
- A low bit-depth output (which means it distorts a lot).
- It phase resets when you switch waves
- 12-bit pitch with a wide frequency range
- A software-controlled D/A register that (potentially) requires all other registers to be stopped to play. Due to this, it is currently, it is not implemented in Furnace as of version 0.6pre4.
## effect commands
- `10xx` Set waveform
- `xx` is a value between 0 and 255, that sets the waveform of the channel you place it on.

View File

@ -54,6 +54,10 @@ Furnace also allows the SNES to use wavetables (and the wavetable synthesizer) i
- 80 to FF for -128 to -1.
- setting this to -128 is not recommended as it may cause echo output to overflow and therefore click.
- `1Dxx`: set noise generator frequency (00 to 1F).
- `1Exx`: set left dry/global volume.
- this does not affect echo.
- `1Fxx`: set right dry/global volume.
- this does not affect echo.
- `20xx`: set attack (0 to F).
- only in ADSR envelope mode.
- `21xx`: set decay (0 to 7).

View File

@ -69,10 +69,13 @@ hex | description
.. | ...
ef | preset delay 15
----|------------------------------------
f4 | call symbol (16-bit index follows; only used internally)
f5 | jump to sub-block (address follows)
f6 | go to sub-block (32-bit offset follows)
f7 | full command (command and data follows)
f8 | go to sub-block (offset follows)
f8 | go to sub-block (16-bit offset follows)
f9 | return from sub-block
fa | jump (offset follows)
fa | jump (address follows)
fb | set tick rate (4 bytes)
fc | wait (16-bit)
fd | wait (8-bit)

View File

@ -32,6 +32,19 @@ these fields are 0 in format versions prior to 100 (0.6pre1).
the format versions are:
- 155: Furnace dev155
- 154: Furnace dev154
- 153: Furnace dev153
- 152: Furnace dev152
- 151: Furnace dev151
- 150: Furnace dev150
- 149: Furnace dev149
- 148: Furnace dev148
- 147: Furnace dev147
- 146: Furnace Pro (joke version)
- 145: Furnace dev145
- 144: Furnace dev144
- 143: Furnace 0.6pre4
- 142: Furnace dev142
- 141: Furnace Tournament Edition (for intro tune contest)
- 140: Furnace dev140
@ -245,7 +258,7 @@ size | description
| - 0x9c: Virtual Boy - 6 channels
| - 0x9d: VRC7 - 6 channels
| - 0x9e: YM2610B - 16 channels
| - 0x9f: ZX Spectrum (beeper) - 6 channels
| - 0x9f: ZX Spectrum (beeper, tildearrow engine) - 6 channels
| - 0xa0: YM2612 extended - 9 channels
| - 0xa1: Konami SCC - 5 channels
| - 0xa2: OPL drums (YM3526) - 11 channels
@ -287,6 +300,9 @@ size | description
| - 0xc6: K007232 - 2 channels
| - 0xc7: GA20 - 4 channels
| - 0xc8: SM8521 - 3 channels
| - 0xc9: M114S - 16 channels
| - 0xca: ZX Spectrum (beeper, QuadTone engine) - 5 channels
| - 0xcb: Casio PV-1000 - 3 channels
| - 0xde: YM2610B extended - 19 channels
| - 0xe0: QSound - 19 channels
| - 0xfc: Pong - 1 channel
@ -295,10 +311,12 @@ size | description
| - 0xff: reserved for development
| - (compound!) means that the system is composed of two or more chips,
| and has to be flattened.
32 | sound chip volumes
32 | sound chip volumes (<135) or reserved
| - signed char, 64=1.0, 127=~2.0
32 | sound chip panning
| - as of version 135 these fields only exist for compatibility reasons.
32 | sound chip panning (<135) or reserved
| - signed char, -128=left, 127=right
| - as of version 135 these fields only exist for compatibility reasons.
128 | sound chip flag pointers (>=119) or sound chip flags
| - before 118, these were 32-bit flags.
| - for conversion details, see the "converting from old flags" section.
@ -407,7 +425,8 @@ size | description
1 | automatic patchbay (>=136)
--- | **a couple more compat flags** (>=138)
1 | broken portamento during legato
7 | reserved
1 | broken macro during note off in some FM chips (>=155)
6 | reserved
--- | **speed pattern of first song** (>=139)
1 | length of speed pattern (fail if this is lower than 0 or higher than 16)
16 | speed pattern (this overrides speed 1 and speed 2 settings)

View File

@ -114,8 +114,9 @@ the following instrument types are available:
- 44: T6W28
- 45: K007232
- 46: GA20
- 47: Pokémon Mini
- 47: Pokémon Mini/QuadTone
- 48: SM8521
- 49: PV-1000
the following feature codes are recognized:
@ -388,7 +389,7 @@ the sample map format:
```
size | description
-----|------------------------------------
2 | note to play
2 | note to play (>=152) or reserved
2 | sample to play
```

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 KiB

After

Width:  |  Height:  |  Size: 554 KiB

View File

@ -15,9 +15,9 @@ fi
cd win32build
# TODO: potential Arch-ism?
i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Werror" -DBUILD_SHARED_LIBS=OFF -DSUPPORT_XP=ON .. || exit 1
i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Werror" -DBUILD_SHARED_LIBS=OFF -DSUPPORT_XP=ON .. || exit 1
make -j8 || exit 1
i686-w64-mingw32-strip -s furnace.exe || exit 1
#i686-w64-mingw32-strip -s furnace.exe || exit 1
cd ..

View File

@ -15,9 +15,9 @@ fi
cd winbuild
# TODO: potential Arch-ism?
x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Werror" .. || exit 1
x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Wno-deprecated-declarations -Werror" .. || exit 1
make -j8 || exit 1
x86_64-w64-mingw32-strip -s furnace.exe || exit 1
#x86_64-w64-mingw32-strip -s furnace.exe || exit 1
cd ..

View File

@ -0,0 +1,4 @@
all: player
player: player.s sample.bin seq.bin wave.bin
vasmm68k_mot -Fhunkexe -kick1hunks -nosym -o player player.s

View File

@ -0,0 +1,56 @@
# Amiga verification export format
"ROM" export format exclusively for verifying whether the Amiga emulation in Furnace is correct.
do not assume this is the actual ROM export! it is nothing more than a register dump kind of thing...
# process
enable the setting in Furnace to unlock the export option by adding this to furnace.cfg:
```
iCannotWait=1
```
go to file > export Amiga validation data...
put sample.bin, sbook.bin, seq.bin, wave.bin and wbook.bin in this directory.
type `make`. you need vasm (68000 with Mot syntax) in order for it to work.
alternatively, type:
```
vasmm68k_mot -Fhunkexe -kick1hunks -nosym -o player player.s
```
run `player` on Amiga. it should play the exported song.
# notes
may not work correctly if you have slow/fast memory!
sequence and wave data should reside in fast memory but I haven't figured out how to...
# sequence format
## 00-0F: per-channel (00, 10, 20, 30)
- 00 xxxxxx yyyy: set loc/len
- x: loc
- y: len
- 01 xxxxxx yyyy: initialize wavetable (xxxx: pos; yy: length)
- 02 xx: set loc/len from sample book
- 03 xx: initialize wavetable from wave book
- 04 xxxx: initialize wavetable from wave book (short)
- 06 xxxx: set period
- 08 xx: set volume
- 0a xxxx: set data
## F0-FF: global
- f0: do nothing
- f1: next tick
- f2 xx: wait
- f3 xxxx: wait
- f6 xxxx: write to DMACON
- fe xxxx: write to ADKCON
- ff: end of song

View File

@ -0,0 +1,386 @@
; Furnace validation player code
; this is NOT the ROM export you're looking for!
VPOSR = $dff004
VHPOSR = $dff006
COLOR00 = $dff180
chipBase=$dff000
DMACONR = $02
POTGOR = $16
DMACON = $96
ADKCON = $9e
AUDBASE = $a0
AUD0LCH = $a0
AUD0LCL = $a2
AUD0LEN = $a4
AUD0PER = $a6
AUD0VOL = $a8
AUD0DAT = $aa
AUD1VOL = $b8
AUD2VOL = $c8
AUD3VOL = $d8
code_c
init:
move.b #2,$bfe001
lea chipBase,a0
move.w #15,DMACON(a0)
waitCon:
move.w DMACONR(a0),d0
andi.w #15,d0
bne waitCon
move.w #$8200,DMACON(a0)
move.w #$40,AUD0VOL(a0)
move.w #$40,AUD1VOL(a0)
move.w #$40,AUD2VOL(a0)
move.w #$40,AUD3VOL(a0)
lea seqAddr(pc),a0
lea sequence(pc),a1
move.l a1,(a0)
main:
bsr waitVBlank
move.w #$000,d4
move.w d4,COLOR00
lea chipBase,a0
btst.b #2,POTGOR(a0)
bne next
lea state(pc),a0
move.w #1,2(a0)
next:
bsr nextTick
lea state(pc),a0
tst.w 2(a0)
beq main
finish:
lea chipBase,a0
move.w #15,DMACON(a0)
clr.l d0
rts
waitVBlank:
move.l (VPOSR),d0
and.l #$1ff00,d0
cmp.l #$bc00,d0
bne waitVBlank
waitVBlank2:
move.l (VPOSR),d0
and.l #$1ff00,d0
cmp.l #$bd00,d0
bne waitVBlank2
rts
nextTick:
lea state(pc),a4
move.w (a4),d0
subi.w #1,d0
bmi nextTick0
move.w d0,(a4)
rts
nextTick0:
move.l seqAddr(pc),a2
nextTick1:
; get next command
clr.w d0
move.b (a2)+,d0
move.w #$0ff,d4
move.w d4,COLOR00
testSpecial:
cmp.w #$f0,d0
bmi testChannel
testF1:
; f1 - next tick
cmp.b #$f1,d0
bne testF2
; end of tick
move.w #0,(a4)
bra endTick
testF2:
; f2 - wait (char)
cmp.b #$f2,d0
bne testF3
move.b (a2)+,d0
andi.w #$ff,d0
move.w d0,(a4)
bra endTick
testF3:
; f3 - wait (short)
cmp.b #$f3,d0
bne testF6
clr.w d2
move.b (a2)+,d2
lsl.w #8,d2
or.b (a2)+,d2
move.w d2,(a4)
bra endTick
testF6:
; f6 - write DMACON
cmp.b #$f6,d0
bne testFE
move.w #$f00,d4
move.w d4,COLOR00
clr.w d2
move.b (a2)+,d2
lsl.w #8,d2
or.b (a2)+,d2
move.w d2,chipBase+DMACON
; wait for DMACON to be done
move.b (VHPOSR),d0
dmaConWait:
cmp.b (VHPOSR),d0
beq dmaConWait
; wait for DMACON to be done -2
move.b (VHPOSR),d0
dmaConWait1:
cmp.b (VHPOSR),d0
beq dmaConWait1
bra nextTick1
testFE:
; fe - write ADKCON
cmp.b #$fe,d0
bne testFF
clr.w d2
move.b (a2)+,d2
lsl.w #8,d2
or.b (a2)+,d2
move.w d2,chipBase+ADKCON
bra nextTick1
testFF:
; ff - end of song
cmp.b #$ff,d0
bne testOther
theEnd:
lea sequence(pc),a2
testOther:
; something else
bra nextTick1
testChannel:
cmp.b #$40,d0
bge invalidCmd
; process channel
move.b d0,d1
andi.b #15,d0
; check for 0
bne chanNotZero
sampleWrite:
; write loc/len
move.w #$f0f,d4
move.w d4,COLOR00
clr.l d2
move.b (a2)+,d2
lsl.l #8,d2
or.b (a2)+,d2
lsl.l #8,d2
or.b (a2)+,d2
lea sampleData,a0
add.l a0,d2
lea chipBase,a0
move.b d1,d0
andi.l #$ff,d0
addi.l #$a0,d0
adda.l d0,a0
move.l d2,(a0)+ ; location
clr.w d2
move.b (a2)+,d2
lsl.w #8,d2
or.b (a2)+,d2
move.w d2,(a0) ; length
bra nextTick1
chanNotZero:
; check for 8 (VOL)
cmp.b #8,d0
bne chanSampleBook
; write volume
clr.w d2
move.b (a2)+,d2
bra chanWrite
chanSampleBook:
; check for 2 (loc/len from book)
cmp.b #2,d0
bne chanWaveChange
move.w #$f0f,d4
move.w d4,COLOR00
clr.l d3
move.b (a2)+,d3
bsr getSampleBook
; write loc/len
lea sampleData,a0
add.l a0,d2
lea chipBase,a0
move.b d1,d0
andi.l #$f0,d0
addi.l #$a0,d0
adda.l d0,a0
move.l d2,(a0)+ ; location
move.w d3,(a0) ; length
bra nextTick1
chanWaveChange:
; check for 1 (wave change)
cmp.b #1,d0
bne chanWaveBookB
; copy wave
clr.l d2
move.b (a2)+,d2
lsl.l #8,d2
or.b (a2)+,d2
lsl.l #8,d2
or.b (a2)+,d2
add.l #wavetable,d2
move.l d2,a0
lea sampleData,a1
andi.l #$30,d1
lsl.l #4,d1
adda.l d1,a1
clr.l d2
move.b (a2)+,d2
lsl.l #8,d2
or.b (a2)+,d2
bsr copyWave
bra nextTick1
chanWaveBookB:
; check for 3 (wave change from book)
cmp.b #3,d0
bne chanWaveBookW
clr.l d3
move.b (a2)+,d3
bsr getWaveBook
lea sampleData,a1
andi.l #$30,d1
lsl.l #4,d1
adda.l d1,a1
bsr copyWave
bra nextTick1
chanWaveBookW:
; check for 4 (wave change from book short)
cmp.b #4,d0
bne chanOther
clr.l d3
move.b (a2)+,d3
lsl.l #8,d3
or.b (a2)+,d3
bsr getWaveBook
lea sampleData,a1
andi.l #$30,d1
lsl.l #4,d1
adda.l d1,a1
bsr copyWave
bra nextTick1
chanOther:
; get value and write
clr.w d2
move.b (a2)+,d2
lsl.w #8,d2
or.b (a2)+,d2
chanWrite:
move.w #$ff0,d4
move.w d4,COLOR00
lea chipBase,a0
or.b d1,d0
addi.b #AUDBASE,d0
andi.l #$ff,d0
adda.l d0,a0
move.w d2,(a0)
invalidCmd:
bra nextTick1
endTick:
lea seqAddr(pc),a3
move.l a2,(a3)
move.w #$000,d4
move.w d4,COLOR00
rts
; a0: source. a1: destination. d2: length.
copyWave:
; don't copy a zero-length wave
tst.l d2
beq noCopy
copyWaveLoop:
move.w (a0)+,(a1)+
subq.l #2,d2
bne copyWaveLoop
noCopy:
rts
; put wave book entry in a0/d2. d3: index (modified).
getWaveBook:
lea waveBook(pc),a3
lsl.l #2,d3
move.l (a3,d3),d2
move.l d2,d3
rol.l #8,d2
andi.l #$ff,d2
bne getWaveBook2
move.w #$100,d2
getWaveBook2:
andi.l #$ffffff,d3
add.l #wavetable,d3
move.l d3,a0
rts
; get sample book entry in d2/d3. d3: index.
getSampleBook:
lea sampleBook(pc),a3
lsl.l #3,d3
adda.l d3,a3
move.l (a3)+,d2
move.l (a3)+,d3
andi.l #$ffffff,d2
andi.l #$ffff,d3
rts
data_c
cnop 0,4
curColor:
dc.w 0
state:
dc.w 0 ; ticks
dc.w 0 ; quit
cnop 0,4
seqAddr:
dc.l 0
cnop 0,4
sampleBook:
incbin "sbook.bin"
cnop 0,4
waveBook:
incbin "wbook.bin"
cnop 0,4
sequence:
incbin "seq.bin"
cnop 0,4
sampleData:
incbin "sample.bin"
cnop 0,4
wavetable:
incbin "wave.bin"

428
src/engine/cmdStream.cpp Normal file
View File

@ -0,0 +1,428 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2023 tildearrow and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#define _USE_MATH_DEFINES
#include "cmdStream.h"
#include "dispatch.h"
#include "engine.h"
#include "../ta-log.h"
bool DivCSChannelState::doCall(unsigned int addr) {
if (callStackPos>=8) {
readPos=0;
return false;
}
callStack[callStackPos++]=readPos;
readPos=addr;
return true;
}
void DivCSPlayer::cleanup() {
delete b;
}
bool DivCSPlayer::tick() {
bool ticked=false;
for (int i=0; i<e->getTotalChannelCount(); i++) {
bool sendVolume=false;
bool sendPitch=false;
if (chan[i].readPos==0) continue;
ticked=true;
chan[i].waitTicks--;
while (chan[i].waitTicks<=0) {
if (!stream.seek(chan[i].readPos,SEEK_SET)) {
logE("%d: access violation! $%x",i,chan[i].readPos);
chan[i].readPos=0;
break;
}
unsigned char next=stream.readC();
unsigned char command=0;
if (next<0xb3) { // note
e->dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,(int)next-60));
chan[i].note=(int)next-60;
chan[i].vibratoPos=0;
} else if (next>=0xd0 && next<=0xdf) {
command=fastCmds[next&15];
} else if (next>=0xe0 && next<=0xef) { // preset delay
chan[i].waitTicks=fastDelays[next&15];
} else switch (next) {
case 0xb4: // note on null
e->dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,DIV_NOTE_NULL));
chan[i].vibratoPos=0;
break;
case 0xb5: // note off
e->dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF,i));
break;
case 0xb6: // note off env
e->dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF_ENV,i));
break;
case 0xb7: // env release
e->dispatchCmd(DivCommand(DIV_CMD_ENV_RELEASE,i));
break;
case 0xb8: case 0xbe: case 0xc0: case 0xc2:
case 0xc3: case 0xc4: case 0xc5: case 0xc6:
case 0xc7: case 0xc8: case 0xc9: case 0xca:
command=next-0xb4;
break;
case 0xf7:
command=stream.readC();
break;
case 0xf8: {
unsigned int callAddr=chan[i].readPos+2+stream.readS();
if (!chan[i].doCall(callAddr)) {
logE("%d: (callb16) stack error!",i);
}
break;
}
case 0xf6: {
unsigned int callAddr=chan[i].readPos+4+stream.readI();
if (!chan[i].doCall(callAddr)) {
logE("%d: (callb32) stack error!",i);
}
break;
}
case 0xf5: {
unsigned int callAddr=stream.readI();
if (!chan[i].doCall(callAddr)) {
logE("%d: (call) stack error!",i);
}
break;
}
case 0xf4: {
logE("%d: (callsym) not supported here!",i);
chan[i].readPos=0;
break;
}
case 0xf9:
if (!chan[i].callStackPos) {
logE("%d: (ret) stack error!",i);
chan[i].readPos=0;
break;
}
chan[i].readPos=chan[i].callStack[--chan[i].callStackPos];
break;
case 0xfa:
chan[i].readPos=stream.readI();
break;
case 0xfb:
logE("TODO: RATE");
stream.readI();
break;
case 0xfc:
chan[i].waitTicks=(unsigned short)stream.readS();
break;
case 0xfd:
chan[i].waitTicks=(unsigned char)stream.readC();
break;
case 0xfe:
chan[i].waitTicks=1;
break;
case 0xff:
chan[i].readPos=0;
logI("%d: stop",i);
break;
default:
logE("%d: illegal instruction $%.2x! $%.x",i,next,chan[i].readPos);
chan[i].readPos=0;
break;
}
if (chan[i].readPos==0) break;
if (command) {
int arg0=0;
int arg1=0;
switch (command) {
case DIV_CMD_INSTRUMENT:
case DIV_CMD_HINT_VIBRATO_RANGE:
case DIV_CMD_HINT_VIBRATO_SHAPE:
case DIV_CMD_HINT_VOLUME:
case DIV_CMD_HINT_ARP_TIME:
arg0=(unsigned char)stream.readC();
break;
case DIV_CMD_HINT_PITCH:
arg0=(signed char)stream.readC();
break;
case DIV_CMD_PANNING:
case DIV_CMD_HINT_VIBRATO:
case DIV_CMD_HINT_ARPEGGIO:
case DIV_CMD_HINT_PORTA:
arg0=(signed char)stream.readC();
arg1=(unsigned char)stream.readC();
break;
case DIV_CMD_PRE_PORTA:
arg0=(unsigned char)stream.readC();
arg1=(arg0&0x40)?1:0;
arg0=(arg0&0x80)?1:0;
break;
case DIV_CMD_HINT_VOL_SLIDE:
arg0=(short)stream.readS();
break;
case DIV_CMD_HINT_LEGATO:
arg0=(unsigned char)stream.readC();
if (arg0==0xff) {
arg0=DIV_NOTE_NULL;
} else {
arg0-=60;
}
break;
case DIV_CMD_SAMPLE_MODE:
case DIV_CMD_SAMPLE_FREQ:
case DIV_CMD_SAMPLE_BANK:
case DIV_CMD_SAMPLE_POS:
case DIV_CMD_SAMPLE_DIR:
case DIV_CMD_FM_HARD_RESET:
case DIV_CMD_FM_LFO:
case DIV_CMD_FM_LFO_WAVE:
case DIV_CMD_FM_FB:
case DIV_CMD_FM_EXTCH:
case DIV_CMD_FM_AM_DEPTH:
case DIV_CMD_FM_PM_DEPTH:
case DIV_CMD_STD_NOISE_FREQ:
case DIV_CMD_STD_NOISE_MODE:
case DIV_CMD_WAVE:
case DIV_CMD_GB_SWEEP_TIME:
case DIV_CMD_GB_SWEEP_DIR:
case DIV_CMD_PCE_LFO_MODE:
case DIV_CMD_PCE_LFO_SPEED:
case DIV_CMD_NES_DMC:
case DIV_CMD_C64_CUTOFF:
case DIV_CMD_C64_RESONANCE:
case DIV_CMD_C64_FILTER_MODE:
case DIV_CMD_C64_RESET_TIME:
case DIV_CMD_C64_RESET_MASK:
case DIV_CMD_C64_FILTER_RESET:
case DIV_CMD_C64_DUTY_RESET:
case DIV_CMD_C64_EXTENDED:
case DIV_CMD_AY_ENVELOPE_SET:
case DIV_CMD_AY_ENVELOPE_LOW:
case DIV_CMD_AY_ENVELOPE_HIGH:
case DIV_CMD_AY_ENVELOPE_SLIDE:
case DIV_CMD_AY_NOISE_MASK_AND:
case DIV_CMD_AY_NOISE_MASK_OR:
case DIV_CMD_AY_AUTO_ENVELOPE:
case DIV_CMD_FDS_MOD_DEPTH:
case DIV_CMD_FDS_MOD_HIGH:
case DIV_CMD_FDS_MOD_LOW:
case DIV_CMD_FDS_MOD_POS:
case DIV_CMD_FDS_MOD_WAVE:
case DIV_CMD_SAA_ENVELOPE:
case DIV_CMD_AMIGA_FILTER:
case DIV_CMD_AMIGA_AM:
case DIV_CMD_AMIGA_PM:
case DIV_CMD_MACRO_OFF:
case DIV_CMD_MACRO_ON:
arg0=(unsigned char)stream.readC();
break;
case DIV_CMD_FM_TL:
case DIV_CMD_FM_AM:
case DIV_CMD_FM_AR:
case DIV_CMD_FM_DR:
case DIV_CMD_FM_SL:
case DIV_CMD_FM_D2R:
case DIV_CMD_FM_RR:
case DIV_CMD_FM_DT:
case DIV_CMD_FM_DT2:
case DIV_CMD_FM_RS:
case DIV_CMD_FM_KSR:
case DIV_CMD_FM_VIB:
case DIV_CMD_FM_SUS:
case DIV_CMD_FM_WS:
case DIV_CMD_FM_SSG:
case DIV_CMD_FM_REV:
case DIV_CMD_FM_EG_SHIFT:
case DIV_CMD_FM_MULT:
case DIV_CMD_FM_FINE:
case DIV_CMD_AY_IO_WRITE:
case DIV_CMD_AY_AUTO_PWM:
case DIV_CMD_SURROUND_PANNING:
arg0=(unsigned char)stream.readC();
arg1=(unsigned char)stream.readC();
break;
case DIV_CMD_C64_FINE_DUTY:
case DIV_CMD_C64_FINE_CUTOFF:
case DIV_CMD_LYNX_LFSR_LOAD:
arg0=(unsigned short)stream.readS();
break;
case DIV_CMD_FM_FIXFREQ:
arg0=(unsigned short)stream.readS();
arg1=arg0&0x7ff;
arg0>>=12;
break;
case DIV_CMD_NES_SWEEP:
arg0=(unsigned char)stream.readC();
arg1=arg0&0x77;
arg0=(arg0&8)?1:0;
break;
}
switch (command) {
case DIV_CMD_HINT_VOLUME:
chan[i].volume=arg0<<8;
sendVolume=true;
break;
case DIV_CMD_HINT_VOL_SLIDE:
chan[i].volSpeed=arg0;
break;
case DIV_CMD_HINT_PITCH:
chan[i].pitch=arg0;
sendPitch=true;
break;
case DIV_CMD_HINT_VIBRATO:
chan[i].vibratoDepth=arg0;
chan[i].vibratoRate=arg1;
sendPitch=true;
break;
case DIV_CMD_HINT_PORTA:
chan[i].portaTarget=arg0;
chan[i].portaSpeed=arg1;
break;
case DIV_CMD_HINT_LEGATO:
chan[i].note=arg0;
e->dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note));
break;
case DIV_CMD_HINT_ARPEGGIO:
chan[i].arp=(((unsigned char)arg0)<<4)|(arg1&15);
break;
case DIV_CMD_HINT_ARP_TIME:
arpSpeed=arg0;
break;
default: // dispatch it
e->dispatchCmd(DivCommand((DivDispatchCmds)command,i,arg0,arg1));
break;
}
}
chan[i].readPos=stream.tell();
}
if (sendVolume || chan[i].volSpeed!=0) {
chan[i].volume+=chan[i].volSpeed;
if (chan[i].volume<0) {
chan[i].volume=0;
}
if (chan[i].volume>chan[i].volMax) {
chan[i].volume=chan[i].volMax;
}
e->dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
}
if (sendPitch || chan[i].vibratoDepth!=0) {
if (chan[i].vibratoDepth>0) {
chan[i].vibratoPos+=chan[i].vibratoRate;
if (chan[i].vibratoPos>=64) chan[i].vibratoPos-=64;
}
e->dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(vibTable[chan[i].vibratoPos&63]*chan[i].vibratoDepth)/15));
}
if (chan[i].portaSpeed) {
e->dispatchCmd(DivCommand(DIV_CMD_NOTE_PORTA,i,chan[i].portaSpeed*(e->song.linearPitch==2?e->song.pitchSlideSpeed:1),chan[i].portaTarget));
}
if (chan[i].arp && !chan[i].portaSpeed) {
if (chan[i].arpTicks==0) {
switch (chan[i].arpStage) {
case 0:
e->dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note));
break;
case 1:
e->dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note+(chan[i].arp>>4)));
break;
case 2:
e->dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note+(chan[i].arp&15)));
break;
}
chan[i].arpStage++;
if (chan[i].arpStage>=3) chan[i].arpStage=0;
chan[i].arpTicks=arpSpeed;
}
chan[i].arpTicks--;
}
}
return ticked;
}
bool DivCSPlayer::init() {
unsigned char magic[4];
stream.seek(0,SEEK_SET);
stream.read(magic,4);
if (memcmp(magic,"FCS",4)!=0) return false;
unsigned int chans=stream.readI();
for (unsigned int i=0; i<chans; i++) {
if (i>=DIV_MAX_CHANS) {
stream.readI();
continue;
}
if ((int)i>=e->getTotalChannelCount()) {
stream.readI();
continue;
}
chan[i].readPos=stream.readI();
}
stream.read(fastDelays,16);
stream.read(fastCmds,16);
// initialize state
for (int i=0; i<e->getTotalChannelCount(); i++) {
chan[i].volMax=(e->getDispatch(e->dispatchOfChan[i])->dispatch(DivCommand(DIV_CMD_GET_VOLMAX,e->dispatchChanOfChan[i]))<<8)|0xff;
chan[i].volume=chan[i].volMax;
}
for (int i=0; i<64; i++) {
vibTable[i]=127*sin(((double)i/64.0)*(2*M_PI));
}
arpSpeed=1;
return true;
}
// DivEngine
bool DivEngine::playStream(unsigned char* f, size_t length) {
BUSY_BEGIN;
cmdStreamInt=new DivCSPlayer(this,f,length);
if (!cmdStreamInt->init()) {
logE("not a command stream!");
lastError="not a command stream";
delete[] f;
delete cmdStreamInt;
cmdStreamInt=NULL;
BUSY_END;
return false;
}
if (!playing) {
reset();
freelance=true;
playing=true;
}
BUSY_END;
return true;
}

89
src/engine/cmdStream.h Normal file
View File

@ -0,0 +1,89 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2023 tildearrow and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef _CMD_STREAM_H
#define _CMD_STREAM_H
#include "defines.h"
#include "safeReader.h"
class DivEngine;
struct DivCSChannelState {
unsigned int readPos;
int waitTicks;
int note, pitch;
int volume, volMax, volSpeed;
int vibratoDepth, vibratoRate, vibratoPos;
int portaTarget, portaSpeed;
unsigned char arp, arpStage, arpTicks;
unsigned int callStack[8];
unsigned char callStackPos;
struct TraceEntry {
unsigned int addr;
unsigned char length;
unsigned char data[11];
} trace[32];
unsigned char tracePos;
bool doCall(unsigned int addr);
DivCSChannelState():
readPos(0),
waitTicks(0),
note(-1),
pitch(0),
volume(0x7f00),
volMax(0),
volSpeed(0),
vibratoDepth(0),
vibratoRate(0),
vibratoPos(0),
portaTarget(0),
portaSpeed(0),
arp(0),
arpStage(0),
arpTicks(0),
callStackPos(0) {}
};
class DivCSPlayer {
DivEngine* e;
unsigned char* b;
SafeReader stream;
DivCSChannelState chan[DIV_MAX_CHANS];
unsigned char fastDelays[16];
unsigned char fastCmds[16];
unsigned char arpSpeed;
short vibTable[64];
public:
void cleanup();
bool tick();
bool init();
DivCSPlayer(DivEngine* en, unsigned char* buf, size_t len):
e(en),
b(buf),
stream(buf,len) {}
};
#endif

View File

@ -23,21 +23,54 @@
#include "../fileutils.h"
#include <fmt/printf.h>
bool DivConfig::save(const char* path) {
#define REDUNDANCY_NUM_ATTEMPTS 5
#define CHECK_BUF_SIZE 8192
bool DivConfig::save(const char* path, bool redundancy) {
if (redundancy) {
char oldPath[4096];
char newPath[4096];
if (fileExists(path)==1) {
logD("rotating config files...");
for (int i=4; i>=0; i--) {
if (i>0) {
snprintf(oldPath,4095,"%s.%d",path,i);
} else {
strncpy(oldPath,path,4095);
}
snprintf(newPath,4095,"%s.%d",path,i+1);
if (i>=4) {
logV("remove %s",oldPath);
deleteFile(oldPath);
} else {
logV("move %s to %s",oldPath,newPath);
moveFiles(oldPath,newPath);
}
}
}
}
logD("opening config for write: %s",path);
FILE* f=ps_fopen(path,"wb");
if (f==NULL) {
logW("could not write config file! %s",strerror(errno));
reportError(fmt::sprintf("could not write config file! %s",strerror(errno)));
return false;
}
for (auto& i: conf) {
String toWrite=fmt::sprintf("%s=%s\n",i.first,i.second);
if (fwrite(toWrite.c_str(),1,toWrite.size(),f)!=toWrite.size()) {
logW("could not write config file! %s",strerror(errno));
reportError(fmt::sprintf("could not write config file! %s",strerror(errno)));
logV("removing config file");
fclose(f);
deleteFile(path);
return false;
}
}
fclose(f);
logD("config file written successfully.");
return true;
}
@ -63,6 +96,7 @@ void DivConfig::parseLine(const char* line) {
String value="";
bool keyOrValue=false;
for (const char* i=line; *i; i++) {
if (*i=='\r') continue;
if (*i=='\n') continue;
if (keyOrValue) {
value+=*i;
@ -79,17 +113,94 @@ void DivConfig::parseLine(const char* line) {
}
}
bool DivConfig::loadFromFile(const char* path, bool createOnFail) {
bool DivConfig::loadFromFile(const char* path, bool createOnFail, bool redundancy) {
char line[4096];
FILE* f=ps_fopen(path,"rb");
if (f==NULL) {
if (createOnFail) {
logI("creating default config.");
return save(path);
} else {
return false;
logD("opening config for read: %s",path);
FILE* f=NULL;
if (redundancy) {
unsigned char* readBuf=new unsigned char[CHECK_BUF_SIZE];
size_t readBufLen=0;
for (int i=0; i<REDUNDANCY_NUM_ATTEMPTS; i++) {
bool viable=false;
if (i>0) {
snprintf(line,4095,"%s.%d",path,i);
} else {
strncpy(line,path,4095);
}
logV("trying: %s",line);
// try to open config
f=ps_fopen(line,"rb");
// check whether we could open it
if (f==NULL) {
logV("fopen(): %s",strerror(errno));
continue;
}
// check whether there's something
while (!feof(f)) {
readBufLen=fread(readBuf,1,CHECK_BUF_SIZE,f);
if (ferror(f)) {
logV("fread(): %s",strerror(errno));
break;
}
for (size_t j=0; j<readBufLen; j++) {
if (readBuf[j]!='\r' && readBuf[j]!='\n' && readBuf[j]!=' ') {
viable=true;
break;
}
}
if (viable) break;
}
// there's something
if (viable) {
if (fseek(f,0,SEEK_SET)==-1) {
logV("fseek(): %s",strerror(errno));
viable=false;
} else {
break;
}
}
// close it (because there's nothing)
fclose(f);
f=NULL;
}
delete[] readBuf;
// we couldn't read at all
if (f==NULL) {
logD("config does not exist");
if (createOnFail) {
logI("creating default config.");
//reportError(fmt::sprintf("Creating default config: %s",strerror(errno)));
return save(path,redundancy);
} else {
reportError(fmt::sprintf("COULD NOT LOAD CONFIG %s",strerror(errno)));
return false;
}
}
} else {
f=ps_fopen(path,"rb");
if (f==NULL) {
logD("config does not exist");
if (createOnFail) {
logI("creating default config.");
//reportError(fmt::sprintf("Creating default config: %s",strerror(errno)));
return save(path);
} else {
reportError(fmt::sprintf("COULD NOT LOAD CONFIG %s",strerror(errno)));
return false;
}
}
}
logI("loading config.");
while (!feof(f)) {
if (fgets(line,4095,f)==NULL) {
@ -97,6 +208,7 @@ bool DivConfig::loadFromFile(const char* path, bool createOnFail) {
}
parseLine(line);
}
logD("end of file (%s)",strerror(errno));
fclose(f);
return true;
}
@ -175,6 +287,33 @@ String DivConfig::getString(String key, String fallback) const {
return fallback;
}
std::vector<int> DivConfig::getIntList(String key, std::initializer_list<int> fallback) const {
String next;
std::vector<int> ret;
try {
String val=conf.at(key);
for (char i: val) {
if (i==',') {
int num=std::stoi(next);
ret.push_back(num);
next="";
} else {
next+=i;
}
}
if (!next.empty()) {
int num=std::stoi(next);
ret.push_back(num);
}
return ret;
} catch (std::out_of_range& e) {
} catch (std::invalid_argument& e) {
}
return fallback;
}
bool DivConfig::has(String key) const {
try {
String test=conf.at(key);
@ -212,6 +351,17 @@ void DivConfig::set(String key, String value) {
conf[key]=value;
}
void DivConfig::set(String key, const std::vector<int>& value) {
String val;
bool comma=false;
for (int i: value) {
if (comma) val+=',';
val+=fmt::sprintf("%d",i);
comma=true;
}
conf[key]=val;
}
bool DivConfig::remove(String key) {
return conf.erase(key);
}

View File

@ -22,6 +22,8 @@
#include "../ta-utils.h"
#include <map>
#include <vector>
#include <initializer_list>
class DivConfig {
std::map<String,String> conf;
@ -30,10 +32,10 @@ class DivConfig {
// config loading/saving
bool loadFromMemory(const char* buf);
bool loadFromBase64(const char* buf);
bool loadFromFile(const char* path, bool createOnFail=true);
bool loadFromFile(const char* path, bool createOnFail=true, bool redundancy=false);
String toString();
String toBase64();
bool save(const char* path);
bool save(const char* path, bool redundancy=false);
// get the map
const std::map<String,String>& configMap();
@ -44,6 +46,7 @@ class DivConfig {
float getFloat(String key, float fallback) const;
double getDouble(String key, double fallback) const;
String getString(String key, String fallback) const;
std::vector<int> getIntList(String key, std::initializer_list<int> fallback) const;
// check for existence
bool has(String key) const;
@ -55,6 +58,7 @@ class DivConfig {
void set(String key, double value);
void set(String key, const char* value);
void set(String key, String value);
void set(String key, const std::vector<int>& value);
// remove a config value
bool remove(String key);

View File

@ -110,12 +110,12 @@ void DivEngine::initConfDir() {
bool DivEngine::saveConf() {
configFile=configPath+String(CONFIG_FILE);
return conf.save(configFile.c_str());
return conf.save(configFile.c_str(),true);
}
bool DivEngine::loadConf() {
configFile=configPath+String(CONFIG_FILE);
return conf.loadFromFile(configFile.c_str());
return conf.loadFromFile(configFile.c_str(),true,true);
}
bool DivEngine::getConfBool(String key, bool fallback) {

View File

@ -229,6 +229,13 @@ enum DivDispatchCmds {
DIV_CMD_ES5506_ENVELOPE_K2RAMP, // (ramp, slowdown)
DIV_CMD_ES5506_PAUSE, // (value)
DIV_CMD_HINT_ARP_TIME, // (value)
DIV_CMD_SNES_GLOBAL_VOL_LEFT,
DIV_CMD_SNES_GLOBAL_VOL_RIGHT,
DIV_CMD_NES_LINEAR_LENGTH,
DIV_ALWAYS_SET_VOLUME, // () -> alwaysSetVol
DIV_CMD_MAX
@ -280,19 +287,31 @@ struct DivRegWrite {
* - 0xffffffff: reset
*/
unsigned int addr;
unsigned short val;
DivRegWrite(unsigned int a, unsigned short v):
unsigned int val;
DivRegWrite(unsigned int a, unsigned int v):
addr(a), val(v) {}
};
struct DivDelayedWrite {
int time;
DivRegWrite write;
DivDelayedWrite(int t, unsigned int a, unsigned short v):
DivDelayedWrite(int t, unsigned int a, unsigned int v):
time(t),
write(a,v) {}
};
struct DivSamplePos {
int sample, pos, freq;
DivSamplePos(int s, int p, int f):
sample(s),
pos(p),
freq(f) {}
DivSamplePos():
sample(-1),
pos(0),
freq(0) {}
};
struct DivDispatchOscBuffer {
bool follow;
unsigned int rate;
@ -371,18 +390,29 @@ class DivDispatch {
/**
* get the state of a channel.
* @param chan the channel.
* @return a pointer, or NULL.
*/
virtual void* getChanState(int chan);
/**
* get the DivMacroInt of a chanmel.
* get the DivMacroInt of a channel.
* @param chan the channel.
* @return a pointer, or NULL.
*/
virtual DivMacroInt* getChanMacroInt(int chan);
/**
* get currently playing sample (and its position).
* @param chan the channel.
* @return a DivSamplePos. if sample is -1 then nothing is playing or the
* channel doesn't play samples.
*/
virtual DivSamplePos getSamplePos(int chan);
/**
* get an oscilloscope buffer for a channel.
* @param chan the channel.
* @return a pointer to a DivDispatchOscBuffer, or NULL if not supported.
*/
virtual DivDispatchOscBuffer* getOscBuffer(int chan);

View File

@ -59,6 +59,7 @@
#include "platform/lynx.h"
#include "platform/pokey.h"
#include "platform/zxbeeper.h"
#include "platform/zxbeeperquadtone.h"
#include "platform/bubsyswsg.h"
#include "platform/n163.h"
#include "platform/pet.h"
@ -76,6 +77,7 @@
#include "platform/k007232.h"
#include "platform/ga20.h"
#include "platform/sm8521.h"
#include "platform/pv1000.h"
#include "platform/pcmdac.h"
#include "platform/dummy.h"
#include "../ta-log.h"
@ -388,6 +390,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do
case DIV_SYSTEM_SFX_BEEPER:
dispatch=new DivPlatformZXBeeper;
break;
case DIV_SYSTEM_SFX_BEEPER_QUADTONE:
dispatch=new DivPlatformZXBeeperQuadTone;
break;
case DIV_SYSTEM_LYNX:
dispatch=new DivPlatformLynx;
break;
@ -493,6 +498,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do
case DIV_SYSTEM_SM8521:
dispatch=new DivPlatformSM8521;
break;
case DIV_SYSTEM_PV1000:
dispatch=new DivPlatformPV1000;
break;
case DIV_SYSTEM_PCM_DAC:
dispatch=new DivPlatformPCMDAC;
break;

View File

@ -55,6 +55,10 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNul
return "03xx: Portamento";
case 0x04:
return "04xy: Vibrato (x: speed; y: depth)";
case 0x05:
return "05xy: Volume slide + vibrato (compatibility only!)";
case 0x06:
return "06xy: Volume slide + portamento (compatibility only!)";
case 0x07:
return "07xy: Tremolo (x: speed; y: depth)";
case 0x08:
@ -418,6 +422,7 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) {
case DIV_CMD_AMIGA_PM:
case DIV_CMD_MACRO_OFF:
case DIV_CMD_MACRO_ON:
case DIV_CMD_HINT_ARP_TIME:
w->writeC(1); // length
w->writeC(c.value);
break;
@ -1481,6 +1486,7 @@ void DivEngine::createNewFromDefaults() {
bool oldVol=getConfInt("configVersion",DIV_ENGINE_VERSION)<135;
if (preset.empty()) {
// try loading old preset
logD("trying to load old preset");
preset=decodeSysDesc(getConfString("initialSys",""));
oldVol=false;
}
@ -1566,6 +1572,49 @@ void DivEngine::changeSong(size_t songIndex) {
prevRow=0;
}
void DivEngine::checkAssetDir(std::vector<DivAssetDir>& dir, size_t entries) {
bool* inAssetDir=new bool[entries];
memset(inAssetDir,0,entries*sizeof(bool));
for (DivAssetDir& i: dir) {
for (size_t j=0; j<i.entries.size(); j++) {
// erase invalid entry
if (i.entries[j]<0 || i.entries[j]>=(int)entries) {
i.entries.erase(i.entries.begin()+j);
j--;
continue;
}
// mark entry as present
inAssetDir[j]=true;
}
}
// get unsorted directory
DivAssetDir* unsortedDir=NULL;
for (DivAssetDir& i: dir) {
if (i.name.empty()) {
unsortedDir=&i;
break;
}
}
// create unsorted directory if it doesn't exist
if (unsortedDir==NULL) {
dir.push_back(DivAssetDir(""));
unsortedDir=&(*dir.rbegin());
}
// add missing items to unsorted directory
for (size_t i=0; i<entries; i++) {
if (!inAssetDir[i]) {
unsortedDir->entries.push_back(i);
}
}
delete[] inAssetDir;
}
void DivEngine::swapChannelsP(int src, int dest) {
if (src<0 || src>=chans) return;
if (dest<0 || dest>=chans) return;
@ -1985,11 +2034,17 @@ String DivEngine::getPlaybackDebugInfo() {
"divider: %f\n"
"cycles: %d\n"
"clockDrift: %f\n"
"midiClockCycles: %d\n"
"midiClockDrift: %f\n"
"midiTimeCycles: %d\n"
"midiTimeDrift: %f\n"
"changeOrd: %d\n"
"changePos: %d\n"
"totalSeconds: %d\n"
"totalTicks: %d\n"
"totalTicksR: %d\n"
"curMidiClock: %d\n"
"curMidiTime: %d\n"
"totalCmds: %d\n"
"lastCmds: %d\n"
"cmdsPerSecond: %d\n"
@ -1999,7 +2054,8 @@ String DivEngine::getPlaybackDebugInfo() {
"totalProcessed: %d\n"
"bufferPos: %d\n",
curOrder,prevOrder,curRow,prevRow,ticks,subticks,totalLoops,lastLoopPos,nextSpeed,divider,cycles,clockDrift,
changeOrd,changePos,totalSeconds,totalTicks,totalTicksR,totalCmds,lastCmds,cmdsPerSecond,globalPitch,
midiClockCycles,midiClockDrift,midiTimeCycles,midiTimeDrift,changeOrd,changePos,totalSeconds,totalTicks,
totalTicksR,curMidiClock,curMidiTime,totalCmds,lastCmds,cmdsPerSecond,globalPitch,
(int)extValue,(int)tempoAccum,(int)totalProcessed,(int)bufferPos
);
}
@ -2075,6 +2131,11 @@ DivMacroInt* DivEngine::getMacroInt(int chan) {
return disCont[dispatchOfChan[chan]].dispatch->getChanMacroInt(dispatchChanOfChan[chan]);
}
DivSamplePos DivEngine::getSamplePos(int chan) {
if (chan<0 || chan>=chans) return DivSamplePos();
return disCont[dispatchOfChan[chan]].dispatch->getSamplePos(dispatchChanOfChan[chan]);
}
DivDispatchOscBuffer* DivEngine::getOscBuffer(int chan) {
if (chan<0 || chan>=chans) return NULL;
return disCont[dispatchOfChan[chan]].dispatch->getOscBuffer(dispatchChanOfChan[chan]);
@ -2111,16 +2172,26 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) {
prevOrder=0;
prevRow=0;
stepPlay=0;
int prevDrift;
int prevDrift, prevMidiClockDrift, prevMidiTimeDrift;
prevDrift=clockDrift;
prevMidiClockDrift=midiClockDrift;
prevMidiTimeDrift=midiTimeDrift;
clockDrift=0;
cycles=0;
midiClockCycles=0;
midiClockDrift=0;
midiTimeCycles=0;
midiTimeDrift=0;
if (!preserveDrift) {
ticks=1;
tempoAccum=0;
totalTicks=0;
totalSeconds=0;
totalTicksR=0;
curMidiClock=0;
curMidiTime=0;
curMidiTimeCode=0;
curMidiTimePiece=0;
totalLoops=0;
lastLoopPos=-1;
}
@ -2137,6 +2208,10 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) {
skipping=false;
return;
}
if (!preserveDrift) {
runMidiClock(cycles);
runMidiTime(cycles);
}
}
int oldOrder=curOrder;
while (playing && (curRow<goalRow || ticks>1)) {
@ -2144,6 +2219,10 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) {
skipping=false;
return;
}
if (!preserveDrift) {
runMidiClock(cycles);
runMidiTime(cycles);
}
if (oldOrder!=curOrder) break;
if (ticks-((tempoAccum+curSubSong->virtualTempoN)/MAX(1,curSubSong->virtualTempoD))<1 && curRow>=goalRow) break;
}
@ -2157,9 +2236,22 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) {
repeatPattern=oldRepeatPattern;
if (preserveDrift) {
clockDrift=prevDrift;
midiClockDrift=prevMidiClockDrift;
midiTimeDrift=prevMidiTimeDrift;
} else {
clockDrift=0;
cycles=0;
midiClockCycles=0;
midiClockDrift=0;
midiTimeCycles=0;
midiTimeDrift=0;
if (curMidiTime>0) {
curMidiTime--;
}
if (curMidiClock>0) {
curMidiClock--;
}
curMidiTimePiece=0;
}
if (!preserveDrift) {
ticks=1;
@ -2328,9 +2420,74 @@ void DivEngine::play() {
for (int i=0; i<DIV_MAX_CHANS; i++) {
keyHit[i]=false;
}
curMidiTimePiece=0;
if (output) if (!skipping && output->midiOut!=NULL) {
int pos=totalTicksR/6;
output->midiOut->send(TAMidiMessage(TA_MIDI_POSITION,(pos>>7)&0x7f,pos&0x7f));
if (midiOutClock) {
output->midiOut->send(TAMidiMessage(TA_MIDI_POSITION,(curMidiClock>>7)&0x7f,curMidiClock&0x7f));
}
if (midiOutTime) {
TAMidiMessage msg;
msg.type=TA_MIDI_SYSEX;
msg.sysExData.reset(new unsigned char[10],std::default_delete<unsigned char[]>());
msg.sysExLen=10;
unsigned char* msgData=msg.sysExData.get();
int actualTime=curMidiTime;
int timeRate=midiOutTimeRate;
int drop=0;
if (timeRate<1 || timeRate>4) {
if (curSubSong->hz>=47.98 && curSubSong->hz<=48.02) {
timeRate=1;
} else if (curSubSong->hz>=49.98 && curSubSong->hz<=50.02) {
timeRate=2;
} else if (curSubSong->hz>=59.9 && curSubSong->hz<=60.11) {
timeRate=4;
} else {
timeRate=4;
}
}
switch (timeRate) {
case 1: // 24
msgData[5]=(actualTime/(60*60*24))%24;
msgData[6]=(actualTime/(60*24))%60;
msgData[7]=(actualTime/24)%60;
msgData[8]=actualTime%24;
break;
case 2: // 25
msgData[5]=(actualTime/(60*60*25))%24;
msgData[6]=(actualTime/(60*25))%60;
msgData[7]=(actualTime/25)%60;
msgData[8]=actualTime%25;
break;
case 3: // 29.97 (NTSC drop)
// drop
drop=((actualTime/(30*60))-(actualTime/(30*600)))*2;
actualTime+=drop;
msgData[5]=(actualTime/(60*60*30))%24;
msgData[6]=(actualTime/(60*30))%60;
msgData[7]=(actualTime/30)%60;
msgData[8]=actualTime%30;
break;
case 4: // 30 (NTSC non-drop)
default:
msgData[5]=(actualTime/(60*60*30))%24;
msgData[6]=(actualTime/(60*30))%60;
msgData[7]=(actualTime/30)%60;
msgData[8]=actualTime%30;
break;
}
msgData[5]|=(timeRate-1)<<5;
msgData[0]=0xf0;
msgData[1]=0x7f;
msgData[2]=0x7f;
msgData[3]=0x01;
msgData[4]=0x01;
msgData[9]=0xf7;
output->midiOut->send(msg);
}
output->midiOut->send(TAMidiMessage(TA_MIDI_MACHINE_PLAY,0,0));
}
BUSY_END;
@ -2369,6 +2526,13 @@ void DivEngine::stepOne(int row) {
void DivEngine::stop() {
BUSY_BEGIN;
freelance=false;
if (!playing) {
//Send midi panic
if (output) if (output->midiOut!=NULL) {
output->midiOut->send(TAMidiMessage(TA_MIDI_CONTROL,0x7B,0));
logV("Midi panic sent");
}
}
playing=false;
extValuePresent=false;
endOfSong=false; // what?
@ -2391,6 +2555,16 @@ void DivEngine::stop() {
}
}
}
// reset all chan oscs
for (int i=0; i<chans; i++) {
DivDispatchOscBuffer* buf=disCont[dispatchOfChan[i]].dispatch->getOscBuffer(dispatchChanOfChan[i]);
if (buf!=NULL) {
memset(buf->data,0,65536*sizeof(short));
buf->needle=0;
buf->readNeedle=0;
}
}
BUSY_END;
}
@ -2454,6 +2628,10 @@ void DivEngine::recalcChans() {
if (isInsTypePossible[i]) possibleInsTypes.push_back((DivInstrumentType)i);
}
checkAssetDir(song.insDir,song.ins.size());
checkAssetDir(song.waveDir,song.wave.size());
checkAssetDir(song.sampleDir,song.sample.size());
hasLoadedSomething=true;
}
@ -2532,6 +2710,10 @@ int DivEngine::divToFileRate(int drate) {
return 4;
}
void DivEngine::testFunction() {
logI("it works!");
}
int DivEngine::getEffectiveSampleRate(int rate) {
if (rate<1) return 0;
switch (song.system[0]) {
@ -2600,10 +2782,17 @@ void DivEngine::previewSampleNoLock(int sample, int note, int pStart, int pEnd)
if (rate<=0) rate=song.sample[sample]->centerRate;
}
if (rate<100) rate=100;
double rateOrig=rate;
sPreview.rateMul=1;
while (sPreview.rateMul<0x40000000 && rate<got.rate) {
sPreview.rateMul<<=1;
rate*=2.0;
}
blip_set_rates(samp_bb,rate,got.rate);
samp_prevSample=0;
sPreview.rate=rate;
sPreview.rate=rateOrig;
sPreview.pos=(sPreview.pBegin>=0)?sPreview.pBegin:0;
sPreview.posSub=0;
sPreview.sample=sample;
sPreview.wave=-1;
sPreview.dir=false;
@ -2628,10 +2817,17 @@ void DivEngine::previewWaveNoLock(int wave, int note) {
blip_clear(samp_bb);
double rate=song.wave[wave]->len*((song.tuning*0.0625)*pow(2.0,(double)(note+3)/12.0));
if (rate<100) rate=100;
double rateOrig=rate;
sPreview.rateMul=1;
while (sPreview.rateMul<0x40000000 && rate<got.rate) {
sPreview.rateMul<<=1;
rate*=2.0;
}
blip_set_rates(samp_bb,rate,got.rate);
samp_prevSample=0;
sPreview.rate=rate;
sPreview.rate=rateOrig;
sPreview.pos=0;
sPreview.posSub=0;
sPreview.sample=-1;
sPreview.wave=wave;
sPreview.dir=false;
@ -2997,15 +3193,17 @@ DivWavetable* DivEngine::waveFromFile(const char* path, bool addRaw) {
// read as .dmw
reader.seek(0,SEEK_SET);
int len=reader.readI();
logD("wave length %d",len);
if (len<=0 || len>256) {
throw EndOfFileException(&reader,reader.size());
}
wave->len=len;
wave->max=(unsigned char)reader.readC();
if (wave->max==255) { // new wavetable format
unsigned char waveVersion=reader.readC();
logI("reading modern .dmw...");
logD("wave version %d",waveVersion);
wave->max=reader.readC();
wave->max=(unsigned char)reader.readC();
for (int i=0; i<len; i++) {
wave->data[i]=reader.readI();
}
@ -3624,7 +3822,8 @@ void DivEngine::addOrder(int pos, bool duplicate, bool where) {
}
curSubSong->ordersLen++;
saveLock.unlock();
if (pos<=curOrder) curOrder++;
curOrder=pos+1;
prevOrder=curOrder;
if (playing && !freelance) {
playSub(false);
}
@ -4271,6 +4470,10 @@ void DivEngine::quitDispatch() {
}
cycles=0;
clockDrift=0;
midiClockCycles=0;
midiClockDrift=0;
midiTimeCycles=0;
midiTimeDrift=0;
chans=0;
playing=false;
curSpeed=0;
@ -4287,6 +4490,10 @@ void DivEngine::quitDispatch() {
totalTicks=0;
totalSeconds=0;
totalTicksR=0;
curMidiClock=0;
curMidiTime=0;
curMidiTimeCode=0;
curMidiTimePiece=0;
totalCmds=0;
lastCmds=0;
cmdsPerSecond=0;
@ -4313,6 +4520,9 @@ bool DivEngine::initAudioBackend() {
lowLatency=getConfInt("lowLatency",0);
metroVol=(float)(getConfInt("metroVol",100))/100.0f;
midiOutClock=getConfInt("midiOutClock",0);
midiOutTime=getConfInt("midiOutTime",0);
midiOutTimeRate=getConfInt("midiOutTimeRate",0);
midiOutProgramChange = getConfInt("midiOutProgramChange",0);
midiOutMode=getConfInt("midiOutMode",DIV_MIDI_MODE_NOTE);
if (metroVol<0.0f) metroVol=0.0f;
if (metroVol>2.0f) metroVol=2.0f;
@ -4468,6 +4678,7 @@ bool DivEngine::init() {
bool oldVol=getConfInt("configVersion",DIV_ENGINE_VERSION)<135;
if (preset.empty()) {
// try loading old preset
logD("trying to load old preset");
preset=decodeSysDesc(getConfString("initialSys",""));
oldVol=false;
}

Some files were not shown because too many files have changed in this diff Show More