mirror of
https://github.com/LMMS/lmms.git
synced 2026-01-06 05:28:17 -05:00
Given these changes, the knife tool now uses `Qt::SplitHCursor`, but `Qt::IBeamCursor` is also a a viable option. I am noting this should substantial concern arise over the appearance of `Qt::SplitHCursor` due to cursor themes, such as the default one applied to applications running under WSL.
1288 lines
32 KiB
C++
1288 lines
32 KiB
C++
/*
|
|
* GigPlayer.cpp - a GIG player using libgig (based on Sf2 player plugin)
|
|
*
|
|
* Copyright (c) 2008 Paul Giblock <drfaygo/at/gmail/dot/com>
|
|
* Copyright (c) 2009-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
|
|
*
|
|
* A few lines of code taken from LinuxSampler (also GPLv2) where noted:
|
|
* Copyright (C) 2003,2004 by Benno Senoner and Christian Schoenebeck
|
|
* Copyright (C) 2005-2008 Christian Schoenebeck
|
|
* Copyright (C) 2009-2010 Christian Schoenebeck and Grigor Iliev
|
|
*
|
|
* 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 "GigPlayer.h"
|
|
|
|
#include <cstring>
|
|
#include <QDebug>
|
|
#include <QLayout>
|
|
#include <QLabel>
|
|
#include <QDomDocument>
|
|
|
|
#include "AudioEngine.h"
|
|
#include "ConfigManager.h"
|
|
#include "endian_handling.h"
|
|
#include "Engine.h"
|
|
#include "FileDialog.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "InstrumentPlayHandle.h"
|
|
#include "Knob.h"
|
|
#include "MixHelpers.h"
|
|
#include "NotePlayHandle.h"
|
|
#include "PathUtil.h"
|
|
#include "Sample.h"
|
|
#include "Song.h"
|
|
|
|
#include "PatchesDialog.h"
|
|
#include "LcdSpinBox.h"
|
|
|
|
#include "embed.h"
|
|
#include "plugin_export.h"
|
|
|
|
namespace lmms
|
|
{
|
|
|
|
|
|
extern "C"
|
|
{
|
|
|
|
Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor =
|
|
{
|
|
LMMS_STRINGIFY( PLUGIN_NAME ),
|
|
"GIG Player",
|
|
QT_TRANSLATE_NOOP( "PluginBrowser", "Player for GIG files" ),
|
|
"Garrett Wilson <g/at/floft/dot/net>",
|
|
0x0100,
|
|
Plugin::Type::Instrument,
|
|
new PluginPixmapLoader( "logo" ),
|
|
"gig",
|
|
nullptr,
|
|
} ;
|
|
|
|
}
|
|
|
|
GigInstrument::GigInstrument(InstrumentTrack* _instrument_track)
|
|
: Instrument(_instrument_track, &gigplayer_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsNotBendable)
|
|
, m_instance(nullptr)
|
|
, m_instrument(nullptr)
|
|
, m_filename("")
|
|
, m_bankNum(0, 0, 999, this, tr("Bank"))
|
|
, m_patchNum(0, 0, 127, this, tr("Patch"))
|
|
, m_gain(1.0f, 0.0f, 5.0f, 0.01f, this, tr("Gain"))
|
|
, m_RandomSeed(0)
|
|
, m_currentKeyDimension(0)
|
|
{
|
|
auto iph = new InstrumentPlayHandle(this, _instrument_track);
|
|
Engine::audioEngine()->addPlayHandle( iph );
|
|
|
|
updateSampleRate();
|
|
|
|
connect( &m_bankNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) );
|
|
connect( &m_patchNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) );
|
|
connect( Engine::audioEngine(), SIGNAL( sampleRateChanged() ), this, SLOT( updateSampleRate() ) );
|
|
}
|
|
|
|
|
|
|
|
|
|
GigInstrument::~GigInstrument()
|
|
{
|
|
Engine::audioEngine()->removePlayHandlesOfTypes( instrumentTrack(),
|
|
PlayHandle::Type::NotePlayHandle
|
|
| PlayHandle::Type::InstrumentPlayHandle );
|
|
freeInstance();
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrument::saveSettings( QDomDocument & _doc, QDomElement & _this )
|
|
{
|
|
_this.setAttribute( "src", m_filename );
|
|
m_patchNum.saveSettings( _doc, _this, "patch" );
|
|
m_bankNum.saveSettings( _doc, _this, "bank" );
|
|
|
|
m_gain.saveSettings( _doc, _this, "gain" );
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrument::loadSettings( const QDomElement & _this )
|
|
{
|
|
openFile( _this.attribute( "src" ), false );
|
|
m_patchNum.loadSettings( _this, "patch" );
|
|
m_bankNum.loadSettings( _this, "bank" );
|
|
|
|
m_gain.loadSettings( _this, "gain" );
|
|
|
|
updatePatch();
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrument::loadFile( const QString & _file )
|
|
{
|
|
if( !_file.isEmpty() && QFileInfo( _file ).exists() )
|
|
{
|
|
openFile( _file, false );
|
|
updatePatch();
|
|
updateSampleRate();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
AutomatableModel * GigInstrument::childModel( const QString & _modelName )
|
|
{
|
|
if( _modelName == "bank" )
|
|
{
|
|
return &m_bankNum;
|
|
}
|
|
else if( _modelName == "patch" )
|
|
{
|
|
return &m_patchNum;
|
|
}
|
|
|
|
qCritical() << "requested unknown model " << _modelName;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
|
|
|
|
QString GigInstrument::nodeName() const
|
|
{
|
|
return gigplayer_plugin_descriptor.name;
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrument::freeInstance()
|
|
{
|
|
QMutexLocker synthLock( &m_synthMutex );
|
|
QMutexLocker notesLock( &m_notesMutex );
|
|
|
|
if( m_instance != nullptr )
|
|
{
|
|
delete m_instance;
|
|
m_instance = nullptr;
|
|
|
|
// If we're changing instruments, we got to make sure that we
|
|
// remove all pointers to the old samples and don't try accessing
|
|
// that instrument again
|
|
m_instrument = nullptr;
|
|
m_notes.clear();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrument::openFile( const QString & _gigFile, bool updateTrackName )
|
|
{
|
|
emit fileLoading();
|
|
|
|
// Remove the current instrument if one is selected
|
|
freeInstance();
|
|
|
|
{
|
|
QMutexLocker locker( &m_synthMutex );
|
|
|
|
try
|
|
{
|
|
m_instance = new GigInstance( PathUtil::toAbsolute( _gigFile ) );
|
|
m_filename = PathUtil::toShortestRelative( _gigFile );
|
|
}
|
|
catch( ... )
|
|
{
|
|
m_instance = nullptr;
|
|
m_filename = "";
|
|
}
|
|
}
|
|
|
|
emit fileChanged();
|
|
|
|
if( updateTrackName == true )
|
|
{
|
|
instrumentTrack()->setName(PathUtil::cleanName( _gigFile ) );
|
|
updatePatch();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrument::updatePatch()
|
|
{
|
|
if( m_bankNum.value() >= 0 && m_patchNum.value() >= 0 )
|
|
{
|
|
getInstrument();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
QString GigInstrument::getCurrentPatchName()
|
|
{
|
|
QMutexLocker locker( &m_synthMutex );
|
|
|
|
if( m_instance == nullptr )
|
|
{
|
|
return "";
|
|
}
|
|
|
|
int iBankSelected = m_bankNum.value();
|
|
int iProgSelected = m_patchNum.value();
|
|
|
|
gig::Instrument * pInstrument = m_instance->gig.GetFirstInstrument();
|
|
|
|
while( pInstrument != nullptr )
|
|
{
|
|
int iBank = pInstrument->MIDIBank;
|
|
int iProg = pInstrument->MIDIProgram;
|
|
|
|
if( iBank == iBankSelected && iProg == iProgSelected )
|
|
{
|
|
QString name = QString::fromStdString( pInstrument->pInfo->Name );
|
|
|
|
if( name == "" )
|
|
{
|
|
name = "<no name>";
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
pInstrument = m_instance->gig.GetNextInstrument();
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
|
|
|
|
|
|
// A key has been pressed
|
|
void GigInstrument::playNote( NotePlayHandle * _n, SampleFrame* )
|
|
{
|
|
const float LOG440 = 2.643452676f;
|
|
|
|
int midiNote = (int) floor( 12.0 * ( log2( _n->unpitchedFrequency() ) - LOG440 ) - 4.0 );
|
|
|
|
// out of range?
|
|
if( midiNote <= 0 || midiNote >= 128 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!_n->m_pluginData)
|
|
{
|
|
auto pluginData = new GIGPluginData;
|
|
pluginData->midiNote = midiNote;
|
|
_n->m_pluginData = pluginData;
|
|
|
|
const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity();
|
|
const uint velocity = _n->midiVelocity( baseVelocity );
|
|
|
|
QMutexLocker locker( &m_notesMutex );
|
|
m_notes.push_back( GigNote( midiNote, velocity, _n->unpitchedFrequency(), pluginData ) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process the notes and output a certain number of frames (e.g. 256, set in
|
|
// the preferences)
|
|
void GigInstrument::play( SampleFrame* _working_buffer )
|
|
{
|
|
const fpp_t frames = Engine::audioEngine()->framesPerPeriod();
|
|
const auto rate = Engine::audioEngine()->outputSampleRate();
|
|
|
|
// Initialize to zeros
|
|
std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) );
|
|
|
|
m_synthMutex.lock();
|
|
m_notesMutex.lock();
|
|
|
|
if( m_instance == nullptr || m_instrument == nullptr )
|
|
{
|
|
m_synthMutex.unlock();
|
|
m_notesMutex.unlock();
|
|
return;
|
|
}
|
|
|
|
for( QList<GigNote>::iterator it = m_notes.begin(); it != m_notes.end(); ++it )
|
|
{
|
|
// Process notes in the KeyUp state, adding release samples if desired
|
|
if( it->state == GigState::KeyUp )
|
|
{
|
|
// If there are no samples, we're done
|
|
if( it->samples.empty() )
|
|
{
|
|
it->state = GigState::Completed;
|
|
}
|
|
else
|
|
{
|
|
it->state = GigState::PlayingKeyUp;
|
|
|
|
// Notify each sample that the key has been released
|
|
for (auto& sample : it->samples)
|
|
{
|
|
sample.adsr.keyup();
|
|
}
|
|
|
|
// Add release samples if available
|
|
if( it->release == true )
|
|
{
|
|
addSamples( *it, true );
|
|
}
|
|
}
|
|
}
|
|
// Process notes in the KeyDown state, adding samples for the notes
|
|
else if( it->state == GigState::KeyDown )
|
|
{
|
|
it->state = GigState::PlayingKeyDown;
|
|
addSamples( *it, false );
|
|
}
|
|
|
|
// Delete ended samples
|
|
for (auto sample = it->samples.begin(); sample != it->samples.end(); ++sample)
|
|
{
|
|
// Delete if the ADSR for a sample is complete for normal
|
|
// notes, or if a release sample, then if we've reached
|
|
// the end of the sample
|
|
if( sample->sample == nullptr || sample->adsr.done() ||
|
|
( it->isRelease == true &&
|
|
sample->pos >= sample->sample->SamplesTotal - 1 ) )
|
|
{
|
|
sample = it->samples.erase( sample );
|
|
|
|
if( sample == it->samples.end() )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete ended notes (either in the completed state or all the samples ended)
|
|
if( it->state == GigState::Completed || it->samples.empty() )
|
|
{
|
|
it = m_notes.erase( it );
|
|
|
|
if( it == m_notes.end() )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fill buffer with portions of the note samples
|
|
for (auto& note : m_notes)
|
|
{
|
|
// Only process the notes if we're in a playing state
|
|
if (!(note.state == GigState::PlayingKeyDown || note.state == GigState::PlayingKeyUp ))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (auto& sample : note.samples)
|
|
{
|
|
if (sample.sample == nullptr || sample.region == nullptr) { continue; }
|
|
|
|
float freq_factor = 1.0; // How much to resample
|
|
|
|
// Resample to be the correct pitch when the sample provided isn't
|
|
// solely for this one note (e.g. one or two samples per octave) or
|
|
// we are processing at a different sample rate
|
|
if (sample.region->PitchTrack == true || rate != sample.sample->SamplesPerSecond)
|
|
{
|
|
// Factor just for resampling
|
|
freq_factor = 1.0 * rate / sample.sample->SamplesPerSecond;
|
|
|
|
// Factor for pitch shifting as well as resampling
|
|
if (sample.region->PitchTrack == true) { freq_factor *= sample.freqFactor; }
|
|
}
|
|
|
|
// Apply ADSR using a copy so if we don't use these samples when
|
|
// resampling, the ADSR doesn't get messed up
|
|
ADSR copy = sample.adsr;
|
|
|
|
sample.m_resampler.setRatio(freq_factor);
|
|
|
|
// TODO: These kind of playback pipelines/graphs are repeated within other parts of the codebase that work
|
|
// with audio samples. We should find a way to unify this but the right abstraction is not so clear yet.
|
|
auto framesMixed = f_cnt_t{0};
|
|
while (framesMixed < frames)
|
|
{
|
|
if (sample.m_sourceBufferView.empty())
|
|
{
|
|
loadSample(sample, sample.m_sourceBuffer.data(), sample.m_sourceBuffer.size());
|
|
|
|
for (auto& frame : sample.m_sourceBuffer)
|
|
{
|
|
frame *= copy.value();
|
|
}
|
|
|
|
sample.pos += sample.m_sourceBuffer.size();
|
|
sample.adsr.inc(sample.m_sourceBuffer.size());
|
|
sample.m_sourceBufferView = sample.m_sourceBuffer;
|
|
}
|
|
|
|
if (sample.m_mixBufferView.empty()) { sample.m_mixBufferView = sample.m_mixBuffer; }
|
|
|
|
const auto [inputFramesUsed, outputFramesGenerated] = sample.m_resampler.process(
|
|
{&sample.m_sourceBufferView.data()[0][0], 2, sample.m_sourceBufferView.size()},
|
|
{&sample.m_mixBufferView.data()[0][0], 2, sample.m_mixBufferView.size()});
|
|
|
|
if (inputFramesUsed == 0 && outputFramesGenerated == 0)
|
|
{
|
|
std::fill_n(&_working_buffer[framesMixed], frames - framesMixed, SampleFrame{});
|
|
break;
|
|
}
|
|
|
|
const auto framesToMix = std::min(outputFramesGenerated, frames - framesMixed);
|
|
for (auto i = f_cnt_t{0}; i < framesToMix; ++i)
|
|
{
|
|
_working_buffer[framesMixed + i] += sample.m_mixBufferView[i];
|
|
}
|
|
|
|
sample.m_sourceBufferView = sample.m_sourceBufferView.subspan(inputFramesUsed);
|
|
sample.m_mixBufferView = sample.m_mixBufferView.subspan(framesToMix);
|
|
framesMixed += framesToMix;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_notesMutex.unlock();
|
|
m_synthMutex.unlock();
|
|
|
|
// Set gain properly based on volume control
|
|
for( f_cnt_t i = 0; i < frames; ++i )
|
|
{
|
|
_working_buffer[i][0] *= m_gain.value();
|
|
_working_buffer[i][1] *= m_gain.value();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrument::loadSample( GigSample& sample, SampleFrame* sampleData, f_cnt_t samples )
|
|
{
|
|
if( sampleData == nullptr || samples < 1 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Determine if we need to loop part of this sample
|
|
bool loop = false;
|
|
gig::loop_type_t loopType = gig::loop_type_normal;
|
|
f_cnt_t loopStart = 0;
|
|
f_cnt_t loopLength = 0;
|
|
|
|
if( sample.region->pSampleLoops != nullptr )
|
|
{
|
|
for( uint32_t i = 0; i < sample.region->SampleLoops; ++i )
|
|
{
|
|
loop = true;
|
|
loopType = static_cast<gig::loop_type_t>( sample.region->pSampleLoops[i].LoopType );
|
|
loopStart = sample.region->pSampleLoops[i].LoopStart;
|
|
loopLength = sample.region->pSampleLoops[i].LoopLength;
|
|
|
|
// Currently only support at max one loop
|
|
break;
|
|
}
|
|
}
|
|
|
|
unsigned long allocationsize = samples * sample.sample->FrameSize;
|
|
int8_t buffer[allocationsize];
|
|
|
|
// Load the sample in different ways depending on if we're looping or not
|
|
if( loop == true && ( sample.pos >= loopStart || sample.pos + samples > loopStart ) )
|
|
{
|
|
// Calculate the new position based on the type of loop
|
|
if( loopType == gig::loop_type_bidirectional )
|
|
{
|
|
sample.pos = getPingPongIndex( sample.pos, loopStart, loopStart + loopLength );
|
|
}
|
|
else
|
|
{
|
|
sample.pos = getLoopedIndex( sample.pos, loopStart, loopStart + loopLength );
|
|
// TODO: also implement loop_type_backward support
|
|
}
|
|
|
|
sample.sample->SetPos( sample.pos );
|
|
|
|
// Load the samples (based on gig::Sample::ReadAndLoop) even around the end
|
|
// of a loop boundary wrapping to the beginning of the loop region
|
|
long samplestoread = samples;
|
|
long samplestoloopend = 0;
|
|
long readsamples = 0;
|
|
long totalreadsamples = 0;
|
|
long loopEnd = loopStart + loopLength;
|
|
|
|
do
|
|
{
|
|
samplestoloopend = loopEnd - sample.sample->GetPos();
|
|
readsamples = sample.sample->Read( &buffer[totalreadsamples * sample.sample->FrameSize],
|
|
std::min( samplestoread, samplestoloopend ) );
|
|
samplestoread -= readsamples;
|
|
totalreadsamples += readsamples;
|
|
|
|
if( readsamples >= samplestoloopend )
|
|
{
|
|
sample.sample->SetPos( loopStart );
|
|
}
|
|
}
|
|
while( samplestoread > 0 && readsamples > 0 );
|
|
}
|
|
else
|
|
{
|
|
sample.sample->SetPos( sample.pos );
|
|
|
|
unsigned long size = sample.sample->Read( &buffer, samples ) * sample.sample->FrameSize;
|
|
std::memset( (int8_t*) &buffer + size, 0, allocationsize - size );
|
|
}
|
|
|
|
// Convert from 16 or 24 bit into 32-bit float
|
|
if( sample.sample->BitDepth == 24 ) // 24 bit
|
|
{
|
|
auto pInt = reinterpret_cast<uint8_t*>(&buffer);
|
|
|
|
for( f_cnt_t i = 0; i < samples; ++i )
|
|
{
|
|
// libgig gives 24-bit data as little endian, so we must
|
|
// convert if on a big endian system
|
|
int32_t valueLeft = swap32IfBE(
|
|
( pInt[ 3 * sample.sample->Channels * i ] << 8 ) |
|
|
( pInt[ 3 * sample.sample->Channels * i + 1 ] << 16 ) |
|
|
( pInt[ 3 * sample.sample->Channels * i + 2 ] << 24 ) );
|
|
|
|
// Store the notes to this buffer before saving to output
|
|
// so we can fade them out as needed
|
|
sampleData[i][0] = 1.0 / 0x100000000 * sample.attenuation * valueLeft;
|
|
|
|
if( sample.sample->Channels == 1 )
|
|
{
|
|
sampleData[i][1] = sampleData[i][0];
|
|
}
|
|
else
|
|
{
|
|
int32_t valueRight = swap32IfBE(
|
|
( pInt[ 3 * sample.sample->Channels * i + 3 ] << 8 ) |
|
|
( pInt[ 3 * sample.sample->Channels * i + 4 ] << 16 ) |
|
|
( pInt[ 3 * sample.sample->Channels * i + 5 ] << 24 ) );
|
|
|
|
sampleData[i][1] = 1.0 / 0x100000000 * sample.attenuation * valueRight;
|
|
}
|
|
}
|
|
}
|
|
else // 16 bit
|
|
{
|
|
auto pInt = reinterpret_cast<int16_t*>(&buffer);
|
|
|
|
for( f_cnt_t i = 0; i < samples; ++i )
|
|
{
|
|
sampleData[i][0] = 1.0 / 0x10000 *
|
|
pInt[ sample.sample->Channels * i ] * sample.attenuation;
|
|
|
|
if( sample.sample->Channels == 1 )
|
|
{
|
|
sampleData[i][1] = sampleData[i][0];
|
|
}
|
|
else
|
|
{
|
|
sampleData[i][1] = 1.0 / 0x10000 *
|
|
pInt[ sample.sample->Channels * i + 1 ] * sample.attenuation;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// These two loop index functions taken from SampleBuffer.cpp
|
|
f_cnt_t GigInstrument::getLoopedIndex( f_cnt_t index, f_cnt_t startf, f_cnt_t endf ) const
|
|
{
|
|
if( index < endf )
|
|
{
|
|
return index;
|
|
}
|
|
|
|
return startf + ( index - startf )
|
|
% ( endf - startf );
|
|
}
|
|
|
|
|
|
|
|
|
|
f_cnt_t GigInstrument::getPingPongIndex( f_cnt_t index, f_cnt_t startf, f_cnt_t endf ) const
|
|
{
|
|
if( index < endf )
|
|
{
|
|
return index;
|
|
}
|
|
|
|
const f_cnt_t looplen = endf - startf;
|
|
const f_cnt_t looppos = ( index - endf ) % ( looplen * 2 );
|
|
|
|
return ( looppos < looplen )
|
|
? endf - looppos
|
|
: startf + ( looppos - looplen );
|
|
}
|
|
|
|
|
|
|
|
|
|
// A key has been released
|
|
void GigInstrument::deleteNotePluginData( NotePlayHandle * _n )
|
|
{
|
|
auto pluginData = static_cast<GIGPluginData*>(_n->m_pluginData);
|
|
QMutexLocker locker( &m_notesMutex );
|
|
|
|
// Mark the note as being released, but only if it was playing or was just
|
|
// pressed (i.e., not if the key was already released)
|
|
for (auto& note : m_notes)
|
|
{
|
|
// Find the note by matching pointers to the plugin data
|
|
if (note.handle == pluginData && (note.state == GigState::KeyDown || note.state == GigState::PlayingKeyDown))
|
|
{
|
|
note.state = GigState::KeyUp;
|
|
}
|
|
}
|
|
|
|
// TODO: not sample exact? What about in the middle of us writing out the sample?
|
|
|
|
delete pluginData;
|
|
}
|
|
|
|
|
|
|
|
|
|
gui::PluginView* GigInstrument::instantiateView( QWidget * _parent )
|
|
{
|
|
return new gui::GigInstrumentView( this, _parent );
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add the desired samples (either the normal samples or the release samples)
|
|
// to the GigNote
|
|
//
|
|
// Note: not thread safe since libgig stores current region position data in
|
|
// the instrument object
|
|
void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample )
|
|
{
|
|
// Change key dimension, e.g. change samples based on what key is pressed
|
|
// in a certain range. From LinuxSampler
|
|
if( wantReleaseSample == true &&
|
|
gignote.midiNote >= m_instrument->DimensionKeyRange.low &&
|
|
gignote.midiNote <= m_instrument->DimensionKeyRange.high )
|
|
{
|
|
m_currentKeyDimension = float( gignote.midiNote -
|
|
m_instrument->DimensionKeyRange.low ) / (
|
|
m_instrument->DimensionKeyRange.high -
|
|
m_instrument->DimensionKeyRange.low + 1 );
|
|
}
|
|
|
|
gig::Region* pRegion = m_instrument->GetFirstRegion();
|
|
|
|
while( pRegion != nullptr )
|
|
{
|
|
Dimension dim = getDimensions( pRegion, gignote.velocity, wantReleaseSample );
|
|
gig::DimensionRegion * pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues );
|
|
gig::Sample * pSample = pDimRegion->pSample;
|
|
|
|
// If this is a release sample, the note won't ever be
|
|
// released, so we handle it differently
|
|
gignote.isRelease = wantReleaseSample;
|
|
|
|
// Does this note have release samples? Set this only on the original
|
|
// notes and not when we get the release samples.
|
|
if( wantReleaseSample != true )
|
|
{
|
|
gignote.release = dim.release;
|
|
}
|
|
|
|
if( pSample != nullptr && pSample->SamplesTotal != 0 )
|
|
{
|
|
int keyLow = pRegion->KeyRange.low;
|
|
int keyHigh = pRegion->KeyRange.high;
|
|
|
|
if( gignote.midiNote >= keyLow && gignote.midiNote <= keyHigh )
|
|
{
|
|
float attenuation = pDimRegion->GetVelocityAttenuation( gignote.velocity );
|
|
float length = (float) pSample->SamplesTotal / Engine::audioEngine()->outputSampleRate();
|
|
|
|
// TODO: sample panning? crossfade different layers?
|
|
|
|
if( wantReleaseSample == true )
|
|
{
|
|
// From LinuxSampler, not sure how it was created
|
|
attenuation *= 1 - 0.01053 * ( 256 >> pDimRegion->ReleaseTriggerDecay ) * length;
|
|
}
|
|
else
|
|
{
|
|
attenuation *= pDimRegion->SampleAttenuation;
|
|
}
|
|
|
|
gignote.samples.emplace_back(pSample, pDimRegion, attenuation, AudioResampler::Mode::Linear, gignote.frequency);
|
|
}
|
|
}
|
|
|
|
pRegion = m_instrument->GetNextRegion();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// Based on our input parameters, generate a "dimension" that specifies which
|
|
// note we wish to select from the GIG file with libgig. libgig will use this
|
|
// information to select the sample.
|
|
Dimension GigInstrument::getDimensions( gig::Region * pRegion, int velocity, bool release )
|
|
{
|
|
Dimension dim;
|
|
|
|
if( pRegion == nullptr )
|
|
{
|
|
return dim;
|
|
}
|
|
|
|
for( int i = pRegion->Dimensions - 1; i >= 0; --i )
|
|
{
|
|
switch( pRegion->pDimensionDefinitions[i].dimension )
|
|
{
|
|
case gig::dimension_layer:
|
|
// TODO: implement this
|
|
dim.DimValues[i] = 0;
|
|
break;
|
|
case gig::dimension_velocity:
|
|
dim.DimValues[i] = velocity;
|
|
break;
|
|
case gig::dimension_releasetrigger:
|
|
dim.release = true;
|
|
dim.DimValues[i] = (uint) release;
|
|
break;
|
|
case gig::dimension_keyboard:
|
|
dim.DimValues[i] = (uint) ( m_currentKeyDimension * pRegion->pDimensionDefinitions[i].zones );
|
|
break;
|
|
case gig::dimension_roundrobin:
|
|
case gig::dimension_roundrobinkeyboard:
|
|
// TODO: implement this
|
|
dim.DimValues[i] = 0;
|
|
break;
|
|
case gig::dimension_random:
|
|
// From LinuxSampler, untested
|
|
m_RandomSeed = m_RandomSeed * 1103515245 + 12345;
|
|
dim.DimValues[i] = uint(
|
|
m_RandomSeed / 4294967296.0f * pRegion->pDimensionDefinitions[i].bits );
|
|
break;
|
|
case gig::dimension_samplechannel:
|
|
case gig::dimension_channelaftertouch:
|
|
case gig::dimension_modwheel:
|
|
case gig::dimension_breath:
|
|
case gig::dimension_foot:
|
|
case gig::dimension_portamentotime:
|
|
case gig::dimension_effect1:
|
|
case gig::dimension_effect2:
|
|
case gig::dimension_genpurpose1:
|
|
case gig::dimension_genpurpose2:
|
|
case gig::dimension_genpurpose3:
|
|
case gig::dimension_genpurpose4:
|
|
case gig::dimension_sustainpedal:
|
|
case gig::dimension_portamento:
|
|
case gig::dimension_sostenutopedal:
|
|
case gig::dimension_softpedal:
|
|
case gig::dimension_genpurpose5:
|
|
case gig::dimension_genpurpose6:
|
|
case gig::dimension_genpurpose7:
|
|
case gig::dimension_genpurpose8:
|
|
case gig::dimension_effect1depth:
|
|
case gig::dimension_effect2depth:
|
|
case gig::dimension_effect3depth:
|
|
case gig::dimension_effect4depth:
|
|
case gig::dimension_effect5depth:
|
|
case gig::dimension_none:
|
|
default:
|
|
dim.DimValues[i] = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return dim;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the selected instrument from the GIG file we opened if we haven't gotten
|
|
// it already. This is based on the bank and patch numbers.
|
|
void GigInstrument::getInstrument()
|
|
{
|
|
// Find instrument
|
|
int iBankSelected = m_bankNum.value();
|
|
int iProgSelected = m_patchNum.value();
|
|
|
|
QMutexLocker locker( &m_synthMutex );
|
|
|
|
if( m_instance != nullptr )
|
|
{
|
|
gig::Instrument * pInstrument = m_instance->gig.GetFirstInstrument();
|
|
|
|
while( pInstrument != nullptr )
|
|
{
|
|
int iBank = pInstrument->MIDIBank;
|
|
int iProg = pInstrument->MIDIProgram;
|
|
|
|
if( iBank == iBankSelected && iProg == iProgSelected )
|
|
{
|
|
break;
|
|
}
|
|
|
|
pInstrument = m_instance->gig.GetNextInstrument();
|
|
}
|
|
|
|
m_instrument = pInstrument;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// Since the sample rate changes when we start an export, clear all the
|
|
// currently-playing notes when we get this signal. Then, the export won't
|
|
// include leftover notes that were playing in the program.
|
|
void GigInstrument::updateSampleRate()
|
|
{
|
|
QMutexLocker locker( &m_notesMutex );
|
|
m_notes.clear();
|
|
}
|
|
|
|
|
|
|
|
namespace gui
|
|
{
|
|
|
|
|
|
class gigKnob : public Knob
|
|
{
|
|
public:
|
|
gigKnob( QWidget * _parent ) :
|
|
Knob( KnobType::Bright26, _parent )
|
|
{
|
|
setFixedSize( 31, 38 );
|
|
}
|
|
} ;
|
|
|
|
|
|
|
|
|
|
GigInstrumentView::GigInstrumentView( Instrument * _instrument, QWidget * _parent ) :
|
|
InstrumentViewFixedSize( _instrument, _parent )
|
|
{
|
|
auto k = castModel<GigInstrument>();
|
|
|
|
connect( &k->m_bankNum, SIGNAL( dataChanged() ), this, SLOT( updatePatchName() ) );
|
|
connect( &k->m_patchNum, SIGNAL( dataChanged() ), this, SLOT( updatePatchName() ) );
|
|
|
|
// File Button
|
|
m_fileDialogButton = new PixmapButton( this );
|
|
m_fileDialogButton->setCursor(Qt::PointingHandCursor);
|
|
m_fileDialogButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( "fileselect_on" ) );
|
|
m_fileDialogButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "fileselect_off" ) );
|
|
m_fileDialogButton->move( 223, 68 );
|
|
|
|
connect( m_fileDialogButton, SIGNAL( clicked() ), this, SLOT( showFileDialog() ) );
|
|
|
|
m_fileDialogButton->setToolTip(tr("Open GIG file"));
|
|
|
|
// Patch Button
|
|
m_patchDialogButton = new PixmapButton( this );
|
|
m_patchDialogButton->setCursor(Qt::PointingHandCursor);
|
|
m_patchDialogButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( "patches_on" ) );
|
|
m_patchDialogButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( "patches_off" ) );
|
|
m_patchDialogButton->setEnabled( false );
|
|
m_patchDialogButton->move( 223, 94 );
|
|
|
|
connect( m_patchDialogButton, SIGNAL( clicked() ), this, SLOT( showPatchDialog() ) );
|
|
|
|
m_patchDialogButton->setToolTip(tr("Choose patch"));
|
|
|
|
// LCDs
|
|
m_bankNumLcd = new LcdSpinBox( 3, "21pink", this );
|
|
m_bankNumLcd->move( 111, 150 );
|
|
|
|
m_patchNumLcd = new LcdSpinBox( 3, "21pink", this );
|
|
m_patchNumLcd->move( 161, 150 );
|
|
|
|
// Next row
|
|
m_filenameLabel = new QLabel( this );
|
|
m_filenameLabel->setGeometry( 61, 70, 156, 14 );
|
|
m_patchLabel = new QLabel( this );
|
|
m_patchLabel->setGeometry( 61, 94, 156, 14 );
|
|
|
|
// Gain
|
|
m_gainKnob = new gigKnob( this );
|
|
m_gainKnob->setHintText( tr( "Gain:" ) + " ", "" );
|
|
m_gainKnob->move( 32, 140 );
|
|
|
|
setAutoFillBackground( true );
|
|
QPalette pal;
|
|
pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "artwork" ) );
|
|
setPalette( pal );
|
|
|
|
updateFilename();
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrumentView::modelChanged()
|
|
{
|
|
auto k = castModel<GigInstrument>();
|
|
m_bankNumLcd->setModel( &k->m_bankNum );
|
|
m_patchNumLcd->setModel( &k->m_patchNum );
|
|
|
|
m_gainKnob->setModel( &k->m_gain );
|
|
|
|
connect( k, SIGNAL( fileChanged() ), this, SLOT( updateFilename() ) );
|
|
connect( k, SIGNAL( fileLoading() ), this, SLOT( invalidateFile() ) );
|
|
|
|
updateFilename();
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrumentView::updateFilename()
|
|
{
|
|
auto i = castModel<GigInstrument>();
|
|
QFontMetrics fm( m_filenameLabel->font() );
|
|
QString file = i->m_filename.endsWith( ".gig", Qt::CaseInsensitive ) ?
|
|
i->m_filename.left( i->m_filename.length() - 4 ) :
|
|
i->m_filename;
|
|
m_filenameLabel->setText( fm.elidedText( file, Qt::ElideLeft, m_filenameLabel->width() ) );
|
|
|
|
m_patchDialogButton->setEnabled( !i->m_filename.isEmpty() );
|
|
|
|
updatePatchName();
|
|
update();
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrumentView::updatePatchName()
|
|
{
|
|
auto i = castModel<GigInstrument>();
|
|
QFontMetrics fm( font() );
|
|
QString patch = i->getCurrentPatchName();
|
|
m_patchLabel->setText( fm.elidedText( patch, Qt::ElideLeft, m_patchLabel->width() ) );
|
|
|
|
update();
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrumentView::invalidateFile()
|
|
{
|
|
m_patchDialogButton->setEnabled( false );
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrumentView::showFileDialog()
|
|
{
|
|
auto k = castModel<GigInstrument>();
|
|
|
|
FileDialog ofd( nullptr, tr( "Open GIG file" ) );
|
|
ofd.setFileMode( FileDialog::ExistingFiles );
|
|
|
|
QStringList types;
|
|
types << tr( "GIG Files (*.gig)" );
|
|
ofd.setNameFilters( types );
|
|
|
|
if( k->m_filename != "" )
|
|
{
|
|
QString f = PathUtil::toAbsolute( k->m_filename );
|
|
ofd.setDirectory( QFileInfo( f ).absolutePath() );
|
|
ofd.selectFile( QFileInfo( f ).fileName() );
|
|
}
|
|
else
|
|
{
|
|
ofd.setDirectory( ConfigManager::inst()->gigDir() );
|
|
}
|
|
|
|
m_fileDialogButton->setEnabled( false );
|
|
|
|
if( ofd.exec() == QDialog::Accepted && !ofd.selectedFiles().isEmpty() )
|
|
{
|
|
QString f = ofd.selectedFiles()[0];
|
|
|
|
if( f != "" )
|
|
{
|
|
k->openFile( f );
|
|
Engine::getSong()->setModified();
|
|
}
|
|
}
|
|
|
|
m_fileDialogButton->setEnabled( true );
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigInstrumentView::showPatchDialog()
|
|
{
|
|
auto k = castModel<GigInstrument>();
|
|
PatchesDialog pd( this );
|
|
pd.setup( k->m_instance, 1, k->instrumentTrack()->name(), &k->m_bankNum, &k->m_patchNum, m_patchLabel );
|
|
pd.exec();
|
|
}
|
|
|
|
|
|
} // namespace gui
|
|
|
|
|
|
// Store information related to playing a sample from the GIG file
|
|
GigSample::GigSample(gig::Sample* pSample, gig::DimensionRegion* pDimRegion, float attenuation,
|
|
AudioResampler::Mode interpolation, float desiredFreq)
|
|
: sample(pSample)
|
|
, region(pDimRegion)
|
|
, attenuation(attenuation)
|
|
, pos(0)
|
|
, m_resampler(interpolation)
|
|
, sampleFreq(0)
|
|
, freqFactor(1)
|
|
{
|
|
if( sample != nullptr && region != nullptr )
|
|
{
|
|
// Calculate note pitch and frequency factor only if we're actually
|
|
// going to be changing the pitch of the notes
|
|
if( region->PitchTrack == true )
|
|
{
|
|
// Calculate what frequency the provided sample is
|
|
sampleFreq = 440.0f * std::exp2((region->UnityNote - 69 - region->FineTune * 0.01) / 12.0f);
|
|
freqFactor = sampleFreq / desiredFreq;
|
|
}
|
|
|
|
// The sample rate we pass in is affected by how we are going to be
|
|
// resampling the note so that a 1.5 second release ends up being 1.5
|
|
// seconds after resampling
|
|
adsr = ADSR( region, sample->SamplesPerSecond / freqFactor );
|
|
}
|
|
}
|
|
|
|
GigSample::GigSample(const GigSample& g)
|
|
: sample(g.sample)
|
|
, region(g.region)
|
|
, attenuation(g.attenuation)
|
|
, adsr(g.adsr)
|
|
, pos(g.pos)
|
|
, m_resampler(AudioResampler::Mode::Linear, DEFAULT_CHANNELS)
|
|
, sampleFreq(g.sampleFreq)
|
|
, freqFactor(g.freqFactor)
|
|
{
|
|
}
|
|
|
|
|
|
|
|
|
|
GigSample& GigSample::operator=( const GigSample& g )
|
|
{
|
|
sample = g.sample;
|
|
region= g.region;
|
|
attenuation = g.attenuation;
|
|
adsr = g.adsr;
|
|
pos = g.pos;
|
|
sampleFreq = g.sampleFreq;
|
|
freqFactor = g.freqFactor;
|
|
return *this;
|
|
}
|
|
|
|
ADSR::ADSR()
|
|
: preattack( 0 ), attack( 0 ), decay1( 0 ), decay2( 0 ), infiniteSustain( false ),
|
|
sustain( 0 ), release( 0 ),
|
|
amplitude( 0 ), isAttack( true ), isRelease( false ), isDone( false ),
|
|
attackPosition( 0 ), attackLength( 0 ), decayLength( 0 ),
|
|
releasePosition( 0 ), releaseLength( 0 )
|
|
{
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the ADSR envelope from the settings in the GIG file
|
|
ADSR::ADSR( gig::DimensionRegion * region, int sampleRate )
|
|
: preattack( 0 ), attack( 0 ), decay1( 0 ), decay2( 0 ), infiniteSustain( false ),
|
|
sustain( 0 ), release( 0 ),
|
|
amplitude( 0 ), isAttack( true ), isRelease( false ), isDone( false ),
|
|
attackPosition( 0 ), attackLength( 0 ), decayLength( 0 ),
|
|
releasePosition( 0 ), releaseLength( 0 )
|
|
{
|
|
if( region != nullptr )
|
|
{
|
|
// Parameters from GIG file
|
|
preattack = 1.0 * region->EG1PreAttack / 1000; // EG1PreAttack is 0-1000 permille
|
|
attack = region->EG1Attack;
|
|
decay1 = region->EG1Decay1;
|
|
decay2 = region->EG1Decay2;
|
|
infiniteSustain = region->EG1InfiniteSustain;
|
|
sustain = 1.0 * region->EG1Sustain / 1000; // EG1Sustain is 0-1000 permille
|
|
release = region->EG1Release;
|
|
|
|
// Simple ADSR using positions in sample
|
|
amplitude = preattack;
|
|
attackLength = attack * sampleRate;
|
|
decayLength = decay1 * sampleRate; // TODO: ignoring decay2 for now
|
|
releaseLength = release * sampleRate;
|
|
|
|
// If there is no attack or decay, start at the sustain amplitude
|
|
if( attackLength == 0 && decayLength == 0 )
|
|
{
|
|
amplitude = sustain;
|
|
}
|
|
// If there is no attack, start at the full amplitude
|
|
else if( attackLength == 0 )
|
|
{
|
|
amplitude = 1.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// Next time we get the amplitude, we'll be releasing the note
|
|
void ADSR::keyup()
|
|
{
|
|
isRelease = true;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Can we delete the sample now?
|
|
bool ADSR::done()
|
|
{
|
|
return isDone;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return the current amplitude and increment internal positions
|
|
float ADSR::value()
|
|
{
|
|
float currentAmplitude = amplitude;
|
|
|
|
// If we're done, don't output any signal
|
|
if( isDone == true )
|
|
{
|
|
return 0;
|
|
}
|
|
// If we're still in the attack phase, release from the current volume
|
|
// instead of jumping to the sustain volume and fading out
|
|
else if( isAttack == true && isRelease == true )
|
|
{
|
|
sustain = amplitude;
|
|
isAttack = false;
|
|
}
|
|
|
|
// If we're in the attack phase, start at the preattack amplitude and
|
|
// increase to the full before decreasing to sustain
|
|
if( isAttack == true )
|
|
{
|
|
if( attackPosition < attackLength )
|
|
{
|
|
amplitude = preattack + ( 1.0 - preattack ) / attackLength * attackPosition;
|
|
}
|
|
else if( attackPosition < attackLength + decayLength )
|
|
{
|
|
amplitude = 1.0 - ( 1.0 - sustain ) / decayLength * ( attackPosition - attackLength );
|
|
}
|
|
else
|
|
{
|
|
isAttack = false;
|
|
}
|
|
|
|
++attackPosition;
|
|
}
|
|
// If we're in the sustain phase, decrease from sustain to zero
|
|
else if( isRelease == true )
|
|
{
|
|
// Maybe not the best way of doing this, but it appears to be about right
|
|
// Satisfies f(0) = sustain and f(releaseLength) = very small
|
|
amplitude = (sustain + 1e-3) * std::exp(-5.0f / releaseLength * releasePosition) - 1e-3;
|
|
|
|
// Don't have an infinite exponential decay
|
|
if( amplitude <= 0 || releasePosition >= releaseLength )
|
|
{
|
|
amplitude = 0;
|
|
isDone = true;
|
|
}
|
|
|
|
++releasePosition;
|
|
}
|
|
|
|
return currentAmplitude;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Increment internal positions a certain number of times
|
|
void ADSR::inc( f_cnt_t num )
|
|
{
|
|
for( f_cnt_t i = 0; i < num; ++i )
|
|
{
|
|
value();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
extern "C"
|
|
{
|
|
|
|
// necessary for getting instance out of shared lib
|
|
PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *m, void * )
|
|
{
|
|
return new GigInstrument( static_cast<InstrumentTrack *>( m ) );
|
|
}
|
|
|
|
}
|
|
|
|
|
|
} // namespace lmms
|