void CueStack_Test::write() { QList<Universe*> ua; ua.append(new Universe(0, new GrandMaster())); CueStack cs(m_doc); Cue cue("One"); cue.setValue(0, 255); cue.setFadeInSpeed(100); cue.setFadeOutSpeed(200); cue.setDuration(300); cs.appendCue(cue); cue = Cue("Two"); cue.setValue(1, 255); cue.setFadeInSpeed(100); cue.setFadeOutSpeed(200); cue.setDuration(300); cs.appendCue(cue); cs.preRun(); QVERIFY(cs.m_fader != NULL); cs.write(ua); QCOMPARE(cs.currentIndex(), -1); cs.start(); cs.write(ua); QCOMPARE(cs.currentIndex(), -1); cs.nextCue(); QCOMPARE(cs.currentIndex(), -1); cs.write(ua); QCOMPARE(cs.currentIndex(), 0); QCOMPARE(cs.m_fader->channels().size(), 1); FadeChannel fc; fc.setChannel(0); QCOMPARE(cs.m_fader->channels()[fc].channel(), uint(0)); QCOMPARE(cs.m_fader->channels()[fc].target(), uchar(255)); cs.previousCue(); QCOMPARE(cs.currentIndex(), 0); cs.write(ua); QCOMPARE(cs.currentIndex(), 1); fc.setChannel(0); QCOMPARE(cs.m_fader->channels()[fc].channel(), uint(0)); QCOMPARE(cs.m_fader->channels()[fc].target(), uchar(0)); fc.setChannel(1); QCOMPARE(cs.m_fader->channels()[fc].channel(), uint(1)); QCOMPARE(cs.m_fader->channels()[fc].target(), uchar(255)); MasterTimer mt(m_doc); cs.postRun(&mt); }
//- LISP API - // playback-set-file (string)fileName -> t/nil base::cell_t set_file(base::lisp &gl, base::cell_t c, base::cells_t &) { if (base::lisp::validate(c, base::cell::list(1), base::cell::typeString)) { const auto &fname = c + 1; // stop current playback ts.stop(); ts.setSource(nullptr); frs = nullptr; AudioFormatReader *r; // ectract CUE information (if any) std::regex cue("^(.*):(\\d+):(\\d+)$"); std::smatch result; std::regex_search(fname->s, result, cue); if (result.size() == 4) { // is cue int32 start = base::fromStr<int32>(result[2].str()); int32 end = base::fromStr<int32>(result[3].str()); int32 duration = end - start; AudioFormatReader *tr = fm.createReaderFor(File(result[1].str())); // start, end are in frames (1 frame = 1/75 second) - convert to sample float samplesInOneSecond = tr->sampleRate; // AudioSubsectionReader will handle channels count float startSecond = (float)start / 75.0f; float durationSecond = (float)duration / 75.0f; float startSample = startSecond * samplesInOneSecond; float durationSamples = durationSecond * samplesInOneSecond; // some CUE may have 0 length (play to end) if (end <= start) durationSamples = tr->lengthInSamples; r = new AudioSubsectionReader(tr, (int)startSample, (int)durationSamples, true); } else { // regular file r = fm.createReaderFor(File(fname->s)); } if (r) { frs = new AudioFormatReaderSource(r, true); ts.setSource(frs, 32768, &thread, r->sampleRate); return gl.t(); } gl.signalError(base::strs("file not found or file format not supported: ", fname->s)); return gl.nil(); } gl.signalError("playback-set-file: invalid arguments, expected (string)"); return gl.nil(); }
void firstAndOthers(const string &line, string &first, string &others) { // skip whitespace before first string::const_iterator curr(line.begin()), cue(line.end()); for(; (curr!=cue) && (*curr<=' '); curr++); // find the end of first string::const_iterator fbeg=curr; for(; (curr!=cue) && (*curr>' '); curr++); first=string(fbeg, curr); // skip the whitespace before others for(; (curr!=cue) && (*curr<=' '); curr++); others=string(curr, cue); }
SongList LibraryWatcher::ScanNewFile(const QString& file, const QString& path, const QString& matching_cue, QSet<QString>* cues_processed) { SongList song_list; uint matching_cue_mtime = GetMtimeForCue(matching_cue); // if it's a cue - create virtual tracks if (matching_cue_mtime) { // don't process the same cue many times if (cues_processed->contains(matching_cue)) return song_list; QFile cue(matching_cue); cue.open(QIODevice::ReadOnly); // Ignore FILEs pointing to other media files. Also, watch out for incorrect // media files. Playlist parser for CUEs considers every entry in sheet // valid and we don't want invalid media getting into library! QString file_nfd = file.normalized(QString::NormalizationForm_D); for (const Song& cue_song : cue_parser_->Load(&cue, matching_cue, path)) { if (cue_song.url().toLocalFile().normalized(QString::NormalizationForm_D) == file_nfd) { if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) { song_list << cue_song; } } } if (!song_list.isEmpty()) { *cues_processed << matching_cue; } // it's a normal media file } else { Song song; TagReaderClient::Instance()->ReadFileBlocking(file, &song); if (song.is_valid()) { song_list << song; } } return song_list; }
void LibraryWatcher::UpdateCueAssociatedSongs(const QString& file, const QString& path, const QString& matching_cue, const QString& image, ScanTransaction* t) { QFile cue(matching_cue); cue.open(QIODevice::ReadOnly); SongList old_sections = backend_->GetSongsByUrl(QUrl::fromLocalFile(file)); QHash<quint64, Song> sections_map; for (const Song& song : old_sections) { sections_map[song.beginning_nanosec()] = song; } QSet<int> used_ids; // update every song that's in the cue and library for (Song cue_song : cue_parser_->Load(&cue, matching_cue, path)) { cue_song.set_directory_id(t->dir()); Song matching = sections_map[cue_song.beginning_nanosec()]; // a new section if (!matching.is_valid()) { t->new_songs << cue_song; // changed section } else { PreserveUserSetData(file, image, matching, &cue_song, t); used_ids.insert(matching.id()); } } // sections that are now missing for (const Song& matching : old_sections) { if (!used_ids.contains(matching.id())) { t->deleted_songs << matching; } } }
//- LISP API - // playback-set-file (string)fileName -> t/nil base::cell_t set_file(base::lisp &gl, base::cell_t c, base::cells_t &) { if (base::lisp::validate(c, base::cell::list(1), base::cell::typeString)) { const auto &fname = c + 1; // stop current playback ts.stop(); ts.setSource(nullptr); frs = nullptr; AudioFormatReader *r; // ectract CUE information (if any) std::regex cue("^(.*):(\\d+):(\\d+)$"); std::smatch result; std::regex_search(fname->s, result, cue); if (result.size() == 4) { // is cue int32 start = base::fromStr<int32>(result[2].str()); int32 end = base::fromStr<int32>(result[3].str()); AudioFormatReader *tr = fm.createReaderFor(File(result[1].str())); r = new AudioSubsectionReader(tr, start, end - start, true); } else { // regular file r = fm.createReaderFor(File(fname->s)); } if (r) { frs = new AudioFormatReaderSource(r, true); ts.setSource(frs, 32768, &thread, r->sampleRate); return gl.t(); } gl.signalError(base::strs("file not found or file format not supported: ", fname->s)); return gl.nil(); } gl.signalError("playback-set-file: invalid arguments, expected (string)"); return gl.nil(); }
/************************************************ Split audio file to temporary dir ************************************************/ void Splitter::doRun() { QStringList args; args << "split"; args << "-w"; args << "-O" << "always"; args << "-n" << "%04d"; args << "-t" << mFilePrefix +"%n"; args << "-d" << mWorkDir; args << disk()->audioFileName(); //qDebug() << args; QString shntool = settings->value(Settings::Prog_Shntool).toString(); mProcess = new QProcess(); mProcess->setReadChannel(QProcess::StandardError); mProcess->start(shntool, args); mProcess->waitForStarted(); sendCueData(); mProcess->closeWriteChannel(); parseOut(); mProcess->waitForFinished(-1); QProcess *proc = mProcess; mProcess = 0; delete proc; if (OutFormat::currentFormat()->createCue()) { CueCreator cue(disk()); cue.setHasPregapFile(mPreGapExists); if (!cue.write()) error(disk()->track(0), cue.errorString()); } }
Playlist::Entries FFDemux::fetchTracks(const QString &url, bool &ok) { if (url.contains("://{") || !url.startsWith("file://")) return {}; const auto createFmtCtx = [&] { FormatContext *fmtCtx = new FormatContext; { QMutexLocker mL(&mutex); formatContexts.append(fmtCtx); } return fmtCtx; }; const auto destroyFmtCtx = [&](FormatContext *fmtCtx) { { QMutexLocker mL(&mutex); const int idx = formatContexts.indexOf(fmtCtx); if (idx > -1) formatContexts.remove(idx); } delete fmtCtx; }; if (url.endsWith(".cue", Qt::CaseInsensitive)) { QFile cue(url.mid(7)); if (cue.size() <= 0xFFFF && cue.open(QFile::ReadOnly | QFile::Text)) { QList<QByteArray> data = Functions::textWithFallbackEncoding(cue.readAll()).split('\n'); QString title, performer, audioUrl; double index0 = -1.0, index1 = -1.0; QHash<int, QPair<double, double>> indexes; Playlist::Entries entries; Playlist::Entry entry; int track = -1; const auto maybeFlushTrack = [&](int prevTrack) { if (track <= 0) return; const auto cutFromQuotation = [](QString &str) { const int idx1 = str.indexOf('"'); const int idx2 = str.lastIndexOf('"'); if (idx1 > -1 && idx2 > idx1) str = str.mid(idx1 + 1, idx2 - idx1 - 1); else str.clear(); }; cutFromQuotation(title); cutFromQuotation(performer); if (!title.isEmpty() && !performer.isEmpty()) entry.name = performer + " - " + title; else if (title.isEmpty() && !performer.isEmpty()) entry.name = performer; else if (!title.isEmpty() && performer.isEmpty()) entry.name = title; if (entry.name.isEmpty()) { if (entry.parent == 1) entry.name = tr("Track") + " " + QString::number(prevTrack); else entry.name = Functions::fileName(entry.url, false); } title.clear(); performer.clear(); if (prevTrack > 0) { if (index0 <= 0.0) index0 = index1; // "INDEX 00" doesn't exist, use "INDEX 01" indexes[prevTrack].first = index0; indexes[prevTrack].second = index1; if (entries.count() <= prevTrack) entries.resize(prevTrack + 1); entries[prevTrack] = entry; } else { entries.prepend(entry); } entry = Playlist::Entry(); entry.parent = 1; entry.url = audioUrl; index0 = index1 = -1.0; }; const auto parseTime = [](const QByteArray &time) { int m = 0, s = 0, f = 0; if (sscanf(time.constData(), "%2d:%2d:%2d", &m, &s, &f) == 3) return m * 60.0 + s + f / 75.0; return -1.0; }; entry.url = url; entry.GID = 1; for (QByteArray &line : data) { line = line.trimmed(); if (track < 0) { if (line.startsWith("TITLE ")) title = line; else if (line.startsWith("PERFORMER ")) performer = line; else if (line.startsWith("FILE ")) { const int idx = line.lastIndexOf('"'); if (idx > -1) { audioUrl = line.mid(6, idx - 6); if (!audioUrl.isEmpty()) audioUrl.prepend(Functions::filePath(url)); } } } else if (line.startsWith("FILE ")) { // QMPlay2 supports CUE files which uses only single audio file return {}; } if (line.startsWith("TRACK ")) { if (entries.isEmpty() && audioUrl.isEmpty()) break; if (line.endsWith(" AUDIO")) { const int prevTrack = track; track = line.mid(6, 2).toInt(); if (track < 99) maybeFlushTrack(prevTrack); else track = 0; } else { track = 0; } } else if (track > 0) { if (line.startsWith("TITLE ")) title = line; else if (line.startsWith("PERFORMER ")) performer = line; else if (line.startsWith("INDEX 00 ")) index0 = parseTime(line.mid(9)); else if (line.startsWith("INDEX 01 ")) index1 = parseTime(line.mid(9)); } } maybeFlushTrack(track); for (int i = entries.count() - 1; i >= 1; --i) { Playlist::Entry &entry = entries[i]; const bool lastItem = (i == entries.count() - 1); const double start = indexes.value(i).second; const double end = indexes.value(i + 1, {-1.0, -1.0}).first; if (entry.url.isEmpty() || start < 0.0 || (end <= 0.0 && !lastItem)) { entries.removeAt(i); continue; } const QString param = QString("CUE:%1:%2").arg(start).arg(end); if (lastItem && end < 0.0) // Last entry doesn't have specified length in CUE file { FormatContext *fmtCtx = createFmtCtx(); if (fmtCtx->open(entry.url, param)) entry.length = fmtCtx->length(); destroyFmtCtx(fmtCtx); if (abortFetchTracks) { ok = false; return {}; } } else { entry.length = end - start; } entry.url = QString("%1://{%2}%3").arg(DemuxerName, entry.url, param); } if (!entries.isEmpty()) return entries; } } OggHelper oggHelper(url.mid(7), abortFetchTracks); if (oggHelper.io) { Playlist::Entries entries; int i = 0; for (const OggHelper::Chain &chains : oggHelper.getOggChains(ok)) { const QString param = QString("OGG:%1:%2:%3").arg(++i).arg(chains.first).arg(chains.second); FormatContext *fmtCtx = createFmtCtx(); if (fmtCtx->open(url, param)) { Playlist::Entry entry; entry.url = QString("%1://{%2}%3").arg(DemuxerName, url, param); entry.name = fmtCtx->title(); entry.length = fmtCtx->length(); entries.append(entry); } destroyFmtCtx(fmtCtx); if (abortFetchTracks) { ok = false; return {}; } } if (ok && !entries.isEmpty()) { for (int i = 0; i < entries.count(); ++i) entries[i].parent = 1; Playlist::Entry entry; entry.name = Functions::fileName(url, false); entry.url = url; entry.GID = 1; entries.prepend(entry); return entries; } } return {}; }
void Splitter::sendCueData() { bool cdQuality = disk()->audioFile()->isCdQuality(); OutFormat *format = OutFormat::currentFormat(); bool fakeIndex = (format->createCue() and format->preGapType() == OutFormat::PreGapAddToFirstTrack); QFile cue(disk()->cueFile()); cue.open(QFile::ReadOnly); int trackNum = 0; QByteArray line; while (!cue.atEnd()) { line = cue.readLine(); QString str = QString(line).trimmed(); QString key = str.section(' ', 0, 0).toUpper(); if (key == "TRACK") { trackNum++; mProcess->write(line); continue; } if (key == "INDEX") { int indexNum = str.section(' ', 1, 1).toInt(); if (fakeIndex && trackNum == 1) { if (indexNum == 1) { if (cdQuality) mProcess->write(" INDEX 01 00:00:00\n"); else mProcess->write(" INDEX 01 00:00.000\n"); } } else { CueIndex index(str.section(' ', 2)); mProcess->write(QString(" INDEX %1 %2\n") .arg(indexNum, 2, 10, QChar('0')) .arg(index.toString(cdQuality)) .toLocal8Bit()); } continue; } if (key == "FILE") { mProcess->write(line); continue; } } cue.close(); }