#include #include "sfizz/Synth.h" #include "sfizz/MathHelpers.h" #include "sfizz/SfzHelpers.h" #include "sfizz/SIMDHelpers.h" #include "MidiHelpers.h" #include "cxxopts.hpp" #include #include #define LOG_ERROR(ostream) std::cerr << ostream << '\n' #define LOG_INFO(ostream) if (verbose) { std::cout << ostream << '\n'; } #define ERROR_IF(check, ostream) if ((check)) { LOG_ERROR(ostream); std::exit(-1); } struct CallbackData { sfz::Synth& synth; int trackIdx; double lastCallTime; double blockSizeInSeconds; bool finished; }; void midiCallback(const fmidi_event_t * event, void * cbdata) { auto data = reinterpret_cast(cbdata); if (event->type != fmidi_event_type::fmidi_event_message) return; switch (midi::status(event->data[0])) { case midi::noteOff: data->synth.noteOff(event->delta, event->data[1], event->data[2]); break; case midi::noteOn: data->synth.noteOn(event->delta, event->data[1], event->data[2]); break; case midi::polyphonicPressure: break; case midi::controlChange: data->synth.cc(event->delta, event->data[1], event->data[2]); break; case midi::programChange: break; case midi::channelPressure: break; case midi::pitchBend: data->synth.pitchWheel(event->delta, midi::buildAndCenterPitch(event->data[1], event->data[2])); break; case midi::systemMessage: break; } } void finishedCallback(void * cbdata) { auto data = reinterpret_cast(cbdata); data->finished = true; } int main(int argc, char** argv) { cxxopts::Options options("sfizz-render", "Render a midi file through an SFZ file using the sfizz library."); unsigned blockSize { 1024 }; int sampleRate { 48000 }; int trackNumber { -1 }; bool verbose { false }; bool help { false }; bool useEOT { false }; int oversampling { 1 }; options.add_options() ("sfz", "SFZ file", cxxopts::value()) ("midi", "Input midi file", cxxopts::value()) ("wav", "Output wav file", cxxopts::value()) ("b,blocksize", "Block size for the sfizz callbacks", cxxopts::value(blockSize)) ("s,samplerate", "Output sample rate", cxxopts::value(sampleRate)) ("t,track", "Track number to use", cxxopts::value(trackNumber)) ("oversampling", "Internal oversampling factor", cxxopts::value(oversampling)) ("v,verbose", "Verbose output", cxxopts::value(verbose)) ("log", "Produce logs", cxxopts::value()) ("use-eot", "End the rendering at the last End of Track Midi message", cxxopts::value(useEOT)) ("h,help", "Show help", cxxopts::value(help)) ; auto params = [&]() { try { return options.parse(argc, argv); } catch (std::exception& e) { LOG_ERROR(e.what()); std::exit(-1); } }(); if (help) { std::cout << options.help(); std::exit(0); } ERROR_IF(params.count("sfz") != 1, "Please specify a single SFZ file using --sfz"); ERROR_IF(params.count("wav") != 1, "Please specify a single WAV file using --wav"); ERROR_IF(params.count("midi") != 1, "Please specify a single MIDI file using --midi"); fs::path sfzPath = fs::current_path() / params["sfz"].as(); fs::path outputPath = fs::current_path() / params["wav"].as(); fs::path midiPath = fs::current_path() / params["midi"].as(); ERROR_IF(!fs::exists(sfzPath) || !fs::is_regular_file(sfzPath), "SFZ file " << sfzPath.string() << " does not exist or is not a regular file"); ERROR_IF(!fs::exists(midiPath) || !fs::is_regular_file(midiPath), "MIDI file " << midiPath.string() << " does not exist or is not a regular file"); if (fs::exists(outputPath)) { LOG_INFO("Output file " << outputPath.string() << " already exists and will be erased."); } LOG_INFO("SFZ file: " << sfzPath.string()); LOG_INFO("MIDI file: " << midiPath.string()); LOG_INFO("Output file: " << outputPath.string()); LOG_INFO("Block size: " << blockSize); LOG_INFO("Sample rate: " << sampleRate); sfz::Synth synth; synth.setSamplesPerBlock(blockSize); synth.setSampleRate(sampleRate); synth.enableFreeWheeling(); if (params.count("log") > 0) synth.enableLogging(params["log"].as()); const auto osFactor = [oversampling] { switch (oversampling){ case 1: return sfz::Oversampling::x1; case 2: return sfz::Oversampling::x2; case 4: return sfz::Oversampling::x4; case 5: return sfz::Oversampling::x8; default: LOG_ERROR("Bad oversampling factor: " << oversampling); std::exit(-1); } }(); synth.setOversamplingFactor(osFactor); ERROR_IF(!synth.loadSfzFile(sfzPath), "There was an error loading the SFZ file."); LOG_INFO(synth.getNumRegions() << " regions in the SFZ."); fmidi_smf_u midiFile { fmidi_smf_file_read(midiPath.c_str()) }; ERROR_IF(!midiFile, "Can't read " << midiPath); const auto* midiInfo = fmidi_smf_get_info(midiFile.get()); ERROR_IF(!midiInfo, "Can't get info on the midi file"); LOG_INFO( midiInfo->track_count << " tracks in the SMF."); ERROR_IF (trackNumber > midiInfo->track_count, "The track number " << trackNumber << " requested does not exist in the SMF file."); if (trackNumber >= 0) { LOG_INFO("-- Rendering only track number " << trackNumber); } if (useEOT) { LOG_INFO("-- Cutting the rendering at the last MIDI End of Track message"); } const auto trackIdx = trackNumber < 1 ? 0 : trackNumber - 1; SndfileHandle outputFile (outputPath, SFM_WRITE, SF_FORMAT_WAV | SF_FORMAT_PCM_16, 2, sampleRate); ERROR_IF(outputFile.error() != 0, "Error writing out the wav file: " << outputFile.strError()); auto sampleRateDouble = static_cast(sampleRate); double blockSizeInSeconds { blockSize / sampleRateDouble }; int numFramesWritten { 0 }; sfz::AudioBuffer audioBuffer { 2, blockSize }; sfz::Buffer interleavedBuffer { 2 * blockSize }; fmidi_player_u midiPlayer { fmidi_player_new(midiFile.get()) }; CallbackData callbackData { synth, trackIdx, 0.0, blockSizeInSeconds, false }; fmidi_player_event_callback(midiPlayer.get(), &midiCallback, &callbackData); fmidi_player_finish_callback(midiPlayer.get(), &finishedCallback, &callbackData); fmidi_player_start(midiPlayer.get()); while (!callbackData.finished) { fmidi_player_tick(midiPlayer.get(), blockSizeInSeconds); callbackData.lastCallTime += blockSizeInSeconds; synth.renderBlock(audioBuffer); sfz::writeInterleaved(audioBuffer.getConstSpan(0), audioBuffer.getConstSpan(1), absl::MakeSpan(interleavedBuffer)); numFramesWritten += outputFile.writef(interleavedBuffer.data(), blockSize); } if (!useEOT) { auto averagePower = sfz::meanSquared(interleavedBuffer); while (averagePower > 1e-12f) { callbackData.lastCallTime += blockSizeInSeconds; synth.renderBlock(audioBuffer); sfz::writeInterleaved(audioBuffer.getConstSpan(0), audioBuffer.getConstSpan(1), absl::MakeSpan(interleavedBuffer)); numFramesWritten += outputFile.writef(interleavedBuffer.data(), blockSize); averagePower = sfz::meanSquared(interleavedBuffer); } } outputFile.writeSync(); LOG_INFO("Wrote " << callbackData.lastCallTime << " seconds of sound data in" << outputPath.string() << " (" << numFramesWritten << " frames)"); return 0; }