918 lines
30 KiB
C++
918 lines
30 KiB
C++
/* Copyright 2013-2019 Matt Tytel
|
|
*
|
|
* vital 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.
|
|
*
|
|
* vital 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 vital. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "synth_base.h"
|
|
|
|
#include "sample_source.h"
|
|
#include "sound_engine.h"
|
|
#include "load_save.h"
|
|
#include "memory.h"
|
|
#include "modulation_connection_processor.h"
|
|
#include "startup.h"
|
|
#include "synth_gui_interface.h"
|
|
#include "synth_parameters.h"
|
|
#include "utils.h"
|
|
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
|
|
SynthBase::SynthBase() : expired_(false) {
|
|
expired_ = LoadSave::isExpired();
|
|
self_reference_ = std::make_shared<SynthBase*>();
|
|
*self_reference_ = this;
|
|
|
|
engine_ = std::make_unique<vital::SoundEngine>();
|
|
engine_->setTuning(&tuning_);
|
|
|
|
mod_connections_.reserve(vital::kMaxModulationConnections);
|
|
|
|
for (int i = 0; i < vital::kNumOscillators; ++i) {
|
|
vital::Wavetable* wavetable = engine_->getWavetable(i);
|
|
if (wavetable) {
|
|
wavetable_creators_[i] = std::make_unique<WavetableCreator>(wavetable);
|
|
wavetable_creators_[i]->init();
|
|
}
|
|
}
|
|
|
|
keyboard_state_ = std::make_unique<MidiKeyboardState>();
|
|
midi_manager_ = std::make_unique<MidiManager>(this, keyboard_state_.get(), &save_info_, this);
|
|
|
|
last_played_note_ = 0.0f;
|
|
last_num_pressed_ = 0;
|
|
audio_memory_ = std::make_unique<vital::StereoMemory>(vital::kAudioMemorySamples);
|
|
memset(oscilloscope_memory_, 0, 2 * vital::kOscilloscopeMemoryResolution * sizeof(vital::poly_float));
|
|
memset(oscilloscope_memory_write_, 0, 2 * vital::kOscilloscopeMemoryResolution * sizeof(vital::poly_float));
|
|
memory_reset_period_ = vital::kOscilloscopeMemoryResolution;
|
|
memory_input_offset_ = 0;
|
|
memory_index_ = 0;
|
|
|
|
controls_ = engine_->getControls();
|
|
|
|
Startup::doStartupChecks(midi_manager_.get());
|
|
}
|
|
|
|
SynthBase::~SynthBase() { }
|
|
|
|
void SynthBase::valueChanged(const std::string& name, vital::mono_float value) {
|
|
controls_[name]->set(value);
|
|
}
|
|
|
|
void SynthBase::valueChangedInternal(const std::string& name, vital::mono_float value) {
|
|
valueChanged(name, value);
|
|
setValueNotifyHost(name, value);
|
|
}
|
|
|
|
void SynthBase::valueChangedThroughMidi(const std::string& name, vital::mono_float value) {
|
|
controls_[name]->set(value);
|
|
ValueChangedCallback* callback = new ValueChangedCallback(self_reference_, name, value);
|
|
setValueNotifyHost(name, value);
|
|
callback->post();
|
|
}
|
|
|
|
void SynthBase::pitchWheelMidiChanged(vital::mono_float value) {
|
|
ValueChangedCallback* callback = new ValueChangedCallback(self_reference_, "pitch_wheel", value);
|
|
callback->post();
|
|
}
|
|
|
|
void SynthBase::modWheelMidiChanged(vital::mono_float value) {
|
|
ValueChangedCallback* callback = new ValueChangedCallback(self_reference_, "mod_wheel", value);
|
|
callback->post();
|
|
}
|
|
|
|
void SynthBase::pitchWheelGuiChanged(vital::mono_float value) {
|
|
engine_->setZonedPitchWheel(value, 0, vital::kNumMidiChannels - 1);
|
|
}
|
|
|
|
void SynthBase::modWheelGuiChanged(vital::mono_float value) {
|
|
engine_->setModWheelAllChannels(value);
|
|
}
|
|
|
|
void SynthBase::presetChangedThroughMidi(File preset) {
|
|
SynthGuiInterface* gui_interface = getGuiInterface();
|
|
if (gui_interface) {
|
|
gui_interface->updateFullGui();
|
|
gui_interface->notifyFresh();
|
|
}
|
|
}
|
|
|
|
void SynthBase::valueChangedExternal(const std::string& name, vital::mono_float value) {
|
|
valueChanged(name, value);
|
|
if (name == "mod_wheel")
|
|
engine_->setModWheelAllChannels(value);
|
|
else if (name == "pitch_wheel")
|
|
engine_->setZonedPitchWheel(value, 0, vital::kNumMidiChannels - 1);
|
|
|
|
ValueChangedCallback* callback = new ValueChangedCallback(self_reference_, name, value);
|
|
callback->post();
|
|
}
|
|
|
|
vital::ModulationConnection* SynthBase::getConnection(const std::string& source, const std::string& destination) {
|
|
for (vital::ModulationConnection* connection : mod_connections_) {
|
|
if (connection->source_name == source && connection->destination_name == destination)
|
|
return connection;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int SynthBase::getConnectionIndex(const std::string& source, const std::string& destination) {
|
|
vital::ModulationConnectionBank& modulation_bank = getModulationBank();
|
|
for (int i = 0; i < vital::kMaxModulationConnections; ++i) {
|
|
vital::ModulationConnection* connection = modulation_bank.atIndex(i);
|
|
if (connection->source_name == source && connection->destination_name == destination)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
vital::modulation_change SynthBase::createModulationChange(vital::ModulationConnection* connection) {
|
|
vital::modulation_change change;
|
|
change.source = engine_->getModulationSource(connection->source_name);
|
|
change.mono_destination = engine_->getMonoModulationDestination(connection->destination_name);
|
|
change.mono_modulation_switch = engine_->getMonoModulationSwitch(connection->destination_name);
|
|
VITAL_ASSERT(change.source != nullptr);
|
|
VITAL_ASSERT(change.mono_destination != nullptr);
|
|
VITAL_ASSERT(change.mono_modulation_switch != nullptr);
|
|
|
|
change.destination_scale = vital::Parameters::getParameterRange(connection->destination_name);
|
|
change.poly_modulation_switch = engine_->getPolyModulationSwitch(connection->destination_name);
|
|
change.poly_destination = engine_->getPolyModulationDestination(connection->destination_name);
|
|
change.modulation_processor = connection->modulation_processor.get();
|
|
|
|
int num_audio_rate = 0;
|
|
vital::ModulationConnectionBank& modulation_bank = getModulationBank();
|
|
for (int i = 0; i < vital::kMaxModulationConnections; ++i) {
|
|
if (modulation_bank.atIndex(i)->source_name == connection->source_name &&
|
|
modulation_bank.atIndex(i)->destination_name != connection->destination_name &&
|
|
!modulation_bank.atIndex(i)->modulation_processor->isControlRate()) {
|
|
num_audio_rate++;
|
|
}
|
|
}
|
|
change.num_audio_rate = num_audio_rate;
|
|
return change;
|
|
}
|
|
|
|
bool SynthBase::isInvalidConnection(const vital::modulation_change& change) {
|
|
return change.poly_destination && change.poly_destination->router() == change.modulation_processor;
|
|
}
|
|
|
|
void SynthBase::connectModulation(vital::ModulationConnection* connection) {
|
|
vital::modulation_change change = createModulationChange(connection);
|
|
if (isInvalidConnection(change)) {
|
|
connection->destination_name = "";
|
|
connection->source_name = "";
|
|
}
|
|
else if (mod_connections_.count(connection) == 0) {
|
|
change.disconnecting = false;
|
|
mod_connections_.push_back(connection);
|
|
modulation_change_queue_.enqueue(change);
|
|
}
|
|
}
|
|
|
|
bool SynthBase::connectModulation(const std::string& source, const std::string& destination) {
|
|
vital::ModulationConnection* connection = getConnection(source, destination);
|
|
bool create = connection == nullptr;
|
|
if (create)
|
|
connection = getModulationBank().createConnection(source, destination);
|
|
|
|
if (connection)
|
|
connectModulation(connection);
|
|
|
|
return create;
|
|
}
|
|
|
|
bool SynthBase::pyConnectModulation(const std::string& source, const std::string& destination) {
|
|
vital::ModulationConnection* connection = getConnection(source, destination);
|
|
bool create = connection == nullptr;
|
|
if (create)
|
|
connection = getModulationBank().createConnection(source, destination);
|
|
|
|
if (connection) {
|
|
connectModulation(connection);
|
|
|
|
LineGenerator* map_generator = connection->modulation_processor->lineMapGenerator();
|
|
map_generator->initLinear(); // todo: allow more options via kwarg
|
|
}
|
|
|
|
return create;
|
|
}
|
|
|
|
void SynthBase::disconnectModulation(vital::ModulationConnection* connection) {
|
|
if (mod_connections_.count(connection) == 0)
|
|
return;
|
|
|
|
vital::modulation_change change = createModulationChange(connection);
|
|
connection->source_name = "";
|
|
connection->destination_name = "";
|
|
|
|
mod_connections_.remove(connection);
|
|
change.disconnecting = true;
|
|
modulation_change_queue_.enqueue(change);
|
|
}
|
|
|
|
void SynthBase::disconnectModulation(const std::string& source, const std::string& destination) {
|
|
vital::ModulationConnection* connection = getConnection(source, destination);
|
|
if (connection)
|
|
disconnectModulation(connection);
|
|
}
|
|
|
|
void SynthBase::clearModulations() {
|
|
clearModulationQueue();
|
|
|
|
while (mod_connections_.size()) {
|
|
vital::ModulationConnection* connection = *mod_connections_.begin();
|
|
mod_connections_.remove(connection);
|
|
vital::modulation_change change = createModulationChange(connection);
|
|
change.disconnecting = true;
|
|
engine_->disconnectModulation(change);
|
|
connection->source_name = "";
|
|
connection->destination_name = "";
|
|
}
|
|
|
|
int num_connections = static_cast<int>(getModulationBank().numConnections());
|
|
for (int i = 0; i < num_connections; ++i)
|
|
getModulationBank().atIndex(i)->modulation_processor->lineMapGenerator()->initLinear();
|
|
|
|
engine_->disableUnnecessaryModSources();
|
|
}
|
|
|
|
void SynthBase::forceShowModulation(const std::string& source, bool force) {
|
|
if (force)
|
|
engine_->enableModSource(source);
|
|
else if (!isSourceConnected(source))
|
|
engine_->disableModSource(source);
|
|
}
|
|
|
|
bool SynthBase::isModSourceEnabled(const std::string& source) {
|
|
return engine_->isModSourceEnabled(source);
|
|
}
|
|
|
|
int SynthBase::getNumModulations(const std::string& destination) {
|
|
int connections = 0;
|
|
for (vital::ModulationConnection* connection : mod_connections_) {
|
|
if (connection->destination_name == destination)
|
|
connections++;
|
|
}
|
|
return connections;
|
|
}
|
|
|
|
std::vector<vital::ModulationConnection*> SynthBase::getSourceConnections(const std::string& source) {
|
|
std::vector<vital::ModulationConnection*> connections;
|
|
for (vital::ModulationConnection* connection : mod_connections_) {
|
|
if (connection->source_name == source)
|
|
connections.push_back(connection);
|
|
}
|
|
return connections;
|
|
}
|
|
|
|
bool SynthBase::isSourceConnected(const std::string& source) {
|
|
for (vital::ModulationConnection* connection : mod_connections_) {
|
|
if (connection->source_name == source)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::vector<vital::ModulationConnection*> SynthBase::getDestinationConnections(const std::string& destination) {
|
|
std::vector<vital::ModulationConnection*> connections;
|
|
for (vital::ModulationConnection* connection : mod_connections_) {
|
|
if (connection->destination_name == destination)
|
|
connections.push_back(connection);
|
|
}
|
|
return connections;
|
|
}
|
|
|
|
const vital::StatusOutput* SynthBase::getStatusOutput(const std::string& name) {
|
|
return engine_->getStatusOutput(name);
|
|
}
|
|
|
|
vital::Wavetable* SynthBase::getWavetable(int index) {
|
|
return engine_->getWavetable(index);
|
|
}
|
|
|
|
WavetableCreator* SynthBase::getWavetableCreator(int index) {
|
|
return wavetable_creators_[index].get();
|
|
}
|
|
|
|
vital::Sample* SynthBase::getSample() {
|
|
return engine_->getSample();
|
|
}
|
|
|
|
LineGenerator* SynthBase::getLfoSource(int index) {
|
|
return engine_->getLfoSource(index);
|
|
}
|
|
|
|
json SynthBase::saveToJson() {
|
|
return LoadSave::stateToJson(this, getCriticalSection());
|
|
}
|
|
|
|
int SynthBase::getSampleRate() {
|
|
return engine_->getSampleRate();
|
|
}
|
|
|
|
void SynthBase::initEngine() {
|
|
clearModulations();
|
|
if (getWavetableCreator(0)) {
|
|
for (int i = 0; i < vital::kNumOscillators; ++i)
|
|
getWavetableCreator(i)->init();
|
|
|
|
engine_->getSample()->init();
|
|
}
|
|
|
|
for (int i = 0; i < vital::kNumLfos; ++i)
|
|
getLfoSource(i)->initTriangle();
|
|
|
|
vital::control_map controls = engine_->getControls();
|
|
for (auto& control : controls) {
|
|
vital::ValueDetails details = vital::Parameters::getDetails(control.first);
|
|
control.second->set(details.default_value);
|
|
}
|
|
checkOversampling();
|
|
|
|
clearActiveFile();
|
|
}
|
|
|
|
void SynthBase::loadTuningFile(const File& file) {
|
|
tuning_.loadFile(file);
|
|
}
|
|
|
|
void SynthBase::loadInitPreset() {
|
|
pauseProcessing(true);
|
|
engine_->allSoundsOff();
|
|
initEngine();
|
|
LoadSave::initSaveInfo(save_info_);
|
|
pauseProcessing(false);
|
|
}
|
|
|
|
bool SynthBase::loadFromJson(const json& data) {
|
|
pauseProcessing(true);
|
|
engine_->allSoundsOff();
|
|
try {
|
|
bool result = LoadSave::jsonToState(this, save_info_, data);
|
|
pauseProcessing(false);
|
|
return result;
|
|
}
|
|
catch (const json::exception& e) {
|
|
pauseProcessing(false);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
bool SynthBase::loadFromFile(File preset, std::string& error) {
|
|
if (!preset.exists())
|
|
return false;
|
|
|
|
try {
|
|
json parsed_json_state = json::parse(preset.loadFileAsString().toStdString(), nullptr);
|
|
if (!loadFromJson(parsed_json_state)) {
|
|
error = "Preset was created with a newer version.";
|
|
return false;
|
|
}
|
|
|
|
active_file_ = preset;
|
|
}
|
|
catch (const json::exception& e) {
|
|
error = "Preset file is corrupted.";
|
|
return false;
|
|
}
|
|
|
|
setPresetName(preset.getFileNameWithoutExtension());
|
|
|
|
SynthGuiInterface* gui_interface = getGuiInterface();
|
|
if (gui_interface) {
|
|
gui_interface->updateFullGui();
|
|
gui_interface->notifyFresh();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SynthBase::pyLoadFromFile(std::string path) {
|
|
try {
|
|
File jsonFile(path);
|
|
std::string error;
|
|
bool result = loadFromFile(jsonFile, error);
|
|
if (!result) {
|
|
std::cerr << "Error: " << error << std::endl;
|
|
}
|
|
return result;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Error: " << e.what() << '\n';
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SynthBase::loadFromString(std::string json_text) {
|
|
std::string error;
|
|
try {
|
|
json parsed_json_state = json::parse(json_text, nullptr);
|
|
if (!loadFromJson(parsed_json_state)) {
|
|
error = "Preset was created with a newer version.";
|
|
return false;
|
|
}
|
|
|
|
//active_file_ = preset; // todo:
|
|
} catch (const json::exception& e) {
|
|
error = "Preset file is corrupted.";
|
|
return false;
|
|
}
|
|
|
|
//setPresetName(preset.getFileNameWithoutExtension()); // todo:
|
|
|
|
SynthGuiInterface* gui_interface = getGuiInterface();
|
|
if (gui_interface) {
|
|
gui_interface->updateFullGui();
|
|
gui_interface->notifyFresh();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SynthBase::pySetBPM(float bpm) {
|
|
// todo: this is hopefully safe as a public method just for headless mode.
|
|
engine_->setBpm(bpm);
|
|
};
|
|
|
|
void SynthBase::renderAudioToFile(File file, std::vector<int> notes, float velocity, float note_dur, float render_dur, bool render_images) {
|
|
static constexpr int kSampleRate = 44100;
|
|
static constexpr int kPreProcessSamples = 44100;
|
|
static constexpr int kFadeSamples = 200;
|
|
static constexpr int kBufferSize = 64;
|
|
static constexpr int kVideoRate = 30;
|
|
static constexpr int kImageNumberPlaces = 3;
|
|
static constexpr int kImageWidth = 500;
|
|
static constexpr int kImageHeight = 250;
|
|
static constexpr int kOscilloscopeResolution = 512;
|
|
|
|
ScopedLock lock(getCriticalSection());
|
|
|
|
engine_->allSoundsOff(); // note: dbraun added this
|
|
|
|
processModulationChanges();
|
|
engine_->setSampleRate(kSampleRate);
|
|
// engine_->setBpm(bpm);
|
|
engine_->updateAllModulationSwitches();
|
|
|
|
double sample_time = 1.0 / getSampleRate();
|
|
double current_time = -kPreProcessSamples * sample_time;
|
|
|
|
for (int samples = 0; samples < kPreProcessSamples; samples += kBufferSize) {
|
|
engine_->correctToTime(current_time);
|
|
current_time += kBufferSize * sample_time;
|
|
engine_->process(kBufferSize);
|
|
}
|
|
|
|
for (int note : notes)
|
|
engine_->noteOn(note, velocity, 0, 0);
|
|
|
|
file.deleteFile();
|
|
std::unique_ptr<FileOutputStream> file_stream = file.createOutputStream();
|
|
WavAudioFormat wav_format;
|
|
std::unique_ptr<AudioFormatWriter> writer(wav_format.createWriterFor(file_stream.get(), kSampleRate, 2, 16, {}, 0));
|
|
|
|
int on_samples = note_dur * kSampleRate;
|
|
int total_samples = render_dur * kSampleRate;
|
|
std::unique_ptr<float[]> left_buffer = std::make_unique<float[]>(kBufferSize);
|
|
std::unique_ptr<float[]> right_buffer = std::make_unique<float[]>(kBufferSize);
|
|
float* buffers[2] = { left_buffer.get(), right_buffer.get() };
|
|
const vital::mono_float* engine_output = (const vital::mono_float*)engine_->output(0)->buffer;
|
|
|
|
#if JUCE_MODULE_AVAILABLE_juce_graphics
|
|
int current_image_index = -1;
|
|
PNGImageFormat png;
|
|
File images_folder = File::getCurrentWorkingDirectory().getChildFile("images");
|
|
if (!images_folder.exists() && render_images)
|
|
images_folder.createDirectory();
|
|
const vital::poly_float* memory = getOscilloscopeMemory();
|
|
#endif
|
|
|
|
for (int samples = 0; samples < total_samples; samples += kBufferSize) {
|
|
engine_->correctToTime(current_time);
|
|
current_time += kBufferSize * sample_time;
|
|
engine_->process(kBufferSize);
|
|
updateMemoryOutput(kBufferSize, engine_->output(0)->buffer);
|
|
|
|
if (on_samples > samples && on_samples <= samples + kBufferSize) {
|
|
for (int note : notes)
|
|
engine_->noteOff(note, 0.5f, 0, 0);
|
|
}
|
|
|
|
for (int i = 0; i < kBufferSize; ++i) {
|
|
vital::mono_float t = (total_samples - samples) / (1.0f * kFadeSamples);
|
|
t = vital::utils::min(t, 1.0f);
|
|
left_buffer[i] = t * engine_output[vital::poly_float::kSize * i];
|
|
right_buffer[i] = t * engine_output[vital::poly_float::kSize * i + 1];
|
|
}
|
|
|
|
writer->writeFromFloatArrays(buffers, 2, kBufferSize);
|
|
|
|
#if JUCE_MODULE_AVAILABLE_juce_graphics
|
|
int image_index = (samples * kVideoRate) / kSampleRate;
|
|
if (image_index > current_image_index && render_images) {
|
|
current_image_index = image_index;
|
|
String number(image_index);
|
|
while (number.length() < kImageNumberPlaces)
|
|
number = "0" + number;
|
|
|
|
File image_file = images_folder.getChildFile("rendered_image" + number + ".png");
|
|
FileOutputStream image_file_stream(image_file);
|
|
Image image(Image::RGB, kImageWidth, kImageHeight, true);
|
|
Graphics g(image);
|
|
g.fillAll(Colour(0xff1d2125));
|
|
|
|
Path left_path;
|
|
Path right_path;
|
|
left_path.startNewSubPath(-2.0f, kImageHeight / 2);
|
|
right_path.startNewSubPath(-2.0f, kImageHeight / 2);
|
|
|
|
for (int i = 0; i < kOscilloscopeResolution; ++i) {
|
|
float t = i / (kOscilloscopeResolution - 1.0f);
|
|
float memory_spot = (1.0f * i * vital::kOscilloscopeMemoryResolution) / kOscilloscopeResolution;
|
|
int memory_index = memory_spot;
|
|
float remainder = memory_spot - memory_index;
|
|
vital::poly_float from = memory[memory_index];
|
|
vital::poly_float to = memory[memory_index + 1];
|
|
vital::poly_float y = -vital::utils::interpolate(from, to, remainder) * kImageHeight / 2.0f + kImageHeight / 2;
|
|
left_path.lineTo(t * kImageWidth, y[0]);
|
|
right_path.lineTo(t * kImageWidth, y[1]);
|
|
}
|
|
left_path.lineTo(kImageWidth + 2.0f, kImageHeight / 2.0f);
|
|
right_path.lineTo(kImageWidth + 2.0f, kImageHeight / 2.0f);
|
|
|
|
g.setColour(Colour(0x64aa88ff));
|
|
g.fillPath(left_path);
|
|
g.fillPath(right_path);
|
|
|
|
g.setColour(Colour(0xffaa88ff));
|
|
g.strokePath(left_path, PathStrokeType(2.0f, PathStrokeType::curved, PathStrokeType::rounded));
|
|
g.strokePath(right_path, PathStrokeType(2.0f, PathStrokeType::curved, PathStrokeType::rounded));
|
|
|
|
png.writeImageToStream(image, image_file_stream);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
writer->flush();
|
|
file_stream->flush();
|
|
|
|
writer = nullptr;
|
|
file_stream.release();
|
|
}
|
|
|
|
nb::ndarray<float, nb::shape<2, -1>, nb::numpy> SynthBase::renderAudioToNumpy(const int& midi_note, float velocity, float note_dur, float render_dur) {
|
|
static constexpr int kSampleRate = 44100;
|
|
static constexpr int kFadeSamples = 200;
|
|
static constexpr int kBufferSize = 64;
|
|
static constexpr int kPreProcessSamples = 256; // note: dbraun decreased this from 44100.
|
|
|
|
ScopedLock lock(getCriticalSection());
|
|
|
|
engine_->allSoundsOff(); // note: dbraun added this
|
|
|
|
processModulationChanges();
|
|
engine_->setSampleRate(kSampleRate);
|
|
engine_->updateAllModulationSwitches();
|
|
|
|
// Preprocess modulation
|
|
double sample_time = 1.0 / getSampleRate();
|
|
double current_time = -kPreProcessSamples * sample_time;
|
|
|
|
for (int samples = 0; samples < kPreProcessSamples; samples += kBufferSize) {
|
|
engine_->correctToTime(current_time);
|
|
current_time += kBufferSize * sample_time;
|
|
engine_->process(kBufferSize);
|
|
}
|
|
|
|
engine_->noteOn(midi_note, velocity, 0, 0);
|
|
|
|
int on_samples = note_dur * kSampleRate;
|
|
int total_samples = render_dur * kSampleRate;
|
|
const vital::mono_float* engine_output =
|
|
(const vital::mono_float*)engine_->output(0)->buffer;
|
|
|
|
size_t total_frames =
|
|
static_cast<size_t>(total_samples * 2); // stereo: 2 channels
|
|
|
|
auto* data = new float[total_frames](); // Zero-initialized
|
|
auto capsule = nb::capsule(
|
|
data, [](void* p) noexcept { delete[] static_cast<float*>(p); });
|
|
|
|
int baseSample = 0;
|
|
|
|
for (int samples = 0; samples < total_samples; samples += kBufferSize) {
|
|
engine_->correctToTime(current_time);
|
|
current_time += kBufferSize * sample_time;
|
|
engine_->process(kBufferSize);
|
|
updateMemoryOutput(kBufferSize, engine_->output(0)->buffer);
|
|
|
|
if (on_samples > samples && on_samples <= samples + kBufferSize) {
|
|
engine_->noteOff(midi_note, 0.5f, 0, 0);
|
|
}
|
|
|
|
for (int i = 0; i < kBufferSize; ++i) {
|
|
vital::mono_float t = (total_samples - samples) / (1.0f * kFadeSamples);
|
|
t = vital::utils::min(t, 1.0f);
|
|
baseSample = samples + i;
|
|
if (baseSample < total_samples) {
|
|
data[samples + i] = t * engine_output[vital::poly_float::kSize * i];
|
|
data[samples + i + total_samples] =
|
|
t * engine_output[vital::poly_float::kSize * i + 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the data as a NumPy array
|
|
return nb::ndarray<float, nb::shape<2, -1>, nb::numpy>(
|
|
data, {2, static_cast<size_t>(total_samples)}, capsule);
|
|
}
|
|
|
|
bool SynthBase::renderAudioToFile2(const std::string& output_path, const int& midi_note, float velocity, float note_dur, float render_dur) {
|
|
File output_file(output_path);
|
|
if (!output_file.hasWriteAccess()) {
|
|
std::cout << "Error: Don't have permission to write output file." << newLine;
|
|
return false;
|
|
}
|
|
bool render_images = false;
|
|
std::vector<int> midi_notes = {midi_note};
|
|
|
|
renderAudioToFile(output_file, midi_notes, velocity, note_dur, render_dur, render_images);
|
|
return true;
|
|
}
|
|
|
|
|
|
void SynthBase::renderAudioForResynthesis(float* data, int samples, int note) {
|
|
static constexpr int kPreProcessSamples = 44100;
|
|
static constexpr int kBufferSize = 64;
|
|
|
|
ScopedLock lock(getCriticalSection());
|
|
|
|
double sample_time = 1.0 / getSampleRate();
|
|
double current_time = -kPreProcessSamples * sample_time;
|
|
|
|
engine_->allSoundsOff();
|
|
for (int s = 0; s < kPreProcessSamples; s += kBufferSize) {
|
|
engine_->correctToTime(current_time);
|
|
current_time += kBufferSize * sample_time;
|
|
engine_->process(kBufferSize);
|
|
}
|
|
|
|
engine_->noteOn(note, 0.7f, 0, 0);
|
|
const vital::poly_float* engine_output = engine_->output(0)->buffer;
|
|
float max_value = 0.01f;
|
|
for (int s = 0; s < samples; s += kBufferSize) {
|
|
int num_samples = std::min(samples - s, kBufferSize);
|
|
engine_->correctToTime(current_time);
|
|
current_time += num_samples * sample_time;
|
|
engine_->process(num_samples);
|
|
|
|
for (int i = 0; i < num_samples; ++i) {
|
|
float sample = engine_output[i][0];
|
|
data[s + i] = sample;
|
|
max_value = std::max(max_value, fabsf(sample));
|
|
}
|
|
}
|
|
|
|
float scale = 1.0f / max_value;
|
|
for (int s = 0; s < samples; ++s)
|
|
data[s] *= scale;
|
|
|
|
engine_->allSoundsOff();
|
|
}
|
|
|
|
bool SynthBase::saveToFile(File preset) {
|
|
preset = preset.withFileExtension(String(vital::kPresetExtension));
|
|
|
|
File parent = preset.getParentDirectory();
|
|
if (!parent.exists()) {
|
|
if (!parent.createDirectory().wasOk() || !parent.hasWriteAccess())
|
|
return false;
|
|
}
|
|
|
|
setPresetName(preset.getFileNameWithoutExtension());
|
|
|
|
SynthGuiInterface* gui_interface = getGuiInterface();
|
|
if (gui_interface)
|
|
gui_interface->notifyFresh();
|
|
|
|
if (preset.replaceWithText(saveToJson().dump())) {
|
|
active_file_ = preset;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SynthBase::saveToActiveFile() {
|
|
if (!active_file_.exists() || !active_file_.hasWriteAccess())
|
|
return false;
|
|
|
|
return saveToFile(active_file_);
|
|
}
|
|
|
|
void SynthBase::setMpeEnabled(bool enabled) {
|
|
midi_manager_->setMpeEnabled(enabled);
|
|
}
|
|
|
|
void SynthBase::processAudio(AudioSampleBuffer* buffer, int channels, int samples, int offset) {
|
|
if (expired_)
|
|
return;
|
|
|
|
engine_->process(samples);
|
|
writeAudio(buffer, channels, samples, offset);
|
|
}
|
|
|
|
void SynthBase::processAudioWithInput(AudioSampleBuffer* buffer, const vital::poly_float* input_buffer,
|
|
int channels, int samples, int offset) {
|
|
if (expired_)
|
|
return;
|
|
|
|
engine_->processWithInput(input_buffer, samples);
|
|
writeAudio(buffer, channels, samples, offset);
|
|
}
|
|
|
|
void SynthBase::writeAudio(AudioSampleBuffer* buffer, int channels, int samples, int offset) {
|
|
const vital::mono_float* engine_output = (const vital::mono_float*)engine_->output(0)->buffer;
|
|
for (int channel = 0; channel < channels; ++channel) {
|
|
float* channel_data = buffer->getWritePointer(channel, offset);
|
|
|
|
for (int i = 0; i < samples; ++i) {
|
|
channel_data[i] = engine_output[vital::poly_float::kSize * i + channel];
|
|
VITAL_ASSERT(std::isfinite(channel_data[i]));
|
|
}
|
|
}
|
|
|
|
updateMemoryOutput(samples, engine_->output(0)->buffer);
|
|
}
|
|
|
|
void SynthBase::processMidi(MidiBuffer& midi_messages, int start_sample, int end_sample) {
|
|
bool process_all = end_sample == 0;
|
|
for (const MidiMessageMetadata message : midi_messages) {
|
|
int midi_sample = message.samplePosition;
|
|
if (process_all || (midi_sample >= start_sample && midi_sample < end_sample))
|
|
midi_manager_->processMidiMessage(message.getMessage(), midi_sample - start_sample);
|
|
}
|
|
}
|
|
|
|
void SynthBase::processKeyboardEvents(MidiBuffer& buffer, int num_samples) {
|
|
midi_manager_->replaceKeyboardMessages(buffer, num_samples);
|
|
}
|
|
|
|
void SynthBase::processModulationChanges() {
|
|
vital::modulation_change change;
|
|
while (getNextModulationChange(change)) {
|
|
if (change.disconnecting)
|
|
engine_->disconnectModulation(change);
|
|
else
|
|
engine_->connectModulation(change);
|
|
}
|
|
}
|
|
|
|
void SynthBase::updateMemoryOutput(int samples, const vital::poly_float* audio) {
|
|
for (int i = 0; i < samples; ++i)
|
|
audio_memory_->push(audio[i]);
|
|
|
|
vital::mono_float last_played = engine_->getLastActiveNote();
|
|
last_played = vital::utils::clamp(last_played, kOutputWindowMinNote, kOutputWindowMaxNote);
|
|
|
|
int num_pressed = engine_->getNumPressedNotes();
|
|
int output_inc = std::max<int>(1, engine_->getSampleRate() / vital::kOscilloscopeMemorySampleRate);
|
|
int oscilloscope_samples = 2 * vital::kOscilloscopeMemoryResolution;
|
|
|
|
if (last_played && (last_played_note_ != last_played || num_pressed > last_num_pressed_)) {
|
|
last_played_note_ = last_played;
|
|
|
|
vital::mono_float frequency = vital::utils::midiNoteToFrequency(last_played_note_);
|
|
vital::mono_float period = engine_->getSampleRate() / frequency;
|
|
int window_length = output_inc * vital::kOscilloscopeMemoryResolution;
|
|
|
|
memory_reset_period_ = period;
|
|
while (memory_reset_period_ < window_length)
|
|
memory_reset_period_ += memory_reset_period_;
|
|
|
|
memory_reset_period_ = std::min(memory_reset_period_, 2.0f * window_length);
|
|
memory_index_ = 0;
|
|
vital::utils::copyBuffer(oscilloscope_memory_, oscilloscope_memory_write_, oscilloscope_samples);
|
|
}
|
|
last_num_pressed_ = num_pressed;
|
|
|
|
for (; memory_input_offset_ < samples; memory_input_offset_ += output_inc) {
|
|
int input_index = vital::utils::iclamp(memory_input_offset_, 0, samples);
|
|
memory_index_ = vital::utils::iclamp(memory_index_, 0, oscilloscope_samples - 1);
|
|
VITAL_ASSERT(input_index >= 0);
|
|
VITAL_ASSERT(input_index < samples);
|
|
VITAL_ASSERT(memory_index_ >= 0);
|
|
VITAL_ASSERT(memory_index_ < oscilloscope_samples);
|
|
oscilloscope_memory_write_[memory_index_++] = audio[input_index];
|
|
|
|
if (memory_index_ * output_inc >= memory_reset_period_) {
|
|
memory_input_offset_ += memory_reset_period_ - memory_index_ * output_inc;
|
|
memory_index_ = 0;
|
|
vital::utils::copyBuffer(oscilloscope_memory_, oscilloscope_memory_write_, oscilloscope_samples);
|
|
}
|
|
}
|
|
|
|
memory_input_offset_ -= samples;
|
|
}
|
|
|
|
void SynthBase::armMidiLearn(const std::string& name) {
|
|
midi_manager_->armMidiLearn(name);
|
|
}
|
|
|
|
void SynthBase::cancelMidiLearn() {
|
|
midi_manager_->cancelMidiLearn();
|
|
}
|
|
|
|
void SynthBase::clearMidiLearn(const std::string& name) {
|
|
midi_manager_->clearMidiLearn(name);
|
|
}
|
|
|
|
bool SynthBase::isMidiMapped(const std::string& name) {
|
|
return midi_manager_->isMidiMapped(name);
|
|
}
|
|
|
|
void SynthBase::setAuthor(const String& author) {
|
|
save_info_["author"] = author;
|
|
}
|
|
|
|
void SynthBase::setComments(const String& comments) {
|
|
save_info_["comments"] = comments;
|
|
}
|
|
|
|
void SynthBase::setStyle(const String& style) {
|
|
save_info_["style"] = style;
|
|
}
|
|
|
|
void SynthBase::setPresetName(const String& preset_name) {
|
|
save_info_["preset_name"] = preset_name;
|
|
}
|
|
|
|
void SynthBase::setMacroName(int index, const String& macro_name) {
|
|
save_info_["macro" + std::to_string(index + 1)] = macro_name;
|
|
}
|
|
|
|
String SynthBase::getAuthor() {
|
|
return save_info_["author"];
|
|
}
|
|
|
|
String SynthBase::getComments() {
|
|
return save_info_["comments"];
|
|
}
|
|
|
|
String SynthBase::getStyle() {
|
|
return save_info_["style"];
|
|
}
|
|
|
|
String SynthBase::getPresetName() {
|
|
return save_info_["preset_name"];
|
|
}
|
|
|
|
String SynthBase::getMacroName(int index) {
|
|
String name = save_info_["macro" + std::to_string(index + 1)];
|
|
if (name.trim().isEmpty())
|
|
return "MACRO " + String(index + 1);
|
|
return name;
|
|
}
|
|
|
|
const vital::StereoMemory* SynthBase::getEqualizerMemory() {
|
|
if (engine_)
|
|
return engine_->getEqualizerMemory();
|
|
return nullptr;
|
|
}
|
|
|
|
vital::ModulationConnectionBank& SynthBase::getModulationBank() {
|
|
return engine_->getModulationBank();
|
|
}
|
|
|
|
void SynthBase::notifyOversamplingChanged() {
|
|
pauseProcessing(true);
|
|
engine_->allSoundsOff();
|
|
checkOversampling();
|
|
pauseProcessing(false);
|
|
}
|
|
|
|
void SynthBase::checkOversampling() {
|
|
return engine_->checkOversampling();
|
|
}
|
|
|
|
void SynthBase::ValueChangedCallback::messageCallback() {
|
|
if (auto synth_base = listener.lock()) {
|
|
SynthGuiInterface* gui_interface = (*synth_base)->getGuiInterface();
|
|
if (gui_interface) {
|
|
gui_interface->updateGuiControl(control_name, value);
|
|
if (control_name != "pitch_wheel")
|
|
gui_interface->notifyChange();
|
|
}
|
|
}
|
|
}
|