/* 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 * based on: * - Raw output support (seq.cpp) by Michael Pearce * - Pseudo /dev/sequencer of TiMidity (timidity-io.c) * by Masanao Izumo * - 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 #include #include #include #include #include #include /* for getaddrinfo */ #include #include #include #include #include // 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)