Files
lmms/plugins/Lb302/Lb302.cpp
Fawn 005b1a8e66 Lb302 cleanup (#8132)
Makes the following changes to the Lb302 plugin for increased functionality, maintainability, and readability:

- Add note velocity & note panning to Lb302
- Remove evil mutex+dynamic array combo in favor of a real-time safe ringbuffer queue
- Use non-static data member initializers where possible
- Use `std::numbers::pi` instead of `M_PI`
- Use `std::array` and `std::unique_ptr` instead of manual memory management
- Change plain-old-data classes with only public members to structs
- Change some ints to f_cnt_t to better communicate their purpose and avoid weird implicit casting nonsense
- Change floats to sample_t where appropriate to better communicate their purpose
- Find suitable homes for loose constants
- Prefix standard library math function calls with std::
- Ensure all floating-point literals intended to be floats end in 'f' and are not doubles to appease MSVC
- Change uses of SIGNAL and SLOT macros to whatever they should be post-Qt6 upgrade
- Tidy code to conform to current code conventions

Also adds Hardware.h for low-level performance tools:

- busyWaitHint(), supporting x86, ARM, and RISC-V
- hardware_destructive_interference_size polyfill, supporting x86, ARM, RISC-V, and PPC
- Moved the contents of denormals.h to Hardware.h, renamed disable_denormals() to disableDenormals()
- Added ARM support to disableDenormals()
2026-04-29 23:19:42 -06:00

780 lines
26 KiB
C++

/*
* Lb302.cpp - Incomplete Roland TB-303 bass synth emulation
*
* Copyright (c) 2006-2008 Paul Giblock <pgib/at/users.sourceforge.net>
* Copyright (c) 2026 Fawn Sannar <rubiefawn/at/gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* Lb302FilterIIR2 is based on the gsyn filter code by Andy Sloane.
*
* Lb302Filter3Pole is based on the TB-303 instrument written by
* Josep M Comajuncosas for the CSounds library
*
* 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 "Lb302.h"
#include <cmath>
#include <numbers>
#include <QDebug>
#include "AutomatableButton.h"
#include "BandLimitedWave.h"
#include "DspEffectLibrary.h"
#include "Engine.h"
#include "embed.h"
#include "InstrumentPlayHandle.h"
#include "InstrumentTrack.h"
#include "Knob.h"
#include "LedCheckBox.h"
#include "NotePlayHandle.h"
#include "Oscillator.h"
#include "PixmapButton.h"
#include "plugin_export.h"
#define LB_24_IGNORE_ENVELOPE
//#define LB_24_RES_TRICK
namespace lmms
{
// Helper to get the phase increment per sample, given a note's frequency and the current sample rate
static inline float phaseInc(float freq) { return freq / Engine::audioEngine()->outputSampleRate(); }
extern "C"
{
Plugin::Descriptor PLUGIN_EXPORT lb302_plugin_descriptor =
{
LMMS_STRINGIFY(PLUGIN_NAME),
"LB302",
QT_TRANSLATE_NOOP("PluginBrowser", "Incomplete monophonic imitation TB-303"),
"Paul Giblock <pgib/at/users.sf.net>",
0x0100,
Plugin::Type::Instrument,
new PluginPixmapLoader("logo"),
nullptr,
nullptr,
};
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*)
{
return new Lb302Synth(static_cast<InstrumentTrack*>(m));
}
} // extern "C"
//
// Lb302Filter
//
void Lb302Filter::recalc()
{
m_vcf.e[1] = std::exp(6.109f + 1.5876f * fs->envmod + 2.1553f * fs->cutoff - 1.2f * (1.0f - fs->reso));
m_vcf.e[0] = std::exp(5.613f - 0.8f * fs->envmod + 2.1553f * fs->cutoff - 0.7696f * (1.0f - fs->reso));
const float pi_sr = std::numbers::pi_v<float> / Engine::audioEngine()->outputSampleRate();
m_vcf.e[0] *= pi_sr;
m_vcf.e[1] *= pi_sr;
m_vcf.e[1] -= m_vcf.e[0];
m_vcf.resCoeff = std::exp(-1.20f + 3.455f * fs->reso);
};
void Lb302Filter::envRecalc() { m_vcf.c0 *= fs->envdecay; }
void Lb302Filter::playNote() { m_vcf.c0 = m_vcf.e[1]; }
//
// Lb302FilterIIR2
//
Lb302FilterIIR2::Lb302FilterIIR2(Lb302FilterKnobState* p_fs)
: Lb302Filter(p_fs)
, m_dist{std::make_unique<DspEffectLibrary::Distortion>(1.f, 1.f)}
{};
void Lb302FilterIIR2::recalc()
{
Lb302Filter::recalc();
m_dist->setThreshold(fs->dist * 75.f);
};
void Lb302FilterIIR2::envRecalc()
{
Lb302Filter::envRecalc();
const float w = m_vcf.e[0] + m_vcf.c0; // e[0] is adjusted for Hz and doesn't need s_envInc
const float k = std::exp(-w / m_vcf.resCoeff); // Does this mean c0 is inheritantly?
m_iir2.a = 2.f * std::cos(2.f * w) * k;
m_iir2.b = -k * k;
m_iir2.c = 1.f - m_iir2.a - m_iir2.b;
}
sample_t Lb302FilterIIR2::process(sample_t samp)
{
sample_t ret = m_iir2.a * m_iir2.d[0] + m_iir2.b * m_iir2.d[1] + m_iir2.c * samp;
// Delayed samples for filter
m_iir2.d[1] = m_iir2.d[0];
m_iir2.d[0] = ret;
if (fs->dist > 0.f) { ret = m_dist->nextSample(ret); }
// output = IIR2 + dry
return ret;
}
//
// Lb302Filter3Pole
//
void Lb302Filter3Pole::recalc()
{
// DO NOT CALL BASE CLASS
m_vcf.e[0] = 0.000001f;
m_vcf.e[1] = 1.f;
}
void Lb302Filter3Pole::envRecalc()
{
Lb302Filter::envRecalc();
// e0 is adjusted for Hz and doesn't need s_envInc
float w = m_vcf.e[0] + m_vcf.c0;
float k = std::min(fs->cutoff, 0.975f);
// sampleRateCutoff should not be changed to anything dynamic that is outside the
// scope of LB302 (like e.g. the audio engine's sample rate) as this changes the filter's cutoff
// behavior without any modification to its controls.
constexpr float sampleRateCutoff = 44100.0f;
float kfco = 50.f + k * (
(2300.f - 1600.f * fs->envmod)
+ w * (700.f + 1500.f * k + (1500.f + k * (sampleRateCutoff / 2.f - 6000.f)) * fs->envmod)
); // + iacc * (0.3f + 0.7f * kfco * kenvmod) * kaccent * kaccurve * 2000.f
#ifdef LB_24_IGNORE_ENVELOPE
// m_kfcn = fs->cutoff;
m_kfcn = 2.f * kfco / Engine::audioEngine()->outputSampleRate();
#else
m_kfcn = w;
#endif
m_kp = ((-2.7528f * m_kfcn + 3.0429f) * m_kfcn + 1.718f) * m_kfcn - 0.9984f;
const auto kp1 = m_kp + 1.f;
m_kp1h = 0.5f * kp1;
#ifdef LB_24_RES_TRICK
k = std::exp(-w / m_vcf.resCoeff);
m_kres = k * (((-2.7079f * kp1 + 10.963f) * kp1 - 14.934f) * kp1 + 8.4974f);
#else
m_kres = fs->reso * (((-2.7079f * kp1 + 10.963f) * kp1 - 14.934f) * kp1 + 8.4974f);
#endif
m_value = 1.f + (fs->dist * (1.5f + 2.f * m_kres * (1.f - m_kfcn))); // ENVMOD was DIST
}
sample_t Lb302Filter3Pole::process(sample_t samp)
{
constexpr float volAdjust = 3.f;
const sample_t ax1 = m_lastin;
const std::array<sample_t, 2> ay1 = { m_ay[0], m_ay[1] };
m_lastin = samp - std::tanh(m_kres * m_ay[2]);
m_ay[0] = m_kp1h * (m_lastin + ax1) - m_kp * m_ay[0];
m_ay[1] = m_kp1h * (m_ay[0] + ay1[0]) - m_kp * m_ay[1];
m_ay[2] = m_kp1h * (m_ay[1] + ay1[1]) - m_kp * m_ay[2];
return std::tanh(m_ay[2] * m_value) * volAdjust / (1.f + fs->dist);
}
//
// Lb302Synth
//
Lb302Synth::Lb302Synth(InstrumentTrack* instrumentTrack)
: Instrument(instrumentTrack, &lb302_plugin_descriptor, nullptr, Flag::IsSingleStreamed)
, m_vcfCutKnob(0.75f, 0.0f, 1.5f, 0.005f, this, tr("VCF Cutoff Frequency"))
, m_vcfResKnob(0.75f, 0.0f, 1.25f, 0.005f, this, tr("VCF Resonance"))
, m_vcfModKnob(0.1f, 0.0f, 1.0f, 0.005f, this, tr("VCF Envelope Mod"))
, m_vcfDecKnob(0.1f, 0.0f, 1.0f, 0.005f, this, tr("VCF Envelope Decay"))
, m_distKnob(0.0f, 0.0f, 1.0f, 0.01f, this, tr("Distortion"))
, m_waveShape(8.0f, 0.0f, 11.0f, this, tr("Waveform"))
, m_slideDecKnob(0.6f, 0.0f, 1.0f, 0.005f, this, tr("Slide Decay"))
, m_slideToggle(false, this, tr("Slide"))
, m_accentToggle(false, this, tr("Accent"))
, m_deadToggle(false, this, tr("Dead"))
, m_db24Toggle(false, this, tr("24dB/oct Filter"))
, m_vcfs{std::make_unique<Lb302FilterIIR2>(&m_fs), std::make_unique<Lb302Filter3Pole>(&m_fs)}
{
connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, this, &Lb302Synth::filterChanged);
connect(&m_vcfCutKnob, &FloatModel::dataChanged, this, &Lb302Synth::filterChanged);
connect(&m_vcfResKnob, &FloatModel::dataChanged, this, &Lb302Synth::filterChanged);
connect(&m_vcfModKnob, &FloatModel::dataChanged, this, &Lb302Synth::filterChanged);
connect(&m_vcfDecKnob, &FloatModel::dataChanged, this, &Lb302Synth::decayChanged);
connect(&m_db24Toggle, &BoolModel::dataChanged, this, &Lb302Synth::db24Toggled);
connect(&m_distKnob, &FloatModel::dataChanged, this, &Lb302Synth::filterChanged);
// db24Toggled() would be called here, but all it does is call
// recalcFilter(), which is already done in filterChanged(), so there's no
// need to explicitly call recalcFilter() again.
filterChanged();
decayChanged();
Engine::audioEngine()->addPlayHandle(new InstrumentPlayHandle(this, instrumentTrack));
}
void Lb302Synth::saveSettings(QDomDocument& doc, QDomElement& el)
{
m_vcfCutKnob.saveSettings(doc, el, "vcf_cut");
m_vcfResKnob.saveSettings(doc, el, "vcf_res");
m_vcfModKnob.saveSettings(doc, el, "vcf_mod");
m_vcfDecKnob.saveSettings(doc, el, "vcf_dec");
m_waveShape.saveSettings(doc, el, "shape");
m_distKnob.saveSettings(doc, el, "dist");
m_slideDecKnob.saveSettings(doc, el, "slide_dec");
m_slideToggle.saveSettings(doc, el, "slide");
m_deadToggle.saveSettings(doc, el, "dead");
m_db24Toggle.saveSettings(doc, el, "db24");
}
void Lb302Synth::loadSettings(const QDomElement& el)
{
m_vcfCutKnob.loadSettings(el, "vcf_cut");
m_vcfResKnob.loadSettings(el, "vcf_res");
m_vcfModKnob.loadSettings(el, "vcf_mod");
m_vcfDecKnob.loadSettings(el, "vcf_dec");
m_distKnob.loadSettings(el, "dist");
m_slideDecKnob.loadSettings(el, "slide_dec");
m_waveShape.loadSettings(el, "shape");
m_slideToggle.loadSettings(el, "slide");
m_deadToggle.loadSettings(el, "dead");
m_db24Toggle.loadSettings(el, "db24");
// db24Toggled() would be called here, but all it does is call
// recalcFilter(), which is already done in filterChanged(), so there's no
// need to explicitly call recalcFilter() again.
filterChanged();
decayChanged();
}
void Lb302Synth::filterChanged()
{
m_fs.cutoff = m_vcfCutKnob.value();
m_fs.reso = m_vcfResKnob.value();
m_fs.envmod = m_vcfModKnob.value();
m_fs.dist = m_distKnob.value() * s_distRatio;
recalcFilter();
}
void Lb302Synth::decayChanged()
{
float d = (2.3f * m_vcfDecKnob.value() + 0.2f) * Engine::audioEngine()->outputSampleRate();
m_fs.envdecay = std::pow(0.1f, 1.0f / d * s_envInc);
}
void Lb302Synth::db24Toggled() { recalcFilter(); }
QString Lb302Synth::nodeName() const { return lb302_plugin_descriptor.name; }
void Lb302Synth::recalcFilter()
{
vcf().recalc();
m_vcfEnvPos = s_envInc; // Trigger filter envelope update in process()
}
void Lb302Synth::process(SampleFrame* outbuf, const f_cnt_t size)
{
if (m_releaseFrame == 0 || !m_playingNote) { m_vcaMode = VcaMode::Decay; }
if (m_playingNote)
{
constexpr float volRatio = 1.f / DefaultVolume;
m_noteVolume = m_playingNote->getVolume() * volRatio;
m_notePan = std::clamp(m_playingNote->getPanning(), PanningLeft, PanningRight);
}
const auto vv = panningToVolumeVector(m_notePan, m_noteVolume);
Lb302Filter& filter = vcf(); // Hold on to the current VCF, and use it throughout this period
if (m_newFreq)
{
m_newFreq = false;
const bool noteIsDead = m_deadToggle.value();
m_vcoInc = phaseInc(m_trueFreq);
// Always reset vca on non-dead notes, and only reset vca on decaying (decayed) and never-played
if (!noteIsDead || (m_vcaMode == VcaMode::Decay || m_vcaMode == VcaMode::NeverPlayed))
{
m_vcaMode = VcaMode::Attack;
}
else { m_vcaMode = VcaMode::Idle; }
if (m_slideInc != 0.f)
{
// Initiate Slide
m_slide = m_vcoInc - m_slideInc; // Slide amount
m_slideBase = m_vcoInc; // The REAL frequency
m_slideInc = 0.f; // reset from-note
}
else { m_slide = 0.f; }
// Slide-from note, save inc for next note
// May need to equal m_slideBase + m_slide if last note slid
if (m_slideToggle.value()) { m_slideInc = m_vcoInc; }
recalcFilter();
if (!noteIsDead)
{
filter.playNote();
m_vcfEnvPos = s_envInc; // Ensure envelope is recalculated
}
}
// Note: this has to be computed during processing and cannot be initialized
// in the constructor because it's dependent on the sample rate and that might
// change during rendering!
//
// At 44.1 kHz this will compute something very close to the previously
// hard coded value of 0.99897516.
constexpr auto computeDecayFactor = [](float decayTimeInSeconds, float targetedAttenuation) -> float
{
// This is the number of samples that correspond to the decay time in seconds
auto samplesNeededForDecay = decayTimeInSeconds * Engine::audioEngine()->outputSampleRate();
// This computes the factor that's needed to make a signal with a value of 1 decay to the
// targeted attenuation over the time in number of samples.
return std::pow(targetedAttenuation, 1.f / samplesNeededForDecay);
};
constexpr auto gateThreshold = 1.f / 65536.f; // Signal below this value is silenced
const auto decay = computeDecayFactor(0.245260770975f, gateThreshold);
const float sampleRatio = 44100.f / Engine::audioEngine()->outputSampleRate();
for (f_cnt_t i = 0; i < size; ++i)
{
// start decay if we're past release
if (i >= m_releaseFrame) { m_vcaMode = VcaMode::Decay; }
// update vcf
if (m_vcfEnvPos >= s_envInc)
{
filter.envRecalc();
m_vcfEnvPos = 0;
if (m_slide)
{
m_vcoInc = m_slideBase - m_slide;
// Calculate coeff from dec_knob on knob change.
// TODO: Adjust for s_envInc
m_slide -= m_slide * (0.1f - m_slideDecKnob.value() * 0.0999f) * sampleRatio;
}
}
m_vcfEnvPos++;
// update vco
m_vcoC += m_vcoInc;
if (m_vcoC > 0.5f) { m_vcoC -= 1.f; }
m_vcoShape = static_cast<VcoShape>(m_waveShape.value());
// TODO: Add VCO shape parameters (p0, p1) that changes the shape of
// each waveform. Merge sawtooths with triangle, and merge square with
// round square?
switch (m_vcoShape)
{
// p0: curviness of line
// Is this sawtooth backwards?
case VcoShape::Sawtooth: m_vcoK = m_vcoC; break;
// p0: duty rev.saw<->triangle<->saw
// p1: curviness
case VcoShape::Triangle:
m_vcoK = m_vcoC * 2.f + 0.5f;
if (m_vcoK > 0.5f) { m_vcoK = 1.f - m_vcoK; }
break;
// p0: slope of top
case VcoShape::Square:
m_vcoK = m_vcoC < 0.f ? 0.5f : -0.5f;
break;
// p0: width of round
case VcoShape::RoundSquare:
m_vcoK = m_vcoC < 0.f ? std::sqrt(1.f - (m_vcoC * m_vcoC * 4.f)) - 0.5f : -0.5f;
break;
// Maybe the fall should be exponential/sinsoidal instead of quadric.
// [-0.5, 0]: Rise, [0,0.25]: Slope down, [0.25,0.5]: Low
case VcoShape::Moog:
m_vcoK = m_vcoC * 2.f + 0.5f;
if (m_vcoK > 1.f) { m_vcoK = -0.5f; }
else if (m_vcoK > 0.5f)
{
float w = 2.f * (m_vcoK - 0.5f) - 1.f;
m_vcoK = 0.5f - std::sqrt(1.f - (w * w));
}
m_vcoK *= 2.f; // MOOG wave gets filtered away
break;
// [-0.5, 0.5] : [-pi, pi]
case VcoShape::Sine: m_vcoK = 0.5f * Oscillator::sinSample(m_vcoC); break;
case VcoShape::Exponential: m_vcoK = 0.5f * Oscillator::expSample(m_vcoC); break;
case VcoShape::WhiteNoise: m_vcoK = 0.5f * Oscillator::noiseSample(m_vcoC); break;
// The next cases all use the BandLimitedWave class which uses the oscillator increment `m_vcoInc` to compute samples.
// If that oscillator increment is 0 we return a 0 sample because calling BandLimitedWave::pdToLen(0) leads to a
// division by 0 which in turn leads to floating point exceptions.
case VcoShape::BLSawtooth:
m_vcoK = m_vcoInc == 0.f ? 0.f : BandLimitedWave::oscillate(m_vcoC + 0.5f, BandLimitedWave::pdToLen(m_vcoInc), BandLimitedWave::Waveform::BLSaw) * 0.5f;
break;
case VcoShape::BLSquare:
m_vcoK = m_vcoInc == 0.f ? 0.f : BandLimitedWave::oscillate(m_vcoC + 0.5f, BandLimitedWave::pdToLen(m_vcoInc), BandLimitedWave::Waveform::BLSquare) * 0.5f;
break;
case VcoShape::BLTriangle:
m_vcoK = m_vcoInc == 0.f ? 0.f : BandLimitedWave::oscillate(m_vcoC + 0.5f, BandLimitedWave::pdToLen(m_vcoInc), BandLimitedWave::Waveform::BLTriangle) * 0.5f;
break;
case VcoShape::BLMoog:
m_vcoK = m_vcoInc == 0.f ? 0.f : BandLimitedWave::oscillate(m_vcoC + 0.5f, BandLimitedWave::pdToLen(m_vcoInc), BandLimitedWave::Waveform::BLMoog);
break;
}
// Write out samples.
sample_t samp = filter.process(m_vcoK) * m_vca;
for (ch_cnt_t c = 0; c < DEFAULT_CHANNELS; c++) { outbuf[i][c] = samp * vv.vol[c]; }
// Handle Envelope
if (m_vcaMode == VcaMode::Attack)
{
m_vca += (s_vcaInitial - m_vca) * s_vcaAttack;
}
else if (m_vcaMode == VcaMode::Decay)
{
m_vca *= decay;
// the following line actually speeds up processing
if (m_vca < gateThreshold)
{
m_vca = 0;
m_vcaMode = VcaMode::NeverPlayed;
}
}
}
}
void Lb302Synth::playNote(NotePlayHandle* nph, SampleFrame*)
{
if (nph->isMasterNote() || (nph->hasParent() && nph->isReleased())) { return; }
// Enqueue new note in m_notes
auto tries = s_maxNoteEnqueueRetries;
auto writeClaimedExpected = m_notesWriteClaimed.load(std::memory_order_relaxed);
std::size_t index;
std::size_t nextIndex;
for (;;)
{
if (!tries--)
{
qDebug() << "Lb302: Note dropped due to catastrophically poor performance! This should never happen!";
return;
}
const auto occupied = static_cast<std::ptrdiff_t>(writeClaimedExpected)
- static_cast<std::ptrdiff_t>(m_notesReadSeq.load(std::memory_order_acquire));
assert(occupied >= 0);
// TODO C++23: [[assume(occupied >= 0)]]
if (static_cast<std::size_t>(occupied) >= s_maxPendingNotes)
{
// Queue is full, try again (at least one sender always makes progress)
busyWaitHint();
writeClaimedExpected = m_notesWriteClaimed.load(std::memory_order_relaxed);
continue;
}
index = writeClaimedExpected;
nextIndex = writeClaimedExpected + 1;
if (m_notesWriteClaimed.compare_exchange_strong(
writeClaimedExpected,
nextIndex,
std::memory_order_acquire)
) { break; } // Note sent
}
m_notes[index & s_notesBufMask] = nph;
std::size_t writeCommittedExpected = index;
while (!m_notesWriteCommitted.compare_exchange_strong(writeCommittedExpected, nextIndex, std::memory_order_release))
{
writeCommittedExpected = index; // Reset this as the CAS will have changed it
busyWaitHint();
}
}
void Lb302Synth::processNote(NotePlayHandle* nph)
{
/// Start a new note.
if (nph->m_pluginData != this)
{
m_playingNote = nph;
nph->m_pluginData = this;
m_newFreq = true;
}
m_releaseFrame = std::max(m_releaseFrame, nph->framesLeft() + nph->offset());
if (!m_playingNote && !nph->isReleased() && m_releaseFrame > 0)
{
m_playingNote = nph;
nph->m_pluginData = this;
if (m_slideToggle.value()) { m_slideInc = phaseInc(nph->frequency()); }
}
// Check for slide
if (m_playingNote == nph)
{
m_trueFreq = nph->frequency();
const auto trueInc = phaseInc(m_trueFreq);
if (m_slideToggle.value()) { m_slideBase = trueInc; } else { m_vcoInc = trueInc; }
}
}
void Lb302Synth::play(SampleFrame* working_buffer)
{
const auto readIdx = m_notesReadSeq.load(std::memory_order_relaxed);
const auto writeCommitted = m_notesWriteCommitted.load(std::memory_order_acquire);
// Process notes, but process new notes last
for (auto i = readIdx; i < writeCommitted; ++i)
{
const auto& nph = m_notes[i & s_notesBufMask];
if (nph->totalFramesPlayed() == 0) { continue; }
processNote(nph);
}
for (auto i = readIdx; i < writeCommitted; ++i)
{
const auto& nph = m_notes[i & s_notesBufMask];
if (nph->totalFramesPlayed() != 0) { continue; }
processNote(nph);
}
// Mark the processed notes as having been read so that playNote() calls can overwrite them
m_notesReadSeq.fetch_add(writeCommitted - readIdx, std::memory_order_release);
process(working_buffer, Engine::audioEngine()->framesPerPeriod());
}
void Lb302Synth::deleteNotePluginData(NotePlayHandle* nph)
{
if (m_playingNote == nph) { m_playingNote = nullptr; }
}
gui::PluginView* Lb302Synth::instantiateView(QWidget* parent)
{
return new gui::Lb302SynthView(this, parent);
}
//
// gui::Lb302SynthView
//
namespace gui
{
Lb302SynthView::Lb302SynthView(Instrument* instrument, QWidget* parent)
: InstrumentViewFixedSize(instrument, parent)
{
setAutoFillBackground(true);
static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork");
QPalette pal;
pal.setBrush(backgroundRole(), s_artwork);
setPalette(pal);
// GUI
m_vcfCutKnob = new Knob(KnobType::Bright26, this);
m_vcfCutKnob->move(75, 130);
m_vcfCutKnob->setHintText(tr("Cutoff Freq:"), "");
m_vcfResKnob = new Knob(KnobType::Bright26, this);
m_vcfResKnob->move(120, 130);
m_vcfResKnob->setHintText(tr("Resonance:"), "");
m_vcfModKnob = new Knob(KnobType::Bright26, this);
m_vcfModKnob->move(165, 130);
m_vcfModKnob->setHintText(tr("Env Mod:"), "");
m_vcfDecKnob = new Knob(KnobType::Bright26, this);
m_vcfDecKnob->move(210, 130);
m_vcfDecKnob->setHintText(tr("Decay:"), "");
m_slideToggle = new LedCheckBox("", this);
m_slideToggle->move(10, 180);
// accent removed pending real implementation - no need for non-functional buttons
/* m_accentToggle = new LedCheckBox("", this);
m_accentToggle->move(10, 200); */
m_deadToggle = new LedCheckBox("", this);
m_deadToggle->move(10, 200);
m_db24Toggle = new LedCheckBox("", this);
m_db24Toggle->move(10, 150);
m_db24Toggle->setToolTip(tr("303-es-que, 24dB/octave, 3 pole filter"));
m_slideDecKnob = new Knob(KnobType::Bright26, this);
m_slideDecKnob->move(210, 75);
m_slideDecKnob->setHintText(tr("Slide Decay:"), "");
m_distKnob = new Knob(KnobType::Bright26, this);
m_distKnob->move(210, 190);
m_distKnob->setHintText(tr("DIST:"), "");
// Shapes
const int waveBtnX = 10;
const int waveBtnY = 96;
m_waveBtnGrp = new AutomatableButtonGroup(this);
auto sawWaveBtn = new PixmapButton(this, tr("Saw wave"));
sawWaveBtn->move(waveBtnX, waveBtnY);
sawWaveBtn->setActiveGraphic(embed::getIconPixmap("saw_wave_active"));
sawWaveBtn->setInactiveGraphic(embed::getIconPixmap("saw_wave_inactive"));
sawWaveBtn->setToolTip(tr("Click here for a sawtooth wave."));
m_waveBtnGrp->addButton(sawWaveBtn);
auto triangleWaveBtn = new PixmapButton(this, tr("Triangle wave"));
triangleWaveBtn->move(waveBtnX + (16 * 1), waveBtnY);
triangleWaveBtn->setActiveGraphic(embed::getIconPixmap("triangle_wave_active"));
triangleWaveBtn->setInactiveGraphic(embed::getIconPixmap("triangle_wave_inactive"));
triangleWaveBtn->setToolTip(tr("Click here for a triangle wave."));
m_waveBtnGrp->addButton(triangleWaveBtn);
auto sqrWaveBtn = new PixmapButton(this, tr("Square wave"));
sqrWaveBtn->move(waveBtnX + (16 * 2), waveBtnY);
sqrWaveBtn->setActiveGraphic(embed::getIconPixmap("square_wave_active"));
sqrWaveBtn->setInactiveGraphic(embed::getIconPixmap("square_wave_inactive"));
sqrWaveBtn->setToolTip(tr("Click here for a square wave."));
m_waveBtnGrp->addButton(sqrWaveBtn);
auto roundSqrWaveBtn = new PixmapButton(this, tr("Rounded square wave"));
roundSqrWaveBtn->move(waveBtnX + (16 * 3), waveBtnY);
roundSqrWaveBtn->setActiveGraphic( embed::getIconPixmap("round_square_wave_active"));
roundSqrWaveBtn->setInactiveGraphic( embed::getIconPixmap("round_square_wave_inactive"));
roundSqrWaveBtn->setToolTip(tr("Click here for a square wave with a rounded end."));
m_waveBtnGrp->addButton(roundSqrWaveBtn);
auto moogWaveBtn = new PixmapButton(this, tr("Moog wave"));
moogWaveBtn->move(waveBtnX + (16 * 4), waveBtnY);
moogWaveBtn->setActiveGraphic(embed::getIconPixmap("moog_saw_wave_active"));
moogWaveBtn->setInactiveGraphic(embed::getIconPixmap("moog_saw_wave_inactive"));
moogWaveBtn->setToolTip(tr("Click here for a moog-like wave."));
m_waveBtnGrp->addButton(moogWaveBtn);
auto sinWaveBtn = new PixmapButton(this, tr("Sine wave"));
sinWaveBtn->move(waveBtnX + (16 * 5), waveBtnY);
sinWaveBtn->setActiveGraphic(embed::getIconPixmap("sin_wave_active"));
sinWaveBtn->setInactiveGraphic(embed::getIconPixmap("sin_wave_inactive"));
sinWaveBtn->setToolTip(tr("Click for a sine wave."));
m_waveBtnGrp->addButton(sinWaveBtn);
auto exponentialWaveBtn = new PixmapButton(this, tr("White noise wave"));
exponentialWaveBtn->move(waveBtnX + (16 * 6), waveBtnY);
exponentialWaveBtn->setActiveGraphic(embed::getIconPixmap("exp_wave_active"));
exponentialWaveBtn->setInactiveGraphic(embed::getIconPixmap("exp_wave_inactive"));
exponentialWaveBtn->setToolTip(tr("Click here for an exponential wave."));
m_waveBtnGrp->addButton(exponentialWaveBtn);
auto whiteNoiseWaveBtn = new PixmapButton(this, tr("White noise wave"));
whiteNoiseWaveBtn->move(waveBtnX + (16 * 7), waveBtnY);
whiteNoiseWaveBtn->setActiveGraphic(embed::getIconPixmap("white_noise_wave_active"));
whiteNoiseWaveBtn->setInactiveGraphic(embed::getIconPixmap("white_noise_wave_inactive"));
whiteNoiseWaveBtn->setToolTip(tr("Click here for white noise."));
m_waveBtnGrp->addButton(whiteNoiseWaveBtn);
auto blSawWaveBtn = new PixmapButton(this, tr("Bandlimited saw wave"));
blSawWaveBtn->move(waveBtnX + (16 * 9) - 8, waveBtnY);
blSawWaveBtn->setActiveGraphic(embed::getIconPixmap("saw_wave_active"));
blSawWaveBtn->setInactiveGraphic(embed::getIconPixmap("saw_wave_inactive"));
blSawWaveBtn->setToolTip(tr("Click here for bandlimited sawtooth wave."));
m_waveBtnGrp->addButton(blSawWaveBtn);
auto blSquareWaveBtn = new PixmapButton(this, tr("Bandlimited square wave"));
blSquareWaveBtn->move(waveBtnX + (16 * 10) - 8, waveBtnY);
blSquareWaveBtn->setActiveGraphic(embed::getIconPixmap("square_wave_active"));
blSquareWaveBtn->setInactiveGraphic(embed::getIconPixmap("square_wave_inactive"));
blSquareWaveBtn->setToolTip(tr("Click here for bandlimited square wave."));
m_waveBtnGrp->addButton(blSquareWaveBtn);
auto blTriangleWaveBtn = new PixmapButton(this, tr("Bandlimited triangle wave"));
blTriangleWaveBtn->move(waveBtnX + (16 * 11) - 8, waveBtnY);
blTriangleWaveBtn->setActiveGraphic(embed::getIconPixmap("triangle_wave_active"));
blTriangleWaveBtn->setInactiveGraphic(embed::getIconPixmap("triangle_wave_inactive"));
blTriangleWaveBtn->setToolTip(tr("Click here for bandlimited triangle wave."));
m_waveBtnGrp->addButton(blTriangleWaveBtn);
auto blMoogWaveBtn = new PixmapButton(this, tr("Bandlimited moog saw wave"));
blMoogWaveBtn->move(waveBtnX + (16 * 12) - 8, waveBtnY);
blMoogWaveBtn->setActiveGraphic(embed::getIconPixmap("moog_saw_wave_active"));
blMoogWaveBtn->setInactiveGraphic(embed::getIconPixmap("moog_saw_wave_inactive"));
blMoogWaveBtn->setToolTip(tr("Click here for bandlimited moog-like wave."));
m_waveBtnGrp->addButton(blMoogWaveBtn);
}
void Lb302SynthView::modelChanged()
{
auto syn = castModel<Lb302Synth>();
m_vcfCutKnob->setModel(&syn->m_vcfCutKnob);
m_vcfResKnob->setModel(&syn->m_vcfResKnob);
m_vcfDecKnob->setModel(&syn->m_vcfDecKnob);
m_vcfModKnob->setModel(&syn->m_vcfModKnob);
m_slideDecKnob->setModel(&syn->m_slideDecKnob);
m_distKnob->setModel(&syn->m_distKnob);
m_waveBtnGrp->setModel(&syn->m_waveShape);
m_slideToggle->setModel(&syn->m_slideToggle);
// m_accentToggle->setModel(&syn->accentToggle);
m_deadToggle->setModel(&syn->m_deadToggle);
m_db24Toggle->setModel(&syn->m_db24Toggle);
}
} // namespace gui
} // namespace lmms