scummvm/backends/midi/timidity.cpp
Johannes Schickel 1431944294 Add GCC_PRINTF parameter.
svn-id: r42104
2009-07-04 18:54:06 +00:00

566 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.
*
* $URL$
* $Id$
*/
/*
* 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)
*
*/
#if defined (UNIX)
#include "common/util.h"
#include "common/endian.h"
#include "sound/musicplugin.h"
#include "sound/mpu401.h"
#include <fcntl.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 gethostbyname */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdarg.h>
#include <stdlib.h>
#include <errno.h>
// WORKAROUND bug #1870304: Solaris does not provide INADDR_NONE.
#ifndef INADDR_NONE
#define INADDR_NONE 0xffffffff
#endif
// 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();
void close();
void send(uint32 b);
void sysEx(const byte *msg, uint16 length);
private:
/* standart routine to extract ip address from a string */
in_addr_t host_to_addr(const char* address);
/* creates a tcp connection to TiMidity server, returns filedesc (like open()) */
int connect_to_server(const char* hostname, unsigned short tcp_port);
/* send command to the server; printf-like; returns reply string */
char *timidity_ctl_command(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[MAXHOSTNAMELEN];
int timidity_port, data_port, i;
/* 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)
strncpy(timidity_host, DEFAULT_TIMIDITY_HOST, MAXHOSTNAMELEN);
else
strncpy(timidity_host, res, sizeof(timidity_host));
timidity_host[sizeof(timidity_host) - 1] = '\0';
/* extract control port */
if ((res = strrchr(timidity_host, ':')) != NULL) {
*res++ = '\0';
timidity_port = atoi(res);
} else {
timidity_port = DEFAULT_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=%d)", 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=%d): %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 */
i = 1;
if (*(char *)&i == 1)
res = timidity_ctl_command("OPEN lsb");
else
res = timidity_ctl_command("OPEN msb");
if (atoi(res) != 200) {
warning("TiMidity: bad reply for OPEN command: %s", res);
close_all();
return -1;
}
/*
* open data connection
*/
data_port = atoi(res + 4);
if ((_data_fd = connect_to_server(timidity_host, data_port)) < 0) {
warning("TiMidity: can't open data connection (host=%s, port=%d)", 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=%d)", 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();
}
in_addr_t MidiDriver_TIMIDITY::host_to_addr(const char* address) {
in_addr_t addr;
struct hostent *hp;
/* first check if IP address is given (like 127.0.0.1)*/
if ((addr = inet_addr(address)) != INADDR_NONE)
return addr;
/* if not, try to resolve a hostname */
if ((hp = gethostbyname(address)) == NULL) {
warning("TiMidity: unknown hostname: %s", address);
return INADDR_NONE;
}
memcpy(&addr, hp->h_addr, (int)sizeof(in_addr_t) <= hp->h_length ? sizeof(in_addr_t) : hp->h_length);
return addr;
}
int MidiDriver_TIMIDITY::connect_to_server(const char* hostname, unsigned short tcp_port) {
int fd;
struct sockaddr_in in;
unsigned int addr;
/* create socket */
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
warning("TiMidity: socket(): %s", strerror(errno));
return -1;
}
/* connect */
memset(&in, 0, sizeof(in));
in.sin_family = AF_INET;
in.sin_port = htons(tcp_port);
addr = host_to_addr(hostname);
memcpy(&in.sin_addr, &addr, 4);
if (connect(fd, (struct sockaddr *)&in, sizeof(in)) < 0) {
warning("TiMidity: connect(): %s", strerror(errno));
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];
int status, len;
va_list ap;
if (fmt != NULL) {
/* if argumends are present, write them to control connection */
va_start(ap, fmt);
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 */
(void)write(_control_fd, buff, len);
}
while (1) {
/* read reply */
if (fdgets(buff, sizeof(buff)) <= 0) {
strcpy(buff, "Read error\n");
break;
}
/* report errors from server */
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 */
u_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, len, count, size;
char *buff_endp = buff + buff_size - 1, *pbuff, *beg;
len = 0;
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;
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);
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(Audio::Mixer *mixer, MidiDriver **mididriver) const;
};
MusicDevices TimidityMusicPlugin::getDevices() const {
MusicDevices devices;
devices.push_back(MusicDevice(this, "", MT_GM));
return devices;
}
Common::Error TimidityMusicPlugin::createInstance(Audio::Mixer *mixer, MidiDriver **mididriver) const {
*mididriver = new MidiDriver_TIMIDITY();
return Common::kNoError;
}
MidiDriver *MidiDriver_TIMIDITY_create(Audio::Mixer *mixer) {
MidiDriver *mididriver;
TimidityMusicPlugin p;
p.createInstance(mixer, &mididriver);
return mididriver;
}
//#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 (UNIX)