Files
scummvm/audio/midiparser_qt.cpp
Matthew Duggan 9749f59365 AUDIO: Make MidiParser more const correct
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.
2025-08-30 22:39:15 +03:00

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);
}