Commit 1a7d2eee authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Improve audio clips display: proper thumbnails and monitor view

parent 3fb2ba13
Pipeline #10102 passed with stage
in 26 minutes and 57 seconds
......@@ -71,6 +71,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <QUrl>
#include <QVBoxLayout>
#include <utility>
/**
* @class BinItemDelegate
* @brief This class is responsible for drawing items in the QTreeView.
......@@ -699,7 +700,6 @@ Bin::Bin(std::shared_ptr<ProjectItemModel> model, QWidget *parent)
setFocusPolicy(Qt::ClickFocus);
connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel);
connect(m_itemModel.get(), &ProjectItemModel::refreshAudioThumbs, this, &Bin::doRefreshAudioThumbs);
connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip);
connect(m_itemModel.get(), &ProjectItemModel::emitMessage, this, &Bin::emitMessage);
......@@ -2197,13 +2197,6 @@ void Bin::refreshClip(const QString &id)
}
}
void Bin::doRefreshAudioThumbs(const QString &id)
{
if (m_monitor->activeClipId() == id) {
slotSendAudioThumb(id);
}
}
void Bin::slotCreateProjectClip()
{
auto *act = qobject_cast<QAction *>(sender());
......@@ -3085,16 +3078,6 @@ QStringList Bin::getProxyHashList()
return list;
}
void Bin::slotSendAudioThumb(const QString &id)
{
std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
if ((clip != nullptr) && clip->audioThumbCreated()) {
m_monitor->prepareAudioThumb(clip->audioChannels(), clip->audioFrameCache);
} else {
m_monitor->prepareAudioThumb(0);
}
}
bool Bin::isEmpty() const
{
if (m_itemModel->getRootFolder() == nullptr) {
......
......@@ -304,9 +304,6 @@ private slots:
/** @brief Rename a Bin Item. */
void slotRenameItem();
void doRefreshPanel(const QString &id);
/** @brief Send audio thumb data to monitor for display. */
void slotSendAudioThumb(const QString &id);
void doRefreshAudioThumbs(const QString &id);
/** @brief Enable item view and hide message */
void slotMessageActionTriggered();
/** @brief Request editing of title or slideshow clip */
......
......@@ -197,9 +197,6 @@ void ProjectClip::updateAudioThumbnail(QList<double> audioLevels)
{
audioFrameCache = audioLevels;
m_audioThumbCreated = true;
if (auto ptr = m_model.lock()) {
emit std::static_pointer_cast<ProjectItemModel>(ptr)->refreshAudioThumbs(m_binId);
}
}
bool ProjectClip::audioThumbCreated() const
......@@ -1195,7 +1192,7 @@ void ProjectClip::discardAudioThumb()
pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB);
}
const QString ProjectClip::getAudioThumbPath()
const QString ProjectClip::getAudioThumbPath(bool miniThumb)
{
if (audioInfo() == nullptr) {
return QString();
......@@ -1211,6 +1208,10 @@ const QString ProjectClip::getAudioThumbPath()
return QString();
}
QString audioPath = thumbFolder.absoluteFilePath(clipHash);
if (miniThumb) {
audioPath.append(QStringLiteral(".png"));
return audioPath;
}
if (audioStream > 0) {
audioPath.append(QLatin1Char('_') + QString::number(audioInfo()->audio_index()));
}
......
......@@ -185,7 +185,7 @@ public:
/** @brief Delete cached audio thumb - needs to be recreated */
void discardAudioThumb();
/** @brief Get path for this clip's audio thumbnail */
const QString getAudioThumbPath();
const QString getAudioThumbPath(bool miniThumb = false);
/** @brief Returns true if this producer has audio and can be splitted on timeline*/
bool isSplittable() const;
......@@ -283,6 +283,7 @@ signals:
void thumbReady(int, const QImage &);
/** @brief Clip is ready, load properties. */
void loadPropertiesPanel();
void audioThumbReady();
};
#endif
......@@ -106,117 +106,135 @@ bool AudioThumbJob::computeWithMlt()
bool AudioThumbJob::computeWithFFMPEG()
{
m_audioLevels.clear();
QStringList args;
std::vector<std::unique_ptr<QTemporaryFile>> channelFiles;
for (int i = 0; i < m_channels; i++) {
std::unique_ptr<QTemporaryFile> channelTmpfile(new QTemporaryFile());
if (!channelTmpfile->open()) {
m_errorMessage.append(i18n("Audio thumbs: cannot create temporary file, check disk space and permissions\n"));
return false;
}
channelTmpfile->close();
channelFiles.emplace_back(std::move(channelTmpfile));
}
// Always create audio thumbs from the original source file, because proxy
// can have a different audio config (channels / mono/ stereo)
QString filePath = m_prod->get("kdenlive:originalurl");
if (filePath.isEmpty()) {
filePath = m_prod->get("resource");
}
args << QStringLiteral("-i") << QUrl::fromLocalFile(filePath).toLocalFile();
// Output progress info
args << QStringLiteral("-progress");
m_ffmpegProcess.reset(new QProcess);
if (!m_thumbInCache) {
QStringList args;
args << QStringLiteral("-hide_banner") << QStringLiteral("-y")<< QStringLiteral("-i") << QUrl::fromLocalFile(filePath).toLocalFile() << QStringLiteral("-filter_complex:a");
args << QString("showwavespic=s=%1x%2:split_channels=1:scale=cbrt:colors=0xffdddd|0xddffdd").arg(m_thumbSize.width()).arg(m_thumbSize.height());
args << QStringLiteral("-frames:v") << QStringLiteral("1");
args << m_binClip->getAudioThumbPath(true);
connect(m_ffmpegProcess.get(), &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress);
m_ffmpegProcess->start(KdenliveSettings::ffmpegpath(), args);
m_ffmpegProcess->waitForFinished(-1);
if (m_ffmpegProcess->exitStatus() != QProcess::CrashExit) {
m_thumbInCache = true;
if (m_dataInCache) {
m_done = true;
return true;
} else {
// Next Processinĝ step can be long, already display audio thumb in monitor
m_binClip->audioThumbReady();
}
}
}
if (!m_dataInCache) {
m_audioLevels.clear();
std::vector<std::unique_ptr<QTemporaryFile>> channelFiles;
for (int i = 0; i < m_channels; i++) {
std::unique_ptr<QTemporaryFile> channelTmpfile(new QTemporaryFile());
if (!channelTmpfile->open()) {
m_errorMessage.append(i18n("Audio thumbs: cannot create temporary file, check disk space and permissions\n"));
return false;
}
channelTmpfile->close();
channelFiles.emplace_back(std::move(channelTmpfile));
}
// Always create audio thumbs from the original source file, because proxy
// can have a different audio config (channels / mono/ stereo)
QStringList args {QStringLiteral("-hide_banner"), QStringLiteral("-i"), QUrl::fromLocalFile(filePath).toLocalFile(), QStringLiteral("-progress")};
#ifdef Q_OS_WIN
args << QStringLiteral("-");
args << QStringLiteral("-");
#else
args << QStringLiteral("/dev/stdout");
args << QStringLiteral("/dev/stdout");
#endif
bool isFFmpeg = KdenliveSettings::ffmpegpath().contains(QLatin1String("ffmpeg"));
args << QStringLiteral("-filter_complex:a");
if (m_channels == 1) {
//TODO: this does not correcty generate the correct stream data
args << QStringLiteral("aformat=channel_layouts=mono,%1=100").arg(isFFmpeg ? "aresample=async" : "sample_rates");
args << QStringLiteral("-map") << QStringLiteral("0:a%1").arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString())
<< QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data")
bool isFFmpeg = KdenliveSettings::ffmpegpath().contains(QLatin1String("ffmpeg"));
args << QStringLiteral("-filter_complex:a");
if (m_channels == 1) {
//TODO: this does not correcty generate the correct stream data
args << QStringLiteral("aformat=channel_layouts=mono,%1=100").arg(isFFmpeg ? "aresample=async" : "sample_rates");
args << QStringLiteral("-map") << QStringLiteral("0:a%1").arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString())
<< QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-frames:v")
<< QStringLiteral("1") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data")
<< channelFiles[0]->fileName();
} else {
QString aformat = QStringLiteral("[0:a%1]%2=100,channelsplit=channel_layout=%3")
} else {
QString aformat = QStringLiteral("[0:a%1]%2=100,channelsplit=channel_layout=%3")
.arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString())
.arg(isFFmpeg ? "aresample=async" : "aformat=sample_rates=")
.arg(m_channels > 2 ? "5.1" : "stereo");
for (int i = 0; i < m_channels; ++i) {
aformat.append(QStringLiteral("[0:%1]").arg(i));
}
args << aformat;
for (int i = 0; i < m_channels; i++) {
// Channel 1
args << QStringLiteral("-map") << QStringLiteral("[0:%1]").arg(i) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y")
for (int i = 0; i < m_channels; ++i) {
aformat.append(QStringLiteral("[0:%1]").arg(i));
}
args << aformat;
args << QStringLiteral("-frames:v") << QStringLiteral("1");
for (int i = 0; i < m_channels; i++) {
// Channel 1
args << QStringLiteral("-map") << QStringLiteral("[0:%1]").arg(i) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y")
<< QStringLiteral("-f") << QStringLiteral("data") << channelFiles[size_t(i)]->fileName();
}
}
}
m_ffmpegProcess = new QProcess;
m_ffmpegProcess->start(KdenliveSettings::ffmpegpath(), args);
connect(m_ffmpegProcess, &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress);
m_ffmpegProcess->waitForFinished(-1);
if (m_ffmpegProcess->exitStatus() != QProcess::CrashExit) {
int dataSize = 0;
std::vector<const qint16 *> rawChannels;
std::vector<QByteArray> sourceChannels;
for (auto &channelFile : channelFiles) {
channelFile->open();
sourceChannels.emplace_back(channelFile->readAll());
QByteArray &res = sourceChannels.back();
channelFile->close();
if (dataSize == 0) {
dataSize = res.size();
m_ffmpegProcess->start(KdenliveSettings::ffmpegpath(), args);
m_ffmpegProcess->waitForFinished(-1);
if (m_ffmpegProcess->exitStatus() != QProcess::CrashExit) {
int dataSize = 0;
std::vector<const qint16 *> rawChannels;
std::vector<QByteArray> sourceChannels;
for (auto &channelFile : channelFiles) {
channelFile->open();
sourceChannels.emplace_back(channelFile->readAll());
QByteArray &res = sourceChannels.back();
channelFile->close();
if (dataSize == 0) {
dataSize = res.size();
}
if (res.isEmpty() || res.size() != dataSize) {
// Something went wrong, abort
m_errorMessage.append(i18n("Audio thumbs: error reading audio thumbnail created with FFmpeg\n"));
return false;
}
rawChannels.emplace_back((const qint16 *)res.constData());
}
if (res.isEmpty() || res.size() != dataSize) {
// Something went wrong, abort
m_errorMessage.append(i18n("Audio thumbs: error reading audio thumbnail created with FFmpeg\n"));
return false;
int progress = 0;
std::vector<long> channelsData;
double offset = (double)dataSize / (2.0 * m_lengthInFrames);
int intraOffset = 1;
if (offset > 1000) {
intraOffset = offset / 60;
} else if (offset > 250) {
intraOffset = offset / 10;
}
rawChannels.emplace_back((const qint16 *)res.constData());
}
int progress = 0;
std::vector<long> channelsData;
double offset = (double)dataSize / (2.0 * m_lengthInFrames);
int intraOffset = 1;
if (offset > 1000) {
intraOffset = offset / 60;
} else if (offset > 250) {
intraOffset = offset / 10;
}
double factor = 800.0 / 32768;
for (int i = 0; i < m_lengthInFrames; i++) {
channelsData.resize((size_t)rawChannels.size());
std::fill(channelsData.begin(), channelsData.end(), 0);
int pos = (int)(i * offset);
int steps = 0;
for (int j = 0; j < (int)offset && (pos + j < dataSize); j += intraOffset) {
steps++;
for (size_t k = 0; k < rawChannels.size(); k++) {
double factor = 800.0 / 32768;
for (int i = 0; i < m_lengthInFrames; i++) {
channelsData.resize((size_t)rawChannels.size());
std::fill(channelsData.begin(), channelsData.end(), 0);
int pos = (int)(i * offset);
int steps = 0;
for (int j = 0; j < (int)offset && (pos + j < dataSize); j += intraOffset) {
steps++;
for (size_t k = 0; k < rawChannels.size(); k++) {
channelsData[k] += abs(rawChannels[k][pos + j]);
}
}
}
for (long &k : channelsData) {
if (steps != 0) {
k /= steps;
for (long &k : channelsData) {
if (steps != 0) {
k /= steps;
}
m_audioLevels << (int)((double)k * factor);
}
int p = 80 + (i * 20 / m_lengthInFrames);
if (p != progress) {
emit jobProgress(p);
progress = p;
}
m_audioLevels << (int)((double)k * factor);
}
int p = 80 + (i * 20 / m_lengthInFrames);
if (p != progress) {
emit jobProgress(p);
progress = p;
}
m_done = true;
return true;
}
m_done = true;
return true;
}
QString err = m_ffmpegProcess->readAllStandardError();
delete m_ffmpegProcess;
// m_errorMessage += err;
// m_errorMessage.append(i18n("Failed to create FFmpeg audio thumbnails, we now try to use MLT"));
qWarning() << "Failed to create FFmpeg audio thumbs:\n" << err << "\n---------------------";
......@@ -242,6 +260,9 @@ bool AudioThumbJob::startJob()
if (m_done) {
return true;
}
m_dataInCache = false;
m_thumbInCache = false;
m_thumbSize = QSize(1000, 1000 / pCore->getCurrentDar());
m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId);
if (m_binClip->audioChannels() == 0 || m_binClip->audioThumbCreated()) {
// nothing to do
......@@ -281,15 +302,24 @@ bool AudioThumbJob::startJob()
}
}
if (!m_audioLevels.isEmpty()) {
m_dataInCache = true;
}
// Check audio thumbnail image
if (ThumbnailCache::get()->hasThumbnail(m_clipId, -1, false)) {
m_thumbInCache = true;
}
if (m_thumbInCache && m_dataInCache) {
m_done = true;
m_successful = true;
return true;
}
bool ok = m_binClip->clipType() == ClipType::Playlist ? false : computeWithFFMPEG();
ok = ok ? ok : computeWithMlt();
Q_ASSERT(ok == m_done);
if (ok && m_done && !m_audioLevels.isEmpty()) {
if (ok && m_done && !m_dataInCache && !m_audioLevels.isEmpty()) {
// Put into an image for caching.
int count = m_audioLevels.size();
image = QImage((int)lrint((count + 3) / 4.0 / m_channels), m_channels, QImage::Format_ARGB32);
......@@ -312,6 +342,9 @@ bool AudioThumbJob::startJob()
image.save(m_cachePath);
m_successful = true;
return true;
} else if (ok && m_thumbInCache && m_done) {
m_successful = true;
return true;
}
m_done = true;
m_successful = false;
......@@ -321,6 +354,7 @@ bool AudioThumbJob::startJob()
bool AudioThumbJob::commitResult(Fun &undo, Fun &redo)
{
Q_ASSERT(!m_resultConsumed);
m_ffmpegProcess.reset();
if (!m_done) {
qDebug() << "ERROR: Trying to consume invalid results";
return false;
......@@ -330,14 +364,22 @@ bool AudioThumbJob::commitResult(Fun &undo, Fun &redo)
return false;
}
QList <double>old = m_binClip->audioFrameCache;
QImage oldImage = m_binClip->thumbnail(m_thumbSize.width(), m_thumbSize.height()).toImage();
QImage result = ThumbnailCache::get()->getAudioThumbnail(m_clipId);
// note that the image is moved into lambda, it won't be available from this class anymore
auto operation = [clip = m_binClip, audio = std::move(m_audioLevels)]() {
auto operation = [clip = m_binClip, audio = std::move(m_audioLevels), image = std::move(result)]() {
clip->updateAudioThumbnail(audio);
if (!image.isNull() && clip->clipType() == ClipType::Audio) {
clip->setThumbnail(image);
}
return true;
};
auto reverse = [clip = m_binClip, audio = std::move(old)]() {
auto reverse = [clip = m_binClip, audio = std::move(old), image = std::move(oldImage)]() {
clip->updateAudioThumbnail(audio);
if (!image.isNull() && clip->clipType() == ClipType::Audio) {
clip->setThumbnail(image);
}
return true;
};
bool ok = operation();
......
......@@ -24,6 +24,7 @@
#include "abstractclipjob.h"
#include <memory>
#include <QImage>
/* @brief This class represents the job that corresponds to computing the audio thumb of a clip (waveform)
*/
......@@ -63,11 +64,14 @@ protected:
private:
std::shared_ptr<ProjectClip> m_binClip;
std::shared_ptr<Mlt::Producer> m_prod;
QString m_miniThumbPath;
QString m_cachePath;
QSize m_thumbSize;
bool m_dataInCache;
bool m_thumbInCache;
bool m_done{false}, m_successful{false};
int m_channels, m_frequency, m_lengthInFrames, m_audioStream;
QList <double>m_audioLevels;
QProcess *m_ffmpegProcess;
std::unique_ptr<QProcess> m_ffmpegProcess;
};
......@@ -81,9 +81,9 @@ bool ThumbJob::startJob()
return true;
}
m_inCache = false;
if (ThumbnailCache::get()->hasThumbnail(m_binClip->clipId(), m_frameNumber, !m_persistent)) {
if (ThumbnailCache::get()->hasThumbnail(m_clipId, m_frameNumber, !m_persistent)) {
m_done = true;
m_result = ThumbnailCache::get()->getThumbnail(m_binClip->clipId(), m_frameNumber);
m_result = ThumbnailCache::get()->getThumbnail(m_clipId, m_frameNumber);
m_inCache = true;
return true;
}
......@@ -129,7 +129,7 @@ bool ThumbJob::commitResult(Fun &undo, Fun &redo)
p.setPen(Qt::white);
p.drawText(0, 0, m_fullWidth, m_imageHeight, Qt::AlignCenter, i18n("Invalid"));
} else {
ThumbnailCache::get()->storeThumbnail(m_binClip->clipId(), m_frameNumber, m_result, m_persistent);
ThumbnailCache::get()->storeThumbnail(m_clipId, m_frameNumber, m_result, m_persistent);
}
}
m_resultConsumed = true;
......
......@@ -354,7 +354,7 @@
<entry name="window_background" type="Color">
<label>Background color for OpenGL monitor.</label>
<default>#999999</default>
<default>#535353</default>
</entry>
<entry name="volume" type="Int">
......
......@@ -35,7 +35,6 @@
#include "kdenlivesettings.h"
#include "monitorproxy.h"
#include "profiles/profilemodel.hpp"
#include "qml/qmlaudiothumb.h"
#include "timeline2/view/qml/timelineitems.h"
#include <mlt++/Mlt.h>
......@@ -95,7 +94,6 @@ GLWidget::GLWidget(int id, QObject *parent)
, m_isZoneMode(false)
, m_isLoopMode(false)
, m_offset(QPoint(0, 0))
, m_audioWaveDisplayed(false)
, m_fbo(nullptr)
, m_shareContext(nullptr)
, m_openGLSync(false)
......@@ -114,7 +112,6 @@ GLWidget::GLWidget(int id, QObject *parent)
qRegisterMetaType<Mlt::Frame>("Mlt::Frame");
qRegisterMetaType<SharedFrame>("SharedFrame");
qmlRegisterType<QmlAudioThumb>("AudioThumb", 1, 0, "QmlAudioThumb");
setPersistentOpenGLContext(true);
setPersistentSceneGraph(true);
setClearBeforeRendering(false);
......@@ -138,8 +135,6 @@ GLWidget::GLWidget(int id, QObject *parent)
connect(this, &QQuickWindow::sceneGraphInitialized, this, &GLWidget::initializeGL, Qt::DirectConnection);
connect(this, &QQuickWindow::beforeRendering, this, &GLWidget::paintGL, Qt::DirectConnection);
connect(this, &GLWidget::buildAudioThumb, this, &GLWidget::setAudioThumb);
registerTimelineItems();
m_proxy = new MonitorProxy(this);
connect(m_proxy, &MonitorProxy::seekRequestChanged, this, &GLWidget::requestSeek);
......@@ -928,20 +923,6 @@ static void onThreadStopped(mlt_properties owner, GLWidget *self)
self->stopGlsl();
}
void GLWidget::slotSwitchAudioOverlay(bool enable)
{
KdenliveSettings::setDisplayAudioOverlay(enable);
if (m_audioWaveDisplayed && !enable) {
if (m_producer && m_producer->get_int("video_index") != -1) {
// We have a video producer, disable filter
removeAudioOverlay();
}
}
if (enable && !m_audioWaveDisplayed && m_producer) {
createAudioOverlay(m_producer->get_int("video_index") == -1);
}
}
int GLWidget::setProducer(const std::shared_ptr<Mlt::Producer> &producer, bool isActive, int position)
{
int error = 0;
......@@ -954,9 +935,6 @@ int GLWidget::setProducer(const std::shared_ptr<Mlt::Producer> &producer, bool i
if (currentId == QLatin1String("black")) {
return 0;
}
if (m_audioWaveDisplayed) {
removeAudioOverlay();
}
m_producer = m_blackClip;
// Reset markersModel
rootContext()->setContextProperty("markersModel", 0);
......@@ -983,27 +961,6 @@ int GLWidget::setProducer(const std::shared_ptr<Mlt::Producer> &producer, bool i
return error;
}
consumerPosition = m_consumer->position();
if (m_producer->get_int("video_index") == -1) {
// This is an audio only clip, attach visualization filter. Currently, the filter crashes MLT when Movit accel is used
if (!m_audioWaveDisplayed) {
createAudioOverlay(true);
} else if (m_consumer) {
if (KdenliveSettings::gpu_accel()) {
removeAudioOverlay();
} else {
adjustAudioOverlay(true);
}
}
} else if (m_audioWaveDisplayed && (m_consumer != nullptr)) {
// This is not an audio clip, hide wave
if (KdenliveSettings::displayAudioOverlay()) {
adjustAudioOverlay(m_producer->get_int("video_index") == -1);
} else {
removeAudioOverlay();
}
} else if (KdenliveSettings::displayAudioOverlay()) {
createAudioOverlay(false);
}
if (position == -1 && m_producer->parent().get("kdenlive:id") == currentId) {
position = consumerPosition;
}
......@@ -1026,74 +983,6 @@ void GLWidget::resetDrops()
}
}
void GLWidget::createAudioOverlay(bool isAudio)
{
if (!m_consumer) {
return;
}
if (isAudio && KdenliveSettings::gpu_accel()) {
// Audiowaveform filter crashes on Movit + audio clips)
return;
}
Mlt::Filter f(pCore->getCurrentProfile()->profile(), "audiowaveform");
if (f.is_valid()) {
// f.set("show_channel", 1);
f.set("color.1", "0xffff0099");
f.set("fill", 1);
if (isAudio) {
// Fill screen
f.set("rect", "0,0,100%,100%");
} else {
// Overlay on lower part of the screen
f.set("rect", "0,80%,100%,20%");
}
m_consumer->attach(f);
m_audioWaveDisplayed = true;
}
}
void GLWidget::removeAudioOverlay()
{
Mlt::Service sourceService(m_consumer->get_service());
// move all effects to the correct producer
int ct = 0;
Mlt::Filter *filter = sourceService.filter(ct);
while (filter != nullptr) {
QString srv = filter->get("mlt_service");
if (srv == QLatin1String("audiowaveform")) {
sourceService.detach(*filter);
delete filter;
break;
} else {
ct++;
}
filter = sourceService.filter(ct);
}