diff --git a/CMakeLists.txt b/CMakeLists.txt index 9edfc252..4feccdf8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -234,6 +234,8 @@ src/engine/platform/sound/gb/apu.c src/engine/platform/sound/gb/timing.c src/engine/platform/sound/pce_psg.cpp src/engine/platform/sound/nes/apu.c +src/engine/platform/sound/vera_psg.c +src/engine/platform/sound/vera_pcm.c src/engine/platform/sound/c64/sid.cc src/engine/platform/sound/c64/voice.cc @@ -265,6 +267,8 @@ src/engine/platform/sound/lynx/Mikey.cpp src/engine/platform/sound/qsound.c +src/engine/platform/sound/x1_010/x1_010.cpp + src/engine/platform/sound/swan.cpp src/engine/platform/ym2610Interface.cpp @@ -308,8 +312,10 @@ src/engine/platform/amiga.cpp src/engine/platform/pcspkr.cpp src/engine/platform/segapcm.cpp src/engine/platform/qsound.cpp +src/engine/platform/x1_010.cpp src/engine/platform/lynx.cpp src/engine/platform/swan.cpp +src/engine/platform/vera.cpp src/engine/platform/dummy.cpp ) @@ -341,6 +347,7 @@ src/gui/font_unifont.cpp src/gui/font_icon.cpp src/gui/fonts.cpp src/gui/debug.cpp +src/gui/fileDialog.cpp src/gui/intConst.cpp src/gui/guiConst.cpp @@ -392,6 +399,9 @@ endif() if (NOT MSVC) set(WARNING_FLAGS -Wall -Wextra -Wno-unused-parameter) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + list(APPEND WARNING_FLAGS -Wno-cast-function-type) + endif() if (WARNINGS_ARE_ERRORS) list(APPEND WARNING_FLAGS -Werror) endif() diff --git a/README.md b/README.md index e1d97726..a1b64854 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ some people have provided packages for Unix/Unix-like distributions. here's a li # developer info +[![Build furnace](https://github.com/tildearrow/furnace/actions/workflows/build.yml/badge.svg)](https://github.com/tildearrow/furnace/actions/workflows/build.yml) + **NOTE: do not download the project's source as a .zip or .tar.gz as these do not include the project's submodules which are necessary to proceed with building.** ## dependencies diff --git a/extern/igfd/ImGuiFileDialog.cpp b/extern/igfd/ImGuiFileDialog.cpp index 5be45793..3980bcd3 100644 --- a/extern/igfd/ImGuiFileDialog.cpp +++ b/extern/igfd/ImGuiFileDialog.cpp @@ -3877,6 +3877,7 @@ namespace IGFD static ImGuiSelectableFlags selectableFlags = ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SpanAvailWidth; + // TODO BUG?! va_list args; va_start(args, vFmt); vsnprintf(fdi.puVariadicBuffer, MAX_FILE_DIALOG_NAME_BUFFER, vFmt, args); @@ -4074,6 +4075,7 @@ namespace IGFD if (ImGui::TableNextColumn()) // file name { + // TODO BUG?!?!?! needToBreakTheloop = prSelectableItem(i, infos, selected, _str.c_str()); if (needToBreakTheloop==2) escape=true; } diff --git a/extern/pfd-fixed/.gitignore b/extern/pfd-fixed/.gitignore new file mode 100644 index 00000000..ec121a99 --- /dev/null +++ b/extern/pfd-fixed/.gitignore @@ -0,0 +1,4 @@ +CMakeCache.txt +CMakeFiles +Makefile +cmake_install.cmake diff --git a/extern/pfd-fixed/.lgtm.yml b/extern/pfd-fixed/.lgtm.yml new file mode 100644 index 00000000..b4c4985b --- /dev/null +++ b/extern/pfd-fixed/.lgtm.yml @@ -0,0 +1,5 @@ +extraction: + cpp: + index: + build_command: + - make -C examples diff --git a/extern/pfd-fixed/CMakeLists.txt b/extern/pfd-fixed/CMakeLists.txt new file mode 100644 index 00000000..3be61ae5 --- /dev/null +++ b/extern/pfd-fixed/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.1.0) + +project(portable_file_dialogs VERSION 1.00 LANGUAGES CXX) + +add_library(${PROJECT_NAME} INTERFACE) +target_include_directories(${PROJECT_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) \ No newline at end of file diff --git a/extern/pfd-fixed/COPYING b/extern/pfd-fixed/COPYING new file mode 100644 index 00000000..8b014d64 --- /dev/null +++ b/extern/pfd-fixed/COPYING @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/extern/pfd-fixed/README.md b/extern/pfd-fixed/README.md new file mode 100644 index 00000000..ceb36a40 --- /dev/null +++ b/extern/pfd-fixed/README.md @@ -0,0 +1,64 @@ +# Portable File Dialogs + +A free C++11 file dialog library. + +- works on Windows, Mac OS X, Linux +- **single-header**, no extra library dependencies +- **synchronous *or* asynchronous** (does not block the rest of your program!) +- **cancelable** (kill asynchronous dialogues without user interaction) +- **secure** (immune to shell-quote vulnerabilities) + +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/a25d3fd6959a4333871f630ac70b6e09)](https://www.codacy.com/manual/samhocevar/portable-file-dialogs?utm_source=github.com&utm_medium=referral&utm_content=samhocevar/portable-file-dialogs&utm_campaign=Badge_Grade) + +## Status + +The library is now pretty robust. It is not as feature-complete as +[Tiny File Dialogs](https://sourceforge.net/projects/tinyfiledialogs/), +but has asynchonous dialogs, more maintainable code, and fewer potential +security issues. + +The currently available backends are: + +- Win32 API (all known versions of Windows) +- Mac OS X (using AppleScript) +- GNOME desktop (using [Zenity](https://en.wikipedia.org/wiki/Zenity) or its clones Matedialog and Qarma) +- KDE desktop (using [KDialog](https://github.com/KDE/kdialog)) + +Experimental support for Emscripten is on its way. + +## Documentation + +- [`pfd`](doc/pfd.md) general documentation +- [`pfd::message`](doc/message.md) message box +- [`pfd::notify`](doc/notify.md) notification +- [`pfd::open_file`](doc/open_file.md) file open +- [`pfd::save_file`](doc/save_file.md) file save +- [`pfd::select_folder`](doc/select_folder.md) folder selection + +## History + +- 0.1.0 (July 16, 2020): first public release + +## Screenshots (Windows 10) + +![warning-win32](https://user-images.githubusercontent.com/245089/47136607-76919a00-d2b4-11e8-8f42-e2d62c4f9570.png) +![notify-win32](https://user-images.githubusercontent.com/245089/47142453-2ff76c00-d2c3-11e8-871a-1a110ac91eb2.png) +![open-win32](https://user-images.githubusercontent.com/245089/47155865-0f8cd900-d2e6-11e8-8041-1e20b6f77dee.png) + +## Screenshots (Mac OS X, dark theme) + +![warning-osxdark](https://user-images.githubusercontent.com/245089/56053001-22dba700-5d53-11e9-8233-ca7a2c58188d.png) +![notify-osxdark](https://user-images.githubusercontent.com/245089/56053188-bc0abd80-5d53-11e9-8298-68aa96315c6c.png) +![open-osxdark](https://user-images.githubusercontent.com/245089/56053378-39363280-5d54-11e9-9583-9f1c978fa0db.png) + +## Screenshots (Linux, GNOME desktop) + +![warning-gnome](https://user-images.githubusercontent.com/245089/47136608-772a3080-d2b4-11e8-9e1d-60a7e743e908.png) +![notify-gnome](https://user-images.githubusercontent.com/245089/47142455-30900280-d2c3-11e8-8b76-ea16c7e502d4.png) +![open-gnome](https://user-images.githubusercontent.com/245089/47155867-0f8cd900-d2e6-11e8-93af-275636491ec4.png) + +## Screenshots (Linux, KDE Plasma desktop) + +![warning-kde](https://user-images.githubusercontent.com/245089/47149255-4dcccd00-d2d3-11e8-84c9-f85612784680.png) +![notify-kde](https://user-images.githubusercontent.com/245089/47149206-27a72d00-d2d3-11e8-8f1b-96e462f08c2b.png) +![open-kde](https://user-images.githubusercontent.com/245089/47155866-0f8cd900-d2e6-11e8-8006-f14b948afc55.png) diff --git a/extern/pfd-fixed/doc/message.md b/extern/pfd-fixed/doc/message.md new file mode 100644 index 00000000..e54c4477 --- /dev/null +++ b/extern/pfd-fixed/doc/message.md @@ -0,0 +1,97 @@ +## Message Box API + +Displaying a message box is done using the `pfd::message` class. It can be provided a title, a +message text, a `choice` representing which buttons need to be rendered, and an `icon` for the +message: + +```cpp +pfd::message::message(std::string const &title, + std::string const &text, + pfd::choice choice = pfd::choice::ok_cancel, + pfd::icon icon = pfd::icon::info); + +enum class pfd::choice { ok, ok_cancel, yes_no, yes_no_cancel }; + +enum class pfd::icon { info, warning, error, question }; +``` + +The pressed button is queried using `pfd::message::result()`. If the dialog box is closed by any +other means, the `pfd::button::cancel` is assumed: + +```cpp +pfd::button pfd::message::result(); + +enum class pfd::button { ok, cancel, yes, no }; +``` + +It is possible to ask the dialog box whether the user took action using the `pfd::message::ready()` +method, with an optional `timeout` argument. If the user did not press a button within `timeout` +milliseconds, the function will return `false`: + +```cpp +bool pfd::message::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple notification + +The `pfd::message` destructor waits for user action, so this operation will block until the user +closes the message box: + +```cpp +pfd::message("Problem", "An error occurred while doing things", + pfd::choice::ok, pfd::icon::error); +``` + +## Example 2: retrieving the pressed button + +Using `pfd::message::result()` will also wait for user action before returning. This operation will block and return the user choice: + +```cpp +// Ask for user opinion +auto button = pfd::message("Action requested", "Do you want to proceed with things?", + pfd::choice::yes_no, pfd::icon::question).result(); +// Do something with button… +``` + +## Example 3: asynchronous message box + +Using `pfd::message::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// Message box with nice message +auto box = pfd::message("Unsaved Files", "Do you want to save the current " + "document before closing the application?", + pfd::choice::yes_no_cancel, + pfd::icon::warning); + +// Do something while waiting for user input +while (!box.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the selected button +switch (box.result()) +{ + case pfd::button::yes: std::cout << "User agreed.\n"; break; + case pfd::button::no: std::cout << "User disagreed.\n"; break; + case pfd::button::cancel: std::cout << "User freaked out.\n"; break; +} +``` + +## Screenshots + +#### Windows 10 + +![warning-win32](https://user-images.githubusercontent.com/245089/47136607-76919a00-d2b4-11e8-8f42-e2d62c4f9570.png) + +#### Mac OS X + +![warning-osx-dark](https://user-images.githubusercontent.com/245089/56053001-22dba700-5d53-11e9-8233-ca7a2c58188d.png) ![warning-osx-light](https://user-images.githubusercontent.com/245089/56053055-49014700-5d53-11e9-8306-e9a03a25e044.png) + +#### Linux (GNOME desktop) + +![warning-gnome](https://user-images.githubusercontent.com/245089/47140824-8662ab80-d2bf-11e8-9c87-2742dd5b58af.png) + +#### Linux (KDE desktop) + +![warning-kde](https://user-images.githubusercontent.com/245089/47149255-4dcccd00-d2d3-11e8-84c9-f85612784680.png) diff --git a/extern/pfd-fixed/doc/notify.md b/extern/pfd-fixed/doc/notify.md new file mode 100644 index 00000000..b140e261 --- /dev/null +++ b/extern/pfd-fixed/doc/notify.md @@ -0,0 +1,40 @@ +## Notification API + +Displaying a desktop notification is done using the `pfd::notify` class. It can be provided a +title, a message text, and an `icon` for the notification style: + +```cpp +pfd::notify::notify(std::string const &title, + std::string const &text, + pfd::icon icon = pfd::icon::info); + +enum class pfd::icon { info, warning, error }; +``` + +## Example + +Displaying a notification is straightforward. Emoji are supported: + +```cpp +pfd::notify("System event", "Something might be on fire 🔥", + pfd::icon::warning); +``` + +The `pfd::notify` object needs not be kept around, letting the object clean up itself is enough. + +## Screenshots + +Windows 10: +![notify-win32](https://user-images.githubusercontent.com/245089/47142453-2ff76c00-d2c3-11e8-871a-1a110ac91eb2.png) + +Mac OS X (dark theme): +![image](https://user-images.githubusercontent.com/245089/56053188-bc0abd80-5d53-11e9-8298-68aa96315c6c.png) + +Mac OS X (light theme): +![image](https://user-images.githubusercontent.com/245089/56053137-92ea2d00-5d53-11e9-8cf2-049486c45713.png) + +Linux (GNOME desktop): +![notify-gnome](https://user-images.githubusercontent.com/245089/47142455-30900280-d2c3-11e8-8b76-ea16c7e502d4.png) + +Linux (KDE desktop): +![notify-kde](https://user-images.githubusercontent.com/245089/47149206-27a72d00-d2d3-11e8-8f1b-96e462f08c2b.png) diff --git a/extern/pfd-fixed/doc/open_file.md b/extern/pfd-fixed/doc/open_file.md new file mode 100644 index 00000000..db3158ef --- /dev/null +++ b/extern/pfd-fixed/doc/open_file.md @@ -0,0 +1,90 @@ +## File Open API + +The `pfd::open_file` class handles file opening dialogs. It can be provided a title, a starting +directory and/or pre-selected file, an optional filter for recognised file types, and an optional +flag to allow multiple selection: + +```cpp +pfd::open_file::open_file(std::string const &title, + std::string const &initial_path, + std::vector filters = { "All Files", "*" }, + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::multiselect` to allow selecting multiple files. + +The selected files are queried using `pfd::open_file::result()`. If the user canceled the +operation, the returned list is empty: + +```cpp +std::vector pfd::open_file::result(); +``` + +It is possible to ask the file open dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::open_file::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple file selection + +Using `pfd::open_file::result()` will wait for user action before returning. This operation will +block and return the user choice: + +```cpp +auto selection = pfd::open_file("Select a file").result(); +if (!selection.empty()) + std::cout << "User selected file " << selection[0] << "\n"; +``` + +## Example 2: filters + +The filter list enumerates filter names and corresponded space-separated wildcard lists. It +defaults to `{ "All Files", "*" }`, but here is how to use other options: + +```cpp +auto selection = pfd::open_file("Select a file", ".", + { "Image Files", "*.png *.jpg *.jpeg *.bmp", + "Audio Files", "*.wav *.mp3", + "All Files", "*" }, + pfd::opt::multiselect).result(); +// Do something with selection +for (auto const &filename : dialog.result()) + std::cout << "Selected file: " << filename << "\n"; +``` + +## Example 3: asynchronous file open + +Using `pfd::open_file::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// File open dialog +auto dialog = pfd::open_file("Select file to open"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "Number of selected files: " << dialog.result().size() << "\n"; +``` + +## Screenshots + +Windows 10: +![open-win32](https://user-images.githubusercontent.com/245089/47155865-0f8cd900-d2e6-11e8-8041-1e20b6f77dee.png) + +Mac OS X (dark theme): +![image](https://user-images.githubusercontent.com/245089/56053378-39363280-5d54-11e9-9583-9f1c978fa0db.png) + +Mac OS X (light theme): +![image](https://user-images.githubusercontent.com/245089/56053413-4fdc8980-5d54-11e9-85e3-e9e5d0e10772.png) + +Linux (GNOME desktop): +![open-gnome](https://user-images.githubusercontent.com/245089/47155867-0f8cd900-d2e6-11e8-93af-275636491ec4.png) + +Linux (KDE desktop): +![open-kde](https://user-images.githubusercontent.com/245089/47155866-0f8cd900-d2e6-11e8-8006-f14b948afc55.png) diff --git a/extern/pfd-fixed/doc/pfd.md b/extern/pfd-fixed/doc/pfd.md new file mode 100644 index 00000000..f62799ef --- /dev/null +++ b/extern/pfd-fixed/doc/pfd.md @@ -0,0 +1,120 @@ +## Portable File Dialogs documentation + +The library can be used either as a [header-only library](https://en.wikipedia.org/wiki/Header-only), +or as a [single file library](https://github.com/nothings/single_file_libs). + +### Use as header-only library + +Just include the main header file wherever needed: + +```cpp +#include "portable-file-dialogs.h" + +/* ... */ + + pfd::message::message("Hello", "This is a test"); + +/* ... */ +``` + +### Use as a single-file library + +Defining the `PFD_SKIP_IMPLEMENTATION` macro before including `portable-file-dialogs.h` will +skip all the implementation code and reduce compilation times. You still need to include the +header without the macro at least once, typically in a `pfd-impl.cpp` file. + +```cpp +// In pfd-impl.cpp +#include "portable-file-dialogs.h" +``` + +```cpp +// In all other files +#define PFD_SKIP_IMPLEMENTATION 1 +#include "portable-file-dialogs.h" +``` + +### General concepts + +Dialogs inherit from `pfd::dialog` and are created by calling their class constructor. Their +destructor will block until the window is closed by user interaction. So for instance this +will block until the end of the line: + +```cpp +pfd::message::message("Hi", "there"); +``` + +Whereas this will only block until the end of the scope, allowing the program to perform +additional operations while the dialog is open: + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + // ... perform asynchronous operations here +} +``` + +It is possible to call `bool pfd::dialog::ready(timeout)` on the dialog in order to query its +status and perform asynchronous operations as long as the user has not interacted: + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + while (!m.ready()) + { + // ... perform asynchronous operations here + } +} +``` + +If necessary, a dialog can be forcibly closed using `bool pfd::dialog::kill()`. Note that this +may be confusing to the user and should only be used in very specific situations. It is also not +possible to close a Windows message box that provides no _Cancel_ button. + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + while (!m.ready()) + { + // ... perform asynchronous operations here + + if (too_much_time_has_passed()) + m.kill(); + } +} +``` + +Finally, the user response can be retrieved using `pfd::dialog::result()`. The return value of +this function depends on which dialog is being used. See their respective documentation for more +information: + + * [`pfd::message`](message.md) (message box) + * [`pfd::notify`](notify.md) (notification) + * [`pfd::open_file`](open_file.md) (file open) + * [`pfd::save_file`](save_file.md) (file save) + * [`pfd::select_folder`](select_folder.md) (folder selection) + +### Settings + +The library can be queried and configured through the `pfd::settings` class. + +```cpp +bool pfd::settings::available(); +void pfd::settings::verbose(bool value); +void pfd::settings::rescan(); +``` + +The return value of `pfd::settings::available()` indicates whether a suitable dialog backend (such +as Zenity or KDialog on Linux) has been found. If not, the library will not work and all dialog +invocations will be no-ops. The program will not crash but you should account for this situation +and add a fallback mechanism or exit gracefully. + +Calling `pfd::settings::rescan()` will force a rescan of available backends. This may change the +result of `pfd::settings::available()` if a backend was installed on the system in the meantime. +This is probably only useful for debugging purposes. + +Calling `pfd::settings::verbose(true)` may help debug the library. It will output debug information +to `std::cout` about some operations being performed. diff --git a/extern/pfd-fixed/doc/save_file.md b/extern/pfd-fixed/doc/save_file.md new file mode 100644 index 00000000..5c10badb --- /dev/null +++ b/extern/pfd-fixed/doc/save_file.md @@ -0,0 +1,73 @@ +## File Open API + +The `pfd::save_file` class handles file saving dialogs. It can be provided a title, a starting +directory and/or pre-selected file, an optional filter for recognised file types, and an optional +flag to allow multiple selection: + +```cpp +pfd::save_file::save_file(std::string const &title, + std::string const &initial_path, + std::vector filters = { "All Files", "*" }, + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::force_overwrite` to disable a potential warning when +saving to an existing file. + +The selected file is queried using `pfd::save_file::result()`. If the user canceled the +operation, the returned file name is empty: + +```cpp +std::string pfd::save_file::result(); +``` + +It is possible to ask the file save dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::save_file::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple file selection + +Using `pfd::save_file::result()` will wait for user action before returning. This operation will +block and return the user choice: + +```cpp +auto destination = pfd::save_file("Select a file").result(); +if (!destination.empty()) + std::cout << "User selected file " << destination << "\n"; +``` + +## Example 2: filters + +The filter list enumerates filter names and corresponded space-separated wildcard lists. It +defaults to `{ "All Files", "*" }`, but here is how to use other options: + +```cpp +auto destination = pfd::save_file("Select a file", ".", + { "Image Files", "*.png *.jpg *.jpeg *.bmp", + "Audio Files", "*.wav *.mp3", + "All Files", "*" }, + pfd::opt::force_overwrite).result(); +// Do something with destination +std::cout << "Selected file: " << destination << "\n"; +``` + +## Example 3: asynchronous file save + +Using `pfd::save_file::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// File save dialog +auto dialog = pfd::save_file("Select file to save"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "User selected file: " << dialog.result() << "\n"; +``` diff --git a/extern/pfd-fixed/doc/select_folder.md b/extern/pfd-fixed/doc/select_folder.md new file mode 100644 index 00000000..28f5f63f --- /dev/null +++ b/extern/pfd-fixed/doc/select_folder.md @@ -0,0 +1,55 @@ +## Folder Selection API + +The `pfd::select_folder` class handles folder opening dialogs. It can be provided a title, and an +optional starting directory: + +```cpp +pfd::select_folder::select_folder(std::string const &title, + std::string const &default_path = "", + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::force_path` to force the operating system to use the +provided path. Some systems default to the most recently used path, if applicable. + +The selected folder is queried using `pfd::select_folder::result()`. If the user canceled the +operation, the returned string is empty: + +```cpp +std::string pfd::select_folder::result(); +``` + +It is possible to ask the folder selection dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::select_folder::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple folder selection + +Using `pfd::select_folder::result()` will wait for user action before returning. This operation +will block and return the user choice: + +```cpp +auto selection = pfd::select_folder("Select a folder").result(); +if (!selection.empty()) + std::cout << "User selected folder " << selection << "\n"; +``` + +## Example 2: asynchronous folder open + +Using `pfd::select_folder::ready()` allows the application to perform other tasks while waiting for user input: + +```cpp +// Folder selection dialog +auto dialog = pfd::select_folder("Select folder to open"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "Selected folder: " << dialog.result() << "\n"; +``` diff --git a/extern/pfd-fixed/examples/.gitignore b/extern/pfd-fixed/examples/.gitignore new file mode 100644 index 00000000..bda6acf7 --- /dev/null +++ b/extern/pfd-fixed/examples/.gitignore @@ -0,0 +1,11 @@ +example +example.exe +kill +kill.exe + +Debug +Release +*.vcxproj.user + +.idea +cmake-build-* diff --git a/extern/pfd-fixed/examples/example.cpp b/extern/pfd-fixed/examples/example.cpp new file mode 100644 index 00000000..a216ae4b --- /dev/null +++ b/extern/pfd-fixed/examples/example.cpp @@ -0,0 +1,110 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This program is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#include "portable-file-dialogs.h" + +#include + +#if _WIN32 +#define DEFAULT_PATH "C:\\" +#else +#define DEFAULT_PATH "/tmp" +#endif + +int main() +{ + // Check that a backend is available + if (!pfd::settings::available()) + { + std::cout << "Portable File Dialogs are not available on this platform.\n"; + return 1; + } + + // Set verbosity to true + pfd::settings::verbose(true); + + // Notification + pfd::notify("Important Notification", + "This is ' a message, pay \" attention \\ to it!", + pfd::icon::info); + + // Message box with nice message + auto m = pfd::message("Personal Message", + "You are an amazing person, don’t let anyone make you think otherwise.", + pfd::choice::yes_no_cancel, + pfd::icon::warning); + + // Optional: do something while waiting for user action + for (int i = 0; i < 10 && !m.ready(1000); ++i) + std::cout << "Waited 1 second for user input...\n"; + + // Do something according to the selected button + switch (m.result()) + { + case pfd::button::yes: std::cout << "User agreed.\n"; break; + case pfd::button::no: std::cout << "User disagreed.\n"; break; + case pfd::button::cancel: std::cout << "User freaked out.\n"; break; + default: break; // Should not happen + } + + // Directory selection + auto dir = pfd::select_folder("Select any directory", DEFAULT_PATH).result(); + std::cout << "Selected dir: " << dir << "\n"; + + // File open + auto f = pfd::open_file("Choose files to read", DEFAULT_PATH, + { "Text Files (.txt .text)", "*.txt *.text", + "All Files", "*" }, + pfd::opt::multiselect); + std::cout << "Selected files:"; + for (auto const &name : f.result()) + std::cout << " " + name; + std::cout << "\n"; +} + +// Unused function that just tests the whole API +void api() +{ + // pfd::settings + pfd::settings::verbose(true); + pfd::settings::rescan(); + + // pfd::notify + pfd::notify("", ""); + pfd::notify("", "", pfd::icon::info); + pfd::notify("", "", pfd::icon::warning); + pfd::notify("", "", pfd::icon::error); + pfd::notify("", "", pfd::icon::question); + + pfd::notify a("", ""); + (void)a.ready(); + (void)a.ready(42); + + // pfd::message + pfd::message("", ""); + pfd::message("", "", pfd::choice::ok); + pfd::message("", "", pfd::choice::ok_cancel); + pfd::message("", "", pfd::choice::yes_no); + pfd::message("", "", pfd::choice::yes_no_cancel); + pfd::message("", "", pfd::choice::retry_cancel); + pfd::message("", "", pfd::choice::abort_retry_ignore); + pfd::message("", "", pfd::choice::ok, pfd::icon::info); + pfd::message("", "", pfd::choice::ok, pfd::icon::warning); + pfd::message("", "", pfd::choice::ok, pfd::icon::error); + pfd::message("", "", pfd::choice::ok, pfd::icon::question); + + pfd::message b("", ""); + (void)b.ready(); + (void)b.ready(42); + (void)b.result(); +} + diff --git a/extern/pfd-fixed/examples/example.vcxproj b/extern/pfd-fixed/examples/example.vcxproj new file mode 100644 index 00000000..d7e91492 --- /dev/null +++ b/extern/pfd-fixed/examples/example.vcxproj @@ -0,0 +1,96 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 15.0 + {10F4364D-27C4-4C74-8079-7C42971E81E7} + Win32Proj + example + 10.0 + + + + Application + v142 + Unicode + + + true + + + false + true + + + + + + + + + + + + true + + + false + + + + .. + NotUsing + Level3 + true + true + + + Console + true + + + + + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + + + + + MaxSpeed + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + + + true + true + + + + + + \ No newline at end of file diff --git a/extern/pfd-fixed/examples/kill.cpp b/extern/pfd-fixed/examples/kill.cpp new file mode 100644 index 00000000..787edbeb --- /dev/null +++ b/extern/pfd-fixed/examples/kill.cpp @@ -0,0 +1,42 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This program is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#include "portable-file-dialogs.h" + +#include + +int main() +{ + // Set verbosity to true + pfd::settings::verbose(true); + + // Message box with nice message + auto m = pfd::message("Upgrade software?", + "Press OK to upgrade this software.\n" + "\n" + "By default, the software will update itself\n" + "automatically in 10 seconds.", + pfd::choice::ok_cancel, + pfd::icon::warning); + + // Wait for an answer for up to 10 seconds + for (int i = 0; i < 10 && !m.ready(1000); ++i) + ; + + // Upgrade software if user clicked OK, or if user didn’t interact + bool upgrade = m.ready() ? m.result() == pfd::button::ok : m.kill(); + if (upgrade) + std::cout << "Upgrading software!\n"; + else + std::cout << "Not upgrading software.\n"; +} + diff --git a/extern/pfd-fixed/examples/kill.vcxproj b/extern/pfd-fixed/examples/kill.vcxproj new file mode 100644 index 00000000..b1ee3c9c --- /dev/null +++ b/extern/pfd-fixed/examples/kill.vcxproj @@ -0,0 +1,96 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 15.0 + {B94D26B1-7EF7-43A2-A973-9A96A08E2E17} + Win32Proj + kill + 10.0 + + + + Application + v142 + Unicode + + + true + + + false + true + + + + + + + + + + + + true + + + false + + + + .. + NotUsing + Level3 + true + true + + + Console + true + + + + + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + + + + + MaxSpeed + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + + + true + true + + + + + + diff --git a/extern/pfd-fixed/portable-file-dialogs.h b/extern/pfd-fixed/portable-file-dialogs.h new file mode 100644 index 00000000..41e588ac --- /dev/null +++ b/extern/pfd-fixed/portable-file-dialogs.h @@ -0,0 +1,1731 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This library is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#pragma once + +#if _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#include +#include +#include // IFileDialog +#include +#include +#include // std::async + +#elif __EMSCRIPTEN__ +#include + +#else +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 2 // for popen() +#endif +#ifdef __APPLE__ +# ifndef _DARWIN_C_SOURCE +# define _DARWIN_C_SOURCE +# endif +#endif +#include // popen() +#include // std::getenv() +#include // fcntl() +#include // read(), pipe(), dup2() +#include // ::kill, std::signal +#include // waitpid() +#endif + +#include // std::string +#include // std::shared_ptr +#include // std::ostream +#include // std::map +#include // std::set +#include // std::regex +#include // std::mutex, std::this_thread +#include // std::chrono + +// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog +#ifndef PFD_HAS_IFILEDIALOG +# define PFD_HAS_IFILEDIALOG 1 +# if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION +# if __GXX_ABI_VERSION <= 1013 +# undef PFD_HAS_IFILEDIALOG +# define PFD_HAS_IFILEDIALOG 0 +# endif +# endif +#endif + +namespace pfd +{ + +enum class button +{ + cancel = -1, + ok, + yes, + no, + abort, + retry, + ignore, +}; + +enum class choice +{ + ok = 0, + ok_cancel, + yes_no, + yes_no_cancel, + retry_cancel, + abort_retry_ignore, +}; + +enum class icon +{ + info = 0, + warning, + error, + question, +}; + +// Additional option flags for various dialog constructors +enum class opt : uint8_t +{ + none = 0, + // For file open, allow multiselect. + multiselect = 0x1, + // For file save, force overwrite and disable the confirmation dialog. + force_overwrite = 0x2, + // For folder select, force path to be the provided argument instead + // of the last opened directory, which is the Microsoft-recommended, + // user-friendly behaviour. + force_path = 0x4, +}; + +inline opt operator |(opt a, opt b) { return opt(uint8_t(a) | uint8_t(b)); } +inline bool operator &(opt a, opt b) { return bool(uint8_t(a) & uint8_t(b)); } + +// The settings class, only exposing to the user a way to set verbose mode +// and to force a rescan of installed desktop helpers (zenity, kdialog…). +class settings +{ +public: + static bool available(); + + static void verbose(bool value); + static void rescan(); + +protected: + explicit settings(bool resync = false); + + bool check_program(std::string const &program); + + inline bool is_osascript() const; + inline bool is_zenity() const; + inline bool is_kdialog() const; + + enum class flag + { + is_scanned = 0, + is_verbose, + + has_zenity, + has_matedialog, + has_qarma, + has_kdialog, + is_vista, + + max_flag, + }; + + // Static array of flags for internal state + bool const &flags(flag in_flag) const; + + // Non-const getter for the static array of flags + bool &flags(flag in_flag); +}; + +// Internal classes, not to be used by client applications +namespace internal +{ + +// Process wait timeout, in milliseconds +static int const default_wait_timeout = 20; + +class executor +{ + friend class dialog; + +public: + // High level function to get the result of a command + std::string result(int *exit_code = nullptr); + + // High level function to abort + bool kill(); + +#if _WIN32 + void start_func(std::function const &fun); + static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam); +#elif __EMSCRIPTEN__ + void start(int exit_code); +#else + void start_process(std::vector const &command); +#endif + + ~executor(); + +protected: + bool ready(int timeout = default_wait_timeout); + void stop(); + +private: + bool m_running = false; + std::string m_stdout; + int m_exit_code = -1; +#if _WIN32 + std::future m_future; + std::set m_windows; + std::condition_variable m_cond; + std::mutex m_mutex; + DWORD m_tid; +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something +#else + pid_t m_pid = 0; + int m_fd = -1; +#endif +}; + +class platform +{ +protected: +#if _WIN32 + // Helper class around LoadLibraryA() and GetProcAddress() with some safety + class dll + { + public: + dll(std::string const &name); + ~dll(); + + template class proc + { + public: + proc(dll const &lib, std::string const &sym) + : m_proc(reinterpret_cast(::GetProcAddress(lib.handle, sym.c_str()))) + {} + + operator bool() const { return m_proc != nullptr; } + operator T *() const { return m_proc; } + + private: + T *m_proc; + }; + + private: + HMODULE handle; + }; + + // Helper class around CoInitialize() and CoUnInitialize() + class ole32_dll : public dll + { + public: + ole32_dll(); + ~ole32_dll(); + bool is_initialized(); + + private: + HRESULT m_state; + }; + + // Helper class around CreateActCtx() and ActivateActCtx() + class new_style_context + { + public: + new_style_context(); + ~new_style_context(); + + private: + HANDLE create(); + ULONG_PTR m_cookie = 0; + }; +#endif +}; + +class dialog : protected settings, protected platform +{ +public: + bool ready(int timeout = default_wait_timeout) const; + bool kill() const; + +protected: + explicit dialog(); + + std::vector desktop_helper() const; + static std::string buttons_to_name(choice _choice); + static std::string get_icon_name(icon _icon); + + std::string powershell_quote(std::string const &str) const; + std::string osascript_quote(std::string const &str) const; + std::string shell_quote(std::string const &str) const; + + // Keep handle to executing command + std::shared_ptr m_async; +}; + +class file_dialog : public dialog +{ +protected: + enum type + { + open, + save, + folder, + }; + + file_dialog(type in_type, + std::string const &title, + std::string const &default_path = "", + std::vector const &filters = {}, + opt options = opt::none); + +protected: + std::string string_result(); + std::vector vector_result(); + +#if _WIN32 + static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); +#if PFD_HAS_IFILEDIALOG + std::string select_folder_vista(IFileDialog *ifd, bool force_path); +#endif + + std::wstring m_wtitle; + std::wstring m_wdefault_path; + + std::vector m_vector_result; +#endif +}; + +} // namespace internal + +// +// The notify widget +// + +class notify : public internal::dialog +{ +public: + notify(std::string const &title, + std::string const &message, + icon _icon = icon::info); +}; + +// +// The message widget +// + +class message : public internal::dialog +{ +public: + message(std::string const &title, + std::string const &text, + choice _choice = choice::ok_cancel, + icon _icon = icon::info); + + button result(); + +private: + // Some extra logic to map the exit code to button number + std::map m_mappings; +}; + +// +// The open_file, save_file, and open_folder widgets +// + +class open_file : public internal::file_dialog +{ +public: + open_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]] +#endif +#endif + open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect); + + std::vector result(); +}; + +class save_file : public internal::file_dialog +{ +public: + save_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]] +#endif +#endif + save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite); + + std::string result(); +}; + +class select_folder : public internal::file_dialog +{ +public: + select_folder(std::string const &title, + std::string const &default_path = "", + opt options = opt::none); + + std::string result(); +}; + +// +// Below this are all the method implementations. You may choose to define the +// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except +// in one place. This may reduce compilation times. +// + +#if !defined PFD_SKIP_IMPLEMENTATION + +// internal free functions implementations + +namespace internal +{ + +#if _WIN32 +static inline std::wstring str2wstr(std::string const &str) +{ + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); + std::wstring ret(len, '\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size()); + return ret; +} + +static inline std::string wstr2str(std::wstring const &str) +{ + int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr); + std::string ret(len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr); + return ret; +} + +static inline bool is_vista() +{ + OSVERSIONINFOEXW osvi; + memset(&osvi, 0, sizeof(osvi)); + DWORDLONG const mask = VerSetConditionMask( + VerSetConditionMask( + VerSetConditionMask( + 0, VER_MAJORVERSION, VER_GREATER_EQUAL), + VER_MINORVERSION, VER_GREATER_EQUAL), + VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + osvi.dwOSVersionInfoSize = sizeof(osvi); + osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA); + osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA); + osvi.wServicePackMajor = 0; + + return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE; +} +#endif + +// This is necessary until C++20 which will have std::string::ends_with() etc. + +static inline bool ends_with(std::string const &str, std::string const &suffix) +{ + return suffix.size() <= str.size() && + str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +static inline bool starts_with(std::string const &str, std::string const &prefix) +{ + return prefix.size() <= str.size() && + str.compare(0, prefix.size(), prefix) == 0; +} + +} // namespace internal + +// settings implementation + +inline settings::settings(bool resync) +{ + flags(flag::is_scanned) &= !resync; + + if (flags(flag::is_scanned)) + return; + +#if _WIN32 + flags(flag::is_vista) = internal::is_vista(); +#elif !__APPLE__ + flags(flag::has_zenity) = check_program("zenity"); + flags(flag::has_matedialog) = check_program("matedialog"); + flags(flag::has_qarma) = check_program("qarma"); + flags(flag::has_kdialog) = check_program("kdialog"); + + // If multiple helpers are available, try to default to the best one + if (flags(flag::has_zenity) && flags(flag::has_kdialog)) + { + auto desktop_name = std::getenv("XDG_SESSION_DESKTOP"); + if (desktop_name && desktop_name == std::string("gnome")) + flags(flag::has_kdialog) = false; + else if (desktop_name && desktop_name == std::string("KDE")) + flags(flag::has_zenity) = false; + } +#endif + + flags(flag::is_scanned) = true; +} + +inline bool settings::available() +{ +#if _WIN32 + return true; +#elif __APPLE__ + return true; +#else + settings tmp; + return tmp.flags(flag::has_zenity) || + tmp.flags(flag::has_matedialog) || + tmp.flags(flag::has_qarma) || + tmp.flags(flag::has_kdialog); +#endif +} + +inline void settings::verbose(bool value) +{ + settings().flags(flag::is_verbose) = value; +} + +inline void settings::rescan() +{ + settings(/* resync = */ true); +} + +// Check whether a program is present using “which”. +inline bool settings::check_program(std::string const &program) +{ +#if _WIN32 + (void)program; + return false; +#elif __EMSCRIPTEN__ + (void)program; + return false; +#else + int exit_code = -1; + internal::executor async; + async.start_process({"/bin/sh", "-c", "which " + program}); + async.result(&exit_code); + return exit_code == 0; +#endif +} + +inline bool settings::is_osascript() const +{ +#if __APPLE__ + return true; +#else + return false; +#endif +} + +inline bool settings::is_zenity() const +{ + return flags(flag::has_zenity) || + flags(flag::has_matedialog) || + flags(flag::has_qarma); +} + +inline bool settings::is_kdialog() const +{ + return flags(flag::has_kdialog); +} + +inline bool const &settings::flags(flag in_flag) const +{ + static bool flags[size_t(flag::max_flag)]; + return flags[size_t(in_flag)]; +} + +inline bool &settings::flags(flag in_flag) +{ + return const_cast(static_cast(this)->flags(in_flag)); +} + +// executor implementation + +inline std::string internal::executor::result(int *exit_code /* = nullptr */) +{ + stop(); + if (exit_code) + *exit_code = m_exit_code; + return m_stdout; +} + +inline bool internal::executor::kill() +{ +#if _WIN32 + if (m_future.valid()) + { + // Close all windows that weren’t open when we started the future + auto previous_windows = m_windows; + EnumWindows(&enum_windows_callback, (LPARAM)this); + for (auto hwnd : m_windows) + if (previous_windows.find(hwnd) == previous_windows.end()) + SendMessage(hwnd, WM_CLOSE, 0, 0); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; + return false; // cannot kill +#else + ::kill(m_pid, SIGKILL); +#endif + stop(); + return true; +} + +#if _WIN32 +inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) +{ + auto that = (executor *)lParam; + + DWORD pid; + auto tid = GetWindowThreadProcessId(hwnd, &pid); + if (tid == that->m_tid) + that->m_windows.insert(hwnd); + return TRUE; +} +#endif + +#if _WIN32 +inline void internal::executor::start_func(std::function const &fun) +{ + stop(); + + auto trampoline = [fun, this]() + { + // Save our thread id so that the caller can cancel us + m_tid = GetCurrentThreadId(); + EnumWindows(&enum_windows_callback, (LPARAM)this); + m_cond.notify_all(); + return fun(&m_exit_code); + }; + + std::unique_lock lock(m_mutex); + m_future = std::async(std::launch::async, trampoline); + m_cond.wait(lock); + m_running = true; +} + +#elif __EMSCRIPTEN__ +inline void internal::executor::start(int exit_code) +{ + m_exit_code = exit_code; +} + +#else +inline void internal::executor::start_process(std::vector const &command) +{ + stop(); + m_stdout.clear(); + m_exit_code = -1; + + int in[2], out[2]; + if (pipe(in) != 0 || pipe(out) != 0) + return; + + m_pid = fork(); + if (m_pid < 0) + return; + + close(in[m_pid ? 0 : 1]); + close(out[m_pid ? 1 : 0]); + + if (m_pid == 0) + { + dup2(in[0], STDIN_FILENO); + dup2(out[1], STDOUT_FILENO); + + // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity) + int fd = open("/dev/null", O_WRONLY); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector args; + std::transform(command.cbegin(), command.cend(), std::back_inserter(args), + [](std::string const &s) { return const_cast(s.c_str()); }); + args.push_back(nullptr); // null-terminate argv[] + + execvp(args[0], args.data()); + exit(1); + } + + close(in[1]); + m_fd = out[0]; + auto flags = fcntl(m_fd, F_GETFL); + fcntl(m_fd, F_SETFL, flags | O_NONBLOCK); + + m_running = true; +} +#endif + +inline internal::executor::~executor() +{ + stop(); +} + +inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) +{ + if (!m_running) + return true; + +#if _WIN32 + if (m_future.valid()) + { + auto status = m_future.wait_for(std::chrono::milliseconds(timeout)); + if (status != std::future_status::ready) + { + // On Windows, we need to run the message pump. If the async + // thread uses a Windows API dialog, it may be attached to the + // main thread and waiting for messages that only we can dispatch. + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return false; + } + + m_stdout = m_future.get(); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; +#else + char buf[BUFSIZ]; + ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore + if (received > 0) + { + m_stdout += std::string(buf, received); + return false; + } + + // Reap child process if it is dead. It is possible that the system has already reaped it + // (this happens when the calling application handles or ignores SIG_CHLD) and results in + // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for + // a little while. + int status; + pid_t child = waitpid(m_pid, &status, WNOHANG); + if (child != m_pid && (child >= 0 || errno != ECHILD)) + { + // FIXME: this happens almost always at first iteration + std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + return false; + } + + close(m_fd); + m_exit_code = WEXITSTATUS(status); +#endif + + m_running = false; + return true; +} + +inline void internal::executor::stop() +{ + // Loop until the user closes the dialog + while (!ready()) + ; +} + +// dll implementation + +#if _WIN32 +inline internal::platform::dll::dll(std::string const &name) + : handle(::LoadLibraryA(name.c_str())) +{} + +inline internal::platform::dll::~dll() +{ + if (handle) + ::FreeLibrary(handle); +} +#endif // _WIN32 + +// ole32_dll implementation + +#if _WIN32 +inline internal::platform::ole32_dll::ole32_dll() + : dll("ole32.dll") +{ + // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes. + // See https://github.com/samhocevar/portable-file-dialogs/issues/51 + auto coinit = proc(*this, "CoInitializeEx"); + m_state = coinit(nullptr, COINIT_MULTITHREADED); +} + +inline internal::platform::ole32_dll::~ole32_dll() +{ + if (is_initialized()) + proc(*this, "CoUninitialize")(); +} + +inline bool internal::platform::ole32_dll::is_initialized() +{ + return m_state == S_OK || m_state == S_FALSE; +} +#endif + +// new_style_context implementation + +#if _WIN32 +inline internal::platform::new_style_context::new_style_context() +{ + // Only create one activation context for the whole app lifetime. + static HANDLE hctx = create(); + + if (hctx != INVALID_HANDLE_VALUE) + ActivateActCtx(hctx, &m_cookie); +} + +inline internal::platform::new_style_context::~new_style_context() +{ + DeactivateActCtx(0, m_cookie); +} + +inline HANDLE internal::platform::new_style_context::create() +{ + // This “hack” seems to be necessary for this code to work on windows XP. + // Without it, dialogs do not show and close immediately. GetError() + // returns 0 so I don’t know what causes this. I was not able to reproduce + // this behavior on Windows 7 and 10 but just in case, let it be here for + // those versions too. + // This hack is not required if other dialogs are used (they load comdlg32 + // automatically), only if message boxes are used. + dll comdlg32("comdlg32.dll"); + + // Using approach as shown here: https://stackoverflow.com/a/10444161 + UINT len = ::GetSystemDirectoryA(nullptr, 0); + std::string sys_dir(len, '\0'); + ::GetSystemDirectoryA(&sys_dir[0], len); + + ACTCTXA act_ctx = + { + // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a + // crash with error “default context is already set”. + sizeof(act_ctx), + ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, + "shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, NULL, NULL + }; + + return ::CreateActCtxA(&act_ctx); +} +#endif // _WIN32 + +// dialog implementation + +inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const +{ + return m_async->ready(timeout); +} + +inline bool internal::dialog::kill() const +{ + return m_async->kill(); +} + +inline internal::dialog::dialog() + : m_async(std::make_shared()) +{ +} + +inline std::vector internal::dialog::desktop_helper() const +{ +#if __APPLE__ + return { "osascript" }; +#else + return { flags(flag::has_zenity) ? "zenity" + : flags(flag::has_matedialog) ? "matedialog" + : flags(flag::has_qarma) ? "qarma" + : flags(flag::has_kdialog) ? "kdialog" + : "echo" }; +#endif +} + +inline std::string internal::dialog::buttons_to_name(choice _choice) +{ + switch (_choice) + { + case choice::ok_cancel: return "okcancel"; + case choice::yes_no: return "yesno"; + case choice::yes_no_cancel: return "yesnocancel"; + case choice::retry_cancel: return "retrycancel"; + case choice::abort_retry_ignore: return "abortretryignore"; + /* case choice::ok: */ default: return "ok"; + } +} + +inline std::string internal::dialog::get_icon_name(icon _icon) +{ + switch (_icon) + { + case icon::warning: return "warning"; + case icon::error: return "error"; + case icon::question: return "question"; + // Zenity wants "information" but WinForms wants "info" + /* case icon::info: */ default: +#if _WIN32 + return "info"; +#else + return "information"; +#endif + } +} + +// THis is only used for debugging purposes +inline std::ostream& operator <<(std::ostream &s, std::vector const &v) +{ + int not_first = 0; + for (auto &e : v) + s << (not_first++ ? " " : "") << e; + return s; +} + +// Properly quote a string for Powershell: replace ' or " with '' or "" +// FIXME: we should probably get rid of newlines! +// FIXME: the \" sequence seems unsafe, too! +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::powershell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; +} + +// Properly quote a string for osascript: replace \ or " with \\ or \" +// XXX: this also used to replace ' with \' when popen was used, but it would be +// smarter to do shell_quote(osascript_quote(...)) if this is needed again. +inline std::string internal::dialog::osascript_quote(std::string const &str) const +{ + return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\""; +} + +// Properly quote a string for the shell: just replace ' with '\'' +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::shell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; +} + +// file_dialog implementation + +inline internal::file_dialog::file_dialog(type in_type, + std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = {} */, + opt options /* = opt::none */) +{ +#if _WIN32 + std::string filter_list; + std::regex whitespace(" *"); + for (size_t i = 0; i + 1 < filters.size(); i += 2) + { + filter_list += filters[i] + '\0'; + filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0'; + } + filter_list += '\0'; + + m_async->start_func([this, in_type, title, default_path, filter_list, + options](int *exit_code) -> std::string + { + (void)exit_code; + m_wtitle = internal::str2wstr(title); + m_wdefault_path = internal::str2wstr(default_path); + auto wfilter_list = internal::str2wstr(filter_list); + + // Initialise COM. This is required for the new folder selection window, + // (see https://github.com/samhocevar/portable-file-dialogs/pull/21) + // and to avoid random crashes with GetOpenFileNameW() (see + // https://github.com/samhocevar/portable-file-dialogs/issues/51) + ole32_dll ole32; + + // Folder selection uses a different method + if (in_type == type::folder) + { +#if PFD_HAS_IFILEDIALOG + if (flags(flag::is_vista)) + { + // On Vista and higher we should be able to use IFileDialog for folder selection + IFileDialog *ifd; + HRESULT hr = dll::proc(ole32, "CoCreateInstance") + (CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd)); + + // In case CoCreateInstance fails (which it should not), try legacy approach + if (SUCCEEDED(hr)) + return select_folder_vista(ifd, options & opt::force_path); + } +#endif + + BROWSEINFOW bi; + memset(&bi, 0, sizeof(bi)); + + bi.lpfn = &bffcallback; + bi.lParam = (LPARAM)this; + + if (flags(flag::is_vista)) + { + if (ole32.is_initialized()) + bi.ulFlags |= BIF_NEWDIALOGSTYLE; + bi.ulFlags |= BIF_EDITBOX; + bi.ulFlags |= BIF_STATUSTEXT; + } + + auto *list = SHBrowseForFolderW(&bi); + std::string ret; + if (list) + { + auto buffer = new wchar_t[MAX_PATH]; + SHGetPathFromIDListW(list, buffer); + dll::proc(ole32, "CoTaskMemFree")(list); + ret = internal::wstr2str(buffer); + delete[] buffer; + } + return ret; + } + + OPENFILENAMEW ofn; + memset(&ofn, 0, sizeof(ofn)); + ofn.lStructSize = sizeof(OPENFILENAMEW); + ofn.hwndOwner = GetActiveWindow(); + + ofn.lpstrFilter = wfilter_list.c_str(); + + auto woutput = std::wstring(MAX_PATH * 256, L'\0'); + ofn.lpstrFile = (LPWSTR)woutput.data(); + ofn.nMaxFile = (DWORD)woutput.size(); + if (!m_wdefault_path.empty()) + { + // If a directory was provided, use it as the initial directory. If + // a valid path was provided, use it as the initial file. Otherwise, + // let the Windows API decide. + auto path_attr = GetFileAttributesW(m_wdefault_path.c_str()); + if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) + ofn.lpstrInitialDir = m_wdefault_path.c_str(); + else if (m_wdefault_path.size() <= woutput.size()) + //second argument is size of buffer, not length of string + StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_wdefault_path.c_str()); + else + { + ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data(); + ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size(); + } + } + ofn.lpstrTitle = m_wtitle.c_str(); + ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; + + dll comdlg32("comdlg32.dll"); + + // Apply new visual style (required for windows XP) + new_style_context ctx; + + if (in_type == type::save) + { + if (!(options & opt::force_overwrite)) + ofn.Flags |= OFN_OVERWRITEPROMPT; + + dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); + if (get_save_file_name(&ofn) == 0) + return ""; + return internal::wstr2str(woutput.c_str()); + } + else + { + if (options & opt::multiselect) + ofn.Flags |= OFN_ALLOWMULTISELECT; + ofn.Flags |= OFN_PATHMUSTEXIST; + + dll::proc get_open_file_name(comdlg32, "GetOpenFileNameW"); + if (get_open_file_name(&ofn) == 0) + return ""; + } + + std::string prefix; + for (wchar_t const *p = woutput.c_str(); *p; ) + { + auto filename = internal::wstr2str(p); + p += wcslen(p); + // In multiselect mode, we advance p one wchar further and + // check for another filename. If there is one and the + // prefix is empty, it means we just read the prefix. + if ((options & opt::multiselect) && *++p && prefix.empty()) + { + prefix = filename + "/"; + continue; + } + + m_vector_result.push_back(prefix + filename); + } + + return ""; + }); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "set ret to choose"; + switch (in_type) + { + case type::save: + script += " file name"; + break; + case type::open: default: + script += " file"; + if (options & opt::multiselect) + script += " with multiple selections allowed"; + break; + case type::folder: + script += " folder"; + break; + } + + if (default_path.size()) + script += " default location " + osascript_quote(default_path); + script += " with prompt " + osascript_quote(title); + + if (in_type == type::open) + { + // Concatenate all user-provided filter patterns + std::string patterns; + for (size_t i = 0; i < filters.size() / 2; ++i) + patterns += " " + filters[2 * i + 1]; + + // Split the pattern list to check whether "*" is in there; if it + // is, we have to disable filters because there is no mechanism in + // OS X for the user to override the filter. + std::regex sep("\\s+"); + std::string filter_list; + bool has_filter = true; + std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1); + std::sregex_token_iterator end; + for ( ; iter != end; ++iter) + { + auto pat = iter->str(); + if (pat == "*" || pat == "*.*") + has_filter = false; + else if (internal::starts_with(pat, "*.")) + filter_list += (filter_list.size() == 0 ? "" : ",") + + osascript_quote(pat.substr(2, pat.size() - 2)); + } + if (has_filter && filter_list.size() > 0) + script += " of type {" + filter_list + "}"; + } + + if (in_type == type::open && (options & opt::multiselect)) + { + script += "\nset s to \"\""; + script += "\nrepeat with i in ret"; + script += "\n set s to s & (POSIX path of i) & \"\\n\""; + script += "\nend repeat"; + script += "\ncopy s to stdout"; + } + else + { + script += "\nPOSIX path of ret"; + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + command.push_back("--file-selection"); + command.push_back("--filename=" + default_path); + command.push_back("--title"); + command.push_back(title); + command.push_back("--separator=\n"); + + for (size_t i = 0; i < filters.size() / 2; ++i) + { + command.push_back("--file-filter"); + command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]); + } + + if (in_type == type::save) + command.push_back("--save"); + if (in_type == type::folder) + command.push_back("--directory"); + if (!(options & opt::force_overwrite)) + command.push_back("--confirm-overwrite"); + if (options & opt::multiselect) + command.push_back("--multiple"); + } + else if (is_kdialog()) + { + switch (in_type) + { + case type::save: command.push_back("--getsavefilename"); break; + case type::open: command.push_back("--getopenfilename"); break; + case type::folder: command.push_back("--getexistingdirectory"); break; + } + if (options & opt::multiselect) + { + command.push_back("--multiple"); + command.push_back("--separate-output"); + } + + command.push_back(default_path); + + std::string filter; + for (size_t i = 0; i < filters.size() / 2; ++i) + filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")"; + command.push_back(filter); + + command.push_back("--title"); + command.push_back(title); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline std::string internal::file_dialog::string_result() +{ +#if _WIN32 + return m_async->result(); +#else + auto ret = m_async->result(); + // Strip potential trailing newline (zenity). Also strip trailing slash + // added by osascript for consistency with other backends. + while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/')) + ret.pop_back(); + return ret; +#endif +} + +inline std::vector internal::file_dialog::vector_result() +{ +#if _WIN32 + m_async->result(); + return m_vector_result; +#else + std::vector ret; + auto result = m_async->result(); + for (;;) + { + // Split result along newline characters + auto i = result.find('\n'); + if (i == 0 || i == std::string::npos) + break; + ret.push_back(result.substr(0, i)); + result = result.substr(i + 1, result.size()); + } + return ret; +#endif +} + +#if _WIN32 +// Use a static function to pass as BFFCALLBACK for legacy folder select +inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, + LPARAM, LPARAM pData) +{ + auto inst = (file_dialog *)pData; + switch (uMsg) + { + case BFFM_INITIALIZED: + SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); + break; + } + return 0; +} + +#if PFD_HAS_IFILEDIALOG +inline std::string internal::file_dialog::select_folder_vista(IFileDialog *ifd, bool force_path) +{ + std::string result; + + IShellItem *folder; + + // Load library at runtime so app doesn't link it at load time (which will fail on windows XP) + dll shell32("shell32.dll"); + dll::proc + create_item(shell32, "SHCreateItemFromParsingName"); + + if (!create_item) + return ""; + + auto hr = create_item(m_wdefault_path.c_str(), + nullptr, + IID_PPV_ARGS(&folder)); + + // Set default folder if found. This only sets the default folder. If + // Windows has any info about the most recently selected folder, it + // will display it instead. Generally, calling SetFolder() to set the + // current directory “is not a good or expected user experience and + // should therefore be avoided”: + // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder + if (SUCCEEDED(hr)) + { + if (force_path) + ifd->SetFolder(folder); + else + ifd->SetDefaultFolder(folder); + folder->Release(); + } + + // Set the dialog title and option to select folders + ifd->SetOptions(FOS_PICKFOLDERS); + ifd->SetTitle(m_wtitle.c_str()); + + hr = ifd->Show(GetActiveWindow()); + if (SUCCEEDED(hr)) + { + IShellItem* item; + hr = ifd->GetResult(&item); + if (SUCCEEDED(hr)) + { + wchar_t* wselected = nullptr; + item->GetDisplayName(SIGDN_FILESYSPATH, &wselected); + item->Release(); + + if (wselected) + { + result = internal::wstr2str(std::wstring(wselected)); + dll::proc(ole32_dll(), "CoTaskMemFree")(wselected); + } + } + } + + ifd->Release(); + + return result; +} +#endif +#endif + +// notify implementation + +inline notify::notify(std::string const &title, + std::string const &message, + icon _icon /* = icon::info */) +{ + if (_icon == icon::question) // Not supported by notifications + _icon = icon::info; + +#if _WIN32 + // Use a static shared pointer for notify_icon so that we can delete + // it whenever we need to display a new one, and we can also wait + // until the program has finished running. + struct notify_icon_data : public NOTIFYICONDATAW + { + ~notify_icon_data() { Shell_NotifyIconW(NIM_DELETE, this); } + }; + + static std::shared_ptr nid; + + // Release the previous notification icon, if any, and allocate a new + // one. Note that std::make_shared() does value initialization, so there + // is no need to memset the structure. + nid = nullptr; + nid = std::make_shared(); + + // For XP support + nid->cbSize = NOTIFYICONDATAW_V2_SIZE; + nid->hWnd = nullptr; + nid->uID = 0; + + // Flag Description: + // - NIF_ICON The hIcon member is valid. + // - NIF_MESSAGE The uCallbackMessage member is valid. + // - NIF_TIP The szTip member is valid. + // - NIF_STATE The dwState and dwStateMask members are valid. + // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and dwInfoFlags members are valid. + // - NIF_GUID Reserved. + nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO; + + // Flag Description + // - NIIF_ERROR An error icon. + // - NIIF_INFO An information icon. + // - NIIF_NONE No icon. + // - NIIF_WARNING A warning icon. + // - NIIF_ICON_MASK Version 6.0. Reserved. + // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips + switch (_icon) + { + case icon::warning: nid->dwInfoFlags = NIIF_WARNING; break; + case icon::error: nid->dwInfoFlags = NIIF_ERROR; break; + /* case icon::info: */ default: nid->dwInfoFlags = NIIF_INFO; break; + } + + ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL + { + ((NOTIFYICONDATAW *)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName); + return false; + }; + + nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); + ::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get()); + + nid->uTimeout = 5000; + + StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str()); + StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str()); + + // Display the new icon + Shell_NotifyIconW(NIM_ADD, nid.get()); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + command.push_back("-e"); + command.push_back("display notification " + osascript_quote(message) + + " with title " + osascript_quote(title)); + } + else if (is_zenity()) + { + command.push_back("--notification"); + command.push_back("--window-icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--text"); + command.push_back(title + "\n" + message); + } + else if (is_kdialog()) + { + command.push_back("--icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--title"); + command.push_back(title); + command.push_back("--passivepopup"); + command.push_back(message); + command.push_back("5"); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +// message implementation + +inline message::message(std::string const &title, + std::string const &text, + choice _choice /* = choice::ok_cancel */, + icon _icon /* = icon::info */) +{ +#if _WIN32 + // Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought + // to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52 + UINT style = MB_SYSTEMMODAL; + switch (_icon) + { + case icon::warning: style |= MB_ICONWARNING; break; + case icon::error: style |= MB_ICONERROR; break; + case icon::question: style |= MB_ICONQUESTION; break; + /* case icon::info: */ default: style |= MB_ICONINFORMATION; break; + } + + switch (_choice) + { + case choice::ok_cancel: style |= MB_OKCANCEL; break; + case choice::yes_no: style |= MB_YESNO; break; + case choice::yes_no_cancel: style |= MB_YESNOCANCEL; break; + case choice::retry_cancel: style |= MB_RETRYCANCEL; break; + case choice::abort_retry_ignore: style |= MB_ABORTRETRYIGNORE; break; + /* case choice::ok: */ default: style |= MB_OK; break; + } + + m_mappings[IDCANCEL] = button::cancel; + m_mappings[IDOK] = button::ok; + m_mappings[IDYES] = button::yes; + m_mappings[IDNO] = button::no; + m_mappings[IDABORT] = button::abort; + m_mappings[IDRETRY] = button::retry; + m_mappings[IDIGNORE] = button::ignore; + + m_async->start_func([text, title, style](int* exit_code) -> std::string + { + auto wtext = internal::str2wstr(text); + auto wtitle = internal::str2wstr(title); + // Apply new visual style (required for all Windows versions) + new_style_context ctx; + *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style); + return ""; + }); + +#elif __EMSCRIPTEN__ + std::string full_message; + switch (_icon) + { + case icon::warning: full_message = "⚠️"; break; + case icon::error: full_message = "⛔"; break; + case icon::question: full_message = "❓"; break; + /* case icon::info: */ default: full_message = "ℹ"; break; + } + + full_message += ' ' + title + "\n\n" + text; + + // This does not really start an async task; it just passes the + // EM_ASM_INT return value to a fake start() function. + m_async->start(EM_ASM_INT( + { + if ($1) + return window.confirm(UTF8ToString($0)) ? 0 : -1; + alert(UTF8ToString($0)); + return 0; + }, full_message.c_str(), _choice == choice::ok_cancel)); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "display dialog " + osascript_quote(text) + + " with title " + osascript_quote(title); + switch (_choice) + { + case choice::ok_cancel: + script += "buttons {\"OK\", \"Cancel\"}" + " default button \"OK\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::yes_no: + script += "buttons {\"Yes\", \"No\"}" + " default button \"Yes\"" + " cancel button \"No\""; + m_mappings[256] = button::no; + break; + case choice::yes_no_cancel: + script += "buttons {\"Yes\", \"No\", \"Cancel\"}" + " default button \"Yes\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::retry_cancel: + script += "buttons {\"Retry\", \"Cancel\"}" + " default button \"Retry\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::abort_retry_ignore: + script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" + " default button \"Retry\"" + " cancel button \"Retry\""; + m_mappings[256] = button::cancel; + break; + case choice::ok: default: + script += "buttons {\"OK\"}" + " default button \"OK\"" + " cancel button \"OK\""; + m_mappings[256] = button::ok; + break; + } + script += " with icon "; + switch (_icon) + { + #define PFD_OSX_ICON(n) "alias ((path to library folder from system domain) as text " \ + "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")" + case icon::info: default: script += PFD_OSX_ICON("ToolBarInfo"); break; + case icon::warning: script += "caution"; break; + case icon::error: script += "stop"; break; + case icon::question: script += PFD_OSX_ICON("GenericQuestionMarkIcon"); break; + #undef PFD_OSX_ICON + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + switch (_choice) + { + case choice::ok_cancel: + command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); break; + case choice::yes_no: + // Do not use standard --question because it causes “No” to return -1, + // which is inconsistent with the “Yes/No/Cancel” mode below. + command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::yes_no_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::retry_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); break; + case choice::abort_retry_ignore: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", "--extra-button=Abort", "--extra-button=Retry" }); break; + case choice::ok: + default: + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--warning"); break; + default: command.push_back("--info"); break; + } + } + + command.insert(command.end(), { "--title", title, + "--width=300", "--height=0", // sensible defaults + "--text", text, + "--icon-name=dialog-" + get_icon_name(_icon) }); + } + else if (is_kdialog()) + { + if (_choice == choice::ok) + { + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--sorry"); break; + default: command.push_back("--msgbox"); break; + } + } + else + { + std::string flag = "--"; + if (_icon == icon::warning || _icon == icon::error) + flag += "warning"; + flag += "yesno"; + if (_choice == choice::yes_no_cancel) + flag += "cancel"; + command.push_back(flag); + if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) + { + m_mappings[0] = button::yes; + m_mappings[256] = button::no; + } + } + + command.push_back(text); + command.push_back("--title"); + command.push_back(title); + + // Must be after the above part + if (_choice == choice::ok_cancel) + command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" }); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline button message::result() +{ + int exit_code; + auto ret = m_async->result(&exit_code); + // osascript will say "button returned:Cancel\n" + // and others will just say "Cancel\n" + if (exit_code < 0 || // this means cancel + internal::ends_with(ret, "Cancel\n")) + return button::cancel; + if (internal::ends_with(ret, "OK\n")) + return button::ok; + if (internal::ends_with(ret, "Yes\n")) + return button::yes; + if (internal::ends_with(ret, "No\n")) + return button::no; + if (internal::ends_with(ret, "Abort\n")) + return button::abort; + if (internal::ends_with(ret, "Retry\n")) + return button::retry; + if (internal::ends_with(ret, "Ignore\n")) + return button::ignore; + if (m_mappings.count(exit_code) != 0) + return m_mappings[exit_code]; + return exit_code == 0 ? button::ok : button::cancel; +} + +// open_file implementation + +inline open_file::open_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::open, title, default_path, filters, options) +{ +} + +inline open_file::open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect) + : open_file(title, default_path, filters, + (allow_multiselect ? opt::multiselect : opt::none)) +{ +} + +inline std::vector open_file::result() +{ + return vector_result(); +} + +// save_file implementation + +inline save_file::save_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::save, title, default_path, filters, options) +{ +} + +inline save_file::save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite) + : save_file(title, default_path, filters, + (confirm_overwrite ? opt::none : opt::force_overwrite)) +{ +} + +inline std::string save_file::result() +{ + return string_result(); +} + +// select_folder implementation + +inline select_folder::select_folder(std::string const &title, + std::string const &default_path /* = "" */, + opt options /* = opt::none */) + : file_dialog(type::folder, title, default_path, {}, options) +{ +} + +inline std::string select_folder::result() +{ + return string_result(); +} + +#endif // PFD_SKIP_IMPLEMENTATION + +} // namespace pfd + diff --git a/papers/doc/4-instrument/README.md b/papers/doc/4-instrument/README.md index 0b9ec0f4..5592fb4e 100644 --- a/papers/doc/4-instrument/README.md +++ b/papers/doc/4-instrument/README.md @@ -10,7 +10,7 @@ double-click to open the instrument editor. every instrument can be renamed and have its type changed. -depending on the instrument type, there are currently 12 different types of an instrument editor: +depending on the instrument type, there are currently 13 different types of an instrument editor: - [FM synthesis](fm.md) - for use with YM2612, YM2151 and FM block portion of YM2610. - [Standard](standard.md) - for use with NES and Sega Master System's PSG sound source and its derivatives. @@ -22,8 +22,10 @@ depending on the instrument type, there are currently 12 different types of an i - [SAA1099](saa.md) - for use with Philips SAA1099 PSG sound source. - [TIA](tia.md) - for use with Atari 2600 system. - [AY-3-8910](ay8910.md) - for use with AY-3-8910 PSG sound source and SSG portion in YM2610. -- [Amiga/sample](amiga.md) for controlling Amiga and other sample based synthsizers like YM2612's Channel 6 PCM mode, NES channel 5, Sega PCM and PC Engine's sample playback mode. -- [Atari Lynx](lynx.md) - for use with Atari Lynx handheld console. +- [Amiga/sample](amiga.md) for controlling Amiga and other sample based synthsizers like YM2612's Channel 6 PCM mode, NES channel 5, Sega PCM, X1-010 and PC Engine's sample playback mode. +- [Atari Lynx](lynx.md) - for use with Atari Lynx handheld console. +- [VERA](vera.md) - for use with Commander X16 VERA. +- [Seta/Allumer X1-010](x1_010.md) - for use with Wavetable portion in Seta/Allumer X1-010. # macros diff --git a/papers/doc/4-instrument/vera.md b/papers/doc/4-instrument/vera.md new file mode 100644 index 00000000..b577ccd2 --- /dev/null +++ b/papers/doc/4-instrument/vera.md @@ -0,0 +1,8 @@ +# VERA instrument editor + +VERA instrument editor consists of only four macros: + +- [Volume] - volume sequence +- [Arpeggio] - pitch sequence +- [Duty cycle] - pulse duty cycle sequence +- [Waveform] - select the waveform used by instrument diff --git a/papers/doc/4-instrument/x1_010.md b/papers/doc/4-instrument/x1_010.md new file mode 100644 index 00000000..8f3691ee --- /dev/null +++ b/papers/doc/4-instrument/x1_010.md @@ -0,0 +1,11 @@ +# X1-010 instrument editor + +X1-010 instrument editor consists of 7 macros. + +- [Volume] - volume levels sequence +- [Arpeggio]- pitch sequence +- [Waveform] - spicifies wavetables sequence +- [Envelope Mode] - allows shaping an envelope +- [Envelope] - spicifies envelope shape sequence, it's also wavetable. +- [Auto envelope numerator] - sets the envelope to the channel's frequency multiplied by numerator +- [Auto envelope denominator] - ets the envelope to the channel's frequency multiplied by denominator diff --git a/papers/doc/5-wave/README.md b/papers/doc/5-wave/README.md index 00573925..5e234bf6 100644 --- a/papers/doc/5-wave/README.md +++ b/papers/doc/5-wave/README.md @@ -2,4 +2,4 @@ Wavetable synthizers, 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. -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 and WonderSwan can handle max 32 byte waveforms as of now, with 16-level height for GB and WS, and 32-level height for PCE. If larger wave will be defined for these two systems, it will be squashed to fit within the constraints of the system. +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 and WonderSwan 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 and WS, and 32-level height for PCE. If larger wave will be defined for these systems, it will be squashed to fit within the constraints of the system. diff --git a/papers/doc/6-sample/README.md b/papers/doc/6-sample/README.md index 78eb322c..87ddf0f8 100644 --- a/papers/doc/6-sample/README.md +++ b/papers/doc/6-sample/README.md @@ -12,7 +12,8 @@ As of Furnace 0.5.5, the following sound chips have sample support: - PC Engine/TurboGrafx 16/Huc6280 (same conditions as above) - Amiga/Paula (on all channels AND resamplable, but you need to make an instrument with the Amiga format and tie it to a sample first) - Arcade/SEGA PCM (same as above but you don't need to make an instrument for it and you have to use the `20xx` effect command to resample your samples) - - Neo Geo/Neo Geo EXT-Ch2 (on the last 5 channels only and can be resampled the same way as above) + - Neo Geo/Neo Geo EXT-Ch2 (on the last 7 channels only and can be resampled the same way as above) + - Seta/Allumer X1-010 (same as above, and both `1701` and `20xx` effect commands are affected on all 16 channels) Furnace also has a feature where you can make an Amiga formarted instrument on the YM2612 and Huc6280 to resample a sample you have in the module. diff --git a/papers/doc/7-systems/README.md b/papers/doc/7-systems/README.md index a42e0d06..a6648a32 100644 --- a/papers/doc/7-systems/README.md +++ b/papers/doc/7-systems/README.md @@ -18,6 +18,7 @@ this is a list of systems that Furnace supports, including each system's effects - [Atari 2600](tia.md) - [Philips SAA1099](saa1099.md) - [Microchip AY8930](ay8930.md) +- [Seta/Allumer X1-010](x1_010.md) - [WonderSwan](wonderswan.md) Furnace also reads .dmf files with the [Yamaha YMU759](ymu759.md) system, but does not emulate the chip at all. diff --git a/papers/doc/7-systems/ay8910.md b/papers/doc/7-systems/ay8910.md index 3dd1049f..4d13a153 100644 --- a/papers/doc/7-systems/ay8910.md +++ b/papers/doc/7-systems/ay8910.md @@ -4,6 +4,8 @@ this chip was used in several home computers (ZX Spectrum, MSX, Amstrad CPC, Ata the chip's powerful sound comes from the envelope... +AY-3-8914 variant was used in Intellivision, it's basically original AY with 4 level envelope volume per channel and different register format. + # effects - `20xx`: set channel mode. `xx` may be one of the following: diff --git a/papers/doc/7-systems/lynx.md b/papers/doc/7-systems/lynx.md new file mode 100644 index 00000000..2e2f3e3c --- /dev/null +++ b/papers/doc/7-systems/lynx.md @@ -0,0 +1,20 @@ +# Atari Lynx/MIKEY + +The Atari Lynx is a 16 bit handheld console developed by (obviously) Atari Corporation, and initially released in September of 1989, with the worldwide release being in 1990. + +The Lynx, while being an incredible handheld for the time (and a lot more powerful than a Game Boy), unfortunately meant nothing in the end due to the Lynx being a market failiure, and ending up as one of the things that contributed to the downfall of Atari. + +Although the Lynx is still getting (rather impressive) homebrew developed for it, it does not mean that the Lynx is a popular system at all. + +The Atari Lynx's custom sound chip and CPU (MIKEY) is a 6502-based 8 bit CPU running at 16MHz, however this information is generally not useful in the context of Furnace. + +## Sound capabilities + + - The MIKEY has 4 channels of square wave-based sound, which can be modulated with different frequencies (×0, ×1, ×2, ×3, ×4, ×5, ×7, ×10, and ×11) to create wavetable-like results. + - Likewise, when a lot of the modulators are activated, this can provide a "pseudo-white noise"-like effect, whoch can be useful for drums and sound effects. + - The MIKEY also has hard stereo panning capabilities via the `08xx` effect command. + - The MIKEY has four 8-bit DACs (Digital to Analog Converter) — one for each voice — that essentially mean you can play samples on the MIKEY (at the cost of CPU time and memory). + - The MIKEY also has a variety of pitches to choose from, and they go from 32Hz to "above the range of human hearing", according to Atari. + +## Effect commands + - `3xxx`: Load LFSR (0 to FFF). For it to work, duty macro in instrument editor must be set to some value, without it LFSR will not be fed with any bits. diff --git a/papers/doc/7-systems/x1_010.md b/papers/doc/7-systems/x1_010.md new file mode 100644 index 00000000..759d519c --- /dev/null +++ b/papers/doc/7-systems/x1_010.md @@ -0,0 +1,47 @@ +# Seta/Allumer X1-010 + +One of sound chip originally designed by Seta, mainly used at their arcade hardwares at late-80s to early-2000s. +It has 2 output channels, but no known hardware using this feature for stereo sound. +Later hardware paired this with external bankswitching logic, but its logic is not emulated now in current furnace revision. +Allumer one is just rebadged Seta's thing for use in their arcade hardwares. + +It has 16 channels, and all channels can be switchable to PCM sample or wavetable playback mode. +Wavetable needs to paired with envelope, this feature is similar as AY PSG, but its shape are stored at RAM: it means it is user-definable. + +In furnace, this chip is can be configurable for original arcade mono output or stereo output - it simulates early 'incorrect' emulation on some mono hardware, but it is also based on the assumption that each channel is connected to each output. + +# waveform type + +This chip supports 2 type waveforms, needs to paired external 8 KB RAM for use these features: + +One is signed 8 bit mono waveform, it's operated like other wavetable based sound systems. +These are stored at the bottom half of RAM at common case. + +Another one ("Envelope") is 4 bit stereo waveform, it's multiplied with above and calculates final output, Each nibble is used for each output channels. +These are stored at the upper half of RAM at common case. + +Both waveforms are 128 byte fixed size, it's freely allocated at each half of RAM except channel register area: each half can be stored total 32/31 waveforms at once. +In furnace, You can set envelope shape split mode. When it sets, its waveform will be split to left half and right half for each outputs. each max size are 128 bytes, total 256 bytes. + +# effects + +- `10xx`: change wave. +- `11xx`: change envelope shape. (also wavetable) +- `17xx`: toggle PCM mode. +- `20xx`: set PCM frequency. (1 to FF)* +- `22xx`: set envelope mode. + - bit 0 sets whether envelope will affect this channel. + - bit 1 sets whether envelope one-shot mode. when it sets, channel is halted after envelope cycle is ended. + - bit 2 sets whether envelope shape split mode. when it sets, envelope shape will splitted to left half and right half. + - bit 3/5 sets whether the right/left shape will mirror the original one. + - bit 4/6 sets whether the right/left output will mirror the original one. +- `23xx`: set envelope period. +- `25xx`: slide envelope period up. +- `26xx`: slide envelope period down. +- `29xy`: enable auto-envelope mode. + - in this mode the envelope period is set to the channel's notes, multiplied by a fraction. + - `x` is the numerator. + - `y` is the denominator. + - if `x` or `y` are 0 this will disable auto-envelope mode. + +* PCM frequency: 255 step, fomula: `step * (Chip clock / 8192)`; 1.95KHz to 498KHz if Chip clock is 16MHz. diff --git a/papers/format.md b/papers/format.md index 7bc30abe..d5b9c67e 100644 --- a/papers/format.md +++ b/papers/format.md @@ -29,6 +29,9 @@ furthermore, an `or reserved` indicates this field is always present, but is res the format versions are: +- 66: Furnace dev66 +- 65: Furnace dev65 +- 64: Furnace dev64 - 63: Furnace dev63 - 62: Furnace dev62 - 61: Furnace dev61 @@ -203,7 +206,10 @@ size | description 1 | ignore duplicate slides (>=50) or reserved 1 | stop portamento on note off (>=62) or reserved 1 | continuous vibrato (>=62) or reserved - 4 | reserved + 1 | broken DAC mode (>=64) or reserved + 1 | one tick cut (>=65) or reserved + 1 | instrument change allowed during porta (>=66) or reserved + 1 | reserved 4?? | pointers to instruments 4?? | pointers to wavetables 4?? | pointers to samples diff --git a/scripts/release-win32.sh b/scripts/release-win32.sh index d2c086d2..2257a595 100755 --- a/scripts/release-win32.sh +++ b/scripts/release-win32.sh @@ -1,6 +1,6 @@ #!/bin/bash # make Windows release -# this script shall be run from Linux with MinGW installed! +# this script shall be run from Arch Linux with MinGW installed! if [ ! -e /tmp/furnace ]; then ln -s "$PWD" /tmp/furnace || exit 1 @@ -14,7 +14,8 @@ fi cd win32build -i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Werror" -DBUILD_SHARED_LIBS=OFF .. || exit 1 +# 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 .. || exit 1 make -j8 || exit 1 i686-w64-mingw32-strip -s furnace.exe || exit 1 @@ -30,3 +31,7 @@ cp -r ../../papers papers || exit 1 cp -r ../../demos demos || exit 1 zip -r furnace.zip LICENSE.txt furnace.exe README.txt papers demos + +furName=$(git describe --tags | sed "s/v0/0/") + +mv furnace.zip furnace-"$furName"-win32.zip diff --git a/scripts/release-win64.sh b/scripts/release-win64.sh index 69bc602a..db580e09 100755 --- a/scripts/release-win64.sh +++ b/scripts/release-win64.sh @@ -1,6 +1,6 @@ #!/bin/bash # make Windows release -# this script shall be run from Linux with MinGW installed! +# this script shall be run from Arch Linux with MinGW installed! if [ ! -e /tmp/furnace ]; then ln -s "$PWD" /tmp/furnace || exit 1 @@ -14,7 +14,8 @@ fi cd winbuild -x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Werror" .. || exit 1 +# 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 make -j8 || exit 1 x86_64-w64-mingw32-strip -s furnace.exe || exit 1 @@ -30,3 +31,7 @@ cp -r ../../papers papers || exit 1 cp -r ../../demos demos || exit 1 zip -r furnace.zip LICENSE.txt furnace.exe README.txt papers demos + +furName=$(git describe --tags | sed "s/v0/0/") + +mv furnace.zip furnace-"$furName"-win64.zip diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index 8fdb112f..0ec3a295 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -110,6 +110,13 @@ enum DivDispatchCmds { DIV_CMD_QSOUND_ECHO_DELAY, DIV_CMD_QSOUND_ECHO_LEVEL, + DIV_CMD_X1_010_ENVELOPE_SHAPE, + DIV_CMD_X1_010_ENVELOPE_ENABLE, + DIV_CMD_X1_010_ENVELOPE_MODE, + DIV_CMD_X1_010_ENVELOPE_PERIOD, + DIV_CMD_X1_010_ENVELOPE_SLIDE, + DIV_CMD_X1_010_AUTO_ENVELOPE, + DIV_CMD_WS_SWEEP_TIME, DIV_CMD_WS_SWEEP_AMOUNT, diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index d90c9a5b..ca2d99ce 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -41,9 +41,11 @@ #include "platform/pcspkr.h" #include "platform/segapcm.h" #include "platform/qsound.h" +#include "platform/vera.h" +#include "platform/x1_010.h" #include "platform/swan.h" -#include "platform/dummy.h" #include "platform/lynx.h" +#include "platform/dummy.h" #include "../ta-log.h" #include "song.h" @@ -237,7 +239,7 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do ((DivPlatformOPL*)dispatch)->setOPLType(3,true); break; case DIV_SYSTEM_SAA1099: { - int saaCore=eng->getConfInt("saaCore",0); + int saaCore=eng->getConfInt("saaCore",1); if (saaCore<0 || saaCore>2) saaCore=0; dispatch=new DivPlatformSAA1099; ((DivPlatformSAA1099*)dispatch)->setCore((DivSAACores)saaCore); @@ -256,9 +258,15 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_SEGAPCM_COMPAT: dispatch=new DivPlatformSegaPCM; break; + case DIV_SYSTEM_X1_010: + dispatch=new DivPlatformX1_010; + break; case DIV_SYSTEM_SWAN: dispatch=new DivPlatformSwan; break; + case DIV_SYSTEM_VERA: + dispatch=new DivPlatformVERA; + break; default: logW("this system is not supported yet! using dummy platform.\n"); dispatch=new DivPlatformDummy; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 5bf1bd55..655b7f5e 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -19,7 +19,6 @@ #include "dataErrors.h" #include "song.h" -#include #define _USE_MATH_DEFINES #include "engine.h" #include "instrument.h" @@ -547,6 +546,36 @@ void DivEngine::renderSamples() { memPos+=length+16; } qsoundMemLen=memPos+256; + + // step 4: allocate x1-010 pcm samples + if (x1_010Mem==NULL) x1_010Mem=new unsigned char[1048576]; + memset(x1_010Mem,0,1048576); + + memPos=0; + for (int i=0; ilength8+4095)&(~0xfff); + // fit sample bank size to 128KB for Seta 2 external bankswitching logic (not emulated yet!) + if (paddedLen>131072) { + paddedLen=131072; + } + if ((memPos&0xfe0000)!=((memPos+paddedLen)&0xfe0000)) { + memPos=(memPos+0x1ffff)&0xfe0000; + } + if (memPos>=1048576) { + logW("out of X1-010 memory for sample %d!\n",i); + break; + } + if (memPos+paddedLen>=1048576) { + memcpy(x1_010Mem+memPos,s->data8,1048576-memPos); + logW("out of X1-010 memory for sample %d!\n",i); + } else { + memcpy(x1_010Mem+memPos,s->data8,paddedLen); + } + s->offX1_010=memPos; + memPos+=paddedLen; + } + x1_010MemLen=memPos+256; } void DivEngine::createNew(const int* description) { @@ -965,10 +994,14 @@ int DivEngine::getEffectiveSampleRate(int rate) { return 1789773/(1789773/rate); case DIV_SYSTEM_SEGAPCM: case DIV_SYSTEM_SEGAPCM_COMPAT: return (31250*MIN(255,(rate*255/31250)))/255; - case DIV_SYSTEM_QSOUND: + case DIV_SYSTEM_QSOUND: return (24038*MIN(65535,(rate*4096/24038)))/4096; case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_FULL: case DIV_SYSTEM_YM2610_FULL_EXT: case DIV_SYSTEM_YM2610B: case DIV_SYSTEM_YM2610B_EXT: return 18518; + case DIV_SYSTEM_VERA: + return (48828*MIN(128,(rate*128/48828)))/128; + case DIV_SYSTEM_X1_010: + return (31250*MIN(255,(rate*16/31250)))/16; // TODO: support variable clock case default: break; } diff --git a/src/engine/engine.h b/src/engine/engine.h index 670f6125..c2b6c443 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -37,8 +37,8 @@ warnings+=(String("\n")+x); \ } -#define DIV_VERSION "dev63" -#define DIV_ENGINE_VERSION 63 +#define DIV_VERSION "dev66" +#define DIV_ENGINE_VERSION 66 enum DivStatusView { DIV_STATUS_NOTHING=0, @@ -69,7 +69,7 @@ enum DivHaltPositions { struct DivChannelState { std::vector delayed; - int note, oldNote, pitch, portaSpeed, portaNote; + int note, oldNote, lastIns, pitch, portaSpeed, portaNote; int volume, volSpeed, cut, rowDelay, volMax; int delayOrder, delayRow, retrigSpeed, retrigTick; int vibratoDepth, vibratoRate, vibratoPos, vibratoDir, vibratoFine; @@ -80,6 +80,7 @@ struct DivChannelState { DivChannelState(): note(-1), oldNote(-1), + lastIns(-1), pitch(0), portaSpeed(-1), portaNote(-1), @@ -633,6 +634,8 @@ class DivEngine { size_t qsoundAMemLen; unsigned char* dpcmMem; size_t dpcmMemLen; + unsigned char* x1_010Mem; + size_t x1_010MemLen; DivEngine(): output(NULL), diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 0a07ceb1..73013f12 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -140,6 +140,9 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.algMacroBehavior=false; ds.brokenShortcutSlides=false; ds.ignoreDuplicateSlides=true; + ds.brokenDACMode=true; + ds.oneTickCut=false; + ds.newInsTriggersInPorta=true; // 1.1 compat flags if (ds.version>24) { @@ -149,8 +152,8 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { // Neo Geo detune if (ds.system[0]==DIV_SYSTEM_YM2610 || ds.system[0]==DIV_SYSTEM_YM2610_EXT - || ds.system[0]==DIV_SYSTEM_YM2610_FULL || ds.system[0]==DIV_SYSTEM_YM2610_FULL_EXT - || ds.system[0]==DIV_SYSTEM_YM2610B || ds.system[0]==DIV_SYSTEM_YM2610B_EXT) { + || ds.system[0]==DIV_SYSTEM_YM2610_FULL || ds.system[0]==DIV_SYSTEM_YM2610_FULL_EXT + || ds.system[0]==DIV_SYSTEM_YM2610B || ds.system[0]==DIV_SYSTEM_YM2610B_EXT) { ds.tuning=443.23; } @@ -259,8 +262,8 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ins->type=DIV_INS_C64; } if (ds.system[0]==DIV_SYSTEM_YM2610 || ds.system[0]==DIV_SYSTEM_YM2610_EXT - || ds.system[0]==DIV_SYSTEM_YM2610_FULL || ds.system[0]==DIV_SYSTEM_YM2610_FULL_EXT - || ds.system[0]==DIV_SYSTEM_YM2610B || ds.system[0]==DIV_SYSTEM_YM2610B_EXT) { + || ds.system[0]==DIV_SYSTEM_YM2610_FULL || ds.system[0]==DIV_SYSTEM_YM2610_FULL_EXT + || ds.system[0]==DIV_SYSTEM_YM2610B || ds.system[0]==DIV_SYSTEM_YM2610B_EXT) { if (!ins->mode) { ins->type=DIV_INS_AY; } @@ -799,6 +802,15 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (ds.version<62) { ds.stopPortaOnNoteOff=true; } + if (ds.version<64) { + ds.brokenDACMode=false; + } + if (ds.version<65) { + ds.oneTickCut=false; + } + if (ds.version<66) { + ds.newInsTriggersInPorta=false; + } ds.isDMF=false; reader.readS(); // reserved @@ -975,7 +987,22 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { reader.readC(); reader.readC(); } - for (int i=0; i<4; i++) reader.readC(); + if (ds.version>=64) { + ds.brokenDACMode=reader.readC(); + } else { + reader.readC(); + } + if (ds.version>=65) { + ds.oneTickCut=reader.readC(); + } else { + reader.readC(); + } + if (ds.version>=66) { + ds.newInsTriggersInPorta=reader.readC(); + } else { + reader.readC(); + } + for (int i=0; i<1; i++) reader.readC(); } else { for (int i=0; i<20; i++) reader.readC(); } @@ -1746,7 +1773,10 @@ SafeWriter* DivEngine::saveFur() { w->writeC(song.ignoreDuplicateSlides); w->writeC(song.stopPortaOnNoteOff); w->writeC(song.continuousVibrato); - for (int i=0; i<4; i++) { + w->writeC(song.brokenDACMode); + w->writeC(song.oneTickCut); + w->writeC(song.newInsTriggersInPorta); + for (int i=0; i<1; i++) { w->writeC(0); } diff --git a/src/engine/instrument.h b/src/engine/instrument.h index a0d37523..f1885225 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -51,6 +51,9 @@ enum DivInstrumentType { DIV_INS_BEEPER=21, DIV_INS_SWAN=22, DIV_INS_MIKEY=23, + DIV_INS_VERA=24, + DIV_INS_X1_010=25, + DIV_INS_MAX, }; // FM operator structure: diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index 0777fca8..526b3fbd 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -24,7 +24,7 @@ #include #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} -#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(regRemap(a),v); if (dumpWrites) {addWrite(regRemap(a),v);} } #define CHIP_DIVIDER 8 @@ -48,8 +48,28 @@ const char* regCheatSheetAY[]={ NULL }; +const char* regCheatSheetAY8914[]={ + "FreqL_A", "0", + "FreqL_B", "1", + "FreqL_C", "2", + "FreqL_Env", "3", + "FreqH_A", "4", + "FreqH_B", "5", + "FreqH_C", "6", + "FreqH_Env", "7", + "Enable", "8", + "FreqNoise", "9", + "Control_Env", "A", + "Volume_A", "B", + "Volume_B", "C", + "Volume_C", "D", + "PortA", "E", + "PortB", "F", + NULL +}; + const char** DivPlatformAY8910::getRegisterSheet() { - return regCheatSheetAY; + return intellivision?regCheatSheetAY8914:regCheatSheetAY; } const char* DivPlatformAY8910::getEffectName(unsigned char effect) { @@ -92,8 +112,13 @@ void DivPlatformAY8910::acquire(short* bufL, short* bufR, size_t start, size_t l } while (!writes.empty()) { QueuedWrite w=writes.front(); - ay->address_w(w.addr); - ay->data_w(w.val); + if (intellivision) { + ay8914_device* ay8914=(ay8914_device*)ay; + ay8914->write(w.addr,w.val); + } else { + ay->address_w(w.addr); + ay->data_w(w.val); + } regPool[w.addr&0x0f]=w.val; writes.pop(); } @@ -125,6 +150,8 @@ void DivPlatformAY8910::tick() { if (chan[i].outVol<0) chan[i].outVol=0; if (isMuted[i]) { rWrite(0x08+i,0); + } else if (intellivision && (chan[i].psgMode&4)) { + rWrite(0x08+i,(chan[i].outVol&0xc)<<2); } else { rWrite(0x08+i,(chan[i].outVol&15)|((chan[i].psgMode&4)<<2)); } @@ -151,6 +178,8 @@ void DivPlatformAY8910::tick() { chan[i].psgMode=(chan[i].std.wave+1)&7; if (isMuted[i]) { rWrite(0x08+i,0); + } else if (intellivision && (chan[i].psgMode&4)) { + rWrite(0x08+i,(chan[i].outVol&0xc)<<2); } else { rWrite(0x08+i,(chan[i].outVol&15)|((chan[i].psgMode&4)<<2)); } @@ -242,6 +271,8 @@ int DivPlatformAY8910::dispatch(DivCommand c) { chan[c.chan].std.init(ins); if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); + } else if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); } else { rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); } @@ -264,7 +295,13 @@ int DivPlatformAY8910::dispatch(DivCommand c) { if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); } else { - if (chan[c.chan].active) rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); + if (chan[c.chan].active) { + if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); + } else { + rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); + } + } } break; } @@ -317,7 +354,11 @@ int DivPlatformAY8910::dispatch(DivCommand c) { if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); } else if (chan[c.chan].active) { - rWrite(0x08+c.chan,(chan[c.chan].outVol&15)|((chan[c.chan].psgMode&4)<<2)); + if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].outVol&0xc)<<2); + } else { + rWrite(0x08+c.chan,(chan[c.chan].outVol&15)|((chan[c.chan].psgMode&4)<<2)); + } } } break; @@ -334,6 +375,8 @@ int DivPlatformAY8910::dispatch(DivCommand c) { } if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); + } else if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); } else { rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); } @@ -383,6 +426,8 @@ void DivPlatformAY8910::muteChannel(int ch, bool mute) { isMuted[ch]=mute; if (isMuted[ch]) { rWrite(0x08+ch,0); + } else if (intellivision && (chan[ch].psgMode&4)) { + rWrite(0x08+ch,(chan[ch].vol&0xc)<<2); } else { rWrite(0x08+ch,(chan[ch].outVol&15)|((chan[ch].psgMode&4)<<2)); } @@ -508,14 +553,22 @@ void DivPlatformAY8910::setFlags(unsigned int flags) { case 1: ay=new ym2149_device(rate); sunsoft=false; + intellivision=false; break; case 2: ay=new sunsoft_5b_sound_device(rate); sunsoft=true; + intellivision=false; + break; + case 3: + ay=new ay8914_device(rate); + sunsoft=false; + intellivision=true; break; default: ay=new ay8910_device(rate); sunsoft=false; + intellivision=false; break; } ay->device_start(); diff --git a/src/engine/platform/ay.h b/src/engine/platform/ay.h index c2292990..41c3c404 100644 --- a/src/engine/platform/ay.h +++ b/src/engine/platform/ay.h @@ -26,6 +26,10 @@ class DivPlatformAY8910: public DivDispatch { protected: + const unsigned char AY8914RegRemap[16]={ + 0,4,1,5,2,6,9,8,11,12,13,3,7,10,14,15 + }; + inline unsigned char regRemap(unsigned char reg) { return intellivision?AY8914RegRemap[reg&0x0f]:reg&0x0f; } struct Channel { unsigned char freqH, freqL; int freq, baseFreq, note, pitch; @@ -60,7 +64,7 @@ class DivPlatformAY8910: public DivDispatch { int delay; bool extMode; - bool stereo, sunsoft; + bool stereo, sunsoft, intellivision; short oldWrites[16]; short pendingWrites[16]; diff --git a/src/engine/platform/c64.cpp b/src/engine/platform/c64.cpp index 6257daea..49be60fb 100644 --- a/src/engine/platform/c64.cpp +++ b/src/engine/platform/c64.cpp @@ -509,10 +509,17 @@ void DivPlatformC64::setChipModel(bool is6581) { } void DivPlatformC64::setFlags(unsigned int flags) { - if (flags&1) { - rate=COLOR_PAL*2.0/9.0; - } else { - rate=COLOR_NTSC*2.0/7.0; + switch (flags&0xf) { + case 0x0: // NTSC C64 + rate=COLOR_NTSC*2.0/7.0; + break; + case 0x1: // PAL C64 + rate=COLOR_PAL*2.0/9.0; + break; + case 0x2: // SSI 2001 + default: + rate=14318180.0/16.0; + break; } chipClock=rate; } diff --git a/src/engine/platform/gb.cpp b/src/engine/platform/gb.cpp index 942032e3..ea6ad771 100644 --- a/src/engine/platform/gb.cpp +++ b/src/engine/platform/gb.cpp @@ -97,8 +97,12 @@ void DivPlatformGB::updateWave() { if (wt->max<1 || wt->len<1) { rWrite(0x30+i,0); } else { - unsigned char nibble1=15-((wt->data[(i*2)*wt->len/32]*15)/wt->max); - unsigned char nibble2=15-((wt->data[(1+i*2)*wt->len/32]*15)/wt->max); + int nibble1=15-((wt->data[(i*2)*wt->len/32]*15)/wt->max); + int nibble2=15-((wt->data[(1+i*2)*wt->len/32]*15)/wt->max); + if (nibble1<0) nibble1=0; + if (nibble1>15) nibble1=15; + if (nibble2<0) nibble2=0; + if (nibble2>15) nibble2=15; rWrite(0x30+i,(nibble1<<4)|nibble2); } } diff --git a/src/engine/platform/genesis.cpp b/src/engine/platform/genesis.cpp index 16c44ae5..75c54af6 100644 --- a/src/engine/platform/genesis.cpp +++ b/src/engine/platform/genesis.cpp @@ -92,13 +92,16 @@ void DivPlatformGenesis::acquire_nuked(short* bufL, short* bufR, size_t start, s DivSample* s=parent->getSample(dacSample); if (s->samples>0) { if (!isMuted[5]) { - immWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); + urgentWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); } if (++dacPos>=s->samples) { if (s->loopStart>=0 && s->loopStart<=(int)s->samples) { dacPos=s->loopStart; } else { dacSample=-1; + if (parent->song.brokenDACMode) { + rWrite(0x2b,0); + } } } dacPeriod+=MAX(40,dacRate); @@ -118,7 +121,7 @@ void DivPlatformGenesis::acquire_nuked(short* bufL, short* bufR, size_t start, s //printf("write: %x = %.2x\n",w.addr,w.val); lastBusy=0; regPool[w.addr&0x1ff]=w.val; - writes.pop(); + writes.pop_front(); } else { lastBusy++; if (fm.write_busy==0) { @@ -156,13 +159,16 @@ void DivPlatformGenesis::acquire_ymfm(short* bufL, short* bufR, size_t start, si DivSample* s=parent->getSample(dacSample); if (s->samples>0) { if (!isMuted[5]) { - immWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); + urgentWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); } if (++dacPos>=s->samples) { if (s->loopStart>=0 && s->loopStart<=(int)s->samples) { dacPos=s->loopStart; } else { dacSample=-1; + if (parent->song.brokenDACMode) { + rWrite(0x2b,0); + } } } dacPeriod+=MAX(40,dacRate); @@ -178,7 +184,7 @@ void DivPlatformGenesis::acquire_ymfm(short* bufL, short* bufR, size_t start, si fm_ymfm->write(0x0+((w.addr>>8)<<1),w.addr); fm_ymfm->write(0x1+((w.addr>>8)<<1),w.val); regPool[w.addr&0x1ff]=w.val; - writes.pop(); + writes.pop_front(); lastBusy=1; } @@ -460,6 +466,7 @@ int DivPlatformGenesis::dispatch(DivCommand c) { if (dumpWrites) addWrite(0xffff0002,0); break; } else { + rWrite(0x2b,1<<7); if (dumpWrites) addWrite(0xffff0000,dacSample); } dacPos=0; @@ -477,6 +484,7 @@ int DivPlatformGenesis::dispatch(DivCommand c) { if (dumpWrites) addWrite(0xffff0002,0); break; } else { + rWrite(0x2b,1<<7); if (dumpWrites) addWrite(0xffff0000,dacSample); } dacPos=0; @@ -541,6 +549,10 @@ int DivPlatformGenesis::dispatch(DivCommand c) { if (c.chan==5) { dacSample=-1; if (dumpWrites) addWrite(0xffff0002,0); + if (parent->song.brokenDACMode) { + rWrite(0x2b,0); + if (dacMode) break; + } } chan[c.chan].keyOff=true; chan[c.chan].keyOn=false; @@ -770,7 +782,7 @@ int DivPlatformGenesis::getRegisterPoolSize() { } void DivPlatformGenesis::reset() { - while (!writes.empty()) writes.pop(); + while (!writes.empty()) writes.pop_front(); memset(regPool,0,512); if (useYMFM) { fm_ymfm->reset(); diff --git a/src/engine/platform/genesis.h b/src/engine/platform/genesis.h index cd7b0396..095744df 100644 --- a/src/engine/platform/genesis.h +++ b/src/engine/platform/genesis.h @@ -20,7 +20,7 @@ #ifndef _GENESIS_H #define _GENESIS_H #include "../dispatch.h" -#include +#include #include "../../../extern/Nuked-OPN2/ym3438.h" #include "sound/ymfm/ymfm_opn.h" @@ -68,7 +68,7 @@ class DivPlatformGenesis: public DivDispatch { bool addrOrVal; QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v), addrOrVal(false) {} }; - std::queue writes; + std::deque writes; ym3438_t fm; int delay; unsigned char lastBusy; diff --git a/src/engine/platform/genesisshared.h b/src/engine/platform/genesisshared.h index d2402a60..01d45f95 100644 --- a/src/engine/platform/genesisshared.h +++ b/src/engine/platform/genesisshared.h @@ -43,6 +43,7 @@ static int orderedOps[4]={ }; #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} -#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define immWrite(a,v) if (!skipRegisterWrites) {writes.push_back(QueuedWrite(a,v)); if (dumpWrites) {addWrite(a,v);} } +#define urgentWrite(a,v) if (!skipRegisterWrites) {if (writes.front().addrOrVal) {writes.push_back(QueuedWrite(a,v));} else {writes.push_front(QueuedWrite(a,v));}; if (dumpWrites) {addWrite(a,v);} } #include "fmshared_OPN.h" diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 2783ba5d..dc0df8a6 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -82,7 +82,44 @@ static int32_t clamp(int32_t v, int32_t lo, int32_t hi) } const char* regCheatSheetLynx[]={ - "DATA", "0", + "AUDIO0_VOLCNTRL", "20", + "AUDIO0_FEEDBACK", "21", + "AUDIO0_OUTPUT", "22", + "AUDIO0_SHIFT", "23", + "AUDIO0_BACKUP", "24", + "AUDIO0_CONTROL", "25", + "AUDIO0_COUNTER", "26", + "AUDIO0_OTHER", "27", + "AUDIO1_VOLCNTRL", "28", + "AUDIO1_FEEDBACK", "29", + "AUDIO1_OUTPUT", "2a", + "AUDIO1_SHIFT", "2b", + "AUDIO1_BACKUP", "2c", + "AUDIO1_CONTROL", "2d", + "AUDIO1_COUNTER", "2e", + "AUDIO1_OTHER", "2f", + "AUDIO2_VOLCNTRL", "30", + "AUDIO2_FEEDBACK", "31", + "AUDIO2_OUTPUT", "32", + "AUDIO2_SHIFT", "33", + "AUDIO2_BACKUP", "34", + "AUDIO2_CONTROL", "35", + "AUDIO2_COUNTER", "36", + "AUDIO2_OTHER", "37", + "AUDIO3_VOLCNTRL", "38", + "AUDIO3_FEEDBACK", "39", + "AUDIO3_OUTPUT", "3a", + "AUDIO3_SHIFT", "3b", + "AUDIO3_BACKUP", "3c", + "AUDIO3_CONTROL", "3d", + "AUDIO3_COUNTER", "3e", + "AUDIO3_OTHER", "3f", + "ATTENREG0", "40", + "ATTENREG1", "41", + "ATTENREG2", "42", + "ATTENREG3", "43", + "MPAN", "44", + "MSTEREO", "50", NULL }; @@ -198,7 +235,7 @@ int DivPlatformLynx::dispatch(DivCommand c) { } break; case DIV_CMD_PANNING: - chan[c.chan].pan=((c.value&0x0f)<<4)|((c.value&0xf0)>>4); + chan[c.chan].pan=c.value; WRITE_ATTEN(c.chan,chan[c.chan].pan); break; case DIV_CMD_GET_VOLUME: diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index 897d8e84..06fbc725 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -201,6 +201,7 @@ void DivPlatformNES::tick() { } else { chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true)-1; if (chan[i].freq>2047) chan[i].freq=2047; + if (chan[i].freq<0) chan[i].freq=0; } if (chan[i].keyOn) { //rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(ins->gb.soundLen&63))); diff --git a/src/engine/platform/opl.cpp b/src/engine/platform/opl.cpp index d8426553..84e8dd26 100644 --- a/src/engine/platform/opl.cpp +++ b/src/engine/platform/opl.cpp @@ -222,7 +222,7 @@ void DivPlatformOPL::acquire(short* bufL, short* bufR, size_t start, size_t len) } void DivPlatformOPL::tick() { - for (int i=0; i<20; i++) { + for (int i=0; icalcFreq(chan[i].baseFreq,chan[i].pitch,false,octave(chan[i].baseFreq)); if (chan[i].freq>131071) chan[i].freq=131071; @@ -368,12 +380,21 @@ void DivPlatformOPL::tick() { chan[i].freqH=freqt>>8; chan[i].freqL=freqt&0xff; immWrite(chanMap[i]+ADDR_FREQ,chan[i].freqL); + if (chan[i].state.ops==4 && i<6) { + immWrite(chanMap[i+1]+ADDR_FREQ,chan[i].freqL); + } } if (chan[i].keyOn) { immWrite(chanMap[i]+ADDR_FREQH,chan[i].freqH|(0x20)); + if (chan[i].state.ops==4 && i<6) { + immWrite(chanMap[i+1]+ADDR_FREQH,chan[i].freqH|(0x20)); + } chan[i].keyOn=false; } else if (chan[i].freqChanged) { immWrite(chanMap[i]+ADDR_FREQH,chan[i].freqH|(chan[i].active<<5)); + if (chan[i].state.ops==4 && i<6) { + immWrite(chanMap[i+1]+ADDR_FREQH,chan[i].freqH|(chan[i].active<<5)); + } } chan[i].freqChanged=false; } @@ -424,25 +445,42 @@ int DivPlatformOPL::toFreq(int freq) { void DivPlatformOPL::muteChannel(int ch, bool mute) { isMuted[ch]=mute; - /* - for (int j=0; j<4; j++) { - unsigned short baseAddr=chanOffs[ch]|opOffs[j]; - DivInstrumentFM::Operator& op=chan[ch].state.op[j]; + int ops=(slots[3][ch]!=255 && chan[ch].state.ops==4 && oplType==3)?4:2; + chan[ch].fourOp=(ops==4); + update4OpMask=true; + for (int i=0; i>1)&1)|(chan[ch].state.fb<<1)); + } + } else { + rWrite(chanMap[ch]+ADDR_LR_FB_ALG,(chan[ch].state.alg&1)|(chan[ch].state.fb<<1)|((chan[ch].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[ch+1]+ADDR_LR_FB_ALG,((chan[ch].state.alg>>1)&1)|(chan[ch].state.fb<<1)|((chan[ch].pan&3)<<4)); + } + } } int DivPlatformOPL::dispatch(DivCommand c) { + // TODO: drums mode! + if (c.chan>=melodicChans) return 0; switch (c.cmd) { case DIV_CMD_NOTE_ON: { DivInstrument* ins=parent->getIns(chan[c.chan].ins); @@ -456,7 +494,9 @@ int DivPlatformOPL::dispatch(DivCommand c) { chan[c.chan].outVol=chan[c.chan].vol; } if (chan[c.chan].insChanged) { - int ops=(slots[3][c.chan]!=255 && ins->fm.ops==4 && oplType==3)?4:2; + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + chan[c.chan].fourOp=(ops==4); + update4OpMask=true; for (int i=0; i0)<<1)|((c.value>>4)>0); + } + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + if (isMuted[c.chan]) { + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)); + if (ops==4) { + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)); + } + } else { + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + } } //rWrite(chanOffs[c.chan]+ADDR_LRAF,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|(chan[c.chan].state.fms&7)|((chan[c.chan].state.ams&3)<<4)); break; @@ -683,39 +731,46 @@ int DivPlatformOPL::dispatch(DivCommand c) { } void DivPlatformOPL::forceIns() { - /* - for (int i=0; i<20; i++) { - for (int j=0; j<4; j++) { - unsigned short baseAddr=chanOffs[i]|opOffs[j]; - DivInstrumentFM::Operator& op=chan[i].state.op[j]; + for (int i=0; i1) { + rWrite(baseAddr+ADDR_WS,op.ws&((oplType==3)?7:3)); + } } - rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); - rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); - if (chan[i].active) { - chan[i].keyOn=true; - chan[i].freqChanged=true; + + if (isMuted[i]) { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)); + } + } else { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)|((chan[i].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)|((chan[i].pan&3)<<4)); + } } } - if (dacMode) { - rWrite(0x2b,0x80); - } - immWrite(0x22,lfoValue); - */ + update4OpMask=true; } void DivPlatformOPL::toggleRegisterDump(bool enable) { @@ -746,7 +801,7 @@ void DivPlatformOPL::reset() { if (dumpWrites) { addWrite(0xffffffff,0); } - for (int i=0; i<20; i++) { + for (int i=0; imax<1 || wt->len<1) { chWrite(ch,0x06,0); } else { - chWrite(ch,0x06,wt->data[i*wt->len/32]*31/wt->max); + int data=wt->data[i*wt->len/32]*31/wt->max; + if (data<0) data=0; + if (data>31) data=31; + chWrite(ch,0x06,data); } } if (chan[ch].active) { diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index ad112baf..5d12bbb8 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -435,8 +435,8 @@ public: case ATTENREG2: case ATTENREG3: mRegisterPool[8*4+idx] = value; - mAttenuationLeft[idx] = ( value & 0x0f ) << 2; - mAttenuationRight[idx] = ( value & 0xf0 ) >> 2; + mAttenuationRight[idx] = ( value & 0x0f ) << 2; + mAttenuationLeft[idx] = ( value & 0xf0 ) >> 2; break; case MPAN: mPan = value; diff --git a/src/engine/platform/sound/vera_pcm.c b/src/engine/platform/sound/vera_pcm.c new file mode 100644 index 00000000..740f5c82 --- /dev/null +++ b/src/engine/platform/sound/vera_pcm.c @@ -0,0 +1,135 @@ +// Commander X16 Emulator +// Copyright (c) 2020 Frank van den Hoef +// All rights reserved. License: 2-clause BSD + +#include "vera_pcm.h" +#include +#include + +static uint8_t volume_lut[16] = {0, 1, 2, 3, 4, 5, 6, 8, 11, 14, 18, 23, 30, 38, 49, 64}; + +static void +fifo_reset(struct VERA_PCM* pcm) +{ + pcm->fifo_wridx = 0; + pcm->fifo_rdidx = 0; + pcm->fifo_cnt = 0; + memset(pcm->fifo,0,sizeof(pcm->fifo)); +} + +void +pcm_reset(struct VERA_PCM* pcm) +{ + fifo_reset(pcm); + pcm->ctrl = 0; + pcm->rate = 0; + pcm->cur_l = 0; + pcm->cur_r = 0; + pcm->phase = 0; +} + +void +pcm_write_ctrl(struct VERA_PCM* pcm, uint8_t val) +{ + if (val & 0x80) { + fifo_reset(pcm); + } + + pcm->ctrl = val & 0x3F; +} + +uint8_t +pcm_read_ctrl(struct VERA_PCM* pcm) +{ + uint8_t result = pcm->ctrl; + if (pcm->fifo_cnt == sizeof(pcm->fifo)) { + result |= 0x80; + } + return result; +} + +void +pcm_write_rate(struct VERA_PCM* pcm, uint8_t val) +{ + pcm->rate = val; +} + +uint8_t +pcm_read_rate(struct VERA_PCM* pcm) +{ + return pcm->rate; +} + +void +pcm_write_fifo(struct VERA_PCM* pcm, uint8_t val) +{ + if (pcm->fifo_cnt < sizeof(pcm->fifo)) { + pcm->fifo[pcm->fifo_wridx++] = val; + if (pcm->fifo_wridx == sizeof(pcm->fifo)) { + pcm->fifo_wridx = 0; + } + pcm->fifo_cnt++; + } +} + +static uint8_t +read_fifo(struct VERA_PCM* pcm) +{ + if (pcm->fifo_cnt == 0) { + return 0; + } + uint8_t result = pcm->fifo[pcm->fifo_rdidx++]; + if (pcm->fifo_rdidx == sizeof(pcm->fifo)) { + pcm->fifo_rdidx = 0; + } + pcm->fifo_cnt--; + return result; +} + +bool +pcm_is_fifo_almost_empty(struct VERA_PCM* pcm) +{ + return pcm->fifo_cnt < 1024; +} + +void +pcm_render(struct VERA_PCM* pcm, int16_t* buf_l, int16_t* buf_r, unsigned num_samples) +{ + while (num_samples--) { + uint8_t old_phase = pcm->phase; + pcm->phase += pcm->rate; + if ((old_phase & 0x80) != (pcm->phase & 0x80)) { + switch ((pcm->ctrl >> 4) & 3) { + case 0: { // mono 8-bit + pcm->cur_l = (int16_t)read_fifo(pcm) << 8; + pcm->cur_r = pcm->cur_l; + break; + } + case 1: { // stereo 8-bit + pcm->cur_l = read_fifo(pcm) << 8; + pcm->cur_r = read_fifo(pcm) << 8; + break; + } + case 2: { // mono 16-bit + pcm->cur_l = read_fifo(pcm); + pcm->cur_l |= read_fifo(pcm) << 8; + pcm->cur_r = pcm->cur_l; + break; + } + case 3: { // stereo 16-bit + pcm->cur_l = read_fifo(pcm); + pcm->cur_l |= read_fifo(pcm) << 8; + pcm->cur_r = read_fifo(pcm); + pcm->cur_r |= read_fifo(pcm) << 8; + break; + } + } + } + + *(buf_l) += ((int)pcm->cur_l * (int)volume_lut[pcm->ctrl & 0xF]) >> 6; + *(buf_r) += ((int)pcm->cur_r * (int)volume_lut[pcm->ctrl & 0xF]) >> 6; + + buf_l++; + buf_r++; + } +} diff --git a/src/engine/platform/sound/vera_pcm.h b/src/engine/platform/sound/vera_pcm.h new file mode 100644 index 00000000..d9b600d0 --- /dev/null +++ b/src/engine/platform/sound/vera_pcm.h @@ -0,0 +1,31 @@ +// Commander X16 Emulator +// Copyright (c) 2020 Frank van den Hoef +// All rights reserved. License: 2-clause BSD + +#pragma once + +#include +#include + +struct VERA_PCM { + uint8_t fifo[4096 - 1]; // Actual hardware FIFO is 4kB, but you can only use 4095 bytes. + unsigned fifo_wridx; + unsigned fifo_rdidx; + unsigned fifo_cnt; + + uint8_t ctrl; + uint8_t rate; + + int16_t cur_l, cur_r; + uint8_t phase; +}; + + +void pcm_reset(struct VERA_PCM* pcm); +void pcm_write_ctrl(struct VERA_PCM* pcm, uint8_t val); +uint8_t pcm_read_ctrl(struct VERA_PCM* pcm); +void pcm_write_rate(struct VERA_PCM* pcm, uint8_t val); +uint8_t pcm_read_rate(struct VERA_PCM* pcm); +void pcm_write_fifo(struct VERA_PCM* pcm, uint8_t val); +void pcm_render(struct VERA_PCM* pcm, int16_t* buf_l, int16_t* buf_r, unsigned num_samples); +bool pcm_is_fifo_almost_empty(struct VERA_PCM* pcm); diff --git a/src/engine/platform/sound/vera_psg.c b/src/engine/platform/sound/vera_psg.c new file mode 100644 index 00000000..f61bd26a --- /dev/null +++ b/src/engine/platform/sound/vera_psg.c @@ -0,0 +1,106 @@ +// Commander X16 Emulator +// Copyright (c) 2020 Frank van den Hoef +// All rights reserved. License: 2-clause BSD + +#include "vera_psg.h" + +#include +#include + +enum waveform { + WF_PULSE = 0, + WF_SAWTOOTH, + WF_TRIANGLE, + WF_NOISE, +}; + +static uint8_t volume_lut[64] = {0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 7, 7, 7, 8, 8, 9, 9, 10, 11, 11, 12, 13, 14, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 28, 29, 31, 33, 35, 37, 39, 42, 44, 47, 50, 52, 56, 59, 63}; + +void +psg_reset(struct VERA_PSG* psg) +{ + memset(psg->channels, 0, sizeof(psg->channels)); + psg->noiseState=1; + psg->noiseOut=0; +} + +void +psg_writereg(struct VERA_PSG* psg, uint8_t reg, uint8_t val) +{ + reg &= 0x3f; + + int ch = reg / 4; + int idx = reg & 3; + + switch (idx) { + case 0: psg->channels[ch].freq = (psg->channels[ch].freq & 0xFF00) | val; break; + case 1: psg->channels[ch].freq = (psg->channels[ch].freq & 0x00FF) | (val << 8); break; + case 2: { + psg->channels[ch].right = (val & 0x80) != 0; + psg->channels[ch].left = (val & 0x40) != 0; + psg->channels[ch].volume = volume_lut[val & 0x3F]; + break; + } + case 3: { + psg->channels[ch].pw = val & 0x3F; + psg->channels[ch].waveform = val >> 6; + break; + } + } +} + +static inline void +render(struct VERA_PSG* psg, int16_t *left, int16_t *right) +{ + int l = 0; + int r = 0; + // TODO this is a currently speculated noise generation + // as the hardware and sources for it are not out in the public + // and the official emulator just uses rand() + psg->noiseOut=((psg->noiseOut<<1)|(psg->noiseState&1))&63; + psg->noiseState=(psg->noiseState<<1)|(((psg->noiseState>>1)^(psg->noiseState>>2)^(psg->noiseState>>4)^(psg->noiseState>>15))&1); + + for (int i = 0; i < 16; i++) { + struct VERAChannel *ch = &psg->channels[i]; + + unsigned new_phase = (ch->phase + ch->freq) & 0x1FFFF; + if ((ch->phase & 0x10000) != (new_phase & 0x10000)) { + ch->noiseval = psg->noiseOut; + } + ch->phase = new_phase; + + uint8_t v = 0; + switch (ch->waveform) { + case WF_PULSE: v = (ch->phase >> 10) > ch->pw ? 0 : 63; break; + case WF_SAWTOOTH: v = ch->phase >> 11; break; + case WF_TRIANGLE: v = (ch->phase & 0x10000) ? (~(ch->phase >> 10) & 0x3F) : ((ch->phase >> 10) & 0x3F); break; + case WF_NOISE: v = ch->noiseval; break; + } + int8_t sv = (v ^ 0x20); + if (sv & 0x20) { + sv |= 0xC0; + } + + int val = (int)sv * (int)ch->volume; + + if (ch->left) { + l += val; + } + if (ch->right) { + r += val; + } + } + + *left = l; + *right = r; +} + +void +psg_render(struct VERA_PSG* psg, int16_t *bufL, int16_t *bufR, unsigned num_samples) +{ + while (num_samples--) { + render(psg, bufL, bufR); + bufL++; + bufR++; + } +} diff --git a/src/engine/platform/sound/vera_psg.h b/src/engine/platform/sound/vera_psg.h new file mode 100644 index 00000000..7a6a7f01 --- /dev/null +++ b/src/engine/platform/sound/vera_psg.h @@ -0,0 +1,28 @@ +// Commander X16 Emulator +// Copyright (c) 2020 Frank van den Hoef +// All rights reserved. License: 2-clause BSD + +#pragma once + +#include +#include + +struct VERAChannel { + uint16_t freq; + uint8_t volume; + bool left, right; + uint8_t pw; + uint8_t waveform; + + unsigned phase; + uint8_t noiseval; +}; + +struct VERA_PSG { + unsigned int noiseState, noiseOut; + struct VERAChannel channels[16]; +}; + +void psg_reset(struct VERA_PSG* psg); +void psg_writereg(struct VERA_PSG* psg, uint8_t reg, uint8_t val); +void psg_render(struct VERA_PSG* psg, int16_t *bufL, int16_t *bufR, unsigned num_samples); diff --git a/src/engine/platform/sound/x1_010/x1_010.cpp b/src/engine/platform/sound/x1_010/x1_010.cpp new file mode 100644 index 00000000..150928e6 --- /dev/null +++ b/src/engine/platform/sound/x1_010/x1_010.cpp @@ -0,0 +1,224 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holders: cam900 + Seta/Allumer X1-010 Emulation core + + the chip has 16 voices, all voices can be switchable to Wavetable or PCM sample playback mode. + It has also 2 output channels, but no known hardware using this feature for stereo sound. + + Wavetable needs to paired with envelope, it's always enabled and similar as AY PSG's one + but its shape is stored at RAM. + + PCM volume is stored by each register. + + Both volume is 4bit per output. + + Everything except PCM sample is stored at paired 8 bit RAM. + + RAM layout (common case: Address bit 12 is swapped when RAM is shared with CPU) + + ----------------------------- + 0000...007f Voice Registers + + 0000...0007 Voice 0 Register + + Address Bits Description + 7654 3210 + 0 x--- ---- Frequency divider* + ---- -x-- Envelope one-shot mode + ---- --x- Sound format + ---- --0- PCM + ---- --1- Wavetable + ---- ---x Keyon/off + PCM case: + 1 xxxx xxxx Volume (Each nibble is for each output) + + 2 xxxx xxxx Frequency* + + 4 xxxx xxxx Start address / 4096 + + 5 xxxx xxxx 0x100 - (End address / 4096) + Wavetable case: + 1 ---x xxxx Wavetable data select + + 2 xxxx xxxx Frequency LSB* + 3 xxxx xxxx "" MSB + + 4 xxxx xxxx Envelope period (.10 fixed point, Low 8 bit) + + 5 ---x xxxx Envelope shape select (!= 0 : Reserved for Voice registers) + + 0008...000f Voice 1 Register + ... + 0078...007f Voice 15 Register + ----------------------------- + 0080...0fff Envelope shape data (Same as volume; Each nibble is for each output) + + 0080...00ff Envelope shape data 1 + 0100...017f Envelope shape data 2 + ... + 0f80...0fff Envelope shape data 31 + ----------------------------- + 1000...1fff Wavetable data + + 1000...107f Wavetable data 0 + 1080...10ff Wavetable data 1 + ... + 1f80...1fff Wavetable data 31 + ----------------------------- + + * Frequency is 4.4 fixed point for PCM, + 6.10 for Wavetable. + Frequency divider is higher precision or just right shift? + needs verification. +*/ + +#include "x1_010.hpp" + +void x1_010_core::tick() +{ + // reset output + m_out[0] = m_out[1] = 0; + for (int i = 0; i < 16; i++) + { + voice_t &v = m_voice[i]; + v.tick(); + m_out[0] += v.data * v.vol_out[0]; + m_out[1] += v.data * v.vol_out[1]; + } +} + +void x1_010_core::voice_t::tick() +{ + data = vol_out[0] = vol_out[1] = 0; + if (flag.keyon) + { + if (flag.wavetable) // Wavetable + { + // envelope, each nibble is for each output + u8 vol = m_host.m_envelope[(bitfield(end_envshape, 0, 5) << 7) | bitfield(env_acc, 10, 7)]; + vol_out[0] = bitfield(vol, 4, 4); + vol_out[1] = bitfield(vol, 0, 4); + env_acc += start_envfreq; + if (flag.env_oneshot && bitfield(env_acc, 17)) + flag.keyon = false; + else + env_acc = bitfield(env_acc, 0, 17); + // get wavetable data + data = m_host.m_wave[(bitfield(vol_wave, 0, 5) << 7) | bitfield(acc, 10, 7)]; + acc = bitfield(acc + (freq >> flag.div), 0, 17); + } + else // PCM sample + { + // volume register, each nibble is for each output + vol_out[0] = bitfield(vol_wave, 4, 4); + vol_out[1] = bitfield(vol_wave, 0, 4); + // get PCM sample + data = m_host.m_intf.read_byte(bitfield(acc, 4, 20)); + acc += bitfield(freq, 0, 8) >> flag.div; + if ((acc >> 16) > (0xff ^ end_envshape)) + flag.keyon = false; + } + } +} + +u8 x1_010_core::ram_r(u16 offset) +{ + if (offset & 0x1000) // wavetable data + return m_wave[offset & 0xfff]; + else if (offset & 0xf80) // envelope shape data + return m_envelope[offset & 0xfff]; + else // channel register + return m_voice[bitfield(offset, 3, 4)].reg_r(offset & 0x7); +} + +void x1_010_core::ram_w(u16 offset, u8 data) +{ + if (offset & 0x1000) // wavetable data + m_wave[offset & 0xfff] = data; + else if (offset & 0xf80) // envelope shape data + m_envelope[offset & 0xfff] = data; + else // channel register + m_voice[bitfield(offset, 3, 4)].reg_w(offset & 0x7, data); +} + +u8 x1_010_core::voice_t::reg_r(u8 offset) +{ + switch (offset & 0x7) + { + case 0x00: return (flag.div << 7) + | (flag.env_oneshot << 2) + | (flag.wavetable << 1) + | (flag.keyon << 0); + case 0x01: return vol_wave; + case 0x02: return bitfield(freq, 0, 8); + case 0x03: return bitfield(freq, 8, 8); + case 0x04: return start_envfreq; + case 0x05: return end_envshape; + default: break; + } + return 0; +} + +void x1_010_core::voice_t::reg_w(u8 offset, u8 data) +{ + switch (offset & 0x7) + { + case 0x00: + { + const bool prev_keyon = flag.keyon; + flag.div = bitfield(data, 7); + flag.env_oneshot = bitfield(data, 2); + flag.wavetable = bitfield(data, 1); + flag.keyon = bitfield(data, 0); + if (!prev_keyon && flag.keyon) // Key on + { + acc = flag.wavetable ? 0 : (u32(start_envfreq) << 16); + env_acc = 0; + } + break; + } + case 0x01: + vol_wave = data; + break; + case 0x02: + freq = (freq & 0xff00) | data; + break; + case 0x03: + freq = (freq & 0x00ff) | (u16(data) << 8); + break; + case 0x04: + start_envfreq = data; + break; + case 0x05: + end_envshape = data; + break; + default: + break; + } +} + +void x1_010_core::voice_t::reset() +{ + flag.reset(); + vol_wave = 0; + freq = 0; + start_envfreq = 0; + end_envshape = 0; + acc = 0; + env_acc = 0; + data = 0; + vol_out[0] = vol_out[1] = 0; +} + +void x1_010_core::reset() +{ + for (auto & elem : m_voice) + elem.reset(); + + std::fill_n(&m_envelope[0], 0x1000, 0); + std::fill_n(&m_wave[0], 0x1000, 0); + m_out[0] = m_out[1] = 0; +} diff --git a/src/engine/platform/sound/x1_010/x1_010.hpp b/src/engine/platform/sound/x1_010/x1_010.hpp new file mode 100644 index 00000000..d5c429fd --- /dev/null +++ b/src/engine/platform/sound/x1_010/x1_010.hpp @@ -0,0 +1,127 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holders: cam900 + Seta/Allumer X1-010 Emulation core + + See x1_010.cpp for more info. +*/ + +#include +#include + +#ifndef _VGSOUND_EMU_X1_010_HPP +#define _VGSOUND_EMU_X1_010_HPP + +#pragma once + +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef signed char s8; +typedef signed int s32; + +template T bitfield(T in, u8 pos, u8 len = 1) +{ + return (in >> pos) & (len ? (T(1 << len) - 1) : 1); +} + +class x1_010_mem_intf +{ +public: + virtual u8 read_byte(u32 address) { return 0; } +}; + +class x1_010_core +{ + friend class x1_010_mem_intf; +public: + // constructor + x1_010_core(x1_010_mem_intf &intf) + : m_voice{*this,*this,*this,*this, + *this,*this,*this,*this, + *this,*this,*this,*this, + *this,*this,*this,*this} + , m_intf(intf) + { + m_envelope = std::make_unique(0x1000); + m_wave = std::make_unique(0x1000); + + std::fill_n(&m_envelope[0], 0x1000, 0); + std::fill_n(&m_wave[0], 0x1000, 0); + } + + // register accessor + u8 ram_r(u16 offset); + void ram_w(u16 offset, u8 data); + + // getters + s32 output(u8 channel) { return m_out[channel & 1]; } + + // internal state + void reset(); + void tick(); + +private: + // 16 voices in chip + struct voice_t + { + // constructor + voice_t(x1_010_core &host) : m_host(host) {} + + // internal state + void reset(); + void tick(); + + // register accessor + u8 reg_r(u8 offset); + void reg_w(u8 offset, u8 data); + + // registers + x1_010_core &m_host; + struct flag_t + { + u8 div : 1; + u8 env_oneshot : 1; + u8 wavetable : 1; + u8 keyon : 1; + void reset() + { + div = 0; + env_oneshot = 0; + wavetable = 0; + keyon = 0; + } + flag_t() + : div(0) + , env_oneshot(0) + , wavetable(0) + , keyon(0) + { } + }; + flag_t flag; + u8 vol_wave = 0; + u16 freq = 0; + u8 start_envfreq = 0; + u8 end_envshape = 0; + + // internal registers + u32 acc = 0; + u32 env_acc = 0; + s8 data = 0; + u8 vol_out[2] = {0}; + }; + voice_t m_voice[16]; + + // RAM + std::unique_ptr m_envelope = nullptr; + std::unique_ptr m_wave = nullptr; + + // output data + s32 m_out[2] = {0}; + + x1_010_mem_intf &m_intf; +}; + +#endif diff --git a/src/engine/platform/swan.cpp b/src/engine/platform/swan.cpp index 21987180..de623647 100644 --- a/src/engine/platform/swan.cpp +++ b/src/engine/platform/swan.cpp @@ -119,8 +119,12 @@ void DivPlatformSwan::updateWave(int ch) { } } else { for (int i=0; i<16; i++) { - unsigned char nibble1=(wt->data[(i*2)*wt->len/32]*15)/wt->max; - unsigned char nibble2=(wt->data[(1+i*2)*wt->len/32]*15)/wt->max; + int nibble1=(wt->data[(i*2)*wt->len/32]*15)/wt->max; + int nibble2=(wt->data[(1+i*2)*wt->len/32]*15)/wt->max; + if (nibble1<0) nibble1=0; + if (nibble1>15) nibble1=15; + if (nibble2<0) nibble2=0; + if (nibble2>15) nibble2=15; rWrite(addr+i,nibble1|(nibble2<<4)); } } diff --git a/src/engine/platform/vera.cpp b/src/engine/platform/vera.cpp new file mode 100644 index 00000000..e63ac8be --- /dev/null +++ b/src/engine/platform/vera.cpp @@ -0,0 +1,347 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 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. + */ + +#include "vera.h" +#include "../engine.h" +#include +#include + +extern "C" { + #include "sound/vera_psg.h" + #include "sound/vera_pcm.h" +} + +#define rWrite(c,a,d) {regPool[(c)*4+(a)]=(d); psg_writereg(psg,((c)*4+(a)),(d));} +#define rWriteLo(c,a,d) rWrite(c,a,(regPool[(c)*4+(a)]&(~0x3f))|((d)&0x3f)) +#define rWriteHi(c,a,d) rWrite(c,a,(regPool[(c)*4+(a)]&(~0xc0))|(((d)<<6)&0xc0)) +#define rWriteFIFOVol(d) rWrite(16,0,(regPool[64]&(~0x3f))|((d)&0x3f)) + +const char* regCheatSheetVERA[]={ + "CHxFreq", "00+x*4", + "CHxVol", "02+x*4", + "CHxWave", "03+x*4", + + "AUDIO_CTRL", "40", + "AUDIO_RATE", "41", + + NULL +}; + +const char** DivPlatformVERA::getRegisterSheet() { + return regCheatSheetVERA; +} + +const char* DivPlatformVERA::getEffectName(unsigned char effect) { + switch (effect) { + case 0x20: + return "20xx: Change waveform"; + break; + case 0x22: + return "22xx: Set duty cycle (0 to 63)"; + break; + } + return NULL; +} + +// TODO: wire up PCM. +void DivPlatformVERA::acquire(short* bufL, short* bufR, size_t start, size_t len) { + psg_render(psg,bufL+start,bufR+start,len); + pcm_render(pcm,bufL+start,bufR+start,len); +} + +void DivPlatformVERA::reset() { + for (int i=0; i<17; i++) { + chan[i]=Channel(); + } + psg_reset(psg); + pcm_reset(pcm); + memset(regPool,0,66); + for (int i=0; i<16; i++) { + chan[i].vol=63; + chan[i].pan=3; + rWriteHi(i,2,isMuted[i]?0:3); + } + chan[16].vol=15; + chan[16].pan=3; +} + +int DivPlatformVERA::calcNoteFreq(int ch, int note) { + if (ch<16) { + return parent->calcBaseFreq(chipClock,2097152,note,false); + } else { + double off=1.0; + if (chan[ch].pcm.sample>=0 && chan[ch].pcm.samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[ch].pcm.sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=s->centerRate/8363.0; + } + } + return (int)(off*parent->calcBaseFreq(chipClock,65536,note,false)); + } +} + +void DivPlatformVERA::tick() { + for (int i=0; i<16; i++) { + chan[i].std.next(); + if (chan[i].std.hadVol) { + chan[i].outVol=MAX(chan[i].vol+chan[i].std.vol-63,0); + rWriteLo(i,2,chan[i].outVol); + } + if (chan[i].std.hadArp) { + if (!chan[i].inPorta) { + if (chan[i].std.arpMode) { + chan[i].baseFreq=calcNoteFreq(0,chan[i].std.arp); + } else { + chan[i].baseFreq=calcNoteFreq(0,chan[i].note+chan[i].std.arp); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arpMode && chan[i].std.finishedArp) { + chan[i].baseFreq=calcNoteFreq(0,chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.hadDuty) { + rWriteLo(i,3,chan[i].std.duty); + } + if (chan[i].std.hadWave) { + rWriteHi(i,3,chan[i].std.wave); + } + if (chan[i].freqChanged) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,8); + if (chan[i].freq>65535) chan[i].freq=65535; + rWrite(i,0,chan[i].freq&0xff); + rWrite(i,1,(chan[i].freq>>8)&0xff); + chan[i].freqChanged=false; + } + } + // PCM + chan[16].std.next(); + if (chan[16].std.hadVol) { + chan[16].outVol=MAX(chan[16].vol+MIN(chan[16].std.vol/4,15)-15,0); + rWriteFIFOVol(chan[16].outVol&15); + } + if (chan[16].std.hadArp) { + if (!chan[16].inPorta) { + if (chan[16].std.arpMode) { + chan[16].baseFreq=calcNoteFreq(16,chan[16].std.arp); + } else { + chan[16].baseFreq=calcNoteFreq(16,chan[16].note+chan[16].std.arp); + } + } + chan[16].freqChanged=true; + } else { + if (chan[16].std.arpMode && chan[16].std.finishedArp) { + chan[16].baseFreq=calcNoteFreq(16,chan[16].note); + chan[16].freqChanged=true; + } + } + if (chan[16].freqChanged) { + chan[16].freq=parent->calcFreq(chan[16].baseFreq,chan[16].pitch,false,8); + if (chan[16].freq>128) chan[16].freq=128; + rWrite(16,1,chan[16].freq&0xff); + chan[16].freqChanged=false; + } +} + +int DivPlatformVERA::dispatch(DivCommand c) { + int tmp; + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + if(c.chan<16) { + rWriteLo(c.chan,2,chan[c.chan].vol) + } else { + chan[c.chan].pcm.sample=parent->getIns(chan[16].ins)->amiga.initSample; + if (chan[c.chan].pcm.sample<0 || chan[c.chan].pcm.sample>=parent->song.sampleLen) { + chan[c.chan].pcm.sample=-1; + } + chan[16].pcm.pos=0; + rWriteFIFOVol(chan[c.chan].vol); + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=calcNoteFreq(c.chan,c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + break; + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + if(c.chan<16) { + rWriteLo(c.chan,2,0) + } else { + chan[16].pcm.sample=-1; + rWriteFIFOVol(0); + rWrite(16,1,0); + } + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + chan[c.chan].ins=(unsigned char)c.value; + break; + case DIV_CMD_VOLUME: + if (c.chan<16) { + tmp=c.value&0x3f; + chan[c.chan].vol=tmp; + rWriteLo(c.chan,2,tmp); + } else { + tmp=c.value&0x0f; + chan[c.chan].vol=tmp; + rWriteFIFOVol(tmp); + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=calcNoteFreq(c.chan,c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: + chan[c.chan].baseFreq=calcNoteFreq(c.chan,c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_STD_NOISE_MODE: + if (c.chan<16) rWriteLo(c.chan,3,c.value); + break; + case DIV_CMD_WAVE: + if (c.chan<16) rWriteHi(c.chan,3,c.value); + break; + case DIV_CMD_PANNING: { + tmp=0; + tmp|=(c.value&0x10)?1:0; + tmp|=(c.value&0x01)?2:0; + chan[c.chan].pan=tmp&3; + if (c.chan<16) { + rWriteHi(c.chan,2,isMuted[c.chan]?0:chan[c.chan].pan); + } + break; + } + case DIV_CMD_GET_VOLMAX: + if(c.chan<16) { + return 63; + } else { + return 15; + } + break; + case DIV_ALWAYS_SET_VOLUME: + return 0; + break; + default: + break; + } + return 1; +} + +void* DivPlatformVERA::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformVERA::getRegisterPool() { + return regPool; +} + +int DivPlatformVERA::getRegisterPoolSize() { + return 66; +} + +void DivPlatformVERA::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + if (ch<16) { + rWriteHi(ch,2,mute?0:chan[ch].pan); + } +} + +bool DivPlatformVERA::isStereo() { + return true; +} + +void DivPlatformVERA::notifyInsDeletion(void* ins) { + for (int i=0; i<2; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformVERA::poke(unsigned int addr, unsigned short val) { + regPool[addr] = (unsigned char)val; +} + +void DivPlatformVERA::poke(std::vector& wlist) { + for (auto &i: wlist) regPool[i.addr] = (unsigned char)i.val; +} + +int DivPlatformVERA::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + for (int i=0; i<17; i++) { + isMuted[i]=false; + } + parent=p; + psg=new struct VERA_PSG; + pcm=new struct VERA_PCM; + dumpWrites=false; + skipRegisterWrites=false; + chipClock=25000000; + rate=chipClock/512; + reset(); + return 17; +} + +void DivPlatformVERA::quit() { + delete psg; + delete pcm; +} +DivPlatformVERA::~DivPlatformVERA() { +} diff --git a/src/engine/platform/vera.h b/src/engine/platform/vera.h new file mode 100644 index 00000000..e1369301 --- /dev/null +++ b/src/engine/platform/vera.h @@ -0,0 +1,78 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 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 _VERA_H +#define _VERA_H +#include "../dispatch.h" +#include "../instrument.h" +#include "../macroInt.h" + +struct VERA_PSG; +struct VERA_PCM; + +class DivPlatformVERA: public DivDispatch { + protected: + struct Channel { + int freq, baseFreq, pitch, note; + unsigned char ins, pan; + bool active, freqChanged, inPorta; + int vol, outVol; + unsigned accum; + int noiseval; + DivMacroInt std; + + struct PCMChannel { + int sample; + int out_l, out_r; + unsigned pos; + unsigned len; + unsigned char freq; + PCMChannel(): sample(-1), out_l(0), out_r(0), pos(0), len(0), freq(0) {} + } pcm; + Channel(): freq(0), baseFreq(0), pitch(0), note(0), ins(-1), pan(0), active(false), freqChanged(false), inPorta(false), vol(0), outVol(0), accum(0), noiseval(0) {} + }; + Channel chan[17]; + bool isMuted[17]; + unsigned char regPool[66]; + struct VERA_PSG* psg; + struct VERA_PCM* pcm; + + int calcNoteFreq(int ch, int note); + friend void putDispatchChan(void*,int,int); + + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void tick(); + void muteChannel(int ch, bool mute); + void notifyInsDeletion(void* ins); + bool isStereo(); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformVERA(); +}; +#endif diff --git a/src/engine/platform/x1_010.cpp b/src/engine/platform/x1_010.cpp new file mode 100644 index 00000000..8d1338e1 --- /dev/null +++ b/src/engine/platform/x1_010.cpp @@ -0,0 +1,893 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 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. + */ + +#include "x1_010.h" +#include "../engine.h" +#include + +//#define rWrite(a,v) pendingWrites[a]=v; +#define rWrite(a,v) if (!skipRegisterWrites) { x1_010->ram_w(a,v); if (dumpWrites) { addWrite(a,v); } } + +#define chRead(c,a) x1_010->ram_r((c<<3)|(a&7)) +#define chWrite(c,a,v) rWrite((c<<3)|(a&7),v) +#define waveWrite(c,a,v) rWrite(0x1000|(chan[c].waveBank<<11)|(c<<7)|(a&0x7f),(v-128)&0xff) +#define envFill(c,a) rWrite(0x800|(c<<7)|(a&0x7f),(chan[c].lvol<<4)|chan[c].rvol) +#define envWrite(c,a,l,r) rWrite(0x800|(c<<7)|(a&0x7f),(((chan[c].lvol*(l))/15)<<4)|((chan[c].rvol*(r))/15)) + +#define refreshControl(c) chWrite(c,0,chan[c].active?(chan[c].pcm?1:((chan[c].env.flag.envEnable && chan[c].env.flag.envOneshot)?7:3)):0); + +#define CHIP_FREQBASE 4194304 + +const char* regCheatSheetX1_010[]={ + // Channel registers + "Ch00_Control", "0000", + "Ch00_PCMVol_WavSel", "0001", + "Ch00_FreqL", "0002", + "Ch00_FreqH", "0003", + "Ch00_Start_EnvFrq", "0004", + "Ch00_End_EnvSel", "0005", + "Ch01_Control", "0008", + "Ch01_PCMVol_WavSel", "0009", + "Ch01_FreqL", "000A", + "Ch01_FreqH", "000B", + "Ch01_Start_EnvFrq", "000C", + "Ch01_End_EnvSel", "000D", + "Ch02_Control", "0010", + "Ch02_PCMVol_WavSel", "0011", + "Ch02_FreqL", "0012", + "Ch02_FreqH", "0013", + "Ch02_Start_EnvFrq", "0014", + "Ch02_End_EnvSel", "0015", + "Ch03_Control", "0018", + "Ch03_PCMVol_WavSel", "0019", + "Ch03_FreqL", "001A", + "Ch03_FreqH", "001B", + "Ch03_Start_EnvFrq", "001C", + "Ch03_End_EnvSel", "001D", + "Ch04_Control", "0020", + "Ch04_PCMVol_WavSel", "0021", + "Ch04_FreqL", "0022", + "Ch04_FreqH", "0023", + "Ch04_Start_EnvFrq", "0024", + "Ch04_End_EnvSel", "0025", + "Ch05_Control", "0028", + "Ch05_PCMVol_WavSel", "0029", + "Ch05_FreqL", "002A", + "Ch05_FreqH", "002B", + "Ch05_Start_EnvFrq", "002C", + "Ch05_End_EnvSel", "002D", + "Ch06_Control", "0030", + "Ch06_PCMVol_WavSel", "0031", + "Ch06_FreqL", "0032", + "Ch06_FreqH", "0033", + "Ch06_Start_EnvFrq", "0034", + "Ch06_End_EnvSel", "0035", + "Ch07_Control", "0038", + "Ch07_PCMVol_WavSel", "0039", + "Ch07_FreqL", "003A", + "Ch07_FreqH", "003B", + "Ch07_Start_EnvFrq", "003C", + "Ch07_End_EnvSel", "003D", + "Ch08_Control", "0040", + "Ch08_PCMVol_WavSel", "0041", + "Ch08_FreqL", "0042", + "Ch08_FreqH", "0043", + "Ch08_Start_EnvFrq", "0044", + "Ch08_End_EnvSel", "0045", + "Ch09_Control", "0048", + "Ch09_PCMVol_WavSel", "0049", + "Ch09_FreqL", "004A", + "Ch09_FreqH", "004B", + "Ch09_Start_EnvFrq", "004C", + "Ch09_End_EnvSel", "004D", + "Ch10_Control", "0050", + "Ch10_PCMVol_WavSel", "0051", + "Ch10_FreqL", "0052", + "Ch10_FreqH", "0053", + "Ch10_Start_EnvFrq", "0054", + "Ch10_End_EnvSel", "0055", + "Ch11_Control", "0058", + "Ch11_PCMVol_WavSel", "0059", + "Ch11_FreqL", "005A", + "Ch11_FreqH", "005B", + "Ch11_Start_EnvFrq", "005C", + "Ch11_End_EnvSel", "005D", + "Ch12_Control", "0060", + "Ch12_PCMVol_WavSel", "0061", + "Ch12_FreqL", "0062", + "Ch12_FreqH", "0063", + "Ch12_Start_EnvFrq", "0064", + "Ch12_End_EnvSel", "0065", + "Ch13_Control", "0068", + "Ch13_PCMVol_WavSel", "0069", + "Ch13_FreqL", "006A", + "Ch13_FreqH", "006B", + "Ch13_Start_EnvFrq", "006C", + "Ch13_End_EnvSel", "006D", + "Ch14_Control", "0070", + "Ch14_PCMVol_WavSel", "0071", + "Ch14_FreqL", "0072", + "Ch14_FreqH", "0073", + "Ch14_Start_EnvFrq", "0074", + "Ch14_End_EnvSel", "0075", + "Ch15_Control", "0078", + "Ch15_PCMVol_WavSel", "0079", + "Ch15_FreqL", "007A", + "Ch15_FreqH", "007B", + "Ch15_Start_EnvFrq", "007C", + "Ch15_End_EnvSel", "007D", + // Envelope data + "Env01Data", "0080", + "Env02Data", "0100", + "Env03Data", "0180", + "Env04Data", "0200", + "Env05Data", "0280", + "Env06Data", "0300", + "Env07Data", "0380", + "Env08Data", "0400", + "Env09Data", "0480", + "Env10Data", "0500", + "Env11Data", "0580", + "Env12Data", "0600", + "Env13Data", "0680", + "Env14Data", "0700", + "Env15Data", "0780", + "Env16Data", "0800", + "Env17Data", "0880", + "Env18Data", "0900", + "Env19Data", "0980", + "Env20Data", "0A00", + "Env21Data", "0A80", + "Env22Data", "0B00", + "Env23Data", "0B80", + "Env24Data", "0C00", + "Env25Data", "0C80", + "Env26Data", "0D00", + "Env27Data", "0D80", + "Env28Data", "0E00", + "Env29Data", "0E80", + "Env30Data", "0F00", + "Env31Data", "0F80", + // Wavetable data + "Wave00Data", "1000", + "Wave01Data", "1080", + "Wave02Data", "1100", + "Wave03Data", "1180", + "Wave04Data", "1200", + "Wave05Data", "1280", + "Wave06Data", "1300", + "Wave07Data", "1380", + "Wave08Data", "1400", + "Wave09Data", "1480", + "Wave10Data", "1500", + "Wave11Data", "1580", + "Wave12Data", "1600", + "Wave13Data", "1680", + "Wave14Data", "1700", + "Wave15Data", "1780", + "Wave16Data", "1800", + "Wave17Data", "1880", + "Wave18Data", "1900", + "Wave19Data", "1980", + "Wave20Data", "1A00", + "Wave21Data", "1A80", + "Wave22Data", "1B00", + "Wave23Data", "1B80", + "Wave24Data", "1C00", + "Wave25Data", "1C80", + "Wave26Data", "1D00", + "Wave27Data", "1D80", + "Wave28Data", "1E00", + "Wave29Data", "1E80", + "Wave30Data", "1F00", + "Wave31Data", "1F80", + NULL +}; + +const char** DivPlatformX1_010::getRegisterSheet() { + return regCheatSheetX1_010; +} + +const char* DivPlatformX1_010::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Change waveform"; + break; + case 0x11: + return "11xx: Change envelope shape"; + break; + case 0x17: + return "17xx: Toggle PCM mode"; + break; + case 0x20: + return "20xx: Set PCM frequency (1 to FF)"; + break; + case 0x22: + return "22xx: Set envelope mode (bit 0: enable, bit 1: one-shot, bit 2: split shape to L/R, bit 3/5: H.invert right/left, bit 4/6: V.invert right/left)"; + break; + case 0x23: + return "23xx: Set envelope period"; + break; + case 0x25: + return "25xx: Envelope slide up"; + break; + case 0x26: + return "26xx: Envelope slide down"; + break; + case 0x29: + return "29xy: Set auto-envelope (x: numerator; y: denominator)"; + break; + } + return NULL; +} + +void DivPlatformX1_010::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t h=start; htick(); + + signed int tempL=x1_010->output(0); + signed int tempR=x1_010->output(1); + + if (tempL<-32768) tempL=-32768; + if (tempL>32767) tempL=32767; + if (tempR<-32768) tempR=-32768; + if (tempR>32767) tempR=32767; + + //printf("tempL: %d tempR: %d\n",tempL,tempR); + bufL[h]=stereo?tempL:((tempL+tempR)>>1); + bufR[h]=stereo?tempR:bufL[h]; + } +} + +double DivPlatformX1_010::NoteX1_010(int ch, int note) { + if (chan[ch].pcm) { // PCM note + double off=1.0; + int sample=chan[ch].sample; + if (sample>=0 && samplesong.sampleLen) { + DivSample* s=parent->getSample(sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=s->centerRate/8363.0; + } + } + return off*parent->calcBaseFreq(chipClock,8192,note,false); + } + // Wavetable note + return NOTE_FREQUENCY(note); +} + +void DivPlatformX1_010::updateWave(int ch) { + DivWavetable* wt=parent->getWave(chan[ch].wave); + if (chan[ch].active) { + chan[ch].waveBank ^= 1; + } + for (int i=0; i<128; i++) { + if (wt->max<1 || wt->len<1) { + waveWrite(ch,i,0); + } else { + int data=wt->data[i*wt->len/128]*255/wt->max; + if (data<0) data=0; + if (data>255) data=255; + waveWrite(ch,i,data); + } + } + if (!chan[ch].pcm) { + chWrite(ch,1,(chan[ch].waveBank<<4)|(ch&0xf)); + } +} + +void DivPlatformX1_010::updateEnvelope(int ch) { + if (!chan[ch].pcm) { + if (isMuted[ch]) { + for (int i=0; i<128; i++) { + rWrite(0x800|(ch<<7)|(i&0x7f),0); + } + } else { + if (!chan[ch].env.flag.envEnable) { + for (int i=0; i<128; i++) { + envFill(ch,i); + } + } else { + DivWavetable* wt=parent->getWave(chan[ch].env.shape); + for (int i=0; i<128; i++) { + if (wt->max<1 || wt->len<1) { + envFill(ch,i); + } else if (chan[ch].env.flag.envSplit || chan[ch].env.flag.envHinvR || chan[ch].env.flag.envVinvR || chan[ch].env.flag.envHinvL || chan[ch].env.flag.envVinvL) { // Stereo config + int la=i,ra=i; + int lo,ro; + if (chan[ch].env.flag.envHinvR) { ra=127-i; } // horizontal invert right envelope + if (chan[ch].env.flag.envHinvL) { la=127-i; } // horizontal invert left envelope + if (chan[ch].env.flag.envSplit) { // Split shape to left and right half + lo=wt->data[la*(wt->len/128/2)]*15/wt->max; + ro=wt->data[(ra+128)*(wt->len/128/2)]*15/wt->max; + } else { + lo=wt->data[la*wt->len/128]*15/wt->max; + ro=wt->data[ra*wt->len/128]*15/wt->max; + } + if (chan[ch].env.flag.envVinvR) { ro=15-ro; } // vertical invert right envelope + if (chan[ch].env.flag.envVinvL) { lo=15-lo; } // vertical invert left envelope + if (lo<0) lo=0; + if (lo>15) lo=15; + if (ro<0) ro=0; + if (ro>15) ro=15; + envWrite(ch,i,lo,ro); + } else { + int out=wt->data[i*wt->len/128]*15/wt->max; + if (out<0) out=0; + if (out>15) out=15; + envWrite(ch,i,out,out); + } + } + } + } + chWrite(ch,5,0x10|(ch&0xf)); + } else { + chWrite(ch,1,(chan[ch].lvol<<4)|chan[ch].rvol); + } +} + +void DivPlatformX1_010::tick() { + for (int i=0; i<16; i++) { + chan[i].std.next(); + if (chan[i].std.hadVol) { + signed char macroVol=((chan[i].vol&15)*MIN(chan[i].furnacePCM?64:15,chan[i].std.vol))/(chan[i].furnacePCM?64:15); + if ((!isMuted[i]) && (macroVol!=chan[i].outVol)) { + chan[i].outVol=macroVol; + chan[i].envChanged=true; + } + } + if ((!chan[i].pcm) || chan[i].furnacePCM) { + if (chan[i].std.hadArp) { + if (!chan[i].inPorta) { + if (chan[i].std.arpMode) { + chan[i].baseFreq=NoteX1_010(i,chan[i].std.arp); + } else { + chan[i].baseFreq=NoteX1_010(i,chan[i].note+chan[i].std.arp); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arpMode && chan[i].std.finishedArp) { + chan[i].baseFreq=NoteX1_010(i,chan[i].note); + chan[i].freqChanged=true; + } + } + } + if (chan[i].std.hadWave && !chan[i].pcm) { + if (chan[i].wave!=chan[i].std.wave) { + chan[i].wave=chan[i].std.wave; + if (!chan[i].pcm) { + updateWave(i); + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + } + if (chan[i].std.hadEx1) { + bool nextEnable=(chan[i].std.ex1&1); + if (nextEnable!=(chan[i].env.flag.envEnable)) { + chan[i].env.flag.envEnable=nextEnable; + if (!chan[i].pcm) { + if (!isMuted[i]) { + chan[i].envChanged=true; + } + refreshControl(i); + } + } + bool nextOneshot=(chan[i].std.ex1&2); + if (nextOneshot!=(chan[i].env.flag.envOneshot)) { + chan[i].env.flag.envOneshot=nextOneshot; + if (!chan[i].pcm) { + refreshControl(i); + } + } + bool nextSplit=(chan[i].std.ex1&4); + if (nextSplit!=(chan[i].env.flag.envSplit)) { + chan[i].env.flag.envSplit=nextSplit; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + bool nextHinvR=(chan[i].std.ex1&8); + if (nextHinvR!=(chan[i].env.flag.envHinvR)) { + chan[i].env.flag.envHinvR=nextHinvR; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + bool nextVinvR=(chan[i].std.ex1&16); + if (nextVinvR!=(chan[i].env.flag.envVinvR)) { + chan[i].env.flag.envVinvR=nextVinvR; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + bool nextHinvL=(chan[i].std.ex1&32); + if (nextHinvL!=(chan[i].env.flag.envHinvL)) { + chan[i].env.flag.envHinvL=nextHinvL; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + bool nextVinvL=(chan[i].std.ex1&64); + if (nextVinvL!=(chan[i].env.flag.envVinvL)) { + chan[i].env.flag.envVinvL=nextVinvL; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + } + if (chan[i].std.hadEx2) { + if (chan[i].env.shape!=chan[i].std.ex2) { + chan[i].env.shape=chan[i].std.ex2; + if (!chan[i].pcm) { + if (chan[i].env.flag.envEnable && (!isMuted[i])) { + chan[i].envChanged=true; + } + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + } + if (chan[i].std.hadEx3) { + chan[i].autoEnvNum=chan[i].std.ex3; + if (!chan[i].pcm) { + chan[i].freqChanged=true; + if (!chan[i].std.willAlg) chan[i].autoEnvDen=1; + } + } + if (chan[i].std.hadAlg) { + chan[i].autoEnvDen=chan[i].std.alg; + if (!chan[i].pcm) { + chan[i].freqChanged=true; + if (!chan[i].std.willEx3) chan[i].autoEnvNum=1; + } + } + if (chan[i].envChanged) { + if (!isMuted[i]) { + chan[i].lvol=((chan[i].outVol&0xf)*((chan[i].pan>>4)&0xf))/15; + chan[i].rvol=((chan[i].outVol&0xf)*((chan[i].pan>>0)&0xf))/15; + } + updateEnvelope(i); + chan[i].envChanged=false; + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false); + if (chan[i].pcm) { + if (chan[i].freq<1) chan[i].freq=1; + if (chan[i].freq>255) chan[i].freq=255; + chWrite(i,2,chan[i].freq&0xff); + } else { + if (chan[i].freq>65535) chan[i].freq=65535; + chWrite(i,2,chan[i].freq&0xff); + chWrite(i,3,(chan[i].freq>>8)&0xff); + if (chan[i].freqChanged && chan[i].autoEnvNum>0 && chan[i].autoEnvDen>0) { + chan[i].env.period=(chan[i].freq*chan[i].autoEnvDen/chan[i].autoEnvNum)>>12; + chWrite(i,4,chan[i].env.period); + } + } + if (chan[i].keyOn || chan[i].keyOff || (chRead(i,0)&1)) { + refreshControl(i); + } + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + if (chan[i].env.slide!=0) { + chan[i].env.slidefrac+=chan[i].env.slide; + while (chan[i].env.slidefrac>0xf) { + chan[i].env.slidefrac-=0x10; + if (chan[i].env.period<0xff) { + chan[i].env.period++; + if (!chan[i].pcm) { + chWrite(i,4,chan[i].env.period); + } + } + } + while (chan[i].env.slidefrac<-0xf) { + chan[i].env.slidefrac+=0x10; + if (chan[i].env.period>0) { + chan[i].env.period--; + if (!chan[i].pcm) { + chWrite(i,4,chan[i].env.period); + } + } + } + } + } +} + +int DivPlatformX1_010::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + chWrite(c.chan,0,0); // reset previous note + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if ((ins->type==DIV_INS_AMIGA) || chan[c.chan].pcm) { + if (ins->type==DIV_INS_AMIGA) { + chan[c.chan].furnacePCM=true; + } else { + chan[c.chan].furnacePCM=false; + } + if (skipRegisterWrites) break; + if (chan[c.chan].furnacePCM) { + chan[c.chan].pcm=true; + chan[c.chan].std.init(ins); + chan[c.chan].sample=ins->amiga.initSample; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + chWrite(c.chan,4,(s->offX1_010>>12)&0xff); + int end=(s->offX1_010+s->length8+0xfff)&~0xfff; // padded + chWrite(c.chan,5,(0x100-(end>>12))&0xff); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NoteX1_010(c.chan,chan[c.chan].note); + chan[c.chan].freqChanged=true; + } + } else { + chan[c.chan].std.init(NULL); + chan[c.chan].outVol=chan[c.chan].vol; + if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + chWrite(c.chan,0,0); // reset + chWrite(c.chan,1,0); + chWrite(c.chan,2,0); + chWrite(c.chan,4,0); + chWrite(c.chan,5,0); + break; + } + } + } else { + chan[c.chan].std.init(NULL); + chan[c.chan].outVol=chan[c.chan].vol; + if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + chWrite(c.chan,0,0); // reset + chWrite(c.chan,1,0); + chWrite(c.chan,2,0); + chWrite(c.chan,4,0); + chWrite(c.chan,5,0); + break; + } + DivSample* s=parent->getSample(12*sampleBank+c.value%12); + chWrite(c.chan,4,(s->offX1_010>>12)&0xff); + int end=(s->offX1_010+s->length8+0xfff)&~0xfff; // padded + chWrite(c.chan,5,(0x100-(end>>12))&0xff); + chan[c.chan].baseFreq=(((unsigned int)s->rate)<<4)/(chipClock/512); + chan[c.chan].freqChanged=true; + } + } else if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NoteX1_010(c.chan,chan[c.chan].note); + chan[c.chan].freqChanged=true; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].envChanged=true; + chan[c.chan].std.init(ins); + refreshControl(c.chan); + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].pcm=false; + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.hasVol) { + if (chan[c.chan].outVol!=c.value) { + chan[c.chan].outVol=c.value; + if (!isMuted[c.chan]) { + chan[c.chan].envChanged=true; + } + } + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.hasVol) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + chan[c.chan].wave=c.value; + updateWave(c.chan); + chan[c.chan].keyOn=true; + break; + case DIV_CMD_X1_010_ENVELOPE_SHAPE: + if (chan[c.chan].env.shape!=c.value) { + chan[c.chan].env.shape=c.value; + if (!chan[c.chan].pcm) { + if (chan[c.chan].env.flag.envEnable && (!isMuted[c.chan])) { + chan[c.chan].envChanged=true; + } + chan[c.chan].keyOn=true; + } + } + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NoteX1_010(c.chan,c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_SAMPLE_MODE: + if (chan[c.chan].pcm!=(c.value&1)) { + chan[c.chan].pcm=c.value&1; + chan[c.chan].freqChanged=true; + if (!isMuted[c.chan]) { + chan[c.chan].envChanged=true; + } + } + break; + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + break; + case DIV_CMD_PANNING: { + if (chan[c.chan].pan!=c.value) { + chan[c.chan].pan=c.value; + if (!isMuted[c.chan]) { + chan[c.chan].envChanged=true; + } + } + break; + } + case DIV_CMD_LEGATO: + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NoteX1_010(c.chan,chan[c.chan].note+((chan[c.chan].std.willArp&&!chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_SAMPLE_FREQ: + if (chan[c.chan].pcm) { + chan[c.chan].freq=MAX(1,c.value&0xff); + chWrite(c.chan,2,chan[c.chan].freq&0xff); + if (chRead(c.chan,0)&1) { + refreshControl(c.chan); + } + } + break; + case DIV_CMD_X1_010_ENVELOPE_MODE: { + bool nextEnable=c.value&1; + if (nextEnable!=(chan[c.chan].env.flag.envEnable)) { + chan[c.chan].env.flag.envEnable=nextEnable; + if (!chan[c.chan].pcm) { + if (!isMuted[c.chan]) { + chan[c.chan].envChanged=true; + } + refreshControl(c.chan); + } + } + bool nextOneshot=c.value&2; + if (nextOneshot!=(chan[c.chan].env.flag.envOneshot)) { + chan[c.chan].env.flag.envOneshot=nextOneshot; + if (!chan[c.chan].pcm) { + refreshControl(c.chan); + } + } + bool nextSplit=c.value&4; + if (nextSplit!=(chan[c.chan].env.flag.envSplit)) { + chan[c.chan].env.flag.envSplit=nextSplit; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + bool nextHinvR=c.value&8; + if (nextHinvR!=(chan[c.chan].env.flag.envHinvR)) { + chan[c.chan].env.flag.envHinvR=nextHinvR; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + bool nextVinvR=c.value&16; + if (nextVinvR!=(chan[c.chan].env.flag.envVinvR)) { + chan[c.chan].env.flag.envVinvR=nextVinvR; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + bool nextHinvL=c.value&32; + if (nextHinvL!=(chan[c.chan].env.flag.envHinvL)) { + chan[c.chan].env.flag.envHinvL=nextHinvL; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + bool nextVinvL=c.value&64; + if (nextVinvL!=(chan[c.chan].env.flag.envVinvL)) { + chan[c.chan].env.flag.envVinvL=nextVinvL; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + break; + } + case DIV_CMD_X1_010_ENVELOPE_PERIOD: + chan[c.chan].env.period=c.value; + if (!chan[c.chan].pcm) { + chWrite(c.chan,4,chan[c.chan].env.period); + } + break; + case DIV_CMD_X1_010_ENVELOPE_SLIDE: + chan[c.chan].env.slide=c.value; + break; + case DIV_CMD_X1_010_AUTO_ENVELOPE: + chan[c.chan].autoEnvNum=c.value>>4; + chan[c.chan].autoEnvDen=c.value&15; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformX1_010::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + chan[ch].envChanged=true; +} + +void DivPlatformX1_010::forceIns() { + for (int i=0; i<16; i++) { + chan[i].insChanged=true; + chan[i].envChanged=true; + chan[i].freqChanged=true; + updateWave(i); + } +} + +void* DivPlatformX1_010::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformX1_010::getRegisterPool() { + for (int i=0; i<0x2000; i++) { + regPool[i]=x1_010->ram_r(i); + } + return regPool; +} + +int DivPlatformX1_010::getRegisterPoolSize() { + return 0x2000; +} + +void DivPlatformX1_010::reset() { + memset(regPool,0,0x2000); + for (int i=0; i<16; i++) { + chan[i]=DivPlatformX1_010::Channel(); + chan[i].reset(); + } + x1_010->reset(); + sampleBank=0; + // set per-channel initial panning + for (int i=0; i<16; i++) { + chWrite(i,0,0); + } +} + +bool DivPlatformX1_010::isStereo() { + return stereo; +} + +bool DivPlatformX1_010::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformX1_010::notifyWaveChange(int wave) { + for (int i=0; i<16; i++) { + if (chan[i].wave==wave) { + updateWave(i); + } + } +} + +void DivPlatformX1_010::notifyInsDeletion(void* ins) { + for (int i=0; i<16; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformX1_010::setFlags(unsigned int flags) { + switch (flags&15) { + case 0: // 16MHz (earlier hardwares) + chipClock=16000000; + break; + case 1: // 16.67MHz (later hardwares) + chipClock=50000000.0/3.0; + break; + // Other clock is used? + } + rate=chipClock/512; + stereo=flags&16; +} + +void DivPlatformX1_010::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformX1_010::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformX1_010::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + stereo=false; + for (int i=0; i<16; i++) { + isMuted[i]=false; + } + setFlags(flags); + intf.parent=parent; + x1_010=new x1_010_core(intf); + x1_010->reset(); + reset(); + return 16; +} + +void DivPlatformX1_010::quit() { + delete x1_010; +} + +DivPlatformX1_010::~DivPlatformX1_010() { +} diff --git a/src/engine/platform/x1_010.h b/src/engine/platform/x1_010.h new file mode 100644 index 00000000..73cbaf6b --- /dev/null +++ b/src/engine/platform/x1_010.h @@ -0,0 +1,144 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 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 _X1_010_H +#define _X1_010_H + +#include +#include "../dispatch.h" +#include "../engine.h" +#include "../macroInt.h" +#include "sound/x1_010/x1_010.hpp" + +class DivX1_010Interface: public x1_010_mem_intf { + public: + DivEngine* parent; + int sampleBank; + virtual u8 read_byte(u32 address) override { + if (parent->x1_010Mem==NULL) return 0; + return parent->x1_010Mem[address & 0xfffff]; + } + DivX1_010Interface(): parent(NULL), sampleBank(0) {} +}; + +class DivPlatformX1_010: public DivDispatch { + struct Channel { + struct Envelope { + struct EnvFlag { + unsigned char envEnable : 1; + unsigned char envOneshot : 1; + unsigned char envSplit : 1; + unsigned char envHinvR : 1; + unsigned char envVinvR : 1; + unsigned char envHinvL : 1; + unsigned char envVinvL : 1; + void reset() { + envEnable=0; + envOneshot=0; + envSplit=0; + envHinvR=0; + envVinvR=0; + envHinvL=0; + envVinvL=0; + } + EnvFlag(): + envEnable(0), + envOneshot(0), + envSplit(0), + envHinvR(0), + envVinvR(0), + envHinvL(0), + envVinvL(0) {} + }; + int shape, period, slide, slidefrac; + EnvFlag flag; + void reset() { + shape=-1; + period=0; + flag.reset(); + } + Envelope(): + shape(-1), + period(0), + slide(0), + slidefrac(0) {} + }; + int freq, baseFreq, pitch, note; + int wave, sample, ins; + unsigned char pan, autoEnvNum, autoEnvDen; + bool active, insChanged, envChanged, freqChanged, keyOn, keyOff, inPorta, furnacePCM, pcm; + int vol, outVol, lvol, rvol; + unsigned char waveBank; + Envelope env; + DivMacroInt std; + void reset() { + freq = baseFreq = pitch = note = 0; + wave = sample = ins = -1; + pan = 255; + autoEnvNum = autoEnvDen = 0; + active = false; + insChanged = envChanged = freqChanged = true; + keyOn = keyOff = inPorta = furnacePCM = pcm = false; + vol = outVol = lvol = rvol = 15; + waveBank = 0; + } + Channel(): + freq(0), baseFreq(0), pitch(0), note(0), + wave(-1), sample(-1), ins(-1), + pan(255), autoEnvNum(0), autoEnvDen(0), + active(false), insChanged(true), envChanged(true), freqChanged(false), keyOn(false), keyOff(false), inPorta(false), furnacePCM(false), pcm(false), + vol(15), outVol(15), lvol(15), rvol(15), + waveBank(0) {} + }; + Channel chan[16]; + bool isMuted[16]; + bool stereo=false; + unsigned char sampleBank; + DivX1_010Interface intf; + x1_010_core* x1_010; + unsigned char regPool[0x2000]; + double NoteX1_010(int ch, int note); + void updateWave(int ch); + void updateEnvelope(int ch); + friend void putDispatchChan(void*,int,int); + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool isStereo(); + bool keyOffAffectsArp(int ch); + void setFlags(unsigned int flags); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformX1_010(); +}; + +#endif diff --git a/src/engine/platform/ym2610.cpp b/src/engine/platform/ym2610.cpp index 1bd460fa..8d762663 100644 --- a/src/engine/platform/ym2610.cpp +++ b/src/engine/platform/ym2610.cpp @@ -313,11 +313,22 @@ const char* DivPlatformYM2610::getEffectName(unsigned char effect) { return NULL; } +double DivPlatformYM2610::NOTE_OPNB(int ch, int note) { + if (ch>6) { // ADPCM + return NOTE_ADPCMB(note); + } else if (ch>3) { // PSG + return NOTE_PERIODIC(note); + } + // FM + return NOTE_FREQUENCY(note); +} + double DivPlatformYM2610::NOTE_ADPCMB(int note) { - DivInstrument* ins=parent->getIns(chan[13].ins); - if (ins->type!=DIV_INS_AMIGA) return 0; - double off=(double)(parent->getSample(ins->amiga.initSample)->centerRate)/8363.0; - return off*parent->calcBaseFreq((double)chipClock/144,65535,note,false); + if (chan[13].sample>=0 && chan[13].samplesong.sampleLen) { + double off=(double)(parent->getSample(chan[13].sample)->centerRate)/8363.0; + return off*parent->calcBaseFreq((double)chipClock/144,65535,note,false); + } + return 0; } void DivPlatformYM2610::acquire(short* bufL, short* bufR, size_t start, size_t len) { @@ -691,22 +702,33 @@ int DivPlatformYM2610::dispatch(DivCommand c) { chan[c.chan].outVol=chan[c.chan].vol; immWrite(0x1b,chan[c.chan].outVol); } - DivSample* s=parent->getSample(ins->amiga.initSample); - immWrite(0x12,(s->offB>>8)&0xff); - immWrite(0x13,s->offB>>16); - int end=s->offB+s->lengthB-1; - immWrite(0x14,(end>>8)&0xff); - immWrite(0x15,end>>16); - immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); - immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat - if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].note=c.value; - chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); - chan[c.chan].freqChanged=true; + chan[c.chan].sample=ins->amiga.initSample; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + immWrite(0x12,(s->offB>>8)&0xff); + immWrite(0x13,s->offB>>16); + int end=s->offB+s->lengthB-1; + immWrite(0x14,(end>>8)&0xff); + immWrite(0x15,end>>16); + immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); + immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); + chan[c.chan].freqChanged=true; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + } else { + immWrite(0x10,0x01); // reset + immWrite(0x12,0); + immWrite(0x13,0); + immWrite(0x14,0); + immWrite(0x15,0); + break; } - chan[c.chan].active=true; - chan[c.chan].keyOn=true; } else { + chan[c.chan].sample=-1; chan[c.chan].std.init(NULL); chan[c.chan].outVol=chan[c.chan].vol; if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { @@ -915,8 +937,8 @@ int DivPlatformYM2610::dispatch(DivCommand c) { break; } case DIV_CMD_NOTE_PORTA: { - if (c.chan>3) { // PSG - int destFreq=NOTE_PERIODIC(c.value2); + if (c.chan>3) { // PSG, ADPCM-B + int destFreq=NOTE_OPNB(c.chan,c.value2); bool return2=false; if (destFreq>chan[c.chan].baseFreq) { chan[c.chan].baseFreq+=c.value; @@ -975,11 +997,7 @@ int DivPlatformYM2610::dispatch(DivCommand c) { iface.sampleBank=sampleBank; break; case DIV_CMD_LEGATO: { - if (c.chan>3) { // PSG - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); - } else { - chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); - } + chan[c.chan].baseFreq=NOTE_OPNB(c.chan,c.value); chan[c.chan].freqChanged=true; break; } @@ -1210,11 +1228,6 @@ void DivPlatformYM2610::reset() { } lastBusy=60; - dacMode=0; - dacPeriod=0; - dacPos=0; - dacRate=0; - dacSample=-1; sampleBank=0; ayEnvPeriod=0; ayEnvMode=0; diff --git a/src/engine/platform/ym2610.h b/src/engine/platform/ym2610.h index a0ee8b8d..4e9b81f9 100644 --- a/src/engine/platform/ym2610.h +++ b/src/engine/platform/ym2610.h @@ -47,6 +47,7 @@ class DivPlatformYM2610: public DivDispatch { signed char konCycles; bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM; int vol, outVol; + int sample; unsigned char pan; DivMacroInt std; Channel(): @@ -70,6 +71,7 @@ class DivPlatformYM2610: public DivDispatch { furnacePCM(false), vol(0), outVol(15), + sample(-1), pan(3) {} }; Channel chan[14]; @@ -87,11 +89,6 @@ class DivPlatformYM2610: public DivDispatch { unsigned char regPool[512]; unsigned char lastBusy; - bool dacMode; - int dacPeriod; - int dacRate; - int dacPos; - int dacSample; int ayNoiseFreq; unsigned char sampleBank; @@ -108,6 +105,7 @@ class DivPlatformYM2610: public DivDispatch { int octave(int freq); int toFreq(int freq); + double NOTE_OPNB(int ch, int note); double NOTE_ADPCMB(int note); friend void putDispatchChan(void*,int,int); diff --git a/src/engine/platform/ym2610b.cpp b/src/engine/platform/ym2610b.cpp index a6bcf9ac..1d954ebe 100644 --- a/src/engine/platform/ym2610b.cpp +++ b/src/engine/platform/ym2610b.cpp @@ -377,11 +377,22 @@ const char* DivPlatformYM2610B::getEffectName(unsigned char effect) { return NULL; } +double DivPlatformYM2610B::NOTE_OPNB(int ch, int note) { + if (ch>8) { // ADPCM-B + return NOTE_ADPCMB(note); + } else if (ch>5) { // PSG + return NOTE_PERIODIC(note); + } + // FM + return NOTE_FREQUENCY(note); +} + double DivPlatformYM2610B::NOTE_ADPCMB(int note) { - DivInstrument* ins=parent->getIns(chan[15].ins); - if (ins->type!=DIV_INS_AMIGA) return 0; - double off=(double)(parent->getSample(ins->amiga.initSample)->centerRate)/8363.0; - return off*parent->calcBaseFreq((double)chipClock/144,65535,note,false); + if (chan[15].sample>=0 && chan[15].samplesong.sampleLen) { + double off=(double)(parent->getSample(chan[15].sample)->centerRate)/8363.0; + return off*parent->calcBaseFreq((double)chipClock/144,65535,note,false); + } + return 0; } void DivPlatformYM2610B::acquire(short* bufL, short* bufR, size_t start, size_t len) { @@ -752,24 +763,35 @@ int DivPlatformYM2610B::dispatch(DivCommand c) { chan[c.chan].std.init(ins); if (!chan[c.chan].std.willVol) { chan[c.chan].outVol=chan[c.chan].vol; - immWrite(0x1b,chan[c.chan].outVol); + immWrite(0x1b,chan[c.chan].outVol); } - DivSample* s=parent->getSample(ins->amiga.initSample); - immWrite(0x12,(s->offB>>8)&0xff); - immWrite(0x13,s->offB>>16); - int end=s->offB+s->lengthB-1; - immWrite(0x14,(end>>8)&0xff); - immWrite(0x15,end>>16); - immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); - immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat - if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].note=c.value; - chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); - chan[c.chan].freqChanged=true; + chan[c.chan].sample=ins->amiga.initSample; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + immWrite(0x12,(s->offB>>8)&0xff); + immWrite(0x13,s->offB>>16); + int end=s->offB+s->lengthB-1; + immWrite(0x14,(end>>8)&0xff); + immWrite(0x15,end>>16); + immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); + immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); + chan[c.chan].freqChanged=true; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + } else { + immWrite(0x10,0x01); // reset + immWrite(0x12,0); + immWrite(0x13,0); + immWrite(0x14,0); + immWrite(0x15,0); + break; } - chan[c.chan].active=true; - chan[c.chan].keyOn=true; } else { + chan[c.chan].sample=-1; chan[c.chan].std.init(NULL); chan[c.chan].outVol=chan[c.chan].vol; if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { @@ -978,8 +1000,8 @@ int DivPlatformYM2610B::dispatch(DivCommand c) { break; } case DIV_CMD_NOTE_PORTA: { - if (c.chan>5) { // PSG - int destFreq=NOTE_PERIODIC(c.value2); + if (c.chan>5) { // PSG, ADPCM-B + int destFreq=NOTE_OPNB(c.chan,c.value2); bool return2=false; if (destFreq>chan[c.chan].baseFreq) { chan[c.chan].baseFreq+=c.value; @@ -1038,11 +1060,7 @@ int DivPlatformYM2610B::dispatch(DivCommand c) { iface.sampleBank=sampleBank; break; case DIV_CMD_LEGATO: { - if (c.chan>5) { // PSG - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); - } else { - chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); - } + chan[c.chan].baseFreq=NOTE_OPNB(c.chan,c.value); chan[c.chan].freqChanged=true; break; } @@ -1273,11 +1291,6 @@ void DivPlatformYM2610B::reset() { } lastBusy=60; - dacMode=0; - dacPeriod=0; - dacPos=0; - dacRate=0; - dacSample=-1; sampleBank=0; ayEnvPeriod=0; ayEnvMode=0; diff --git a/src/engine/platform/ym2610b.h b/src/engine/platform/ym2610b.h index ca43e36b..99c641a6 100644 --- a/src/engine/platform/ym2610b.h +++ b/src/engine/platform/ym2610b.h @@ -40,6 +40,7 @@ class DivPlatformYM2610B: public DivDispatch { signed char konCycles; bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM; int vol, outVol; + int sample; unsigned char pan; DivMacroInt std; Channel(): @@ -63,6 +64,7 @@ class DivPlatformYM2610B: public DivDispatch { furnacePCM(false), vol(0), outVol(15), + sample(-1), pan(3) {} }; Channel chan[16]; @@ -80,11 +82,6 @@ class DivPlatformYM2610B: public DivDispatch { unsigned char regPool[512]; unsigned char lastBusy; - bool dacMode; - int dacPeriod; - int dacRate; - int dacPos; - int dacSample; int ayNoiseFreq; unsigned char sampleBank; @@ -101,6 +98,7 @@ class DivPlatformYM2610B: public DivDispatch { int octave(int freq); int toFreq(int freq); + double NOTE_OPNB(int ch, int note); double NOTE_ADPCMB(int note); friend void putDispatchChan(void*,int,int); diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index e3313a9d..85c21aac 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -117,6 +117,16 @@ const char* cmdName[DIV_CMD_MAX]={ "QSOUND_ECHO_DELAY", "QSOUND_ECHO_LEVEL", + "X1_010_ENVELOPE_SHAPE", + "X1_010_ENVELOPE_ENABLE", + "X1_010_ENVELOPE_MODE", + "X1_010_ENVELOPE_PERIOD", + "X1_010_ENVELOPE_SLIDE", + "X1_010_AUTO_ENVELOPE", + + "WS_SWEEP_TIME", + "WS_SWEEP_AMOUNT", + "ALWAYS_SET_VOLUME" }; @@ -251,6 +261,21 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe break; } break; + case DIV_SYSTEM_X1_010: + switch (effect) { + case 0x10: // select waveform + dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal)); + break; + case 0x11: // select envelope shape + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_SHAPE,ch,effectVal)); + break; + case 0x17: // PCM enable + dispatchCmd(DivCommand(DIV_CMD_SAMPLE_MODE,ch,(effectVal>0))); + break; + default: + return false; + } + break; case DIV_SYSTEM_SWAN: switch (effect) { case 0x10: // select waveform @@ -272,6 +297,18 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe return false; } break; + case DIV_SYSTEM_VERA: + switch (effect) { + case 0x20: // select waveform + dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal)); + break; + case 0x22: // duty + dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_MODE,ch,effectVal)); + break; + default: + return false; + } + break; default: return false; } @@ -555,6 +592,30 @@ bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char } return false; break; + case DIV_SYSTEM_X1_010: + switch (effect) { + case 0x20: // PCM frequency + dispatchCmd(DivCommand(DIV_CMD_SAMPLE_FREQ,ch,effectVal)); + break; + case 0x22: // envelope mode + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_MODE,ch,effectVal)); + break; + case 0x23: // envelope period + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_PERIOD,ch,effectVal)); + break; + case 0x25: // envelope slide up + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_SLIDE,ch,effectVal)); + break; + case 0x26: // envelope slide down + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_SLIDE,ch,-effectVal)); + break; + case 0x29: // auto-envelope + dispatchCmd(DivCommand(DIV_CMD_X1_010_AUTO_ENVELOPE,ch,effectVal)); + break; + default: + return false; + } + break; default: return false; } @@ -593,6 +654,12 @@ void DivEngine::processRow(int i, bool afterDelay) { // instrument if (pat->data[whatRow][2]!=-1) { dispatchCmd(DivCommand(DIV_CMD_INSTRUMENT,i,pat->data[whatRow][2])); + if (chan[i].lastIns!=pat->data[whatRow][2]) { + chan[i].lastIns=pat->data[whatRow][2]; + if (chan[i].inPorta && song.newInsTriggersInPorta) { + dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,DIV_NOTE_NULL)); + } + } } // note if (pat->data[whatRow][0]==100) { // note off @@ -1052,8 +1119,28 @@ void DivEngine::nextRow() { for (int i=0; idata[curRow][0]==0 && pat->data[curRow][1]==0)) { - if (pat->data[curRow][0]!=100) { - if (!chan[i].legato) dispatchCmd(DivCommand(DIV_CMD_PRE_NOTE,i,ticks)); + if (pat->data[curRow][0]!=100 && pat->data[curRow][0]!=101 && pat->data[curRow][0]!=102) { + if (!chan[i].legato) { + dispatchCmd(DivCommand(DIV_CMD_PRE_NOTE,i,ticks)); + + if (song.oneTickCut) { + bool doPrepareCut=true; + + for (int j=0; jdata[curRow][4+(j<<1)]==0x03) { + doPrepareCut=false; + break; + } + if (pat->data[curRow][4+(j<<1)]==0xea) { + if (pat->data[curRow][5+(j<<1)]>0) { + doPrepareCut=false; + break; + } + } + } + if (doPrepareCut) chan[i].cut=ticks; + } + } } } } diff --git a/src/engine/safeReader.cpp b/src/engine/safeReader.cpp index dcc85fd1..b2b95551 100644 --- a/src/engine/safeReader.cpp +++ b/src/engine/safeReader.cpp @@ -112,9 +112,9 @@ int SafeReader::readI() { int SafeReader::readI_BE() { if (curSeek+4>len) throw EndOfFileException(this,len); - int ret=*(int*)(&buf[curSeek]); + unsigned int ret=*(unsigned int*)(&buf[curSeek]); curSeek+=4; - return (ret>>24)|((ret&0xff0000)>>8)|((ret&0xff00)<<8)|((ret&0xff)<<24); + return (int)((ret>>24)|((ret&0xff0000)>>8)|((ret&0xff00)<<8)|((ret&0xff)<<24)); } int64_t SafeReader::readL() { diff --git a/src/engine/sample.cpp b/src/engine/sample.cpp index ec6313ff..571e3511 100644 --- a/src/engine/sample.cpp +++ b/src/engine/sample.cpp @@ -115,8 +115,9 @@ bool DivSample::initInternal(unsigned char d, int count) { case 8: // 8-bit if (data8!=NULL) delete[] data8; length8=count; - data8=new signed char[length8]; - memset(data8,0,length8); + // for padding X1-010 sample + data8=new signed char[(count+4095)&(~0xfff)]; + memset(data8,0,(count+4095)&(~0xfff)); break; case 9: // BRR if (dataBRR!=NULL) delete[] dataBRR; diff --git a/src/engine/sample.h b/src/engine/sample.h index 2915f2ce..8b6d16b1 100644 --- a/src/engine/sample.h +++ b/src/engine/sample.h @@ -49,7 +49,7 @@ struct DivSample { unsigned int length8, length16, length1, lengthDPCM, lengthQSoundA, lengthA, lengthB, lengthX68, lengthBRR, lengthVOX; unsigned int off8, off16, off1, offDPCM, offQSoundA, offA, offB, offX68, offBRR, offVOX; - unsigned int offSegaPCM, offQSound; + unsigned int offSegaPCM, offQSound, offX1_010; unsigned int samples; diff --git a/src/engine/song.h b/src/engine/song.h index e4bd428b..7fc6ec7f 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -90,8 +90,10 @@ enum DivSystem { DIV_SYSTEM_OPLL_DRUMS, DIV_SYSTEM_LYNX, DIV_SYSTEM_QSOUND, + DIV_SYSTEM_VERA, DIV_SYSTEM_YM2610B_EXT, - DIV_SYSTEM_SEGAPCM_COMPAT + DIV_SYSTEM_SEGAPCM_COMPAT, + DIV_SYSTEM_X1_010 }; struct DivSong { @@ -241,6 +243,13 @@ struct DivSong { // - 2: YM2423 // - 3: VRC7 // - 4: custom (TODO) + // - X1-010: + // - bit 0-3: clock rate + // - 0: 16MHz (Seta 1) + // - 1: 16.67MHz (Seta 2) + // - bit 4: stereo + // - 0: mono + // - 1: stereo unsigned int systemFlags[32]; // song information @@ -286,6 +295,9 @@ struct DivSong { bool ignoreDuplicateSlides; bool stopPortaOnNoteOff; bool continuousVibrato; + bool brokenDACMode; + bool oneTickCut; + bool newInsTriggersInPorta; DivOrders orders; std::vector ins; @@ -350,7 +362,10 @@ struct DivSong { brokenShortcutSlides(false), ignoreDuplicateSlides(false), stopPortaOnNoteOff(false), - continuousVibrato(false) { + continuousVibrato(false), + brokenDACMode(false), + oneTickCut(false), + newInsTriggersInPorta(true) { for (int i=0; i<32; i++) { system[i]=DIV_SYSTEM_NULL; systemVol[i]=64; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index fe45a4e7..5e57f6e3 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -135,6 +135,10 @@ DivSystem DivEngine::systemFromFile(unsigned char val) { return DIV_SYSTEM_LYNX; case 0xa9: return DIV_SYSTEM_SEGAPCM_COMPAT; + case 0xac: + return DIV_SYSTEM_VERA; + case 0xb0: + return DIV_SYSTEM_X1_010; case 0xde: return DIV_SYSTEM_YM2610B_EXT; case 0xe0: @@ -258,6 +262,10 @@ unsigned char DivEngine::systemToFile(DivSystem val) { return 0xa8; case DIV_SYSTEM_SEGAPCM_COMPAT: return 0xa9; + case DIV_SYSTEM_VERA: + return 0xac; + case DIV_SYSTEM_X1_010: + return 0xb0; case DIV_SYSTEM_YM2610B_EXT: return 0xde; case DIV_SYSTEM_QSOUND: @@ -335,7 +343,7 @@ int DivEngine::getChannelCount(DivSystem sys) { case DIV_SYSTEM_OPL3: return 18; case DIV_SYSTEM_MULTIPCM: - return 24; + return 28; case DIV_SYSTEM_PCSPKR: return 1; case DIV_SYSTEM_POKEY: @@ -351,6 +359,7 @@ int DivEngine::getChannelCount(DivSystem sys) { case DIV_SYSTEM_POKEMINI: return 1; case DIV_SYSTEM_SEGAPCM: + case DIV_SYSTEM_X1_010: return 16; case DIV_SYSTEM_VBOY: return 6; @@ -383,6 +392,8 @@ int DivEngine::getChannelCount(DivSystem sys) { case DIV_SYSTEM_YM2610B_EXT: case DIV_SYSTEM_QSOUND: return 19; + case DIV_SYSTEM_VERA: + return 17; } return 0; } @@ -408,10 +419,6 @@ const char* DivEngine::getSongSystemName() { return "Vectrex"; case 5: // AY-3-8910, 1MHz return "Amstrad CPC"; - case 6: // AY-3-8910, 0.somethingMhz - return "Intellivision"; - case 8: // AY-3-8910, 0.somethingMhz - return "Intellivision (PAL)"; case 0x10: // YM2149, 1.79MHz return "MSX"; @@ -421,7 +428,12 @@ const char* DivEngine::getSongSystemName() { return "Sunsoft 5B standalone"; case 0x28: // 5B PAL return "Sunsoft 5B standalone (PAL)"; - + + case 0x30: // AY-3-8914, 1.79MHz + return "Intellivision"; + case 0x33: // AY-3-8914, 2MHz + return "Intellivision (PAL)"; + default: if ((song.systemFlags[0]&0x30)==0x00) { return "AY-3-8910"; @@ -429,6 +441,8 @@ const char* DivEngine::getSongSystemName() { return "Yamaha YM2149"; } else if ((song.systemFlags[0]&0x30)==0x20) { return "Overclocked Sunsoft 5B"; + } else if ((song.systemFlags[0]&0x30)==0x30) { + return "Intellivision"; } } } else if (song.system[0]==DIV_SYSTEM_SMS) { @@ -522,6 +536,10 @@ const char* DivEngine::getSongSystemName() { if (song.system[0]==DIV_SYSTEM_AY8910 && song.system[1]==DIV_SYSTEM_AY8910) { return "Bally Midway MCR"; } + + if (song.system[0]==DIV_SYSTEM_YM2151 && song.system[1]==DIV_SYSTEM_VERA) { + return "Commander X16"; + } break; case 3: break; @@ -650,6 +668,10 @@ const char* DivEngine::getSystemName(DivSystem sys) { return "Taito Arcade Extended Channel 3"; case DIV_SYSTEM_QSOUND: return "Capcom QSound"; + case DIV_SYSTEM_VERA: + return "VERA"; + case DIV_SYSTEM_X1_010: + return "Seta/Allumer X1-010"; } return "Unknown"; } @@ -775,6 +797,10 @@ const char* DivEngine::getSystemChips(DivSystem sys) { return "Yamaha YM2610B Extended Channel 3"; case DIV_SYSTEM_QSOUND: return "Capcom DL-1425"; + case DIV_SYSTEM_VERA: + return "VERA"; + case DIV_SYSTEM_X1_010: + return "Seta/Allumer X1-010"; } return "Unknown"; } @@ -857,7 +883,7 @@ bool DivEngine::isSTDSystem(DivSystem sys) { sys!=DIV_SYSTEM_YM2151); } -const char* chanNames[38][24]={ +const char* chanNames[40][32]={ {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8", "Channel 9", "Channel 10", "Channel 11", "Channel 12", "Channel 13", "Channel 14", "Channel 15", "Channel 16", "PCM"}, // YMU759/SegaPCM {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis {"FM 1", "FM 2", "FM 3 OP1", "FM 3 OP2", "FM 3 OP3", "FM 3 OP4", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis (extended channel 3) @@ -870,7 +896,7 @@ const char* chanNames[38][24]={ {"FM 1", "FM 2", "FM 3", "FM 4", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610 {"FM 1", "FM 2 OP1", "FM 2 OP2", "FM 2 OP3", "FM 2 OP4", "FM 3", "FM 4", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610 (extended channel 2) {"PSG 1", "PSG 2", "PSG 3"}, // AY-3-8910 - {"Channel 1", "Channel 2", "Channel 3", "Channel 4"}, // Amiga/POKEY/Swan/Lynx + {"Channel 1", "Channel 2", "Channel 3", "Channel 4"}, // Amiga/POKEY/Lynx {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8"}, // YM2151/YM2414 {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6"}, // YM2612 {"Channel 1", "Channel 2"}, // TIA @@ -886,7 +912,7 @@ const char* chanNames[38][24]={ {"FM 1", "FM 2", "FM 3", "PSG 1", "PSG 2", "PSG 3"}, // OPN {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Kick", "Snare", "Top", "HiHat", "Tom", "Rim", "ADPCM"}, // PC-98 {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8", "FM 9", "FM 10", "FM 11", "FM 12", "FM 13", "FM 14", "FM 15", "FM 16", "FM 17", "FM 18"}, // OPL3 - {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8", "Channel 9", "Channel 10", "Channel 11", "Channel 12", "Channel 13", "Channel 14", "Channel 15", "Channel 16", "Channel 17", "Channel 18", "Channel 19", "Channel 20", "Channel 21", "Channel 22", "Channel 23", "Channel 24"}, // MultiPCM + {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8", "Channel 9", "Channel 10", "Channel 11", "Channel 12", "Channel 13", "Channel 14", "Channel 15", "Channel 16", "Channel 17", "Channel 18", "Channel 19", "Channel 20", "Channel 21", "Channel 22", "Channel 23", "Channel 24", "Channel 25", "Channel 26", "Channel 27", "Channel 28"}, // MultiPCM {"Square"}, // PC Speaker/Pokémon Mini {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Noise"}, // Virtual Boy/SCC {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610B @@ -896,9 +922,11 @@ const char* chanNames[38][24]={ {"FM 1", "FM 2", "FM 3", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 4-op + drums {"PCM 1", "PCM 2", "PCM 3", "PCM 4", "PCM 5", "PCM 6", "PCM 7", "PCM 8", "PCM 9", "PCM 10", "PCM 11", "PCM 12", "PCM 13", "PCM 14", "PCM 15", "PCM 16", "ADPCM 1", "ADPCM 2", "ADPCM 3"}, // QSound {"FM 1", "FM 2", "FM 3 OP1", "FM 3 OP2", "FM 3 OP3", "FM 3 OP4", "FM 4", "FM 5", "FM 6", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610B (extended channel 3) + {"Wave", "Wave/PCM", "Wave", "Wave/Noise"}, // Swan + {"PSG 1", "PSG 2", "PSG 3", "PSG 4", "PSG 5", "PSG 6", "PSG 7", "PSG 8", "PSG 9", "PSG 10", "PSG 11", "PSG 12", "PSG 13", "PSG 14", "PSG 15", "PSG 16", "PCM"}, // VERA }; -const char* chanShortNames[38][24]={ +const char* chanShortNames[38][32]={ {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "PCM"}, // YMU759 {"F1", "F2", "F3", "F4", "F5", "F6", "S1", "S2", "S3", "NO"}, // Genesis {"F1", "F2", "O1", "O2", "O3", "O4", "F4", "F5", "F6", "S1", "S2", "S3", "S4"}, // Genesis (extended channel 3) @@ -927,7 +955,7 @@ const char* chanShortNames[38][24]={ {"F1", "F2", "F3", "S1", "S2", "S3"}, // OPN {"F1", "F2", "F3", "F4", "F5", "F6", "S1", "S2", "S3", "BD", "SD", "TP", "HH", "TM", "RM", "P"}, // PC-98 {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18"}, // OPL3 - {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24"}, // MultiPCM + {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28"}, // MultiPCM {"SQ"}, // PC Speaker/Pokémon Mini {"CH1", "CH2", "CH3", "CH4", "CH5", "NO"}, // Virtual Boy/SCC {"F1", "F2", "F3", "F4", "F5", "F6", "S1", "S2", "S3", "P1", "P2", "P3", "P4", "P5", "P6", "B"}, // YM2610B @@ -939,7 +967,7 @@ const char* chanShortNames[38][24]={ {"F1", "F2", "O1", "O2", "O3", "O4", "F4", "F5", "F6", "S1", "S2", "S3", "P1", "P2", "P3", "P4", "P5", "P6", "B"}, // YM2610B (extended channel 3) }; -const int chanTypes[38][24]={ +const int chanTypes[41][32]={ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}, // YMU759 {0, 0, 0, 0, 0, 0, 1, 1, 1, 2}, // Genesis {0, 0, 5, 5, 5, 5, 0, 0, 0, 1, 1, 1, 2}, // Genesis (extended channel 3) @@ -968,7 +996,7 @@ const int chanTypes[38][24]={ {0, 0, 0, 1, 1, 1}, // OPN {0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 4}, // PC-98 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // OPL3 - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, // MultiPCM/QSound + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, // MultiPCM/QSound {1}, // PC Speaker/Pokémon Mini {3, 3, 3, 3, 3, 2}, // Virtual Boy/SCC {0, 0, 0, 0, 0, 0, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4}, // YM2610B @@ -976,11 +1004,14 @@ const int chanTypes[38][24]={ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 drums {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // OPL3 4-op {0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 4-op + drums - {3, 3, 3, 3}, //Lynx + {3, 3, 3, 3}, // Lynx {0, 0, 5, 5, 5, 5, 0, 0, 0, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4}, // YM2610B (extended channel 3) + {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4}, // VERA + {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}, // X1-010 + {3, 4, 3, 2}, // Swan }; -const DivInstrumentType chanPrefType[44][24]={ +const DivInstrumentType chanPrefType[46][28]={ {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM}, // YMU759 {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis (extended channel 3) @@ -1009,7 +1040,7 @@ const DivInstrumentType chanPrefType[44][24]={ {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_AY, DIV_INS_AY, DIV_INS_AY}, // OPN {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_AY, DIV_INS_AY, DIV_INS_AY, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // PC-98 {DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL}, // OPL/OPL2/OPL3 - {DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // MultiPCM/QSound + {DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // MultiPCM/QSound {DIV_INS_STD}, // PC Speaker/Pokémon Mini {DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY}, // Virtual Boy {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_AY, DIV_INS_AY, DIV_INS_AY, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // YM2610B @@ -1025,6 +1056,8 @@ const DivInstrumentType chanPrefType[44][24]={ {DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ}, // Z {DIV_INS_MIKEY, DIV_INS_MIKEY, DIV_INS_MIKEY, DIV_INS_MIKEY}, // Lynx {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_AY, DIV_INS_AY, DIV_INS_AY, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // YM2610B (extended channel 3) + {DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_AMIGA}, // VERA + {DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010}, // X1-010 }; const char* DivEngine::getChannelName(int chan) { @@ -1078,10 +1111,12 @@ const char* DivEngine::getChannelName(int chan) { break; case DIV_SYSTEM_AMIGA: case DIV_SYSTEM_POKEY: - case DIV_SYSTEM_SWAN: case DIV_SYSTEM_LYNX: return chanNames[12][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_SWAN: + return chanNames[38][dispatchChanOfChan[chan]]; + break; case DIV_SYSTEM_YM2151: return chanNames[13][dispatchChanOfChan[chan]]; break; @@ -1129,6 +1164,7 @@ const char* DivEngine::getChannelName(int chan) { case DIV_SYSTEM_MULTIPCM: case DIV_SYSTEM_SEGAPCM: case DIV_SYSTEM_SEGAPCM_COMPAT: + case DIV_SYSTEM_X1_010: return chanNames[28][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_PCSPKR: @@ -1162,6 +1198,9 @@ const char* DivEngine::getChannelName(int chan) { case DIV_SYSTEM_QSOUND: return chanNames[36][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_VERA: + return chanNames[39][dispatchChanOfChan[chan]]; + break; } return "??"; } @@ -1268,6 +1307,7 @@ const char* DivEngine::getChannelShortName(int chan) { case DIV_SYSTEM_MULTIPCM: case DIV_SYSTEM_SEGAPCM: case DIV_SYSTEM_SEGAPCM_COMPAT: + case DIV_SYSTEM_X1_010: return chanShortNames[28][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_PCSPKR: @@ -1301,6 +1341,9 @@ const char* DivEngine::getChannelShortName(int chan) { case DIV_SYSTEM_QSOUND: return chanShortNames[36][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_VERA: + return chanShortNames[0][dispatchChanOfChan[chan]]; + break; } return "??"; } @@ -1354,7 +1397,6 @@ int DivEngine::getChannelType(int chan) { break; case DIV_SYSTEM_AMIGA: case DIV_SYSTEM_POKEY: - case DIV_SYSTEM_SWAN: return chanTypes[12][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_YM2151: @@ -1438,6 +1480,15 @@ int DivEngine::getChannelType(int chan) { case DIV_SYSTEM_LYNX: return chanTypes[36][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_VERA: + return chanTypes[38][dispatchChanOfChan[chan]]; + break; + case DIV_SYSTEM_X1_010: + return chanTypes[39][dispatchChanOfChan[chan]]; + break; + case DIV_SYSTEM_SWAN: + return chanTypes[40][dispatchChanOfChan[chan]]; + break; } return 1; } @@ -1588,6 +1639,12 @@ DivInstrumentType DivEngine::getPreferInsType(int chan) { case DIV_SYSTEM_LYNX: return chanPrefType[42][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_VERA: + return chanPrefType[44][dispatchChanOfChan[chan]]; + break; + case DIV_SYSTEM_X1_010: + return chanPrefType[45][dispatchChanOfChan[chan]]; + break; } return DIV_INS_FM; } @@ -1616,6 +1673,7 @@ bool DivEngine::isVGMExportable(DivSystem which) { case DIV_SYSTEM_OPLL: case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_VRC7: + case DIV_SYSTEM_X1_010: case DIV_SYSTEM_SWAN: return true; default: diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index c3388399..6133fff6 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -154,6 +154,13 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeC(3); } break; + case DIV_SYSTEM_X1_010: + for (int i=0; i<16; i++) { + w->writeC(0xc8); + w->writeS_BE(baseAddr2S+(i<<3)); + w->writeC(0); + } + break; case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_FULL: case DIV_SYSTEM_YM2610B: @@ -395,6 +402,11 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeS(baseAddr2S|(write.addr&0xffff)); w->writeC(write.val); break; + case DIV_SYSTEM_X1_010: + w->writeC(0xc8); + w->writeS_BE(baseAddr2S|(write.addr&0x1fff)); + w->writeC(write.val); + break; case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_FULL: case DIV_SYSTEM_YM2610B: @@ -568,6 +580,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { bool writePCESamples=false; bool writeADPCM=false; bool writeSegaPCM=false; + bool writeX1010=false; bool writeQSound=false; for (int i=0; ichipClock; + willExport[i]=true; + writeX1010=true; + } else if (!(hasX1&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasX1|=0x40000000; + howManyChips++; + } + break; case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_FULL: case DIV_SYSTEM_YM2610B: @@ -669,7 +694,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { } if (((song.system[i]==DIV_SYSTEM_YM2610B) || (song.system[i]==DIV_SYSTEM_YM2610B_EXT)) && (!(hasOPNB&0x80000000))) { // YM2610B flag hasOPNB|=0x80000000; - } + } break; case DIV_SYSTEM_AY8910: case DIV_SYSTEM_AY8930: @@ -995,6 +1020,16 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { w->write(qsoundMem,blockSize); } + if (writeX1010 && x1_010MemLen>0) { + w->writeC(0x67); + w->writeC(0x66); + w->writeC(0x91); + w->writeI(x1_010MemLen+8); + w->writeI(x1_010MemLen); + w->writeI(0); + w->write(x1_010Mem,x1_010MemLen); + } + // initialize streams int streamID=0; for (int i=0; iinPorta?colorOn:colorOff,">> InPorta"); break; } + case DIV_SYSTEM_X1_010: { + DivPlatformX1_010::Channel* ch=(DivPlatformX1_010::Channel*)data; + ImGui::Text("> X1-010"); + ImGui::Text("* freq: %.4x",ch->freq); + ImGui::Text(" - base: %d",ch->baseFreq); + ImGui::Text(" - pitch: %d",ch->pitch); + ImGui::Text("- note: %d",ch->note); + ImGui::Text("- wave: %d",ch->wave); + ImGui::Text("- sample: %d",ch->sample); + ImGui::Text("- ins: %d",ch->ins); + ImGui::Text("- pan: %d",ch->pan); + ImGui::Text("* envelope:"); + ImGui::Text(" - shape: %d",ch->env.shape); + ImGui::Text(" - period: %.2x",ch->env.period); + ImGui::Text(" - slide: %.2x",ch->env.slide); + ImGui::Text(" - slidefrac: %.2x",ch->env.slidefrac); + ImGui::Text(" - autoEnvNum: %.2x",ch->autoEnvNum); + ImGui::Text(" - autoEnvDen: %.2x",ch->autoEnvDen); + ImGui::Text("- WaveBank: %d",ch->waveBank); + ImGui::Text("- vol: %.2x",ch->vol); + ImGui::Text("- outVol: %.2x",ch->outVol); + ImGui::Text("- Lvol: %.2x",ch->lvol); + ImGui::Text("- Rvol: %.2x",ch->rvol); + ImGui::TextColored(ch->active?colorOn:colorOff,">> Active"); + ImGui::TextColored(ch->insChanged?colorOn:colorOff,">> InsChanged"); + ImGui::TextColored(ch->envChanged?colorOn:colorOff,">> EnvChanged"); + ImGui::TextColored(ch->freqChanged?colorOn:colorOff,">> FreqChanged"); + ImGui::TextColored(ch->keyOn?colorOn:colorOff,">> KeyOn"); + ImGui::TextColored(ch->keyOff?colorOn:colorOff,">> KeyOff"); + ImGui::TextColored(ch->inPorta?colorOn:colorOff,">> InPorta"); + ImGui::TextColored(ch->furnacePCM?colorOn:colorOff,">> FurnacePCM"); + ImGui::TextColored(ch->pcm?colorOn:colorOff,">> PCM"); + ImGui::TextColored(ch->env.flag.envEnable?colorOn:colorOff,">> EnvEnable"); + ImGui::TextColored(ch->env.flag.envOneshot?colorOn:colorOff,">> EnvOneshot"); + ImGui::TextColored(ch->env.flag.envSplit?colorOn:colorOff,">> EnvSplit"); + ImGui::TextColored(ch->env.flag.envHinvR?colorOn:colorOff,">> EnvHinvR"); + ImGui::TextColored(ch->env.flag.envVinvR?colorOn:colorOff,">> EnvVinvR"); + ImGui::TextColored(ch->env.flag.envHinvL?colorOn:colorOff,">> EnvHinvL"); + ImGui::TextColored(ch->env.flag.envVinvL?colorOn:colorOff,">> EnvVinvL"); + break; + } default: ImGui::Text("Unknown system! Help!"); break; diff --git a/src/gui/fileDialog.cpp b/src/gui/fileDialog.cpp new file mode 100644 index 00000000..79a87f3e --- /dev/null +++ b/src/gui/fileDialog.cpp @@ -0,0 +1,106 @@ +#include "fileDialog.h" +#include "ImGuiFileDialog.h" +#include "../ta-log.h" + +#include "../../extern/pfd-fixed/portable-file-dialogs.h" + +bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale) { + if (opened) return false; + saving=false; + curPath=path; + if (sysDialog) { + dialogO=new pfd::open_file(header,path,filter); + } else { + ImGuiFileDialog::Instance()->DpiScale=dpiScale; + ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path); + } + opened=true; + return true; +} + +bool FurnaceGUIFileDialog::openSave(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale) { + if (opened) return false; + saving=true; + curPath=path; + if (sysDialog) { + dialogS=new pfd::save_file(header,path,filter); + } else { + ImGuiFileDialog::Instance()->DpiScale=dpiScale; + ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + } + opened=true; + return true; +} + +bool FurnaceGUIFileDialog::accepted() { + if (sysDialog) { + return (fileName!=""); + } else { + return ImGuiFileDialog::Instance()->IsOk(); + } +} + +void FurnaceGUIFileDialog::close() { + if (sysDialog) { + if (saving) { + if (dialogS!=NULL) { + delete dialogS; + dialogS=NULL; + } + } else { + if (dialogO!=NULL) { + delete dialogO; + dialogO=NULL; + } + } + } else { + ImGuiFileDialog::Instance()->Close(); + } + opened=false; +} + +bool FurnaceGUIFileDialog::render(const ImVec2& min, const ImVec2& max) { + if (sysDialog) { + if (saving) { + if (dialogS!=NULL) { + if (dialogS->ready(1)) { + fileName=dialogS->result(); + logD("returning %s\n",fileName.c_str()); + return true; + } + } + } else { + if (dialogO!=NULL) { + if (dialogO->ready(1)) { + if (dialogO->result().empty()) { + fileName=""; + logD("returning nothing\n"); + } else { + fileName=dialogO->result()[0]; + logD("returning %s\n",fileName.c_str()); + } + return true; + } + } + } + return false; + } else { + return ImGuiFileDialog::Instance()->Display("FileDialog",ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoMove,min,max); + } +} + +String FurnaceGUIFileDialog::getPath() { + if (sysDialog) { + return curPath; + } else { + return ImGuiFileDialog::Instance()->GetCurrentPath(); + } +} + +String FurnaceGUIFileDialog::getFileName() { + if (sysDialog) { + return fileName; + } else { + return ImGuiFileDialog::Instance()->GetFilePathName(); + } +} diff --git a/src/gui/fileDialog.h b/src/gui/fileDialog.h new file mode 100644 index 00000000..b7f21abf --- /dev/null +++ b/src/gui/fileDialog.h @@ -0,0 +1,32 @@ +#include "../ta-utils.h" +#include "imgui.h" +#include + +namespace pfd { + class open_file; + class save_file; +} + +class FurnaceGUIFileDialog { + bool sysDialog; + bool opened; + bool saving; + String curPath; + String fileName; + pfd::open_file* dialogO; + pfd::save_file* dialogS; + public: + bool openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale); + bool openSave(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale); + bool accepted(); + void close(); + bool render(const ImVec2& min, const ImVec2& max); + String getPath(); + String getFileName(); + FurnaceGUIFileDialog(bool system): + sysDialog(system), + opened(false), + saving(false), + dialogO(NULL), + dialogS(NULL) {} +}; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 81aad9c1..5765788e 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -32,6 +32,7 @@ #include "ImGuiFileDialog.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" +#include "plot_nolerp.h" #include "guiConst.h" #include "intConst.h" #include @@ -1090,6 +1091,13 @@ void FurnaceGUI::drawInsList() { } ImGui::Separator(); if (ImGui::BeginTable("InsListScroll",1,ImGuiTableFlags_ScrollY)) { + if (settings.unifiedDataView) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_TASKS " Instruments"); + ImGui::Indent(); + } + for (int i=0; i<(int)e->song.ins.size(); i++) { DivInstrument* ins=e->song.ins[i]; String name; @@ -1190,6 +1198,14 @@ void FurnaceGUI::drawInsList() { ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MIKEY]); name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); break; + case DIV_INS_VERA: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VERA]); + name=fmt::sprintf(ICON_FA_KEYBOARD_O " %.2X: %s##_INS%d\n",i,ins->name,i); + break; + case DIV_INS_X1_010: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_X1_010]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); + break; default: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_UNKNOWN]); name=fmt::sprintf(ICON_FA_QUESTION " %.2X: %s##_INS%d\n",i,ins->name,i); @@ -1206,12 +1222,32 @@ void FurnaceGUI::drawInsList() { } ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s",(ins->type>DIV_INS_MAX)?"Unknown":insTypes[ins->type]); if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { insEditOpen=true; nextWindow=GUI_WINDOW_INS_EDIT; } } } + + if (settings.unifiedDataView) { + ImGui::Unindent(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_AREA_CHART " Wavetables"); + ImGui::Indent(); + actualWaveList(); + ImGui::Unindent(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_VOLUME_UP " Samples"); + ImGui::Indent(); + actualSampleList(); + ImGui::Unindent(); + } + ImGui::EndTable(); } } @@ -1223,6 +1259,47 @@ const char* sampleNote[12]={ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; + +void FurnaceGUI::actualWaveList() { + float wavePreview[256]; + for (int i=0; i<(int)e->song.wave.size(); i++) { + DivWavetable* wave=e->song.wave[i]; + for (int i=0; ilen; i++) { + wavePreview[i]=wave->data[i]; + } + if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(fmt::sprintf("%d##_WAVE%d\n",i,i).c_str(),curWave==i)) { + curWave=i; + } + if (ImGui::IsItemHovered()) { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + waveEditOpen=true; + } + } + ImGui::SameLine(); + PlotNoLerp(fmt::sprintf("##_WAVEP%d",i).c_str(),wavePreview,wave->len+1,0,NULL,0,wave->max); + } +} + +void FurnaceGUI::actualSampleList() { + for (int i=0; i<(int)e->song.sample.size(); i++) { + DivSample* sample=e->song.sample[i]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(fmt::sprintf("%d: %s##_SAM%d",i,sample->name,i).c_str(),curSample==i)) { + curSample=i; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Bank %d: %s",i/12,sampleNote[i%12]); + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + sampleEditOpen=true; + } + } + } +} + void FurnaceGUI::drawSampleList() { if (nextWindow==GUI_WINDOW_SAMPLE_LIST) { sampleListOpen=true; @@ -1264,24 +1341,7 @@ void FurnaceGUI::drawSampleList() { } ImGui::Separator(); if (ImGui::BeginTable("SampleListScroll",1,ImGuiTableFlags_ScrollY)) { - for (int i=0; i<(int)e->song.sample.size(); i++) { - DivSample* sample=e->song.sample[i]; - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if ((i%12)==0) { - if (i>0) ImGui::Unindent(); - ImGui::Text("Bank %d",i/12); - ImGui::Indent(); - } - if (ImGui::Selectable(fmt::sprintf("%s: %s##_SAM%d",sampleNote[i%12],sample->name,i).c_str(),curSample==i)) { - curSample=i; - } - if (ImGui::IsItemHovered()) { - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - sampleEditOpen=true; - } - } - } + actualSampleList(); ImGui::EndTable(); } ImGui::Unindent(); @@ -1346,7 +1406,7 @@ void FurnaceGUI::drawSampleEdit() { ImGui::Text("notes:"); if (sample->loopStart>=0) { considerations=true; - ImGui::Text("- sample won't loop on Neo Geo ADPCM-A"); + ImGui::Text("- sample won't loop on Neo Geo ADPCM-A and X1-010"); if (sample->loopStart&1) { ImGui::Text("- sample loop start will be aligned to the nearest even sample on Amiga"); } @@ -1362,10 +1422,18 @@ void FurnaceGUI::drawSampleEdit() { considerations=true; ImGui::Text("- sample length will be aligned and padded to 512 sample units on Neo Geo ADPCM."); } + if (sample->samples&4095) { + considerations=true; + ImGui::Text("- sample length will be aligned and padded to 4096 sample units on X1-010."); + } if (sample->samples>65535) { considerations=true; ImGui::Text("- maximum sample length on Sega PCM and QSound is 65536 samples"); } + if (sample->samples>131071) { + considerations=true; + ImGui::Text("- maximum sample length on X1-010 is 131072 samples"); + } if (sample->samples>2097151) { considerations=true; ImGui::Text("- maximum sample length on Neo Geo ADPCM is 2097152 samples"); @@ -2027,6 +2095,7 @@ void FurnaceGUI::drawStats() { String adpcmAUsage=fmt::sprintf("%d/16384KB",e->adpcmAMemLen/1024); String adpcmBUsage=fmt::sprintf("%d/16384KB",e->adpcmBMemLen/1024); String qsoundUsage=fmt::sprintf("%d/16384KB",e->qsoundMemLen/1024); + String x1_010Usage=fmt::sprintf("%d/1024KB",e->x1_010MemLen/1024); ImGui::Text("ADPCM-A"); ImGui::SameLine(); ImGui::ProgressBar(((float)e->adpcmAMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmAUsage.c_str()); @@ -2036,6 +2105,9 @@ void FurnaceGUI::drawStats() { ImGui::Text("QSound"); ImGui::SameLine(); ImGui::ProgressBar(((float)e->qsoundMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),qsoundUsage.c_str()); + ImGui::Text("X1-010"); + ImGui::SameLine(); + ImGui::ProgressBar(((float)e->x1_010MemLen)/1048576.0f,ImVec2(-FLT_MIN,0),x1_010Usage.c_str()); } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_STATS; ImGui::End(); @@ -2049,7 +2121,7 @@ void FurnaceGUI::drawCompatFlags() { } if (!compatFlagsOpen) return; if (ImGui::Begin("Compatibility Flags",&compatFlagsOpen)) { - ImGui::TextWrapped("these flags are stored in the song when saving in .fur format, and are automatically enabled when saving in .dmf format."); + ImGui::TextWrapped("these flags are designed to provide better DefleMask/older Furnace compatibility."); ImGui::Checkbox("Limit slide range",&e->song.limitSlides); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("when enabled, slides are limited to a compatible range.\nmay cause problems with slides in negative octaves."); @@ -2095,6 +2167,14 @@ void FurnaceGUI::drawCompatFlags() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("when enabled, vibrato will not be reset on a new note."); } + ImGui::Checkbox("Broken DAC mode",&e->song.brokenDACMode); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, the DAC in YM2612 will be disabled if there isn't any sample playing."); + } + ImGui::Checkbox("Auto-insert one tick gap between notes",&e->song.oneTickCut); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, a one-tick note cut will be inserted between non-legato/non-portamento notes.\nthis simulates the behavior of some Amiga/SNES music engines."); + } ImGui::Text("Loop modality:"); if (ImGui::RadioButton("Reset channels",e->song.loopModality==0)) { @@ -2136,6 +2216,10 @@ void FurnaceGUI::drawCompatFlags() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("behavior changed in 0.6"); } + ImGui::Checkbox("Allow instrument change during slides",&e->song.newInsTriggersInPorta); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.6"); + } } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_COMPAT_FLAGS; ImGui::End(); @@ -2479,6 +2563,7 @@ void FurnaceGUI::moveCursorPrevChannel(bool overflow) { selStart=cursor; selEnd=cursor; + demandScrollX=true; } void FurnaceGUI::moveCursorNextChannel(bool overflow) { @@ -2501,6 +2586,7 @@ void FurnaceGUI::moveCursorNextChannel(bool overflow) { selStart=cursor; selEnd=cursor; + demandScrollX=true; } void FurnaceGUI::moveCursorTop(bool select) { @@ -2510,6 +2596,7 @@ void FurnaceGUI::moveCursorTop(bool select) { DETERMINE_FIRST; cursor.xCoarse=firstChannel; cursor.xFine=0; + demandScrollX=true; } else { cursor.y=0; } @@ -2527,6 +2614,7 @@ void FurnaceGUI::moveCursorBottom(bool select) { DETERMINE_LAST; cursor.xCoarse=lastChannel-1; cursor.xFine=2+e->song.pat[cursor.xCoarse].effectRows*2; + demandScrollX=true; } else { cursor.y=e->song.patLen-1; } @@ -2559,6 +2647,15 @@ void FurnaceGUI::prepareUndo(ActionType action) { case GUI_UNDO_PATTERN_PUSH: case GUI_UNDO_PATTERN_CUT: case GUI_UNDO_PATTERN_PASTE: + case GUI_UNDO_PATTERN_CHANGE_INS: + case GUI_UNDO_PATTERN_INTERPOLATE: + case GUI_UNDO_PATTERN_FADE: + case GUI_UNDO_PATTERN_SCALE: + case GUI_UNDO_PATTERN_RANDOMIZE: + case GUI_UNDO_PATTERN_INVERT_VAL: + case GUI_UNDO_PATTERN_FLIP: + case GUI_UNDO_PATTERN_COLLAPSE: + case GUI_UNDO_PATTERN_EXPAND: for (int i=0; igetTotalChannelCount(); i++) { e->song.pat[i].getPattern(e->song.orders.ord[i][order],false)->copyOn(oldPat[i]); } @@ -2600,6 +2697,15 @@ void FurnaceGUI::makeUndo(ActionType action) { case GUI_UNDO_PATTERN_PUSH: case GUI_UNDO_PATTERN_CUT: case GUI_UNDO_PATTERN_PASTE: + case GUI_UNDO_PATTERN_CHANGE_INS: + case GUI_UNDO_PATTERN_INTERPOLATE: + case GUI_UNDO_PATTERN_FADE: + case GUI_UNDO_PATTERN_SCALE: + case GUI_UNDO_PATTERN_RANDOMIZE: + case GUI_UNDO_PATTERN_INVERT_VAL: + case GUI_UNDO_PATTERN_FLIP: + case GUI_UNDO_PATTERN_COLLAPSE: + case GUI_UNDO_PATTERN_EXPAND: for (int i=0; igetTotalChannelCount(); i++) { DivPattern* p=e->song.pat[i].getPattern(e->song.orders.ord[i][order],false); for (int j=0; jsong.patLen; j++) { @@ -2666,6 +2772,19 @@ void FurnaceGUI::doSelectAll() { } } +#define maskOut(x) \ + if (x==0) { \ + if (!opMaskNote) continue; \ + } else if (x==1) { \ + if (!opMaskIns) continue; \ + } else if (x==2) { \ + if (!opMaskVol) continue; \ + } else if (((x)&1)==0) { \ + if (!opMaskEffectVal) continue; \ + } else if (((x)&1)==1) { \ + if (!opMaskEffect) continue; \ + } + void FurnaceGUI::doDelete() { finishSelection(); prepareUndo(GUI_UNDO_PATTERN_DELETE); @@ -2678,6 +2797,7 @@ void FurnaceGUI::doDelete() { if (!e->song.chanShow[iCoarse]) continue; DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][iFine]=0; @@ -2711,6 +2831,7 @@ void FurnaceGUI::doPullDelete() { if (!e->song.chanShow[iCoarse]) continue; DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.patLen; j++) { if (jsong.patLen-1) { if (iFine==0) { @@ -2743,6 +2864,7 @@ void FurnaceGUI::doInsert() { if (!e->song.chanShow[iCoarse]) continue; DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.patLen-1; j>=selStart.y; j--) { if (j==selStart.y) { if (iFine==0) { @@ -2775,6 +2897,7 @@ void FurnaceGUI::doTranspose(int amount) { if (!e->song.chanShow[iCoarse]) continue; DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; @@ -2789,9 +2912,13 @@ void FurnaceGUI::doTranspose(int amount) { origNote+=12; origOctave--; } - if (origOctave>7) { - origNote=12; - origOctave=7; + if (origOctave==9 && origNote>11) { + origNote=11; + origOctave=9; + } + if (origOctave>9) { + origNote=11; + origOctave=9; } if (origOctave<-5) { origNote=1; @@ -2800,6 +2927,16 @@ void FurnaceGUI::doTranspose(int amount) { pat->data[j][0]=origNote; pat->data[j][1]=(unsigned char)origOctave; } + } else { + int top=255; + if (iFine==1) { + if (e->song.ins.empty()) continue; + top=e->song.ins.size()-1; + } else if (iFine==2) { // volume + top=e->getMaxVolumeChan(iCoarse); + } + if (pat->data[j][iFine+1]==-1) continue; + pat->data[j][iFine+1]=MIN(top,MAX(0,pat->data[j][iFine+1]+amount)); } } } @@ -2857,7 +2994,7 @@ void FurnaceGUI::doCopy(bool cut) { } } -void FurnaceGUI::doPaste() { +void FurnaceGUI::doPaste(PasteMode mode) { finishSelection(); prepareUndo(GUI_UNDO_PATTERN_PASTE); char* clipText=SDL_GetClipboardText(); @@ -2929,9 +3066,20 @@ void FurnaceGUI::doPaste() { note[2]=line[charPos++]; note[3]=0; - if (!decodeNote(note,pat->data[j][0],pat->data[j][1])) { - invalidData=true; - break; + if (iFine==0 && !opMaskNote) { + iFine++; + continue; + } + + if ((mode==GUI_PASTE_MODE_MIX_BG || mode==GUI_PASTE_MODE_MIX_FG) && strcmp(note,"...")==0) { + // do nothing. + } else { + if (mode!=GUI_PASTE_MODE_MIX_BG || (pat->data[j][0]==0 && pat->data[j][1]==0)) { + if (!decodeNote(note,pat->data[j][0],pat->data[j][1])) { + invalidData=true; + break; + } + } } } else { if (charPos>=line.size()) { @@ -2946,15 +3094,41 @@ void FurnaceGUI::doPaste() { note[1]=line[charPos++]; note[2]=0; + if (iFine==1) { + if (!opMaskIns) { + iFine++; + continue; + } + } else if (iFine==2) { + if (!opMaskVol) { + iFine++; + continue; + } + } else if ((iFine&1)==0) { + if (!opMaskEffectVal) { + iFine++; + continue; + } + } else if ((iFine&1)==1) { + if (!opMaskEffect) { + iFine++; + continue; + } + } + if (strcmp(note,"..")==0) { - pat->data[j][iFine+1]=-1; + if (!(mode==GUI_PASTE_MODE_MIX_BG || mode==GUI_PASTE_MODE_MIX_FG)) { + pat->data[j][iFine+1]=-1; + } } else { unsigned int val=0; if (sscanf(note,"%2X",&val)!=1) { invalidData=true; break; } - if (iFine<(3+e->song.pat[iCoarse].effectRows*2)) pat->data[j][iFine+1]=val; + if (mode!=GUI_PASTE_MODE_MIX_BG || pat->data[j][iFine+1]==-1) { + if (iFine<(3+e->song.pat[iCoarse].effectRows*2)) pat->data[j][iFine+1]=val; + } } } iFine++; @@ -2966,11 +3140,380 @@ void FurnaceGUI::doPaste() { break; } j++; + if (mode==GUI_PASTE_MODE_OVERFLOW && j>=e->song.patLen && ordsong.ordersLen-1) { + j=0; + ord++; + } + + if (mode==GUI_PASTE_MODE_FLOOD && i==data.size()-1) { + i=1; + } } makeUndo(GUI_UNDO_PATTERN_PASTE); } +void FurnaceGUI::doChangeIns(int ins) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_CHANGE_INS); + + int iCoarse=selStart.xCoarse; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (int j=selStart.y; j<=selEnd.y; j++) { + if (pat->data[j][2]!=-1 || !(pat->data[j][0]==0 && pat->data[j][1]==0)) { + pat->data[j][2]=ins; + } + } + } + + makeUndo(GUI_UNDO_PATTERN_CHANGE_INS); +} + +void FurnaceGUI::doInterpolate() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_INTERPOLATE); + + std::vector> points; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][iFine+1]!=-1) { + points.emplace(points.end(),j,pat->data[j][iFine+1]); + } + } + + if (points.size()>1) for (size_t j=0; j& curPoint=points[j]; + std::pair& nextPoint=points[j+1]; + double distance=nextPoint.first-curPoint.first; + for (int k=0; k<(nextPoint.first-curPoint.first); k++) { + pat->data[k+curPoint.first][iFine+1]=curPoint.second+((nextPoint.second-curPoint.second)*(double)k/distance); + } + } + } else { + for (int j=selStart.y; j<=selEnd.y; j++) { + if (pat->data[j][0]!=0 && pat->data[j][1]!=0) { + if (pat->data[j][0]!=100 && pat->data[j][0]!=101 && pat->data[j][0]!=102) { + points.emplace(points.end(),j,pat->data[j][0]+pat->data[j][1]*12); + } + } + } + + if (points.size()>1) for (size_t j=0; j& curPoint=points[j]; + std::pair& nextPoint=points[j+1]; + double distance=nextPoint.first-curPoint.first; + for (int k=0; k<(nextPoint.first-curPoint.first); k++) { + int val=curPoint.second+((nextPoint.second-curPoint.second)*(double)k/distance); + pat->data[k+curPoint.first][0]=val%12; + pat->data[k+curPoint.first][1]=val/12; + if (pat->data[k+curPoint.first][0]==0) { + pat->data[k+curPoint.first][0]=12; + pat->data[k+curPoint.first][1]--; + } + pat->data[k+curPoint.first][1]&=255; + } + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_INTERPOLATE); +} + +void FurnaceGUI::doFade(int p0, int p1, bool mode) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_FADE); + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.ins.empty()) continue; + absoluteTop=e->song.ins.size()-1; + } else if (iFine==2) { // volume + absoluteTop=e->getMaxVolumeChan(iCoarse); + } + if (selEnd.y-selStart.y<1) continue; + for (int j=selStart.y; j<=selEnd.y; j++) { + double fraction=double(j-selStart.y)/double(selEnd.y-selStart.y); + int value=p0+double(p1-p0)*fraction; + if (mode) { // nibble + value&=15; + pat->data[j][iFine+1]=MIN(absoluteTop,value|(value<<4)); + } else { // byte + pat->data[j][iFine+1]=MIN(absoluteTop,value); + } + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_FADE); +} + +void FurnaceGUI::doInvertValues() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_INVERT_VAL); + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.ins.empty()) continue; + top=e->song.ins.size()-1; + } else if (iFine==2) { // volume + top=e->getMaxVolumeChan(iCoarse); + } + for (int j=selStart.y; j<=selEnd.y; j++) { + if (pat->data[j][iFine+1]==-1) continue; + pat->data[j][iFine+1]=top-pat->data[j][iFine+1]; + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_INVERT_VAL); +} + +void FurnaceGUI::doScale(float top) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_SCALE); + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.ins.empty()) continue; + absoluteTop=e->song.ins.size()-1; + } else if (iFine==2) { // volume + absoluteTop=e->getMaxVolumeChan(iCoarse); + } + for (int j=selStart.y; j<=selEnd.y; j++) { + if (pat->data[j][iFine+1]==-1) continue; + pat->data[j][iFine+1]=MIN(absoluteTop,(double)pat->data[j][iFine+1]*(top/100.0f)); + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_SCALE); +} + +void FurnaceGUI::doRandomize(int bottom, int top, bool mode) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_RANDOMIZE); + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.ins.empty()) continue; + absoluteTop=e->song.ins.size()-1; + } else if (iFine==2) { // volume + absoluteTop=e->getMaxVolumeChan(iCoarse); + } + for (int j=selStart.y; j<=selEnd.y; j++) { + int value=0; + int value2=0; + if (top-bottom<=0) { + value=MIN(absoluteTop,bottom); + value2=MIN(absoluteTop,bottom); + } else { + value=MIN(absoluteTop,bottom+(rand()%(top-bottom+1))); + value2=MIN(absoluteTop,bottom+(rand()%(top-bottom+1))); + } + if (mode) { + value&=15; + value2&=15; + pat->data[j][iFine+1]=value|(value2<<4); + } else { + pat->data[j][iFine+1]=value; + } + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_RANDOMIZE); +} + +void FurnaceGUI::doFlip() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_FLIP); + + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=selStart.y; j<=selEnd.y; j++) { + if (iFine==0) { + pat->data[j][0]=patBuffer.data[selEnd.y-j+selStart.y][0]; + } + pat->data[j][iFine+1]=patBuffer.data[selEnd.y-j+selStart.y][iFine+1]; + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_FLIP); +} + +void FurnaceGUI::doCollapse(int divider) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_COLLAPSE); + + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=0; j<=selEnd.y-selStart.y; j++) { + if (j*divider>=selEnd.y-selStart.y) { + if (iFine==0) { + pat->data[j+selStart.y][0]=0; + pat->data[j+selStart.y][1]=0; + } else { + pat->data[j+selStart.y][iFine+1]=-1; + } + } else { + if (iFine==0) { + pat->data[j+selStart.y][0]=patBuffer.data[j*divider+selStart.y][0]; + } + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j*divider+selStart.y][iFine+1]; + + if (iFine==0) { + for (int k=1; k=selEnd.y-selStart.y) break; + if (!(pat->data[j+selStart.y][0]==0 && pat->data[j+selStart.y][1]==0)) break; + pat->data[j+selStart.y][0]=patBuffer.data[j*divider+selStart.y+k][0]; + pat->data[j+selStart.y][1]=patBuffer.data[j*divider+selStart.y+k][1]; + } + } else { + for (int k=1; k=selEnd.y-selStart.y) break; + if (pat->data[j+selStart.y][iFine+1]!=-1) break; + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j*divider+selStart.y+k][iFine+1]; + } + } + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_COLLAPSE); +} + +void FurnaceGUI::doExpand(int multiplier) { + if (multiplier<1) return; + + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_EXPAND); + + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=0; j<=(selEnd.y-selStart.y)*multiplier; j++) { + if ((j+selStart.y)>=e->song.patLen) break; + if ((j%multiplier)!=0) { + if (iFine==0) { + pat->data[j+selStart.y][0]=0; + pat->data[j+selStart.y][1]=0; + } else { + pat->data[j+selStart.y][iFine+1]=-1; + } + continue; + } + if (iFine==0) { + pat->data[j+selStart.y][0]=patBuffer.data[j/multiplier+selStart.y][0]; + } + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j/multiplier+selStart.y][iFine+1]; + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_EXPAND); +} + void FurnaceGUI::doUndo() { if (undoHist.empty()) return; UndoStep& us=undoHist.back(); @@ -2990,6 +3533,15 @@ void FurnaceGUI::doUndo() { case GUI_UNDO_PATTERN_PUSH: case GUI_UNDO_PATTERN_CUT: case GUI_UNDO_PATTERN_PASTE: + case GUI_UNDO_PATTERN_CHANGE_INS: + case GUI_UNDO_PATTERN_INTERPOLATE: + case GUI_UNDO_PATTERN_FADE: + case GUI_UNDO_PATTERN_SCALE: + case GUI_UNDO_PATTERN_RANDOMIZE: + case GUI_UNDO_PATTERN_INVERT_VAL: + case GUI_UNDO_PATTERN_FLIP: + case GUI_UNDO_PATTERN_COLLAPSE: + case GUI_UNDO_PATTERN_EXPAND: for (UndoPatternData& i: us.pat) { DivPattern* p=e->song.pat[i.chan].getPattern(i.pat,true); p->data[i.row][i.col]=i.oldVal; @@ -3027,6 +3579,15 @@ void FurnaceGUI::doRedo() { case GUI_UNDO_PATTERN_PUSH: case GUI_UNDO_PATTERN_CUT: case GUI_UNDO_PATTERN_PASTE: + case GUI_UNDO_PATTERN_CHANGE_INS: + case GUI_UNDO_PATTERN_INTERPOLATE: + case GUI_UNDO_PATTERN_FADE: + case GUI_UNDO_PATTERN_SCALE: + case GUI_UNDO_PATTERN_RANDOMIZE: + case GUI_UNDO_PATTERN_INVERT_VAL: + case GUI_UNDO_PATTERN_FLIP: + case GUI_UNDO_PATTERN_COLLAPSE: + case GUI_UNDO_PATTERN_EXPAND: for (UndoPatternData& i: us.pat) { DivPattern* p=e->song.pat[i.chan].getPattern(i.pat,true); p->data[i.row][i.col]=i.newVal; @@ -3467,6 +4028,9 @@ void FurnaceGUI::doAction(int what) { break; case GUI_ACTION_PAT_INSERT: doInsert(); + if (settings.stepOnInsert) { + moveCursor(0,editStep,false); + } break; case GUI_ACTION_PAT_MUTE_CURSOR: if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; @@ -3503,6 +4067,31 @@ void FurnaceGUI::doAction(int what) { e->song.pat[cursor.xCoarse].effectRows--; if (e->song.pat[cursor.xCoarse].effectRows<1) e->song.pat[cursor.xCoarse].effectRows=1; break; + case GUI_ACTION_PAT_INTERPOLATE: + doInterpolate(); + break; + case GUI_ACTION_PAT_INVERT_VALUES: + doInvertValues(); + break; + case GUI_ACTION_PAT_FLIP_SELECTION: + doFlip(); + break; + case GUI_ACTION_PAT_COLLAPSE_ROWS: + doCollapse(2); + break; + case GUI_ACTION_PAT_EXPAND_ROWS: + doExpand(2); + break; + case GUI_ACTION_PAT_COLLAPSE_PAT: // TODO + break; + case GUI_ACTION_PAT_EXPAND_PAT: // TODO + break; + case GUI_ACTION_PAT_COLLAPSE_SONG: // TODO + break; + case GUI_ACTION_PAT_EXPAND_SONG: // TODO + break; + case GUI_ACTION_PAT_LATCH: // TODO + break; case GUI_ACTION_INS_LIST_ADD: curIns=e->addInstrument(cursor.xCoarse); @@ -3799,6 +4388,7 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { int num=12*curOctave+key; if (edit) { + // TODO: separate when adding MIDI input. DivPattern* pat=e->song.pat[cursor.xCoarse].getPattern(e->song.orders.ord[cursor.xCoarse][e->getOrder()],true); prepareUndo(GUI_UNDO_PATTERN_EDIT); @@ -3820,7 +4410,17 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { pat->data[cursor.y][1]--; } pat->data[cursor.y][1]=(unsigned char)pat->data[cursor.y][1]; - pat->data[cursor.y][2]=curIns; + if (latchIns==-2) { + pat->data[cursor.y][2]=curIns; + } else if (latchIns!=-1 && !e->song.ins.empty()) { + pat->data[cursor.y][2]=MIN(((int)e->song.ins.size())-1,latchIns); + } + if (latchVol!=-1) { + int maxVol=e->getMaxVolumeChan(cursor.xCoarse); + pat->data[cursor.y][3]=MIN(maxVol,latchVol); + } + if (latchEffect!=-1) pat->data[cursor.y][4]=latchEffect; + if (latchEffectVal!=-1) pat->data[cursor.y][5]=latchEffectVal; previewNote(cursor.xCoarse,num); } makeUndo(GUI_UNDO_PATTERN_EDIT); @@ -4041,73 +4641,168 @@ bool dirExists(String what) { } void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { - ImGuiFileDialog::Instance()->DpiScale=dpiScale; + bool hasOpened=false; switch (type) { case GUI_FILE_OPEN: if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Open File","compatible files{.fur,.dmf,.mod},.*",workingDirSong); + hasOpened=fileDialog->openLoad( + "Open File", + {"compatible files", "*.fur *.dmf *.mod", + "all files", ".*"}, + "compatible files{.fur,.dmf,.mod},.*", + workingDirSong, + dpiScale + ); break; case GUI_FILE_SAVE: if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save File","Furnace song{.fur},DefleMask 1.1 module{.dmf}",workingDirSong,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save File", + {"Furnace song", "*.fur", + "DefleMask 1.1 module", "*.dmf"}, + "Furnace song{.fur},DefleMask 1.1 module{.dmf}", + workingDirSong, + dpiScale + ); break; case GUI_FILE_SAVE_DMF_LEGACY: if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save File","DefleMask 1.0/legacy module{.dmf}",workingDirSong,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save File", + {"DefleMask 1.0/legacy module", "*.dmf"}, + "DefleMask 1.0/legacy module{.dmf}", + workingDirSong, + dpiScale + ); break; case GUI_FILE_INS_OPEN: if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Instrument","compatible files{.fui,.dmp,.tfi,.vgi},.*",workingDirIns); + hasOpened=fileDialog->openLoad( + "Load Instrument", + {"compatible files", "*.fui *.dmp *.tfi *.vgi", + "all files", ".*"}, + "compatible files{.fui,.dmp,.tfi,.vgi},.*", + workingDirIns, + dpiScale + ); break; case GUI_FILE_INS_SAVE: if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Instrument","Furnace instrument{.fui}",workingDirIns,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save Instrument", + {"Furnace instrument", "*.fui"}, + "Furnace instrument{.fui}", + workingDirIns, + dpiScale + ); break; case GUI_FILE_WAVE_OPEN: if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Wavetable","compatible files{.fuw,.dmw},.*",workingDirWave); + hasOpened=fileDialog->openLoad( + "Load Wavetable", + {"compatible files", "*.fuw *.dmw", + "all files", ".*"}, + "compatible files{.fuw,.dmw},.*", + workingDirWave, + dpiScale + ); break; case GUI_FILE_WAVE_SAVE: if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Wavetable","Furnace wavetable{.fuw}",workingDirWave,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save Wavetable", + {"Furnace wavetable", ".fuw"}, + "Furnace wavetable{.fuw}", + workingDirWave, + dpiScale + ); break; case GUI_FILE_SAMPLE_OPEN: if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Sample","Wave file{.wav},.*",workingDirSample); + hasOpened=fileDialog->openLoad( + "Load Sample", + {"Wave file", "*.wav", + "all files", ".*"}, + "Wave file{.wav},.*", + workingDirSample, + dpiScale + ); break; case GUI_FILE_SAMPLE_SAVE: if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Sample","Wave file{.wav}",workingDirSample,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save Sample", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirSample, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_ONE: if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDirAudioExport,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_PER_SYS: if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDirAudioExport,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDirAudioExport,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_VGM: if (!dirExists(workingDirVGMExport)) workingDirVGMExport=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export VGM",".vgm",workingDirVGMExport,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Export VGM", + {"VGM file", "*.vgm"}, + "VGM file{.vgm}", + workingDirVGMExport, + dpiScale + ); break; case GUI_FILE_EXPORT_ROM: showError("Coming soon!"); break; case GUI_FILE_LOAD_MAIN_FONT: if (!dirExists(workingDirFont)) workingDirFont=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Select Font","compatible files{.ttf,.otf,.ttc}",workingDirFont); + hasOpened=fileDialog->openLoad( + "Select Font", + {"compatible files", "*.ttf *.otf *.ttc"}, + "compatible files{.ttf,.otf,.ttc}", + workingDirFont, + dpiScale + ); break; case GUI_FILE_LOAD_PAT_FONT: if (!dirExists(workingDirFont)) workingDirFont=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Select Font","compatible files{.ttf,.otf,.ttc}",workingDirFont); + hasOpened=fileDialog->openLoad( + "Select Font", + {"compatible files", "*.ttf *.otf *.ttc"}, + "compatible files{.ttf,.otf,.ttc}", + workingDirFont, + dpiScale + ); break; } - curFileDialog=type; + if (hasOpened) curFileDialog=type; //ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_NavEnableKeyboard; } @@ -4232,7 +4927,7 @@ int FurnaceGUI::load(String path) { } if (len<1) { if (len==0) { - printf("that file is empty!\n"); + logE("that file is empty!\n"); lastError="file is empty"; } else { perror("tell error"); @@ -4388,8 +5083,204 @@ void FurnaceGUI::processDrags(int dragX, int dragY) { fileName+=x; \ } +#define checkExtensionDual(x,y,fallback) \ + String lowerCase=fileName; \ + for (char& i: lowerCase) { \ + if (i>='A' && i<='Z') i+='a'-'A'; \ + } \ + if (lowerCase.size()<4 || (lowerCase.rfind(x)!=lowerCase.size()-4 && lowerCase.rfind(y)!=lowerCase.size()-4)) { \ + fileName+=fallback; \ + } + #define BIND_FOR(x) getKeyName(actionKeys[x],true).c_str() +void FurnaceGUI::editOptions(bool topMenu) { + char id[4096]; + if (ImGui::MenuItem("cut",BIND_FOR(GUI_ACTION_PAT_CUT))) doCopy(true); + if (ImGui::MenuItem("copy",BIND_FOR(GUI_ACTION_PAT_COPY))) doCopy(false); + if (ImGui::MenuItem("paste",BIND_FOR(GUI_ACTION_PAT_PASTE))) doPaste(); + if (ImGui::BeginMenu("paste special...")) { + if (ImGui::MenuItem("paste mix",BIND_FOR(GUI_ACTION_PAT_PASTE_MIX))) doPaste(GUI_PASTE_MODE_MIX_FG); + if (ImGui::MenuItem("paste mix (background)",BIND_FOR(GUI_ACTION_PAT_PASTE_MIX_BG))) doPaste(GUI_PASTE_MODE_MIX_BG); + if (ImGui::MenuItem("paste flood",BIND_FOR(GUI_ACTION_PAT_PASTE_FLOOD))) doPaste(GUI_PASTE_MODE_FLOOD); + if (ImGui::MenuItem("paste overflow",BIND_FOR(GUI_ACTION_PAT_PASTE_OVERFLOW))) doPaste(GUI_PASTE_MODE_OVERFLOW); + ImGui::EndMenu(); + } + if (ImGui::MenuItem("delete",BIND_FOR(GUI_ACTION_PAT_DELETE))) doDelete(); + if (topMenu) { + if (ImGui::MenuItem("select all",BIND_FOR(GUI_ACTION_PAT_SELECT_ALL))) doSelectAll(); + } + ImGui::Separator(); + + ImGui::Text("operation mask"); + ImGui::SameLine(); + + ImGui::PushFont(patFont); + if (ImGui::BeginTable("opMaskTable",5,ImGuiTableFlags_Borders|ImGuiTableFlags_SizingFixedFit|ImGuiTableFlags_NoHostExtendX)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_ACTIVE]); + if (ImGui::Selectable(opMaskNote?"C-4##opMaskNote":"---##opMaskNote",opMaskNote,ImGuiSelectableFlags_DontClosePopups)) { + opMaskNote=!opMaskNote; + } + ImGui::PopStyleColor(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS]); + if (ImGui::Selectable(opMaskIns?"01##opMaskIns":"--##opMaskIns",opMaskIns,ImGuiSelectableFlags_DontClosePopups)) { + opMaskIns=!opMaskIns; + } + ImGui::PopStyleColor(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_VOLUME_MAX]); + if (ImGui::Selectable(opMaskVol?"7F##opMaskVol":"--##opMaskVol",opMaskVol,ImGuiSelectableFlags_DontClosePopups)) { + opMaskVol=!opMaskVol; + } + ImGui::PopStyleColor(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_PITCH]); + if (ImGui::Selectable(opMaskEffect?"04##opMaskEffect":"--##opMaskEffect",opMaskEffect,ImGuiSelectableFlags_DontClosePopups)) { + opMaskEffect=!opMaskEffect; + } + ImGui::TableNextColumn(); + if (ImGui::Selectable(opMaskEffectVal?"72##opMaskEffectVal":"--##opMaskEffectVal",opMaskEffectVal,ImGuiSelectableFlags_DontClosePopups)) { + opMaskEffectVal=!opMaskEffectVal; + } + ImGui::PopStyleColor(); + ImGui::EndTable(); + } + ImGui::PopFont(); + + ImGui::Text("input latch"); + if (ImGui::MenuItem("set latch",BIND_FOR(GUI_ACTION_PAT_LATCH))) { + // TODO + } + ImGui::Separator(); + + if (ImGui::MenuItem("note up",BIND_FOR(GUI_ACTION_PAT_NOTE_UP))) doTranspose(1); + if (ImGui::MenuItem("note down",BIND_FOR(GUI_ACTION_PAT_NOTE_DOWN))) doTranspose(-1); + if (ImGui::MenuItem("octave up",BIND_FOR(GUI_ACTION_PAT_OCTAVE_UP))) doTranspose(12); + if (ImGui::MenuItem("octave down",BIND_FOR(GUI_ACTION_PAT_OCTAVE_DOWN))) doTranspose(-12); + if (ImGui::InputInt("##TransposeAmount",&transposeAmount,1,1)) { + if (transposeAmount<-96) transposeAmount=-96; + if (transposeAmount>96) transposeAmount=96; + } + ImGui::SameLine(); + if (ImGui::Button("Transpose")) { + doTranspose(transposeAmount); + ImGui::CloseCurrentPopup(); + } + + ImGui::Separator(); + if (ImGui::MenuItem("interpolate",BIND_FOR(GUI_ACTION_PAT_INTERPOLATE))) doInterpolate(); + if (ImGui::BeginMenu("change instrument...")) { + if (e->song.ins.empty()) { + ImGui::Text("no instruments available"); + } + for (size_t i=0; isong.ins.size(); i++) { + snprintf(id,4095,"%.2X: %s",(int)i,e->song.ins[i]->name.c_str()); + if (ImGui::MenuItem(id)) { + doChangeIns(i); + } + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("gradient/fade...")) { + if (ImGui::InputInt("Start",&fadeMin,1,1)) { + if (fadeMin<0) fadeMin=0; + if (fadeMode) { + if (fadeMin>15) fadeMin=15; + } else { + if (fadeMin>255) fadeMin=255; + } + } + if (ImGui::InputInt("End",&fadeMax,1,1)) { + if (fadeMax<0) fadeMax=0; + if (fadeMode) { + if (fadeMax>15) fadeMax=15; + } else { + if (fadeMax>255) fadeMax=255; + } + } + if (ImGui::Checkbox("Nibble mode",&fadeMode)) { + if (fadeMode) { + if (fadeMin>15) fadeMin=15; + if (fadeMax>15) fadeMax=15; + } else { + if (fadeMin>255) fadeMin=255; + if (fadeMax>255) fadeMax=255; + } + } + if (ImGui::Button("Go ahead")) { + doFade(fadeMin,fadeMax,fadeMode); + ImGui::CloseCurrentPopup(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("scale...")) { + if (ImGui::InputFloat("##ScaleMax",&scaleMax,1,1,"%.1f%%")) { + if (scaleMax<0.0f) scaleMax=0.0f; + if (scaleMax>25600.0f) scaleMax=25600.0f; + } + if (ImGui::Button("Scale")) { + doScale(scaleMax); + ImGui::CloseCurrentPopup(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("randomize...")) { + if (ImGui::InputInt("Minimum",&randomizeMin,1,1)) { + if (randomizeMin<0) randomizeMin=0; + if (randomMode) { + if (randomizeMin>15) randomizeMin=15; + } else { + if (randomizeMin>255) randomizeMin=255; + } + if (randomizeMin>randomizeMax) randomizeMin=randomizeMax; + } + if (ImGui::InputInt("Maximum",&randomizeMax,1,1)) { + if (randomizeMax<0) randomizeMax=0; + if (randomizeMax15) randomizeMax=15; + } else { + if (randomizeMax>255) randomizeMax=255; + } + } + if (ImGui::Checkbox("Nibble mode",&randomMode)) { + if (randomMode) { + if (randomizeMin>15) randomizeMin=15; + if (randomizeMax>15) randomizeMax=15; + } else { + if (randomizeMin>255) randomizeMin=255; + if (randomizeMax>255) randomizeMax=255; + } + } + // TODO: add an option to set effect to specific value? + if (ImGui::Button("Randomize")) { + doRandomize(randomizeMin,randomizeMax,randomMode); + ImGui::CloseCurrentPopup(); + } + ImGui::EndMenu(); + } + if (ImGui::MenuItem("invert values",BIND_FOR(GUI_ACTION_PAT_INVERT_VALUES))) doInvertValues(); + + ImGui::Separator(); + + if (ImGui::MenuItem("flip selection",BIND_FOR(GUI_ACTION_PAT_FLIP_SELECTION))) doFlip(); + if (ImGui::MenuItem("collapse",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_ROWS))) doCollapse(2); + if (ImGui::MenuItem("expand",BIND_FOR(GUI_ACTION_PAT_EXPAND_ROWS))) doExpand(2); + + if (topMenu) { + ImGui::Separator(); + ImGui::MenuItem("collapse pattern",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_PAT)); + ImGui::MenuItem("expand pattern",BIND_FOR(GUI_ACTION_PAT_EXPAND_PAT)); + + ImGui::Separator(); + ImGui::MenuItem("collapse song",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_SONG)); + ImGui::MenuItem("expand song",BIND_FOR(GUI_ACTION_PAT_EXPAND_SONG)); + } +} + bool FurnaceGUI::loop() { while (!quit) { SDL_Event ev; @@ -4641,7 +5532,9 @@ bool FurnaceGUI::loop() { sysAddOption(DIV_SYSTEM_AY8930); sysAddOption(DIV_SYSTEM_LYNX); sysAddOption(DIV_SYSTEM_QSOUND); + sysAddOption(DIV_SYSTEM_X1_010); sysAddOption(DIV_SYSTEM_SWAN); + sysAddOption(DIV_SYSTEM_VERA); ImGui::EndMenu(); } if (ImGui::BeginMenu("configure system...")) { @@ -4760,7 +5653,7 @@ bool FurnaceGUI::loop() { break; } case DIV_SYSTEM_YM2151: - if (ImGui::RadioButton("NTSC (3.58MHz)",flags==0)) { + if (ImGui::RadioButton("NTSC/X16 (3.58MHz)",flags==0)) { e->setSysFlags(i,0,restart); updateWindowTitle(); } @@ -4768,7 +5661,7 @@ bool FurnaceGUI::loop() { e->setSysFlags(i,1,restart); updateWindowTitle(); } - if (ImGui::RadioButton("X68000 (4MHz)",flags==2)) { + if (ImGui::RadioButton("X1/X68000 (4MHz)",flags==2)) { e->setSysFlags(i,2,restart); updateWindowTitle(); } @@ -4787,6 +5680,21 @@ bool FurnaceGUI::loop() { updateWindowTitle(); } break; + case DIV_SYSTEM_C64_8580: + case DIV_SYSTEM_C64_6581: + if (ImGui::RadioButton("NTSC (1.02MHz)",flags==0)) { + e->setSysFlags(i,0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (0.99MHz)",flags==1)) { + e->setSysFlags(i,1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("SSI 2001 (0.89MHz)",flags==2)) { + e->setSysFlags(i,2,restart); + updateWindowTitle(); + } + break; case DIV_SYSTEM_AY8910: case DIV_SYSTEM_AY8930: { ImGui::Text("Clock rate:"); @@ -4802,7 +5710,7 @@ bool FurnaceGUI::loop() { e->setSysFlags(i,(flags&(~15))|2,restart); updateWindowTitle(); } - if (ImGui::RadioButton("2MHz (Atari ST)",(flags&15)==3)) { + if (ImGui::RadioButton("2MHz (Atari ST/Sharp X1)",(flags&15)==3)) { e->setSysFlags(i,(flags&(~15))|3,restart); updateWindowTitle(); } @@ -4848,6 +5756,10 @@ bool FurnaceGUI::loop() { e->setSysFlags(i,(flags&(~0x30))|32,restart); updateWindowTitle(); } + if (ImGui::RadioButton("AY-3-8914",(flags&0x30)==48)) { + e->setSysFlags(i,(flags&(~0x30))|48,restart); + updateWindowTitle(); + } } bool stereo=flags&0x40; ImGui::BeginDisabled((flags&0x30)==32); @@ -4934,8 +5846,26 @@ bool FurnaceGUI::loop() { } rightClickable break; } + case DIV_SYSTEM_X1_010: { + ImGui::Text("Clock rate:"); + if (ImGui::RadioButton("16MHz (Seta 1)",(flags&15)==0)) { + e->setSysFlags(i,(flags&(~16))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("16.67MHz (Seta 2)",(flags&15)==1)) { + e->setSysFlags(i,(flags&(~16))|1,restart); + updateWindowTitle(); + } + bool x1_010Stereo=flags&16; + if (ImGui::Checkbox("Stereo",&x1_010Stereo)) { + e->setSysFlags(i,(flags&(~15))|(x1_010Stereo<<4),restart); + updateWindowTitle(); + } + break; + } case DIV_SYSTEM_GB: case DIV_SYSTEM_SWAN: + case DIV_SYSTEM_VERA: case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_FULL: @@ -4994,7 +5924,9 @@ bool FurnaceGUI::loop() { sysChangeOption(i,DIV_SYSTEM_AY8930); sysChangeOption(i,DIV_SYSTEM_LYNX); sysChangeOption(i,DIV_SYSTEM_QSOUND); + sysChangeOption(i,DIV_SYSTEM_X1_010); sysChangeOption(i,DIV_SYSTEM_SWAN); + sysChangeOption(i,DIV_SYSTEM_VERA); ImGui::EndMenu(); } } @@ -5024,16 +5956,7 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem("undo",BIND_FOR(GUI_ACTION_UNDO))) doUndo(); if (ImGui::MenuItem("redo",BIND_FOR(GUI_ACTION_REDO))) doRedo(); ImGui::Separator(); - if (ImGui::MenuItem("cut",BIND_FOR(GUI_ACTION_PAT_CUT))) doCopy(true); - if (ImGui::MenuItem("copy",BIND_FOR(GUI_ACTION_PAT_COPY))) doCopy(false); - if (ImGui::MenuItem("paste",BIND_FOR(GUI_ACTION_PAT_PASTE))) doPaste(); - if (ImGui::MenuItem("delete",BIND_FOR(GUI_ACTION_PAT_DELETE))) doDelete(); - if (ImGui::MenuItem("select all",BIND_FOR(GUI_ACTION_PAT_SELECT_ALL))) doSelectAll(); - ImGui::Separator(); - if (ImGui::MenuItem("note up",BIND_FOR(GUI_ACTION_PAT_NOTE_UP))) doTranspose(1); - if (ImGui::MenuItem("note down",BIND_FOR(GUI_ACTION_PAT_NOTE_DOWN))) doTranspose(-1); - if (ImGui::MenuItem("octave up",BIND_FOR(GUI_ACTION_PAT_OCTAVE_UP))) doTranspose(12); - if (ImGui::MenuItem("octave down",BIND_FOR(GUI_ACTION_PAT_OCTAVE_DOWN))) doTranspose(-12); + editOptions(true); /*ImGui::Separator(); ImGui::MenuItem("clear...");*/ ImGui::EndMenu(); @@ -5174,49 +6097,52 @@ bool FurnaceGUI::loop() { drawChannels(); drawRegView(); - if (ImGuiFileDialog::Instance()->Display("FileDialog",ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoMove,ImVec2(600.0f*dpiScale,400.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale))) { + if (firstFrame) { + firstFrame=false; + if (patternOpen) nextWindow=GUI_WINDOW_PATTERN; + } + + if (fileDialog->render(ImVec2(600.0f*dpiScale,400.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale))) { //ImGui::GetIO().ConfigFlags&=~ImGuiConfigFlags_NavEnableKeyboard; switch (curFileDialog) { case GUI_FILE_OPEN: case GUI_FILE_SAVE: case GUI_FILE_SAVE_DMF_LEGACY: - workingDirSong=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirSong=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_INS_OPEN: case GUI_FILE_INS_SAVE: - workingDirIns=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirIns=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_WAVE_OPEN: case GUI_FILE_WAVE_SAVE: - workingDirWave=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirWave=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_SAMPLE_OPEN: case GUI_FILE_SAMPLE_SAVE: - workingDirSample=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirSample=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_EXPORT_AUDIO_ONE: case GUI_FILE_EXPORT_AUDIO_PER_SYS: case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: - workingDirAudioExport=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirAudioExport=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_EXPORT_VGM: case GUI_FILE_EXPORT_ROM: - workingDirVGMExport=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirVGMExport=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_LOAD_MAIN_FONT: case GUI_FILE_LOAD_PAT_FONT: - workingDirFont=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirFont=fileDialog->getPath()+DIR_SEPARATOR_STR; break; } - if (ImGuiFileDialog::Instance()->IsOk()) { - fileName=ImGuiFileDialog::Instance()->GetFilePathName(); + if (fileDialog->accepted()) { + fileName=fileDialog->getFileName(); if (fileName!="") { if (curFileDialog==GUI_FILE_SAVE) { - if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song") { - checkExtension(".fur"); - } else { - checkExtension(".dmf"); - } + // we can't tell whether the user chose .dmf or .fur in the system file picker + const char* fallbackExt=(settings.sysFileDialog || ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song")?".fur":".dmf"; + checkExtensionDual(".fur",".dmf",fallbackExt); } if (curFileDialog==GUI_FILE_SAVE_DMF_LEGACY) { checkExtension(".dmf"); @@ -5243,9 +6169,13 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf("Error while loading file! (%s)",lastError)); } break; - case GUI_FILE_SAVE: - printf("saving: %s\n",copyOfName.c_str()); - if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song") { + case GUI_FILE_SAVE: { + logD("saving: %s\n",copyOfName.c_str()); + String lowerCase=fileName; + for (char& i: lowerCase) { + if (i>='A' && i<='Z') i+='a'-'A'; + } + if ((lowerCase.size()<4 || lowerCase.rfind(".dmf")!=lowerCase.size()-4)) { if (save(copyOfName,0)>0) { showError(fmt::sprintf("Error while saving file! (%s)",lastError)); } @@ -5255,8 +6185,9 @@ bool FurnaceGUI::loop() { } } break; + } case GUI_FILE_SAVE_DMF_LEGACY: - printf("saving: %s\n",copyOfName.c_str()); + logD("saving: %s\n",copyOfName.c_str()); if (save(copyOfName,24)>0) { showError(fmt::sprintf("Error while saving file! (%s)",lastError)); } @@ -5335,7 +6266,7 @@ bool FurnaceGUI::loop() { curFileDialog=GUI_FILE_OPEN; } } - ImGuiFileDialog::Instance()->Close(); + fileDialog->close(); } if (warnQuit) { @@ -5578,6 +6509,8 @@ void FurnaceGUI::applyUISettings() { GET_UI_COLOR(GUI_COLOR_INSTR_BEEPER,ImVec4(0.0f,1.0f,0.0f,1.0f)); GET_UI_COLOR(GUI_COLOR_INSTR_SWAN,ImVec4(0.3f,0.5f,1.0f,1.0f)); GET_UI_COLOR(GUI_COLOR_INSTR_MIKEY,ImVec4(0.5f,1.0f,0.3f,1.0f)); + GET_UI_COLOR(GUI_COLOR_INSTR_VERA,ImVec4(0.4f,0.6f,1.0f,1.0f)); + GET_UI_COLOR(GUI_COLOR_INSTR_X1_010,ImVec4(0.3f,0.5f,1.0f,1.0f)); GET_UI_COLOR(GUI_COLOR_INSTR_UNKNOWN,ImVec4(0.3f,0.3f,0.3f,1.0f)); GET_UI_COLOR(GUI_COLOR_CHANNEL_FM,ImVec4(0.2f,0.8f,1.0f,1.0f)); GET_UI_COLOR(GUI_COLOR_CHANNEL_PULSE,ImVec4(0.4f,1.0f,0.2f,1.0f)); @@ -5811,6 +6744,9 @@ void FurnaceGUI::applyUISettings() { if ((bigFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(font_plexSans_compressed_data,font_plexSans_compressed_size,40*dpiScale))==NULL) { logE("could not load big UI font!\n"); } + + if (fileDialog!=NULL) delete fileDialog; + fileDialog=new FurnaceGUIFileDialog(settings.sysFileDialog); } bool FurnaceGUI::init() { @@ -5940,6 +6876,7 @@ bool FurnaceGUI::init() { ImGui::GetIO().IniFilename=finalLayoutPath; ImGui::LoadIniSettingsFromDisk(finalLayoutPath); + // TODO: allow changing these colors. ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeDir,"",ImVec4(0.0f,1.0f,1.0f,1.0f),ICON_FA_FOLDER_O); ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeFile,"",ImVec4(0.7f,0.7f,0.7f,1.0f),ICON_FA_FILE_O); ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fur",ImVec4(0.5f,1.0f,0.5f,1.0f),ICON_FA_FILE); @@ -5965,6 +6902,8 @@ bool FurnaceGUI::init() { oldPat[i]=new DivPattern; } + firstFrame=true; + #ifdef __APPLE__ SDL_RaiseWindow(sdlWin); #endif @@ -6033,6 +6972,7 @@ FurnaceGUI::FurnaceGUI(): displayNew(false), curFileDialog(GUI_FILE_OPEN), warnAction(GUI_WARN_OPEN), + fileDialog(NULL), scrW(1280), scrH(800), dpiScale(1), @@ -6091,9 +7031,20 @@ FurnaceGUI::FurnaceGUI(): demandScrollX(false), fancyPattern(false), wantPatName(false), + firstFrame(true), curWindow(GUI_WINDOW_NOTHING), nextWindow(GUI_WINDOW_NOTHING), nextDesc(NULL), + opMaskNote(true), + opMaskIns(true), + opMaskVol(true), + opMaskEffect(true), + opMaskEffectVal(true), + latchNote(-1), + latchIns(-2), + latchVol(-1), + latchEffect(-1), + latchEffectVal(-1), wavePreviewOn(false), wavePreviewKey((SDL_Scancode)0), wavePreviewNote(0), @@ -6135,6 +7086,14 @@ FurnaceGUI::FurnaceGUI(): bindSetPending(false), nextScroll(-1.0f), nextAddScroll(0.0f), + transposeAmount(0), + randomizeMin(0), + randomizeMax(255), + fadeMin(0), + fadeMax(255), + scaleMax(100.0f), + fadeMode(false), + randomMode(false), oldOrdersLen(0) { // octave 1 @@ -6265,6 +7224,12 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2413 (drums mode)", { + DIV_SYSTEM_OPLL_DRUMS, 64, 0, 0, + 0 + } + )); sysCategories.push_back(cat); cat=FurnaceGUISysCategory("Square"); @@ -6307,6 +7272,12 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Seta/Allumer X1-010", { + DIV_SYSTEM_X1_010, 64, 0, 0, + 0 + } + )); sysCategories.push_back(cat); cat=FurnaceGUISysCategory("Game consoles"); @@ -6378,7 +7349,7 @@ FurnaceGUI::FurnaceGUI(): )); cat.systems.push_back(FurnaceGUISysDef( "Mattel Intellivision", { - DIV_SYSTEM_AY8910, 64, 0, 6, + DIV_SYSTEM_AY8910, 64, 0, 48, 0 } )); @@ -6418,6 +7389,12 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Gamate", { + DIV_SYSTEM_AY8910, 64, 0, 73, + 0 + } + )); sysCategories.push_back(cat); cat=FurnaceGUISysCategory("Computers"); @@ -6445,6 +7422,35 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + /* + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (6581 SID + Sound Expander)", { + DIV_SYSTEM_OPL, 64, 0, 0, + DIV_SYSTEM_C64_6581, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (6581 SID + Sound Expander with drums mode)", { + DIV_SYSTEM_OPL_DRUMS, 64, 0, 0, + DIV_SYSTEM_C64_6581, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (8580 SID + Sound Expander)", { + DIV_SYSTEM_OPL, 64, 0, 0, + DIV_SYSTEM_C64_8580, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (8580 SID + Sound Expander with drums mode)", { + DIV_SYSTEM_OPL_DRUMS, 64, 0, 0, + DIV_SYSTEM_C64_8580, 64, 0, 1, + 0 + } + ));*/ cat.systems.push_back(FurnaceGUISysDef( "Amiga", { DIV_SYSTEM_AMIGA, 64, 0, 0, @@ -6457,6 +7463,27 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "MSX + SFG-01", { + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 16, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "MSX + MSX-MUSIC", { + DIV_SYSTEM_OPLL, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 16, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "MSX + MSX-MUSIC (drums mode)", { + DIV_SYSTEM_OPLL_DRUMS, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 16, + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "ZX Spectrum (48K)", { DIV_SYSTEM_AY8910, 64, 0, 2, @@ -6500,6 +7527,13 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + SSI 2001", { + DIV_SYSTEM_C64_6581, 64, 0, 2, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "PC + Game Blaster", { DIV_SYSTEM_SAA1099, 64, -127, 1, @@ -6522,6 +7556,24 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Sound Blaster w/Game Blaster Compatible", { + DIV_SYSTEM_OPL2, 64, 0, 0, + DIV_SYSTEM_SAA1099, 64, -127, 1, + DIV_SYSTEM_SAA1099, 64, 127, 1, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Sound Blaster w/Game Blaster Compatible (drums mode)", { + DIV_SYSTEM_OPL2_DRUMS, 64, 0, 0, + DIV_SYSTEM_SAA1099, 64, -127, 1, + DIV_SYSTEM_SAA1099, 64, 127, 1, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "PC + Sound Blaster Pro 2", { DIV_SYSTEM_OPL3, 64, 0, 0, @@ -6536,13 +7588,34 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Sharp X1", { + DIV_SYSTEM_AY8910, 64, 0, 3, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sharp X1 + FM Addon", { + DIV_SYSTEM_AY8910, 64, 0, 3, + DIV_SYSTEM_YM2151, 64, 0, 2, + 0 + } + )); /* cat.systems.push_back(FurnaceGUISysDef( "Sharp X68000", { - DIV_SYSTEM_AY8910, 64, 0, 16, + DIV_SYSTEM_YM2151, 64, 0, 2, + DIV_SYSTEM_MSM6258, 64, 0, 0, 0 } ));*/ + cat.systems.push_back(FurnaceGUISysDef( + "Commander X16", { + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_VERA, 64, 0, 0, + 0 + } + )); sysCategories.push_back(cat); cat=FurnaceGUISysCategory("Arcade systems"); @@ -6562,7 +7635,7 @@ FurnaceGUI::FurnaceGUI(): )); cat.systems.push_back(FurnaceGUISysDef( "Sega OutRun/X Board", { - DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_YM2151, 64, 0, 2, DIV_SYSTEM_SEGAPCM, 64, 0, 0, 0 } @@ -6597,6 +7670,18 @@ FurnaceGUI::FurnaceGUI(): 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Seta 1", { + DIV_SYSTEM_X1_010, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Seta 2", { + DIV_SYSTEM_X1_010, 64, 0, 1, + 0 + } + )); sysCategories.push_back(cat); cat=FurnaceGUISysCategory("DefleMask-compatible"); diff --git a/src/gui/gui.h b/src/gui/gui.h index 7d7f6b4a..22e8036e 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -27,6 +27,8 @@ #include #include +#include "fileDialog.h" + #define rightClickable if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) ImGui::SetKeyboardFocusHere(-1); #define handleUnimportant if (settings.insFocusesPattern && patternOpen) {nextWindow=GUI_WINDOW_PATTERN;} @@ -73,6 +75,8 @@ enum FurnaceGUIColors { GUI_COLOR_INSTR_BEEPER, GUI_COLOR_INSTR_SWAN, GUI_COLOR_INSTR_MIKEY, + GUI_COLOR_INSTR_VERA, + GUI_COLOR_INSTR_X1_010, GUI_COLOR_INSTR_UNKNOWN, GUI_COLOR_CHANNEL_FM, GUI_COLOR_CHANNEL_PULSE, @@ -233,6 +237,10 @@ enum FurnaceGUIActions { GUI_ACTION_PAT_CUT, GUI_ACTION_PAT_COPY, GUI_ACTION_PAT_PASTE, + GUI_ACTION_PAT_PASTE_MIX, + GUI_ACTION_PAT_PASTE_MIX_BG, + GUI_ACTION_PAT_PASTE_FLOOD, + GUI_ACTION_PAT_PASTE_OVERFLOW, GUI_ACTION_PAT_CURSOR_UP, GUI_ACTION_PAT_CURSOR_DOWN, GUI_ACTION_PAT_CURSOR_LEFT, @@ -268,6 +276,17 @@ enum FurnaceGUIActions { GUI_ACTION_PAT_COLLAPSE, GUI_ACTION_PAT_INCREASE_COLUMNS, GUI_ACTION_PAT_DECREASE_COLUMNS, + GUI_ACTION_PAT_INTERPOLATE, + GUI_ACTION_PAT_FADE, + GUI_ACTION_PAT_INVERT_VALUES, + GUI_ACTION_PAT_FLIP_SELECTION, + GUI_ACTION_PAT_COLLAPSE_ROWS, + GUI_ACTION_PAT_EXPAND_ROWS, + GUI_ACTION_PAT_COLLAPSE_PAT, + GUI_ACTION_PAT_EXPAND_PAT, + GUI_ACTION_PAT_COLLAPSE_SONG, + GUI_ACTION_PAT_EXPAND_SONG, + GUI_ACTION_PAT_LATCH, GUI_ACTION_PAT_MAX, GUI_ACTION_INS_LIST_MIN, @@ -334,6 +353,14 @@ enum FurnaceGUIActions { GUI_ACTION_MAX }; +enum PasteMode { + GUI_PASTE_MODE_NORMAL=0, + GUI_PASTE_MODE_MIX_FG, + GUI_PASTE_MODE_MIX_BG, + GUI_PASTE_MODE_FLOOD, + GUI_PASTE_MODE_OVERFLOW +}; + #define FURKMOD_CTRL (1<<31) #define FURKMOD_SHIFT (1<<29) #define FURKMOD_META (1<<28) @@ -354,7 +381,16 @@ enum ActionType { GUI_UNDO_PATTERN_PULL, GUI_UNDO_PATTERN_PUSH, GUI_UNDO_PATTERN_CUT, - GUI_UNDO_PATTERN_PASTE + GUI_UNDO_PATTERN_PASTE, + GUI_UNDO_PATTERN_CHANGE_INS, + GUI_UNDO_PATTERN_INTERPOLATE, + GUI_UNDO_PATTERN_FADE, + GUI_UNDO_PATTERN_SCALE, + GUI_UNDO_PATTERN_RANDOMIZE, + GUI_UNDO_PATTERN_INVERT_VAL, + GUI_UNDO_PATTERN_FLIP, + GUI_UNDO_PATTERN_COLLAPSE, + GUI_UNDO_PATTERN_EXPAND }; struct UndoPatternData { @@ -442,6 +478,8 @@ class FurnaceGUI { FurnaceGUIFileDialogs curFileDialog; FurnaceGUIWarnings warnAction; + FurnaceGUIFileDialog* fileDialog; + int scrW, scrH; double dpiScale; @@ -499,6 +537,11 @@ class FurnaceGUI { int guiColorsBase; int avoidRaisingPattern; int insFocusesPattern; + int stepOnInsert; + // TODO flags + int unifiedDataView; + int sysFileDialog; + // end unsigned int maxUndoSteps; String mainFontPath; String patFontPath; @@ -512,7 +555,7 @@ class FurnaceGUI { audioQuality(0), arcadeCore(0), ym2612Core(0), - saaCore(0), + saaCore(1), mainFont(0), patFont(0), audioRate(44100), @@ -542,6 +585,9 @@ class FurnaceGUI { guiColorsBase(0), avoidRaisingPattern(0), insFocusesPattern(1), + stepOnInsert(0), + unifiedDataView(0), + sysFileDialog(1), maxUndoSteps(100), mainFontPath(""), patFontPath(""), @@ -558,13 +604,16 @@ class FurnaceGUI { bool pianoOpen, notesOpen, channelsOpen, regViewOpen; SelectionPoint selStart, selEnd, cursor; bool selecting, curNibble, orderNibble, followOrders, followPattern, changeAllOrders; - bool collapseWindow, demandScrollX, fancyPattern, wantPatName; + bool collapseWindow, demandScrollX, fancyPattern, wantPatName, firstFrame; FurnaceGUIWindows curWindow, nextWindow; float peak[2]; float patChanX[DIV_MAX_CHANS+1]; float patChanSlideY[DIV_MAX_CHANS+1]; const int* nextDesc; + bool opMaskNote, opMaskIns, opMaskVol, opMaskEffect, opMaskEffectVal; + short latchNote, latchIns, latchVol, latchEffect, latchEffectVal; + // bit 31: ctrl // bit 30: reserved for SDL scancode mask // bit 29: shift @@ -624,6 +673,7 @@ class FurnaceGUI { bool macroDragInitialValueSet; bool macroDragInitialValue; bool macroDragChar; + bool macroDragLineMode; // TODO bool macroDragActive; ImVec2 macroLoopDragStart; @@ -650,6 +700,9 @@ class FurnaceGUI { ImVec2 threeChars, twoChars; SelectionPoint sel1, sel2; int dummyRows, demandX; + int transposeAmount, randomizeMin, randomizeMax, fadeMin, fadeMax; + float scaleMax; + bool fadeMode, randomMode; int oldOrdersLen; DivOrders oldOrders; @@ -668,6 +721,9 @@ class FurnaceGUI { void patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord); + void actualWaveList(); + void actualSampleList(); + void drawEditControls(); void drawSongInfo(); void drawOrders(); @@ -718,9 +774,19 @@ class FurnaceGUI { void doInsert(); void doTranspose(int amount); void doCopy(bool cut); - void doPaste(); + void doPaste(PasteMode mode=GUI_PASTE_MODE_NORMAL); + void doChangeIns(int ins); + void doInterpolate(); + void doFade(int p0, int p1, bool mode); + void doInvertValues(); + void doScale(float top); + void doRandomize(int bottom, int top, bool mode); + void doFlip(); + void doCollapse(int divider); + void doExpand(int multiplier); void doUndo(); void doRedo(); + void editOptions(bool topMenu); void play(int row=0); void stop(); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index c872b05b..83220d25 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -19,6 +19,7 @@ // guiConst: constants used in the GUI like arrays, strings and other stuff #include "guiConst.h" +#include "../engine/instrument.h" const int opOrder[4]={ 0, 2, 1, 3 @@ -64,3 +65,31 @@ const char* pitchLabel[11]={ "1/6", "1/5", "1/4", "1/3", "1/2", "1x", "2x", "3x", "4x", "5x", "6x" }; +const char* insTypes[DIV_INS_MAX]={ + "Standard", + "FM (4-operator)", + "Game Boy", + "C64", + "Amiga/Sample", + "PC Engine", + "AY-3-8910/SSG", + "AY8930", + "TIA", + "SAA1099", + "VIC", + "PET", + "VRC6", + "FM (OPLL)", + "FM (OPL)", + "FDS", + "Virtual Boy", + "Namco 163", + "Konami SCC", + "FM (OPZ)", + "POKEY", + "PC Beeper", + "WonderSwan", + "Atari Lynx", + "VERA", + "X1-010" +}; \ No newline at end of file diff --git a/src/gui/guiConst.h b/src/gui/guiConst.h index 3d252b53..9f415915 100644 --- a/src/gui/guiConst.h +++ b/src/gui/guiConst.h @@ -23,3 +23,4 @@ extern const int opOrder[4]; extern const char* noteNames[180]; extern const char* noteNamesG[180]; extern const char* pitchLabel[11]; +extern const char* insTypes[]; \ No newline at end of file diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index aaab3f64..828eced3 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -27,33 +27,6 @@ #include #include "plot_nolerp.h" -const char* insTypes[24]={ - "Standard", - "FM (4-operator)", - "Game Boy", - "C64", - "Amiga/Sample", - "PC Engine", - "AY-3-8910/SSG", - "AY8930", - "TIA", - "SAA1099", - "VIC", - "PET", - "VRC6", - "FM (OPLL)", - "FM (OPL)", - "FDS", - "Virtual Boy", - "Namco 163", - "Konami SCC", - "FM (OPZ)", - "POKEY", - "PC Beeper", - "WonderSwan", - "Atari Lynx" -}; - const char* ssgEnvTypes[8]={ "Down Down Down", "Down.", "Down Up Down Up", "Down UP", "Up Up Up", "Up.", "Up Down Up Down", "Up DOWN" }; @@ -156,6 +129,10 @@ const char* mikeyFeedbackBits[11] = { "0", "1", "2", "3", "4", "5", "7", "10", "11", "int", NULL }; +const char* x1_010EnvBits[8]={ + "enable", "oneshot", "split L/R", "HinvR", "VinvR", "HinvL", "VinvL", NULL +}; + const char* oneBit[2]={ "on", NULL }; @@ -791,9 +768,9 @@ void FurnaceGUI::drawInsEdit() { } else { DivInstrument* ins=e->song.ins[curIns]; ImGui::InputText("Name",&ins->name); - if (ins->type<0 || ins->type>23) ins->type=DIV_INS_FM; + if (ins->type<0 || ins->type>=DIV_INS_MAX) ins->type=DIV_INS_FM; int insType=ins->type; - if (ImGui::Combo("Type",&insType,insTypes,24,24)) { + if (ImGui::Combo("Type",&insType,insTypes,DIV_INS_MAX,DIV_INS_MAX)) { ins->type=(DivInstrumentType)insType; } @@ -1355,7 +1332,7 @@ void FurnaceGUI::drawInsEdit() { float loopIndicator[256]; const char* volumeLabel="Volume"; - int volMax=(ins->type==DIV_INS_PCE || ins->type==DIV_INS_AY8930)?31:15; + int volMax=15; int volMin=0; if (ins->type==DIV_INS_C64) { if (ins->c64.volIsCutoff) { @@ -1368,6 +1345,12 @@ void FurnaceGUI::drawInsEdit() { } } } + if ((ins->type==DIV_INS_PCE || ins->type==DIV_INS_AY8930)) { + volMax=31; + } + if (ins->type==DIV_INS_VERA) { + volMax=63; + } if (ins->type==DIV_INS_AMIGA) { volMax=64; } @@ -1381,7 +1364,7 @@ void FurnaceGUI::drawInsEdit() { bool arpMode=ins->std.arpMacroMode; const char* dutyLabel="Duty/Noise"; - int dutyMax=(ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?31:3; + int dutyMax=3; if (ins->type==DIV_INS_C64) { dutyLabel="Duty"; if (ins->c64.dutyIsAbs) { @@ -1393,6 +1376,9 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_FM) { dutyMax=32; } + if ((ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)) { + dutyMax=31; + } if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_FM) { dutyLabel="Noise Freq"; } @@ -1416,9 +1402,13 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPL) { dutyMax=0; } + if (ins->type==DIV_INS_VERA) { + dutyLabel="Duty"; + dutyMax=63; + } bool dutyIsRel=(ins->type==DIV_INS_C64 && !ins->c64.dutyIsAbs); - int waveMax=(ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?3:63; + int waveMax=(ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_VERA)?3:63; bool bitMode=false; if (ins->type==DIV_INS_C64 || ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_SAA1099) { bitMode=true; @@ -1435,11 +1425,18 @@ void FurnaceGUI::drawInsEdit() { int ex1Max=(ins->type==DIV_INS_AY8930)?8:0; int ex2Max=(ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?4:0; + bool ex2Bit=true; if (ins->type==DIV_INS_C64) { ex1Max=4; ex2Max=15; } + if (ins->type==DIV_INS_X1_010) { + dutyMax=0; + ex1Max=7; + ex2Max=63; + ex2Bit=false; + } if (ins->type==DIV_INS_SAA1099) ex1Max=8; if (settings.macroView==0) { // modern view @@ -1464,6 +1461,8 @@ void FurnaceGUI::drawInsEdit() { NORMAL_MACRO(ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel,0,ex1Max,"ex1","Filter Mode",64,ins->std.ex1MacroOpen,true,filtModeBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); } else if (ins->type==DIV_INS_SAA1099) { NORMAL_MACRO(ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel,0,ex1Max,"ex1","Envelope",160,ins->std.ex1MacroOpen,true,saaEnvBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); + } else if (ins->type==DIV_INS_X1_010) { + NORMAL_MACRO(ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel,0,ex1Max,"ex1","Envelope Mode",160,ins->std.ex1MacroOpen,true,x1_010EnvBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); } else { NORMAL_MACRO(ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel,0,ex1Max,"ex1","Duty",160,ins->std.ex1MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); } @@ -1472,13 +1471,13 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_C64) { NORMAL_MACRO(ins->std.ex2Macro,ins->std.ex2MacroLen,ins->std.ex2MacroLoop,ins->std.ex2MacroRel,0,ex2Max,"ex2","Resonance",64,ins->std.ex2MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); } else { - NORMAL_MACRO(ins->std.ex2Macro,ins->std.ex2MacroLen,ins->std.ex2MacroLoop,ins->std.ex2MacroRel,0,ex2Max,"ex2","Envelope",64,ins->std.ex2MacroOpen,true,ayEnvBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); + NORMAL_MACRO(ins->std.ex2Macro,ins->std.ex2MacroLen,ins->std.ex2MacroLoop,ins->std.ex2MacroRel,0,ex2Max,"ex2","Envelope",ex2Bit?64:160,ins->std.ex2MacroOpen,ex2Bit,ayEnvBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); } } if (ins->type==DIV_INS_C64) { NORMAL_MACRO(ins->std.ex3Macro,ins->std.ex3MacroLen,ins->std.ex3MacroLoop,ins->std.ex3MacroRel,0,2,"ex3","Special",32,ins->std.ex3MacroOpen,true,c64SpecialBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,2,NULL,false); } - if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930) { + if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_X1_010) { NORMAL_MACRO(ins->std.ex3Macro,ins->std.ex3MacroLen,ins->std.ex3MacroLoop,ins->std.ex3MacroRel,0,15,"ex3","AutoEnv Num",96,ins->std.ex3MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,15,NULL,false); NORMAL_MACRO(ins->std.algMacro,ins->std.algMacroLen,ins->std.algMacroLoop,ins->std.algMacroRel,0,15,"alg","AutoEnv Den",96,ins->std.algMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[7],0,15,NULL,false); } @@ -1759,7 +1758,6 @@ void FurnaceGUI::drawWaveList() { nextWindow=GUI_WINDOW_NOTHING; } if (!waveListOpen) return; - float wavePreview[256]; if (ImGui::Begin("Wavetables",&waveListOpen)) { if (ImGui::Button(ICON_FA_PLUS "##WaveAdd")) { doAction(GUI_ACTION_WAVE_LIST_ADD); @@ -1790,25 +1788,7 @@ void FurnaceGUI::drawWaveList() { } ImGui::Separator(); if (ImGui::BeginTable("WaveListScroll",1,ImGuiTableFlags_ScrollY)) { - for (int i=0; i<(int)e->song.wave.size(); i++) { - DivWavetable* wave=e->song.wave[i]; - for (int i=0; ilen; i++) { - wavePreview[i]=wave->data[i]; - } - if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if (ImGui::Selectable(fmt::sprintf("%d##_WAVE%d\n",i,i).c_str(),curWave==i)) { - curWave=i; - } - if (ImGui::IsItemHovered()) { - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - waveEditOpen=true; - } - } - ImGui::SameLine(); - PlotNoLerp(fmt::sprintf("##_WAVEP%d",i).c_str(),wavePreview,wave->len+1,0,NULL,0,wave->max); - } + actualWaveList(); ImGui::EndTable(); } } @@ -1857,6 +1837,7 @@ void FurnaceGUI::drawWaveEdit() { modified=true; } for (int i=0; ilen; i++) { + if (wave->data[i]>wave->max) wave->data[i]=wave->max; wavePreview[i]=wave->data[i]; } if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index 7841667f..38a09340 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -830,22 +830,8 @@ void FurnaceGUI::drawPattern() { ImGui::PopStyleVar(); if (patternOpen) { if (!inhibitMenu && ImGui::IsItemClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("patternActionMenu"); - if (ImGui::BeginPopup("patternActionMenu",ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings)) { - char id[4096]; - ImGui::Selectable("cut"); - ImGui::Selectable("copy"); - ImGui::Selectable("paste"); - if (ImGui::BeginMenu("change instrument...")) { - if (e->song.ins.empty()) { - ImGui::Text("no instruments available"); - } - for (size_t i=0; isong.ins.size(); i++) { - snprintf(id,4095,"%.2X: %s",(int)i,e->song.ins[i]->name.c_str()); - if (ImGui::Selectable(id)) { // TODO - } - } - ImGui::EndMenu(); - } + if (ImGui::BeginPopup("patternActionMenu",ImGuiWindowFlags_NoMove|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings)) { + editOptions(false); ImGui::EndPopup(); } } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 3f0e532d..a10795fb 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -157,6 +157,11 @@ void FurnaceGUI::drawSettings() { settings.stepOnDelete=stepOnDeleteB; } + bool stepOnInsertB=settings.stepOnInsert; + if (ImGui::Checkbox("Move cursor by edit step on insert (push)",&stepOnInsertB)) { + settings.stepOnInsert=stepOnInsertB; + } + bool allowEditDockingB=settings.allowEditDocking; if (ImGui::Checkbox("Allow docking editors",&allowEditDockingB)) { settings.allowEditDocking=allowEditDockingB; @@ -177,6 +182,11 @@ void FurnaceGUI::drawSettings() { settings.restartOnFlagChange=restartOnFlagChangeB; } + bool sysFileDialogB=settings.sysFileDialog; + if (ImGui::Checkbox("Use system file picker",&sysFileDialogB)) { + settings.sysFileDialog=sysFileDialogB; + } + ImGui::Text("Wrap pattern cursor horizontally:"); if (ImGui::RadioButton("No##wrapH0",settings.wrapHorizontal==0)) { settings.wrapHorizontal=0; @@ -206,6 +216,7 @@ void FurnaceGUI::drawSettings() { if (ImGui::RadioButton("Move by Edit Step##cmk1",settings.scrollStep==1)) { settings.scrollStep=1; } + ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Audio")) { @@ -397,6 +408,11 @@ void FurnaceGUI::drawSettings() { settings.macroView=macroViewB; } + bool unifiedDataViewB=settings.unifiedDataView; + if (ImGui::Checkbox("Unified instrument/wavetable/sample list",&unifiedDataViewB)) { + settings.unifiedDataView=unifiedDataViewB; + } + bool chipNamesB=settings.chipNames; if (ImGui::Checkbox("Use chip names instead of system names",&chipNamesB)) { settings.chipNames=chipNamesB; @@ -492,6 +508,8 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_INSTR_BEEPER,"PC Beeper"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_SWAN,"WonderSwan"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_MIKEY,"Lynx"); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_VERA,"VERA"); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_X1_010,"X1-010"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_UNKNOWN,"Other/Unknown"); ImGui::TreePop(); } @@ -853,7 +871,7 @@ void FurnaceGUI::syncSettings() { settings.audioRate=e->getConfInt("audioRate",44100); settings.arcadeCore=e->getConfInt("arcadeCore",0); settings.ym2612Core=e->getConfInt("ym2612Core",0); - settings.saaCore=e->getConfInt("saaCore",0); + settings.saaCore=e->getConfInt("saaCore",1); settings.mainFont=e->getConfInt("mainFont",0); settings.patFont=e->getConfInt("patFont",0); settings.mainFontPath=e->getConfString("mainFontPath",""); @@ -883,6 +901,9 @@ void FurnaceGUI::syncSettings() { settings.guiColorsBase=e->getConfInt("guiColorsBase",0); settings.avoidRaisingPattern=e->getConfInt("avoidRaisingPattern",0); settings.insFocusesPattern=e->getConfInt("insFocusesPattern",1); + settings.stepOnInsert=e->getConfInt("stepOnInsert",0); + settings.unifiedDataView=e->getConfInt("unifiedDataView",0); + settings.sysFileDialog=e->getConfInt("sysFileDialog",1); clampSetting(settings.mainFontSize,2,96); clampSetting(settings.patFontSize,2,96); @@ -920,6 +941,9 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.guiColorsBase,0,1); clampSetting(settings.avoidRaisingPattern,0,1); clampSetting(settings.insFocusesPattern,0,1); + clampSetting(settings.stepOnInsert,0,1); + clampSetting(settings.unifiedDataView,0,1); + clampSetting(settings.sysFileDialog,0,1); // keybinds LOAD_KEYBIND(GUI_ACTION_OPEN,FURKMOD_CMD|SDLK_o); @@ -1118,6 +1142,9 @@ void FurnaceGUI::commitSettings() { e->setConf("guiColorsBase",settings.guiColorsBase); e->setConf("avoidRaisingPattern",settings.avoidRaisingPattern); e->setConf("insFocusesPattern",settings.insFocusesPattern); + e->setConf("stepOnInsert",settings.stepOnInsert); + e->setConf("unifiedDataView",settings.unifiedDataView); + e->setConf("sysFileDialog",settings.sysFileDialog); PUT_UI_COLOR(GUI_COLOR_BACKGROUND); PUT_UI_COLOR(GUI_COLOR_FRAME_BACKGROUND); @@ -1159,6 +1186,8 @@ void FurnaceGUI::commitSettings() { PUT_UI_COLOR(GUI_COLOR_INSTR_BEEPER); PUT_UI_COLOR(GUI_COLOR_INSTR_SWAN); PUT_UI_COLOR(GUI_COLOR_INSTR_MIKEY); + PUT_UI_COLOR(GUI_COLOR_INSTR_VERA); + PUT_UI_COLOR(GUI_COLOR_INSTR_X1_010); PUT_UI_COLOR(GUI_COLOR_INSTR_UNKNOWN); PUT_UI_COLOR(GUI_COLOR_CHANNEL_FM); PUT_UI_COLOR(GUI_COLOR_CHANNEL_PULSE);