2022-02-14 22:12:20 -05:00
/**
* Furnace Tracker - multi - system chiptune tracker
2025-01-28 18:49:19 -05:00
* Copyright ( C ) 2021 - 2025 tildearrow and contributors
2022-02-14 22:12:20 -05:00
*
* 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 .
*/
2025-10-18 21:42:57 -05:00
// this file contains most of the playback code.
// it is a mess due to the amount of compatibility flags that have been
// added over time, so I have tried my best to comment it.
// these compatibility flags are preceded by a "COMPAT FLAG" comment upon use.
2022-06-05 18:17:00 -05:00
# include "macroInt.h"
2022-05-03 02:29:12 -05:00
# include <chrono>
2022-01-17 18:18:28 -05:00
# define _USE_MATH_DEFINES
2021-12-07 01:23:57 -05:00
# include "dispatch.h"
2021-05-12 03:58:55 -05:00
# include "engine.h"
2023-09-06 04:03:53 -05:00
# include "workPool.h"
2021-12-07 04:22:36 -05:00
# include "../ta-log.h"
2022-01-04 00:29:59 -05:00
# include <math.h>
2021-05-12 03:58:55 -05:00
2025-10-18 21:42:57 -05:00
// go to next order
2021-05-12 03:58:55 -05:00
void DivEngine : : nextOrder ( ) {
curRow = 0 ;
2021-12-21 17:42:27 -05:00
if ( repeatPattern ) return ;
2022-05-15 01:42:49 -05:00
if ( + + curOrder > = curSubSong - > ordersLen ) {
2022-09-10 01:39:42 -05:00
logV ( " end of orders reached " ) ;
2021-12-07 04:22:36 -05:00
endOfSong = true ;
2025-10-18 21:42:57 -05:00
// the walked array is used for loop detection
// since we've reached the end, we are guaranteed to loop here, so
// just reset it.
2022-09-10 01:39:42 -05:00
memset ( walked , 0 , 8192 ) ;
2021-05-12 03:58:55 -05:00
curOrder = 0 ;
}
}
2025-10-18 21:42:57 -05:00
// used for the pattern visualizer in console mode.
2023-12-04 15:22:29 -05:00
static const char * notes [ 12 ] = {
2021-05-12 03:58:55 -05:00
" C- " , " C# " , " D- " , " D# " , " E- " , " F- " , " F# " , " G- " , " G# " , " A- " , " A# " , " B- "
} ;
2025-10-18 21:42:57 -05:00
// update this when adding new commands in dispatch.h.
2022-04-15 16:10:57 -05:00
const char * cmdName [ ] = {
2021-05-19 02:05:24 -05:00
" NOTE_ON " ,
" NOTE_OFF " ,
2022-02-08 03:50:42 -05:00
" NOTE_OFF_ENV " ,
" ENV_RELEASE " ,
2021-05-19 02:05:24 -05:00
" INSTRUMENT " ,
" VOLUME " ,
" GET_VOLUME " ,
" GET_VOLMAX " ,
" NOTE_PORTA " ,
" PITCH " ,
" PANNING " ,
" LEGATO " ,
" PRE_PORTA " ,
2021-12-05 16:11:12 -05:00
" PRE_NOTE " ,
2021-05-19 02:05:24 -05:00
2022-08-03 17:21:47 -05:00
" HINT_VIBRATO " ,
2022-08-04 15:14:29 -05:00
" HINT_VIBRATO_RANGE " ,
2022-08-03 17:21:47 -05:00
" HINT_VIBRATO_SHAPE " ,
" HINT_PITCH " ,
" HINT_ARPEGGIO " ,
2022-08-04 15:14:29 -05:00
" HINT_VOLUME " ,
2022-08-04 17:47:59 -05:00
" HINT_VOL_SLIDE " ,
2022-08-04 15:14:29 -05:00
" HINT_PORTA " ,
" HINT_LEGATO " ,
2025-01-26 04:28:03 -05:00
" HINT_VOL_SLIDE_TARGET " ,
2025-04-05 04:33:46 -05:00
" HINT_TREMOLO " ,
" HINT_PANBRELLO " ,
" HINT_PAN_SLIDE " ,
" HINT_PANNING " ,
2022-08-03 17:21:47 -05:00
2021-05-19 02:05:24 -05:00
" SAMPLE_MODE " ,
2021-12-08 18:29:50 -05:00
" SAMPLE_FREQ " ,
2021-12-09 03:13:37 -05:00
" SAMPLE_BANK " ,
2022-03-15 03:59:42 +07:00
" SAMPLE_POS " ,
2022-05-27 02:47:44 -05:00
" SAMPLE_DIR " ,
2021-05-19 02:05:24 -05:00
2022-04-10 02:11:36 -05:00
" FM_HARD_RESET " ,
2021-06-05 23:27:02 -05:00
" FM_LFO " ,
2021-12-09 00:46:48 -05:00
" FM_LFO_WAVE " ,
2021-05-19 02:05:24 -05:00
" FM_TL " ,
2022-05-02 03:52:45 -05:00
" FM_AM " ,
2021-05-19 02:05:24 -05:00
" FM_AR " ,
2022-05-02 03:52:45 -05:00
" FM_DR " ,
" FM_SL " ,
" FM_D2R " ,
" FM_RR " ,
" FM_DT " ,
" FM_DT2 " ,
" FM_RS " ,
" FM_KSR " ,
" FM_VIB " ,
" FM_SUS " ,
" FM_WS " ,
" FM_SSG " ,
2022-05-03 17:37:17 -05:00
" FM_REV " ,
" FM_EG_SHIFT " ,
2021-05-19 02:05:24 -05:00
" FM_FB " ,
" FM_MULT " ,
2022-05-02 03:52:45 -05:00
" FM_FINE " ,
" FM_FIXFREQ " ,
2021-05-19 02:05:24 -05:00
" FM_EXTCH " ,
2022-01-20 17:54:11 -05:00
" FM_AM_DEPTH " ,
" FM_PM_DEPTH " ,
2021-05-19 02:05:24 -05:00
2023-02-03 17:00:15 -05:00
" FM_LFO2 " ,
" FM_LFO2_WAVE " ,
2021-05-19 02:05:24 -05:00
" STD_NOISE_FREQ " ,
2021-05-28 00:36:25 -05:00
" STD_NOISE_MODE " ,
" WAVE " ,
" GB_SWEEP_TIME " ,
" GB_SWEEP_DIR " ,
2021-06-09 01:08:42 -05:00
" PCE_LFO_MODE " ,
" PCE_LFO_SPEED " ,
2022-02-01 18:28:48 -05:00
" NES_SWEEP " ,
2022-04-29 00:18:51 -05:00
" NES_DMC " ,
2022-02-01 18:28:48 -05:00
2021-12-07 01:23:57 -05:00
" C64_CUTOFF " ,
" C64_RESONANCE " ,
" C64_FILTER_MODE " ,
" C64_RESET_TIME " ,
" C64_RESET_MASK " ,
" C64_FILTER_RESET " ,
" C64_DUTY_RESET " ,
" C64_EXTENDED " ,
2022-01-11 18:38:26 -05:00
" C64_FINE_DUTY " ,
" C64_FINE_CUTOFF " ,
2021-12-07 01:23:57 -05:00
2021-12-09 17:06:28 -05:00
" AY_ENVELOPE_SET " ,
" AY_ENVELOPE_LOW " ,
" AY_ENVELOPE_HIGH " ,
2022-01-11 18:38:26 -05:00
" AY_ENVELOPE_SLIDE " ,
2022-01-18 18:21:27 -05:00
" AY_NOISE_MASK_AND " ,
" AY_NOISE_MASK_OR " ,
2022-01-20 03:23:03 -05:00
" AY_AUTO_ENVELOPE " ,
2022-03-26 20:55:43 -05:00
" AY_IO_WRITE " ,
" AY_AUTO_PWM " ,
2021-12-09 17:06:28 -05:00
2022-04-03 22:37:16 -05:00
" FDS_MOD_DEPTH " ,
" FDS_MOD_HIGH " ,
" FDS_MOD_LOW " ,
" FDS_MOD_POS " ,
" FDS_MOD_WAVE " ,
2022-01-14 23:26:22 -05:00
" SAA_ENVELOPE " ,
2022-03-26 23:39:20 -05:00
" AMIGA_FILTER " ,
2022-03-27 00:02:17 -05:00
" AMIGA_AM " ,
" AMIGA_PM " ,
2022-02-22 17:40:29 -05:00
" LYNX_LFSR_LOAD " ,
2022-01-14 23:26:22 -05:00
2022-02-22 10:01:57 +01:00
" QSOUND_ECHO_FEEDBACK " ,
2022-02-22 21:16:46 +01:00
" QSOUND_ECHO_DELAY " ,
2022-02-22 10:01:57 +01:00
" QSOUND_ECHO_LEVEL " ,
2022-04-28 23:58:11 -05:00
" QSOUND_SURROUND " ,
2022-02-22 10:01:57 +01:00
2022-03-07 13:34:13 +09:00
" X1_010_ENVELOPE_SHAPE " ,
" X1_010_ENVELOPE_ENABLE " ,
" X1_010_ENVELOPE_MODE " ,
" X1_010_ENVELOPE_PERIOD " ,
" X1_010_ENVELOPE_SLIDE " ,
" X1_010_AUTO_ENVELOPE " ,
Prepare for split sample chip instrument
(MSM6258, MSM6295, QSound, Sega PCM, ADPCM-A, ADPCM-B, YMZ280B, RF5C68)
Instrument color and icons are placeholder.
different volume range, hard panned/soft panned and/or independent volume per output, chip-dependent features (global volume, echo, etc)
Allow use sample in instrument tab for chip with sample support
Prepare to support X1-010 Seta 2 style bankswitch behavior
Prepare to support AY89x0 PCM DAC
Support volume for PCE sample (DAC)
Fix Lynx, Y8950 sample pitch matches to sample preview
Support PCM DAC with backward and pingpong loop mode
Reduce some codes
Add Sega PCM, AY89x0, QSound, PCM DAC, Lynx per-channel debug support
2022-08-27 16:27:36 +09:00
" X1_010_SAMPLE_BANK_SLOT " ,
2022-03-07 13:34:13 +09:00
2022-03-08 13:38:24 +09:00
" WS_SWEEP_TIME " ,
" WS_SWEEP_AMOUNT " ,
2022-03-23 01:48:45 +09:00
" N163_WAVE_POSITION " ,
" N163_WAVE_LENGTH " ,
" N163_WAVE_MODE " ,
" N163_WAVE_LOAD " ,
" N163_WAVE_LOADPOS " ,
" N163_WAVE_LOADLEN " ,
2022-04-15 16:10:57 -05:00
" N163_WAVE_LOADMODE " ,
2022-03-23 01:48:45 +09:00
" N163_CHANNEL_LIMIT " ,
" N163_GLOBAL_WAVE_LOAD " ,
" N163_GLOBAL_WAVE_LOADPOS " ,
" N163_GLOBAL_WAVE_LOADLEN " ,
" N163_GLOBAL_WAVE_LOADMODE " ,
2022-09-25 04:02:06 -05:00
" SU_SWEEP_PERIOD_LOW " ,
" SU_SWEEP_PERIOD_HIGH " ,
" SU_SWEEP_BOUND " ,
" SU_SWEEP_ENABLE " ,
" SU_SYNC_PERIOD_LOW " ,
2022-09-25 20:22:22 +09:00
" SU_SYNC_PERIOD_HIGH " ,
2022-05-19 04:36:26 -05:00
Prepare for split sample chip instrument
(MSM6258, MSM6295, QSound, Sega PCM, ADPCM-A, ADPCM-B, YMZ280B, RF5C68)
Instrument color and icons are placeholder.
different volume range, hard panned/soft panned and/or independent volume per output, chip-dependent features (global volume, echo, etc)
Allow use sample in instrument tab for chip with sample support
Prepare to support X1-010 Seta 2 style bankswitch behavior
Prepare to support AY89x0 PCM DAC
Support volume for PCE sample (DAC)
Fix Lynx, Y8950 sample pitch matches to sample preview
Support PCM DAC with backward and pingpong loop mode
Reduce some codes
Add Sega PCM, AY89x0, QSound, PCM DAC, Lynx per-channel debug support
2022-08-27 16:27:36 +09:00
" ADPCMA_GLOBAL_VOLUME " ,
2022-09-25 04:02:06 -05:00
" SNES_ECHO " ,
" SNES_PITCH_MOD " ,
2022-09-25 20:25:05 +09:00
" SNES_INVERT " ,
2022-09-25 04:02:06 -05:00
" SNES_GAIN_MODE " ,
" SNES_GAIN " ,
" SNES_ECHO_ENABLE " ,
" SNES_ECHO_DELAY " ,
" SNES_ECHO_VOL_LEFT " ,
" SNES_ECHO_VOL_RIGHT " ,
" SNES_ECHO_FEEDBACK " ,
" SNES_ECHO_FIR " ,
2022-12-17 00:09:56 -05:00
" NES_ENV_MODE " ,
" NES_LENGTH " ,
" NES_COUNT_MODE " ,
" MACRO_OFF " ,
" MACRO_ON " ,
2022-10-25 00:43:03 -05:00
2023-02-05 10:04:31 +09:00
" SURROUND_PANNING " ,
" FM_AM2_DEPTH " ,
" FM_PM2_DEPTH " ,
2022-12-25 18:51:23 +09:00
" ES5506_FILTER_MODE " ,
" ES5506_FILTER_K1 " ,
" ES5506_FILTER_K2 " ,
" ES5506_FILTER_K1_SLIDE " ,
" ES5506_FILTER_K2_SLIDE " ,
" ES5506_ENVELOPE_COUNT " ,
" ES5506_ENVELOPE_LVRAMP " ,
" ES5506_ENVELOPE_RVRAMP " ,
" ES5506_ENVELOPE_K1RAMP " ,
" ES5506_ENVELOPE_K2RAMP " ,
" ES5506_PAUSE " ,
2023-03-27 03:29:43 -05:00
" HINT_ARP_TIME " ,
2023-05-04 16:49:47 -05:00
" SNES_GLOBAL_VOL_LEFT " ,
" SNES_GLOBAL_VOL_RIGHT " ,
2023-05-05 01:10:03 -05:00
" NES_LINEAR_LENGTH " ,
2023-07-05 15:29:11 -07:00
" EXTERNAL " ,
2023-07-05 15:07:44 -07:00
2023-10-23 13:49:03 -05:00
" C64_AD " ,
" C64_SR " ,
2023-10-25 17:10:16 -03:00
" ESFM_OP_PANNING " ,
" ESFM_OUTLVL " ,
" ESFM_MODIN " ,
" ESFM_ENV_DELAY " ,
2024-01-17 14:48:47 -05:00
2024-03-17 15:57:41 -05:00
" MACRO_RESTART " ,
2024-01-24 03:15:41 -05:00
" POWERNOISE_COUNTER_LOAD " ,
" POWERNOISE_IO_WRITE " ,
2024-01-21 06:44:29 -05:00
2024-02-04 03:02:12 -05:00
" DAVE_HIGH_PASS " ,
" DAVE_RING_MOD " ,
" DAVE_SWAP_COUNTERS " ,
" DAVE_LOW_PASS " ,
" DAVE_CLOCK_DIV " ,
2024-03-16 14:59:02 +07:00
" MINMOD_ECHO " ,
2024-04-01 17:27:31 +07:00
" BIFURCATOR_STATE_LOAD " ,
2024-08-02 02:21:44 -05:00
" BIFURCATOR_PARAMETER " ,
2024-08-08 12:11:47 +03:00
" FDS_MOD_AUTO " ,
2024-08-25 13:34:19 +03:00
" FM_OPMASK " ,
2024-08-08 12:11:47 +03:00
2024-07-13 19:15:23 -05:00
" MULTIPCM_MIX_FM " ,
" MULTIPCM_MIX_PCM " ,
" MULTIPCM_LFO " ,
" MULTIPCM_VIB " ,
" MULTIPCM_AM " ,
" MULTIPCM_AR " ,
" MULTIPCM_D1R " ,
" MULTIPCM_DL " ,
" MULTIPCM_D2R " ,
" MULTIPCM_RR " ,
" MULTIPCM_RC " ,
" MULTIPCM_DAMP " ,
" MULTIPCM_PSEUDO_REVERB " ,
" MULTIPCM_LFO_RESET " ,
2024-09-13 23:46:03 -05:00
" MULTIPCM_LEVEL_DIRECT " ,
2024-08-08 12:11:47 +03:00
" SID3_SPECIAL_WAVE " ,
" SID3_RING_MOD_SRC " ,
" SID3_HARD_SYNC_SRC " ,
" SID3_PHASE_MOD_SRC " ,
" SID3_WAVE_MIX " ,
" SID3_LFSR_FEEDBACK_BITS " ,
" SID3_1_BIT_NOISE " ,
2024-08-12 12:59:34 +03:00
" SID3_FILTER_DISTORTION " ,
" SID3_FILTER_OUTPUT_VOLUME " ,
" SID3_CHANNEL_INVERSION " ,
" SID3_FILTER_CONNECTION " ,
" SID3_FILTER_MATRIX " ,
" SID3_FILTER_ENABLE " ,
2024-08-12 16:54:26 +03:00
2024-08-13 20:36:45 +03:00
" C64_PW_SLIDE " ,
" C64_CUTOFF_SLIDE " ,
" SID3_PHASE_RESET " ,
" SID3_NOISE_PHASE_RESET " ,
" SID3_ENVELOPE_RESET " ,
2024-08-18 18:55:46 +03:00
" SID3_CUTOFF_SCALING " ,
2025-03-11 21:58:11 +01:00
" SID3_RESONANCE_SCALING " ,
2025-05-20 21:24:20 +04:00
" WS_GLOBAL_SPEAKER_VOLUME " ,
2025-05-21 15:10:47 -05:00
" FM_ALG " ,
" FM_FMS " ,
" FM_AMS " ,
" FM_FMS2 " ,
" FM_AMS2 "
2021-05-19 02:05:24 -05:00
} ;
2025-10-18 21:42:57 -05:00
// fail build if you forgot to update the array
2022-04-15 16:10:57 -05:00
static_assert ( ( sizeof ( cmdName ) / sizeof ( void * ) ) = = DIV_CMD_MAX , " update cmdName! " ) ;
2025-10-18 21:42:57 -05:00
// formats a note
// used for the pattern visualizer in console mode, justifying the use
// of a static array.
2025-10-15 21:05:13 -05:00
const char * formatNote ( short note ) {
2025-10-15 21:56:04 -05:00
static char ret [ 16 ] ;
2025-10-15 21:05:13 -05:00
if ( note = = DIV_NOTE_OFF ) {
2021-05-12 03:58:55 -05:00
return " OFF " ;
2025-10-15 21:05:13 -05:00
} else if ( note = = DIV_NOTE_REL ) {
2022-02-08 03:50:42 -05:00
return " === " ;
2025-10-15 21:05:13 -05:00
} else if ( note = = DIV_MACRO_REL ) {
2022-02-08 03:50:42 -05:00
return " REL " ;
2025-10-15 21:05:13 -05:00
} else if ( note < 0 ) {
2021-05-12 03:58:55 -05:00
return " --- " ;
}
2025-10-15 21:56:04 -05:00
snprintf ( ret , 16 , " %s%d " , notes [ note % 12 ] , ( note - 60 ) / 12 ) ;
2021-05-12 03:58:55 -05:00
return ret ;
}
2025-10-18 21:42:57 -05:00
// send a command to a dispatch.
2021-05-19 02:05:24 -05:00
int DivEngine : : dispatchCmd ( DivCommand c ) {
2025-10-18 21:42:57 -05:00
// used for the commands visualizer in console mode
2021-05-19 02:05:24 -05:00
if ( view = = DIV_STATUS_COMMANDS ) {
2025-10-18 21:42:57 -05:00
// don't print if we are "skipping" (seeking to a position, usually after channel reset on loop)
2022-08-07 17:40:01 -05:00
if ( ! skipping ) {
switch ( c . cmd ) {
// strip away hinted/useless commands
case DIV_CMD_GET_VOLUME :
break ;
case DIV_CMD_VOLUME :
break ;
case DIV_CMD_NOTE_PORTA :
break ;
case DIV_CMD_LEGATO :
break ;
case DIV_CMD_PITCH :
break ;
case DIV_CMD_PRE_NOTE :
break ;
default :
2025-10-18 21:42:57 -05:00
// print command
2022-08-07 17:40:01 -05:00
printf ( " %8d | %d: %s(%d, %d) \n " , totalTicksR , c . chan , cmdName [ c . cmd ] , c . value , c . value2 ) ;
}
}
2021-05-19 02:05:24 -05:00
}
totalCmds + + ;
2025-10-18 21:42:57 -05:00
// up to 2000 commands can be queued in the command queue (used by the GUI for pattern visualizer)
2022-02-16 16:11:15 -05:00
if ( cmdStreamEnabled & & cmdStream . size ( ) < 2000 ) {
cmdStream . push_back ( c ) ;
}
2022-03-31 03:33:05 -05:00
2025-10-18 21:42:57 -05:00
// MIDI output code
// we turn this command into MIDI messages if the output mode is "melodic"
// if the channel is outside the range 0-15, it will be wrapped back
2023-03-22 00:51:54 -04:00
if ( output ) if ( ! skipping & & output - > midiOut ! = NULL & & ! isChannelMuted ( c . chan ) ) {
2022-03-31 03:33:05 -05:00
if ( output - > midiOut - > isDeviceOpen ( ) ) {
2022-09-26 01:27:36 -05:00
if ( midiOutMode = = DIV_MIDI_MODE_NOTE ) {
2025-10-18 21:42:57 -05:00
// scale volume to MIDI velocity range
2022-09-26 01:27:36 -05:00
int scaledVol = ( chan [ c . chan ] . volume * 127 ) / MAX ( 1 , chan [ c . chan ] . volMax ) ;
if ( scaledVol < 0 ) scaledVol = 0 ;
if ( scaledVol > 127 ) scaledVol = 127 ;
2025-10-18 21:42:57 -05:00
// process the command
2022-09-26 01:27:36 -05:00
switch ( c . cmd ) {
case DIV_CMD_NOTE_ON :
case DIV_CMD_LEGATO :
2025-10-18 21:42:57 -05:00
// turn the previous note off (if we have one)
2022-09-26 01:27:36 -05:00
if ( chan [ c . chan ] . curMidiNote > = 0 ) {
output - > midiOut - > send ( TAMidiMessage ( 0x80 | ( c . chan & 15 ) , chan [ c . chan ] . curMidiNote , scaledVol ) ) ;
}
2025-10-18 21:42:57 -05:00
// set current MIDI note
2022-09-26 01:27:36 -05:00
if ( c . value ! = DIV_NOTE_NULL ) {
chan [ c . chan ] . curMidiNote = c . value + 12 ;
if ( chan [ c . chan ] . curMidiNote < 0 ) chan [ c . chan ] . curMidiNote = 0 ;
if ( chan [ c . chan ] . curMidiNote > 127 ) chan [ c . chan ] . curMidiNote = 127 ;
}
2025-10-18 21:42:57 -05:00
// send note on (if we have one)
2025-03-09 02:25:20 -05:00
if ( chan [ c . chan ] . curMidiNote > = 0 ) {
output - > midiOut - > send ( TAMidiMessage ( 0x90 | ( c . chan & 15 ) , chan [ c . chan ] . curMidiNote , scaledVol ) ) ;
}
2022-09-26 01:27:36 -05:00
break ;
case DIV_CMD_NOTE_OFF :
case DIV_CMD_NOTE_OFF_ENV :
2025-10-18 21:42:57 -05:00
// turn the current note off (if we have one)
// we don't do this for macro release...
2022-09-26 01:27:36 -05:00
if ( chan [ c . chan ] . curMidiNote > = 0 ) {
output - > midiOut - > send ( TAMidiMessage ( 0x80 | ( c . chan & 15 ) , chan [ c . chan ] . curMidiNote , scaledVol ) ) ;
}
chan [ c . chan ] . curMidiNote = - 1 ;
break ;
case DIV_CMD_INSTRUMENT :
2025-10-18 21:42:57 -05:00
// instrument changes mapped to program change
// only first 128 instruments
2023-03-22 00:51:54 -04:00
if ( chan [ c . chan ] . lastIns ! = c . value & & midiOutProgramChange ) {
2025-03-09 02:25:20 -05:00
output - > midiOut - > send ( TAMidiMessage ( 0xc0 | ( c . chan & 15 ) , c . value & 0x7f , 0 ) ) ;
2022-09-26 01:27:36 -05:00
}
break ;
case DIV_CMD_VOLUME :
2025-10-18 21:42:57 -05:00
// volume changes are sent as MIDI aftertouch, as long as there isn't a note
// (processRow will set midiAftertouch to true on every row without note)
2022-09-26 01:27:36 -05:00
if ( chan [ c . chan ] . curMidiNote > = 0 & & chan [ c . chan ] . midiAftertouch ) {
chan [ c . chan ] . midiAftertouch = false ;
output - > midiOut - > send ( TAMidiMessage ( 0xa0 | ( c . chan & 15 ) , chan [ c . chan ] . curMidiNote , scaledVol ) ) ;
}
break ;
case DIV_CMD_PITCH : {
2025-10-18 21:42:57 -05:00
// map pitch changes to pitch bend (including vibrato)
2022-09-26 01:27:36 -05:00
int pitchBend = 8192 + ( c . value < < 5 ) ;
if ( pitchBend < 0 ) pitchBend = 0 ;
if ( pitchBend > 16383 ) pitchBend = 16383 ;
if ( pitchBend ! = chan [ c . chan ] . midiPitch ) {
chan [ c . chan ] . midiPitch = pitchBend ;
output - > midiOut - > send ( TAMidiMessage ( 0xe0 | ( c . chan & 15 ) , pitchBend & 0x7f , pitchBend > > 7 ) ) ;
}
break ;
2022-04-09 01:50:44 -05:00
}
2022-09-26 01:27:36 -05:00
case DIV_CMD_PANNING : {
2025-10-18 21:42:57 -05:00
// this is mapped to General MIDI panning CC
2022-09-26 01:27:36 -05:00
int pan = convertPanSplitToLinearLR ( c . value , c . value2 , 127 ) ;
2025-03-09 02:25:20 -05:00
if ( pan < 0 ) pan = 0 ;
if ( pan > 127 ) pan = 127 ;
2022-09-26 01:27:36 -05:00
output - > midiOut - > send ( TAMidiMessage ( 0xb0 | ( c . chan & 15 ) , 0x0a , pan ) ) ;
break ;
2022-04-09 01:50:44 -05:00
}
2022-09-26 01:27:36 -05:00
case DIV_CMD_HINT_PORTA : {
2025-10-18 21:42:57 -05:00
// portamento handling is complicated
// in General MIDI, portamento consists of sending a CC for duration and another for target note
// this differs from Furnace, where the parameter is speed rather than duration
// it is also impossible to perform an indefinite slide down/up other than
// by using pitch bend, but the default range is limited and we already
// use it for pitch changes/vibrato
// only send portamento if it is enabling
2022-09-26 01:27:36 -05:00
if ( c . value2 > 0 ) {
2025-10-18 21:42:57 -05:00
// and only if we have a target note
2022-09-26 01:27:36 -05:00
if ( c . value < = 0 | | c . value > = 255 ) break ;
//output->midiOut->send(TAMidiMessage(0x80|(c.chan&15),chan[c.chan].curMidiNote,scaledVol));
int target = c . value + 12 ;
if ( target < 0 ) target = 0 ;
if ( target > 127 ) target = 127 ;
2025-10-18 21:42:57 -05:00
// set the source note?
2022-09-26 01:27:36 -05:00
if ( chan [ c . chan ] . curMidiNote > = 0 ) {
output - > midiOut - > send ( TAMidiMessage ( 0xb0 | ( c . chan & 15 ) , 0x54 , chan [ c . chan ] . curMidiNote ) ) ;
}
2025-10-18 21:42:57 -05:00
// set the duration
// no effort whatsoever is done to predict how long will the slide last
2022-09-26 01:27:36 -05:00
output - > midiOut - > send ( TAMidiMessage ( 0xb0 | ( c . chan & 15 ) , 0x05 , 1 /*MIN(0x7f,c.value2/4)*/ ) ) ;
2025-10-18 21:42:57 -05:00
// turn portamento on
2022-09-26 01:27:36 -05:00
output - > midiOut - > send ( TAMidiMessage ( 0xb0 | ( c . chan & 15 ) , 0x41 , 0x7f ) ) ;
2025-10-18 21:42:57 -05:00
// send a note on (why?)
2022-09-26 01:27:36 -05:00
output - > midiOut - > send ( TAMidiMessage ( 0x90 | ( c . chan & 15 ) , target , scaledVol ) ) ;
} else {
2025-10-18 21:42:57 -05:00
// disable portamento otherwise
2022-09-26 01:27:36 -05:00
output - > midiOut - > send ( TAMidiMessage ( 0xb0 | ( c . chan & 15 ) , 0x41 , 0 ) ) ;
}
break ;
2022-04-09 01:50:44 -05:00
}
2025-10-18 21:42:57 -05:00
// other commands are simply ignored
2022-09-26 01:27:36 -05:00
default :
break ;
2022-03-31 03:33:05 -05:00
}
}
}
}
2025-10-18 21:42:57 -05:00
// map the channel to channel of chip
// c.dis is a copy of c.chan because we'll use it in the next call
2022-01-08 16:03:32 -05:00
c . chan = dispatchChanOfChan [ c . dis ] ;
2022-03-31 03:33:05 -05:00
2025-10-18 21:42:57 -05:00
// dispatch command to chip dispatch
2022-01-08 16:03:32 -05:00
return disCont [ dispatchOfChan [ c . dis ] ] . dispatch - > dispatch ( c ) ;
2021-05-19 02:05:24 -05:00
}
2025-10-18 21:42:57 -05:00
// this function handles per-chip normal effects
2021-05-13 02:39:26 -05:00
bool DivEngine : : perSystemEffect ( int ch , unsigned char effect , unsigned char effectVal ) {
2025-10-18 21:42:57 -05:00
// don't process invalid chips
2022-08-18 13:26:22 +07:00
DivSysDef * sysDef = sysDefs [ sysOfChan [ ch ] ] ;
if ( sysDef = = NULL ) return false ;
2025-10-18 21:42:57 -05:00
// find the effect handler
2022-08-18 13:26:22 +07:00
auto iter = sysDef - > effectHandlers . find ( effect ) ;
if ( iter = = sysDef - > effectHandlers . end ( ) ) return false ;
EffectHandler handler = iter - > second ;
int val = 0 ;
int val2 = 0 ;
2025-10-18 21:42:57 -05:00
// map values using the handler's function
2022-08-18 13:26:22 +07:00
try {
val = handler . val ? handler . val ( effect , effectVal ) : effectVal ;
val2 = handler . val2 ? handler . val2 ( effect , effectVal ) : 0 ;
} catch ( DivDoNotHandleEffect & e ) {
return false ;
}
2025-10-18 21:42:57 -05:00
// dispatch command
2022-08-18 13:26:22 +07:00
// wouldn't this cause problems if it were to return 0?
return dispatchCmd ( DivCommand ( handler . dispatchCmd , ch , val , val2 ) ) ;
2021-05-13 02:39:26 -05:00
}
2025-10-18 21:42:57 -05:00
// this handles per-chip post effects...
2021-05-15 03:13:21 -05:00
bool DivEngine : : perSystemPostEffect ( int ch , unsigned char effect , unsigned char effectVal ) {
2025-10-18 21:42:57 -05:00
// don't process invalid chips
2022-08-18 13:26:22 +07:00
DivSysDef * sysDef = sysDefs [ sysOfChan [ ch ] ] ;
if ( sysDef = = NULL ) return false ;
2025-10-18 21:42:57 -05:00
// find the effect handler
2022-08-18 13:26:22 +07:00
auto iter = sysDef - > postEffectHandlers . find ( effect ) ;
if ( iter = = sysDef - > postEffectHandlers . end ( ) ) return false ;
EffectHandler handler = iter - > second ;
int val = 0 ;
int val2 = 0 ;
2025-10-18 21:42:57 -05:00
// map values using the handler's function
2022-08-18 13:26:22 +07:00
try {
val = handler . val ? handler . val ( effect , effectVal ) : effectVal ;
val2 = handler . val2 ? handler . val2 ( effect , effectVal ) : 0 ;
} catch ( DivDoNotHandleEffect & e ) {
return true ;
}
2025-10-18 21:42:57 -05:00
// dispatch command
2022-08-18 13:26:22 +07:00
// wouldn't this cause problems if it were to return 0?
return dispatchCmd ( DivCommand ( handler . dispatchCmd , ch , val , val2 ) ) ;
2021-05-15 03:13:21 -05:00
}
2025-10-18 21:42:57 -05:00
// ...and this handles chip pre-effects
2023-08-30 02:17:16 -05:00
bool DivEngine : : perSystemPreEffect ( int ch , unsigned char effect , unsigned char effectVal ) {
DivSysDef * sysDef = sysDefs [ sysOfChan [ ch ] ] ;
if ( sysDef = = NULL ) return false ;
auto iter = sysDef - > preEffectHandlers . find ( effect ) ;
if ( iter = = sysDef - > preEffectHandlers . end ( ) ) return false ;
EffectHandler handler = iter - > second ;
int val = 0 ;
int val2 = 0 ;
try {
val = handler . val ? handler . val ( effect , effectVal ) : effectVal ;
val2 = handler . val2 ? handler . val2 ( effect , effectVal ) : 0 ;
} catch ( DivDoNotHandleEffect & e ) {
return false ;
}
// wouldn't this cause problems if it were to return 0?
return dispatchCmd ( DivCommand ( handler . dispatchCmd , ch , val , val2 ) ) ;
}
2025-10-18 21:42:57 -05:00
// this is called by nextRow() before it calls processRow()
// `i` is the channel
2023-08-30 02:17:16 -05:00
void DivEngine : : processRowPre ( int i ) {
int whatOrder = curOrder ;
int whatRow = curRow ;
DivPattern * pat = curPat [ i ] . getPattern ( curOrders - > ord [ i ] [ whatOrder ] , false ) ;
2025-10-18 21:42:57 -05:00
// check all effects
2023-08-30 02:17:16 -05:00
for ( int j = 0 ; j < curPat [ i ] . effectCols ; j + + ) {
2025-10-15 21:05:13 -05:00
short effect = pat - > newData [ whatRow ] [ DIV_PAT_FX ( j ) ] ;
short effectVal = pat - > newData [ whatRow ] [ DIV_PAT_FXVAL ( j ) ] ;
2023-08-30 02:17:16 -05:00
2025-10-18 21:42:57 -05:00
// empty effect value is the same as zero
2023-08-30 02:17:16 -05:00
if ( effectVal = = - 1 ) effectVal = 0 ;
effectVal & = 255 ;
2025-10-18 21:42:57 -05:00
// per-chip pre-effects (that's it for now!)
// the other pre-effects are handled in processRow()
2023-08-30 02:17:16 -05:00
perSystemPreEffect ( i , effect , effectVal ) ;
}
}
2025-10-18 21:42:57 -05:00
// this is called by nextRow() or nextTick() (in the case of delay). it processes the next row for a channel.
// `i` is the channel and `afterDelay` determines whether this happens after EDxx or not.
// the processing order is:
// 1. pre-effects (delay and song control)
// 2. instrument
// 3. note reading (note off is done immediately)
// 4. volume
// 5. effects
// 6. note on
// 7. post-effects
2021-05-16 03:03:23 -05:00
void DivEngine : : processRow ( int i , bool afterDelay ) {
2025-10-18 21:42:57 -05:00
// if this is after delay, use the order/row where delay occurred
2021-12-05 16:11:12 -05:00
int whatOrder = afterDelay ? chan [ i ] . delayOrder : curOrder ;
int whatRow = afterDelay ? chan [ i ] . delayRow : curRow ;
2022-05-15 01:42:49 -05:00
DivPattern * pat = curPat [ i ] . getPattern ( curOrders - > ord [ i ] [ whatOrder ] , false ) ;
2021-05-16 03:03:23 -05:00
// pre effects
2025-10-18 21:42:57 -05:00
// these include song control ones such as speed, tempo or jumps which shall not be delayed
// it also includes EDxx (delay) itself so we can handle it
2022-05-11 02:42:05 -05:00
if ( ! afterDelay ) {
2025-10-18 21:42:57 -05:00
// set to true if we found an EDxx effect
2022-05-11 02:42:05 -05:00
bool returnAfterPre = false ;
2025-10-18 21:42:57 -05:00
// check all effects
2022-05-15 01:42:49 -05:00
for ( int j = 0 ; j < curPat [ i ] . effectCols ; j + + ) {
2025-10-15 21:05:13 -05:00
short effect = pat - > newData [ whatRow ] [ DIV_PAT_FX ( j ) ] ;
short effectVal = pat - > newData [ whatRow ] [ DIV_PAT_FXVAL ( j ) ] ;
2021-05-16 03:03:23 -05:00
2025-10-18 21:42:57 -05:00
// empty effect value is the same as zero
2022-05-11 02:42:05 -05:00
if ( effectVal = = - 1 ) effectVal = 0 ;
2023-05-09 03:36:05 -05:00
effectVal & = 255 ;
2022-05-11 02:42:05 -05:00
switch ( effect ) {
2023-02-05 02:56:39 -05:00
case 0x09 : // select groove pattern/speed 1
if ( song . grooves . empty ( ) ) {
2025-10-18 21:42:57 -05:00
// special case: sets speed 1 if the song lacks groove patterns
2023-02-05 02:56:39 -05:00
if ( effectVal > 0 ) speeds . val [ 0 ] = effectVal ;
} else {
2025-10-18 21:42:57 -05:00
// sets the groove pattern and resets current speed index
2023-02-05 02:56:39 -05:00
if ( effectVal < ( short ) song . grooves . size ( ) ) {
speeds = song . grooves [ effectVal ] ;
curSpeed = 0 ;
}
}
2022-05-11 02:42:05 -05:00
break ;
2023-02-05 02:56:39 -05:00
case 0x0f : // speed 1/speed 2
2025-10-18 21:42:57 -05:00
// if the value is 0 then ignore it
2023-02-05 02:56:39 -05:00
if ( speeds . len = = 2 & & song . grooves . empty ( ) ) {
2025-10-18 21:42:57 -05:00
// if there are two speeds and no groove patterns, set the second speed
2023-02-05 02:56:39 -05:00
if ( effectVal > 0 ) speeds . val [ 1 ] = effectVal ;
} else {
2025-10-18 21:42:57 -05:00
// otherwise set the first speed
2023-02-05 02:56:39 -05:00
if ( effectVal > 0 ) speeds . val [ 0 ] = effectVal ;
}
2022-05-11 02:42:05 -05:00
break ;
2024-03-15 14:56:55 -05:00
case 0xfd : // virtual tempo num
if ( effectVal > 0 ) virtualTempoN = effectVal ;
break ;
case 0xfe : // virtual tempo den
if ( effectVal > 0 ) virtualTempoD = effectVal ;
break ;
2022-05-11 02:42:05 -05:00
case 0x0b : // change order
2025-10-18 21:42:57 -05:00
// this actually schedules an order change
// we perform this change at the end of nextRow()
// COMPAT FLAG: simultaneous jump treatment
// - 0: normal (another 0Bxx effect will override the previous one)
// - 1: old Furnace (only the first 0Bxx effect in a row takes effect)
// - 2: DefleMask (same as 1)
// in the case of normal, the jump row (changePos) is not reset to 0
// this means that you can do 0Dxx 0Byy and it'll work, taking you to row xx of order yy
2022-09-10 01:39:42 -05:00
if ( changeOrd = = - 1 | | song . jumpTreatment = = 0 ) {
2022-05-11 02:42:05 -05:00
changeOrd = effectVal ;
2022-09-10 01:39:42 -05:00
if ( song . jumpTreatment = = 1 | | song . jumpTreatment = = 2 ) {
changePos = 0 ;
}
2022-05-11 02:42:05 -05:00
}
break ;
case 0x0d : // next order
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: ignore 0Dxx on the last order (ignoreJumpAtEnd)
// if there is a 0Dxx effect on the very last order, it is ignored
// COMPAT FLAG: simultaneous jump treatment
2022-09-10 01:39:42 -05:00
if ( song . jumpTreatment = = 2 ) {
2025-10-18 21:42:57 -05:00
// - 2: DefleMask (jump to next order unless it is the last one and ignoreJumpAtEnd is on)
2022-09-10 01:39:42 -05:00
if ( ( curOrder < ( curSubSong - > ordersLen - 1 ) | | ! song . ignoreJumpAtEnd ) ) {
2025-10-18 21:42:57 -05:00
// changeOrd -2 means increase order by 1
// it overrides a previous 0Bxx effect
2022-09-10 01:39:42 -05:00
changeOrd = - 2 ;
changePos = effectVal ;
}
} else if ( song . jumpTreatment = = 1 ) {
2025-10-18 21:42:57 -05:00
// - 1: old Furnace (same as 2 but ignored if 0Bxx is present)
2022-09-10 01:39:42 -05:00
if ( changeOrd < 0 & & ( curOrder < ( curSubSong - > ordersLen - 1 ) | | ! song . ignoreJumpAtEnd ) ) {
changeOrd = - 2 ;
changePos = effectVal ;
}
} else {
2025-10-18 21:42:57 -05:00
// - 0: normal
2022-09-10 01:39:42 -05:00
if ( curOrder < ( curSubSong - > ordersLen - 1 ) | | ! song . ignoreJumpAtEnd ) {
2025-10-18 21:42:57 -05:00
// set the target order if not set, allowing you to use 0B and 0D regardless of position
2022-09-10 01:39:42 -05:00
if ( changeOrd < 0 ) {
changeOrd = - 2 ;
}
changePos = effectVal ;
}
2022-05-11 02:42:05 -05:00
}
break ;
case 0xed : // delay
if ( effectVal ! = 0 ) {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: cut/delay effect policy (delayBehavior)
// - 0: strict
// - delays equal or greater to the speed * timeBase are ignored
// - 1: strict old
// - delays equal or greater to the speed are ignored
// - 2: lax (default)
// - no delay is ever ignored unless overridden by another
2022-09-21 23:41:22 -05:00
bool comparison = ( song . delayBehavior = = 1 ) ? ( effectVal < = nextSpeed ) : ( effectVal < ( nextSpeed * ( curSubSong - > timeBase + 1 ) ) ) ;
2022-08-21 23:56:58 -05:00
if ( song . delayBehavior = = 2 ) comparison = true ;
if ( comparison ) {
2025-10-18 21:42:57 -05:00
// set the delay row, order and timer
2024-08-24 11:25:50 -07:00
chan [ i ] . rowDelay = effectVal ;
2022-05-11 02:42:05 -05:00
chan [ i ] . delayOrder = whatOrder ;
chan [ i ] . delayRow = whatRow ;
2025-10-18 21:42:57 -05:00
// this here was a compatibility hack for DefleMask...
// if the delay time happens to be equal to the speed, it'll
// result in "delay lock" which halts all row processing
// until another good EDxx effect is found
// for some reason this didn't occur on Neo Geo...
// this hack is disabled due to its dirtiness and the fact I
// don't feel like being compatible with a buggy tracker any further
2022-05-11 02:42:05 -05:00
if ( effectVal = = nextSpeed ) {
//if (sysOfChan[i]!=DIV_SYSTEM_YM2610 && sysOfChan[i]!=DIV_SYSTEM_YM2610_EXT) chan[i].delayLocked=true;
} else {
chan [ i ] . delayLocked = false ;
}
2025-10-18 21:42:57 -05:00
// once we're done with pre-effects, get out and don't process any further
2022-05-11 02:42:05 -05:00
returnAfterPre = true ;
} else {
2022-08-21 23:56:58 -05:00
logV ( " higher than nextSpeed! %d>%d " , effectVal , nextSpeed ) ;
2022-05-11 02:42:05 -05:00
chan [ i ] . delayLocked = false ;
}
}
break ;
2021-12-08 00:27:20 -05:00
}
2021-05-16 03:03:23 -05:00
}
2025-10-18 21:42:57 -05:00
// stop processing if EDxx was found
2022-05-11 02:42:05 -05:00
if ( returnAfterPre ) return ;
2022-08-22 00:20:40 -05:00
} else {
2022-10-02 01:32:12 -05:00
//logV("honoring delay at position %d",whatRow);
2021-05-16 03:03:23 -05:00
}
2025-10-18 21:42:57 -05:00
// stop processing if delay lock is on (won't happen, ever)
2021-12-08 00:27:20 -05:00
if ( chan [ i ] . delayLocked ) return ;
2025-10-18 21:42:57 -05:00
// now we start reading...
2021-05-16 03:03:23 -05:00
// instrument
2022-03-30 00:08:04 -05:00
bool insChanged = false ;
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ whatRow ] [ DIV_PAT_INS ] ! = - 1 ) {
2025-10-18 21:42:57 -05:00
// only send an instrument change if it differs from the current ins
2025-10-15 21:05:13 -05:00
if ( chan [ i ] . lastIns ! = pat - > newData [ whatRow ] [ DIV_PAT_INS ] ) {
dispatchCmd ( DivCommand ( DIV_CMD_INSTRUMENT , i , pat - > newData [ whatRow ] [ DIV_PAT_INS ] ) ) ;
chan [ i ] . lastIns = pat - > newData [ whatRow ] [ DIV_PAT_INS ] ;
2022-03-30 00:08:04 -05:00
insChanged = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: legacy volume slides
// - sets volume to max once a vol slide down has finished (thus setting volume to volMax+1)
2022-07-03 23:31:35 -05:00
if ( song . legacyVolumeSlides & & chan [ i ] . volume = = chan [ i ] . volMax + 1 ) {
logV ( " forcing volume " ) ;
chan [ i ] . volume = chan [ i ] . volMax ;
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-07-03 23:31:35 -05:00
}
2022-03-14 01:23:31 -05:00
}
2021-05-16 03:03:23 -05:00
}
2025-10-18 21:42:57 -05:00
// note reading
// note offs are sent immediately
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ whatRow ] [ DIV_PAT_NOTE ] = = DIV_NOTE_OFF ) { // note off
2021-05-19 02:22:26 -05:00
chan [ i ] . keyOn = false ;
2022-01-19 01:36:20 -05:00
chan [ i ] . keyOff = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: reset slides on note off (inverted in the GUI)
// - a portamento/pitch slide will be halted upon encountering note off
// - this will not occur if the stopPortaOnNoteOff flag is on and this is a portamento
2022-02-08 17:17:01 -05:00
if ( chan [ i ] . inPorta & & song . noteOffResetsSlides ) {
2025-10-18 21:42:57 -05:00
// stopOnOff will be false if stopPortaOnNoteOff flag is off
2022-01-19 22:05:39 -05:00
if ( chan [ i ] . stopOnOff ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2022-01-19 22:05:39 -05:00
chan [ i ] . stopOnOff = false ;
}
2025-10-18 21:42:57 -05:00
// depending on the system, portamento may still be disabled
2022-01-19 22:05:39 -05:00
if ( disCont [ dispatchOfChan [ i ] ] . dispatch - > keyOffAffectsPorta ( dispatchChanOfChan [ i ] ) ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2025-10-18 21:42:57 -05:00
// this here is a now-disabled hack which makes the noise channel also stop when square 3 is
2022-03-24 01:27:53 -05:00
/*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) {
2022-01-19 22:05:39 -05:00
chan [ i + 1 ] . portaNote = - 1 ;
chan [ i + 1 ] . portaSpeed = - 1 ;
2022-03-24 01:27:53 -05:00
} */
2021-12-29 02:08:50 -05:00
}
2025-10-18 21:42:57 -05:00
// another compatibility hack which schedules a second reset later just in case
2022-01-19 22:05:39 -05:00
chan [ i ] . scheduledSlideReset = true ;
2021-12-29 02:08:50 -05:00
}
2025-10-18 21:42:57 -05:00
// send note off
2021-05-19 02:05:24 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_OFF , i ) ) ;
2025-10-15 21:05:13 -05:00
} else if ( pat - > newData [ whatRow ] [ DIV_PAT_NOTE ] = = DIV_NOTE_REL ) { // note off + env release
2022-02-08 03:50:42 -05:00
//chan[i].note=-1;
chan [ i ] . keyOn = false ;
chan [ i ] . keyOff = true ;
2025-10-18 21:42:57 -05:00
// same thing here regarding reset slide behavior
2022-02-08 17:17:01 -05:00
if ( chan [ i ] . inPorta & & song . noteOffResetsSlides ) {
2022-02-08 03:50:42 -05:00
if ( chan [ i ] . stopOnOff ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2022-02-08 03:50:42 -05:00
chan [ i ] . stopOnOff = false ;
}
if ( disCont [ dispatchOfChan [ i ] ] . dispatch - > keyOffAffectsPorta ( dispatchChanOfChan [ i ] ) ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2022-03-24 01:27:53 -05:00
/*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) {
2022-02-08 03:50:42 -05:00
chan [ i + 1 ] . portaNote = - 1 ;
chan [ i + 1 ] . portaSpeed = - 1 ;
2022-03-24 01:27:53 -05:00
} */
2022-02-08 03:50:42 -05:00
}
chan [ i ] . scheduledSlideReset = true ;
}
2025-10-18 21:42:57 -05:00
// send note release
2022-02-08 03:50:42 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_OFF_ENV , i ) ) ;
2023-10-03 04:38:28 -05:00
chan [ i ] . releasing = true ;
2025-10-15 21:05:13 -05:00
} else if ( pat - > newData [ whatRow ] [ DIV_PAT_NOTE ] = = DIV_MACRO_REL ) { // env release
2025-10-18 21:42:57 -05:00
// send macro release
2022-02-08 03:50:42 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_ENV_RELEASE , i ) ) ;
2023-10-03 04:38:28 -05:00
chan [ i ] . releasing = true ;
2025-10-15 21:05:13 -05:00
} else if ( pat - > newData [ whatRow ] [ DIV_PAT_NOTE ] ! = - 1 ) {
2025-10-18 21:42:57 -05:00
// prepare/schedule a new note
2021-12-19 00:02:48 -05:00
chan [ i ] . oldNote = chan [ i ] . note ;
2025-10-15 21:05:13 -05:00
chan [ i ] . note = pat - > newData [ whatRow ] [ DIV_PAT_NOTE ] - 60 ;
2025-10-18 21:42:57 -05:00
// I have no idea why is this check here since keyOn is guaranteed to be false at this point
// ...unless there's a way to trigger keyOn twice
2021-05-26 02:06:40 -05:00
if ( ! chan [ i ] . keyOn ) {
2025-10-18 21:42:57 -05:00
// the behavior of arpeggio reset upon note off varies per system
2022-01-08 16:03:32 -05:00
if ( disCont [ dispatchOfChan [ i ] ] . dispatch - > keyOffAffectsArp ( dispatchChanOfChan [ i ] ) ) {
2021-12-07 01:23:57 -05:00
chan [ i ] . arp = 0 ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_ARPEGGIO , i , chan [ i ] . arp ) ) ;
2021-12-07 01:23:57 -05:00
}
2021-05-26 02:06:40 -05:00
}
2021-05-16 03:03:23 -05:00
chan [ i ] . doNote = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: compatible arpeggio
// - once a new note plays, arp will not be applied for this tick
2022-02-08 16:48:19 -05:00
if ( chan [ i ] . arp ! = 0 & & song . compatibleArpeggio ) {
2021-05-28 00:36:25 -05:00
chan [ i ] . arpYield = true ;
}
2021-05-16 03:03:23 -05:00
}
// volume
2024-08-23 10:39:14 -07:00
int volPortaTarget = - 1 ;
2024-08-23 11:16:22 -07:00
bool noApplyVolume = false ;
2025-10-18 21:42:57 -05:00
// here we read all effects and check for a volume slide with target/volume "portamento"/"scivolando" (a term I invented as an equivalent)
2024-08-23 10:39:14 -07:00
for ( int j = 0 ; j < curPat [ i ] . effectCols ; j + + ) {
2025-10-15 21:05:13 -05:00
short effect = pat - > newData [ whatRow ] [ DIV_PAT_FX ( j ) ] ;
2024-08-23 11:16:22 -07:00
if ( effect = = 0xd3 | | effect = = 0xd4 ) { // vol porta
2025-10-15 21:05:13 -05:00
volPortaTarget = pat - > newData [ whatRow ] [ DIV_PAT_VOL ] < < 8 ; // can be -256
2024-08-23 11:16:22 -07:00
2025-10-18 21:42:57 -05:00
// empty effect value is treated as 0
2025-10-15 21:05:13 -05:00
short effectVal = pat - > newData [ whatRow ] [ DIV_PAT_FXVAL ( j ) ] ;
2024-08-23 11:16:22 -07:00
if ( effectVal = = - 1 ) effectVal = 0 ;
effectVal & = 255 ;
noApplyVolume = effectVal > 0 ; // "D3.." or "D300" shouldn't stop volume from applying
break ; // technically you could have both D3 and D4... let's not care
2024-08-23 10:39:14 -07:00
}
}
2025-10-18 21:42:57 -05:00
// don't apply volume if a scivolando is set
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ whatRow ] [ DIV_PAT_VOL ] ! = - 1 & & ! noApplyVolume ) {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: legacy ALWAYS_SET_VOLUME behavior (oldAlwaysSetVolume)
// - prior to its addition, volume changes wouldn't be effective depending on the system if the volume is the same as the current one
// - afterwards, volume change is made regardless in order to set the bottom byte of volume ("subvolume")
2025-10-15 21:05:13 -05:00
if ( ! song . oldAlwaysSetVolume | | disCont [ dispatchOfChan [ i ] ] . dispatch - > getLegacyAlwaysSetVolume ( ) | | ( MIN ( chan [ i ] . volMax , chan [ i ] . volume ) > > 8 ) ! = pat - > newData [ whatRow ] [ DIV_PAT_VOL ] ) {
2025-10-18 21:42:57 -05:00
// here we let dispatchCmd() know we can do MIDI aftertouch if there isn't a note
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ whatRow ] [ DIV_PAT_NOTE ] = = - 1 ) {
2022-04-09 01:50:44 -05:00
chan [ i ] . midiAftertouch = true ;
}
2025-10-18 21:42:57 -05:00
// set the volume (bottom byte is set to 0)
2025-10-15 21:05:13 -05:00
chan [ i ] . volume = pat - > newData [ whatRow ] [ DIV_PAT_VOL ] < < 8 ;
2021-05-19 02:05:24 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2021-05-17 15:06:11 -05:00
}
2021-05-16 03:03:23 -05:00
}
2025-10-18 21:42:57 -05:00
// reset retrigger status
// this is the only effect that takes place only in the row it is placed, in a ProTracker-like fashion (why?)
2022-01-19 00:01:34 -05:00
chan [ i ] . retrigSpeed = 0 ;
2025-10-18 21:42:57 -05:00
// stuff necessary for effect processing
2022-02-18 02:03:31 -05:00
short lastSlide = - 1 ;
2022-03-30 00:08:04 -05:00
bool calledPorta = false ;
2022-04-29 23:41:14 -05:00
bool panChanged = false ;
2023-01-05 03:08:57 -05:00
bool surroundPanChanged = false ;
2024-04-23 14:36:06 -05:00
bool sampleOffSet = false ;
2022-02-18 02:03:31 -05:00
2021-05-16 03:03:23 -05:00
// effects
2022-05-15 01:42:49 -05:00
for ( int j = 0 ; j < curPat [ i ] . effectCols ; j + + ) {
2025-10-15 21:05:13 -05:00
short effect = pat - > newData [ whatRow ] [ DIV_PAT_FX ( j ) ] ;
short effectVal = pat - > newData [ whatRow ] [ DIV_PAT_FXVAL ( j ) ] ;
2021-05-16 03:03:23 -05:00
2025-10-18 21:42:57 -05:00
// an empty effect value is treated as zero
2021-05-16 03:03:23 -05:00
if ( effectVal = = - 1 ) effectVal = 0 ;
2023-05-09 03:36:05 -05:00
effectVal & = 255 ;
2021-05-16 03:03:23 -05:00
// per-system effect
2025-10-18 21:42:57 -05:00
// if there isn't one, go through normal effects
2021-05-16 03:03:23 -05:00
if ( ! perSystemEffect ( i , effect , effectVal ) ) switch ( effect ) {
2025-10-18 21:42:57 -05:00
/// PANNING
2022-04-29 23:41:14 -05:00
case 0x08 : // panning (split 4-bit)
chan [ i ] . panL = ( effectVal > > 4 ) | ( effectVal & 0xf0 ) ;
chan [ i ] . panR = ( effectVal & 15 ) | ( ( effectVal & 15 ) < < 4 ) ;
2025-10-18 21:42:57 -05:00
// panning command isn't sent until later
2022-04-29 23:41:14 -05:00
panChanged = true ;
break ;
case 0x80 : { // panning (linear)
2025-10-18 21:42:57 -05:00
// convert to splir
2022-04-29 23:41:14 -05:00
unsigned short pan = convertPanLinearToSplit ( effectVal , 8 , 255 ) ;
chan [ i ] . panL = pan > > 8 ;
chan [ i ] . panR = pan & 0xff ;
panChanged = true ;
break ;
}
case 0x81 : // panning left (split 8-bit)
chan [ i ] . panL = effectVal ;
panChanged = true ;
break ;
case 0x82 : // panning right (split 8-bit)
chan [ i ] . panR = effectVal ;
panChanged = true ;
2021-05-16 03:03:23 -05:00
break ;
2024-07-07 18:55:22 -05:00
case 0x83 : // pan slide
if ( effectVal ! = 0 ) {
2025-10-18 21:42:57 -05:00
// set the pan slide speed
2024-07-07 18:55:22 -05:00
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . panSpeed = ( effectVal & 15 ) ;
} else {
2024-07-08 03:55:17 -05:00
chan [ i ] . panSpeed = - ( effectVal > > 4 ) ;
2024-07-07 18:55:22 -05:00
}
// panbrello and slides are incompatible
chan [ i ] . panDepth = 0 ;
chan [ i ] . panRate = 0 ;
chan [ i ] . panPos = 0 ;
} else {
chan [ i ] . panSpeed = 0 ;
}
2025-10-18 21:42:57 -05:00
// send hint (for command stream export)
2025-04-05 04:33:46 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PAN_SLIDE , i , chan [ i ] . panSpeed & 0xff ) ) ;
2024-07-07 18:55:22 -05:00
break ;
case 0x84 : // panbrello
if ( chan [ i ] . panDepth = = 0 ) {
chan [ i ] . panPos = 0 ;
}
chan [ i ] . panDepth = effectVal & 15 ;
chan [ i ] . panRate = effectVal > > 4 ;
if ( chan [ i ] . panDepth ! = 0 ) {
2025-10-18 21:42:57 -05:00
// panbrello and slides are incompatible
2024-07-07 18:55:22 -05:00
chan [ i ] . panSpeed = 0 ;
}
2025-10-18 21:42:57 -05:00
// send hint (for command stream export)
2025-04-05 04:33:46 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PANBRELLO , i , effectVal ) ) ;
2024-07-07 18:55:22 -05:00
break ;
2023-01-05 03:08:57 -05:00
case 0x88 : // panning rear (split 4-bit)
chan [ i ] . panRL = ( effectVal > > 4 ) | ( effectVal & 0xf0 ) ;
chan [ i ] . panRR = ( effectVal & 15 ) | ( ( effectVal & 15 ) < < 4 ) ;
surroundPanChanged = true ;
break ;
case 0x89 : // panning left (split 8-bit)
chan [ i ] . panRL = effectVal ;
surroundPanChanged = true ;
break ;
case 0x8a : // panning right (split 8-bit)
chan [ i ] . panRR = effectVal ;
surroundPanChanged = true ;
break ;
2025-10-18 21:42:57 -05:00
/// PITCH and more
// internally, slides and portamento share the same variables
case 0x01 : // pitch slide up
// COMPAT FLAG: ignore duplicate slides
// - only the first 01xx effect is considered
// - 02xx still works
// - a previous portamento (03xx) will prevent this slide from occurring
// - E1xy/E2xy also will if *another* flag is set
2022-02-18 02:21:01 -05:00
if ( song . ignoreDuplicateSlides & & ( lastSlide = = 0x01 | | lastSlide = = 0x1337 ) ) break ;
2022-02-18 02:03:31 -05:00
lastSlide = 0x01 ;
2021-05-16 03:03:23 -05:00
if ( effectVal = = 0 ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2021-12-19 00:27:04 -05:00
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: arpeggio inhibits non-porta slides
// - the PRE_PORTA command is used to let the dispatch know we're entering a pitch slide
// - this prompts dispatch to stop processing arp macros during a slide
// - this only happens if pitch linearity is set to None
// - if we don't let dispatch know, the slide will never occur as arp takes over
2022-02-09 22:07:32 -05:00
if ( ! song . arpNonPorta ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
2021-05-16 03:03:23 -05:00
} else {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: limit slide range
// - this confines pitch slides from dispatch->getPortaFloor to C-8 (I think)
// - yep, the lowest portamento note depends on the system...
2022-02-03 02:24:11 -05:00
chan [ i ] . portaNote = song . limitSlides ? 0x60 : 255 ;
2021-05-16 03:03:23 -05:00
chan [ i ] . portaSpeed = effectVal ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2025-10-18 21:42:57 -05:00
// most of these are used for compat flag handling
2021-05-26 02:34:40 -05:00
chan [ i ] . portaStop = true ;
chan [ i ] . stopOnOff = false ;
2022-01-19 01:27:32 -05:00
chan [ i ] . scheduledSlideReset = false ;
2022-06-29 04:57:05 -05:00
chan [ i ] . wasShorthandPorta = false ;
2022-01-19 01:27:32 -05:00
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: arpeggio inhibits non-porta slides
// - the PRE_PORTA command is used to let the dispatch know we're entering a pitch slide
// - this prompts dispatch to stop processing arp macros during a slide
// - this only happens if pitch linearity is set to None
// - if we don't let dispatch know, the slide will never occur as arp takes over
2022-02-09 22:07:32 -05:00
if ( ! song . arpNonPorta ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , true , 0 ) ) ;
2021-05-16 03:03:23 -05:00
}
break ;
2025-10-18 21:42:57 -05:00
case 0x02 : // pitch slide down
// COMPAT FLAG: ignore duplicate slides
// - only the first 02xx effect is considered
// - 01xx still works
// - a previous portamento (03xx) will prevent this slide from occurring
// - E1xy/E2xy also will if *another* flag is set
2022-02-18 02:21:01 -05:00
if ( song . ignoreDuplicateSlides & & ( lastSlide = = 0x02 | | lastSlide = = 0x1337 ) ) break ;
2022-02-18 02:03:31 -05:00
lastSlide = 0x02 ;
2021-05-16 03:03:23 -05:00
if ( effectVal = = 0 ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2021-12-19 00:27:04 -05:00
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: arpeggio inhibits non-porta slides
2022-02-09 22:07:32 -05:00
if ( ! song . arpNonPorta ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
2021-05-16 03:03:23 -05:00
} else {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: limit slide range
// - this confines pitch slides from dispatch->getPortaFloor to C-8 (I think)
// - yep, the lowest portamento note depends on the system...
2022-02-05 16:33:06 -05:00
chan [ i ] . portaNote = song . limitSlides ? disCont [ dispatchOfChan [ i ] ] . dispatch - > getPortaFloor ( dispatchChanOfChan [ i ] ) : - 60 ;
2021-05-16 03:03:23 -05:00
chan [ i ] . portaSpeed = effectVal ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2021-05-19 03:09:51 -05:00
chan [ i ] . portaStop = true ;
2021-05-26 02:34:40 -05:00
chan [ i ] . stopOnOff = false ;
2022-01-19 01:27:32 -05:00
chan [ i ] . scheduledSlideReset = false ;
2022-06-29 04:57:05 -05:00
chan [ i ] . wasShorthandPorta = false ;
2022-01-19 01:27:32 -05:00
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: arpeggio inhibits non-porta slides
2022-02-09 22:07:32 -05:00
if ( ! song . arpNonPorta ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , true , 0 ) ) ;
2021-05-16 03:03:23 -05:00
}
break ;
case 0x03 : // portamento
2025-10-18 21:42:57 -05:00
// exception: the arpNonPorta flag is not checked here.
// a portamento shall override arp macros on non-linear pitch.
2021-05-16 03:03:23 -05:00
if ( effectVal = = 0 ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2021-12-19 00:27:04 -05:00
chan [ i ] . inPorta = false ;
2022-02-08 22:49:52 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
2021-05-16 03:03:23 -05:00
} else {
2025-10-18 21:42:57 -05:00
// lastPorta is used for the 06xy effect
2023-04-30 13:46:09 -05:00
chan [ i ] . lastPorta = effectVal ;
2025-10-18 21:42:57 -05:00
// this here is for a compatibility flag...
2022-03-30 00:08:04 -05:00
calledPorta = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: buggy portamento after sliding
// - you might want to slide up or down and then 03xx to return to the original note
// - if a porta to the same note is attempted after slide, for some reason it does not occur
2022-03-26 22:15:15 -05:00
if ( chan [ i ] . note = = chan [ i ] . oldNote & & ! chan [ i ] . inPorta & & song . buggyPortaAfterSlide ) {
2021-12-19 00:02:48 -05:00
chan [ i ] . portaNote = chan [ i ] . note ;
2021-12-19 00:27:04 -05:00
chan [ i ] . portaSpeed = - 1 ;
2021-12-19 00:02:48 -05:00
} else {
2025-10-18 21:42:57 -05:00
// compat flags get on my way
2021-12-19 00:02:48 -05:00
chan [ i ] . portaNote = chan [ i ] . note ;
chan [ i ] . portaSpeed = effectVal ;
2021-12-19 00:27:04 -05:00
chan [ i ] . inPorta = true ;
2025-10-18 21:42:57 -05:00
// ...but this one is for ANOTHER compat flag. yuck!
2022-06-29 04:57:05 -05:00
chan [ i ] . wasShorthandPorta = false ;
2021-12-19 00:02:48 -05:00
}
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2025-10-18 21:42:57 -05:00
// TODO; portaStop is guaranteed to be true anyway. what's the point of this?
2021-05-19 02:05:24 -05:00
chan [ i ] . portaStop = true ;
2025-10-18 21:42:57 -05:00
// this is why we didn't send noye on before.
// there may be a portamento which of course prevents a note on
2021-12-19 00:27:04 -05:00
if ( chan [ i ] . keyOn ) chan [ i ] . doNote = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: stop portamento on note off
// - if a portamento is called and then a note off occurs, stop portamento before the next note
// - ...unless noteOffResetsSlides is disabled
2022-03-03 23:14:38 -05:00
chan [ i ] . stopOnOff = song . stopPortaOnNoteOff ; // what?!
2022-01-19 01:27:32 -05:00
chan [ i ] . scheduledSlideReset = false ;
2022-02-08 22:49:52 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , true , 1 ) ) ;
2025-10-18 21:42:57 -05:00
// this is used to inhibit any other slide commands if the respective compat flag is enabled
2022-02-18 02:21:01 -05:00
lastSlide = 0x1337 ; // i hate this so much
2021-05-16 03:03:23 -05:00
}
break ;
2025-10-18 21:42:57 -05:00
// vibratos and pitch changes are mixed in.
2021-05-16 03:03:23 -05:00
case 0x04 : // vibrato
2025-10-18 21:42:57 -05:00
// remember the last vibrato for 05xy
2023-04-30 13:46:09 -05:00
if ( effectVal ) chan [ i ] . lastVibrato = effectVal ;
2021-05-16 03:03:23 -05:00
chan [ i ] . vibratoDepth = effectVal & 15 ;
chan [ i ] . vibratoRate = effectVal > > 4 ;
2025-04-17 18:58:11 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VIBRATO , i , ( chan [ i ] . vibratoDepth & 15 ) | ( chan [ i ] . vibratoRate < < 4 ) ) ) ;
2025-10-18 21:42:57 -05:00
// update pitch now
2021-05-19 02:05:24 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PITCH , i , chan [ i ] . pitch + ( ( ( chan [ i ] . vibratoDepth * vibTable [ chan [ i ] . vibratoPos ] * chan [ i ] . vibratoFine ) > > 4 ) / 15 ) ) ) ;
2021-05-16 03:03:23 -05:00
break ;
2025-10-18 21:42:57 -05:00
/// VOLUME-RELATED
2023-04-30 13:46:09 -05:00
case 0x05 : // vol slide + vibrato
2025-10-18 21:42:57 -05:00
// this effect is weird. it shouldn't be here considering we have more
// than one effect column, but I guess it had to be done
2023-04-30 13:46:09 -05:00
if ( effectVal = = 0 ) {
chan [ i ] . vibratoDepth = 0 ;
chan [ i ] . vibratoRate = 0 ;
} else {
chan [ i ] . vibratoDepth = chan [ i ] . lastVibrato & 15 ;
chan [ i ] . vibratoRate = chan [ i ] . lastVibrato > > 4 ;
}
2025-04-17 18:58:11 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VIBRATO , i , ( chan [ i ] . vibratoDepth & 15 ) | ( chan [ i ] . vibratoRate < < 4 ) ) ) ;
2025-10-18 21:42:57 -05:00
// update pitch now
2023-04-30 13:46:09 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PITCH , i , chan [ i ] . pitch + ( ( ( chan [ i ] . vibratoDepth * vibTable [ chan [ i ] . vibratoPos ] * chan [ i ] . vibratoFine ) > > 4 ) / 15 ) ) ) ;
// TODO: non-0x-or-x0 value should be treated as 00
if ( effectVal ! = 0 ) {
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . volSpeed = - ( effectVal & 15 ) * 64 ;
} else {
chan [ i ] . volSpeed = ( effectVal > > 4 ) * 64 ;
}
// tremolo and vol slides are incompatible
chan [ i ] . tremoloDepth = 0 ;
chan [ i ] . tremoloRate = 0 ;
} else {
chan [ i ] . volSpeed = 0 ;
}
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2023-04-30 13:46:09 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , chan [ i ] . volSpeed ) ) ;
break ;
case 0x06 : // vol slide + porta
2025-10-18 21:42:57 -05:00
// same thing here. this is another effect that doesn't need to exist.
2023-04-30 13:46:09 -05:00
if ( effectVal = = 0 | | chan [ i ] . lastPorta = = 0 ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
chan [ i ] . inPorta = false ;
dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
} else {
2025-10-18 21:42:57 -05:00
// this here is for a compatibility flag...
2023-04-30 13:46:09 -05:00
calledPorta = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: buggy portamento after sliding
// yes, this also affects 06xy.
2023-04-30 13:46:09 -05:00
if ( chan [ i ] . note = = chan [ i ] . oldNote & & ! chan [ i ] . inPorta & & song . buggyPortaAfterSlide ) {
chan [ i ] . portaNote = chan [ i ] . note ;
chan [ i ] . portaSpeed = - 1 ;
} else {
chan [ i ] . portaNote = chan [ i ] . note ;
chan [ i ] . portaSpeed = chan [ i ] . lastPorta ;
chan [ i ] . inPorta = true ;
chan [ i ] . wasShorthandPorta = false ;
}
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2025-10-18 21:42:57 -05:00
// this is the same as 03xx.
2023-04-30 13:46:09 -05:00
chan [ i ] . portaStop = true ;
if ( chan [ i ] . keyOn ) chan [ i ] . doNote = false ;
chan [ i ] . stopOnOff = song . stopPortaOnNoteOff ; // what?!
chan [ i ] . scheduledSlideReset = false ;
dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , true , 1 ) ) ;
lastSlide = 0x1337 ; // i hate this so much
}
2025-10-18 21:42:57 -05:00
// now handle volume slide
2023-04-30 13:46:09 -05:00
// TODO: non-0x-or-x0 value should be treated as 00
if ( effectVal ! = 0 ) {
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . volSpeed = - ( effectVal & 15 ) * 64 ;
} else {
chan [ i ] . volSpeed = ( effectVal > > 4 ) * 64 ;
}
// tremolo and vol slides are incompatible
chan [ i ] . tremoloDepth = 0 ;
chan [ i ] . tremoloRate = 0 ;
} else {
chan [ i ] . volSpeed = 0 ;
}
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2023-04-30 13:46:09 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , chan [ i ] . volSpeed ) ) ;
break ;
2022-03-14 21:50:52 +07:00
case 0x07 : // tremolo
2025-10-18 21:42:57 -05:00
// the implementation of tremolo in Furnace is more like that of
// vibrato. it oscillates according to a waveform.
// this differs from Defle where it is a consecutive vol slide
// up/down and exhibits a numbee of bugs.
2023-02-04 16:08:20 -05:00
if ( chan [ i ] . tremoloDepth = = 0 ) {
chan [ i ] . tremoloPos = 0 ;
}
chan [ i ] . tremoloDepth = effectVal & 15 ;
chan [ i ] . tremoloRate = effectVal > > 4 ;
2025-04-05 04:33:46 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_TREMOLO , i , effectVal ) ) ;
2025-10-18 21:42:57 -05:00
// unfortunately, we cannot run both tremolo and vol slide at once.
2023-06-06 15:54:24 -05:00
if ( chan [ i ] . tremoloDepth ! = 0 ) {
chan [ i ] . volSpeed = 0 ;
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2023-06-06 15:54:24 -05:00
} else {
2025-10-18 21:42:57 -05:00
// restore the volume if tremolo is disabled
2023-06-06 15:54:24 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
}
2022-03-14 21:50:52 +07:00
break ;
2025-10-18 21:42:57 -05:00
case 0x0a : // volume slide
// the speed multipler is 64, which means 4 ticks between volume changes with a value of 1
2022-03-26 21:43:15 -05:00
// TODO: non-0x-or-x0 value should be treated as 00
2021-05-16 03:03:23 -05:00
if ( effectVal ! = 0 ) {
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . volSpeed = - ( effectVal & 15 ) * 64 ;
} else {
chan [ i ] . volSpeed = ( effectVal > > 4 ) * 64 ;
}
2023-02-04 16:08:20 -05:00
// tremolo and vol slides are incompatible
chan [ i ] . tremoloDepth = 0 ;
chan [ i ] . tremoloRate = 0 ;
2021-05-16 03:03:23 -05:00
} else {
chan [ i ] . volSpeed = 0 ;
}
2025-10-18 21:42:57 -05:00
// reset the volume target
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , chan [ i ] . volSpeed ) ) ;
2021-05-16 03:03:23 -05:00
break ;
2025-10-18 21:42:57 -05:00
/// NOTE
2021-05-16 03:03:23 -05:00
case 0x00 : // arpeggio
chan [ i ] . arp = effectVal ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: reset note to base on arp stop (inverted in the GUI)
// - a 0000 effect resets arpeggio position
2022-03-17 16:37:49 -05:00
if ( chan [ i ] . arp = = 0 & & song . arp0Reset ) {
chan [ i ] . resetArp = true ;
2022-04-01 05:20:00 -05:00
}
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_ARPEGGIO , i , chan [ i ] . arp ) ) ;
2021-05-16 03:03:23 -05:00
break ;
2021-05-18 02:29:17 -05:00
case 0x0c : // retrigger
2025-10-18 21:42:57 -05:00
// this is the only non-continuous effect. it takes place exclusively
// within one row like most PC/Amiga trackers.
// consecutive 0Cxx effects will reset on each row...
2021-12-29 02:08:50 -05:00
if ( effectVal ! = 0 ) {
2022-01-19 00:01:34 -05:00
chan [ i ] . retrigSpeed = effectVal ;
chan [ i ] . retrigTick = 0 ;
2021-12-29 02:08:50 -05:00
}
2021-05-18 02:29:17 -05:00
break ;
2025-10-18 21:42:57 -05:00
/// MISC
2022-03-14 21:50:52 +07:00
case 0x90 : case 0x91 : case 0x92 : case 0x93 :
case 0x94 : case 0x95 : case 0x96 : case 0x97 :
case 0x98 : case 0x99 : case 0x9a : case 0x9b :
case 0x9c : case 0x9d : case 0x9e : case 0x9f : // set samp. pos
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: old sample offset effect
// - before 0.6.3 the sample offset effect was 9xxx, where `xxx` is multiplied by 256
// - the effect was then changed to 90xx/91xx/92xx, allowing you to set the low, mid and high bytes of the offset respectively
2024-04-23 14:36:06 -05:00
if ( song . oldSampleOffset ) {
2025-10-18 21:42:57 -05:00
// send sample position now
2024-04-23 14:36:06 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_SAMPLE_POS , i , ( ( ( effect & 0x0f ) < < 8 ) | effectVal ) * 256 ) ) ;
} else {
2025-10-18 21:42:57 -05:00
// change one byte and schedule sample position
2024-04-23 14:36:06 -05:00
if ( effect < 0x93 ) {
chan [ i ] . sampleOff & = ~ ( 0xff < < ( ( effect - 0x90 ) < < 3 ) ) ;
chan [ i ] . sampleOff | = effectVal < < ( ( effect - 0x90 ) < < 3 ) ;
sampleOffSet = true ;
}
}
2022-03-14 21:50:52 +07:00
break ;
2022-01-12 02:45:26 -05:00
case 0xc0 : case 0xc1 : case 0xc2 : case 0xc3 : // set Hz
2025-10-18 21:42:57 -05:00
// Cxxx, where `xxx` is between 1 and 1023
// divider is the tick rate in Hz
// cycles is the number of samples between ticks
// clockDrift is used for accuracy and subticks for low-latency mode
// (where we run faster thsn the tick rate to allow sub-tick note events from live playback)
2022-03-16 02:35:33 -05:00
divider = ( double ) ( ( ( effect & 0x3 ) < < 8 ) | effectVal ) ;
2022-08-15 22:40:04 -05:00
if ( divider < 1 ) divider = 1 ;
2025-03-05 04:49:22 -05:00
cycles = got . rate / divider ;
2022-01-12 17:45:07 -05:00
clockDrift = 0 ;
2022-05-22 22:36:48 -05:00
subticks = 0 ;
2022-01-12 02:45:26 -05:00
break ;
2024-07-17 04:11:24 -05:00
case 0xdc : // delayed mute
2025-10-18 21:42:57 -05:00
// used on XM import, where ECx actually mutes the note
// COMPAT FLAG: cut/delay effect policy (delayBehavior)
// - 0: strict
// - ignore cut if equal or greater than speed
// - 1: strict old
// - ignore cut if equal or greater than speed
// - 2: lax (default)
// - no cut is ever ignored unless overridden by another
2024-07-17 04:11:24 -05:00
if ( effectVal > 0 & & ( song . delayBehavior = = 2 | | effectVal < nextSpeed ) ) {
2025-10-18 21:42:57 -05:00
// the cut timer is ticked after nextRow(), so we set it one tick higher.
2024-07-17 04:11:24 -05:00
chan [ i ] . volCut = effectVal + 1 ;
chan [ i ] . cutType = 0 ;
}
break ;
2025-10-18 21:42:57 -05:00
case 0xd3 : // volume portamento (vol porta)/scivolando
2024-08-23 11:24:24 -07:00
// tremolo and vol slides are incompatible
chan [ i ] . tremoloDepth = 0 ;
chan [ i ] . tremoloRate = 0 ;
2025-10-18 21:42:57 -05:00
// check whether we will slide up or down
// the speed is 1, which means that 256 ticks will elapse between volume changes with a value of 1.
2024-08-23 11:24:24 -07:00
chan [ i ] . volSpeed = volPortaTarget < 0 ? 0 : volPortaTarget > chan [ i ] . volume ? effectVal : - effectVal ;
chan [ i ] . volSpeedTarget = chan [ i ] . volSpeed = = 0 ? - 1 : volPortaTarget ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE_TARGET , i , chan [ i ] . volSpeed , chan [ i ] . volSpeedTarget ) ) ;
break ;
case 0xd4 : // volume portamento fast (vol porta fast)
2025-10-18 21:42:57 -05:00
// this is the same as D3xx, but 256 times faster.
2024-08-23 11:24:24 -07:00
// tremolo and vol slides are incompatible
chan [ i ] . tremoloDepth = 0 ;
chan [ i ] . tremoloRate = 0 ;
chan [ i ] . volSpeed = volPortaTarget < 0 ? 0 : volPortaTarget > chan [ i ] . volume ? 256 * effectVal : - 256 * effectVal ;
chan [ i ] . volSpeedTarget = chan [ i ] . volSpeed = = 0 ? - 1 : volPortaTarget ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE_TARGET , i , chan [ i ] . volSpeed , chan [ i ] . volSpeedTarget ) ) ;
break ;
2021-05-18 02:53:59 -05:00
case 0xe0 : // arp speed
2025-10-18 21:42:57 -05:00
// the arp speed is global. I have no idea why.
2022-02-18 01:53:46 -05:00
if ( effectVal > 0 ) {
2022-05-15 01:42:49 -05:00
curSubSong - > arpLen = effectVal ;
2023-03-27 03:29:43 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_ARP_TIME , i , curSubSong - > arpLen ) ) ;
2022-02-18 01:53:46 -05:00
}
2021-05-18 02:53:59 -05:00
break ;
2021-05-16 03:03:23 -05:00
case 0xe1 : // portamento up
2025-10-18 21:42:57 -05:00
// this is a shortcut for 03xx and a higher note.
// it has the benefit of being able to be used in conjunction with a note.
2021-05-16 03:03:23 -05:00
chan [ i ] . portaNote = chan [ i ] . note + ( effectVal & 15 ) ;
2021-05-18 01:37:14 -05:00
chan [ i ] . portaSpeed = ( effectVal > > 4 ) * 4 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2025-10-18 21:42:57 -05:00
// these are for compatibility stuff
2021-05-19 02:05:24 -05:00
chan [ i ] . portaStop = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: stop portamento on note off
2022-03-03 23:14:38 -05:00
chan [ i ] . stopOnOff = song . stopPortaOnNoteOff ; // what?!
2022-01-19 01:27:32 -05:00
chan [ i ] . scheduledSlideReset = false ;
2025-10-18 21:42:57 -05:00
// only enter portamento if the speed is set
2022-02-03 01:30:03 -05:00
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . inPorta = true ;
2025-10-18 21:42:57 -05:00
// these are for compatibility flaga.
2022-02-03 01:30:03 -05:00
chan [ i ] . shorthandPorta = true ;
2022-06-29 04:57:05 -05:00
chan [ i ] . wasShorthandPorta = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: broken shortcut slides
// - oddly enough, shortcut slides are not communicated to the dispatch
// - this was fixed in 0.5.7
2022-02-18 01:27:26 -05:00
if ( ! song . brokenShortcutSlides ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , true , 0 ) ) ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: E1xy/E2xy also take priority over slides
// - another Defle hack. it places shortcut slides above pitch slides.
2022-04-13 00:34:00 -05:00
if ( song . e1e2AlsoTakePriority ) lastSlide = 0x1337 ; // ...
2022-02-03 01:30:03 -05:00
} else {
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: broken shortcut slides
2022-02-18 01:27:26 -05:00
if ( ! song . brokenShortcutSlides ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
2022-02-03 01:30:03 -05:00
}
2021-05-16 03:03:23 -05:00
break ;
case 0xe2 : // portamento down
2025-10-18 21:42:57 -05:00
// this is the same as E1xy but in the opposite direction.
2021-05-16 03:03:23 -05:00
chan [ i ] . portaNote = chan [ i ] . note - ( effectVal & 15 ) ;
2021-05-18 01:37:14 -05:00
chan [ i ] . portaSpeed = ( effectVal > > 4 ) * 4 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2021-05-19 02:05:24 -05:00
chan [ i ] . portaStop = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: stop portamento on note off
2022-03-03 23:14:38 -05:00
chan [ i ] . stopOnOff = song . stopPortaOnNoteOff ; // what?!
2022-01-19 01:27:32 -05:00
chan [ i ] . scheduledSlideReset = false ;
2022-02-03 01:30:03 -05:00
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . inPorta = true ;
2025-10-18 21:42:57 -05:00
// these are for compatibility flaga.
2022-02-03 01:30:03 -05:00
chan [ i ] . shorthandPorta = true ;
2022-06-29 04:57:05 -05:00
chan [ i ] . wasShorthandPorta = true ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: broken shortcut slides
2022-02-18 01:27:26 -05:00
if ( ! song . brokenShortcutSlides ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , true , 0 ) ) ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: E1xy/E2xy also take priority over slides
2022-04-13 00:34:00 -05:00
if ( song . e1e2AlsoTakePriority ) lastSlide = 0x1337 ; // ...
2022-02-03 01:30:03 -05:00
} else {
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: broken shortcut slides
2022-02-18 01:27:26 -05:00
if ( ! song . brokenShortcutSlides ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
2022-02-03 01:30:03 -05:00
}
2021-05-16 03:03:23 -05:00
break ;
2024-06-24 06:24:14 -05:00
case 0xe3 : // vibrato shape
chan [ i ] . vibratoShape = effectVal ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VIBRATO_SHAPE , i , chan [ i ] . vibratoShape ) ) ;
2021-05-18 02:29:17 -05:00
break ;
2021-05-18 03:02:47 -05:00
case 0xe4 : // vibrato fine
2025-10-18 21:42:57 -05:00
// this sets the multiplier for vibrato depth
2021-05-18 03:02:47 -05:00
chan [ i ] . vibratoFine = effectVal ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VIBRATO_RANGE , i , chan [ i ] . vibratoFine ) ) ;
2021-05-18 03:02:47 -05:00
break ;
2021-05-16 03:03:23 -05:00
case 0xe5 : // pitch
chan [ i ] . pitch = effectVal - 0x80 ;
2025-10-18 21:42:57 -05:00
// send pitch now
2021-05-19 02:05:24 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PITCH , i , chan [ i ] . pitch + ( ( ( chan [ i ] . vibratoDepth * vibTable [ chan [ i ] . vibratoPos ] * chan [ i ] . vibratoFine ) > > 4 ) / 15 ) ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PITCH , i , chan [ i ] . pitch ) ) ;
2021-05-16 03:03:23 -05:00
break ;
2025-10-18 21:42:57 -05:00
case 0xe6 : // delayed legato
2024-03-16 19:41:08 -05:00
// why does this have to follow FamiTracker verbatim
// couldn't you do better?
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . legatoDelay = ( ( ( effectVal & 0xf0 ) > > 4 ) & 7 ) + 1 ;
if ( effectVal & 128 ) {
chan [ i ] . legatoTarget = - ( effectVal & 15 ) ;
} else {
chan [ i ] . legatoTarget = ( effectVal & 15 ) ;
}
} else {
chan [ i ] . legatoDelay = - 1 ;
chan [ i ] . legatoTarget = 0 ;
}
break ;
2024-03-15 13:45:57 -05:00
case 0xe7 : // delayed macro release
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: cut/delay effect policy (delayBehavior)
// - 0: strict
// - ignore cut if equal or greater than speed
// - 1: strict old
// - ignore cut if equal or greater than speed
// - 2: lax (default)
// - no cut is ever ignored unless overridden by another
2024-03-15 13:45:57 -05:00
// "Bruh"
if ( effectVal > 0 & & ( song . delayBehavior = = 2 | | effectVal < nextSpeed ) ) {
2025-10-18 21:42:57 -05:00
// the cut timer is ticked after nextRow(), so we set it one tick higher.
2024-03-15 13:45:57 -05:00
chan [ i ] . cut = effectVal + 1 ;
chan [ i ] . cutType = 2 ;
}
break ;
2024-03-16 19:41:08 -05:00
case 0xe8 : // delayed legato up
// see? you COULD do better!
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . legatoDelay = ( ( effectVal & 0xf0 ) > > 4 ) + 1 ;
chan [ i ] . legatoTarget = ( effectVal & 15 ) ;
} else {
chan [ i ] . legatoDelay = - 1 ;
chan [ i ] . legatoTarget = 0 ;
}
break ;
case 0xe9 : // delayed legato down
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . legatoDelay = ( ( effectVal & 0xf0 ) > > 4 ) + 1 ;
chan [ i ] . legatoTarget = - ( effectVal & 15 ) ;
} else {
chan [ i ] . legatoDelay = - 1 ;
chan [ i ] . legatoTarget = 0 ;
}
break ;
2021-05-16 03:03:23 -05:00
case 0xea : // legato mode
2025-10-18 21:42:57 -05:00
// again, that's why we didn't just send note on back then.
// this effect inhibits note on.
2021-05-16 03:03:23 -05:00
chan [ i ] . legato = effectVal ;
break ;
2021-12-09 03:13:37 -05:00
case 0xeb : // sample bank
2025-10-18 21:42:57 -05:00
// this is a legacy effect for compatibility.
// in legacy sample mode (17xx), 12 samples are mapped to an octave.
// this effect allows you to use another group of 12 samples.
2021-12-09 03:13:37 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_SAMPLE_BANK , i , effectVal ) ) ;
break ;
2021-05-16 03:03:23 -05:00
case 0xec : // delayed note cut
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: cut/delay effect policy (delayBehavior)
// - 0: strict
// - ignore cut if equal or greater than speed
// - 1: strict old
// - ignore cut if equal or greater than speed
// - 2: lax (default)
// - no cut is ever ignored unless overridden by another
2022-08-22 00:01:21 -05:00
if ( effectVal > 0 & & ( song . delayBehavior = = 2 | | effectVal < nextSpeed ) ) {
2025-10-18 21:42:57 -05:00
// the cut timer is ticked after nextRow(), so we set it one tick higher.
2021-12-08 00:37:23 -05:00
chan [ i ] . cut = effectVal + 1 ;
2024-03-15 13:45:57 -05:00
chan [ i ] . cutType = 0 ;
2021-12-08 00:37:23 -05:00
}
2021-05-16 03:03:23 -05:00
break ;
2021-05-18 02:53:59 -05:00
case 0xee : // external command
2025-10-18 21:42:57 -05:00
// this does nothing in Furnace but is useful for export.
2021-12-18 04:26:17 -05:00
//printf("\x1b[1;36m%d: extern command %d\x1b[m\n",i,effectVal);
2021-12-20 19:46:49 -05:00
extValue = effectVal ;
extValuePresent = true ;
2023-07-07 11:38:44 -07:00
dispatchCmd ( DivCommand ( DIV_CMD_EXTERNAL , i , effectVal ) ) ;
2021-05-18 02:53:59 -05:00
break ;
2022-03-15 03:59:42 +07:00
case 0xf0 : // set Hz by tempo
2025-10-18 21:42:57 -05:00
// the resulting tick rate is effectVal*2/5
// 125 BPM = 50Hz; 150 BPM = 60Hz...
2022-03-15 23:30:15 -05:00
divider = ( double ) effectVal * 2.0 / 5.0 ;
2022-08-15 22:40:04 -05:00
if ( divider < 1 ) divider = 1 ;
2025-03-05 04:49:22 -05:00
cycles = got . rate / divider ;
2022-03-15 03:59:42 +07:00
clockDrift = 0 ;
2022-05-22 22:36:48 -05:00
subticks = 0 ;
2022-03-15 03:59:42 +07:00
break ;
2025-10-18 21:42:57 -05:00
case 0xf3 : // fine volume slide up
2023-02-04 16:08:20 -05:00
// tremolo and vol slides are incompatible
chan [ i ] . tremoloDepth = 0 ;
chan [ i ] . tremoloRate = 0 ;
2025-10-18 21:42:57 -05:00
// this is 64 times slower than 0Axy.
2022-03-26 20:55:43 -05:00
chan [ i ] . volSpeed = effectVal ;
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , chan [ i ] . volSpeed ) ) ;
2022-03-26 20:55:43 -05:00
break ;
2025-10-18 21:42:57 -05:00
case 0xf4 : // fine volume slide down
2023-02-04 16:08:20 -05:00
// tremolo and vol slides are incompatible
chan [ i ] . tremoloDepth = 0 ;
chan [ i ] . tremoloRate = 0 ;
2025-10-18 21:42:57 -05:00
// this is 64 times slower than 0Axy.
2022-03-26 20:55:43 -05:00
chan [ i ] . volSpeed = - effectVal ;
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , chan [ i ] . volSpeed ) ) ;
2022-03-26 20:55:43 -05:00
break ;
2022-12-17 00:09:56 -05:00
case 0xf5 : // disable macro
dispatchCmd ( DivCommand ( DIV_CMD_MACRO_OFF , i , effectVal & 0xff ) ) ;
break ;
case 0xf6 : // enable macro
dispatchCmd ( DivCommand ( DIV_CMD_MACRO_ON , i , effectVal & 0xff ) ) ;
break ;
2024-01-17 14:48:47 -05:00
case 0xf7 : // restart macro
dispatchCmd ( DivCommand ( DIV_CMD_MACRO_RESTART , i , effectVal & 0xff ) ) ;
2024-01-17 15:28:29 +03:00
break ;
2025-10-18 21:42:57 -05:00
case 0xf8 : // single volume slide up
// this will stop volume slides
2024-06-24 02:50:44 -05:00
chan [ i ] . volSpeed = 0 ; // add compat flag?
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2024-07-23 14:44:14 -05:00
chan [ i ] . volume = MIN ( chan [ i ] . volume + effectVal * 256 , chan [ i ] . volMax ) ;
2022-03-14 21:50:52 +07:00
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-03-14 21:50:52 +07:00
break ;
2025-10-18 21:42:57 -05:00
case 0xf9 : // single volume slide down
// this will stop volume slides
2024-06-24 02:50:44 -05:00
chan [ i ] . volSpeed = 0 ; // add compat flag?
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2024-07-23 14:44:14 -05:00
chan [ i ] . volume = MAX ( chan [ i ] . volume - effectVal * 256 , 0 ) ;
2022-03-14 21:50:52 +07:00
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-03-14 21:50:52 +07:00
break ;
2025-10-18 21:42:57 -05:00
case 0xfa : // fast volume slide
// this is four times the speed of 0Axy.
// effectively the value is the number of volume steps to change on each tick.
2022-03-14 21:50:52 +07:00
if ( effectVal ! = 0 ) {
if ( ( effectVal & 15 ) ! = 0 ) {
chan [ i ] . volSpeed = - ( effectVal & 15 ) * 256 ;
} else {
chan [ i ] . volSpeed = ( effectVal > > 4 ) * 256 ;
}
2023-02-04 16:08:20 -05:00
// tremolo and vol slides are incompatible
chan [ i ] . tremoloDepth = 0 ;
chan [ i ] . tremoloRate = 0 ;
2022-03-14 21:50:52 +07:00
} else {
chan [ i ] . volSpeed = 0 ;
}
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , chan [ i ] . volSpeed ) ) ;
2022-03-14 21:50:52 +07:00
break ;
2024-03-15 13:45:57 -05:00
case 0xfc : // delayed note release
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: cut/delay effect policy (delayBehavior)
// - 0: strict
// - ignore cut if equal or greater than speed
// - 1: strict old
// - ignore cut if equal or greater than speed
// - 2: lax (default)
// - no cut is ever ignored unless overridden by another
2024-03-16 11:14:45 -05:00
if ( song . delayBehavior = = 2 | | effectVal < nextSpeed ) {
2025-10-18 21:42:57 -05:00
// the cut timer is ticked after nextRow(), so we set it one tick higher.
2024-03-15 13:45:57 -05:00
chan [ i ] . cut = effectVal + 1 ;
chan [ i ] . cutType = 1 ;
}
break ;
2025-10-18 21:42:57 -05:00
2022-02-17 03:15:51 -05:00
case 0xff : // stop song
2025-10-18 21:42:57 -05:00
// this is handled in nextTick()
2022-10-22 03:46:39 -05:00
shallStopSched = true ;
logV ( " scheduling stop " ) ;
2022-02-04 14:43:57 -05:00
break ;
2021-05-16 03:03:23 -05:00
}
}
2025-10-18 21:42:57 -05:00
// commit pending effects
// sample offset (9xxx)
2024-04-23 14:36:06 -05:00
if ( sampleOffSet ) {
dispatchCmd ( DivCommand ( DIV_CMD_SAMPLE_POS , i , chan [ i ] . sampleOff ) ) ;
}
2025-10-18 21:42:57 -05:00
// panning effects
2022-04-29 23:41:14 -05:00
if ( panChanged ) {
dispatchCmd ( DivCommand ( DIV_CMD_PANNING , i , chan [ i ] . panL , chan [ i ] . panR ) ) ;
2025-04-05 04:33:46 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PANNING , i , chan [ i ] . panL , chan [ i ] . panR ) ) ;
2022-04-29 23:41:14 -05:00
}
2023-01-05 03:08:57 -05:00
if ( surroundPanChanged ) {
dispatchCmd ( DivCommand ( DIV_CMD_SURROUND_PANNING , i , 2 , chan [ i ] . panRL ) ) ;
dispatchCmd ( DivCommand ( DIV_CMD_SURROUND_PANNING , i , 3 , chan [ i ] . panRR ) ) ;
}
2022-04-29 23:41:14 -05:00
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: instrument changes triggee on portamento (inverted in the GUI)
// - before 0.6pre1 it was not possible to change instrument during portamento
// - now it is. this sends a "null" note to allow such change
2022-03-30 00:08:04 -05:00
if ( insChanged & & ( chan [ i ] . inPorta | | calledPorta ) & & song . newInsTriggersInPorta ) {
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_ON , i , DIV_NOTE_NULL ) ) ;
}
2025-10-18 21:42:57 -05:00
// commit note on
2021-05-16 03:03:23 -05:00
if ( chan [ i ] . doNote ) {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: continuous vibrato
// - when enabled, the vibrato position is not reset on each note
2022-03-03 23:14:38 -05:00
if ( ! song . continuousVibrato ) {
chan [ i ] . vibratoPos = 0 ;
}
2025-10-18 21:42:57 -05:00
// send pitch now (why? didn't we do that already?)
2021-05-19 02:05:24 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PITCH , i , chan [ i ] . pitch + ( ( ( chan [ i ] . vibratoDepth * vibTable [ chan [ i ] . vibratoPos ] * chan [ i ] . vibratoFine ) > > 4 ) / 15 ) ) ) ;
2025-10-18 21:42:57 -05:00
// handle legato
// COMPAT FLAG: broken portamento during legato
// - portamento would not occur if legato is on
// - this was fixed in 0.6pre4
2023-01-17 01:58:59 -05:00
if ( chan [ i ] . legato & & ( ! chan [ i ] . inPorta | | song . brokenPortaLegato ) ) {
2021-05-19 02:05:24 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_LEGATO , i , chan [ i ] . note ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_LEGATO , i , chan [ i ] . note ) ) ;
2021-05-16 03:03:23 -05:00
} else {
2025-10-18 21:42:57 -05:00
// this is where we actually send a note on command to the dispatch.
// this does not occur if portamento is in progress and it is not a shortcut slide (E1xy/E2xy)
2022-02-03 01:30:03 -05:00
if ( chan [ i ] . inPorta & & chan [ i ] . keyOn & & ! chan [ i ] . shorthandPorta ) {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: E1xy/E2xy stop on same note
// - if there was a shortcut slide, stop it
2022-06-29 04:57:05 -05:00
if ( song . e1e2StopOnSameNote & & chan [ i ] . wasShorthandPorta ) {
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: broken shortcut slides
// - oddly enough, shortcut slides are not communicated to the dispatch
// - this was fixed in 0.5.7
2022-06-29 04:57:05 -05:00
if ( ! song . brokenShortcutSlides ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
chan [ i ] . wasShorthandPorta = false ;
chan [ i ] . inPorta = false ;
} else {
2025-10-18 21:42:57 -05:00
// otherwise we change the portamento target
2022-06-29 04:57:05 -05:00
chan [ i ] . portaNote = chan [ i ] . note ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2022-06-29 04:57:05 -05:00
}
2022-02-08 13:31:57 -05:00
} else if ( ! chan [ i ] . noteOnInhibit ) {
2025-10-18 21:42:57 -05:00
// noteOnInhibit is set during live playback to prevent an extra note from playing
// we finally send the note on command
2022-01-19 01:27:32 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_ON , i , chan [ i ] . note , chan [ i ] . volume > > 8 ) ) ;
2023-10-03 04:38:28 -05:00
chan [ i ] . releasing = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: reset arp position on new note
// - this does exactly what it says
2023-09-10 17:41:16 -04:00
if ( song . resetArpPhaseOnNewNote ) {
chan [ i ] . arpStage = - 1 ;
}
2025-10-18 21:42:57 -05:00
// these are used by VGM/ROM export to determine the duration of loop trail.
// goneThroughNote and wentThroughNote arw set once a note plays on a channel.
// wentThroughNote is then reset on loop, and the loop trail begins.
// once all channels which had gone through a note get a note on, the loop trail duration is determined.
2023-02-08 19:25:03 -05:00
chan [ i ] . goneThroughNote = true ;
chan [ i ] . wentThroughNote = true ;
2025-10-18 21:42:57 -05:00
// this may be used by the GUI for visualizers.
2022-02-10 03:15:39 -05:00
keyHit [ i ] = true ;
2022-01-19 01:27:32 -05:00
}
2021-05-16 03:03:23 -05:00
}
2025-10-18 21:42:57 -05:00
// now that we did note, clear this flag
2021-05-16 03:03:23 -05:00
chan [ i ] . doNote = false ;
2025-10-18 21:42:57 -05:00
// keyOn is false after a keyOff so we can do this
// reset slide if scheduled and not keying on
// I don't understand
2022-01-19 01:27:32 -05:00
if ( ! chan [ i ] . keyOn & & chan [ i ] . scheduledSlideReset ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2022-01-19 01:27:32 -05:00
chan [ i ] . scheduledSlideReset = false ;
2022-01-30 15:55:31 -05:00
chan [ i ] . inPorta = false ;
2022-01-19 01:27:32 -05:00
}
2025-10-18 21:42:57 -05:00
// cap the volume if it is too high and not key on
2021-12-19 00:42:20 -05:00
if ( ! chan [ i ] . keyOn & & chan [ i ] . volume > chan [ i ] . volMax ) {
chan [ i ] . volume = chan [ i ] . volMax ;
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2021-12-19 00:42:20 -05:00
}
2025-10-18 21:42:57 -05:00
// now set key on
2021-05-19 02:22:26 -05:00
chan [ i ] . keyOn = true ;
2022-01-19 01:36:20 -05:00
chan [ i ] . keyOff = false ;
2021-05-16 03:03:23 -05:00
}
2025-10-18 21:42:57 -05:00
// reset these
2022-02-03 01:30:03 -05:00
chan [ i ] . shorthandPorta = false ;
2022-02-08 13:31:57 -05:00
chan [ i ] . noteOnInhibit = false ;
2021-05-16 03:03:23 -05:00
// post effects
2022-05-15 01:42:49 -05:00
for ( int j = 0 ; j < curPat [ i ] . effectCols ; j + + ) {
2025-10-15 21:05:13 -05:00
short effect = pat - > newData [ whatRow ] [ DIV_PAT_FX ( j ) ] ;
short effectVal = pat - > newData [ whatRow ] [ DIV_PAT_FXVAL ( j ) ] ;
2021-05-16 03:03:23 -05:00
2025-10-18 21:42:57 -05:00
// an empty effect value is treated as zero
2021-05-16 03:03:23 -05:00
if ( effectVal = = - 1 ) effectVal = 0 ;
2023-05-09 03:36:05 -05:00
effectVal & = 255 ;
2025-10-18 21:42:57 -05:00
// per-system post-effects
// if there isn't one, try with normal effects
2024-07-03 16:55:28 -05:00
if ( ! perSystemPostEffect ( i , effect , effectVal ) ) {
switch ( effect ) {
2025-10-18 21:42:57 -05:00
// these are done later to let note on happen first
case 0xf1 : // single pitch slide up
case 0xf2 : // single pitch slide down
2024-07-03 16:55:28 -05:00
if ( effect = = 0xf1 ) {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: limit slide range
2024-07-03 16:55:28 -05:00
chan [ i ] . portaNote = song . limitSlides ? 0x60 : 255 ;
} else {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: limit slide range
2024-07-03 16:55:28 -05:00
chan [ i ] . portaNote = song . limitSlides ? disCont [ dispatchOfChan [ i ] ] . dispatch - > getPortaFloor ( dispatchChanOfChan [ i ] ) : - 60 ;
}
chan [ i ] . portaSpeed = effectVal ;
chan [ i ] . portaStop = true ;
chan [ i ] . stopOnOff = false ;
chan [ i ] . scheduledSlideReset = false ;
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: arpeggio inhibits non-porta slides
2024-07-03 16:55:28 -05:00
if ( ! song . arpNonPorta ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , true , 0 ) ) ;
2025-10-22 14:00:52 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_PORTA , i , chan [ i ] . portaSpeed * ( song . linearPitch ? song . pitchSlideSpeed : 1 ) , chan [ i ] . portaNote ) ) ;
2024-07-03 16:55:28 -05:00
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: arpeggio inhibits non-porta slides
2024-07-03 16:55:28 -05:00
if ( ! song . arpNonPorta ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
break ;
}
}
2021-05-16 03:03:23 -05:00
}
}
2025-10-18 21:42:57 -05:00
// this is called by nextTick().
// this reads the next row:
// 1. update pattern console visualizer
// 2. update the metronome
// 3. call processRowPre() on all channels
// 4. call processRow() on all channels
// 5. mark row as "walked" on
// 6. advance to next row/commit pattern jumps
// 7. detect song loop
// 8. perform speed alternation
// 9. schedule cuts and pre-notes if necessary
2021-05-12 03:58:55 -05:00
void DivEngine : : nextRow ( ) {
2025-10-18 21:42:57 -05:00
// update pattern visualizer in console mode
// buffers for printing the next row
2021-05-12 03:58:55 -05:00
static char pb [ 4096 ] ;
static char pb1 [ 4096 ] ;
static char pb2 [ 4096 ] ;
static char pb3 [ 4096 ] ;
2022-07-25 17:23:56 -05:00
if ( view = = DIV_STATUS_PATTERN & & ! skipping ) {
2021-05-19 02:05:24 -05:00
strcpy ( pb1 , " " ) ;
strcpy ( pb3 , " " ) ;
for ( int i = 0 ; i < chans ; i + + ) {
2025-10-18 21:42:57 -05:00
// orders
2022-05-15 01:42:49 -05:00
snprintf ( pb , 4095 , " %.2x " , curOrders - > ord [ i ] [ curOrder ] ) ;
2021-05-19 02:05:24 -05:00
strcat ( pb1 , pb ) ;
2025-10-18 21:42:57 -05:00
// pattern data
2022-05-15 01:42:49 -05:00
DivPattern * pat = curPat [ i ] . getPattern ( curOrders - > ord [ i ] [ curOrder ] , false ) ;
2021-05-19 02:05:24 -05:00
snprintf ( pb2 , 4095 , " \x1b [37m %s " ,
2025-10-15 21:05:13 -05:00
formatNote ( pat - > newData [ curRow ] [ DIV_PAT_NOTE ] ) ) ;
2021-05-12 03:58:55 -05:00
strcat ( pb3 , pb2 ) ;
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_VOL ] = = - 1 ) {
2021-05-12 03:58:55 -05:00
strcat ( pb3 , " \x1b [m-- " ) ;
} else {
2025-10-15 21:05:13 -05:00
snprintf ( pb2 , 4095 , " \x1b [1;32m%.2x " , pat - > newData [ curRow ] [ DIV_PAT_VOL ] ) ;
2021-05-12 03:58:55 -05:00
strcat ( pb3 , pb2 ) ;
}
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_INS ] = = - 1 ) {
2021-05-12 03:58:55 -05:00
strcat ( pb3 , " \x1b [m-- " ) ;
} else {
2025-10-15 21:05:13 -05:00
snprintf ( pb2 , 4095 , " \x1b [0;36m%.2x " , pat - > newData [ curRow ] [ DIV_PAT_INS ] ) ;
2021-05-12 03:58:55 -05:00
strcat ( pb3 , pb2 ) ;
}
2022-05-15 01:42:49 -05:00
for ( int j = 0 ; j < curPat [ i ] . effectCols ; j + + ) {
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = - 1 ) {
2021-05-19 02:05:24 -05:00
strcat ( pb3 , " \x1b [m-- " ) ;
} else {
2025-10-15 21:05:13 -05:00
snprintf ( pb2 , 4095 , " \x1b [1;31m%.2x " , pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] ) ;
2021-05-19 02:05:24 -05:00
strcat ( pb3 , pb2 ) ;
}
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] = = - 1 ) {
2021-05-19 02:05:24 -05:00
strcat ( pb3 , " \x1b [m-- " ) ;
} else {
2025-10-15 21:05:13 -05:00
snprintf ( pb2 , 4095 , " \x1b [1;37m%.2x " , pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ) ;
2021-05-19 02:05:24 -05:00
strcat ( pb3 , pb2 ) ;
}
}
2021-05-12 03:58:55 -05:00
}
2025-10-18 21:42:57 -05:00
// print orders and pattern row
2021-05-19 02:05:24 -05:00
printf ( " | %.2x:%s | \x1b [1;33m%3d%s \x1b [m \n " , curOrder , pb1 , curRow , pb3 ) ;
2021-05-12 03:58:55 -05:00
}
2021-05-12 05:22:01 -05:00
2025-10-18 21:42:57 -05:00
// update and tick metronome if necessary
// elapsedBeats/Bars is used by the GUI for the clock
2022-11-09 23:52:10 -05:00
if ( curSubSong - > hilightA > 0 ) {
2022-11-10 01:26:59 -05:00
if ( ( curRow % curSubSong - > hilightA ) = = 0 ) {
pendingMetroTick = 1 ;
elapsedBeats + + ;
}
2022-11-09 23:52:10 -05:00
}
if ( curSubSong - > hilightB > 0 ) {
2022-11-10 01:26:59 -05:00
if ( ( curRow % curSubSong - > hilightB ) = = 0 ) {
pendingMetroTick = 2 ;
elapsedBars + + ;
elapsedBeats = 0 ;
}
2022-11-09 23:52:10 -05:00
}
2025-10-18 21:42:57 -05:00
// set the previous order as we'll be in the next one once done
2023-07-14 19:24:57 -05:00
if ( ! stepPlay ) {
2023-09-16 15:04:11 -05:00
playPosLock . lock ( ) ;
2023-07-14 19:24:57 -05:00
prevOrder = curOrder ;
prevRow = curRow ;
2023-09-16 15:04:11 -05:00
playPosLock . unlock ( ) ;
2023-07-14 19:24:57 -05:00
}
2022-06-06 01:05:06 -05:00
2025-10-18 21:42:57 -05:00
// process row pre on all channels
2023-08-30 02:17:16 -05:00
for ( int i = 0 ; i < chans ; i + + ) {
// try to find pre effects
processRowPre ( i ) ;
}
2025-10-18 21:42:57 -05:00
// process row on all channels
2021-05-12 05:22:01 -05:00
for ( int i = 0 ; i < chans ; i + + ) {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: cut/delay effect policy (delayBehavior)
// - if not lax, reset the row delay timer so it never happens
2022-08-22 00:20:40 -05:00
if ( song . delayBehavior ! = 2 ) {
chan [ i ] . rowDelay = 0 ;
}
2021-05-16 03:03:23 -05:00
processRow ( i , false ) ;
2021-05-12 05:22:01 -05:00
}
2021-12-05 16:11:12 -05:00
2025-10-18 21:42:57 -05:00
// mark this row as "walked" over
// this is used to determine loop position
2022-09-10 01:39:42 -05:00
walked [ ( ( curOrder < < 5 ) + ( curRow > > 3 ) ) & 8191 ] | = 1 < < ( curRow & 7 ) ;
2025-10-18 21:42:57 -05:00
// commit a pending jump if there is one
// otherwise, advance row position
2021-12-21 17:42:27 -05:00
if ( changeOrd ! = - 1 ) {
2025-10-18 21:42:57 -05:00
// disregard if repeat pattern is on
2021-12-21 17:42:27 -05:00
if ( repeatPattern ) {
curRow = 0 ;
changeOrd = - 1 ;
} else {
2025-10-18 21:42:57 -05:00
// jump to order and reset position
2021-12-21 17:42:27 -05:00
curRow = changePos ;
2022-09-10 01:39:42 -05:00
changePos = 0 ;
2025-10-18 21:42:57 -05:00
// jump to next order if it is -2
2021-12-21 17:42:27 -05:00
if ( changeOrd = = - 2 ) changeOrd = curOrder + 1 ;
2025-10-18 21:42:57 -05:00
// old loop detection routine, now commented
2022-09-10 01:39:42 -05:00
//if (changeOrd<=curOrder) endOfSong=true;
2021-12-21 17:42:27 -05:00
curOrder = changeOrd ;
2025-10-18 21:42:57 -05:00
// if we're out of bounds, return to the beginning
// if this happens we're guaranteed to loop
2022-05-15 01:42:49 -05:00
if ( curOrder > = curSubSong - > ordersLen ) {
2021-12-21 17:42:27 -05:00
curOrder = 0 ;
endOfSong = true ;
2022-09-10 01:39:42 -05:00
memset ( walked , 0 , 8192 ) ;
2021-12-21 17:42:27 -05:00
}
changeOrd = - 1 ;
2021-12-05 16:11:12 -05:00
}
2025-10-18 21:42:57 -05:00
// halt engine if requested (debug menu)
2022-02-03 18:38:57 -05:00
if ( haltOn = = DIV_HALT_PATTERN ) halted = true ;
2025-04-01 14:11:45 -05:00
} else if ( playing ) if ( + + curRow > = curSubSong - > patLen ) {
2025-10-18 21:42:57 -05:00
// if we are here it means we reached the end of this pattern, so
// advance to next order unless the song is about to stop
2025-04-01 14:11:45 -05:00
if ( shallStopSched ) {
curRow = curSubSong - > patLen - 1 ;
2023-08-25 17:51:10 -05:00
} else {
2025-04-01 14:11:45 -05:00
nextOrder ( ) ;
2023-08-25 17:51:10 -05:00
}
2025-10-18 21:42:57 -05:00
// halt engine if requested (debug menu)
2025-04-01 14:11:45 -05:00
if ( haltOn = = DIV_HALT_PATTERN ) halted = true ;
2021-12-05 16:11:12 -05:00
}
2022-09-10 01:39:42 -05:00
// new loop detection routine
2025-10-18 21:42:57 -05:00
// if we're stepping on a row we've already walked over, we found loop
// if the song is going to stop though, don't do anything
2023-08-28 16:02:29 -05:00
if ( ! endOfSong & & walked [ ( ( curOrder < < 5 ) + ( curRow > > 3 ) ) & 8191 ] & ( 1 < < ( curRow & 7 ) ) & & ! shallStopSched ) {
2022-09-10 01:39:42 -05:00
logV ( " loop reached " ) ;
endOfSong = true ;
memset ( walked , 0 , 8192 ) ;
}
2025-10-18 21:42:57 -05:00
// perform speed alternation
2025-03-11 04:15:14 -05:00
prevSpeed = nextSpeed ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: broken speed alternation
// - DefleMask uses a mandatory two-speed system
// - if the pattern length is odd, the speed to use is determined correctly...
// - ...unless the order count is also odd! in that case the first row of order 0 will always use speed 1, even if the song looped and we should be using speed 2
2022-03-23 01:35:57 -05:00
if ( song . brokenSpeedSel ) {
2023-02-05 02:56:39 -05:00
unsigned char speed2 = ( speeds . len > = 2 ) ? speeds . val [ 1 ] : speeds . val [ 0 ] ;
unsigned char speed1 = speeds . val [ 0 ] ;
2025-10-18 21:42:57 -05:00
// if the pattern length is odd and the current order is odd, use speed 2 for even rows and speed 1 for odd ones
2022-05-15 01:42:49 -05:00
if ( ( curSubSong - > patLen & 1 ) & & curOrder & 1 ) {
ticks = ( ( curRow & 1 ) ? speed2 : speed1 ) * ( curSubSong - > timeBase + 1 ) ;
2022-03-23 01:35:57 -05:00
nextSpeed = ( curRow & 1 ) ? speed1 : speed2 ;
} else {
2022-05-15 01:42:49 -05:00
ticks = ( ( curRow & 1 ) ? speed1 : speed2 ) * ( curSubSong - > timeBase + 1 ) ;
2022-03-23 01:35:57 -05:00
nextSpeed = ( curRow & 1 ) ? speed2 : speed1 ;
}
2021-12-05 16:11:12 -05:00
} else {
2025-10-18 21:42:57 -05:00
// normal speed alternation
// set the number of ticks and cycle to the next speed
2023-02-05 02:56:39 -05:00
ticks = speeds . val [ curSpeed ] * ( curSubSong - > timeBase + 1 ) ;
curSpeed + + ;
if ( curSpeed > = speeds . len ) curSpeed = 0 ;
2025-10-18 21:42:57 -05:00
// cache the next speed for future operations
2023-02-05 02:56:39 -05:00
nextSpeed = speeds . val [ curSpeed ] ;
2021-12-05 16:11:12 -05:00
}
2023-06-20 04:26:23 -05:00
/*
if ( skipping ) {
ticks = 1 ;
} */
2021-12-05 16:11:12 -05:00
// post row details
2025-10-18 21:42:57 -05:00
// schedule pre-notes and delays (for C64 and/or a compat flag)
2021-12-05 16:11:12 -05:00
for ( int i = 0 ; i < chans ; i + + ) {
2022-05-15 01:42:49 -05:00
DivPattern * pat = curPat [ i ] . getPattern ( curOrders - > ord [ i ] [ curOrder ] , false ) ;
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_NOTE ] ! = - 1 ) {
2025-10-18 21:42:57 -05:00
// if there is a note
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_NOTE ] ! = DIV_NOTE_OFF & & pat - > newData [ curRow ] [ DIV_PAT_NOTE ] ! = DIV_NOTE_REL & & pat - > newData [ curRow ] [ DIV_PAT_NOTE ] ! = DIV_MACRO_REL ) {
2025-10-18 21:42:57 -05:00
// if legato isn't on
2022-03-11 22:33:22 -05:00
if ( ! chan [ i ] . legato ) {
2025-10-18 21:42:57 -05:00
// check whether we should fire a pre-note event
2022-11-30 02:14:02 -05:00
bool wantPreNote = false ;
2022-08-03 16:21:30 -05:00
if ( disCont [ dispatchOfChan [ i ] ] . dispatch ! = NULL ) {
2022-11-30 02:14:02 -05:00
wantPreNote = disCont [ dispatchOfChan [ i ] ] . dispatch - > getWantPreNote ( ) ;
2023-04-17 19:08:14 -05:00
if ( wantPreNote ) {
2023-08-23 12:50:22 -05:00
bool doPreparePreNote = true ;
2023-04-17 19:08:14 -05:00
int addition = 0 ;
2023-08-23 12:50:22 -05:00
2025-10-18 21:42:57 -05:00
// check whether there is a portamento, legato or delay effect
// in the former two we shouldn't send pre-note
// the latter one will delay our pre-note command
2023-04-17 19:08:14 -05:00
for ( int j = 0 ; j < curPat [ i ] . effectCols ; j + + ) {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: pre-note does not take effect into consideration
// - a bug which does not cancel pre-note before a portamento or during legato
// - fixed in 0.6pre9
2023-08-23 12:50:22 -05:00
if ( ! song . preNoteNoEffect ) {
2025-10-18 21:42:57 -05:00
// handle portamento
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = 0x03 & & pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ! = 0 & & pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ! = - 1 ) {
2023-08-23 12:50:22 -05:00
doPreparePreNote = false ;
break ;
}
2025-10-18 21:42:57 -05:00
// handle vol slide + portamento
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = 0x06 & & pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ! = 0 & & pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ! = - 1 ) {
2023-08-23 12:50:22 -05:00
doPreparePreNote = false ;
break ;
}
2025-10-18 21:42:57 -05:00
// handle legato
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = 0xea ) {
if ( pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] > 0 ) {
2023-08-23 12:50:22 -05:00
doPreparePreNote = false ;
break ;
}
}
}
2025-10-18 21:42:57 -05:00
// delay pre-note if there is a delay effect
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = 0xed ) {
if ( pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] > 0 ) {
addition = pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] & 255 ;
2023-04-17 19:08:14 -05:00
break ;
}
}
}
2025-10-18 21:42:57 -05:00
// send pre-note command
2023-08-23 12:50:22 -05:00
if ( doPreparePreNote ) dispatchCmd ( DivCommand ( DIV_CMD_PRE_NOTE , i , ticks + addition ) ) ;
2023-04-17 19:08:14 -05:00
}
2022-08-03 16:21:30 -05:00
}
2022-03-11 23:01:18 -05:00
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: auto-insert one tick gap between notes
// - simulates behavior of certain Amiga/C64 sound drivers where a one-tick cut occurred before another note
2022-03-11 23:01:18 -05:00
if ( song . oneTickCut ) {
bool doPrepareCut = true ;
2023-04-17 19:08:14 -05:00
int addition = 0 ;
2022-03-11 23:01:18 -05:00
2022-05-15 01:42:49 -05:00
for ( int j = 0 ; j < curPat [ i ] . effectCols ; j + + ) {
2025-10-18 21:42:57 -05:00
// handle portamento
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = 0x03 & & pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ! = 0 & & pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ! = - 1 ) {
2022-03-11 23:01:18 -05:00
doPrepareCut = false ;
break ;
}
2025-10-18 21:42:57 -05:00
// handle vol slide + portamento
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = 0x06 & & pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ! = 0 & & pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] ! = - 1 ) {
2023-04-30 13:46:09 -05:00
doPrepareCut = false ;
break ;
}
2025-10-18 21:42:57 -05:00
// handle legato
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = 0xea ) {
if ( pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] > 0 ) {
2022-03-11 23:01:18 -05:00
doPrepareCut = false ;
break ;
}
}
2025-10-18 21:42:57 -05:00
// delay cut if there is a delay effect
2025-10-15 21:05:13 -05:00
if ( pat - > newData [ curRow ] [ DIV_PAT_FX ( j ) ] = = 0xed ) {
if ( pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] > 0 ) {
addition = pat - > newData [ curRow ] [ DIV_PAT_FXVAL ( j ) ] & 255 ;
2023-04-17 19:08:14 -05:00
break ;
}
}
2022-03-11 23:01:18 -05:00
}
2025-10-18 21:42:57 -05:00
// prepare a cut if a cut hasn't been scheduled already
// and the dispatch does not want pre-note events
2024-03-15 13:45:57 -05:00
if ( doPrepareCut & & ! wantPreNote & & chan [ i ] . cut < = 0 ) {
chan [ i ] . cut = ticks + addition ;
chan [ i ] . cutType = 0 ;
}
2022-03-11 23:01:18 -05:00
}
2022-03-11 22:33:22 -05:00
}
2021-12-05 16:45:29 -05:00
}
2021-12-05 16:11:12 -05:00
}
}
2022-02-03 18:38:57 -05:00
2025-10-18 21:42:57 -05:00
// halt engine if requested (debug menu)
2022-02-03 18:38:57 -05:00
if ( haltOn = = DIV_HALT_ROW ) halted = true ;
2025-10-18 21:42:57 -05:00
// set firstTick to indicate this is the first tick (used for a compat flag)
2022-03-23 23:56:59 -05:00
firstTick = true ;
2021-05-12 03:58:55 -05:00
}
2025-10-18 21:42:57 -05:00
// advances one tick.
// it is called by nextBuf(), playSub() nd the export functions.
// noAccum will prevent the playback time from increasing.
// if inhibitLowLat is on, low-latency mode is not taken into account. this is used by the export functions.
// returns whether the song has ended.
2022-04-15 22:27:44 -05:00
bool DivEngine : : nextTick ( bool noAccum , bool inhibitLowLat ) {
2021-12-07 04:22:36 -05:00
bool ret = false ;
2025-10-18 21:42:57 -05:00
// prevent a division by zero
2022-08-15 22:40:04 -05:00
if ( divider < 1 ) divider = 1 ;
2022-04-15 05:37:23 -05:00
2025-10-18 21:42:57 -05:00
// low-latency mode only
// set the tick multiplier so that when multiplied by the divider the product is close to 1000
2022-04-15 22:27:44 -05:00
if ( lowLatency & & ! skipping & & ! inhibitLowLat ) {
2022-04-15 05:37:23 -05:00
tickMult = 1000 / divider ;
if ( tickMult < 1 ) tickMult = 1 ;
2022-04-15 22:22:47 -05:00
} else {
tickMult = 1 ;
2022-04-15 05:37:23 -05:00
}
2022-01-08 16:03:32 -05:00
2025-10-18 21:42:57 -05:00
// set the number of samples between ticks (or sub-ticks in low-latency mode)
2025-03-05 04:49:22 -05:00
cycles = got . rate / ( divider * tickMult ) ;
clockDrift + = fmod ( got . rate , ( double ) ( divider * tickMult ) ) ;
2022-04-15 22:22:47 -05:00
if ( clockDrift > = ( divider * tickMult ) ) {
2025-10-18 21:42:57 -05:00
// correct clock since cycles is an integer
2022-04-15 22:22:47 -05:00
clockDrift - = ( divider * tickMult ) ;
2022-01-12 17:45:07 -05:00
cycles + + ;
2021-12-08 02:57:41 -05:00
}
2025-06-21 10:18:54 -05:00
// don't let user play anything during export
if ( exporting ) pendingNotes . clear ( ) ;
2025-10-18 21:42:57 -05:00
// process pending notes (live playback)
2022-08-27 00:37:32 -05:00
if ( ! pendingNotes . empty ( ) ) {
bool isOn [ DIV_MAX_CHANS ] ;
memset ( isOn , 0 , DIV_MAX_CHANS * sizeof ( bool ) ) ;
2025-10-18 21:42:57 -05:00
// this is a check that nullifies any note off event that right after a note on
// it prevents a situation where some notes do not play
2022-08-27 00:37:32 -05:00
for ( int i = pendingNotes . size ( ) - 1 ; i > = 0 ; i - - ) {
if ( pendingNotes [ i ] . channel < 0 | | pendingNotes [ i ] . channel > = chans ) continue ;
if ( pendingNotes [ i ] . on ) {
isOn [ pendingNotes [ i ] . channel ] = true ;
} else {
2025-10-18 21:42:57 -05:00
// this is a note off - check whether the channel is going up.
// if so, cancel this event.
2022-08-27 00:37:32 -05:00
if ( isOn [ pendingNotes [ i ] . channel ] ) {
2023-09-13 02:40:12 -05:00
//logV("erasing off -> on sequence in %d",pendingNotes[i].channel);
pendingNotes [ i ] . nop = true ;
2022-08-27 00:37:32 -05:00
}
}
}
2022-04-09 01:50:44 -05:00
}
2025-10-18 21:42:57 -05:00
// process pending notes, for real this time
2021-12-28 18:23:57 -05:00
while ( ! pendingNotes . empty ( ) ) {
2025-10-18 21:42:57 -05:00
// fetch event
2021-12-28 18:23:57 -05:00
DivNoteEvent & note = pendingNotes . front ( ) ;
2025-10-18 21:42:57 -05:00
// don't if channel is out of bounds or event is canceled
2023-09-13 02:40:12 -05:00
if ( note . nop | | note . channel < 0 | | note . channel > = chans ) {
2022-08-27 00:37:32 -05:00
pendingNotes . pop_front ( ) ;
2022-06-05 18:17:00 -05:00
continue ;
}
2025-10-18 21:42:57 -05:00
// process an instrument change event
2023-12-17 15:30:51 -05:00
if ( note . insChange ) {
dispatchCmd ( DivCommand ( DIV_CMD_INSTRUMENT , note . channel , note . ins , 0 ) ) ;
pendingNotes . pop_front ( ) ;
continue ;
}
2025-10-18 21:42:57 -05:00
// otherwise process a note event
2021-12-28 18:23:57 -05:00
if ( note . on ) {
2025-10-18 21:42:57 -05:00
// note on
// set the instrument except on MIDI direct mode
2023-12-17 15:30:51 -05:00
if ( ! ( midiIsDirect & & midiIsDirectProgram & & note . fromMIDI ) ) {
dispatchCmd ( DivCommand ( DIV_CMD_INSTRUMENT , note . channel , note . ins , 1 ) ) ;
}
2025-10-18 21:42:57 -05:00
// set volume as long as there's one associated with the event
// and the chip has per-channel volume
2023-12-16 19:37:14 -05:00
if ( note . volume > = 0 & & ! disCont [ dispatchOfChan [ note . channel ] ] . dispatch - > isVolGlobal ( ) ) {
2025-10-18 21:42:57 -05:00
// map velocity to curve and then to equivalent chip volume
2023-12-16 19:52:37 -05:00
float curvedVol = pow ( ( float ) note . volume / 127.0f , midiVolExp ) ;
int mappedVol = disCont [ dispatchOfChan [ note . channel ] ] . dispatch - > mapVelocity ( dispatchChanOfChan [ note . channel ] , curvedVol ) ;
2025-10-18 21:42:57 -05:00
// fire command
2023-12-13 20:44:32 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , note . channel , mappedVol ) ) ;
}
2025-10-18 21:42:57 -05:00
// send note on command and set channel state
2021-12-28 18:23:57 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_ON , note . channel , note . note ) ) ;
2022-02-10 03:15:39 -05:00
keyHit [ note . channel ] = true ;
2025-10-18 21:42:57 -05:00
chan [ note . channel ] . note = note . note ;
2023-10-03 04:38:28 -05:00
chan [ note . channel ] . releasing = false ;
2025-10-18 21:42:57 -05:00
// this prevents a duplicate note from being played while editing the pattern
2022-02-08 13:31:57 -05:00
chan [ note . channel ] . noteOnInhibit = true ;
2023-08-16 01:03:56 -05:00
chan [ note . channel ] . lastIns = note . ins ;
2021-12-28 18:23:57 -05:00
} else {
2025-10-18 21:42:57 -05:00
// note off
2022-06-05 18:17:00 -05:00
DivMacroInt * macroInt = disCont [ dispatchOfChan [ note . channel ] ] . dispatch - > getChanMacroInt ( dispatchChanOfChan [ note . channel ] ) ;
if ( macroInt ! = NULL ) {
2025-10-18 21:42:57 -05:00
// if the current instrument has a release point in any macros and
// volume is per-channel, send a note release instead of a note off
2023-08-07 18:05:31 -05:00
if ( macroInt - > hasRelease & & ! disCont [ dispatchOfChan [ note . channel ] ] . dispatch - > isVolGlobal ( ) ) {
2022-06-05 18:17:00 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_OFF_ENV , note . channel ) ) ;
} else {
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_OFF , note . channel ) ) ;
}
} else {
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_OFF , note . channel ) ) ;
}
2021-12-14 13:16:35 -05:00
}
2022-08-27 00:37:32 -05:00
pendingNotes . pop_front ( ) ;
2021-05-12 03:58:55 -05:00
}
2021-12-28 18:23:57 -05:00
2025-10-18 21:42:57 -05:00
// tick the engine state if we are not in freelance mode (engine active but not running).
// this includes ticking the sub-tick counter, processing delayed rows,
// effects and of course, playing the next row.
2021-12-28 18:23:57 -05:00
if ( ! freelance ) {
2025-10-18 21:42:57 -05:00
// decrease sub-tick counter (low-latency mode)
// run a tick once it reached zero
2022-04-15 05:37:23 -05:00
if ( - - subticks < = 0 ) {
subticks = tickMult ;
2022-09-26 01:27:36 -05:00
2024-08-24 11:25:50 -07:00
// apply delayed rows before potentially advancing to a new row, which would overwrite the
// delayed row's state before it has a chance to do anything. a typical example would be
// a delay scheduling a note-on to be simultaneous with the next row, and the next row also
2025-10-18 21:42:57 -05:00
// containing a delayed note. if we don't apply the delayed row first, the world explodes.
2024-08-24 11:25:50 -07:00
for ( int i = 0 ; i < chans ; i + + ) {
// delay effects
if ( chan [ i ] . rowDelay > 0 ) {
if ( - - chan [ i ] . rowDelay = = 0 ) {
2025-10-18 21:42:57 -05:00
// we call processRow() here for the delayed row
2024-08-24 11:25:50 -07:00
processRow ( i , true ) ;
}
}
}
2025-10-18 21:42:57 -05:00
// advance tempo accumulator (for virtual tempo) unless we are step playing and waiting for the next step (stepPlay==2)
// then advance tick counter and then call nextRow()
2022-05-18 00:05:25 -05:00
if ( stepPlay ! = 1 ) {
2025-11-18 02:51:20 -05:00
// increase accumulator by virtual tempo numerator
tempoAccum + = virtualTempoN ;
2025-10-18 21:42:57 -05:00
// while accumulator is higher than virtual tempo denominator
2024-03-15 14:56:55 -05:00
while ( tempoAccum > = virtualTempoD ) {
2025-10-18 21:42:57 -05:00
// wrap the accumulator back
2024-03-15 14:56:55 -05:00
tempoAccum - = virtualTempoD ;
2025-10-18 21:42:57 -05:00
// tick the tick counter
2022-05-18 00:05:25 -05:00
if ( - - ticks < = 0 ) {
ret = endOfSong ;
2025-10-18 21:42:57 -05:00
// get out if the song is going to stop (we'll stop at the end of this function)
2022-10-22 03:46:39 -05:00
if ( shallStopSched ) {
logV ( " acknowledging scheduled stop " ) ;
shallStop = true ;
break ;
} else if ( endOfSong ) {
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: loop modality
// - 0: reset channels. call playSub() to seek back to the loop position
// - 1: soft-reset channels. same as 0 for now
// - 2: don't reset
2022-05-18 00:05:25 -05:00
if ( song . loopModality ! = 2 ) {
playSub ( true ) ;
}
}
endOfSong = false ;
2025-10-18 21:42:57 -05:00
// check whether we were told to step to the next row
// if so, go back to waiting state (stepPlay==1) and update position
2023-07-14 19:24:57 -05:00
if ( stepPlay = = 2 ) {
stepPlay = 1 ;
2023-09-16 15:04:11 -05:00
playPosLock . lock ( ) ;
2023-07-14 19:24:57 -05:00
prevOrder = curOrder ;
prevRow = curRow ;
2023-09-16 15:04:11 -05:00
playPosLock . unlock ( ) ;
2025-10-30 01:49:02 -05:00
// also set the playback position and sync file player if necessary
2025-10-30 18:44:59 -05:00
TimeMicros rowTS = curSubSong - > ts . getTimes ( curOrder , curRow ) ;
2025-10-30 20:35:14 -05:00
if ( rowTS . seconds ! = - 1 ) {
totalTime = rowTS ;
}
2025-10-30 01:49:02 -05:00
if ( curFilePlayer & & filePlayerSync ) {
syncFilePlayer ( ) ;
}
2023-07-14 19:24:57 -05:00
}
2025-10-18 21:42:57 -05:00
// ...and now process the next row!
2022-05-18 00:05:25 -05:00
nextRow ( ) ;
break ;
2022-04-15 05:37:23 -05:00
}
2022-02-03 00:52:50 -05:00
}
2022-10-02 01:54:31 -05:00
// under no circumstances shall the accumulator become this large
if ( tempoAccum > 1023 ) tempoAccum = 1023 ;
2021-05-18 02:29:17 -05:00
}
2024-08-24 11:25:50 -07:00
2025-10-18 21:42:57 -05:00
// process stuff such as effects
2022-10-22 03:46:39 -05:00
if ( ! shallStop ) for ( int i = 0 ; i < chans ; i + + ) {
2024-07-07 18:55:22 -05:00
// retrigger
2022-04-15 05:37:23 -05:00
if ( chan [ i ] . retrigSpeed ) {
if ( - - chan [ i ] . retrigTick < 0 ) {
chan [ i ] . retrigTick = chan [ i ] . retrigSpeed - 1 ;
2025-10-18 21:42:57 -05:00
// retrigger is a null note, which allows it to be combined with a pitch slide
2022-04-15 05:37:23 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_ON , i , DIV_NOTE_NULL ) ) ;
keyHit [ i ] = true ;
}
2022-01-19 00:01:34 -05:00
}
2024-07-07 18:55:22 -05:00
// volume slides and tremolo
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: don't slide on the first tick of a row
// - Amiga/PC tracker behavior where slides and vibrato do not take course during the first tick of a row
2022-04-15 05:37:23 -05:00
if ( ! song . noSlidesOnFirstTick | | ! firstTick ) {
2025-10-18 21:42:57 -05:00
// volume slides
2022-04-15 05:37:23 -05:00
if ( chan [ i ] . volSpeed ! = 0 ) {
2025-10-18 21:42:57 -05:00
// the call to GET_VOLUME is part of a compatibility process
// where the stored volume in the dispatch may be different
// from our volume (see legacy volume slides)
2022-04-15 05:37:23 -05:00
chan [ i ] . volume = ( chan [ i ] . volume & 0xff ) | ( dispatchCmd ( DivCommand ( DIV_CMD_GET_VOLUME , i ) ) < < 8 ) ;
2024-09-06 16:27:51 -07:00
int preSpeedVol = chan [ i ] . volume ;
2022-04-15 05:37:23 -05:00
chan [ i ] . volume + = chan [ i ] . volSpeed ;
2025-10-18 21:42:57 -05:00
// handle scivolando
2024-08-24 00:59:03 -07:00
if ( chan [ i ] . volSpeedTarget ! = - 1 ) {
bool atTarget = false ;
if ( chan [ i ] . volSpeed > 0 ) {
atTarget = ( chan [ i ] . volume > = chan [ i ] . volSpeedTarget ) ;
} else if ( chan [ i ] . volSpeed < 0 ) {
atTarget = ( chan [ i ] . volume < = chan [ i ] . volSpeedTarget ) ;
} else {
atTarget = true ;
chan [ i ] . volSpeedTarget = chan [ i ] . volume ;
}
if ( atTarget ) {
2025-10-18 21:42:57 -05:00
// once we are there, stop the slide
2024-09-06 16:27:51 -07:00
if ( chan [ i ] . volSpeed > 0 ) {
chan [ i ] . volume = MAX ( preSpeedVol , chan [ i ] . volSpeedTarget ) ;
} else if ( chan [ i ] . volSpeed < 0 ) {
chan [ i ] . volume = MIN ( preSpeedVol , chan [ i ] . volSpeedTarget ) ;
}
2024-08-24 00:59:03 -07:00
chan [ i ] . volSpeed = 0 ;
chan [ i ] . volSpeedTarget = - 1 ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , 0 ) ) ;
}
}
2025-10-18 21:42:57 -05:00
// stop sliding if we reach maximum/minimum volume
// there isn't a compat flag for this yet... sorry...
2024-07-23 14:44:14 -05:00
if ( chan [ i ] . volume > chan [ i ] . volMax ) {
chan [ i ] . volume = chan [ i ] . volMax ;
2022-04-15 05:37:23 -05:00
chan [ i ] . volSpeed = 0 ;
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-04-15 05:37:23 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , 0 ) ) ;
2024-07-23 14:44:14 -05:00
} else if ( chan [ i ] . volume < 0 ) {
2022-04-15 05:37:23 -05:00
chan [ i ] . volSpeed = 0 ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOL_SLIDE , i , 0 ) ) ;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: legacy volume slides
// - sets volume to max once a vol slide down has finished (thus setting volume to volMax+1)
// - there is more to this, such as the first step of volume macro resulting in unpredictable behavior, but I don't feel like implementing THAT...
2022-04-15 05:37:23 -05:00
if ( song . legacyVolumeSlides ) {
2024-07-23 14:44:14 -05:00
chan [ i ] . volume = chan [ i ] . volMax + 1 ;
2022-04-15 05:37:23 -05:00
} else {
2024-07-23 14:44:14 -05:00
chan [ i ] . volume = 0 ;
2022-04-15 05:37:23 -05:00
}
2024-08-23 10:39:14 -07:00
chan [ i ] . volSpeedTarget = - 1 ;
2022-04-15 05:37:23 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-03-23 23:56:59 -05:00
} else {
2022-04-15 05:37:23 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
2022-03-23 23:56:59 -05:00
}
2023-02-04 16:08:20 -05:00
} else if ( chan [ i ] . tremoloDepth > 0 ) {
2025-10-18 21:42:57 -05:00
// tremolo (increase position in look-up table and send a volume change)
2023-02-04 16:08:20 -05:00
chan [ i ] . tremoloPos + = chan [ i ] . tremoloRate ;
chan [ i ] . tremoloPos & = 127 ;
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , MAX ( 0 , chan [ i ] . volume - ( tremTable [ chan [ i ] . tremoloPos ] * chan [ i ] . tremoloDepth ) ) > > 8 ) ) ;
2022-02-08 17:05:18 -05:00
}
2021-12-28 18:23:57 -05:00
}
2024-07-07 18:55:22 -05:00
// panning slides
if ( chan [ i ] . panSpeed ! = 0 ) {
int newPanL = chan [ i ] . panL ;
int newPanR = chan [ i ] . panR ;
2025-10-18 21:42:57 -05:00
// increase one side until it has reached max. then decrease the other.
2024-07-07 18:55:22 -05:00
if ( chan [ i ] . panSpeed > 0 ) { // right
if ( newPanR > = 0xff ) {
newPanL - = chan [ i ] . panSpeed ;
} else {
newPanR + = chan [ i ] . panSpeed ;
}
} else { // left
if ( newPanL > = 0xff ) {
newPanR + = chan [ i ] . panSpeed ;
} else {
newPanL - = chan [ i ] . panSpeed ;
}
}
2025-10-18 21:42:57 -05:00
// clamp to boundaries
2024-07-07 18:55:22 -05:00
if ( newPanL < 0 ) newPanL = 0 ;
if ( newPanL > 0xff ) newPanL = 0xff ;
if ( newPanR < 0 ) newPanR = 0 ;
if ( newPanR > 0xff ) newPanR = 0xff ;
2025-10-18 21:42:57 -05:00
// set new pan
2024-07-07 18:55:22 -05:00
chan [ i ] . panL = newPanL ;
chan [ i ] . panR = newPanR ;
2025-10-18 21:42:57 -05:00
// send panning command
2024-07-07 18:55:22 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PANNING , i , chan [ i ] . panL , chan [ i ] . panR ) ) ;
} else if ( chan [ i ] . panDepth > 0 ) {
2025-10-18 21:42:57 -05:00
// panbrello, similar to vibrato and tremolo
2024-07-07 18:55:22 -05:00
chan [ i ] . panPos + = chan [ i ] . panRate ;
chan [ i ] . panPos & = 255 ;
2024-08-16 23:59:20 -05:00
// calculate inverted...
2025-10-18 21:42:57 -05:00
// split position into four sections and calculate panning value
2024-07-08 03:53:42 -05:00
switch ( chan [ i ] . panPos & 0xc0 ) {
2024-07-07 18:55:22 -05:00
case 0 : // center -> right
2024-08-16 23:59:20 -05:00
chan [ i ] . panL = ( ( chan [ i ] . panPos & 0x3f ) < < 2 ) ;
chan [ i ] . panR = 0 ;
2024-07-07 18:55:22 -05:00
break ;
case 0x40 : // right -> center
2024-08-16 23:59:20 -05:00
chan [ i ] . panL = 0xff - ( ( chan [ i ] . panPos & 0x3f ) < < 2 ) ;
chan [ i ] . panR = 0 ;
2024-07-07 18:55:22 -05:00
break ;
case 0x80 : // center -> left
2024-08-16 23:59:20 -05:00
chan [ i ] . panL = 0 ;
chan [ i ] . panR = ( ( chan [ i ] . panPos & 0x3f ) < < 2 ) ;
2024-07-07 18:55:22 -05:00
break ;
case 0xc0 : // left -> center
2024-08-16 23:59:20 -05:00
chan [ i ] . panL = 0 ;
chan [ i ] . panR = 0xff - ( ( chan [ i ] . panPos & 0x3f ) < < 2 ) ;
2024-07-07 18:55:22 -05:00
break ;
}
2024-08-16 23:59:20 -05:00
// 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 ;
2024-07-07 18:55:22 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PANNING , i , chan [ i ] . panL , chan [ i ] . panR ) ) ;
}
// vibrato
2022-04-15 05:37:23 -05:00
if ( chan [ i ] . vibratoDepth > 0 ) {
chan [ i ] . vibratoPos + = chan [ i ] . vibratoRate ;
2025-10-18 21:42:57 -05:00
// clamp vibrato position
2023-05-09 03:36:05 -05:00
while ( chan [ i ] . vibratoPos > = 64 ) chan [ i ] . vibratoPos - = 64 ;
2022-06-14 04:41:31 -05:00
2025-10-18 21:42:57 -05:00
// this is for the GUI's pattern visualizer
2022-06-14 04:41:31 -05:00
chan [ i ] . vibratoPosGiant + = chan [ i ] . vibratoRate ;
2024-06-24 06:24:14 -05:00
while ( chan [ i ] . vibratoPosGiant > = 512 ) chan [ i ] . vibratoPosGiant - = 512 ;
2022-06-14 04:41:31 -05:00
2025-10-18 21:42:57 -05:00
// look-up table
2024-06-24 06:24:14 -05:00
int vibratoOut = 0 ;
switch ( chan [ i ] . vibratoShape ) {
case 1 : // sine, up only
vibratoOut = MAX ( 0 , vibTable [ chan [ i ] . vibratoPos ] ) ;
2022-04-15 05:37:23 -05:00
break ;
2024-06-24 06:24:14 -05:00
case 2 : // sine, down only
vibratoOut = MIN ( 0 , vibTable [ chan [ i ] . vibratoPos ] ) ;
2022-04-15 05:37:23 -05:00
break ;
2024-06-24 06:24:14 -05:00
case 3 : // triangle
vibratoOut = ( chan [ i ] . vibratoPos & 31 ) ;
if ( chan [ i ] . vibratoPos & 16 ) {
vibratoOut = 32 - ( chan [ i ] . vibratoPos & 31 ) ;
}
if ( chan [ i ] . vibratoPos & 32 ) {
vibratoOut = - vibratoOut ;
}
vibratoOut < < = 3 ;
break ;
case 4 : // ramp up
vibratoOut = chan [ i ] . vibratoPos < < 1 ;
break ;
case 5 : // ramp down
vibratoOut = - chan [ i ] . vibratoPos < < 1 ;
break ;
case 6 : // square
vibratoOut = ( chan [ i ] . vibratoPos > = 32 ) ? - 127 : 127 ;
break ;
case 7 : // random (TODO: use LFSR)
vibratoOut = ( rand ( ) & 255 ) - 128 ;
break ;
case 8 : // square up
vibratoOut = ( chan [ i ] . vibratoPos > = 32 ) ? 0 : 127 ;
break ;
case 9 : // square down
vibratoOut = ( chan [ i ] . vibratoPos > = 32 ) ? 0 : - 127 ;
break ;
case 10 : // half sine up
vibratoOut = vibTable [ chan [ i ] . vibratoPos > > 1 ] ;
break ;
case 11 : // half sine down
vibratoOut = vibTable [ 32 | ( chan [ i ] . vibratoPos > > 1 ) ] ;
break ;
default : // sine
vibratoOut = vibTable [ chan [ i ] . vibratoPos ] ;
2022-04-15 05:37:23 -05:00
break ;
2022-03-23 23:56:59 -05:00
}
2025-10-18 21:42:57 -05:00
// vibrato and pitch are merged into one
2024-06-24 06:24:14 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_PITCH , i , chan [ i ] . pitch + ( ( ( chan [ i ] . vibratoDepth * vibratoOut * chan [ i ] . vibratoFine ) > > 4 ) / 15 ) ) ) ;
2021-12-28 18:23:57 -05:00
}
2024-07-07 18:55:22 -05:00
// delayed legato
2024-03-16 19:41:08 -05:00
if ( chan [ i ] . legatoDelay > 0 ) {
if ( - - chan [ i ] . legatoDelay < 1 ) {
2025-10-18 21:42:57 -05:00
// change note and send legato
2024-03-16 19:41:08 -05:00
chan [ i ] . note + = chan [ i ] . legatoTarget ;
dispatchCmd ( DivCommand ( DIV_CMD_LEGATO , i , chan [ i ] . note ) ) ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_LEGATO , i , chan [ i ] . note ) ) ;
chan [ i ] . legatoDelay = - 1 ;
chan [ i ] . legatoTarget = 0 ;
}
}
2024-07-07 18:55:22 -05:00
// portamento and pitch slides
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: don't slide on the first tick of a row
// - Amiga/PC tracker behavior where slides and vibrato do not take course during the first tick of a row
2022-04-15 05:37:23 -05:00
if ( ! song . noSlidesOnFirstTick | | ! firstTick ) {
2025-10-18 21:42:57 -05:00
// portamento only runs if the channel has been used and the porta speed is higher than 0
2022-04-15 05:37:23 -05:00
if ( ( chan [ i ] . keyOn | | chan [ i ] . keyOff ) & & chan [ i ] . portaSpeed > 0 ) {
2025-10-18 21:42:57 -05:00
// send a portamento update command to the dispatch.
// it returns whether the portamento is complete and has reached the target note.
// COMPAT FLAG: pitch linearity
// - 0: none (pitch control and slides non-linear)
2025-10-22 14:00:52 -05:00
// - 1: full (pitch slides linear... we multiply the portamento speed by a user-defined multiplier)
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: reset pitch slide/portamento upon reaching target (inverted in the GUI)
// - when disabled, portamento remains active after it has finished
2025-10-22 14:00:52 -05:00
if ( dispatchCmd ( DivCommand ( DIV_CMD_NOTE_PORTA , i , chan [ i ] . portaSpeed * ( song . linearPitch ? song . pitchSlideSpeed : 1 ) , chan [ i ] . portaNote ) ) = = 2 & & chan [ i ] . portaStop & & song . targetResetsSlides ) {
2025-10-18 21:42:57 -05:00
// if we are here, it means we reached the target and shall stop
2022-04-15 05:37:23 -05:00
chan [ i ] . portaSpeed = 0 ;
2023-03-27 00:40:54 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
2022-04-15 05:37:23 -05:00
chan [ i ] . oldNote = chan [ i ] . note ;
chan [ i ] . note = chan [ i ] . portaNote ;
chan [ i ] . inPorta = false ;
2025-10-18 21:42:57 -05:00
// send legato just in case
2022-04-15 05:37:23 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_LEGATO , i , chan [ i ] . note ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_LEGATO , i , chan [ i ] . note ) ) ;
2022-01-19 22:05:39 -05:00
}
2022-04-15 05:37:23 -05:00
}
}
2024-07-07 18:55:22 -05:00
// note cut
2022-04-15 05:37:23 -05:00
if ( chan [ i ] . cut > 0 ) {
if ( - - chan [ i ] . cut < 1 ) {
2025-10-18 21:42:57 -05:00
if ( chan [ i ] . cutType = = 2 ) { // macro release
2024-03-15 13:45:57 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_ENV_RELEASE , i ) ) ;
chan [ i ] . releasing = true ;
2025-10-18 21:42:57 -05:00
} else { // note off or release
2024-03-15 13:45:57 -05:00
chan [ i ] . oldNote = chan [ i ] . note ;
//chan[i].note=-1;
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: reset slides on note off (inverted in the GUI)
// - a portamento/pitch slide will be halted upon encountering note off
// - this will not occur if the stopPortaOnNoteOff flag is on and this is a portamento
2024-03-15 13:45:57 -05:00
if ( chan [ i ] . inPorta & & song . noteOffResetsSlides ) {
chan [ i ] . keyOff = true ;
chan [ i ] . keyOn = false ;
2025-10-18 21:42:57 -05:00
// stopOnOff will be false if stopPortaOnNoteOff flag is off
2024-03-15 13:45:57 -05:00
if ( chan [ i ] . stopOnOff ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
chan [ i ] . stopOnOff = false ;
}
2025-10-18 21:42:57 -05:00
// depending on the system, portamento may still be disabled
2024-03-15 13:45:57 -05:00
if ( disCont [ dispatchOfChan [ i ] ] . dispatch - > keyOffAffectsPorta ( dispatchChanOfChan [ i ] ) ) {
chan [ i ] . portaNote = - 1 ;
chan [ i ] . portaSpeed = - 1 ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_PORTA , i , CLAMP ( chan [ i ] . portaNote , - 128 , 127 ) , MAX ( chan [ i ] . portaSpeed , 0 ) ) ) ;
}
dispatchCmd ( DivCommand ( DIV_CMD_PRE_PORTA , i , false , 0 ) ) ;
2025-10-18 21:42:57 -05:00
// another compatibility hack which schedules a second reset later just in case
2024-03-15 13:45:57 -05:00
chan [ i ] . scheduledSlideReset = true ;
2022-04-15 05:37:23 -05:00
}
2025-10-18 21:42:57 -05:00
if ( chan [ i ] . cutType = = 1 ) { // note release
2024-03-15 13:45:57 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_OFF_ENV , i ) ) ;
2025-10-18 21:42:57 -05:00
} else { // note off
2024-03-15 13:45:57 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_NOTE_OFF , i ) ) ;
2022-04-15 05:37:23 -05:00
}
2025-10-18 21:42:57 -05:00
// I am not sure why is this here and not inside the previous statement
2024-03-15 13:45:57 -05:00
chan [ i ] . releasing = true ;
2022-01-19 21:53:09 -05:00
}
}
2021-12-28 18:23:57 -05:00
}
2024-07-07 18:55:22 -05:00
2024-07-17 04:11:24 -05:00
// volume cut/mute
if ( chan [ i ] . volCut > 0 ) {
if ( - - chan [ i ] . volCut < 1 ) {
chan [ i ] . volume = 0 ;
dispatchCmd ( DivCommand ( DIV_CMD_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
dispatchCmd ( DivCommand ( DIV_CMD_HINT_VOLUME , i , chan [ i ] . volume > > 8 ) ) ;
}
}
2024-07-07 18:55:22 -05:00
// arpeggio
2022-04-15 05:37:23 -05:00
if ( chan [ i ] . resetArp ) {
2025-10-18 21:42:57 -05:00
// if we must reset arp, sent a legato with the current note
2022-04-15 05:37:23 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_LEGATO , i , chan [ i ] . note ) ) ;
2022-08-04 15:14:29 -05:00
dispatchCmd ( DivCommand ( DIV_CMD_HINT_LEGATO , i , chan [ i ] . note ) ) ;
2022-04-15 05:37:23 -05:00
chan [ i ] . resetArp = false ;
}
2025-10-18 21:42:57 -05:00
// COMPAT FLAG: reset arp position on row change
// - simulates Amiga/PC tracker behavior where the next row resets arp pos
2022-04-15 05:37:23 -05:00
if ( song . rowResetsArpPos & & firstTick ) {
chan [ i ] . arpStage = - 1 ;
}
2025-10-18 21:42:57 -05:00
// arpeggio (actually)
// don't run it if arp yield is enabled (which will be if a compat flag is on)
2022-04-15 05:37:23 -05:00
if ( chan [ i ] . arp ! = 0 & & ! chan [ i ] . arpYield & & chan [ i ] . portaSpeed < 1 ) {
if ( - - chan [ i ] . arpTicks < 1 ) {
2022-05-15 01:42:49 -05:00
chan [ i ] . arpTicks = curSubSong - > arpLen ;
2025-10-18 21:42:57 -05:00
// there are three arp stages, corresponding to note, note+x and note+y in the 00xy effect
2022-04-15 05:37:23 -05:00
chan [ i ] . arpStage + + ;
if ( chan [ i ] . arpStage > 2 ) chan [ i ] . arpStage = 0 ;
2025-10-18 21:42:57 -05:00
// arp is sent as legato
2022-04-15 05:37:23 -05:00
switch ( chan [ i ] . arpStage ) {
case 0 :
dispatchCmd ( DivCommand ( DIV_CMD_LEGATO , i , chan [ i ] . note ) ) ;
break ;
case 1 :
dispatchCmd ( DivCommand ( DIV_CMD_LEGATO , i , chan [ i ] . note + ( chan [ i ] . arp > > 4 ) ) ) ;
break ;
case 2 :
dispatchCmd ( DivCommand ( DIV_CMD_LEGATO , i , chan [ i ] . note + ( chan [ i ] . arp & 15 ) ) ) ;
break ;
}
2021-12-28 18:23:57 -05:00
}
2022-04-15 05:37:23 -05:00
} else {
2025-10-18 21:42:57 -05:00
// acknowledge arp yield
2022-04-15 05:37:23 -05:00
chan [ i ] . arpYield = false ;
2021-12-28 18:23:57 -05:00
}
2021-05-16 03:03:23 -05:00
}
}
2022-10-24 03:25:19 -05:00
} else {
2025-10-18 21:42:57 -05:00
// we are in freelance mode
2022-10-24 03:25:19 -05:00
// still tick the subtick counter
if ( - - subticks < = 0 ) {
subticks = tickMult ;
}
2021-05-14 03:23:40 -05:00
}
2025-10-18 21:42:57 -05:00
// tick the command stream player if one is attached
2023-03-26 18:48:16 -05:00
if ( subticks = = tickMult & & cmdStreamInt ) {
if ( ! cmdStreamInt - > tick ( ) ) {
2024-03-08 17:53:37 -05:00
// !!!
2023-03-26 18:48:16 -05:00
}
}
2025-10-18 21:42:57 -05:00
// this was set by nextRow()
2022-03-23 23:56:59 -05:00
firstTick = false ;
2025-10-18 21:42:57 -05:00
// acknowledge request to stop playback
2022-09-29 00:27:40 -05:00
if ( shallStop ) {
freelance = false ;
playing = false ;
extValuePresent = false ;
stepPlay = 0 ;
remainingLoops = - 1 ;
sPreview . sample = - 1 ;
sPreview . wave = - 1 ;
sPreview . pos = 0 ;
sPreview . dir = false ;
ret = true ;
2022-09-29 00:27:58 -05:00
shallStop = false ;
2022-10-22 03:46:39 -05:00
shallStopSched = false ;
2023-04-06 01:42:52 -05:00
// reset all chan oscs
for ( int i = 0 ; i < chans ; i + + ) {
DivDispatchOscBuffer * buf = disCont [ dispatchOfChan [ i ] ] . dispatch - > getOscBuffer ( dispatchChanOfChan [ i ] ) ;
if ( buf ! = NULL ) {
2025-03-01 05:05:50 -05:00
buf - > reset ( ) ;
2023-04-06 01:42:52 -05:00
}
}
2022-09-29 00:27:40 -05:00
return ret ;
}
2025-10-18 21:42:57 -05:00
// tick all chip dispatches (the argument determines whether it is a system tick or a sub-tick)
2022-04-15 15:01:11 -05:00
for ( int i = 0 ; i < song . systemLen ; i + + ) disCont [ i ] . dispatch - > tick ( subticks = = tickMult ) ;
2021-05-19 02:05:24 -05:00
2025-10-18 21:42:57 -05:00
// update playback time
2021-12-28 18:23:57 -05:00
if ( ! freelance ) {
2022-02-08 17:43:26 -05:00
if ( stepPlay ! = 1 ) {
if ( ! noAccum ) {
2024-12-09 13:56:20 -05:00
double dt = divider * tickMult ;
2022-02-08 17:43:26 -05:00
totalTicksR + + ;
2025-10-30 20:35:14 -05:00
totalTime . micros + = 1000000 / dt ;
2025-10-30 20:40:22 -05:00
totalTimeDrift + = fmod ( 1000000.0 , dt ) ;
while ( totalTimeDrift > = dt ) {
totalTimeDrift - = dt ;
2025-10-30 20:35:14 -05:00
totalTime . micros + + ;
2024-12-09 13:56:20 -05:00
}
2022-02-08 17:43:26 -05:00
}
2025-10-30 20:35:14 -05:00
if ( totalTime . micros > = 1000000 ) {
totalTime . micros - = 1000000 ;
2025-10-18 21:42:57 -05:00
// who's gonna play a song for 68 years?
2025-10-30 20:35:14 -05:00
if ( totalTime . seconds < 0x7fffffff ) totalTime . seconds + + ;
2022-02-08 17:43:26 -05:00
cmdsPerSecond = totalCmds - lastCmds ;
lastCmds = totalCmds ;
}
2021-12-28 18:23:57 -05:00
}
2022-01-12 02:45:26 -05:00
2025-10-18 21:42:57 -05:00
// print status in console mode
2025-10-30 20:35:14 -05:00
if ( consoleMode & & ! disableStatusOut & & subticks < = 1 & & ! skipping ) {
String timeFormatted = totalTime . toString ( 2 , TA_TIME_FORMAT_HMS ) ;
fprintf ( stderr , " \x1b [2K> %s %.2x/%.2x:%.3d/%.3d %4dcmd/s \x1b [G " , timeFormatted . c_str ( ) , curOrder , curSubSong - > ordersLen , curRow , curSubSong - > patLen , cmdsPerSecond ) ;
}
2021-05-19 02:05:24 -05:00
}
2021-12-07 04:22:36 -05:00
2025-10-18 21:42:57 -05:00
// halt engine if requested (debug menu)
2022-02-03 18:38:57 -05:00
if ( haltOn = = DIV_HALT_TICK ) halted = true ;
2021-12-07 04:22:36 -05:00
return ret ;
2021-05-12 03:58:55 -05:00
}
2025-10-20 06:29:45 -05:00
// returns the buffer position. used by audio export.
2023-01-03 01:09:46 -05:00
int DivEngine : : getBufferPos ( ) {
2025-03-05 04:49:22 -05:00
return bufferPos ;
2023-01-03 01:09:46 -05:00
}
2025-10-20 06:29:45 -05:00
// runs MIDI clock.
2023-05-10 03:30:05 -05:00
void DivEngine : : runMidiClock ( int totalCycles ) {
2025-10-20 06:29:45 -05:00
// not in freelance mode
2023-05-10 03:30:05 -05:00
if ( freelance ) return ;
midiClockCycles - = totalCycles ;
2025-10-20 06:29:45 -05:00
// run by the amount of cycles
2023-05-10 03:30:05 -05:00
while ( midiClockCycles < = 0 ) {
2025-10-20 06:29:45 -05:00
// send MIDI clock event
2023-05-10 03:30:05 -05:00
curMidiClock + + ;
if ( output ) if ( ! skipping & & output - > midiOut ! = NULL & & midiOutClock ) {
output - > midiOut - > send ( TAMidiMessage ( TA_MIDI_CLOCK , 0 , 0 ) ) ;
}
2025-10-20 06:29:45 -05:00
// calculate tempo using highlight, timeBase, tick rate, speeds and virtual tempo
2023-05-10 03:30:05 -05:00
double hl = curSubSong - > hilightA ;
if ( hl < = 0.0 ) hl = 4.0 ;
double timeBase = curSubSong - > timeBase + 1 ;
double speedSum = 0 ;
2024-03-15 14:56:55 -05:00
double vD = virtualTempoD ;
2023-05-10 03:30:05 -05:00
for ( int i = 0 ; i < MIN ( 16 , speeds . len ) ; i + + ) {
speedSum + = speeds . val [ i ] ;
}
speedSum / = MAX ( 1 , speeds . len ) ;
if ( timeBase < 1.0 ) timeBase = 1.0 ;
if ( speedSum < 1.0 ) speedSum = 1.0 ;
if ( vD < 1 ) vD = 1 ;
2024-03-15 14:56:55 -05:00
double bpm = ( ( 24.0 * divider ) / ( timeBase * hl * speedSum ) ) * ( double ) virtualTempoN / vD ;
2025-10-20 06:29:45 -05:00
// avoid a division by zer
2023-06-20 01:08:51 -05:00
if ( bpm < 1.0 ) bpm = 1.0 ;
2025-03-05 04:49:22 -05:00
int increment = got . rate / ( bpm ) ;
2025-10-20 06:29:45 -05:00
// increment should be at least 1
2025-03-20 17:26:01 -05:00
if ( increment < 1 ) increment = 1 ;
2023-05-10 03:30:05 -05:00
2025-10-20 06:29:45 -05:00
// drift is for precision
2023-06-20 01:08:51 -05:00
midiClockCycles + = increment ;
2025-03-05 04:49:22 -05:00
midiClockDrift + = fmod ( got . rate , ( double ) ( bpm ) ) ;
2023-05-10 03:30:05 -05:00
if ( midiClockDrift > = ( bpm ) ) {
midiClockDrift - = ( bpm ) ;
midiClockCycles + + ;
}
}
}
2025-10-20 06:29:45 -05:00
// runs MIDI timecode.
2023-05-10 03:30:05 -05:00
void DivEngine : : runMidiTime ( int totalCycles ) {
2025-10-20 06:29:45 -05:00
// not in freelance mode
2023-05-10 03:30:05 -05:00
if ( freelance ) return ;
2025-10-20 06:29:45 -05:00
// not if the rate is too low
2025-03-21 02:34:02 -05:00
if ( got . rate < 1 ) return ;
2025-10-20 06:29:45 -05:00
// run by the amount of cycles
2023-05-10 03:30:05 -05:00
midiTimeCycles - = totalCycles ;
while ( midiTimeCycles < = 0 ) {
if ( curMidiTimePiece = = 0 ) {
curMidiTimeCode = curMidiTime ;
}
if ( ! ( curMidiTimePiece & 3 ) ) curMidiTime + + ;
double frameRate = 96.0 ;
int timeRate = midiOutTimeRate ;
2025-10-20 06:29:45 -05:00
// determine the rate depending on tick rate if set to automatic
2023-05-10 03:30:05 -05:00
if ( timeRate < 1 | | timeRate > 4 ) {
if ( curSubSong - > hz > = 47.98 & & curSubSong - > hz < = 48.02 ) {
timeRate = 1 ;
} else if ( curSubSong - > hz > = 49.98 & & curSubSong - > hz < = 50.02 ) {
timeRate = 2 ;
} else if ( curSubSong - > hz > = 59.9 & & curSubSong - > hz < = 60.11 ) {
timeRate = 4 ;
} else {
timeRate = 4 ;
}
}
2025-10-20 06:29:45 -05:00
// calculate the current time
2023-05-10 03:30:05 -05:00
int hour = 0 ;
int minute = 0 ;
int second = 0 ;
int frame = 0 ;
int drop = 0 ;
int actualTime = curMidiTimeCode ;
switch ( timeRate ) {
case 1 : // 24
frameRate = 96.0 ;
hour = ( actualTime / ( 60 * 60 * 24 ) ) % 24 ;
minute = ( actualTime / ( 60 * 24 ) ) % 60 ;
second = ( actualTime / 24 ) % 60 ;
frame = actualTime % 24 ;
break ;
case 2 : // 25
frameRate = 100.0 ;
hour = ( actualTime / ( 60 * 60 * 25 ) ) % 24 ;
minute = ( actualTime / ( 60 * 25 ) ) % 60 ;
second = ( actualTime / 25 ) % 60 ;
frame = actualTime % 25 ;
break ;
case 3 : // 29.97 (NTSC drop)
frameRate = 120.0 * ( 1000.0 / 1001.0 ) ;
// drop
drop = ( ( actualTime / ( 30 * 60 ) ) - ( actualTime / ( 30 * 600 ) ) ) * 2 ;
actualTime + = drop ;
hour = ( actualTime / ( 60 * 60 * 30 ) ) % 24 ;
minute = ( actualTime / ( 60 * 30 ) ) % 60 ;
second = ( actualTime / 30 ) % 60 ;
frame = actualTime % 30 ;
break ;
case 4 : // 30 (NTSC non-drop)
default :
frameRate = 120.0 ;
hour = ( actualTime / ( 60 * 60 * 30 ) ) % 24 ;
minute = ( actualTime / ( 60 * 30 ) ) % 60 ;
second = ( actualTime / 30 ) % 60 ;
frame = actualTime % 30 ;
break ;
}
2025-10-20 06:29:45 -05:00
// output timecode
2023-05-10 03:30:05 -05:00
if ( output ) if ( ! skipping & & output - > midiOut ! = NULL & & midiOutTime ) {
unsigned char val = 0 ;
switch ( curMidiTimePiece ) {
case 0 :
val = frame & 15 ;
break ;
case 1 :
val = frame > > 4 ;
break ;
case 2 :
val = second & 15 ;
break ;
case 3 :
val = second > > 4 ;
break ;
case 4 :
val = minute & 15 ;
break ;
case 5 :
val = minute > > 4 ;
break ;
case 6 :
val = hour & 15 ;
break ;
case 7 :
val = ( hour > > 4 ) | ( ( timeRate - 1 ) < < 1 ) ;
break ;
}
val | = curMidiTimePiece < < 4 ;
output - > midiOut - > send ( TAMidiMessage ( TA_MIDI_MTC_FRAME , val , 0 ) ) ;
}
curMidiTimePiece = ( curMidiTimePiece + 1 ) & 7 ;
2025-03-05 04:49:22 -05:00
midiTimeCycles + = got . rate / ( frameRate ) ;
midiTimeDrift + = fmod ( got . rate , ( double ) ( frameRate ) ) ;
2023-05-10 03:30:05 -05:00
if ( midiTimeDrift > = ( frameRate ) ) {
midiTimeDrift - = ( frameRate ) ;
midiTimeCycles + + ;
}
}
}
2025-10-20 06:29:45 -05:00
// these two functions are either leftovers or something or they are there for test purposes.
// I don't remember very well.
2023-09-07 00:16:47 -05:00
void _runDispatch1 ( void * d ) {
}
void _runDispatch2 ( void * d ) {
}
2025-10-20 06:29:45 -05:00
// this fills the audio buffer and runs tbe engine.
// called by the audio backend and during audio export.
2021-05-12 03:58:55 -05:00
void DivEngine : : nextBuf ( float * * in , float * * out , int inChans , int outChans , unsigned int size ) {
2025-10-20 06:29:45 -05:00
// debug information
2023-07-06 18:29:29 -05:00
lastNBIns = inChans ;
lastNBOuts = outChans ;
lastNBSize = size ;
2025-10-20 06:29:45 -05:00
// don't fill a buffer if the size is 0
2023-06-24 17:45:43 -05:00
if ( ! size ) {
logW ( " nextBuf called with size 0! " ) ;
return ;
}
2022-06-06 03:05:55 -05:00
lastLoopPos = - 1 ;
2025-10-20 06:29:45 -05:00
// clear the output
2021-12-21 16:05:21 -05:00
if ( out ! = NULL ) {
2023-01-04 20:04:02 -05:00
for ( int i = 0 ; i < outChans ; i + + ) {
memset ( out [ i ] , 0 , size * sizeof ( float ) ) ;
}
2021-12-21 16:05:21 -05:00
}
2021-12-21 13:06:14 -05:00
2025-10-20 06:29:45 -05:00
// check the mutex.
// soft-locking happens when synchronizedSoft is called.
2022-03-23 21:38:28 -05:00
if ( softLocked ) {
2025-10-20 06:29:45 -05:00
// in this case we just return
2022-03-23 21:38:28 -05:00
if ( ! isBusy . try_lock ( ) ) {
2022-04-10 22:12:02 -05:00
logV ( " audio is soft-locked (%d) " , softLockCount + + ) ;
2022-03-23 21:38:28 -05:00
return ;
}
} else {
isBusy . lock ( ) ;
}
2022-02-07 21:31:58 -05:00
got . bufsize = size ;
2022-03-28 03:46:50 -05:00
2025-10-20 06:29:45 -05:00
// this is used to calculate audio load
2022-05-03 02:29:12 -05:00
std : : chrono : : steady_clock : : time_point ts_processBegin = std : : chrono : : steady_clock : : now ( ) ;
2025-10-20 06:29:45 -05:00
// set up the render thread pool
2023-09-06 04:03:53 -05:00
if ( renderPool = = NULL ) {
2023-09-07 01:16:27 -05:00
unsigned int howManyThreads = song . systemLen ;
if ( howManyThreads < 2 ) howManyThreads = 0 ;
if ( howManyThreads > renderPoolThreads ) howManyThreads = renderPoolThreads ;
renderPool = new DivWorkPool ( howManyThreads ) ;
2023-09-06 04:03:53 -05:00
}
2025-10-20 06:29:45 -05:00
// process MIDI input events
2022-03-29 00:25:28 -05:00
if ( output ) if ( output - > midiIn ) while ( ! output - > midiIn - > queue . empty ( ) ) {
2022-03-28 03:46:50 -05:00
TAMidiMessage & msg = output - > midiIn - > queue . front ( ) ;
2025-10-20 06:29:45 -05:00
// print MIDI events if MIDI debug is enabled
2024-01-25 15:29:37 -05:00
if ( midiDebug ) {
if ( msg . type = = TA_MIDI_SYSEX ) {
logD ( " MIDI debug: %.2X SysEx " , msg . type ) ;
} else {
logD ( " MIDI debug: %.2X %.2X %.2X " , msg . type , msg . data [ 0 ] , msg . data [ 1 ] ) ;
}
}
2025-10-20 06:29:45 -05:00
// call the MIDI callback, which may process this event further.
// the function should return an instrument index, which will be used
// for all forthcoming notes.
// special values:
// - -1: don't change
// - -2: "preview" instrument
// - -3: cancel event (do not add to pending notes)
2022-03-28 18:19:47 -05:00
int ins = - 1 ;
2025-06-24 03:34:05 -05:00
if ( ( ins = midiCallback ( msg ) ) ! = - 3 ) {
2025-10-20 06:29:45 -05:00
// process event if not canceled
2022-03-28 15:24:09 -05:00
int chan = msg . type & 15 ;
switch ( msg . type & 0xf0 ) {
case TA_MIDI_NOTE_OFF : {
2022-04-01 02:21:10 -05:00
if ( midiIsDirect ) {
2025-10-20 06:29:45 -05:00
// in direct mode, map the event directly to the channel
2024-01-25 19:19:55 -05:00
if ( chan < 0 | | chan > = chans ) break ;
2023-12-17 15:30:51 -05:00
pendingNotes . push_back ( DivNoteEvent ( chan , - 1 , - 1 , - 1 , false , false , true ) ) ;
2022-04-01 02:21:10 -05:00
} else {
2025-10-20 06:29:45 -05:00
// find a suitable channel and add this event to the queue
2022-04-01 02:21:10 -05:00
autoNoteOff ( msg . type & 15 , msg . data [ 0 ] - 12 , msg . data [ 1 ] ) ;
}
2025-10-20 06:29:45 -05:00
// start the engine if necessary
2022-03-28 18:19:47 -05:00
if ( ! playing ) {
reset ( ) ;
freelance = true ;
playing = true ;
}
2022-03-28 15:24:09 -05:00
break ;
}
case TA_MIDI_NOTE_ON : {
2025-10-20 06:29:45 -05:00
// trigger note off if the velocity is 0
2022-03-28 18:19:47 -05:00
if ( msg . data [ 1 ] = = 0 ) {
2022-04-01 02:21:10 -05:00
if ( midiIsDirect ) {
2025-10-20 06:29:45 -05:00
// in direct mode, map the event directly to the channel
2024-01-25 19:19:55 -05:00
if ( chan < 0 | | chan > = chans ) break ;
2023-12-17 15:30:51 -05:00
pendingNotes . push_back ( DivNoteEvent ( chan , - 1 , - 1 , - 1 , false , false , true ) ) ;
2022-04-01 02:21:10 -05:00
} else {
2025-10-20 06:29:45 -05:00
// find a suitable channel and add this event to the queue
2022-04-01 02:21:10 -05:00
autoNoteOff ( msg . type & 15 , msg . data [ 0 ] - 12 , msg . data [ 1 ] ) ;
}
2022-03-28 18:19:47 -05:00
} else {
2022-04-01 02:21:10 -05:00
if ( midiIsDirect ) {
2025-10-20 06:29:45 -05:00
// in direct mode, map the event directly to the channel
2024-01-25 19:19:55 -05:00
if ( chan < 0 | | chan > = chans ) break ;
2023-12-17 15:30:51 -05:00
pendingNotes . push_back ( DivNoteEvent ( chan , ins , msg . data [ 0 ] - 12 , msg . data [ 1 ] , true , false , true ) ) ;
2022-04-01 02:21:10 -05:00
} else {
2025-10-20 06:29:45 -05:00
// find a suitable channel and add this event to the queue
2022-04-01 02:21:10 -05:00
autoNoteOn ( msg . type & 15 , ins , msg . data [ 0 ] - 12 , msg . data [ 1 ] ) ;
}
2022-03-28 18:19:47 -05:00
}
2022-03-28 15:24:09 -05:00
break ;
}
case TA_MIDI_PROGRAM : {
2025-10-20 06:29:45 -05:00
// program changes in direct mode are handled here
// the GUI should cancel this event and change the current instrument
2023-12-17 15:30:51 -05:00
if ( midiIsDirect & & midiIsDirectProgram ) {
pendingNotes . push_back ( DivNoteEvent ( chan , msg . data [ 0 ] , 0 , 0 , false , true , true ) ) ;
}
2022-03-28 15:24:09 -05:00
break ;
}
2022-03-28 03:46:50 -05:00
}
2024-01-25 19:19:55 -05:00
} else if ( midiDebug ) {
logD ( " callback wants ignore " ) ;
2022-03-28 03:46:50 -05:00
}
2023-09-13 02:40:12 -05:00
//logD("%.2x",msg.type);
2022-03-28 03:46:50 -05:00
output - > midiIn - > queue . pop ( ) ;
}
2022-02-07 21:31:58 -05:00
2025-10-20 06:29:45 -05:00
// process sample/wave preview (not during audio export)
2025-06-21 10:18:54 -05:00
if ( ( ( sPreview . sample > = 0 & & sPreview . sample < ( int ) song . sample . size ( ) ) | | ( sPreview . wave > = 0 & & sPreview . wave < ( int ) song . wave . size ( ) ) ) & & ! exporting ) {
2025-10-20 06:29:45 -05:00
// we use blip_buf to pitch the sample
2022-01-19 23:23:47 -05:00
unsigned int samp_bbOff = 0 ;
2025-10-20 06:29:45 -05:00
// if there are samples, flush them (this can happen when the playback
// rate is less than the output rate)
2022-01-19 23:23:47 -05:00
unsigned int prevAvail = blip_samples_avail ( samp_bb ) ;
if ( prevAvail > size ) prevAvail = size ;
if ( prevAvail > 0 ) {
blip_read_samples ( samp_bb , samp_bbOut , prevAvail , 0 ) ;
samp_bbOff = prevAvail ;
}
2025-10-20 06:29:45 -05:00
// prepare to fill the buffer
2022-01-19 23:23:47 -05:00
size_t prevtotal = blip_clocks_needed ( samp_bb , size - prevAvail ) ;
2021-12-21 13:06:14 -05:00
2025-10-20 06:29:45 -05:00
// play the sample
2022-01-20 00:07:53 -05:00
if ( sPreview . sample > = 0 & & sPreview . sample < ( int ) song . sample . size ( ) ) {
DivSample * s = song . sample [ sPreview . sample ] ;
for ( size_t i = 0 ; i < prevtotal ; i + + ) {
2022-08-11 22:21:54 +09:00
if ( sPreview . pos > = ( int ) s - > samples | | ( sPreview . pEnd > = 0 & & sPreview . pos > = sPreview . pEnd ) ) {
2025-10-20 06:29:45 -05:00
// zero if out of bounds
2022-01-20 00:07:53 -05:00
samp_temp = 0 ;
} else {
2025-10-20 06:29:45 -05:00
// fetch sample
2022-04-21 01:52:37 +09:00
samp_temp = s - > data16 [ sPreview . pos ] ;
2023-04-13 18:11:10 -05:00
if ( - - sPreview . posSub < = 0 ) {
sPreview . posSub = sPreview . rateMul ;
if ( sPreview . dir ) {
sPreview . pos - - ;
} else {
sPreview . pos + + ;
}
2022-04-21 01:52:37 +09:00
}
2022-01-20 00:07:53 -05:00
}
2025-10-20 06:29:45 -05:00
// insert sample
2022-01-20 00:07:53 -05:00
blip_add_delta ( samp_bb , i , samp_temp - samp_prevSample ) ;
samp_prevSample = samp_temp ;
2022-02-03 16:52:27 -05:00
2025-10-20 06:29:45 -05:00
// check playback direction and move needle
2022-08-11 22:21:54 +09:00
if ( sPreview . dir ) { // backward
2022-08-28 10:50:57 +09:00
if ( sPreview . pos < s - > loopStart | | ( sPreview . pBegin > = 0 & & sPreview . pos < sPreview . pBegin ) ) {
if ( s - > isLoopable ( ) & & sPreview . pos < s - > loopEnd ) {
2022-08-11 22:21:54 +09:00
switch ( s - > loopMode ) {
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_FORWARD :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopStart ;
2022-08-11 22:21:54 +09:00
sPreview . dir = false ;
break ;
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_BACKWARD :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopEnd - 1 ;
2022-08-11 22:21:54 +09:00
sPreview . dir = true ;
break ;
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_PINGPONG :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopStart ;
2022-08-11 22:21:54 +09:00
sPreview . dir = false ;
break ;
default :
break ;
}
}
}
} else { // forward
2022-08-28 10:50:57 +09:00
if ( sPreview . pos > = s - > loopEnd | | ( sPreview . pEnd > = 0 & & sPreview . pos > = sPreview . pEnd ) ) {
if ( s - > isLoopable ( ) & & sPreview . pos > = s - > loopStart ) {
2022-08-11 22:21:54 +09:00
switch ( s - > loopMode ) {
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_FORWARD :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopStart ;
2022-08-11 22:21:54 +09:00
sPreview . dir = false ;
break ;
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_BACKWARD :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopEnd - 1 ;
2022-08-11 22:21:54 +09:00
sPreview . dir = true ;
break ;
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_PINGPONG :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopEnd - 1 ;
2022-08-11 22:21:54 +09:00
sPreview . dir = true ;
break ;
default :
break ;
}
}
2022-02-03 16:52:27 -05:00
}
}
2021-12-21 13:06:14 -05:00
}
2022-08-11 22:21:54 +09:00
if ( sPreview . dir ) { // backward
2022-08-28 10:50:57 +09:00
if ( sPreview . pos < = s - > loopStart | | ( sPreview . pBegin > = 0 & & sPreview . pos < = sPreview . pBegin ) ) {
if ( s - > isLoopable ( ) & & sPreview . pos > = s - > loopStart ) {
2022-04-21 01:52:37 +09:00
switch ( s - > loopMode ) {
2022-08-11 22:21:54 +09:00
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_FORWARD :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopStart ;
2022-04-21 01:52:37 +09:00
sPreview . dir = false ;
break ;
2022-08-11 22:21:54 +09:00
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_BACKWARD :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopEnd - 1 ;
2022-04-21 01:52:37 +09:00
sPreview . dir = true ;
break ;
2022-08-11 22:21:54 +09:00
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_PINGPONG :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopStart ;
2022-04-21 01:52:37 +09:00
sPreview . dir = false ;
break ;
default :
break ;
}
2022-08-11 22:21:54 +09:00
} else if ( sPreview . pos < 0 ) {
sPreview . sample = - 1 ;
2022-04-21 01:52:37 +09:00
}
2022-08-11 22:21:54 +09:00
}
} else { // forward
2022-08-28 10:50:57 +09:00
if ( sPreview . pos > = s - > loopEnd | | ( sPreview . pEnd > = 0 & & sPreview . pos > = sPreview . pEnd ) ) {
if ( s - > isLoopable ( ) & & sPreview . pos > = s - > loopStart ) {
2022-04-21 01:52:37 +09:00
switch ( s - > loopMode ) {
2022-08-11 22:21:54 +09:00
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_FORWARD :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopStart ;
2022-04-21 01:52:37 +09:00
sPreview . dir = false ;
break ;
2022-08-11 22:21:54 +09:00
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_BACKWARD :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopEnd - 1 ;
2022-04-21 01:52:37 +09:00
sPreview . dir = true ;
break ;
2022-08-11 22:21:54 +09:00
case DivSampleLoopMode : : DIV_SAMPLE_LOOP_PINGPONG :
2022-08-28 10:50:57 +09:00
sPreview . pos = s - > loopEnd - 1 ;
2022-04-21 01:52:37 +09:00
sPreview . dir = true ;
break ;
default :
break ;
}
2022-08-28 10:50:57 +09:00
} else if ( sPreview . pos > = ( int ) s - > samples ) {
2022-08-11 22:21:54 +09:00
sPreview . sample = - 1 ;
2022-02-03 16:52:27 -05:00
}
}
2021-12-21 13:06:14 -05:00
}
2022-01-20 00:07:53 -05:00
} else if ( sPreview . wave > = 0 & & sPreview . wave < ( int ) song . wave . size ( ) ) {
DivWavetable * wave = song . wave [ sPreview . wave ] ;
for ( size_t i = 0 ; i < prevtotal ; i + + ) {
if ( wave - > max < = 0 ) {
samp_temp = 0 ;
} else {
samp_temp = ( ( MIN ( wave - > data [ sPreview . pos ] , wave - > max ) < < 14 ) / wave - > max ) - 8192 ;
}
2023-04-13 18:11:10 -05:00
if ( - - sPreview . posSub < = 0 ) {
sPreview . posSub = sPreview . rateMul ;
if ( + + sPreview . pos > = wave - > len ) {
sPreview . pos = 0 ;
}
2022-01-20 00:07:53 -05:00
}
blip_add_delta ( samp_bb , i , samp_temp - samp_prevSample ) ;
samp_prevSample = samp_temp ;
}
}
2021-12-21 13:06:14 -05:00
2022-01-08 17:15:12 -05:00
blip_end_frame ( samp_bb , prevtotal ) ;
2022-01-19 23:23:47 -05:00
blip_read_samples ( samp_bb , samp_bbOut + samp_bbOff , size - samp_bbOff , 0 ) ;
2023-01-12 03:31:43 -05:00
} else {
memset ( samp_bbOut , 0 , size * sizeof ( short ) ) ;
2021-12-21 13:06:14 -05:00
}
2025-10-20 06:29:45 -05:00
// process audio (run the engine)
2023-10-12 03:54:32 -05:00
bool mustPlay = playing & & ! halted ;
if ( mustPlay ) {
2023-01-12 03:31:43 -05:00
// logic starts here
2025-10-20 06:29:45 -05:00
// first reset the run position of all dispatches
2023-01-12 03:31:43 -05:00
for ( int i = 0 ; i < song . systemLen ; i + + ) {
disCont [ i ] . runPos = 0 ;
2022-01-28 00:55:51 -05:00
}
2022-01-08 16:03:32 -05:00
2025-10-20 06:29:45 -05:00
// resize the metronome tick buffer if necessary
2023-01-12 03:31:43 -05:00
if ( metroTickLen < size ) {
if ( metroTick ! = NULL ) delete [ ] metroTick ;
metroTick = new unsigned char [ size ] ;
metroTickLen = size ;
2022-01-08 17:15:12 -05:00
}
2022-01-04 00:02:41 -05:00
2025-10-20 06:29:45 -05:00
// reset the metronome tick buffer
2023-01-12 03:31:43 -05:00
memset ( metroTick , 0 , size ) ;
2025-10-20 06:29:45 -05:00
// this variable counts how many loops we had to go through in order to fill audio buffer
// it prevents hangs under extraordinary bug situations
2023-01-12 03:31:43 -05:00
int attempts = 0 ;
2025-03-05 04:49:22 -05:00
int runLeftG = size ;
2025-10-20 06:29:45 -05:00
// run until the buffer is full or we believe the engine stalled
2023-01-12 03:31:43 -05:00
while ( + + attempts < ( int ) size ) {
// -1. set bufferPos
2025-03-05 04:49:22 -05:00
bufferPos = size - runLeftG ;
2023-01-12 03:31:43 -05:00
// 0. check if we've halted
if ( halted ) break ;
// 1. check whether we are done with all buffers
if ( runLeftG < = 0 ) break ;
// 2. check whether we gonna tick
if ( cycles < = 0 ) {
// we have to tick
if ( nextTick ( ) ) {
2023-08-03 03:54:06 -05:00
/*totalTicks=0;
totalSeconds = 0 ; */
2025-10-20 06:29:45 -05:00
// used by audio export to determine how many samples to write (otherwise it'll add silence at the end)
2025-03-05 04:49:22 -05:00
lastLoopPos = size - runLeftG ;
2023-01-12 03:31:43 -05:00
logD ( " last loop pos: %d for a size of %d and runLeftG of %d " , lastLoopPos , size , runLeftG ) ;
2025-10-30 04:07:27 -05:00
// if file player is synchronized then set its position to that of the loop row
if ( curFilePlayer & & filePlayerSync ) {
if ( curFilePlayer - > isPlaying ( ) ) {
2025-10-30 18:44:59 -05:00
TimeMicros rowTS = curSubSong - > ts . loopStartTime ;
2025-10-30 04:07:27 -05:00
2025-10-30 20:35:14 -05:00
if ( rowTS . seconds = = - 1 ) {
logW ( " that row isn't supposed to play. report this now! " ) ;
2025-10-30 04:07:27 -05:00
}
2025-10-30 20:35:14 -05:00
curFilePlayer - > setPosSeconds ( rowTS + filePlayerCue , lastLoopPos ) ;
2025-10-30 04:07:27 -05:00
}
}
// increase total loop count
2023-01-12 03:31:43 -05:00
totalLoops + + ;
2025-10-20 06:29:45 -05:00
// stop playing once we hit a specific number of loops (set during audio export)
2023-01-12 03:31:43 -05:00
if ( remainingLoops > 0 ) {
remainingLoops - - ;
if ( ! remainingLoops ) {
logI ( " end of song! " ) ;
remainingLoops = - 1 ;
playing = false ;
freelance = false ;
extValuePresent = false ;
break ;
}
2022-01-17 23:34:29 -05:00
}
2022-01-08 17:15:12 -05:00
}
2025-10-20 06:29:45 -05:00
// check whether we gotta insert a metronome tick
2023-01-12 03:31:43 -05:00
if ( pendingMetroTick ) {
2025-03-05 04:49:22 -05:00
unsigned int realPos = size - runLeftG ;
2023-01-12 03:31:43 -05:00
if ( realPos > = size ) realPos = size - 1 ;
metroTick [ realPos ] = pendingMetroTick ;
pendingMetroTick = 0 ;
2022-01-12 17:45:07 -05:00
}
2022-01-12 17:02:48 -05:00
} else {
2025-10-20 06:29:45 -05:00
// we don't have to tick yet. run chip dispatches.
2023-05-09 05:05:53 -05:00
// 3. run MIDI clock
2023-05-09 05:33:26 -05:00
int midiTotal = MIN ( cycles , runLeftG ) ;
2023-07-05 23:55:50 -05:00
runMidiClock ( midiTotal ) ;
2023-05-09 05:05:53 -05:00
2023-05-10 02:57:59 -05:00
// 4. run MIDI timecode
2023-07-05 23:55:50 -05:00
runMidiTime ( midiTotal ) ;
2023-05-10 02:57:59 -05:00
// 5. tick the clock and fill buffers as needed
2025-10-20 06:29:45 -05:00
// check which is nearest: a tick or end of audio buffer
2023-01-12 03:31:43 -05:00
if ( cycles < runLeftG ) {
2025-10-20 06:29:45 -05:00
// a tick will happen before the buffer ends
2025-03-05 04:49:22 -05:00
// run until the end of this tick
2023-01-12 03:31:43 -05:00
for ( int i = 0 ; i < song . systemLen ; i + + ) {
2023-09-07 00:16:47 -05:00
disCont [ i ] . cycles = cycles ;
disCont [ i ] . size = size ;
renderPool - > push ( [ ] ( void * d ) {
2023-09-06 04:03:53 -05:00
DivDispatchContainer * dc = ( DivDispatchContainer * ) d ;
2025-03-05 04:49:22 -05:00
int lastAvail = blip_samples_avail ( dc - > bb [ 0 ] ) ;
if ( lastAvail > 0 ) {
if ( lastAvail > = dc - > cycles ) {
dc - > flush ( dc - > runPos , dc - > cycles ) ;
dc - > runPos + = dc - > cycles ;
return ;
} else {
dc - > flush ( dc - > runPos , lastAvail ) ;
dc - > runPos + = lastAvail ;
dc - > cycles - = lastAvail ;
}
}
2025-10-20 06:29:45 -05:00
// if the buffer is too small, resize it
2025-03-05 04:49:22 -05:00
int total = blip_clocks_needed ( dc - > bb [ 0 ] , dc - > cycles ) ;
if ( total > ( int ) dc - > bbInLen ) {
logD ( " growing dispatch %p bbIn to %d " , ( void * ) dc , total + 256 ) ;
dc - > grow ( total + 256 ) ;
}
2025-03-05 18:57:17 -05:00
dc - > acquire ( total ) ;
2025-03-05 04:49:22 -05:00
dc - > fillBuf ( total , dc - > runPos , dc - > cycles ) ;
2025-10-20 06:29:45 -05:00
// advance run position
2025-03-05 04:49:22 -05:00
dc - > runPos + = dc - > cycles ;
2023-09-06 04:03:53 -05:00
} , & disCont [ i ] ) ;
2023-01-12 03:31:43 -05:00
}
2023-09-06 04:03:53 -05:00
renderPool - > wait ( ) ;
2023-01-12 03:31:43 -05:00
runLeftG - = cycles ;
cycles = 0 ;
} else {
2025-10-20 06:29:45 -05:00
// the buffer will end before a tick happens
2025-03-05 04:49:22 -05:00
// run until the end of this audio buffer
2023-01-12 03:31:43 -05:00
cycles - = runLeftG ;
for ( int i = 0 ; i < song . systemLen ; i + + ) {
2025-03-05 04:49:22 -05:00
disCont [ i ] . cycles = runLeftG ;
2023-09-06 04:03:53 -05:00
renderPool - > push ( [ ] ( void * d ) {
DivDispatchContainer * dc = ( DivDispatchContainer * ) d ;
2025-03-05 04:49:22 -05:00
int lastAvail = blip_samples_avail ( dc - > bb [ 0 ] ) ;
if ( lastAvail > 0 ) {
if ( lastAvail > = dc - > cycles ) {
dc - > flush ( dc - > runPos , dc - > cycles ) ;
dc - > runPos + = dc - > cycles ;
return ;
} else {
dc - > flush ( dc - > runPos , lastAvail ) ;
dc - > runPos + = lastAvail ;
dc - > cycles - = lastAvail ;
}
}
int total = blip_clocks_needed ( dc - > bb [ 0 ] , dc - > cycles ) ;
if ( total > ( int ) dc - > bbInLen ) {
logD ( " growing dispatch %p bbIn to %d " , ( void * ) dc , total + 256 ) ;
dc - > grow ( total + 256 ) ;
}
2025-03-05 18:57:17 -05:00
dc - > acquire ( total ) ;
2025-03-05 04:49:22 -05:00
dc - > fillBuf ( total , dc - > runPos , dc - > cycles ) ;
2023-09-06 04:03:53 -05:00
} , & disCont [ i ] ) ;
2023-01-12 03:31:43 -05:00
}
2025-10-20 06:29:45 -05:00
// at this point runLeftG will be zero and we can break out of the loop
2025-03-05 04:49:22 -05:00
runLeftG = 0 ;
2023-09-06 04:03:53 -05:00
renderPool - > wait ( ) ;
2022-01-12 17:45:07 -05:00
}
2022-01-12 17:02:48 -05:00
}
2022-01-08 17:15:12 -05:00
}
2022-02-03 18:38:57 -05:00
2025-10-20 06:29:45 -05:00
// complain and stop playback if we believe the engine has stalled
2023-01-12 03:31:43 -05:00
//logD("attempts: %d",attempts);
2023-08-31 04:46:52 -05:00
if ( attempts > = ( int ) ( size + 10 ) ) {
2025-10-30 20:35:14 -05:00
logE ( " hang detected! stopping! at %s (%d>=%d) " , totalTime . toString ( ) , attempts , ( int ) size ) ;
2023-01-12 03:31:43 -05:00
freelance = false ;
playing = false ;
extValuePresent = false ;
}
2025-10-20 06:29:45 -05:00
// this is also used by audio export to cut out unnecessary silence after a stop song effect (FFxx)
2025-03-05 04:49:22 -05:00
totalProcessed = size - runLeftG ;
2022-02-03 18:38:57 -05:00
2025-10-20 06:29:45 -05:00
// complain if a dispatch's audio buffer must be flushed and our audio buffer is too small for it
// this may happen when a chip's output rate is lower than the sample rate
2023-01-12 03:31:43 -05:00
for ( int i = 0 ; i < song . systemLen ; i + + ) {
if ( size < disCont [ i ] . lastAvail ) {
logW ( " %d: size<lastAvail! %d<%d " , i , size , disCont [ i ] . lastAvail ) ;
continue ;
}
2023-09-07 14:21:00 -05:00
disCont [ i ] . size = size ;
2025-03-05 04:49:22 -05:00
/*
2023-09-07 00:48:26 -05:00
renderPool - > push ( [ ] ( void * d ) {
DivDispatchContainer * dc = ( DivDispatchContainer * ) d ;
dc - > fillBuf ( dc - > runtotal , dc - > lastAvail , dc - > size - dc - > lastAvail ) ;
2025-03-05 04:49:22 -05:00
} , & disCont [ i ] ) ; */
2023-01-12 03:31:43 -05:00
}
2023-09-07 00:48:26 -05:00
renderPool - > wait ( ) ;
2022-01-12 02:45:26 -05:00
}
2021-05-12 03:58:55 -05:00
2025-10-27 05:15:47 -05:00
// process file player
// resize file player audio buffer if necessary
if ( filePlayerBufLen < size ) {
for ( int i = 0 ; i < DIV_MAX_OUTPUTS ; i + + ) {
if ( filePlayerBuf [ i ] ! = NULL ) delete [ ] filePlayerBuf [ i ] ;
filePlayerBuf [ i ] = new float [ size ] ;
}
filePlayerBufLen = size ;
}
2025-10-27 19:34:21 -05:00
if ( curFilePlayer ! = NULL & & ! exporting ) {
2025-10-27 05:15:47 -05:00
curFilePlayer - > mix ( filePlayerBuf , outChans , size ) ;
2025-10-27 14:24:16 -05:00
} else {
for ( int i = 0 ; i < DIV_MAX_OUTPUTS ; i + + ) {
memset ( filePlayerBuf [ i ] , 0 , size * sizeof ( float ) ) ;
}
2025-10-27 05:15:47 -05:00
}
2023-10-12 03:54:32 -05:00
// process metronome
2025-10-20 06:29:45 -05:00
// resize the metronome's audio buffer if necessary
2023-01-12 03:31:43 -05:00
if ( metroBufLen < size | | metroBuf = = NULL ) {
if ( metroBuf ! = NULL ) delete [ ] metroBuf ;
metroBuf = new float [ size ] ;
metroBufLen = size ;
2021-12-06 16:51:18 -05:00
}
2023-01-12 03:31:43 -05:00
memset ( metroBuf , 0 , metroBufLen * sizeof ( float ) ) ;
2025-10-20 06:29:45 -05:00
// insert metronome ticks
// a 1400Hz tick is used for bars (highlight 2) and a 1050Hz one for beats (highlight 1)
2024-11-10 21:28:02 -07:00
if ( mustPlay & & metronome & & ! freelance ) {
2023-01-12 03:31:43 -05:00
for ( size_t i = 0 ; i < size ; i + + ) {
if ( metroTick [ i ] ) {
if ( metroTick [ i ] = = 2 ) {
metroFreq = 1400 / got . rate ;
} else {
metroFreq = 1050 / got . rate ;
}
metroPos = 0 ;
metroAmp = 0.7f ;
2022-01-08 17:15:12 -05:00
}
2025-10-20 06:29:45 -05:00
// mix in the tick
2023-01-12 03:31:43 -05:00
if ( metroAmp > 0.0f ) {
for ( int j = 0 ; j < outChans ; j + + ) {
metroBuf [ i ] = ( sin ( metroPos * 2 * M_PI ) ) * metroAmp * metroVol ;
}
2022-01-08 17:15:12 -05:00
}
2025-10-20 06:29:45 -05:00
// decay
2023-01-12 03:31:43 -05:00
metroAmp - = 0.0003f ;
if ( metroAmp < 0.0f ) metroAmp = 0.0f ;
metroPos + = metroFreq ;
while ( metroPos > = 1 ) metroPos - - ;
2021-12-06 16:51:18 -05:00
}
2021-05-12 03:58:55 -05:00
}
2022-01-04 00:02:41 -05:00
2025-10-30 01:30:48 -05:00
// calculate volume of reference file player (so we can attenuate the rest according to the mix slider)
// -1 to 0: player volume goes from 0% to 100%
// 0 to +1: tracker volume goes from 100% to 0%
float refPlayerVol = 1.0f ;
if ( curFilePlayer ! = NULL ) {
// only if the player window is open
if ( curFilePlayer - > getActive ( ) ) {
refPlayerVol = 1.0f - curFilePlayer - > getVolume ( ) ;
2025-10-30 01:49:02 -05:00
if ( refPlayerVol < 0.0f ) refPlayerVol = 0.0f ;
2025-10-30 01:30:48 -05:00
if ( refPlayerVol > 1.0f ) refPlayerVol = 1.0f ;
}
}
2025-10-20 06:29:45 -05:00
// now mix everything (resolve patchbay)
2023-01-06 18:44:20 -05:00
for ( unsigned int i : song . patchbay ) {
2025-10-20 06:29:45 -05:00
// there are 4096 portsets. each portset may have up to 16 outputs (subports).
2023-01-06 18:44:20 -05:00
const unsigned short srcPort = i > > 16 ;
const unsigned short destPort = i & 0xffff ;
const unsigned short srcPortSet = srcPort > > 4 ;
const unsigned short destPortSet = destPort > > 4 ;
const unsigned char srcSubPort = srcPort & 15 ;
const unsigned char destSubPort = destPort & 15 ;
2025-10-20 06:29:45 -05:00
// null portset (disconnected)
2023-01-06 18:44:20 -05:00
if ( destPortSet = = 0xfff ) continue ;
2025-10-20 06:29:45 -05:00
// system outputs (the audio buffer)
2023-01-06 18:44:20 -05:00
if ( destPortSet = = 0x000 ) {
if ( destSubPort > = outChans ) continue ;
// chip outputs
2023-01-12 03:31:43 -05:00
if ( srcPortSet < song . systemLen & & playing & & ! halted ) {
2023-01-06 18:44:20 -05:00
if ( srcSubPort < disCont [ srcPortSet ] . dispatch - > getOutputCount ( ) ) {
2025-10-30 01:30:48 -05:00
float vol = song . systemVol [ srcPortSet ] * disCont [ srcPortSet ] . dispatch - > getPostAmp ( ) * song . masterVol * refPlayerVol ;
2023-01-06 18:44:20 -05:00
2025-10-20 06:29:45 -05:00
// apply volume and panning
2023-01-06 18:44:20 -05:00
switch ( destSubPort & 3 ) {
case 0 :
2023-01-07 16:39:01 -05:00
vol * = MIN ( 1.0f , 1.0f - song . systemPan [ srcPortSet ] ) * MIN ( 1.0f , 1.0f + song . systemPanFR [ srcPortSet ] ) ;
2023-01-06 18:44:20 -05:00
break ;
case 1 :
2023-01-07 16:39:01 -05:00
vol * = MIN ( 1.0f , 1.0f + song . systemPan [ srcPortSet ] ) * MIN ( 1.0f , 1.0f + song . systemPanFR [ srcPortSet ] ) ;
2023-01-06 18:44:20 -05:00
break ;
case 2 :
2023-01-07 16:39:01 -05:00
vol * = MIN ( 1.0f , 1.0f - song . systemPan [ srcPortSet ] ) * MIN ( 1.0f , 1.0f - song . systemPanFR [ srcPortSet ] ) ;
2023-01-06 18:44:20 -05:00
break ;
case 3 :
2023-01-07 16:39:01 -05:00
vol * = MIN ( 1.0f , 1.0f + song . systemPan [ srcPortSet ] ) * MIN ( 1.0f , 1.0f - song . systemPanFR [ srcPortSet ] ) ;
2023-01-06 18:44:20 -05:00
break ;
}
for ( size_t j = 0 ; j < size ; j + + ) {
out [ destSubPort ] [ j ] + = ( ( float ) disCont [ srcPortSet ] . bbOut [ srcSubPort ] [ j ] / 32768.0 ) * vol ;
}
2023-01-05 02:40:17 -05:00
}
2025-10-27 05:15:47 -05:00
} else if ( srcPortSet = = 0xffc ) {
// file player
for ( size_t j = 0 ; j < size ; j + + ) {
out [ destSubPort ] [ j ] + = filePlayerBuf [ srcSubPort ] [ j ] ;
}
2023-01-12 03:31:43 -05:00
} else if ( srcPortSet = = 0xffd ) {
// sample preview
for ( size_t j = 0 ; j < size ; j + + ) {
2023-09-11 16:04:19 -05:00
out [ destSubPort ] [ j ] + = previewVol * ( samp_bbOut [ j ] / 32768.0 ) ;
2023-01-12 03:31:43 -05:00
}
} else if ( srcPortSet = = 0xffe & & playing & & ! halted ) {
// metronome
for ( size_t j = 0 ; j < size ; j + + ) {
out [ destSubPort ] [ j ] + = metroBuf [ j ] ;
}
2022-01-04 00:02:41 -05:00
}
2023-01-06 18:44:20 -05:00
// nothing/invalid
2022-01-04 00:02:41 -05:00
}
2023-01-06 18:44:20 -05:00
// nothing/invalid
2022-01-04 00:02:41 -05:00
}
2022-01-27 17:49:00 -05:00
2025-10-20 06:29:45 -05:00
// dump to oscillator buffer (a ring buffer)
2022-04-09 02:42:58 -05:00
for ( unsigned int i = 0 ; i < size ; i + + ) {
2023-01-05 02:40:17 -05:00
for ( int j = 0 ; j < outChans ; j + + ) {
if ( oscBuf [ j ] = = NULL ) continue ;
oscBuf [ j ] [ oscWritePos ] = out [ j ] [ i ] ;
}
2022-04-09 02:42:58 -05:00
if ( + + oscWritePos > = 32768 ) oscWritePos = 0 ;
}
2022-01-27 17:49:00 -05:00
oscSize = size ;
2022-02-04 17:04:36 -05:00
2025-10-10 19:36:02 +04:00
// get per-chip peaks
float decay = 2.f * size / got . rate ;
for ( int i = 0 ; i < song . systemLen ; i + + ) {
DivDispatch * disp = disCont [ i ] . dispatch ;
2025-10-22 03:54:37 -05:00
if ( disp = = NULL ) continue ;
2025-10-10 19:36:02 +04:00
for ( int j = 0 ; j < disp - > getOutputCount ( ) ; j + + ) {
2025-10-29 02:44:45 -05:00
if ( disCont [ i ] . bbOut [ j ] = = NULL ) continue ;
2025-10-10 19:36:02 +04:00
chipPeak [ i ] [ j ] * = 1.0 - decay ;
float peak = chipPeak [ i ] [ j ] ;
for ( unsigned int k = 0 ; k < size ; k + + ) {
float out = disCont [ i ] . bbOut [ j ] [ k ] * song . systemVol [ i ] * disp - > getPostAmp ( ) / 32768.0f ; // TODO: PARSE PANNING, FRONT/REAR AND PATCHBAY
// switch (j) {
// case 0:
// out*=MIN(1.0f,1.0f-song.systemPan[i])*MIN(1.0f,1.0f+song.systemPanFR[i]);
// break;
// case 1:
// out*=MIN(1.0f,1.0f+song.systemPan[i])*MIN(1.0f,1.0f+song.systemPanFR[i]);
// break;
// case 2:
// out*=MIN(1.0f,1.0f-song.systemPan[i])*MIN(1.0f,1.0f-song.systemPanFR[i]);
// break;
// case 3:
// out*=MIN(1.0f,1.0f+song.systemPan[i])*MIN(1.0f,1.0f-song.systemPanFR[i]);
// break;
// default: break;
// }
if ( out > peak ) peak = out ;
}
chipPeak [ i ] [ j ] + = ( peak - chipPeak [ i ] [ j ] ) * 0.9 ;
}
}
2023-10-12 03:54:32 -05:00
// force mono audio (if enabled)
2023-01-05 02:40:17 -05:00
if ( forceMono & & outChans > 1 ) {
2022-02-04 17:04:36 -05:00
for ( size_t i = 0 ; i < size ; i + + ) {
2023-01-05 02:40:17 -05:00
float chanSum = out [ 0 ] [ i ] ;
for ( int j = 1 ; j < outChans ; j + + ) {
2023-06-09 14:57:14 -05:00
chanSum + = out [ j ] [ i ] ;
2023-01-05 02:40:17 -05:00
}
out [ 0 ] [ i ] = chanSum / outChans ;
for ( int j = 1 ; j < outChans ; j + + ) {
out [ j ] [ i ] = out [ 0 ] [ i ] ;
}
2022-02-04 17:04:36 -05:00
}
}
2023-10-12 03:54:32 -05:00
// clamp output (if enabled)
2022-07-25 18:41:47 -05:00
if ( clampSamples ) {
for ( size_t i = 0 ; i < size ; i + + ) {
2023-01-04 20:04:02 -05:00
for ( int j = 0 ; j < outChans ; j + + ) {
2025-10-26 02:25:24 -05:00
if ( out [ j ] [ i ] < - 0.9999 ) out [ j ] [ i ] = - 0.9999 ;
if ( out [ j ] [ i ] > 0.9999 ) out [ j ] [ i ] = 0.9999 ;
2023-01-04 20:04:02 -05:00
}
2022-07-25 18:41:47 -05:00
}
}
2021-12-15 00:37:27 -05:00
isBusy . unlock ( ) ;
2022-05-03 02:29:12 -05:00
std : : chrono : : steady_clock : : time_point ts_processEnd = std : : chrono : : steady_clock : : now ( ) ;
2025-10-20 06:29:45 -05:00
// this is shown in the GUI as audio load
2022-05-03 02:29:12 -05:00
processTime = std : : chrono : : duration_cast < std : : chrono : : nanoseconds > ( ts_processEnd - ts_processBegin ) . count ( ) ;
2021-05-12 05:22:01 -05:00
}