Files
2026-02-02 04:50:13 +01:00

483 lines
19 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/>.
*
*/
/*
* The MCI documentation is: https://learn.microsoft.com/en-us/windows/win32/multimedia/mci
* Based on sources from Wine: https://github.com/wine-mirror/wine/blob/master/dlls/winmm/mci.c
* Table structure and algorithms also described in: https://patentimages.storage.googleapis.com/ad/06/7a/48766ca9df6fbc/US6397263.pdf
*/
#include "audio/audiostream.h"
#include "audio/decoders/wave.h"
#include "common/file.h"
#include "director/director.h"
#include "director/score.h"
#include "director/sound.h"
#include "director/window.h"
namespace Director {
enum MCITokenType {
MCI_PLAY,
MCI_OPEN,
MCI_CLOSE,
MCI_STATUS,
MCI_RECORD,
MCI_SEEK,
MCI_STOP,
MCI_PAUSE,
MCI_GETDEVCAPS,
MCI_SYSINFO,
MCI_BREAK,
MCI_SOUND,
MCI_SAVE,
MCI_LOAD,
MCI_RESUME,
MCI_SET,
MCI_INFO,
};
enum MCIDataType {
MCI_COMMAND_HEAD,
MCI_END_COMMAND,
MCI_END_COMMAND_LIST,
MCI_CONSTANT,
MCI_END_CONSTANT,
MCI_RETURN,
MCI_FLAG,
MCI_INTEGER,
MCI_STRING,
MCI_RECT,
MCI_DWORD_PTR,
};
typedef struct MCITokenData {
MCIDataType type;
Common::String string;
int integer = 0;
} MCITokenData;
typedef struct MCICommand {
MCITokenType id;
uint flags = 0;
Common::String device; /* MCI device name */
Common::HashMap<Common::String, MCITokenData> parameters;
} MCICommand;
enum MCIError {
MCIERR_NO_ERROR,
MCIERR_UNRECOGNISED_COMMAND,
MCIERR_STRING_PARSE,
};
struct CmdTableRow {
const char *keystr;
int flag;
int num;
MCIDataType data_type;
};
static const CmdTableRow table[] = {
{"open" ,MCI_OPEN ,0 ,MCI_COMMAND_HEAD },
{"" ,MCI_INTEGER ,0 ,MCI_RETURN } ,
{"notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{"wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{"type" ,0x00002000L ,-1 ,MCI_STRING } ,
{"element" ,0x00000200L ,-1 ,MCI_STRING } ,
{"alias" ,0x00000400L ,-1 ,MCI_STRING } ,
{"shareable" ,0x00000100L ,-1 ,MCI_FLAG } ,
{"" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{"close" ,MCI_CLOSE ,0 ,MCI_COMMAND_HEAD },
{"notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{"wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{"" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{"play" ,MCI_PLAY ,0 ,MCI_COMMAND_HEAD },
{"notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{"wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{"from" ,0x00000004L ,-1 ,MCI_INTEGER } ,
{"to" ,0x00000008L ,-1 ,MCI_INTEGER } ,
{"" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{"status" ,MCI_STATUS ,0 ,MCI_COMMAND_HEAD },
{"" ,MCI_DWORD_PTR ,0 ,MCI_RETURN } ,
{"notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{"wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{"" ,0x00000100L ,-1 ,MCI_CONSTANT } ,
{"position" ,0x00000002L ,-1 ,MCI_INTEGER } ,
{"length" ,0x00000001L ,-1 ,MCI_INTEGER } ,
{"number of tracks",-1 ,0x00000003L,MCI_INTEGER } ,
{"ready" ,0x00000007L ,-1 ,MCI_INTEGER } ,
{"mode" ,0x00000004L ,-1 ,MCI_INTEGER } ,
{"time format" ,-1 ,0x00000006L,MCI_INTEGER } ,
{"current track" ,-1 ,0x00000008L,MCI_INTEGER } ,
{"" ,0x00000000L ,-1 ,MCI_END_CONSTANT },
{ "track" ,0x00000010L ,-1 ,MCI_INTEGER } ,
{ "start" ,0x00000200L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "record" ,MCI_RECORD ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "from" ,0x00000004L ,-1 ,MCI_INTEGER } ,
{ "to" ,0x00000008L ,-1 ,MCI_INTEGER } ,
{ "insert" ,0x00000100L ,-1 ,MCI_FLAG } ,
{ "overwrite" ,0x00000200L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "seek" ,MCI_SEEK ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "to start" ,0x00000100L ,-1 ,MCI_FLAG } ,
{ "to end" ,0x00000200L ,-1 ,MCI_FLAG } ,
{ "to" ,0x00000008L ,-1 ,MCI_INTEGER } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "stop" ,MCI_STOP ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "pause" ,MCI_PAUSE ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "capability" ,MCI_GETDEVCAPS,0 ,MCI_COMMAND_HEAD },
{ "" ,MCI_INTEGER ,0 ,MCI_RETURN } ,
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000100L ,-1 ,MCI_CONSTANT } ,
{ "can record" ,0x00000001L ,-1 ,MCI_INTEGER } ,
{ "has audio" ,0x00000002L ,-1 ,MCI_INTEGER } ,
{ "has video" ,0x00000003L ,-1 ,MCI_INTEGER } ,
{ "uses files" ,0x00000005L ,-1 ,MCI_INTEGER } ,
{ "compound device",0x00000006L ,-1 ,MCI_INTEGER } ,
{ "device type" ,0x00000004L ,-1 ,MCI_INTEGER } ,
{ "can eject" ,0x00000007L ,-1 ,MCI_INTEGER } ,
{ "can play" ,0x00000008L ,-1 ,MCI_INTEGER } ,
{ "can save" ,0x00000009L ,-1 ,MCI_INTEGER } ,
{ "" ,0x00000000L ,-1 ,MCI_END_CONSTANT },
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "info" ,MCI_INFO ,0 ,MCI_COMMAND_HEAD },
{ "" ,MCI_STRING ,0 ,MCI_RETURN } ,
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "product" ,0x00000100L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "set" ,MCI_SET ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "time format" ,0x00000400L ,-1 ,MCI_CONSTANT } ,
{ "milliseconds" ,0x00000000L ,-1 ,MCI_INTEGER } ,
{ "ms" ,0x00000000L ,-1 ,MCI_INTEGER } ,
{ "" ,0x00000000L ,-1 ,MCI_END_CONSTANT },
{ "door open" ,0x00000100L ,-1 ,MCI_FLAG } ,
{ "door closed" ,0x00000200L ,-1 ,MCI_FLAG } ,
{ "audio" ,0x00000800L ,-1 ,MCI_CONSTANT } ,
{ "all" ,0x00000000L ,-1 ,MCI_INTEGER } ,
{ "left" ,0x00000001L ,-1 ,MCI_INTEGER } ,
{ "right" ,0x00000002L ,-1 ,MCI_INTEGER } ,
{ "" ,0x00000000L ,-1 ,MCI_END_CONSTANT },
{ "video" ,0x00001000L ,-1 ,MCI_FLAG } ,
{ "on" ,0x00002000L ,-1 ,MCI_FLAG } ,
{ "off" ,0x00004000L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "sysinfo" ,MCI_SYSINFO ,0 ,MCI_COMMAND_HEAD },
{ "" ,MCI_STRING ,0 ,MCI_RETURN } ,
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "quantity" ,0x00000100L ,-1 ,MCI_FLAG } ,
{ "open" ,0x00000200L ,-1 ,MCI_FLAG } ,
{ "installname" ,0x00000800L ,-1 ,MCI_FLAG } ,
{ "name" ,0x00000400L ,-1 ,MCI_INTEGER } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "break" ,MCI_BREAK ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "on" ,0x00000100L ,-1 ,MCI_INTEGER } ,
{ "off" ,0x00000400L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "sound" ,MCI_SOUND ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "save" ,MCI_SAVE ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000100L ,-1 ,MCI_STRING } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "load" ,MCI_LOAD ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000100L ,-1 ,MCI_STRING } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
{ "resume" ,MCI_RESUME ,0 ,MCI_COMMAND_HEAD },
{ "notify" ,0x00000001L ,-1 ,MCI_FLAG } ,
{ "wait" ,0x00000002L ,-1 ,MCI_FLAG } ,
{ "" ,0x00000000L ,-1 ,MCI_END_COMMAND } ,
};
static MCIError getString(const Common::String &name, uint &idx, Common::String &str) {
uint i_end;
if (name[idx] == '"' || name[idx] == '\'') { /* Quoted string */
char quote = name[idx];
i_end = name.findFirstOf(quote, idx + 1);
if (i_end == Common::String::npos) {
return MCIERR_STRING_PARSE; /* Unterminated string */
}
str = name.substr(idx + 1, i_end - idx - 1);
idx = i_end + 2;
} else { /* No quotes, so just find the next space */
i_end = name.findFirstOf(' ', idx);
if (i_end == Common::String::npos) {
i_end = name.size();
}
str = name.substr(idx, i_end - idx);
idx = i_end + 1;
}
debugC(5, kDebugLingoExec, "get_string(): Got string \"%s\"", str.c_str());
return MCIERR_NO_ERROR;
}
static void createTokenList(Common::StringArray &tokenList, const Common::String &name) {
uint idx = 0;
while (idx < name.size()) {
Common::String str;
MCIError err = getString(name, idx, str);
if (err != MCIERR_NO_ERROR) {
break;
}
tokenList.push_back(str);
}
}
static MCIError parseMCICommand(const Common::String &name, MCICommand &parsedCmd) {
Common::StringArray token_list;
createTokenList(token_list, name);
uint i_token = 0;
int i_table = 0;
int tableStart = -1, tableEnd = -1;
Common::String &verb = token_list[0];
/* Find the table section corresponding to the command's verb. */
for (auto& cmd : table) {
if ((tableStart < 0) && (cmd.data_type == MCI_COMMAND_HEAD) && (cmd.keystr == verb)) {
tableStart = i_table;
} else if ((tableStart >= 0) && (cmd.data_type == MCI_END_COMMAND)) {
tableEnd = i_table;
break;
}
i_table++;
}
debugC(5, kDebugLingoExec, "parseMCICommand(): tableStart: %d, tableEnd: %d", tableStart, tableEnd);
if (tableStart == -1 || tableEnd == -1) {
warning("parseMCICommand(): Verb %s not found in table", verb.c_str());
return MCIERR_UNRECOGNISED_COMMAND;
}
auto cmd = table[tableStart];
parsedCmd.id = (MCITokenType)cmd.flag;
parsedCmd.flags = cmd.num;
/* The MCI device will ALWAYS be the second token. */
parsedCmd.device = token_list[1];
/* Parse the rest of the arguments */
i_token = 2;
while (i_token < token_list.size()) {
bool found = false;
bool inConst = false;
int flag, cflag = 0;
const CmdTableRow *cmdtable, *c_cmdtable = nullptr;
auto& token = token_list[i_token];
for (i_table = tableStart; i_table < tableEnd; i_table++) {
MCIDataType cmd_type = table[i_table].data_type;
flag = table[i_table].flag;
cmdtable = &table[i_table];
switch (cmd_type) {
case MCI_CONSTANT:
c_cmdtable = cmdtable;
inConst = true;
cflag = flag;
break;
case MCI_END_CONSTANT:
c_cmdtable = nullptr;
inConst = false;
cflag = 0;
break;
default:
break;
}
if (token != table[i_table].keystr)
continue;
found = true;
switch (cmd_type) {
case MCI_COMMAND_HEAD:
case MCI_RETURN:
case MCI_END_COMMAND:
case MCI_END_COMMAND_LIST:
case MCI_CONSTANT:
case MCI_END_CONSTANT:
break;
case MCI_FLAG:
parsedCmd.flags |= flag;
i_token++;
break;
case MCI_INTEGER: {
if (inConst) { /* Handle the case where we've hit a MCI_INTEGER which is inside a MCI_CONSTANT block. */
MCITokenData token_data;
if (parsedCmd.parameters.tryGetVal(c_cmdtable->keystr, token_data)) {
token_data.integer |= flag;
} else {
token_data.type = MCI_CONSTANT;
token_data.integer |= flag;
parsedCmd.parameters[c_cmdtable->keystr] = token_data;
}
parsedCmd.flags |= cflag;
inConst = false;
} else {
parsedCmd.flags |= flag;
MCITokenData token_data;
token_data.type = MCI_INTEGER;
token_data.integer = atoi(token_list[++i_token].c_str());
parsedCmd.parameters[token] = token_data;
}
i_token++;
break;
}
case MCI_STRING: {
parsedCmd.flags |= flag;
MCITokenData token_data;
token_data.type = MCI_STRING;
token_data.string = token_list[++i_token];
parsedCmd.parameters[token] = token_data;
i_token++;
break;
}
default:
warning("parseMCICommand(): Unhandled command type %d", cmd_type);
return MCIERR_UNRECOGNISED_COMMAND;
}
}
if (!found) {
warning("parseMCICommand(): Parameter %s not found in table", token_list[i_token].c_str());
return MCIERR_UNRECOGNISED_COMMAND;
}
}
return MCIERR_NO_ERROR;
}
void Lingo::func_mci(const Common::String &name) {
MCICommand parsedCmd;
parseMCICommand(name, parsedCmd);
switch (parsedCmd.id) {
case MCI_OPEN: {
Common::File *file = new Common::File();
if (!file->open(Common::Path(parsedCmd.device, g_director->_dirSeparator))) {
warning("func_mci(): Failed to open %s", parsedCmd.device.c_str());
delete file;
return;
}
parsedCmd.parameters["type"].string.toLowercase(); /* In the case the open command type has something like WaveAudio instead of waveaudio */
if (parsedCmd.parameters["type"].string == "waveaudio") {
Audio::AudioStream *sound = Audio::makeWAVStream(file, DisposeAfterUse::YES);
if (parsedCmd.parameters.contains("alias")) {
_audioAliases[parsedCmd.parameters["alias"].string] = sound;
} else {
delete sound;
}
} else {
warning("func_mci(): Unhandled audio type %s", parsedCmd.parameters["type"].string.c_str());
delete file;
}
}
break;
case MCI_PLAY: {
warning("func_mci(): MCI play file: %s, from: %d, to: %d", parsedCmd.device.c_str(), parsedCmd.parameters["from"].integer, parsedCmd.parameters["to"].integer);
if (!_audioAliases.contains(parsedCmd.device)) {
warning("func_mci(): Unknown alias %s", parsedCmd.device.c_str());
return;
}
uint32 from = parsedCmd.parameters["from"].integer;
uint32 to = parsedCmd.parameters.contains("to") ? parsedCmd.parameters["to"].integer : -1;
_vm->getCurrentWindow()->getSoundManager()->playMCI(*_audioAliases[parsedCmd.device], from, to);
}
break;
default:
warning("func_mci: Unhandled MCI command: %d", parsedCmd.id); /* TODO: Convert MCITokenType into string */
}
}
void Lingo::func_mciwait(const Common::String &name) {
warning("STUB: MCI wait file: %s", name.c_str());
}
} // End of namespace Director