mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-12 02:57:31 -04:00
* extremly basic slicer, note playback and gui works * very simple peak detection working * basic phase vocoder implementation, no effects yet * phase vocoder slight rewrite * pitch shifting works more or less * basic timeshift working * PV timeshift working (no pitch shift) * basic functions work (UI, editing, playback) * slice editor Ui working * fundamental functionality done * Everything basic works fully * cleanup and code guidelines * more file cleanup * Tried fixing multi slice playback (still broken) * remove includes, add license * code factoring issues * more code factoring * fixed multinote playback and bpm check * UI performance improvments + code style * initial UI changes + more code style * threadsafe(maybe) + UI finished * preparing for dinamic timeshifting * dynamic timeshifting start * realtime time scaling (no stereo) * stereo added, very slow * playback performance improvments * Roxas new UI start * fixed cmake * Waveform UI finished * Roxas UI knobs + layout * Spectral flux onset detection * build + PV fixes * clang-format formatting * slice snap + better defaults * windows build fixes * windows build fixes part 2 * Fixed slice bug + Waveform code cleanup * UI button text + reorder + file cleanup * Added knob colors * comments + code cleanup * var names fit convention * PV better windowing * waveform zoom * Minor style fixes. * Initial artistic rebalancing of the plugin artwork. * PV phase ghosting fix * Use base note as keyboard slice start * Good draft of Artwork, renamed bg to artwork * Removed soft glow. * Fixed load crashes + findSlices cleanup * Added sync button * added pitch shifting, sometimes crashes * pitch fixes * MacOs build fixes * use linear interpolation * copyright + div by 0 fixes * Fixed rare crash + name changes + license * review: memcpy, no array, LMMS header, name change * static constexpr added * static vars to public + LMMS guards * remove references in classes * remove constexpr and parent pointer in waveform * std::array for fft * fixed wrong name in style * remove c style casts * use src_process * use note vector for return * Moved PhaseVocoder into core * removed PV from plugin * remove pointers in waveform * clang-format again * Use std:: + review suggestions Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: saker <sakertooth@gmail.com> * More review changes * new signal slot + more review * Fixed pitch shifting * Fixed buffer overflow in PV * Fixed mouse bug + better empty screen * Small editor refactor + improvments * Editor playback visual + small fixes * Roxas UI improvments * initial timeshift removing * Remove timeshift + slice refactor * Removed unused files * Fix export bug * Fix zoom bug * Review changes SakerTooth#2 * Remove most comments * Performance + click to load * update PlaybackState + zerocross snapping * Fix windows build issue * Review + version * Fixed fade out bug * Use cosine interpolation * Apply suggestions from code review Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * More review changes * Renamed files * Full sample only at base note * Fix memory leak Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> * Style fixes --------- Co-authored-by: Katherine Pratt <consolegrl@gmail.com> Co-authored-by: Dalton Messmer <33463986+messmerd@users.noreply.github.com> Co-authored-by: saker <sakertooth@gmail.com>
411 lines
12 KiB
C++
411 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 "Song.h"
|
|
#include "embed.h"
|
|
#include "lmms_constants.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.frames() <= 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;
|
|
speedRatio *= Engine::audioEngine()->processingSampleRate() / static_cast<float>(m_originalSample.sampleRate());
|
|
|
|
float sliceStart, sliceEnd;
|
|
if (noteIndex == 0) // full sample at base note
|
|
{
|
|
sliceStart = 0;
|
|
sliceEnd = 1;
|
|
}
|
|
else if (noteIndex > 0 && noteIndex < m_slicePoints.size())
|
|
{
|
|
noteIndex -= 1;
|
|
sliceStart = m_slicePoints[noteIndex];
|
|
sliceEnd = m_slicePoints[noteIndex + 1];
|
|
}
|
|
else
|
|
{
|
|
emit isPlaying(-1, 0, 0);
|
|
return;
|
|
}
|
|
|
|
if (!handle->m_pluginData) { handle->m_pluginData = new PlaybackState(sliceStart); }
|
|
auto playbackState = static_cast<PlaybackState*>(handle->m_pluginData);
|
|
|
|
float noteDone = playbackState->noteDone();
|
|
float noteLeft = sliceEnd - noteDone;
|
|
|
|
if (noteLeft > 0)
|
|
{
|
|
int noteFrame = noteDone * m_originalSample.frames();
|
|
|
|
SRC_STATE* resampleState = playbackState->resamplingState();
|
|
SRC_DATA resampleData;
|
|
resampleData.data_in = (m_originalSample.data() + noteFrame)->data();
|
|
resampleData.data_out = (workingBuffer + offset)->data();
|
|
resampleData.input_frames = noteLeft * m_originalSample.frames();
|
|
resampleData.output_frames = frames;
|
|
resampleData.src_ratio = speedRatio;
|
|
|
|
src_process(resampleState, &resampleData);
|
|
|
|
float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.frames();
|
|
playbackState->setNoteDone(nextNoteDone);
|
|
|
|
// exponential fade out, applyRelease() not used since it extends the note length
|
|
int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->processingSampleRate();
|
|
int noteFramesLeft = noteLeft * m_originalSample.frames() * speedRatio;
|
|
for (int i = 0; i < frames; i++)
|
|
{
|
|
float fadeValue = static_cast<float>(noteFramesLeft - 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;
|
|
}
|
|
|
|
instrumentTrack()->processAudioBuffer(workingBuffer, frames + offset, handle);
|
|
|
|
emit isPlaying(noteDone, sliceStart, sliceEnd);
|
|
}
|
|
else { emit isPlaying(-1, 0, 0); }
|
|
}
|
|
|
|
void SlicerT::deleteNotePluginData(NotePlayHandle* handle)
|
|
{
|
|
delete static_cast<PlaybackState*>(handle->m_pluginData);
|
|
}
|
|
|
|
// 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.frames() <= 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.frames(), 0);
|
|
for (int i = 0; i < m_originalSample.frames(); 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 (int i = 0; i < singleChannel.size(); i++)
|
|
{
|
|
singleChannel[i] /= maxMag;
|
|
if (sign(lastValue) != sign(singleChannel[i]))
|
|
{
|
|
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-10; // small value, no divison by zero
|
|
float real, imag, magnitude, diff;
|
|
|
|
for (int i = 0; i < 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
|
|
{
|
|
real = fftOut[j][0];
|
|
imag = fftOut[j][1];
|
|
magnitude = std::sqrt(real * real + imag * imag);
|
|
|
|
// using L2-norm (euclidean distance)
|
|
diff = std::sqrt(std::pow(magnitude - prevMags[j], 2));
|
|
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-10; // again for no divison by zero
|
|
}
|
|
|
|
m_slicePoints.push_back(m_originalSample.frames());
|
|
|
|
for (float& sliceValue : m_slicePoints)
|
|
{
|
|
int closestZeroCrossing = *std::lower_bound(zeroCrossings.begin(), zeroCrossings.end(), sliceValue);
|
|
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;
|
|
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.frames();
|
|
}
|
|
|
|
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.frames() <= 1) { return; }
|
|
|
|
float sampleRate = m_originalSample.sampleRate();
|
|
float totalFrames = m_originalSample.frames();
|
|
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.frames() * speedRatio;
|
|
|
|
float framesPerTick = Engine::framesPerTick();
|
|
float totalTicks = outFrames / framesPerTick;
|
|
float lastEnd = 0;
|
|
|
|
for (int i = 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)
|
|
{
|
|
m_originalSample.setAudioFile(file);
|
|
|
|
findBPM();
|
|
findSlices();
|
|
|
|
emit dataChanged();
|
|
}
|
|
|
|
void SlicerT::updateSlices()
|
|
{
|
|
findSlices();
|
|
}
|
|
|
|
void SlicerT::saveSettings(QDomDocument& document, QDomElement& element)
|
|
{
|
|
element.setAttribute("version", "1");
|
|
element.setAttribute("src", m_originalSample.audioFile());
|
|
if (m_originalSample.audioFile().isEmpty())
|
|
{
|
|
QString s;
|
|
element.setAttribute("sampledata", m_originalSample.toBase64(s));
|
|
}
|
|
|
|
element.setAttribute("totalSlices", static_cast<int>(m_slicePoints.size()));
|
|
for (int i = 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 (!element.attribute("src").isEmpty())
|
|
{
|
|
m_originalSample.setAudioFile(element.attribute("src"));
|
|
|
|
QString absolutePath = PathUtil::toAbsolute(m_originalSample.audioFile());
|
|
if (!QFileInfo(absolutePath).exists())
|
|
{
|
|
QString message = tr("Sample not found: %1").arg(m_originalSample.audioFile());
|
|
Engine::getSong()->collectError(message);
|
|
}
|
|
}
|
|
else if (!element.attribute("sampledata").isEmpty())
|
|
{
|
|
m_originalSample.loadFromBase64(element.attribute("srcdata"));
|
|
}
|
|
|
|
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
|