diff --git a/.travis.yml b/.travis.yml index 4dd7576f8..6d79557ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: - if [ $TARGET_OS != linux ]; then sudo apt-get install -y nsis cloog-isl libmpc2 mingw32; fi - if [ $TARGET_OS != linux ]; then sudo apt-get install -y mingw32-x-qt mingw32-x-sdl mingw32-x-libvorbis mingw32-x-fluidsynth mingw32-x-stk mingw32-x-glib2 mingw32-x-portaudio mingw32-x-libsndfile mingw32-x-fftw mingw32-x-flac mingw32-x-fltk mingw32-x-libsamplerate mingw32-x-pkgconfig mingw32-x-binutils mingw32-x-gcc mingw32-x-runtime; fi - if [ $TARGET_OS == win64 ]; then sudo apt-get install -y mingw64-x-qt mingw64-x-sdl mingw64-x-libvorbis mingw64-x-fluidsynth mingw64-x-stk mingw64-x-glib2 mingw64-x-portaudio mingw64-x-libsndfile mingw64-x-fftw mingw64-x-flac mingw64-x-fltk mingw64-x-libsamplerate mingw64-x-pkgconfig mingw64-x-binutils mingw64-x-gcc mingw64-x-runtime; fi - - if [ $TARGET_OS == linux ]; then sudo apt-get install -y libqt4-dev libsndfile-dev fftw3-dev libvorbis-dev libogg-dev libasound2-dev libjack-dev libsdl-dev libsamplerate0-dev libstk0-dev libfluidsynth-dev portaudio19-dev wine-dev g++-multilib libfltk1.3-dev; fi + - if [ $TARGET_OS == linux ]; then sudo apt-get install -y libqt4-dev libsndfile-dev fftw3-dev libvorbis-dev libogg-dev libasound2-dev libjack-dev libsdl-dev libsamplerate0-dev libstk0-dev libfluidsynth-dev portaudio19-dev wine-dev g++-multilib libfltk1.3-dev libgig-dev; fi before_script: - mkdir build && cd build script: diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b4011cbd..1a6c9b975 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ OPTION(WANT_PULSEAUDIO "Include PulseAudio support" ON) OPTION(WANT_PORTAUDIO "Include PortAudio support" ON) OPTION(WANT_SDL "Include SDL (Simple DirectMedia Layer) support" ON) OPTION(WANT_SF2 "Include SoundFont2 player plugin" ON) +OPTION(WANT_GIG "Include GIG player plugin" ON) OPTION(WANT_STK "Include Stk (Synthesis Toolkit) support" ON) OPTION(WANT_SWH "Include Steve Harris's LADSPA plugins" ON) OPTION(WANT_TAP "Include Tom's Audio Processing LADSPA plugins" ON) @@ -315,6 +316,17 @@ IF(WANT_SF2) ENDIF(FLUIDSYNTH_FOUND) ENDIF(WANT_SF2) +# check for libgig +If(WANT_GIG) + PKG_CHECK_MODULES(GIG gig) + IF(GIG_FOUND) + SET(LMMS_HAVE_GIG TRUE) + SET(STATUS_GIG "OK") + ELSE(GIG_FOUND) + SET(STATUS_GIG "not found, libgig needed for decoding .gig files") + ENDIF(GIG_FOUND) +ENDIF(WANT_GIG) + # check for pthreads IF(LMMS_BUILD_LINUX OR LMMS_BUILD_APPLE) FIND_PACKAGE(Threads) @@ -652,6 +664,7 @@ MESSAGE( "* CMT LADSPA plugins : ${STATUS_CMT}\n" "* TAP LADSPA plugins : ${STATUS_TAP}\n" "* SWH LADSPA plugins : ${STATUS_SWH}\n" +"* GIG player : ${STATUS_GIG}\n" ) MESSAGE( diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 1ba586768..4f4a668e7 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -26,6 +26,7 @@ ADD_SUBDIRECTORY(peak_controller_effect) IF(NOT LMMS_BUILD_APPLE) ADD_SUBDIRECTORY(sf2_player) ENDIF() +ADD_SUBDIRECTORY(GigPlayer) ADD_SUBDIRECTORY(sfxr) ADD_SUBDIRECTORY(sid) ADD_SUBDIRECTORY(SpectrumAnalyzer) diff --git a/plugins/GigPlayer/CMakeLists.txt b/plugins/GigPlayer/CMakeLists.txt new file mode 100644 index 000000000..4be4dff82 --- /dev/null +++ b/plugins/GigPlayer/CMakeLists.txt @@ -0,0 +1,13 @@ +if(LMMS_HAVE_GIG) + INCLUDE(BuildPlugin) + INCLUDE_DIRECTORIES(${GIG_INCLUDE_DIRS}) + + # Required for not crashing loading files with libgig + SET(GCC_COVERAGE_COMPILE_FLAGS "-fexceptions") + add_definitions(${GCC_COVERAGE_COMPILE_FLAGS}) + + LINK_DIRECTORIES(${GIG_LIBRARY_DIRS}) + LINK_LIBRARIES(${GIG_LIBRARIES}) + BUILD_PLUGIN(gigplayer GigPlayer.cpp GigPlayer.h PatchesDialog.cpp PatchesDialog.h PatchesDialog.ui MOCFILES GigPlayer.h PatchesDialog.h UICFILES PatchesDialog.ui EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") +endif(LMMS_HAVE_GIG) + diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp new file mode 100644 index 000000000..34e404420 --- /dev/null +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -0,0 +1,1417 @@ +/* + * GigPlayer.cpp - a GIG player using libgig (based on Sf2 player plugin) + * + * Copyright (c) 2008 Paul Giblock + * Copyright (c) 2009-2014 Tobias Doerffel + * + * 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 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 +#include +#include +#include + +#include "FileDialog.h" +#include "GigPlayer.h" +#include "engine.h" +#include "InstrumentTrack.h" +#include "InstrumentPlayHandle.h" +#include "NotePlayHandle.h" +#include "knob.h" +#include "song.h" +#include "ConfigManager.h" +#include "endian_handling.h" + +#include "PatchesDialog.h" +#include "tooltip.h" +#include "LcdSpinBox.h" + +#include "embed.cpp" + + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor = +{ + STRINGIFY( PLUGIN_NAME ), + "GIG Player", + QT_TRANSLATE_NOOP( "pluginBrowser", "Player for GIG files" ), + "Garrett Wilson ", + 0x0100, + Plugin::Instrument, + new PluginPixmapLoader( "logo" ), + "gig", + NULL +} ; + +} + + + + +GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : + Instrument( _instrument_track, &gigplayer_plugin_descriptor ), + m_instance( NULL ), + m_instrument( NULL ), + 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 ) +{ + InstrumentPlayHandle * iph = new InstrumentPlayHandle( this, _instrument_track ); + engine::mixer()->addPlayHandle( iph ); + + updateSampleRate(); + + connect( &m_bankNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) ); + connect( &m_patchNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) ); + connect( engine::mixer(), SIGNAL( sampleRateChanged() ), this, SLOT( updateSampleRate() ) ); +} + + + + +GigInstrument::~GigInstrument() +{ + engine::mixer()->removePlayHandles( instrumentTrack() ); + 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 NULL; +} + + + + +QString GigInstrument::nodeName() const +{ + return gigplayer_plugin_descriptor.name; +} + + + + +void GigInstrument::freeInstance() +{ + QMutexLocker synthLock( &m_synthMutex ); + QMutexLocker notesLock( &m_notesMutex ); + + if( m_instance != NULL ) + { + delete m_instance; + m_instance = NULL; + + // 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 = NULL; + 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( _gigFile ); + m_filename = SampleBuffer::tryToMakeRelative( _gigFile ); + } + catch( ... ) + { + m_instance = NULL; + m_filename = ""; + } + } + + emit fileChanged(); + + if( updateTrackName == true ) + { + instrumentTrack()->setName( QFileInfo( _gigFile ).baseName() ); + updatePatch(); + } +} + + + + +void GigInstrument::updatePatch() +{ + if( m_bankNum.value() >= 0 && m_patchNum.value() >= 0 ) + { + getInstrument(); + } +} + + + + +QString GigInstrument::getCurrentPatchName() +{ + QMutexLocker locker( &m_synthMutex ); + + if( m_instance == NULL ) + { + return ""; + } + + int iBankSelected = m_bankNum.value(); + int iProgSelected = m_patchNum.value(); + + gig::Instrument * pInstrument = m_instance->gig.GetFirstInstrument(); + + while( pInstrument != NULL ) + { + int iBank = pInstrument->MIDIBank; + int iProg = pInstrument->MIDIProgram; + + if( iBank == iBankSelected && iProg == iProgSelected ) + { + QString name = QString::fromStdString( pInstrument->pInfo->Name ); + + if( name == "" ) + { + 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; + + const f_cnt_t tfp = _n->totalFramesPlayed(); + + int midiNote = (int) floor( 12.0 * ( log2( _n->unpitchedFrequency() ) - LOG440 ) - 4.0 ); + + // out of range? + if( midiNote <= 0 || midiNote >= 128 ) + { + return; + } + + if( tfp == 0 ) + { + GIGPluginData * 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::mixer()->framesPerPeriod(); + const int rate = engine::mixer()->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 == NULL || m_instrument == NULL ) + { + m_synthMutex.unlock(); + m_notesMutex.unlock(); + return; + } + + for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) + { + // Process notes in the KeyUp state, adding release samples if desired + if( it->state == KeyUp ) + { + // If there are no samples, we're done + if( it->samples.empty() ) + { + it->state = Completed; + } + else + { + it->state = PlayingKeyUp; + + // Notify each sample that the key has been released + for( QList::iterator sample = it->samples.begin(); + sample != it->samples.end(); ++sample ) + { + 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 == KeyDown ) + { + it->state = PlayingKeyDown; + addSamples( *it, false ); + } + + // Delete ended samples + for( QList::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 == NULL || 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 == Completed || it->samples.empty() ) + { + it = m_notes.erase( it ); + + if( it == m_notes.end() ) + { + break; + } + } + } + + // Fill buffer with portions of the note samples + for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) + { + // Only process the notes if we're in a playing state + if( !( it->state == PlayingKeyDown || + it->state == PlayingKeyUp ) ) + { + continue; + } + + for( QList::iterator sample = it->samples.begin(); + sample != it->samples.end(); ++sample ) + { + if( sample->sample == NULL || sample->region == NULL ) + { + 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 + MARGIN[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(); + } + + instrumentTrack()->processAudioBuffer( _working_buffer, frames, NULL ); +} + + + + +void GigInstrument::loadSample( GigSample& sample, sampleFrame* sampleData, f_cnt_t samples ) +{ + if( sampleData == NULL || 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 != NULL ) + { + for( uint32_t i = 0; i < sample.region->SampleLoops; ++i ) + { + loop = true; + loopType = static_cast( 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], + 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 + { + uint8_t * pInt = reinterpret_cast( &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 + { + int16_t * pInt = reinterpret_cast( &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 ) +{ + GIGPluginData * pluginData = static_cast( _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( QList::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) + { + // Find the note by matching pointers to the plugin data + if( i->handle == pluginData && + ( i->state == KeyDown || i->state == PlayingKeyDown ) ) + { + i->state = KeyUp; + } + } + + // TODO: not sample exact? What about in the middle of us writing out the sample? + + delete pluginData; +} + + + + +PluginView * GigInstrument::instantiateView( QWidget * _parent ) +{ + return new 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 != NULL ) + { + 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 != NULL && 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::mixer()->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 == NULL ) + { + 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 != NULL ) + { + gig::Instrument * pInstrument = m_instance->gig.GetFirstInstrument(); + + while( pInstrument != NULL ) + { + 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(); +} + + + + +class gigKnob : public knob +{ +public: + gigKnob( QWidget * _parent ) : + knob( knobBright_26, _parent ) + { + setFixedSize( 31, 38 ); + } +} ; + + + + +GigInstrumentView::GigInstrumentView( Instrument * _instrument, QWidget * _parent ) : + InstrumentView( _instrument, _parent ) +{ + GigInstrument * k = castModel(); + + 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() ) ); + + toolTip::add( m_fileDialogButton, tr( "Open other GIG file" ) ); + + m_fileDialogButton->setWhatsThis( tr( "Click here to open another 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() ) ); + + toolTip::add( m_patchDialogButton, tr( "Choose the patch" ) ); + + m_patchDialogButton->setWhatsThis( tr( "Click here to change which patch of the GIG file to use" ) ); + + // 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 ); + + m_bankNumLcd->setWhatsThis( tr( "Change which instrument of the GIG file is being played" ) ); + m_patchNumLcd->setWhatsThis( tr( "Change which instrument of the GIG file is being played" ) ); + + // 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 ); + + m_filenameLabel->setWhatsThis( tr( "Which GIG file is currently being used" ) ); + m_patchLabel->setWhatsThis( tr( "Which patch of the GIG file is currently being used" ) ); + + // Gain + m_gainKnob = new gigKnob( this ); + m_gainKnob->setHintText( tr( "Gain" ) + " ", "" ); + m_gainKnob->move( 32, 140 ); + m_gainKnob->setWhatsThis( tr( "Factor to multiply samples by" ) ); + + setAutoFillBackground( true ); + QPalette pal; + pal.setBrush( backgroundRole(), PLUGIN_NAME::getIconPixmap( "artwork" ) ); + setPalette( pal ); + + updateFilename(); +} + + + + +GigInstrumentView::~GigInstrumentView() +{ +} + + + + +void GigInstrumentView::modelChanged() +{ + GigInstrument * k = castModel(); + 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() +{ + GigInstrument * i = castModel(); + 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() +{ + GigInstrument * i = castModel(); + 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() +{ + GigInstrument * k = castModel(); + + FileDialog ofd( NULL, tr( "Open GIG file" ) ); + ofd.setFileMode( FileDialog::ExistingFiles ); + + QStringList types; + types << tr( "GIG Files (*.gig)" ); + ofd.setFilters( types ); + + QString dir; + if( k->m_filename != "" ) + { + QString f = k->m_filename; + + if( QFileInfo( f ).isRelative() ) + { + f = ConfigManager::inst()->userSamplesDir() + f; + + if( QFileInfo( f ).exists() == false ) + { + f = ConfigManager::inst()->factorySamplesDir() + k->m_filename; + } + } + + ofd.setDirectory( QFileInfo( f ).absolutePath() ); + ofd.selectFile( QFileInfo( f ).fileName() ); + } + else + { + ofd.setDirectory( ConfigManager::inst()->userSamplesDir() ); + } + + 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() +{ + GigInstrument * k = castModel(); + PatchesDialog pd( this ); + pd.setup( k->m_instance, 1, k->instrumentTrack()->name(), &k->m_bankNum, &k->m_patchNum, m_patchLabel ); + pd.exec(); +} + + + + +// 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( NULL ), + sampleFreq( 0 ), freqFactor( 1 ) +{ + if( sample != NULL && region != NULL ) + { + // 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 != NULL ) + { + 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( NULL ), 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 = NULL; + sampleFreq = g.sampleFreq; + freqFactor = g.freqFactor; + + if( g.srcState != NULL ) + { + updateSampleRate(); + } + + return *this; +} + + + + +void GigSample::updateSampleRate() +{ + if( srcState != NULL ) + { + src_delete( srcState ); + } + + int error = 0; + srcState = src_new( interpolation, DEFAULT_CHANNELS, &error ); + + if( srcState == NULL || 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 == NULL ) + { + 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 != NULL ) + { + // 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 * PLUGIN_EXPORT lmms_plugin_main( Model *, void * _data ) +{ + return new GigInstrument( static_cast( _data ) ); +} + +} diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h new file mode 100644 index 000000000..f302df622 --- /dev/null +++ b/plugins/GigPlayer/GigPlayer.h @@ -0,0 +1,370 @@ +/* + * GigPlayer.h - a GIG player using libgig (based on Sf2 player plugin) + * + * Copyright (c) 2008 Paul Giblock + * Copyright (c) 2009-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 GIG_PLAYER_H +#define GIG_PLAYER_H + +#include +#include +#include + +#include "Instrument.h" +#include "pixmap_button.h" +#include "InstrumentView.h" +#include "knob.h" +#include "LcdSpinBox.h" +#include "led_checkbox.h" +#include "SampleBuffer.h" +#include "MemoryManager.h" +#include "gig.h" + +class GigInstrumentView; +class NotePlayHandle; + +class PatchesDialog; +class QLabel; + + + + +struct GIGPluginData +{ + int midiNote; +} ; + + + + +// Load a GIG file using libgig +class GigInstance +{ +public: + GigInstance( QString filename ) : + riff( filename.toUtf8().constData() ), + gig( &riff ) + {} + +private: + RIFF::File riff; + +public: + gig::File gig; +} ; + + + + +// Stores options for the notes, e.g. velocity and release time +struct Dimension +{ + Dimension() : + release( false ) + { + for( int i = 0; i < 8; ++i ) + { + DimValues[i] = 0; + } + } + + uint DimValues[8]; + bool release; +} ; + + + + +// Takes information from the GIG file for a certain note and provides the +// amplitude (0-1) to multiply the signal by (internally incrementing the +// position in the envelope when asking for the amplitude). +class ADSR +{ + // From the file + float preattack; // initial amplitude (0-1) + float attack; // 0-60s + float decay1; // 0-60s + float decay2; // 0-60s + bool infiniteSustain; // i.e., no decay2 + float sustain; // sustain amplitude (0-1) + float release; // 0-60s + + // Used to calculate current amplitude + float amplitude; + bool isAttack; + bool isRelease; + bool isDone; + f_cnt_t attackPosition; + f_cnt_t attackLength; + f_cnt_t decayLength; + f_cnt_t releasePosition; + f_cnt_t releaseLength; + +public: + ADSR(); + ADSR( gig::DimensionRegion * region, int sampleRate ); + void keyup(); // We will begin releasing starting now + bool done(); // Is this sample done playing? + float value(); // What's the current amplitude + void inc( f_cnt_t num ); // Increment internal positions by num +} ; + + + + +// The sample from the GIG file with our current position in both the sample +// and the envelope +class GigSample +{ +public: + GigSample( gig::Sample * pSample, gig::DimensionRegion * pDimRegion, + float attenuation, int interpolation, float desiredFreq ); + ~GigSample(); + + // Needed when initially creating in QList + GigSample( const GigSample& g ); + GigSample& operator=( const GigSample& g ); + + // Needed since libsamplerate stores data internally between calls + void updateSampleRate(); + bool convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, + f_cnt_t oldSize, f_cnt_t newSize, float freq_factor, f_cnt_t& used ); + + gig::Sample * sample; + gig::DimensionRegion * region; + float attenuation; + ADSR adsr; + + // The position in sample + f_cnt_t pos; + + // Whether to change the pitch of the samples, e.g. if there's only one + // sample per octave and you want that sample pitch shifted for the rest of + // the notes in the octave, this will be true + bool pitchtrack; + + // Used to convert sample rates + int interpolation; + SRC_STATE * srcState; + + // Used changing the pitch of the note if desired + float sampleFreq; + float freqFactor; +} ; + + + + +// What portion of a note are we in? +enum GigState +{ + // We just pressed the key + KeyDown, + // The note is currently playing + PlayingKeyDown, + // We just released the key + KeyUp, + // The note is being released, e.g. a release sample is playing + PlayingKeyUp, + // The note is done playing, you can delete this note now + Completed +} ; + + + + +// Corresponds to a certain midi note pressed, but may contain multiple samples +class GigNote +{ +public: + int midiNote; + int velocity; + bool release; // Whether to trigger a release sample on key up + bool isRelease; // Whether this is a release sample, changes when we delete it + GigState state; + float frequency; + QList samples; + + // Used to determine which note should be released on key up + // + // Note: if accessing the data, be careful not to access it after the key + // has been released since that's when it is deleted + GIGPluginData * handle; + + GigNote( int midiNote, int velocity, float frequency, GIGPluginData * handle ) + : midiNote( midiNote ), velocity( velocity ), + release( false ), isRelease( false ), state( KeyDown ), + frequency( frequency ), handle( handle ) + { + } +} ; + + + + +class GigInstrument : public Instrument +{ + Q_OBJECT + MM_OPERATORS + + mapPropertyFromModel( int, getBank, setBank, m_bankNum ); + mapPropertyFromModel( int, getPatch, setPatch, m_patchNum ); + +public: + GigInstrument( InstrumentTrack * _instrument_track ); + virtual ~GigInstrument(); + + virtual void play( sampleFrame * _working_buffer ); + + virtual void playNote( NotePlayHandle * _n, + sampleFrame * _working_buffer ); + virtual void deleteNotePluginData( NotePlayHandle * _n ); + + + virtual void saveSettings( QDomDocument & _doc, QDomElement & _parent ); + virtual void loadSettings( const QDomElement & _this ); + + virtual void loadFile( const QString & _file ); + + virtual AutomatableModel * childModel( const QString & _modelName ); + + virtual QString nodeName() const; + + virtual f_cnt_t desiredReleaseFrames() const + { + return 0; + } + + virtual Flags flags() const + { + return IsSingleStreamed|IsNotBendable; + } + + virtual PluginView * instantiateView( QWidget * _parent ); + + QString getCurrentPatchName(); + + + void setParameter( const QString & _param, const QString & _value ); + + +public slots: + void openFile( const QString & _gigFile, bool updateTrackName = true ); + void updatePatch(); + void updateSampleRate(); + + +private: + // The GIG file and instrument we're using + GigInstance * m_instance; + gig::Instrument * m_instrument; + + // Part of the UI + QString m_filename; + + LcdSpinBoxModel m_bankNum; + LcdSpinBoxModel m_patchNum; + + FloatModel m_gain; + + // Locking for the data + QMutex m_synthMutex; + QMutex m_notesMutex; + + // Used for resampling + int m_interpolation; + + // List of all the currently playing notes + QList m_notes; + + // Used when determining which samples to use + uint32_t m_RandomSeed; + float m_currentKeyDimension; + +private: + // Delete the current GIG instance if one is open + void freeInstance(); + + // Open the instrument in the currently-open GIG file + void getInstrument(); + + // Create "dimension" to select desired samples from GIG file based on + // parameters such as velocity + Dimension getDimensions( gig::Region * pRegion, int velocity, bool release ); + + // Load sample data from the Gig file, looping the sample where needed + void loadSample( GigSample& sample, sampleFrame* sampleData, f_cnt_t samples ); + f_cnt_t getLoopedIndex( f_cnt_t index, f_cnt_t startf, f_cnt_t endf ) const; + f_cnt_t getPingPongIndex( f_cnt_t index, f_cnt_t startf, f_cnt_t endf ) const; + + // Add the desired samples to the note, either normal samples or release + // samples + void addSamples( GigNote & gignote, bool wantReleaseSample ); + + friend class GigInstrumentView; + +signals: + void fileLoading(); + void fileChanged(); + void patchChanged(); + +} ; + + + + +class GigInstrumentView : public InstrumentView +{ + Q_OBJECT +public: + GigInstrumentView( Instrument * _instrument, + QWidget * _parent ); + virtual ~GigInstrumentView(); + +private: + virtual void modelChanged(); + + pixmapButton * m_fileDialogButton; + pixmapButton * m_patchDialogButton; + + LcdSpinBox * m_bankNumLcd; + LcdSpinBox * m_patchNumLcd; + + QLabel * m_filenameLabel; + QLabel * m_patchLabel; + + knob * m_gainKnob; + + static PatchesDialog * s_patchDialog; + +protected slots: + void invalidateFile(); + void showFileDialog(); + void showPatchDialog(); + void updateFilename(); + void updatePatchName(); +} ; + + +#endif diff --git a/plugins/GigPlayer/PatchesDialog.cpp b/plugins/GigPlayer/PatchesDialog.cpp new file mode 100644 index 000000000..71f1e26ff --- /dev/null +++ b/plugins/GigPlayer/PatchesDialog.cpp @@ -0,0 +1,414 @@ +/* + * PatchesDialog.cpp - display GIG patches (based on Sf2 patches_dialog.cpp) + * + * Copyright (c) 2008 Paul Giblock + * + * 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 "PatchesDialog.h" + +#include + + +// Custom list-view item (as for numerical sort purposes...) +class PatchItem : public QTreeWidgetItem +{ +public: + + // Constructor. + PatchItem( QTreeWidget *pListView, + QTreeWidgetItem *pItemAfter ) + : QTreeWidgetItem( pListView, pItemAfter ) {} + + // Sort/compare overriden method. + bool operator< ( const QTreeWidgetItem& other ) const + { + int iColumn = QTreeWidgetItem::treeWidget()->sortColumn(); + const QString& s1 = text( iColumn ); + const QString& s2 = other.text( iColumn ); + + if( iColumn == 0 || iColumn == 2 ) + { + return s1.toInt() < s2.toInt(); + } + else + { + return s1 < s2; + } + } +}; + + + + +// Constructor. +PatchesDialog::PatchesDialog( QWidget * pParent, Qt::WindowFlags wflags ) + : QDialog( pParent, wflags ) +{ + // Setup UI struct... + setupUi( this ); + + m_pSynth = NULL; + m_iChan = 0; + m_iBank = 0; + m_iProg = 0; + + // Soundfonts list view... + QHeaderView * pHeader = m_progListView->header(); + pHeader->setDefaultAlignment( Qt::AlignLeft ); +#if QT_VERSION >= 0x050000 + pHeader->setSectionsMovable( false ); +#else + pHeader->setMovable( false ); +#endif + pHeader->setStretchLastSection( true ); + + m_progListView->resizeColumnToContents( 0 ); // Prog. + + // Initial sort order... + m_bankListView->sortItems( 0, Qt::AscendingOrder ); + m_progListView->sortItems( 0, Qt::AscendingOrder ); + + // UI connections... + QObject::connect( m_bankListView, + SIGNAL( currentItemChanged( QTreeWidgetItem *,QTreeWidgetItem * ) ), + SLOT( bankChanged() ) ); + QObject::connect( m_progListView, + SIGNAL( currentItemChanged( QTreeWidgetItem *, QTreeWidgetItem * ) ), + SLOT( progChanged( QTreeWidgetItem *, QTreeWidgetItem * ) ) ); + QObject::connect( m_progListView, + SIGNAL( itemActivated( QTreeWidgetItem *, int ) ), + SLOT( accept() ) ); + QObject::connect( m_okButton, + SIGNAL( clicked() ), + SLOT( accept() ) ); + QObject::connect( m_cancelButton, + SIGNAL( clicked() ), + SLOT( reject() ) ); +} + + + + +// Destructor. +PatchesDialog::~PatchesDialog() +{ +} + + + + +// Dialog setup loader. +void PatchesDialog::setup( GigInstance * pSynth, int iChan, + const QString & chanName, + LcdSpinBoxModel * bankModel, + LcdSpinBoxModel * progModel, + QLabel * patchLabel ) +{ + + // We'll going to changes the whole thing... + m_dirty = 0; + m_bankModel = bankModel; + m_progModel = progModel; + m_patchLabel = patchLabel; + + // Set the proper caption... + setWindowTitle( chanName + " - GIG patches" ); + + // set m_pSynth to NULL so we don't trigger any progChanged events + m_pSynth = NULL; + + // Load bank list from actual synth stack... + m_bankListView->setSortingEnabled( false ); + m_bankListView->clear(); + + // now it should be safe to set internal stuff + m_pSynth = pSynth; + m_iChan = iChan; + + + //fluid_preset_t preset; + QTreeWidgetItem * pBankItem = NULL; + + // Currently just use zero as the only bank + int iBankDefault = -1; + int iProgDefault = -1; + + gig::Instrument * pInstrument = m_pSynth->gig.GetFirstInstrument(); + + while( pInstrument ) + { + int iBank = pInstrument->MIDIBank; + int iProg = pInstrument->MIDIProgram; + + if ( !findBankItem( iBank ) ) + { + pBankItem = new PatchItem( m_bankListView, pBankItem ); + + if( pBankItem ) + { + pBankItem->setText( 0, QString::number( iBank ) ); + + if( iBankDefault == -1 ) + { + iBankDefault = iBank; + iProgDefault = iProg; + } + } + } + + pInstrument = m_pSynth->gig.GetNextInstrument(); + } + + m_bankListView->setSortingEnabled( true ); + + // Set the selected bank. + if( iBankDefault != -1 ) + { + m_iBank = iBankDefault; + } + + pBankItem = findBankItem( m_iBank ); + m_bankListView->setCurrentItem( pBankItem ); + m_bankListView->scrollToItem( pBankItem ); + bankChanged(); + + // Set the selected program. + if( iProgDefault != -1 ) + { + m_iProg = iProgDefault; + } + + QTreeWidgetItem * pProgItem = findProgItem( m_iProg ); + m_progListView->setCurrentItem( pProgItem ); + m_progListView->scrollToItem( pProgItem ); +} + + + + +// Stabilize current state form. +void PatchesDialog::stabilizeForm() +{ + m_okButton->setEnabled( validateForm() ); +} + + + + +// Validate form fields. +bool PatchesDialog::validateForm() +{ + bool bValid = true; + + bValid = bValid && ( m_bankListView->currentItem() != NULL ); + bValid = bValid && ( m_progListView->currentItem() != NULL ); + + return bValid; +} + + + + +// Realize a bank-program selection preset. +void PatchesDialog::setBankProg( int iBank, int iProg ) +{ + if( m_pSynth == NULL ) + { + return; + } +} + + + + +// Validate form fields and accept it valid. +void PatchesDialog::accept() +{ + if( validateForm() ) + { + // Unload from current selected dialog items. + int iBank = ( m_bankListView->currentItem() )->text( 0 ).toInt(); + int iProg = ( m_progListView->currentItem() )->text( 0 ).toInt(); + + // And set it right away... + setBankProg( iBank, iProg ); + + if( m_dirty > 0 ) + { + m_bankModel->setValue( iBank ); + m_progModel->setValue( iProg ); + m_patchLabel->setText( m_progListView-> + currentItem()->text( 1 ) ); + } + + // We got it. + QDialog::accept(); + } +} + + + + +// Reject settings (Cancel button slot). +void PatchesDialog::reject() +{ + // Reset selection to initial selection, if applicable... + if( m_dirty > 0 ) + { + setBankProg( m_bankModel->value(), m_progModel->value() ); + } + + // Done (hopefully nothing). + QDialog::reject(); +} + + + + +// Find the bank item of given bank number id. +QTreeWidgetItem * PatchesDialog::findBankItem( int iBank ) +{ + QList banks + = m_bankListView->findItems( + QString::number( iBank ), Qt::MatchExactly, 0 ); + + QListIterator iter( banks ); + + if( iter.hasNext() ) + { + return iter.next(); + } + else + { + return NULL; + } +} + + + + +// Find the program item of given program number id. +QTreeWidgetItem *PatchesDialog::findProgItem( int iProg ) +{ + QList progs + = m_progListView->findItems( + QString::number( iProg ), Qt::MatchExactly, 0 ); + + QListIterator iter( progs ); + + if( iter.hasNext() ) + { + return iter.next(); + } + else + { + return NULL; + } +} + + + + +// Bank change slot. +void PatchesDialog::bankChanged() +{ + if( m_pSynth == NULL ) + { + return; + } + + QTreeWidgetItem * pBankItem = m_bankListView->currentItem(); + + if( pBankItem == NULL ) + { + return; + } + + int iBankSelected = pBankItem->text( 0 ).toInt(); + + // Clear up the program listview. + m_progListView->setSortingEnabled( false ); + m_progListView->clear(); + QTreeWidgetItem * pProgItem = NULL; + + gig::Instrument * pInstrument = m_pSynth->gig.GetFirstInstrument(); + + while( pInstrument ) + { + QString name = QString::fromStdString( pInstrument->pInfo->Name ); + + if( name == "" ) + { + name = ""; + } + + int iBank = pInstrument->MIDIBank; + int iProg = pInstrument->MIDIProgram; + + if( iBank == iBankSelected && !findProgItem( iProg ) ) + { + pProgItem = new PatchItem( m_progListView, pProgItem ); + + if( pProgItem ) + { + pProgItem->setText( 0, QString::number( iProg ) ); + pProgItem->setText( 1, name ); + } + } + + pInstrument = m_pSynth->gig.GetNextInstrument(); + } + + m_progListView->setSortingEnabled( true ); + + // Stabilize the form. + stabilizeForm(); +} + + + + +// Program change slot. +void PatchesDialog::progChanged( QTreeWidgetItem * curr, QTreeWidgetItem * prev ) +{ + if( m_pSynth == NULL || curr == NULL ) + { + return; + } + + // Which preview state... + if( validateForm() ) + { + // Set current selection. + int iBank = ( m_bankListView->currentItem() )->text( 0 ).toInt(); + int iProg = curr->text( 0 ).toInt(); + + // And set it right away... + setBankProg( iBank, iProg ); + + // Now we're dirty nuff. + m_dirty++; + } + + // Stabilize the form. + stabilizeForm(); +} diff --git a/plugins/GigPlayer/PatchesDialog.h b/plugins/GigPlayer/PatchesDialog.h new file mode 100644 index 000000000..bb4dedde2 --- /dev/null +++ b/plugins/GigPlayer/PatchesDialog.h @@ -0,0 +1,92 @@ +/* + * PatchesDialog.h - display GIG patches (based on Sf2 patches_dialog.h) + * + * Copyright (c) 2008 Paul Giblock + * + * 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 PATCHES_DIALOG_H +#define PATCHES_DIALOG_H + +#include "ui_PatchesDialog.h" +#include "LcdSpinBox.h" +#include "GigPlayer.h" + +#include +#include +#include + +//---------------------------------------------------------------------------- +// qsynthPresetForm -- UI wrapper form. + +class PatchesDialog : public QDialog, private Ui::PatchesDialog +{ + Q_OBJECT + +public: + + // Constructor. + PatchesDialog( QWidget * pParent = 0, Qt::WindowFlags wflags = 0 ); + + // Destructor. + virtual ~PatchesDialog(); + + + void setup( GigInstance * pSynth, int iChan, const QString & chanName, + LcdSpinBoxModel * bankModel, LcdSpinBoxModel * progModel, QLabel * patchLabel ); + +public slots: + + void stabilizeForm(); + void bankChanged(); + void progChanged( QTreeWidgetItem * curr, QTreeWidgetItem * prev ); + +protected slots: + + void accept(); + void reject(); + +protected: + + void setBankProg( int iBank, int iProg ); + + QTreeWidgetItem * findBankItem( int iBank ); + QTreeWidgetItem * findProgItem( int iProg ); + + bool validateForm(); + +private: + + // Instance variables. + GigInstance * m_pSynth; + + int m_iChan; + int m_iBank; + int m_iProg; + + int m_dirty; + + LcdSpinBoxModel * m_bankModel; + LcdSpinBoxModel * m_progModel; + QLabel * m_patchLabel; +}; + +#endif diff --git a/plugins/GigPlayer/PatchesDialog.ui b/plugins/GigPlayer/PatchesDialog.ui new file mode 100644 index 000000000..2f9f8a865 --- /dev/null +++ b/plugins/GigPlayer/PatchesDialog.ui @@ -0,0 +1,216 @@ + + rncbc aka Rui Nuno Capela + qsynth - A fluidsynth Qt GUI Interface. + + Copyright (C) 2003-2007, rncbc aka Rui Nuno Capela. All rights reserved. + + 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; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + + PatchesDialog + + + + 0 + 0 + 480 + 350 + + + + + 300 + 150 + + + + Qsynth: Channel Preset + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + 20 + 0 + + + + + 80 + 32767 + + + + Bank selector + + + true + + + 4 + + + false + + + true + + + false + + + true + + + true + + + + Bank + + + + + + Program selector + + + true + + + 4 + + + false + + + true + + + false + + + true + + + true + + + + Patch + + + + + Name + + + + + + + + + 4 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 120 + 8 + + + + + + + + + + + OK + + + + 0 + 0 + + + + true + + + + + + + + + + Cancel + + + + 0 + 0 + + + + + + + + + + + m_okButton + m_cancelButton + + + + diff --git a/plugins/GigPlayer/artwork.png b/plugins/GigPlayer/artwork.png new file mode 100644 index 000000000..991d779aa Binary files /dev/null and b/plugins/GigPlayer/artwork.png differ diff --git a/plugins/GigPlayer/fileselect_off.png b/plugins/GigPlayer/fileselect_off.png new file mode 100644 index 000000000..e95e10ba8 Binary files /dev/null and b/plugins/GigPlayer/fileselect_off.png differ diff --git a/plugins/GigPlayer/fileselect_on.png b/plugins/GigPlayer/fileselect_on.png new file mode 100644 index 000000000..9f735ae96 Binary files /dev/null and b/plugins/GigPlayer/fileselect_on.png differ diff --git a/plugins/GigPlayer/logo.png b/plugins/GigPlayer/logo.png new file mode 100644 index 000000000..73f1b37fd Binary files /dev/null and b/plugins/GigPlayer/logo.png differ diff --git a/plugins/GigPlayer/patches_off.png b/plugins/GigPlayer/patches_off.png new file mode 100644 index 000000000..e77637bb4 Binary files /dev/null and b/plugins/GigPlayer/patches_off.png differ diff --git a/plugins/GigPlayer/patches_on.png b/plugins/GigPlayer/patches_on.png new file mode 100644 index 000000000..349882abf Binary files /dev/null and b/plugins/GigPlayer/patches_on.png differ