mirror of
https://github.com/scummvm/scummvm.git
synced 2026-05-21 05:40:43 +00:00
9749f59365
The midi parser interface should take a const byte array so that engines can pass data with more confidence. This also allows them to be more const correct on their side. There was one place where the input byte stream was actually being modified by the midi parser to overwrite tempo values. Pointed to a static byte array for this case.
517 lines
13 KiB
C++
517 lines
13 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* 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
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "audio/midiparser_qt.h"
|
|
#include "audio/mididrv.h"
|
|
#include "common/debug.h"
|
|
#include "common/memstream.h"
|
|
|
|
bool MidiParser_QT::loadMusic(const byte *data, uint32 size) {
|
|
if (size < 8)
|
|
return false;
|
|
|
|
Common::SeekableReadStream *stream = new Common::MemoryReadStream(data, size, DisposeAfterUse::NO);
|
|
|
|
// Attempt to detect what format we have
|
|
bool result;
|
|
if (READ_BE_UINT32(data + 4) == MKTAG('m', 'u', 's', 'i'))
|
|
result = loadFromTune(stream);
|
|
else
|
|
result = loadFromContainerStream(stream);
|
|
|
|
if (!result) {
|
|
delete stream;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void MidiParser_QT::unloadMusic() {
|
|
MidiParser::unloadMusic();
|
|
close();
|
|
|
|
// Unlike those lesser formats, we *do* hold track data
|
|
for (uint i = 0; i < _trackInfo.size(); i++)
|
|
free(_trackInfo[i].data);
|
|
|
|
_trackInfo.clear();
|
|
}
|
|
|
|
bool MidiParser_QT::loadFromTune(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) {
|
|
unloadMusic();
|
|
|
|
// a tune starts off with a sample description
|
|
stream->readUint32BE(); // header size
|
|
|
|
if (stream->readUint32BE() != MKTAG('m', 'u', 's', 'i'))
|
|
return false;
|
|
|
|
stream->readUint32BE(); // reserved
|
|
stream->readUint16BE(); // reserved
|
|
stream->readUint16BE(); // index
|
|
|
|
stream->readUint32BE(); // flags, ignore
|
|
|
|
MIDITrackInfo trackInfo;
|
|
trackInfo.size = stream->size() - stream->pos();
|
|
assert(trackInfo.size > 0);
|
|
|
|
trackInfo.data = (byte *)malloc(trackInfo.size);
|
|
stream->read(trackInfo.data, trackInfo.size);
|
|
|
|
trackInfo.timeScale = 600; // the default
|
|
_trackInfo.push_back(trackInfo);
|
|
|
|
initCommon();
|
|
return true;
|
|
}
|
|
|
|
bool MidiParser_QT::loadFromContainerStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) {
|
|
unloadMusic();
|
|
|
|
if (!parseStream(stream, disposeAfterUse))
|
|
return false;
|
|
|
|
initFromContainerTracks();
|
|
return true;
|
|
}
|
|
|
|
bool MidiParser_QT::loadFromContainerFile(const Common::Path &fileName) {
|
|
unloadMusic();
|
|
|
|
if (!parseFile(fileName))
|
|
return false;
|
|
|
|
initFromContainerTracks();
|
|
return true;
|
|
}
|
|
|
|
void MidiParser_QT::parseNextEvent(EventInfo &info) {
|
|
uint32 delta = 0;
|
|
|
|
while (_queuedEvents.empty())
|
|
delta += readNextEvent();
|
|
|
|
info = _queuedEvents.pop();
|
|
info.delta = delta;
|
|
}
|
|
|
|
uint32 MidiParser_QT::readNextEvent() {
|
|
if (_position._subtracks[0]._playPos >= _trackInfo[_activeTrack].data + _trackInfo[_activeTrack].size) {
|
|
// Manually insert end of track when we reach the end
|
|
EventInfo info;
|
|
info.event = 0xFF;
|
|
info.ext.type = 0x2F;
|
|
_queuedEvents.push(info);
|
|
return 0;
|
|
}
|
|
|
|
uint32 control = readUint32();
|
|
|
|
switch (control >> 28) {
|
|
case 0x0:
|
|
case 0x1:
|
|
// Rest
|
|
// We handle this by recursively adding up all the rests into the
|
|
// next event's delta
|
|
return readNextEvent() + (control & 0xFFFFFF);
|
|
case 0x2:
|
|
case 0x3:
|
|
// Note event
|
|
handleNoteEvent((control >> 24) & 0x1F, ((control >> 18) & 0x3F) + 32, (control >> 11) & 0x7F, control & 0x7FF);
|
|
break;
|
|
case 0x4:
|
|
case 0x5:
|
|
// Controller
|
|
handleControllerEvent((control >> 16) & 0xFF, (control >> 24) & 0x1F, (control >> 8) & 0xFF, control & 0xFF);
|
|
break;
|
|
case 0x6:
|
|
case 0x7:
|
|
// Marker
|
|
// Used for editing only, so we don't need to care about this
|
|
break;
|
|
case 0x9: {
|
|
// Extended note event
|
|
uint32 extra = readUint32();
|
|
handleNoteEvent((control >> 16) & 0xFFF, (control >> 8) & 0xFF, (extra >> 22) & 0x7F, extra & 0x3FFFFF);
|
|
break;
|
|
}
|
|
case 0xA: {
|
|
// Extended controller
|
|
uint32 extra = readUint32();
|
|
handleControllerEvent((extra >> 16) & 0x3FFF, (control >> 16) & 0xFFF, (extra >> 8) & 0xFF, extra & 0xFF);
|
|
break;
|
|
}
|
|
case 0xB:
|
|
// Knob
|
|
error("Encountered knob event in QuickTime MIDI");
|
|
break;
|
|
case 0x8:
|
|
case 0xC:
|
|
case 0xD:
|
|
case 0xE:
|
|
// Reserved
|
|
readUint32();
|
|
break;
|
|
case 0xF:
|
|
// General
|
|
handleGeneralEvent(control);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void MidiParser_QT::handleNoteEvent(uint32 part, byte pitch, byte velocity, uint32 length) {
|
|
byte channel = getChannel(part);
|
|
|
|
EventInfo info;
|
|
info.event = 0x90 | channel;
|
|
info.basic.param1 = pitch;
|
|
info.basic.param2 = velocity;
|
|
info.length = (velocity == 0) ? 0 : length;
|
|
_queuedEvents.push(info);
|
|
}
|
|
|
|
void MidiParser_QT::handleControllerEvent(uint32 control, uint32 part, byte intPart, byte fracPart) {
|
|
byte channel = getChannel(part);
|
|
EventInfo info;
|
|
|
|
if (control == 0) {
|
|
// "Bank select"
|
|
// QuickTime docs don't list this, but IHNM Mac calls this anyway
|
|
// We have to ignore this.
|
|
return;
|
|
} else if (control == 32) {
|
|
// Pitch bend
|
|
info.event = 0xE0 | channel;
|
|
|
|
// Actually an 8.8 fixed point number
|
|
int16 value = (int16)((intPart << 8) | fracPart);
|
|
|
|
if (value < -0x200 || value > 0x1FF) {
|
|
warning("QuickTime MIDI pitch bend value (%d) out of range, clipping", value);
|
|
value = CLIP<int16>(value, -0x200, 0x1FF);
|
|
}
|
|
|
|
// Now convert the value to 'normal' MIDI values
|
|
value += 0x200;
|
|
value *= 16;
|
|
|
|
// param1 holds the low 7 bits, param2 holds the high 7 bits
|
|
info.basic.param1 = value & 0x7F;
|
|
info.basic.param2 = value >> 7;
|
|
|
|
_partMap[part].pitchBend = value;
|
|
} else {
|
|
// Regular controller
|
|
info.event = 0xB0 | channel;
|
|
info.basic.param1 = control;
|
|
info.basic.param2 = intPart;
|
|
|
|
// TODO: Parse more controls to hold their status
|
|
switch (control) {
|
|
case 7:
|
|
_partMap[part].volume = intPart;
|
|
break;
|
|
case 10:
|
|
_partMap[part].pan = intPart;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
_queuedEvents.push(info);
|
|
}
|
|
|
|
void MidiParser_QT::handleGeneralEvent(uint32 control) {
|
|
uint32 part = (control >> 16) & 0xFFF;
|
|
uint32 dataSize = ((control & 0xFFFF) - 2) * 4;
|
|
byte subType = READ_BE_UINT16(_position._subtracks[0]._playPos + dataSize) & 0x3FFF;
|
|
|
|
switch (subType) {
|
|
case 1:
|
|
// Note Request
|
|
// Currently we're only using the GM number from the request
|
|
assert(dataSize == 84);
|
|
|
|
// We have to remap channels because GM needs percussion to be on the
|
|
// percussion channel but QuickTime can have that anywhere.
|
|
definePart(part, READ_BE_UINT32(_position._subtracks[0]._playPos + 80));
|
|
break;
|
|
case 5: // Tune Difference
|
|
case 8: // MIDI Channel
|
|
case 10: // No-op
|
|
case 11: // Used Notes
|
|
// Should be safe to skip these
|
|
break;
|
|
default:
|
|
warning("Unhandled general event %d", subType);
|
|
}
|
|
|
|
_position._subtracks[0]._playPos += dataSize + 4;
|
|
}
|
|
|
|
void MidiParser_QT::definePart(uint32 part, uint32 instrument) {
|
|
if (_partMap.contains(part))
|
|
warning("QuickTime MIDI part %d being redefined", part);
|
|
|
|
PartStatus partStatus;
|
|
partStatus.instrument = instrument;
|
|
partStatus.volume = 127;
|
|
partStatus.pan = 64;
|
|
partStatus.pitchBend = 0x2000;
|
|
_partMap[part] = partStatus;
|
|
}
|
|
|
|
byte MidiParser_QT::getChannel(uint32 part) {
|
|
// If we already mapped it, just go with it
|
|
if (!_channelMap.contains(part)) {
|
|
byte newChannel = findFreeChannel(part);
|
|
_channelMap[part] = newChannel;
|
|
setupPart(part);
|
|
}
|
|
|
|
return _channelMap[part];
|
|
}
|
|
|
|
byte MidiParser_QT::findFreeChannel(uint32 part) {
|
|
if (_partMap[part].instrument != 0x4001) {
|
|
// Normal Instrument -> First Free Channel
|
|
if (allChannelsAllocated())
|
|
deallocateFreeChannel();
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
if (i != 9 && !isChannelAllocated(i)) // 9 is reserved for Percussion
|
|
return i;
|
|
|
|
// Can't actually get here
|
|
}
|
|
|
|
// Drum Kit -> Percussion Channel
|
|
deallocateChannel(9);
|
|
return 9;
|
|
}
|
|
|
|
void MidiParser_QT::deallocateFreeChannel() {
|
|
for (int i = 0; i < 16; i++) {
|
|
if (i != 9 && !_activeNotes[i]) {
|
|
// TODO: Improve this by looking for the channel with the longest
|
|
// time since the last note.
|
|
deallocateChannel(i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
error("Exceeded QuickTime MIDI channel polyphony");
|
|
}
|
|
|
|
void MidiParser_QT::deallocateChannel(byte channel) {
|
|
for (auto &curChannel : _channelMap) {
|
|
if (curChannel._value == channel) {
|
|
_channelMap.erase(curChannel._key);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool MidiParser_QT::isChannelAllocated(byte channel) const {
|
|
for (const auto &curChannel : _channelMap)
|
|
if (curChannel._value == channel)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool MidiParser_QT::allChannelsAllocated() const {
|
|
// Less than 15? We definitely have room
|
|
if (_channelMap.size() < 15)
|
|
return false;
|
|
|
|
// 15? One of the allocated channels might be the percussion one
|
|
if (_channelMap.size() == 15)
|
|
for (const auto &channel : _channelMap)
|
|
if (channel._value == 9)
|
|
return false;
|
|
|
|
// 16 -> definitely all allocated
|
|
return true;
|
|
}
|
|
|
|
void MidiParser_QT::setupPart(uint32 part) {
|
|
PartStatus &status = _partMap[part];
|
|
byte channel = _channelMap[part];
|
|
EventInfo info;
|
|
|
|
// First, the program change
|
|
if (channel != 9) {
|
|
// 9 is always percussion
|
|
info.event = 0xC0 | channel;
|
|
info.basic.param1 = status.instrument;
|
|
_queuedEvents.push(info);
|
|
}
|
|
|
|
// Volume
|
|
info.event = 0xB0 | channel;
|
|
info.basic.param1 = 7;
|
|
info.basic.param2 = status.volume;
|
|
_queuedEvents.push(info);
|
|
|
|
// Pan
|
|
info.event = 0xB0 | channel;
|
|
info.basic.param1 = 10;
|
|
info.basic.param2 = status.pan;
|
|
_queuedEvents.push(info);
|
|
|
|
// Pitch Bend
|
|
info.event = 0xE0 | channel;
|
|
info.basic.param1 = status.pitchBend & 0x7F;
|
|
info.basic.param2 = status.pitchBend >> 7;
|
|
_queuedEvents.push(info);
|
|
}
|
|
|
|
void MidiParser_QT::resetTracking() {
|
|
MidiParser::resetTracking();
|
|
_channelMap.clear();
|
|
_queuedEvents.clear();
|
|
_partMap.clear();
|
|
}
|
|
|
|
void MidiParser_QT::sendToDriver(uint32 b) {
|
|
if (_source < 0) {
|
|
MidiParser::sendToDriver(b);
|
|
} else {
|
|
_driver->send(_source, b);
|
|
}
|
|
}
|
|
|
|
void MidiParser_QT::sendMetaEventToDriver(byte type, const byte *data, uint16 length) {
|
|
if (_source < 0) {
|
|
MidiParser::sendMetaEventToDriver(type, data, length);
|
|
} else {
|
|
_driver->metaEvent(_source, type, data, length);
|
|
}
|
|
}
|
|
|
|
Common::QuickTimeParser::SampleDesc *MidiParser_QT::readSampleDesc(Track *track, uint32 format, uint32 descSize) {
|
|
if (track->codecType == CODEC_TYPE_MIDI) {
|
|
debug(0, "MIDI Codec FourCC '%s'", tag2str(format));
|
|
|
|
_fd->readUint32BE(); // flags, ignore
|
|
descSize -= 4;
|
|
|
|
MIDISampleDesc *entry = new MIDISampleDesc(track, format);
|
|
entry->_requestSize = descSize;
|
|
entry->_requestData = (byte *)malloc(descSize);
|
|
_fd->read(entry->_requestData, descSize);
|
|
return entry;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
MidiParser_QT::MIDISampleDesc::MIDISampleDesc(Common::QuickTimeParser::Track *parentTrack, uint32 codecTag) :
|
|
Common::QuickTimeParser::SampleDesc(parentTrack, codecTag) {
|
|
}
|
|
|
|
void MidiParser_QT::initFromContainerTracks() {
|
|
const Common::Array<Common::QuickTimeParser::Track *> &tracks = Common::QuickTimeParser::_tracks;
|
|
|
|
for (uint32 i = 0; i < tracks.size(); i++) {
|
|
if (tracks[i]->codecType == CODEC_TYPE_MIDI) {
|
|
assert(tracks[i]->sampleDescs.size() == 1);
|
|
|
|
if (tracks[i]->editList.size() != 1)
|
|
warning("Unhandled QuickTime MIDI edit lists, things may go awry");
|
|
|
|
MIDITrackInfo trackInfo;
|
|
trackInfo.data = readWholeTrack(tracks[i], trackInfo.size);
|
|
trackInfo.timeScale = tracks[i]->timeScale;
|
|
_trackInfo.push_back(trackInfo);
|
|
}
|
|
}
|
|
|
|
initCommon();
|
|
}
|
|
|
|
void MidiParser_QT::initCommon() {
|
|
// Now we have all our info needed in _trackInfo from whatever container
|
|
// form, we can fill in the MidiParser tracks.
|
|
|
|
_numTracks = _trackInfo.size();
|
|
assert(_numTracks > 0);
|
|
|
|
for (uint32 i = 0; i < _trackInfo.size(); i++)
|
|
MidiParser::_tracks[i][0] = _trackInfo[i].data;
|
|
|
|
_ppqn = _trackInfo[0].timeScale;
|
|
resetTracking();
|
|
setTempo(1000000);
|
|
setTrack(0);
|
|
}
|
|
|
|
byte *MidiParser_QT::readWholeTrack(Common::QuickTimeParser::Track *track, uint32 &trackSize) {
|
|
// This just goes through all chunks and appends them together
|
|
|
|
Common::MemoryWriteStreamDynamic output(DisposeAfterUse::NO);
|
|
uint32 curSample = 0;
|
|
|
|
// Read in the note request data first
|
|
MIDISampleDesc *entry = (MIDISampleDesc *)track->sampleDescs[0];
|
|
output.write(entry->_requestData, entry->_requestSize);
|
|
|
|
for (uint i = 0; i < track->chunkCount; i++) {
|
|
_fd->seek(track->chunkOffsets[i]);
|
|
|
|
uint32 sampleCount = 0;
|
|
|
|
for (uint32 j = 0; j < track->sampleToChunkCount; j++)
|
|
if (i >= track->sampleToChunk[j].first)
|
|
sampleCount = track->sampleToChunk[j].count;
|
|
|
|
for (uint32 j = 0; j < sampleCount; j++, curSample++) {
|
|
uint32 size = (track->sampleSize != 0) ? track->sampleSize : track->sampleSizes[curSample];
|
|
|
|
byte *data = new byte[size];
|
|
_fd->read(data, size);
|
|
output.write(data, size);
|
|
delete[] data;
|
|
}
|
|
}
|
|
|
|
trackSize = output.size();
|
|
return output.getData();
|
|
}
|
|
|
|
uint32 MidiParser_QT::readUint32() {
|
|
uint32 value = READ_BE_UINT32(_position._subtracks[0]._playPos);
|
|
_position._subtracks[0]._playPos += 4;
|
|
return value;
|
|
}
|
|
|
|
MidiParser *MidiParser::createParser_QT(int8 source) {
|
|
return new MidiParser_QT(source);
|
|
}
|