diff --git a/CMakeLists.txt b/CMakeLists.txt index ea612efbd..f24a156dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -682,6 +682,14 @@ src/engine/fileOps/text.cpp src/engine/fileOps/tfm.cpp src/engine/fileOps/xm.cpp +src/engine/fileOps/p.cpp +src/engine/fileOps/p86.cpp +src/engine/fileOps/pdx.cpp +src/engine/fileOps/ppc.cpp +src/engine/fileOps/pps.cpp +src/engine/fileOps/pvi.cpp +src/engine/fileOps/pzi.cpp + src/engine/blip_buf.c src/engine/brrUtils.c src/engine/safeReader.cpp diff --git a/demos/arcade/U.N. Owen Was Her (NS2).fur b/demos/arcade/U.N. Owen Was Her (NS2).fur new file mode 100644 index 000000000..1d5d9e350 Binary files /dev/null and b/demos/arcade/U.N. Owen Was Her (NS2).fur differ diff --git a/doc/2-interface/README.md b/doc/2-interface/README.md index aa16af8d4..7d4300adf 100644 --- a/doc/2-interface/README.md +++ b/doc/2-interface/README.md @@ -18,30 +18,31 @@ the default layout of Furnace is depicted below. - [play/edit controls](play-edit-controls.md) - [instrument/wavetable/sample list](asset-list.md) - [song information](song-info.md) -- [effect list window](effect-list-window.md) - [pattern view](../3-pattern/README.md) +- [effect list window](effect-list-window.md) - [instrument editor](../4-instrument/README.md) - [wavetable editor](../5-wave/README.md) - [sample editor](../6-sample/README.md) ## advanced topics -- [mixer](../8-advanced/mixer.md) -- [grooves](../8-advanced/grooves.md) -- [channels](../8-advanced/channels.md) -- [pattern manager](../8-advanced/pat-manager.md) -- [chip manager](../8-advanced/chip-manager.md) -- [compatibility flags](../8-advanced/compat-flags.md) - [song comments](../8-advanced/comments.md) -- [piano/input pad](../8-advanced/piano.md) +- [channels](../8-advanced/channels.md) +- [chip manager](../8-advanced/chip-manager.md) +- [pattern manager](../8-advanced/pat-manager.md) +- [mixer](../8-advanced/mixer.md) +- [compatibility flags](../8-advanced/compat-flags.md) - [oscilloscope](../8-advanced/osc.md) - [oscilloscope (per channel)](../8-advanced/chanosc.md) +- [oscilloscope (X-Y)](../8-advanced/xyosc.md) - [clock](../8-advanced/clock.md) -- [register view](../8-advanced/regview.md) +- [grooves](../8-advanced/grooves.md) - [log viewer](../8-advanced/log-viewer.md) +- [register view](../8-advanced/regview.md) - [statistics](../8-advanced/stats.md) +- [memory composition](../8-advanced/memory-composition.md) ## other topics -- [basic mode](basic-mode.md) +- [piano/input pad](../8-advanced/piano.md) - [settings](settings.md) diff --git a/doc/2-interface/export.md b/doc/2-interface/export.md index 8d6f32074..2b68a0a9a 100644 --- a/doc/2-interface/export.md +++ b/doc/2-interface/export.md @@ -2,25 +2,27 @@ Furnace allows you to export your song in several formats. this section deals with describing the available export options. -## export audio +## audio this option allows you to export your song in .wav format. I know I know, no .mp3 or .ogg export yet, but you can use a converter. -there are two parameters: - +- **Export type**: + - **one file**: exports your song to one .wav file. + - **multiple files (one per chip)**: exports the output of each chip to .wav files. + - **multiple files (one per channel)**: exports the output of each channel to .wav files. + - useful for usage with a channel visualizer such as corrscope. +- **Bit depth**: default is 16-bit integer. +- **Sample rate**: affects the quality of the output file. + - default is 44100, "CD quality". + - lower sample rates lose fidelity as upper frequencies disappear. + - higher sample rates gain frequencies that can't be heard at the cost of file size and rendering time. +- **Channels in file**: default is 2 (stereo). Set to 1 for mono. - **Loops**: sets the number of times the song will loop. - does not have effect if the song ends with `FFxx` effect. - **Fade out (seconds)**: sets the fade out time when the song is over. - does not have effect if the song ends with `FFxx` effect. -and three export choices: - -- **one file**: exports your song to one .wav file. -- **multiple files (one per chip)**: exports the output of each chip to .wav files. -- **multiple files (one per channel)**: exports the output of each channel to .wav files. - - useful for usage with a channel visualizer such as corrscope. - -## export VGM +## VGM this option allows exporting to a VGM (Video Game Music) file. these can be played back with VGMPlay (for example). @@ -40,9 +42,6 @@ the following settings exist: - **custom**: allows you to specify how many ticks to add. - `0` is effectively none, disabling loop trail completely. - this option will not appear if the loop modality isn't set to None as there wouldn't be a need to. -- **chips to export**: select which chips are going to be exported. - - due to VGM format limitations, you can only select up to two of each chip type. - - some chips will not be available, either because VGM doesn't support these yet, or because you selected an old format version. - **add pattern change hints**: this option adds a "hint" when a pattern change occurs. only useful if you're a developer. - the format of the "hint" data block that gets written is: `67 66 FE ll ll ll ll 01 oo rr pp pp pp ...` - `ll`: length, a 32-bit little-endian number @@ -51,14 +50,11 @@ the following settings exist: - `pp`: pattern index (one per channel) - **direct stream mode**: this option allows DualPCM to work. don't use this for other chips. - may or may not play well with hardware VGM players. +- **chips to export**: select which chips are going to be exported. + - due to VGM format limitations, you can only select up to two of each chip type. + - some chips will not be available, either because VGM doesn't support these yet, or because you selected an old format version. -click on **click to export** to begin exporting. - -## export text - -this option allows you to export your song as a text file. - -## export ZSM +## ZSM ZSM (ZSound Music) is a format designed for the Commander X16 to allow hardware playback. it may contain data for either YM2151 or VERA chips. @@ -72,21 +68,21 @@ the following settings are available: - **loop**: enables loop. if disabled, the song won't loop. - **optimize size**: removes unnecessary commands to reduce size. -click on **Begin Export** to... you know. +## text -## export command stream +this option allows you to export your song as a text file. -this option exports a binary file which contains a dump of the internal command stream produced when playing the song. +## command stream -it's not really useful, unless you're a developer and want to use a command stream dump for some reason (e.g. writing a hardware sound driver). +this option exports a binary file in Furnace's own command stream format (FCS) which contains a dump of the internal command stream produced when playing the song. -- **export**: exports in Furnace's own command stream format (FCS). see `export-tech.md` in `papers/` for details. +it's not really useful, unless you're a developer and want to use a command stream dump for some reason (e.g. writing a hardware sound driver). see `export-tech.md` in `papers/` for details. -## export DMF +## DMF this option allows you to save your song as a .dmf which can be opened in DefleMask. -the following systems are supported when saving as 1.0/legacy: +the following systems are supported when saving as 1.0/legacy (0.12): - Sega Genesis/Mega Drive (YM2612 + SN76489) - Sega Genesis/Mega Drive (YM2612 + SN76489, extended channel 3) - Sega Master System @@ -97,7 +93,7 @@ the following systems are supported when saving as 1.0/legacy: - Arcade (YM2151 + SegaPCM 5-channel compatibility) - Neo Geo CD (DefleMask 1.0+) -the following systems are supported when saving as 1.1.3+: +the following systems are also supported when saving as 1.1.3+: - Sega Master System (with FM expansion) - NES + Konami VRC7 - Famicom Disk System diff --git a/doc/2-interface/keyboard.md b/doc/2-interface/keyboard.md index 317538567..675ba0b55 100644 --- a/doc/2-interface/keyboard.md +++ b/doc/2-interface/keyboard.md @@ -39,7 +39,7 @@ the keys in the "Global hotkeys" section can be used in any window, although not | Panic | `F12` | | | | | **Window activation** | | -| Find/Replace | Ctrl-F | +| Find/Replace | `Ctrl-F` | | Settings | — | | Song Information | — | | Subsongs | — | @@ -63,16 +63,23 @@ the keys in the "Global hotkeys" section can be used in any window, although not | Piano | — | | Oscilloscope (master) | — | | Oscilloscope (per-channel) | — | +| Oscilloscope (X-Y) | — | | Volume Meter | — | | Clock | — | | Register View | — | | Log Viewer | — | | Statistics | — | +| Memory Composition | — | | Effect List | — | | Debug Menu | `Ctrl-Shift-D` | +| Command Stream Player | — | | About | — | | Collapse/expand current window | — | | Close current window | `Shift-Escape` | +| Command Palette | `Ctrl-P` | +| Recent files (Palette) | — | +| Insstruments (Palette) | — | +| Samples (Palette) | — | | | | | **Note input** | | | _see "note input" section after table_ | | @@ -102,8 +109,8 @@ the keys in the "Global hotkeys" section can be used in any window, although not | Move cursor down by one (override Edit Step) | `Shift-End` | | Move cursor to previous channel | — | | Move cursor to next channel | — | -| Move cursor to next channel (overflow) | — | | Move cursor to previous channel (overflow) | — | +| Move cursor to next channel (overflow) | — | | Move cursor to beginning of pattern | `Home` | | Move cursor to end of pattern | `End` | | Move cursor up (coarse) | `PageUp` | @@ -118,6 +125,10 @@ the keys in the "Global hotkeys" section can be used in any window, although not | Expand selection to end of pattern | — | | Expand selection upwards (coarse) | `Shift-PageUp` | | Expand selection downwards (coarse) | `Shift-PageDown` | +| Move selection up by one | `Alt-Up` | +| Move selection down by one | `Alt-Down` | +| Move selection to previous channel | `Alt-Left` | +| Move selection to next channel | `Alt-Right` | | Delete | `Delete` | | Pull delete | `Backspace` | | Insert | `Insert` | @@ -143,102 +154,103 @@ the keys in the "Global hotkeys" section can be used in any window, although not | Clear note input latch | — | | | | | **Instrument list** | | -| Add | `Insert` | -| Duplicate | `Ctrl-D` | -| Open | — | -| Open (replace current) | — | -| Save | — | -| Save (.dmp) | — | -| Move up | `Shift-Up` | -| Move down | `Shift-Down` | -| Delete | — | -| Edit | `Shift-Return` | -| Cursor up | `Up` | -| Cursor down | `Down` | -| Toggle folders/standard view | `Ctrl-V` | +| Add instrument | `Insert` | +| Duplicate instrument | `Ctrl-D` | +| Open instrument | — | +| Open instrument (replace current) | — | +| Save instrument | — | +| Save instrument (.dmp) | — | +| Move instrument up in list | `Shift-Up` | +| Move instrument down in list | `Shift-Down` | +| Delete instrument | — | +| Edit instrument | `Shift-Return` | +| Instrument cursor up | `Up` | +| Instrument cursor down | `Down` | +| Instruments: toggle folders/standard view | `Ctrl-V` | | | | | **Wavetable list** | | -| Add | `Insert` | -| Duplicate | `Ctrl-D` | -| Open | — | -| Open (replace current) | — | -| Save | — | -| Save (.dmw) | — | -| Save (raw) | — | -| Move up | `Shift-Up` | -| Move down | `Shift-Down` | -| Delete | — | -| Edit | `Shift-Return` | -| Cursor up | `Up` | -| Cursor down | `Down` | -| Toggle folders/standard view | `Ctrl-V` | +| Add wavetable | `Insert` | +| Duplicate wavetable | `Ctrl-D` | +| Open wavetable | — | +| Open wavetable (replace current) | — | +| Save wavetable | — | +| Save wavetable (.dmw) | — | +| Save wavetable (raw) | — | +| Move wavetable up in list | `Shift-Up` | +| Move wavetable down in list | `Shift-Down` | +| Delete wavetable | — | +| Edit wavetable | `Shift-Return` | +| Wavetable cursor up | `Up` | +| Wavetable cursor down | `Down` | +| Wavetables: toggle folders/standard view | `Ctrl-V` | | | | | **Sample list** | | -| Add | `Insert` | -| Duplicate | `Ctrl-D` | -| Create wavetable from selection | `Ctrl-W` | -| Open | — | -| Open (replace current) | — | -| Import raw data | — | -| Import raw data (replace current) | — | -| Save | — | -| Save (raw) | — | -| Move up | `Shift-Up` | -| Move down | `Shift-Down` | -| Delete | — | -| Edit | `Shift-Return` | -| Cursor up | `Up` | -| Cursor down | `Down` | -| Preview | — | -| Stop preview | — | -| Toggle folders/standard view | `Ctrl-V` | +| Add sample | `Insert` | +| Duplicate sample | `Ctrl-D` | +| Sample Editor: Create wavetable from selection | `Ctrl-W` | +| Open sample | — | +| Open sample (replace current) | — | +| Import raw sample data | — | +| Import raw sample data (replace current) | — | +| Save sample | — | +| Save sample (raw) | — | +| Move sample up in list | `Shift-Up` | +| Move sample down in list | `Shift-Down` | +| Delete sample | — | +| Edit sample | `Shift-Return` | +| Sample cursor up | `Up` | +| Sample cursor down | `Down` | +| Sample Preview | — | +| Stop sample preview | — | +| Samples: Toggle folders/standard view | `Ctrl-V` | +| Samples: Make me a drum kit | — | | | | | **Orders** | | | Previous order | `Up` | | Next order | `Down` | -| Cursor left | `Left` | -| Cursor right | `Right` | -| Increase value | — | -| Decrease value | — | -| Switch edit mode | — | -| Toggle alter entire row | `Ctrl-L` | -| Add | `Insert` | -| Duplicate | `Ctrl-D` | -| Deep clone | `Ctrl-Shift-D` | -| Duplicate to end of song | `Ctrl-E` | -| Deep clone to end of song | `Ctrl-Shift-E` | -| Remove | `Delete` | -| Move up | `Shift-Up` | -| Move down | `Shift-Down` | -| Replay | — | +| Order cursor left | `Left` | +| Order cursor right | `Right` | +| Increase order value | — | +| Decrease order value | — | +| Switch order edit mode | — | +| Order: Toggle alter entire row | `Ctrl-L` | +| Add order | `Insert` | +| Duplicate order | `Ctrl-D` | +| Deep clone order | `Ctrl-Shift-D` | +| Copy current order to end of song | `Ctrl-E` | +| Deep clone current order to end of song | `Ctrl-Shift-E` | +| Remove order | `Delete` | +| Move order up | `Shift-Up` | +| Move order down | `Shift-Down` | +| Replay order | — | | | | | **Sample editor** | | -| Edit mode: Select | `Shift-I` | -| Edit mode: Draw | `Shift-D` | -| Cut | `Ctrl-X` | -| Copy | `Ctrl-C` | -| Paste | `Ctrl-V` | -| Paste replace | `Ctrl-Shift-V` | -| Paste mix | `Ctrl-Alt-V` | -| Select all | `Ctrl-A` | -| Resize | `Ctrl-R` | -| Resample | `Ctrl-E` | -| Amplify | `Ctrl-B` | -| Normalize | `Ctrl-N` | -| Fade in | `Ctrl-I` | -| Fade out | `Ctrl-O` | -| Insert silence | `Insert` | -| Apply silence | `Shift-Delete` | -| Delete | `Delete` | -| Trim | `Ctrl-Delete` | -| Reverse | `Ctrl-T` | -| Invert | `Ctrl-Shift-T` | -| Signed/unsigned exchange | `Ctrl-U` | -| Apply filter | `Ctrl-F` | -| Preview sample | — | -| Stop sample preview | — | -| Zoom in | `Ctrl-=` | -| Zoom out | `Ctrl--` | -| Toggle auto-zoom | `Ctrl-0` | -| Create instrument from sample | — | -| Set loop to selection | `Ctrl-L` | +| Sample editor mode: Select | `Shift-I` | +| Sample editor mode: Draw | `Shift-D` | +| Sample editor: Cut | `Ctrl-X` | +| Sample editor: Copy | `Ctrl-C` | +| Sample editor: Paste | `Ctrl-V` | +| Sample editor: Paste replace | `Ctrl-Shift-V` | +| Sample editor: Paste mix | `Ctrl-Alt-V` | +| Sample editor: Select all | `Ctrl-A` | +| Sample editor: Resize | `Ctrl-R` | +| Sample editor: Resample | `Ctrl-E` | +| Sample editor: Amplify | `Ctrl-B` | +| Sample editor: Normalize | `Ctrl-N` | +| Sample editor: Fade in | `Ctrl-I` | +| Sample editor: Fade out | `Ctrl-O` | +| Sample editor: Insert silence | `Insert` | +| Sample editor: Apply silence | `Shift-Delete` | +| Sample editor: Delete | `Delete` | +| Sample editor: Trim | `Ctrl-Delete` | +| Sample editor: Reverse | `Ctrl-T` | +| Sample editor: Invert | `Ctrl-Shift-T` | +| Sample editor: Signed/unsigned exchange | `Ctrl-U` | +| Sample editor: Apply filter | `Ctrl-F` | +| Sample editor: Preview sample | — | +| Sample editor: Stop sample preview | — | +| Sample editor: Zoom in | `Ctrl-=` | +| Sample editor: Zoom out | `Ctrl--` | +| Sample editor: Toggle auto-zoom | `Ctrl-0` | +| Sample editor: Create instrument from sample | — | +| Sample editor: Set loop to selection | `Ctrl-L` | diff --git a/doc/2-interface/menu-bar.md b/doc/2-interface/menu-bar.md index ec3c6b1bf..fff10825b 100644 --- a/doc/2-interface/menu-bar.md +++ b/doc/2-interface/menu-bar.md @@ -2,24 +2,20 @@ the menu bar allows you to select from five menus: file, edit, settings, window and help. -items in _italic_ don't appear in basic mode and are only available in advanced mode. - ## file -- **new...**: creates a new song. +- **new...**: opens the new song dialog to choose a system. + - click a system name to create a new song with it. + - some systems have several variants, which are inside a group. - **open...**: opens the file picker, allowing you to select a song to open. - see [file formats](formats.md) for a list of formats Furnace is able to open. - **open recent**: contains a list of the songs you've opened before. - **clear history**: erases the file history. - - **save**: saves the current song. - opens the file picker if this is a new song, or a backup. - **save as...**: opens the file picker, allowing you to save the song under a different name. - -- **export**: allows you to export your song into other formats, such as audio files, VGM and more. see the [export](export.md) page for more information. - +- **export...**: allows you to export your song into other formats, such as audio files, VGM and more. see the [export](export.md) page for more information. - **manage chips**: opens the [Chip Manager](../8-advanced/chip-manager.md) dialog. - - **restore backup**: restores a previously saved backup. - Furnace keeps up to 5 backups of a song. - the backup directory is located in: @@ -28,21 +24,18 @@ items in _italic_ don't appear in basic mode and are only available in advanced - Linux/other: `~/.config/furnace/backups` - this directory grows in size as you use Furnace. remember to delete old backups periodically to save space. - **do NOT rely on the backup system as auto-save!** you should save a restored backup because Furnace will not save backups of backups. - - **exit**: closes Furnace. ## edit - **...**: does nothing except prevent accidental clicks on later menu items if the menu is too tall to fit on the program window. - - **undo**: reverts the last action. - **redo**: repeats what you undid previously. - - **cut**: moves the current selection in the pattern view to clipboard. - **copy**: copies the current selection in the pattern view to clipboard. - **paste**: inserts the clipboard's contents in the cursor position. - you may be able to paste from OpenMPT as well. -- _**paste special...**:_ variants of the paste feature. +- **paste special...**: variants of the paste feature. - **paste mix**: inserts the clipboard's contents in the cursor position, but does not erase the occupied region. - **paste mix (background)**: does the same thing as paste mix, but doesn't alter content which is already there. - **paste with ins (foreground)**: same thing as paste mix, but changes the instrument. @@ -55,81 +48,84 @@ items in _italic_ don't appear in basic mode and are only available in advanced - if the selection is tall, it will select the entire column. - if a column is already selected, it will select the entire channel. - if a channel is already selected, it will select the entire pattern. - -- _**operation mask**:_ toggles which columns will be affected by the listed operations. [more information here.](../8-advanced/opmask.md) -- _**input latch**:_ determines which data are placed along with a note. [more information here.](../8-advanced/inputlatch.md) - +- **operation mask**: toggles which columns will be affected by the listed operations. [more information here.](../8-advanced/opmask.md) +- **input latch**: determines which data are placed along with a note. [more information here.](../8-advanced/inputlatch.md) - **note/octave up/down**: transposes notes in the current selection. - - **values up/down**: changes values in the current selection by ±1 or ±16. - - **transpose**: transpose notes or change values by a specific amount. - - **interpolate**: fills in gaps in the selection by interpolation between values. -- **change instrument**: changes the instrument number in a selection. -- **gradient/fade**: replace the selection with a "gradient" that goes from the beginning of the selection to the end. +- **change instrument...**: changes the instrument number in a selection. +- **gradient/fade...**: replace the selection with a "gradient" that goes from the beginning of the selection to the end. - does not affect the note column. - **Nibble mode**: when enabled, the fade will be per-nibble (0 to F) rather than per-value (00 to FF). - use for effects like `04xy` (vibrato). -- **scale**: scales values in the selection by a specific amount. +- **scale...**: scales values in the selection by a specific amount. - use to change volume in a selection for example. - **randomize**: replaces the selection with random values. - does not affect the note column. + - **Nibble mode**: when enabled, the randomization will be per-nibble (0 to F) rather than per-value (00 to FF). - **invert values**: `00` becomes `FF`, `01` becomes `FE`, `02` becomes `FD` and so on. - - **flip selection**: flips the selection so it is backwards. - **collapse/expand amount**: allows you to specify how much to collapse/expand in the next two menu items. - **collapse**: shrinks the selected contents. - **expand**: expands the selected contents. - - **collapse pattern**: same as collapse, but affects the entire pattern. - **expand pattern**: same as expand, but affects the entire pattern. - - **collapse song**: same as collapse, but affects the entire song. - it also changes speeds and pattern length to compensate. - **expand song**: same as expand, but affects the entire song. - it also changes speeds and pattern length to compensate. - -- _**find/replace**:_ shows [the Find/Replace window](../8-advanced/find-replace.md). - -- **clear**: opens a window that allows you to mass-delete things like songs, unused instruments, and the like. +- **find/replace**: shows [the Find/Replace window](../8-advanced/find-replace.md). +- **clear...**: opens a window that allows you to mass-delete things like songs, unused instruments, and the like. ## settings - **full screen**: expands the Furnace window so it covers your screen. - **lock layout**: prevents you from dragging/resizing docked windows, or docking more. -- **basic mode**: toggles [Basic Mode](basic-mode.md). -- **visualizer**: toggles pattern view particle effects when the song plays. +- **pattern visualizer**: toggles pattern view particle effects when the song plays. - **reset layout**: resets the workspace to its defaults. -- **settings...**: shows the Settings window. these are detailed in [settings.md]. +- **user systems...**: shows the User Systems window. this is detailed in [the User Systems documentation](../8-advanced/user-systems.md). +- **settings...**: shows the Settings window. these are detailed in [the Settings documentation](settings.md). ## window all these menu items show or hide their associated windows. -- [song information](song-info.md) -- [subsongs](song-info.md) -- [speed](song-info.md) -- [instruments](../4-instrument/README.md) -- [wavetables](../5-wave/README.md) -- [samples](../6-sample/README.md) -- [orders](order-list.md) -- [pattern](../3-pattern/README.md) -- _[mixer](../8-advanced/mixer.md)_ -- _[grooves](../8-advanced/grooves.md)_ -- _[channels](../8-advanced/channels.md)_ -- _[pattern manager](../8-advanced/pat-manager.md)_ -- _[chip manager](../8-advanced/chip-manager.md)_ -- _[compatibility flags](../8-advanced/compat-flags.md)_ -- [song comments](../8-advanced/comments.md) - -- [piano](../8-advanced/piano.md) -- [oscilloscope](../8-advanced/osc.md) -- [oscilloscopes (per-channel)](../8-advanced/chanosc.md) -- [clock](../8-advanced/clock.md) -- [register view](../8-advanced/regview.md) -- [log viewer](../8-advanced/log-viewer.md) -- [stats](../8-advanced/stats.md) +- song + - **[song comments](../8-advanced/comments.md)** + - **[song information](song-info.md)** + - **[subsongs](song-info.md)** + - **[channels](../8-advanced/channels.md)** + - **[chip manager](../8-advanced/chip-manager.md)** + - **[orders](order-list.md)** + - **[pattern](../3-pattern/README.md)** + - **[pattern manager](../8-advanced/pat-manager.md)** + - **[mixer](../8-advanced/mixer.md)** + - **[compatibility flags](../8-advanced/compat-flags.md)** +- assets + - **[instruments](../4-instrument/README.md)** + - **[samples](../6-sample/README.md)** + - **[wavetables](../5-wave/README.md)** + - **[instrument editor](../4-instrument/README.md)** + - **[sample editor](../6-sample/README.md)** + - **[wavetable editor](../5-wave/README.md)** +- visualizers + - **[oscilloscope](../8-advanced/osc.md)** + - **[oscilloscope (per-channel)](../8-advanced/chanosc.md)** + - **[oscilloscope (X-Y)](../8-advanced/xyosc.md)** + - volume meter +- tempo + - **[clock](../8-advanced/clock.md)** + - **[grooves](../8-advanced/grooves.md)** + - **[speed](song-info.md)** +- debug + - **[log viewer](../8-advanced/log-viewer.md)** + - **[register view](../8-advanced/regview.md)** + - **[statistics](../8-advanced/stats.md)** + - **[memory composition](../8-advanced/memory-composition.md)** +- **[effect list](../3-pattern/effects.md)** +- **[play/edit controls](play-edit-controls.md)** +- **[piano/input pad](../8-advanced/piano.md)** ## help diff --git a/doc/2-interface/play-edit-controls.md b/doc/2-interface/play-edit-controls.md index d7814ac25..47e4f103b 100644 --- a/doc/2-interface/play-edit-controls.md +++ b/doc/2-interface/play-edit-controls.md @@ -13,6 +13,7 @@ the "Play/Edit Controls" are used to control playback and change parameters of t - **Poly**: turns on polyphony for previewing notes. toggles to **Mono** for monophony (one note at a time only). - **Octave**: sets current input octave. - **Step**: sets edit step. if this is 1, entering a note or effect will move to the next row. if this is a larger number, rows will be skipped. if this is 0, the cursor will stay in place. + - if clicked, Step becomes **Coarse**, which sets the number of rows moved with `PgUp`, `PgDn`, and related movement shortcuts. clicking again will revert it to Step. - **Follow orders**: if on, the selected order in the orders window will follow the song during playback. - **Follow pattern**: if on, the cursor will follow playback and the song will scroll by as it plays. diff --git a/doc/2-interface/settings.md b/doc/2-interface/settings.md index ec61e9371..cfe12c949 100644 --- a/doc/2-interface/settings.md +++ b/doc/2-interface/settings.md @@ -1,25 +1,31 @@ # settings -the Settings window allows you to change Furnace setting. +the Settings window allows you to change Furnace settings. + +settings are saved when clicking the **OK** or **Apply** buttons at the bottom of the window, and when closing the program. several backups are kept in the Furnace settings directory. -settings are saved when clicking the **OK** or **Apply** buttons at the bottom of the window. ## General ### Program +- **Language**: select the language used for the interface. some languages are incomplete, and are listed with their approximate completion percentage. - **Render backend**: changing this may help with performace or compatibility issues. the available render backends are: + - SDL Renderer: this was the only available render backend prior to the addition of dedicated OpenGL/DirectX backends in 0.6. default on macOS. + - it is slower than the other backends. + - DirectX 11: works with the majority of graphics chips/cards and is optimized specifically for Windows. + - DirectX 9: use if your hardware is incompatible with DirectX 11. - OpenGL 3.0: works with the majority of graphics chips/cards (from 2010 onwards). default on Linux. - OpenGL 2.0: use if you have a card without OpenGL 3.0 support. - OpenGL 1.1: use if your card doesn't even support OpenGL 2.0. - - DirectX 11: works with the majority of graphics chips/cards and is optimized specifically for Windows. - - SDL Renderer: this was the only available render backend prior to the addition of dedicated OpenGL/DirectX backends in 0.6. default on macOS. - - it is slower than the other backends. - Software: this is a last resort backend which renders the interface in software. very slow! -- **Render driver**: this setting appears when using the SDL Renderer backend. it allows you to select an SDL render driver. +- **Advanced render backend settings**: only applicable with some render backends. + - **Render driver**: this setting only appears when using the SDL Renderer backend. it allows you to select an SDL render driver. + - OpenGL settings: these only appear when using an OpenGL backend, and should only be adjusted if the display is incorrect. - **VSync**: synchronizes rendering to VBlank and eliminates tearing. - **Frame rate limit**: allows you to set a frame rate limit (in frames per second). - only has effect when VSync is off or not available (e.g. software rendering or force-disabled on driver settings). +- **Display render time**: displays frame rate and frame render time at the right side of the menu bar. - **Late render clear**: this option is only useful when using old versions of Mesa drivers. it force-waits for VBlank by clearing after present, reducing latency. - **Power-saving mode**: saves power by lowering the frame rate to 2fps when idle. - may cause issues under Mesa drivers! @@ -49,6 +55,9 @@ settings are saved when clicking the **OK** or **Apply** buttons at the bottom o - **Remember last values** - **Store instrument name in .fui**: when enabled, saving an instrument will store its name. this may increase file size. - **Load instrument name from .fui**: when enabled, loading an instrument will use the stored name (if present). otherwise, it will use the file name. +- **Auto-fill file name when saving**: pre-fill the file name field when saving or exporting. + - when saving a module, the existing file name will be auto-filled. + - when saving an instrument or sample, its name will be auto-filled. ### New Song @@ -79,6 +88,11 @@ settings are saved when clicking the **OK** or **Apply** buttons at the bottom o - **New instruments are blank**: when enabled, adding FM instruments will make them blank (rather than loading the default one). +### Configuration +- **Import**: select an exported `.ini` config file to overwrite current settings. +- **Export**: select an `.ini` file to save current settings. +- **Factory Reset**: resets all settings to default and purges settings backups. + ## Audio ### Output @@ -100,9 +114,7 @@ settings are saved when clicking the **OK** or **Apply** buttons at the bottom o - **Sample rate**: audio output rate. - a lower rate decreases quality and isn't really beneficial. - if using PortAudio backend, be careful about this value. -- **Outputs**: number of audio outputs created, up to 16. - - only appears when Backend is JACK. -- **Channels**: mono, stereo or something. +- **Outputs**: number of audio outputs created, up to 16. default is 2 (stereo). - **Buffer size**: size of buffer in both samples and milliseconds. - setting this to a low value may cause stuttering/glitches in playback (known as "underruns" or "xruns"). - setting this to a high value increases latency. @@ -135,6 +147,7 @@ settings are saved when clicking the **OK** or **Apply** buttons at the bottom o ### MIDI input - **MIDI input**: input device. + - **Rescan MIDI devices**: repopulates list with all currently connected MIDI devices. useful if a device is connected while Furnace is running. - **Note input**: enables note input. disable if you intend to use this device only for binding actions. - **Velocity input**: enables velocity input when entering notes in the pattern. - **Map MIDI channels to direct channels**: when enabled, notes from MIDI channels will be mapped to channels rather than the cursor position. @@ -291,6 +304,7 @@ below all the binds, select a key from the dropdown list to add it. it will appe - **Push value when overwriting instead of clearing it**: in the order list and pattern editors, typing into an already-filled value will shift digits instead of starting fresh. for example: - if off: moving the cursor onto the value `A5` and typing a "B" results in `0B`. - if on: moving the cursor onto the value `A5` and typing a "B" results in `5B`. +- **Keyboard note/value input repeat (hold key to input continuously)** - **Effect input behavior:** - **Move down**: after entering an effect (or effect value), the cursor moves down. - **Move to effect value (otherwise move down)**: after entering an effect, the cursor moves to its value. if entering a value, the cursor moves down. @@ -359,11 +373,7 @@ below all the binds, select a key from the dropdown list to add it. it will appe - **Pattern font** font for the pattern view, the order list, and related. - if "Custom...", a file path selector will appear. - **Size**: font size. -- **Display Japanese characters**, **Display Chinese (Simplified) characters**, **Display Chinese (Traditional) characters** and **Display Korean characters**: only toggle these options if you have enough graphics memory. - - these are a temporary solution until dynamic font atlas is implemented in Dear ImGui. - #### FreeType-specific settings - - **Anti-aliased fonts**: when enabled, fonts will be rendered smooth. - **Support bitmap fonts**: this option allows you to enable the loading of bitmap fonts. - be noted that this may force non-bitmap fonts to undesired sizes! @@ -376,6 +386,13 @@ below all the binds, select a key from the dropdown list to add it. it will appe - **Disable**: only rely upon font hinting data. - **Enable**: prefer font hinting data if present. - **Force**: ignore font hinting data. +#### non-specific settings +- **Oversample**: renders the font internally at higher resolution for visual quality. + - higher settings use more video memory. + - for pixel or bitmap fonts, set this to **1x**. +- **Load fallback font**: load an extra font that contains nearly all characters that can be used, in case the selected fonts lack them. uses much video memory +- **Display Japanese characters**, **Display Chinese (Simplified) characters**, **Display Chinese (Traditional) characters** and **Display Korean characters**: only toggle these options if you have enough graphics memory. + - these are a temporary solution until dynamic font atlas is implemented in Dear ImGui. ### Program @@ -386,15 +403,16 @@ below all the binds, select a key from the dropdown list to add it. it will appe - **/path/to/file.fur - Furnace** - **Display system name on title bar** - **Display chip names instead of "multi-system" in title bar** -- **Export options layout:** - - **Sub-menus in File menu**: export options appear in the File menu as sub-menus. - - **Modal window with tabs**: a single "export..." option that opens a dialog with export options. this is the default. - - **Modal windows with options in File menu**: like Sub-menus in File menu, but instead of being sub-menus, selecting one opens a dialog with export settings. - **Status bar:** - **Cursor details** - **File path** - **Cursor details or file path** - **Nothing** +- **Display playback status when playing**: display playback time and current location in the menu bar. +- **Export options layout:** + - **Sub-menus in File menu**: export options appear in the File menu as sub-menus. + - **Modal window with tabs**: a single "export..." option that opens a dialog with export options. this is the default. + - **Modal windows with options in File menu**: like Sub-menus in File menu, but instead of being sub-menus, selecting one opens a dialog with export settings. - **Capitalize menu bar** - **Display add/configure/change/remove chip menus in File menu**: if enabled, the "manage chips" item in the file menu is split into the four listed items for quick access. @@ -532,10 +550,10 @@ below all the binds, select a key from the dropdown list to add it. it will appe - **Rounded window corners** - **Rounded buttons** - **Rounded menu corners** +- **Rounded tabs** +- **Rounded scrollbars** - **Borders around widgets**: draws borders on buttons, checkboxes, text widgets, and the like. - - ## Color ### Color scheme @@ -544,9 +562,28 @@ below all the binds, select a key from the dropdown list to add it. it will appe - **Export** - **Reset defaults** - **Guru mode**: exposes all color options (instead of accent colors). -- **General** +- **Interface** + - **Frame shading**: applies a gradient effect to buttons and input boxes. - **Color scheme type:** - **Dark** - **Light** - - **Frame shading**: applies a gradient effect to buttons and input boxes. + - **Accent colors**: select main interface colors. + - **Primary** + - **Secondary** - several more categories... + +## Backup + +### Configuration + +- **Enable backup system**: turn on automatic backups of the current open file. +- **Interval (in seconds)**: time between automatic backups. +- **Backups per file**: maximum number of backups to store for each file. oldest backups are deleted first. + +### Backup Management + +- **Purge before**: + - **Go**: purge all backups from before the selected date. +- total space used by all backups: + - **Refresh**: recalculate space. + - **Delete All**: purge all backups. diff --git a/doc/2-interface/song-info.md b/doc/2-interface/song-info.md index 24a42f95e..f9773807c 100644 --- a/doc/2-interface/song-info.md +++ b/doc/2-interface/song-info.md @@ -3,11 +3,11 @@ - **Name**: the track's title. - **Author**: the author(s) of this track. - **Album**: the associated album name (or the name of the game the song is from). -- **System**: the game console or computer the track is designed for. this is automatically set when creating a new tune, but in advanced mode, it can be changed to anything one wants. the **Auto** button will provide a guess based on the chips in use. +- **System**: the name of the game console or computer the track is designed for. this is automatically set when creating a new tune, but can be changed to anything. the **Auto** button will provide a guess based on the chips in use. all of this metadata will be included in a VGM export. this isn't the case for an audio export, however. -- _**Tuning (A-4)**_: set tuning based on the note A-4, which should be 440 in most cases. opening an Amiga MOD will set it to 436 for hardware compatibility. available only in advanced mode. +- **Tuning (A-4)**: set tuning based on the note A-4, which should be 440 in most cases. opening an Amiga MOD will set it to 436 for hardware compatibility. ## subsongs @@ -23,16 +23,17 @@ this window allows one to create **subsongs** - multiple individual songs within there are multiple ways to set the tempo of a song. -items in _italic_ don't appear in basic mode and are only available in advanced mode. +**Base Tempo**: tempo in beats per minute (BPM). this is affected by the Highlight settings below. +- clicking the Base Tempo button switches to the more technical Tick Rate. **Tick Rate**: the frequency of ticks per second, thus the rate at which notes and effects are processed. - all values are allowed for all chips, though most chips have hardware limitations that mean they should stay at either 60 (approximately NTSC) or 50 (exactly PAL). -- clicking the Tick Rate button switches to a more traditional **Base Tempo** BPM setting. +- clicking the Tick Rate button switches to the more traditional Base Tempo BPM setting. **Speed**: the number of ticks per row. - clicking the "Speed" button changes to more complex modes covered in the [grooves](../8-advanced/grooves.md) page. -_**Virtual Tempo**:_ Simulates any arbitrary tempo without altering the tick rate. it does this by adding or skipping ticks to approximate the tempo. the two numbers represent a ratio applied to the actual tick rate. example: +**Virtual Tempo**: simulates any arbitrary tempo without altering the tick rate. it does this by adding or skipping ticks to approximate the tempo. the two numbers represent a ratio applied to the actual tick rate. example: - set tick rate to 150 BPM (60 Hz) and speed to 6. - set the first virtual tempo number (numerator) to 200. - set the second virtual tempo number (denominator) to 150. @@ -40,7 +41,8 @@ _**Virtual Tempo**:_ Simulates any arbitrary tempo without altering the tick rat - the ratio doesn't have to match BPM numbers. set the numerator to 4 and the denominator to 5, and the virtual BPM becomes 150 × 4/5 = 120. - another way to accomplish this with more control over the results is to use grooves. see the page on [grooves](../8-advanced/grooves.md) for details. -_**Divider**:_ Changes the effective tick rate. a tick rate of 60Hz and a divisor of 6 will result in ticks lasting a tenth of a second each! +**Divider**: changes the effective tick rate. a tick rate of 60Hz and a divisor of 6 will result in ticks lasting a tenth of a second each! +- to the right, the effective BPM is listed, taking all settings into account. **Highlight**: sets the pattern row highlights: - the first value represents the number of rows per beat. @@ -50,4 +52,4 @@ _**Divider**:_ Changes the effective tick rate. a tick rate of 60Hz and a diviso **Pattern Length**: the length of each pattern in rows. this affects all patterns in the song, and every pattern must be the same length. (Individual patterns can be cut short by `0Bxx`, `0Dxx`, and `FFxx` commands.) -_**Song Length**:_ how many orders are in the order list. decreasing it will hide the orders at the bottom. increasing it will restore those orders; increasing it further will add new orders of all `00` patterns. +**Song Length**: how many orders are in the order list. decreasing it will hide the orders at the bottom. increasing it will restore those orders; increasing it further will add new orders of all `00` patterns. diff --git a/doc/3-pattern/README.md b/doc/3-pattern/README.md index 31713ea64..77060b5f2 100644 --- a/doc/3-pattern/README.md +++ b/doc/3-pattern/README.md @@ -119,6 +119,10 @@ Shift-Up | expand selection upwards Shift-Down | expand selection downwards Shift-Left | expand selection to the left Shift-Right | expand selection to the right +Alt-Up | move selection up by one +Alt-Down | move selection down by one +Alt-Left | move selection to previous channel +Alt-Right | move selection to next channel Backspace | delete note at cursor and/or pull pattern upwards (configurable) Delete | delete selection Insert | create blank row at cursor position and push pattern diff --git a/doc/6-sample/README.md b/doc/6-sample/README.md index 2e870c95d..5ebe571c1 100644 --- a/doc/6-sample/README.md +++ b/doc/6-sample/README.md @@ -89,6 +89,7 @@ in there, you can modify certain data pertaining to your sample, such as the: - **Open**: replaces current sample. - right-clicking brings up a menu: - **import raw...**: brings up a file selector, then presents a dialog to choose the format of the selected file. + - **import raw (replace)...**: same as above, but instead of adding it to the sample list, it replaces the currently selected sample. - **Save**: saves current sample to disk. - right-clicking brings up a menu: - **save raw...**: brings up a file selector, then saves the sample as raw data. diff --git a/doc/7-systems/gba.md b/doc/7-systems/gba.md index 692d8db0d..a0653a5fe 100644 --- a/doc/7-systems/gba.md +++ b/doc/7-systems/gba.md @@ -15,11 +15,11 @@ it features echo and up to 16 voices. - `10xx`: **change wave.** - `11xy`: **configure echo.** - - this effect is kinda odd. this is how it works: - -> How do you echo on GBA -> -> Create an empty instrment and put a very high note of it in channel 1 then do 110x in effect column and set volume column to set feedback and do nothing else on it + - this effect is kinda odd. here's how to use it: + - create an empty instrument and put a very high note of it in channel 1. + - put `110x` in the effect column. + - set volume column to set feedback. + - don't use the channel for anything else. - `12xy`: **toggle invert.** - `x` left channel. diff --git a/doc/7-systems/snes.md b/doc/7-systems/snes.md index 7b39ed87f..978af2106 100644 --- a/doc/7-systems/snes.md +++ b/doc/7-systems/snes.md @@ -59,7 +59,7 @@ Furnace also allows the SNES to use wavetables (and the wavetable synthesizer) i - `00` to `7F` for 0 to 127. - `80` to `FF` for -128 to -1. - note: be sure the sum of all coefficients is between -128 and 127. sums outside that may result in overflow and therefore clicking. - - see [SnesLab](https://sneslab.net/wiki/FIR_Filter) for a full explanation and examples. + - see SnesLab for [echo filter explanations and examples](https://sneslab.net/wiki/FIR_Filter#Uses). ## info @@ -67,6 +67,8 @@ this chip uses the [SNES](../4-instrument/snes.md) instrument editor. when two channels are joined for pitch modulation, the channel bar will show `mod` on a bracket tying them together. +when using sample offset commands, be sure to open each involved sample in the sample editor, look to the "Info" section at the top-left, and check the "no BRR filters" box. this prevents sound glitches, at the cost of lowering the sample quality to 4-bit. + ## channel status the following icons are displayed when channel status is enabled in the pattern view: @@ -93,6 +95,8 @@ the following options are available in the Chip Manager window: - **Feedback**: sets how much of the echo output will be fed back into the buffer. - **Echo volume**: sets echo volume. - **Echo filter**: adjusts echo filter. +- **Dec/Hex**: toggles decimal or hexadecimal mode for the filter settings text entry box to the right. + - SnesLab provides [echo filter explanations and examples](https://sneslab.net/wiki/FIR_Filter#Uses). their example filter strings can be pasted directly into the filter settings text entry box if set to Hex mode. ## ADSR diff --git a/doc/8-advanced/README.md b/doc/8-advanced/README.md index 4a9512823..759737e0e 100644 --- a/doc/8-advanced/README.md +++ b/doc/8-advanced/README.md @@ -8,22 +8,27 @@ as listed in the "Edit" menu: as listed in the "Window" menu: -- [mixer](mixer.md) -- [grooves](grooves.md) -- [channel manager](channels.md) -- [pattern manager](pat-manager.md) -- [chip manager](chip-manager.md) -- [compatibility flags](compat-flags.md) -- [song comments](comments.md) - -- [piano](piano.md) -- [oscilloscope](osc.md) -- [oscilloscope (X-Y)](xyosc.md) -- [oscilloscopes (per-channel)](chanosc.md) -- [clock](clock.md) -- [register view](regview.md) -- [log viewer](log-viewer.md) -- [stats](stats.md) +- song + - [song comments](../8-advanced/comments.md) + - [channels](../8-advanced/channels.md) + - [chip manager](../8-advanced/chip-manager.md) + - [pattern manager](../8-advanced/pat-manager.md) + - [mixer](../8-advanced/mixer.md) + - [compatibility flags](../8-advanced/compat-flags.md) +- visualizers + - [oscilloscope](../8-advanced/osc.md) + - [oscilloscope (per-channel)](../8-advanced/chanosc.md) + - [oscilloscope (X-Y)](../8-advanced/xyosc.md) + - volume meter +- tempo + - [clock](../8-advanced/clock.md) + - [grooves](../8-advanced/grooves.md) +- debug + - [log viewer](../8-advanced/log-viewer.md) + - [register view](../8-advanced/regview.md) + - [statistics](../8-advanced/stats.md) + - [memory composition](../8-advanced/memory-composition.md) +- [piano/input pad](../8-advanced/piano.md) other: diff --git a/doc/8-advanced/channels.md b/doc/8-advanced/channels.md index 5b70f00dd..53bc00065 100644 --- a/doc/8-advanced/channels.md +++ b/doc/8-advanced/channels.md @@ -5,8 +5,9 @@ the "Channels" dialog allows manipulation of the song's channels. ![channels dialog](channels.png) each channel has the following options: -- **Visible**: uncheck the box to hide the channel from the pattern view. pattern data will be kept. -- crossed-arrows button: click and drag to rearrange pattern data throughout the song. +- **Pat**: uncheck the box to hide the channel from the pattern view. pattern data will be kept. +- **Osc**: uncheck the box to hide the channel from the per-channel oscilloscope view. +- **Swap**: click and drag to rearrange pattern data throughout the song. - note: this does **not** move channels around! it only moves the channel's pattern data. - **Name**: the name displayed at the top of each channel in the pattern view. - the next setting is "short name", which is displayed in the orders view and/or when a channel is collapsed. diff --git a/doc/8-advanced/chanosc.md b/doc/8-advanced/chanosc.md index 6a2cd27ed..54163dfb3 100644 --- a/doc/8-advanced/chanosc.md +++ b/doc/8-advanced/chanosc.md @@ -14,6 +14,7 @@ right-clicking the view will display the configuration view shown above: - **Mode 2**: bias slightly toward more columns. - **Mode 3**: always more columns than rows. - **Amplitude**: scales amplitude for all oscilloscope views. +- **Line size**: controls line thickness. - **Gradient**: this allows you to use a gradient for determining the waveforms' colors instead of a single color. see the gradient section for more information. - if this option is off, a color selector will be displayed. right-click on it for some options: - select between the square selector and the color wheel selector. diff --git a/doc/8-advanced/chip-manager.md b/doc/8-advanced/chip-manager.md index 41a187fd9..09388e802 100644 --- a/doc/8-advanced/chip-manager.md +++ b/doc/8-advanced/chip-manager.md @@ -8,6 +8,8 @@ the **chip manager** window allows you to manage chips, including adding, changi **Clone channel data**: when cloning chips, also copy patterns, pattern names, channel names and other parameters to the clone. +**Clone at end**: instead of inserting the clone directly after the cloned chip, add it to the end. + to move a chip around, click and drag the ![crossed-arrows](chip-manager-move.png) button to the left. to duplicate a chip, click the **Clone** button. diff --git a/doc/8-advanced/user-systems.md b/doc/8-advanced/user-systems.md new file mode 100644 index 000000000..d60772654 --- /dev/null +++ b/doc/8-advanced/user-systems.md @@ -0,0 +1,22 @@ +# user systems + +combinations of chips and chip configurations can be stored as **user systems** – presets that are easily accessed when starting a new song. + +![user systems window](user-systems.png) + +the `+` button at the top of the **Systems** list will add a new system. + +next to the **Name** field, the **Remove** button removes the current system from the list. + +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. + +**Save and Close**: as it says. + +**Import**: opens a dialog to select a `.cfgu` file, then adds its systems to the list. + +**Import (replace)**: opens a similar dialog, then clears the existing systems list and replaces it with the imported one. + +**Export**: stores the current list of systems in a selected `.cfgu` file. \ No newline at end of file diff --git a/doc/8-advanced/user-systems.png b/doc/8-advanced/user-systems.png new file mode 100644 index 000000000..c300530a3 Binary files /dev/null and b/doc/8-advanced/user-systems.png differ diff --git a/src/engine/engine.h b/src/engine/engine.h index f010d7001..ac897d136 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -619,6 +619,17 @@ class DivEngine { void loadFF(SafeReader& reader, std::vector& ret, String& stripPath); void loadWOPL(SafeReader& reader, std::vector& ret, String& stripPath); void loadWOPN(SafeReader& reader, std::vector& ret, String& stripPath); + + //sample banks + void loadP(SafeReader& reader, std::vector& ret, String& stripPath); + void loadPPC(SafeReader& reader, std::vector& ret, String& stripPath); + void loadPPS(SafeReader& reader, std::vector& ret, String& stripPath); + void loadPVI(SafeReader& reader, std::vector& ret, String& stripPath); + void loadPDX(SafeReader& reader, std::vector& ret, String& stripPath); + void loadPZI(SafeReader& reader, std::vector& ret, String& stripPath); + void loadP86(SafeReader& reader, std::vector& ret, String& stripPath); + + int loadSampleROM(String path, ssize_t expectedSize, unsigned char*& ret); @@ -1034,7 +1045,8 @@ class DivEngine { int addSamplePtr(DivSample* which); // get sample from file - DivSample* sampleFromFile(const char* path); + //DivSample* sampleFromFile(const char* path); + std::vector sampleFromFile(const char* path); // get raw sample DivSample* sampleFromFileRaw(const char* path, DivSampleDepth depth, int channels, bool bigEndian, bool unsign, bool swapNibbles, int rate); diff --git a/src/engine/fileOps/fileOpsCommon.h b/src/engine/fileOps/fileOpsCommon.h index 306166c55..d3821bf3b 100644 --- a/src/engine/fileOps/fileOpsCommon.h +++ b/src/engine/fileOps/fileOpsCommon.h @@ -63,3 +63,34 @@ enum DivFurVariants: int { DIV_FUR_VARIANT_VANILLA=0, DIV_FUR_VARIANT_B=1, }; + +// MIDI-related +struct midibank_t { + String name; + uint8_t bankMsb, + bankLsb; +}; + +// Reused patch data structures + +// SBI and some other OPL containers + +struct sbi_t { + uint8_t Mcharacteristics, + Ccharacteristics, + Mscaling_output, + Cscaling_output, + Meg_AD, + Ceg_AD, + Meg_SR, + Ceg_SR, + Mwave, + Cwave, + FeedConnect; +}; + +//bool stringNotBlank(String& str); +// detune needs extra translation from register to furnace format +//uint8_t fmDtRegisterToFurnace(uint8_t&& dtNative); + +//void readSbiOpData(sbi_t& sbi, SafeReader& reader); diff --git a/src/engine/fileOps/it.cpp b/src/engine/fileOps/it.cpp index a3ed09c49..a729706d7 100644 --- a/src/engine/fileOps/it.cpp +++ b/src/engine/fileOps/it.cpp @@ -639,6 +639,8 @@ bool DivEngine::loadIT(unsigned char* file, size_t len) { logD("seek not needed..."); } + logV("reading sample data (%d)",s->samples); + if (flags&8) { // compressed sample unsigned int ret=0; logV("decompression begin... (%d)",s->samples); @@ -672,62 +674,66 @@ bool DivEngine::loadIT(unsigned char* file, size_t len) { } logV("got: %d",ret); } else { - if (s->depth==DIV_SAMPLE_DEPTH_16BIT) { - if (flags&4) { // downmix stereo - for (unsigned int i=0; isamples; i++) { - short l; - if (convert&2) { - l=reader.readS_BE(); - } else { - l=reader.readS(); + try { + if (s->depth==DIV_SAMPLE_DEPTH_16BIT) { + if (flags&4) { // downmix stereo + for (unsigned int i=0; isamples; i++) { + short l; + if (convert&2) { + l=reader.readS_BE(); + } else { + l=reader.readS(); + } + if (!(convert&1)) { + l^=0x8000; + } + s->data16[i]=l; } - if (!(convert&1)) { - l^=0x8000; + for (unsigned int i=0; isamples; i++) { + short r; + if (convert&2) { + r=reader.readS_BE(); + } else { + r=reader.readS(); + } + if (!(convert&1)) { + r^=0x8000; + } + s->data16[i]=(s->data16[i]+r)>>1; } - s->data16[i]=l; - } - for (unsigned int i=0; isamples; i++) { - short r; - if (convert&2) { - r=reader.readS_BE(); - } else { - r=reader.readS(); + } else { + for (unsigned int i=0; isamples; i++) { + if (convert&2) { + s->data16[i]=reader.readS_BE()^((convert&1)?0:0x8000); + } else { + s->data16[i]=reader.readS()^((convert&1)?0:0x8000); + } } - if (!(convert&1)) { - r^=0x8000; - } - s->data16[i]=(s->data16[i]+r)>>1; } } else { - for (unsigned int i=0; isamples; i++) { - if (convert&2) { - s->data16[i]=reader.readS_BE()^((convert&1)?0:0x8000); - } else { - s->data16[i]=reader.readS()^((convert&1)?0:0x8000); + if (flags&4) { // downmix stereo + for (unsigned int i=0; isamples; i++) { + signed char l=reader.readC(); + if (!(convert&1)) { + l^=0x80; + } + s->data8[i]=l; + } + for (unsigned int i=0; isamples; i++) { + signed char r=reader.readC(); + if (!(convert&1)) { + r^=0x80; + } + s->data8[i]=(s->data8[i]+r)>>1; + } + } else { + for (unsigned int i=0; isamples; i++) { + s->data8[i]=reader.readC()^((convert&1)?0:0x80); } } } - } else { - if (flags&4) { // downmix stereo - for (unsigned int i=0; isamples; i++) { - signed char l=reader.readC(); - if (!(convert&1)) { - l^=0x80; - } - s->data8[i]=l; - } - for (unsigned int i=0; isamples; i++) { - signed char r=reader.readC(); - if (!(convert&1)) { - r^=0x80; - } - s->data8[i]=(s->data8[i]+r)>>1; - } - } else { - for (unsigned int i=0; isamples; i++) { - s->data8[i]=reader.readC()^((convert&1)?0:0x80); - } - } + } catch (EndOfFileException& e) { + logW("premature end of file..."); } } diff --git a/src/engine/fileOps/p.cpp b/src/engine/fileOps/p.cpp new file mode 100644 index 000000000..9ba650c0e --- /dev/null +++ b/src/engine/fileOps/p.cpp @@ -0,0 +1,124 @@ +/** + * 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 "fileOpsCommon.h" + +class DivEngine; + +//P VOX ADPCM sample bank + +/* ======================================= + Header +======================================= + +0x0000 - 0x03FF 256 * { + Sample start (uint32_t) + - 0x00000000 = unused +} + + +======================================= + Body +======================================= + +Stream of Sample Data { + + MSM6258 ADPCM encoding + nibble-swapped VOX / Dialogic ADPCM + Mono + Sample rate? + 16000Hz seems fine + +} */ + +#define P_BANK_SIZE 256 +#define P_SAMPLE_RATE 16000 + +typedef struct +{ + uint32_t start_pointer; +} P_HEADER; + + +void DivEngine::loadP(SafeReader& reader, std::vector& ret, String& stripPath) +{ + try + { + reader.seek(0, SEEK_SET); + + P_HEADER headers[P_BANK_SIZE]; + + for(int i = 0; i < P_BANK_SIZE; i++) + { + headers[i].start_pointer = (unsigned int)reader.readI_BE(); + } + + for(int i = 0; i < P_BANK_SIZE; i++) + { + if(headers[i].start_pointer != 0) + { + DivSample* s = new DivSample; + + s->rate = P_SAMPLE_RATE; + s->centerRate = P_SAMPLE_RATE; + s->depth = DIV_SAMPLE_DEPTH_VOX; + + reader.seek((int)headers[i].start_pointer, SEEK_SET); + + int sample_pos = 0; + int sample_len = 0; + + if(i < P_BANK_SIZE - 1) + { + sample_len = headers[i + 1].start_pointer - headers[i].start_pointer; + } + else + { + sample_len = (int)reader.size() - headers[i].start_pointer; + } + + if(sample_len > 0) + { + s->init(sample_len * 2); + + for(int j = 0; j < sample_len; j++) + { + unsigned char curr_byte = (unsigned char)reader.readC(); + curr_byte = (curr_byte << 4) | (curr_byte >> 4); + + s->dataVOX[sample_pos] = curr_byte; + sample_pos++; + } + + ret.push_back(s); + logI("p: start %d len %d", headers[i].start_pointer, sample_len); + } + else + { + delete s; + } + } + } + } + catch (EndOfFileException& e) + { + lastError=_("premature end of file"); + logE("premature end of file"); + } +} \ No newline at end of file diff --git a/src/engine/fileOps/p86.cpp b/src/engine/fileOps/p86.cpp new file mode 100644 index 000000000..560867007 --- /dev/null +++ b/src/engine/fileOps/p86.cpp @@ -0,0 +1,142 @@ +/** + * 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 "fileOpsCommon.h" + +class DivEngine; + +//P86 8-bit PCM sample bank + +/* ======================================= + Header +======================================= + +0x0000 Identifier (12b) + "PCM86 DATA(\n)(\0)" +0x000C Targeted P86DRV version (1b) + version . +0x000D File Length (3b) +0x0010 - 0x060F 256 * { + + Pointer to Sample Data Start (3b) + Length of Sample Data (3b) + + (0x000000 0x000000 -> no sample for this instrument ID) + +} + + +======================================= + Body +======================================= + +Stream of Sample Data { + + 8-Bit Signed + Mono + 16540Hz + (above sample rate according to KAJA's documentation + any sample rate possible, for different base note & octave) + +} */ + +#define P86_BANK_SIZE 256 +#define P86_SAMPLE_RATE 16540 + +#define P86_FILE_SIG "PCM86 DATA\n\0" + +typedef struct +{ + uint32_t start_pointer; + uint32_t sample_length; +} P86_HEADER; + +#define UNUSED(x) (void)(x) + +uint32_t read_3bytes(SafeReader& reader) +{ + unsigned char arr[3]; + + for (int i = 0; i < 3; i++) + { + arr[i] = (unsigned char)reader.readC(); + } + + return (arr[0] | (arr[1] << 8) | (arr[2] << 16)); +} + +void DivEngine::loadP86(SafeReader& reader, std::vector& ret, String& stripPath) +{ + try + { + reader.seek(0, SEEK_SET); + + P86_HEADER headers[P86_BANK_SIZE]; + + String file_sig = reader.readString(12); + if(file_sig != P86_FILE_SIG) return; + + uint8_t version = reader.readC(); + UNUSED(version); + + uint32_t file_size = read_3bytes(reader); + UNUSED(file_size); + + for(int i = 0; i < P86_BANK_SIZE; i++) + { + headers[i].start_pointer = read_3bytes(reader); + headers[i].sample_length = read_3bytes(reader); + } + + for(int i = 0; i < P86_BANK_SIZE; i++) + { + if(headers[i].start_pointer != 0 && headers[i].sample_length != 0) + { + DivSample* s = new DivSample; + + s->rate = P86_SAMPLE_RATE; + s->centerRate = P86_SAMPLE_RATE; + s->depth = DIV_SAMPLE_DEPTH_8BIT; + s->init(headers[i].sample_length); //byte per sample + + reader.seek((int)headers[i].start_pointer, SEEK_SET); + + int sample_pos = 0; + + for(uint32_t j = 0; j < headers[i].sample_length; j++) + { + unsigned char curr_byte = (unsigned char)reader.readC(); + //curr_byte += 0x80; + + s->data8[sample_pos] = curr_byte; + sample_pos++; + } + + ret.push_back(s); + + logI("p86: start %06X len %06X", headers[i].start_pointer, headers[i].sample_length); + } + } + } + catch (EndOfFileException& e) + { + lastError=_("premature end of file"); + logE("premature end of file"); + } +} \ No newline at end of file diff --git a/src/engine/fileOps/pdx.cpp b/src/engine/fileOps/pdx.cpp new file mode 100644 index 000000000..cd03369a7 --- /dev/null +++ b/src/engine/fileOps/pdx.cpp @@ -0,0 +1,101 @@ +/** + * 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 "fileOpsCommon.h" + +class DivEngine; + +//PDX 8-bit OKI ADPCM sample bank + +/* File format +The file starts with a header with 96 8-byte pairs, with each pair containing offset and length of ADPCM data chunks for each note value, like so: + +[[byte pointer to sample in file -- 32-bit unsigned integer (4 bytes)] [empty: $0000 -- 16-bit integer] [length of sample, amount in bytes -- 16-bit unsigned integer] ×96] [ADPCM data] EOF + +The first sample (1) is mapped to 0x80 (= the lowest note within MXDRV) and the last sample (96) is mapped to 0xDF (= the highest note within MXDRV). + + +Samples are encoded in 4-bit OKI ADPCM encoded nibbles, where each byte contains 2 nibbles: [nibble 2 << 4 || nibble 1] + + +Unfortunately, sample rates for each samples are not defined within the .PDX file and have to be set manually with the appropriate command for that at play-time */ + +#define PDX_BANK_SIZE 96 +#define PDX_SAMPLE_RATE 16000 + +typedef struct +{ + unsigned int start_pointer; + unsigned short sample_length; +} PDX_HEADER; + +#define UNUSED(x) (void)(x) + +void DivEngine::loadPDX(SafeReader& reader, std::vector& ret, String& stripPath) +{ + try + { + reader.seek(0, SEEK_SET); + + PDX_HEADER headers[PDX_BANK_SIZE]; + + for(int i = 0; i < PDX_BANK_SIZE; i++) + { + headers[i].start_pointer = (unsigned int)reader.readI_BE(); + unsigned short empty = (unsigned short)reader.readS_BE(); //skip 1st 2 bytes + UNUSED(empty); + headers[i].sample_length = (unsigned short)reader.readS_BE(); + } + + for(int i = 0; i < PDX_BANK_SIZE; i++) + { + if(headers[i].start_pointer != 0 && headers[i].sample_length != 0) + { + DivSample* s = new DivSample; + + s->rate = PDX_SAMPLE_RATE; + s->centerRate = PDX_SAMPLE_RATE; + s->depth = DIV_SAMPLE_DEPTH_VOX; + s->init(headers[i].sample_length * 2); + + reader.seek((int)headers[i].start_pointer, SEEK_SET); + + int sample_pos = 0; + + for(unsigned short j = 0; j < headers[i].sample_length; j++) + { + unsigned char curr_byte = (unsigned char)reader.readC(); + curr_byte = (curr_byte << 4) | (curr_byte >> 4); + + s->dataVOX[sample_pos] = curr_byte; + sample_pos++; + } + + ret.push_back(s); + + logI("pdx: start %d len %d", headers[i].start_pointer, headers[i].sample_length); + } + } + } + catch (EndOfFileException& e) + { + lastError=_("premature end of file"); + logE("premature end of file"); + } +} \ No newline at end of file diff --git a/src/engine/fileOps/ppc.cpp b/src/engine/fileOps/ppc.cpp new file mode 100644 index 000000000..3c5144dbd --- /dev/null +++ b/src/engine/fileOps/ppc.cpp @@ -0,0 +1,142 @@ +/** + * 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 "fileOpsCommon.h" + +class DivEngine; + +//PPC PMD's YM2608 ADPCM-B sample bank + +/* ======================================== + General +======================================== + +ADPCM RAM addresses: see docs/common.txt { + + address_start = 0x0026 + file_header_size = 0x0420 + +} + + +======================================== + Header +======================================== + +0x0000 Identifier (30b) + "ADPCM DATA for PMD ver.4.4- " +0x001E Address of End of Data (32B blocks) in ADPCM RAM (1h) + File Size == address -> file offset + +0x0020 - 0x041F 256 * { + + Start of Sample (32b blocks) in ADPCM RAM (1h) + End of Sample (32b blocks) in ADPCM RAM (1h) + + (0x0000 0x0000 -> no sample for this instrument ID) + +} + + +======================================== + Body +======================================== + +Stream of Sample Data { + + Yamaha ADPCM-B encoding (4-Bit Signed ADPCM) + Mono + 16kHz + (above sample rate according to KAJA's documentation + any sample rate possible, for different base note & octave) + +} */ + +#define PPC_FILE_SIG "ADPCM DATA for PMD ver.4.4- " + +#define PPC_BANK_SIZE 256 +#define PPC_SAMPLE_RATE 16000 + +typedef struct +{ + uint16_t start_pointer; + uint16_t end_pointer; +} PPC_HEADER; + +#define UNUSED(x) (void)(x) + +#define ADPCM_DATA_START 0x0420 + +void DivEngine::loadPPC(SafeReader& reader, std::vector& ret, String& stripPath) +{ + try + { + reader.seek(0, SEEK_SET); + + String file_sig = reader.readString(30); + unsigned short end_of_data = (unsigned short)reader.readS(); + UNUSED(end_of_data); + + if(file_sig != PPC_FILE_SIG) return; + + PPC_HEADER headers[PPC_BANK_SIZE]; + + for(int i = 0; i < PPC_BANK_SIZE; i++) + { + headers[i].start_pointer = (unsigned short)reader.readS(); + headers[i].end_pointer = (unsigned short)reader.readS(); + } + + for(int i = 0; i < PPC_BANK_SIZE; i++) + { + if((headers[i].start_pointer != 0 || headers[i].end_pointer != 0) && headers[i].start_pointer < headers[i].end_pointer) + { + DivSample* s = new DivSample; + + s->rate = PPC_SAMPLE_RATE; + s->centerRate = PPC_SAMPLE_RATE; + s->depth = DIV_SAMPLE_DEPTH_ADPCM_B; + s->init((headers[i].end_pointer - headers[i].start_pointer) * 32 * 2); + + int sample_pos = 0; + int sample_length = (headers[i].end_pointer - headers[i].start_pointer) * 32; + + //reader.seek(ADPCM_DATA_START + headers[i].start_pointer * 32, SEEK_SET); + + for(int j = 0; j < sample_length; j++) + { + unsigned char curr_byte = (unsigned char)reader.readC(); + //curr_byte=(curr_byte<<4)|(curr_byte>>4); + + s->dataB[sample_pos] = curr_byte; + sample_pos++; + } + + logI("ppc: start %d end %d len in bytes %d", headers[i].start_pointer, headers[i].end_pointer, (headers[i].end_pointer - headers[i].start_pointer) * 32); + + ret.push_back(s); + } + } + } + catch (EndOfFileException& e) + { + lastError=_("premature end of file"); + logE("premature end of file"); + } +} \ No newline at end of file diff --git a/src/engine/fileOps/pps.cpp b/src/engine/fileOps/pps.cpp new file mode 100644 index 000000000..39c573342 --- /dev/null +++ b/src/engine/fileOps/pps.cpp @@ -0,0 +1,125 @@ +/** + * 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 "fileOpsCommon.h" + +class DivEngine; + +//PPS AY-3-8910 sample bank + +/* ======================================= + Header +======================================= + +0x0000 - 0x0053 14 * { + + Pointer to Sample Data Start (1h) + Length of Sample Data (1h) + "Pitch"(?) (1b) + Volume Reduction (1b) + +} + + (0x0000 0x0000 [0x00 0x00] -> no sample for this instrument ID) + +} + + +======================================= + Body +======================================= + +Stream of Sample Data { + + 4-Bit Unsigned + (afaict) + Mono + 16Hz + (based on tests, maybe alternatively 8kHz) + +} */ + +#define PPS_BANK_SIZE 14 +#define PPS_SAMPLE_RATE 16000 + +typedef struct +{ + uint16_t start_pointer; + uint16_t sample_length; + uint8_t _pitch; + uint8_t _vol; +} PPS_HEADER; + + +void DivEngine::loadPPS(SafeReader& reader, std::vector& ret, String& stripPath) +{ + try + { + reader.seek(0, SEEK_SET); + + PPS_HEADER headers[PPS_BANK_SIZE]; + + for(int i = 0; i < PPS_BANK_SIZE; i++) + { + headers[i].start_pointer = (unsigned short)reader.readS(); + headers[i].sample_length = (unsigned short)reader.readS(); + headers[i]._pitch = (unsigned char)reader.readC(); + headers[i]._vol = (unsigned char)reader.readC(); + } + + for(int i = 0; i < PPS_BANK_SIZE; i++) + { + if(headers[i].start_pointer != 0 || headers[i].sample_length != 0 + || headers[i]._pitch != 0 || headers[i]._vol != 0) + { + DivSample* s = new DivSample; + + s->rate = PPS_SAMPLE_RATE; + s->centerRate = PPS_SAMPLE_RATE; + s->depth = DIV_SAMPLE_DEPTH_8BIT; + s->init(headers[i].sample_length * 2); //byte per sample + + reader.seek((int)headers[i].start_pointer, SEEK_SET); + + int sample_pos = 0; + + for(int j = 0; j < headers[i].sample_length; j++) + { + unsigned char curr_byte = (unsigned char)reader.readC(); + + s->data8[sample_pos] = (curr_byte >> 4) | (curr_byte & 0xf0); + s->data8[sample_pos] += 0x80; + sample_pos++; + s->data8[sample_pos] = (curr_byte << 4) | (curr_byte & 0xf); + s->data8[sample_pos] += 0x80; + sample_pos++; + } + + ret.push_back(s); + + logI("pps: start %d len %d", headers[i].start_pointer, headers[i].sample_length); + } + } + } + catch (EndOfFileException& e) + { + lastError=_("premature end of file"); + logE("premature end of file"); + } +} diff --git a/src/engine/fileOps/pvi.cpp b/src/engine/fileOps/pvi.cpp new file mode 100644 index 000000000..4181ea60d --- /dev/null +++ b/src/engine/fileOps/pvi.cpp @@ -0,0 +1,158 @@ +/** + * 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 "fileOpsCommon.h" + +class DivEngine; + +//PVI YM2608 ADPCM-B sample bank + +/* ======================================= + General +======================================= + +ADPCM RAM addresses: see docs/common.txt { + + address_start = 0x0000 + file_header_size = 0x0210 + +} + +======================================= + Header +======================================= + +0x0000 Identifier (4b) + "PVI2" +0x0004 - 0x0007 Unknown Settings (4b) + Unknown mappings PCME switches <-> Values + "0x10 0x00 0x10 0x02" in all example files + First 1h may be Start Address in ADPCM RAM? +0x0008 - 0x0009 "Delta-N" playback frequency (1h) + Default 0x49BA "== 16kHz"?? +0x000A Unknown (1b) + RAM type? (Docs indicate values 0 or 8, all example files have value 0x02?) +0x000B Amount of defined Samples (1b) +0x000C - 0x000F Unknown (4b) + Padding? + "0x00 0x00 0x00 0x00" in all example files +0x0010 - 0x020F 128 * { + + Start of Sample (32b blocks) in ADPCM RAM (1h) + End of Sample (32b blocks) in ADPCM RAM (1h) + + (0x0000 0x0000 -> no sample for this instrument ID) + +} + + +======================================= + Body +======================================= + +Stream of Sample Data { + + Yamaha ADPCM-B encoding (4-Bit Signed ADPCM) + Mono + Sample rate as specified earlier + (examples i have seems Stereo or 32kHz despite "16kHz" playback frequency setting?) + +} */ + +#define PVIV2_FILE_SIG "PVI2" +#define PVIV1_FILE_SIG "PVI1" + +#define PVI_BANK_SIZE 128 +#define PVI_SAMPLE_RATE 16000 + +typedef struct +{ + uint16_t start_pointer; + uint16_t end_pointer; +} PVI_HEADER; + +#define UNUSED(x) (void)(x) + +#define ADPCM_DATA_START 0x0210 + +void DivEngine::loadPVI(SafeReader& reader, std::vector& ret, String& stripPath) +{ + try + { + reader.seek(0, SEEK_SET); + + String file_sig = reader.readString(4); + if(file_sig != PVIV1_FILE_SIG && file_sig != PVIV2_FILE_SIG) return; + + unsigned int unknown_settings = (unsigned int)reader.readI(); + UNUSED(unknown_settings); + unsigned short delta_n = (unsigned short)reader.readS(); + UNUSED(delta_n); + unsigned char one_byte = (unsigned char)reader.readC(); + UNUSED(one_byte); + unsigned char amount_of_samples = (unsigned char)reader.readC(); + UNUSED(amount_of_samples); + unsigned int padding = (unsigned int)reader.readI(); + UNUSED(padding); + + PVI_HEADER headers[PVI_BANK_SIZE]; + + for(int i = 0; i < PVI_BANK_SIZE; i++) + { + headers[i].start_pointer = (unsigned short)reader.readS(); + headers[i].end_pointer = (unsigned short)reader.readS(); + } + + for(int i = 0; i < PVI_BANK_SIZE; i++) + { + if((headers[i].start_pointer != 0 || headers[i].end_pointer != 0) && headers[i].start_pointer < headers[i].end_pointer) + { + DivSample* s = new DivSample; + + s->rate = PVI_SAMPLE_RATE; + s->centerRate = PVI_SAMPLE_RATE; + s->depth = DIV_SAMPLE_DEPTH_ADPCM_B; + s->init((headers[i].end_pointer - headers[i].start_pointer) * 32 * 2); + + int sample_pos = 0; + int sample_length = (headers[i].end_pointer - headers[i].start_pointer) * 32; + + reader.seek(ADPCM_DATA_START + headers[i].start_pointer * 32, SEEK_SET); + + for(int j = 0; j < sample_length; j++) + { + unsigned char curr_byte = (unsigned char)reader.readC(); + //curr_byte=(curr_byte<<4)|(curr_byte>>4); + + s->dataB[sample_pos] = curr_byte; + sample_pos++; + } + + logI("pvi: start %d end %d len in bytes %d", headers[i].start_pointer, headers[i].end_pointer, (headers[i].end_pointer - headers[i].start_pointer) * 32); + + ret.push_back(s); + } + } + } + catch (EndOfFileException& e) + { + lastError=_("premature end of file"); + logE("premature end of file"); + } +} \ No newline at end of file diff --git a/src/engine/fileOps/pzi.cpp b/src/engine/fileOps/pzi.cpp new file mode 100644 index 000000000..906e1f43b --- /dev/null +++ b/src/engine/fileOps/pzi.cpp @@ -0,0 +1,155 @@ +/** + * 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 "fileOpsCommon.h" + +class DivEngine; + +//PZI 8-bit PCM sample bank + +/* ======================================= + Header +======================================= + +0x0000 Identifier (4b) + "PZI1" +0x0004 - 0x001F Unknown (28b) + Part of identifier? Settings? + All (\0)s in all example files +0x0020 - 0x091F 128 * { + + Start of Sample after header (2h) + Length of Sample (2h) + Offset of loop start from sample start (2h) + Offset of loop end from sample start (2h) + Sample rate (1h) + + (0xFFFFFFFF 0xFFFFFFFF loop offsets -> no loop information) + +} + + +======================================= + Body +======================================= + +Stream of Sample Data { + + Unsigned 8-Bit + Mono + Sample rate as specified in header + +} */ + +#define PZI_BANK_SIZE 128 + +#define PZI_FILE_SIG "PZI1" + +#define NO_LOOP (0xFFFFFFFFU) + +#define SAMPLE_DATA_OFFSET 0x0920 + +#define MAX_SANITY_CAP 9999999 + +#define HEADER_JUNK_SIZE 28 + +typedef struct +{ + uint32_t start_pointer; + uint32_t sample_length; + uint32_t loop_start; + uint32_t loop_end; + uint16_t sample_rate; +} PZI_HEADER; + +#define UNUSED(x) (void)(x) + +void DivEngine::loadPZI(SafeReader& reader, std::vector& ret, String& stripPath) +{ + try + { + reader.seek(0, SEEK_SET); + + PZI_HEADER headers[PZI_BANK_SIZE]; + + String file_sig = reader.readString(4); + if(file_sig != PZI_FILE_SIG) return; + + for (int i = 0; i < HEADER_JUNK_SIZE; i++) + { + unsigned char curr_byte = (unsigned char)reader.readC(); + UNUSED(curr_byte); + } + + for(int i = 0; i < PZI_BANK_SIZE; i++) + { + headers[i].start_pointer = (unsigned int)reader.readI(); + headers[i].sample_length = (unsigned int)reader.readI(); + headers[i].loop_start = (unsigned int)reader.readI(); + headers[i].loop_end = (unsigned int)reader.readI(); + headers[i].sample_rate = (unsigned short)reader.readS(); + } + + for(int i = 0; i < PZI_BANK_SIZE; i++) + { + if (headers[i].start_pointer < MAX_SANITY_CAP && headers[i].sample_length < MAX_SANITY_CAP && + headers[i].loop_start < MAX_SANITY_CAP && headers[i].loop_end < MAX_SANITY_CAP && + headers[i].start_pointer > 0 && headers[i].sample_length > 0) + { + DivSample* s = new DivSample; + + s->rate = headers[i].sample_rate; + s->centerRate = headers[i].sample_rate; + s->depth = DIV_SAMPLE_DEPTH_8BIT; + s->init(headers[i].sample_length); //byte per sample + + reader.seek((int)headers[i].start_pointer + SAMPLE_DATA_OFFSET, SEEK_SET); + + int sample_pos = 0; + + for (uint32_t j = 0; j < headers[i].sample_length; j++) + { + unsigned char curr_byte = (unsigned char)reader.readC(); + curr_byte += 0x80; + + s->data8[sample_pos] = curr_byte; + sample_pos++; + } + + if (headers[i].loop_start != NO_LOOP && headers[i].loop_end != NO_LOOP) + { + s->loop = true; + s->loopMode = DIV_SAMPLE_LOOP_FORWARD; + s->loopStart = headers[i].loop_start; + s->loopEnd = headers[i].loop_end; + } + + ret.push_back(s); + + logI("pzi: start %d len %d sample rate %d loop start %d loop end %d", headers[i].start_pointer, headers[i].sample_length, + headers[i].sample_rate, headers[i].loop_start, headers[i].loop_end); + } + } + } + catch (EndOfFileException& e) + { + lastError=_("premature end of file"); + logE("premature end of file"); + } +} \ No newline at end of file diff --git a/src/engine/fileOps/s3m.cpp b/src/engine/fileOps/s3m.cpp index d13c1c7c2..11d0f4fb4 100644 --- a/src/engine/fileOps/s3m.cpp +++ b/src/engine/fileOps/s3m.cpp @@ -19,21 +19,6 @@ #include "fileOpsCommon.h" -// SBI and some other OPL containers -struct sbi_t { - uint8_t Mcharacteristics, - Ccharacteristics, - Mscaling_output, - Cscaling_output, - Meg_AD, - Ceg_AD, - Meg_SR, - Ceg_SR, - Mwave, - Cwave, - FeedConnect; -}; - static void readSbiOpData(sbi_t& sbi, SafeReader& reader) { sbi.Mcharacteristics = reader.readC(); sbi.Ccharacteristics = reader.readC(); @@ -51,6 +36,7 @@ static void readSbiOpData(sbi_t& sbi, SafeReader& reader) { bool DivEngine::loadS3M(unsigned char* file, size_t len) { struct InvalidHeaderException {}; bool success=false; + bool opl2=!getConfInt("s3mOPL3",0); char magic[4]={0,0,0,0}; SafeReader reader=SafeReader(file,len); warnings=""; @@ -273,11 +259,16 @@ bool DivEngine::loadS3M(unsigned char* file, size_t len) { bool hasPCM=false; bool hasFM=false; int numChans=0; + int realNumChans=0; - for (int i=0; i<32; i++) { - if (chanSettings[i]==255) continue; - if ((chanSettings[i]&127)>=32) continue; - if ((chanSettings[i]&127)>=16) { + for (int ch=0; ch<32; ch++) { + if (chanSettings[ch]!=255) realNumChans++; + } + + for (int ch=0; ch<32; ch++) { + if (chanSettings[ch]==255) continue; + if ((chanSettings[ch]&127)>=32) continue; + if ((chanSettings[ch]&127)>=16) { hasFM=true; } else { hasPCM=true; @@ -287,34 +278,69 @@ bool DivEngine::loadS3M(unsigned char* file, size_t len) { if (hasFM && hasPCM) break; } - int pcmChan=hasFM?9:0; + int pcmChan=hasFM?(opl2 ? 9 : 18):0; int fmChan=hasPCM?32:0; int invalidChan=40; - for (int i=0; i<32; i++) { - if (chanSettings[i]==255) { - chanMap[i]=invalidChan++; + for (int ch=0; ch<32; ch++) { + if (chanSettings[ch]==255) { + chanMap[ch]=invalidChan++; continue; } - if ((chanSettings[i]&127)>=32) { - chanMap[i]=invalidChan++; + if ((chanSettings[ch]&127)>=32) { + chanMap[ch]=invalidChan++; continue; } - if ((chanSettings[i]&127)>=16) { - chanMap[i]=fmChan++; + if ((chanSettings[ch]&127)>=16) { + chanMap[ch]=fmChan++; } else { - chanMap[i]=pcmChan++; + chanMap[ch]=pcmChan++; } } + char buffer[40]; + int chanIndex = 1; + if (hasPCM) { - for (int i=pcmChan; i<32; i++) { - ds.subsong[0]->chanShow[i]=false; - ds.subsong[0]->chanShowChanOsc[i]=false; + for(int ch = 0; ch < pcmChan - (realNumChans - (hasFM ? 9 : 0)); ch++) + { + ds.subsong[0]->chanShow[ch]=false; + ds.subsong[0]->chanShowChanOsc[ch]=false; + } + + for (int ch=pcmChan; ch<32; ch++) { + ds.subsong[0]->chanShow[ch]=false; + ds.subsong[0]->chanShowChanOsc[ch]=false; + } + + for(int ch = 0; ch < 32; ch++) + { + if(ds.subsong[0]->chanShow[ch]) + { + snprintf(buffer, 40, _("Channel %d"), chanIndex); + ds.subsong[0]->chanName[ch] = buffer; + chanIndex++; + } + } + } + + if (hasFM && !opl2) { + for (int ch=(hasPCM?32:0) + 9; ch<(hasPCM?32:0) + 18; ch++) { + ds.subsong[0]->chanShow[ch]=false; + ds.subsong[0]->chanShowChanOsc[ch]=false; + } + + chanIndex = 1; + + for (int ch=(hasPCM?32:0); ch<(hasPCM?32:0) + 9; ch++) { + snprintf(buffer, 40, _("FM %d"), chanIndex); + ds.subsong[0]->chanName[ch] = buffer; + chanIndex++; } } logV("numChans: %d",numChans); + logV("realNumChans: %d",realNumChans); ds.systemName="PC"; if (hasPCM) { @@ -327,7 +353,7 @@ bool DivEngine::loadS3M(unsigned char* file, size_t len) { ds.systemLen++; } if (hasFM) { - ds.system[ds.systemLen]=DIV_SYSTEM_OPL2; + ds.system[ds.systemLen]=opl2 ? DIV_SYSTEM_OPL2 : DIV_SYSTEM_OPL3; ds.systemVol[ds.systemLen]=1.0f; ds.systemPan[ds.systemLen]=0; ds.systemLen++; diff --git a/src/engine/fileOpsSample.cpp b/src/engine/fileOpsSample.cpp index 26d921ac8..5008392f8 100644 --- a/src/engine/fileOpsSample.cpp +++ b/src/engine/fileOpsSample.cpp @@ -24,10 +24,12 @@ #include "sfWrapper.h" #endif -DivSample* DivEngine::sampleFromFile(const char* path) { +std::vector DivEngine::sampleFromFile(const char* path) { + std::vector ret; + if (song.sample.size()>=256) { lastError="too many samples!"; - return NULL; + return ret; } BUSY_BEGIN; warnings=""; @@ -58,6 +60,110 @@ DivSample* DivEngine::sampleFromFile(const char* path) { } extS+=i; } + + if(extS == ".pps" || extS == ".ppc" || extS == ".pvi" || + extS == ".pdx" || extS == ".pzi" || extS == ".p86" || + extS == ".p") //sample banks! + { + String stripPath; + const char* pathReduxEnd=strrchr(pathRedux,'.'); + if (pathReduxEnd==NULL) { + stripPath=pathRedux; + } else { + for (const char* i=pathRedux; i!=pathReduxEnd && (*i); i++) { + stripPath+=*i; + } + } + + FILE* f=ps_fopen(path,"rb"); + if (f==NULL) { + lastError=strerror(errno); + return ret; + } + unsigned char* buf; + ssize_t len; + if (fseek(f,0,SEEK_END)!=0) { + lastError=strerror(errno); + fclose(f); + return ret; + } + len=ftell(f); + if (len<0) { + lastError=strerror(errno); + fclose(f); + return ret; + } + if (len==(SIZE_MAX>>1)) { + lastError=strerror(errno); + fclose(f); + return ret; + } + if (len==0) { + lastError=strerror(errno); + fclose(f); + return ret; + } + if (fseek(f,0,SEEK_SET)!=0) { + lastError=strerror(errno); + fclose(f); + return ret; + } + buf=new unsigned char[len]; + if (fread(buf,1,len,f)!=(size_t)len) { + logW("did not read entire sample bank file buffer!"); + lastError=_("did not read entire sample bank file!"); + delete[] buf; + return ret; + } + fclose(f); + + SafeReader reader = SafeReader(buf,len); + + if(extS == ".pps") + { + loadPPS(reader,ret,stripPath); + } + if(extS == ".ppc") + { + loadPPC(reader,ret,stripPath); + } + if(extS == ".pvi") + { + loadPVI(reader,ret,stripPath); + } + if(extS == ".pdx") + { + loadPDX(reader,ret,stripPath); + } + if(extS == ".pzi") + { + loadPZI(reader,ret,stripPath); + } + if(extS == ".p86") + { + loadP86(reader,ret,stripPath); + } + if(extS == ".p") + { + loadP(reader,ret,stripPath); + } + + if((int)ret.size() > 0) + { + int counter = 0; + + for(DivSample* s: ret) + { + s->name = fmt::sprintf("%s sample %d", stripPath, counter); + counter++; + } + } + + delete[] buf; //done with buffer + BUSY_END; + return ret; + } + if (extS==".dmc" || extS==".brr") { // read as .dmc or .brr size_t len=0; DivSample* sample=new DivSample; @@ -68,7 +174,7 @@ DivSample* DivEngine::sampleFromFile(const char* path) { BUSY_END; lastError=fmt::sprintf("could not open file! (%s)",strerror(errno)); delete sample; - return NULL; + return ret; } if (fseek(f,0,SEEK_END)<0) { @@ -76,7 +182,7 @@ DivSample* DivEngine::sampleFromFile(const char* path) { BUSY_END; lastError=fmt::sprintf("could not get file length! (%s)",strerror(errno)); delete sample; - return NULL; + return ret; } len=ftell(f); @@ -86,7 +192,7 @@ DivSample* DivEngine::sampleFromFile(const char* path) { BUSY_END; lastError="file is empty!"; delete sample; - return NULL; + return ret; } if (len==(SIZE_MAX>>1)) { @@ -94,7 +200,7 @@ DivSample* DivEngine::sampleFromFile(const char* path) { BUSY_END; lastError="file is invalid!"; delete sample; - return NULL; + return ret; } if (fseek(f,0,SEEK_SET)<0) { @@ -102,7 +208,7 @@ DivSample* DivEngine::sampleFromFile(const char* path) { BUSY_END; lastError=fmt::sprintf("could not seek to beginning of file! (%s)",strerror(errno)); delete sample; - return NULL; + return ret; } if (extS==".dmc") { @@ -120,7 +226,7 @@ DivSample* DivEngine::sampleFromFile(const char* path) { BUSY_END; lastError="wait... is that right? no I don't think so..."; delete sample; - return NULL; + return ret; } unsigned char* dataBuf=sample->dataDPCM; @@ -147,14 +253,14 @@ DivSample* DivEngine::sampleFromFile(const char* path) { BUSY_END; lastError="BRR sample is empty!"; delete sample; - return NULL; + return ret; } } else if ((len%9)!=0) { fclose(f); BUSY_END; lastError="possibly corrupt BRR sample!"; delete sample; - return NULL; + return ret; } } @@ -163,16 +269,17 @@ DivSample* DivEngine::sampleFromFile(const char* path) { BUSY_END; lastError=fmt::sprintf("could not read file! (%s)",strerror(errno)); delete sample; - return NULL; + return ret; } BUSY_END; - return sample; + ret.push_back(sample); + return ret; } } #ifndef HAVE_SNDFILE lastError="Furnace was not compiled with libsndfile!"; - return NULL; + return ret; #else SF_INFO si; SFWrapper sfWrap; @@ -186,13 +293,13 @@ DivSample* DivEngine::sampleFromFile(const char* path) { } else { lastError=fmt::sprintf("could not open file! (%s)\nif this is raw sample data, you may import it by right-clicking the Load Sample icon and selecting \"import raw\".",sf_error_number(err)); } - return NULL; + return ret; } if (si.frames>16777215) { lastError="this sample is too big! max sample size is 16777215."; sfWrap.doClose(); BUSY_END; - return NULL; + return ret; } void* buf=NULL; sf_count_t sampleLen=sizeof(short); @@ -298,7 +405,8 @@ DivSample* DivEngine::sampleFromFile(const char* path) { if (sample->centerRate>64000) sample->centerRate=64000; sfWrap.doClose(); BUSY_END; - return sample; + ret.push_back(sample); + return ret; #endif } diff --git a/src/engine/platform/oplAInterface.cpp b/src/engine/platform/oplAInterface.cpp index 46d39b869..dcd5c8e85 100644 --- a/src/engine/platform/oplAInterface.cpp +++ b/src/engine/platform/oplAInterface.cpp @@ -27,7 +27,7 @@ uint8_t DivOPLAInterface::ymfm_external_read(ymfm::access_class type, uint32_t a if (adpcmBMem==NULL) { return 0; } - return adpcmBMem[address&0xffffff]; + return adpcmBMem[address&0x3ffff]; case ymfm::ACCESS_PCM: if (pcmMem==NULL) { return 0; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index d2094d49c..3c834fa93 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -1663,26 +1663,34 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { chan[i].panPos+=chan[i].panRate; chan[i].panPos&=255; - // calculate... + // calculate inverted... switch (chan[i].panPos&0xc0) { case 0: // center -> right - chan[i].panL=0xff-((chan[i].panPos&0x3f)<<2); - chan[i].panR=0xff; + chan[i].panL=((chan[i].panPos&0x3f)<<2); + chan[i].panR=0; break; case 0x40: // right -> center - chan[i].panL=(chan[i].panPos&0x3f)<<2; - chan[i].panR=0xff; + chan[i].panL=0xff-((chan[i].panPos&0x3f)<<2); + chan[i].panR=0; break; case 0x80: // center -> left - chan[i].panL=0xff; - chan[i].panR=0xff-((chan[i].panPos&0x3f)<<2); + chan[i].panL=0; + chan[i].panR=((chan[i].panPos&0x3f)<<2); break; case 0xc0: // left -> center - chan[i].panL=0xff; - chan[i].panR=(chan[i].panPos&0x3f)<<2; + chan[i].panL=0; + chan[i].panR=0xff-((chan[i].panPos&0x3f)<<2); break; } + // multiply by depth + chan[i].panL=(chan[i].panL*chan[i].panDepth)/15; + chan[i].panR=(chan[i].panR*chan[i].panDepth)/15; + + // then invert it to get final panning + chan[i].panL^=0xff; + chan[i].panR^=0xff; + dispatchCmd(DivCommand(DIV_CMD_PANNING,i,chan[i].panL,chan[i].panR)); } diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index 594554c57..b08c60532 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -36,6 +36,8 @@ static float oscDebugMax=1.0; static float oscDebugPower=1.0; static int oscDebugRepeat=1; static int numApples=1; +static int getGainChan=0; +static int getGainVol=0; static void _drawOsc(const ImDrawList* drawList, const ImDrawCmd* cmd) { if (cmd!=NULL) { @@ -721,6 +723,13 @@ void FurnaceGUI::drawDebug() { ImGui::TreePop(); } #endif + if (ImGui::TreeNode("Get Gain Test")) { + float realVol=e->getGain(getGainChan,getGainVol); + ImGui::InputInt("Chan",&getGainChan); + ImGui::InputInt("Vol",&getGainVol); + ImGui::Text("result: %.0f%%",realVol*100.0f); + ImGui::TreePop(); + } if (ImGui::TreeNode("User Interface")) { if (ImGui::Button("Inspect")) { inspectorOpen=!inspectorOpen; diff --git a/src/gui/doAction.cpp b/src/gui/doAction.cpp index fff5a85d9..bf5b89519 100644 --- a/src/gui/doAction.cpp +++ b/src/gui/doAction.cpp @@ -676,6 +676,17 @@ void FurnaceGUI::doAction(int what) { latchTarget=0; latchNibble=false; break; + case GUI_ACTION_PAT_ABSORB_INSTRUMENT: { + DivPattern* pat=e->curPat[cursor.xCoarse].getPattern(e->curOrders->ord[cursor.xCoarse][curOrder],false); + if (!pat) break; + for (int i=cursor.y; i>=0; i--) { + if (pat->data[i][2] >= 0) { + curIns=pat->data[i][2]; + break; + } + } + break; + } case GUI_ACTION_INS_LIST_ADD: if (settings.insTypeMenu) { diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 37443c46e..95e8ffd36 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -3828,8 +3828,9 @@ bool FurnaceGUI::loop() { } int sampleCountBefore=e->song.sampleLen; std::vector instruments=e->instrumentFromFile(ev.drop.file,true,settings.readInsNames); + std::vector samples = e->sampleFromFile(ev.drop.file); DivWavetable* droppedWave=NULL; - DivSample* droppedSample=NULL; + //DivSample* droppedSample=NULL; if (!instruments.empty()) { if (e->song.sampleLen!=sampleCountBefore) { e->renderSamplesP(); @@ -3854,10 +3855,24 @@ bool FurnaceGUI::loop() { } nextWindow=GUI_WINDOW_WAVE_LIST; MARK_MODIFIED; - } else if ((droppedSample=e->sampleFromFile(ev.drop.file))!=NULL) { + } + else if (!samples.empty()) + { + if (e->song.sampleLen!=sampleCountBefore) { + //e->renderSamplesP(); + } + if (!e->getWarnings().empty()) + { + showWarning(e->getWarnings(),GUI_WARN_GENERIC); + } int sampleCount=-1; - sampleCount=e->addSamplePtr(droppedSample); - if (sampleCount>=0 && settings.selectAssetOnLoad) { + for (DivSample* s: samples) + { + sampleCount=e->addSamplePtr(s); + } + //sampleCount=e->addSamplePtr(droppedSample); + if (sampleCount>=0 && settings.selectAssetOnLoad) + { curSample=sampleCount; updateSampleTex=true; } @@ -5319,24 +5334,43 @@ bool FurnaceGUI::loop() { String errs=_("there were some errors while loading samples:\n"); bool warn=false; for (String i: fileDialog->getFileName()) { - DivSample* s=e->sampleFromFile(i.c_str()); - if (s==NULL) { + std::vector samples=e->sampleFromFile(i.c_str()); + if (samples.empty()) { if (fileDialog->getFileName().size()>1) { warn=true; errs+=fmt::sprintf("- %s: %s\n",i,e->getLastError()); } else {; showError(e->getLastError()); } - } else { - if (e->addSamplePtr(s)==-1) { - if (fileDialog->getFileName().size()>1) { - warn=true; - errs+=fmt::sprintf("- %s: %s\n",i,e->getLastError()); - } else { - showError(e->getLastError()); + } + else + { + if((int)samples.size() == 1) + { + if (e->addSamplePtr(samples[0]) == -1) + { + if (fileDialog->getFileName().size()>1) + { + warn=true; + errs+=fmt::sprintf("- %s: %s\n",i,e->getLastError()); + } + else + { + showError(e->getLastError()); + } + } + else + { + MARK_MODIFIED; } - } else { - MARK_MODIFIED; + } + else + { + for (DivSample* s: samples) { //ask which samples to load! + pendingSamples.push_back(std::make_pair(s,false)); + } + displayPendingSamples=true; + replacePendingSample = false; } } } @@ -5345,24 +5379,44 @@ bool FurnaceGUI::loop() { } break; } - case GUI_FILE_SAMPLE_OPEN_REPLACE: { - DivSample* s=e->sampleFromFile(copyOfName.c_str()); - if (s==NULL) { + case GUI_FILE_SAMPLE_OPEN_REPLACE: + { + std::vector samples=e->sampleFromFile(copyOfName.c_str()); + if (samples.empty()) + { showError(e->getLastError()); - } else { - if (curSample>=0 && curSample<(int)e->song.sample.size()) { - e->lockEngine([this,s]() { - // if it crashes here please tell me... - DivSample* oldSample=e->song.sample[curSample]; - e->song.sample[curSample]=s; - delete oldSample; - e->renderSamples(); - MARK_MODIFIED; - }); - updateSampleTex=true; - } else { - showError(_("...but you haven't selected a sample!")); - delete s; + } + else + { + if((int)samples.size() == 1) + { + if (curSample>=0 && curSample<(int)e->song.sample.size()) + { + DivSample* s = samples[0]; + e->lockEngine([this, s]() + { + // if it crashes here please tell me... + DivSample* oldSample=e->song.sample[curSample]; + e->song.sample[curSample]= s; + delete oldSample; + e->renderSamples(); + MARK_MODIFIED; + }); + updateSampleTex=true; + } + else + { + showError(_("...but you haven't selected a sample!")); + delete samples[0]; + } + } + else + { + for (DivSample* s: samples) { //ask which samples to load! + pendingSamples.push_back(std::make_pair(s,false)); + } + displayPendingSamples=true; + replacePendingSample = true; } } break; @@ -5741,6 +5795,11 @@ bool FurnaceGUI::loop() { ImGui::OpenPopup(_("Select Instrument")); } + if (displayPendingSamples) { + displayPendingSamples=false; + ImGui::OpenPopup(_("Select Sample")); + } + if (displayPendingRawSample) { displayPendingRawSample=false; ImGui::OpenPopup(_("Import Raw Sample")); @@ -6569,6 +6628,191 @@ bool FurnaceGUI::loop() { ImGui::EndPopup(); } + // TODO: fix style + centerNextWindow(_("Select Sample"),canvasW,canvasH); + if (ImGui::BeginPopupModal(_("Select Sample"),NULL,ImGuiWindowFlags_AlwaysAutoResize)) { + bool quitPlease=false; + + ImGui::AlignTextToFramePadding(); + ImGui::Text(_("this is a sample bank! select which ones to load:")); + ImGui::SameLine(); + if (ImGui::Button(_("All"))) { + for (std::pair& i: pendingSamples) { + i.second=true; + } + } + ImGui::SameLine(); + if (ImGui::Button(_("None"))) { + for (std::pair& i: pendingSamples) { + i.second=false; + } + } + bool reissueSearch=false; + + bool anySelected=false; + float sizeY=ImGui::GetFrameHeightWithSpacing()*pendingSamples.size(); + if (sizeY>(canvasH-180.0*dpiScale)) + { + sizeY=canvasH-180.0*dpiScale; + if (sizeY<60.0*dpiScale) sizeY=60.0*dpiScale; + } + if (ImGui::BeginTable("PendingSamplesList",1,ImGuiTableFlags_ScrollY,ImVec2(0.0f,sizeY))) + { + if (sampleBankSearchQuery.empty()) + { + for (size_t i=0; iname); + if (pendingInsSingle) + { + if (ImGui::Selectable(id.c_str())) + { + pendingSamples[i].second=true; + quitPlease=true; + } + } + else + { + // TODO:fixstyle from hereonwards + ImGuiIO& io = ImGui::GetIO(); + if(ImGui::Checkbox(id.c_str(),&pendingSamples[i].second) && io.KeyShift) + { + for(int jj = (int)i - 1; jj >= 0; jj--) + { + if(pendingSamples[jj].second) //pressed shift and there's selected item above + { + for(int k = jj; k < (int)i; k++) + { + pendingSamples[k].second = true; + } + + break; + } + } + } + } + if (pendingSamples[i].second) anySelected=true; + } + } + else //display search results + { + if(reissueSearch) + { + String lowerCase=sampleBankSearchQuery; + + for (char& ii: lowerCase) + { + if (ii>='A' && ii<='Z') ii+='a'-'A'; + } + + sampleBankSearchResults.clear(); + for (int j=0; j < (int)pendingSamples.size(); j++) + { + String lowerCase1 = pendingSamples[j].first->name; + + for (char& ii: lowerCase1) + { + if (ii>='A' && ii<='Z') ii+='a'-'A'; + } + + if (lowerCase1.find(lowerCase)!=String::npos) + { + sampleBankSearchResults.push_back(std::make_pair(pendingSamples[j].first, pendingSamples[j].second)); + } + } + } + + for (size_t i=0; iname); + + ImGuiIO& io = ImGui::GetIO(); + if(ImGui::Checkbox(id.c_str(),&sampleBankSearchResults[i].second) && io.KeyShift) + { + for(int jj = (int)i - 1; jj >= 0; jj--) + { + if(sampleBankSearchResults[jj].second) //pressed shift and there's selected item above + { + for(int k = jj; k < (int)i; k++) + { + sampleBankSearchResults[k].second = true; + } + + break; + } + } + } + if (sampleBankSearchResults[i].second) anySelected=true; + } + + for (size_t i=0; i 0) + { + for (size_t j=0; j& i: pendingSamples) { + i.second=false; + } + quitPlease=true; + } + if (quitPlease) + { + ImGui::CloseCurrentPopup(); + int counter = 0; + for (std::pair& i: pendingSamples) + { + if (!i.second) + { + delete i.first; + } + else + { + if(counter == 0 && replacePendingSample) + { + *e->song.sample[curSample]=*i.first; + replacePendingSample = false; + } + else + { + e->addSamplePtr(i.first); + } + } + counter++; + } + + curSample = (int)e->song.sample.size() - 1; + pendingSamples.clear(); + } + + ImGui::EndPopup(); + } + centerNextWindow(_("Import Raw Sample"),canvasW,canvasH); if (ImGui::BeginPopupModal(_("Import Raw Sample"),NULL,ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text(_("Data type:")); @@ -7581,7 +7825,15 @@ bool FurnaceGUI::init() { #endif compatFormats+="*.dmc "; - compatFormats+="*.brr"; + compatFormats+="*.brr "; + + compatFormats+="*.ppc "; + compatFormats+="*.pps "; + compatFormats+="*.pvi "; + compatFormats+="*.pdx "; + compatFormats+="*.pzi "; + compatFormats+="*.p86 "; + compatFormats+="*.p"; audioLoadFormats[1]=compatFormats; audioLoadFormats.push_back(_("NES DPCM data")); @@ -7590,6 +7842,27 @@ bool FurnaceGUI::init() { audioLoadFormats.push_back(_("SNES Bit Rate Reduction")); audioLoadFormats.push_back("*.brr"); + audioLoadFormats.push_back(_("PMD YM2608 ADPCM-B sample bank")); + audioLoadFormats.push_back("*.ppc"); + + audioLoadFormats.push_back(_("PDR 4-bit AY-3-8910 sample bank")); + audioLoadFormats.push_back("*.pps"); + + audioLoadFormats.push_back(_("FMP YM2608 ADPCM-B sample bank")); + audioLoadFormats.push_back("*.pvi"); + + audioLoadFormats.push_back(_("MDX OKI ADPCM sample bank")); + audioLoadFormats.push_back("*.pdx"); + + audioLoadFormats.push_back(_("FMP 8-bit PCM sample bank")); + audioLoadFormats.push_back("*.pzi"); + + audioLoadFormats.push_back(_("PMD 8-bit PCM sample bank")); + audioLoadFormats.push_back("*.p86"); + + audioLoadFormats.push_back(_("PMD OKI ADPCM sample bank")); + audioLoadFormats.push_back("*.p"); + audioLoadFormats.push_back(_("all files")); audioLoadFormats.push_back("*"); @@ -8012,6 +8285,8 @@ FurnaceGUI::FurnaceGUI(): snesFilterHex(false), modTableHex(false), displayEditString(false), + displayPendingSamples(false), + replacePendingSample(false), displayExportingROM(false), changeCoarse(false), mobileEdit(false), diff --git a/src/gui/gui.h b/src/gui/gui.h index dcc2bf405..8853100c9 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -816,6 +816,7 @@ enum FurnaceGUIActions { GUI_ACTION_PAT_LATCH, GUI_ACTION_PAT_SCROLL_MODE, GUI_ACTION_PAT_CLEAR_LATCH, + GUI_ACTION_PAT_ABSORB_INSTRUMENT, GUI_ACTION_PAT_MAX, GUI_ACTION_INS_LIST_MIN, @@ -1591,7 +1592,7 @@ class FurnaceGUI { int sampleTexW, sampleTexH; bool updateSampleTex; - String workingDir, fileName, clipboard, warnString, errorString, lastError, curFileName, nextFile, sysSearchQuery, newSongQuery, paletteQuery; + String workingDir, fileName, clipboard, warnString, errorString, lastError, curFileName, nextFile, sysSearchQuery, newSongQuery, paletteQuery, sampleBankSearchQuery; String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport; String workingDirVGMExport, workingDirZSMExport, workingDirROMExport; String workingDirFont, workingDirColors, workingDirKeybinds; @@ -1603,6 +1604,7 @@ class FurnaceGUI { String folderString; std::vector sysSearchResults; + std::vector> sampleBankSearchResults; std::vector newSongSearchResults; std::vector paletteSearchResults; FixedQueue recentFile; @@ -1618,6 +1620,7 @@ class FurnaceGUI { bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, notifyWaveChange; bool wantScrollListIns, wantScrollListWave, wantScrollListSample; bool displayPendingIns, pendingInsSingle, displayPendingRawSample, snesFilterHex, modTableHex, displayEditString; + bool displayPendingSamples, replacePendingSample; bool displayExportingROM; bool changeCoarse; bool mobileEdit; @@ -1959,6 +1962,7 @@ class FurnaceGUI { unsigned int maxUndoSteps; float vibrationStrength; int vibrationLength; + int s3mOPL3; String mainFontPath; String headFontPath; String patFontPath; @@ -2217,6 +2221,7 @@ class FurnaceGUI { maxUndoSteps(100), vibrationStrength(0.5f), vibrationLength(20), + s3mOPL3(1), mainFontPath(""), headFontPath(""), patFontPath(""), @@ -2373,6 +2378,7 @@ class FurnaceGUI { std::vector cmdStream; std::vector particles; std::vector> pendingIns; + std::vector> pendingSamples; std::vector sysCategories; diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index cffcb4047..780fc46af 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -687,6 +687,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("PAT_LATCH", _N("Set note input latch"), 0), 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("Set current instrument to channel's current instrument column"), 0), D("PAT_MAX", "", NOT_AN_ACTION), D("INS_LIST_MIN", _N("---Instrument list"), NOT_AN_ACTION), diff --git a/src/gui/plot_nolerp.cpp b/src/gui/plot_nolerp.cpp index 170fc20d9..aa57f0da7 100644 --- a/src/gui/plot_nolerp.cpp +++ b/src/gui/plot_nolerp.cpp @@ -26,6 +26,8 @@ #include "imgui.h" #include "imgui_internal.h" +#include "../ta-utils.h" + struct FurnacePlotArrayGetterData { const float* Values; @@ -270,12 +272,13 @@ int PlotBitfieldEx(const char* label, int (*values_getter)(void* data, int idx), float lineHeight=ImGui::GetTextLineHeight()/2.0; for (int i=0; i