mirror of
https://github.com/ZDoom/gzdoom-gles.git
synced 2024-11-17 10:00:54 +00:00
2673 lines
87 KiB
C++
2673 lines
87 KiB
C++
|
/*
|
||
|
* libADLMIDI is a free MIDI to WAV conversion library with OPL3 emulation
|
||
|
*
|
||
|
* Original ADLMIDI code: Copyright (c) 2010-2014 Joel Yliluoma <bisqwit@iki.fi>
|
||
|
* ADLMIDI Library API: Copyright (c) 2015-2018 Vitaly Novichkov <admin@wohlnet.ru>
|
||
|
*
|
||
|
* 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 <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
#include "adlmidi_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
|
||
|
};
|
||
|
|
||
|
|
||
|
//static const char MIDIsymbols[256+1] =
|
||
|
//"PPPPPPhcckmvmxbd" // Ins 0-15
|
||
|
//"oooooahoGGGGGGGG" // Ins 16-31
|
||
|
//"BBBBBBBBVVVVVHHM" // Ins 32-47
|
||
|
//"SSSSOOOcTTTTTTTT" // Ins 48-63
|
||
|
//"XXXXTTTFFFFFFFFF" // Ins 64-79
|
||
|
//"LLLLLLLLpppppppp" // Ins 80-95
|
||
|
//"XXXXXXXXGGGGGTSS" // Ins 96-111
|
||
|
//"bbbbMMMcGXXXXXXX" // Ins 112-127
|
||
|
//"????????????????" // Prc 0-15
|
||
|
//"????????????????" // Prc 16-31
|
||
|
//"???DDshMhhhCCCbM" // Prc 32-47
|
||
|
//"CBDMMDDDMMDDDDDD" // Prc 48-63
|
||
|
//"DDDDDDDDDDDDDDDD" // Prc 64-79
|
||
|
//"DD??????????????" // Prc 80-95
|
||
|
//"????????????????" // Prc 96-111
|
||
|
//"????????????????"; // Prc 112-127
|
||
|
|
||
|
static const uint8_t PercussionMap[256] =
|
||
|
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"//GM
|
||
|
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" // 3 = bass drum
|
||
|
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" // 4 = snare
|
||
|
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" // 5 = tom
|
||
|
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" // 6 = cymbal
|
||
|
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" // 7 = hihat
|
||
|
"\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\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"//GP0
|
||
|
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"//GP16
|
||
|
//2 3 4 5 6 7 8 940 1 2 3 4 5 6 7
|
||
|
"\0\0\0\3\3\0\0\7\0\5\7\5\0\5\7\5"//GP32
|
||
|
//8 950 1 2 3 4 5 6 7 8 960 1 2 3
|
||
|
"\5\6\5\0\6\0\5\6\0\6\0\6\5\5\5\5"//GP48
|
||
|
//4 5 6 7 8 970 1 2 3 4 5 6 7 8 9
|
||
|
"\5\0\0\0\0\0\7\0\0\0\0\0\0\0\0\0"//GP64
|
||
|
"\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\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||
|
|
||
|
inline bool isXgPercChannel(uint8_t msb, uint8_t lsb)
|
||
|
{
|
||
|
return (msb == 0x7E || msb == 0x7F) && (lsb == 0);
|
||
|
}
|
||
|
|
||
|
void MIDIplay::AdlChannel::AddAge(int64_t ms)
|
||
|
{
|
||
|
if(users.empty())
|
||
|
koff_time_until_neglible =
|
||
|
std::max(int64_t(koff_time_until_neglible - ms), static_cast<int64_t>(-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<int64_t>(-0x1FFFFFFFl));
|
||
|
i->second.vibdelay += ms;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
MIDIplay::MidiEvent::MidiEvent() :
|
||
|
type(T_UNKNOWN),
|
||
|
subtype(T_UNKNOWN),
|
||
|
channel(0),
|
||
|
isValid(1),
|
||
|
absPosition(0)
|
||
|
{}
|
||
|
|
||
|
|
||
|
MIDIplay::MidiTrackRow::MidiTrackRow() :
|
||
|
time(0.0),
|
||
|
delay(0),
|
||
|
absPos(0),
|
||
|
timeDelay(0.0)
|
||
|
{}
|
||
|
|
||
|
void MIDIplay::MidiTrackRow::reset()
|
||
|
{
|
||
|
time = 0.0;
|
||
|
delay = 0;
|
||
|
absPos = 0;
|
||
|
timeDelay = 0.0;
|
||
|
events.clear();
|
||
|
}
|
||
|
|
||
|
void MIDIplay::MidiTrackRow::sortEvents(bool *noteStates)
|
||
|
{
|
||
|
typedef std::vector<MidiEvent> 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<size_t> 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<size_t>::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 MIDIplay::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<MidiEvent> 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(opl.m_musicMode == OPL3::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<uint64_t> 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<TempoChangePoint> points;
|
||
|
fraction<uint64_t> 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<uint64_t>(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<uint64_t> 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(opl.m_musicMode == OPL3::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;
|
||
|
}
|
||
|
|
||
|
MIDIplay::MIDIplay(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.AdlBank = 0;
|
||
|
m_setup.NumFourOps = 7;
|
||
|
m_setup.NumCards = 2;
|
||
|
m_setup.HighTremoloMode = -1;
|
||
|
m_setup.HighVibratoMode = -1;
|
||
|
m_setup.AdlPercussionMode = -1;
|
||
|
m_setup.LogarithmicVolumes = false;
|
||
|
m_setup.VolumeModel = ADLMIDI_VolumeModel_AUTO;
|
||
|
//m_setup.SkipForward = 0;
|
||
|
m_setup.loopingIsEnabled = false;
|
||
|
m_setup.ScaleModulators = -1;
|
||
|
m_setup.delay = 0.0;
|
||
|
m_setup.carry = 0.0;
|
||
|
m_setup.tick_skip_samples_delay = 0;
|
||
|
|
||
|
applySetup();
|
||
|
ChooseDevice("none");
|
||
|
realTime_ResetState();
|
||
|
}
|
||
|
|
||
|
void MIDIplay::applySetup()
|
||
|
{
|
||
|
m_setup.tick_skip_samples_delay = 0;
|
||
|
opl.HighTremoloMode = m_setup.HighTremoloMode == -1 ? adlbanksetup[m_setup.AdlBank].deepTremolo : (bool)m_setup.HighTremoloMode;
|
||
|
opl.HighVibratoMode = m_setup.HighVibratoMode == -1 ? adlbanksetup[m_setup.AdlBank].deepVibrato : (bool)m_setup.HighVibratoMode;
|
||
|
opl.AdlPercussionMode = m_setup.AdlPercussionMode == -1 ? adlbanksetup[m_setup.AdlBank].adLibPercussions : (bool)m_setup.AdlPercussionMode;
|
||
|
opl.ScaleModulators = m_setup.ScaleModulators == -1 ? adlbanksetup[m_setup.AdlBank].scaleModulators : (bool)m_setup.ScaleModulators;
|
||
|
opl.LogarithmicVolumes = m_setup.LogarithmicVolumes;
|
||
|
//opl.CartoonersVolumes = false;
|
||
|
opl.m_musicMode = OPL3::MODE_MIDI;
|
||
|
opl.ChangeVolumeRangesModel(static_cast<ADLMIDI_VolumeModels>(m_setup.VolumeModel));
|
||
|
if(m_setup.VolumeModel == ADLMIDI_VolumeModel_AUTO)//Use bank default volume model
|
||
|
opl.m_volumeScale = (OPL3::VolumesScale)adlbanksetup[m_setup.AdlBank].volumeModel;
|
||
|
|
||
|
opl.NumCards = m_setup.NumCards;
|
||
|
opl.NumFourOps = m_setup.NumFourOps;
|
||
|
cmf_percussion_mode = false;
|
||
|
|
||
|
opl.Reset(m_setup.PCM_RATE);
|
||
|
ch.clear();
|
||
|
ch.resize(opl.NumChannels);
|
||
|
}
|
||
|
|
||
|
uint64_t MIDIplay::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 MIDIplay::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 MIDIplay::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 < opl.NumChannels; ++c)
|
||
|
ch[c].AddAge(static_cast<int64_t>(s * 1000.0));
|
||
|
|
||
|
UpdateVibrato(s);
|
||
|
UpdateArpeggio(s);
|
||
|
|
||
|
if(CurrentPositionNew.wait < 0.0)//Avoid negative delay value!
|
||
|
return 0.0;
|
||
|
|
||
|
return CurrentPositionNew.wait;
|
||
|
}
|
||
|
|
||
|
void MIDIplay::TickIteratos(double s)
|
||
|
{
|
||
|
for(uint16_t c = 0; c < opl.NumChannels; ++c)
|
||
|
ch[c].AddAge(static_cast<int64_t>(s * 1000.0));
|
||
|
UpdateVibrato(s);
|
||
|
UpdateArpeggio(s);
|
||
|
}
|
||
|
|
||
|
void MIDIplay::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 MIDIplay::tell()
|
||
|
{
|
||
|
return CurrentPositionNew.absTimePosition;
|
||
|
}
|
||
|
|
||
|
double MIDIplay::timeLength()
|
||
|
{
|
||
|
return fullSongTimeLength;
|
||
|
}
|
||
|
|
||
|
double MIDIplay::getLoopStart()
|
||
|
{
|
||
|
return loopStartTime;
|
||
|
}
|
||
|
|
||
|
double MIDIplay::getLoopEnd()
|
||
|
{
|
||
|
return loopEndTime;
|
||
|
}
|
||
|
|
||
|
void MIDIplay::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 MIDIplay::setTempo(double tempo)
|
||
|
{
|
||
|
tempoMultiplier = tempo;
|
||
|
}
|
||
|
|
||
|
void MIDIplay::realTime_ResetState()
|
||
|
{
|
||
|
for(size_t ch = 0; ch < Ch.size(); ch++)
|
||
|
{
|
||
|
MIDIchannel &chan = Ch[ch];
|
||
|
chan.volume = (opl.m_musicMode == OPL3::MODE_RSXX) ? 127 : 100;
|
||
|
chan.expression = 127;
|
||
|
chan.panning = 0x30;
|
||
|
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 MIDIplay::realTime_NoteOn(uint8_t channel, uint8_t note, uint8_t velocity)
|
||
|
{
|
||
|
if((opl.m_musicMode == OPL3::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 = opl.dynamic_percussion_offset + note; // Percussion instrument
|
||
|
isXgPercussion = true;
|
||
|
isPercussion = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(isPercussion)
|
||
|
{
|
||
|
bank = (uint16_t)midiins; // MIDI instrument defines the patch
|
||
|
midiins = opl.dynamic_percussion_offset + note; // Percussion instrument
|
||
|
}
|
||
|
|
||
|
//Set bank bank
|
||
|
if(bank > 0)
|
||
|
{
|
||
|
if(isPercussion || isXgPercussion)
|
||
|
{
|
||
|
OPL3::BankMap::iterator b = opl.dynamic_percussion_banks.find(bank);
|
||
|
if(b != opl.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
|
||
|
{
|
||
|
OPL3::BankMap::iterator b = opl.dynamic_melodic_banks.find(bank);
|
||
|
if(b != opl.dynamic_melodic_banks.end())
|
||
|
midiins += b->second * 128;
|
||
|
else
|
||
|
if(hooks.onDebugMessage)
|
||
|
{
|
||
|
if(!caugh_missing_banks_percussion.count(bank))
|
||
|
{
|
||
|
hooks.onDebugMessage(hooks.onDebugMessage_userData, "[%i] Playing missing melodic bank %i (patch %i)", channel, bank, midiins);
|
||
|
caugh_missing_banks_percussion.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
|
||
|
|
||
|
//int meta = banks[opl.AdlBank][midiins];
|
||
|
const size_t meta = opl.GetAdlMetaNumber(midiins);
|
||
|
const adlinsdata &ains = opl.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.adlno1, ains.adlno2 };
|
||
|
bool pseudo_4op = ains.flags & adlinsdata::Flag_Pseudo4op;
|
||
|
MIDIchannel::NoteInfo::Phys voices[2] =
|
||
|
{
|
||
|
{ains.adlno1, false},
|
||
|
{ains.adlno2, pseudo_4op}
|
||
|
};
|
||
|
|
||
|
if((opl.AdlPercussionMode == 1) && PercussionMap[midiins & 0xFF])
|
||
|
voices[1] = voices[0];//i[1] = i[0];
|
||
|
|
||
|
if(hooks.onDebugMessage)
|
||
|
{
|
||
|
if(!caugh_missing_instruments.count(static_cast<uint8_t>(midiins)) && (ains.flags & adlinsdata::Flag_NoSound))
|
||
|
{
|
||
|
hooks.onDebugMessage(hooks.onDebugMessage_userData, "[%i] Playing missing instrument %i", channel, midiins);
|
||
|
caugh_missing_instruments.insert(static_cast<uint8_t>(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(voices[0] == voices[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)opl.NumChannels; ++a)
|
||
|
{
|
||
|
if(ccount == 1 && static_cast<int32_t>(a) == adlchannel[0]) continue;
|
||
|
// ^ Don't use the same channel for primary&secondary
|
||
|
|
||
|
if(voices[0].insId == voices[1].insId || pseudo_4op/*i[0] == i[1] || pseudo_4op*/)
|
||
|
{
|
||
|
// Only use regular channels
|
||
|
uint8_t expected_mode = 0;
|
||
|
|
||
|
if(opl.AdlPercussionMode == 1)
|
||
|
{
|
||
|
if(cmf_percussion_mode)
|
||
|
expected_mode = channel < 11 ? 0 : (3 + channel - 11); // CMF
|
||
|
else
|
||
|
expected_mode = PercussionMap[midiins & 0xFF];
|
||
|
}
|
||
|
|
||
|
if(opl.four_op_category[a] != expected_mode)
|
||
|
continue;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if(ccount == 0)
|
||
|
{
|
||
|
// Only use four-op master channels
|
||
|
if(opl.four_op_category[a] != 1)
|
||
|
continue;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// The secondary must be played on a specific channel.
|
||
|
if(a != static_cast<uint32_t>(adlchannel[0]) + 3)
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
int64_t s = CalculateAdlChannelGoodness(a, voices[ccount], channel);
|
||
|
if(s > bs)
|
||
|
{
|
||
|
bs = (int32_t)s; // Best candidate wins
|
||
|
c = static_cast<int32_t>(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<size_t>(c), voices[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<MIDIchannel::activenoteiterator, bool>
|
||
|
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<uint16_t>(adlchannel[ccount]);
|
||
|
ir.first->second.phys[chipChan] = voices[ccount];
|
||
|
}
|
||
|
NoteUpdate(channel, ir.first, Upd_All | Upd_Patch);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void MIDIplay::realTime_NoteOff(uint8_t channel, uint8_t note)
|
||
|
{
|
||
|
channel = channel % 16;
|
||
|
NoteOff(channel, note);
|
||
|
}
|
||
|
|
||
|
void MIDIplay::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 MIDIplay::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 MIDIplay::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<uint16_t>((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 |= 0x10;
|
||
|
if(value >= 64 - 32) Ch[channel].panning |= 0x20;
|
||
|
|
||
|
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 = 0x30;
|
||
|
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 MIDIplay::realTime_PatchChange(uint8_t channel, uint8_t patch)
|
||
|
{
|
||
|
channel = channel % 16;
|
||
|
Ch[channel].patch = patch;
|
||
|
}
|
||
|
|
||
|
void MIDIplay::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 MIDIplay::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 MIDIplay::realTime_BankChangeLSB(uint8_t channel, uint8_t lsb)
|
||
|
{
|
||
|
channel = channel % 16;
|
||
|
Ch[channel].bank_lsb = lsb;
|
||
|
}
|
||
|
|
||
|
void MIDIplay::realTime_BankChangeMSB(uint8_t channel, uint8_t msb)
|
||
|
{
|
||
|
channel = channel % 16;
|
||
|
Ch[channel].bank_msb = msb;
|
||
|
}
|
||
|
|
||
|
void MIDIplay::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 MIDIplay::realTime_panic()
|
||
|
{
|
||
|
Panic();
|
||
|
KillSustainingNotes(-1, -1);
|
||
|
}
|
||
|
|
||
|
|
||
|
void MIDIplay::NoteUpdate(uint16_t MidCh,
|
||
|
MIDIplay::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 int midiins = info.midiins;
|
||
|
const size_t insmeta = info.insmeta;
|
||
|
const adlinsdata &ains = opl.GetAdlMetaIns(insmeta);
|
||
|
AdlChannel::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)
|
||
|
{
|
||
|
opl.Patch(c, ins.insId);
|
||
|
AdlChannel::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)
|
||
|
{
|
||
|
AdlChannel::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, midiins, 0, 0.0);
|
||
|
|
||
|
if(ch[c].users.empty())
|
||
|
{
|
||
|
opl.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.
|
||
|
AdlChannel::LocationData &d = ch[c].users[my_loc];
|
||
|
d.sustained = true; // note: not erased!
|
||
|
if(hooks.onNote)
|
||
|
hooks.onNote(hooks.onNote_userData, c, tone, midiins, -1, 0.0);
|
||
|
}
|
||
|
|
||
|
info.phys.erase(j);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if(props_mask & Upd_Pan)
|
||
|
opl.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(opl.m_volumeScale)
|
||
|
{
|
||
|
case OPL3::VOLUME_Generic:
|
||
|
case OPL3::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(opl.LogarithmicVolumes)
|
||
|
volume = volume * 127 / (127 * 127 * 127) / 2;
|
||
|
else
|
||
|
{
|
||
|
// The formula below: SOLVE(V=127^3 * 2^( (A-63.49999) / 8), A)
|
||
|
volume = volume > 8725 ? static_cast<uint32_t>(std::log(static_cast<double>(volume)) * 11.541561 + (0.5 - 104.22845)) : 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);
|
||
|
}
|
||
|
opl.Touch_Real(c, volume, brightness);
|
||
|
//opl.Touch(c, volume);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case OPL3::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;
|
||
|
opl.Touch_Real(c, volume, brightness);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case OPL3::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;
|
||
|
opl.Touch_Real(c, volume, brightness);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case OPL3::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;
|
||
|
opl.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)
|
||
|
{
|
||
|
AdlChannel::LocationData &d = ch[c].users[my_loc];
|
||
|
|
||
|
// Don't bend a sustained note
|
||
|
if(!d.sustained)
|
||
|
{
|
||
|
double bend = Ch[MidCh].bend + opl.GetAdlIns(ins.insId).finetune;
|
||
|
double phase = 0.0;
|
||
|
|
||
|
if((ains.flags & adlinsdata::Flag_Pseudo4op) && ins.pseudo4op)
|
||
|
{
|
||
|
phase = ains.voice2_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);
|
||
|
|
||
|
#ifdef ADLMIDI_USE_DOSBOX_OPL
|
||
|
#define BEND_COEFFICIENT 172.00093
|
||
|
#else
|
||
|
#define BEND_COEFFICIENT 172.4387
|
||
|
#endif
|
||
|
opl.NoteOn(c, BEND_COEFFICIENT * std::exp(0.057762265 * (tone + bend + phase)));
|
||
|
#undef BEND_COEFFICIENT
|
||
|
if(hooks.onNote)
|
||
|
hooks.onNote(hooks.onNote_userData, c, tone, midiins, vol, Ch[MidCh].bend);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(info.phys.empty())
|
||
|
Ch[MidCh].activenotes.erase(i);
|
||
|
}
|
||
|
|
||
|
|
||
|
bool MIDIplay::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<uint64_t> 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
|
||
|
}
|
||
|
|
||
|
MIDIplay::MidiEvent MIDIplay::parseEvent(uint8_t **pptr, uint8_t *end, int &status)
|
||
|
{
|
||
|
uint8_t *&ptr = *pptr;
|
||
|
MIDIplay::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<uint8_t>(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 &MIDIplay::getErrorString()
|
||
|
{
|
||
|
return errorStringOut;
|
||
|
}
|
||
|
|
||
|
void MIDIplay::setErrorString(const std::string &err)
|
||
|
{
|
||
|
errorStringOut = err;
|
||
|
}
|
||
|
|
||
|
|
||
|
void MIDIplay::HandleEvent(size_t tk, const MIDIplay::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<uint64_t>(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<uint8_t>(data[0]), v = static_cast<uint8_t>(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<uint8_t>(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 MIDIplay::CalculateAdlChannelGoodness(unsigned c, const MIDIchannel::NoteInfo::Phys &ins, uint16_t) const
|
||
|
{
|
||
|
long s = -ch[c].koff_time_until_neglible;
|
||
|
|
||
|
// Same midi-instrument = some stability
|
||
|
//if(c == MidCh) s += 4;
|
||
|
for(AdlChannel::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 < opl.NumChannels; ++c2)
|
||
|
{
|
||
|
if(c2 == c) continue;
|
||
|
|
||
|
if(opl.four_op_category[c2]
|
||
|
!= opl.four_op_category[c]) continue;
|
||
|
|
||
|
for(AdlChannel::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 MIDIplay::PrepareAdlChannelForNewNote(size_t c, const MIDIchannel::NoteInfo::Phys &ins)
|
||
|
{
|
||
|
if(ch[c].users.empty()) return; // Nothing to do
|
||
|
|
||
|
//bool doing_arpeggio = false;
|
||
|
for(AdlChannel::users_t::iterator
|
||
|
jnext = ch[c].users.begin();
|
||
|
jnext != ch[c].users.end();
|
||
|
)
|
||
|
{
|
||
|
AdlChannel::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<int32_t>(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())
|
||
|
opl.NoteOff(c);
|
||
|
}
|
||
|
|
||
|
void MIDIplay::KillOrEvacuate(size_t from_channel, AdlChannel::users_t::iterator j, MIDIplay::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 < opl.NumChannels; ++c)
|
||
|
{
|
||
|
uint16_t cs = static_cast<uint16_t>(c);
|
||
|
|
||
|
if(c > std::numeric_limits<uint32_t>::max())
|
||
|
break;
|
||
|
if(c == from_channel)
|
||
|
continue;
|
||
|
if(opl.four_op_category[c] != opl.four_op_category[from_channel])
|
||
|
continue;
|
||
|
|
||
|
for(AdlChannel::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,
|
||
|
i->second.midiins, 0, 0.0);
|
||
|
hooks.onNote(hooks.onNote_userData,
|
||
|
(int)c,
|
||
|
i->second.tone,
|
||
|
i->second.midiins,
|
||
|
i->second.vol, 0.0);
|
||
|
}
|
||
|
|
||
|
i->second.phys.erase(static_cast<uint16_t>(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<int32_t>(from_channel));
|
||
|
}
|
||
|
|
||
|
void MIDIplay::Panic()
|
||
|
{
|
||
|
for(uint8_t chan = 0; chan < Ch.size(); chan++)
|
||
|
{
|
||
|
for(uint8_t note = 0; note < 128; note++)
|
||
|
realTime_NoteOff(chan, note);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void MIDIplay::KillSustainingNotes(int32_t MidCh, int32_t this_adlchn)
|
||
|
{
|
||
|
uint32_t first = 0, last = opl.NumChannels;
|
||
|
|
||
|
if(this_adlchn >= 0)
|
||
|
{
|
||
|
first = static_cast<uint32_t>(this_adlchn);
|
||
|
last = first + 1;
|
||
|
}
|
||
|
|
||
|
for(unsigned c = first; c < last; ++c)
|
||
|
{
|
||
|
if(ch[c].users.empty()) continue; // Nothing to do
|
||
|
|
||
|
for(AdlChannel::users_t::iterator
|
||
|
jnext = ch[c].users.begin();
|
||
|
jnext != ch[c].users.end();
|
||
|
)
|
||
|
{
|
||
|
AdlChannel::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())
|
||
|
opl.NoteOff(c);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void MIDIplay::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 MIDIplay::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 MIDIplay::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 MIDIplay::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<uint16_t>(a), Upd_Pitch);
|
||
|
Ch[a].vibpos += amount * Ch[a].vibspeed;
|
||
|
}
|
||
|
else
|
||
|
Ch[a].vibpos = 0.0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
uint64_t MIDIplay::ChooseDevice(const std::string &name)
|
||
|
{
|
||
|
std::map<std::string, uint64_t>::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 MIDIplay::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 < opl.NumChannels; ++c)
|
||
|
{
|
||
|
retry_arpeggio:
|
||
|
if(c > uint32_t(std::numeric_limits<int32_t>::max()))
|
||
|
break;
|
||
|
|
||
|
size_t n_users = ch[c].users.size();
|
||
|
|
||
|
if(n_users > 1)
|
||
|
{
|
||
|
AdlChannel::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<int32_t>(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<int32_t>(c));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
#ifndef ADLMIDI_DISABLE_CPP_EXTRAS
|
||
|
|
||
|
ADLMIDI_EXPORT AdlInstrumentTester::AdlInstrumentTester(ADL_MIDIPlayer *device)
|
||
|
{
|
||
|
cur_gm = 0;
|
||
|
ins_idx = 0;
|
||
|
play = reinterpret_cast<MIDIplay *>(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<unsigned> 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<unsigned int>(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
|