diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 52a97aaa9..d5c973e87 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -696,6 +696,16 @@ MonstroView knob { qproperty-lineWidth: 2.5; } +NesInstrumentView knob { + color: #e7231b; + qproperty-outerColor: #fff; + qproperty-outerRadius: 11.0; + qproperty-innerRadius: 8.0; + qproperty-centerPointX: 14.5; + qproperty-centerPointY: 14.5; + qproperty-lineWidth: 2; +} + /* palette information */ LmmsPalette { diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f61906e67..d275b4f65 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -13,6 +13,7 @@ ADD_SUBDIRECTORY(lb302) #ADD_SUBDIRECTORY(lb303) ADD_SUBDIRECTORY(midi_import) ADD_SUBDIRECTORY(monstro) +ADD_SUBDIRECTORY(nes) IF(NOT LMMS_BUILD_APPLE) ADD_SUBDIRECTORY(opl2) ENDIF() diff --git a/plugins/nes/CMakeLists.txt b/plugins/nes/CMakeLists.txt new file mode 100644 index 000000000..b864f6125 --- /dev/null +++ b/plugins/nes/CMakeLists.txt @@ -0,0 +1,3 @@ +INCLUDE(BuildPlugin) + +BUILD_PLUGIN(nes Nes.cpp Nes.h MOCFILES Nes.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") diff --git a/plugins/nes/Nes.cpp b/plugins/nes/Nes.cpp new file mode 100644 index 000000000..a4de6d99d --- /dev/null +++ b/plugins/nes/Nes.cpp @@ -0,0 +1,821 @@ +/* Nes.cpp - A NES instrument plugin for LMMS + * + * Copyright (c) 2014 Vesa Kivimäki + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net + * + * 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 + +#include "Nes.h" +#include "engine.h" +#include "InstrumentTrack.h" +#include "templates.h" +#include "tooltip.h" +#include "song.h" +#include "lmms_math.h" +#include "interpolation.h" +#include "Oscillator.h" + +#include "embed.cpp" + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT nes_plugin_descriptor = +{ + STRINGIFY( PLUGIN_NAME ), + "Nescaline", + QT_TRANSLATE_NOOP( "pluginBrowser", + "A NES-like synthesizer" ), + "Vesa Kivimäki ", + 0x0100, + Plugin::Instrument, + new PluginPixmapLoader( "logo" ), + NULL, + NULL +} ; + +} + + +NesObject::NesObject( NesInstrument * nes, const sample_rate_t samplerate, NotePlayHandle * nph, fpp_t frames ) : + m_parent( nes ), + m_samplerate( samplerate ), + m_nph( nph ), + m_fpp( frames ) +{ + m_LFSR = LFSR_INIT; + + m_ch1Counter = 0; + m_ch2Counter = 0; + m_ch3Counter = 0; + m_ch4Counter = 0; + + m_ch1EnvCounter = 0; + m_ch2EnvCounter = 0; + m_ch4EnvCounter = 0; + + m_ch1EnvValue = 15; + m_ch2EnvValue = 15; + m_ch4EnvValue = 15; + + m_ch1SweepCounter = 0; + m_ch2SweepCounter = 0; + + m_12Last = 0.0f; + m_34Last = 0.0f; + + m_itm = 0.0f; + m_otm = 0.0f; + + m_lastNoteFreq = 0.0f; + + m_maxWlen = wavelength( MIN_FREQ ); + + m_nsf = NES_SIMPLE_FILTER * ( m_samplerate / 44100.0 ); +} + + +NesObject::~NesObject() +{ +} + + +void NesObject::renderOutput( sampleFrame * buf, fpp_t frames ) +{ + // check if frequency has changed, if so, update wavelengths of ch1-3 + if( m_nph->frequency() != m_lastNoteFreq ) + { + m_wlen1 = wavelength( m_nph->frequency() * m_parent->m_freq1 ); + m_wlen2 = wavelength( m_nph->frequency() * m_parent->m_freq2 ); + m_wlen3 = wavelength( m_nph->frequency() * m_parent->m_freq3 ); + } + // noise channel can use either note freq or preset freqs + if( m_parent->m_ch4NoiseFreqMode.value() ) + { + m_wlen4 = wavelength( m_nph->frequency() ); + } + else + { + m_wlen4 = wavelength( NOISE_FREQS[ 15 - static_cast( m_parent->m_ch4NoiseFreq.value() ) ] ); + } + + m_lastNoteFreq = m_nph->frequency(); + + //////////////////////////////// + // // + // variables for processing // + // // + //////////////////////////////// + + bool ch1Enabled = m_parent->m_ch1Enabled.value(); + bool ch2Enabled = m_parent->m_ch2Enabled.value(); + bool ch3Enabled = m_parent->m_ch3Enabled.value(); + bool ch4Enabled = m_parent->m_ch4Enabled.value(); + + float ch1DutyCycle = DUTY_CYCLE[ m_parent->m_ch1DutyCycle.value() ]; + int ch1EnvLen = wavelength( floorf( 240.0 / ( m_parent->m_ch1EnvLen.value() + 1 ) ) ); + bool ch1EnvLoop = m_parent->m_ch1EnvLooped.value(); + + float ch2DutyCycle = DUTY_CYCLE[ m_parent->m_ch2DutyCycle.value() ]; + int ch2EnvLen = wavelength( floorf( 240.0 / ( m_parent->m_ch2EnvLen.value() + 1 ) ) ); + bool ch2EnvLoop = m_parent->m_ch2EnvLooped.value(); + + int ch4EnvLen = wavelength( floorf( 240.0 / ( m_parent->m_ch4EnvLen.value() + 1 ) ) ); + bool ch4EnvLoop = m_parent->m_ch4EnvLooped.value(); + + int ch1; + int ch2; + int ch3; + int ch4; + + int ch1SweepRate = wavelength( floorf( 120.0 / ( m_parent->m_ch1SweepRate.value() + 1 ) ) ); + int ch2SweepRate = wavelength( floorf( 120.0 / ( m_parent->m_ch2SweepRate.value() + 1 ) ) ); + + int ch1Sweep = static_cast( m_parent->m_ch1SweepAmt.value() * -1.0 ); + int ch2Sweep = static_cast( m_parent->m_ch2SweepAmt.value() * -1.0 ); + + // the amounts are inverted so we correct them here + if( ch1Sweep > 0 ) + { + ch1Sweep = 8 - ch1Sweep; + } + if( ch1Sweep < 0 ) + { + ch1Sweep = -8 - ch1Sweep; + } + + if( ch2Sweep > 0 ) + { + ch2Sweep = 8 - ch2Sweep; + } + if( ch2Sweep < 0 ) + { + ch2Sweep = -8 - ch2Sweep; + } + + + // start framebuffer loop + + for( f_cnt_t f = 0; f < frames; f++ ) + { + //////////////////////////////// + // // + // channel 1 // + // // + //////////////////////////////// + + // render pulse wave + if( m_wlen1 <= m_maxWlen && m_wlen1 >= MIN_WLEN && ch1Enabled ) + { + ch1 = m_ch1Counter > m_wlen1 * ch1DutyCycle + ? 0 + : m_parent->m_ch1EnvEnabled.value() + ? static_cast( ( m_parent->m_ch1Volume.value() * m_ch1EnvValue ) / 15.0 ) + : static_cast( m_parent->m_ch1Volume.value() ); + } + else ch1 = 0; + + // update sweep + m_ch1SweepCounter++; + if( m_ch1SweepCounter >= ch1SweepRate ) + { + m_ch1SweepCounter = 0; + if( m_parent->m_ch1SweepEnabled.value() && m_wlen1 <= m_maxWlen && m_wlen1 >= MIN_WLEN ) + { + // check if the sweep goes up or down + if( ch1Sweep > 0 ) + { + m_wlen1 += m_wlen1 >> qAbs( ch1Sweep ); + } + if( ch1Sweep < 0 ) + { + m_wlen1 -= m_wlen1 >> qAbs( ch1Sweep ); + m_wlen1--; // additional minus 1 for ch1 only + } + } + } + + // update framecounters + m_ch1Counter++; + m_ch1Counter = m_ch1Counter % m_wlen1; + + m_ch1EnvCounter++; + if( m_ch1EnvCounter >= ch1EnvLen ) + { + m_ch1EnvCounter = 0; + m_ch1EnvValue--; + if( m_ch1EnvValue < 0 ) + { + m_ch1EnvValue = ch1EnvLoop ? 15 : 0; + } + } + + + //////////////////////////////// + // // + // channel 2 // + // // + //////////////////////////////// + + // render pulse wave + if( m_wlen2 <= m_maxWlen && m_wlen2 >= MIN_WLEN && ch2Enabled ) + { + ch2 = m_ch2Counter > m_wlen2 * ch2DutyCycle + ? 0 + : m_parent->m_ch2EnvEnabled.value() + ? static_cast( ( m_parent->m_ch2Volume.value() * m_ch2EnvValue ) / 15.0 ) + : static_cast( m_parent->m_ch2Volume.value() ); + } + else ch2 = 0; + + // update sweep + m_ch2SweepCounter++; + if( m_ch2SweepCounter >= ch2SweepRate ) + { + m_ch2SweepCounter = 0; + if( m_parent->m_ch2SweepEnabled.value() && m_wlen2 <= m_maxWlen && m_wlen2 >= MIN_WLEN ) + { + // check if the sweep goes up or down + if( ch2Sweep > 0 ) + { + m_wlen2 += m_wlen2 >> qAbs( ch2Sweep ); + } + if( ch2Sweep < 0 ) + { + m_wlen2 -= m_wlen2 >> qAbs( ch2Sweep ); + } + } + } + + // update framecounters + m_ch2Counter++; + m_ch2Counter = m_ch2Counter % m_wlen2; + + m_ch2EnvCounter++; + if( m_ch2EnvCounter >= ch2EnvLen ) + { + m_ch2EnvCounter = 0; + m_ch2EnvValue--; + if( m_ch2EnvValue < 0 ) + { + m_ch2EnvValue = ch2EnvLoop + ? 15 + : 0; + } + } + + + //////////////////////////////// + // // + // channel 3 // + // // + //////////////////////////////// + + // make sure we don't overflow + m_ch3Counter %= m_wlen3; + + // render triangle wave + if( m_wlen3 <= m_maxWlen && ch3Enabled ) + { + ch3 = TRIANGLE_WAVETABLE[ ( m_ch3Counter * 32 ) / m_wlen3 ]; + ch3 = ( ch3 * static_cast( m_parent->m_ch3Volume.value() ) ) / 15; + } + else ch3 = 0; + + m_ch3Counter++; + + + //////////////////////////////// + // // + // channel 4 // + // // + //////////////////////////////// + + // render pseudo noise + if( ch4Enabled ) + { + ch4 = LFSR() + ? ( m_parent->m_ch4EnvEnabled.value() + ? ( static_cast( m_parent->m_ch4Volume.value() ) * m_ch4EnvValue ) / 15 + : static_cast( m_parent->m_ch4Volume.value() ) ) + : 0; + } + else ch4 = 0; + + // update framecounters + m_ch4Counter++; + if( m_ch4Counter >= m_wlen4 ) + { + m_ch4Counter = 0; + updateLFSR( m_parent->m_ch4NoiseMode.value() ); + } + m_ch4EnvCounter++; + if( m_ch4EnvCounter >= ch4EnvLen ) + { + m_ch4EnvCounter = 0; + m_ch4EnvValue--; + if( m_ch4EnvValue < 0 ) + { + m_ch4EnvValue = ch4EnvLoop + ? 15 + : 0; + } + } + + + //////////////////////////////// + // // + // final stage - mixing // + // // + //////////////////////////////// + + float ch12 = static_cast( ch1 + ch2 ); + // add dithering noise + ch12 *= 1.0 + ( Oscillator::noiseSample( 0.0f ) * DITHER_AMP ); + ch12 = ch12 / 15.0f - 1.0f; + + ch12 = signedPow( ch12, NES_DIST ); + + // simple first order iir filter, to simulate the frequency response falloff in nes analog audio output + ch12 = linearInterpolate( ch12, m_12Last, m_nsf ); + m_12Last = ch12; + + + ch12 *= NES_MIXING_12; + + + float ch34 = static_cast( ch3 + ch4 ); + // add dithering noise + ch34 *= 1.0 + ( Oscillator::noiseSample( 0.0f ) * DITHER_AMP ); + ch34 = ch34 / 15.0f - 1.0f; + + ch34 = signedPow( ch34, NES_DIST ); + + // simple first order iir filter, to simulate the frequency response falloff in nes analog audio output + ch34 = linearInterpolate( ch34, m_34Last, m_nsf ); + m_34Last = ch34; + + + ch34 *= NES_MIXING_34; + + float mixdown = ( ch12 + ch34 ) * NES_MIXING_ALL * m_parent->m_masterVol.value(); + + // dc offset removal + m_otm = 0.999f * m_otm + mixdown - m_itm; + m_itm = mixdown; + buf[f][0] = m_otm; + buf[f][1] = m_otm; + + } // end framebuffer loop + +} + + +NesInstrument::NesInstrument( InstrumentTrack * instrumentTrack ) : + Instrument( instrumentTrack, &nes_plugin_descriptor ), + m_ch1Enabled( true, this ), + m_ch1Crs( 0.f, -24.f, 24.f, 1.f, this, tr( "Channel 1 Coarse detune" ) ), + m_ch1Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 1 Volume" ) ), + + m_ch1EnvEnabled( false, this ), + m_ch1EnvLooped( false, this ), + m_ch1EnvLen( 0.f, 0.f, 15.f, 1.f, this, tr( "Channel 1 Envelope length" ) ), + + m_ch1DutyCycle( 0, 0, 3, this, tr( "Channel 1 Duty cycle" ) ), + + m_ch1SweepEnabled( false, this ), + m_ch1SweepAmt( 0.f, -7.f, 7.f, 1.f, this, tr( "Channel 1 Sweep amount" ) ), + m_ch1SweepRate( 0.f, 0.f, 7.f, 1.f, this, tr( "Channel 1 Sweep rate" ) ), + + m_ch2Enabled( true, this ), + m_ch2Crs( 0.f, -24.f, 24.f, 1.f, this, tr( "Channel 2 Coarse detune" ) ), + m_ch2Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 2 Volume" ) ), + + m_ch2EnvEnabled( false, this ), + m_ch2EnvLooped( false, this ), + m_ch2EnvLen( 0.f, 0.f, 15.f, 1.f, this, tr( "Channel 2 Envelope length" ) ), + + m_ch2DutyCycle( 2, 0, 3, this, tr( "Channel 2 Duty cycle" ) ), + + m_ch2SweepEnabled( false, this ), + m_ch2SweepAmt( 0.f, -7.f, 7.f, 1.f, this, tr( "Channel 2 Sweep amount" ) ), + m_ch2SweepRate( 0.f, 0.f, 7.f, 1.f, this, tr( "Channel 2 Sweep rate" ) ), + + //channel 3 + m_ch3Enabled( true, this ), + m_ch3Crs( 0.f, -24.f, 24.f, 1.f, this, tr( "Channel 3 Coarse detune" ) ), + m_ch3Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 3 Volume" ) ), + + //channel 4 + m_ch4Enabled( true, this ), + m_ch4Volume( 15.f, 0.f, 15.f, 1.f, this, tr( "Channel 4 Volume" ) ), + + m_ch4EnvEnabled( false, this ), + m_ch4EnvLooped( false, this ), + m_ch4EnvLen( 0.f, 0.f, 15.f, 1.f, this, tr( "Channel 4 Envelope length" ) ), + + m_ch4NoiseMode( false, this ), + m_ch4NoiseFreqMode( false, this ), + m_ch4NoiseFreq( 0.f, 0.f, 15.f, 1.f, this, tr( "Channel 4 Noise frequency" ) ), + + //master + m_masterVol( 1.0f, 0.0f, 2.0f, 0.01f, this, tr( "Master volume" ) ), + m_vibrato( 0.0f, 0.0f, 15.0f, 1.0f, this, tr( "Vibrato (unimplemented)" ) ) +{ + connect( &m_ch1Crs, SIGNAL( dataChanged() ), this, SLOT( updateFreq1() ) ); + connect( &m_ch2Crs, SIGNAL( dataChanged() ), this, SLOT( updateFreq2() ) ); + connect( &m_ch3Crs, SIGNAL( dataChanged() ), this, SLOT( updateFreq3() ) ); + + updateFreq1(); + updateFreq2(); + updateFreq3(); +} + + + +NesInstrument::~NesInstrument() +{ +} + + +void NesInstrument::playNote( NotePlayHandle * n, sampleFrame * workingBuffer ) +{ + if ( n->totalFramesPlayed() == 0 || n->m_pluginData == NULL ) + { + NesObject * nes = new NesObject( this, engine::mixer()->processingSampleRate(), n, engine::mixer()->framesPerPeriod() ); + n->m_pluginData = nes; + } + + const fpp_t frames = n->framesLeftForCurrentPeriod(); + + NesObject * nes = static_cast( n->m_pluginData ); + + nes->renderOutput( workingBuffer, frames ); + + applyRelease( workingBuffer, n ); + + instrumentTrack()->processAudioBuffer( workingBuffer, frames, n ); +} + + +void NesInstrument::deleteNotePluginData( NotePlayHandle * n ) +{ + delete static_cast( n->m_pluginData ); +} + + +void NesInstrument::saveSettings( QDomDocument & doc, QDomElement & element ) +{ + m_ch1Enabled.saveSettings( doc, element, "on1" ); + m_ch1Crs.saveSettings( doc, element, "crs1" ); + m_ch1Volume.saveSettings( doc, element, "vol1" ); + + m_ch1EnvEnabled.saveSettings( doc, element, "envon1" ); + m_ch1EnvLooped.saveSettings( doc, element, "envloop1" ); + m_ch1EnvLen.saveSettings( doc, element, "envlen1" ); + + m_ch1DutyCycle.saveSettings( doc, element, "dc1" ); + + m_ch1SweepEnabled.saveSettings( doc, element, "sweep1" ); + m_ch1SweepAmt.saveSettings( doc, element, "swamt1" ); + m_ch1SweepRate.saveSettings( doc, element, "swrate1" ); + + // channel 2 + m_ch2Enabled.saveSettings( doc, element, "on2" ); + m_ch2Crs.saveSettings( doc, element, "crs2" ); + m_ch2Volume.saveSettings( doc, element, "vol2" ); + + m_ch2EnvEnabled.saveSettings( doc, element, "envon2" ); + m_ch2EnvLooped.saveSettings( doc, element, "envloop2" ); + m_ch2EnvLen.saveSettings( doc, element, "envlen2" ); + + m_ch2DutyCycle.saveSettings( doc, element, "dc2" ); + + m_ch2SweepEnabled.saveSettings( doc, element, "sweep2" ); + m_ch2SweepAmt.saveSettings( doc, element, "swamt2" ); + m_ch2SweepRate.saveSettings( doc, element, "swrate2" ); + + //channel 3 + m_ch3Enabled.saveSettings( doc, element, "on3" ); + m_ch3Crs.saveSettings( doc, element, "crs3" ); + m_ch3Volume.saveSettings( doc, element, "vol3" ); + + //channel 4 + m_ch4Enabled.saveSettings( doc, element, "on4" ); + m_ch4Volume.saveSettings( doc, element, "vol4" ); + + m_ch4EnvEnabled.saveSettings( doc, element, "envon4" ); + m_ch4EnvLooped.saveSettings( doc, element, "envloop4" ); + m_ch4EnvLen.saveSettings( doc, element, "envlen4" ); + + m_ch4NoiseMode.saveSettings( doc, element, "nmode4" ); + m_ch4NoiseFreqMode.saveSettings( doc, element, "nfrqmode4" ); + m_ch4NoiseFreq.saveSettings( doc, element, "nfreq4" ); + + //master + m_masterVol.saveSettings( doc, element, "vol" ); + m_vibrato.saveSettings( doc, element, "vibr" ); +} + + +void NesInstrument::loadSettings( const QDomElement & element ) +{ + m_ch1Enabled.loadSettings( element, "on1" ); + m_ch1Crs.loadSettings( element, "crs1" ); + m_ch1Volume.loadSettings( element, "vol1" ); + + m_ch1EnvEnabled.loadSettings( element, "envon1" ); + m_ch1EnvLooped.loadSettings( element, "envloop1" ); + m_ch1EnvLen.loadSettings( element, "envlen1" ); + + m_ch1DutyCycle.loadSettings( element, "dc1" ); + + m_ch1SweepEnabled.loadSettings( element, "sweep1" ); + m_ch1SweepAmt.loadSettings( element, "swamt1" ); + m_ch1SweepRate.loadSettings( element, "swrate1" ); + + // channel 2 + m_ch2Enabled.loadSettings( element, "on2" ); + m_ch2Crs.loadSettings( element, "crs2" ); + m_ch2Volume.loadSettings( element, "vol2" ); + + m_ch2EnvEnabled.loadSettings( element, "envon2" ); + m_ch2EnvLooped.loadSettings( element, "envloop2" ); + m_ch2EnvLen.loadSettings( element, "envlen2" ); + + m_ch2DutyCycle.loadSettings( element, "dc2" ); + + m_ch2SweepEnabled.loadSettings( element, "sweep2" ); + m_ch2SweepAmt.loadSettings( element, "swamt2" ); + m_ch2SweepRate.loadSettings( element, "swrate2" ); + + //channel 3 + m_ch3Enabled.loadSettings( element, "on3" ); + m_ch3Crs.loadSettings( element, "crs3" ); + m_ch3Volume.loadSettings( element, "vol3" ); + + //channel 4 + m_ch4Enabled.loadSettings( element, "on4" ); + m_ch4Volume.loadSettings( element, "vol4" ); + + m_ch4EnvEnabled.loadSettings( element, "envon4" ); + m_ch4EnvLooped.loadSettings( element, "envloop4" ); + m_ch4EnvLen.loadSettings( element, "envlen4" ); + + m_ch4NoiseMode.loadSettings( element, "nmode4" ); + m_ch4NoiseFreqMode.loadSettings( element, "nfrqmode4" ); + m_ch4NoiseFreq.loadSettings( element, "nfreq4" ); + + //master + m_masterVol.loadSettings( element, "vol" ); + m_vibrato.loadSettings( element, "vibr" ); +} + + +QString NesInstrument::nodeName() const +{ + return( nes_plugin_descriptor.name ); +} + + +PluginView * NesInstrument::instantiateView( QWidget * parent ) +{ + return( new NesInstrumentView( this, parent ) ); +} + + + +void NesInstrument::updateFreq1() +{ + m_freq1 = powf( 2, m_ch1Crs.value() / 12.0f ); +} + + +void NesInstrument::updateFreq2() +{ + m_freq2 = powf( 2, m_ch2Crs.value() / 12.0f ); +} + + +void NesInstrument::updateFreq3() +{ + m_freq3 = powf( 2, m_ch3Crs.value() / 12.0f ); +} + + + + +QPixmap * NesInstrumentView::s_artwork = NULL; + + +NesInstrumentView::NesInstrumentView( Instrument * instrument, QWidget * parent ) : + InstrumentView( instrument, parent ) +{ + setAutoFillBackground( true ); + QPalette pal; + + if( s_artwork == NULL ) + { + s_artwork = new QPixmap( PLUGIN_NAME::getIconPixmap( "artwork" ) ); + } + + pal.setBrush( backgroundRole(), *s_artwork ); + setPalette( pal ); + + const int KNOB_Y1 = 24; + const int KNOB_Y2 = 81; + const int KNOB_Y3 = 138; + const int KNOB_Y4 = 195; + + const int KNOB_X1 = 12; + const int KNOB_X2 = 46; + const int KNOB_X3 = 84; + const int KNOB_X4 = 117; + const int KNOB_X5 = 151; + const int KNOB_X6 = 172; + const int KNOB_X7 = 206; + + // channel 1 + + makeknob( m_ch1VolumeKnob, KNOB_X1, KNOB_Y1, "Volume", "", "" ) + makeknob( m_ch1CrsKnob, KNOB_X2, KNOB_Y1, "Coarse detune", "", "" ) + makeknob( m_ch1EnvLenKnob, KNOB_X3, KNOB_Y1, "Envelope length", "", "" ) + + makenesled( m_ch1EnabledBtn, KNOB_X1, KNOB_Y1 - 12, "Enable channel 1" ) + makenesled( m_ch1EnvEnabledBtn, KNOB_X3, KNOB_Y1 - 12, "Enable envelope 1" ) + makenesled( m_ch1EnvLoopedBtn, 129, KNOB_Y1 - 12, "Enable envelope 1 loop" ) + + makenesled( m_ch1SweepEnabledBtn, KNOB_X6, KNOB_Y1 - 12, "Enable sweep 1" ) + makeknob( m_ch1SweepAmtKnob, KNOB_X6, KNOB_Y1, "Sweep amount", "", "" ) + makeknob( m_ch1SweepRateKnob, KNOB_X7, KNOB_Y1, "Sweep rate", "", "" ) + + int dcx = 117; + makedcled( ch1_dc1, dcx, 42, "12.5% Duty cycle", "nesdc1_on" ) + dcx += 13; + makedcled( ch1_dc2, dcx, 42, "25% Duty cycle", "nesdc2_on" ) + dcx += 13; + makedcled( ch1_dc3, dcx, 42, "50% Duty cycle", "nesdc3_on" ) + dcx += 13; + makedcled( ch1_dc4, dcx, 42, "75% Duty cycle", "nesdc4_on" ) + + m_ch1DutyCycleGrp = new automatableButtonGroup( this ); + m_ch1DutyCycleGrp -> addButton( ch1_dc1 ); + m_ch1DutyCycleGrp -> addButton( ch1_dc2 ); + m_ch1DutyCycleGrp -> addButton( ch1_dc3 ); + m_ch1DutyCycleGrp -> addButton( ch1_dc4 ); + + + + // channel 2 + + makeknob( m_ch2VolumeKnob, KNOB_X1, KNOB_Y2, "Volume", "", "" ) + makeknob( m_ch2CrsKnob, KNOB_X2, KNOB_Y2, "Coarse detune", "", "" ) + makeknob( m_ch2EnvLenKnob, KNOB_X3, KNOB_Y2, "Envelope length", "", "" ) + + makenesled( m_ch2EnabledBtn, KNOB_X1, KNOB_Y2 - 12, "Enable channel 2" ) + makenesled( m_ch2EnvEnabledBtn, KNOB_X3, KNOB_Y2 - 12, "Enable envelope 2" ) + makenesled( m_ch2EnvLoopedBtn, 129, KNOB_Y2 - 12, "Enable envelope 2 loop" ) + + makenesled( m_ch2SweepEnabledBtn, KNOB_X6, KNOB_Y2 - 12, "Enable sweep 2" ) + makeknob( m_ch2SweepAmtKnob, KNOB_X6, KNOB_Y2, "Sweep amount", "", "" ) + makeknob( m_ch2SweepRateKnob, KNOB_X7, KNOB_Y2, "Sweep rate", "", "" ) + + dcx = 117; + makedcled( ch2_dc1, dcx, 99, "12.5% Duty cycle", "nesdc1_on" ) + dcx += 13; + makedcled( ch2_dc2, dcx, 99, "25% Duty cycle", "nesdc2_on" ) + dcx += 13; + makedcled( ch2_dc3, dcx, 99, "50% Duty cycle", "nesdc3_on" ) + dcx += 13; + makedcled( ch2_dc4, dcx, 99, "75% Duty cycle", "nesdc4_on" ) + + m_ch2DutyCycleGrp = new automatableButtonGroup( this ); + m_ch2DutyCycleGrp -> addButton( ch2_dc1 ); + m_ch2DutyCycleGrp -> addButton( ch2_dc2 ); + m_ch2DutyCycleGrp -> addButton( ch2_dc3 ); + m_ch2DutyCycleGrp -> addButton( ch2_dc4 ); + + + + //channel 3 + makenesled( m_ch3EnabledBtn, KNOB_X1, KNOB_Y3 - 12, "Enable channel 3" ) + makeknob( m_ch3VolumeKnob, KNOB_X1, KNOB_Y3, "Volume", "", "" ) + makeknob( m_ch3CrsKnob, KNOB_X2, KNOB_Y3, "Coarse detune", "", "" ) + + + //channel 4 + makeknob( m_ch4VolumeKnob, KNOB_X1, KNOB_Y4, "Volume", "", "" ) + makeknob( m_ch4NoiseFreqKnob, KNOB_X2, KNOB_Y4, "Noise Frequency", "", "" ) + makeknob( m_ch4EnvLenKnob, KNOB_X3, KNOB_Y4, "Envelope length", "", "" ) + + makenesled( m_ch4EnabledBtn, KNOB_X1, KNOB_Y4 - 12, "Enable channel 4" ) + makenesled( m_ch4EnvEnabledBtn, KNOB_X3, KNOB_Y4 - 12, "Enable envelope 4" ) + makenesled( m_ch4EnvLoopedBtn, 129, KNOB_Y4 - 12, "Enable envelope 4 loop" ) + + makenesled( m_ch4NoiseModeBtn, 129, 203, "Noise mode" ) + makenesled( m_ch4NoiseFreqModeBtn, 129, 224, "Use note frequency for noise" ) + + + //master + makeknob( m_masterVolKnob, KNOB_X4, KNOB_Y3, "Master Volume", "", "" ) + makeknob( m_vibratoKnob, KNOB_X5, KNOB_Y3, "Vibrato (unimplemented)", "", "" ) + +} + + + +NesInstrumentView::~NesInstrumentView() +{ +} + + +void NesInstrumentView::modelChanged() +{ + NesInstrument * nes = castModel(); + + m_ch1EnabledBtn->setModel( &nes->m_ch1Enabled ); + m_ch1CrsKnob->setModel( &nes->m_ch1Crs ); + m_ch1VolumeKnob->setModel( &nes->m_ch1Volume ); + + m_ch1EnvEnabledBtn->setModel( &nes->m_ch1EnvEnabled ); + m_ch1EnvLoopedBtn->setModel( &nes->m_ch1EnvLooped ); + m_ch1EnvLenKnob->setModel( &nes->m_ch1EnvLen ); + + m_ch1DutyCycleGrp->setModel( &nes->m_ch1DutyCycle ); + + m_ch1SweepEnabledBtn->setModel( &nes->m_ch1SweepEnabled ); + m_ch1SweepAmtKnob->setModel( &nes->m_ch1SweepAmt ); + m_ch1SweepRateKnob->setModel( &nes->m_ch1SweepRate ); + + // channel 2 + m_ch2EnabledBtn->setModel( &nes->m_ch2Enabled ); + m_ch2CrsKnob->setModel( &nes->m_ch2Crs ); + m_ch2VolumeKnob->setModel( &nes->m_ch2Volume ); + + m_ch2EnvEnabledBtn->setModel( &nes->m_ch2EnvEnabled ); + m_ch2EnvLoopedBtn->setModel( &nes->m_ch2EnvLooped ); + m_ch2EnvLenKnob->setModel( &nes->m_ch2EnvLen ); + + m_ch2DutyCycleGrp->setModel( &nes->m_ch2DutyCycle ); + + m_ch2SweepEnabledBtn->setModel( &nes->m_ch2SweepEnabled ); + m_ch2SweepAmtKnob->setModel( &nes->m_ch2SweepAmt ); + m_ch2SweepRateKnob->setModel( &nes->m_ch2SweepRate ); + + //channel 3 + m_ch3EnabledBtn->setModel( &nes->m_ch3Enabled ); + m_ch3CrsKnob->setModel( &nes->m_ch3Crs ); + m_ch3VolumeKnob->setModel( &nes->m_ch3Volume ); + + //channel 4 + m_ch4EnabledBtn->setModel( &nes->m_ch4Enabled ); + m_ch4VolumeKnob->setModel( &nes->m_ch4Volume ); + + m_ch4EnvEnabledBtn->setModel( &nes->m_ch4EnvEnabled ); + m_ch4EnvLoopedBtn->setModel( &nes->m_ch4EnvLooped ); + m_ch4EnvLenKnob->setModel( &nes->m_ch4EnvLen ); + + m_ch4NoiseModeBtn->setModel( &nes->m_ch4NoiseMode ); + m_ch4NoiseFreqModeBtn->setModel( &nes->m_ch4NoiseFreqMode ); + m_ch4NoiseFreqKnob->setModel( &nes->m_ch4NoiseFreq ); + + //master + m_masterVolKnob->setModel( &nes->m_masterVol ); + m_vibratoKnob->setModel( &nes->m_vibrato ); +} + + +extern "C" +{ + +// necessary for getting instance out of shared lib +Plugin * PLUGIN_EXPORT lmms_plugin_main( Model *, void * _data ) +{ + return( new NesInstrument( static_cast( _data ) ) ); +} + + +} + + +#include "moc_Nes.cxx" diff --git a/plugins/nes/Nes.h b/plugins/nes/Nes.h new file mode 100644 index 000000000..19a669e19 --- /dev/null +++ b/plugins/nes/Nes.h @@ -0,0 +1,336 @@ +/* Nes.h - A NES instrument plugin for LMMS + * + * Copyright (c) 2014 Vesa Kivimäki + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net + * + * 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. + * + */ + +#ifndef NES_H +#define NES_H + +#include "Instrument.h" +#include "InstrumentView.h" +#include "AutomatableModel.h" +#include "automatable_button.h" +#include "TempoSyncKnob.h" +#include "NotePlayHandle.h" +#include "pixmap_button.h" + + +#define makeknob( name, x, y, hint, unit, oname ) \ + name = new knob( knobStyled, this ); \ + name ->move( x, y ); \ + name ->setHintText( tr( hint ) + " ", unit ); \ + name ->setObjectName( oname ); \ + name ->setFixedSize( 29, 29 ); + +#define makenesled( name, x, y, ttip ) \ + name = new pixmapButton( this, NULL ); \ + name -> setCheckable( true ); \ + name -> move( x, y ); \ + name -> setActiveGraphic( PLUGIN_NAME::getIconPixmap( "nesled_on" ) ); \ + name -> setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "nesled_off" ) ); \ + toolTip::add( name, tr( ttip ) ); + +#define makedcled( name, x, y, ttip, active ) \ + pixmapButton * name = new pixmapButton( this, NULL ); \ + name -> move( x, y ); \ + name -> setActiveGraphic( PLUGIN_NAME::getIconPixmap( active ) ); \ + name -> setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "nesdc_off" ) ); \ + toolTip::add( name, tr( ttip ) ); + +const float NES_SIMPLE_FILTER = 1.0 / 20.0; // simulate nes analog audio output +const float NFB = 895000.0f; +const float NOISE_FREQS[16] = + { NFB/5, NFB/9, NFB/17, NFB/33, NFB/65, NFB/97, NFB/129, NFB/161, NFB/193, NFB/255, NFB/381, NFB/509, NFB/763, NFB/1017, NFB/2035, NFB/4069 }; +const uint16_t LFSR_INIT = 1; +const float DUTY_CYCLE[4] = { 0.125, 0.25, 0.5, 0.75 }; +const float DITHER_AMP = 1.0 / 60.0; +const float MIN_FREQ = 10.0; +const int TRIANGLE_WAVETABLE[32] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; + +const float NES_DIST = 0.9f; // simulate the slight nonlinear distortion in nes audio output + +const float NES_MIXING_12 = 1.0 / 20.0; +const float NES_MIXING_34 = 1.0 / 12.0; +const float NES_MIXING_ALL = 1.0 / ( NES_MIXING_12 + NES_MIXING_34 ); // constants to simulate the hardwired mixing values for nes channels + +const int MIN_WLEN = 8; + + +class NesInstrument; + +class NesObject +{ +public: + NesObject( NesInstrument * nes, const sample_rate_t samplerate, NotePlayHandle * nph, fpp_t frames ); + virtual ~NesObject(); + + void renderOutput( sampleFrame * buf, fpp_t frames ); + + void updateLFSR( bool mode ) + { + uint16_t LFSRx; + if( mode ) + { + LFSRx = m_LFSR & ( 1 << 8 ); // get bit 13 + LFSRx <<= 6; // shit to left so it overlaps with bit 14 + } + else + { + LFSRx = m_LFSR & ( 1 << 13 ); // get bit 13 + LFSRx <<= 1; // shit to left so it overlaps with bit 14 + } + m_LFSR ^= LFSRx; // xor bit 14 with bit 8/13 depending on mode + m_LFSR <<= 1; // shift bit 14 to bit 15 + + // cycle bit 14 to 0 + if( m_LFSR & ( 1 << 15 ) ) // if bit 15 is set + { + m_LFSR++; // set bit 0 - we know it to be 0 because of the left shift so we can just inc here + } + } + + inline bool LFSR() // returns true if bit 14 is set + { + if( m_LFSR & ( 1 << 14 ) ) + { + return true; + } + return false; + } + + inline int wavelength( float freq ) + { + return static_cast( m_samplerate / freq ); + } + + inline float signedPow( float f, float e ) + { + return f < 0 + ? powf( qAbs( f ), e ) * -1.0f + : powf( f, e ); + } + +private: + NesInstrument * m_parent; + const sample_rate_t m_samplerate; + NotePlayHandle * m_nph; + fpp_t m_fpp; + + int m_ch1Counter; + int m_ch2Counter; + int m_ch3Counter; + int m_ch4Counter; + + int m_ch1EnvCounter; + int m_ch2EnvCounter; + int m_ch4EnvCounter; + + int m_ch1EnvValue; + int m_ch2EnvValue; + int m_ch4EnvValue; + + int m_ch1SweepCounter; + int m_ch2SweepCounter; + + uint16_t m_LFSR; + + float m_12Last; + float m_34Last; + + float m_itm; + float m_otm; + + float m_lastNoteFreq; + + int m_maxWlen; + + float m_nsf; + +// wavelengths + int m_wlen1; + int m_wlen2; + int m_wlen3; + int m_wlen4; +}; + + +class NesInstrument : public Instrument +{ + Q_OBJECT +public: + NesInstrument( InstrumentTrack * instrumentTrack ); + virtual ~NesInstrument(); + + virtual void playNote( NotePlayHandle * n, + sampleFrame * workingBuffer ); + virtual void deleteNotePluginData( NotePlayHandle * n ); + + + virtual void saveSettings( QDomDocument & doc, + QDomElement & element ); + virtual void loadSettings( const QDomElement & element ); + + virtual QString nodeName() const; + + virtual f_cnt_t desiredReleaseFrames() const + { + return( 8 ); + } + + virtual PluginView * instantiateView( QWidget * parent ); + +public slots: + void updateFreq1(); + void updateFreq2(); + void updateFreq3(); + +protected: + //freq helpers + float m_freq1; + float m_freq2; + float m_freq3; + +private: + // channel 1 + BoolModel m_ch1Enabled; + FloatModel m_ch1Crs; + FloatModel m_ch1Volume; + + BoolModel m_ch1EnvEnabled; + BoolModel m_ch1EnvLooped; + FloatModel m_ch1EnvLen; + + IntModel m_ch1DutyCycle; + + BoolModel m_ch1SweepEnabled; + FloatModel m_ch1SweepAmt; + FloatModel m_ch1SweepRate; + + // channel 2 + BoolModel m_ch2Enabled; + FloatModel m_ch2Crs; + FloatModel m_ch2Volume; + + BoolModel m_ch2EnvEnabled; + BoolModel m_ch2EnvLooped; + FloatModel m_ch2EnvLen; + + IntModel m_ch2DutyCycle; + + BoolModel m_ch2SweepEnabled; + FloatModel m_ch2SweepAmt; + FloatModel m_ch2SweepRate; + + //channel 3 + BoolModel m_ch3Enabled; + FloatModel m_ch3Crs; + FloatModel m_ch3Volume; + + //channel 4 + BoolModel m_ch4Enabled; + FloatModel m_ch4Volume; + + BoolModel m_ch4EnvEnabled; + BoolModel m_ch4EnvLooped; + FloatModel m_ch4EnvLen; + + BoolModel m_ch4NoiseMode; + BoolModel m_ch4NoiseFreqMode; + FloatModel m_ch4NoiseFreq; + + //master + FloatModel m_masterVol; + FloatModel m_vibrato; + + + friend class NesObject; + friend class NesInstrumentView; +}; + + +class NesInstrumentView : public InstrumentView +{ + Q_OBJECT +public: + NesInstrumentView( Instrument * instrument, + QWidget * parent ); + virtual ~NesInstrumentView(); + +private: + virtual void modelChanged(); + + // channel 1 + pixmapButton * m_ch1EnabledBtn; + knob * m_ch1CrsKnob; + knob * m_ch1VolumeKnob; + + pixmapButton * m_ch1EnvEnabledBtn; + pixmapButton * m_ch1EnvLoopedBtn; + knob * m_ch1EnvLenKnob; + + automatableButtonGroup * m_ch1DutyCycleGrp; + + pixmapButton * m_ch1SweepEnabledBtn; + knob * m_ch1SweepAmtKnob; + knob * m_ch1SweepRateKnob; + + // channel 2 + pixmapButton * m_ch2EnabledBtn; + knob * m_ch2CrsKnob; + knob * m_ch2VolumeKnob; + + pixmapButton * m_ch2EnvEnabledBtn; + pixmapButton * m_ch2EnvLoopedBtn; + knob * m_ch2EnvLenKnob; + + automatableButtonGroup * m_ch2DutyCycleGrp; + + pixmapButton * m_ch2SweepEnabledBtn; + knob * m_ch2SweepAmtKnob; + knob * m_ch2SweepRateKnob; + + //channel 3 + pixmapButton * m_ch3EnabledBtn; + knob * m_ch3CrsKnob; + knob * m_ch3VolumeKnob; + + //channel 4 + pixmapButton * m_ch4EnabledBtn; + knob * m_ch4VolumeKnob; + + pixmapButton * m_ch4EnvEnabledBtn; + pixmapButton * m_ch4EnvLoopedBtn; + knob * m_ch4EnvLenKnob; + + pixmapButton * m_ch4NoiseModeBtn; + pixmapButton * m_ch4NoiseFreqModeBtn; + knob * m_ch4NoiseFreqKnob; + + //master + knob * m_masterVolKnob; + knob * m_vibratoKnob; + + static QPixmap * s_artwork; +}; + +#endif diff --git a/plugins/nes/artwork.png b/plugins/nes/artwork.png new file mode 100644 index 000000000..ac7dc4abb Binary files /dev/null and b/plugins/nes/artwork.png differ diff --git a/plugins/nes/logo.png b/plugins/nes/logo.png new file mode 100644 index 000000000..44211ca5b Binary files /dev/null and b/plugins/nes/logo.png differ diff --git a/plugins/nes/nesdc1_on.png b/plugins/nes/nesdc1_on.png new file mode 100644 index 000000000..9edc33070 Binary files /dev/null and b/plugins/nes/nesdc1_on.png differ diff --git a/plugins/nes/nesdc2_on.png b/plugins/nes/nesdc2_on.png new file mode 100644 index 000000000..6dbb38c41 Binary files /dev/null and b/plugins/nes/nesdc2_on.png differ diff --git a/plugins/nes/nesdc3_on.png b/plugins/nes/nesdc3_on.png new file mode 100644 index 000000000..f8eccbe86 Binary files /dev/null and b/plugins/nes/nesdc3_on.png differ diff --git a/plugins/nes/nesdc4_on.png b/plugins/nes/nesdc4_on.png new file mode 100644 index 000000000..8f7590195 Binary files /dev/null and b/plugins/nes/nesdc4_on.png differ diff --git a/plugins/nes/nesdc_off.png b/plugins/nes/nesdc_off.png new file mode 100644 index 000000000..7da254d6d Binary files /dev/null and b/plugins/nes/nesdc_off.png differ diff --git a/plugins/nes/nesled_off.png b/plugins/nes/nesled_off.png new file mode 100644 index 000000000..2fe4d0bd0 Binary files /dev/null and b/plugins/nes/nesled_off.png differ diff --git a/plugins/nes/nesled_on.png b/plugins/nes/nesled_on.png new file mode 100644 index 000000000..34f83a2eb Binary files /dev/null and b/plugins/nes/nesled_on.png differ