| 1 | /* ScummVM - Graphic Adventure Engine |
| 2 | * |
| 3 | * ScummVM is the legal property of its developers, whose names |
| 4 | * are too numerous to list here. Please refer to the COPYRIGHT |
| 5 | * file distributed with this source distribution. |
| 6 | * |
| 7 | * This program is free software; you can redistribute it and/or |
| 8 | * modify it under the terms of the GNU General Public License |
| 9 | * as published by the Free Software Foundation; either version 2 |
| 10 | * of the License, or (at your option) any later version. |
| 11 | * |
| 12 | * This program is distributed in the hope that it will be useful, |
| 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | * GNU General Public License for more details. |
| 16 | * |
| 17 | * You should have received a copy of the GNU General Public License |
| 18 | * along with this program; if not, write to the Free Software |
| 19 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 20 | * |
| 21 | * $URL$ |
| 22 | * $Id$ |
| 23 | */ |
| 24 | |
| 25 | /* |
| 26 | * Output to TiMidity++ MIDI server support |
| 27 | * by Dmitry Marakasov <amdmi3@amdmi3.ru> |
| 28 | * based on: |
| 29 | * - Raw output support (seq.cpp) by Michael Pearce |
| 30 | * - Pseudo /dev/sequencer of TiMidity (timidity-io.c) |
| 31 | * by Masanao Izumo <mo@goice.co.jp> |
| 32 | * - sys/soundcard.h by Hannu Savolainen (got from my FreeBSD |
| 33 | * distribution, for which it was modified by Luigi Rizzo) |
| 34 | * |
| 35 | */ |
| 36 | |
| 37 | #if defined (UNIX) |
| 38 | |
| 39 | #include "sound/mpu401.h" |
| 40 | #include "common/util.h" |
| 41 | |
| 42 | #include <fcntl.h> |
| 43 | #include <unistd.h> |
| 44 | #include <stdio.h> |
| 45 | #include <string.h> |
| 46 | #include <sys/types.h> |
| 47 | #include <sys/socket.h> |
| 48 | #include <sys/param.h> |
| 49 | #include <netdb.h> /* for gethostbyname */ |
| 50 | #include <netinet/in.h> |
| 51 | #include <arpa/inet.h> |
| 52 | #include <stdarg.h> |
| 53 | #include <stdlib.h> |
| 54 | #include <errno.h> |
| 55 | |
| 56 | #define SEQ_MIDIPUTC 5 |
| 57 | |
| 58 | #define TIMIDITY_LOW_DELAY |
| 59 | |
| 60 | #ifdef TIMIDITY_LOW_DELAY |
| 61 | #define BUF_LOW_SYNC 0.1 |
| 62 | #define BUF_HIGH_SYNC 0.15 |
| 63 | #else |
| 64 | #define BUF_LOW_SYNC 0.4 |
| 65 | #define BUF_HIGH_SYNC 0.8 |
| 66 | #endif |
| 67 | |
| 68 | /* default host & port */ |
| 69 | #define DEFAULT_TIMIDITY_HOST "127.0.0.1" |
| 70 | #define DEFAULT_TIMIDITY_PORT 7777 |
| 71 | |
| 72 | class MidiDriver_TIMIDITY : public MidiDriver_MPU401 { |
| 73 | public: |
| 74 | MidiDriver_TIMIDITY(); |
| 75 | |
| 76 | int open(); |
| 77 | void close(); |
| 78 | void send(uint32 b); |
| 79 | void sysEx(const byte *msg, uint16 length); |
| 80 | |
| 81 | private: |
| 82 | /* standart routine to extract ip address from a string */ |
| 83 | in_addr_t host_to_addr(const char* address); |
| 84 | |
| 85 | /* creates a tcp connection to TiMidity server, returns filedesc (like open()) */ |
| 86 | int connect_to_server(const char* hostname, unsigned short tcp_port); |
| 87 | |
| 88 | /* send command to the server; printf-like; returns reply string */ |
| 89 | char *timidity_ctl_command(const char *fmt, ...); |
| 90 | |
| 91 | /* timidity data socket-related stuff */ |
| 92 | void timidity_meta_seq(int p1, int p2, int p3); |
| 93 | int timidity_sync(int centsec); |
| 94 | int timidity_eot(); |
| 95 | |
| 96 | /* write() analogue for any midi data */ |
| 97 | void timidity_write_data(const void *buf, size_t nbytes); |
| 98 | |
| 99 | /* get single line of server reply on control connection */ |
| 100 | int fdgets(char *buff, size_t buff_size); |
| 101 | |
| 102 | /* teardown connection to server */ |
| 103 | void teardown(); |
| 104 | |
| 105 | /* close (if needed) and nullify both control and data filedescs */ |
| 106 | void close_all(); |
| 107 | |
| 108 | private: |
| 109 | bool _isOpen; |
| 110 | int _device_num; |
| 111 | |
| 112 | int _control_fd; |
| 113 | int _data_fd; |
| 114 | |
| 115 | /* buffer for partial data read from _control_fd - from timidity-io.c, see fdgets() */ |
| 116 | char _controlbuffer[BUFSIZ]; |
| 117 | int _controlbuffer_count; /* beginning of read pointer */ |
| 118 | int _controlbuffer_size; /* end of read pointer */ |
| 119 | }; |
| 120 | |
| 121 | MidiDriver_TIMIDITY::MidiDriver_TIMIDITY() { |
| 122 | _isOpen = false; |
| 123 | _device_num = 0; |
| 124 | |
| 125 | /* init fd's */ |
| 126 | _control_fd = _data_fd = -1; |
| 127 | |
| 128 | /* init buffer for control connection */ |
| 129 | _controlbuffer_count = _controlbuffer_size = 0; |
| 130 | } |
| 131 | |
| 132 | int MidiDriver_TIMIDITY::open() { |
| 133 | char *res; |
| 134 | char timidity_host[MAXHOSTNAMELEN]; |
| 135 | int timidity_port, data_port, i; |
| 136 | |
| 137 | /* count ourselves open */ |
| 138 | if (_isOpen) |
| 139 | return MERR_ALREADY_OPEN; |
| 140 | _isOpen = true; |
| 141 | |
| 142 | /* get server hostname; if not specified in env, use default */ |
| 143 | if ((res = getenv("TIMIDITY_HOST")) == NULL) |
| 144 | strncpy(timidity_host, DEFAULT_TIMIDITY_HOST, MAXHOSTNAMELEN); |
| 145 | else |
| 146 | strncpy(timidity_host, res, sizeof(timidity_host)); |
| 147 | |
| 148 | timidity_host[sizeof(timidity_host) - 1] = '\0'; |
| 149 | |
| 150 | /* extract control port */ |
| 151 | if ((res = strrchr(timidity_host, ':')) != NULL) { |
| 152 | *res++ = '\0'; |
| 153 | timidity_port = atoi(res); |
| 154 | } else { |
| 155 | timidity_port = DEFAULT_TIMIDITY_PORT; |
| 156 | } |
| 157 | |
| 158 | /* |
| 159 | * create control connection to the server |
| 160 | */ |
| 161 | if ((_control_fd = connect_to_server(timidity_host, timidity_port)) < 0) { |
| 162 | warning("TiMidity: can't open control connection (host=%s, port=%d)", timidity_host, timidity_port); |
| 163 | return -1; |
| 164 | } |
| 165 | |
| 166 | /* should read greeting issued by server upon connect: |
| 167 | * "220 TiMidity++ v2.13.2 ready)" */ |
| 168 | res = timidity_ctl_command(NULL); |
| 169 | if (atoi(res) != 220) { |
| 170 | warning("TiMidity: bad response from server (host=%s, port=%d): %s", timidity_host, timidity_port, res); |
| 171 | close_all(); |
| 172 | return -1; |
| 173 | } |
| 174 | |
| 175 | /* |
| 176 | * setup buf and prepare data connection |
| 177 | */ |
| 178 | /* should read: "200 OK" */ |
| 179 | res = timidity_ctl_command("SETBUF %f %f", BUF_LOW_SYNC, BUF_HIGH_SYNC); |
| 180 | if (atoi(res) != 200) |
| 181 | warning("TiMidity: bad reply for SETBUF command: %s", res); |
| 182 | |
| 183 | /* should read something like "200 63017 is ready acceptable", |
| 184 | * where 63017 is port for data connection */ |
| 185 | i = 1; |
| 186 | if (*(char *)&i == 1) |
| 187 | res = timidity_ctl_command("OPEN lsb"); |
| 188 | else |
| 189 | res = timidity_ctl_command("OPEN msb"); |
| 190 | |
| 191 | if (atoi(res) != 200) { |
| 192 | warning("TiMidity: bad reply for OPEN command: %s", res); |
| 193 | close_all(); |
| 194 | return -1; |
| 195 | } |
| 196 | |
| 197 | /* |
| 198 | * open data connection |
| 199 | */ |
| 200 | data_port = atoi(res + 4); |
| 201 | if ((_data_fd = connect_to_server(timidity_host, data_port)) < 0) { |
| 202 | warning("TiMidity: can't open data connection (host=%s, port=%d)", timidity_host, data_port); |
| 203 | close_all(); |
| 204 | return -1; |
| 205 | } |
| 206 | |
| 207 | /* should read message issued after connecting to data port: |
| 208 | * "200 Ready data connection" */ |
| 209 | res = timidity_ctl_command(NULL); |
| 210 | if (atoi(res) != 200) { |
| 211 | fprintf(stderr, "Can't connect timidity: %s\t(host=%s, port=%d)\n", res, timidity_host, data_port); |
| 212 | close_all(); |
| 213 | return -1; |
| 214 | } |
| 215 | |
| 216 | /* |
| 217 | * From seq.cpp |
| 218 | */ |
| 219 | if (getenv("SCUMMVM_MIDIPORT")) |
| 220 | _device_num = atoi(getenv("SCUMMVM_MIDIPORT")); |
| 221 | |
| 222 | return 0; |
| 223 | } |
| 224 | |
| 225 | void MidiDriver_TIMIDITY::close() { |
| 226 | teardown(); |
| 227 | |
| 228 | MidiDriver_MPU401::close(); |
| 229 | _isOpen = false; |
| 230 | } |
| 231 | |
| 232 | void MidiDriver_TIMIDITY::close_all() { |
| 233 | if (_control_fd >= 0) |
| 234 | ::close(_control_fd); |
| 235 | |
| 236 | if (_data_fd >= 0) |
| 237 | ::close(_data_fd); |
| 238 | |
| 239 | _control_fd = _data_fd = -1; |
| 240 | } |
| 241 | |
| 242 | void MidiDriver_TIMIDITY::teardown() { |
| 243 | char *res; |
| 244 | |
| 245 | /* teardown connection to server (see timidity-io.c) if it |
| 246 | * is initialized */ |
| 247 | if (_data_fd >= 0 && _control_fd >= 0) { |
| 248 | timidity_eot(); |
| 249 | timidity_sync(0); |
| 250 | |
| 251 | /* scroll through all "302 Data connection is (already) closed" |
| 252 | * messages till we reach something like "200 Bye" */ |
| 253 | do { |
| 254 | res = timidity_ctl_command("QUIT"); |
| 255 | } while(*res && atoi(res) && atoi(res) != 302); |
| 256 | } |
| 257 | |
| 258 | /* now close and nullify both filedescs */ |
| 259 | close_all(); |
| 260 | } |
| 261 | |
| 262 | in_addr_t MidiDriver_TIMIDITY::host_to_addr(const char* address) { |
| 263 | in_addr_t addr; |
| 264 | struct hostent *hp; |
| 265 | |
| 266 | /* first check if IP address is given (like 127.0.0.1)*/ |
| 267 | if ((addr = inet_addr(address)) != INADDR_NONE) |
| 268 | return addr; |
| 269 | |
| 270 | /* if not, try to resolve a hostname */ |
| 271 | if ((hp = gethostbyname(address)) == NULL) { |
| 272 | warning("TiMidity: unknown hostname: %s\n", address); |
| 273 | return INADDR_NONE; |
| 274 | } |
| 275 | |
| 276 | memcpy(&addr, hp->h_addr, (int)sizeof(in_addr_t) <= hp->h_length ? sizeof(in_addr_t) : hp->h_length); |
| 277 | |
| 278 | return addr; |
| 279 | } |
| 280 | |
| 281 | int MidiDriver_TIMIDITY::connect_to_server(const char* hostname, unsigned short tcp_port) { |
| 282 | int fd; |
| 283 | struct sockaddr_in in; |
| 284 | unsigned int addr; |
| 285 | |
| 286 | /* create socket */ |
| 287 | if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { |
| 288 | warning("TiMidity: socket(): %s", strerror(errno)); |
| 289 | return -1; |
| 290 | } |
| 291 | |
| 292 | /* connect */ |
| 293 | memset(&in, 0, sizeof(in)); |
| 294 | in.sin_family = AF_INET; |
| 295 | in.sin_port = htons(tcp_port); |
| 296 | addr = host_to_addr(hostname); |
| 297 | memcpy(&in.sin_addr, &addr, 4); |
| 298 | |
| 299 | if (connect(fd, (struct sockaddr *)&in, sizeof(in)) < 0) { |
| 300 | warning("TiMidity: connect(): %s", strerror(errno)); |
| 301 | return -1; |
| 302 | } |
| 303 | |
| 304 | return fd; |
| 305 | } |
| 306 | |
| 307 | char *MidiDriver_TIMIDITY::timidity_ctl_command(const char *fmt, ...) { |
| 308 | /* XXX: I don't like this static buffer!!! */ |
| 309 | static char buff[BUFSIZ]; |
| 310 | int status, len; |
| 311 | va_list ap; |
| 312 | |
| 313 | if (fmt != NULL) { |
| 314 | /* if argumends are present, write them to control connection */ |
| 315 | va_start(ap, fmt); |
| 316 | len = vsnprintf(buff, BUFSIZ-1, fmt, ap); /* leave one byte for \n */ |
| 317 | va_end(ap); |
| 318 | |
| 319 | /* add newline if needed */ |
| 320 | if(len > 0 && buff[len - 1] != '\n') |
| 321 | buff[len++] = '\n'; |
| 322 | |
| 323 | /* write command to control socket */ |
| 324 | write(_control_fd, buff, len); |
| 325 | } |
| 326 | |
| 327 | while(1) { |
| 328 | /* read reply */ |
| 329 | if (fdgets(buff, sizeof(buff)) <= 0) { |
| 330 | strcpy(buff, "Read error\n"); |
| 331 | break; |
| 332 | } |
| 333 | |
| 334 | /* report errors from server */ |
| 335 | status = atoi(buff); |
| 336 | if (400 <= status && status <= 499) { /* Error of data stream */ |
| 337 | warning("TiMidity: error from server: %s", buff); |
| 338 | continue; |
| 339 | } |
| 340 | break; |
| 341 | } |
| 342 | |
| 343 | return buff; |
| 344 | } |
| 345 | |
| 346 | void MidiDriver_TIMIDITY::timidity_meta_seq(int p1, int p2, int p3) { |
| 347 | /* see _CHN_COMMON from soundcard.h; this is simplified |
| 348 | * to just send seq to the server without any buffers, |
| 349 | * delays and extra functions/macros */ |
| 350 | u_char seqbuf[8]; |
| 351 | |
| 352 | seqbuf[0] = 0x92; |
| 353 | seqbuf[1] = 0; |
| 354 | seqbuf[2] = 0xff; |
| 355 | seqbuf[3] = 0x7f; |
| 356 | seqbuf[4] = p1; |
| 357 | seqbuf[5] = p2; |
| 358 | *(short *)&seqbuf[6] = p3; |
| 359 | |
| 360 | timidity_write_data(seqbuf, sizeof(seqbuf)); |
| 361 | } |
| 362 | |
| 363 | int MidiDriver_TIMIDITY::timidity_sync(int centsec) { |
| 364 | char *res; |
| 365 | int status; |
| 366 | unsigned long sleep_usec; |
| 367 | |
| 368 | timidity_meta_seq(0x02, 0x00, centsec); /* Wait playout */ |
| 369 | |
| 370 | /* Wait "301 Sync OK" */ |
| 371 | do { |
| 372 | res = timidity_ctl_command(NULL); |
| 373 | status = atoi(res); |
| 374 | |
| 375 | if(status != 301) |
| 376 | warning("TiMidity: error: SYNC: %s", res); |
| 377 | |
| 378 | } while(status && status != 301); |
| 379 | |
| 380 | if(status != 301) |
| 381 | return -1; /* error */ |
| 382 | |
| 383 | sleep_usec = (unsigned long)(atof(res + 4) * 1000000); |
| 384 | |
| 385 | if(sleep_usec > 0) |
| 386 | usleep(sleep_usec); |
| 387 | |
| 388 | return 0; |
| 389 | } |
| 390 | |
| 391 | int MidiDriver_TIMIDITY::timidity_eot(void) { |
| 392 | timidity_meta_seq(0x00, 0x00, 0); /* End of playing */ |
| 393 | return timidity_sync(0); |
| 394 | } |
| 395 | |
| 396 | void MidiDriver_TIMIDITY::timidity_write_data(const void *buf, size_t nbytes) { |
| 397 | /* nowhere to write... */ |
| 398 | if (_data_fd < 0) |
| 399 | return; |
| 400 | |
| 401 | /* write, and disable everything if write failed */ |
| 402 | /* TODO: add reconnect? */ |
| 403 | if (write(_data_fd, buf, nbytes) == -1) { |
| 404 | warning("TiMidity: DATA WRITE FAILED (%s), DISABLING MUSIC OUTPUT", strerror(errno)); |
| 405 | close_all(); |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | int MidiDriver_TIMIDITY::fdgets(char *buff, size_t buff_size) { |
| 410 | int n, len, count, size; |
| 411 | char *buff_endp = buff + buff_size - 1, *pbuff, *beg; |
| 412 | |
| 413 | len = 0; |
| 414 | count = _controlbuffer_count; |
| 415 | size = _controlbuffer_size; |
| 416 | pbuff = _controlbuffer; |
| 417 | beg = buff; |
| 418 | do { |
| 419 | if (count == size) { |
| 420 | if ((n = read(_control_fd, pbuff, BUFSIZ)) <= 0) { |
| 421 | *buff = '\0'; |
| 422 | if (n == 0) { |
| 423 | _controlbuffer_count = _controlbuffer_size = 0; |
| 424 | return buff - beg; |
| 425 | } |
| 426 | return -1; /* < 0 error */ |
| 427 | } |
| 428 | count = _controlbuffer_count = 0; |
| 429 | size = _controlbuffer_size = n; |
| 430 | } |
| 431 | *buff++ = pbuff[count++]; |
| 432 | } while(*(buff - 1) != '\n' && buff != buff_endp); |
| 433 | |
| 434 | *buff = '\0'; |
| 435 | _controlbuffer_count = count; |
| 436 | |
| 437 | return buff - beg; |
| 438 | } |
| 439 | |
| 440 | void MidiDriver_TIMIDITY::send(uint32 b) { |
| 441 | unsigned char buf[256]; |
| 442 | int position = 0; |
| 443 | |
| 444 | switch (b & 0xF0) { |
| 445 | case 0x80: |
| 446 | case 0x90: |
| 447 | case 0xA0: |
| 448 | case 0xB0: |
| 449 | case 0xE0: |
| 450 | buf[position++] = SEQ_MIDIPUTC; |
| 451 | buf[position++] = (unsigned char)b; |
| 452 | buf[position++] = _device_num; |
| 453 | buf[position++] = 0; |
| 454 | buf[position++] = SEQ_MIDIPUTC; |
| 455 | buf[position++] = (unsigned char)((b >> 8) & 0x7F); |
| 456 | buf[position++] = _device_num; |
| 457 | buf[position++] = 0; |
| 458 | buf[position++] = SEQ_MIDIPUTC; |
| 459 | buf[position++] = (unsigned char)((b >> 16) & 0x7F); |
| 460 | buf[position++] = _device_num; |
| 461 | buf[position++] = 0; |
| 462 | break; |
| 463 | case 0xC0: |
| 464 | case 0xD0: |
| 465 | buf[position++] = SEQ_MIDIPUTC; |
| 466 | buf[position++] = (unsigned char)b; |
| 467 | buf[position++] = _device_num; |
| 468 | buf[position++] = 0; |
| 469 | buf[position++] = SEQ_MIDIPUTC; |
| 470 | buf[position++] = (unsigned char)((b >> 8) & 0x7F); |
| 471 | buf[position++] = _device_num; |
| 472 | buf[position++] = 0; |
| 473 | break; |
| 474 | default: |
| 475 | warning("MidiDriver_TIMIDITY::send: unknown : %08x", (int)b); |
| 476 | break; |
| 477 | } |
| 478 | |
| 479 | timidity_write_data(buf, position); |
| 480 | } |
| 481 | |
| 482 | void MidiDriver_TIMIDITY::sysEx(const byte *msg, uint16 length) { |
| 483 | fprintf(stderr, "Timidity::sysEx\n"); |
| 484 | unsigned char buf[1024]; |
| 485 | int position = 0; |
| 486 | const byte *chr = msg; |
| 487 | |
| 488 | assert(length + 2 <= 256); |
| 489 | |
| 490 | buf[position++] = SEQ_MIDIPUTC; |
| 491 | buf[position++] = 0xF0; |
| 492 | buf[position++] = _device_num; |
| 493 | buf[position++] = 0; |
| 494 | for (; length; --length, ++chr) { |
| 495 | buf[position++] = SEQ_MIDIPUTC; |
| 496 | buf[position++] = (unsigned char) *chr & 0x7F; |
| 497 | buf[position++] = _device_num; |
| 498 | buf[position++] = 0; |
| 499 | } |
| 500 | buf[position++] = SEQ_MIDIPUTC; |
| 501 | buf[position++] = 0xF7; |
| 502 | buf[position++] = _device_num; |
| 503 | buf[position++] = 0; |
| 504 | |
| 505 | timidity_write_data(buf, position); |
| 506 | } |
| 507 | |
| 508 | MidiDriver *MidiDriver_TIMIDITY_create() { |
| 509 | return new MidiDriver_TIMIDITY(); |
| 510 | } |
| 511 | |
| 512 | #endif // defined (UNIX) |