Files
lmms/plugins/Vibed/Vibed.cpp
2024-07-21 22:34:34 +01:00

534 lines
17 KiB
C++

/*
* Vibed.cpp - combination of PluckedStringSynth and BitInvader
*
* Copyright (c) 2006-2008 Danny McRae <khjklujn/at/yahoo/com>
*
* This file is part of LMMS - https://lmms.io
*
* This program 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 2 of the License, or (at your option) any later version.
*
* This program 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 this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "Vibed.h"
#include <array>
#include <cassert>
#include <QDomElement>
#include "AudioEngine.h"
#include "Engine.h"
#include "InstrumentTrack.h"
#include "NotePlayHandle.h"
#include "VibratingString.h"
#include "base64.h"
#include "CaptionMenu.h"
#include "volume.h"
#include "Song.h"
#include "embed.h"
#include "plugin_export.h"
namespace lmms
{
extern "C"
{
Plugin::Descriptor PLUGIN_EXPORT vibedstrings_plugin_descriptor =
{
LMMS_STRINGIFY(PLUGIN_NAME),
"Vibed",
QT_TRANSLATE_NOOP("PluginBrowser", "Vibrating string modeler"),
"Danny McRae <khjklujn/at/yahoo/com>",
0x0100,
Plugin::Type::Instrument,
new PluginPixmapLoader("logo"),
nullptr,
nullptr
};
}
class Vibed::StringContainer
{
public:
StringContainer(float pitch, sample_rate_t sampleRate, int bufferLength) :
m_pitch(pitch), m_sampleRate(sampleRate), m_bufferLength(bufferLength) {}
~StringContainer() = default;
void addString(std::size_t harm, float pick, float pickup, const float* impulse, float randomize,
float stringLoss, float detune, int oversample, bool state, int id)
{
constexpr auto octave = std::array{0.25f, 0.5f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f};
assert(harm < octave.size());
m_strings[id] = VibratingString{m_pitch * octave[harm], pick, pickup, impulse, m_bufferLength,
m_sampleRate, oversample, randomize, stringLoss, detune, state};
m_exists[id] = true;
}
bool exists(int id) const { return m_exists[id]; }
sample_t getStringSample(int id) { return m_strings[id].nextSample(); }
private:
const float m_pitch;
const sample_rate_t m_sampleRate;
const int m_bufferLength;
std::array<VibratingString, s_stringCount> m_strings{};
std::array<bool, s_stringCount> m_exists{};
};
Vibed::Vibed(InstrumentTrack* instrumentTrack) :
Instrument(instrumentTrack, &vibedstrings_plugin_descriptor, nullptr, Flag::IsNotBendable)
{
for (int harm = 0; harm < s_stringCount; ++harm)
{
m_volumeModels[harm] = std::make_unique<FloatModel>(
DefaultVolume, MinVolume, MaxVolume, 1.0f, this, tr("String %1 volume").arg(harm + 1));
m_stiffnessModels[harm] = std::make_unique<FloatModel>(
0.0f, 0.0f, 0.05f, 0.001f, this, tr("String %1 stiffness").arg(harm + 1));
m_pickModels[harm] = std::make_unique<FloatModel>(
0.0f, 0.0f, 0.05f, 0.005f, this, tr("Pick %1 position").arg(harm + 1));
m_pickupModels[harm] = std::make_unique<FloatModel>(
0.05f, 0.0f, 0.05f, 0.005f, this, tr("Pickup %1 position").arg( harm + 1));
m_panModels[harm] = std::make_unique<FloatModel>(
0.0f, -1.0f, 1.0f, 0.01f, this, tr("String %1 panning").arg(harm + 1));
m_detuneModels[harm] = std::make_unique<FloatModel>(
0.0f, -0.1f, 0.1f, 0.001f, this, tr("String %1 detune").arg(harm + 1));
m_randomModels[harm] = std::make_unique<FloatModel>(
0.0f, 0.0f, 0.75f, 0.01f, this, tr("String %1 fuzziness").arg(harm + 1));
m_lengthModels[harm] = std::make_unique<FloatModel>(
1, 1, 16, 1, this, tr("String %1 length").arg(harm + 1));
m_impulseModels[harm] = std::make_unique<BoolModel>(false, this, tr("Impulse %1").arg(harm + 1));
m_powerModels[harm] = std::make_unique<BoolModel>(harm == 0, this, tr("String %1").arg(harm + 1));
m_harmonicModels[harm] = std::make_unique<NineButtonSelectorModel>(2, 0, 8, this);
m_graphModels[harm] = std::make_unique<graphModel>(-1.0, 1.0, s_sampleLength, this);
m_graphModels[harm]->setWaveToSine();
}
}
void Vibed::saveSettings(QDomDocument& doc, QDomElement& elem)
{
// Save plugin version
elem.setAttribute("version", "0.2");
for (int i = 0; i < s_stringCount; ++i)
{
const auto is = QString::number(i);
elem.setAttribute("active" + is, QString::number(m_powerModels[i]->value()));
m_volumeModels[i]->saveSettings(doc, elem, "volume" + is);
m_stiffnessModels[i]->saveSettings(doc, elem, "stiffness" + is);
m_pickModels[i]->saveSettings(doc, elem, "pick" + is);
m_pickupModels[i]->saveSettings(doc, elem, "pickup" + is);
m_harmonicModels[i]->saveSettings(doc, elem, "octave" + is);
m_lengthModels[i]->saveSettings(doc, elem, "length" + is);
m_panModels[i]->saveSettings(doc, elem, "pan" + is);
m_detuneModels[i]->saveSettings(doc, elem, "detune" + is);
m_randomModels[i]->saveSettings(doc, elem, "slap" + is);
m_impulseModels[i]->saveSettings(doc, elem, "impulse" + is);
QString sampleString;
base64::encode((const char*)m_graphModels[i]->samples(), s_sampleLength * sizeof(float), sampleString);
elem.setAttribute("graph" + is, sampleString);
}
}
void Vibed::loadSettings(const QDomElement& elem)
{
// Load plugin version
bool newVersion = false;
if (elem.hasAttribute("version"))
{
newVersion = elem.attribute("version").toFloat() >= 0.2f;
}
for (int i = 0; i < s_stringCount; ++i)
{
const auto is = QString::number(i);
m_powerModels[i]->setValue(elem.attribute("active" + is).toInt());
// Version 0.2 saves/loads all instrument data unconditionally
const bool hasVolumeAttr = elem.hasAttribute("volume" + is) || !elem.firstChildElement("volume" + is).isNull();
if (newVersion || (m_powerModels[i]->value() && hasVolumeAttr))
{
m_volumeModels[i]->loadSettings(elem, "volume" + is);
m_stiffnessModels[i]->loadSettings(elem, "stiffness" + is);
m_pickModels[i]->loadSettings(elem, "pick" + is);
m_pickupModels[i]->loadSettings(elem, "pickup" + is);
m_harmonicModels[i]->loadSettings(elem, "octave" + is);
m_lengthModels[i]->loadSettings(elem, "length" + is);
m_panModels[i]->loadSettings(elem, "pan" + is);
m_detuneModels[i]->loadSettings(elem, "detune" + is);
m_randomModels[i]->loadSettings(elem, "slap" + is);
m_impulseModels[i]->loadSettings(elem, "impulse" + is);
int size = 0;
float* shp = nullptr;
base64::decode(elem.attribute("graph" + is), &shp, &size);
assert(size == 128 * sizeof(float));
m_graphModels[i]->setSamples(shp);
delete[] shp;
}
}
}
QString Vibed::nodeName() const
{
return vibedstrings_plugin_descriptor.name;
}
void Vibed::playNote(NotePlayHandle* n, SampleFrame* workingBuffer)
{
if (!n->m_pluginData)
{
const auto newContainer = new StringContainer{n->frequency(),
Engine::audioEngine()->outputSampleRate(), s_sampleLength};
n->m_pluginData = newContainer;
for (int i = 0; i < s_stringCount; ++i)
{
if (m_powerModels[i]->value())
{
newContainer->addString(
m_harmonicModels[i]->value(),
m_pickModels[i]->value(),
m_pickupModels[i]->value(),
m_graphModels[i]->samples(),
m_randomModels[i]->value(),
m_stiffnessModels[i]->value(),
m_detuneModels[i]->value(),
static_cast<int>(m_lengthModels[i]->value()),
m_impulseModels[i]->value(),
i);
}
}
}
const fpp_t frames = n->framesLeftForCurrentPeriod();
const f_cnt_t offset = n->noteOffset();
auto ps = static_cast<StringContainer*>(n->m_pluginData);
for (fpp_t i = offset; i < frames + offset; ++i)
{
workingBuffer[i][0] = 0.0f;
workingBuffer[i][1] = 0.0f;
for (int str = 0; str < s_stringCount; ++str)
{
if (ps->exists(str))
{
// pan: 0 -> left, 1 -> right
const float pan = (m_panModels[str]->value() + 1) / 2.0f;
const sample_t sample = ps->getStringSample(str) * m_volumeModels[str]->value() / 100.0f;
workingBuffer[i][0] += (1.0f - pan) * sample;
workingBuffer[i][1] += pan * sample;
}
}
}
}
void Vibed::deleteNotePluginData(NotePlayHandle* n)
{
delete static_cast<StringContainer*>(n->m_pluginData);
}
gui::PluginView* Vibed::instantiateView(QWidget* parent)
{
return new gui::VibedView(this, parent);
}
namespace gui
{
VibedView::VibedView(Instrument* instrument, QWidget* parent) :
InstrumentViewFixedSize(instrument, parent),
m_volumeKnob(KnobType::Bright26, this),
m_stiffnessKnob(KnobType::Bright26, this),
m_pickKnob(KnobType::Bright26, this),
m_pickupKnob(KnobType::Bright26, this),
m_panKnob(KnobType::Bright26, this),
m_detuneKnob(KnobType::Bright26, this),
m_randomKnob(KnobType::Bright26, this),
m_lengthKnob(KnobType::Bright26, this),
m_graph(this),
m_impulse("", this),
m_power("", this, tr("Enable waveform")),
m_smoothBtn(this, tr("Smooth waveform")),
m_normalizeBtn(this, tr("Normalize waveform")),
m_sinWaveBtn(this, tr("Sine wave")),
m_triangleWaveBtn(this, tr("Triangle wave")),
m_sawWaveBtn(this, tr("Saw wave")),
m_sqrWaveBtn(this, tr("Square wave")),
m_whiteNoiseWaveBtn(this, tr("White noise")),
m_usrWaveBtn(this, tr("User-defined wave"))
{
setAutoFillBackground(true);
QPalette pal;
pal.setBrush(backgroundRole(), PLUGIN_NAME::getIconPixmap("artwork"));
setPalette(pal);
m_volumeKnob.setVolumeKnob(true);
m_volumeKnob.move(103, 142);
m_volumeKnob.setHintText(tr("String volume:"), "");
m_stiffnessKnob.move(129, 142);
m_stiffnessKnob.setHintText(tr("String stiffness:"), "");
m_pickKnob.move(153, 142);
m_pickKnob.setHintText(tr("Pick position:"), "");
m_pickupKnob.move(177, 142);
m_pickupKnob.setHintText(tr("Pickup position:"), "");
m_panKnob.move(105, 187);
m_panKnob.setHintText(tr("String panning:"), "");
m_detuneKnob.move(150, 187);
m_detuneKnob.setHintText(tr("String detune:"), "");
m_randomKnob.move(194, 187);
m_randomKnob.setHintText(tr("String fuzziness:"), "");
m_lengthKnob.move(23, 193);
m_lengthKnob.setHintText(tr("String length:"), "");
m_graph.setWindowTitle(tr("Impulse Editor"));
m_graph.setForeground(PLUGIN_NAME::getIconPixmap("wavegraph4"));
m_graph.move(76, 21);
m_graph.resize(132, 104);
m_impulse.move(23, 94);
m_impulse.setToolTip(tr("Impulse"));
m_power.move(212, 130);
m_power.setToolTip(tr("Enable/disable string"));
m_harmonic = std::make_unique<NineButtonSelector>(
std::array{
PLUGIN_NAME::getIconPixmap("button_-2_on"),
PLUGIN_NAME::getIconPixmap("button_-2_off"),
PLUGIN_NAME::getIconPixmap("button_-1_on"),
PLUGIN_NAME::getIconPixmap("button_-1_off"),
PLUGIN_NAME::getIconPixmap("button_f_on"),
PLUGIN_NAME::getIconPixmap("button_f_off"),
PLUGIN_NAME::getIconPixmap("button_2_on"),
PLUGIN_NAME::getIconPixmap("button_2_off"),
PLUGIN_NAME::getIconPixmap("button_3_on"),
PLUGIN_NAME::getIconPixmap("button_3_off"),
PLUGIN_NAME::getIconPixmap("button_4_on"),
PLUGIN_NAME::getIconPixmap("button_4_off"),
PLUGIN_NAME::getIconPixmap("button_5_on"),
PLUGIN_NAME::getIconPixmap("button_5_off"),
PLUGIN_NAME::getIconPixmap("button_6_on"),
PLUGIN_NAME::getIconPixmap("button_6_off"),
PLUGIN_NAME::getIconPixmap("button_7_on"),
PLUGIN_NAME::getIconPixmap("button_7_off")},
2,
21, 127,
this);
m_harmonic->setWindowTitle(tr("Octave"));
m_stringSelector = std::make_unique<NineButtonSelector>(
std::array{
PLUGIN_NAME::getIconPixmap("button_1_on"),
PLUGIN_NAME::getIconPixmap("button_1_off"),
PLUGIN_NAME::getIconPixmap("button_2_on"),
PLUGIN_NAME::getIconPixmap("button_2_off"),
PLUGIN_NAME::getIconPixmap("button_3_on"),
PLUGIN_NAME::getIconPixmap("button_3_off"),
PLUGIN_NAME::getIconPixmap("button_4_on"),
PLUGIN_NAME::getIconPixmap("button_4_off"),
PLUGIN_NAME::getIconPixmap("button_5_on"),
PLUGIN_NAME::getIconPixmap("button_5_off"),
PLUGIN_NAME::getIconPixmap("button_6_on"),
PLUGIN_NAME::getIconPixmap("button_6_off"),
PLUGIN_NAME::getIconPixmap("button_7_on"),
PLUGIN_NAME::getIconPixmap("button_7_off"),
PLUGIN_NAME::getIconPixmap("button_8_on"),
PLUGIN_NAME::getIconPixmap("button_8_off"),
PLUGIN_NAME::getIconPixmap("button_9_on"),
PLUGIN_NAME::getIconPixmap("button_9_off")},
0,
21, 39,
this);
// String selector is not a part of the model
m_stringSelector->setWindowTitle(tr("String"));
connect(m_stringSelector.get(), SIGNAL(NineButtonSelection(int)), this, SLOT(showString(int)));
showString(0);
m_smoothBtn.move(79, 129);
m_smoothBtn.setActiveGraphic(PLUGIN_NAME::getIconPixmap("smooth_active"));
m_smoothBtn.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("smooth_inactive"));
m_smoothBtn.setChecked(false);
m_smoothBtn.setToolTip(tr("Smooth waveform"));
connect(&m_smoothBtn, SIGNAL(clicked()), this, SLOT(smoothClicked()));
m_normalizeBtn.move(96, 129);
m_normalizeBtn.setActiveGraphic(PLUGIN_NAME::getIconPixmap("normalize_active"));
m_normalizeBtn.setInactiveGraphic(PLUGIN_NAME::getIconPixmap("normalize_inactive"));
m_normalizeBtn.setChecked(false);
m_normalizeBtn.setToolTip(tr("Normalize waveform"));
connect(&m_normalizeBtn, SIGNAL(clicked()), this, SLOT(normalizeClicked()));
m_sinWaveBtn.move(212, 24);
m_sinWaveBtn.setActiveGraphic(embed::getIconPixmap("sin_wave_active"));
m_sinWaveBtn.setInactiveGraphic(embed::getIconPixmap("sin_wave_inactive"));
m_sinWaveBtn.setToolTip(tr("Sine wave"));
connect(&m_sinWaveBtn, SIGNAL(clicked()), this, SLOT(sinWaveClicked()));
m_triangleWaveBtn.move(212, 41);
m_triangleWaveBtn.setActiveGraphic(embed::getIconPixmap("triangle_wave_active"));
m_triangleWaveBtn.setInactiveGraphic(embed::getIconPixmap("triangle_wave_inactive"));
m_triangleWaveBtn.setToolTip(tr("Triangle wave"));
connect(&m_triangleWaveBtn, SIGNAL(clicked()), this, SLOT(triangleWaveClicked()));
m_sawWaveBtn.move(212, 58);
m_sawWaveBtn.setActiveGraphic(embed::getIconPixmap("saw_wave_active"));
m_sawWaveBtn.setInactiveGraphic(embed::getIconPixmap("saw_wave_inactive"));
m_sawWaveBtn.setToolTip(tr("Saw wave"));
connect(&m_sawWaveBtn, SIGNAL(clicked()), this, SLOT(sawWaveClicked()));
m_sqrWaveBtn.move(212, 75);
m_sqrWaveBtn.setActiveGraphic(embed::getIconPixmap("square_wave_active"));
m_sqrWaveBtn.setInactiveGraphic(embed::getIconPixmap("square_wave_inactive"));
m_sqrWaveBtn.setToolTip(tr("Square wave"));
connect(&m_sqrWaveBtn, SIGNAL(clicked()), this, SLOT(sqrWaveClicked()));
m_whiteNoiseWaveBtn.move(212, 92);
m_whiteNoiseWaveBtn.setActiveGraphic(embed::getIconPixmap("white_noise_wave_active"));
m_whiteNoiseWaveBtn.setInactiveGraphic(embed::getIconPixmap("white_noise_wave_inactive"));
m_whiteNoiseWaveBtn.setToolTip(tr("White noise"));
connect(&m_whiteNoiseWaveBtn, SIGNAL(clicked()), this, SLOT(noiseWaveClicked()));
m_usrWaveBtn.move(212, 109);
m_usrWaveBtn.setActiveGraphic(embed::getIconPixmap("usr_wave_active"));
m_usrWaveBtn.setInactiveGraphic(embed::getIconPixmap("usr_wave_inactive"));
m_usrWaveBtn.setToolTip(tr("User-defined wave"));
connect(&m_usrWaveBtn, SIGNAL(clicked()),this, SLOT(usrWaveClicked()));
}
void VibedView::modelChanged()
{
showString(0);
}
void VibedView::showString(int str)
{
auto v = castModel<Vibed>();
m_pickKnob.setModel(v->m_pickModels[str].get());
m_pickupKnob.setModel(v->m_pickupModels[str].get());
m_stiffnessKnob.setModel(v->m_stiffnessModels[str].get());
m_volumeKnob.setModel(v->m_volumeModels[str].get());
m_panKnob.setModel(v->m_panModels[str].get());
m_detuneKnob.setModel(v->m_detuneModels[str].get());
m_randomKnob.setModel(v->m_randomModels[str].get());
m_lengthKnob.setModel(v->m_lengthModels[str].get());
m_graph.setModel(v->m_graphModels[str].get());
m_impulse.setModel(v->m_impulseModels[str].get());
m_harmonic->setModel(v->m_harmonicModels[str].get());
m_power.setModel(v->m_powerModels[str].get());
}
void VibedView::sinWaveClicked()
{
m_graph.model()->setWaveToSine();
Engine::getSong()->setModified();
}
void VibedView::triangleWaveClicked()
{
m_graph.model()->setWaveToTriangle();
Engine::getSong()->setModified();
}
void VibedView::sawWaveClicked()
{
m_graph.model()->setWaveToSaw();
Engine::getSong()->setModified();
}
void VibedView::sqrWaveClicked()
{
m_graph.model()->setWaveToSquare();
Engine::getSong()->setModified();
}
void VibedView::noiseWaveClicked()
{
m_graph.model()->setWaveToNoise();
Engine::getSong()->setModified();
}
void VibedView::usrWaveClicked()
{
QString fileName = m_graph.model()->setWaveToUser();
m_usrWaveBtn.setToolTip(fileName);
Engine::getSong()->setModified();
}
void VibedView::smoothClicked()
{
m_graph.model()->smooth();
Engine::getSong()->setModified();
}
void VibedView::normalizeClicked()
{
m_graph.model()->normalize();
Engine::getSong()->setModified();
}
void VibedView::contextMenuEvent(QContextMenuEvent*)
{
CaptionMenu contextMenu(model()->displayName(), this);
contextMenu.exec(QCursor::pos());
}
} // namespace gui
extern "C"
{
// necessary for getting instance out of shared lib
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*)
{
return new Vibed(static_cast<InstrumentTrack*>(m));
}
}
} // namespace lmms