mirror of
https://github.com/LMMS/lmms.git
synced 2026-01-24 06:18:10 -05:00
* use c++ std::* math functions This updates usages of sin, cos, tan, pow, exp, log, log10, sqrt, fmod, fabs, and fabsf, excluding any usages that look like they might be part of a submodule or 3rd-party code. There's probably some std math functions not listed here that haven't been updated yet. * fix std::sqrt typo lmao one always sneaks by * Apply code review suggestions - std::pow(2, x) -> std::exp2(x) - std::pow(10, x) -> lmms::fastPow10f(x) - std::pow(x, 2) -> x * x, std::pow(x, 3) -> x * x * x, etc. - Resolve TODOs, fix typos, and so forth Co-authored-by: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> * Fix double -> float truncation, DrumSynth fix I mistakenly introduced a bug in my recent PR regarding template constants, in which a -1 that was supposed to appear outside of an abs() instead was moved inside it, screwing up the generated waveform. I fixed that and also simplified the function by factoring out the phase domain wrapping using the new `ediv()` function from this PR. It should behave how it's supposed to now... assuming all my parentheses are in the right place lol * Annotate magic numbers with TODOs for C++20 * On second thought, why wait? What else is lmms::numbers for? * begone inline Co-authored-by: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> * begone other inline Co-authored-by: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> * Re-inline function in lmms_math.h For functions, constexpr implies inline so this just re-adds inline to the one that isn't constexpr yet * Formatting fixes, readability improvements Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Fix previously missed pow() calls, cleanup Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Just delete ediv() entirely lmao No ediv(), no std::fmod(), no std::remainder(), just std::floor(). It should all work for negative phase inputs as well. If I end up needing ediv() in the future, I can add it then. * Simplify DrumSynth triangle waveform This reuses more work and is also a lot more easy to visualize. It's probably a meaningless micro-optimization, but it might be worth changing it back to a switch-case and just calculating ph_tau and saw01 at the beginning of the function in all code paths, even if it goes unused for the first two cases. Guess I'll see if anybody has strong opinions about it. * Move multiplication inside abs() * Clean up a few more pow(x, 2) -> x * x * Remove numbers::inv_pi, numbers::inv_tau * delete spooky leading 0 Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> --------- Co-authored-by: Rossmaxx <74815851+Rossmaxx@users.noreply.github.com> Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
786 lines
24 KiB
C++
786 lines
24 KiB
C++
/*
|
|
* SidInstrument.cpp - ResID based software-synthesizer
|
|
*
|
|
* Copyright (c) 2008 Csaba Hruska <csaba.hruska/at/gmail.com>
|
|
* Attila Herman <attila589/at/gmail.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 <QDomElement>
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
|
|
#include <sid.h>
|
|
|
|
#include "SidInstrument.h"
|
|
#include "AudioEngine.h"
|
|
#include "Engine.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "Knob.h"
|
|
#include "NotePlayHandle.h"
|
|
#include "PixmapButton.h"
|
|
|
|
#include "embed.h"
|
|
#include "plugin_export.h"
|
|
|
|
namespace lmms
|
|
{
|
|
|
|
|
|
#define C64_PAL_CYCLES_PER_SEC 985248
|
|
|
|
#define NUMSIDREGS 0x19
|
|
#define SIDWRITEDELAY 9 // lda $xxxx,x 4 cycles, sta $d400,x 5 cycles
|
|
#define SIDWAVEDELAY 4 // and $xxxx,x 4 cycles extra
|
|
|
|
auto sidorder = std::array<unsigned char, 25>
|
|
{0x15,0x16,0x18,0x17,
|
|
0x05,0x06,0x02,0x03,0x00,0x01,0x04,
|
|
0x0c,0x0d,0x09,0x0a,0x07,0x08,0x0b,
|
|
0x13,0x14,0x10,0x11,0x0e,0x0f,0x12};
|
|
|
|
static auto attackTime = std::array<const char*, 16>{ "2 ms", "8 ms", "16 ms", "24 ms",
|
|
"38 ms", "56 ms", "68 ms", "80 ms",
|
|
"100 ms", "250 ms", "500 ms", "800 ms",
|
|
"1 s", "3 s", "5 s", "8 s" };
|
|
static auto decRelTime = std::array<const char*, 16>{ "6 ms", "24 ms", "48 ms", "72 ms",
|
|
"114 ms", "168 ms", "204 ms", "240 ms",
|
|
"300 ms", "750 ms", "1.5 s", "2.4 s",
|
|
"3 s", "9 s", "15 s", "24 s" };
|
|
// release time time in ms
|
|
static const auto relTime = std::array{ 6, 24, 48, 72, 114, 168, 204, 240, 300, 750,
|
|
1500, 2400, 3000, 9000, 15000, 24000 };
|
|
|
|
|
|
extern "C"
|
|
{
|
|
Plugin::Descriptor PLUGIN_EXPORT sid_plugin_descriptor =
|
|
{
|
|
LMMS_STRINGIFY( PLUGIN_NAME ),
|
|
"SID",
|
|
QT_TRANSLATE_NOOP( "PluginBrowser", "Emulation of the MOS6581 and MOS8580 "
|
|
"SID.\nThis chip was used in the Commodore 64 computer." ),
|
|
|
|
"Csaba Hruska <csaba.hruska/at/gmail.com>"
|
|
"Attila Herman <attila589/at/gmail.com>",
|
|
0x0100,
|
|
Plugin::Type::Instrument,
|
|
new PluginPixmapLoader( "logo" ),
|
|
nullptr,
|
|
nullptr,
|
|
} ;
|
|
|
|
}
|
|
|
|
VoiceObject::VoiceObject( Model * _parent, int _idx ) :
|
|
Model( _parent ),
|
|
m_pulseWidthModel( 2048.0f, 0.0f, 4095.0f, 1.0f, this,
|
|
tr( "Voice %1 pulse width" ).arg( _idx+1 ) ),
|
|
m_attackModel( 8.0f, 0.0f, 15.0f, 1.0f, this,
|
|
tr( "Voice %1 attack" ).arg( _idx+1 ) ),
|
|
m_decayModel( 8.0f, 0.0f, 15.0f, 1.0f, this,
|
|
tr( "Voice %1 decay" ).arg( _idx+1 ) ),
|
|
m_sustainModel( 15.0f, 0.0f, 15.0f, 1.0f, this,
|
|
tr( "Voice %1 sustain" ).arg( _idx+1 ) ),
|
|
m_releaseModel( 8.0f, 0.0f, 15.0f, 1.0f, this,
|
|
tr( "Voice %1 release" ).arg( _idx+1 ) ),
|
|
m_coarseModel( 0.0f, -24.0, 24.0, 1.0f, this,
|
|
tr( "Voice %1 coarse detuning" ).arg( _idx+1 ) ),
|
|
m_waveFormModel( static_cast<int>(WaveForm::Triangle), 0, NumWaveShapes-1, this,
|
|
tr( "Voice %1 wave shape" ).arg( _idx+1 ) ),
|
|
|
|
m_syncModel( false, this, tr( "Voice %1 sync" ).arg( _idx+1 ) ),
|
|
m_ringModModel( false, this, tr( "Voice %1 ring modulate" ).arg( _idx+1 ) ),
|
|
m_filteredModel( false, this, tr( "Voice %1 filtered" ).arg( _idx+1 ) ),
|
|
m_testModel( false, this, tr( "Voice %1 test" ).arg( _idx+1 ) )
|
|
{
|
|
}
|
|
|
|
|
|
SidInstrument::SidInstrument( InstrumentTrack * _instrument_track ) :
|
|
Instrument( _instrument_track, &sid_plugin_descriptor ),
|
|
// filter
|
|
m_filterFCModel( 1024.0f, 0.0f, 2047.0f, 1.0f, this, tr( "Cutoff frequency" ) ),
|
|
m_filterResonanceModel( 8.0f, 0.0f, 15.0f, 1.0f, this, tr( "Resonance" ) ),
|
|
m_filterModeModel( static_cast<int>(FilterType::LowPass), 0, NumFilterTypes-1, this, tr( "Filter type" )),
|
|
|
|
// misc
|
|
m_voice3OffModel( false, this, tr( "Voice 3 off" ) ),
|
|
m_volumeModel( 15.0f, 0.0f, 15.0f, 1.0f, this, tr( "Volume" ) ),
|
|
m_chipModel( static_cast<int>(ChipModel::MOS8580), 0, NumChipModels-1, this, tr( "Chip model" ) )
|
|
{
|
|
// A Filter object needs to be created only once to do some initialization, avoiding
|
|
// dropouts down the line when we have to play a note for the first time.
|
|
[[maybe_unused]] static auto s_filter = reSID::Filter{};
|
|
|
|
for( int i = 0; i < 3; ++i )
|
|
{
|
|
m_voice[i] = new VoiceObject( this, i );
|
|
}
|
|
}
|
|
|
|
|
|
void SidInstrument::saveSettings( QDomDocument & _doc,
|
|
QDomElement & _this )
|
|
{
|
|
// voices
|
|
for( int i = 0; i < 3; ++i )
|
|
{
|
|
const QString is = QString::number( i );
|
|
|
|
m_voice[i]->m_pulseWidthModel.saveSettings(
|
|
_doc, _this, "pulsewidth" + is );
|
|
m_voice[i]->m_attackModel.saveSettings(
|
|
_doc, _this, "attack" + is );
|
|
m_voice[i]->m_decayModel.saveSettings(
|
|
_doc, _this, "decay" + is );
|
|
m_voice[i]->m_sustainModel.saveSettings(
|
|
_doc, _this, "sustain" + is );
|
|
m_voice[i]->m_releaseModel.saveSettings(
|
|
_doc, _this, "release" + is );
|
|
m_voice[i]->m_coarseModel.saveSettings(
|
|
_doc, _this, "coarse" + is );
|
|
m_voice[i]->m_waveFormModel.saveSettings(
|
|
_doc, _this,"waveform" + is );
|
|
m_voice[i]->m_syncModel.saveSettings(
|
|
_doc, _this, "sync" + is );
|
|
m_voice[i]->m_ringModModel.saveSettings(
|
|
_doc, _this, "ringmod" + is );
|
|
m_voice[i]->m_filteredModel.saveSettings(
|
|
_doc, _this,"filtered" + is );
|
|
m_voice[i]->m_testModel.saveSettings(
|
|
_doc, _this, "test" + is );
|
|
}
|
|
|
|
// filter
|
|
m_filterFCModel.saveSettings( _doc, _this, "filterFC" );
|
|
m_filterResonanceModel.saveSettings( _doc, _this, "filterResonance" );
|
|
m_filterModeModel.saveSettings( _doc, _this, "filterMode" );
|
|
|
|
// misc
|
|
m_voice3OffModel.saveSettings( _doc, _this, "voice3Off" );
|
|
m_volumeModel.saveSettings( _doc, _this, "volume" );
|
|
m_chipModel.saveSettings( _doc, _this, "chipModel" );
|
|
}
|
|
|
|
|
|
|
|
|
|
void SidInstrument::loadSettings( const QDomElement & _this )
|
|
{
|
|
// voices
|
|
for( int i = 0; i < 3; ++i )
|
|
{
|
|
const QString is = QString::number( i );
|
|
|
|
m_voice[i]->m_pulseWidthModel.loadSettings( _this, "pulsewidth" + is );
|
|
m_voice[i]->m_attackModel.loadSettings( _this, "attack" + is );
|
|
m_voice[i]->m_decayModel.loadSettings( _this, "decay" + is );
|
|
m_voice[i]->m_sustainModel.loadSettings( _this, "sustain" + is );
|
|
m_voice[i]->m_releaseModel.loadSettings( _this, "release" + is );
|
|
m_voice[i]->m_coarseModel.loadSettings( _this, "coarse" + is );
|
|
m_voice[i]->m_waveFormModel.loadSettings( _this, "waveform" + is );
|
|
m_voice[i]->m_syncModel.loadSettings( _this, "sync" + is );
|
|
m_voice[i]->m_ringModModel.loadSettings( _this, "ringmod" + is );
|
|
m_voice[i]->m_filteredModel.loadSettings( _this, "filtered" + is );
|
|
m_voice[i]->m_testModel.loadSettings( _this, "test" + is );
|
|
}
|
|
|
|
// filter
|
|
m_filterFCModel.loadSettings( _this, "filterFC" );
|
|
m_filterResonanceModel.loadSettings( _this, "filterResonance" );
|
|
m_filterModeModel.loadSettings( _this, "filterMode" );
|
|
|
|
// misc
|
|
m_voice3OffModel.loadSettings( _this, "voice3Off" );
|
|
m_volumeModel.loadSettings( _this, "volume" );
|
|
m_chipModel.loadSettings( _this, "chipModel" );
|
|
}
|
|
|
|
|
|
|
|
|
|
QString SidInstrument::nodeName() const
|
|
{
|
|
return( sid_plugin_descriptor.name );
|
|
}
|
|
|
|
|
|
float SidInstrument::desiredReleaseTimeMs() const
|
|
{
|
|
int maxrel = 0;
|
|
for (const auto& voice : m_voice)
|
|
{
|
|
maxrel = std::max(maxrel, static_cast<int>(voice->m_releaseModel.value()));
|
|
}
|
|
|
|
return computeReleaseTimeMsByFrameCount(relTime[maxrel]);
|
|
}
|
|
|
|
|
|
static int sid_fillbuffer(unsigned char* sidreg, reSID::SID *sid, int tdelta, short *ptr, int samples)
|
|
{
|
|
int total = 0;
|
|
// customly added
|
|
int residdelay = 0;
|
|
|
|
int badline = rand() % NUMSIDREGS;
|
|
|
|
for (int c = 0; c < NUMSIDREGS; c++)
|
|
{
|
|
unsigned char o = sidorder[c];
|
|
|
|
// Extra delay for loading the waveform (and mt_chngate,x)
|
|
if ((o == 4) || (o == 11) || (o == 18))
|
|
{
|
|
int tdelta2 = SIDWAVEDELAY;
|
|
int result = sid->clock(tdelta2, ptr, samples);
|
|
total += result;
|
|
ptr += result;
|
|
samples -= result;
|
|
tdelta -= SIDWAVEDELAY;
|
|
}
|
|
|
|
// Possible random badline delay once per writing
|
|
if ((badline == c) && (residdelay))
|
|
{
|
|
int tdelta2 = residdelay;
|
|
int result = sid->clock(tdelta2, ptr, samples);
|
|
total += result;
|
|
ptr += result;
|
|
samples -= result;
|
|
tdelta -= residdelay;
|
|
}
|
|
|
|
sid->write(o, sidreg[o]);
|
|
|
|
int tdelta2 = SIDWRITEDELAY;
|
|
int result = sid->clock(tdelta2, ptr, samples);
|
|
total += result;
|
|
ptr += result;
|
|
samples -= result;
|
|
tdelta -= SIDWRITEDELAY;
|
|
}
|
|
int result = sid->clock(tdelta, ptr, samples);
|
|
total += result;
|
|
|
|
return total;
|
|
}
|
|
|
|
|
|
|
|
|
|
void SidInstrument::playNote( NotePlayHandle * _n,
|
|
SampleFrame* _working_buffer )
|
|
{
|
|
const int clockrate = C64_PAL_CYCLES_PER_SEC;
|
|
const int samplerate = Engine::audioEngine()->outputSampleRate();
|
|
|
|
if (!_n->m_pluginData)
|
|
{
|
|
auto sid = new reSID::SID();
|
|
sid->set_sampling_parameters(clockrate, reSID::SAMPLE_FAST, samplerate);
|
|
sid->set_chip_model(reSID::MOS8580);
|
|
sid->enable_filter( true );
|
|
sid->reset();
|
|
_n->m_pluginData = sid;
|
|
}
|
|
const fpp_t frames = _n->framesLeftForCurrentPeriod();
|
|
const f_cnt_t offset = _n->noteOffset();
|
|
|
|
auto sid = static_cast<reSID::SID*>(_n->m_pluginData);
|
|
int delta_t = clockrate * frames / samplerate + 4;
|
|
#ifndef _MSC_VER
|
|
short buf[frames];
|
|
#else
|
|
const auto buf = static_cast<short*>(_alloca(frames * sizeof(short)));
|
|
#endif
|
|
auto sidreg = std::array<unsigned char, NUMSIDREGS>{};
|
|
|
|
for (auto& reg : sidreg)
|
|
{
|
|
reg = 0x00;
|
|
}
|
|
|
|
if( (ChipModel)m_chipModel.value() == ChipModel::MOS6581 )
|
|
{
|
|
sid->set_chip_model(reSID::MOS6581);
|
|
}
|
|
else
|
|
{
|
|
sid->set_chip_model(reSID::MOS8580);
|
|
}
|
|
|
|
// voices
|
|
reSID::reg8 data8 = 0;
|
|
reSID::reg16 data16 = 0;
|
|
size_t base = 0;
|
|
float freq = 0.0;
|
|
float note = 0.0;
|
|
for (size_t i = 0; i < 3; ++i)
|
|
{
|
|
base = i*7;
|
|
// freq ( Fn = Fout / Fclk * 16777216 ) + coarse detuning
|
|
freq = _n->frequency();
|
|
note = 69.0 + 12.0 * std::log2(freq / 440.0);
|
|
note += m_voice[i]->m_coarseModel.value();
|
|
freq = 440.0 * std::exp2((note - 69.0) / 12.0);
|
|
data16 = int( freq / float(clockrate) * 16777216.0 );
|
|
|
|
sidreg[base+0] = data16&0x00FF;
|
|
sidreg[base+1] = (data16>>8)&0x00FF;
|
|
// pw
|
|
data16 = (int)m_voice[i]->m_pulseWidthModel.value();
|
|
|
|
sidreg[base+2] = data16&0x00FF;
|
|
sidreg[base+3] = (data16>>8)&0x000F;
|
|
// control: wave form, (test), ringmod, sync, gate
|
|
data8 = _n->isReleased()?0:1;
|
|
data8 += m_voice[i]->m_syncModel.value()?2:0;
|
|
data8 += m_voice[i]->m_ringModModel.value()?4:0;
|
|
data8 += m_voice[i]->m_testModel.value()?8:0;
|
|
switch( static_cast<VoiceObject::WaveForm>(m_voice[i]->m_waveFormModel.value()) )
|
|
{
|
|
default: break;
|
|
case VoiceObject::WaveForm::Noise: data8 += 128; break;
|
|
case VoiceObject::WaveForm::Square: data8 += 64; break;
|
|
case VoiceObject::WaveForm::Saw: data8 += 32; break;
|
|
case VoiceObject::WaveForm::Triangle: data8 += 16; break;
|
|
}
|
|
sidreg[base+4] = data8&0x00FF;
|
|
// ad
|
|
data16 = (int)m_voice[i]->m_attackModel.value();
|
|
|
|
data8 = (data16&0x0F)<<4;
|
|
data16 = (int)m_voice[i]->m_decayModel.value();
|
|
|
|
data8 += (data16&0x0F);
|
|
sidreg[base+5] = data8&0x00FF;
|
|
// sr
|
|
data16 = (int)m_voice[i]->m_sustainModel.value();
|
|
|
|
data8 = (data16&0x0F)<<4;
|
|
data16 = (int)m_voice[i]->m_releaseModel.value();
|
|
|
|
data8 += (data16&0x0F);
|
|
sidreg[base+6] = data8&0x00FF;
|
|
}
|
|
// filtered
|
|
|
|
// FC (FilterCutoff)
|
|
data16 = (int)m_filterFCModel.value();
|
|
sidreg[21] = data16&0x0007;
|
|
sidreg[22] = (data16>>3)&0x00FF;
|
|
|
|
// res, filt ex,3,2,1
|
|
data16 = (int)m_filterResonanceModel.value();
|
|
data8 = (data16&0x000F)<<4;
|
|
data8 += m_voice[2]->m_filteredModel.value()?4:0;
|
|
data8 += m_voice[1]->m_filteredModel.value()?2:0;
|
|
data8 += m_voice[0]->m_filteredModel.value()?1:0;
|
|
sidreg[23] = data8&0x00FF;
|
|
|
|
// mode vol
|
|
data16 = (int)m_volumeModel.value();
|
|
data8 = data16&0x000F;
|
|
data8 += m_voice3OffModel.value()?128:0;
|
|
|
|
switch( static_cast<FilterType>(m_filterModeModel.value()) )
|
|
{
|
|
default: break;
|
|
case FilterType::LowPass: data8 += 16; break;
|
|
case FilterType::BandPass: data8 += 32; break;
|
|
case FilterType::HighPass: data8 += 64; break;
|
|
}
|
|
|
|
sidreg[24] = data8&0x00FF;
|
|
|
|
const auto num = static_cast<f_cnt_t>(sid_fillbuffer(sidreg.data(), sid, delta_t, buf, frames));
|
|
if (num != frames) {
|
|
printf("!!!Not enough samples\n");
|
|
}
|
|
|
|
// loop backwards to avoid overwriting data in the short-to-float conversion
|
|
for (auto frame = std::size_t{0}; frame < frames; ++frame)
|
|
{
|
|
sample_t s = float(buf[frame])/32768.0;
|
|
for( ch_cnt_t ch = 0; ch < DEFAULT_CHANNELS; ++ch )
|
|
{
|
|
_working_buffer[frame+offset][ch] = s;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void SidInstrument::deleteNotePluginData( NotePlayHandle * _n )
|
|
{
|
|
delete static_cast<reSID::SID*>(_n->m_pluginData);
|
|
}
|
|
|
|
|
|
|
|
|
|
gui::PluginView* SidInstrument::instantiateView( QWidget * _parent )
|
|
{
|
|
return( new gui::SidInstrumentView( this, _parent ) );
|
|
}
|
|
|
|
|
|
|
|
namespace gui
|
|
{
|
|
|
|
|
|
class sidKnob : public Knob
|
|
{
|
|
public:
|
|
sidKnob( QWidget * _parent ) :
|
|
Knob( KnobType::Styled, _parent )
|
|
{
|
|
setFixedSize( 16, 16 );
|
|
setCenterPointX( 7.5 );
|
|
setCenterPointY( 7.5 );
|
|
setInnerRadius( 2 );
|
|
setOuterRadius( 8 );
|
|
setTotalAngle( 270.0 );
|
|
setLineWidth( 2 );
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
SidInstrumentView::SidInstrumentView( Instrument * _instrument,
|
|
QWidget * _parent ) :
|
|
InstrumentViewFixedSize( _instrument, _parent )
|
|
{
|
|
|
|
setAutoFillBackground( true );
|
|
QPalette pal;
|
|
pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "artwork" ) );
|
|
setPalette( pal );
|
|
|
|
m_volKnob = new sidKnob( this );
|
|
m_volKnob->setHintText( tr( "Volume:" ), "" );
|
|
m_volKnob->move( 7, 64 );
|
|
|
|
m_resKnob = new sidKnob( this );
|
|
m_resKnob->setHintText( tr( "Resonance:" ), "" );
|
|
m_resKnob->move( 7 + 28, 64 );
|
|
|
|
m_cutKnob = new sidKnob( this );
|
|
m_cutKnob->setHintText( tr( "Cutoff frequency:" ), " Hz" );
|
|
m_cutKnob->move( 7 + 2*28, 64 );
|
|
|
|
auto hp_btn = new PixmapButton(this, nullptr);
|
|
hp_btn->move( 140, 77 );
|
|
hp_btn->setActiveGraphic( PLUGIN_NAME::getIconPixmap( "hpred" ) );
|
|
hp_btn->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "hp" ) );
|
|
hp_btn->setToolTip(tr("High-pass filter "));
|
|
|
|
auto bp_btn = new PixmapButton(this, nullptr);
|
|
bp_btn->move( 164, 77 );
|
|
bp_btn->setActiveGraphic( PLUGIN_NAME::getIconPixmap( "bpred" ) );
|
|
bp_btn->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "bp" ) );
|
|
bp_btn->setToolTip(tr("Band-pass filter "));
|
|
|
|
auto lp_btn = new PixmapButton(this, nullptr);
|
|
lp_btn->move( 185, 77 );
|
|
lp_btn->setActiveGraphic( PLUGIN_NAME::getIconPixmap( "lpred" ) );
|
|
lp_btn->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "lp" ) );
|
|
lp_btn->setToolTip(tr("Low-pass filter "));
|
|
|
|
m_passBtnGrp = new automatableButtonGroup( this );
|
|
m_passBtnGrp->addButton( hp_btn );
|
|
m_passBtnGrp->addButton( bp_btn );
|
|
m_passBtnGrp->addButton( lp_btn );
|
|
|
|
m_offButton = new PixmapButton( this, nullptr );
|
|
m_offButton->setCheckable( true );
|
|
m_offButton->move( 207, 77 );
|
|
m_offButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( "3offred" ) );
|
|
m_offButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "3off" ) );
|
|
m_offButton->setToolTip(tr("Voice 3 off "));
|
|
|
|
auto mos6581_btn = new PixmapButton(this, nullptr);
|
|
mos6581_btn->move( 170, 59 );
|
|
mos6581_btn->setActiveGraphic( PLUGIN_NAME::getIconPixmap( "6581red" ) );
|
|
mos6581_btn->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "6581" ) );
|
|
mos6581_btn->setToolTip(tr("MOS6581 SID "));
|
|
|
|
auto mos8580_btn = new PixmapButton(this, nullptr);
|
|
mos8580_btn->move( 207, 59 );
|
|
mos8580_btn->setActiveGraphic( PLUGIN_NAME::getIconPixmap( "8580red" ) );
|
|
mos8580_btn->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "8580" ) );
|
|
mos8580_btn->setToolTip(tr("MOS8580 SID "));
|
|
|
|
m_sidTypeBtnGrp = new automatableButtonGroup( this );
|
|
m_sidTypeBtnGrp->addButton( mos6581_btn );
|
|
m_sidTypeBtnGrp->addButton( mos8580_btn );
|
|
|
|
for( int i = 0; i < 3; i++ )
|
|
{
|
|
Knob *ak = new sidKnob( this );
|
|
ak->setHintText( tr("Attack:"), "" );
|
|
ak->move( 7, 114 + i*50 );
|
|
|
|
Knob *dk = new sidKnob( this );
|
|
dk->setHintText( tr("Decay:") , "" );
|
|
dk->move( 7 + 28, 114 + i*50 );
|
|
|
|
Knob *sk = new sidKnob( this );
|
|
sk->setHintText( tr("Sustain:"), "" );
|
|
sk->move( 7 + 2*28, 114 + i*50 );
|
|
|
|
Knob *rk = new sidKnob( this );
|
|
rk->setHintText( tr("Release:"), "" );
|
|
rk->move( 7 + 3*28, 114 + i*50 );
|
|
|
|
Knob *pwk = new sidKnob( this );
|
|
pwk->setHintText( tr("Pulse Width:"), "" );
|
|
pwk->move( 7 + 4*28, 114 + i*50 );
|
|
|
|
Knob *crsk = new sidKnob( this );
|
|
crsk->setHintText( tr("Coarse:"), " semitones" );
|
|
crsk->move( 147, 114 + i*50 );
|
|
|
|
auto pulse_btn = new PixmapButton(this, nullptr);
|
|
pulse_btn->move( 187, 101 + i*50 );
|
|
pulse_btn->setActiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "pulsered" ) );
|
|
pulse_btn->setInactiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "pulse" ) );
|
|
pulse_btn->setToolTip(tr("Pulse wave"));
|
|
|
|
auto triangle_btn = new PixmapButton(this, nullptr);
|
|
triangle_btn->move( 168, 101 + i*50 );
|
|
triangle_btn->setActiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "trianglered" ) );
|
|
triangle_btn->setInactiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "triangle" ) );
|
|
triangle_btn->setToolTip(tr("Triangle wave"));
|
|
|
|
auto saw_btn = new PixmapButton(this, nullptr);
|
|
saw_btn->move( 207, 101 + i*50 );
|
|
saw_btn->setActiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "sawred" ) );
|
|
saw_btn->setInactiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "saw" ) );
|
|
saw_btn->setToolTip(tr("Saw wave"));
|
|
|
|
auto noise_btn = new PixmapButton(this, nullptr);
|
|
noise_btn->move( 226, 101 + i*50 );
|
|
noise_btn->setActiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "noisered" ) );
|
|
noise_btn->setInactiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "noise" ) );
|
|
noise_btn->setToolTip(tr("Noise"));
|
|
|
|
auto wfbg = new automatableButtonGroup(this);
|
|
|
|
wfbg->addButton( pulse_btn );
|
|
wfbg->addButton( triangle_btn );
|
|
wfbg->addButton( saw_btn );
|
|
wfbg->addButton( noise_btn );
|
|
|
|
auto sync_btn = new PixmapButton(this, nullptr);
|
|
sync_btn->setCheckable( true );
|
|
sync_btn->move( 207, 134 + i*50 );
|
|
sync_btn->setActiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "syncred" ) );
|
|
sync_btn->setInactiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "sync" ) );
|
|
sync_btn->setToolTip(tr("Sync"));
|
|
|
|
auto ringMod_btn = new PixmapButton(this, nullptr);
|
|
ringMod_btn->setCheckable( true );
|
|
ringMod_btn->move( 170, 116 + i*50 );
|
|
ringMod_btn->setActiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "ringred" ) );
|
|
ringMod_btn->setInactiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "ring" ) );
|
|
ringMod_btn->setToolTip(tr("Ring modulation"));
|
|
|
|
auto filter_btn = new PixmapButton(this, nullptr);
|
|
filter_btn->setCheckable( true );
|
|
filter_btn->move( 207, 116 + i*50 );
|
|
filter_btn->setActiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "filterred" ) );
|
|
filter_btn->setInactiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "filter" ) );
|
|
filter_btn->setToolTip(tr("Filtered"));
|
|
|
|
auto test_btn = new PixmapButton(this, nullptr);
|
|
test_btn->setCheckable( true );
|
|
test_btn->move( 170, 134 + i*50 );
|
|
test_btn->setActiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "testred" ) );
|
|
test_btn->setInactiveGraphic(
|
|
PLUGIN_NAME::getIconPixmap( "test" ) );
|
|
test_btn->setToolTip(tr("Test"));
|
|
|
|
m_voiceKnobs[i] = voiceKnobs( ak, dk, sk, rk, pwk, crsk, wfbg,
|
|
sync_btn, ringMod_btn, filter_btn, test_btn );
|
|
}
|
|
}
|
|
|
|
|
|
void SidInstrumentView::updateKnobHint()
|
|
{
|
|
auto k = castModel<SidInstrument>();
|
|
|
|
for( int i = 0; i < 3; ++i )
|
|
{
|
|
m_voiceKnobs[i].m_attKnob->setHintText( tr( "Attack:" ) + " ", " (" +
|
|
QString::fromLatin1( attackTime[(int)k->m_voice[i]->
|
|
m_attackModel.value()] ) + ")" );
|
|
m_voiceKnobs[i].m_attKnob->setToolTip(
|
|
attackTime[(int)k->m_voice[i]->m_attackModel.value()] );
|
|
|
|
m_voiceKnobs[i].m_decKnob->setHintText( tr( "Decay:" ) + " ", " (" +
|
|
QString::fromLatin1( decRelTime[(int)k->m_voice[i]->
|
|
m_decayModel.value()] ) + ")" );
|
|
m_voiceKnobs[i].m_decKnob->setToolTip(
|
|
decRelTime[(int)k->m_voice[i]->m_decayModel.value()] );
|
|
|
|
m_voiceKnobs[i].m_relKnob->setHintText( tr( "Release:" ) + " ", " (" +
|
|
QString::fromLatin1( decRelTime[(int)k->m_voice[i]->
|
|
m_releaseModel.value()] ) + ")" );
|
|
m_voiceKnobs[i].m_relKnob->setToolTip(
|
|
decRelTime[(int)k->m_voice[i]->m_releaseModel.value()]);
|
|
|
|
m_voiceKnobs[i].m_pwKnob->setHintText( tr( "Pulse width:" )+ " ", " (" +
|
|
QString::number( (double)k->m_voice[i]->
|
|
m_pulseWidthModel.value() / 40.95 ) + "%)" );
|
|
m_voiceKnobs[i].m_pwKnob->setToolTip(
|
|
QString::number( (double)k->m_voice[i]->
|
|
m_pulseWidthModel.value() / 40.95 ) + "%" );
|
|
}
|
|
m_cutKnob->setHintText( tr( "Cutoff frequency:" ) + " ", " (" +
|
|
QString::number ( (int) ( 9970.0 / 2047.0 *
|
|
(double)k->m_filterFCModel.value() + 30.0 ) ) + " Hz)" );
|
|
m_cutKnob->setToolTip(QString::number((int) (9970.0 / 2047.0 *
|
|
(double)k->m_filterFCModel.value() + 30.0 ) ) + " Hz" );
|
|
}
|
|
|
|
|
|
|
|
|
|
void SidInstrumentView::updateKnobToolTip()
|
|
{
|
|
auto k = castModel<SidInstrument>();
|
|
for( int i = 0; i < 3; ++i )
|
|
{
|
|
m_voiceKnobs[i].m_sustKnob->setToolTip(
|
|
QString::number( (int)k->m_voice[i]->m_sustainModel.value() ) );
|
|
m_voiceKnobs[i].m_crsKnob->setToolTip(
|
|
QString::number( (int)k->m_voice[i]->m_coarseModel.value() ) +
|
|
" semitones" );
|
|
}
|
|
m_volKnob->setToolTip(
|
|
QString::number( (int)k->m_volumeModel.value() ) );
|
|
m_resKnob->setToolTip(
|
|
QString::number( (int)k->m_filterResonanceModel.value() ) );
|
|
}
|
|
|
|
|
|
|
|
|
|
void SidInstrumentView::modelChanged()
|
|
{
|
|
auto k = castModel<SidInstrument>();
|
|
|
|
m_volKnob->setModel( &k->m_volumeModel );
|
|
m_resKnob->setModel( &k->m_filterResonanceModel );
|
|
m_cutKnob->setModel( &k->m_filterFCModel );
|
|
m_passBtnGrp->setModel( &k->m_filterModeModel );
|
|
m_offButton->setModel( &k->m_voice3OffModel );
|
|
m_sidTypeBtnGrp->setModel( &k->m_chipModel );
|
|
|
|
for( int i = 0; i < 3; ++i )
|
|
{
|
|
m_voiceKnobs[i].m_attKnob->setModel(
|
|
&k->m_voice[i]->m_attackModel );
|
|
m_voiceKnobs[i].m_decKnob->setModel(
|
|
&k->m_voice[i]->m_decayModel );
|
|
m_voiceKnobs[i].m_sustKnob->setModel(
|
|
&k->m_voice[i]->m_sustainModel );
|
|
m_voiceKnobs[i].m_relKnob->setModel(
|
|
&k->m_voice[i]->m_releaseModel );
|
|
m_voiceKnobs[i].m_pwKnob->setModel(
|
|
&k->m_voice[i]->m_pulseWidthModel );
|
|
m_voiceKnobs[i].m_crsKnob->setModel(
|
|
&k->m_voice[i]->m_coarseModel );
|
|
m_voiceKnobs[i].m_waveFormBtnGrp->setModel(
|
|
&k->m_voice[i]->m_waveFormModel );
|
|
m_voiceKnobs[i].m_syncButton->setModel(
|
|
&k->m_voice[i]->m_syncModel );
|
|
m_voiceKnobs[i].m_ringModButton->setModel(
|
|
&k->m_voice[i]->m_ringModModel );
|
|
m_voiceKnobs[i].m_filterButton->setModel(
|
|
&k->m_voice[i]->m_filteredModel );
|
|
m_voiceKnobs[i].m_testButton->setModel(
|
|
&k->m_voice[i]->m_testModel );
|
|
}
|
|
|
|
for (const auto& voice : k->m_voice)
|
|
{
|
|
connect(&voice->m_attackModel, SIGNAL(dataChanged()), this, SLOT(updateKnobHint()));
|
|
connect(&voice->m_decayModel, SIGNAL(dataChanged()), this, SLOT(updateKnobHint()));
|
|
connect(&voice->m_releaseModel, SIGNAL(dataChanged()), this, SLOT(updateKnobHint()));
|
|
connect(&voice->m_pulseWidthModel, SIGNAL(dataChanged()), this, SLOT(updateKnobHint()));
|
|
connect(&voice->m_sustainModel, SIGNAL(dataChanged()), this, SLOT(updateKnobToolTip()));
|
|
connect(&voice->m_coarseModel, SIGNAL(dataChanged()), this, SLOT(updateKnobToolTip()));
|
|
}
|
|
|
|
connect( &k->m_volumeModel, SIGNAL( dataChanged() ),
|
|
this, SLOT( updateKnobToolTip() ) );
|
|
connect( &k->m_filterResonanceModel, SIGNAL( dataChanged() ),
|
|
this, SLOT( updateKnobToolTip() ) );
|
|
connect( &k->m_filterFCModel, SIGNAL( dataChanged() ),
|
|
this, SLOT( updateKnobHint() ) );
|
|
|
|
updateKnobHint();
|
|
updateKnobToolTip();
|
|
}
|
|
|
|
|
|
} // namespace gui
|
|
|
|
|
|
extern "C"
|
|
{
|
|
|
|
// necessary for getting instance out of shared lib
|
|
PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *m, void * )
|
|
{
|
|
return( new SidInstrument( static_cast<InstrumentTrack *>( m ) ) );
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} // namespace lmms
|