WIP: improved multistream audio workflow

- Allow selecting multiple streams
- Allow renaming streams from clip properties panel
parent 425cce14
Pipeline #19962 passed with stage
in 9 minutes and 30 seconds
......@@ -2373,7 +2373,7 @@ void Bin::showClipProperties(const std::shared_ptr<ProjectClip> &clip, bool forc
}
m_propertiesPanel->setProperty("clipId", clip->AbstractProjectItem::clipId());
// Setup timeline targets
emit setupTargets(clip->hasVideo(), clip->audioStreams());
emit setupTargets(clip->hasVideo(), clip->activeStreams());
auto *lay = static_cast<QVBoxLayout *>(m_propertiesPanel->layout());
if (lay == nullptr) {
lay = new QVBoxLayout(m_propertiesPanel);
......@@ -2420,6 +2420,17 @@ void Bin::reloadMonitorStreamIfActive(const QString &id)
}
}
void Bin::updateTargets(const QString &id)
{
if (m_monitor->activeClipId() != id) {
return;
}
std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
if (clip) {
emit setupTargets(clip->hasVideo(), clip->activeStreams());
}
}
QStringList Bin::getBinFolderClipIds(const QString &id) const
{
QStringList ids;
......
......@@ -221,6 +221,8 @@ public:
void reloadMonitorIfActive(const QString &id);
/** @brief refresh monitor stream selector */
void reloadMonitorStreamIfActive(const QString &id);
/** @brief Update timeline targets according to selected audio streams */
void updateTargets(const QString &id);
void doMoveClip(const QString &id, const QString &newParentId);
void doMoveFolder(const QString &id, const QString &newParentId);
......
......@@ -983,7 +983,7 @@ void ProjectClip::setProperties(const QMap<QString, QString> &properties, bool r
QStringLiteral("force_colorspace"), QStringLiteral("force_tff"), QStringLiteral("force_progressive"), QStringLiteral("video_delay")
};
QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"), QStringLiteral("resource"),
QStringLiteral("force_fps"), QStringLiteral("set.test_image"), QStringLiteral("set.test_audio"),
QStringLiteral("force_fps"), QStringLiteral("set.test_image"),
QStringLiteral("video_index")};
QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"), QStringLiteral("ttl"),
QStringLiteral("softness"), QStringLiteral("crop"), QStringLiteral("animation")};
......@@ -1102,6 +1102,15 @@ void ProjectClip::setProperties(const QMap<QString, QString> &properties, bool r
updateTimelineClips(updateRoles);
}
} else {
if (properties.contains(QStringLiteral("kdenlive:active_streams")) && m_audioInfo) {
// Clip is a multi audio stream and currently in clip monitor, update target tracks
m_audioInfo->updateActiveStreams(properties.value(QStringLiteral("kdenlive:active_streams")));
pCore->bin()->updateTargets(clipId());
if (!audioStreamChanged) {
pCore->bin()->reloadMonitorStreamIfActive(clipId());
refreshPanel = true;
}
}
if (audioStreamChanged) {
refreshAudioInfo();
audioThumbReady();
......@@ -1564,3 +1573,16 @@ void ProjectClip::setClipStatus(AbstractProjectItem::CLIPSTATUS status)
AbstractProjectItem::IconOverlay);
}
}
void ProjectClip::renameAudioStream(int id, QString name)
{
if (m_audioInfo) {
m_audioInfo->renameStream(id, name);
QString prop = QString("kdenlive:streamname.%1").arg(id);
m_masterProducer->set(prop.toUtf8().constData(), name.toUtf8().constData());
if (m_audioInfo->activeStreams().keys().contains(id)) {
pCore->bin()->updateTargets(clipId());
}
pCore->bin()->reloadMonitorStreamIfActive(clipId());
}
}
......@@ -230,6 +230,9 @@ public:
*/
int getAudioStreamFfmpegIndex(int mltStream);
void setClipStatus(AbstractProjectItem::CLIPSTATUS status) override;
/** @brief Rename an audio stream for this clip
*/
void renameAudioStream(int id, QString name) override;
protected:
friend class ClipModel;
......
......@@ -33,35 +33,45 @@ AudioStreamInfo::AudioStreamInfo(const std::shared_ptr<Mlt::Producer> &producer,
memset(property, 0, 200);
snprintf(property, sizeof(property), "meta.media.%d.codec.channels", ix);
int chan = producer->get_int(property);
QString channelDescription = QString("%1|").arg(streamIndex++);
switch (chan) {
case 1:
channelDescription.append(i18n("Mono "));
break;
case 2:
channelDescription.append(i18n("Stereo "));
break;
default:
channelDescription.append(i18n("%1 channels ", chan));
break;
}
// Frequency
memset(property, 0, 200);
snprintf(property, sizeof(property), "meta.media.%d.codec.sample_rate", ix);
QString frequency(producer->get(property));
if (frequency.endsWith(QLatin1String("000"))) {
frequency.chop(3);
frequency.append(i18n("kHz "));
snprintf(property, sizeof(property), "kdenlive:streamname.%d", ix);
QString channelDescription = producer->get(property);
if (channelDescription.isEmpty()) {
channelDescription = QString("%1|").arg(streamIndex++);
switch (chan) {
case 1:
channelDescription.append(i18n("Mono "));
break;
case 2:
channelDescription.append(i18n("Stereo "));
break;
default:
channelDescription.append(i18n("%1 channels ", chan));
break;
}
// Frequency
memset(property, 0, 200);
snprintf(property, sizeof(property), "meta.media.%d.codec.sample_rate", ix);
QString frequency(producer->get(property));
if (frequency.endsWith(QLatin1String("000"))) {
frequency.chop(3);
frequency.append(i18n("kHz "));
} else {
frequency.append(i18n("Hz "));
}
channelDescription.append(frequency);
memset(property, 0, 200);
snprintf(property, sizeof(property), "meta.media.%d.codec.name", ix);
channelDescription.append(producer->get(property));
} else {
frequency.append(i18n("Hz "));
streamIndex++;
}
channelDescription.append(frequency);
memset(property, 0, 200);
snprintf(property, sizeof(property), "meta.media.%d.codec.name", ix);
channelDescription.append(producer->get(property));
m_audioStreams.insert(ix, channelDescription);
}
}
QString active = producer->get("kdenlive:active_streams");
updateActiveStreams(active);
if (audioStreamIndex > -1) {
QByteArray key;
key = QStringLiteral("meta.media.%1.codec.sample_fmt").arg(audioStreamIndex).toLocal8Bit();
......@@ -75,7 +85,6 @@ AudioStreamInfo::AudioStreamInfo(const std::shared_ptr<Mlt::Producer> &producer,
key = QStringLiteral("meta.media.%1.codec.channels").arg(audioStreamIndex).toLocal8Bit();
m_channels = producer->get_int(key.data());
setAudioIndex(producer, m_audioStreamIndex);
}
}
......@@ -97,6 +106,19 @@ QMap <int, QString> AudioStreamInfo::streams() const
return m_audioStreams;
}
QMap <int, QString> AudioStreamInfo::activeStreams() const
{
QMap <int, QString> active;
QMapIterator<int, QString> i(m_audioStreams);
while (i.hasNext()) {
i.next();
if (m_activeStreams.contains(i.key())) {
active.insert(i.key(), i.value());
}
}
return active;
}
int AudioStreamInfo::bitrate() const
{
return m_bitRate;
......@@ -137,3 +159,25 @@ void AudioStreamInfo::setAudioIndex(const std::shared_ptr<Mlt::Producer> &produc
}
}
void AudioStreamInfo::updateActiveStreams(const QString &activeStreams)
{
// -1 = disable all audio
// empty = enable all audio
m_activeStreams.clear();
if (activeStreams.isEmpty()) {
m_activeStreams = m_audioStreams.keys();
return;
}
QStringList st = activeStreams.split(QLatin1Char(';'));
for (const QString &s : st) {
m_activeStreams << s.toInt();
}
}
void AudioStreamInfo::renameStream(int ix, const QString streamName)
{
if (m_audioStreams.contains(ix)) {
m_audioStreams.insert(ix, streamName);
}
}
......@@ -29,6 +29,7 @@ public:
int samplingRate() const;
int channels() const;
QMap <int, QString> streams() const;
QMap <int, QString> activeStreams() const;
int bitrate() const;
const QString &samplingFormat() const;
int audio_index() const;
......@@ -36,10 +37,13 @@ public:
void dumpInfo() const;
void setAudioIndex(const std::shared_ptr<Mlt::Producer> &producer, int ix);
QMap<int, QString> streamInfo(Mlt::Properties sourceProperties);
void updateActiveStreams(const QString &activeStreams);
void renameStream(int ix, const QString streamName);
private:
int m_audioStreamIndex;
QMap <int, QString> m_audioStreams;
QList <int> m_activeStreams;
int m_ffmpegAudioIndex;
int m_samplingRate;
int m_channels;
......
......@@ -1047,6 +1047,14 @@ QMap <int, QString> ClipController::audioStreams() const
return {};
}
QMap <int, QString> ClipController::activeStreams() const
{
if (m_audioInfo) {
return m_audioInfo->activeStreams();
}
return {};
}
int ClipController::audioStreamsCount() const
{
if (m_audioInfo) {
......@@ -1054,3 +1062,4 @@ int ClipController::audioStreamsCount() const
}
return 0;
}
......@@ -97,6 +97,9 @@ public:
/** @brief Returns this clip's producer. */
virtual std::shared_ptr<Mlt::Producer> thumbProducer() = 0;
/** @brief Rename an audio stream. */
virtual void renameAudioStream(int id, QString name) = 0;
/** @brief Returns the clip's duration */
GenTime getPlaytime() const;
......@@ -207,8 +210,10 @@ public:
/** @brief Append an effect to this producer's effect list */
bool addEffect(const QString &effectId);
/** @brief Returns the list of audio streams indexes for this clip */
/** @brief Returns the list of all audio streams indexes for this clip */
QMap <int, QString> audioStreams() const;
/** @brief Returns the list of active audio streams indexes for this clip */
QMap <int, QString> activeStreams() const;
/** @brief Returns the count of audio streams for this clip */
int audioStreamsCount() const;
......
......@@ -35,6 +35,7 @@ class QMimeData;
class QTextEdit;
class QLabel;
class QComboBox;
class QListWidget;
class AnalysisTree : public QTreeWidget
{
......@@ -114,6 +115,7 @@ private:
QTreeView *m_markerTree;
AnalysisTree *m_analysisTree;
QTextEdit *m_textEdit;
QListWidget *m_audioStreamsView;
void fillProperties();
signals:
......
......@@ -141,7 +141,6 @@ Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *paren
, m_contextMenu(nullptr)
, m_markerMenu(nullptr)
, m_audioChannels(nullptr)
, m_audioChannelsGroup(nullptr)
, m_loopClipTransition(true)
, m_editMarker(nullptr)
, m_forceSizeFactor(0)
......@@ -277,10 +276,48 @@ Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *paren
m_toolbar->addAction(manager->getAction(QStringLiteral("insert_project_tree")));
m_toolbar->setToolTip(i18n("Insert Zone to Project Bin"));
m_toolbar->addSeparator();
m_streamsButton = new QToolButton(this);
m_streamsButton->setPopupMode(QToolButton::InstantPopup);
m_streamsButton->setIcon(QIcon::fromTheme(QStringLiteral("speaker")));
m_streamAction = m_toolbar->addWidget(m_streamsButton);
m_audioChannels = new QMenu(this);
m_audioChannels->setIcon(QIcon::fromTheme(QStringLiteral("speaker")));
m_toolbar->addAction(m_audioChannels->menuAction());
m_audioChannels->menuAction()->setVisible(false);
m_streamsButton->setMenu(m_audioChannels);
m_streamAction->setVisible(false);
connect(m_audioChannels, &QMenu::triggered, [this] () {
m_audioChannels->show();
QList <QAction*> actions = m_audioChannels->actions();
QMap <int, QString> enabledStreams;
for (const auto act : actions) {
if (act->isChecked()) {
// Audio stream is selected
enabledStreams.insert(act->data().toInt(), act->text().remove(QLatin1Char('&')));
}
}
if (!enabledStreams.isEmpty()) {
// Only 1 stream wanted, easy
m_glMonitor->getControllerProxy()->setAudioStream(enabledStreams.first());
QMap <QString, QString> props;
props.insert(QStringLiteral("audio_index"), QString::number(enabledStreams.firstKey()));
if (enabledStreams.count() > 1) {
// Mix audio channels
}
QList <int> streams = enabledStreams.keys();
QStringList astreams;
for (const int st : streams) {
astreams << QString::number(st);
}
props.insert(QStringLiteral("kdenlive:active_streams"), astreams.join(QLatin1Char(';')));
m_controller->setProperties(props, true);
} else {
// No active stream
m_glMonitor->getControllerProxy()->setAudioStream(QString());
QMap <QString, QString> props;
props.insert(QStringLiteral("audio_index"), QStringLiteral("-1"));
props.insert(QStringLiteral("kdenlive:active_streams"), QStringLiteral("-1"));
m_controller->setProperties(props, true);
}
});
} else if (id == Kdenlive::ProjectMonitor) {
connect(m_glMonitor, &GLWidget::paused, m_monitorManager, &MonitorManager::cleanMixer);
}
......@@ -1420,47 +1457,36 @@ void Monitor::slotOpenClip(const std::shared_ptr<ProjectClip> &controller, int i
m_glMonitor->getControllerProxy()->resetZone();
if (controller) {
m_audioChannels->clear();
delete m_audioChannelsGroup;
m_audioChannelsGroup = nullptr;
if (m_controller->audioInfo()) {
QMap<int, QString> audioStreamsInfo = m_controller->audioStreams();
if (audioStreamsInfo.size() > 1) {
// Multi stream clip
m_audioChannelsGroup = new QActionGroup(this);
// Multi stream clip
QMapIterator<int, QString> i(audioStreamsInfo);
int activeStream = m_controller->getProducerIntProperty(QLatin1String("audio_index"));
QList <int> activeStreams = m_controller->activeStreams().keys();
QAction *ac;
while (i.hasNext()) {
i.next();
ac = m_audioChannels->addAction(i.value());
ac->setData(i.key());
ac->setCheckable(true);
if (i.key() == activeStream) {
if (activeStreams.contains(i.key())) {
ac->setChecked(true);
m_glMonitor->getControllerProxy()->setAudioStream(ac->text().remove(QLatin1Char('&')));
}
m_audioChannelsGroup->addAction(ac);
}
ac = m_audioChannels->addAction(i18n("Merge all streams"));
ac->setData(INT_MAX);
ac->setCheckable(true);
if (activeStream == INT_MAX) {
if (activeStreams.contains(INT_MAX)) {
ac->setChecked(true);
}
m_audioChannelsGroup->addAction(ac);
connect(m_audioChannelsGroup, &QActionGroup::triggered, [this] (QAction *act) {
int selectedStream = act->data().toInt();
m_glMonitor->getControllerProxy()->setAudioStream(act->text().remove(QLatin1Char('&')));
QMap <QString, QString> props;
props.insert(QStringLiteral("audio_index"), QString::number(selectedStream));
m_controller->setProperties(props, true);
});
m_audioChannels->menuAction()->setVisible(true);
m_streamAction->setVisible(true);
} else {
m_audioChannels->menuAction()->setVisible(false);
m_streamAction->setVisible(false);
}
} else {
m_audioChannels->menuAction()->setVisible(false);
m_streamAction->setVisible(false);
//m_audioChannels->menuAction()->setVisible(false);
}
connect(m_controller.get(), &ProjectClip::audioThumbReady, this, &Monitor::prepareAudioThumb);
connect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector<int> &)), this,
......@@ -1506,7 +1532,8 @@ void Monitor::slotOpenClip(const std::shared_ptr<ProjectClip> &controller, int i
m_glMonitor->getControllerProxy()->setAudioThumb();
m_audioMeterWidget->audioChannels = 0;
m_glMonitor->getControllerProxy()->setClipProperties(-1, ClipType::Unknown, false, QString());
m_audioChannels->menuAction()->setVisible(false);
//m_audioChannels->menuAction()->setVisible(false);
m_streamAction->setVisible(false);
}
if (slotActivateMonitor()) {
start();
......@@ -1517,14 +1544,26 @@ void Monitor::slotOpenClip(const std::shared_ptr<ProjectClip> &controller, int i
void Monitor::reloadActiveStream()
{
if (m_controller) {
int activeStream = m_controller->getProducerIntProperty(QLatin1String("audio_index"));
QList <QAction*> actions = m_audioChannels->actions();
QSignalBlocker bk(m_audioChannelsGroup);
for (QAction *ac : actions) {
if (ac->data().toInt() == activeStream) {
QList <QAction *> acts = m_audioChannels->actions();
QSignalBlocker bk(m_audioChannels);
QList <int> activeStreams = m_controller->activeStreams().keys();
QMap <int, QString> streams = m_controller->audioStreams();
qDebug()<<"==== REFRESHING MONITOR STREAMS: "<<activeStreams;
bool displayedStream = false;
for (auto ac : acts) {
int val = ac->data().toInt();
if (streams.contains(val)) {
// Update stream name in case of renaming
ac->setText(streams.value(val));
}
if (activeStreams.contains(val)) {
ac->setChecked(true);
m_glMonitor->getControllerProxy()->setAudioStream(ac->text().remove(QLatin1Char('&')));
break;
if (!displayedStream) {
m_glMonitor->getControllerProxy()->setAudioStream(ac->text().remove(QLatin1Char('&')));
displayedStream = true;
}
} else {
ac->setChecked(false);
}
}
}
......
......@@ -37,6 +37,7 @@ class SnapModel;
class ProjectClip;
class MonitorManager;
class QSlider;
class QToolButton;
class KDualAction;
class KSelectAction;
class KMessageWidget;
......@@ -210,7 +211,8 @@ private:
QMenu *m_playMenu;
QMenu *m_markerMenu;
QMenu *m_audioChannels;
QActionGroup *m_audioChannelsGroup;
QToolButton *m_streamsButton;
QAction *m_streamAction;
QPoint m_DragStartPosition;
/** true if selected clip is transition, false = selected clip is clip.
* Necessary because sometimes we get two signals, e.g. we get a clip and we get selected transition = nullptr. */
......
......@@ -147,12 +147,8 @@ void TimelineController::setTargetTracks(bool hasVideo, QMap <int, QString> audi
}
emit hasAudioTargetChanged();
emit hasVideoTargetChanged();
//if (m_videoTargetActive) {
setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack);
//}
//if (m_audioTargetActive) {
setAudioTarget((m_hasAudioTarget > 0 && (m_lastAudioTarget.size() == audioTargets.size())) ? m_lastAudioTarget : audioTracks);
//}
setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack);
setAudioTarget(audioTracks);
}
std::shared_ptr<TimelineItemModel> TimelineController::getModel() const
......@@ -2438,7 +2434,11 @@ const QString TimelineController::audioTargetName(int tid) const
if (m_binAudioTargets.contains(streamIndex)) {
QString targetName = m_binAudioTargets.value(streamIndex);
return targetName.isEmpty() ? QChar('x') : targetName.at(0);
} else {
qDebug()<<"STREAM INDEX NOT IN TARGET : "<<streamIndex<<" = "<<m_binAudioTargets;
}
} else {
qDebug()<<"TRACK NOT IN TARGET : "<<tid<<" = "<<m_model->m_audioTarget.keys();
}
return QString();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment