/* * AudioFileProcessor.cpp - instrument for using audio files * * Copyright (c) 2004-2014 Tobias Doerffel * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program (see COPYING); if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA. * */ #include "AudioFileProcessor.h" #include "AudioFileProcessorView.h" #include "InstrumentTrack.h" #include "PathUtil.h" #include "SampleLoader.h" #include "Song.h" #include "lmms_basics.h" #include "plugin_export.h" #include namespace lmms { extern "C" { Plugin::Descriptor PLUGIN_EXPORT audiofileprocessor_plugin_descriptor = { LMMS_STRINGIFY( PLUGIN_NAME ), "AudioFileProcessor", QT_TRANSLATE_NOOP( "PluginBrowser", "Simple sampler with various settings for " "using samples (e.g. drums) in an " "instrument-track" ), "Tobias Doerffel ", 0x0100, Plugin::Type::Instrument, new PluginPixmapLoader( "logo" ), "wav,ogg,ds,spx,au,voc,aif,aiff,flac,raw" #ifdef LMMS_HAVE_SNDFILE_MP3 ",mp3" #endif , nullptr, } ; } AudioFileProcessor::AudioFileProcessor( InstrumentTrack * _instrument_track ) : Instrument( _instrument_track, &audiofileprocessor_plugin_descriptor ), m_ampModel( 100, 0, 500, 1, this, tr( "Amplify" ) ), m_startPointModel( 0, 0, 1, 0.0000001f, this, tr( "Start of sample" ) ), m_endPointModel( 1, 0, 1, 0.0000001f, this, tr( "End of sample" ) ), m_loopPointModel( 0, 0, 1, 0.0000001f, this, tr( "Loopback point" ) ), m_reverseModel( false, this, tr( "Reverse sample" ) ), m_loopModel( 0, 0, 2, this, tr( "Loop mode" ) ), m_stutterModel( false, this, tr( "Stutter" ) ), m_interpolationModel( this, tr( "Interpolation mode" ) ), m_nextPlayStartPoint( 0 ), m_nextPlayBackwards( false ) { connect( &m_reverseModel, SIGNAL( dataChanged() ), this, SLOT( reverseModelChanged() ), Qt::DirectConnection ); connect( &m_ampModel, SIGNAL( dataChanged() ), this, SLOT( ampModelChanged() ), Qt::DirectConnection ); connect( &m_startPointModel, SIGNAL( dataChanged() ), this, SLOT( startPointChanged() ), Qt::DirectConnection ); connect( &m_endPointModel, SIGNAL( dataChanged() ), this, SLOT( endPointChanged() ), Qt::DirectConnection ); connect( &m_loopPointModel, SIGNAL( dataChanged() ), this, SLOT( loopPointChanged() ), Qt::DirectConnection ); connect( &m_stutterModel, SIGNAL( dataChanged() ), this, SLOT( stutterModelChanged() ), Qt::DirectConnection ); //interpolation modes m_interpolationModel.addItem( tr( "None" ) ); m_interpolationModel.addItem( tr( "Linear" ) ); m_interpolationModel.addItem( tr( "Sinc" ) ); m_interpolationModel.setValue( 1 ); pointChanged(); } void AudioFileProcessor::playNote( NotePlayHandle * _n, SampleFrame* _working_buffer ) { const fpp_t frames = _n->framesLeftForCurrentPeriod(); const f_cnt_t offset = _n->noteOffset(); // Magic key - a frequency < 20 (say, the bottom piano note if using // a A4 base tuning) restarts the start point. The note is not actually // played. if( m_stutterModel.value() == true && _n->frequency() < 20.0 ) { m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; return; } if( !_n->m_pluginData ) { if (m_stutterModel.value() == true && m_nextPlayStartPoint >= static_cast(m_sample.endFrame())) { // Restart playing the note if in stutter mode, not in loop mode, // and we're at the end of the sample. m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; } // set interpolation mode for libsamplerate int srcmode = SRC_LINEAR; switch( m_interpolationModel.value() ) { case 0: srcmode = SRC_ZERO_ORDER_HOLD; break; case 1: srcmode = SRC_LINEAR; break; case 2: srcmode = SRC_SINC_MEDIUM_QUALITY; break; } _n->m_pluginData = new Sample::PlaybackState(_n->hasDetuningInfo(), srcmode); static_cast(_n->m_pluginData)->setFrameIndex(m_nextPlayStartPoint); static_cast(_n->m_pluginData)->setBackwards(m_nextPlayBackwards); // debug code /* qDebug( "frames %d", m_sample->frames() ); qDebug( "startframe %d", m_sample->startFrame() ); qDebug( "nextPlayStartPoint %d", m_nextPlayStartPoint );*/ } if( ! _n->isFinished() ) { if (m_sample.play(_working_buffer + offset, static_cast(_n->m_pluginData), frames, _n->frequency(), static_cast(m_loopModel.value()))) { applyRelease( _working_buffer, _n ); emit isPlaying(static_cast(_n->m_pluginData)->frameIndex()); } else { zeroSampleFrames(_working_buffer, frames + offset); emit isPlaying( 0 ); } } else { emit isPlaying( 0 ); } if( m_stutterModel.value() == true ) { m_nextPlayStartPoint = static_cast(_n->m_pluginData)->frameIndex(); m_nextPlayBackwards = static_cast(_n->m_pluginData)->backwards(); } } void AudioFileProcessor::deleteNotePluginData( NotePlayHandle * _n ) { delete static_cast(_n->m_pluginData); } void AudioFileProcessor::saveSettings(QDomDocument& doc, QDomElement& elem) { elem.setAttribute("src", m_sample.sampleFile()); if (m_sample.sampleFile().isEmpty()) { elem.setAttribute("sampledata", m_sample.toBase64()); } m_reverseModel.saveSettings(doc, elem, "reversed"); m_loopModel.saveSettings(doc, elem, "looped"); m_ampModel.saveSettings(doc, elem, "amp"); m_startPointModel.saveSettings(doc, elem, "sframe"); m_endPointModel.saveSettings(doc, elem, "eframe"); m_loopPointModel.saveSettings(doc, elem, "lframe"); m_stutterModel.saveSettings(doc, elem, "stutter"); m_interpolationModel.saveSettings(doc, elem, "interp"); } void AudioFileProcessor::loadSettings(const QDomElement& elem) { if (auto srcFile = elem.attribute("src"); !srcFile.isEmpty()) { if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) { setAudioFile(srcFile, false); } else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), srcFile)); } } else if (auto sampleData = elem.attribute("sampledata"); !sampleData.isEmpty()) { m_sample = Sample(gui::SampleLoader::createBufferFromBase64(sampleData)); } m_loopModel.loadSettings(elem, "looped"); m_ampModel.loadSettings(elem, "amp"); m_endPointModel.loadSettings(elem, "eframe"); m_startPointModel.loadSettings(elem, "sframe"); // compat code for not having a separate loopback point if (elem.hasAttribute("lframe") || !elem.firstChildElement("lframe").isNull()) { m_loopPointModel.loadSettings(elem, "lframe"); } else { m_loopPointModel.loadSettings(elem, "sframe"); } m_reverseModel.loadSettings(elem, "reversed"); m_stutterModel.loadSettings(elem, "stutter"); if (elem.hasAttribute("interp") || !elem.firstChildElement("interp").isNull()) { m_interpolationModel.loadSettings(elem, "interp"); } else { m_interpolationModel.setValue(1.0f); // linear by default } pointChanged(); emit sampleUpdated(); } void AudioFileProcessor::loadFile( const QString & _file ) { setAudioFile( _file ); } QString AudioFileProcessor::nodeName() const { return audiofileprocessor_plugin_descriptor.name; } auto AudioFileProcessor::beatLen(NotePlayHandle* note) const -> f_cnt_t { // If we can play indefinitely, use the default beat note duration if (static_cast(m_loopModel.value()) != Sample::Loop::Off) { return 0; } // Otherwise, use the remaining sample duration const auto baseFreq = instrumentTrack()->baseFreq(); const auto freqFactor = baseFreq / note->frequency() * Engine::audioEngine()->outputSampleRate() / Engine::audioEngine()->baseSampleRate(); const auto startFrame = m_nextPlayStartPoint >= static_cast(m_sample.endFrame()) ? m_sample.startFrame() : m_nextPlayStartPoint; const auto duration = m_sample.endFrame() - startFrame; return static_cast(std::floor(duration * freqFactor)); } gui::PluginView* AudioFileProcessor::instantiateView( QWidget * _parent ) { return new gui::AudioFileProcessorView( this, _parent ); } void AudioFileProcessor::setAudioFile(const QString& _audio_file, bool _rename) { // is current channel-name equal to previous-filename?? if( _rename && ( instrumentTrack()->name() == QFileInfo(m_sample.sampleFile()).fileName() || m_sample.sampleFile().isEmpty())) { // then set it to new one instrumentTrack()->setName( PathUtil::cleanName( _audio_file ) ); } // else we don't touch the track-name, because the user named it self m_sample = Sample(gui::SampleLoader::createBufferFromFile(_audio_file)); loopPointChanged(); emit sampleUpdated(); } void AudioFileProcessor::reverseModelChanged() { m_sample.setReversed(m_reverseModel.value()); m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; emit sampleUpdated(); } void AudioFileProcessor::ampModelChanged() { m_sample.setAmplification(m_ampModel.value() / 100.0f); emit sampleUpdated(); } void AudioFileProcessor::stutterModelChanged() { m_nextPlayStartPoint = m_sample.startFrame(); m_nextPlayBackwards = false; } void AudioFileProcessor::startPointChanged() { // check if start is over end and swap values if so if( m_startPointModel.value() > m_endPointModel.value() ) { float tmp = m_endPointModel.value(); m_endPointModel.setValue( m_startPointModel.value() ); m_startPointModel.setValue( tmp ); } // nudge loop point with end if( m_loopPointModel.value() >= m_endPointModel.value() ) { m_loopPointModel.setValue( qMax( m_endPointModel.value() - 0.001f, 0.0f ) ); } // nudge loop point with start if( m_loopPointModel.value() < m_startPointModel.value() ) { m_loopPointModel.setValue( m_startPointModel.value() ); } // check if start & end overlap and nudge end up if so if( m_startPointModel.value() == m_endPointModel.value() ) { m_endPointModel.setValue( qMin( m_endPointModel.value() + 0.001f, 1.0f ) ); } pointChanged(); } void AudioFileProcessor::endPointChanged() { // same as start, for now startPointChanged(); } void AudioFileProcessor::loopPointChanged() { // check that loop point is between start-end points and not overlapping with endpoint // ...and move start/end points ahead if loop point is moved over them if( m_loopPointModel.value() >= m_endPointModel.value() ) { m_endPointModel.setValue( m_loopPointModel.value() + 0.001f ); if( m_endPointModel.value() == 1.0f ) { m_loopPointModel.setValue( 1.0f - 0.001f ); } } // nudge start point with loop if( m_loopPointModel.value() < m_startPointModel.value() ) { m_startPointModel.setValue( m_loopPointModel.value() ); } pointChanged(); } void AudioFileProcessor::pointChanged() { const auto f_start = static_cast(m_startPointModel.value() * m_sample.sampleSize()); const auto f_end = static_cast(m_endPointModel.value() * m_sample.sampleSize()); const auto f_loop = static_cast(m_loopPointModel.value() * m_sample.sampleSize()); m_nextPlayStartPoint = f_start; m_nextPlayBackwards = false; m_sample.setAllPointFrames(f_start, f_end, f_loop, f_end); emit dataChanged(); } extern "C" { // necessary for getting instance out of shared lib PLUGIN_EXPORT Plugin * lmms_plugin_main(Model * model, void *) { return new AudioFileProcessor(static_cast(model)); } } } // namespace lmms