diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b72369151..9791fb643 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ defaults: shell: bash env: - BUILD_TYPE: Release + BUILD_TYPE: Debug jobs: build: diff --git a/CMakeLists.txt b/CMakeLists.txt index d2661ca99..11bf00be7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -552,6 +552,7 @@ src/engine/platform/sound/nes/fds.c src/engine/platform/sound/nes/mmc5.c src/engine/platform/sound/vera_psg.c src/engine/platform/sound/vera_pcm.c +src/engine/platform/sound/ymf278b/ymf278.cpp src/engine/platform/sound/atomicssg/ssg.c @@ -644,6 +645,10 @@ src/engine/platform/sound/ga20/iremga20.cpp src/engine/platform/sound/sm8521.c +src/engine/platform/sound/supervision.c + +src/engine/platform/sound/upd1771c.c + src/engine/platform/sound/d65modified.c src/engine/platform/sound/ted-sound.c @@ -779,6 +784,8 @@ src/engine/platform/snes.cpp src/engine/platform/k007232.cpp src/engine/platform/ga20.cpp src/engine/platform/sm8521.cpp +src/engine/platform/supervision.cpp +src/engine/platform/upd1771c.cpp src/engine/platform/pv1000.cpp src/engine/platform/k053260.cpp src/engine/platform/ted.cpp diff --git a/android/app/build.gradle b/android/app/build.gradle index f61229a5d..056579823 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,8 +15,8 @@ android { } minSdkVersion 21 targetSdkVersion 26 - versionCode 218 - versionName "0.6.6" + versionCode 219 + versionName "0.6.7" externalNativeBuild { cmake { arguments "-DANDROID_APP_PLATFORM=android-21", "-DANDROID_STL=c++_static", "-DWARNINGS_ARE_ERRORS=ON", "-DWITH_LOCALE=ON", "-DUSE_MOMO=ON" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 170bdb6c6..a1a1330ce 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ diff --git a/demos/genesis/yky.fur b/demos/genesis/yky.fur index cb8732a6b..0074ed545 100644 Binary files a/demos/genesis/yky.fur and b/demos/genesis/yky.fur differ diff --git a/demos/sn7/doorintosummer.fur b/demos/sn7/doritosummer.fur similarity index 100% rename from demos/sn7/doorintosummer.fur rename to demos/sn7/doritosummer.fur diff --git a/doc/1-intro/qs-insteditor.png b/doc/1-intro/qs-insteditor.png index e00dabe0c..a28fda719 100644 Binary files a/doc/1-intro/qs-insteditor.png and b/doc/1-intro/qs-insteditor.png differ diff --git a/doc/1-intro/qs-instlist-empty.png b/doc/1-intro/qs-instlist-empty.png index 644cae3b8..ef87dc31c 100644 Binary files a/doc/1-intro/qs-instlist-empty.png and b/doc/1-intro/qs-instlist-empty.png differ diff --git a/doc/1-intro/qs-instlist-full.png b/doc/1-intro/qs-instlist-full.png index fa7fe3d52..e282f9f4b 100644 Binary files a/doc/1-intro/qs-instlist-full.png and b/doc/1-intro/qs-instlist-full.png differ diff --git a/doc/1-intro/qs-instlist-lessempty.png b/doc/1-intro/qs-instlist-lessempty.png index ec2005c9d..81beaf6e8 100644 Binary files a/doc/1-intro/qs-instlist-lessempty.png and b/doc/1-intro/qs-instlist-lessempty.png differ diff --git a/doc/1-intro/qs-interface.png b/doc/1-intro/qs-interface.png index 5be5597e7..e1e7967a2 100644 Binary files a/doc/1-intro/qs-interface.png and b/doc/1-intro/qs-interface.png differ diff --git a/doc/1-intro/qs-macro-release.png b/doc/1-intro/qs-macro-release.png index 659982f3b..b8fd2d899 100644 Binary files a/doc/1-intro/qs-macro-release.png and b/doc/1-intro/qs-macro-release.png differ diff --git a/doc/1-intro/qs-macro.png b/doc/1-intro/qs-macro.png index a4481aba3..ef29ebbd7 100644 Binary files a/doc/1-intro/qs-macro.png and b/doc/1-intro/qs-macro.png differ diff --git a/doc/1-intro/qs-noteoffs-channels.png b/doc/1-intro/qs-noteoffs-channels.png index e8405df23..b0c2b867c 100644 Binary files a/doc/1-intro/qs-noteoffs-channels.png and b/doc/1-intro/qs-noteoffs-channels.png differ diff --git a/doc/1-intro/qs-notes-channel.png b/doc/1-intro/qs-notes-channel.png index cbf84d307..3df201f9c 100644 Binary files a/doc/1-intro/qs-notes-channel.png and b/doc/1-intro/qs-notes-channel.png differ diff --git a/doc/1-intro/qs-notes-channels.png b/doc/1-intro/qs-notes-channels.png index 77bf3b16e..d8ef4c81b 100644 Binary files a/doc/1-intro/qs-notes-channels.png and b/doc/1-intro/qs-notes-channels.png differ diff --git a/doc/1-intro/qs-notes-effect.png b/doc/1-intro/qs-notes-effect.png index 1d88ee485..744d58de6 100644 Binary files a/doc/1-intro/qs-notes-effect.png and b/doc/1-intro/qs-notes-effect.png differ diff --git a/doc/1-intro/qs-notes-inst.png b/doc/1-intro/qs-notes-inst.png index 2c0c2e9bf..a0c97caee 100644 Binary files a/doc/1-intro/qs-notes-inst.png and b/doc/1-intro/qs-notes-inst.png differ diff --git a/doc/1-intro/qs-notes-spaced.png b/doc/1-intro/qs-notes-spaced.png index 507dec35b..e07fffb24 100644 Binary files a/doc/1-intro/qs-notes-spaced.png and b/doc/1-intro/qs-notes-spaced.png differ diff --git a/doc/1-intro/qs-notes-vol.png b/doc/1-intro/qs-notes-vol.png index 5644da1a0..a44c43032 100644 Binary files a/doc/1-intro/qs-notes-vol.png and b/doc/1-intro/qs-notes-vol.png differ diff --git a/doc/1-intro/qs-notes-wronginst.png b/doc/1-intro/qs-notes-wronginst.png index b92e6a70d..2c8288f67 100644 Binary files a/doc/1-intro/qs-notes-wronginst.png and b/doc/1-intro/qs-notes-wronginst.png differ diff --git a/doc/1-intro/qs-order-added.png b/doc/1-intro/qs-order-added.png index 372968f46..ac1b0e6cb 100644 Binary files a/doc/1-intro/qs-order-added.png and b/doc/1-intro/qs-order-added.png differ diff --git a/doc/1-intro/qs-order-changed.png b/doc/1-intro/qs-order-changed.png index 47abdcc6c..b47c84c2d 100644 Binary files a/doc/1-intro/qs-order-changed.png and b/doc/1-intro/qs-order-changed.png differ diff --git a/doc/1-intro/qs-order-default.png b/doc/1-intro/qs-order-default.png index e7823b0f8..23c532f94 100644 Binary files a/doc/1-intro/qs-order-default.png and b/doc/1-intro/qs-order-default.png differ diff --git a/doc/1-intro/qs-playeditcontrols.png b/doc/1-intro/qs-playeditcontrols.png index 7e65fcef4..234a4ab3f 100644 Binary files a/doc/1-intro/qs-playeditcontrols.png and b/doc/1-intro/qs-playeditcontrols.png differ diff --git a/doc/1-intro/qs-samplist.png b/doc/1-intro/qs-samplist.png index cfe4990e8..0a681c8b0 100644 Binary files a/doc/1-intro/qs-samplist.png and b/doc/1-intro/qs-samplist.png differ diff --git a/doc/1-intro/qs-selection.png b/doc/1-intro/qs-selection.png index fb7a841a0..bb85b9032 100644 Binary files a/doc/1-intro/qs-selection.png and b/doc/1-intro/qs-selection.png differ diff --git a/doc/1-intro/qs-speed.png b/doc/1-intro/qs-speed.png index 19a9e109d..e6c1bf8f5 100644 Binary files a/doc/1-intro/qs-speed.png and b/doc/1-intro/qs-speed.png differ diff --git a/doc/1-intro/qs-waveeditor.png b/doc/1-intro/qs-waveeditor.png index 33ad7dfbe..c6829fc05 100644 Binary files a/doc/1-intro/qs-waveeditor.png and b/doc/1-intro/qs-waveeditor.png differ diff --git a/doc/1-intro/qs-wavelist.png b/doc/1-intro/qs-wavelist.png index fd4f85062..17255c2f6 100644 Binary files a/doc/1-intro/qs-wavelist.png and b/doc/1-intro/qs-wavelist.png differ diff --git a/doc/2-interface/docking.png b/doc/2-interface/docking.png index 35b828688..03a143291 100644 Binary files a/doc/2-interface/docking.png and b/doc/2-interface/docking.png differ diff --git a/doc/2-interface/effect-list.png b/doc/2-interface/effect-list.png index dd5d0cc8b..c87b19256 100644 Binary files a/doc/2-interface/effect-list.png and b/doc/2-interface/effect-list.png differ diff --git a/doc/2-interface/interface1.png b/doc/2-interface/interface1.png index 47160cae5..dced4ef60 100644 Binary files a/doc/2-interface/interface1.png and b/doc/2-interface/interface1.png differ diff --git a/doc/2-interface/keyboard.md b/doc/2-interface/keyboard.md index b8244fa17..d7d2e151c 100644 --- a/doc/2-interface/keyboard.md +++ b/doc/2-interface/keyboard.md @@ -2,6 +2,8 @@ everything on this list can be configured in the "Keyboard" tab of the Settings dialog. +additionally, everything on this list can be accessed with the "command palette" using the default key combo of `Ctrl-P`. + the keys in the "Global hotkeys" section can be used in any window, although not when a text field is activated. | action | default keybind | @@ -78,7 +80,7 @@ the keys in the "Global hotkeys" section can be used in any window, although not | Close current window | `Shift-Escape` | | Command Palette | `Ctrl-P` | | Recent files (Palette) | — | -| Insstruments (Palette) | — | +| Instruments (Palette) | — | | Samples (Palette) | — | | | | | **Note input** | | @@ -153,6 +155,8 @@ the keys in the "Global hotkeys" section can be used in any window, although not | Set note input latch | — | | Clear note input latch | — | | Absorb instrument/octave from status at cursor | — | +| Return cursor to previous jump point | — | +| Reverse recent cursor undo | — | | | | | **Instrument list** | | | Add instrument | `Insert` | diff --git a/doc/2-interface/settings.md b/doc/2-interface/settings.md index a5a448a8f..9dd473737 100644 --- a/doc/2-interface/settings.md +++ b/doc/2-interface/settings.md @@ -505,6 +505,7 @@ below all the binds, select a key from the dropdown list to add it. it will appe - **Grid** - **Single (with list)** - **Use classic macro editor vertical slider** +- **Automatic macro step size/horizontal zoom** ### Wave Editor @@ -528,6 +529,8 @@ below all the binds, select a key from the dropdown list to add it. it will appe - **Position of Sustain in FM editor:** - **Between Decay and Sustain Rate** - **After Release Rate** + - **After Release Rate, after spacing** + - **After TL** - **Use separate colors for carriers/modulators in FM editor** - **Unsigned FM detune values**: uses the internal representation of detune values, such that detune amounts of -1, -2, and -3 are shown as 5, 6, and 7. diff --git a/doc/3-pattern/channelbar.png b/doc/3-pattern/channelbar.png index dc7193e39..e22870603 100644 Binary files a/doc/3-pattern/channelbar.png and b/doc/3-pattern/channelbar.png differ diff --git a/doc/3-pattern/effects.md b/doc/3-pattern/effects.md index 653e5b214..4edcbbc0d 100644 --- a/doc/3-pattern/effects.md +++ b/doc/3-pattern/effects.md @@ -15,16 +15,15 @@ however, effects are continuous (unless specified), which means you only need to - `F8xx`: **Single tick volume up.** adds `x` to volume. - `F9xx`: **Single tick volume down.** subtracts `x` from volume. - --- -- `D3xx`: **Volume portamento.** slides volume toward the new value instead of jumping immediately. `x` is the speed of the slide. -- `D4xx`: **Volume portamento (fast).** same as `D3xx` but 256× faster. +- `D3xx`: **Volume portamento.** slides the volume to the one specified in the volume column. `x` is the slide speed. + - a volume _must_ be present with this effect for it to work. +- `D4xx`: **Volume portamento (fast).** like `D3xx` but 4× faster. - --- - `07xy`: **Tremolo.** changes volume to be "wavy" with a sine LFO. `x` is the speed. `y` is the depth. - tremolo is downward only. - maximum tremolo depth is -60 volume steps. - --- -- `D3xx`: **Volume portamento.** slides the volume to the one specified in the volume column. `x` is the slide speed. - - a volume _must_ be present with this effect for it to work. -- `D4xx`: **Volume portamento (fast).** like `D3xx` but four times faster. +- `DCxx`: **Delayed mute.** sets channel volume to 0 after `xx` ticks. ## pitch @@ -47,6 +46,7 @@ however, effects are continuous (unless specified), which means you only need to - `E8xy`: **Quick legato up**. transposes note up by `y` semitones after `x` ticks. - `E9xy`: **Quick legato down**. transposes note down by `y` semitones after `x` ticks. - `00xy`: **Arpeggio.** this effect produces a rapid cycle between the current note, the note plus `x` semitones and the note plus `y` semitones. + - as an example, start with a chord of C-3, G-3, and D#4. the G-3 and D#4 are 7 and 15 semitones higher than the root note, so the corresponding effect is `007F`. - `E0xx`: **Set arpeggio speed.** this sets the number of ticks between arpeggio values. default is 1. - --- - `04xy`: **Vibrato.** makes the pitch oscillate. `x` is the speed, while `y` is the depth. diff --git a/doc/3-pattern/pattern.png b/doc/3-pattern/pattern.png index 0d31fcf1f..7556df6b2 100644 Binary files a/doc/3-pattern/pattern.png and b/doc/3-pattern/pattern.png differ diff --git a/doc/4-instrument/fm-opn.md b/doc/4-instrument/fm-opn.md index 7e294c267..9a287c89f 100644 --- a/doc/4-instrument/fm-opn.md +++ b/doc/4-instrument/fm-opn.md @@ -39,7 +39,7 @@ these apply to each operator: - **Decay Rate 2 (D2R) / Sustain Rate (SR)**: determines the diminishing time for the sound. the higher the value, the shorter the decay. this is the long "tail" of the sound that continues as long as the key is depressed (0 to 31). - **Release Rate (RR)**: determines the rate at which the sound disappears after note off. the higher the value, the shorter the release (0 to 15). - **Total Level (TL)**: represents the envelope’s highest amplitude, with 0 being the largest and 127 (decimal) the smallest. a change of one unit is about 0.75 dB. -- **Hardware Envelope Generator (SSG-EG)**: executes the built-in envelope, inherited from AY-3-8910 PSG. speed of execution is controlled via Decay Rate. +- **Hardware Envelope Generator (SSG-EG)**: executes the built-in envelope, inherited from AY-3-8910 PSG. speed of execution is controlled via envelope parameters. ![FM ADSR chart](FM-ADSRchart.png) diff --git a/doc/4-instrument/instrument-editor-top.png b/doc/4-instrument/instrument-editor-top.png index 3cfab1ec1..7c032326d 100644 Binary files a/doc/4-instrument/instrument-editor-top.png and b/doc/4-instrument/instrument-editor-top.png differ diff --git a/doc/4-instrument/sample-map.png b/doc/4-instrument/sample-map.png index 03e12c2cf..5598d429f 100644 Binary files a/doc/4-instrument/sample-map.png and b/doc/4-instrument/sample-map.png differ diff --git a/doc/6-sample/sample-editor.png b/doc/6-sample/sample-editor.png index 30dce72da..7c8d9b50c 100644 Binary files a/doc/6-sample/sample-editor.png and b/doc/6-sample/sample-editor.png differ diff --git a/doc/7-systems/nes.md b/doc/7-systems/nes.md index f224b4107..c697be215 100644 --- a/doc/7-systems/nes.md +++ b/doc/7-systems/nes.md @@ -20,6 +20,7 @@ the console is powered by the Ricoh 2A03, a CPU with sound generator built-in. i - may be `0` or `1` for the noise channel: - `0`: long (15-bit LFSR, 32767-step) - `1`: short (9-bit LFSR, 93-step) + - short noise may sound very different depending on its initial LFSR state. more info can be found at [NESdev](https://forums.nesdev.org/viewtopic.php?t=11535). - `13xy`: **setup sweep up.** - `x` is the time. - `y` is the shift. @@ -50,7 +51,7 @@ the console is powered by the Ricoh 2A03, a CPU with sound generator built-in. i - `18xx`: **set PCM channel mode.** - `00`: PCM (software). - `01`: DPCM (hardware). - - when in DPCM mode, samples will sound muffled (due to its nature), availables pitches are limited, and loop point is ignored. + - when in DPCM mode, samples will sound muffled (due to its nature) and available pitches are limited. see "DPCM samples" below. - `19xx`: **set triangle linear counter.** - `00` to `7F` set the counter. - `80` and higher halt it. @@ -58,7 +59,6 @@ the console is powered by the Ricoh 2A03, a CPU with sound generator built-in. i - only works in DPCM mode. - see table below for possible values. - ## info this chip uses the [NES](../4-instrument/nes.md) instrument editor. @@ -70,12 +70,14 @@ the following options are available in the Chip Manager window: - **Clock rate**: sets the rate at which the chip will run. - **DPCM channel mode**: allows you to set which mode to use for the DPCM channel. - DPCM: the default mode, playing 1-bit DPCM samples as supported by the hardware. - - PCM: this mode provides crispier samples by writing the delta counter directly. uses a lot of CPU time in console. + - PCM: this mode provides crisper 7-bit samples by writing to the delta counter directly. uses a lot of CPU time in console. -## DPCM sample loop +## DPCM samples due to hardware limitations, a loop in a DPCM sample must start on a multiple of 512 samples (512, 1024, 1536...) and have a length that is a multiple of 128 plus 8 samples (136, 264, 392...) +NES DPCM only has 16 preset sample rates, shown in a table below. for help adapting samples to work with this, see the [limited samples guide](../9-guides/limited-samples.md). + ## short noise frequencies (NTSC) note | arpeggio | fundamental | MIDI note | pitch diff --git a/doc/7-systems/ym2203.md b/doc/7-systems/ym2203.md index a9003bd57..d51b039de 100644 --- a/doc/7-systems/ym2203.md +++ b/doc/7-systems/ym2203.md @@ -107,15 +107,21 @@ several variants of this chip were released as well, with more features. - if `x` is `1` to `4`, the effect targets that operator; `y` turns it off with a value of `0` and on with a value of `1`. - for example, the effect `6031` enables OP3. +## info + +this chip uses the [FM (OPN)](../4-instrument/fm-opn.md) and [AY-3-8910/SSG](../4-instrument/ay8910.md) instrument editor. + ## extended channel 3 in ExtCh mode, channel 3 is split into one column for each of its four operators and feedback are shared. the frequency of each operator may be controlled independently with notes and effects. this can be used for more polyphony or more complex sounds. all four operators are still combined according to the algorithm in use. for example, algorithm 7 acts as four independent sine waves. algorithm 4 acts as two independent 2op sounds. even with algorithm 0, placing a note in any operator triggers that operator alone. -## info +## SSG-EG -this chip uses the [FM (OPN)](../4-instrument/fm-opn.md) and [AY-3-8910/SSG](../4-instrument/ay8910.md) instrument editor. +SSG-EG is short for "Software-controlled Sound Generator – Envelope Generator". it is the AY-3-8910/YM2149 envelope generator applied to each individual operator. it makes the operator's envelope play through attack, decay, sustain, and decay 2 until it reaches zero amplitude, at which time SSG triggers. according to the shape of the SSG envelope, the operator's envelope may then either loop or hold, and either of these can be set to invert the envelope (attack decreases and decay increases) when triggered. + +a full guide to SSG-EG is beyond the scope of this documentation. for more information, see this [brief SSG-EG and CSM video tutorial](https://www.youtube.com/watch?v=IKOR0TUlnWU), this [detailed technical explanation](https://gendev.spritesmind.net/forum/viewtopic.php?t=386&start=106), and this [chart of tunings](https://docs.google.com/spreadsheets/d/1HGKQ08CnLGAjA1U0StJFldod3FkQ3uq86rYy1VBIuZc/). ## chip config diff --git a/doc/7-systems/ym2608.md b/doc/7-systems/ym2608.md index e2c307721..b2cb91a41 100644 --- a/doc/7-systems/ym2608.md +++ b/doc/7-systems/ym2608.md @@ -107,15 +107,21 @@ the YM2610 (OPNB) and YM2610B chips are very similar to this one, but the built- - if `x` is `1` to `4`, the effect targets that operator; `y` turns it off with a value of `0` and on with a value of `1`. - for example, the effect `6031` enables OP3. +## info + +this chip uses the [FM (OPN)](../4-instrument/fm-opn.md), [AY-3-8910/SSG](../4-instrument/ay8910.md), [ADPCM-A](../4-instrument/adpcm-a.md) and [ADPCM-B](../4-instrument/adpcm-b.md) instrument editors. + ## extended channel 3 in ExtCh mode, channel 3 is split into one column for each of its four operators. feedback and LFO levels are shared. the frequency of each operator may be controlled independently with notes and effects. this can be used for more polyphony or more complex sounds. all four operators are still combined according to the algorithm in use. for example, algorithm 7 acts as four independent sine waves. algorithm 4 acts as two independent 2op sounds. even with algorithm 0, placing a note in any operator triggers that operator alone. -## info +## SSG-EG -this chip uses the [FM (OPN)](../4-instrument/fm-opn.md), [AY-3-8910/SSG](../4-instrument/ay8910.md), [ADPCM-A](../4-instrument/adpcm-a.md) and [ADPCM-B](../4-instrument/adpcm-b.md) instrument editors. +SSG-EG is short for "Software-controlled Sound Generator – Envelope Generator". it is the AY-3-8910/YM2149 envelope generator applied to each individual operator. it makes the operator's envelope play through attack, decay, sustain, and decay 2 until it reaches zero amplitude, at which time SSG triggers. according to the shape of the SSG envelope, the operator's envelope may then either loop or hold, and either of these can be set to invert the envelope (attack decreases and decay increases) when triggered. + +a full guide to SSG-EG is beyond the scope of this documentation. for more information, see this [brief SSG-EG and CSM video tutorial](https://www.youtube.com/watch?v=IKOR0TUlnWU), this [detailed technical explanation](https://gendev.spritesmind.net/forum/viewtopic.php?t=386&start=106), and this [chart of tunings](https://docs.google.com/spreadsheets/d/1HGKQ08CnLGAjA1U0StJFldod3FkQ3uq86rYy1VBIuZc/). ## chip config diff --git a/doc/7-systems/ym2610.md b/doc/7-systems/ym2610.md index 26845eb8b..2edcfaf6d 100644 --- a/doc/7-systems/ym2610.md +++ b/doc/7-systems/ym2610.md @@ -105,15 +105,21 @@ its soundchip is a 4-in-1: 4ch 4-op FM, YM2149 (AY-3-8910 clone) and [2 differen - if `x` is `1` to `4`, the effect targets that operator; `y` turns it off with a value of `0` and on with a value of `1`. - for example, the effect `6031` enables OP3. +## info + +this chip uses the [FM (OPN)](../4-instrument/fm-opn.md), [AY-3-8910/SSG](../4-instrument/ay8910.md), [ADPCM-A](../4-instrument/adpcm-a.md) and [ADPCM-B](../4-instrument/adpcm-b.md) instrument editors. + ## extended channel 2 in ExtCh mode, channel 2 is split into one column for each of its four operators. feedback and LFO levels are shared. the frequency of each operator may be controlled independently with notes and effects. this can be used for more polyphony or more complex sounds. all four operators are still combined according to the algorithm in use. for example, algorithm 7 acts as four independent sine waves. algorithm 4 acts as two independent 2op sounds. even with algorithm 0, placing a note in any operator triggers that operator alone. -## info +## SSG-EG -this chip uses the [FM (OPN)](../4-instrument/fm-opn.md), [AY-3-8910/SSG](../4-instrument/ay8910.md), [ADPCM-A](../4-instrument/adpcm-a.md) and [ADPCM-B](../4-instrument/adpcm-b.md) instrument editors. +SSG-EG is short for "Software-controlled Sound Generator – Envelope Generator". it is the AY-3-8910/YM2149 envelope generator applied to each individual operator. it makes the operator's envelope play through attack, decay, sustain, and decay 2 until it reaches zero amplitude, at which time SSG triggers. according to the shape of the SSG envelope, the operator's envelope may then either loop or hold, and either of these can be set to invert the envelope (attack decreases and decay increases) when triggered. + +a full guide to SSG-EG is beyond the scope of this documentation. for more information, see this [brief SSG-EG and CSM video tutorial](https://www.youtube.com/watch?v=IKOR0TUlnWU), this [detailed technical explanation](https://gendev.spritesmind.net/forum/viewtopic.php?t=386&start=106), and this [chart of tunings](https://docs.google.com/spreadsheets/d/1HGKQ08CnLGAjA1U0StJFldod3FkQ3uq86rYy1VBIuZc/). ## chip config diff --git a/doc/7-systems/ym2610b.md b/doc/7-systems/ym2610b.md index f9681d5a7..50f20d107 100644 --- a/doc/7-systems/ym2610b.md +++ b/doc/7-systems/ym2610b.md @@ -104,15 +104,21 @@ it is backward compatible with the original chip. - if `x` is `1` to `4`, the effect targets that operator; `y` turns it off with a value of `0` and on with a value of `1`. - for example, the effect `6031` enables OP3. +## info + +this chip uses the [FM (OPN)](../4-instrument/fm-opn.md), [AY-3-8910/SSG](../4-instrument/ay8910.md), [ADPCM-A](../4-instrument/adpcm-a.md) and [ADPCM-B](../4-instrument/adpcm-b.md) instrument editors. + ## extended channel 3 in ExtCh mode, channel 3 is split into one column for each of its four operators. feedback and LFO levels are shared. the frequency of each operator may be controlled independently with notes and effects. this can be used for more polyphony or more complex sounds. all four operators are still combined according to the algorithm in use. for example, algorithm 7 acts as four independent sine waves. algorithm 4 acts as two independent 2op sounds. even with algorithm 0, placing a note in any operator triggers that operator alone. -## info +## SSG-EG -this chip uses the [FM (OPN)](../4-instrument/fm-opn.md), [AY-3-8910/SSG](../4-instrument/ay8910.md), [ADPCM-A](../4-instrument/adpcm-a.md) and [ADPCM-B](../4-instrument/adpcm-b.md) instrument editors. +SSG-EG is short for "Software-controlled Sound Generator – Envelope Generator". it is the AY-3-8910/YM2149 envelope generator applied to each individual operator. it makes the operator's envelope play through attack, decay, sustain, and decay 2 until it reaches zero amplitude, at which time SSG triggers. according to the shape of the SSG envelope, the operator's envelope may then either loop or hold, and either of these can be set to invert the envelope (attack decreases and decay increases) when triggered. + +a full guide to SSG-EG is beyond the scope of this documentation. for more information, see this [brief SSG-EG and CSM video tutorial](https://www.youtube.com/watch?v=IKOR0TUlnWU), this [detailed technical explanation](https://gendev.spritesmind.net/forum/viewtopic.php?t=386&start=106), and this [chart of tunings](https://docs.google.com/spreadsheets/d/1HGKQ08CnLGAjA1U0StJFldod3FkQ3uq86rYy1VBIuZc/). ## chip config diff --git a/doc/7-systems/ym2612.md b/doc/7-systems/ym2612.md index 087cd914b..c5c8da66f 100644 --- a/doc/7-systems/ym2612.md +++ b/doc/7-systems/ym2612.md @@ -1,23 +1,8 @@ # Yamaha YM2612 -one of two chips that powered the Sega Genesis. it is a six-channel, four-operator FM synthesizer. channel #6 can be turned into 8-bit PCM player, that via software mixing, thanks to Z80 sound CPU, can play more than one channel of straight-shot samples at once. +one of two chips that powered the Sega Genesis. it is a six-channel, four-operator FM synthesizer. channel 6 can be turned into 8-bit PCM player, that via software mixing, thanks to Z80 sound CPU, can play more than one channel of straight-shot samples at once. Furnace also offers DualPCM, a Z80 driver that splits channel 6 into two individual PCM channels with variable pitch. using the console's Z80 processor, these are mixed together in software and streamed to channel 6 in PCM mode with a mix rate of 13750 Hz. VGM export requires the "direct stream mode" option to be enabled, and resulting files will be very large. -## extended channel 3 - -in ExtCh mode, channel 3 is split into one column for each of its four operators. feedback and LFO levels are shared. the frequency of each operator may be controlled independently with notes and effects. this can be used for more polyphony or more complex sounds. - -all four operators are still combined according to the algorithm in use. for example, algorithm 7 acts as four independent sine waves. algorithm 4 acts as two independent 2op sounds. even with algorithm 0, placing a note in any operator triggers that operator alone. - -## CSM - -CSM is short for "Composite Sinusoidal Modeling". CSM works by sending key-on and key-off commands to channel 3 at a specific frequency, controlled by the added "CSM Timer" channel. this can be used to create vocal formants (speech synthesis!) or other complex effects. - -CSM is beyond the scope of this documentation. for more information, see this [brief SSG-EG and CSM video tutorial](https://www.youtube.com/watch?v=IKOR0TUlnWU). - -## DualPCM - -thanks to the Z80 sound CPU, DualPCM can play two samples at once! this mode splits channel 6 into two individual PCM channels with variable pitch. these are mixed together in software and streamed to channel 6 with a mix rate of 13750 Hz. VGM export requires the "direct stream mode" option to be enabled, and resulting files will be very large. ## effects - `10xy`: **set LFO parameters.** @@ -30,10 +15,10 @@ thanks to the Z80 sound CPU, DualPCM can play two samples at once! this mode spl - `15xx`: **set operator 4 level.** - `16xy`: **set multiplier of operator.** - `x` is the operator (1-4). - - `y` is the new MULT value.. + - `y` is the new MULT value. - `17xx`: **toggle LEGACY sample mode.** - this only works on channel 6. - - **this effect exists only for compatibility reasons! its use is NOT recommented. use Sample type instruments instead.** + - **this effect exists only for compatibility reasons! its use is NOT recommended. use Sample type instruments instead.** - `18xx`: **toggle extended channel 3 mode.** - 0 disables it and 1 enables it. - only in extended channel 3 chip. @@ -94,6 +79,28 @@ thanks to the Z80 sound CPU, DualPCM can play two samples at once! this mode spl this chip uses the [FM (OPN)](../4-instrument/fm-opn.md) and [Generic Sample](../4-instrument/sample.md) instrument editors. +## extended channel 3 + +in ExtCh mode, channel 3 is split into one column for each of its four operators. feedback and LFO levels are shared. the frequency of each operator may be controlled independently with notes and effects. this can be used for more polyphony or more complex sounds. + +all four operators are still combined according to the algorithm in use. for example, algorithm 7 acts as four independent sine waves. algorithm 4 acts as two independent 2op sounds. even with algorithm 0, placing a note in any operator triggers that operator alone. + +## CSM + +CSM, or "Composite Sine Mode", involves a timer matching the frequency of the note in the "CSM Timer" channel. each time it triggers, it generates key-on and key-off commands to reset the phase of all operators on channel 3 and force their envelopes to restart at the release point. this can be used to create vocal formants (speech synthesis!) or other complex effects. outside this chip's specific implementation, the technique is known as "oscillator sync". + +working with CSM is beyond the scope of this documentation. for more information, see this [brief SSG-EG and CSM video tutorial](https://www.youtube.com/watch?v=IKOR0TUlnWU). + +## DualPCM + +thanks to the Z80 sound CPU, DualPCM can play two samples at once! this mode splits channel 6 into two individual PCM channels with variable pitch. these are mixed together in software and streamed to channel 6 with a mix rate of 13750 Hz. VGM export requires the "direct stream mode" option to be enabled, and resulting files will be very large. + +## SSG-EG + +SSG-EG is short for "Software-controlled Sound Generator – Envelope Generator". it is the AY-3-8910/YM2149 envelope generator applied to each individual operator. it makes the operator's envelope play through attack, decay, sustain, and decay 2 until it reaches zero amplitude, at which time SSG triggers. according to the shape of the SSG envelope, the operator's envelope may then either loop or hold, and either of these can be set to invert the envelope (attack decreases and decay increases) when triggered. + +a full guide to SSG-EG is beyond the scope of this documentation. for more information, see this [brief SSG-EG and CSM video tutorial](https://www.youtube.com/watch?v=IKOR0TUlnWU), this [detailed technical explanation](https://gendev.spritesmind.net/forum/viewtopic.php?t=386&start=106), and this [chart of tunings](https://docs.google.com/spreadsheets/d/1HGKQ08CnLGAjA1U0StJFldod3FkQ3uq86rYy1VBIuZc/). + ## chip config the following options are available in the Chip Manager window: diff --git a/doc/8-advanced/channels.png b/doc/8-advanced/channels.png index 31afb7c6c..9fc26fde5 100644 Binary files a/doc/8-advanced/channels.png and b/doc/8-advanced/channels.png differ diff --git a/doc/8-advanced/chanosc-gradient.png b/doc/8-advanced/chanosc-gradient.png index 0a413d266..4d3c973e5 100644 Binary files a/doc/8-advanced/chanosc-gradient.png and b/doc/8-advanced/chanosc-gradient.png differ diff --git a/doc/8-advanced/chanosc.png b/doc/8-advanced/chanosc.png index 8128b1d8e..feeb8cd5e 100644 Binary files a/doc/8-advanced/chanosc.png and b/doc/8-advanced/chanosc.png differ diff --git a/doc/8-advanced/chip-manager.png b/doc/8-advanced/chip-manager.png index b746d85d0..208a2fa7f 100644 Binary files a/doc/8-advanced/chip-manager.png and b/doc/8-advanced/chip-manager.png differ diff --git a/doc/8-advanced/find-find.png b/doc/8-advanced/find-find.png index 1d5e05ba9..69889f360 100644 Binary files a/doc/8-advanced/find-find.png and b/doc/8-advanced/find-find.png differ diff --git a/doc/8-advanced/find-replace.png b/doc/8-advanced/find-replace.png index a9206beae..95f54b297 100644 Binary files a/doc/8-advanced/find-replace.png and b/doc/8-advanced/find-replace.png differ diff --git a/doc/8-advanced/grooves.png b/doc/8-advanced/grooves.png index e38e944f0..db3d19301 100644 Binary files a/doc/8-advanced/grooves.png and b/doc/8-advanced/grooves.png differ diff --git a/doc/8-advanced/memcompo.png b/doc/8-advanced/memcompo.png new file mode 100644 index 000000000..f9ae5f948 Binary files /dev/null and b/doc/8-advanced/memcompo.png differ diff --git a/doc/8-advanced/piano.png b/doc/8-advanced/piano.png index 9b2b7b4c3..4773fef74 100644 Binary files a/doc/8-advanced/piano.png and b/doc/8-advanced/piano.png differ diff --git a/doc/8-advanced/stats.png b/doc/8-advanced/stats.png index 6e4ec92a0..7a99aff14 100644 Binary files a/doc/8-advanced/stats.png and b/doc/8-advanced/stats.png differ diff --git a/doc/8-advanced/user-systems.md b/doc/8-advanced/user-systems.md index d60772654..5d63e8565 100644 --- a/doc/8-advanced/user-systems.md +++ b/doc/8-advanced/user-systems.md @@ -12,6 +12,7 @@ chip configuration is exactly as in the [chip manager](chip-manager.md) window. the **Advanced** field stores additional settings that are set when a new song is started. these are listed in "option=value" format, one per line. - `tickRate`: sets tick rate. +- `chanMask`: sets which channels to hide. written as a comma-separated list of integers **Save and Close**: as it says. diff --git a/doc/8-advanced/user-systems.png b/doc/8-advanced/user-systems.png index c300530a3..b5a3fd491 100644 Binary files a/doc/8-advanced/user-systems.png and b/doc/8-advanced/user-systems.png differ diff --git a/doc/8-advanced/xyosc.md b/doc/8-advanced/xyosc.md index e03a1f945..9b238fb65 100644 --- a/doc/8-advanced/xyosc.md +++ b/doc/8-advanced/xyosc.md @@ -2,7 +2,7 @@ also called "vectorscope", this is similar to the normal oscilloscope in that it draws audio output as a waveform, but instead of being one-dimensional and going from left to right, it is plotted in two dimensions (usually X represents left output and Y represents right output). -(TODO: image) +![oscilloscope (X-Y)](xyosc.png) right-clicking the X-Y oscilloscope window displays settings: - **X Channel**: the output channel which will affect the X (horizontal) axis. default is 1 (left). diff --git a/doc/8-advanced/xyosc.png b/doc/8-advanced/xyosc.png new file mode 100644 index 000000000..dc49e74d1 Binary files /dev/null and b/doc/8-advanced/xyosc.png differ diff --git a/instruments/OPL/2-op strings.fui b/instruments/OPL/2-op strings.fui new file mode 100644 index 000000000..be6d61bfe Binary files /dev/null and b/instruments/OPL/2-op strings.fui differ diff --git a/instruments/OPL/asterix gb clarinet.fui b/instruments/OPL/asterix gb clarinet.fui new file mode 100644 index 000000000..b6a2ddd64 Binary files /dev/null and b/instruments/OPL/asterix gb clarinet.fui differ diff --git a/instruments/OPL/clavinetmaybe.fui b/instruments/OPL/clavinetmaybe.fui new file mode 100644 index 000000000..840fb01bf Binary files /dev/null and b/instruments/OPL/clavinetmaybe.fui differ diff --git a/instruments/OPL/grand piano.fui b/instruments/OPL/grand piano.fui new file mode 100644 index 000000000..2bae68d39 Binary files /dev/null and b/instruments/OPL/grand piano.fui differ diff --git a/instruments/OPL/kalimba or plain sine i cant tell.fui b/instruments/OPL/kalimba or plain sine i cant tell.fui new file mode 100644 index 000000000..05de28372 Binary files /dev/null and b/instruments/OPL/kalimba or plain sine i cant tell.fui differ diff --git a/instruments/OPL/marimba.fui b/instruments/OPL/marimba.fui new file mode 100644 index 000000000..1b129efd8 Binary files /dev/null and b/instruments/OPL/marimba.fui differ diff --git a/instruments/OPL/sax.fui b/instruments/OPL/sax.fui new file mode 100644 index 000000000..80e9714f0 Binary files /dev/null and b/instruments/OPL/sax.fui differ diff --git a/instruments/OPL/tuba trumpet thing.fui b/instruments/OPL/tuba trumpet thing.fui new file mode 100644 index 000000000..7693bf015 Binary files /dev/null and b/instruments/OPL/tuba trumpet thing.fui differ diff --git a/papers/clipboard-format.md b/papers/clipboard-format.md index a33c71394..80e0002db 100644 --- a/papers/clipboard-format.md +++ b/papers/clipboard-format.md @@ -6,7 +6,7 @@ when copying pattern data from Furnace, it's stored in the clipboard as plain te org.tildearrow.furnace - Pattern Data (144) ``` -this top line of text is always the same except for the number in parentheses, which is the internal build number. for example, 0.6.6 is `218`. +this top line of text is always the same except for the number in parentheses, which is the internal build number. for example, 0.6.7 is `219`. the second line is a number between 0 and 18 (decimal) which indicates which column the clip starts from. - `0`: note. diff --git a/papers/format.md b/papers/format.md index 62f40c7fb..a36016c8b 100644 --- a/papers/format.md +++ b/papers/format.md @@ -32,6 +32,7 @@ these fields are 0 in format versions prior to 100 (0.6pre1). the format versions are: +- 219: Furnace 0.6.7 - 218: Furnace 0.6.6 - 214: Furnace 0.6.5 - 212: Furnace 0.6.4 @@ -190,7 +191,7 @@ size | description | - 0x9c: Virtual Boy - 6 channels | - 0x9d: VRC7 - 6 channels | - 0x9e: YM2610B - 16 channels - | - 0x9f: ZX Spectrum (beeper, tildearrow engine) - 6 channels + | - 0x9f: ZX Spectrum (beeper, SFX-like tildearrow engine) - 6 channels | - 0xa0: YM2612 extended - 9 channels | - 0xa1: Konami SCC - 5 channels | - 0xa2: OPL drums (YM3526) - 11 channels @@ -205,8 +206,8 @@ size | description | - 0xab: MSM6258 - 1 channel | - 0xac: Commander X16 (VERA) - 17 channels | - 0xad: Bubble System WSG - 2 channels - | - 0xae: OPL4 (YMF278B) - 42 channels (UNAVAILABLE) - | - 0xaf: OPL4 drums (YMF278B) - 44 channels (UNAVAILABLE) + | - 0xae: OPL4 (YMF278B) - 42 channels + | - 0xaf: OPL4 drums (YMF278B) - 44 channels | - 0xb0: Seta/Allumer X1-010 - 16 channels | - 0xb1: Ensoniq ES5506 - 32 channels | - 0xb2: Yamaha Y8950 - 10 channels @@ -257,8 +258,8 @@ size | description | - 0xe0: QSound - 19 channels | - 0xe1: PS1 - 24 channels (UNAVAILABLE) | - 0xe2: C64 (6581) with PCM - 4 channels (UNAVAILABLE) - | - 0xe3: Watara Supervision - 4 channels (UNAVAILABLE) - | - 0xe4: µPD1771C - 1 channel (UNAVAILABLE) + | - 0xe3: Watara Supervision - 4 channels + | - 0xe4: µPD1771C - 1 channel | - 0xf0: SID2 - 3 channels | - 0xf1: 5E01 - 5 channels | - 0xf5: SID3 - 7 channels @@ -390,7 +391,7 @@ size | description 1 | reset arp effect phase on new note (>=184) 1 | linear volume scaling rounds up (>=188) 1 | legacy "always set volume" behavior (>=191) - 1 | reserved + 1 | legacy sample offset effect (>=200) --- | **speed pattern of first song** (>=139) 1 | length of speed pattern (fail if this is lower than 0 or higher than 16) 16 | speed pattern (this overrides speed 1 and speed 2 settings) @@ -589,6 +590,7 @@ size | description | - 11: 8-bit μ-law PCM | - 12: C219 PCM | - 13: IMA ADPCM + | - 14: 12-bit PCM (MultiPCM) | - 16: 16-bit PCM 1 | loop direction (>=123) or reserved | - 0: forward diff --git a/papers/newIns.md b/papers/newIns.md index e9141e1bc..7cc0242b6 100644 --- a/papers/newIns.md +++ b/papers/newIns.md @@ -585,6 +585,11 @@ size | description 1 | LFO rate 1 | vibrato depth 1 | AM depth + 1 | flags (>=221) + | - bit 0: damp + | - bit 1: pseudo-reverb + | - bit 2: LFO reset + | - bit 3: level direct ``` # Sound Unit data (SU) diff --git a/po/de.po b/po/de.po index 86a85d5e2..52b3f3531 100644 --- a/po/de.po +++ b/po/de.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: de\n" diff --git a/po/es.po b/po/es.po index f4932f25a..34b1990b2 100644 --- a/po/es.po +++ b/po/es.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: es\n" diff --git a/po/fi.po b/po/fi.po index 3bc443208..93b0c535a 100644 --- a/po/fi.po +++ b/po/fi.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: fi\n" diff --git a/po/fr.po b/po/fr.po index 10f9d377c..62d220482 100644 --- a/po/fr.po +++ b/po/fr.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: fr\n" diff --git a/po/hy.po b/po/hy.po index 01575d611..b04a5f235 100644 --- a/po/hy.po +++ b/po/hy.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: hy\n" diff --git a/po/id.po b/po/id.po index e91345f0a..da1ab5e86 100644 --- a/po/id.po +++ b/po/id.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: id\n" diff --git a/po/ja.po b/po/ja.po index 0ef5ee2cc..f9cad3daa 100644 --- a/po/ja.po +++ b/po/ja.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: ja\n" diff --git a/po/ko.po b/po/ko.po index 0b9b532f4..bd7b33218 100644 --- a/po/ko.po +++ b/po/ko.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: 희민Heemin\n" "Language-Team: none\n" "Language: ko\n" diff --git a/po/locale/de/LC_MESSAGES/furnace.mo b/po/locale/de/LC_MESSAGES/furnace.mo index 531f60770..1e45fa104 100644 Binary files a/po/locale/de/LC_MESSAGES/furnace.mo and b/po/locale/de/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/es/LC_MESSAGES/furnace.mo b/po/locale/es/LC_MESSAGES/furnace.mo index b874698cf..2ff1335cb 100644 Binary files a/po/locale/es/LC_MESSAGES/furnace.mo and b/po/locale/es/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/fi/LC_MESSAGES/furnace.mo b/po/locale/fi/LC_MESSAGES/furnace.mo index 282171e2e..6d359fab5 100644 Binary files a/po/locale/fi/LC_MESSAGES/furnace.mo and b/po/locale/fi/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/fr/LC_MESSAGES/furnace.mo b/po/locale/fr/LC_MESSAGES/furnace.mo index d4397233e..14f1d2cd2 100644 Binary files a/po/locale/fr/LC_MESSAGES/furnace.mo and b/po/locale/fr/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/hy/LC_MESSAGES/furnace.mo b/po/locale/hy/LC_MESSAGES/furnace.mo index 89af149cd..5308e6dff 100644 Binary files a/po/locale/hy/LC_MESSAGES/furnace.mo and b/po/locale/hy/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/id/LC_MESSAGES/furnace.mo b/po/locale/id/LC_MESSAGES/furnace.mo index 71d4f4077..7d521bd7f 100644 Binary files a/po/locale/id/LC_MESSAGES/furnace.mo and b/po/locale/id/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/ko/LC_MESSAGES/furnace.mo b/po/locale/ko/LC_MESSAGES/furnace.mo index ddcb27cfb..187e279f0 100644 Binary files a/po/locale/ko/LC_MESSAGES/furnace.mo and b/po/locale/ko/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/nl/LC_MESSAGES/furnace.mo b/po/locale/nl/LC_MESSAGES/furnace.mo index b555d7f07..5c48de89d 100644 Binary files a/po/locale/nl/LC_MESSAGES/furnace.mo and b/po/locale/nl/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/pl/LC_MESSAGES/furnace.mo b/po/locale/pl/LC_MESSAGES/furnace.mo index 31c57da4c..30fd737dd 100644 Binary files a/po/locale/pl/LC_MESSAGES/furnace.mo and b/po/locale/pl/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/pt_BR/LC_MESSAGES/furnace.mo b/po/locale/pt_BR/LC_MESSAGES/furnace.mo index 75dec639a..f72219c5e 100644 Binary files a/po/locale/pt_BR/LC_MESSAGES/furnace.mo and b/po/locale/pt_BR/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/ru/LC_MESSAGES/furnace.mo b/po/locale/ru/LC_MESSAGES/furnace.mo index a0d49aaf8..be4af5e62 100644 Binary files a/po/locale/ru/LC_MESSAGES/furnace.mo and b/po/locale/ru/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/sk/LC_MESSAGES/furnace.mo b/po/locale/sk/LC_MESSAGES/furnace.mo index 4c0e2ab31..7d57134a9 100644 Binary files a/po/locale/sk/LC_MESSAGES/furnace.mo and b/po/locale/sk/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/sv/LC_MESSAGES/furnace.mo b/po/locale/sv/LC_MESSAGES/furnace.mo index d1d72f9e8..00fea2110 100644 Binary files a/po/locale/sv/LC_MESSAGES/furnace.mo and b/po/locale/sv/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/th/LC_MESSAGES/furnace.mo b/po/locale/th/LC_MESSAGES/furnace.mo index d9439feef..fa6611dc3 100644 Binary files a/po/locale/th/LC_MESSAGES/furnace.mo and b/po/locale/th/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/tr/LC_MESSAGES/furnace.mo b/po/locale/tr/LC_MESSAGES/furnace.mo index b913962dd..7cc9e855b 100644 Binary files a/po/locale/tr/LC_MESSAGES/furnace.mo and b/po/locale/tr/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/uk/LC_MESSAGES/furnace.mo b/po/locale/uk/LC_MESSAGES/furnace.mo index 877f306f6..bd7131b72 100644 Binary files a/po/locale/uk/LC_MESSAGES/furnace.mo and b/po/locale/uk/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/zh/LC_MESSAGES/furnace.mo b/po/locale/zh/LC_MESSAGES/furnace.mo index de4e85411..e230d7209 100644 Binary files a/po/locale/zh/LC_MESSAGES/furnace.mo and b/po/locale/zh/LC_MESSAGES/furnace.mo differ diff --git a/po/locale/zh_HK/LC_MESSAGES/furnace.mo b/po/locale/zh_HK/LC_MESSAGES/furnace.mo index 1526e90ac..6533b7eb3 100644 Binary files a/po/locale/zh_HK/LC_MESSAGES/furnace.mo and b/po/locale/zh_HK/LC_MESSAGES/furnace.mo differ diff --git a/po/nl.po b/po/nl.po index 94b118649..d716edfb3 100644 --- a/po/nl.po +++ b/po/nl.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Lunathir\n" diff --git a/po/pl.po b/po/pl.po index 0386d3653..6446ff6e7 100644 --- a/po/pl.po +++ b/po/pl.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: freq-mod, PoznańskiSzybkowiec\n" "Language: pl\n" diff --git a/po/pt_BR.po b/po/pt_BR.po index e4a89766d..7f2c655aa 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: pt_BR\n" diff --git a/po/ru.po b/po/ru.po index b330cccad..1254bb3b9 100644 --- a/po/ru.po +++ b/po/ru.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: LTVA\n" "Language-Team: none\n" "Language: ru\n" diff --git a/po/sk.po b/po/sk.po index 0256cd7c2..8aa7c0622 100644 --- a/po/sk.po +++ b/po/sk.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: sk\n" diff --git a/po/sv.po b/po/sv.po index 825d83695..b7e5265b9 100644 --- a/po/sv.po +++ b/po/sv.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: RevvoBolt\n" "Language-Team: RevvoBolt\n" "Language: sv\n" @@ -20,12 +20,12 @@ msgstr "för tidigt slut på filen" #: src/engine/fileOps/s3m.cpp:320 #, c-format msgid "Channel %d" -msgstr "" +msgstr "Kanal %d" #: src/engine/fileOps/s3m.cpp:336 #, c-format msgid "FM %d" -msgstr "" +msgstr "FM %d" #: src/engine/sysDef.cpp:428 src/engine/sysDef.cpp:442 msgid "20xx: Set channel mode (bit 0: square; bit 1: noise; bit 2: envelope)" @@ -253,7 +253,7 @@ msgstr "5Fxx: Ange decay 2 för operator 4 (0 till 1F)" #: src/engine/sysDef.cpp:507 src/engine/sysDef.cpp:518 msgid "60xx: Set operator mask (bits 0-3)" -msgstr "" +msgstr "60xx: Ställ in operatörsmask (bits 0-3)" #: src/engine/sysDef.cpp:512 msgid "10xx: Set noise frequency (xx: value; 0 disables noise)" @@ -1799,7 +1799,7 @@ msgstr "15xx: Ställ in modulatortabellen på vågform" #: src/engine/sysDef.cpp:1059 msgid "16xy: Automatic modulation speed (x: numerator; y: denominator)" -msgstr "" +msgstr "16xy: Automatisk moduleringshastighet (x: täljare; y: nämnare)" #: src/engine/sysDef.cpp:1064 msgid "MMC5" @@ -3672,15 +3672,15 @@ msgstr "Cxxx: Ställ in tickfrekvens (hz)" #: src/engine/engine.cpp:102 msgid "D3xx: Volume portamento" -msgstr "" +msgstr "D3xx: Portamentovolym" #: src/engine/engine.cpp:104 msgid "D4xx: Volume portamento (fast)" -msgstr "" +msgstr "D4xx: Portamentovolym (snabb)" #: src/engine/engine.cpp:106 msgid "DCxx: Delayed mute" -msgstr "" +msgstr "DCxx: Fördröjd tystnad" #: src/engine/engine.cpp:108 msgid "E0xx: Set arp speed" @@ -3748,11 +3748,11 @@ msgstr "F0xx: Ange tickhastighet (bpm)" #: src/engine/engine.cpp:140 msgid "F1xx: Single tick pitch up" -msgstr "" +msgstr "F1xx: Enkel ticktonhöjd upp" #: src/engine/engine.cpp:142 msgid "F2xx: Single tick pitch down" -msgstr "" +msgstr "F2xx: Enkel ticktonhöjd ner" #: src/engine/engine.cpp:144 msgid "F3xx: Fine volume slide up" @@ -3776,11 +3776,11 @@ msgstr "F7xx: Starta om makro (se manualen)" #: src/engine/engine.cpp:154 msgid "F8xx: Single tick volume up" -msgstr "" +msgstr "F8xx: Enkel tickvolym upp" #: src/engine/engine.cpp:156 msgid "F9xx: Single tick volume down" -msgstr "" +msgstr "F9xx: Enkel tickvolym ned" #: src/engine/engine.cpp:158 msgid "FAxx: Fast volume slide (0y: down; x0: up)" @@ -4320,15 +4320,15 @@ msgstr "spara råsample..." #: src/gui/dataList.cpp:447 msgid "save all instruments..." -msgstr "" +msgstr "spara alla instrument..." #: src/gui/dataList.cpp:450 msgid "save all wavetables..." -msgstr "" +msgstr "spara alla wavetables..." #: src/gui/dataList.cpp:453 msgid "save all samples..." -msgstr "" +msgstr "spara alla samples..." #: src/gui/dataList.cpp:457 src/gui/insEdit.cpp:5365 msgid "save as .dmp..." @@ -4336,7 +4336,7 @@ msgstr "spara som .dmp..." #: src/gui/dataList.cpp:461 src/gui/dataList.cpp:769 src/gui/dataList.cpp:915 msgid "save all..." -msgstr "" +msgstr "spara alla..." #: src/gui/dataList.cpp:474 src/gui/dataList.cpp:782 src/gui/dataList.cpp:927 msgid "Toggle folders/standard view" @@ -4465,11 +4465,11 @@ msgstr "Inga" #: src/gui/exportOptions.cpp:90 msgid "Shown in pattern" -msgstr "" +msgstr "Visas i mönster" #: src/gui/exportOptions.cpp:96 msgid "Shown in oscilloscope" -msgstr "" +msgstr "Visas i oscilloskop" #: src/gui/exportOptions.cpp:122 src/gui/exportOptions.cpp:226 #: src/gui/exportOptions.cpp:237 src/gui/exportOptions.cpp:357 @@ -4607,7 +4607,7 @@ msgstr "maxstorlek i andra banker" #: src/gui/exportOptions.cpp:303 msgid "chip to export:" -msgstr "" +msgstr "chip att exportera:" #: src/gui/exportOptions.cpp:327 msgid "Tick Rate (Hz)" @@ -4619,11 +4619,11 @@ msgstr "optimera storlek" #: src/gui/exportOptions.cpp:346 msgid "select a target from the menu at the top of this dialog." -msgstr "" +msgstr "välj ett mål från menyn högst upp i denna dialogruta." #: src/gui/exportOptions.cpp:349 msgid "this export method doesn't offer any options." -msgstr "" +msgstr "denna exportmetod erbjuder inga alternativ." #: src/gui/exportOptions.cpp:370 msgid "this option exports the song to a text file.\n" @@ -5475,7 +5475,7 @@ msgstr "Underlåtar" #: src/gui/editControls.cpp:536 msgid "Comments" -msgstr "" +msgstr "Kommentarer" #: src/gui/editControls.cpp:545 msgid "Channels here..." @@ -6656,7 +6656,7 @@ msgstr "summa: %d" #: src/gui/sysConf.cpp:2089 msgid "Disable Gaussian interpolation" -msgstr "" +msgstr "Inaktivera Gaussisk interpolation" #: src/gui/sysConf.cpp:2146 src/gui/insEdit.cpp:50 src/gui/insEdit.cpp:63 #: src/gui/insEdit.cpp:75 @@ -12378,7 +12378,7 @@ msgstr "Wavetable: växla mappar/standardvy" #: src/gui/guiConst.cpp:725 msgid "Save all wavetables" -msgstr "" +msgstr "Spara alla wavetables" #: src/gui/guiConst.cpp:728 msgid "---Sample list" @@ -13639,15 +13639,15 @@ msgstr "för många instrument!" #: src/gui/doAction.cpp:794 msgid "this song doesn't have any instruments." -msgstr "" +msgstr "denna låt har inga instrument." #: src/gui/doAction.cpp:921 msgid "this song doesn't have any wavetables." -msgstr "" +msgstr "denna låt har inga wavetables." #: src/gui/doAction.cpp:1082 msgid "this song doesn't have any samples." -msgstr "" +msgstr "denna låt har inga samples." #: src/gui/doAction.cpp:1153 msgid "couldn't paste! make sure your sample is 8 or 16-bit." @@ -13715,7 +13715,7 @@ msgstr "vols" #: src/gui/csPlayer.cpp:164 msgid "volst" -msgstr "" +msgstr "volst" #: src/gui/csPlayer.cpp:166 msgid "vib" @@ -13933,7 +13933,7 @@ msgstr "binärfil" #: src/gui/gui.cpp:2049 src/gui/gui.cpp:2055 msgid "Export ROM" -msgstr "" +msgstr "Exportera ROM" #: src/gui/gui.cpp:2066 src/gui/gui.cpp:2075 src/gui/gui.cpp:2084 msgid "Select Font" @@ -17032,31 +17032,31 @@ msgstr "Vågmix" #: src/gui/insEdit.cpp:7568 msgid "Timer Macros" -msgstr "" +msgstr "Timermakron" #: src/gui/insEdit.cpp:7570 msgid "warning: timer effects are not supported by VGM export!" -msgstr "" +msgstr "varning: timereffekter stöds inte av VGM-export!" #: src/gui/insEdit.cpp:7571 msgid "Timer FX" -msgstr "" +msgstr "Timer FX" #: src/gui/insEdit.cpp:7572 msgid "TFX Offset" -msgstr "" +msgstr "TFX-offset" #: src/gui/insEdit.cpp:7573 msgid "Timer Num" -msgstr "" +msgstr "Timertäljare" #: src/gui/insEdit.cpp:7574 msgid "Timer Den" -msgstr "" +msgstr "Timernämnare" #: src/gui/insEdit.cpp:7575 msgid "PWM Boundary" -msgstr "" +msgstr "PWM-gräns" #: src/gui/insEdit.cpp:7588 msgid "Octave offset" diff --git a/po/th.po b/po/th.po index b0c3c6bce..413730580 100644 --- a/po/th.po +++ b/po/th.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: th\n" diff --git a/po/tr.po b/po/tr.po index d8d67f9f4..5c7c8a84e 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: tr\n" diff --git a/po/uk.po b/po/uk.po index 8cdf5acd0..a798febee 100644 --- a/po/uk.po +++ b/po/uk.po @@ -1,7 +1,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: uk\n" diff --git a/po/zh.po b/po/zh.po index 60316bb72..84328c1c4 100644 --- a/po/zh.po +++ b/po/zh.po @@ -27,7 +27,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: DJRen_GTR3QQ\n" "Language-Team: null\n" "Language: zh\n" diff --git a/po/zh_HK.po b/po/zh_HK.po index a110a9bab..406c4d453 100644 --- a/po/zh_HK.po +++ b/po/zh_HK.po @@ -27,7 +27,7 @@ # msgid "" msgstr "" -"Project-Id-Version: furnace 0.6.6\n" +"Project-Id-Version: furnace 0.6.7\n" "Last-Translator: DJRen_GTR3QQ\n" "Language-Team: null\n" "Language: zh_HK\n" diff --git a/res/Info.plist b/res/Info.plist index 63aa3ebde..94a9090d6 100644 --- a/res/Info.plist +++ b/res/Info.plist @@ -15,17 +15,17 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString - 0.6.6 + 0.6.7 CFBundleName Furnace CFBundlePackageType APPL CFBundleShortVersionString - 0.6.6 + 0.6.7 CFBundleSignature ???? CFBundleVersion - 0.6.6 + 0.6.7 NSHumanReadableCopyright NSHighResolutionCapable diff --git a/res/docpdf/make_paper.py b/res/docpdf/make_paper.py index 4106fcddf..7929f8f76 100644 --- a/res/docpdf/make_paper.py +++ b/res/docpdf/make_paper.py @@ -325,7 +325,7 @@ if __name__ == "__main__":

Furnace
User Manual

- for version 0.6.6 + for version 0.6.7
@@ -348,7 +348,7 @@ if __name__ == "__main__":

this documentation is under the Creative Commons Attribution 3.0 Unported license.

you may reproduce, modify and/or distribute this documentation provided this copyright notice (including license and attribution) is present and any necessary disclaimers whether modifications have been made.

this documentation is provided as-is and without warranty of any kind.

-

this manual is written for version 0.6.6 of Furnace.
it may not necessarily apply to previous or future versions.

+

this manual is written for version 0.6.7 of Furnace.
it may not necessarily apply to previous or future versions.

%s diff --git a/res/furnace.rc b/res/furnace.rc index 986364064..ea459aaf2 100644 --- a/res/furnace.rc +++ b/res/furnace.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO - FILEVERSION 0,6,6,0 - PRODUCTVERSION 0,6,6,0 + FILEVERSION 0,6,7,0 + PRODUCTVERSION 0,6,7,0 { BLOCK "VarFileInfo" { @@ -33,10 +33,10 @@ "Furnace" VALUE "ProductVersion", - "0.6.6" + "0.6.7" VALUE "FileVersion", - "0.6.6" + "0.6.7" VALUE "CompanyName", "tildearrow" diff --git a/res/releaseReadme/stable-linux.txt b/res/releaseReadme/stable-linux.txt index 151e82f09..c3e9bf6ca 100644 --- a/res/releaseReadme/stable-linux.txt +++ b/res/releaseReadme/stable-linux.txt @@ -16,7 +16,7 @@ if you find issues (e.g. bugs or annoyances), report them. links below. - Furnace on GitHub (project page and issue tracker): https://github.com/tildearrow/furnace - issues: https://github.com/tildearrow/furnace/issues - discussion: https://github.com/tildearrow/furnace/discussions -- online manual: https://tildearrow.org/furnace/doc/v0.6.6/ +- online manual: https://tildearrow.org/furnace/doc/v0.6.7/ # notes diff --git a/res/releaseReadme/stable-mac.txt b/res/releaseReadme/stable-mac.txt index 8fd364fc4..0f98a4ec8 100644 --- a/res/releaseReadme/stable-mac.txt +++ b/res/releaseReadme/stable-mac.txt @@ -26,7 +26,7 @@ if you find issues (e.g. bugs or annoyances), report them. links below. - Furnace on GitHub (project page and issue tracker): https://github.com/tildearrow/furnace - issues: https://github.com/tildearrow/furnace/issues - discussion: https://github.com/tildearrow/furnace/discussions -- online manual: https://tildearrow.org/furnace/doc/v0.6.6/ +- online manual: https://tildearrow.org/furnace/doc/v0.6.7/ # notes diff --git a/res/releaseReadme/stable-win.txt b/res/releaseReadme/stable-win.txt index 72e520a6d..41e8c1508 100644 --- a/res/releaseReadme/stable-win.txt +++ b/res/releaseReadme/stable-win.txt @@ -16,7 +16,8 @@ if you find issues (e.g. bugs or annoyances), report them. links below. - Furnace on GitHub (project page and issue tracker): https://github.com/tildearrow/furnace - issues: https://github.com/tildearrow/furnace/issues - discussion: https://github.com/tildearrow/furnace/discussions -- online manual: https://tildearrow.org/furnace/doc/v0.6.6/ +- online manual: https://tildearrow.org/furnace/doc/v0.6.7/ + # notes copyright (C) 2021-2024 tildearrow and contributors. diff --git a/scripts/update-po.sh b/scripts/update-po.sh index 4425cb097..8953e1dc2 100755 --- a/scripts/update-po.sh +++ b/scripts/update-po.sh @@ -1,6 +1,6 @@ #!/bin/bash -FUR_VERSION="0.6.6" +FUR_VERSION="0.6.7" EXPORT_LANGS=("de" "es" "fr" "fi" "hy" "id" "ja" "ko" "nl" "pl" "pt_BR" "ru" "sk" "sv" "th" "tr" "uk" "zh" "zh_HK") diff --git a/src/engine/cmdStream.cpp b/src/engine/cmdStream.cpp index 40542ef98..f1b267869 100644 --- a/src/engine/cmdStream.cpp +++ b/src/engine/cmdStream.cpp @@ -364,6 +364,7 @@ bool DivCSPlayer::tick() { } if (sendVolume || chan[i].volSpeed!=0) { + int preSpeedVol=chan[i].volume; chan[i].volume+=chan[i].volSpeed; if (chan[i].volSpeedTarget!=-1) { bool atTarget=false; @@ -377,7 +378,11 @@ bool DivCSPlayer::tick() { } if (atTarget) { - chan[i].volume=chan[i].volSpeedTarget; + if (chan[i].volSpeed>0) { + chan[i].volume=MAX(preSpeedVol,chan[i].volSpeedTarget); + } else if (chan[i].volSpeed<0) { + chan[i].volume=MIN(preSpeedVol,chan[i].volSpeedTarget); + } chan[i].volSpeed=0; chan[i].volSpeedTarget=-1; } diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index aaf8944e5..273c58957 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -267,6 +267,22 @@ enum DivDispatchCmds { DIV_CMD_FDS_MOD_AUTO, DIV_CMD_FM_OPMASK, // (mask) + + DIV_CMD_MULTIPCM_MIX_FM, // (value) + DIV_CMD_MULTIPCM_MIX_PCM, // (value) + DIV_CMD_MULTIPCM_LFO, // (value) + DIV_CMD_MULTIPCM_VIB, // (value) + DIV_CMD_MULTIPCM_AM, // (value) + DIV_CMD_MULTIPCM_AR, // (value) + DIV_CMD_MULTIPCM_D1R, // (value) + DIV_CMD_MULTIPCM_DL, // (value) + DIV_CMD_MULTIPCM_D2R, // (value) + DIV_CMD_MULTIPCM_RC, // (value) + DIV_CMD_MULTIPCM_RR, // (value) + DIV_CMD_MULTIPCM_DAMP, // (value) + DIV_CMD_MULTIPCM_PSEUDO_REVERB, // (value) + DIV_CMD_MULTIPCM_LFO_RESET, // (value) + DIV_CMD_MULTIPCM_LEVEL_DIRECT, // (value) DIV_CMD_SID3_SPECIAL_WAVE, DIV_CMD_SID3_RING_MOD_SRC, diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index f13258c86..af472a268 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -76,6 +76,8 @@ #include "platform/vb.h" #include "platform/k007232.h" #include "platform/ga20.h" +#include "platform/supervision.h" +#include "platform/upd1771c.h" #include "platform/sm8521.h" #include "platform/pv1000.h" #include "platform/k053260.h" @@ -685,6 +687,12 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_GA20: dispatch=new DivPlatformGA20; break; + case DIV_SYSTEM_SUPERVISION: + dispatch=new DivPlatformSupervision; + break; + case DIV_SYSTEM_UPD1771C: + dispatch=new DivPlatformUPD1771c; + break; case DIV_SYSTEM_SM8521: dispatch=new DivPlatformSM8521; if (isRender) { @@ -764,6 +772,24 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_SID3: dispatch=new DivPlatformSID3; break; + case DIV_SYSTEM_OPL4: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(4,false); + if (isRender) { + ((DivPlatformOPL*)dispatch)->setCore(eng->getConfInt("opl4CoreRender",0)); + } else { + ((DivPlatformOPL*)dispatch)->setCore(eng->getConfInt("opl4Core",0)); + } + break; + case DIV_SYSTEM_OPL4_DRUMS: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(4,true); + if (isRender) { + ((DivPlatformOPL*)dispatch)->setCore(eng->getConfInt("opl4CoreRender",0)); + } else { + ((DivPlatformOPL*)dispatch)->setCore(eng->getConfInt("opl4Core",0)); + } + break; case DIV_SYSTEM_DUMMY: dispatch=new DivPlatformDummy; break; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index c611ff3e9..d42b742c4 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -520,6 +520,15 @@ void DivEngine::initSongWithDesc(const char* description, bool inBase64, bool ol if (song.subsong[0]->hz<1.0) song.subsong[0]->hz=1.0; if (song.subsong[0]->hz>999.0) song.subsong[0]->hz=999.0; + curChanMask=c.getIntList("chanMask",{}); + for (unsigned char i:curChanMask) { + int j=i-1; + if (j<0) j=0; + if (j>DIV_MAX_CHANS) j=DIV_MAX_CHANS-1; + curSubSong->chanShow[j]=false; + curSubSong->chanShowChanOsc[j]=false; + } + song.author=getConfString("defaultAuthorName",""); } @@ -754,6 +763,13 @@ int DivEngine::addSubSong() { BUSY_BEGIN; saveLock.lock(); song.subsong.push_back(new DivSubSong); + for (unsigned char i:curChanMask) { + int j=i-1; + if (j<0) j=0; + if (j>DIV_MAX_CHANS) j=DIV_MAX_CHANS-1; + song.subsong.back()->chanShow[j]=false; + song.subsong.back()->chanShowChanOsc[j]=false; + } saveLock.unlock(); BUSY_END; return song.subsong.size()-1; diff --git a/src/engine/engine.h b/src/engine/engine.h index 5a17fa2ad..2b033be5d 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -52,10 +52,10 @@ class DivWorkPool; #define EXTERN_BUSY_BEGIN_SOFT e->softLocked=true; e->isBusy.lock(); #define EXTERN_BUSY_END e->isBusy.unlock(); e->softLocked=false; -//#define DIV_UNSTABLE +#define DIV_UNSTABLE -#define DIV_VERSION "0.6.6" -#define DIV_ENGINE_VERSION 218 +#define DIV_VERSION "dev222" +#define DIV_ENGINE_VERSION 222 // for imports #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 @@ -518,6 +518,7 @@ class DivEngine { std::vector cmdStream; std::vector possibleInsTypes; std::vector effectInst; + std::vector curChanMask; static DivSysDef* sysDefs[DIV_MAX_CHIP_DEFS]; static DivSystem sysFileMapFur[DIV_MAX_CHIP_DEFS]; static DivSystem sysFileMapDMF[DIV_MAX_CHIP_DEFS]; diff --git a/src/engine/export/tiuna.cpp b/src/engine/export/tiuna.cpp index 5b3dcc0ac..45827b7f4 100644 --- a/src/engine/export/tiuna.cpp +++ b/src/engine/export/tiuna.cpp @@ -503,12 +503,6 @@ void DivExportTiuna::run() { running=false; return; } - SafeWriter dbg; - dbg.init(); - dbg.writeText(fmt::format("renderedCmds size={}\n",renderedCmds.size())); - for (const auto& i: confirmedMatches) { - dbg.writeText(fmt::format("pos={},end={},id={}\n",i.pos,i.endPos,i.id,i.size)); - } // write commands int totalSize=0; diff --git a/src/engine/export/zsm.cpp b/src/engine/export/zsm.cpp index 81c5021a0..d5786edd9 100644 --- a/src/engine/export/zsm.cpp +++ b/src/engine/export/zsm.cpp @@ -674,7 +674,10 @@ void DivExportZSM::run() { if (writes.size()>0) logD("zsmOps: Writing %d messages to chip %d",writes.size(),i); for (DivRegWrite& write: writes) { - if (i==YM) zsm.writeYM(write.addr&0xff,write.val); + if (i==YM) { + if (done && write.addr==0x08 && (write.val&0x78)>0) continue; // don't process keydown on lookahead + zsm.writeYM(write.addr&0xff,write.val); + } if (i==VERA) { if (done && write.addr>=64) continue; // don't process any PCM or sync events on the loop lookahead zsm.writePSG(write.addr&0xff,write.val); diff --git a/src/engine/fileOps/fur.cpp b/src/engine/fileOps/fur.cpp index 5483d8fb9..1f0e74c4c 100644 --- a/src/engine/fileOps/fur.cpp +++ b/src/engine/fileOps/fur.cpp @@ -2102,6 +2102,16 @@ bool DivEngine::loadFur(unsigned char* file, size_t len, int variantID) { } } + // SNES no anti-click + if (ds.version<220) { + for (int i=0; iwriteC(multipcm.vib); w->writeC(multipcm.am); + unsigned char next=( + (multipcm.damp?1:0)& + (multipcm.pseudoReverb?2:0)& + (multipcm.lfoReset?4:0)& + (multipcm.levelDirect?8:0) + ); + w->writeC(next); + FEATURE_END; } @@ -1386,6 +1398,12 @@ void DivInstrument::putInsData2(SafeWriter* w, bool fui, const DivSong* song, bo featureS2=true; featureS3=true; break; + case DIV_INS_SUPERVISION: + featureSM=true; + if (amiga.useSample) featureSL=true; + break; + case DIV_INS_UPD1771C: + break; case DIV_INS_MAX: break; case DIV_INS_NULL: @@ -2337,6 +2355,14 @@ void DivInstrument::readFeatureMP(SafeReader& reader, short version) { multipcm.vib=reader.readC(); multipcm.am=reader.readC(); + if (version>=221) { + unsigned char next=reader.readC(); + multipcm.damp=next&1; + multipcm.pseudoReverb=next&2; + multipcm.lfoReset=next&4; + multipcm.levelDirect=next&8; + } + READ_FEAT_END; } @@ -3676,4 +3702,4 @@ DivInstrument& DivInstrument::operator=( const DivInstrument& ins ) { *(DivInstrumentPOD*)this=ins; name=ins.name; return *this; -} \ No newline at end of file +} diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 9fc9fe08a..5d3bf695c 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -96,6 +96,8 @@ enum DivInstrumentType: unsigned short { DIV_INS_GBA_MINMOD=61, DIV_INS_BIFURCATOR=62, DIV_INS_SID2=63, // coincidence! + DIV_INS_SUPERVISION=64, + DIV_INS_UPD1771C=65, DIV_INS_SID3=66, DIV_INS_MAX, DIV_INS_NULL @@ -612,6 +614,7 @@ struct DivInstrumentFDS { struct DivInstrumentMultiPCM { unsigned char ar, d1r, dl, d2r, rr, rc; unsigned char lfo, vib, am; + bool damp, pseudoReverb, lfoReset, levelDirect; bool operator==(const DivInstrumentMultiPCM& other); bool operator!=(const DivInstrumentMultiPCM& other) { @@ -620,7 +623,11 @@ struct DivInstrumentMultiPCM { DivInstrumentMultiPCM(): ar(15), d1r(15), dl(0), d2r(0), rr(15), rc(15), - lfo(0), vib(0), am(0) { + lfo(0), vib(0), am(0), + damp(false), + pseudoReverb(false), + lfoReset(false), + levelDirect(true) { } }; diff --git a/src/engine/platform/arcade.cpp b/src/engine/platform/arcade.cpp index 74cfcc6e7..d46976bb0 100644 --- a/src/engine/platform/arcade.cpp +++ b/src/engine/platform/arcade.cpp @@ -762,7 +762,20 @@ int DivPlatformArcade::dispatch(DivCommand c) { break; } case DIV_CMD_FM_OPMASK: - chan[c.chan].opMask=c.value&15; + switch (c.value>>4) { + case 1: + case 2: + case 3: + case 4: + chan[c.chan].opMask&=~(1<<((c.value>>4)-1)); + if (c.value&15) { + chan[c.chan].opMask|=(1<<((c.value>>4)-1)); + } + break; + default: + chan[c.chan].opMask=c.value&15; + break; + } if (chan[c.chan].active) { chan[c.chan].opMaskChanged=true; } diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index 11ce83389..ab2904572 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -113,14 +113,15 @@ const unsigned char dacLogTableAY[256]={ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15 }; -void DivPlatformAY8910::runDAC() { +void DivPlatformAY8910::runDAC(int runRate) { + if (runRate==0) runRate=dacRate; for (int i=0; i<3; i++) { if (chan[i].active && (chan[i].curPSGMode.val&8) && chan[i].dac.sample!=-1) { chan[i].dac.period+=chan[i].dac.rate; bool end=false; bool changed=false; int prevOut=chan[i].dac.out; - while (chan[i].dac.period>dacRate && !end) { + while (chan[i].dac.period>runRate && !end) { DivSample* s=parent->getSample(chan[i].dac.sample); if (s->samples<=0 || chan[i].dac.pos<0 || chan[i].dac.pos>=(int)s->samples) { chan[i].dac.sample=-1; @@ -143,7 +144,7 @@ void DivPlatformAY8910::runDAC() { end=true; break; } - chan[i].dac.period-=dacRate; + chan[i].dac.period-=runRate; } if (changed && !end) { if (!isMuted[i]) { @@ -154,13 +155,15 @@ void DivPlatformAY8910::runDAC() { } } -void DivPlatformAY8910::runTFX() { +void DivPlatformAY8910::runTFX(int runRate) { /* developer's note: if you are checking for intellivision make sure to add "&& selCore" because for some reason, the register remap doesn't work when the user uses AtomicSSG core */ + float counterRatio=1.0; + if (runRate!=0) counterRatio=(double)rate/(double)runRate; int timerPeriod, output; for (int i=0; i<3; i++) { if (chan[i].active && (chan[i].curPSGMode.val&16) && !(chan[i].curPSGMode.val&8) && chan[i].tfx.mode!=-1) { @@ -182,9 +185,9 @@ void DivPlatformAY8910::runTFX() { continue; } } - chan[i].tfx.counter += 1; + chan[i].tfx.counter += counterRatio; if (chan[i].tfx.counter >= chan[i].tfx.period && chan[i].tfx.mode == 0) { - chan[i].tfx.counter = 0; + chan[i].tfx.counter -= chan[i].tfx.period; chan[i].tfx.out ^= 1; output = ((chan[i].tfx.out) ? chan[i].outVol : (chan[i].tfx.lowBound-(15-chan[i].outVol))); // TODO: fix this stupid crackling noise that happens @@ -201,7 +204,7 @@ void DivPlatformAY8910::runTFX() { } } if (chan[i].tfx.counter >= chan[i].tfx.period && chan[i].tfx.mode == 1) { - chan[i].tfx.counter = 0; + chan[i].tfx.counter -= chan[i].tfx.period; if (!isMuted[i]) { if (intellivision && selCore) { immWrite(0xa, ayEnvMode); @@ -211,7 +214,7 @@ void DivPlatformAY8910::runTFX() { } } if (chan[i].tfx.counter >= chan[i].tfx.period && chan[i].tfx.mode == 2) { - chan[i].tfx.counter = 0; + chan[i].tfx.counter -= chan[i].tfx.period; } } if (chan[i].tfx.num > 0) { @@ -327,12 +330,9 @@ void DivPlatformAY8910::acquire(short** buf, size_t len) { void DivPlatformAY8910::fillStream(std::vector& stream, int sRate, size_t len) { writes.clear(); - int rate=(int)(chipClock/sRate); for (size_t i=0; i& stream, int sRate, size_t len); diff --git a/src/engine/platform/genesis.cpp b/src/engine/platform/genesis.cpp index 1b8707896..4bf56581e 100644 --- a/src/engine/platform/genesis.cpp +++ b/src/engine/platform/genesis.cpp @@ -1465,7 +1465,20 @@ int DivPlatformGenesis::dispatch(DivCommand c) { } case DIV_CMD_FM_OPMASK: if (c.chan>=psgChanOffs) break; - chan[c.chan].opMask=c.value&15; + switch (c.value>>4) { + case 1: + case 2: + case 3: + case 4: + chan[c.chan].opMask&=~(1<<((c.value>>4)-1)); + if (c.value&15) { + chan[c.chan].opMask|=(1<<((c.value>>4)-1)); + } + break; + default: + chan[c.chan].opMask=c.value&15; + break; + } if (chan[c.chan].active) { chan[c.chan].opMaskChanged=true; } diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index 67fdbc11e..79c725c90 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -175,6 +175,13 @@ void DivPlatformNES::acquire_NSFPlayE(short** buf, size_t len) { int out2[2]; for (size_t i=0; iTick(8); e2_NP->TickFrameSequence(8); @@ -436,7 +443,7 @@ void DivPlatformNES::tick(bool sysTick) { // https://www.youtube.com/watch?v=vB4P8x2Am6Y if (lsamp->loopEnd>lsamp->loopStart && goingToLoop) { - int loopStartAddr=(sampleOffDPCM[dacSample]+lsamp->loopStart)>>3; + int loopStartAddr=sampleOffDPCM[dacSample]+(lsamp->loopStart>>3); int loopLen=(lsamp->loopEnd-lsamp->loopStart)>>3; rWrite(0x4012,(loopStartAddr>>6)&0xff); @@ -492,14 +499,14 @@ int DivPlatformNES::dispatch(DivCommand c) { } } if (c.value!=DIV_NOTE_NULL) { - dacSample=ins->amiga.getSample(c.value); + dacSample=(int)ins->amiga.getSample(c.value); if (ins->type==DIV_INS_AMIGA) { chan[c.chan].sampleNote=c.value; c.value=ins->amiga.getFreq(c.value); chan[c.chan].sampleNoteDelta=c.value-chan[c.chan].sampleNote; } } else if (chan[c.chan].sampleNote!=DIV_NOTE_NULL) { - dacSample=ins->amiga.getSample(chan[c.chan].sampleNote); + dacSample=(int)ins->amiga.getSample(chan[c.chan].sampleNote); if (ins->type==DIV_INS_AMIGA) { c.value=ins->amiga.getFreq(chan[c.chan].sampleNote); } diff --git a/src/engine/platform/opl.cpp b/src/engine/platform/opl.cpp index 9a98f9de0..cc3ccf41e 100644 --- a/src/engine/platform/opl.cpp +++ b/src/engine/platform/opl.cpp @@ -29,6 +29,12 @@ #define KVSL(x,y) ((chan[x].state.op[orderedOpsL1[ops==4][y]].kvs==2 && isOutputL[ops==4][chan[x].state.alg][y]) || chan[x].state.op[orderedOpsL1[ops==4][y]].kvs==1) #define CHIP_FREQBASE chipFreqBase +#define PCM_FREQBASE (402653184) + +#define NOTE_PCM(x) parent->calcBaseFreq(chipClock,PCM_FREQBASE,x,false) + +#define PCM_CHECK(ch) ((chipType==4) && (ch>=pcmChanOffs)) +#define PCM_REG(ch) (ch-pcmChanOffs) // N = invalid #define N 255 @@ -159,37 +165,59 @@ const int orderedOpsL[4]={ #define ADDR_FREQH 0xb0 #define ADDR_LR_FB_ALG 0xc0 + +#define PCM_ADDR_WAVE_L 0x208 // Wavetable number LSB +#define PCM_ADDR_WAVE_H_FN_L 0x220 // Wavetable number MSB, F-number LSB +#define PCM_ADDR_FN_H_PR_OCT 0x238 // F-number MSB, Pseudo-reverb, Octave +#define PCM_ADDR_TL 0x250 // Total level, Level direct +#define PCM_ADDR_KEY_DAMP_LFORST_CH_PAN 0x268 // Key, Damp, LFO Reset, Channel select, Panpot + +#define PCM_ADDR_LFO_VIB 0x280 +#define PCM_ADDR_AR_D1R 0x298 +#define PCM_ADDR_DL_D2R 0x2b0 +#define PCM_ADDR_RC_RR 0x2c8 +#define PCM_ADDR_AM 0x2e0 + +#define PCM_ADDR_MIX_FM 0x2f8 +#define PCM_ADDR_MIX_PCM 0x2f9 + void DivPlatformOPL::acquire_nuked(short** buf, size_t len) { - thread_local short o[4]; - thread_local int os[4]; + thread_local short o[8]; + thread_local int os[6]; thread_local ymfm::ymfm_output<2> aOut; + thread_local short pcmBuf[24]; for (size_t h=0; h=0) { - adpcmB->write(w.addr-7,(w.val&15)|0x80); - OPL3_WriteReg(&fm,w.addr,w.val&0xc0); - } else { + if (w.addr>=0x200) { + pcm.writeReg(w.addr&0xff,w.val); + regPool[0x200|(w.addr&0xff)]=w.val; + } else { + switch (w.addr) { + case 8: + if (adpcmChan>=0) { + adpcmB->write(w.addr-7,(w.val&15)|0x80); + OPL3_WriteReg(&fm,w.addr,w.val&0xc0); + } else { + OPL3_WriteReg(&fm,w.addr,w.val); + } + break; + case 7: case 9: case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 21: case 22: case 23: + if (adpcmChan>=0) { + adpcmB->write(w.addr-7,w.val); + } else { + OPL3_WriteReg(&fm,w.addr,w.val); + } + break; + default: OPL3_WriteReg(&fm,w.addr,w.val); - } - break; - case 7: case 9: case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 21: case 22: case 23: - if (adpcmChan>=0) { - adpcmB->write(w.addr-7,w.val); - } else { - OPL3_WriteReg(&fm,w.addr,w.val); - } - break; - default: - OPL3_WriteReg(&fm,w.addr,w.val); - break; + break; + } + regPool[w.addr&511]=w.val; } - regPool[w.addr&511]=w.val; writes.pop(); } @@ -198,10 +226,20 @@ void DivPlatformOPL::acquire_nuked(short** buf, size_t len) { } else { OPL3_Generate4Ch(&fm,o); } - os[0]+=o[0]; - os[1]+=o[1]; - os[2]+=o[2]; - os[3]+=o[3]; + if (chipType==4) { + pcm.generateMix(o[0],o[1],o[4],o[5],o[6],o[7],pcmBuf); + os[0]+=o[4]; // FM + PCM left + os[1]+=o[5]; // FM + PCM right + os[2]+=o[2]; // FM left + os[3]+=o[3]; // FM right + os[4]+=o[6]; // PCM left + os[5]+=o[7]; // PCM right + } else { + os[0]+=o[0]; + os[1]+=o[1]; + os[2]+=o[2]; + os[3]+=o[3]; + } if (adpcmChan>=0) { adpcmB->clock(); @@ -261,6 +299,12 @@ void DivPlatformOPL::acquire_nuked(short** buf, size_t len) { oscBuf[i]->data[oscBuf[i]->needle++]=CLAMP(chOut<<2,-32768,32767); } } + + if (chipType==4) { + for (int i=pcmChanOffs; idata[oscBuf[i]->needle++]=CLAMP(pcmBuf[i-pcmChanOffs],-32768,32767); + } + } if (os[0]<-32768) os[0]=-32768; if (os[0]>32767) os[0]=32767; @@ -274,6 +318,12 @@ void DivPlatformOPL::acquire_nuked(short** buf, size_t len) { if (os[3]<-32768) os[3]=-32768; if (os[3]>32767) os[3]=32767; + if (os[4]<-32768) os[4]=-32768; + if (os[4]>32767) os[4]=32767; + + if (os[5]<-32768) os[5]=-32768; + if (os[5]>32767) os[5]=32767; + buf[0][h]=os[0]; if (totalOutputs>1) { buf[1][h]=os[1]; @@ -285,9 +335,8 @@ void DivPlatformOPL::acquire_nuked(short** buf, size_t len) { buf[3][h]=os[3]; } if (totalOutputs==6) { - // placeholder for OPL4 - buf[4][h]=0; - buf[5][h]=0; + buf[4][h]=os[4]; + buf[5][h]=os[5]; } } } @@ -505,6 +554,103 @@ void DivPlatformOPL::acquire_ymfm3(short** buf, size_t len) { } } +void DivPlatformOPL::acquire_ymfm4(short** buf, size_t len) { + ymfm::ymfm_output<6> out; + + ymfm::ymf278b::fm_engine* fme=fm_ymfm4->debug_fm_engine(); + ymfm::pcm_engine* pcme=fm_ymfm4->debug_pcm_engine(); + ymfm::fm_channel>* fmChan[18]; + ymfm::pcm_channel* pcmChan[24]; + + for (int i=0; i<18; i++) { + fmChan[i]=fme->debug_channel(i); + } + + for (int i=0; i<24; i++) { + pcmChan[i]=pcme->debug_channel(i); + } + + for (size_t h=0; hwrite((w.addr&0x200)?4:(w.addr&0x100)?2:0,w.addr); + fm_ymfm4->write((w.addr&0x200)?5:1,w.val); + + regPool[(w.addr&0x200)?(0x200+(w.addr&255)):(w.addr&511)]=w.val; + writes.pop(); + } + + fm_ymfm4->generate(&out,1); + + buf[0][h]=out.data[4]>>1; // FM + PCM left + if (totalOutputs>1) { + buf[1][h]=out.data[5]>>1; // FM + PCM right + } + if (totalOutputs>2) { + buf[2][h]=out.data[0]>>1; // FM left + } + if (totalOutputs>3) { + buf[3][h]=out.data[1]>>1; // FM right + } + if (totalOutputs==6) { + buf[4][h]=out.data[2]>>1; // PCM left + buf[5][h]=out.data[3]>>1; // PCM right + } + + if (properDrums) { + for (int i=0; i<16; i++) { + unsigned char ch=(i<12 && chan[i&(~1)].fourOp)?outChanMap[i^1]:outChanMap[i]; + if (ch==255) continue; + int chOut=fmChan[ch]->debug_output(0); + if (chOut==0) { + chOut=fmChan[ch]->debug_output(1); + } + if (chOut==0) { + chOut=fmChan[ch]->debug_output(2); + } + if (chOut==0) { + chOut=fmChan[ch]->debug_output(3); + } + if (i==15) { + oscBuf[i]->data[oscBuf[i]->needle++]=CLAMP(chOut,-32768,32767); + } else { + oscBuf[i]->data[oscBuf[i]->needle++]=CLAMP(chOut<<1,-32768,32767); + } + } + oscBuf[16]->data[oscBuf[16]->needle++]=CLAMP(fmChan[7]->debug_special2()<<1,-32768,32767); + oscBuf[17]->data[oscBuf[17]->needle++]=CLAMP(fmChan[8]->debug_special1()<<1,-32768,32767); + oscBuf[18]->data[oscBuf[18]->needle++]=CLAMP(fmChan[8]->debug_special2()<<1,-32768,32767); + oscBuf[19]->data[oscBuf[19]->needle++]=CLAMP(fmChan[7]->debug_special1()<<1,-32768,32767); + } else { + for (int i=0; i<18; i++) { + unsigned char ch=outChanMap[i]; + if (ch==255) continue; + int chOut=fmChan[ch]->debug_output(0); + if (chOut==0) { + chOut=fmChan[ch]->debug_output(1); + } + if (chOut==0) { + chOut=fmChan[ch]->debug_output(2); + } + if (chOut==0) { + chOut=fmChan[ch]->debug_output(3); + } + oscBuf[i]->data[oscBuf[i]->needle++]=CLAMP(chOut<<1,-32768,32767); + } + } + for (int i=0; i<24; i++) { + unsigned char oscOffs=i+pcmChanOffs; + int chOut=pcmChan[i]->debug_output(0); + chOut+=pcmChan[i]->debug_output(1); + chOut+=pcmChan[i]->debug_output(2); + chOut+=pcmChan[i]->debug_output(3); + oscBuf[oscOffs]->data[oscBuf[oscOffs]->needle++]=CLAMP(chOut<<1,-32768,32767); + } + } +} + static const int cycleMap[18]={ 6, 7, 8, 6, 7, 8, 0, 1, 2, 0, 1, 2, 3, 4, 5, 3, 4, 5, @@ -816,6 +962,9 @@ void DivPlatformOPL::acquire(short** buf, size_t len) { case 3: case 759: acquire_ymfm3(buf,len); break; + case 4: + acquire_ymfm4(buf,len); + break; } } else { // OPL3 acquire_nuked(buf,len); @@ -833,144 +982,230 @@ double DivPlatformOPL::NOTE_ADPCMB(int note) { void DivPlatformOPL::tick(bool sysTick) { for (int i=0; icalcArp(chan[i].note,chan[i].std.arp.val)); + } + chan[i].freqChanged=true; + } + + if (chan[i].std.pitch.had) { + if (chan[i].std.pitch.mode) { + chan[i].pitch2+=chan[i].std.pitch.val; + CLAMP_VAR(chan[i].pitch2,-131071,131071); + } else { + chan[i].pitch2=chan[i].std.pitch.val; + } + chan[i].freqChanged=true; + } + + if (chan[i].std.phaseReset.had) { + if (chan[i].std.phaseReset.val==1 && chan[i].active) { + chan[i].keyOn=true; + chan[i].writeCtrl=true; + } + } + + if (chan[i].std.panL.had) { // panning + chan[i].pan=chan[i].std.panL.val&0xf; + chan[i].freqChanged=true; + chan[i].writeCtrl=true; + } + + if (chan[i].std.ex1.had) { + chan[i].lfo=chan[i].std.ex1.val&0x7; + rWrite(PCM_ADDR_LFO_VIB+PCM_REG(i),(chan[i].lfo<<3)|(chan[i].vib)); + } + + if (chan[i].std.fms.had) { + chan[i].vib=chan[i].std.fms.val&0x7; + rWrite(PCM_ADDR_LFO_VIB+PCM_REG(i),(chan[i].lfo<<3)|(chan[i].vib)); + } + + if (chan[i].std.ams.had) { + chan[i].am=chan[i].std.ams.val&0x7; + rWrite(PCM_ADDR_AM+PCM_REG(i),chan[i].am); + } + + if (chan[i].std.ex2.had) { + chan[i].ar=chan[i].std.ex2.val&0xf; + rWrite(PCM_ADDR_AR_D1R+PCM_REG(i),(chan[i].ar<<4)|(chan[i].d1r)); + } + + if (chan[i].std.ex3.had) { + chan[i].d1r=chan[i].std.ex3.val&0xf; + rWrite(PCM_ADDR_AR_D1R+PCM_REG(i),(chan[i].ar<<4)|(chan[i].d1r)); + } + + if (chan[i].std.ex4.had) { + chan[i].dl=chan[i].std.ex4.val&0xf; + rWrite(PCM_ADDR_DL_D2R+PCM_REG(i),(chan[i].dl<<4)|(chan[i].d2r)); + } + + if (chan[i].std.ex5.had) { + chan[i].d2r=chan[i].std.ex5.val&0xf; + rWrite(PCM_ADDR_DL_D2R+PCM_REG(i),(chan[i].dl<<4)|(chan[i].d2r)); + } + + if (chan[i].std.ex6.had) { + chan[i].rc=chan[i].std.ex6.val&0xf; + rWrite(PCM_ADDR_RC_RR+PCM_REG(i),(chan[i].rc<<4)|(chan[i].rr)); + } + + if (chan[i].std.ex7.had) { + chan[i].rr=chan[i].std.ex7.val&0xf; + rWrite(PCM_ADDR_RC_RR+PCM_REG(i),(chan[i].rc<<4)|(chan[i].rr)); + } + + } else { + int ops=(slots[3][i]!=255 && chan[i].state.ops==4 && oplType==3)?4:2; + chan[i].std.next(); + + if (chan[i].std.vol.had) { + chan[i].outVol=VOL_SCALE_LOG_BROKEN(chan[i].vol,MIN(63,chan[i].std.vol.val),63); + for (int j=0; jmelodicChans) { + rWrite(baseAddr+ADDR_KSL_TL,(63-VOL_SCALE_LOG_BROKEN(63-op.tl,chan[i].outVol&0x3f,63))|(op.ksl<<6)); + } else { + rWrite(baseAddr+ADDR_KSL_TL,op.tl|(op.ksl<<6)); + } + } + } + } + + if (NEW_ARP_STRAT) { + chan[i].handleArp(); + } else if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + chan[i].baseFreq=NOTE_FREQUENCY(parent->calcArp(chan[i].note,chan[i].std.arp.val)); + } + chan[i].freqChanged=true; + } + + if (oplType==3 && chan[i].std.panL.had) { + chan[i].pan=((chan[i].std.panL.val&1)<<1)|((chan[i].std.panL.val&2)>>1)|((chan[i].std.panL.val&4)<<1)|((chan[i].std.panL.val&8)>>1); + } + + if (chan[i].std.pitch.had) { + if (chan[i].std.pitch.mode) { + chan[i].pitch2+=chan[i].std.pitch.val; + CLAMP_VAR(chan[i].pitch2,-131071,131071); + } else { + chan[i].pitch2=chan[i].std.pitch.val; + } + chan[i].freqChanged=true; + } + + if (chan[i].std.phaseReset.had) { + if (chan[i].std.phaseReset.val==1 && chan[i].active) { + chan[i].keyOn=true; + } + } + + if (chan[i].std.alg.had) { + chan[i].state.alg=chan[i].std.alg.val; + } + if (chan[i].std.fb.had) { + chan[i].state.fb=chan[i].std.fb.val; + } + + if (chan[i].std.alg.had || chan[i].std.fb.had || (oplType==3 && chan[i].std.panL.had)) { + if (isMuted[i] && i<=melodicChans) { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)); + } + } else { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)|((chan[i].pan&15)<<4)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)|((chan[i].pan&15)<<4)); + } + } + } - if (chan[i].std.vol.had) { - chan[i].outVol=VOL_SCALE_LOG_BROKEN(chan[i].vol,MIN(63,chan[i].std.vol.val),63); for (int j=0; jmelodicChans) { - rWrite(baseAddr+ADDR_KSL_TL,(63-VOL_SCALE_LOG_BROKEN(63-op.tl,chan[i].outVol&0x3f,63))|(op.ksl<<6)); - } else { - rWrite(baseAddr+ADDR_KSL_TL,op.tl|(op.ksl<<6)); + if (m.ar.had) { + op.ar=m.ar.val; + rWrite(baseAddr+ADDR_AR_DR,(op.ar<<4)|op.dr); + } + if (m.dr.had) { + op.dr=m.dr.val; + rWrite(baseAddr+ADDR_AR_DR,(op.ar<<4)|op.dr); + } + if (m.sl.had) { + op.sl=m.sl.val; + rWrite(baseAddr+ADDR_SL_RR,(op.sl<<4)|op.rr); + } + if (m.rr.had) { + op.rr=m.rr.val; + rWrite(baseAddr+ADDR_SL_RR,(op.sl<<4)|op.rr); + } + + if (oplType>1) { + if (m.ws.had) { + op.ws=m.ws.val; + rWrite(baseAddr+ADDR_WS,op.ws&((oplType==3)?7:3)); } } - } - } - if (NEW_ARP_STRAT) { - chan[i].handleArp(); - } else if (chan[i].std.arp.had) { - if (!chan[i].inPorta) { - chan[i].baseFreq=NOTE_FREQUENCY(parent->calcArp(chan[i].note,chan[i].std.arp.val)); - } - chan[i].freqChanged=true; - } - - if (oplType==3 && chan[i].std.panL.had) { - chan[i].pan=((chan[i].std.panL.val&1)<<1)|((chan[i].std.panL.val&2)>>1)|((chan[i].std.panL.val&4)<<1)|((chan[i].std.panL.val&8)>>1); - } - - if (chan[i].std.pitch.had) { - if (chan[i].std.pitch.mode) { - chan[i].pitch2+=chan[i].std.pitch.val; - CLAMP_VAR(chan[i].pitch2,-131071,131071); - } else { - chan[i].pitch2=chan[i].std.pitch.val; - } - chan[i].freqChanged=true; - } - - if (chan[i].std.phaseReset.had) { - if (chan[i].std.phaseReset.val==1 && chan[i].active) { - chan[i].keyOn=true; - } - } - - if (chan[i].std.alg.had) { - chan[i].state.alg=chan[i].std.alg.val; - } - if (chan[i].std.fb.had) { - chan[i].state.fb=chan[i].std.fb.val; - } - - if (chan[i].std.alg.had || chan[i].std.fb.had || (oplType==3 && chan[i].std.panL.had)) { - if (isMuted[i] && i<=melodicChans) { - rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)); - if (ops==4) { - rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)); + if (m.tl.had) { + op.tl=m.tl.val&63; } - } else { - rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)|((chan[i].pan&15)<<4)); - if (ops==4) { - rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)|((chan[i].pan&15)<<4)); + if (m.ksl.had) { + op.ksl=m.ksl.val; } - } - } - - for (int j=0; j1) { - if (m.ws.had) { - op.ws=m.ws.val; - rWrite(baseAddr+ADDR_WS,op.ws&((oplType==3)?7:3)); - } - } - - if (m.tl.had) { - op.tl=m.tl.val&63; - } - if (m.ksl.had) { - op.ksl=m.ksl.val; - } - if (m.tl.had || m.ksl.had) { - if (isMuted[i] && i<=melodicChans) { - rWrite(baseAddr+ADDR_KSL_TL,63|(op.ksl<<6)); - } else { - if (KVSL(i,j) || i>melodicChans) { - rWrite(baseAddr+ADDR_KSL_TL,(63-VOL_SCALE_LOG_BROKEN(63-op.tl,chan[i].outVol&0x3f,63))|(op.ksl<<6)); + if (m.tl.had || m.ksl.had) { + if (isMuted[i] && i<=melodicChans) { + rWrite(baseAddr+ADDR_KSL_TL,63|(op.ksl<<6)); } else { - rWrite(baseAddr+ADDR_KSL_TL,op.tl|(op.ksl<<6)); + if (KVSL(i,j) || i>melodicChans) { + rWrite(baseAddr+ADDR_KSL_TL,(63-VOL_SCALE_LOG_BROKEN(63-op.tl,chan[i].outVol&0x3f,63))|(op.ksl<<6)); + } else { + rWrite(baseAddr+ADDR_KSL_TL,op.tl|(op.ksl<<6)); + } } } } @@ -1090,7 +1325,7 @@ void DivPlatformOPL::tick(bool sysTick) { } } - for (int i=0; i<512; i++) { + for (int i=0; i<768; i++) { if (pendingWrites[i]!=oldWrites[i]) { if ((i>=0x80 && i<0xa0)) { if (weWillWriteRRLater[i-0x80]) continue; @@ -1104,38 +1339,118 @@ void DivPlatformOPL::tick(bool sysTick) { bool updateDrums=false; for (int i=0; icalcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,false,octave(chan[i].baseFreq)*2,chan[i].pitch2,chipClock,CHIP_FREQBASE); - if (chan[i].fixedFreq>0) chan[i].freq=chan[i].fixedFreq; - if (chan[i].freq<0) chan[i].freq=0; - if (chan[i].freq>131071) chan[i].freq=131071; - int freqt=toFreq(chan[i].freq); - chan[i].freqH=freqt>>8; - chan[i].freqL=freqt&0xff; - immWrite(chanMap[i]+ADDR_FREQ,chan[i].freqL); - } - if (igetSample(chan[i].sample); + unsigned char ctrl=0; + double off=(s->centerRate>=1)?((double)s->centerRate/8363.0):1.0; + chan[i].freq=(int)(off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,false,2,chan[i].pitch2,chipClock,(524288*768))); + if (chan[i].freq<0x400) chan[i].freq=0x400; + if (chan[i].freq>0x4000000) chan[i].freq=0x4000000; + if (chan[i].freq>=0x2000000) { + chan[i].freqH=15; + } else if (chan[i].freq>=0x1000000) { + chan[i].freqH=14; + } else if (chan[i].freq>=0x800000) { + chan[i].freqH=13; + } else if (chan[i].freq>=0x400000) { + chan[i].freqH=12; + } else if (chan[i].freq>=0x200000) { + chan[i].freqH=11; + } else if (chan[i].freq>=0x100000) { + chan[i].freqH=10; + } else if (chan[i].freq>=0x80000) { + chan[i].freqH=9; + } else if (chan[i].freq>=0x40000) { + chan[i].freqH=8; + } else if (chan[i].freq>=0x20000) { + chan[i].freqH=7; + } else if (chan[i].freq>=0x10000) { + chan[i].freqH=6; + } else if (chan[i].freq>=0x8000) { + chan[i].freqH=5; + } else if (chan[i].freq>=0x4000) { + chan[i].freqH=4; + } else if (chan[i].freq>=0x2000) { + chan[i].freqH=3; + } else if (chan[i].freq>=0x1000) { + chan[i].freqH=2; + } else if (chan[i].freq>=0x800) { + chan[i].freqH=1; } else { - immWrite(chanMap[i]+ADDR_FREQH,chan[i].freqH|(chan[i].active<<5)); + chan[i].freqH=0; + } + chan[i].freqL=(chan[i].freq>>chan[i].freqH)&0x3ff; + chan[i].freqH=8^chan[i].freqH; + ctrl|=(chan[i].active?0x80:0)|(chan[i].damp?0x40:0)|(chan[i].lfoReset?0x20:0)|(chan[i].ch?0x10:0)|(isMuted[i]?8:(chan[i].pan&0xf)); + unsigned int waveNum=chan[i].sample; + if (ramSize<=0x200000) { + waveNum=CLAMP(waveNum,0,0x7f)|0x180; + } + if (chan[i].keyOn) { + immWrite(PCM_ADDR_KEY_DAMP_LFORST_CH_PAN+PCM_REG(i),ctrl&~0x80); // force keyoff first + immWrite(PCM_ADDR_WAVE_H_FN_L+PCM_REG(i),((chan[i].freqL&0x7f)<<1)|((waveNum>>8)&1)); + immWrite(PCM_ADDR_WAVE_L+PCM_REG(i),waveNum&0xff); + immWrite(PCM_ADDR_LFO_VIB+PCM_REG(i),(chan[i].lfo<<3)|(chan[i].vib)); + immWrite(PCM_ADDR_AR_D1R+PCM_REG(i),(chan[i].ar<<4)|(chan[i].d1r)); + immWrite(PCM_ADDR_DL_D2R+PCM_REG(i),(chan[i].dl<<4)|(chan[i].d2r)); + immWrite(PCM_ADDR_RC_RR+PCM_REG(i),(chan[i].rc<<4)|(chan[i].rr)); + immWrite(PCM_ADDR_AM+PCM_REG(i),chan[i].am); + if (!chan[i].std.vol.had) { + chan[i].outVol=chan[i].vol; + immWrite(PCM_ADDR_TL+(PCM_REG(i)),((0x7f-chan[i].outVol)<<1)|(chan[i].levelDirect?1:0)); + } + chan[i].writeCtrl=true; + chan[i].keyOn=false; + } + if (chan[i].keyOff) { + chan[i].writeCtrl=true; + chan[i].keyOff=false; + } + if (chan[i].freqChanged) { + immWrite(PCM_ADDR_WAVE_H_FN_L+PCM_REG(i),((chan[i].freqL&0x7f)<<1)|((waveNum>>8)&1)); + immWrite(PCM_ADDR_FN_H_PR_OCT+PCM_REG(i),((chan[i].freqH&0xf)<<4)|(chan[i].pseudoReverb?0x08:0x00)|((chan[i].freqL>>7)&0x7)); + chan[i].freqChanged=false; + } + if (chan[i].writeCtrl) { + immWrite(PCM_ADDR_KEY_DAMP_LFORST_CH_PAN+PCM_REG(i),ctrl); + chan[i].writeCtrl=false; } } } else { - if (chan[i].keyOn) { - immWrite(chanMap[i]+ADDR_FREQH,chan[i].freqH); - if (!isMuted[i]) drumState|=(1<<(totalChans-i-1)); - updateDrums=true; - chan[i].keyOn=false; - } else if (chan[i].freqChanged) { - immWrite(chanMap[i]+ADDR_FREQH,chan[i].freqH); + if (chan[i].freqChanged) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,false,octave(chan[i].baseFreq)*2,chan[i].pitch2,chipClock,CHIP_FREQBASE); + if (chan[i].fixedFreq>0) chan[i].freq=chan[i].fixedFreq; + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>131071) chan[i].freq=131071; + int freqt=toFreq(chan[i].freq); + chan[i].freqH=freqt>>8; + chan[i].freqL=freqt&0xff; + immWrite(chanMap[i]+ADDR_FREQ,chan[i].freqL); } + if (imelodicChans && ins->type==DIV_INS_OPL_DRUMS) { for (int i=0; i<4; i++) { @@ -1369,7 +1692,62 @@ int DivPlatformOPL::dispatch(DivCommand c) { } switch (c.cmd) { case DIV_CMD_NOTE_ON: { - if (c.chan==adpcmChan) { // ADPCM + if (PCM_CHECK(c.chan)) { // OPL4 PCM + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_MULTIPCM); + chan[c.chan].macroVolMul=ins->type==DIV_INS_AMIGA?64:127; + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].sample=ins->amiga.getSample(c.value); + chan[c.chan].sampleNote=c.value; + c.value=ins->amiga.getFreq(c.value); + chan[c.chan].sampleNoteDelta=c.value-chan[c.chan].sampleNote; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PCM(c.value); + } + if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { + chan[c.chan].sample=-1; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + if (ins->type==DIV_INS_MULTIPCM) { + chan[c.chan].lfo=ins->multipcm.lfo; + chan[c.chan].vib=ins->multipcm.vib; + chan[c.chan].am=ins->multipcm.am; + chan[c.chan].ar=ins->multipcm.ar; + chan[c.chan].d1r=ins->multipcm.d1r; + chan[c.chan].dl=ins->multipcm.dl; + chan[c.chan].d2r=ins->multipcm.d2r; + chan[c.chan].rc=ins->multipcm.rc; + chan[c.chan].rr=ins->multipcm.rr; + chan[c.chan].damp=ins->multipcm.damp; + chan[c.chan].pseudoReverb=ins->multipcm.pseudoReverb; + chan[c.chan].levelDirect=ins->multipcm.levelDirect; + chan[c.chan].lfoReset=ins->multipcm.lfoReset; + } else { + chan[c.chan].lfo=0; + chan[c.chan].vib=0; + chan[c.chan].am=0; + chan[c.chan].ar=15; + chan[c.chan].d1r=15; + chan[c.chan].dl=0; + chan[c.chan].d2r=0; + chan[c.chan].rc=15; + chan[c.chan].rr=15; + chan[c.chan].damp=false; + chan[c.chan].pseudoReverb=false; + chan[c.chan].levelDirect=true; + chan[c.chan].lfoReset=false; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].macroInit(ins); + if (!chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + break; + } else if (c.chan==adpcmChan) { // ADPCM DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_FM); chan[c.chan].macroVolMul=ins->type==DIV_INS_AMIGA?64:255; if (ins->type==DIV_INS_AMIGA || ins->type==DIV_INS_ADPCMB) { @@ -1493,6 +1871,10 @@ int DivPlatformOPL::dispatch(DivCommand c) { chan[c.chan].keyOff=true; chan[c.chan].keyOn=false; chan[c.chan].active=false; + if (PCM_CHECK(c.chan)) { + chan[c.chan].sample=-1; + chan[c.chan].macroInit(NULL); + } break; case DIV_CMD_NOTE_OFF_ENV: chan[c.chan].keyOff=true; @@ -1513,6 +1895,10 @@ int DivPlatformOPL::dispatch(DivCommand c) { if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } + if (PCM_CHECK(c.chan)) { // OPL4 PCM + immWrite(PCM_ADDR_TL+PCM_REG(c.chan),((0x7f-chan[c.chan].outVol)<<1)|(chan[c.chan].levelDirect?1:0)); + break; + } if (c.chan==adpcmChan) { // ADPCM-B immWrite(18,(isMuted[adpcmChan]?0:chan[adpcmChan].outVol)); break; @@ -1550,10 +1936,17 @@ int DivPlatformOPL::dispatch(DivCommand c) { chan[c.chan].ins=c.value; break; case DIV_CMD_PANNING: { + if (PCM_CHECK(c.chan)) { + chan[c.chan].ch=false; + chan[c.chan].pan=8^MIN(parent->convertPanSplitToLinearLR(c.value,c.value2,15)+1,15); + chan[c.chan].freqChanged=true; + chan[c.chan].writeCtrl=true; + break; + } if (oplType!=3) break; if (c.chan==adpcmChan) break; chan[c.chan].pan&=~3; - if (c.value==0 && c.value2==0 && compatPan) { + if (c.value==0 && c.value2==0 && ((chipType!=4) && compatPan)) { chan[c.chan].pan|=3; } else { chan[c.chan].pan|=(c.value>0)|((c.value2>0)<<1); @@ -1573,6 +1966,12 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_SURROUND_PANNING: { + if (PCM_CHECK(c.chan)) { + chan[c.chan].ch=true; + chan[c.chan].freqChanged=true; + chan[c.chan].writeCtrl=true; + break; + } if (oplType!=3) break; if (c.chan==adpcmChan) break; @@ -1608,6 +2007,29 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_NOTE_PORTA: { + if (PCM_CHECK(c.chan)) { + int destFreq=NOTE_PCM(c.value2+chan[c.chan].sampleNoteDelta); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } int destFreq=(c.chan==adpcmChan)?(NOTE_ADPCMB(c.value2)):(NOTE_FREQUENCY(c.value2)); int newFreq; bool return2=false; @@ -1648,17 +2070,20 @@ int DivPlatformOPL::dispatch(DivCommand c) { iface.sampleBank=sampleBank; break; case DIV_CMD_LEGATO: { + // TODO: OPL4 PCM if (chan[c.chan].insChanged) { DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_FM); commitState(c.chan,ins); chan[c.chan].insChanged=false; } - chan[c.chan].baseFreq=(c.chan==adpcmChan)?(NOTE_ADPCMB(c.value)):(NOTE_FREQUENCY(c.value)); + chan[c.chan].baseFreq=(PCM_CHECK(c.chan))?NOTE_PCM(c.value+chan[c.chan].sampleNoteDelta+((HACKY_LEGATO_MESS)?(chan[c.chan].std.arp.val-12):(0))): + (c.chan==adpcmChan)?(NOTE_ADPCMB(c.value)):(NOTE_FREQUENCY(c.value)); chan[c.chan].note=c.value; chan[c.chan].freqChanged=true; break; } case DIV_CMD_FM_LFO: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; if (c.value&2) { dvb=c.value&1; @@ -1669,6 +2094,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_FB: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; chan[c.chan].state.fb=c.value&7; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; @@ -1686,6 +2112,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_MULT: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value>=ops) break; @@ -1698,6 +2125,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_TL: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value>=ops) break; @@ -1718,6 +2146,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_AR: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value<0) { @@ -1741,6 +2170,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_DR: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value<0) { @@ -1764,6 +2194,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_SL: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value<0) { @@ -1787,6 +2218,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_RR: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value<0) { @@ -1810,6 +2242,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_AM: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value<0) { @@ -1833,6 +2266,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_VIB: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value<0) { @@ -1856,6 +2290,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_SUS: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value<0) { @@ -1879,6 +2314,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_KSR: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; if (c.value<0) { @@ -1902,6 +2338,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_WS: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; if (oplType<2) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; @@ -1926,6 +2363,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_RS: { + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; if (oplType<2) break; int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; @@ -1974,6 +2412,10 @@ int DivPlatformOPL::dispatch(DivCommand c) { chanMap=properDrums?chanMapOPL3Drums:chanMapOPL3; melodicChans=properDrums?15:18; totalChans=properDrums?20:18; + if (chipType==4) { + pcmChanOffs=totalChans; + totalChans+=24; + } } else { chanMap=properDrums?chanMapOPL2Drums:chanMapOPL2; melodicChans=properDrums?6:9; @@ -1982,9 +2424,103 @@ int DivPlatformOPL::dispatch(DivCommand c) { break; } case DIV_CMD_FM_HARD_RESET: + if (PCM_CHECK(c.chan)) break; if (c.chan==adpcmChan) break; chan[c.chan].hardReset=c.value; break; + case DIV_CMD_MULTIPCM_MIX_FM: + if (chipType==4) { + fmMixL=CLAMP((c.value&0x70)>>4,0,7); + fmMixR=CLAMP((c.value&0x7),0,7); + immWrite(PCM_ADDR_MIX_FM,((7-fmMixR)<<3)|(7-fmMixL)); + } + break; + case DIV_CMD_MULTIPCM_MIX_PCM: + if (chipType==4) { + pcmMixL=CLAMP((c.value&0x70)>>4,0,7); + pcmMixR=CLAMP((c.value&0x7),0,7); + immWrite(PCM_ADDR_MIX_PCM,((7-pcmMixR)<<3)|(7-pcmMixL)); + } + break; + case DIV_CMD_MULTIPCM_LFO: + if (PCM_CHECK(c.chan)) { + chan[c.chan].lfo=c.value&7; + rWrite(PCM_ADDR_LFO_VIB+PCM_REG(c.chan),(chan[c.chan].lfo<<3)|(chan[c.chan].vib)); + } + break; + case DIV_CMD_MULTIPCM_VIB: + if (PCM_CHECK(c.chan)) { + chan[c.chan].vib=c.value&7; + rWrite(PCM_ADDR_LFO_VIB+PCM_REG(c.chan),(chan[c.chan].lfo<<3)|(chan[c.chan].vib)); + } + break; + case DIV_CMD_MULTIPCM_AM: + if (PCM_CHECK(c.chan)) { + chan[c.chan].am=c.value&7; + rWrite(PCM_ADDR_AM+PCM_REG(c.chan),chan[c.chan].am); + } + break; + case DIV_CMD_MULTIPCM_AR: + if (PCM_CHECK(c.chan)) { + chan[c.chan].ar=c.value&0xf; + rWrite(PCM_ADDR_AR_D1R+PCM_REG(c.chan),(chan[c.chan].ar<<4)|(chan[c.chan].d1r)); + } + break; + case DIV_CMD_MULTIPCM_D1R: + if (PCM_CHECK(c.chan)) { + chan[c.chan].d1r=c.value&0xf; + rWrite(PCM_ADDR_AR_D1R+PCM_REG(c.chan),(chan[c.chan].ar<<4)|(chan[c.chan].d1r)); + } + break; + case DIV_CMD_MULTIPCM_DL: + if (PCM_CHECK(c.chan)) { + chan[c.chan].dl=c.value&0xf; + rWrite(PCM_ADDR_DL_D2R+PCM_REG(c.chan),(chan[c.chan].dl<<4)|(chan[c.chan].d2r)); + } + break; + case DIV_CMD_MULTIPCM_D2R: + if (PCM_CHECK(c.chan)) { + chan[c.chan].d2r=c.value&0xf; + rWrite(PCM_ADDR_DL_D2R+PCM_REG(c.chan),(chan[c.chan].dl<<4)|(chan[c.chan].d2r)); + } + break; + case DIV_CMD_MULTIPCM_RC: + if (PCM_CHECK(c.chan)) { + chan[c.chan].rc=c.value&0xf; + rWrite(PCM_ADDR_RC_RR+PCM_REG(c.chan),(chan[c.chan].rc<<4)|(chan[c.chan].rr)); + } + break; + case DIV_CMD_MULTIPCM_RR: + if (PCM_CHECK(c.chan)) { + chan[c.chan].rr=c.value&0xf; + rWrite(PCM_ADDR_RC_RR+PCM_REG(c.chan),(chan[c.chan].rc<<4)|(chan[c.chan].rr)); + } + break; + case DIV_CMD_MULTIPCM_DAMP: + if (PCM_CHECK(c.chan)) { + chan[c.chan].damp=c.value&1; + chan[c.chan].freqChanged=true; + chan[c.chan].writeCtrl=true; + } + break; + case DIV_CMD_MULTIPCM_PSEUDO_REVERB: + if (PCM_CHECK(c.chan)) { + chan[c.chan].pseudoReverb=c.value&1; + chan[c.chan].freqChanged=true; + } + break; + case DIV_CMD_MULTIPCM_LFO_RESET: + if (PCM_CHECK(c.chan)) { + chan[c.chan].lfoReset=c.value&1; + chan[c.chan].freqChanged=true; + chan[c.chan].writeCtrl=true; + } + break; + case DIV_CMD_MULTIPCM_LEVEL_DIRECT: + if (PCM_CHECK(c.chan)) { + immWrite(PCM_ADDR_TL+PCM_REG(c.chan),((0x7f-chan[c.chan].outVol)<<1)|(chan[c.chan].levelDirect?1:0)); + } + break; case DIV_CMD_MACRO_OFF: chan[c.chan].std.mask(c.value,true); break; @@ -1995,13 +2531,18 @@ int DivPlatformOPL::dispatch(DivCommand c) { chan[c.chan].std.restart(c.value); break; case DIV_CMD_GET_VOLMAX: + if (PCM_CHECK(c.chan)) return 127; if (c.chan==adpcmChan) return 255; if (pretendYMU) return 127; return 63; break; case DIV_CMD_PRE_PORTA: + if (PCM_CHECK(c.chan) && chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_MULTIPCM)); + } if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will && !NEW_ARP_STRAT) { - chan[c.chan].baseFreq=(c.chan==adpcmChan)?(NOTE_ADPCMB(chan[c.chan].note)):(NOTE_FREQUENCY(chan[c.chan].note)); + chan[c.chan].baseFreq=(PCM_CHECK(c.chan))?NOTE_PCM(chan[c.chan].note): + ((c.chan==adpcmChan)?(NOTE_ADPCMB(chan[c.chan].note)):(NOTE_FREQUENCY(chan[c.chan].note))); } chan[c.chan].inPorta=c.value; break; @@ -2019,6 +2560,10 @@ void DivPlatformOPL::forceIns() { chanMap=properDrums?chanMapOPL3Drums:chanMapOPL3; melodicChans=properDrums?15:18; totalChans=properDrums?20:18; + if (chipType==4) { + pcmChanOffs=totalChans; + totalChans+=24; + } } else { chanMap=properDrums?chanMapOPL2Drums:chanMapOPL2; melodicChans=properDrums?6:9; @@ -2067,7 +2612,7 @@ void DivPlatformOPL::forceIns() { } */ } - for (int i=0; i<512; i++) { + for (int i=0; i<768; i++) { oldWrites[i]=-1; } immWrite(0xbd,(dam<<7)|(dvb<<6)|(properDrums<<5)|drumState); @@ -2088,6 +2633,9 @@ DivMacroInt* DivPlatformOPL::getChanMacroInt(int ch) { unsigned short DivPlatformOPL::getPan(int ch) { if (totalOutputs<=1) return 0; + if (PCM_CHECK(ch)) { + return parent->convertPanLinearToSplit(8^chan[ch].pan,8,15); + } /*if (chan[ch&(~1)].fourOp) { if (ch&1) { return ((chan[ch-1].pan&2)<<7)|(chan[ch-1].pan&1); @@ -2125,6 +2673,19 @@ DivDispatchOscBuffer* DivPlatformOPL::getOscBuffer(int ch) { } int DivPlatformOPL::mapVelocity(int ch, float vel) { + if (PCM_CHECK(ch)) { // TODO: correct? + // -0.375dB per step + // -6: 64: 16 + // -12: 32: 32 + // -18: 16: 48 + // -24: 8: 64 + // -30: 4: 80 + // -36: 2: 96 + // -42: 1: 112 + if (vel==0) return 0; + if (vel>=1.0) return 127; + return CLAMP(round(128.0-(112.0-log2(vel*127.0)*16.0)),0,127); + } if (ch==adpcmChan) return vel*255.0; // -0.75dB per step // -6: 64: 8 @@ -2141,6 +2702,8 @@ int DivPlatformOPL::mapVelocity(int ch, float vel) { float DivPlatformOPL::getGain(int ch, int vol) { if (vol==0) return 0; + if (PCM_CHECK(ch)) return 1.0/pow(10.0,(float)(127-vol)*0.375/20.0); + if (ch==adpcmChan) return (float)vol/255.0; return 1.0/pow(10.0,(float)(63-vol)*0.75/20.0); } @@ -2149,12 +2712,12 @@ unsigned char* DivPlatformOPL::getRegisterPool() { } int DivPlatformOPL::getRegisterPoolSize() { - return (oplType<3)?256:512; + return (chipType==4)?768:((oplType<3)?256:512); } void DivPlatformOPL::reset() { while (!writes.empty()) writes.pop(); - memset(regPool,0,512); + memset(regPool,0,768); dacVal=0; dacVal2=0; @@ -2208,6 +2771,9 @@ void DivPlatformOPL::reset() { case 3: case 759: fm_ymfm3->reset(); break; + case 4: + fm_ymfm4->reset(); + break; } } else { if (downsample) { @@ -2216,6 +2782,7 @@ void DivPlatformOPL::reset() { OPL3_Reset(&fm,rate); } } + pcm.reset(); if (dumpWrites) { addWrite(0xffffffff,0); @@ -2227,6 +2794,10 @@ void DivPlatformOPL::reset() { outChanMap=outChanMapOPL3; melodicChans=properDrums?15:18; totalChans=properDrums?20:18; + if (chipType==4) { + pcmChanOffs=totalChans; + totalChans+=24; + } } else { chanMap=properDrums?chanMapOPL2Drums:chanMapOPL2; outChanMap=outChanMapOPL2; @@ -2237,8 +2808,9 @@ void DivPlatformOPL::reset() { for (int i=0; i=0) { @@ -2260,7 +2832,7 @@ void DivPlatformOPL::reset() { fm.channel[outChanMap[i]].muted=isMuted[i]; } - for (int i=0; i<512; i++) { + for (int i=0; i<768; i++) { oldWrites[i]=-1; pendingWrites[i]=-1; } @@ -2281,7 +2853,20 @@ void DivPlatformOPL::reset() { } if (oplType==3) { // enable OPL3 features - immWrite(0x105,1); + if (chipType==4) { + immWrite(0x105,3); + // Reset wavetable header + immWrite(0x202,(ramSize<=0x200000)?0x10:0x00); + // initialize mixer volume + fmMixL=7; + fmMixR=7; + pcmMixL=7; + pcmMixR=7; + immWrite(PCM_ADDR_MIX_FM,((7-fmMixR)<<3)|(7-fmMixL)); + immWrite(PCM_ADDR_MIX_PCM,((7-pcmMixR)<<3)|(7-pcmMixL)); + } else { + immWrite(0x105,1); + } } update4OpMask=true; @@ -2372,6 +2957,8 @@ void DivPlatformOPL::setOPLType(int type, bool drums) { pretendYMU=true; adpcmChan=16; } else if (type==4) { + pcmChanOffs=totalChans; + totalChans+=24; chipFreqBase=32768*684; downsample=true; } @@ -2497,18 +3084,43 @@ void DivPlatformOPL::setFlags(const DivConfig& flags) { case 4: switch (flags.getInt("clockSel",0)) { case 0x01: - chipClock=COLOR_PAL*32.0/5.0; + chipClock=COLOR_NTSC*8.0; break; case 0x02: - chipClock=33868800.0; + chipClock=COLOR_PAL*32.0/5.0; break; default: - chipClock=COLOR_NTSC*8.0; + chipClock=33868800.0; + break; + } + switch (flags.getInt("ramSize",0)) { + case 0x01: // 2MB (512KB 512KB 512KB 512KB) + ramSize=0x200000; + break; + case 0x02: // 1MB (512KB 512KB) + ramSize=0x100000; + break; + case 0x03: // 640KB (512KB 128KB) + ramSize=0xa0000; + break; + case 0x04: // 512KB + ramSize=0x80000; + break; + case 0x05: // 256KB (128KB 128KB) + ramSize=0x40000; + break; + case 0x06: // 128KB + ramSize=0x20000; + break; + default: + ramSize=0x400000; break; } CHECK_CUSTOM_CLOCK; + pcm.setClockFrequency(chipClock); rate=chipClock/768; chipRateBase=chipClock/684; + immWrite(0x202,(ramSize<=0x200000)?0x10:0x00); break; case 759: rate=48000; @@ -2518,21 +3130,25 @@ void DivPlatformOPL::setFlags(const DivConfig& flags) { } compatPan=flags.getBool("compatPan",false); - for (int i=0; i<20; i++) { + for (int i=0; i<44; i++) { oscBuf[i]->rate=rate; } } const void* DivPlatformOPL::getSampleMem(int index) { - return (index==0 && adpcmChan>=0) ? adpcmBMem : NULL; + return (index==0 && pcmChanOffs>=0)?pcmMem: + (index==0 && adpcmChan>=0)?adpcmBMem:NULL; } size_t DivPlatformOPL::getSampleMemCapacity(int index) { - return (index==0 && adpcmChan>=0) ? 262144 : 0; + return (index==0 && pcmChanOffs>=0)? + ((ramSize<=0x200000)?0x200000+ramSize:ramSize): + ((index==0 && adpcmChan>=0)?262144:0); } size_t DivPlatformOPL::getSampleMemUsage(int index) { - return (index==0 && adpcmChan>=0) ? adpcmBMemLen : 0; + return (index==0 && pcmChanOffs>=0)?pcmMemLen: + (index==0 && adpcmChan>=0)?adpcmBMemLen:0; } bool DivPlatformOPL::isSampleLoaded(int index, int sample) { @@ -2542,50 +3158,147 @@ bool DivPlatformOPL::isSampleLoaded(int index, int sample) { } const DivMemoryComposition* DivPlatformOPL::getMemCompo(int index) { - if (adpcmChan<0) return NULL; + if ((adpcmChan<0) && (pcmChanOffs<0)) return NULL; if (index!=0) return NULL; return &memCompo; } void DivPlatformOPL::renderSamples(int sysID) { - if (adpcmChan<0) return; - memset(adpcmBMem,0,getSampleMemCapacity(0)); + if (adpcmChan<0 && pcmChanOffs<0) return; + if (adpcmChan>=0 && adpcmBMem!=NULL) { + memset(adpcmBMem,0,262144); + } + if (pcmChanOffs>=0 && pcmMem!=NULL) { + memset(pcmMem,0,4194304); + } + memset(sampleOffPCM,0,256*sizeof(unsigned int)); memset(sampleOffB,0,256*sizeof(unsigned int)); memset(sampleLoaded,0,256*sizeof(bool)); memCompo=DivMemoryComposition(); memCompo.name="Sample Memory"; - size_t memPos=0; - for (int i=0; isong.sampleLen; i++) { - DivSample* s=parent->song.sample[i]; - if (!s->renderOn[0][sysID]) { - sampleOffB[i]=0; - continue; - } + if (pcmChanOffs>=0) { // OPL4 PCM + size_t memPos=((ramSize<=0x200000)?0x200600:0x1800); + const int maxSample=(ramSize<=0x200000)?127:511; + int sampleCount=parent->song.sampleLen; + if (sampleCount>maxSample) sampleCount=maxSample; + for (int i=0; isong.sample[i]; + if (!s->renderOn[0][sysID]) { + sampleOffPCM[i]=0; + continue; + } - int paddedLen=(s->lengthB+255)&(~0xff); - if ((memPos&0xf00000)!=((memPos+paddedLen)&0xf00000)) { - memPos=(memPos+0xfffff)&0xf00000; - } - if (memPos>=getSampleMemCapacity(0)) { - logW("out of ADPCM memory for sample %d!",i); - break; - } - if (memPos+paddedLen>=getSampleMemCapacity(0)) { - memcpy(adpcmBMem+memPos,s->dataB,getSampleMemCapacity(0)-memPos); - logW("out of ADPCM memory for sample %d!",i); - } else { - memcpy(adpcmBMem+memPos,s->dataB,paddedLen); + int length; + switch (s->depth) { + default: + case DIV_SAMPLE_DEPTH_8BIT: + length=MIN(65535,s->getLoopEndPosition(DIV_SAMPLE_DEPTH_8BIT)); + break; + case DIV_SAMPLE_DEPTH_12BIT: + length=MIN(98303,s->getLoopEndPosition(DIV_SAMPLE_DEPTH_12BIT)); + break; + case DIV_SAMPLE_DEPTH_16BIT: + length=MIN(131070,s->getLoopEndPosition(DIV_SAMPLE_DEPTH_16BIT)); + break; + } + unsigned char* src=(unsigned char*)s->getCurBuf(); + int actualLength=MIN((int)(getSampleMemCapacity(0)-memPos),length); + if (actualLength>0) { + #ifdef TA_BIG_ENDIAN + memcpy(&pcmMem[memPos],src,actualLength); + #else + if (s->depth==DIV_SAMPLE_DEPTH_16BIT) { + for (int i=0; isong.sample[i]; + unsigned int insAddr=(i*12)+((ramSize<=0x200000)?0x200000:0); + unsigned char bitDepth; + int endPos=CLAMP(s->loopEnd,1,0x10000); + int loop=s->isLoopable()?CLAMP(s->loopStart,0,endPos-1):(endPos-1); + switch (s->depth) { + default: + case DIV_SAMPLE_DEPTH_8BIT: + bitDepth=0; + break; + case DIV_SAMPLE_DEPTH_12BIT: + bitDepth=1; + break; + case DIV_SAMPLE_DEPTH_16BIT: + bitDepth=2; + break; + } + pcmMem[insAddr]=(bitDepth<<6)|((sampleOffPCM[i]>>16)&0x3f); + pcmMem[1+insAddr]=(sampleOffPCM[i]>>8)&0xff; + pcmMem[2+insAddr]=(sampleOffPCM[i])&0xff; + pcmMem[3+insAddr]=(loop>>8)&0xff; + pcmMem[4+insAddr]=(loop)&0xff; + pcmMem[5+insAddr]=((~(endPos-1))>>8)&0xff; + pcmMem[6+insAddr]=(~(endPos-1))&0xff; + // TODO: how to fill in rest of instrument table? + pcmMem[7+insAddr]=0; // LFO, VIB + pcmMem[8+insAddr]=(0xf << 4) | (0xf << 0); // AR, D1R + pcmMem[9+insAddr]=0; // DL, D2R + pcmMem[10+insAddr]=(0xf << 4) | (0xf << 0); // RC, RR + pcmMem[11+insAddr]=0; // AM + } + if (ramSize<=0x200000) { + memCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_RESERVED,"ROM data",0,0,0x200000)); + } + + memCompo.used=pcmMemLen; + } else if (adpcmChan>=0) { // ADPCM + size_t memPos=0; + for (int i=0; isong.sampleLen; i++) { + DivSample* s=parent->song.sample[i]; + if (!s->renderOn[0][sysID]) { + sampleOffB[i]=0; + continue; + } + + int paddedLen=(s->lengthB+255)&(~0xff); + if ((memPos&0xf00000)!=((memPos+paddedLen)&0xf00000)) { + memPos=(memPos+0xfffff)&0xf00000; + } + if (memPos>=getSampleMemCapacity(0)) { + logW("out of ADPCM memory for sample %d!",i); + break; + } + if (memPos+paddedLen>=getSampleMemCapacity(0)) { + memcpy(adpcmBMem+memPos,s->dataB,getSampleMemCapacity(0)-memPos); + logW("out of ADPCM memory for sample %d!",i); + } else { + memcpy(adpcmBMem+memPos,s->dataB,paddedLen); + sampleLoaded[i]=true; + } + sampleOffB[i]=memPos; + memCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_SAMPLE,"Sample",i,memPos,memPos+paddedLen)); + memPos+=paddedLen; + } + adpcmBMemLen=memPos+256; + + memCompo.used=adpcmBMemLen; + } memCompo.capacity=getSampleMemCapacity(0); } @@ -2593,10 +3306,10 @@ int DivPlatformOPL::init(DivEngine* p, int channels, int sugRate, const DivConfi parent=p; dumpWrites=false; skipRegisterWrites=false; - for (int i=0; i<20; i++) { + for (int i=0; i<44; i++) { isMuted[i]=false; } - for (int i=0; i<20; i++) { + for (int i=0; i<44; i++) { oscBuf[i]=new DivDispatchOscBuffer; } @@ -2604,6 +3317,7 @@ int DivPlatformOPL::init(DivEngine* p, int channels, int sugRate, const DivConfi fm_ymfm2=NULL; fm_ymfm8950=NULL; fm_ymfm3=NULL; + fm_ymfm4=NULL; if (emuCore==1) { switch (chipType) { @@ -2619,31 +3333,45 @@ int DivPlatformOPL::init(DivEngine* p, int channels, int sugRate, const DivConfi case 3: case 759: fm_ymfm3=new ymfm::ymf262(iface); break; + case 4: + fm_ymfm4=new ymfm::ymf278b(iface); + break; } } setFlags(flags); if (adpcmChan>=0) { - adpcmBMem=new unsigned char[getSampleMemCapacity(0)]; + adpcmBMem=new unsigned char[262144]; adpcmBMemLen=0; iface.adpcmBMem=adpcmBMem; iface.sampleBank=0; adpcmB=new ymfm::adpcm_b_engine(iface,2); } + if (pcmChanOffs>=0) { + pcmMem=new unsigned char[4194304]; + pcmMemLen=0; + iface.pcmMem=pcmMem; + iface.sampleBank=0; + pcmMemory.memory=pcmMem; + } + reset(); return totalChans; } void DivPlatformOPL::quit() { - for (int i=0; i<20; i++) { + for (int i=0; i<44; i++) { delete oscBuf[i]; } if (adpcmChan>=0) { delete adpcmB; delete[] adpcmBMem; } + if (pcmChanOffs>=0) { + delete[] pcmMem; + } if (fm_ymfm1!=NULL) { delete fm_ymfm1; fm_ymfm1=NULL; @@ -2660,6 +3388,10 @@ void DivPlatformOPL::quit() { delete fm_ymfm3; fm_ymfm3=NULL; } + if (fm_ymfm4!=NULL) { + delete fm_ymfm4; + fm_ymfm4=NULL; + } } DivPlatformOPL::~DivPlatformOPL() { diff --git a/src/engine/platform/opl.h b/src/engine/platform/opl.h index 5f4134796..7a6826c5a 100644 --- a/src/engine/platform/opl.h +++ b/src/engine/platform/opl.h @@ -29,24 +29,41 @@ extern "C" { } #include "sound/ymfm/ymfm_adpcm.h" #include "sound/ymfm/ymfm_opl.h" +#include "sound/ymfm/ymfm_pcm.h" +#include "sound/ymf278b/ymf278.h" class DivOPLAInterface: public ymfm::ymfm_interface { public: unsigned char* adpcmBMem; + unsigned char* pcmMem; int sampleBank; uint8_t ymfm_external_read(ymfm::access_class type, uint32_t address); void ymfm_external_write(ymfm::access_class type, uint32_t address, uint8_t data); - DivOPLAInterface(): adpcmBMem(NULL), sampleBank(0) {} + DivOPLAInterface(): adpcmBMem(NULL), pcmMem(NULL), sampleBank(0) {} +}; + +class DivYMF278MemoryInterface: public MemoryInterface { + public: + unsigned char* memory; + DivYMF278MemoryInterface(unsigned size_) : memory(NULL), size(size_) {}; + byte operator[](unsigned address) const override; + unsigned getSize() const override { return size; }; + void write(unsigned address, byte value) override {}; + void clear(byte value) override {}; + private: + unsigned size; }; class DivPlatformOPL: public DivDispatch { protected: struct Channel: public SharedChannel { DivInstrumentFM state; - unsigned char freqH, freqL; + unsigned int freqH, freqL; int sample, fixedFreq; - bool furnacePCM, fourOp, hardReset; - unsigned char pan; + bool furnacePCM, fourOp, hardReset, writeCtrl; + bool levelDirect, damp, pseudoReverb, lfoReset, ch; + int lfo, vib, am, ar, d1r, d2r, dl, rc, rr; + int pan; int macroVolMul; Channel(): SharedChannel(0), @@ -57,14 +74,29 @@ class DivPlatformOPL: public DivDispatch { furnacePCM(false), fourOp(false), hardReset(false), + writeCtrl(false), + levelDirect(true), + damp(false), + pseudoReverb(false), + lfoReset(false), + ch(false), + lfo(0), + vib(0), + am(0), + ar(15), + d1r(15), + d2r(0), + dl(0), + rc(15), + rr(15), pan(3), macroVolMul(64) { state.ops=2; } }; - Channel chan[20]; - DivDispatchOscBuffer* oscBuf[20]; - bool isMuted[20]; + Channel chan[44]; + DivDispatchOscBuffer* oscBuf[44]; + bool isMuted[44]; struct QueuedWrite { unsigned short addr; unsigned char val; @@ -72,7 +104,7 @@ class DivPlatformOPL: public DivDispatch { QueuedWrite(): addr(0), val(0), addrOrVal(false) {} QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v), addrOrVal(false) {} }; - FixedQueue writes; + FixedQueue writes; unsigned int dacVal; unsigned int dacVal2; @@ -86,8 +118,12 @@ class DivPlatformOPL: public DivDispatch { unsigned char* adpcmBMem; size_t adpcmBMemLen; + unsigned char* pcmMem; + size_t pcmMemLen; DivOPLAInterface iface; + DivYMF278MemoryInterface pcmMemory; unsigned int sampleOffB[256]; + unsigned int sampleOffPCM[256]; bool sampleLoaded[256]; ymfm::adpcm_b_engine* adpcmB; @@ -97,12 +133,13 @@ class DivPlatformOPL: public DivDispatch { const unsigned short* chanMap; const unsigned char* outChanMap; int chipFreqBase, chipRateBase; - int delay, chipType, oplType, chans, melodicChans, totalChans, adpcmChan, sampleBank, totalOutputs; + int delay, chipType, oplType, chans, melodicChans, totalChans, adpcmChan=-1, pcmChanOffs=-1, sampleBank, totalOutputs, ramSize; + int fmMixL=7, fmMixR=7, pcmMixL=7, pcmMixR=7; unsigned char lastBusy; unsigned char drumState; unsigned char drumVol[5]; - unsigned char regPool[512]; + unsigned char regPool[768]; bool properDrums, properDrumsSys, dam, dvb; @@ -115,15 +152,17 @@ class DivPlatformOPL: public DivDispatch { bool update4OpMask, pretendYMU, downsample, compatPan; - short oldWrites[512]; - short pendingWrites[512]; + short oldWrites[768]; + short pendingWrites[768]; // chips opl3_chip fm; + YMF278 pcm; ymfm::ym3526* fm_ymfm1; ymfm::ym3812* fm_ymfm2; ymfm::y8950* fm_ymfm8950; ymfm::ymf262* fm_ymfm3; + ymfm::ymf278b* fm_ymfm4; fmopl2_t fm_lle2; fmopl3_t fm_lle3; @@ -141,6 +180,7 @@ class DivPlatformOPL: public DivDispatch { void acquire_nukedLLE3(short** buf, size_t len); void acquire_nuked(short** buf, size_t len); void acquire_ymfm3(short** buf, size_t len); + void acquire_ymfm4(short** buf, size_t len); void acquire_ymfm8950(short** buf, size_t len); void acquire_ymfm2(short** buf, size_t len); void acquire_ymfm1(short** buf, size_t len); @@ -182,6 +222,9 @@ class DivPlatformOPL: public DivDispatch { void renderSamples(int chipID); int init(DivEngine* parent, int channels, int sugRate, const DivConfig& flags); void quit(); + DivPlatformOPL(): + pcmMemory(0x400000), + pcm(pcmMemory) {} ~DivPlatformOPL(); }; #endif diff --git a/src/engine/platform/oplAInterface.cpp b/src/engine/platform/oplAInterface.cpp index 9643b80c0..dcd5c8e85 100644 --- a/src/engine/platform/oplAInterface.cpp +++ b/src/engine/platform/oplAInterface.cpp @@ -28,6 +28,11 @@ uint8_t DivOPLAInterface::ymfm_external_read(ymfm::access_class type, uint32_t a return 0; } return adpcmBMem[address&0x3ffff]; + case ymfm::ACCESS_PCM: + if (pcmMem==NULL) { + return 0; + } + return pcmMem[address&0x3fffff]; default: return 0; } @@ -36,3 +41,10 @@ uint8_t DivOPLAInterface::ymfm_external_read(ymfm::access_class type, uint32_t a void DivOPLAInterface::ymfm_external_write(ymfm::access_class type, uint32_t address, uint8_t data) { } + +byte DivYMF278MemoryInterface::operator[](unsigned address) const { + if (memory && address writes; diff --git a/src/engine/platform/sound/su.cpp b/src/engine/platform/sound/su.cpp index 8d5373cef..505a0d48b 100644 --- a/src/engine/platform/sound/su.cpp +++ b/src/engine/platform/sound/su.cpp @@ -146,7 +146,8 @@ void SoundUnit::NextSample(short* l, short* r) { } } } - fns[i]=ns[i]*chan[i].vol*((chan[i].flags0&8)?4:2); + fns[i]=ns[i]*chan[i].vol; + if (!(chan[i].flags0&8)) fns[i]>>=1; if ((chan[i].flags0&0xe0)!=0) { int ff=chan[i].cutoff; nslow[i]=nslow[i]+(((ff)*nsband[i])>>16); @@ -259,12 +260,12 @@ void SoundUnit::NextSample(short* l, short* r) { // mix if (dsOut) { - tnsL=nsL[dsChannel]<<1; - tnsR=nsR[dsChannel]<<1; + tnsL=nsL[dsChannel]<<3; + tnsR=nsR[dsChannel]<<3; dsChannel=(dsChannel+1)&7; } else { - tnsL=(nsL[0]+nsL[1]+nsL[2]+nsL[3]+nsL[4]+nsL[5]+nsL[6]+nsL[7])>>2; - tnsR=(nsR[0]+nsR[1]+nsR[2]+nsR[3]+nsR[4]+nsR[5]+nsR[6]+nsR[7])>>2; + tnsL=(nsL[0]+nsL[1]+nsL[2]+nsL[3]+nsL[4]+nsL[5]+nsL[6]+nsL[7]); + tnsR=(nsR[0]+nsR[1]+nsR[2]+nsR[3]+nsR[4]+nsR[5]+nsR[6]+nsR[7]); IL1=minval(32767,maxval(-32767,tnsL))>>8; IL2=minval(32767,maxval(-32767,tnsR))>>8; diff --git a/src/engine/platform/sound/su.h b/src/engine/platform/sound/su.h index 8e8009568..091f7de73 100644 --- a/src/engine/platform/sound/su.h +++ b/src/engine/platform/sound/su.h @@ -33,12 +33,12 @@ class SoundUnit { int rcycle[8]; unsigned int lfsr[8]; signed char ns[8]; - int fns[8]; - int nsL[8]; - int nsR[8]; - int nslow[8]; - int nshigh[8]; - int nsband[8]; + short fns[8]; + short nsL[8]; + short nsR[8]; + short nslow[8]; + short nshigh[8]; + short nsband[8]; int tnsL, tnsR; unsigned char ilBufPeriod; unsigned short ilBufPos; diff --git a/src/engine/platform/sound/supervision.c b/src/engine/platform/sound/supervision.c new file mode 100644 index 000000000..c042ba313 --- /dev/null +++ b/src/engine/platform/sound/supervision.c @@ -0,0 +1,282 @@ +// THIS IS A MODIFIED VERSION OF POTATOR'S SOUND EMULATION CORE +// MODIFIED BY AART1256 IN 2024 + +#include "supervision.h" + +#include + +#define SV_SAMPLE_RATE ((svision->UNSCALED_CLOCK)/64) +#define SV_DEC_TICK ((SV_SAMPLE_RATE)/60) + +void supervision_sound_set_clock(struct svision_t *svision, uint32 clock) { + svision->UNSCALED_CLOCK = clock; +} + +void supervision_memorymap_registers_write(struct svision_t *svision, uint32 Addr, uint8 Value) +{ + switch (Addr & 0x1fff) { + case 0x10: case 0x11: case 0x12: case 0x13: + case 0x14: case 0x15: case 0x16: case 0x17: + supervision_sound_wave_write(svision, ((Addr & 0x4) >> 2), Addr & 3, Value); + break; + case 0x18: + case 0x19: + case 0x1a: + case 0x1b: + case 0x1c: + supervision_sound_dma_write(svision, Addr & 0x07, Value); + break; + case 0x28: + case 0x29: + case 0x2a: + supervision_sound_noise_write(svision, Addr & 0x07, Value); + break; + } +} + +void supervision_set_mute_mask(struct svision_t *svision, uint8 mask) { + svision->ch_mask = mask; +} + +void supervision_sound_set_flags(struct svision_t *svision, uint8 flags_set) +{ + svision->flags = flags_set; +} + +void supervision_sound_reset(struct svision_t *svision) +{ + memset(svision->m_channel, 0, sizeof(svision->m_channel)); + memset(&svision->m_noise, 0, sizeof(svision->m_noise) ); + memset(&svision->m_dma, 0, sizeof(svision->m_dma) ); + + memset(svision->ch, 0, sizeof(svision->ch) ); + svision->decrement_tick = 0; + svision->ch_mask = 15; +} + +void supervision_sound_stream_update(struct svision_t *svision, uint8 *stream, uint32 len) +{ + size_t i, j; + SVISION_CHANNEL *channel; + uint8 s = 0; + uint8 *left = stream + 0; + uint8 *right = stream + 1; + uint8 *chout = stream + 2; + + for (i = 0; i < len >> 1; i++, left += 2, right += 2) { + *left = *right = 0; + for (channel = svision->m_channel, j = 0; j < 2; j++, channel++) { + chout[j] = 0; + if (svision->ch[j].size != 0) { + if (svision->ch[j].on || channel->count != 0) { + BOOL on = 0; + switch (svision->ch[j].waveform) { + case 0: // 12.5% + on = svision->ch[j].pos < (28 * svision->ch[j].size) >> 5; + break; + case 1: // 25% + on = svision->ch[j].pos < (24 * svision->ch[j].size) >> 5; + break; + case 2: // 50% + on = svision->ch[j].pos < svision->ch[j].size / 2; + break; + case 3: // 75% + on = svision->ch[j].pos < svision->ch[j].size / 4; + // MESS/MAME: <= (9 * svision->ch[j].size) >> 5; + break; + } + s = on ? (svision->ch[j].volume)<<2 : 0; + s = ((svision->ch_mask>>(3-j))&1)?s:0; + if (svision->flags&1) { + if (j == 0) + *right += s; + else + *left += s; + } else { + *left += s; + *right += s; + } + chout[j] = s; + } + svision->ch[j].pos++; + if (svision->ch[j].pos >= svision->ch[j].size) { + svision->ch[j].pos = 0; + // Transition from off to on + if (channel->on) { + memcpy(&svision->ch[j], channel, sizeof(svision->ch[j])); + channel->on = 0; + } + } + } + } + + if (svision->m_noise.on && (svision->m_noise.play || svision->m_noise.count != 0)) { + s = (svision->m_noise.value * svision->m_noise.volume) << 2; + s = svision->ch_mask&1?s:0; + chout[3] = 0; + if (svision->m_noise.left) { + *left += s; + chout[3] = s; + } + if (svision->m_noise.right) { + *right += s; + chout[3] = s; + } + svision->m_noise.pos += svision->m_noise.step; + while (svision->m_noise.pos >= 1.0) { // if/while difference - Pacific Battle + // LFSR: x^2 + x + 1 + uint16 feedback; + svision->m_noise.value = svision->m_noise.state & 1; + feedback = ((svision->m_noise.state >> 1) ^ svision->m_noise.state) & 0x0001; + feedback <<= svision->m_noise.type; + svision->m_noise.state = (svision->m_noise.state >> 1) | feedback; + svision->m_noise.pos -= 1.0; + } + } + + chout[2] = 0; + if (svision->m_dma.on) { + uint8 sample; + uint16 addr = svision->m_dma.start + (uint16)svision->m_dma.pos / 2; + if (addr >= 0x8000 && addr < 0xc000) { + sample = svision->supervision_dma_mem[(addr & 0x3fff) | svision->m_dma.ca14to16]; + } + if (((uint16)svision->m_dma.pos) & 1) + s = (sample & 0xf); + else + s = (sample & 0xf0) >> 4; + s <<= 2; + s = ((svision->ch_mask>>1)&1)?s:0; + chout[2] = 0; + if (svision->m_dma.left) { + *left += s; + chout[2] = s; + } + if (svision->m_dma.right) { + *right += s; + chout[2] = s; + } + svision->m_dma.pos += svision->m_dma.step; + if (svision->m_dma.pos >= svision->m_dma.size) { + svision->m_dma.on = 0; + } + } + + if (svision->decrement_tick > SV_DEC_TICK) { + svision->decrement_tick = 0; + supervision_sound_decrement(svision); + } + svision->decrement_tick++; + } +} + +void supervision_sound_decrement(struct svision_t *svision) +{ + if (svision->m_channel[0].count > 0) + svision->m_channel[0].count--; + if (svision->m_channel[1].count > 0) + svision->m_channel[1].count--; + if (svision->m_noise.count > 0) + svision->m_noise.count--; +} + +void supervision_sound_wave_write(struct svision_t *svision, int which, int offset, uint8 data) +{ + SVISION_CHANNEL *channel = &svision->m_channel[which]; + + channel->reg[offset] = data; + switch (offset) { + case 0: + case 1: { + uint16 size; + size = channel->reg[0] | ((channel->reg[1] & 7) << 8); + // if size == 0 then channel->size == 0 + if (size) + channel->size = (uint16)(((real)SV_SAMPLE_RATE) * ((real)((size + 1) << 5)) / ((real)svision->UNSCALED_CLOCK)); + else + channel->size = 0; + channel->pos = 0; + // Popo Team + if (channel->count != 0 || svision->ch[which].size == 0 || channel->size == 0) { + svision->ch[which].size = channel->size; + if (channel->count == 0) + svision->ch[which].pos = 0; + } + } + break; + case 2: + channel->on = data & 0x40; + channel->waveform = (data & 0x30) >> 4; + channel->volume = data & 0x0f; + if (!channel->on || svision->ch[which].size == 0 || channel->size == 0) { + uint16 pos = svision->ch[which].pos; + memcpy(&svision->ch[which], channel, sizeof(svision->ch[which])); + if (channel->count != 0) // Journey to the West + svision->ch[which].pos = pos; + } + break; + case 3: + channel->count = data + 1; + svision->ch[which].size = channel->size; // Sonny Xpress! + break; + } +} + +void supervision_sound_dma_write(struct svision_t *svision, int offset, uint8 data) +{ + svision->m_dma.reg[offset] = data; + switch (offset) { + case 0: + case 1: + svision->m_dma.start = (svision->m_dma.reg[0] | (svision->m_dma.reg[1] << 8)); + break; + case 2: + svision->m_dma.size = (data ? data : 0x100) * 32; // Number of 4-bit samples + break; + case 3: + // Test games: Classic Casino, SSSnake + svision->m_dma.step = ((real)svision->UNSCALED_CLOCK) / ((real)SV_SAMPLE_RATE * (256 << (data & 3))); + // MESS/MAME. Wrong + //svision->m_dma.step = svision->UNSCALED_CLOCK / (256.0 * SV_SAMPLE_RATE * (1 + (data & 3))); + svision->m_dma.right = data & 4; + svision->m_dma.left = data & 8; + svision->m_dma.ca14to16 = ((data & 0x70) >> 4) << 14; + break; + case 4: + svision->m_dma.on = data & 0x80; + if (svision->m_dma.on) { + svision->m_dma.pos = 0.0; + } + break; + } +} + +void supervision_sound_noise_write(struct svision_t *svision, int offset, uint8 data) +{ + svision->m_noise.reg[offset] = data; + switch (offset) { + case 0: { + uint32 divisor = 8 << (data >> 4); + if (divisor) + svision->m_noise.step = ((real)svision->UNSCALED_CLOCK) / ((real)SV_SAMPLE_RATE * divisor); + else + svision->m_noise.step = 0; + + svision->m_noise.step = ((real)svision->UNSCALED_CLOCK) / ((real)SV_SAMPLE_RATE * divisor); + svision->m_noise.volume = data & 0xf; + } + break; + case 1: + svision->m_noise.count = data + 1; + break; + case 2: + svision->m_noise.type = (data & 1) ? 14 : 6; + svision->m_noise.play = data & 2; + svision->m_noise.right = data & 4; + svision->m_noise.left = data & 8; + svision->m_noise.on = data & 0x10; /* honey bee start */ + svision->m_noise.state = 1; + break; + } + svision->m_noise.pos = 0.0; +} diff --git a/src/engine/platform/sound/supervision.h b/src/engine/platform/sound/supervision.h new file mode 100644 index 000000000..0c3cfec00 --- /dev/null +++ b/src/engine/platform/sound/supervision.h @@ -0,0 +1,76 @@ +#ifndef __SUPERVISION_SOUND_H__ +#define __SUPERVISION_SOUND_H__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + +typedef uint8_t uint8; +typedef uint16_t uint16; +typedef uint32_t uint32; +typedef float real; +typedef uint8_t BOOL; + + +typedef struct { + uint8 reg[4]; + int on; + uint8 waveform, volume; + uint16 pos, size; + uint16 count; +} SVISION_CHANNEL; + +typedef struct { + uint8 reg[3]; + int on, right, left, play; + uint8 type; // 6 - 7-Bit, 14 - 15-Bit + uint16 state; + uint8 value, volume; + uint16 count; + real pos, step; +} SVISION_NOISE; + +typedef struct { + uint8 reg[5]; + int on, right, left; + uint32 ca14to16; + uint16 start; + uint16 size; + real pos, step; +} SVISION_DMA; + +struct svision_t { + SVISION_CHANNEL m_channel[2]; + // For clear sound (no grating), sync with m_channel + SVISION_CHANNEL ch[2]; + SVISION_NOISE m_noise; + SVISION_DMA m_dma; + uint8 supervision_dma_mem[65536]; + uint32 decrement_tick; + uint32 UNSCALED_CLOCK; + uint8 ch_mask, flags; +}; + +void supervision_sound_reset(struct svision_t *svision); +void supervision_sound_set_clock(struct svision_t *svision, uint32 clock); +void supervision_sound_stream_update(struct svision_t *svision, uint8 *stream, uint32 len); +void supervision_sound_decrement(struct svision_t *svision); +void supervision_sound_wave_write(struct svision_t *svision, int which, int offset, uint8 data); +void supervision_sound_dma_write(struct svision_t *svision,int offset, uint8 data); +void supervision_sound_noise_write(struct svision_t *svision, int offset, uint8 data); +void supervision_sound_noise_write(struct svision_t *svision, int offset, uint8 data); +void supervision_memorymap_registers_write(struct svision_t *svision, uint32 Addr, uint8 Value); +// 12SN +void supervision_set_mute_mask(struct svision_t *svision, uint8 mask); +void supervision_sound_set_flags(struct svision_t *svision, uint8 flags_set); + + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif + diff --git a/src/engine/platform/sound/upd1771c.c b/src/engine/platform/sound/upd1771c.c new file mode 100644 index 000000000..0ed1c9f5e --- /dev/null +++ b/src/engine/platform/sound/upd1771c.c @@ -0,0 +1,300 @@ +// SOME CODE IS TAKEN FROM MAME'S EMULATION OF THE UPD1771C +// MADE BY AART1256 IN 2024 + +#include "upd1771c.h" + +#include + +/* + Each of the 8 waveforms have been extracted from the uPD1771c-017 internal + ROM, from offset 0x1fd (start of first waveform) to offset 0x2fc (end of + last waveform). + (note: given test mode dumping offset non-clarity it may be 0x200-0x2ff) + The waveforms are stored in an 8-bit sign-magnitude format, so if in ROM the + upper bit is 0x80, invert the lower 7 bits to get the 2's complement result + seen here. + Note that only the last 4 waveforms appear to have been intended for use as + waveforms; the first four look as if they're playing back a piece of code as + wave data. +*/ +const signed char WAVEFORMS[8][32]={ +{ 0, 0,-123,-123, -61, -23, 125, 107, 94, 83,-128,-128,-128, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0,-128,-128,-128, 0, 0, 0, 0, 0, 0}, +{ 37, 16, 32, -21, 32, 52, 4, 4, 33, 18, 60, 56, 0, 8, 5, 16, 65, 19, 69, 16, -2, 19, 37, 16, 97, 19, 0, 87, 127, -3, 1, 2}, +{ 0, 8, 1, 52, 4, 0, 0, 77, 81,-109, 47, 97, -83,-109, 38, 97, 0, 52, 4, 0, 1, 4, 1, 22, 2, -46, 33, 97, 0, 8, -85, -99}, +{ 47, 97, 40, 97, -3, 25, 64, 17, 0, 52, 12, 5, 12, 5, 12, 5, 12, 5, 12, 5, 8, 4,-114, 19, 0, 52,-122, 21, 2, 5, 0, 8}, +{ -52, -96,-118,-128,-111, -74, -37, -5, 31, 62, 89, 112, 127, 125, 115, 93, 57, 23, 0, -16, -8, 15, 37, 54, 65, 70, 62, 54, 43, 31, 19, 0}, +{ -81,-128, -61, 13, 65, 93, 127, 47, 41, 44, 52, 55, 56, 58, 58, 34, 0, 68, 76, 72, 61, 108, 55, 29, 32, 39, 43, 49, 50, 51, 51, 0}, +{ -21, -45, -67, -88,-105,-114,-122,-128,-123,-116,-103, -87, -70, -53, -28, -9, 22, 46, 67, 86, 102, 114, 123, 125, 127, 117, 104, 91, 72, 51, 28, 0}, +{ -78,-118,-128,-102, -54, -3, 40, 65, 84, 88, 84, 80, 82, 88, 94, 103, 110, 119, 122, 125, 122, 122, 121, 123, 125, 126, 127, 127, 125, 118, 82, 0} +}; + + +#define NOISE_SIZE 255 + + +/* +const unsigned char noise_tbl[]= +{ + 0x1c,0x86,0x8a,0x8f,0x98,0xa1,0xad,0xbe,0xd9,0x8a,0x66,0x4d,0x40,0x33,0x2b,0x23, + 0x1e,0x8a,0x90,0x97,0xa4,0xae,0xb8,0xd6,0xec,0xe9,0x69,0x4a,0x3e,0x34,0x2d,0x27, + 0x24,0x24,0x89,0x8e,0x93,0x9c,0xa5,0xb0,0xc1,0xdd,0x40,0x36,0x30,0x29,0x27,0x24, + 0x8b,0x90,0x96,0x9e,0xa7,0xb3,0xc4,0xe1,0x25,0x21,0x8a,0x8f,0x93,0x9d,0xa5,0xb2, + 0xc2,0xdd,0xdd,0x98,0xa2,0xaf,0xbf,0xd8,0xfd,0x65,0x4a,0x3c,0x31,0x2b,0x24,0x22, + 0x1e,0x87,0x8c,0x91,0x9a,0xa3,0xaf,0xc0,0xdb,0xbe,0xd9,0x8c,0x66,0x4d,0x40,0x34, + 0x2c,0x24,0x1f,0x88,0x90,0x9a,0xa4,0xb2,0xc2,0xda,0xff,0x67,0x4d,0x3d,0x34,0x2d, + 0x26,0x24,0x20,0x89,0x8e,0x93,0x9c,0xa5,0xb1,0xc2,0xde,0xc1,0xda,0xff,0x67,0x4d, + 0x3d,0x33,0x2d,0x26,0x24,0x20,0x89,0x8e,0x93,0x9c,0xa5,0xb1,0xc2,0xdd,0xa3,0xb0, + 0xc0,0xd9,0xfe,0x66,0x4b,0x3c,0x32,0x2b,0x24,0x23,0x1e,0x88,0x8d,0x92,0x9b,0xa4, + 0xb0,0xc1,0xdc,0xad,0xbe,0xda,0x22,0x20,0x1c,0x85,0x8a,0x8f,0x98,0xa1,0xad,0xbe, + 0xda,0x20,0x1b,0x85,0x8d,0x97,0xa1,0xaf,0xbf,0xd8,0xfd,0x64,0x49,0x3a,0x30,0x2a, + 0x23,0x21,0x1d,0x86,0x8b,0x91,0x9a,0xa2,0xae,0xc0,0xdb,0x33,0x2b,0x24,0x1f,0x88, + 0x90,0x9a,0xa4,0xb2,0xc2,0xda,0xff,0x67,0x4c,0x3e,0x33,0x2d,0x25,0x24,0x1f,0x89, + 0x8e,0x93,0x9c,0xa5,0xb1,0xc2,0xde,0x85,0x8e,0x98,0xa2,0xb0,0xc0,0xd9,0xfe,0x64, + 0x4b,0x3b,0x31,0x2a,0x23,0x22,0x1e,0x88,0x8c,0x91,0x9b,0xa3,0xaf,0xc1,0xdc,0xdc +}; +*/ + +const unsigned char noise_tbl[8][256] = { +{ + 0x84, 0x87, 0x8c, 0x93, 0x9a, 0xa4, 0xb2, 0xc8, 0x8a, 0x6c, 0x58, 0x4e, 0x43, 0x3d, 0x37, 0x33, + 0x86, 0x8d, 0x95, 0x9d, 0xa8, 0xb5, 0xc9, 0xe7, 0x6d, 0x58, 0x4b, 0x43, 0x3e, 0x38, 0x37, 0x34, + 0x87, 0x8b, 0x8f, 0x96, 0x9d, 0xa7, 0xb5, 0xcb, 0x4d, 0x45, 0x40, 0x3a, 0x39, 0x35, 0x89, 0x8d, + 0x91, 0x98, 0x9f, 0xa9, 0xb7, 0xcd, 0x37, 0x34, 0x87, 0x8c, 0x90, 0x97, 0x9e, 0xa8, 0xb5, 0xcb, + 0xca, 0x93, 0x9b, 0xa6, 0xb3, 0xc7, 0xe5, 0x6b, 0x56, 0x49, 0x41, 0x3c, 0x36, 0x35, 0x32, 0x86, + 0x89, 0x8e, 0x95, 0x9c, 0xa6, 0xb4, 0xc9, 0xb2, 0xc8, 0x8a, 0x6c, 0x58, 0x4d, 0x43, 0x3d, 0x37, + 0x33, 0x86, 0x8d, 0x95, 0x9d, 0xa8, 0xb5, 0xc9, 0xe7, 0x6d, 0x58, 0x4b, 0x43, 0x3e, 0x38, 0x37, + 0x34, 0x87, 0x8b, 0x8f, 0x97, 0x9e, 0xa8, 0xb5, 0xcb, 0xb5, 0xc9, 0xe6, 0x6d, 0x58, 0x4b, 0x43, + 0x3d, 0x38, 0x37, 0x33, 0x87, 0x8b, 0x8f, 0x96, 0x9e, 0xa7, 0xb5, 0xcb, 0x9c, 0xa7, 0xb3, 0xc8, + 0xe6, 0x6c, 0x56, 0x4a, 0x42, 0x3d, 0x37, 0x36, 0x33, 0x86, 0x8a, 0x8e, 0x95, 0x9c, 0xa6, 0xb4, + 0xca, 0xa4, 0xb2, 0xc8, 0x35, 0x33, 0x30, 0x84, 0x88, 0x8c, 0x93, 0x9a, 0xa5, 0xb2, 0xc8, 0x34, + 0x30, 0x84, 0x8a, 0x93, 0x9a, 0xa5, 0xb2, 0xc6, 0xe4, 0x6a, 0x55, 0x49, 0x41, 0x3b, 0x36, 0x34, + 0x31, 0x85, 0x89, 0x8d, 0x94, 0x9b, 0xa5, 0xb3, 0xc9, 0x43, 0x3d, 0x37, 0x33, 0x86, 0x8d, 0x95, + 0x9d, 0xa8, 0xb4, 0xc9, 0xe7, 0x6d, 0x57, 0x4b, 0x43, 0x3e, 0x38, 0x37, 0x33, 0x87, 0x8b, 0x8f, + 0x96, 0x9d, 0xa7, 0xb5, 0xcb, 0x85, 0x8b, 0x93, 0x9b, 0xa6, 0xb3, 0xc7, 0xe5, 0x6b, 0x56, 0x4a, + 0x41, 0x3c, 0x36, 0x35, 0x32, 0x86, 0x89, 0x8e, 0x95, 0x9c, 0xa6, 0xb4, 0xca, 0x30 +}, + +{ + 0x95, 0x95, 0x98, 0x9c, 0x9e, 0xa3, 0xaa, 0xb4, 0xb3, 0x98, 0x9c, 0xa1, 0xa7, 0xb1, 0xc0, 0x57, + 0x4f, 0x49, 0x48, 0x44, 0x42, 0x40, 0x40, 0x94, 0x95, 0x96, 0x9a, 0x9d, 0xa2, 0xa9, 0xb3, 0xa8, + 0xb1, 0x66, 0x59, 0x50, 0x4c, 0x4a, 0x46, 0x44, 0x41, 0x94, 0x97, 0x9b, 0x9e, 0xa4, 0xaa, 0xb3, + 0xc2, 0x59, 0x52, 0x4c, 0x4a, 0x46, 0x45, 0x43, 0x43, 0x96, 0x97, 0x99, 0x9d, 0x9f, 0xa5, 0xab, + 0xb5, 0xaa, 0xb4, 0xc2, 0x5a, 0x52, 0x4c, 0x4b, 0x47, 0x45, 0x43, 0x43, 0x96, 0x97, 0x99, 0x9d, + 0xa0, 0xa5, 0xab, 0xb5, 0x9d, 0xa3, 0xa9, 0xb3, 0xc2, 0x59, 0x51, 0x4b, 0x4a, 0x46, 0x44, 0x42, + 0x42, 0x96, 0x97, 0x98, 0x9c, 0x9f, 0xa4, 0xab, 0xb4, 0xa2, 0xa9, 0xb3, 0x41, 0x40, 0x40, 0x93, + 0x94, 0x96, 0x9a, 0x9d, 0xa2, 0xa8, 0xb2, 0x41, 0x3e, 0x92, 0x94, 0x99, 0x9b, 0xa2, 0xa7, 0xb1, + 0xc0, 0x57, 0x4f, 0x4a, 0x48, 0x44, 0x42, 0x40, 0x40, 0x94, 0x95, 0x97, 0x9a, 0x9d, 0xa2, 0xa9, + 0xb3, 0x48, 0x45, 0x43, 0x41, 0x94, 0x97, 0x9b, 0x9d, 0xa3, 0xa9, 0xb3, 0xc1, 0x59, 0x51, 0x4b, + 0x4a, 0x45, 0x44, 0x42, 0x42, 0x95, 0x97, 0x98, 0x9c, 0x9f, 0xa4, 0xab, 0xb4, 0x92, 0x95, 0x99, + 0x9c, 0xa2, 0xa8, 0xb1, 0xc0, 0x57, 0x4f, 0x4a, 0x48, 0x44, 0x42, 0x41, 0x41, 0x94, 0x95, 0x97, + 0x9b, 0x9e, 0xa3, 0xa9, 0xb3, 0x3e, 0x92, 0x93, 0x95, 0x98, 0x9b, 0xa0, 0xa7, 0xb1, 0x65, 0x58, + 0x50, 0x4b, 0x49, 0x45, 0x43, 0x41, 0x94, 0x97, 0x9b, 0x9e, 0xa4, 0xa9, 0xb3, 0xc2, 0x59, 0x51, + 0x4b, 0x4a, 0x46, 0x45, 0x42, 0x42, 0x96, 0x96, 0x99, 0x9c, 0x9f, 0xa4, 0xab, 0xb5, 0x4d, 0x4b, + 0x47, 0x45, 0x43, 0x43, 0x97, 0x97, 0x9a, 0x9d, 0xa0, 0xa5, 0xac, 0xb6, 0x41, 0x42 + +}, + +{ + 0x93, 0x93, 0x93, 0x94, 0x95, 0x99, 0x9c, 0xa1, 0xa8, 0x4e, 0x48, 0x45, 0x43, 0x44, 0x42, 0x42, + 0x95, 0x95, 0x95, 0x96, 0x97, 0x98, 0x9b, 0x9e, 0xa6, 0x43, 0x41, 0x41, 0x95, 0x94, 0x94, 0x95, + 0x97, 0x9b, 0x9d, 0xa3, 0xa9, 0x4f, 0x4a, 0x46, 0x45, 0x45, 0x43, 0x43, 0x97, 0x96, 0x96, 0x97, + 0x99, 0x9a, 0x9c, 0x9f, 0xa7, 0x94, 0x93, 0x94, 0x96, 0x9a, 0x9d, 0xa2, 0xa9, 0x4f, 0x49, 0x45, + 0x44, 0x45, 0x43, 0x43, 0x96, 0x96, 0x96, 0x96, 0x98, 0x99, 0x9c, 0x9f, 0xa7, 0x93, 0x93, 0x93, + 0x93, 0x95, 0x96, 0x99, 0x9c, 0xa4, 0x50, 0x4b, 0x49, 0x45, 0x44, 0x42, 0x42, 0x95, 0x95, 0x95, + 0x96, 0x97, 0x9b, 0x9e, 0xa3, 0xaa, 0x50, 0x4b, 0x46, 0x46, 0x46, 0x44, 0x44, 0x97, 0x97, 0x97, + 0x97, 0x99, 0x9a, 0x9d, 0x9f, 0xa8, 0x47, 0x46, 0x46, 0x44, 0x44, 0x98, 0x98, 0x97, 0x98, 0x9a, + 0x9b, 0x9d, 0xa0, 0xa8, 0x42, 0x96, 0x95, 0x95, 0x96, 0x98, 0x98, 0x9b, 0x9e, 0xa6, 0xa6, 0x93, + 0x95, 0x99, 0x9c, 0xa1, 0xa7, 0x4e, 0x48, 0x44, 0x43, 0x44, 0x41, 0x42, 0x95, 0x95, 0x95, 0x95, + 0x97, 0x98, 0x9b, 0x9e, 0xa6, 0x9d, 0xa5, 0x52, 0x4c, 0x4b, 0x46, 0x45, 0x42, 0x43, 0x97, 0x96, + 0x96, 0x97, 0x98, 0x9c, 0x9f, 0xa4, 0xab, 0x51, 0x4b, 0x48, 0x46, 0x47, 0x44, 0x45, 0x98, 0x98, + 0x98, 0x98, 0x9a, 0x9b, 0x9e, 0xa0, 0xa8, 0x9f, 0xa4, 0xab, 0x51, 0x4c, 0x48, 0x47, 0x47, 0x45, + 0x45, 0x99, 0x98, 0x98, 0x99, 0x9a, 0x9b, 0x9e, 0xa1, 0xa9, 0x98, 0x9d, 0x9f, 0xa4, 0xab, 0x51, + 0x4b, 0x47, 0x47, 0x47, 0x45, 0x45, 0x98, 0x98, 0x98, 0x98, 0x9a, 0x9b, 0x9e, 0xa1, 0xa8, 0x9c, + 0x9f, 0xa7, 0x42, 0x42, 0x95, 0x95, 0x95, 0x96, 0x97, 0x98, 0x9b, 0x9e, 0xa6, 0x3f + +}, + +{ + 0x94, 0x93, 0x92, 0x92, 0x93, 0x95, 0x96, 0x99, 0x9c, 0x43, 0x43, 0x43, 0x41, 0x41, 0x42, 0x95, + 0x95, 0x95, 0x94, 0x94, 0x94, 0x95, 0x97, 0x97, 0x9a, 0x40, 0x40, 0x41, 0x95, 0x95, 0x94, 0x95, + 0x95, 0x97, 0x97, 0x9a, 0x9e, 0x45, 0x44, 0x44, 0x42, 0x42, 0x43, 0x97, 0x96, 0x96, 0x96, 0x96, + 0x95, 0x96, 0x98, 0x99, 0x9c, 0x94, 0x94, 0x94, 0x94, 0x96, 0x97, 0x9a, 0x9d, 0x44, 0x43, 0x44, + 0x42, 0x42, 0x43, 0x96, 0x96, 0x96, 0x95, 0x95, 0x95, 0x96, 0x98, 0x98, 0x9b, 0x93, 0x93, 0x93, + 0x92, 0x92, 0x93, 0x95, 0x95, 0x98, 0x49, 0x44, 0x43, 0x44, 0x42, 0x42, 0x43, 0x96, 0x96, 0x96, + 0x95, 0x96, 0x98, 0x99, 0x9b, 0x9f, 0x46, 0x45, 0x45, 0x43, 0x44, 0x44, 0x98, 0x98, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x99, 0x9a, 0x9d, 0x45, 0x43, 0x43, 0x44, 0x98, 0x98, 0x98, 0x97, 0x97, 0x97, + 0x97, 0x99, 0x9a, 0x9d, 0x95, 0x95, 0x94, 0x94, 0x94, 0x94, 0x94, 0x96, 0x97, 0x9a, 0x9a, 0x92, + 0x93, 0x94, 0x95, 0x98, 0x9c, 0x43, 0x42, 0x43, 0x40, 0x40, 0x41, 0x95, 0x95, 0x94, 0x94, 0x94, + 0x93, 0x94, 0x96, 0x97, 0x9a, 0x96, 0x99, 0x4a, 0x45, 0x44, 0x44, 0x42, 0x42, 0x43, 0x97, 0x96, + 0x96, 0x96, 0x97, 0x98, 0x99, 0x9c, 0xa0, 0x47, 0x46, 0x46, 0x44, 0x44, 0x45, 0x98, 0x98, 0x98, + 0x98, 0x97, 0x97, 0x98, 0x99, 0x9a, 0x9d, 0x9a, 0x9c, 0xa0, 0x47, 0x46, 0x46, 0x44, 0x44, 0x45, + 0x99, 0x98, 0x98, 0x98, 0x97, 0x97, 0x98, 0x9a, 0x9a, 0x9e, 0x97, 0x98, 0x99, 0x9c, 0xa0, 0x47, + 0x46, 0x46, 0x44, 0x44, 0x45, 0x98, 0x98, 0x98, 0x98, 0x98, 0x97, 0x98, 0x9a, 0x9a, 0x9d, 0x98, + 0x99, 0x9c, 0x42, 0x95, 0x95, 0x95, 0x94, 0x94, 0x94, 0x95, 0x96, 0x97, 0x9a, 0x40 + +}, + +{ + 0xe6, 0x1c, 0x87, 0xcb, 0x42, 0x9b, 0x87, 0x35, 0xd8, 0x1c, 0xa3, 0x58, 0x84, 0xa4, 0x28, 0x71, + 0xd9, 0x3e, 0x82, 0xbb, 0x28, 0xd2, 0x80, 0x63, 0xe0, 0x38, 0x71, 0xcb, 0x22, 0x81, 0x9b, 0x83, + 0xa4, 0x28, 0x71, 0xd8, 0x3e, 0x81, 0xbb, 0x28, 0xd2, 0x80, 0x63, 0xe0, 0x37, 0x71, 0xcb, 0x22, + 0x81, 0x9b, 0xa3, 0x57, 0x83, 0xa3, 0x28, 0x70, 0xd8, 0x3e, 0x81, 0xba, 0x28, 0xd1, 0x80, 0x63, + 0xe0, 0x37, 0x70, 0xcb, 0x22, 0x81, 0x9b, 0x23, 0x82, 0x9b, 0x28, 0xd2, 0x80, 0x63, 0xe1, 0x38, + 0x71, 0xcc, 0x23, 0x81, 0x9b, 0x9c, 0x88, 0x36, 0xd9, 0x1d, 0xa4, 0x58, 0x84, 0xa5, 0x28, 0x72, + 0xd9, 0x3f, 0x82, 0xbb, 0x29, 0xd2, 0x81, 0x64, 0xe1, 0x38, 0x72, 0xcc, 0x23, 0x82, 0x9b, 0xcc, + 0x43, 0x9c, 0x88, 0x36, 0xd8, 0x1d, 0xa4, 0x58, 0x84, 0xa5, 0x29, 0x71, 0xd9, 0x3f, 0x82, 0xbc, + 0x29, 0xd2, 0x81, 0x64, 0xe1, 0x39, 0x72, 0xcc, 0x23, 0x82, 0x9c, 0x37, 0xd9, 0x1d, 0xa4, 0x59, + 0x85, 0xa6, 0x29, 0x72, 0xda, 0x40, 0x83, 0xbc, 0x2a, 0xd3, 0x82, 0x64, 0xe2, 0x39, 0x72, 0xcc, + 0x24, 0x82, 0x9c, 0x82, 0x64, 0xe1, 0x3a, 0x72, 0xcc, 0x24, 0x82, 0x9c, 0x40, 0xe8, 0x1e, 0x89, + 0xcd, 0x44, 0x9d, 0x89, 0x37, 0xd9, 0x1e, 0xa5, 0x59, 0x85, 0xa6, 0x2a, 0x73, 0xda, 0x40, 0x84, + 0xbd, 0x2a, 0xd3, 0x82, 0x65, 0xe2, 0x39, 0x73, 0xcd, 0x24, 0x83, 0x9c, 0x40, 0x83, 0xbd, 0x2a, + 0xd3, 0x82, 0x65, 0xe2, 0x39, 0x73, 0xcd, 0x24, 0x83, 0x9c, 0xd2, 0x81, 0x63, 0xe0, 0x39, 0x71, + 0xcc, 0x23, 0x82, 0x9b, 0x9b, 0x1d, 0xa4, 0x59, 0x84, 0xa5, 0x29, 0x72, 0xd9, 0x3f, 0x83, 0xbc, + 0x29, 0xd3, 0x81, 0x64, 0xe1, 0x39, 0x72, 0xcc, 0x24, 0x82, 0x9c, 0x82, 0x9c, 0x3f + +}, + +{ + 0xe7, 0x89, 0x1b, 0x8b, 0xe6, 0x89, 0x1b, 0x8b, 0xe6, 0x89, 0x1b, 0x8a, 0xe6, 0x88, 0x1b, 0x8a, + 0xe6, 0x88, 0x1a, 0x8a, 0x1c, 0x8b, 0xe7, 0x89, 0x1b, 0x8b, 0xe6, 0x89, 0x1b, 0x8a, 0xe6, 0x89, + 0x1b, 0x8a, 0xe6, 0x89, 0x1b, 0x8a, 0xe6, 0x89, 0x1a, 0x8a, 0xe6, 0x88, 0x1a, 0x89, 0xe5, 0x88, + 0x1a, 0x89, 0xe5, 0x88, 0x1a, 0xe5, 0x88, 0x1a, 0x89, 0xe5, 0x88, 0x1a, 0x89, 0xe4, 0x87, 0x19, + 0x89, 0xe4, 0x87, 0x19, 0x88, 0xe4, 0x87, 0x19, 0x1a, 0x8a, 0xe5, 0x88, 0x1a, 0x89, 0xe5, 0x88, + 0x1a, 0x89, 0xe5, 0x88, 0x1a, 0x89, 0xe4, 0x87, 0x19, 0x89, 0xe4, 0x87, 0x19, 0xe4, 0x87, 0x19, + 0xe4, 0x87, 0x1a, 0x88, 0xe4, 0x87, 0x19, 0x88, 0xe4, 0x87, 0x19, 0x88, 0x1a, 0x89, 0xe5, 0x88, + 0x1a, 0x89, 0xe5, 0x88, 0x1a, 0x89, 0xe5, 0x87, 0x1a, 0x89, 0xe4, 0x88, 0x19, 0x89, 0xe4, 0x87, + 0x19, 0x88, 0xe4, 0x87, 0x19, 0x88, 0xe4, 0x87, 0x19, 0x88, 0xe4, 0x87, 0x19, 0x88, 0xe4, 0x87, + 0x19, 0x88, 0xe3, 0x86, 0x19, 0x88, 0xe3, 0x86, 0x18, 0x87, 0xe3, 0x86, 0x18, 0x88, 0xe3, 0x86, + 0x18, 0x87, 0xe3, 0x86, 0x18, 0x87, 0xe3, 0x86, 0x18, 0x87, 0xe3, 0x86, 0x18, 0x87, 0xe3, 0x86, + 0x18, 0x87, 0xe2, 0x85, 0x18, 0x87, 0xe3, 0x85, 0x17, 0x19, 0x88, 0xe3, 0x86, 0x19, 0x88, 0xe4, + 0x86, 0x19, 0x88, 0xe3, 0x86, 0x18, 0x88, 0xe3, 0x86, 0x18, 0x87, 0xe3, 0x86, 0x18, 0x87, 0xe3, + 0x86, 0x18, 0x87, 0xe3, 0x85, 0x18, 0x87, 0xe3, 0x86, 0x18, 0x87, 0xe3, 0x86, 0x18, 0x87, 0xe3, + 0x85, 0x18, 0x87, 0x19, 0x88, 0xe3, 0x86, 0x19, 0x88, 0xe3, 0x86, 0x18, 0x87, 0xe3, 0x86, 0x18, + 0x87, 0x1a, 0x89, 0xe4, 0x87, 0x19, 0x88, 0xe4, 0x87, 0x19, 0x1b, 0x89, 0x1c, 0x8b + +}, + +{ + 0xa1, 0xe6, 0xcf, 0x79, 0x58, 0x4a, 0x78, 0x7a, 0x53, 0x7b, 0xc2, 0xc3, 0x82, 0x36, 0x48, 0x84, + 0xcb, 0xa6, 0x67, 0x2e, 0x49, 0x7c, 0xaa, 0x93, 0x87, 0x73, 0x62, 0x6c, 0xa4, 0xaa, 0x71, 0xa7, + 0x67, 0x2f, 0x4a, 0x7c, 0xab, 0x94, 0x87, 0x74, 0x62, 0x6d, 0xa4, 0xab, 0x72, 0x7b, 0xaa, 0x93, + 0x87, 0x72, 0x61, 0x6c, 0xa4, 0xaa, 0x71, 0x72, 0x7b, 0xc2, 0xc3, 0x81, 0x36, 0x48, 0x84, 0xcb, + 0xa6, 0x67, 0x2e, 0x49, 0x7b, 0xaa, 0x93, 0x87, 0x72, 0x61, 0x6c, 0xa4, 0xaa, 0x71, 0xaa, 0x71, + 0x59, 0xa0, 0xe5, 0xce, 0x78, 0x57, 0x49, 0x77, 0x79, 0x52, 0x7a, 0xc1, 0xc2, 0x81, 0x35, 0x47, + 0x83, 0xca, 0xa6, 0x66, 0x2d, 0x48, 0x7b, 0xa9, 0x92, 0x86, 0x72, 0x61, 0x6b, 0xa3, 0xa9, 0x70, + 0x82, 0x36, 0x49, 0x84, 0xcc, 0xa7, 0x67, 0x2e, 0x4a, 0x7c, 0xab, 0x94, 0x87, 0x73, 0x62, 0x6c, + 0xa4, 0xab, 0x72, 0xc2, 0xc3, 0x82, 0x36, 0x48, 0x84, 0xcb, 0xa6, 0x67, 0x2e, 0x4a, 0x7c, 0xaa, + 0x93, 0x87, 0x73, 0x62, 0x6c, 0xa4, 0xaa, 0x71, 0xa3, 0xaa, 0x70, 0x49, 0x7b, 0xa9, 0x92, 0x86, + 0x72, 0x61, 0x6b, 0xa3, 0xaa, 0x70, 0x4b, 0x78, 0x7a, 0x53, 0x7c, 0xc2, 0xc3, 0x82, 0x36, 0x49, + 0x84, 0xcc, 0xa6, 0x67, 0x2f, 0x4a, 0x7c, 0xaa, 0x93, 0x87, 0x73, 0x62, 0x6c, 0xa4, 0xab, 0x72, + 0x7b, 0x5a, 0x4c, 0x7a, 0x7c, 0x55, 0x7e, 0xc4, 0xc5, 0x83, 0x38, 0x4a, 0x85, 0xcd, 0xa8, 0x69, + 0x30, 0x4b, 0x7d, 0xac, 0x95, 0x88, 0x74, 0x63, 0x6e, 0xa5, 0xac, 0x73, 0x7c, 0x55, 0x7d, 0xc4, + 0xc5, 0x83, 0x37, 0x4a, 0x85, 0xcd, 0xa8, 0x68, 0x30, 0x4b, 0x7d, 0xac, 0x95, 0x88, 0x74, 0x63, + 0x6e, 0xa6, 0xac, 0x73, 0xab, 0x94, 0x87, 0x73, 0x62, 0x6d, 0xa5, 0xab, 0x72, 0x5a + +}, + +{ + 0xbf, 0xdc, 0xd4, 0xaa, 0x82, 0x81, 0x35, 0x34, 0x4d, 0x75, 0x95, 0xcb, 0xde, 0xc9, 0x91, 0x72, + 0x4a, 0x30, 0x6b, 0x57, 0x69, 0x7d, 0x65, 0x3f, 0x33, 0x4f, 0x69, 0x81, 0xc1, 0xde, 0xd6, 0xac, + 0x83, 0x82, 0x37, 0x36, 0x4f, 0x77, 0x96, 0xcd, 0xe0, 0xca, 0x93, 0x73, 0x4c, 0x31, 0x6d, 0x58, + 0x6a, 0x4f, 0x68, 0x80, 0xc0, 0xdd, 0xd5, 0xab, 0x82, 0x82, 0x36, 0x35, 0x4e, 0x76, 0x95, 0xcc, + 0xdf, 0xca, 0x92, 0x73, 0x4b, 0x31, 0x6c, 0x57, 0x6a, 0xe0, 0xca, 0x92, 0x73, 0x4c, 0x31, 0x6c, + 0x58, 0x6a, 0xad, 0xd5, 0xdb, 0xbc, 0x7b, 0x63, 0x3c, 0x31, 0x4e, 0x66, 0x7e, 0xbe, 0xdb, 0xd3, + 0xaa, 0x81, 0x80, 0x34, 0x33, 0x4c, 0x75, 0x94, 0xcb, 0xde, 0xc8, 0x91, 0x71, 0x4a, 0x2f, 0x6b, + 0x56, 0x68, 0x35, 0x4e, 0x76, 0x95, 0xcc, 0xdf, 0xca, 0x92, 0x72, 0x4b, 0x30, 0x6c, 0x57, 0x6a, + 0xcc, 0xdf, 0xca, 0x91, 0x72, 0x4b, 0x30, 0x6c, 0x57, 0x69, 0x69, 0x7e, 0xbe, 0xdb, 0xd3, 0xa9, + 0x81, 0x80, 0x34, 0x33, 0x4c, 0x74, 0x94, 0xcb, 0xdd, 0xc8, 0x90, 0x71, 0x4a, 0x2f, 0x6b, 0x55, + 0x68, 0x56, 0x69, 0xac, 0xd4, 0xd9, 0xbb, 0x7a, 0x62, 0x3c, 0x30, 0x4c, 0x65, 0x7d, 0xbd, 0xda, + 0xd2, 0xa8, 0x80, 0x7f, 0x34, 0x33, 0x4c, 0x74, 0x94, 0xca, 0xdd, 0xc8, 0x90, 0x71, 0x49, 0x2e, + 0x6a, 0x55, 0x67, 0xd3, 0xa9, 0x81, 0x80, 0x34, 0x33, 0x4c, 0x74, 0x94, 0xca, 0xde, 0xc8, 0x91, + 0x71, 0x4a, 0x2f, 0x6a, 0x56, 0x68, 0xbd, 0xda, 0xd2, 0xa8, 0x7f, 0x7e, 0x33, 0x32, 0x4b, 0x73, + 0x92, 0xc9, 0xdc, 0xc7, 0x8f, 0x70, 0x49, 0x2e, 0x69, 0x54, 0x67, 0x6a, 0x56, 0x68, 0x93, 0xca, + 0xdd, 0xc8, 0x90, 0x70, 0x49, 0x2e, 0x6a, 0x55, 0x68, 0x3d, 0x31, 0x4e, 0x67, 0x7f + +}, + +}; + +void upd1771c_reset(struct upd1771c_t *scv) { + memset(scv->upd1771c_packets,0,16); + scv->upd1771c_mode = 0; + scv->upd1771c_pos = 0; + scv->upd1771c_posc = 0; + scv->upd1771c_wave = 0; + scv->upd1771c_vol = 0; + scv->upd1771c_period = 0; + scv->upd1771c_off = 0; + scv->upd1771c_npos = 0; + //scv->upd1771c_repsamp = 0; +} + +void upd1771c_write_packet(struct upd1771c_t *scv, uint8_t ind, uint8_t val) { + scv->upd1771c_packets[ind&15] = val; + switch (scv->upd1771c_packets[0]) { + case 1: + if (ind == 3) { + scv->upd1771c_mode = 1; + scv->upd1771c_wave = (scv->upd1771c_packets[1]&0xe0)>>5; + scv->upd1771c_off = 0; //? + scv->upd1771c_period = scv->upd1771c_packets[2]; + scv->upd1771c_vol = scv->upd1771c_packets[3]&0x1f; + } + break; + case 2: + if (ind == 3) { + scv->upd1771c_mode = 2; + scv->upd1771c_wave = (scv->upd1771c_packets[1]&0xe0)>>5; + scv->upd1771c_off = scv->upd1771c_packets[1]&0x1f; + scv->upd1771c_period = scv->upd1771c_packets[2]<0x20?0x20:scv->upd1771c_packets[2]; + scv->upd1771c_vol = scv->upd1771c_packets[3]&0x1f; + } + break; + default: + case 0: + scv->upd1771c_mode = 0; + break; + } +} + +void upd1771c_sound_set_clock(struct upd1771c_t *scv, unsigned int clock, unsigned int divi) { + scv->upd1771c_repsamp = divi; +} + +int16_t upd1771c_sound_stream_update(struct upd1771c_t *scv) { + int16_t s = 0; + for (int i = 0; i < scv->upd1771c_repsamp; i++) { + s = 0; + switch (scv->upd1771c_mode) { + case 2: + s = ((int16_t)WAVEFORMS[scv->upd1771c_wave][scv->upd1771c_posc])*scv->upd1771c_vol; + scv->upd1771c_pos++; + if (scv->upd1771c_pos >= scv->upd1771c_period) { + scv->upd1771c_pos=0; + scv->upd1771c_posc++; + if (scv->upd1771c_posc == 32) + scv->upd1771c_posc = scv->upd1771c_off; + } + break; + case 1: + scv->upd1771c_pos++; + if (scv->upd1771c_pos >= ((((uint32_t)scv->upd1771c_period) + 1)*128)) { + scv->upd1771c_pos=0; + scv->upd1771c_posc++; + if (scv->upd1771c_posc == NOISE_SIZE) + scv->upd1771c_posc = 0; + } + uint16_t p = scv->upd1771c_posc; + p = p>=254?253:p; + s = ((int16_t)(noise_tbl[scv->upd1771c_wave][p])-127)*scv->upd1771c_vol; + // inaccurate noise mixing :/ + // s |= (scv->upd1771c_npos&128)?127*scv->upd1771c_vol:0; + break; + case 0: + default: + break; + } + } + return s; +} + diff --git a/src/engine/platform/sound/upd1771c.h b/src/engine/platform/sound/upd1771c.h new file mode 100644 index 000000000..d0ea80f78 --- /dev/null +++ b/src/engine/platform/sound/upd1771c.h @@ -0,0 +1,33 @@ +#ifndef __UPD1771C_SOUND_H__ +#define __UPD1771C_SOUND_H__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + +struct upd1771c_t { + uint8_t upd1771c_packets[16]; + uint8_t upd1771c_mode; + uint32_t upd1771c_pos; + uint8_t upd1771c_off; + uint8_t upd1771c_posc; + uint8_t upd1771c_wave; + uint8_t upd1771c_vol; + uint8_t upd1771c_period; + uint8_t upd1771c_npos; + int upd1771c_repsamp; +}; + +void upd1771c_reset(struct upd1771c_t *scv); +void upd1771c_write_packet(struct upd1771c_t *scv, uint8_t ind, uint8_t val); +int16_t upd1771c_sound_stream_update(struct upd1771c_t *scv); +void upd1771c_sound_set_clock(struct upd1771c_t *scv, unsigned int clock, unsigned int divi); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif diff --git a/src/engine/platform/sound/ymf278b/License.txt b/src/engine/platform/sound/ymf278b/License.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/src/engine/platform/sound/ymf278b/License.txt @@ -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/ymf278b/ymf278.cpp b/src/engine/platform/sound/ymf278b/ymf278.cpp new file mode 100644 index 000000000..9eec6c85b --- /dev/null +++ b/src/engine/platform/sound/ymf278b/ymf278.cpp @@ -0,0 +1,1094 @@ +// Based on ymf278b.c written by R. Belmont and O. Galibert + +// Improved by Valley Bell, 2018 +// Thanks to niekniek and l_oliveira for providing recordings from OPL4 hardware. +// Thanks to superctr and wouterv for discussing changes. +// +// Improvements: +// - added TL interpolation, recordings show that internal TL levels are 0x00..0xff +// - fixed ADSR speeds, attack rate 15 is now instant +// - correct clamping of intermediate Rate Correction values +// - emulation of "loop glitch" (going out-of-bounds by playing a sample faster than it the loop is long) +// - made calculation of sample position cleaner and closer to how the HW works +// - increased output resolution from TL (0.375dB) to envelope (0.09375dB) +// - fixed volume table -6dB steps are done using bit shifts, steps in between are multiplicators +// - made octave -8 freeze the sample +// - verified that TL and envelope levels are applied separately, both go silent at -60dB +// - implemented pseudo-reverb and damping according to manual +// - made pseudo-reverb ignore Rate Correction (real hardware ignores it) +// - reimplemented LFO, speed exactly matches the formulas that were probably used when creating the manual +// - fixed LFO (tremolo) amplitude modulation +// - made LFO vibrato and tremolo accurate to hardware +// +// Known issues: +// - Octave -8 was only tested with fnum 0. Other fnum values might behave differently. + +// This class doesn't model a full YMF278b chip. Instead it only models the +// wave part. The FM part in modeled in YMF262 (it's almost 100% compatible, +// the small differences are handled in YMF262). The status register and +// interaction with the FM registers (e.g. the NEW2 bit) is currently handled +// in the MSXMoonSound class. + +// MODIFIED: +// Add YMW258 support by Grauw +// Add DO1 output support by cam900 + +#include "ymf278.h" +#include +#include + +// envelope output entries +// fixed to match recordings from actual OPL4 -Valley Bell +constexpr int MAX_ATT_INDEX = 0x280; // makes attack phase right and also goes well with "envelope stops at -60dB" +constexpr int MIN_ATT_INDEX = 0; +constexpr int TL_SHIFT = 2; // envelope values are 4x as fine as TL levels + +constexpr unsigned LFO_SHIFT = 18; // LFO period of up to 0x40000 sample +constexpr unsigned LFO_PERIOD = 1 << LFO_SHIFT; + +// Envelope Generator phases +constexpr int EG_ATT = 4; +constexpr int EG_DEC = 3; +constexpr int EG_SUS = 2; +constexpr int EG_REL = 1; +constexpr int EG_OFF = 0; + +// Pan values, units are -3dB, i.e. 8. +constexpr uint8_t pan_left[16] = { + 0, 8, 16, 24, 32, 40, 48, 255, 255, 0, 0, 0, 0, 0, 0, 0 +}; +constexpr uint8_t pan_right[16] = { + 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 48, 40, 32, 24, 16, 8 +}; + +// decay level table (3dB per step) +// 0 - 15: 0, 3, 6, 9,12,15,18,21,24,27,30,33,36,39,42,93 (dB) +static constexpr int16_t SC(int dB) { return int16_t(dB / 3 * 0x20); } +constexpr int16_t dl_tab[16] = { + SC( 0), SC( 3), SC( 6), SC( 9), SC(12), SC(15), SC(18), SC(21), + SC(24), SC(27), SC(30), SC(33), SC(36), SC(39), SC(42), SC(93) +}; + +constexpr byte RATE_STEPS = 8; +constexpr byte eg_inc[15 * RATE_STEPS] = { +//cycle:0 1 2 3 4 5 6 7 + 0, 1, 0, 1, 0, 1, 0, 1, // 0 rates 00..12 0 (increment by 0 or 1) + 0, 1, 0, 1, 1, 1, 0, 1, // 1 rates 00..12 1 + 0, 1, 1, 1, 0, 1, 1, 1, // 2 rates 00..12 2 + 0, 1, 1, 1, 1, 1, 1, 1, // 3 rates 00..12 3 + + 1, 1, 1, 1, 1, 1, 1, 1, // 4 rate 13 0 (increment by 1) + 1, 1, 1, 2, 1, 1, 1, 2, // 5 rate 13 1 + 1, 2, 1, 2, 1, 2, 1, 2, // 6 rate 13 2 + 1, 2, 2, 2, 1, 2, 2, 2, // 7 rate 13 3 + + 2, 2, 2, 2, 2, 2, 2, 2, // 8 rate 14 0 (increment by 2) + 2, 2, 2, 4, 2, 2, 2, 4, // 9 rate 14 1 + 2, 4, 2, 4, 2, 4, 2, 4, // 10 rate 14 2 + 2, 4, 4, 4, 2, 4, 4, 4, // 11 rate 14 3 + + 4, 4, 4, 4, 4, 4, 4, 4, // 12 rates 15 0, 15 1, 15 2, 15 3 for decay + 8, 8, 8, 8, 8, 8, 8, 8, // 13 rates 15 0, 15 1, 15 2, 15 3 for attack (zero time) + 0, 0, 0, 0, 0, 0, 0, 0, // 14 infinity rates for attack and decay(s) +}; + +static constexpr byte O(int a) { return a * RATE_STEPS; } +constexpr byte eg_rate_select[64] = { + O(14),O(14),O(14),O(14), // inf rate + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 0),O( 1),O( 2),O( 3), + O( 4),O( 5),O( 6),O( 7), + O( 8),O( 9),O(10),O(11), + O(12),O(12),O(12),O(12), +}; + +// rate 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 +// shift 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0 +// mask 4095, 2047, 1023, 511, 255, 127, 63, 31, 15, 7, 3, 1, 0, 0, 0, 0 +constexpr byte eg_rate_shift[64] = { + 12, 12, 12, 12, + 11, 11, 11, 11, + 10, 10, 10, 10, + 9, 9, 9, 9, + 8, 8, 8, 8, + 7, 7, 7, 7, + 6, 6, 6, 6, + 5, 5, 5, 5, + 4, 4, 4, 4, + 3, 3, 3, 3, + 2, 2, 2, 2, + 1, 1, 1, 1, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, +}; + + +// number of steps the LFO counter advances per sample +// LFO frequency (Hz) -> LFO counter steps per sample +static constexpr int L(double a) { return int((LFO_PERIOD * a) / 44100.0 + 0.5); } +constexpr int lfo_period[8] = { + L(0.168), // step: 1, period: 262144 samples + L(2.019), // step: 12, period: 21845 samples + L(3.196), // step: 19, period: 13797 samples + L(4.206), // step: 25, period: 10486 samples + L(5.215), // step: 31, period: 8456 samples + L(5.888), // step: 35, period: 7490 samples + L(6.224), // step: 37, period: 7085 samples + L(7.066), // step: 42, period: 6242 samples +}; + + +// formula used by Yamaha docs: +// vib_depth_cents(x) = (log2(0x400 + x) - 10) * 1200 +constexpr int16_t vib_depth[8] = { + 0, // 0.000 cents + 2, // 3.378 cents + 3, // 5.065 cents + 4, // 6.750 cents + 6, // 10.114 cents + 12, // 20.170 cents + 24, // 40.106 cents + 48, // 79.307 cents +}; + + +// formula used by Yamaha docs: +// am_depth_db(x) = (x-1) / 0x40 * 6.0 +// They use (x-1), because the depth is multiplied with the AM counter, which has a range of 0..0x7F. +// Thus the maximum attenuation with x=0x80 is (0x7F * 0x80) >> 7 = 0x7F. +// reversed formula: +// am_depth(dB) = round(dB / 6.0 * 0x40) + 1 +constexpr uint8_t am_depth[8] = { + 0x00, // 0.000 dB + 0x14, // 1.781 dB + 0x20, // 2.906 dB + 0x28, // 3.656 dB + 0x30, // 4.406 dB + 0x40, // 5.906 dB + 0x50, // 7.406 dB + 0x80, // 11.910 dB +}; + +// divisions of 16 +constexpr int mix_level[8] = { + 16, // 0dB + 12, // -3dB (approx) + 8, // -6dB + 6, // -9dB (approx) + 4, // -12dB + 3, // -15dB (approx) + 2, // -18dB + 0, // -inf dB +}; + +YMF278::Slot::Slot() +{ + reset(); +} + +// Sign extend a 4-bit value to int (32-bit) +// require: x in range [0..15] +static constexpr int sign_extend_4(int x) +{ + return (x ^ 8) - 8; +} + +// Params: oct in [-8 .. +7] +// fn in [ 0 .. 1023] +// We want to interpret oct as a signed 4-bit number and calculate +// ((fn | 1024) + vib) << (5 + sign_extend_4(oct)) +// Though in this formula the shift can go over a negative distance (in that +// case we should shift in the other direction). +static constexpr unsigned calcStep(int8_t oct, uint16_t fn, int16_t vib = 0) +{ + if (oct == -8) return 0; + unsigned t = (fn + 1024 + vib) << (8 + oct); // use '+' iso '|' (generates slightly better code) + return t >> 3; // was shifted 3 positions too far +} + +void YMF278::Slot::reset() +{ + wave = FN = OCT = TLdest = TL = pan = vib = AM = 0; + DL = AR = D1R = D2R = RC = RR = 0; + PRVB = keyon = DAMP = false; + stepptr = 0; + step = calcStep(OCT, FN); + bits = startaddr = loopaddr = endaddr = 0; + env_vol = MAX_ATT_INDEX; + + lfo_active = false; + lfo_cnt = 0; + lfo = 0; + + state = EG_OFF; + + // not strictly needed, but avoid UMR on savestate + pos = 0; +} + +int YMF278::Slot::compute_rate(int val) const +{ + if (val == 0) { + return 0; + } else if (val == 15) { + return 63; + } + int res = val * 4; + if (RC != 15) { + // clamping verified with HW tests -Valley Bell + res += 2 * YMF_clamp(OCT + RC, 0, 15); + res += (FN & 0x200) ? 1 : 0; + } + return YMF_clamp(res, 0, 63); +} + +int YMF278::Slot::compute_decay_rate(int val) const +{ + if (DAMP) { + // damping + // The manual lists these values for time and attenuation: (44100 samples/second) + // -12dB at 5.8ms, sample 256 + // -48dB at 8.0ms, sample 352 + // -72dB at 9.4ms, sample 416 + // -96dB at 10.9ms, sample 480 + // This results in these durations and rate values for the respective phases: + // 0dB .. -12dB: 256 samples (5.80ms) -> 128 samples per -6dB = rate 48 + // -12dB .. -48dB: 96 samples (2.18ms) -> 16 samples per -6dB = rate 63 + // -48dB .. -72dB: 64 samples (1.45ms) -> 16 samples per -6dB = rate 63 + // -72dB .. -96dB: 64 samples (1.45ms) -> 16 samples per -6dB = rate 63 + // Damping was verified to ignore rate correction. + if (env_vol < dl_tab[4]) { + return 48; // 0dB .. -12dB + } else { + return 63; // -12dB .. -96dB + } + } + if (PRVB) { + // pseudo reverb + // activated when reaching -18dB, overrides D1R/D2R/RR with reverb rate 5 + // + // The manual is actually a bit unclear and just says "RATE=5", + // referring to the D1R/D2R/RR register value. However, later + // pages use "RATE" to refer to the "internal" rate, which is + // (register * 4) + rate correction. HW recordings prove that + // Rate Correction is ignored, so pseudo reverb just sets the + // "internal" rate to a value of 4*5 = 20. + if (env_vol >= dl_tab[6]) { + return 20; + } + } + return compute_rate(val); +} + +int16_t YMF278::Slot::compute_vib() const +{ + // verified via hardware recording: + // With LFO speed 0 (period 262144 samples), each vibrato step takes + // 4096 samples. + // -> 64 steps total + // Also, with vibrato depth 7 (80 cents) and an F-Num of 0x400, the + // final F-Nums are: 0x400 .. 0x43C, 0x43C .. 0x400, 0x400 .. 0x3C4, + // 0x3C4 .. 0x400 + int16_t lfo_fm = lfo_cnt / (LFO_PERIOD / 0x40); + // results in +0x00..+0x0F, +0x0F..+0x00, -0x00..-0x0F, -0x0F..-0x00 + if (lfo_fm & 0x10) lfo_fm ^= 0x1F; + if (lfo_fm & 0x20) lfo_fm = -(lfo_fm & 0x0F); + + return (lfo_fm * vib_depth[vib]) / 12; +} + +uint16_t YMF278::Slot::compute_am() const +{ + // verified via hardware recording: + // With LFO speed 0 (period 262144 samples), each tremolo step takes + // 1024 samples. + // -> 256 steps total + uint16_t lfo_am = lfo_cnt / (LFO_PERIOD / 0x100); + // results in 0x00..0x7F, 0x7F..0x00 + if (lfo_am >= 0x80) lfo_am ^= 0xFF; + + return (lfo_am * am_depth[AM]) >> 7; +} + + +void YMF278Base::advance() +{ + eg_cnt++; + + // modulo counters for volume interpolation + int tl_int_cnt = eg_cnt % 9; // 0 .. 8 + int tl_int_step = (eg_cnt / 9) % 3; // 0 .. 2 + + for (auto& op : slots) { + // volume interpolation + if (tl_int_cnt == 0) { + if (tl_int_step == 0) { + // decrease volume by one step every 27 samples + if (op.TL < op.TLdest) ++op.TL; + } else { + // increase volume by one step every 13.5 samples + if (op.TL > op.TLdest) --op.TL; + } + } + + if (op.lfo_active) { + op.lfo_cnt = (op.lfo_cnt + lfo_period[op.lfo]) & (LFO_PERIOD - 1); + } + + // Envelope Generator + switch (op.state) { + case EG_ATT: { // attack phase + uint8_t rate = op.compute_rate(op.AR); + // Verified by HW recording (and matches Nemesis' tests of the YM2612): + // AR = 0xF during KeyOn results in instant switch to EG_DEC. (see keyOnHelper) + // Setting AR = 0xF while the attack phase is in progress freezes the envelope. + if (rate >= 63) { + break; + } + uint8_t shift = eg_rate_shift[rate]; + if (!(eg_cnt & ((1 << shift) - 1))) { + uint8_t select = eg_rate_select[rate]; + // >>4 makes the attack phase's shape match the actual chip -Valley Bell + op.env_vol += (~op.env_vol * eg_inc[select + ((eg_cnt >> shift) & 7)]) >> 4; + if (op.env_vol <= MIN_ATT_INDEX) { + op.env_vol = MIN_ATT_INDEX; + // TODO does the real HW skip EG_DEC completely, + // or is it active for 1 sample? + op.state = op.DL ? EG_DEC : EG_SUS; + } + } + break; + } + case EG_DEC: { // decay phase + uint8_t rate = op.compute_decay_rate(op.D1R); + uint8_t shift = eg_rate_shift[rate]; + if (!(eg_cnt & ((1 << shift) - 1))) { + uint8_t select = eg_rate_select[rate]; + op.env_vol += eg_inc[select + ((eg_cnt >> shift) & 7)]; + if (op.env_vol >= op.DL) { + op.state = (op.env_vol < MAX_ATT_INDEX) ? EG_SUS : EG_OFF; + } + } + break; + } + case EG_SUS: { // sustain phase + uint8_t rate = op.compute_decay_rate(op.D2R); + uint8_t shift = eg_rate_shift[rate]; + if (!(eg_cnt & ((1 << shift) - 1))) { + uint8_t select = eg_rate_select[rate]; + op.env_vol += eg_inc[select + ((eg_cnt >> shift) & 7)]; + if (op.env_vol >= MAX_ATT_INDEX) { + op.env_vol = MAX_ATT_INDEX; + op.state = EG_OFF; + } + } + break; + } + case EG_REL: { // release phase + uint8_t rate = op.compute_decay_rate(op.RR); + uint8_t shift = eg_rate_shift[rate]; + if (!(eg_cnt & ((1 << shift) - 1))) { + uint8_t select = eg_rate_select[rate]; + op.env_vol += eg_inc[select + ((eg_cnt >> shift) & 7)]; + if (op.env_vol >= MAX_ATT_INDEX) { + op.env_vol = MAX_ATT_INDEX; + op.state = EG_OFF; + } + } + break; + } + case EG_OFF: + // nothing + break; + + default: + UNREACHABLE; + } + } +} + +int16_t YMF278Base::getSample(Slot& slot, uint16_t pos) const +{ + // TODO How does this behave when R#2 bit 0 = 1? + // As-if read returns 0xff? (Like for CPU memory reads.) Or is + // sound generation blocked at some higher level? + switch (slot.bits) { + case 0: { + // 8 bit + return memory[slot.startaddr + pos] << 8; + } + case 1: { + // 12 bit + unsigned addr = slot.startaddr + ((pos / 2) * 3); + if (pos & 1) { + return (memory[addr + 2] << 8) | + (memory[addr + 1] & 0xF0); + } else { + return (memory[addr + 0] << 8) | + ((memory[addr + 1] << 4) & 0xF0); + } + } + case 2: { + // 16 bit + unsigned addr = slot.startaddr + (pos * 2); + return (memory[addr + 0] << 8) | + (memory[addr + 1]); + } + default: + // TODO unspecified + return 0; + } +} + +uint16_t YMF278Base::nextPos(Slot& slot, uint16_t pos, uint16_t increment) +{ + // If there is a 4-sample loop and you advance 12 samples per step, + // it may exceed the end offset. + // This is abused by the "Lizard Star" song to generate noise at 0:52. -Valley Bell + pos += increment; + if ((uint32_t(pos) + slot.endaddr) >= 0x10000) // check position >= (negated) end address + pos += slot.endaddr + slot.loopaddr; // This is how the actual chip does it. + return pos; +} + +bool YMF278Base::anyActive() +{ + return std::any_of(std::begin(slots), std::end(slots), [](auto& op) { return op.state != EG_OFF; }); +} + +// In: 'envVol', 0=max volume, others -> -3/32 = -0.09375 dB/step +// Out: 'x' attenuated by the corresponding factor. +// Note: microbenchmarks have shown that re-doing this calculation is about the +// same speed as using a 4kB lookup table. +static constexpr int vol_factor(int x, unsigned envVol) +{ + if (envVol >= MAX_ATT_INDEX) return 0; // hardware clips to silence below -60dB + int vol_mul = 0x80 - (envVol & 0x3F); // 0x40 values per 6dB + int vol_shift = 7 + (envVol >> 6); + return (x * ((0x8000 * vol_mul) >> vol_shift)) >> 15; +} + +void YMF278Base::generate(short& fleft, short& fright, short& rleft, short& rright, short* channelBufs) +{ + int sampleFLeft = 0; + int sampleFRight = 0; + int sampleRLeft = 0; + int sampleRRight = 0; + for (size_t i = 0, count = slots.size(); i < count; i++) { + Slot& sl = slots[i]; + if (sl.state == EG_OFF) { + //sampleLeft += 0; + //sampleRight += 0; + if (channelBufs != nullptr) { + channelBufs[i] = 0; + } + continue; + } + + int16_t sample = (getSample(sl, sl.pos) * (0x10000 - sl.stepptr) + + getSample(sl, nextPos(sl, sl.pos, 1)) * sl.stepptr) >> 16; + // TL levels are 00..FF internally (TL register value 7F is mapped to TL level FF) + // Envelope levels have 4x the resolution (000..3FF) + // Volume levels are approximate logarithmic. -6dB result in half volume. Steps in between use linear interpolation. + // A volume of -60dB or lower results in silence. (value 0x280..0x3FF). + // Recordings from actual hardware indicate that TL level and envelope level are applied separarely. + // Each of them is clipped to silence below -60dB, but TL+envelope might result in a lower volume. -Valley Bell + uint16_t envVol = std::min(sl.env_vol + ((sl.lfo_active && sl.AM) ? sl.compute_am() : 0), + MAX_ATT_INDEX); + int smplOut = vol_factor(vol_factor(sample, envVol), sl.TL << TL_SHIFT); + + // Panning is also done separately. (low-volume TL + low-volume panning goes below -60dB) + // I'll be taking wild guess and assume that -3dB is approximated with 75%. (same as with TL and envelope levels) + // The same applies to the PCM mix level. + int32_t volLeft = pan_left [sl.pan]; // note: register 0xF9 is handled externally + int32_t volRight = pan_right[sl.pan]; + // 0 -> 0x20, 8 -> 0x18, 16 -> 0x10, 24 -> 0x0C, etc. (not using vol_factor here saves array boundary checks) + volLeft = (0x20 - (volLeft & 0x0f)) >> (volLeft >> 4); + volRight = (0x20 - (volRight & 0x0f)) >> (volRight >> 4); + + if (sl.ch) + { + sampleRLeft += (smplOut * volLeft ) >> 5; + sampleRRight += (smplOut * volRight) >> 5; + } + else + { + sampleFLeft += (smplOut * volLeft ) >> 5; + sampleFRight += (smplOut * volRight) >> 5; + } + + unsigned step = (sl.lfo_active && sl.vib) + ? calcStep(sl.OCT, sl.FN, sl.compute_vib()) + : sl.step; + sl.stepptr += step; + + if (sl.stepptr >= 0x10000) { + sl.pos = nextPos(sl, sl.pos, sl.stepptr >> 16); + sl.stepptr &= 0xffff; + } + + if (channelBufs != nullptr) { + channelBufs[i] = sl.pan != 8 ? smplOut : 0; + } + } + advance(); + + fleft = sampleFLeft >> 4; + fright = sampleFRight >> 4; + rleft = sampleRLeft >> 4; + rright = sampleRRight >> 4; +} + +void YMF278Base::keyOnHelper(Slot& slot) +{ + // Unlike FM, the envelope level is reset. (And it makes sense, because you restart the sample.) + slot.env_vol = MAX_ATT_INDEX; + if (slot.compute_rate(slot.AR) < 63) { + slot.state = EG_ATT; + } else { + // Nuke.YKT verified that the FM part does it exactly this way, + // and the OPL4 manual says it's instant as well. + slot.env_vol = MIN_ATT_INDEX; + // see comment in 'case EG_ATT' in YMF278::advance() + slot.state = slot.DL ? EG_DEC : EG_SUS; + } + slot.stepptr = 0; + slot.pos = 0; +} + +YMF278Base::YMF278Base(MemoryInterface& memory, int channelCount, int clockDivider, double clockFrequency) + : memory(memory) + , slots(channelCount) + , channelCount(channelCount) + , clockDivider(clockDivider) + , clockFrequency(clockFrequency) +{ + reset(); +} + +YMF278Base::~YMF278Base() +{ +} + +int YMF278Base::getChannelCount() +{ + return channelCount; +} + +int YMF278Base::getClockDivider() +{ + return clockDivider; +} + +double YMF278Base::getClockFrequency() +{ + return clockFrequency; +} + +void YMF278Base::setClockFrequency(double clockFrequency_) +{ + clockFrequency = clockFrequency_; +} + +double YMF278Base::getSampleRate() +{ + return clockFrequency / (channelCount * clockDivider); +} + +void YMF278Base::reset() +{ + eg_cnt = 0; + for (auto& op : slots) { + op.reset(); + } + memory.setMemoryType(false); +} + +YMF278::YMF278(MemoryInterface& memory) + : YMF278Base(memory, 24, 32, 33868800) + , fmMixL(0), fmMixR(0), pcmMixL(0), pcmMixR(0) +{ + memAdr = 0; // avoid UMR + std::fill(std::begin(regs), std::end(regs), 0); +} + +void YMF278::reset() +{ + YMF278Base::reset(); + + regs[2] = 0; // avoid UMR + for (int i = 0xf7; i >= 0; --i) { // reverse order to avoid UMR + writeReg(i, 0); + } + writeReg(0xf8, 0x1b); + writeReg(0xf9, 0x00); + memAdr = 0; +} + +void YMF278::writeReg(byte reg, byte data) +{ + // Handle slot registers specifically + if (reg >= 0x08 && reg <= 0xF7) { + int sNum = (reg - 8) % 24; + auto& slot = slots[sNum]; + switch ((reg - 8) / 24) { + case 0: { + slot.wave = (slot.wave & 0x100) | data; + int waveTblHdr = (regs[2] >> 2) & 0x7; + int base = (slot.wave < 384 || !waveTblHdr) ? + (slot.wave * 12) : + (waveTblHdr * 0x80000 + ((slot.wave - 384) * 12)); + byte buf[12]; + for (unsigned i = 0; i < 12; ++i) { + // TODO What if R#2 bit 0 = 1? + // See also getSample() + buf[i] = memory[base + i]; + } + slot.bits = (buf[0] & 0xC0) >> 6; + slot.startaddr = buf[2] | (buf[1] << 8) | ((buf[0] & 0x3F) << 16); + slot.loopaddr = buf[4] | (buf[3] << 8); + slot.endaddr = buf[6] | (buf[5] << 8); + for (unsigned i = 7; i < 12; ++i) { + // Verified on real YMF278: + // After tone loading, if you read these + // registers, their value actually has changed. + writeReg(8 + sNum + (i - 2) * 24, buf[i]); + } + if (slot.keyon) { + keyOnHelper(slot); + } else { + slot.stepptr = 0; + slot.pos = 0; + } + break; + } + case 1: { + slot.wave = (slot.wave & 0xFF) | ((data & 0x1) << 8); + slot.FN = (slot.FN & 0x380) | (data >> 1); + slot.step = calcStep(slot.OCT, slot.FN); + break; + } + case 2: { + slot.FN = (slot.FN & 0x07F) | ((data & 0x07) << 7); + slot.PRVB = (data & 0x08) != 0; + slot.OCT = sign_extend_4((data & 0xF0) >> 4); + slot.step = calcStep(slot.OCT, slot.FN); + break; + } + case 3: { + uint8_t t = data >> 1; + slot.TLdest = (t != 0x7f) ? t : 0xff; // verified on HW via volume interpolation + if (data & 1) { + // directly change volume + slot.TL = slot.TLdest; + } else { + // interpolate volume + } + break; + } + case 4: + slot.ch = data & 0x10; + slot.pan = data & 0x0F; + + if (data & 0x20) { + // LFO reset + slot.lfo_active = false; + slot.lfo_cnt = 0; + } else { + // LFO activate + slot.lfo_active = true; + } + + slot.DAMP = (data & 0x40) != 0; + + if (data & 0x80) { + if (!slot.keyon) { + slot.keyon = true; + keyOnHelper(slot); + } + } else { + if (slot.keyon) { + slot.keyon = false; + slot.state = EG_REL; + } + } + break; + case 5: + slot.lfo = (data >> 3) & 0x7; + slot.vib = data & 0x7; + break; + case 6: + slot.AR = data >> 4; + slot.D1R = data & 0xF; + break; + case 7: + slot.DL = dl_tab[data >> 4]; + slot.D2R = data & 0xF; + break; + case 8: + slot.RC = data >> 4; + slot.RR = data & 0xF; + break; + case 9: + slot.AM = data & 0x7; + break; + } + } else { + // All non-slot registers + switch (reg) { + case 0x00: // TEST + case 0x01: + break; + + case 0x02: + // wave-table-header / memory-type / memory-access-mode + // Simply store in regs[2] + memory.setMemoryType(regs[2] & 2); + break; + + case 0x03: + // Verified on real YMF278: + // * Don't update the 'memAdr' variable on writes to + // reg 3 and 4. Only store the value in the 'regs' + // array for later use. + // * The upper 2 bits are not used to address the + // external memories (so from a HW pov they don't + // matter). But if you read back this register, the + // upper 2 bits always read as '0' (even if you wrote + // '1'). So we mask the bits here already. + data &= 0x3F; + break; + + case 0x04: + // See reg 3. + break; + + case 0x05: + // Verified on real YMF278: (see above) + // Only writes to reg 5 change the (full) 'memAdr'. + memAdr = (regs[3] << 16) | (regs[4] << 8) | data; + break; + + case 0x06: // memory data + if (regs[2] & 1) { + memory.write(memAdr, data); + ++memAdr; // no need to mask (again) here + } else { + // Verified on real YMF278: + // - writes are ignored + // - memAdr is NOT increased + } + break; + + case 0xf8: + fmMixL = mix_level[data & 0x7]; + fmMixR = mix_level[data >> 3 & 0x7]; + break; + case 0xf9: + pcmMixL = mix_level[data & 0x7]; + pcmMixR = mix_level[data >> 3 & 0x7]; + break; + } + } + + regs[reg] = data; +} + +byte YMF278::readReg(byte reg) +{ + // no need to call updateStream(time) + byte result = peekReg(reg); + if (reg == 6) { + // Memory Data Register + if (regs[2] & 1) { + // Verified on real YMF278: + // memAdr is only increased when 'regs[2] & 1' + ++memAdr; // no need to mask (again) here + } + } + return result; +} + +byte YMF278::peekReg(byte reg) const +{ + switch (reg) { + case 2: // 3 upper bits are device ID + return (regs[2] & 0x1F) | 0x20; + + case 6: // Memory Data Register + if (regs[2] & 1) { + return memory[memAdr]; + } else { + // Verified on real YMF278 + return 0xff; + } + + default: + return regs[reg]; + } +} + +YMW258::YMW258(MemoryInterface& memory) + : YMF278Base(memory, 28, 8, 9878400) +{ +} + +void YMW258::writeReg(byte channel, byte reg, byte data) +{ + if ((channel & 0x7) == 0x7 || channel >= 0x20 || reg >= 0x8) + return; + int sNum = (channel >> 3) * 7 + (channel & 0x7); + auto& slot = slots[sNum]; + + switch (reg) { + case 0: { + slot.pan = data >> 4; + break; + } + case 1: { + slot.wave = (slot.wave & 0x100) | data; + int base = slot.wave * 12; + byte buf[12]; + for (unsigned i = 0; i < 12; ++i) { + buf[i] = memory[base + i]; + } + slot.bits = (buf[0] >> 6) == 0x3 ? 1 : 0; // 00 / 10: 8 bit, 11: 12 bit, 01: unknown + slot.startaddr = buf[2] | (buf[1] << 8) | ((buf[0] & 0x1F) << 16); + slot.loopaddr = buf[4] | (buf[3] << 8); + slot.endaddr = buf[6] | (buf[5] << 8); + slot.lfo = (buf[7] >> 3) & 0x7; + slot.vib = buf[7] & 0x7; + slot.AR = buf[8] >> 4; + slot.D1R = buf[8] & 0xF; + slot.DL = dl_tab[buf[9] >> 4]; + slot.D2R = buf[9] & 0xF; + slot.RC = buf[10] >> 4; + slot.RR = buf[10] & 0xF; + slot.AM = buf[11] & 0x7; + if (slot.keyon) { + keyOnHelper(slot); + } else { + slot.stepptr = 0; + slot.pos = 0; + } + break; + } + case 2: { + slot.wave = (slot.wave & 0xFF) | ((data & 0x1) << 8); + slot.FN = (slot.FN & 0x3C0) | (data >> 2); + slot.step = calcStep(slot.OCT, slot.FN); + break; + } + case 3: { + slot.FN = (slot.FN & 0x03F) | ((data & 0x0F) << 6); + slot.OCT = sign_extend_4((data & 0xF0) >> 4); + slot.step = calcStep(slot.OCT, slot.FN); + break; + } + case 4: { + slot.lfo_active = true; + if (data & 0x80) { + if (!slot.keyon) { + slot.keyon = true; + keyOnHelper(slot); + } + } else { + if (slot.keyon) { + slot.keyon = false; + slot.state = EG_REL; + } + } + break; + } + case 5: { + uint8_t t = data >> 1; + slot.TLdest = (t != 0x7f) ? t : 0xff; // verified on YMF278 via volume interpolation + if (data & 1) { + // directly change volume + slot.TL = slot.TLdest; + } else { + // interpolate volume + } + break; + } + case 6: { + slot.lfo = (data >> 3) & 0x7; + slot.vib = data & 0x7; + break; + } + case 7: { + slot.AM = data & 0x7; + break; + } + } +} + +MemoryMoonSound::MemoryMoonSound(MemoryInterface& rom, MemoryInterface& ram) + : rom(rom) + , ram(ram) + , memoryType(false) +{ + if (rom.getSize() != 0x200000) { // 2MB + assert(false); + } + assert((ram.getSize() & (1024 - 1)) == 0); + int ramSize_ = ram.getSize() / 1024; + if ((ramSize_ != 0) && // - - + (ramSize_ != 128) && // 128kB - + (ramSize_ != 256) && // 128kB 128kB + (ramSize_ != 512) && // 512kB - + (ramSize_ != 640) && // 512kB 128kB + (ramSize_ != 1024) && // 512kB 512kB + (ramSize_ != 2048)) { // 512kB 512kB 512kB 512kB + assert(false); + } +} + +// This routine translates an address from the (upper) MoonSound address space +// to an address inside the (linearized) SRAM address space. +// +// The following info is based on measurements on a real MoonSound (v2.0) +// PCB. This PCB can have several possible SRAM configurations: +// 128kB: +// 1 SRAM chip of 128kB, chip enable (/CE) of this SRAM chip is connected to +// the 1Y0 output of a 74LS139 (2-to-4 decoder). The enable input of the +// 74LS139 is connected to YMF278 pin /MCS6 and the 74LS139 1B:1A inputs are +// connected to YMF278 pins MA18:MA17. So the SRAM is selected when /MC6 is +// active and MA18:MA17 == 0:0. +// 256kB: +// 2 SRAM chips of 128kB. First one connected as above. Second one has /CE +// connected to 74LS139 pin 1Y1. So SRAM2 is selected when /MSC6 is active +// and MA18:MA17 == 0:1. +// 512kB: +// 1 SRAM chip of 512kB, /CE connected to /MCS6 +// 640kB: +// 1 SRAM chip of 512kB, /CE connected to /MCS6 +// 1 SRAM chip of 128kB, /CE connected to /MCS7. +// (This means SRAM2 is potentially mirrored over a 512kB region) +// 1024kB: +// 1 SRAM chip of 512kB, /CE connected to /MCS6 +// 1 SRAM chip of 512kB, /CE connected to /MCS7 +// 2048kB: +// 1 SRAM chip of 512kB, /CE connected to /MCS6 +// 1 SRAM chip of 512kB, /CE connected to /MCS7 +// 1 SRAM chip of 512kB, /CE connected to /MCS8 +// 1 SRAM chip of 512kB, /CE connected to /MCS9 +// This configuration is not so easy to create on the v2.0 PCB. So it's +// very rare. +// +// So the /MCS6 and /MCS7 (and /MCS8 and /MCS9 in case of 2048kB) signals are +// used to select the different SRAM chips. The meaning of these signals +// depends on the 'memory access mode'. This mode can be changed at run-time +// via bit 1 in register 2. The following table indicates for which regions +// these signals are active (normally MoonSound should be used with mode=0): +// mode=0 mode=1 +// /MCS6 0x200000-0x27FFFF 0x380000-0x39FFFF +// /MCS7 0x280000-0x2FFFFF 0x3A0000-0x3BFFFF +// /MCS8 0x300000-0x37FFFF 0x3C0000-0x3DFFFF +// /MCS9 0x380000-0x3FFFFF 0x3E0000-0x3FFFFF +// +// (For completeness) MoonSound also has 2MB ROM (YRW801), /CE of this ROM is +// connected to YMF278 /MCS0. In both mode=0 and mode=1 this signal is active +// for the region 0x000000-0x1FFFFF. (But this routine does not handle ROM). +unsigned MemoryMoonSound::getRamAddress(unsigned addr) const +{ + addr -= 0x200000; // RAM starts at 0x200000 + if (memoryType) { + // Normally MoonSound is used in 'memory access mode = 0'. But + // in the rare case that mode=1 we adjust the address. + if ((0x180000 <= addr) && (addr <= 0x1FFFFF)) { + addr -= 0x180000; + switch (addr & 0x060000) { + case 0x000000: // [0x380000-0x39FFFF] + // 1st 128kB of SRAM1 + break; + case 0x020000: // [0x3A0000-0x3BFFFF] + if (ram.getSize() == 256 * 1024) { + // 2nd 128kB SRAM chip + } else { + // 2nd block of 128kB in SRAM2 + // In case of 512+128, we use mirroring + addr += 0x080000; + } + break; + case 0x040000: // [0x3C0000-0x3DFFFF] + // 3rd 128kB block in SRAM3 + addr += 0x100000; + break; + case 0x060000: // [0x3EFFFF-0x3FFFFF] + // 4th 128kB block in SRAM4 + addr += 0x180000; + break; + } + } else { + addr = unsigned(-1); // unmapped + } + } + if (ram.getSize() == 640 * 1024) { + // Verified on real MoonSound cartridge (v2.0): In case of + // 640kB (1x512kB + 1x128kB), the 128kB SRAM chip is 4 times + // visible. None of the other SRAM configurations show similar + // mirroring (because the others are powers of two). + if (addr > 0x080000) { + addr &= ~0x060000; + } + } + return addr; +} + +byte MemoryMoonSound::operator[](unsigned address) const +{ + // Verified on real YMF278: address space wraps at 4MB. + address &= 0x3FFFFF; + if (address < 0x200000) { + // ROM connected to /MCS0 + return rom[address]; + } else { + unsigned ramAddr = getRamAddress(address); + if (ramAddr < ram.getSize()) { + return ram[ramAddr]; + } else { + // unmapped region + return 255; // TODO check + } + } +} + +unsigned MemoryMoonSound::getSize() const { + return 0x400000; +} + +void MemoryMoonSound::write(unsigned address, byte value) +{ + address &= 0x3FFFFF; + if (address < 0x200000) { + // can't write to ROM + } else { + unsigned ramAddr = getRamAddress(address); + if (ramAddr < ram.getSize()) { + ram.write(ramAddr, value); + } else { + // can't write to unmapped memory + } + } +} + +void MemoryMoonSound::clear(byte value) { + ram.clear(value); +} + +void MemoryMoonSound::setMemoryType(bool memoryType_) { + memoryType = memoryType_; +} diff --git a/src/engine/platform/sound/ymf278b/ymf278.h b/src/engine/platform/sound/ymf278b/ymf278.h new file mode 100644 index 000000000..b0622adb2 --- /dev/null +++ b/src/engine/platform/sound/ymf278b/ymf278.h @@ -0,0 +1,155 @@ +#ifndef YMF278_HH +#define YMF278_HH + +#include +#include +#include + +#define UNREACHABLE while (1) assert(false) + +using byte = uint8_t; + +template +const T& YMF_clamp(const T& value, const T& min, const T& max) { + return std::min(std::max(value, min), max); +} + +class MemoryInterface { +public: + virtual byte operator[](unsigned address) const = 0; + virtual unsigned getSize() const = 0; + virtual void write(unsigned address, byte value) = 0; + virtual void clear(byte value) = 0; + virtual void setMemoryType(bool memoryType) {}; +}; + +class YMF278Base +{ +public: + YMF278Base(MemoryInterface& memory, int channelCount, int clockDivider, double clockFrequency); + ~YMF278Base(); + int getChannelCount(); + int getClockDivider(); + double getClockFrequency(); + void setClockFrequency(double clockFrequency); + double getSampleRate(); + virtual void reset(); + + void generate(short& fleft, short& fright, short& rleft, short& rright, short* channelBufs = nullptr); + + class Slot final { + public: + Slot(); + void reset(); + int compute_rate(int val) const; + int compute_decay_rate(int val) const; + unsigned decay_rate(int num, int sample_rate); + void envelope_next(int sample_rate); + int16_t compute_vib() const; + uint16_t compute_am() const; + + template + void serialize(Archive& ar, unsigned version); + + uint32_t startaddr; + uint16_t loopaddr; + uint16_t endaddr; // Note: stored in 2s complement (0x0000 = 0, 0x0001 = -65536, 0xffff = -1) + uint32_t step; // fixed-point frequency step + // invariant: step == calcStep(OCT, FN) + uint32_t stepptr; // fixed-point pointer into the sample + uint16_t pos; + + int16_t env_vol; + + uint32_t lfo_cnt; + + int16_t DL; + uint16_t wave; // wavetable number + uint16_t FN; // f-number TODO store 'FN | 1024'? + int8_t OCT; // octave [-8..+7] + bool PRVB; // pseudo-reverb + uint8_t TLdest; // destination total level + uint8_t TL; // total level (goes towards TLdest) + uint8_t pan; // panpot 0..15 + bool ch; // channel select + bool keyon; // slot keyed on + bool DAMP; + uint8_t lfo; // LFO speed 0..7 + uint8_t vib; // vibrato 0..7 + uint8_t AM; // AM level 0..7 + uint8_t AR; // 0..15 + uint8_t D1R; // 0..15 + uint8_t D2R; // 0..15 + uint8_t RC; // rate correction 0..15 + uint8_t RR; // 0..15 + + uint8_t bits; // width of the samples + + uint8_t state; // envelope generator state + bool lfo_active; + }; + +protected: + void keyOnHelper(Slot& slot); + + MemoryInterface& memory; + std::vector slots; + +private: + int16_t getSample(Slot& slot, uint16_t pos) const; + static uint16_t nextPos(Slot& slot, uint16_t pos, uint16_t increment); + void advance(); + bool anyActive(); + + /** Global envelope generator counter. */ + unsigned eg_cnt; + + unsigned channelCount, clockDivider; + double clockFrequency; +}; + +class YMF278 final : public YMF278Base { +public: + YMF278(MemoryInterface& memory); + void reset() override; + void writeReg(byte reg, byte data); + byte readReg(byte reg); + byte peekReg(byte reg) const; + + void generateMix(short fmL, short fmR, short& bufFL, short& bufFR, short& bufRL, short& bufRR, short* channelBufs = nullptr) { + generate(bufFL, bufFR, bufRL, bufRR, channelBufs); + bufFL = std::min(std::max((pcmMixL * bufFL + fmMixL * fmL) >> 4, -0x8000), 0x7fff); + bufFR = std::min(std::max((pcmMixR * bufFR + fmMixR * fmR) >> 4, -0x8000), 0x7fff);; + } + +private: + int fmMixL, fmMixR, pcmMixL, pcmMixR; + int memAdr; + byte regs[256]; +}; + +class YMW258 final : public YMF278Base { +public: + YMW258(MemoryInterface& memory); + void writeReg(byte channel, byte reg, byte data); +}; + +class MemoryMoonSound : MemoryInterface { +public: + MemoryMoonSound(MemoryInterface& rom, MemoryInterface& ram); + byte operator[](unsigned address) const override; + unsigned getSize() const override; + void write(unsigned address, byte value) override; + void clear(byte value) override; + void setMemoryType(bool memoryType) override; + +private: + unsigned getRamAddress(unsigned addr) const; + + MemoryInterface& rom; + MemoryInterface& ram; + + bool memoryType; +}; + +#endif diff --git a/src/engine/platform/sound/ymfm/ymfm_fm.h b/src/engine/platform/sound/ymfm/ymfm_fm.h index f77e89434..0c4342f0b 100644 --- a/src/engine/platform/sound/ymfm/ymfm_fm.h +++ b/src/engine/platform/sound/ymfm/ymfm_fm.h @@ -304,9 +304,9 @@ public: // simple getters for debugging fm_operator *debug_operator(uint32_t index) const { return m_op[index]; } - int32_t debug_output(uint32_t index) const { return m_output[index]; } - int32_t debug_special1() const { return m_special1; } - int32_t debug_special2() const { return m_special2; } + int32_t debug_output(uint32_t index) const { return m_output[index]; } + int32_t debug_special1() const { return m_special1; } + int32_t debug_special2() const { return m_special2; } private: // helper to add values to the outputs based on channel enables @@ -320,21 +320,21 @@ private: constexpr int out3_index = 3 % RegisterType::OUTPUTS; if (RegisterType::OUTPUTS == 1 || m_regs.ch_output_0(choffs)) { - m_output[out0_index]=value; + m_output[out0_index]=value; output.data[out0_index] += value; - } + } if (RegisterType::OUTPUTS >= 2 && m_regs.ch_output_1(choffs)) { - m_output[out1_index]=value; + m_output[out1_index]=value; output.data[out1_index] += value; - } + } if (RegisterType::OUTPUTS >= 3 && m_regs.ch_output_2(choffs)) { - m_output[out2_index]=value; + m_output[out2_index]=value; output.data[out2_index] += value; - } + } if (RegisterType::OUTPUTS >= 4 && m_regs.ch_output_3(choffs)) { - m_output[out3_index]=value; + m_output[out3_index]=value; output.data[out3_index] += value; - } + } } // internal state @@ -344,9 +344,9 @@ private: fm_operator *m_op[4]; // up to 4 operators RegisterType &m_regs; // direct reference to registers fm_engine_base &m_owner; // reference to the owning engine - mutable int32_t m_output[4]; - mutable int32_t m_special1; - mutable int32_t m_special2; + mutable int32_t m_output[4]; + mutable int32_t m_special1; + mutable int32_t m_special2; }; diff --git a/src/engine/platform/sound/ymfm/ymfm_opl.h b/src/engine/platform/sound/ymfm/ymfm_opl.h index 8a2dd5147..c23a5f0b9 100644 --- a/src/engine/platform/sound/ymfm/ymfm_opl.h +++ b/src/engine/platform/sound/ymfm/ymfm_opl.h @@ -529,7 +529,7 @@ public: // generate samples of sound void generate(output_data *output, uint32_t numsamples = 1); - fm_engine* debug_fm_engine() { return &m_fm; } + fm_engine* debug_fm_engine() { return &m_fm; } protected: // internal state uint8_t m_address; // address register @@ -577,8 +577,8 @@ public: // generate samples of sound void generate(output_data *output, uint32_t numsamples = 1); - fm_engine* debug_fm_engine() { return &m_fm; } - adpcm_b_engine* debug_adpcm_b_engine() { return &m_adpcm_b; } + fm_engine* debug_fm_engine() { return &m_fm; } + adpcm_b_engine* debug_adpcm_b_engine() { return &m_adpcm_b; } protected: // internal state @@ -628,7 +628,7 @@ public: // generate samples of sound void generate(output_data *output, uint32_t numsamples = 1); - fm_engine* debug_fm_engine() { return &m_fm; } + fm_engine* debug_fm_engine() { return &m_fm; } protected: // internal state @@ -677,7 +677,7 @@ public: // generate samples of sound void generate(output_data *output, uint32_t numsamples = 1); - fm_engine* debug_fm_engine() { return &m_fm; } + fm_engine* debug_fm_engine() { return &m_fm; } protected: // internal state @@ -791,6 +791,8 @@ public: // generate samples of sound void generate(output_data *output, uint32_t numsamples = 1); + fm_engine* debug_fm_engine() { return &m_fm; } + pcm_engine* debug_pcm_engine() { return &m_pcm; } protected: // internal state uint16_t m_address; // address register diff --git a/src/engine/platform/sound/ymfm/ymfm_pcm.cpp b/src/engine/platform/sound/ymfm/ymfm_pcm.cpp index 34417490c..30fbe2396 100644 --- a/src/engine/platform/sound/ymfm/ymfm_pcm.cpp +++ b/src/engine/platform/sound/ymfm/ymfm_pcm.cpp @@ -309,6 +309,7 @@ void pcm_channel::clock(uint32_t env_counter) void pcm_channel::output(output_data &output) const { + m_output[0] = m_output[1] = m_output[2] = m_output[3] = 0; // early out if the envelope is effectively off uint32_t envelope = m_env_attenuation; if (envelope > EG_QUIET) @@ -340,6 +341,8 @@ void pcm_channel::output(output_data &output) const uint32_t outnum = m_regs.ch_output_channel(m_choffs) * 2; output.data[outnum + 0] += (lvol * sample) >> 15; output.data[outnum + 1] += (rvol * sample) >> 15; + m_output[outnum + 0] = output.data[outnum + 0]; + m_output[outnum + 1] = output.data[outnum + 1]; } diff --git a/src/engine/platform/sound/ymfm/ymfm_pcm.h b/src/engine/platform/sound/ymfm/ymfm_pcm.h index b471fa611..ad15a80de 100644 --- a/src/engine/platform/sound/ymfm/ymfm_pcm.h +++ b/src/engine/platform/sound/ymfm/ymfm_pcm.h @@ -267,6 +267,8 @@ public: // load a new wavetable entry void load_wavetable(); + int32_t debug_output(uint32_t index) const { return m_output[index]; } + private: // internal helpers void start_attack(); @@ -291,6 +293,7 @@ private: pcm_cache m_cache; // cached data pcm_registers &m_regs; // reference to registers pcm_engine &m_owner; // reference to our owner + mutable int32_t m_output[4]; }; @@ -331,6 +334,8 @@ public: // return a reference to our registers pcm_registers ®s() { return m_regs; } + // simple getters for debugging + pcm_channel *debug_channel(uint32_t index) const { return m_channel[index].get(); } private: // internal state ymfm_interface &m_intf; // reference to the interface diff --git a/src/engine/platform/supervision.cpp b/src/engine/platform/supervision.cpp new file mode 100644 index 000000000..45439ec4f --- /dev/null +++ b/src/engine/platform/supervision.cpp @@ -0,0 +1,599 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "supervision.h" +#include "../engine.h" +#include "../../ta-log.h" +#include "furIcons.h" +#include + +//#define rWrite(a,v) pendingWrites[a]=v; +#define rWrite(a,v) if (!skipRegisterWrites) {writes.push(QueuedWrite(a,v)); if (dumpWrites) {addWrite(a,v);} } + +#define CHIP_DIVIDER 32 + +const char* regCheatSheetSupervision[]={ + "Freq0L", "10", + "Freq0H", "11", + "Control0", "12", + "Len0", "13", + + "Freq1L", "14", + "Freq1H", "15", + "Control1", "16", + "Len1", "17", + + "DMAstartL", "18", + "DMAstartH", "19", + "DMAlen", "1a", + "DMAstep", "1b", + "DMAon", "1c", + + "NoisDiv", "28", + "NoisLen", "29", + "NoisCtrl", "2a", + + NULL +}; + +const char** DivPlatformSupervision::getRegisterSheet() { + return regCheatSheetSupervision; +} + +void DivPlatformSupervision::acquire(short** buf, size_t len) { + int mask_bits=0; + for (int i=0; i<4; i++) + mask_bits |= isMuted[i]?0:8>>i; + supervision_set_mute_mask(&svision,mask_bits); + + for (size_t h=0; hdata[oscBuf[i]->needle++]=CLAMP((((int)s[2+i])-128)*256,-32768,32767); + } + + tempL[0]=(tempL[0]>>1)+(tempL[0]>>2); + tempR[0]=(tempR[0]>>1)+(tempR[0]>>2); + + if (tempL[0]<-32768) tempL[0]=-32768; + if (tempL[0]>32767) tempL[0]=32767; + if (tempR[0]<-32768) tempR[0]=-32768; + if (tempR[0]>32767) tempR[0]=32767; + + //printf("tempL: %d tempR: %d\n",tempL,tempR); + buf[0][h]=tempL[0]; + buf[1][h]=tempR[0]; + } +} + +void DivPlatformSupervision::tick(bool sysTick) { + for (int i=0; i<4; i++) { + + chan[i].std.next(); + if (chan[i].std.vol.had) { + chan[i].outVol=VOL_SCALE_LINEAR(chan[i].vol&15,MIN(15,chan[i].std.vol.val),15); + } + if (NEW_ARP_STRAT) { + chan[i].handleArp(); + } else if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + int f=parent->calcArp(chan[i].note,chan[i].std.arp.val); + if (i==2 || i==3) { + chan[i].baseFreq=f; + //if (chan[i].baseFreq>255) chan[i].baseFreq=255; + if (chan[i].baseFreq<0) chan[i].baseFreq=0; + } else { + chan[i].baseFreq=NOTE_PERIODIC(f); + } + } + chan[i].freqChanged=true; + } + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; + } + if (chan[i].std.panL.had) { + chan[i].pan=chan[i].std.panL.val&3; + } + if (chan[i].std.pitch.had) { + if (chan[i].std.pitch.mode) { + chan[i].pitch2+=chan[i].std.pitch.val; + CLAMP_VAR(chan[i].pitch2,-32768,32767); + } else { + chan[i].pitch2=chan[i].std.pitch.val; + } + chan[i].freqChanged=true; + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + //DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_PCE); + if (i<2) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,true,0,chan[i].pitch2,chipClock<<1,CHIP_DIVIDER); + if (chan[i].freq<1) chan[i].freq=1; + if (chan[i].freq>2047) chan[i].freq=2047; + if (chan[i].freqChanged || chan[i].initWrite) { + rWrite(0x10|(i<<2),chan[i].freq&0xff); + rWrite(0x11|(i<<2),(chan[i].freq>>8)&0x7); + } + chan[i].initWrite=false; + } else if (i==3) { + int ntPos=chan[i].baseFreq; + if (NEW_ARP_STRAT) { + if (chan[i].fixedArp) { + ntPos=chan[i].baseNoteOverride; + } else { + ntPos+=chan[i].arpOff; + } + } + ntPos+=chan[i].pitch2; + chan[i].freq=15-(ntPos&15); + unsigned char r=(chan[i].freq<<4)|(chan[i].outVol&0xf); + rWrite(0x28,r); + noiseReg[0]=r; + rWrite(0x29,0xc8); + r=((chan[i].duty&1)^dutySwap)|(0x02|0x10)|(chan[i].pan<<2); + rWrite(0x2A,r); + } + if (chan[i].keyOn && i==2) { + if (chan[i].pcm) { + int ntPos=chan[i].sampleNote; + ntPos+=chan[i].pitch2; + chan[i].freq=3-(ntPos&3); + int sNum=chan[i].sample; + DivSample* sample=parent->getSample(sNum); + if (sample!=NULL && sNum>=0 && sNumsong.sampleLen) { + unsigned int off=MIN(sampleOff[sNum]+chan[i].hasOffset,sampleOff[sNum]+sampleLen[sNum]); + unsigned int len=MAX((((int)sampleLen[sNum])-((int)chan[i].hasOffset)),0); + if (len) { + rWrite(0x18,off&0xff); + rWrite(0x19,(off>>8&0x3f)|0x80); + rWrite(0x1A,MIN(MAX(len>>4,0),255)); + rWrite(0x1B,chan[i].freq|((chan[i].pan&3)<<2)|((off>>14&7)<<4)); + rWrite(0x1C,0x80); + } + sampleOffset=chan[i].hasOffset; + chan[i].hasOffset=0; + } + } + } + if (chan[i].keyOff && i==2) { + if (chan[i].pcm) { + int ntPos=chan[i].sampleNote; + ntPos+=chan[i].pitch2; + chan[i].freq=3-(ntPos&3); + int sNum=chan[i].sample; + DivSample* sample=parent->getSample(sNum); + if (sample!=NULL && sNum>=0 && sNumsong.sampleLen) { + rWrite(0x1C,0x00); + } + } + } + if (chan[i].keyOn) chan[i].kon=true; + if (chan[i].keyOff) chan[i].kon=false; + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + + if (chan[i].kon) { + if (i<2) { + rWrite(0x12|(i<<2),(chan[i].outVol&0xf)|((chan[i].duty&3)<<4)); + rWrite(0x13|(i<<2),0xc8); + } else if (i == 3) { + int ntPos=chan[i].baseFreq; + if (NEW_ARP_STRAT) { + if (chan[i].fixedArp) { + ntPos=chan[i].baseNoteOverride; + } else { + ntPos+=chan[i].arpOff; + } + } + ntPos+=chan[i].pitch2; + chan[i].freq=15-(ntPos&15); + unsigned char r=(chan[i].freq<<4)|(chan[i].outVol&0xf); + if (noiseReg[0] != r) rWrite(0x28,r); + noiseReg[0]=r; + rWrite(0x29,0xc8); + r=((chan[i].duty&1)^dutySwap)|(0x02|0x10)|(chan[i].pan<<2); + if (noiseReg[2] != r) rWrite(0x2A,r); + noiseReg[2]=r; + } else if (i==2) { + if (chan[i].pcm) { + int ntPos=chan[i].sampleNote; + ntPos+=chan[i].pitch2; + chan[i].freq=3-(ntPos&3); + int sNum=chan[i].sample; + DivSample* sample=parent->getSample(sNum); + if (sample!=NULL && sNum>=0 && sNumsong.sampleLen) { + unsigned int off=MIN(sampleOff[sNum]+sampleOffset,sampleOff[sNum]+sampleLen[sNum]); + unsigned int len=MAX((((int)sampleLen[sNum])-((int)sampleOffset)),0); + if (len) { + rWrite(0x1A,MIN(MAX(len>>4,0),255)); + if (chan[i].outVol==0) { + rWrite(0x1B,chan[i].freq|((off>>14&7)<<4)); + } else { + rWrite(0x1B,chan[i].freq|((chan[i].pan&3)<<2)|((off>>14&7)<<4)); + } + } + } + } + } + } else { + if (i < 2) { + rWrite(0x12|(i<<2),0); + rWrite(0x13|(i<<2),0xc8); + } else if (i == 3) { + rWrite(0x29,0); + unsigned char r=0; + if (noiseReg[2] != r) rWrite(0x2A,r); + noiseReg[2]=r; + } + } + + } +} + +int DivPlatformSupervision::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_SUPERVISION); + if (ins->type==DIV_INS_AMIGA || ins->amiga.useSample) { + chan[c.chan].pcm=true; + } else { + chan[c.chan].pcm=false; + } + if (chan[c.chan].pcm) { + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].sample=ins->amiga.getSample(c.value); + chan[c.chan].sampleNote=c.value; + if (c.chan==2) { + c.value=ins->amiga.getFreq(c.value); + chan[c.chan].sampleNote=c.value; + } + } + } else { + chan[c.chan].sampleNote=DIV_NOTE_NULL; + chan[c.chan].sampleNoteDelta=0; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=c.chan==3?c.value:NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + //chwrite(c.chan,0x04,0x80|chan[c.chan].vol); + chan[c.chan].macroInit(ins); + if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + chan[c.chan].insChanged=false; + break; + } + case DIV_CMD_NOTE_OFF: + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].macroInit(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + if (chan[c.chan].active) { + //chwrite(c.chan,0x04,0x80|chan[c.chan].outVol); + } + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.vol.has) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2+chan[c.chan].sampleNoteDelta); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_STD_NOISE_MODE: + chan[c.chan].duty=c.value; + break; + case DIV_CMD_SAMPLE_POS: + chan[c.chan].hasOffset=c.value; + chan[c.chan].keyOn=true; + break; + case DIV_CMD_PANNING: { + chan[c.chan].pan=0; + if (c.value&0xf0) chan[c.chan].pan|=2; + if (c.value2>>4) chan[c.chan].pan|=1; + if (chan[c.chan].pan==0) chan[c.chan].pan=3; + break; + } + case DIV_CMD_LEGATO: + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+chan[c.chan].sampleNoteDelta+((HACKY_LEGATO_MESS)?(chan[c.chan].std.arp.val):(0))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_SUPERVISION)); + } + if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will && !NEW_ARP_STRAT) chan[c.chan].baseFreq=NOTE_PERIODIC(chan[c.chan].note); + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_CMD_MACRO_OFF: + chan[c.chan].std.mask(c.value,true); + break; + case DIV_CMD_MACRO_ON: + chan[c.chan].std.mask(c.value,false); + break; + case DIV_CMD_MACRO_RESTART: + chan[c.chan].std.restart(c.value); + break; + default: + break; + } + return 1; +} + +void DivPlatformSupervision::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; +} + +void DivPlatformSupervision::forceIns() { + for (int i=0; i<4; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + //chwrite(i,0x05,isMuted[i]?0:chan[i].pan); + } +} + +void* DivPlatformSupervision::getChanState(int ch) { + return &chan[ch]; +} + +DivMacroInt* DivPlatformSupervision::getChanMacroInt(int ch) { + return &chan[ch].std; +} + +DivDispatchOscBuffer* DivPlatformSupervision::getOscBuffer(int ch) { + return oscBuf[ch]; +} + +unsigned char* DivPlatformSupervision::getRegisterPool() { + return regPool; +} + +int DivPlatformSupervision::getRegisterPoolSize() { + return 64; +} + +void DivPlatformSupervision::reset() { + writes.clear(); + memset(regPool,0,64); + for (int i=0; i<4; i++) { + chan[i]=DivPlatformSupervision::Channel(); + chan[i].std.setEngine(parent); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + supervision_sound_reset(&svision); + memset(tempL,0,32*sizeof(int)); + memset(tempR,0,32*sizeof(int)); + memset(noiseReg,0,3*sizeof(unsigned char)); + noiseReg[2]=0xff; + sampleOffset=0; +} + +int DivPlatformSupervision::getOutputCount() { + return 2; +} + +bool DivPlatformSupervision::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformSupervision::notifyInsDeletion(void* ins) { + for (int i=0; i<4; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformSupervision::setFlags(const DivConfig& flags) { + if (flags.getInt("swapDuty",true)) { + dutySwap=1; + } else { + dutySwap=0; + } + otherFlags=0; + if (flags.getInt("sqStereo",false)) { + otherFlags |= 1; + } + chipClock=4000000; + CHECK_CUSTOM_CLOCK; + rate=chipClock/64; + for (int i=0; i<4; i++) { + oscBuf[i]->rate=rate; + } + supervision_sound_set_clock(&svision,(unsigned int)chipClock); + supervision_sound_set_flags(&svision,(unsigned int)otherFlags); +} + +void DivPlatformSupervision::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformSupervision::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +const void* DivPlatformSupervision::getSampleMem(int index) { + return index==0?sampleMem:NULL; +} + +size_t DivPlatformSupervision::getSampleMemCapacity(int index) { + return index==0?65536:0; +} + +size_t DivPlatformSupervision::getSampleMemUsage(int index) { + return index==0?sampleMemLen:0; +} + +bool DivPlatformSupervision::isSampleLoaded(int index, int sample) { + if (index!=0) return false; + if (sample<0 || sample>255) return false; + return sampleLoaded[sample]; +} + +const DivMemoryComposition* DivPlatformSupervision::getMemCompo(int index) { + if (index!=0) return NULL; + return &memCompo; +} + +void DivPlatformSupervision::renderSamples(int sysID) { + memset(sampleMem,0,getSampleMemCapacity(0)); + memset(sampleLoaded,0,256*sizeof(bool)); + + memCompo=DivMemoryComposition(); + memCompo.name="Sample Memory"; + + size_t memPos=0; + for (int i=0; isong.sampleLen; i++) { + DivSample* s=parent->song.sample[i]; + if (!s->renderOn[0][sysID]) { + sampleOff[i]=0; + continue; + } + unsigned int paddedLen=((s->length8>>1)+31)&(~0x1f); + logV("%d padded length: %d",i,paddedLen); + if (paddedLen>=4096) { + paddedLen=4096; + } + if ((memPos&(~0x3fff))!=((memPos+paddedLen)&(~0x3fff))) { + memPos=(memPos+0x3fff)&(~0x3fff); + } + if (memPos>=getSampleMemCapacity(0)) { + logW("out of memory for sample %d!",i); + break; + } + if (memPos+paddedLen>=getSampleMemCapacity(0)) { + sampleLen[i]=(getSampleMemCapacity(0)-memPos)>>1; + for (size_t i=0; i<(getSampleMemCapacity(0)-memPos)>>1; i++) { + sampleMem[memPos+i]=(((s->data8[i*2+0]+128)>>4)<<4&0xf0)|(((s->data8[i*2+1]+128)>>4)&0xf); + } + logW("out of memory for sample %d!",i); + } else { + size_t len=MIN((s->length8>>1),paddedLen); + sampleLen[i]=(unsigned int)(len); + for (size_t i=0; idata8[i*2+0]+128)>>4)<<4&0xf0)|(((s->data8[i*2+1]+128)>>4)&0xf); + } + sampleLoaded[i]=true; + } + sampleOff[i]=memPos; + memCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_SAMPLE,"Sample",i,memPos,memPos+paddedLen)); + memPos+=paddedLen; + } + sampleMemLen=memPos; + + memCompo.capacity=65536; + memCompo.used=sampleMemLen; +} + +bool DivPlatformSupervision::getDCOffRequired() { + return true; +} + +int DivPlatformSupervision::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<4; i++) { + isMuted[i]=false; + oscBuf[i]=new DivDispatchOscBuffer; + } + sampleMem=svision.supervision_dma_mem; + dutySwap=0; + otherFlags=0; + sampleOffset=0; + sampleMemLen=0; + memset(sampleMem,0,65536); + svision.ch_mask=15; + svision.flags=1; + setFlags(flags); + reset(); + return 4; +} + +void DivPlatformSupervision::quit() { + for (int i=0; i<4; i++) { + delete oscBuf[i]; + } +} + +DivPlatformSupervision::~DivPlatformSupervision() { +} diff --git a/src/engine/platform/supervision.h b/src/engine/platform/supervision.h new file mode 100644 index 000000000..8fb9dd550 --- /dev/null +++ b/src/engine/platform/supervision.h @@ -0,0 +1,104 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _SUPERVISION_H +#define _SUPERVISION_H + +#include "../dispatch.h" +#include "../../fixedQueue.h" +#include "sound/supervision.h" + +class DivPlatformSupervision: public DivDispatch { + struct Channel: public SharedChannel { + unsigned int duty, len, pan, pcm; // pcm is channel 3 ONLY + int sample, hasOffset; // again, for channel 3 ONLY + bool setPos, kon, initWrite; + Channel(): + SharedChannel(63), + duty(0), + len(0x1f), + pan(3), + pcm(false), + hasOffset(0), + setPos(false), + kon(false), + initWrite(true) {} + }; + Channel chan[4]; + DivDispatchOscBuffer* oscBuf[4]; + bool isMuted[4]; + struct QueuedWrite { + unsigned char addr; + unsigned char val; + QueuedWrite(): addr(0), val(9) {} + QueuedWrite(unsigned char a, unsigned char v): addr(a), val(v) {} + }; + FixedQueue writes; + + int curChan; + int tempL[32]; + int tempR[32]; + int coreQuality; + unsigned char regPool[64]; + unsigned int sampleOff[256]; + unsigned int sampleLen[256]; + bool sampleLoaded[256]; + DivMemoryComposition memCompo; + unsigned char* sampleMem; + size_t sampleMemLen; + unsigned char dutySwap; + unsigned char otherFlags; + unsigned int sampleOffset; + unsigned char noiseReg[3]; + struct svision_t svision; + + friend void putDispatchChip(void*,int); + friend void putDispatchChan(void*,int,int); + public: + void acquire(short** buf, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + DivMacroInt* getChanMacroInt(int ch); + DivDispatchOscBuffer* getOscBuffer(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(bool sysTick=true); + void muteChannel(int ch, bool mute); + int getOutputCount(); + bool keyOffAffectsArp(int ch); + void setFlags(const DivConfig& flags); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const void* getSampleMem(int index); + size_t getSampleMemCapacity(int index); + size_t getSampleMemUsage(int index); + bool isSampleLoaded(int index, int sample); + const DivMemoryComposition* getMemCompo(int index); + void renderSamples(int chipID); + bool getDCOffRequired(); + int init(DivEngine* parent, int channels, int sugRate, const DivConfig& flags); + void quit(); + ~DivPlatformSupervision(); +}; + +#endif diff --git a/src/engine/platform/upd1771c.cpp b/src/engine/platform/upd1771c.cpp new file mode 100644 index 000000000..d583771a7 --- /dev/null +++ b/src/engine/platform/upd1771c.cpp @@ -0,0 +1,366 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "upd1771c.h" +#include "../engine.h" +#include "../../ta-log.h" +#include "furIcons.h" +#include + +//#define rWrite(a,v) pendingWrites[a]=v; +#define rWrite(a,v) if (!skipRegisterWrites) {writes.push(QueuedWrite(a,v)); if (dumpWrites) {addWrite(a,v);} } + +#define CHIP_DIVIDER 64 + +const char* regCheatSheetUPD1771c[]={ + NULL +}; + +const char** DivPlatformUPD1771c::getRegisterSheet() { + return regCheatSheetUPD1771c; +} + +void DivPlatformUPD1771c::acquire(short** buf, size_t len) { + for (size_t h=0; hdata[oscBuf[0]->needle++]=s; + buf[0][h]=s; + buf[1][h]=s; + } +} + +void DivPlatformUPD1771c::tick(bool sysTick) { + for (int i=0; i<1; i++) { + + chan[i].std.next(); + if (chan[i].std.vol.had) { + chan[i].outVol=VOL_SCALE_LINEAR(chan[i].vol&31,MIN(31,chan[i].std.vol.val),31); + } + if (NEW_ARP_STRAT) { + chan[i].handleArp(); + } else if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + int f=parent->calcArp(chan[i].note,chan[i].std.arp.val); + chan[i].baseFreq=NOTE_PERIODIC(f); + } + chan[i].freqChanged=true; + } + if (chan[i].std.wave.had) { + chan[i].wave=chan[i].std.wave.val&7; + } + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val&1; + } + if (chan[i].std.ex1.had) { + chan[i].pos=chan[i].std.ex1.val&31; + } + if (chan[i].std.pitch.had) { + if (chan[i].std.pitch.mode) { + chan[i].pitch2+=chan[i].std.pitch.val; + CLAMP_VAR(chan[i].pitch2,-32768,32767); + } else { + chan[i].pitch2=chan[i].std.pitch.val; + } + chan[i].freqChanged=true; + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + //DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_PCE); + if (i==0) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,true,0,chan[i].pitch2,chipClock,CHIP_DIVIDER); + } + if (chan[i].freqChanged || initWrite[i] || chan[i].keyOn) { + if (chan[i].duty == 0) { + rWrite(0,2); + rWrite(1,(chan[i].wave<<5)|chan[i].pos); + float p = ((float)chan[i].freq)/((float)(31-chan[i].pos))*31.0; + rWrite(2,MIN(MAX((int)p,0),255)); + rWrite(3,chan[i].outVol); + } else if (chan[i].duty == 1) { + rWrite(0,1); + rWrite(1,(chan[i].wave<<5)); + rWrite(2,MIN(MAX(chan[i].freq>>7,0),255)); + rWrite(3,chan[i].outVol); + } else { + rWrite(0,0); + } + initWrite[i]=0; + } + if (chan[i].keyOff) { + rWrite(0,0); + } + if (chan[i].keyOn) kon[i]=1; + if (chan[i].keyOff) kon[i]=0; + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + + if (kon[i]) { + if (i==0) { + if (chan[i].duty == 0) { + rWrite(0,2); + rWrite(1,(chan[i].wave<<5)|chan[i].pos); + // TODO: improve + float p = ((float)chan[i].freq)/((float)(32-chan[i].pos))*32.0; + rWrite(2,MIN(MAX((int)p,0),255)); + rWrite(3,chan[i].outVol); + } else if (chan[i].duty == 1) { + rWrite(0,1); + rWrite(1,(chan[i].wave<<5)); + rWrite(2,MIN(MAX(chan[i].freq>>7,0),255)); + rWrite(3,chan[i].outVol); + } else { + rWrite(0,0); + } + } + } else { + if (i == 0) { + rWrite(0,0); + } + } + + } +} + +int DivPlatformUPD1771c::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_UPD1771C); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=c.chan==3?c.value:NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + //chwrite(c.chan,0x04,0x80|chan[c.chan].vol); + chan[c.chan].macroInit(ins); + if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + chan[c.chan].insChanged=false; + break; + } + case DIV_CMD_NOTE_OFF: + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].macroInit(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + if (chan[c.chan].active) { + //chwrite(c.chan,0x04,0x80|chan[c.chan].outVol); + } + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.vol.has) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_STD_NOISE_MODE: + chan[c.chan].wave=c.value&7; + chan[c.chan].duty=c.value>>4&1; + break; + case DIV_CMD_N163_WAVE_POSITION: + chan[c.chan].pos=c.value; + break; + case DIV_CMD_LEGATO: + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((HACKY_LEGATO_MESS)?(chan[c.chan].std.arp.val):(0))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_UPD1771C)); + } + if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will && !NEW_ARP_STRAT) chan[c.chan].baseFreq=NOTE_PERIODIC(chan[c.chan].note); + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 31; + break; + case DIV_CMD_MACRO_OFF: + chan[c.chan].std.mask(c.value,true); + break; + case DIV_CMD_MACRO_ON: + chan[c.chan].std.mask(c.value,false); + break; + case DIV_CMD_MACRO_RESTART: + chan[c.chan].std.restart(c.value); + break; + default: + break; + } + return 1; +} + +void DivPlatformUPD1771c::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; +} + +void DivPlatformUPD1771c::forceIns() { + for (int i=0; i<1; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + //chwrite(i,0x05,isMuted[i]?0:chan[i].pan); + } +} + +void* DivPlatformUPD1771c::getChanState(int ch) { + return &chan[ch]; +} + +DivMacroInt* DivPlatformUPD1771c::getChanMacroInt(int ch) { + return &chan[ch].std; +} + +DivDispatchOscBuffer* DivPlatformUPD1771c::getOscBuffer(int ch) { + return oscBuf[ch]; +} + +unsigned char* DivPlatformUPD1771c::getRegisterPool() { + return regPool; +} + +int DivPlatformUPD1771c::getRegisterPoolSize() { + return 16; +} + +void DivPlatformUPD1771c::reset() { + writes.clear(); + memset(regPool,0,16); + for (int i=0; i<1; i++) { + chan[i]=DivPlatformUPD1771c::Channel(); + chan[i].std.setEngine(parent); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + upd1771c_reset(&scv); + memset(tempL,0,32*sizeof(int)); + memset(tempR,0,32*sizeof(int)); + memset(kon,0,1*sizeof(unsigned char)); + memset(initWrite,1,1*sizeof(unsigned char)); +} + +int DivPlatformUPD1771c::getOutputCount() { + return 2; +} + +bool DivPlatformUPD1771c::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformUPD1771c::notifyInsDeletion(void* ins) { + for (int i=0; i<1; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformUPD1771c::setFlags(const DivConfig& flags) { + chipClock=6000000; + CHECK_CUSTOM_CLOCK; + rate=chipClock/32; + for (int i=0; i<1; i++) { + oscBuf[i]->rate=rate; + } + upd1771c_sound_set_clock(&scv,(unsigned int)chipClock,8); +} + +void DivPlatformUPD1771c::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformUPD1771c::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformUPD1771c::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<1; i++) { + isMuted[i]=false; + oscBuf[i]=new DivDispatchOscBuffer; + } + setFlags(flags); + reset(); + return 1; +} + +void DivPlatformUPD1771c::quit() { + for (int i=0; i<1; i++) { + delete oscBuf[i]; + } +} + +DivPlatformUPD1771c::~DivPlatformUPD1771c() { +} diff --git a/src/engine/platform/upd1771c.h b/src/engine/platform/upd1771c.h new file mode 100644 index 000000000..b225d342c --- /dev/null +++ b/src/engine/platform/upd1771c.h @@ -0,0 +1,84 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _UPD1771C_H +#define _UPD1771C_H + +#include "../dispatch.h" +#include "../../fixedQueue.h" +#include "sound/upd1771c.h" + +class DivPlatformUPD1771c: public DivDispatch { + struct Channel: public SharedChannel { + unsigned int wave; + int pos, duty; + Channel(): + SharedChannel(15), + wave(0), + pos(0), + duty(0) {} + }; + Channel chan[1]; + DivDispatchOscBuffer* oscBuf[1]; + bool isMuted[4]; + struct QueuedWrite { + unsigned char addr; + unsigned char val; + QueuedWrite(): addr(0), val(9) {} + QueuedWrite(unsigned char a, unsigned char v): addr(a), val(v) {} + }; + FixedQueue writes; + + int curChan; + int tempL[32]; + int tempR[32]; + int coreQuality; + unsigned char regPool[16]; + unsigned char kon[1]; + unsigned char initWrite[1]; + struct upd1771c_t scv; + + + friend void putDispatchChip(void*,int); + friend void putDispatchChan(void*,int,int); + public: + void acquire(short** buf, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + DivMacroInt* getChanMacroInt(int ch); + DivDispatchOscBuffer* getOscBuffer(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(bool sysTick=true); + void muteChannel(int ch, bool mute); + int getOutputCount(); + bool keyOffAffectsArp(int ch); + void setFlags(const DivConfig& flags); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + int init(DivEngine* parent, int channels, int sugRate, const DivConfig& flags); + void quit(); + ~DivPlatformUPD1771c(); +}; + +#endif diff --git a/src/engine/platform/ym2203.cpp b/src/engine/platform/ym2203.cpp index 2a19fea0f..63e3db25e 100644 --- a/src/engine/platform/ym2203.cpp +++ b/src/engine/platform/ym2203.cpp @@ -173,6 +173,7 @@ void DivPlatformYM2203::acquire_combo(short** buf, size_t len) { for (size_t h=0; h OPN ay->runDAC(); + ay->runTFX(rate); ay->flushWrites(); for (DivRegWrite& i: ay->getRegisterWrites()) { if (i.addr>15) continue; @@ -255,6 +256,7 @@ void DivPlatformYM2203::acquire_ymfm(short** buf, size_t len) { for (size_t h=0; h OPN ay->runDAC(); + ay->runTFX(rate); ay->flushWrites(); for (DivRegWrite& i: ay->getRegisterWrites()) { if (i.addr>15) continue; @@ -311,6 +313,16 @@ void DivPlatformYM2203::acquire_lle(short** buf, size_t len) { fmOut[i]=0; } + // AY -> OPN + ay->runDAC(); + ay->runTFX(rate); + ay->flushWrites(); + for (DivRegWrite& i: ay->getRegisterWrites()) { + if (i.addr>15) continue; + immWrite(i.addr&15,i.val); + } + ay->getRegisterWrites().clear(); + while (true) { bool canWeWrite=fm_lle.prescaler_latch[1]&1; @@ -444,6 +456,10 @@ void DivPlatformYM2203::acquire_lle(short** buf, size_t len) { } } +void DivPlatformYM2203::fillStream(std::vector& stream, int sRate, size_t len) { + ay->fillStream(stream,sRate,len); +} + void DivPlatformYM2203::tick(bool sysTick) { // PSG ay->tick(sysTick); @@ -1006,7 +1022,20 @@ int DivPlatformYM2203::dispatch(DivCommand c) { } case DIV_CMD_FM_OPMASK: if (c.chan>=psgChanOffs) break; - chan[c.chan].opMask=c.value&15; + switch (c.value>>4) { + case 1: + case 2: + case 3: + case 4: + chan[c.chan].opMask&=~(1<<((c.value>>4)-1)); + if (c.value&15) { + chan[c.chan].opMask|=(1<<((c.value>>4)-1)); + } + break; + default: + chan[c.chan].opMask=c.value&15; + break; + } if (chan[c.chan].active) { chan[c.chan].opMaskChanged=true; } diff --git a/src/engine/platform/ym2203.h b/src/engine/platform/ym2203.h index 1a2d0eb3e..baff93ebe 100644 --- a/src/engine/platform/ym2203.h +++ b/src/engine/platform/ym2203.h @@ -74,6 +74,7 @@ class DivPlatformYM2203: public DivPlatformOPN { public: void acquire(short** buf, size_t len); + void fillStream(std::vector& stream, int sRate, size_t len); int dispatch(DivCommand c); void* getChanState(int chan); DivMacroInt* getChanMacroInt(int ch); diff --git a/src/engine/platform/ym2608.cpp b/src/engine/platform/ym2608.cpp index 046e20c20..29cf262ea 100644 --- a/src/engine/platform/ym2608.cpp +++ b/src/engine/platform/ym2608.cpp @@ -325,6 +325,7 @@ void DivPlatformYM2608::acquire_combo(short** buf, size_t len) { for (size_t h=0; h OPN ay->runDAC(); + ay->runTFX(rate); ay->flushWrites(); for (DivRegWrite& i: ay->getRegisterWrites()) { if (i.addr>15) continue; @@ -440,6 +441,7 @@ void DivPlatformYM2608::acquire_ymfm(short** buf, size_t len) { for (size_t h=0; h OPN ay->runDAC(); + ay->runTFX(rate); ay->flushWrites(); for (DivRegWrite& i: ay->getRegisterWrites()) { if (i.addr>15) continue; @@ -680,6 +682,10 @@ void DivPlatformYM2608::acquire_lle(short** buf, size_t len) { } } +void DivPlatformYM2608::fillStream(std::vector& stream, int sRate, size_t len) { + ay->fillStream(stream,sRate,len); +} + void DivPlatformYM2608::tick(bool sysTick) { // FM for (int i=0; i<6; i++) { @@ -1539,7 +1545,20 @@ int DivPlatformYM2608::dispatch(DivCommand c) { } case DIV_CMD_FM_OPMASK: if (c.chan>=psgChanOffs) break; - chan[c.chan].opMask=c.value&15; + switch (c.value>>4) { + case 1: + case 2: + case 3: + case 4: + chan[c.chan].opMask&=~(1<<((c.value>>4)-1)); + if (c.value&15) { + chan[c.chan].opMask|=(1<<((c.value>>4)-1)); + } + break; + default: + chan[c.chan].opMask=c.value&15; + break; + } if (chan[c.chan].active) { chan[c.chan].opMaskChanged=true; } diff --git a/src/engine/platform/ym2608.h b/src/engine/platform/ym2608.h index 2b92ae5e0..c4114e3d7 100644 --- a/src/engine/platform/ym2608.h +++ b/src/engine/platform/ym2608.h @@ -93,6 +93,7 @@ class DivPlatformYM2608: public DivPlatformOPN { public: void acquire(short** buf, size_t len); + void fillStream(std::vector& stream, int sRate, size_t len); int dispatch(DivCommand c); void* getChanState(int chan); DivMacroInt* getChanMacroInt(int ch); diff --git a/src/engine/platform/ym2610.cpp b/src/engine/platform/ym2610.cpp index 038cbc807..ff1a7af46 100644 --- a/src/engine/platform/ym2610.cpp +++ b/src/engine/platform/ym2610.cpp @@ -260,6 +260,7 @@ void DivPlatformYM2610::acquire_combo(short** buf, size_t len) { for (size_t h=0; h OPN ay->runDAC(); + ay->runTFX(rate); ay->flushWrites(); for (DivRegWrite& i: ay->getRegisterWrites()) { if (i.addr>15) continue; @@ -373,6 +374,7 @@ void DivPlatformYM2610::acquire_ymfm(short** buf, size_t len) { for (size_t h=0; h OPN ay->runDAC(); + ay->runTFX(rate); ay->flushWrites(); for (DivRegWrite& i: ay->getRegisterWrites()) { if (i.addr>15) continue; @@ -1509,7 +1511,20 @@ int DivPlatformYM2610::dispatch(DivCommand c) { } case DIV_CMD_FM_OPMASK: if (c.chan>=psgChanOffs) break; - chan[c.chan].opMask=c.value&15; + switch (c.value>>4) { + case 1: + case 2: + case 3: + case 4: + chan[c.chan].opMask&=~(1<<((c.value>>4)-1)); + if (c.value&15) { + chan[c.chan].opMask|=(1<<((c.value>>4)-1)); + } + break; + default: + chan[c.chan].opMask=c.value&15; + break; + } if (chan[c.chan].active) { chan[c.chan].opMaskChanged=true; } diff --git a/src/engine/platform/ym2610b.cpp b/src/engine/platform/ym2610b.cpp index 8b12ca862..51e0de088 100644 --- a/src/engine/platform/ym2610b.cpp +++ b/src/engine/platform/ym2610b.cpp @@ -324,6 +324,7 @@ void DivPlatformYM2610B::acquire_combo(short** buf, size_t len) { for (size_t h=0; h OPN ay->runDAC(); + ay->runTFX(rate); ay->flushWrites(); for (DivRegWrite& i: ay->getRegisterWrites()) { if (i.addr>15) continue; @@ -439,6 +440,7 @@ void DivPlatformYM2610B::acquire_ymfm(short** buf, size_t len) { for (size_t h=0; h OPN ay->runDAC(); + ay->runTFX(rate); ay->flushWrites(); for (DivRegWrite& i: ay->getRegisterWrites()) { if (i.addr>15) continue; @@ -1578,7 +1580,20 @@ int DivPlatformYM2610B::dispatch(DivCommand c) { } case DIV_CMD_FM_OPMASK: if (c.chan>=psgChanOffs) break; - chan[c.chan].opMask=c.value&15; + switch (c.value>>4) { + case 1: + case 2: + case 3: + case 4: + chan[c.chan].opMask&=~(1<<((c.value>>4)-1)); + if (c.value&15) { + chan[c.chan].opMask|=(1<<((c.value>>4)-1)); + } + break; + default: + chan[c.chan].opMask=c.value&15; + break; + } if (chan[c.chan].active) { chan[c.chan].opMaskChanged=true; } diff --git a/src/engine/platform/ym2610shared.h b/src/engine/platform/ym2610shared.h index 056639cda..08977d0d1 100644 --- a/src/engine/platform/ym2610shared.h +++ b/src/engine/platform/ym2610shared.h @@ -108,6 +108,10 @@ class DivPlatformYM2610Base: public DivPlatformOPN { } public: + void fillStream(std::vector& stream, int sRate, size_t len) { + ay->fillStream(stream,sRate,len); + } + void reset() { writeADPCMAOff=0; writeADPCMAOn=0; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 0bae875dc..89207ce54 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -268,6 +268,22 @@ const char* cmdName[]={ "FM_OPMASK", + "MULTIPCM_MIX_FM", + "MULTIPCM_MIX_PCM", + "MULTIPCM_LFO", + "MULTIPCM_VIB", + "MULTIPCM_AM", + "MULTIPCM_AR", + "MULTIPCM_D1R", + "MULTIPCM_DL", + "MULTIPCM_D2R", + "MULTIPCM_RR", + "MULTIPCM_RC", + "MULTIPCM_DAMP", + "MULTIPCM_PSEUDO_REVERB", + "MULTIPCM_LFO_RESET", + "MULTIPCM_LEVEL_DIRECT", + "SID3_SPECIAL_WAVE", "SID3_RING_MOD_SRC", "SID3_HARD_SYNC_SRC", @@ -289,7 +305,7 @@ const char* cmdName[]={ "SID3_NOISE_PHASE_RESET", "SID3_ENVELOPE_RESET", "SID3_CUTOFF_SCALING", - "SID3_RESONANCE_SCALING", + "SID3_RESONANCE_SCALING" }; static_assert((sizeof(cmdName)/sizeof(void*))==DIV_CMD_MAX,"update cmdName!"); @@ -559,7 +575,7 @@ void DivEngine::processRow(int i, bool afterDelay) { bool comparison=(song.delayBehavior==1)?(effectVal<=nextSpeed):(effectVal<(nextSpeed*(curSubSong->timeBase+1))); if (song.delayBehavior==2) comparison=true; if (comparison) { - chan[i].rowDelay=effectVal+1; + chan[i].rowDelay=effectVal; chan[i].delayOrder=whatOrder; chan[i].delayRow=whatRow; if (effectVal==nextSpeed) { @@ -1603,6 +1619,19 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { if (--subticks<=0) { subticks=tickMult; + // apply delayed rows before potentially advancing to a new row, which would overwrite the + // delayed row's state before it has a chance to do anything. a typical example would be + // a delay scheduling a note-on to be simultaneous with the next row, and the next row also + // containing a delayed note. if we don't apply the delayed row first, + for (int i=0; i0) { + if (--chan[i].rowDelay==0) { + processRow(i,true); + } + } + } + if (stepPlay!=1) { tempoAccum+=(skipping && virtualTempoN=virtualTempoD) { @@ -1633,15 +1662,9 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { // under no circumstances shall the accumulator become this large if (tempoAccum>1023) tempoAccum=1023; } + // process stuff if (!shallStop) for (int i=0; i0) { - if (--chan[i].rowDelay==0) { - processRow(i,true); - } - } - // retrigger if (chan[i].retrigSpeed) { if (--chan[i].retrigTick<0) { @@ -1655,6 +1678,7 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { if (!song.noSlidesOnFirstTick || !firstTick) { if (chan[i].volSpeed!=0) { chan[i].volume=(chan[i].volume&0xff)|(dispatchCmd(DivCommand(DIV_CMD_GET_VOLUME,i))<<8); + int preSpeedVol=chan[i].volume; chan[i].volume+=chan[i].volSpeed; if (chan[i].volSpeedTarget!=-1) { bool atTarget=false; @@ -1668,7 +1692,11 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { } if (atTarget) { - chan[i].volume=chan[i].volSpeedTarget; + if (chan[i].volSpeed>0) { + chan[i].volume=MAX(preSpeedVol,chan[i].volSpeedTarget); + } else if (chan[i].volSpeed<0) { + chan[i].volume=MIN(preSpeedVol,chan[i].volSpeedTarget); + } chan[i].volSpeed=0; chan[i].volSpeedTarget=-1; dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); diff --git a/src/engine/sample.cpp b/src/engine/sample.cpp index 020e0dec7..e51558190 100644 --- a/src/engine/sample.cpp +++ b/src/engine/sample.cpp @@ -285,6 +285,9 @@ int DivSample::getSampleOffset(int offset, int length, DivSampleDepth depth) { case DIV_SAMPLE_DEPTH_IMA_ADPCM: off=(offset+1)/2; break; + case DIV_SAMPLE_DEPTH_12BIT: + off=((offset*3)+1)/2; + break; case DIV_SAMPLE_DEPTH_16BIT: off=offset*2; break; @@ -348,6 +351,10 @@ int DivSample::getSampleOffset(int offset, int length, DivSampleDepth depth) { off=(offset+1)/2; len=(length+1)/2; break; + case DIV_SAMPLE_DEPTH_12BIT: + off=((offset*3)+1)/2; + len=((length*3)+1)/2; + break; case DIV_SAMPLE_DEPTH_16BIT: off=offset*2; len=length*2; @@ -409,6 +416,9 @@ int DivSample::getEndPosition(DivSampleDepth depth) { case DIV_SAMPLE_DEPTH_IMA_ADPCM: off=lengthIMA; break; + case DIV_SAMPLE_DEPTH_12BIT: + off=length12; + break; case DIV_SAMPLE_DEPTH_16BIT: off=length16; break; @@ -606,6 +616,12 @@ bool DivSample::initInternal(DivSampleDepth d, int count) { dataIMA=new unsigned char[lengthIMA]; memset(dataIMA,0,lengthIMA); break; + case DIV_SAMPLE_DEPTH_12BIT: // 12-bit PCM (MultiPCM) + if (data12!=NULL) delete[] data12; + length12=((count*3)+1)/2; + data12=new unsigned char[length12]; + memset(data12,0,length12); + break; case DIV_SAMPLE_DEPTH_16BIT: // 16-bit if (data16!=NULL) delete[] data16; length16=count*2; @@ -1293,6 +1309,14 @@ void DivSample::render(unsigned int formatMask) { case DIV_SAMPLE_DEPTH_IMA_ADPCM: // IMA ADPCM if (adpcm_decode_block(data16,dataIMA,lengthIMA,samples)==0) logE("oh crap!"); break; + case DIV_SAMPLE_DEPTH_12BIT: // 12-bit PCM (MultiPCM) + for (unsigned int i=0, j=0; i>8; + data12[j+1]=((data16[i+0]>>4)&0xf)|(i+1>4)&0xf:0); + if (i+1>8; + } + } + } } void* DivSample::getCurBuf() { @@ -1512,6 +1546,8 @@ void* DivSample::getCurBuf() { return dataC219; case DIV_SAMPLE_DEPTH_IMA_ADPCM: return dataIMA; + case DIV_SAMPLE_DEPTH_12BIT: + return data12; case DIV_SAMPLE_DEPTH_16BIT: return data16; default: @@ -1548,6 +1584,8 @@ unsigned int DivSample::getCurBufLen() { return lengthC219; case DIV_SAMPLE_DEPTH_IMA_ADPCM: return lengthIMA; + case DIV_SAMPLE_DEPTH_12BIT: + return length12; case DIV_SAMPLE_DEPTH_16BIT: return length16; default: @@ -1662,4 +1700,5 @@ DivSample::~DivSample() { if (dataMuLaw) delete[] dataMuLaw; if (dataC219) delete[] dataC219; if (dataIMA) delete[] dataIMA; + if (data12) delete[] data12; } diff --git a/src/engine/sample.h b/src/engine/sample.h index 5fc593d23..c585c7e32 100644 --- a/src/engine/sample.h +++ b/src/engine/sample.h @@ -47,6 +47,7 @@ enum DivSampleDepth: unsigned char { DIV_SAMPLE_DEPTH_MULAW=11, DIV_SAMPLE_DEPTH_C219=12, DIV_SAMPLE_DEPTH_IMA_ADPCM=13, + DIV_SAMPLE_DEPTH_12BIT=14, DIV_SAMPLE_DEPTH_16BIT=16, DIV_SAMPLE_DEPTH_MAX // boundary for sample depth }; @@ -118,6 +119,7 @@ struct DivSample { // - 11: 8-bit µ-law PCM // - 12: C219 "µ-law" PCM // - 13: IMA ADPCM + // - 14: 12-bit PCM (MultiPCM) // - 16: 16-bit PCM DivSampleDepth depth; bool loop, brrEmphasis, brrNoFilter, dither; @@ -144,8 +146,9 @@ struct DivSample { unsigned char* dataMuLaw; // 11 unsigned char* dataC219; // 12 unsigned char* dataIMA; // 13 + unsigned char* data12; // 14 - unsigned int length8, length16, length1, lengthDPCM, lengthZ, lengthQSoundA, lengthA, lengthB, lengthK, lengthBRR, lengthVOX, lengthMuLaw, lengthC219, lengthIMA; + unsigned int length8, length16, length1, lengthDPCM, lengthZ, lengthQSoundA, lengthA, lengthB, lengthK, lengthBRR, lengthVOX, lengthMuLaw, lengthC219, lengthIMA, length12; unsigned int samples; @@ -356,6 +359,7 @@ struct DivSample { dataMuLaw(NULL), dataC219(NULL), dataIMA(NULL), + data12(NULL), length8(0), length16(0), length1(0), @@ -370,6 +374,7 @@ struct DivSample { lengthMuLaw(0), lengthC219(0), lengthIMA(0), + length12(0), samples(0) { for (int i=0; i, effectValAnd<1>}}, }; + EffectHandlerMap fmOPL4PostEffectHandlerMap(fmOPLPostEffectHandlerMap); + fmOPL4PostEffectHandlerMap.insert({ + {0x1e, {DIV_CMD_MULTIPCM_MIX_FM, _("1Exy: FM global level (x: left, y: right; 0 to 7)"), effectVal}}, + {0x1f, {DIV_CMD_MULTIPCM_MIX_PCM, _("1Fxy: PCM global level (x: left, y: right; 0 to 7)"), effectVal}}, + {0x20, {DIV_CMD_MULTIPCM_LFO, _("20xx: PCM LFO Rate (0 to 7)"), effectValAnd<7>}}, + {0x21, {DIV_CMD_MULTIPCM_VIB, _("21xx: PCM LFO PM Depth (0 to 7)"), effectValAnd<7>}}, + {0x22, {DIV_CMD_MULTIPCM_AM, _("22xx: PCM LFO AM Depth (0 to 7)"), effectValAnd<7>}}, + {0x23, {DIV_CMD_MULTIPCM_AR, _("23xx: PCM Attack Rate (0 to 15)"), effectValAnd<15>}}, + {0x24, {DIV_CMD_MULTIPCM_D1R, _("24xx: PCM Decay 1 Rate (0 to 15)"), effectValAnd<15>}}, + {0x25, {DIV_CMD_MULTIPCM_DL, _("25xx: PCM Decay Level (0 to 15)"), effectValAnd<15>}}, + {0x26, {DIV_CMD_MULTIPCM_D2R, _("26xx: PCM Decay 2 Rate (0 to 15)"), effectValAnd<15>}}, + {0x27, {DIV_CMD_MULTIPCM_RR, _("27xx: PCM Release Rate (0 to 15)"), effectValAnd<15>}}, + {0x28, {DIV_CMD_MULTIPCM_RC, _("28xx: PCM Rate Correction (0 to 15)"), effectValAnd<15>}}, + {0x2c, {DIV_CMD_MULTIPCM_DAMP, _("2Cxx: PCM Damp"), effectValAnd<1>}}, + {0x2d, {DIV_CMD_MULTIPCM_PSEUDO_REVERB, _("2Dxx: PCM Pseudo Reverb"), effectValAnd<1>}}, + {0x2e, {DIV_CMD_MULTIPCM_LFO_RESET, _("2Exx: PCM LFO Reset"), effectValAnd<1>}}, + {0x2f, {DIV_CMD_MULTIPCM_LEVEL_DIRECT, _("2Fxx: PCM Level Direct"), effectValAnd<1>}}, + }); + EffectHandlerMap c64PostEffectHandlerMap={ {0x10, {DIV_CMD_WAVE, _("10xx: Set waveform (bit 0: triangle; bit 1: saw; bit 2: pulse; bit 3: noise)")}}, {0x11, {DIV_CMD_C64_CUTOFF, _("11xx: Set coarse cutoff (not recommended; use 4xxx instead)")}}, @@ -1709,24 +1728,28 @@ void DivEngine::registerSystems() { ); // to Grauw: feel free to change this to 24 during development of OPL4's PCM part. - // TODO: add 12-bit and 16-bit big-endian sample formats sysDefs[DIV_SYSTEM_OPL4]=new DivSysDef( - _("Yamaha YMF278B (OPL4)"), NULL, 0xae, 0, 42, true, true, 0, false, (1U<writeC(0); } break; + case DIV_SYSTEM_OPL4: + case DIV_SYSTEM_OPL4_DRUMS: + // disable envelope + for (int i=0; i<6; i++) { + w->writeC(0xd0); + w->writeC(0x00|baseAddr2); + w->writeC(0x80+i); + w->writeC(0x0f); + w->writeC(0xd0); + w->writeC(0x00|baseAddr2); + w->writeC(0x88+i); + w->writeC(0x0f); + w->writeC(0xd0); + w->writeC(0x00|baseAddr2); + w->writeC(0x90+i); + w->writeC(0x0f); + w->writeC(0xd0); + w->writeC(0x01|baseAddr2); + w->writeC(0x80+i); + w->writeC(0x0f); + w->writeC(0xd0); + w->writeC(0x01|baseAddr2); + w->writeC(0x88+i); + w->writeC(0x0f); + w->writeC(0xd0); + w->writeC(0x01|baseAddr2); + w->writeC(0x90+i); + w->writeC(0x0f); + } + for (int i=0; i<24; i++) { + w->writeC(0xd0); + w->writeC(0x02|baseAddr2); + w->writeC(0x80+i); + w->writeC(0x00); + w->writeC(0xd0); + w->writeC(0x02|baseAddr2); + w->writeC(0x98+i); + w->writeC(0x00); + w->writeC(0xd0); + w->writeC(0x02|baseAddr2); + w->writeC(0xb0+i); + w->writeC(0x00); + w->writeC(0xd0); + w->writeC(0x02|baseAddr2); + w->writeC(0xc8+i); + w->writeC(0x00); + w->writeC(0xd0); + w->writeC(0x02|baseAddr2); + w->writeC(0xe0+i); + w->writeC(0x00); + } + // key off + freq reset + for (int i=0; i<9; i++) { + w->writeC(0xd0); + w->writeC(0x00|baseAddr2); + w->writeC(0xa0+i); + w->writeC(0); + w->writeC(0xd0); + w->writeC(0x00|baseAddr2); + w->writeC(0xb0+i); + w->writeC(0); + w->writeC(0xd0); + w->writeC(0x01|baseAddr2); + w->writeC(0xa0+i); + w->writeC(0); + w->writeC(0xd0); + w->writeC(0x01|baseAddr2); + w->writeC(0xb0+i); + w->writeC(0); + } + for (int i=0; i<24; i++) { + w->writeC(0xd0); + w->writeC(0x02|baseAddr2); + w->writeC(0x20+i); + w->writeC(0); + w->writeC(0xd0); + w->writeC(0x02|baseAddr2); + w->writeC(0x38+i); + w->writeC(0); + w->writeC(0xd0); + w->writeC(0x02|baseAddr2); + w->writeC(0x68+i); + w->writeC(8); + } + // reset 4-op + w->writeC(0xd0); + w->writeC(0x01|baseAddr2); + w->writeC(0x04); + w->writeC(0x00); + break; default: break; } @@ -1096,6 +1186,13 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeS_BE(baseAddr2S|(write.addr&0x1ff)); w->writeC(write.val&0xff); break; + case DIV_SYSTEM_OPL4: + case DIV_SYSTEM_OPL4_DRUMS: + w->writeC(0xd0); + w->writeC(((write.addr>>8)&0x7f)|baseAddr2); + w->writeC(write.addr&0xff); + w->writeC(write.val); + break; default: logW("write not handled!"); break; @@ -1276,6 +1373,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p DivDispatch* writeC140[2]={NULL,NULL}; DivDispatch* writeC219[2]={NULL,NULL}; DivDispatch* writeNES[2]={NULL,NULL}; + DivDispatch* writePCM_OPL4[2]={NULL,NULL}; int writeNESIndex[2]={0,0}; @@ -1874,6 +1972,22 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p howManyChips++; } break; + case DIV_SYSTEM_OPL4: + case DIV_SYSTEM_OPL4_DRUMS: + if (!hasOPL4) { + hasOPL4=disCont[i].dispatch->chipClock; + CHIP_VOL(13,1.0); + willExport[i]=true; + writePCM_OPL4[0]=disCont[i].dispatch; + } else if (!(hasOPL4&0x40000000)) { + isSecond[i]=true; + CHIP_VOL_SECOND(13,1.0); + willExport[i]=true; + writePCM_OPL4[1]=disCont[i].dispatch; + hasOPL4|=0x40000000; + howManyChips++; + } + break; default: break; } @@ -2209,6 +2323,16 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p w->write(sampleMem,sampleMemLen); delete[] sampleMem; } + // PCM (OPL4) + if (writePCM_OPL4[i]!=NULL && writePCM_OPL4[i]->getSampleMemUsage(0)>0) { + w->writeC(0x67); + w->writeC(0x66); + w->writeC(0x84); + w->writeI((writePCM_OPL4[i]->getSampleMemUsage(0)+8)|(i*0x80000000)); + w->writeI(writePCM_OPL4[i]->getSampleMemCapacity(0)); + w->writeI(0); + w->write(writePCM_OPL4[i]->getSampleMem(0),writePCM_OPL4[i]->getSampleMemUsage(0)); + } } for (int i=0; i<2; i++) { diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 75da58875..e0677be01 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -300,6 +300,7 @@ const char* aboutLine[]={ _N("PowerNoise emulator by scratchminer"), _N("ep128emu by Istvan Varga"), _N("NDS sound emulator by cam900"), + _N("openMSX YMF278 emulator (modified version) by the openMSX developers"), _N("SID2 emulator by LTVA (modification of reSID emulator)"), _N("SID3 emulator by LTVA"), "", diff --git a/src/gui/cursor.cpp b/src/gui/cursor.cpp index 64f7114b3..de8f7b8c4 100644 --- a/src/gui/cursor.cpp +++ b/src/gui/cursor.cpp @@ -69,6 +69,9 @@ void FurnaceGUI::startSelection(int xCoarse, int xFine, int y, bool fullRow) { selStart.y=y; selEnd.y=y; } else { + if (xCoarse!=cursor.xCoarse || y!=cursor.y) { + makeCursorUndo(); + } cursor.xCoarse=xCoarse; cursor.xFine=xFine; cursor.y=y; @@ -208,6 +211,9 @@ void FurnaceGUI::finishSelection() { } void FurnaceGUI::moveCursor(int x, int y, bool select) { + if (y>=editStepCoarse || y<=-editStepCoarse || x<=-5 || x>=5 ) { + makeCursorUndo(); + } if (!select) { finishSelection(); } @@ -326,6 +332,7 @@ void FurnaceGUI::moveCursor(int x, int y, bool select) { } void FurnaceGUI::moveCursorPrevChannel(bool overflow) { + makeCursorUndo(); finishSelection(); curNibble=false; @@ -354,6 +361,7 @@ void FurnaceGUI::moveCursorPrevChannel(bool overflow) { } void FurnaceGUI::moveCursorNextChannel(bool overflow) { + makeCursorUndo(); finishSelection(); curNibble=false; @@ -382,6 +390,7 @@ void FurnaceGUI::moveCursorNextChannel(bool overflow) { } void FurnaceGUI::moveCursorTop(bool select) { + makeCursorUndo(); if (!select) { finishSelection(); } @@ -403,6 +412,7 @@ void FurnaceGUI::moveCursorTop(bool select) { } void FurnaceGUI::moveCursorBottom(bool select) { + makeCursorUndo(); if (!select) { finishSelection(); } diff --git a/src/gui/debug.cpp b/src/gui/debug.cpp index 3cb6b4c13..141ce50b2 100644 --- a/src/gui/debug.cpp +++ b/src/gui/debug.cpp @@ -51,6 +51,7 @@ #include "../engine/platform/pcmdac.h" #include "../engine/platform/k007232.h" #include "../engine/platform/ga20.h" +#include "../engine/platform/supervision.h" #include "../engine/platform/sm8521.h" #include "../engine/platform/pv1000.h" #include "../engine/platform/k053260.h" diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index b28126fc5..d19bfce22 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -731,6 +731,25 @@ void FurnaceGUI::drawDebug() { ImGui::Text("result: %.0f%%",realVol*100.0f); ImGui::TreePop(); } + if (ImGui::TreeNode("Cursor Undo Debug")) { + auto DrawSpot=[&](const CursorJumpPoint& spot) { + ImGui::Text("[%d:%d] <%d:%d, %d>", spot.subSong, spot.order, spot.point.xCoarse, spot.point.xFine, spot.point.y); + }; + if (ImGui::BeginChild("##CursorUndoDebugChild", ImVec2(0, 300), true)) { + if (ImGui::BeginTable("##CursorUndoDebug", 2, ImGuiTableFlags_Borders|ImGuiTableFlags_SizingStretchSame)) { + for (size_t row=0; row=e->curSubSong->patLen) cursor.y=e->curSubSong->patLen-1; selStart=cursor; @@ -1220,6 +1221,7 @@ void FurnaceGUI::doPasteMPT(PasteMode mode, int arg, bool readClipboard, String if (readClipboard) { if (settings.cursorPastePos) { + makeCursorUndo(); cursor.y=j; if (cursor.y>=e->curSubSong->patLen) cursor.y=e->curSubSong->patLen-1; selStart=cursor; @@ -1846,8 +1848,11 @@ void FurnaceGUI::doAbsorbInstrument() { } // absorb most recent octave (i.e. set curOctave such that the "main row" (QWERTY) of - // notes will result in an octave number equal to the previous note). - if (!foundOctave && pat->data[i][0] != 0) { + // notes will result in an octave number equal to the previous note). make sure to + // skip "special note values" like OFF/REL/=== and "none", since there won't be valid + // octave values + unsigned char note=pat->data[i][0]; + if (!foundOctave && note!=0 && note!=100 && note!=101 && note!=102) { foundOctave=true; // decode octave data (was signed cast to unsigned char) @@ -2058,3 +2063,52 @@ void FurnaceGUI::doRedo() { redoHist.pop_back(); } + +CursorJumpPoint FurnaceGUI::getCurrentCursorJumpPoint() { + return CursorJumpPoint(cursor, curOrder, e->getCurrentSubSong()); +} + +void FurnaceGUI::applyCursorJumpPoint(const CursorJumpPoint& spot) { + cursor=spot.point; + curOrder=MIN(e->curSubSong->ordersLen-1, spot.order); + e->setOrder(curOrder); + e->changeSongP(spot.subSong); + if (!settings.cursorMoveNoScroll) { + updateScroll(cursor.y); + } +} + +void FurnaceGUI::makeCursorUndo() { + CursorJumpPoint spot = getCurrentCursorJumpPoint(); + if (!cursorUndoHist.empty() && spot == cursorUndoHist.back()) return; + + if (cursorUndoHist.size()>=settings.maxUndoSteps) cursorUndoHist.pop_front(); + cursorUndoHist.push_back(spot); + + // redo history no longer relevant, we've changed timeline + cursorRedoHist.clear(); +} + +void FurnaceGUI::doCursorUndo() { + if (cursorUndoHist.empty()) return; + + // allow returning to current spot + if (cursorRedoHist.size()>=settings.maxUndoSteps) cursorRedoHist.pop_front(); + cursorRedoHist.push_back(getCurrentCursorJumpPoint()); + + // apply spot + applyCursorJumpPoint(cursorUndoHist.back()); + cursorUndoHist.pop_back(); +} + +void FurnaceGUI::doCursorRedo() { +if (cursorRedoHist.empty()) return; + + // allow returning to current spot + if (cursorUndoHist.size()>=settings.maxUndoSteps) cursorUndoHist.pop_front(); + cursorUndoHist.push_back(getCurrentCursorJumpPoint()); + + // apply spot + applyCursorJumpPoint(cursorRedoHist.back()); + cursorRedoHist.pop_back(); +} \ No newline at end of file diff --git a/src/gui/findReplace.cpp b/src/gui/findReplace.cpp index 711f15e62..43eeb9625 100644 --- a/src/gui/findReplace.cpp +++ b/src/gui/findReplace.cpp @@ -560,6 +560,7 @@ void FurnaceGUI::drawFindReplace() { if (ImGui::TableNextColumn()) { snprintf(tempID,1024,ICON_FA_CHEVRON_RIGHT "##_FR%d",index); if (ImGui::Selectable(tempID)) { + makeCursorUndo(); e->changeSongP(i.subsong); if (e->isPlaying()) { followPattern=false; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 7f26b0f83..ac2e59fe4 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -388,6 +388,7 @@ void FurnaceGUI::decodeMMLStr(String& source, int* macro, unsigned char& macroLe setBit30=false; macroLen++; buf=0; + MARK_MODIFIED; } } @@ -424,6 +425,21 @@ void FurnaceGUI::decodeMMLStr(String& source, int* macro, unsigned char& macroLe } \ } +bool FurnaceGUI::isCtrlWheelModifierHeld() const { + switch (settings.ctrlWheelModifier) { + case 0: + return ImGui::IsKeyDown(ImGuiMod_Ctrl) || ImGui::IsKeyDown(ImGuiMod_Super); + case 1: + return ImGui::IsKeyDown(ImGuiMod_Ctrl); + case 2: + return ImGui::IsKeyDown(ImGuiMod_Super); + case 3: + return ImGui::IsKeyDown(ImGuiMod_Alt); + default: + return false; + } +} + bool FurnaceGUI::CWSliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { flags^=ImGuiSliderFlags_AlwaysClamp; if (ImGui::SliderScalar(label,data_type,p_data,p_min,p_max,format,flags)) { @@ -993,11 +1009,6 @@ Pos=339,177\n\ Size=601,400\n\ Collapsed=0\n\ \n\ -[Window][Rendering...]\n\ -Pos=585,342\n\ -Size=600,100\n\ -Collapsed=0\n\ -\n\ [Window][Export VGM##FileDialog]\n\ Pos=340,177\n\ Size=600,400\n\ @@ -1216,6 +1227,7 @@ void FurnaceGUI::play(int row) { memset(chanOscBright,0,DIV_MAX_CHANS*sizeof(float)); e->walkSong(loopOrder,loopRow,loopEnd); memset(lastIns,-1,sizeof(int)*DIV_MAX_CHANS); + if (followPattern) makeCursorUndo(); if (!followPattern) e->setOrder(curOrder); if (row>0) { if (!e->playToRow(row)) { @@ -5859,7 +5871,8 @@ bool FurnaceGUI::loop() { MEASURE_BEGIN(popup); centerNextWindow(_("Rendering..."),canvasW,canvasH); - if (ImGui::BeginPopupModal(_("Rendering..."),NULL,ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoMove)) { + // ImGui::SetNextWindowSize(ImVec2(0.0f,0.0f)); + if (ImGui::BeginPopupModal(_("Rendering..."),NULL,ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoSavedSettings)) { // WHAT the HELL?! WAKE_UP; if (audioExportOptions.mode!=DIV_EXPORT_MODE_MANY_CHAN) { @@ -5875,35 +5888,32 @@ bool FurnaceGUI::loop() { int curFile=0; int* curFileLambda=&curFile; if (e->isExporting()) { - e->lockEngine([this, progressLambda, curPosInRowsLambda, curFileLambda, - loopsLeftLambda, totalLoopsLambda] () { - int curRow=0; int curOrder=0; - e->getCurSongPos(curRow, curOrder); *curFileLambda=0; - e->getCurFileIndex(*curFileLambda); - *curPosInRowsLambda=curRow; for (int i=0; igetLoopsLeft(*loopsLeftLambda); e->getTotalLoops(*totalLoopsLambda); if ((*totalLoopsLambda)!=(*loopsLeftLambda)) //we are going 2nd, 3rd, etc. time through the song - { - *curPosInRowsLambda-=(songLength-songLoopedSectionLength); //a hack so progress bar does not jump? - } - if (e->getIsFadingOut()) //we are in fadeout??? why it works like that bruh - { - // LIVE WITH IT damn it - *curPosInRowsLambda-=(songLength-songLoopedSectionLength); //a hack so progress bar does not jump? - } - } - // this horrible indentation courtesy of `indent` - *progressLambda=(float) ((*curPosInRowsLambda) + ((*totalLoopsLambda)- (*loopsLeftLambda)) * songLength + lengthOfOneFile * (*curFileLambda)) / (float) totalLength;}); + e->lockEngine( + [this, progressLambda, curPosInRowsLambda, curFileLambda, loopsLeftLambda, totalLoopsLambda] () { + int curRow=0; int curOrder=0; + e->getCurSongPos(curRow, curOrder); + *curFileLambda=0; + e->getCurFileIndex(*curFileLambda); + *curPosInRowsLambda=curRow; + for (int i=0; igetLoopsLeft(*loopsLeftLambda); + e->getTotalLoops(*totalLoopsLambda); + if ((*totalLoopsLambda)!=(*loopsLeftLambda)) { // we are going 2nd, 3rd, etc. time through the song + *curPosInRowsLambda-=(songLength-songLoopedSectionLength); // a hack so progress bar does not jump? + } + if (e->getIsFadingOut()) { // we are in fadeout??? why it works like that bruh + // LIVE WITH IT damn it + *curPosInRowsLambda-=(songLength-songLoopedSectionLength); // a hack so progress bar does not jump? + } + } + *progressLambda=(float)((*curPosInRowsLambda)+((*totalLoopsLambda)-(*loopsLeftLambda))*songLength+lengthOfOneFile*(*curFileLambda))/(float)totalLength; + } + ); } ImGui::Text(_("Row %d of %d"),curPosInRows+((totalLoops)-(loopsLeft))*songLength,lengthOfOneFile); - - if (audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN) { - ImGui::Text(_("Channel %d of %d"),curFile+1,totalFiles); - } + if (audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN) ImGui::Text(_("Channel %d of %d"),curFile+1,totalFiles); ImGui::ProgressBar(curProgress,ImVec2(320.0f*dpiScale,0),fmt::sprintf("%.2f%%",curProgress*100.0f).c_str()); diff --git a/src/gui/gui.h b/src/gui/gui.h index a546d2b78..b514ee68d 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -39,7 +39,7 @@ #define FURNACE_APP_ID "org.tildearrow.furnace" #define rightClickable if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) ImGui::SetKeyboardFocusHere(-1); -#define ctrlWheeling ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && wheelY!=0) +#define ctrlWheeling (isCtrlWheelModifierHeld() && wheelY!=0) #define handleUnimportant if (settings.insFocusesPattern && patternOpen) {nextWindow=GUI_WINDOW_PATTERN;} #define unimportant(x) if (x) {handleUnimportant} @@ -354,6 +354,8 @@ enum FurnaceGUIColors { GUI_COLOR_INSTR_GBA_MINMOD, GUI_COLOR_INSTR_BIFURCATOR, GUI_COLOR_INSTR_SID2, + GUI_COLOR_INSTR_SUPERVISION, + GUI_COLOR_INSTR_UPD1771C, GUI_COLOR_INSTR_SID3, GUI_COLOR_INSTR_UNKNOWN, @@ -819,6 +821,8 @@ enum FurnaceGUIActions { GUI_ACTION_PAT_SCROLL_MODE, GUI_ACTION_PAT_CLEAR_LATCH, GUI_ACTION_PAT_ABSORB_INSTRUMENT, + GUI_ACTION_PAT_CURSOR_UNDO, + GUI_ACTION_PAT_CURSOR_REDO, GUI_ACTION_PAT_MAX, GUI_ACTION_INS_LIST_MIN, @@ -1104,6 +1108,22 @@ struct UndoStep { newPatLen(0) {} }; +struct CursorJumpPoint { + SelectionPoint point; + int order; + int subSong; + CursorJumpPoint(const SelectionPoint& p, int o, int ss): + point(p), order(o), subSong(ss) {} + CursorJumpPoint(): + point(), order(0), subSong(0) {} + bool operator== (const CursorJumpPoint& spot) { + return point.xCoarse==spot.point.xCoarse && point.xFine==spot.point.xFine && point.y==spot.point.y && order==spot.order && subSong==spot.subSong; + } + bool operator!= (const CursorJumpPoint& spot) { + return !(*this == spot); + } +}; + // -1 = any struct MIDIBind { int type, channel, data1, data2; @@ -1753,6 +1773,7 @@ class FurnaceGUI { int opnbCore; int opl2Core; int opl3Core; + int opl4Core; int esfmCore; int opllCore; int ayCore; @@ -1779,6 +1800,7 @@ class FurnaceGUI { int opnbCoreRender; int opl2CoreRender; int opl3CoreRender; + int opl4CoreRender; int esfmCoreRender; int opllCoreRender; int ayCoreRender; @@ -1805,6 +1827,7 @@ class FurnaceGUI { int patRowsBase; int orderRowsBase; int soloAction; + int ctrlWheelModifier; int pullDeleteBehavior; int wrapHorizontal; int wrapVertical; @@ -2013,6 +2036,7 @@ class FurnaceGUI { opnbCore(1), opl2Core(0), opl3Core(0), + opl4Core(0), esfmCore(0), opllCore(0), ayCore(0), @@ -2039,6 +2063,7 @@ class FurnaceGUI { opnbCoreRender(1), opl2CoreRender(0), opl3CoreRender(0), + opl4CoreRender(0), esfmCoreRender(0), opllCoreRender(0), ayCoreRender(0), @@ -2064,6 +2089,7 @@ class FurnaceGUI { patRowsBase(0), orderRowsBase(1), soloAction(0), + ctrlWheelModifier(0), pullDeleteBehavior(1), wrapHorizontal(0), wrapVertical(0), @@ -2501,6 +2527,8 @@ class FurnaceGUI { std::map oldPatMap; FixedQueue undoHist; FixedQueue redoHist; + FixedQueue cursorUndoHist; + FixedQueue cursorRedoHist; // sample editor specific double sampleZoom; @@ -2741,6 +2769,7 @@ class FurnaceGUI { static bool LocalizedComboGetter(void* data, int idx, const char** out_text); // these ones offer ctrl-wheel fine value changes. + bool isCtrlWheelModifierHeld() const; bool CWSliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format=NULL, ImGuiSliderFlags flags=0); bool CWVSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format=NULL, ImGuiSliderFlags flags=0); bool CWSliderInt(const char* label, int* v, int v_min, int v_max, const char* format="%d", ImGuiSliderFlags flags=0); @@ -2941,6 +2970,12 @@ class FurnaceGUI { void doGenerateWave(); + CursorJumpPoint getCurrentCursorJumpPoint(); + void applyCursorJumpPoint(const CursorJumpPoint& spot); + void makeCursorUndo(); + void doCursorUndo(); + void doCursorRedo(); + void doUndoSample(); void doRedoSample(); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 49af3205a..ef6d71ba8 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -184,6 +184,8 @@ const char* insTypes[DIV_INS_MAX+1][3]={ {"GBA MinMod",ICON_FA_VOLUME_UP,ICON_FUR_INS_GBA_MINMOD}, {"Bifurcator",ICON_FA_LINE_CHART,ICON_FUR_INS_BIFURCATOR}, {"SID2",ICON_FA_KEYBOARD_O,ICON_FUR_INS_SID2}, + {"Watara Supervision",ICON_FA_GAMEPAD,ICON_FUR_INS_SUPERVISION}, + {"NEC μPD1771C",ICON_FA_BAR_CHART,ICON_FUR_INS_UPD1771C}, {"SID3",ICON_FA_KEYBOARD_O,ICON_FUR_INS_SID3}, {NULL,ICON_FA_QUESTION,ICON_FA_QUESTION} }; @@ -209,7 +211,7 @@ const char* sampleDepths[DIV_SAMPLE_DEPTH_MAX]={ "8-bit µ-law PCM", "C219 PCM", "IMA ADPCM", - NULL, + "12-bit PCM", NULL, "16-bit PCM" }; @@ -689,6 +691,8 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("PAT_SCROLL_MODE", _N("Change mobile scroll mode"), 0), D("PAT_CLEAR_LATCH", _N("Clear note input latch"), 0), D("PAT_ABSORB_INSTRUMENT", _N("Absorb instrument/octave from status at cursor"), 0), + D("PAT_CURSOR_UNDO", _N("Return cursor to previous jump point"), 0), + D("PAT_CURSOR_REDO", _N("Reverse recent cursor undo"), 0), D("PAT_MAX", "", NOT_AN_ACTION), D("INS_LIST_MIN", _N("---Instrument list"), NOT_AN_ACTION), @@ -1017,6 +1021,8 @@ const FurnaceGUIColorDef guiColors[GUI_COLOR_MAX]={ D(GUI_COLOR_INSTR_GBA_MINMOD,"",ImVec4(0.5f,0.45f,0.7f,1.0f)), D(GUI_COLOR_INSTR_BIFURCATOR,"",ImVec4(0.8925f,0.8925f,0.8925f,1.0f)), D(GUI_COLOR_INSTR_SID2,"",ImVec4(0.6f,0.75f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_SUPERVISION,"",ImVec4(0.52f,1.0f,0.6f,1.0f)), + D(GUI_COLOR_INSTR_UPD1771C,"",ImVec4(0.94f,0.52f,0.6f,1.0f)), D(GUI_COLOR_INSTR_SID3,"",ImVec4(0.6f,0.75f,0.6f,1.0f)), D(GUI_COLOR_INSTR_UNKNOWN,"",ImVec4(0.3f,0.3f,0.3f,1.0f)), @@ -1266,6 +1272,10 @@ const int availableSystems[]={ DIV_SYSTEM_5E01, DIV_SYSTEM_BIFURCATOR, DIV_SYSTEM_SID2, + DIV_SYSTEM_OPL4, + DIV_SYSTEM_OPL4_DRUMS, + DIV_SYSTEM_SUPERVISION, + DIV_SYSTEM_UPD1771C, DIV_SYSTEM_SID3, 0 // don't remove this last one! }; @@ -1302,6 +1312,8 @@ const int chipsFM[]={ DIV_SYSTEM_OPL3_DRUMS, DIV_SYSTEM_OPZ, DIV_SYSTEM_ESFM, + DIV_SYSTEM_OPL4, + DIV_SYSTEM_OPL4_DRUMS, 0 // don't remove this last one! }; @@ -1362,6 +1374,8 @@ const int chipsSpecial[]={ DIV_SYSTEM_5E01, DIV_SYSTEM_BIFURCATOR, DIV_SYSTEM_SID2, + DIV_SYSTEM_SUPERVISION, + DIV_SYSTEM_UPD1771C, DIV_SYSTEM_SID3, 0 // don't remove this last one! }; @@ -1388,6 +1402,8 @@ const int chipsSample[]={ DIV_SYSTEM_NDS, DIV_SYSTEM_GBA_DMA, DIV_SYSTEM_GBA_MINMOD, + DIV_SYSTEM_OPL4, + DIV_SYSTEM_OPL4_DRUMS, 0 // don't remove this last one! }; diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 1c228fea5..531934474 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -2746,48 +2746,68 @@ void FurnaceGUI::drawMacros(std::vector& macros, FurnaceGUI drawMacroEdit(m,totalFit,availableWidth,index); if (m.macro->open&1) { - if ((m.macro->open&6)==0) { - ImGui::Text(_("Length")); - ImGui::SameLine(); - ImGui::SetNextItemWidth(120.0f*dpiScale); - int macroLen=m.macro->len; - if (ImGui::InputScalar("##IMacroLen",ImGuiDataType_U8,¯oLen,&_ONE,&_THREE)) { MARK_MODIFIED - if (macroLen<0) macroLen=0; - if (macroLen>255) macroLen=255; - m.macro->len=macroLen; - } - ImGui::SameLine(); - } - ImGui::Text(_("StepLen")); - ImGui::SameLine(); - ImGui::SetNextItemWidth(120.0f*dpiScale); - if (ImGui::InputScalar("##IMacroSpeed",ImGuiDataType_U8,&m.macro->speed,&_ONE,&_THREE)) { - if (m.macro->speed<1) m.macro->speed=1; - MARK_MODIFIED; - } - ImGui::SameLine(); - ImGui::Text(_("Delay")); - ImGui::SameLine(); - ImGui::SetNextItemWidth(120.0f*dpiScale); - if (ImGui::InputScalar("##IMacroDelay",ImGuiDataType_U8,&m.macro->delay,&_ONE,&_THREE)) { - MARK_MODIFIED; - } - ImGui::SameLine(); - { - FurnaceGUIMacroDesc& i=m; - BUTTON_TO_SET_MODE(ImGui::Button); - if ((i.macro->open&6)==0) { + bool showLen=((m.macro->open&6)==0); + int colCount=showLen ? 4 : 3; + float availX=ImGui::GetContentRegionAvail().x; + + // fairly arbitrary scaling logic + bool shortLabels=(availX<600.0f*dpiScale); + float scalarItemWidth=MIN((availX-90.0f*dpiScale)/colCount, 120.0f*dpiScale); + if (ImGui::BeginTable("##MacroMetaData",colCount)) { + if (showLen) ImGui::TableSetupColumn("len",ImGuiTableColumnFlags_WidthStretch,0.0); + ImGui::TableSetupColumn("stepLen",ImGuiTableColumnFlags_WidthStretch,0.0); + ImGui::TableSetupColumn("delay",ImGuiTableColumnFlags_WidthStretch,0.0); + ImGui::TableSetupColumn("buttons",ImGuiTableColumnFlags_WidthFixed,0.0); + + ImGui::TableNextRow(); + if (showLen) { + ImGui::TableNextColumn(); + ImGui::Text(shortLabels ? _("Len##macroEditLengthShortLabel") : _("Length")); + if (shortLabels && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _("Length")); ImGui::SameLine(); - BUTTON_TO_SET_RELEASE(ImGui::Button); + ImGui::SetNextItemWidth(scalarItemWidth); + int macroLen=m.macro->len; + if (ImGui::InputScalar("##IMacroLen",ImGuiDataType_U8,¯oLen,&_ONE,&_THREE)) { MARK_MODIFIED + if (macroLen<0) macroLen=0; + if (macroLen>255) macroLen=255; + m.macro->len=macroLen; + } } - } - if (m.modeName!=NULL) { - bool modeVal=m.macro->mode; - String modeName=fmt::sprintf("%s##IMacroMode",m.modeName); + ImGui::TableNextColumn(); + ImGui::Text(shortLabels ? _("SLen##macroEditStepLenShortLabel") : _("StepLen")); + if (shortLabels && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _("StepLen")); ImGui::SameLine(); - if (ImGui::Checkbox(modeName.c_str(),&modeVal)) { - m.macro->mode=modeVal; + ImGui::SetNextItemWidth(scalarItemWidth); + if (ImGui::InputScalar("##IMacroSpeed",ImGuiDataType_U8,&m.macro->speed,&_ONE,&_THREE)) { + if (m.macro->speed<1) m.macro->speed=1; + MARK_MODIFIED; } + ImGui::TableNextColumn(); + ImGui::Text(shortLabels ? _("Del##macroEditDelayShortLabel") : _("Delay")); + if (shortLabels && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _("Delay")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(scalarItemWidth); + if (ImGui::InputScalar("##IMacroDelay",ImGuiDataType_U8,&m.macro->delay,&_ONE,&_THREE)) { + MARK_MODIFIED; + } + ImGui::TableNextColumn(); + { + FurnaceGUIMacroDesc& i=m; + BUTTON_TO_SET_MODE(ImGui::Button); + if ((i.macro->open&6)==0) { + ImGui::SameLine(); + BUTTON_TO_SET_RELEASE(ImGui::Button); + } + } + if (m.modeName!=NULL) { + bool modeVal=m.macro->mode; + String modeName=fmt::sprintf("%s##IMacroMode",m.modeName); + ImGui::SameLine(); + if (ImGui::Checkbox(modeName.c_str(),&modeVal)) { + m.macro->mode=modeVal; + } + } + ImGui::EndTable(); } } else { ImGui::Text(_("The heck? No, this isn't even working correctly...")); @@ -3289,6 +3309,7 @@ void FurnaceGUI::insTabSample(DivInstrument* ins) { ins->type==DIV_INS_VRC6 || ins->type==DIV_INS_SU || ins->type==DIV_INS_NDS || + ins->type==DIV_INS_SUPERVISION || ins->type==DIV_INS_SID3) { P(ImGui::Checkbox(_("Use sample"),&ins->amiga.useSample)); if (ins->type==DIV_INS_X1_010) { @@ -4120,8 +4141,8 @@ void FurnaceGUI::insTabFM(DivInstrument* ins) { opllPreview.alg=ins->fm.alg; opllPreview.fb=patch->fb; - opllPreview.fms=patch->dm; - opllPreview.ams=patch->dc; + opllPreview.fms=patch->dc; + opllPreview.ams=patch->dm; opllPreview.op[0].tl=patch->tl; opllPreview.op[1].tl=ins->fm.op[1].tl; @@ -4131,6 +4152,7 @@ void FurnaceGUI::insTabFM(DivInstrument* ins) { opllPreview.op[i].vib=patch->vib[i]; opllPreview.op[i].ssgEnv=patch->et[i]?8:0; opllPreview.op[i].ksr=patch->ksr[i]; + opllPreview.op[i].ksl=patch->ksl[i]; opllPreview.op[i].mult=patch->multi[i]; opllPreview.op[i].ar=patch->ar[i]; opllPreview.op[i].dr=patch->dr[i]; @@ -4650,7 +4672,7 @@ void FurnaceGUI::insTabFM(DivInstrument* ins) { ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); P(CWSliderScalar("##WS",ImGuiDataType_U8,&op.ws,&_ZERO,&_SEVEN,(ins->type==DIV_INS_OPZ)?opzWaveforms[op.ws&7]:(settings.oplStandardWaveNames?oplWaveformsStandard[op.ws&7]:oplWaveforms[op.ws&7]))); rightClickable if ((ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPL_DRUMS) && ImGui::IsItemHovered()) { - ImGui::SetTooltip(_("OPL2/3 only (last 4 waveforms are OPL3 only)")); + ImGui::SetTooltip(_("OPL2/3/4 only (last 4 waveforms are OPL3/4 only)")); } if (ins->type==DIV_INS_ESFM && fixedOn) { if (ImGui::Checkbox(FM_SHORT_NAME(FM_VIB),&vibOn)) { PARAMETER @@ -5014,7 +5036,7 @@ void FurnaceGUI::insTabFM(DivInstrument* ins) { ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); P(CWSliderScalar("##WS",ImGuiDataType_U8,&op.ws,&_ZERO,&_SEVEN,(ins->type==DIV_INS_OPZ)?opzWaveforms[op.ws&7]:(settings.oplStandardWaveNames?oplWaveformsStandard[op.ws&7]:oplWaveforms[op.ws&7]))); rightClickable if ((ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPL_DRUMS) && ImGui::IsItemHovered()) { - ImGui::SetTooltip(_("OPL2/3 only (last 4 waveforms are OPL3 only)")); + ImGui::SetTooltip(_("OPL2/3/4 only (last 4 waveforms are OPL3/4 only)")); } // params @@ -5064,7 +5086,7 @@ void FurnaceGUI::insTabFM(DivInstrument* ins) { ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); P(CWSliderScalar("##WS",ImGuiDataType_U8,&op.ws,&_ZERO,&_SEVEN,(ins->type==DIV_INS_OPZ)?opzWaveforms[op.ws&7]:(settings.oplStandardWaveNames?oplWaveformsStandard[op.ws&7]:oplWaveforms[op.ws&7]))); rightClickable if ((ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPL_DRUMS) && ImGui::IsItemHovered()) { - ImGui::SetTooltip(_("OPL2/3 only (last 4 waveforms are OPL3 only)")); + ImGui::SetTooltip(_("OPL2/3/4 only (last 4 waveforms are OPL3/4 only)")); } // params @@ -5772,7 +5794,7 @@ void FurnaceGUI::insTabFM(DivInstrument* ins) { ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); P(CWSliderScalar("##WS",ImGuiDataType_U8,&op.ws,&_ZERO,&_SEVEN,(ins->type==DIV_INS_OPZ)?opzWaveforms[op.ws&7]:(settings.oplStandardWaveNames?oplWaveformsStandard[op.ws&7]:oplWaveforms[op.ws&7]))); rightClickable if ((ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPL_DRUMS) && ImGui::IsItemHovered()) { - ImGui::SetTooltip(_("OPL2/3 only (last 4 waveforms are OPL3 only)")); + ImGui::SetTooltip(_("OPL2/3/4 only (last 4 waveforms are OPL3/4 only)")); } ImGui::TableNextColumn(); ImGui::Text("%s",FM_NAME(FM_WS)); @@ -7475,7 +7497,8 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_C219 || ins->type==DIV_INS_NDS || ins->type==DIV_INS_GBA_DMA || - ins->type==DIV_INS_GBA_MINMOD) { + ins->type==DIV_INS_GBA_MINMOD || + ins->type==DIV_INS_SUPERVISION) { insTabSample(ins); } if (ins->type==DIV_INS_N163) if (ImGui::BeginTabItem("Namco 163")) { @@ -7789,6 +7812,19 @@ void FurnaceGUI::drawInsEdit() { P(CWSliderScalar(_("AM Depth"),ImGuiDataType_U8,&ins->multipcm.am,&_ZERO,&_SEVEN)); rightClickable ImGui::EndTable(); } + P(ImGui::Checkbox(_("Damp"),&ins->multipcm.damp)); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(_("Only for OPL4 PCM.")); + } + P(ImGui::Checkbox(_("Pseudo Reverb"),&ins->multipcm.pseudoReverb)); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(_("Only for OPL4 PCM.")); + } + P(ImGui::Checkbox(_("LFO Reset"),&ins->multipcm.lfoReset)); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(_("Only for OPL4 PCM.")); + } + P(ImGui::Checkbox(_("Level Direct"),&ins->multipcm.levelDirect)); ImGui::EndTabItem(); } } @@ -8225,6 +8261,9 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc(_("Panning"),&ins->std.panLMacro,-7,7,45,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL)); macroList.push_back(FurnaceGUIMacroDesc(_("Pitch"),&ins->std.pitchMacro,-2048,2047,160,uiColors[GUI_COLOR_MACRO_PITCH],true,macroRelativeMode)); macroList.push_back(FurnaceGUIMacroDesc(_("Phase Reset"),&ins->std.phaseResetMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true)); + macroList.push_back(FurnaceGUIMacroDesc(_("LFO Speed"),&ins->std.ex1Macro,0,7,160,uiColors[GUI_COLOR_MACRO_OTHER])); + macroList.push_back(FurnaceGUIMacroDesc(_("LFO Vib Depth"),&ins->std.fmsMacro,0,7,160,uiColors[GUI_COLOR_MACRO_PITCH])); + macroList.push_back(FurnaceGUIMacroDesc(_("LFO AM Depth"),&ins->std.amsMacro,0,7,160,uiColors[GUI_COLOR_MACRO_VOLUME])); break; case DIV_INS_SNES: macroList.push_back(FurnaceGUIMacroDesc(_("Volume"),&ins->std.volMacro,0,127,160,uiColors[GUI_COLOR_MACRO_VOLUME])); @@ -8376,6 +8415,13 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc(_("Pulse Width"),&ins->std.dutyMacro,0,255,160,uiColors[GUI_COLOR_MACRO_OTHER])); macroList.push_back(FurnaceGUIMacroDesc(_("Pitch"),&ins->std.pitchMacro,-2048,2047,160,uiColors[GUI_COLOR_MACRO_PITCH],true,macroRelativeMode)); break; + case DIV_INS_SUPERVISION: + macroList.push_back(FurnaceGUIMacroDesc(_("Volume"),&ins->std.volMacro,0,15,160,uiColors[GUI_COLOR_MACRO_VOLUME])); + macroList.push_back(FurnaceGUIMacroDesc(_("Arpeggio"),&ins->std.arpMacro,-120,120,160,uiColors[GUI_COLOR_MACRO_PITCH],true,NULL,macroHoverNote,false,NULL,true,ins->std.arpMacro.val)); + macroList.push_back(FurnaceGUIMacroDesc(_("Duty/Noise"),&ins->std.dutyMacro,0,3,160,uiColors[GUI_COLOR_MACRO_NOISE])); + macroList.push_back(FurnaceGUIMacroDesc(_("Noise/PCM Pan"),&ins->std.panLMacro,0,2,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,panBits)); + macroList.push_back(FurnaceGUIMacroDesc(_("Pitch"),&ins->std.pitchMacro,-2048,2047,160,uiColors[GUI_COLOR_MACRO_PITCH],true,macroRelativeMode)); + break; case DIV_INS_SM8521: macroList.push_back(FurnaceGUIMacroDesc(_("Volume"),&ins->std.volMacro,0,31,160,uiColors[GUI_COLOR_MACRO_VOLUME])); macroList.push_back(FurnaceGUIMacroDesc(_("Arpeggio"),&ins->std.arpMacro,-120,120,160,uiColors[GUI_COLOR_MACRO_PITCH],true,NULL,macroHoverNote,false,NULL,true,ins->std.arpMacro.val)); @@ -8518,8 +8564,17 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc(_("Noise Mode"),&ins->std.fmsMacro,0,3,64,uiColors[GUI_COLOR_MACRO_NOISE])); macroList.push_back(FurnaceGUIMacroDesc(_("Wave Mix"),&ins->std.amsMacro,0,3,64,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,macroSID2WaveMixMode)); break; - case DIV_INS_SID3: break; - + case DIV_INS_UPD1771C: + macroList.push_back(FurnaceGUIMacroDesc(_("Volume"),&ins->std.volMacro,0,31,160,uiColors[GUI_COLOR_MACRO_VOLUME])); + macroList.push_back(FurnaceGUIMacroDesc(_("Arpeggio"),&ins->std.arpMacro,-120,120,160,uiColors[GUI_COLOR_MACRO_PITCH],true,NULL,macroHoverNote,false,NULL,true,ins->std.arpMacro.val)); + macroList.push_back(FurnaceGUIMacroDesc(_("Waveform"),&ins->std.waveMacro,0,7,160,uiColors[GUI_COLOR_MACRO_WAVE],false,NULL,NULL,false,NULL)); + macroList.push_back(FurnaceGUIMacroDesc(_("Wave Pos"),&ins->std.ex1Macro,0,31,160,uiColors[GUI_COLOR_MACRO_OTHER])); + macroList.push_back(FurnaceGUIMacroDesc(_("Duty/Mode"),&ins->std.dutyMacro,0,1,160,uiColors[GUI_COLOR_MACRO_NOISE])); + macroList.push_back(FurnaceGUIMacroDesc(_("Pitch"),&ins->std.pitchMacro,-2048,2047,160,uiColors[GUI_COLOR_MACRO_PITCH],true,macroRelativeMode)); + break; + case DIV_INS_SID3: + // TODO: put stuff here to kill that goto. + break; case DIV_INS_MAX: case DIV_INS_NULL: break; diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index d3cc86495..75e57c73f 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -320,6 +320,11 @@ void FurnaceGUI::initSystemPresets() { CH(DIV_SYSTEM_NDS, 1.0f, 0, "") } ); + ENTRY( + "Watara Supervision", { + CH(DIV_SYSTEM_SUPERVISION, 1.0f, 0, "") + } + ); CATEGORY_END; CATEGORY_BEGIN(_("Computers"),_("let's get to work on chiptune today.")); @@ -519,6 +524,18 @@ void FurnaceGUI::initSystemPresets() { ) // variable rate, Mono DAC } ); + SUB_ENTRY( + "MSX + MoonSound", { + CH(DIV_SYSTEM_AY8910, 1.0f, 0, "chipType=1"), + CH(DIV_SYSTEM_OPL4, 1.0f, 0, "") + } + ); + SUB_ENTRY( + "MSX + MoonSound (drums mode)", { + CH(DIV_SYSTEM_AY8910, 1.0f, 0, "chipType=1"), + CH(DIV_SYSTEM_OPL4_DRUMS, 1.0f, 0, "") + } + ); ENTRY( _("NEC PC-6001"), { CH(DIV_SYSTEM_AY8910, 1.0f, 0, "customClock=3993600") @@ -1319,6 +1336,11 @@ void FurnaceGUI::initSystemPresets() { ) } ); + ENTRY( + "Epoch Super Cassette Vision", { + CH(DIV_SYSTEM_UPD1771C, 1.0f, 0, "") + } + ); CATEGORY_END; CATEGORY_BEGIN(_("Arcade systems"),_("INSERT COIN")); @@ -1895,6 +1917,30 @@ void FurnaceGUI::initSystemPresets() { } ); + ENTRY( + _("Psikyo"), {} + ); + SUB_ENTRY( + _("Psikyo 68EC020 hardware with OPL4"), { + CH(DIV_SYSTEM_OPL4, 1.0f, 0, "") + } + ); + SUB_ENTRY( + _("Psikyo 68EC020 hardware with OPL4 (drums mode)"), { + CH(DIV_SYSTEM_OPL4_DRUMS, 1.0f, 0, "") + } + ); + SUB_ENTRY( + _("Psikyo SH-2 hardware"), { + CH(DIV_SYSTEM_OPL4, 1.0f, 0, "clockSel=1") + } + ); + SUB_ENTRY( + _("Psikyo SH-2 hardware (drums mode)"), { + CH(DIV_SYSTEM_OPL4_DRUMS, 1.0f, 0, "clockSel=1") + } + ); + ENTRY( _("Sega"), {} ); @@ -2719,6 +2765,16 @@ void FurnaceGUI::initSystemPresets() { CH(DIV_SYSTEM_ESFM, 1.0f, 0, "") } ); + ENTRY( + "Yamaha YMF278B (OPL4)", { + CH(DIV_SYSTEM_OPL4, 1.0f, 0, "") + } + ); + SUB_ENTRY( + "Yamaha YMF278B (drums mode)", { + CH(DIV_SYSTEM_OPL4_DRUMS, 1.0f, 0, "") + } + ); if (settings.hiddenSystems) { ENTRY( _("Yamaha YMU759 (MA-2)"), { @@ -2930,6 +2986,16 @@ void FurnaceGUI::initSystemPresets() { CH(DIV_SYSTEM_NDS, 1.0f, 0, "") } ); + ENTRY( + "Yamaha YMF278B (OPL4)", { + CH(DIV_SYSTEM_OPL4, 1.0f, 0, "") + } + ); + SUB_ENTRY( + "Yamaha YMF278B (drums mode)", { + CH(DIV_SYSTEM_OPL4_DRUMS, 1.0f, 0, "") + } + ); CATEGORY_END; CATEGORY_BEGIN(_("Wavetable"),_("chips which use user-specified waveforms to generate sound.")); @@ -3125,6 +3191,16 @@ void FurnaceGUI::initSystemPresets() { CH(DIV_SYSTEM_SID3, 1.0f, 0, "") } ); + ENTRY( + _("Watara Supervision"), { + CH(DIV_SYSTEM_SUPERVISION, 1.0f, 0, "") + } + ); + ENTRY( + _("NEC μPD1771C"), { + CH(DIV_SYSTEM_UPD1771C, 1.0f, 0, "") + } + ); CATEGORY_END; CATEGORY_BEGIN(_("DefleMask-compatible"),_("these configurations are compatible with DefleMask.\nselect this if you need to save as .dmf or work with that program.")); diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index 4f858af19..9a6000820 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -413,6 +413,25 @@ void FurnaceGUI::drawSampleEdit() { SAMPLE_WARN(warnLength,_("GBA DMA: sample length will be padded to multiple of 16")); } break; + case DIV_SYSTEM_OPL4: + case DIV_SYSTEM_OPL4_DRUMS: + if (sample->samples>65535) { + SAMPLE_WARN(warnLength,_("OPL4: maximum sample length is 65535")); + } + break; + case DIV_SYSTEM_SUPERVISION: + if (sample->loop) { + if (sample->loopStart!=0 || sample->loopEnd!=(int)(sample->samples)) { + SAMPLE_WARN(warnLoopPos,_("Supervision: loop point ignored on sample channel")); + } + } + if (sample->samples&31) { + SAMPLE_WARN(warnLength,_("Supervision: sample length will be padded to multiple of 32")); + } + if (sample->samples>8192) { + SAMPLE_WARN(warnLength,_("Supervision: maximum sample length is 8192")); + } + break; default: break; } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 33151945f..a206f4473 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -189,6 +189,11 @@ const char* opl3Cores[]={ "YMF262-LLE" }; +const char* opl4Cores[]={ + "Nuked-OPL3 (FM) + openMSX (PCM)", + "ymfm" +}; + const char* esfmCores[]={ "ESFMu", _N("ESFMu (fast)") @@ -2058,6 +2063,17 @@ void FurnaceGUI::drawSettings() { ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (ImGui::Combo("##OPL3CoreRender",&settings.opl3CoreRender,opl3Cores,3)) settingsChanged=true; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::Text("OPL4"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::Combo("##OPL4Core",&settings.opl4Core,opl4Cores,2)) settingsChanged=true; + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::Combo("##OPL4CoreRender",&settings.opl4CoreRender,opl4Cores,2)) settingsChanged=true; + ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::AlignTextToFramePadding(); @@ -2137,7 +2153,6 @@ void FurnaceGUI::drawSettings() { ImGui::SameLine(); if (ImGui::Combo("##PCSOutMethod",&settings.pcSpeakerOutMethod,LocalizedComboGetter,pcspkrOutMethods,5)) settingsChanged=true; - /* ImGui::Separator(); ImGui::Text(_("Sample ROMs:")); @@ -2150,6 +2165,7 @@ void FurnaceGUI::drawSettings() { openFileDialog(GUI_FILE_YRW801_ROM_OPEN); } + /* ImGui::AlignTextToFramePadding(); ImGui::Text(_("MultiPCM TG100 path")); ImGui::SameLine(); @@ -2428,6 +2444,8 @@ void FurnaceGUI::drawSettings() { UI_KEYBIND_CONFIG(GUI_ACTION_PAT_LATCH); UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CLEAR_LATCH); UI_KEYBIND_CONFIG(GUI_ACTION_PAT_ABSORB_INSTRUMENT); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_UNDO); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_REDO); KEYBIND_CONFIG_END; ImGui::TreePop(); @@ -2720,6 +2738,27 @@ void FurnaceGUI::drawSettings() { } ImGui::Unindent(); + ImGui::Text(_("Modifier for alternate wheel-scrolling (vertical/zoom/slider-input):")); + ImGui::Indent(); + if (ImGui::RadioButton(_("Ctrl or Meta/Cmd##cwm1"),settings.ctrlWheelModifier==0)) { + settings.ctrlWheelModifier=0; + settingsChanged=true; + } + if (ImGui::RadioButton(_("Ctrl##cwm2"),settings.ctrlWheelModifier==1)) { + settings.ctrlWheelModifier=1; + settingsChanged=true; + } + if (ImGui::RadioButton(_("Meta/Cmd##cwm3"),settings.ctrlWheelModifier==2)) { + settings.ctrlWheelModifier=2; + settingsChanged=true; + } + // technically this key is called Option on mac, but we call it Alt in getKeyName(s) + if (ImGui::RadioButton(_("Alt##cwm4"),settings.ctrlWheelModifier==3)) { + settings.ctrlWheelModifier=3; + settingsChanged=true; + } + ImGui::Unindent(); + bool doubleClickColumnB=settings.doubleClickColumn; if (ImGui::Checkbox(_("Double click selects entire column"),&doubleClickColumnB)) { settings.doubleClickColumn=doubleClickColumnB; @@ -4126,6 +4165,8 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_INSTR_GBA_MINMOD,_("GBA MinMod")); UI_COLOR_CONFIG(GUI_COLOR_INSTR_BIFURCATOR,_("Bifurcator")); UI_COLOR_CONFIG(GUI_COLOR_INSTR_SID2,_("SID2")); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_SUPERVISION,_("Supervision")); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_UPD1771C,_("μPD1771C")); UI_COLOR_CONFIG(GUI_COLOR_INSTR_SID3,_("SID3")); UI_COLOR_CONFIG(GUI_COLOR_INSTR_UNKNOWN,_("Other/Unknown")); ImGui::TreePop(); @@ -4820,6 +4861,7 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { if (groups&GUI_SETTINGS_BEHAVIOR) { settings.soloAction=conf.getInt("soloAction",0); + settings.ctrlWheelModifier=conf.getInt("ctrlWheelModifier",0); settings.pullDeleteBehavior=conf.getInt("pullDeleteBehavior",1); settings.wrapHorizontal=conf.getInt("wrapHorizontal",0); settings.wrapVertical=conf.getInt("wrapVertical",0); @@ -4999,6 +5041,7 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { settings.opnbCore=conf.getInt("opnbCore",1); settings.opl2Core=conf.getInt("opl2Core",0); settings.opl3Core=conf.getInt("opl3Core",0); + settings.opl4Core=conf.getInt("opl4Core",0); settings.esfmCore=conf.getInt("esfmCore",0); settings.opllCore=conf.getInt("opllCore",0); settings.ayCore=conf.getInt("ayCore",0); @@ -5027,6 +5070,7 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { settings.opnbCoreRender=conf.getInt("opnbCoreRender",1); settings.opl2CoreRender=conf.getInt("opl2CoreRender",0); settings.opl3CoreRender=conf.getInt("opl3CoreRender",0); + settings.opl4CoreRender=conf.getInt("opl4CoreRender",0); settings.esfmCoreRender=conf.getInt("esfmCoreRender",0); settings.opllCoreRender=conf.getInt("opllCoreRender",0); settings.ayCoreRender=conf.getInt("ayCoreRender",0); @@ -5072,6 +5116,7 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { clampSetting(settings.opnbCore,0,2); clampSetting(settings.opl2Core,0,2); clampSetting(settings.opl3Core,0,2); + clampSetting(settings.opl4Core,0,1); clampSetting(settings.esfmCore,0,1); clampSetting(settings.opllCore,0,1); clampSetting(settings.ayCore,0,1); @@ -5098,6 +5143,7 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { clampSetting(settings.opnbCoreRender,0,2); clampSetting(settings.opl2CoreRender,0,2); clampSetting(settings.opl3CoreRender,0,2); + clampSetting(settings.opl4CoreRender,0,1); clampSetting(settings.esfmCoreRender,0,1); clampSetting(settings.opllCoreRender,0,1); clampSetting(settings.ayCoreRender,0,1); @@ -5118,6 +5164,7 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { clampSetting(settings.patRowsBase,0,1); clampSetting(settings.orderRowsBase,0,1); clampSetting(settings.soloAction,0,2); + clampSetting(settings.ctrlWheelModifier,0,3); clampSetting(settings.pullDeleteBehavior,0,1); clampSetting(settings.wrapHorizontal,0,2); clampSetting(settings.wrapVertical,0,3); @@ -5405,6 +5452,7 @@ void FurnaceGUI::writeConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { // behavior if (groups&GUI_SETTINGS_BEHAVIOR) { conf.set("soloAction",settings.soloAction); + conf.set("ctrlWheelModifier",settings.ctrlWheelModifier); conf.set("pullDeleteBehavior",settings.pullDeleteBehavior); conf.set("wrapHorizontal",settings.wrapHorizontal); conf.set("wrapVertical",settings.wrapVertical); @@ -5588,6 +5636,7 @@ void FurnaceGUI::writeConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { conf.set("opnbCore",settings.opnbCore); conf.set("opl2Core",settings.opl2Core); conf.set("opl3Core",settings.opl3Core); + conf.set("opl4Core",settings.opl4Core); conf.set("esfmCore",settings.esfmCore); conf.set("opllCore",settings.opllCore); conf.set("ayCore",settings.ayCore); @@ -5616,6 +5665,7 @@ void FurnaceGUI::writeConfig(DivConfig& conf, FurnaceGUISettingGroups groups) { conf.set("opnbCoreRender",settings.opnbCoreRender); conf.set("opl2CoreRender",settings.opl2CoreRender); conf.set("opl3CoreRender",settings.opl3CoreRender); + conf.set("opl4CoreRender",settings.opl4CoreRender); conf.set("esfmCoreRender",settings.esfmCoreRender); conf.set("opllCoreRender",settings.opllCoreRender); conf.set("ayCoreRender",settings.ayCoreRender); @@ -5679,6 +5729,7 @@ void FurnaceGUI::commitSettings() { settings.opnbCore!=e->getConfInt("opnbCore",1) || settings.opl2Core!=e->getConfInt("opl2Core",0) || settings.opl3Core!=e->getConfInt("opl3Core",0) || + settings.opl4Core!=e->getConfInt("opl4Core",0) || settings.esfmCore!=e->getConfInt("esfmCore",0) || settings.opllCore!=e->getConfInt("opllCore",0) || settings.ayCore!=e->getConfInt("ayCore",0) || diff --git a/src/gui/subSongs.cpp b/src/gui/subSongs.cpp index fba8ddff1..cc69dbcd5 100644 --- a/src/gui/subSongs.cpp +++ b/src/gui/subSongs.cpp @@ -36,6 +36,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) { ImGui::TableNextRow(); ImGui::TableNextColumn(); if (ImGui::Selectable(id,i==e->getCurrentSubSong())) { + makeCursorUndo(); e->changeSongP(i); updateScroll(0); oldRow=0; @@ -72,6 +73,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) { if (!e->addSubSong()) { showError(_("too many subsongs!")); } else { + makeCursorUndo(); e->changeSongP(e->song.subsong.size()-1); updateScroll(0); oldRow=0; @@ -92,6 +94,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) { if (!e->duplicateSubSong(e->getCurrentSubSong())) { showError(_("too many subsongs!")); } else { + makeCursorUndo(); e->changeSongP(e->song.subsong.size()-1); updateScroll(0); oldRow=0; diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index caa3891e9..5a741508e 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -1970,6 +1970,7 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl echoFilter[7]=flags.getInt("echoFilter7",0); bool interpolationOff=flags.getBool("interpolationOff",false); + bool antiClick=flags.getBool("antiClick",true); ImGui::Text(_("Volume scale:")); if (CWSliderInt(_("Left##VolScaleL"),&vsL,0,127)) { @@ -2090,6 +2091,10 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl altered=true; } + if (ImGui::Checkbox(_("Anti-click"),&antiClick)) { + altered=true; + } + if (altered) { e->lockSave([&]() { flags.set("volScaleL",127-vsL); @@ -2109,6 +2114,7 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl flags.set("echoFilter7",echoFilter[7]); flags.set("echoMask",echoMask); flags.set("interpolationOff",interpolationOff); + flags.set("antiClick",antiClick); }); } @@ -2324,6 +2330,29 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl } break; } + case DIV_SYSTEM_SUPERVISION: { + bool swapDuty=flags.getInt("swapDuty",true); + + if (ImGui::Checkbox(_("Swap noise duty cycles"),&swapDuty)) { + altered=true; + } + + bool sqStereo=flags.getInt("sqStereo",false); + + if (ImGui::Checkbox(_("Stereo pulse waves"),&sqStereo)) { + altered=true; + } + + if (altered) { + e->lockSave([&]() { + flags.set("swapDuty",(int)swapDuty); + }); + e->lockSave([&]() { + flags.set("sqStereo",(int)sqStereo); + }); + } + break; + } case DIV_SYSTEM_SM8521:/* { bool noAntiClick=flags.getBool("noAntiClick",false); @@ -2514,6 +2543,67 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl } break; } + case DIV_SYSTEM_OPL4: + case DIV_SYSTEM_OPL4_DRUMS: { + int clockSel=flags.getInt("clockSel",0); + int ramSize=flags.getInt("ramSize",0); + + ImGui::Text(_("Clock rate:")); + ImGui::Indent(); + if (ImGui::RadioButton(_("33.8688MHz"),clockSel==0)) { + clockSel=0; + altered=true; + } + if (ImGui::RadioButton(_("28.64MHz (NTSC)"),clockSel==1)) { + clockSel=1; + altered=true; + } + if (ImGui::RadioButton(_("28.38MHz (PAL)"),clockSel==2)) { + clockSel=2; + altered=true; + } + ImGui::Unindent(); + + ImGui::Text(_("RAM size:")); + ImGui::Indent(); + if (ImGui::RadioButton(_("4MB"),ramSize==0)) { + ramSize=0; + altered=true; + } + if (ImGui::RadioButton(_("2MB"),ramSize==1)) { + ramSize=1; + altered=true; + } + if (ImGui::RadioButton(_("1MB"),ramSize==2)) { + ramSize=2; + altered=true; + } + if (ImGui::RadioButton(_("640KB"),ramSize==3)) { + ramSize=3; + altered=true; + } + if (ImGui::RadioButton(_("512KB"),ramSize==4)) { + ramSize=4; + altered=true; + } + if (ImGui::RadioButton(_("256KB"),ramSize==5)) { + ramSize=5; + altered=true; + } + if (ImGui::RadioButton(_("128KB"),ramSize==6)) { + ramSize=6; + altered=true; + } + ImGui::Unindent(); + + if (altered) { + e->lockSave([&]() { + flags.set("clockSel",clockSel); + flags.set("ramSize",ramSize); + }); + } + break; + } case DIV_SYSTEM_SWAN: case DIV_SYSTEM_BUBSYS_WSG: case DIV_SYSTEM_PET: @@ -2522,6 +2612,7 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl case DIV_SYSTEM_C219: case DIV_SYSTEM_BIFURCATOR: case DIV_SYSTEM_POWERNOISE: + case DIV_SYSTEM_UPD1771C: break; case DIV_SYSTEM_YMU759: case DIV_SYSTEM_ESFM: diff --git a/src/gui/sysPartNumber.cpp b/src/gui/sysPartNumber.cpp index 1ce6e2f38..5211ce814 100644 --- a/src/gui/sysPartNumber.cpp +++ b/src/gui/sysPartNumber.cpp @@ -279,6 +279,12 @@ const char* FurnaceGUI::getSystemPartNumber(DivSystem sys, DivConfig& flags) { break; case DIV_SYSTEM_ESFM: return "ES1xxx"; + case DIV_SYSTEM_SUPERVISION: + return "Watara Supervision"; + break; + case DIV_SYSTEM_UPD1771C: + return "μPD1771C"; + break; default: return FurnaceGUI::getSystemName(sys); break; diff --git a/src/gui/userPresets.cpp b/src/gui/userPresets.cpp index d2999c6cc..1dadbca26 100644 --- a/src/gui/userPresets.cpp +++ b/src/gui/userPresets.cpp @@ -475,7 +475,8 @@ void FurnaceGUI::drawUserPresets() { ImGui::SetTooltip(_( "insert additional settings in `option=value` format.\n" "available options:\n" - "- tickRate" + "- tickRate \n" + "- chanMask \n" )); } diff --git a/src/gui/waveEdit.cpp b/src/gui/waveEdit.cpp index 389fee809..0a2c25135 100644 --- a/src/gui/waveEdit.cpp +++ b/src/gui/waveEdit.cpp @@ -948,6 +948,7 @@ void FurnaceGUI::drawWaveEdit() { wave->len=waveGenScaleX; MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } ImGui::TableNextRow(); @@ -966,6 +967,7 @@ void FurnaceGUI::drawWaveEdit() { wave->max=waveGenScaleY-1; MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } ImGui::TableNextRow(); @@ -988,6 +990,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } ImGui::TableNextRow(); @@ -1005,6 +1008,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } ImGui::TableNextRow(); @@ -1031,6 +1035,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } ImGui::TableNextRow(); @@ -1050,6 +1055,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } ImGui::EndTable(); @@ -1093,6 +1099,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } if (ImGui::Button(_("Invert"),buttonSizeHalf)) { e->lockEngine([this,wave]() { @@ -1101,6 +1108,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } ImGui::SameLine(); if (ImGui::Button(_("Reverse"),buttonSizeHalf)) { @@ -1113,6 +1121,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } if (ImGui::Button(_("Half"),buttonSizeHalf)) { @@ -1122,6 +1131,7 @@ void FurnaceGUI::drawWaveEdit() { for (int i=0; ilen; i++) { wave->data[i]=origData[i>>1]; } + e->notifyWaveChange(curWave); MARK_MODIFIED; } ImGui::SameLine(); @@ -1132,6 +1142,7 @@ void FurnaceGUI::drawWaveEdit() { for (int i=0; ilen; i++) { wave->data[i]=origData[(i*2)%wave->len]; } + e->notifyWaveChange(curWave); MARK_MODIFIED; } @@ -1146,6 +1157,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } if (ImGui::Button(_("Randomize"),buttonSize)) { if (wave->max>0) e->lockEngine([this,wave]() { @@ -1154,6 +1166,7 @@ void FurnaceGUI::drawWaveEdit() { } MARK_MODIFIED; }); + e->notifyWaveChange(curWave); } ImGui::EndTabItem(); } diff --git a/src/icon/furIcons.h b/src/icon/furIcons.h index 0384354ce..3af6b8fe6 100644 --- a/src/icon/furIcons.h +++ b/src/icon/furIcons.h @@ -1,7 +1,7 @@ // not auto-generated. update every time you change icons.ttf! #define ICON_MIN_FUR 0xe0f0 -#define ICON_MAX_FUR 0xe161 +#define ICON_MAX_FUR 0xe164 // test #define ICON_FUR_TEST0 u8"\ue0f0" @@ -75,7 +75,9 @@ #define ICON_FUR_INS_GBA_MINMOD u8"\ue15f" #define ICON_FUR_INS_BIFURCATOR u8"\ue160" #define ICON_FUR_INS_SID2 u8"\ue161" -#define ICON_FUR_INS_SID3 u8"\ue162" +#define ICON_FUR_INS_SUPERVISION u8"\ue162" +#define ICON_FUR_INS_UPD1771C u8"\ue163" +#define ICON_FUR_INS_SID3 u8"\ue164" // sample editor #define ICON_FUR_SAMPLE_APPLY_SILENCE u8"\ue136" diff --git a/src/main.cpp b/src/main.cpp index c9cfda470..b1e2afd18 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -341,6 +341,7 @@ TAParamResult pVersion(String) { printf("- NDS sound emulator by cam900 (zlib license)\n"); printf("- SID2 emulator by LTVA (GPLv2, modification of reSID emulator)\n"); printf("- SID3 emulator by LTVA (MIT)\n"); + printf("- openMSX YMF278 emulator (modified version) by the openMSX developers (GPLv2)\n"); return TA_PARAM_QUIT; }