From 751fdc599e241c1f1c4c20513787e5f3706f21cc Mon Sep 17 00:00:00 2001 From: Jonas Kulla Date: Thu, 31 Jul 2014 03:32:07 +0200 Subject: [PATCH] Audio: Add MIDI format playback support This adds a new dependency with libfuildsynth. MIDI support is built by default, but can be disabled if not desired. All RTP songs should work well, but there are known problems with other files (see README). Also, the pitch shift implementation is somewhat poor and doesn't match RMXP (at least subjectively). A soundfont is not included and must be provided by the user themself. --- CMakeLists.txt | 16 + README.md | 14 +- mkxp.conf.sample | 16 + mkxp.pro | 18 + src/aldatasource.h | 13 +- src/alstream.cpp | 39 +- src/alstream.h | 3 + src/audio.cpp | 6 +- src/config.cpp | 8 +- src/config.h | 7 + src/filesystem.cpp | 5 + src/midisource.cpp | 859 +++++++++++++++++++++++++++++++++++++++++ src/sdlsoundsource.cpp | 15 +- src/sharedmidistate.h | 138 +++++++ src/sharedstate.cpp | 21 + src/sharedstate.h | 8 + src/vorbissource.cpp | 19 +- 17 files changed, 1170 insertions(+), 35 deletions(-) create mode 100644 src/midisource.cpp create mode 100644 src/sharedmidistate.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e0c064e..ca3a09d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ Project(mkxp) ## Setup options ## option(RGSS2 "Enable RGSS2" OFF) +option(MIDI "Enable midi support" ON) option(FORCE32 "Force 32bit compile on 64bit OS" OFF) set(BINDING "MRI" CACHE STRING "The Binding Type (MRI, MRUBY, NULL)") set(EXTERNAL_LIB_PATH "" CACHE PATH "External precompiled lib prefix") @@ -216,6 +217,19 @@ if (RGSS2) ) endif() +if (MIDI) + pkg_check_modules(MIDI REQUIRED fluidsynth) + list(APPEND DEFINES + MIDI + ) + list(APPEND MAIN_HEADERS + src/sharedmidistate.h + ) + list(APPEND MAIN_SOURCE + src/midisource.cpp + ) +endif() + ## Process Embeddeds ## find_program(XXD_EXE xxd @@ -374,6 +388,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE ${Boost_INCLUDE_DIR} ${MRI_INCLUDE_DIRS} ${RGSS2_INCLUDE_DIRS} + ${MIDI_INCLUDE_DIRS} ${OPENAL_INCLUDE_DIR} ) @@ -388,6 +403,7 @@ target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES} ${MRI_LIBRARIES} ${RGSS2_LIBRARIES} + ${MIDI_LIBRARIES} ${OPENAL_LIBRARY} ${ZLIB_LIBRARY} diff --git a/README.md b/README.md index 4ebc742..37cbd1d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ This binding only exists for testing purposes and does nothing (the engine quits * SDL2_ttf * SDL_sound (latest hg, apply provided patches!) * pixman +* fluidsynth (if midi enabled) * zlib (only ruby bindings) * OpenGL header (alternatively GLES2 with `DEFINES+=GLES2_HEADER`) @@ -50,6 +51,8 @@ qmake will use pkg-config to locate the respective include/library paths. If you The exception is boost, which is weird in that it still hasn't managed to pull off pkg-config support (seriously?). *If you installed boost in a non-standard prefix*, you will need to pass its include path via `BOOST_I` and library path via `BOOST_L`, either as direct arguments to qmake (`qmake BOOST_I="/usr/include" ...`) or via environment variables. You can specify a library suffix (eg. "-mt") via `BOOST_LIB_SUFFIX` if needed. +Midi support is enabled by default; you can disable it via `qmake CONFIG+=DISABLE_MIDI`, in which case the fluidsynth dependency is dropped. When building fluidsynth yourself, you can disable almost all options (audio drivers etc.) as they are not used. Note that upstream fluidsynth has support for sharing soundfont data between synthesizers (mkxp uses multiple synths), so if your memory usage is very high, you might want to try compiling fluidsynth from git master. + **MRI-Binding**: pkg-config will look for `ruby-2.1.pc`, but you can modify mkxp.pro to use 2.0 instead. This is the default binding, so no arguments to qmake needed (`BINDING=MRI` to be explicit). **MRuby-Binding**: place the "mruby" folder into the project folder and build it first. Add `BINDING=MRUBY` to qmake's arguments. @@ -65,9 +68,14 @@ To run mkxp, you should have a graphics card capable of at least **OpenGL (ES) 2 mkxp reads configuration data from the file "mkxp.conf" contained in the current directory. The format is ini-style. Do *not* use quotes around file paths (spaces won't break). Lines starting with '#' are comments. See 'mkxp.conf.sample' for a list of accepted entries. -## RTPs +## Midi music (*ALPHA STATUS*) -As of right now, mkxp doesn't support midi files, so to use the default RTPs provided by Enterbrain you will have to convert all midi tracks (those in BGM and ME) to ogg or wav. Make sure that the file names match up, ie. "foobar.mid" should be converted to "foobar.ogg". +mkxp doesn't come with a soundfont by default, so you will have to supply it yourself (set its path in the config). Playback has been tested and should work reasonably well with all RTP assets. + +Known issues with midi playback: + +* Some songs' instruments become mute after looping +* Pitch shifting is implemented poorly ## Fonts @@ -77,7 +85,7 @@ If a requested font is not found, no error is generated. Instead, a built-in fon ## What doesn't work (yet) -* midi and wma audio files +* wma audio files * The Win32API ruby class (for obvious reasons) * Restarting the game with F12 * Creating Bitmaps with sizes greater than the OpenGL texture size limit (around 8192 on modern cards)* diff --git a/mkxp.conf.sample b/mkxp.conf.sample index 574369e..86c33a9 100644 --- a/mkxp.conf.sample +++ b/mkxp.conf.sample @@ -147,3 +147,19 @@ # # rubyLoadpath=/usr/lib64/ruby/ # rubyLoadpath=/usr/local/share/ruby/site_ruby + + +# SoundFont to use for midi playback (via fluidsynth) +# (default: none) +# +# midi.soundFont=/usr/share/mysoundfont.sf2 + + +# Activate "chorus" effect for midi playback +# +# midi.chorus=false + + +# Activate "reverb" effect for midi playback +# +# midi.reverb=false diff --git a/mkxp.pro b/mkxp.pro index 7a87626..cdcd039 100644 --- a/mkxp.pro +++ b/mkxp.pro @@ -8,6 +8,12 @@ INCLUDEPATH += . src CONFIG(release, debug|release): DEFINES += NDEBUG +CONFIG += MIDI + +DISABLE_MIDI { + CONFIG -= MIDI +} + isEmpty(BINDING) { BINDING = MRI } @@ -57,6 +63,10 @@ unix { PKGCONFIG += vorbisfile } + MIDI { + PKGCONFIG += fluidsynth + } + # Deal with boost paths... isEmpty(BOOST_I) { BOOST_I = $$(BOOST_I) @@ -196,6 +206,14 @@ RGSS2 { shader/simpleMatrix.vert } +MIDI { + SOURCES += \ + src/midisource.cpp \ + src/sharedmidistate.h + + DEFINES += MIDI +} + defineReplace(xxdOutput) { return($$basename(1).xxd) } diff --git a/src/aldatasource.h b/src/aldatasource.h index 4292595..1b72177 100644 --- a/src/aldatasource.h +++ b/src/aldatasource.h @@ -42,13 +42,15 @@ struct ALDataSource virtual int sampleRate() = 0; + /* If the source doesn't support seeking, it will + * reset back to the beginning */ virtual void seekToOffset(float seconds) = 0; - /* Seek back to start */ - virtual void reset() = 0; - /* The frame count right after wrap around */ virtual uint32_t loopStartFrames() = 0; + + /* Returns false if not supported */ + virtual bool setPitch(float value) = 0; }; ALDataSource *createSDLSource(SDL_RWops &ops, @@ -61,4 +63,9 @@ ALDataSource *createVorbisSource(SDL_RWops &ops, bool looped); #endif +#ifdef MIDI +ALDataSource *createMidiSource(SDL_RWops &ops, + bool looped); +#endif + #endif // ALDATASOURCE_H diff --git a/src/alstream.cpp b/src/alstream.cpp index d5b9bdf..a8646d4 100644 --- a/src/alstream.cpp +++ b/src/alstream.cpp @@ -37,7 +37,8 @@ ALStream::ALStream(LoopMode loopMode, thread(0), preemptPause(false), streamInited(false), - needsRewind(false) + needsRewind(false), + pitch(1.0) { alSrc = AL::Source::gen(); @@ -161,7 +162,12 @@ void ALStream::setVolume(float value) void ALStream::setPitch(float value) { - AL::Source::setPitch(alSrc, value); + /* If the source supports setting pitch natively, + * we don't have to do it via OpenAL */ + if (source && source->setPitch(value)) + AL::Source::setPitch(alSrc, 1.0); + else + AL::Source::setPitch(alSrc, value); } ALStream::State ALStream::queryState() @@ -190,23 +196,31 @@ void ALStream::openSource(const std::string &filename) { const char *ext; shState->fileSystem().openRead(srcOps, filename.c_str(), FileSystem::Audio, false, &ext); + needsRewind = false; -#ifdef RGSS2 +#if RGSS2 || MIDI /* Try to read ogg file signature */ - char sig[5]; - memset(sig, '\0', sizeof(sig)); + char sig[5] = { 0 }; SDL_RWread(&srcOps, sig, 1, 4); SDL_RWseek(&srcOps, 0, RW_SEEK_SET); +#ifdef RGSS2 if (!strcmp(sig, "OggS")) + { source = createVorbisSource(srcOps, looped); - else - source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped); -#else - source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped); + return; + } +#endif +#ifdef MIDI + if (!strcmp(sig, "MThd")) + { + source = createMidiSource(srcOps, looped); + return; + } +#endif #endif - needsRewind = false; + source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped); } void ALStream::stopStream() @@ -306,10 +320,7 @@ void ALStream::streamData() if (needsRewind) { - if (startOffset > 0) - source->seekToOffset(startOffset); - else - source->reset(); + source->seekToOffset(startOffset); } for (int i = 0; i < STREAM_BUFS; ++i) diff --git a/src/alstream.h b/src/alstream.h index a20e350..7de277d 100644 --- a/src/alstream.h +++ b/src/alstream.h @@ -67,6 +67,8 @@ struct ALStream bool needsRewind; float startOffset; + float pitch; + AL::Source::ID alSrc; AL::Buffer::ID alBuf[STREAM_BUFS]; @@ -101,6 +103,7 @@ struct ALStream void setPitch(float value); State queryState(); float queryOffset(); + bool queryNativePitch(); private: void closeSource(); diff --git a/src/audio.cpp b/src/audio.cpp index c307fd2..d984bdc 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -23,6 +23,8 @@ #include "audiostream.h" #include "soundemitter.h" +#include "sharedstate.h" +#include "sharedmidistate.h" #include @@ -327,7 +329,9 @@ void Audio::seStop() void Audio::setupMidi() { - +#ifdef MIDI + shState->midiState().initDefaultSynths(); +#endif } float Audio::bgmPos() diff --git a/src/config.cpp b/src/config.cpp index 99f2e80..3db101e 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -50,7 +50,10 @@ Config::Config() allowSymlinks(false), pathCache(true), useScriptNames(false) -{} +{ + midi.chorus = false; + midi.reverb = false; +} void Config::read(int argc, char *argv[]) { @@ -70,6 +73,9 @@ void Config::read(int argc, char *argv[]) PO_DESC(anyAltToggleFS, bool) \ PO_DESC(allowSymlinks, bool) \ PO_DESC(iconPath, std::string) \ + PO_DESC(midi.soundFont, std::string) \ + PO_DESC(midi.chorus, bool) \ + PO_DESC(midi.reverb, bool) \ PO_DESC(customScript, std::string) \ PO_DESC(pathCache, bool) \ PO_DESC(useScriptNames, bool) diff --git a/src/config.h b/src/config.h index ade57b1..fd8a113 100644 --- a/src/config.h +++ b/src/config.h @@ -50,6 +50,13 @@ struct Config std::string iconPath; + struct + { + std::string soundFont; + bool chorus; + bool reverb; + } midi; + bool useScriptNames; std::string customScript; diff --git a/src/filesystem.cpp b/src/filesystem.cpp index bcadad1..74ac971 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -332,6 +332,11 @@ FileSystem::FileSystem(const char *argv0, } } +#if MIDI + p->extensions[Audio].push_back("mid"); + p->extensions[Audio].push_back("midi"); +#endif + /* Font extensions */ p->extensions[Font].push_back("ttf"); p->extensions[Font].push_back("otf"); diff --git a/src/midisource.cpp b/src/midisource.cpp new file mode 100644 index 0000000..4c75dff --- /dev/null +++ b/src/midisource.cpp @@ -0,0 +1,859 @@ +/* +** midisource.cpp +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp 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. +** +** mkxp 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 mkxp. If not, see . +*/ + +#include "aldatasource.h" + +#include "al-util.h" +#include "exception.h" +#include "sharedstate.h" +#include "sharedmidistate.h" +#include "util.h" +#include "debugwriter.h" + +#include +#include + +#include +#include +#include +#include +#include + +/* Vocabulary: + * + * Tick: + * Ticks are the smallest batch of samples that fluidsynth + * allows midi state changes to take effect in, ie. if two midi + * events are fired within less than a tick, it will look as + * if they fired at the same time. Midisource therefore always + * synthesizes sample blocks which are multiples of ticks. + * One tick is 64 samples, or 32 frames with two channels. + * + * Delta: + * Deltas are the abstract time unit in which the relative + * offsets between midi events are encoded. + */ + +#define TICK_FRAMES 32 +#define BUF_TICKS (STREAM_BUF_SIZE / TICK_FRAMES) +#define DEFAULT_BPM 120 +#define LOOP_MARKER 111 + +enum MidiEventType +{ + NoteOff, + NoteOn, + ChanTouch, + PitchBend, + CC, + PC, + Tempo, + + Last +}; + +struct ChannelEvent +{ + uint8_t chan; +}; + +struct NoteOffEvent : ChannelEvent +{ + uint8_t key; +}; + +struct NoteOnEvent : ChannelEvent +{ + uint8_t key; + uint8_t vel; +}; + +struct NoteTouchEvent : ChannelEvent +{ + uint8_t val; +}; + +struct PitchBendEvent : ChannelEvent +{ + uint16_t val; +}; + +struct CCEvent : ChannelEvent +{ + uint8_t ctrl; + uint8_t val; +}; + +struct PCEvent : ChannelEvent +{ + uint8_t prog; +}; + +struct TempoEvent +{ + uint32_t bpm; +}; + +struct MidiEvent +{ + MidiEventType type; + uint32_t delta; + union { + ChannelEvent chan; + NoteOnEvent noteOn; + NoteOffEvent noteOff; + NoteTouchEvent chanTouch; + PitchBendEvent pitchBend; + CCEvent cc; + PCEvent pc; + TempoEvent tempo; + } e; +}; + +struct MidiReadHandler +{ + virtual void onMidiHeader(uint16_t midiType, uint16_t trackCount, uint16_t division) = 0; + virtual void onMidiTrackBegin() = 0; + virtual void onMidiEvent(const MidiEvent &e, uint32_t absDelta) = 0; +}; + +static void +badMidiFormat() +{ + throw Exception(Exception::MKXPError, "Midi: Bad format"); +} + +/* File-like interface to a read-only memory buffer */ +struct MemChunk +{ + const uint8_t *data; + const size_t len; + size_t i; + + MemChunk(const uint8_t *data, size_t len) + : data(data), + len(len), + i(0) + {} + + uint8_t readByte() + { + if (i >= len) + endOfFile(); + + return data[i++]; + } + + void readData(void *buf, size_t dataLen) + { + if (i + dataLen > len) + endOfFile(); + + memcpy(buf, &data[i], dataLen); + i += dataLen; + } + + void skipData(size_t dataLen) + { + if ((i += dataLen) > len) + endOfFile(); + } + + void endOfFile() + { + throw Exception(Exception::MKXPError, "Midi: EOF"); + } +}; + +static uint32_t +readVarNum(MemChunk &chunk) +{ + uint32_t result = 0; + uint8_t byte; + uint8_t len = 0; + + do + { + /* A variable length number can at most be made of 4 bytes */ + if (++len > 4) + badMidiFormat(); + + byte = chunk.readByte(); + result = (result << 0x7) | (byte & 0x7F); + } + while (byte & 0x80); + + return result; +} + +template +static T readBigEndian(MemChunk &chunk) +{ + T result = 0; + + for (size_t i = 0; i < sizeof(T); ++i) + result = (result << 0x8) | chunk.readByte(); + + return result; +} + +static void +readVoiceEvent(MidiEvent &e, MemChunk &chunk, + uint8_t type, uint8_t data1, bool &handled) +{ + e.e.chan.chan = (type & 0x0F); + + uint8_t tmp; + + switch (type >> 4) + { + case 0x8 : + e.type = NoteOff; + e.e.noteOff.key = data1; + chunk.readByte(); /* We don't care about velocity */ + break; + + case 0x9 : + e.type = NoteOn; + e.e.noteOn.key = data1; + e.e.noteOn.vel = chunk.readByte(); + break; + + case 0xA : + /* Note aftertouch unhandled */ + handled = false; + break; + + case 0xB : + e.type = CC; + e.e.cc.ctrl = data1; + e.e.cc.val = chunk.readByte(); + break; + + case 0xC : + e.type = PC; + e.e.pc.prog = data1; + break; + + case 0xD : + e.type = ChanTouch; + e.e.chanTouch.val = data1; + break; + + case 0xE : + e.type = PitchBend; + tmp = chunk.readByte(); + e.e.pitchBend.val = ((tmp & 0x7F) << 7) | (data1 & 0x7F); + break; + + default : + assert(!"unreachable"); + break; + } +} + +static void +readEvent(MidiReadHandler *handler, MemChunk &chunk, + uint8_t &prevType, uint32_t &deltaBase, uint32_t &deltaCarry, + bool &endOfTrack) +{ + MidiEvent e; + bool handled = true; + + e.delta = readVarNum(chunk); + + uint8_t type = chunk.readByte(); + + /* Check for running status */ + if (!(type & 0x80)) + { + /* Running status for meta/system events is not allowed */ + if (prevType == 0) + badMidiFormat(); + + /* Running status voice event: 'type' becomes + * first data byte instead */ + readVoiceEvent(e, chunk, prevType, type, handled); + goto event_read; + } + + if ((type >> 4) != 0xF) + { + /* Normal voice event */ + readVoiceEvent(e, chunk, type, chunk.readByte(), handled); + prevType = type; + } + else if (type == 0xFF) + { + /* Meta event */ + uint8_t metaType = chunk.readByte(); + uint32_t len = readVarNum(chunk); + + if (metaType == 0x51) + { + /* Tempo event */ + if (len != 3) + badMidiFormat(); + + uint8_t data[3]; + chunk.readData(data, 3); + + uint32_t mpqn = (data[0] << 0x10) + | (data[1] << 0x08) + | (data[2] << 0x00); + + e.type = Tempo; + e.e.tempo.bpm = 60000000.0 / mpqn; + } + else if (metaType == 0x2F) + { + /* End-of-track event */ + if (len != 0) + badMidiFormat(); + + endOfTrack = true; + handled = false; + } + else + { + handled = false; + } + + if (!handled) + chunk.skipData(len); + + prevType = 0; + } + else if (type == 0xF0 || type == 0xF7) + { + /* SysEx event */ + uint32_t len = readVarNum(chunk); + handled = false; + chunk.skipData(len); + + prevType = 0; + } + else + { + badMidiFormat(); + } + +event_read: + + if (handled) + { + e.delta += deltaCarry; + deltaCarry = 0; + deltaBase += e.delta; + + handler->onMidiEvent(e, deltaBase); + } + else + { + deltaCarry += e.delta; + } +} + +void readMidiTrack(MidiReadHandler *handler, MemChunk &chunk) +{ + /* Track signature */ + char sig[5] = { 0 }; + chunk.readData(sig, 4); + if (strcmp(sig, "MTrk")) + badMidiFormat(); + + handler->onMidiTrackBegin(); + + uint32_t trackLen = readBigEndian(chunk); + + /* The combined delta of all events on this track so far */ + uint32_t deltaBase = 0; + + /* If we don't care about an event, instead of inserting a + * useless event, we carry over its delta into the next one */ + uint32_t deltaCarry = 0; + + /* Holds the previous status byte for voice event, in case + * we encounter a running status */ + uint8_t prevType = 0; + + /* The 'EndOfTrack' meta event will toggle this flag on */ + bool endOfTrack = false; + + size_t savedPos = chunk.i; + + /* Read all events */ + while (!endOfTrack) + readEvent(handler, chunk, prevType, deltaBase, deltaCarry, endOfTrack); + + /* Check that the track byte length from the header + * matches the amount we actually ended up reading */ + if ((chunk.i - savedPos) != trackLen) + badMidiFormat(); +} + +void readMidi(MidiReadHandler *handler, uint8_t *data, size_t len) +{ + MemChunk chunk(data, len); + + /* Midi signature */ + char sig[5] = { 0 }; + chunk.readData(sig, 4); + if (strcmp(sig, "MThd")) + badMidiFormat(); + + /* Header length must always be 6 */ + uint32_t hdrLen = readBigEndian(chunk); + if (hdrLen != 6) + badMidiFormat(); + + /* Only types 0, 1, 2 exist */ + uint16_t type = readBigEndian(chunk); + if (type > 2) + badMidiFormat(); + + /* Type 0 only contains one track */ + uint16_t trackCount = readBigEndian(chunk); + if (trackCount == 0) + badMidiFormat(); + if (type == 0 && trackCount > 1) + badMidiFormat(); + + uint16_t timeDiv = readBigEndian(chunk); + + handler->onMidiHeader(type, trackCount, timeDiv); + + /* Read tracks */ + for (uint16_t i = 0; i < trackCount; ++i) + readMidiTrack(handler, chunk); +} + +struct Track +{ + std::vector events; + + /* Combined deltas of all events */ + uint64_t length; + + /* Event index that is resumed from after loop wraparound */ + int32_t loopI; + + /* Difference between last event's position and length of + * the longest overall track / song length */ + uint32_t loopOffsetEnd; + + /* Delta offset from the loop beginning to event[loopI] */ + uint32_t loopOffsetStart; + + bool valid; + MidiEvent event; + int32_t remDeltas; + + uint32_t index; + bool wrapAroundFlag; + bool atEnd; + + Track() + : length(0), + loopI(-1), + loopOffsetEnd(0), + loopOffsetStart(0), + valid(false), + index(0), + wrapAroundFlag(false), + atEnd(false) + {} + + void appendEvent(const MidiEvent &e) + { + length += e.delta; + events.push_back(e); + } + + void scheduleEvent(bool looped) + { + if (valid) + return; + + /* At end and not looped? */ + if (index == events.size() && ((!looped || loopI < 0) || length == 0)) + { + valid = false; + atEnd = true; + return; + } + + MidiEvent &e = events[index++]; + + event = e; + remDeltas = e.delta; + valid = true; + + if (wrapAroundFlag) + { + remDeltas = loopOffsetEnd + loopOffsetStart; + wrapAroundFlag = false; + } + + if (looped && (index == events.size()) && loopI >= 0) + { + index = loopI; + wrapAroundFlag = true; + } + } + + void reset() + { + valid = false; + index = 0; + wrapAroundFlag = false; + atEnd = false; + } +}; + +struct MidiSource : ALDataSource, MidiReadHandler +{ + const uint16_t freq; + fluid_synth_t *synth; + + int16_t synthBuf[BUF_TICKS*TICK_FRAMES*2]; + + std::vector tracks; + + /* Index of longest track */ + uint8_t longestI; + + bool looped; + + /* Absolute delta at which we received the LOOP_MARKER CC event */ + uint32_t loopDelta; + + /* Deltas per beat */ + uint16_t dpb; + + int8_t pitchShift; + + /* Deltas per tick */ + float playbackSpeed; + + float genDeltasCarry; + + /* MidiReadHandler (track that's currently being read) */ + int16_t curTrack; + + MidiSource(SDL_RWops &ops, + bool looped) + : freq(SYNTH_SAMPLERATE), + looped(looped), + dpb(480), + pitchShift(0), + genDeltasCarry(0), + curTrack(-1) + { + uint8_t *data = 0; + + try + { + size_t dataLen = SDL_RWsize(&ops); + data = new uint8_t[dataLen]; + if (SDL_RWread(&ops, data, 1, dataLen) < dataLen) + throw Exception(Exception::MKXPError, "Reading midi data failed"); + + readMidi(this, data, dataLen); + } + catch (Exception &e) + { + delete[] data; + throw e; + } + + synth = shState->midiState().allocateSynth(); + + uint64_t longest = 0; + + for (size_t i = 0; i < tracks.size(); ++i) + if (tracks[i].length > longest) + { + longest = tracks[i].length; + longestI = i; + } + + for (size_t i = 0; i < tracks.size(); ++i) + tracks[i].loopOffsetEnd = longest - tracks[i].length; + + /* Enterbrain likes to be funny and put loop markers at + * the very end of ME tracks */ + if (loopDelta >= longest) + loopDelta = 0; + + for (size_t i = 0; i < tracks.size(); ++i) + { + Track &track = tracks[i]; + + int64_t base = 0; + for (size_t j = 0; j < track.events.size(); ++j) + { + MidiEvent &e = track.events[j]; + base += e.delta; + + if (base < loopDelta) + continue; + + /* Don't make the last event loop eternally */ + if (j < track.events.size()) + { + track.loopI = j; + track.loopOffsetStart = base - loopDelta; + } + + break; + } + } + + updatePlaybackSpeed(DEFAULT_BPM); + + // FIXME: It would make the code in 'fillBuffer' a lot nicer if + // we could combine all tracks into one giant one on construction, + // instead of having to constantly iterate through all of them + } + + ~MidiSource() + { + shState->midiState().releaseSynth(synth); + } + + + void updatePlaybackSpeed(uint32_t bpm) + { + float deltaLength = 60.0 / (dpb * bpm); + playbackSpeed = TICK_FRAMES / (deltaLength * freq); + } + + void activateEvent(MidiEvent &e) + { + // FIXME: This is not a good solution for very high/low + // keys, but I'm not sure what else would be. + int8_t shift = (e.e.chan.chan != 9) ? pitchShift : 0; + int16_t key; + + switch (e.type) + { + case NoteOn: + key = clamp(e.e.noteOn.key+shift, 0, 127); + fluid_synth_noteon(synth, e.e.noteOn.chan, key, e.e.noteOn.vel); + break; + case NoteOff: + key = clamp(e.e.noteOn.key+shift, 0, 127); + fluid_synth_noteoff(synth, e.e.noteOff.chan, key); + break; + case ChanTouch: + fluid_synth_channel_pressure(synth, e.e.chanTouch.chan, e.e.chanTouch.val); + break; + case PitchBend: + fluid_synth_pitch_bend(synth, e.e.pitchBend.chan, e.e.pitchBend.val); + break; + case CC: + fluid_synth_cc(synth, e.e.cc.chan, e.e.cc.ctrl, e.e.cc.val); + break; + case PC: + fluid_synth_program_change(synth, e.e.pc.chan, e.e.pc.prog); + break; + case Tempo: + updatePlaybackSpeed(e.e.tempo.bpm); + break; + default: + break; + } + } + + void renderTicks(size_t count, size_t offset) + { + size_t bufOffset = offset * TICK_FRAMES * 2; + int len = count * TICK_FRAMES; + void *buffer = &synthBuf[bufOffset]; + + fluid_synth_write_s16(synth, len, buffer, 0, 2, buffer, 1, 2); + } + + /* MidiReadHandler */ + void onMidiHeader(uint16_t midiType, uint16_t trackCount, uint16_t division) + { + if (midiType != 0 && midiType != 1) + throw Exception(Exception::MKXPError, "Midi: Type 2 not supported"); + + tracks.resize(trackCount); + + // SMTP unhandled + if (division & 0x8000) + throw Exception(Exception::MKXPError, "Midi: SMTP parameters not supported"); + else + dpb = division; + } + + void onMidiTrackBegin() + { + ++curTrack; + } + + void onMidiEvent(const MidiEvent &e, uint32_t absDelta) + { + assert(curTrack >= 0 && curTrack < (int16_t) tracks.size()); + tracks[curTrack].appendEvent(e); + + if (e.type == CC && e.e.cc.ctrl == LOOP_MARKER) + loopDelta = absDelta; + } + + /* ALDataSource */ + Status fillBuffer(AL::Buffer::ID buf) + { + /* In case there is no currently scheduled one */ + for (size_t i = 0; i < tracks.size(); ++i) + tracks[i].scheduleEvent(looped); + + size_t remTicks = BUF_TICKS; + + /* Iterate until all ticks that fit into the buffer + * have been rendered */ + while (remTicks > 0) + { + /* Check for events that have to be activated now, activate them, + * and schedule new ones if the queue isn't empty */ + for (size_t i = 0; i < tracks.size(); ++i) + { + Track &track = tracks[i]; + + /* We have to loop and ensure that the final scheduled + * event lies in the future, as multiple events might + * have to be activated at once */ + while (true) + { + if (!track.valid || track.remDeltas > 0) + break; + + int32_t prevOffset = track.remDeltas; + + activateEvent(track.event); + + track.valid = false; + track.scheduleEvent(looped); + + /* Negative deltas from the previous event have to + * be carried over into the next to stay in sync */ + if (prevOffset < 0) + track.remDeltas += prevOffset; + + /* Ensure it lies in the future */ + if (track.remDeltas > 0) + break; + } + } + + size_t nextEvent = (size_t) -1; + bool allInvalid = true; + + /* Search all tracks for the temporally nearest event */ + for (size_t i = 0; i < tracks.size(); ++i) + { + Track &track = tracks[i]; + + if (!track.valid) + continue; + + allInvalid = false; + + uint32_t remDelta = track.remDeltas / playbackSpeed; + + /* We need to render at least one tick regardless to + * avoid an endless loop of waiting for the next event + * to become current */ + if (remDelta < nextEvent) + nextEvent = std::max(remDelta, 1); + } + + /* Calculate amount of ticks we'll render next */ + size_t genTicks = allInvalid ? remTicks : std::min(remTicks, nextEvent); + + if (genTicks == 0) + continue; + + renderTicks(genTicks, BUF_TICKS - remTicks); + remTicks -= genTicks; + + float genDeltas = (genTicks * playbackSpeed) + genDeltasCarry; + + float intDeltas; + genDeltasCarry = modff(genDeltas, &intDeltas); + + /* Substract integer part of consumed deltas while carrying + * over the fractional amount into the next iteration */ + for (size_t i = 0; i < tracks.size(); ++i) + if (tracks[i].valid) + tracks[i].remDeltas -= intDeltas; + } + + /* Fill AL buffer */ + AL::Buffer::uploadData(buf, AL_FORMAT_STEREO16, synthBuf, sizeof(synthBuf), freq); + + if (tracks[longestI].atEnd) + return EndOfStream; + + return NoError; + } + + int sampleRate() + { + return freq; + } + + /* Midi sources cannot seek, and so always reset to beginning */ + void seekToOffset(float) + { + /* Reset synth */ + fluid_synth_system_reset(synth); + + /* Reset runtime variables */ + genDeltasCarry = 0; + updatePlaybackSpeed(DEFAULT_BPM); + + /* Reset tracks */ + for (size_t i = 0; i < tracks.size(); ++i) + tracks[i].reset(); + } + + uint32_t loopStartFrames() { return 0; } + + bool setPitch(float value) + { + // 1.0 = one octave, not sure about this yet + pitchShift = 12 * (value - 1.0); + + return true; + } +}; + +ALDataSource *createMidiSource(SDL_RWops &ops, + bool looped) +{ + return new MidiSource(ops, looped); + + return 0; +} diff --git a/src/sdlsoundsource.cpp b/src/sdlsoundsource.cpp index a90709f..8e1de83 100644 --- a/src/sdlsoundsource.cpp +++ b/src/sdlsoundsource.cpp @@ -103,12 +103,10 @@ struct SDLSoundSource : ALDataSource void seekToOffset(float seconds) { - Sound_Seek(sample, static_cast(seconds * 1000)); - } - - void reset() - { - Sound_Rewind(sample); + if (seconds <= 0) + Sound_Rewind(sample); + else + Sound_Seek(sample, static_cast(seconds * 1000)); } uint32_t loopStartFrames() @@ -116,6 +114,11 @@ struct SDLSoundSource : ALDataSource /* Loops from the beginning of the file */ return 0; } + + bool setPitch(float) + { + return false; + } }; ALDataSource *createSDLSource(SDL_RWops &ops, diff --git a/src/sharedmidistate.h b/src/sharedmidistate.h new file mode 100644 index 0000000..9066981 --- /dev/null +++ b/src/sharedmidistate.h @@ -0,0 +1,138 @@ +/* +** sharedmidistate.h +** +** This file is part of mkxp. +** +** Copyright (C) 2014 Jonas Kulla +** +** mkxp 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. +** +** mkxp 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 mkxp. If not, see . +*/ + +#ifndef SHAREDMIDISTATE_H +#define SHAREDMIDISTATE_H + +#include "config.h" +#include "debugwriter.h" + +#include + +#include +#include +#include + +#define SYNTH_INIT_COUNT 2 +#define SYNTH_SAMPLERATE 44100 + +struct Synth +{ + fluid_synth_t *synth; + bool inUse; +}; + +struct SharedMidiState +{ + bool inited; + std::vector synths; + const std::string &soundFont; + fluid_settings_t *flSettings; + + SharedMidiState(const Config &conf) + : inited(false), + soundFont(conf.midi.soundFont) + { + flSettings = new_fluid_settings(); + fluid_settings_setnum(flSettings, "synth.gain", 1.0); + fluid_settings_setnum(flSettings, "synth.sample-rate", SYNTH_SAMPLERATE); + fluid_settings_setstr(flSettings, "synth.chorus.active", conf.midi.chorus ? "yes" : "no"); + fluid_settings_setstr(flSettings, "synth.reverb.active", conf.midi.reverb ? "yes" : "no"); + } + + ~SharedMidiState() + { + delete_fluid_settings(flSettings); + + for (size_t i = 0; i < synths.size(); ++i) + { + assert(!synths[i].inUse); + delete_fluid_synth(synths[i].synth); + } + } + + fluid_synth_t *addSynth(bool usedNow) + { + fluid_synth_t *syn = new_fluid_synth(flSettings); + + if (!soundFont.empty()) + fluid_synth_sfload(syn, soundFont.c_str(), 1); + else + Debug() << "Warning: No soundfont specified, sound might be mute"; + + Synth synth; + synth.inUse = usedNow; + synth.synth = syn; + synths.push_back(synth); + + return syn; + } + + void initDefaultSynths() + { + if (inited) + return; + + for (size_t i = 0; i < SYNTH_INIT_COUNT; ++i) + addSynth(false); + + inited = true; + } + + fluid_synth_t *allocateSynth() + { + size_t i; + + initDefaultSynths(); + + for (i = 0; i < synths.size(); ++i) + if (!synths[i].inUse) + break; + + if (i < synths.size()) + { + fluid_synth_t *syn = synths[i].synth; + fluid_synth_system_reset(syn); + synths[i].inUse = true; + + return syn; + } + else + { + return addSynth(true); + } + } + + void releaseSynth(fluid_synth_t *synth) + { + size_t i; + + for (i = 0; i < synths.size(); ++i) + if (synths[i].synth == synth) + break; + + assert(i < synths.size()); + + synths[i].inUse = false; + } +}; + +#endif // SHAREDMIDISTATE_H diff --git a/src/sharedstate.cpp b/src/sharedstate.cpp index 23ade67..6f430cc 100644 --- a/src/sharedstate.cpp +++ b/src/sharedstate.cpp @@ -37,6 +37,10 @@ #include "binding.h" #include "exception.h" +#ifdef MIDI +#include "sharedmidistate.h" +#endif + #include #include #include @@ -58,6 +62,10 @@ struct SharedStatePrivate RGSSThreadData &rtData; Config &config; +#ifdef MIDI + SharedMidiState midiState; +#endif + Graphics graphics; Input input; Audio audio; @@ -89,6 +97,9 @@ struct SharedStatePrivate eThread(*threadData->ethread), rtData(*threadData), config(threadData->config), +#ifdef MIDI + midiState(threadData->config), +#endif graphics(threadData), fontState(threadData->config), stampCounter(0) @@ -137,6 +148,12 @@ struct SharedStatePrivate /* Reuse starting values */ TEXFBO::allocEmpty(gpTexFBO, globalTexW, globalTexH); TEXFBO::linkFBO(gpTexFBO); + + /* RGSS3 games will call setup_midi, so there's + * no need to do it on startup */ +#if MIDI && !RGSS3 + midiState.initDefaultSynths(); +#endif } ~SharedStatePrivate() @@ -212,6 +229,10 @@ GSATT(TexPool&, texPool) GSATT(Quad&, gpQuad) GSATT(SharedFontState&, fontState) +#ifdef MIDI +GSATT(SharedMidiState&, midiState) +#endif + void SharedState::setBindingData(void *data) { p->bindingData = data; diff --git a/src/sharedstate.h b/src/sharedstate.h index 62689e7..686155f 100644 --- a/src/sharedstate.h +++ b/src/sharedstate.h @@ -49,6 +49,10 @@ struct GlobalIBO; struct Config; struct Vec2i; +#ifdef MIDI +struct SharedMidiState; +#endif + struct SharedState { void *bindingData(); @@ -78,6 +82,10 @@ struct SharedState SharedFontState &fontState(); Font &defaultFont(); +#ifdef MIDI + SharedMidiState &midiState(); +#endif + sigc::signal prepareDraw; unsigned int genTimeStamp(); diff --git a/src/vorbissource.cpp b/src/vorbissource.cpp index c007ad8..07dcdb4 100644 --- a/src/vorbissource.cpp +++ b/src/vorbissource.cpp @@ -157,6 +157,12 @@ struct VorbisSource : ALDataSource void seekToOffset(float seconds) { + if (seconds <= 0) + { + ov_raw_seek(&vf, 0); + currentFrame = 0; + } + currentFrame = seconds * info.rate; if (loop.valid && currentFrame > loop.end) @@ -205,7 +211,7 @@ struct VorbisSource : ALDataSource if (loop.requested) { retStatus = ALDataSource::WrapAround; - reset(); + seekToOffset(0); } else { @@ -261,12 +267,6 @@ struct VorbisSource : ALDataSource return retStatus; } - void reset() - { - ov_raw_seek(&vf, 0); - currentFrame = 0; - } - uint32_t loopStartFrames() { if (loop.valid) @@ -274,6 +274,11 @@ struct VorbisSource : ALDataSource else return 0; } + + bool setPitch(float) + { + return false; + } }; ALDataSource *createVorbisSource(SDL_RWops &ops,