/*************************************************************************** * 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 . * ***************************************************************************/ #include "mixerwidget.hpp" #include "mlt++/MltFilter.h" #include "mlt++/MltTractor.h" #include "mlt++/MltEvent.h" #include "mlt++/MltProfile.h" #include "core.h" #include "kdenlivesettings.h" #include "mixermanager.hpp" #include "audiolevelwidget.hpp" #include "capture/mediacapture.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static inline double IEC_Scale(double dB) { dB = log10(dB) * 20.0; double fScale = 1.0; if (dB < -70.0) fScale = 0.0; else if (dB < -60.0) fScale = (dB + 70.0) * 0.0025; else if (dB < -50.0) fScale = (dB + 60.0) * 0.005 + 0.025; else if (dB < -40.0) fScale = (dB + 50.0) * 0.0075 + 0.075; else if (dB < -30.0) fScale = (dB + 40.0) * 0.015 + 0.15; else if (dB < -20.0) fScale = (dB + 30.0) * 0.02 + 0.3; else if (dB < -0.001 || dB > 0.001) /* if (dB < 0.0f) */ fScale = (dB + 20.0) * 0.025 + 0.5; return fScale; } static inline int fromDB(double level) { int value = 60; if (level > 0.) { // increase volume value = 100 - int((pow(10, 1. - level/24) - 1) / .225); } else if (level < 0.) { value = int((10 - pow(10, 1. - level/-50)) / -0.11395) + 59; } return value; } void MixerWidget::property_changed( mlt_service , MixerWidget *widget, mlt_event_data data ) { if (widget && !strcmp(Mlt::EventData(data).to_string(), "_position")) { mlt_properties filter_props = MLT_FILTER_PROPERTIES( widget->m_monitorFilter->get_filter()); int pos = mlt_properties_get_int(filter_props, "_position"); if (!widget->m_levels.contains(pos)) { QVector levels; for (int i = 0; i < widget->m_channels; i++) { levels << IEC_Scale(mlt_properties_get_double(filter_props, QString("_audio_level.%1").arg(i).toUtf8().constData())); } widget->m_levels[pos] = std::move(levels); //{IEC_Scale(mlt_properties_get_double(filter_props, "_audio_level.0")), IEC_Scale(mlt_properties_get_double(filter_props, "_audio_level.1"))}; if (widget->m_levels.size() > widget->m_maxLevels) { widget->m_levels.erase(widget->m_levels.begin()); } } } } MixerWidget::MixerWidget(int tid, std::shared_ptr service, QString trackTag, const QString &trackName, MixerManager *parent) : QWidget(parent) , m_manager(parent) , m_tid(tid) , m_levelFilter(nullptr) , m_monitorFilter(nullptr) , m_balanceFilter(nullptr) , m_channels(pCore->audioChannels()) , m_balanceSlider(nullptr) , m_maxLevels(qMax(30, int(service->get_fps() * 1.5))) , m_solo(nullptr) , m_record(nullptr) , m_collapse(nullptr) , m_lastVolume(0) , m_listener(nullptr) , m_recording(false) , m_trackTag(std::move(trackTag)) { buildUI(service.get(), trackName); } MixerWidget::MixerWidget(int tid, Mlt::Tractor *service, QString trackTag, const QString &trackName, MixerManager *parent) : QWidget(parent) , m_manager(parent) , m_tid(tid) , m_levelFilter(nullptr) , m_monitorFilter(nullptr) , m_balanceFilter(nullptr) , m_channels(pCore->audioChannels()) , m_balanceSpin(nullptr) , m_balanceSlider(nullptr) , m_maxLevels(qMax(30, int(service->get_fps() * 1.5))) , m_solo(nullptr) , m_record(nullptr) , m_collapse(nullptr) , m_lastVolume(0) , m_listener(nullptr) , m_recording(false) , m_trackTag(std::move(trackTag)) { buildUI(service, trackName); } MixerWidget::~MixerWidget() { if (m_listener) { delete m_listener; } } void MixerWidget::buildUI(Mlt::Tractor *service, const QString &trackName) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); // Build audio meter widget m_audioMeterWidget.reset(new AudioLevelWidget(width(), this)); // initialize for stereo display for (int i = 0; i < m_channels; i++) { m_audioData << -100; } m_audioMeterWidget->setAudioValues(m_audioData); // Build volume widget m_volumeSlider = new QSlider(Qt::Vertical, this); m_volumeSlider->setRange(0, 100); m_volumeSlider->setValue(60); m_volumeSlider->setToolTip(i18n("Volume")); m_volumeSpin = new QDoubleSpinBox(this); m_volumeSpin->setRange(-50, 24); m_volumeSpin->setSuffix(i18n("dB")); m_volumeSpin->setFrame(false); connect(m_volumeSpin, static_cast(&QDoubleSpinBox::valueChanged), this, [&](double val) { m_volumeSlider->setValue(fromDB(val)); }); QLabel *labelLeft = nullptr; QLabel *labelRight = nullptr; if (m_channels == 2) { m_balanceSlider = new QSlider(Qt::Horizontal,this); m_balanceSlider->setRange(-50, 50); m_balanceSlider->setValue(0); m_balanceSlider->setTickPosition(QSlider::TicksBelow); m_balanceSlider->setTickInterval(50); m_balanceSlider->setToolTip(i18n("Balance")); labelLeft = new QLabel(i18nc("Left", "L"), this); labelLeft->setAlignment(Qt::AlignHCenter); labelRight = new QLabel(i18nc("Right", "R"), this); labelRight->setAlignment(Qt::AlignHCenter); m_balanceSpin = new QSpinBox(this); m_balanceSpin->setRange(-50, 50); m_balanceSpin->setValue(0); m_balanceSpin->setFrame(false); m_balanceSpin->setToolTip(i18n("Balance")); } // Check if we already have build-in filters for this tractor int max = service->filter_count(); for (int i = 0; i < max; i++) { std::shared_ptr fl(service->filter(i)); if (!fl->is_valid()) { continue; } const QString filterService = fl->get("mlt_service"); if (filterService == QLatin1String("audiolevel")) { m_monitorFilter = fl; m_monitorFilter->set("disable", 0); } else if (filterService == QLatin1String("volume")) { m_levelFilter = fl; double volume = m_levelFilter->get_double("level"); m_volumeSpin->setValue(volume); m_volumeSlider->setValue(fromDB(volume)); } else if (m_channels == 2 && filterService == QLatin1String("panner")) { m_balanceFilter = fl; int val = int(m_balanceFilter->get_double("start") * 100) - 50; m_balanceSpin->setValue(val); m_balanceSlider->setValue(val); } } // Build default filters if not found if (m_levelFilter == nullptr) { m_levelFilter.reset(new Mlt::Filter(service->get_profile(), "volume")); if (m_levelFilter->is_valid()) { m_levelFilter->set("internal_added", 237); m_levelFilter->set("disable", 1); service->attach(*m_levelFilter.get()); } } if (m_balanceFilter == nullptr && m_channels == 2) { m_balanceFilter.reset(new Mlt::Filter(service->get_profile(), "panner")); if (m_balanceFilter->is_valid()) { m_balanceFilter->set("internal_added", 237); m_balanceFilter->set("start", 0.5); m_balanceFilter->set("disable", 1); service->attach(*m_balanceFilter.get()); } } // Monitoring should be appended last so that other effects are reflected in audio monitor if (m_monitorFilter == nullptr) { m_monitorFilter.reset(new Mlt::Filter(service->get_profile(), "audiolevel")); if (m_monitorFilter->is_valid()) { m_monitorFilter->set("iec_scale", 0); service->attach(*m_monitorFilter.get()); } } m_trackLabel = new KSqueezedTextLabel(this); m_trackLabel->setAutoFillBackground(true); m_trackLabel->setAlignment(Qt::AlignHCenter); m_trackLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken); m_trackLabel->setTextElideMode(Qt::ElideRight); setTrackName(trackName); m_muteAction = new KDualAction(i18n("Mute track"), i18n("Unmute track"), this); m_muteAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-audio"))); m_muteAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-audio"))); if (m_balanceSlider) { connect(m_balanceSlider, &QSlider::valueChanged, m_balanceSpin, &QSpinBox::setValue); } connect(m_muteAction, &KDualAction::activeChangedByUser, this, [&](bool active) { if (m_tid == -1) { // Muting master, special case if (m_levelFilter) { if (active) { m_lastVolume = m_levelFilter->get_double("level"); m_levelFilter->set("level", -1000); m_levelFilter->set("disable", 0); } else { m_levelFilter->set("level", m_lastVolume); m_levelFilter->set("disable", qFuzzyIsNull(m_lastVolume) ? 1 : 0); } } } else { emit muteTrack(m_tid, !active); reset(); } pCore->setDocumentModified(); updateLabel(); }); auto *mute = new QToolButton(this); mute->setDefaultAction(m_muteAction); mute->setAutoRaise(true); // Setup default width setFixedWidth(3 * mute->sizeHint().width()); if (m_tid > -1) { // No solo / rec button on master m_solo = new QToolButton(this); m_solo->setCheckable(true); m_solo->setIcon(QIcon::fromTheme("headphones")); m_solo->setToolTip(i18n("Solo mode")); m_solo->setAutoRaise(true); connect(m_solo, &QToolButton::toggled, this, [&](bool toggled) { emit toggleSolo(m_tid, toggled); updateLabel(); }); m_record = new QToolButton(this); m_record->setIcon(QIcon::fromTheme("media-record")); m_record->setToolTip(i18n("Record")); m_record->setCheckable(true); m_record->setAutoRaise(true); connect(m_record, &QToolButton::clicked, this, [&]() { emit m_manager->recordAudio(m_tid); }); } else { m_collapse = new QToolButton(this); m_collapse->setIcon(KdenliveSettings::mixerCollapse() ? QIcon::fromTheme("arrow-left") : QIcon::fromTheme("arrow-right")); m_collapse->setToolTip(i18n("Show Channels")); m_collapse->setCheckable(true); m_collapse->setAutoRaise(true); m_collapse->setChecked(KdenliveSettings::mixerCollapse() ); connect(m_collapse, &QToolButton::clicked, this, [&]() { KdenliveSettings::setMixerCollapse(m_collapse->isChecked()); m_collapse->setIcon(m_collapse->isChecked() ? QIcon::fromTheme("arrow-left") : QIcon::fromTheme("arrow-right")); m_manager->collapseMixers(); }); } auto *showEffects = new QToolButton(this); showEffects->setIcon(QIcon::fromTheme("autocorrection")); showEffects->setToolTip(i18n("Open Effect Stack")); showEffects->setAutoRaise(true); connect(showEffects, &QToolButton::clicked, this, [&]() { emit m_manager->showEffectStack(m_tid); }); connect(m_volumeSlider, &QSlider::valueChanged, this, [&](int value) { QSignalBlocker bk(m_volumeSpin); if (m_recording) { m_volumeSpin->setValue(value); KdenliveSettings::setAudiocapturevolume(value); emit m_manager->updateRecVolume(); //TODO update capture volume } else if (m_levelFilter != nullptr) { double dbValue = 0; if (value > 60) { // increase volume dbValue = 24 * (1 - log10((100 - value)*0.225 + 1)); } else if (value < 60) { dbValue = -50 * (1 - log10(10 - (value - 59)*(-0.11395))); } m_volumeSpin->setValue(dbValue); m_levelFilter->set("level", dbValue); m_levelFilter->set("disable", value == 60 ? 1 : 0); m_levels.clear(); emit m_manager->purgeCache(); pCore->setDocumentModified(); } }); if (m_balanceSlider) { connect(m_balanceSpin, static_cast(&QSpinBox::valueChanged), this, [&](int value) { QSignalBlocker bk(m_balanceSlider); m_balanceSlider->setValue(value); if (m_balanceFilter != nullptr) { m_balanceFilter->set("start", (value + 50) / 100.); m_balanceFilter->set("disable", value == 0 ? 1 : 0); m_levels.clear(); emit m_manager->purgeCache(); pCore->setDocumentModified(); } }); } auto *lay = new QVBoxLayout; setContentsMargins(0, 0, 0, 0); lay->setContentsMargins(0, 0, 0, 0); lay->addWidget(m_trackLabel); auto *buttonslay = new QHBoxLayout; buttonslay->setSpacing(0); buttonslay->setContentsMargins(0, 0, 0, 0); if (m_collapse) { buttonslay->addWidget(m_collapse); } buttonslay->addWidget(mute); if (m_solo) { buttonslay->addWidget(m_solo); } if (m_record) { buttonslay->addWidget(m_record); } buttonslay->addWidget(showEffects); lay->addLayout(buttonslay); if (m_balanceSlider) { auto *balancelay = new QGridLayout; balancelay->addWidget(m_balanceSlider, 0, 0, 1, 3); balancelay->addWidget(labelLeft, 1, 0, 1, 1); balancelay->addWidget(m_balanceSpin, 1, 1, 1, 1); balancelay->addWidget(labelRight, 1, 2, 1, 1); lay->addLayout(balancelay); } auto *hlay = new QHBoxLayout; hlay->addWidget(m_audioMeterWidget.get()); hlay->addWidget(m_volumeSlider); lay->addLayout(hlay); lay->addWidget(m_volumeSpin); lay->setStretch(4, 10); setLayout(lay); if (service->get_int("hide") > 1) { setMute(true); } } void MixerWidget::mousePressEvent(QMouseEvent *event) { if(event->button() == Qt::RightButton) { QWidget *child = childAt(event->pos()); if (child == m_balanceSlider) { m_balanceSpin->setValue(0); } else if (child == m_volumeSlider) { m_volumeSlider->setValue(60); } } else { QWidget::mousePressEvent(event); } } void MixerWidget::setTrackName(const QString &name) { // Show only tag on master or if name is empty if (name.isEmpty() || m_tid == -1) { m_trackLabel->setText(m_trackTag); } else { m_trackLabel->setText(QString("%1 - %2").arg(m_trackTag).arg(name)); } } void MixerWidget::setMute(bool mute) { m_muteAction->setActive(mute); m_volumeSlider->setEnabled(!mute); m_volumeSpin->setEnabled(!mute); m_audioMeterWidget->setEnabled(!mute); if (m_balanceSlider) { m_balanceSpin->setEnabled(!mute); m_balanceSlider->setEnabled(!mute); } updateLabel(); } void MixerWidget::updateLabel() { if (m_recording) { QPalette pal = m_trackLabel->palette(); pal.setColor(QPalette::Window, Qt::red); m_trackLabel->setPalette(pal); } else if (m_muteAction->isActive()) { QPalette pal = m_trackLabel->palette(); pal.setColor(QPalette::Window, QColor(0xff8c00)); m_trackLabel->setPalette(pal); } else if (m_solo && m_solo->isChecked()) { QPalette pal = m_trackLabel->palette(); pal.setColor(QPalette::Window, Qt::darkGreen); m_trackLabel->setPalette(pal); } else { QPalette pal = palette(); m_trackLabel->setPalette(pal); } } void MixerWidget::updateAudioLevel(int pos) { QMutexLocker lk(&m_storeMutex); if (m_levels.contains(pos)) { m_audioMeterWidget->setAudioValues(m_levels.value(pos)); //m_levels.remove(pos); } else { m_audioMeterWidget->setAudioValues(m_audioData); } } void MixerWidget::reset() { QMutexLocker lk(&m_storeMutex); m_levels.clear(); m_audioMeterWidget->setAudioValues(m_audioData); } void MixerWidget::clear() { QMutexLocker lk(&m_storeMutex); m_levels.clear(); } bool MixerWidget::isMute() const { return m_muteAction->isActive(); } void MixerWidget::unSolo() { if (m_solo) { QSignalBlocker bl(m_solo); m_solo->setChecked(false); } } void MixerWidget::gotRecLevels(QVectorlevels) { switch (levels.size()) { case 0: m_audioMeterWidget->setAudioValues({-100, -100}); break; case 1: m_audioMeterWidget->setAudioValues({IEC_Scale(levels[0]), -100}); break; default: m_audioMeterWidget->setAudioValues({IEC_Scale(levels[0]), IEC_Scale(levels[1])}); break; } } void MixerWidget::setRecordState(bool recording) { m_recording = recording; m_record->setChecked(m_recording); QSignalBlocker bk(m_volumeSpin); QSignalBlocker bk2(m_volumeSlider); if (m_recording) { connect(pCore->getAudioDevice(), &MediaCapture::audioLevels, this, &MixerWidget::gotRecLevels); if (m_balanceSlider) { m_balanceSlider->setEnabled(false); m_balanceSpin->setEnabled(false); } m_volumeSpin->setRange(0, 100); m_volumeSpin->setSuffix(QStringLiteral("%")); m_volumeSpin->setValue(KdenliveSettings::audiocapturevolume()); m_volumeSlider->setValue(KdenliveSettings::audiocapturevolume()); } else { if (m_balanceSlider) { m_balanceSlider->setEnabled(true); m_balanceSpin->setEnabled(true); } int level = m_levelFilter->get_int("level"); disconnect(pCore->getAudioDevice(), &MediaCapture::audioLevels, this, &MixerWidget::gotRecLevels); m_volumeSpin->setRange(-100, 60); m_volumeSpin->setSuffix(i18n("dB")); m_volumeSpin->setValue(level); m_volumeSlider->setValue(fromDB(level)); } updateLabel(); } void MixerWidget::connectMixer(bool doConnect) { if (doConnect) { if (m_listener == nullptr) { m_listener = m_monitorFilter->listen("property-changed", this, reinterpret_cast(property_changed)); } } else { delete m_listener; m_listener = nullptr; } } void MixerWidget::pauseMonitoring(bool pause) { if (m_monitorFilter) { m_monitorFilter->set("disable", pause ? 1 : 0); } }