2020-08-05 01:00:01 +02:00
|
|
|
// SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
|
|
|
|
|
// This code is part of the sfizz library and is licensed under a BSD 2-clause
|
|
|
|
|
// license. You should have receive a LICENSE.md file along with the code.
|
|
|
|
|
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
This program generates the data file of a LFO output recorded for a fixed
|
|
|
|
|
duration. The file contains columns for each LFO in the SFZ region.
|
|
|
|
|
The columns are: Time, Lfo1, ... LfoN
|
|
|
|
|
One can use Gnuplot to display this data.
|
|
|
|
|
Example:
|
|
|
|
|
sfizz_plot_lfo file.sfz > lfo.dat
|
|
|
|
|
gnuplot
|
|
|
|
|
plot "lfo.dat" using 1:2 with lines
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "sfizz/Synth.h"
|
|
|
|
|
#include "sfizz/LFO.h"
|
2020-11-01 09:00:22 +01:00
|
|
|
#include "sfizz/Region.h"
|
2020-08-05 01:00:01 +02:00
|
|
|
#include "sfizz/LFODescription.h"
|
2020-08-13 03:48:10 +02:00
|
|
|
#include "sfizz/MathHelpers.h"
|
2020-08-13 03:34:03 +02:00
|
|
|
#include "cxxopts.hpp"
|
2020-08-05 01:00:01 +02:00
|
|
|
#include <absl/types/span.h>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <iostream>
|
2021-03-12 17:31:05 +01:00
|
|
|
#include <fstream>
|
2020-08-05 01:00:01 +02:00
|
|
|
#include <cmath>
|
2020-08-13 03:48:10 +02:00
|
|
|
#ifdef _WIN32
|
|
|
|
|
#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1
|
|
|
|
|
#endif
|
|
|
|
|
#include <sndfile.hh>
|
2020-08-05 01:00:01 +02:00
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
|
|
2020-08-13 03:34:03 +02:00
|
|
|
static double sampleRate = 1000.0; // sample rate used to compute
|
|
|
|
|
static double duration = 5.0; // length in seconds
|
2020-08-13 03:48:10 +02:00
|
|
|
static std::string outputFilename;
|
|
|
|
|
static bool saveFlac = false;
|
2020-08-05 01:00:01 +02:00
|
|
|
|
|
|
|
|
static std::vector<sfz::LFODescription> lfoDescriptionFromSfzFile(const fs::path &sfzPath, bool &success)
|
|
|
|
|
{
|
|
|
|
|
sfz::Synth synth;
|
|
|
|
|
|
|
|
|
|
if (!synth.loadSfzFile(sfzPath)) {
|
|
|
|
|
std::cerr << "Cannot load the SFZ file.\n";
|
|
|
|
|
success = false;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (synth.getNumRegions() != 1) {
|
|
|
|
|
std::cerr << "The SFZ file must contain exactly one region.\n";
|
|
|
|
|
success = false;
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
success = true;
|
|
|
|
|
return synth.getRegionView(0)->lfos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
Program which loads LFO configuration and generates plot data for the given duration.
|
|
|
|
|
*/
|
|
|
|
|
int main(int argc, char* argv[])
|
|
|
|
|
{
|
2020-08-13 03:34:03 +02:00
|
|
|
cxxopts::Options options("sfizz_plot_lfo", "Compute LFO and generate plot data");
|
|
|
|
|
|
|
|
|
|
options.add_options()
|
|
|
|
|
("s,samplerate", "Sample rate", cxxopts::value(sampleRate))
|
|
|
|
|
("d,duration", "Duration", cxxopts::value(duration))
|
2020-08-13 03:48:10 +02:00
|
|
|
("o,output", "Output file", cxxopts::value(outputFilename))
|
|
|
|
|
("F,flac", "Save output as FLAC", cxxopts::value(saveFlac))
|
2020-08-13 03:34:03 +02:00
|
|
|
("h,help", "Print usage")
|
|
|
|
|
;
|
|
|
|
|
options.positional_help("sfz-file");
|
|
|
|
|
|
|
|
|
|
fs::path sfzPath;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
cxxopts::ParseResult result = options.parse(argc, argv);
|
|
|
|
|
options.parse_positional({ "sfz-file" });
|
|
|
|
|
|
|
|
|
|
if (result.count("help")) {
|
|
|
|
|
std::cout << options.help() << std::endl;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (argc != 2) {
|
|
|
|
|
std::cerr << "Please indicate the SFZ file to process.\n";
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sfzPath = argv[1];
|
|
|
|
|
}
|
|
|
|
|
catch (cxxopts::OptionException& ex) {
|
|
|
|
|
std::cerr << ex.what() << "\n";
|
2020-08-05 01:00:01 +02:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool success = false;
|
|
|
|
|
const std::vector<sfz::LFODescription> desc = lfoDescriptionFromSfzFile(sfzPath, success);
|
2020-08-13 03:34:03 +02:00
|
|
|
if (!success){
|
|
|
|
|
std::cerr << "Could not extract LFO descriptions from SFZ file.\n";
|
2020-08-05 01:00:01 +02:00
|
|
|
return 1;
|
2020-08-13 03:34:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sampleRate <= 0) {
|
|
|
|
|
std::cerr << "The sample rate provided is invalid.\n";
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
2020-08-05 01:00:01 +02:00
|
|
|
|
2021-02-01 13:05:15 +01:00
|
|
|
constexpr size_t bufferSize = 1024;
|
2021-03-31 00:03:49 +02:00
|
|
|
sfz::Resources resources;
|
|
|
|
|
resources.setSamplesPerBlock(bufferSize);
|
2020-11-15 13:23:59 +01:00
|
|
|
|
2020-08-05 01:00:01 +02:00
|
|
|
size_t numLfos = desc.size();
|
2020-11-15 13:23:59 +01:00
|
|
|
std::vector<std::unique_ptr<sfz::LFO>> lfos(numLfos);
|
2020-08-05 01:00:01 +02:00
|
|
|
|
|
|
|
|
for (size_t l = 0; l < numLfos; ++l) {
|
2021-03-31 00:03:49 +02:00
|
|
|
sfz::LFO* lfo = new sfz::LFO(resources);
|
2020-11-15 13:23:59 +01:00
|
|
|
lfos[l].reset(lfo);
|
|
|
|
|
lfo->setSampleRate(sampleRate);
|
|
|
|
|
lfo->configure(&desc[l]);
|
2020-08-05 01:00:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t numFrames = (size_t)std::ceil(sampleRate * duration);
|
|
|
|
|
std::vector<float> outputMemory(numLfos * numFrames);
|
|
|
|
|
|
2020-08-11 23:46:08 +02:00
|
|
|
for (size_t l = 0; l < numLfos; ++l) {
|
2020-11-15 13:23:59 +01:00
|
|
|
lfos[l]->start(0);
|
2020-08-11 23:46:08 +02:00
|
|
|
}
|
|
|
|
|
|
2020-08-05 01:00:01 +02:00
|
|
|
std::vector<absl::Span<float>> lfoOutputs(numLfos);
|
|
|
|
|
for (size_t l = 0; l < numLfos; ++l) {
|
|
|
|
|
lfoOutputs[l] = absl::MakeSpan(&outputMemory[l * numFrames], numFrames);
|
2021-02-01 13:05:15 +01:00
|
|
|
for (size_t i = 0, currentFrames; i < numFrames; i += currentFrames) {
|
|
|
|
|
currentFrames = std::min(numFrames - i, bufferSize);
|
|
|
|
|
lfos[l]->process(lfoOutputs[l].subspan(i, currentFrames));
|
|
|
|
|
}
|
2020-08-05 01:00:01 +02:00
|
|
|
}
|
|
|
|
|
|
2020-08-13 03:48:10 +02:00
|
|
|
if (saveFlac) {
|
|
|
|
|
if (outputFilename.empty()) {
|
|
|
|
|
std::cerr << "Please indicate the audio file to save.\n";
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs::path outputPath = fs::u8path(outputFilename);
|
|
|
|
|
SndfileHandle snd(
|
|
|
|
|
#ifndef _WIN32
|
|
|
|
|
outputPath.c_str(),
|
|
|
|
|
#else
|
|
|
|
|
outputPath.wstring().c_str(),
|
|
|
|
|
#endif
|
|
|
|
|
SFM_WRITE, SF_FORMAT_FLAC|SF_FORMAT_PCM_16, numLfos, sampleRate);
|
|
|
|
|
|
|
|
|
|
std::unique_ptr<float[]> frame(new float[numLfos]);
|
|
|
|
|
size_t numClips = 0;
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < numFrames; ++i) {
|
|
|
|
|
for (size_t l = 0; l < numLfos; ++l) {
|
|
|
|
|
float orig = lfoOutputs[l][i];
|
|
|
|
|
float clamped = clamp(orig, -1.0f, 1.0f);
|
|
|
|
|
numClips += clamped != orig;
|
|
|
|
|
frame[l] = clamped;
|
|
|
|
|
}
|
|
|
|
|
snd.writef(frame.get(), 1);
|
|
|
|
|
}
|
|
|
|
|
snd.writeSync();
|
|
|
|
|
|
|
|
|
|
if (snd.error()) {
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
fs::remove(outputPath, ec);
|
|
|
|
|
std::cerr << "Could not save audio to the output file.\n";
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (numClips > 0)
|
|
|
|
|
std::cerr << "Warning: the audio output has been clipped on " << numClips << " frames.\n";
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
std::ostream* os = &std::cout;
|
|
|
|
|
fs::ofstream of;
|
|
|
|
|
|
|
|
|
|
fs::path outputPath;
|
|
|
|
|
if (!outputFilename.empty()) {
|
|
|
|
|
outputPath = fs::u8path(outputFilename);
|
|
|
|
|
of.open(outputPath);
|
|
|
|
|
os = &of;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < numFrames; ++i) {
|
|
|
|
|
*os << (i / sampleRate);
|
|
|
|
|
for (size_t l = 0; l < numLfos; ++l)
|
|
|
|
|
*os << ' ' << lfoOutputs[l][i];
|
|
|
|
|
*os << '\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (os == &of) {
|
|
|
|
|
of.flush();
|
|
|
|
|
if (!of) {
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
fs::remove(outputPath, ec);
|
|
|
|
|
std::cerr << "Could not save data to the output file.\n";
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-05 01:00:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|