Skip to content

Commit fa1ad10

Browse files
committed
Support inserting audio clips
1 parent bc7c43f commit fa1ad10

6 files changed

Lines changed: 432 additions & 0 deletions

File tree

src/plugins/audio/internal/AudioPlugin.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <audio/internal/AudioAndMidiPage.h>
2222
#include <audio/internal/AudioOutputPage.h>
2323
#include <audio/internal/AudioPreference.h>
24+
#include <audio/internal/InsertAudioClipAddOn.h>
2425
#include <audio/internal/ProjectAudioAddOn.h>
2526
#include <audio/internal/PlaybackAddOn.h>
2627
#include <audio/internal/PlaybackPage.h>
@@ -98,6 +99,7 @@ namespace Audio::Internal {
9899

99100
void AudioPlugin::initializeWindows() {
100101
Core::ProjectWindowInterfaceRegistry::instance()->attach<ProjectAudioAddOn>();
102+
Core::ProjectWindowInterfaceRegistry::instance()->attach<InsertAudioClipAddOn>();
101103
Core::ProjectWindowInterfaceRegistry::instance()->attach<PlaybackAddOn>();
102104
}
103105
}
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
#include "InsertAudioClipAddOn.h"
2+
3+
#include <QDir>
4+
#include <QEventLoop>
5+
#include <QFileInfo>
6+
#include <QLoggingCategory>
7+
#include <QQmlComponent>
8+
#include <QQuickItem>
9+
#include <QQuickWindow>
10+
#include <QSettings>
11+
#include <QStandardPaths>
12+
#include <QVariant>
13+
14+
#include <CoreApi/filelocker.h>
15+
#include <CoreApi/runtimeinterface.h>
16+
17+
#include <QAKQuick/quickactioncontext.h>
18+
19+
#include <SVSCraftCore/MusicTimeline.h>
20+
#include <SVSCraftCore/MusicTime.h>
21+
#include <SVSCraftQuick/MessageBox.h>
22+
23+
#include <TalcsFormat/AbstractAudioFormatIO.h>
24+
#include <TalcsWidgets/AudioFileDialog.h>
25+
26+
#include <dspxmodel/AudioClip.h>
27+
#include <dspxmodel/BusControl.h>
28+
#include <dspxmodel/Clip.h>
29+
#include <dspxmodel/ClipSequence.h>
30+
#include <dspxmodel/ClipTime.h>
31+
#include <dspxmodel/Model.h>
32+
#include <dspxmodel/SelectionModel.h>
33+
#include <dspxmodel/Track.h>
34+
#include <dspxmodel/TrackList.h>
35+
#include <dspxmodel/TrackSelectionModel.h>
36+
37+
#include <coreplugin/DspxDocument.h>
38+
#include <coreplugin/ProjectDocumentContext.h>
39+
#include <coreplugin/ProjectTimeline.h>
40+
#include <coreplugin/ProjectWindowInterface.h>
41+
42+
#include <transactional/TransactionController.h>
43+
44+
#include <audio/GlobalAudioContext.h>
45+
#include <audio/internal/HashHelper.h>
46+
#include <audio/internal/ProjectAudioAddOn.h>
47+
48+
namespace Audio::Internal {
49+
50+
Q_STATIC_LOGGING_CATEGORY(lcInsertAudioClipAddOn, "diffscope.audio.insertaudioclipaddon")
51+
52+
static bool execDialog(QObject *dialog) {
53+
QEventLoop eventLoop;
54+
QObject::connect(dialog, SIGNAL(accepted()), &eventLoop, SLOT(quit()));
55+
QObject::connect(dialog, SIGNAL(rejected()), &eventLoop, SLOT(quit()));
56+
QMetaObject::invokeMethod(dialog, "open");
57+
eventLoop.exec();
58+
return dialog->property("result").toInt() == 1;
59+
}
60+
61+
static QObject *createAndPositionDialog(QQuickWindow *window, QQmlComponent *component, const QVariantMap &initialProperties) {
62+
if (component->isError()) {
63+
qFatal() << component->errorString();
64+
}
65+
QVariantMap properties = initialProperties;
66+
properties.insert("parent", QVariant::fromValue(window->contentItem()));
67+
auto dialog = component->createWithInitialProperties(properties);
68+
if (!dialog) {
69+
qFatal() << component->errorString();
70+
}
71+
auto width = dialog->property("width").toDouble();
72+
auto height = dialog->property("height").toDouble();
73+
dialog->setProperty("x", window->width() / 2.0 - width / 2);
74+
if (auto popupTopMarginHint = window->property("popupTopMarginHint"); popupTopMarginHint.isValid()) {
75+
dialog->setProperty("y", popupTopMarginHint);
76+
} else {
77+
dialog->setProperty("y", window->height() / 2.0 - height / 2);
78+
}
79+
return dialog;
80+
}
81+
82+
static dspx::Track *currentTrack(Core::DspxDocument *document) {
83+
auto selectionModel = document ? document->selectionModel() : nullptr;
84+
auto trackSelectionModel = selectionModel ? selectionModel->trackSelectionModel() : nullptr;
85+
auto track = trackSelectionModel ? trackSelectionModel->currentItem() : nullptr;
86+
if (!track) {
87+
if (auto currentClip = qobject_cast<dspx::Clip *>(selectionModel ? selectionModel->currentItem() : nullptr)) {
88+
if (auto clipSequence = currentClip->clipSequence()) {
89+
track = clipSequence->track();
90+
}
91+
}
92+
}
93+
return track;
94+
}
95+
96+
static bool isInDirectoryOrSubdirectory(const QString &filePath, const QDir &directory) {
97+
const auto relativePath = directory.relativeFilePath(QFileInfo(filePath).absoluteFilePath());
98+
return relativePath == "." || (!relativePath.startsWith("..") && !QDir::isAbsolutePath(relativePath));
99+
}
100+
101+
InsertAudioClipAddOn::InsertAudioClipAddOn(QObject *parent) : WindowInterfaceAddOn(parent) {
102+
}
103+
104+
InsertAudioClipAddOn::~InsertAudioClipAddOn() = default;
105+
106+
void InsertAudioClipAddOn::initialize() {
107+
auto windowInterface = windowHandle()->cast<Core::ProjectWindowInterface>();
108+
windowInterface->addObject(this);
109+
110+
QQmlComponent component(Core::RuntimeInterface::qmlEngine(), "DiffScope.Audio", "InsertAudioClipAddOnActions");
111+
if (component.isError()) {
112+
qFatal() << component.errorString();
113+
}
114+
auto o = component.createWithInitialProperties({
115+
{"addOn", QVariant::fromValue(this)},
116+
});
117+
o->setParent(this);
118+
QMetaObject::invokeMethod(o, "registerToContext", windowInterface->actionContext());
119+
}
120+
121+
void InsertAudioClipAddOn::extensionsInitialized() {
122+
}
123+
124+
bool InsertAudioClipAddOn::delayedInitialize() {
125+
return WindowInterfaceAddOn::delayedInitialize();
126+
}
127+
128+
InsertAudioClipAddOn *InsertAudioClipAddOn::of(Core::ProjectWindowInterface *windowHandle) {
129+
return windowHandle->getFirstObject<InsertAudioClipAddOn>();
130+
}
131+
132+
void InsertAudioClipAddOn::insertAudioClip() {
133+
auto windowInterface = windowHandle()->cast<Core::ProjectWindowInterface>();
134+
if (!windowInterface || !windowInterface->window() || !windowInterface->projectTimeline())
135+
return;
136+
137+
auto document = windowInterface->projectDocumentContext()->document();
138+
if (!document)
139+
return;
140+
141+
auto model = document->model();
142+
auto trackList = model->tracks();
143+
if (!trackList || trackList->size() == 0)
144+
return;
145+
146+
qCInfo(lcInsertAudioClipAddOn) << "Opening audio file for insertion";
147+
QString fileName;
148+
QVariant userData;
149+
QString entryClassName;
150+
auto settings = Core::RuntimeInterface::settings();
151+
settings->beginGroup(staticMetaObject.className());
152+
const auto defaultDir = settings->value(QStringLiteral("defaultDir"), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).toString();
153+
settings->endGroup();
154+
auto io = talcs::AudioFileDialog::getOpenAudioFileIO(
155+
GlobalAudioContext::formatManager(),
156+
fileName,
157+
userData,
158+
entryClassName,
159+
windowInterface->invisibleCentralWidget(),
160+
tr("Open Audio File"),
161+
defaultDir
162+
);
163+
qCDebug(lcInsertAudioClipAddOn) << "Audio file dialog returned" << io << fileName << userData << entryClassName;
164+
if (!io) {
165+
if (fileName.isEmpty()) {
166+
return;
167+
}
168+
qCWarning(lcInsertAudioClipAddOn) << "Failed to open audio file" << fileName << userData << entryClassName;
169+
SVS::MessageBox::critical(
170+
Core::RuntimeInterface::qmlEngine(),
171+
windowInterface->window(),
172+
tr("Failed to open audio file"),
173+
tr("Unable to open \"%1\" as an audio file.").arg(QDir::toNativeSeparators(fileName))
174+
);
175+
return;
176+
}
177+
178+
const QFileInfo fileInfo(fileName);
179+
settings->beginGroup(staticMetaObject.className());
180+
settings->setValue(QStringLiteral("defaultDir"), fileInfo.absolutePath());
181+
settings->endGroup();
182+
183+
auto selectedTrack = currentTrack(document);
184+
if (!selectedTrack) {
185+
selectedTrack = trackList->items().first();
186+
}
187+
188+
QQmlComponent component(Core::RuntimeInterface::qmlEngine(), "DiffScope.Audio", "InsertAudioClipDialog");
189+
QVariantMap properties;
190+
properties.insert("trackList", QVariant::fromValue(trackList));
191+
properties.insert("selectedTrack", QVariant::fromValue(selectedTrack));
192+
properties.insert("timeline", QVariant::fromValue(windowInterface->projectTimeline()->musicTimeline()));
193+
properties.insert("clipPosition", windowInterface->projectTimeline()->position());
194+
properties.insert("clipName", fileInfo.completeBaseName());
195+
auto quickWindow = qobject_cast<QQuickWindow *>(windowInterface->window());
196+
if (!quickWindow) {
197+
delete io;
198+
return;
199+
}
200+
auto dialog = createAndPositionDialog(quickWindow, &component, properties);
201+
if (!execDialog(dialog)) {
202+
delete io;
203+
return;
204+
}
205+
206+
selectedTrack = qobject_cast<dspx::Track *>(dialog->property("selectedTrack").value<QObject *>());
207+
if (!selectedTrack) {
208+
selectedTrack = trackList->items().first();
209+
}
210+
const auto clipPosition = qMax(0, dialog->property("clipPosition").toInt());
211+
const auto clipName = dialog->property("clipName").toString();
212+
213+
auto timeline = windowInterface->projectTimeline()->musicTimeline();
214+
io->open(talcs::AbstractAudioFormatIO::Read);
215+
const auto durationMsec = io->sampleRate() > 0 ? static_cast<double>(io->length()) * 1000.0 / io->sampleRate() : 0.0;
216+
const auto clipPositionMsec = timeline->create(0, 0, clipPosition).millisecond();
217+
const auto clipEndPosition = timeline->create(clipPositionMsec + durationMsec).totalTick();
218+
const auto clipLength = qMax(1, clipEndPosition - clipPosition);
219+
220+
dspx::AudioPathInfo path;
221+
path.absoluteDir = fileInfo.absolutePath();
222+
path.fileName = fileInfo.fileName();
223+
path.formatEntryClassName = entryClassName;
224+
path.userData = userData;
225+
path.sha512 = HashHelper::sha512(fileInfo.absoluteFilePath());
226+
227+
auto fileLocker = windowInterface->projectDocumentContext()->fileLocker();
228+
const auto projectPath = fileLocker ? fileLocker->path() : QString();
229+
if (!projectPath.isEmpty()) {
230+
const QFileInfo projectFileInfo(projectPath);
231+
const QDir projectDir(projectFileInfo.absolutePath());
232+
if (isInDirectoryOrSubdirectory(fileInfo.absoluteFilePath(), projectDir)) {
233+
path.relativeDir = projectDir.relativeFilePath(fileInfo.absolutePath());
234+
}
235+
}
236+
237+
qCDebug(lcInsertAudioClipAddOn) << "Inserting audio clip" << fileName << "at" << clipPosition
238+
<< "length" << clipLength << "track" << selectedTrack;
239+
240+
auto projectAudioAddOn = ProjectAudioAddOn::of(windowInterface);
241+
dspx::AudioClip *newClip = nullptr;
242+
bool success = false;
243+
bool cacheTransferred = false;
244+
document->transactionController()->beginScopedTransaction(tr("Inserting audio clip"), [=, &newClip, &success, &cacheTransferred] {
245+
newClip = model->createAudioClip();
246+
newClip->setName(clipName);
247+
newClip->setPath(path);
248+
auto time = newClip->time();
249+
time->setClipStart(0);
250+
time->setClipLen(clipLength);
251+
time->setStart(clipPosition);
252+
newClip->control()->setGain(1);
253+
if (projectAudioAddOn) {
254+
projectAudioAddOn->addAudioClipCache(newClip, io);
255+
cacheTransferred = true;
256+
}
257+
auto clipSequence = selectedTrack->clips();
258+
if (!clipSequence || !clipSequence->insertItem(newClip)) {
259+
if (projectAudioAddOn && cacheTransferred) {
260+
delete projectAudioAddOn->takeAudioClipCache(newClip);
261+
cacheTransferred = false;
262+
}
263+
model->destroyItem(newClip);
264+
newClip = nullptr;
265+
return false;
266+
}
267+
success = true;
268+
return true;
269+
}, [] {
270+
qCCritical(lcInsertAudioClipAddOn) << "Failed to insert audio clip in exclusive transaction";
271+
});
272+
273+
if (!success) {
274+
if (!cacheTransferred) {
275+
delete io;
276+
}
277+
return;
278+
}
279+
if (!cacheTransferred) {
280+
delete io;
281+
}
282+
283+
if (newClip) {
284+
document->selectionModel()->select(newClip, dspx::SelectionModel::Select | dspx::SelectionModel::SetCurrentItem | dspx::SelectionModel::ClearPreviousSelection);
285+
}
286+
}
287+
288+
}
289+
290+
#include "moc_InsertAudioClipAddOn.cpp"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#ifndef DIFFSCOPE_AUDIO_INSERTAUDIOCLIPADDON_H
2+
#define DIFFSCOPE_AUDIO_INSERTAUDIOCLIPADDON_H
3+
4+
#include <qqmlintegration.h>
5+
6+
#include <CoreApi/windowinterface.h>
7+
8+
namespace Core {
9+
class ProjectWindowInterface;
10+
}
11+
12+
namespace Audio::Internal {
13+
14+
class InsertAudioClipAddOn : public Core::WindowInterfaceAddOn {
15+
Q_OBJECT
16+
QML_ELEMENT
17+
QML_UNCREATABLE("")
18+
public:
19+
explicit InsertAudioClipAddOn(QObject *parent = nullptr);
20+
~InsertAudioClipAddOn() override;
21+
22+
void initialize() override;
23+
void extensionsInitialized() override;
24+
bool delayedInitialize() override;
25+
26+
static InsertAudioClipAddOn *of(Core::ProjectWindowInterface *windowHandle);
27+
28+
Q_INVOKABLE void insertAudioClip();
29+
};
30+
31+
}
32+
33+
#endif // DIFFSCOPE_AUDIO_INSERTAUDIOCLIPADDON_H
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import QtQml
2+
import QtQuick
3+
import QtQuick.Controls
4+
5+
import QActionKit
6+
7+
import DiffScope.Audio
8+
9+
ActionCollection {
10+
id: d
11+
12+
required property InsertAudioClipAddOn addOn
13+
14+
ActionItem {
15+
actionId: "org.diffscope.audio.insert.insertAudioClip"
16+
Action {
17+
onTriggered: Qt.callLater(() => d.addOn.insertAudioClip())
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)