diff --git a/src/sound/opnmidi/Ym2612_ChipEmu.cpp b/src/sound/opnmidi/Ym2612_ChipEmu.cpp new file mode 100644 index 000000000..66219ebdc --- /dev/null +++ b/src/sound/opnmidi/Ym2612_ChipEmu.cpp @@ -0,0 +1,1319 @@ +// Game_Music_Emu 0.6.0. http://www.slack.net/~ant/ + +// Based on Gens 2.10 ym2612.c + +#include "Ym2612_ChipEmu.h" + +#include +#include +#include +#include +#include +#include + +/* Copyright (C) 2002 Stéphane Dallongeville (gens AT consolemul.com) */ +/* Copyright (C) 2004-2006 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module 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 Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +// This is mostly the original source in its C style and all. +// +// Somewhat optimized and simplified. Uses a template to generate the many +// variants of Update_Chan. Rewrote header file. In need of full rewrite by +// someone more familiar with FM sound and the YM2612. Has some inaccuracies +// compared to the Sega Genesis sound, particularly being mixed at such a +// high sample accuracy (the Genesis sounds like it has only 8 bit samples). +// - Shay + +#ifdef BLARGG_ENABLE_OPTIMIZER + #include BLARGG_ENABLE_OPTIMIZER +#endif + +const int output_bits = 14; + +struct slot_t +{ + const int *DT; // parametre detune + int MUL; // parametre "multiple de frequence" + int TL; // Total Level = volume lorsque l'enveloppe est au plus haut + int TLL; // Total Level ajusted + int SLL; // Sustin Level (ajusted) = volume oů l'enveloppe termine sa premiere phase de regression + int KSR_S; // Key Scale Rate Shift = facteur de prise en compte du KSL dans la variations de l'enveloppe + int KSR; // Key Scale Rate = cette valeur est calculee par rapport ŕ la frequence actuelle, elle va influer + // sur les differents parametres de l'enveloppe comme l'attaque, le decay ... comme dans la realite ! + int SEG; // Type enveloppe SSG + int env_xor; + int env_max; + + const int *AR; // Attack Rate (table pointeur) = Taux d'attaque (AR[KSR]) + const int *DR; // Decay Rate (table pointeur) = Taux pour la regression (DR[KSR]) + const int *SR; // Sustin Rate (table pointeur) = Taux pour le maintien (SR[KSR]) + const int *RR; // Release Rate (table pointeur) = Taux pour le rel'chement (RR[KSR]) + int Fcnt; // Frequency Count = compteur-frequence pour determiner l'amplitude actuelle (SIN[Finc >> 16]) + int Finc; // frequency step = pas d'incrementation du compteur-frequence + // plus le pas est grand, plus la frequence est aďgu (ou haute) + int Ecurp; // Envelope current phase = cette variable permet de savoir dans quelle phase + // de l'enveloppe on se trouve, par exemple phase d'attaque ou phase de maintenue ... + // en fonction de la valeur de cette variable, on va appeler une fonction permettant + // de mettre ŕ jour l'enveloppe courante. + int Ecnt; // Envelope counter = le compteur-enveloppe permet de savoir oů l'on se trouve dans l'enveloppe + int Einc; // Envelope step courant + int Ecmp; // Envelope counter limite pour la prochaine phase + int EincA; // Envelope step for Attack = pas d'incrementation du compteur durant la phase d'attaque + // cette valeur est egal ŕ AR[KSR] + int EincD; // Envelope step for Decay = pas d'incrementation du compteur durant la phase de regression + // cette valeur est egal ŕ DR[KSR] + int EincS; // Envelope step for Sustain = pas d'incrementation du compteur durant la phase de maintenue + // cette valeur est egal ŕ SR[KSR] + int EincR; // Envelope step for Release = pas d'incrementation du compteur durant la phase de rel'chement + // cette valeur est egal ŕ RR[KSR] + int *OUTp; // pointeur of SLOT output = pointeur permettant de connecter la sortie de ce slot ŕ l'entree + // d'un autre ou carrement ŕ la sortie de la voie + int INd; // input data of the slot = donnees en entree du slot + int ChgEnM; // Change envelop mask. + int AMS; // AMS depth level of this SLOT = degre de modulation de l'amplitude par le LFO + int AMSon; // AMS enable flag = drapeau d'activation de l'AMS +}; + +struct channel_t +{ + int S0_OUT[4]; // anciennes sorties slot 0 (pour le feed back) + int LEFT; // LEFT enable flag + int RIGHT; // RIGHT enable flag + int ALGO; // Algorythm = determine les connections entre les operateurs + int FB; // shift count of self feed back = degre de "Feed-Back" du SLOT 1 (il est son unique entree) + int FMS; // Frequency Modulation Sensitivity of channel = degre de modulation de la frequence sur la voie par le LFO + int AMS; // Amplitude Modulation Sensitivity of channel = degre de modulation de l'amplitude sur la voie par le LFO + int FNUM[4]; // hauteur frequence de la voie (+ 3 pour le mode special) + int FOCT[4]; // octave de la voie (+ 3 pour le mode special) + int KC[4]; // Key Code = valeur fonction de la frequence (voir KSR pour les slots, KSR = KC >> KSR_S) + slot_t SLOT[4]; // four slot.operators = les 4 slots de la voie + int FFlag; // Frequency step recalculation flag +}; + +struct state_t +{ + int TimerBase; // TimerBase calculation + int Status; // YM2612 Status (timer overflow) + int TimerA; // timerA limit = valeur jusqu'ŕ laquelle le timer A doit compter + int TimerAL; + int TimerAcnt; // timerA counter = valeur courante du Timer A + int TimerB; // timerB limit = valeur jusqu'ŕ laquelle le timer B doit compter + int TimerBL; + int TimerBcnt; // timerB counter = valeur courante du Timer B + int Mode; // Mode actuel des voie 3 et 6 (normal / special) + int DAC; // DAC enabled flag + channel_t CHANNEL[OPNMIDI_Ym2612_Emu::channel_count]; // Les 6 voies du YM2612 + int REG[2][0x100]; // Sauvegardes des valeurs de tout les registres, c'est facultatif + // cela nous rend le debuggage plus facile +}; + +#ifndef PI +#define PI 3.14159265358979323846 +#endif + +#define ATTACK 0 +#define DECAY 1 +#define SUBSTAIN 2 +#define RELEASE 3 + +// SIN_LBITS <= 16 +// LFO_HBITS <= 16 +// (SIN_LBITS + SIN_HBITS) <= 26 +// (ENV_LBITS + ENV_HBITS) <= 28 +// (LFO_LBITS + LFO_HBITS) <= 28 + +#define SIN_HBITS 12 // Sinus phase counter int part +#define SIN_LBITS (26 - SIN_HBITS) // Sinus phase counter float part (best setting) + +#if (SIN_LBITS > 16) +#define SIN_LBITS 16 // Can't be greater than 16 bits +#endif + +#define ENV_HBITS 12 // Env phase counter int part +#define ENV_LBITS (28 - ENV_HBITS) // Env phase counter float part (best setting) + +#define LFO_HBITS 10 // LFO phase counter int part +#define LFO_LBITS (28 - LFO_HBITS) // LFO phase counter float part (best setting) + +#define SIN_LENGHT (1 << SIN_HBITS) +#define ENV_LENGHT (1 << ENV_HBITS) +#define LFO_LENGHT (1 << LFO_HBITS) + +#define TL_LENGHT (ENV_LENGHT * 3) // Env + TL scaling + LFO + +#define SIN_MASK (SIN_LENGHT - 1) +#define ENV_MASK (ENV_LENGHT - 1) +#define LFO_MASK (LFO_LENGHT - 1) + +#define ENV_STEP (96.0 / ENV_LENGHT) // ENV_MAX = 96 dB + +#define ENV_ATTACK ((ENV_LENGHT * 0) << ENV_LBITS) +#define ENV_DECAY ((ENV_LENGHT * 1) << ENV_LBITS) +#define ENV_END ((ENV_LENGHT * 2) << ENV_LBITS) + +#define MAX_OUT_BITS (SIN_HBITS + SIN_LBITS + 2) // Modulation = -4 <--> +4 +#define MAX_OUT ((1 << MAX_OUT_BITS) - 1) + +#define PG_CUT_OFF ((int) (78.0 / ENV_STEP)) +#define ENV_CUT_OFF ((int) (68.0 / ENV_STEP)) + +#define AR_RATE 399128 +#define DR_RATE 5514396 + +//#define AR_RATE 426136 +//#define DR_RATE (AR_RATE * 12) + +#define LFO_FMS_LBITS 9 // FIXED (LFO_FMS_BASE gives somethink as 1) +#define LFO_FMS_BASE ((int) (0.05946309436 * 0.0338 * (double) (1 << LFO_FMS_LBITS))) + +#define S0 0 // Stupid typo of the YM2612 +#define S1 2 +#define S2 1 +#define S3 3 + +inline void set_seg( slot_t& s, int seg ) +{ + s.env_xor = 0; + s.env_max = INT_MAX; + s.SEG = seg; + if ( seg & 4 ) + { + s.env_xor = ENV_MASK; + s.env_max = ENV_MASK; + } +} + +struct tables_t +{ + short SIN_TAB [SIN_LENGHT]; // SINUS TABLE (offset into TL TABLE) + int LFOcnt; // LFO counter = compteur-frequence pour le LFO + int LFOinc; // LFO step counter = pas d'incrementation du compteur-frequence du LFO + // plus le pas est grand, plus la frequence est grande + unsigned int AR_TAB [128]; // Attack rate table + unsigned int DR_TAB [96]; // Decay rate table + unsigned int DT_TAB [8] [32]; // Detune table + unsigned int SL_TAB [16]; // Substain level table + unsigned int NULL_RATE [32]; // Table for NULL rate + int LFO_INC_TAB [8]; // LFO step table + + short ENV_TAB [2 * ENV_LENGHT + 8]; // ENV CURVE TABLE (attack & decay) + + short LFO_ENV_TAB [LFO_LENGHT]; // LFO AMS TABLE (adjusted for 11.8 dB) + short LFO_FREQ_TAB [LFO_LENGHT]; // LFO FMS TABLE + int TL_TAB [TL_LENGHT * 2]; // TOTAL LEVEL TABLE (positif and minus) + unsigned int DECAY_TO_ATTACK [ENV_LENGHT]; // Conversion from decay to attack phase + unsigned int FINC_TAB [2048]; // Frequency step table +}; + +static const unsigned char DT_DEF_TAB [4 * 32] = +{ +// FD = 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + +// FD = 1 + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, + 2, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 8, 8, 8, 8, + +// FD = 2 + 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, + 5, 6, 6, 7, 8, 8, 9, 10, 11, 12, 13, 14, 16, 16, 16, 16, + +// FD = 3 + 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, + 8 , 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 20, 22, 22, 22, 22 +}; + +static const unsigned char FKEY_TAB [16] = +{ + 0, 0, 0, 0, + 0, 0, 0, 1, + 2, 3, 3, 3, + 3, 3, 3, 3 +}; + +static const unsigned char LFO_AMS_TAB [4] = +{ + 31, 4, 1, 0 +}; + +static const unsigned char LFO_FMS_TAB [8] = +{ + LFO_FMS_BASE * 0, LFO_FMS_BASE * 1, + LFO_FMS_BASE * 2, LFO_FMS_BASE * 3, + LFO_FMS_BASE * 4, LFO_FMS_BASE * 6, + LFO_FMS_BASE * 12, LFO_FMS_BASE * 24 +}; + +inline void YM2612_Special_Update() { } + +struct OPNMIDI_Ym2612_Impl +{ + enum { channel_count = OPNMIDI_Ym2612_Emu::channel_count }; + + state_t YM2612; + int mute_mask; + tables_t g; + + void KEY_ON( channel_t&, int ); + void KEY_OFF( channel_t&, int ); + int SLOT_SET( int, int ); + int CHANNEL_SET( int, int ); + int YM_SET( int, int ); + + void set_rate( double sample_rate, double clock_factor ); + void reset(); + void write0( int addr, int data ); + void write1( int addr, int data ); + void run_timer( int ); + void run( int pair_count, OPNMIDI_Ym2612_Emu::sample_t* ); +}; + +void OPNMIDI_Ym2612_Impl::KEY_ON( channel_t& ch, int nsl) +{ + slot_t *SL = &(ch.SLOT [nsl]); // on recupere le bon pointeur de slot + + if (SL->Ecurp == RELEASE) // la touche est-elle rel'chee ? + { + SL->Fcnt = 0; + + // Fix Ecco 2 splash sound + + SL->Ecnt = (g.DECAY_TO_ATTACK [g.ENV_TAB [SL->Ecnt >> ENV_LBITS]] + ENV_ATTACK) & SL->ChgEnM; + SL->ChgEnM = ~0; + +// SL->Ecnt = g.DECAY_TO_ATTACK [g.ENV_TAB [SL->Ecnt >> ENV_LBITS]] + ENV_ATTACK; +// SL->Ecnt = 0; + + SL->Einc = SL->EincA; + SL->Ecmp = ENV_DECAY; + SL->Ecurp = ATTACK; + } +} + + +void OPNMIDI_Ym2612_Impl::KEY_OFF(channel_t& ch, int nsl) +{ + slot_t *SL = &(ch.SLOT [nsl]); // on recupere le bon pointeur de slot + + if (SL->Ecurp != RELEASE) // la touche est-elle appuyee ? + { + if (SL->Ecnt < ENV_DECAY) // attack phase ? + { + SL->Ecnt = (g.ENV_TAB [SL->Ecnt >> ENV_LBITS] << ENV_LBITS) + ENV_DECAY; + } + + SL->Einc = SL->EincR; + SL->Ecmp = ENV_END; + SL->Ecurp = RELEASE; + } +} + + +int OPNMIDI_Ym2612_Impl::SLOT_SET( int Adr, int data ) +{ + int nch = Adr & 3; + if ( nch == 3 ) + return 1; + + channel_t& ch = YM2612.CHANNEL [nch + (Adr & 0x100 ? 3 : 0)]; + slot_t& sl = ch.SLOT [(Adr >> 2) & 3]; + + switch ( Adr & 0xF0 ) + { + case 0x30: + if ( (sl.MUL = (data & 0x0F)) != 0 ) sl.MUL <<= 1; + else sl.MUL = 1; + + sl.DT = (int*) g.DT_TAB [(data >> 4) & 7]; + + ch.SLOT [0].Finc = -1; + + break; + + case 0x40: + sl.TL = data & 0x7F; + + // SOR2 do a lot of TL adjustement and this fix R.Shinobi jump sound... + YM2612_Special_Update(); + +#if ((ENV_HBITS - 7) < 0) + sl.TLL = sl.TL >> (7 - ENV_HBITS); +#else + sl.TLL = sl.TL << (ENV_HBITS - 7); +#endif + + break; + + case 0x50: + sl.KSR_S = 3 - (data >> 6); + + ch.SLOT [0].Finc = -1; + + if (data &= 0x1F) sl.AR = (int*) &g.AR_TAB [data << 1]; + else sl.AR = (int*) &g.NULL_RATE [0]; + + sl.EincA = sl.AR [sl.KSR]; + if (sl.Ecurp == ATTACK) sl.Einc = sl.EincA; + break; + + case 0x60: + if ( (sl.AMSon = (data & 0x80)) != 0 ) sl.AMS = ch.AMS; + else sl.AMS = 31; + + if (data &= 0x1F) sl.DR = (int*) &g.DR_TAB [data << 1]; + else sl.DR = (int*) &g.NULL_RATE [0]; + + sl.EincD = sl.DR [sl.KSR]; + if (sl.Ecurp == DECAY) sl.Einc = sl.EincD; + break; + + case 0x70: + if (data &= 0x1F) sl.SR = (int*) &g.DR_TAB [data << 1]; + else sl.SR = (int*) &g.NULL_RATE [0]; + + sl.EincS = sl.SR [sl.KSR]; + if ((sl.Ecurp == SUBSTAIN) && (sl.Ecnt < ENV_END)) sl.Einc = sl.EincS; + break; + + case 0x80: + sl.SLL = g.SL_TAB [data >> 4]; + + sl.RR = (int*) &g.DR_TAB [((data & 0xF) << 2) + 2]; + + sl.EincR = sl.RR [sl.KSR]; + if ((sl.Ecurp == RELEASE) && (sl.Ecnt < ENV_END)) sl.Einc = sl.EincR; + break; + + case 0x90: + // SSG-EG envelope shapes : + /* + E At Al H + + 1 0 0 0 \\\\ + 1 0 0 1 \___ + 1 0 1 0 \/\/ + 1 0 1 1 \ + 1 1 0 0 //// + 1 1 0 1 / + 1 1 1 0 /\/\ + 1 1 1 1 /___ + + E = SSG-EG enable + At = Start negate + Al = Altern + H = Hold */ + + set_seg( sl, (data & 8) ? (data & 0x0F) : 0 ); + break; + } + + return 0; +} + + +int OPNMIDI_Ym2612_Impl::CHANNEL_SET( int Adr, int data ) +{ + int num = Adr & 3; + if ( num == 3 ) + return 1; + + channel_t& ch = YM2612.CHANNEL [num + (Adr & 0x100 ? 3 : 0)]; + + switch ( Adr & 0xFC ) + { + case 0xA0: + YM2612_Special_Update(); + + ch.FNUM [0] = (ch.FNUM [0] & 0x700) + data; + ch.KC [0] = (ch.FOCT [0] << 2) | FKEY_TAB [ch.FNUM [0] >> 7]; + + ch.SLOT [0].Finc = -1; + break; + + case 0xA4: + YM2612_Special_Update(); + + ch.FNUM [0] = (ch.FNUM [0] & 0x0FF) + ((data & 0x07) << 8); + ch.FOCT [0] = (data & 0x38) >> 3; + ch.KC [0] = (ch.FOCT [0] << 2) | FKEY_TAB [ch.FNUM [0] >> 7]; + + ch.SLOT [0].Finc = -1; + break; + + case 0xA8: + if ( Adr < 0x100 ) + { + num++; + + YM2612_Special_Update(); + + YM2612.CHANNEL [2].FNUM [num] = (YM2612.CHANNEL [2].FNUM [num] & 0x700) + data; + YM2612.CHANNEL [2].KC [num] = (YM2612.CHANNEL [2].FOCT [num] << 2) | + FKEY_TAB [YM2612.CHANNEL [2].FNUM [num] >> 7]; + + YM2612.CHANNEL [2].SLOT [0].Finc = -1; + } + break; + + case 0xAC: + if ( Adr < 0x100 ) + { + num++; + + YM2612_Special_Update(); + + YM2612.CHANNEL [2].FNUM [num] = (YM2612.CHANNEL [2].FNUM [num] & 0x0FF) + ((data & 0x07) << 8); + YM2612.CHANNEL [2].FOCT [num] = (data & 0x38) >> 3; + YM2612.CHANNEL [2].KC [num] = (YM2612.CHANNEL [2].FOCT [num] << 2) | + FKEY_TAB [YM2612.CHANNEL [2].FNUM [num] >> 7]; + + YM2612.CHANNEL [2].SLOT [0].Finc = -1; + } + break; + + case 0xB0: + if ( ch.ALGO != (data & 7) ) + { + // Fix VectorMan 2 heli sound (level 1) + YM2612_Special_Update(); + + ch.ALGO = data & 7; + + ch.SLOT [0].ChgEnM = 0; + ch.SLOT [1].ChgEnM = 0; + ch.SLOT [2].ChgEnM = 0; + ch.SLOT [3].ChgEnM = 0; + } + + ch.FB = 9 - ((data >> 3) & 7); // Real thing ? + +// if (ch.FB = ((data >> 3) & 7)) ch.FB = 9 - ch.FB; // Thunder force 4 (music stage 8), Gynoug, Aladdin bug sound... +// else ch.FB = 31; + break; + + case 0xB4: { + YM2612_Special_Update(); + + ch.LEFT = 0 - ((data >> 7) & 1); + ch.RIGHT = 0 - ((data >> 6) & 1); + + ch.AMS = LFO_AMS_TAB [(data >> 4) & 3]; + ch.FMS = LFO_FMS_TAB [data & 7]; + + for ( int i = 0; i < 4; i++ ) + { + slot_t& sl = ch.SLOT [i]; + sl.AMS = (sl.AMSon ? ch.AMS : 31); + } + break; + } + } + + return 0; +} + + +int OPNMIDI_Ym2612_Impl::YM_SET(int Adr, int data) +{ + switch ( Adr ) + { + case 0x22: + if (data & 8) // LFO enable + { + // Cool Spot music 1, LFO modified severals time which + // distord the sound, have to check that on a real genesis... + + g.LFOinc = g.LFO_INC_TAB [data & 7]; + } + else + { + g.LFOinc = g.LFOcnt = 0; + } + break; + + case 0x24: + YM2612.TimerA = (YM2612.TimerA & 0x003) | (((int) data) << 2); + + if (YM2612.TimerAL != (1024 - YM2612.TimerA) << 12) + { + YM2612.TimerAcnt = YM2612.TimerAL = (1024 - YM2612.TimerA) << 12; + } + break; + + case 0x25: + YM2612.TimerA = (YM2612.TimerA & 0x3FC) | (data & 3); + + if (YM2612.TimerAL != (1024 - YM2612.TimerA) << 12) + { + YM2612.TimerAcnt = YM2612.TimerAL = (1024 - YM2612.TimerA) << 12; + } + break; + + case 0x26: + YM2612.TimerB = data; + + if (YM2612.TimerBL != (256 - YM2612.TimerB) << (4 + 12)) + { + YM2612.TimerBcnt = YM2612.TimerBL = (256 - YM2612.TimerB) << (4 + 12); + } + break; + + case 0x27: + // Parametre divers + // b7 = CSM MODE + // b6 = 3 slot mode + // b5 = reset b + // b4 = reset a + // b3 = timer enable b + // b2 = timer enable a + // b1 = load b + // b0 = load a + + if ((data ^ YM2612.Mode) & 0x40) + { + // We changed the channel 2 mode, so recalculate phase step + // This fix the punch sound in Street of Rage 2 + + YM2612_Special_Update(); + + YM2612.CHANNEL [2].SLOT [0].Finc = -1; // recalculate phase step + } + +// if ((data & 2) && (YM2612.Status & 2)) YM2612.TimerBcnt = YM2612.TimerBL; +// if ((data & 1) && (YM2612.Status & 1)) YM2612.TimerAcnt = YM2612.TimerAL; + +// YM2612.Status &= (~data >> 4); // Reset du Status au cas ou c'est demande + YM2612.Status &= (~data >> 4) & (data >> 2); // Reset Status + + YM2612.Mode = data; + break; + + case 0x28: { + int nch = data & 3; + if ( nch == 3 ) + return 1; + if ( data & 4 ) + nch += 3; + channel_t& ch = YM2612.CHANNEL [nch]; + + YM2612_Special_Update(); + + if (data & 0x10) KEY_ON(ch, S0); // On appuie sur la touche pour le slot 1 + else KEY_OFF(ch, S0); // On rel'che la touche pour le slot 1 + if (data & 0x20) KEY_ON(ch, S1); // On appuie sur la touche pour le slot 3 + else KEY_OFF(ch, S1); // On rel'che la touche pour le slot 3 + if (data & 0x40) KEY_ON(ch, S2); // On appuie sur la touche pour le slot 2 + else KEY_OFF(ch, S2); // On rel'che la touche pour le slot 2 + if (data & 0x80) KEY_ON(ch, S3); // On appuie sur la touche pour le slot 4 + else KEY_OFF(ch, S3); // On rel'che la touche pour le slot 4 + break; + } + + case 0x2B: + if (YM2612.DAC ^ (data & 0x80)) YM2612_Special_Update(); + + YM2612.DAC = data & 0x80; // activation/desactivation du DAC + break; + } + + return 0; +} + +void OPNMIDI_Ym2612_Impl::set_rate( double sample_rate, double clock_rate ) +{ + assert( sample_rate ); + assert( clock_rate > sample_rate ); + + int i; + + // 144 = 12 * (prescale * 2) = 12 * 6 * 2 + // prescale set to 6 by default + + double Frequence = clock_rate / sample_rate / 144.0; + if ( fabs( Frequence - 1.0 ) < 0.0000001 ) + Frequence = 1.0; + YM2612.TimerBase = int (Frequence * 4096.0); + + // Tableau TL : + // [0 - 4095] = +output [4095 - ...] = +output overflow (fill with 0) + // [12288 - 16383] = -output [16384 - ...] = -output overflow (fill with 0) + + for(i = 0; i < TL_LENGHT; i++) + { + if (i >= PG_CUT_OFF) // YM2612 cut off sound after 78 dB (14 bits output ?) + { + g.TL_TAB [TL_LENGHT + i] = g.TL_TAB [i] = 0; + } + else + { + double x = MAX_OUT; // Max output + x /= pow( 10.0, (ENV_STEP * i) / 20.0 ); // Decibel -> Voltage + + g.TL_TAB [i] = (int) x; + g.TL_TAB [TL_LENGHT + i] = -g.TL_TAB [i]; + } + } + + // Tableau SIN : + // g.SIN_TAB [x] [y] = sin(x) * y; + // x = phase and y = volume + + g.SIN_TAB [0] = g.SIN_TAB [SIN_LENGHT / 2] = PG_CUT_OFF; + + for(i = 1; i <= SIN_LENGHT / 4; i++) + { + double x = sin(2.0 * PI * (double) (i) / (double) (SIN_LENGHT)); // Sinus + x = 20 * log10(1 / x); // convert to dB + + int j = (int) (x / ENV_STEP); // Get TL range + + if (j > PG_CUT_OFF) j = (int) PG_CUT_OFF; + + g.SIN_TAB [i] = g.SIN_TAB [(SIN_LENGHT / 2) - i] = j; + g.SIN_TAB [(SIN_LENGHT / 2) + i] = g.SIN_TAB [SIN_LENGHT - i] = TL_LENGHT + j; + } + + // Tableau LFO (LFO wav) : + + for(i = 0; i < LFO_LENGHT; i++) + { + double x = sin(2.0 * PI * (double) (i) / (double) (LFO_LENGHT)); // Sinus + x += 1.0; + x /= 2.0; // positive only + x *= 11.8 / ENV_STEP; // ajusted to MAX enveloppe modulation + + g.LFO_ENV_TAB [i] = (int) x; + + x = sin(2.0 * PI * (double) (i) / (double) (LFO_LENGHT)); // Sinus + x *= (double) ((1 << (LFO_HBITS - 1)) - 1); + + g.LFO_FREQ_TAB [i] = (int) x; + + } + + // Tableau Enveloppe : + // g.ENV_TAB [0] -> g.ENV_TAB [ENV_LENGHT - 1] = attack curve + // g.ENV_TAB [ENV_LENGHT] -> g.ENV_TAB [2 * ENV_LENGHT - 1] = decay curve + + for(i = 0; i < ENV_LENGHT; i++) + { + // Attack curve (x^8 - music level 2 Vectorman 2) + double x = pow(((double) ((ENV_LENGHT - 1) - i) / (double) (ENV_LENGHT)), 8); + x *= ENV_LENGHT; + + g.ENV_TAB [i] = (int) x; + + // Decay curve (just linear) + x = pow(((double) (i) / (double) (ENV_LENGHT)), 1); + x *= ENV_LENGHT; + + g.ENV_TAB [ENV_LENGHT + i] = (int) x; + } + for ( i = 0; i < 8; i++ ) + g.ENV_TAB [i + ENV_LENGHT * 2] = 0; + + g.ENV_TAB [ENV_END >> ENV_LBITS] = ENV_LENGHT - 1; // for the stopped state + + // Tableau pour la conversion Attack -> Decay and Decay -> Attack + + int j = ENV_LENGHT - 1; + for ( i = 0; i < ENV_LENGHT; i++ ) + { + while ( j && g.ENV_TAB [j] < i ) + j--; + + g.DECAY_TO_ATTACK [i] = j << ENV_LBITS; + } + + // Tableau pour le Substain Level + + for(i = 0; i < 15; i++) + { + double x = i * 3; // 3 and not 6 (Mickey Mania first music for test) + x /= ENV_STEP; + + g.SL_TAB [i] = ((int) x << ENV_LBITS) + ENV_DECAY; + } + + g.SL_TAB [15] = ((ENV_LENGHT - 1) << ENV_LBITS) + ENV_DECAY; // special case : volume off + + // Tableau Frequency Step + + for(i = 0; i < 2048; i++) + { + double x = (double) (i) * Frequence; + +#if ((SIN_LBITS + SIN_HBITS - (21 - 7)) < 0) + x /= (double) (1 << ((21 - 7) - SIN_LBITS - SIN_HBITS)); +#else + x *= (double) (1 << (SIN_LBITS + SIN_HBITS - (21 - 7))); +#endif + + x /= 2.0; // because MUL = value * 2 + + g.FINC_TAB [i] = (unsigned int) x; + } + + // Tableaux Attack & Decay Rate + + for(i = 0; i < 4; i++) + { + g.AR_TAB [i] = 0; + g.DR_TAB [i] = 0; + } + + for(i = 0; i < 60; i++) + { + double x = Frequence; + + x *= 1.0 + ((i & 3) * 0.25); // bits 0-1 : x1.00, x1.25, x1.50, x1.75 + x *= (double) (1 << ((i >> 2))); // bits 2-5 : shift bits (x2^0 - x2^15) + x *= (double) (ENV_LENGHT << ENV_LBITS); // on ajuste pour le tableau g.ENV_TAB + + g.AR_TAB [i + 4] = (unsigned int) (x / AR_RATE); + g.DR_TAB [i + 4] = (unsigned int) (x / DR_RATE); + } + + for(i = 64; i < 96; i++) + { + g.AR_TAB [i] = g.AR_TAB [63]; + g.DR_TAB [i] = g.DR_TAB [63]; + + g.NULL_RATE [i - 64] = 0; + } + + for ( i = 96; i < 128; i++ ) + g.AR_TAB [i] = 0; + + // Tableau Detune + + for(i = 0; i < 4; i++) + { + for (int j = 0; j < 32; j++) + { +#if ((SIN_LBITS + SIN_HBITS - 21) < 0) + double y = (double) DT_DEF_TAB [(i << 5) + j] * Frequence / (double) (1 << (21 - SIN_LBITS - SIN_HBITS)); +#else + double y = (double) DT_DEF_TAB [(i << 5) + j] * Frequence * (double) (1 << (SIN_LBITS + SIN_HBITS - 21)); +#endif + + g.DT_TAB [i + 0] [j] = (int) y; + g.DT_TAB [i + 4] [j] = (int) -y; + } + } + + // Tableau LFO + g.LFO_INC_TAB [0] = (unsigned int) (3.98 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / sample_rate); + g.LFO_INC_TAB [1] = (unsigned int) (5.56 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / sample_rate); + g.LFO_INC_TAB [2] = (unsigned int) (6.02 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / sample_rate); + g.LFO_INC_TAB [3] = (unsigned int) (6.37 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / sample_rate); + g.LFO_INC_TAB [4] = (unsigned int) (6.88 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / sample_rate); + g.LFO_INC_TAB [5] = (unsigned int) (9.63 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / sample_rate); + g.LFO_INC_TAB [6] = (unsigned int) (48.1 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / sample_rate); + g.LFO_INC_TAB [7] = (unsigned int) (72.2 * (double) (1 << (LFO_HBITS + LFO_LBITS)) / sample_rate); + + reset(); +} + +const char* OPNMIDI_Ym2612_Emu::set_rate( double sample_rate, double clock_rate ) +{ + if ( !impl ) + { + impl = (OPNMIDI_Ym2612_Impl*) malloc( sizeof *impl ); + if ( !impl ) + return "Out of memory"; + impl->mute_mask = 0; + } + memset( &impl->YM2612, 0, sizeof impl->YM2612 ); + + impl->set_rate( sample_rate, clock_rate ); + + return 0; +} + +OPNMIDI_Ym2612_Emu::~OPNMIDI_Ym2612_Emu() +{ + free( impl ); +} + +inline void OPNMIDI_Ym2612_Impl::write0( int opn_addr, int data ) +{ + assert( (unsigned) data <= 0xFF ); + + if ( opn_addr < 0x30 ) + { + YM2612.REG [0] [opn_addr] = data; + YM_SET( opn_addr, data ); + } + else if ( YM2612.REG [0] [opn_addr] != data ) + { + YM2612.REG [0] [opn_addr] = data; + + if ( opn_addr < 0xA0 ) + SLOT_SET( opn_addr, data ); + else + CHANNEL_SET( opn_addr, data ); + } +} + +inline void OPNMIDI_Ym2612_Impl::write1( int opn_addr, int data ) +{ + assert( (unsigned) data <= 0xFF ); + + if ( opn_addr >= 0x30 && YM2612.REG [1] [opn_addr] != data ) + { + YM2612.REG [1] [opn_addr] = data; + + if ( opn_addr < 0xA0 ) + SLOT_SET( opn_addr + 0x100, data ); + else + CHANNEL_SET( opn_addr + 0x100, data ); + } +} + +void OPNMIDI_Ym2612_Emu::reset() +{ + impl->reset(); +} + +void OPNMIDI_Ym2612_Impl::reset() +{ + g.LFOcnt = 0; + YM2612.TimerA = 0; + YM2612.TimerAL = 0; + YM2612.TimerAcnt = 0; + YM2612.TimerB = 0; + YM2612.TimerBL = 0; + YM2612.TimerBcnt = 0; + YM2612.DAC = 0; + + YM2612.Status = 0; + + int i; + for ( i = 0; i < channel_count; i++ ) + { + channel_t& ch = YM2612.CHANNEL [i]; + + ch.LEFT = ~0; + ch.RIGHT = ~0; + ch.ALGO = 0; + ch.FB = 31; + ch.FMS = 0; + ch.AMS = 0; + + for ( int j = 0 ;j < 4 ; j++ ) + { + ch.S0_OUT [j] = 0; + ch.FNUM [j] = 0; + ch.FOCT [j] = 0; + ch.KC [j] = 0; + + ch.SLOT [j].Fcnt = 0; + ch.SLOT [j].Finc = 0; + ch.SLOT [j].Ecnt = ENV_END; // Put it at the end of Decay phase... + ch.SLOT [j].Einc = 0; + ch.SLOT [j].Ecmp = 0; + ch.SLOT [j].Ecurp = RELEASE; + + ch.SLOT [j].ChgEnM = 0; + } + } + + for ( i = 0; i < 0x100; i++ ) + { + YM2612.REG [0] [i] = -1; + YM2612.REG [1] [i] = -1; + } + + for ( i = 0xB6; i >= 0xB4; i-- ) + { + write0( i, 0xC0 ); + write1( i, 0xC0 ); + } + + for ( i = 0xB2; i >= 0x22; i-- ) + { + write0( i, 0 ); + write1( i, 0 ); + } + + write0( 0x2A, 0x80 ); +} + +void OPNMIDI_Ym2612_Emu::write0( int addr, int data ) +{ + impl->write0( addr, data ); +} + +void OPNMIDI_Ym2612_Emu::write1( int addr, int data ) +{ + impl->write1( addr, data ); +} + +void OPNMIDI_Ym2612_Emu::mute_voices( int mask ) { impl->mute_mask = mask; } + +static void update_envelope_( slot_t* sl ) +{ + switch ( sl->Ecurp ) + { + case 0: + // Env_Attack_Next + + // Verified with Gynoug even in HQ (explode SFX) + sl->Ecnt = ENV_DECAY; + + sl->Einc = sl->EincD; + sl->Ecmp = sl->SLL; + sl->Ecurp = DECAY; + break; + + case 1: + // Env_Decay_Next + + // Verified with Gynoug even in HQ (explode SFX) + sl->Ecnt = sl->SLL; + + sl->Einc = sl->EincS; + sl->Ecmp = ENV_END; + sl->Ecurp = SUBSTAIN; + break; + + case 2: + // Env_Substain_Next(slot_t *SL) + if (sl->SEG & 8) // SSG envelope type + { + int release = sl->SEG & 1; + + if ( !release ) + { + // re KEY ON + + // sl->Fcnt = 0; + // sl->ChgEnM = ~0; + + sl->Ecnt = 0; + sl->Einc = sl->EincA; + sl->Ecmp = ENV_DECAY; + sl->Ecurp = ATTACK; + } + + set_seg( *sl, (sl->SEG << 1) & 4 ); + + if ( !release ) + break; + } + // fall through + + case 3: + // Env_Release_Next + sl->Ecnt = ENV_END; + sl->Einc = 0; + sl->Ecmp = ENV_END + 1; + break; + + // default: no op + } +} + +inline void update_envelope( slot_t& sl ) +{ + int ecmp = sl.Ecmp; + if ( (sl.Ecnt += sl.Einc) >= ecmp ) + update_envelope_( &sl ); +} + +template +struct ym2612_update_chan { + static void func( tables_t&, channel_t&, OPNMIDI_Ym2612_Emu::sample_t*, int ); +}; + +typedef void (*ym2612_update_chan_t)( tables_t&, channel_t&, OPNMIDI_Ym2612_Emu::sample_t*, int ); + +template +void ym2612_update_chan::func( tables_t& g, channel_t& ch, + OPNMIDI_Ym2612_Emu::sample_t* buf, int length ) +{ + int not_end = ch.SLOT [S3].Ecnt - ENV_END; + + // algo is a compile-time constant, so all conditions based on it are resolved + // during compilation + + // special cases + if ( algo == 7 ) + not_end |= ch.SLOT [S0].Ecnt - ENV_END; + + if ( algo >= 5 ) + not_end |= ch.SLOT [S2].Ecnt - ENV_END; + + if ( algo >= 4 ) + not_end |= ch.SLOT [S1].Ecnt - ENV_END; + + int CH_S0_OUT_1 = ch.S0_OUT [1]; + + int in0 = ch.SLOT [S0].Fcnt; + int in1 = ch.SLOT [S1].Fcnt; + int in2 = ch.SLOT [S2].Fcnt; + int in3 = ch.SLOT [S3].Fcnt; + + int YM2612_LFOinc = g.LFOinc; + int YM2612_LFOcnt = g.LFOcnt + YM2612_LFOinc; + + if ( !not_end ) + return; + + do + { + // envelope + int const env_LFO = g.LFO_ENV_TAB [YM2612_LFOcnt >> LFO_LBITS & LFO_MASK]; + + short const* const ENV_TAB = g.ENV_TAB; + + #define CALC_EN( x ) \ + int temp##x = ENV_TAB [ch.SLOT [S##x].Ecnt >> ENV_LBITS] + ch.SLOT [S##x].TLL; \ + int en##x = ((temp##x ^ ch.SLOT [S##x].env_xor) + (env_LFO >> ch.SLOT [S##x].AMS)) & \ + ((temp##x - ch.SLOT [S##x].env_max) >> 31); + + CALC_EN( 0 ) + CALC_EN( 1 ) + CALC_EN( 2 ) + CALC_EN( 3 ) + + int const* const TL_TAB = g.TL_TAB; + + #define SINT( i, o ) (TL_TAB [g.SIN_TAB [(i)] + (o)]) + + // feedback + int CH_S0_OUT_0 = ch.S0_OUT [0]; + { + int temp = in0 + ((CH_S0_OUT_0 + CH_S0_OUT_1) >> ch.FB); + CH_S0_OUT_1 = CH_S0_OUT_0; + CH_S0_OUT_0 = SINT( (temp >> SIN_LBITS) & SIN_MASK, en0 ); + } + + int CH_OUTd; + if ( algo == 0 ) + { + int temp = in1 + CH_S0_OUT_1; + temp = in2 + SINT( (temp >> SIN_LBITS) & SIN_MASK, en1 ); + temp = in3 + SINT( (temp >> SIN_LBITS) & SIN_MASK, en2 ); + CH_OUTd = SINT( (temp >> SIN_LBITS) & SIN_MASK, en3 ); + } + else if ( algo == 1 ) + { + int temp = in2 + CH_S0_OUT_1 + SINT( (in1 >> SIN_LBITS) & SIN_MASK, en1 ); + temp = in3 + SINT( (temp >> SIN_LBITS) & SIN_MASK, en2 ); + CH_OUTd = SINT( (temp >> SIN_LBITS) & SIN_MASK, en3 ); + } + else if ( algo == 2 ) + { + int temp = in2 + SINT( (in1 >> SIN_LBITS) & SIN_MASK, en1 ); + temp = in3 + CH_S0_OUT_1 + SINT( (temp >> SIN_LBITS) & SIN_MASK, en2 ); + CH_OUTd = SINT( (temp >> SIN_LBITS) & SIN_MASK, en3 ); + } + else if ( algo == 3 ) + { + int temp = in1 + CH_S0_OUT_1; + temp = in3 + SINT( (temp >> SIN_LBITS) & SIN_MASK, en1 ) + + SINT( (in2 >> SIN_LBITS) & SIN_MASK, en2 ); + CH_OUTd = SINT( (temp >> SIN_LBITS) & SIN_MASK, en3 ); + } + else if ( algo == 4 ) + { + int temp = in3 + SINT( (in2 >> SIN_LBITS) & SIN_MASK, en2 ); + CH_OUTd = SINT( (temp >> SIN_LBITS) & SIN_MASK, en3 ) + + SINT( ((in1 + CH_S0_OUT_1) >> SIN_LBITS) & SIN_MASK, en1 ); + //DO_LIMIT + } + else if ( algo == 5 ) + { + int temp = CH_S0_OUT_1; + CH_OUTd = SINT( ((in3 + temp) >> SIN_LBITS) & SIN_MASK, en3 ) + + SINT( ((in1 + temp) >> SIN_LBITS) & SIN_MASK, en1 ) + + SINT( ((in2 + temp) >> SIN_LBITS) & SIN_MASK, en2 ); + //DO_LIMIT + } + else if ( algo == 6 ) + { + CH_OUTd = SINT( (in3 >> SIN_LBITS) & SIN_MASK, en3 ) + + SINT( ((in1 + CH_S0_OUT_1) >> SIN_LBITS) & SIN_MASK, en1 ) + + SINT( (in2 >> SIN_LBITS) & SIN_MASK, en2 ); + //DO_LIMIT + } + else if ( algo == 7 ) + { + CH_OUTd = SINT( (in3 >> SIN_LBITS) & SIN_MASK, en3 ) + + SINT( (in1 >> SIN_LBITS) & SIN_MASK, en1 ) + + SINT( (in2 >> SIN_LBITS) & SIN_MASK, en2 ) + CH_S0_OUT_1; + //DO_LIMIT + } + + CH_OUTd >>= MAX_OUT_BITS - output_bits + 2; + + // update phase + unsigned freq_LFO = ((g.LFO_FREQ_TAB [YM2612_LFOcnt >> LFO_LBITS & LFO_MASK] * + ch.FMS) >> (LFO_HBITS - 1 + 1)) + (1L << (LFO_FMS_LBITS - 1)); + YM2612_LFOcnt += YM2612_LFOinc; + in0 += (ch.SLOT [S0].Finc * freq_LFO) >> (LFO_FMS_LBITS - 1); + in1 += (ch.SLOT [S1].Finc * freq_LFO) >> (LFO_FMS_LBITS - 1); + in2 += (ch.SLOT [S2].Finc * freq_LFO) >> (LFO_FMS_LBITS - 1); + in3 += (ch.SLOT [S3].Finc * freq_LFO) >> (LFO_FMS_LBITS - 1); + + int t0 = buf [0] + (CH_OUTd & ch.LEFT); + int t1 = buf [1] + (CH_OUTd & ch.RIGHT); + + update_envelope( ch.SLOT [0] ); + update_envelope( ch.SLOT [1] ); + update_envelope( ch.SLOT [2] ); + update_envelope( ch.SLOT [3] ); + + ch.S0_OUT [0] = CH_S0_OUT_0; + buf [0] = t0; + buf [1] = t1; + buf += 2; + } + while ( --length ); + + ch.S0_OUT [1] = CH_S0_OUT_1; + + ch.SLOT [S0].Fcnt = in0; + ch.SLOT [S1].Fcnt = in1; + ch.SLOT [S2].Fcnt = in2; + ch.SLOT [S3].Fcnt = in3; +} + +static const ym2612_update_chan_t UPDATE_CHAN [8] = { + &ym2612_update_chan<0>::func, + &ym2612_update_chan<1>::func, + &ym2612_update_chan<2>::func, + &ym2612_update_chan<3>::func, + &ym2612_update_chan<4>::func, + &ym2612_update_chan<5>::func, + &ym2612_update_chan<6>::func, + &ym2612_update_chan<7>::func +}; + +void OPNMIDI_Ym2612_Impl::run_timer( int length ) +{ + int const step = 6; + int remain = length; + do + { + int n = step; + if ( n > remain ) + n = remain; + remain -= n; + + long i = n * YM2612.TimerBase; + if (YM2612.Mode & 1) // Timer A ON ? + { + // if ((YM2612.TimerAcnt -= 14073) <= 0) // 13879=NTSC (old: 14475=NTSC 14586=PAL) + if ((YM2612.TimerAcnt -= i) <= 0) + { + // timer a overflow + + YM2612.Status |= (YM2612.Mode & 0x04) >> 2; + YM2612.TimerAcnt += YM2612.TimerAL; + + if (YM2612.Mode & 0x80) + { + KEY_ON( YM2612.CHANNEL [2], 0 ); + KEY_ON( YM2612.CHANNEL [2], 1 ); + KEY_ON( YM2612.CHANNEL [2], 2 ); + KEY_ON( YM2612.CHANNEL [2], 3 ); + } + } + } + + if (YM2612.Mode & 2) // Timer B ON ? + { + // if ((YM2612.TimerBcnt -= 14073) <= 0) // 13879=NTSC (old: 14475=NTSC 14586=PAL) + if ((YM2612.TimerBcnt -= i) <= 0) + { + // timer b overflow + YM2612.Status |= (YM2612.Mode & 0x08) >> 2; + YM2612.TimerBcnt += YM2612.TimerBL; + } + } + } + while ( remain > 0 ); +} + +void OPNMIDI_Ym2612_Impl::run( int pair_count, OPNMIDI_Ym2612_Emu::sample_t* out ) +{ + if ( pair_count <= 0 ) + return; + + if ( YM2612.Mode & 3 ) + run_timer( pair_count ); + + // Mise ŕ jour des pas des compteurs-frequences s'ils ont ete modifies + + for ( int chi = 0; chi < channel_count; chi++ ) + { + channel_t& ch = YM2612.CHANNEL [chi]; + if ( ch.SLOT [0].Finc != -1 ) + continue; + + int i2 = 0; + if ( chi == 2 && (YM2612.Mode & 0x40) ) + i2 = 2; + + for ( int i = 0; i < 4; i++ ) + { + // static int seq [4] = { 2, 1, 3, 0 }; + // if ( i2 ) i2 = seq [i]; + + slot_t& sl = ch.SLOT [i]; + int finc = g.FINC_TAB [ch.FNUM [i2]] >> (7 - ch.FOCT [i2]); + int ksr = ch.KC [i2] >> sl.KSR_S; // keycode attenuation + sl.Finc = (finc + sl.DT [ch.KC [i2]]) * sl.MUL; + if (sl.KSR != ksr) // si le KSR a change alors + { // les differents taux pour l'enveloppe sont mis ŕ jour + sl.KSR = ksr; + + sl.EincA = sl.AR [ksr]; + sl.EincD = sl.DR [ksr]; + sl.EincS = sl.SR [ksr]; + sl.EincR = sl.RR [ksr]; + + if (sl.Ecurp == ATTACK) + { + sl.Einc = sl.EincA; + } + else if (sl.Ecurp == DECAY) + { + sl.Einc = sl.EincD; + } + else if (sl.Ecnt < ENV_END) + { + if (sl.Ecurp == SUBSTAIN) + sl.Einc = sl.EincS; + else if (sl.Ecurp == RELEASE) + sl.Einc = sl.EincR; + } + } + + if ( i2 ) + i2 = (i2 ^ 2) ^ (i2 >> 1); + } + } + + for ( int i = 0; i < channel_count; i++ ) + { + if ( !(mute_mask & (1 << i)) && (i != 5 || !YM2612.DAC) ) + UPDATE_CHAN [YM2612.CHANNEL [i].ALGO]( g, YM2612.CHANNEL [i], out, pair_count ); + } + + g.LFOcnt += g.LFOinc * pair_count; +} + +void OPNMIDI_Ym2612_Emu::run( int pair_count, sample_t* out ) { impl->run( pair_count, out ); } diff --git a/src/sound/opnmidi/Ym2612_ChipEmu.h b/src/sound/opnmidi/Ym2612_ChipEmu.h new file mode 100644 index 000000000..2628954d5 --- /dev/null +++ b/src/sound/opnmidi/Ym2612_ChipEmu.h @@ -0,0 +1,38 @@ +// YM2612 FM sound chip emulator interface + +// Game_Music_Emu 0.6.0 +#ifndef YM2612_EMU_H +#define YM2612_EMU_H + +struct OPNMIDI_Ym2612_Impl; + +class OPNMIDI_Ym2612_Emu { + OPNMIDI_Ym2612_Impl* impl; +public: + OPNMIDI_Ym2612_Emu() { impl = 0; } + ~OPNMIDI_Ym2612_Emu(); + + // Set output sample rate and chip clock rates, in Hz. Returns non-zero + // if error. + const char* set_rate( double sample_rate, double clock_rate ); + + // Reset to power-up state + void reset(); + + // Mute voice n if bit n (1 << n) of mask is set + enum { channel_count = 6 }; + void mute_voices( int mask ); + + // Write addr to register 0 then data to register 1 + void write0( int addr, int data ); + + // Write addr to register 2 then data to register 3 + void write1( int addr, int data ); + + // Run and add pair_count samples into current output buffer contents + typedef short sample_t; + enum { out_chan_count = 2 }; // stereo + void run( int pair_count, sample_t* out ); +}; + +#endif diff --git a/src/sound/opnmidi/fraction.hpp b/src/sound/opnmidi/fraction.hpp new file mode 100644 index 000000000..1ec10ab57 --- /dev/null +++ b/src/sound/opnmidi/fraction.hpp @@ -0,0 +1,206 @@ +#ifndef bqw_fraction_h +#define bqw_fraction_h + +#include +#include + +/* Fraction number handling. + * Copyright (C) 1992,2001 Bisqwit (http://iki.fi/bisqwit/) + */ + +template +class fraction +{ + inttype num1, num2; + typedef fraction self; + void Optim(); + + #if 1 + inline void Debug(char, const self &) { } + #else + inline void Debug(char op, const self &b) + { + cerr << nom() << '/' << denom() << ' ' << op + << ' ' << b.nom() << '/' << denom() + << ":\n"; + } + #endif +public: + void set(inttype n, inttype d) { num1=n; num2=d; Optim(); } + + fraction() : num1(0), num2(1) { } + fraction(inttype value) : num1(value), num2(1) { } + fraction(inttype n, inttype d) : num1(n), num2(d) { } + fraction(int value) : num1(value), num2(1) { } + template + explicit fraction(const floattype value) { operator= (value); } + inline double value() const {return nom() / (double)denom(); } + inline long double valuel() const {return nom() / (long double)denom(); } + self &operator+= (const inttype &value) { num1+=value*denom(); Optim(); return *this; } + self &operator-= (const inttype &value) { num1-=value*denom(); Optim(); return *this; } + self &operator*= (const inttype &value) { num1*=value; Optim(); return *this; } + self &operator/= (const inttype &value) { num2*=value; Optim(); return *this; } + self &operator+= (const self &b); + self &operator-= (const self &b); + self &operator*= (const self &b) { Debug('*',b);num1*=b.nom(); num2*=b.denom(); Optim(); return *this; } + self &operator/= (const self &b) { Debug('/',b);num1*=b.denom(); num2*=b.nom(); Optim(); return *this; } + self operator- () const { return self(-num1, num2); } + +#define fraction_blah_func(op1, op2) \ + self operator op1 (const self &b) const { self tmp(*this); tmp op2 b; return tmp; } + + fraction_blah_func( +, += ) + fraction_blah_func( -, -= ) + fraction_blah_func( /, /= ) + fraction_blah_func( *, *= ) + +#undef fraction_blah_func +#define fraction_blah_func(op) \ + bool operator op(const self &b) const { return value() op b.value(); } \ + bool operator op(inttype b) const { return value() op b; } + + fraction_blah_func( < ) + fraction_blah_func( > ) + fraction_blah_func( <= ) + fraction_blah_func( >= ) + +#undef fraction_blah_func + + const inttype &nom() const { return num1; } + const inttype &denom() const { return num2; } + inline bool operator == (inttype b) const { return denom() == 1 && nom() == b; } + inline bool operator != (inttype b) const { return denom() != 1 || nom() != b; } + inline bool operator == (const self &b) const { return denom()==b.denom() && nom()==b.nom(); } + inline bool operator != (const self &b) const { return denom()!=b.denom() || nom()!=b.nom(); } + //operator bool () const { return nom() != 0; } + inline bool negative() const { return (nom() < 0) ^ (denom() < 0); } + + self &operator= (const inttype value) { num2=1; num1=value; return *this; } + //self &operator= (int value) { num2=1; num1=value; return *this; } + + self &operator= (double orig) { return *this = (long double)orig; } + self &operator= (long double orig); +}; + +template +void fraction::Optim() +{ + /* Euclidean algorithm */ + inttype n1, n2, nn1, nn2; + + nn1 = std::numeric_limits::is_signed ? (num1 >= 0 ? num1 : -num1) : num1; + nn2 = std::numeric_limits::is_signed ? (num2 >= 0 ? num2 : -num2) : num2; + + if(nn1 < nn2) + n1 = num1, n2 = num2; + else + n1 = num2, n2 = num1; + + if(!num1) { num2 = 1; return; } + for(;;) + { + //fprintf(stderr, "%d/%d: n1=%d,n2=%d\n", nom(),denom(),n1,n2); + inttype tmp = n2 % n1; + if(!tmp)break; + n2 = n1; + n1 = tmp; + } + num1 /= n1; + num2 /= n1; + //fprintf(stderr, "result: %d/%d\n\n", nom(), denom()); +} + +template +inline const fraction abs(const fraction &f) +{ + return fraction(abs(f.nom()), abs(f.denom())); +} + +#define fraction_blah_func(op) \ + template \ + fraction operator op \ + (const inttype bla, const fraction &b) \ + { return fraction (bla) op b; } +fraction_blah_func( + ) +fraction_blah_func( - ) +fraction_blah_func( * ) +fraction_blah_func( / ) +#undef fraction_blah_func + +#define fraction_blah_func(op1, op2) \ + template \ + fraction &fraction::operator op2 (const fraction &b) \ + { \ + inttype newnom = nom()*b.denom() op1 denom()*b.nom(); \ + num2 *= b.denom(); \ + num1 = newnom; \ + Optim(); \ + return *this; \ + } +fraction_blah_func( +, += ) +fraction_blah_func( -, -= ) +#undef fraction_blah_func + +template +fraction &fraction::operator= (long double orig) +{ + if(orig == 0.0) + { + set(0, 0); + return *this; + } + + inttype cf[25]; + for(int maxdepth=1; maxdepth<25; ++maxdepth) + { + inttype u,v; + long double virhe, a=orig; + int i, viim; + + for(i = 0; i < maxdepth; ++i) + { + cf[i] = (inttype)a; + if(cf[i]-1 > cf[i])break; + a = 1.0 / (a - cf[i]); + } + + for(viim=i-1; i < maxdepth; ++i) + cf[i] = 0; + + u = cf[viim]; + v = 1; + for(i = viim-1; i >= 0; --i) + { + inttype w = cf[i] * u + v; + v = u; + u = w; + } + + virhe = (orig - (u / (long double)v)) / orig; + + set(u, v); + //if(verbose > 4) + // cerr << "Guess: " << *this << " - error = " << virhe*100 << "%\n"; + + if(virhe < 1e-8 && virhe > -1e-8)break; + } + + //if(verbose > 4) + //{ + // cerr << "Fraction=" << orig << ": " << *this << endl; + //} + + return *this; +} + + +/* +template +ostream &operator << (ostream &dest, const fraction &m) +{ + if(m.denom() == (inttype)1) return dest << m.nom(); + return dest << m.nom() << '/' << m.denom(); +} +*/ + +#endif diff --git a/src/sound/opnmidi/opnbank.h b/src/sound/opnmidi/opnbank.h new file mode 100644 index 000000000..fcf3abb77 --- /dev/null +++ b/src/sound/opnmidi/opnbank.h @@ -0,0 +1,81 @@ +/* + * libOPNMIDI is a free MIDI to WAV conversion library with OPN2 (YM2612) emulation + * + * MIDI parser and player (Original code from ADLMIDI): Copyright (c) 2010-2014 Joel Yliluoma + * ADLMIDI Library API: Copyright (c) 2016 Vitaly Novichkov + * + * Library is based on the ADLMIDI, a MIDI player for Linux and Windows with OPL3 emulation: + * http://iki.fi/bisqwit/source/adlmidi.html + * + * 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 3 of the License, or + * 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, see . + */ + +#include + +#ifdef ADLMIDI_buildAsApp +#include +class MutexType +{ + SDL_mutex* mut; +public: + MutexType() : mut(SDL_CreateMutex()) { } + ~MutexType() { SDL_DestroyMutex(mut); } + void Lock() { SDL_mutexP(mut); } + void Unlock() { SDL_mutexV(mut); } +}; +#endif + +/* *********** FM Operator indexes *********** */ +#define OPERATOR1 0 +#define OPERATOR2 1 +#define OPERATOR3 2 +#define OPERATOR4 3 +/* *********** FM Operator indexes *end******* */ + +struct OPN_Operator +{ + //! Raw register data + uint8_t data[7]; + /* + Bytes: + 0 - Deture/Multiple + 1 - Total Level + 2 - Rate Scale / Attack + 3 - Amplitude modulation / Decay-1 + 4 - Decay-2 + 5 - Systain / Release + 6 - SSG-EG byte + */ +}; + +struct opnInstData +{ + //! Operators prepared for sending to OPL chip emulator + OPN_Operator OPS[4]; + uint8_t fbalg; + uint8_t lfosens; + //! Note offset + int16_t finetune; +}; + +struct opnInstMeta +{ + enum { Flag_Pseudo8op = 0x01, Flag_NoSound = 0x02 }; + uint16_t opnno1, opnno2; + uint8_t tone; + uint8_t flags; + uint16_t ms_sound_kon; // Number of milliseconds it produces sound; + uint16_t ms_sound_koff; + double fine_tune; +}; diff --git a/src/sound/opnmidi/opnmidi.cpp b/src/sound/opnmidi/opnmidi.cpp new file mode 100644 index 000000000..61a7e8346 --- /dev/null +++ b/src/sound/opnmidi/opnmidi.cpp @@ -0,0 +1,776 @@ +/* + * libOPNMIDI is a free MIDI to WAV conversion library with OPN2 (YM2612) emulation + * + * MIDI parser and player (Original code from ADLMIDI): Copyright (c) 2010-2014 Joel Yliluoma + * OPNMIDI Library and YM2612 support: Copyright (c) 2017-2018 Vitaly Novichkov + * + * Library is based on the ADLMIDI, a MIDI player for Linux and Windows with OPL3 emulation: + * http://iki.fi/bisqwit/source/adlmidi.html + * + * 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 3 of the License, or + * 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, see . + */ + +#include "opnmidi_private.hpp" + +#define MaxCards 100 +#define MaxCards_STR "100" + +static OPN2_Version opn2_version = { + OPNMIDI_VERSION_MAJOR, + OPNMIDI_VERSION_MINOR, + OPNMIDI_VERSION_PATCHLEVEL +}; + +/*---------------------------EXPORTS---------------------------*/ + +OPNMIDI_EXPORT struct OPN2_MIDIPlayer *opn2_init(long sample_rate) +{ + OPN2_MIDIPlayer *midi_device; + midi_device = (OPN2_MIDIPlayer *)malloc(sizeof(OPN2_MIDIPlayer)); + if(!midi_device) + { + OPN2MIDI_ErrorString = "Can't initialize OPNMIDI: out of memory!"; + return NULL; + } + + OPNMIDIplay *player = new OPNMIDIplay(static_cast(sample_rate)); + if(!player) + { + free(midi_device); + OPN2MIDI_ErrorString = "Can't initialize OPNMIDI: out of memory!"; + return NULL; + } + midi_device->opn2_midiPlayer = player; + opn2RefreshNumCards(midi_device); + return midi_device; +} + +OPNMIDI_EXPORT int opn2_setNumChips(OPN2_MIDIPlayer *device, int numCards) +{ + if(device == NULL) + return -2; + + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.NumCards = static_cast(numCards); + if(play->m_setup.NumCards < 1 || play->m_setup.NumCards > MaxCards) + { + play->setErrorString("number of chips may only be 1.." MaxCards_STR ".\n"); + return -1; + } + + play->opn.NumCards = play->m_setup.NumCards; + opn2_reset(device); + + return opn2RefreshNumCards(device); +} + +OPNMIDI_EXPORT int opn2_getNumChips(struct OPN2_MIDIPlayer *device) +{ + if(device == NULL) + return -2; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return (int)play->m_setup.NumCards; + return -2; +} + +OPNMIDI_EXPORT int opn2_openBankFile(OPN2_MIDIPlayer *device, const char *filePath) +{ + if(device && device->opn2_midiPlayer) + { + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.tick_skip_samples_delay = 0; + if(!play->LoadBank(filePath)) + { + std::string err = play->getErrorString(); + if(err.empty()) + play->setErrorString("OPN2 MIDI: Can't load file"); + return -1; + } + else return opn2RefreshNumCards(device); + } + OPN2MIDI_ErrorString = "Can't load file: OPN2 MIDI is not initialized"; + return -1; +} + +OPNMIDI_EXPORT int opn2_openBankData(OPN2_MIDIPlayer *device, const void *mem, long size) +{ + if(device && device->opn2_midiPlayer) + { + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.tick_skip_samples_delay = 0; + if(!play->LoadBank(mem, static_cast(size))) + { + std::string err = play->getErrorString(); + if(err.empty()) + play->setErrorString("OPN2 MIDI: Can't load data from memory"); + return -1; + } + else return 0; + } + + OPN2MIDI_ErrorString = "Can't load file: OPN2 MIDI is not initialized"; + return -1; +} + +OPNMIDI_EXPORT void opn2_setScaleModulators(OPN2_MIDIPlayer *device, int smod) +{ + if(!device) return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.ScaleModulators = smod; + play->opn.ScaleModulators = play->m_setup.ScaleModulators; +} + +OPNMIDI_EXPORT void opn2_setLoopEnabled(OPN2_MIDIPlayer *device, int loopEn) +{ + if(!device) return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.loopingIsEnabled = (loopEn != 0); +} + +OPNMIDI_EXPORT void opn2_setLogarithmicVolumes(struct OPN2_MIDIPlayer *device, int logvol) +{ + if(!device) return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.LogarithmicVolumes = static_cast(logvol); + play->opn.LogarithmicVolumes = play->m_setup.LogarithmicVolumes; +} + +OPNMIDI_EXPORT void opn2_setVolumeRangeModel(OPN2_MIDIPlayer *device, int volumeModel) +{ + if(!device) return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.VolumeModel = volumeModel; + play->opn.ChangeVolumeRangesModel(static_cast(volumeModel)); +} + +OPNMIDI_EXPORT int opn2_openFile(OPN2_MIDIPlayer *device, const char *filePath) +{ + if(device && device->opn2_midiPlayer) + { + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.tick_skip_samples_delay = 0; + if(!play->LoadMIDI(filePath)) + { + std::string err = play->getErrorString(); + if(err.empty()) + play->setErrorString("OPN2 MIDI: Can't load file"); + return -1; + } + else return 0; + } + + OPN2MIDI_ErrorString = "Can't load file: OPN2 MIDI is not initialized"; + return -1; +} + +OPNMIDI_EXPORT int opn2_openData(OPN2_MIDIPlayer *device, const void *mem, unsigned long size) +{ + if(device && device->opn2_midiPlayer) + { + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.tick_skip_samples_delay = 0; + if(!play->LoadMIDI(mem, static_cast(size))) + { + std::string err = play->getErrorString(); + if(err.empty()) + play->setErrorString("OPN2 MIDI: Can't load data from memory"); + return -1; + } + else return 0; + } + + OPN2MIDI_ErrorString = "Can't load file: OPN2 MIDI is not initialized"; + return -1; +} + +OPNMIDI_EXPORT const char *opn2_emulatorName() +{ + #ifdef USE_LEGACY_EMULATOR + return "GENS 2.10 YM2612"; + #else + return "Nuked OPN2 YM3438"; + #endif +} + +OPNMIDI_EXPORT const char *opn2_linkedLibraryVersion() +{ + return OPNMIDI_VERSION; +} + +OPNMIDI_EXPORT const OPN2_Version *opn2_linkedVersion() +{ + return &opn2_version; +} + +OPNMIDI_EXPORT const char *opn2_errorString() +{ + return OPN2MIDI_ErrorString.c_str(); +} + +OPNMIDI_EXPORT const char *opn2_errorInfo(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return opn2_errorString(); + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(!play) + return opn2_errorString(); + return play->getErrorString().c_str(); +} + +OPNMIDI_EXPORT const char *opn2_getMusicTitle(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return ""; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(!play) + return ""; + return play->musTitle.c_str(); +} + +OPNMIDI_EXPORT void opn2_close(OPN2_MIDIPlayer *device) +{ + if(device->opn2_midiPlayer) + delete reinterpret_cast(device->opn2_midiPlayer); + device->opn2_midiPlayer = NULL; + free(device); + device = NULL; +} + +OPNMIDI_EXPORT void opn2_reset(OPN2_MIDIPlayer *device) +{ + if(!device) + return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->m_setup.tick_skip_samples_delay = 0; + play->opn.Reset(play->m_setup.PCM_RATE); + play->ch.clear(); + play->ch.resize(play->opn.NumChannels); +} + +OPNMIDI_EXPORT double opn2_totalTimeLength(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return -1.0; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->timeLength(); + else + return -1.0; +} + +OPNMIDI_EXPORT double opn2_loopStartTime(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return -1.0; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->getLoopStart(); + else + return -1.0; +} + +OPNMIDI_EXPORT double opn2_loopEndTime(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return -1.0; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->getLoopEnd(); + else + return -1.0; +} + +OPNMIDI_EXPORT double opn2_positionTell(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return -1.0; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->tell(); + else + return -1.0; +} + +OPNMIDI_EXPORT void opn2_positionSeek(struct OPN2_MIDIPlayer *device, double seconds) +{ + if(!device) + return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->seek(seconds); +} + +OPNMIDI_EXPORT void opn2_positionRewind(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->rewind(); +} + +OPNMIDI_EXPORT void opn2_setTempo(struct OPN2_MIDIPlayer *device, double tempo) +{ + if(!device || (tempo <= 0.0)) + return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->setTempo(tempo); +} + + + +OPNMIDI_EXPORT const char *opn2_metaMusicTitle(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return ""; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->musTitle.c_str(); + else + return ""; +} + + +OPNMIDI_EXPORT const char *opn2_metaMusicCopyright(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return ""; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->musCopyright.c_str(); + else + return ""; +} + +OPNMIDI_EXPORT size_t opn2_metaTrackTitleCount(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return 0; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->musTrackTitles.size(); + else + return 0; +} + +OPNMIDI_EXPORT const char *opn2_metaTrackTitle(struct OPN2_MIDIPlayer *device, size_t index) +{ + if(!device) + return 0; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(index >= play->musTrackTitles.size()) + return "INVALID"; + return play->musTrackTitles[index].c_str(); +} + + +OPNMIDI_EXPORT size_t opn2_metaMarkerCount(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return 0; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(play) + return play->musMarkers.size(); + else + return 0; +} + +OPNMIDI_EXPORT Opn2_MarkerEntry opn2_metaMarker(struct OPN2_MIDIPlayer *device, size_t index) +{ + struct Opn2_MarkerEntry marker; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + if(!device || !play || (index >= play->musMarkers.size())) + { + marker.label = "INVALID"; + marker.pos_time = 0.0; + marker.pos_ticks = 0; + return marker; + } + else + { + OPNMIDIplay::MIDI_MarkerEntry &mk = play->musMarkers[index]; + marker.label = mk.label.c_str(); + marker.pos_time = mk.pos_time; + marker.pos_ticks = (unsigned long)mk.pos_ticks; + } + return marker; +} + +OPNMIDI_EXPORT void opn2_setRawEventHook(struct OPN2_MIDIPlayer *device, OPN2_RawEventHook rawEventHook, void *userData) +{ + if(!device) + return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->hooks.onEvent = rawEventHook; + play->hooks.onEvent_userData = userData; +} + +/* Set note hook */ +OPNMIDI_EXPORT void opn2_setNoteHook(struct OPN2_MIDIPlayer *device, OPN2_NoteHook noteHook, void *userData) +{ + if(!device) + return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->hooks.onNote = noteHook; + play->hooks.onNote_userData = userData; +} + +/* Set debug message hook */ +OPNMIDI_EXPORT void opn2_setDebugMessageHook(struct OPN2_MIDIPlayer *device, OPN2_DebugMessageHook debugMessageHook, void *userData) +{ + if(!device) + return; + OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + play->hooks.onDebugMessage = debugMessageHook; + play->hooks.onDebugMessage_userData = userData; +} + + + +inline static void SendStereoAudio(int &samples_requested, + ssize_t &in_size, + short *_in, + ssize_t out_pos, + short *_out) +{ + if(!in_size) + return; + size_t offset = static_cast(out_pos); + size_t inSamples = static_cast(in_size * 2); + size_t maxSamples = static_cast(samples_requested) - offset; + size_t toCopy = std::min(maxSamples, inSamples); + std::memcpy(_out + out_pos, _in, toCopy * sizeof(short)); +} + + +OPNMIDI_EXPORT int opn2_play(OPN2_MIDIPlayer *device, int sampleCount, short *out) +{ + sampleCount -= sampleCount % 2; //Avoid even sample requests + if(sampleCount < 0) + return 0; + if(!device) + return 0; + + OPNMIDIplay * player = (reinterpret_cast(device->opn2_midiPlayer)); + OPNMIDIplay::Setup &setup = player->m_setup; + + ssize_t gotten_len = 0; + ssize_t n_periodCountStereo = 512; + //ssize_t n_periodCountPhys = n_periodCountStereo * 2; + int left = sampleCount; + bool hasSkipped = setup.tick_skip_samples_delay > 0; + + while(left > 0) + { + {// + const double eat_delay = setup.delay < setup.maxdelay ? setup.delay : setup.maxdelay; + if(hasSkipped) + { + size_t samples = setup.tick_skip_samples_delay > sampleCount ? sampleCount : setup.tick_skip_samples_delay; + n_periodCountStereo = samples / 2; + } + else + { + setup.delay -= eat_delay; + setup.carry += setup.PCM_RATE * eat_delay; + n_periodCountStereo = static_cast(setup.carry); + setup.carry -= n_periodCountStereo; + } + + //if(setup.SkipForward > 0) + // setup.SkipForward -= 1; + //else + { + if((player->atEnd) && (setup.delay <= 0.0)) + break;//Stop to fetch samples at reaching the song end with disabled loop + + ssize_t leftSamples = left / 2; + if(n_periodCountStereo > leftSamples) + { + setup.tick_skip_samples_delay = (n_periodCountStereo - leftSamples) * 2; + n_periodCountStereo = leftSamples; + } + //! Count of stereo samples + ssize_t in_generatedStereo = (n_periodCountStereo > 512) ? 512 : n_periodCountStereo; + //! Total count of samples + ssize_t in_generatedPhys = in_generatedStereo * 2; + //! Unsigned total sample count + //fill buffer with zeros + int16_t *out_buf = player->outBuf; + std::memset(out_buf, 0, static_cast(in_generatedPhys) * sizeof(int16_t)); + unsigned int chips = player->opn.NumCards; + if(chips == 1) + { + #ifdef USE_LEGACY_EMULATOR + player->opn.cardsOP2[0]->run(int(in_generatedStereo), out_buf); + #else + OPN2_GenerateStream(player->opn.cardsOP2[0], out_buf, (Bit32u)in_generatedStereo); + #endif + } + else/* if(n_periodCountStereo > 0)*/ + { + /* Generate data from every chip and mix result */ + for(unsigned card = 0; card < chips; ++card) + { + #ifdef USE_LEGACY_EMULATOR + player->opn.cardsOP2[card]->run(int(in_generatedStereo), out_buf); + #else + OPN2_GenerateStreamMix(player->opn.cardsOP2[card], out_buf, (Bit32u)in_generatedStereo); + #endif + } + } + /* Process it */ + SendStereoAudio(sampleCount, in_generatedStereo, out_buf, gotten_len, out); + + left -= (int)in_generatedPhys; + gotten_len += (in_generatedPhys) /* - setup.stored_samples*/; + } + if(hasSkipped) + { + setup.tick_skip_samples_delay -= n_periodCountStereo * 2; + hasSkipped = setup.tick_skip_samples_delay > 0; + } + else + setup.delay = player->Tick(eat_delay, setup.mindelay); + }// + } + + return static_cast(gotten_len); +} + + +OPNMIDI_EXPORT int opn2_generate(struct OPN2_MIDIPlayer *device, int sampleCount, short *out) +{ + sampleCount -= sampleCount % 2; //Avoid even sample requests + if(sampleCount < 0) + return 0; + if(!device) + return 0; + + OPNMIDIplay * player = (reinterpret_cast(device->opn2_midiPlayer)); + OPNMIDIplay::Setup &setup = player->m_setup; + + ssize_t gotten_len = 0; + ssize_t n_periodCountStereo = 512; + + int left = sampleCount; + double delay = double(sampleCount) / double(setup.PCM_RATE); + + while(left > 0) + { + {// + const double eat_delay = delay < setup.maxdelay ? delay : setup.maxdelay; + delay -= eat_delay; + setup.carry += setup.PCM_RATE * eat_delay; + n_periodCountStereo = static_cast(setup.carry); + setup.carry -= n_periodCountStereo; + + { + ssize_t leftSamples = left / 2; + if(n_periodCountStereo > leftSamples) + n_periodCountStereo = leftSamples; + //! Count of stereo samples + ssize_t in_generatedStereo = (n_periodCountStereo > 512) ? 512 : n_periodCountStereo; + //! Total count of samples + ssize_t in_generatedPhys = in_generatedStereo * 2; + //! Unsigned total sample count + //fill buffer with zeros + int16_t *out_buf = player->outBuf; + std::memset(out_buf, 0, static_cast(in_generatedPhys) * sizeof(int16_t)); + unsigned int chips = player->opn.NumCards; + if(chips == 1) + { + #ifdef USE_LEGACY_EMULATOR + player->opn.cardsOP2[0]->run(int(in_generatedStereo), out_buf); + #else + OPN2_GenerateStream(player->opn.cardsOP2[0], out_buf, (Bit32u)in_generatedStereo); + #endif + } + else/* if(n_periodCountStereo > 0)*/ + { + /* Generate data from every chip and mix result */ + for(unsigned card = 0; card < chips; ++card) + { + #ifdef USE_LEGACY_EMULATOR + player->opn.cardsOP2[card]->run(int(in_generatedStereo), out_buf); + #else + OPN2_GenerateStreamMix(player->opn.cardsOP2[card], out_buf, (Bit32u)in_generatedStereo); + #endif + } + } + /* Process it */ + SendStereoAudio(sampleCount, in_generatedStereo, out_buf, gotten_len, out); + + left -= (int)in_generatedPhys; + gotten_len += (in_generatedPhys) /* - setup.stored_samples*/; + } + + player->TickIteratos(eat_delay); + }// + } + + return static_cast(gotten_len); +} + +OPNMIDI_EXPORT double opn2_tickEvents(struct OPN2_MIDIPlayer *device, double seconds, double granuality) +{ + if(!device) + return -1.0; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return -1.0; + return player->Tick(seconds, granuality); +} + +OPNMIDI_EXPORT int opn2_atEnd(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return 1; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return 1; + return (int)player->atEnd; +} + +OPNMIDI_EXPORT void opn2_panic(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_panic(); +} + +OPNMIDI_EXPORT void opn2_rt_resetState(struct OPN2_MIDIPlayer *device) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_ResetState(); +} + +OPNMIDI_EXPORT int opn2_rt_noteOn(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 note, OPN2_UInt8 velocity) +{ + if(!device) + return 0; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return 0; + return (int)player->realTime_NoteOn(channel, note, velocity); +} + +OPNMIDI_EXPORT void opn2_rt_noteOff(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 note) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_NoteOff(channel, note); +} + +OPNMIDI_EXPORT void opn2_rt_noteAfterTouch(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 note, OPN2_UInt8 atVal) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_NoteAfterTouch(channel, note, atVal); +} + +OPNMIDI_EXPORT void opn2_rt_channelAfterTouch(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 atVal) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_ChannelAfterTouch(channel, atVal); +} + +OPNMIDI_EXPORT void opn2_rt_controllerChange(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 type, OPN2_UInt8 value) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_Controller(channel, type, value); +} + +OPNMIDI_EXPORT void opn2_rt_patchChange(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 patch) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_PatchChange(channel, patch); +} + +OPNMIDI_EXPORT void opn2_rt_pitchBend(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt16 pitch) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_PitchBend(channel, pitch); +} + +OPNMIDI_EXPORT void opn2_rt_pitchBendML(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 msb, OPN2_UInt8 lsb) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_PitchBend(channel, msb, lsb); +} + +OPNMIDI_EXPORT void opn2_rt_bankChangeLSB(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 lsb) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_BankChangeLSB(channel, lsb); +} + +OPNMIDI_EXPORT void opn2_rt_bankChangeMSB(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 msb) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_BankChangeMSB(channel, msb); +} + +OPNMIDI_EXPORT void opn2_rt_bankChange(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_SInt16 bank) +{ + if(!device) + return; + OPNMIDIplay *player = reinterpret_cast(device->opn2_midiPlayer); + if(!player) + return; + player->realTime_BankChange(channel, (uint16_t)bank); +} diff --git a/src/sound/opnmidi/opnmidi.h b/src/sound/opnmidi/opnmidi.h new file mode 100644 index 000000000..5988e5dc1 --- /dev/null +++ b/src/sound/opnmidi/opnmidi.h @@ -0,0 +1,266 @@ +/* + * libOPNMIDI is a free MIDI to WAV conversion library with OPN2 (YM2612) emulation + * + * MIDI parser and player (Original code from ADLMIDI): Copyright (c) 2010-2014 Joel Yliluoma + * OPNMIDI Library and YM2612 support: Copyright (c) 2017-2018 Vitaly Novichkov + * + * Library is based on the ADLMIDI, a MIDI player for Linux and Windows with OPL3 emulation: + * http://iki.fi/bisqwit/source/adlmidi.html + * + * 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 3 of the License, or + * 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, see . + */ + +#ifndef OPNMIDI_H +#define OPNMIDI_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define OPNMIDI_VERSION_MAJOR 1 +#define OPNMIDI_VERSION_MINOR 1 +#define OPNMIDI_VERSION_PATCHLEVEL 0 + +#define OPNMIDI_TOSTR(s) #s +#define OPNMIDI_VERSION \ + OPNMIDI_TOSTR(OPNMIDI_VERSION_MAJOR) "." \ + OPNMIDI_TOSTR(OPNMIDI_VERSION_MINOR) "." \ + OPNMIDI_TOSTR(OPNMIDI_VERSION_PATCHLEVEL) + +#include + +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +#include +typedef uint8_t OPN2_UInt8; +typedef uint16_t OPN2_UInt16; +typedef int8_t OPN2_SInt8; +typedef int16_t OPN2_SInt16; +#else +typedef unsigned char OPN2_UInt8; +typedef unsigned short OPN2_UInt16; +typedef char OPN2_SInt8; +typedef short OPN2_SInt16; +#endif + +enum OPNMIDI_VolumeModels +{ + OPNMIDI_VolumeModel_AUTO = 0, + OPNMIDI_VolumeModel_Generic, + OPNMIDI_VolumeModel_CMF, + OPNMIDI_VolumeModel_DMX, + OPNMIDI_VolumeModel_APOGEE, + OPNMIDI_VolumeModel_9X +}; + +struct OPN2_MIDIPlayer +{ + void *opn2_midiPlayer; +}; + +/* DEPRECATED */ +#define opn2_setNumCards opn2_setNumChips + +/* Sets number of emulated sound cards (from 1 to 100). Emulation of multiple sound cards exchanges polyphony limits*/ +extern int opn2_setNumChips(struct OPN2_MIDIPlayer *device, int numCards); + +/* Get current number of emulated chips */ +extern int opn2_getNumChips(struct OPN2_MIDIPlayer *device); + +/*Enable or disable Enables scaling of modulator volumes*/ +extern void opn2_setScaleModulators(struct OPN2_MIDIPlayer *device, int smod); + +/*Enable or disable built-in loop (built-in loop supports 'loopStart' and 'loopEnd' tags to loop specific part)*/ +extern void opn2_setLoopEnabled(struct OPN2_MIDIPlayer *device, int loopEn); + +/*Enable or disable Logariphmic volume changer (-1 sets default per bank, 0 disable, 1 enable) */ +extern void opn2_setLogarithmicVolumes(struct OPN2_MIDIPlayer *device, int logvol); + +/*Set different volume range model */ +extern void opn2_setVolumeRangeModel(struct OPN2_MIDIPlayer *device, int volumeModel); + +/*Load WOPN bank file from File System. Is recommended to call adl_reset() to apply changes to already-loaded file player or real-time.*/ +extern int opn2_openBankFile(struct OPN2_MIDIPlayer *device, const char *filePath); + +/*Load WOPN bank file from memory data*/ +extern int opn2_openBankData(struct OPN2_MIDIPlayer *device, const void *mem, long size); + + +/*Returns chip emulator name string*/ +extern const char *opn2_emulatorName(); + +typedef struct { + OPN2_UInt16 major; + OPN2_UInt16 minor; + OPN2_UInt16 patch; +} OPN2_Version; + +/*Returns string which contains a version number*/ +extern const char *opn2_linkedLibraryVersion(); + +/*Returns structure which contains a version number of library */ +extern const OPN2_Version *opn2_linkedVersion(); + +/*Returns string which contains last error message*/ +extern const char *opn2_errorString(); + +/*Returns string which contains last error message on specific device*/ +extern const char *opn2_errorInfo(struct OPN2_MIDIPlayer *device); + +/*Initialize ADLMIDI Player device*/ +extern struct OPN2_MIDIPlayer *opn2_init(long sample_rate); + +/*Load MIDI file from File System*/ +extern int opn2_openFile(struct OPN2_MIDIPlayer *device, const char *filePath); + +/*Load MIDI file from memory data*/ +extern int opn2_openData(struct OPN2_MIDIPlayer *device, const void *mem, unsigned long size); + +/*Resets MIDI player*/ +extern void opn2_reset(struct OPN2_MIDIPlayer *device); + +/*Get total time length of current song*/ +extern double opn2_totalTimeLength(struct OPN2_MIDIPlayer *device); + +/*Get loop start time if presented. -1 means MIDI file has no loop points */ +extern double opn2_loopStartTime(struct OPN2_MIDIPlayer *device); + +/*Get loop end time if presented. -1 means MIDI file has no loop points */ +extern double opn2_loopEndTime(struct OPN2_MIDIPlayer *device); + +/*Get current time position in seconds*/ +extern double opn2_positionTell(struct OPN2_MIDIPlayer *device); + +/*Jump to absolute time position in seconds*/ +extern void opn2_positionSeek(struct OPN2_MIDIPlayer *device, double seconds); + +/*Reset MIDI track position to begin */ +extern void opn2_positionRewind(struct OPN2_MIDIPlayer *device); + +/*Set tempo multiplier: 1.0 - original tempo, >1 - play faster, <1 - play slower */ +extern void opn2_setTempo(struct OPN2_MIDIPlayer *device, double tempo); + +/*Close and delete OPNMIDI device*/ +extern void opn2_close(struct OPN2_MIDIPlayer *device); + + + +/**META**/ + +/*Returns string which contains a music title*/ +extern const char *opn2_metaMusicTitle(struct OPN2_MIDIPlayer *device); + +/*Returns string which contains a copyright string*/ +extern const char *opn2_metaMusicCopyright(struct OPN2_MIDIPlayer *device); + +/*Returns count of available track titles: NOTE: there are CAN'T be associated with channel in any of event or note hooks */ +extern size_t opn2_metaTrackTitleCount(struct OPN2_MIDIPlayer *device); + +/*Get track title by index*/ +extern const char *opn2_metaTrackTitle(struct OPN2_MIDIPlayer *device, size_t index); + +struct Opn2_MarkerEntry +{ + const char *label; + double pos_time; + unsigned long pos_ticks; +}; + +/*Returns count of available markers*/ +extern size_t opn2_metaMarkerCount(struct OPN2_MIDIPlayer *device); + +/*Returns the marker entry*/ +extern struct Opn2_MarkerEntry opn2_metaMarker(struct OPN2_MIDIPlayer *device, size_t index); + + + + +/*Take a sample buffer and iterate MIDI timers */ +extern int opn2_play(struct OPN2_MIDIPlayer *device, int sampleCount, short out[]); + +/*Generate audio output from chip emulators without iteration of MIDI timers.*/ +extern int opn2_generate(struct OPN2_MIDIPlayer *device, int sampleCount, short *out); + +/** + * @brief Periodic tick handler. + * @param device + * @param seconds seconds since last call + * @param granularity don't expect intervals smaller than this, in seconds + * @return desired number of seconds until next call + * + * Use it for Hardware OPL3 mode or when you want to process events differently from opn2_play() function. + * DON'T USE IT TOGETHER WITH opn2_play()!!! + */ +extern double opn2_tickEvents(struct OPN2_MIDIPlayer *device, double seconds, double granuality); + +/*Returns 1 if music position has reached end*/ +extern int opn2_atEnd(struct OPN2_MIDIPlayer *device); + +/**RealTime**/ + +/*Force Off all notes on all channels*/ +extern void opn2_panic(struct OPN2_MIDIPlayer *device); + +/*Reset states of all controllers on all MIDI channels*/ +extern void opn2_rt_resetState(struct OPN2_MIDIPlayer *device); + +/*Turn specific MIDI note ON*/ +extern int opn2_rt_noteOn(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 note, OPN2_UInt8 velocity); + +/*Turn specific MIDI note OFF*/ +extern void opn2_rt_noteOff(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 note); + +/*Set note after-touch*/ +extern void opn2_rt_noteAfterTouch(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 note, OPN2_UInt8 atVal); +/*Set channel after-touch*/ +extern void opn2_rt_channelAfterTouch(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 atVal); + +/*Apply controller change*/ +extern void opn2_rt_controllerChange(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 type, OPN2_UInt8 value); + +/*Apply patch change*/ +extern void opn2_rt_patchChange(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 patch); + +/*Apply pitch bend change*/ +extern void opn2_rt_pitchBend(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt16 pitch); +/*Apply pitch bend change*/ +extern void opn2_rt_pitchBendML(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 msb, OPN2_UInt8 lsb); + +/*Change LSB of the bank*/ +extern void opn2_rt_bankChangeLSB(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 lsb); +/*Change MSB of the bank*/ +extern void opn2_rt_bankChangeMSB(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_UInt8 msb); +/*Change bank by absolute signed value*/ +extern void opn2_rt_bankChange(struct OPN2_MIDIPlayer *device, OPN2_UInt8 channel, OPN2_SInt16 bank); + + +/**Hooks**/ + +typedef void (*OPN2_RawEventHook)(void *userdata, OPN2_UInt8 type, OPN2_UInt8 subtype, OPN2_UInt8 channel, const OPN2_UInt8 *data, size_t len); +typedef void (*OPN2_NoteHook)(void *userdata, int adlchn, int note, int ins, int pressure, double bend); +typedef void (*OPN2_DebugMessageHook)(void *userdata, const char *fmt, ...); + +/* Set raw MIDI event hook */ +extern void opn2_setRawEventHook(struct OPN2_MIDIPlayer *device, OPN2_RawEventHook rawEventHook, void *userData); + +/* Set note hook */ +extern void opn2_setNoteHook(struct OPN2_MIDIPlayer *device, OPN2_NoteHook noteHook, void *userData); + +/* Set debug message hook */ +extern void opn2_setDebugMessageHook(struct OPN2_MIDIPlayer *device, OPN2_DebugMessageHook debugMessageHook, void *userData); + +#ifdef __cplusplus +} +#endif + +#endif /* OPNMIDI_H */ diff --git a/src/sound/opnmidi/opnmidi_load.cpp b/src/sound/opnmidi/opnmidi_load.cpp new file mode 100644 index 000000000..c17e989de --- /dev/null +++ b/src/sound/opnmidi/opnmidi_load.cpp @@ -0,0 +1,557 @@ +/* + * libOPNMIDI is a free MIDI to WAV conversion library with OPN2 (YM2612) emulation + * + * MIDI parser and player (Original code from ADLMIDI): Copyright (c) 2010-2014 Joel Yliluoma + * OPNMIDI Library and YM2612 support: Copyright (c) 2017-2018 Vitaly Novichkov + * + * Library is based on the ADLMIDI, a MIDI player for Linux and Windows with OPL3 emulation: + * http://iki.fi/bisqwit/source/adlmidi.html + * + * 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 3 of the License, or + * 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, see . + */ + +#include "opnmidi_private.hpp" + +#include "opnmidi_mus2mid.h" +#include "opnmidi_xmi2mid.h" + +uint64_t OPNMIDIplay::ReadBEint(const void *buffer, size_t nbytes) +{ + uint64_t result = 0; + const unsigned char *data = reinterpret_cast(buffer); + + for(unsigned n = 0; n < nbytes; ++n) + result = (result << 8) + data[n]; + + return result; +} + +uint64_t OPNMIDIplay::ReadLEint(const void *buffer, size_t nbytes) +{ + uint64_t result = 0; + const unsigned char *data = reinterpret_cast(buffer); + + for(unsigned n = 0; n < nbytes; ++n) + result = result + static_cast(data[n] << (n * 8)); + + return result; +} + +//uint64_t OPNMIDIplay::ReadVarLenEx(size_t tk, bool &ok) +//{ +// uint64_t result = 0; +// ok = false; + +// for(;;) +// { +// if(tk >= TrackData.size()) +// return 1; + +// if(tk >= CurrentPosition.track.size()) +// return 2; + +// size_t ptr = CurrentPosition.track[tk].ptr; + +// if(ptr >= TrackData[tk].size()) +// return 3; + +// unsigned char byte = TrackData[tk][CurrentPosition.track[tk].ptr++]; +// result = (result << 7) + (byte & 0x7F); + +// if(!(byte & 0x80)) break; +// } + +// ok = true; +// return result; +//} + +bool OPNMIDIplay::LoadBank(const std::string &filename) +{ + fileReader file; + file.openFile(filename.c_str()); + return LoadBank(file); +} + +bool OPNMIDIplay::LoadBank(const void *data, size_t size) +{ + fileReader file; + file.openData(data, (size_t)size); + return LoadBank(file); +} + +size_t readU16BE(OPNMIDIplay::fileReader &fr, uint16_t &out) +{ + uint8_t arr[2]; + size_t ret = fr.read(arr, 1, 2); + out = arr[1]; + out |= ((arr[0] << 8) & 0xFF00); + return ret; +} + +size_t readS16BE(OPNMIDIplay::fileReader &fr, int16_t &out) +{ + uint8_t arr[2]; + size_t ret = fr.read(arr, 1, 2); + out = *reinterpret_cast(&arr[0]); + out *= 1 << 8; + out |= arr[1]; + return ret; +} + +int16_t toSint16BE(uint8_t *arr) +{ + int16_t num = *reinterpret_cast(&arr[0]); + num *= 1 << 8; + num |= arr[1]; + return num; +} + +static uint16_t toUint16LE(const uint8_t *arr) +{ + uint16_t num = arr[0]; + num |= ((arr[1] << 8) & 0xFF00); + return num; +} + +static uint16_t toUint16BE(const uint8_t *arr) +{ + uint16_t num = arr[1]; + num |= ((arr[0] << 8) & 0xFF00); + return num; +} + + +static const char *wopn2_magic1 = "WOPN2-BANK\0"; +static const char *wopn2_magic2 = "WOPN2-B2NK\0"; + +#define WOPL_INST_SIZE_V1 65 +#define WOPL_INST_SIZE_V2 69 + +static const uint16_t latest_version = 2; + +bool OPNMIDIplay::LoadBank(OPNMIDIplay::fileReader &fr) +{ + size_t fsize; + ADL_UNUSED(fsize); + if(!fr.isValid()) + { + errorStringOut = "Can't load bank file: Invalid data stream!"; + return false; + } + + char magic[32]; + std::memset(magic, 0, 32); + uint16_t version = 1; + + uint16_t count_melodic_banks = 1; + uint16_t count_percusive_banks = 1; + + if(fr.read(magic, 1, 11) != 11) + { + errorStringOut = "Can't load bank file: Can't read magic number!"; + return false; + } + + bool is1 = std::strncmp(magic, wopn2_magic1, 11) == 0; + bool is2 = std::strncmp(magic, wopn2_magic2, 11) == 0; + + if(!is1 && !is2) + { + errorStringOut = "Can't load bank file: Invalid magic number!"; + return false; + } + + if(is2) + { + uint8_t ver[2]; + if(fr.read(ver, 1, 2) != 2) + { + errorStringOut = "Can't load bank file: Can't read version number!"; + return false; + } + version = toUint16LE(ver); + if(version < 2 || version > latest_version) + { + errorStringOut = "Can't load bank file: unsupported WOPN version!"; + return false; + } + } + + opn.dynamic_instruments.clear(); + opn.dynamic_metainstruments.clear(); + if((readU16BE(fr, count_melodic_banks) != 2) || (readU16BE(fr, count_percusive_banks) != 2)) + { + errorStringOut = "Can't load bank file: Can't read count of banks!"; + return false; + } + + if((count_melodic_banks < 1) || (count_percusive_banks < 1)) + { + errorStringOut = "Custom bank: Too few banks in this file!"; + return false; + } + + if(fr.read(&opn.regLFO, 1, 1) != 1) + { + errorStringOut = "Can't load bank file: Can't read LFO registry state!"; + return false; + } + + opn.dynamic_melodic_banks.clear(); + opn.dynamic_percussion_banks.clear(); + + if(version >= 2)//Read bank meta-entries + { + for(uint16_t i = 0; i < count_melodic_banks; i++) + { + uint8_t bank_meta[34]; + if(fr.read(bank_meta, 1, 34) != 34) + { + opn.dynamic_melodic_banks.clear(); + errorStringOut = "Custom bank: Fail to read melodic bank meta-data!"; + return false; + } + uint16_t bank = uint16_t(bank_meta[33]) * 256 + uint16_t(bank_meta[32]); + size_t offset = opn.dynamic_melodic_banks.size(); + opn.dynamic_melodic_banks[bank] = offset; + //strncpy(bankMeta.name, char_p(bank_meta), 32); + } + + for(uint16_t i = 0; i < count_percusive_banks; i++) + { + uint8_t bank_meta[34]; + if(fr.read(bank_meta, 1, 34) != 34) + { + opn.dynamic_melodic_banks.clear(); + opn.dynamic_percussion_banks.clear(); + errorStringOut = "Custom bank: Fail to read percussion bank meta-data!"; + return false; + } + uint16_t bank = uint16_t(bank_meta[33]) * 256 + uint16_t(bank_meta[32]); + size_t offset = opn.dynamic_percussion_banks.size(); + opn.dynamic_percussion_banks[bank] = offset; + //strncpy(bankMeta.name, char_p(bank_meta), 32); + } + } + + opn.dynamic_percussion_offset = count_melodic_banks * 128; + uint16_t total = 128 * count_melodic_banks + 128 * count_percusive_banks; + + for(uint16_t i = 0; i < total; i++) + { + opnInstData data; + opnInstMeta meta; + uint8_t idata[WOPL_INST_SIZE_V2]; + + size_t readSize = version >= 2 ? WOPL_INST_SIZE_V2 : WOPL_INST_SIZE_V1; + if(fr.read(idata, 1, readSize) != readSize) + { + opn.dynamic_instruments.clear(); + opn.dynamic_metainstruments.clear(); + opn.dynamic_melodic_banks.clear(); + opn.dynamic_percussion_banks.clear(); + errorStringOut = "Can't load bank file: Failed to read instrument data"; + return false; + } + data.finetune = toSint16BE(idata + 32); + //Percussion instrument note number or a "fixed note sound" + meta.tone = idata[34]; + data.fbalg = idata[35]; + data.lfosens = idata[36]; + for(size_t op = 0; op < 4; op++) + { + size_t off = 37 + op * 7; + std::memcpy(data.OPS[op].data, idata + off, 7); + } + if(version >= 2) + { + meta.ms_sound_kon = toUint16BE(idata + 65); + meta.ms_sound_koff = toUint16BE(idata + 67); + } + else + { + meta.ms_sound_kon = 1000; + meta.ms_sound_koff = 500; + } + + meta.opnno1 = uint16_t(opn.dynamic_instruments.size()); + meta.opnno2 = uint16_t(opn.dynamic_instruments.size()); + + /* Junk, delete later */ + meta.flags = 0; + meta.fine_tune = 0.0; + /* Junk, delete later */ + + opn.dynamic_instruments.push_back(data); + opn.dynamic_metainstruments.push_back(meta); + } + + applySetup(); + + return true; +} + +bool OPNMIDIplay::LoadMIDI(const std::string &filename) +{ + fileReader file; + file.openFile(filename.c_str()); + if(!LoadMIDI(file)) + return false; + return true; +} + +bool OPNMIDIplay::LoadMIDI(const void *data, size_t size) +{ + fileReader file; + file.openData(data, size); + return LoadMIDI(file); +} + +bool OPNMIDIplay::LoadMIDI(OPNMIDIplay::fileReader &fr) +{ + size_t fsize; + ADL_UNUSED(fsize); + //! Temp buffer for conversion + AdlMIDI_CPtr cvt_buf; + errorString.clear(); + + if(opn.dynamic_instruments.empty()) + { + errorStringOut = "Bank is not set! Please load any instruments bank by using of adl_openBankFile() or adl_openBankData() functions!"; + return false; + } + + if(!fr.isValid()) + { + errorStringOut = "Invalid data stream!\n"; + #ifndef _WIN32 + errorStringOut += std::strerror(errno); + #endif + return false; + } + + /**** Set all properties BEFORE starting of actial file reading! ****/ + applySetup(); + + atEnd = false; + loopStart = true; + invalidLoop = false; + + bool is_GMF = false; // GMD/MUS files (ScummVM) + bool is_RSXX = false; // RSXX, such as Cartooners + + const size_t HeaderSize = 4 + 4 + 2 + 2 + 2; // 14 + char HeaderBuf[HeaderSize] = ""; + size_t DeltaTicks = 192, TrackCount = 1; + +riffskip: + fsize = fr.read(HeaderBuf, 1, HeaderSize); + + if(std::memcmp(HeaderBuf, "RIFF", 4) == 0) + { + fr.seek(6l, SEEK_CUR); + goto riffskip; + } + + if(std::memcmp(HeaderBuf, "GMF\x1", 4) == 0) + { + // GMD/MUS files (ScummVM) + fr.seek(7 - static_cast(HeaderSize), SEEK_CUR); + is_GMF = true; + } + else if(std::memcmp(HeaderBuf, "MUS\x1A", 4) == 0) + { + // MUS/DMX files (Doom) + fr.seek(0, SEEK_END); + size_t mus_len = fr.tell(); + fr.seek(0, SEEK_SET); + uint8_t *mus = (uint8_t *)malloc(mus_len); + if(!mus) + { + errorStringOut = "Out of memory!"; + return false; + } + fr.read(mus, 1, mus_len); + //Close source stream + fr.close(); + + uint8_t *mid = NULL; + uint32_t mid_len = 0; + int m2mret = OpnMidi_mus2midi(mus, static_cast(mus_len), + &mid, &mid_len, 0); + if(mus) free(mus); + if(m2mret < 0) + { + errorStringOut = "Invalid MUS/DMX data format!"; + return false; + } + cvt_buf.reset(mid); + //Open converted MIDI file + fr.openData(mid, static_cast(mid_len)); + //Re-Read header again! + goto riffskip; + } + else if(std::memcmp(HeaderBuf, "FORM", 4) == 0) + { + if(std::memcmp(HeaderBuf + 8, "XDIR", 4) != 0) + { + fr.close(); + errorStringOut = fr._fileName + ": Invalid format\n"; + return false; + } + + fr.seek(0, SEEK_END); + size_t mus_len = fr.tell(); + fr.seek(0, SEEK_SET); + uint8_t *mus = (uint8_t*)malloc(mus_len); + if(!mus) + { + errorStringOut = "Out of memory!"; + return false; + } + fr.read(mus, 1, mus_len); + //Close source stream + fr.close(); + + uint8_t *mid = NULL; + uint32_t mid_len = 0; + int m2mret = OpnMidi_xmi2midi(mus, static_cast(mus_len), + &mid, &mid_len, XMIDI_CONVERT_NOCONVERSION); + if(mus) free(mus); + if(m2mret < 0) + { + errorStringOut = "Invalid XMI data format!"; + return false; + } + cvt_buf.reset(mid); + //Open converted MIDI file + fr.openData(mid, static_cast(mid_len)); + //Re-Read header again! + goto riffskip; + } + else + { + // Try to identify RSXX format + if(HeaderBuf[0] == 0x7D) + { + fr.seek(0x6D, SEEK_SET); + fr.read(HeaderBuf, 6, 1); + if(std::memcmp(HeaderBuf, "rsxx}u", 6) == 0) + { + is_RSXX = true; + fr.seek(0x7D, SEEK_SET); + TrackCount = 1; + DeltaTicks = 60; + opn.LogarithmicVolumes = true; + //opl.CartoonersVolumes = true; + opn.m_musicMode = OPN2::MODE_RSXX; + opn.m_volumeScale = OPN2::VOLUME_CMF; + } + } + + if(!is_RSXX) + { + if(std::memcmp(HeaderBuf, "MThd\0\0\0\6", 8) != 0) + { + fr.close(); + errorStringOut = fr._fileName + ": Invalid format, Header signature is unknown!\n"; + return false; + } + + /*size_t Fmt = ReadBEint(HeaderBuf + 8, 2);*/ + TrackCount = (size_t)ReadBEint(HeaderBuf + 10, 2); + DeltaTicks = (size_t)ReadBEint(HeaderBuf + 12, 2); + } + } + + TrackData.clear(); + TrackData.resize(TrackCount, std::vector()); + //CurrentPosition.track.clear(); + //CurrentPosition.track.resize(TrackCount); + InvDeltaTicks = fraction(1, 1000000l * static_cast(DeltaTicks)); + //Tempo = 1000000l * InvDeltaTicks; + Tempo = fraction(1, static_cast(DeltaTicks)); + static const unsigned char EndTag[4] = {0xFF, 0x2F, 0x00, 0x00}; + size_t totalGotten = 0; + + for(size_t tk = 0; tk < TrackCount; ++tk) + { + // Read track header + size_t TrackLength; + { + if(is_GMF || is_RSXX) // Take the rest of the file + { + size_t pos = fr.tell(); + fr.seek(0, SEEK_END); + TrackLength = fr.tell() - pos; + fr.seek(static_cast(pos), SEEK_SET); + } + else + { + fsize = fr.read(HeaderBuf, 1, 8); + if(std::memcmp(HeaderBuf, "MTrk", 4) != 0) + { + fr.close(); + errorStringOut = fr._fileName + ": Invalid format, MTrk signature is not found!\n"; + return false; + } + TrackLength = (size_t)ReadBEint(HeaderBuf + 4, 4); + } + + // Read track data + TrackData[tk].resize(TrackLength); + fsize = fr.read(&TrackData[tk][0], 1, TrackLength); + totalGotten += fsize; + + if(is_GMF/*|| is_MUS*/) // Note: CMF does include the track end tag. + TrackData[tk].insert(TrackData[tk].end(), EndTag + 0, EndTag + 4); + if(is_RSXX)//Finalize raw track data with a zero + TrackData[tk].push_back(0); + + //bool ok = false; + //// Read next event time + //uint64_t tkDelay = ReadVarLenEx(tk, ok); + //if(ok) + // CurrentPosition.track[tk].delay = tkDelay; + //else + //{ + // std::stringstream msg; + // msg << fr._fileName << ": invalid variable length in the track " << tk << "! (error code " << tkDelay << ")"; + // OPN2MIDI_ErrorString = msg.str(); + // return false; + //} + } + } + + for(size_t tk = 0; tk < TrackCount; ++tk) + totalGotten += TrackData[tk].size(); + + if(totalGotten == 0) + { + errorStringOut = fr._fileName + ": Empty track data"; + return false; + } + + //Build new MIDI events table (ALPHA!!!) + if(!buildTrackData()) + { + errorStringOut = fr._fileName + ": MIDI data parsing error has occouped!\n" + errorString; + return false; + } + + opn.Reset(m_setup.PCM_RATE); // Reset AdLib + ch.clear(); + ch.resize(opn.NumChannels); + return true; +} diff --git a/src/sound/opnmidi/opnmidi_midiplay.cpp b/src/sound/opnmidi/opnmidi_midiplay.cpp new file mode 100644 index 000000000..8495a5831 --- /dev/null +++ b/src/sound/opnmidi/opnmidi_midiplay.cpp @@ -0,0 +1,2600 @@ +/* + * libOPNMIDI is a free MIDI to WAV conversion library with OPN2 (YM2612) emulation + * + * MIDI parser and player (Original code from ADLMIDI): Copyright (c) 2010-2014 Joel Yliluoma + * OPNMIDI Library and YM2612 support: Copyright (c) 2017-2018 Vitaly Novichkov + * + * Library is based on the ADLMIDI, a MIDI player for Linux and Windows with OPL3 emulation: + * http://iki.fi/bisqwit/source/adlmidi.html + * + * 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 3 of the License, or + * 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, see . + */ + +#include "opnmidi_private.hpp" + +// Mapping from MIDI volume level to OPL level value. + +static const uint32_t DMX_volume_mapping_table[] = +{ + 0, 1, 3, 5, 6, 8, 10, 11, + 13, 14, 16, 17, 19, 20, 22, 23, + 25, 26, 27, 29, 30, 32, 33, 34, + 36, 37, 39, 41, 43, 45, 47, 49, + 50, 52, 54, 55, 57, 59, 60, 61, + 63, 64, 66, 67, 68, 69, 71, 72, + 73, 74, 75, 76, 77, 79, 80, 81, + 82, 83, 84, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 92, 93, 94, 95, + 96, 96, 97, 98, 99, 99, 100, 101, + 101, 102, 103, 103, 104, 105, 105, 106, + 107, 107, 108, 109, 109, 110, 110, 111, + 112, 112, 113, 113, 114, 114, 115, 115, + 116, 117, 117, 118, 118, 119, 119, 120, + 120, 121, 121, 122, 122, 123, 123, 123, + 124, 124, 125, 125, 126, 126, 127, 127, + //Protection entries to avoid crash if value more than 127 + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, +}; + +static const uint8_t W9X_volume_mapping_table[32] = +{ + 63, 63, 40, 36, 32, 28, 23, 21, + 19, 17, 15, 14, 13, 12, 11, 10, + 9, 8, 7, 6, 5, 5, 4, 4, + 3, 3, 2, 2, 1, 1, 0, 0 +}; + +inline bool isXgPercChannel(uint8_t msb, uint8_t lsb) +{ + return (msb == 0x7E || msb == 0x7F) && (lsb == 0); +} + +void OPNMIDIplay::OpnChannel::AddAge(int64_t ms) +{ + if(users.empty()) + koff_time_until_neglible = + std::max(int64_t(koff_time_until_neglible - ms), static_cast(-0x1FFFFFFFl)); + else + { + koff_time_until_neglible = 0; + + for(users_t::iterator i = users.begin(); i != users.end(); ++i) + { + i->second.kon_time_until_neglible = + std::max(i->second.kon_time_until_neglible - ms, static_cast(-0x1FFFFFFFl)); + i->second.vibdelay += ms; + } + } +} + +OPNMIDIplay::MidiEvent::MidiEvent() : + type(T_UNKNOWN), + subtype(T_UNKNOWN), + channel(0), + isValid(1), + absPosition(0) +{} + + +OPNMIDIplay::MidiTrackRow::MidiTrackRow() : + time(0.0), + delay(0), + absPos(0), + timeDelay(0.0) +{} + +void OPNMIDIplay::MidiTrackRow::reset() +{ + time = 0.0; + delay = 0; + absPos = 0; + timeDelay = 0.0; + events.clear(); +} + +void OPNMIDIplay::MidiTrackRow::sortEvents(bool *noteStates) +{ + typedef std::vector EvtArr; + EvtArr metas; + EvtArr noteOffs; + EvtArr controllers; + EvtArr anyOther; + + metas.reserve(events.size()); + noteOffs.reserve(events.size()); + controllers.reserve(events.size()); + anyOther.reserve(events.size()); + + for(size_t i = 0; i < events.size(); i++) + { + if(events[i].type == MidiEvent::T_NOTEOFF) + noteOffs.push_back(events[i]); + else if((events[i].type == MidiEvent::T_CTRLCHANGE) + || (events[i].type == MidiEvent::T_PATCHCHANGE) + || (events[i].type == MidiEvent::T_WHEEL) + || (events[i].type == MidiEvent::T_CHANAFTTOUCH)) + { + controllers.push_back(events[i]); + } + else if((events[i].type == MidiEvent::T_SPECIAL) && (events[i].subtype == MidiEvent::ST_MARKER)) + metas.push_back(events[i]); + else + anyOther.push_back(events[i]); + } + + /* + * If Note-Off and it's Note-On is on the same row - move this damned note off down! + */ + if(noteStates) + { + std::set markAsOn; + for(size_t i = 0; i < anyOther.size(); i++) + { + const MidiEvent e = anyOther[i]; + if(e.type == MidiEvent::T_NOTEON) + { + const size_t note_i = (e.channel * 255) + (e.data[0] & 0x7F); + //Check, was previously note is on or off + bool wasOn = noteStates[note_i]; + markAsOn.insert(note_i); + // Detect zero-length notes are following previously pressed note + int noteOffsOnSameNote = 0; + for(EvtArr::iterator j = noteOffs.begin(); j != noteOffs.end();) + { + //If note was off, and note-off on same row with note-on - move it down! + if( + ((*j).channel == e.channel) && + ((*j).data[0] == e.data[0]) + ) + { + //If note is already off OR more than one note-off on same row and same note + if(!wasOn || (noteOffsOnSameNote != 0)) + { + anyOther.push_back(*j); + j = noteOffs.erase(j); + markAsOn.erase(note_i); + continue; + } else { + //When same row has many note-offs on same row + //that means a zero-length note follows previous note + //it must be shuted down + noteOffsOnSameNote++; + } + } + j++; + } + } + } + + //Mark other notes as released + for(EvtArr::iterator j = noteOffs.begin(); j != noteOffs.end(); j++) + { + size_t note_i = (j->channel * 255) + (j->data[0] & 0x7F); + noteStates[note_i] = false; + } + + for(std::set::iterator j = markAsOn.begin(); j != markAsOn.end(); j++) + noteStates[*j] = true; + + } + /***********************************************************************************/ + + events.clear(); + events.insert(events.end(), noteOffs.begin(), noteOffs.end()); + events.insert(events.end(), metas.begin(), metas.end()); + events.insert(events.end(), controllers.begin(), controllers.end()); + events.insert(events.end(), anyOther.begin(), anyOther.end()); +} + +bool OPNMIDIplay::buildTrackData() +{ + fullSongTimeLength = 0.0; + loopStartTime = -1.0; + loopEndTime = -1.0; + musTitle.clear(); + musCopyright.clear(); + musTrackTitles.clear(); + musMarkers.clear(); + caugh_missing_instruments.clear(); + caugh_missing_banks_melodic.clear(); + caugh_missing_banks_percussion.clear(); + trackDataNew.clear(); + const size_t trackCount = TrackData.size(); + trackDataNew.resize(trackCount, MidiTrackQueue()); + + invalidLoop = false; + bool gotLoopStart = false, gotLoopEnd = false, gotLoopEventInThisRow = false; + //! Tick position of loop start tag + uint64_t loopStartTicks = 0; + //! Tick position of loop end tag + uint64_t loopEndTicks = 0; + //! Full length of song in ticks + uint64_t ticksSongLength = 0; + //! Cache for error message strign + char error[150]; + + CurrentPositionNew.track.clear(); + CurrentPositionNew.track.resize(trackCount); + + //! Caches note on/off states. + bool noteStates[16 * 255]; + /* This is required to carefully detect zero-length notes * + * and avoid a move of "note-off" event over "note-on" while sort. * + * Otherwise, after sort those notes will play infinite sound */ + + //Tempo change events + std::vector tempos; + + /* + * TODO: Make this be safer for memory in case of broken input data + * which may cause going away of available track data (and then give a crash!) + * + * POST: Check this more carefully for possible vulnuabilities are can crash this + */ + for(size_t tk = 0; tk < trackCount; ++tk) + { + uint64_t abs_position = 0; + int status = 0; + MidiEvent event; + bool ok = false; + uint8_t *end = TrackData[tk].data() + TrackData[tk].size(); + uint8_t *trackPtr = TrackData[tk].data(); + std::memset(noteStates, 0, sizeof(noteStates)); + + //Time delay that follows the first event in the track + { + MidiTrackRow evtPos; + if(opn.m_musicMode == OPN2::MODE_RSXX) + ok = true; + else + evtPos.delay = ReadVarLenEx(&trackPtr, end, ok); + if(!ok) + { + int len = std::sprintf(error, "buildTrackData: Can't read variable-length value at begin of track %d.\n", (int)tk); + if((len > 0) && (len < 150)) + errorString += std::string(error, (size_t)len); + return false; + } + + //HACK: Begin every track with "Reset all controllers" event to avoid controllers state break came from end of song + for(uint8_t chan = 0; chan < 16; chan++) + { + MidiEvent event; + event.type = MidiEvent::T_CTRLCHANGE; + event.channel = chan; + event.data.push_back(121); + event.data.push_back(0); + evtPos.events.push_back(event); + } + + evtPos.absPos = abs_position; + abs_position += evtPos.delay; + trackDataNew[tk].push_back(evtPos); + } + + MidiTrackRow evtPos; + do + { + event = parseEvent(&trackPtr, end, status); + if(!event.isValid) + { + int len = std::sprintf(error, "buildTrackData: Fail to parse event in the track %d.\n", (int)tk); + if((len > 0) && (len < 150)) + errorString += std::string(error, (size_t)len); + return false; + } + + evtPos.events.push_back(event); + if(event.type == MidiEvent::T_SPECIAL) + { + if(event.subtype == MidiEvent::ST_TEMPOCHANGE) + { + event.absPosition = abs_position; + tempos.push_back(event); + } + else if(!invalidLoop && (event.subtype == MidiEvent::ST_LOOPSTART)) + { + /* + * loopStart is invalid when: + * - starts together with loopEnd + * - appears more than one time in same MIDI file + */ + if(gotLoopStart || gotLoopEventInThisRow) + invalidLoop = true; + else + { + gotLoopStart = true; + loopStartTicks = abs_position; + } + //In this row we got loop event, register this! + gotLoopEventInThisRow = true; + } + else if(!invalidLoop && (event.subtype == MidiEvent::ST_LOOPEND)) + { + /* + * loopEnd is invalid when: + * - starts before loopStart + * - starts together with loopStart + * - appars more than one time in same MIDI file + */ + if(gotLoopEnd || gotLoopEventInThisRow) + invalidLoop = true; + else + { + gotLoopEnd = true; + loopEndTicks = abs_position; + } + //In this row we got loop event, register this! + gotLoopEventInThisRow = true; + } + } + + if(event.subtype != MidiEvent::ST_ENDTRACK)//Don't try to read delta after EndOfTrack event! + { + evtPos.delay = ReadVarLenEx(&trackPtr, end, ok); + if(!ok) + { + /* End of track has been reached! However, there is no EOT event presented */ + event.type = MidiEvent::T_SPECIAL; + event.subtype = MidiEvent::ST_ENDTRACK; + } + } + + if((evtPos.delay > 0) || (event.subtype == MidiEvent::ST_ENDTRACK)) + { + evtPos.absPos = abs_position; + abs_position += evtPos.delay; + evtPos.sortEvents(noteStates); + trackDataNew[tk].push_back(evtPos); + evtPos.reset(); + gotLoopEventInThisRow = false; + } + } + while((trackPtr <= end) && (event.subtype != MidiEvent::ST_ENDTRACK)); + + if(ticksSongLength < abs_position) + ticksSongLength = abs_position; + //Set the chain of events begin + if(trackDataNew[tk].size() > 0) + CurrentPositionNew.track[tk].pos = trackDataNew[tk].begin(); + } + + if(gotLoopStart && !gotLoopEnd) + { + gotLoopEnd = true; + loopEndTicks = ticksSongLength; + } + + //loopStart must be located before loopEnd! + if(loopStartTicks >= loopEndTicks) + invalidLoop = true; + + /********************************************************************************/ + //Calculate time basing on collected tempo events + /********************************************************************************/ + for(size_t tk = 0; tk < trackCount; ++tk) + { + fraction currentTempo = Tempo; + double time = 0.0; + uint64_t abs_position = 0; + size_t tempo_change_index = 0; + MidiTrackQueue &track = trackDataNew[tk]; + if(track.empty()) + continue;//Empty track is useless! + + #ifdef DEBUG_TIME_CALCULATION + std::fprintf(stdout, "\n============Track %" PRIuPTR "=============\n", tk); + std::fflush(stdout); + #endif + + MidiTrackRow *posPrev = &(*(track.begin()));//First element + for(MidiTrackQueue::iterator it = track.begin(); it != track.end(); it++) + { + #ifdef DEBUG_TIME_CALCULATION + bool tempoChanged = false; + #endif + MidiTrackRow &pos = *it; + if((posPrev != &pos) && //Skip first event + (!tempos.empty()) && //Only when in-track tempo events are available + (tempo_change_index < tempos.size()) + ) + { + // If tempo event is going between of current and previous event + if(tempos[tempo_change_index].absPosition <= pos.absPos) + { + //Stop points: begin point and tempo change points are before end point + std::vector points; + fraction t; + TempoChangePoint firstPoint = {posPrev->absPos, currentTempo}; + points.push_back(firstPoint); + + //Collect tempo change points between previous and current events + do + { + TempoChangePoint tempoMarker; + MidiEvent &tempoPoint = tempos[tempo_change_index]; + tempoMarker.absPos = tempoPoint.absPosition; + tempoMarker.tempo = InvDeltaTicks * fraction(ReadBEint(tempoPoint.data.data(), tempoPoint.data.size())); + points.push_back(tempoMarker); + tempo_change_index++; + } + while((tempo_change_index < tempos.size()) && + (tempos[tempo_change_index].absPosition <= pos.absPos)); + + // Re-calculate time delay of previous event + time -= posPrev->timeDelay; + posPrev->timeDelay = 0.0; + + for(size_t i = 0, j = 1; j < points.size(); i++, j++) + { + /* If one or more tempo events are appears between of two events, + * calculate delays between each tempo point, begin and end */ + uint64_t midDelay = 0; + //Delay between points + midDelay = points[j].absPos - points[i].absPos; + //Time delay between points + t = midDelay * currentTempo; + posPrev->timeDelay += t.value(); + + //Apply next tempo + currentTempo = points[j].tempo; + #ifdef DEBUG_TIME_CALCULATION + tempoChanged = true; + #endif + } + //Then calculate time between last tempo change point and end point + TempoChangePoint tailTempo = points.back(); + uint64_t postDelay = pos.absPos - tailTempo.absPos; + t = postDelay * currentTempo; + posPrev->timeDelay += t.value(); + + //Store Common time delay + posPrev->time = time; + time += posPrev->timeDelay; + } + } + + fraction t = pos.delay * currentTempo; + pos.timeDelay = t.value(); + pos.time = time; + time += pos.timeDelay; + + //Capture markers after time value calculation + for(size_t i = 0; i < pos.events.size(); i++) + { + MidiEvent &e = pos.events[i]; + if((e.type == MidiEvent::T_SPECIAL) && (e.subtype == MidiEvent::ST_MARKER)) + { + MIDI_MarkerEntry marker; + marker.label = std::string((char *)e.data.data(), e.data.size()); + marker.pos_ticks = pos.absPos; + marker.pos_time = pos.time; + musMarkers.push_back(marker); + } + } + + //Capture loop points time positions + if(!invalidLoop) + { + // Set loop points times + if(loopStartTicks == pos.absPos) + loopStartTime = pos.time; + else if(loopEndTicks == pos.absPos) + loopEndTime = pos.time; + } + + #ifdef DEBUG_TIME_CALCULATION + std::fprintf(stdout, "= %10" PRId64 " = %10f%s\n", pos.absPos, pos.time, tempoChanged ? " <----TEMPO CHANGED" : ""); + std::fflush(stdout); + #endif + + abs_position += pos.delay; + posPrev = &pos; + } + + if(time > fullSongTimeLength) + fullSongTimeLength = time; + } + + fullSongTimeLength += postSongWaitDelay; + //Set begin of the music + trackBeginPositionNew = CurrentPositionNew; + //Initial loop position will begin at begin of track until passing of the loop point + LoopBeginPositionNew = CurrentPositionNew; + + /********************************************************************************/ + //Resolve "hell of all times" of too short drum notes: + //move too short percussion note-offs far far away as possible + /********************************************************************************/ + #if 1 //Use this to record WAVEs for comparison before/after implementing of this + if(opn.m_musicMode == OPN2::MODE_MIDI)//Percussion fix is needed for MIDI only, not for IMF/RSXX or CMF + { + //! Minimal real time in seconds +#define DRUM_NOTE_MIN_TIME 0.03 + //! Minimal ticks count +#define DRUM_NOTE_MIN_TICKS 15 + struct NoteState + { + double delay; + uint64_t delayTicks; + bool isOn; + char ___pad[7]; + } drNotes[255]; + uint16_t banks[16]; + + for(size_t tk = 0; tk < trackCount; ++tk) + { + std::memset(drNotes, 0, sizeof(drNotes)); + std::memset(banks, 0, sizeof(banks)); + MidiTrackQueue &track = trackDataNew[tk]; + if(track.empty()) + continue;//Empty track is useless! + + for(MidiTrackQueue::iterator it = track.begin(); it != track.end(); it++) + { + MidiTrackRow &pos = *it; + + for(ssize_t e = 0; e < (ssize_t)pos.events.size(); e++) + { + MidiEvent *et = &pos.events[(size_t)e]; + + /* Set MSB/LSB bank */ + if(et->type == MidiEvent::T_CTRLCHANGE) + { + uint8_t ctrlno = et->data[0]; + uint8_t value = et->data[1]; + switch(ctrlno) + { + case 0: // Set bank msb (GM bank) + banks[et->channel] = uint16_t(uint16_t(value) << 8) | uint16_t(banks[et->channel] & 0x00FF); + break; + case 32: // Set bank lsb (XG bank) + banks[et->channel] = (banks[et->channel] & 0xFF00) | (uint16_t(value) & 0x00FF); + break; + } + continue; + } + + bool percussion = (et->channel == 9) || + banks[et->channel] == 0x7E00 || //XG SFX1/SFX2 channel (16128 signed decimal) + banks[et->channel] == 0x7F00; //XG Percussion channel (16256 signed decimal) + if(!percussion) + continue; + + if(et->type == MidiEvent::T_NOTEON) + { + uint8_t note = et->data[0] & 0x7F; + NoteState &ns = drNotes[note]; + ns.isOn = true; + ns.delay = 0.0; + ns.delayTicks = 0; + } + else if(et->type == MidiEvent::T_NOTEOFF) + { + uint8_t note = et->data[0] & 0x7F; + NoteState &ns = drNotes[note]; + if(ns.isOn) + { + ns.isOn = false; + if(ns.delayTicks < DRUM_NOTE_MIN_TICKS || ns.delay < DRUM_NOTE_MIN_TIME)//If note is too short + { + //Move it into next event position if that possible + for(MidiTrackQueue::iterator itNext = it; + itNext != track.end(); + itNext++) + { + MidiTrackRow &posN = *itNext; + if(ns.delayTicks > DRUM_NOTE_MIN_TICKS && ns.delay > DRUM_NOTE_MIN_TIME) + { + //Put note-off into begin of next event list + posN.events.insert(posN.events.begin(), pos.events[(size_t)e]); + //Renive this event from a current row + pos.events.erase(pos.events.begin() + (int)e); + e--; + break; + } + ns.delay += posN.timeDelay; + ns.delayTicks += posN.delay; + } + } + ns.delay = 0.0; + ns.delayTicks = 0; + } + } + } + + //Append time delays to sustaining notes + for(size_t no = 0; no < 128; no++) + { + NoteState &ns = drNotes[no]; + if(ns.isOn) + { + ns.delay += pos.timeDelay; + ns.delayTicks += pos.delay; + } + } + } + } +#undef DRUM_NOTE_MIN_TIME +#undef DRUM_NOTE_MIN_TICKS + } + #endif + + return true; +} + + + +OPNMIDIplay::OPNMIDIplay(unsigned long sampleRate): + //cmf_percussion_mode(false), + fullSongTimeLength(0.0), + postSongWaitDelay(1.0), + loopStartTime(-1.0), + loopEndTime(-1.0), + tempoMultiplier(1.0), + atEnd(false), + loopStart(false), + loopEnd(false), + invalidLoop(false) +{ + devices.clear(); + + m_setup.PCM_RATE = sampleRate; + m_setup.mindelay = 1.0 / (double)m_setup.PCM_RATE; + m_setup.maxdelay = 512.0 / (double)m_setup.PCM_RATE; + + m_setup.OpnBank = 0; + m_setup.NumCards = 2; + m_setup.LogarithmicVolumes = false; + m_setup.VolumeModel = OPNMIDI_VolumeModel_AUTO; + //m_setup.SkipForward = 0; + m_setup.loopingIsEnabled = false; + m_setup.ScaleModulators = 0; + m_setup.delay = 0.0; + m_setup.carry = 0.0; + m_setup.tick_skip_samples_delay = 0; + + applySetup(); + ChooseDevice("none"); + realTime_ResetState(); +} + +void OPNMIDIplay::applySetup() +{ + m_setup.tick_skip_samples_delay = 0; + + opn.ScaleModulators = m_setup.ScaleModulators; + opn.LogarithmicVolumes = m_setup.LogarithmicVolumes; + opn.m_musicMode = OPN2::MODE_MIDI; + opn.ChangeVolumeRangesModel(static_cast(m_setup.VolumeModel)); + if(m_setup.VolumeModel == OPNMIDI_VolumeModel_AUTO) + opn.m_volumeScale = OPN2::VOLUME_Generic; + + opn.NumCards = m_setup.NumCards; + + opn.Reset(m_setup.PCM_RATE); + ch.clear(); + ch.resize(opn.NumChannels); +} + +uint64_t OPNMIDIplay::ReadVarLen(uint8_t **ptr) +{ + uint64_t result = 0; + for(;;) + { + uint8_t byte = *((*ptr)++); + result = (result << 7) + (byte & 0x7F); + if(!(byte & 0x80)) + break; + } + return result; +} + +uint64_t OPNMIDIplay::ReadVarLenEx(uint8_t **ptr, uint8_t *end, bool &ok) +{ + uint64_t result = 0; + ok = false; + + for(;;) + { + if(*ptr >= end) + return 2; + unsigned char byte = *((*ptr)++); + result = (result << 7) + (byte & 0x7F); + if(!(byte & 0x80)) + break; + } + + ok = true; + return result; +} + + +double OPNMIDIplay::Tick(double s, double granularity) +{ + s *= tempoMultiplier; + #ifdef ENABLE_BEGIN_SILENCE_SKIPPING + if(CurrentPositionNew.began) + #endif + CurrentPositionNew.wait -= s; + CurrentPositionNew.absTimePosition += s; + + int antiFreezeCounter = 10000;//Limit 10000 loops to avoid freezing + while((CurrentPositionNew.wait <= granularity * 0.5) && (antiFreezeCounter > 0)) + { + //std::fprintf(stderr, "wait = %g...\n", CurrentPosition.wait); + if(!ProcessEventsNew()) + break; + if(CurrentPositionNew.wait <= 0.0) + antiFreezeCounter--; + } + + if(antiFreezeCounter <= 0) + CurrentPositionNew.wait += 1.0;/* Add extra 1 second when over 10000 events + with zero delay are been detected */ + + for(uint16_t c = 0; c < opn.NumChannels; ++c) + ch[c].AddAge(static_cast(s * 1000.0)); + + UpdateVibrato(s); + UpdateArpeggio(s); + + if(CurrentPositionNew.wait < 0.0)//Avoid negative delay value! + return 0.0; + + return CurrentPositionNew.wait; +} + +void OPNMIDIplay::TickIteratos(double s) +{ + for(uint16_t c = 0; c < opn.NumChannels; ++c) + ch[c].AddAge(static_cast(s * 1000.0)); + UpdateVibrato(s); + UpdateArpeggio(s); +} + +void OPNMIDIplay::seek(double seconds) +{ + if(seconds < 0.0) + return;//Seeking negative position is forbidden! :-P + const double granularity = m_setup.mindelay, + granualityHalf = granularity * 0.5, + s = seconds;//m_setup.delay < m_setup.maxdelay ? m_setup.delay : m_setup.maxdelay; + + /* Attempt to go away out of song end must rewind position to begin */ + if(seconds > fullSongTimeLength) + { + rewind(); + return; + } + + bool loopFlagState = m_setup.loopingIsEnabled; + // Turn loop pooints off because it causes wrong position rememberin on a quick seek + m_setup.loopingIsEnabled = false; + + /* + * Seeking search is similar to regular ticking, except of next things: + * - We don't processsing arpeggio and vibrato + * - To keep correctness of the state after seek, begin every search from begin + * - All sustaining notes must be killed + * - Ignore Note-On events + */ + rewind(); + + /* + * Set "loop Start" to false to prevent overwrite of loopStart position with + * seek destinition position + * + * TODO: Detect & set loopStart position on load time to don't break loop while seeking + */ + loopStart = false; + + while((CurrentPositionNew.absTimePosition < seconds) && + (CurrentPositionNew.absTimePosition < fullSongTimeLength)) + { + CurrentPositionNew.wait -= s; + CurrentPositionNew.absTimePosition += s; + int antiFreezeCounter = 10000;//Limit 10000 loops to avoid freezing + double dstWait = CurrentPositionNew.wait + granualityHalf; + while((CurrentPositionNew.wait <= granualityHalf)/*&& (antiFreezeCounter > 0)*/) + { + //std::fprintf(stderr, "wait = %g...\n", CurrentPosition.wait); + if(!ProcessEventsNew(true)) + break; + //Avoid freeze because of no waiting increasing in more than 10000 cycles + if(CurrentPositionNew.wait <= dstWait) + antiFreezeCounter--; + else + { + dstWait = CurrentPositionNew.wait + granualityHalf; + antiFreezeCounter = 10000; + } + } + if(antiFreezeCounter <= 0) + CurrentPositionNew.wait += 1.0;/* Add extra 1 second when over 10000 events + with zero delay are been detected */ + } + + if(CurrentPositionNew.wait < 0.0) + CurrentPositionNew.wait = 0.0; + + m_setup.loopingIsEnabled = loopFlagState; + m_setup.delay = CurrentPositionNew.wait; + m_setup.carry = 0.0; +} + +double OPNMIDIplay::tell() +{ + return CurrentPositionNew.absTimePosition; +} + +double OPNMIDIplay::timeLength() +{ + return fullSongTimeLength; +} + +double OPNMIDIplay::getLoopStart() +{ + return loopStartTime; +} + +double OPNMIDIplay::getLoopEnd() +{ + return loopEndTime; +} + +void OPNMIDIplay::rewind() +{ + Panic(); + KillSustainingNotes(-1, -1); + CurrentPositionNew = trackBeginPositionNew; + atEnd = false; + loopStart = true; + loopEnd = false; + //invalidLoop = false;//No more needed here as this flag is set on load time +} + +void OPNMIDIplay::setTempo(double tempo) +{ + tempoMultiplier = tempo; +} + +void OPNMIDIplay::realTime_ResetState() +{ + for(size_t ch = 0; ch < Ch.size(); ch++) + { + MIDIchannel &chan = Ch[ch]; + chan.volume = (opn.m_musicMode == OPN2::MODE_RSXX) ? 127 : 100; + chan.expression = 127; + chan.panning = 0xC0; + chan.vibrato = 0; + chan.sustain = 0; + chan.bend = 0.0; + chan.bendsense = 2 / 8192.0; + chan.vibpos = 0.0; + chan.vibdepth = 0.5 / 127.0; + chan.vibdelay = 0; + chan.lastlrpn = 0; + chan.lastmrpn = 0; + chan.nrpn = false; + chan.brightness = 127; + NoteUpdate_All(uint16_t(ch), Upd_All); + NoteUpdate_All(uint16_t(ch), Upd_Off); + } +} + +bool OPNMIDIplay::realTime_NoteOn(uint8_t channel, uint8_t note, uint8_t velocity) +{ + if((opn.m_musicMode == OPN2::MODE_RSXX) && (velocity != 0)) + { + // Check if this is just a note after-touch + MIDIchannel::activenoteiterator i = Ch[channel].activenotes.find(note); + if(i != Ch[channel].activenotes.end()) + { + i->second.vol = velocity; + NoteUpdate(channel, i, Upd_Volume); + return false; + } + } + + channel = channel % 16; + NoteOff(channel, note); + // On Note on, Keyoff the note first, just in case keyoff + // was omitted; this fixes Dance of sugar-plum fairy + // by Microsoft. Now that we've done a Keyoff, + // check if we still need to do a Keyon. + // vol=0 and event 8x are both Keyoff-only. + if(velocity == 0) + return false; + + size_t midiins = Ch[channel].patch; + bool isPercussion = (channel % 16 == 9); + bool isXgPercussion = false; + + uint16_t bank = 0; + if(Ch[channel].bank_msb || Ch[channel].bank_lsb) + { + bank = (uint16_t(Ch[channel].bank_msb) * 256) + uint16_t(Ch[channel].bank_lsb); + //0x7E00 - XG SFX1/SFX2 channel (16128 signed decimal) + //0x7F00 - XG Percussion channel (16256 signed decimal) + if(bank == 0x7E00 || bank == 0x7F00) + { + //Let XG SFX1/SFX2 bank will have LSB==1 (128...255 range in WOPN file) + //Let XG Percussion bank will use (0...127 range in WOPN file) + bank = (uint16_t)midiins + ((bank == 0x7E00) ? 128 : 0); // MIDI instrument defines the patch + midiins = opn.dynamic_percussion_offset + note; // Percussion instrument + isXgPercussion = true; + isPercussion = false; + } + } + + if(isPercussion) + { + bank = (uint16_t)midiins; // MIDI instrument defines the patch + midiins = opn.dynamic_percussion_offset + note; // Percussion instrument + } + + //Set bank bank + if(bank > 0) + { + if(isPercussion || isXgPercussion) + { + OPN2::BankMap::iterator b = opn.dynamic_percussion_banks.find(bank); + if(b != opn.dynamic_percussion_banks.end()) + midiins += b->second * 128; + else + if(hooks.onDebugMessage) + { + if(!caugh_missing_banks_melodic.count(bank)) + { + hooks.onDebugMessage(hooks.onDebugMessage_userData, "[%i] Playing missing percussion bank %i (patch %i)", channel, bank, midiins); + caugh_missing_banks_melodic.insert(bank); + } + } + } + else + { + OPN2::BankMap::iterator b = opn.dynamic_melodic_banks.find(bank); + if(b != opn.dynamic_melodic_banks.end()) + midiins += b->second * 128; + else + if(hooks.onDebugMessage) + { + if(!caugh_missing_banks_melodic.count(bank)) + { + hooks.onDebugMessage(hooks.onDebugMessage_userData, "[%i] Playing missing melodic bank %i (patch %i)", channel, bank, midiins); + caugh_missing_banks_melodic.insert(bank); + } + } + } + } + + /* + if(MidCh%16 == 9 || (midiins != 32 && midiins != 46 && midiins != 48 && midiins != 50)) + break; // HACK + if(midiins == 46) vol = (vol*7)/10; // HACK + if(midiins == 48 || midiins == 50) vol /= 4; // HACK + */ + //if(midiins == 56) vol = vol*6/10; // HACK + + const size_t meta = opn.GetAdlMetaNumber(midiins); + const opnInstMeta &ains = opn.GetAdlMetaIns(meta); + int16_t tone = note; + + if(ains.tone) + { + /*if(ains.tone < 20) + tone += ains.tone; + else*/ + if(ains.tone < 128) + tone = ains.tone; + else + tone -= ains.tone - 128; + } + + uint16_t i[2] = { ains.opnno1, ains.opnno2 }; + //bool pseudo_4op = ains.flags & opnInstMeta::Flag_Pseudo8op; + //if((opn.AdlPercussionMode == 1) && PercussionMap[midiins & 0xFF]) i[1] = i[0]; + + if(hooks.onDebugMessage) + { + if(!caugh_missing_instruments.count(static_cast(midiins)) && (ains.flags & opnInstMeta::Flag_NoSound)) + { + hooks.onDebugMessage(hooks.onDebugMessage_userData, "[%i] Playing missing instrument %i", channel, midiins); + caugh_missing_instruments.insert(static_cast(midiins)); + } + } + + // Allocate AdLib channel (the physical sound channel for the note) + int32_t adlchannel[2] = { -1, -1 }; + + for(uint32_t ccount = 0; ccount < 2; ++ccount) + { + if(ccount == 1) + { + if(i[0] == i[1]) + break; // No secondary channel + if(adlchannel[0] == -1) + break; // No secondary if primary failed + } + + int32_t c = -1; + int32_t bs = -0x7FFFFFFFl; + + for(size_t a = 0; a < (size_t)opn.NumChannels; ++a) + { + if(ccount == 1 && static_cast(a) == adlchannel[0]) continue; + // ^ Don't use the same channel for primary&secondary + // ===== Kept for future pseudo-8-op mode + //if(i[0] == i[1] || pseudo_4op) + //{ + // // Only use regular channels + // uint8_t expected_mode = 0; + // if(opn.AdlPercussionMode == 1) + // { + // if(cmf_percussion_mode) + // expected_mode = MidCh < 11 ? 0 : (3 + MidCh - 11); // CMF + // else + // expected_mode = PercussionMap[midiins & 0xFF]; + // } + // if(opn.four_op_category[a] != expected_mode) + // continue; + //} + int64_t s = CalculateAdlChannelGoodness(a, i[ccount], channel); + if(s > bs) + { + bs = (int32_t)s; // Best candidate wins + c = static_cast(a); + } + } + + if(c < 0) + { + if(hooks.onDebugMessage) + hooks.onDebugMessage(hooks.onDebugMessage_userData, + "ignored unplaceable note [bank %i, inst %i, note %i, MIDI channel %i]", + bank, Ch[channel].patch, note, channel); + continue; // Could not play this note. Ignore it. + } + + PrepareAdlChannelForNewNote(static_cast(c), i[ccount]); + adlchannel[ccount] = c; + } + + if(adlchannel[0] < 0 && adlchannel[1] < 0) + { + // The note could not be played, at all. + return false; + } + + //if(hooks.onDebugMessage) + // hooks.onDebugMessage(hooks.onDebugMessage_userData, "i1=%d:%d, i2=%d:%d", i[0],adlchannel[0], i[1],adlchannel[1]); + + // Allocate active note for MIDI channel + std::pair + ir = Ch[channel].activenotes.insert(std::make_pair(note, MIDIchannel::NoteInfo())); + ir.first->second.vol = velocity; + ir.first->second.tone = tone; + ir.first->second.midiins = midiins; + ir.first->second.insmeta = meta; + + for(unsigned ccount = 0; ccount < 2; ++ccount) + { + int32_t c = adlchannel[ccount]; + if(c < 0) + continue; + uint16_t chipChan = static_cast(adlchannel[ccount]); + ir.first->second.phys[chipChan] = i[ccount]; + } + NoteUpdate(channel, ir.first, Upd_All | Upd_Patch); + return true; +} + +void OPNMIDIplay::realTime_NoteOff(uint8_t channel, uint8_t note) +{ + channel = channel % 16; + NoteOff(channel, note); +} + +void OPNMIDIplay::realTime_NoteAfterTouch(uint8_t channel, uint8_t note, uint8_t atVal) +{ + channel = channel % 16; + MIDIchannel::activenoteiterator + i = Ch[channel].activenotes.find(note); + if(i == Ch[channel].activenotes.end()) + { + // Ignore touch if note is not active + return; + } + i->second.vol = 127 - atVal; + NoteUpdate(channel, i, Upd_Volume); +} + +void OPNMIDIplay::realTime_ChannelAfterTouch(uint8_t channel, uint8_t atVal) +{ + // TODO: Verify, is this correct action? + channel = channel % 16; + for(MIDIchannel::activenoteiterator + i = Ch[channel].activenotes.begin(); + i != Ch[channel].activenotes.end(); + ++i) + { + // Set this pressure to all active notes on the channel + i->second.vol = 127 - atVal; + } + + NoteUpdate_All(channel, Upd_Volume); +} + +void OPNMIDIplay::realTime_Controller(uint8_t channel, uint8_t type, uint8_t value) +{ + channel = channel % 16; + switch(type) + { + case 1: // Adjust vibrato + //UI.PrintLn("%u:vibrato %d", MidCh,value); + Ch[channel].vibrato = value; + break; + + case 0: // Set bank msb (GM bank) + Ch[channel].bank_msb = value; + Ch[channel].is_xg_percussion = isXgPercChannel(Ch[channel].bank_msb, Ch[channel].bank_lsb); + break; + + case 32: // Set bank lsb (XG bank) + Ch[channel].bank_lsb = value; + Ch[channel].is_xg_percussion = isXgPercChannel(Ch[channel].bank_msb, Ch[channel].bank_lsb); + break; + + case 5: // Set portamento msb + Ch[channel].portamento = static_cast((Ch[channel].portamento & 0x7F) | (value << 7)); + //UpdatePortamento(MidCh); + break; + + case 37: // Set portamento lsb + Ch[channel].portamento = (Ch[channel].portamento & 0x3F80) | (value); + //UpdatePortamento(MidCh); + break; + + case 65: // Enable/disable portamento + // value >= 64 ? enabled : disabled + //UpdatePortamento(MidCh); + break; + + case 7: // Change volume + Ch[channel].volume = value; + NoteUpdate_All(channel, Upd_Volume); + break; + + case 74: // Change brightness + Ch[channel].brightness = value; + NoteUpdate_All(channel, Upd_Volume); + break; + + case 64: // Enable/disable sustain + Ch[channel].sustain = value; + if(!value) KillSustainingNotes(channel); + break; + + case 11: // Change expression (another volume factor) + Ch[channel].expression = value; + NoteUpdate_All(channel, Upd_Volume); + break; + + case 10: // Change panning + Ch[channel].panning = 0x00; + if(value < 64 + 32) Ch[channel].panning |= 0x80; + if(value >= 64 - 32) Ch[channel].panning |= 0x40; + NoteUpdate_All(channel, Upd_Pan); + break; + + case 121: // Reset all controllers + Ch[channel].bend = 0; + Ch[channel].volume = 100; + Ch[channel].expression = 127; + Ch[channel].sustain = 0; + Ch[channel].vibrato = 0; + Ch[channel].vibspeed = 2 * 3.141592653 * 5.0; + Ch[channel].vibdepth = 0.5 / 127; + Ch[channel].vibdelay = 0; + Ch[channel].panning = 0xC0; + Ch[channel].portamento = 0; + Ch[channel].brightness = 127; + //UpdatePortamento(MidCh); + NoteUpdate_All(channel, Upd_Pan + Upd_Volume + Upd_Pitch); + // Kill all sustained notes + KillSustainingNotes(channel); + break; + + case 123: // All notes off + NoteUpdate_All(channel, Upd_Off); + break; + + case 91: + break; // Reverb effect depth. We don't do per-channel reverb. + + case 92: + break; // Tremolo effect depth. We don't do... + + case 93: + break; // Chorus effect depth. We don't do. + + case 94: + break; // Celeste effect depth. We don't do. + + case 95: + break; // Phaser effect depth. We don't do. + + case 98: + Ch[channel].lastlrpn = value; + Ch[channel].nrpn = true; + break; + + case 99: + Ch[channel].lastmrpn = value; + Ch[channel].nrpn = true; + break; + + case 100: + Ch[channel].lastlrpn = value; + Ch[channel].nrpn = false; + break; + + case 101: + Ch[channel].lastmrpn = value; + Ch[channel].nrpn = false; + break; + + case 113: + break; // Related to pitch-bender, used by missimp.mid in Duke3D + + case 6: + SetRPN(channel, value, true); + break; + + case 38: + SetRPN(channel, value, false); + break; + + //case 103: + // cmf_percussion_mode = (value != 0); + // break; // CMF (ctrl 0x67) rhythm mode + + default: + break; + //UI.PrintLn("Ctrl %d <- %d (ch %u)", ctrlno, value, MidCh); + } +} + +void OPNMIDIplay::realTime_PatchChange(uint8_t channel, uint8_t patch) +{ + channel = channel % 16; + Ch[channel].patch = patch; +} + +void OPNMIDIplay::realTime_PitchBend(uint8_t channel, uint16_t pitch) +{ + channel = channel % 16; + Ch[channel].bend = (uint32_t(pitch) - 8192) * Ch[channel].bendsense; + NoteUpdate_All(channel, Upd_Pitch); +} + +void OPNMIDIplay::realTime_PitchBend(uint8_t channel, uint8_t msb, uint8_t lsb) +{ + channel = channel % 16; + Ch[channel].bend = (int(lsb) + int(msb) * 128 - 8192) * Ch[channel].bendsense; + NoteUpdate_All(channel, Upd_Pitch); +} + +void OPNMIDIplay::realTime_BankChangeLSB(uint8_t channel, uint8_t lsb) +{ + channel = channel % 16; + Ch[channel].bank_lsb = lsb; +} + +void OPNMIDIplay::realTime_BankChangeMSB(uint8_t channel, uint8_t msb) +{ + channel = channel % 16; + Ch[channel].bank_msb = msb; +} + +void OPNMIDIplay::realTime_BankChange(uint8_t channel, uint16_t bank) +{ + channel = channel % 16; + Ch[channel].bank_lsb = uint8_t(bank & 0xFF); + Ch[channel].bank_msb = uint8_t((bank >> 8) & 0xFF); +} + +void OPNMIDIplay::realTime_panic() +{ + Panic(); + KillSustainingNotes(-1, -1); +} + + +void OPNMIDIplay::NoteUpdate(uint16_t MidCh, + OPNMIDIplay::MIDIchannel::activenoteiterator i, + unsigned props_mask, + int32_t select_adlchn) +{ + MIDIchannel::NoteInfo &info = i->second; + const int16_t tone = info.tone; + const uint8_t vol = info.vol; + const size_t midiins = info.midiins; + const size_t insmeta = info.insmeta; + const opnInstMeta &ains = opn.GetAdlMetaIns(insmeta); + OpnChannel::Location my_loc; + my_loc.MidCh = MidCh; + my_loc.note = i->first; + + for(MIDIchannel::NoteInfo::PhysMap::iterator + jnext = info.phys.begin(); + jnext != info.phys.end(); + ) + { + MIDIchannel::NoteInfo::PhysMap::iterator j(jnext++); + uint16_t c = j->first; + const MIDIchannel::NoteInfo::Phys &ins = j->second; + + if(select_adlchn >= 0 && c != select_adlchn) continue; + + if(props_mask & Upd_Patch) + { + opn.Patch(c, ins); + OpnChannel::LocationData &d = ch[c].users[my_loc]; + d.sustained = false; // inserts if necessary + d.vibdelay = 0; + d.kon_time_until_neglible = ains.ms_sound_kon; + d.ins = ins; + } + } + + for(MIDIchannel::NoteInfo::PhysMap::iterator + jnext = info.phys.begin(); + jnext != info.phys.end(); + ) + { + MIDIchannel::NoteInfo::PhysMap::iterator j(jnext++); + uint16_t c = j->first; + const MIDIchannel::NoteInfo::Phys &ins = j->second; + + if(select_adlchn >= 0 && c != select_adlchn) continue; + + if(props_mask & Upd_Off) // note off + { + if(Ch[MidCh].sustain == 0) + { + OpnChannel::users_t::iterator k = ch[c].users.find(my_loc); + + if(k != ch[c].users.end()) + ch[c].users.erase(k); + + if(hooks.onNote) + hooks.onNote(hooks.onNote_userData, c, tone, (int)midiins, 0, 0.0); + + if(ch[c].users.empty()) + { + opn.NoteOff(c); + ch[c].koff_time_until_neglible = + ains.ms_sound_koff; + } + } + else + { + // Sustain: Forget about the note, but don't key it off. + // Also will avoid overwriting it very soon. + OpnChannel::LocationData &d = ch[c].users[my_loc]; + d.sustained = true; // note: not erased! + if(hooks.onNote) + hooks.onNote(hooks.onNote_userData, c, tone, (int)midiins, -1, 0.0); + } + + info.phys.erase(j); + continue; + } + + if(props_mask & Upd_Pan) + opn.Pan(c, Ch[MidCh].panning); + + if(props_mask & Upd_Volume) + { + uint32_t volume; + bool is_percussion = (MidCh == 9) || Ch[MidCh].is_xg_percussion; + uint8_t brightness = is_percussion ? 127 : Ch[MidCh].brightness; + + switch(opn.m_volumeScale) + { + case OPN2::VOLUME_Generic: + case OPN2::VOLUME_CMF: + { + volume = vol * Ch[MidCh].volume * Ch[MidCh].expression; + + /* If the channel has arpeggio, the effective volume of + * *this* instrument is actually lower due to timesharing. + * To compensate, add extra volume that corresponds to the + * time this note is *not* heard. + * Empirical tests however show that a full equal-proportion + * increment sounds wrong. Therefore, using the square root. + */ + //volume = (int)(volume * std::sqrt( (double) ch[c].users.size() )); + + if(opn.LogarithmicVolumes) + volume = volume * 127 / (2048383/*127 * 127 * 127*/); + else + { + // The formula below: SOLVE(V=127^3 * 2^( (A-63.49999) / 8), A) + volume = volume > 8725 ? static_cast((std::log(static_cast(volume)) * (11.541561) + (0.5 - 104.22845)) * 2.0) : 0; + // The incorrect formula below: SOLVE(V=127^3 * (2^(A/63)-1), A) + //opl.Touch_Real(c, volume>11210 ? 91.61112 * std::log(4.8819E-7*volume + 1.0)+0.5 : 0); + } + + opn.Touch_Real(c, volume, brightness); + //opl.Touch(c, volume); + } + break; + + case OPN2::VOLUME_DMX: + { + volume = 2 * ((Ch[MidCh].volume * Ch[MidCh].expression) * 127 / 16129) + 1; + //volume = 2 * (Ch[MidCh].volume) + 1; + volume = (DMX_volume_mapping_table[vol] * volume) >> 9; + opn.Touch_Real(c, volume, brightness); + } + break; + + case OPN2::VOLUME_APOGEE: + { + volume = ((Ch[MidCh].volume * Ch[MidCh].expression) * 127 / 16129); + volume = ((64 * (vol + 0x80)) * volume) >> 15; + //volume = ((63 * (vol + 0x80)) * Ch[MidCh].volume) >> 15; + volume *= 2;//OPN has 0~127 range + opn.Touch_Real(c, volume, brightness); + } + break; + + case OPN2::VOLUME_9X: + { + //volume = 63 - W9X_volume_mapping_table[(((vol * Ch[MidCh].volume /** Ch[MidCh].expression*/) * 127 / 16129 /*2048383*/) >> 2)]; + volume = 63 - W9X_volume_mapping_table[(((vol * Ch[MidCh].volume * Ch[MidCh].expression) * 127 / 2048383) >> 2)]; + //volume = W9X_volume_mapping_table[vol >> 2] + volume; + volume *= 2;//OPN has 0~127 range + opn.Touch_Real(c, volume, brightness); + } + break; + } + + /* DEBUG ONLY!!! + static uint32_t max = 0; + + if(volume == 0) + max = 0; + + if(volume > max) + max = volume; + + printf("%d\n", max); + fflush(stdout); + */ + } + + if(props_mask & Upd_Pitch) + { + OpnChannel::LocationData &d = ch[c].users[my_loc]; + + // Don't bend a sustained note + if(!d.sustained) + { + double bend = Ch[MidCh].bend + opn.GetAdlIns(ins).finetune; + double phase = 0.0; + + if((ains.flags & opnInstMeta::Flag_Pseudo8op) && ins == ains.opnno2) + { + phase = ains.fine_tune;//0.125; // Detune the note slightly (this is what Doom does) + } + + if(Ch[MidCh].vibrato && d.vibdelay >= Ch[MidCh].vibdelay) + bend += Ch[MidCh].vibrato * Ch[MidCh].vibdepth * std::sin(Ch[MidCh].vibpos); + +#define BEND_COEFFICIENT 321.88557 + opn.NoteOn(c, BEND_COEFFICIENT * std::exp(0.057762265 * (tone + bend + phase))); +#undef BEND_COEFFICIENT + if(hooks.onNote) + hooks.onNote(hooks.onNote_userData, c, tone, (int)midiins, vol, Ch[MidCh].bend); + } + } + } + + if(info.phys.empty()) + Ch[MidCh].activenotes.erase(i); +} + + +bool OPNMIDIplay::ProcessEventsNew(bool isSeek) +{ + if(CurrentPositionNew.track.size() == 0) + atEnd = true;//No MIDI track data to play + if(atEnd) + return false;//No more events in the queue + + loopEnd = false; + const size_t TrackCount = CurrentPositionNew.track.size(); + const PositionNew RowBeginPosition(CurrentPositionNew); + + #ifdef DEBUG_TIME_CALCULATION + double maxTime = 0.0; + #endif + + for(size_t tk = 0; tk < TrackCount; ++tk) + { + PositionNew::TrackInfo &track = CurrentPositionNew.track[tk]; + if((track.status >= 0) && (track.delay <= 0)) + { + //Check is an end of track has been reached + if(track.pos == trackDataNew[tk].end()) + { + track.status = -1; + break; + } + + // Handle event + for(size_t i = 0; i < track.pos->events.size(); i++) + { + const MidiEvent &evt = track.pos->events[i]; + #ifdef ENABLE_BEGIN_SILENCE_SKIPPING + if(!CurrentPositionNew.began && (evt.type == MidiEvent::T_NOTEON)) + CurrentPositionNew.began = true; + #endif + if(isSeek && (evt.type == MidiEvent::T_NOTEON)) + continue; + HandleEvent(tk, evt, track.status); + if(loopEnd) + break;//Stop event handling on catching loopEnd event! + } + + #ifdef DEBUG_TIME_CALCULATION + if(maxTime < track.pos->time) + maxTime = track.pos->time; + #endif + // Read next event time (unless the track just ended) + if(track.status >= 0) + { + track.delay += track.pos->delay; + track.pos++; + } + } + } + + #ifdef DEBUG_TIME_CALCULATION + std::fprintf(stdout, " \r"); + std::fprintf(stdout, "Time: %10f; Audio: %10f\r", maxTime, CurrentPositionNew.absTimePosition); + std::fflush(stdout); + #endif + + // Find shortest delay from all track + uint64_t shortest = 0; + bool shortest_no = true; + + for(size_t tk = 0; tk < TrackCount; ++tk) + { + PositionNew::TrackInfo &track = CurrentPositionNew.track[tk]; + if((track.status >= 0) && (shortest_no || track.delay < shortest)) + { + shortest = track.delay; + shortest_no = false; + } + } + + //if(shortest > 0) UI.PrintLn("shortest: %ld", shortest); + + // Schedule the next playevent to be processed after that delay + for(size_t tk = 0; tk < TrackCount; ++tk) + CurrentPositionNew.track[tk].delay -= shortest; + + fraction t = shortest * Tempo; + + #ifdef ENABLE_BEGIN_SILENCE_SKIPPING + if(CurrentPositionNew.began) + #endif + CurrentPositionNew.wait += t.value(); + + //if(shortest > 0) UI.PrintLn("Delay %ld (%g)", shortest, (double)t.valuel()); + if(loopStart) + { + LoopBeginPositionNew = RowBeginPosition; + loopStart = false; + } + + if(shortest_no || loopEnd) + { + //Loop if song end or loop end point has reached + loopEnd = false; + shortest = 0; + if(!m_setup.loopingIsEnabled) + { + atEnd = true; //Don't handle events anymore + CurrentPositionNew.wait += postSongWaitDelay;//One second delay until stop playing + return true;//We have caugh end here! + } + CurrentPositionNew = LoopBeginPositionNew; + } + + return true;//Has events in queue +} + +OPNMIDIplay::MidiEvent OPNMIDIplay::parseEvent(uint8_t **pptr, uint8_t *end, int &status) +{ + uint8_t *&ptr = *pptr; + OPNMIDIplay::MidiEvent evt; + + if(ptr + 1 > end) + { + //When track doesn't ends on the middle of event data, it's must be fine + evt.type = MidiEvent::T_SPECIAL; + evt.subtype = MidiEvent::ST_ENDTRACK; + return evt; + } + + unsigned char byte = *(ptr++); + bool ok = false; + + if(byte == MidiEvent::T_SYSEX || byte == MidiEvent::T_SYSEX2)// Ignore SysEx + { + uint64_t length = ReadVarLenEx(pptr, end, ok); + if(!ok || (ptr + length > end)) + { + errorString += "parseEvent: Can't read SysEx event - Unexpected end of track data.\n"; + evt.isValid = 0; + return evt; + } + ptr += (size_t)length; + return evt; + } + + if(byte == MidiEvent::T_SPECIAL) + { + // Special event FF + uint8_t evtype = *(ptr++); + uint64_t length = ReadVarLenEx(pptr, end, ok); + if(!ok || (ptr + length > end)) + { + errorString += "parseEvent: Can't read Special event - Unexpected end of track data.\n"; + evt.isValid = 0; + return evt; + } + std::string data(length ? (const char *)ptr : 0, (size_t)length); + ptr += (size_t)length; + + evt.type = byte; + evt.subtype = evtype; + evt.data.insert(evt.data.begin(), data.begin(), data.end()); + + /* TODO: Store those meta-strings separately and give ability to read them + * by external functions (to display song title and copyright in the player) */ + if(evt.subtype == MidiEvent::ST_COPYRIGHT) + { + if(musCopyright.empty()) + { + musCopyright = std::string((const char *)evt.data.data(), evt.data.size()); + if(hooks.onDebugMessage) + hooks.onDebugMessage(hooks.onDebugMessage_userData, "Music copyright: %s", musCopyright.c_str()); + } + else if(hooks.onDebugMessage) + { + std::string str((const char *)evt.data.data(), evt.data.size()); + hooks.onDebugMessage(hooks.onDebugMessage_userData, "Extra copyright event: %s", str.c_str()); + } + } + else if(evt.subtype == MidiEvent::ST_SQTRKTITLE) + { + if(musTitle.empty()) + { + musTitle = std::string((const char *)evt.data.data(), evt.data.size()); + if(hooks.onDebugMessage) + hooks.onDebugMessage(hooks.onDebugMessage_userData, "Music title: %s", musTitle.c_str()); + } + else if(hooks.onDebugMessage) + { + //TODO: Store track titles and associate them with each track and make API to retreive them + std::string str((const char *)evt.data.data(), evt.data.size()); + musTrackTitles.push_back(str); + hooks.onDebugMessage(hooks.onDebugMessage_userData, "Track title: %s", str.c_str()); + } + } + else if(evt.subtype == MidiEvent::ST_INSTRTITLE) + { + if(hooks.onDebugMessage) + { + std::string str((const char *)evt.data.data(), evt.data.size()); + hooks.onDebugMessage(hooks.onDebugMessage_userData, "Instrument: %s", str.c_str()); + } + } + else if(evt.subtype == MidiEvent::ST_MARKER) + { + //To lower + for(size_t i = 0; i < data.size(); i++) + { + if(data[i] <= 'Z' && data[i] >= 'A') + data[i] = data[i] - ('Z' - 'z'); + } + + if(data == "loopstart") + { + //Return a custom Loop Start event instead of Marker + evt.subtype = MidiEvent::ST_LOOPSTART; + evt.data.clear();//Data is not needed + return evt; + } + + if(data == "loopend") + { + //Return a custom Loop End event instead of Marker + evt.subtype = MidiEvent::ST_LOOPEND; + evt.data.clear();//Data is not needed + return evt; + } + } + + if(evtype == MidiEvent::ST_ENDTRACK) + status = -1;//Finalize track + + return evt; + } + + // Any normal event (80..EF) + if(byte < 0x80) + { + byte = static_cast(status | 0x80); + ptr--; + } + + //Sys Com Song Select(Song #) [0-127] + if(byte == MidiEvent::T_SYSCOMSNGSEL) + { + if(ptr + 1 > end) + { + errorString += "parseEvent: Can't read System Command Song Select event - Unexpected end of track data.\n"; + evt.isValid = 0; + return evt; + } + evt.type = byte; + evt.data.push_back(*(ptr++)); + return evt; + } + + //Sys Com Song Position Pntr [LSB, MSB] + if(byte == MidiEvent::T_SYSCOMSPOSPTR) + { + if(ptr + 2 > end) + { + errorString += "parseEvent: Can't read System Command Position Pointer event - Unexpected end of track data.\n"; + evt.isValid = 0; + return evt; + } + evt.type = byte; + evt.data.push_back(*(ptr++)); + evt.data.push_back(*(ptr++)); + return evt; + } + + uint8_t midCh = byte & 0x0F, evType = (byte >> 4) & 0x0F; + status = byte; + evt.channel = midCh; + evt.type = evType; + + switch(evType) + { + case MidiEvent::T_NOTEOFF://2 byte length + case MidiEvent::T_NOTEON: + case MidiEvent::T_NOTETOUCH: + case MidiEvent::T_CTRLCHANGE: + case MidiEvent::T_WHEEL: + if(ptr + 2 > end) + { + errorString += "parseEvent: Can't read regular 2-byte event - Unexpected end of track data.\n"; + evt.isValid = 0; + return evt; + } + + evt.data.push_back(*(ptr++)); + evt.data.push_back(*(ptr++)); + + if((evType == MidiEvent::T_NOTEON) && (evt.data[1] == 0)) + evt.type = MidiEvent::T_NOTEOFF; // Note ON with zero velocity is Note OFF! + //111'th loopStart controller (RPG Maker and others) + else if((evType == MidiEvent::T_CTRLCHANGE) && (evt.data[0] == 111)) + { + //Change event type to custom Loop Start event and clear data + evt.type = MidiEvent::T_SPECIAL; + evt.subtype = MidiEvent::ST_LOOPSTART; + evt.data.clear(); + } + + return evt; + case MidiEvent::T_PATCHCHANGE://1 byte length + case MidiEvent::T_CHANAFTTOUCH: + if(ptr + 1 > end) + { + errorString += "parseEvent: Can't read regular 1-byte event - Unexpected end of track data.\n"; + evt.isValid = 0; + return evt; + } + evt.data.push_back(*(ptr++)); + return evt; + } + + return evt; +} + +const std::string &OPNMIDIplay::getErrorString() +{ + return errorStringOut; +} + +void OPNMIDIplay::setErrorString(const std::string &err) +{ + errorStringOut = err; +} + + +void OPNMIDIplay::HandleEvent(size_t tk, const OPNMIDIplay::MidiEvent &evt, int &status) +{ + if(hooks.onEvent) + { + hooks.onEvent(hooks.onEvent_userData, + evt.type, + evt.subtype, + evt.channel, + evt.data.data(), + evt.data.size()); + } + + if(evt.type == MidiEvent::T_SYSEX || evt.type == MidiEvent::T_SYSEX2) // Ignore SysEx + { + //std::string data( length?(const char*) &TrackData[tk][CurrentPosition.track[tk].ptr]:0, length ); + //UI.PrintLn("SysEx %02X: %u bytes", byte, length/*, data.c_str()*/); + return; + } + + if(evt.type == MidiEvent::T_SPECIAL) + { + // Special event FF + uint8_t evtype = evt.subtype; + uint64_t length = (uint64_t)evt.data.size(); + std::string data(length ? (const char *)evt.data.data() : 0, (size_t)length); + + if(evtype == MidiEvent::ST_ENDTRACK)//End Of Track + { + status = -1; + return; + } + + if(evtype == MidiEvent::ST_TEMPOCHANGE)//Tempo change + { + Tempo = InvDeltaTicks * fraction(ReadBEint(evt.data.data(), evt.data.size())); + return; + } + + if(evtype == MidiEvent::ST_MARKER)//Meta event + { + //Do nothing! :-P + return; + } + + if(evtype == MidiEvent::ST_DEVICESWITCH) + { + current_device[tk] = ChooseDevice(data); + return; + } + + //if(evtype >= 1 && evtype <= 6) + // UI.PrintLn("Meta %d: %s", evtype, data.c_str()); + + //Turn on Loop handling when loop is enabled + if(m_setup.loopingIsEnabled && !invalidLoop) + { + if(evtype == MidiEvent::ST_LOOPSTART) // Special non-spec ADLMIDI special for IMF playback: Direct poke to AdLib + { + loopStart = true; + return; + } + + if(evtype == MidiEvent::ST_LOOPEND) // Special non-spec ADLMIDI special for IMF playback: Direct poke to AdLib + { + loopEnd = true; + return; + } + } + + //if(evtype == MidiEvent::ST_RAWOPL) // Special non-spec ADLMIDI special for IMF playback: Direct poke to AdLib + //{ + // uint8_t i = static_cast(data[0]), v = static_cast(data[1]); + // if((i & 0xF0) == 0xC0) + // v |= 0x30; + // //std::printf("OPL poke %02X, %02X\n", i, v); + // //std::fflush(stdout); + // opl.PokeN(0, i, v); + // return; + //} + + return; + } + + // Any normal event (80..EF) + // if(evt.type < 0x80) + // { + // byte = static_cast(CurrentPosition.track[tk].status | 0x80); + // CurrentPosition.track[tk].ptr--; + // } + + if(evt.type == MidiEvent::T_SYSCOMSNGSEL || + evt.type == MidiEvent::T_SYSCOMSPOSPTR) + return; + + /*UI.PrintLn("@%X Track %u: %02X %02X", + CurrentPosition.track[tk].ptr-1, (unsigned)tk, byte, + TrackData[tk][CurrentPosition.track[tk].ptr]);*/ + uint8_t midCh = evt.channel;//byte & 0x0F, EvType = byte >> 4; + midCh += (uint8_t)current_device[tk]; + status = evt.type; + + switch(evt.type) + { + case MidiEvent::T_NOTEOFF: // Note off + { + uint8_t note = evt.data[0]; + realTime_NoteOff(midCh, note); + break; + } + + case MidiEvent::T_NOTEON: // Note on + { + uint8_t note = evt.data[0]; + uint8_t vol = evt.data[1]; + /*if(*/ realTime_NoteOn(midCh, note, vol); /*)*/ + //CurrentPosition.began = true; + break; + } + + case MidiEvent::T_NOTETOUCH: // Note touch + { + uint8_t note = evt.data[0]; + uint8_t vol = evt.data[1]; + realTime_NoteAfterTouch(midCh, note, vol); + break; + } + + case MidiEvent::T_CTRLCHANGE: // Controller change + { + uint8_t ctrlno = evt.data[0]; + uint8_t value = evt.data[1]; + realTime_Controller(midCh, ctrlno, value); + break; + } + + case MidiEvent::T_PATCHCHANGE: // Patch change + realTime_PatchChange(midCh, evt.data[0]); + break; + + case MidiEvent::T_CHANAFTTOUCH: // Channel after-touch + { + // TODO: Verify, is this correct action? + uint8_t vol = evt.data[0]; + realTime_ChannelAfterTouch(midCh, vol); + break; + } + + case MidiEvent::T_WHEEL: // Wheel/pitch bend + { + uint8_t a = evt.data[0]; + uint8_t b = evt.data[1]; + realTime_PitchBend(midCh, b, a); + break; + } + } +} + +long OPNMIDIplay::CalculateAdlChannelGoodness(size_t c, uint16_t ins, uint16_t) const +{ + long s = -ch[c].koff_time_until_neglible; + + // Same midi-instrument = some stability + //if(c == MidCh) s += 4; + for(OpnChannel::users_t::const_iterator + j = ch[c].users.begin(); + j != ch[c].users.end(); + ++j) + { + s -= 4000; + + if(!j->second.sustained) + s -= (long)j->second.kon_time_until_neglible; + else + s -= (long)(j->second.kon_time_until_neglible / 2); + + MIDIchannel::activenotemap_t::const_iterator + k = Ch[j->first.MidCh].activenotes.find(j->first.note); + + if(k != Ch[j->first.MidCh].activenotes.end()) + { + // Same instrument = good + if(j->second.ins == ins) + { + s += 300; + // Arpeggio candidate = even better + if(j->second.vibdelay < 70 + || j->second.kon_time_until_neglible > 20000) + s += 0; + } + + // Percussion is inferior to melody + s += 50 * (k->second.midiins / 128); + /* + if(k->second.midiins >= 25 + && k->second.midiins < 40 + && j->second.ins != ins) + { + s -= 14000; // HACK: Don't clobber the bass or the guitar + } + */ + } + + // If there is another channel to which this note + // can be evacuated to in the case of congestion, + // increase the score slightly. +// unsigned n_evacuation_stations = 0; + +// for(unsigned c2 = 0; c2 < opn.NumChannels; ++c2) +// { +// if(c2 == c) continue; + +// if(opn.four_op_category[c2] +// != opn.four_op_category[c]) continue; + +// for(OpnChannel::users_t::const_iterator +// m = ch[c2].users.begin(); +// m != ch[c2].users.end(); +// ++m) +// { +// if(m->second.sustained) continue; +// if(m->second.vibdelay >= 200) continue; +// if(m->second.ins != j->second.ins) continue; +// n_evacuation_stations += 1; +// } +// } + +// s += n_evacuation_stations * 4; + } + + return s; +} + + +void OPNMIDIplay::PrepareAdlChannelForNewNote(size_t c, size_t ins) +{ + if(ch[c].users.empty()) return; // Nothing to do + + //bool doing_arpeggio = false; + for(OpnChannel::users_t::iterator + jnext = ch[c].users.begin(); + jnext != ch[c].users.end(); + ) + { + OpnChannel::users_t::iterator j(jnext++); + + if(!j->second.sustained) + { + // Collision: Kill old note, + // UNLESS we're going to do arpeggio + MIDIchannel::activenoteiterator i + (Ch[j->first.MidCh].activenotes.find(j->first.note)); + + // Check if we can do arpeggio. + if((j->second.vibdelay < 70 + || j->second.kon_time_until_neglible > 20000) + && j->second.ins == ins) + { + // Do arpeggio together with this note. + //doing_arpeggio = true; + continue; + } + + KillOrEvacuate(c, j, i); + // ^ will also erase j from ch[c].users. + } + } + + // Kill all sustained notes on this channel + // Don't keep them for arpeggio, because arpeggio requires + // an intact "activenotes" record. This is a design flaw. + KillSustainingNotes(-1, static_cast(c)); + + // Keyoff the channel so that it can be retriggered, + // unless the new note will be introduced as just an arpeggio. + if(ch[c].users.empty()) + opn.NoteOff(c); +} + +void OPNMIDIplay::KillOrEvacuate(size_t from_channel, OpnChannel::users_t::iterator j, OPNMIDIplay::MIDIchannel::activenoteiterator i) +{ + // Before killing the note, check if it can be + // evacuated to another channel as an arpeggio + // instrument. This helps if e.g. all channels + // are full of strings and we want to do percussion. + // FIXME: This does not care about four-op entanglements. + for(uint32_t c = 0; c < opn.NumChannels; ++c) + { + uint16_t cs = static_cast(c); + + if(c > std::numeric_limits::max()) + break; + if(c == from_channel) + continue; + //if(opn.four_op_category[c] != opn.four_op_category[from_channel]) + // continue; + + for(OpnChannel::users_t::iterator + m = ch[c].users.begin(); + m != ch[c].users.end(); + ++m) + { + if(m->second.vibdelay >= 200 + && m->second.kon_time_until_neglible < 10000) continue; + if(m->second.ins != j->second.ins) + continue; + if(hooks.onNote) + { + hooks.onNote(hooks.onNote_userData, + (int)from_channel, + i->second.tone, + (int)i->second.midiins, 0, 0.0); + hooks.onNote(hooks.onNote_userData, + (int)c, + i->second.tone, + (int)i->second.midiins, + i->second.vol, 0.0); + } + + i->second.phys.erase(static_cast(from_channel)); + i->second.phys[cs] = j->second.ins; + ch[cs].users.insert(*j); + ch[from_channel].users.erase(j); + return; + } + } + + /*UI.PrintLn( + "collision @%u: [%ld] <- ins[%3u]", + c, + //ch[c].midiins<128?'M':'P', ch[c].midiins&127, + ch[c].age, //adlins[ch[c].insmeta].ms_sound_kon, + ins + );*/ + // Kill it + NoteUpdate(j->first.MidCh, + i, + Upd_Off, + static_cast(from_channel)); +} + +void OPNMIDIplay::Panic() +{ + for(uint8_t chan = 0; chan < Ch.size(); chan++) + { + for(uint8_t note = 0; note < 128; note++) + realTime_NoteOff(chan, note); + } +} + +void OPNMIDIplay::KillSustainingNotes(int32_t MidCh, int32_t this_adlchn) +{ + uint32_t first = 0, last = opn.NumChannels; + + if(this_adlchn >= 0) + { + first = static_cast(this_adlchn); + last = first + 1; + } + + for(unsigned c = first; c < last; ++c) + { + if(ch[c].users.empty()) continue; // Nothing to do + + for(OpnChannel::users_t::iterator + jnext = ch[c].users.begin(); + jnext != ch[c].users.end(); + ) + { + OpnChannel::users_t::iterator j(jnext++); + + if((MidCh < 0 || j->first.MidCh == MidCh) + && j->second.sustained) + { + int midiins = '?'; + if(hooks.onNote) + hooks.onNote(hooks.onNote_userData, (int)c, j->first.note, midiins, 0, 0.0); + ch[c].users.erase(j); + } + } + + // Keyoff the channel, if there are no users left. + if(ch[c].users.empty()) + opn.NoteOff(c); + } +} + +void OPNMIDIplay::SetRPN(unsigned MidCh, unsigned value, bool MSB) +{ + bool nrpn = Ch[MidCh].nrpn; + unsigned addr = Ch[MidCh].lastmrpn * 0x100 + Ch[MidCh].lastlrpn; + + switch(addr + nrpn * 0x10000 + MSB * 0x20000) + { + case 0x0000 + 0*0x10000 + 1*0x20000: // Pitch-bender sensitivity + Ch[MidCh].bendsense = value / 8192.0; + break; + case 0x0108 + 1*0x10000 + 1*0x20000: // Vibrato speed + if(value == 64) Ch[MidCh].vibspeed = 1.0; + else if(value < 100) Ch[MidCh].vibspeed = 1.0 / (1.6e-2 * (value ? value : 1)); + else Ch[MidCh].vibspeed = 1.0 / (0.051153846 * value - 3.4965385); + Ch[MidCh].vibspeed *= 2 * 3.141592653 * 5.0; + break; + case 0x0109 + 1*0x10000 + 1*0x20000: // Vibrato depth + Ch[MidCh].vibdepth = ((value - 64) * 0.15) * 0.01; + break; + case 0x010A + 1*0x10000 + 1*0x20000: // Vibrato delay in millisecons + Ch[MidCh].vibdelay = value ? int64_t(0.2092 * std::exp(0.0795 * (double)value)) : 0; + break; + default:/* UI.PrintLn("%s %04X <- %d (%cSB) (ch %u)", + "NRPN"+!nrpn, addr, value, "LM"[MSB], MidCh);*/ + break; + } +} + +//void MIDIplay::UpdatePortamento(unsigned MidCh) +//{ +// // mt = 2^(portamento/2048) * (1.0 / 5000.0) +// /* +// double mt = std::exp(0.00033845077 * Ch[MidCh].portamento); +// NoteUpdate_All(MidCh, Upd_Pitch); +// */ +// //UI.PrintLn("Portamento %u: %u (unimplemented)", MidCh, Ch[MidCh].portamento); +//} + +void OPNMIDIplay::NoteUpdate_All(uint16_t MidCh, unsigned props_mask) +{ + for(MIDIchannel::activenoteiterator + i = Ch[MidCh].activenotes.begin(); + i != Ch[MidCh].activenotes.end(); + ) + { + MIDIchannel::activenoteiterator j(i++); + NoteUpdate(MidCh, j, props_mask); + } +} + +void OPNMIDIplay::NoteOff(uint16_t MidCh, uint8_t note) +{ + MIDIchannel::activenoteiterator + i = Ch[MidCh].activenotes.find(note); + + if(i != Ch[MidCh].activenotes.end()) + NoteUpdate(MidCh, i, Upd_Off); +} + + +void OPNMIDIplay::UpdateVibrato(double amount) +{ + for(size_t a = 0, b = Ch.size(); a < b; ++a) + { + if(Ch[a].vibrato && !Ch[a].activenotes.empty()) + { + NoteUpdate_All(static_cast(a), Upd_Pitch); + Ch[a].vibpos += amount * Ch[a].vibspeed; + } + else + Ch[a].vibpos = 0.0; + } +} + + + + +uint64_t OPNMIDIplay::ChooseDevice(const std::string &name) +{ + std::map::iterator i = devices.find(name); + + if(i != devices.end()) + return i->second; + + size_t n = devices.size() * 16; + devices.insert(std::make_pair(name, n)); + Ch.resize(n + 16); + return n; +} + +void OPNMIDIplay::UpdateArpeggio(double) // amount = amount of time passed +{ + // If there is an adlib channel that has multiple notes + // simulated on the same channel, arpeggio them. + #if 0 + const unsigned desired_arpeggio_rate = 40; // Hz (upper limit) + #if 1 + static unsigned cache = 0; + amount = amount; // Ignore amount. Assume we get a constant rate. + cache += MaxSamplesAtTime * desired_arpeggio_rate; + + if(cache < PCM_RATE) return; + + cache %= PCM_RATE; + #else + static double arpeggio_cache = 0; + arpeggio_cache += amount * desired_arpeggio_rate; + + if(arpeggio_cache < 1.0) return; + + arpeggio_cache = 0.0; + #endif + #endif + static unsigned arpeggio_counter = 0; + ++arpeggio_counter; + + for(uint32_t c = 0; c < opn.NumChannels; ++c) + { +retry_arpeggio: + if(c > uint32_t(std::numeric_limits::max())) + break; + + size_t n_users = ch[c].users.size(); + + if(n_users > 1) + { + OpnChannel::users_t::const_iterator i = ch[c].users.begin(); + size_t rate_reduction = 3; + + if(n_users >= 3) + rate_reduction = 2; + + if(n_users >= 4) + rate_reduction = 1; + + std::advance(i, (arpeggio_counter / rate_reduction) % n_users); + + if(i->second.sustained == false) + { + if(i->second.kon_time_until_neglible <= 0l) + { + NoteUpdate( + i->first.MidCh, + Ch[ i->first.MidCh ].activenotes.find(i->first.note), + Upd_Off, + static_cast(c)); + goto retry_arpeggio; + } + + NoteUpdate( + i->first.MidCh, + Ch[ i->first.MidCh ].activenotes.find(i->first.note), + Upd_Pitch | Upd_Volume | Upd_Pan, + static_cast(c)); + } + } + } +} + + +/* TODO */ + +//#ifndef ADLMIDI_DISABLE_CPP_EXTRAS + +//ADLMIDI_EXPORT AdlInstrumentTester::AdlInstrumentTester(ADL_MIDIPlayer *device) +//{ +// cur_gm = 0; +// ins_idx = 0; +// play = reinterpret_cast(device->adl_midiPlayer); +// if(!play) +// return; +// opl = &play->opl; +//} + +//ADLMIDI_EXPORT AdlInstrumentTester::~AdlInstrumentTester() +//{} + +//ADLMIDI_EXPORT void AdlInstrumentTester::FindAdlList() +//{ +// const unsigned NumBanks = (unsigned)adl_getBanksCount(); +// std::set adl_ins_set; +// for(unsigned bankno = 0; bankno < NumBanks; ++bankno) +// adl_ins_set.insert(banks[bankno][cur_gm]); +// adl_ins_list.assign(adl_ins_set.begin(), adl_ins_set.end()); +// ins_idx = 0; +// NextAdl(0); +// opl->Silence(); +//} + + + +//ADLMIDI_EXPORT void AdlInstrumentTester::Touch(unsigned c, unsigned volume) // Volume maxes at 127*127*127 +//{ +// if(opl->LogarithmicVolumes) +// opl->Touch_Real(c, volume * 127 / (127 * 127 * 127) / 2); +// else +// { +// // The formula below: SOLVE(V=127^3 * 2^( (A-63.49999) / 8), A) +// opl->Touch_Real(c, volume > 8725 ? static_cast(std::log((double)volume) * 11.541561 + (0.5 - 104.22845)) : 0); +// // The incorrect formula below: SOLVE(V=127^3 * (2^(A/63)-1), A) +// //Touch_Real(c, volume>11210 ? 91.61112 * std::log(4.8819E-7*volume + 1.0)+0.5 : 0); +// } +//} + +//ADLMIDI_EXPORT void AdlInstrumentTester::DoNote(int note) +//{ +// if(adl_ins_list.empty()) FindAdlList(); +// const unsigned meta = adl_ins_list[ins_idx]; +// const adlinsdata &ains = opl->GetAdlMetaIns(meta); + +// int tone = (cur_gm & 128) ? (cur_gm & 127) : (note + 50); +// if(ains.tone) +// { +// /*if(ains.tone < 20) +// tone += ains.tone; +// else */ +// if(ains.tone < 128) +// tone = ains.tone; +// else +// tone -= ains.tone - 128; +// } +// double hertz = 172.00093 * std::exp(0.057762265 * (tone + 0.0)); +// int i[2] = { ains.adlno1, ains.adlno2 }; +// int32_t adlchannel[2] = { 0, 3 }; +// if(i[0] == i[1]) +// { +// adlchannel[1] = -1; +// adlchannel[0] = 6; // single-op +// if(play->hooks.onDebugMessage) +// { +// play->hooks.onDebugMessage(play->hooks.onDebugMessage_userData, +// "noteon at %d(%d) for %g Hz\n", adlchannel[0], i[0], hertz); +// } +// } +// else +// { +// if(play->hooks.onDebugMessage) +// { +// play->hooks.onDebugMessage(play->hooks.onDebugMessage_userData, +// "noteon at %d(%d) and %d(%d) for %g Hz\n", adlchannel[0], i[0], adlchannel[1], i[1], hertz); +// } +// } + +// opl->NoteOff(0); +// opl->NoteOff(3); +// opl->NoteOff(6); +// for(unsigned c = 0; c < 2; ++c) +// { +// if(adlchannel[c] < 0) continue; +// opl->Patch((uint16_t)adlchannel[c], (uint16_t)i[c]); +// opl->Touch_Real((uint16_t)adlchannel[c], 127 * 127 * 100); +// opl->Pan((uint16_t)adlchannel[c], 0x30); +// opl->NoteOn((uint16_t)adlchannel[c], hertz); +// } +//} + +//ADLMIDI_EXPORT void AdlInstrumentTester::NextGM(int offset) +//{ +// cur_gm = (cur_gm + 256 + (uint32_t)offset) & 0xFF; +// FindAdlList(); +//} + +//ADLMIDI_EXPORT void AdlInstrumentTester::NextAdl(int offset) +//{ +// if(adl_ins_list.empty()) FindAdlList(); +// const unsigned NumBanks = (unsigned)adl_getBanksCount(); +// ins_idx = (uint32_t)((int32_t)ins_idx + (int32_t)adl_ins_list.size() + offset) % adl_ins_list.size(); + +// #if 0 +// UI.Color(15); +// std::fflush(stderr); +// std::printf("SELECTED G%c%d\t%s\n", +// cur_gm < 128 ? 'M' : 'P', cur_gm < 128 ? cur_gm + 1 : cur_gm - 128, +// "<-> select GM, ^v select ins, qwe play note"); +// std::fflush(stdout); +// UI.Color(7); +// std::fflush(stderr); +// #endif + +// for(unsigned a = 0; a < adl_ins_list.size(); ++a) +// { +// const unsigned i = adl_ins_list[a]; +// const adlinsdata &ains = opl->GetAdlMetaIns(i); + +// char ToneIndication[8] = " "; +// if(ains.tone) +// { +// /*if(ains.tone < 20) +// std::sprintf(ToneIndication, "+%-2d", ains.tone); +// else*/ +// if(ains.tone < 128) +// std::sprintf(ToneIndication, "=%-2d", ains.tone); +// else +// std::sprintf(ToneIndication, "-%-2d", ains.tone - 128); +// } +// std::printf("%s%s%s%u\t", +// ToneIndication, +// ains.adlno1 != ains.adlno2 ? "[2]" : " ", +// (ins_idx == a) ? "->" : "\t", +// i +// ); + +// for(unsigned bankno = 0; bankno < NumBanks; ++bankno) +// if(banks[bankno][cur_gm] == i) +// std::printf(" %u", bankno); + +// std::printf("\n"); +// } +//} + +//ADLMIDI_EXPORT bool AdlInstrumentTester::HandleInputChar(char ch) +//{ +// static const char notes[] = "zsxdcvgbhnjmq2w3er5t6y7ui9o0p"; +// // c'd'ef'g'a'bC'D'EF'G'A'Bc'd'e +// switch(ch) +// { +// case '/': +// case 'H': +// case 'A': +// NextAdl(-1); +// break; +// case '*': +// case 'P': +// case 'B': +// NextAdl(+1); +// break; +// case '-': +// case 'K': +// case 'D': +// NextGM(-1); +// break; +// case '+': +// case 'M': +// case 'C': +// NextGM(+1); +// break; +// case 3: +// #if !((!defined(__WIN32__) || defined(__CYGWIN__)) && !defined(__DJGPP__)) +// case 27: +// #endif +// return false; +// default: +// const char *p = std::strchr(notes, ch); +// if(p && *p) +// DoNote((int)(p - notes) - 12); +// } +// return true; +//} + +//#endif//ADLMIDI_DISABLE_CPP_EXTRAS diff --git a/src/sound/opnmidi/opnmidi_mus2mid.c b/src/sound/opnmidi/opnmidi_mus2mid.c new file mode 100644 index 000000000..a38af3f6a --- /dev/null +++ b/src/sound/opnmidi/opnmidi_mus2mid.c @@ -0,0 +1,451 @@ +/* + * MUS2MIDI: MUS to MIDI Library + * + * Copyright (C) 2014 Bret Curtis + * Copyright (C) WildMIDI Developers 2015-2016 + * ADLMIDI Library API: Copyright (c) 2017-2018 Vitaly Novichkov + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include "opnmidi_mus2mid.h" + +#define FREQUENCY 140 /* default Hz or BPM */ + +#if 0 /* older units: */ +#define TEMPO 0x001aa309 /* MPQN: 60000000 / 34.37Hz = 1745673 */ +#define DIVISION 0x0059 /* 89 -- used by many mus2midi converters */ +#endif + +#define TEMPO 0x00068A1B /* MPQN: 60000000 / 140BPM (140Hz) = 428571 */ + /* 0x000D1436 -> MPQN: 60000000 / 70BPM (70Hz) = 857142 */ + +#define DIVISION 0x0101 /* 257 for 140Hz files with a 140MPQN */ + /* 0x0088 -> 136 for 70Hz files with a 140MPQN */ + /* 0x010B -> 267 for 70hz files with a 70MPQN */ + /* 0x01F9 -> 505 for 140hz files with a 70MPQN */ + +/* New + * QLS: MPQN/1000000 = 0.428571 + * TDPS: QLS/PPQN = 0.428571/136 = 0.003151257 + * PPQN: 136 + * + * QLS: MPQN/1000000 = 0.428571 + * TDPS: QLS/PPQN = 0.428571/257 = 0.001667591 + * PPQN: 257 + * + * QLS: MPQN/1000000 = 0.857142 + * TDPS: QLS/PPQN = 0.857142/267 = 0.00321027 + * PPQN: 267 + * + * QLS: MPQN/1000000 = 0.857142 + * TDPS: QLS/PPQN = 0.857142/505 = 0.001697311 + * PPQN: 505 + * + * Old + * QLS: MPQN/1000000 = 1.745673 + * TDPS: QLS/PPQN = 1.745673 / 89 = 0.019614303 (seconds per tick) + * PPQN: (TDPS = QLS/PPQN) (0.019614303 = 1.745673/PPQN) (0.019614303*PPQN = 1.745673) (PPQN = 89.000001682) + * + */ + +#define MUSEVENT_KEYOFF 0 +#define MUSEVENT_KEYON 1 +#define MUSEVENT_PITCHWHEEL 2 +#define MUSEVENT_CHANNELMODE 3 +#define MUSEVENT_CONTROLLERCHANGE 4 +#define MUSEVENT_END 6 + +#define MIDI_MAXCHANNELS 16 + +static char MUS_ID[] = { 'M', 'U', 'S', 0x1A }; + +static uint8_t midimap[] = +{/* MIDI Number Description */ + 0, /* 0 program change */ + 0, /* 1 bank selection */ + 0x01, /* 2 Modulation pot (frequency vibrato depth) */ + 0x07, /* 3 Volume: 0-silent, ~100-normal, 127-loud */ + 0x0A, /* 4 Pan (balance) pot: 0-left, 64-center (default), 127-right */ + 0x0B, /* 5 Expression pot */ + 0x5B, /* 6 Reverb depth */ + 0x5D, /* 7 Chorus depth */ + 0x40, /* 8 Sustain pedal */ + 0x43, /* 9 Soft pedal */ + 0x78, /* 10 All sounds off */ + 0x7B, /* 11 All notes off */ + 0x7E, /* 12 Mono (use numchannels + 1) */ + 0x7F, /* 13 Poly */ + 0x79, /* 14 reset all controllers */ +}; + +typedef struct MUSHeader { + char ID[4]; /* identifier: "MUS" 0x1A */ + uint16_t scoreLen; + uint16_t scoreStart; + uint16_t channels; /* count of primary channels */ + uint16_t sec_channels; /* count of secondary channels */ + uint16_t instrCnt; +} MUSHeader ; +#define MUS_HEADERSIZE 14 + +typedef struct MidiHeaderChunk { + char name[4]; + int32_t length; + int16_t format; /* make 0 */ + int16_t ntracks;/* make 1 */ + int16_t division; /* 0xe250 ?? */ +} MidiHeaderChunk; +#define MIDI_HEADERSIZE 14 + +typedef struct MidiTrackChunk { + char name[4]; + int32_t length; +} MidiTrackChunk; +#define TRK_CHUNKSIZE 8 + +struct mus_ctx { + uint8_t *src, *src_ptr; + uint32_t srcsize; + uint32_t datastart; + uint8_t *dst, *dst_ptr; + uint32_t dstsize, dstrem; +}; + +#define DST_CHUNK 8192 +static void resize_dst(struct mus_ctx *ctx) { + uint32_t pos = (uint32_t)(ctx->dst_ptr - ctx->dst); + ctx->dst = realloc(ctx->dst, ctx->dstsize + DST_CHUNK); + ctx->dstsize += DST_CHUNK; + ctx->dstrem += DST_CHUNK; + ctx->dst_ptr = ctx->dst + pos; +} + +static void write1(struct mus_ctx *ctx, uint32_t val) +{ + if (ctx->dstrem < 1) + resize_dst(ctx); + *ctx->dst_ptr++ = val & 0xff; + ctx->dstrem--; +} + +static void write2(struct mus_ctx *ctx, uint32_t val) +{ + if (ctx->dstrem < 2) + resize_dst(ctx); + *ctx->dst_ptr++ = (val>>8) & 0xff; + *ctx->dst_ptr++ = val & 0xff; + ctx->dstrem -= 2; +} + +static void write4(struct mus_ctx *ctx, uint32_t val) +{ + if (ctx->dstrem < 4) + resize_dst(ctx); + *ctx->dst_ptr++ = (val>>24)&0xff; + *ctx->dst_ptr++ = (val>>16)&0xff; + *ctx->dst_ptr++ = (val>>8) & 0xff; + *ctx->dst_ptr++ = val & 0xff; + ctx->dstrem -= 4; +} + +static void seekdst(struct mus_ctx *ctx, uint32_t pos) { + ctx->dst_ptr = ctx->dst + pos; + while (ctx->dstsize < pos) + resize_dst(ctx); + ctx->dstrem = ctx->dstsize - pos; +} + +static void skipdst(struct mus_ctx *ctx, int32_t pos) { + size_t newpos; + ctx->dst_ptr += pos; + newpos = ctx->dst_ptr - ctx->dst; + while (ctx->dstsize < newpos) + resize_dst(ctx); + ctx->dstrem = (uint32_t)(ctx->dstsize - newpos); +} + +static uint32_t getdstpos(struct mus_ctx *ctx) { + return (uint32_t)(ctx->dst_ptr - ctx->dst); +} + +/* writes a variable length integer to a buffer, and returns bytes written */ +static int32_t writevarlen(int32_t value, uint8_t *out) +{ + int32_t buffer, count = 0; + + buffer = value & 0x7f; + while ((value >>= 7) > 0) { + buffer <<= 8; + buffer += 0x80; + buffer += (value & 0x7f); + } + + while (1) { + ++count; + *out = (uint8_t)buffer; + ++out; + if (buffer & 0x80) + buffer >>= 8; + else + break; + } + return (count); +} + +#define READ_INT16(b) ((b)[0] | ((b)[1] << 8)) +#define READ_INT32(b) ((b)[0] | ((b)[1] << 8) | ((b)[2] << 16) | ((b)[3] << 24)) + +int OpnMidi_mus2midi(uint8_t *in, uint32_t insize, + uint8_t **out, uint32_t *outsize, + uint16_t frequency) { + struct mus_ctx ctx; + MUSHeader header; + uint8_t *cur, *end; + uint32_t track_size_pos, begin_track_pos, current_pos; + int32_t delta_time;/* Delta time for midi event */ + int temp, ret = -1; + int channel_volume[MIDI_MAXCHANNELS]; + int channelMap[MIDI_MAXCHANNELS], currentChannel; + + if (insize < MUS_HEADERSIZE) { + /*_WM_GLOBAL_ERROR(__FUNCTION__, __LINE__, WM_ERR_CORUPT, "(too short)", 0);*/ + return (-1); + } + + if (!frequency) + frequency = FREQUENCY; + + /* read the MUS header and set our location */ + memcpy(header.ID, in, 4); + header.scoreLen = READ_INT16(&in[4]); + header.scoreStart = READ_INT16(&in[6]); + header.channels = READ_INT16(&in[8]); + header.sec_channels = READ_INT16(&in[10]); + header.instrCnt = READ_INT16(&in[12]); + + if (memcmp(header.ID, MUS_ID, 4)) { + /*_WM_GLOBAL_ERROR(__FUNCTION__, __LINE__, WM_ERR_NOT_MUS, NULL, 0);*/ + return (-1); + } + if (insize < (uint32_t)header.scoreLen + (uint32_t)header.scoreStart) { + /*_WM_GLOBAL_ERROR(__FUNCTION__, __LINE__, WM_ERR_CORUPT, "(too short)", 0);*/ + return (-1); + } + /* channel #15 should be excluded in the numchannels field: */ + if (header.channels > MIDI_MAXCHANNELS - 1) { + /*_WM_GLOBAL_ERROR(__FUNCTION__, __LINE__, WM_ERR_INVALID, NULL, 0);*/ + return (-1); + } + + memset(&ctx, 0, sizeof(struct mus_ctx)); + ctx.src = ctx.src_ptr = in; + ctx.srcsize = insize; + + ctx.dst = calloc(DST_CHUNK, sizeof(uint8_t)); + ctx.dst_ptr = ctx.dst; + ctx.dstsize = DST_CHUNK; + ctx.dstrem = DST_CHUNK; + + /* Map channel 15 to 9 (percussions) */ + for (temp = 0; temp < MIDI_MAXCHANNELS; ++temp) { + channelMap[temp] = -1; + channel_volume[temp] = 0x40; + } + channelMap[15] = 9; + + /* Header is 14 bytes long and add the rest as well */ + write1(&ctx, 'M'); + write1(&ctx, 'T'); + write1(&ctx, 'h'); + write1(&ctx, 'd'); + write4(&ctx, 6); /* length of header */ + write2(&ctx, 0); /* MIDI type (always 0) */ + write2(&ctx, 1); /* MUS files only have 1 track */ + write2(&ctx, DIVISION); /* division */ + + /* Write out track header and track length position for later */ + begin_track_pos = getdstpos(&ctx); + write1(&ctx, 'M'); + write1(&ctx, 'T'); + write1(&ctx, 'r'); + write1(&ctx, 'k'); + track_size_pos = getdstpos(&ctx); + skipdst(&ctx, 4); + + /* write tempo: microseconds per quarter note */ + write1(&ctx, 0x00); /* delta time */ + write1(&ctx, 0xff); /* sys command */ + write2(&ctx, 0x5103); /* command - set tempo */ + write1(&ctx, TEMPO & 0x000000ff); + write1(&ctx, (TEMPO & 0x0000ff00) >> 8); + write1(&ctx, (TEMPO & 0x00ff0000) >> 16); + + /* Percussions channel starts out at full volume */ + write1(&ctx, 0x00); + write1(&ctx, 0xB9); + write1(&ctx, 0x07); + write1(&ctx, 127); + + /* get current position in source, and end of position */ + cur = in + header.scoreStart; + end = cur + header.scoreLen; + + currentChannel = 0; + delta_time = 0; + + /* main loop */ + while(cur < end){ + /*printf("LOOP DEBUG: %d\r\n",iterator++);*/ + uint8_t channel; + uint8_t event; + uint8_t temp_buffer[32]; /* temp buffer for current iterator */ + uint8_t *out_local = temp_buffer; + uint8_t status, bit1, bit2, bitc = 2; + + /* read in current bit */ + event = *cur++; + channel = (event & 15); /* current channel */ + + /* write variable length delta time */ + out_local += writevarlen(delta_time, out_local); + + /* set all channels to 127 (max) volume */ + if (channelMap[channel] < 0) { + *out_local++ = 0xB0 + currentChannel; + *out_local++ = 0x07; + *out_local++ = 127; + *out_local++ = 0x00; + channelMap[channel] = currentChannel++; + if (currentChannel == 9) + ++currentChannel; + } + status = channelMap[channel]; + + /* handle events */ + switch ((event & 122) >> 4){ + case MUSEVENT_KEYOFF: + status |= 0x80; + bit1 = *cur++; + bit2 = 0x40; + break; + case MUSEVENT_KEYON: + status |= 0x90; + bit1 = *cur & 127; + if (*cur++ & 128) /* volume bit? */ + channel_volume[channelMap[channel]] = *cur++; + bit2 = channel_volume[channelMap[channel]]; + break; + case MUSEVENT_PITCHWHEEL: + status |= 0xE0; + bit1 = (*cur & 1) >> 6; + bit2 = (*cur++ >> 1) & 127; + break; + case MUSEVENT_CHANNELMODE: + status |= 0xB0; + if (*cur >= sizeof(midimap) / sizeof(midimap[0])) { + /*_WM_ERROR_NEW("%s:%i: can't map %u to midi", + __FUNCTION__, __LINE__, *cur);*/ + goto _end; + } + bit1 = midimap[*cur++]; + bit2 = (*cur++ == 12) ? header.channels + 1 : 0x00; + break; + case MUSEVENT_CONTROLLERCHANGE: + if (*cur == 0) { + cur++; + status |= 0xC0; + bit1 = *cur++; + bit2 = 0;/* silence bogus warnings */ + bitc = 1; + } else { + status |= 0xB0; + if (*cur >= sizeof(midimap) / sizeof(midimap[0])) { + /*_WM_ERROR_NEW("%s:%i: can't map %u to midi", + __FUNCTION__, __LINE__, *cur);*/ + goto _end; + } + bit1 = midimap[*cur++]; + bit2 = *cur++; + } + break; + case MUSEVENT_END: /* End */ + status = 0xff; + bit1 = 0x2f; + bit2 = 0x00; + if (cur != end) { /* should we error here or report-only? */ + /*_WM_DEBUG_MSG("%s:%i: MUS buffer off by %ld bytes", + __FUNCTION__, __LINE__, (long)(cur - end));*/ + } + break; + case 5:/* Unknown */ + case 7:/* Unknown */ + default:/* shouldn't happen */ + /*_WM_ERROR_NEW("%s:%i: unrecognized event (%u)", + __FUNCTION__, __LINE__, event);*/ + goto _end; + } + + /* write it out */ + *out_local++ = status; + *out_local++ = bit1; + if (bitc == 2) + *out_local++ = bit2; + + /* write out our temp buffer */ + if (out_local != temp_buffer) + { + if (ctx.dstrem < sizeof(temp_buffer)) + resize_dst(&ctx); + + memcpy(ctx.dst_ptr, temp_buffer, out_local - temp_buffer); + ctx.dst_ptr += out_local - temp_buffer; + ctx.dstrem -= out_local - temp_buffer; + } + + if (event & 128) { + delta_time = 0; + do { + delta_time = (int32_t)((delta_time * 128 + (*cur & 127)) * (140.0 / (double)frequency)); + } while ((*cur++ & 128)); + } else { + delta_time = 0; + } + } + + /* write out track length */ + current_pos = getdstpos(&ctx); + seekdst(&ctx, track_size_pos); + write4(&ctx, current_pos - begin_track_pos - TRK_CHUNKSIZE); + seekdst(&ctx, current_pos); /* reseek to end position */ + + *out = ctx.dst; + *outsize = ctx.dstsize - ctx.dstrem; + ret = 0; + +_end: /* cleanup */ + if (ret < 0) { + free(ctx.dst); + *out = NULL; + *outsize = 0; + } + + return (ret); +} + diff --git a/src/sound/opnmidi/opnmidi_mus2mid.h b/src/sound/opnmidi/opnmidi_mus2mid.h new file mode 100644 index 000000000..4668831b3 --- /dev/null +++ b/src/sound/opnmidi/opnmidi_mus2mid.h @@ -0,0 +1,49 @@ +/* + * MUS2MIDI: DMX (DOOM) MUS to MIDI Library Header + * + * Copyright (C) 2014-2016 Bret Curtis + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef MUSLIB_H +#define MUSLIB_H + +#include + +#ifdef __DJGPP__ +typedef signed char int8_t; +typedef unsigned char uint8_t; +typedef signed short int16_t; +typedef unsigned short uint16_t; +typedef signed long int32_t; +typedef unsigned long uint32_t; +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +int OpnMidi_mus2midi(uint8_t *in, uint32_t insize, + uint8_t **out, uint32_t *outsize, + uint16_t frequency); + +#ifdef __cplusplus +} +#endif + +#endif /* MUSLIB_H */ diff --git a/src/sound/opnmidi/opnmidi_opn2.cpp b/src/sound/opnmidi/opnmidi_opn2.cpp new file mode 100644 index 000000000..17182def1 --- /dev/null +++ b/src/sound/opnmidi/opnmidi_opn2.cpp @@ -0,0 +1,318 @@ +/* + * libOPNMIDI is a free MIDI to WAV conversion library with OPN2 (YM2612) emulation + * + * MIDI parser and player (Original code from ADLMIDI): Copyright (c) 2010-2014 Joel Yliluoma + * OPNMIDI Library and YM2612 support: Copyright (c) 2017-2018 Vitaly Novichkov + * + * Library is based on the ADLMIDI, a MIDI player for Linux and Windows with OPL3 emulation: + * http://iki.fi/bisqwit/source/adlmidi.html + * + * 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 3 of the License, or + * 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, see . + */ + +#include "opnmidi_private.hpp" + +static const uint8_t NoteChannels[6] = { 0, 1, 2, 4, 5, 6 }; + +static inline void getOpnChannel(uint32_t in_channel, + unsigned &out_card, + uint8_t &out_port, + uint8_t &out_ch) +{ + out_card = in_channel / 6; + uint8_t ch4 = in_channel % 6; + out_port = ((ch4 < 3) ? 0 : 1); + out_ch = ch4 % 3; +} + +const opnInstMeta &OPN2::GetAdlMetaIns(size_t n) +{ + return dynamic_metainstruments[n]; +} + +size_t OPN2::GetAdlMetaNumber(size_t midiins) +{ + return midiins; +} + +static const opnInstData opn2_emptyInstrument = { + { + {{0, 0, 0, 0, 0, 0, 0}}, + {{0, 0, 0, 0, 0, 0, 0}}, + {{0, 0, 0, 0, 0, 0, 0}}, + {{0, 0, 0, 0, 0, 0, 0}} + }, + 0, 0, 0 +}; + +const opnInstData &OPN2::GetAdlIns(size_t insno) +{ + if(insno >= dynamic_instruments.size()) + return opn2_emptyInstrument; + return dynamic_instruments[insno]; +} + +OPN2::OPN2() : + regLFO(0), + dynamic_percussion_offset(128), + DynamicInstrumentTag(0x8000u), + DynamicMetaInstrumentTag(0x4000000u), + NumCards(1), + LogarithmicVolumes(false), + m_musicMode(MODE_MIDI), + m_volumeScale(VOLUME_Generic) +{} + +OPN2::~OPN2() +{ + ClearChips(); +} + +void OPN2::PokeO(size_t card, uint8_t port, uint8_t index, uint8_t value) +{ + #ifdef USE_LEGACY_EMULATOR + if(port == 1) + cardsOP2[card]->write1(index, value); + else + cardsOP2[card]->write0(index, value); + #else + OPN2_WriteBuffered(cardsOP2[card], 0 + (port) * 2, index); + OPN2_WriteBuffered(cardsOP2[card], 1 + (port) * 2, value); + #endif +} + +void OPN2::NoteOff(size_t c) +{ + unsigned card; + uint8_t port, cc; + uint8_t ch4 = c % 6; + getOpnChannel(uint16_t(c), card, port, cc); + PokeO(card, 0, 0x28, NoteChannels[ch4]); +} + +void OPN2::NoteOn(unsigned c, double hertz) // Hertz range: 0..131071 +{ + unsigned card; + uint8_t port, cc; + uint8_t ch4 = c % 6; + getOpnChannel(uint16_t(c), card, port, cc); + + uint16_t x2 = 0x0000; + + if(hertz < 0 || hertz > 262143) // Avoid infinite loop + return; + + while(hertz >= 2047.5) + { + hertz /= 2.0; // Calculate octave + x2 += 0x800; + } + + x2 += static_cast(hertz + 0.5); + PokeO(card, port, 0xA4 + cc, (x2>>8) & 0xFF);//Set frequency and octave + PokeO(card, port, 0xA0 + cc, x2 & 0xFF); + PokeO(card, 0, 0x28, 0xF0 + NoteChannels[ch4]); + pit[c] = static_cast(x2 >> 8); +} + +void OPN2::Touch_Real(unsigned c, unsigned volume, uint8_t brightness) +{ + if(volume > 127) volume = 127; + + unsigned card; + uint8_t port, cc; + getOpnChannel(c, card, port, cc); + + size_t i = ins[c]; + const opnInstData &adli = GetAdlIns(i); + + uint8_t op_vol[4] = + { + adli.OPS[OPERATOR1].data[1], + adli.OPS[OPERATOR2].data[1], + adli.OPS[OPERATOR3].data[1], + adli.OPS[OPERATOR4].data[1], + }; + + bool alg_do[8][4] = + { + /* + * Yeah, Operator 2 and 3 are seems swapped + * which we can see in the algorithm 4 + */ + //OP1 OP3 OP2 OP4 + //30 34 38 3C + {false,false,false,true},//Algorithm #0: W = 1 * 2 * 3 * 4 + {false,false,false,true},//Algorithm #1: W = (1 + 2) * 3 * 4 + {false,false,false,true},//Algorithm #2: W = (1 + (2 * 3)) * 4 + {false,false,false,true},//Algorithm #3: W = ((1 * 2) + 3) * 4 + {false,false,true, true},//Algorithm #4: W = (1 * 2) + (3 * 4) + {false,true ,true ,true},//Algorithm #5: W = (1 * (2 + 3 + 4) + {false,true ,true ,true},//Algorithm #6: W = (1 * 2) + 3 + 4 + {true ,true ,true ,true},//Algorithm #7: W = 1 + 2 + 3 + 4 + }; + + uint8_t alg = adli.fbalg & 0x07; + for(uint8_t op = 0; op < 4; op++) + { + bool do_op = alg_do[alg][op] || ScaleModulators; + uint8_t x = op_vol[op]; + uint8_t vol_res = do_op ? uint8_t(127 - (volume * (127 - (x&127)))/127) : x; + if(brightness != 127) + { + brightness = static_cast(::round(127.0 * ::sqrt((static_cast(brightness)) * (1.0 / 127.0)))); + if(!do_op) + vol_res = uint8_t(127 - (brightness * (127 - (uint32_t(vol_res) & 127))) / 127); + } + PokeO(card, port, 0x40 + cc + (4 * op), vol_res); + } + // Correct formula (ST3, AdPlug): + // 63-((63-(instrvol))/63)*chanvol + // Reduces to (tested identical): + // 63 - chanvol + chanvol*instrvol/63 + // Also (slower, floats): + // 63 + chanvol * (instrvol / 63.0 - 1) +} + +void OPN2::Patch(uint16_t c, size_t i) +{ + unsigned card; + uint8_t port, cc; + getOpnChannel(uint16_t(c), card, port, cc); + ins[c] = i; + const opnInstData &adli = GetAdlIns(i); + #if 1 //Reg1-Op1, Reg1-Op2, Reg1-Op3, Reg1-Op4,.... + for(uint8_t d = 0; d < 7; d++) + { + for(uint8_t op = 0; op < 4; op++) + PokeO(card, port, 0x30 + (0x10 * d) + (op * 4) + cc, adli.OPS[op].data[d]); + } + #else //Reg1-Op1, Reg2-Op1, Reg3-Op1, Reg4-Op1,.... + for(uint8_t op = 0; op < 4; op++) + { + PokeO(card, port, 0x30 + (op * 4) + cc, adli.OPS[op].data[0]); + PokeO(card, port, 0x40 + (op * 4) + cc, adli.OPS[op].data[1]); + PokeO(card, port, 0x50 + (op * 4) + cc, adli.OPS[op].data[2]); + PokeO(card, port, 0x60 + (op * 4) + cc, adli.OPS[op].data[3]); + PokeO(card, port, 0x70 + (op * 4) + cc, adli.OPS[op].data[4]); + PokeO(card, port, 0x80 + (op * 4) + cc, adli.OPS[op].data[5]); + PokeO(card, port, 0x90 + (op * 4) + cc, adli.OPS[op].data[6]); + } + #endif + + PokeO(card, port, 0xB0 + cc, adli.fbalg);//Feedback/Algorithm + regBD[c] = (regBD[c] & 0xC0) | (adli.lfosens & 0x3F); + PokeO(card, port, 0xB4 + cc, regBD[c]);//Panorame and LFO bits +} + +void OPN2::Pan(unsigned c, unsigned value) +{ + unsigned card; + uint8_t port, cc; + getOpnChannel(uint16_t(c), card, port, cc); + const opnInstData &adli = GetAdlIns(ins[c]); + uint8_t val = (value & 0xC0) | (adli.lfosens & 0x3F); + regBD[c] = val; + PokeO(card, port, 0xB4 + cc, val); +} + +void OPN2::Silence() // Silence all OPL channels. +{ + for(unsigned c = 0; c < NumChannels; ++c) + { + NoteOff(c); + Touch_Real(c, 0); + } +} + +void OPN2::ChangeVolumeRangesModel(OPNMIDI_VolumeModels volumeModel) +{ + switch(volumeModel) + { + case OPNMIDI_VolumeModel_AUTO://Do nothing until restart playing + break; + + case OPNMIDI_VolumeModel_Generic: + m_volumeScale = OPN2::VOLUME_Generic; + break; + + case OPNMIDI_VolumeModel_CMF: + LogarithmicVolumes = true; + m_volumeScale = OPN2::VOLUME_CMF; + break; + + case OPNMIDI_VolumeModel_DMX: + m_volumeScale = OPN2::VOLUME_DMX; + break; + + case OPNMIDI_VolumeModel_APOGEE: + m_volumeScale = OPN2::VOLUME_APOGEE; + break; + + case OPNMIDI_VolumeModel_9X: + m_volumeScale = OPN2::VOLUME_9X; + break; + } +} + +void OPN2::ClearChips() +{ + for(size_t i = 0; i < cardsOP2.size(); i++) + delete cardsOP2[i]; + cardsOP2.clear(); +} + +void OPN2::Reset(unsigned long PCM_RATE) +{ + ClearChips(); + ins.clear(); + pit.clear(); + regBD.clear(); + cardsOP2.resize(NumCards, NULL); + + #ifndef USE_LEGACY_EMULATOR + OPN2_SetChipType(ym3438_type_asic); + #endif + for(size_t i = 0; i < cardsOP2.size(); i++) + { + #ifdef USE_LEGACY_EMULATOR + cardsOP2[i] = new OPNMIDI_Ym2612_Emu(); + cardsOP2[i]->set_rate(PCM_RATE, 7670454.0); + #else + cardsOP2[i] = new ym3438_t; + std::memset(cardsOP2[i], 0, sizeof(ym3438_t)); + OPN2_Reset(cardsOP2[i], (Bit32u)PCM_RATE, 7670454); + #endif + } + NumChannels = NumCards * 6; + ins.resize(NumChannels, 189); + pit.resize(NumChannels, 0); + regBD.resize(NumChannels, 0); + + for(unsigned card = 0; card < NumCards; ++card) + { + PokeO(card, 0, 0x22, regLFO);//push current LFO state + PokeO(card, 0, 0x27, 0x00); //set Channel 3 normal mode + PokeO(card, 0, 0x2B, 0x00); //Disable DAC + //Shut up all channels + PokeO(card, 0, 0x28, 0x00 ); //Note Off 0 channel + PokeO(card, 0, 0x28, 0x01 ); //Note Off 1 channel + PokeO(card, 0, 0x28, 0x02 ); //Note Off 2 channel + PokeO(card, 0, 0x28, 0x04 ); //Note Off 3 channel + PokeO(card, 0, 0x28, 0x05 ); //Note Off 4 channel + PokeO(card, 0, 0x28, 0x06 ); //Note Off 5 channel + } + + Silence(); +} diff --git a/src/sound/opnmidi/opnmidi_private.cpp b/src/sound/opnmidi/opnmidi_private.cpp new file mode 100644 index 000000000..1faced79d --- /dev/null +++ b/src/sound/opnmidi/opnmidi_private.cpp @@ -0,0 +1,55 @@ +/* + * libOPNMIDI is a free MIDI to WAV conversion library with OPN2 (YM2612) emulation + * + * MIDI parser and player (Original code from ADLMIDI): Copyright (c) 2010-2014 Joel Yliluoma + * OPNMIDI Library and YM2612 support: Copyright (c) 2017-2018 Vitaly Novichkov + * + * Library is based on the ADLMIDI, a MIDI player for Linux and Windows with OPL3 emulation: + * http://iki.fi/bisqwit/source/adlmidi.html + * + * 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 3 of the License, or + * 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, see . + */ + +#include "opnmidi_private.hpp" + +std::string OPN2MIDI_ErrorString; + +int opn2RefreshNumCards(OPN2_MIDIPlayer * /*device*/) +{ +// OPNMIDIplay *play = reinterpret_cast(device->opn2_midiPlayer); + //OPN uses 4-op instruments only +// unsigned n_fourop[2] = {0, 0}, n_total[2] = {0, 0}; +// for(unsigned a = 0; a < 256; ++a) +// { +// unsigned insno = banks[device->OpnBank][a]; +// if(insno == 198) continue; +// ++n_total[a / 128]; +// if(adlins[insno].adlno1 != adlins[insno].adlno2) +// ++n_fourop[a / 128]; +// } + +// device->NumFourOps = +// (n_fourop[0] >= n_total[0] * 7 / 8) ? device->NumCards * 6 +// : (n_fourop[0] < n_total[0] * 1 / 8) ? 0 +// : (device->NumCards == 1 ? 1 : device->NumCards * 4); +// reinterpret_cast(device->opn2_midiPlayer)->opn.NumFourOps = device->NumFourOps; +// if(n_fourop[0] >= n_total[0] * 15 / 16 && device->NumFourOps == 0) +// { +// OPN2MIDI_ErrorString = "ERROR: You have selected a bank that consists almost exclusively of four-op patches.\n" +// " The results (silence + much cpu load) would be probably\n" +// " not what you want, therefore ignoring the request.\n"; +// return -1; +// } + return 0; +} diff --git a/src/sound/opnmidi/opnmidi_private.hpp b/src/sound/opnmidi/opnmidi_private.hpp new file mode 100644 index 000000000..98f61fc00 --- /dev/null +++ b/src/sound/opnmidi/opnmidi_private.hpp @@ -0,0 +1,894 @@ +/* + * libADLMIDI is a free MIDI to WAV conversion library with OPL3 emulation + * + * Original ADLMIDI code: Copyright (c) 2010-2014 Joel Yliluoma + * ADLMIDI Library API: Copyright (c) 2017-2018 Vitaly Novichkov + * + * Library is based on the ADLMIDI, a MIDI player for Linux and Windows with OPL3 emulation: + * http://iki.fi/bisqwit/source/adlmidi.html + * + * 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 3 of the License, or + * 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, see . + */ + +#ifndef ADLMIDI_PRIVATE_HPP +#define ADLMIDI_PRIVATE_HPP + +// Setup compiler defines useful for exporting required public API symbols in gme.cpp +#ifndef OPNMIDI_EXPORT +# if defined (_WIN32) && defined(ADLMIDI_BUILD_DLL) +# define OPNMIDI_EXPORT __declspec(dllexport) +# elif defined (LIBADLMIDI_VISIBILITY) +# define OPNMIDI_EXPORT __attribute__((visibility ("default"))) +# else +# define OPNMIDI_EXPORT +# endif +#endif + +#ifdef _WIN32 +# undef NO_OLDNAMES + +# ifdef _MSC_VER +# ifdef _WIN64 +typedef __int64 ssize_t; +# else +typedef __int32 ssize_t; +# endif +# define NOMINMAX //Don't override std::min and std::max +# endif +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // vector +#include // deque +#include // exp, log, ceil +#include +#include +#include // numeric_limit + +#ifndef _WIN32 +#include +#endif + +#include +#include + +#include "fraction.hpp" +#ifdef USE_LEGACY_EMULATOR +#include "Ym2612_ChipEmu.h" +#else +#include "ym3438.h" +#endif + +#include "opnbank.h" +#include "opnmidi.h" + +#define ADL_UNUSED(x) (void)x + +extern std::string OPN2MIDI_ErrorString; + +/* + Smart pointer for C heaps, created with malloc() call. + FAQ: Why not std::shared_ptr? Because of Android NDK now doesn't supports it +*/ +template +class AdlMIDI_CPtr +{ + PTR *m_p; +public: + AdlMIDI_CPtr() : m_p(NULL) {} + ~AdlMIDI_CPtr() + { + reset(NULL); + } + + void reset(PTR *p = NULL) + { + if(m_p) + free(m_p); + m_p = p; + } + + PTR *get() + { + return m_p; + } + PTR &operator*() + { + return *m_p; + } + PTR *operator->() + { + return m_p; + } +}; + +class OPNMIDIplay; +class OPN2 +{ +public: + friend class OPNMIDIplay; + uint32_t NumChannels; + char ____padding[4]; +#ifdef USE_LEGACY_EMULATOR + std::vector cardsOP2; +#else + std::vector cardsOP2; +#endif +private: + std::vector ins; // index to adl[], cached, needed by Touch() + std::vector pit; // value poked to B0, cached, needed by NoteOff)( + std::vector regBD; + uint8_t regLFO; + + //! Meta information about every instrument + std::vector dynamic_metainstruments; + //! Raw instrument data ready to be sent to the chip + std::vector dynamic_instruments; + size_t dynamic_percussion_offset; + + typedef std::map BankMap; + BankMap dynamic_melodic_banks; + BankMap dynamic_percussion_banks; + const unsigned DynamicInstrumentTag /* = 0x8000u*/, + DynamicMetaInstrumentTag /* = 0x4000000u*/; + const opnInstMeta &GetAdlMetaIns(size_t n); + size_t GetAdlMetaNumber(size_t midiins); + const opnInstData &GetAdlIns(size_t insno); + +public: + //! Total number of running concurrent emulated chips + unsigned int NumCards; + //! Carriers-only are scaled by default by volume level. This flag will tell to scale modulators too. + bool ScaleModulators; + //! Required to play CMF files. Can be turned on by using of "CMF" volume model + bool LogarithmicVolumes; + char ___padding2[3]; + + enum MusicMode + { + MODE_MIDI, + //MODE_IMF, OPN2 chip is not able to interpret OPL's except of a creepy and ugly conversion :-P + //MODE_CMF, CMF also is not supported :-P + MODE_RSXX + } m_musicMode; + + enum VolumesScale + { + VOLUME_Generic, + VOLUME_CMF, + VOLUME_DMX, + VOLUME_APOGEE, + VOLUME_9X + } m_volumeScale; + + OPN2(); + ~OPN2(); + char ____padding3[8]; + std::vector four_op_category; // 1 = quad-master, 2 = quad-slave, 0 = regular + // 3 = percussion BassDrum + // 4 = percussion Snare + // 5 = percussion Tom + // 6 = percussion Crash cymbal + // 7 = percussion Hihat + // 8 = percussion slave + + void PokeO(size_t card, uint8_t port, uint8_t index, uint8_t value); + + void NoteOff(size_t c); + void NoteOn(unsigned c, double hertz); + void Touch_Real(unsigned c, unsigned volume, uint8_t brightness = 127); + + void Patch(uint16_t c, size_t i); + void Pan(unsigned c, unsigned value); + void Silence(); + void ChangeVolumeRangesModel(OPNMIDI_VolumeModels volumeModel); + void ClearChips(); + void Reset(unsigned long PCM_RATE); +}; + + +/** + * @brief Hooks of the internal events + */ +struct MIDIEventHooks +{ + MIDIEventHooks() : + onEvent(NULL), + onEvent_userData(NULL), + onNote(NULL), + onNote_userData(NULL), + onDebugMessage(NULL), + onDebugMessage_userData(NULL) + {} + //! Raw MIDI event hook + typedef void (*RawEventHook)(void *userdata, uint8_t type, uint8_t subtype, uint8_t channel, const uint8_t *data, size_t len); + RawEventHook onEvent; + void *onEvent_userData; + + //! Note on/off hooks + typedef void (*NoteHook)(void *userdata, int adlchn, int note, int ins, int pressure, double bend); + NoteHook onNote; + void *onNote_userData; + + //! Library internal debug messages + typedef void (*DebugMessageHook)(void *userdata, const char *fmt, ...); + DebugMessageHook onDebugMessage; + void *onDebugMessage_userData; +}; + + +class OPNMIDIplay +{ + friend void opn2_reset(struct OPN2_MIDIPlayer*); +public: + OPNMIDIplay(unsigned long sampleRate = 22050); + + ~OPNMIDIplay() + {} + + void applySetup(); + + /**********************Internal structures and classes**********************/ + + /** + * @brief A little class gives able to read filedata from disk and also from a memory segment + */ + class fileReader + { + public: + enum relTo + { + SET = 0, + CUR = 1, + END = 2 + }; + + fileReader() + { + fp = NULL; + mp = NULL; + mp_size = 0; + mp_tell = 0; + } + ~fileReader() + { + close(); + } + + void openFile(const char *path) + { + #ifndef _WIN32 + fp = std::fopen(path, "rb"); + #else + wchar_t widePath[MAX_PATH]; + int size = MultiByteToWideChar(CP_UTF8, 0, path, (int)std::strlen(path), widePath, MAX_PATH); + widePath[size] = '\0'; + fp = _wfopen(widePath, L"rb"); + #endif + _fileName = path; + mp = NULL; + mp_size = 0; + mp_tell = 0; + } + + void openData(const void *mem, size_t lenght) + { + fp = NULL; + mp = mem; + mp_size = lenght; + mp_tell = 0; + } + + void seek(long pos, int rel_to) + { + if(fp) + std::fseek(fp, pos, rel_to); + else + { + switch(rel_to) + { + case SET: + mp_tell = static_cast(pos); + break; + + case END: + mp_tell = mp_size - static_cast(pos); + break; + + case CUR: + mp_tell = mp_tell + static_cast(pos); + break; + } + + if(mp_tell > mp_size) + mp_tell = mp_size; + } + } + + inline void seeku(uint64_t pos, int rel_to) + { + seek(static_cast(pos), rel_to); + } + + size_t read(void *buf, size_t num, size_t size) + { + if(fp) + return std::fread(buf, num, size, fp); + else + { + size_t pos = 0; + size_t maxSize = static_cast(size * num); + + while((pos < maxSize) && (mp_tell < mp_size)) + { + reinterpret_cast(buf)[pos] = reinterpret_cast(mp)[mp_tell]; + mp_tell++; + pos++; + } + + return pos; + } + } + + int getc() + { + if(fp) + return std::getc(fp); + else + { + if(mp_tell >= mp_size) return -1; + int x = reinterpret_cast(mp)[mp_tell]; + mp_tell++; + return x; + } + } + + size_t tell() + { + if(fp) + return static_cast(std::ftell(fp)); + else + return mp_tell; + } + + void close() + { + if(fp) std::fclose(fp); + + fp = NULL; + mp = NULL; + mp_size = 0; + mp_tell = 0; + } + + bool isValid() + { + return (fp) || (mp); + } + + bool eof() + { + if(fp) + return std::feof(fp); + else + return mp_tell >= mp_size; + } + std::string _fileName; + std::FILE *fp; + const void *mp; + size_t mp_size; + size_t mp_tell; + }; + + // Persistent settings for each MIDI channel + struct MIDIchannel + { + uint16_t portamento; + uint8_t bank_lsb, bank_msb; + uint8_t patch; + uint8_t volume, expression; + uint8_t panning, vibrato, sustain; + char ____padding[6]; + double bend, bendsense; + double vibpos, vibspeed, vibdepth; + int64_t vibdelay; + uint8_t lastlrpn, lastmrpn; + bool nrpn; + uint8_t brightness; + bool is_xg_percussion; + struct NoteInfo + { + // Current pressure + uint8_t vol; + char ____padding[1]; + // Tone selected on noteon: + int16_t tone; + char ____padding2[4]; + // Patch selected on noteon; index to banks[AdlBank][] + size_t midiins; + // Index to physical adlib data structure, adlins[] + size_t insmeta; + typedef std::map PhysMap; + typedef uint16_t Phys; + // List of OPN2 channels it is currently occupying. + std::map phys; + }; + typedef std::map activenotemap_t; + typedef activenotemap_t::iterator activenoteiterator; + char ____padding2[5]; + activenotemap_t activenotes; + + void reset() + { + portamento = 0; + bank_lsb = 0; + bank_msb = 0; + patch = 0; + volume = 100; + expression = 127; + panning = 0xC0; + vibrato = 0; + sustain = 0; + bend = 0.0; + bendsense = 2 / 8192.0; + vibpos = 0; + vibspeed = 2 * 3.141592653 * 5.0; + vibdepth = 0.5 / 127; + vibdelay = 0; + lastlrpn = 0; + lastmrpn = 0; + nrpn = false; + brightness = 127; + is_xg_percussion = false; + } + + MIDIchannel() + : activenotes() + { + reset(); + } + }; + + // Additional information about OPN channels + struct OpnChannel + { + // For collisions + struct Location + { + uint16_t MidCh; + uint8_t note; + bool operator==(const Location &b) const + { + return MidCh == b.MidCh && note == b.note; + } + bool operator< (const Location &b) const + { + return MidCh < b.MidCh || (MidCh == b.MidCh && note < b.note); + } + char ____padding[1]; + }; + struct LocationData + { + bool sustained; + char ____padding[1]; + MIDIchannel::NoteInfo::Phys ins; // a copy of that in phys[] + char ____padding2[4]; + int64_t kon_time_until_neglible; + int64_t vibdelay; + }; + typedef std::map users_t; + users_t users; + + // If the channel is keyoff'd + int64_t koff_time_until_neglible; + // For channel allocation: + OpnChannel(): users(), koff_time_until_neglible(0) { } + void AddAge(int64_t ms); + }; + + /** + * @brief MIDI Event utility container + */ + class MidiEvent + { + public: + MidiEvent(); + + enum Types + { + T_UNKNOWN = 0x00, + T_NOTEOFF = 0x08,//size == 2 + T_NOTEON = 0x09,//size == 2 + T_NOTETOUCH = 0x0A,//size == 2 + T_CTRLCHANGE = 0x0B,//size == 2 + T_PATCHCHANGE = 0x0C,//size == 1 + T_CHANAFTTOUCH = 0x0D,//size == 1 + T_WHEEL = 0x0E,//size == 2 + + T_SYSEX = 0xF0,//size == len + T_SYSCOMSPOSPTR = 0xF2,//size == 2 + T_SYSCOMSNGSEL = 0xF3,//size == 1 + T_SYSEX2 = 0xF7,//size == len + T_SPECIAL = 0xFF + }; + enum SubTypes + { + ST_SEQNUMBER = 0x00,//size == 2 + ST_TEXT = 0x01,//size == len + ST_COPYRIGHT = 0x02,//size == len + ST_SQTRKTITLE = 0x03,//size == len + ST_INSTRTITLE = 0x04,//size == len + ST_LYRICS = 0x05,//size == len + ST_MARKER = 0x06,//size == len + ST_CUEPOINT = 0x07,//size == len + ST_DEVICESWITCH = 0x09,//size == len + ST_MIDICHPREFIX = 0x20,//size == 1 + + ST_ENDTRACK = 0x2F,//size == 0 + ST_TEMPOCHANGE = 0x51,//size == 3 + ST_SMPTEOFFSET = 0x54,//size == 5 + ST_TIMESIGNATURE = 0x55, //size == 4 + ST_KEYSIGNATURE = 0x59,//size == 2 + ST_SEQUENCERSPEC = 0x7F, //size == len + + /* Non-standard, internal ADLMIDI usage only */ + ST_LOOPSTART = 0xE1,//size == 0 + ST_LOOPEND = 0xE2,//size == 0 + ST_RAWOPL = 0xE3//size == 0 + }; + //! Main type of event + uint8_t type; + //! Sub-type of the event + uint8_t subtype; + //! Targeted MIDI channel + uint8_t channel; + //! Is valid event + uint8_t isValid; + //! Reserved 5 bytes padding + uint8_t __padding[4]; + //! Absolute tick position (Used for the tempo calculation only) + uint64_t absPosition; + //! Raw data of this event + std::vector data; + }; + + /** + * @brief A track position event contains a chain of MIDI events until next delay value + * + * Created with purpose to sort events by type in the same position + * (for example, to keep controllers always first than note on events or lower than note-off events) + */ + class MidiTrackRow + { + public: + MidiTrackRow(); + void reset(); + //! Absolute time position in seconds + double time; + //! Delay to next event in ticks + uint64_t delay; + //! Absolute position in ticks + uint64_t absPos; + //! Delay to next event in seconds + double timeDelay; + std::vector events; + /** + * @brief Sort events in this position + */ + void sortEvents(bool *noteStates = NULL); + }; + + /** + * @brief Tempo change point entry. Used in the MIDI data building function only. + */ + struct TempoChangePoint + { + uint64_t absPos; + fraction tempo; + }; + //P.S. I declared it here instead of local in-function because C++99 can't process templates with locally-declared structures + + typedef std::list MidiTrackQueue; + + // Information about each track + struct PositionNew + { + bool began; + char padding[7]; + double wait; + double absTimePosition; + struct TrackInfo + { + size_t ptr; + uint64_t delay; + int status; + char padding2[4]; + MidiTrackQueue::iterator pos; + TrackInfo(): ptr(0), delay(0), status(0) {} + }; + std::vector track; + PositionNew(): began(false), wait(0.0), absTimePosition(0.0), track() + {} + }; + + struct Setup + { + unsigned int OpnBank; + unsigned int NumCards; + unsigned int LogarithmicVolumes; + int VolumeModel; + //unsigned int SkipForward; + bool loopingIsEnabled; + int ScaleModulators; + + double delay; + double carry; + + /* The lag between visual content and audio content equals */ + /* the sum of these two buffers. */ + double mindelay; + double maxdelay; + + /* For internal usage */ + ssize_t tick_skip_samples_delay; /* Skip tick processing after samples count. */ + /* For internal usage */ + + unsigned long PCM_RATE; + }; + + struct MIDI_MarkerEntry + { + std::string label; + double pos_time; + uint64_t pos_ticks; + }; + + std::vector Ch; + //bool cmf_percussion_mode; + + MIDIEventHooks hooks; + +private: + std::map devices; + std::map current_device; + + std::vector ch; + std::vector > TrackData; + + PositionNew CurrentPositionNew, LoopBeginPositionNew, trackBeginPositionNew; + + //! Full song length in seconds + double fullSongTimeLength; + //! Delay after song playd before rejecting the output stream requests + double postSongWaitDelay; + + //! Loop start time + double loopStartTime; + //! Loop end time + double loopEndTime; + //! Local error string + std::string errorString; + //! Local error string + std::string errorStringOut; + + //! Pre-processed track data storage + std::vector trackDataNew; + + //! Missing instruments catches + std::set caugh_missing_instruments; + //! Missing melodic banks catches + std::set caugh_missing_banks_melodic; + //! Missing percussion banks catches + std::set caugh_missing_banks_percussion; + + /** + * @brief Build MIDI track data from the raw track data storage + * @return true if everything successfully processed, or false on any error + */ + bool buildTrackData(); + + /** + * @brief Parse one event from raw MIDI track stream + * @param [_inout] ptr pointer to pointer to current position on the raw data track + * @param [_in] end address to end of raw track data, needed to validate position and size + * @param [_inout] status status of the track processing + * @return Parsed MIDI event entry + */ + MidiEvent parseEvent(uint8_t **ptr, uint8_t *end, int &status); + +public: + + const std::string &getErrorString(); + void setErrorString(const std::string &err); + + std::string musTitle; + std::string musCopyright; + std::vector musTrackTitles; + std::vector musMarkers; + + fraction InvDeltaTicks, Tempo; + //! Tempo multiplier + double tempoMultiplier; + bool atEnd, + loopStart, + loopEnd, + invalidLoop; /*Loop points are invalid (loopStart after loopEnd or loopStart and loopEnd are on same place)*/ + char ____padding2[2]; + OPN2 opn; + + int16_t outBuf[1024]; + + Setup m_setup; + + static uint64_t ReadBEint(const void *buffer, size_t nbytes); + static uint64_t ReadLEint(const void *buffer, size_t nbytes); + + /** + * @brief Standard MIDI Variable-Length numeric value parser without of validation + * @param [_inout] ptr Pointer to memory block that contains begin of variable-length value + * @return Unsigned integer that conains parsed variable-length value + */ + uint64_t ReadVarLen(uint8_t **ptr); + /** + * @brief Secure Standard MIDI Variable-Length numeric value parser with anti-out-of-range protection + * @param [_inout] ptr Pointer to memory block that contains begin of variable-length value, will be iterated forward + * @param [_in end Pointer to end of memory block where variable-length value is stored (after end of track) + * @param [_out] ok Reference to boolean which takes result of variable-length value parsing + * @return Unsigned integer that conains parsed variable-length value + */ + uint64_t ReadVarLenEx(uint8_t **ptr, uint8_t *end, bool &ok); + + bool LoadBank(const std::string &filename); + bool LoadBank(const void *data, size_t size); + bool LoadBank(fileReader &fr); + + bool LoadMIDI(const std::string &filename); + bool LoadMIDI(const void *data, size_t size); + bool LoadMIDI(fileReader &fr); + + /** + * @brief Periodic tick handler. + * @param s seconds since last call + * @param granularity don't expect intervals smaller than this, in seconds + * @return desired number of seconds until next call + */ + double Tick(double s, double granularity); + + /** + * @brief Process extra iterators like vibrato or arpeggio + * @param s seconds since last call + */ + void TickIteratos(double s); + + /** + * @brief Change current position to specified time position in seconds + * @param seconds Absolute time position in seconds + */ + void seek(double seconds); + + /** + * @brief Gives current time position in seconds + * @return Current time position in seconds + */ + double tell(); + + /** + * @brief Gives time length of current song in seconds + * @return Time length of current song in seconds + */ + double timeLength(); + + /** + * @brief Gives loop start time position in seconds + * @return Loop start time position in seconds or -1 if song has no loop points + */ + double getLoopStart(); + + /** + * @brief Gives loop end time position in seconds + * @return Loop end time position in seconds or -1 if song has no loop points + */ + double getLoopEnd(); + + /** + * @brief Return to begin of current song + */ + void rewind(); + + /** + * @brief Set tempo multiplier + * @param tempo Tempo multiplier: 1.0 - original tempo. >1 - faster, <1 - slower + */ + void setTempo(double tempo); + + /* RealTime event triggers */ + void realTime_ResetState(); + + bool realTime_NoteOn(uint8_t channel, uint8_t note, uint8_t velocity); + void realTime_NoteOff(uint8_t channel, uint8_t note); + + void realTime_NoteAfterTouch(uint8_t channel, uint8_t note, uint8_t atVal); + void realTime_ChannelAfterTouch(uint8_t channel, uint8_t atVal); + + void realTime_Controller(uint8_t channel, uint8_t type, uint8_t value); + + void realTime_PatchChange(uint8_t channel, uint8_t patch); + + void realTime_PitchBend(uint8_t channel, uint16_t pitch); + void realTime_PitchBend(uint8_t channel, uint8_t msb, uint8_t lsb); + + void realTime_BankChangeLSB(uint8_t channel, uint8_t lsb); + void realTime_BankChangeMSB(uint8_t channel, uint8_t msb); + void realTime_BankChange(uint8_t channel, uint16_t bank); + + void realTime_panic(); + +private: + enum + { + Upd_Patch = 0x1, + Upd_Pan = 0x2, + Upd_Volume = 0x4, + Upd_Pitch = 0x8, + Upd_All = Upd_Pan + Upd_Volume + Upd_Pitch, + Upd_Off = 0x20 + }; + + void NoteUpdate(uint16_t MidCh, + MIDIchannel::activenoteiterator i, + unsigned props_mask, + int32_t select_adlchn = -1); + bool ProcessEventsNew(bool isSeek = false); + void HandleEvent(size_t tk, const MidiEvent &evt, int &status); + + // Determine how good a candidate this adlchannel + // would be for playing a note from this instrument. + long CalculateAdlChannelGoodness(size_t c, uint16_t ins, uint16_t /*MidCh*/) const; + + // A new note will be played on this channel using this instrument. + // Kill existing notes on this channel (or don't, if we do arpeggio) + void PrepareAdlChannelForNewNote(size_t c, size_t ins); + + void KillOrEvacuate( + size_t from_channel, + OpnChannel::users_t::iterator j, + MIDIchannel::activenoteiterator i); + void Panic(); + void KillSustainingNotes(int32_t MidCh = -1, int32_t this_adlchn = -1); + void SetRPN(unsigned MidCh, unsigned value, bool MSB); + //void UpdatePortamento(unsigned MidCh) + void NoteUpdate_All(uint16_t MidCh, unsigned props_mask); + void NoteOff(uint16_t MidCh, uint8_t note); + void UpdateVibrato(double amount); + void UpdateArpeggio(double /*amount*/); + +public: + uint64_t ChooseDevice(const std::string &name); +}; + +extern int opn2RefreshNumCards(OPN2_MIDIPlayer *device); + + +#endif // ADLMIDI_PRIVATE_HPP diff --git a/src/sound/opnmidi/opnmidi_xmi2mid.c b/src/sound/opnmidi/opnmidi_xmi2mid.c new file mode 100644 index 000000000..b90905273 --- /dev/null +++ b/src/sound/opnmidi/opnmidi_xmi2mid.c @@ -0,0 +1,1106 @@ +/* + * XMIDI: Miles XMIDI to MID Library + * + * Copyright (C) 2001 Ryan Nunn + * Copyright (C) 2014 Bret Curtis + * Copyright (C) WildMIDI Developers 2015-2016 + * Copyright (c) 2017-2018 Vitaly Novichkov + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/* XMIDI Converter */ + +#include +#include +#include +#include + +#include "opnmidi_xmi2mid.h" + +/* Midi Status Bytes */ +#define MIDI_STATUS_NOTE_OFF 0x8 +#define MIDI_STATUS_NOTE_ON 0x9 +#define MIDI_STATUS_AFTERTOUCH 0xA +#define MIDI_STATUS_CONTROLLER 0xB +#define MIDI_STATUS_PROG_CHANGE 0xC +#define MIDI_STATUS_PRESSURE 0xD +#define MIDI_STATUS_PITCH_WHEEL 0xE +#define MIDI_STATUS_SYSEX 0xF + +typedef struct _midi_event { + int32_t time; + uint8_t status; + uint8_t data[2]; + uint32_t len; + uint8_t *buffer; + struct _midi_event *next; +} midi_event; + +typedef struct { + uint16_t type; + uint16_t tracks; +} midi_descriptor; + +struct xmi_ctx { + uint8_t *src, *src_ptr; + uint32_t srcsize; + uint32_t datastart; + uint8_t *dst, *dst_ptr; + uint32_t dstsize, dstrem; + uint32_t convert_type; + midi_descriptor info; + int bank127[16]; + midi_event **events; + signed short *timing; + midi_event *list; + midi_event *current; +}; + +/* forward declarations of private functions */ +static void DeleteEventList(midi_event *mlist); +static void CreateNewEvent(struct xmi_ctx *ctx, int32_t time); /* List manipulation */ +static int GetVLQ(struct xmi_ctx *ctx, uint32_t *quant); /* Variable length quantity */ +static int GetVLQ2(struct xmi_ctx *ctx, uint32_t *quant);/* Variable length quantity */ +static int PutVLQ(struct xmi_ctx *ctx, uint32_t value); /* Variable length quantity */ +static int ConvertEvent(struct xmi_ctx *ctx, + const int32_t time, const uint8_t status, const int size); +static int32_t ConvertSystemMessage(struct xmi_ctx *ctx, + const int32_t time, const uint8_t status); +static int32_t ConvertFiletoList(struct xmi_ctx *ctx); +static uint32_t ConvertListToMTrk(struct xmi_ctx *ctx, midi_event *mlist); +static int ParseXMI(struct xmi_ctx *ctx); +static int ExtractTracks(struct xmi_ctx *ctx); +static uint32_t ExtractTracksFromXmi(struct xmi_ctx *ctx); + +static uint32_t read1(struct xmi_ctx *ctx) +{ + uint8_t b0; + b0 = *ctx->src_ptr++; + return (b0); +} + +static uint32_t read2(struct xmi_ctx *ctx) +{ + uint8_t b0, b1; + b0 = *ctx->src_ptr++; + b1 = *ctx->src_ptr++; + return (b0 + ((uint32_t)b1 << 8)); +} + +static uint32_t read4(struct xmi_ctx *ctx) +{ + uint8_t b0, b1, b2, b3; + b3 = *ctx->src_ptr++; + b2 = *ctx->src_ptr++; + b1 = *ctx->src_ptr++; + b0 = *ctx->src_ptr++; + return (b0 + ((uint32_t)b1<<8) + ((uint32_t)b2<<16) + ((uint32_t)b3<<24)); +} + +static void copy(struct xmi_ctx *ctx, char *b, uint32_t len) +{ + memcpy(b, ctx->src_ptr, len); + ctx->src_ptr += len; +} + +#define DST_CHUNK 8192 +static void resize_dst(struct xmi_ctx *ctx) { + uint32_t pos = (uint32_t)(ctx->dst_ptr - ctx->dst); + ctx->dst = realloc(ctx->dst, ctx->dstsize + DST_CHUNK); + ctx->dstsize += DST_CHUNK; + ctx->dstrem += DST_CHUNK; + ctx->dst_ptr = ctx->dst + pos; +} + +static void write1(struct xmi_ctx *ctx, uint32_t val) +{ + if (ctx->dstrem < 1) + resize_dst(ctx); + *ctx->dst_ptr++ = val & 0xff; + ctx->dstrem--; +} + +static void write2(struct xmi_ctx *ctx, uint32_t val) +{ + if (ctx->dstrem < 2) + resize_dst(ctx); + *ctx->dst_ptr++ = (val>>8) & 0xff; + *ctx->dst_ptr++ = val & 0xff; + ctx->dstrem -= 2; +} + +static void write4(struct xmi_ctx *ctx, uint32_t val) +{ + if (ctx->dstrem < 4) + resize_dst(ctx); + *ctx->dst_ptr++ = (val>>24)&0xff; + *ctx->dst_ptr++ = (val>>16)&0xff; + *ctx->dst_ptr++ = (val>>8) & 0xff; + *ctx->dst_ptr++ = val & 0xff; + ctx->dstrem -= 4; +} + +static void seeksrc(struct xmi_ctx *ctx, uint32_t pos) { + ctx->src_ptr = ctx->src + pos; +} + +static void seekdst(struct xmi_ctx *ctx, uint32_t pos) { + ctx->dst_ptr = ctx->dst + pos; + while (ctx->dstsize < pos) + resize_dst(ctx); + ctx->dstrem = ctx->dstsize - pos; +} + +static void skipsrc(struct xmi_ctx *ctx, int32_t pos) { + ctx->src_ptr += pos; +} + +static void skipdst(struct xmi_ctx *ctx, int32_t pos) { + size_t newpos; + ctx->dst_ptr += pos; + newpos = ctx->dst_ptr - ctx->dst; + while (ctx->dstsize < newpos) + resize_dst(ctx); + ctx->dstrem = (uint32_t)(ctx->dstsize - newpos); +} + +static uint32_t getsrcsize(struct xmi_ctx *ctx) { + return (ctx->srcsize); +} + +static uint32_t getsrcpos(struct xmi_ctx *ctx) { + return (uint32_t)(ctx->src_ptr - ctx->src); +} + +static uint32_t getdstpos(struct xmi_ctx *ctx) { + return (uint32_t)(ctx->dst_ptr - ctx->dst); +} + +/* This is a default set of patches to convert from MT32 to GM + * The index is the MT32 Patch number and the value is the GM Patch + * This is only suitable for music that doesn't do timbre changes + * XMIDIs that contain Timbre changes will not convert properly. + */ +static const char mt32asgm[128] = { + 0, /* 0 Piano 1 */ + 1, /* 1 Piano 2 */ + 2, /* 2 Piano 3 (synth) */ + 4, /* 3 EPiano 1 */ + 4, /* 4 EPiano 2 */ + 5, /* 5 EPiano 3 */ + 5, /* 6 EPiano 4 */ + 3, /* 7 Honkytonk */ + 16, /* 8 Organ 1 */ + 17, /* 9 Organ 2 */ + 18, /* 10 Organ 3 */ + 16, /* 11 Organ 4 */ + 19, /* 12 Pipe Organ 1 */ + 19, /* 13 Pipe Organ 2 */ + 19, /* 14 Pipe Organ 3 */ + 21, /* 15 Accordion */ + 6, /* 16 Harpsichord 1 */ + 6, /* 17 Harpsichord 2 */ + 6, /* 18 Harpsichord 3 */ + 7, /* 19 Clavinet 1 */ + 7, /* 20 Clavinet 2 */ + 7, /* 21 Clavinet 3 */ + 8, /* 22 Celesta 1 */ + 8, /* 23 Celesta 2 */ + 62, /* 24 Synthbrass 1 (62) */ + 63, /* 25 Synthbrass 2 (63) */ + 62, /* 26 Synthbrass 3 Bank 8 */ + 63, /* 27 Synthbrass 4 Bank 8 */ + 38, /* 28 Synthbass 1 */ + 39, /* 29 Synthbass 2 */ + 38, /* 30 Synthbass 3 Bank 8 */ + 39, /* 31 Synthbass 4 Bank 8 */ + 88, /* 32 Fantasy */ + 90, /* 33 Harmonic Pan - No equiv closest is polysynth(90) :( */ + 52, /* 34 Choral ?? Currently set to SynthVox(54). Should it be ChoirAhhs(52)??? */ + 92, /* 35 Glass */ + 97, /* 36 Soundtrack */ + 99, /* 37 Atmosphere */ + 14, /* 38 Warmbell, sounds kind of like crystal(98) perhaps Tubular Bells(14) would be better. It is! */ + 54, /* 39 FunnyVox, sounds alot like Bagpipe(109) and Shania(111) */ + 98, /* 40 EchoBell, no real equiv, sounds like Crystal(98) */ + 96, /* 41 IceRain */ + 68, /* 42 Oboe 2001, no equiv, just patching it to normal oboe(68) */ + 95, /* 43 EchoPans, no equiv, setting to SweepPad */ + 81, /* 44 DoctorSolo Bank 8 */ + 87, /* 45 SchoolDaze, no real equiv */ + 112,/* 46 Bell Singer */ + 80, /* 47 SquareWave */ + 48, /* 48 Strings 1 */ + 48, /* 49 Strings 2 - should be 49 */ + 44, /* 50 Strings 3 (Synth) - Experimental set to Tremollo Strings - should be 50 */ + 45, /* 51 Pizzicato Strings */ + 40, /* 52 Violin 1 */ + 40, /* 53 Violin 2 ? Viola */ + 42, /* 54 Cello 1 */ + 42, /* 55 Cello 2 */ + 43, /* 56 Contrabass */ + 46, /* 57 Harp 1 */ + 46, /* 58 Harp 2 */ + 24, /* 59 Guitar 1 (Nylon) */ + 25, /* 60 Guitar 2 (Steel) */ + 26, /* 61 Elec Guitar 1 */ + 27, /* 62 Elec Guitar 2 */ + 104,/* 63 Sitar */ + 32, /* 64 Acou Bass 1 */ + 32, /* 65 Acou Bass 2 */ + 33, /* 66 Elec Bass 1 */ + 34, /* 67 Elec Bass 2 */ + 36, /* 68 Slap Bass 1 */ + 37, /* 69 Slap Bass 2 */ + 35, /* 70 Fretless Bass 1 */ + 35, /* 71 Fretless Bass 2 */ + 73, /* 72 Flute 1 */ + 73, /* 73 Flute 2 */ + 72, /* 74 Piccolo 1 */ + 72, /* 75 Piccolo 2 */ + 74, /* 76 Recorder */ + 75, /* 77 Pan Pipes */ + 64, /* 78 Sax 1 */ + 65, /* 79 Sax 2 */ + 66, /* 80 Sax 3 */ + 67, /* 81 Sax 4 */ + 71, /* 82 Clarinet 1 */ + 71, /* 83 Clarinet 2 */ + 68, /* 84 Oboe */ + 69, /* 85 English Horn (Cor Anglais) */ + 70, /* 86 Bassoon */ + 22, /* 87 Harmonica */ + 56, /* 88 Trumpet 1 */ + 56, /* 89 Trumpet 2 */ + 57, /* 90 Trombone 1 */ + 57, /* 91 Trombone 2 */ + 60, /* 92 French Horn 1 */ + 60, /* 93 French Horn 2 */ + 58, /* 94 Tuba */ + 61, /* 95 Brass Section 1 */ + 61, /* 96 Brass Section 2 */ + 11, /* 97 Vibes 1 */ + 11, /* 98 Vibes 2 */ + 99, /* 99 Syn Mallet Bank 1 */ + 112,/* 100 WindBell no real equiv Set to TinkleBell(112) */ + 9, /* 101 Glockenspiel */ + 14, /* 102 Tubular Bells */ + 13, /* 103 Xylophone */ + 12, /* 104 Marimba */ + 107,/* 105 Koto */ + 111,/* 106 Sho?? set to Shanai(111) */ + 77, /* 107 Shakauhachi */ + 78, /* 108 Whistle 1 */ + 78, /* 109 Whistle 2 */ + 76, /* 110 Bottle Blow */ + 76, /* 111 Breathpipe no real equiv set to bottle blow(76) */ + 47, /* 112 Timpani */ + 117,/* 113 Melodic Tom */ + 116,/* 114 Deap Snare no equiv, set to Taiko(116) */ + 118,/* 115 Electric Perc 1 */ + 118,/* 116 Electric Perc 2 */ + 116,/* 117 Taiko */ + 115,/* 118 Taiko Rim, no real equiv, set to Woodblock(115) */ + 119,/* 119 Cymbal, no real equiv, set to reverse cymbal(119) */ + 115,/* 120 Castanets, no real equiv, in GM set to Woodblock(115) */ + 112,/* 121 Triangle, no real equiv, set to TinkleBell(112) */ + 55, /* 122 Orchestral Hit */ + 124,/* 123 Telephone */ + 123,/* 124 BirdTweet */ + 94, /* 125 Big Notes Pad no equiv, set to halo pad (94) */ + 98, /* 126 Water Bell set to Crystal Pad(98) */ + 121 /* 127 Jungle Tune set to Breath Noise */ +}; + +/* Same as above, except include patch changes + * so GS instruments can be used */ +static const char mt32asgs[256] = { + 0, 0, /* 0 Piano 1 */ + 1, 0, /* 1 Piano 2 */ + 2, 0, /* 2 Piano 3 (synth) */ + 4, 0, /* 3 EPiano 1 */ + 4, 0, /* 4 EPiano 2 */ + 5, 0, /* 5 EPiano 3 */ + 5, 0, /* 6 EPiano 4 */ + 3, 0, /* 7 Honkytonk */ + 16, 0, /* 8 Organ 1 */ + 17, 0, /* 9 Organ 2 */ + 18, 0, /* 10 Organ 3 */ + 16, 0, /* 11 Organ 4 */ + 19, 0, /* 12 Pipe Organ 1 */ + 19, 0, /* 13 Pipe Organ 2 */ + 19, 0, /* 14 Pipe Organ 3 */ + 21, 0, /* 15 Accordion */ + 6, 0, /* 16 Harpsichord 1 */ + 6, 0, /* 17 Harpsichord 2 */ + 6, 0, /* 18 Harpsichord 3 */ + 7, 0, /* 19 Clavinet 1 */ + 7, 0, /* 20 Clavinet 2 */ + 7, 0, /* 21 Clavinet 3 */ + 8, 0, /* 22 Celesta 1 */ + 8, 0, /* 23 Celesta 2 */ + 62, 0, /* 24 Synthbrass 1 (62) */ + 63, 0, /* 25 Synthbrass 2 (63) */ + 62, 0, /* 26 Synthbrass 3 Bank 8 */ + 63, 0, /* 27 Synthbrass 4 Bank 8 */ + 38, 0, /* 28 Synthbass 1 */ + 39, 0, /* 29 Synthbass 2 */ + 38, 0, /* 30 Synthbass 3 Bank 8 */ + 39, 0, /* 31 Synthbass 4 Bank 8 */ + 88, 0, /* 32 Fantasy */ + 90, 0, /* 33 Harmonic Pan - No equiv closest is polysynth(90) :( */ + 52, 0, /* 34 Choral ?? Currently set to SynthVox(54). Should it be ChoirAhhs(52)??? */ + 92, 0, /* 35 Glass */ + 97, 0, /* 36 Soundtrack */ + 99, 0, /* 37 Atmosphere */ + 14, 0, /* 38 Warmbell, sounds kind of like crystal(98) perhaps Tubular Bells(14) would be better. It is! */ + 54, 0, /* 39 FunnyVox, sounds alot like Bagpipe(109) and Shania(111) */ + 98, 0, /* 40 EchoBell, no real equiv, sounds like Crystal(98) */ + 96, 0, /* 41 IceRain */ + 68, 0, /* 42 Oboe 2001, no equiv, just patching it to normal oboe(68) */ + 95, 0, /* 43 EchoPans, no equiv, setting to SweepPad */ + 81, 0, /* 44 DoctorSolo Bank 8 */ + 87, 0, /* 45 SchoolDaze, no real equiv */ + 112, 0, /* 46 Bell Singer */ + 80, 0, /* 47 SquareWave */ + 48, 0, /* 48 Strings 1 */ + 48, 0, /* 49 Strings 2 - should be 49 */ + 44, 0, /* 50 Strings 3 (Synth) - Experimental set to Tremollo Strings - should be 50 */ + 45, 0, /* 51 Pizzicato Strings */ + 40, 0, /* 52 Violin 1 */ + 40, 0, /* 53 Violin 2 ? Viola */ + 42, 0, /* 54 Cello 1 */ + 42, 0, /* 55 Cello 2 */ + 43, 0, /* 56 Contrabass */ + 46, 0, /* 57 Harp 1 */ + 46, 0, /* 58 Harp 2 */ + 24, 0, /* 59 Guitar 1 (Nylon) */ + 25, 0, /* 60 Guitar 2 (Steel) */ + 26, 0, /* 61 Elec Guitar 1 */ + 27, 0, /* 62 Elec Guitar 2 */ + 104, 0, /* 63 Sitar */ + 32, 0, /* 64 Acou Bass 1 */ + 32, 0, /* 65 Acou Bass 2 */ + 33, 0, /* 66 Elec Bass 1 */ + 34, 0, /* 67 Elec Bass 2 */ + 36, 0, /* 68 Slap Bass 1 */ + 37, 0, /* 69 Slap Bass 2 */ + 35, 0, /* 70 Fretless Bass 1 */ + 35, 0, /* 71 Fretless Bass 2 */ + 73, 0, /* 72 Flute 1 */ + 73, 0, /* 73 Flute 2 */ + 72, 0, /* 74 Piccolo 1 */ + 72, 0, /* 75 Piccolo 2 */ + 74, 0, /* 76 Recorder */ + 75, 0, /* 77 Pan Pipes */ + 64, 0, /* 78 Sax 1 */ + 65, 0, /* 79 Sax 2 */ + 66, 0, /* 80 Sax 3 */ + 67, 0, /* 81 Sax 4 */ + 71, 0, /* 82 Clarinet 1 */ + 71, 0, /* 83 Clarinet 2 */ + 68, 0, /* 84 Oboe */ + 69, 0, /* 85 English Horn (Cor Anglais) */ + 70, 0, /* 86 Bassoon */ + 22, 0, /* 87 Harmonica */ + 56, 0, /* 88 Trumpet 1 */ + 56, 0, /* 89 Trumpet 2 */ + 57, 0, /* 90 Trombone 1 */ + 57, 0, /* 91 Trombone 2 */ + 60, 0, /* 92 French Horn 1 */ + 60, 0, /* 93 French Horn 2 */ + 58, 0, /* 94 Tuba */ + 61, 0, /* 95 Brass Section 1 */ + 61, 0, /* 96 Brass Section 2 */ + 11, 0, /* 97 Vibes 1 */ + 11, 0, /* 98 Vibes 2 */ + 99, 0, /* 99 Syn Mallet Bank 1 */ + 112, 0, /* 100 WindBell no real equiv Set to TinkleBell(112) */ + 9, 0, /* 101 Glockenspiel */ + 14, 0, /* 102 Tubular Bells */ + 13, 0, /* 103 Xylophone */ + 12, 0, /* 104 Marimba */ + 107, 0, /* 105 Koto */ + 111, 0, /* 106 Sho?? set to Shanai(111) */ + 77, 0, /* 107 Shakauhachi */ + 78, 0, /* 108 Whistle 1 */ + 78, 0, /* 109 Whistle 2 */ + 76, 0, /* 110 Bottle Blow */ + 76, 0, /* 111 Breathpipe no real equiv set to bottle blow(76) */ + 47, 0, /* 112 Timpani */ + 117, 0, /* 113 Melodic Tom */ + 116, 0, /* 114 Deap Snare no equiv, set to Taiko(116) */ + 118, 0, /* 115 Electric Perc 1 */ + 118, 0, /* 116 Electric Perc 2 */ + 116, 0, /* 117 Taiko */ + 115, 0, /* 118 Taiko Rim, no real equiv, set to Woodblock(115) */ + 119, 0, /* 119 Cymbal, no real equiv, set to reverse cymbal(119) */ + 115, 0, /* 120 Castanets, no real equiv, in GM set to Woodblock(115) */ + 112, 0, /* 121 Triangle, no real equiv, set to TinkleBell(112) */ + 55, 0, /* 122 Orchestral Hit */ + 124, 0, /* 123 Telephone */ + 123, 0, /* 124 BirdTweet */ + 94, 0, /* 125 Big Notes Pad no equiv, set to halo pad (94) */ + 98, 0, /* 126 Water Bell set to Crystal Pad(98) */ + 121, 0 /* 127 Jungle Tune set to Breath Noise */ +}; + +int OpnMidi_xmi2midi(uint8_t *in, uint32_t insize, + uint8_t **out, uint32_t *outsize, + uint32_t convert_type) { + struct xmi_ctx ctx; + unsigned int i; + int ret = -1; + + if (convert_type > XMIDI_CONVERT_MT32_TO_GS) { + /*_WM_ERROR_NEW("%s:%i: %d is an invalid conversion type.", __FUNCTION__, __LINE__, convert_type);*/ + return (ret); + } + + memset(&ctx, 0, sizeof(struct xmi_ctx)); + ctx.src = ctx.src_ptr = in; + ctx.srcsize = insize; + ctx.convert_type = convert_type; + + if (ParseXMI(&ctx) < 0) { + /*_WM_GLOBAL_ERROR(__FUNCTION__, __LINE__, WM_ERR_NOT_XMI, NULL, 0);*/ + goto _end; + } + + if (ExtractTracks(&ctx) < 0) { + /*_WM_GLOBAL_ERROR(__FUNCTION__, __LINE__, WM_ERR_NOT_MIDI, NULL, 0);*/ + goto _end; + } + + ctx.dst = malloc(DST_CHUNK); + ctx.dst_ptr = ctx.dst; + ctx.dstsize = DST_CHUNK; + ctx.dstrem = DST_CHUNK; + + /* Header is 14 bytes long and add the rest as well */ + write1(&ctx, 'M'); + write1(&ctx, 'T'); + write1(&ctx, 'h'); + write1(&ctx, 'd'); + + write4(&ctx, 6); + + write2(&ctx, ctx.info.type); + write2(&ctx, ctx.info.tracks); + write2(&ctx, ctx.timing[0]);/* write divisions from track0 */ + + for (i = 0; i < ctx.info.tracks; i++) + ConvertListToMTrk(&ctx, ctx.events[i]); + *out = ctx.dst; + *outsize = ctx.dstsize - ctx.dstrem; + ret = 0; + +_end: /* cleanup */ + if (ret < 0) { + free(ctx.dst); + *out = NULL; + *outsize = 0; + } + if (ctx.events) { + for (i = 0; i < ctx.info.tracks; i++) + DeleteEventList(ctx.events[i]); + free(ctx.events); + } + free(ctx.timing); + + return (ret); +} + +static void DeleteEventList(midi_event *mlist) { + midi_event *event; + midi_event *next; + + next = mlist; + + while ((event = next) != NULL) { + next = event->next; + free(event->buffer); + free(event); + } +} + +/* Sets current to the new event and updates list */ +static void CreateNewEvent(struct xmi_ctx *ctx, int32_t time) { + if (!ctx->list) { + ctx->list = ctx->current = calloc(1, sizeof(midi_event)); + ctx->current->time = (time < 0)? 0 : time; + return; + } + + if (time < 0) { + midi_event *event = calloc(1, sizeof(midi_event)); + event->next = ctx->list; + ctx->list = ctx->current = event; + return; + } + + if (ctx->current->time > time) + ctx->current = ctx->list; + + while (ctx->current->next) { + if (ctx->current->next->time > time) { + midi_event *event = calloc(1, sizeof(midi_event)); + event->next = ctx->current->next; + ctx->current->next = event; + ctx->current = event; + ctx->current->time = time; + return; + } + + ctx->current = ctx->current->next; + } + + ctx->current->next = calloc(1, sizeof(midi_event)); + ctx->current = ctx->current->next; + ctx->current->time = time; +} + +/* Conventional Variable Length Quantity */ +static int GetVLQ(struct xmi_ctx *ctx, uint32_t *quant) { + int i; + uint32_t data; + + *quant = 0; + for (i = 0; i < 4; i++) { + data = read1(ctx); + *quant <<= 7; + *quant |= data & 0x7F; + + if (!(data & 0x80)) { + i++; + break; + } + } + return (i); +} + +/* XMIDI Delta Variable Length Quantity */ +static int GetVLQ2(struct xmi_ctx *ctx, uint32_t *quant) { + int i; + int32_t data; + + *quant = 0; + for (i = 0; i < 4; i++) { + data = read1(ctx); + if (data & 0x80) { + skipsrc(ctx, -1); + break; + } + *quant += data; + } + return (i); +} + +static int PutVLQ(struct xmi_ctx *ctx, uint32_t value) { + int32_t buffer; + int i = 1, j; + buffer = value & 0x7F; + while (value >>= 7) { + buffer <<= 8; + buffer |= ((value & 0x7F) | 0x80); + i++; + } + for (j = 0; j < i; j++) { + write1(ctx, buffer & 0xFF); + buffer >>= 8; + } + + return (i); +} + +/* Converts Events + * + * Source is at the first data byte + * size 1 is single data byte + * size 2 is dual data byte + * size 3 is XMI Note on + * Returns bytes converted */ +static int ConvertEvent(struct xmi_ctx *ctx, const int32_t time, + const uint8_t status, const int size) { + uint32_t delta = 0; + int32_t data; + midi_event *prev; + int i; + + data = read1(ctx); + + /*HACK!*/ + if (((status >> 4) == 0xB) && (status & 0xF) != 9 && (data == 114)) { + data = 32; /*Change XMI 114 controller into XG bank*/ + } + + /* Bank changes are handled here */ + if ((status >> 4) == 0xB && data == 0) { + data = read1(ctx); + + ctx->bank127[status & 0xF] = 0; + + if ( ctx->convert_type == XMIDI_CONVERT_MT32_TO_GM || + ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS || + ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS127 || + (ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS127DRUM + && (status & 0xF) == 9) ) + return (2); + + CreateNewEvent(ctx, time); + ctx->current->status = status; + ctx->current->data[0] = 0; + ctx->current->data[1] = data == 127 ? 0 : data;/*HACK:*/ + + if (ctx->convert_type == XMIDI_CONVERT_GS127_TO_GS && data == 127) + ctx->bank127[status & 0xF] = 1; + + return (2); + } + + /* Handling for patch change mt32 conversion, probably should go elsewhere */ + if ((status >> 4) == 0xC && (status&0xF) != 9 + && ctx->convert_type != XMIDI_CONVERT_NOCONVERSION) + { + if (ctx->convert_type == XMIDI_CONVERT_MT32_TO_GM) + { + data = mt32asgm[data]; + } + else if ((ctx->convert_type == XMIDI_CONVERT_GS127_TO_GS && ctx->bank127[status&0xF]) || + ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS || + ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS127DRUM) + { + CreateNewEvent (ctx, time); + ctx->current->status = 0xB0 | (status&0xF); + ctx->current->data[0] = 0; + ctx->current->data[1] = mt32asgs[data*2+1]; + + data = mt32asgs[data*2]; + } + else if (ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS127) + { + CreateNewEvent (ctx, time); + ctx->current->status = 0xB0 | (status&0xF); + ctx->current->data[0] = 0; + ctx->current->data[1] = 127; + } + } + /* Drum track handling */ + else if ((status >> 4) == 0xC && (status&0xF) == 9 && + (ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS127DRUM || ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS127)) + { + CreateNewEvent (ctx, time); + ctx->current->status = 0xB9; + ctx->current->data[0] = 0; + ctx->current->data[1] = 127; + } + + CreateNewEvent(ctx, time); + ctx->current->status = status; + + ctx->current->data[0] = data; + + if (size == 1) + return (1); + + ctx->current->data[1] = read1(ctx); + + if (size == 2) + return (2); + + /* XMI Note On handling */ + prev = ctx->current; + i = GetVLQ(ctx, &delta); + CreateNewEvent(ctx, time + delta * 3); + + ctx->current->status = status; + ctx->current->data[0] = data; + ctx->current->data[1] = 0; + ctx->current = prev; + + return (i + 2); +} + +/* Simple routine to convert system messages */ +static int32_t ConvertSystemMessage(struct xmi_ctx *ctx, const int32_t time, + const uint8_t status) { + int32_t i = 0; + + CreateNewEvent(ctx, time); + ctx->current->status = status; + + /* Handling of Meta events */ + if (status == 0xFF) { + ctx->current->data[0] = read1(ctx); + i++; + } + + i += GetVLQ(ctx, &ctx->current->len); + + if (!ctx->current->len) + return (i); + + ctx->current->buffer = malloc(sizeof(uint8_t)*ctx->current->len); + copy(ctx, (char *) ctx->current->buffer, ctx->current->len); + + return (i + ctx->current->len); +} + +/* XMIDI and Midi to List + * Returns XMIDI PPQN */ +static int32_t ConvertFiletoList(struct xmi_ctx *ctx) { + int32_t time = 0; + uint32_t data; + int32_t end = 0; + int32_t tempo = 500000; + int32_t tempo_set = 0; + uint32_t status = 0; + uint32_t file_size = getsrcsize(ctx); + + /* Set Drum track to correct setting if required */ + if (ctx->convert_type == XMIDI_CONVERT_MT32_TO_GS127) { + CreateNewEvent(ctx, 0); + ctx->current->status = 0xB9; + ctx->current->data[0] = 0; + ctx->current->data[1] = 127; + } + + while (!end && getsrcpos(ctx) < file_size) { + GetVLQ2(ctx, &data); + time += data * 3; + + status = read1(ctx); + + switch (status >> 4) { + case MIDI_STATUS_NOTE_ON: + ConvertEvent(ctx, time, status, 3); + break; + + /* 2 byte data */ + case MIDI_STATUS_NOTE_OFF: + case MIDI_STATUS_AFTERTOUCH: + case MIDI_STATUS_CONTROLLER: + case MIDI_STATUS_PITCH_WHEEL: + ConvertEvent(ctx, time, status, 2); + break; + + /* 1 byte data */ + case MIDI_STATUS_PROG_CHANGE: + case MIDI_STATUS_PRESSURE: + ConvertEvent(ctx, time, status, 1); + break; + + case MIDI_STATUS_SYSEX: + if (status == 0xFF) { + int32_t pos = getsrcpos(ctx); + uint32_t dat = read1(ctx); + + if (dat == 0x2F) /* End */ + end = 1; + else if (dat == 0x51 && !tempo_set) /* Tempo. Need it for PPQN */ + { + skipsrc(ctx, 1); + tempo = read1(ctx) << 16; + tempo += read1(ctx) << 8; + tempo += read1(ctx); + tempo *= 3; + tempo_set = 1; + } else if (dat == 0x51 && tempo_set) /* Skip any other tempo changes */ + { + GetVLQ(ctx, &dat); + skipsrc(ctx, dat); + break; + } + + seeksrc(ctx, pos); + } + ConvertSystemMessage(ctx, time, status); + break; + + default: + break; + } + } + return ((tempo * 3) / 25000); +} + +/* Converts and event list to a MTrk + * Returns bytes of the array + * buf can be NULL */ +static uint32_t ConvertListToMTrk(struct xmi_ctx *ctx, midi_event *mlist) { + int32_t time = 0; + midi_event *event; + uint32_t delta; + uint8_t last_status = 0; + uint32_t i = 8; + uint32_t j; + uint32_t size_pos, cur_pos; + int end = 0; + + write1(ctx, 'M'); + write1(ctx, 'T'); + write1(ctx, 'r'); + write1(ctx, 'k'); + + size_pos = getdstpos(ctx); + skipdst(ctx, 4); + + for (event = mlist; event && !end; event = event->next) { + delta = (event->time - time); + time = event->time; + + i += PutVLQ(ctx, delta); + + if ((event->status != last_status) || (event->status >= 0xF0)) { + write1(ctx, event->status); + i++; + } + + last_status = event->status; + + switch (event->status >> 4) { + /* 2 bytes data + * Note off, Note on, Aftertouch, Controller and Pitch Wheel */ + case 0x8: + case 0x9: + case 0xA: + case 0xB: + case 0xE: + write1(ctx, event->data[0]); + write1(ctx, event->data[1]); + i += 2; + break; + + /* 1 bytes data + * Program Change and Channel Pressure */ + case 0xC: + case 0xD: + write1(ctx, event->data[0]); + i++; + break; + + /* Variable length + * SysEx */ + case 0xF: + if (event->status == 0xFF) { + if (event->data[0] == 0x2f) + end = 1; + write1(ctx, event->data[0]); + i++; + } + i += PutVLQ(ctx, event->len); + if (event->len) { + for (j = 0; j < event->len; j++) { + write1(ctx, event->buffer[j]); + i++; + } + } + break; + + /* Never occur */ + default: + /*_WM_DEBUG_MSG("%s: unrecognized event", __FUNCTION__);*/ + break; + } + } + + cur_pos = getdstpos(ctx); + seekdst(ctx, size_pos); + write4(ctx, i - 8); + seekdst(ctx, cur_pos); + + return (i); +} + +/* Assumes correct xmidi */ +static uint32_t ExtractTracksFromXmi(struct xmi_ctx *ctx) { + uint32_t num = 0; + signed short ppqn; + uint32_t len = 0; + int32_t begin; + char buf[32]; + + while (getsrcpos(ctx) < getsrcsize(ctx) && num != ctx->info.tracks) { + /* Read first 4 bytes of name */ + copy(ctx, buf, 4); + len = read4(ctx); + + /* Skip the FORM entries */ + if (!memcmp(buf, "FORM", 4)) { + skipsrc(ctx, 4); + copy(ctx, buf, 4); + len = read4(ctx); + } + + if (memcmp(buf, "EVNT", 4)) { + skipsrc(ctx, (len + 1) & ~1); + continue; + } + + ctx->list = NULL; + begin = getsrcpos(ctx); + + /* Convert it */ + if (!(ppqn = ConvertFiletoList(ctx))) { + /*_WM_GLOBAL_ERROR(__FUNCTION__, __LINE__, WM_ERR_CORUPT, NULL, 0);*/ + break; + } + ctx->timing[num] = ppqn; + ctx->events[num] = ctx->list; + + /* Increment Counter */ + num++; + + /* go to start of next track */ + seeksrc(ctx, begin + ((len + 1) & ~1)); + } + + /* Return how many were converted */ + return (num); +} + +static int ParseXMI(struct xmi_ctx *ctx) { + uint32_t i; + uint32_t start; + uint32_t len; + uint32_t chunk_len; + uint32_t file_size; + char buf[32]; + + file_size = getsrcsize(ctx); + if (getsrcpos(ctx) + 8 > file_size) { +badfile: /*_WM_GLOBAL_ERROR(__FUNCTION__, __LINE__, WM_ERR_CORUPT, "(too short)", 0);*/ + return (-1); + } + + /* Read first 4 bytes of header */ + copy(ctx, buf, 4); + + /* Could be XMIDI */ + if (!memcmp(buf, "FORM", 4)) { + /* Read length of */ + len = read4(ctx); + + start = getsrcpos(ctx); + if (start + 4 > file_size) + goto badfile; + + /* Read 4 bytes of type */ + copy(ctx, buf, 4); + + /* XDIRless XMIDI, we can handle them here. */ + if (!memcmp(buf, "XMID", 4)) { + /*_WM_DEBUG_MSG("Warning: XMIDI without XDIR");*/ + ctx->info.tracks = 1; + } + /* Not an XMIDI that we recognise */ + else if (memcmp(buf, "XDIR", 4)) { + goto badfile; + } + else { /* Seems Valid */ + ctx->info.tracks = 0; + + for (i = 4; i < len; i++) { + /* check too short files */ + if (getsrcpos(ctx) + 10 > file_size) + break; + + /* Read 4 bytes of type */ + copy(ctx, buf, 4); + + /* Read length of chunk */ + chunk_len = read4(ctx); + + /* Add eight bytes */ + i += 8; + + if (memcmp(buf, "INFO", 4)) { + /* Must align */ + skipsrc(ctx, (chunk_len + 1) & ~1); + i += (chunk_len + 1) & ~1; + continue; + } + + /* Must be at least 2 bytes long */ + if (chunk_len < 2) + break; + + ctx->info.tracks = read2(ctx); + break; + } + + /* Didn't get to fill the header */ + if (ctx->info.tracks == 0) { + goto badfile; + } + + /* Ok now to start part 2 + * Goto the right place */ + seeksrc(ctx, start + ((len + 1) & ~1)); + if (getsrcpos(ctx) + 12 > file_size) + goto badfile; + + /* Read 4 bytes of type */ + copy(ctx, buf, 4); + + if (memcmp(buf, "CAT ", 4)) { + /*_WM_ERROR_NEW("XMI error: expected \"CAT \", found \"%c%c%c%c\".", + buf[0], buf[1], buf[2], buf[3]);*/ + return (-1); + } + + /* Now read length of this track */ + read4(ctx); + + /* Read 4 bytes of type */ + copy(ctx, buf, 4); + + if (memcmp(buf, "XMID", 4)) { + /*_WM_ERROR_NEW("XMI error: expected \"XMID\", found \"%c%c%c%c\".", + buf[0], buf[1], buf[2], buf[3]);*/ + return (-1); + } + + /* Valid XMID */ + ctx->datastart = getsrcpos(ctx); + return (0); + } + } + + return (-1); +} + +static int ExtractTracks(struct xmi_ctx *ctx) { + uint32_t i; + + ctx->events = calloc(ctx->info.tracks, sizeof(midi_event*)); + ctx->timing = calloc(ctx->info.tracks, sizeof(int16_t)); + /* type-2 for multi-tracks, type-0 otherwise */ + ctx->info.type = (ctx->info.tracks > 1)? 2 : 0; + + seeksrc(ctx, ctx->datastart); + i = ExtractTracksFromXmi(ctx); + + if (i != ctx->info.tracks) { + /*_WM_ERROR_NEW("XMI error: extracted only %u out of %u tracks from XMIDI", + ctx->info.tracks, i);*/ + return (-1); + } + + return (0); +} + diff --git a/src/sound/opnmidi/opnmidi_xmi2mid.h b/src/sound/opnmidi/opnmidi_xmi2mid.h new file mode 100644 index 000000000..d137c7396 --- /dev/null +++ b/src/sound/opnmidi/opnmidi_xmi2mid.h @@ -0,0 +1,60 @@ +/* + * XMIDI: Miles XMIDI to MID Library Header + * + * Copyright (C) 2001 Ryan Nunn + * Copyright (C) 2014-2016 Bret Curtis + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/* XMIDI Converter */ + +#ifndef XMIDILIB_H +#define XMIDILIB_H + +#include + +#ifdef __DJGPP__ +typedef signed char int8_t; +typedef unsigned char uint8_t; +typedef signed short int16_t; +typedef unsigned short uint16_t; +typedef signed long int32_t; +typedef unsigned long uint32_t; +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Conversion types for Midi files */ +#define XMIDI_CONVERT_NOCONVERSION 0x00 +#define XMIDI_CONVERT_MT32_TO_GM 0x01 +#define XMIDI_CONVERT_MT32_TO_GS 0x02 +#define XMIDI_CONVERT_MT32_TO_GS127 0x03 /* This one is broken, don't use */ +#define XMIDI_CONVERT_MT32_TO_GS127DRUM 0x04 /* This one is broken, don't use */ +#define XMIDI_CONVERT_GS127_TO_GS 0x05 + +int OpnMidi_xmi2midi(uint8_t *in, uint32_t insize, + uint8_t **out, uint32_t *outsize, + uint32_t convert_type); + +#ifdef __cplusplus +} +#endif + +#endif /* XMIDILIB_H */