AquaSynth/tools/zyn-reference/ZynPadReference.cpp
2026-05-16 05:49:44 +01:00

244 lines
7.6 KiB
C++

/*
Test-only AquaSynth ZynAddSubFX PAD oracle.
This file links against the pinned ZynAddSubFX reference source in an ignored
artifact build. It is development/parity tooling, not AquaSynth runtime code.
*/
#include "../globals.h"
#include "../DSP/FFTwrapper.h"
#include "../Misc/Allocator.h"
#include "../Misc/Time.h"
#include "../Misc/Util.h"
#include "../Misc/XMLwrapper.h"
#include "../Params/Controller.h"
#include "../Params/PADnoteParameters.h"
#include "../Synth/OscilGen.h"
#include "../Synth/PADnote.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
using namespace zyn;
namespace {
bool enterInstrument(XMLwrapper& xml)
{
if(xml.enterbranch("INSTRUMENT"))
return true;
if(xml.enterbranch("MASTER")) {
if(xml.enterbranch("PART", 0) && xml.enterbranch("INSTRUMENT"))
return true;
}
return false;
}
int parseInt(const char* text, const char* name)
{
char* end = nullptr;
const long value = std::strtol(text, &end, 10);
if(end == text || *end != '\0') {
std::cerr << "Invalid " << name << ": " << text << "\n";
std::exit(2);
}
return static_cast<int>(value);
}
void writeFloat32(const std::string& path, const float* samples, int count)
{
std::ofstream output(path, std::ios::binary);
if(!output) {
std::cerr << "Could not open output file: " << path << "\n";
std::exit(2);
}
output.write(reinterpret_cast<const char*>(samples), sizeof(float) * count);
if(!output) {
std::cerr << "Could not write output file: " << path << "\n";
std::exit(2);
}
}
void normalizeMaxLikePad(float* values, int count)
{
float max = 0.0f;
for(int i = 0; i < count; ++i)
max = std::max(max, std::fabs(values[i]));
if(max > 0.000001f)
for(int i = 0; i < count; ++i)
values[i] /= max;
}
void writeHarmonics(
PADnoteParameters& parameters,
const SYNTH_T& synth,
const std::string& path,
float baseFrequency)
{
std::vector<float> harmonics(synth.oscilsize, 0.0f);
parameters.oscilgen->get(harmonics.data(), baseFrequency, true);
normalizeMaxLikePad(harmonics.data(), synth.oscilsize / 2);
writeFloat32(path, harmonics.data(), synth.oscilsize / 2);
}
int selectedSampleIndex(const PADnoteParameters& parameters, float frequency)
{
const float noteLog2Frequency = std::log2(frequency);
const float detune = getdetune(
parameters.PDetuneType,
parameters.PCoarseDetune,
parameters.PDetune);
const float log2Frequency = noteLog2Frequency + detune / 1200.0f;
float minDistance = std::fabs(log2Frequency - std::log2(parameters.sample[0].basefreq + 0.0001f));
int selected = 0;
for(int i = 1; i < PAD_MAX_SAMPLES; ++i) {
if(parameters.sample[i].smp == nullptr)
break;
const float distance = std::fabs(log2Frequency - std::log2(parameters.sample[i].basefreq + 0.0001f));
if(distance < minDistance) {
selected = i;
minDistance = distance;
}
}
return selected;
}
void renderNote(
const PADnoteParameters& parameters,
const SYNTH_T& synth,
const AbsTime& time,
const std::string& path,
float frequency,
float seconds)
{
Alloc memory;
Controller controller(synth, &time);
const float log2Frequency = std::log2(frequency);
SynthParams synthParams{memory, controller, synth, time, 120, nullptr, log2Frequency, false, prng()};
PADnote note(&parameters, synthParams, 0, nullptr, nullptr, false);
const int total = std::max(1, static_cast<int>(seconds * synth.samplerate));
std::vector<float> rendered(total);
std::vector<float> left(synth.buffersize);
std::vector<float> right(synth.buffersize);
int written = 0;
while(written < total) {
std::fill(left.begin(), left.end(), 0.0f);
std::fill(right.begin(), right.end(), 0.0f);
note.noteout(left.data(), right.data());
const int count = std::min(synth.buffersize, total - written);
std::copy(left.begin(), left.begin() + count, rendered.begin() + written);
written += count;
}
writeFloat32(path, rendered.data(), static_cast<int>(rendered.size()));
}
}
int main(int argc, char** argv)
{
if(argc < 3 || argc > 7) {
std::cerr << "Usage: ZynPadReference <input.xiz> <output.f32> [kit-index] [sample-index|selected|note|harmonics] [frequency-hz] [seconds]\n";
return 2;
}
const std::string inputPath = argv[1];
const std::string outputPath = argv[2];
const int kitIndex = argc >= 4 ? parseInt(argv[3], "kit index") : 0;
const bool renderAudio = argc >= 5 && std::string(argv[4]) == "note";
const bool renderHarmonics = argc >= 5 && std::string(argv[4]) == "harmonics";
const bool renderSelectedSample = argc >= 5 && std::string(argv[4]) == "selected";
const int requestedSample = argc >= 5 && !renderAudio && !renderHarmonics && !renderSelectedSample ? parseInt(argv[4], "sample index") : 0;
const float frequency = argc >= 6 ? std::strtof(argv[5], nullptr) : 261.6256f;
const float seconds = argc >= 7 ? std::strtof(argv[6], nullptr) : 1.5f;
SYNTH_T synth;
synth.samplerate = 44100;
synth.buffersize = 256;
synth.oscilsize = 1024;
synth.alias(false);
AbsTime time(synth);
FFTwrapper fft(synth.oscilsize);
PADnoteParameters parameters(synth, &fft, &time);
XMLwrapper xml;
if(xml.loadXMLfile(inputPath) != 0) {
std::cerr << "Could not load Zyn instrument: " << inputPath << "\n";
return 2;
}
if(!enterInstrument(xml) ||
!xml.enterbranch("INSTRUMENT_KIT") ||
!xml.enterbranch("INSTRUMENT_KIT_ITEM", kitIndex) ||
!xml.enterbranch("PAD_SYNTH_PARAMETERS")) {
std::cerr << "Could not find PAD_SYNTH_PARAMETERS for kit item " << kitIndex
<< " in " << inputPath << "\n";
return 2;
}
parameters.getfromXML(xml);
parameters.applyparameters([] { return false; }, 1);
if(renderAudio) {
renderNote(parameters, synth, time, outputPath, frequency, seconds);
std::cout << "mode=note samples=" << static_cast<int>(seconds * synth.samplerate)
<< " frequency=" << frequency << "\n";
FFT_cleanup();
return 0;
}
if(renderHarmonics) {
writeHarmonics(parameters, synth, outputPath, frequency);
std::cout << "mode=harmonics count=" << synth.oscilsize / 2
<< " basefreq=" << frequency << "\n";
FFT_cleanup();
return 0;
}
int sampleIndex = renderSelectedSample ? selectedSampleIndex(parameters, frequency) : requestedSample;
if(sampleIndex < 0 || sampleIndex >= PAD_MAX_SAMPLES || parameters.sample[sampleIndex].smp == nullptr) {
sampleIndex = -1;
for(int i = 0; i < PAD_MAX_SAMPLES; ++i) {
if(parameters.sample[i].smp != nullptr) {
sampleIndex = i;
break;
}
}
}
if(sampleIndex < 0) {
std::cerr << "Zyn generated no PAD samples for " << inputPath << "\n";
return 2;
}
const auto& sample = parameters.sample[sampleIndex];
writeFloat32(outputPath, sample.smp, sample.size);
const int preview = std::min(sample.size, 8);
std::cout << "sampleIndex=" << sampleIndex
<< " size=" << sample.size
<< " basefreq=" << sample.basefreq
<< " preview=";
for(int i = 0; i < preview; ++i) {
if(i) std::cout << ",";
std::cout << sample.smp[i];
}
std::cout << "\n";
FFT_cleanup();
return 0;
}