Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "convertercontroller.h"
23 :
24 : #include <QJsonDocument>
25 : #include <QJsonObject>
26 : #include <QJsonArray>
27 : #include <QJsonParseError>
28 :
29 : #include "global/defer.h"
30 : #include "global/io/file.h"
31 : #include "global/io/filestream.h"
32 : #include "global/io/dir.h"
33 :
34 : #include "engraving/dom/masterscore.h"
35 : #include "engraving/infrastructure/mscio.h"
36 :
37 : #include "notation/iexcerptnotation.h" // IWYU pragma: keep
38 : #include "notation/imasternotation.h"
39 : #include "notation/inotation.h"
40 : #include "notation/inotationelements.h" // IWYU pragma: keep
41 : #include "notation/inotationinteraction.h"
42 : #include "notation/inotationselectionfilter.h" // IWYU pragma: keep
43 :
44 : #include "project/iprojectaudiosettings.h" // IWYU pragma: keep
45 :
46 : #include "convertercodes.h"
47 : #include "compat/backendapi.h"
48 : #include "compat/notationmeta.h"
49 : #include "converterutils.h"
50 :
51 : #include "log.h"
52 :
53 : using namespace mu::converter;
54 : using namespace mu::project;
55 : using namespace mu::notation;
56 : using namespace muse;
57 : using namespace muse::io;
58 :
59 : static const std::string PDF_SUFFIX = "pdf";
60 : static const std::string PNG_SUFFIX = "png";
61 : static const std::string SVG_SUFFIX = "svg";
62 : static const std::string MP3_SUFFIX = "mp3";
63 :
64 : //! NOTE: Returns the dirs of the path that don't exist yet, from the deepest one to the topmost one
65 16 : static paths_t missingDirs(const path_t& dirPath)
66 : {
67 16 : paths_t missing;
68 16 : path_t dir = dirPath;
69 :
70 21 : while (!dir.empty() && !Dir(dir).exists()) {
71 5 : missing.push_back(dir);
72 :
73 5 : const path_t parent = io::absoluteDirpath(dir);
74 5 : if (parent == dir) {
75 0 : break;
76 : }
77 :
78 5 : dir = parent;
79 5 : }
80 :
81 16 : return missing;
82 16 : }
83 :
84 : //! NOTE: Write directly to the file, some writers (e.g. video) write to the "file_path" themselves
85 16 : Ret ConverterController::writeNotationToFile(INotationWriterPtr writer, INotationPtr notation, const path_t& filePath,
86 : const INotationWriter::Options& options, const path_t& dirPath) const
87 : {
88 16 : const path_t outDir = io::absoluteDirpath(filePath);
89 16 : const paths_t createdDirs = missingDirs(outDir);
90 :
91 16 : Ret ret = Dir::mkpath(outDir);
92 16 : if (!ret) {
93 0 : LOGE() << "failed to create dir: " << outDir << ", err: " << ret.toString();
94 0 : return make_ret(Err::OutFileFailedOpen);
95 : }
96 :
97 : //! NOTE: Don't leave behind the dirs created for the output file if the convert fails.
98 : //! Remove them bottom-up and only while they are empty, so that files written there by others are kept
99 6 : auto removeCreatedDirs = [this, &createdDirs]() {
100 9 : for (const path_t& dir : createdDirs) {
101 4 : if (!fileSystem()->remove(dir, /*onlyIfEmpty*/ true)) {
102 1 : break;
103 : }
104 : }
105 6 : };
106 :
107 16 : FileStream outFile(filePath);
108 32 : outFile.setMeta("file_path", filePath.toStdString());
109 16 : if (!dirPath.empty()) {
110 6 : outFile.setMeta("dir_path", dirPath.toStdString());
111 : }
112 :
113 16 : if (!outFile.open(IODevice::WriteOnly)) {
114 1 : LOGE() << "failed to open file: " << filePath;
115 1 : removeCreatedDirs();
116 1 : return make_ret(Err::OutFileFailedOpen);
117 : }
118 :
119 15 : ret = writer->write(notation, outFile, options);
120 15 : outFile.close();
121 :
122 15 : if (!ret || outFile.hasError()) {
123 5 : LOGE() << "failed write, err: " << (ret ? outFile.errorString() : ret.toString()) << ", path: " << filePath;
124 5 : File::remove(filePath);
125 5 : removeCreatedDirs();
126 5 : return make_ret(Err::OutFileFailedWrite);
127 : }
128 :
129 10 : return make_ok();
130 16 : }
131 :
132 0 : Ret ConverterController::batchConvert(const path_t& batchJobFile, const OpenParams& openParams,
133 : const String& soundProfile, const UriQuery& extensionUri,
134 : ProgressPtr progress)
135 : {
136 0 : TRACEFUNC;
137 :
138 0 : if (progress) {
139 0 : progress->start();
140 : }
141 :
142 0 : RetVal<BatchJob> batchJob = parseBatchJob(batchJobFile);
143 0 : if (!batchJob.ret) {
144 0 : LOGE() << "failed parse batch job file, err: " << batchJob.ret.toString();
145 0 : if (progress) {
146 0 : progress->finish(ProgressResult(batchJob.ret));
147 : }
148 0 : return batchJob.ret;
149 : }
150 :
151 0 : StringList errors;
152 :
153 0 : int64_t current = 0;
154 0 : int64_t total = batchJob.val.size();
155 0 : for (const Job& job : batchJob.val) {
156 0 : if (progress) {
157 0 : ++current;
158 0 : progress->progress(current, total, job.in.toStdString());
159 : }
160 :
161 0 : Ret ret = convertFile(job.in, job.out, openParams, soundProfile, job.tracksDiffPath, extensionUri, job.transposeOptions,
162 0 : job.pageNum, job.visibleParts,
163 0 : job.copyright);
164 0 : if (!ret) {
165 0 : errors.emplace_back(String(u"failed convert, err: %1, in: %2, out: %3")
166 0 : .arg(String::fromStdString(ret.toString())).arg(job.in.toString()).arg(job.out.toString()));
167 : }
168 0 : }
169 :
170 0 : Ret ret;
171 0 : if (!errors.empty()) {
172 0 : ret = make_ret(Err::ConvertFailed, errors.join(u"\n").toStdString());
173 : } else {
174 0 : ret = make_ret(Ret::Code::Ok);
175 : }
176 :
177 0 : if (progress) {
178 0 : progress->finish(ProgressResult(ret));
179 : }
180 :
181 0 : return ret;
182 0 : }
183 :
184 16 : Ret ConverterController::fileConvert(const path_t& in, const path_t& out,
185 : const OpenParams& openParams,
186 : const String& soundProfile,
187 : const path_t& tracksDiffPath,
188 : const UriQuery& extensionUri,
189 : const std::string& transposeOptionsJson,
190 : const std::optional<ConvertTarget>& target)
191 : {
192 16 : std::optional<TransposeOptions> transposeOptions;
193 :
194 16 : if (!transposeOptionsJson.empty()) {
195 0 : RetVal<TransposeOptions> transposeOptionsRet = ConverterUtils::parseTransposeOptions(transposeOptionsJson);
196 0 : if (!transposeOptionsRet.ret) {
197 0 : return transposeOptionsRet.ret;
198 : }
199 :
200 0 : transposeOptions = transposeOptionsRet.val;
201 0 : }
202 :
203 16 : return convertFile(in, out, openParams, soundProfile, tracksDiffPath, extensionUri, transposeOptions, target);
204 : }
205 :
206 16 : Ret ConverterController::convertFile(const muse::io::path_t& in, const muse::io::path_t& out,
207 : const OpenParams& openParams,
208 : const String& soundProfile,
209 : const path_t& tracksDiffPath,
210 : const muse::UriQuery& extensionUri,
211 : const std::optional<notation::TransposeOptions>& transposeOptions,
212 : const std::optional<ConvertTarget>& target,
213 : const std::vector<size_t>& visibleParts,
214 : const CopyrightInfo& copyright)
215 : {
216 17 : TRACEFUNC;
217 :
218 16 : LOGI() << "in: " << in << ", out: " << out;
219 :
220 16 : std::string suffix = io::suffix(out);
221 :
222 16 : auto writer = writers()->writer(suffix);
223 16 : if (!writer) {
224 1 : return make_ret(Err::ConvertTypeUnknown);
225 : }
226 :
227 15 : auto notationProject = notationCreator()->newProject(iocContext());
228 15 : IF_ASSERT_FAILED(notationProject) {
229 0 : return make_ret(Err::UnknownError);
230 : }
231 :
232 30 : Ret ret = notationProject->load(in, openParams);
233 15 : if (!ret) {
234 1 : LOGE() << "failed load notation, err: " << ret.toString() << ", path: " << in;
235 1 : return make_ret(Err::InFileFailedLoad);
236 : }
237 :
238 14 : QJsonArray oldTracks;
239 14 : if (!tracksDiffPath.empty()) {
240 0 : oldTracks = NotationMeta::tracksJsonArray(notationProject->masterNotation()->notation());
241 : }
242 :
243 14 : if (!soundProfile.isEmpty()) {
244 0 : notationProject->audioSettings()->clearTrackInputParams();
245 0 : notationProject->audioSettings()->setActiveSoundProfile(soundProfile);
246 : }
247 :
248 14 : if (transposeOptions.has_value()) {
249 0 : ret = ConverterUtils::applyTranspose(notationProject->masterNotation()->notation(), transposeOptions.value());
250 0 : if (!ret) {
251 0 : LOGE() << "Failed to apply transposition, err: " << ret.toString();
252 0 : return ret;
253 : }
254 : }
255 :
256 14 : if (!visibleParts.empty()) {
257 0 : ConverterUtils::setVisibleParts(notationProject->masterNotation()->notation(), visibleParts);
258 : }
259 :
260 14 : if (!copyright.text.isEmpty()) {
261 0 : auto masterScore = notationProject->masterNotation()->masterScore();
262 :
263 0 : engraving::MStyle& style = masterScore->style();
264 0 : String footerOdd = style.value(engraving::Sid::oddFooterC).value<String>();
265 0 : String footerEven = style.value(engraving::Sid::evenFooterC).value<String>();
266 0 : footerOdd += copyright.text;
267 0 : footerEven += copyright.text;
268 0 : style.set(engraving::Sid::oddFooterC, footerOdd);
269 0 : style.set(engraving::Sid::evenFooterC, footerEven);
270 :
271 0 : masterScore->doLayout();
272 0 : }
273 :
274 14 : projectSession()->setProject(notationProject);
275 :
276 14 : DEFER {
277 14 : projectSession()->setProject(nullptr);
278 28 : };
279 :
280 : // Check if this is a part conversion job
281 14 : QString baseName = QString::fromStdString(io::completeBasename(out).toStdString());
282 14 : if (baseName.contains('*')) {
283 2 : return convertScoreParts(writer, notationProject->masterNotation(), out);
284 : }
285 :
286 : // use a extension for convert
287 12 : if (extensionUri.isValid()) {
288 0 : ret = convertByExtension(writer, notationProject->masterNotation()->notation(), out, extensionUri);
289 0 : if (!ret) {
290 0 : LOGE() << "Failed to convert by extension, err: " << ret.toString();
291 : }
292 : }
293 : // standart convert
294 : else {
295 12 : const bool pageNumIsSet = target.has_value() && std::holds_alternative<page_num_t>(target.value());
296 12 : const bool regionIsSet = target.has_value() && std::holds_alternative<ConvertRegionJson>(target.value());
297 :
298 12 : if (suffix == engraving::MSCZ || suffix == engraving::MSCX || suffix == engraving::MSCS) {
299 1 : if (pageNumIsSet) {
300 0 : return notationProject->savePage(out, std::get<page_num_t>(target.value()));
301 1 : } else if (regionIsSet) {
302 0 : return saveRegion(notationProject, std::get<ConvertRegionJson>(target.value()), out);
303 : }
304 :
305 1 : return notationProject->save(out);
306 : }
307 :
308 11 : if (pageNumIsSet || isConvertPageByPage(suffix)) {
309 2 : if (pageNumIsSet) {
310 1 : page_num_t pageNum = std::get<page_num_t>(target.value());
311 1 : ret = convertPage(writer, notationProject->masterNotation()->notation(), pageNum, out);
312 : } else {
313 1 : ret = convertPageByPage(writer, notationProject->masterNotation()->notation(), out);
314 : }
315 :
316 2 : if (!ret) {
317 0 : LOGE() << "Failed to convert page by page, err: " << ret.toString();
318 : }
319 : } else {
320 9 : ret = convertFullNotation(writer, notationProject->masterNotation()->notation(), out);
321 9 : if (!ret) {
322 5 : LOGE() << "Failed to convert full notation, err: " << ret.toString();
323 : }
324 : }
325 : }
326 :
327 11 : if (ret && !tracksDiffPath.empty()) {
328 0 : ret = writeTracksDiff(notationProject, oldTracks, tracksDiffPath);
329 : }
330 :
331 11 : return ret;
332 16 : }
333 :
334 0 : Ret ConverterController::convertScoreParts(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams)
335 : {
336 0 : TRACEFUNC;
337 :
338 0 : auto notationProject = notationCreator()->newProject(iocContext());
339 0 : IF_ASSERT_FAILED(notationProject) {
340 0 : return make_ret(Err::UnknownError);
341 : }
342 :
343 0 : std::string suffix = io::suffix(out);
344 0 : auto writer = writers()->writer(suffix);
345 0 : if (!writer) {
346 0 : return make_ret(Err::ConvertTypeUnknown);
347 : }
348 :
349 0 : Ret ret = notationProject->load(in, openParams);
350 0 : if (!ret) {
351 0 : LOGE() << "failed load notation, err: " << ret.toString() << ", path: " << in;
352 0 : return make_ret(Err::InFileFailedLoad);
353 : }
354 :
355 0 : ret = convertScoreParts(writer, notationProject->masterNotation(), out);
356 :
357 0 : return ret;
358 0 : }
359 :
360 2 : Ret ConverterController::convertScoreParts(INotationWriterPtr writer, IMasterNotationPtr masterNotation, const muse::io::path_t& out)
361 : {
362 2 : std::string suffix = io::suffix(out);
363 :
364 2 : if (suffix == PDF_SUFFIX) {
365 1 : return convertScorePartsToPdf(writer, masterNotation, out);
366 1 : } else if (suffix == PNG_SUFFIX) {
367 0 : return convertScorePartsToPngs(writer, masterNotation, out);
368 1 : } else if (suffix == MP3_SUFFIX) {
369 1 : return convertScorePartsToMp3(writer, masterNotation, out);
370 : }
371 :
372 0 : return make_ret(Ret::Code::NotSupported);
373 2 : }
374 :
375 0 : RetVal<ConverterController::BatchJob> ConverterController::parseBatchJob(const muse::io::path_t& batchJobFile) const
376 : {
377 0 : TRACEFUNC;
378 :
379 0 : RetVal<BatchJob> rv;
380 0 : QFile file(batchJobFile.toQString());
381 0 : if (!file.open(QIODevice::ReadOnly)) {
382 0 : rv.ret = make_ret(Err::BatchJobFileFailedOpen, file.errorString().toStdString());
383 0 : return rv;
384 : }
385 :
386 0 : const QByteArray data = file.readAll();
387 :
388 0 : QJsonParseError err;
389 0 : const QJsonDocument doc = QJsonDocument::fromJson(data, &err);
390 0 : if (err.error != QJsonParseError::NoError || !doc.isArray()) {
391 0 : rv.ret = make_ret(Err::BatchJobFileFailedParse, err.errorString().toStdString());
392 0 : return rv;
393 : }
394 :
395 0 : const QJsonArray arr = doc.array();
396 :
397 0 : auto correctUserInputPath = [](const QString& path) -> QString {
398 0 : return io::Dir::fromNativeSeparators(path).toQString();
399 : };
400 :
401 0 : for (const auto obj : arr) {
402 0 : Job job;
403 0 : job.in = correctUserInputPath(obj[u"in"].toString());
404 :
405 0 : QJsonObject transposeOptionsObj = obj[u"transpose"].toObject();
406 0 : if (!transposeOptionsObj.isEmpty()) {
407 0 : RetVal<TransposeOptions> transposeOptions = ConverterUtils::parseTransposeOptions(transposeOptionsObj);
408 0 : if (!transposeOptions.ret) {
409 0 : rv.ret = transposeOptions.ret;
410 0 : return rv;
411 : }
412 0 : job.transposeOptions = transposeOptions.val;
413 0 : }
414 :
415 0 : QJsonValue pageVal = obj[u"page"];
416 0 : if (!pageVal.isUndefined()) {
417 0 : job.pageNum = pageVal.toInt() - 1;
418 : }
419 :
420 0 : QJsonValue visibleParts = obj[u"visibleParts"];
421 0 : if (!visibleParts.isUndefined()) {
422 0 : if (visibleParts.isArray()) {
423 0 : const QJsonArray partsArray = visibleParts.toArray();
424 0 : for (const auto part : partsArray) {
425 0 : if (part.isDouble()) {
426 0 : job.visibleParts.push_back(part.toInt());
427 : } else {
428 0 : LOGE() << "Visible parts value must be a Number";
429 : }
430 : }
431 0 : } else {
432 0 : rv.ret = make_ret(Err::BatchJobFileFailedParse, err.errorString().toStdString());
433 0 : return rv;
434 : }
435 : }
436 :
437 0 : QJsonValue copyright = obj[u"copyright"];
438 0 : if (!copyright.isUndefined()) {
439 0 : if (copyright.isObject()) {
440 0 : QJsonValue copyrightText = copyright[u"text"];
441 0 : if (copyrightText.isUndefined()) {
442 0 : rv.ret = make_ret(Err::BatchJobFileFailedParse, err.errorString().toStdString());
443 0 : return rv;
444 : }
445 :
446 0 : job.copyright.text = copyrightText.toString();
447 :
448 0 : QJsonValue showOnAllPages = copyright[u"showOnAllPages"];
449 0 : if (!showOnAllPages.isUndefined() && showOnAllPages.isBool()) {
450 0 : job.copyright.showOnAllPages = showOnAllPages.toBool();
451 : }
452 0 : } else {
453 0 : rv.ret = make_ret(Err::BatchJobFileFailedParse, err.errorString().toStdString());
454 0 : return rv;
455 : }
456 : }
457 :
458 0 : const QJsonValue tracksDiffValue = obj[u"tracksDiff"];
459 0 : if (tracksDiffValue.isString()) {
460 0 : job.tracksDiffPath = correctUserInputPath(tracksDiffValue.toString());
461 : }
462 :
463 0 : const QJsonValue outValue = obj[u"out"];
464 0 : if (outValue.isString()) {
465 0 : job.out = correctUserInputPath(outValue.toString());
466 0 : rv.val.push_back(std::move(job));
467 0 : } else if (outValue.isArray()) {
468 0 : const QJsonArray outArray = outValue.toArray();
469 0 : for (const auto outItem : outArray) {
470 0 : Job partJob = job; // Copy the input path
471 0 : if (outItem.isString()) {
472 0 : partJob.out = correctUserInputPath(outItem.toString());
473 0 : } else if (outItem.isArray() && outItem.toArray().size() == 2) {
474 0 : const QJsonArray partOutArray = outItem.toArray();
475 0 : const QString prefix = correctUserInputPath(partOutArray[0].toString());
476 0 : const QString suffix = partOutArray[1].toString();
477 0 : partJob.out = prefix + "*" + suffix; // Use "*" as a placeholder for part names
478 0 : }
479 0 : rv.val.push_back(std::move(partJob));
480 0 : }
481 0 : }
482 0 : }
483 :
484 0 : rv.ret = make_ret(Ret::Code::Ok);
485 0 : return rv;
486 0 : }
487 :
488 0 : Ret ConverterController::convertByExtension(INotationWriterPtr writer, INotationPtr notation, const muse::io::path_t& out,
489 : const muse::UriQuery& extensionUri)
490 : {
491 0 : extensions::ExtensionUri uri = extensionUri.uri();
492 0 : extensions::ExtensionActionCode actionCode = extensionUri.param("action", Val("main")).toString();
493 :
494 : //! NOTE First we do the extension, it can modify the notation (score)
495 0 : Ret ret = extensionsProvider()->perform(uri, actionCode);
496 0 : if (!ret) {
497 0 : return ret;
498 : }
499 :
500 0 : return writeNotationToFile(writer, notation, out);
501 0 : }
502 :
503 10 : bool ConverterController::isConvertPageByPage(const std::string& suffix) const
504 : {
505 : static const std::unordered_set<std::string> TYPES {
506 : PNG_SUFFIX,
507 : SVG_SUFFIX,
508 14 : };
509 :
510 10 : return muse::contains(TYPES, suffix);
511 1 : }
512 :
513 1 : Ret ConverterController::convertPageByPage(INotationWriterPtr writer, INotationPtr notation, const muse::io::path_t& out) const
514 : {
515 2 : TRACEFUNC;
516 :
517 3 : for (size_t i = 0; i < notation->elements()->pages().size(); i++) {
518 6 : const String filePath = muse::io::path_t(io::dirpath(out) + "/"
519 8 : + io::completeBasename(out) + "-%1."
520 6 : + io::suffix(out)).toString().arg(i + 1);
521 :
522 2 : Ret ret = convertPage(writer, notation, i, filePath, out);
523 2 : if (!ret) {
524 0 : return ret;
525 : }
526 4 : }
527 :
528 1 : return make_ret(Ret::Code::Ok);
529 1 : }
530 :
531 3 : muse::Ret ConverterController::convertPage(INotationWriterPtr writer, INotationPtr notation, const size_t pageNum,
532 : const muse::io::path_t& filePath, const muse::io::path_t& dirPath) const
533 : {
534 4 : TRACEFUNC;
535 :
536 : const INotationWriter::Options options {
537 3 : { INotationWriter::OptionKey::PAGE_NUMBER, Val(static_cast<int>(pageNum)) },
538 9 : };
539 :
540 6 : return writeNotationToFile(writer, notation, filePath, options, dirPath);
541 6 : }
542 :
543 10 : Ret ConverterController::convertFullNotation(INotationWriterPtr writer, INotationPtr notation, const muse::io::path_t& out,
544 : const project::INotationWriter::Options& options) const
545 : {
546 10 : return writeNotationToFile(writer, notation, out, options);
547 : }
548 :
549 1 : Ret ConverterController::convertScorePartsToPdf(INotationWriterPtr writer, IMasterNotationPtr masterNotation,
550 : const muse::io::path_t& out) const
551 : {
552 2 : TRACEFUNC;
553 :
554 : const INotationWriter::Options options {
555 1 : { INotationWriter::OptionKey::UNIT_TYPE, Val(INotationWriter::UnitType::PER_PART) },
556 3 : };
557 :
558 3 : for (const IExcerptNotationPtr& e : masterNotation->excerpts()) {
559 2 : QString partName = e->notation()->name();
560 2 : QString baseName = QString::fromStdString(io::completeBasename(out).toStdString());
561 2 : muse::io::path_t partOut = io::dirpath(out) + "/" + baseName.replace("*", partName).toStdString() + ".pdf";
562 :
563 2 : Ret ret = writeNotationToFile(writer, e->notation(), partOut, options);
564 2 : if (!ret) {
565 0 : return ret;
566 : }
567 4 : }
568 :
569 1 : return make_ret(Ret::Code::Ok);
570 2 : }
571 :
572 0 : Ret ConverterController::convertScorePartsToPngs(INotationWriterPtr writer, mu::notation::IMasterNotationPtr masterNotation,
573 : const muse::io::path_t& out) const
574 : {
575 0 : TRACEFUNC;
576 :
577 0 : for (const IExcerptNotationPtr& e : masterNotation->excerpts()) {
578 0 : QString partName = e->notation()->name();
579 0 : QString baseName = QString::fromStdString(io::completeBasename(out).toStdString());
580 0 : muse::io::path_t pngFilePath = io::dirpath(out) + "/" + baseName.replace("*", partName).toStdString() + ".png";
581 0 : Ret ret = convertPageByPage(writer, e->notation(), pngFilePath);
582 0 : if (!ret) {
583 0 : return ret;
584 : }
585 0 : }
586 :
587 0 : return make_ret(Ret::Code::Ok);
588 0 : }
589 :
590 1 : Ret ConverterController::convertScorePartsToMp3(INotationWriterPtr writer, IMasterNotationPtr masterNotation,
591 : const muse::io::path_t& out) const
592 : {
593 2 : TRACEFUNC;
594 :
595 : const INotationWriter::Options options {
596 1 : { INotationWriter::OptionKey::UNIT_TYPE, Val(INotationWriter::UnitType::PER_PART) },
597 3 : };
598 :
599 1 : for (const IExcerptNotationPtr& e : masterNotation->excerpts()) {
600 1 : QString partName = e->notation()->name();
601 1 : QString baseName = QString::fromStdString(io::completeBasename(out).toStdString());
602 1 : muse::io::path_t partOut = io::dirpath(out) + "/" + baseName.replace("*", partName).toStdString() + ".mp3";
603 :
604 1 : Ret ret = writeNotationToFile(writer, e->notation(), partOut, options);
605 1 : if (!ret) {
606 1 : return ret;
607 : }
608 3 : }
609 :
610 0 : return make_ret(Ret::Code::Ok);
611 2 : }
612 :
613 0 : muse::Ret ConverterController::saveRegion(INotationProjectPtr project, const ConvertRegionJson& regionJson, const path_t& out) const
614 : {
615 0 : RetVal<ConvertRegion> region = ConverterUtils::parseRegion(regionJson);
616 0 : if (!region.ret) {
617 0 : return region.ret;
618 : }
619 :
620 0 : INotationPtr notation = project->masterNotation()->notation();
621 0 : const mu::engraving::Score* score = notation->elements()->msScore();
622 :
623 0 : Measure* startMeasure = score->crMeasure(static_cast<int>(region.val.start.measureIdx));
624 0 : if (!startMeasure) {
625 0 : startMeasure = score->firstMeasure();
626 : }
627 :
628 0 : Measure* endMeasure = score->crMeasure(static_cast<int>(region.val.end.measureIdx));
629 0 : if (!endMeasure) {
630 0 : endMeasure = score->lastMeasure();
631 : }
632 :
633 0 : IF_ASSERT_FAILED(startMeasure && endMeasure) {
634 0 : return make_ret(Err::UnknownError);
635 : }
636 :
637 0 : INotationInteractionPtr interaction = notation->interaction();
638 :
639 0 : if (!region.val.voiceIdxSet.empty()) {
640 : using VoiceFilterType = mu::engraving::VoicesSelectionFilterTypes;
641 : const std::vector<std::pair<size_t, mu::engraving::VoicesSelectionFilterTypes> > VOICE_FILTERS {
642 : { 0, VoiceFilterType::FIRST_VOICE },
643 : { 1, VoiceFilterType::SECOND_VOICE },
644 : { 2, VoiceFilterType::THIRD_VOICE },
645 : { 3, VoiceFilterType::FOURTH_VOICE },
646 0 : };
647 :
648 0 : for (const auto& pair : VOICE_FILTERS) {
649 0 : const bool voiceAccepted = muse::contains(region.val.voiceIdxSet, pair.first);
650 0 : interaction->selectionFilter()->setSelectionTypeFiltered(pair.second, voiceAccepted);
651 : }
652 0 : }
653 :
654 0 : interaction->select({ startMeasure }, SelectType::RANGE, region.val.start.staffIdx);
655 0 : interaction->select({ endMeasure }, SelectType::RANGE, region.val.end.staffIdx);
656 :
657 0 : return project->save(out, SaveMode::SaveSelection);
658 0 : }
659 :
660 0 : Ret ConverterController::writeTracksDiff(INotationProjectPtr project, const QJsonArray& oldTracks, const path_t& path) const
661 : {
662 0 : TRACEFUNC;
663 :
664 0 : QJsonObject root;
665 0 : root["oldTracks"] = oldTracks;
666 0 : root["newTracks"] = NotationMeta::tracksJsonArray(project->masterNotation()->notation());
667 :
668 0 : QJsonDocument document(root);
669 0 : QByteArray qJson = document.toJson(QJsonDocument::Compact);
670 0 : ByteArray json = ByteArray::fromQByteArrayNoCopy(qJson);
671 :
672 0 : return File::writeFile(path, json);
673 0 : }
674 :
675 0 : Ret ConverterController::exportScoreMedia(const muse::io::path_t& in, const muse::io::path_t& out,
676 : const OpenParams& openParams,
677 : const muse::io::path_t& highlightConfigPath)
678 : {
679 0 : TRACEFUNC;
680 :
681 0 : return BackendApi::exportScoreMedia(iocContext(), in, out, highlightConfigPath, openParams);
682 0 : }
683 :
684 0 : Ret ConverterController::exportScoreMeta(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams)
685 : {
686 0 : TRACEFUNC;
687 :
688 0 : return BackendApi::exportScoreMeta(iocContext(), in, out, openParams);
689 0 : }
690 :
691 0 : Ret ConverterController::exportScoreParts(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams)
692 : {
693 0 : TRACEFUNC;
694 :
695 0 : return BackendApi::exportScoreParts(iocContext(), in, out, openParams);
696 0 : }
697 :
698 0 : Ret ConverterController::exportScorePartsPdfs(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams)
699 : {
700 0 : TRACEFUNC;
701 :
702 0 : return BackendApi::exportScorePartsPdfs(iocContext(), in, out, openParams);
703 0 : }
704 :
705 0 : Ret ConverterController::exportScoreTranspose(const muse::io::path_t& in, const muse::io::path_t& out, const std::string& optionsJson,
706 : const OpenParams& openParams)
707 : {
708 0 : TRACEFUNC;
709 :
710 0 : return BackendApi::exportScoreTranspose(iocContext(), in, out, optionsJson, openParams);
711 0 : }
712 :
713 0 : Ret ConverterController::exportScoreElements(const muse::io::path_t& in, const muse::io::path_t& out,
714 : const OpenParams& openParams)
715 : {
716 0 : TRACEFUNC;
717 :
718 0 : return BackendApi::exportScoreElements(iocContext(), in, out, openParams);
719 0 : }
720 :
721 1 : Ret ConverterController::exportScoreVideo(const muse::io::path_t& in, const muse::io::path_t& out, const OpenParams& openParams,
722 : bool withAudio)
723 : {
724 2 : TRACEFUNC;
725 :
726 1 : auto notationProject = notationCreator()->newProject(iocContext());
727 1 : IF_ASSERT_FAILED(notationProject) {
728 0 : return make_ret(Err::UnknownError);
729 : }
730 :
731 1 : std::string suffix = io::suffix(out);
732 1 : auto writer = writers()->writer(suffix);
733 1 : if (!writer) {
734 0 : return make_ret(Err::ConvertTypeUnknown);
735 : }
736 :
737 2 : Ret ret = notationProject->load(in, openParams);
738 1 : if (!ret) {
739 0 : LOGE() << "failed load notation, err: " << ret.toString() << ", path: " << in;
740 0 : return make_ret(Err::InFileFailedLoad);
741 : }
742 :
743 1 : projectSession()->setProject(notationProject);
744 :
745 1 : DEFER {
746 1 : projectSession()->setProject(nullptr);
747 2 : };
748 :
749 : const INotationWriter::Options options {
750 1 : { INotationWriter::OptionKey::WITH_AUDIO, Val(withAudio) },
751 3 : };
752 :
753 1 : return convertFullNotation(writer, notationProject->masterNotation()->notation(), out, options);
754 2 : }
755 :
756 0 : Ret ConverterController::updateSource(const muse::io::path_t& in, const std::string& newSource, bool forceMode)
757 : {
758 0 : TRACEFUNC;
759 :
760 0 : return BackendApi::updateSource(iocContext(), in, newSource, forceMode);
761 0 : }
|