Commit ddd7a425 authored by Nicolas Carion's avatar Nicolas Carion

some fixes on audio splitting + tests

parent 7cf732a8
......@@ -292,6 +292,12 @@ std::unordered_set<int> GroupsModel::getDirectChildren(int id) const
Q_ASSERT(m_downLink.count(id) > 0);
return m_downLink.at(id);
}
int GroupsModel::getDirectAncestor(int id) const
{
READ_LOCK();
Q_ASSERT(m_upLink.count(id) > 0);
return m_upLink.at(id);
}
void GroupsModel::setGroup(int id, int groupId)
{
......@@ -531,6 +537,7 @@ bool GroupsModel::split(int id, const std::function<bool(int)> &criterion, Fun &
void GroupsModel::setInGroupOf(int id, int targetId, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_upLink.count(targetId) > 0);
Fun operation = [ this, id, group = m_upLink[targetId] ]()
{
......@@ -546,6 +553,46 @@ void GroupsModel::setInGroupOf(int id, int targetId, Fun &undo, Fun &redo)
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
bool GroupsModel::createGroupAtSameLevel(int id, std::unordered_set<int> to_add, GroupType type, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_upLink.count(id) > 0);
Q_ASSERT(isLeaf(id));
if (to_add.size() == 0) {
return true;
}
int gid = TimelineModel::getNextId();
std::unordered_map<int, int> old_parents;
to_add.insert(id);
for (int g : to_add) {
Q_ASSERT(m_upLink.count(g) > 0);
old_parents[g] = m_upLink[g];
}
Fun operation = [ this, id, gid, type, to_add, parent = m_upLink.at(id) ]()
{
createGroupItem(gid);
setGroup(gid, parent);
for (const auto &g : to_add) {
setGroup(g, gid);
}
setType(gid, type);
return true;
};
Fun reverse = [this, id, old_parents, gid]() {
for (const auto &g : old_parents) {
setGroup(g.first, g.second);
}
setGroup(gid, -1);
destructGroupItem_lambda(gid)();
return true;
};
bool success = operation();
if (success) {
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
return success;
}
bool GroupsModel::processCopy(int gid, std::unordered_map<int, int> &mapping, Fun &undo, Fun &redo)
{
qDebug() << "processCopy" << gid;
......
......@@ -122,6 +122,12 @@ public:
/* @brief Move element id in the same group as targetId */
void setInGroupOf(int id, int targetId, Fun &undo, Fun &redo);
/* @brief We replace the leaf node given by id with a group that contains the leaf plus all the clips in to_add.
* The created group type is given in parameter
* Returns true on success
*/
bool createGroupAtSameLevel(int id, std::unordered_set<int> to_add, GroupType type, Fun &undo, Fun &redo);
/* @brief Returns the id of all the descendant of given item (including item)
@param id of the groupItem
*/
......@@ -138,6 +144,11 @@ public:
*/
std::unordered_set<int> getDirectChildren(int id) const;
/* @brief Gets direct ancestor of a given group item. Returns -1 if not in a group
@param id of the groupItem
*/
int getDirectAncestor(int id) const;
/* @brief Get the type of the group
@param id of the groupItem. Must be a proper group, not a leaf
*/
......@@ -192,7 +203,10 @@ protected:
/* @brief Transform a group node with no children into a leaf. This implies doing the deregistration to the timeline */
void downgradeToLeaf(int gid);
/* @brief Simple type setter */
/* @Brief helper function to change the type of a group.
@param id of the groupItem
@param type: new type of the group
*/
void setType(int gid, GroupType type);
private:
......
......@@ -60,7 +60,8 @@ bool TimelineFunctions::copyClip(std::shared_ptr<TimelineItemModel> timeline, in
return res;
}
bool TimelineFunctions::requestMultipleClipsInsertion(std::shared_ptr<TimelineItemModel> timeline, const QStringList &binIds, int trackId, int position, QList<int> &clipIds, bool logUndo, bool refreshView)
bool TimelineFunctions::requestMultipleClipsInsertion(std::shared_ptr<TimelineItemModel> timeline, const QStringList &binIds, int trackId, int position,
QList<int> &clipIds, bool logUndo, bool refreshView)
{
std::function<bool(void)> undo = []() { return true; };
std::function<bool(void)> redo = []() { return true; };
......@@ -108,7 +109,7 @@ bool TimelineFunctions::processClipCut(std::shared_ptr<TimelineItemModel> timeli
res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo);
if (durationChanged) {
// Track length changed, check project duration
Fun updateDuration = [timeline]() {
Fun updateDuration = [timeline]() {
timeline->updateDuration();
return true;
};
......@@ -129,7 +130,6 @@ bool TimelineFunctions::requestClipCut(std::shared_ptr<TimelineItemModel> timeli
return result;
}
bool TimelineFunctions::requestClipCut(std::shared_ptr<TimelineItemModel> timeline, int clipId, int position, Fun &undo, Fun &redo)
{
const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
......@@ -356,8 +356,9 @@ bool TimelineFunctions::requestItemCopy(std::shared_ptr<TimelineItemModel> timel
res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo);
} else {
const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
QScopedPointer <Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties());
res = res & timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, timeline->m_allCompositions[id]->getPlaytime(), transProps.data(), newId, undo, redo);
QScopedPointer<Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties());
res = res & timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
timeline->m_allCompositions[id]->getPlaytime(), transProps.data(), newId, undo, redo);
}
} else {
res = false;
......@@ -397,7 +398,7 @@ bool TimelineFunctions::changeClipState(std::shared_ptr<TimelineItemModel> timel
{
PlaylistState::ClipState oldState = timeline->m_allClips[clipId]->clipState();
if (oldState == status) {
return false;
return true;
}
std::function<bool(void)> undo = []() { return true; };
std::function<bool(void)> redo = []() { return true; };
......@@ -412,13 +413,13 @@ bool TimelineFunctions::changeClipState(std::shared_ptr<TimelineItemModel> timel
{
PlaylistState::ClipState oldState = timeline->m_allClips[clipId]->clipState();
if (oldState == status) {
return false;
return true;
}
Fun local_redo = [timeline, clipId, status]() {
Fun operation = [timeline, clipId, status]() {
int trackId = timeline->getClipTrackId(clipId);
bool res = timeline->m_allClips[clipId]->setClipState(status);
// in order to make the producer change effective, we need to unplant / replant the clip in int track
if (trackId != -1) {
if (res && trackId != -1) {
timeline->getTrackById(trackId)->replugClip(clipId);
QModelIndex ix = timeline->makeClipIndexFromID(clipId);
timeline->dataChanged(ix, ix, {TimelineModel::StatusRole});
......@@ -429,11 +430,11 @@ bool TimelineFunctions::changeClipState(std::shared_ptr<TimelineItemModel> timel
}
return res;
};
Fun local_undo = [timeline, clipId, oldState]() {
Fun reverse = [timeline, clipId, oldState]() {
bool res = timeline->m_allClips[clipId]->setClipState(oldState);
// in order to make the producer change effective, we need to unplant / replant the clip in int track
int trackId = timeline->getClipTrackId(clipId);
if (trackId != -1) {
if (res && trackId != -1) {
int start = timeline->getItemPosition(clipId);
int end = start + timeline->getItemPlaytime(clipId);
timeline->getTrackById(trackId)->replugClip(clipId);
......@@ -444,10 +445,9 @@ bool TimelineFunctions::changeClipState(std::shared_ptr<TimelineItemModel> timel
}
return res;
};
bool result = local_redo();
bool result = reverse();
if (result) {
PUSH_LAMBDA(local_undo, undo);
PUSH_LAMBDA(local_redo, redo);
UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo);
}
return result;
}
......@@ -457,10 +457,15 @@ bool TimelineFunctions::requestSplitAudio(std::shared_ptr<TimelineItemModel> tim
std::function<bool(void)> undo = []() { return true; };
std::function<bool(void)> redo = []() { return true; };
const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
bool done = false;
for (int cid : clips) {
if (!timeline->getClipPtr(cid)->hasAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
// clip without audio or audio only, skip
continue;
}
int position = timeline->getClipPosition(cid);
int track = timeline->getClipTrackId(cid);
QList<int> possibleTracks = audioTarget >= 0 ? QList<int>() <<audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack);
QList<int> possibleTracks = audioTarget >= 0 ? QList<int>() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack);
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
undo();
......@@ -469,32 +474,38 @@ bool TimelineFunctions::requestSplitAudio(std::shared_ptr<TimelineItemModel> tim
}
int newId;
bool res = copyClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
bool move = false;
while (!move && !possibleTracks.isEmpty()) {
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
return false;
}
bool success = false;
while (!success && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
move = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo);
success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo);
}
std::unordered_set<int> groupClips;
groupClips.insert(cid);
groupClips.insert(newId);
if (!res || !move) {
TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
if (!success) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
return false;
}
TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
timeline->requestClipsGroup(groupClips, undo, redo, GroupType::AVSplit);
done = true;
}
pCore->pushUndo(undo, redo, i18n("Split Audio"));
return true;
if (done) {
pCore->pushUndo(undo, redo, i18n("Split Audio"));
}
return done;
}
void TimelineFunctions::setCompositionATrack(std::shared_ptr<TimelineItemModel> timeline, int cid, int aTrack)
{
std::function<bool(void)> undo = []() { return true; };
std::function<bool(void)> redo = []() { return true; };
std::shared_ptr<CompositionModel>compo = timeline->getCompositionPtr(cid);
std::shared_ptr<CompositionModel> compo = timeline->getCompositionPtr(cid);
int previousATrack = compo->getATrack();
int previousAutoTrack = compo->getForcedTrack() == -1;
bool autoTrack = aTrack < 0;
......@@ -520,7 +531,7 @@ void TimelineFunctions::setCompositionATrack(std::shared_ptr<TimelineItemModel>
QScopedPointer<Mlt::Field> field(timeline->m_tractor->field());
field->lock();
timeline->getCompositionPtr(cid)->setForceTrack(!previousAutoTrack);
timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack<= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
field->unlock();
QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack});
......
......@@ -73,8 +73,18 @@ struct TimelineFunctions
static bool requestItemCopy(std::shared_ptr<TimelineItemModel> timeline, int clipId, int trackId, int position);
static void showClipKeyframes(std::shared_ptr<TimelineItemModel> timeline, int clipId, bool value);
static void showCompositionKeyframes(std::shared_ptr<TimelineItemModel> timeline, int compoId, bool value);
/* @brief Change the status of the clip to Video+audio, video only, or audio only
* @param timeline: pointer to the timeline that we modify
* @param clipId: Id of the clip to modify
* @param status: target status of the clip
This function creates an undo object and returns true on success
*/
static bool changeClipState(std::shared_ptr<TimelineItemModel> timeline, int clipId, PlaylistState::ClipState status);
/* @brief Same function as above, but accumulates for undo/redo
*/
static bool changeClipState(std::shared_ptr<TimelineItemModel> timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo);
static bool requestSplitAudio(std::shared_ptr<TimelineItemModel> timeline, int clipId, int audioTarget);
static void setCompositionATrack(std::shared_ptr<TimelineItemModel> timeline, int cid, int aTrack);
};
......
......@@ -214,6 +214,7 @@ bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView,
void TrackModel::replugClip(int clipId)
{
QWriteLocker locker(&m_lock);
int clip_position = m_allClips[clipId]->getPosition();
auto clip_loc = getClipIndexAt(clip_position);
int target_track = clip_loc.first;
......
......@@ -206,7 +206,11 @@ protected:
int getCompositionByPosition(int position);
/* @brief Add a track effect */
bool addEffect(const QString &effectId);
/* @brief This function removes the clip from the mlt object, and then insert it back in the same spot again.
* This is used when some properties of the clip have changed, and we need this to refresh it */
void replugClip(int clipId);
int trackDuration();
public slots:
......
......@@ -1580,6 +1580,7 @@ TEST_CASE("Advanced trimming operations", "[Trimming]")
QString binId = createProducer(profile_model, "red", binModel);
QString binId2 = createProducer(profile_model, "blue", binModel);
QString binId3 = createProducerWithSound(profile_model, binModel);
int cid1 = ClipModel::construct(timeline, binId);
int tid1 = TrackModel::construct(timeline);
......@@ -1592,6 +1593,10 @@ TEST_CASE("Advanced trimming operations", "[Trimming]")
int cid6 = ClipModel::construct(timeline, binId);
int cid7 = ClipModel::construct(timeline, binId);
int audio1 = ClipModel::construct(timeline, binId3);
int audio2 = ClipModel::construct(timeline, binId3);
int audio3 = ClipModel::construct(timeline, binId3);
timeline->m_allClips[cid1]->m_endlessResize = false;
timeline->m_allClips[cid2]->m_endlessResize = false;
timeline->m_allClips[cid3]->m_endlessResize = false;
......@@ -1846,4 +1851,150 @@ TEST_CASE("Advanced trimming operations", "[Trimming]")
undoStack->redo();
state3();
}
SECTION("Simple audio split")
{
int l = timeline->getClipPlaytime(audio1);
REQUIRE(timeline->requestClipMove(audio1, tid1, 3));
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(audio1) == l);
REQUIRE(timeline->getClipPosition(audio1) == 3);
REQUIRE(timeline->getClipTrackId(audio1) == tid1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set<int>({audio1}));
};
state();
REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid2));
int splitted1 = timeline->getClipByPosition(tid2, 3);
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(audio1) == l);
REQUIRE(timeline->getClipPosition(audio1) == 3);
REQUIRE(timeline->getClipPlaytime(splitted1) == l);
REQUIRE(timeline->getClipPosition(splitted1) == 3);
REQUIRE(timeline->getClipTrackId(audio1) == tid1);
REQUIRE(timeline->getClipTrackId(splitted1) == tid2);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set<int>({audio1, splitted1}));
int g1 = timeline->m_groups->getDirectAncestor(audio1);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set<int>({audio1, splitted1}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
// We also make sure that clips that are audio only cannot be further splitted
REQUIRE(timeline->requestClipMove(cid1, tid1, 30));
// This is a color clip, shouldn't be splittable
REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, cid1, tid2));
REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, splitted1, tid2));
}
SECTION("Split audio on a selection")
{
int l = timeline->getClipPlaytime(audio2);
REQUIRE(timeline->requestClipMove(audio1, tid1, 0));
REQUIRE(timeline->requestClipMove(audio2, tid1, l));
REQUIRE(timeline->requestClipMove(audio3, tid1, 2 * l));
std::unordered_set<int> selection{audio1, audio3, audio2};
REQUIRE(timeline->requestClipsGroup(selection, false, GroupType::Selection));
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(audio1) == l);
REQUIRE(timeline->getClipPlaytime(audio2) == l);
REQUIRE(timeline->getClipPlaytime(audio3) == l);
REQUIRE(timeline->getClipPosition(audio1) == 0);
REQUIRE(timeline->getClipPosition(audio2) == l);
REQUIRE(timeline->getClipPosition(audio3) == l + l);
REQUIRE(timeline->getClipTrackId(audio1) == tid1);
REQUIRE(timeline->getClipTrackId(audio2) == tid1);
REQUIRE(timeline->getClipTrackId(audio3) == tid1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set<int>({audio1, audio2, audio3}));
int sel = timeline->m_temporarySelectionGroup;
// check that selection is preserved
REQUIRE(sel != -1);
REQUIRE(timeline->m_groups->getType(sel) == GroupType::Selection);
};
state();
REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid2));
int splitted1 = timeline->getClipByPosition(tid2, 0);
int splitted2 = timeline->getClipByPosition(tid2, l);
int splitted3 = timeline->getClipByPosition(tid2, 2 * l);
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(audio1) == l);
REQUIRE(timeline->getClipPlaytime(audio2) == l);
REQUIRE(timeline->getClipPlaytime(audio3) == l);
REQUIRE(timeline->getClipPosition(audio1) == 0);
REQUIRE(timeline->getClipPosition(audio2) == l);
REQUIRE(timeline->getClipPosition(audio3) == l + l);
REQUIRE(timeline->getClipPlaytime(splitted1) == l);
REQUIRE(timeline->getClipPlaytime(splitted2) == l);
REQUIRE(timeline->getClipPlaytime(splitted3) == l);
REQUIRE(timeline->getClipPosition(splitted1) == 0);
REQUIRE(timeline->getClipPosition(splitted2) == l);
REQUIRE(timeline->getClipPosition(splitted3) == l + l);
REQUIRE(timeline->getClipTrackId(audio1) == tid1);
REQUIRE(timeline->getClipTrackId(audio2) == tid1);
REQUIRE(timeline->getClipTrackId(audio3) == tid1);
REQUIRE(timeline->getClipTrackId(splitted1) == tid2);
REQUIRE(timeline->getClipTrackId(splitted2) == tid2);
REQUIRE(timeline->getClipTrackId(splitted3) == tid2);
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 3);
REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set<int>({audio1, splitted1, audio2, audio3, splitted2, splitted3}));
int sel = timeline->m_temporarySelectionGroup;
// check that selection is preserved
REQUIRE(sel != -1);
REQUIRE(timeline->m_groups->getType(sel) == GroupType::Selection);
REQUIRE(timeline->m_groups->getRootId(audio1) == sel);
REQUIRE(timeline->m_groups->getDirectChildren(sel).size() == 3);
REQUIRE(timeline->m_groups->getLeaves(sel).size() == 6);
int g1 = timeline->m_groups->getDirectAncestor(audio1);
int g2 = timeline->m_groups->getDirectAncestor(audio2);
int g3 = timeline->m_groups->getDirectAncestor(audio3);
REQUIRE(timeline->m_groups->getDirectChildren(sel) == std::unordered_set<int>({g1, g2, g3}));
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set<int>({audio1, splitted1}));
REQUIRE(timeline->m_groups->getDirectChildren(g2) == std::unordered_set<int>({audio2, splitted2}));
REQUIRE(timeline->m_groups->getDirectChildren(g3) == std::unordered_set<int>({audio3, splitted3}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
REQUIRE(timeline->m_groups->getType(g2) == GroupType::AVSplit);
REQUIRE(timeline->m_groups->getType(g3) == GroupType::AVSplit);
};
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
}
}
......@@ -19,3 +19,23 @@ QString createProducer(Mlt::Profile &prof, std::string color, std::shared_ptr<Pr
return binId;
}
QString createProducerWithSound(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> binModel, int length, bool limited)
{
std::shared_ptr<Mlt::Producer> producer = std::make_shared<Mlt::Producer>(prof, "blipflash:");
producer->set("length", length);
producer->set("out", length - 1);
REQUIRE(producer->is_valid());
QString binId = QString::number(binModel->getFreeClipId());
auto binClip = ProjectClip::construct(binId, QIcon(), binModel, producer);
if (limited) {
binClip->forceLimitedDuration();
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
REQUIRE(binModel->addItem(binClip, binModel->getRootFolder()->clipId(), undo, redo));
return binId;
}
......@@ -74,3 +74,5 @@ using namespace fakeit;
NO_OTHERS();
QString createProducer(Mlt::Profile &prof, std::string color, std::shared_ptr<ProjectItemModel> binModel, int length = 20, bool limited = true);
QString createProducerWithSound(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> binModel, int length = 20, bool limited = true);
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