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.
This commit is contained in:
Jonas Kulla 2014-07-31 03:32:07 +02:00
parent 818ca18ebb
commit 751fdc599e
17 changed files with 1170 additions and 35 deletions

View File

@ -4,6 +4,7 @@ Project(mkxp)
## Setup options ## ## Setup options ##
option(RGSS2 "Enable RGSS2" OFF) option(RGSS2 "Enable RGSS2" OFF)
option(MIDI "Enable midi support" ON)
option(FORCE32 "Force 32bit compile on 64bit OS" OFF) option(FORCE32 "Force 32bit compile on 64bit OS" OFF)
set(BINDING "MRI" CACHE STRING "The Binding Type (MRI, MRUBY, NULL)") set(BINDING "MRI" CACHE STRING "The Binding Type (MRI, MRUBY, NULL)")
set(EXTERNAL_LIB_PATH "" CACHE PATH "External precompiled lib prefix") set(EXTERNAL_LIB_PATH "" CACHE PATH "External precompiled lib prefix")
@ -216,6 +217,19 @@ if (RGSS2)
) )
endif() 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 ## ## Process Embeddeds ##
find_program(XXD_EXE xxd find_program(XXD_EXE xxd
@ -374,6 +388,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE
${Boost_INCLUDE_DIR} ${Boost_INCLUDE_DIR}
${MRI_INCLUDE_DIRS} ${MRI_INCLUDE_DIRS}
${RGSS2_INCLUDE_DIRS} ${RGSS2_INCLUDE_DIRS}
${MIDI_INCLUDE_DIRS}
${OPENAL_INCLUDE_DIR} ${OPENAL_INCLUDE_DIR}
) )
@ -388,6 +403,7 @@ target_link_libraries(${PROJECT_NAME}
${Boost_LIBRARIES} ${Boost_LIBRARIES}
${MRI_LIBRARIES} ${MRI_LIBRARIES}
${RGSS2_LIBRARIES} ${RGSS2_LIBRARIES}
${MIDI_LIBRARIES}
${OPENAL_LIBRARY} ${OPENAL_LIBRARY}
${ZLIB_LIBRARY} ${ZLIB_LIBRARY}

View File

@ -41,6 +41,7 @@ This binding only exists for testing purposes and does nothing (the engine quits
* SDL2_ttf * SDL2_ttf
* SDL_sound (latest hg, apply provided patches!) * SDL_sound (latest hg, apply provided patches!)
* pixman * pixman
* fluidsynth (if midi enabled)
* zlib (only ruby bindings) * zlib (only ruby bindings)
* OpenGL header (alternatively GLES2 with `DEFINES+=GLES2_HEADER`) * 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. 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). **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. **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. 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 ## 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) ## What doesn't work (yet)
* midi and wma audio files * wma audio files
* The Win32API ruby class (for obvious reasons) * The Win32API ruby class (for obvious reasons)
* Restarting the game with F12 * Restarting the game with F12
* Creating Bitmaps with sizes greater than the OpenGL texture size limit (around 8192 on modern cards)* * Creating Bitmaps with sizes greater than the OpenGL texture size limit (around 8192 on modern cards)*

View File

@ -147,3 +147,19 @@
# #
# rubyLoadpath=/usr/lib64/ruby/ # rubyLoadpath=/usr/lib64/ruby/
# rubyLoadpath=/usr/local/share/ruby/site_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

View File

@ -8,6 +8,12 @@ INCLUDEPATH += . src
CONFIG(release, debug|release): DEFINES += NDEBUG CONFIG(release, debug|release): DEFINES += NDEBUG
CONFIG += MIDI
DISABLE_MIDI {
CONFIG -= MIDI
}
isEmpty(BINDING) { isEmpty(BINDING) {
BINDING = MRI BINDING = MRI
} }
@ -57,6 +63,10 @@ unix {
PKGCONFIG += vorbisfile PKGCONFIG += vorbisfile
} }
MIDI {
PKGCONFIG += fluidsynth
}
# Deal with boost paths... # Deal with boost paths...
isEmpty(BOOST_I) { isEmpty(BOOST_I) {
BOOST_I = $$(BOOST_I) BOOST_I = $$(BOOST_I)
@ -196,6 +206,14 @@ RGSS2 {
shader/simpleMatrix.vert shader/simpleMatrix.vert
} }
MIDI {
SOURCES += \
src/midisource.cpp \
src/sharedmidistate.h
DEFINES += MIDI
}
defineReplace(xxdOutput) { defineReplace(xxdOutput) {
return($$basename(1).xxd) return($$basename(1).xxd)
} }

View File

@ -42,13 +42,15 @@ struct ALDataSource
virtual int sampleRate() = 0; virtual int sampleRate() = 0;
/* If the source doesn't support seeking, it will
* reset back to the beginning */
virtual void seekToOffset(float seconds) = 0; virtual void seekToOffset(float seconds) = 0;
/* Seek back to start */
virtual void reset() = 0;
/* The frame count right after wrap around */ /* The frame count right after wrap around */
virtual uint32_t loopStartFrames() = 0; virtual uint32_t loopStartFrames() = 0;
/* Returns false if not supported */
virtual bool setPitch(float value) = 0;
}; };
ALDataSource *createSDLSource(SDL_RWops &ops, ALDataSource *createSDLSource(SDL_RWops &ops,
@ -61,4 +63,9 @@ ALDataSource *createVorbisSource(SDL_RWops &ops,
bool looped); bool looped);
#endif #endif
#ifdef MIDI
ALDataSource *createMidiSource(SDL_RWops &ops,
bool looped);
#endif
#endif // ALDATASOURCE_H #endif // ALDATASOURCE_H

View File

@ -37,7 +37,8 @@ ALStream::ALStream(LoopMode loopMode,
thread(0), thread(0),
preemptPause(false), preemptPause(false),
streamInited(false), streamInited(false),
needsRewind(false) needsRewind(false),
pitch(1.0)
{ {
alSrc = AL::Source::gen(); alSrc = AL::Source::gen();
@ -161,6 +162,11 @@ void ALStream::setVolume(float value)
void ALStream::setPitch(float value) void ALStream::setPitch(float 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); AL::Source::setPitch(alSrc, value);
} }
@ -190,23 +196,31 @@ void ALStream::openSource(const std::string &filename)
{ {
const char *ext; const char *ext;
shState->fileSystem().openRead(srcOps, filename.c_str(), FileSystem::Audio, false, &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 */ /* Try to read ogg file signature */
char sig[5]; char sig[5] = { 0 };
memset(sig, '\0', sizeof(sig));
SDL_RWread(&srcOps, sig, 1, 4); SDL_RWread(&srcOps, sig, 1, 4);
SDL_RWseek(&srcOps, 0, RW_SEEK_SET); SDL_RWseek(&srcOps, 0, RW_SEEK_SET);
#ifdef RGSS2
if (!strcmp(sig, "OggS")) if (!strcmp(sig, "OggS"))
{
source = createVorbisSource(srcOps, looped); source = createVorbisSource(srcOps, looped);
else return;
source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped); }
#else #endif
source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped); #ifdef MIDI
if (!strcmp(sig, "MThd"))
{
source = createMidiSource(srcOps, looped);
return;
}
#endif
#endif #endif
needsRewind = false; source = createSDLSource(srcOps, ext, STREAM_BUF_SIZE, looped);
} }
void ALStream::stopStream() void ALStream::stopStream()
@ -306,10 +320,7 @@ void ALStream::streamData()
if (needsRewind) if (needsRewind)
{ {
if (startOffset > 0)
source->seekToOffset(startOffset); source->seekToOffset(startOffset);
else
source->reset();
} }
for (int i = 0; i < STREAM_BUFS; ++i) for (int i = 0; i < STREAM_BUFS; ++i)

View File

@ -67,6 +67,8 @@ struct ALStream
bool needsRewind; bool needsRewind;
float startOffset; float startOffset;
float pitch;
AL::Source::ID alSrc; AL::Source::ID alSrc;
AL::Buffer::ID alBuf[STREAM_BUFS]; AL::Buffer::ID alBuf[STREAM_BUFS];
@ -101,6 +103,7 @@ struct ALStream
void setPitch(float value); void setPitch(float value);
State queryState(); State queryState();
float queryOffset(); float queryOffset();
bool queryNativePitch();
private: private:
void closeSource(); void closeSource();

View File

@ -23,6 +23,8 @@
#include "audiostream.h" #include "audiostream.h"
#include "soundemitter.h" #include "soundemitter.h"
#include "sharedstate.h"
#include "sharedmidistate.h"
#include <string> #include <string>
@ -327,7 +329,9 @@ void Audio::seStop()
void Audio::setupMidi() void Audio::setupMidi()
{ {
#ifdef MIDI
shState->midiState().initDefaultSynths();
#endif
} }
float Audio::bgmPos() float Audio::bgmPos()

View File

@ -50,7 +50,10 @@ Config::Config()
allowSymlinks(false), allowSymlinks(false),
pathCache(true), pathCache(true),
useScriptNames(false) useScriptNames(false)
{} {
midi.chorus = false;
midi.reverb = false;
}
void Config::read(int argc, char *argv[]) void Config::read(int argc, char *argv[])
{ {
@ -70,6 +73,9 @@ void Config::read(int argc, char *argv[])
PO_DESC(anyAltToggleFS, bool) \ PO_DESC(anyAltToggleFS, bool) \
PO_DESC(allowSymlinks, bool) \ PO_DESC(allowSymlinks, bool) \
PO_DESC(iconPath, std::string) \ 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(customScript, std::string) \
PO_DESC(pathCache, bool) \ PO_DESC(pathCache, bool) \
PO_DESC(useScriptNames, bool) PO_DESC(useScriptNames, bool)

View File

@ -50,6 +50,13 @@ struct Config
std::string iconPath; std::string iconPath;
struct
{
std::string soundFont;
bool chorus;
bool reverb;
} midi;
bool useScriptNames; bool useScriptNames;
std::string customScript; std::string customScript;

View File

@ -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 */ /* Font extensions */
p->extensions[Font].push_back("ttf"); p->extensions[Font].push_back("ttf");
p->extensions[Font].push_back("otf"); p->extensions[Font].push_back("otf");

859
src/midisource.cpp Normal file
View File

@ -0,0 +1,859 @@
/*
** midisource.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2014 Jonas Kulla <Nyocurio@gmail.com>
**
** 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 <http://www.gnu.org/licenses/>.
*/
#include "aldatasource.h"
#include "al-util.h"
#include "exception.h"
#include "sharedstate.h"
#include "sharedmidistate.h"
#include "util.h"
#include "debugwriter.h"
#include <fluidsynth.h>
#include <SDL_rwops.h>
#include <assert.h>
#include <math.h>
#include <vector>
#include <algorithm>
#include <string>
/* 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<typename T>
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<uint32_t>(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<uint32_t>(chunk);
if (hdrLen != 6)
badMidiFormat();
/* Only types 0, 1, 2 exist */
uint16_t type = readBigEndian<uint16_t>(chunk);
if (type > 2)
badMidiFormat();
/* Type 0 only contains one track */
uint16_t trackCount = readBigEndian<uint16_t>(chunk);
if (trackCount == 0)
badMidiFormat();
if (type == 0 && trackCount > 1)
badMidiFormat();
uint16_t timeDiv = readBigEndian<uint16_t>(chunk);
handler->onMidiHeader(type, trackCount, timeDiv);
/* Read tracks */
for (uint16_t i = 0; i < trackCount; ++i)
readMidiTrack(handler, chunk);
}
struct Track
{
std::vector<MidiEvent> 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<Track> 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<int16_t>(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<int16_t>(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<uint32_t>(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;
}

View File

@ -103,12 +103,10 @@ struct SDLSoundSource : ALDataSource
void seekToOffset(float seconds) void seekToOffset(float seconds)
{ {
Sound_Seek(sample, static_cast<uint32_t>(seconds * 1000)); if (seconds <= 0)
}
void reset()
{
Sound_Rewind(sample); Sound_Rewind(sample);
else
Sound_Seek(sample, static_cast<uint32_t>(seconds * 1000));
} }
uint32_t loopStartFrames() uint32_t loopStartFrames()
@ -116,6 +114,11 @@ struct SDLSoundSource : ALDataSource
/* Loops from the beginning of the file */ /* Loops from the beginning of the file */
return 0; return 0;
} }
bool setPitch(float)
{
return false;
}
}; };
ALDataSource *createSDLSource(SDL_RWops &ops, ALDataSource *createSDLSource(SDL_RWops &ops,

138
src/sharedmidistate.h Normal file
View File

@ -0,0 +1,138 @@
/*
** sharedmidistate.h
**
** This file is part of mkxp.
**
** Copyright (C) 2014 Jonas Kulla <Nyocurio@gmail.com>
**
** 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SHAREDMIDISTATE_H
#define SHAREDMIDISTATE_H
#include "config.h"
#include "debugwriter.h"
#include <fluidsynth.h>
#include <assert.h>
#include <vector>
#include <string>
#define SYNTH_INIT_COUNT 2
#define SYNTH_SAMPLERATE 44100
struct Synth
{
fluid_synth_t *synth;
bool inUse;
};
struct SharedMidiState
{
bool inited;
std::vector<Synth> 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

View File

@ -37,6 +37,10 @@
#include "binding.h" #include "binding.h"
#include "exception.h" #include "exception.h"
#ifdef MIDI
#include "sharedmidistate.h"
#endif
#include <unistd.h> #include <unistd.h>
#include <stdio.h> #include <stdio.h>
#include <string> #include <string>
@ -58,6 +62,10 @@ struct SharedStatePrivate
RGSSThreadData &rtData; RGSSThreadData &rtData;
Config &config; Config &config;
#ifdef MIDI
SharedMidiState midiState;
#endif
Graphics graphics; Graphics graphics;
Input input; Input input;
Audio audio; Audio audio;
@ -89,6 +97,9 @@ struct SharedStatePrivate
eThread(*threadData->ethread), eThread(*threadData->ethread),
rtData(*threadData), rtData(*threadData),
config(threadData->config), config(threadData->config),
#ifdef MIDI
midiState(threadData->config),
#endif
graphics(threadData), graphics(threadData),
fontState(threadData->config), fontState(threadData->config),
stampCounter(0) stampCounter(0)
@ -137,6 +148,12 @@ struct SharedStatePrivate
/* Reuse starting values */ /* Reuse starting values */
TEXFBO::allocEmpty(gpTexFBO, globalTexW, globalTexH); TEXFBO::allocEmpty(gpTexFBO, globalTexW, globalTexH);
TEXFBO::linkFBO(gpTexFBO); 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() ~SharedStatePrivate()
@ -212,6 +229,10 @@ GSATT(TexPool&, texPool)
GSATT(Quad&, gpQuad) GSATT(Quad&, gpQuad)
GSATT(SharedFontState&, fontState) GSATT(SharedFontState&, fontState)
#ifdef MIDI
GSATT(SharedMidiState&, midiState)
#endif
void SharedState::setBindingData(void *data) void SharedState::setBindingData(void *data)
{ {
p->bindingData = data; p->bindingData = data;

View File

@ -49,6 +49,10 @@ struct GlobalIBO;
struct Config; struct Config;
struct Vec2i; struct Vec2i;
#ifdef MIDI
struct SharedMidiState;
#endif
struct SharedState struct SharedState
{ {
void *bindingData(); void *bindingData();
@ -78,6 +82,10 @@ struct SharedState
SharedFontState &fontState(); SharedFontState &fontState();
Font &defaultFont(); Font &defaultFont();
#ifdef MIDI
SharedMidiState &midiState();
#endif
sigc::signal<void> prepareDraw; sigc::signal<void> prepareDraw;
unsigned int genTimeStamp(); unsigned int genTimeStamp();

View File

@ -157,6 +157,12 @@ struct VorbisSource : ALDataSource
void seekToOffset(float seconds) void seekToOffset(float seconds)
{ {
if (seconds <= 0)
{
ov_raw_seek(&vf, 0);
currentFrame = 0;
}
currentFrame = seconds * info.rate; currentFrame = seconds * info.rate;
if (loop.valid && currentFrame > loop.end) if (loop.valid && currentFrame > loop.end)
@ -205,7 +211,7 @@ struct VorbisSource : ALDataSource
if (loop.requested) if (loop.requested)
{ {
retStatus = ALDataSource::WrapAround; retStatus = ALDataSource::WrapAround;
reset(); seekToOffset(0);
} }
else else
{ {
@ -261,12 +267,6 @@ struct VorbisSource : ALDataSource
return retStatus; return retStatus;
} }
void reset()
{
ov_raw_seek(&vf, 0);
currentFrame = 0;
}
uint32_t loopStartFrames() uint32_t loopStartFrames()
{ {
if (loop.valid) if (loop.valid)
@ -274,6 +274,11 @@ struct VorbisSource : ALDataSource
else else
return 0; return 0;
} }
bool setPitch(float)
{
return false;
}
}; };
ALDataSource *createVorbisSource(SDL_RWops &ops, ALDataSource *createVorbisSource(SDL_RWops &ops,