mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-16 06:39:17 +00:00
552 lines
14 KiB
C++
552 lines
14 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 2
|
|
* 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, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Output to TiMidity++ MIDI server support
|
|
* by Dmitry Marakasov <amdmi3@amdmi3.ru>
|
|
* based on:
|
|
* - Raw output support (seq.cpp) by Michael Pearce
|
|
* - Pseudo /dev/sequencer of TiMidity (timidity-io.c)
|
|
* by Masanao Izumo <mo@goice.co.jp>
|
|
* - sys/soundcard.h by Hannu Savolainen (got from my FreeBSD
|
|
* distribution, for which it was modified by Luigi Rizzo)
|
|
*
|
|
*/
|
|
|
|
// Disable symbol overrides so that we can use system headers.
|
|
#define FORBIDDEN_SYMBOL_ALLOW_ALL
|
|
|
|
#include "common/scummsys.h"
|
|
|
|
#if defined(USE_TIMIDITY)
|
|
|
|
#include "common/endian.h"
|
|
#include "common/error.h"
|
|
#include "common/str.h"
|
|
#include "common/textconsole.h"
|
|
#include "audio/musicplugin.h"
|
|
#include "audio/mpu401.h"
|
|
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/param.h>
|
|
#include <netdb.h> /* for getaddrinfo */
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <stdarg.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
|
|
// BeOS BONE uses snooze (x/1000) in place of usleep(x)
|
|
#ifdef __BEOS__
|
|
#define usleep(v) snooze(v/1000)
|
|
#endif
|
|
|
|
#define SEQ_MIDIPUTC 5
|
|
|
|
#define TIMIDITY_LOW_DELAY
|
|
|
|
#ifdef TIMIDITY_LOW_DELAY
|
|
#define BUF_LOW_SYNC 0.1
|
|
#define BUF_HIGH_SYNC 0.15
|
|
#else
|
|
#define BUF_LOW_SYNC 0.4
|
|
#define BUF_HIGH_SYNC 0.8
|
|
#endif
|
|
|
|
/* default host & port */
|
|
#define DEFAULT_TIMIDITY_HOST "127.0.0.1"
|
|
#define DEFAULT_TIMIDITY_PORT "7777"
|
|
|
|
class MidiDriver_TIMIDITY : public MidiDriver_MPU401 {
|
|
public:
|
|
MidiDriver_TIMIDITY();
|
|
|
|
int open() override;
|
|
bool isOpen() const override { return _isOpen; }
|
|
void close() override;
|
|
void send(uint32 b) override;
|
|
void sysEx(const byte *msg, uint16 length) override;
|
|
|
|
private:
|
|
/* creates a tcp connection to TiMidity server, returns filedesc (like open()) */
|
|
int connect_to_server(const char* hostname, const char* tcp_port);
|
|
|
|
/* send command to the server; printf-like; returns reply string */
|
|
char *timidity_ctl_command(MSVC_PRINTF const char *fmt, ...) GCC_PRINTF(2, 3);
|
|
|
|
/* timidity data socket-related stuff */
|
|
void timidity_meta_seq(int p1, int p2, int p3);
|
|
int timidity_sync(int centsec);
|
|
int timidity_eot();
|
|
|
|
/* write() analogue for any midi data */
|
|
void timidity_write_data(const void *buf, size_t nbytes);
|
|
|
|
/* get single line of server reply on control connection */
|
|
int fdgets(char *buff, size_t buff_size);
|
|
|
|
/* teardown connection to server */
|
|
void teardown();
|
|
|
|
/* close (if needed) and nullify both control and data filedescs */
|
|
void close_all();
|
|
|
|
private:
|
|
bool _isOpen;
|
|
int _device_num;
|
|
|
|
int _control_fd;
|
|
int _data_fd;
|
|
|
|
/* buffer for partial data read from _control_fd - from timidity-io.c, see fdgets() */
|
|
char _controlbuffer[BUFSIZ];
|
|
int _controlbuffer_count; /* beginning of read pointer */
|
|
int _controlbuffer_size; /* end of read pointer */
|
|
};
|
|
|
|
MidiDriver_TIMIDITY::MidiDriver_TIMIDITY() {
|
|
_isOpen = false;
|
|
_device_num = 0;
|
|
|
|
/* init fd's */
|
|
_control_fd = _data_fd = -1;
|
|
|
|
/* init buffer for control connection */
|
|
_controlbuffer_count = _controlbuffer_size = 0;
|
|
}
|
|
|
|
int MidiDriver_TIMIDITY::open() {
|
|
char *res;
|
|
char timidity_host[NI_MAXHOST];
|
|
char timidity_port[6], data_port[6];
|
|
int num;
|
|
|
|
/* count ourselves open */
|
|
if (_isOpen)
|
|
return MERR_ALREADY_OPEN;
|
|
_isOpen = true;
|
|
|
|
/* get server hostname; if not specified in env, use default */
|
|
if ((res = getenv("TIMIDITY_HOST")) == NULL)
|
|
Common::strlcpy(timidity_host, DEFAULT_TIMIDITY_HOST, sizeof(timidity_host));
|
|
else
|
|
Common::strlcpy(timidity_host, res, sizeof(timidity_host));
|
|
|
|
/* extract control port */
|
|
if ((res = strrchr(timidity_host, ':')) != NULL) {
|
|
*res++ = '\0';
|
|
Common::strlcpy(timidity_port, res, sizeof(timidity_port));
|
|
} else {
|
|
Common::strlcpy(timidity_port, DEFAULT_TIMIDITY_PORT, sizeof(timidity_port));
|
|
}
|
|
|
|
/*
|
|
* create control connection to the server
|
|
*/
|
|
if ((_control_fd = connect_to_server(timidity_host, timidity_port)) < 0) {
|
|
warning("TiMidity: can't open control connection (host=%s, port=%s)", timidity_host, timidity_port);
|
|
return -1;
|
|
}
|
|
|
|
/* should read greeting issued by server upon connect:
|
|
* "220 TiMidity++ v2.13.2 ready)" */
|
|
res = timidity_ctl_command(NULL);
|
|
if (atoi(res) != 220) {
|
|
warning("TiMidity: bad response from server (host=%s, port=%s): %s", timidity_host, timidity_port, res);
|
|
close_all();
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* setup buf and prepare data connection
|
|
*/
|
|
/* should read: "200 OK" */
|
|
res = timidity_ctl_command("SETBUF %f %f", BUF_LOW_SYNC, BUF_HIGH_SYNC);
|
|
if (atoi(res) != 200)
|
|
warning("TiMidity: bad reply for SETBUF command: %s", res);
|
|
|
|
/* should read something like "200 63017 is ready acceptable",
|
|
* where 63017 is port for data connection */
|
|
#ifdef SCUMM_LITTLE_ENDIAN
|
|
res = timidity_ctl_command("OPEN lsb");
|
|
#else
|
|
res = timidity_ctl_command("OPEN msb");
|
|
#endif
|
|
|
|
if (atoi(res) != 200) {
|
|
warning("TiMidity: bad reply for OPEN command: %s", res);
|
|
close_all();
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* open data connection
|
|
*/
|
|
num = atoi(res + 4);
|
|
if (num > 65535) {
|
|
warning("TiMidity: Invalid port %d given.\n", num);
|
|
close_all();
|
|
return -1;
|
|
}
|
|
snprintf(data_port, sizeof(data_port), "%d", num);
|
|
if ((_data_fd = connect_to_server(timidity_host, data_port)) < 0) {
|
|
warning("TiMidity: can't open data connection (host=%s, port=%s)", timidity_host, data_port);
|
|
close_all();
|
|
return -1;
|
|
}
|
|
|
|
/* should read message issued after connecting to data port:
|
|
* "200 Ready data connection" */
|
|
res = timidity_ctl_command(NULL);
|
|
if (atoi(res) != 200) {
|
|
warning("Can't connect timidity: %s\t(host=%s, port=%s)", res, timidity_host, data_port);
|
|
close_all();
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* From seq.cpp
|
|
*/
|
|
if (getenv("SCUMMVM_MIDIPORT"))
|
|
_device_num = atoi(getenv("SCUMMVM_MIDIPORT"));
|
|
|
|
return 0;
|
|
}
|
|
|
|
void MidiDriver_TIMIDITY::close() {
|
|
teardown();
|
|
|
|
MidiDriver_MPU401::close();
|
|
_isOpen = false;
|
|
}
|
|
|
|
void MidiDriver_TIMIDITY::close_all() {
|
|
if (_control_fd >= 0)
|
|
::close(_control_fd);
|
|
|
|
if (_data_fd >= 0)
|
|
::close(_data_fd);
|
|
|
|
_control_fd = _data_fd = -1;
|
|
}
|
|
|
|
void MidiDriver_TIMIDITY::teardown() {
|
|
char *res;
|
|
|
|
/* teardown connection to server (see timidity-io.c) if it
|
|
* is initialized */
|
|
if (_data_fd >= 0 && _control_fd >= 0) {
|
|
timidity_eot();
|
|
timidity_sync(0);
|
|
|
|
/* scroll through all "302 Data connection is (already) closed"
|
|
* messages till we reach something like "200 Bye" */
|
|
do {
|
|
res = timidity_ctl_command("QUIT");
|
|
} while (*res && atoi(res) && atoi(res) != 302);
|
|
}
|
|
|
|
/* now close and nullify both filedescs */
|
|
close_all();
|
|
}
|
|
|
|
int MidiDriver_TIMIDITY::connect_to_server(const char* hostname, const char* tcp_port) {
|
|
int fd;
|
|
struct addrinfo hints;
|
|
struct addrinfo *result, *rp;
|
|
|
|
/* get all address(es) matching host and port */
|
|
memset(&hints, 0, sizeof(struct addrinfo));
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */
|
|
if (getaddrinfo(hostname, tcp_port, &hints, &result) != 0) {
|
|
warning("TiMidity: getaddrinfo: %s\n", strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* Try all address structures we have got previously */
|
|
for (rp = result; rp != NULL; rp = rp->ai_next) {
|
|
if ((fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1)
|
|
continue;
|
|
if (connect(fd, rp->ai_addr, rp->ai_addrlen) != -1)
|
|
break;
|
|
::close(fd);
|
|
}
|
|
|
|
freeaddrinfo(result);
|
|
|
|
if (rp == NULL) {
|
|
warning("TiMidity: Could not connect\n");
|
|
return -1;
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
char *MidiDriver_TIMIDITY::timidity_ctl_command(const char *fmt, ...) {
|
|
/* XXX: I don't like this static buffer!!! */
|
|
static char buff[BUFSIZ];
|
|
va_list ap;
|
|
|
|
if (fmt != NULL) {
|
|
/* if argumends are present, write them to control connection */
|
|
va_start(ap, fmt);
|
|
int len = vsnprintf(buff, BUFSIZ-1, fmt, ap); /* leave one byte for \n */
|
|
va_end(ap);
|
|
|
|
/* add newline if needed */
|
|
if (len > 0 && buff[len - 1] != '\n')
|
|
buff[len++] = '\n';
|
|
|
|
/* write command to control socket */
|
|
if (write(_control_fd, buff, len) == -1) {
|
|
warning("TiMidity: CONTROL WRITE FAILED (%s)", strerror(errno));
|
|
// TODO: Disable output?
|
|
//close_all();
|
|
}
|
|
}
|
|
|
|
while (1) {
|
|
/* read reply */
|
|
if (fdgets(buff, sizeof(buff)) <= 0) {
|
|
strcpy(buff, "Read error\n");
|
|
break;
|
|
}
|
|
|
|
/* report errors from server */
|
|
int status = atoi(buff);
|
|
if (400 <= status && status <= 499) { /* Error of data stream */
|
|
warning("TiMidity: error from server: %s", buff);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return buff;
|
|
}
|
|
|
|
void MidiDriver_TIMIDITY::timidity_meta_seq(int p1, int p2, int p3) {
|
|
/* see _CHN_COMMON from soundcard.h; this is simplified
|
|
* to just send seq to the server without any buffers,
|
|
* delays and extra functions/macros */
|
|
unsigned char seqbuf[8];
|
|
|
|
seqbuf[0] = 0x92;
|
|
seqbuf[1] = 0;
|
|
seqbuf[2] = 0xff;
|
|
seqbuf[3] = 0x7f;
|
|
seqbuf[4] = p1;
|
|
seqbuf[5] = p2;
|
|
WRITE_UINT16(&seqbuf[6], p3);
|
|
|
|
timidity_write_data(seqbuf, sizeof(seqbuf));
|
|
}
|
|
|
|
int MidiDriver_TIMIDITY::timidity_sync(int centsec) {
|
|
char *res;
|
|
int status;
|
|
unsigned long sleep_usec;
|
|
|
|
timidity_meta_seq(0x02, 0x00, centsec); /* Wait playout */
|
|
|
|
/* Wait "301 Sync OK" */
|
|
do {
|
|
res = timidity_ctl_command(NULL);
|
|
status = atoi(res);
|
|
|
|
if (status != 301)
|
|
warning("TiMidity: error: SYNC: %s", res);
|
|
|
|
} while (status && status != 301);
|
|
|
|
if (status != 301)
|
|
return -1; /* error */
|
|
|
|
sleep_usec = (unsigned long)(atof(res + 4) * 1000000);
|
|
|
|
if (sleep_usec > 0)
|
|
usleep(sleep_usec);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int MidiDriver_TIMIDITY::timidity_eot(void) {
|
|
timidity_meta_seq(0x00, 0x00, 0); /* End of playing */
|
|
return timidity_sync(0);
|
|
}
|
|
|
|
void MidiDriver_TIMIDITY::timidity_write_data(const void *buf, size_t nbytes) {
|
|
/* nowhere to write... */
|
|
if (_data_fd < 0)
|
|
return;
|
|
|
|
/* write, and disable everything if write failed */
|
|
/* TODO: add reconnect? */
|
|
if (write(_data_fd, buf, nbytes) == -1) {
|
|
warning("TiMidity: DATA WRITE FAILED (%s), DISABLING MUSIC OUTPUT", strerror(errno));
|
|
close_all();
|
|
}
|
|
}
|
|
|
|
int MidiDriver_TIMIDITY::fdgets(char *buff, size_t buff_size) {
|
|
int n, count, size;
|
|
char *buff_endp = buff + buff_size - 1, *pbuff, *beg;
|
|
|
|
count = _controlbuffer_count;
|
|
size = _controlbuffer_size;
|
|
pbuff = _controlbuffer;
|
|
beg = buff;
|
|
do {
|
|
if (count == size) {
|
|
if ((n = read(_control_fd, pbuff, BUFSIZ)) <= 0) {
|
|
*buff = '\0';
|
|
if (n == 0) {
|
|
_controlbuffer_count = _controlbuffer_size = 0;
|
|
return buff - beg;
|
|
}
|
|
return -1; /* < 0 error */
|
|
}
|
|
count = _controlbuffer_count = 0;
|
|
size = _controlbuffer_size = n;
|
|
}
|
|
*buff++ = pbuff[count++];
|
|
} while (*(buff - 1) != '\n' && buff != buff_endp);
|
|
|
|
*buff = '\0';
|
|
_controlbuffer_count = count;
|
|
|
|
return buff - beg;
|
|
}
|
|
|
|
void MidiDriver_TIMIDITY::send(uint32 b) {
|
|
unsigned char buf[256];
|
|
int position = 0;
|
|
|
|
midiDriverCommonSend(b);
|
|
|
|
switch (b & 0xF0) {
|
|
case 0x80:
|
|
case 0x90:
|
|
case 0xA0:
|
|
case 0xB0:
|
|
case 0xE0:
|
|
buf[position++] = SEQ_MIDIPUTC;
|
|
buf[position++] = (unsigned char)b;
|
|
buf[position++] = _device_num;
|
|
buf[position++] = 0;
|
|
buf[position++] = SEQ_MIDIPUTC;
|
|
buf[position++] = (unsigned char)((b >> 8) & 0x7F);
|
|
buf[position++] = _device_num;
|
|
buf[position++] = 0;
|
|
buf[position++] = SEQ_MIDIPUTC;
|
|
buf[position++] = (unsigned char)((b >> 16) & 0x7F);
|
|
buf[position++] = _device_num;
|
|
buf[position++] = 0;
|
|
break;
|
|
case 0xC0:
|
|
case 0xD0:
|
|
buf[position++] = SEQ_MIDIPUTC;
|
|
buf[position++] = (unsigned char)b;
|
|
buf[position++] = _device_num;
|
|
buf[position++] = 0;
|
|
buf[position++] = SEQ_MIDIPUTC;
|
|
buf[position++] = (unsigned char)((b >> 8) & 0x7F);
|
|
buf[position++] = _device_num;
|
|
buf[position++] = 0;
|
|
break;
|
|
default:
|
|
warning("MidiDriver_TIMIDITY::send: unknown : %08x", (int)b);
|
|
break;
|
|
}
|
|
|
|
timidity_write_data(buf, position);
|
|
}
|
|
|
|
void MidiDriver_TIMIDITY::sysEx(const byte *msg, uint16 length) {
|
|
fprintf(stderr, "Timidity::sysEx\n");
|
|
unsigned char buf[266*4];
|
|
int position = 0;
|
|
const byte *chr = msg;
|
|
|
|
assert(length + 2 <= 266);
|
|
|
|
midiDriverCommonSysEx(msg, length);
|
|
|
|
buf[position++] = SEQ_MIDIPUTC;
|
|
buf[position++] = 0xF0;
|
|
buf[position++] = _device_num;
|
|
buf[position++] = 0;
|
|
for (; length; --length, ++chr) {
|
|
buf[position++] = SEQ_MIDIPUTC;
|
|
buf[position++] = (unsigned char) *chr & 0x7F;
|
|
buf[position++] = _device_num;
|
|
buf[position++] = 0;
|
|
}
|
|
buf[position++] = SEQ_MIDIPUTC;
|
|
buf[position++] = 0xF7;
|
|
buf[position++] = _device_num;
|
|
buf[position++] = 0;
|
|
|
|
timidity_write_data(buf, position);
|
|
}
|
|
|
|
|
|
// Plugin interface
|
|
|
|
class TimidityMusicPlugin : public MusicPluginObject {
|
|
public:
|
|
const char *getName() const {
|
|
return "TiMidity";
|
|
}
|
|
|
|
const char *getId() const {
|
|
return "timidity";
|
|
}
|
|
|
|
MusicDevices getDevices() const;
|
|
Common::Error createInstance(MidiDriver **mididriver, MidiDriver::DeviceHandle = 0) const;
|
|
};
|
|
|
|
MusicDevices TimidityMusicPlugin::getDevices() const {
|
|
MusicDevices devices;
|
|
devices.push_back(MusicDevice(this, "", MT_GM));
|
|
return devices;
|
|
}
|
|
|
|
Common::Error TimidityMusicPlugin::createInstance(MidiDriver **mididriver, MidiDriver::DeviceHandle) const {
|
|
*mididriver = new MidiDriver_TIMIDITY();
|
|
|
|
return Common::kNoError;
|
|
}
|
|
|
|
//#if PLUGIN_ENABLED_DYNAMIC(TIMIDITY)
|
|
//REGISTER_PLUGIN_DYNAMIC(TIMIDITY, PLUGIN_TYPE_MUSIC, TimidityMusicPlugin);
|
|
//#else
|
|
REGISTER_PLUGIN_STATIC(TIMIDITY, PLUGIN_TYPE_MUSIC, TimidityMusicPlugin);
|
|
//#endif
|
|
|
|
#endif // defined(USE_TIMIDITY)
|