Commit 6eb94b42 authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle

Merge branch 'audiomix' into 'master'

Audiomix merge

See merge request kde/kdenlive!60
parents 0ce0f757 9f3c1670
Pipeline #8798 passed with stage
in 18 minutes and 28 seconds
......@@ -41,6 +41,7 @@ endif()
### Find source files
add_subdirectory(abstractmodel)
add_subdirectory(assets)
add_subdirectory(audiomixer)
add_subdirectory(bin)
add_subdirectory(capture)
add_subdirectory(dialogs)
......
set(kdenlive_SRCS
${kdenlive_SRCS}
audiomixer/mixerwidget.cpp
audiomixer/audiolevelwidget.cpp
audiomixer/mixermanager.cpp PARENT_SCOPE)
/*
Copyright (C) 2016 Jean-Baptiste Mardelle <jb@kdenlive.org>
This file is part of Kdenlive. See www.kdenlive.org.
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) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
by the membership of KDE e.V.), which shall act as a proxy
defined in Section 14 of version 3 of the license.
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. If not, see <http://www.gnu.org/licenses/>.
*/
#include "audiolevelwidget.hpp"
#include "core.h"
#include "profiles/profilemodel.hpp"
#include "mlt++/Mlt.h"
#include <cmath>
#include <QFont>
#include <QPaintEvent>
#include <QPainter>
#include <QFontDatabase>
const double log_factor = 1.0 / log10(1.0 / 127);
static inline double levelToDB(double level)
{
if (level <= 0) {
return -100;
}
return 100 * (1.0 - log10(level) * log_factor);
}
AudioLevelWidget::AudioLevelWidget(int width, QWidget *parent)
: QWidget(parent)
, audioChannels(2)
, m_width(width)
, m_channelWidth(width / 2)
, m_channelDistance(2)
, m_channelFillWidth(m_channelWidth)
{
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
QFont ft = font();
ft.setPointSizeF(font().pointSize() * 0.8);
setFont(ft);
setMinimumWidth(4);
}
AudioLevelWidget::~AudioLevelWidget()
{
}
void AudioLevelWidget::resizeEvent(QResizeEvent *event)
{
drawBackground(m_peaks.size());
}
void AudioLevelWidget::refreshPixmap()
{
drawBackground(m_peaks.size());
}
void AudioLevelWidget::drawBackground(int channels)
{
if (height() == 0) {
return;
}
QSize newSize = QWidget::size();
if (!newSize.isValid()) {
return;
}
m_offset = fontMetrics().boundingRect(QStringLiteral("-45")).width() + 5;
newSize.setWidth(newSize.width() - m_offset);
QLinearGradient gradient(0, newSize.height(), 0, 0);
gradient.setColorAt(0.0, Qt::darkGreen);
gradient.setColorAt(0.379, Qt::darkGreen);
gradient.setColorAt(0.38, Qt::green); // -20db
gradient.setColorAt(0.868, Qt::green);
gradient.setColorAt(0.869, Qt::yellow); // -2db
gradient.setColorAt(0.95, Qt::yellow);
gradient.setColorAt(0.951, Qt::red); // 0db
m_pixmap = QPixmap(QWidget::size());
if (m_pixmap.isNull()) {
return;
}
m_pixmap.fill(Qt::transparent);
int totalWidth;
if (channels < 2) {
m_channelWidth = newSize.width() / 2;
totalWidth = m_channelWidth;
} else {
m_channelWidth = (newSize.width() - (channels - 1)) / channels;
totalWidth= channels * m_channelWidth + (channels - 1);
}
QRect rect(m_offset, 0, newSize.width(), newSize.height());
QPainter p(&m_pixmap);
p.setOpacity(0.4);
p.setFont(font());
p.fillRect(rect, QBrush(gradient));
// Channel labels are vertical along the bottom.
QVector<int> dbscale;
dbscale << 0 << -2 << -5 << -10 << -15 << -20 << -30 << -45;
int dbLabelCount = dbscale.size();
// dB scale is vertical along the bottom
int labelHeight = fontMetrics().ascent();
for (int i = 0; i < dbLabelCount; i++) {
int value = dbscale.at(i);
QString label = QString().sprintf("%d", value);
//int labelWidth = fontMetrics().width(label);
double xf = m_pixmap.height() - pow(10.0, (double)dbscale.at(i) / 50.0) * m_pixmap.height() * 40.0 / 42;
/*if (xf + labelWidth / 2 > m_pixmap.height()) {
xf = height() - labelWidth / 2;
}*/
p.setPen(palette().dark().color());
p.drawLine(m_offset, xf, m_offset + totalWidth - 1, xf);
xf -= labelHeight * 2 / 3;
p.setPen(palette().text().color().rgb());
p.drawText(QRectF(0, xf, m_offset - 5, labelHeight), label, QTextOption(Qt::AlignRight));
}
p.setOpacity(isEnabled() ? 1 : 0.5);
p.setPen(palette().dark().color());
// Clear space between the 2 channels
p.setCompositionMode(QPainter::CompositionMode_Source);
if (m_channelWidth < 4) {
// too many audio channels, simple line between channels
m_channelDistance = 1;
m_channelFillWidth = m_channelWidth;
for (int i = 0; i < channels; i++) {
p.drawLine(m_offset + i * (m_channelWidth + m_channelDistance), 0, i * (m_channelWidth + m_channelDistance), rect.height() - 1);
}
} else {
m_channelDistance = 2;
m_channelFillWidth = m_channelWidth - 2;
for (int i = 0; i < channels; i++) {
p.drawRect(m_offset + i * (m_channelWidth + m_channelDistance), 0, m_channelWidth - 1, rect.height() - 1);
if (i > 0) {
p.fillRect(m_offset + i * (m_channelWidth + m_channelDistance) - 2, 0, 2, rect.height(), Qt::transparent);
}
}
}
p.end();
}
// cppcheck-suppress unusedFunction
void AudioLevelWidget::setAudioValues(const QVector<int> &values)
{
m_values = values;
if (m_peaks.size() != m_values.size()) {
m_peaks = values;
drawBackground(values.size());
} else {
for (int i = 0; i < m_values.size(); i++) {
m_peaks[i]--;
if (m_values.at(i) > m_peaks.at(i)) {
m_peaks[i] = m_values.at(i);
}
}
}
update();
}
void AudioLevelWidget::setVisibility(bool enable)
{
if (enable) {
setVisible(true);
setFixedWidth(m_width);
} else {
// set height to 0 so the toolbar layout is not affected
setFixedHeight(0);
setVisible(false);
}
}
void AudioLevelWidget::paintEvent(QPaintEvent *pe)
{
if (!isVisible()) {
return;
}
QPainter p(this);
p.setClipRect(pe->rect());
QRect rect(0, 0, width(), height());
if (m_values.isEmpty()) {
p.setOpacity(0.2);
p.drawPixmap(rect, m_pixmap);
return;
}
p.drawPixmap(rect, m_pixmap);
p.setPen(palette().dark().color());
p.setOpacity(0.9);
int width = m_channelDistance == 1 ? rect.width() : rect.width() - 1;
for (int i = 0; i < m_values.count(); i++) {
if (m_values.at(i) >= 100) {
continue;
}
int val = (50 + m_values.at(i)) / 150.0 * rect.height();
p.fillRect(m_offset + i * (m_channelWidth + m_channelDistance) + 1, 0, m_channelFillWidth, height() - val, palette().dark());
p.fillRect(m_offset + i * (m_channelWidth + m_channelDistance) + 1, height() - (50 + m_peaks.at(i)) / 150.0 * rect.height(), m_channelFillWidth, 1, palette().text());
}
}
/*
Copyright (C) 2016 Jean-Baptiste Mardelle <jb@kdenlive.org>
This file is part of Kdenlive. See www.kdenlive.org.
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) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
by the membership of KDE e.V.), which shall act as a proxy
defined in Section 14 of version 3 of the license.
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. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef AUDIOLEVELWIDGET_H
#define AUDIOLEVELWIDGET_H
#include <QWidget>
#include <memory>
namespace Mlt {
class Filter;
} // namespace Mlt
class AudioLevelWidget : public QWidget
{
Q_OBJECT
public:
explicit AudioLevelWidget(int width, QWidget *parent = nullptr);
~AudioLevelWidget() override;
void refreshPixmap();
int audioChannels;
bool isValid;
void setVisibility(bool enable);
protected:
void paintEvent(QPaintEvent *) override;
void resizeEvent(QResizeEvent *event) override;
private:
std::unique_ptr<Mlt::Filter> m_filter;
int m_width;
int m_offset;
QPixmap m_pixmap;
QVector<int> m_peaks;
QVector<int> m_values;
int m_channelWidth;
int m_channelDistance;
int m_channelFillWidth;
void drawBackground(int channels = 2);
public slots:
void setAudioValues(const QVector<int> &values);
};
#endif
/***************************************************************************
* Copyright (C) 2019 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* 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) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* 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. If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
#include "mixermanager.hpp"
#include "mixerwidget.hpp"
#include "timeline2/model/timelineitemmodel.hpp"
#include "mlt++/MltService.h"
#include "mlt++/MltTractor.h"
#include <klocalizedstring.h>
#include <QHBoxLayout>
#include <QModelIndex>
const double log_factor = 1.0 / log10(1.0 / 127);
static inline double levelToDB(double level)
{
if (level <= 0) {
return -100;
}
return 100 * (1.0 - log10(level) * log_factor);
}
MixerManager::MixerManager(QWidget *parent)
: QWidget(parent)
, renderPosition(0)
, m_connectedWidgets(0)
{
m_box = new QHBoxLayout;
m_box->addStretch(10);
setLayout(m_box);
m_timer.setSingleShot(true);
m_timer.setInterval(100);
connect(&m_timer, &QTimer::timeout, [&] () {
for (auto w : m_mixers) {
w.second->updateAudioLevel(m_lastFrame);
}
});
}
void MixerManager::registerTrack(int tid, std::shared_ptr<Mlt::Tractor> service, const QString &trackTag)
{
if (m_mixers.count(tid) > 0) {
// Track already registered
return;
}
std::shared_ptr<MixerWidget> mixer(new MixerWidget(tid, service, trackTag, this));
connect(mixer.get(), &MixerWidget::muteTrack, [&](int id, bool mute) {
m_model->setTrackProperty(id, "hide", mute ? QStringLiteral("1") : QStringLiteral("3"));
});
connect(this, &MixerManager::updateLevels, mixer.get(), &MixerWidget::updateAudioLevel);
connect(mixer.get(), &MixerWidget::updateConnection, [&](int id, bool doConnect) {
//connect(this, &MixerManager::storeLevels, m_mixers[id].get(), &MixerWidget::storeAudioLevel, Qt::DirectConnection);
m_connectedWidgets++;
if (m_connectedWidgets == 1) {
emit connectMixerRenderer(true);
}
});
connect(mixer.get(), &MixerWidget::toggleSolo, [&](int trid, bool solo) {
if (!solo) {
// unmute
for (int id : m_soloMuted) {
if (m_mixers.count(id) > 0) {
m_model->setTrackProperty(id, "hide", QStringLiteral("1"));
}
}
m_soloMuted.clear();
} else {
if (!m_soloMuted.isEmpty()) {
// Another track was solo, discard first
for (int id : m_soloMuted) {
if (m_mixers.count(id) > 0) {
m_model->setTrackProperty(id, "hide", QStringLiteral("1"));
}
}
m_soloMuted.clear();
}
for (auto item : m_mixers) {
if (item.first > -1 && item.first != trid && !item.second->isMute()) {
m_model->setTrackProperty(item.first, "hide", QStringLiteral("3"));
m_soloMuted << item.first;
item.second->unSolo();
}
}
}
});
m_mixers[tid] = mixer;
m_box->insertWidget(0, mixer.get());
}
void MixerManager::deregisterTrack(int tid)
{
Q_ASSERT(m_mixers.count(tid) > 0);
m_mixers.erase(tid);
}
void MixerManager::resetAudioValues()
{
qDebug()<<"======\n\nRESTTING AUDIO VALUES\n\n------------------_";
for (auto item : m_mixers) {
item.second.get()->clear();
}
}
void MixerManager::cleanup()
{
for (auto item : m_mixers) {
m_box->removeWidget(item.second.get());
}
m_mixers.clear();
m_connectedWidgets = 0;
}
void MixerManager::setModel(std::shared_ptr<TimelineItemModel> model)
{
// Insert master mixer
int tid = -1;
m_model = model;
connect(m_model.get(), &TimelineItemModel::dataChanged, [&](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
if (roles.contains(TimelineModel::IsDisabledRole)) {
int id = (int) topLeft.internalId();
if (m_mixers.count(id) > 0) {
m_mixers[id]->setMute(m_model->data(topLeft, TimelineModel::IsDisabledRole).toBool());
} else {
qDebug()<<"=== MODEL DATA CHANGED: MUTE DONE TRACK NOT FOUND!!!";
}
}
});
Mlt::Tractor *service = model->tractor();
if (m_mixers.count(tid) > 0) {
// delete previous master mixer
m_box->removeWidget(m_mixers[tid].get());
m_mixers.erase(tid);
}
std::shared_ptr<MixerWidget> mixer(new MixerWidget(tid, service, i18n("Master"), this));
connect(mixer.get(), &MixerWidget::muteTrack, [&](int /*id*/, bool mute) {
qDebug()<<"=== SETTING MUITE";
m_model->tractor()->set("hide", mute ? 3 : 1);
});
connect(this, &MixerManager::updateLevels, mixer.get(), &MixerWidget::updateAudioLevel);
connect(mixer.get(), &MixerWidget::updateConnection, [&](int id, bool doConnect) {
//connect(this, &MixerManager::storeLevels, m_mixers[id].get(), &MixerWidget::storeAudioLevel, Qt::DirectConnection);
m_connectedWidgets++;
if (m_connectedWidgets == 1) {
emit connectMixerRenderer(true);
}
});
m_mixers[tid] = mixer;
m_box->insertWidget(0, mixer.get());
}
/***************************************************************************
* Copyright (C) 2019 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* 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) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* 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. If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
#ifndef MIXERMANGER_H
#define MIXERMANGER_H
#include "definitions.h"
#include <memory>
#include <unordered_map>
#include <QWidget>
#include <QTimer>
namespace Mlt {
class Tractor;
}
class MixerWidget;
class QHBoxLayout;
class TimelineItemModel;
class MixerManager : public QWidget
{
Q_OBJECT
public:
MixerManager(QWidget *parent);
/* @brief Shows the parameters of the given transition model */
void registerTrack(int tid, std::shared_ptr<Mlt::Tractor> service, const QString &trackTag);
void deregisterTrack(int tid);
void setModel(std::shared_ptr<TimelineItemModel> model);
void cleanup();
public slots:
void resetAudioValues();
signals:
void updateLevels(int);
void connectMixerRenderer(bool);
void triggerUpdate();
public:
int renderPosition;
protected:
std::unordered_map<int, std::shared_ptr<MixerWidget>> m_mixers;
private:
std::shared_ptr<Mlt::Tractor> m_masterService;
std::shared_ptr<TimelineItemModel> m_model;
QHBoxLayout *m_box;
QTimer m_timer;
int m_lastFrame;
int m_connectedWidgets;
QVector <int> m_soloMuted;
};
#endif
/***************************************************************************
* Copyright (C) 2019 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* 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) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* 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. If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
#include "mixerwidget.hpp"
#include "mlt++/MltFilter.h"
#include "mlt++/MltTractor.h"
#include "mlt++/MltProfile.h"
#include "mixerwidget.hpp"
#include "mixermanager.hpp"
#include "audiolevelwidget.hpp"
#include <klocalizedstring.h>
#include <KDualAction>
#include <QGridLayout>
#include <QToolButton>
#include <QCheckBox>
#include <QSlider>
#include <QSpinBox>
#include <QDial>
#include <QLabel>
#include <QFontDatabase>
const double log_factor = 1.0 / log10(1.0 / 127);
static inline int levelToDB(double level)
{
if (level <= 0) {
return -100;
}
return 100 * (1.0 - log10(level) * log_factor);
}
void MixerWidget::property_changed( mlt_service , MixerWidget *widget, char * /*name*/ )
{
if (!widget->m_levels.contains(widget->m_manager->renderPosition)) {
widget->m_levels[widget->m_manager->renderPosition] = {levelToDB(mlt_properties_get_double(MLT_FILTER_PROPERTIES(widget->m_monitorFilter->get_filter()), "_audio_level.0")), levelToDB(mlt_properties_get_double(MLT_FILTER_PROPERTIES(widget->m_monitorFilter->get_filter()), "_audio_level.1"))};
}
}
MixerWidget::MixerWidget(int tid, std::shared_ptr<Mlt::Tractor> service, const QString &trackTag, MixerManager *parent)
: QWidget(parent)
, m_manager(parent)
, m_tid(tid)
, m_levelFilter(nullptr)
, m_monitorFilter(nullptr)
, m_balanceFilter(nullptr)
, m_solo(nullptr)
, m_lastVolume(0)
{
buildUI(service.get(), trackTag);
}
MixerWidget::MixerWidget(int tid, Mlt::Tractor *service, const QString &trackTag, MixerManager *parent)
: QWidget(parent)
, m_manager(parent)
, m_tid(tid)
, m_levelFilter(nullptr)
, m_monitorFilter(nullptr)