mirror of
https://github.com/LMMS/lmms.git
synced 2026-02-23 09:48:06 -05:00
Removes `SampleLoader`. File dialog functions were moved into `FileDialog`. Creation functions were moved into `SampleBuffer`. --------- Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
404 lines
12 KiB
C++
404 lines
12 KiB
C++
/*
|
|
* SlicerT.cpp - simple slicer plugin
|
|
*
|
|
* Copyright (c) 2023 Daniel Kauss Serna <daniel.kauss.serna@gmail.com>
|
|
*
|
|
* 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 "SlicerT.h"
|
|
|
|
#include <QDomElement>
|
|
#include <cmath>
|
|
#include <fftw3.h>
|
|
|
|
#include "Engine.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "PathUtil.h"
|
|
#include "SlicerTView.h"
|
|
#include "Song.h"
|
|
#include "embed.h"
|
|
#include "interpolation.h"
|
|
#include "plugin_export.h"
|
|
|
|
namespace lmms {
|
|
|
|
extern "C" {
|
|
Plugin::Descriptor PLUGIN_EXPORT slicert_plugin_descriptor = {
|
|
LMMS_STRINGIFY(PLUGIN_NAME),
|
|
"SlicerT",
|
|
QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"),
|
|
"Daniel Kauss Serna <daniel.kauss.serna@gmail.com>",
|
|
0x0100,
|
|
Plugin::Type::Instrument,
|
|
new PluginPixmapLoader("logo"),
|
|
nullptr,
|
|
nullptr,
|
|
};
|
|
} // end extern
|
|
|
|
// ################################# SlicerT ####################################
|
|
|
|
SlicerT::SlicerT(InstrumentTrack* instrumentTrack)
|
|
: Instrument(instrumentTrack, &slicert_plugin_descriptor)
|
|
, m_noteThreshold(0.6f, 0.0f, 2.0f, 0.01f, this, tr("Note threshold"))
|
|
, m_fadeOutFrames(10.0f, 0.0f, 100.0f, 0.1f, this, tr("FadeOut"))
|
|
, m_originalBPM(1, 1, 999, this, tr("Original bpm"))
|
|
, m_sliceSnap(this, tr("Slice snap"))
|
|
, m_enableSync(false, this, tr("BPM sync"))
|
|
, m_originalSample()
|
|
, m_parentTrack(instrumentTrack)
|
|
{
|
|
m_sliceSnap.addItem("Off");
|
|
m_sliceSnap.addItem("1/1");
|
|
m_sliceSnap.addItem("1/2");
|
|
m_sliceSnap.addItem("1/4");
|
|
m_sliceSnap.addItem("1/8");
|
|
m_sliceSnap.addItem("1/16");
|
|
m_sliceSnap.addItem("1/32");
|
|
m_sliceSnap.setValue(0);
|
|
}
|
|
|
|
void SlicerT::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer)
|
|
{
|
|
if (m_originalSample.sampleSize() <= 1) { return; }
|
|
|
|
int noteIndex = handle->key() - m_parentTrack->baseNote();
|
|
const fpp_t frames = handle->framesLeftForCurrentPeriod();
|
|
const f_cnt_t offset = handle->noteOffset();
|
|
const int bpm = Engine::getSong()->getTempo();
|
|
const float pitchRatio = 1 / std::exp2(m_parentTrack->pitchModel()->value() / 1200);
|
|
|
|
float speedRatio = static_cast<float>(m_originalBPM.value()) / bpm;
|
|
if (!m_enableSync.value()) { speedRatio = 1; }
|
|
speedRatio *= pitchRatio;
|
|
|
|
float sliceStart, sliceEnd;
|
|
if (noteIndex == 0) // full sample at base note
|
|
{
|
|
sliceStart = 0;
|
|
sliceEnd = 1;
|
|
}
|
|
else if (noteIndex > 0 && static_cast<std::size_t>(noteIndex) < m_slicePoints.size())
|
|
{
|
|
noteIndex -= 1;
|
|
sliceStart = m_slicePoints[noteIndex];
|
|
sliceEnd = m_slicePoints[noteIndex + 1];
|
|
}
|
|
else
|
|
{
|
|
emit isPlaying(-1, 0, 0);
|
|
return;
|
|
}
|
|
|
|
const auto startFrame = static_cast<int>(sliceStart * m_originalSample.sampleSize());
|
|
if (!handle->m_pluginData) { handle->m_pluginData = new Sample::PlaybackState(AudioResampler::Mode::Linear, startFrame); }
|
|
|
|
auto playbackState = static_cast<Sample::PlaybackState*>(handle->m_pluginData);
|
|
const auto endFrame = sliceEnd * m_originalSample.sampleSize();
|
|
const auto framesLeft = endFrame - playbackState->frameIndex();
|
|
|
|
if (framesLeft > 0
|
|
&& m_originalSample.play(workingBuffer + offset, playbackState, frames, Sample::Loop::Off, speedRatio))
|
|
{
|
|
// exponential fade out, applyRelease() not used since it extends the note length
|
|
int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->outputSampleRate();
|
|
for (auto i = std::size_t{0}; i < frames; i++)
|
|
{
|
|
float fadeValue = static_cast<float>(framesLeft * speedRatio - static_cast<int>(i)) / fadeOutFrames;
|
|
fadeValue = std::clamp(fadeValue, 0.0f, 1.0f);
|
|
fadeValue = cosinusInterpolate(0, 1, fadeValue);
|
|
|
|
workingBuffer[i + offset][0] *= fadeValue;
|
|
workingBuffer[i + offset][1] *= fadeValue;
|
|
}
|
|
|
|
const auto currentNote = static_cast<float>(playbackState->frameIndex()) / m_originalSample.sampleSize();
|
|
emit isPlaying(currentNote, sliceStart, sliceEnd);
|
|
}
|
|
else { emit isPlaying(-1, 0, 0); }
|
|
}
|
|
|
|
void SlicerT::deleteNotePluginData(NotePlayHandle* handle)
|
|
{
|
|
delete static_cast<Sample::PlaybackState*>(handle->m_pluginData);
|
|
emit isPlaying(-1, 0, 0);
|
|
}
|
|
|
|
// uses the spectral flux to determine the change in magnitude
|
|
// resources:
|
|
// http://www.iro.umontreal.ca/~pift6080/H09/documents/papers/bello_onset_tutorial.pdf
|
|
void SlicerT::findSlices()
|
|
{
|
|
if (m_originalSample.sampleSize() <= 1) { return; }
|
|
m_slicePoints = {};
|
|
|
|
const int windowSize = 512;
|
|
const float minBeatLength = 0.05f; // in seconds, ~ 1/4 length at 220 bpm
|
|
|
|
int sampleRate = m_originalSample.sampleRate();
|
|
int minDist = sampleRate * minBeatLength;
|
|
|
|
float maxMag = -1;
|
|
std::vector<float> singleChannel(m_originalSample.sampleSize(), 0);
|
|
for (auto i = std::size_t{0}; i < m_originalSample.sampleSize(); i++)
|
|
{
|
|
singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2;
|
|
maxMag = std::max(maxMag, singleChannel[i]);
|
|
}
|
|
|
|
// normalize and find 0 crossings
|
|
std::vector<int> zeroCrossings;
|
|
float lastValue = 1;
|
|
for (auto i = std::size_t{0}; i < singleChannel.size(); i++)
|
|
{
|
|
singleChannel[i] /= maxMag;
|
|
if ((lastValue >= 0) != (singleChannel[i] >= 0))
|
|
{
|
|
zeroCrossings.push_back(i);
|
|
lastValue = singleChannel[i];
|
|
}
|
|
}
|
|
|
|
std::vector<float> prevMags(windowSize / 2, 0);
|
|
std::vector<float> fftIn(windowSize, 0);
|
|
std::array<fftwf_complex, windowSize> fftOut;
|
|
|
|
fftwf_plan fftPlan = fftwf_plan_dft_r2c_1d(windowSize, fftIn.data(), fftOut.data(), FFTW_MEASURE);
|
|
|
|
int lastPoint = -minDist - 1; // to always store 0 first
|
|
float spectralFlux = 0;
|
|
float prevFlux = 1E-10f; // small value, no divison by zero
|
|
|
|
for (int i = 0; i < static_cast<int>(singleChannel.size()) - windowSize; i += windowSize)
|
|
{
|
|
// fft
|
|
std::copy_n(singleChannel.data() + i, windowSize, fftIn.data());
|
|
fftwf_execute(fftPlan);
|
|
|
|
// calculate spectral flux in regard to last window
|
|
for (int j = 0; j < windowSize / 2; j++) // only use niquistic frequencies
|
|
{
|
|
float real = fftOut[j][0];
|
|
float imag = fftOut[j][1];
|
|
float magnitude = std::sqrt(real * real + imag * imag);
|
|
|
|
// using L2-norm (euclidean distance)
|
|
float diff = std::abs(magnitude - prevMags[j]);
|
|
spectralFlux += diff;
|
|
|
|
prevMags[j] = magnitude;
|
|
}
|
|
|
|
if (spectralFlux / prevFlux > 1.0f + m_noteThreshold.value() && i - lastPoint > minDist)
|
|
{
|
|
m_slicePoints.push_back(i);
|
|
lastPoint = i;
|
|
if (m_slicePoints.size() > 128) { break; } // no more keys on the keyboard
|
|
}
|
|
|
|
prevFlux = spectralFlux;
|
|
spectralFlux = 1E-10f; // again for no divison by zero
|
|
}
|
|
|
|
m_slicePoints.push_back(m_originalSample.sampleSize());
|
|
|
|
for (float& sliceValue : m_slicePoints)
|
|
{
|
|
auto closestZeroCrossing = std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue);
|
|
if (closestZeroCrossing == zeroCrossings.end()) { continue; }
|
|
if (std::abs(sliceValue - *closestZeroCrossing) < windowSize) { sliceValue = *closestZeroCrossing; }
|
|
}
|
|
|
|
float beatsPerMin = m_originalBPM.value() / 60.0f;
|
|
float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f;
|
|
int noteSnap = m_sliceSnap.value();
|
|
int sliceLock = samplesPerBeat / std::exp2(noteSnap + 1);
|
|
if (noteSnap == 0) { sliceLock = 1; }
|
|
for (float& sliceValue : m_slicePoints)
|
|
{
|
|
sliceValue += sliceLock / 2.f;
|
|
sliceValue -= static_cast<int>(sliceValue) % sliceLock;
|
|
}
|
|
|
|
m_slicePoints.erase(std::unique(m_slicePoints.begin(), m_slicePoints.end()), m_slicePoints.end());
|
|
|
|
for (float& sliceIndex : m_slicePoints)
|
|
{
|
|
sliceIndex /= m_originalSample.sampleSize();
|
|
}
|
|
|
|
m_slicePoints[0] = 0;
|
|
m_slicePoints[m_slicePoints.size() - 1] = 1;
|
|
|
|
emit dataChanged();
|
|
}
|
|
|
|
// find the bpm of the sample by assuming its in 4/4 time signature ,
|
|
// and lies in the 100 - 200 bpm range
|
|
void SlicerT::findBPM()
|
|
{
|
|
if (m_originalSample.sampleSize() <= 1) { return; }
|
|
|
|
float sampleRate = m_originalSample.sampleRate();
|
|
float totalFrames = m_originalSample.sampleSize();
|
|
float sampleLength = totalFrames / sampleRate;
|
|
|
|
float bpmEstimate = 240.0f / sampleLength;
|
|
|
|
while (bpmEstimate < 100)
|
|
{
|
|
bpmEstimate *= 2;
|
|
}
|
|
|
|
while (bpmEstimate > 200)
|
|
{
|
|
bpmEstimate /= 2;
|
|
}
|
|
|
|
m_originalBPM.setValue(bpmEstimate);
|
|
m_originalBPM.setInitValue(bpmEstimate);
|
|
}
|
|
|
|
std::vector<Note> SlicerT::getMidi()
|
|
{
|
|
std::vector<Note> outputNotes;
|
|
|
|
float speedRatio = static_cast<float>(m_originalBPM.value()) / Engine::getSong()->getTempo();
|
|
float outFrames = m_originalSample.sampleSize() * speedRatio;
|
|
|
|
float framesPerTick = Engine::framesPerTick();
|
|
float totalTicks = outFrames / framesPerTick;
|
|
float lastEnd = 0;
|
|
|
|
for (auto i = std::size_t{0}; i < m_slicePoints.size() - 1; i++)
|
|
{
|
|
float sliceStart = lastEnd;
|
|
float sliceEnd = totalTicks * m_slicePoints[i + 1];
|
|
|
|
Note sliceNote = Note();
|
|
sliceNote.setKey(i + m_parentTrack->baseNote() + 1);
|
|
sliceNote.setPos(sliceStart);
|
|
sliceNote.setLength(sliceEnd - sliceStart + 1); // + 1 so that the notes allign
|
|
outputNotes.push_back(sliceNote);
|
|
|
|
lastEnd = sliceEnd;
|
|
}
|
|
|
|
return outputNotes;
|
|
}
|
|
|
|
void SlicerT::updateFile(QString file)
|
|
{
|
|
if (auto buffer = SampleBuffer::fromFile(file)) { m_originalSample = Sample(std::move(buffer)); }
|
|
|
|
findBPM();
|
|
findSlices();
|
|
|
|
emit dataChanged();
|
|
}
|
|
|
|
void SlicerT::loadFile(const QString& file)
|
|
{
|
|
updateFile(file);
|
|
}
|
|
|
|
void SlicerT::updateSlices()
|
|
{
|
|
findSlices();
|
|
}
|
|
|
|
void SlicerT::saveSettings(QDomDocument& document, QDomElement& element)
|
|
{
|
|
element.setAttribute("version", "1");
|
|
element.setAttribute("src", m_originalSample.sampleFile());
|
|
if (m_originalSample.sampleFile().isEmpty())
|
|
{
|
|
element.setAttribute("sampledata", m_originalSample.toBase64());
|
|
}
|
|
|
|
element.setAttribute("totalSlices", static_cast<int>(m_slicePoints.size()));
|
|
for (auto i = std::size_t{0}; i < m_slicePoints.size(); i++)
|
|
{
|
|
element.setAttribute(tr("slice_%1").arg(i), m_slicePoints[i]);
|
|
}
|
|
|
|
m_fadeOutFrames.saveSettings(document, element, "fadeOut");
|
|
m_noteThreshold.saveSettings(document, element, "threshold");
|
|
m_originalBPM.saveSettings(document, element, "origBPM");
|
|
m_enableSync.saveSettings(document, element, "syncEnable");
|
|
}
|
|
|
|
void SlicerT::loadSettings(const QDomElement& element)
|
|
{
|
|
if (auto srcFile = element.attribute("src"); !srcFile.isEmpty())
|
|
{
|
|
if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists())
|
|
{
|
|
auto buffer = SampleBuffer::fromFile(srcFile);
|
|
m_originalSample = Sample(std::move(buffer));
|
|
}
|
|
else
|
|
{
|
|
QString message = tr("Sample not found: %1").arg(srcFile);
|
|
Engine::getSong()->collectError(message);
|
|
}
|
|
}
|
|
else if (auto sampleData = element.attribute("sampledata"); !sampleData.isEmpty())
|
|
{
|
|
auto buffer = SampleBuffer::fromBase64(sampleData);
|
|
m_originalSample = Sample(std::move(buffer));
|
|
}
|
|
|
|
if (!element.attribute("totalSlices").isEmpty())
|
|
{
|
|
int totalSlices = element.attribute("totalSlices").toInt();
|
|
m_slicePoints = {};
|
|
for (int i = 0; i < totalSlices; i++)
|
|
{
|
|
m_slicePoints.push_back(element.attribute(tr("slice_%1").arg(i)).toFloat());
|
|
}
|
|
}
|
|
|
|
m_fadeOutFrames.loadSettings(element, "fadeOut");
|
|
m_noteThreshold.loadSettings(element, "threshold");
|
|
m_originalBPM.loadSettings(element, "origBPM");
|
|
m_enableSync.loadSettings(element, "syncEnable");
|
|
|
|
emit dataChanged();
|
|
}
|
|
|
|
QString SlicerT::nodeName() const
|
|
{
|
|
return slicert_plugin_descriptor.name;
|
|
}
|
|
|
|
gui::PluginView* SlicerT::instantiateView(QWidget* parent)
|
|
{
|
|
return new gui::SlicerTView(this, parent);
|
|
}
|
|
|
|
extern "C" {
|
|
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*)
|
|
{
|
|
return new SlicerT(static_cast<InstrumentTrack*>(m));
|
|
}
|
|
} // extern
|
|
} // namespace lmms
|