mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-27 10:23:18 -04:00
* clang-tidy: Apply cppcoreguidelines-init-variables everywhere (treating NaNs as zeros) * Initialize msec and tick outside switch * Update plugins/Vestige/Vestige.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update plugins/Vestige/Vestige.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update plugins/Vestige/Vestige.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update plugins/VstEffect/VstEffectControls.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update plugins/VstEffect/VstEffectControls.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update plugins/VstEffect/VstEffectControls.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Use initialization with = * Use tabs * Use static_cast * Update DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Do not use tabs for alignment in src/core/DrumSynth.cpp Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com> * Move x variable inside loop * Use ternary operator for b variable * Revert "Use tabs" This reverts commit 07afd8a83f58b539c3673310b2aad4b63c9198a0. * Remove unnecessary variables in XpressiveView * Simplify initialization in Plugin * Combine declaration and initialization in EqCurve * Combine declaration and initialization in Song * Combine declaration and initialization in AudioAlsa * Combine declaration and initialization in EqCurve (again) * Missed some * Undo changes made to non-LMMS files * Undo indentation changes in SidInstrument.cpp * Combine declaration with assignment in IoHelper * Combine declaration with assignment using auto in Carla * Combine declaration with assignment * Combine declaration with assignment in BasicFilters * Simplify assignments in AudioFileProcessorWaveView::zoom * Simplify out sample variable in BitInvader * Remove sampleLength variable in DelayEffect * Move gain variable in DynamicsProcessor * Combine peak variable declaration with assignment in EqSpectrumView * Move left/right lfo variables in for loop in FlangerEffect * Use ternary operator for group variable in LadspaControlDialog * Combine declaration with assignment in Lb302 * Combine declaration with assignment in MidiExport * Combine declaration with assignment in MidiFile * Combine declaration with assignment in MidiImport * Use ternary operator for vel_adjusted variable in OpulenZ * Move tmpL and dcblkL variables in for loop in ReverbSC * Combine declaration with initialization in SlicerT * Combine declaration with assignment in SaSpectrumView * Combine declaration with assignment in SaWaterfallView * Combine declaration with assignment in StereoEnhancerEffect * Combine declaration with assignment in VibratingString * Combine declaration with assignment in VstEffectControls * Combine declaration with assignment in Xpressive * Combine declaration with assignment in AutomatableModel * Combine declaration with assignment in AutomationClip * Move sample variable in for loop in BandLimitedWave * Combine declaration with assignment in DataFile * Combine declaration with assignment in DrumSynth * Combine declaration with assignment in Effect * Remove redundant assignment to nphsLeft in InstrumentPlayHandle * Combine declaration with assignment in LadspaManager * Combine declaration with assignment in LinkedModelGroups * Combine declaration with assignment in MemoryHelper * Combine declaration with assignment in AudioAlsa * Combine declaration with assignment in AudioFileOgg * Combine declaration with assignment in AudioPortAudio * Combine declaration with assignment in AudioSoundIo * Combine declaration with assignment in Lv2Evbuf * Combine declaration with assignment in Lv2Proc * Combine declaration with assignment in main * Combine declaration with assignment in MidiAlsaRaw * Combine declaration with assignment in MidiAlsaSeq * Combine declaration with assignment in MidiController * Combine declaration with assignment in MidiJack * Combine declaration with assignment in MidiSndio * Combine declaration with assignment in ControlLayout * Combine declaration with assignment in MainWindow * Combine declaration with assignment in ProjectNotes * Use ternary operator for nextValue variable in AutomationClipView * Combine declaration with assignment in AutomationEditor * Move length variable in for-loop in PianoRoll * Combine declaration with assignment in ControllerConnectionDialog * Combine declaration with assignment in Graph * Combine declaration with assignment in LcdFloatSpinBox * Combine declaration with assignment in TimeDisplayWidget * Remove currentNote variable in InstrumentTrack * Combine declaration with assignment in DrumSynth (again) * Use ternary operator for factor variable in BitInvader * Use ternary operator for highestBandwich variable in EqCurve Bandwich? * Move sum variable into for loop in Graph * Fix format in MidiSndio * Fixup a few more * Cleanup error variables * Use ternary operators and combine declaration with initialization * Combine declaration with initialization * Update plugins/LadspaEffect/LadspaControlDialog.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update plugins/OpulenZ/OpulenZ.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update plugins/SpectrumAnalyzer/SaProcessor.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/midi/MidiAlsaRaw.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/gui/MainWindow.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/gui/clips/AutomationClipView.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/gui/editors/AutomationEditor.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/gui/widgets/Fader.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Move static_cast conversion into separate variable * Use real index when interpolating * Remove empty line * Make helpBtn a private member * Move controller type into separate variable * Fix format of DrumSynth::waveform function * Use tabs and static_cast * Remove redundant if branch * Refactor using static_cast/reinterpret_cast * Add std namespace prefix * Store repeated conditional into boolean variable * Cast to int before assigning to m_currentLength * Rename note_frames to noteFrames * Update src/core/Controller.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/core/DrumSynth.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Update src/gui/widgets/Graph.cpp Co-authored-by: Kevin Zander <veratil@gmail.com> * Revert changes that initialized variables redudantly For situations where the initialization is more complex or passed into a function by a pointer, we dont need to do initialization ourselves since it is already done for us, just in a different way. * Remove redundant err variable * Remove explicit check of err variable * Clean up changes and address review * Do not initialize to 0/nullptr when not needed * Wrap condition in parentheses for readability --------- Co-authored-by: Kevin Zander <veratil@gmail.com> Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
337 lines
13 KiB
C++
337 lines
13 KiB
C++
/* VectorView.cpp - implementation of VectorView class.
|
|
*
|
|
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/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 "VectorView.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <QImage>
|
|
#include <QPainter>
|
|
|
|
#include "ColorChooser.h"
|
|
#include "GuiApplication.h"
|
|
#include "MainWindow.h"
|
|
#include "VecControls.h"
|
|
|
|
namespace lmms::gui
|
|
{
|
|
|
|
|
|
VectorView::VectorView(VecControls *controls, LocklessRingBuffer<sampleFrame> *inputBuffer, unsigned short displaySize, QWidget *parent) :
|
|
QWidget(parent),
|
|
m_controls(controls),
|
|
m_inputBuffer(inputBuffer),
|
|
m_bufferReader(*inputBuffer),
|
|
m_displaySize(displaySize),
|
|
m_zoom(1.f),
|
|
m_persistTimestamp(0),
|
|
m_zoomTimestamp(0),
|
|
m_oldHQ(m_controls->m_highQualityModel.value()),
|
|
m_oldX(m_displaySize / 2),
|
|
m_oldY(m_displaySize / 2)
|
|
{
|
|
setMinimumSize(200, 200);
|
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
|
|
connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
|
|
|
|
m_displayBuffer.resize(sizeof qRgb(0,0,0) * m_displaySize * m_displaySize, 0);
|
|
|
|
#ifdef VEC_DEBUG
|
|
m_executionAvg = 0;
|
|
#endif
|
|
}
|
|
|
|
|
|
// Compose and draw all the content; called by Qt.
|
|
void VectorView::paintEvent(QPaintEvent *event)
|
|
{
|
|
#ifdef VEC_DEBUG
|
|
unsigned int drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
|
#endif
|
|
|
|
// All drawing done in this method, local variables are sufficient for the boundary
|
|
const int displayTop = 2;
|
|
const int displayBottom = height() - 2;
|
|
const int displayLeft = 2;
|
|
const int displayRight = width() - 2;
|
|
const int displayWidth = displayRight - displayLeft;
|
|
const int displayHeight = displayBottom - displayTop;
|
|
|
|
const float centerX = displayLeft + (displayWidth / 2.f);
|
|
const float centerY = displayTop + (displayWidth / 2.f);
|
|
|
|
const int margin = 4;
|
|
const int gridCorner = 30;
|
|
|
|
// Setup QPainter and font sizes
|
|
QPainter painter(this);
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
QFont normalFont, boldFont;
|
|
boldFont.setPixelSize(26);
|
|
boldFont.setBold(true);
|
|
const int labelWidth = 26;
|
|
const int labelHeight = 26;
|
|
|
|
bool hq = m_controls->m_highQualityModel.value();
|
|
|
|
// Clear display buffer if quality setting was changed
|
|
if (hq != m_oldHQ)
|
|
{
|
|
m_oldHQ = hq;
|
|
for (std::size_t i = 0; i < m_displayBuffer.size(); i++)
|
|
{
|
|
m_displayBuffer.data()[i] = 0;
|
|
}
|
|
}
|
|
|
|
// Dim stored image based on persistence setting and elapsed time.
|
|
// Update period is limited to 50 ms (20 FPS) for non-HQ mode and 10 ms (100 FPS) for HQ mode.
|
|
const unsigned int currentTimestamp = std::chrono::duration_cast<std::chrono::milliseconds>
|
|
(
|
|
std::chrono::high_resolution_clock::now().time_since_epoch()
|
|
).count();
|
|
const unsigned int elapsed = currentTimestamp - m_persistTimestamp;
|
|
const unsigned int threshold = hq ? 10 : 50;
|
|
if (elapsed > threshold)
|
|
{
|
|
m_persistTimestamp = currentTimestamp;
|
|
// Non-HQ mode uses half the resolution → use limited buffer space.
|
|
const std::size_t useableBuffer = hq ? m_displayBuffer.size() : m_displayBuffer.size() / 4;
|
|
// The knob value is interpreted on log. scale, otherwise the effect would ramp up too slowly.
|
|
// Persistence value specifies fraction of light intensity that remains after 10 ms.
|
|
// → Compensate it based on elapsed time (exponential decay).
|
|
const float persist = log10(1 + 9 * m_controls->m_persistenceModel.value());
|
|
const float persistPerFrame = pow(persist, elapsed / 10.f);
|
|
// Note that for simplicity and performance reasons, this implementation only dims all stored
|
|
// values by a given factor. A true simulation would also do the inverse of desaturation that
|
|
// occurs in high-intensity traces in HQ mode.
|
|
for (std::size_t i = 0; i < useableBuffer; i++)
|
|
{
|
|
m_displayBuffer.data()[i] *= persistPerFrame;
|
|
}
|
|
}
|
|
|
|
// Get new samples from the lockless input FIFO buffer
|
|
auto inBuffer = m_bufferReader.read_max(m_inputBuffer->capacity());
|
|
std::size_t frameCount = inBuffer.size();
|
|
|
|
// Draw new points on top
|
|
|
|
const bool logScale = m_controls->m_logarithmicModel.value();
|
|
const unsigned short activeSize = hq ? m_displaySize : m_displaySize / 2;
|
|
|
|
// Helper lambda functions for better readability
|
|
// Make sure pixel stays within display bounds:
|
|
auto saturate = [=](short pixelPos) {return qBound((short)0, pixelPos, (short)(activeSize - 1));};
|
|
// Take existing pixel and brigthen it. Very bright light should reduce saturation and become
|
|
// white. This effect is easily approximated by capping elementary colors to 255 individually.
|
|
auto updatePixel = [&](unsigned short x, unsigned short y, QColor addedColor)
|
|
{
|
|
QColor currentColor = ((QRgb*)m_displayBuffer.data())[x + y * activeSize];
|
|
currentColor.setRed(std::min(currentColor.red() + addedColor.red(), 255));
|
|
currentColor.setGreen(std::min(currentColor.green() + addedColor.green(), 255));
|
|
currentColor.setBlue(std::min(currentColor.blue() + addedColor.blue(), 255));
|
|
((QRgb*)m_displayBuffer.data())[x + y * activeSize] = currentColor.rgb();
|
|
};
|
|
|
|
if (hq)
|
|
{
|
|
// High quality mode: check distance between points and draw a line.
|
|
// The longer the line is, the dimmer, simulating real electron trace on luminescent screen.
|
|
for (std::size_t frame = 0; frame < frameCount; frame++)
|
|
{
|
|
float left = 0.0f;
|
|
float right = 0.0f;
|
|
float inLeft = inBuffer[frame][0] * m_zoom;
|
|
float inRight = inBuffer[frame][1] * m_zoom;
|
|
// Scale left and right channel from (-1.0, 1.0) to display range
|
|
if (logScale)
|
|
{
|
|
// To better preserve shapes, the log scale is applied to the distance from origin,
|
|
// not the individual channels.
|
|
const float distance = sqrt(inLeft * inLeft + inRight * inRight);
|
|
const float distanceLog = log10(1 + 9 * std::abs(distance));
|
|
const float angleCos = inLeft / distance;
|
|
const float angleSin = inRight / distance;
|
|
left = distanceLog * angleCos * (activeSize - 1) / 4;
|
|
right = distanceLog * angleSin * (activeSize - 1) / 4;
|
|
}
|
|
else
|
|
{
|
|
left = inLeft * (activeSize - 1) / 4;
|
|
right = inRight * (activeSize - 1) / 4;
|
|
}
|
|
|
|
// Rotate display coordinates 45 degrees, flip Y axis and make sure the result stays within bounds
|
|
int x = saturate(right - left + activeSize / 2.f);
|
|
int y = saturate(activeSize - (right + left + activeSize / 2.f));
|
|
|
|
// Estimate number of points needed to fill space between the old and new pixel. Cap at 100.
|
|
unsigned char points = std::min((int)sqrt((m_oldX - x) * (m_oldX - x) + (m_oldY - y) * (m_oldY - y)), 100);
|
|
|
|
// Large distance = dim trace. The curve for darker() is choosen so that:
|
|
// - no movement (0 points) actually _increases_ brightness slightly,
|
|
// - one point between samples = returns exactly the specified color,
|
|
// - one to 99 points between samples = follows a sharp "1/x" decaying curve,
|
|
// - 100 points between samples = returns approximately 5 % brightness.
|
|
// Everything else is discarded (by the 100 point cap) because there is not much to see anyway.
|
|
QColor addedColor = m_controls->m_colorFG.darker(75 + 20 * points).rgb();
|
|
|
|
// Draw the new pixel: the beam sweeps across area that may have been excited before
|
|
// → add new value to existing pixel state.
|
|
updatePixel(x, y, addedColor);
|
|
|
|
// Draw interpolated points between the old pixel and the new one
|
|
int newX = right - left + activeSize / 2.f;
|
|
int newY = activeSize - (right + left + activeSize / 2.f);
|
|
for (unsigned char i = 1; i < points; i++)
|
|
{
|
|
x = saturate(((points - i) * m_oldX + i * newX) / points);
|
|
y = saturate(((points - i) * m_oldY + i * newY) / points);
|
|
updatePixel(x, y, addedColor);
|
|
}
|
|
m_oldX = newX;
|
|
m_oldY = newY;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// To improve performance, non-HQ mode uses smaller display size and only
|
|
// one full-color pixel per sample.
|
|
for (std::size_t frame = 0; frame < frameCount; frame++)
|
|
{
|
|
float left = 0.0f;
|
|
float right = 0.0f;
|
|
float inLeft = inBuffer[frame][0] * m_zoom;
|
|
float inRight = inBuffer[frame][1] * m_zoom;
|
|
if (logScale) {
|
|
const float distance = sqrt(inLeft * inLeft + inRight * inRight);
|
|
const float distanceLog = log10(1 + 9 * std::abs(distance));
|
|
const float angleCos = inLeft / distance;
|
|
const float angleSin = inRight / distance;
|
|
left = distanceLog * angleCos * (activeSize - 1) / 4;
|
|
right = distanceLog * angleSin * (activeSize - 1) / 4;
|
|
} else {
|
|
left = inLeft * (activeSize - 1) / 4;
|
|
right = inRight * (activeSize - 1) / 4;
|
|
}
|
|
int x = saturate(right - left + activeSize / 2.f);
|
|
int y = saturate(activeSize - (right + left + activeSize / 2.f));
|
|
((QRgb*)m_displayBuffer.data())[x + y * activeSize] = m_controls->m_colorFG.rgb();
|
|
}
|
|
}
|
|
|
|
// Draw background
|
|
painter.fillRect(displayLeft, displayTop, displayWidth, displayHeight, QColor(0,0,0));
|
|
|
|
// Draw the final image
|
|
QImage temp = QImage(m_displayBuffer.data(),
|
|
activeSize,
|
|
activeSize,
|
|
QImage::Format_RGB32);
|
|
temp.setDevicePixelRatio(devicePixelRatio());
|
|
painter.drawImage(displayLeft, displayTop,
|
|
temp.scaledToWidth(displayWidth * devicePixelRatio(),
|
|
Qt::SmoothTransformation));
|
|
|
|
// Draw the grid and labels
|
|
painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
|
painter.drawEllipse(QPointF(centerX, centerY), displayWidth / 2.f, displayWidth / 2.f);
|
|
painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::DotLine, Qt::RoundCap, Qt::BevelJoin));
|
|
painter.drawLine(QPointF(centerX, centerY), QPointF(displayLeft + gridCorner, displayTop + gridCorner));
|
|
painter.drawLine(QPointF(centerX, centerY), QPointF(displayRight - gridCorner, displayTop + gridCorner));
|
|
|
|
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
|
painter.setFont(boldFont);
|
|
painter.drawText(displayLeft + margin, displayTop,
|
|
labelWidth, labelHeight, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
|
|
QString("L"));
|
|
painter.drawText(displayRight - margin - labelWidth, displayTop,
|
|
labelWidth, labelHeight, Qt::AlignRight| Qt::AlignTop | Qt::TextDontClip,
|
|
QString("R"));
|
|
|
|
// Draw the outline
|
|
painter.setPen(QPen(m_controls->m_colorOutline, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
|
painter.drawRoundedRect(1, 1, width() - 2, height() - 2, 2.f, 2.f);
|
|
|
|
// Draw zoom info if changed within last second (re-using timestamp acquired for dimming)
|
|
if (currentTimestamp - m_zoomTimestamp < 1000)
|
|
{
|
|
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
|
painter.setFont(normalFont);
|
|
painter.drawText(displayWidth / 2 - 50, displayBottom - 20, 100, 16, Qt::AlignCenter,
|
|
QString("Zoom: ").append(std::to_string((int)round(m_zoom * 100)).c_str()).append(" %"));
|
|
}
|
|
|
|
// Optionally measure drawing performance
|
|
#ifdef VEC_DEBUG
|
|
drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count() - drawTime;
|
|
m_executionAvg = 0.95f * m_executionAvg + 0.05f * drawTime / 1000000.f;
|
|
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
|
painter.setFont(normalFont);
|
|
painter.drawText(displayWidth / 2 - 50, displayBottom - 16, 100, 16, Qt::AlignLeft,
|
|
QString("Exec avg.: ").append(std::to_string(m_executionAvg).substr(0, 5).c_str()).append(" ms"));
|
|
#endif
|
|
}
|
|
|
|
|
|
// Periodically trigger repaint and check if the widget is visible
|
|
void VectorView::periodicUpdate()
|
|
{
|
|
m_visible = isVisible();
|
|
if (m_visible) {update();}
|
|
}
|
|
|
|
|
|
// Allow to change color on double-click.
|
|
// More of an Easter egg, to avoid cluttering the interface with non-essential functionality.
|
|
void VectorView::mouseDoubleClickEvent(QMouseEvent *event)
|
|
{
|
|
auto colorDialog = new ColorChooser(m_controls->m_colorFG, this);
|
|
if (colorDialog->exec())
|
|
{
|
|
m_controls->m_colorFG = colorDialog->currentColor();
|
|
}
|
|
}
|
|
|
|
|
|
// Change zoom level using the mouse wheel
|
|
void VectorView::wheelEvent(QWheelEvent *event)
|
|
{
|
|
// Go through integers to avoid accumulating errors
|
|
const unsigned short old_zoom = round(100 * m_zoom);
|
|
// Min-max bounds are 20 and 1000 %, step for 15°-increment mouse wheel is 20 %
|
|
const unsigned short new_zoom = qBound(20, old_zoom + event->angleDelta().y() / 6, 1000);
|
|
m_zoom = new_zoom / 100.f;
|
|
event->accept();
|
|
m_zoomTimestamp = std::chrono::duration_cast<std::chrono::milliseconds>
|
|
(
|
|
std::chrono::high_resolution_clock::now().time_since_epoch()
|
|
).count();
|
|
|
|
}
|
|
|
|
|
|
} // namespace lmms::gui
|