mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-13 11:38:24 -04:00
* Add refactored SampleBuffer * Add Sample * Add SampleLoader * Integrate changes into AudioSampleRecorder * Integrate changes into Oscillator * Integrate changes into SampleClip/SamplePlayHandle * Integrate changes into Graph * Remove SampleBuffer include from SampleClipView * Integrate changes into Patman * Reduce indirection to sample buffer from Sample * Integrate changes into AudioFileProcessor * Remove old SampleBuffer * Include memory header in TripleOscillator * Include memory header in Oscillator * Use atomic_load within SampleClip::sample * Include memory header in EnvelopeAndLfoParameters * Use std::atomic_load for most calls to Oscillator::userWaveSample * Revert accidental change on SamplePlayHandle L.111 * Check if audio file is empty before loading * Add asserts to Sample * Add cassert include within Sample * Adjust assert expressions in Sample * Remove use of shared ownership for Sample Sample does not need to be wrapped around a std::shared_ptr. This was to work with the audio thread, but the audio thread can instead have their own Sample separate from the UI's Sample, so changes to the UI's Sample would not leave the audio worker thread using freed data if it had pointed to it. * Use ArrayVector in Sample * Enforce std::atomic_load for users of std::shared_ptr<const SampleBuffer> * Use requestChangesGuard in ClipView::remove Fixes data race when deleting SampleClip * Revert only formatting changes * Update ClipView::remove comment * Revert "Remove use of shared ownership for Sample" This reverts commit1d452331d1. In some cases, you can infact do away with shared ownership on Sample if there are no writes being made to either of them, but to make sure changes are reflected to the object in cases where writes do happen, they should work with the same one. * Fix heap-use-after-free in Track::loadSettings * Remove m_buffer asserts * Refactor play functionality (again) The responsibility of resampling the buffer and moving the frame index is now in Sample::play, allowing the removal of both playSampleRangeLoop and playSampleRangePingPong. * Change copyright * Cast processingSampleRate to float Fixes division by zero error * Update include/SampleLoader.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Update include/SampleLoader.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Format SampleLoader.h * Remove SampleBuffer.h include in SampleRecordHandle.h * Update src/core/Oscillator.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Use typeInfo<float> for float equality comparison Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Use std::min in Sample::visualize Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Move in result to m_data * Use if block in playSampleRange * Pass in unique_ptr to SampleClip::setSampleBuffer * Return const QString& from SampleBuffer::audioFile * Do not pass in unique_ptr by r-value reference * Use isEmpty() within SampleClipView::updateSample * Remove use of atomic_store and atomic_load * Remove ArrayVector comment * Use array specialization for unique_ptr when managing DrumSynth data Also made it so that we don't create result before checking if we failed to decode the file, potentially saving us an allocation. * Don't manually delete Clip if it has a Track * Clean up generateAntiAliasUserWaveTable function Also, make it so that we actually call this function when necessary in TripleOscillator. * Set user wave, even when value is empty If the value or file is empty, I think showing a error popup here is ideal. * Remove whitespace in EnvelopeAndLfoParameters.cpp L#121 * Fix error inc5f7ccba49We still have to delete the Clip's, or else we would just be eating up memory. But we should first make sure that the Track's no longer see this Clip in their m_clips vector. This has to happen as it's own operation because we have to wait for the audio thread(s) first. This would ensure that Track's do not create PlayHandle's that would refer to a Clip that is currently being destroyed. After that, then we call deleteLater on the Clip. * Convert std::shared_ptr<Sample> to Sample This conversion does not apply to Patman as there seems to be issues with it causing heap-use-after-free issues, such as with PatmanInstrument::unloadCurrentPatch * Fix segfault when closing LMMS Song should be deleted before AudioEngine. * Construct buffer through SampleLoader in FileBrowser's previewFileItem function + Remove const qualification in SamplePlayHandle(const QString&) constructor for m_sample * Move guard out of removeClip and deleteClips + Revert commit1769ed517dsince this would fix it anyway (we don't try to lock the engine to delete the global automation track when closing LMMS now) * Simplify the switch in play function for loopMode * Add SampleDecoder * Add LMMS_HAVE_OGGVORBIS comment * Fix unused variable error * Include unordered_map * Simplify SampleDecoder Instead of using the extension (which could be wrong) for the file, we simply loop through all the decoders available. First sndfile because it covers a lot of formats, then the ogg decoder for the few cases where sndfile would not work for certain audio codecs, and then the DrumSynth decoder. * Attempt to fix Mac builds * Attempt to fix Mac builds take 2 * Add vector include to SampleDecoder * Add TODO comment about shared ownership with clips Calls to ClipView::remove may occur at any point, which can cause a problem when the Track is using the clip about to be removed. A suitable solution would be to use shared ownership between the Track and ClipView for the clip. Track's can then simply remove the shared pointer in their m_clips vector, and ClipView can call reset on the shared pointer on calls to ClipView::remove. * Adjust TODO comment Disregard the shared ownership idea. Since we would be modifying the collection of Clip's in Track when removing the Clip, the Track could be iterating said collection while this happens, causing a bug. In this case, we do actually want a synchronization mechanism. However, I didn't mention another separate issue in the TODO comment that should've been addressed: ~Clip should not be responsible for actually removing the itself from it's Track. With calls to removeClip, one would expect that to already occur. * Remove Sample::playbackSize Inside SampleClip::sampleLength, we should be using Sample::sampleSize instead. * Fix issues involving length of Sample's SampleClip::sampleLength should be passing the Sample's sample rate to Engine::framesPerTick. I also changed sampleDuration to return a std::chrono::milliseconds instead of an int so that the callers know what time interval is being used. * Simplify if condition in src/gui/FileBrowser.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Simplify if condition in src/core/SampleBuffer.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Update style in include/Oscillator.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Format src/core/SampleDecoder.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Set the sample rate to be that of the AudioEngine by default I also removed some checks involving the state of the SampleBuffer. These functions should expect a valid SampleBuffer each time. This helps to simplify things since we don't have to validate it in each function. * Set single-argument constructors in Sample and SampleBuffer to be explicit * Do not make a copy when reading result from the decoder * Add constructor to pass in vector of sampleFrame's directly * Do a pass by value and move in SampleBuffer.cpp Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Pass vector by value in SampleBuffer.h Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Make Sample(std::shared_ptr) constructor explicit * Properly draw sample waveform when reversed * Collect sample not found errors when loading project Also return empty buffers when trying to load either an empty file or empty Base64 string * Use std::make_unique<SampleBuffer> in SampleLoader * Fix loop modes * Limit sample duration to [start, end] and not the entire buffer * Use structured binding to access buffer * Check if GUI exists before displaying error * Make Base64 constructor pass in the string instead * Remove use of QByteArray::fromBase64Encoding * Inline simple functions in SampleBuffer * Dynamically include supported audio file types * Remove redundant inline specifier Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Translate file types * Cache calls to SampleDecoder::supportedAudioTypes * Fix translations in SampleLoader (again) Also ensure that all the file types are listed first. Also simplified the generation of the list a bit. * Store static local variable for supported audio types instead of in the header Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Clamp frame index depending on loop mode * Inline member functions of PlaybackState * Do not collect errors in SampleLoader when loading projects Also fix conflicts with surrounding codebase * Default construct shared pointers to SampleBuffer * Simplify and optimize Sample::visulaize() * Remove redundant gui:: prefix * Rearrange Sample::visualize after optimizations by DanielKauss * Apply amplification when visualizing sample waveforms * Set default min and max values to 1 and -1 * Treat waveform as mono signal when visualizing * Ensure visualization works when framesPerPixel < 1 * Simplify Sample::visualize a bit more * Fix CPU lag in Sample by using atomics (with relaxed ordering) Changing any of the frame markers originally took a writer lock on a mutex. The problem is that Sample::play took a reader lock first before executing. Because Sample::play has to wait on the writer, this created a lot of lag and raised the CPU meter. The solution would to be to use atomics instead. * Fix errors from merge * Fix broken LFO controller functionality The shared_ptr should have been taken by reference. * Remove TODO * Update EnvelopeAndLfoView.cpp Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Update src/gui/clips/SampleClipView.cpp Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Update plugins/SlicerT/SlicerT.cpp Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Update plugins/SlicerT/SlicerT.cpp Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Store shortest relative path in SampleBuffer * Tie up a few loose ends * Use sample_rate_t when storing sample rate in SampleBuffer * Add missing named requirement functions and aliases * Use sampledata attribute when loading from Base64 in AFP * Remove initializer for m_userWave in the constructor * Do not use trailing return syntax when return is void * Move decoder functionality into unnamed namespace * Remove redundant gui:: prefix * Use PathUtil::toAbsolute to simplify code in SampleLoader::openAudioFile * Fix translations in SampleLoader::openAudioFile Co-authored-by: DomClark <mrdomclark@gmail.com> * Fix formatting for ternary operator * Remove redundant inlines * Resolve UB when decoding from Base64 data in SampleBuffer * Fix up SampleClip constructors * Add AudioResampler, a wrapper class around libsamplerate The wrapper has only been applied to Sample::PlaybackState for now. AudioResampler should be used by other classes in the future that do resampling with libsamplerate. * Move buffer when moving and simplify assignment functions in Sample * Move Sample::visualize out of Sample and into the GUI namespace * Initialize supportedAudioTypes in static lambda * Return shared pointer from SampleLoader * Create and use static empty SampleBuffer by default * Fix header guard in SampleWaveform.h * Remove use of src_clone CI seems to have an old version of libsamplerate and does not have this method. * Include memory header in SampleBuffer.h * Remove mutex and shared_mutex includes in Sample.h * Attempt to fix string operand error within AudioResampler * Include string header in AudioResampler.cpp * Add LMMS_EXPORT for SampleWaveform class declaration * Add LMMS_EXPORT for AudioResampler class declaration * Enforce returning std::shared_ptr<const SampleBuffer> * Restrict the size of the memcpy to the destination size, not the source size * Do not make resample const AudioResampler::resample, while seemingly not changing the data of the resampler, still alters its internal state and therefore should not be const. This is because libsamplerate manages state when resampling. * Initialize data.end_of_input * Add trailing new lines * Simplify AudioResampler interface * Fix header guard prefix to LMMS_GUI instead of LMMS * Remove Sample::resampleSampleRange --------- Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: Daniel Kauss <daniel.kauss.serna@gmail.com> Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> Co-authored-by: DomClark <mrdomclark@gmail.com>
1388 lines
33 KiB
C++
1388 lines
33 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 "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 ),
|
|
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_interpolation( SRC_LINEAR ),
|
|
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 int rate = Engine::audioEngine()->processingSampleRate();
|
|
|
|
// 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( QList<GigSample>::iterator 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; }
|
|
|
|
// Will change if resampling
|
|
bool resample = false;
|
|
f_cnt_t samples = frames; // How many to grab
|
|
f_cnt_t used = frames; // How many we used
|
|
float freq_factor = 1.0; // How 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)
|
|
{
|
|
resample = true;
|
|
|
|
// 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; }
|
|
|
|
// We need a bit of margin so we don't get glitching
|
|
samples = frames / freq_factor + Sample::s_interpolationMargins[m_interpolation];
|
|
}
|
|
|
|
// Load this note's data
|
|
sampleFrame sampleData[samples];
|
|
loadSample(sample, sampleData, samples);
|
|
|
|
// 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;
|
|
|
|
for( f_cnt_t i = 0; i < samples; ++i )
|
|
{
|
|
float amplitude = copy.value();
|
|
sampleData[i][0] *= amplitude;
|
|
sampleData[i][1] *= amplitude;
|
|
}
|
|
|
|
// Output the data resampling if needed
|
|
if( resample == true )
|
|
{
|
|
sampleFrame convertBuf[frames];
|
|
|
|
// Only output if resampling is successful (note that "used" is output)
|
|
if (sample.convertSampleRate(*sampleData, *convertBuf, samples, frames, freq_factor, used))
|
|
{
|
|
for( f_cnt_t i = 0; i < frames; ++i )
|
|
{
|
|
_working_buffer[i][0] += convertBuf[i][0];
|
|
_working_buffer[i][1] += convertBuf[i][1];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for( f_cnt_t i = 0; i < frames; ++i )
|
|
{
|
|
_working_buffer[i][0] += sampleData[i][0];
|
|
_working_buffer[i][1] += sampleData[i][1];
|
|
}
|
|
}
|
|
|
|
// Update note position with how many samples we actually used
|
|
sample.pos += used;
|
|
sample.adsr.inc(used);
|
|
}
|
|
}
|
|
|
|
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()->processingSampleRate();
|
|
|
|
// 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.push_back( GigSample( pSample, pDimRegion,
|
|
attenuation, m_interpolation, 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( QCursor( 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( QCursor( 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, int interpolation, float desiredFreq )
|
|
: sample( pSample ), region( pDimRegion ), attenuation( attenuation ),
|
|
pos( 0 ), interpolation( interpolation ), srcState( nullptr ),
|
|
sampleFreq( 0 ), freqFactor( 1 )
|
|
{
|
|
if( sample != nullptr && region != nullptr )
|
|
{
|
|
// Note: we don't create the libsamplerate object here since we always
|
|
// also call the copy constructor when appending to the end of the
|
|
// QList. We'll create it only in the copy constructor so we only have
|
|
// to create it once.
|
|
|
|
// 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.0 * powf( 2, 1.0 / 12 * (
|
|
1.0 * region->UnityNote - 69 -
|
|
0.01 * region->FineTune ) );
|
|
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()
|
|
{
|
|
if( srcState != nullptr )
|
|
{
|
|
src_delete( srcState );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
GigSample::GigSample( const GigSample& g )
|
|
: sample( g.sample ), region( g.region ), attenuation( g.attenuation ),
|
|
adsr( g.adsr ), pos( g.pos ), interpolation( g.interpolation ),
|
|
srcState( nullptr ), sampleFreq( g.sampleFreq ), freqFactor( g.freqFactor )
|
|
{
|
|
// On the copy, we want to create the object
|
|
updateSampleRate();
|
|
}
|
|
|
|
|
|
|
|
|
|
GigSample& GigSample::operator=( const GigSample& g )
|
|
{
|
|
sample = g.sample;
|
|
region= g.region;
|
|
attenuation = g.attenuation;
|
|
adsr = g.adsr;
|
|
pos = g.pos;
|
|
interpolation = g.interpolation;
|
|
srcState = nullptr;
|
|
sampleFreq = g.sampleFreq;
|
|
freqFactor = g.freqFactor;
|
|
|
|
if( g.srcState != nullptr )
|
|
{
|
|
updateSampleRate();
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
|
|
|
|
|
|
void GigSample::updateSampleRate()
|
|
{
|
|
if( srcState != nullptr )
|
|
{
|
|
src_delete( srcState );
|
|
}
|
|
|
|
int error = 0;
|
|
srcState = src_new( interpolation, DEFAULT_CHANNELS, &error );
|
|
|
|
if( srcState == nullptr || error != 0 )
|
|
{
|
|
qCritical( "error while creating libsamplerate data structure in GigSample" );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GigSample::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf,
|
|
f_cnt_t oldSize, f_cnt_t newSize, float freq_factor, f_cnt_t& used )
|
|
{
|
|
if( srcState == nullptr )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SRC_DATA src_data;
|
|
src_data.data_in = &oldBuf[0];
|
|
src_data.data_out = &newBuf[0];
|
|
src_data.input_frames = oldSize;
|
|
src_data.output_frames = newSize;
|
|
src_data.src_ratio = freq_factor;
|
|
src_data.end_of_input = 0;
|
|
|
|
// We don't need to lock this assuming that we're only outputting the
|
|
// samples in one thread
|
|
int error = src_process( srcState, &src_data );
|
|
|
|
used = src_data.input_frames_used;
|
|
|
|
if( error != 0 )
|
|
{
|
|
qCritical( "GigInstrument: error while resampling: %s", src_strerror( error ) );
|
|
return false;
|
|
}
|
|
|
|
if( oldSize != 0 && src_data.output_frames_gen == 0 )
|
|
{
|
|
qCritical( "GigInstrument: could not resample, no frames generated" );
|
|
return false;
|
|
}
|
|
|
|
if( src_data.output_frames_gen > 0 && src_data.output_frames_gen < newSize )
|
|
{
|
|
qCritical() << "GigInstrument: not enough frames, wanted"
|
|
<< newSize << "generated" << src_data.output_frames_gen;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
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 ) * expf( -5.0 / 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
|