mirror of
https://github.com/dwhinham/mt32-pi.git
synced 2026-10-27 06:21:59 +00:00
Add FluidSynth synthesizer
This commit is contained in:
1 parent
ff8cca440d
commit
61759d278c
6 files changed
+335
-7
No files matched your search
@@ -16,7 +16,8 @@ OBJS := src/config.o \
|
||||
src/mt32pi.o \
|
||||
src/power.o \
|
||||
src/rommanager.o \
|
||||
src/synth/mt32synth.o
|
||||
src/synth/mt32synth.o \
|
||||
src/synth/soundfontsynth.o
|
||||
|
||||
EXTRACLEAN += src/*.d src/*.o \
|
||||
src/lcd/*.d src/lcd/*.o \
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "midiparser.h"
|
||||
#include "power.h"
|
||||
#include "synth/mt32synth.h"
|
||||
#include "synth/soundfontsynth.h"
|
||||
|
||||
class CMT32Pi : public CMultiCoreSupport, CPower, CMIDIParser
|
||||
{
|
||||
@@ -110,7 +111,9 @@ private:
|
||||
CSoundBaseDevice* m_pSound;
|
||||
|
||||
// Synthesizers
|
||||
CSynthBase* m_pCurrentSynth;
|
||||
CMT32Synth* m_pMT32Synth;
|
||||
CSoundFontSynth* m_pSoundFontSynth;
|
||||
|
||||
static void USBMIDIPacketHandler(unsigned nCable, u8* pPacket, unsigned nLength);
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// soundfontsynth.h
|
||||
//
|
||||
// mt32-pi - A bare-metal Roland MT-32 emulator for Raspberry Pi
|
||||
// Copyright (C) 2020 Dale Whinham <daleyo@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#ifndef _soundfontsynth_h
|
||||
#define _soundfontsynth_h
|
||||
|
||||
#include <circle/types.h>
|
||||
|
||||
#include <fluidsynth.h>
|
||||
|
||||
#include "synth/synthbase.h"
|
||||
|
||||
class CSoundFontSynth : public CSynthBase
|
||||
{
|
||||
public:
|
||||
CSoundFontSynth(unsigned nSampleRate);
|
||||
virtual ~CSoundFontSynth() override;
|
||||
|
||||
// CSynthBase
|
||||
virtual bool Initialize() override;
|
||||
virtual void HandleMIDIShortMessage(u32 nMessage) override;
|
||||
virtual void HandleMIDISysExMessage(const u8* pData, size_t nSize) override;
|
||||
virtual bool IsActive() const override { return fluid_synth_get_active_voice_count(m_pSynth) > 0; }
|
||||
virtual void AllSoundOff() override;
|
||||
virtual size_t Render(s16* pOutBuffer, size_t nFrames) override;
|
||||
virtual size_t Render(float* pOutBuffer, size_t nFrames) override;
|
||||
|
||||
private:
|
||||
unsigned m_nSampleRate;
|
||||
|
||||
fluid_settings_t* m_pSettings;
|
||||
fluid_synth_t* m_pSynth;
|
||||
|
||||
static void FluidSynthLogCallback(int nLevel, const char* pMessage, void* pUser);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -91,6 +91,8 @@ output_device = pwm
|
||||
# mt32emu uses an internal samplerate of 32000Hz (just like the real hardware)
|
||||
# which is then resampled to this value.
|
||||
#
|
||||
# FluidSynth renders at this sample rate directly.
|
||||
#
|
||||
# Values: 32000-192000 (96000*)
|
||||
sample_rate = 96000
|
||||
|
||||
|
||||
+28
-6
@@ -64,7 +64,9 @@ CMT32Pi::CMT32Pi(CI2CMaster* pI2CMaster, CInterruptSystem* pInterrupt, CSerialDe
|
||||
m_nLEDOnTime(0),
|
||||
|
||||
m_pSound(nullptr),
|
||||
m_pMT32Synth(nullptr)
|
||||
m_pCurrentSynth(nullptr),
|
||||
m_pMT32Synth(nullptr),
|
||||
m_pSoundFontSynth(nullptr)
|
||||
{
|
||||
s_pThis = this;
|
||||
}
|
||||
@@ -140,6 +142,26 @@ bool CMT32Pi::Initialize(bool bSerialMIDIEnabled)
|
||||
if (config.MT32EmuMIDIChannels == CMT32Synth::TMIDIChannels::Alternate)
|
||||
m_pMT32Synth->SetMIDIChannels(config.MT32EmuMIDIChannels);
|
||||
|
||||
LCDLog(TLCDLogType::Startup, "Init FluidSynth");
|
||||
m_pSoundFontSynth = new CSoundFontSynth(config.AudioSampleRate);
|
||||
if (!m_pSoundFontSynth->Initialize())
|
||||
{
|
||||
logger.Write(MT32PiName, LogWarning, "FluidSynth init failed; no SoundFonts present?");
|
||||
delete m_pSoundFontSynth;
|
||||
m_pSoundFontSynth = nullptr;
|
||||
}
|
||||
|
||||
// TODO: Config option
|
||||
// Set initial synthesizer
|
||||
m_pCurrentSynth = m_pMT32Synth;
|
||||
|
||||
if (!m_pCurrentSynth)
|
||||
{
|
||||
logger.Write(MT32PiName, LogError, "No synthesizers initialized successfully");
|
||||
LCDLog(TLCDLogType::Startup, "Synth init failed!");
|
||||
return false;
|
||||
}
|
||||
|
||||
CUSBMIDIDevice* pMIDIDevice = static_cast<CUSBMIDIDevice*>(CDeviceNameService::Get()->GetDevice("umidi1", FALSE));
|
||||
if (pMIDIDevice)
|
||||
{
|
||||
@@ -190,13 +212,13 @@ void CMT32Pi::MainTask()
|
||||
// Check for active sensing timeout
|
||||
if (m_bActiveSenseFlag && (ticks > m_nActiveSenseTime) && (ticks - m_nActiveSenseTime) >= MSEC2HZ(ACTIVE_SENSE_TIMEOUT_MILLIS))
|
||||
{
|
||||
m_pMT32Synth->AllSoundOff();
|
||||
m_pCurrentSynth->AllSoundOff();
|
||||
m_bActiveSenseFlag = false;
|
||||
logger.Write(MT32PiName, LogNotice, "Active sense timeout - turning notes off");
|
||||
}
|
||||
|
||||
// Update power management
|
||||
if (m_pMT32Synth->IsActive())
|
||||
if (m_pCurrentSynth->IsActive())
|
||||
Awaken();
|
||||
|
||||
CPower::Update();
|
||||
@@ -258,7 +280,7 @@ void CMT32Pi::AudioTask()
|
||||
const size_t nFrames = nQueueSize - m_pSound->GetQueueFramesAvail();
|
||||
const size_t nWriteBytes = nFrames * 2 * sizeof(s16);
|
||||
|
||||
m_pMT32Synth->Render(Buffer, nFrames);
|
||||
m_pCurrentSynth->Render(Buffer, nFrames);
|
||||
int nResult = m_pSound->Write(Buffer, nWriteBytes);
|
||||
|
||||
if (nResult != static_cast<int>(nWriteBytes))
|
||||
@@ -330,7 +352,7 @@ void CMT32Pi::OnShortMessage(u32 nMessage)
|
||||
// Wake from power saving mode if necessary
|
||||
Awaken();
|
||||
|
||||
m_pMT32Synth->HandleMIDIShortMessage(nMessage);
|
||||
m_pCurrentSynth->HandleMIDIShortMessage(nMessage);
|
||||
}
|
||||
|
||||
void CMT32Pi::OnSysExMessage(const u8* pData, size_t nSize)
|
||||
@@ -343,7 +365,7 @@ void CMT32Pi::OnSysExMessage(const u8* pData, size_t nSize)
|
||||
|
||||
// If we don't consume the SysEx message, forward it to mt32emu
|
||||
if (!ParseCustomSysEx(pData, nSize))
|
||||
m_pMT32Synth->HandleMIDISysExMessage(pData, nSize);
|
||||
m_pCurrentSynth->HandleMIDISysExMessage(pData, nSize);
|
||||
}
|
||||
|
||||
void CMT32Pi::OnUnexpectedStatus()
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
//
|
||||
// soundfontsynth.cpp
|
||||
//
|
||||
// mt32-pi - A bare-metal Roland MT-32 emulator for Raspberry Pi
|
||||
// Copyright (C) 2020 Dale Whinham <daleyo@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#include <fatfs/ff.h>
|
||||
#include <circle/logger.h>
|
||||
#include <circle/timer.h>
|
||||
|
||||
#include "synth/soundfontsynth.h"
|
||||
#include "utility.h"
|
||||
|
||||
const char SoundFontSynthName[] = "soundfontsynth";
|
||||
const char SoundFontPath[] = "soundfonts";
|
||||
|
||||
extern "C"
|
||||
{
|
||||
// Replacements for fluid_sys.c functions
|
||||
FILE* fluid_file_open(const char* path, const char** errMsg)
|
||||
{
|
||||
FILE* handle = fopen(path, "rb");
|
||||
|
||||
if (!handle && errMsg)
|
||||
*errMsg = "Failed to open file";
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
void fluid_msleep(unsigned int msecs) { CTimer::SimpleMsDelay(msecs); }
|
||||
double fluid_utime() { return static_cast<double>(CTimer::GetClockTicks()); }
|
||||
|
||||
// Replacements for fluid_sfont.c functions
|
||||
// These were found to be much faster than FluidSynth's default approach of going through libc
|
||||
void* default_fopen(const char* path)
|
||||
{
|
||||
FIL* pFile = new FIL;
|
||||
if (f_open(pFile, path, FA_READ) != FR_OK)
|
||||
{
|
||||
delete pFile;
|
||||
pFile = nullptr;
|
||||
}
|
||||
|
||||
return pFile;
|
||||
}
|
||||
|
||||
int default_fclose(void* handle)
|
||||
{
|
||||
FIL* pFile = static_cast<FIL*>(handle);
|
||||
|
||||
if (f_close(pFile) == FR_OK)
|
||||
{
|
||||
delete pFile;
|
||||
return FLUID_OK;
|
||||
}
|
||||
|
||||
return FLUID_FAILED;
|
||||
}
|
||||
|
||||
long default_ftell(void* handle)
|
||||
{
|
||||
FIL* pFile = static_cast<FIL*>(handle);
|
||||
return f_tell(pFile);
|
||||
}
|
||||
|
||||
int safe_fread(void* buf, int count, void* fd)
|
||||
{
|
||||
FIL* pFile = static_cast<FIL*>(fd);
|
||||
UINT nRead;
|
||||
return f_read(pFile, buf, count, &nRead) == FR_OK ? FLUID_OK : FLUID_FAILED;
|
||||
}
|
||||
|
||||
int safe_fseek(void* fd, long ofs, int whence)
|
||||
{
|
||||
FIL* pFile = static_cast<FIL*>(fd);
|
||||
|
||||
switch (whence)
|
||||
{
|
||||
case SEEK_CUR:
|
||||
ofs += f_tell(pFile);
|
||||
break;
|
||||
|
||||
case SEEK_END:
|
||||
ofs += f_size(pFile);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return f_lseek(pFile, ofs) == FR_OK ? FLUID_OK : FLUID_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
CSoundFontSynth::CSoundFontSynth(unsigned nSampleRate)
|
||||
: m_nSampleRate(nSampleRate),
|
||||
|
||||
m_pSettings(nullptr),
|
||||
m_pSynth(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
CSoundFontSynth::~CSoundFontSynth()
|
||||
{
|
||||
if (m_pSynth)
|
||||
delete_fluid_synth(m_pSynth);
|
||||
|
||||
if (m_pSettings)
|
||||
delete_fluid_settings(m_pSettings);
|
||||
}
|
||||
|
||||
void CSoundFontSynth::FluidSynthLogCallback(int nLevel, const char* pMessage, void* pUser)
|
||||
{
|
||||
CLogger::Get()->Write(SoundFontSynthName, static_cast<TLogSeverity>(nLevel), pMessage);
|
||||
}
|
||||
|
||||
bool CSoundFontSynth::Initialize()
|
||||
{
|
||||
// Install logging handlers
|
||||
fluid_set_log_function(FLUID_PANIC, FluidSynthLogCallback, this);
|
||||
fluid_set_log_function(FLUID_ERR, FluidSynthLogCallback, this);
|
||||
// fluid_set_log_function(FLUID_WARN, FluidSynthLogCallback, this);
|
||||
// fluid_set_log_function(FLUID_INFO, FluidSynthLogCallback, this);
|
||||
// fluid_set_log_function(FLUID_DBG, FluidSynthLogCallback, this);
|
||||
|
||||
CLogger::Get()->Write(SoundFontSynthName, LogNotice, "Loading soundfont");
|
||||
|
||||
m_pSettings = new_fluid_settings();
|
||||
if (!m_pSettings)
|
||||
{
|
||||
CLogger::Get()->Write(SoundFontSynthName, LogError, "Failed to create settings");
|
||||
return false;
|
||||
}
|
||||
|
||||
fluid_settings_setnum(m_pSettings, "synth.sample-rate", static_cast<double>(m_nSampleRate));
|
||||
fluid_settings_setint(m_pSettings, "synth.threadsafe-api", false);
|
||||
|
||||
m_pSynth = new_fluid_synth(m_pSettings);
|
||||
if (!m_pSynth)
|
||||
{
|
||||
CLogger::Get()->Write(SoundFontSynthName, LogError, "Failed to create synth");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fluid_synth_sfload(m_pSynth, "soundfonts/SC-55.sf2", true) == FLUID_FAILED)
|
||||
{
|
||||
CLogger::Get()->Write(SoundFontSynthName, LogError, "Failed to load SoundFont");
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Expose in config file
|
||||
fluid_synth_set_polyphony(m_pSynth, 128);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CSoundFontSynth::HandleMIDIShortMessage(u32 nMessage)
|
||||
{
|
||||
const u8 status = nMessage & 0xFF;
|
||||
const u8 channel = nMessage & 0xF;
|
||||
const u8 data1 = (nMessage >> 8) & 0xFF;
|
||||
const u8 data2 = (nMessage >> 16) & 0xFF;
|
||||
|
||||
// Handle system real-time messages
|
||||
if (status == 0xFF)
|
||||
{
|
||||
fluid_synth_system_reset(m_pSynth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle channel messages
|
||||
switch (status & 0xF0)
|
||||
{
|
||||
// Note off
|
||||
case 0x80:
|
||||
fluid_synth_noteoff(m_pSynth, channel, data1);
|
||||
break;
|
||||
|
||||
// Note on
|
||||
case 0x90:
|
||||
fluid_synth_noteon(m_pSynth, channel, data1, data2);
|
||||
break;
|
||||
|
||||
// Polyphonic key pressure/aftertouch
|
||||
case 0xA0:
|
||||
fluid_synth_key_pressure(m_pSynth, channel, data1, data2);
|
||||
break;
|
||||
|
||||
// Control change
|
||||
case 0xB0:
|
||||
fluid_synth_cc(m_pSynth, channel, data1, data2);
|
||||
break;
|
||||
|
||||
// Program change
|
||||
case 0xC0:
|
||||
fluid_synth_program_change(m_pSynth, channel, data1);
|
||||
break;
|
||||
|
||||
// Channel pressure/aftertouch
|
||||
case 0xD0:
|
||||
fluid_synth_channel_pressure(m_pSynth, channel, data1);
|
||||
break;
|
||||
|
||||
// Pitch bend
|
||||
case 0xE0:
|
||||
fluid_synth_pitch_bend(m_pSynth, channel, (data2 << 7) | data1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CSoundFontSynth::HandleMIDISysExMessage(const u8* pData, size_t nSize)
|
||||
{
|
||||
// Exclude leading 0xF0 and trailing 0xF7
|
||||
fluid_synth_sysex(m_pSynth, reinterpret_cast<const char*>(pData + 1), nSize - 1, nullptr, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
void CSoundFontSynth::AllSoundOff()
|
||||
{
|
||||
fluid_synth_all_sounds_off(m_pSynth, -1);
|
||||
}
|
||||
|
||||
size_t CSoundFontSynth::Render(float* pOutBuffer, size_t nFrames)
|
||||
{
|
||||
assert(fluid_synth_write_float(m_pSynth, nFrames, pOutBuffer, 0, 2, pOutBuffer, 1, 2) == FLUID_OK);
|
||||
return nFrames;
|
||||
}
|
||||
|
||||
size_t CSoundFontSynth::Render(s16* pOutBuffer, size_t nFrames)
|
||||
{
|
||||
assert(fluid_synth_write_s16(m_pSynth, nFrames, pOutBuffer, 0, 2, pOutBuffer, 1, 2) == FLUID_OK);
|
||||
return nFrames;
|
||||
}
|
||||
Reference in new issue
Block a user