diff --git a/.gitignore b/.gitignore index 566a3c3f..25bd6d0e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ linuxbuild/ test/songs/ test/delta/ test/result/ +test/assert_delta android/.gradle/ android/app/build/ android/app/.cxx/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ea45d8e..62064f55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -368,7 +368,26 @@ src/engine/platform/sound/c64/wave8580_PST.cc src/engine/platform/sound/c64/wave8580_P_T.cc src/engine/platform/sound/c64/wave8580__ST.cc -src/engine/platform/sound/tia/TIASnd.cpp +src/engine/platform/sound/c64_fp/Dac.cpp +src/engine/platform/sound/c64_fp/EnvelopeGenerator.cpp +src/engine/platform/sound/c64_fp/ExternalFilter.cpp +src/engine/platform/sound/c64_fp/Filter6581.cpp +src/engine/platform/sound/c64_fp/Filter8580.cpp +src/engine/platform/sound/c64_fp/Filter.cpp +src/engine/platform/sound/c64_fp/FilterModelConfig6581.cpp +src/engine/platform/sound/c64_fp/FilterModelConfig8580.cpp +src/engine/platform/sound/c64_fp/FilterModelConfig.cpp +src/engine/platform/sound/c64_fp/Integrator6581.cpp +src/engine/platform/sound/c64_fp/Integrator8580.cpp +src/engine/platform/sound/c64_fp/OpAmp.cpp +src/engine/platform/sound/c64_fp/SID.cpp +src/engine/platform/sound/c64_fp/Spline.cpp +src/engine/platform/sound/c64_fp/WaveformCalculator.cpp +src/engine/platform/sound/c64_fp/WaveformGenerator.cpp +src/engine/platform/sound/c64_fp/resample/SincResampler.cpp + +src/engine/platform/sound/tia/AudioChannel.cpp +src/engine/platform/sound/tia/Audio.cpp src/engine/platform/sound/ymfm/ymfm_adpcm.cpp src/engine/platform/sound/ymfm/ymfm_opm.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6931ce8..853020f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ bug fixes, improvements and several other things accepted. the coding style is described here: -- indentation: two spaces +- indentation: two spaces. **strictly** spaces. do NOT use tabs. - modified 1TBS style: - no spaces in function calls - spaces between arguments in function declarations diff --git a/README.md b/README.md index d2308dd7..7efe2c2a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ the biggest multi-system chiptune tracker ever made! [downloads](#downloads) | [discussion/help](#quick-references) | [developer info](#developer-info) | [unofficial packages](#unofficial-packages) | [FAQ](#frequently-asked-questions) -*** +--- ## downloads check out the [Releases](https://github.com/tildearrow/furnace/releases) page. available for Windows, macOS and Linux (AppImage). @@ -74,7 +74,7 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a - loads .dmf modules from all versions (beta 1 to 1.1.3) - saves .dmf modules - both modern and legacy - Furnace doubles as a module downgrader - - loads .dmp instruments and .dmw wavetables as well + - loads/saves .dmp instruments and .dmw wavetables as well - clean-room design (guesswork and ABX tests only, no decompilation involved) - bug/quirk implementation for increased playback accuracy through compatibility flags - VGM export @@ -103,7 +103,7 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a - built-in visualizer in pattern view - open-source under GPLv2 or later. -*** +--- # quick references - **discussion**: see the [Discussions](https://github.com/tildearrow/furnace/discussions) section, or (preferably) the [official Discord server](https://discord.gg/EfrwT2wq7z). @@ -119,7 +119,7 @@ some people have provided packages for Unix/Unix-like distributions. here's a li - **Nix**: [package](https://search.nixos.org/packages?channel=unstable&show=furnace&from=0&size=50&sort=relevance&type=packages&query=furnace) thanks to OPNA2608. - **openSUSE**: [a package](https://software.opensuse.org/package/furnace) is available, courtesy of fpesari. -*** +--- # developer info [![Build furnace](https://github.com/tildearrow/furnace/actions/workflows/build.yml/badge.svg)](https://github.com/tildearrow/furnace/actions/workflows/build.yml) @@ -228,7 +228,7 @@ this will play a compatible file and enable the commands view. **note that these commands only actually work in Linux environments. on other command lines, such as Windows' Command Prompt, or MacOS Terminal, it may not work correctly.** -*** +--- # frequently asked questions > woah! 50 sound chips?! I can't believe it! @@ -274,7 +274,7 @@ the DefleMask format has several limitations. save in Furnace song format instea right click on the channel name. -*** +--- # footnotes copyright (C) 2021-2022 tildearrow and contributors. diff --git a/TODO.md b/TODO.md index cba0b7c6..859ad68c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,7 @@ -# to-do for 0.6pre1.5-0.6pre2 +# to-do for 0.6pre1.5 -- volume commands should work on Game Boy - stereo separation control for AY -- "paste with instrument" \ No newline at end of file +- "paste with instrument" +- FM operator muting +- FM operator swap +- bug fixes diff --git a/android/app/build.gradle b/android/app/build.gradle index cb4b3c59..92a79e4d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,8 +15,8 @@ android { } minSdkVersion 21 targetSdkVersion 26 - versionCode 93 - versionName "0.6pre1" + versionCode 113 + versionName "dev113" externalNativeBuild { cmake { arguments "-DANDROID_APP_PLATFORM=android-21", "-DANDROID_STL=c++_static" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5f7a06ef..b77cf32e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ @@ -83,13 +83,17 @@ - diff --git a/demos/Bullet_Hell.fur b/demos/Bullet_Hell.fur index 48c50f67..dfd2a042 100644 Binary files a/demos/Bullet_Hell.fur and b/demos/Bullet_Hell.fur differ diff --git a/demos/Cafe - 010 Editor 2.0crk.fur b/demos/Cafe - 010 Editor 2.0crk.fur new file mode 100644 index 00000000..339e7238 Binary files /dev/null and b/demos/Cafe - 010 Editor 2.0crk.fur differ diff --git a/demos/ChaosTune.fur b/demos/ChaosTune.fur new file mode 100644 index 00000000..b75fbf9c Binary files /dev/null and b/demos/ChaosTune.fur differ diff --git a/demos/FEDMS.fur b/demos/FEDMS.fur new file mode 100644 index 00000000..4c521dad Binary files /dev/null and b/demos/FEDMS.fur differ diff --git a/demos/GEN Equinox Intro.fur b/demos/GEN Equinox Intro.fur new file mode 100644 index 00000000..04a5bb05 Binary files /dev/null and b/demos/GEN Equinox Intro.fur differ diff --git a/demos/her11_veraedit.fur b/demos/her11_veraedit.fur new file mode 100644 index 00000000..1cfabac1 Binary files /dev/null and b/demos/her11_veraedit.fur differ diff --git a/demos/hope_for_the_dream.fur b/demos/hope_for_the_dream.fur new file mode 100644 index 00000000..77a3fed2 Binary files /dev/null and b/demos/hope_for_the_dream.fur differ diff --git a/demos/iji_tor.fur b/demos/iji_tor.fur new file mode 100644 index 00000000..84a6ac5b Binary files /dev/null and b/demos/iji_tor.fur differ diff --git a/demos/lunacommdemo.fur b/demos/lunacommdemo.fur new file mode 100644 index 00000000..43531b8b Binary files /dev/null and b/demos/lunacommdemo.fur differ diff --git a/extern/Nuked-OPN2 b/extern/Nuked-OPN2 index 64704a44..b0e9de0f 160000 --- a/extern/Nuked-OPN2 +++ b/extern/Nuked-OPN2 @@ -1 +1 @@ -Subproject commit 64704a443f8f6c1906ba26297092ea70fa1d45d7 +Subproject commit b0e9de0f816943ad3820ddfefa0fff276d659250 diff --git a/extern/opm/opm.c b/extern/opm/opm.c index 79f6414c..aafce483 100644 --- a/extern/opm/opm.c +++ b/extern/opm/opm.c @@ -1,5 +1,5 @@ /* Nuked OPM - * Copyright (C) 2020 Nuke.YKT + * Copyright (C) 2022 Nuke.YKT * * This file is part of Nuked OPM. * @@ -21,7 +21,7 @@ * siliconpr0n.org(digshadow, John McMaster): * YM2151 and other FM chip decaps and die shots. * - * version: 0.9.2 beta + * version: 0.9.3 beta */ #include #include @@ -651,7 +651,7 @@ static inline void OPM_EnvelopePhase4(opm_t *chip) chip->eg_instantattack = chip->eg_ratemax[1] && (kon || !chip->eg_ratemax[1]); eg_off = (chip->eg_level[slot] & 0x3f0) == 0x3f0; - slreach = (chip->eg_level[slot] >> 5) == chip->eg_sl[1]; + slreach = (chip->eg_level[slot] >> 4) == (chip->eg_sl[1] << 1); eg_zero = chip->eg_level[slot] == 0; chip->eg_mute = eg_off && chip->eg_state[slot] != eg_num_attack && !kon; diff --git a/instruments/FM/guitar/Banjo (Muted).opni b/instruments/FM/guitar/Banjo (Muted).opni new file mode 100644 index 00000000..1bf6e88f Binary files /dev/null and b/instruments/FM/guitar/Banjo (Muted).opni differ diff --git a/instruments/FM/guitar/Banjo.opni b/instruments/FM/guitar/Banjo.opni new file mode 100644 index 00000000..bfe78989 Binary files /dev/null and b/instruments/FM/guitar/Banjo.opni differ diff --git a/instruments/FM/guitar/Koto.opni b/instruments/FM/guitar/Koto.opni new file mode 100644 index 00000000..9bac79ba Binary files /dev/null and b/instruments/FM/guitar/Koto.opni differ diff --git a/instruments/FM/guitar/Oud.opni b/instruments/FM/guitar/Oud.opni new file mode 100644 index 00000000..029a0c2a Binary files /dev/null and b/instruments/FM/guitar/Oud.opni differ diff --git a/instruments/FM/guitar/Shamisen (Regular Pluck).opni b/instruments/FM/guitar/Shamisen (Regular Pluck).opni new file mode 100644 index 00000000..c25acca1 Binary files /dev/null and b/instruments/FM/guitar/Shamisen (Regular Pluck).opni differ diff --git a/instruments/FM/guitar/Shamisen (Tsugaru Slap).opni b/instruments/FM/guitar/Shamisen (Tsugaru Slap).opni new file mode 100644 index 00000000..ed9c9497 Binary files /dev/null and b/instruments/FM/guitar/Shamisen (Tsugaru Slap).opni differ diff --git a/instruments/FM/guitar/Sitar.opni b/instruments/FM/guitar/Sitar.opni new file mode 100644 index 00000000..3427fe2a Binary files /dev/null and b/instruments/FM/guitar/Sitar.opni differ diff --git a/instruments/FM/guitar/Tamboura (Bass Sitar).opni b/instruments/FM/guitar/Tamboura (Bass Sitar).opni new file mode 100644 index 00000000..e6514b73 Binary files /dev/null and b/instruments/FM/guitar/Tamboura (Bass Sitar).opni differ diff --git a/instruments/FM/keys/brickblock369 Harpsichord.dmp b/instruments/FM/keys/brickblock369 Harpsichord.dmp index a2aba1f2..0f5ea587 100644 Binary files a/instruments/FM/keys/brickblock369 Harpsichord.dmp and b/instruments/FM/keys/brickblock369 Harpsichord.dmp differ diff --git a/instruments/FM/percussion/Kalimba.fui b/instruments/FM/percussion/Kalimba.fui new file mode 100644 index 00000000..128deca3 Binary files /dev/null and b/instruments/FM/percussion/Kalimba.fui differ diff --git a/instruments/other/(SMS) 2-Arp Chord High.dmp b/instruments/other/(SMS) 2-Arp Chord High.dmp index 93401736..845babb2 100644 Binary files a/instruments/other/(SMS) 2-Arp Chord High.dmp and b/instruments/other/(SMS) 2-Arp Chord High.dmp differ diff --git a/instruments/other/(SMS) 2-Arp Major Low.dmp b/instruments/other/(SMS) 2-Arp Major Low.dmp index 532c2456..f65623f0 100644 Binary files a/instruments/other/(SMS) 2-Arp Major Low.dmp and b/instruments/other/(SMS) 2-Arp Major Low.dmp differ diff --git a/instruments/other/(SMS) 2-Arp Minor Low.dmp b/instruments/other/(SMS) 2-Arp Minor Low.dmp index 608e5483..5345dd45 100644 Binary files a/instruments/other/(SMS) 2-Arp Minor Low.dmp and b/instruments/other/(SMS) 2-Arp Minor Low.dmp differ diff --git a/instruments/other/(SMS) 3-Arp High.dmp b/instruments/other/(SMS) 3-Arp High.dmp index a5ebf975..d0fab8e5 100644 Binary files a/instruments/other/(SMS) 3-Arp High.dmp and b/instruments/other/(SMS) 3-Arp High.dmp differ diff --git a/instruments/other/(SMS) 3-Arp Major.dmp b/instruments/other/(SMS) 3-Arp Major.dmp index 3d284983..d9f9e3a7 100644 Binary files a/instruments/other/(SMS) 3-Arp Major.dmp and b/instruments/other/(SMS) 3-Arp Major.dmp differ diff --git a/instruments/other/(SMS) 3-Arp Minor.dmp b/instruments/other/(SMS) 3-Arp Minor.dmp index a28275fb..f36e80e9 100644 Binary files a/instruments/other/(SMS) 3-Arp Minor.dmp and b/instruments/other/(SMS) 3-Arp Minor.dmp differ diff --git a/instruments/other/(SMS) Arp Snare.dmp b/instruments/other/(SMS) Arp Snare.dmp index fa5734d4..71359542 100644 Binary files a/instruments/other/(SMS) Arp Snare.dmp and b/instruments/other/(SMS) Arp Snare.dmp differ diff --git a/instruments/other/(SMS) Attack.dmp b/instruments/other/(SMS) Attack.dmp index 0be93062..ac5d24ff 100644 Binary files a/instruments/other/(SMS) Attack.dmp and b/instruments/other/(SMS) Attack.dmp differ diff --git a/instruments/other/(SMS) Buzz Noise.dmp b/instruments/other/(SMS) Buzz Noise.dmp index 8b7dfd97..d1bc3921 100644 Binary files a/instruments/other/(SMS) Buzz Noise.dmp and b/instruments/other/(SMS) Buzz Noise.dmp differ diff --git a/instruments/other/(SMS) Crash.dmp b/instruments/other/(SMS) Crash.dmp index f9855bde..6f05ff80 100644 Binary files a/instruments/other/(SMS) Crash.dmp and b/instruments/other/(SMS) Crash.dmp differ diff --git a/instruments/other/(SMS) Decay Noise.dmp b/instruments/other/(SMS) Decay Noise.dmp index 9a829f44..65e45cf8 100644 Binary files a/instruments/other/(SMS) Decay Noise.dmp and b/instruments/other/(SMS) Decay Noise.dmp differ diff --git a/instruments/other/(SMS) Decay.dmp b/instruments/other/(SMS) Decay.dmp index 7f42d6ec..8c2688d1 100644 Binary files a/instruments/other/(SMS) Decay.dmp and b/instruments/other/(SMS) Decay.dmp differ diff --git a/instruments/other/(SMS) Down Slider.dmp b/instruments/other/(SMS) Down Slider.dmp index 5d0b6b27..cc13d5fa 100644 Binary files a/instruments/other/(SMS) Down Slider.dmp and b/instruments/other/(SMS) Down Slider.dmp differ diff --git a/instruments/other/(SMS) Hi-Hat & Note.dmp b/instruments/other/(SMS) Hi-Hat & Note.dmp index 3f92dc68..cff02382 100644 Binary files a/instruments/other/(SMS) Hi-Hat & Note.dmp and b/instruments/other/(SMS) Hi-Hat & Note.dmp differ diff --git a/instruments/other/(SMS) Hi-Hat Closed.dmp b/instruments/other/(SMS) Hi-Hat Closed.dmp index 24f92bf2..5e4e60a8 100644 Binary files a/instruments/other/(SMS) Hi-Hat Closed.dmp and b/instruments/other/(SMS) Hi-Hat Closed.dmp differ diff --git a/instruments/other/(SMS) Hi-Hat Open.dmp b/instruments/other/(SMS) Hi-Hat Open.dmp index 6726e12d..ae6e88e7 100644 Binary files a/instruments/other/(SMS) Hi-Hat Open.dmp and b/instruments/other/(SMS) Hi-Hat Open.dmp differ diff --git a/instruments/other/(SMS) Kick Noise.dmp b/instruments/other/(SMS) Kick Noise.dmp index 8f2d3cbf..b76fb886 100644 Binary files a/instruments/other/(SMS) Kick Noise.dmp and b/instruments/other/(SMS) Kick Noise.dmp differ diff --git a/instruments/other/(SMS) Multi Slider.dmp b/instruments/other/(SMS) Multi Slider.dmp index abd3094a..4f2bd3fc 100644 Binary files a/instruments/other/(SMS) Multi Slider.dmp and b/instruments/other/(SMS) Multi Slider.dmp differ diff --git a/instruments/other/(SMS) Obvious Crash.dmp b/instruments/other/(SMS) Obvious Crash.dmp index 54c7387e..0f54a85e 100644 Binary files a/instruments/other/(SMS) Obvious Crash.dmp and b/instruments/other/(SMS) Obvious Crash.dmp differ diff --git a/instruments/other/(SMS) Record Scratch Down.dmp b/instruments/other/(SMS) Record Scratch Down.dmp index 8e28c418..b3f4b07f 100644 Binary files a/instruments/other/(SMS) Record Scratch Down.dmp and b/instruments/other/(SMS) Record Scratch Down.dmp differ diff --git a/instruments/other/(SMS) Record Scratch Up.dmp b/instruments/other/(SMS) Record Scratch Up.dmp index b17b654e..a710de04 100644 Binary files a/instruments/other/(SMS) Record Scratch Up.dmp and b/instruments/other/(SMS) Record Scratch Up.dmp differ diff --git a/instruments/other/(SMS) Retrig.dmp b/instruments/other/(SMS) Retrig.dmp index 65c77610..b2822b8c 100644 Binary files a/instruments/other/(SMS) Retrig.dmp and b/instruments/other/(SMS) Retrig.dmp differ diff --git a/instruments/other/(SMS) Ride.dmp b/instruments/other/(SMS) Ride.dmp index 01070ff7..45c4b5fe 100644 Binary files a/instruments/other/(SMS) Ride.dmp and b/instruments/other/(SMS) Ride.dmp differ diff --git a/instruments/other/(SMS) Snare.dmp b/instruments/other/(SMS) Snare.dmp index c0e32b21..d624b92a 100644 Binary files a/instruments/other/(SMS) Snare.dmp and b/instruments/other/(SMS) Snare.dmp differ diff --git a/instruments/other/(SMS) Splash.dmp b/instruments/other/(SMS) Splash.dmp index e758a67b..f3296072 100644 Binary files a/instruments/other/(SMS) Splash.dmp and b/instruments/other/(SMS) Splash.dmp differ diff --git a/instruments/other/(SMS) Thump & Note.dmp b/instruments/other/(SMS) Thump & Note.dmp index 41fb6d51..3dccb56c 100644 Binary files a/instruments/other/(SMS) Thump & Note.dmp and b/instruments/other/(SMS) Thump & Note.dmp differ diff --git a/instruments/other/(SMS) Tim Follin 6-Arp Fast Major.dmp b/instruments/other/(SMS) Tim Follin 6-Arp Fast Major.dmp index e306203c..6440474c 100644 Binary files a/instruments/other/(SMS) Tim Follin 6-Arp Fast Major.dmp and b/instruments/other/(SMS) Tim Follin 6-Arp Fast Major.dmp differ diff --git a/instruments/other/(SMS) Tim Follin 6-Arp Fast Minor.dmp b/instruments/other/(SMS) Tim Follin 6-Arp Fast Minor.dmp index a845fc1c..105421b6 100644 Binary files a/instruments/other/(SMS) Tim Follin 6-Arp Fast Minor.dmp and b/instruments/other/(SMS) Tim Follin 6-Arp Fast Minor.dmp differ diff --git a/instruments/other/(SMS) Tim Follin 6-Arp Slow Major.dmp b/instruments/other/(SMS) Tim Follin 6-Arp Slow Major.dmp index 9536150c..11eeafb4 100644 Binary files a/instruments/other/(SMS) Tim Follin 6-Arp Slow Major.dmp and b/instruments/other/(SMS) Tim Follin 6-Arp Slow Major.dmp differ diff --git a/instruments/other/(SMS) Tim Follin 6-Arp Slow Minor.dmp b/instruments/other/(SMS) Tim Follin 6-Arp Slow Minor.dmp index 166a1cb3..3cbc3f38 100644 Binary files a/instruments/other/(SMS) Tim Follin 6-Arp Slow Minor.dmp and b/instruments/other/(SMS) Tim Follin 6-Arp Slow Minor.dmp differ diff --git a/instruments/other/(SMS) Tim Follin Lead.dmp b/instruments/other/(SMS) Tim Follin Lead.dmp index 08a3a081..69ab7d40 100644 Binary files a/instruments/other/(SMS) Tim Follin Lead.dmp and b/instruments/other/(SMS) Tim Follin Lead.dmp differ diff --git a/instruments/other/(SMS) Tom A.dmp b/instruments/other/(SMS) Tom A.dmp index 8976d0cf..813de521 100644 Binary files a/instruments/other/(SMS) Tom A.dmp and b/instruments/other/(SMS) Tom A.dmp differ diff --git a/instruments/other/(SMS) Tom B.dmp b/instruments/other/(SMS) Tom B.dmp index 664021f6..e06cccb8 100644 Binary files a/instruments/other/(SMS) Tom B.dmp and b/instruments/other/(SMS) Tom B.dmp differ diff --git a/instruments/other/(SMS) Up Slider.dmp b/instruments/other/(SMS) Up Slider.dmp index 765f156e..b57cfffe 100644 Binary files a/instruments/other/(SMS) Up Slider.dmp and b/instruments/other/(SMS) Up Slider.dmp differ diff --git a/instruments/other/(SMS) Variable.dmp b/instruments/other/(SMS) Variable.dmp index 7ed3209f..16a5c0f7 100644 Binary files a/instruments/other/(SMS) Variable.dmp and b/instruments/other/(SMS) Variable.dmp differ diff --git a/instruments/other/(SMS) Whistle.dmp b/instruments/other/(SMS) Whistle.dmp index 0fd9127c..1ed679bb 100644 Binary files a/instruments/other/(SMS) Whistle.dmp and b/instruments/other/(SMS) Whistle.dmp differ diff --git a/instruments/other/2A03 Noise Hi-Hat Closed.fui b/instruments/other/2A03 Noise Hi-Hat Closed.fui new file mode 100644 index 00000000..a24dff83 Binary files /dev/null and b/instruments/other/2A03 Noise Hi-Hat Closed.fui differ diff --git a/instruments/other/2A03 Noise Hi-Hat Open.fui b/instruments/other/2A03 Noise Hi-Hat Open.fui new file mode 100644 index 00000000..26889b0a Binary files /dev/null and b/instruments/other/2A03 Noise Hi-Hat Open.fui differ diff --git a/instruments/other/2A03 Noise Kick.fui b/instruments/other/2A03 Noise Kick.fui new file mode 100644 index 00000000..7cf815e1 Binary files /dev/null and b/instruments/other/2A03 Noise Kick.fui differ diff --git a/instruments/other/2A03 Noise Snare.fui b/instruments/other/2A03 Noise Snare.fui new file mode 100644 index 00000000..0bcd5dac Binary files /dev/null and b/instruments/other/2A03 Noise Snare.fui differ diff --git a/instruments/other/2A03 Triangle Kick+Bass.fui b/instruments/other/2A03 Triangle Kick+Bass.fui new file mode 100644 index 00000000..9beaea86 Binary files /dev/null and b/instruments/other/2A03 Triangle Kick+Bass.fui differ diff --git a/instruments/other/2A03 Triangle Kick.fui b/instruments/other/2A03 Triangle Kick.fui new file mode 100644 index 00000000..f2263010 Binary files /dev/null and b/instruments/other/2A03 Triangle Kick.fui differ diff --git a/instruments/other/2A03 Triangle Snare+Bass.fui b/instruments/other/2A03 Triangle Snare+Bass.fui new file mode 100644 index 00000000..e91831c8 Binary files /dev/null and b/instruments/other/2A03 Triangle Snare+Bass.fui differ diff --git a/instruments/other/2A03 Triangle Snare.fui b/instruments/other/2A03 Triangle Snare.fui new file mode 100644 index 00000000..7ed4b8ac Binary files /dev/null and b/instruments/other/2A03 Triangle Snare.fui differ diff --git a/instruments/other/AY kick.fui b/instruments/other/AY kick.fui new file mode 100644 index 00000000..111f1da7 Binary files /dev/null and b/instruments/other/AY kick.fui differ diff --git a/instruments/other/AY snare.fui b/instruments/other/AY snare.fui new file mode 100644 index 00000000..b8c03a2d Binary files /dev/null and b/instruments/other/AY snare.fui differ diff --git a/papers/doc/6-sample/README.md b/papers/doc/6-sample/README.md index 59613da2..4fbb6c91 100644 --- a/papers/doc/6-sample/README.md +++ b/papers/doc/6-sample/README.md @@ -64,7 +64,3 @@ In there, you can modify certain data pertaining to your sample, such as the: - and many more. The changes you make will be applied as soon as you've committed them to your sample, but they can be undoed and redoed, just like text. - -# tips - -if you have a sample you wanna use that is about 44100 or anything over 32000Hz, downsample the sample to 32000Hz so that the pitch of the sample in Furnace stays like the original audio file, diff --git a/papers/doc/7-systems/soundunit.md b/papers/doc/7-systems/soundunit.md index 89e34655..34cc16b4 100644 --- a/papers/doc/7-systems/soundunit.md +++ b/papers/doc/7-systems/soundunit.md @@ -1,5 +1,5 @@ # tildearrow Sound Unit -This is a fantasy sound chip, used in the specs2 fantasy computer designed by tildearrow. It includes native support for sample playback, but with only 8KB of sample data. Since 0.6pre1, this sound chip is no longer hidden by default and can be accessed through the module creation screen and can be added or removed. +This is a fantasy sound chip, used in the specs2 fantasy computer designed by tildearrow. It includes native support for sample playback, but with only 8KB or 64KB of sample data, depending on the configuration used. Since 0.6pre1, this sound chip is no longer hidden by default and can be accessed through the module creation screen and can be added or removed. # effects @@ -12,7 +12,7 @@ This is a fantasy sound chip, used in the specs2 fantasy computer designed by ti - 5: periodic noise - 6: XOR sine - 7: XOR triangle -- `12xx`: set waveform (0 to 7F) +- `12xx`: set pulse width (0 to 7F) - `13xx`: set resonance of filter (0 to FF) - despite what the internal effects list says (0 to F), you can use a resonance value from 0 to FF (255) - `14xx`: set filter mode and ringmod diff --git a/papers/format.md b/papers/format.md index def8cba1..da06488f 100644 --- a/papers/format.md +++ b/papers/format.md @@ -32,6 +32,8 @@ these fields are 0 in format versions prior to 100 (0.6pre1). the format versions are: +- 114: Furnace dev114 +- 113: Furnace dev113 - 112: Furnace dev112 - 111: Furnace dev111 - 110: Furnace dev110 @@ -254,6 +256,7 @@ size | description | - 0xc3: OPN CSM - 10 channels | - 0xc4: PC-98 CSM - 20 channels | - 0xc5: YM2610B CSM - 20 channels + | - 0xc6: MSM5232 - 8 channels | - 0xde: YM2610B extended - 19 channels | - 0xe0: QSound - 19 channels | - 0xfd: Dummy System - 8 channels @@ -338,7 +341,8 @@ size | description 1 | broken initial position of porta after arp (>=101) or reserved 1 | SN periods under 8 are treated as 1 (>=108) or reserved 1 | cut/delay effect policy (>=110) or reserved - 5 | reserved + 1 | 0B/0D effect treatment (>=113) or reserved + 4 | reserved --- | **virtual tempo data** 2 | virtual tempo numerator of first song (>=96) or reserved 2 | virtual tempo denominator of first song (>=96) or reserved @@ -494,7 +498,8 @@ size | description 1 | vib 1 | ws 1 | ksr - 12 | reserved + 1 | operator enabled (>=114) or reserved + 11 | reserved --- | **Game Boy instrument data** 1 | volume 1 | direction diff --git a/src/engine/config.cpp b/src/engine/config.cpp index f404c0a4..38cb2b04 100644 --- a/src/engine/config.cpp +++ b/src/engine/config.cpp @@ -36,12 +36,20 @@ #define CONFIG_FILE "/furnace.cfg" #endif +#ifdef IS_MOBILE +#ifdef HAVE_SDL2 +#include +#else +#error "Furnace mobile requires SDL2!" +#endif +#endif + void DivEngine::initConfDir() { #ifdef _WIN32 // maybe move this function in here instead? configPath=getWinConfigPath(); -#elif defined (IS_MOBILE) - configPath=SDL_GetPrefPath(); +#elif defined(IS_MOBILE) + configPath=SDL_GetPrefPath("tildearrow","furnace"); #else #ifdef __HAIKU__ char userSettingsDir[PATH_MAX]; @@ -231,6 +239,10 @@ void DivEngine::setConf(String key, double value) { conf[key]=fmt::sprintf("%f",value); } +void DivEngine::setConf(String key, const char* value) { + conf[key]=String(value); +} + void DivEngine::setConf(String key, String value) { conf[key]=value; } diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 8d8db5c5..158bce4e 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -218,10 +218,12 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do break; case DIV_SYSTEM_C64_6581: dispatch=new DivPlatformC64; + ((DivPlatformC64*)dispatch)->setFP(eng->getConfInt("c64Core",1)==1); ((DivPlatformC64*)dispatch)->setChipModel(true); break; case DIV_SYSTEM_C64_8580: dispatch=new DivPlatformC64; + ((DivPlatformC64*)dispatch)->setFP(eng->getConfInt("c64Core",1)==1); ((DivPlatformC64*)dispatch)->setChipModel(false); break; case DIV_SYSTEM_YM2151: diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 1230cb59..bb03eda1 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -147,37 +147,68 @@ void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) { int nextOrder=-1; int nextRow=0; int effectVal=0; + int lastSuspectedLoopEnd=-1; DivPattern* pat[DIV_MAX_CHANS]; + unsigned char wsWalked[8192]; + memset(wsWalked,0,8192); for (int i=0; iordersLen; i++) { for (int j=0; jord[j][i],false); } + if (i>lastSuspectedLoopEnd) { + lastSuspectedLoopEnd=i; + } for (int j=nextRow; jpatLen; j++) { nextRow=0; + bool changingOrder=false; + bool jumpingOrder=false; + if (wsWalked[((i<<5)+(j>>3))&8191]&(1<<(j&7))) { + loopOrder=i; + loopRow=j; + loopEnd=lastSuspectedLoopEnd; + return; + } for (int k=0; kdata[j][5+(l<<1)]; if (effectVal<0) effectVal=0; if (pat[k]->data[j][4+(l<<1)]==0x0d) { - if (nextOrder==-1 && (iordersLen-1 || !song.ignoreJumpAtEnd)) { - nextOrder=i+1; - nextRow=effectVal; + if (song.jumpTreatment==2) { + if ((iordersLen-1 || !song.ignoreJumpAtEnd)) { + nextOrder=i+1; + nextRow=effectVal; + jumpingOrder=true; + } + } else if (song.jumpTreatment==1) { + if (nextOrder==-1 && (iordersLen-1 || !song.ignoreJumpAtEnd)) { + nextOrder=i+1; + nextRow=effectVal; + jumpingOrder=true; + } + } else { + if ((iordersLen-1 || !song.ignoreJumpAtEnd)) { + if (!changingOrder) { + nextOrder=i+1; + } + jumpingOrder=true; + nextRow=effectVal; + } } } else if (pat[k]->data[j][4+(l<<1)]==0x0b) { - if (nextOrder==-1) { + if (nextOrder==-1 || song.jumpTreatment==0) { nextOrder=effectVal; - nextRow=0; + if (song.jumpTreatment==1 || song.jumpTreatment==2 || !jumpingOrder) { + nextRow=0; + } + changingOrder=true; } } } } + + wsWalked[((i<<5)+(j>>3))&8191]|=1<<(j&7); + if (nextOrder!=-1) { - if (nextOrder<=i) { - loopOrder=nextOrder; - loopRow=nextRow; - loopEnd=i; - return; - } i=nextOrder-1; nextOrder=-1; break; @@ -1140,7 +1171,7 @@ void DivEngine::swapChannels(int src, int dest) { String prevChanName=curSubSong->chanName[src]; String prevChanShortName=curSubSong->chanShortName[src]; bool prevChanShow=curSubSong->chanShow[src]; - bool prevChanCollapse=curSubSong->chanCollapse[src]; + unsigned char prevChanCollapse=curSubSong->chanCollapse[src]; curSubSong->chanName[src]=curSubSong->chanName[dest]; curSubSong->chanShortName[src]=curSubSong->chanShortName[dest]; @@ -1445,25 +1476,44 @@ bool DivEngine::swapSystem(int src, int dest, bool preserveOrder) { } } + // swap channels logV("swap list:"); for (int i=0; i %d",unswappedChannels[i],swappedChannels[i]); } - // swap channels - bool allComplete=false; - while (!allComplete) { - logD("doing swap..."); - allComplete=true; - for (int i=0; i %d -> %d",unswappedChannels[i],unswappedChannels[swappedChannels[i]]); - unswappedChannels[i]^=unswappedChannels[swappedChannels[i]]; - unswappedChannels[swappedChannels[i]]^=unswappedChannels[i]; - unswappedChannels[i]^=unswappedChannels[swappedChannels[i]]; + for (size_t i=0; iorders; + DivPattern* prevPat[DIV_MAX_CHANS][256]; + unsigned char prevEffectCols[DIV_MAX_CHANS]; + String prevChanName[DIV_MAX_CHANS]; + String prevChanShortName[DIV_MAX_CHANS]; + bool prevChanShow[DIV_MAX_CHANS]; + unsigned char prevChanCollapse[DIV_MAX_CHANS]; + + for (int j=0; jpat[j].data[k]; } + prevEffectCols[j]=song.subsong[i]->pat[j].effectCols; + + prevChanName[j]=song.subsong[i]->chanName[j]; + prevChanShortName[j]=song.subsong[i]->chanShortName[j]; + prevChanShow[j]=song.subsong[i]->chanShow[j]; + prevChanCollapse[j]=song.subsong[i]->chanCollapse[j]; + } + + for (int j=0; jorders.ord[j][k]=prevOrders.ord[swappedChannels[j]][k]; + song.subsong[i]->pat[j].data[k]=prevPat[swappedChannels[j]][k]; + } + + song.subsong[i]->pat[j].effectCols=prevEffectCols[swappedChannels[j]]; + song.subsong[i]->chanName[j]=prevChanName[swappedChannels[j]]; + song.subsong[i]->chanShortName[j]=prevChanShortName[swappedChannels[j]]; + song.subsong[i]->chanShow[j]=prevChanShow[swappedChannels[j]]; + song.subsong[i]->chanCollapse[j]=prevChanCollapse[swappedChannels[j]]; } } } @@ -1639,6 +1689,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { speedAB=false; playing=true; skipping=true; + memset(walked,0,8192); for (int i=0; isetSkipRegisterWrites(true); while (playing && curOrder1)) { if (nextTick(preserveDrift)) { skipping=false; return; @@ -3402,9 +3453,8 @@ void DivEngine::setConsoleMode(bool enable) { } bool DivEngine::switchMaster() { - deinitAudioBackend(); - quitDispatch(); - initDispatch(); + logI("switching output..."); + deinitAudioBackend(true); if (initAudioBackend()) { for (int i=0; iquit(); if (output->midiIn) { if (output->midiIn->isDeviceOpen()) { @@ -3671,7 +3723,9 @@ bool DivEngine::deinitAudioBackend() { output->quitMidi(); delete output; output=NULL; - //audioEngine=DIV_AUDIO_NULL; + if (dueToSwitchMaster) { + audioEngine=DIV_AUDIO_NULL; + } } return true; } diff --git a/src/engine/engine.h b/src/engine/engine.h index 62c782b0..8eba281c 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -46,9 +46,8 @@ #define BUSY_BEGIN_SOFT softLocked=true; isBusy.lock(); #define BUSY_END isBusy.unlock(); softLocked=false; -#define DIV_VERSION "dev112" -#define DIV_ENGINE_VERSION 112 - +#define DIV_VERSION "dev114" +#define DIV_ENGINE_VERSION 114 // for imports #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 @@ -358,6 +357,8 @@ class DivEngine { double exportFadeOut; std::map conf; std::deque pendingNotes; + // bitfield + unsigned char walked[8192]; bool isMuted[DIV_MAX_CHANS]; std::mutex isBusy, saveLock; String configPath; @@ -451,7 +452,7 @@ class DivEngine { int loadSampleROM(String path, ssize_t expectedSize, unsigned char*& ret); bool initAudioBackend(); - bool deinitAudioBackend(); + bool deinitAudioBackend(bool dueToSwitchMaster=false); void registerSystems(); void initSongWithDesc(const int* description); @@ -545,6 +546,7 @@ class DivEngine { void setConf(String key, int value); void setConf(String key, float value); void setConf(String key, double value); + void setConf(String key, const char* value); void setConf(String key, String value); // calculate base frequency/period @@ -1074,6 +1076,7 @@ class DivEngine { memset(reversePitchTable,0,4096*sizeof(int)); memset(pitchTable,0,4096*sizeof(int)); memset(sysDefs,0,256*sizeof(void*)); + memset(walked,0,8192); for (int i=0; i<256; i++) { sysFileMapFur[i]=DIV_SYSTEM_NULL; diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 0e0ceb8b..10a4bcce 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -179,6 +179,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.brokenPortaArp=false; ds.snNoLowPeriods=true; ds.delayBehavior=0; + ds.jumpTreatment=2; // 1.1 compat flags if (ds.version>24) { @@ -1081,6 +1082,9 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (ds.version<110) { ds.delayBehavior=1; } + if (ds.version<113) { + ds.jumpTreatment=1; + } ds.isDMF=false; reader.readS(); // reserved @@ -1503,7 +1507,12 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { } else { reader.readC(); } - for (int i=0; i<5; i++) { + if (ds.version>=113) { + ds.jumpTreatment=reader.readC(); + } else { + reader.readC(); + } + for (int i=0; i<4; i++) { reader.readC(); } } @@ -3747,7 +3756,8 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { w->writeC(song.brokenPortaArp); w->writeC(song.snNoLowPeriods); w->writeC(song.delayBehavior); - for (int i=0; i<5; i++) { + w->writeC(song.jumpTreatment); + for (int i=0; i<4; i++) { w->writeC(0); } @@ -4342,14 +4352,25 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { } } + bool relWarning=false; + for (int i=0; iwriteC(curPat[i].effectCols); for (int j=0; jordersLen; j++) { DivPattern* pat=curPat[i].getPattern(curOrders->ord[i][j],false); for (int k=0; kpatLen; k++) { - w->writeS(pat->data[k][0]); // note - w->writeS(pat->data[k][1]); // octave + if ((pat->data[k][0]==101 || pat->data[k][0]==102) && pat->data[k][1]==0) { + w->writeS(100); + w->writeS(0); + if (!relWarning) { + relWarning=true; + addWarning("note/macro release will be converted to note off!"); + } + } else { + w->writeS(pat->data[k][0]); // note + w->writeS(pat->data[k][1]); // octave + } w->writeS(pat->data[k][3]); // volume #ifdef TA_BIG_ENDIAN for (int l=0; l& ret, St ins->type=DIV_INS_FM; logD("instrument type is Arcade"); break; + case 9: // Neo Geo + ins->type=DIV_INS_FM; + logD("instrument type is Neo Geo"); + break; default: logD("instrument type is unknown"); - lastError="unknown instrument type!"; + lastError=fmt::sprintf("unknown instrument type %d!",sys); delete ins; return; break; @@ -171,11 +175,21 @@ void DivEngine::loadDMP(SafeReader& reader, std::vector& ret, St mode=reader.readC(); logD("instrument mode is %d",mode); if (mode==0) { - if (version<11) { - ins->type=DIV_INS_STD; + if (ins->type==DIV_INS_FM) { + if (sys==9) { + ins->type=DIV_INS_AY; + } else { + ins->type=DIV_INS_STD; + } } } else { - ins->type=DIV_INS_FM; + if (sys==3 || sys==6) { + ins->type=DIV_INS_OPLL; + } else if (sys==1) { + ins->type=DIV_INS_OPL; + } else { + ins->type=DIV_INS_FM; + } } } else { ins->type=DIV_INS_FM; @@ -232,12 +246,23 @@ void DivEngine::loadDMP(SafeReader& reader, std::vector& ret, St ins->fm.op[j].dvb=reader.readC(); ins->fm.op[j].dam=reader.readC(); } else { - ins->fm.op[j].rs=reader.readC(); - ins->fm.op[j].dt=reader.readC(); - ins->fm.op[j].dt2=ins->fm.op[j].dt>>4; - ins->fm.op[j].dt&=15; - ins->fm.op[j].d2r=reader.readC(); - ins->fm.op[j].ssgEnv=reader.readC(); + if (sys==3 || sys==6) { // OPLL/VRC7 + ins->fm.op[j].ksr=reader.readC()?1:0; + ins->fm.op[j].vib=reader.readC(); + if (j==0) { + ins->fm.opllPreset=ins->fm.op[j].vib>>4; + } + ins->fm.op[j].vib=ins->fm.op[j].vib?1:0; + ins->fm.op[j].ksl=reader.readC()?1:0; + ins->fm.op[j].ssgEnv=reader.readC(); + } else { + ins->fm.op[j].rs=reader.readC(); + ins->fm.op[j].dt=reader.readC(); + ins->fm.op[j].dt2=ins->fm.op[j].dt>>4; + ins->fm.op[j].dt&=15; + ins->fm.op[j].d2r=reader.readC(); + ins->fm.op[j].ssgEnv=reader.readC(); + } } } } else { // STD @@ -247,6 +272,9 @@ void DivEngine::loadDMP(SafeReader& reader, std::vector& ret, St if (version>5) { for (int i=0; istd.volMacro.len; i++) { ins->std.volMacro.val[i]=reader.readI(); + if (ins->std.volMacro.val[i]>15 && sys==6) { // FDS + ins->type=DIV_INS_FDS; + } } } else { for (int i=0; istd.volMacro.len; i++) { @@ -805,7 +833,7 @@ void DivEngine::loadOPNI(SafeReader& reader, std::vector& ret, S op.mult = dtMul & 0xF; op.dt = ((dtMul >> 4) & 0x7); - op.tl = totalLevel & 0x3F; + op.tl = totalLevel & 0x7F; op.rs = ((arRateScale >> 6) & 0x3); op.ar = arRateScale & 0x1F; op.dr = drAmpEnable & 0x1F; @@ -1643,7 +1671,7 @@ void DivEngine::loadWOPN(SafeReader& reader, std::vector& ret, S total += (op.mult = dtMul & 0xF); total += (op.dt = ((dtMul >> 4) & 0x7)); - total += (op.tl = totalLevel & 0x3F); + total += (op.tl = totalLevel & 0x7F); total += (op.rs = ((arRateScale >> 6) & 0x3)); total += (op.ar = arRateScale & 0x1F); total += (op.dr = drAmpEnable & 0x1F); diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index 19cb1ba5..22cdddf4 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -71,8 +71,10 @@ void DivInstrument::putInsData(SafeWriter* w) { w->writeC(op.ws); w->writeC(op.ksr); + w->writeC(op.enable); + // reserved - for (int k=0; k<12; k++) { + for (int k=0; k<11; k++) { w->writeC(0); } } @@ -716,8 +718,14 @@ DivDataErrors DivInstrument::readInsData(SafeReader& reader, short version) { op.ws=reader.readC(); op.ksr=reader.readC(); + if (version>=114) { + op.enable=reader.readC(); + } else { + reader.readC(); + } + // reserved - for (int k=0; k<12; k++) reader.readC(); + for (int k=0; k<11; k++) reader.readC(); } // GB diff --git a/src/engine/macroInt.h b/src/engine/macroInt.h index ad761ba5..5208dc54 100644 --- a/src/engine/macroInt.h +++ b/src/engine/macroInt.h @@ -50,7 +50,7 @@ struct DivMacroStruct { finished(false), will(false), linger(false), - began(false), + began(true), mode(0) {} }; diff --git a/src/engine/platform/arcade.cpp b/src/engine/platform/arcade.cpp index cca54d28..6e925c9a 100644 --- a/src/engine/platform/arcade.cpp +++ b/src/engine/platform/arcade.cpp @@ -67,7 +67,7 @@ void DivPlatformArcade::acquire_nuked(short* bufL, short* bufR, size_t start, si w.addrOrVal=true; } } - + OPM_Clock(&fm,NULL,NULL,NULL,NULL); OPM_Clock(&fm,NULL,NULL,NULL,NULL); OPM_Clock(&fm,NULL,NULL,NULL,NULL); @@ -77,13 +77,13 @@ void DivPlatformArcade::acquire_nuked(short* bufL, short* bufR, size_t start, si for (int i=0; i<8; i++) { oscBuf[i]->data[oscBuf[i]->needle++]=fm.ch_out[i]; } - + if (o[0]<-32768) o[0]=-32768; if (o[0]>32767) o[0]=32767; if (o[1]<-32768) o[1]=-32768; if (o[1]>32767) o[1]=32767; - + bufL[h]=o[0]; bufR[h]=o[1]; } @@ -106,7 +106,7 @@ void DivPlatformArcade::acquire_ymfm(short* bufL, short* bufR, size_t start, siz delay=1; } } - + fm_ymfm->generate(&out_ymfm); for (int i=0; i<8; i++) { @@ -120,7 +120,7 @@ void DivPlatformArcade::acquire_ymfm(short* bufL, short* bufR, size_t start, siz os[1]=out_ymfm.data[1]; if (os[1]<-32768) os[1]=-32768; if (os[1]>32767) os[1]=32767; - + bufL[h]=os[0]; bufR[h]=os[1]; } @@ -255,6 +255,10 @@ void DivPlatformArcade::tick(bool sysTick) { chan[i].state.ams=chan[i].std.ams.val; rWrite(chanOffs[i]+ADDR_FMS_AMS,((chan[i].state.fms&7)<<4)|(chan[i].state.ams&3)); } + if (chan[i].std.ex4.had && chan[i].active) { + chan[i].opMask=chan[i].std.ex4.val&15; + chan[i].opMaskChanged=true; + } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -347,8 +351,9 @@ void DivPlatformArcade::tick(bool sysTick) { immWrite(i+0x30,chan[i].freq<<2); chan[i].freqChanged=false; } - if (chan[i].keyOn) { - immWrite(0x08,0x78|i); + if (chan[i].keyOn || chan[i].opMaskChanged) { + immWrite(0x08,(chan[i].opMask<<3)|i); + chan[i].opMaskChanged=false; chan[i].keyOn=false; } } @@ -370,6 +375,11 @@ int DivPlatformArcade::dispatch(DivCommand c) { if (chan[c.chan].insChanged) { chan[c.chan].state=ins->fm; + chan[c.chan].opMask= + (chan[c.chan].state.op[0].enable?1:0)| + (chan[c.chan].state.op[2].enable?2:0)| + (chan[c.chan].state.op[1].enable?4:0)| + (chan[c.chan].state.op[3].enable?8:0); } chan[c.chan].macroInit(ins); @@ -502,6 +512,12 @@ int DivPlatformArcade::dispatch(DivCommand c) { break; } case DIV_CMD_FM_LFO: { + if(c.value==0) { + rWrite(0x01,0x02); + } + else { + rWrite(0x01,0x00); + } rWrite(0x18,c.value); break; } @@ -825,6 +841,8 @@ void DivPlatformArcade::reset() { pmDepth=0x7f; //rWrite(0x18,0x10); + immWrite(0x01,0x02); // LFO Off + immWrite(0x18,0x00); // LFO Freq Off immWrite(0x19,amDepth); immWrite(0x19,0x80|pmDepth); //rWrite(0x1b,0x00); diff --git a/src/engine/platform/arcade.h b/src/engine/platform/arcade.h index 93aa490d..2a9c2b40 100644 --- a/src/engine/platform/arcade.h +++ b/src/engine/platform/arcade.h @@ -43,9 +43,9 @@ class DivPlatformArcade: public DivPlatformOPM { int freq, baseFreq, pitch, pitch2, note; int ins; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, portaPause, furnacePCM, hardReset; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, portaPause, furnacePCM, hardReset, opMaskChanged; int vol, outVol; - unsigned char chVolL, chVolR; + unsigned char chVolL, chVolR, opMask; void macroInit(DivInstrument* which) { std.init(which); pitch2=0; @@ -68,10 +68,12 @@ class DivPlatformArcade: public DivPlatformOPM { portaPause(false), furnacePCM(false), hardReset(false), + opMaskChanged(false), vol(0), outVol(0), chVolL(127), - chVolR(127) {} + chVolR(127), + opMask(15) {} }; Channel chan[8]; DivDispatchOscBuffer* oscBuf[8]; diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index f45c51ea..23f20c2a 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -69,6 +69,14 @@ const char* regCheatSheetAY8914[]={ NULL }; +// taken from ay8910.cpp +const int sunsoftVolTable[32]={ + 103350, 73770, 52657, 37586, 32125, 27458, 24269, 21451, + 18447, 15864, 14009, 12371, 10506, 8922, 7787, 6796, + 5689, 4763, 4095, 3521, 2909, 2403, 2043, 1737, + 1397, 1123, 925, 762, 578, 438, 332, 251 +}; + const char** DivPlatformAY8910::getRegisterSheet() { return intellivision?regCheatSheetAY8914:regCheatSheetAY; } @@ -93,27 +101,33 @@ void DivPlatformAY8910::acquire(short* bufL, short* bufR, size_t start, size_t l regPool[w.addr&0x0f]=w.val; writes.pop(); } - ay->sound_stream_update(ayBuf,len); if (sunsoft) { for (size_t i=0; isound_stream_update(ayBuf,1); + bufL[i+start]=ayBuf[0][0]; bufR[i+start]=bufL[i+start]; - } - } else if (stereo) { - for (size_t i=0; idata[oscBuf[0]->needle++]=sunsoftVolTable[31-(ay->lastIndx&31)]>>3; + oscBuf[1]->data[oscBuf[1]->needle++]=sunsoftVolTable[31-((ay->lastIndx>>5)&31)]>>3; + oscBuf[2]->data[oscBuf[2]->needle++]=sunsoftVolTable[31-((ay->lastIndx>>10)&31)]>>3; } } else { - for (size_t i=0; isound_stream_update(ayBuf,len); + if (stereo) { + for (size_t i=0; idata[oscBuf[ch]->needle++]=ayBuf[ch][i]; + for (int ch=0; ch<3; ch++) { + for (size_t i=0; idata[oscBuf[ch]->needle++]=ayBuf[ch][i]; + } } } } diff --git a/src/engine/platform/c64.cpp b/src/engine/platform/c64.cpp index 9825f189..ad111347 100644 --- a/src/engine/platform/c64.cpp +++ b/src/engine/platform/c64.cpp @@ -19,9 +19,10 @@ #include "c64.h" #include "../engine.h" +#include "sound/c64_fp/siddefs-fp.h" #include -#define rWrite(a,v) if (!skipRegisterWrites) {sid.write(a,v); regPool[(a)&0x1f]=v; if (dumpWrites) {addWrite(a,v);} } +#define rWrite(a,v) if (!skipRegisterWrites) {if (isFP) {sid_fp.write(a,v);} else {sid.write(a,v);}; regPool[(a)&0x1f]=v; if (dumpWrites) {addWrite(a,v);} } #define CHIP_FREQBASE 524288 @@ -63,15 +64,25 @@ const char** DivPlatformC64::getRegisterSheet() { } void DivPlatformC64::acquire(short* bufL, short* bufR, size_t start, size_t len) { - int dcOff=sid.get_dc(0); + int dcOff=isFP?0:sid.get_dc(0); for (size_t i=start; i=8) { - writeOscBuf=0; - oscBuf[0]->data[oscBuf[0]->needle++]=(sid.last_chan_out[0]-dcOff)>>5; - oscBuf[1]->data[oscBuf[1]->needle++]=(sid.last_chan_out[1]-dcOff)>>5; - oscBuf[2]->data[oscBuf[2]->needle++]=(sid.last_chan_out[2]-dcOff)>>5; + if (isFP) { + sid_fp.clock(4,&bufL[i]); + if (++writeOscBuf>=4) { + writeOscBuf=0; + oscBuf[0]->data[oscBuf[0]->needle++]=(sid_fp.lastChanOut[0]-dcOff)>>5; + oscBuf[1]->data[oscBuf[1]->needle++]=(sid_fp.lastChanOut[1]-dcOff)>>5; + oscBuf[2]->data[oscBuf[2]->needle++]=(sid_fp.lastChanOut[2]-dcOff)>>5; + } + } else { + sid.clock(); + bufL[i]=sid.output(); + if (++writeOscBuf>=16) { + writeOscBuf=0; + oscBuf[0]->data[oscBuf[0]->needle++]=(sid.last_chan_out[0]-dcOff)>>5; + oscBuf[1]->data[oscBuf[1]->needle++]=(sid.last_chan_out[1]-dcOff)>>5; + oscBuf[2]->data[oscBuf[2]->needle++]=(sid.last_chan_out[2]-dcOff)>>5; + } } } } @@ -405,7 +416,11 @@ int DivPlatformC64::dispatch(DivCommand c) { void DivPlatformC64::muteChannel(int ch, bool mute) { isMuted[ch]=mute; - sid.set_is_muted(ch,mute); + if (isFP) { + sid_fp.mute(ch,mute); + } else { + sid.set_is_muted(ch,mute); + } } void DivPlatformC64::forceIns() { @@ -462,13 +477,21 @@ bool DivPlatformC64::getWantPreNote() { return true; } +float DivPlatformC64::getPostAmp() { + return isFP?3.0f:1.0f; +} + void DivPlatformC64::reset() { for (int i=0; i<3; i++) { chan[i]=DivPlatformC64::Channel(); chan[i].std.setEngine(parent); } - sid.reset(); + if (isFP) { + sid_fp.reset(); + } else { + sid.reset(); + } memset(regPool,0,32); rWrite(0x18,0x0f); @@ -490,12 +513,24 @@ void DivPlatformC64::poke(std::vector& wlist) { void DivPlatformC64::setChipModel(bool is6581) { if (is6581) { - sid.set_chip_model(MOS6581); + if (isFP) { + sid_fp.setChipModel(reSIDfp::MOS6581); + } else { + sid.set_chip_model(MOS6581); + } } else { - sid.set_chip_model(MOS8580); + if (isFP) { + sid_fp.setChipModel(reSIDfp::MOS8580); + } else { + sid.set_chip_model(MOS8580); + } } } +void DivPlatformC64::setFP(bool fp) { + isFP=fp; +} + void DivPlatformC64::setFlags(unsigned int flags) { switch (flags&0xf) { case 0x0: // NTSC C64 @@ -513,6 +548,10 @@ void DivPlatformC64::setFlags(unsigned int flags) { for (int i=0; i<3; i++) { oscBuf[i]->rate=rate/16; } + if (isFP) { + rate/=4; + sid_fp.setSamplingParameters(chipClock,reSIDfp::DECIMATE,rate,0); + } } int DivPlatformC64::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { diff --git a/src/engine/platform/c64.h b/src/engine/platform/c64.h index 79963597..7587730b 100644 --- a/src/engine/platform/c64.h +++ b/src/engine/platform/c64.h @@ -23,6 +23,7 @@ #include "../dispatch.h" #include "../macroInt.h" #include "sound/c64/sid.h" +#include "sound/c64_fp/SID.h" class DivPlatformC64: public DivDispatch { struct Channel { @@ -76,12 +77,17 @@ class DivPlatformC64: public DivDispatch { unsigned char filtControl, filtRes, vol; unsigned char writeOscBuf; int filtCut, resetTime; + bool isFP; SID sid; + reSIDfp::SID sid_fp; unsigned char regPool[32]; friend void putDispatchChan(void*,int,int); + void acquire_classic(short* bufL, short* bufR, size_t start, size_t len); + void acquire_fp(short* bufL, short* bufR, size_t start, size_t len); + void updateFilter(); public: void acquire(short* bufL, short* bufR, size_t start, size_t len); @@ -98,6 +104,7 @@ class DivPlatformC64: public DivDispatch { void notifyInsChange(int ins); bool getDCOffRequired(); bool getWantPreNote(); + float getPostAmp(); DivMacroInt* getChanMacroInt(int ch); void notifyInsDeletion(void* ins); void poke(unsigned int addr, unsigned short val); @@ -105,6 +112,7 @@ class DivPlatformC64: public DivDispatch { const char** getRegisterSheet(); int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); void setChipModel(bool is6581); + void setFP(bool fp); void quit(); ~DivPlatformC64(); }; diff --git a/src/engine/platform/genesis.cpp b/src/engine/platform/genesis.cpp index f499865e..515f0c0a 100644 --- a/src/engine/platform/genesis.cpp +++ b/src/engine/platform/genesis.cpp @@ -347,6 +347,10 @@ void DivPlatformGenesis::tick(bool sysTick) { chan[i].state.ams=chan[i].std.ams.val; rWrite(chanOffs[i]+ADDR_LRAF,(IS_REALLY_MUTED(i)?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); } + if (chan[i].std.ex4.had && chan[i].active) { + chan[i].opMask=chan[i].std.ex4.val&15; + chan[i].opMaskChanged=true; + } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -479,8 +483,9 @@ void DivPlatformGenesis::tick(bool sysTick) { } chan[i].freqChanged=false; } - if (chan[i].keyOn) { - if (i<6) immWrite(0x28,0xf0|konOffs[i]); + if (chan[i].keyOn || chan[i].opMaskChanged) { + if (i<6) immWrite(0x28,(chan[i].opMask<<4)|konOffs[i]); + chan[i].opMaskChanged=false; chan[i].keyOn=false; } } @@ -591,6 +596,11 @@ int DivPlatformGenesis::dispatch(DivCommand c) { if (chan[c.chan].insChanged) { chan[c.chan].state=ins->fm; + chan[c.chan].opMask= + (chan[c.chan].state.op[0].enable?1:0)| + (chan[c.chan].state.op[2].enable?2:0)| + (chan[c.chan].state.op[1].enable?4:0)| + (chan[c.chan].state.op[3].enable?8:0); } chan[c.chan].macroInit(ins); diff --git a/src/engine/platform/genesis.h b/src/engine/platform/genesis.h index 5e61fe67..8588b21d 100644 --- a/src/engine/platform/genesis.h +++ b/src/engine/platform/genesis.h @@ -45,9 +45,9 @@ class DivPlatformGenesis: public DivPlatformOPN { unsigned char freqH, freqL; int freq, baseFreq, pitch, pitch2, portaPauseFreq, note; int ins; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, furnaceDac, inPorta, hardReset; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, furnaceDac, inPorta, hardReset, opMaskChanged; int vol, outVol; - unsigned char pan; + unsigned char pan, opMask; bool dacMode; int dacPeriod; @@ -82,9 +82,11 @@ class DivPlatformGenesis: public DivPlatformOPN { furnaceDac(false), inPorta(false), hardReset(false), + opMaskChanged(false), vol(0), outVol(0), pan(3), + opMask(15), dacMode(false), dacPeriod(0), dacRate(0), diff --git a/src/engine/platform/genesisext.cpp b/src/engine/platform/genesisext.cpp index dfd7d514..a2df33c4 100644 --- a/src/engine/platform/genesisext.cpp +++ b/src/engine/platform/genesisext.cpp @@ -25,6 +25,7 @@ #define CHIP_DIVIDER fmDivBase #define IS_REALLY_MUTED(x) (isMuted[x] && (x<5 || !softPCM || (isMuted[5] && isMuted[6]))) +#define IS_EXTCH_MUTED (isOpMuted[0] && isOpMuted[1] && isOpMuted[2] && isOpMuted[3]) int DivPlatformGenesisExt::dispatch(DivCommand c) { if (c.chan<2) { @@ -69,10 +70,11 @@ int DivPlatformGenesisExt::dispatch(DivCommand c) { rWrite(baseAddr+0x70,op.d2r&31); rWrite(baseAddr+0x80,(op.rr&15)|(op.sl<<4)); rWrite(baseAddr+0x90,op.ssgEnv&15); + opChan[ch].mask=op.enable; } if (opChan[ch].insChanged) { // TODO how does this work? rWrite(chanOffs[2]+0xb0,(chan[2].state.alg&7)|(chan[2].state.fb<<3)); - rWrite(chanOffs[2]+0xb4,(opChan[ch].pan<<6)|(chan[2].state.fms&7)|((chan[2].state.ams&3)<<4)); + rWrite(chanOffs[2]+0xb4,(IS_EXTCH_MUTED?0:(opChan[ch].pan<<6))|(chan[2].state.fms&7)|((chan[2].state.ams&3)<<4)); } opChan[ch].insChanged=false; @@ -123,7 +125,7 @@ int DivPlatformGenesisExt::dispatch(DivCommand c) { opChan[i].pan=opChan[ch].pan; } } - rWrite(chanOffs[2]+0xb4,(opChan[ch].pan<<6)|(chan[2].state.fms&7)|((chan[2].state.ams&3)<<4)); + rWrite(chanOffs[2]+0xb4,(IS_EXTCH_MUTED?0:(opChan[ch].pan<<6))|(chan[2].state.fms&7)|((chan[2].state.ams&3)<<4)); break; } case DIV_CMD_PITCH: { @@ -397,6 +399,8 @@ void DivPlatformGenesisExt::muteChannel(int ch, bool mute) { rWrite(baseAddr+0x40,op.tl); immWrite(baseAddr+0x40,op.tl); } + + rWrite(chanOffs[2]+0xb4,(IS_EXTCH_MUTED?0:(opChan[ch-2].pan<<6))|(chan[2].state.fms&7)|((chan[2].state.ams&3)<<4)); } static int opChanOffsL[4]={ @@ -412,7 +416,7 @@ void DivPlatformGenesisExt::tick(bool sysTick) { bool writeSomething=false; unsigned char writeMask=2; for (int i=0; i<4; i++) { - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn || opChan[i].keyOff) { writeSomething=true; writeMask&=~(1<<(4+i)); @@ -459,10 +463,12 @@ void DivPlatformGenesisExt::tick(bool sysTick) { immWrite(opChanOffsH[i],opChan[i].freq>>8); immWrite(opChanOffsL[i],opChan[i].freq&0xff); } - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn) { writeNoteOn=true; - writeMask|=1<<(4+i); + if (opChan[i].mask) { + writeMask|=1<<(4+i); + } opChan[i].keyOn=false; } } @@ -544,7 +550,11 @@ void DivPlatformGenesisExt::forceIns() { rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); } rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); - rWrite(chanOffs[i]+ADDR_LRAF,(IS_REALLY_MUTED(i)?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); + if (i==2) { + rWrite(chanOffs[i]+ADDR_LRAF,(IS_EXTCH_MUTED?0:(opChan[0].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); + } else { + rWrite(chanOffs[i]+ADDR_LRAF,(IS_REALLY_MUTED(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; diff --git a/src/engine/platform/genesisext.h b/src/engine/platform/genesisext.h index 07e0d5cc..d4dd93e7 100644 --- a/src/engine/platform/genesisext.h +++ b/src/engine/platform/genesisext.h @@ -27,7 +27,7 @@ class DivPlatformGenesisExt: public DivPlatformGenesis { unsigned char freqH, freqL; int freq, baseFreq, pitch, pitch2, portaPauseFreq, ins; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, mask; int vol; unsigned char pan; OpChannel(): @@ -46,6 +46,7 @@ class DivPlatformGenesisExt: public DivPlatformGenesis { keyOff(false), portaPause(false), inPorta(false), + mask(true), vol(0), pan(3) {} }; diff --git a/src/engine/platform/n163.cpp b/src/engine/platform/n163.cpp index 7da2e0a1..bc985b42 100644 --- a/src/engine/platform/n163.cpp +++ b/src/engine/platform/n163.cpp @@ -642,6 +642,9 @@ void DivPlatformN163::setFlags(unsigned int flags) { for (int i=0; i<8; i++) { oscBuf[i]->rate=rate/(initChanMax+1); } + + // needed to make sure changing channel count won't trigger glitches + reset(); } int DivPlatformN163::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index ab466f1b..50fcd5ca 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -115,11 +115,11 @@ void DivPlatformNES::acquire_puNES(short* bufL, short* bufR, size_t start, size_ bufL[i]=sample; if (++writeOscBuf>=32) { writeOscBuf=0; - oscBuf[0]->data[oscBuf[0]->needle++]=nes->S1.output<<11; - oscBuf[1]->data[oscBuf[1]->needle++]=nes->S2.output<<11; - oscBuf[2]->data[oscBuf[2]->needle++]=nes->TR.output<<11; - oscBuf[3]->data[oscBuf[3]->needle++]=nes->NS.output<<11; - oscBuf[4]->data[oscBuf[4]->needle++]=nes->DMC.output<<8; + oscBuf[0]->data[oscBuf[0]->needle++]=isMuted[0]?0:(nes->S1.output<<11); + oscBuf[1]->data[oscBuf[1]->needle++]=isMuted[1]?0:(nes->S2.output<<11); + oscBuf[2]->data[oscBuf[2]->needle++]=isMuted[2]?0:(nes->TR.output<<11); + oscBuf[3]->data[oscBuf[3]->needle++]=isMuted[3]?0:(nes->NS.output<<11); + oscBuf[4]->data[oscBuf[4]->needle++]=isMuted[4]?0:(nes->DMC.output<<8); } } } diff --git a/src/engine/platform/sound/ay8910.cpp b/src/engine/platform/sound/ay8910.cpp index 2c11b6e3..c7be503e 100644 --- a/src/engine/platform/sound/ay8910.cpp +++ b/src/engine/platform/sound/ay8910.cpp @@ -924,6 +924,7 @@ float ay8910_device::mix_3D() indx |= tone_mask | (m_vol_enabled[chan] ? tone_volume(tone) << (chan*5) : 0); } } + lastIndx=indx; return m_vol3d_table[indx]; } @@ -1359,6 +1360,7 @@ unsigned char ay8910_device::ay8910_read_ym() void ay8910_device::device_reset() { + lastIndx=0; ay8910_reset_ym(); } diff --git a/src/engine/platform/sound/ay8910.h b/src/engine/platform/sound/ay8910.h index 314383f5..6f4c6f31 100644 --- a/src/engine/platform/sound/ay8910.h +++ b/src/engine/platform/sound/ay8910.h @@ -146,6 +146,8 @@ public: double m_Kn[32]; }; + int lastIndx; + // internal interface for PSG component of YM device // FIXME: these should be private, but vector06 accesses them directly diff --git a/src/engine/platform/sound/c64_fp/AUTHORS b/src/engine/platform/sound/c64_fp/AUTHORS new file mode 100644 index 00000000..b04ee0f0 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/AUTHORS @@ -0,0 +1,6 @@ +Authors of reSIDfp. + +Dag Lem: Designed and programmed complete emulation engine. +Antti S. Lankila: Distortion simulation and calculation of combined waveforms +Ken Händel: source code conversion to Java +Leandro Nini: port to c++, merge with reSID 1.0 diff --git a/src/engine/platform/sound/c64_fp/COPYING b/src/engine/platform/sound/c64_fp/COPYING new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/src/engine/platform/sound/c64_fp/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/engine/platform/sound/c64_fp/Dac.cpp b/src/engine/platform/sound/c64_fp/Dac.cpp new file mode 100644 index 00000000..0665da81 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Dac.cpp @@ -0,0 +1,123 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 "Dac.h" + +namespace reSIDfp +{ + +Dac::Dac(unsigned int bits) : + dac(new double[bits]), + dacLength(bits) +{} + +Dac::~Dac() +{ + delete [] dac; +} + +double Dac::getOutput(unsigned int input) const +{ + double dacValue = 0.; + + for (unsigned int i = 0; i < dacLength; i++) + { + if ((input & (1 << i)) != 0) + { + dacValue += dac[i]; + } + } + + return dacValue; +} + +void Dac::kinkedDac(ChipModel chipModel) +{ + const double R_INFINITY = 1e6; + + // Non-linearity parameter, 8580 DACs are perfectly linear + const double _2R_div_R = chipModel == MOS6581 ? 2.20 : 2.00; + + // 6581 DACs are not terminated by a 2R resistor + const bool term = chipModel == MOS8580; + + // Calculate voltage contribution by each individual bit in the R-2R ladder. + for (unsigned int set_bit = 0; set_bit < dacLength; set_bit++) + { + double Vn = 1.; // Normalized bit voltage. + double R = 1.; // Normalized R + const double _2R = _2R_div_R * R; // 2R + double Rn = term ? // Rn = 2R for correct termination, + _2R : R_INFINITY; // INFINITY for missing termination. + + unsigned int bit; + + // Calculate DAC "tail" resistance by repeated parallel substitution. + for (bit = 0; bit < set_bit; bit++) + { + Rn = (Rn == R_INFINITY) ? + R + _2R : + R + (_2R * Rn) / (_2R + Rn); // R + 2R || Rn + } + + // Source transformation for bit voltage. + if (Rn == R_INFINITY) + { + Rn = _2R; + } + else + { + Rn = (_2R * Rn) / (_2R + Rn); // 2R || Rn + Vn = Vn * Rn / _2R; + } + + // Calculate DAC output voltage by repeated source transformation from + // the "tail". + + for (++bit; bit < dacLength; bit++) + { + Rn += R; + const double I = Vn / Rn; + Rn = (_2R * Rn) / (_2R + Rn); // 2R || Rn + Vn = Rn * I; + } + + dac[set_bit] = Vn; + } + + // Normalize to integerish behavior + double Vsum = 0.; + + for (unsigned int i = 0; i < dacLength; i++) + { + Vsum += dac[i]; + } + + Vsum /= 1 << dacLength; + + for (unsigned int i = 0; i < dacLength; i++) + { + dac[i] /= Vsum; + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Dac.h b/src/engine/platform/sound/c64_fp/Dac.h new file mode 100644 index 00000000..35bc0b2c --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Dac.h @@ -0,0 +1,111 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 DAC_H +#define DAC_H + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * Estimate DAC nonlinearity. + * The SID DACs are built up as R-2R ladder as follows: + * + * n n-1 2 1 0 VGND + * | | | | | | Termination + * 2R 2R 2R 2R 2R 2R only for + * | | | | | | MOS 8580 + * Vo -o-R-o-R-...-o-R-o-R-- --+ + * + * + * All MOS 6581 DACs are missing a termination resistor at bit 0. This causes + * pronounced errors for the lower 4 - 5 bits (e.g. the output for bit 0 is + * actually equal to the output for bit 1), resulting in DAC discontinuities + * for the lower bits. + * In addition to this, the 6581 DACs exhibit further severe discontinuities + * for higher bits, which may be explained by a less than perfect match between + * the R and 2R resistors, or by output impedance in the NMOS transistors + * providing the bit voltages. A good approximation of the actual DAC output is + * achieved for 2R/R ~ 2.20. + * + * The MOS 8580 DACs, on the other hand, do not exhibit any discontinuities. + * These DACs include the correct termination resistor, and also seem to have + * very accurately matched R and 2R resistors (2R/R = 2.00). + * + * On the 6581 the output of the waveform and envelope DACs go through + * a voltage follower built with two NMOS: + * + * Vdd + * + * | + * |-+ + * Vin -------| T1 (enhancement-mode) + * |-+ + * | + * o-------- Vout + * | + * |-+ + * +---| T2 (depletion-mode) + * | |-+ + * | | + * + * GND GND + */ +class Dac +{ +private: + /// analog values + double * const dac; + + /// the dac array length + const unsigned int dacLength; + +public: + /** + * Initialize DAC model. + * + * @param bits the number of input bits + */ + Dac(unsigned int bits); + ~Dac(); + + /** + * Build DAC model for specific chip. + * + * @param chipModel 6581 or 8580 + */ + void kinkedDac(ChipModel chipModel); + + /** + * Get the Vo output for a given combination of input bits. + * + * @param input the digital input + * @return the analog output value + */ + double getOutput(unsigned int input) const; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/EnvelopeGenerator.cpp b/src/engine/platform/sound/c64_fp/EnvelopeGenerator.cpp new file mode 100644 index 00000000..af636ac7 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/EnvelopeGenerator.cpp @@ -0,0 +1,155 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2018 VICE Project + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define ENVELOPEGENERATOR_CPP + +#include "EnvelopeGenerator.h" + +namespace reSIDfp +{ + +/** + * Lookup table to convert from attack, decay, or release value to rate + * counter period. + * + * The rate counter is a 15 bit register which is left shifted each cycle. + * When the counter reaches a specific comparison value, + * the envelope counter is incremented (attack) or decremented + * (decay/release) and the rate counter is resetted. + * + * see [kevtris.org](http://blog.kevtris.org/?p=13) + */ +const unsigned int EnvelopeGenerator::adsrtable[16] = +{ + 0x007f, + 0x3000, + 0x1e00, + 0x0660, + 0x0182, + 0x5573, + 0x000e, + 0x3805, + 0x2424, + 0x2220, + 0x090c, + 0x0ecd, + 0x010e, + 0x23f7, + 0x5237, + 0x64a8 +}; + +void EnvelopeGenerator::reset() +{ + // counter is not changed on reset + envelope_pipeline = 0; + + state_pipeline = 0; + + attack = 0; + decay = 0; + sustain = 0; + release = 0; + + gate = false; + + resetLfsr = true; + + exponential_counter = 0; + exponential_counter_period = 1; + new_exponential_counter_period = 0; + + state = RELEASE; + counter_enabled = true; + rate = adsrtable[release]; +} + +void EnvelopeGenerator::writeCONTROL_REG(unsigned char control) +{ + const bool gate_next = (control & 0x01) != 0; + + if (gate_next != gate) + { + gate = gate_next; + + // The rate counter is never reset, thus there will be a delay before the + // envelope counter starts counting up (attack) or down (release). + + if (gate_next) + { + // Gate bit on: Start attack, decay, sustain. + next_state = ATTACK; + state_pipeline = 2; + + if (resetLfsr || (exponential_pipeline == 2)) + { + envelope_pipeline = (exponential_counter_period == 1) || (exponential_pipeline == 2) ? 2 : 4; + } + else if (exponential_pipeline == 1) + { + state_pipeline = 3; + } + } + else + { + // Gate bit off: Start release. + next_state = RELEASE; + state_pipeline = envelope_pipeline > 0 ? 3 : 2; + } + } +} + +void EnvelopeGenerator::writeATTACK_DECAY(unsigned char attack_decay) +{ + attack = (attack_decay >> 4) & 0x0f; + decay = attack_decay & 0x0f; + + if (state == ATTACK) + { + rate = adsrtable[attack]; + } + else if (state == DECAY_SUSTAIN) + { + rate = adsrtable[decay]; + } +} + +void EnvelopeGenerator::writeSUSTAIN_RELEASE(unsigned char sustain_release) +{ + // From the sustain levels it follows that both the low and high 4 bits + // of the envelope counter are compared to the 4-bit sustain value. + // This has been verified by sampling ENV3. + // + // For a detailed description see: + // http://ploguechipsounds.blogspot.it/2010/11/new-research-on-sid-adsr.html + sustain = (sustain_release & 0xf0) | ((sustain_release >> 4) & 0x0f); + + release = sustain_release & 0x0f; + + if (state == RELEASE) + { + rate = adsrtable[release]; + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/EnvelopeGenerator.h b/src/engine/platform/sound/c64_fp/EnvelopeGenerator.h new file mode 100644 index 00000000..f2aab387 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/EnvelopeGenerator.h @@ -0,0 +1,419 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2018 VICE Project + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 ENVELOPEGENERATOR_H +#define ENVELOPEGENERATOR_H + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * A 15 bit [LFSR] is used to implement the envelope rates, in effect dividing + * the clock to the envelope counter by the currently selected rate period. + * + * In addition, another 5 bit counter is used to implement the exponential envelope decay, + * in effect further dividing the clock to the envelope counter. + * The period of this counter is set to 1, 2, 4, 8, 16, 30 at the envelope counter + * values 255, 93, 54, 26, 14, 6, respectively. + * + * [LFSR]: https://en.wikipedia.org/wiki/Linear_feedback_shift_register + */ +class EnvelopeGenerator +{ +private: + /** + * The envelope state machine's distinct states. In addition to this, + * envelope has a hold mode, which freezes envelope counter to zero. + */ + enum State + { + ATTACK, DECAY_SUSTAIN, RELEASE + }; + +private: + /// XOR shift register for ADSR prescaling. + unsigned int lfsr; + + /// Comparison value (period) of the rate counter before next event. + unsigned int rate; + + /** + * During release mode, the SID approximates envelope decay via piecewise + * linear decay rate. + */ + unsigned int exponential_counter; + + /** + * Comparison value (period) of the exponential decay counter before next + * decrement. + */ + unsigned int exponential_counter_period; + unsigned int new_exponential_counter_period; + + unsigned int state_pipeline; + + /// + unsigned int envelope_pipeline; + + unsigned int exponential_pipeline; + + /// Current envelope state + State state; + State next_state; + + /// Whether counter is enabled. Only switching to ATTACK can release envelope. + bool counter_enabled; + + /// Gate bit + bool gate; + + /// + bool resetLfsr; + + /// The current digital value of envelope output. + unsigned char envelope_counter; + + /// Attack register + unsigned char attack; + + /// Decay register + unsigned char decay; + + /// Sustain register + unsigned char sustain; + + /// Release register + unsigned char release; + + /// The ENV3 value, sampled at the first phase of the clock + unsigned char env3; + +private: + static const unsigned int adsrtable[16]; + +private: + void set_exponential_counter(); + + void state_change(); + +public: + /** + * SID clocking. + */ + void clock(); + + /** + * Get the Envelope Generator digital output. + */ + unsigned int output() const { return envelope_counter; } + + /** + * Constructor. + */ + EnvelopeGenerator() : + lfsr(0x7fff), + rate(0), + exponential_counter(0), + exponential_counter_period(1), + new_exponential_counter_period(0), + state_pipeline(0), + envelope_pipeline(0), + exponential_pipeline(0), + state(RELEASE), + next_state(RELEASE), + counter_enabled(true), + gate(false), + resetLfsr(false), + envelope_counter(0xaa), + attack(0), + decay(0), + sustain(0), + release(0), + env3(0) + {} + + /** + * SID reset. + */ + void reset(); + + /** + * Write control register. + * + * @param control + * control register value + */ + void writeCONTROL_REG(unsigned char control); + + /** + * Write Attack/Decay register. + * + * @param attack_decay + * attack/decay value + */ + void writeATTACK_DECAY(unsigned char attack_decay); + + /** + * Write Sustain/Release register. + * + * @param sustain_release + * sustain/release value + */ + void writeSUSTAIN_RELEASE(unsigned char sustain_release); + + /** + * Return the envelope current value. + * + * @return envelope counter value + */ + unsigned char readENV() const { return env3; } +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(ENVELOPEGENERATOR_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +void EnvelopeGenerator::clock() +{ + env3 = envelope_counter; + + if (unlikely(new_exponential_counter_period > 0)) + { + exponential_counter_period = new_exponential_counter_period; + new_exponential_counter_period = 0; + } + + if (unlikely(state_pipeline)) + { + state_change(); + } + + if (unlikely(envelope_pipeline != 0) && (--envelope_pipeline == 0)) + { + if (likely(counter_enabled)) + { + if (state == ATTACK) + { + if (++envelope_counter==0xff) + { + next_state = DECAY_SUSTAIN; + state_pipeline = 3; + } + } + else if ((state == DECAY_SUSTAIN) || (state == RELEASE)) + { + if (--envelope_counter==0x00) + { + counter_enabled = false; + } + } + + set_exponential_counter(); + } + } + else if (unlikely(exponential_pipeline != 0) && (--exponential_pipeline == 0)) + { + exponential_counter = 0; + + if (((state == DECAY_SUSTAIN) && (envelope_counter != sustain)) + || (state == RELEASE)) + { + // The envelope counter can flip from 0x00 to 0xff by changing state to + // attack, then to release. The envelope counter will then continue + // counting down in the release state. + // This has been verified by sampling ENV3. + + envelope_pipeline = 1; + } + } + else if (unlikely(resetLfsr)) + { + lfsr = 0x7fff; + resetLfsr = false; + + if (state == ATTACK) + { + // The first envelope step in the attack state also resets the exponential + // counter. This has been verified by sampling ENV3. + exponential_counter = 0; // NOTE this is actually delayed one cycle, not modeled + + // The envelope counter can flip from 0xff to 0x00 by changing state to + // release, then to attack. The envelope counter is then frozen at + // zero; to unlock this situation the state must be changed to release, + // then to attack. This has been verified by sampling ENV3. + + envelope_pipeline = 2; + } + else + { + if (counter_enabled && (++exponential_counter == exponential_counter_period)) + exponential_pipeline = exponential_counter_period != 1 ? 2 : 1; + } + } + + // ADSR delay bug. + // If the rate counter comparison value is set below the current value of the + // rate counter, the counter will continue counting up until it wraps around + // to zero at 2^15 = 0x8000, and then count rate_period - 1 before the + // envelope can constly be stepped. + // This has been verified by sampling ENV3. + + // check to see if LFSR matches table value + if (likely(lfsr != rate)) + { + // it wasn't a match, clock the LFSR once + // by performing XOR on last 2 bits + const unsigned int feedback = ((lfsr << 14) ^ (lfsr << 13)) & 0x4000; + lfsr = (lfsr >> 1) | feedback; + } + else + { + resetLfsr = true; + } +} + +/** + * This is what happens on chip during state switching, + * based on die reverse engineering and transistor level + * emulation. + * + * Attack + * + * 0 - Gate on + * 1 - Counting direction changes + * During this cycle the decay rate is "accidentally" activated + * 2 - Counter is being inverted + * Now the attack rate is correctly activated + * Counter is enabled + * 3 - Counter will be counting upward from now on + * + * Decay + * + * 0 - Counter == $ff + * 1 - Counting direction changes + * The attack state is still active + * 2 - Counter is being inverted + * During this cycle the decay state is activated + * 3 - Counter will be counting downward from now on + * + * Release + * + * 0 - Gate off + * 1 - During this cycle the release state is activated if coming from sustain/decay + * *2 - Counter is being inverted, the release state is activated + * *3 - Counter will be counting downward from now on + * + * (* only if coming directly from Attack state) + * + * Freeze + * + * 0 - Counter == $00 + * 1 - Nothing + * 2 - Counter is disabled + */ +RESID_INLINE +void EnvelopeGenerator::state_change() +{ + state_pipeline--; + + switch (next_state) + { + case ATTACK: + if (state_pipeline == 1) + { + // The decay rate is "accidentally" enabled during first cycle of attack phase + rate = adsrtable[decay]; + } + else if (state_pipeline == 0) + { + state = ATTACK; + // The attack rate is correctly enabled during second cycle of attack phase + rate = adsrtable[attack]; + counter_enabled = true; + } + break; + case DECAY_SUSTAIN: + if (state_pipeline == 0) + { + state = DECAY_SUSTAIN; + rate = adsrtable[decay]; + } + break; + case RELEASE: + if (((state == ATTACK) && (state_pipeline == 0)) + || ((state == DECAY_SUSTAIN) && (state_pipeline == 1))) + { + state = RELEASE; + rate = adsrtable[release]; + } + break; + } +} + +RESID_INLINE +void EnvelopeGenerator::set_exponential_counter() +{ + // Check for change of exponential counter period. + // + // For a detailed description see: + // http://ploguechipsounds.blogspot.it/2010/03/sid-6581r3-adsr-tables-up-close.html + switch (envelope_counter) + { + case 0xff: + case 0x00: + new_exponential_counter_period = 1; + break; + + case 0x5d: + new_exponential_counter_period = 2; + break; + + case 0x36: + new_exponential_counter_period = 4; + break; + + case 0x1a: + new_exponential_counter_period = 8; + break; + + case 0x0e: + new_exponential_counter_period = 16; + break; + + case 0x06: + new_exponential_counter_period = 30; + break; + } +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/ExternalFilter.cpp b/src/engine/platform/sound/c64_fp/ExternalFilter.cpp new file mode 100644 index 00000000..eac790b3 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/ExternalFilter.cpp @@ -0,0 +1,68 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define EXTERNALFILTER_CPP + +#include "ExternalFilter.h" + +namespace reSIDfp +{ + +/** + * Get the 3 dB attenuation point. + * + * @param res the resistance value in Ohms + * @param cap the capacitance value in Farads + */ +inline double getRC(double res, double cap) +{ + return res * cap; +} + +ExternalFilter::ExternalFilter() : + w0lp_1_s7(0), + w0hp_1_s17(0) +{ + reset(); +} + +void ExternalFilter::setClockFrequency(double frequency) +{ + const double dt = 1. / frequency; + + // Low-pass: R = 10kOhm, C = 1000pF; w0l = dt/(dt+RC) = 1e-6/(1e-6+1e4*1e-9) = 0.091 + // Cutoff 1/2*PI*RC = 1/2*PI*1e4*1e-9 = 15915.5 Hz + w0lp_1_s7 = static_cast((dt / (dt + getRC(10e3, 1000e-12))) * (1 << 7) + 0.5); + + // High-pass: R = 10kOhm, C = 10uF; w0h = dt/(dt+RC) = 1e-6/(1e-6+1e4*1e-5) = 0.00000999 + // Cutoff 1/2*PI*RC = 1/2*PI*1e4*1e-5 = 1.59155 Hz + w0hp_1_s17 = static_cast((dt / (dt + getRC(10e3, 10e-6))) * (1 << 17) + 0.5); +} + +void ExternalFilter::reset() +{ + // State of filter. + Vlp = 0; //1 << (15 + 11); + Vhp = 0; +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/ExternalFilter.h b/src/engine/platform/sound/c64_fp/ExternalFilter.h new file mode 100644 index 00000000..760ee5c2 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/ExternalFilter.h @@ -0,0 +1,125 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * 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 EXTERNALFILTER_H +#define EXTERNALFILTER_H + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * The audio output stage in a Commodore 64 consists of two STC networks, a + * low-pass RC filter with 3 dB frequency 16kHz followed by a DC-blocker which + * acts as a high-pass filter with a cutoff dependent on the attached audio + * equipment impedance. Here we suppose an impedance of 10kOhm resulting + * in a 3 dB attenuation at 1.6Hz. + * To operate properly the 6581 audio output needs a pull-down resistor + *(1KOhm recommended, not needed on 8580) + * + * ~~~ + * 9/12V + * -----+ + * audio| 10k | + * +---o----R---o--------o-----(K) +----- + * out | | | | | |audio + * -----+ R 1k C 1000 | | 10 uF | + * | | pF +-C----o-----C-----+ 10k + * 470 | | + * GND GND pF R 1K | amp + * * * | +----- + * + * GND + * ~~~ + * + * The STC networks are connected with a [BJT] based [common collector] + * used as a voltage follower (featuring a 2SC1815 NPN transistor). + * * The C64c board additionally includes a [bootstrap] condenser to increase + * the input impedance of the common collector. + * + * [BJT]: https://en.wikipedia.org/wiki/Bipolar_junction_transistor + * [common collector]: https://en.wikipedia.org/wiki/Common_collector + * [bootstrap]: https://en.wikipedia.org/wiki/Bootstrapping_(electronics) + */ +class ExternalFilter +{ +private: + /// Lowpass filter voltage + int Vlp; + + /// Highpass filter voltage + int Vhp; + + int w0lp_1_s7; + + int w0hp_1_s17; + +public: + /** + * SID clocking. + * + * @param input + */ + int clock(unsigned short input); + + /** + * Constructor. + */ + ExternalFilter(); + + /** + * Setup of the external filter sampling parameters. + * + * @param frequency the main system clock frequency + */ + void setClockFrequency(double frequency); + + /** + * SID reset. + */ + void reset(); +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(EXTERNALFILTER_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +int ExternalFilter::clock(unsigned short input) +{ + const int Vi = (static_cast(input)<<11) - (1 << (11+15)); + const int dVlp = (w0lp_1_s7 * (Vi - Vlp) >> 7); + const int dVhp = (w0hp_1_s17 * (Vlp - Vhp) >> 17); + Vlp += dVlp; + Vhp += dVhp; + return (Vlp - Vhp) >> 11; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/Filter.cpp b/src/engine/platform/sound/c64_fp/Filter.cpp new file mode 100644 index 00000000..2a2dd24f --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter.cpp @@ -0,0 +1,90 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * 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 "Filter.h" + +namespace reSIDfp +{ + +void Filter::enable(bool enable) +{ + enabled = enable; + + if (enabled) + { + writeRES_FILT(filt); + } + else + { + filt1 = filt2 = filt3 = filtE = false; + } +} + +void Filter::reset() +{ + writeFC_LO(0); + writeFC_HI(0); + writeMODE_VOL(0); + writeRES_FILT(0); +} + +void Filter::writeFC_LO(unsigned char fc_lo) +{ + fc = (fc & 0x7f8) | (fc_lo & 0x007); + updatedCenterFrequency(); +} + +void Filter::writeFC_HI(unsigned char fc_hi) +{ + fc = (fc_hi << 3 & 0x7f8) | (fc & 0x007); + updatedCenterFrequency(); +} + +void Filter::writeRES_FILT(unsigned char res_filt) +{ + filt = res_filt; + + updateResonance((res_filt >> 4) & 0x0f); + + if (enabled) + { + filt1 = (filt & 0x01) != 0; + filt2 = (filt & 0x02) != 0; + filt3 = (filt & 0x04) != 0; + filtE = (filt & 0x08) != 0; + } + + updatedMixing(); +} + +void Filter::writeMODE_VOL(unsigned char mode_vol) +{ + vol = mode_vol & 0x0f; + lp = (mode_vol & 0x10) != 0; + bp = (mode_vol & 0x20) != 0; + hp = (mode_vol & 0x40) != 0; + voice3off = (mode_vol & 0x80) != 0; + + updatedMixing(); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Filter.h b/src/engine/platform/sound/c64_fp/Filter.h new file mode 100644 index 00000000..4b347336 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter.h @@ -0,0 +1,177 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2017 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * 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 FILTER_H +#define FILTER_H + +namespace reSIDfp +{ + +/** + * SID filter base class + */ +class Filter +{ +protected: + /// Current volume amplifier setting. + unsigned short* currentGain; + + /// Current filter/voice mixer setting. + unsigned short* currentMixer; + + /// Filter input summer setting. + unsigned short* currentSummer; + + /// Filter resonance value. + unsigned short* currentResonance; + + /// Filter highpass state. + int Vhp; + + /// Filter bandpass state. + int Vbp; + + /// Filter lowpass state. + int Vlp; + + /// Filter external input. + int ve; + + /// Filter cutoff frequency. + unsigned int fc; + + /// Routing to filter or outside filter + bool filt1, filt2, filt3, filtE; + + /// Switch voice 3 off. + bool voice3off; + + /// Highpass, bandpass, and lowpass filter modes. + bool hp, bp, lp; + + /// Current volume. + unsigned char vol; + +private: + /// Filter enabled. + bool enabled; + + /// Selects which inputs to route through filter. + unsigned char filt; + +protected: + /** + * Set filter cutoff frequency. + */ + virtual void updatedCenterFrequency() = 0; + + /** + * Set filter resonance. + */ + virtual void updateResonance(unsigned char res) = 0; + + /** + * Mixing configuration modified (offsets change) + */ + virtual void updatedMixing() = 0; + +public: + Filter() : + currentGain(nullptr), + currentMixer(nullptr), + currentSummer(nullptr), + currentResonance(nullptr), + Vhp(0), + Vbp(0), + Vlp(0), + ve(0), + fc(0), + filt1(false), + filt2(false), + filt3(false), + filtE(false), + voice3off(false), + hp(false), + bp(false), + lp(false), + vol(0), + enabled(true), + filt(0) {} + + virtual ~Filter() {} + + /** + * SID clocking - 1 cycle + * + * @param v1 voice 1 in + * @param v2 voice 2 in + * @param v3 voice 3 in + * @return filtered output + */ + virtual unsigned short clock(int v1, int v2, int v3) = 0; + + /** + * Enable filter. + * + * @param enable + */ + void enable(bool enable); + + /** + * SID reset. + */ + void reset(); + + /** + * Write Frequency Cutoff Low register. + * + * @param fc_lo Frequency Cutoff Low-Byte + */ + void writeFC_LO(unsigned char fc_lo); + + /** + * Write Frequency Cutoff High register. + * + * @param fc_hi Frequency Cutoff High-Byte + */ + void writeFC_HI(unsigned char fc_hi); + + /** + * Write Resonance/Filter register. + * + * @param res_filt Resonance/Filter + */ + void writeRES_FILT(unsigned char res_filt); + + /** + * Write filter Mode/Volume register. + * + * @param mode_vol Filter Mode/Volume + */ + void writeMODE_VOL(unsigned char mode_vol); + + virtual void input(int input) = 0; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/Filter6581.cpp b/src/engine/platform/sound/c64_fp/Filter6581.cpp new file mode 100644 index 00000000..c064a880 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter6581.cpp @@ -0,0 +1,75 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define FILTER6581_CPP + +#include "Filter6581.h" + +#include "Integrator6581.h" + +namespace reSIDfp +{ + +Filter6581::~Filter6581() +{ + delete [] f0_dac; +} + +void Filter6581::updatedCenterFrequency() +{ + const unsigned short Vw = f0_dac[fc]; + hpIntegrator->setVw(Vw); + bpIntegrator->setVw(Vw); +} + +void Filter6581::updatedMixing() +{ + currentGain = gain_vol[vol]; + + unsigned int ni = 0; + unsigned int no = 0; + + (filt1 ? ni : no)++; + (filt2 ? ni : no)++; + + if (filt3) ni++; + else if (!voice3off) no++; + + (filtE ? ni : no)++; + + currentSummer = summer[ni]; + + if (lp) no++; + if (bp) no++; + if (hp) no++; + + currentMixer = mixer[no]; +} + +void Filter6581::setFilterCurve(double curvePosition) +{ + delete [] f0_dac; + f0_dac = FilterModelConfig6581::getInstance()->getDAC(curvePosition); + updatedCenterFrequency(); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Filter6581.h b/src/engine/platform/sound/c64_fp/Filter6581.h new file mode 100644 index 00000000..7fca331a --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter6581.h @@ -0,0 +1,425 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 FILTER6581_H +#define FILTER6581_H + +#include "siddefs-fp.h" + +#include + +#include "Filter.h" +#include "FilterModelConfig6581.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +class Integrator6581; + +/** + * The SID filter is modeled with a two-integrator-loop biquadratic filter, + * which has been confirmed by Bob Yannes to be the actual circuit used in + * the SID chip. + * + * Measurements show that excellent emulation of the SID filter is achieved, + * except when high resonance is combined with high sustain levels. + * In this case the SID op-amps are performing less than ideally and are + * causing some peculiar behavior of the SID filter. This however seems to + * have more effect on the overall amplitude than on the color of the sound. + * + * The theory for the filter circuit can be found in "Microelectric Circuits" + * by Adel S. Sedra and Kenneth C. Smith. + * The circuit is modeled based on the explanation found there except that + * an additional inverter is used in the feedback from the bandpass output, + * allowing the summer op-amp to operate in single-ended mode. This yields + * filter outputs with levels independent of Q, which corresponds with the + * results obtained from a real SID. + * + * We have been able to model the summer and the two integrators of the circuit + * to form components of an IIR filter. + * Vhp is the output of the summer, Vbp is the output of the first integrator, + * and Vlp is the output of the second integrator in the filter circuit. + * + * According to Bob Yannes, the active stages of the SID filter are not really + * op-amps. Rather, simple NMOS inverters are used. By biasing an inverter + * into its region of quasi-linear operation using a feedback resistor from + * input to output, a MOS inverter can be made to act like an op-amp for + * small signals centered around the switching threshold. + * + * In 2008, Michael Huth facilitated closer investigation of the SID 6581 + * filter circuit by publishing high quality microscope photographs of the die. + * Tommi Lempinen has done an impressive work on re-vectorizing and annotating + * the die photographs, substantially simplifying further analysis of the + * filter circuit. + * + * The filter schematics below are reverse engineered from these re-vectorized + * and annotated die photographs. While the filter first depicted in reSID 0.9 + * is a correct model of the basic filter, the schematics are now completed + * with the audio mixer and output stage, including details on intended + * relative resistor values. Also included are schematics for the NMOS FET + * voltage controlled resistors (VCRs) used to control cutoff frequency, the + * DAC which controls the VCRs, the NMOS op-amps, and the output buffer. + * + * + * SID filter / mixer / output + * --------------------------- + * ~~~ + * +---------------------------------------------------+ + * | | + * | +--1R1-- \--+ D7 | + * | +---R1--+ | | | + * | | | o--2R1-- \--o D6 | + * | +---------o----o--Rw--o--[A>--o--Rw--o--[A>--o + * ve (EXT IN) | | | | + * D3 \ ---------------R8--o | | (CAP2A) | (CAP1A) + * | v3 | | vhp | vbp | vlp + * D2 | \ -----------R8--o +-----+ | | + * | | v2 | | | | + * D1 | | \ -------R8--o | +----------------+ | + * | | | v1 | | | | + * D0 | | | \ ---R8--+ | | +---------------------------+ + * | | | | | | | + * R6 R6 R6 R6 R6 R6 R6 + * | | | | $18 | | | $18 + * | \ | | D7: 1=open \ \ \ D6 - D4: 0=open + * | | | | | | | + * +---o---o---o-------------o---o---+ 12V + * | + * | D3 +--/ --1R2--+ | + * | +---R8--+ | | +---R2--+ | + * | | | D2 o--/ --2R2--o | | ||--+ + * +---o--[A>--o------o o--o--[A>--o--|| + * D1 o--/ --4R2--o (4.25R2) ||--+ + * $18 | | | + * 0=open D0 +--/ --8R2--+ (8.75R2) | + * + * vo (AUDIO + * OUT) + * + * + * v1 - voice 1 + * v2 - voice 2 + * v3 - voice 3 + * ve - ext in + * vhp - highpass output + * vbp - bandpass output + * vlp - lowpass output + * vo - audio out + * [A> - single ended inverting op-amp (self-biased NMOS inverter) + * Rn - "resistors", implemented with custom NMOS FETs + * Rw - cutoff frequency resistor (VCR) + * C - capacitor + * ~~~ + * Notes: + * + * R2 ~ 2.0*R1 + * R6 ~ 6.0*R1 + * R8 ~ 8.0*R1 + * R24 ~ 24.0*R1 + * + * The Rn "resistors" in the circuit are implemented with custom NMOS FETs, + * probably because of space constraints on the SID die. The silicon substrate + * is laid out in a narrow strip or "snake", with a strip length proportional + * to the intended resistance. The polysilicon gate electrode covers the entire + * silicon substrate and is fixed at 12V in order for the NMOS FET to operate + * in triode mode (a.k.a. linear mode or ohmic mode). + * + * Even in "linear mode", an NMOS FET is only an approximation of a resistor, + * as the apparant resistance increases with increasing drain-to-source + * voltage. If the drain-to-source voltage should approach the gate voltage + * of 12V, the NMOS FET will enter saturation mode (a.k.a. active mode), and + * the NMOS FET will not operate anywhere like a resistor. + * + * + * + * NMOS FET voltage controlled resistor (VCR) + * ------------------------------------------ + * ~~~ + * Vw + * + * | + * | + * R1 + * | + * +--R1--o + * | __|__ + * | ----- + * | | | + * vi -----o----+ +--o----- vo + * | | + * +----R24----+ + * + * + * vi - input + * vo - output + * Rn - "resistors", implemented with custom NMOS FETs + * Vw - voltage from 11-bit DAC (frequency cutoff control) + * ~~~ + * Notes: + * + * An approximate value for R24 can be found by using the formula for the + * filter cutoff frequency: + * + * FCmin = 1/(2*pi*Rmax*C) + * + * Assuming that a the setting for minimum cutoff frequency in combination with + * a low level input signal ensures that only negligible current will flow + * through the transistor in the schematics above, values for FCmin and C can + * be substituted in this formula to find Rmax. + * Using C = 470pF and FCmin = 220Hz (measured value), we get: + * + * FCmin = 1/(2*pi*Rmax*C) + * Rmax = 1/(2*pi*FCmin*C) = 1/(2*pi*220*470e-12) ~ 1.5MOhm + * + * From this it follows that: + * R24 = Rmax ~ 1.5MOhm + * R1 ~ R24/24 ~ 64kOhm + * R2 ~ 2.0*R1 ~ 128kOhm + * R6 ~ 6.0*R1 ~ 384kOhm + * R8 ~ 8.0*R1 ~ 512kOhm + * + * Note that these are only approximate values for one particular SID chip, + * due to process variations the values can be substantially different in + * other chips. + * + * + * + * Filter frequency cutoff DAC + * --------------------------- + * + * ~~~ + * 12V 10 9 8 7 6 5 4 3 2 1 0 VGND + * | | | | | | | | | | | | | Missing + * 2R 2R 2R 2R 2R 2R 2R 2R 2R 2R 2R 2R 2R termination + * | | | | | | | | | | | | | + * Vw --o-R-o-R-o-R-o-R-o-R-o-R-o-R-o-R-o-R-o-R-o-R-o- -+ + * + * + * Bit on: 12V + * Bit off: 5V (VGND) + * ~~~ + * As is the case with all MOS 6581 DACs, the termination to (virtual) ground + * at bit 0 is missing. + * + * Furthermore, the control of the two VCRs imposes a load on the DAC output + * which varies with the input signals to the VCRs. This can be seen from the + * VCR figure above. + * + * + * + * "Op-amp" (self-biased NMOS inverter) + * ------------------------------------ + * ~~~ + * + * 12V + * + * | + * +-----------o + * | | + * | +------o + * | | | + * | | ||--+ + * | +--|| + * | ||--+ + * ||--+ | + * vi -----|| o---o----- vo + * ||--+ | | + * | ||--+ | + * |-------|| | + * | ||--+ | + * ||--+ | | + * +--|| | | + * | ||--+ | | + * | | | | + * | +-----------o | + * | | | + * | | + * | GND | + * | | + * +----------------------+ + * + * + * vi - input + * vo - output + * ~~~ + * Notes: + * + * The schematics above are laid out to show that the "op-amp" logically + * consists of two building blocks; a saturated load NMOS inverter (on the + * right hand side of the schematics) with a buffer / bias input stage + * consisting of a variable saturated load NMOS inverter (on the left hand + * side of the schematics). + * + * Provided a reasonably high input impedance and a reasonably low output + * impedance, the "op-amp" can be modeled as a voltage transfer function + * mapping input voltage to output voltage. + * + * + * + * Output buffer (NMOS voltage follower) + * ------------------------------------- + * ~~~ + * + * 12V + * + * | + * | + * ||--+ + * vi -----|| + * ||--+ + * | + * o------ vo + * | (AUDIO + * Rext OUT) + * | + * | + * + * GND + * + * vi - input + * vo - output + * Rext - external resistor, 1kOhm + * ~~~ + * Notes: + * + * The external resistor Rext is needed to complete the NMOS voltage follower, + * this resistor has a recommended value of 1kOhm. + * + * Die photographs show that actually, two NMOS transistors are used in the + * voltage follower. However the two transistors are coupled in parallel (all + * terminals are pairwise common), which implies that we can model the two + * transistors as one. + */ +class Filter6581 final : public Filter +{ +private: + const unsigned short* f0_dac; + + unsigned short** mixer; + unsigned short** summer; + unsigned short** gain_res; + unsigned short** gain_vol; + + const int voiceScaleS11; + const int voiceDC; + + /// VCR + associated capacitor connected to highpass output. + std::unique_ptr const hpIntegrator; + + /// VCR + associated capacitor connected to bandpass output. + std::unique_ptr const bpIntegrator; + +protected: + /** + * Set filter cutoff frequency. + */ + void updatedCenterFrequency() override; + + /** + * Set filter resonance. + * + * In the MOS 6581, 1/Q is controlled linearly by res. + */ + void updateResonance(unsigned char res) override { currentResonance = gain_res[res]; } + + void updatedMixing() override; + +public: + Filter6581() : + f0_dac(FilterModelConfig6581::getInstance()->getDAC(0.5)), + mixer(FilterModelConfig6581::getInstance()->getMixer()), + summer(FilterModelConfig6581::getInstance()->getSummer()), + gain_res(FilterModelConfig6581::getInstance()->getGainRes()), + gain_vol(FilterModelConfig6581::getInstance()->getGainVol()), + voiceScaleS11(FilterModelConfig6581::getInstance()->getVoiceScaleS11()), + voiceDC(FilterModelConfig6581::getInstance()->getNormalizedVoiceDC()), + hpIntegrator(FilterModelConfig6581::getInstance()->buildIntegrator()), + bpIntegrator(FilterModelConfig6581::getInstance()->buildIntegrator()) + { + input(0); + } + + ~Filter6581(); + + unsigned short clock(int voice1, int voice2, int voice3) override; + + void input(int sample) override { ve = (sample * voiceScaleS11 * 3 >> 11) + mixer[0][0]; } + + /** + * Set filter curve type based on single parameter. + * + * @param curvePosition 0 .. 1, where 0 sets center frequency high ("light") and 1 sets it low ("dark"), default is 0.5 + */ + void setFilterCurve(double curvePosition); +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(FILTER6581_CPP) + +#include "Integrator6581.h" + +namespace reSIDfp +{ + +RESID_INLINE +unsigned short Filter6581::clock(int voice1, int voice2, int voice3) +{ + voice1 = (voice1 * voiceScaleS11 >> 15) + voiceDC; + voice2 = (voice2 * voiceScaleS11 >> 15) + voiceDC; + // Voice 3 is silenced by voice3off if it is not routed through the filter. + voice3 = (filt3 || !voice3off) ? (voice3 * voiceScaleS11 >> 15) + voiceDC : 0; + + int Vi = 0; + int Vo = 0; + + (filt1 ? Vi : Vo) += voice1; + (filt2 ? Vi : Vo) += voice2; + (filt3 ? Vi : Vo) += voice3; + (filtE ? Vi : Vo) += ve; + + Vhp = currentSummer[currentResonance[Vbp] + Vlp + Vi]; + Vbp = hpIntegrator->solve(Vhp); + Vlp = bpIntegrator->solve(Vbp); + + if (lp) Vo += Vlp; + if (bp) Vo += Vbp; + if (hp) Vo += Vhp; + + return currentGain[currentMixer[Vo]]; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/Filter8580.cpp b/src/engine/platform/sound/c64_fp/Filter8580.cpp new file mode 100644 index 00000000..a70285a8 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter8580.cpp @@ -0,0 +1,101 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2019 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define FILTER8580_CPP + +#include "Filter8580.h" + +#include "Integrator8580.h" + +namespace reSIDfp +{ + +/** + * W/L ratio of frequency DAC bit 0, + * other bit are proportional. + * When no bit are selected a resistance with half + * W/L ratio is selected. + */ +const double DAC_WL0 = 0.00615; + +Filter8580::~Filter8580() {} + +void Filter8580::updatedCenterFrequency() +{ + double wl; + double dacWL = DAC_WL0; + if (fc) + { + wl = 0.; + for (unsigned int i = 0; i < 11; i++) + { + if (fc & (1 << i)) + { + wl += dacWL; + } + dacWL *= 2.; + } + } + else + { + wl = dacWL/2.; + } + + hpIntegrator->setFc(wl); + bpIntegrator->setFc(wl); +} + +void Filter8580::updatedMixing() +{ + currentGain = gain_vol[vol]; + + unsigned int ni = 0; + unsigned int no = 0; + + (filt1 ? ni : no)++; + (filt2 ? ni : no)++; + + if (filt3) ni++; + else if (!voice3off) no++; + + (filtE ? ni : no)++; + + currentSummer = summer[ni]; + + if (lp) no++; + if (bp) no++; + if (hp) no++; + + currentMixer = mixer[no]; +} + +void Filter8580::setFilterCurve(double curvePosition) +{ + // Adjust cp + // 1.2 <= cp <= 1.8 + cp = 1.8 - curvePosition * 3./5.; + + hpIntegrator->setV(cp); + bpIntegrator->setV(cp); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Filter8580.h b/src/engine/platform/sound/c64_fp/Filter8580.h new file mode 100644 index 00000000..2166ec0d --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter8580.h @@ -0,0 +1,383 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 FILTER8580_H +#define FILTER8580_H + +#include "siddefs-fp.h" + +#include + +#include "Filter.h" +#include "FilterModelConfig8580.h" +#include "Integrator8580.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +class Integrator8580; + +/** + * Filter for 8580 chip + * -------------------- + * The 8580 filter stage had been redesigned to be more linear and robust + * against temperature change. It also features real op-amps and a + * revisited resonance model. + * The filter schematics below are reverse engineered from re-vectorized + * and annotated die photographs. Credits to Michael Huth for the microscope + * photographs of the die, Tommi Lempinen for re-vectorizating and annotating + * the images and ttlworks from forum.6502.org for the circuit analysis. + * + * ~~~ + * + * +---------------------------------------------------+ + * | $17 +----Rf-+ | + * | | | | + * | D4&!D5 o- \-R3-o | + * | | | $17 | + * | !D4&!D5 o- \-R2-o | + * | | | +---R8-- \--+ !D6&D7 | + * | D4&!D5 o- \-R1-o | | | + * | | | o---RC-- \--o D6&D7 | + * | +---------o----o--Rfc-o--[A>--o--Rfc-o--[A>--o + * ve (EXT IN) | | | | + * D3 \ --------------R12--o | | (CAP2A) | (CAP1A) + * | v3 | | vhp | vbp | vlp + * D2 | \ -----------R7--o +-----+ | | + * | | v2 | | | | + * D1 | | \ -------R7--o | +----------------+ | + * | | | v1 | | | | + * D0 | | | \ ---R7--+ | | +---------------------------+ + * | | | | | | | + * R9 R5 R5 R5 R5 R5 R5 + * | | | | $18 | | | $18 + * | \ | | D7: 1=open \ \ \ D6 - D4: 0=open + * | | | | | | | + * +---o---o---o-------------o---o---+ + * | + * | D3 +--/ --1R4--+ + * | +---R8--+ | | +---R2--+ + * | | | D2 o--/ --2R4--o | | + * +---o--[A>--o------o o--o--[A>--o-- vo (AUDIO OUT) + * D1 o--/ --4R4--o + * $18 | | + * 0=open D0 +--/ --8R4--+ + * + * + * + * Resonance + * --------- + * For resonance, we have two tiny DACs that controls both the input + * and feedback resistances. + * + * The "resistors" are switched in as follows by bits in register $17: + * + * feedback: + * R1: bit4&!bit5 + * R2: !bit4&bit5 + * R3: bit4&bit5 + * Rf: always on + * + * input: + * R4: bit6&!bit7 + * R8: !bit6&bit7 + * RC: bit6&bit7 + * Ri: !(R4|R8|RC) = !(bit6|bit7) = !bit6&!bit7 + * + * + * The relative "resistor" values are approximately (using channel length): + * + * R1 = 15.3*Ri + * R2 = 7.3*Ri + * R3 = 4.7*Ri + * Rf = 1.4*Ri + * R4 = 1.4*Ri + * R8 = 2.0*Ri + * RC = 2.8*Ri + * + * + * Approximate values for 1/Q can now be found as follows (assuming an + * ideal op-amp): + * + * res feedback input -gain (1/Q) + * --- -------- ----- ---------- + * 0 Rf Ri Rf/Ri = 1/(Ri*(1/Rf)) = 1/0.71 + * 1 Rf|R1 Ri (Rf|R1)/Ri = 1/(Ri*(1/Rf+1/R1)) = 1/0.78 + * 2 Rf|R2 Ri (Rf|R2)/Ri = 1/(Ri*(1/Rf+1/R2)) = 1/0.85 + * 3 Rf|R3 Ri (Rf|R3)/Ri = 1/(Ri*(1/Rf+1/R3)) = 1/0.92 + * 4 Rf R4 Rf/R4 = 1/(R4*(1/Rf)) = 1/1.00 + * 5 Rf|R1 R4 (Rf|R1)/R4 = 1/(R4*(1/Rf+1/R1)) = 1/1.10 + * 6 Rf|R2 R4 (Rf|R2)/R4 = 1/(R4*(1/Rf+1/R2)) = 1/1.20 + * 7 Rf|R3 R4 (Rf|R3)/R4 = 1/(R4*(1/Rf+1/R3)) = 1/1.30 + * 8 Rf R8 Rf/R8 = 1/(R8*(1/Rf)) = 1/1.43 + * 9 Rf|R1 R8 (Rf|R1)/R8 = 1/(R8*(1/Rf+1/R1)) = 1/1.56 + * A Rf|R2 R8 (Rf|R2)/R8 = 1/(R8*(1/Rf+1/R2)) = 1/1.70 + * B Rf|R3 R8 (Rf|R3)/R8 = 1/(R8*(1/Rf+1/R3)) = 1/1.86 + * C Rf RC Rf/RC = 1/(RC*(1/Rf)) = 1/2.00 + * D Rf|R1 RC (Rf|R1)/RC = 1/(RC*(1/Rf+1/R1)) = 1/2.18 + * E Rf|R2 RC (Rf|R2)/RC = 1/(RC*(1/Rf+1/R2)) = 1/2.38 + * F Rf|R3 RC (Rf|R3)/RC = 1/(RC*(1/Rf+1/R3)) = 1/2.60 + * + * + * These data indicate that the following function for 1/Q has been + * modeled in the MOS 8580: + * + * 1/Q = 2^(1/2)*2^(-x/8) = 2^(1/2 - x/8) = 2^((4 - x)/8) + * + * + * + * Op-amps + * ------- + * Unlike the 6581, the 8580 has real OpAmps. + * + * Temperature compensated differential amplifier: + * + * 9V + * + * | + * +-------o-o-o-------+ + * | | | | + * | R R | + * +--|| | | ||--+ + * ||---o o---|| + * +--|| | | ||--+ + * | | | | + * o-----+ | | o--- Va + * | | | | | + * +--|| | | | ||--+ + * ||-o-+---+---|| + * +--|| | | ||--+ + * | | | | + * | | + * GND | | GND + * ||--+ +--|| + * in- -----|| ||------ in+ + * ||----o----|| + * | + * 8 Current sink + * | + * + * GND + * + * Inverter + non-inverting output amplifier: + * + * Va ---o---||-------------------o--------------------+ + * | | 9V | + * | +----------+----------+ | | + * | 9V | | 9V | ||--+ | + * | | | 9V | | +-|| | + * | R | | | ||--+ ||--+ | + * | | | ||--+ +--|| o---o--- Vout + * | o---o---|| ||--+ ||--+ + * | | ||--+ o-----|| + * | ||--+ | ||--+ ||--+ + * +-----|| o-----|| | + * ||--+ | ||--+ + * | R | GND + * | + * GND GND + * GND + * + * + * + * Virtual ground + * -------------- + * A PolySi resitive voltage divider provides the voltage + * for the positive input of the filter op-amps. + * + * 5V + * +----------+ + * | | |\ | + * R1 +---|-\ | + * 5V | |A >---o--- Vref + * o-------|+/ + * | | |/ + * R10 R4 + * | | + * o---+ + * | + * R10 + * | + * + * GND + * + * Rn = n*R1 + * + * + * + * Rfc - freq control DAC resistance ladder + * ---------------------------------------- + * The 8580 has 11 bits for frequency control, but 12 bit DACs. + * If those 11 bits would be '0', the impedance of the DACs would be "infinitely high". + * To get around this, there is an 11 input NOR gate below the DACs sensing those 11 bits. + * If all are 0, the NOR gate gives the gate control voltage to the 12 bit DAC LSB. + * + * ----o---o--...--o---o---o--- + * | | | | | + * Rb10 Rb9 ... Rb1 Rb0 R0 + * | | | | | + * ----o---o--...--o---o---o--- + * + * + * + * Crystal stabilized precision switched capacitor voltage divider + * --------------------------------------------------------------- + * There is a FET working as a temperature sensor close to the DACs which changes the gate voltage + * of the frequency control DACs according to the temperature of the DACs, + * to reduce the effects of temperature on the filter curve. + * An asynchronous 3 bit binary counter, running at the speed of PHI2, drives two big capacitors + * whose AC resistance is then used as a voltage divider. + * This implicates that frequency difference between PAL and NTSC might shift the filter curve by 4% or such. + * + * |\ OpAmp has a smaller capacitor than the other OPs + * Vref ---|+\ + * |A >---o--- Vdac + * +-------|-/ | + * | |/ | + * | | + * C1 | C2 | + * +---||---o---+ +---o-----||-------o + * | | | | | | + * o----+ | ----- | | + * | | | ----- +----+ +-----o + * | ----- | | | | + * | ----- | ----- | + * | | | ----- | + * | +-----------+ | | + * | /Q Q | +-------+ + * GND +-----------+ FET close to DAC + * | clk/8 | working as temperature sensor + * +-----------+ + */ +class Filter8580 final : public Filter +{ +private: + unsigned short** mixer; + unsigned short** summer; + unsigned short** gain_res; + unsigned short** gain_vol; + + const int voiceScaleS11; + const int voiceDC; + + double cp; + + /// VCR + associated capacitor connected to highpass output. + std::unique_ptr const hpIntegrator; + + /// VCR + associated capacitor connected to bandpass output. + std::unique_ptr const bpIntegrator; + +protected: + /** + * Set filter cutoff frequency. + */ + void updatedCenterFrequency() override; + + /** + * Set filter resonance. + * + * @param res the new resonance value + */ + void updateResonance(unsigned char res) override { currentResonance = gain_res[res]; } + + void updatedMixing() override; + +public: + Filter8580() : + mixer(FilterModelConfig8580::getInstance()->getMixer()), + summer(FilterModelConfig8580::getInstance()->getSummer()), + gain_res(FilterModelConfig8580::getInstance()->getGainRes()), + gain_vol(FilterModelConfig8580::getInstance()->getGainVol()), + voiceScaleS11(FilterModelConfig8580::getInstance()->getVoiceScaleS11()), + voiceDC(FilterModelConfig8580::getInstance()->getNormalizedVoiceDC()), + cp(0.5), + hpIntegrator(FilterModelConfig8580::getInstance()->buildIntegrator()), + bpIntegrator(FilterModelConfig8580::getInstance()->buildIntegrator()) + { + setFilterCurve(cp); + input(0); + } + + ~Filter8580(); + + unsigned short clock(int voice1, int voice2, int voice3) override; + + void input(int sample) override { ve = (sample * voiceScaleS11 * 3 >> 11) + mixer[0][0]; } + + /** + * Set filter curve type based on single parameter. + * + * @param curvePosition 0 .. 1, where 0 sets center frequency high ("light") and 1 sets it low ("dark"), default is 0.5 + */ + void setFilterCurve(double curvePosition); +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(FILTER8580_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +unsigned short Filter8580::clock(int voice1, int voice2, int voice3) +{ + voice1 = (voice1 * voiceScaleS11 >> 15) + voiceDC; + voice2 = (voice2 * voiceScaleS11 >> 15) + voiceDC; + // Voice 3 is silenced by voice3off if it is not routed through the filter. + voice3 = (filt3 || !voice3off) ? (voice3 * voiceScaleS11 >> 15) + voiceDC : 0; + + int Vi = 0; + int Vo = 0; + + (filt1 ? Vi : Vo) += voice1; + (filt2 ? Vi : Vo) += voice2; + (filt3 ? Vi : Vo) += voice3; + (filtE ? Vi : Vo) += ve; + + Vhp = currentSummer[currentResonance[Vbp] + Vlp + Vi]; + Vbp = hpIntegrator->solve(Vhp); + Vlp = bpIntegrator->solve(Vbp); + + if (lp) Vo += Vlp; + if (bp) Vo += Vbp; + if (hp) Vo += Vhp; + + return currentGain[currentMixer[Vo]]; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig.cpp b/src/engine/platform/sound/c64_fp/FilterModelConfig.cpp new file mode 100644 index 00000000..8fb76238 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig.cpp @@ -0,0 +1,79 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 "FilterModelConfig.h" + +#include + +namespace reSIDfp +{ + +FilterModelConfig::FilterModelConfig( + double vvr, + double vdv, + double c, + double vdd, + double vth, + double ucox, + const Spline::Point *opamp_voltage, + int opamp_size +) : + voice_voltage_range(vvr), + voice_DC_voltage(vdv), + C(c), + Vdd(vdd), + Vth(vth), + Ut(26.0e-3), + uCox(ucox), + Vddt(Vdd - Vth), + vmin(opamp_voltage[0].x), + vmax(std::max(Vddt, opamp_voltage[0].y)), + denorm(vmax - vmin), + norm(1.0 / denorm), + N16(norm * ((1 << 16) - 1)), + currFactorCoeff(denorm * (uCox / 2. * 1.0e-6 / C)) +{ + // Convert op-amp voltage transfer to 16 bit values. + + std::vector scaled_voltage(opamp_size); + + for (int i = 0; i < opamp_size; i++) + { + scaled_voltage[i].x = N16 * (opamp_voltage[i].x - opamp_voltage[i].y + denorm) / 2.; + scaled_voltage[i].y = N16 * (opamp_voltage[i].x - vmin); + } + + // Create lookup table mapping capacitor voltage to op-amp input voltage: + + Spline s(scaled_voltage); + + for (int x = 0; x < (1 << 16); x++) + { + const Spline::Point out = s.evaluate(x); + // If Vmax > max opamp_voltage the first elements may be negative + double tmp = out.x > 0. ? out.x : 0.; + assert(tmp < 65535.5); + opamp_rev[x] = static_cast(tmp + 0.5); + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig.h b/src/engine/platform/sound/c64_fp/FilterModelConfig.h new file mode 100644 index 00000000..d8ae77ab --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig.h @@ -0,0 +1,166 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 FILTERMODELCONFIG_H +#define FILTERMODELCONFIG_H + +#include +#include + +#include "Spline.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +class FilterModelConfig +{ +protected: + const double voice_voltage_range; + const double voice_DC_voltage; + + /// Capacitor value. + const double C; + + /// Transistor parameters. + //@{ + const double Vdd; + const double Vth; ///< Threshold voltage + const double Ut; ///< Thermal voltage: Ut = kT/q = 8.61734315e-5*T ~ 26mV + const double uCox; ///< Transconductance coefficient: u*Cox + const double Vddt; ///< Vdd - Vth + //@} + + // Derived stuff + const double vmin, vmax; + const double denorm, norm; + + /// Fixed point scaling for 16 bit op-amp output. + const double N16; + + /// Current factor coefficient for op-amp integrators. + const double currFactorCoeff; + + /// Lookup tables for gain and summer op-amps in output stage / filter. + //@{ + unsigned short* mixer[8]; //-V730_NOINIT this is initialized in the derived class constructor + unsigned short* summer[5]; //-V730_NOINIT this is initialized in the derived class constructor + unsigned short* gain_vol[16]; //-V730_NOINIT this is initialized in the derived class constructor + unsigned short* gain_res[16]; //-V730_NOINIT this is initialized in the derived class constructor + //@} + + /// Reverse op-amp transfer function. + unsigned short opamp_rev[1 << 16]; //-V730_NOINIT this is initialized in the derived class constructor + +private: + FilterModelConfig (const FilterModelConfig&) DELETE; + FilterModelConfig& operator= (const FilterModelConfig&) DELETE; + +protected: + /** + * @param vvr voice voltage range + * @param vdv voice DC voltage + * @param c capacitor value + * @param vdd Vdd + * @param vth threshold voltage + * @param ucox u*Cox + * @param ominv opamp min voltage + * @param omaxv opamp max voltage + */ + FilterModelConfig( + double vvr, + double vdv, + double c, + double vdd, + double vth, + double ucox, + const Spline::Point *opamp_voltage, + int opamp_size + ); + + ~FilterModelConfig() + { + for (int i = 0; i < 8; i++) + { + delete [] mixer[i]; + } + + for (int i = 0; i < 5; i++) + { + delete [] summer[i]; + } + + for (int i = 0; i < 16; i++) + { + delete [] gain_vol[i]; + delete [] gain_res[i]; + } + } + +public: + unsigned short** getGainVol() { return gain_vol; } + unsigned short** getGainRes() { return gain_res; } + unsigned short** getSummer() { return summer; } + unsigned short** getMixer() { return mixer; } + + /** + * The digital range of one voice is 20 bits; create a scaling term + * for multiplication which fits in 11 bits. + */ + int getVoiceScaleS11() const { return static_cast((norm * ((1 << 11) - 1)) * voice_voltage_range); } + + /** + * The "zero" output level of the voices. + */ + int getNormalizedVoiceDC() const { return static_cast(N16 * (voice_DC_voltage - vmin)); } + + inline unsigned short getOpampRev(int i) const { return opamp_rev[i]; } + inline double getVddt() const { return Vddt; } + inline double getVth() const { return Vth; } + inline double getVoiceDCVoltage() const { return voice_DC_voltage; } + + // helper functions + inline unsigned short getNormalizedValue(double value) const + { + const double tmp = N16 * (value - vmin); + assert(tmp > -0.5 && tmp < 65535.5); + return static_cast(tmp + 0.5); + } + + inline unsigned short getNormalizedCurrentFactor(double wl) const + { + const double tmp = (1 << 13) * currFactorCoeff * wl; + assert(tmp > -0.5 && tmp < 65535.5); + return static_cast(tmp + 0.5); + } + + inline unsigned short getNVmin() const { + const double tmp = N16 * vmin; + assert(tmp > -0.5 && tmp < 65535.5); + return static_cast(tmp + 0.5); + } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig6581.cpp b/src/engine/platform/sound/c64_fp/FilterModelConfig6581.cpp new file mode 100644 index 00000000..3d86bdcf --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig6581.cpp @@ -0,0 +1,263 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2010 Dag Lem + * + * 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 "FilterModelConfig6581.h" + +#include + +#include "Integrator6581.h" +#include "OpAmp.h" + +namespace reSIDfp +{ + +#ifndef HAVE_CXX11 +/** + * Compute log(1+x) without losing precision for small values of x + * + * @note when compiling with -ffastm-math the compiler will + * optimize the expression away leaving a plain log(1. + x) + */ +inline double log1p(double x) +{ + return log(1. + x) - (((1. + x) - 1.) - x) / (1. + x); +} +#endif + +const unsigned int OPAMP_SIZE = 33; + +/** + * This is the SID 6581 op-amp voltage transfer function, measured on + * CAP1B/CAP1A on a chip marked MOS 6581R4AR 0687 14. + * All measured chips have op-amps with output voltages (and thus input + * voltages) within the range of 0.81V - 10.31V. + */ +const Spline::Point opamp_voltage[OPAMP_SIZE] = +{ + { 0.81, 10.31 }, // Approximate start of actual range + { 2.40, 10.31 }, + { 2.60, 10.30 }, + { 2.70, 10.29 }, + { 2.80, 10.26 }, + { 2.90, 10.17 }, + { 3.00, 10.04 }, + { 3.10, 9.83 }, + { 3.20, 9.58 }, + { 3.30, 9.32 }, + { 3.50, 8.69 }, + { 3.70, 8.00 }, + { 4.00, 6.89 }, + { 4.40, 5.21 }, + { 4.54, 4.54 }, // Working point (vi = vo) + { 4.60, 4.19 }, + { 4.80, 3.00 }, + { 4.90, 2.30 }, // Change of curvature + { 4.95, 2.03 }, + { 5.00, 1.88 }, + { 5.05, 1.77 }, + { 5.10, 1.69 }, + { 5.20, 1.58 }, + { 5.40, 1.44 }, + { 5.60, 1.33 }, + { 5.80, 1.26 }, + { 6.00, 1.21 }, + { 6.40, 1.12 }, + { 7.00, 1.02 }, + { 7.50, 0.97 }, + { 8.50, 0.89 }, + { 10.00, 0.81 }, + { 10.31, 0.81 }, // Approximate end of actual range +}; + +std::unique_ptr FilterModelConfig6581::instance(nullptr); + +FilterModelConfig6581* FilterModelConfig6581::getInstance() +{ + if (!instance.get()) + { + instance.reset(new FilterModelConfig6581()); + } + + return instance.get(); +} + +FilterModelConfig6581::FilterModelConfig6581() : + FilterModelConfig( + 1.5, // voice voltage range + 5.075, // voice DC voltage + 470e-12, // capacitor value + 12.18, // Vdd + 1.31, // Vth + 20e-6, // uCox + opamp_voltage, + OPAMP_SIZE + ), + WL_vcr(9.0 / 1.0), + WL_snake(1.0 / 115.0), + dac_zero(6.65), + dac_scale(2.63), + dac(DAC_BITS) +{ + dac.kinkedDac(MOS6581); + + // Create lookup tables for gains / summers. + + OpAmp opampModel(std::vector(std::begin(opamp_voltage), std::end(opamp_voltage)), Vddt); + + // The filter summer operates at n ~ 1, and has 5 fundamentally different + // input configurations (2 - 6 input "resistors"). + // + // Note that all "on" transistors are modeled as one. This is not + // entirely accurate, since the input for each transistor is different, + // and transistors are not linear components. However modeling all + // transistors separately would be extremely costly. + for (int i = 0; i < 5; i++) + { + const int idiv = 2 + i; // 2 - 6 input "resistors". + const int size = idiv << 16; + const double n = idiv; + opampModel.reset(); + summer[i] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16 / idiv; /* vmin .. vmax */ + summer[i][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // The audio mixer operates at n ~ 8/6, and has 8 fundamentally different + // input configurations (0 - 7 input "resistors"). + // + // All "on", transistors are modeled as one - see comments above for + // the filter summer. + for (int i = 0; i < 8; i++) + { + const int idiv = (i == 0) ? 1 : i; + const int size = (i == 0) ? 1 : i << 16; + const double n = i * 8.0 / 6.0; + opampModel.reset(); + mixer[i] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16 / idiv; /* vmin .. vmax */ + mixer[i][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // 4 bit "resistor" ladders in the audio + // output gain necessitate 16 gain tables. + // From die photographs of the bandpass and volume "resistor" ladders + // it follows that gain ~ vol/12 (assuming ideal + // op-amps and ideal "resistors"). + for (int n8 = 0; n8 < 16; n8++) + { + const int size = 1 << 16; + const double n = n8 / 12.0; + opampModel.reset(); + gain_vol[n8] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16; /* vmin .. vmax */ + gain_vol[n8][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // 4 bit "resistor" ladders in the bandpass resonance gain + // necessitate 16 gain tables. + // From die photographs of the bandpass and volume "resistor" ladders + // it follows that 1/Q ~ ~res/8 (assuming ideal + // op-amps and ideal "resistors"). + for (int n8 = 0; n8 < 16; n8++) + { + const int size = 1 << 16; + const double n = (~n8 & 0xf) / 8.0; + opampModel.reset(); + gain_res[n8] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16; /* vmin .. vmax */ + gain_res[n8][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + const double nVddt = N16 * (Vddt - vmin); + + for (unsigned int i = 0; i < (1 << 16); i++) + { + // The table index is right-shifted 16 times in order to fit in + // 16 bits; the argument to sqrt is thus multiplied by (1 << 16). + const double tmp = nVddt - sqrt(static_cast(i << 16)); + assert(tmp > -0.5 && tmp < 65535.5); + vcr_nVg[i] = static_cast(tmp + 0.5); + } + + // EKV model: + // + // Ids = Is * (if - ir) + // Is = (2 * u*Cox * Ut^2)/k * W/L + // if = ln^2(1 + e^((k*(Vg - Vt) - Vs)/(2*Ut)) + // ir = ln^2(1 + e^((k*(Vg - Vt) - Vd)/(2*Ut)) + + // moderate inversion characteristic current + const double Is = (2. * uCox * Ut * Ut) * WL_vcr; + + // Normalized current factor for 1 cycle at 1MHz. + const double N15 = norm * ((1 << 15) - 1); + const double n_Is = N15 * 1.0e-6 / C * Is; + + // kVgt_Vx = k*(Vg - Vt) - Vx + // I.e. if k != 1.0, Vg must be scaled accordingly. + for (int kVgt_Vx = 0; kVgt_Vx < (1 << 16); kVgt_Vx++) + { + const double log_term = log1p(exp((kVgt_Vx / N16) / (2. * Ut))); + // Scaled by m*2^15 + const double tmp = n_Is * log_term * log_term; + assert(tmp > -0.5 && tmp < 65535.5); + vcr_n_Ids_term[kVgt_Vx] = static_cast(tmp + 0.5); + } +} + +unsigned short* FilterModelConfig6581::getDAC(double adjustment) const +{ + const double dac_zero = getDacZero(adjustment); + + unsigned short* f0_dac = new unsigned short[1 << DAC_BITS]; + + for (unsigned int i = 0; i < (1 << DAC_BITS); i++) + { + const double fcd = dac.getOutput(i); + f0_dac[i] = getNormalizedValue(dac_zero + fcd * dac_scale / (1 << DAC_BITS)); + } + + return f0_dac; +} + +std::unique_ptr FilterModelConfig6581::buildIntegrator() +{ + return MAKE_UNIQUE(Integrator6581, this, WL_snake); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig6581.h b/src/engine/platform/sound/c64_fp/FilterModelConfig6581.h new file mode 100644 index 00000000..85cbd43f --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig6581.h @@ -0,0 +1,112 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 FILTERMODELCONFIG6581_H +#define FILTERMODELCONFIG6581_H + +#include "FilterModelConfig.h" + +#include + +#include "Dac.h" + +#include "sidcxx14.h" + +namespace reSIDfp +{ + +class Integrator6581; + +/** + * Calculate parameters for 6581 filter emulation. + */ +class FilterModelConfig6581 final : public FilterModelConfig +{ +private: + static const unsigned int DAC_BITS = 11; + +private: + static std::unique_ptr instance; + // This allows access to the private constructor +#ifdef HAVE_CXX11 + friend std::unique_ptr::deleter_type; +#else + friend class std::auto_ptr; +#endif + + /// Transistor parameters. + //@{ + const double WL_vcr; ///< W/L for VCR + const double WL_snake; ///< W/L for "snake" + //@} + + /// DAC parameters. + //@{ + const double dac_zero; + const double dac_scale; + //@} + + /// DAC lookup table + Dac dac; + + /// VCR - 6581 only. + //@{ + unsigned short vcr_nVg[1 << 16]; + unsigned short vcr_n_Ids_term[1 << 16]; + //@} + +private: + double getDacZero(double adjustment) const { return dac_zero + (1. - adjustment); } + + FilterModelConfig6581(); + ~FilterModelConfig6581() DEFAULT; + +public: + static FilterModelConfig6581* getInstance(); + + /** + * Construct an 11 bit cutoff frequency DAC output voltage table. + * Ownership is transferred to the requester which becomes responsible + * of freeing the object when done. + * + * @param adjustment + * @return the DAC table + */ + unsigned short* getDAC(double adjustment) const; + + /** + * Construct an integrator solver. + * + * @return the integrator + */ + std::unique_ptr buildIntegrator(); + + inline unsigned short getVcr_nVg(int i) const { return vcr_nVg[i]; } + inline unsigned short getVcr_n_Ids_term(int i) const { return vcr_n_Ids_term[i]; } + // only used if SLOPE_FACTOR is defined + inline double getUt() const { return Ut; } + inline double getN16() const { return N16; } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig8580.cpp b/src/engine/platform/sound/c64_fp/FilterModelConfig8580.cpp new file mode 100644 index 00000000..fd2a16fa --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig8580.cpp @@ -0,0 +1,222 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2010 Dag Lem + * + * 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 "FilterModelConfig8580.h" + +#include "Integrator8580.h" +#include "OpAmp.h" + +namespace reSIDfp +{ + +/* + * R1 = 15.3*Ri + * R2 = 7.3*Ri + * R3 = 4.7*Ri + * Rf = 1.4*Ri + * R4 = 1.4*Ri + * R8 = 2.0*Ri + * RC = 2.8*Ri + * + * res feedback input + * --- -------- ----- + * 0 Rf Ri + * 1 Rf|R1 Ri + * 2 Rf|R2 Ri + * 3 Rf|R3 Ri + * 4 Rf R4 + * 5 Rf|R1 R4 + * 6 Rf|R2 R4 + * 7 Rf|R3 R4 + * 8 Rf R8 + * 9 Rf|R1 R8 + * A Rf|R2 R8 + * B Rf|R3 R8 + * C Rf RC + * D Rf|R1 RC + * E Rf|R2 RC + * F Rf|R3 RC + */ +const double resGain[16] = +{ + 1.4/1.0, // Rf/Ri 1.4 + ((1.4*15.3)/(1.4+15.3))/1.0, // (Rf|R1)/Ri 1.28263 + ((1.4*7.3)/(1.4+7.3))/1.0, // (Rf|R2)/Ri 1.17471 + ((1.4*4.7)/(1.4+4.7))/1.0, // (Rf|R3)/Ri 1.07869 + 1.4/1.4, // Rf/R4 1 + ((1.4*15.3)/(1.4+15.3))/1.4, // (Rf|R1)/R4 0.916168 + ((1.4*7.3)/(1.4+7.3))/1.4, // (Rf|R2)/R4 0.83908 + ((1.4*4.7)/(1.4+4.7))/1.4, // (Rf|R3)/R4 0.770492 + 1.4/2.0, // Rf/R8 0.7 + ((1.4*15.3)/(1.4+15.3))/2.0, // (Rf|R1)/R8 0.641317 + ((1.4*7.3)/(1.4+7.3))/2.0, // (Rf|R2)/R8 0.587356 + ((1.4*4.7)/(1.4+4.7))/2.0, // (Rf|R3)/R8 0.539344 + 1.4/2.8, // Rf/RC 0.5 + ((1.4*15.3)/(1.4+15.3))/2.8, // (Rf|R1)/RC 0.458084 + ((1.4*7.3)/(1.4+7.3))/2.8, // (Rf|R2)/RC 0.41954 + ((1.4*4.7)/(1.4+4.7))/2.8, // (Rf|R3)/RC 0.385246 +}; + +const unsigned int OPAMP_SIZE = 21; + +/** + * This is the SID 8580 op-amp voltage transfer function, measured on + * CAP1B/CAP1A on a chip marked CSG 8580R5 1690 25. + */ +const Spline::Point opamp_voltage[OPAMP_SIZE] = +{ + { 1.30, 8.91 }, // Approximate start of actual range + { 4.76, 8.91 }, + { 4.77, 8.90 }, + { 4.78, 8.88 }, + { 4.785, 8.86 }, + { 4.79, 8.80 }, + { 4.795, 8.60 }, + { 4.80, 8.25 }, + { 4.805, 7.50 }, + { 4.81, 6.10 }, + { 4.815, 4.05 }, // Change of curvature + { 4.82, 2.27 }, + { 4.825, 1.65 }, + { 4.83, 1.55 }, + { 4.84, 1.47 }, + { 4.85, 1.43 }, + { 4.87, 1.37 }, + { 4.90, 1.34 }, + { 5.00, 1.30 }, + { 5.10, 1.30 }, + { 8.91, 1.30 }, // Approximate end of actual range +}; + +std::unique_ptr FilterModelConfig8580::instance(nullptr); + +FilterModelConfig8580* FilterModelConfig8580::getInstance() +{ + if (!instance.get()) + { + instance.reset(new FilterModelConfig8580()); + } + + return instance.get(); +} + +FilterModelConfig8580::FilterModelConfig8580() : + FilterModelConfig( + 0.25, // voice voltage range FIXME measure + 4.80, // voice DC voltage FIXME was 4.76 + 22e-9, // capacitor value + 9.09, // Vdd + 0.80, // Vth + 100e-6, // uCox + opamp_voltage, + OPAMP_SIZE + ) +{ + // Create lookup tables for gains / summers. + + OpAmp opampModel(std::vector(std::begin(opamp_voltage), std::end(opamp_voltage)), Vddt); + + // The filter summer operates at n ~ 1, and has 5 fundamentally different + // input configurations (2 - 6 input "resistors"). + // + // Note that all "on" transistors are modeled as one. This is not + // entirely accurate, since the input for each transistor is different, + // and transistors are not linear components. However modeling all + // transistors separately would be extremely costly. + for (int i = 0; i < 5; i++) + { + const int idiv = 2 + i; // 2 - 6 input "resistors". + const int size = idiv << 16; + const double n = idiv; + opampModel.reset(); + summer[i] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16 / idiv; /* vmin .. vmax */ + summer[i][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // The audio mixer operates at n ~ 8/5, and has 8 fundamentally different + // input configurations (0 - 7 input "resistors"). + // + // All "on", transistors are modeled as one - see comments above for + // the filter summer. + for (int i = 0; i < 8; i++) + { + const int idiv = (i == 0) ? 1 : i; + const int size = (i == 0) ? 1 : i << 16; + const double n = i * 8.0 / 5.0; + opampModel.reset(); + mixer[i] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16 / idiv; /* vmin .. vmax */ + mixer[i][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // 4 bit "resistor" ladders in the audio output gain + // necessitate 16 gain tables. + // From die photographs of the volume "resistor" ladders + // it follows that gain ~ vol/16 (assuming ideal op-amps + for (int n8 = 0; n8 < 16; n8++) + { + const int size = 1 << 16; + const double n = n8 / 16.0; + opampModel.reset(); + gain_vol[n8] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16; /* vmin .. vmax */ + gain_vol[n8][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // 4 bit "resistor" ladders in the bandpass resonance gain + // necessitate 16 gain tables. + // From die photographs of the bandpass and volume "resistor" ladders + // it follows that 1/Q ~ 2^((4 - res)/8) (assuming ideal + // op-amps and ideal "resistors"). + for (int n8 = 0; n8 < 16; n8++) + { + const int size = 1 << 16; + opampModel.reset(); + gain_res[n8] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16; /* vmin .. vmax */ + gain_res[n8][vi] = getNormalizedValue(opampModel.solve(resGain[n8], vin)); + } + } +} + +std::unique_ptr FilterModelConfig8580::buildIntegrator() +{ + return MAKE_UNIQUE(Integrator8580, this); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig8580.h b/src/engine/platform/sound/c64_fp/FilterModelConfig8580.h new file mode 100644 index 00000000..ee2b4008 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig8580.h @@ -0,0 +1,68 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 FILTERMODELCONFIG8580_H +#define FILTERMODELCONFIG8580_H + +#include "FilterModelConfig.h" + +#include + +#include "sidcxx14.h" + +namespace reSIDfp +{ + +class Integrator8580; + +/** + * Calculate parameters for 8580 filter emulation. + */ +class FilterModelConfig8580 final : public FilterModelConfig +{ +private: + static std::unique_ptr instance; + // This allows access to the private constructor +#ifdef HAVE_CXX11 + friend std::unique_ptr::deleter_type; +#else + friend class std::auto_ptr; +#endif + +private: + FilterModelConfig8580(); + ~FilterModelConfig8580() DEFAULT; + +public: + static FilterModelConfig8580* getInstance(); + + /** + * Construct an integrator solver. + * + * @return the integrator + */ + std::unique_ptr buildIntegrator(); +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/Integrator6581.cpp b/src/engine/platform/sound/c64_fp/Integrator6581.cpp new file mode 100644 index 00000000..490be9b5 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Integrator6581.cpp @@ -0,0 +1,25 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2014 Leandro Nini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define INTEGRATOR_CPP + +#include "Integrator6581.h" + +// This is needed when compiling with --disable-inline diff --git a/src/engine/platform/sound/c64_fp/Integrator6581.h b/src/engine/platform/sound/c64_fp/Integrator6581.h new file mode 100644 index 00000000..99ac3bea --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Integrator6581.h @@ -0,0 +1,285 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004, 2010 Dag Lem + * + * 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 INTEGRATOR6581_H +#define INTEGRATOR6581_H + +#include "FilterModelConfig6581.h" + +#include +#include + +// uncomment to enable use of the slope factor +// in the EKV model +// actually produces worse results, needs investigation +//#define SLOPE_FACTOR + +#ifdef SLOPE_FACTOR +# include +#endif + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * Find output voltage in inverting integrator SID op-amp circuits, using a + * single fixpoint iteration step. + * + * A circuit diagram of a MOS 6581 integrator is shown below. + * + * +---C---+ + * | | + * vi --o--Rw--o-o--[A>--o-- vo + * | | vx + * +--Rs--+ + * + * From Kirchoff's current law it follows that + * + * IRw + IRs + ICr = 0 + * + * Using the formula for current through a capacitor, i = C*dv/dt, we get + * + * IRw + IRs + C*(vc - vc0)/dt = 0 + * dt/C*(IRw + IRs) + vc - vc0 = 0 + * vc = vc0 - n*(IRw(vi,vx) + IRs(vi,vx)) + * + * which may be rewritten as the following iterative fixpoint function: + * + * vc = vc0 - n*(IRw(vi,g(vc)) + IRs(vi,g(vc))) + * + * To accurately calculate the currents through Rs and Rw, we need to use + * transistor models. Rs has a gate voltage of Vdd = 12V, and can be + * assumed to always be in triode mode. For Rw, the situation is rather + * more complex, as it turns out that this transistor will operate in + * both subthreshold, triode, and saturation modes. + * + * The Shichman-Hodges transistor model routinely used in textbooks may + * be written as follows: + * + * Ids = 0 , Vgst < 0 (subthreshold mode) + * Ids = K*W/L*(2*Vgst - Vds)*Vds , Vgst >= 0, Vds < Vgst (triode mode) + * Ids = K*W/L*Vgst^2 , Vgst >= 0, Vds >= Vgst (saturation mode) + * + * where + * K = u*Cox/2 (transconductance coefficient) + * W/L = ratio between substrate width and length + * Vgst = Vg - Vs - Vt (overdrive voltage) + * + * This transistor model is also called the quadratic model. + * + * Note that the equation for the triode mode can be reformulated as + * independent terms depending on Vgs and Vgd, respectively, by the + * following substitution: + * + * Vds = Vgst - (Vgst - Vds) = Vgst - Vgdt + * + * Ids = K*W/L*(2*Vgst - Vds)*Vds + * = K*W/L*(2*Vgst - (Vgst - Vgdt)*(Vgst - Vgdt) + * = K*W/L*(Vgst + Vgdt)*(Vgst - Vgdt) + * = K*W/L*(Vgst^2 - Vgdt^2) + * + * This turns out to be a general equation which covers both the triode + * and saturation modes (where the second term is 0 in saturation mode). + * The equation is also symmetrical, i.e. it can calculate negative + * currents without any change of parameters (since the terms for drain + * and source are identical except for the sign). + * + * FIXME: Subthreshold as function of Vgs, Vgd. + * + * Ids = I0*W/L*e^(Vgst/(Ut/k)) , Vgst < 0 (subthreshold mode) + * + * where + * I0 = (2 * uCox * Ut^2) / k + * + * The remaining problem with the textbook model is that the transition + * from subthreshold the triode/saturation is not continuous. + * + * Realizing that the subthreshold and triode/saturation modes may both + * be defined by independent (and equal) terms of Vgs and Vds, + * respectively, the corresponding terms can be blended into (equal) + * continuous functions suitable for table lookup. + * + * The EKV model (Enz, Krummenacher and Vittoz) essentially performs this + * blending using an elegant mathematical formulation: + * + * Ids = Is * (if - ir) + * Is = ((2 * u*Cox * Ut^2)/k) * W/L + * if = ln^2(1 + e^((k*(Vg - Vt) - Vs)/(2*Ut)) + * ir = ln^2(1 + e^((k*(Vg - Vt) - Vd)/(2*Ut)) + * + * For our purposes, the EKV model preserves two important properties + * discussed above: + * + * - It consists of two independent terms, which can be represented by + * the same lookup table. + * - It is symmetrical, i.e. it calculates current in both directions, + * facilitating a branch-free implementation. + * + * Rw in the circuit diagram above is a VCR (voltage controlled resistor), + * as shown in the circuit diagram below. + * + * + * Vdd + * | + * Vdd _|_ + * | +---+ +---- Vw + * _|_ | + * +--+ +---o Vg + * | __|__ + * | ----- Rw + * | | | + * vi -----o------+ +-------- vo + * + * + * In order to calculalate the current through the VCR, its gate voltage + * must be determined. + * + * Assuming triode mode and applying Kirchoff's current law, we get the + * following equation for Vg: + * + * u*Cox/2*W/L*((nVddt - Vg)^2 - (nVddt - vi)^2 + (nVddt - Vg)^2 - (nVddt - Vw)^2) = 0 + * 2*(nVddt - Vg)^2 - (nVddt - vi)^2 - (nVddt - Vw)^2 = 0 + * (nVddt - Vg) = sqrt(((nVddt - vi)^2 + (nVddt - Vw)^2)/2) + * + * Vg = nVddt - sqrt(((nVddt - vi)^2 + (nVddt - Vw)^2)/2) + */ +class Integrator6581 +{ +private: + unsigned int nVddt_Vw_2; + mutable int vx; + mutable int vc; + +#ifdef SLOPE_FACTOR + // Slope factor n = 1/k + // where k is the gate coupling coefficient + // k = Cox/(Cox+Cdep) ~ 0.7 (depends on gate voltage) + mutable double n; +#endif + const unsigned short nVddt; + const unsigned short nVt; + const unsigned short nVmin; + const unsigned short nSnake; + + const FilterModelConfig6581* fmc; + +public: + Integrator6581(const FilterModelConfig6581* fmc, + double WL_snake) : + nVddt_Vw_2(0), + vx(0), + vc(0), +#ifdef SLOPE_FACTOR + n(1.4), +#endif + nVddt(fmc->getNormalizedValue(fmc->getVddt())), + nVt(fmc->getNormalizedValue(fmc->getVth())), + nVmin(fmc->getNVmin()), + nSnake(fmc->getNormalizedCurrentFactor(WL_snake)), + fmc(fmc) {} + + void setVw(unsigned short Vw) { nVddt_Vw_2 = ((nVddt - Vw) * (nVddt - Vw)) >> 1; } + + int solve(int vi) const; +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(INTEGRATOR_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +int Integrator6581::solve(int vi) const +{ + // Make sure Vgst>0 so we're not in subthreshold mode + assert(vx < nVddt); + + // Check that transistor is actually in triode mode + // Vds < Vgs - Vth + assert(vi < nVddt); + + // "Snake" voltages for triode mode calculation. + const unsigned int Vgst = nVddt - vx; + const unsigned int Vgdt = nVddt - vi; + + const unsigned int Vgst_2 = Vgst * Vgst; + const unsigned int Vgdt_2 = Vgdt * Vgdt; + + // "Snake" current, scaled by (1/m)*2^13*m*2^16*m*2^16*2^-15 = m*2^30 + const int n_I_snake = nSnake * (static_cast(Vgst_2 - Vgdt_2) >> 15); + + // VCR gate voltage. // Scaled by m*2^16 + // Vg = Vddt - sqrt(((Vddt - Vw)^2 + Vgdt^2)/2) + const int nVg = static_cast(fmc->getVcr_nVg((nVddt_Vw_2 + (Vgdt_2 >> 1)) >> 16)); +#ifdef SLOPE_FACTOR + const double nVp = static_cast(nVg - nVt) / n; // Pinch-off voltage + const int kVg = static_cast(nVp + 0.5) - nVmin; +#else + const int kVg = (nVg - nVt) - nVmin; +#endif + + // VCR voltages for EKV model table lookup. + const int kVgt_Vs = (vx < kVg) ? kVg - vx : 0; + assert(kVgt_Vs < (1 << 16)); + const int kVgt_Vd = (vi < kVg) ? kVg - vi : 0; + assert(kVgt_Vd < (1 << 16)); + + // VCR current, scaled by m*2^15*2^15 = m*2^30 + const unsigned int If = static_cast(fmc->getVcr_n_Ids_term(kVgt_Vs)) << 15; + const unsigned int Ir = static_cast(fmc->getVcr_n_Ids_term(kVgt_Vd)) << 15; +#ifdef SLOPE_FACTOR + const double iVcr = static_cast(If - Ir); + const int n_I_vcr = static_cast((iVcr * n) + 0.5); +#else + const int n_I_vcr = If - Ir; +#endif + +#ifdef SLOPE_FACTOR + // estimate new slope factor based on gate voltage + const double gamma = 1.0; // body effect factor + const double phi = 0.8; // bulk Fermi potential + const double Vp = nVp / fmc->getN16(); + n = 1. + (gamma / (2. * sqrt(Vp + phi + 4. * fmc->getUt()))); + assert((n > 1.2) && (n < 1.8)); +#endif + + // Change in capacitor charge. + vc += n_I_snake + n_I_vcr; + + // vx = g(vc) + const int tmp = (vc >> 15) + (1 << 15); + assert(tmp < (1 << 16)); + vx = fmc->getOpampRev(tmp); + + // Return vo. + return vx - (vc >> 14); +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/Integrator8580.cpp b/src/engine/platform/sound/c64_fp/Integrator8580.cpp new file mode 100644 index 00000000..6fba9521 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Integrator8580.cpp @@ -0,0 +1,25 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2014-2016 Leandro Nini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define INTEGRATOR8580_CPP + +#include "Integrator8580.h" + +// This is needed when compiling with --disable-inline diff --git a/src/engine/platform/sound/c64_fp/Integrator8580.h b/src/engine/platform/sound/c64_fp/Integrator8580.h new file mode 100644 index 00000000..7137e940 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Integrator8580.h @@ -0,0 +1,142 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004, 2010 Dag Lem + * + * 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 INTEGRATOR8580_H +#define INTEGRATOR8580_H + +#include "FilterModelConfig8580.h" + +#include +#include + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * 8580 integrator + * + * +---C---+ + * | | + * vi -----Rfc---o--[A>--o-- vo + * vx + * + * IRfc + ICr = 0 + * IRfc + C*(vc - vc0)/dt = 0 + * dt/C*(IRfc) + vc - vc0 = 0 + * vc = vc0 - n*(IRfc(vi,vx)) + * vc = vc0 - n*(IRfc(vi,g(vc))) + * + * IRfc = K*W/L*(Vgst^2 - Vgdt^2) = n*((Vddt - vx)^2 - (Vddt - vi)^2) + * + * Rfc gate voltage is generated by an OP Amp and depends on chip temperature. + */ +class Integrator8580 +{ +private: + mutable int vx; + mutable int vc; + + unsigned short nVgt; + unsigned short n_dac; + + const FilterModelConfig8580* fmc; + +public: + Integrator8580(const FilterModelConfig8580* fmc) : + vx(0), + vc(0), + fmc(fmc) + { + setV(1.5); + } + + /** + * Set Filter Cutoff resistor ratio. + */ + void setFc(double wl) + { + // Normalized current factor, 1 cycle at 1MHz. + // Fit in 5 bits. + n_dac = fmc->getNormalizedCurrentFactor(wl); + } + + /** + * Set FC gate voltage multiplier. + */ + void setV(double v) + { + // Gate voltage is controlled by the switched capacitor voltage divider + // Ua = Ue * v = 4.76v 1 1.0 && v < 2.0); + const double Vg = fmc->getVoiceDCVoltage() * v; + const double Vgt = Vg - fmc->getVth(); + + // Vg - Vth, normalized so that translated values can be subtracted: + // Vgt - x = (Vgt - t) - (x - t) + nVgt = fmc->getNormalizedValue(Vgt); + } + + int solve(int vi) const; +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(INTEGRATOR8580_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +int Integrator8580::solve(int vi) const +{ + // Make sure we're not in subthreshold mode + assert(vx < nVgt); + + // DAC voltages + const unsigned int Vgst = nVgt - vx; + const unsigned int Vgdt = (vi < nVgt) ? nVgt - vi : 0; // triode/saturation mode + + const unsigned int Vgst_2 = Vgst * Vgst; + const unsigned int Vgdt_2 = Vgdt * Vgdt; + + // DAC current, scaled by (1/m)*2^13*m*2^16*m*2^16*2^-15 = m*2^30 + const int n_I_dac = n_dac * (static_cast(Vgst_2 - Vgdt_2) >> 15); + + // Change in capacitor charge. + vc += n_I_dac; + + // vx = g(vc) + const int tmp = (vc >> 15) + (1 << 15); + assert(tmp < (1 << 16)); + vx = fmc->getOpampRev(tmp); + + // Return vo. + return vx - (vc >> 14); +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/OpAmp.cpp b/src/engine/platform/sound/c64_fp/OpAmp.cpp new file mode 100644 index 00000000..b26b2efc --- /dev/null +++ b/src/engine/platform/sound/c64_fp/OpAmp.cpp @@ -0,0 +1,84 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 "OpAmp.h" + +#include + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +const double EPSILON = 1e-8; + +double OpAmp::solve(double n, double vi) const +{ + // Start off with an estimate of x and a root bracket [ak, bk]. + // f is decreasing, so that f(ak) > 0 and f(bk) < 0. + double ak = vmin; + double bk = vmax; + + const double a = n + 1.; + const double b = Vddt; + const double b_vi = (b > vi) ? (b - vi) : 0.; + const double c = n * (b_vi * b_vi); + + for (;;) + { + const double xk = x; + + // Calculate f and df. + + Spline::Point out = opamp->evaluate(x); + const double vo = out.x; + const double dvo = out.y; + + const double b_vx = (b > x) ? b - x : 0.; + const double b_vo = (b > vo) ? b - vo : 0.; + + // f = a*(b - vx)^2 - c - (b - vo)^2 + const double f = a * (b_vx * b_vx) - c - (b_vo * b_vo); + + // df = 2*((b - vo)*dvo - a*(b - vx)) + const double df = 2. * (b_vo * dvo - a * b_vx); + + // Newton-Raphson step: xk1 = xk - f(xk)/f'(xk) + x -= f / df; + + if (unlikely(fabs(x - xk) < EPSILON)) + { + out = opamp->evaluate(x); + return out.x; + } + + // Narrow down root bracket. + (f < 0. ? bk : ak) = xk; + + if (unlikely(x <= ak) || unlikely(x >= bk)) + { + // Bisection step (ala Dekker's method). + x = (ak + bk) * 0.5; + } + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/OpAmp.h b/src/engine/platform/sound/c64_fp/OpAmp.h new file mode 100644 index 00000000..9d2c8f16 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/OpAmp.h @@ -0,0 +1,113 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 OPAMP_H +#define OPAMP_H + +#include +#include + +#include "Spline.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +/** + * Find output voltage in inverting gain and inverting summer SID op-amp + * circuits, using a combination of Newton-Raphson and bisection. + * + * +---R2--+ + * | | + * vi ---R1--o--[A>--o-- vo + * vx + * + * From Kirchoff's current law it follows that + * + * IR1f + IR2r = 0 + * + * Substituting the triode mode transistor model K*W/L*(Vgst^2 - Vgdt^2) + * for the currents, we get: + * + * n*((Vddt - vx)^2 - (Vddt - vi)^2) + (Vddt - vx)^2 - (Vddt - vo)^2 = 0 + * + * Our root function f can thus be written as: + * + * f = (n + 1)*(Vddt - vx)^2 - n*(Vddt - vi)^2 - (Vddt - vo)^2 = 0 + * + * Using substitution constants + * + * a = n + 1 + * b = Vddt + * c = n*(Vddt - vi)^2 + * + * the equations for the root function and its derivative can be written as: + * + * f = a*(b - vx)^2 - c - (b - vo)^2 + * df = 2*((b - vo)*dvo - a*(b - vx)) + */ +class OpAmp +{ +private: + /// Current root position (cached as guess to speed up next iteration) + mutable double x; + + const double Vddt; + const double vmin; + const double vmax; + + std::unique_ptr const opamp; + +public: + /** + * Opamp input -> output voltage conversion + * + * @param opamp opamp mapping table as pairs of points (in -> out) + * @param opamplength length of the opamp array + * @param kVddt transistor dt parameter (in volts) + */ + OpAmp(const std::vector &opamp, double Vddt) : + x(0.), + Vddt(Vddt), + vmin(opamp.front().x), + vmax(opamp.back().x), + opamp(new Spline(opamp)) {} + + void reset() const + { + x = vmin; + } + + /** + * Solve the opamp equation for input vi in loading context n + * + * @param n the ratio of input/output loading + * @param vi input + * @return vo + */ + double solve(double n, double vi) const; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/Potentiometer.h b/src/engine/platform/sound/c64_fp/Potentiometer.h new file mode 100644 index 00000000..8b63df13 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Potentiometer.h @@ -0,0 +1,50 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright (C) 2004 Dag Lem + * + * 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 POTENTIOMETER_H +#define POTENTIOMETER_H + +namespace reSIDfp +{ + +/** + * Potentiometer representation. + * + * This class will probably never be implemented in any real way. + * + * @author Ken Händel + * @author Dag Lem + */ +class Potentiometer +{ +public: + /** + * Read paddle value. Not modeled. + * + * @return paddle value (always 0xff) + */ + unsigned char readPOT() const { return 0xff; } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/README b/src/engine/platform/sound/c64_fp/README new file mode 100644 index 00000000..45d4bfb9 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/README @@ -0,0 +1,20 @@ +reSIDfp is a fork of Dag Lem's reSID 0.16, a reverse engineered software emulation +of the MOS6581/8580 SID (Sound Interface Device). + +The project was started by Antti S. Lankila in order to improve SID emulation +with special focus on the 6581 filter. +The codebase has been later on ported to java by Ken Händel within the jsidplay2 project +and has seen further work by Antti Lankila. +It was then ported back to c++ and integrated with improvements from reSID 1.0 by Leandro Nini. + + +Main differences from reSID: + +* combined waveforms are emulated by a parametrized model based on samplings from Kevtris; +* envelope generator is implemented like in the real machine with a shift register; +* high quality resampling is done in two steps to allow computational savings using lower order filters; +* part of the calculations are done with floats instead of fixed point; +* interpolation is accomplished with Fritsch-Carlson method to preserve monotonicity. + + +reSIDfp is free software. See the file COPYING for copying permission. diff --git a/src/engine/platform/sound/c64_fp/SID.cpp b/src/engine/platform/sound/c64_fp/SID.cpp new file mode 100644 index 00000000..a996d223 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/SID.cpp @@ -0,0 +1,504 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define SID_CPP + +#include "SID.h" + +#include + +#include "array.h" +#include "Dac.h" +#include "Filter6581.h" +#include "Filter8580.h" +#include "Potentiometer.h" +#include "WaveformCalculator.h" +#include "resample/TwoPassSincResampler.h" +#include "resample/ZeroOrderResampler.h" + +namespace reSIDfp +{ + +const unsigned int ENV_DAC_BITS = 8; +const unsigned int OSC_DAC_BITS = 12; + +/** + * The waveform D/A converter introduces a DC offset in the signal + * to the envelope multiplying D/A converter. The "zero" level of + * the waveform D/A converter can be found as follows: + * + * Measure the "zero" voltage of voice 3 on the SID audio output + * pin, routing only voice 3 to the mixer ($d417 = $0b, $d418 = + * $0f, all other registers zeroed). + * + * Then set the sustain level for voice 3 to maximum and search for + * the waveform output value yielding the same voltage as found + * above. This is done by trying out different waveform output + * values until the correct value is found, e.g. with the following + * program: + * + * lda #$08 + * sta $d412 + * lda #$0b + * sta $d417 + * lda #$0f + * sta $d418 + * lda #$f0 + * sta $d414 + * lda #$21 + * sta $d412 + * lda #$01 + * sta $d40e + * + * ldx #$00 + * lda #$38 ; Tweak this to find the "zero" level + *l cmp $d41b + * bne l + * stx $d40e ; Stop frequency counter - freeze waveform output + * brk + * + * The waveform output range is 0x000 to 0xfff, so the "zero" + * level should ideally have been 0x800. In the measured chip, the + * waveform output "zero" level was found to be 0x380 (i.e. $d41b + * = 0x38) at an audio output voltage of 5.94V. + * + * With knowledge of the mixer op-amp characteristics, further estimates + * of waveform voltages can be obtained by sampling the EXT IN pin. + * From EXT IN samples, the corresponding waveform output can be found by + * using the model for the mixer. + * + * Such measurements have been done on a chip marked MOS 6581R4AR + * 0687 14, and the following results have been obtained: + * * The full range of one voice is approximately 1.5V. + * * The "zero" level rides at approximately 5.0V. + * + * + * zero-x did the measuring on the 8580 (https://sourceforge.net/p/vice-emu/bugs/1036/#c5b3): + * When it sits on basic from powerup it's at 4.72 + * Run 1.prg and check the output pin level. + * Then run 2.prg andadjust it until the output level is the same... + * 0x94-0xA8 gives me the same 4.72 1.prg shows. + * On another 8580 it's 0x90-0x9C + * Third chip 0x94-0xA8 + * Fourth chip 0x90-0xA4 + * On the 8580 that plays digis the output is 4.66 and 0x93 is the only value to reach that. + * To me that seems as regular 8580s have somewhat wide 0-level range, + * whereas that digi-compatible 8580 has it very narrow. + * On my 6581R4AR has 0x3A as the only value giving the same output level as 1.prg + */ +//@{ +unsigned int constexpr OFFSET_6581 = 0x380; +unsigned int constexpr OFFSET_8580 = 0x9c0; +//@} + +/** + * Bus value stays alive for some time after each operation. + * Values differs between chip models, the timings used here + * are taken from VICE [1]. + * See also the discussion "How do I reliably detect 6581/8580 sid?" on CSDb [2]. + * + * Results from real C64 (testprogs/SID/bitfade/delayfrq0.prg): + * + * (new SID) (250469/8580R5) (250469/8580R5) + * delayfrq0 ~7a000 ~108000 + * + * (old SID) (250407/6581) + * delayfrq0 ~01d00 + * + * [1]: http://sourceforge.net/p/vice-emu/patches/99/ + * [2]: http://noname.c64.org/csdb/forums/?roomid=11&topicid=29025&showallposts=1 + */ +//@{ +int constexpr BUS_TTL_6581 = 0x01d00; +int constexpr BUS_TTL_8580 = 0xa2000; +//@} + +SID::SID() : + filter6581(new Filter6581()), + filter8580(new Filter8580()), + externalFilter(new ExternalFilter()), + resampler(nullptr), + potX(new Potentiometer()), + potY(new Potentiometer()) +{ + voice[0].reset(new Voice()); + voice[1].reset(new Voice()); + voice[2].reset(new Voice()); + + muted[0] = muted[1] = muted[2] = false; + + reset(); + setChipModel(MOS8580); +} + +SID::~SID() +{ + // Needed to delete auto_ptr with complete type +} + +void SID::setFilter6581Curve(double filterCurve) +{ + filter6581->setFilterCurve(filterCurve); +} + +void SID::setFilter8580Curve(double filterCurve) +{ + filter8580->setFilterCurve(filterCurve); +} + +void SID::enableFilter(bool enable) +{ + filter6581->enable(enable); + filter8580->enable(enable); +} + +void SID::voiceSync(bool sync) +{ + if (sync) + { + // Synchronize the 3 waveform generators. + for (int i = 0; i < 3; i++) + { + voice[i]->wave()->synchronize(voice[(i + 1) % 3]->wave(), voice[(i + 2) % 3]->wave()); + } + } + + // Calculate the time to next voice sync + nextVoiceSync = std::numeric_limits::max(); + + for (int i = 0; i < 3; i++) + { + WaveformGenerator* const wave = voice[i]->wave(); + const unsigned int freq = wave->readFreq(); + + if (wave->readTest() || freq == 0 || !voice[(i + 1) % 3]->wave()->readSync()) + { + continue; + } + + const unsigned int accumulator = wave->readAccumulator(); + const unsigned int thisVoiceSync = ((0x7fffff - accumulator) & 0xffffff) / freq + 1; + + if (thisVoiceSync < nextVoiceSync) + { + nextVoiceSync = thisVoiceSync; + } + } +} + +void SID::setChipModel(ChipModel model) +{ + switch (model) + { + case MOS6581: + filter = filter6581.get(); + modelTTL = BUS_TTL_6581; + break; + + case MOS8580: + filter = filter8580.get(); + modelTTL = BUS_TTL_8580; + break; + + default: + throw SIDError("Unknown chip type"); + } + + this->model = model; + + // calculate waveform-related tables + matrix_t* tables = WaveformCalculator::getInstance()->buildTable(model); + + // calculate envelope DAC table + { + Dac dacBuilder(ENV_DAC_BITS); + dacBuilder.kinkedDac(model); + + for (unsigned int i = 0; i < (1 << ENV_DAC_BITS); i++) + { + envDAC[i] = static_cast(dacBuilder.getOutput(i)); + } + } + + // calculate oscillator DAC table + const bool is6581 = model == MOS6581; + + { + Dac dacBuilder(OSC_DAC_BITS); + dacBuilder.kinkedDac(model); + + const double offset = dacBuilder.getOutput(is6581 ? OFFSET_6581 : OFFSET_8580); + + for (unsigned int i = 0; i < (1 << OSC_DAC_BITS); i++) + { + const double dacValue = dacBuilder.getOutput(i); + oscDAC[i] = static_cast(dacValue - offset); + } + } + + // set voice tables + for (int i = 0; i < 3; i++) + { + voice[i]->setEnvDAC(envDAC); + voice[i]->setWavDAC(oscDAC); + voice[i]->wave()->setModel(is6581); + voice[i]->wave()->setWaveformModels(tables); + } +} + +void SID::reset() +{ + for (int i = 0; i < 3; i++) + { + voice[i]->reset(); + } + + filter6581->reset(); + filter8580->reset(); + externalFilter->reset(); + + if (resampler.get()) + { + resampler->reset(); + } + + busValue = 0; + busValueTtl = 0; + voiceSync(false); +} + +void SID::input(int value) +{ + filter6581->input(value); + filter8580->input(value); +} + +unsigned char SID::read(int offset) +{ + switch (offset) + { + case 0x19: // X value of paddle + busValue = potX->readPOT(); + busValueTtl = modelTTL; + break; + + case 0x1a: // Y value of paddle + busValue = potY->readPOT(); + busValueTtl = modelTTL; + break; + + case 0x1b: // Voice #3 waveform output + busValue = voice[2]->wave()->readOSC(); + busValueTtl = modelTTL; + break; + + case 0x1c: // Voice #3 ADSR output + busValue = voice[2]->envelope()->readENV(); + busValueTtl = modelTTL; + break; + + default: + // Reading from a write-only or non-existing register + // makes the bus discharge faster. + // Emulate this by halving the residual TTL. + busValueTtl /= 2; + break; + } + + return busValue; +} + +void SID::write(int offset, unsigned char value) +{ + busValue = value; + busValueTtl = modelTTL; + + switch (offset) + { + case 0x00: // Voice #1 frequency (Low-byte) + voice[0]->wave()->writeFREQ_LO(value); + break; + + case 0x01: // Voice #1 frequency (High-byte) + voice[0]->wave()->writeFREQ_HI(value); + break; + + case 0x02: // Voice #1 pulse width (Low-byte) + voice[0]->wave()->writePW_LO(value); + break; + + case 0x03: // Voice #1 pulse width (bits #8-#15) + voice[0]->wave()->writePW_HI(value); + break; + + case 0x04: // Voice #1 control register + voice[0]->writeCONTROL_REG(muted[0] ? 0 : value); + break; + + case 0x05: // Voice #1 Attack and Decay length + voice[0]->envelope()->writeATTACK_DECAY(value); + break; + + case 0x06: // Voice #1 Sustain volume and Release length + voice[0]->envelope()->writeSUSTAIN_RELEASE(value); + break; + + case 0x07: // Voice #2 frequency (Low-byte) + voice[1]->wave()->writeFREQ_LO(value); + break; + + case 0x08: // Voice #2 frequency (High-byte) + voice[1]->wave()->writeFREQ_HI(value); + break; + + case 0x09: // Voice #2 pulse width (Low-byte) + voice[1]->wave()->writePW_LO(value); + break; + + case 0x0a: // Voice #2 pulse width (bits #8-#15) + voice[1]->wave()->writePW_HI(value); + break; + + case 0x0b: // Voice #2 control register + voice[1]->writeCONTROL_REG(muted[1] ? 0 : value); + break; + + case 0x0c: // Voice #2 Attack and Decay length + voice[1]->envelope()->writeATTACK_DECAY(value); + break; + + case 0x0d: // Voice #2 Sustain volume and Release length + voice[1]->envelope()->writeSUSTAIN_RELEASE(value); + break; + + case 0x0e: // Voice #3 frequency (Low-byte) + voice[2]->wave()->writeFREQ_LO(value); + break; + + case 0x0f: // Voice #3 frequency (High-byte) + voice[2]->wave()->writeFREQ_HI(value); + break; + + case 0x10: // Voice #3 pulse width (Low-byte) + voice[2]->wave()->writePW_LO(value); + break; + + case 0x11: // Voice #3 pulse width (bits #8-#15) + voice[2]->wave()->writePW_HI(value); + break; + + case 0x12: // Voice #3 control register + voice[2]->writeCONTROL_REG(muted[2] ? 0 : value); + break; + + case 0x13: // Voice #3 Attack and Decay length + voice[2]->envelope()->writeATTACK_DECAY(value); + break; + + case 0x14: // Voice #3 Sustain volume and Release length + voice[2]->envelope()->writeSUSTAIN_RELEASE(value); + break; + + case 0x15: // Filter cut off frequency (bits #0-#2) + filter6581->writeFC_LO(value); + filter8580->writeFC_LO(value); + break; + + case 0x16: // Filter cut off frequency (bits #3-#10) + filter6581->writeFC_HI(value); + filter8580->writeFC_HI(value); + break; + + case 0x17: // Filter control + filter6581->writeRES_FILT(value); + filter8580->writeRES_FILT(value); + break; + + case 0x18: // Volume and filter modes + filter6581->writeMODE_VOL(value); + filter8580->writeMODE_VOL(value); + break; + + default: + break; + } + + // Update voicesync just in case. + voiceSync(false); +} + +void SID::setSamplingParameters(double clockFrequency, SamplingMethod method, double samplingFrequency, double highestAccurateFrequency) +{ + externalFilter->setClockFrequency(clockFrequency); + + switch (method) + { + case DECIMATE: + resampler.reset(new ZeroOrderResampler(clockFrequency, samplingFrequency)); + break; + + case RESAMPLE: + resampler.reset(TwoPassSincResampler::create(clockFrequency, samplingFrequency, highestAccurateFrequency)); + break; + + default: + throw SIDError("Unknown sampling method"); + } +} + +void SID::clockSilent(unsigned int cycles) +{ + ageBusValue(cycles); + + while (cycles != 0) + { + int delta_t = std::min(nextVoiceSync, cycles); + + if (delta_t > 0) + { + for (int i = 0; i < delta_t; i++) + { + // clock waveform generators (can affect OSC3) + voice[0]->wave()->clock(); + voice[1]->wave()->clock(); + voice[2]->wave()->clock(); + + voice[0]->wave()->output(voice[2]->wave()); + voice[1]->wave()->output(voice[0]->wave()); + voice[2]->wave()->output(voice[1]->wave()); + + // clock ENV3 only + voice[2]->envelope()->clock(); + } + + cycles -= delta_t; + nextVoiceSync -= delta_t; + } + + if (nextVoiceSync == 0) + { + voiceSync(true); + } + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/SID.h b/src/engine/platform/sound/c64_fp/SID.h new file mode 100644 index 00000000..85b6a4e4 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/SID.h @@ -0,0 +1,378 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * 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 SIDFP_H +#define SIDFP_H + +#include + +#include "siddefs-fp.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +class Filter; +class Filter6581; +class Filter8580; +class ExternalFilter; +class Potentiometer; +class Voice; +class Resampler; + +/** + * SID error exception. + */ +class SIDError +{ +private: + const char* message; + +public: + SIDError(const char* msg) : + message(msg) {} + const char* getMessage() const { return message; } +}; + +/** + * MOS6581/MOS8580 emulation. + */ +class SID +{ +private: + /// Currently active filter + Filter* filter; + + /// Filter used, if model is set to 6581 + std::unique_ptr const filter6581; + + /// Filter used, if model is set to 8580 + std::unique_ptr const filter8580; + + /** + * External filter that provides high-pass and low-pass filtering + * to adjust sound tone slightly. + */ + std::unique_ptr const externalFilter; + + /// Resampler used by audio generation code. + std::unique_ptr resampler; + + /// Paddle X register support + std::unique_ptr const potX; + + /// Paddle Y register support + std::unique_ptr const potY; + + /// SID voices + std::unique_ptr voice[3]; + + /// Time to live for the last written value + int busValueTtl; + + /// Current chip model's bus value TTL + int modelTTL; + + /// Time until #voiceSync must be run. + unsigned int nextVoiceSync; + + /// Currently active chip model. + ChipModel model; + + /// Last written value + unsigned char busValue; + + /// Flags for muted channels + bool muted[3]; + + /** + * Emulated nonlinearity of the envelope DAC. + * + * @See Dac + */ + float envDAC[256]; + + /** + * Emulated nonlinearity of the oscillator DAC. + * + * @See Dac + */ + float oscDAC[4096]; + +private: + /** + * Age the bus value and zero it if it's TTL has expired. + * + * @param n the number of cycles + */ + void ageBusValue(unsigned int n); + + /** + * Get output sample. + * + * @return the output sample + */ + int output(); + + /** + * Calculate the numebr of cycles according to current parameters + * that it takes to reach sync. + * + * @param sync whether to do the actual voice synchronization + */ + void voiceSync(bool sync); + +public: + SID(); + ~SID(); + + int lastChanOut[3]; + + /** + * Set chip model. + * + * @param model chip model to use + * @throw SIDError + */ + void setChipModel(ChipModel model); + + /** + * Get currently emulated chip model. + */ + ChipModel getChipModel() const { return model; } + + /** + * SID reset. + */ + void reset(); + + /** + * 16-bit input (EXT IN). Write 16-bit sample to audio input. NB! The caller + * is responsible for keeping the value within 16 bits. Note that to mix in + * an external audio signal, the signal should be resampled to 1MHz first to + * avoid sampling noise. + * + * @param value input level to set + */ + void input(int value); + + /** + * Read registers. + * + * Reading a write only register returns the last char written to any SID register. + * The individual bits in this value start to fade down towards zero after a few cycles. + * All bits reach zero within approximately $2000 - $4000 cycles. + * It has been claimed that this fading happens in an orderly fashion, + * however sampling of write only registers reveals that this is not the case. + * NOTE: This is not correctly modeled. + * The actual use of write only registers has largely been made + * in the belief that all SID registers are readable. + * To support this belief the read would have to be done immediately + * after a write to the same register (remember that an intermediate write + * to another register would yield that value instead). + * With this in mind we return the last value written to any SID register + * for $2000 cycles without modeling the bit fading. + * + * @param offset SID register to read + * @return value read from chip + */ + unsigned char read(int offset); + + /** + * Write registers. + * + * @param offset chip register to write + * @param value value to write + */ + void write(int offset, unsigned char value); + + /** + * SID voice muting. + * + * @param channel channel to modify + * @param enable is muted? + */ + void mute(int channel, bool enable) { muted[channel] = enable; } + + /** + * Setting of SID sampling parameters. + * + * Use a clock freqency of 985248Hz for PAL C64, 1022730Hz for NTSC C64. + * The default end of passband frequency is pass_freq = 0.9*sample_freq/2 + * for sample frequencies up to ~ 44.1kHz, and 20kHz for higher sample frequencies. + * + * For resampling, the ratio between the clock frequency and the sample frequency + * is limited as follows: 125*clock_freq/sample_freq < 16384 + * E.g. provided a clock frequency of ~ 1MHz, the sample frequency can not be set + * lower than ~ 8kHz. A lower sample frequency would make the resampling code + * overfill its 16k sample ring buffer. + * + * The end of passband frequency is also limited: pass_freq <= 0.9*sample_freq/2 + * + * E.g. for a 44.1kHz sampling rate the end of passband frequency + * is limited to slightly below 20kHz. + * This constraint ensures that the FIR table is not overfilled. + * + * @param clockFrequency System clock frequency at Hz + * @param method sampling method to use + * @param samplingFrequency Desired output sampling rate + * @param highestAccurateFrequency + * @throw SIDError + */ + void setSamplingParameters(double clockFrequency, SamplingMethod method, double samplingFrequency, double highestAccurateFrequency); + + /** + * Clock SID forward using chosen output sampling algorithm. + * + * @param cycles c64 clocks to clock + * @param buf audio output buffer + * @return number of samples produced + */ + int clock(unsigned int cycles, short* buf); + + /** + * Clock SID forward with no audio production. + * + * _Warning_: + * You can't mix this method of clocking with the audio-producing + * clock() because components that don't affect OSC3/ENV3 are not + * emulated. + * + * @param cycles c64 clocks to clock. + */ + void clockSilent(unsigned int cycles); + + /** + * Set filter curve parameter for 6581 model. + * + * @see Filter6581::setFilterCurve(double) + */ + void setFilter6581Curve(double filterCurve); + + /** + * Set filter curve parameter for 8580 model. + * + * @see Filter8580::setFilterCurve(double) + */ + void setFilter8580Curve(double filterCurve); + + /** + * Enable filter emulation. + * + * @param enable false to turn off filter emulation + */ + void enableFilter(bool enable); +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(SID_CPP) + +#include + +#include "Filter.h" +#include "ExternalFilter.h" +#include "Voice.h" +#include "resample/Resampler.h" + +namespace reSIDfp +{ + +RESID_INLINE +void SID::ageBusValue(unsigned int n) +{ + if (likely(busValueTtl != 0)) + { + busValueTtl -= n; + + if (unlikely(busValueTtl <= 0)) + { + busValue = 0; + busValueTtl = 0; + } + } +} + +RESID_INLINE +int SID::output() +{ + const int v1 = voice[0]->output(voice[2]->wave()); + const int v2 = voice[1]->output(voice[0]->wave()); + const int v3 = voice[2]->output(voice[1]->wave()); + + lastChanOut[0]=v1; + lastChanOut[1]=v2; + lastChanOut[2]=v3; + + return externalFilter->clock(filter->clock(v1, v2, v3)); +} + + +RESID_INLINE +int SID::clock(unsigned int cycles, short* buf) +{ + ageBusValue(cycles); + int s = 0; + + while (cycles != 0) + { + unsigned int delta_t = std::min(nextVoiceSync, cycles); + + if (likely(delta_t > 0)) + { + for (unsigned int i = 0; i < delta_t; i++) + { + // clock waveform generators + voice[0]->wave()->clock(); + voice[1]->wave()->clock(); + voice[2]->wave()->clock(); + + // clock envelope generators + voice[0]->envelope()->clock(); + voice[1]->envelope()->clock(); + voice[2]->envelope()->clock(); + + if (unlikely(resampler->input(output()))) + { + buf[s++] = resampler->getOutput(); + } + } + + cycles -= delta_t; + nextVoiceSync -= delta_t; + } + + if (unlikely(nextVoiceSync == 0)) + { + voiceSync(true); + } + } + + return s; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/Spline.cpp b/src/engine/platform/sound/c64_fp/Spline.cpp new file mode 100644 index 00000000..50d55fef --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Spline.cpp @@ -0,0 +1,119 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 "Spline.h" + +#include +#include + +namespace reSIDfp +{ + +Spline::Spline(const std::vector &input) : + params(input.size()), + c(¶ms[0]) +{ + assert(input.size() > 2); + + const size_t coeffLength = input.size() - 1; + + std::vector dxs(coeffLength); + std::vector ms(coeffLength); + + // Get consecutive differences and slopes + for (size_t i = 0; i < coeffLength; i++) + { + assert(input[i].x < input[i + 1].x); + + const double dx = input[i + 1].x - input[i].x; + const double dy = input[i + 1].y - input[i].y; + dxs[i] = dx; + ms[i] = dy/dx; + } + + // Get degree-1 coefficients + params[0].c = ms[0]; + for (size_t i = 1; i < coeffLength; i++) + { + const double m = ms[i - 1]; + const double mNext = ms[i]; + if (m * mNext <= 0) + { + params[i].c = 0.0; + } + else + { + const double dx = dxs[i - 1]; + const double dxNext = dxs[i]; + const double common = dx + dxNext; + params[i].c = 3.0 * common / ((common + dxNext) / m + (common + dx) / mNext); + } + } + params[coeffLength].c = ms[coeffLength - 1]; + + // Get degree-2 and degree-3 coefficients + for (size_t i = 0; i < coeffLength; i++) + { + params[i].x1 = input[i].x; + params[i].x2 = input[i + 1].x; + params[i].d = input[i].y; + + const double c1 = params[i].c; + const double m = ms[i]; + const double invDx = 1.0 / dxs[i]; + const double common = c1 + params[i + 1].c - m - m; + params[i].b = (m - c1 - common) * invDx; + params[i].a = common * invDx * invDx; + } + + // Fix the upper range, because we interpolate outside original bounds if necessary. + params[coeffLength - 1].x2 = std::numeric_limits::max(); +} + +Spline::Point Spline::evaluate(double x) const +{ + if ((x < c->x1) || (x > c->x2)) + { + for (size_t i = 0; i < params.size(); i++) + { + if (x <= params[i].x2) + { + c = ¶ms[i]; + break; + } + } + } + + // Interpolate + const double diff = x - c->x1; + + Point out; + + // y = a*x^3 + b*x^2 + c*x + d + out.x = ((c->a * diff + c->b) * diff + c->c) * diff + c->d; + + // dy = 3*a*x^2 + 2*b*x + c + out.y = (3.0 * c->a * diff + 2.0 * c->b) * diff + c->c; + + return out; +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Spline.h b/src/engine/platform/sound/c64_fp/Spline.h new file mode 100644 index 00000000..6cc2b1ed --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Spline.h @@ -0,0 +1,78 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 SPLINE_H +#define SPLINE_H + +#include +#include + +namespace reSIDfp +{ + +/** + * Fritsch-Carlson monotone cubic spline interpolation. + * + * Based on the implementation from the [Monotone cubic interpolation] wikipedia page. + * + * [Monotone cubic interpolation]: https://en.wikipedia.org/wiki/Monotone_cubic_interpolation + */ +class Spline +{ +public: + typedef struct + { + double x; + double y; + } Point; + +private: + typedef struct + { + double x1; + double x2; + double a; + double b; + double c; + double d; + } Param; + + typedef std::vector ParamVector; + +private: + /// Interpolation parameters + ParamVector params; + + /// Last used parameters, cached for speed up + mutable ParamVector::const_pointer c; + +public: + Spline(const std::vector &input); + + /** + * Evaluate y and its derivative at given point x. + */ + Point evaluate(double x) const; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/Voice.h b/src/engine/platform/sound/c64_fp/Voice.h new file mode 100644 index 00000000..fc7ed41b --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Voice.h @@ -0,0 +1,130 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * 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 VOICE_H +#define VOICE_H + +#include + +#include "siddefs-fp.h" +#include "WaveformGenerator.h" +#include "EnvelopeGenerator.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +/** + * Representation of SID voice block. + */ +class Voice +{ +private: + std::unique_ptr const waveformGenerator; + + std::unique_ptr const envelopeGenerator; + + /// The DAC LUT for analog waveform output + float* wavDAC; //-V730_NOINIT this is initialized in the SID constructor + + /// The DAC LUT for analog envelope output + float* envDAC; //-V730_NOINIT this is initialized in the SID constructor + +public: + /** + * Amplitude modulated waveform output. + * + * The waveform DAC generates a voltage between virtual ground and Vdd + * (5-12 V for the 6581 and 4.75-9 V for the 8580) + * corresponding to oscillator state 0 .. 4095. + * + * The envelope DAC generates a voltage between waveform gen output and + * the virtual ground level, corresponding to envelope state 0 .. 255. + * + * Ideal range [-2048*255, 2047*255]. + * + * @param ringModulator Ring-modulator for waveform + * @return the voice analog output + */ + RESID_INLINE + int output(const WaveformGenerator* ringModulator) const + { + unsigned int const wav = waveformGenerator->output(ringModulator); + unsigned int const env = envelopeGenerator->output(); + + // DAC imperfections are emulated by using the digital output + // as an index into a DAC lookup table. + return static_cast(wavDAC[wav] * envDAC[env]); + } + + /** + * Constructor. + */ + Voice() : + waveformGenerator(new WaveformGenerator()), + envelopeGenerator(new EnvelopeGenerator()) {} + + /** + * Set the analog DAC emulation for waveform generator. + * Must be called before any operation. + * + * @param dac + */ + void setWavDAC(float* dac) { wavDAC = dac; } + + /** + * Set the analog DAC emulation for envelope. + * Must be called before any operation. + * + * @param dac + */ + void setEnvDAC(float* dac) { envDAC = dac; } + + WaveformGenerator* wave() const { return waveformGenerator.get(); } + + EnvelopeGenerator* envelope() const { return envelopeGenerator.get(); } + + /** + * Write control register. + * + * @param control Control register value. + */ + void writeCONTROL_REG(unsigned char control) + { + waveformGenerator->writeCONTROL_REG(control); + envelopeGenerator->writeCONTROL_REG(control); + } + + /** + * SID reset. + */ + void reset() + { + waveformGenerator->reset(); + envelopeGenerator->reset(); + } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/WaveformCalculator.cpp b/src/engine/platform/sound/c64_fp/WaveformCalculator.cpp new file mode 100644 index 00000000..fe5030fa --- /dev/null +++ b/src/engine/platform/sound/c64_fp/WaveformCalculator.cpp @@ -0,0 +1,204 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 "WaveformCalculator.h" + +#include + +namespace reSIDfp +{ + +WaveformCalculator* WaveformCalculator::getInstance() +{ + static WaveformCalculator instance; + return &instance; +} + +/** + * Parameters derived with the Monte Carlo method based on + * samplings by kevtris. Code and data available in the project repository [1]. + * + * The score here reported is the acoustic error + * calculated XORing the estimated and the sampled values. + * In parentheses the number of mispredicted bits + * on a total of 32768. + * + * [1] https://github.com/libsidplayfp/combined-waveforms + */ +const CombinedWaveformConfig config[2][4] = +{ + { /* kevtris chip G (6581 R2) */ + {0.90251f, 0.f, 0.f, 1.9147f, 1.6747f, 0.62376f }, // error 1689 (280) + {0.93088f, 2.4843f, 0.f, 1.0353f, 1.1484f, 0.f }, // error 6128 (130) + {0.90988f, 2.26303f, 1.13126f, 1.0035f, 1.13801f, 0.f }, // error 14243 (632) + {0.91f, 1.192f, 0.f, 1.0169f, 1.2f, 0.637f }, // error 64 (2) + }, + { /* kevtris chip V (8580 R5) */ + {0.9632f, 0.f, 0.975f, 1.7467f, 2.36132f, 0.975395f}, // error 1380 (169) + {0.92886f, 1.67696f, 0.f, 1.1014f, 1.4352f, 0.f }, // error 8007 (218) + {0.94043f, 1.7937f, 0.981f, 1.1213f, 1.4259f, 0.f }, // error 11957 (362) + {0.96211f, 0.98695f, 1.00387f, 1.46499f, 1.98375f, 0.77777f }, // error 2369 (89) + }, +}; + +/** + * Generate bitstate based on emulation of combined waves. + * + * @param config model parameters matrix + * @param waveform the waveform to emulate, 1 .. 7 + * @param accumulator the high bits of the accumulator value + */ +short calculateCombinedWaveform(const CombinedWaveformConfig& config, int waveform, int accumulator) +{ + float o[12]; + + // Saw + for (unsigned int i = 0; i < 12; i++) + { + o[i] = (accumulator & (1 << i)) != 0 ? 1.f : 0.f; + } + + // convert to Triangle + if ((waveform & 3) == 1) + { + const bool top = (accumulator & 0x800) != 0; + + for (int i = 11; i > 0; i--) + { + o[i] = top ? 1.0f - o[i - 1] : o[i - 1]; + } + + o[0] = 0.f; + } + + // or to Saw+Triangle + else if ((waveform & 3) == 3) + { + // bottom bit is grounded via T waveform selector + o[0] *= config.stmix; + + for (int i = 1; i < 12; i++) + { + /* + * Enabling the S waveform pulls the XOR circuit selector transistor down + * (which would normally make the descending ramp of the triangle waveform), + * so ST does not actually have a sawtooth and triangle waveform combined, + * but merely combines two sawtooths, one rising double the speed the other. + * + * http://www.lemon64.com/forum/viewtopic.php?t=25442&postdays=0&postorder=asc&start=165 + */ + o[i] = o[i - 1] * (1.f - config.stmix) + o[i] * config.stmix; + } + } + + // topbit for Saw + if ((waveform & 2) == 2) + { + o[11] *= config.topbit; + } + + // ST, P* waveforms + if (waveform == 3 || waveform > 4) + { + float distancetable[12 * 2 + 1]; + distancetable[12] = 1.f; + for (int i = 12; i > 0; i--) + { + distancetable[12-i] = 1.0f / pow(config.distance1, i); + distancetable[12+i] = 1.0f / pow(config.distance2, i); + } + + float tmp[12]; + + for (int i = 0; i < 12; i++) + { + float avg = 0.f; + float n = 0.f; + + for (int j = 0; j < 12; j++) + { + const float weight = distancetable[i - j + 12]; + avg += o[j] * weight; + n += weight; + } + + // pulse control bit + if (waveform > 4) + { + const float weight = distancetable[i - 12 + 12]; + avg += config.pulsestrength * weight; + n += weight; + } + + tmp[i] = (o[i] + avg / n) * 0.5f; + } + + for (int i = 0; i < 12; i++) + { + o[i] = tmp[i]; + } + } + + short value = 0; + + for (unsigned int i = 0; i < 12; i++) + { + if (o[i] > config.bias) + { + value |= 1 << i; + } + } + + return value; +} + +matrix_t* WaveformCalculator::buildTable(ChipModel model) +{ + const CombinedWaveformConfig* cfgArray = config[model == MOS6581 ? 0 : 1]; + + cw_cache_t::iterator lb = CACHE.lower_bound(cfgArray); + + if (lb != CACHE.end() && !(CACHE.key_comp()(cfgArray, lb->first))) + { + return &(lb->second); + } + + matrix_t wftable(8, 4096); + + for (unsigned int idx = 0; idx < 1 << 12; idx++) + { + wftable[0][idx] = 0xfff; + wftable[1][idx] = static_cast((idx & 0x800) == 0 ? idx << 1 : (idx ^ 0xfff) << 1); + wftable[2][idx] = static_cast(idx); + wftable[3][idx] = calculateCombinedWaveform(cfgArray[0], 3, idx); + wftable[4][idx] = 0xfff; + wftable[5][idx] = calculateCombinedWaveform(cfgArray[1], 5, idx); + wftable[6][idx] = calculateCombinedWaveform(cfgArray[2], 6, idx); + wftable[7][idx] = calculateCombinedWaveform(cfgArray[3], 7, idx); + } +#ifdef HAVE_CXX11 + return &(CACHE.emplace_hint(lb, cw_cache_t::value_type(cfgArray, wftable))->second); +#else + return &(CACHE.insert(lb, cw_cache_t::value_type(cfgArray, wftable))->second); +#endif +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/WaveformCalculator.h b/src/engine/platform/sound/c64_fp/WaveformCalculator.h new file mode 100644 index 00000000..f9183c5d --- /dev/null +++ b/src/engine/platform/sound/c64_fp/WaveformCalculator.h @@ -0,0 +1,128 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 WAVEFORMCALCULATOR_h +#define WAVEFORMCALCULATOR_h + +#include + +#include "array.h" +#include "sidcxx11.h" +#include "siddefs-fp.h" + + +namespace reSIDfp +{ + +/** + * Combined waveform model parameters. + */ +typedef struct +{ + float bias; + float pulsestrength; + float topbit; + float distance1; + float distance2; + float stmix; +} CombinedWaveformConfig; + +/** + * Combined waveform calculator for WaveformGenerator. + * By combining waveforms, the bits of each waveform are effectively short + * circuited. A zero bit in one waveform will result in a zero output bit + * (thus the infamous claim that the waveforms are AND'ed). + * However, a zero bit in one waveform may also affect the neighboring bits + * in the output. + * + * Example: + * + * 1 1 + * Bit # 1 0 9 8 7 6 5 4 3 2 1 0 + * ----------------------- + * Sawtooth 0 0 0 1 1 1 1 1 1 0 0 0 + * + * Triangle 0 0 1 1 1 1 1 1 0 0 0 0 + * + * AND 0 0 0 1 1 1 1 1 0 0 0 0 + * + * Output 0 0 0 0 1 1 1 0 0 0 0 0 + * + * + * Re-vectorized die photographs reveal the mechanism behind this behavior. + * Each waveform selector bit acts as a switch, which directly connects + * internal outputs into the waveform DAC inputs as follows: + * + * - Noise outputs the shift register bits to DAC inputs as described above. + * Each output is also used as input to the next bit when the shift register + * is shifted. Lower four bits are grounded. + * - Pulse connects a single line to all DAC inputs. The line is connected to + * either 5V (pulse on) or 0V (pulse off) at bit 11, and ends at bit 0. + * - Triangle connects the upper 11 bits of the (MSB EOR'ed) accumulator to the + * DAC inputs, so that DAC bit 0 = 0, DAC bit n = accumulator bit n - 1. + * - Sawtooth connects the upper 12 bits of the accumulator to the DAC inputs, + * so that DAC bit n = accumulator bit n. Sawtooth blocks out the MSB from + * the EOR used to generate the triangle waveform. + * + * We can thus draw the following conclusions: + * + * - The shift register may be written to by combined waveforms. + * - The pulse waveform interconnects all bits in combined waveforms via the + * pulse line. + * - The combination of triangle and sawtooth interconnects neighboring bits + * of the sawtooth waveform. + * + * Also in the 6581 the MSB of the oscillator, used as input for the + * triangle xor logic and the pulse adder's last bit, is connected directly + * to the waveform selector, while in the 8580 it is latched at sid_clk2 + * before being forwarded to the selector. Thus in the 6581 if the sawtooth MSB + * is pulled down it might affect the oscillator's adder + * driving the top bit low. + * + */ +class WaveformCalculator +{ +private: + typedef std::map cw_cache_t; + +private: + cw_cache_t CACHE; + + WaveformCalculator() DEFAULT; + +public: + /** + * Get the singleton instance. + */ + static WaveformCalculator* getInstance(); + + /** + * Build waveform tables for use by WaveformGenerator. + * + * @param model Chip model to use + * @return Waveform table + */ + matrix_t* buildTable(ChipModel model); +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/WaveformGenerator.cpp b/src/engine/platform/sound/c64_fp/WaveformGenerator.cpp new file mode 100644 index 00000000..e5e544e7 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/WaveformGenerator.cpp @@ -0,0 +1,357 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2021 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define WAVEFORMGENERATOR_CPP + +#include "WaveformGenerator.h" + +/* + * This fixes tests + * SID/wb_testsuite/noise_writeback_check_8_to_C_old + * SID/wb_testsuite/noise_writeback_check_9_to_C_old + * SID/wb_testsuite/noise_writeback_check_A_to_C_old + * SID/wb_testsuite/noise_writeback_check_C_to_C_old + * + * but breaks SID/wf12nsr/wf12nsr + * + * needs more digging... + */ +//#define NO_WB_NOI_PUL + +namespace reSIDfp +{ + +/** + * Number of cycles after which the waveform output fades to 0 when setting + * the waveform register to 0. + * Values measured on warm chips (6581R3/R4 and 8580R5) + * checking OSC3. + * Times vary wildly with temperature and may differ + * from chip to chip so the numbers here represent + * only the big difference between the old and new models. + * + * See [VICE Bug #290](http://sourceforge.net/p/vice-emu/bugs/290/) + * and [VICE Bug #1128](http://sourceforge.net/p/vice-emu/bugs/1128/) + */ +// ~95ms +const unsigned int FLOATING_OUTPUT_TTL_6581R3 = 54000; +const unsigned int FLOATING_OUTPUT_FADE_6581R3 = 1400; +// ~1s +//const unsigned int FLOATING_OUTPUT_TTL_6581R4 = 1000000; +// ~1s +const unsigned int FLOATING_OUTPUT_TTL_8580R5 = 800000; +const unsigned int FLOATING_OUTPUT_FADE_8580R5 = 50000; + +/** + * Number of cycles after which the shift register is reset + * when the test bit is set. + * Values measured on warm chips (6581R3/R4 and 8580R5) + * checking OSC3. + * Times vary wildly with temperature and may differ + * from chip to chip so the numbers here represent + * only the big difference between the old and new models. + */ +// ~210ms +const unsigned int SHIFT_REGISTER_RESET_6581R3 = 50000; +const unsigned int SHIFT_REGISTER_FADE_6581R3 = 15000; +// ~2.15s +//const unsigned int SHIFT_REGISTER_RESET_6581R4 = 2150000; +// ~2.8s +const unsigned int SHIFT_REGISTER_RESET_8580R5 = 986000; +const unsigned int SHIFT_REGISTER_FADE_8580R5 = 314300; + +/* + * This is what happens when the lfsr is clocked: + * + * cycle 0: bit 19 of the accumulator goes from low to high, the noise register acts normally, + * the output may overwrite a bit; + * + * cycle 1: first phase of the shift, the bits are interconnected and the output of each bit + * is latched into the following. The output may overwrite the latched value. + * + * cycle 2: second phase of the shift, the latched value becomes active in the first + * half of the clock and from the second half the register returns to normal operation. + * + * When the test or reset lines are active the first phase is executed at every cyle + * until the signal is released triggering the second phase. + */ +void WaveformGenerator::clock_shift_register(unsigned int bit0) +{ + shift_register = (shift_register >> 1) | bit0; + + // New noise waveform output. + set_noise_output(); +} + +unsigned int WaveformGenerator::get_noise_writeback() +{ + return + ~( + (1 << 2) | // Bit 20 + (1 << 4) | // Bit 18 + (1 << 8) | // Bit 14 + (1 << 11) | // Bit 11 + (1 << 13) | // Bit 9 + (1 << 17) | // Bit 5 + (1 << 20) | // Bit 2 + (1 << 22) // Bit 0 + ) | + ((waveform_output & (1 << 11)) >> 9) | // Bit 11 -> bit 20 + ((waveform_output & (1 << 10)) >> 6) | // Bit 10 -> bit 18 + ((waveform_output & (1 << 9)) >> 1) | // Bit 9 -> bit 14 + ((waveform_output & (1 << 8)) << 3) | // Bit 8 -> bit 11 + ((waveform_output & (1 << 7)) << 6) | // Bit 7 -> bit 9 + ((waveform_output & (1 << 6)) << 11) | // Bit 6 -> bit 5 + ((waveform_output & (1 << 5)) << 15) | // Bit 5 -> bit 2 + ((waveform_output & (1 << 4)) << 18); // Bit 4 -> bit 0 +} + +void WaveformGenerator::write_shift_register() +{ + if (unlikely(waveform > 0x8) && likely(!test) && likely(shift_pipeline != 1)) + { + // Write changes to the shift register output caused by combined waveforms + // back into the shift register. This happens only when the register is clocked + // (see $D1+$81_wave_test [1]) or when the test bit is falling. + // A bit once set to zero cannot be changed, hence the and'ing. + // + // [1] ftp://ftp.untergrund.net/users/nata/sid_test/$D1+$81_wave_test.7z + // + // FIXME: Write test program to check the effect of 1 bits and whether + // neighboring bits are affected. + +#ifdef NO_WB_NOI_PUL + if (waveform == 0xc) + return; +#endif + shift_register &= get_noise_writeback(); + + noise_output &= waveform_output; + set_no_noise_or_noise_output(); + } +} + +void WaveformGenerator::set_noise_output() +{ + noise_output = + ((shift_register & (1 << 2)) << 9) | // Bit 20 -> bit 11 + ((shift_register & (1 << 4)) << 6) | // Bit 18 -> bit 10 + ((shift_register & (1 << 8)) << 1) | // Bit 14 -> bit 9 + ((shift_register & (1 << 11)) >> 3) | // Bit 11 -> bit 8 + ((shift_register & (1 << 13)) >> 6) | // Bit 9 -> bit 7 + ((shift_register & (1 << 17)) >> 11) | // Bit 5 -> bit 6 + ((shift_register & (1 << 20)) >> 15) | // Bit 2 -> bit 5 + ((shift_register & (1 << 22)) >> 18); // Bit 0 -> bit 4 + + set_no_noise_or_noise_output(); +} + +void WaveformGenerator::setWaveformModels(matrix_t* models) +{ + model_wave = models; +} + +void WaveformGenerator::synchronize(WaveformGenerator* syncDest, const WaveformGenerator* syncSource) const +{ + // A special case occurs when a sync source is synced itself on the same + // cycle as when its MSB is set high. In this case the destination will + // not be synced. This has been verified by sampling OSC3. + if (unlikely(msb_rising) && syncDest->sync && !(sync && syncSource->msb_rising)) + { + syncDest->accumulator = 0; + } +} + +bool do_pre_writeback(unsigned int waveform_prev, unsigned int waveform, bool is6581) +{ + // no writeback without combined waveforms + if (likely(waveform_prev <= 0x8)) + return false; + // no writeback when changing to noise + if (waveform == 8) + return false; + // What's happening here? + if (is6581 && + ((((waveform_prev & 0x3) == 0x1) && ((waveform & 0x3) == 0x2)) + || (((waveform_prev & 0x3) == 0x2) && ((waveform & 0x3) == 0x1)))) + return false; + if (waveform_prev == 0xc) + { + if (is6581) + return false; + else if ((waveform != 0x9) && (waveform != 0xe)) + return false; + } +#ifdef NO_WB_NOI_PUL + if (waveform == 0xc) + return false; +#endif + // ok do the writeback + return true; +} + +/* + * When noise and pulse are combined all the bits are + * connected and the four lower ones are grounded. + * This causes the adjacent bits to be pulled down, + * with different strength depending on model. + * + * This is just a rough attempt at modelling the effect. + */ + +static unsigned int noise_pulse6581(unsigned int noise) +{ + return (noise < 0xf00) ? 0x000 : noise & (noise << 1) & (noise << 2); +} + +static unsigned int noise_pulse8580(unsigned int noise) +{ + return (noise < 0xfc0) ? noise & (noise << 1) : 0xfc0; +} + +void WaveformGenerator::set_no_noise_or_noise_output() +{ + no_noise_or_noise_output = no_noise | noise_output; + + // pulse+noise + if (unlikely((waveform & 0xc) == 0xc)) + no_noise_or_noise_output = is6581 + ? noise_pulse6581(no_noise_or_noise_output) + : noise_pulse8580(no_noise_or_noise_output); + +} + +void WaveformGenerator::writeCONTROL_REG(unsigned char control) +{ + const unsigned int waveform_prev = waveform; + const bool test_prev = test; + + waveform = (control >> 4) & 0x0f; + test = (control & 0x08) != 0; + sync = (control & 0x02) != 0; + + // Substitution of accumulator MSB when sawtooth = 0, ring_mod = 1. + ring_msb_mask = ((~control >> 5) & (control >> 2) & 0x1) << 23; + + if (waveform != waveform_prev) + { + // Set up waveform table. + wave = (*model_wave)[waveform & 0x7]; + + // no_noise and no_pulse are used in set_waveform_output() as bitmasks to + // only let the noise or pulse influence the output when the noise or pulse + // waveforms are selected. + no_noise = (waveform & 0x8) != 0 ? 0x000 : 0xfff; + set_no_noise_or_noise_output(); + no_pulse = (waveform & 0x4) != 0 ? 0x000 : 0xfff; + + if (waveform == 0) + { + // Change to floating DAC input. + // Reset fading time for floating DAC input. + floating_output_ttl = is6581 ? FLOATING_OUTPUT_TTL_6581R3 : FLOATING_OUTPUT_TTL_8580R5; + } + } + + if (test != test_prev) + { + if (test) + { + // Reset accumulator. + accumulator = 0; + + // Flush shift pipeline. + shift_pipeline = 0; + + // Set reset time for shift register. + shift_register_reset = is6581 ? SHIFT_REGISTER_RESET_6581R3 : SHIFT_REGISTER_RESET_8580R5; + } + else + { + // When the test bit is falling, the second phase of the shift is + // completed by enabling SRAM write. + + // During first phase of the shift the bits are interconnected + // and the output of each bit is latched into the following. + // The output may overwrite the latched value. + if (do_pre_writeback(waveform_prev, waveform, is6581)) + { + shift_register &= get_noise_writeback(); + } + + // bit0 = (bit22 | test) ^ bit17 = 1 ^ bit17 = ~bit17 + clock_shift_register((~shift_register << 17) & (1 << 22)); + } + } +} + +void WaveformGenerator::waveBitfade() +{ + waveform_output &= waveform_output >> 1; + osc3 = waveform_output; + if (waveform_output != 0) + floating_output_ttl = is6581 ? FLOATING_OUTPUT_FADE_6581R3 : FLOATING_OUTPUT_FADE_8580R5; +} + +void WaveformGenerator::shiftregBitfade() +{ + shift_register |= shift_register >> 1; + shift_register |= 0x400000; + if (shift_register != 0x7fffff) + shift_register_reset = is6581 ? SHIFT_REGISTER_FADE_6581R3 : SHIFT_REGISTER_FADE_8580R5; +} + +void WaveformGenerator::reset() +{ + // accumulator is not changed on reset + freq = 0; + pw = 0; + + msb_rising = false; + + waveform = 0; + osc3 = 0; + + test = false; + sync = false; + + wave = model_wave ? (*model_wave)[0] : nullptr; + + ring_msb_mask = 0; + no_noise = 0xfff; + no_pulse = 0xfff; + pulse_output = 0xfff; + + shift_register_reset = 0; + shift_register = 0x7fffff; + // when reset is released the shift register is clocked once + // so the lower bit is zeroed out + // bit0 = (bit22 | test) ^ bit17 = 1 ^ 1 = 0 + clock_shift_register(0); + + shift_pipeline = 0; + + waveform_output = 0; + floating_output_ttl = 0; +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/WaveformGenerator.h b/src/engine/platform/sound/c64_fp/WaveformGenerator.h new file mode 100644 index 00000000..9fd617f6 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/WaveformGenerator.h @@ -0,0 +1,396 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * 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 WAVEFORMGENERATOR_H +#define WAVEFORMGENERATOR_H + +#include "siddefs-fp.h" +#include "array.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +/** + * A 24 bit accumulator is the basis for waveform generation. + * FREQ is added to the lower 16 bits of the accumulator each cycle. + * The accumulator is set to zero when TEST is set, and starts counting + * when TEST is cleared. + * + * Waveforms are generated as follows: + * + * - No waveform: + * When no waveform is selected, the DAC input is floating. + * + * + * - Triangle: + * The upper 12 bits of the accumulator are used. + * The MSB is used to create the falling edge of the triangle by inverting + * the lower 11 bits. The MSB is thrown away and the lower 11 bits are + * left-shifted (half the resolution, full amplitude). + * Ring modulation substitutes the MSB with MSB EOR NOT sync_source MSB. + * + * + * - Sawtooth: + * The output is identical to the upper 12 bits of the accumulator. + * + * + * - Pulse: + * The upper 12 bits of the accumulator are used. + * These bits are compared to the pulse width register by a 12 bit digital + * comparator; output is either all one or all zero bits. + * The pulse setting is delayed one cycle after the compare. + * The test bit, when set to one, holds the pulse waveform output at 0xfff + * regardless of the pulse width setting. + * + * + * - Noise: + * The noise output is taken from intermediate bits of a 23-bit shift register + * which is clocked by bit 19 of the accumulator. + * The shift is delayed 2 cycles after bit 19 is set high. + * + * Operation: Calculate EOR result, shift register, set bit 0 = result. + * + * reset +--------------------------------------------+ + * | | | + * test--OR-->EOR<--+ | + * | | | + * 2 2 2 1 1 1 1 1 1 1 1 1 1 | + * Register bits: 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 <---+ + * | | | | | | | | + * Waveform bits: 1 1 9 8 7 6 5 4 + * 1 0 + * + * The low 4 waveform bits are zero (grounded). + */ +class WaveformGenerator +{ +private: + matrix_t* model_wave; + + short* wave; + + // PWout = (PWn/40.95)% + unsigned int pw; + + unsigned int shift_register; + + /// Emulation of pipeline causing bit 19 to clock the shift register. + int shift_pipeline; + + unsigned int ring_msb_mask; + unsigned int no_noise; + unsigned int noise_output; + unsigned int no_noise_or_noise_output; + unsigned int no_pulse; + unsigned int pulse_output; + + /// The control register right-shifted 4 bits; used for output function table lookup. + unsigned int waveform; + + unsigned int waveform_output; + + /// Current accumulator value. + unsigned int accumulator; + + // Fout = (Fn*Fclk/16777216)Hz + unsigned int freq; + + /// 8580 tri/saw pipeline + unsigned int tri_saw_pipeline; + + /// The OSC3 value + unsigned int osc3; + + /// Remaining time to fully reset shift register. + unsigned int shift_register_reset; + + // The wave signal TTL when no waveform is selected + unsigned int floating_output_ttl; + + /// The control register bits. Gate is handled by EnvelopeGenerator. + //@{ + bool test; + bool sync; + //@} + + /// Tell whether the accumulator MSB was set high on this cycle. + bool msb_rising; + + bool is6581; //-V730_NOINIT this is initialized in the SID constructor + +private: + void clock_shift_register(unsigned int bit0); + + unsigned int get_noise_writeback(); + + void write_shift_register(); + + void set_noise_output(); + + void set_no_noise_or_noise_output(); + + void waveBitfade(); + + void shiftregBitfade(); + +public: + void setWaveformModels(matrix_t* models); + + /** + * Set the chip model. + * Must be called before any operation. + * + * @param is6581 true if MOS6581, false if CSG8580 + */ + void setModel(bool is6581) { this->is6581 = is6581; } + + /** + * SID clocking. + */ + void clock(); + + /** + * Synchronize oscillators. + * This must be done after all the oscillators have been clock()'ed, + * so that they are in the same state. + * + * @param syncDest The oscillator that will be synced + * @param syncSource The sync source oscillator + */ + void synchronize(WaveformGenerator* syncDest, const WaveformGenerator* syncSource) const; + + /** + * Constructor. + */ + WaveformGenerator() : + model_wave(nullptr), + wave(nullptr), + pw(0), + shift_register(0), + shift_pipeline(0), + ring_msb_mask(0), + no_noise(0), + noise_output(0), + no_noise_or_noise_output(0), + no_pulse(0), + pulse_output(0), + waveform(0), + waveform_output(0), + accumulator(0x555555), // Accumulator's even bits are high on powerup + freq(0), + tri_saw_pipeline(0x555), + osc3(0), + shift_register_reset(0), + floating_output_ttl(0), + test(false), + sync(false), + msb_rising(false) {} + + /** + * Write FREQ LO register. + * + * @param freq_lo low 8 bits of frequency + */ + void writeFREQ_LO(unsigned char freq_lo) { freq = (freq & 0xff00) | (freq_lo & 0xff); } + + /** + * Write FREQ HI register. + * + * @param freq_hi high 8 bits of frequency + */ + void writeFREQ_HI(unsigned char freq_hi) { freq = (freq_hi << 8 & 0xff00) | (freq & 0xff); } + + /** + * Write PW LO register. + * + * @param pw_lo low 8 bits of pulse width + */ + void writePW_LO(unsigned char pw_lo) { pw = (pw & 0xf00) | (pw_lo & 0x0ff); } + + /** + * Write PW HI register. + * + * @param pw_hi high 8 bits of pulse width + */ + void writePW_HI(unsigned char pw_hi) { pw = (pw_hi << 8 & 0xf00) | (pw & 0x0ff); } + + /** + * Write CONTROL REGISTER register. + * + * @param control control register value + */ + void writeCONTROL_REG(unsigned char control); + + /** + * SID reset. + */ + void reset(); + + /** + * 12-bit waveform output. + * + * @param ringModulator The oscillator ring-modulating current one. + * @return the waveform generator digital output + */ + unsigned int output(const WaveformGenerator* ringModulator); + + /** + * Read OSC3 value. + */ + unsigned char readOSC() const { return static_cast(osc3 >> 4); } + + /** + * Read accumulator value. + */ + unsigned int readAccumulator() const { return accumulator; } + + /** + * Read freq value. + */ + unsigned int readFreq() const { return freq; } + + /** + * Read test value. + */ + bool readTest() const { return test; } + + /** + * Read sync value. + */ + bool readSync() const { return sync; } +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(WAVEFORMGENERATOR_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +void WaveformGenerator::clock() +{ + if (unlikely(test)) + { + if (unlikely(shift_register_reset != 0) && unlikely(--shift_register_reset == 0)) + { + shiftregBitfade(); + + // New noise waveform output. + set_noise_output(); + } + + // The test bit sets pulse high. + pulse_output = 0xfff; + } + else + { + // Calculate new accumulator value; + const unsigned int accumulator_old = accumulator; + accumulator = (accumulator + freq) & 0xffffff; + + // Check which bit have changed + const unsigned int accumulator_bits_set = ~accumulator_old & accumulator; + + // Check whether the MSB is set high. This is used for synchronization. + msb_rising = (accumulator_bits_set & 0x800000) != 0; + + // Shift noise register once for each time accumulator bit 19 is set high. + // The shift is delayed 2 cycles. + if (unlikely((accumulator_bits_set & 0x080000) != 0)) + { + // Pipeline: Detect rising bit, shift phase 1, shift phase 2. + shift_pipeline = 2; + } + else if (unlikely(shift_pipeline != 0) && --shift_pipeline == 0) + { + // bit0 = (bit22 | test) ^ bit17 + clock_shift_register(((shift_register << 22) ^ (shift_register << 17)) & (1 << 22)); + } + } +} + +RESID_INLINE +unsigned int WaveformGenerator::output(const WaveformGenerator* ringModulator) +{ + // Set output value. + if (likely(waveform != 0)) + { + const unsigned int ix = (accumulator ^ (~ringModulator->accumulator & ring_msb_mask)) >> 12; + + // The bit masks no_pulse and no_noise are used to achieve branch-free + // calculation of the output value. + waveform_output = wave[ix] & (no_pulse | pulse_output) & no_noise_or_noise_output; + + // Triangle/Sawtooth output is delayed half cycle on 8580. + // This will appear as a one cycle delay on OSC3 as it is latched first phase of the clock. + if ((waveform & 3) && !is6581) + { + osc3 = tri_saw_pipeline & (no_pulse | pulse_output) & no_noise_or_noise_output; + tri_saw_pipeline = wave[ix]; + } + else + { + osc3 = waveform_output; + } + + // In the 6581 the top bit of the accumulator may be driven low by combined waveforms + // when the sawtooth is selected + // FIXME doesn't seem to always happen + if ((waveform & 2) && unlikely(waveform & 0xd) && is6581) + accumulator &= (waveform_output << 12) | 0x7fffff; + + write_shift_register(); + } + else + { + // Age floating DAC input. + if (likely(floating_output_ttl != 0) && unlikely(--floating_output_ttl == 0)) + { + waveBitfade(); + } + } + + // The pulse level is defined as (accumulator >> 12) >= pw ? 0xfff : 0x000. + // The expression -((accumulator >> 12) >= pw) & 0xfff yields the same + // results without any branching (and thus without any pipeline stalls). + // NB! This expression relies on that the result of a boolean expression + // is either 0 or 1, and furthermore requires two's complement integer. + // A few more cycles may be saved by storing the pulse width left shifted + // 12 bits, and dropping the and with 0xfff (this is valid since pulse is + // used as a bit mask on 12 bit values), yielding the expression + // -(accumulator >= pw24). However this only results in negligible savings. + + // The result of the pulse width compare is delayed one cycle. + // Push next pulse level into pulse level pipeline. + pulse_output = ((accumulator >> 12) >= pw) ? 0xfff : 0x000; + + return waveform_output; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/array.h b/src/engine/platform/sound/c64_fp/array.h new file mode 100644 index 00000000..5291938c --- /dev/null +++ b/src/engine/platform/sound/c64_fp/array.h @@ -0,0 +1,73 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright (C) 2011-2014 Leandro Nini + * + * 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 ARRAY_H +#define ARRAY_H + +/** + * Counter. + */ +class counter +{ +private: + unsigned int c; + +public: + counter() : c(1) {} + void increase() { ++c; } + unsigned int decrease() { return --c; } +}; + +/** + * Reference counted pointer to matrix wrapper, for use with standard containers. + */ +template +class matrix +{ +private: + T* data; + counter* count; + const unsigned int x, y; + +public: + matrix(unsigned int x, unsigned int y) : + data(new T[x * y]), + count(new counter()), + x(x), + y(y) {} + + matrix(const matrix& p) : + data(p.data), + count(p.count), + x(p.x), + y(p.y) { count->increase(); } + + ~matrix() { if (count->decrease() == 0) { delete count; delete [] data; } } + + unsigned int length() const { return x * y; } + + T* operator[](unsigned int a) { return &data[a * y]; } + + T const* operator[](unsigned int a) const { return &data[a * y]; } +}; + +typedef matrix matrix_t; + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/Resampler.h b/src/engine/platform/sound/c64_fp/resample/Resampler.h new file mode 100644 index 00000000..904f6545 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/Resampler.h @@ -0,0 +1,86 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 RESAMPLER_H +#define RESAMPLER_H + +#include + +#include "../sidcxx11.h" + +#include "../siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * Abstraction of a resampling process. Given enough input, produces output. + * Constructors take additional arguments that configure these objects. + */ +class Resampler +{ +protected: + inline short softClip(int x) const + { + constexpr int threshold = 28000; + if (likely(x < threshold)) + return x; + + constexpr double t = threshold / 32768.; + constexpr double a = 1. - t; + constexpr double b = 1. / a; + + double value = static_cast(x - threshold) / 32768.; + value = t + a * tanh(b * value); + return static_cast(value * 32768.); + } + + virtual int output() const = 0; + + Resampler() {} + +public: + virtual ~Resampler() {} + + /** + * Input a sample into resampler. Output "true" when resampler is ready with new sample. + * + * @param sample input sample + * @return true when a sample is ready + */ + virtual bool input(int sample) = 0; + + /** + * Output a sample from resampler. + * + * @return resampled sample + */ + short getOutput() const + { + return softClip(output()); + } + + virtual void reset() = 0; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/SincResampler.cpp b/src/engine/platform/sound/c64_fp/resample/SincResampler.cpp new file mode 100755 index 00000000..adb17f9e --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/SincResampler.cpp @@ -0,0 +1,393 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * 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 "SincResampler.h" + +#include +#include +#include +#include +#include + +#include "../siddefs-fp.h" + +#ifdef HAVE_EMMINTRIN_H +# include +#elif defined HAVE_MMINTRIN_H +# include +#elif defined(HAVE_ARM_NEON_H) +# include +#endif + +namespace reSIDfp +{ + +typedef std::map fir_cache_t; + +/// Cache for the expensive FIR table computation results. +fir_cache_t FIR_CACHE; + +/// Maximum error acceptable in I0 is 1e-6, or ~96 dB. +const double I0E = 1e-6; + +const int BITS = 16; + +/** + * Compute the 0th order modified Bessel function of the first kind. + * This function is originally from resample-1.5/filterkit.c by J. O. Smith. + * It is used to build the Kaiser window for resampling. + * + * @param x evaluate I0 at x + * @return value of I0 at x. + */ +double I0(double x) +{ + double sum = 1.; + double u = 1.; + double n = 1.; + const double halfx = x / 2.; + + do + { + const double temp = halfx / n; + u *= temp * temp; + sum += u; + n += 1.; + } + while (u >= I0E * sum); + + return sum; +} + +/** + * Calculate convolution with sample and sinc. + * + * @param a sample buffer input + * @param b sinc buffer + * @param bLength length of the sinc buffer + * @return convolved result + */ +int convolve(const short* a, const short* b, int bLength) +{ +#ifdef HAVE_EMMINTRIN_H + int out = 0; + + const uintptr_t offset = (uintptr_t)(a) & 0x0f; + + // check for aligned accesses + if (offset == ((uintptr_t)(b) & 0x0f)) + { + if (offset) + { + const int l = (0x10 - offset)/2; + + for (int i = 0; i < l; i++) + { + out += *a++ * *b++; + } + + bLength -= offset; + } + + __m128i acc = _mm_setzero_si128(); + + const int n = bLength / 8; + + for (int i = 0; i < n; i++) + { + const __m128i tmp = _mm_madd_epi16(*(__m128i*)a, *(__m128i*)b); + acc = _mm_add_epi16(acc, tmp); + a += 8; + b += 8; + } + + __m128i vsum = _mm_add_epi32(acc, _mm_srli_si128(acc, 8)); + vsum = _mm_add_epi32(vsum, _mm_srli_si128(vsum, 4)); + out += _mm_cvtsi128_si32(vsum); + + bLength &= 7; + } +#elif defined HAVE_MMINTRIN_H + __m64 acc = _mm_setzero_si64(); + + const int n = bLength / 4; + + for (int i = 0; i < n; i++) + { + const __m64 tmp = _mm_madd_pi16(*(__m64*)a, *(__m64*)b); + acc = _mm_add_pi16(acc, tmp); + a += 4; + b += 4; + } + + int out = _mm_cvtsi64_si32(acc) + _mm_cvtsi64_si32(_mm_srli_si64(acc, 32)); + _mm_empty(); + + bLength &= 3; +#elif defined(HAVE_ARM_NEON_H) +#if (defined(__arm64__) && defined(__APPLE__)) || defined(__aarch64__) + int32x4_t acc1Low = vdupq_n_s32(0); + int32x4_t acc1High = vdupq_n_s32(0); + int32x4_t acc2Low = vdupq_n_s32(0); + int32x4_t acc2High = vdupq_n_s32(0); + + const int n = bLength / 16; + + for (int i = 0; i < n; i++) + { + int16x8_t v11 = vld1q_s16(a); + int16x8_t v12 = vld1q_s16(a + 8); + int16x8_t v21 = vld1q_s16(b); + int16x8_t v22 = vld1q_s16(b + 8); + + acc1Low = vmlal_s16(acc1Low, vget_low_s16(v11), vget_low_s16(v21)); + acc1High = vmlal_high_s16(acc1High, v11, v21); + acc2Low = vmlal_s16(acc2Low, vget_low_s16(v12), vget_low_s16(v22)); + acc2High = vmlal_high_s16(acc2High, v12, v22); + + a += 16; + b += 16; + } + + bLength &= 15; + + if (bLength >= 8) + { + int16x8_t v1 = vld1q_s16(a); + int16x8_t v2 = vld1q_s16(b); + + acc1Low = vmlal_s16(acc1Low, vget_low_s16(v1), vget_low_s16(v2)); + acc1High = vmlal_high_s16(acc1High, v1, v2); + + a += 8; + b += 8; + } + + bLength &= 7; + + if (bLength >= 4) + { + int16x4_t v1 = vld1_s16(a); + int16x4_t v2 = vld1_s16(b); + + acc1Low = vmlal_s16(acc1Low, v1, v2); + + a += 4; + b += 4; + } + + int32x4_t accSumsNeon = vaddq_s32(acc1Low, acc1High); + accSumsNeon = vaddq_s32(accSumsNeon, acc2Low); + accSumsNeon = vaddq_s32(accSumsNeon, acc2High); + + int out = vaddvq_s32(accSumsNeon); + + bLength &= 3; +#else + int32x4_t acc = vdupq_n_s32(0); + + const int n = bLength / 4; + + for (int i = 0; i < n; i++) + { + const int16x4_t h_vec = vld1_s16(a); + const int16x4_t x_vec = vld1_s16(b); + acc = vmlal_s16(acc, h_vec, x_vec); + a += 4; + b += 4; + } + + int out = vgetq_lane_s32(acc, 0) + + vgetq_lane_s32(acc, 1) + + vgetq_lane_s32(acc, 2) + + vgetq_lane_s32(acc, 3); + + bLength &= 3; +#endif +#else + int out = 0; +#endif + + for (int i = 0; i < bLength; i++) + { + out += *a++ * *b++; + } + + return (out + (1 << 14)) >> 15; +} + +int SincResampler::fir(int subcycle) +{ + // Find the first of the nearest fir tables close to the phase + int firTableFirst = (subcycle * firRES >> 10); + const int firTableOffset = (subcycle * firRES) & 0x3ff; + + // Find firN most recent samples, plus one extra in case the FIR wraps. + int sampleStart = sampleIndex - firN + RINGSIZE - 1; + + const int v1 = convolve(sample + sampleStart, (*firTable)[firTableFirst], firN); + + // Use next FIR table, wrap around to first FIR table using + // previous sample. + if (unlikely(++firTableFirst == firRES)) + { + firTableFirst = 0; + ++sampleStart; + } + + const int v2 = convolve(sample + sampleStart, (*firTable)[firTableFirst], firN); + + // Linear interpolation between the sinc tables yields good + // approximation for the exact value. + return v1 + (firTableOffset * (v2 - v1) >> 10); +} + +SincResampler::SincResampler(double clockFrequency, double samplingFrequency, double highestAccurateFrequency) : + sampleIndex(0), + cyclesPerSample(static_cast(clockFrequency / samplingFrequency * 1024.)), + sampleOffset(0), + outputValue(0) +{ + // 16 bits -> -96dB stopband attenuation. + const double A = -20. * log10(1.0 / (1 << BITS)); + // A fraction of the bandwidth is allocated to the transition band, which we double + // because we design the filter to transition halfway at nyquist. + const double dw = (1. - 2.*highestAccurateFrequency / samplingFrequency) * M_PI * 2.; + + // For calculation of beta and N see the reference for the kaiserord + // function in the MATLAB Signal Processing Toolbox: + // http://www.mathworks.com/help/signal/ref/kaiserord.html + const double beta = 0.1102 * (A - 8.7); + const double I0beta = I0(beta); + const double cyclesPerSampleD = clockFrequency / samplingFrequency; + + { + // The filter order will maximally be 124 with the current constraints. + // N >= (96.33 - 7.95)/(2 * pi * 2.285 * (maxfreq - passbandfreq) >= 123 + // The filter order is equal to the number of zero crossings, i.e. + // it should be an even number (sinc is symmetric with respect to x = 0). + int N = static_cast((A - 7.95) / (2.285 * dw) + 0.5); + N += N & 1; + + // The filter length is equal to the filter order + 1. + // The filter length must be an odd number (sinc is symmetric with respect to + // x = 0). + firN = static_cast(N * cyclesPerSampleD) + 1; + firN |= 1; + + // Check whether the sample ring buffer would overflow. + assert(firN < RINGSIZE); + + // Error is bounded by err < 1.234 / L^2, so L = sqrt(1.234 / (2^-16)) = sqrt(1.234 * 2^16). + firRES = static_cast(ceil(sqrt(1.234 * (1 << BITS)) / cyclesPerSampleD)); + + // firN*firRES represent the total resolution of the sinc sampling. JOS + // recommends a length of 2^BITS, but we don't quite use that good a filter. + // The filter test program indicates that the filter performs well, though. + } + + // Create the map key + std::ostringstream o; + o << firN << "," << firRES << "," << cyclesPerSampleD; + const std::string firKey = o.str(); + fir_cache_t::iterator lb = FIR_CACHE.lower_bound(firKey); + + // The FIR computation is expensive and we set sampling parameters often, but + // from a very small set of choices. Thus, caching is used to speed initialization. + if (lb != FIR_CACHE.end() && !(FIR_CACHE.key_comp()(firKey, lb->first))) + { + firTable = &(lb->second); + } + else + { + // Allocate memory for FIR tables. + matrix_t tempTable(firRES, firN); +#ifdef HAVE_CXX11 + firTable = &(FIR_CACHE.emplace_hint(lb, fir_cache_t::value_type(firKey, tempTable))->second); +#else + firTable = &(FIR_CACHE.insert(lb, fir_cache_t::value_type(firKey, tempTable))->second); +#endif + + // The cutoff frequency is midway through the transition band, in effect the same as nyquist. + const double wc = M_PI; + + // Calculate the sinc tables. + const double scale = 32768.0 * wc / cyclesPerSampleD / M_PI; + + // we're not interested in the fractional part + // so use int division before converting to double + const int tmp = firN / 2; + const double firN_2 = static_cast(tmp); + + for (int i = 0; i < firRES; i++) + { + const double jPhase = (double) i / firRES + firN_2; + + for (int j = 0; j < firN; j++) + { + const double x = j - jPhase; + + const double xt = x / firN_2; + const double kaiserXt = fabs(xt) < 1. ? I0(beta * sqrt(1. - xt * xt)) / I0beta : 0.; + + const double wt = wc * x / cyclesPerSampleD; + const double sincWt = fabs(wt) >= 1e-8 ? sin(wt) / wt : 1.; + + (*firTable)[i][j] = static_cast(scale * sincWt * kaiserXt); + } + } + } +} + +bool SincResampler::input(int input) +{ + bool ready = false; + + /* + * Clip the input as it may overflow the 16 bit range. + * + * Approximate measured input ranges: + * 6581: [-24262,+25080] (Kawasaki_Synthesizer_Demo) + * 8580: [-21514,+35232] (64_Forever, Drum_Fool) + */ + sample[sampleIndex] = sample[sampleIndex + RINGSIZE] = softClip(input); + sampleIndex = (sampleIndex + 1) & (RINGSIZE - 1); + + if (sampleOffset < 1024) + { + outputValue = fir(sampleOffset); + ready = true; + sampleOffset += cyclesPerSample; + } + + sampleOffset -= 1024; + + return ready; +} + +void SincResampler::reset() +{ + memset(sample, 0, sizeof(sample)); + sampleOffset = 0; +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/resample/SincResampler.h b/src/engine/platform/sound/c64_fp/resample/SincResampler.h new file mode 100644 index 00000000..7502d96f --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/SincResampler.h @@ -0,0 +1,114 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * 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 SINCRESAMPLER_H +#define SINCRESAMPLER_H + +#include "Resampler.h" + +#include +#include + +#include "../array.h" + +#include "../sidcxx11.h" + +namespace reSIDfp +{ + +/** + * This is the theoretically correct (and computationally intensive) audio sample generation. + * The samples are generated by resampling to the specified sampling frequency. + * The work rate is inversely proportional to the percentage of the bandwidth + * allocated to the filter transition band. + * + * This implementation is based on the paper "A Flexible Sampling-Rate Conversion Method", + * by J. O. Smith and P. Gosset, or rather on the expanded tutorial on the + * [Digital Audio Resampling Home Page](http://www-ccrma.stanford.edu/~jos/resample/). + * + * By building shifted FIR tables with samples according to the sampling frequency, + * this implementation dramatically reduces the computational effort in the + * filter convolutions, without any loss of accuracy. + * The filter convolutions are also vectorizable on current hardware. + */ +class SincResampler final : public Resampler +{ +private: + /// Size of the ring buffer, must be a power of 2 + static const int RINGSIZE = 2048; + +private: + /// Table of the fir filter coefficients + matrix_t* firTable; + + int sampleIndex; + + /// Filter resolution + int firRES; + + /// Filter length + int firN; + + const int cyclesPerSample; + + int sampleOffset; + + int outputValue; + + short sample[RINGSIZE * 2]; + +private: + int fir(int subcycle); + +public: + /** + * Use a clock freqency of 985248Hz for PAL C64, 1022730Hz for NTSC C64. + * The default end of passband frequency is pass_freq = 0.9*sample_freq/2 + * for sample frequencies up to ~ 44.1kHz, and 20kHz for higher sample frequencies. + * + * For resampling, the ratio between the clock frequency and the sample frequency + * is limited as follows: 125*clock_freq/sample_freq < 16384 + * E.g. provided a clock frequency of ~ 1MHz, the sample frequency + * can not be set lower than ~ 8kHz. + * A lower sample frequency would make the resampling code overfill its 16k sample ring buffer. + * + * The end of passband frequency is also limited: pass_freq <= 0.9*sample_freq/2 + * + * E.g. for a 44.1kHz sampling rate the end of passband frequency is limited + * to slightly below 20kHz. This constraint ensures that the FIR table is not overfilled. + * + * @param clockFrequency System clock frequency at Hz + * @param samplingFrequency Desired output sampling rate + * @param highestAccurateFrequency + */ + SincResampler(double clockFrequency, double samplingFrequency, double highestAccurateFrequency); + + bool input(int input) override; + + int output() const override { return outputValue; } + + void reset() override; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/TwoPassSincResampler.h b/src/engine/platform/sound/c64_fp/resample/TwoPassSincResampler.h new file mode 100644 index 00000000..81659193 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/TwoPassSincResampler.h @@ -0,0 +1,83 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 TWOPASSSINCRESAMPLER_H +#define TWOPASSSINCRESAMPLER_H + +#include + +#include + +#include "Resampler.h" +#include "SincResampler.h" + +#include "../sidcxx11.h" + +namespace reSIDfp +{ + +/** + * Compose a more efficient SINC from chaining two other SINCs. + */ +class TwoPassSincResampler final : public Resampler +{ +private: + std::unique_ptr const s1; + std::unique_ptr const s2; + +private: + TwoPassSincResampler(double clockFrequency, double samplingFrequency, double highestAccurateFrequency, double intermediateFrequency) : + s1(new SincResampler(clockFrequency, intermediateFrequency, highestAccurateFrequency)), + s2(new SincResampler(intermediateFrequency, samplingFrequency, highestAccurateFrequency)) + {} + +public: + // Named constructor + static TwoPassSincResampler* create(double clockFrequency, double samplingFrequency, double highestAccurateFrequency) + { + // Calculation according to Laurent Ganier. It evaluates to about 120 kHz at typical settings. + // Some testing around the chosen value seems to confirm that this does work. + double const intermediateFrequency = 2. * highestAccurateFrequency + + sqrt(2. * highestAccurateFrequency * clockFrequency + * (samplingFrequency - 2. * highestAccurateFrequency) / samplingFrequency); + return new TwoPassSincResampler(clockFrequency, samplingFrequency, highestAccurateFrequency, intermediateFrequency); + } + + bool input(int sample) override + { + return s1->input(sample) && s2->input(s1->output()); + } + + int output() const override + { + return s2->output(); + } + + void reset() override + { + s1->reset(); + s2->reset(); + } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/ZeroOrderResampler.h b/src/engine/platform/sound/c64_fp/resample/ZeroOrderResampler.h new file mode 100644 index 00000000..2bc80cde --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/ZeroOrderResampler.h @@ -0,0 +1,88 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 ZEROORDER_RESAMPLER_H +#define ZEROORDER_RESAMPLER_H + +#include "Resampler.h" + +#include "../sidcxx11.h" + +namespace reSIDfp +{ + +/** + * Return sample with linear interpolation. + * + * @author Antti Lankila + */ +class ZeroOrderResampler final : public Resampler +{ + +private: + /// Last sample + int cachedSample; + + /// Number of cycles per sample + const int cyclesPerSample; + + int sampleOffset; + + /// Calculated sample + int outputValue; + +public: + ZeroOrderResampler(double clockFrequency, double samplingFrequency) : + cachedSample(0), + cyclesPerSample(static_cast(clockFrequency / samplingFrequency * 1024.)), + sampleOffset(0), + outputValue(0) {} + + bool input(int sample) override + { + bool ready = false; + + if (sampleOffset < 1024) + { + outputValue = cachedSample + (sampleOffset * (sample - cachedSample) >> 10); + ready = true; + sampleOffset += cyclesPerSample; + } + + sampleOffset -= 1024; + + cachedSample = sample; + + return ready; + } + + int output() const override { return outputValue; } + + void reset() override + { + sampleOffset = 0; + cachedSample = 0; + } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/test.cpp b/src/engine/platform/sound/c64_fp/resample/test.cpp new file mode 100644 index 00000000..5e5026ff --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/test.cpp @@ -0,0 +1,87 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2012-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * 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 +#include +#include +#include +#include +#include + +#include "siddefs-fp.h" + +#include "Resampler.h" +#include "TwoPassSincResampler.h" + +/** + * Simple sin waveform in, power output measurement function. + * It would be far better to use FFT. + */ +int main(int argc, const char* argv[]) +{ + const double RATE = 985248.4; + const int RINGSIZE = 2048; + + std::auto_ptr r(reSIDfp::TwoPassSincResampler::create(RATE, 48000.0, 20000.0)); + + std::map results; + clock_t start = clock(); + + for (double freq = 1000.; freq < RATE / 2.; freq *= 1.01) + { + /* prefill resampler buffer */ + int k = 0; + double omega = 2 * M_PI * freq / RATE; + + for (int j = 0; j < RINGSIZE; j ++) + { + int signal = static_cast(32768.0 * sin(k++ * omega) * sqrt(2)); + r->input(signal); + } + + int n = 0; + float pwr = 0; + + /* Now, during measurement stage, put 100 cycles of waveform through filter. */ + for (int j = 0; j < 100000; j ++) + { + int signal = static_cast(32768.0 * sin(k++ * omega) * sqrt(2)); + + if (r->input(signal)) + { + float out = r->output(); + pwr += out * out; + n += 1; + } + } + + results.insert(std::make_pair(freq, 10 * log10(pwr / n))); + } + + clock_t end = clock(); + + for (std::map::iterator it = results.begin(); it != results.end(); ++it) + { + std::cout << std::fixed << std::setprecision(0) << std::setw(6) << (*it).first << " Hz " << (*it).second << " dB" << std::endl; + } + + std::cout << "Filtering time " << (end - start) * 1000. / CLOCKS_PER_SEC << " ms" << std::endl; +} diff --git a/src/engine/platform/sound/c64_fp/sidcxx11.h b/src/engine/platform/sound/c64_fp/sidcxx11.h new file mode 100644 index 00000000..18eadf4a --- /dev/null +++ b/src/engine/platform/sound/c64_fp/sidcxx11.h @@ -0,0 +1,29 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2014-2015 Leandro Nini + * + * 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 SIDCXX11_H +#define SIDCXX11_H + +#define DEFAULT = default +#define DELETE = delete + +#define HAVE_CXX11 + +#endif diff --git a/src/engine/platform/sound/c64_fp/sidcxx14.h b/src/engine/platform/sound/c64_fp/sidcxx14.h new file mode 100644 index 00000000..5078a0b1 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/sidcxx14.h @@ -0,0 +1,29 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2014-2015 Leandro Nini + * + * 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 SIDCXX14_H +#define SIDCXX14_H + +#include "sidcxx11.h" + +#define MAKE_UNIQUE(type, ...) std::make_unique(__VA_ARGS__) +#define HAVE_CXX14 + +#endif diff --git a/src/engine/platform/sound/c64_fp/siddefs-fp.h b/src/engine/platform/sound/c64_fp/siddefs-fp.h new file mode 100644 index 00000000..43900862 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/siddefs-fp.h @@ -0,0 +1,62 @@ +// --------------------------------------------------------------------------- +// This file is part of reSID, a MOS6581 SID emulator engine. +// Copyright (C) 1999 Dag Lem +// +// 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 SIDDEFS_FP_H +#define SIDDEFS_FP_H + +// Compilation configuration. +#define RESID_BRANCH_HINTS 0 + +// Compiler specifics. +#define HAVE_BUILTIN_EXPECT 0 + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +// Branch prediction macros, lifted off the Linux kernel. +#if RESID_BRANCH_HINTS && HAVE_BUILTIN_EXPECT +# define likely(x) __builtin_expect(!!(x), 1) +# define unlikely(x) __builtin_expect(!!(x), 0) +#else +# define likely(x) (x) +# define unlikely(x) (x) +#endif + +namespace reSIDfp { + +typedef enum { MOS6581=1, MOS8580 } ChipModel; + +typedef enum { DECIMATE=1, RESAMPLE } SamplingMethod; +} + +extern "C" +{ +#ifndef __VERSION_CC__ +extern const char* residfp_version_string; +#else +const char* residfp_version_string = "furnace"; +#endif +} + +// Inlining on/off. +#define RESID_INLINING 1 +#define RESID_INLINE inline + +#endif // SIDDEFS_FP_H diff --git a/src/engine/platform/sound/c64_fp/version.cc b/src/engine/platform/sound/c64_fp/version.cc new file mode 100644 index 00000000..3ed8b449 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/version.cc @@ -0,0 +1,21 @@ +// --------------------------------------------------------------------------- +// This file is part of reSID, a MOS6581 SID emulator engine. +// Copyright (C) 2004 Dag Lem +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// --------------------------------------------------------------------------- + +#define __VERSION_CC__ +#include "siddefs-fp.h" diff --git a/src/engine/platform/sound/tia/Audio.cpp b/src/engine/platform/sound/tia/Audio.cpp new file mode 100644 index 00000000..662bd7fd --- /dev/null +++ b/src/engine/platform/sound/tia/Audio.cpp @@ -0,0 +1,143 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#define _USE_MATH_DEFINES +#include "Audio.h" + +#include + +namespace { + constexpr double R_MAX = 30.; + constexpr double R = 1.; + + short mixingTableEntry(unsigned char v, unsigned char vMax) + { + return static_cast( + floor(0x7fff * double(v) / double(vMax) * (R_MAX + R * double(vMax)) / (R_MAX + R * double(v))) + ); + } +} + +namespace TIA { + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Audio::Audio() +{ + for (unsigned char i = 0; i <= 0x1e; ++i) myMixingTableSum[i] = mixingTableEntry(i, 0x1e); + for (unsigned char i = 0; i <= 0x0f; ++i) myMixingTableIndividual[i] = mixingTableEntry(i, 0x0f); + + reset(false); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::reset(bool st) +{ + myCounter = 0; + mySampleIndex = 0; + stereo = st; + + myCurrentSample[0]=0; + myCurrentSample[1]=0; + + myChannelOut[0]=0; + myChannelOut[1]=0; + + myChannel0.reset(); + myChannel1.reset(); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::tick() +{ + switch (myCounter) { + case 9: + case 81: + myChannel0.phase0(); + myChannel1.phase0(); + + break; + + case 37: + case 149: + phase1(); + break; + } + + if (++myCounter == 228) myCounter = 0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::write(unsigned char addr, unsigned char val) { + switch (addr&0x3f) { + case 0x15: + myChannel0.audc(val); + break; + case 0x16: + myChannel1.audc(val); + break; + case 0x17: + myChannel0.audf(val); + break; + case 0x18: + myChannel1.audf(val); + break; + case 0x19: + myChannel0.audv(val); + break; + case 0x1a: + myChannel1.audv(val); + break; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::phase1() +{ + unsigned char sample0 = myChannel0.phase1(); + unsigned char sample1 = myChannel1.phase1(); + + addSample(sample0, sample1); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void Audio::addSample(unsigned char sample0, unsigned char sample1) +{ + if(stereo) { + myCurrentSample[0] = myMixingTableIndividual[sample0]; + myCurrentSample[1] = myMixingTableIndividual[sample1]; + } + else { + myCurrentSample[0] = myMixingTableSum[sample0 + sample1]; + } + + myChannelOut[0] = myMixingTableIndividual[sample0]; + myChannelOut[1] = myMixingTableIndividual[sample1]; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioChannel& Audio::channel0() +{ + return myChannel0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +AudioChannel& Audio::channel1() +{ + return myChannel1; +} + +} \ No newline at end of file diff --git a/src/engine/platform/sound/tia/Audio.h b/src/engine/platform/sound/tia/Audio.h new file mode 100644 index 00000000..4a782bb3 --- /dev/null +++ b/src/engine/platform/sound/tia/Audio.h @@ -0,0 +1,72 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef TIA_AUDIO_HXX +#define TIA_AUDIO_HXX + +#include "AudioChannel.h" +#include + +namespace TIA { + class Audio + { + public: + Audio(); + + void reset(bool stereo); + + void tick(); + + void write(unsigned char addr, unsigned char val); + + AudioChannel& channel0(); + + AudioChannel& channel1(); + + short myCurrentSample[2]; + short myChannelOut[2]; + + private: + void phase1(); + void addSample(unsigned char sample0, unsigned char sample1); + + private: + unsigned char myCounter{0}; + + AudioChannel myChannel0; + AudioChannel myChannel1; + + bool stereo; + + std::array myMixingTableSum; + std::array myMixingTableIndividual; + + unsigned int mySampleIndex{0}; + #ifdef GUI_SUPPORT + bool myRewindMode{false}; + mutable ByteArray mySamples; + #endif + + private: + Audio(const Audio&) = delete; + Audio(Audio&&) = delete; + Audio& operator=(const Audio&) = delete; + Audio& operator=(Audio&&) = delete; + }; +} + +#endif // TIA_AUDIO_HXX diff --git a/src/engine/platform/sound/tia/AudioChannel.cpp b/src/engine/platform/sound/tia/AudioChannel.cpp new file mode 100644 index 00000000..c780c172 --- /dev/null +++ b/src/engine/platform/sound/tia/AudioChannel.cpp @@ -0,0 +1,140 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#include "AudioChannel.h" + +namespace TIA { + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::reset() +{ + myAudc = myAudv = myAudf = 0; + myClockEnable = myNoiseFeedback = myNoiseCounterBit4 = myPulseCounterHold = false; + myDivCounter = myPulseCounter = myNoiseCounter = 0; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::phase0() +{ + if (myClockEnable) { + myNoiseCounterBit4 = myNoiseCounter & 0x01; + + switch (myAudc & 0x03) { + case 0x00: + case 0x01: + myPulseCounterHold = false; + break; + + case 0x02: + myPulseCounterHold = (myNoiseCounter & 0x1e) != 0x02; + break; + + case 0x03: + myPulseCounterHold = !myNoiseCounterBit4; + break; + } + + switch (myAudc & 0x03) { + case 0x00: + myNoiseFeedback = + ((myPulseCounter ^ myNoiseCounter) & 0x01) || + !(myNoiseCounter || (myPulseCounter != 0x0a)) || + !(myAudc & 0x0c); + + break; + + default: + myNoiseFeedback = + (((myNoiseCounter & 0x04) ? 1 : 0) ^ (myNoiseCounter & 0x01)) || + myNoiseCounter == 0; + + break; + } + } + + myClockEnable = myDivCounter == myAudf; + + if (myDivCounter == myAudf || myDivCounter == 0x1f) { + myDivCounter = 0; + } else { + ++myDivCounter; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +unsigned char AudioChannel::phase1() +{ + if (myClockEnable) { + bool pulseFeedback = false; + switch (myAudc >> 2) { + case 0x00: + pulseFeedback = + (((myPulseCounter & 0x02) ? 1 : 0) ^ (myPulseCounter & 0x01)) && + (myPulseCounter != 0x0a) && + (myAudc & 0x03); + + break; + + case 0x01: + pulseFeedback = !(myPulseCounter & 0x08); + break; + + case 0x02: + pulseFeedback = !myNoiseCounterBit4; + break; + + case 0x03: + pulseFeedback = !((myPulseCounter & 0x02) || !(myPulseCounter & 0x0e)); + break; + } + + myNoiseCounter >>= 1; + if (myNoiseFeedback) { + myNoiseCounter |= 0x10; + } + + if (!myPulseCounterHold) { + myPulseCounter = ~(myPulseCounter >> 1) & 0x07; + + if (pulseFeedback) { + myPulseCounter |= 0x08; + } + } + } + + return (myPulseCounter & 0x01) * myAudv; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::audc(unsigned char value) +{ + myAudc = value & 0x0f; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::audv(unsigned char value) +{ + myAudv = value & 0x0f; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void AudioChannel::audf(unsigned char value) +{ + myAudf = value & 0x1f; +} + +} \ No newline at end of file diff --git a/src/engine/platform/sound/tia/AudioChannel.h b/src/engine/platform/sound/tia/AudioChannel.h new file mode 100644 index 00000000..942e101a --- /dev/null +++ b/src/engine/platform/sound/tia/AudioChannel.h @@ -0,0 +1,61 @@ +//============================================================================ +// +// SSSS tt lll lll +// SS SS tt ll ll +// SS tttttt eeee ll ll aaaa +// SSSS tt ee ee ll ll aa +// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" +// SS SS tt ee ll ll aa aa +// SSSS ttt eeeee llll llll aaaaa +// +// Copyright (c) 1995-2021 by Bradford W. Mott, Stephen Anthony +// and the Stella Team +// +// See the file "License.txt" for information on usage and redistribution of +// this file, and for a DISCLAIMER OF ALL WARRANTIES. +//============================================================================ + +#ifndef TIA_AUDIO_CHANNEL_HXX +#define TIA_AUDIO_CHANNEL_HXX + +namespace TIA { + class AudioChannel + { + public: + AudioChannel() = default; + + void reset(); + + void phase0(); + + unsigned char phase1(); + + void audc(unsigned char value); + + void audf(unsigned char value); + + void audv(unsigned char value); + + private: + unsigned char myAudc{0}; + unsigned char myAudv{0}; + unsigned char myAudf{0}; + + bool myClockEnable{false}; + bool myNoiseFeedback{false}; + bool myNoiseCounterBit4{false}; + bool myPulseCounterHold{false}; + + unsigned char myDivCounter{0}; + unsigned char myPulseCounter{0}; + unsigned char myNoiseCounter{0}; + + private: + AudioChannel(const AudioChannel&) = delete; + AudioChannel(AudioChannel&&) = delete; + AudioChannel& operator=(const AudioChannel&) = delete; + AudioChannel& operator=(AudioChannel&&) = delete; + }; +} + +#endif // TIA_AUDIO_CHANNEL_HXX diff --git a/src/engine/platform/sound/tia/TIASnd.cpp b/src/engine/platform/sound/tia/TIASnd.cpp deleted file mode 100644 index e2d0568c..00000000 --- a/src/engine/platform/sound/tia/TIASnd.cpp +++ /dev/null @@ -1,377 +0,0 @@ -//============================================================================ -// -// SSSS tt lll lll -// SS SS tt ll ll -// SS tttttt eeee ll ll aaaa -// SSSS tt ee ee ll ll aa -// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" -// SS SS tt ee ll ll aa aa -// SSSS ttt eeeee llll llll aaaaa -// -// Copyright (c) 1995-2016 by Bradford W. Mott, Stephen Anthony -// and the Stella Team -// -// See the file "License.txt" for information on usage and redistribution of -// this file, and for a DISCLAIMER OF ALL WARRANTIES. -// -// $Id: TIASnd.cxx 3239 2015-12-29 19:22:46Z stephena $ -//============================================================================ - -#include "TIATables.h" -#include "TIASnd.h" - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -TIASound::TIASound(int outputFrequency) - : myChannelMode(Hardware2Stereo), - myOutputFrequency(outputFrequency), - myVolumePercentage(100) -{ - reset(); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::reset() -{ - // Fill the polynomials - polyInit(Bit4, 4, 4, 3); - polyInit(Bit5, 5, 5, 3); - polyInit(Bit9, 9, 9, 5); - - // Initialize instance variables - for(int chan = 0; chan <= 1; ++chan) - { - myVolume[chan] = 0; - myDivNCnt[chan] = 0; - myDivNMax[chan] = 0; - myDiv3Cnt[chan] = 3; - myAUDC[chan] = 0; - myAUDF[chan] = 0; - myAUDV[chan] = 0; - myP4[chan] = 0; - myP5[chan] = 0; - myP9[chan] = 0; - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::outputFrequency(int freq) -{ - myOutputFrequency = freq; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -std::string TIASound::channels(unsigned int hardware, bool stereo) -{ - if(hardware == 1) - myChannelMode = Hardware1; - else - myChannelMode = stereo ? Hardware2Stereo : Hardware2Mono; - - switch(myChannelMode) - { - case Hardware1: return "Hardware1"; - case Hardware2Mono: return "Hardware2Mono"; - case Hardware2Stereo: return "Hardware2Stereo"; - default: return ""; - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::set(unsigned short address, unsigned char value) -{ - int chan = ~address & 0x1; - switch(address) - { - case TIARegister::AUDC0: - case TIARegister::AUDC1: - myAUDC[chan] = value & 0x0f; - break; - - case TIARegister::AUDF0: - case TIARegister::AUDF1: - myAUDF[chan] = value & 0x1f; - break; - - case TIARegister::AUDV0: - case TIARegister::AUDV1: - myAUDV[chan] = (value & 0x0f) << AUDV_SHIFT; - break; - - default: - return; - } - - unsigned short newVal = 0; - - // An AUDC value of 0 is a special case - if (myAUDC[chan] == SET_TO_1 || myAUDC[chan] == POLY5_POLY5) - { - // Indicate the clock is zero so no processing will occur, - // and set the output to the selected volume - newVal = 0; - myVolume[chan] = (myAUDV[chan] * myVolumePercentage) / 100; - } - else - { - // Otherwise calculate the 'divide by N' value - newVal = myAUDF[chan] + 1; - - // If bits 2 & 3 are set, then multiply the 'div by n' count by 3 - if((myAUDC[chan] & DIV3_MASK) == DIV3_MASK && myAUDC[chan] != POLY5_DIV3) - newVal *= 3; - } - - // Only reset those channels that have changed - if(newVal != myDivNMax[chan]) - { - // Reset the divide by n counters - myDivNMax[chan] = newVal; - - // If the channel is now volume only or was volume only, - // reset the counter (otherwise let it complete the previous) - if ((myDivNCnt[chan] == 0) || (newVal == 0)) - myDivNCnt[chan] = newVal; - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -unsigned char TIASound::get(unsigned short address) const -{ - switch(address) - { - case TIARegister::AUDC0: return myAUDC[0]; - case TIARegister::AUDC1: return myAUDC[1]; - case TIARegister::AUDF0: return myAUDF[0]; - case TIARegister::AUDF1: return myAUDF[1]; - case TIARegister::AUDV0: return myAUDV[0] >> AUDV_SHIFT; - case TIARegister::AUDV1: return myAUDV[1] >> AUDV_SHIFT; - default: return 0; - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::volume(unsigned int percent) -{ - if(percent <= 100) - myVolumePercentage = percent; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::process(short* buffer, unsigned int samples, DivDispatchOscBuffer** oscBuf) -{ - // Make temporary local copy - unsigned char audc0 = myAUDC[0], audc1 = myAUDC[1]; - unsigned char p5_0 = myP5[0], p5_1 = myP5[1]; - unsigned char div_n_cnt0 = myDivNCnt[0], div_n_cnt1 = myDivNCnt[1]; - short v0 = myVolume[0], v1 = myVolume[1]; - - // Take external volume into account - short audv0 = (myAUDV[0] * myVolumePercentage) / 100, - audv1 = (myAUDV[1] * myVolumePercentage) / 100; - - // Loop until the sample buffer is full - while(samples > 0) - { - // Process channel 0 - if (div_n_cnt0 > 1) - { - div_n_cnt0--; - } - else if (div_n_cnt0 == 1) - { - int prev_bit5 = Bit5[p5_0]; - div_n_cnt0 = myDivNMax[0]; - - // The P5 counter has multiple uses, so we increment it here - p5_0++; - if (p5_0 == POLY5_SIZE) - p5_0 = 0; - - // Check clock modifier for clock tick - if ((audc0 & 0x02) == 0 || - ((audc0 & 0x01) == 0 && Div31[p5_0]) || - ((audc0 & 0x01) == 1 && Bit5[p5_0]) || - ((audc0 & 0x0f) == POLY5_DIV3 && Bit5[p5_0] != prev_bit5)) - { - if (audc0 & 0x04) // Pure modified clock selected - { - if ((audc0 & 0x0f) == POLY5_DIV3) // POLY5 -> DIV3 mode - { - if ( Bit5[p5_0] != prev_bit5 ) - { - myDiv3Cnt[0]--; - if ( !myDiv3Cnt[0] ) - { - myDiv3Cnt[0] = 3; - v0 = v0 ? 0 : audv0; - } - } - } - else - { - // If the output was set turn it off, else turn it on - v0 = v0 ? 0 : audv0; - } - } - else if (audc0 & 0x08) // Check for p5/p9 - { - if (audc0 == POLY9) // Check for poly9 - { - // Increase the poly9 counter - myP9[0]++; - if (myP9[0] == POLY9_SIZE) - myP9[0] = 0; - - v0 = Bit9[myP9[0]] ? audv0 : 0; - } - else if ( audc0 & 0x02 ) - { - v0 = (v0 || audc0 & 0x01) ? 0 : audv0; - } - else // Must be poly5 - { - v0 = Bit5[p5_0] ? audv0 : 0; - } - } - else // Poly4 is the only remaining option - { - // Increase the poly4 counter - myP4[0]++; - if (myP4[0] == POLY4_SIZE) - myP4[0] = 0; - - v0 = Bit4[myP4[0]] ? audv0 : 0; - } - } - } - - // Process channel 1 - if (div_n_cnt1 > 1) - { - div_n_cnt1--; - } - else if (div_n_cnt1 == 1) - { - int prev_bit5 = Bit5[p5_1]; - - div_n_cnt1 = myDivNMax[1]; - - // The P5 counter has multiple uses, so we increment it here - p5_1++; - if (p5_1 == POLY5_SIZE) - p5_1 = 0; - - // Check clock modifier for clock tick - if ((audc1 & 0x02) == 0 || - ((audc1 & 0x01) == 0 && Div31[p5_1]) || - ((audc1 & 0x01) == 1 && Bit5[p5_1]) || - ((audc1 & 0x0f) == POLY5_DIV3 && Bit5[p5_1] != prev_bit5)) - { - if (audc1 & 0x04) // Pure modified clock selected - { - if ((audc1 & 0x0f) == POLY5_DIV3) // POLY5 -> DIV3 mode - { - if ( Bit5[p5_1] != prev_bit5 ) - { - myDiv3Cnt[1]--; - if ( ! myDiv3Cnt[1] ) - { - myDiv3Cnt[1] = 3; - v1 = v1 ? 0 : audv1; - } - } - } - else - { - // If the output was set turn it off, else turn it on - v1 = v1 ? 0 : audv1; - } - } - else if (audc1 & 0x08) // Check for p5/p9 - { - if (audc1 == POLY9) // Check for poly9 - { - // Increase the poly9 counter - myP9[1]++; - if (myP9[1] == POLY9_SIZE) - myP9[1] = 0; - - v1 = Bit9[myP9[1]] ? audv1 : 0; - } - else if ( audc1 & 0x02 ) - { - v1 = (v1 || audc1 & 0x01) ? 0 : audv1; - } - else // Must be poly5 - { - v1 = Bit5[p5_1] ? audv1 : 0; - } - } - else // Poly4 is the only remaining option - { - // Increase the poly4 counter - myP4[1]++; - if (myP4[1] == POLY4_SIZE) - myP4[1] = 0; - - v1 = Bit4[myP4[1]] ? audv1 : 0; - } - } - } - - short byte = v0 + v1; - switch(myChannelMode) - { - case Hardware2Mono: // mono sampling with 2 hardware channels - *(buffer++) = byte; - *(buffer++) = byte; - samples--; - break; - - case Hardware2Stereo: // stereo sampling with 2 hardware channels - *(buffer++) = v0; - *(buffer++) = v1; - samples--; - break; - - case Hardware1: // mono/stereo sampling with only 1 hardware channel - *(buffer++) = (v0 + v1) >> 1; - samples--; - break; - } - - if (oscBuf!=NULL) { - oscBuf[0]->data[oscBuf[0]->needle++]=v0; - oscBuf[1]->data[oscBuf[1]->needle++]=v1; - } - } - - // Save for next round - myP5[0] = p5_0; - myP5[1] = p5_1; - myVolume[0] = v0; - myVolume[1] = v1; - myDivNCnt[0] = div_n_cnt0; - myDivNCnt[1] = div_n_cnt1; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TIASound::polyInit(unsigned char* poly, int size, int f0, int f1) -{ - int mask = (1 << size) - 1, x = mask; - - for(int i = 0; i < mask; i++) - { - int bit0 = ( ( size - f0 ) ? ( x >> ( size - f0 ) ) : x ) & 0x01; - int bit1 = ( ( size - f1 ) ? ( x >> ( size - f1 ) ) : x ) & 0x01; - poly[i] = x & 1; - // calculate next bit - x = ( x >> 1 ) | ( ( bit0 ^ bit1 ) << ( size - 1) ); - } -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -const unsigned char TIASound::Div31[POLY5_SIZE] = { - 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -}; diff --git a/src/engine/platform/sound/tia/TIASnd.h b/src/engine/platform/sound/tia/TIASnd.h deleted file mode 100644 index 78459426..00000000 --- a/src/engine/platform/sound/tia/TIASnd.h +++ /dev/null @@ -1,186 +0,0 @@ -//============================================================================ -// -// SSSS tt lll lll -// SS SS tt ll ll -// SS tttttt eeee ll ll aaaa -// SSSS tt ee ee ll ll aa -// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" -// SS SS tt ee ll ll aa aa -// SSSS ttt eeeee llll llll aaaaa -// -// Copyright (c) 1995-2016 by Bradford W. Mott, Stephen Anthony -// and the Stella Team -// -// See the file "License.txt" for information on usage and redistribution of -// this file, and for a DISCLAIMER OF ALL WARRANTIES. -// -// $Id: TIASnd.hxx 3239 2015-12-29 19:22:46Z stephena $ -//============================================================================ - -#ifndef TIASOUND_HXX -#define TIASOUND_HXX - -#include -#include "../../../dispatch.h" - -/** - This class implements a fairly accurate emulation of the TIA sound - hardware. This class uses code/ideas from z26 and MESS. - - Currently, the sound generation routines work at 31400Hz only. - Resampling can be done by passing in a different output frequency. - - @author Bradford W. Mott, Stephen Anthony, z26 and MESS teams - @version $Id: TIASnd.hxx 3239 2015-12-29 19:22:46Z stephena $ -*/ -class TIASound -{ - public: - /** - Create a new TIA Sound object using the specified output frequency - */ - TIASound(int outputFrequency = 31400); - - public: - /** - Reset the sound emulation to its power-on state - */ - void reset(); - - /** - Set the frequency output samples should be generated at - */ - void outputFrequency(int freq); - - /** - Selects the number of audio channels per sample. There are two factors - to consider: hardware capability and desired mixing. - - @param hardware The number of channels supported by the sound system - @param stereo Whether to output the internal sound signals into 1 - or 2 channels - - @return Status of the channel configuration used - */ - std::string channels(unsigned int hardware, bool stereo); - - public: - /** - Sets the specified sound register to the given value - - @param address Register address - @param value Value to store in the register - */ - void set(unsigned short address, unsigned char value); - - /** - Gets the specified sound register's value - - @param address Register address - */ - unsigned char get(unsigned short address) const; - - /** - Create sound samples based on the current sound register settings - in the specified buffer. NOTE: If channels is set to stereo then - the buffer will need to be twice as long as the number of samples. - - @param buffer The location to store generated samples - @param samples The number of samples to generate - */ - void process(short* buffer, unsigned int samples, DivDispatchOscBuffer** oscBuf=NULL); - - /** - Set the volume of the samples created (0-100) - */ - void volume(unsigned int percent); - - private: - void polyInit(unsigned char* poly, int size, int f0, int f1); - - private: - // Definitions for AUDCx (15, 16) - enum AUDCxRegister - { - SET_TO_1 = 0x00, // 0000 - POLY4 = 0x01, // 0001 - DIV31_POLY4 = 0x02, // 0010 - POLY5_POLY4 = 0x03, // 0011 - PURE1 = 0x04, // 0100 - PURE2 = 0x05, // 0101 - DIV31_PURE = 0x06, // 0110 - POLY5_2 = 0x07, // 0111 - POLY9 = 0x08, // 1000 - POLY5 = 0x09, // 1001 - DIV31_POLY5 = 0x0a, // 1010 - POLY5_POLY5 = 0x0b, // 1011 - DIV3_PURE = 0x0c, // 1100 - DIV3_PURE2 = 0x0d, // 1101 - DIV93_PURE = 0x0e, // 1110 - POLY5_DIV3 = 0x0f // 1111 - }; - - enum { - POLY4_SIZE = 0x000f, - POLY5_SIZE = 0x001f, - POLY9_SIZE = 0x01ff, - DIV3_MASK = 0x0c, - AUDV_SHIFT = 10 // shift 2 positions for AUDV, - // then another 8 for 16-bit sound - }; - - enum ChannelMode { - Hardware2Mono, // mono sampling with 2 hardware channels - Hardware2Stereo, // stereo sampling with 2 hardware channels - Hardware1 // mono/stereo sampling with only 1 hardware channel - }; - - private: - // Structures to hold the 6 tia sound control bytes - unsigned char myAUDC[2]; // AUDCx (15, 16) - unsigned char myAUDF[2]; // AUDFx (17, 18) - short myAUDV[2]; // AUDVx (19, 1A) - - short myVolume[2]; // Last output volume for each channel - - unsigned char myP4[2]; // Position pointer for the 4-bit POLY array - unsigned char myP5[2]; // Position pointer for the 5-bit POLY array - unsigned short myP9[2]; // Position pointer for the 9-bit POLY array - - unsigned char myDivNCnt[2]; // Divide by n counter. one for each channel - unsigned char myDivNMax[2]; // Divide by n maximum, one for each channel - unsigned char myDiv3Cnt[2]; // Div 3 counter, used for POLY5_DIV3 mode - - ChannelMode myChannelMode; - int myOutputFrequency; - unsigned int myVolumePercentage; - - /* - Initialize the bit patterns for the polynomials (at runtime). - - The 4bit and 5bit patterns are the identical ones used in the tia chip. - Though the patterns could be packed with 8 bits per byte, using only a - single bit per byte keeps the math simple, which is important for - efficient processing. - */ - unsigned char Bit4[POLY4_SIZE]; - unsigned char Bit5[POLY5_SIZE]; - unsigned char Bit9[POLY9_SIZE]; - - /* - The 'Div by 31' counter is treated as another polynomial because of - the way it operates. It does not have a 50% duty cycle, but instead - has a 13:18 ratio (of course, 13+18 = 31). This could also be - implemented by using counters. - */ - static const unsigned char Div31[POLY5_SIZE]; - - private: - // Following constructors and assignment operators not supported - TIASound(const TIASound&) = delete; - TIASound(TIASound&&) = delete; - TIASound& operator=(const TIASound&) = delete; - TIASound& operator=(TIASound&&) = delete; -}; - -#endif diff --git a/src/engine/platform/sound/tia/TIATables.h b/src/engine/platform/sound/tia/TIATables.h deleted file mode 100644 index 782a24de..00000000 --- a/src/engine/platform/sound/tia/TIATables.h +++ /dev/null @@ -1,219 +0,0 @@ -//============================================================================ -// -// SSSS tt lll lll -// SS SS tt ll ll -// SS tttttt eeee ll ll aaaa -// SSSS tt ee ee ll ll aa -// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" -// SS SS tt ee ll ll aa aa -// SSSS ttt eeeee llll llll aaaaa -// -// Copyright (c) 1995-2016 by Bradford W. Mott, Stephen Anthony -// and the Stella Team -// -// See the file "License.txt" for information on usage and redistribution of -// this file, and for a DISCLAIMER OF ALL WARRANTIES. -// -// $Id: TIATables.hxx 3239 2015-12-29 19:22:46Z stephena $ -//============================================================================ - -#ifndef TIA_TABLES_HXX -#define TIA_TABLES_HXX - -enum TIABit { - P0Bit = 0x01, // Bit for Player 0 - M0Bit = 0x02, // Bit for Missle 0 - P1Bit = 0x04, // Bit for Player 1 - M1Bit = 0x08, // Bit for Missle 1 - BLBit = 0x10, // Bit for Ball - PFBit = 0x20, // Bit for Playfield - ScoreBit = 0x40, // Bit for Playfield score mode - PriorityBit = 0x80 // Bit for Playfield priority -}; - -enum TIAColor { - BKColor = 0, // Color index for Background - PFColor = 1, // Color index for Playfield - P0Color = 2, // Color index for Player 0 - P1Color = 3, // Color index for Player 1 - M0Color = 4, // Color index for Missle 0 - M1Color = 5, // Color index for Missle 1 - BLColor = 6, // Color index for Ball - HBLANKColor = 7 // Color index for HMove blank area -}; - -enum CollisionBit -{ - Cx_M0P1 = 1 << 0, // Missle0 - Player1 collision - Cx_M0P0 = 1 << 1, // Missle0 - Player0 collision - Cx_M1P0 = 1 << 2, // Missle1 - Player0 collision - Cx_M1P1 = 1 << 3, // Missle1 - Player1 collision - Cx_P0PF = 1 << 4, // Player0 - Playfield collision - Cx_P0BL = 1 << 5, // Player0 - Ball collision - Cx_P1PF = 1 << 6, // Player1 - Playfield collision - Cx_P1BL = 1 << 7, // Player1 - Ball collision - Cx_M0PF = 1 << 8, // Missle0 - Playfield collision - Cx_M0BL = 1 << 9, // Missle0 - Ball collision - Cx_M1PF = 1 << 10, // Missle1 - Playfield collision - Cx_M1BL = 1 << 11, // Missle1 - Ball collision - Cx_BLPF = 1 << 12, // Ball - Playfield collision - Cx_P0P1 = 1 << 13, // Player0 - Player1 collision - Cx_M0M1 = 1 << 14 // Missle0 - Missle1 collision -}; - -// TIA Write/Read register names -enum TIARegister { - VSYNC = 0x00, // Write: vertical sync set-clear (D1) - VBLANK = 0x01, // Write: vertical blank set-clear (D7-6,D1) - WSYNC = 0x02, // Write: wait for leading edge of hrz. blank (strobe) - RSYNC = 0x03, // Write: reset hrz. sync counter (strobe) - NUSIZ0 = 0x04, // Write: number-size player-missle 0 (D5-0) - NUSIZ1 = 0x05, // Write: number-size player-missle 1 (D5-0) - COLUP0 = 0x06, // Write: color-lum player 0 (D7-1) - COLUP1 = 0x07, // Write: color-lum player 1 (D7-1) - COLUPF = 0x08, // Write: color-lum playfield (D7-1) - COLUBK = 0x09, // Write: color-lum background (D7-1) - CTRLPF = 0x0a, // Write: cntrl playfield ballsize & coll. (D5-4,D2-0) - REFP0 = 0x0b, // Write: reflect player 0 (D3) - REFP1 = 0x0c, // Write: reflect player 1 (D3) - PF0 = 0x0d, // Write: playfield register byte 0 (D7-4) - PF1 = 0x0e, // Write: playfield register byte 1 (D7-0) - PF2 = 0x0f, // Write: playfield register byte 2 (D7-0) - RESP0 = 0x10, // Write: reset player 0 (strobe) - RESP1 = 0x11, // Write: reset player 1 (strobe) - RESM0 = 0x12, // Write: reset missle 0 (strobe) - RESM1 = 0x13, // Write: reset missle 1 (strobe) - RESBL = 0x14, // Write: reset ball (strobe) - AUDC0 = 0x15, // Write: audio control 0 (D3-0) - AUDC1 = 0x16, // Write: audio control 1 (D4-0) - AUDF0 = 0x17, // Write: audio frequency 0 (D4-0) - AUDF1 = 0x18, // Write: audio frequency 1 (D3-0) - AUDV0 = 0x19, // Write: audio volume 0 (D3-0) - AUDV1 = 0x1a, // Write: audio volume 1 (D3-0) - GRP0 = 0x1b, // Write: graphics player 0 (D7-0) - GRP1 = 0x1c, // Write: graphics player 1 (D7-0) - ENAM0 = 0x1d, // Write: graphics (enable) missle 0 (D1) - ENAM1 = 0x1e, // Write: graphics (enable) missle 1 (D1) - ENABL = 0x1f, // Write: graphics (enable) ball (D1) - HMP0 = 0x20, // Write: horizontal motion player 0 (D7-4) - HMP1 = 0x21, // Write: horizontal motion player 1 (D7-4) - HMM0 = 0x22, // Write: horizontal motion missle 0 (D7-4) - HMM1 = 0x23, // Write: horizontal motion missle 1 (D7-4) - HMBL = 0x24, // Write: horizontal motion ball (D7-4) - VDELP0 = 0x25, // Write: vertical delay player 0 (D0) - VDELP1 = 0x26, // Write: vertical delay player 1 (D0) - VDELBL = 0x27, // Write: vertical delay ball (D0) - RESMP0 = 0x28, // Write: reset missle 0 to player 0 (D1) - RESMP1 = 0x29, // Write: reset missle 1 to player 1 (D1) - HMOVE = 0x2a, // Write: apply horizontal motion (strobe) - HMCLR = 0x2b, // Write: clear horizontal motion registers (strobe) - CXCLR = 0x2c, // Write: clear collision latches (strobe) - - CXM0P = 0x00, // Read collision: D7=(M0,P1); D6=(M0,P0) - CXM1P = 0x01, // Read collision: D7=(M1,P0); D6=(M1,P1) - CXP0FB = 0x02, // Read collision: D7=(P0,PF); D6=(P0,BL) - CXP1FB = 0x03, // Read collision: D7=(P1,PF); D6=(P1,BL) - CXM0FB = 0x04, // Read collision: D7=(M0,PF); D6=(M0,BL) - CXM1FB = 0x05, // Read collision: D7=(M1,PF); D6=(M1,BL) - CXBLPF = 0x06, // Read collision: D7=(BL,PF); D6=(unused) - CXPPMM = 0x07, // Read collision: D7=(P0,P1); D6=(M0,M1) - INPT0 = 0x08, // Read pot port: D7 - INPT1 = 0x09, // Read pot port: D7 - INPT2 = 0x0a, // Read pot port: D7 - INPT3 = 0x0b, // Read pot port: D7 - INPT4 = 0x0c, // Read P1 joystick trigger: D7 - INPT5 = 0x0d // Read P2 joystick trigger: D7 -}; - -/** - The TIA class uses some static tables that aren't dependent on the actual - TIA state. For code organization, it's better to place that functionality - here. - - @author Stephen Anthony - @version $Id: TIATables.hxx 3239 2015-12-29 19:22:46Z stephena $ -*/ -class TIATables -{ - public: - /** - Compute all static tables used by the TIA - */ - static void computeAllTables(); - - // Player mask table - // [suppress mode][nusiz][pixel] - static unsigned char PxMask[2][8][320]; - - // Missle mask table (entries are true or false) - // [number][size][pixel] - // There are actually only 4 possible size combinations on a real system - // The fifth size is used for simulating the starfield effect in - // Cosmic Ark and Stay Frosty - static unsigned char MxMask[8][5][320]; - - // Ball mask table (entries are true or false) - // [size][pixel] - static unsigned char BLMask[4][320]; - - // Playfield mask table for reflected and non-reflected playfields - // [reflect, pixel] - static unsigned int PFMask[2][160]; - - // A mask table which can be used when an object is disabled - static unsigned char DisabledMask[640]; - - // Used to set the collision register to the correct value - static unsigned short CollisionMask[64]; - - // Indicates the update delay associated with poking at a TIA address - static const short PokeDelay[64]; - -#if 0 - // Used to convert value written in a motion register into - // its internal representation - static const int CompleteMotion[76][16]; -#endif - - // Indicates if HMOVE blanks should occur for the corresponding cycle - static const bool HMOVEBlankEnableCycles[76]; - - // Used to reflect a players graphics - static unsigned char GRPReflect[256]; - - // Indicates if player is being reset during delay, display or other times - // [nusiz][old pixel][new pixel] - static signed char PxPosResetWhen[8][160][160]; - - private: - // Compute the collision decode table - static void buildCollisionMaskTable(); - - // Compute the player mask table - static void buildPxMaskTable(); - - // Compute the missle mask table - static void buildMxMaskTable(); - - // Compute the ball mask table - static void buildBLMaskTable(); - - // Compute playfield mask table - static void buildPFMaskTable(); - - // Compute the player reflect table - static void buildGRPReflectTable(); - - // Compute the player position reset when table - static void buildPxPosResetWhenTable(); - - private: - // Following constructors and assignment operators not supported - TIATables() = delete; - TIATables(const TIATables&) = delete; - TIATables(TIATables&&) = delete; - TIATables& operator=(const TIATables&) = delete; - TIATables& operator=(TIATables&&) = delete; -}; - -#endif diff --git a/src/engine/platform/sound/ymfm/ymfm_opz.cpp b/src/engine/platform/sound/ymfm/ymfm_opz.cpp index 94123bf6..37c6a5fc 100644 --- a/src/engine/platform/sound/ymfm/ymfm_opz.cpp +++ b/src/engine/platform/sound/ymfm/ymfm_opz.cpp @@ -292,6 +292,10 @@ bool opz_registers::write(uint16_t index, uint8_t data, uint32_t &channel, uint3 // note from tildearrow: // - are you kidding? I have to write to this "load preset" register before keying on? + // another note from tildearrow: + // - see https://github.com/110-kenichi/ymfm/blob/main/src/ymfm_opz.cpp + // - is 0x08 the actual key on register just like OPM? + // - if so then what's bit 5? if ((index & 0xf8) == 0x20 /*&& bitfield(index, 0, 3) == bitfield(m_regdata[0x08], 0, 3)*/) { channel = bitfield(index, 0, 3); diff --git a/src/engine/platform/su.cpp b/src/engine/platform/su.cpp index 13650345..730f8d89 100644 --- a/src/engine/platform/su.cpp +++ b/src/engine/platform/su.cpp @@ -581,7 +581,7 @@ int DivPlatformSoundUnit::init(DivEngine* p, int channels, int sugRate, unsigned su=new SoundUnit(); setFlags(flags); reset(); - return 6; + return 8; } void DivPlatformSoundUnit::quit() { diff --git a/src/engine/platform/swan.cpp b/src/engine/platform/swan.cpp index 7f1e10a0..7ac26629 100644 --- a/src/engine/platform/swan.cpp +++ b/src/engine/platform/swan.cpp @@ -59,7 +59,8 @@ void DivPlatformSwan::acquire(short* bufL, short* bufR, size_t start, size_t len DivSample* s=parent->getSample(dacSample); if (s->samples<=0) { dacSample=-1; - continue; + dacPeriod=0; + break; } rWrite(0x09,(unsigned char)s->data8[dacPos++]+0x80); if (s->isLoopable() && dacPos>=s->getEndPosition()) { diff --git a/src/engine/platform/tia.cpp b/src/engine/platform/tia.cpp index 82d03270..796c9448 100644 --- a/src/engine/platform/tia.cpp +++ b/src/engine/platform/tia.cpp @@ -22,7 +22,7 @@ #include #include -#define rWrite(a,v) if (!skipRegisterWrites) {tia.set(a,v); regPool[((a)-0x15)&0x0f]=v; if (dumpWrites) {addWrite(a,v);} } +#define rWrite(a,v) if (!skipRegisterWrites) {tia.write(a,v); regPool[((a)-0x15)&0x0f]=v; if (dumpWrites) {addWrite(a,v);} } const char* regCheatSheetTIA[]={ "AUDC0", "15", @@ -39,7 +39,22 @@ const char** DivPlatformTIA::getRegisterSheet() { } void DivPlatformTIA::acquire(short* bufL, short* bufR, size_t start, size_t len) { - tia.process(bufL+start,len,oscBuf); + for (size_t h=start; h>1; + } else { + bufL[h]=tia.myCurrentSample[0]; + } + if (++chanOscCounter>=114) { + chanOscCounter=0; + oscBuf[0]->data[oscBuf[0]->needle++]=tia.myChannelOut[0]; + oscBuf[1]->data[oscBuf[1]->needle++]=tia.myChannelOut[1]; + } + } } unsigned char DivPlatformTIA::dealWithFreq(unsigned char shape, int base, int pitch) { @@ -300,7 +315,7 @@ int DivPlatformTIA::getRegisterPoolSize() { } void DivPlatformTIA::reset() { - tia.reset(); + tia.reset(mixingType); memset(regPool,0,16); for (int i=0; i<2; i++) { chan[i]=DivPlatformTIA::Channel(); @@ -309,8 +324,12 @@ void DivPlatformTIA::reset() { } } +float DivPlatformTIA::getPostAmp() { + return 0.5f; +} + bool DivPlatformTIA::isStereo() { - return false; + return (mixingType==2); } bool DivPlatformTIA::keyOffAffectsArp(int ch) { @@ -333,25 +352,28 @@ void DivPlatformTIA::poke(std::vector& wlist) { void DivPlatformTIA::setFlags(unsigned int flags) { if (flags&1) { - rate=31250; + rate=COLOR_PAL*4.0/5.0; } else { - rate=31468; + rate=COLOR_NTSC; } chipClock=rate; + mixingType=(flags>>1)&3; for (int i=0; i<2; i++) { - oscBuf[i]->rate=rate; + oscBuf[i]->rate=rate/114; } + tia.reset(mixingType); } int DivPlatformTIA::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { parent=p; dumpWrites=false; skipRegisterWrites=false; + mixingType=0; + chanOscCounter=0; for (int i=0; i<2; i++) { isMuted[i]=false; oscBuf[i]=new DivDispatchOscBuffer; } - tia.channels(1,false); setFlags(flags); reset(); return 2; diff --git a/src/engine/platform/tia.h b/src/engine/platform/tia.h index 16817536..b838e068 100644 --- a/src/engine/platform/tia.h +++ b/src/engine/platform/tia.h @@ -22,7 +22,7 @@ #include "../dispatch.h" #include "../macroInt.h" #include -#include "sound/tia/TIASnd.h" +#include "sound/tia/Audio.h" class DivPlatformTIA: public DivDispatch { protected: @@ -42,7 +42,9 @@ class DivPlatformTIA: public DivDispatch { Channel chan[2]; DivDispatchOscBuffer* oscBuf[2]; bool isMuted[2]; - TIASound tia; + unsigned char mixingType; + unsigned char chanOscCounter; + TIA::Audio tia; unsigned char regPool[16]; friend void putDispatchChan(void*,int,int); @@ -61,6 +63,7 @@ class DivPlatformTIA: public DivDispatch { void tick(bool sysTick=true); void muteChannel(int ch, bool mute); void setFlags(unsigned int flags); + float getPostAmp(); bool isStereo(); bool keyOffAffectsArp(int ch); void notifyInsDeletion(void* ins); diff --git a/src/engine/platform/tx81z.cpp b/src/engine/platform/tx81z.cpp index 0fe0265e..1c6470fc 100644 --- a/src/engine/platform/tx81z.cpp +++ b/src/engine/platform/tx81z.cpp @@ -906,6 +906,7 @@ void DivPlatformTX81Z::reset() { pmDepth=0x7f; //rWrite(0x18,0x10); + immWrite(0x18,0x00); // LFO Freq Off immWrite(0x19,amDepth); immWrite(0x19,0x80|pmDepth); //rWrite(0x1b,0x00); diff --git a/src/engine/platform/vrc6.cpp b/src/engine/platform/vrc6.cpp index 2500ce19..6e5e0d73 100644 --- a/src/engine/platform/vrc6.cpp +++ b/src/engine/platform/vrc6.cpp @@ -195,7 +195,7 @@ void DivPlatformVRC6::tick(bool sysTick) { if (chan[i].freq<0) chan[i].freq=0; if (chan[i].keyOff) { chWrite(i,2,0); - } else { + } else if (chan[i].active) { chWrite(i,1,chan[i].freq&0xff); chWrite(i,2,0x80|((chan[i].freq>>8)&0xf)); } diff --git a/src/engine/platform/ym2203.cpp b/src/engine/platform/ym2203.cpp index 178ef05b..1c348800 100644 --- a/src/engine/platform/ym2203.cpp +++ b/src/engine/platform/ym2203.cpp @@ -273,6 +273,10 @@ void DivPlatformYM2203::tick(bool sysTick) { chan[i].state.fb=chan[i].std.fb.val; rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); } + if (chan[i].std.ex4.had && chan[i].active) { + chan[i].opMask=chan[i].std.ex4.val&15; + chan[i].opMaskChanged=true; + } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -385,8 +389,9 @@ void DivPlatformYM2203::tick(bool sysTick) { immWrite(chanOffs[i]+ADDR_FREQ,chan[i].freq&0xff); chan[i].freqChanged=false; } - if (chan[i].keyOn) { - immWrite(0x28,0xf0|konOffs[i]); + if (chan[i].keyOn || chan[i].opMaskChanged) { + immWrite(0x28,(chan[i].opMask<<4)|konOffs[i]); + chan[i].opMaskChanged=false; chan[i].keyOn=false; } } @@ -409,6 +414,11 @@ int DivPlatformYM2203::dispatch(DivCommand c) { if (chan[c.chan].insChanged) { chan[c.chan].state=ins->fm; + chan[c.chan].opMask= + (chan[c.chan].state.op[0].enable?1:0)| + (chan[c.chan].state.op[2].enable?2:0)| + (chan[c.chan].state.op[1].enable?4:0)| + (chan[c.chan].state.op[3].enable?8:0); } for (int i=0; i<4; i++) { diff --git a/src/engine/platform/ym2203.h b/src/engine/platform/ym2203.h index 0395c9d0..75660365 100644 --- a/src/engine/platform/ym2203.h +++ b/src/engine/platform/ym2203.h @@ -43,9 +43,9 @@ class DivPlatformYM2203: public DivPlatformOPN { DivInstrumentFM state; unsigned char freqH, freqL; int freq, baseFreq, pitch, pitch2, portaPauseFreq, note, ins; - unsigned char psgMode, autoEnvNum, autoEnvDen; + unsigned char psgMode, autoEnvNum, autoEnvDen, opMask; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset, opMaskChanged; int vol, outVol; int sample; DivMacroInt std; @@ -66,6 +66,7 @@ class DivPlatformYM2203: public DivPlatformOPN { psgMode(1), autoEnvNum(0), autoEnvDen(0), + opMask(15), active(false), insChanged(true), freqChanged(false), @@ -75,6 +76,7 @@ class DivPlatformYM2203: public DivPlatformOPN { inPorta(false), furnacePCM(false), hardReset(false), + opMaskChanged(false), vol(0), outVol(15), sample(-1) {} diff --git a/src/engine/platform/ym2203ext.cpp b/src/engine/platform/ym2203ext.cpp index 3ff24eb7..c7080d43 100644 --- a/src/engine/platform/ym2203ext.cpp +++ b/src/engine/platform/ym2203ext.cpp @@ -59,6 +59,7 @@ int DivPlatformYM2203Ext::dispatch(DivCommand c) { rWrite(baseAddr+0x70,op.d2r&31); rWrite(baseAddr+0x80,(op.rr&15)|(op.sl<<4)); rWrite(baseAddr+0x90,op.ssgEnv&15); + opChan[ch].mask=op.enable; } if (opChan[ch].insChanged) { // TODO how does this work? rWrite(chanOffs[2]+0xb0,(ins->fm.alg&7)|(ins->fm.fb<<3)); @@ -358,7 +359,7 @@ void DivPlatformYM2203Ext::tick(bool sysTick) { bool writeSomething=false; unsigned char writeMask=2; for (int i=0; i<4; i++) { - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn || opChan[i].keyOff) { writeSomething=true; writeMask&=~(1<<(4+i)); @@ -395,10 +396,12 @@ void DivPlatformYM2203Ext::tick(bool sysTick) { immWrite(opChanOffsH[i],opChan[i].freq>>8); immWrite(opChanOffsL[i],opChan[i].freq&0xff); } - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn) { writeNoteOn=true; - writeMask|=1<<(4+i); + if (opChan[i].mask) { + writeMask|=1<<(4+i); + } opChan[i].keyOn=false; } } diff --git a/src/engine/platform/ym2203ext.h b/src/engine/platform/ym2203ext.h index 1a398d1a..d25ca45d 100644 --- a/src/engine/platform/ym2203ext.h +++ b/src/engine/platform/ym2203ext.h @@ -27,12 +27,12 @@ class DivPlatformYM2203Ext: public DivPlatformYM2203 { unsigned char freqH, freqL; int freq, baseFreq, pitch, pitch2, portaPauseFreq, ins; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, mask; int vol; unsigned char pan; // UGLY OpChannel(): freqH(0), freqL(0), freq(0), baseFreq(0), pitch(0), pitch2(0), portaPauseFreq(0), ins(-1), active(false), insChanged(true), freqChanged(false), keyOn(false), keyOff(false), portaPause(false), - inPorta(false), vol(0), pan(3) {} + inPorta(false), mask(true), vol(0), pan(3) {} }; OpChannel opChan[4]; bool isOpMuted[4]; diff --git a/src/engine/platform/ym2608.cpp b/src/engine/platform/ym2608.cpp index 244e4a16..ec90c1c6 100644 --- a/src/engine/platform/ym2608.cpp +++ b/src/engine/platform/ym2608.cpp @@ -441,6 +441,10 @@ void DivPlatformYM2608::tick(bool sysTick) { chan[i].state.ams=chan[i].std.ams.val; 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].std.ex4.had && chan[i].active) { + chan[i].opMask=chan[i].std.ex4.val&15; + chan[i].opMaskChanged=true; + } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -581,8 +585,9 @@ void DivPlatformYM2608::tick(bool sysTick) { immWrite(chanOffs[i]+ADDR_FREQ,chan[i].freq&0xff); chan[i].freqChanged=false; } - if (chan[i].keyOn) { - immWrite(0x28,0xf0|konOffs[i]); + if (chan[i].keyOn || chan[i].opMaskChanged) { + immWrite(0x28,(chan[i].opMask<<4)|konOffs[i]); + chan[i].opMaskChanged=false; chan[i].keyOn=false; } } @@ -683,6 +688,11 @@ int DivPlatformYM2608::dispatch(DivCommand c) { if (chan[c.chan].insChanged) { chan[c.chan].state=ins->fm; + chan[c.chan].opMask= + (chan[c.chan].state.op[0].enable?1:0)| + (chan[c.chan].state.op[2].enable?2:0)| + (chan[c.chan].state.op[1].enable?4:0)| + (chan[c.chan].state.op[3].enable?8:0); } for (int i=0; i<4; i++) { diff --git a/src/engine/platform/ym2608.h b/src/engine/platform/ym2608.h index 7a471b8b..ed850bf8 100644 --- a/src/engine/platform/ym2608.h +++ b/src/engine/platform/ym2608.h @@ -48,9 +48,9 @@ class DivPlatformYM2608: public DivPlatformOPN { DivInstrumentFM state; unsigned char freqH, freqL; int freq, baseFreq, pitch, pitch2, portaPauseFreq, note, ins; - unsigned char psgMode, autoEnvNum, autoEnvDen; + unsigned char psgMode, autoEnvNum, autoEnvDen, opMask; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset, opMaskChanged; int vol, outVol; int sample; unsigned char pan; @@ -72,6 +72,7 @@ class DivPlatformYM2608: public DivPlatformOPN { psgMode(1), autoEnvNum(0), autoEnvDen(0), + opMask(15), active(false), insChanged(true), freqChanged(false), @@ -81,6 +82,7 @@ class DivPlatformYM2608: public DivPlatformOPN { inPorta(false), furnacePCM(false), hardReset(false), + opMaskChanged(false), vol(0), outVol(15), sample(-1), diff --git a/src/engine/platform/ym2608ext.cpp b/src/engine/platform/ym2608ext.cpp index 63503ccc..c6d7e03b 100644 --- a/src/engine/platform/ym2608ext.cpp +++ b/src/engine/platform/ym2608ext.cpp @@ -59,6 +59,7 @@ int DivPlatformYM2608Ext::dispatch(DivCommand c) { rWrite(baseAddr+0x70,op.d2r&31); rWrite(baseAddr+0x80,(op.rr&15)|(op.sl<<4)); rWrite(baseAddr+0x90,op.ssgEnv&15); + opChan[ch].mask=op.enable; } if (opChan[ch].insChanged) { // TODO how does this work? rWrite(chanOffs[2]+0xb0,(ins->fm.alg&7)|(ins->fm.fb<<3)); @@ -358,7 +359,7 @@ void DivPlatformYM2608Ext::tick(bool sysTick) { bool writeSomething=false; unsigned char writeMask=2; for (int i=0; i<4; i++) { - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn || opChan[i].keyOff) { writeSomething=true; writeMask&=~(1<<(4+i)); @@ -395,10 +396,12 @@ void DivPlatformYM2608Ext::tick(bool sysTick) { immWrite(opChanOffsH[i],opChan[i].freq>>8); immWrite(opChanOffsL[i],opChan[i].freq&0xff); } - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn) { writeNoteOn=true; - writeMask|=1<<(4+i); + if (opChan[i].mask) { + writeMask|=1<<(4+i); + } opChan[i].keyOn=false; } } diff --git a/src/engine/platform/ym2608ext.h b/src/engine/platform/ym2608ext.h index bc3d4f99..21c8a35c 100644 --- a/src/engine/platform/ym2608ext.h +++ b/src/engine/platform/ym2608ext.h @@ -27,12 +27,12 @@ class DivPlatformYM2608Ext: public DivPlatformYM2608 { unsigned char freqH, freqL; int freq, baseFreq, pitch, pitch2, portaPauseFreq, ins; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, mask; int vol; unsigned char pan; // UGLY OpChannel(): freqH(0), freqL(0), freq(0), baseFreq(0), pitch(0), pitch2(0), portaPauseFreq(0), ins(-1), active(false), insChanged(true), freqChanged(false), keyOn(false), keyOff(false), portaPause(false), - inPorta(false), vol(0), pan(3) {} + inPorta(false), mask(true), vol(0), pan(3) {} }; OpChannel opChan[4]; bool isOpMuted[4]; diff --git a/src/engine/platform/ym2610.cpp b/src/engine/platform/ym2610.cpp index 064b49f2..6a8509d5 100644 --- a/src/engine/platform/ym2610.cpp +++ b/src/engine/platform/ym2610.cpp @@ -482,6 +482,10 @@ void DivPlatformYM2610::tick(bool sysTick) { chan[i].state.ams=chan[i].std.ams.val; 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].std.ex4.had && chan[i].active) { + chan[i].opMask=chan[i].std.ex4.val&15; + chan[i].opMaskChanged=true; + } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -618,8 +622,9 @@ void DivPlatformYM2610::tick(bool sysTick) { immWrite(chanOffs[i]+ADDR_FREQ,chan[i].freq&0xff); chan[i].freqChanged=false; } - if (chan[i].keyOn) { - immWrite(0x28,0xf0|konOffs[i]); + if (chan[i].keyOn || chan[i].opMaskChanged) { + immWrite(0x28,(chan[i].opMask<<4)|konOffs[i]); + chan[i].opMaskChanged=false; chan[i].keyOn=false; } } @@ -727,6 +732,11 @@ int DivPlatformYM2610::dispatch(DivCommand c) { if (chan[c.chan].insChanged) { chan[c.chan].state=ins->fm; + chan[c.chan].opMask= + (chan[c.chan].state.op[0].enable?1:0)| + (chan[c.chan].state.op[2].enable?2:0)| + (chan[c.chan].state.op[1].enable?4:0)| + (chan[c.chan].state.op[3].enable?8:0); } for (int i=0; i<4; i++) { diff --git a/src/engine/platform/ym2610.h b/src/engine/platform/ym2610.h index 5e22ed2a..4f1a1664 100644 --- a/src/engine/platform/ym2610.h +++ b/src/engine/platform/ym2610.h @@ -73,10 +73,10 @@ class DivPlatformYM2610: public DivPlatformYM2610Base { int freq, baseFreq, pitch, pitch2, portaPauseFreq, note, ins; unsigned char psgMode, autoEnvNum, autoEnvDen; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset, opMaskChanged; int vol, outVol; int sample; - unsigned char pan; + unsigned char pan, opMask; DivMacroInt std; void macroInit(DivInstrument* which) { std.init(which); @@ -104,10 +104,12 @@ class DivPlatformYM2610: public DivPlatformYM2610Base { inPorta(false), furnacePCM(false), hardReset(false), + opMaskChanged(false), vol(0), outVol(15), sample(-1), - pan(3) {} + pan(3), + opMask(15) {} }; Channel chan[14]; DivDispatchOscBuffer* oscBuf[14]; diff --git a/src/engine/platform/ym2610b.cpp b/src/engine/platform/ym2610b.cpp index 8d283374..31cd7b3f 100644 --- a/src/engine/platform/ym2610b.cpp +++ b/src/engine/platform/ym2610b.cpp @@ -465,6 +465,10 @@ void DivPlatformYM2610B::tick(bool sysTick) { chan[i].state.ams=chan[i].std.ams.val; 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].std.ex4.had && chan[i].active) { + chan[i].opMask=chan[i].std.ex4.val&15; + chan[i].opMaskChanged=true; + } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -600,8 +604,9 @@ void DivPlatformYM2610B::tick(bool sysTick) { immWrite(chanOffs[i]+ADDR_FREQ,chan[i].freq&0xff); chan[i].freqChanged=false; } - if (chan[i].keyOn) { - immWrite(0x28,0xf0|konOffs[i]); + if (chan[i].keyOn || chan[i].opMaskChanged) { + immWrite(0x28,(chan[i].opMask<<4)|konOffs[i]); + chan[i].opMaskChanged=false; chan[i].keyOn=false; } } @@ -709,6 +714,11 @@ int DivPlatformYM2610B::dispatch(DivCommand c) { if (chan[c.chan].insChanged) { chan[c.chan].state=ins->fm; + chan[c.chan].opMask= + (chan[c.chan].state.op[0].enable?1:0)| + (chan[c.chan].state.op[2].enable?2:0)| + (chan[c.chan].state.op[1].enable?4:0)| + (chan[c.chan].state.op[3].enable?8:0); } for (int i=0; i<4; i++) { diff --git a/src/engine/platform/ym2610b.h b/src/engine/platform/ym2610b.h index 703f8dd4..1d5fc1f3 100644 --- a/src/engine/platform/ym2610b.h +++ b/src/engine/platform/ym2610b.h @@ -40,10 +40,10 @@ class DivPlatformYM2610B: public DivPlatformYM2610Base { int freq, baseFreq, pitch, pitch2, portaPauseFreq, note, ins; unsigned char psgMode, autoEnvNum, autoEnvDen; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset, opMaskChanged; int vol, outVol; int sample; - unsigned char pan; + unsigned char pan, opMask; DivMacroInt std; void macroInit(DivInstrument* which) { std.init(which); @@ -71,10 +71,12 @@ class DivPlatformYM2610B: public DivPlatformYM2610Base { inPorta(false), furnacePCM(false), hardReset(false), + opMaskChanged(false), vol(0), outVol(15), sample(-1), - pan(3) {} + pan(3), + opMask(15) {} }; Channel chan[16]; DivDispatchOscBuffer* oscBuf[16]; diff --git a/src/engine/platform/ym2610bext.cpp b/src/engine/platform/ym2610bext.cpp index 7c8247ff..f55e6561 100644 --- a/src/engine/platform/ym2610bext.cpp +++ b/src/engine/platform/ym2610bext.cpp @@ -59,6 +59,7 @@ int DivPlatformYM2610BExt::dispatch(DivCommand c) { rWrite(baseAddr+0x70,op.d2r&31); rWrite(baseAddr+0x80,(op.rr&15)|(op.sl<<4)); rWrite(baseAddr+0x90,op.ssgEnv&15); + opChan[ch].mask=op.enable; } if (opChan[ch].insChanged) { // TODO how does this work? rWrite(chanOffs[2]+0xb0,(ins->fm.alg&7)|(ins->fm.fb<<3)); @@ -358,7 +359,7 @@ void DivPlatformYM2610BExt::tick(bool sysTick) { bool writeSomething=false; unsigned char writeMask=2; for (int i=0; i<4; i++) { - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn || opChan[i].keyOff) { writeSomething=true; writeMask&=~(1<<(4+i)); @@ -395,10 +396,12 @@ void DivPlatformYM2610BExt::tick(bool sysTick) { immWrite(opChanOffsH[i],opChan[i].freq>>8); immWrite(opChanOffsL[i],opChan[i].freq&0xff); } - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn) { writeNoteOn=true; - writeMask|=1<<(4+i); + if (opChan[i].mask) { + writeMask|=1<<(4+i); + } opChan[i].keyOn=false; } } diff --git a/src/engine/platform/ym2610bext.h b/src/engine/platform/ym2610bext.h index 732678fe..e60f9713 100644 --- a/src/engine/platform/ym2610bext.h +++ b/src/engine/platform/ym2610bext.h @@ -27,12 +27,12 @@ class DivPlatformYM2610BExt: public DivPlatformYM2610B { unsigned char freqH, freqL; int freq, baseFreq, pitch, pitch2, portaPauseFreq, ins; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, mask; int vol; unsigned char pan; // UGLY OpChannel(): freqH(0), freqL(0), freq(0), baseFreq(0), pitch(0), pitch2(0), portaPauseFreq(0), ins(-1), active(false), insChanged(true), freqChanged(false), keyOn(false), keyOff(false), portaPause(false), - inPorta(false), vol(0), pan(3) {} + inPorta(false), mask(true), vol(0), pan(3) {} }; OpChannel opChan[4]; bool isOpMuted[4]; diff --git a/src/engine/platform/ym2610ext.cpp b/src/engine/platform/ym2610ext.cpp index b2bd06a8..6cee242f 100644 --- a/src/engine/platform/ym2610ext.cpp +++ b/src/engine/platform/ym2610ext.cpp @@ -59,6 +59,7 @@ int DivPlatformYM2610Ext::dispatch(DivCommand c) { rWrite(baseAddr+0x70,op.d2r&31); rWrite(baseAddr+0x80,(op.rr&15)|(op.sl<<4)); rWrite(baseAddr+0x90,op.ssgEnv&15); + opChan[ch].mask=op.enable; } if (opChan[ch].insChanged) { // TODO how does this work? rWrite(chanOffs[1]+0xb0,(ins->fm.alg&7)|(ins->fm.fb<<3)); @@ -358,7 +359,7 @@ void DivPlatformYM2610Ext::tick(bool sysTick) { bool writeSomething=false; unsigned char writeMask=2; for (int i=0; i<4; i++) { - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn || opChan[i].keyOff) { writeSomething=true; writeMask&=~(1<<(4+i)); @@ -395,10 +396,12 @@ void DivPlatformYM2610Ext::tick(bool sysTick) { immWrite(opChanOffsH[i],opChan[i].freq>>8); immWrite(opChanOffsL[i],opChan[i].freq&0xff); } - writeMask|=opChan[i].active<<(4+i); + writeMask|=(unsigned char)(opChan[i].mask && opChan[i].active)<<(4+i); if (opChan[i].keyOn) { writeNoteOn=true; - writeMask|=1<<(4+i); + if (opChan[i].mask) { + writeMask|=1<<(4+i); + } opChan[i].keyOn=false; } } diff --git a/src/engine/platform/ym2610ext.h b/src/engine/platform/ym2610ext.h index 119d6356..07d855c0 100644 --- a/src/engine/platform/ym2610ext.h +++ b/src/engine/platform/ym2610ext.h @@ -27,12 +27,12 @@ class DivPlatformYM2610Ext: public DivPlatformYM2610 { unsigned char freqH, freqL; int freq, baseFreq, pitch, pitch2, portaPauseFreq, ins; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, mask; int vol; unsigned char pan; // UGLY OpChannel(): freqH(0), freqL(0), freq(0), baseFreq(0), pitch(0), pitch2(0), portaPauseFreq(0), ins(-1), active(false), insChanged(true), freqChanged(false), keyOn(false), keyOff(false), portaPause(false), - inPorta(false), vol(0), pan(3) {} + inPorta(false), mask(true), vol(0), pan(3) {} }; OpChannel opChan[4]; bool isOpMuted[4]; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 7fb9583d..898bb2b0 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -31,7 +31,9 @@ void DivEngine::nextOrder() { curRow=0; if (repeatPattern) return; if (++curOrder>=curSubSong->ordersLen) { + logV("end of orders reached"); endOfSong=true; + memset(walked,0,8192); curOrder=0; } } @@ -348,15 +350,31 @@ void DivEngine::processRow(int i, bool afterDelay) { if (effectVal>0) speed2=effectVal; break; case 0x0b: // change order - if (changeOrd==-1) { + if (changeOrd==-1 || song.jumpTreatment==0) { changeOrd=effectVal; - changePos=0; + if (song.jumpTreatment==1 || song.jumpTreatment==2) { + changePos=0; + } } break; case 0x0d: // next order - if (changeOrd<0 && (curOrder<(curSubSong->ordersLen-1) || !song.ignoreJumpAtEnd)) { - changeOrd=-2; - changePos=effectVal; + if (song.jumpTreatment==2) { + if ((curOrder<(curSubSong->ordersLen-1) || !song.ignoreJumpAtEnd)) { + changeOrd=-2; + changePos=effectVal; + } + } else if (song.jumpTreatment==1) { + if (changeOrd<0 && (curOrder<(curSubSong->ordersLen-1) || !song.ignoreJumpAtEnd)) { + changeOrd=-2; + changePos=effectVal; + } + } else { + if (curOrder<(curSubSong->ordersLen-1) || !song.ignoreJumpAtEnd) { + if (changeOrd<0) { + changeOrd=-2; + } + changePos=effectVal; + } } break; case 0xed: // delay @@ -911,18 +929,23 @@ void DivEngine::nextRow() { processRow(i,false); } + walked[((curOrder<<5)+(curRow>>3))&8191]|=1<<(curRow&7); + if (changeOrd!=-1) { if (repeatPattern) { curRow=0; changeOrd=-1; } else { curRow=changePos; + changePos=0; if (changeOrd==-2) changeOrd=curOrder+1; - if (changeOrd<=curOrder) endOfSong=true; + // old loop detection routine + //if (changeOrd<=curOrder) endOfSong=true; curOrder=changeOrd; if (curOrder>=curSubSong->ordersLen) { curOrder=0; endOfSong=true; + memset(walked,0,8192); } changeOrd=-1; } @@ -932,6 +955,13 @@ void DivEngine::nextRow() { if (haltOn==DIV_HALT_PATTERN) halted=true; } + // new loop detection routine + if (!endOfSong && walked[((curOrder<<5)+(curRow>>3))&8191]&(1<<(curRow&7))) { + logV("loop reached"); + endOfSong=true; + memset(walked,0,8192); + } + if (song.brokenSpeedSel) { if ((curSubSong->patLen&1) && curOrder&1) { ticks=((curRow&1)?speed2:speed1)*(curSubSong->timeBase+1); diff --git a/src/engine/song.h b/src/engine/song.h index ac49832e..493fc412 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -468,6 +468,11 @@ struct DivSong { // 1: broken (don't allow value higher than speed) // 2: lax (allow value higher than speed) unsigned char delayBehavior; + // 0B/0D treatment + // 0: normal (0B/0D accepted) + // 1: old Furnace (first one accepted) + // 2: DefleMask (0D takes priority over 0B) + unsigned char jumpTreatment; bool properNoiseLayout; bool waveDutyIsVol; bool resetMacroOnPorta; @@ -571,6 +576,7 @@ struct DivSong { pitchSlideSpeed(4), loopModality(2), delayBehavior(2), + jumpTreatment(0), properNoiseLayout(true), waveDutyIsVol(false), resetMacroOnPorta(false), diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 1c63431f..78af262a 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1370,7 +1370,7 @@ void DivEngine::registerSystems() { // to Grauw: feel free to change this to 24 during development of OPL4's PCM part. sysDefs[DIV_SYSTEM_OPL4]=new DivSysDef( - "Yamaha OPL4", NULL, 0xae, 0, 42, true, true, 0, false, + "Yamaha YMF278B (OPL4)", NULL, 0xae, 0, 42, true, true, 0, false, "like OPL3, but this time it also has a 24-channel version of MultiPCM.", {"4OP 1", "FM 2", "4OP 3", "FM 4", "4OP 5", "FM 6", "4OP 7", "FM 8", "4OP 9", "FM 10", "4OP 11", "FM 12", "FM 13", "FM 14", "FM 15", "FM 16", "FM 17", "FM 18", "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", "PCM 17", "PCM 18", "PCM 19", "PCM 20", "PCM 21", "PCM 22", "PCM 23", "PCM 24"}, {"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", "F16", "F17", "F18", "P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P8", "P10", "P11", "P12", "P13", "P14", "P15", "P16", "P17", "P18", "P19", "P20", "P21", "P22", "P23", "P24"}, @@ -1379,7 +1379,7 @@ void DivEngine::registerSystems() { ); sysDefs[DIV_SYSTEM_OPL4_DRUMS]=new DivSysDef( - "Yamaha OPL4 with drums", NULL, 0xaf, 0, 44, true, true, 0, false, + "Yamaha YMF278B (OPL4) with drums", NULL, 0xaf, 0, 44, true, true, 0, false, "the OPL4 but with drums mode turned on.", {"4OP 1", "FM 2", "4OP 3", "FM 4", "4OP 5", "FM 6", "4OP 7", "FM 8", "4OP 9", "FM 10", "4OP 11", "FM 12", "FM 13", "FM 14", "FM 15", "Kick/FM 16", "Snare", "Tom", "Top", "HiHat", "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", "PCM 17", "PCM 18", "PCM 19", "PCM 20", "PCM 21", "PCM 22", "PCM 23", "PCM 24"}, {"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", "BD", "SD", "TM", "TP", "HH", "P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P8", "P10", "P11", "P12", "P13", "P14", "P15", "P16", "P17", "P18", "P19", "P20", "P21", "P22", "P23", "P24"}, diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 65baf4fd..b763ab97 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -82,13 +82,15 @@ const char* aboutLine[]={ "Laggy", "LovelyA72", "LunaMoth", + "Lunathir", "LVintageNerd", "Mahbod Karamoozian", "Miker", "nicco1690", "NikonTeen", - "psdominator", + "psxdominator", "Raijin", + "SnugglyBun", "SuperJet Spade", "TheDuccinator", "theloredev", @@ -102,6 +104,7 @@ const char* aboutLine[]={ "fd", "GENATARi", "host12prog", + "Lunathir", "plane", "TheEssem", "", @@ -133,6 +136,8 @@ const char* aboutLine[]={ "puNES (NES, MMC5 and FDS) by FHorse", "NSFPlay (NES and FDS) by Brad Smith and Brezza", "reSID by Dag Lem", + "reSIDfp by Dag Lem, Antti Lankila", + "and Leandro Nini", "Stella by Stella Team", "QSound emulator by superctr and Valley Bell", "VICE VIC-20 sound core by Rami Rasanen and viznut", diff --git a/src/gui/compatFlags.cpp b/src/gui/compatFlags.cpp index 74d493e7..63571dca 100644 --- a/src/gui/compatFlags.cpp +++ b/src/gui/compatFlags.cpp @@ -213,6 +213,26 @@ void FurnaceGUI::drawCompatFlags() { ImGui::SetTooltip("no checks (like FamiTracker)"); } + ImGui::Text("Simultaneous jump (0B+0D) treatment:"); + if (ImGui::RadioButton("Normal",e->song.jumpTreatment==0)) { + e->song.jumpTreatment=0; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("accept 0B+0D to jump to a specific row of an order"); + } + if (ImGui::RadioButton("Old Furnace",e->song.jumpTreatment==1)) { + e->song.jumpTreatment=1; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("only accept the first jump effect"); + } + if (ImGui::RadioButton("DefleMask",e->song.jumpTreatment==2)) { + e->song.jumpTreatment=2; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("only accept 0Dxx"); + } + ImGui::Separator(); ImGui::TextWrapped("the following flags are for compatibility with older Furnace versions."); diff --git a/src/gui/dataList.cpp b/src/gui/dataList.cpp index 10dda6aa..0519fca3 100644 --- a/src/gui/dataList.cpp +++ b/src/gui/dataList.cpp @@ -29,14 +29,20 @@ const char* sampleNote[12]={ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; -void FurnaceGUI::drawInsList() { +void FurnaceGUI::drawInsList(bool asChild) { if (nextWindow==GUI_WINDOW_INS_LIST) { insListOpen=true; ImGui::SetNextWindowFocus(); nextWindow=GUI_WINDOW_NOTHING; } - if (!insListOpen) return; - if (ImGui::Begin("Instruments",&insListOpen,globalWinFlags)) { + if (!insListOpen && !asChild) return; + bool began=false; + if (asChild) { + began=ImGui::BeginChild("Instruments"); + } else { + began=ImGui::Begin("Instruments",&insListOpen,globalWinFlags); + } + if (began) { if (settings.unifiedDataView) settings.horizontalDataView=0; if (ImGui::Button(ICON_FA_PLUS "##InsAdd")) { if (!settings.unifiedDataView) doAction(GUI_ACTION_INS_LIST_ADD); @@ -121,16 +127,30 @@ void FurnaceGUI::drawInsList() { if (ImGui::MenuItem("instrument")) { doAction(GUI_ACTION_INS_LIST_SAVE); } + if (ImGui::MenuItem("instrument (.dmp)")) { + doAction(GUI_ACTION_INS_LIST_SAVE_DMP); + } if (ImGui::MenuItem("wavetable")) { doAction(GUI_ACTION_WAVE_LIST_SAVE); } + if (ImGui::MenuItem("wavetable (.dmw)")) { + doAction(GUI_ACTION_WAVE_LIST_SAVE_DMW); + } + if (ImGui::MenuItem("wavetable (raw)")) { + doAction(GUI_ACTION_WAVE_LIST_SAVE_RAW); + } if (ImGui::MenuItem("sample")) { doAction(GUI_ACTION_SAMPLE_LIST_SAVE); } ImGui::EndPopup(); } - } - if (!settings.unifiedDataView) { + } else { + if (ImGui::BeginPopupContextItem("InsSaveFormats",ImGuiMouseButton_Right)) { + if (ImGui::MenuItem("save as .dmp...")) { + doAction(GUI_ACTION_INS_LIST_SAVE_DMP); + } + ImGui::EndPopup(); + } ImGui::SameLine(); if (ImGui::ArrowButton("InsUp",ImGuiDir_Up)) { doAction(GUI_ACTION_INS_LIST_MOVE_UP); @@ -359,6 +379,9 @@ void FurnaceGUI::drawInsList() { if (ImGui::MenuItem("save")) { doAction(GUI_ACTION_INS_LIST_SAVE); } + if (ImGui::MenuItem("save (.dmp)")) { + doAction(GUI_ACTION_INS_LIST_SAVE_DMP); + } if (ImGui::MenuItem("delete")) { doAction(GUI_ACTION_INS_LIST_DELETE); } @@ -392,11 +415,15 @@ void FurnaceGUI::drawInsList() { ImGui::EndTable(); } } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_INS_LIST; - ImGui::End(); + if (asChild) { + ImGui::EndChild(); + } else { + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_INS_LIST; + ImGui::End(); + } } -void FurnaceGUI::drawWaveList() { +void FurnaceGUI::drawWaveList(bool asChild) { if (nextWindow==GUI_WINDOW_WAVE_LIST) { waveListOpen=true; if (settings.unifiedDataView) { @@ -407,8 +434,14 @@ void FurnaceGUI::drawWaveList() { nextWindow=GUI_WINDOW_NOTHING; } if (settings.unifiedDataView) return; - if (!waveListOpen) return; - if (ImGui::Begin("Wavetables",&waveListOpen,globalWinFlags)) { + if (!waveListOpen && !asChild) return; + bool began=false; + if (asChild) { + began=ImGui::BeginChild("Wavetables"); + } else { + began=ImGui::Begin("Wavetables",&waveListOpen,globalWinFlags); + } + if (began) { if (ImGui::Button(ICON_FA_PLUS "##WaveAdd")) { doAction(GUI_ACTION_WAVE_LIST_ADD); } @@ -430,6 +463,17 @@ void FurnaceGUI::drawWaveList() { if (ImGui::Button(ICON_FA_FLOPPY_O "##WaveSave")) { doAction(GUI_ACTION_WAVE_LIST_SAVE); } + if (!settings.unifiedDataView) { + if (ImGui::BeginPopupContextItem("WaveSaveFormats",ImGuiMouseButton_Right)) { + if (ImGui::MenuItem("save as .dmw...")) { + doAction(GUI_ACTION_WAVE_LIST_SAVE_DMW); + } + if (ImGui::MenuItem("save raw...")) { + doAction(GUI_ACTION_WAVE_LIST_SAVE_RAW); + } + ImGui::EndPopup(); + } + } ImGui::SameLine(); if (ImGui::ArrowButton("WaveUp",ImGuiDir_Up)) { doAction(GUI_ACTION_WAVE_LIST_MOVE_UP); @@ -448,11 +492,15 @@ void FurnaceGUI::drawWaveList() { ImGui::EndTable(); } } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_WAVE_LIST; - ImGui::End(); + if (asChild) { + ImGui::EndChild(); + } else { + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_WAVE_LIST; + ImGui::End(); + } } -void FurnaceGUI::drawSampleList() { +void FurnaceGUI::drawSampleList(bool asChild) { if (nextWindow==GUI_WINDOW_SAMPLE_LIST) { sampleListOpen=true; if (settings.unifiedDataView) { @@ -463,8 +511,14 @@ void FurnaceGUI::drawSampleList() { nextWindow=GUI_WINDOW_NOTHING; } if (settings.unifiedDataView) return; - if (!sampleListOpen) return; - if (ImGui::Begin("Samples",&sampleListOpen,globalWinFlags)) { + if (!sampleListOpen && !asChild) return; + bool began=false; + if (asChild) { + began=ImGui::BeginChild("Samples"); + } else { + began=ImGui::Begin("Samples",&sampleListOpen,globalWinFlags); + } + if (began) { if (ImGui::Button(ICON_FA_FILE "##SampleAdd")) { doAction(GUI_ACTION_SAMPLE_LIST_ADD); } @@ -520,8 +574,12 @@ void FurnaceGUI::drawSampleList() { } ImGui::Unindent(); } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SAMPLE_LIST; - ImGui::End(); + if (asChild) { + ImGui::EndChild(); + } else { + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SAMPLE_LIST; + ImGui::End(); + } } void FurnaceGUI::actualWaveList() { diff --git a/src/gui/doAction.cpp b/src/gui/doAction.cpp index e0f0ca30..92e12a74 100644 --- a/src/gui/doAction.cpp +++ b/src/gui/doAction.cpp @@ -599,6 +599,9 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_INS_LIST_SAVE: if (curIns>=0 && curIns<(int)e->song.ins.size()) openFileDialog(GUI_FILE_INS_SAVE); break; + case GUI_ACTION_INS_LIST_SAVE_DMP: + if (curIns>=0 && curIns<(int)e->song.ins.size()) openFileDialog(GUI_FILE_INS_SAVE_DMP); + break; case GUI_ACTION_INS_LIST_MOVE_UP: if (e->moveInsUp(curIns)) { curIns--; @@ -666,6 +669,12 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_WAVE_LIST_SAVE: if (curWave>=0 && curWave<(int)e->song.wave.size()) openFileDialog(GUI_FILE_WAVE_SAVE); break; + case GUI_ACTION_WAVE_LIST_SAVE_DMW: + if (curWave>=0 && curWave<(int)e->song.wave.size()) openFileDialog(GUI_FILE_WAVE_SAVE_DMW); + break; + case GUI_ACTION_WAVE_LIST_SAVE_RAW: + if (curWave>=0 && curWave<(int)e->song.wave.size()) openFileDialog(GUI_FILE_WAVE_SAVE_RAW); + break; case GUI_ACTION_WAVE_LIST_MOVE_UP: if (e->moveWaveUp(curWave)) { curWave--; @@ -1295,6 +1304,32 @@ void FurnaceGUI::doAction(int what) { MARK_MODIFIED; break; } + case GUI_ACTION_SAMPLE_CREATE_WAVE: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + SAMPLE_OP_BEGIN; + if (end-start<1) { + showError("select at least one sample!"); + } else if (end-start>256) { + showError("maximum size is 256 samples!"); + } else { + curWave=e->addWave(); + if (curWave==-1) { + showError("too many wavetables!"); + } else { + DivWavetable* wave=e->song.wave[curWave]; + wave->min=0; + wave->max=255; + wave->len=end-start; + for (unsigned int i=start; idata[i-start]=(sample->data8[i]&0xff)^0x80; + } + nextWindow=GUI_WINDOW_WAVE_EDIT; + MARK_MODIFIED; + } + } + break; + } case GUI_ACTION_ORDERS_UP: if (curOrder>0) { diff --git a/src/gui/editControls.cpp b/src/gui/editControls.cpp index 912bd1c0..1d0e2a25 100644 --- a/src/gui/editControls.cpp +++ b/src/gui/editControls.cpp @@ -22,54 +22,242 @@ #include void FurnaceGUI::drawMobileControls() { + float timeScale=1.0f/(60.0f*ImGui::GetIO().DeltaTime); + if (mobileMenuOpen) { + if (mobileMenuPos<0.999f) { + WAKE_UP; + mobileMenuPos+=MIN(0.1,(1.0-mobileMenuPos)*0.65)*timeScale; + } else { + mobileMenuPos=1.0f; + } + } else { + if (mobileMenuPos>0.001f) { + WAKE_UP; + mobileMenuPos-=MIN(0.1,mobileMenuPos*0.65)*timeScale; + } else { + mobileMenuPos=0.0f; + } + } + ImGui::SetNextWindowPos(portrait?ImVec2(0.0f,((1.0-mobileMenuPos*0.65)*scrH*dpiScale)-(0.16*scrW*dpiScale)):ImVec2(0.5*scrW*dpiScale*mobileMenuPos,0.0f)); + ImGui::SetNextWindowSize(portrait?ImVec2(scrW*dpiScale,0.16*scrW*dpiScale):ImVec2(0.16*scrH*dpiScale,scrH*dpiScale)); if (ImGui::Begin("Mobile Controls",NULL,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse|globalWinFlags)) { - float availX=ImGui::GetContentRegionAvail().x; - ImVec2 buttonSize=ImVec2(availX,availX); + float avail=portrait?ImGui::GetContentRegionAvail().y:ImGui::GetContentRegionAvail().x; + ImVec2 buttonSize=ImVec2(avail,avail); - if (ImGui::Button(ICON_FA_CHEVRON_RIGHT "##MobileMenu",buttonSize)) { + const char* mobButtonName=ICON_FA_CHEVRON_RIGHT "##MobileMenu"; + if (portrait) mobButtonName=ICON_FA_CHEVRON_UP "##MobileMenu"; + if (mobileMenuOpen) { + if (portrait) { + mobButtonName=ICON_FA_CHEVRON_DOWN "##MobileMenu"; + } else { + mobButtonName=ICON_FA_CHEVRON_LEFT "##MobileMenu"; + } + } + if (ImGui::Button(mobButtonName,buttonSize)) { + mobileMenuOpen=!mobileMenuOpen; } - ImGui::Separator(); + if (!portrait) ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(e->isPlaying())); + pushToggleColors(e->isPlaying()); + if (portrait) ImGui::SameLine(); if (ImGui::Button(ICON_FA_PLAY "##Play",buttonSize)) { play(); } - ImGui::PopStyleColor(); + popToggleColors(); + if (portrait) ImGui::SameLine(); if (ImGui::Button(ICON_FA_STOP "##Stop",buttonSize)) { stop(); } + if (portrait) ImGui::SameLine(); if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne",buttonSize)) { e->stepOne(cursor.y); pendingStepUpdate=true; } bool repeatPattern=e->getRepeatPattern(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(repeatPattern)); + pushToggleColors(repeatPattern); + if (portrait) ImGui::SameLine(); if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern",buttonSize)) { e->setRepeatPattern(!repeatPattern); } - ImGui::PopStyleColor(); + popToggleColors(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(edit)); + pushToggleColors(edit); + if (portrait) ImGui::SameLine(); if (ImGui::Button(ICON_FA_CIRCLE "##Edit",buttonSize)) { edit=!edit; } - ImGui::PopStyleColor(); + popToggleColors(); bool metro=e->getMetronome(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(metro)); + pushToggleColors(metro); + if (portrait) ImGui::SameLine(); if (ImGui::Button(ICON_FA_BELL_O "##Metronome",buttonSize)) { e->setMetronome(!metro); } - ImGui::PopStyleColor(); - - if (ImGui::Button("Get me out of here")) { - toggleMobileUI(false); - } + popToggleColors(); } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; ImGui::End(); + + ImGui::SetNextWindowPos(portrait?ImVec2(0.0f,((1.0-mobileMenuPos*0.65)*scrH*dpiScale)):ImVec2(0.5*scrW*dpiScale*(mobileMenuPos-1.0),0.0f)); + ImGui::SetNextWindowSize(portrait?ImVec2(scrW*dpiScale,0.65*scrH*dpiScale):ImVec2(0.5*scrW*dpiScale,scrH*dpiScale)); + if (ImGui::Begin("Mobile Menu",NULL,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse|globalWinFlags)) { + if (ImGui::BeginTable("SceneSel",5)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch,1.0f); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,1.0f); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,1.0f); + ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthStretch,1.0f); + ImGui::TableSetupColumn("c4",ImGuiTableColumnFlags_WidthStretch,1.0f); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImVec2 buttonSize=ImGui::GetContentRegionAvail(); + buttonSize.y=30.0f*dpiScale; + + if (ImGui::Button("Pattern",buttonSize)) { + mobScene=GUI_SCENE_PATTERN; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Orders",buttonSize)) { + mobScene=GUI_SCENE_ORDERS; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Ins",buttonSize)) { + mobScene=GUI_SCENE_INSTRUMENT; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Wave",buttonSize)) { + mobScene=GUI_SCENE_WAVETABLE; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Sample",buttonSize)) { + mobScene=GUI_SCENE_SAMPLE; + } + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Button("Song",buttonSize)) { + mobScene=GUI_SCENE_SONG; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Channels",buttonSize)) { + mobScene=GUI_SCENE_CHANNELS; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Chips",buttonSize)) { + mobScene=GUI_SCENE_CHIPS; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Other",buttonSize)) { + mobScene=GUI_SCENE_OTHER; + } + ImGui::EndTable(); + } + + ImGui::Separator(); + + if (settings.unifiedDataView) { + drawInsList(true); + } else { + switch (mobScene) { + case GUI_SCENE_PATTERN: + case GUI_SCENE_ORDERS: + case GUI_SCENE_INSTRUMENT: + drawInsList(true); + break; + case GUI_SCENE_WAVETABLE: + drawWaveList(true); + break; + case GUI_SCENE_SAMPLE: + drawSampleList(true); + break; + case GUI_SCENE_SONG: { + if (ImGui::Button("New")) { + mobileMenuOpen=false; + //doAction(GUI_ACTION_NEW); + if (modified) { + showWarning("Unsaved changes! Save changes before creating a new song?",GUI_WARN_NEW); + } else { + displayNew=true; + } + } + ImGui::SameLine(); + if (ImGui::Button("Open")) { + mobileMenuOpen=false; + doAction(GUI_ACTION_OPEN); + } + ImGui::SameLine(); + if (ImGui::Button("Save")) { + mobileMenuOpen=false; + doAction(GUI_ACTION_SAVE); + } + ImGui::SameLine(); + if (ImGui::Button("Save as...")) { + mobileMenuOpen=false; + doAction(GUI_ACTION_SAVE_AS); + } + + ImGui::Button("1.1+ .dmf"); + ImGui::SameLine(); + ImGui::Button("Legacy .dmf"); + ImGui::SameLine(); + ImGui::Button("Export Audio"); + ImGui::SameLine(); + ImGui::Button("Export VGM"); + + ImGui::Button("CmdStream"); + + ImGui::Separator(); + + ImGui::Text("Song info here..."); + break; + } + case GUI_SCENE_CHANNELS: + ImGui::Text("Channels here..."); + break; + case GUI_SCENE_CHIPS: + ImGui::Text("Chips here..."); + break; + case GUI_SCENE_OTHER: { + if (ImGui::Button("Osc")) { + oscOpen=!oscOpen; + } + ImGui::SameLine(); + if (ImGui::Button("ChanOsc")) { + chanOscOpen=!chanOscOpen; + } + ImGui::SameLine(); + if (ImGui::Button("RegView")) { + regViewOpen=!regViewOpen; + } + ImGui::SameLine(); + if (ImGui::Button("Stats")) { + statsOpen=!statsOpen; + } + + ImGui::Separator(); + + ImGui::Button("Panic"); + ImGui::SameLine(); + if (ImGui::Button("Settings")) { + mobileMenuOpen=false; + } + ImGui::SameLine(); + if (ImGui::Button("About")) { + mobileMenuOpen=false; + mobileMenuPos=0.0f; + aboutOpen=true; + } + if (ImGui::Button("Switch to Desktop Mode")) { + toggleMobileUI(!mobileUI); + } + break; + } + } + } + } + ImGui::End(); } void FurnaceGUI::drawEditControls() { @@ -118,11 +306,11 @@ void FurnaceGUI::drawEditControls() { ImGui::EndTable(); } - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(e->isPlaying())); + pushToggleColors(e->isPlaying()); if (ImGui::Button(ICON_FA_PLAY "##Play")) { play(); } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); if (ImGui::Button(ICON_FA_STOP "##Stop")) { stop(); @@ -152,12 +340,12 @@ void FurnaceGUI::drawEditControls() { } ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(noteInputPoly)); + pushToggleColors(noteInputPoly); if (ImGui::Button(noteInputPoly?("Poly##PolyInput"):("Mono##PolyInput"))) { noteInputPoly=!noteInputPoly; e->setAutoNotePoly(noteInputPoly); } - ImGui::PopStyleColor(); + popToggleColors(); } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; ImGui::End(); @@ -168,11 +356,11 @@ void FurnaceGUI::drawEditControls() { stop(); } ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(e->isPlaying())); + pushToggleColors(e->isPlaying()); if (ImGui::Button(ICON_FA_PLAY "##Play")) { play(); } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { e->stepOne(cursor.y); @@ -181,26 +369,26 @@ void FurnaceGUI::drawEditControls() { ImGui::SameLine(); bool repeatPattern=e->getRepeatPattern(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(repeatPattern)); + pushToggleColors(repeatPattern); if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern")) { e->setRepeatPattern(!repeatPattern); } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(edit)); + pushToggleColors(edit); if (ImGui::Button(ICON_FA_CIRCLE "##Edit")) { edit=!edit; } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); bool metro=e->getMetronome(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(metro)); + pushToggleColors(metro); if (ImGui::Button(ICON_FA_BELL_O "##Metronome")) { e->setMetronome(!metro); } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); ImGui::Text("Octave"); @@ -237,12 +425,12 @@ void FurnaceGUI::drawEditControls() { unimportant(ImGui::Checkbox("Pattern",&followPattern)); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(noteInputPoly)); + pushToggleColors(noteInputPoly); if (ImGui::Button(noteInputPoly?("Poly##PolyInput"):("Mono##PolyInput"))) { noteInputPoly=!noteInputPoly; e->setAutoNotePoly(noteInputPoly); } - ImGui::PopStyleColor(); + popToggleColors(); } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; ImGui::End(); @@ -250,11 +438,11 @@ void FurnaceGUI::drawEditControls() { case 2: // compact vertical if (ImGui::Begin("Play/Edit Controls",&editControlsOpen,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse|globalWinFlags)) { ImVec2 buttonSize=ImVec2(ImGui::GetContentRegionAvail().x,0.0f); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(e->isPlaying())); + pushToggleColors(e->isPlaying()); if (ImGui::Button(ICON_FA_PLAY "##Play",buttonSize)) { play(); } - ImGui::PopStyleColor(); + popToggleColors(); if (ImGui::Button(ICON_FA_STOP "##Stop",buttonSize)) { stop(); } @@ -264,24 +452,24 @@ void FurnaceGUI::drawEditControls() { } bool repeatPattern=e->getRepeatPattern(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(repeatPattern)); + pushToggleColors(repeatPattern); if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern",buttonSize)) { e->setRepeatPattern(!repeatPattern); } - ImGui::PopStyleColor(); + popToggleColors(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(edit)); + pushToggleColors(edit); if (ImGui::Button(ICON_FA_CIRCLE "##Edit",buttonSize)) { edit=!edit; } - ImGui::PopStyleColor(); + popToggleColors(); bool metro=e->getMetronome(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(metro)); + pushToggleColors(metro); if (ImGui::Button(ICON_FA_BELL_O "##Metronome",buttonSize)) { e->setMetronome(!metro); } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::Text("Oct."); float avail=ImGui::GetContentRegionAvail().x; @@ -308,23 +496,23 @@ void FurnaceGUI::drawEditControls() { } ImGui::Text("Foll."); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(followOrders)); + pushToggleColors(followOrders); if (ImGui::Button("Ord##FollowOrders",buttonSize)) { handleUnimportant followOrders=!followOrders; } - ImGui::PopStyleColor(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(followPattern)); + popToggleColors(); + pushToggleColors(followPattern); if (ImGui::Button("Pat##FollowPattern",buttonSize)) { handleUnimportant followPattern=!followPattern; } - ImGui::PopStyleColor(); + popToggleColors(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(noteInputPoly)); + pushToggleColors(noteInputPoly); if (ImGui::Button(noteInputPoly?("Poly##PolyInput"):("Mono##PolyInput"))) { noteInputPoly=!noteInputPoly; e->setAutoNotePoly(noteInputPoly); } - ImGui::PopStyleColor(); + popToggleColors(); } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; ImGui::End(); @@ -332,11 +520,11 @@ void FurnaceGUI::drawEditControls() { case 3: // split if (ImGui::Begin("Play Controls",&editControlsOpen,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse|globalWinFlags)) { if (e->isPlaying()) { - ImGui::PushStyleColor(ImGuiCol_Button,uiColors[GUI_COLOR_TOGGLE_ON]); + pushToggleColors(true); if (ImGui::Button(ICON_FA_STOP "##Stop")) { stop(); } - ImGui::PopStyleColor(); + popToggleColors(); } else { if (ImGui::Button(ICON_FA_PLAY "##Play")) { play(oldRow); @@ -359,35 +547,35 @@ void FurnaceGUI::drawEditControls() { } ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(edit)); + pushToggleColors(edit); if (ImGui::Button(ICON_FA_CIRCLE "##Edit")) { edit=!edit; } - ImGui::PopStyleColor(); + popToggleColors(); bool metro=e->getMetronome(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(metro)); + pushToggleColors(metro); if (ImGui::Button(ICON_FA_BELL_O "##Metronome")) { e->setMetronome(!metro); } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); bool repeatPattern=e->getRepeatPattern(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(repeatPattern)); + pushToggleColors(repeatPattern); if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern")) { e->setRepeatPattern(!repeatPattern); } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(noteInputPoly)); + pushToggleColors(noteInputPoly); if (ImGui::Button(noteInputPoly?("Poly##PolyInput"):("Mono##PolyInput"))) { noteInputPoly=!noteInputPoly; e->setAutoNotePoly(noteInputPoly); } - ImGui::PopStyleColor(); + popToggleColors(); } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; ImGui::End(); diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp index 391a39f6..201c9be2 100644 --- a/src/gui/editing.cpp +++ b/src/gui/editing.cpp @@ -431,7 +431,7 @@ void FurnaceGUI::doPaste(PasteMode mode) { int startOff=-1; bool invalidData=false; if (data.size()<2) return; - if (data[0]!=fmt::sprintf("org.tildearrow.furnace - Pattern Data (%d)",DIV_ENGINE_VERSION)) return; + if (data[0].find("org.tildearrow.furnace - Pattern Data")!=0) return; if (sscanf(data[1].c_str(),"%d",&startOff)!=1) return; if (startOff<0) return; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index edbe9864..9dea74fa 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -17,6 +17,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +// I hate you clangd extension! +// how about you DON'T insert random headers before this freaking important +// define!!!!!! #define _USE_MATH_DEFINES #include "gui.h" #include "util.h" @@ -225,7 +228,7 @@ void FurnaceGUI::encodeMMLStr(String& target, int* macro, int macroLen, int macr } } -void FurnaceGUI::decodeMMLStrW(String& source, int* macro, int& macroLen, int macroMax, bool hex) { +void FurnaceGUI::decodeMMLStrW(String& source, int* macro, int& macroLen, int macroMin, int macroMax, bool hex) { int buf=0; bool negaBuf=false; bool hasVal=false; @@ -261,9 +264,9 @@ void FurnaceGUI::decodeMMLStrW(String& source, int* macro, int& macroLen, int ma case ' ': if (hasVal) { hasVal=false; - negaBuf=false; macro[macroLen]=negaBuf?-buf:buf; - if (macro[macroLen]<0) macro[macroLen]=0; + negaBuf=false; + if (macro[macroLen]macroMax) macro[macroLen]=macroMax; macroLen++; buf=0; @@ -274,9 +277,9 @@ void FurnaceGUI::decodeMMLStrW(String& source, int* macro, int& macroLen, int ma } if (hasVal && macroLen<256) { hasVal=false; - negaBuf=false; macro[macroLen]=negaBuf?-buf:buf; - if (macro[macroLen]<0) macro[macroLen]=0; + negaBuf=false; + if (macro[macroLen]macroMax) macro[macroLen]=macroMax; macroLen++; buf=0; @@ -533,6 +536,7 @@ void FurnaceGUI::setFileName(String name) { } #endif updateWindowTitle(); + pushRecentFile(curFileName); } void FurnaceGUI::updateWindowTitle() { @@ -1151,6 +1155,7 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { e->lockSave([this,num]() { e->curOrders->ord[orderCursor][curOrder]=((e->curOrders->ord[orderCursor][curOrder]<<4)|num); }); + MARK_MODIFIED; if (orderEditMode==2 || orderEditMode==3) { curNibble=!curNibble; if (!curNibble) { @@ -1258,9 +1263,18 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); hasOpened=fileDialog->openSave( "Save File", - {"Furnace song", "*.fur", - "DefleMask 1.1.3 module", "*.dmf"}, - "Furnace song{.fur},DefleMask 1.1.3 module{.dmf}", + {"Furnace song", "*.fur"}, + "Furnace song{.fur}", + workingDirSong, + dpiScale + ); + break; + case GUI_FILE_SAVE_DMF: + if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save File", + {"DefleMask 1.1.3 module", "*.dmf"}, + "DefleMask 1.1.3 module{.dmf}", workingDirSong, dpiScale ); @@ -1335,9 +1349,18 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); hasOpened=fileDialog->openSave( "Save Instrument", - {"Furnace instrument", "*.fui", - "DefleMask preset", "*.dmp"}, - "Furnace instrument{.fui},DefleMask preset{.dmp}", + {"Furnace instrument", "*.fui"}, + "Furnace instrument{.fui}", + workingDirIns, + dpiScale + ); + break; + case GUI_FILE_INS_SAVE_DMP: + if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save Instrument", + {"DefleMask preset", "*.dmp"}, + "DefleMask preset{.dmp}", workingDirIns, dpiScale ); @@ -1358,10 +1381,28 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); hasOpened=fileDialog->openSave( "Save Wavetable", - {"Furnace wavetable", ".fuw", - "DefleMask wavetable", ".dmw", - "raw data", ".raw"}, - "Furnace wavetable{.fuw},DefleMask wavetable{.dmw},raw data{.raw}", + {"Furnace wavetable", ".fuw"}, + "Furnace wavetable{.fuw}", + workingDirWave, + dpiScale + ); + break; + case GUI_FILE_WAVE_SAVE_DMW: + if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save Wavetable", + {"DefleMask wavetable", ".dmw"}, + "DefleMask wavetable{.dmw}", + workingDirWave, + dpiScale + ); + break; + case GUI_FILE_WAVE_SAVE_RAW: + if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save Wavetable", + {"raw data", ".raw"}, + "raw data{.raw}", workingDirWave, dpiScale ); @@ -1694,6 +1735,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { if (!e->getWarnings().empty()) { showWarning(e->getWarnings(),GUI_WARN_GENERIC); } + pushRecentFile(path); return 0; } @@ -1771,9 +1813,26 @@ int FurnaceGUI::load(String path) { if (!e->getWarnings().empty()) { showWarning(e->getWarnings(),GUI_WARN_GENERIC); } + pushRecentFile(path); return 0; } +void FurnaceGUI::pushRecentFile(String path) { + if (path.empty()) return; + if (path==backupPath) return; + for (int i=0; i<(int)recentFile.size(); i++) { + if (recentFile[i]==path) { + recentFile.erase(recentFile.begin()+i); + i--; + } + } + recentFile.push_front(path); + + while (!recentFile.empty() && (int)recentFile.size()>settings.maxRecentFile) { + recentFile.pop_back(); + } +} + void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { e->saveAudio(path.c_str(),exportLoops+1,mode,exportFadeOut); displayExporting=true; @@ -2353,12 +2412,41 @@ void FurnaceGUI::toggleMobileUI(bool enable, bool force) { if (mobileUI) { ImGui::GetIO().IniFilename=NULL; } else { - ImGui::GetIO().IniFilename=finalLayoutPath; + ImGui::GetIO().IniFilename=NULL; ImGui::LoadIniSettingsFromDisk(finalLayoutPath); } } } +void FurnaceGUI::pushToggleColors(bool status) { + ImVec4 toggleColor=status?uiColors[GUI_COLOR_TOGGLE_ON]:uiColors[GUI_COLOR_TOGGLE_OFF]; + ImGui::PushStyleColor(ImGuiCol_Button,toggleColor); + if (settings.guiColorsBase) { + toggleColor.x*=0.8f; + toggleColor.y*=0.8f; + toggleColor.z*=0.8f; + } else { + toggleColor.x=CLAMP(toggleColor.x*1.3f,0.0f,1.0f); + toggleColor.y=CLAMP(toggleColor.y*1.3f,0.0f,1.0f); + toggleColor.z=CLAMP(toggleColor.z*1.3f,0.0f,1.0f); + } + ImGui::PushStyleColor(ImGuiCol_ButtonHovered,toggleColor); + if (settings.guiColorsBase) { + toggleColor.x*=0.8f; + toggleColor.y*=0.8f; + toggleColor.z*=0.8f; + } else { + toggleColor.x=CLAMP(toggleColor.x*1.5f,0.0f,1.0f); + toggleColor.y=CLAMP(toggleColor.y*1.5f,0.0f,1.0f); + toggleColor.z=CLAMP(toggleColor.z*1.5f,0.0f,1.0f); + } + ImGui::PushStyleColor(ImGuiCol_ButtonActive,toggleColor); +} + +void FurnaceGUI::popToggleColors() { + ImGui::PopStyleColor(3); +} + int _processEvent(void* instance, SDL_Event* event) { return ((FurnaceGUI*)instance)->processEvent(event); } @@ -2514,12 +2602,15 @@ void FurnaceGUI::processPoint(SDL_Event& ev) { TouchPoint* point=NULL; FIND_POINT(point,ev.tfinger.fingerId); if (point!=NULL) { + float prevX=point->x; + float prevY=point->y; point->x=ev.tfinger.x*scrW*dpiScale; point->y=ev.tfinger.y*scrH*dpiScale; point->z=ev.tfinger.pressure; if (point->id==0) { ImGui::GetIO().AddMousePosEvent(point->x,point->y); + pointMotion(point->x,point->y,point->x-prevX,point->y-prevY); } } break; @@ -2540,6 +2631,7 @@ void FurnaceGUI::processPoint(SDL_Event& ev) { if (newPoint.id==0) { ImGui::GetIO().AddMousePosEvent(newPoint.x,newPoint.y); ImGui::GetIO().AddMouseButtonEvent(ImGuiMouseButton_Left,true); + pointDown(newPoint.x,newPoint.y,0); } break; } @@ -2547,13 +2639,15 @@ void FurnaceGUI::processPoint(SDL_Event& ev) { for (size_t i=0; irenderSamplesP(); + } else { + if (sampleSelStart>sampleSelEnd) { + sampleSelStart^=sampleSelEnd; + sampleSelEnd^=sampleSelStart; + sampleSelStart^=sampleSelEnd; + } + } + } + sampleDragActive=false; + if (selecting) { + if (!selectingFull) cursor=selEnd; + finishSelection(); + demandScrollX=true; + if (cursor.xCoarse==selStart.xCoarse && cursor.xFine==selStart.xFine && cursor.y==selStart.y && + cursor.xCoarse==selEnd.xCoarse && cursor.xFine==selEnd.xFine && cursor.y==selEnd.y) { + if (!settings.cursorMoveNoScroll) { + updateScroll(cursor.y); + } + } + } +} + +void FurnaceGUI::pointMotion(int x, int y, int xrel, int yrel) { + if (selecting) { + // detect whether we have to scroll + if (ypatWindowPos.y+patWindowSize.y-2.0f*dpiScale) { + addScroll(1); + } + } + if (macroDragActive || macroLoopDragActive || waveDragActive || sampleDragActive) { + int distance=fabs((double)xrel); + if (distance<1) distance=1; + float start=x-xrel; + float end=x; + float startY=y-yrel; + float endY=y; + for (int i=0; i<=distance; i++) { + float fraction=(float)i/(float)distance; + float x=start+(end-start)*fraction; + float y=startY+(endY-startY)*fraction; + processDrags(x,y); + } + } +} + +// how many pixels should be visible at least at x/y dir +#define OOB_PIXELS_SAFETY 25 + +bool FurnaceGUI::detectOutOfBoundsWindow() { + int count=SDL_GetNumVideoDisplays(); + if (count<1) { + logW("bounds check: error %s",SDL_GetError()); + return false; + } + + SDL_Rect rect; + for (int i=0; i=scrX); + bool ybound=((rect.y+OOB_PIXELS_SAFETY)<=(scrY+scrH)) && ((rect.y+rect.h-OOB_PIXELS_SAFETY)>=scrY); + logD("bounds check: display %d is at %dx%dx%dx%d: %s%s",i,rect.x+OOB_PIXELS_SAFETY,rect.y+OOB_PIXELS_SAFETY,rect.x+rect.w-OOB_PIXELS_SAFETY,rect.y+rect.h-OOB_PIXELS_SAFETY,xbound?"x":"",ybound?"y":""); + + if (xbound && ybound) { + return true; + } + } + + return false; +} + bool FurnaceGUI::loop() { bool doThreadedInput=!settings.noThreadedInput; if (doThreadedInput) { @@ -2581,6 +2782,7 @@ bool FurnaceGUI::loop() { if (settings.powerSave) SDL_WaitEventTimeout(NULL,500); } eventTimeBegin=SDL_GetPerformanceCounter(); + bool updateWindow=false; while (SDL_PollEvent(&ev)) { WAKE_UP; ImGui_ImplSDL2_ProcessEvent(&ev); @@ -2598,80 +2800,14 @@ bool FurnaceGUI::loop() { motionXrel*=dpiScale; motionYrel*=dpiScale; #endif - if (selecting) { - // detect whether we have to scroll - if (motionYpatWindowPos.y+patWindowSize.y-2.0f*dpiScale) { - addScroll(1); - } - } - if (macroDragActive || macroLoopDragActive || waveDragActive || sampleDragActive) { - int distance=fabs((double)motionXrel); - if (distance<1) distance=1; - float start=motionX-motionXrel; - float end=motionX; - float startY=motionY-motionYrel; - float endY=motionY; - for (int i=0; i<=distance; i++) { - float fraction=(float)i/(float)distance; - float x=start+(end-start)*fraction; - float y=startY+(endY-startY)*fraction; - processDrags(x,y); - } - } + pointMotion(motionX,motionY,motionXrel,motionYrel); break; } case SDL_MOUSEBUTTONUP: - if (macroDragActive || macroLoopDragActive || waveDragActive || (sampleDragActive && sampleDragMode)) { - MARK_MODIFIED; - } - if (macroDragActive && macroDragLineMode && !macroDragMouseMoved) { - displayMacroMenu=true; - } - macroDragActive=false; - macroDragBitMode=false; - macroDragInitialValue=false; - macroDragInitialValueSet=false; - macroDragLastX=-1; - macroDragLastY=-1; - macroLoopDragActive=false; - waveDragActive=false; - if (sampleDragActive) { - logD("stopping sample drag"); - if (sampleDragMode) { - e->renderSamplesP(); - } else { - if (sampleSelStart>sampleSelEnd) { - sampleSelStart^=sampleSelEnd; - sampleSelEnd^=sampleSelStart; - sampleSelStart^=sampleSelEnd; - } - } - } - sampleDragActive=false; - if (selecting) { - if (!selectingFull) cursor=selEnd; - finishSelection(); - demandScrollX=true; - if (cursor.xCoarse==selStart.xCoarse && cursor.xFine==selStart.xFine && cursor.y==selStart.y && - cursor.xCoarse==selEnd.xCoarse && cursor.xFine==selEnd.xFine && cursor.y==selEnd.y) { - if (!settings.cursorMoveNoScroll) { - updateScroll(cursor.y); - } - } - } + pointUp(ev.button.x,ev.button.y,ev.button.button); break; case SDL_MOUSEBUTTONDOWN: - aboutOpen=false; - if (bindSetActive) { - bindSetActive=false; - bindSetPending=false; - actionKeys[bindSetTarget]=bindSetPrevValue; - bindSetTarget=0; - bindSetPrevValue=0; - } + pointDown(ev.button.x,ev.button.y,ev.button.button); break; case SDL_MOUSEWHEEL: wheelX+=ev.wheel.x; @@ -2687,6 +2823,22 @@ bool FurnaceGUI::loop() { scrW=ev.window.data1/dpiScale; scrH=ev.window.data2/dpiScale; #endif + portrait=(scrW0) { + showError(fmt::sprintf("Error while loading file! (%s)",lastError)); + } + } + } + } + if (recentFile.empty()) { + ImGui::Text("nothing here yet"); + } + ImGui::EndMenu(); + } ImGui::Separator(); if (ImGui::MenuItem("save",BIND_FOR(GUI_ACTION_SAVE))) { if (curFileName=="" || curFileName==backupPath || e->song.version>=0xff00) { @@ -2954,7 +3137,10 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem("save as...",BIND_FOR(GUI_ACTION_SAVE_AS))) { openFileDialog(GUI_FILE_SAVE); } - if (ImGui::MenuItem("save as .dmf (1.0/legacy)...",BIND_FOR(GUI_ACTION_SAVE_AS))) { + if (ImGui::MenuItem("save as .dmf (1.1.3+)...")) { + openFileDialog(GUI_FILE_SAVE_DMF); + } + if (ImGui::MenuItem("save as .dmf (1.0/legacy)...")) { openFileDialog(GUI_FILE_SAVE_DMF_LEGACY); } ImGui::Separator(); @@ -3158,6 +3344,11 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem("reset layout")) { showWarning("Are you sure you want to reset the workspace layout?",GUI_WARN_RESET_LAYOUT); } +#ifdef IS_MOBILE + if (ImGui::MenuItem("switch to mobile view")) { + toggleMobileUI(!mobileUI); + } +#endif if (ImGui::MenuItem("settings...",BIND_FOR(GUI_ACTION_WINDOW_SETTINGS))) { syncSettings(); settingsOpen=true; @@ -3280,9 +3471,41 @@ bool FurnaceGUI::loop() { if (mobileUI) { globalWinFlags=ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoBringToFrontOnFocus; + //globalWinFlags=ImGuiWindowFlags_NoTitleBar; + // scene handling goes here! + pianoOpen=true; drawMobileControls(); - drawPattern(); - drawPiano(); + switch (mobScene) { + case GUI_SCENE_ORDERS: + ordersOpen=true; + curWindow=GUI_WINDOW_ORDERS; + drawOrders(); + break; + case GUI_SCENE_INSTRUMENT: + insEditOpen=true; + curWindow=GUI_WINDOW_INS_EDIT; + drawInsEdit(); + drawPiano(); + break; + case GUI_SCENE_WAVETABLE: + waveEditOpen=true; + curWindow=GUI_WINDOW_WAVE_EDIT; + drawWaveEdit(); + drawPiano(); + break; + case GUI_SCENE_SAMPLE: + sampleEditOpen=true; + curWindow=GUI_WINDOW_SAMPLE_EDIT; + drawSampleEdit(); + drawPiano(); + break; + default: + patternOpen=true; + curWindow=GUI_WINDOW_PATTERN; + drawPattern(); + drawPiano(); + break; + } } else { globalWinFlags=0; ImGui::DockSpaceOverViewport(NULL,lockLayout?(ImGuiDockNodeFlags_NoWindowMenuButton|ImGuiDockNodeFlags_NoMove|ImGuiDockNodeFlags_NoResize|ImGuiDockNodeFlags_NoCloseButton|ImGuiDockNodeFlags_NoDocking|ImGuiDockNodeFlags_NoDockingSplitMe|ImGuiDockNodeFlags_NoDockingSplitOther):0); @@ -3325,6 +3548,13 @@ bool FurnaceGUI::loop() { if (firstFrame) { firstFrame=false; +#ifdef IS_MOBILE + SDL_GetWindowSize(sdlWin,&scrW,&scrH); + scrW/=dpiScale; + scrH/=dpiScale; + portrait=(scrWgetPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_INS_OPEN: case GUI_FILE_INS_OPEN_REPLACE: case GUI_FILE_INS_SAVE: + case GUI_FILE_INS_SAVE_DMP: workingDirIns=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_WAVE_OPEN: case GUI_FILE_WAVE_OPEN_REPLACE: case GUI_FILE_WAVE_SAVE: + case GUI_FILE_WAVE_SAVE_DMW: + case GUI_FILE_WAVE_SAVE_RAW: workingDirWave=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_SAMPLE_OPEN: @@ -3442,9 +3676,10 @@ bool FurnaceGUI::loop() { } if (fileName!="") { if (curFileDialog==GUI_FILE_SAVE) { - // 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); + checkExtension(".fur"); + } + if (curFileDialog==GUI_FILE_SAVE_DMF) { + checkExtension(".dmf"); } if (curFileDialog==GUI_FILE_SAVE_DMF_LEGACY) { checkExtension(".dmf"); @@ -3456,21 +3691,19 @@ bool FurnaceGUI::loop() { checkExtension(".wav"); } if (curFileDialog==GUI_FILE_INS_SAVE) { - // we can't tell whether the user chose .fui or .dmp in the system file picker - const char* fallbackExt=(settings.sysFileDialog || ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace instrument")?".fui":".dmp"; - checkExtensionDual(".fui",".dmp",fallbackExt); + checkExtension(".fui"); + } + if (curFileDialog==GUI_FILE_INS_SAVE_DMP) { + checkExtension(".dmp"); } if (curFileDialog==GUI_FILE_WAVE_SAVE) { - // same thing here - const char* fallbackExt=".fuw"; - if (!settings.sysFileDialog) { - if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="raw data") { - fallbackExt=".raw"; - } else if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="DefleMask wavetable") { - fallbackExt=".dmw"; - } - } - checkExtensionTriple(".fuw",".dmw",".raw",fallbackExt); + checkExtension(".fuw"); + } + if (curFileDialog==GUI_FILE_WAVE_SAVE_DMW) { + checkExtension(".dmw"); + } + if (curFileDialog==GUI_FILE_WAVE_SAVE_RAW) { + checkExtension(".raw"); } if (curFileDialog==GUI_FILE_EXPORT_VGM) { checkExtension(".vgm"); @@ -3501,21 +3734,10 @@ bool FurnaceGUI::loop() { break; case GUI_FILE_SAVE: { logD("saving: %s",copyOfName.c_str()); - String lowerCase=fileName; - for (char& i: lowerCase) { - if (i>='A' && i<='Z') i+='a'-'A'; - } bool saveWasSuccessful=true; - if ((lowerCase.size()<4 || lowerCase.rfind(".dmf")!=lowerCase.size()-4)) { - if (save(copyOfName,0)>0) { - showError(fmt::sprintf("Error while saving file! (%s)",lastError)); - saveWasSuccessful=false; - } - } else { - if (save(copyOfName,26)>0) { - showError(fmt::sprintf("Error while saving file! (%s)",lastError)); - saveWasSuccessful=false; - } + if (save(copyOfName,0)>0) { + showError(fmt::sprintf("Error while saving file! (%s)",lastError)); + saveWasSuccessful=false; } if (saveWasSuccessful && postWarnAction!=GUI_WARN_GENERIC) { switch (postWarnAction) { @@ -3548,6 +3770,12 @@ bool FurnaceGUI::loop() { } break; } + case GUI_FILE_SAVE_DMF: + logD("saving: %s",copyOfName.c_str()); + if (save(copyOfName,26)>0) { + showError(fmt::sprintf("Error while saving file! (%s)",lastError)); + } + break; case GUI_FILE_SAVE_DMF_LEGACY: logD("saving: %s",copyOfName.c_str()); if (save(copyOfName,24)>0) { @@ -3556,34 +3784,29 @@ bool FurnaceGUI::loop() { break; case GUI_FILE_INS_SAVE: if (curIns>=0 && curIns<(int)e->song.ins.size()) { - String lowerCase=fileName; - for (char& i: lowerCase) { - if (i>='A' && i<='Z') i+='a'-'A'; - } - if ((lowerCase.size()<4 || lowerCase.rfind(".dmp")!=lowerCase.size()-4)) { - e->song.ins[curIns]->save(copyOfName.c_str()); - } else { - if (!e->song.ins[curIns]->saveDMP(copyOfName.c_str())) { - showError("error while saving instrument! make sure your instrument is compatible."); - } + e->song.ins[curIns]->save(copyOfName.c_str()); + } + break; + case GUI_FILE_INS_SAVE_DMP: + if (curIns>=0 && curIns<(int)e->song.ins.size()) { + if (!e->song.ins[curIns]->saveDMP(copyOfName.c_str())) { + showError("error while saving instrument! make sure your instrument is compatible."); } } break; case GUI_FILE_WAVE_SAVE: if (curWave>=0 && curWave<(int)e->song.wave.size()) { - String lowerCase=fileName; - for (char& i: lowerCase) { - if (i>='A' && i<='Z') i+='a'-'A'; - } - if (lowerCase.size()<4) { - e->song.wave[curWave]->save(copyOfName.c_str()); - } else if (lowerCase.rfind(".dmw")==lowerCase.size()-4) { - e->song.wave[curWave]->saveDMW(copyOfName.c_str()); - } else if (lowerCase.rfind(".raw")==lowerCase.size()-4) { - e->song.wave[curWave]->saveRaw(copyOfName.c_str()); - } else { - e->song.wave[curWave]->save(copyOfName.c_str()); - } + e->song.wave[curWave]->save(copyOfName.c_str()); + } + break; + case GUI_FILE_WAVE_SAVE_DMW: + if (curWave>=0 && curWave<(int)e->song.wave.size()) { + e->song.wave[curWave]->saveDMW(copyOfName.c_str()); + } + break; + case GUI_FILE_WAVE_SAVE_RAW: + if (curWave>=0 && curWave<(int)e->song.wave.size()) { + e->song.wave[curWave]->saveRaw(copyOfName.c_str()); } break; case GUI_FILE_SAMPLE_OPEN: { @@ -4484,6 +4707,7 @@ bool FurnaceGUI::init() { tempoView=e->getConfBool("tempoView",true); waveHex=e->getConfBool("waveHex",false); + waveSigned=e->getConfBool("waveSigned",false); waveGenVisible=e->getConfBool("waveGenVisible",false); waveEditStyle=e->getConfInt("waveEditStyle",0); lockLayout=e->getConfBool("lockLayout",false); @@ -4531,6 +4755,13 @@ bool FurnaceGUI::init() { syncSettings(); + for (int i=0; igetConfString(fmt::sprintf("recentFile%d",i),""); + if (!r.empty()) { + recentFile.push_back(r); + } + } + if (settings.dpiScale>=0.5f) { dpiScale=settings.dpiScale; } @@ -4544,10 +4775,22 @@ bool FurnaceGUI::init() { SDL_Surface* icon=SDL_CreateRGBSurfaceFrom(furIcon,256,256,32,256*4,0xff,0xff00,0xff0000,0xff000000); #endif - scrW=e->getConfInt("lastWindowWidth",1280); - scrH=e->getConfInt("lastWindowHeight",800); +#ifdef IS_MOBILE + scrW=960; + scrH=540; + scrX=0; + scrY=0; +#else + scrW=scrConfW=e->getConfInt("lastWindowWidth",1280); + scrH=scrConfH=e->getConfInt("lastWindowHeight",800); + scrX=scrConfX=e->getConfInt("lastWindowX",SDL_WINDOWPOS_CENTERED); + scrY=scrConfY=e->getConfInt("lastWindowY",SDL_WINDOWPOS_CENTERED); + scrMax=e->getConfBool("lastWindowMax",false); +#endif + portrait=(scrWdisplaySize.w/dpiScale) scrW=(displaySize.w/dpiScale)-32; if (scrH>displaySize.h/dpiScale) scrH=(displaySize.h/dpiScale)-32; + portrait=(scrWsetConf("spoilerOpen",spoilerOpen); // commit last window size - e->setConf("lastWindowWidth",scrW); - e->setConf("lastWindowHeight",scrH); + e->setConf("lastWindowWidth",scrConfW); + e->setConf("lastWindowHeight",scrConfH); + e->setConf("lastWindowX",settings.saveWindowPos?scrConfX:(int)SDL_WINDOWPOS_CENTERED); + e->setConf("lastWindowY",settings.saveWindowPos?scrConfY:(int)SDL_WINDOWPOS_CENTERED); + e->setConf("lastWindowMax",scrMax); e->setConf("tempoView",tempoView); e->setConf("waveHex",waveHex); + e->setConf("waveSigned",waveSigned); e->setConf("waveGenVisible",waveGenVisible); e->setConf("waveEditStyle",waveEditStyle); e->setConf("lockLayout",lockLayout); @@ -4774,6 +5036,16 @@ bool FurnaceGUI::finish() { e->setConf("chanOscUseGrad",chanOscUseGrad); e->setConf("chanOscGrad",chanOscGrad.toString()); + // commit recent files + for (int i=0; i<30; i++) { + String key=fmt::sprintf("recentFile%d",i); + if (i>=settings.maxRecentFile || i>=(int)recentFile.size()) { + e->setConf(key,""); + } else { + e->setConf(key,recentFile[i]); + } + } + for (int i=0; i hell this->min=macroMin; this->max=macroMax; @@ -980,8 +1000,10 @@ class FurnaceGUI { std::vector sysSearchResults; std::vector newSongSearchResults; + std::deque recentFile; bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting, vgmExportLoop, zsmExportLoop, vgmExportPatternHints; + bool portrait, mobileMenuOpen; bool wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu; bool displayNew, fullScreen, preserveChanPos, wantScrollList, noteInputPoly; bool displayPendingIns, pendingInsSingle, displayPendingRawSample; @@ -991,6 +1013,7 @@ class FurnaceGUI { int zsmExportTickRate; int macroPointSize; int waveEditStyle; + float mobileMenuPos; const int* curSysSection; String pendingRawSample; @@ -1002,10 +1025,13 @@ class FurnaceGUI { FurnaceGUIFileDialogs curFileDialog; FurnaceGUIWarnings warnAction; FurnaceGUIWarnings postWarnAction; + FurnaceGUIMobileScenes mobScene; FurnaceGUIFileDialog* fileDialog; - int scrW, scrH; + int scrW, scrH, scrConfW, scrConfH; + int scrX, scrY, scrConfX, scrConfY; + bool scrMax; double dpiScale; @@ -1052,6 +1078,7 @@ class FurnaceGUI { int snCore; int nesCore; int fdsCore; + int c64Core; int pcSpeakerOutMethod; String yrw801Path; String tg100Path; @@ -1138,6 +1165,7 @@ class FurnaceGUI { int dragMovesSelection; int unsignedDetune; int noThreadedInput; + int saveWindowPos; int clampSamples; int saveUnusedPatterns; int channelColors; @@ -1145,6 +1173,7 @@ class FurnaceGUI { int channelStyle; int channelVolStyle; int channelFeedbackStyle; + int maxRecentFile; unsigned int maxUndoSteps; String mainFontPath; String patFontPath; @@ -1171,6 +1200,7 @@ class FurnaceGUI { snCore(0), nesCore(0), fdsCore(0), + c64Core(1), pcSpeakerOutMethod(0), yrw801Path(""), tg100Path(""), @@ -1264,6 +1294,7 @@ class FurnaceGUI { channelStyle(0), channelVolStyle(0), channelFeedbackStyle(1), + maxRecentFile(10), maxUndoSteps(100), mainFontPath(""), patFontPath(""), @@ -1297,7 +1328,7 @@ class FurnaceGUI { SelectionPoint selStart, selEnd, cursor, cursorDrag, dragStart, dragEnd; bool selecting, selectingFull, dragging, curNibble, orderNibble, followOrders, followPattern, changeAllOrders, mobileUI; - bool collapseWindow, demandScrollX, fancyPattern, wantPatName, firstFrame, tempoView, waveHex, waveGenVisible, lockLayout, editOptsVisible, latchNibble, nonLatchNibble; + bool collapseWindow, demandScrollX, fancyPattern, wantPatName, firstFrame, tempoView, waveHex, waveSigned, waveGenVisible, lockLayout, editOptsVisible, latchNibble, nonLatchNibble; FurnaceGUIWindows curWindow, nextWindow, curWindowLast; float peak[2]; float patChanX[DIV_MAX_CHANS+1]; @@ -1441,7 +1472,7 @@ class FurnaceGUI { int renderTimeBegin, renderTimeEnd, renderTimeDelta; int eventTimeBegin, eventTimeEnd, eventTimeDelta; - int chanToMove, sysToMove, sysToDelete; + int chanToMove, sysToMove, sysToDelete, opToMove; ImVec2 patWindowPos, patWindowSize; @@ -1545,7 +1576,9 @@ class FurnaceGUI { float waveGenPhase[16]; float waveGenTL[4]; int waveGenMult[4]; - float waveGenFB[4]; + int waveGenFB[4]; + int waveGenScaleX, waveGenScaleY, waveGenOffsetX, waveGenOffsetY, waveGenSmooth; + float waveGenAmplify; bool waveGenFMCon1[4]; bool waveGenFMCon2[3]; bool waveGenFMCon3[2]; @@ -1584,16 +1617,19 @@ class FurnaceGUI { void toggleMobileUI(bool enable, bool force=false); + void pushToggleColors(bool status); + void popToggleColors(); + void drawMobileControls(); void drawEditControls(); void drawSongInfo(); void drawOrders(); void drawPattern(); - void drawInsList(); + void drawInsList(bool asChild=false); void drawInsEdit(); - void drawWaveList(); + void drawWaveList(bool asChild=false); void drawWaveEdit(); - void drawSampleList(); + void drawSampleList(bool asChild=false); void drawSampleEdit(); void drawMixer(); void drawOsc(); @@ -1692,9 +1728,14 @@ class FurnaceGUI { void keyDown(SDL_Event& ev); void keyUp(SDL_Event& ev); + void pointDown(int x, int y, int button); + void pointUp(int x, int y, int button); + void pointMotion(int x, int y, int xrel, int yrel); + void openFileDialog(FurnaceGUIFileDialogs type); int save(String path, int dmfVersion); int load(String path); + void pushRecentFile(String path); void exportAudio(String path, DivAudioExportModes mode); bool parseSysEx(unsigned char* data, size_t len); @@ -1704,7 +1745,7 @@ class FurnaceGUI { void encodeMMLStr(String& target, int* macro, int macroLen, int macroLoop, int macroRel, bool hex=false, bool bit30=false); void decodeMMLStr(String& source, int* macro, unsigned char& macroLen, unsigned char& macroLoop, int macroMin, int macroMax, unsigned char& macroRel, bool bit30=false); - void decodeMMLStrW(String& source, int* macro, int& macroLen, int macroMax, bool hex=false); + void decodeMMLStrW(String& source, int* macro, int& macroLen, int macroMin, int macroMax, bool hex=false); String encodeKeyMap(std::map& map); void decodeKeyMap(std::map& map, String source); @@ -1724,6 +1765,7 @@ class FurnaceGUI { void runBackupThread(); void pushPartBlend(); void popPartBlend(); + bool detectOutOfBoundsWindow(); int processEvent(SDL_Event* ev); bool loop(); bool finish(); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index a72b40b0..6851eef0 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -571,6 +571,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("INS_LIST_OPEN", "Open", 0), D("INS_LIST_OPEN_REPLACE", "Open (replace current)", 0), D("INS_LIST_SAVE", "Save", 0), + D("INS_LIST_SAVE_DMP", "Save (.dmp)", 0), D("INS_LIST_MOVE_UP", "Move up", FURKMOD_SHIFT|SDLK_UP), D("INS_LIST_MOVE_DOWN", "Move down", FURKMOD_SHIFT|SDLK_DOWN), D("INS_LIST_DELETE", "Delete", 0), @@ -585,6 +586,8 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("WAVE_LIST_OPEN", "Open", 0), D("WAVE_LIST_OPEN_REPLACE", "Open (replace current)", 0), D("WAVE_LIST_SAVE", "Save", 0), + D("WAVE_LIST_SAVE_DMW", "Save (.dmw)", 0), + D("WAVE_LIST_SAVE_RAW", "Save (raw)", 0), D("WAVE_LIST_MOVE_UP", "Move up", FURKMOD_SHIFT|SDLK_UP), D("WAVE_LIST_MOVE_DOWN", "Move down", FURKMOD_SHIFT|SDLK_DOWN), D("WAVE_LIST_DELETE", "Delete", 0), @@ -641,6 +644,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("SAMPLE_ZOOM_AUTO", "Toggle auto-zoom", FURKMOD_CMD|SDLK_0), D("SAMPLE_MAKE_INS", "Create instrument from sample", 0), D("SAMPLE_SET_LOOP", "Set loop to selection", FURKMOD_CMD|SDLK_l), + D("SAMPLE_CREATE_WAVE", "Create wavetable from selection", FURKMOD_CMD|SDLK_w), D("SAMPLE_MAX", "", NOT_AN_ACTION), D("ORDERS_MIN", "---Orders", NOT_AN_ACTION), diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index c89652a3..a4b99d14 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -316,27 +316,31 @@ const char* macroRelativeMode="Relative"; const char* macroQSoundMode="QSound"; const char* macroDummyMode="Bug"; -String macroHoverNote(int id, float val) { - if (val<-60 || val>=120) return "???"; - return fmt::sprintf("%d: %s",id,noteNames[(int)val+60]); +String macroHoverNote(int id, float val, void* u) { + int* macroVal=(int*)u; + if ((macroVal[id]&0xc0000000)==0x40000000 || (macroVal[id]&0xc0000000)==0x80000000) { + if (val<-60 || val>=120) return "???"; + return fmt::sprintf("%d: %s",id,noteNames[(int)val+60]); + } + return fmt::sprintf("%d: %d",id,(int)val); } -String macroHover(int id, float val) { +String macroHover(int id, float val, void* u) { return fmt::sprintf("%d: %d",id,val); } -String macroHoverLoop(int id, float val) { +String macroHoverLoop(int id, float val, void* u) { if (val>1) return "Release"; if (val>0) return "Loop"; return ""; } -String macroHoverBit30(int id, float val) { +String macroHoverBit30(int id, float val, void* u) { if (val>0) return "Fixed"; return "Relative"; } -String macroHoverES5506FilterMode(int id, float val) { +String macroHoverES5506FilterMode(int id, float val, void* u) { String mode="???"; switch (((int)val)&3) { case 0: @@ -357,7 +361,7 @@ String macroHoverES5506FilterMode(int id, float val) { return fmt::sprintf("%d: %s",id,mode); } -String macroLFOWaves(int id, float val) { +String macroLFOWaves(int id, float val, void* u) { switch (((int)val)&3) { case 0: return "Saw"; @@ -1355,7 +1359,7 @@ void FurnaceGUI::drawMacros(std::vector& macros) { if (i.isBitfield) { PlotBitfield("##IMacro",asInt,totalFit,0,i.bitfieldBits,i.max,ImVec2(availableWidth,(i.macro->open)?(i.height*dpiScale):(32.0f*dpiScale)),sizeof(float),doHighlight); } else { - PlotCustom("##IMacro",asFloat,totalFit,macroDragScroll,NULL,i.min+i.macro->vScroll,i.min+i.macro->vScroll+i.macro->vZoom,ImVec2(availableWidth,(i.macro->open)?(i.height*dpiScale):(32.0f*dpiScale)),sizeof(float),i.color,i.macro->len-macroDragScroll,i.hoverFunc,i.blockMode,i.macro->open?genericGuide:NULL,doHighlight); + PlotCustom("##IMacro",asFloat,totalFit,macroDragScroll,NULL,i.min+i.macro->vScroll,i.min+i.macro->vScroll+i.macro->vZoom,ImVec2(availableWidth,(i.macro->open)?(i.height*dpiScale):(32.0f*dpiScale)),sizeof(float),i.color,i.macro->len-macroDragScroll,i.hoverFunc,i.hoverFuncUser,i.blockMode,i.macro->open?genericGuide:NULL,doHighlight); } if (i.macro->open && (ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right))) { macroDragStart=ImGui::GetItemRectMin(); @@ -1553,6 +1557,47 @@ void FurnaceGUI::drawMacros(std::vector& macros) { #define CENTER_TEXT_20(text) \ ImGui::SetCursorPosX(ImGui::GetCursorPosX()+0.5*(20.0f*dpiScale-ImGui::CalcTextSize(text).x)); +#define OP_DRAG_POINT \ + if (ImGui::Button(ICON_FA_ARROWS)) { \ + } \ + if (ImGui::BeginDragDropSource()) { \ + opToMove=i; \ + ImGui::SetDragDropPayload("FUR_OP",NULL,0,ImGuiCond_Once); \ + ImGui::Button(ICON_FA_ARROWS "##SysDrag"); \ + ImGui::SameLine(); \ + if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { \ + ImGui::Text("(copying)"); \ + } else { \ + ImGui::Text("(swapping)"); \ + } \ + ImGui::EndDragDropSource(); \ + } else if (ImGui::IsItemHovered()) { \ + ImGui::SetTooltip("- drag to swap operator\n- shift-drag to copy operator"); \ + } \ + if (ImGui::BeginDragDropTarget()) { \ + const ImGuiPayload* dragItem=ImGui::AcceptDragDropPayload("FUR_OP"); \ + if (dragItem!=NULL) { \ + if (dragItem->IsDataType("FUR_OP")) { \ + if (opToMove!=i && opToMove>=0) { \ + if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { \ + e->lockEngine([this,ins,i]() { \ + ins->fm.op[orderedOps[i]]=ins->fm.op[orderedOps[opToMove]]; \ + }); \ + } else { \ + e->lockEngine([this,ins,i]() { \ + DivInstrumentFM::Operator origOp=ins->fm.op[orderedOps[opToMove]]; \ + ins->fm.op[orderedOps[opToMove]]=ins->fm.op[orderedOps[i]]; \ + ins->fm.op[orderedOps[i]]=origOp; \ + }); \ + } \ + PARAMETER; \ + } \ + opToMove=-1; \ + } \ + } \ + ImGui::EndDragDropTarget(); \ + } + void FurnaceGUI::drawInsEdit() { if (nextWindow==GUI_WINDOW_INS_EDIT) { insEditOpen=true; @@ -1560,7 +1605,14 @@ void FurnaceGUI::drawInsEdit() { nextWindow=GUI_WINDOW_NOTHING; } if (!insEditOpen) return; - ImGui::SetNextWindowSizeConstraints(ImVec2(440.0f*dpiScale,400.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + if (mobileUI) { + patWindowPos=(portrait?ImVec2(0.0f,(mobileMenuPos*-0.65*scrH*dpiScale)):ImVec2((0.16*scrH*dpiScale)+0.5*scrW*dpiScale*mobileMenuPos,0.0f)); + patWindowSize=(portrait?ImVec2(scrW*dpiScale,scrH*dpiScale-(0.16*scrW*dpiScale)-(pianoOpen?(0.4*scrW*dpiScale):0.0f)):ImVec2(scrW*dpiScale-(0.16*scrH*dpiScale),scrH*dpiScale-(pianoOpen?(0.3*scrH*dpiScale):0.0f))); + ImGui::SetNextWindowPos(patWindowPos); + ImGui::SetNextWindowSize(patWindowSize); + } else { + ImGui::SetNextWindowSizeConstraints(ImVec2(440.0f*dpiScale,400.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + } if (ImGui::Begin("Instrument Editor",&insEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking))) { if (curIns<0 || curIns>=(int)e->song.ins.size()) { ImGui::Text("no instrument selected"); @@ -1608,6 +1660,12 @@ void FurnaceGUI::drawInsEdit() { if (ImGui::Button(ICON_FA_FLOPPY_O "##IESave")) { doAction(GUI_ACTION_INS_LIST_SAVE); } + if (ImGui::BeginPopupContextItem("InsSaveFormats",ImGuiMouseButton_Right)) { + if (ImGui::MenuItem("save as .dmp...")) { + doAction(GUI_ACTION_INS_LIST_SAVE_DMP); + } + ImGui::EndPopup(); + } ImGui::TableNextColumn(); ImGui::Text("Type"); @@ -1691,6 +1749,7 @@ void FurnaceGUI::drawInsEdit() { int opCount=4; if (ins->type==DIV_INS_OPLL) opCount=2; if (ins->type==DIV_INS_OPL) opCount=(ins->fm.ops==4)?4:2; + bool opsAreMutable=(ins->type==DIV_INS_FM); if (ImGui::BeginTabItem("FM")) { if (ImGui::BeginTable("fmDetails",3,ImGuiTableFlags_SizingStretchSame)) { @@ -2045,17 +2104,31 @@ void FurnaceGUI::drawInsEdit() { if (i==0) sliderHeight=(ImGui::GetContentRegionAvail().y/opCount)-ImGui::GetStyle().ItemSpacing.y; ImGui::PushID(fmt::sprintf("op%d",i).c_str()); + String opNameLabel; if (ins->type==DIV_INS_OPL_DRUMS) { - ImGui::Text("%s",oplDrumNames[i]); + opNameLabel=fmt::sprintf("%s",oplDrumNames[i]); } else if (ins->type==DIV_INS_OPL && ins->fm.opllPreset==16) { if (i==1) { - ImGui::Text("Kick"); + opNameLabel="Kick"; } else { - ImGui::Text("Env"); + opNameLabel="Env"; } } else { - ImGui::Text("OP%d",i+1); + opNameLabel=fmt::sprintf("OP%d",i+1); } + if (opsAreMutable) { + pushToggleColors(op.enable); + if (ImGui::Button(opNameLabel.c_str())) { + op.enable=!op.enable; + PARAMETER; + } + popToggleColors(); + } else { + ImGui::TextUnformatted(opNameLabel.c_str()); + } + + // drag point + OP_DRAG_POINT; int maxTl=127; if (ins->type==DIV_INS_OPLL) { @@ -2339,8 +2412,20 @@ void FurnaceGUI::drawInsEdit() { } else { snprintf(tempID,1024,"Operator %d",i+1); } - CENTER_TEXT(tempID); - ImGui::TextUnformatted(tempID); + float nextCursorPosX=ImGui::GetCursorPosX()+0.5*(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize(tempID).x-(opsAreMutable?(ImGui::GetStyle().FramePadding.x*2.0f):0.0f)); + OP_DRAG_POINT; + ImGui::SameLine(); + ImGui::SetCursorPosX(nextCursorPosX); + if (opsAreMutable) { + pushToggleColors(op.enable); + if (ImGui::Button(tempID)) { + op.enable=!op.enable; + PARAMETER; + } + popToggleColors(); + } else { + ImGui::TextUnformatted(tempID); + } float sliderHeight=200.0f*dpiScale; float waveWidth=140.0*dpiScale; @@ -2772,16 +2857,29 @@ void FurnaceGUI::drawInsEdit() { } ImGui::Dummy(ImVec2(dpiScale,dpiScale)); + String opNameLabel; + OP_DRAG_POINT; + ImGui::SameLine(); if (ins->type==DIV_INS_OPL_DRUMS) { - ImGui::Text("%s",oplDrumNames[i]); + opNameLabel=fmt::sprintf("%s",oplDrumNames[i]); } else if (ins->type==DIV_INS_OPL && ins->fm.opllPreset==16) { if (i==1) { - ImGui::Text("Envelope 2 (kick only)"); + opNameLabel="Envelope 2 (kick only)"; } else { - ImGui::Text("Envelope"); + opNameLabel="Envelope"; } } else { - ImGui::Text("OP%d",i+1); + opNameLabel=fmt::sprintf("OP%d",i+1); + } + if (opsAreMutable) { + pushToggleColors(op.enable); + if (ImGui::Button(opNameLabel.c_str())) { + op.enable=!op.enable; + PARAMETER; + } + popToggleColors(); + } else { + ImGui::TextUnformatted(opNameLabel.c_str()); } ImGui::SameLine(); @@ -3015,7 +3113,14 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc("PM Depth",&ins->std.ex2Macro,0,127,128,uiColors[GUI_COLOR_MACRO_OTHER])); macroList.push_back(FurnaceGUIMacroDesc("LFO Speed",&ins->std.ex3Macro,0,255,128,uiColors[GUI_COLOR_MACRO_OTHER])); macroList.push_back(FurnaceGUIMacroDesc("LFO Shape",&ins->std.waveMacro,0,3,48,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,macroLFOWaves)); + } + if (ins->type==DIV_INS_FM) { macroList.push_back(FurnaceGUIMacroDesc("OpMask",&ins->std.ex4Macro,0,4,128,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,fmOperatorBits)); + } else if (ins->type==DIV_INS_OPZ) { + macroList.push_back(FurnaceGUIMacroDesc("AM Depth 2",&ins->std.ex5Macro,0,127,128,uiColors[GUI_COLOR_MACRO_OTHER])); + macroList.push_back(FurnaceGUIMacroDesc("PM Depth 2",&ins->std.ex6Macro,0,127,128,uiColors[GUI_COLOR_MACRO_OTHER])); + macroList.push_back(FurnaceGUIMacroDesc("LFO2 Speed",&ins->std.ex7Macro,0,255,128,uiColors[GUI_COLOR_MACRO_OTHER])); + macroList.push_back(FurnaceGUIMacroDesc("LFO2 Shape",&ins->std.ex8Macro,0,3,48,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,macroLFOWaves)); } drawMacros(macroList); ImGui::EndTabItem(); @@ -3346,29 +3451,29 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_C64) if (ImGui::BeginTabItem("C64")) { ImGui::Text("Waveform"); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.triOn)); + pushToggleColors(ins->c64.triOn); if (ImGui::Button("tri")) { PARAMETER ins->c64.triOn=!ins->c64.triOn; } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.sawOn)); + pushToggleColors(ins->c64.sawOn); if (ImGui::Button("saw")) { PARAMETER ins->c64.sawOn=!ins->c64.sawOn; } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.pulseOn)); + pushToggleColors(ins->c64.pulseOn); if (ImGui::Button("pulse")) { PARAMETER ins->c64.pulseOn=!ins->c64.pulseOn; } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.noiseOn)); + pushToggleColors(ins->c64.noiseOn); if (ImGui::Button("noise")) { PARAMETER ins->c64.noiseOn=!ins->c64.noiseOn; } - ImGui::PopStyleColor(); + popToggleColors(); ImVec2 sliderSize=ImVec2(20.0f*dpiScale,128.0*dpiScale); @@ -3430,29 +3535,29 @@ void FurnaceGUI::drawInsEdit() { ImGui::Text("Filter Mode"); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.lp)); + pushToggleColors(ins->c64.lp); if (ImGui::Button("low")) { PARAMETER ins->c64.lp=!ins->c64.lp; } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.bp)); + pushToggleColors(ins->c64.bp); if (ImGui::Button("band")) { PARAMETER ins->c64.bp=!ins->c64.bp; } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.hp)); + pushToggleColors(ins->c64.hp); if (ImGui::Button("high")) { PARAMETER ins->c64.hp=!ins->c64.hp; } - ImGui::PopStyleColor(); + popToggleColors(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.ch3off)); + pushToggleColors(ins->c64.ch3off); if (ImGui::Button("ch3off")) { PARAMETER ins->c64.ch3off=!ins->c64.ch3off; } - ImGui::PopStyleColor(); + popToggleColors(); P(ImGui::Checkbox("Volume Macro is Cutoff Macro",&ins->c64.volIsCutoff)); P(ImGui::Checkbox("Absolute Cutoff Macro",&ins->c64.filterIsAbs)); @@ -3600,7 +3705,7 @@ void FurnaceGUI::drawInsEdit() { modTable[i]=ins->fds.modTable[i]; } ImVec2 modTableSize=ImVec2(ImGui::GetContentRegionAvail().x,96.0f*dpiScale); - PlotCustom("ModTable",modTable,32,0,NULL,-4,3,modTableSize,sizeof(float),ImVec4(1.0f,1.0f,1.0f,1.0f),0,NULL,true); + PlotCustom("ModTable",modTable,32,0,NULL,-4,3,modTableSize,sizeof(float),ImVec4(1.0f,1.0f,1.0f,1.0f),0,NULL,NULL,true); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroDragStart=ImGui::GetItemRectMin(); macroDragAreaSize=modTableSize; @@ -4219,7 +4324,7 @@ void FurnaceGUI::drawInsEdit() { if (volMax>0) { macroList.push_back(FurnaceGUIMacroDesc(volumeLabel,&ins->std.volMacro,volMin,volMax,160,uiColors[GUI_COLOR_MACRO_VOLUME])); } - macroList.push_back(FurnaceGUIMacroDesc("Arpeggio",&ins->std.arpMacro,-120,120,160,uiColors[GUI_COLOR_MACRO_PITCH],true,NULL,NULL,false,NULL,0,true)); + macroList.push_back(FurnaceGUIMacroDesc("Arpeggio",&ins->std.arpMacro,-120,120,160,uiColors[GUI_COLOR_MACRO_PITCH],true,NULL,macroHoverNote,false,NULL,0,true,ins->std.arpMacro.val)); if (dutyMax>0) { if (ins->type==DIV_INS_MIKEY) { macroList.push_back(FurnaceGUIMacroDesc(dutyLabel,&ins->std.dutyMacro,0,dutyMax,160,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,mikeyFeedbackBits)); diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index 254c53be..ec65101d 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -372,10 +372,17 @@ void FurnaceGUI::drawPattern() { sel2.xFine^=sel1.xFine; } ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,ImVec2(0.0f,0.0f)); + if (mobileUI) { + patWindowPos=(portrait?ImVec2(0.0f,(mobileMenuPos*-0.65*scrH*dpiScale)):ImVec2((0.16*scrH*dpiScale)+0.5*scrW*dpiScale*mobileMenuPos,0.0f)); + patWindowSize=(portrait?ImVec2(scrW*dpiScale,scrH*dpiScale-(0.16*scrW*dpiScale)-(pianoOpen?(0.4*scrW*dpiScale):0.0f)):ImVec2(scrW*dpiScale-(0.16*scrH*dpiScale),scrH*dpiScale-(pianoOpen?(0.3*scrH*dpiScale):0.0f))); + ImGui::SetNextWindowPos(patWindowPos); + ImGui::SetNextWindowSize(patWindowSize); + } if (ImGui::Begin("Pattern",&patternOpen,globalWinFlags|(settings.avoidRaisingPattern?ImGuiWindowFlags_NoBringToFrontOnFocus:0))) { - //ImGui::SetWindowSize(ImVec2(scrW*dpiScale,scrH*dpiScale)); - patWindowPos=ImGui::GetWindowPos(); - patWindowSize=ImGui::GetWindowSize(); + if (!mobileUI) { + patWindowPos=ImGui::GetWindowPos(); + patWindowSize=ImGui::GetWindowSize(); + } //char id[32]; ImGui::PushFont(patFont); int ord=oldOrder; diff --git a/src/gui/piano.cpp b/src/gui/piano.cpp index 0e0063bb..082cc65e 100644 --- a/src/gui/piano.cpp +++ b/src/gui/piano.cpp @@ -52,27 +52,28 @@ void FurnaceGUI::drawPiano() { nextWindow=GUI_WINDOW_NOTHING; } if (!pianoOpen) return; + if (mobileUI) { + ImGui::SetNextWindowPos(ImVec2(patWindowPos.x,patWindowPos.y+patWindowSize.y)); + ImGui::SetNextWindowSize(portrait?ImVec2(scrW*dpiScale,0.4*scrW*dpiScale):ImVec2(scrW*dpiScale-(0.16*scrH*dpiScale),0.3*scrH*dpiScale)); + } if (ImGui::Begin("Piano",&pianoOpen,((pianoOptions)?0:ImGuiWindowFlags_NoTitleBar)|ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse|globalWinFlags)) { bool oldPianoKeyPressed[180]; memcpy(oldPianoKeyPressed,pianoKeyPressed,180*sizeof(bool)); memset(pianoKeyPressed,0,180*sizeof(bool)); - if (ImGui::BeginTable("PianoLayout",(pianoOptions?2:1)+((pianoInputPadMode==1 && cursor.xFine>0)?1:0),ImGuiTableFlags_BordersInnerV)) { + if (ImGui::BeginTable("PianoLayout",((pianoOptions && (!mobileUI || !portrait))?2:1),ImGuiTableFlags_BordersInnerV)) { int& off=(e->isPlaying() || pianoSharePosition)?pianoOffset:pianoOffsetEdit; int& oct=(e->isPlaying() || pianoSharePosition)?pianoOctaves:pianoOctavesEdit; bool view=(pianoView==2)?(!e->isPlaying()):pianoView; - if (pianoOptions) { + if (pianoOptions && (!mobileUI || !portrait)) { ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); } - if (pianoInputPadMode==1 && cursor.xFine>0) { - ImGui::TableSetupColumn("c0s",ImGuiTableColumnFlags_WidthStretch,2.0f); - } ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,1.0f); ImGui::TableNextRow(); if (pianoOptions) { ImGui::TableNextColumn(); - float optionSizeY=ImGui::GetContentRegionAvail().y*0.5-ImGui::GetStyle().ItemSpacing.y; - ImVec2 optionSize=ImVec2(1.2f*optionSizeY,optionSizeY); + float optionSizeY=ImGui::GetContentRegionAvail().y*((mobileUI && portrait)?0.3:0.5)-ImGui::GetStyle().ItemSpacing.y; + ImVec2 optionSize=ImVec2((mobileUI && portrait)?((ImGui::GetContentRegionAvail().x-ImGui::GetStyle().ItemSpacing.x*5.0f)/6.0f):(1.2f*optionSizeY),optionSizeY); if (pianoOptionsSet) { if (ImGui::Button("OFF##PianoNOff",optionSize)) { if (edit) noteInput(0,100); @@ -122,6 +123,10 @@ void FurnaceGUI::drawPiano() { ImGui::EndPopup(); } + if (mobileUI && portrait) { + ImGui::SameLine(); + } + if (pianoOptionsSet) { if (ImGui::Button("REL##PianoNMRel",optionSize)) { if (edit) noteInput(0,102); @@ -148,6 +153,10 @@ void FurnaceGUI::drawPiano() { } } + if (mobileUI && portrait) { + ImGui::TableNextRow(); + } + ImGui::TableNextColumn(); if (pianoInputPadMode==1 && cursor.xFine>0) { ImVec2 buttonSize=ImGui::GetContentRegionAvail(); @@ -195,192 +204,192 @@ void FurnaceGUI::drawPiano() { ImGui::EndTable(); } - ImGui::TableNextColumn(); - } - ImGuiWindow* window=ImGui::GetCurrentWindow(); - ImVec2 size=ImGui::GetContentRegionAvail(); - ImDrawList* dl=ImGui::GetWindowDrawList(); + } else { + ImGuiWindow* window=ImGui::GetCurrentWindow(); + ImVec2 size=ImGui::GetContentRegionAvail(); + ImDrawList* dl=ImGui::GetWindowDrawList(); - ImVec2 minArea=window->DC.CursorPos; - ImVec2 maxArea=ImVec2( - minArea.x+size.x, - minArea.y+size.y - ); - ImRect rect=ImRect(minArea,maxArea); + ImVec2 minArea=window->DC.CursorPos; + ImVec2 maxArea=ImVec2( + minArea.x+size.x, + minArea.y+size.y + ); + ImRect rect=ImRect(minArea,maxArea); - // render piano - //ImGui::ItemSize(size,ImGui::GetStyle().FramePadding.y); - if (ImGui::ItemAdd(rect,ImGui::GetID("pianoDisplay"))) { - ImGui::ItemHoverable(rect,ImGui::GetID("pianoDisplay")); - if (view) { - int notes=oct*12; - // evaluate input - for (TouchPoint& i: activePoints) { - if (rect.Contains(ImVec2(i.x,i.y))) { - int note=(((i.x-rect.Min.x)/(rect.Max.x-rect.Min.x))*notes)+12*off; - if (note<0) continue; - if (note>=180) continue; - pianoKeyPressed[note]=true; - } - } - - for (int i=0; i=180) continue; - float pkh=pianoKeyHit[note]; - ImVec4 color=isTopKey[i%12]?uiColors[GUI_COLOR_PIANO_KEY_TOP]:uiColors[GUI_COLOR_PIANO_KEY_BOTTOM]; - if (pianoKeyPressed[note]) { - color=isTopKey[i%12]?uiColors[GUI_COLOR_PIANO_KEY_TOP_ACTIVE]:uiColors[GUI_COLOR_PIANO_KEY_BOTTOM_ACTIVE]; - } else { - ImVec4 colorHit=isTopKey[i%12]?uiColors[GUI_COLOR_PIANO_KEY_TOP_HIT]:uiColors[GUI_COLOR_PIANO_KEY_BOTTOM_HIT]; - color.x+=(colorHit.x-color.x)*pkh; - color.y+=(colorHit.y-color.y)*pkh; - color.z+=(colorHit.z-color.z)*pkh; - color.w+=(colorHit.w-color.w)*pkh; - } - ImVec2 p0=ImLerp(rect.Min,rect.Max,ImVec2((float)i/notes,0.0f)); - ImVec2 p1=ImLerp(rect.Min,rect.Max,ImVec2((float)(i+1)/notes,1.0f)); - p1.x-=dpiScale; - dl->AddRectFilled(p0,p1,ImGui::ColorConvertFloat4ToU32(color)); - if ((i%12)==0) { - String label=fmt::sprintf("%d",(note-60)/12); - ImVec2 pText=ImLerp(p0,p1,ImVec2(0.5f,1.0f)); - ImVec2 labelSize=ImGui::CalcTextSize(label.c_str()); - pText.x-=labelSize.x*0.5f; - pText.y-=labelSize.y+ImGui::GetStyle().ItemSpacing.y; - dl->AddText(pText,0xff404040,label.c_str()); - } - } - } else { - int bottomNotes=7*oct; - // evaluate input - for (TouchPoint& i: activePoints) { - if (rect.Contains(ImVec2(i.x,i.y))) { - // top - int o=((i.x-rect.Min.x)/(rect.Max.x-rect.Min.x))*oct; - ImVec2 op0=ImLerp(rect.Min,rect.Max,ImVec2((float)o/oct,0.0f)); - ImVec2 op1=ImLerp(rect.Min,rect.Max,ImVec2((float)(o+1)/oct,1.0f)); - bool foundTopKey=false; - - for (int j=0; j<5; j++) { - int note=topKeyNotes[j]+12*(o+off); + // render piano + //ImGui::ItemSize(size,ImGui::GetStyle().FramePadding.y); + if (ImGui::ItemAdd(rect,ImGui::GetID("pianoDisplay"))) { + ImGui::ItemHoverable(rect,ImGui::GetID("pianoDisplay")); + if (view) { + int notes=oct*12; + // evaluate input + for (TouchPoint& i: activePoints) { + if (rect.Contains(ImVec2(i.x,i.y))) { + int note=(((i.x-rect.Min.x)/(rect.Max.x-rect.Min.x))*notes)+12*off; if (note<0) continue; if (note>=180) continue; - ImRect keyRect=ImRect( - ImLerp(op0,op1,ImVec2(topKeyStarts[j]-0.05f,0.0f)), - ImLerp(op0,op1,ImVec2(topKeyStarts[j]+0.05f,0.64f)) - ); - if (keyRect.Contains(ImVec2(i.x,i.y))) { - pianoKeyPressed[note]=true; - foundTopKey=true; - break; - } + pianoKeyPressed[note]=true; } - if (foundTopKey) continue; - - // bottom - int n=((i.x-rect.Min.x)/(rect.Max.x-rect.Min.x))*bottomNotes; - int note=bottomKeyNotes[n%7]+12*((n/7)+off); - if (note<0) continue; - if (note>=180) continue; - pianoKeyPressed[note]=true; - } - } - - for (int i=0; i=180) continue; - - float pkh=pianoKeyHit[note]; - ImVec4 color=uiColors[GUI_COLOR_PIANO_KEY_BOTTOM]; - if (pianoKeyPressed[note]) { - color=uiColors[GUI_COLOR_PIANO_KEY_BOTTOM_ACTIVE]; - } else { - ImVec4 colorHit=uiColors[GUI_COLOR_PIANO_KEY_BOTTOM_HIT]; - color.x+=(colorHit.x-color.x)*pkh; - color.y+=(colorHit.y-color.y)*pkh; - color.z+=(colorHit.z-color.z)*pkh; - color.w+=(colorHit.w-color.w)*pkh; } - ImVec2 p0=ImLerp(rect.Min,rect.Max,ImVec2((float)i/bottomNotes,0.0f)); - ImVec2 p1=ImLerp(rect.Min,rect.Max,ImVec2((float)(i+1)/bottomNotes,1.0f)); - p1.x-=dpiScale; - - dl->AddRectFilled(p0,p1,ImGui::ColorConvertFloat4ToU32(color)); - if ((i%7)==0) { - String label=fmt::sprintf("%d",(note-60)/12); - ImVec2 pText=ImLerp(p0,p1,ImVec2(0.5f,1.0f)); - ImVec2 labelSize=ImGui::CalcTextSize(label.c_str()); - pText.x-=labelSize.x*0.5f; - pText.y-=labelSize.y+ImGui::GetStyle().ItemSpacing.y; - dl->AddText(pText,0xff404040,label.c_str()); - } - } - - for (int i=0; i=180) continue; float pkh=pianoKeyHit[note]; - ImVec4 color=uiColors[GUI_COLOR_PIANO_KEY_TOP]; + ImVec4 color=isTopKey[i%12]?uiColors[GUI_COLOR_PIANO_KEY_TOP]:uiColors[GUI_COLOR_PIANO_KEY_BOTTOM]; if (pianoKeyPressed[note]) { - color=uiColors[GUI_COLOR_PIANO_KEY_TOP_ACTIVE]; + color=isTopKey[i%12]?uiColors[GUI_COLOR_PIANO_KEY_TOP_ACTIVE]:uiColors[GUI_COLOR_PIANO_KEY_BOTTOM_ACTIVE]; } else { - ImVec4 colorHit=uiColors[GUI_COLOR_PIANO_KEY_TOP_HIT]; + ImVec4 colorHit=isTopKey[i%12]?uiColors[GUI_COLOR_PIANO_KEY_TOP_HIT]:uiColors[GUI_COLOR_PIANO_KEY_BOTTOM_HIT]; color.x+=(colorHit.x-color.x)*pkh; color.y+=(colorHit.y-color.y)*pkh; color.z+=(colorHit.z-color.z)*pkh; color.w+=(colorHit.w-color.w)*pkh; } - ImVec2 p0=ImLerp(op0,op1,ImVec2(topKeyStarts[j]-0.05f,0.0f)); - ImVec2 p1=ImLerp(op0,op1,ImVec2(topKeyStarts[j]+0.05f,0.64f)); - dl->AddRectFilled(p0,p1,ImGui::GetColorU32(uiColors[GUI_COLOR_PIANO_BACKGROUND])); - p0.x+=dpiScale; + ImVec2 p0=ImLerp(rect.Min,rect.Max,ImVec2((float)i/notes,0.0f)); + ImVec2 p1=ImLerp(rect.Min,rect.Max,ImVec2((float)(i+1)/notes,1.0f)); p1.x-=dpiScale; - p1.y-=dpiScale; dl->AddRectFilled(p0,p1,ImGui::ColorConvertFloat4ToU32(color)); + if ((i%12)==0) { + String label=fmt::sprintf("%d",(note-60)/12); + ImVec2 pText=ImLerp(p0,p1,ImVec2(0.5f,1.0f)); + ImVec2 labelSize=ImGui::CalcTextSize(label.c_str()); + pText.x-=labelSize.x*0.5f; + pText.y-=labelSize.y+ImGui::GetStyle().ItemSpacing.y; + dl->AddText(pText,0xff404040,label.c_str()); + } + } + } else { + int bottomNotes=7*oct; + // evaluate input + for (TouchPoint& i: activePoints) { + if (rect.Contains(ImVec2(i.x,i.y))) { + // top + int o=((i.x-rect.Min.x)/(rect.Max.x-rect.Min.x))*oct; + ImVec2 op0=ImLerp(rect.Min,rect.Max,ImVec2((float)o/oct,0.0f)); + ImVec2 op1=ImLerp(rect.Min,rect.Max,ImVec2((float)(o+1)/oct,1.0f)); + bool foundTopKey=false; + + for (int j=0; j<5; j++) { + int note=topKeyNotes[j]+12*(o+off); + if (note<0) continue; + if (note>=180) continue; + ImRect keyRect=ImRect( + ImLerp(op0,op1,ImVec2(topKeyStarts[j]-0.05f,0.0f)), + ImLerp(op0,op1,ImVec2(topKeyStarts[j]+0.05f,0.64f)) + ); + if (keyRect.Contains(ImVec2(i.x,i.y))) { + pianoKeyPressed[note]=true; + foundTopKey=true; + break; + } + } + if (foundTopKey) continue; + + // bottom + int n=((i.x-rect.Min.x)/(rect.Max.x-rect.Min.x))*bottomNotes; + int note=bottomKeyNotes[n%7]+12*((n/7)+off); + if (note<0) continue; + if (note>=180) continue; + pianoKeyPressed[note]=true; + } + } + + for (int i=0; i=180) continue; + + float pkh=pianoKeyHit[note]; + ImVec4 color=uiColors[GUI_COLOR_PIANO_KEY_BOTTOM]; + if (pianoKeyPressed[note]) { + color=uiColors[GUI_COLOR_PIANO_KEY_BOTTOM_ACTIVE]; + } else { + ImVec4 colorHit=uiColors[GUI_COLOR_PIANO_KEY_BOTTOM_HIT]; + color.x+=(colorHit.x-color.x)*pkh; + color.y+=(colorHit.y-color.y)*pkh; + color.z+=(colorHit.z-color.z)*pkh; + color.w+=(colorHit.w-color.w)*pkh; + } + + ImVec2 p0=ImLerp(rect.Min,rect.Max,ImVec2((float)i/bottomNotes,0.0f)); + ImVec2 p1=ImLerp(rect.Min,rect.Max,ImVec2((float)(i+1)/bottomNotes,1.0f)); + p1.x-=dpiScale; + + dl->AddRectFilled(p0,p1,ImGui::ColorConvertFloat4ToU32(color)); + if ((i%7)==0) { + String label=fmt::sprintf("%d",(note-60)/12); + ImVec2 pText=ImLerp(p0,p1,ImVec2(0.5f,1.0f)); + ImVec2 labelSize=ImGui::CalcTextSize(label.c_str()); + pText.x-=labelSize.x*0.5f; + pText.y-=labelSize.y+ImGui::GetStyle().ItemSpacing.y; + dl->AddText(pText,0xff404040,label.c_str()); + } + } + + for (int i=0; i=180) continue; + float pkh=pianoKeyHit[note]; + ImVec4 color=uiColors[GUI_COLOR_PIANO_KEY_TOP]; + if (pianoKeyPressed[note]) { + color=uiColors[GUI_COLOR_PIANO_KEY_TOP_ACTIVE]; + } else { + ImVec4 colorHit=uiColors[GUI_COLOR_PIANO_KEY_TOP_HIT]; + color.x+=(colorHit.x-color.x)*pkh; + color.y+=(colorHit.y-color.y)*pkh; + color.z+=(colorHit.z-color.z)*pkh; + color.w+=(colorHit.w-color.w)*pkh; + } + ImVec2 p0=ImLerp(op0,op1,ImVec2(topKeyStarts[j]-0.05f,0.0f)); + ImVec2 p1=ImLerp(op0,op1,ImVec2(topKeyStarts[j]+0.05f,0.64f)); + dl->AddRectFilled(p0,p1,ImGui::GetColorU32(uiColors[GUI_COLOR_PIANO_BACKGROUND])); + p0.x+=dpiScale; + p1.x-=dpiScale; + p1.y-=dpiScale; + dl->AddRectFilled(p0,p1,ImGui::ColorConvertFloat4ToU32(color)); + } + } + } + + const float reduction=ImGui::GetIO().DeltaTime*60.0f*0.12; + for (int i=0; i<180; i++) { + pianoKeyHit[i]-=reduction; + if (pianoKeyHit[i]<0) pianoKeyHit[i]=0; + } + } + + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + pianoOptions=!pianoOptions; + } + + // first check released keys + for (int i=0; i<180; i++) { + int note=i-60; + if (!pianoKeyPressed[i]) { + if (pianoKeyPressed[i]!=oldPianoKeyPressed[i]) { + e->synchronized([this,note]() { + e->autoNoteOff(-1,note); + }); } } } - - const float reduction=ImGui::GetIO().DeltaTime*60.0f*0.12; + // then pressed ones for (int i=0; i<180; i++) { - pianoKeyHit[i]-=reduction; - if (pianoKeyHit[i]<0) pianoKeyHit[i]=0; - } - } - - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - pianoOptions=!pianoOptions; - } - - // first check released keys - for (int i=0; i<180; i++) { - int note=i-60; - if (!pianoKeyPressed[i]) { - if (pianoKeyPressed[i]!=oldPianoKeyPressed[i]) { - e->synchronized([this,note]() { - e->autoNoteOff(-1,note); - }); - } - } - } - // then pressed ones - for (int i=0; i<180; i++) { - int note=i-60; - if (pianoKeyPressed[i]) { - if (pianoKeyPressed[i]!=oldPianoKeyPressed[i]) { - e->synchronized([this,note]() { - e->autoNoteOn(-1,curIns,note); - }); - if (edit) noteInput(note,0); + int note=i-60; + if (pianoKeyPressed[i]) { + if (pianoKeyPressed[i]!=oldPianoKeyPressed[i]) { + e->synchronized([this,note]() { + e->autoNoteOn(-1,curIns,note); + }); + if (edit) noteInput(note,0); + } } } } diff --git a/src/gui/plot_nolerp.cpp b/src/gui/plot_nolerp.cpp index d802cbc5..b75bda4f 100644 --- a/src/gui/plot_nolerp.cpp +++ b/src/gui/plot_nolerp.cpp @@ -293,7 +293,7 @@ void PlotBitfield(const char* label, const int* values, int values_count, int va PlotBitfieldEx(label, &Plot_IntArrayGetter, (void*)&data, values_count, values_offset, overlay_text, bits, graph_size, values_highlight, highlightColor); } -int PlotCustomEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_display_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 frame_size, ImVec4 color, int highlight, std::string (*hoverFunc)(int,float), bool blockMode, std::string (*guideFunc)(float), const bool* values_highlight, ImVec4 highlightColor) +int PlotCustomEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_display_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 frame_size, ImVec4 color, int highlight, std::string (*hoverFunc)(int,float,void*), void* hoverFuncUser, bool blockMode, std::string (*guideFunc)(float), const bool* values_highlight, ImVec4 highlightColor) { ImGuiContext& g = *GImGui; ImGuiWindow* window = ImGui::GetCurrentWindow(); @@ -359,7 +359,7 @@ int PlotCustomEx(ImGuiPlotType plot_type, const char* label, float (*values_gett const float v0 = values_getter(data, (v_idx) % values_count); const float v1 = values_getter(data, (v_idx + 1) % values_count); if (hoverFunc) { - std::string hoverText=hoverFunc(v_idx+values_display_offset,v0); + std::string hoverText=hoverFunc(v_idx+values_display_offset,v0,hoverFuncUser); if (!hoverText.empty()) { ImGui::SetTooltip("%s",hoverText.c_str()); } @@ -459,8 +459,8 @@ int PlotCustomEx(ImGuiPlotType plot_type, const char* label, float (*values_gett return idx_hovered; } -void PlotCustom(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride, ImVec4 color, int highlight, std::string (*hoverFunc)(int,float), bool blockMode, std::string (*guideFunc)(float), const bool* values_highlight, ImVec4 highlightColor) +void PlotCustom(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride, ImVec4 color, int highlight, std::string (*hoverFunc)(int,float,void*), void* hoverFuncUser, bool blockMode, std::string (*guideFunc)(float), const bool* values_highlight, ImVec4 highlightColor) { FurnacePlotArrayGetterData data(values, stride); - PlotCustomEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size, color, highlight, hoverFunc, blockMode, guideFunc, values_highlight, highlightColor); -} \ No newline at end of file + PlotCustomEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size, color, highlight, hoverFunc, hoverFuncUser, blockMode, guideFunc, values_highlight, highlightColor); +} diff --git a/src/gui/plot_nolerp.h b/src/gui/plot_nolerp.h index b353e6f8..48332b33 100644 --- a/src/gui/plot_nolerp.h +++ b/src/gui/plot_nolerp.h @@ -22,4 +22,4 @@ void PlotNoLerp(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); void PlotBitfield(const char* label, const int* values, int values_count, int values_offset = 0, const char** overlay_text = NULL, int bits = 8, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float), const bool* values_highlight = NULL, ImVec4 highlightColor = ImVec4(1.0f,1.0f,1.0f,1.0f)); -void PlotCustom(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float), ImVec4 fgColor = ImVec4(1.0f,1.0f,1.0f,1.0f), int highlight = 0, std::string (*hoverFunc)(int,float) = NULL, bool blockMode=false, std::string (*guideFunc)(float) = NULL, const bool* values_highlight = NULL, ImVec4 highlightColor = ImVec4(1.0f,1.0f,1.0f,1.0f)); \ No newline at end of file +void PlotCustom(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float), ImVec4 fgColor = ImVec4(1.0f,1.0f,1.0f,1.0f), int highlight = 0, std::string (*hoverFunc)(int,float,void*) = NULL, void* hoverFuncUser=NULL, bool blockMode=false, std::string (*guideFunc)(float) = NULL, const bool* values_highlight = NULL, ImVec4 highlightColor = ImVec4(1.0f,1.0f,1.0f,1.0f)); diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index e9b5cfb3..51411c43 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -35,6 +35,12 @@ void FurnaceGUI::drawSampleEdit() { nextWindow=GUI_WINDOW_NOTHING; } if (!sampleEditOpen) return; + if (mobileUI) { + patWindowPos=(portrait?ImVec2(0.0f,(mobileMenuPos*-0.65*scrH*dpiScale)):ImVec2((0.16*scrH*dpiScale)+0.5*scrW*dpiScale*mobileMenuPos,0.0f)); + patWindowSize=(portrait?ImVec2(scrW*dpiScale,scrH*dpiScale-(0.16*scrW*dpiScale)-(pianoOpen?(0.4*scrW*dpiScale):0.0f)):ImVec2(scrW*dpiScale-(0.16*scrH*dpiScale),scrH*dpiScale-(pianoOpen?(0.3*scrH*dpiScale):0.0f))); + ImGui::SetNextWindowPos(patWindowPos); + ImGui::SetNextWindowSize(patWindowSize); + } if (ImGui::Begin("Sample Editor",&sampleEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking))) { if (curSample<0 || curSample>=(int)e->song.sample.size()) { ImGui::Text("no sample selected"); @@ -146,20 +152,20 @@ void FurnaceGUI::drawSampleEdit() { ImGui::BeginDisabled(sample->depth!=DIV_SAMPLE_DEPTH_8BIT && sample->depth!=DIV_SAMPLE_DEPTH_16BIT); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(!sampleDragMode)); + pushToggleColors(!sampleDragMode); if (ImGui::Button(ICON_FA_I_CURSOR "##SSelect")) { sampleDragMode=false; } - ImGui::PopStyleColor(); + popToggleColors(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Edit mode: Select"); } ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(sampleDragMode)); + pushToggleColors(sampleDragMode); if (ImGui::Button(ICON_FA_PENCIL "##SDraw")) { sampleDragMode=true; } - ImGui::PopStyleColor(); + popToggleColors(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Edit mode: Draw"); } @@ -681,20 +687,20 @@ void FurnaceGUI::drawSampleEdit() { ImGui::BeginDisabled(sample->depth!=DIV_SAMPLE_DEPTH_8BIT && sample->depth!=DIV_SAMPLE_DEPTH_16BIT); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(!sampleDragMode)); + pushToggleColors(!sampleDragMode); if (ImGui::Button(ICON_FA_I_CURSOR "##SSelect")) { sampleDragMode=false; } - ImGui::PopStyleColor(); + popToggleColors(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Edit mode: Select"); } ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(sampleDragMode)); + pushToggleColors(sampleDragMode); if (ImGui::Button(ICON_FA_PENCIL "##SDraw")) { sampleDragMode=true; } - ImGui::PopStyleColor(); + popToggleColors(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Edit mode: Draw"); } @@ -1293,6 +1299,9 @@ void FurnaceGUI::drawSampleEdit() { if (ImGui::MenuItem("set loop to selection",BIND_FOR(GUI_ACTION_SAMPLE_SET_LOOP))) { doAction(GUI_ACTION_SAMPLE_SET_LOOP); } + if (ImGui::MenuItem("create wavetable from selection",BIND_FOR(GUI_ACTION_SAMPLE_CREATE_WAVE))) { + doAction(GUI_ACTION_SAMPLE_CREATE_WAVE); + } ImGui::EndPopup(); } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 9f69a72b..0344d50f 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -97,6 +97,11 @@ const char* nesCores[]={ "NSFplay" }; +const char* c64Cores[]={ + "reSID", + "reSIDfp" +}; + const char* pcspkrOutMethods[]={ "evdev SND_TONE", "KIOCSOUND on /dev/tty1", @@ -259,7 +264,7 @@ void FurnaceGUI::drawSettings() { } ImGui::Separator(); - + ImGui::Text("Initial system:"); ImGui::SameLine(); if (ImGui::Button("Current system")) { @@ -369,7 +374,7 @@ void FurnaceGUI::drawSettings() { } rightClickable ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-(50.0f*dpiScale)); CWSliderScalar("Panning",ImGuiDataType_S8,&settings.initialSys[i+2],&_MINUS_ONE_HUNDRED_TWENTY_SEVEN,&_ONE_HUNDRED_TWENTY_SEVEN); rightClickable - + // oh please MSVC don't cry if (ImGui::TreeNode("Configure")) { drawSysConf(-1,(DivSystem)settings.initialSys[i],(unsigned int&)settings.initialSys[i+3],false); @@ -452,7 +457,7 @@ void FurnaceGUI::drawSettings() { if (ImGui::Checkbox("Double click selects entire column",&doubleClickColumnB)) { settings.doubleClickColumn=doubleClickColumnB; } - + bool allowEditDockingB=settings.allowEditDocking; if (ImGui::Checkbox("Allow docking editors",&allowEditDockingB)) { settings.allowEditDocking=allowEditDockingB; @@ -509,6 +514,14 @@ void FurnaceGUI::drawSettings() { ImGui::SetTooltip("threaded input processes key presses for note preview on a separate thread (on supported platforms), which reduces latency.\nhowever, crashes have been reported when threaded input is on. enable this option if that is the case."); } + bool saveWindowPosB=settings.saveWindowPos; + if (ImGui::Checkbox("Remember window position",&saveWindowPosB)) { + settings.saveWindowPos=saveWindowPosB; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("remembers the window's last position on startup."); + } + bool blankInsB=settings.blankIns; if (ImGui::Checkbox("New instruments are blank",&blankInsB)) { settings.blankIns=blankInsB; @@ -639,7 +652,7 @@ void FurnaceGUI::drawSettings() { BUFFER_SIZE_SELECTABLE(2048); ImGui::EndCombo(); } - + ImGui::Text("Quality"); ImGui::SameLine(); ImGui::Combo("##Quality",&settings.audioQuality,audioQualities,2); @@ -697,7 +710,7 @@ void FurnaceGUI::drawSettings() { } if (hasToReloadMidi) { - midiMap.read(e->getConfigPath()+DIR_SEPARATOR_STR+"midiIn_"+stripName(settings.midiInDevice)+".cfg"); + midiMap.read(e->getConfigPath()+DIR_SEPARATOR_STR+"midiIn_"+stripName(settings.midiInDevice)+".cfg"); midiMap.compile(); } @@ -972,6 +985,10 @@ void FurnaceGUI::drawSettings() { ImGui::SameLine(); ImGui::Combo("##FDSCore",&settings.fdsCore,nesCores,2); + ImGui::Text("SID core"); + ImGui::SameLine(); + ImGui::Combo("##C64Core",&settings.c64Core,c64Cores,2); + ImGui::Separator(); ImGui::Text("PC Speaker strategy"); @@ -1113,6 +1130,13 @@ void FurnaceGUI::drawSettings() { ImGui::Separator(); + if (ImGui::InputInt("Number of recent files",&settings.maxRecentFile)) { + if (settings.maxRecentFile<0) settings.maxRecentFile=0; + if (settings.maxRecentFile>30) settings.maxRecentFile=30; + } + + ImGui::Separator(); + ImGui::Text("Pattern view labels:"); ImGui::InputTextWithHint("Note off (3-char)","OFF",&settings.noteOffLabel); ImGui::InputTextWithHint("Note release (3-char)","===",&settings.noteRelLabel); @@ -1370,7 +1394,7 @@ void FurnaceGUI::drawSettings() { if (ImGui::Checkbox("Unsigned FM detune values",&unsignedDetuneB)) { settings.unsignedDetune=unsignedDetuneB; } - + // sorry. temporarily disabled until ImGui has a way to add separators in tables arbitrarily. /*bool sysSeparatorsB=settings.sysSeparators; if (ImGui::Checkbox("Add separators between systems in Orders",&sysSeparatorsB)) { @@ -1580,12 +1604,12 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_FM_SECONDARY_MOD,"Mod. accent (secondary)"); UI_COLOR_CONFIG(GUI_COLOR_FM_BORDER_MOD,"Mod. border"); UI_COLOR_CONFIG(GUI_COLOR_FM_BORDER_SHADOW_MOD,"Mod. border shadow"); - + UI_COLOR_CONFIG(GUI_COLOR_FM_PRIMARY_CAR,"Car. accent (primary"); UI_COLOR_CONFIG(GUI_COLOR_FM_SECONDARY_CAR,"Car. accent (secondary)"); UI_COLOR_CONFIG(GUI_COLOR_FM_BORDER_CAR,"Car. border"); UI_COLOR_CONFIG(GUI_COLOR_FM_BORDER_SHADOW_CAR,"Car. border shadow"); - + ImGui::TreePop(); } if (ImGui::TreeNode("Macro Editor")) { @@ -1944,7 +1968,7 @@ void FurnaceGUI::drawSettings() { UI_KEYBIND_CONFIG(GUI_ACTION_PAT_COLLAPSE_ROWS); UI_KEYBIND_CONFIG(GUI_ACTION_PAT_EXPAND_ROWS); UI_KEYBIND_CONFIG(GUI_ACTION_PAT_LATCH); - + // TODO: collapse/expand pattern and song KEYBIND_CONFIG_END; @@ -2156,6 +2180,7 @@ void FurnaceGUI::syncSettings() { settings.snCore=e->getConfInt("snCore",0); settings.nesCore=e->getConfInt("nesCore",0); settings.fdsCore=e->getConfInt("fdsCore",0); + settings.c64Core=e->getConfInt("c64Core",1); settings.pcSpeakerOutMethod=e->getConfInt("pcSpeakerOutMethod",0); settings.yrw801Path=e->getConfString("yrw801Path",""); settings.tg100Path=e->getConfString("tg100Path",""); @@ -2242,6 +2267,7 @@ void FurnaceGUI::syncSettings() { settings.dragMovesSelection=e->getConfInt("dragMovesSelection",2); settings.unsignedDetune=e->getConfInt("unsignedDetune",0); settings.noThreadedInput=e->getConfInt("noThreadedInput",0); + settings.saveWindowPos=e->getConfInt("saveWindowPos",1); settings.initialSysName=e->getConfString("initialSysName",""); settings.clampSamples=e->getConfInt("clampSamples",0); settings.noteOffLabel=e->getConfString("noteOffLabel","OFF"); @@ -2255,6 +2281,7 @@ void FurnaceGUI::syncSettings() { settings.channelStyle=e->getConfInt("channelStyle",0); settings.channelVolStyle=e->getConfInt("channelVolStyle",0); settings.channelFeedbackStyle=e->getConfInt("channelFeedbackStyle",1); + settings.maxRecentFile=e->getConfInt("maxRecentFile",10); clampSetting(settings.mainFontSize,2,96); clampSetting(settings.patFontSize,2,96); @@ -2268,6 +2295,7 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.snCore,0,1); clampSetting(settings.nesCore,0,1); clampSetting(settings.fdsCore,0,1); + clampSetting(settings.c64Core,0,1); clampSetting(settings.pcSpeakerOutMethod,0,4); clampSetting(settings.mainFont,0,6); clampSetting(settings.patFont,0,6); @@ -2344,6 +2372,7 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.dragMovesSelection,0,2); clampSetting(settings.unsignedDetune,0,1); clampSetting(settings.noThreadedInput,0,1); + clampSetting(settings.saveWindowPos,0,1); clampSetting(settings.clampSamples,0,1); clampSetting(settings.saveUnusedPatterns,0,1); clampSetting(settings.channelColors,0,2); @@ -2351,6 +2380,7 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.channelStyle,0,5); clampSetting(settings.channelVolStyle,0,3); clampSetting(settings.channelFeedbackStyle,0,3); + clampSetting(settings.maxRecentFile,0,30); settings.initialSys=e->decodeSysDesc(e->getConfString("initialSys","")); if (settings.initialSys.size()<4) { @@ -2375,7 +2405,7 @@ void FurnaceGUI::syncSettings() { parseKeybinds(); - midiMap.read(e->getConfigPath()+DIR_SEPARATOR_STR+"midiIn_"+stripName(settings.midiInDevice)+".cfg"); + midiMap.read(e->getConfigPath()+DIR_SEPARATOR_STR+"midiIn_"+stripName(settings.midiInDevice)+".cfg"); midiMap.compile(); e->setMidiDirect(midiMap.directChannel); @@ -2383,7 +2413,7 @@ void FurnaceGUI::syncSettings() { } void FurnaceGUI::commitSettings() { - bool sampleROMsChanged = settings.yrw801Path!=e->getConfString("yrw801Path","") || + bool sampleROMsChanged=settings.yrw801Path!=e->getConfString("yrw801Path","") || settings.tg100Path!=e->getConfString("tg100Path","") || settings.mu5Path!=e->getConfString("mu5Path",""); @@ -2403,6 +2433,7 @@ void FurnaceGUI::commitSettings() { e->setConf("snCore",settings.snCore); e->setConf("nesCore",settings.nesCore); e->setConf("fdsCore",settings.fdsCore); + e->setConf("c64Core",settings.c64Core); e->setConf("pcSpeakerOutMethod",settings.pcSpeakerOutMethod); e->setConf("yrw801Path",settings.yrw801Path); e->setConf("tg100Path",settings.tg100Path); @@ -2491,6 +2522,7 @@ void FurnaceGUI::commitSettings() { e->setConf("dragMovesSelection",settings.dragMovesSelection); e->setConf("unsignedDetune",settings.unsignedDetune); e->setConf("noThreadedInput",settings.noThreadedInput); + e->setConf("saveWindowPos",settings.saveWindowPos); e->setConf("clampSamples",settings.clampSamples); e->setConf("noteOffLabel",settings.noteOffLabel); e->setConf("noteRelLabel",settings.noteRelLabel); @@ -2503,6 +2535,7 @@ void FurnaceGUI::commitSettings() { e->setConf("channelStyle",settings.channelStyle); e->setConf("channelVolStyle",settings.channelVolStyle); e->setConf("channelFeedbackStyle",settings.channelFeedbackStyle); + e->setConf("maxRecentFile",settings.maxRecentFile); // colors for (int i=0; isaveConf(); + while (!recentFile.empty() && (int)recentFile.size()>settings.maxRecentFile) { + recentFile.pop_back(); + } + if (sampleROMsChanged) { if (e->loadSampleROMs()) { showError(e->getLastError()); @@ -3169,7 +3206,7 @@ void FurnaceGUI::applyUISettings(bool updateFonts) { if ((iconFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(iconFont_compressed_data,iconFont_compressed_size,e->getConfInt("iconSize",16)*dpiScale,&fc,fontRangeIcon))==NULL) { logE("could not load icon font!"); } - + if (settings.mainFontSize==settings.patFontSize && settings.patFont<5 && builtinFontM[settings.patFont]==builtinFont[settings.mainFont]) { logD("using main font for pat font."); patFont=mainFont; @@ -3203,7 +3240,7 @@ void FurnaceGUI::applyUISettings(bool updateFonts) { } } } - + if ((bigFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(font_plexSans_compressed_data,font_plexSans_compressed_size,40*dpiScale))==NULL) { logE("could not load big UI font!"); } diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index a6bbcb92..ba3ded1a 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -402,6 +402,24 @@ void FurnaceGUI::drawSysConf(int chan, DivSystem type, unsigned int& flags, bool } break; } + case DIV_SYSTEM_TIA: { + ImGui::Text("Mixing mode:"); + if (ImGui::RadioButton("Mono",(flags&6)==0)) { + copyOfFlags=(flags&(~6)); + } + if (ImGui::RadioButton("Mono (no distortion)",(flags&6)==2)) { + copyOfFlags=(flags&(~6))|2; + } + if (ImGui::RadioButton("Stereo",(flags&6)==4)) { + copyOfFlags=(flags&(~6))|4; + } + + sysPal=flags&1; + if (ImGui::Checkbox("PAL",&sysPal)) { + copyOfFlags=(flags&(~1))|(unsigned int)sysPal; + } + break; + } case DIV_SYSTEM_PCSPKR: { ImGui::Text("Speaker type:"); if (ImGui::RadioButton("Unfiltered",(flags&3)==0)) { diff --git a/src/gui/sysEx.cpp b/src/gui/sysEx.cpp index 5b2b6657..f8849b56 100644 --- a/src/gui/sysEx.cpp +++ b/src/gui/sysEx.cpp @@ -1,6 +1,17 @@ #include "gui.h" #include "../ta-log.h" +// table taken from https://nornand.hatenablog.com/entry/2020/11/21/201911 +// Yamaha why didn't you just use 0-127 as it should be? +const unsigned char tlTable[100]={ + 127, 122, 118, 114, 110, 107, 104, 102, 100, 98, 96, 94, 92, 90, 88, 86, 85, 84, 82, 81, + // desde aquí la tabla consiste de valores que bajan de 1 en 1 + 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, + 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, + 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, + 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 +}; + bool FurnaceGUI::parseSysEx(unsigned char* data, size_t len) { SafeReader reader(data,len); @@ -137,7 +148,7 @@ bool FurnaceGUI::parseSysEx(unsigned char* data, size_t len) { op.sl=15-reader.readC(); reader.readC(); // LS - ignore op.am=(reader.readC()&0x40)?1:0; - op.tl=3+((99-reader.readC())*124)/99; + op.tl=tlTable[reader.readC()%100]; unsigned char freq=reader.readC(); logV("OP%d freq: %d",i,freq); op.mult=freq>>2; diff --git a/src/gui/waveEdit.cpp b/src/gui/waveEdit.cpp index 7e453ef5..006ee430 100644 --- a/src/gui/waveEdit.cpp +++ b/src/gui/waveEdit.cpp @@ -32,6 +32,25 @@ const char* waveGenBaseShapes[4]={ "Pulse" }; +const float multFactors[16]={ + M_PI, + 2*M_PI, + 4*M_PI, + 6*M_PI, + 8*M_PI, + 10*M_PI, + 12*M_PI, + 14*M_PI, + 16*M_PI, + 18*M_PI, + 20*M_PI, + 22*M_PI, + 24*M_PI, + 26*M_PI, + 28*M_PI, + 30*M_PI, +}; + void FurnaceGUI::doGenerateWave() { float finalResult[256]; if (curWave<0 || curWave>=(int)e->song.wave.size()) return; @@ -42,7 +61,37 @@ void FurnaceGUI::doGenerateWave() { if (wave->len<2) return; if (waveGenFM) { + float s0fb0=0; + float s0fb1=0; + float s1fb0=0; + float s1fb1=0; + float s2fb0=0; + float s2fb1=0; + float s3fb0=0; + float s3fb1=0; + for (int i=0; ilen; i++) { + float pos=(float)i/(float)wave->len; + float s0=sin((pos+(waveGenFB[0]?((s0fb0+s0fb1)*pow(2.0f,waveGenFB[0]-8)):0.0f))*multFactors[waveGenMult[0]])*waveGenTL[0]; + s0fb0=s0fb1; + s0fb1=s0; + + float s1=sin((pos+(waveGenFB[1]?((s1fb0+s1fb1)*pow(2.0f,waveGenFB[1]-8)):0.0f)+(waveGenFMCon1[0]?s0:0.0f))*multFactors[waveGenMult[1]])*waveGenTL[1]; + s1fb0=s1fb1; + s1fb1=s1; + float s2=sin((pos+(waveGenFB[2]?((s2fb0+s2fb1)*pow(2.0f,waveGenFB[2]-8)):0.0f)+(waveGenFMCon1[1]?s0:0.0f)+(waveGenFMCon2[0]?s1:0.0f))*multFactors[waveGenMult[2]])*waveGenTL[2]; + s2fb0=s2fb1; + s2fb1=s2; + + float s3=sin((pos+(waveGenFB[3]?((s3fb0+s3fb1)*pow(2.0f,waveGenFB[3]-8)):0.0f)+(waveGenFMCon1[2]?s0:0.0f)+(waveGenFMCon2[1]?s1:0.0f)+(waveGenFMCon3[0]?s2:0.0f))*multFactors[waveGenMult[3]])*waveGenTL[3]; + s3fb0=s3fb1; + s3fb1=s3; + + if (waveGenFMCon1[3]) finalResult[i]+=s0; + if (waveGenFMCon2[2]) finalResult[i]+=s1; + if (waveGenFMCon3[1]) finalResult[i]+=s2; + finalResult[i]+=s3; + } } else { switch (waveGenBaseShape) { case 0: // sine @@ -104,6 +153,9 @@ void FurnaceGUI::doGenerateWave() { } } +#define CENTER_TEXT(text) \ + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+0.5*(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize(text).x)); + void FurnaceGUI::drawWaveEdit() { if (nextWindow==GUI_WINDOW_WAVE_EDIT) { waveEditOpen=true; @@ -112,7 +164,14 @@ void FurnaceGUI::drawWaveEdit() { } if (!waveEditOpen) return; float wavePreview[257]; - ImGui::SetNextWindowSizeConstraints(ImVec2(300.0f*dpiScale,300.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + if (mobileUI) { + patWindowPos=(portrait?ImVec2(0.0f,(mobileMenuPos*-0.65*scrH*dpiScale)):ImVec2((0.16*scrH*dpiScale)+0.5*scrW*dpiScale*mobileMenuPos,0.0f)); + patWindowSize=(portrait?ImVec2(scrW*dpiScale,scrH*dpiScale-(0.16*scrW*dpiScale)-(pianoOpen?(0.4*scrW*dpiScale):0.0f)):ImVec2(scrW*dpiScale-(0.16*scrH*dpiScale),scrH*dpiScale-(pianoOpen?(0.3*scrH*dpiScale):0.0f))); + ImGui::SetNextWindowPos(patWindowPos); + ImGui::SetNextWindowSize(patWindowSize); + } else { + ImGui::SetNextWindowSizeConstraints(ImVec2(300.0f*dpiScale,300.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + } if (ImGui::Begin("Wavetable Editor",&waveEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking))) { if (curWave<0 || curWave>=(int)e->song.wave.size()) { ImGui::Text("no wavetable selected"); @@ -138,6 +197,15 @@ void FurnaceGUI::drawWaveEdit() { if (ImGui::Button(ICON_FA_FLOPPY_O "##WESave")) { doAction(GUI_ACTION_WAVE_LIST_SAVE); } + if (ImGui::BeginPopupContextItem("WaveSaveFormats",ImGuiMouseButton_Right)) { + if (ImGui::MenuItem("save as .dmw...")) { + doAction(GUI_ACTION_WAVE_LIST_SAVE_DMW); + } + if (ImGui::MenuItem("save raw...")) { + doAction(GUI_ACTION_WAVE_LIST_SAVE_RAW); + } + ImGui::EndPopup(); + } ImGui::SameLine(); if (ImGui::RadioButton("Steps",waveEditStyle==0)) { @@ -187,6 +255,9 @@ void FurnaceGUI::drawWaveEdit() { for (int i=0; ilen; i++) { if (wave->data[i]>wave->max) wave->data[i]=wave->max; wavePreview[i]=wave->data[i]; + if (waveSigned && !waveHex) { + wavePreview[i]-=(int)((wave->max+1)/2); + } } if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; @@ -201,9 +272,9 @@ void FurnaceGUI::drawWaveEdit() { ImVec2 contentRegion=ImGui::GetContentRegionAvail(); // wavetable graph size determined here contentRegion.y-=ImGui::GetFrameHeightWithSpacing()+ImGui::GetStyle().WindowPadding.y; if (waveEditStyle) { - PlotNoLerp("##Waveform",wavePreview,wave->len+1,0,NULL,0,wave->max,contentRegion); + PlotNoLerp("##Waveform",wavePreview,wave->len+1,0,NULL,(waveSigned && !waveHex)?(-(int)((wave->max+1)/2)):0,(waveSigned && !waveHex)?((int)(wave->max/2)):wave->max,contentRegion); } else { - PlotCustom("##Waveform",wavePreview,wave->len,0,NULL,0,wave->max,contentRegion,sizeof(float),ImVec4(1.0f,1.0f,1.0f,1.0f),0,NULL,true); + PlotCustom("##Waveform",wavePreview,wave->len,0,NULL,(waveSigned && !waveHex)?(-(int)((wave->max+1)/2)):0,(waveSigned && !waveHex)?((int)(wave->max/2)):wave->max,contentRegion,sizeof(float),ImVec4(1.0f,1.0f,1.0f,1.0f),0,NULL,NULL,true); } if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { waveDragStart=ImGui::GetItemRectMin(); @@ -312,10 +383,347 @@ void FurnaceGUI::drawWaveEdit() { if (ImGui::BeginTabItem("FM")) { waveGenFM=true; - ImGui::Text("FM stuff here"); + if (ImGui::BeginTable("WGFMProps",4)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,ImGui::CalcTextSize("Op").x); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.5); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.25); + ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthStretch,0.25); + + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::Text("Op"); + ImGui::TableNextColumn(); + ImGui::Text("Level"); + ImGui::TableNextColumn(); + ImGui::Text("Mult"); + ImGui::TableNextColumn(); + ImGui::Text("FB"); + + for (int i=0; i<4; i++) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d",i+1); + + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::PushID(i); + if (CWSliderFloat("##WGTL",&waveGenTL[i],0.0f,1.0f)) { + doGenerateWave(); + } + ImGui::PopID(); + + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::PushID(i); + if (CWSliderInt("##WGMULT",&waveGenMult[i],1,16)) { + doGenerateWave(); + } + ImGui::PopID(); + + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::PushID(i); + if (CWSliderInt("##WGFB",&waveGenFB[i],0,7)) { + doGenerateWave(); + } + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + CENTER_TEXT("Connection Diagram"); + ImGui::Text("Connection Diagram"); + + if (ImGui::BeginTable("WGFMCon",5)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(">>"); + ImGui::TableNextColumn(); + ImGui::Text("2"); + ImGui::TableNextColumn(); + ImGui::Text("3"); + ImGui::TableNextColumn(); + ImGui::Text("4"); + ImGui::TableNextColumn(); + ImGui::Text("Out"); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("1"); + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con12",&waveGenFMCon1[0])) { + doGenerateWave(); + } + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con13",&waveGenFMCon1[1])) { + doGenerateWave(); + } + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con14",&waveGenFMCon1[2])) { + doGenerateWave(); + } + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con1O",&waveGenFMCon1[3])) { + doGenerateWave(); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("2"); + ImGui::TableNextColumn(); + // blank + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con23",&waveGenFMCon2[0])) { + doGenerateWave(); + } + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con24",&waveGenFMCon2[1])) { + doGenerateWave(); + } + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con2O",&waveGenFMCon2[2])) { + doGenerateWave(); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("3"); + ImGui::TableNextColumn(); + // blank + ImGui::TableNextColumn(); + // blank + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con34",&waveGenFMCon3[0])) { + doGenerateWave(); + } + ImGui::TableNextColumn(); + if (ImGui::Checkbox("##Con3O",&waveGenFMCon3[1])) { + doGenerateWave(); + } + + ImGui::EndTable(); + } ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Mangle")) { + if (ImGui::BeginTabItem("WaveTools")) { + if (ImGui::BeginTable("WGParamItems",2)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##WGScaleX",&waveGenScaleX,1,16)) { + if (waveGenScaleX<2) waveGenScaleX=2; + if (waveGenScaleX>256) waveGenScaleX=256; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Scale X")) { + if (waveGenScaleX>0 && wave->len!=waveGenScaleX) e->lockEngine([this,wave]() { + int origData[256]; + memcpy(origData,wave->data,wave->len*sizeof(int)); + for (int i=0; idata[i]=origData[i*wave->len/waveGenScaleX]; + } + wave->len=waveGenScaleX; + MARK_MODIFIED; + }); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##WGScaleY",&waveGenScaleY,1,16)) { + if (waveGenScaleY<2) waveGenScaleY=2; + if (waveGenScaleY>256) waveGenScaleY=256; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Scale Y")) { + if (waveGenScaleY>0 && wave->max!=waveGenScaleY) e->lockEngine([this,wave]() { + for (int i=0; ilen; i++) { + wave->data[i]=(wave->data[i]*(waveGenScaleY+1))/(wave->max+1); + } + wave->max=waveGenScaleY; + MARK_MODIFIED; + }); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##WGOffsetX",&waveGenOffsetX,1,16)) { + if (waveGenOffsetX<-wave->len+1) waveGenOffsetX=-wave->len+1; + if (waveGenOffsetX>wave->len-1) waveGenOffsetX=wave->len-1; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Offset X")) { + if (waveGenOffsetX!=0 && wave->len>0) e->lockEngine([this,wave]() { + int origData[256]; + memcpy(origData,wave->data,wave->len*sizeof(int)); + int realOff=-waveGenOffsetX; + while (realOff<0) realOff+=wave->len; + + for (int i=0; ilen; i++) { + wave->data[i]=origData[(i+realOff)%wave->len]; + } + MARK_MODIFIED; + }); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##WGOffsetY",&waveGenOffsetY,1,16)) { + if (waveGenOffsetY<-wave->max) waveGenOffsetY=-wave->max; + if (waveGenOffsetY>wave->max) waveGenOffsetY=wave->max; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Offset Y")) { + if (waveGenOffsetY!=0) e->lockEngine([this,wave]() { + for (int i=0; ilen; i++) { + wave->data[i]=CLAMP(wave->data[i]+waveGenOffsetY,0,wave->max); + } + MARK_MODIFIED; + }); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##WGSmooth",&waveGenSmooth,1,4)) { + if (waveGenSmooth>wave->len) waveGenSmooth=wave->len; + if (waveGenSmooth<1) waveGenSmooth=1; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Smooth")) { + if (waveGenSmooth>0) e->lockEngine([this,wave]() { + int origData[256]; + memcpy(origData,wave->data,wave->len*sizeof(int)); + for (int i=0; ilen; i++) { + int dataSum=0; + for (int j=i; jlen; + dataSum+=origData[pos%wave->len]; + } + dataSum/=waveGenSmooth+1; + wave->data[i]=dataSum; + } + MARK_MODIFIED; + }); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + float amp=waveGenAmplify*100.0f; + if (ImGui::InputFloat("##WGAmplify",&,1.0f,10.0f)) { + waveGenAmplify=amp/100.0f; + if (waveGenAmplify<0.0f) waveGenAmplify=0.0f; + if (waveGenAmplify>100.0f) waveGenAmplify=100.0f; + } + ImGui::TableNextColumn(); + if (ImGui::Button("Amplify")) { + if (waveGenAmplify!=1.0f) e->lockEngine([this,wave]() { + for (int i=0; ilen; i++) { + wave->data[i]=CLAMP(round((float)(wave->data[i]-(int)( /* Clang can you stop complaining */ (int)(wave->max+1)/(int)2))*waveGenAmplify),(int)(-((wave->max+1)/2)),(int)(wave->max/2))+(int)((wave->max+1)/2); + } + MARK_MODIFIED; + }); + } + + ImGui::EndTable(); + } + + ImVec2 buttonSize=ImGui::GetContentRegionAvail(); + buttonSize.y=0.0f; + ImVec2 buttonSizeHalf=buttonSize; + buttonSizeHalf.x-=ImGui::GetStyle().ItemSpacing.x; + buttonSizeHalf.x*=0.5; + + if (ImGui::Button("Normalize",buttonSize)) { + e->lockEngine([this,wave]() { + // find lowest point + int lowest=wave->max; + for (int i=0; ilen; i++) { + if (wave->data[i]data[i]; + } + + // find highest point + int highest=0; + for (int i=0; ilen; i++) { + if (wave->data[i]>highest) highest=wave->data[i]; + } + + // abort if lowest and highest points are equal + if (lowest==highest) return; + + // abort if lowest and highest points already span the entire height + if (lowest==wave->max && highest==0) return; + + // apply offset + for (int i=0; ilen; i++) { + wave->data[i]-=lowest; + } + highest-=lowest; + + // scale + for (int i=0; ilen; i++) { + wave->data[i]=(wave->data[i]*wave->max)/highest; + } + MARK_MODIFIED; + }); + } + if (ImGui::Button("Invert",buttonSize)) { + e->lockEngine([this,wave]() { + for (int i=0; ilen; i++) { + wave->data[i]=wave->max-wave->data[i]; + } + MARK_MODIFIED; + }); + } + + if (ImGui::Button("Half",buttonSizeHalf)) { + int origData[256]; + memcpy(origData,wave->data,wave->len*sizeof(int)); + + for (int i=0; ilen; i++) { + wave->data[i]=origData[i>>1]; + } + MARK_MODIFIED; + } + ImGui::SameLine(); + if (ImGui::Button("Double",buttonSizeHalf)) { + int origData[256]; + memcpy(origData,wave->data,wave->len*sizeof(int)); + + for (int i=0; ilen; i++) { + wave->data[i]=origData[(i*2)%wave->len]; + } + MARK_MODIFIED; + } + + if (ImGui::Button("Convert Signed/Unsigned",buttonSize)) { + if (wave->max>0) e->lockEngine([this,wave]() { + for (int i=0; ilen; i++) { + if (wave->data[i]>(wave->max/2)) { + wave->data[i]-=(wave->max+1)/2; + } else { + wave->data[i]+=(wave->max+1)/2; + } + } + MARK_MODIFIED; + }); + } + if (ImGui::Button("Randomize",buttonSize)) { + if (wave->max>0) e->lockEngine([this,wave]() { + for (int i=0; ilen; i++) { + wave->data[i]=rand()%wave->max; + } + MARK_MODIFIED; + }); + } ImGui::EndTabItem(); } ImGui::EndTabBar(); @@ -332,12 +740,33 @@ void FurnaceGUI::drawWaveEdit() { waveHex=true; } ImGui::SameLine(); + if (!waveHex) if (ImGui::Button(waveSigned?"±##WaveSign":"+##WaveSign",ImVec2(ImGui::GetFrameHeight(),ImGui::GetFrameHeight()))) { + waveSigned=!waveSigned; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Signed/Unsigned"); + } + ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); // wavetable text input size found here if (ImGui::InputText("##MMLWave",&mmlStringW)) { - decodeMMLStrW(mmlStringW,wave->data,wave->len,wave->max,waveHex); + int actualData[256]; + decodeMMLStrW(mmlStringW,actualData,wave->len,(waveSigned && !waveHex)?(-((wave->max+1)/2)):0,(waveSigned && !waveHex)?(wave->max/2):wave->max,waveHex); + if (waveSigned && !waveHex) { + for (int i=0; ilen; i++) { + actualData[i]+=(wave->max+1)/2; + } + } + memcpy(wave->data,actualData,wave->len*sizeof(int)); } if (!ImGui::IsItemActive()) { - encodeMMLStr(mmlStringW,wave->data,wave->len,-1,-1,waveHex); + int actualData[256]; + memcpy(actualData,wave->data,256*sizeof(int)); + if (waveSigned && !waveHex) { + for (int i=0; ilen; i++) { + actualData[i]-=(wave->max+1)/2; + } + } + encodeMMLStr(mmlStringW,actualData,wave->len,-1,-1,waveHex); } } } diff --git a/src/log.cpp b/src/log.cpp index 89602b42..ef2750a2 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -19,7 +19,11 @@ #include "ta-log.h" +#ifdef IS_MOBILE +int logLevel=LOGLEVEL_TRACE; +#else int logLevel=LOGLEVEL_INFO; +#endif std::atomic logPosition; diff --git a/src/main.cpp b/src/main.cpp index 484de545..15cd3179 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -182,6 +182,7 @@ TAParamResult pVersion(String) { printf("- puNES by FHorse (GPLv2)\n"); printf("- NSFPlay by Brad Smith and Brezza (unknown open-source license)\n"); printf("- reSID by Dag Lem (GPLv2)\n"); + printf("- reSIDfp by Dag Lem, Antti Lankila and Leandro Nini (GPLv2)\n"); printf("- Stella by Stella Team (GPLv2)\n"); printf("- vgsound_emu (first version) by cam900 (BSD 3-clause)\n"); return TA_PARAM_QUIT; diff --git a/test/assert_delta.c b/test/assert_delta.c new file mode 100644 index 00000000..6dad6992 --- /dev/null +++ b/test/assert_delta.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include + +#define BUF_SIZE 8192 + +// usage: assert_delta file +// return values: +// - 0: pass (file is silence) +// - 1: fail (noise found) +// - 2: command line error +// - 3: file open error +int main(int argc, char** argv) { + if (argc<2) return 2; + + SF_INFO si; + memset(&si,0,sizeof(SF_INFO)); + SNDFILE* sf=sf_open(argv[1],SFM_READ,&si); + if (sf==NULL) { + fprintf(stderr,"open: %s\n",sf_strerror(NULL)); + return 3; + } + + if (si.channels<1) { + fprintf(stderr,"invalid channel count\n"); + return 3; + } + + float* buf=malloc(BUF_SIZE*si.channels*sizeof(float)); + + sf_count_t totalRead=0; + size_t seekPos=0; + while ((totalRead=sf_readf_float(sf,buf,BUF_SIZE))!=0) { + for (int i=0; i