diff --git a/CMakeLists.txt b/CMakeLists.txt index ef1e08e16..f7c139d96 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 8311c4fa6..bd073e6a2 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -25,6 +25,7 @@ ADD_SUBDIRECTORY(peak_controller_effect) IF(NOT LMMS_BUILD_APPLE) ADD_SUBDIRECTORY(sf2_player) ENDIF() +ADD_SUBDIRECTORY(gig_player) ADD_SUBDIRECTORY(sfxr) ADD_SUBDIRECTORY(sid) ADD_SUBDIRECTORY(SpectrumAnalyzer) diff --git a/plugins/gig_player/CMakeLists.txt b/plugins/gig_player/CMakeLists.txt new file mode 100644 index 000000000..7e32e5340 --- /dev/null +++ b/plugins/gig_player/CMakeLists.txt @@ -0,0 +1,8 @@ +if(LMMS_HAVE_GIG) + INCLUDE(BuildPlugin) + INCLUDE_DIRECTORIES(${GIG_INCLUDE_DIRS}{) + LINK_DIRECTORIES(${GIG_LIBRARY_DIRS}) + LINK_LIBRARIES(${GIG_LIBRARIES}) + BUILD_PLUGIN(gigplayer gig_player.cpp gig_player.h patches_dialog.cpp patches_dialog.h patches_dialog.ui MOCFILES gig_player.h patches_dialog.h UICFILES patches_dialog.ui EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png") +endif(LMMS_HAVE_GIG) + diff --git a/plugins/gig_player/artwork.png b/plugins/gig_player/artwork.png new file mode 100644 index 000000000..a784766c8 Binary files /dev/null and b/plugins/gig_player/artwork.png differ diff --git a/plugins/gig_player/chorus_off.png b/plugins/gig_player/chorus_off.png new file mode 100644 index 000000000..14e2414bd Binary files /dev/null and b/plugins/gig_player/chorus_off.png differ diff --git a/plugins/gig_player/chorus_on.png b/plugins/gig_player/chorus_on.png new file mode 100644 index 000000000..181385043 Binary files /dev/null and b/plugins/gig_player/chorus_on.png differ diff --git a/plugins/gig_player/fileselect_off.png b/plugins/gig_player/fileselect_off.png new file mode 100644 index 000000000..e95e10ba8 Binary files /dev/null and b/plugins/gig_player/fileselect_off.png differ diff --git a/plugins/gig_player/fileselect_on.png b/plugins/gig_player/fileselect_on.png new file mode 100644 index 000000000..9f735ae96 Binary files /dev/null and b/plugins/gig_player/fileselect_on.png differ diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp new file mode 100644 index 000000000..cf5df5edf --- /dev/null +++ b/plugins/gig_player/gig_player.cpp @@ -0,0 +1,832 @@ +/* + * gig_player.cpp - a gig player using libgig + * + * 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. + * + */ + +#include +#include +#include +#include +#include // sin, cos, ... +#include // memset + +#include "FileDialog.h" +#include "gig_player.h" +#include "engine.h" +#include "InstrumentTrack.h" +#include "InstrumentPlayHandle.h" +#include "NotePlayHandle.h" +#include "knob.h" +#include "song.h" + +#include "patches_dialog.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 +} ; + +} + + +struct GIGPluginData +{ + int midiNote; + int lastPanning; + float lastVelocity; +} ; + + + +// Static map of current GIG instances +QMap gigInstrument::s_instances; +QMutex gigInstrument::s_instancesMutex; + + + +gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : + Instrument( _instrument_track, &gigplayer_plugin_descriptor ), + m_srcState( NULL ), + m_instance( NULL ), + m_filename( "" ), + m_lastMidiPitch( -1 ), + m_lastMidiPitchRange( -1 ), + m_channel( 1 ), + 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" ) ) +{ + InstrumentPlayHandle * iph = new InstrumentPlayHandle( this ); + engine::mixer()->addPlayHandle( iph ); + + connect( &m_bankNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) ); + connect( &m_patchNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) ); + //connect( engine::mixer(), SIGNAL( sampleRateChanged() ), this, SLOT( updateSampleRate() ) ); + + // Gain + //connect( &m_gain, SIGNAL( dataChanged() ), this, SLOT( updateGain() ) ); +} + + + +gigInstrument::~gigInstrument() +{ + engine::mixer()->removePlayHandles( instrumentTrack() ); + freeInstance(); + + if( m_srcState != NULL ) + { + src_delete( m_srcState ); + } + +} + + + +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(); + } +} + + + + +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() +{ + m_synthMutex.lock(); + + if ( m_instance != NULL ) + { + s_instancesMutex.lock(); + --(m_instance->refCount); + + // No more references + if( m_instance->refCount <= 0 ) + { + qDebug() << "Really deleting " << m_filename; + + // Need we do more to delete the gig instance? + s_instances.remove( m_filename ); + delete m_instance; + } + // Just remove our reference + else + { + qDebug() << "un-referencing " << m_filename; + } + s_instancesMutex.unlock(); + + m_instance = NULL; + } + m_synthMutex.unlock(); +} + + + +void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) +{ + emit fileLoading(); + + // Used for loading file + char * gigAscii = qstrdup( qPrintable( SampleBuffer::tryToMakeAbsolute( _gigFile ) ) ); + QString relativePath = SampleBuffer::tryToMakeRelative( _gigFile ); + + // free reference to gig file if one is selected + freeInstance(); + + m_synthMutex.lock(); + s_instancesMutex.lock(); + + // Increment Reference + if( s_instances.contains( relativePath ) ) + { + qDebug() << "Using existing reference to " << relativePath; + + m_instance = s_instances[ relativePath ]; + + m_instance->refCount++; + } + + // Add to map, if doesn't exist. + else + { + // Grab this sf from the top of the stack and add to list + m_instance = new gigInstance( _gigFile ); + s_instances.insert( relativePath, m_instance ); + } + + s_instancesMutex.unlock(); + m_synthMutex.unlock(); + + // TODO: only do this if successfully loaded + if( true ) + { + m_filename = relativePath; + + emit fileChanged(); + } + + delete[] gigAscii; + + if( updateTrackName ) + { + instrumentTrack()->setName( QFileInfo( _gigFile ).baseName() ); + } +} + + + + +void gigInstrument::updatePatch() +{ + if( m_bankNum.value() >= 0 && m_patchNum.value() >= 0 ) + { + // TODO: are there patches in GIG files? + //fluid_synth_program_select( m_synth, m_channel, m_fontId, + // m_bankNum.value(), m_patchNum.value() ); + } +} + + + + +QString gigInstrument::getCurrentPatchName() +{ + if (!m_instance) + return ""; + + int iBankSelected = m_bankNum.value(); + int iProgSelected = m_patchNum.value(); + + gig::Instrument* pInstrument = m_instance->gig.GetFirstInstrument(); + while (pInstrument) { + 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 ""; +} + + + +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; + pluginData->lastPanning = -1; + pluginData->lastVelocity = 127; + + _n->m_pluginData = pluginData; + + // TODO: Start the note here + //const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); + //fluid_synth_noteon( m_synth, m_channel, midiNote, _n->midiVelocity( baseVelocity ) ); + + if (m_instance) + { + // Find instrument + int iBankSelected = m_bankNum.value(); + int iProgSelected = m_patchNum.value(); + + gig::Instrument* pInstrument = m_instance->gig.GetFirstInstrument(); + while (pInstrument) { + int iBank = pInstrument->MIDIBank; + int iProg = pInstrument->MIDIProgram; + + if (iBank == iBankSelected && iProg == iProgSelected) { + break; + } + + pInstrument = m_instance->gig.GetNextInstrument(); + } + + // Find sample + if (pInstrument) + { + gig::Region* pRegion = pInstrument->GetFirstRegion(); + + while (pRegion) { + gig::Sample* pSample = pRegion->GetSample(); + + if (pSample) + { + int rate = pSample->SamplesPerSecond; + QString name = QString::fromStdString(pSample->pInfo->Name); + int keyLow = pRegion->KeyRange.low; + int keyHigh = pRegion->KeyRange.high; + + if (rate != engine::mixer()->processingSampleRate()) + { + qDebug() << "Warning: wrong sample rate, conversion not implemented"; + } + + if (midiNote >= keyLow && midiNote <= keyHigh) + { + /*qDebug() << "Playing note " << midiNote << " between " << keyLow + << " and " << keyHigh + << " from sample " << name << " at " << rate << " Hz of size " + << pSample->SamplesTotal;*/ + + gig::buffer_t buf = pSample->LoadSampleData(); + gigNote note(pSample->SamplesTotal); + + if (pSample->BitDepth == 24) + { + qDebug() << "Error: not 16 bit... not implemented"; + /*int n = pSample->SamplesTotal * pSample->Channels; + + for (int i = n-1; i>=0; i-=pSample->Channels) + for (int j = 0; j < pSample->Channels; ++j) + note.note[i][j] = + 1.0/0x800000*(pWave[i*3+j] + | pWave[i*3+1+3*j]<<8 + | pWave[i*3+2+3*j]<<16);*/ + } + else + { + int16_t* pInt = reinterpret_cast(buf.pStart); + + if (pSample->Channels <= 2) + { + for (int i = 0; i < pSample->SamplesTotal/pSample->Channels; ++i) + { + note.note[i][0] = 1.0/0x10000*pInt[pSample->Channels*i]; + + if (pSample->Channels == 1) + note.note[i][1] = note.note[i][0]; + else + note.note[i][1] = 1.0/0x10000*pInt[pSample->Channels*i+1]; + } + } + else + { + qDebug() << "Error: not stereo... not implemented"; + } + } + + m_synthMutex.lock(); + m_notes.push_back(note); + m_synthMutex.unlock(); + + pSample->ReleaseSampleData(); + } + } + + pRegion = pInstrument->GetNextRegion(); + } + } + } + else + { + // TODO: eventually get rid of this + + // By default just play a sine wave instead of a GIG file. + double freq = _n->unpitchedFrequency(); + //int size = ceil(engine::mixer()->processingSampleRate()/freq); + int size = 1.0/2*engine::mixer()->processingSampleRate(); + float scale = 0.25; // Default max volume for one note + gigNote note(size); + note.midiNote = midiNote; + + for (int i = 0; i < size; ++i) + { + note.note[i][0] = sin(1.0*i*freq*2*M_PI/engine::mixer()->processingSampleRate()) + * (1.0-1.0/size*i)*scale; + note.note[i][1] = note.note[i][0]; + } + + m_synthMutex.lock(); + m_notes.push_back(note); + m_synthMutex.unlock(); + } + } + + /*GIGPluginData * pluginData = static_cast( + _n->m_pluginData ); + + const float currentVelocity = _n->volumeLevel( tfp ) * instrumentTrack()->midiPort()->baseVelocity(); + if( pluginData->lastVelocity != currentVelocity ) + { + m_synthMutex.lock(); + // TODO: set the new velocity to currentvelocity + m_synthMutex.unlock(); + + pluginData->lastVelocity = currentVelocity; + }*/ +} + + + + +// Could we get iph-based instruments support sample-exact models by using a +// frame-length of 1 while rendering? +void gigInstrument::play( sampleFrame * _working_buffer ) +{ + const fpp_t frames = engine::mixer()->framesPerPeriod(); + + // Initialize to zeros + for (int i = 0; i < frames; ++i) + { + _working_buffer[i][0] = float(); + _working_buffer[i][1] = float(); + } + + m_synthMutex.lock(); + + const int currentMidiPitch = instrumentTrack()->midiPitch(); + if( m_lastMidiPitch != currentMidiPitch ) + { + m_lastMidiPitch = currentMidiPitch; + // TODO: pitch bend... + //fluid_synth_pitch_bend( m_synth, m_channel, m_lastMidiPitch ); + } + + const int currentMidiPitchRange = instrumentTrack()->midiPitchRange(); + if( m_lastMidiPitchRange != currentMidiPitchRange ) + { + m_lastMidiPitchRange = currentMidiPitchRange; + // TODO: pitch bend... + //fluid_synth_pitch_wheel_sens( m_synth, m_channel, m_lastMidiPitchRange ); + } + + if( m_internalSampleRate < engine::mixer()->processingSampleRate() && + m_srcState != NULL ) + { + const fpp_t f = frames * m_internalSampleRate / engine::mixer()->processingSampleRate(); +#ifdef __GNUC__ + sampleFrame tmp[f]; +#else + sampleFrame * tmp = new sampleFrame[f]; +#endif + + qDebug() << "Different internal sample rate"; + + // TODO: get sample, interleave left/right channels + SRC_DATA src_data; + src_data.data_in = tmp[0]; + src_data.data_out = _working_buffer[0]; + src_data.input_frames = f; + src_data.output_frames = frames; + src_data.src_ratio = (double) frames / f; + src_data.end_of_input = 0; + int error = src_process( m_srcState, &src_data ); +#ifndef __GNUC__ + delete[] tmp; +#endif + if( error ) + { + qCritical( "gigInstrument: error while resampling: %s", src_strerror( error ) ); + } + if( src_data.output_frames_gen > frames ) + { + qCritical( "gigInstrument: not enough frames: %ld / %d", src_data.output_frames_gen, frames ); + } + } + else + { + // TODO: get sample, interleave left/right channels + //fluid_synth_write_float( m_synth, frames, _working_buffer, 0, 2, _working_buffer, 1, 2 ); + + for( int i = 0; i < frames; ++i ) + { + for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + { + if( note->position < note->size && note->size > 0 ) + { + _working_buffer[i][0] += note->note[note->position][0]*m_gain.value(); + _working_buffer[i][1] += note->note[note->position][1]*m_gain.value(); + ++note->position; + } + } + } + } + + m_synthMutex.unlock(); + + instrumentTrack()->processAudioBuffer( _working_buffer, frames, NULL ); +} + + + + +void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) +{ + //GIGPluginData * pluginData = static_cast( _n->m_pluginData ); + m_synthMutex.lock(); + + // Delete ended notes. Continue looping starting at the beginning. Erasing + // invalidates the iterator. + bool deleted = false; + + do + { + deleted = false; + std::list::iterator i = m_notes.begin(); + + while( i != m_notes.end() ) + { + if( i->position >= i->size ) + { + m_notes.erase(i); + deleted = true; + break; + } + + ++i; + } + } + while( deleted ); + + m_synthMutex.unlock(); + + //delete pluginData; +} + + + + +PluginView * gigInstrument::instantiateView( QWidget * _parent ) +{ + return new gigInstrumentView( this, _parent ); +} + + + + + + + +class gigKnob : public knob +{ +public: + gigKnob( QWidget * _parent ) : +// knob( knobStyled, _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( 217, 107 ); + + 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( 217, 125 ); + + connect( m_patchDialogButton, SIGNAL( clicked() ), this, SLOT( showPatchDialog() ) ); + + toolTip::add( m_patchDialogButton, tr( "Choose the patch" ) ); + + + // LCDs + m_bankNumLcd = new LcdSpinBox( 3, "21pink", this ); + m_bankNumLcd->move(131, 62); + + m_patchNumLcd = new LcdSpinBox( 3, "21pink", this ); + m_patchNumLcd->move(190, 62); + + // Next row + m_filenameLabel = new QLabel( this ); + m_filenameLabel->setGeometry( 58, 109, 156, 11 ); + m_patchLabel = new QLabel( this ); + m_patchLabel->setGeometry( 58, 127, 156, 11 ); + + // Gain + m_gainKnob = new gigKnob( this ); + m_gainKnob->setHintText( tr("Gain") + " ", "" ); + m_gainKnob->move( 86, 55 ); + + 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() +{ + // TODO: does it have patches? + 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(); +} + + + +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 ) ); +} + + +} + +#include "moc_gig_player.cxx" + diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h new file mode 100644 index 000000000..c18991abe --- /dev/null +++ b/plugins/gig_player/gig_player.h @@ -0,0 +1,247 @@ +/* + * gig_player.h - a gig player using libgig + * + * 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 "Instrument.h" +#include "pixmap_button.h" +#include "InstrumentView.h" +#include "knob.h" +#include "LcdSpinBox.h" +#include "led_checkbox.h" +#include "SampleBuffer.h" +#include "gig.h" + +class gigInstrumentView; +class gigInstance; +class NotePlayHandle; + +class patchesDialog; +class QLabel; + +class gigInstance +{ +public: + gigInstance( QString filename ) : + riff( filename.toUtf8().constData() ), + gig( &riff ), + refCount( 1 ) + {} + +private: + RIFF::File riff; + +public: + gig::File gig; + int refCount; +}; + + + +class gigNote +{ +public: + gigNote() : + position( 0 ), + midiNote( -1 ), + note( NULL ), + size( 0 ) + { + } + + gigNote(int size) : + position( 0 ), + midiNote( -1 ), + size( size ) + { + note = new sampleFrame[size]; + + // Initialize to no sound + for (int i = 0; i < size; ++i) + { + note[i][0] = float(); + note[i][1] = float(); + } + } + + gigNote( const gigNote& g ) : + position( g.position ), + midiNote( g.midiNote ), + note( NULL ), + size( g.size ) + { + if (size > 0) + { + note = new sampleFrame[size]; + std::copy(&g.note[0], &g.note[size], ¬e[0]); + } + } + + ~gigNote() + { + if (note) + delete[] note; + } + + int position; + int midiNote; + sampleFrame* note; + int size; // Don't try changing this... +}; + + + + +class gigInstrument : public Instrument +{ + Q_OBJECT + 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 | IsMidiBased; + } + + 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(); + + +private: + static QMutex s_instancesMutex; + static QMap s_instances; + std::list m_notes; + + SRC_STATE * m_srcState; + + gigInstance* m_instance; + + QString m_filename; + + // Protect the array of active notes + QMutex m_notesRunningMutex; + + // Protect synth when we are re-creating it. + QMutex m_synthMutex; + QMutex m_loadMutex; + + sample_rate_t m_internalSampleRate; + int m_lastMidiPitch; + int m_lastMidiPitchRange; + int m_channel; + + LcdSpinBoxModel m_bankNum; + LcdSpinBoxModel m_patchNum; + + FloatModel m_gain; + +private: + void freeInstance(); + + 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/gig_player/logo.png b/plugins/gig_player/logo.png new file mode 100644 index 000000000..1c900e8ff Binary files /dev/null and b/plugins/gig_player/logo.png differ diff --git a/plugins/gig_player/patches_dialog.cpp b/plugins/gig_player/patches_dialog.cpp new file mode 100644 index 000000000..e1a8dac6c --- /dev/null +++ b/plugins/gig_player/patches_dialog.cpp @@ -0,0 +1,384 @@ +/* + * patches_dialog.cpp - display sf2 patches + * + * 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 "patches_dialog.h" + +#include +//#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->setResizeMode(QHeaderView::Custom); + pHeader->setDefaultAlignment(Qt::AlignLeft); +// pHeader->setDefaultSectionSize(200); + pHeader->setMovable(false); + pHeader->setStretchLastSection(true); + + m_progListView->resizeColumnToContents(0); // Prog. + //pHeader->resizeSection(1, 200); // Name. + + // 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; + // For all soundfonts (in reversed stack order) fill the available banks... + /*int cSoundFonts = ::fluid_synth_sfcount(m_pSynth); + for (int i = 0; i < cSoundFonts; i++) { + fluid_sfont_t *pSoundFont = ::fluid_synth_get_sfont(m_pSynth, i); + if (pSoundFont) { +#ifdef CONFIG_FLUID_BANK_OFFSET + int iBankOffset = ::fluid_synth_get_bank_offset(m_pSynth, pSoundFont->id); +#endif + pSoundFont->iteration_start(pSoundFont); + while (pSoundFont->iteration_next(pSoundFont, &preset)) { + int iBank = preset.get_banknum(&preset); +#ifdef CONFIG_FLUID_BANK_OFFSET + iBank += iBankOffset; +#endif + if (!findBankItem(iBank)) { + pBankItem = new patchItem(m_bankListView, pBankItem); + if (pBankItem) + pBankItem->setText(0, QString::number(iBank)); + } + } + } + }*/ + + // TODO: get rid of "banks" altogether + // 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); + + // Done with setup... + //m_iDirtySetup--; +} + + +// 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; + + // just select the synth's program preset... + //::fluid_synth_bank_select(m_pSynth, m_iChan, iBank); + //::fluid_synth_program_change(m_pSynth, m_iChan, iProg); + // Maybe this is needed to stabilize things around. + //::fluid_synth_program_reset(m_pSynth); +} + + +// 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 ) ); + } + + // Do remember preview state... + // if (m_pOptions) + // m_pOptions->bPresetPreview = m_ui.PreviewCheckBox->isChecked(); + // We got it. + QDialog::accept(); + } +} + + +// Reject settings (Cancel button slot). +void patchesDialog::reject (void) +{ + // 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 (void) +{ + 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(); +} + + +#include "moc_patches_dialog.cxx" diff --git a/plugins/gig_player/patches_dialog.h b/plugins/gig_player/patches_dialog.h new file mode 100644 index 000000000..1dad78005 --- /dev/null +++ b/plugins/gig_player/patches_dialog.h @@ -0,0 +1,96 @@ +/* + * patches_dialog.h - display sf2 patches + * + * 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_patches_dialog.h" +#include "LcdSpinBox.h" +#include "gig_player.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_iDirtySetup; + //int m_iDirtyCount; + int m_dirty; + + LcdSpinBoxModel * m_bankModel; + LcdSpinBoxModel * m_progModel; + QLabel *m_patchLabel; +}; + + +#endif + diff --git a/plugins/gig_player/patches_dialog.ui b/plugins/gig_player/patches_dialog.ui new file mode 100644 index 000000000..5204dc09d --- /dev/null +++ b/plugins/gig_player/patches_dialog.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/gig_player/patches_off.png b/plugins/gig_player/patches_off.png new file mode 100644 index 000000000..e77637bb4 Binary files /dev/null and b/plugins/gig_player/patches_off.png differ diff --git a/plugins/gig_player/patches_on.png b/plugins/gig_player/patches_on.png new file mode 100644 index 000000000..349882abf Binary files /dev/null and b/plugins/gig_player/patches_on.png differ diff --git a/plugins/gig_player/reverb_off.png b/plugins/gig_player/reverb_off.png new file mode 100644 index 000000000..83478c51b Binary files /dev/null and b/plugins/gig_player/reverb_off.png differ diff --git a/plugins/gig_player/reverb_on.png b/plugins/gig_player/reverb_on.png new file mode 100644 index 000000000..e29fc58dd Binary files /dev/null and b/plugins/gig_player/reverb_on.png differ