From d4adf8cc3148977b5159f3128ad2bf14334ae901 Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 27 Apr 2014 16:40:43 -0700 Subject: [PATCH 01/33] Initial version of the GIG Player It currently loads 16-bit mono and stereo GIG files. It plays the whole note (no attack/delay/release), doesn't resample when exporting as a different sample rate, and it doesn't do anything different based on velocity. --- CMakeLists.txt | 13 + plugins/CMakeLists.txt | 1 + plugins/gig_player/CMakeLists.txt | 8 + plugins/gig_player/artwork.png | Bin 0 -> 83967 bytes plugins/gig_player/chorus_off.png | Bin 0 -> 1481 bytes plugins/gig_player/chorus_on.png | Bin 0 -> 1735 bytes plugins/gig_player/fileselect_off.png | Bin 0 -> 936 bytes plugins/gig_player/fileselect_on.png | Bin 0 -> 1087 bytes plugins/gig_player/gig_player.cpp | 832 ++++++++++++++++++++++++++ plugins/gig_player/gig_player.h | 247 ++++++++ plugins/gig_player/logo.png | Bin 0 -> 3137 bytes plugins/gig_player/patches_dialog.cpp | 384 ++++++++++++ plugins/gig_player/patches_dialog.h | 96 +++ plugins/gig_player/patches_dialog.ui | 216 +++++++ plugins/gig_player/patches_off.png | Bin 0 -> 884 bytes plugins/gig_player/patches_on.png | Bin 0 -> 893 bytes plugins/gig_player/reverb_off.png | Bin 0 -> 1439 bytes plugins/gig_player/reverb_on.png | Bin 0 -> 1705 bytes 18 files changed, 1797 insertions(+) create mode 100644 plugins/gig_player/CMakeLists.txt create mode 100644 plugins/gig_player/artwork.png create mode 100644 plugins/gig_player/chorus_off.png create mode 100644 plugins/gig_player/chorus_on.png create mode 100644 plugins/gig_player/fileselect_off.png create mode 100644 plugins/gig_player/fileselect_on.png create mode 100644 plugins/gig_player/gig_player.cpp create mode 100644 plugins/gig_player/gig_player.h create mode 100644 plugins/gig_player/logo.png create mode 100644 plugins/gig_player/patches_dialog.cpp create mode 100644 plugins/gig_player/patches_dialog.h create mode 100644 plugins/gig_player/patches_dialog.ui create mode 100644 plugins/gig_player/patches_off.png create mode 100644 plugins/gig_player/patches_on.png create mode 100644 plugins/gig_player/reverb_off.png create mode 100644 plugins/gig_player/reverb_on.png 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 0000000000000000000000000000000000000000..a784766c89cc8868b9e95e543cd7340864f69de2 GIT binary patch literal 83967 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 P00000NkvXXu0mjfQ-PFV literal 0 HcmV?d00001 diff --git a/plugins/gig_player/chorus_off.png b/plugins/gig_player/chorus_off.png new file mode 100644 index 0000000000000000000000000000000000000000..14e2414bdff5a07ffe2af4ca21ddd9a393e55a01 GIT binary patch literal 1481 zcmV;)1vdJLP)qseQ>y?6n@C! zWlHg{WwwyMFDuNEe{~-OnWy*%ITKbVmL1 z`ZWm<#fqMupVMeGe(!jU+pMi+n`SGUEDK;*whS-}AO)=DgS~Ae@4$|MpaYlCyk4UC zoPwr#43a|mt|u0Y$wrQ`I#Dzh6HDQljD|Gmb;+B~csx$2wH1m+qI7e8P0Q)D%mom* zwRP2?LUA+nT9f~12>6MJ^Ipe)9|2`Kv8jVsqlc7X%QTpy=LiKW)Hn+C} zRMhTV%bHXwO*3XnhtKYG+6o3pE-vb|T;7<+CxMUyR)(TcCql!U8*0_6+>cQvo99Dg zbkl74Kw4r{nakr)y;-kQjAgX5lVhq^DwO4Q%gZS`J1$Y9T9t1$1FNyJYuPNt;|p}O z|0ngkUHJykwMtn9`(W?!aER{;GK|x1x72!}s0$k#8rs7cXrv7i@d&&eaU2@+SgTQ^ ztxtC;zK{@l*r}AE6*p|)@VrRGk-q0mWe)nV;C1tgUVQ%p*$z{X|1#{2U*Ei;tp^V% z5{>X0n+k=Z?BlVO8}%Bk7YgLMj@gIjkWVWB#~IN`gxam9`ebKUr;Vm3uE!>ZTOD>-;No0?4+T<@mK*3OAVgBV#|%kejx ztvg~bBnaU;VtG6nQ?*j2hdaCE*b#CpQA3OgqaxrXF8OTiUEJ8xM+f^V$8=&!>CDPX z2xK~S=YhBq*qT#{#Rn_f^7*F(jIx!CfscrhL?S6cK5<>@b~*wS+>WrTAtRevlG0%1 zn1r$ISj_n;os^EK$MFX`!&7)$jwO`ET9dhgvz3f(^1N(5tE=S-ot~5wzCC-sClNg! z&2D4FolYeKEwF;%CW0>np^RmqI240>SJ?7v`CRB?SZ|qwvd*yu&%wKB!{LDXtaLhU z=n1|*+uNg8KmJ6EOG|WII+XDRu3@9mNRo%F<*|ORr#Pu-p|yoMNqWeXfBxMI?Xm$o zZIe)DGj9&a#04>#>ru*ZrYT>XQ=h2Q3Bd3W z0EP_@e28|S(&FAUz z;zE@U4-|t?pMUX40|?@!Rs*Gu)y4T=YB1=lo!!s$(z1!;4g;$<8Y6`(%G0Ocrbi59 zGI8f?jR|%XKaLv&t<6CRu3HLy6h@N-Y`-8%u9AXjiX{{V;GiI?pddotHr3AKzkif00000NkvXXu0mjf2pGRV literal 0 HcmV?d00001 diff --git a/plugins/gig_player/chorus_on.png b/plugins/gig_player/chorus_on.png new file mode 100644 index 0000000000000000000000000000000000000000..181385043e721b374817b7747267440c59c16e1c GIT binary patch literal 1735 zcmV;&1~~bNP)7Z+&w3*ol>Ty*^a0)%br8Rjq(L!SZC|`IskIo?Vx(#Rw2%WV78~a{%Yw zeOKC@Ha>*s9ycOj1cM-A5K~=YkR2={ARtd(eVzg!D2gaVt!5G3{*0pAaT*oXYL9KK zs%VTm6cq)n2n`Pn%kM8-zyXL6X71mo*Xv^x(wLCN1S<|Tk$&dELOtdL7l}?V2An!_ z9Hk3HD>yaeAtj9t3=}RsE8PWG|M0gt)juH`HVwLB~7m^<%Z7>IaXWB`_}{i+?O%CyTlk} z_iBwZ*L!^Oc8hLOFxDC5_|-P!t6O*@Ja}b*yT=A-XMILjGR}Us0zf;r%uh79chJyJ zN_IWAeCO{QTzYSi&AdliIKDMqk`$5aM{Cr&jw84FOfL1rMVKFQdg+avZc-rINniks zF=R=~hz{`4jf|h)Q%dK!vZq0jTFy@wjQJL49t-2`gxqH2NsX~~&f5#ZkTw}FNBGHw zB~H)xSQr~%t5M^JH#d3fQiqW^z^<*7H9KRYFlqO^Cly@G9v=|^y za~h_`&BB$0%u@hK7S1F0rVECn`f;NGOLS?hxpR6NEhrfUE=F|)#fxvIGHvQmzml$RH` zxX~=x*cB)n5b!J>9i;okV}>8~=nXZn2)kx`*iMgcF88=L*@V;}Qsut07UvS2aXSJM za^=SEx_nh zDI!X1L)qWyV2r`WNY4r+2ICA?EaEI36R|#G#n1?bL;rZdNIw!pSUSCzm~}7}53F%m zXRyxgC}2@juD(CQ!`Z0&t}k=;_PXfpHGciGNnIXn)A!+Njg;OKtn%;CjH!Oko&`hQ zXIOEhF`@1=T&1-vyi(`Z>osn_*KIK_n0VBdJ{w7 zV8aHk{rDt*_{B++0={#Xu zTFWFbdwP_08#w;)G*dS%d;i+wp9Q|a4-LsBnFQq=WLNf>(-xweorTkiI1y6{9wXDl; z-x}oX)h_4mb*O*YVKGxaKh&V(3T7M7X(*8h7`7V9SB*$7@pS8+-+j2B*Dij=_x?14 z7v=6)&h!U+X*c^MO=YGTD3XY`fu$+Sr&B4dJM*l(x`&JBr}V^U59GDq+@fb9pB@<0 z*^^Cz23l*L*DuUT8ij{7`1t6cE{z9*iIingb@{``POO|d_1&Rf>7^)2j2h}?Lgu#{ zqEm{Rhg4{U8X>P5qH_kPj#^A9GmknU36@5glSGS%p`S&{ng@xP;K)ixKP!k9Oi+@- zp@NpqLx5V>VZ9-s^pZYZSD;ZzyNB^$?yFG?SM)AQ{ zb^s-)5{Ot45|d$!5G7WHEd~)#iFBo331x&*O4c)vDCo90NT14Th|mG(D!Of!c5Fyb ziK?u|Ktzcl62rD#iIJ!Z+cHZ4x=+o|)>ijvYTdqb2j`r=(6~LFY@0rLVa$KSvSR>W deC~f8{{bJZJUjH~p!)y-002ovPDHLkV1l+yD186` literal 0 HcmV?d00001 diff --git a/plugins/gig_player/fileselect_off.png b/plugins/gig_player/fileselect_off.png new file mode 100644 index 0000000000000000000000000000000000000000..e95e10ba85b7a703c89c86c5aae102446d1e1425 GIT binary patch literal 936 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&N!3-otS@yI7Ddu7)&kzm{j@u9Y9{{(yEr+qAXP8FD1G)j8z}|`CgA{| z5LZn-11)_+Z6Go*(lIpFH8RmNHVuu6iB3q2OG=JUPEANjPe@HmNJ~#lO;1eANJ>vn z$;e90%udVBNzcyB0HPcq%FD_x$j-^nDJaY-EY2+|$tx<(D=Nt^Dk&%}FOAEt@QQ2p zOPg++vdON<$;;a#Ajr!<$S*X)KQt^LEHWf2DkL^OEG8~2CO$kiE<8RlA~7j4At@?8 zF*-3RIx#sWDJ4E7H9j>hJ}omTJu4|QJ2^cgIXyEa6NqwwE=kMDP0!9w&&|)s&CLX% zyv#fxD#*&q2RW>uAg7=>Hy|Mw=&ItaH(#kkwPAN&~PQH=PGmfwV;mcVO=+Z zI&O)#-I8p-CEavOuKvFGjE4+S55;CbbZL6Tnea$#_7jGb=L}gd7)oC#ZzgDk$BQ*DoL)#nIj&}^*@68%MFiif)Fy#}&l+O(9pBbip7MTB8 zwB`%L)GxyIUqtG@m^FQ6nEF+y`5VKuZ~SfF_~(BUX!!=@>NkJ0X!*|9@!h`v2kXoq zJoA2X&0V8Eak7A<24hqt_7!frf|mY18+fR+W>| z5z)=j)zM+itzkVlT~z0dPEHMDD~FOg$D_MyDP_ms3j@+Bc2J3#_oP^!d69ik zmczyU$gjrPcv<|^qyOe*$-5O!QIgM{eNE{0wcVOwB9nJ!|8l*q@$)z4*}Q$iB}#Za9I literal 0 HcmV?d00001 diff --git a/plugins/gig_player/fileselect_on.png b/plugins/gig_player/fileselect_on.png new file mode 100644 index 0000000000000000000000000000000000000000..9f735ae9605da19b64af864902dd1616998def28 GIT binary patch literal 1087 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&N!3-otS@yI7Ddu7)&kzm{j@u9Y9{{(yEr+qAXP8FD1G)j8z}|`rpf@H z5LZn-11&v6Z9OAxeM22xBOP619Rp(>Lt|ZC6J0|SJzWz$JySh>V?AS2eO*(1BQpbI zb3+|7Lp^gtLvuqT3nLv1V;xIlT}u-kD-#1NQ*A3#U29WAYjbTIOMN>l1AA*N2U{&i zJ547$eP;(v7e`H3M?F_p4G%XB&#e}e~y0H4@kminv&hF^$iGF>PLMKk~o;V|9`W&|zbKIvduxwlEw&1wx(z7bF z@2D&ebP11(NJ~sfPfAKpNzYA7$xhG7%S zi>N6}sI01~Z;WqgOloclX=;vYX^-#huxjmz?U~@$GaKCjFD_Cr*i&JT-Lc zG~>xL!luo1nl{sG>P(+$vjb<&37j?0W7d54Sxfcjt~Ok{OMTOB(@lF+Htkj2d_Za2 zaqT@P)i$40-*ZZ3>uI_DXJt2?lihzw>c}Ojqt}$zUuTNEWw838(4`m5i7)xnUWsSF z7A$ycy84}9$vcty_o6ND#aF$z?0xUj^WJmo2l2fhR5yQ;nEKgn(if?@UzGQJvEKMq zYVJ4T_1`4tebZR_U2@(J(Mdlf=l`(V^dn^XPk}iE-g?T^ZuKbjN&@-6r) zxbCmuhQA^k{z}jLD?R_O%=Eu9GylrW`YSW{uiUP`O6&hBullPw^{>OWf4mF+2`>C6 zxagng(ti@W{wc5jXEg7h@y>r{JO7#Q_-C>GpY7&MHy}s?(S?WKX+Ou z%VM+p>+dqTi_R~f&-QUeb9DQY9UV6N_IbPJTP~duq{qRs#7ot3>5CdIhX32-i}XWe SFY^Mu%;4$j=d#Wzp$PyCX#gw$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1c900e8ff047083fd924720c57cb6660cb3047eb GIT binary patch literal 3137 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 + * + * 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 0000000000000000000000000000000000000000..e77637bb4413bb31de9063f18c3610461ad9e9ef GIT binary patch literal 884 zcmbV}>uVEP6u_s}M_cF$KA;vsl!ex|n>LBCyI)9K{VWD+y)nM~%LnaQM` zwv<{Iil~Sp*eVFJ_(6QPE{h8KK@eY{P*fJ&`bGAGxVtE;g0EZu1P{M+4)=1naDI0h zMS82NYO5d!s)oZs4D>Z1f2b%2b^Y?|`#>u<18}UOqT8TsfSZ07=N;qWr6zH z61d!8g_2fO<1L3OaFF9T9#NG{hSE8YRuE=i@2rKOvWMvISZJ%KrJ>o|2x##&dHp~6 zwt4)mflw&e+l!!s*ibT>OvUlcPRcml)IF&vNWA>w#CkAQ5W9gYk zaIia8vth+8Z(n)$x``KG9^7}Y!N-D?Tj%C4UtKe>NbQ^wE?$1SEZY-&wfEGi`N_lk zBW`P#podmqt{FWWACddyM|_MDj8;VeH$`~MmLf4}W|XHq@% zc}42^(7Dd@``YQLnbu8r>;Anycr-Zo^x3AHuVm}QMC9>VrtA4k&FQr@xA7au#qHJS zQkz?*R~~3ARc+aFyo*@4kwXOJIzIv3u9O=&ITqJp9ChwE&V4!eo_p`yxo>md zQpYs6jdr(Si3O?IS}=k#DS8nqEQsots7MlYC~_Zw%KrHU*l9V5B7!iG*6o zrf94wN;Z>pm}UryX`w)r22qA30Z+0V$w3l|6cibPWB52DNQ@`}UI4rZ_&5+G77;~O zl(`@UX$i&^6b&Y1UXplOf{1<^!R7d0JajBC22p`?b40veWLT8d?{2)H9m{X&Lp4b)35TFcX`bFT<~u$^Ero`!r)g7Udqh4 zjsC$_Gl(}#{jaro+kFYWbkFar4!KKaT(87$RL!j#-uBZ4`@kGme`xXd-oC77{~@5w zFTxhI3iFz)yxx(&l-gw_PYyc!Cil+7y0MSZ?* zW4TlA{?g^Y3li8f-Y?(7`|9w_*DT@l!Q$h+ksA})iPwA1o&Hdf{>69a`hnrz$1g^T kUS7%c-roD?+{G@eyKCLsaKibntQRH2{581mjF;;923y~VJOBUy literal 0 HcmV?d00001 diff --git a/plugins/gig_player/reverb_off.png b/plugins/gig_player/reverb_off.png new file mode 100644 index 0000000000000000000000000000000000000000..83478c51b06e6a0c3574bcf7825ffff6b4fc4001 GIT binary patch literal 1439 zcmV;Q1z`G#P) zbCp)A@*$Nf6)8$vo(hG4F<=}B@%!$4+nL$hvk}lQQP(Utw>!7@o0;FtoLG7M&82PI zc{`PY|9eO&l_0b%^u}!+EsN=OaYKjhf*^oSxBJ1Oh40tbMYq=le>}eRazhCE2L?VS z1pYVF5quxYf^_&#+-CyoN&E>E1F7x;x@RyFWT+=emTC_|nI|gA=rqYk(z=s(#7{3? z02?9@=>Fj$42Po|ucvhr(Hf?jrZB-U$QYCjWtc%mvav>J>|_F8wI8)0bx2?u7XzOH zsAhmhLL}cc5{U#KWGSx$#1jej6penT1Kw!JU{ZD(63Gl$mWAL==;7X=-%|iXz_qz~ zI6pt-s}+lddj|Bn9T*MT#9|4VcM9_A{d=*$yDQ@1 z^B=FT1v#qV6eVHY+5B6Wgf$+ICP-xoM}(qGFZ5#;Xv31W4LQeQuWi41%`~%cpZ8KW zlq%(jV&uWOg#|d++k=J0Mb`CQ4}pAur)z6S*&$e_1-06oqK}wRi_Rz%9Vk|+P_9(u z%U^yLrb+o1W7?v@vRtjO^V+QzpOr^kdITZm7`g9{@gb9`qrxR1z%>HXMKDRMXyhT< zC(2y4+Z+J`L>~Hlc?C+P3hZre!e}srnQRtLPLGgkb$l*~x<0t>2zIu%B17u+d2pNp z=aoD1V9@J9v(XSlIeLcD-N1LXFIr-N^z8LIV5ie)bdjB@hwppShIs5nj2q~@ByS>j zp^rRYLZGxq2a}QMOa_W&9T6%7=m7NM{K5k`X&l4q!C8Qk6g4&LrS4k;oyk+A@zGq_(t8KcMss;af{U=d!u{1s~RbvDi^7MLDn zEENli&@F1=?~9dXSwY$nbzX1$A=ZERQBsc5yRx~QtX5~4TCVHL(`FN+=0qiLJPszw z?zx`igTo5QBb3vbG*+D=oHZI;J-P0WeXMFO&Jj;4D|ApYnZ&%x!<&r_c(}Nvs#^G? zvs~2GbL@GlT!+SxZI~laMC!cT|0n850Rr_sA3E39WT?P>sc{Gn!ZfuI76Sq~i1O_B-(E%3 znw_m-BvygSEmbMyI0?(EU&_tD{zTtAl9|j5MxY^(QrHGk@Nj7f6Tb>F@-`acx#~#z zL|I$=Hb3-y;kq7756S_H@&Gw&SyT+Lmsmt4x~ImdsAY!KGO2!X!&J?Z+)UKc4N>TZ zNJ21ah*2kxy0~tJ0eAf%xW6HuHcwz{dq;huP^FJr(KowAYaSjb6i?0`+6 zNbx`wRTNb5!ZTI8@y_4HQ@}u|q`**+1edWR`4(H2EXlg9R?=#BXQsOk52H(523{!o zWxnn{efg%(<&)WWuC%MwYO`9Y@c(YAS^zo1ay0R}%@LNbj>{`H0)!CQ?{+&8!1q7+ zK)MHAocFICZbZNc20_Firr5$D$5=!_K#op*odO^ziYNpvdI3GWjG~8j8Wh!Hjzg@f zXb8s?6$LE_wVEyY&8=HR0AhqM7Z(`}Qj7{^9}{Ve7zj3y#{nXUh^UZ|XhI|g5d&7q zBS%Dxvz}lT5y6Nd3Kok&B~WrP1_LNTAU6(Y9I=lz_6CoVTSsmjo&W(7XR)~jRdUJ6 ztiv1Sh&Di}R8pWIVg!;>iJ(w*C4Ts%%2YStM0qi6nSax=-t18#;XCs&=eBbofCPdl zf4$n`+Pw~+exu38cuKHJ$tS$^AkgTfteqd``e*Iqw^30RClj7tZgKVRdyMX-k{jXW zSVjj=x>UKanNxAX+IWrSH>xb1s$eP!V?DU~#ejwkQBF#Xga+^5->2eZ zM!N~uw}q|oCaaS}tW6Hlov2bX6)wF@MaAlFtERIrdh(OsZOe$S7iX>K5_`k?SY4~m z@XNn+&J9jiXiM( z6JE@Y&@bm`3@!>hnr`vkFLs#s6JUk$w&mhM#O*6nRJINn7-e^EicaHzV4$C5RMH4R z=!_4s_{|aetCRZk-#(K+WC;hgfC%h1%DOr?LT8Y$wfTrsYkjsR%jC9rm7w8-h;tre zEY?_zF$mBY1TL;b%&e}noH%;Jb*v2t!iAlb`X{RdQCy^a^5fI2U9MC6=PsugbL3Ku z>AP#(X+eJkY8}h4H+=n%3j|ZR)}y&5`!h8FYWt2e4^yTdFUgZ~jvtDMtVdLs-t0;J z;}?wd9or?t_RJ^}?-QAb;_mrZiR6e8#2UmH1Yx$5^Jcfh#eU4MXIi8sN20~zt+HX| z^bmOjK304J{W#;!M8x#MI_}J6uDpECM>kH9RTadLin2L5Mp|;HQTh{AOjP`|3maV- zY^$b7z@Hu;T+c;6S=9+|3 zim@;-%Epb;>@<6*fVY9#fkA=$Gh@1T<1`C*Hu>Q0imcCF)NUi7K)YGi#cwrZx;l`IAz$pd3A;f+;c zYz3+V3r5+Qig;cL+eP&+T`I z`N8KqXz+A~B4#%-&V9UGkQVs;_r_VBE72=w+`lwRGQY+1n^UBT#|Weqp`9q#KAJx+ zXse7`)BpapO<7oyzbOvo!&T*>03`}mxa&H4e8@=H%R03 zxR&I3@iH>|&XxA{>(`ot+)0+@ML~24-gxpT5Lrt}VrrDh6Qx2-#g~YTL5xLWAuZ>) z1nglkO?*sR@wiB_Mu?oDGDxs0MO_vlj~rR-iJehr0$Joi6_Z1pTGBWtN(Gx))GN+- z^2m|K4waLFxVE~&{KA7mqNM!+t&vfjcZD3FkOdV4D}oqHFhVSXH_Cxzh;STJ27`v; zl0EC`n?cdfzyWDcy7oj@sjA{3_!AitRMFrGsstYh>7iT+fuIU56q^lrrS*FK6W3aJ z_>d@yj%mIo+)g^hSL8q1{5vd1_`j_CU)#R`42nog)|Ogi00000NkvXXu0mjf$yYB5 literal 0 HcmV?d00001 From a18061bdab4a5764cf16a778b2d446789116a3da Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 29 Apr 2014 22:53:50 -0700 Subject: [PATCH 02/33] GIG Player now uses correct velocity and changes sample rate properly Fixed crashes when loading invalid file, but this required exceptions since libgig throws exceptions, hence the changes to CMakeLists.txt. If the sample rate is changed (e.g. on exporting at 48k when the samples are at 44.1k), when playing the note convert each note individually to the correct sample rate instead of converting the output from the notes to the correct sample rate. Save the instrument so we don't have to search the entire GIG file every time we play a note. This should make it work better on computers that don't have tons of RAM. Currently there is a 0.5 second fade out time when the note is released. I still have to implement the release-triggered samples. --- plugins/gig_player/CMakeLists.txt | 5 + plugins/gig_player/gig_player.cpp | 487 +++++++++++++++++++----------- plugins/gig_player/gig_player.h | 23 +- 3 files changed, 341 insertions(+), 174 deletions(-) diff --git a/plugins/gig_player/CMakeLists.txt b/plugins/gig_player/CMakeLists.txt index 7e32e5340..f808f15fb 100644 --- a/plugins/gig_player/CMakeLists.txt +++ b/plugins/gig_player/CMakeLists.txt @@ -1,6 +1,11 @@ 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 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") diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index cf5df5edf..15dc4616a 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -27,8 +27,6 @@ #include #include #include -#include // sin, cos, ... -#include // memset #include "FileDialog.h" #include "gig_player.h" @@ -84,6 +82,8 @@ gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &gigplayer_plugin_descriptor ), m_srcState( NULL ), m_instance( NULL ), + m_instrument( NULL ), + m_RandomSeed( 0 ), m_filename( "" ), m_lastMidiPitch( -1 ), m_lastMidiPitchRange( -1 ), @@ -95,12 +95,11 @@ gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : InstrumentPlayHandle * iph = new InstrumentPlayHandle( this ); 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() ) ); - - // Gain - //connect( &m_gain, SIGNAL( dataChanged() ), this, SLOT( updateGain() ) ); + connect( engine::mixer(), SIGNAL( sampleRateChanged() ), this, SLOT( updateSampleRate() ) ); } @@ -151,6 +150,7 @@ void gigInstrument::loadFile( const QString & _file ) { openFile( _file, false ); updatePatch(); + updateSampleRate(); } } @@ -195,7 +195,6 @@ void gigInstrument::freeInstance() { qDebug() << "Really deleting " << m_filename; - // Need we do more to delete the gig instance? s_instances.remove( m_filename ); delete m_instance; } @@ -216,6 +215,7 @@ void gigInstrument::freeInstance() void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) { emit fileLoading(); + bool succeeded = false; // Used for loading file char * gigAscii = qstrdup( qPrintable( SampleBuffer::tryToMakeAbsolute( _gigFile ) ) ); @@ -240,19 +240,31 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) // 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 ); + try + { + // Grab this sf from the top of the stack and add to list + m_instance = new gigInstance( _gigFile ); + s_instances.insert( relativePath, m_instance ); + succeeded = true; + } + catch( ... ) + { + m_instance = NULL; + succeeded = false; + } } s_instancesMutex.unlock(); m_synthMutex.unlock(); - // TODO: only do this if successfully loaded - if( true ) + if( succeeded ) { m_filename = relativePath; - + emit fileChanged(); + } + else + { + m_filename = ""; emit fileChanged(); } @@ -261,6 +273,7 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) if( updateTrackName ) { instrumentTrack()->setName( QFileInfo( _gigFile ).baseName() ); + updatePatch(); } } @@ -270,11 +283,7 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) 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() ); - } + getInstrument(); } @@ -282,21 +291,24 @@ void gigInstrument::updatePatch() QString gigInstrument::getCurrentPatchName() { - if (!m_instance) + if( !m_instance ) return ""; int iBankSelected = m_bankNum.value(); int iProgSelected = m_patchNum.value(); gig::Instrument* pInstrument = m_instance->gig.GetFirstInstrument(); - while (pInstrument) { + + while( pInstrument ) + { int iBank = pInstrument->MIDIBank; int iProg = pInstrument->MIDIProgram; - if (iBank == iBankSelected && iProg == iProgSelected) { + if( iBank == iBankSelected && iProg == iProgSelected ) + { QString name = QString::fromStdString(pInstrument->pInfo->Name); - if (name == "") + if( name == "" ) name = ""; return name; @@ -330,144 +342,47 @@ void gigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) 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) + if( m_instance ) { - // Find instrument - int iBankSelected = m_bankNum.value(); - int iProgSelected = m_patchNum.value(); + if( !m_instrument ) + getInstrument(); - 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) + // Find the sample we want to play (based on velocity, etc.) + if( m_instrument ) { - gig::Region* pRegion = pInstrument->GetFirstRegion(); + gig::Region* pRegion = m_instrument->GetFirstRegion(); - while (pRegion) { - gig::Sample* pSample = pRegion->GetSample(); + while( pRegion ) + { + const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); + const uint velocity = _n->midiVelocity( baseVelocity ); - if (pSample) + Dimension dim = getDimensions( pRegion, velocity, false ); + gig::DimensionRegion* pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); + gig::Sample* pSample = pDimRegion->pSample; + + 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()) + if( midiNote >= keyLow && midiNote <= keyHigh ) { - 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"; - } - } + gigNote note = sampleToNote(pSample, midiNote); m_synthMutex.lock(); m_notes.push_back(note); m_synthMutex.unlock(); - - pSample->ReleaseSampleData(); } } - pRegion = pInstrument->GetNextRegion(); + pRegion = m_instrument->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; - }*/ } @@ -482,31 +397,23 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // Initialize to zeros for (int i = 0; i < frames; ++i) { - _working_buffer[i][0] = float(); - _working_buffer[i][1] = float(); + _working_buffer[i][0] = 0; + _working_buffer[i][1] = 0; } m_synthMutex.lock(); - const int currentMidiPitch = instrumentTrack()->midiPitch(); + /*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 ); - } + m_lastMidiPitchRange = currentMidiPitchRange;*/ - if( m_internalSampleRate < engine::mixer()->processingSampleRate() && + /*if( m_internalSampleRate < engine::mixer()->processingSampleRate() && m_srcState != NULL ) { + const fpp_t frames = engine::mixer()->framesPerPeriod(); const fpp_t f = frames * m_internalSampleRate / engine::mixer()->processingSampleRate(); #ifdef __GNUC__ sampleFrame tmp[f]; @@ -514,7 +421,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) sampleFrame * tmp = new sampleFrame[f]; #endif - qDebug() << "Different internal sample rate"; + qDebug() << "Warning: playing at different sample rate"; // TODO: get sample, interleave left/right channels SRC_DATA src_data; @@ -538,10 +445,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) } } 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 ) @@ -554,7 +458,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) } } } - } + //} m_synthMutex.unlock(); @@ -566,19 +470,19 @@ void gigInstrument::play( sampleFrame * _working_buffer ) void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { - //GIGPluginData * pluginData = static_cast( _n->m_pluginData ); + GIGPluginData * pluginData = static_cast( _n->m_pluginData ); m_synthMutex.lock(); - // Delete ended notes. Continue looping starting at the beginning. Erasing + // Continue looping starting at the beginning. Erasing // invalidates the iterator. bool deleted = false; + // Delete ended notes do { deleted = false; - std::list::iterator i = m_notes.begin(); - while( i != m_notes.end() ) + for( std::list::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) { if( i->position >= i->size ) { @@ -586,15 +490,44 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) deleted = true; break; } - - ++i; } } while( deleted ); + // TODO: instead of fading out, use getDimensions(region, velocity, release = true) + // to see if we should create a new "note" for the release of the note, e.g. + // the piano releasing a key sound + + // Fade out the note we want to end + for( std::list::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) + { + if( i->midiNote == pluginData->midiNote ) + { + float fadeOut = 0.5; // Seconds + int len = std::min( int( floor( fadeOut * engine::mixer()->processingSampleRate() ) ), + i->size-i->position ); + int endPoint = i->position+len; + + if (len <= 0) + break; + + for( int k = i->position, j = 0; k < endPoint; ++k, ++j ) + { + i->note[k][0] *= 1.0-1.0/len*j; + i->note[k][1] *= 1.0-1.0/len*j; + } + + for( int k = endPoint; k < i->size; ++k ) + { + i->note[k][0] = 0; + i->note[k][1] = 0; + } + } + } + m_synthMutex.unlock(); - //delete pluginData; + delete pluginData; } @@ -608,6 +541,225 @@ PluginView * gigInstrument::instantiateView( QWidget * _parent ) +Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool release ) +{ + Dimension dim; + + if( !pRegion ) + return dim; + + for( int i = pRegion->Dimensions - 1; i >= 0; --i ) + { + switch( pRegion->pDimensionDefinitions[i].dimension ) + { + case gig::dimension_layer: + //DimValues[i] = iLayer; + dim.DimValues[i] = 0; + break; + case gig::dimension_velocity: + dim.DimValues[i] = velocity; + break; + case gig::dimension_releasetrigger: + //VoiceType = (ReleaseTriggerVoice) ? Voice::type_release_trigger : (!iLayer) ? Voice::type_release_trigger_required : Voice::type_normal; + dim.DimValues[i] = (uint) release; + break; + case gig::dimension_keyboard: + //dim.DimValues[i] = (uint) (pEngineChannel->CurrentKeyDimension * pRegion->pDimensionDefinitions[i].zones); + dim.DimValues[i] = 0; + // TODO: may be useful? + break; + case gig::dimension_roundrobin: + case gig::dimension_roundrobinkeyboard: + //dim.DimValues[i] = (uint) pEngineChannel->pMIDIKeyInfo[MIDIKey].RoundRobinIndex; // incremented for each note on + dim.DimValues[i] = 0; + // TODO: implement this + break; + case gig::dimension_random: + m_RandomSeed = m_RandomSeed * 1103515245 + 12345; // classic pseudo random number generator + dim.DimValues[i] = (uint) m_RandomSeed >> (32 - pRegion->pDimensionDefinitions[i].bits); // highest bits are most random + 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: + dim.DimValues[i] = 0; + break; + case gig::dimension_none: + qDebug() << "Error: dimension=none"; + break; + default: + qDebug() << "Error: Unknown dimension"; + } + } + + return dim; +} + + + + +gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote ) +{ + if( !pSample ) + return gigNote(); + + gig::buffer_t buf = pSample->LoadSampleData(); + gigNote note( pSample->SamplesTotal ); + note.midiNote = midiNote; + + // 24 Bit + // TODO: implement this + 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);*/ + } + // 16 Bit + 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"; + } + } + + pSample->ReleaseSampleData(); + + // Convert sample rate + int sampleRate = pSample->SamplesPerSecond; + int outputRate = engine::mixer()->processingSampleRate(); + if( sampleRate != outputRate ) + { + qDebug() << "Warning: converting sample rate"; + gigNote converted = convertSampleRate( note, sampleRate, outputRate ); + return converted; + } + else + { + return note; + } +} + + + + +void gigInstrument::getInstrument() +{ + // Find instrument + int iBankSelected = m_bankNum.value(); + int iProgSelected = m_patchNum.value(); + + if( m_instance ) + { + 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(); + } + + m_instrument = pInstrument; + } +} + + + + +gigNote gigInstrument::convertSampleRate( gigNote& old, int oldRate, int newRate ) +{ + gigNote note( ceil( (double)old.size*newRate/oldRate ) ); + note.midiNote = old.midiNote; + + SRC_DATA src_data; + src_data.data_in = old.note[0]; + src_data.data_out = note.note[0]; + src_data.input_frames = old.size; + src_data.output_frames = note.size; + src_data.src_ratio = (double) note.size / old.size; + src_data.end_of_input = 0; + int error = src_process( m_srcState, &src_data ); + + if( error ) + { + qCritical( "gigInstrument: error while resampling: %s", src_strerror( error ) ); + } + + if( src_data.output_frames_gen > note.size ) + { + qCritical( "gigInstrument: not enough frames: %ld / %d", src_data.output_frames_gen, old.size ); + } + + return note; +} + + + + +void gigInstrument::updateSampleRate() +{ + m_synthMutex.lock(); + + if( m_srcState != NULL ) + src_delete( m_srcState ); + + int error; + m_srcState = src_new( engine::mixer()->currentQualitySettings().libsrcInterpolation(), DEFAULT_CHANNELS, &error ); + + if( m_srcState == NULL || error ) + qCritical( "error while creating libsamplerate data structure in gigInstrument::updateSampleRate()" ); + + m_synthMutex.unlock(); +} + + @@ -615,7 +767,6 @@ class gigKnob : public knob { public: gigKnob( QWidget * _parent ) : -// knob( knobStyled, _parent ) knob( knobBright_26, _parent ) { setFixedSize( 31, 38 ); @@ -703,13 +854,10 @@ void gigInstrumentView::modelChanged() m_gainKnob->setModel( &k->m_gain ); - connect( k, SIGNAL( fileChanged() ), this, SLOT( updateFilename() ) ); - connect( k, SIGNAL( fileLoading() ), this, SLOT( invalidateFile() ) ); updateFilename(); - } @@ -727,7 +875,6 @@ void gigInstrumentView::updateFilename() m_patchDialogButton->setEnabled( !i->m_filename.isEmpty() ); updatePatchName(); - update(); } @@ -741,7 +888,6 @@ void gigInstrumentView::updatePatchName() QString patch = i->getCurrentPatchName(); m_patchLabel->setText( fm.elidedText( patch, Qt::ElideLeft, m_patchLabel->width() ) ); - update(); } @@ -807,7 +953,6 @@ void gigInstrumentView::showFileDialog() 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 ); diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index c18991abe..1ee6c0cc4 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -65,6 +65,19 @@ public: +struct Dimension +{ + Dimension() + { + for( int i = 0; i < 8; ++i) + DimValues[i] = 0; + } + + uint DimValues[8]; +}; + + + class gigNote { public: @@ -166,6 +179,7 @@ public: public slots: void openFile( const QString & _gigFile, bool updateTrackName = true ); void updatePatch(); + void updateSampleRate(); private: @@ -176,12 +190,11 @@ private: SRC_STATE * m_srcState; gigInstance* m_instance; + gig::Instrument* m_instrument; + uint32_t m_RandomSeed; 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; @@ -198,6 +211,10 @@ private: private: void freeInstance(); + void getInstrument(); + gigNote sampleToNote( gig::Sample* pSample, int midiNote ); + Dimension getDimensions( gig::Region* pRegion, int velocity, bool release ); + gigNote convertSampleRate( gigNote& old, int oldRate, int newRate ); friend class gigInstrumentView; From cc59313ad905002e3d0785db66323ce0f324df47 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 1 May 2014 08:19:32 -0700 Subject: [PATCH 03/33] Release samples, move constructors, attenuation An initial implementation of playing release samples on key up. It seems a bit messy at the moment. Wrote a move constructor since there's quite a bit of passing around the gigNote class. Use attenuation so that notes set their gain properly based on the information in the GIG file, quite noticeable on some release samples. Note that to support move constructors I enabled building the plugin with the C++11 standard. --- plugins/gig_player/CMakeLists.txt | 2 +- plugins/gig_player/gig_player.cpp | 244 ++++++++++++++++++------------ plugins/gig_player/gig_player.h | 55 ++----- 3 files changed, 165 insertions(+), 136 deletions(-) diff --git a/plugins/gig_player/CMakeLists.txt b/plugins/gig_player/CMakeLists.txt index f808f15fb..b989479f5 100644 --- a/plugins/gig_player/CMakeLists.txt +++ b/plugins/gig_player/CMakeLists.txt @@ -3,7 +3,7 @@ if(LMMS_HAVE_GIG) INCLUDE_DIRECTORIES(${GIG_INCLUDE_DIRS}{) # Required for not crashing loading files with libgig - SET(GCC_COVERAGE_COMPILE_FLAGS "-fexceptions") + SET(GCC_COVERAGE_COMPILE_FLAGS "-fexceptions -std=c++11") add_definitions(${GCC_COVERAGE_COMPILE_FLAGS}) LINK_DIRECTORIES(${GIG_LIBRARY_DIRS}) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 15dc4616a..071911fb4 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -349,38 +349,9 @@ void gigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) if( !m_instrument ) getInstrument(); - // Find the sample we want to play (based on velocity, etc.) - if( m_instrument ) - { - gig::Region* pRegion = m_instrument->GetFirstRegion(); - - while( pRegion ) - { - const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); - const uint velocity = _n->midiVelocity( baseVelocity ); - - Dimension dim = getDimensions( pRegion, velocity, false ); - gig::DimensionRegion* pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); - gig::Sample* pSample = pDimRegion->pSample; - - if( pSample ) - { - int keyLow = pRegion->KeyRange.low; - int keyHigh = pRegion->KeyRange.high; - - if( midiNote >= keyLow && midiNote <= keyHigh ) - { - gigNote note = sampleToNote(pSample, midiNote); - - m_synthMutex.lock(); - m_notes.push_back(note); - m_synthMutex.unlock(); - } - } - - pRegion = m_instrument->GetNextRegion(); - } - } + const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); + const uint velocity = _n->midiVelocity( baseVelocity ); + addNotes( midiNote, velocity, false ); } } } @@ -403,62 +374,18 @@ void gigInstrument::play( sampleFrame * _working_buffer ) m_synthMutex.lock(); - /*const int currentMidiPitch = instrumentTrack()->midiPitch(); - if( m_lastMidiPitch != currentMidiPitch ) - m_lastMidiPitch = currentMidiPitch; - const int currentMidiPitchRange = instrumentTrack()->midiPitchRange(); - if( m_lastMidiPitchRange != currentMidiPitchRange ) - m_lastMidiPitchRange = currentMidiPitchRange;*/ - - /*if( m_internalSampleRate < engine::mixer()->processingSampleRate() && - m_srcState != NULL ) + for( int i = 0; i < frames; ++i ) { - const fpp_t frames = engine::mixer()->framesPerPeriod(); - const fpp_t f = frames * m_internalSampleRate / engine::mixer()->processingSampleRate(); -#ifdef __GNUC__ - sampleFrame tmp[f]; -#else - sampleFrame * tmp = new sampleFrame[f]; -#endif - - qDebug() << "Warning: playing at different 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 ) + for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { - 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 - {*/ - 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 ) { - 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; - } + _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(); @@ -494,15 +421,17 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) } while( deleted ); - // TODO: instead of fading out, use getDimensions(region, velocity, release = true) - // to see if we should create a new "note" for the release of the note, e.g. - // the piano releasing a key sound + // Is the note supposed to have a release sample played? + bool noteRelease = false; // Fade out the note we want to end + // TODO: implement proper ADSR from GIG specs for( std::list::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) { if( i->midiNote == pluginData->midiNote ) { + noteRelease = i->release; + float fadeOut = 0.5; // Seconds int len = std::min( int( floor( fadeOut * engine::mixer()->processingSampleRate() ) ), i->size-i->position ); @@ -527,6 +456,14 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) m_synthMutex.unlock(); + if (noteRelease) + { + // Add the release notes if available + const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); + const uint velocity = _n->midiVelocity( baseVelocity ); + addNotes( pluginData->midiNote , velocity, true ); + } + delete pluginData; } @@ -561,6 +498,7 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool break; case gig::dimension_releasetrigger: //VoiceType = (ReleaseTriggerVoice) ? Voice::type_release_trigger : (!iLayer) ? Voice::type_release_trigger_required : Voice::type_normal; + dim.release = true; dim.DimValues[i] = (uint) release; break; case gig::dimension_keyboard: @@ -603,13 +541,12 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool case gig::dimension_effect3depth: case gig::dimension_effect4depth: case gig::dimension_effect5depth: - dim.DimValues[i] = 0; - break; case gig::dimension_none: - qDebug() << "Error: dimension=none"; + dim.DimValues[i] = 0; break; default: qDebug() << "Error: Unknown dimension"; + dim.DimValues[i] = 0; } } @@ -619,13 +556,13 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool -gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote ) +gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, float attenuation, bool release ) { if( !pSample ) return gigNote(); gig::buffer_t buf = pSample->LoadSampleData(); - gigNote note( pSample->SamplesTotal ); + gigNote note( pSample->SamplesTotal, release ); note.midiNote = midiNote; // 24 Bit @@ -651,12 +588,12 @@ gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote ) { for( int i = 0; i < pSample->SamplesTotal/pSample->Channels; ++i ) { - note.note[i][0] = 1.0/0x10000*pInt[pSample->Channels*i]; + note.note[i][0] = 1.0/0x10000*pInt[pSample->Channels*i] * attenuation; 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]; + note.note[i][1] = 1.0/0x10000*pInt[pSample->Channels*i+1] * attenuation; } } else @@ -715,7 +652,7 @@ void gigInstrument::getInstrument() gigNote gigInstrument::convertSampleRate( gigNote& old, int oldRate, int newRate ) { - gigNote note( ceil( (double)old.size*newRate/oldRate ) ); + gigNote note( ceil( (double)old.size*newRate/oldRate ), old.release ); note.midiNote = old.midiNote; SRC_DATA src_data; @@ -761,6 +698,50 @@ void gigInstrument::updateSampleRate() +// Find the sample we want to play (based on velocity, etc.) +void gigInstrument::addNotes( int midiNote, int velocity, bool release ) +{ + if( m_instrument ) + { + gig::Region* pRegion = m_instrument->GetFirstRegion(); + + while( pRegion ) + { + Dimension dim = getDimensions( pRegion, velocity, release ); + gig::DimensionRegion* pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); + gig::Sample* pSample = pDimRegion->pSample; + + if( pSample && pDimRegion->pSample->SamplesTotal != 0 ) + { + int keyLow = pRegion->KeyRange.low; + int keyHigh = pRegion->KeyRange.high; + + if( midiNote >= keyLow && midiNote <= keyHigh ) + { + float attenuation; + float length = (double) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); + + if (release) + attenuation = 1 - 0.01053 * (256 >> pDimRegion->ReleaseTriggerDecay) * length; + else + attenuation = pDimRegion->SampleAttenuation; + + gigNote note = sampleToNote( pSample, midiNote, + attenuation, dim.release ); + + m_synthMutex.lock(); + m_notes.push_back(note); + m_synthMutex.unlock(); + } + } + + pRegion = m_instrument->GetNextRegion(); + } + } +} + + + class gigKnob : public knob @@ -961,6 +942,81 @@ void gigInstrumentView::showPatchDialog() +gigNote::gigNote() : + position( 0 ), + midiNote( -1 ), + note( NULL ), + size( 0 ), + release( false ) +{ +} + +gigNote::gigNote(int size, bool release ) : + position( 0 ), + midiNote( -1 ), + size( size ), + release( release ) +{ + note = new sampleFrame[size]; + + // Initialize to no sound + for (int i = 0; i < size; ++i) + { + note[i][0] = float(); + note[i][1] = float(); + } +} + +gigNote::gigNote( const gigNote& g ) : + position( g.position ), + midiNote( g.midiNote ), + note( NULL ), + size( g.size ), + release( g.release ) +{ + if (size > 0) + { + note = new sampleFrame[size]; + std::copy(&g.note[0], &g.note[size], ¬e[0]); + } +} + +// Move constructor +gigNote::gigNote( gigNote&& g ) : + position( g.position ), + midiNote( g.midiNote ), + note( NULL ), + size( 0 ), + release( g.release ) +{ + *this = std::move( g ); +} + +// Move assignment +gigNote& gigNote::operator=( gigNote&& g ) +{ + if( this != &g ) + { + if( note ) + delete[] note; + + size = g.size; + note = g.note; + + g.size = 0; + g.note = NULL; + } + + return *this; +} + +gigNote::~gigNote() +{ + if (note) + delete[] note; +} + + extern "C" { diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index 1ee6c0cc4..00285a26f 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -67,13 +67,15 @@ public: struct Dimension { - Dimension() + Dimension() : + release( false ) { for( int i = 0; i < 8; ++i) DimValues[i] = 0; } uint DimValues[8]; + bool release; }; @@ -81,52 +83,22 @@ struct Dimension class gigNote { public: - gigNote() : - position( 0 ), - midiNote( -1 ), - note( NULL ), - size( 0 ) - { - } + gigNote(); + gigNote(int size, bool release ); + gigNote( const gigNote& g ); + ~gigNote(); - gigNote(int size) : - position( 0 ), - midiNote( -1 ), - size( size ) - { - note = new sampleFrame[size]; + // Move constructor + gigNote( gigNote&& g ); - // 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; - } + // Move assignment + gigNote& operator=( gigNote&& g ); int position; int midiNote; sampleFrame* note; int size; // Don't try changing this... + bool release; // Whether to trigger a release sample on key up }; @@ -212,9 +184,10 @@ private: private: void freeInstance(); void getInstrument(); - gigNote sampleToNote( gig::Sample* pSample, int midiNote ); + gigNote sampleToNote( gig::Sample* pSample, int midiNote, float attenuation, bool release ); Dimension getDimensions( gig::Region* pRegion, int velocity, bool release ); gigNote convertSampleRate( gigNote& old, int oldRate, int newRate ); + void addNotes( int midiNote, int velocity, bool release ); // Locks m_synthMutex internally friend class gigInstrumentView; From f544caf18b936803cf1e2a84fcc5096c2c2d96c0 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 2 May 2014 14:51:19 -0700 Subject: [PATCH 04/33] Key changing dimension, 24-bit samples, locking Support for 24-bit samples. It just so happens that the only GIG file I have that is 24-bit also has the key changing dimension set, so I implemented that so I could compare the output sound with that of Linux Sampler. It still sounds different, brighter in LMMS. Not sure if that has to do with gain, ADSR, or incorrect 24-bit decoding. I'm guessing ADSR. There were many crashing issues when playing notes when changing patches/instruments. More locking, less crashing. However, this also means that it is quite slow when playing a lot of notes with large samples and converting sample rates. Linux Sampler amazingly can handle 36 or so keys being pressed at once whereas LMMS nowhere near that many at the moment. --- plugins/gig_player/gig_player.cpp | 136 +++++++++++++++++------------- plugins/gig_player/gig_player.h | 2 + 2 files changed, 81 insertions(+), 57 deletions(-) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 071911fb4..2a334c1eb 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -84,6 +84,7 @@ gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : m_instance( NULL ), m_instrument( NULL ), m_RandomSeed( 0 ), + m_currentKeyDimension( 0 ), m_filename( "" ), m_lastMidiPitch( -1 ), m_lastMidiPitchRange( -1 ), @@ -193,20 +194,14 @@ void gigInstrument::freeInstance() // No more references if( m_instance->refCount <= 0 ) { - qDebug() << "Really deleting " << m_filename; - s_instances.remove( m_filename ); delete m_instance; } - // Just remove our reference - else - { - qDebug() << "un-referencing " << m_filename; - } - s_instancesMutex.unlock(); + s_instancesMutex.unlock(); m_instance = NULL; } + m_synthMutex.unlock(); } @@ -230,10 +225,7 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) // Increment Reference if( s_instances.contains( relativePath ) ) { - qDebug() << "Using existing reference to " << relativePath; - m_instance = s_instances[ relativePath ]; - m_instance->refCount++; } @@ -258,15 +250,11 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) m_synthMutex.unlock(); if( succeeded ) - { m_filename = relativePath; - emit fileChanged(); - } else - { m_filename = ""; - emit fileChanged(); - } + + emit fileChanged(); delete[] gigAscii; @@ -291,8 +279,13 @@ void gigInstrument::updatePatch() QString gigInstrument::getCurrentPatchName() { + m_synthMutex.lock(); + if( !m_instance ) + { + m_synthMutex.unlock(); return ""; + } int iBankSelected = m_bankNum.value(); int iProgSelected = m_patchNum.value(); @@ -311,12 +304,14 @@ QString gigInstrument::getCurrentPatchName() if( name == "" ) name = ""; + m_synthMutex.unlock(); return name; } pInstrument = m_instance->gig.GetNextInstrument(); } + m_synthMutex.unlock(); return ""; } @@ -366,7 +361,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) const fpp_t frames = engine::mixer()->framesPerPeriod(); // Initialize to zeros - for (int i = 0; i < frames; ++i) + for( int i = 0; i < frames; ++i ) { _working_buffer[i][0] = 0; _working_buffer[i][1] = 0; @@ -432,7 +427,7 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { noteRelease = i->release; - float fadeOut = 0.5; // Seconds + float fadeOut = 0.25; // Seconds int len = std::min( int( floor( fadeOut * engine::mixer()->processingSampleRate() ) ), i->size-i->position ); int endPoint = i->position+len; @@ -456,7 +451,7 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) m_synthMutex.unlock(); - if (noteRelease) + if( noteRelease ) { // Add the release notes if available const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); @@ -497,14 +492,11 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool dim.DimValues[i] = velocity; break; case gig::dimension_releasetrigger: - //VoiceType = (ReleaseTriggerVoice) ? Voice::type_release_trigger : (!iLayer) ? Voice::type_release_trigger_required : Voice::type_normal; dim.release = true; dim.DimValues[i] = (uint) release; break; case gig::dimension_keyboard: - //dim.DimValues[i] = (uint) (pEngineChannel->CurrentKeyDimension * pRegion->pDimensionDefinitions[i].zones); - dim.DimValues[i] = 0; - // TODO: may be useful? + dim.DimValues[i] = (uint) (m_currentKeyDimension * pRegion->pDimensionDefinitions[i].zones); break; case gig::dimension_roundrobin: case gig::dimension_roundrobinkeyboard: @@ -558,47 +550,56 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, float attenuation, bool release ) { - if( !pSample ) + if( !pSample || pSample->Channels == 0 ) return gigNote(); gig::buffer_t buf = pSample->LoadSampleData(); gigNote note( pSample->SamplesTotal, release ); note.midiNote = midiNote; + if( pSample->Channels > 2 ) + qDebug() << "Warning: only using first two channels of GIG file"; + // 24 Bit - // TODO: implement this if( pSample->BitDepth == 24 ) { - qDebug() << "Error: not 16 bit... not implemented"; - /*int n = pSample->SamplesTotal * pSample->Channels; + uint8_t* pInt = static_cast( buf.pStart ); - 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);*/ + for( int i = 0; i < pSample->SamplesTotal/pSample->Channels; ++i ) + { + int32_t valueLeft = (pInt[3*pSample->Channels*i]<<8) | + (pInt[3*pSample->Channels*i+1]<<16) | + (pInt[3*pSample->Channels*i+2]<<24); + + note.note[i][0] = 1.0/0x100000000*attenuation*valueLeft; + + if( pSample->Channels == 1 ) + { + note.note[i][1] = note.note[i][0]; + } + else + { + int32_t valueRight = (pInt[3*pSample->Channels*i+3]<<8) | + (pInt[3*pSample->Channels*i+4]<<16) | + (pInt[3*pSample->Channels*i+5]<<24); + + note.note[i][1] = 1.0/0x100000000*attenuation*valueRight; + } + } } // 16 Bit else { - int16_t* pInt = reinterpret_cast( buf.pStart ); + int16_t* pInt = static_cast( buf.pStart ); - if( pSample->Channels <= 2 ) + for( int i = 0; i < pSample->SamplesTotal/pSample->Channels; ++i ) { - for( int i = 0; i < pSample->SamplesTotal/pSample->Channels; ++i ) - { - note.note[i][0] = 1.0/0x10000*pInt[pSample->Channels*i] * attenuation; + note.note[i][0] = 1.0/0x10000*pInt[pSample->Channels*i] * attenuation; - 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] * attenuation; - } - } - else - { - qDebug() << "Error: not stereo... not implemented"; + 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] * attenuation; } } @@ -609,7 +610,6 @@ gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, float a int outputRate = engine::mixer()->processingSampleRate(); if( sampleRate != outputRate ) { - qDebug() << "Warning: converting sample rate"; gigNote converted = convertSampleRate( note, sampleRate, outputRate ); return converted; } @@ -628,6 +628,8 @@ void gigInstrument::getInstrument() int iBankSelected = m_bankNum.value(); int iProgSelected = m_patchNum.value(); + m_synthMutex.lock(); + if( m_instance ) { gig::Instrument* pInstrument = m_instance->gig.GetFirstInstrument(); @@ -645,6 +647,8 @@ void gigInstrument::getInstrument() m_instrument = pInstrument; } + + m_synthMutex.unlock(); } @@ -662,7 +666,10 @@ gigNote gigInstrument::convertSampleRate( gigNote& old, int oldRate, int newRate src_data.output_frames = note.size; src_data.src_ratio = (double) note.size / old.size; src_data.end_of_input = 0; + + m_srcMutex.lock(); int error = src_process( m_srcState, &src_data ); + m_srcMutex.unlock(); if( error ) { @@ -682,7 +689,7 @@ gigNote gigInstrument::convertSampleRate( gigNote& old, int oldRate, int newRate void gigInstrument::updateSampleRate() { - m_synthMutex.lock(); + m_srcMutex.lock(); if( m_srcState != NULL ) src_delete( m_srcState ); @@ -693,7 +700,7 @@ void gigInstrument::updateSampleRate() if( m_srcState == NULL || error ) qCritical( "error while creating libsamplerate data structure in gigInstrument::updateSampleRate()" ); - m_synthMutex.unlock(); + m_srcMutex.unlock(); } @@ -701,10 +708,21 @@ void gigInstrument::updateSampleRate() // Find the sample we want to play (based on velocity, etc.) void gigInstrument::addNotes( int midiNote, int velocity, bool release ) { + m_synthMutex.lock(); + if( m_instrument ) { gig::Region* pRegion = m_instrument->GetFirstRegion(); + // Change key dimension, e.g. change samples based on what key is pressed + // in a certain range. + if( midiNote >= m_instrument->DimensionKeyRange.low && midiNote + <= m_instrument->DimensionKeyRange.high ) + m_currentKeyDimension = float( midiNote - + m_instrument->DimensionKeyRange.low ) / ( + m_instrument->DimensionKeyRange.high - + m_instrument->DimensionKeyRange.low + 1 ); + while( pRegion ) { Dimension dim = getDimensions( pRegion, velocity, release ); @@ -718,26 +736,30 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) if( midiNote >= keyLow && midiNote <= keyHigh ) { - float attenuation; + float attenuation = pDimRegion->GetVelocityAttenuation( velocity );; float length = (double) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); + //qDebug() << "Playing sample " << QString::fromStdString( pSample->pInfo->Name ); + + // TODO: sample panning? looping? crossfade different layers? + if (release) - attenuation = 1 - 0.01053 * (256 >> pDimRegion->ReleaseTriggerDecay) * length; + attenuation *= 1 - 0.01053 * (256 >> pDimRegion->ReleaseTriggerDecay) * length; else - attenuation = pDimRegion->SampleAttenuation; + attenuation *= pDimRegion->SampleAttenuation; gigNote note = sampleToNote( pSample, midiNote, attenuation, dim.release ); - m_synthMutex.lock(); m_notes.push_back(note); - m_synthMutex.unlock(); } } pRegion = m_instrument->GetNextRegion(); } } + + m_synthMutex.unlock(); } @@ -789,7 +811,6 @@ gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _paren toolTip::add( m_patchDialogButton, tr( "Choose the patch" ) ); - // LCDs m_bankNumLcd = new LcdSpinBox( 3, "21pink", this ); m_bankNumLcd->move(131, 62); @@ -827,6 +848,7 @@ gigInstrumentView::~gigInstrumentView() + void gigInstrumentView::modelChanged() { gigInstrument * k = castModel(); diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index 00285a26f..d05342bf2 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -164,12 +164,14 @@ private: gigInstance* m_instance; gig::Instrument* m_instrument; uint32_t m_RandomSeed; + float m_currentKeyDimension; QString m_filename; // Protect synth when we are re-creating it. QMutex m_synthMutex; QMutex m_loadMutex; + QMutex m_srcMutex; sample_rate_t m_internalSampleRate; int m_lastMidiPitch; From 5b7eb30756df4910d0b574fdef7d95aa980d9dcc Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 23 May 2014 12:45:52 -0700 Subject: [PATCH 05/33] Use release time as linear fade out time This makes it sound better than before, so that pianos fade out like expected and synths abruptly stop as intended. It's not the fully implemented ADSR of the format, but it's better. --- plugins/gig_player/gig_player.cpp | 41 +++++++++++++++++++++---------- plugins/gig_player/gig_player.h | 5 ++-- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 2a334c1eb..8b2e2edef 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -427,12 +427,12 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { noteRelease = i->release; - float fadeOut = 0.25; // Seconds + float fadeOut = i->releaseTime; // Seconds int len = std::min( int( floor( fadeOut * engine::mixer()->processingSampleRate() ) ), i->size-i->position ); int endPoint = i->position+len; - if (len <= 0) + if (len < 0) break; for( int k = i->position, j = 0; k < endPoint; ++k, ++j ) @@ -548,13 +548,14 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool -gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, float attenuation, bool release ) +gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, + float attenuation, bool release, float releaseTime ) { if( !pSample || pSample->Channels == 0 ) return gigNote(); gig::buffer_t buf = pSample->LoadSampleData(); - gigNote note( pSample->SamplesTotal, release ); + gigNote note( pSample->SamplesTotal, release, releaseTime ); note.midiNote = midiNote; if( pSample->Channels > 2 ) @@ -656,7 +657,7 @@ void gigInstrument::getInstrument() gigNote gigInstrument::convertSampleRate( gigNote& old, int oldRate, int newRate ) { - gigNote note( ceil( (double)old.size*newRate/oldRate ), old.release ); + gigNote note( ceil( (double)old.size*newRate/oldRate ), old.release, old.releaseTime ); note.midiNote = old.midiNote; SRC_DATA src_data; @@ -749,7 +750,7 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) attenuation *= pDimRegion->SampleAttenuation; gigNote note = sampleToNote( pSample, midiNote, - attenuation, dim.release ); + attenuation, dim.release, pDimRegion->EG1Release ); m_notes.push_back(note); } @@ -969,17 +970,20 @@ gigNote::gigNote() : midiNote( -1 ), note( NULL ), size( 0 ), - release( false ) + release( false ), + releaseTime( 0 ) { } -gigNote::gigNote(int size, bool release ) : +gigNote::gigNote(int size, bool release, float releaseTime ) : position( 0 ), midiNote( -1 ), size( size ), - release( release ) + release( release ), + releaseTime( releaseTime ) { - note = new sampleFrame[size]; + if( size > 0 ) + note = new sampleFrame[size]; // Initialize to no sound for (int i = 0; i < size; ++i) @@ -994,12 +998,18 @@ gigNote::gigNote( const gigNote& g ) : midiNote( g.midiNote ), note( NULL ), size( g.size ), - release( g.release ) + release( g.release ), + releaseTime( g.releaseTime ) { if (size > 0) { note = new sampleFrame[size]; - std::copy(&g.note[0], &g.note[size], ¬e[0]); + + for (int i = 0; i < size; ++i) + { + note[i][0] = g.note[i][0]; + note[i][1] = g.note[i][1]; + } } } @@ -1009,7 +1019,8 @@ gigNote::gigNote( gigNote&& g ) : midiNote( g.midiNote ), note( NULL ), size( 0 ), - release( g.release ) + release( g.release ), + releaseTime( g.releaseTime ) { *this = std::move( g ); } @@ -1024,6 +1035,10 @@ gigNote& gigNote::operator=( gigNote&& g ) size = g.size; note = g.note; + position = g.position; + midiNote = g.midiNote; + release = g.release; + releaseTime = g.releaseTime; g.size = 0; g.note = NULL; diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index d05342bf2..ff210e3d4 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -84,7 +84,7 @@ class gigNote { public: gigNote(); - gigNote(int size, bool release ); + gigNote(int size, bool release, float releaseTime ); gigNote( const gigNote& g ); ~gigNote(); @@ -99,6 +99,7 @@ public: sampleFrame* note; int size; // Don't try changing this... bool release; // Whether to trigger a release sample on key up + float releaseTime; // After letting up, time to fade out }; @@ -186,7 +187,7 @@ private: private: void freeInstance(); void getInstrument(); - gigNote sampleToNote( gig::Sample* pSample, int midiNote, float attenuation, bool release ); + gigNote sampleToNote( gig::Sample* pSample, int midiNote, float attenuation, bool release, float releaseTime ); Dimension getDimensions( gig::Region* pRegion, int velocity, bool release ); gigNote convertSampleRate( gigNote& old, int oldRate, int newRate ); void addNotes( int midiNote, int velocity, bool release ); // Locks m_synthMutex internally From d9b45113210c007f948c7559672b6c340eb5fd45 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 20 Oct 2014 20:45:33 -0700 Subject: [PATCH 06/33] Mutex for note output, delete note after fade out Hopefully the separate mutex for playing the samples reduces the glitching. Deleting notes after fading out instead of after the entire sample finished playing (with many zeros after the fade out) will reduce the number of notes playing at the same time which should allow for more actually-heard notes to be played. Also, moved delete note code from release function into the rendering the notes to the output function. This seemed to fix notes occasionally not being released. --- plugins/gig_player/CMakeLists.txt | 2 +- plugins/gig_player/gig_player.cpp | 118 +++++++++++++++++------------- plugins/gig_player/gig_player.h | 3 + 3 files changed, 71 insertions(+), 52 deletions(-) diff --git a/plugins/gig_player/CMakeLists.txt b/plugins/gig_player/CMakeLists.txt index b989479f5..b786c0fe4 100644 --- a/plugins/gig_player/CMakeLists.txt +++ b/plugins/gig_player/CMakeLists.txt @@ -1,6 +1,6 @@ if(LMMS_HAVE_GIG) INCLUDE(BuildPlugin) - INCLUDE_DIRECTORIES(${GIG_INCLUDE_DIRS}{) + INCLUDE_DIRECTORIES(${GIG_INCLUDE_DIRS}) # Required for not crashing loading files with libgig SET(GCC_COVERAGE_COMPILE_FLAGS "-fexceptions -std=c++11") diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 8b2e2edef..26a085473 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -1,9 +1,14 @@ /* - * gig_player.cpp - a gig player using libgig + * gig_player.cpp - a gig player using libgig (based on sf2 player plugin) * * Copyright (c) 2008 Paul Giblock * Copyright (c) 2009-2014 Tobias Doerffel * + * Based partially on some code from LinuxSampler (also GPL): + * 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 @@ -367,24 +372,47 @@ void gigInstrument::play( sampleFrame * _working_buffer ) _working_buffer[i][1] = 0; } - m_synthMutex.lock(); + m_notesMutex.lock(); - for( int i = 0; i < frames; ++i ) + // Fill with portions of the note samples + for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { - for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + if( note->size != 0 ) { - if( note->position < note->size && note->size > 0 ) + for( int i = 0; i < frames && note->position < note->noteEnd; ++i, ++note->position ) { - _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; + _working_buffer[i][0] += note->note[note->position][0]; + _working_buffer[i][1] += note->note[note->position][1]; } } } - m_synthMutex.unlock(); + m_notesMutex.unlock(); + + // Set gain properly based on volume control + for( int 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 ); + + m_notesMutex.lock(); + + // Delete ended notes + for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + { + if( note->size == 0 || note->position >= note->noteEnd ) + { + note = m_notes.erase(note); + + if (note == m_notes.end()) + break; + } + } + + m_notesMutex.unlock(); } @@ -393,28 +421,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { GIGPluginData * pluginData = static_cast( _n->m_pluginData ); - m_synthMutex.lock(); - - // Continue looping starting at the beginning. Erasing - // invalidates the iterator. - bool deleted = false; - - // Delete ended notes - do - { - deleted = false; - - for( std::list::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) - { - if( i->position >= i->size ) - { - m_notes.erase(i); - deleted = true; - break; - } - } - } - while( deleted ); + m_notesMutex.lock(); // Is the note supposed to have a release sample played? bool noteRelease = false; @@ -423,33 +430,30 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) // TODO: implement proper ADSR from GIG specs for( std::list::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) { - if( i->midiNote == pluginData->midiNote ) + if( i->midiNote == pluginData->midiNote && !i->fadeOut ) { noteRelease = i->release; float fadeOut = i->releaseTime; // Seconds - int len = std::min( int( floor( fadeOut * engine::mixer()->processingSampleRate() ) ), + int len = std::min( int( std::floor( fadeOut * engine::mixer()->processingSampleRate() ) ), i->size-i->position ); int endPoint = i->position+len; + i->noteEnd = endPoint; // Delete note after it becomes zero + i->fadeOut = true; // We're now fading this thing out + if (len < 0) - break; + continue; for( int k = i->position, j = 0; k < endPoint; ++k, ++j ) { i->note[k][0] *= 1.0-1.0/len*j; i->note[k][1] *= 1.0-1.0/len*j; } - - for( int k = endPoint; k < i->size; ++k ) - { - i->note[k][0] = 0; - i->note[k][1] = 0; - } } } - m_synthMutex.unlock(); + m_notesMutex.unlock(); if( noteRelease ) { @@ -537,7 +541,7 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool dim.DimValues[i] = 0; break; default: - qDebug() << "Error: Unknown dimension"; + qDebug() << "gigInstrument: Unknown dimension"; dim.DimValues[i] = 0; } } @@ -559,7 +563,7 @@ gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, note.midiNote = midiNote; if( pSample->Channels > 2 ) - qDebug() << "Warning: only using first two channels of GIG file"; + qDebug() << "gigInstrument: only using first two channels of GIG file"; // 24 Bit if( pSample->BitDepth == 24 ) @@ -569,8 +573,8 @@ gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, for( int i = 0; i < pSample->SamplesTotal/pSample->Channels; ++i ) { int32_t valueLeft = (pInt[3*pSample->Channels*i]<<8) | - (pInt[3*pSample->Channels*i+1]<<16) | - (pInt[3*pSample->Channels*i+2]<<24); + (pInt[3*pSample->Channels*i+1]<<16) | + (pInt[3*pSample->Channels*i+2]<<24); note.note[i][0] = 1.0/0x100000000*attenuation*valueLeft; @@ -668,6 +672,7 @@ gigNote gigInstrument::convertSampleRate( gigNote& old, int oldRate, int newRate src_data.src_ratio = (double) note.size / old.size; src_data.end_of_input = 0; + // TODO: should we have multiple sample rate converters? m_srcMutex.lock(); int error = src_process( m_srcState, &src_data ); m_srcMutex.unlock(); @@ -740,8 +745,6 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) float attenuation = pDimRegion->GetVelocityAttenuation( velocity );; float length = (double) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); - //qDebug() << "Playing sample " << QString::fromStdString( pSample->pInfo->Name ); - // TODO: sample panning? looping? crossfade different layers? if (release) @@ -752,7 +755,10 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) gigNote note = sampleToNote( pSample, midiNote, attenuation, dim.release, pDimRegion->EG1Release ); + m_notesMutex.lock(); m_notes.push_back(note); + m_notesMutex.unlock(); + break; // Only grab one sample to play } } @@ -970,8 +976,10 @@ gigNote::gigNote() : midiNote( -1 ), note( NULL ), size( 0 ), + noteEnd( 0 ), release( false ), - releaseTime( 0 ) + releaseTime( 0 ), + fadeOut( false ) { } @@ -979,8 +987,10 @@ gigNote::gigNote(int size, bool release, float releaseTime ) : position( 0 ), midiNote( -1 ), size( size ), + noteEnd( size ), release( release ), - releaseTime( releaseTime ) + releaseTime( releaseTime ), + fadeOut( false ) { if( size > 0 ) note = new sampleFrame[size]; @@ -998,8 +1008,10 @@ gigNote::gigNote( const gigNote& g ) : midiNote( g.midiNote ), note( NULL ), size( g.size ), + noteEnd( g.noteEnd ), release( g.release ), - releaseTime( g.releaseTime ) + releaseTime( g.releaseTime ), + fadeOut( g.fadeOut ) { if (size > 0) { @@ -1019,8 +1031,10 @@ gigNote::gigNote( gigNote&& g ) : midiNote( g.midiNote ), note( NULL ), size( 0 ), + noteEnd( 0 ), release( g.release ), - releaseTime( g.releaseTime ) + releaseTime( g.releaseTime ), + fadeOut( g.fadeOut ) { *this = std::move( g ); } @@ -1034,11 +1048,13 @@ gigNote& gigNote::operator=( gigNote&& g ) delete[] note; size = g.size; + noteEnd = g.noteEnd; note = g.note; position = g.position; midiNote = g.midiNote; release = g.release; releaseTime = g.releaseTime; + fadeOut = g.fadeOut; g.size = 0; g.note = NULL; diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index ff210e3d4..08fb136af 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -98,8 +98,10 @@ public: int midiNote; sampleFrame* note; int size; // Don't try changing this... + int noteEnd; // Delete note before end of note if released and faded out early bool release; // Whether to trigger a release sample on key up float releaseTime; // After letting up, time to fade out + bool fadeOut; // Whether we have started fading out yet }; @@ -173,6 +175,7 @@ private: QMutex m_synthMutex; QMutex m_loadMutex; QMutex m_srcMutex; + QMutex m_notesMutex; sample_rate_t m_internalSampleRate; int m_lastMidiPitch; From 85998a7b7864d733f30913b74675fdc0ef47e9dc Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 20 Oct 2014 22:23:26 -0700 Subject: [PATCH 07/33] Rebased on master, minor changes --- plugins/gig_player/gig_player.cpp | 11 ++++------- plugins/gig_player/gig_player.h | 8 ++++---- plugins/gig_player/patches_dialog.cpp | 9 +++++---- plugins/gig_player/patches_dialog.h | 4 ++-- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 26a085473..35246da6d 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -28,10 +28,10 @@ * */ -#include -#include -#include -#include +#include +#include +#include +#include #include "FileDialog.h" #include "gig_player.h" @@ -1081,6 +1081,3 @@ Plugin * PLUGIN_EXPORT lmms_plugin_main( Model *, void * _data ) } - -#include "moc_gig_player.cxx" - diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index 08fb136af..bd57e98fa 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -24,10 +24,10 @@ */ -#ifndef _GIG_PLAYER_H -#define _GIG_PLAYER_H +#ifndef GIG_PLAYER_H +#define GIG_PLAYER_H -#include +#include #include #include "Instrument.h" @@ -140,7 +140,7 @@ public: virtual Flags flags() const { - return IsSingleStreamed | IsMidiBased; + return IsSingleStreamed; } virtual PluginView * instantiateView( QWidget * _parent ); diff --git a/plugins/gig_player/patches_dialog.cpp b/plugins/gig_player/patches_dialog.cpp index e1a8dac6c..40853fcae 100644 --- a/plugins/gig_player/patches_dialog.cpp +++ b/plugins/gig_player/patches_dialog.cpp @@ -25,7 +25,7 @@ #include "patches_dialog.h" -#include +#include //#include @@ -75,7 +75,11 @@ patchesDialog::patchesDialog( QWidget *pParent, Qt::WindowFlags wflags ) // pHeader->setResizeMode(QHeaderView::Custom); pHeader->setDefaultAlignment(Qt::AlignLeft); // pHeader->setDefaultSectionSize(200); +#if QT_VERSION >= 0x050000 + pHeader->setSectionsMovable(false); +#else pHeader->setMovable(false); +#endif pHeader->setStretchLastSection(true); m_progListView->resizeColumnToContents(0); // Prog. @@ -379,6 +383,3 @@ void patchesDialog::progChanged (QTreeWidgetItem * _curr, QTreeWidgetItem * _pre // 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 index 1dad78005..ebe68cb8a 100644 --- a/plugins/gig_player/patches_dialog.h +++ b/plugins/gig_player/patches_dialog.h @@ -31,8 +31,8 @@ #include "gig_player.h" #include -#include -#include +#include +#include //---------------------------------------------------------------------------- // qsynthPresetForm -- UI wrapper form. From 261b436f086d967d73fa196dd5378e8c8b04d20d Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 20 Oct 2014 23:01:28 -0700 Subject: [PATCH 08/33] Fixed second instance no name Previously if you created a second instance of a certain GIG file, it would set the name to an empty string since it didn't create a new instance, only increased a reference count. Though, it still seg faults eventually when the reference count is greater than one. --- plugins/gig_player/gig_player.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 35246da6d..1d43bf7e3 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -200,8 +200,13 @@ void gigInstrument::freeInstance() if( m_instance->refCount <= 0 ) { s_instances.remove( m_filename ); + qDebug() << "gigInstrument: deleting instance of" << m_filename; delete m_instance; } + else + { + qDebug() << "gigInstrument: decreasing reference count for" << m_filename; + } s_instancesMutex.unlock(); m_instance = NULL; @@ -232,6 +237,8 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) { m_instance = s_instances[ relativePath ]; m_instance->refCount++; + qDebug() << "gigInstrument: increasing reference count for" << relativePath; + succeeded = true; } // Add to map, if doesn't exist. @@ -243,6 +250,7 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) m_instance = new gigInstance( _gigFile ); s_instances.insert( relativePath, m_instance ); succeeded = true; + qDebug() << "gigInstrument: adding instance of" << relativePath; } catch( ... ) { From 337cc833dd2589b88016eebca411d40940f1d812 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 21 Oct 2014 00:10:27 -0700 Subject: [PATCH 09/33] Multiple references load samples from one instance without segfault Now it doesn't appear to segfault when multiple references to the same instance try accessing samples at the same time. In libgig it said I just have to create another decompression buffer for each thread. This doesn't quite make the whole addNotes function thread safe since I still hear missing notes. But, it's getting closer. --- plugins/gig_player/gig_player.cpp | 66 ++++++++++++++++++++++++++----- plugins/gig_player/gig_player.h | 3 +- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 1d43bf7e3..654a35ab0 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -28,6 +28,7 @@ * */ +#include #include #include #include @@ -189,7 +190,7 @@ QString gigInstrument::nodeName() const void gigInstrument::freeInstance() { - m_synthMutex.lock(); + m_synthMutex.lockForWrite(); if ( m_instance != NULL ) { @@ -229,7 +230,7 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) // free reference to gig file if one is selected freeInstance(); - m_synthMutex.lock(); + m_synthMutex.lockForWrite(); s_instancesMutex.lock(); // Increment Reference @@ -292,7 +293,7 @@ void gigInstrument::updatePatch() QString gigInstrument::getCurrentPatchName() { - m_synthMutex.lock(); + m_synthMutex.lockForWrite(); if( !m_instance ) { @@ -566,8 +567,46 @@ gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, if( !pSample || pSample->Channels == 0 ) return gigNote(); - gig::buffer_t buf = pSample->LoadSampleData(); - gigNote note( pSample->SamplesTotal, release, releaseTime ); + // Thread-safe gig::Sample::LoadSampleData() based on: + // gig::Sample::LoadSampleDataWithNullSamplesExtension + gig::buffer_t buf; + unsigned long samples = pSample->SamplesTotal; + unsigned long allocationsize = samples * pSample->FrameSize; + + // Generate decompression buffer + int8_t* decompressData = NULL; + gig::buffer_t decompress; + + if (pSample->Compressed) + { + // Based on: gig::Sample::GuessSize + unsigned long WorstCaseFrameSize = + ((pSample->BitDepth == 24)?256:2048) * pSample->FrameSize + pSample->Channels; + unsigned long decompressionSize = + pSample->BitDepth == 24 ? samples + (samples >> 1) + (samples >> 8) * 13 + : samples + (samples >> 10) * 5; + if (pSample->Channels == 2) + decompressionSize <<= 1; + decompressionSize += WorstCaseFrameSize; + decompressData = new int8_t[decompressionSize]; + + decompress.pStart = decompressData; + decompress.Size = decompressionSize; + decompress.NullExtensionSize = 0; + } + + // Read into buffer + pSample->SetPos(0); // TODO: is this thread safe? + buf.pStart = new int8_t[allocationsize]; + if (pSample->Compressed) + buf.Size = pSample->Read(buf.pStart, samples, &decompress) * pSample->FrameSize; + else + buf.Size = pSample->Read(buf.pStart, samples) * pSample->FrameSize; + buf.NullExtensionSize = allocationsize - buf.Size; + std::memset((int8_t*)buf.pStart + buf.Size, 0, buf.NullExtensionSize); + + // Convert from 16 or 24 bit into 32 bit float + gigNote note( samples, release, releaseTime ); note.midiNote = midiNote; if( pSample->Channels > 2 ) @@ -578,7 +617,7 @@ gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, { uint8_t* pInt = static_cast( buf.pStart ); - for( int i = 0; i < pSample->SamplesTotal/pSample->Channels; ++i ) + for( int i = 0; i < samples/pSample->Channels; ++i ) { int32_t valueLeft = (pInt[3*pSample->Channels*i]<<8) | (pInt[3*pSample->Channels*i+1]<<16) | @@ -605,7 +644,7 @@ gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, { int16_t* pInt = static_cast( buf.pStart ); - for( int i = 0; i < pSample->SamplesTotal/pSample->Channels; ++i ) + for( int i = 0; i < samples/pSample->Channels; ++i ) { note.note[i][0] = 1.0/0x10000*pInt[pSample->Channels*i] * attenuation; @@ -616,7 +655,10 @@ gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, } } - pSample->ReleaseSampleData(); + delete[] (int8_t*)buf.pStart; + + if (pSample->Compressed) + delete[] (int8_t*)decompressData; // Convert sample rate int sampleRate = pSample->SamplesPerSecond; @@ -641,7 +683,7 @@ void gigInstrument::getInstrument() int iBankSelected = m_bankNum.value(); int iProgSelected = m_patchNum.value(); - m_synthMutex.lock(); + m_synthMutex.lockForWrite(); if( m_instance ) { @@ -722,7 +764,11 @@ void gigInstrument::updateSampleRate() // Find the sample we want to play (based on velocity, etc.) void gigInstrument::addNotes( int midiNote, int velocity, bool release ) { - m_synthMutex.lock(); + // TODO: we can't quite use ForRead here, we still end up skipping notes + // and having other odd issues. Possibly has to do with the + // pSample->SetPos(0) call + //m_synthMutex.lockForRead(); + m_synthMutex.lockForWrite(); if( m_instrument ) { diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index bd57e98fa..e13609c05 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -28,6 +28,7 @@ #define GIG_PLAYER_H #include +#include #include #include "Instrument.h" @@ -172,7 +173,7 @@ private: QString m_filename; // Protect synth when we are re-creating it. - QMutex m_synthMutex; + QReadWriteLock m_synthMutex; QMutex m_loadMutex; QMutex m_srcMutex; QMutex m_notesMutex; From 96194bcee2c8d57bd683503fe7b59ba90e5b061a Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 21 Oct 2014 22:26:37 -0700 Subject: [PATCH 10/33] Stream instead of loading all into memory Now, when you press a note, it won't have to load the entire sample into memory before playing the note. This means that now you can play many more notes without it glitching. Frequently, the entire note sample isn't played, so before there was a lot of wasted processing time converting the sample into float and doing sample rate conversions if needed. Also, perform sample rate conversion on the final rendered-out version of all the combined notes for a period. This drastically decreases processing time. Note: currently having more than one instance causes glitching --- plugins/gig_player/gig_player.cpp | 488 ++++++++++++++---------------- plugins/gig_player/gig_player.h | 34 +-- 2 files changed, 247 insertions(+), 275 deletions(-) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 654a35ab0..4b15ba794 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -97,7 +97,9 @@ gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : 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" ) ) + m_gain( 1.0f, 0.0f, 5.0f, 0.01f, this, tr( "Gain" ) ), + m_noteData( NULL ), + m_noteDataSize( 0 ) { InstrumentPlayHandle * iph = new InstrumentPlayHandle( this ); engine::mixer()->addPlayHandle( iph ); @@ -107,20 +109,25 @@ gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : connect( &m_bankNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) ); connect( &m_patchNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) ); connect( engine::mixer(), SIGNAL( sampleRateChanged() ), this, SLOT( updateSampleRate() ) ); + + // Buffer for reading samples + const fpp_t frames = engine::mixer()->framesPerPeriod(); + m_noteData = new sampleFrame[frames]; + m_noteDataSize = frames; } gigInstrument::~gigInstrument() { + if( m_noteData ) + delete[] m_noteData; + engine::mixer()->removePlayHandles( instrumentTrack() ); freeInstance(); if( m_srcState != NULL ) - { src_delete( m_srcState ); - } - } @@ -190,7 +197,7 @@ QString gigInstrument::nodeName() const void gigInstrument::freeInstance() { - m_synthMutex.lockForWrite(); + m_synthMutex.lock(); if ( m_instance != NULL ) { @@ -230,7 +237,7 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) // free reference to gig file if one is selected freeInstance(); - m_synthMutex.lockForWrite(); + m_synthMutex.lock(); s_instancesMutex.lock(); // Increment Reference @@ -293,7 +300,7 @@ void gigInstrument::updatePatch() QString gigInstrument::getCurrentPatchName() { - m_synthMutex.lockForWrite(); + m_synthMutex.lock(); if( !m_instance ) { @@ -375,29 +382,179 @@ 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] = 0; - _working_buffer[i][1] = 0; - } + std::memset(&_working_buffer[0][0], 0, 2*frames*sizeof(float)); // *2 for channels + + // Determine if we need to convert sample rates + int oldRate = -1; + int newRate = engine::mixer()->processingSampleRate(); + bool sampleError = false; + bool sampleConvert = false; + sampleFrame* convertBuf = NULL; + + // How many frames we'll be grabbing from the sample + int samples = frames; m_notesMutex.lock(); + // Verify all the samples have the same rate + for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + { + if( note->sample ) + { + int currentRate = note->sample->SamplesPerSecond; + + if (oldRate == -1) + { + oldRate = currentRate; + } + else if (oldRate != currentRate) + { + qCritical() << "gigInstrument: not all samples are the same rate, not converting"; + sampleError = true; + } + } + } + + if (oldRate != -1 && !sampleError && oldRate != newRate) + { + sampleConvert = true; + + // At a higher sample rate, we'll go output the samples slightly + // faster while maintaining the same note + samples = frames*oldRate/newRate; + + // Buffer for the resampled data + convertBuf = new sampleFrame[samples]; + std::memset(&convertBuf[0][0], 0, 2*samples*sizeof(float)); // *2 for channels + } + + // Recreate buffer if it is of a different size + if (m_noteDataSize != samples) + { + delete[] m_noteData; + m_noteData = new sampleFrame[samples]; + m_noteDataSize = samples; + } + // Fill with portions of the note samples for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { - if( note->size != 0 ) + if( note->sample ) { - for( int i = 0; i < frames && note->position < note->noteEnd; ++i, ++note->position ) + // Set the position to where we currently are in the sample + note->sample->SetPos(note->pos); // Note: not thread safe + + // Load the next portion of the sample + gig::buffer_t buf; + unsigned long allocationsize = samples * note->sample->FrameSize; + buf.pStart = new int8_t[allocationsize]; + buf.Size = note->sample->Read(buf.pStart, samples) * note->sample->FrameSize; + buf.NullExtensionSize = allocationsize - buf.Size; + std::memset((int8_t*)buf.pStart + buf.Size, 0, buf.NullExtensionSize); + + // Save the new position in the sample + note->pos = note->sample->GetPos(); + + // Convert from 16 or 24 bit into 32-bit float + if( note->sample->BitDepth == 24 ) // 24 bit { - _working_buffer[i][0] += note->note[note->position][0]; - _working_buffer[i][1] += note->note[note->position][1]; + uint8_t* pInt = static_cast( buf.pStart ); + + for( int i = 0; i < samples; ++i ) + { + int32_t valueLeft = (pInt[3*note->sample->Channels*i]<<8) | + (pInt[3*note->sample->Channels*i+1]<<16) | + (pInt[3*note->sample->Channels*i+2]<<24); + + // Store the notes to this buffer before saving to output + // so we can fade them out as needed + m_noteData[i][0] = 1.0/0x100000000*note->attenuation*valueLeft; + + if( note->sample->Channels == 1 ) + { + m_noteData[i][1] = m_noteData[i][0]; + } + else + { + int32_t valueRight = (pInt[3*note->sample->Channels*i+3]<<8) | + (pInt[3*note->sample->Channels*i+4]<<16) | + (pInt[3*note->sample->Channels*i+5]<<24); + + m_noteData[i][1] = 1.0/0x100000000*note->attenuation*valueRight; + } + } + } + else // 16 bit + { + int16_t* pInt = static_cast( buf.pStart ); + + for( int i = 0; i < samples; ++i ) + { + m_noteData[i][0] = 1.0/0x10000*pInt[note->sample->Channels*i] * note->attenuation; + + if( note->sample->Channels == 1 ) + m_noteData[i][1] = m_noteData[i][0]; + else + m_noteData[i][1] = 1.0/0x10000*pInt[note->sample->Channels*i+1] * note->attenuation; + } + } + + // Cleanup + delete[] (int8_t*)buf.pStart; + + // Fade out note and if it ends before our buffer ends, set the + // rest of the samples to zero + if( note->fadeOut ) + { + int i = 0; + + for( int pos = note->fadeOutPos; i < samples && pos < note->fadeOutLen; ++i, ++pos ) + { + m_noteData[i][0] *= 1.0-1.0/note->fadeOutLen*note->fadeOutPos; + m_noteData[i][1] *= 1.0-1.0/note->fadeOutLen*note->fadeOutPos; + } + + for( ; i < samples; ++i ) + { + m_noteData[i][0] = 0; + m_noteData[i][1] = 0; + } + + // We fade out [0, samples) but we only want to advance the + // fade out as if we rendered out [0, frames) to base the fade + // out on actual time instead of the time in the sample + note->fadeOutPos += frames; + } + + // Save to output buffer + if( sampleConvert ) + { + for( int i = 0; i < samples; ++i ) + { + convertBuf[i][0] += m_noteData[i][0]; + convertBuf[i][1] += m_noteData[i][1]; + } + } + else + { + for( int i = 0; i < frames; ++i ) + { + _working_buffer[i][0] += m_noteData[i][0]; + _working_buffer[i][1] += m_noteData[i][1]; + } } } } m_notesMutex.unlock(); + // Convert sample rate if needed + if( sampleConvert ) + { + convertSampleRate(*convertBuf, *_working_buffer, samples, frames, oldRate, newRate); + delete[] convertBuf; + } + // Set gain properly based on volume control for( int i = 0; i < frames; ++i) { @@ -412,7 +569,8 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // Delete ended notes for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { - if( note->size == 0 || note->position >= note->noteEnd ) + if( (note->fadeOut && note->fadeOutPos >= note->fadeOutLen-1) || + note->pos >= note->sample->SamplesTotal-1 ) { note = m_notes.erase(note); @@ -443,22 +601,19 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { noteRelease = i->release; - float fadeOut = i->releaseTime; // Seconds + float fadeOut = i->releaseTime; + int samples = i->sample->SamplesTotal; int len = std::min( int( std::floor( fadeOut * engine::mixer()->processingSampleRate() ) ), - i->size-i->position ); - int endPoint = i->position+len; + samples - i->pos ); - i->noteEnd = endPoint; // Delete note after it becomes zero - i->fadeOut = true; // We're now fading this thing out + // TODO: not sample exact? What about in the middle of us writing out the sample? + + i->fadeOutPos = 0; + i->fadeOutLen = len; + i->fadeOut = true; if (len < 0) continue; - - for( int k = i->position, j = 0; k < endPoint; ++k, ++j ) - { - i->note[k][0] *= 1.0-1.0/len*j; - i->note[k][1] *= 1.0-1.0/len*j; - } } } @@ -561,129 +716,13 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool -gigNote gigInstrument::sampleToNote( gig::Sample* pSample, int midiNote, - float attenuation, bool release, float releaseTime ) -{ - if( !pSample || pSample->Channels == 0 ) - return gigNote(); - - // Thread-safe gig::Sample::LoadSampleData() based on: - // gig::Sample::LoadSampleDataWithNullSamplesExtension - gig::buffer_t buf; - unsigned long samples = pSample->SamplesTotal; - unsigned long allocationsize = samples * pSample->FrameSize; - - // Generate decompression buffer - int8_t* decompressData = NULL; - gig::buffer_t decompress; - - if (pSample->Compressed) - { - // Based on: gig::Sample::GuessSize - unsigned long WorstCaseFrameSize = - ((pSample->BitDepth == 24)?256:2048) * pSample->FrameSize + pSample->Channels; - unsigned long decompressionSize = - pSample->BitDepth == 24 ? samples + (samples >> 1) + (samples >> 8) * 13 - : samples + (samples >> 10) * 5; - if (pSample->Channels == 2) - decompressionSize <<= 1; - decompressionSize += WorstCaseFrameSize; - decompressData = new int8_t[decompressionSize]; - - decompress.pStart = decompressData; - decompress.Size = decompressionSize; - decompress.NullExtensionSize = 0; - } - - // Read into buffer - pSample->SetPos(0); // TODO: is this thread safe? - buf.pStart = new int8_t[allocationsize]; - if (pSample->Compressed) - buf.Size = pSample->Read(buf.pStart, samples, &decompress) * pSample->FrameSize; - else - buf.Size = pSample->Read(buf.pStart, samples) * pSample->FrameSize; - buf.NullExtensionSize = allocationsize - buf.Size; - std::memset((int8_t*)buf.pStart + buf.Size, 0, buf.NullExtensionSize); - - // Convert from 16 or 24 bit into 32 bit float - gigNote note( samples, release, releaseTime ); - note.midiNote = midiNote; - - if( pSample->Channels > 2 ) - qDebug() << "gigInstrument: only using first two channels of GIG file"; - - // 24 Bit - if( pSample->BitDepth == 24 ) - { - uint8_t* pInt = static_cast( buf.pStart ); - - for( int i = 0; i < samples/pSample->Channels; ++i ) - { - int32_t valueLeft = (pInt[3*pSample->Channels*i]<<8) | - (pInt[3*pSample->Channels*i+1]<<16) | - (pInt[3*pSample->Channels*i+2]<<24); - - note.note[i][0] = 1.0/0x100000000*attenuation*valueLeft; - - if( pSample->Channels == 1 ) - { - note.note[i][1] = note.note[i][0]; - } - else - { - int32_t valueRight = (pInt[3*pSample->Channels*i+3]<<8) | - (pInt[3*pSample->Channels*i+4]<<16) | - (pInt[3*pSample->Channels*i+5]<<24); - - note.note[i][1] = 1.0/0x100000000*attenuation*valueRight; - } - } - } - // 16 Bit - else - { - int16_t* pInt = static_cast( buf.pStart ); - - for( int i = 0; i < samples/pSample->Channels; ++i ) - { - note.note[i][0] = 1.0/0x10000*pInt[pSample->Channels*i] * attenuation; - - 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] * attenuation; - } - } - - delete[] (int8_t*)buf.pStart; - - if (pSample->Compressed) - delete[] (int8_t*)decompressData; - - // Convert sample rate - int sampleRate = pSample->SamplesPerSecond; - int outputRate = engine::mixer()->processingSampleRate(); - if( sampleRate != outputRate ) - { - gigNote converted = convertSampleRate( note, sampleRate, outputRate ); - return converted; - } - else - { - return note; - } -} - - - - void gigInstrument::getInstrument() { // Find instrument int iBankSelected = m_bankNum.value(); int iProgSelected = m_patchNum.value(); - m_synthMutex.lockForWrite(); + m_synthMutex.lock(); if( m_instance ) { @@ -708,21 +747,17 @@ void gigInstrument::getInstrument() - -gigNote gigInstrument::convertSampleRate( gigNote& old, int oldRate, int newRate ) +bool gigInstrument::convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, + int oldSize, int newSize, int oldRate, int newRate ) { - gigNote note( ceil( (double)old.size*newRate/oldRate ), old.release, old.releaseTime ); - note.midiNote = old.midiNote; - SRC_DATA src_data; - src_data.data_in = old.note[0]; - src_data.data_out = note.note[0]; - src_data.input_frames = old.size; - src_data.output_frames = note.size; - src_data.src_ratio = (double) note.size / old.size; + 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 = (double) newSize / oldSize; src_data.end_of_input = 0; - // TODO: should we have multiple sample rate converters? m_srcMutex.lock(); int error = src_process( m_srcState, &src_data ); m_srcMutex.unlock(); @@ -730,14 +765,16 @@ gigNote gigInstrument::convertSampleRate( gigNote& old, int oldRate, int newRate if( error ) { qCritical( "gigInstrument: error while resampling: %s", src_strerror( error ) ); + return false; } - if( src_data.output_frames_gen > note.size ) + if( src_data.output_frames_gen > newSize ) { - qCritical( "gigInstrument: not enough frames: %ld / %d", src_data.output_frames_gen, old.size ); + qCritical( "gigInstrument: not enough frames: %ld / %d", src_data.output_frames_gen, newSize ); + return false; } - return note; + return true; } @@ -764,16 +801,8 @@ void gigInstrument::updateSampleRate() // Find the sample we want to play (based on velocity, etc.) void gigInstrument::addNotes( int midiNote, int velocity, bool release ) { - // TODO: we can't quite use ForRead here, we still end up skipping notes - // and having other odd issues. Possibly has to do with the - // pSample->SetPos(0) call - //m_synthMutex.lockForRead(); - m_synthMutex.lockForWrite(); - if( m_instrument ) { - gig::Region* pRegion = m_instrument->GetFirstRegion(); - // Change key dimension, e.g. change samples based on what key is pressed // in a certain range. if( midiNote >= m_instrument->DimensionKeyRange.low && midiNote @@ -783,6 +812,11 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) m_instrument->DimensionKeyRange.high - m_instrument->DimensionKeyRange.low + 1 ); + m_synthMutex.lock(); + m_notesMutex.lock(); + + gig::Region* pRegion = m_instrument->GetFirstRegion(); + while( pRegion ) { Dimension dim = getDimensions( pRegion, velocity, release ); @@ -806,21 +840,19 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) else attenuation *= pDimRegion->SampleAttenuation; - gigNote note = sampleToNote( pSample, midiNote, - attenuation, dim.release, pDimRegion->EG1Release ); + m_notes.push_back(gigNote(pSample, midiNote, attenuation, dim.release, + pDimRegion->EG1Release)); - m_notesMutex.lock(); - m_notes.push_back(note); - m_notesMutex.unlock(); break; // Only grab one sample to play } } pRegion = m_instrument->GetNextRegion(); } - } - m_synthMutex.unlock(); + m_notesMutex.unlock(); + m_synthMutex.unlock(); + } } @@ -1026,104 +1058,45 @@ void gigInstrumentView::showPatchDialog() gigNote::gigNote() : - position( 0 ), - midiNote( -1 ), - note( NULL ), - size( 0 ), - noteEnd( 0 ), - release( false ), - releaseTime( 0 ), - fadeOut( false ) + sample( NULL ), + midiNote( -1 ), + attenuation( 1 ), + release( false ), + releaseTime( 0 ), + pos( 0 ), + fadeOut( false ), + fadeOutPos( 0 ), + fadeOutLen( 0 ) { } -gigNote::gigNote(int size, bool release, float releaseTime ) : - position( 0 ), - midiNote( -1 ), - size( size ), - noteEnd( size ), +gigNote::gigNote( gig::Sample* pSample, int midiNote, float attenuation, + bool release, float releaseTime ) : + sample( pSample ), + midiNote( midiNote ), + attenuation( attenuation ), release( release ), releaseTime( releaseTime ), - fadeOut( false ) + pos( 0 ), + fadeOut( false ), + fadeOutPos( 0 ), + fadeOutLen( 0 ) { - if( size > 0 ) - note = new sampleFrame[size]; - - // Initialize to no sound - for (int i = 0; i < size; ++i) - { - note[i][0] = float(); - note[i][1] = float(); - } } gigNote::gigNote( const gigNote& g ) : - position( g.position ), + sample( g.sample ), midiNote( g.midiNote ), - note( NULL ), - size( g.size ), - noteEnd( g.noteEnd ), + attenuation( g.attenuation ), release( g.release ), releaseTime( g.releaseTime ), - fadeOut( g.fadeOut ) + pos( g.pos ), + fadeOut( g.fadeOut ), + fadeOutPos( g.fadeOutPos ), + fadeOutLen( g.fadeOutLen ) { - if (size > 0) - { - note = new sampleFrame[size]; - - for (int i = 0; i < size; ++i) - { - note[i][0] = g.note[i][0]; - note[i][1] = g.note[i][1]; - } - } } -// Move constructor -gigNote::gigNote( gigNote&& g ) : - position( g.position ), - midiNote( g.midiNote ), - note( NULL ), - size( 0 ), - noteEnd( 0 ), - release( g.release ), - releaseTime( g.releaseTime ), - fadeOut( g.fadeOut ) -{ - *this = std::move( g ); -} - -// Move assignment -gigNote& gigNote::operator=( gigNote&& g ) -{ - if( this != &g ) - { - if( note ) - delete[] note; - - size = g.size; - noteEnd = g.noteEnd; - note = g.note; - position = g.position; - midiNote = g.midiNote; - release = g.release; - releaseTime = g.releaseTime; - fadeOut = g.fadeOut; - - g.size = 0; - g.note = NULL; - } - - return *this; -} - -gigNote::~gigNote() -{ - if (note) - delete[] note; -} - - extern "C" { @@ -1133,5 +1106,4 @@ Plugin * PLUGIN_EXPORT lmms_plugin_main( Model *, void * _data ) return new gigInstrument( static_cast( _data ) ); } - } diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index e13609c05..4b5fed5f1 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -28,7 +28,6 @@ #define GIG_PLAYER_H #include -#include #include #include "Instrument.h" @@ -85,24 +84,21 @@ class gigNote { public: gigNote(); - gigNote(int size, bool release, float releaseTime ); + gigNote( gig::Sample* pSample, int midiNote, float attenuation, + bool release, float releaseTime ); gigNote( const gigNote& g ); - ~gigNote(); - // Move constructor - gigNote( gigNote&& g ); - - // Move assignment - gigNote& operator=( gigNote&& g ); - - int position; + // Data for the note + gig::Sample* sample; int midiNote; - sampleFrame* note; - int size; // Don't try changing this... - int noteEnd; // Delete note before end of note if released and faded out early + float attenuation; bool release; // Whether to trigger a release sample on key up - float releaseTime; // After letting up, time to fade out + float releaseTime; // After letting up, time to fade out (seconds) + + int pos; // Position in sample bool fadeOut; // Whether we have started fading out yet + int fadeOutPos; // Position in fade out + int fadeOutLen; // Length of fade out }; @@ -173,7 +169,7 @@ private: QString m_filename; // Protect synth when we are re-creating it. - QReadWriteLock m_synthMutex; + QMutex m_synthMutex; QMutex m_loadMutex; QMutex m_srcMutex; QMutex m_notesMutex; @@ -188,12 +184,16 @@ private: FloatModel m_gain; + // Buffer for note samples + sampleFrame* m_noteData; + unsigned int m_noteDataSize; + private: void freeInstance(); void getInstrument(); - gigNote sampleToNote( gig::Sample* pSample, int midiNote, float attenuation, bool release, float releaseTime ); Dimension getDimensions( gig::Region* pRegion, int velocity, bool release ); - gigNote convertSampleRate( gigNote& old, int oldRate, int newRate ); + bool convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, + int oldSize, int newSize, int oldRate, int newRate ); void addNotes( int midiNote, int velocity, bool release ); // Locks m_synthMutex internally friend class gigInstrumentView; From 641be31d66546d2a819872cad4b6a94b26221f29 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 21 Oct 2014 22:58:30 -0700 Subject: [PATCH 11/33] No more reference counts for GIG file instances Since libgig can't really be used in a multithreaded way unless it was somewhat rewritten, just use a separate instance of the file for each new GIG file regardless of if we already have one open in the current file. Since it's fast now, you can easily have quite a few very large GIG files open and still have low latency. Also removed C++11 requirement since I no longer need a move constructor. --- plugins/gig_player/CMakeLists.txt | 2 +- plugins/gig_player/gig_player.cpp | 74 +++++++------------------------ plugins/gig_player/gig_player.h | 4 +- 3 files changed, 19 insertions(+), 61 deletions(-) diff --git a/plugins/gig_player/CMakeLists.txt b/plugins/gig_player/CMakeLists.txt index b786c0fe4..2eb71eca5 100644 --- a/plugins/gig_player/CMakeLists.txt +++ b/plugins/gig_player/CMakeLists.txt @@ -3,7 +3,7 @@ if(LMMS_HAVE_GIG) INCLUDE_DIRECTORIES(${GIG_INCLUDE_DIRS}) # Required for not crashing loading files with libgig - SET(GCC_COVERAGE_COMPILE_FLAGS "-fexceptions -std=c++11") + SET(GCC_COVERAGE_COMPILE_FLAGS "-fexceptions") add_definitions(${GCC_COVERAGE_COMPILE_FLAGS}) LINK_DIRECTORIES(${GIG_LIBRARY_DIRS}) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 4b15ba794..9761c245b 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -78,11 +78,6 @@ struct GIGPluginData -// 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 ), @@ -201,22 +196,7 @@ void gigInstrument::freeInstance() if ( m_instance != NULL ) { - s_instancesMutex.lock(); - --(m_instance->refCount); - - // No more references - if( m_instance->refCount <= 0 ) - { - s_instances.remove( m_filename ); - qDebug() << "gigInstrument: deleting instance of" << m_filename; - delete m_instance; - } - else - { - qDebug() << "gigInstrument: decreasing reference count for" << m_filename; - } - - s_instancesMutex.unlock(); + delete m_instance; m_instance = NULL; } @@ -228,7 +208,6 @@ void gigInstrument::freeInstance() void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) { emit fileLoading(); - bool succeeded = false; // Used for loading file char * gigAscii = qstrdup( qPrintable( SampleBuffer::tryToMakeAbsolute( _gigFile ) ) ); @@ -238,42 +217,19 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) freeInstance(); m_synthMutex.lock(); - s_instancesMutex.lock(); - // Increment Reference - if( s_instances.contains( relativePath ) ) + try { - m_instance = s_instances[ relativePath ]; - m_instance->refCount++; - qDebug() << "gigInstrument: increasing reference count for" << relativePath; - succeeded = true; - } - - // Add to map, if doesn't exist. - else - { - try - { - // Grab this sf from the top of the stack and add to list - m_instance = new gigInstance( _gigFile ); - s_instances.insert( relativePath, m_instance ); - succeeded = true; - qDebug() << "gigInstrument: adding instance of" << relativePath; - } - catch( ... ) - { - m_instance = NULL; - succeeded = false; - } - } - - s_instancesMutex.unlock(); - m_synthMutex.unlock(); - - if( succeeded ) + m_instance = new gigInstance( _gigFile ); m_filename = relativePath; - else + } + catch( ... ) + { + m_instance = NULL; m_filename = ""; + } + + m_synthMutex.unlock(); emit fileChanged(); @@ -551,7 +507,11 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // Convert sample rate if needed if( sampleConvert ) { - convertSampleRate(*convertBuf, *_working_buffer, samples, frames, oldRate, newRate); + // If an error occured, it's better to render nothing than have some + // screetching high-volume noise + if (!convertSampleRate(*convertBuf, *_working_buffer, samples, frames, oldRate, newRate)) + std::memset(&_working_buffer[0][0], 0, 2*frames*sizeof(float)); // *2 for channels + delete[] convertBuf; } @@ -603,7 +563,7 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) float fadeOut = i->releaseTime; int samples = i->sample->SamplesTotal; - int len = std::min( int( std::floor( fadeOut * engine::mixer()->processingSampleRate() ) ), + int len = std::min( int( floor( fadeOut * engine::mixer()->processingSampleRate() ) ), samples - i->pos ); // TODO: not sample exact? What about in the middle of us writing out the sample? @@ -705,7 +665,7 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool dim.DimValues[i] = 0; break; default: - qDebug() << "gigInstrument: Unknown dimension"; + // Warning: unknown dimension dim.DimValues[i] = 0; } } diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index 4b5fed5f1..c65342a2b 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -51,8 +51,7 @@ class gigInstance public: gigInstance( QString filename ) : riff( filename.toUtf8().constData() ), - gig( &riff ), - refCount( 1 ) + gig( &riff ) {} private: @@ -60,7 +59,6 @@ private: public: gig::File gig; - int refCount; }; From 95726eafede822b0ab68b4a67c5e3a4819cb8deb Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 22 Oct 2014 20:21:43 -0700 Subject: [PATCH 12/33] Better ADSR support, fixed some segfaults Now it supports a simple envelope using attack, decay1, sustain, and release from the GIG file. I couldn't figure out what amplitude it should go to after decay2 (if set), so currently that is unused. It would segfault if you had notes being played and then switched the instrument since the samples no longer exited. Now it'll delete all notes when you switch GIG files. --- plugins/gig_player/gig_player.cpp | 303 ++++++++++++++++-------------- plugins/gig_player/gig_player.h | 43 ++++- 2 files changed, 200 insertions(+), 146 deletions(-) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 9761c245b..9d4643de5 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -115,7 +115,7 @@ gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : gigInstrument::~gigInstrument() { - if( m_noteData ) + if( m_noteData != NULL ) delete[] m_noteData; engine::mixer()->removePlayHandles( instrumentTrack() ); @@ -192,15 +192,19 @@ QString gigInstrument::nodeName() const void gigInstrument::freeInstance() { - m_synthMutex.lock(); + QMutexLocker locker(&m_synthMutex); - if ( m_instance != NULL ) + if( m_instance != NULL ) { delete m_instance; m_instance = NULL; - } - m_synthMutex.unlock(); + // 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(); + } } @@ -216,20 +220,20 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) // free reference to gig file if one is selected freeInstance(); - m_synthMutex.lock(); - - try { - m_instance = new gigInstance( _gigFile ); - m_filename = relativePath; - } - catch( ... ) - { - m_instance = NULL; - m_filename = ""; - } + QMutexLocker locker(&m_synthMutex); - m_synthMutex.unlock(); + try + { + m_instance = new gigInstance( _gigFile ); + m_filename = relativePath; + } + catch( ... ) + { + m_instance = NULL; + m_filename = ""; + } + } emit fileChanged(); @@ -256,11 +260,10 @@ void gigInstrument::updatePatch() QString gigInstrument::getCurrentPatchName() { - m_synthMutex.lock(); + QMutexLocker locker(&m_synthMutex); if( !m_instance ) { - m_synthMutex.unlock(); return ""; } @@ -281,14 +284,12 @@ QString gigInstrument::getCurrentPatchName() if( name == "" ) name = ""; - m_synthMutex.unlock(); return name; } pInstrument = m_instance->gig.GetNextInstrument(); } - m_synthMutex.unlock(); return ""; } @@ -316,9 +317,18 @@ void gigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) pluginData->lastVelocity = 127; _n->m_pluginData = pluginData; - if( m_instance ) + bool instance = false; + bool instrument = false; + { - if( !m_instrument ) + QMutexLocker locker(&m_synthMutex); + instance = m_instance; + instrument = m_instrument; + } + + if( instance ) + { + if( !instrument ) getInstrument(); const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); @@ -350,28 +360,37 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // How many frames we'll be grabbing from the sample int samples = frames; - m_notesMutex.lock(); + m_synthMutex.lock(); - // Verify all the samples have the same rate for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { if( note->sample ) { + // Verify all the samples have the same rate int currentRate = note->sample->SamplesPerSecond; - if (oldRate == -1) + if( oldRate == -1 ) { oldRate = currentRate; } - else if (oldRate != currentRate) + else if( oldRate != currentRate ) { qCritical() << "gigInstrument: not all samples are the same rate, not converting"; sampleError = true; } + + // Delete ended notes + if( note->adsr.done() || note->pos >= note->sample->SamplesTotal-1 ) + { + note = m_notes.erase(note); + + if( note == m_notes.end() ) + break; + } } } - if (oldRate != -1 && !sampleError && oldRate != newRate) + if( oldRate != -1 && !sampleError && oldRate != newRate ) { sampleConvert = true; @@ -385,7 +404,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) } // Recreate buffer if it is of a different size - if (m_noteDataSize != samples) + if( m_noteDataSize != samples ) { delete[] m_noteData; m_noteData = new sampleFrame[samples]; @@ -458,28 +477,12 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // Cleanup delete[] (int8_t*)buf.pStart; - // Fade out note and if it ends before our buffer ends, set the - // rest of the samples to zero - if( note->fadeOut ) + // Apply ADSR + for( int i = 0; i < samples; ++i ) { - int i = 0; - - for( int pos = note->fadeOutPos; i < samples && pos < note->fadeOutLen; ++i, ++pos ) - { - m_noteData[i][0] *= 1.0-1.0/note->fadeOutLen*note->fadeOutPos; - m_noteData[i][1] *= 1.0-1.0/note->fadeOutLen*note->fadeOutPos; - } - - for( ; i < samples; ++i ) - { - m_noteData[i][0] = 0; - m_noteData[i][1] = 0; - } - - // We fade out [0, samples) but we only want to advance the - // fade out as if we rendered out [0, frames) to base the fade - // out on actual time instead of the time in the sample - note->fadeOutPos += frames; + double amplitude = note->adsr.value(); + m_noteData[i][0] *= amplitude; + m_noteData[i][1] *= amplitude; } // Save to output buffer @@ -502,14 +505,14 @@ void gigInstrument::play( sampleFrame * _working_buffer ) } } - m_notesMutex.unlock(); + m_synthMutex.unlock(); // Convert sample rate if needed if( sampleConvert ) { // If an error occured, it's better to render nothing than have some // screetching high-volume noise - if (!convertSampleRate(*convertBuf, *_working_buffer, samples, frames, oldRate, newRate)) + if( !convertSampleRate(*convertBuf, *_working_buffer, samples, frames, oldRate, newRate) ) std::memset(&_working_buffer[0][0], 0, 2*frames*sizeof(float)); // *2 for channels delete[] convertBuf; @@ -523,23 +526,6 @@ void gigInstrument::play( sampleFrame * _working_buffer ) } instrumentTrack()->processAudioBuffer( _working_buffer, frames, NULL ); - - m_notesMutex.lock(); - - // Delete ended notes - for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) - { - if( (note->fadeOut && note->fadeOutPos >= note->fadeOutLen-1) || - note->pos >= note->sample->SamplesTotal-1 ) - { - note = m_notes.erase(note); - - if (note == m_notes.end()) - break; - } - } - - m_notesMutex.unlock(); } @@ -548,36 +534,25 @@ void gigInstrument::play( sampleFrame * _working_buffer ) void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { GIGPluginData * pluginData = static_cast( _n->m_pluginData ); - m_notesMutex.lock(); + + m_synthMutex.lock(); // Is the note supposed to have a release sample played? bool noteRelease = false; // Fade out the note we want to end - // TODO: implement proper ADSR from GIG specs for( std::list::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) { - if( i->midiNote == pluginData->midiNote && !i->fadeOut ) + if( i->midiNote == pluginData->midiNote) { noteRelease = i->release; - - float fadeOut = i->releaseTime; - int samples = i->sample->SamplesTotal; - int len = std::min( int( floor( fadeOut * engine::mixer()->processingSampleRate() ) ), - samples - i->pos ); + i->adsr.keyup(); // TODO: not sample exact? What about in the middle of us writing out the sample? - - i->fadeOutPos = 0; - i->fadeOutLen = len; - i->fadeOut = true; - - if (len < 0) - continue; } } - m_notesMutex.unlock(); + m_synthMutex.unlock(); if( noteRelease ) { @@ -613,7 +588,7 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool switch( pRegion->pDimensionDefinitions[i].dimension ) { case gig::dimension_layer: - //DimValues[i] = iLayer; + // TODO: implement this dim.DimValues[i] = 0; break; case gig::dimension_velocity: @@ -628,13 +603,14 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool break; case gig::dimension_roundrobin: case gig::dimension_roundrobinkeyboard: - //dim.DimValues[i] = (uint) pEngineChannel->pMIDIKeyInfo[MIDIKey].RoundRobinIndex; // incremented for each note on - dim.DimValues[i] = 0; // TODO: implement this + dim.DimValues[i] = 0; break; case gig::dimension_random: - m_RandomSeed = m_RandomSeed * 1103515245 + 12345; // classic pseudo random number generator - dim.DimValues[i] = (uint) m_RandomSeed >> (32 - pRegion->pDimensionDefinitions[i].bits); // highest bits are most 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: @@ -662,11 +638,9 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool case gig::dimension_effect4depth: case gig::dimension_effect5depth: case gig::dimension_none: + default: dim.DimValues[i] = 0; break; - default: - // Warning: unknown dimension - dim.DimValues[i] = 0; } } @@ -682,7 +656,7 @@ void gigInstrument::getInstrument() int iBankSelected = m_bankNum.value(); int iProgSelected = m_patchNum.value(); - m_synthMutex.lock(); + QMutexLocker locker(&m_synthMutex); if( m_instance ) { @@ -701,8 +675,6 @@ void gigInstrument::getInstrument() m_instrument = pInstrument; } - - m_synthMutex.unlock(); } @@ -742,18 +714,17 @@ bool gigInstrument::convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, void gigInstrument::updateSampleRate() { - m_srcMutex.lock(); + QMutexLocker locker(&m_srcMutex); if( m_srcState != NULL ) src_delete( m_srcState ); int error; - m_srcState = src_new( engine::mixer()->currentQualitySettings().libsrcInterpolation(), DEFAULT_CHANNELS, &error ); + m_srcState = src_new( engine::mixer()->currentQualitySettings().libsrcInterpolation(), + DEFAULT_CHANNELS, &error ); if( m_srcState == NULL || error ) qCritical( "error while creating libsamplerate data structure in gigInstrument::updateSampleRate()" ); - - m_srcMutex.unlock(); } @@ -761,10 +732,12 @@ void gigInstrument::updateSampleRate() // Find the sample we want to play (based on velocity, etc.) void gigInstrument::addNotes( int midiNote, int velocity, bool release ) { + QMutexLocker synthLock(&m_synthMutex); + if( m_instrument ) { // Change key dimension, e.g. change samples based on what key is pressed - // in a certain range. + // in a certain range. From LinuxSampler if( midiNote >= m_instrument->DimensionKeyRange.low && midiNote <= m_instrument->DimensionKeyRange.high ) m_currentKeyDimension = float( midiNote - @@ -772,9 +745,6 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) m_instrument->DimensionKeyRange.high - m_instrument->DimensionKeyRange.low + 1 ); - m_synthMutex.lock(); - m_notesMutex.lock(); - gig::Region* pRegion = m_instrument->GetFirstRegion(); while( pRegion ) @@ -783,7 +753,7 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) gig::DimensionRegion* pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); gig::Sample* pSample = pDimRegion->pSample; - if( pSample && pDimRegion->pSample->SamplesTotal != 0 ) + if( pSample && pSample->SamplesTotal != 0 ) { int keyLow = pRegion->KeyRange.low; int keyHigh = pRegion->KeyRange.high; @@ -795,23 +765,19 @@ void gigInstrument::addNotes( int midiNote, int velocity, bool release ) // TODO: sample panning? looping? crossfade different layers? - if (release) + if( release ) + // From LinuxSampler, not sure how it was created attenuation *= 1 - 0.01053 * (256 >> pDimRegion->ReleaseTriggerDecay) * length; else attenuation *= pDimRegion->SampleAttenuation; m_notes.push_back(gigNote(pSample, midiNote, attenuation, dim.release, - pDimRegion->EG1Release)); - - break; // Only grab one sample to play + ADSR(pDimRegion, pSample->SamplesPerSecond))); } } pRegion = m_instrument->GetNextRegion(); } - - m_notesMutex.unlock(); - m_synthMutex.unlock(); } } @@ -1017,30 +983,14 @@ void gigInstrumentView::showPatchDialog() -gigNote::gigNote() : - sample( NULL ), - midiNote( -1 ), - attenuation( 1 ), - release( false ), - releaseTime( 0 ), - pos( 0 ), - fadeOut( false ), - fadeOutPos( 0 ), - fadeOutLen( 0 ) -{ -} - gigNote::gigNote( gig::Sample* pSample, int midiNote, float attenuation, - bool release, float releaseTime ) : + bool release, const ADSR& adsr ) : sample( pSample ), midiNote( midiNote ), attenuation( attenuation ), release( release ), - releaseTime( releaseTime ), - pos( 0 ), - fadeOut( false ), - fadeOutPos( 0 ), - fadeOutLen( 0 ) + adsr( adsr ), + pos( 0 ) { } @@ -1049,14 +999,95 @@ gigNote::gigNote( const gigNote& g ) : midiNote( g.midiNote ), attenuation( g.attenuation ), release( g.release ), - releaseTime( g.releaseTime ), - pos( g.pos ), - fadeOut( g.fadeOut ), - fadeOutPos( g.fadeOutPos ), - fadeOutLen( g.fadeOutLen ) + adsr( g.adsr ), + pos( g.pos ) { } +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 ) + { + // 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; + } +} + +// Next time we get the amplitude, we'll be releasing the note +void ADSR::keyup() +{ + isRelease = true; +} + +// Can we delete the note now? +bool ADSR::done() +{ + return isDone; +} + +// Return the current amplitude and increment +double ADSR::value() +{ + double currentAmplitude = amplitude; + + // If we're done, don't output any signal + if( isDone ) + { + 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 && isRelease ) + { + 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 ) + { + if( attackPosition < attackLength ) + amplitude = preattack + (attack - 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 ) + { + if( releasePosition < releaseLength ) + amplitude = sustain*(1.0-1.0/releaseLength*releasePosition); + else + isDone = true; + + ++releasePosition; + } + + return currentAmplitude; +} + + extern "C" { diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index c65342a2b..0d17e5c30 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -28,6 +28,7 @@ #define GIG_PLAYER_H #include +#include #include #include "Instrument.h" @@ -77,26 +78,49 @@ struct Dimension }; +class ADSR +{ + // From the file + double preattack; // initial amplitude (0-1) + double attack; // 0-60s + double decay1; // 0-60s + double decay2; // 0-60s + bool infiniteSustain; // i.e., no decay2 + double sustain; // sustain amplitude (0-1) + double release; // 0-60s + + // Used to calculate current amplitude + double amplitude; + bool isAttack; + bool isRelease; + bool isDone; + int attackPosition; + int attackLength; + int decayLength; + int releasePosition; + int releaseLength; + +public: + ADSR( gig::DimensionRegion* region, int sampleRate ); + void keyup(); // We will begin releasing starting now + bool done(); // Are we done? + double value(); // What's the current amplitude +}; + class gigNote { public: - gigNote(); gigNote( gig::Sample* pSample, int midiNote, float attenuation, - bool release, float releaseTime ); + bool release, const ADSR& adsr ); gigNote( const gigNote& g ); - // Data for the note gig::Sample* sample; int midiNote; float attenuation; bool release; // Whether to trigger a release sample on key up - float releaseTime; // After letting up, time to fade out (seconds) - + ADSR adsr; int pos; // Position in sample - bool fadeOut; // Whether we have started fading out yet - int fadeOutPos; // Position in fade out - int fadeOutLen; // Length of fade out }; @@ -135,7 +159,7 @@ public: virtual Flags flags() const { - return IsSingleStreamed; + return IsSingleStreamed|IsNotBendable; } virtual PluginView * instantiateView( QWidget * _parent ); @@ -170,7 +194,6 @@ private: QMutex m_synthMutex; QMutex m_loadMutex; QMutex m_srcMutex; - QMutex m_notesMutex; sample_rate_t m_internalSampleRate; int m_lastMidiPitch; From 473fda505a38b4b4ce60e2acbbfb0589b20d0770 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 22 Oct 2014 21:35:10 -0700 Subject: [PATCH 13/33] Removed unused images, replaced logo and background They aren't all that great, but at least now it doesn't say that it's a soundfont player. --- plugins/gig_player/artwork.png | Bin 83967 -> 14239 bytes plugins/gig_player/chorus_off.png | Bin 1481 -> 0 bytes plugins/gig_player/chorus_on.png | Bin 1735 -> 0 bytes plugins/gig_player/gig_player.cpp | 14 +++++++------- plugins/gig_player/logo.png | Bin 3137 -> 1530 bytes plugins/gig_player/reverb_off.png | Bin 1439 -> 0 bytes plugins/gig_player/reverb_on.png | Bin 1705 -> 0 bytes 7 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 plugins/gig_player/chorus_off.png delete mode 100644 plugins/gig_player/chorus_on.png delete mode 100644 plugins/gig_player/reverb_off.png delete mode 100644 plugins/gig_player/reverb_on.png diff --git a/plugins/gig_player/artwork.png b/plugins/gig_player/artwork.png index a784766c89cc8868b9e95e543cd7340864f69de2..991d779aab7dc8f68905037c227a8baf82d425be 100644 GIT binary patch literal 14239 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 P00000NkvXXu0mjfQ-PFV diff --git a/plugins/gig_player/chorus_off.png b/plugins/gig_player/chorus_off.png deleted file mode 100644 index 14e2414bdff5a07ffe2af4ca21ddd9a393e55a01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1481 zcmV;)1vdJLP)qseQ>y?6n@C! zWlHg{WwwyMFDuNEe{~-OnWy*%ITKbVmL1 z`ZWm<#fqMupVMeGe(!jU+pMi+n`SGUEDK;*whS-}AO)=DgS~Ae@4$|MpaYlCyk4UC zoPwr#43a|mt|u0Y$wrQ`I#Dzh6HDQljD|Gmb;+B~csx$2wH1m+qI7e8P0Q)D%mom* zwRP2?LUA+nT9f~12>6MJ^Ipe)9|2`Kv8jVsqlc7X%QTpy=LiKW)Hn+C} zRMhTV%bHXwO*3XnhtKYG+6o3pE-vb|T;7<+CxMUyR)(TcCql!U8*0_6+>cQvo99Dg zbkl74Kw4r{nakr)y;-kQjAgX5lVhq^DwO4Q%gZS`J1$Y9T9t1$1FNyJYuPNt;|p}O z|0ngkUHJykwMtn9`(W?!aER{;GK|x1x72!}s0$k#8rs7cXrv7i@d&&eaU2@+SgTQ^ ztxtC;zK{@l*r}AE6*p|)@VrRGk-q0mWe)nV;C1tgUVQ%p*$z{X|1#{2U*Ei;tp^V% z5{>X0n+k=Z?BlVO8}%Bk7YgLMj@gIjkWVWB#~IN`gxam9`ebKUr;Vm3uE!>ZTOD>-;No0?4+T<@mK*3OAVgBV#|%kejx ztvg~bBnaU;VtG6nQ?*j2hdaCE*b#CpQA3OgqaxrXF8OTiUEJ8xM+f^V$8=&!>CDPX z2xK~S=YhBq*qT#{#Rn_f^7*F(jIx!CfscrhL?S6cK5<>@b~*wS+>WrTAtRevlG0%1 zn1r$ISj_n;os^EK$MFX`!&7)$jwO`ET9dhgvz3f(^1N(5tE=S-ot~5wzCC-sClNg! z&2D4FolYeKEwF;%CW0>np^RmqI240>SJ?7v`CRB?SZ|qwvd*yu&%wKB!{LDXtaLhU z=n1|*+uNg8KmJ6EOG|WII+XDRu3@9mNRo%F<*|ORr#Pu-p|yoMNqWeXfBxMI?Xm$o zZIe)DGj9&a#04>#>ru*ZrYT>XQ=h2Q3Bd3W z0EP_@e28|S(&FAUz z;zE@U4-|t?pMUX40|?@!Rs*Gu)y4T=YB1=lo!!s$(z1!;4g;$<8Y6`(%G0Ocrbi59 zGI8f?jR|%XKaLv&t<6CRu3HLy6h@N-Y`-8%u9AXjiX{{V;GiI?pddotHr3AKzkif00000NkvXXu0mjf2pGRV diff --git a/plugins/gig_player/chorus_on.png b/plugins/gig_player/chorus_on.png deleted file mode 100644 index 181385043e721b374817b7747267440c59c16e1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1735 zcmV;&1~~bNP)7Z+&w3*ol>Ty*^a0)%br8Rjq(L!SZC|`IskIo?Vx(#Rw2%WV78~a{%Yw zeOKC@Ha>*s9ycOj1cM-A5K~=YkR2={ARtd(eVzg!D2gaVt!5G3{*0pAaT*oXYL9KK zs%VTm6cq)n2n`Pn%kM8-zyXL6X71mo*Xv^x(wLCN1S<|Tk$&dELOtdL7l}?V2An!_ z9Hk3HD>yaeAtj9t3=}RsE8PWG|M0gt)juH`HVwLB~7m^<%Z7>IaXWB`_}{i+?O%CyTlk} z_iBwZ*L!^Oc8hLOFxDC5_|-P!t6O*@Ja}b*yT=A-XMILjGR}Us0zf;r%uh79chJyJ zN_IWAeCO{QTzYSi&AdliIKDMqk`$5aM{Cr&jw84FOfL1rMVKFQdg+avZc-rINniks zF=R=~hz{`4jf|h)Q%dK!vZq0jTFy@wjQJL49t-2`gxqH2NsX~~&f5#ZkTw}FNBGHw zB~H)xSQr~%t5M^JH#d3fQiqW^z^<*7H9KRYFlqO^Cly@G9v=|^y za~h_`&BB$0%u@hK7S1F0rVECn`f;NGOLS?hxpR6NEhrfUE=F|)#fxvIGHvQmzml$RH` zxX~=x*cB)n5b!J>9i;okV}>8~=nXZn2)kx`*iMgcF88=L*@V;}Qsut07UvS2aXSJM za^=SEx_nh zDI!X1L)qWyV2r`WNY4r+2ICA?EaEI36R|#G#n1?bL;rZdNIw!pSUSCzm~}7}53F%m zXRyxgC}2@juD(CQ!`Z0&t}k=;_PXfpHGciGNnIXn)A!+Njg;OKtn%;CjH!Oko&`hQ zXIOEhF`@1=T&1-vyi(`Z>osn_*KIK_n0VBdJ{w7 zV8aHk{rDt*_{B++0={#Xu zTFWFbdwP_08#w;)G*dS%d;i+wp9Q|a4-LsBnFQq=WLNf>(-xweorTkiI1y6{9wXDl; z-x}oX)h_4mb*O*YVKGxaKh&V(3T7M7X(*8h7`7V9SB*$7@pS8+-+j2B*Dij=_x?14 z7v=6)&h!U+X*c^MO=YGTD3XY`fu$+Sr&B4dJM*l(x`&JBr}V^U59GDq+@fb9pB@<0 z*^^Cz23l*L*DuUT8ij{7`1t6cE{z9*iIingb@{``POO|d_1&Rf>7^)2j2h}?Lgu#{ zqEm{Rhg4{U8X>P5qH_kPj#^A9GmknU36@5glSGS%p`S&{ng@xP;K)ixKP!k9Oi+@- zp@NpqLx5V>VZ9-s^pZYZSD;ZzyNB^$?yFG?SM)AQ{ zb^s-)5{Ot45|d$!5G7WHEd~)#iFBo331x&*O4c)vDCo90NT14Th|mG(D!Of!c5Fyb ziK?u|Ktzcl62rD#iIJ!Z+cHZ4x=+o|)>ijvYTdqb2j`r=(6~LFY@0rLVa$KSvSR>W deC~f8{{bJZJUjH~p!)y-002ovPDHLkV1l+yD186` diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 9d4643de5..64d3da204 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -810,7 +810,7 @@ gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _paren 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 ); + m_fileDialogButton->move( 223, 68 ); connect( m_fileDialogButton, SIGNAL( clicked() ), this, SLOT( showFileDialog() ) ); @@ -824,7 +824,7 @@ gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _paren 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 ); + m_patchDialogButton->move( 223, 94 ); connect( m_patchDialogButton, SIGNAL( clicked() ), this, SLOT( showPatchDialog() ) ); @@ -832,21 +832,21 @@ gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _paren // LCDs m_bankNumLcd = new LcdSpinBox( 3, "21pink", this ); - m_bankNumLcd->move(131, 62); + m_bankNumLcd->move(111, 150); m_patchNumLcd = new LcdSpinBox( 3, "21pink", this ); - m_patchNumLcd->move(190, 62); + m_patchNumLcd->move(161, 150); // Next row m_filenameLabel = new QLabel( this ); - m_filenameLabel->setGeometry( 58, 109, 156, 11 ); + m_filenameLabel->setGeometry( 61, 70, 156, 14 ); m_patchLabel = new QLabel( this ); - m_patchLabel->setGeometry( 58, 127, 156, 11 ); + m_patchLabel->setGeometry( 61, 94, 156, 14 ); // Gain m_gainKnob = new gigKnob( this ); m_gainKnob->setHintText( tr("Gain") + " ", "" ); - m_gainKnob->move( 86, 55 ); + m_gainKnob->move( 32, 140 ); setAutoFillBackground( true ); QPalette pal; diff --git a/plugins/gig_player/logo.png b/plugins/gig_player/logo.png index 1c900e8ff047083fd924720c57cb6660cb3047eb..73f1b37fde5be6638a75283c9ab5bb44c27cdb83 100644 GIT binary patch delta 1512 zcmVfzKgb*;ZJb&-K?+JSo{89;-K&hz&6heUYns48%c%lK1paaKIjMoUymiPGk^DqpQuJXJ*z zY9P?g2aLL1&I63^&d<@h&L?MP1hYuk2vOh-{6ANGs(*?CN-J3R2?61BfIns}qb5Yt z@T3U@gaBM%oQXsV!igCV5In*$#&TVAaqCuLMi3%iB0j|bECh%GpJTj<+6)o@6k&~V z1Q|cAFHd6BiCt*7UB}ITJv!p$-4AfiVP^R2Yrem@jEjXkq2Zzw5?BKs;%QCcx9|Ki zz^|^a`G4W!k|gHw0Dq@2+8$#R!h05rMc#nArpfx-j3P!l0m<{j5weNNDE>QOQQ%F5 z-N{7OM4PMuT#g|R``hplw|^$q!iA?Q1s`%~0(;N|;bFH zfq&UbGiBfjJ1-qK+yH;)C={d>BEhO^E(OZt9v--skP{@qRwg46>Y=;X~kbSo)f= zsB5ln-|i8pi|TURgxV}Tk57xQnoIItEq^o>Ms-AsR>bd;Tsr!4ZCQ?#bLnZ2OmG(8 zbkaxMHYaY1Qb9zhkB<21;X{5tIpJ;bj>&i_(L_yeOJq~{>%sneU+PmCtb_PmRTbWQ z0-dHe71LdS8f3DVYcjs|@GI}B3nlCyMrGgw(%n(h)5K~qM2!#ayWZl&n?BaZHh(9* zOl|uTu%2z&WK6ERFGtr;ZElfdx`CC4BqBMv=}ie_0BH@}CNx;hB`Se1mYifpK-r9f zdz+Aw$^b)$?GI_mHHqjzNR9K%5Y(cW8@5+8l_4^70%Kv6;65 zqV7qgghn>QXkS1Z$4@oV$lBd%A%B4y!5a`7nx@I%5<+|967VBY#(-J`Z#JpDR=`-% zjo^!P-HH93SR9Q--xTAEdd=0@Dm&Xk4IiCtEk<1}`sHtA5k%0D`o+Z22zV2;z}I*O zIf|!OW0UjUme!VHYIf2~gLhSxfkfKdScohd8Gpxr?L8j{ z1%w5#Y<1N#m$tEGz$GpIV&YEVrdoP`+`G>!*J>fePMH>&wZ=FXcyVuuUo3iYyuHJA zz9dDpn(%)w@HIXm?v&@)&8`*)tfxv;le>|#vey{yMj(-4Hky}KA1w94; O0000_1E+$9>$Xb281oi{E!8k^P<&a{M@%@)FIG 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 From 0ae048b7a45c7182ccc6c8ee74a2dcbe4f4ae4fa Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 22 Oct 2014 22:02:13 -0700 Subject: [PATCH 14/33] Added libgig-dev to Travis build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 82aa2cab4..afece2cdf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: - if [ $TARGET_OS != linux ]; then sudo apt-get install -y 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: From 74ded6b7ef78b4f12a2e2339bb8ca7bfa80c04fa Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 23 Oct 2014 07:24:06 -0700 Subject: [PATCH 15/33] Switched from std::list to QList --- plugins/gig_player/gig_player.cpp | 6 +++--- plugins/gig_player/gig_player.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index 64d3da204..d05ed8c41 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -362,7 +362,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) m_synthMutex.lock(); - for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { if( note->sample ) { @@ -412,7 +412,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) } // Fill with portions of the note samples - for( std::list::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { if( note->sample ) { @@ -541,7 +541,7 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) bool noteRelease = false; // Fade out the note we want to end - for( std::list::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) + for( QList::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) { if( i->midiNote == pluginData->midiNote) { diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index 0d17e5c30..c1e97e6da 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -27,9 +27,9 @@ #ifndef GIG_PLAYER_H #define GIG_PLAYER_H +#include #include #include -#include #include "Instrument.h" #include "pixmap_button.h" @@ -179,7 +179,7 @@ public slots: private: static QMutex s_instancesMutex; static QMap s_instances; - std::list m_notes; + QList m_notes; SRC_STATE * m_srcState; From 5a3b8d3da16ab7e0ea922e3b66ab1caf529773fa Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 23 Oct 2014 22:34:55 -0700 Subject: [PATCH 16/33] Restructured into GigNotes which contain GigSamples Now notes are added/removed by locking only a note mutex when pressing or releasing a note. Then, while processing we actually find and play the samples using libgig. --- plugins/gig_player/gig_player.cpp | 456 +++++++++++++------------- plugins/gig_player/gig_player.h | 111 +++++-- plugins/gig_player/patches_dialog.cpp | 2 +- plugins/gig_player/patches_dialog.h | 4 +- 4 files changed, 308 insertions(+), 265 deletions(-) diff --git a/plugins/gig_player/gig_player.cpp b/plugins/gig_player/gig_player.cpp index d05ed8c41..11ed3c213 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/gig_player/gig_player.cpp @@ -4,7 +4,7 @@ * Copyright (c) 2008 Paul Giblock * Copyright (c) 2009-2014 Tobias Doerffel * - * Based partially on some code from LinuxSampler (also GPL): + * 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 @@ -72,29 +72,23 @@ Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor = struct GIGPluginData { int midiNote; - int lastPanning; - float lastVelocity; } ; - gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &gigplayer_plugin_descriptor ), m_srcState( NULL ), m_instance( NULL ), m_instrument( NULL ), - m_RandomSeed( 0 ), - m_currentKeyDimension( 0 ), 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" ) ), - m_noteData( NULL ), - m_noteDataSize( 0 ) + m_sampleData( NULL ), + m_sampleDataSize( 0 ), + m_RandomSeed( 0 ), + m_currentKeyDimension( 0 ) { InstrumentPlayHandle * iph = new InstrumentPlayHandle( this ); engine::mixer()->addPlayHandle( iph ); @@ -107,16 +101,16 @@ gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : // Buffer for reading samples const fpp_t frames = engine::mixer()->framesPerPeriod(); - m_noteData = new sampleFrame[frames]; - m_noteDataSize = frames; + m_sampleData = new sampleFrame[frames]; + m_sampleDataSize = frames; } gigInstrument::~gigInstrument() { - if( m_noteData != NULL ) - delete[] m_noteData; + if( m_sampleData != NULL ) + delete[] m_sampleData; engine::mixer()->removePlayHandles( instrumentTrack() ); freeInstance(); @@ -138,7 +132,6 @@ void gigInstrument::saveSettings( QDomDocument & _doc, QDomElement & _this ) - void gigInstrument::loadSettings( const QDomElement & _this ) { openFile( _this.attribute( "src" ), false ); @@ -152,7 +145,6 @@ void gigInstrument::loadSettings( const QDomElement & _this ) - void gigInstrument::loadFile( const QString & _file ) { if( !_file.isEmpty() && QFileInfo( _file ).exists() ) @@ -165,18 +157,15 @@ void gigInstrument::loadFile( const QString & _file ) - 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; } @@ -189,10 +178,10 @@ QString gigInstrument::nodeName() const - void gigInstrument::freeInstance() { - QMutexLocker locker(&m_synthMutex); + QMutexLocker synthLock(&m_synthMutex); + QMutexLocker notesLock(&m_notesMutex); if( m_instance != NULL ) { @@ -213,11 +202,7 @@ 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 + // Remove the current instrument if one is selected freeInstance(); { @@ -225,8 +210,8 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) try { - m_instance = new gigInstance( _gigFile ); - m_filename = relativePath; + m_instance = new GigInstance( _gigFile ); + m_filename = SampleBuffer::tryToMakeRelative( _gigFile ); } catch( ... ) { @@ -237,8 +222,6 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) emit fileChanged(); - delete[] gigAscii; - if( updateTrackName ) { instrumentTrack()->setName( QFileInfo( _gigFile ).baseName() ); @@ -248,7 +231,6 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) - void gigInstrument::updatePatch() { if( m_bankNum.value() >= 0 && m_patchNum.value() >= 0 ) @@ -257,7 +239,6 @@ void gigInstrument::updatePatch() - QString gigInstrument::getCurrentPatchName() { QMutexLocker locker(&m_synthMutex); @@ -295,6 +276,7 @@ QString gigInstrument::getCurrentPatchName() +// A key has been pressed void gigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) { const float LOG440 = 2.643452676f; @@ -313,44 +295,38 @@ void gigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) { GIGPluginData * pluginData = new GIGPluginData; pluginData->midiNote = midiNote; - pluginData->lastPanning = -1; - pluginData->lastVelocity = 127; _n->m_pluginData = pluginData; - bool instance = false; - bool instrument = false; + const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); + const uint velocity = _n->midiVelocity( baseVelocity ); - { - QMutexLocker locker(&m_synthMutex); - instance = m_instance; - instrument = m_instrument; - } - - if( instance ) - { - if( !instrument ) - getInstrument(); - - const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); - const uint velocity = _n->midiVelocity( baseVelocity ); - addNotes( midiNote, velocity, false ); - } + QMutexLocker locker(&m_notesMutex); + m_notes.push_back(GigNote(midiNote, velocity)); } } - -// Could we get iph-based instruments support sample-exact models by using a -// frame-length of 1 while rendering? +// 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(); // Initialize to zeros - std::memset(&_working_buffer[0][0], 0, 2*frames*sizeof(float)); // *2 for channels + std::memset(&_working_buffer[0][0], 0, DEFAULT_CHANNELS*frames*sizeof(float)); - // Determine if we need to convert sample rates + m_synthMutex.lock(); + m_notesMutex.lock(); + + if( !m_instance || !m_instrument ) + { + m_synthMutex.unlock(); + m_notesMutex.unlock(); + return; + } + + // Data for converting sample rate int oldRate = -1; int newRate = engine::mixer()->processingSampleRate(); bool sampleError = false; @@ -360,102 +336,152 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // How many frames we'll be grabbing from the sample int samples = frames; - m_synthMutex.lock(); - - for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { - if( note->sample ) + // Process notes in the KeyUp state, adding release samples if desired + if( note->state == KeyUp ) { - // Verify all the samples have the same rate - int currentRate = note->sample->SamplesPerSecond; - - if( oldRate == -1 ) + // If there are no samples, we're done + if( note->samples.empty() ) { - oldRate = currentRate; + note->state = Completed; } - else if( oldRate != currentRate ) + else { - qCritical() << "gigInstrument: not all samples are the same rate, not converting"; - sampleError = true; + note->state = PlayingKeyUp; + + // Notify each sample that the key has been released + for( QList::iterator sample = note->samples.begin(); + sample != note->samples.end(); ++sample ) + sample->adsr.keyup(); + + // Add release samples if available + if( note->release ) + addSamples(*note, true); } + } + // Process notes in the KeyDown state, adding samples for the notes + else if( note->state == KeyDown ) + { + note->state = PlayingKeyDown; + addSamples(*note, false); + } - // Delete ended notes - if( note->adsr.done() || note->pos >= note->sample->SamplesTotal-1 ) + for( QList::iterator sample = note->samples.begin(); + sample != note->samples.end(); ++sample ) + { + // Delete ended samples + if( !sample->sample || sample->adsr.done() || sample->pos >= sample->sample->SamplesTotal-1 ) { - note = m_notes.erase(note); + sample = note->samples.erase(sample); - if( note == m_notes.end() ) + if( sample == note->samples.end() ) break; } + // Verify all the samples have the same rate + else + { + int currentRate = sample->sample->SamplesPerSecond; + + if( oldRate == -1 ) + { + oldRate = currentRate; + } + else if( oldRate != currentRate ) + { + qCritical() << "gigInstrument: not all samples are the same rate, not converting"; + sampleError = true; + } + } + } + + // Delete ended notes (either in the completed state or all the samples ended) + if( note->state == Completed || note->samples.empty() ) + { + note = m_notes.erase(note); + + if( note == m_notes.end() ) + break; } } + // If all samples have the same sample rate and it's not the output sample + // rate, then we'll convert sample rates if( oldRate != -1 && !sampleError && oldRate != newRate ) { sampleConvert = true; - // At a higher sample rate, we'll go output the samples slightly - // faster while maintaining the same note + // Read a different number of samples depending on the sample rate, but + // resample it to always output the right number of frames samples = frames*oldRate/newRate; // Buffer for the resampled data convertBuf = new sampleFrame[samples]; - std::memset(&convertBuf[0][0], 0, 2*samples*sizeof(float)); // *2 for channels + std::memset(&convertBuf[0][0], 0, DEFAULT_CHANNELS*samples*sizeof(float)); } // Recreate buffer if it is of a different size - if( m_noteDataSize != samples ) + if( m_sampleDataSize != samples ) { - delete[] m_noteData; - m_noteData = new sampleFrame[samples]; - m_noteDataSize = samples; + delete[] m_sampleData; + m_sampleData = new sampleFrame[samples]; + m_sampleDataSize = samples; } - // Fill with portions of the note samples - for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + // Fill buffer with portions of the note samples + for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { - if( note->sample ) + // Only process the notes if we're in a playing state + if( !(note->state == PlayingKeyDown || + note->state == PlayingKeyUp) ) + continue; + + for( QList::iterator sample = note->samples.begin(); + sample != note->samples.end(); ++sample ) { + if( !sample->sample ) + continue; + // Set the position to where we currently are in the sample - note->sample->SetPos(note->pos); // Note: not thread safe + sample->sample->SetPos(sample->pos); // Note: not thread safe // Load the next portion of the sample gig::buffer_t buf; - unsigned long allocationsize = samples * note->sample->FrameSize; + unsigned long allocationsize = samples * sample->sample->FrameSize; buf.pStart = new int8_t[allocationsize]; - buf.Size = note->sample->Read(buf.pStart, samples) * note->sample->FrameSize; + buf.Size = sample->sample->Read(buf.pStart, samples) * sample->sample->FrameSize; buf.NullExtensionSize = allocationsize - buf.Size; std::memset((int8_t*)buf.pStart + buf.Size, 0, buf.NullExtensionSize); // Save the new position in the sample - note->pos = note->sample->GetPos(); + sample->pos = sample->sample->GetPos(); // Convert from 16 or 24 bit into 32-bit float - if( note->sample->BitDepth == 24 ) // 24 bit + if( sample->sample->BitDepth == 24 ) // 24 bit { uint8_t* pInt = static_cast( buf.pStart ); for( int i = 0; i < samples; ++i ) { - int32_t valueLeft = (pInt[3*note->sample->Channels*i]<<8) | - (pInt[3*note->sample->Channels*i+1]<<16) | - (pInt[3*note->sample->Channels*i+2]<<24); + int32_t valueLeft = (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 - m_noteData[i][0] = 1.0/0x100000000*note->attenuation*valueLeft; + m_sampleData[i][0] = 1.0/0x100000000*sample->attenuation*valueLeft; - if( note->sample->Channels == 1 ) + if( sample->sample->Channels == 1 ) { - m_noteData[i][1] = m_noteData[i][0]; + m_sampleData[i][1] = m_sampleData[i][0]; } else { - int32_t valueRight = (pInt[3*note->sample->Channels*i+3]<<8) | - (pInt[3*note->sample->Channels*i+4]<<16) | - (pInt[3*note->sample->Channels*i+5]<<24); + int32_t valueRight = (pInt[3*sample->sample->Channels*i+3]<<8) | + (pInt[3*sample->sample->Channels*i+4]<<16) | + (pInt[3*sample->sample->Channels*i+5]<<24); - m_noteData[i][1] = 1.0/0x100000000*note->attenuation*valueRight; + m_sampleData[i][1] = 1.0/0x100000000*sample->attenuation*valueRight; } } } @@ -465,12 +491,12 @@ void gigInstrument::play( sampleFrame * _working_buffer ) for( int i = 0; i < samples; ++i ) { - m_noteData[i][0] = 1.0/0x10000*pInt[note->sample->Channels*i] * note->attenuation; + m_sampleData[i][0] = 1.0/0x10000*pInt[sample->sample->Channels*i] * sample->attenuation; - if( note->sample->Channels == 1 ) - m_noteData[i][1] = m_noteData[i][0]; + if( sample->sample->Channels == 1 ) + m_sampleData[i][1] = m_sampleData[i][0]; else - m_noteData[i][1] = 1.0/0x10000*pInt[note->sample->Channels*i+1] * note->attenuation; + m_sampleData[i][1] = 1.0/0x10000*pInt[sample->sample->Channels*i+1] * sample->attenuation; } } @@ -480,9 +506,9 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // Apply ADSR for( int i = 0; i < samples; ++i ) { - double amplitude = note->adsr.value(); - m_noteData[i][0] *= amplitude; - m_noteData[i][1] *= amplitude; + double amplitude = sample->adsr.value(); + m_sampleData[i][0] *= amplitude; + m_sampleData[i][1] *= amplitude; } // Save to output buffer @@ -490,23 +516,21 @@ void gigInstrument::play( sampleFrame * _working_buffer ) { for( int i = 0; i < samples; ++i ) { - convertBuf[i][0] += m_noteData[i][0]; - convertBuf[i][1] += m_noteData[i][1]; + convertBuf[i][0] += m_sampleData[i][0]; + convertBuf[i][1] += m_sampleData[i][1]; } } else { for( int i = 0; i < frames; ++i ) { - _working_buffer[i][0] += m_noteData[i][0]; - _working_buffer[i][1] += m_noteData[i][1]; + _working_buffer[i][0] += m_sampleData[i][0]; + _working_buffer[i][1] += m_sampleData[i][1]; } } } } - m_synthMutex.unlock(); - // Convert sample rate if needed if( sampleConvert ) { @@ -518,6 +542,9 @@ void gigInstrument::play( sampleFrame * _working_buffer ) delete[] convertBuf; } + m_notesMutex.unlock(); + m_synthMutex.unlock(); + // Set gain properly based on volume control for( int i = 0; i < frames; ++i) { @@ -530,44 +557,26 @@ void gigInstrument::play( sampleFrame * _working_buffer ) - +// A key has been released void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { GIGPluginData * pluginData = static_cast( _n->m_pluginData ); + QMutexLocker locker(&m_notesMutex); - m_synthMutex.lock(); + // 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 ) + if( i->midiNote == pluginData->midiNote && + ( i->state == KeyDown || i->state == PlayingKeyDown ) ) + i->state = KeyUp; - // Is the note supposed to have a release sample played? - bool noteRelease = false; - - // Fade out the note we want to end - for( QList::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) - { - if( i->midiNote == pluginData->midiNote) - { - noteRelease = i->release; - i->adsr.keyup(); - - // TODO: not sample exact? What about in the middle of us writing out the sample? - } - } - - m_synthMutex.unlock(); - - if( noteRelease ) - { - // Add the release notes if available - const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); - const uint velocity = _n->midiVelocity( baseVelocity ); - addNotes( pluginData->midiNote , velocity, true ); - } + // 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 ); @@ -575,7 +584,68 @@ PluginView * gigInstrument::instantiateView( QWidget * _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& note, bool wantReleaseSample) +{ + // Change key dimension, e.g. change samples based on what key is pressed + // in a certain range. From LinuxSampler + if( !wantReleaseSample && + note.midiNote >= m_instrument->DimensionKeyRange.low && + note.midiNote <= m_instrument->DimensionKeyRange.high ) + m_currentKeyDimension = float( note.midiNote - + m_instrument->DimensionKeyRange.low ) / ( + m_instrument->DimensionKeyRange.high - + m_instrument->DimensionKeyRange.low + 1 ); + gig::Region* pRegion = m_instrument->GetFirstRegion(); + + while( pRegion ) + { + Dimension dim = getDimensions( pRegion, note.velocity, wantReleaseSample ); + gig::DimensionRegion* pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); + gig::Sample* pSample = pDimRegion->pSample; + + // Does this note have release samples? Set this only on the original + // notes and not when we get the release samples. + if( !wantReleaseSample ) + note.release = dim.release; + + if( pSample && pSample->SamplesTotal != 0 ) + { + int keyLow = pRegion->KeyRange.low; + int keyHigh = pRegion->KeyRange.high; + + if( note.midiNote >= keyLow && note.midiNote <= keyHigh ) + { + float attenuation = pDimRegion->GetVelocityAttenuation( note.velocity );; + float length = (double) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); + + // TODO: sample panning? looping? crossfade different layers? + + if( wantReleaseSample ) + // From LinuxSampler, not sure how it was created + attenuation *= 1 - 0.01053 * (256 >> pDimRegion->ReleaseTriggerDecay) * length; + else + attenuation *= pDimRegion->SampleAttenuation; + + note.samples.push_back(GigSample(pSample, attenuation, + ADSR(pDimRegion, pSample->SamplesPerSecond))); + } + } + + 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; @@ -649,7 +719,8 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool - +// 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 @@ -711,7 +782,6 @@ bool gigInstrument::convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, - void gigInstrument::updateSampleRate() { QMutexLocker locker(&m_srcMutex); @@ -729,62 +799,6 @@ void gigInstrument::updateSampleRate() -// Find the sample we want to play (based on velocity, etc.) -void gigInstrument::addNotes( int midiNote, int velocity, bool release ) -{ - QMutexLocker synthLock(&m_synthMutex); - - if( m_instrument ) - { - // Change key dimension, e.g. change samples based on what key is pressed - // in a certain range. From LinuxSampler - if( midiNote >= m_instrument->DimensionKeyRange.low && midiNote - <= m_instrument->DimensionKeyRange.high ) - m_currentKeyDimension = float( midiNote - - m_instrument->DimensionKeyRange.low ) / ( - m_instrument->DimensionKeyRange.high - - m_instrument->DimensionKeyRange.low + 1 ); - - gig::Region* pRegion = m_instrument->GetFirstRegion(); - - while( pRegion ) - { - Dimension dim = getDimensions( pRegion, velocity, release ); - gig::DimensionRegion* pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); - gig::Sample* pSample = pDimRegion->pSample; - - if( pSample && pSample->SamplesTotal != 0 ) - { - int keyLow = pRegion->KeyRange.low; - int keyHigh = pRegion->KeyRange.high; - - if( midiNote >= keyLow && midiNote <= keyHigh ) - { - float attenuation = pDimRegion->GetVelocityAttenuation( velocity );; - float length = (double) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); - - // TODO: sample panning? looping? crossfade different layers? - - if( release ) - // From LinuxSampler, not sure how it was created - attenuation *= 1 - 0.01053 * (256 >> pDimRegion->ReleaseTriggerDecay) * length; - else - attenuation *= pDimRegion->SampleAttenuation; - - m_notes.push_back(gigNote(pSample, midiNote, attenuation, dim.release, - ADSR(pDimRegion, pSample->SamplesPerSecond))); - } - } - - pRegion = m_instrument->GetNextRegion(); - } - } -} - - - - - class gigKnob : public knob { public: @@ -793,7 +807,7 @@ public: { setFixedSize( 31, 38 ); } -}; +} ; @@ -859,15 +873,12 @@ gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _paren - gigInstrumentView::~gigInstrumentView() { } - - void gigInstrumentView::modelChanged() { gigInstrument * k = castModel(); @@ -884,7 +895,6 @@ void gigInstrumentView::modelChanged() - void gigInstrumentView::updateFilename() { gigInstrument * i = castModel(); @@ -902,7 +912,6 @@ void gigInstrumentView::updateFilename() - void gigInstrumentView::updatePatchName() { gigInstrument * i = castModel(); @@ -915,7 +924,6 @@ void gigInstrumentView::updatePatchName() - void gigInstrumentView::invalidateFile() { m_patchDialogButton->setEnabled( false ); @@ -923,7 +931,6 @@ void gigInstrumentView::invalidateFile() - void gigInstrumentView::showFileDialog() { gigInstrument * k = castModel(); @@ -972,7 +979,6 @@ void gigInstrumentView::showFileDialog() - void gigInstrumentView::showPatchDialog() { gigInstrument * k = castModel(); @@ -983,27 +989,18 @@ void gigInstrumentView::showPatchDialog() -gigNote::gigNote( gig::Sample* pSample, int midiNote, float attenuation, - bool release, const ADSR& adsr ) : - sample( pSample ), - midiNote( midiNote ), - attenuation( attenuation ), - release( release ), - adsr( adsr ), - pos( 0 ) +// Store information related to playing a sample from the GIG file +GigSample::GigSample( gig::Sample* pSample, float attenuation, const ADSR& adsr ) + : sample( pSample ), + attenuation( attenuation ), + adsr( adsr ), + pos( 0 ) { } -gigNote::gigNote( const gigNote& g ) : - sample( g.sample ), - midiNote( g.midiNote ), - attenuation( g.attenuation ), - release( g.release ), - adsr( g.adsr ), - pos( g.pos ) -{ -} + +// 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), @@ -1030,19 +1027,25 @@ ADSR::ADSR( gig::DimensionRegion* region, int sampleRate ) } } + + // Next time we get the amplitude, we'll be releasing the note void ADSR::keyup() { isRelease = true; } -// Can we delete the note now? + + +// Can we delete the sample now? bool ADSR::done() { return isDone; } -// Return the current amplitude and increment + + +// Return the current amplitude and increment internal positions double ADSR::value() { double currentAmplitude = amplitude; @@ -1088,6 +1091,7 @@ double ADSR::value() } + extern "C" { diff --git a/plugins/gig_player/gig_player.h b/plugins/gig_player/gig_player.h index c1e97e6da..1218cceaa 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/gig_player/gig_player.h @@ -1,5 +1,5 @@ /* - * gig_player.h - a gig player using libgig + * gig_player.h - a gig player using libgig (based on sf2 player plugin) * * Copyright (c) 2008 Paul Giblock * Copyright (c) 2009-2014 Tobias Doerffel @@ -41,16 +41,16 @@ #include "gig.h" class gigInstrumentView; -class gigInstance; class NotePlayHandle; class patchesDialog; class QLabel; -class gigInstance +// Load a GIG file using libgig +class GigInstance { public: - gigInstance( QString filename ) : + GigInstance( QString filename ) : riff( filename.toUtf8().constData() ), gig( &riff ) {} @@ -60,10 +60,10 @@ private: public: gig::File gig; -}; - +} ; +// Stores options for the notes, e.g. velocity and release time struct Dimension { Dimension() : @@ -75,9 +75,12 @@ struct Dimension 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 @@ -103,25 +106,51 @@ class ADSR public: ADSR( gig::DimensionRegion* region, int sampleRate ); void keyup(); // We will begin releasing starting now - bool done(); // Are we done? + bool done(); // Is this sample done playing? double value(); // What's the current amplitude -}; +} ; -class gigNote +// The sample from the GIG file with our current position in both the sample +// and the envelope +class GigSample { public: - gigNote( gig::Sample* pSample, int midiNote, float attenuation, - bool release, const ADSR& adsr ); - gigNote( const gigNote& g ); + GigSample( gig::Sample* pSample, float attenuation, const ADSR& adsr ); gig::Sample* sample; - int midiNote; float attenuation; - bool release; // Whether to trigger a release sample on key up ADSR adsr; int pos; // Position in sample -}; +} ; + + +// What portion of a note are we in? +enum GigState +{ + KeyDown, // We just pressed the key + PlayingKeyDown, // The note is currently playing + KeyUp, // We just released the key + PlayingKeyUp, // The note is being released, e.g. a release sample is playing + Completed // The note is done playing, you can delete this note now +} ; + + +// 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 + GigState state; + QList samples; + + GigNote( int midiNote, int velocity ) + : midiNote(midiNote), velocity(velocity), release(false), state(KeyDown) + { + } +} ; @@ -177,45 +206,55 @@ public slots: private: - static QMutex s_instancesMutex; - static QMap s_instances; - QList m_notes; - + // Used to convert sample rates SRC_STATE * m_srcState; - gigInstance* m_instance; + // The GIG file and instrument we're using + GigInstance* m_instance; gig::Instrument* m_instrument; - uint32_t m_RandomSeed; - float m_currentKeyDimension; + // Part of the UI QString m_filename; - // Protect synth when we are re-creating it. - QMutex m_synthMutex; - QMutex m_loadMutex; - QMutex m_srcMutex; - - sample_rate_t m_internalSampleRate; - int m_lastMidiPitch; - int m_lastMidiPitchRange; - int m_channel; - LcdSpinBoxModel m_bankNum; LcdSpinBoxModel m_patchNum; FloatModel m_gain; + // Locking for the data + QMutex m_synthMutex; + QMutex m_srcMutex; + QMutex m_notesMutex; + + // List of all the currently playing notes + QList m_notes; + // Buffer for note samples - sampleFrame* m_noteData; - unsigned int m_noteDataSize; + sampleFrame* m_sampleData; + unsigned int m_sampleDataSize; + + // 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 ); + + // Add the desired samples to the note, either normal samples or release + // samples + void addSamples( GigNote& note, bool wantReleaseSample ); + + // Convert sample rates bool convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, int oldSize, int newSize, int oldRate, int newRate ); - void addNotes( int midiNote, int velocity, bool release ); // Locks m_synthMutex internally friend class gigInstrumentView; diff --git a/plugins/gig_player/patches_dialog.cpp b/plugins/gig_player/patches_dialog.cpp index 40853fcae..7003f888b 100644 --- a/plugins/gig_player/patches_dialog.cpp +++ b/plugins/gig_player/patches_dialog.cpp @@ -115,7 +115,7 @@ patchesDialog::~patchesDialog() // Dialog setup loader. -void patchesDialog::setup ( gigInstance * pSynth, int iChan, +void patchesDialog::setup ( GigInstance * pSynth, int iChan, const QString & _chanName, LcdSpinBoxModel * _bankModel, LcdSpinBoxModel * _progModel, diff --git a/plugins/gig_player/patches_dialog.h b/plugins/gig_player/patches_dialog.h index ebe68cb8a..6b74993f9 100644 --- a/plugins/gig_player/patches_dialog.h +++ b/plugins/gig_player/patches_dialog.h @@ -50,7 +50,7 @@ public: virtual ~patchesDialog(); - void setup(gigInstance * pSynth, int iChan, const QString & _chanName, + void setup(GigInstance * pSynth, int iChan, const QString & _chanName, LcdSpinBoxModel * _bankModel, LcdSpinBoxModel * _progModel, QLabel *_patchLabel ); public slots: @@ -76,7 +76,7 @@ protected: private: // Instance variables. - gigInstance *m_pSynth; + GigInstance *m_pSynth; int m_iChan; int m_iBank; From 3aaa7ac6c45d0d63c1fa5a439ac9ab4eb7870e10 Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 2 Nov 2014 21:27:30 -0800 Subject: [PATCH 17/33] Coding style fixes --- plugins/CMakeLists.txt | 2 +- .../{gig_player => GigPlayer}/CMakeLists.txt | 2 +- .../GigPlayer.cpp} | 373 ++++++++++------ .../gig_player.h => GigPlayer/GigPlayer.h} | 81 ++-- plugins/GigPlayer/PatchesDialog.cpp | 414 ++++++++++++++++++ .../PatchesDialog.h} | 36 +- .../PatchesDialog.ui} | 4 +- plugins/{gig_player => GigPlayer}/artwork.png | Bin .../fileselect_off.png | Bin .../fileselect_on.png | Bin plugins/{gig_player => GigPlayer}/logo.png | Bin .../{gig_player => GigPlayer}/patches_off.png | Bin .../{gig_player => GigPlayer}/patches_on.png | Bin plugins/gig_player/patches_dialog.cpp | 385 ---------------- 14 files changed, 720 insertions(+), 577 deletions(-) rename plugins/{gig_player => GigPlayer}/CMakeLists.txt (59%) rename plugins/{gig_player/gig_player.cpp => GigPlayer/GigPlayer.cpp} (73%) rename plugins/{gig_player/gig_player.h => GigPlayer/GigPlayer.h} (79%) create mode 100644 plugins/GigPlayer/PatchesDialog.cpp rename plugins/{gig_player/patches_dialog.h => GigPlayer/PatchesDialog.h} (66%) rename plugins/{gig_player/patches_dialog.ui => GigPlayer/PatchesDialog.ui} (98%) rename plugins/{gig_player => GigPlayer}/artwork.png (100%) rename plugins/{gig_player => GigPlayer}/fileselect_off.png (100%) rename plugins/{gig_player => GigPlayer}/fileselect_on.png (100%) rename plugins/{gig_player => GigPlayer}/logo.png (100%) rename plugins/{gig_player => GigPlayer}/patches_off.png (100%) rename plugins/{gig_player => GigPlayer}/patches_on.png (100%) delete mode 100644 plugins/gig_player/patches_dialog.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index bd073e6a2..91a1664f7 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -25,7 +25,7 @@ ADD_SUBDIRECTORY(peak_controller_effect) IF(NOT LMMS_BUILD_APPLE) ADD_SUBDIRECTORY(sf2_player) ENDIF() -ADD_SUBDIRECTORY(gig_player) +ADD_SUBDIRECTORY(GigPlayer) ADD_SUBDIRECTORY(sfxr) ADD_SUBDIRECTORY(sid) ADD_SUBDIRECTORY(SpectrumAnalyzer) diff --git a/plugins/gig_player/CMakeLists.txt b/plugins/GigPlayer/CMakeLists.txt similarity index 59% rename from plugins/gig_player/CMakeLists.txt rename to plugins/GigPlayer/CMakeLists.txt index 2eb71eca5..4be4dff82 100644 --- a/plugins/gig_player/CMakeLists.txt +++ b/plugins/GigPlayer/CMakeLists.txt @@ -8,6 +8,6 @@ if(LMMS_HAVE_GIG) 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") + 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/gig_player/gig_player.cpp b/plugins/GigPlayer/GigPlayer.cpp similarity index 73% rename from plugins/gig_player/gig_player.cpp rename to plugins/GigPlayer/GigPlayer.cpp index 11ed3c213..0eea3a6d0 100644 --- a/plugins/gig_player/gig_player.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -1,5 +1,5 @@ /* - * gig_player.cpp - a gig player using libgig (based on sf2 player plugin) + * GigPlayer.cpp - a GIG player using libgig (based on Sf2 player plugin) * * Copyright (c) 2008 Paul Giblock * Copyright (c) 2009-2014 Tobias Doerffel @@ -35,15 +35,16 @@ #include #include "FileDialog.h" -#include "gig_player.h" +#include "GigPlayer.h" #include "engine.h" #include "InstrumentTrack.h" #include "InstrumentPlayHandle.h" #include "NotePlayHandle.h" #include "knob.h" #include "song.h" +#include "config_mgr.h" -#include "patches_dialog.h" +#include "PatchesDialog.h" #include "tooltip.h" #include "LcdSpinBox.h" @@ -69,6 +70,8 @@ Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor = } + + struct GIGPluginData { int midiNote; @@ -76,14 +79,15 @@ struct GIGPluginData -gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : + +GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &gigplayer_plugin_descriptor ), m_srcState( NULL ), 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_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_sampleData( NULL ), m_sampleDataSize( 0 ), @@ -107,21 +111,27 @@ gigInstrument::gigInstrument( InstrumentTrack * _instrument_track ) : -gigInstrument::~gigInstrument() + +GigInstrument::~GigInstrument() { if( m_sampleData != NULL ) + { delete[] m_sampleData; + } engine::mixer()->removePlayHandles( instrumentTrack() ); freeInstance(); if( m_srcState != NULL ) + { src_delete( m_srcState ); + } } -void gigInstrument::saveSettings( QDomDocument & _doc, QDomElement & _this ) + +void GigInstrument::saveSettings( QDomDocument & _doc, QDomElement & _this ) { _this.setAttribute( "src", m_filename ); m_patchNum.saveSettings( _doc, _this, "patch" ); @@ -132,7 +142,8 @@ void gigInstrument::saveSettings( QDomDocument & _doc, QDomElement & _this ) -void gigInstrument::loadSettings( const QDomElement & _this ) + +void GigInstrument::loadSettings( const QDomElement & _this ) { openFile( _this.attribute( "src" ), false ); m_patchNum.loadSettings( _this, "patch" ); @@ -145,7 +156,8 @@ void gigInstrument::loadSettings( const QDomElement & _this ) -void gigInstrument::loadFile( const QString & _file ) + +void GigInstrument::loadFile( const QString & _file ) { if( !_file.isEmpty() && QFileInfo( _file ).exists() ) { @@ -157,12 +169,17 @@ void gigInstrument::loadFile( const QString & _file ) -AutomatableModel * gigInstrument::childModel( const QString & _modelName ) + +AutomatableModel * GigInstrument::childModel( const QString & _modelName ) { if( _modelName == "bank" ) + { return &m_bankNum; + } else if( _modelName == "patch" ) + { return &m_patchNum; + } qCritical() << "requested unknown model " << _modelName; @@ -171,17 +188,19 @@ AutomatableModel * gigInstrument::childModel( const QString & _modelName ) -QString gigInstrument::nodeName() const + +QString GigInstrument::nodeName() const { return gigplayer_plugin_descriptor.name; } -void gigInstrument::freeInstance() + +void GigInstrument::freeInstance() { - QMutexLocker synthLock(&m_synthMutex); - QMutexLocker notesLock(&m_notesMutex); + QMutexLocker synthLock( &m_synthMutex ); + QMutexLocker notesLock( &m_notesMutex ); if( m_instance != NULL ) { @@ -198,7 +217,8 @@ void gigInstrument::freeInstance() -void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) + +void GigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) { emit fileLoading(); @@ -206,7 +226,7 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) freeInstance(); { - QMutexLocker locker(&m_synthMutex); + QMutexLocker locker( &m_synthMutex ); try { @@ -222,7 +242,7 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) emit fileChanged(); - if( updateTrackName ) + if( updateTrackName == true ) { instrumentTrack()->setName( QFileInfo( _gigFile ).baseName() ); updatePatch(); @@ -231,19 +251,23 @@ void gigInstrument::openFile( const QString & _gigFile, bool updateTrackName ) -void gigInstrument::updatePatch() + +void GigInstrument::updatePatch() { if( m_bankNum.value() >= 0 && m_patchNum.value() >= 0 ) + { getInstrument(); + } } -QString gigInstrument::getCurrentPatchName() -{ - QMutexLocker locker(&m_synthMutex); - if( !m_instance ) +QString GigInstrument::getCurrentPatchName() +{ + QMutexLocker locker( &m_synthMutex ); + + if( m_instance == NULL ) { return ""; } @@ -251,19 +275,21 @@ QString gigInstrument::getCurrentPatchName() int iBankSelected = m_bankNum.value(); int iProgSelected = m_patchNum.value(); - gig::Instrument* pInstrument = m_instance->gig.GetFirstInstrument(); + gig::Instrument * pInstrument = m_instance->gig.GetFirstInstrument(); - while( pInstrument ) + while( pInstrument != NULL ) { int iBank = pInstrument->MIDIBank; int iProg = pInstrument->MIDIProgram; if( iBank == iBankSelected && iProg == iProgSelected ) { - QString name = QString::fromStdString(pInstrument->pInfo->Name); + QString name = QString::fromStdString( pInstrument->pInfo->Name ); if( name == "" ) + { name = ""; + } return name; } @@ -276,14 +302,15 @@ QString gigInstrument::getCurrentPatchName() + // A key has been pressed -void gigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) +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 ); + int midiNote = (int) floor( 12.0 * ( log2( _n->unpitchedFrequency() ) - LOG440 ) - 4.0 ); // out of range? if( midiNote <= 0 || midiNote >= 128 ) @@ -300,26 +327,27 @@ void gigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) const int baseVelocity = instrumentTrack()->midiPort()->baseVelocity(); const uint velocity = _n->midiVelocity( baseVelocity ); - QMutexLocker locker(&m_notesMutex); - m_notes.push_back(GigNote(midiNote, velocity)); + QMutexLocker locker( &m_notesMutex ); + m_notes.push_back( GigNote( midiNote, velocity ) ); } } + // Process the notes and output a certain number of frames (e.g. 256, set in // the preferences) -void gigInstrument::play( sampleFrame * _working_buffer ) +void GigInstrument::play( sampleFrame * _working_buffer ) { const fpp_t frames = engine::mixer()->framesPerPeriod(); // Initialize to zeros - std::memset(&_working_buffer[0][0], 0, DEFAULT_CHANNELS*frames*sizeof(float)); + std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) ); m_synthMutex.lock(); m_notesMutex.lock(); - if( !m_instance || !m_instrument ) + if( m_instance == NULL || m_instrument == NULL ) { m_synthMutex.unlock(); m_notesMutex.unlock(); @@ -331,7 +359,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) int newRate = engine::mixer()->processingSampleRate(); bool sampleError = false; bool sampleConvert = false; - sampleFrame* convertBuf = NULL; + sampleFrame * convertBuf = NULL; // How many frames we'll be grabbing from the sample int samples = frames; @@ -353,30 +381,37 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // Notify each sample that the key has been released for( QList::iterator sample = note->samples.begin(); sample != note->samples.end(); ++sample ) + { sample->adsr.keyup(); + } // Add release samples if available - if( note->release ) - addSamples(*note, true); + if( note->release == true ) + { + addSamples( *note, true ); + } } } // Process notes in the KeyDown state, adding samples for the notes else if( note->state == KeyDown ) { note->state = PlayingKeyDown; - addSamples(*note, false); + addSamples( *note, false ); } for( QList::iterator sample = note->samples.begin(); sample != note->samples.end(); ++sample ) { // Delete ended samples - if( !sample->sample || sample->adsr.done() || sample->pos >= sample->sample->SamplesTotal-1 ) + if( sample->sample == NULL || sample->adsr.done() || + sample->pos >= sample->sample->SamplesTotal - 1 ) { - sample = note->samples.erase(sample); + sample = note->samples.erase( sample ); if( sample == note->samples.end() ) + { break; + } } // Verify all the samples have the same rate else @@ -389,7 +424,7 @@ void gigInstrument::play( sampleFrame * _working_buffer ) } else if( oldRate != currentRate ) { - qCritical() << "gigInstrument: not all samples are the same rate, not converting"; + qCritical() << "GigInstrument: not all samples are the same rate, not converting"; sampleError = true; } } @@ -398,26 +433,28 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // Delete ended notes (either in the completed state or all the samples ended) if( note->state == Completed || note->samples.empty() ) { - note = m_notes.erase(note); + note = m_notes.erase( note ); if( note == m_notes.end() ) + { break; + } } } // If all samples have the same sample rate and it's not the output sample // rate, then we'll convert sample rates - if( oldRate != -1 && !sampleError && oldRate != newRate ) + if( oldRate != -1 && sampleError != true && oldRate != newRate ) { sampleConvert = true; // Read a different number of samples depending on the sample rate, but // resample it to always output the right number of frames - samples = frames*oldRate/newRate; + samples = frames * oldRate / newRate; // Buffer for the resampled data convertBuf = new sampleFrame[samples]; - std::memset(&convertBuf[0][0], 0, DEFAULT_CHANNELS*samples*sizeof(float)); + std::memset( &convertBuf[0][0], 0, DEFAULT_CHANNELS * samples * sizeof( float ) ); } // Recreate buffer if it is of a different size @@ -432,26 +469,30 @@ void gigInstrument::play( sampleFrame * _working_buffer ) for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { // Only process the notes if we're in a playing state - if( !(note->state == PlayingKeyDown || - note->state == PlayingKeyUp) ) + if( !( note->state == PlayingKeyDown || + note->state == PlayingKeyUp ) ) + { continue; + } for( QList::iterator sample = note->samples.begin(); sample != note->samples.end(); ++sample ) { - if( !sample->sample ) + if( sample->sample == NULL ) + { continue; + } // Set the position to where we currently are in the sample - sample->sample->SetPos(sample->pos); // Note: not thread safe + sample->sample->SetPos( sample->pos ); // Note: not thread safe // Load the next portion of the sample gig::buffer_t buf; unsigned long allocationsize = samples * sample->sample->FrameSize; buf.pStart = new int8_t[allocationsize]; - buf.Size = sample->sample->Read(buf.pStart, samples) * sample->sample->FrameSize; + buf.Size = sample->sample->Read( buf.pStart, samples ) * sample->sample->FrameSize; buf.NullExtensionSize = allocationsize - buf.Size; - std::memset((int8_t*)buf.pStart + buf.Size, 0, buf.NullExtensionSize); + std::memset( (int8_t*) buf.pStart + buf.Size, 0, buf.NullExtensionSize ); // Save the new position in the sample sample->pos = sample->sample->GetPos(); @@ -459,17 +500,17 @@ void gigInstrument::play( sampleFrame * _working_buffer ) // Convert from 16 or 24 bit into 32-bit float if( sample->sample->BitDepth == 24 ) // 24 bit { - uint8_t* pInt = static_cast( buf.pStart ); + uint8_t * pInt = static_cast( buf.pStart ); for( int i = 0; i < samples; ++i ) { - int32_t valueLeft = (pInt[3*sample->sample->Channels*i]<<8) | - (pInt[3*sample->sample->Channels*i+1]<<16) | - (pInt[3*sample->sample->Channels*i+2]<<24); + int32_t valueLeft = ( 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 - m_sampleData[i][0] = 1.0/0x100000000*sample->attenuation*valueLeft; + m_sampleData[i][0] = 1.0 / 0x100000000 * sample->attenuation * valueLeft; if( sample->sample->Channels == 1 ) { @@ -477,31 +518,35 @@ void gigInstrument::play( sampleFrame * _working_buffer ) } else { - int32_t valueRight = (pInt[3*sample->sample->Channels*i+3]<<8) | - (pInt[3*sample->sample->Channels*i+4]<<16) | - (pInt[3*sample->sample->Channels*i+5]<<24); + int32_t valueRight = ( pInt[ 3 * sample->sample->Channels * i + 3 ] << 8 ) | + ( pInt[ 3 * sample->sample->Channels * i + 4 ] << 16 ) | + ( pInt[ 3 * sample->sample->Channels * i + 5 ] << 24 ); - m_sampleData[i][1] = 1.0/0x100000000*sample->attenuation*valueRight; + m_sampleData[i][1] = 1.0 / 0x100000000 * sample->attenuation * valueRight; } } } else // 16 bit { - int16_t* pInt = static_cast( buf.pStart ); + int16_t * pInt = static_cast( buf.pStart ); for( int i = 0; i < samples; ++i ) { - m_sampleData[i][0] = 1.0/0x10000*pInt[sample->sample->Channels*i] * sample->attenuation; + m_sampleData[i][0] = 1.0 / 0x10000 * pInt[ sample->sample->Channels * i ] * sample->attenuation; if( sample->sample->Channels == 1 ) + { m_sampleData[i][1] = m_sampleData[i][0]; + } else - m_sampleData[i][1] = 1.0/0x10000*pInt[sample->sample->Channels*i+1] * sample->attenuation; + { + m_sampleData[i][1] = 1.0 / 0x10000 * pInt[ sample->sample->Channels * i + 1 ] * sample->attenuation; + } } } // Cleanup - delete[] (int8_t*)buf.pStart; + delete[] (int8_t*) buf.pStart; // Apply ADSR for( int i = 0; i < samples; ++i ) @@ -536,8 +581,10 @@ void gigInstrument::play( sampleFrame * _working_buffer ) { // If an error occured, it's better to render nothing than have some // screetching high-volume noise - if( !convertSampleRate(*convertBuf, *_working_buffer, samples, frames, oldRate, newRate) ) - std::memset(&_working_buffer[0][0], 0, 2*frames*sizeof(float)); // *2 for channels + if( !convertSampleRate( *convertBuf, *_working_buffer, samples, frames, oldRate, newRate ) ) + { + std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) ); + } delete[] convertBuf; } @@ -557,18 +604,23 @@ void gigInstrument::play( sampleFrame * _working_buffer ) + // A key has been released -void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) +void GigInstrument::deleteNotePluginData( NotePlayHandle * _n ) { GIGPluginData * pluginData = static_cast( _n->m_pluginData ); - QMutexLocker locker(&m_notesMutex); + 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 ) + { if( i->midiNote == pluginData->midiNote && ( i->state == KeyDown || i->state == PlayingKeyDown ) ) + { i->state = KeyUp; + } + } // TODO: not sample exact? What about in the middle of us writing out the sample? @@ -577,44 +629,50 @@ void gigInstrument::deleteNotePluginData( NotePlayHandle * _n ) -PluginView * gigInstrument::instantiateView( QWidget * _parent ) + +PluginView * GigInstrument::instantiateView( QWidget * _parent ) { - return new gigInstrumentView( this, _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& note, bool wantReleaseSample) +void GigInstrument::addSamples( GigNote & note, bool wantReleaseSample ) { // Change key dimension, e.g. change samples based on what key is pressed // in a certain range. From LinuxSampler - if( !wantReleaseSample && + if( wantReleaseSample == true && note.midiNote >= m_instrument->DimensionKeyRange.low && note.midiNote <= m_instrument->DimensionKeyRange.high ) + { m_currentKeyDimension = float( note.midiNote - m_instrument->DimensionKeyRange.low ) / ( m_instrument->DimensionKeyRange.high - m_instrument->DimensionKeyRange.low + 1 ); + } gig::Region* pRegion = m_instrument->GetFirstRegion(); - while( pRegion ) + while( pRegion != NULL ) { Dimension dim = getDimensions( pRegion, note.velocity, wantReleaseSample ); - gig::DimensionRegion* pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); - gig::Sample* pSample = pDimRegion->pSample; + gig::DimensionRegion * pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); + gig::Sample * pSample = pDimRegion->pSample; // Does this note have release samples? Set this only on the original // notes and not when we get the release samples. - if( !wantReleaseSample ) + if( wantReleaseSample != true ) + { note.release = dim.release; + } - if( pSample && pSample->SamplesTotal != 0 ) + if( pSample != NULL && pSample->SamplesTotal != 0 ) { int keyLow = pRegion->KeyRange.low; int keyHigh = pRegion->KeyRange.high; @@ -626,14 +684,18 @@ void gigInstrument::addSamples(GigNote& note, bool wantReleaseSample) // TODO: sample panning? looping? crossfade different layers? - if( wantReleaseSample ) + if( wantReleaseSample == true ) + { // From LinuxSampler, not sure how it was created - attenuation *= 1 - 0.01053 * (256 >> pDimRegion->ReleaseTriggerDecay) * length; + attenuation *= 1 - 0.01053 * ( 256 >> pDimRegion->ReleaseTriggerDecay ) * length; + } else + { attenuation *= pDimRegion->SampleAttenuation; + } - note.samples.push_back(GigSample(pSample, attenuation, - ADSR(pDimRegion, pSample->SamplesPerSecond))); + note.samples.push_back( GigSample( pSample, attenuation, + ADSR( pDimRegion, pSample->SamplesPerSecond ) ) ); } } @@ -643,15 +705,18 @@ void gigInstrument::addSamples(GigNote& note, bool wantReleaseSample) + // 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 GigInstrument::getDimensions( gig::Region * pRegion, int velocity, bool release ) { Dimension dim; - if( !pRegion ) + if( pRegion == NULL ) + { return dim; + } for( int i = pRegion->Dimensions - 1; i >= 0; --i ) { @@ -669,7 +734,7 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool dim.DimValues[i] = (uint) release; break; case gig::dimension_keyboard: - dim.DimValues[i] = (uint) (m_currentKeyDimension * pRegion->pDimensionDefinitions[i].zones); + dim.DimValues[i] = (uint) ( m_currentKeyDimension * pRegion->pDimensionDefinitions[i].zones ); break; case gig::dimension_roundrobin: case gig::dimension_roundrobinkeyboard: @@ -680,7 +745,7 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool // From LinuxSampler, untested m_RandomSeed = m_RandomSeed * 1103515245 + 12345; dim.DimValues[i] = uint( - m_RandomSeed / 4294967296.0f * pRegion->pDimensionDefinitions[i].bits); + m_RandomSeed / 4294967296.0f * pRegion->pDimensionDefinitions[i].bits ); break; case gig::dimension_samplechannel: case gig::dimension_channelaftertouch: @@ -719,27 +784,30 @@ Dimension gigInstrument::getDimensions( gig::Region* pRegion, int velocity, bool + // 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() +void GigInstrument::getInstrument() { // Find instrument int iBankSelected = m_bankNum.value(); int iProgSelected = m_patchNum.value(); - QMutexLocker locker(&m_synthMutex); + QMutexLocker locker( &m_synthMutex ); - if( m_instance ) + if( m_instance != NULL ) { - gig::Instrument* pInstrument = m_instance->gig.GetFirstInstrument(); + gig::Instrument * pInstrument = m_instance->gig.GetFirstInstrument(); - while( pInstrument ) + while( pInstrument != NULL ) { int iBank = pInstrument->MIDIBank; int iProg = pInstrument->MIDIProgram; if( iBank == iBankSelected && iProg == iProgSelected ) + { break; + } pInstrument = m_instance->gig.GetNextInstrument(); } @@ -750,7 +818,8 @@ void gigInstrument::getInstrument() -bool gigInstrument::convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, + +bool GigInstrument::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, int oldSize, int newSize, int oldRate, int newRate ) { SRC_DATA src_data; @@ -765,15 +834,15 @@ bool gigInstrument::convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, int error = src_process( m_srcState, &src_data ); m_srcMutex.unlock(); - if( error ) + if( error != 0 ) { - qCritical( "gigInstrument: error while resampling: %s", src_strerror( error ) ); + qCritical( "GigInstrument: error while resampling: %s", src_strerror( error ) ); return false; } if( src_data.output_frames_gen > newSize ) { - qCritical( "gigInstrument: not enough frames: %ld / %d", src_data.output_frames_gen, newSize ); + qCritical( "GigInstrument: not enough frames: %ld / %d", src_data.output_frames_gen, newSize ); return false; } @@ -782,23 +851,29 @@ bool gigInstrument::convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, -void gigInstrument::updateSampleRate() + +void GigInstrument::updateSampleRate() { - QMutexLocker locker(&m_srcMutex); + QMutexLocker locker( &m_srcMutex ); if( m_srcState != NULL ) + { src_delete( m_srcState ); + } int error; m_srcState = src_new( engine::mixer()->currentQualitySettings().libsrcInterpolation(), DEFAULT_CHANNELS, &error ); - if( m_srcState == NULL || error ) - qCritical( "error while creating libsamplerate data structure in gigInstrument::updateSampleRate()" ); + if( m_srcState == NULL || error != 0 ) + { + qCritical( "error while creating libsamplerate data structure in GigInstrument::updateSampleRate()" ); + } } + class gigKnob : public knob { public: @@ -811,10 +886,11 @@ public: -gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _parent ) : + +GigInstrumentView::GigInstrumentView( Instrument * _instrument, QWidget * _parent ) : InstrumentView( _instrument, _parent ) { - gigInstrument* k = castModel(); + GigInstrument * k = castModel(); connect( &k->m_bankNum, SIGNAL( dataChanged() ), this, SLOT( updatePatchName() ) ); connect( &k->m_patchNum, SIGNAL( dataChanged() ), this, SLOT( updatePatchName() ) ); @@ -846,10 +922,10 @@ gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _paren // LCDs m_bankNumLcd = new LcdSpinBox( 3, "21pink", this ); - m_bankNumLcd->move(111, 150); + m_bankNumLcd->move( 111, 150 ); m_patchNumLcd = new LcdSpinBox( 3, "21pink", this ); - m_patchNumLcd->move(161, 150); + m_patchNumLcd->move( 161, 150 ); // Next row m_filenameLabel = new QLabel( this ); @@ -859,7 +935,7 @@ gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _paren // Gain m_gainKnob = new gigKnob( this ); - m_gainKnob->setHintText( tr("Gain") + " ", "" ); + m_gainKnob->setHintText( tr( "Gain" ) + " ", "" ); m_gainKnob->move( 32, 140 ); setAutoFillBackground( true ); @@ -873,15 +949,17 @@ gigInstrumentView::gigInstrumentView( Instrument * _instrument, QWidget * _paren -gigInstrumentView::~gigInstrumentView() + +GigInstrumentView::~GigInstrumentView() { } -void gigInstrumentView::modelChanged() + +void GigInstrumentView::modelChanged() { - gigInstrument * k = castModel(); + GigInstrument * k = castModel(); m_bankNumLcd->setModel( &k->m_bankNum ); m_patchNumLcd->setModel( &k->m_patchNum ); @@ -895,9 +973,10 @@ void gigInstrumentView::modelChanged() -void gigInstrumentView::updateFilename() + +void GigInstrumentView::updateFilename() { - gigInstrument * i = castModel(); + 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 ) : @@ -912,9 +991,10 @@ void gigInstrumentView::updateFilename() -void gigInstrumentView::updatePatchName() + +void GigInstrumentView::updatePatchName() { - gigInstrument * i = castModel(); + GigInstrument * i = castModel(); QFontMetrics fm( font() ); QString patch = i->getCurrentPatchName(); m_patchLabel->setText( fm.elidedText( patch, Qt::ElideLeft, m_patchLabel->width() ) ); @@ -924,16 +1004,18 @@ void gigInstrumentView::updatePatchName() -void gigInstrumentView::invalidateFile() + +void GigInstrumentView::invalidateFile() { m_patchDialogButton->setEnabled( false ); } -void gigInstrumentView::showFileDialog() + +void GigInstrumentView::showFileDialog() { - gigInstrument * k = castModel(); + GigInstrument * k = castModel(); FileDialog ofd( NULL, tr( "Open GIG file" ) ); ofd.setFileMode( FileDialog::ExistingFiles ); @@ -979,18 +1061,20 @@ void gigInstrumentView::showFileDialog() -void gigInstrumentView::showPatchDialog() + +void GigInstrumentView::showPatchDialog() { - gigInstrument * k = castModel(); - patchesDialog pd( this ); + 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, float attenuation, const ADSR& adsr ) +GigSample::GigSample( gig::Sample * pSample, float attenuation, const ADSR & adsr ) : sample( pSample ), attenuation( attenuation ), adsr( adsr ), @@ -1000,35 +1084,37 @@ GigSample::GigSample( gig::Sample* pSample, float attenuation, const ADSR& adsr + // 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) +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 ) + if( region != NULL ) { // Parameters from GIG file - preattack = 1.0*region->EG1PreAttack/1000; // EG1PreAttack is 0-1000 permille + 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 + 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; + attackLength = attack * sampleRate; + decayLength = decay1 * sampleRate; // TODO: ignoring decay2 for now + releaseLength = release * sampleRate; } } + // Next time we get the amplitude, we'll be releasing the note void ADSR::keyup() { @@ -1037,6 +1123,7 @@ void ADSR::keyup() + // Can we delete the sample now? bool ADSR::done() { @@ -1045,19 +1132,20 @@ bool ADSR::done() + // Return the current amplitude and increment internal positions double ADSR::value() { double currentAmplitude = amplitude; // If we're done, don't output any signal - if( isDone ) + 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 && isRelease ) + else if( isAttack == true && isRelease == true ) { sustain = amplitude; isAttack = false; @@ -1065,24 +1153,34 @@ double ADSR::value() // If we're in the attack phase, start at the preattack amplitude and // increase to the full before decreasing to sustain - if( isAttack ) + if( isAttack == true ) { if( attackPosition < attackLength ) - amplitude = preattack + (attack - preattack)/attackLength*attackPosition; - else if( attackPosition < attackLength+decayLength ) - amplitude = 1.0 - (1.0-sustain)/decayLength*(attackPosition-attackLength); + { + amplitude = preattack + ( attack - 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 ) + else if( isRelease == true ) { if( releasePosition < releaseLength ) - amplitude = sustain*(1.0-1.0/releaseLength*releasePosition); + { + amplitude = sustain * ( 1.0 - 1.0 / releaseLength * releasePosition ); + } else + { isDone = true; + } ++releasePosition; } @@ -1092,13 +1190,14 @@ double ADSR::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 ) ); + return new GigInstrument( static_cast( _data ) ); } } diff --git a/plugins/gig_player/gig_player.h b/plugins/GigPlayer/GigPlayer.h similarity index 79% rename from plugins/gig_player/gig_player.h rename to plugins/GigPlayer/GigPlayer.h index 1218cceaa..51f2b2b7f 100644 --- a/plugins/gig_player/gig_player.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -1,5 +1,5 @@ /* - * gig_player.h - a gig player using libgig (based on sf2 player plugin) + * GigPlayer.h - a GIG player using libgig (based on Sf2 player plugin) * * Copyright (c) 2008 Paul Giblock * Copyright (c) 2009-2014 Tobias Doerffel @@ -40,12 +40,13 @@ #include "SampleBuffer.h" #include "gig.h" -class gigInstrumentView; +class GigInstrumentView; class NotePlayHandle; -class patchesDialog; +class PatchesDialog; class QLabel; + // Load a GIG file using libgig class GigInstance { @@ -63,14 +64,18 @@ public: } ; + + // Stores options for the notes, e.g. velocity and release time struct Dimension { Dimension() : release( false ) { - for( int i = 0; i < 8; ++i) + for( int i = 0; i < 8; ++i ) + { DimValues[i] = 0; + } } uint DimValues[8]; @@ -78,6 +83,8 @@ struct Dimension } ; + + // 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). @@ -104,38 +111,49 @@ class ADSR int releaseLength; public: - ADSR( gig::DimensionRegion* region, int sampleRate ); + ADSR( gig::DimensionRegion * region, int sampleRate ); void keyup(); // We will begin releasing starting now bool done(); // Is this sample done playing? double value(); // What's the current amplitude } ; + + // The sample from the GIG file with our current position in both the sample // and the envelope class GigSample { public: - GigSample( gig::Sample* pSample, float attenuation, const ADSR& adsr ); + GigSample( gig::Sample * pSample, float attenuation, const ADSR & adsr ); - gig::Sample* sample; + gig::Sample * sample; float attenuation; ADSR adsr; int pos; // Position in sample } ; + + // What portion of a note are we in? enum GigState { - KeyDown, // We just pressed the key - PlayingKeyDown, // The note is currently playing - KeyUp, // We just released the key - PlayingKeyUp, // The note is being released, e.g. a release sample is playing - Completed // The note is done playing, you can delete this note now + // 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 { @@ -147,7 +165,8 @@ public: QList samples; GigNote( int midiNote, int velocity ) - : midiNote(midiNote), velocity(velocity), release(false), state(KeyDown) + : midiNote( midiNote ), velocity( velocity ), + release( false ), state( KeyDown ) { } } ; @@ -155,15 +174,15 @@ public: -class gigInstrument : public Instrument +class GigInstrument : public Instrument { Q_OBJECT - mapPropertyFromModel(int,getBank,setBank,m_bankNum); - mapPropertyFromModel(int,getPatch,setPatch,m_patchNum); + mapPropertyFromModel( int, getBank, setBank, m_bankNum ); + mapPropertyFromModel( int, getPatch, setPatch, m_patchNum ); public: - gigInstrument( InstrumentTrack * _instrument_track ); - virtual ~gigInstrument(); + GigInstrument( InstrumentTrack * _instrument_track ); + virtual ~GigInstrument(); virtual void play( sampleFrame * _working_buffer ); @@ -210,8 +229,8 @@ private: SRC_STATE * m_srcState; // The GIG file and instrument we're using - GigInstance* m_instance; - gig::Instrument* m_instrument; + GigInstance * m_instance; + gig::Instrument * m_instrument; // Part of the UI QString m_filename; @@ -230,7 +249,7 @@ private: QList m_notes; // Buffer for note samples - sampleFrame* m_sampleData; + sampleFrame * m_sampleData; unsigned int m_sampleDataSize; // Used when determining which samples to use @@ -246,17 +265,17 @@ private: // Create "dimension" to select desired samples from GIG file based on // parameters such as velocity - Dimension getDimensions( gig::Region* pRegion, int velocity, bool release ); + Dimension getDimensions( gig::Region * pRegion, int velocity, bool release ); // Add the desired samples to the note, either normal samples or release // samples - void addSamples( GigNote& note, bool wantReleaseSample ); + void addSamples( GigNote & note, bool wantReleaseSample ); // Convert sample rates - bool convertSampleRate( sampleFrame& oldBuf, sampleFrame& newBuf, + bool convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, int oldSize, int newSize, int oldRate, int newRate ); - friend class gigInstrumentView; + friend class GigInstrumentView; signals: void fileLoading(); @@ -267,13 +286,14 @@ signals: -class gigInstrumentView : public InstrumentView + +class GigInstrumentView : public InstrumentView { Q_OBJECT public: - gigInstrumentView( Instrument * _instrument, + GigInstrumentView( Instrument * _instrument, QWidget * _parent ); - virtual ~gigInstrumentView(); + virtual ~GigInstrumentView(); private: virtual void modelChanged(); @@ -287,9 +307,9 @@ private: QLabel * m_filenameLabel; QLabel * m_patchLabel; - knob * m_gainKnob; + knob * m_gainKnob; - static patchesDialog * s_patchDialog; + static PatchesDialog * s_patchDialog; protected slots: void invalidateFile(); @@ -300,5 +320,4 @@ protected slots: } ; - #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/gig_player/patches_dialog.h b/plugins/GigPlayer/PatchesDialog.h similarity index 66% rename from plugins/gig_player/patches_dialog.h rename to plugins/GigPlayer/PatchesDialog.h index 6b74993f9..bb4dedde2 100644 --- a/plugins/gig_player/patches_dialog.h +++ b/plugins/GigPlayer/PatchesDialog.h @@ -1,5 +1,5 @@ /* - * patches_dialog.h - display sf2 patches + * PatchesDialog.h - display GIG patches (based on Sf2 patches_dialog.h) * * Copyright (c) 2008 Paul Giblock * @@ -23,12 +23,12 @@ */ -#ifndef _PATCHES_DIALOG_H -#define _PATCHES_DIALOG_H +#ifndef PATCHES_DIALOG_H +#define PATCHES_DIALOG_H -#include "ui_patches_dialog.h" +#include "ui_PatchesDialog.h" #include "LcdSpinBox.h" -#include "gig_player.h" +#include "GigPlayer.h" #include #include @@ -37,27 +37,27 @@ //---------------------------------------------------------------------------- // qsynthPresetForm -- UI wrapper form. -class patchesDialog : public QDialog, private Ui::patchesDialog +class PatchesDialog : public QDialog, private Ui::PatchesDialog { Q_OBJECT public: // Constructor. - patchesDialog(QWidget *pParent = 0, Qt::WindowFlags wflags = 0); + PatchesDialog( QWidget * pParent = 0, Qt::WindowFlags wflags = 0 ); // Destructor. - virtual ~patchesDialog(); + virtual ~PatchesDialog(); - void setup(GigInstance * pSynth, int iChan, const QString & _chanName, - LcdSpinBoxModel * _bankModel, LcdSpinBoxModel * _progModel, QLabel *_patchLabel ); + 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 ); + void progChanged( QTreeWidgetItem * curr, QTreeWidgetItem * prev ); protected slots: @@ -66,31 +66,27 @@ protected slots: protected: - void setBankProg(int iBank, int iProg); + void setBankProg( int iBank, int iProg ); - QTreeWidgetItem *findBankItem(int iBank); - QTreeWidgetItem *findProgItem(int iProg); + QTreeWidgetItem * findBankItem( int iBank ); + QTreeWidgetItem * findProgItem( int iProg ); bool validateForm(); private: // Instance variables. - GigInstance *m_pSynth; + 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; + QLabel * m_patchLabel; }; - #endif - diff --git a/plugins/gig_player/patches_dialog.ui b/plugins/GigPlayer/PatchesDialog.ui similarity index 98% rename from plugins/gig_player/patches_dialog.ui rename to plugins/GigPlayer/PatchesDialog.ui index 5204dc09d..2f9f8a865 100644 --- a/plugins/gig_player/patches_dialog.ui +++ b/plugins/GigPlayer/PatchesDialog.ui @@ -19,8 +19,8 @@ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - patchesDialog - + PatchesDialog + 0 diff --git a/plugins/gig_player/artwork.png b/plugins/GigPlayer/artwork.png similarity index 100% rename from plugins/gig_player/artwork.png rename to plugins/GigPlayer/artwork.png diff --git a/plugins/gig_player/fileselect_off.png b/plugins/GigPlayer/fileselect_off.png similarity index 100% rename from plugins/gig_player/fileselect_off.png rename to plugins/GigPlayer/fileselect_off.png diff --git a/plugins/gig_player/fileselect_on.png b/plugins/GigPlayer/fileselect_on.png similarity index 100% rename from plugins/gig_player/fileselect_on.png rename to plugins/GigPlayer/fileselect_on.png diff --git a/plugins/gig_player/logo.png b/plugins/GigPlayer/logo.png similarity index 100% rename from plugins/gig_player/logo.png rename to plugins/GigPlayer/logo.png diff --git a/plugins/gig_player/patches_off.png b/plugins/GigPlayer/patches_off.png similarity index 100% rename from plugins/gig_player/patches_off.png rename to plugins/GigPlayer/patches_off.png diff --git a/plugins/gig_player/patches_on.png b/plugins/GigPlayer/patches_on.png similarity index 100% rename from plugins/gig_player/patches_on.png rename to plugins/GigPlayer/patches_on.png diff --git a/plugins/gig_player/patches_dialog.cpp b/plugins/gig_player/patches_dialog.cpp deleted file mode 100644 index 7003f888b..000000000 --- a/plugins/gig_player/patches_dialog.cpp +++ /dev/null @@ -1,385 +0,0 @@ -/* - * 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); -#if QT_VERSION >= 0x050000 - pHeader->setSectionsMovable(false); -#else - pHeader->setMovable(false); -#endif - 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(); -} From 07032260e84504347cce35cf8b318b7a681118a4 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 3 Nov 2014 08:13:01 -0800 Subject: [PATCH 18/33] configManager to ConfigManager rename --- plugins/GigPlayer/GigPlayer.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 0eea3a6d0..e17d4591c 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -42,7 +42,7 @@ #include "NotePlayHandle.h" #include "knob.h" #include "song.h" -#include "config_mgr.h" +#include "ConfigManager.h" #include "PatchesDialog.h" #include "tooltip.h" @@ -1030,10 +1030,10 @@ void GigInstrumentView::showFileDialog() QString f = k->m_filename; if( QFileInfo( f ).isRelative() ) { - f = configManager::inst()->userSamplesDir() + f; + f = ConfigManager::inst()->userSamplesDir() + f; if( QFileInfo( f ).exists() == false ) { - f = configManager::inst()->factorySamplesDir() + k->m_filename; + f = ConfigManager::inst()->factorySamplesDir() + k->m_filename; } } ofd.setDirectory( QFileInfo( f ).absolutePath() ); @@ -1041,7 +1041,7 @@ void GigInstrumentView::showFileDialog() } else { - ofd.setDirectory( configManager::inst()->userSamplesDir() ); + ofd.setDirectory( ConfigManager::inst()->userSamplesDir() ); } m_fileDialogButton->setEnabled( false ); From a679e4e938c526f97da3980f6e1afddc3d591990 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 6 Nov 2014 22:25:17 -0800 Subject: [PATCH 19/33] Fixed detuned resampling Apparently the most noticeable detuning issues were caused by rounding error by integer division. --- plugins/GigPlayer/GigPlayer.cpp | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index e17d4591c..bf7abf468 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -450,7 +450,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) // Read a different number of samples depending on the sample rate, but // resample it to always output the right number of frames - samples = frames * oldRate / newRate; + samples = round( (double) frames * oldRate / newRate ); // Buffer for the resampled data convertBuf = new sampleFrame[samples]; @@ -532,7 +532,8 @@ void GigInstrument::play( sampleFrame * _working_buffer ) for( int i = 0; i < samples; ++i ) { - m_sampleData[i][0] = 1.0 / 0x10000 * pInt[ sample->sample->Channels * i ] * sample->attenuation; + m_sampleData[i][0] = 1.0 / 0x10000 * + pInt[ sample->sample->Channels * i ] * sample->attenuation; if( sample->sample->Channels == 1 ) { @@ -540,7 +541,8 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } else { - m_sampleData[i][1] = 1.0 / 0x10000 * pInt[ sample->sample->Channels * i + 1 ] * sample->attenuation; + m_sampleData[i][1] = 1.0 / 0x10000 * + pInt[ sample->sample->Channels * i + 1 ] * sample->attenuation; } } } @@ -557,7 +559,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } // Save to output buffer - if( sampleConvert ) + if( sampleConvert == true ) { for( int i = 0; i < samples; ++i ) { @@ -577,10 +579,10 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } // Convert sample rate if needed - if( sampleConvert ) + if( sampleConvert == true ) { - // If an error occured, it's better to render nothing than have some - // screetching high-volume noise + // If an error occurred, it's better to render nothing than have some + // screeching high-volume noise if( !convertSampleRate( *convertBuf, *_working_buffer, samples, frames, oldRate, newRate ) ) { std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) ); @@ -846,6 +848,12 @@ bool GigInstrument::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBu return false; } + if( src_data.input_frames_used == 0 || src_data.output_frames_gen == 0 ) + { + qCritical( "GigInstrument: could not resample, no frames generated" ); + return false; + } + return true; } From d0a821947af2765615f13d39e5940707e19ac51a Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 9 Nov 2014 12:22:45 -0800 Subject: [PATCH 20/33] Fixed resampling issues When providing extra frames, libsamplerate stores the extras internally and outputs them in the next period. But, when it has enough internally to output a whole period, it just outputs the internal buffer while not using any more input frames. Now I provide some extra frames, check to see how many frames we used actually used, and update the sample positions and ADSR accordingly. --- plugins/GigPlayer/GigPlayer.cpp | 69 +++++++++++++++++++++++++++------ plugins/GigPlayer/GigPlayer.h | 3 +- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index bf7abf468..e475efa98 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -72,6 +72,12 @@ Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor = +// Margins for extra samples to provide to libsamplerate to reduce glitching +const f_cnt_t GIGMARGIN[] = { 128, 64, 64, 4, 4 }; + + + + struct GIGPluginData { int midiNote; @@ -340,6 +346,7 @@ void GigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) void GigInstrument::play( sampleFrame * _working_buffer ) { const fpp_t frames = engine::mixer()->framesPerPeriod(); + const int interpolation = engine::mixer()->currentQualitySettings().libsrcInterpolation(); // Initialize to zeros std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) ); @@ -364,6 +371,9 @@ void GigInstrument::play( sampleFrame * _working_buffer ) // How many frames we'll be grabbing from the sample int samples = frames; + // How many we actually used from the sample + int used = frames; + for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) { // Process notes in the KeyUp state, adding release samples if desired @@ -450,7 +460,10 @@ void GigInstrument::play( sampleFrame * _working_buffer ) // Read a different number of samples depending on the sample rate, but // resample it to always output the right number of frames - samples = round( (double) frames * oldRate / newRate ); + samples = frames * oldRate / newRate; + + // We need a bit of margin so we don't get glitching + samples += GIGMARGIN[interpolation]; // Buffer for the resampled data convertBuf = new sampleFrame[samples]; @@ -494,9 +507,6 @@ void GigInstrument::play( sampleFrame * _working_buffer ) buf.NullExtensionSize = allocationsize - buf.Size; std::memset( (int8_t*) buf.pStart + buf.Size, 0, buf.NullExtensionSize ); - // Save the new position in the sample - sample->pos = sample->sample->GetPos(); - // Convert from 16 or 24 bit into 32-bit float if( sample->sample->BitDepth == 24 ) // 24 bit { @@ -550,10 +560,13 @@ void GigInstrument::play( sampleFrame * _working_buffer ) // Cleanup delete[] (int8_t*) buf.pStart; - // Apply ADSR + // 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( int i = 0; i < samples; ++i ) { - double amplitude = sample->adsr.value(); + double amplitude = copy.value(); m_sampleData[i][0] *= amplitude; m_sampleData[i][1] *= amplitude; } @@ -583,14 +596,37 @@ void GigInstrument::play( sampleFrame * _working_buffer ) { // If an error occurred, it's better to render nothing than have some // screeching high-volume noise - if( !convertSampleRate( *convertBuf, *_working_buffer, samples, frames, oldRate, newRate ) ) + if( !convertSampleRate( *convertBuf, *_working_buffer, samples, frames, + oldRate, newRate, used ) ) { - std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) ); + std::memset( &_working_buffer[0][0], 0, + DEFAULT_CHANNELS * frames * sizeof( float ) ); } delete[] convertBuf; } + // Update the note positions with how many samples we actually used + for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + { + // Only process the notes if we're in a playing state + if( !( note->state == PlayingKeyDown || + note->state == PlayingKeyUp ) ) + { + continue; + } + + for( QList::iterator sample = note->samples.begin(); + sample != note->samples.end(); ++sample ) + { + if( sample->sample != NULL ) + { + sample->pos += used; + sample->adsr.inc( used ); + } + } + } + m_notesMutex.unlock(); m_synthMutex.unlock(); @@ -822,20 +858,22 @@ void GigInstrument::getInstrument() bool GigInstrument::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, - int oldSize, int newSize, int oldRate, int newRate ) + int oldSize, int newSize, int oldRate, int newRate, int& used ) { 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 = (double) newSize / oldSize; + src_data.src_ratio = (double) newRate / oldRate; src_data.end_of_input = 0; m_srcMutex.lock(); int error = src_process( m_srcState, &src_data ); m_srcMutex.unlock(); + used = src_data.input_frames_used; + if( error != 0 ) { qCritical( "GigInstrument: error while resampling: %s", src_strerror( error ) ); @@ -848,7 +886,7 @@ bool GigInstrument::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBu return false; } - if( src_data.input_frames_used == 0 || src_data.output_frames_gen == 0 ) + if( src_data.output_frames_gen == 0 ) { qCritical( "GigInstrument: could not resample, no frames generated" ); return false; @@ -1196,6 +1234,15 @@ double ADSR::value() return currentAmplitude; } +// Increment internal positions a certain number of times +void ADSR::inc( int num ) +{ + for( int i = 0; i < num; ++i ) + { + value(); + } +} + diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 51f2b2b7f..03a207650 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -115,6 +115,7 @@ public: void keyup(); // We will begin releasing starting now bool done(); // Is this sample done playing? double value(); // What's the current amplitude + void inc( int num ); // Increment internal positions by num } ; @@ -273,7 +274,7 @@ private: // Convert sample rates bool convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, - int oldSize, int newSize, int oldRate, int newRate ); + int oldSize, int newSize, int oldRate, int newRate, int& used ); friend class GigInstrumentView; From d1bf19ef4af91c0743caf92f4a579e529f16bdee Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 9 Nov 2014 12:31:01 -0800 Subject: [PATCH 21/33] Don't use "note" as a variable name --- plugins/GigPlayer/GigPlayer.cpp | 76 ++++++++++++++++----------------- plugins/GigPlayer/GigPlayer.h | 2 +- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index e475efa98..39033ecec 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -374,51 +374,51 @@ void GigInstrument::play( sampleFrame * _working_buffer ) // How many we actually used from the sample int used = frames; - for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) { // Process notes in the KeyUp state, adding release samples if desired - if( note->state == KeyUp ) + if( it->state == KeyUp ) { // If there are no samples, we're done - if( note->samples.empty() ) + if( it->samples.empty() ) { - note->state = Completed; + it->state = Completed; } else { - note->state = PlayingKeyUp; + it->state = PlayingKeyUp; // Notify each sample that the key has been released - for( QList::iterator sample = note->samples.begin(); - sample != note->samples.end(); ++sample ) + for( QList::iterator sample = it->samples.begin(); + sample != it->samples.end(); ++sample ) { sample->adsr.keyup(); } // Add release samples if available - if( note->release == true ) + if( it->release == true ) { - addSamples( *note, true ); + addSamples( *it, true ); } } } // Process notes in the KeyDown state, adding samples for the notes - else if( note->state == KeyDown ) + else if( it->state == KeyDown ) { - note->state = PlayingKeyDown; - addSamples( *note, false ); + it->state = PlayingKeyDown; + addSamples( *it, false ); } - for( QList::iterator sample = note->samples.begin(); - sample != note->samples.end(); ++sample ) + for( QList::iterator sample = it->samples.begin(); + sample != it->samples.end(); ++sample ) { // Delete ended samples if( sample->sample == NULL || sample->adsr.done() || sample->pos >= sample->sample->SamplesTotal - 1 ) { - sample = note->samples.erase( sample ); + sample = it->samples.erase( sample ); - if( sample == note->samples.end() ) + if( sample == it->samples.end() ) { break; } @@ -441,11 +441,11 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } // Delete ended notes (either in the completed state or all the samples ended) - if( note->state == Completed || note->samples.empty() ) + if( it->state == Completed || it->samples.empty() ) { - note = m_notes.erase( note ); + it = m_notes.erase( it ); - if( note == m_notes.end() ) + if( it == m_notes.end() ) { break; } @@ -479,17 +479,17 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } // Fill buffer with portions of the note samples - for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) { // Only process the notes if we're in a playing state - if( !( note->state == PlayingKeyDown || - note->state == PlayingKeyUp ) ) + if( !( it->state == PlayingKeyDown || + it->state == PlayingKeyUp ) ) { continue; } - for( QList::iterator sample = note->samples.begin(); - sample != note->samples.end(); ++sample ) + for( QList::iterator sample = it->samples.begin(); + sample != it->samples.end(); ++sample ) { if( sample->sample == NULL ) { @@ -607,17 +607,17 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } // Update the note positions with how many samples we actually used - for( QList::iterator note = m_notes.begin(); note != m_notes.end(); ++note ) + for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) { // Only process the notes if we're in a playing state - if( !( note->state == PlayingKeyDown || - note->state == PlayingKeyUp ) ) + if( !( it->state == PlayingKeyDown || + it->state == PlayingKeyUp ) ) { continue; } - for( QList::iterator sample = note->samples.begin(); - sample != note->samples.end(); ++sample ) + for( QList::iterator sample = it->samples.begin(); + sample != it->samples.end(); ++sample ) { if( sample->sample != NULL ) { @@ -681,15 +681,15 @@ PluginView * GigInstrument::instantiateView( QWidget * _parent ) // // Note: not thread safe since libgig stores current region position data in // the instrument object -void GigInstrument::addSamples( GigNote & note, bool wantReleaseSample ) +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 && - note.midiNote >= m_instrument->DimensionKeyRange.low && - note.midiNote <= m_instrument->DimensionKeyRange.high ) + gignote.midiNote >= m_instrument->DimensionKeyRange.low && + gignote.midiNote <= m_instrument->DimensionKeyRange.high ) { - m_currentKeyDimension = float( note.midiNote - + m_currentKeyDimension = float( gignote.midiNote - m_instrument->DimensionKeyRange.low ) / ( m_instrument->DimensionKeyRange.high - m_instrument->DimensionKeyRange.low + 1 ); @@ -699,7 +699,7 @@ void GigInstrument::addSamples( GigNote & note, bool wantReleaseSample ) while( pRegion != NULL ) { - Dimension dim = getDimensions( pRegion, note.velocity, wantReleaseSample ); + Dimension dim = getDimensions( pRegion, gignote.velocity, wantReleaseSample ); gig::DimensionRegion * pDimRegion = pRegion->GetDimensionRegionByValue( dim.DimValues ); gig::Sample * pSample = pDimRegion->pSample; @@ -707,7 +707,7 @@ void GigInstrument::addSamples( GigNote & note, bool wantReleaseSample ) // notes and not when we get the release samples. if( wantReleaseSample != true ) { - note.release = dim.release; + gignote.release = dim.release; } if( pSample != NULL && pSample->SamplesTotal != 0 ) @@ -715,9 +715,9 @@ void GigInstrument::addSamples( GigNote & note, bool wantReleaseSample ) int keyLow = pRegion->KeyRange.low; int keyHigh = pRegion->KeyRange.high; - if( note.midiNote >= keyLow && note.midiNote <= keyHigh ) + if( gignote.midiNote >= keyLow && gignote.midiNote <= keyHigh ) { - float attenuation = pDimRegion->GetVelocityAttenuation( note.velocity );; + float attenuation = pDimRegion->GetVelocityAttenuation( gignote.velocity );; float length = (double) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); // TODO: sample panning? looping? crossfade different layers? @@ -732,7 +732,7 @@ void GigInstrument::addSamples( GigNote & note, bool wantReleaseSample ) attenuation *= pDimRegion->SampleAttenuation; } - note.samples.push_back( GigSample( pSample, attenuation, + gignote.samples.push_back( GigSample( pSample, attenuation, ADSR( pDimRegion, pSample->SamplesPerSecond ) ) ); } } diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 03a207650..b64bfff7f 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -270,7 +270,7 @@ private: // Add the desired samples to the note, either normal samples or release // samples - void addSamples( GigNote & note, bool wantReleaseSample ); + void addSamples( GigNote & gignote, bool wantReleaseSample ); // Convert sample rates bool convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, From c0ad77dfcad3b134c3a8e298f4ad34251504327c Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 9 Nov 2014 12:40:51 -0800 Subject: [PATCH 22/33] Stack buffers instead of allocating on the heap --- plugins/GigPlayer/GigPlayer.cpp | 54 ++++++++++----------------------- plugins/GigPlayer/GigPlayer.h | 4 --- 2 files changed, 16 insertions(+), 42 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 39033ecec..0c439464d 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -95,8 +95,6 @@ GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : 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_sampleData( NULL ), - m_sampleDataSize( 0 ), m_RandomSeed( 0 ), m_currentKeyDimension( 0 ) { @@ -108,11 +106,6 @@ GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : connect( &m_bankNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) ); connect( &m_patchNum, SIGNAL( dataChanged() ), this, SLOT( updatePatch() ) ); connect( engine::mixer(), SIGNAL( sampleRateChanged() ), this, SLOT( updateSampleRate() ) ); - - // Buffer for reading samples - const fpp_t frames = engine::mixer()->framesPerPeriod(); - m_sampleData = new sampleFrame[frames]; - m_sampleDataSize = frames; } @@ -120,11 +113,6 @@ GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : GigInstrument::~GigInstrument() { - if( m_sampleData != NULL ) - { - delete[] m_sampleData; - } - engine::mixer()->removePlayHandles( instrumentTrack() ); freeInstance(); @@ -366,7 +354,6 @@ void GigInstrument::play( sampleFrame * _working_buffer ) int newRate = engine::mixer()->processingSampleRate(); bool sampleError = false; bool sampleConvert = false; - sampleFrame * convertBuf = NULL; // How many frames we'll be grabbing from the sample int samples = frames; @@ -464,19 +451,12 @@ void GigInstrument::play( sampleFrame * _working_buffer ) // We need a bit of margin so we don't get glitching samples += GIGMARGIN[interpolation]; - - // Buffer for the resampled data - convertBuf = new sampleFrame[samples]; - std::memset( &convertBuf[0][0], 0, DEFAULT_CHANNELS * samples * sizeof( float ) ); } - // Recreate buffer if it is of a different size - if( m_sampleDataSize != samples ) - { - delete[] m_sampleData; - m_sampleData = new sampleFrame[samples]; - m_sampleDataSize = samples; - } + // Create buffers + sampleFrame sampleData[samples]; // Individual note signal, overwritten for each note + sampleFrame convertBuf[samples]; // Sum of note signals + std::memset( &convertBuf[0][0], 0, DEFAULT_CHANNELS * samples * sizeof( float ) ); // Fill buffer with portions of the note samples for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) @@ -520,11 +500,11 @@ void GigInstrument::play( sampleFrame * _working_buffer ) // Store the notes to this buffer before saving to output // so we can fade them out as needed - m_sampleData[i][0] = 1.0 / 0x100000000 * sample->attenuation * valueLeft; + sampleData[i][0] = 1.0 / 0x100000000 * sample->attenuation * valueLeft; if( sample->sample->Channels == 1 ) { - m_sampleData[i][1] = m_sampleData[i][0]; + sampleData[i][1] = sampleData[i][0]; } else { @@ -532,7 +512,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) ( pInt[ 3 * sample->sample->Channels * i + 4 ] << 16 ) | ( pInt[ 3 * sample->sample->Channels * i + 5 ] << 24 ); - m_sampleData[i][1] = 1.0 / 0x100000000 * sample->attenuation * valueRight; + sampleData[i][1] = 1.0 / 0x100000000 * sample->attenuation * valueRight; } } } @@ -542,16 +522,16 @@ void GigInstrument::play( sampleFrame * _working_buffer ) for( int i = 0; i < samples; ++i ) { - m_sampleData[i][0] = 1.0 / 0x10000 * + sampleData[i][0] = 1.0 / 0x10000 * pInt[ sample->sample->Channels * i ] * sample->attenuation; if( sample->sample->Channels == 1 ) { - m_sampleData[i][1] = m_sampleData[i][0]; + sampleData[i][1] = sampleData[i][0]; } else { - m_sampleData[i][1] = 1.0 / 0x10000 * + sampleData[i][1] = 1.0 / 0x10000 * pInt[ sample->sample->Channels * i + 1 ] * sample->attenuation; } } @@ -567,8 +547,8 @@ void GigInstrument::play( sampleFrame * _working_buffer ) for( int i = 0; i < samples; ++i ) { double amplitude = copy.value(); - m_sampleData[i][0] *= amplitude; - m_sampleData[i][1] *= amplitude; + sampleData[i][0] *= amplitude; + sampleData[i][1] *= amplitude; } // Save to output buffer @@ -576,16 +556,16 @@ void GigInstrument::play( sampleFrame * _working_buffer ) { for( int i = 0; i < samples; ++i ) { - convertBuf[i][0] += m_sampleData[i][0]; - convertBuf[i][1] += m_sampleData[i][1]; + convertBuf[i][0] += sampleData[i][0]; + convertBuf[i][1] += sampleData[i][1]; } } else { for( int i = 0; i < frames; ++i ) { - _working_buffer[i][0] += m_sampleData[i][0]; - _working_buffer[i][1] += m_sampleData[i][1]; + _working_buffer[i][0] += sampleData[i][0]; + _working_buffer[i][1] += sampleData[i][1]; } } } @@ -602,8 +582,6 @@ void GigInstrument::play( sampleFrame * _working_buffer ) std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) ); } - - delete[] convertBuf; } // Update the note positions with how many samples we actually used diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index b64bfff7f..32a86e983 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -249,10 +249,6 @@ private: // List of all the currently playing notes QList m_notes; - // Buffer for note samples - sampleFrame * m_sampleData; - unsigned int m_sampleDataSize; - // Used when determining which samples to use uint32_t m_RandomSeed; float m_currentKeyDimension; From 822a3c52bb58f2fd8f109353b387b02474c01bdd Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 9 Nov 2014 21:06:48 -0800 Subject: [PATCH 23/33] Convert 24-bit data if on big endian system This is needed since libgig returns 24-bit data in a little endian. Note: untested as I don't have a big endian system. --- plugins/GigPlayer/GigPlayer.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 0c439464d..f678bf7bc 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -43,6 +43,7 @@ #include "knob.h" #include "song.h" #include "ConfigManager.h" +#include "endian_handling.h" #include "PatchesDialog.h" #include "tooltip.h" @@ -494,9 +495,12 @@ void GigInstrument::play( sampleFrame * _working_buffer ) for( int i = 0; i < samples; ++i ) { - int32_t valueLeft = ( pInt[ 3 * sample->sample->Channels * i ] << 8 ) | + // 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 ); + ( 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 @@ -508,9 +512,10 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } else { - int32_t valueRight = ( pInt[ 3 * sample->sample->Channels * i + 3 ] << 8 ) | + 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 ); + ( pInt[ 3 * sample->sample->Channels * i + 5 ] << 24 ) ); sampleData[i][1] = 1.0 / 0x100000000 * sample->attenuation * valueRight; } From 4e8508b8a3b3c41ea5d6f4dfe2dffe4fad868b4b Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 9 Nov 2014 22:41:46 -0800 Subject: [PATCH 24/33] Exponential decay for release instead of linear It now sounds much more like the release in Linux Sampler. --- plugins/GigPlayer/GigPlayer.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index f678bf7bc..0e6a97a99 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -1204,7 +1204,15 @@ double ADSR::value() { if( releasePosition < releaseLength ) { - amplitude = sustain * ( 1.0 - 1.0 / releaseLength * releasePosition ); + // Maybe not the best way of doing this, but it appears to be about right + amplitude = sustain * exp( -5.0 / releaseLength * releasePosition ) - 1e-5; + + // Don't have an infinite exponential decay + if( amplitude < 0 ) + { + amplitude = 0; + isDone = true; + } } else { From a25139124923f61217b98543c433d85d8e8bc0e3 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 10 Nov 2014 18:32:20 -0800 Subject: [PATCH 25/33] Always use linear interpolation for resampling --- plugins/GigPlayer/GigPlayer.cpp | 17 +++++++---------- plugins/GigPlayer/GigPlayer.h | 3 +++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 0e6a97a99..363ac3681 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -73,12 +73,6 @@ Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor = -// Margins for extra samples to provide to libsamplerate to reduce glitching -const f_cnt_t GIGMARGIN[] = { 128, 64, 64, 4, 4 }; - - - - struct GIGPluginData { int midiNote; @@ -96,6 +90,7 @@ GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : 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 ) { @@ -335,7 +330,6 @@ void GigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) void GigInstrument::play( sampleFrame * _working_buffer ) { const fpp_t frames = engine::mixer()->framesPerPeriod(); - const int interpolation = engine::mixer()->currentQualitySettings().libsrcInterpolation(); // Initialize to zeros std::memset( &_working_buffer[0][0], 0, DEFAULT_CHANNELS * frames * sizeof( float ) ); @@ -451,7 +445,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) samples = frames * oldRate / newRate; // We need a bit of margin so we don't get glitching - samples += GIGMARGIN[interpolation]; + samples += MARGIN[m_interpolation]; } // Create buffers @@ -881,6 +875,10 @@ bool GigInstrument::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBu +// Recreate the libsamplerate instance each time the sample rate is changed. +// We could also create it once and then reset it whenever the sample rate +// changes. However, if we ever switch to changing interpolations, you have to +// recreate it. void GigInstrument::updateSampleRate() { QMutexLocker locker( &m_srcMutex ); @@ -891,8 +889,7 @@ void GigInstrument::updateSampleRate() } int error; - m_srcState = src_new( engine::mixer()->currentQualitySettings().libsrcInterpolation(), - DEFAULT_CHANNELS, &error ); + m_srcState = src_new( m_interpolation, DEFAULT_CHANNELS, &error ); if( m_srcState == NULL || error != 0 ) { diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 32a86e983..61d07e0ba 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -246,6 +246,9 @@ private: QMutex m_srcMutex; QMutex m_notesMutex; + // Used for resampling + int m_interpolation; + // List of all the currently playing notes QList m_notes; From 8693623758edfffa7e917660df1d251423edb29e Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 11 Nov 2014 09:39:24 -0800 Subject: [PATCH 26/33] Fixed resampling glitches when deleting notes Moving the code to detect the sample rates of the currently used samples after the code deleting notes seemed to fix these glitches. Also, fixed a few ADSR issues that could have resulted in clipping in the attack or glitching after the release. --- plugins/GigPlayer/GigPlayer.cpp | 56 +++++++++++++++------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 363ac3681..1df0303fe 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -391,10 +391,10 @@ void GigInstrument::play( sampleFrame * _working_buffer ) addSamples( *it, false ); } + // Delete ended samples for( QList::iterator sample = it->samples.begin(); sample != it->samples.end(); ++sample ) { - // Delete ended samples if( sample->sample == NULL || sample->adsr.done() || sample->pos >= sample->sample->SamplesTotal - 1 ) { @@ -405,21 +405,6 @@ void GigInstrument::play( sampleFrame * _working_buffer ) break; } } - // Verify all the samples have the same rate - else - { - int currentRate = sample->sample->SamplesPerSecond; - - if( oldRate == -1 ) - { - oldRate = currentRate; - } - else if( oldRate != currentRate ) - { - qCritical() << "GigInstrument: not all samples are the same rate, not converting"; - sampleError = true; - } - } } // Delete ended notes (either in the completed state or all the samples ended) @@ -432,6 +417,23 @@ void GigInstrument::play( sampleFrame * _working_buffer ) break; } } + + // Verify all the samples have the same rate + for( QList::iterator sample = it->samples.begin(); + sample != it->samples.end(); ++sample ) + { + int currentRate = sample->sample->SamplesPerSecond; + + if( oldRate == -1 ) + { + oldRate = currentRate; + } + else if( oldRate != currentRate ) + { + qCritical() << "GigInstrument: not all samples are the same rate, not converting"; + sampleError = true; + } + } } // If all samples have the same sample rate and it's not the output sample @@ -863,7 +865,7 @@ bool GigInstrument::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBu return false; } - if( src_data.output_frames_gen == 0 ) + if( oldSize != 0 && src_data.output_frames_gen == 0 ) { qCritical( "GigInstrument: could not resample, no frames generated" ); return false; @@ -1183,7 +1185,7 @@ double ADSR::value() { if( attackPosition < attackLength ) { - amplitude = preattack + ( attack - preattack ) / attackLength * attackPosition; + amplitude = preattack + ( 1.0 - preattack ) / attackLength * attackPosition; } else if( attackPosition < attackLength + decayLength ) { @@ -1199,20 +1201,14 @@ double ADSR::value() // If we're in the sustain phase, decrease from sustain to zero else if( isRelease == true ) { - if( releasePosition < releaseLength ) - { - // Maybe not the best way of doing this, but it appears to be about right - amplitude = sustain * exp( -5.0 / releaseLength * releasePosition ) - 1e-5; + // 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 ) * exp( -5.0 / releaseLength * releasePosition ) - 1e-3; - // Don't have an infinite exponential decay - if( amplitude < 0 ) - { - amplitude = 0; - isDone = true; - } - } - else + // Don't have an infinite exponential decay + if( amplitude < 0 ) { + amplitude = 0; isDone = true; } From 2c0b1ef4b08c6c7169cf6c9fde4a0fd2b83bd8b0 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 13 Nov 2014 14:36:02 -0800 Subject: [PATCH 27/33] Use stack array for buffer instead of gig::buffer_t --- plugins/GigPlayer/GigPlayer.cpp | 40 +++++++++++++++------------------ 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 1df0303fe..0604713bd 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -28,6 +28,7 @@ * */ + #include #include #include @@ -477,17 +478,16 @@ void GigInstrument::play( sampleFrame * _working_buffer ) sample->sample->SetPos( sample->pos ); // Note: not thread safe // Load the next portion of the sample - gig::buffer_t buf; unsigned long allocationsize = samples * sample->sample->FrameSize; - buf.pStart = new int8_t[allocationsize]; - buf.Size = sample->sample->Read( buf.pStart, samples ) * sample->sample->FrameSize; - buf.NullExtensionSize = allocationsize - buf.Size; - std::memset( (int8_t*) buf.pStart + buf.Size, 0, buf.NullExtensionSize ); + int8_t buffer[allocationsize]; + unsigned long size = sample->sample->Read( &buffer, samples ) * sample->sample->FrameSize; + unsigned long nullExtensionSize = allocationsize - size; + std::memset( (int8_t*) &buffer + size, 0, nullExtensionSize ); // Convert from 16 or 24 bit into 32-bit float if( sample->sample->BitDepth == 24 ) // 24 bit { - uint8_t * pInt = static_cast( buf.pStart ); + uint8_t * pInt = reinterpret_cast( &buffer ); for( int i = 0; i < samples; ++i ) { @@ -519,7 +519,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } else // 16 bit { - int16_t * pInt = static_cast( buf.pStart ); + int16_t * pInt = reinterpret_cast( &buffer ); for( int i = 0; i < samples; ++i ) { @@ -538,9 +538,6 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } } - // Cleanup - delete[] (int8_t*) buf.pStart; - // 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; @@ -588,20 +585,16 @@ void GigInstrument::play( sampleFrame * _working_buffer ) // Update the note positions with how many samples we actually used 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 ) ) + if( it->state == PlayingKeyDown || it->state == PlayingKeyUp ) { - continue; - } - - for( QList::iterator sample = it->samples.begin(); - sample != it->samples.end(); ++sample ) - { - if( sample->sample != NULL ) + for( QList::iterator sample = it->samples.begin(); + sample != it->samples.end(); ++sample ) { - sample->pos += used; - sample->adsr.inc( used ); + if( sample->sample != NULL ) + { + sample->pos += used; + sample->adsr.inc( used ); + } } } } @@ -1218,6 +1211,9 @@ double ADSR::value() return currentAmplitude; } + + + // Increment internal positions a certain number of times void ADSR::inc( int num ) { From 71b681472990a791073cc7f889c99439adb3959b Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 17 Nov 2014 16:55:38 -0800 Subject: [PATCH 28/33] Change pitch of notes if PitchTrack is set Now if a Gig file provides a few samples per octave, it'll change the pitch of the sample specified for a note instead of just assuming it is the right pitch. Also, fixed issue where if attack length was zero the note would never sound. --- plugins/GigPlayer/GigPlayer.cpp | 376 ++++++++++++++++++-------------- plugins/GigPlayer/GigPlayer.h | 60 +++-- 2 files changed, 257 insertions(+), 179 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 0604713bd..243038eb9 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -84,7 +84,6 @@ struct GIGPluginData GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &gigplayer_plugin_descriptor ), - m_srcState( NULL ), m_instance( NULL ), m_instrument( NULL ), m_filename( "" ), @@ -112,11 +111,6 @@ GigInstrument::~GigInstrument() { engine::mixer()->removePlayHandles( instrumentTrack() ); freeInstance(); - - if( m_srcState != NULL ) - { - src_delete( m_srcState ); - } } @@ -319,7 +313,7 @@ void GigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) const uint velocity = _n->midiVelocity( baseVelocity ); QMutexLocker locker( &m_notesMutex ); - m_notes.push_back( GigNote( midiNote, velocity ) ); + m_notes.push_back( GigNote( midiNote, velocity, _n->unpitchedFrequency() ) ); } } @@ -331,6 +325,7 @@ void GigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) 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 ) ); @@ -345,18 +340,6 @@ void GigInstrument::play( sampleFrame * _working_buffer ) return; } - // Data for converting sample rate - int oldRate = -1; - int newRate = engine::mixer()->processingSampleRate(); - bool sampleError = false; - bool sampleConvert = false; - - // How many frames we'll be grabbing from the sample - int samples = frames; - - // How many we actually used from the sample - int used = frames; - for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) { // Process notes in the KeyUp state, adding release samples if desired @@ -418,44 +401,8 @@ void GigInstrument::play( sampleFrame * _working_buffer ) break; } } - - // Verify all the samples have the same rate - for( QList::iterator sample = it->samples.begin(); - sample != it->samples.end(); ++sample ) - { - int currentRate = sample->sample->SamplesPerSecond; - - if( oldRate == -1 ) - { - oldRate = currentRate; - } - else if( oldRate != currentRate ) - { - qCritical() << "GigInstrument: not all samples are the same rate, not converting"; - sampleError = true; - } - } } - // If all samples have the same sample rate and it's not the output sample - // rate, then we'll convert sample rates - if( oldRate != -1 && sampleError != true && oldRate != newRate ) - { - sampleConvert = true; - - // Read a different number of samples depending on the sample rate, but - // resample it to always output the right number of frames - samples = frames * oldRate / newRate; - - // We need a bit of margin so we don't get glitching - samples += MARGIN[m_interpolation]; - } - - // Create buffers - sampleFrame sampleData[samples]; // Individual note signal, overwritten for each note - sampleFrame convertBuf[samples]; // Sum of note signals - std::memset( &convertBuf[0][0], 0, DEFAULT_CHANNELS * samples * sizeof( float ) ); - // Fill buffer with portions of the note samples for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) { @@ -474,6 +421,35 @@ void GigInstrument::play( sampleFrame * _working_buffer ) continue; } + // Will change if resampling + bool resample = false; + int samples = frames; // How many to grab + int 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->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->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]; + } + + // This note's data + sampleFrame sampleData[samples]; + // Set the position to where we currently are in the sample sample->sample->SetPos( sample->pos ); // Note: not thread safe @@ -544,18 +520,25 @@ void GigInstrument::play( sampleFrame * _working_buffer ) for( int i = 0; i < samples; ++i ) { - double amplitude = copy.value(); + float amplitude = copy.value(); sampleData[i][0] *= amplitude; sampleData[i][1] *= amplitude; } - // Save to output buffer - if( sampleConvert == true ) + // Output the data resampling if needed + if( resample == true ) { - for( int i = 0; i < samples; ++i ) + sampleFrame convertBuf[frames]; + + // Only output if resampling is successful (note that "used" is output) + if( sample->convertSampleRate( *sampleData, *convertBuf, samples, frames, + freq_factor, used ) ) { - convertBuf[i][0] += sampleData[i][0]; - convertBuf[i][1] += sampleData[i][1]; + for( int i = 0; i < frames; ++i ) + { + _working_buffer[i][0] += convertBuf[i][0]; + _working_buffer[i][1] += convertBuf[i][1]; + } } } else @@ -566,36 +549,10 @@ void GigInstrument::play( sampleFrame * _working_buffer ) _working_buffer[i][1] += sampleData[i][1]; } } - } - } - // Convert sample rate if needed - if( sampleConvert == true ) - { - // If an error occurred, it's better to render nothing than have some - // screeching high-volume noise - if( !convertSampleRate( *convertBuf, *_working_buffer, samples, frames, - oldRate, newRate, used ) ) - { - std::memset( &_working_buffer[0][0], 0, - DEFAULT_CHANNELS * frames * sizeof( float ) ); - } - } - - // Update the note positions with how many samples we actually used - for( QList::iterator it = m_notes.begin(); it != m_notes.end(); ++it ) - { - if( it->state == PlayingKeyDown || it->state == PlayingKeyUp ) - { - for( QList::iterator sample = it->samples.begin(); - sample != it->samples.end(); ++sample ) - { - if( sample->sample != NULL ) - { - sample->pos += used; - sample->adsr.inc( used ); - } - } + // Update note position with how many samples we actually used + sample->pos += used; + sample->adsr.inc( used ); } } @@ -690,7 +647,7 @@ void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample ) if( gignote.midiNote >= keyLow && gignote.midiNote <= keyHigh ) { float attenuation = pDimRegion->GetVelocityAttenuation( gignote.velocity );; - float length = (double) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); + float length = (float) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); // TODO: sample panning? looping? crossfade different layers? @@ -704,8 +661,8 @@ void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample ) attenuation *= pDimRegion->SampleAttenuation; } - gignote.samples.push_back( GigSample( pSample, attenuation, - ADSR( pDimRegion, pSample->SamplesPerSecond ) ) ); + gignote.samples.push_back( GigSample( pSample, pDimRegion, + attenuation, m_interpolation, gignote.frequency ) ); } } @@ -829,67 +786,13 @@ void GigInstrument::getInstrument() -bool GigInstrument::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, - int oldSize, int newSize, int oldRate, int newRate, int& used ) -{ - 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 = (double) newRate / oldRate; - src_data.end_of_input = 0; - - m_srcMutex.lock(); - int error = src_process( m_srcState, &src_data ); - m_srcMutex.unlock(); - - used = src_data.input_frames_used; - - if( error != 0 ) - { - qCritical( "GigInstrument: error while resampling: %s", src_strerror( error ) ); - return false; - } - - if( src_data.output_frames_gen > newSize ) - { - qCritical( "GigInstrument: not enough frames: %ld / %d", src_data.output_frames_gen, newSize ); - return false; - } - - if( oldSize != 0 && src_data.output_frames_gen == 0 ) - { - qCritical( "GigInstrument: could not resample, no frames generated" ); - return false; - } - - return true; -} - - - - -// Recreate the libsamplerate instance each time the sample rate is changed. -// We could also create it once and then reset it whenever the sample rate -// changes. However, if we ever switch to changing interpolations, you have to -// recreate it. +// 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_srcMutex ); - - if( m_srcState != NULL ) - { - src_delete( m_srcState ); - } - - int error; - m_srcState = src_new( m_interpolation, DEFAULT_CHANNELS, &error ); - - if( m_srcState == NULL || error != 0 ) - { - qCritical( "error while creating libsamplerate data structure in GigInstrument::updateSampleRate()" ); - } + QMutexLocker locker( &m_notesMutex ); + m_notes.clear(); } @@ -1095,11 +998,160 @@ void GigInstrumentView::showPatchDialog() // Store information related to playing a sample from the GIG file -GigSample::GigSample( gig::Sample * pSample, float attenuation, const ADSR & adsr ) - : sample( pSample ), - attenuation( attenuation ), - adsr( adsr ), - pos( 0 ) +GigSample::GigSample( gig::Sample * pSample, gig::DimensionRegion * pDimRegion, + float attenuation, int interpolation, float desiredFreq ) + : sample( pSample ), attenuation( attenuation ), pos( 0 ), + pitchtrack( false ), interpolation( interpolation ), + srcState( NULL ), sampleFreq( 0 ), freqFactor( 1 ) +{ + if( pSample != NULL && pDimRegion != 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. + + pitchtrack = pDimRegion->PitchTrack; + + // Calculate note pitch and frequency factor only if we're actually + // going to be changing the pitch of the notes + if( pitchtrack == true ) + { + // Calculate what frequency the provided sample is + float semitones = 1.0 * pSample->FineTune / 0xFFFFFFFF; + sampleFreq = 440.0 * powf( 2, 1.0 / 12 * ( + 1.0 * pDimRegion->UnityNote - 69 ) + semitones ); + 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( pDimRegion, pSample->SamplesPerSecond / freqFactor ); + } +} + + + + +GigSample::~GigSample() +{ + if( srcState != NULL ) + { + src_delete( srcState ); + } +} + + + + +GigSample::GigSample( const GigSample& g ) + : sample( g.sample ), attenuation( g.attenuation ), adsr( g.adsr ), + pos( g.pos ), pitchtrack( g.pitchtrack ), 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; + attenuation = g.attenuation; + adsr = g.adsr; + pos = g.pos; + pitchtrack = g.pitchtrack; + 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, + int oldSize, int newSize, float freq_factor, int& 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 ) { } @@ -1130,6 +1182,12 @@ ADSR::ADSR( gig::DimensionRegion * region, int sampleRate ) attackLength = attack * sampleRate; decayLength = decay1 * sampleRate; // TODO: ignoring decay2 for now releaseLength = release * sampleRate; + + // If there is no attack, start at the full amplitude + if( attackLength == 0 ) + { + amplitude = 1.0; + } } } @@ -1155,9 +1213,9 @@ bool ADSR::done() // Return the current amplitude and increment internal positions -double ADSR::value() +float ADSR::value() { - double currentAmplitude = amplitude; + float currentAmplitude = amplitude; // If we're done, don't output any signal if( isDone == true ) diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 61d07e0ba..9898501e7 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -91,16 +91,16 @@ struct Dimension class ADSR { // From the file - double preattack; // initial amplitude (0-1) - double attack; // 0-60s - double decay1; // 0-60s - double decay2; // 0-60s + float preattack; // initial amplitude (0-1) + float attack; // 0-60s + float decay1; // 0-60s + float decay2; // 0-60s bool infiniteSustain; // i.e., no decay2 - double sustain; // sustain amplitude (0-1) - double release; // 0-60s + float sustain; // sustain amplitude (0-1) + float release; // 0-60s // Used to calculate current amplitude - double amplitude; + float amplitude; bool isAttack; bool isRelease; bool isDone; @@ -111,10 +111,11 @@ class ADSR int releaseLength; public: + ADSR(); ADSR( gig::DimensionRegion * region, int sampleRate ); void keyup(); // We will begin releasing starting now bool done(); // Is this sample done playing? - double value(); // What's the current amplitude + float value(); // What's the current amplitude void inc( int num ); // Increment internal positions by num } ; @@ -126,12 +127,38 @@ public: class GigSample { public: - GigSample( gig::Sample * pSample, float attenuation, const ADSR & adsr ); + 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, + int oldSize, int newSize, float freq_factor, int& used ); gig::Sample * sample; float attenuation; ADSR adsr; - int pos; // Position in sample + + // The position in sample + int 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; } ; @@ -163,11 +190,12 @@ public: int velocity; bool release; // Whether to trigger a release sample on key up GigState state; + float frequency; QList samples; - GigNote( int midiNote, int velocity ) + GigNote( int midiNote, int velocity, float frequency ) : midiNote( midiNote ), velocity( velocity ), - release( false ), state( KeyDown ) + release( false ), state( KeyDown ), frequency( frequency ) { } } ; @@ -226,9 +254,6 @@ public slots: private: - // Used to convert sample rates - SRC_STATE * m_srcState; - // The GIG file and instrument we're using GigInstance * m_instance; gig::Instrument * m_instrument; @@ -243,7 +268,6 @@ private: // Locking for the data QMutex m_synthMutex; - QMutex m_srcMutex; QMutex m_notesMutex; // Used for resampling @@ -271,10 +295,6 @@ private: // samples void addSamples( GigNote & gignote, bool wantReleaseSample ); - // Convert sample rates - bool convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, - int oldSize, int newSize, int oldRate, int newRate, int& used ); - friend class GigInstrumentView; signals: From 702e2a1ee394a669996aeaba2e054474b9916f99 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 17 Nov 2014 23:18:51 -0800 Subject: [PATCH 29/33] Added loop support and fixed fine tunings Now it'll honor the loop regions specified in the file and it'll properly use the fine tuning for the samples if specified. Also, modified the exponential decay code again since it was glitching at the end of some notes for some reason. --- plugins/GigPlayer/GigPlayer.cpp | 299 ++++++++++++++++++++++---------- plugins/GigPlayer/GigPlayer.h | 22 ++- 2 files changed, 217 insertions(+), 104 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 243038eb9..b46296ea4 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -379,8 +379,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) for( QList::iterator sample = it->samples.begin(); sample != it->samples.end(); ++sample ) { - if( sample->sample == NULL || sample->adsr.done() || - sample->pos >= sample->sample->SamplesTotal - 1 ) + if( sample->sample == NULL || sample->adsr.done() ) { sample = it->samples.erase( sample ); @@ -416,21 +415,21 @@ void GigInstrument::play( sampleFrame * _working_buffer ) for( QList::iterator sample = it->samples.begin(); sample != it->samples.end(); ++sample ) { - if( sample->sample == NULL ) + if( sample->sample == NULL || sample->region == NULL ) { continue; } // Will change if resampling bool resample = false; - int samples = frames; // How many to grab - int used = frames; // How many we used + 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->pitchtrack == true || rate != sample->sample->SamplesPerSecond ) + if( sample->region->PitchTrack == true || rate != sample->sample->SamplesPerSecond ) { resample = true; @@ -438,7 +437,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) freq_factor = 1.0 * rate / sample->sample->SamplesPerSecond; // Factor for pitch shifting as well as resampling - if( sample->pitchtrack == true ) + if( sample->region->PitchTrack == true ) { freq_factor *= sample->freqFactor; } @@ -447,78 +446,15 @@ void GigInstrument::play( sampleFrame * _working_buffer ) samples = frames / freq_factor + MARGIN[m_interpolation]; } - // This note's data + // Load this note's data sampleFrame sampleData[samples]; - - // Set the position to where we currently are in the sample - sample->sample->SetPos( sample->pos ); // Note: not thread safe - - // Load the next portion of the sample - unsigned long allocationsize = samples * sample->sample->FrameSize; - int8_t buffer[allocationsize]; - unsigned long size = sample->sample->Read( &buffer, samples ) * sample->sample->FrameSize; - unsigned long nullExtensionSize = allocationsize - size; - std::memset( (int8_t*) &buffer + size, 0, nullExtensionSize ); - - // Convert from 16 or 24 bit into 32-bit float - if( sample->sample->BitDepth == 24 ) // 24 bit - { - uint8_t * pInt = reinterpret_cast( &buffer ); - - for( int 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( int 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; - } - } - } + 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( int i = 0; i < samples; ++i ) + for( f_cnt_t i = 0; i < samples; ++i ) { float amplitude = copy.value(); sampleData[i][0] *= amplitude; @@ -534,7 +470,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) if( sample->convertSampleRate( *sampleData, *convertBuf, samples, frames, freq_factor, used ) ) { - for( int i = 0; i < frames; ++i ) + for( f_cnt_t i = 0; i < frames; ++i ) { _working_buffer[i][0] += convertBuf[i][0]; _working_buffer[i][1] += convertBuf[i][1]; @@ -543,7 +479,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) } else { - for( int i = 0; i < frames; ++i ) + for( f_cnt_t i = 0; i < frames; ++i ) { _working_buffer[i][0] += sampleData[i][0]; _working_buffer[i][1] += sampleData[i][1]; @@ -560,7 +496,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) m_synthMutex.unlock(); // Set gain properly based on volume control - for( int i = 0; i < frames; ++i) + for( f_cnt_t i = 0; i < frames; ++i) { _working_buffer[i][0] *= m_gain.value(); _working_buffer[i][1] *= m_gain.value(); @@ -572,6 +508,174 @@ void GigInstrument::play( sampleFrame * _working_buffer ) +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 ) { @@ -649,7 +753,7 @@ void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample ) float attenuation = pDimRegion->GetVelocityAttenuation( gignote.velocity );; float length = (float) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); - // TODO: sample panning? looping? crossfade different layers? + // TODO: sample panning? crossfade different layers? if( wantReleaseSample == true ) { @@ -1000,34 +1104,32 @@ void GigInstrumentView::showPatchDialog() // 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 ), attenuation( attenuation ), pos( 0 ), - pitchtrack( false ), interpolation( interpolation ), - srcState( NULL ), sampleFreq( 0 ), freqFactor( 1 ) + : sample( pSample ), region( pDimRegion ), attenuation( attenuation ), + pos( 0 ), interpolation( interpolation ), srcState( NULL ), + sampleFreq( 0 ), freqFactor( 1 ) { - if( pSample != NULL && pDimRegion != NULL ) + 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. - pitchtrack = pDimRegion->PitchTrack; - // Calculate note pitch and frequency factor only if we're actually // going to be changing the pitch of the notes - if( pitchtrack == true ) + if( region->PitchTrack == true ) { // Calculate what frequency the provided sample is - float semitones = 1.0 * pSample->FineTune / 0xFFFFFFFF; sampleFreq = 440.0 * powf( 2, 1.0 / 12 * ( - 1.0 * pDimRegion->UnityNote - 69 ) + semitones ); + 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( pDimRegion, pSample->SamplesPerSecond / freqFactor ); + adsr = ADSR( region, sample->SamplesPerSecond / freqFactor ); } } @@ -1046,8 +1148,8 @@ GigSample::~GigSample() GigSample::GigSample( const GigSample& g ) - : sample( g.sample ), attenuation( g.attenuation ), adsr( g.adsr ), - pos( g.pos ), pitchtrack( g.pitchtrack ), interpolation( g.interpolation ), + : 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 @@ -1060,10 +1162,10 @@ GigSample::GigSample( const GigSample& g ) GigSample& GigSample::operator=( const GigSample& g ) { sample = g.sample; + region= g.region; attenuation = g.attenuation; adsr = g.adsr; pos = g.pos; - pitchtrack = g.pitchtrack; interpolation = g.interpolation; srcState = NULL; sampleFreq = g.sampleFreq; @@ -1100,7 +1202,7 @@ void GigSample::updateSampleRate() bool GigSample::convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, - int oldSize, int newSize, float freq_factor, int& used ) + f_cnt_t oldSize, f_cnt_t newSize, float freq_factor, f_cnt_t& used ) { if( srcState == NULL ) { @@ -1183,8 +1285,13 @@ ADSR::ADSR( gig::DimensionRegion * region, int 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 - if( attackLength == 0 ) + else if( attackLength == 0 ) { amplitude = 1.0; } @@ -1254,10 +1361,10 @@ float ADSR::value() { // 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 ) * exp( -5.0 / releaseLength * releasePosition ) - 1e-3; + amplitude = ( sustain + 1e-3 ) * expf( -5.0 / releaseLength * releasePosition ) - 1e-3; // Don't have an infinite exponential decay - if( amplitude < 0 ) + if( amplitude <= 0 || releasePosition >= releaseLength ) { amplitude = 0; isDone = true; @@ -1273,9 +1380,9 @@ float ADSR::value() // Increment internal positions a certain number of times -void ADSR::inc( int num ) +void ADSR::inc( f_cnt_t num ) { - for( int i = 0; i < num; ++i ) + for( f_cnt_t i = 0; i < num; ++i ) { value(); } diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 9898501e7..cf3f55853 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -104,11 +104,11 @@ class ADSR bool isAttack; bool isRelease; bool isDone; - int attackPosition; - int attackLength; - int decayLength; - int releasePosition; - int releaseLength; + f_cnt_t attackPosition; + f_cnt_t attackLength; + f_cnt_t decayLength; + f_cnt_t releasePosition; + f_cnt_t releaseLength; public: ADSR(); @@ -116,7 +116,7 @@ public: void keyup(); // We will begin releasing starting now bool done(); // Is this sample done playing? float value(); // What's the current amplitude - void inc( int num ); // Increment internal positions by num + void inc( f_cnt_t num ); // Increment internal positions by num } ; @@ -138,14 +138,15 @@ public: // Needed since libsamplerate stores data internally between calls void updateSampleRate(); bool convertSampleRate( sampleFrame & oldBuf, sampleFrame & newBuf, - int oldSize, int newSize, float freq_factor, int& used ); + 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 - int pos; + 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 @@ -291,6 +292,11 @@ private: // 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 ); From 3f641c2c555072efc6c9bcd550ff52a32062fd66 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 18 Nov 2014 08:30:31 -0800 Subject: [PATCH 30/33] Make it work with MemoryManager --- plugins/GigPlayer/GigPlayer.cpp | 2 +- plugins/GigPlayer/GigPlayer.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index b46296ea4..98c26e6d1 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -94,7 +94,7 @@ GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : m_RandomSeed( 0 ), m_currentKeyDimension( 0 ) { - InstrumentPlayHandle * iph = new InstrumentPlayHandle( this ); + InstrumentPlayHandle * iph = new InstrumentPlayHandle( this, _instrument_track ); engine::mixer()->addPlayHandle( iph ); updateSampleRate(); diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index cf3f55853..aea3118e3 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -38,6 +38,7 @@ #include "LcdSpinBox.h" #include "led_checkbox.h" #include "SampleBuffer.h" +#include "MemoryManager.h" #include "gig.h" class GigInstrumentView; @@ -207,6 +208,8 @@ public: class GigInstrument : public Instrument { Q_OBJECT + MM_OPERATORS + mapPropertyFromModel( int, getBank, setBank, m_bankNum ); mapPropertyFromModel( int, getPatch, setPatch, m_patchNum ); From 76e182e586e35746aef61427977f889bfe79cb46 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 18 Nov 2014 09:02:50 -0800 Subject: [PATCH 31/33] Release only one note on keyup Previously if you release a C4 then all C4 notes would be released. Now it stores the pointer to the plugin data which is unique for each key press and determines which to release based on the matching pointers. --- plugins/GigPlayer/GigPlayer.cpp | 13 +++---------- plugins/GigPlayer/GigPlayer.h | 20 ++++++++++++++++++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 98c26e6d1..6cbf9da1a 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -74,14 +74,6 @@ Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor = -struct GIGPluginData -{ - int midiNote; -} ; - - - - GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &gigplayer_plugin_descriptor ), m_instance( NULL ), @@ -313,7 +305,7 @@ void GigInstrument::playNote( NotePlayHandle * _n, sampleFrame * ) const uint velocity = _n->midiVelocity( baseVelocity ); QMutexLocker locker( &m_notesMutex ); - m_notes.push_back( GigNote( midiNote, velocity, _n->unpitchedFrequency() ) ); + m_notes.push_back( GigNote( midiNote, velocity, _n->unpitchedFrequency(), pluginData ) ); } } @@ -686,7 +678,8 @@ void GigInstrument::deleteNotePluginData( NotePlayHandle * _n ) // pressed (i.e., not if the key was already released) for( QList::iterator i = m_notes.begin(); i != m_notes.end(); ++i ) { - if( i->midiNote == pluginData->midiNote && + // Find the note by matching pointers to the plugin data + if( i->handle == pluginData && ( i->state == KeyDown || i->state == PlayingKeyDown ) ) { i->state = KeyUp; diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index aea3118e3..25600e166 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -48,6 +48,16 @@ class PatchesDialog; class QLabel; + + +struct GIGPluginData +{ + int midiNote; +} ; + + + + // Load a GIG file using libgig class GigInstance { @@ -195,9 +205,15 @@ public: float frequency; QList samples; - GigNote( int midiNote, int velocity, float frequency ) + // 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 ), state( KeyDown ), frequency( frequency ) + release( false ), state( KeyDown ), frequency( frequency ), handle( handle ) { } } ; From 366e79979162bb2f24c1703691a89fdb8bd97bab Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 23 Nov 2014 10:31:18 -0800 Subject: [PATCH 32/33] More What's This messages --- plugins/GigPlayer/GigPlayer.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 6cbf9da1a..c4f98ca2c 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -488,7 +488,7 @@ void GigInstrument::play( sampleFrame * _working_buffer ) m_synthMutex.unlock(); // Set gain properly based on volume control - for( f_cnt_t i = 0; i < frames; ++i) + for( f_cnt_t i = 0; i < frames; ++i ) { _working_buffer[i][0] *= m_gain.value(); _working_buffer[i][1] *= m_gain.value(); @@ -518,7 +518,7 @@ void GigInstrument::loadSample( GigSample& sample, sampleFrame* sampleData, f_cn for( uint32_t i = 0; i < sample.region->SampleLoops; ++i ) { loop = true; - loopType = static_cast(sample.region->pSampleLoops[i].LoopType); + loopType = static_cast( sample.region->pSampleLoops[i].LoopType ); loopStart = sample.region->pSampleLoops[i].LoopStart; loopLength = sample.region->pSampleLoops[i].LoopLength; @@ -743,7 +743,7 @@ void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample ) if( gignote.midiNote >= keyLow && gignote.midiNote <= keyHigh ) { - float attenuation = pDimRegion->GetVelocityAttenuation( gignote.velocity );; + float attenuation = pDimRegion->GetVelocityAttenuation( gignote.velocity ); float length = (float) pSample->SamplesTotal / engine::mixer()->processingSampleRate(); // TODO: sample panning? crossfade different layers? @@ -941,6 +941,8 @@ GigInstrumentView::GigInstrumentView( Instrument * _instrument, QWidget * _paren 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 ); @@ -948,16 +950,23 @@ GigInstrumentView::GigInstrumentView( Instrument * _instrument, QWidget * _paren 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; @@ -965,7 +974,6 @@ GigInstrumentView::GigInstrumentView( Instrument * _instrument, QWidget * _paren setPalette( pal ); updateFilename(); - } @@ -1049,14 +1057,17 @@ void GigInstrumentView::showFileDialog() 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() ); } @@ -1070,6 +1081,7 @@ void GigInstrumentView::showFileDialog() if( ofd.exec() == QDialog::Accepted && !ofd.selectedFiles().isEmpty() ) { QString f = ofd.selectedFiles()[0]; + if( f != "" ) { k->openFile( f ); From 205056621c353fe50316f963b479b6f824ca6246 Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 23 Nov 2014 14:24:51 -0800 Subject: [PATCH 33/33] Fixed release samples never being deleted I removed code in a previous commit that deleted ended samples since that sometimes caused issues when the samples had loop points. However, removing the code caused issues with the release samples. Thus, now it removes ended samples only if they are release samples. Otherwise, the keyup event and ADSR handle ending the note. --- plugins/GigPlayer/GigPlayer.cpp | 11 ++++++++++- plugins/GigPlayer/GigPlayer.h | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index c4f98ca2c..34e404420 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -371,7 +371,12 @@ void GigInstrument::play( sampleFrame * _working_buffer ) for( QList::iterator sample = it->samples.begin(); sample != it->samples.end(); ++sample ) { - if( sample->sample == NULL || sample->adsr.done() ) + // 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 ); @@ -729,6 +734,10 @@ void GigInstrument::addSamples( GigNote & gignote, bool 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 ) diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 25600e166..f302df622 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -201,6 +201,7 @@ 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; @@ -213,7 +214,8 @@ public: GigNote( int midiNote, int velocity, float frequency, GIGPluginData * handle ) : midiNote( midiNote ), velocity( velocity ), - release( false ), state( KeyDown ), frequency( frequency ), handle( handle ) + release( false ), isRelease( false ), state( KeyDown ), + frequency( frequency ), handle( handle ) { } } ;