QtFM::Engine Class
The Engine class plays patches in real time, or renders them offline. More…
| Header | #include <QtFM> |
|---|---|
| CMake | target_link_libraries(app PRIVATE Nt::FM) |
| Module | Nt FM C++ Classes |
| Inherits | QObject |
List of all members, including inherited members
Public Functions
| Engine(const EngineConfig &config = {}, QObject *parent = nullptr) | |
| ~Engine() | |
| EngineConfig | config() const |
| bool | start(AudioOutput *output = nullptr) |
| void | stop() |
| bool | isStarted() const |
| VoiceHandle | play(const Patch &patch, const PlayOptions &options = {}) |
| QList<VoiceHandle> | play(const Sequence &sequence, const PlayOptions &options = {}) |
| VoiceHandle | playAt(const Patch &patch, const PlayOptions &options, quint64 frame) |
| quint64 | frame() const |
| void | stopAll(bool immediate = false) |
| Chip * | chip(ChipType type) const |
| MusicPlayer * | music() const |
| int | render(float *interleaved, int frames) |
| int | render(qint16 *interleaved, int frames) |
| void | setBusGain(int bus, float dB) |
| float | busGain(int bus) const |
| void | setMasterGain(float dB) |
| float | masterGain() const |
| int | activeVoices() const |
Signals
| void | voiceFinished(quint32 voiceId) |
| void | underrun() |
| void | musicFinished() |
Static Public Members
| QByteArray | renderToWav(const Patch &patch, int sampleRate = 48000, const PlayOptions &options = {}) |
Detailed Description
An engine owns a fixed number of voices. play() starts a patch on one and returns a VoiceHandle; when every voice is busy, the voice of lowest priority, then the quietest, then the oldest fades out in 2 ms to make room.
Engine engine;
if (!engine.start()) {
std::fprintf(stderr, "fmplay: no sound output\n");
return 1;
}
const Patch coin = Patch::generate(Category::Pickup, 3);
engine.play(coin);
start() connects the engine to a sound output, which renders on its own audio thread. Without start(), call render() to pull samples, for example into a mixer of the application's own or into a file.
Threads
play(), stopAll(), the gains and every VoiceHandle call are thread-safe and never wait for the audio thread: they send fixed-size commands through a lock-free queue. The audio thread never locks, allocates or touches a QObject: everything it needs is allocated by the constructor, and patches are compiled on the calling thread. render() must be called from one thread at a time. Denormals are flushed to zero while it runs.
Finished voices and underruns are reported by voiceFinished() and underrun(), emitted from the engine's thread about 60 times a second, so they need its event loop.
Chips
An engine configured with chips in EngineConfig::chips runs their emulation cores in the same render. chip() gives register-level access, for ports of DOS games and for trackers. A patch made for the OPL2 or OPL3 (Patch::chip()) plays on a free channel of such a chip instead of the native engine, through the same play() call; music() plays VGM, DRO and IMF files on cores of its own.
See also QtFM::Patch, QtFM::AudioOutput and Using Nt FM.
Differences from Qt
Only the OPL2 and OPL3 are emulated. A voice on a chip channel follows its note, duration and pitch changes only: pitch envelopes, arpeggios and the other native parameters do not apply. There is no 2x oversampling, and no fixed-point synthesis path: render() into qint16 converts the floating-point output. The default output of start() is ALSA on Linux; elsewhere, pass an AudioOutput.
Member Function Documentation
QtFM::Engine::Engine(const EngineConfig &config = {}, QObject *parent = nullptr)
Constructs an engine with config, as a child of parent. All the memory that the audio thread uses is allocated here.
QtFM::Engine::~Engine()
Destroys the engine, stopping its output. Handles to its voices stop playing.
int QtFM::Engine::activeVoices() const
Returns the number of voices playing at the end of the last render().
float QtFM::Engine::busGain(int bus) const
Returns the gain of bus in decibels.
Chip * QtFM::Engine::chip(ChipType type) const
Returns the chip of type that the engine was configured with, for register-level access, or nullptr.
See also QtFM::EngineConfig::chips.
EngineConfig QtFM::Engine::config() const
Returns the configuration of the engine. Its seed is the one in use, even if the configuration passed to the constructor asked for a random one.
quint64 QtFM::Engine::frame() const
Returns the number of frames rendered so far, as of the last render().
See also playAt().
bool QtFM::Engine::isStarted() const
Returns true if the engine renders into an output.
float QtFM::Engine::masterGain() const
Returns the gain of the mix in decibels.
MusicPlayer * QtFM::Engine::music() const
Returns the engine's music player.
void QtFM::Engine::musicFinished()
This signal is emitted when music that does not loop has reached its end.
See also QtFM::MusicPlayer.
VoiceHandle QtFM::Engine::play(const Patch &patch, const PlayOptions &options = {})
Starts playing patch with options and returns a handle to the voice, or an invalid handle if the patch is invalid or the command queue is full. The patch is compiled, with its random variance drawn, on the calling thread.
QList<VoiceHandle> QtFM::Engine::play(const Sequence &sequence, const PlayOptions &options = {})
This is an overloaded function.
Starts every note of sequence, each on its exact sample, with options applied to all of them; the delay of options delays the whole sequence. Returns a handle to each voice.
VoiceHandle QtFM::Engine::playAt(const Patch &patch, const PlayOptions &options, quint64 frame)
Starts playing patch with options at the absolute frame of the engine's output, as frame() counts it, and returns a handle to the voice; the delay of options is ignored. A frame already rendered starts the voice at once. A scheduler that streams notes, such as MidiPlayer, places each one on its sample this way.
See also frame().
int QtFM::Engine::render(float *interleaved, int frames)
Renders frames frames of interleaved stereo into interleaved and returns the number of frames rendered. A sound output calls it on its audio thread; without start(), call it to pull samples or to render offline.
int QtFM::Engine::render(qint16 *interleaved, int frames)
This is an overloaded function.
Renders frames frames of interleaved stereo into interleaved as 16-bit samples.
QByteArray QtFM::Engine::renderToWav(const Patch &patch, int sampleRate = 48000, const PlayOptions &options = {})
Renders patch once, played with options at sampleRate, and returns it as a 16-bit stereo WAV file. The render ends when the voice does, or after 60 seconds; a looping patch without a duration is released after 2 seconds. A seed of 0 in options means 1, so the same patch always gives the same file.
const QByteArray wav = Engine::renderToWav(patch, rate, options);
QFile file(parser.value(outOption));
if (!file.open(QIODevice::WriteOnly) || file.write(wav) != wav.size())
return 1;
void QtFM::Engine::setBusGain(int bus, float dB)
Sets the gain of bus (0 for effects, 1 for music, 2 for the user interface) to dB.
void QtFM::Engine::setMasterGain(float dB)
Sets the gain of the mix, before its soft-clip limiter, to dB.
bool QtFM::Engine::start(AudioOutput *output = nullptr)
Starts rendering into output, opening it at the engine's sample rate, in stereo. If output is nullptr, the engine opens the platform's default output: the ALSA default device on Linux. Returns false if the output cannot be opened, or the engine is already started. The engine does not take ownership of output.
See also stop().
void QtFM::Engine::stop()
Stops rendering and closes the output. Voices keep their state, and continue if the engine is started again.
void QtFM::Engine::stopAll(bool immediate = false)
Releases every voice, including those waiting to start; if immediate is true, they fade out in 2 ms instead.
void QtFM::Engine::underrun()
This signal is emitted when the sound output ran out of samples, and played silence.
void QtFM::Engine::voiceFinished(quint32 voiceId)
This signal is emitted when the voice voiceId has ended, or was dropped because no voice could be stolen for it.
See also QtFM::VoiceHandle::id().