diff options
Diffstat (limited to 'utils/adlmidi-2/midiplay.cc')
-rw-r--r-- | utils/adlmidi-2/midiplay.cc | 2010 |
1 files changed, 2010 insertions, 0 deletions
diff --git a/utils/adlmidi-2/midiplay.cc b/utils/adlmidi-2/midiplay.cc new file mode 100644 index 0000000..c57ab61 --- /dev/null +++ b/utils/adlmidi-2/midiplay.cc @@ -0,0 +1,2010 @@ +//#ifdef __MINGW32__ +//typedef struct vswprintf {} swprintf; +//#endif + +#include <vector> +#include <string> +#include <map> +#include <set> +#include <cstdlib> +#include <cstring> +#include <chrono> +#include <cstdarg> +#include <cmath> +#include <unistd.h> +#include <stdarg.h> +#include <cstdio> +#include <vector> // vector +#include <deque> // deque +#include <cmath> // exp, log, ceil + +#include <assert.h> + +#define SUPPORT_VIDEO_OUTPUT +#define SUPPORT_PUZZLE_GAME + +#ifdef __WIN32__ +# include <cctype> +# define WIN32_LEAN_AND_MEAN +# include <windows.h> +# include <mmsystem.h> +#endif + +#if defined(__WIN32__) || defined(__DJGPP__) +typedef unsigned char Uint8; +typedef unsigned short Uint16; +typedef unsigned Uint32; +#else +#include <SDL2/SDL.h> +class MutexType +{ + SDL_mutex *mut; +public: + MutexType() : mut(SDL_CreateMutex()) { } + ~MutexType() + { + SDL_DestroyMutex(mut); + } + void Lock() + { + SDL_mutexP(mut); + } + void Unlock() + { + SDL_mutexV(mut); + } +}; +#endif + +#ifdef __DJGPP__ +# include <conio.h> +# include <pc.h> +# include <dpmi.h> +# include <go32.h> +# include <sys/farptr.h> +# include <dos.h> +# define BIOStimer _farpeekl(_dos_ds, 0x46C) +static const unsigned NewTimerFreq = 209; +#elif !defined(__WIN32__) || defined(__CYGWIN__) +# include <termios.h> +# include <fcntl.h> +# include <sys/ioctl.h> +# include <csignal> +#endif + +#include <deque> +#include <algorithm> + +#include <signal.h> + + +#include "adlmidi.h" +#include "adlmidi_private.hpp" // For OPL3 and MIDIplay classes + +#include "fraction.h" + +#ifndef __DJGPP__ +#include "dbopl.h" + +#include "adldata.hh" + +static const unsigned long PCM_RATE = 48000; +static const unsigned MaxCards = 100; +static const unsigned MaxSamplesAtTime = 512; // 512=dbopl limitation +#else // DJGPP +static const unsigned MaxCards = 1; +static const unsigned OPLBase = 0x388; +#endif +static unsigned AdlBank = 0; +static unsigned NumFourOps = 7; +static unsigned NumCards = 2; +static bool AdlPercussionMode = false; +static bool ReverbIsOn = true; +static bool QuitFlag = false, FakeDOSshell = false; +static bool DoingInstrumentTesting = false; +static bool WritePCMfile = false; +static std::string PCMfilepath = "adlmidi.wav"; +static std::string VidFilepath = "adlmidi.mkv"; +static bool WriteVideoFile = false; +static unsigned WindowLines = 0; +static bool WritingToTTY; + +static unsigned WinHeight() +{ + unsigned result = + AdlPercussionMode + ? std::min(2u, NumCards) * 23 + : std::min(3u, NumCards) * 18; + + if(WindowLines) result = std::min(WindowLines - 1, result); + return result; +} + +#if (!defined(__WIN32__) || defined(__CYGWIN__)) && defined(TIOCGWINSZ) +extern "C" { + static void SigWinchHandler(int); +} +static void SigWinchHandler(int) +{ + struct winsize w; + if(ioctl(2, TIOCGWINSZ, &w) >= 0 || ioctl(1, TIOCGWINSZ, &w) >= 0 || ioctl(0, TIOCGWINSZ, &w) >= 0) + WindowLines = w.ws_row; +} +#else +static void SigWinchHandler(int) {} +#endif + +static void GuessInitialWindowHeight() +{ + auto s = std::getenv("LINES"); + if(s && std::atoi(s)) WindowLines = std::atoi(s); + SigWinchHandler(0); +} + +static class Input +{ + #ifdef __WIN32__ + void *inhandle; + #endif + #if (!defined(__WIN32__) || defined(__CYGWIN__)) && !defined(__DJGPP__) + struct termio back; + #endif +public: + Input() + { + #ifdef __WIN32__ + inhandle = GetStdHandle(STD_INPUT_HANDLE); + #endif + #if (!defined(__WIN32__) || defined(__CYGWIN__)) && !defined(__DJGPP__) + ioctl(0, TCGETA, &back); + struct termio term = back; + term.c_lflag &= ~(ICANON | ECHO); + term.c_cc[VMIN] = 0; // 0=no block, 1=do block + if(ioctl(0, TCSETA, &term) < 0) + fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK); + #endif + } + ~Input() + { + #if (!defined(__WIN32__) || defined(__CYGWIN__)) && !defined(__DJGPP__) + if(ioctl(0, TCSETA, &back) < 0) + fcntl(0, F_SETFL, fcntl(0, F_GETFL) & ~ O_NONBLOCK); + #endif + } + + char PeekInput() + { + #ifdef __DJGPP__ + if(kbhit()) + { + int c = getch(); + return c ? c : getch(); + } + #endif + #ifdef __WIN32__ + DWORD nread = 0; + INPUT_RECORD inbuf[1]; + while(PeekConsoleInput(inhandle, inbuf, sizeof(inbuf) / sizeof(*inbuf), &nread) && nread) + { + ReadConsoleInput(inhandle, inbuf, sizeof(inbuf) / sizeof(*inbuf), &nread); + if(inbuf[0].EventType == KEY_EVENT + && inbuf[0].Event.KeyEvent.bKeyDown) + { + char c = inbuf[0].Event.KeyEvent.uChar.AsciiChar; + unsigned s = inbuf[0].Event.KeyEvent.wVirtualScanCode; + if(c == 0) c = s; + return c; + } + } + #endif + #if (!defined(__WIN32__) || defined(__CYGWIN__)) && !defined(__DJGPP__) + char c = 0; + if(read(0, &c, 1) == 1) return c; + #endif + return '\0'; + } +} Input; + +#ifdef SUPPORT_PUZZLE_GAME +#include "puzzlegame.hpp" +#endif + +#ifdef SUPPORT_VIDEO_OUTPUT +class UIfontBase {}; +static unsigned UnicodeToASCIIapproximation(unsigned n) +{ + return n; +} +//#include "6x9.hpp" +#include "9x15.hpp" +#endif + +static const char MIDIsymbols[256 + 1] = + "PPPPPPhcckmvmxbd" // Ins 0-15 + "oooooahoGGGGGGGG" // Ins 16-31 + "BBBBBBBBVVVVVHHM" // Ins 32-47 + "SSSSOOOcTTTTTTTT" // Ins 48-63 + "XXXXTTTFFFFFFFFF" // Ins 64-79 + "LLLLLLLLpppppppp" // Ins 80-95 + "XXXXXXXXGGGGGTSS" // Ins 96-111 + "bbbbMMMcGXXXXXXX" // Ins 112-127 + "????????????????" // Prc 0-15 + "????????????????" // Prc 16-31 + "???DDshMhhhCCCbM" // Prc 32-47 + "CBDMMDDDMMDDDDDD" // Prc 48-63 + "DDDDDDDDDDDDDDDD" // Prc 64-79 + "DD??????????????" // Prc 80-95 + "????????????????" // Prc 96-111 + "????????????????"; // Prc 112-127 + +static class UserInterface +{ +public: + static constexpr unsigned NColumns = 1216 / 20; + #ifdef SUPPORT_VIDEO_OUTPUT + static constexpr unsigned VidWidth = 1216, VidHeight = 2160; + static constexpr unsigned FontWidth = 20, FontHeight = 45; + static constexpr unsigned TxWidth = (VidWidth / FontWidth), TxHeight = (VidHeight / FontHeight); + unsigned int PixelBuffer[VidWidth * VidHeight] = {0}; + unsigned short CharBuffer[TxWidth * TxHeight] = {0}; + bool DirtyCells[TxWidth * TxHeight] = {false}; + unsigned NDirty = 0; + #endif + #ifdef __WIN32__ + void *handle; + #endif + int x, y, color, txtline, maxy; + + // Text: + char background[NColumns][1 + 23 * MaxCards]; + unsigned char backgroundcolor[NColumns][1 + 23 * MaxCards]; + bool touched[1 + 23 * MaxCards] {false}; + // Notes: + char slots[NColumns][1 + 23 * MaxCards]; + unsigned char slotcolors[NColumns][1 + 23 * MaxCards]; + + bool cursor_visible; + char stderr_buffer[256]; +public: + UserInterface(): x(0), y(0), color(-1), txtline(0), + maxy(0), cursor_visible(true) + { + GuessInitialWindowHeight(); + #ifdef __WIN32__ + handle = GetStdHandle(STD_OUTPUT_HANDLE); + GotoXY(41, 13); + CONSOLE_SCREEN_BUFFER_INFO tmp; + GetConsoleScreenBufferInfo(handle, &tmp); + if(tmp.dwCursorPosition.X != 41) + { + // Console is not obeying controls! Probably cygwin xterm. + handle = 0; + } + else + { + WindowLines = tmp.dwSize.Y; + //COORD size = { NColumns, 23*NumCards+5 }; + //SetConsoleScreenBufferSize(handle,size); + } + #endif + #if (!defined(__WIN32__) || defined(__CYGWIN__)) && defined(TIOCGWINSZ) + std::signal(SIGWINCH, SigWinchHandler); + #endif + #ifdef __DJGPP__ + color = 7; + #endif + std::memset(slots, '.', sizeof(slots)); + std::memset(background, '.', sizeof(background)); + std::memset(backgroundcolor, 1, sizeof(backgroundcolor)); + setbuffer(stderr, stderr_buffer, sizeof(stderr_buffer)); + RawPrn("\r"); // Ensure cursor is at the x=0 we imagine it being + Print(0, 7, true, "Hit Ctrl-C to quit"); + } + void HideCursor() + { + if(!cursor_visible) return; + cursor_visible = false; + #ifdef __WIN32__ + if(handle) + { + const CONSOLE_CURSOR_INFO info = {100, false}; + SetConsoleCursorInfo(handle, &info); + if(!DoingInstrumentTesting) + CheckTetris(); + return; + } + #endif + if(!DoingInstrumentTesting) + CheckTetris(); + #ifdef __DJGPP__ + { + _setcursortype(_NOCURSOR); + return; + } + #endif + RawPrn("\33[?25l"); // hide cursor + } + void ShowCursor() + { + if(cursor_visible) return; + cursor_visible = true; + GotoXY(0, maxy); + Color(7); + #ifdef __WIN32__ + if(handle) + { + const CONSOLE_CURSOR_INFO info = {100, true}; + SetConsoleCursorInfo(handle, &info); + return; + } + #endif + #ifdef __DJGPP__ + { + _setcursortype(_NORMALCURSOR); + return; + } + #endif + RawPrn("\33[?25h"); // show cursor + std::fflush(stderr); + } + void VidPut(char c) + { + #ifndef SUPPORT_VIDEO_OUTPUT + c = c; + #else + unsigned clr = (unsigned)color, tx = (unsigned)x, ty = (unsigned)y; + unsigned cell_index = ty * TxWidth + tx; + if(cell_index < TxWidth * TxHeight) + { + unsigned short what = (unsigned short)(clr << 8) + (unsigned short)(unsigned(c) & 0xFF); + if(what != CharBuffer[cell_index]) + { + CharBuffer[cell_index] = what; + if(!DirtyCells[cell_index]) + { + DirtyCells[cell_index] = true; + ++NDirty; + } + } + } + #endif + } + #ifdef SUPPORT_VIDEO_OUTPUT + static unsigned VidTranslateColor(unsigned c) + { + static const unsigned colors[16] = + { + 0x000000, 0x00005F, 0x00AA00, 0x5F5FAF, + 0xAA0000, 0xAA00AA, 0x87875F, 0xAAAAAA, + 0x005F87, 0x5555FF, 0x55FF55, 0x55FFFF, + 0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF + }; + return colors[c % 16]; + } + void VidRenderCh(unsigned x, unsigned y, unsigned attr, const unsigned char *bitmap) + { + unsigned bg = VidTranslateColor(attr >> 4), fg = VidTranslateColor(attr); + for(unsigned line = 0; line < FontHeight; ++line) + { + unsigned int *pix = PixelBuffer + (y * FontHeight + line) * VidWidth + x * FontWidth; + + unsigned char bm = bitmap[line * 15 / FontHeight]; + for(unsigned w = 0; w < FontWidth; ++w) + { + int shift0 = 7 - w * 9 / FontWidth; + int shift1 = 7 - (w - 1) * 9 / FontWidth; + int shift2 = 7 - (w + 1) * 9 / FontWidth; + bool flag = (shift0 >= 0 && (bm & (1u << shift0))); + if(!flag && (attr & 8)) + { + flag = (shift1 >= 0 && (bm & (1u << shift1))) + || (shift2 >= 0 && (bm & (1u << shift2))); + } + pix[w] = flag ? fg : bg; + } + } + } + void VidRender() + { + static const font9x15 font; + if(NDirty) + { + #pragma omp parallel for schedule(static) + for(unsigned scan = 0; scan < TxWidth * TxHeight; ++scan) + if(DirtyCells[scan]) + { + --NDirty; + DirtyCells[scan] = false; + VidRenderCh(scan % TxWidth, scan / TxWidth, + CharBuffer[scan] >> 8, + font.GetBitmap() + 15 * font.GetIndex(CharBuffer[scan] & 0xFF)); + } + } + } + #endif + void PutC(char c) + { + #ifdef __WIN32__ + if(handle) WriteConsole(handle, &c, 1, 0, 0); + else + #endif + { + #ifdef __DJGPP__ + putch(c); + #else + std::fputc(c, stderr); + #endif + } + VidPut(c); + ++x; // One letter drawn. Update cursor position. + } + #ifdef __DJGPP__ +#define RawPrn cprintf + #else + static void RawPrn(const char *fmt, ...) __attribute__((format(printf, 1, 2))) + { + // Note: should essentially match PutC, except without updates to x + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + } + #endif + int Print(unsigned beginx, unsigned color, bool ln, const char *fmt, va_list ap) + { + char Line[1024]; + #ifndef __CYGWIN__ + int nchars = std::vsnprintf(Line, sizeof(Line), fmt, ap); + #else + int nchars = std::vsprintf(Line, fmt, ap); /* SECURITY: POSSIBLE BUFFER OVERFLOW (Cygwin) */ + #endif + //if(nchars == 0) return nchars; + + HideCursor(); + for(unsigned tx = beginx; tx < NColumns; ++tx) + { + int n = (int)(tx - beginx); // Index into Line[] + if(n < nchars && Line[n] != '\n') + { + GotoXY((int)tx, txtline); + Color(backgroundcolor[tx][txtline] = (unsigned char)(/*Line[n] == '.' ? 1 :*/ color)); + PutC(background[tx][txtline] = Line[n]); + } + else //if(background[tx][txtline]!='.' && slots[tx][txtline]=='.') + { + if(!ln) break; + GotoXY((int)tx, txtline); + Color(backgroundcolor[tx][txtline] = 1); + PutC(background[tx][txtline] = '.'); + } + } + std::fflush(stderr); + + if(ln) + { + txtline = (txtline + 1) % (int)WinHeight(); + } + + return nchars; + } + int Print(unsigned beginx, unsigned color, bool ln, const char *fmt, ...) __attribute__((format(printf, 5, 6))) + { + va_list ap; + va_start(ap, fmt); + int r = Print(beginx, color, ln, fmt, ap); + va_end(ap); + return r; + } + int PrintLn(const char *fmt, ...) __attribute__((format(printf, 2, 3))) + { + va_list ap; + va_start(ap, fmt); + int r = Print(2/*column*/, 8/*color*/, true/*line*/, fmt, ap); + va_end(ap); + return r; + } + void IllustrateNote(int adlchn, int note, int ins, int pressure, double bend) + { + HideCursor(); + #if 1 + int notex = 2 + (note + 55) % (NColumns - 3); + int limit = (int)WinHeight(), minline = 3; + + int notey = minline + adlchn; + notey %= std::max(1, limit - minline); + notey += minline; + notey %= limit; + + char illustrate_char = background[notex][notey]; + if(pressure > 0) + { + illustrate_char = MIDIsymbols[ins]; + if(bend < 0) illustrate_char = '<'; + if(bend > 0) illustrate_char = '>'; + } + else if(pressure < 0) + { + illustrate_char = '%'; + } + // Special exceptions for '.' (background slot) + // '&' (tetris edges) + Draw(notex, notey, + pressure != 0 + ? AllocateColor(ins) /* use note's color if active */ + : illustrate_char == '.' ? backgroundcolor[notex][notey] + : illustrate_char == '&' ? 1 + : 8, + illustrate_char); + #endif + std::fflush(stderr); + } + + void Draw(int notex, int notey, int color, char ch) + { + if(slots[notex][notey] != ch + || slotcolors[notex][notey] != color) + { + slots[notex][notey] = ch; + slotcolors[notex][notey] = color; + GotoXY(notex, notey); + Color(color); + PutC(ch); + + if(!touched[notey]) + { + touched[notey] = true; + + GotoXY(0, notey); + for(int tx = 0; tx < int(NColumns); ++tx) + { + if(slots[tx][notey] != '.') + { + Color(slotcolors[tx][notey]); + PutC(slots[tx][notey]); + } + else + { + Color(backgroundcolor[tx][notey]); + PutC(background[tx][notey]); + } + } + } + } + } + + void IllustrateVolumes(double left, double right) + { + const unsigned maxy = WinHeight(); + const unsigned white_threshold = maxy / 23; + const unsigned red_threshold = maxy * 4 / 23; + const unsigned yellow_threshold = maxy * 8 / 23; + + double amp[2] = {left * maxy, right * maxy}; + for(unsigned y = 2; y < maxy; ++y) + for(unsigned w = 0; w < 2; ++w) + { + char c = amp[w] > (maxy - 1) - y ? '|' : background[w][y + 1]; + Draw(w, y + 1, + c == '|' ? y < white_threshold ? 15 + : y < red_threshold ? 12 + : y < yellow_threshold ? 14 + : 10 : (c == '.' ? 1 : 8), + c); + } + std::fflush(stderr); + } + + // Move tty cursor to the indicated position. + // Movements will be done in relative terms + // to the current cursor position only. + void GotoXY(int newx, int newy) + { + // Record the maximum line count seen + if(newy > maxy) maxy = newy; + // Go down with '\n' (resets cursor at beginning of line) + while(newy > y) + { + std::fputc('\n', stderr); + y += 1; + x = 0; + } + #ifdef __WIN32__ + if(handle) + { + CONSOLE_SCREEN_BUFFER_INFO tmp; + GetConsoleScreenBufferInfo(handle, &tmp); + COORD tmp2 = { x = newx, tmp.dwCursorPosition.Y } ; + if(newy < y) + { + tmp2.Y -= (y - newy); + y = newy; + } + SetConsoleCursorPosition(handle, tmp2); + } + #endif + #ifdef __DJGPP__ + { + gotoxy(x = newx, wherey() - (y - newy)); + y = newy; + return; + } + #endif + // Go up with \33[A + if(newy < y) + { + RawPrn("\33[%dA", y - newy); + y = newy; + } + // Adjust X coordinate + if(newx != x) + { + // Use '\r' to go to column 0 + if(newx == 0) // || (newx<10 && std::abs(newx-x)>=10)) + { + std::fputc('\r', stderr); + x = 0; + } + // Go left with \33[D + if(newx < x) RawPrn("\33[%dD", x - newx); + // Go right with \33[C + if(newx > x) RawPrn("\33[%dC", newx - x); + x = newx; + } + } + // Set color (4-bit). Bits: 1=blue, 2=green, 4=red, 8=+intensity + void Color(int newcolor) + { + if(color != newcolor) + { + #ifdef __WIN32__ + if(handle) + SetConsoleTextAttribute(handle, newcolor); + else + #endif + #ifdef __DJGPP__ + textattr(newcolor); + if(0) + #endif + { + static const char map[8 + 1] = "04261537"; + RawPrn("\33[0;%s40;3%c", (newcolor & 8) ? "1;" : "", map[newcolor & 7]); + // If xterm-256color is used, try using improved colors: + // Translate 8 (dark gray) into #003366 (bluish dark cyan) + // Translate 1 (dark blue) into #000033 (darker blue) + if(newcolor == 8) RawPrn(";38;5;24;25"); + if(newcolor == 6) RawPrn(";38;5;101;25"); + if(newcolor == 3) RawPrn(";38;5;61;25"); + if(newcolor == 1) RawPrn(";38;5;17;25"); + RawPrn("m"); + } + color = newcolor; + } + } + // Choose a permanent color for given instrument + int AllocateColor(int ins) + { + static char ins_colors[256] = { 0 }, ins_color_counter = 0; + if(ins_colors[ins]) + return ins_colors[ins]; + if(ins & 0x80) + { + static const char shuffle[] = {2, 3, 4, 5, 6, 7}; + return ins_colors[ins] = shuffle[ins_color_counter++ % 6]; + } + else + { + static const char shuffle[] = {10, 11, 12, 13, 14, 15}; + return ins_colors[ins] = shuffle[ins_color_counter++ % 6]; + } + } + + bool DoCheckTetris() + { + #ifdef SUPPORT_PUZZLE_GAME + static ADLMIDI_PuzzleGame::TetrisAI player(2); + static ADLMIDI_PuzzleGame::TetrisAI computer(31); + + int a = player.GameLoop(); + int b = computer.GameLoop(); + + if(a >= 0 && b >= 0) + { + player.incoming += b; + computer.incoming += a; + } + return player.DelayOpinion() >= 0 + && computer.DelayOpinion() >= 0; + #else + return true; + #endif + } + + bool TetrisLaunched = false; + bool CheckTetris() + { + if(TetrisLaunched) return DoCheckTetris(); + return true; + } +} UI; + +#ifdef SUPPORT_PUZZLE_GAME +namespace ADLMIDI_PuzzleGame +{ + static void PutCell(int x, int y, unsigned cell) + { + static const unsigned char valid_attrs[] = {8, 6, 5, 3}; + unsigned char ch = cell, attr = cell >> 8; + int height = WinHeight();//std::min(NumCards*18, 50u); + y = std::max(0, int(std::min(height, 40) - 25 + y)); + if(ch == 0xDB) ch = '#'; + if(ch == 0xB0) ch = '*'; + if(attr != 1) attr = valid_attrs[attr % sizeof(valid_attrs)]; + + //bool diff = UI.background[x][y] != UI.slots[x][y]; + UI.backgroundcolor[x][y] = attr; + UI.background[x][y] = ch; + UI.GotoXY(x, y); + UI.Color(attr); + UI.PutC(ch); + //UI.Draw(x,y, attr, ch); + } +} +#endif + +#ifndef __DJGPP__ +struct Reverb /* This reverb implementation is based on Freeverb impl. in Sox */ +{ + float feedback, hf_damping, gain; + struct FilterArray + { + struct Filter + { + std::vector<float> Ptr; + size_t pos; + float Store; + void Create(size_t size) + { + Ptr.resize(size); + pos = 0; + Store = 0.f; + } + float Update(float a, float b) + { + Ptr[pos] = a; + if(!pos) pos = Ptr.size() - 1; + else --pos; + return b; + } + float ProcessComb(float input, const float feedback, const float hf_damping) + { + Store = Ptr[pos] + (Store - Ptr[pos]) * hf_damping; + return Update(input + feedback * Store, Ptr[pos]); + } + float ProcessAllPass(float input) + { + return Update(input + Ptr[pos] * .5f, Ptr[pos] - input); + } + } comb[8], allpass[4]; + void Create(double rate, double scale, double offset) + { + /* Filter delay lengths in samples (44100Hz sample-rate) */ + static const int comb_lengths[8] = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617}; + static const int allpass_lengths[4] = {225, 341, 441, 556}; + double r = rate * (1 / 44100.0); // Compensate for actual sample-rate + const int stereo_adjust = 12; + for(size_t i = 0; i < 8; ++i, offset = -offset) + comb[i].Create(scale * r * (comb_lengths[i] + stereo_adjust * offset) + .5); + for(size_t i = 0; i < 4; ++i, offset = -offset) + allpass[i].Create(r * (allpass_lengths[i] + stereo_adjust * offset) + .5); + } + void Process(size_t length, + const std::deque<float> &input, std::vector<float> &output, + const float feedback, const float hf_damping, const float gain) + { + for(size_t a = 0; a < length; ++a) + { + float out = 0, in = input[a]; + for(size_t i = 8; i-- > 0;) out += comb[i].ProcessComb(in, feedback, hf_damping); + for(size_t i = 4; i-- > 0;) out += allpass[i].ProcessAllPass(out); + output[a] = out * gain; + } + } + } chan[2]; + std::vector<float> out[2]; + std::deque<float> input_fifo; + + void Create(double sample_rate_Hz, + double wet_gain_dB, + double room_scale, double reverberance, double fhf_damping, /* 0..1 */ + double pre_delay_s, double stereo_depth, + size_t buffer_size) + { + size_t delay = pre_delay_s * sample_rate_Hz + .5; + double scale = room_scale * .9 + .1; + double depth = stereo_depth; + double a = -1 / std::log(1 - /**/.3 /**/); // Set minimum feedback + double b = 100 / (std::log(1 - /**/.98/**/) * a + 1); // Set maximum feedback + feedback = 1 - std::exp((reverberance * 100.0 - b) / (a * b)); + hf_damping = fhf_damping * .3 + .2; + gain = std::exp(wet_gain_dB * (std::log(10.0) * 0.05)) * .015; + input_fifo.insert(input_fifo.end(), delay, 0.f); + for(size_t i = 0; i <= std::ceil(depth); ++i) + { + chan[i].Create(sample_rate_Hz, scale, i * depth); + out[i].resize(buffer_size); + } + } + void Process(size_t length) + { + for(size_t i = 0; i < 2; ++i) + if(!out[i].empty()) + chan[i].Process(length, + input_fifo, + out[i], feedback, hf_damping, gain); + input_fifo.erase(input_fifo.begin(), input_fifo.begin() + length); + } +}; +static struct MyReverbData +{ + bool wetonly; + Reverb chan[2]; + + MyReverbData() : wetonly(false) + { + for(size_t i = 0; i < 2; ++i) + chan[i].Create(PCM_RATE, + 6.0, // wet_gain_dB (-10..10) + .7, // room_scale (0..1) + .6, // reverberance (0..1) + .8, // hf_damping (0..1) + .000, // pre_delay_s (0.. 0.5) + 1, // stereo_depth (0..1) + MaxSamplesAtTime); + } +} reverb_data; + +#ifdef __WIN32__ +namespace WindowsAudio +{ + static const unsigned BUFFER_COUNT = 16; + static const unsigned BUFFER_SIZE = 8192; + static HWAVEOUT hWaveOut; + static WAVEHDR headers[BUFFER_COUNT]; + static volatile unsigned buf_read = 0, buf_write = 0; + + static void CALLBACK Callback(HWAVEOUT, UINT msg, DWORD, DWORD, DWORD) + { + if(msg == WOM_DONE) + { + buf_read = (buf_read + 1) % BUFFER_COUNT; + } + } + static void Open(const int rate, const int channels, const int bits) + { + WAVEFORMATEX wformat; + MMRESULT result; + + //fill waveformatex + memset(&wformat, 0, sizeof(wformat)); + wformat.nChannels = channels; + wformat.nSamplesPerSec = rate; + wformat.wFormatTag = WAVE_FORMAT_PCM; + wformat.wBitsPerSample = bits; + wformat.nBlockAlign = wformat.nChannels * (wformat.wBitsPerSample >> 3); + wformat.nAvgBytesPerSec = wformat.nSamplesPerSec * wformat.nBlockAlign; + + //open sound device + //WAVE_MAPPER always points to the default wave device on the system + result = waveOutOpen + ( + &hWaveOut, WAVE_MAPPER, &wformat, + (DWORD_PTR)Callback, 0, CALLBACK_FUNCTION + ); + if(result == WAVERR_BADFORMAT) + { + fprintf(stderr, "ao_win32: format not supported\n"); + return; + } + if(result != MMSYSERR_NOERROR) + { + fprintf(stderr, "ao_win32: unable to open wave mapper device\n"); + return; + } + char *buffer = new char[BUFFER_COUNT * BUFFER_SIZE]; + std::memset(headers, 0, sizeof(headers)); + std::memset(buffer, 0, BUFFER_COUNT * BUFFER_SIZE); + for(unsigned a = 0; a < BUFFER_COUNT; ++a) + headers[a].lpData = buffer + a * BUFFER_SIZE; + } + static void Close() + { + waveOutReset(hWaveOut); + waveOutClose(hWaveOut); + } + static void Write(const unsigned char *Buf, unsigned len) + { + static std::vector<unsigned char> cache; + size_t cache_reduction = 0; + if(0 && len < BUFFER_SIZE && cache.size() + len <= BUFFER_SIZE) + { + cache.insert(cache.end(), Buf, Buf + len); + Buf = &cache[0]; + len = cache.size(); + if(len < BUFFER_SIZE / 2) + return; + cache_reduction = cache.size(); + } + + while(len > 0) + { + unsigned buf_next = (buf_write + 1) % BUFFER_COUNT; + WAVEHDR *Work = &headers[buf_write]; + while(buf_next == buf_read) + { + /*UI.Color(4); + UI.GotoXY(60,-5+5); fprintf(stderr, "waits\r"); UI.x=0; std::fflush(stderr); + UI.Color(4); + UI.GotoXY(60,-4+5); fprintf(stderr, "r%u w%u n%u\r",buf_read,buf_write,buf_next); UI.x=0; std::fflush(stderr); + */ + /* Wait until at least one of the buffers is free */ + Sleep(0); + /*UI.Color(2); + UI.GotoXY(60,-3+5); fprintf(stderr, "wait completed\r"); UI.x=0; std::fflush(stderr);*/ + } + + unsigned npending = (buf_write + BUFFER_COUNT - buf_read) % BUFFER_COUNT; + static unsigned counter = 0, lo = 0; + if(!counter-- || npending < lo) + { + lo = npending; + counter = 100; + } + + if(!DoingInstrumentTesting) + { + if(UI.maxy >= 5) + { + UI.Color(9); + UI.GotoXY(70, -5 + 6); + fprintf(stderr, "%3u bufs\r", (unsigned)npending); + UI.x = 0; + std::fflush(stderr); + UI.GotoXY(71, -4 + 6); + fprintf(stderr, "lo:%3u\r", lo); + UI.x = 0; + } + } + + //unprepare the header if it is prepared + if(Work->dwFlags & WHDR_PREPARED) waveOutUnprepareHeader(hWaveOut, Work, sizeof(WAVEHDR)); + unsigned x = BUFFER_SIZE; + if(x > len) x = len; + std::memcpy(Work->lpData, Buf, x); + Buf += x; + len -= x; + //prepare the header and write to device + Work->dwBufferLength = x; + { + int err = waveOutPrepareHeader(hWaveOut, Work, sizeof(WAVEHDR)); + if(err != MMSYSERR_NOERROR) fprintf(stderr, "waveOutPrepareHeader: %d\n", err); + } + { + int err = waveOutWrite(hWaveOut, Work, sizeof(WAVEHDR)); + if(err != MMSYSERR_NOERROR) fprintf(stderr, "waveOutWrite: %d\n", err); + } + buf_write = buf_next; + //if(npending>=BUFFER_COUNT-2) + // buf_read=(buf_read+1)%BUFFER_COUNT; // Simulate a read + } + if(cache_reduction) + cache.erase(cache.begin(), cache.begin() + cache_reduction); + } +} +#else +static std::deque<short> AudioBuffer; +static MutexType AudioBuffer_lock; +static void AdlAudioCallback(void *, Uint8 *stream, int len) +{ + SDL_LockAudio(); + short *target = (short *) stream; + AudioBuffer_lock.Lock(); + /*if(len != AudioBuffer.size()) + fprintf(stderr, "len=%d stereo samples, AudioBuffer has %u stereo samples", + len/4, (unsigned) AudioBuffer.size()/2);*/ + unsigned ate = len / 2; // number of shorts + if(ate > AudioBuffer.size()) ate = AudioBuffer.size(); + for(unsigned a = 0; a < ate; ++a) + target[a] = AudioBuffer[a]; + AudioBuffer.erase(AudioBuffer.begin(), AudioBuffer.begin() + ate); + //fprintf(stderr, " - remain %u\n", (unsigned) AudioBuffer.size()/2); + AudioBuffer_lock.Unlock(); + SDL_UnlockAudio(); +} +#endif // WIN32 + +struct FourChars +{ + char ret[4]; + + FourChars(const char *s) + { + for(unsigned c = 0; c < 4; ++c) ret[c] = s[c]; + } + FourChars(unsigned w) // Little-endian + { + for(unsigned c = 0; c < 4; ++c) ret[c] = (w >> (c * 8)) & 0xFF; + } +}; + + + +static void SendStereoAudio(unsigned long count, short *samples) +{ + if(count > MaxSamplesAtTime) + { + SendStereoAudio(MaxSamplesAtTime, samples); + SendStereoAudio(count - MaxSamplesAtTime, samples + MaxSamplesAtTime); + return; + } + #if 0 + if(count % 2 == 1) + { + // An uneven number of samples? To avoid complicating matters, + // just ignore the odd sample. + count -= 1; + samples += 1; + } + #endif + if(!count) return; + + // Attempt to filter out the DC component. However, avoid doing + // sudden changes to the offset, for it can be audible. + double average[2] = {0, 0}; + for(unsigned w = 0; w < 2; ++w) + for(unsigned long p = 0; p < count; ++p) + average[w] += samples[p * 2 + w]; + static float prev_avg_flt[2] = {0, 0}; + float average_flt[2] = + { + prev_avg_flt[0] = (float)((double(prev_avg_flt[0]) + average[0] * 0.04 / double(count)) / 1.04), + prev_avg_flt[1] = (float)((double(prev_avg_flt[1]) + average[1] * 0.04 / double(count)) / 1.04) + }; + + // Figure out the amplitude of both channels + if(!DoingInstrumentTesting) + { + static unsigned amplitude_display_counter = 0; + if(!amplitude_display_counter--) + { + amplitude_display_counter = (PCM_RATE / count) / 24; + double amp[2] = {0, 0}; + for(unsigned w = 0; w < 2; ++w) + { + average[w] /= double(count); + for(unsigned long p = 0; p < count; ++p) + amp[w] += std::fabs(samples[p * 2 + w] - average[w]); + amp[w] /= double(count); + // Turn into logarithmic scale + const double dB = std::log(amp[w] < 1 ? 1 : amp[w]) * 4.328085123; + const double maxdB = 3 * 16; // = 3 * log2(65536) + amp[w] = dB / maxdB; + } + UI.IllustrateVolumes(amp[0], amp[1]); + } + } + + //static unsigned counter = 0; if(++counter < 8000) return; + + #if defined(__WIN32__) && 0 + // Cheat on dosbox recording: easier on the cpu load. + { + count *= 2; + std::vector<short> AudioBuffer(count); + for(unsigned long p = 0; p < count; ++p) + AudioBuffer[p] = samples[p]; + WindowsAudio::Write((const unsigned char *) &AudioBuffer[0], count * 2); + return; + } + #endif + + #ifdef __WIN32__ + std::vector<short> AudioBuffer(count * 2); + const size_t pos = 0; + #else + AudioBuffer_lock.Lock(); + size_t pos = AudioBuffer.size(); + AudioBuffer.resize(pos + count * 2); + #endif + + if(ReverbIsOn) + { + // Convert input to float format + std::vector<float> dry[2]; + for(unsigned w = 0; w < 2; ++w) + { + dry[w].resize(count); + float a = average_flt[w]; + for(unsigned long p = 0; p < count; ++p) + { + int s = samples[p * 2 + w]; + dry[w][p] = (s - a) * double(0.3 / 32768.0); + } + // ^ Note: ftree-vectorize causes an error in this loop on g++-4.4.5 + reverb_data.chan[w].input_fifo.insert( + reverb_data.chan[w].input_fifo.end(), + dry[w].begin(), dry[w].end()); + } + // Reverbify it + for(unsigned w = 0; w < 2; ++w) + reverb_data.chan[w].Process(count); + + // Convert to signed 16-bit int format and put to playback queue + for(unsigned long p = 0; p < count; ++p) + for(unsigned w = 0; w < 2; ++w) + { + float out = ((1 - reverb_data.wetonly) * dry[w][p] + + .5 * (reverb_data.chan[0].out[w][p] + + reverb_data.chan[1].out[w][p])) * 32768.0f + + average_flt[w]; + AudioBuffer[pos + p * 2 + w] = + out < -32768.f ? -32768 : + out > 32767.f ? 32767 : out; + } + } + else + { + for(unsigned long p = 0; p < count; ++p) + for(unsigned w = 0; w < 2; ++w) + { +// float out = ((1 - reverb_data.wetonly) * dry[w][p] + +// .5 * (reverb_data.chan[0].out[w][p] +// + reverb_data.chan[1].out[w][p])) * 32768.0f +// + average_flt[w]; + AudioBuffer[pos + p * 2 + w] = samples[p * 2 + w]; + } + } + + + if(WritePCMfile) + { + /* HACK: Cheat on DOSBox recording: Record audio separately on Windows. */ + static FILE *fp = nullptr; + if(!fp) + { + fp = PCMfilepath == "-" ? stdout + : fopen(PCMfilepath.c_str(), "wb"); + if(fp) + { + FourChars Bufs[] = + { + "RIFF", (0x24u), // RIFF type, file length - 8 + "WAVE", // WAVE file + "fmt ", (0x10u), // fmt subchunk, which is 16 bytes: + "\1\0\2\0", // PCM (1) & stereo (2) + (48000u), // sampling rate + (48000u * 2 * 2), // byte rate + "\2\0\20\0", // block align & bits per sample + "data", (0x00u) // data subchunk, which is so far 0 bytes. + }; + for(unsigned c = 0; c < sizeof(Bufs) / sizeof(*Bufs); ++c) + std::fwrite(Bufs[c].ret, 1, 4, fp); + } + } + + // Using a loop, because our data type is a deque, and + // the data might not be contiguously stored in memory. + for(unsigned long p = 0; p < 2 * count; ++p) + std::fwrite(&AudioBuffer[pos + p], 1, 2, fp); + + /* Update the WAV header */ + if(true) + { + long pos = std::ftell(fp); + if(pos != -1) + { + long datasize = pos - 0x2C; + if(std::fseek(fp, 4, SEEK_SET) == 0) // Patch the RIFF length + std::fwrite(FourChars(0x24u + datasize).ret, 1, 4, fp); + if(std::fseek(fp, 40, SEEK_SET) == 0) // Patch the data length + std::fwrite(FourChars(datasize).ret, 1, 4, fp); + std::fseek(fp, pos, SEEK_SET); + } + } + + std::fflush(fp); + + //if(std::ftell(fp) >= 48000*4*10*60) + // raise(SIGINT); + } + + if(WriteVideoFile) + { + static constexpr unsigned framerate = 15; + static FILE *fp = nullptr; + static unsigned long samples_carry = 0; + + if(!fp) + { + std::string cmdline = + "ffmpeg -f rawvideo" + " -pixel_format bgra " + " -video_size " + std::to_string(UI.VidWidth) + "x" + std::to_string(UI.VidHeight) + + " -framerate " + std::to_string(framerate) + + " -i -" + " -c:v h264" + " -aspect " + std::to_string(UI.VidWidth) + "/" + std::to_string(UI.VidHeight) + + " -pix_fmt yuv420p" + " -preset superfast -partitions all -refs 2 -tune animation -y '" + VidFilepath + "'"; // FIXME: escape filename + cmdline += " >/dev/null 2>/dev/null"; + fp = popen(cmdline.c_str(), "w"); + } + if(fp) + { + samples_carry += count; + while(samples_carry >= PCM_RATE / framerate) + { + UI.VidRender(); + + const unsigned char *source = (const unsigned char *)&UI.PixelBuffer; + std::size_t bytes_remain = sizeof(UI.PixelBuffer); + while(bytes_remain) + { + int r = std::fwrite(source, 1, bytes_remain, fp); + if(r == 0) break; + bytes_remain -= r; + source += r; + } + samples_carry -= PCM_RATE / framerate; + } + } + } + #ifndef __WIN32__ + AudioBuffer_lock.Unlock(); + #else + if(!WritePCMfile) + WindowsAudio::Write((const unsigned char *) &AudioBuffer[0], 2 * AudioBuffer.size()); + #endif +} +#endif /* not DJGPP */ + +/* + * THIS CLASS USES !!!ADL PRIVATE!!! + */ +class Tester +{ + unsigned cur_gm; + unsigned ins_idx; + std::vector<unsigned> adl_ins_list; + OPL3 &opl;// !!!ADL PRIVATE!!! +public: + Tester(OPL3 &o) // !!!ADL PRIVATE!!! + : opl(o) + { + cur_gm = 0; + ins_idx = 0; + } + ~Tester() + { + } + + // Find list of adlib instruments that supposedly implement this GM + void FindAdlList() + { + const unsigned NumBanks = (unsigned)adl_getBanksCount(); + std::set<unsigned> adl_ins_set; + for(unsigned bankno = 0; bankno < NumBanks; ++bankno) + adl_ins_set.insert(banks[bankno][cur_gm]); + adl_ins_list.assign(adl_ins_set.begin(), adl_ins_set.end()); + ins_idx = 0; + NextAdl(0); + opl.Silence(); + } + + + void Touch(unsigned c, unsigned volume) // Volume maxes at 127*127*127 + { + if(opl.LogarithmicVolumes)// !!!ADL PRIVATE!!! + opl.Touch_Real(c, volume * 127 / (127 * 127 * 127) / 2);// !!!ADL PRIVATE!!! + else + { + // The formula below: SOLVE(V=127^3 * 2^( (A-63.49999) / 8), A) + opl.Touch_Real(c, volume > 8725 ? static_cast<unsigned int>(std::log(volume) * 11.541561 + (0.5 - 104.22845)) : 0);// !!!ADL PRIVATE!!! + // The incorrect formula below: SOLVE(V=127^3 * (2^(A/63)-1), A) + //Touch_Real(c, volume>11210 ? 91.61112 * std::log(4.8819E-7*volume + 1.0)+0.5 : 0); + } + } + + void DoNote(int note) + { + if(adl_ins_list.empty()) FindAdlList(); + const unsigned meta = adl_ins_list[ins_idx]; + const adlinsdata &ains = opl.GetAdlMetaIns(meta);// !!!ADL PRIVATE!!! + + int tone = (cur_gm & 128) ? (cur_gm & 127) : (note + 50); + if(ains.tone) + { + if(ains.tone < 20) + tone += ains.tone; + else if(ains.tone < 128) + tone = ains.tone; + else + tone -= ains.tone - 128; + } + double hertz = 172.00093 * std::exp(0.057762265 * (tone + 0.0)); + int i[2] = { ains.adlno1, ains.adlno2 }; + int adlchannel[2] = { 0, 3 }; + if(i[0] == i[1]) + { + adlchannel[1] = -1; + adlchannel[0] = 6; // single-op + std::printf("noteon at %d(%d) for %g Hz\n", + adlchannel[0], i[0], hertz); + } + else + { + std::printf("noteon at %d(%d) and %d(%d) for %g Hz\n", + adlchannel[0], i[0], adlchannel[1], i[1], hertz); + } + + opl.NoteOff(0); + opl.NoteOff(3); + opl.NoteOff(6); + for(unsigned c = 0; c < 2; ++c) + { + if(adlchannel[c] < 0) continue; + opl.Patch(adlchannel[c], i[c]); + opl.Touch_Real(adlchannel[c], 127 * 127 * 100); + opl.Pan(adlchannel[c], 0x30); + opl.NoteOn(adlchannel[c], hertz); + } + } + + void NextGM(int offset) + { + cur_gm = (cur_gm + 256 + offset) & 0xFF; + FindAdlList(); + } + + void NextAdl(int offset) + { + if(adl_ins_list.empty()) FindAdlList(); + const unsigned NumBanks = (unsigned)adl_getBanksCount(); + ins_idx = (ins_idx + adl_ins_list.size() + offset) % adl_ins_list.size(); + + UI.Color(15); + std::fflush(stderr); + std::printf("SELECTED G%c%d\t%s\n", + cur_gm < 128 ? 'M' : 'P', cur_gm < 128 ? cur_gm + 1 : cur_gm - 128, + "<-> select GM, ^v select ins, qwe play note"); + std::fflush(stdout); + UI.Color(7); + std::fflush(stderr); + for(unsigned a = 0; a < adl_ins_list.size(); ++a) + { + const unsigned i = adl_ins_list[a]; + const adlinsdata &ains = opl.GetAdlMetaIns(i); + + char ToneIndication[8] = " "; + if(ains.tone) + { + if(ains.tone < 20) + sprintf(ToneIndication, "+%-2d", ains.tone); + else if(ains.tone < 128) + sprintf(ToneIndication, "=%-2d", ains.tone); + else + sprintf(ToneIndication, "-%-2d", ains.tone - 128); + } + std::printf("%s%s%s%u\t", + ToneIndication, + ains.adlno1 != ains.adlno2 ? "[2]" : " ", + (ins_idx == a) ? "->" : "\t", + i + ); + + for(unsigned bankno = 0; bankno < NumBanks; ++bankno) + if(banks[bankno][cur_gm] == i) + std::printf(" %u", bankno); + + std::printf("\n"); + } + } + + void HandleInputChar(char ch) + { + static const char notes[] = "zsxdcvgbhnjmq2w3er5t6y7ui9o0p"; + // c'd'ef'g'a'bC'D'EF'G'A'Bc'd'e + switch(ch) + { + case '/': + case 'H': + case 'A': + NextAdl(-1); + break; + case '*': + case 'P': + case 'B': + NextAdl(+1); + break; + case '-': + case 'K': + case 'D': + NextGM(-1); + break; + case '+': + case 'M': + case 'C': + NextGM(+1); + break; + case 3: + #if !((!defined(__WIN32__) || defined(__CYGWIN__)) && !defined(__DJGPP__)) + case 27: + #endif + QuitFlag = true; + break; + default: + const char *p = strchr(notes, ch); + if(p && *p) DoNote((p - notes) - 12); + } + } + + double Tick(double /*eat_delay*/, double /*mindelay*/) + { + HandleInputChar(Input.PeekInput()); + //return eat_delay; + return 0.1; + } +}; + +static void TidyupAndExit(int) +{ + UI.ShowCursor(); + UI.Color(7); + std::fflush(stderr); + signal(SIGINT, SIG_DFL); + raise(SIGINT); +} + +#ifdef __WIN32__ +/* Parse a command line buffer into arguments */ +static void UnEscapeQuotes(char *arg) +{ + for(char *last = 0; *arg != '\0'; last = arg++) + if(*arg == '"' && *last == '\\') + { + char *c_last = last; + for(char *c_curr = arg; *c_curr; ++c_curr) + { + *c_last = *c_curr; + c_last = c_curr; + } + *c_last = '\0'; + } +} +static int ParseCommandLine(char *cmdline, char **argv) +{ + char *bufp, *lastp = NULL; + int argc = 0, last_argc = 0; + for(bufp = cmdline; *bufp;) + { + /* Skip leading whitespace */ + while(std::isspace(*bufp)) ++bufp; + /* Skip over argument */ + if(*bufp == '"') + { + ++bufp; + if(*bufp) + { + if(argv) argv[argc] = bufp; + ++argc; + } + /* Skip over word */ + while(*bufp && (*bufp != '"' || *lastp == '\\')) + { + lastp = bufp; + ++bufp; + } + } + else + { + if(*bufp) + { + if(argv) argv[argc] = bufp; + ++argc; + } + /* Skip over word */ + while(*bufp && ! std::isspace(*bufp)) ++bufp; + } + if(*bufp) + { + if(argv) *bufp = '\0'; + ++bufp; + } + /* Strip out \ from \" sequences */ + if(argv && last_argc != argc) UnEscapeQuotes(argv[last_argc]); + last_argc = argc; + } + if(argv) argv[argc] = 0; + return(argc); +} + +int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int sw) +{ + extern int main(int, char **); + char *cmdline = GetCommandLine(); + int argc = ParseCommandLine(cmdline, NULL); + char **argv = new char *[argc + 1]; + ParseCommandLine(cmdline, argv); +#else +#undef main + + + +static void adlEventHook(void *ui, ADL_UInt8 type, ADL_UInt8 subtype, ADL_UInt8 /*channel*/, ADL_UInt8 *data, size_t len) +{ + UserInterface *mUI = (UserInterface *)ui; + + if(type == 0xF7 || type == 0xF0) // Ignore SysEx + { + mUI->PrintLn("SysEx %02X: %u bytes", type, (unsigned)len/*, data.c_str()*/); + return; + } + + if(type == 0xFF) + { + std::string eData((const char *)data, len); + if(subtype >= 1 && subtype <= 6) + mUI->PrintLn("Meta %d: %s", subtype, eData.c_str()); + } +} + +static void adlNoteHook(void *userdata, int adlchn, int note, int ins, int pressure, double bend) +{ + UserInterface *mUI = (UserInterface *)userdata; + mUI->IllustrateNote(adlchn, note, ins, pressure, bend); +} + +static void adlDebugMsgHook(void *userdata, const char *fmt, ...) +{ + UserInterface *mUI = (UserInterface *)userdata; + va_list ap; + va_start(ap, fmt); + /*int r = */ mUI->Print(2/*column*/, 8/*color*/, true/*line*/, fmt, ap); + va_end(ap); + //return r; +} + +int main(int argc, char **argv) +{ +#endif + // How long is SDL buffer, in seconds? + // The smaller the value, the more often AdlAudioCallBack() + // is called. + const double AudioBufferLength = 0.045; + // How much do WE buffer, in seconds? The smaller the value, + // the more prone to sound chopping we are. + const double OurHeadRoomLength = 0.1; + // The lag between visual content and audio content equals + // the sum of these two buffers. + + WritingToTTY = isatty(STDOUT_FILENO); + if(WritingToTTY) + { + UI.Print(0, 15, true, + #ifdef __DJGPP__ + "ADLMIDI_A: MIDI player for OPL3 hardware" + #else + "ADLMIDI: MIDI (etc.) player with OPL3 emulation" + #endif + ); + } + if(WritingToTTY) + { + UI.Print(0, 3, true, "(C) -- http://iki.fi/bisqwit/source/adlmidi.html"); + } + UI.Color(7); + std::fflush(stderr); + + signal(SIGINT, TidyupAndExit); + + if(argc < 2 || std::string(argv[1]) == "--help" || std::string(argv[1]) == "-h") + { + UI.Color(7); + std::fflush(stderr); + std::printf( + "Usage: adlmidi <midifilename> [ <options> ] [ <banknumber> [ <numcards> [ <numfourops>] ] ]\n" + " adlmidi <midifilename> -1 To enter instrument tester\n" + " -p Enables adlib percussion instrument mode (use with CMF files)\n" + " -t Enables tremolo amplification mode\n" + " -v Enables vibrato amplification mode\n" + " -s Enables scaling of modulator volumes\n" + " -nl Quit without looping\n" + " -nr Disables the reverb effect\n" + " -w [<filename>] Write WAV file rather than playing\n" + " -d [<filename>] Write video file using ffmpeg\n" + ); + int banksCount = adl_getBanksCount(); + const char *const *bankNames = adl_getBankNames(); + for(int a = 0; a < banksCount; ++a) + std::printf("%10s%2u = %s\n", + a ? "" : "Banks:", + a, + bankNames[a]); + std::printf( + " Use banks 2-5 to play Descent \"q\" soundtracks.\n" + " Look up the relevant bank number from descent.sng.\n" + "\n" + " <numfourops> can be used to specify the number\n" + " of four-op channels to use. Each four-op channel eats\n" + " the room of two regular channels. Use as many as required.\n" + " The Doom & Hexen sets require one or two, while\n" + " Miles four-op set requires the maximum of numcards*6.\n" + "\n" + " When playing Creative Music Files (CMF), try the\n" + " -p and -v options if it sounds wrong otherwise.\n" + "\n" + ); + return 0; + } + + std::srand((unsigned int)std::time(0)); + + ADL_MIDIPlayer *myDevice; + myDevice = adl_init(PCM_RATE); + + // Set hooks + adl_setNoteHook(myDevice, adlNoteHook, (void *)&UI); + adl_setRawEventHook(myDevice, adlEventHook, (void *)&UI); + adl_setDebugMessageHook(myDevice, adlDebugMsgHook, (void *)&UI); + + int loopEnabled = 1; + + while(argc > 2) + { + bool had_option = false; + + if(!std::strcmp("-p", argv[2])) + adl_setPercMode(myDevice, 1); + else if(!std::strcmp("-v", argv[2])) + adl_setHVibrato(myDevice, 1); + else if(!std::strcmp("-t", argv[2])) + adl_setHTremolo(myDevice, 1); + else if(!std::strcmp("-nl", argv[2])) + loopEnabled = 0; + else if(!std::strcmp("-w", argv[2])) + { + loopEnabled = 0; + WritePCMfile = true; + if(argc > 3 && argv[3][0] != '\0' && (argv[3][0] != '-' || argv[3][1] == '\0')) + { + // Allow the option argument if + // - it's not empty, and... + // - it does not begin with "-" or it is "-" + // - it is not a positive integer + char *endptr = 0; + if(std::strtol(argv[3], &endptr, 10) < 0 || (endptr && *endptr)) + { + PCMfilepath = argv[3]; + had_option = true; + } + } + } + else if(!std::strcmp("-d", argv[2])) + { + loopEnabled = 0; + WriteVideoFile = true; + if(argc > 3 && argv[3][0] != '\0' && (argv[3][0] != '-' || argv[3][1] == '\0')) + { + char *endptr = 0; + if(std::strtol(argv[3], &endptr, 10) < 0 || (endptr && *endptr)) + { + VidFilepath = argv[3]; + had_option = true; + } + } + } + else if(!std::strcmp("-s", argv[2])) + adl_setScaleModulators(myDevice, 1); + else if(!std::strcmp("-nr", argv[2])) + ReverbIsOn = false; + else break; + + std::copy(argv + (had_option ? 4 : 3), argv + argc, + argv + 2); + argc -= (had_option ? 2 : 1); + } + + adl_setLoopEnabled(myDevice, loopEnabled); + + #ifndef __DJGPP__ + + #ifndef __WIN32__ + static SDL_AudioSpec spec, obtained; + spec.freq = PCM_RATE; + spec.format = AUDIO_S16SYS; + spec.channels = 2; + spec.samples = (Uint16)(spec.freq * AudioBufferLength); + spec.callback = AdlAudioCallback; + if(!WritePCMfile) + { + // Set up SDL + if(SDL_OpenAudio(&spec, &obtained) < 0) + { + std::fprintf(stderr, "Couldn't open audio: %s\n", SDL_GetError()); + //return 1; + } + if(spec.samples != obtained.samples) + std::fprintf(stderr, "Wanted (samples=%u,rate=%u,channels=%u); obtained (samples=%u,rate=%u,channels=%u)\n", + spec.samples, spec.freq, spec.channels, + obtained.samples, obtained.freq, obtained.channels); + } + #endif + + #endif /* not DJGPP */ + + if(argc >= 3) + { + const unsigned NumBanks = (unsigned)adl_getBanksCount(); + int bankno = std::atoi(argv[2]); + if(bankno == -1) + { + bankno = 0; + DoingInstrumentTesting = true; + } + AdlBank = (unsigned)bankno; + if(AdlBank >= NumBanks) + { + std::fprintf(stderr, "bank number may only be 0..%u.\n", NumBanks - 1); + UI.ShowCursor(); + return 0; + } + if(WritingToTTY) + UI.PrintLn("FM instrument bank %u selected.", AdlBank); + adl_setBank(myDevice, bankno); + } + + unsigned n_fourop[2] = {0, 0}, n_total[2] = {0, 0}; + for(unsigned a = 0; a < 256; ++a) + { + unsigned insno = banks[AdlBank][a]; + if(insno == 198) continue; + ++n_total[a / 128]; + if(adlins[insno].adlno1 != adlins[insno].adlno2) + ++n_fourop[a / 128]; + } + if(WritingToTTY) + { + UI.PrintLn("This bank has %u/%u four-op melodic instruments", n_fourop[0], n_total[0]); + UI.PrintLn(" and %u/%u percussive ones.", n_fourop[1], n_total[1]); + } + + if(argc >= 4) + { + NumCards = (unsigned)std::atoi(argv[3]); + if(NumCards < 1 || NumCards > MaxCards) + { + std::fprintf(stderr, "number of cards may only be 1..%u.\n", MaxCards); + UI.ShowCursor(); + return 0; + } + adl_setNumCards(myDevice, (int)NumCards); + } + if(argc >= 5) + { + NumFourOps = (unsigned)std::atoi(argv[4]); + if(NumFourOps > 6 * NumCards) + { + std::fprintf(stderr, "number of four-op channels may only be 0..%u when %u OPL3 cards are used.\n", + 6 * NumCards, NumCards); + UI.ShowCursor(); + return 0; + } + adl_setNumFourOpsChn(myDevice, (int)NumFourOps); + } + else + NumFourOps = + DoingInstrumentTesting ? 2 + : (n_fourop[0] >= n_total[0] * 7 / 8) ? NumCards * 6 + : (n_fourop[0] < n_total[0] * 1 / 8) ? 0 + : (NumCards == 1 ? 1 : NumCards * 4); + if(WritingToTTY) + { + UI.PrintLn("Simulating %u OPL3 cards for a total of %u operators.", NumCards, NumCards * 36); + std::string s = "Operator set-up: " + + std::to_string(NumFourOps) + + " 4op, " + + std::to_string((AdlPercussionMode ? 15 : 18) * NumCards - NumFourOps * 2) + + " 2op"; + if(AdlPercussionMode) + s += ", " + std::to_string(NumCards * 5) + " percussion"; + s += " channels"; + UI.PrintLn("%s", s.c_str()); + } + + UI.Color(7); + if(adl_openFile(myDevice, argv[1]) != 0) + { + UI.ShowCursor(); + return 2; + } + + if(n_fourop[0] >= n_total[0] * 15 / 16 && NumFourOps == 0) + { + std::fprintf(stderr, + "ERROR: You have selected a bank that consists almost exclusively of four-op patches.\n" + " The results (silence + much cpu load) would be probably\n" + " not what you want, therefore ignoring the request.\n"); + return 0; + } + + #ifdef __DJGPP__ + + unsigned TimerPeriod = 0x1234DDul / NewTimerFreq; + //disable(); + outportb(0x43, 0x34); + outportb(0x40, TimerPeriod & 0xFF); + outportb(0x40, TimerPeriod >> 8); + //enable(); + unsigned long BIOStimer_begin = BIOStimer; + + #else + + const double mindelay = 1 / (double)PCM_RATE; + const double maxdelay = MaxSamplesAtTime / (double)PCM_RATE; + + #ifdef __WIN32 + WindowsAudio::Open(PCM_RATE, 2, 16); + #else + SDL_PauseAudio(0); + #endif + + #endif /* djgpp */ + + // !!!ADL PRIVATE!!! + Tester InstrumentTester(((MIDIplay *)myDevice->adl_midiPlayer)->opl); + + //static std::vector<int> sample_buf; + double delay = 0.0; + //sample_buf.resize(1024); + short buff[1024]; + + UI.TetrisLaunched = true; + while(!QuitFlag) + { + #ifndef __DJGPP__ + const double eat_delay = delay < maxdelay ? delay : maxdelay; + delay -= eat_delay; + size_t got = (size_t)adl_play(myDevice, 1024, buff); + if(got <= 0) + break; + /* Process it */ + SendStereoAudio(got / 2, buff); + + //static double carry = 0.0; + //carry += PCM_RATE * eat_delay; + //const unsigned long n_samples = (unsigned) carry; + //carry -= n_samples; + + //if(SkipForward > 0) + // SkipForward -= 1; + //else + //{ + // if(NumCards == 1) + // { + // player.opl.cards[0].Generate(0, SendStereoAudio, n_samples); + // } + // else if(n_samples > 0) + // { + // /* Mix together the audio from different cards */ + // static std::vector<int> sample_buf; + // sample_buf.clear(); + // sample_buf.resize(n_samples*2); + // struct Mix + // { + // static void AddStereoAudio(unsigned long count, int* samples) + // { + // for(unsigned long a=0; a<count*2; ++a) + // sample_buf[a] += samples[a]; + // } + // }; + // for(unsigned card = 0; card < NumCards; ++card) + // { + // player.opl.cards[card].Generate( + // 0, + // Mix::AddStereoAudio, + // n_samples); + // } + // /* Process it */ + // SendStereoAudio(n_samples, &sample_buf[0]); + // } + + //fprintf(stderr, "Enter: %u (%.2f ms)\n", (unsigned)AudioBuffer.size(), + // AudioBuffer.size() * .5e3 / obtained.freq); + #ifndef __WIN32__ + const SDL_AudioSpec &spec_ = (WritePCMfile ? spec : obtained); + for(unsigned grant = 0; AudioBuffer.size() > spec_.samples + (spec_.freq * 2) * OurHeadRoomLength; ++grant) + { + if(!WritePCMfile) + { + if(UI.CheckTetris() || grant % 4 == 0) + { + SDL_Delay(1); // std::min(10.0, 1e3 * eat_delay) ); + } + } + else + { + for(unsigned n = 0; n < 128; ++n) UI.CheckTetris(); + AudioBuffer_lock.Lock(); + AudioBuffer.clear(); + AudioBuffer_lock.Unlock(); + } + } + #else + //Sleep(1e3 * eat_delay); + #endif + //fprintf(stderr, "Exit: %u\n", (unsigned)AudioBuffer.size()); + //} + #else /* DJGPP */ + UI.IllustrateVolumes(0, 0); + const double mindelay = 1.0 / NewTimerFreq; + + //__asm__ volatile("sti\nhlt"); + //usleep(10000); + __dpmi_yield(); + + static unsigned long PrevTimer = BIOStimer; + const unsigned long CurTimer = BIOStimer; + const double eat_delay = (CurTimer - PrevTimer) / (double)NewTimerFreq; + PrevTimer = CurTimer; + #endif + + //double nextdelay = + if(DoingInstrumentTesting) + InstrumentTester.Tick(eat_delay, mindelay); + //: player.Tick(eat_delay, mindelay); + + UI.GotoXY(0, 0); + UI.ShowCursor(); + + /* + * TODO: Implement the public "tick()" function for the Hardware OPL3 chip support on DJGPP + */ + + //delay = nextdelay; + } + + #ifdef __DJGPP__ + + // Fix the skewed clock and reset BIOS tick rate + _farpokel(_dos_ds, 0x46C, BIOStimer_begin + + (BIOStimer - BIOStimer_begin) + * (0x1234DD / 65536.0) / NewTimerFreq); + //disable(); + outportb(0x43, 0x34); + outportb(0x40, 0); + outportb(0x40, 0); + //enable(); + + #else + + #ifdef __WIN32__ + WindowsAudio::Close(); + #else + SDL_CloseAudio(); + #endif + + #endif /* djgpp */ + + adl_close(myDevice); + + if(FakeDOSshell) + { + fprintf(stderr, + "Going TSR. Type 'EXIT' to return to ADLMIDI.\n" + "\n" + /*"Megasoft(R) Orifices 98\n" + " (C)Copyright Megasoft Corp 1981 - 1999.\n"*/ + "" + ); + } + return 0; +} |