/* * scribiu -- read notebooks and voice memos from Livescribe pens * Copyright (C) 2015 Javier S. Pedro * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include "smartpen.h" #include "afdpageaddress.h" #include "paperreplay.h" namespace { bool readUtfString(QDataStream &stream, QString &s) { quint16 len; stream >> len; QByteArray buffer(len, Qt::Uninitialized); if (stream.readRawData(buffer.data(), len) != len) { return false; } s = QString::fromUtf8(buffer); return true; } QString findPenSerial(const QString &path) { QDir dir(path + "/userdata"); QStringList entries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); if (entries.isEmpty()) { return QString(); } else { return entries.first(); } } } PaperReplay::PaperReplay(QObject *parent) : QObject(parent) { } PaperReplay::Session::Session() : d() { } PaperReplay::Session::Session(SessionId id) : d(new SessionData) { d->id = id; } PaperReplay::Session::~Session() { } bool PaperReplay::Session::isValid() const { return d; } PaperReplay::SessionId PaperReplay::Session::id() const { return d ? d->id : 0; } QString PaperReplay::Session::name() const { return d->name; } QString PaperReplay::Session::fileName() const { return d->file; } PaperReplay::PenTime PaperReplay::Session::startTime() const { return d->start; } PaperReplay::PenTime PaperReplay::Session::endTime() const { return d->end; } PaperReplay::PenTime PaperReplay::Session::duration() const { return d->end - d->start; } bool PaperReplay::Session::compareByStartTime(const Session &a, const Session &b) { return a.d->start < b.d->start; } PaperReplay::SessionList::SessionList() { } PaperReplay::SessionList::SessionList(const QMap &byTime) : _m(byTime) { } QList PaperReplay::SessionList::sessionsDuringTime(PenTime time) const { QList sessions; if (_m.isEmpty()) return sessions; QMap::const_iterator it = _m.lowerBound(time); if (it == _m.end()) --it; while (it->d->start <= time && time <= it->d->end) { sessions.append(*it); if (it == _m.begin()) { break; } else { --it; } } return sessions; } bool PaperReplay::open(const QString &path, quint64 notebookGuid, PenTime userTime) { QString penSerial = findPenSerial(path); if (penSerial.isEmpty()) { qDebug() << "Cannot open paper replay:" << path << "does not contain any pen data"; return false; } _dir.setPath(path + QString("/userdata/%1/Paper Replay/99/%2/sessions") .arg(penSerial) .arg(qulonglong(notebookGuid), notebookGuid == 0 ? 1 : 16, 16, QLatin1Char('0'))); if (!_dir.exists()) { qDebug() << "Cannot open paper replay:" << _dir.absolutePath() << "does not exist"; return false; } _userTime = userTime; QDirIterator iter(_dir.path(), QStringList("PRS-*"), QDir::Dirs | QDir::NoDotAndDotDot); while (iter.hasNext()) { bool ok; QDir sessionDir(iter.next()); quint64 sessionId = sessionDir.dirName().mid(4).toULongLong(&ok, 16); if (!ok) { qWarning() << "Invalid session identifier:" << sessionDir.dirName(); continue; } Session session(sessionId); if (!parseSessionInfo(session.d, sessionDir.filePath("session.info"))) { qWarning() << "Could not parse:" << sessionDir.absoluteFilePath("session.info"); } if (sessionDir.exists("session.pages")) { if (!parseSessionPages(session.d, sessionDir.filePath("session.pages"))) { qWarning() << "Could not parse:" << sessionDir.absoluteFilePath("session.pages"); } } if (!session.d->file.isEmpty()) { session.d->file = sessionDir.filePath(session.d->file); } _sessions.insert(sessionId, session); foreach (quint64 page, session.d->pages) { _byPageTime[page].insert(session.d->start, session); } } return true; } void PaperReplay::close() { _byPageTime.clear(); _sessions.clear(); _dir.setPath(QString()); _userTime = 0; } QList PaperReplay::sessions() const { return _sessions.values(); } PaperReplay::SessionList PaperReplay::sessions(PageAddress pageAddress) const { return SessionList(_byPageTime[pageAddress]); } PaperReplay::PenTime PaperReplay::userTime() const { return _userTime; } bool PaperReplay::parseSessionInfo(SessionData *session, const QString &path) const { QFile f(path); if (f.open(QIODevice::ReadOnly)) { return parseSessionInfo(session, &f); } else { return false; } } bool PaperReplay::parseSessionInfo(SessionData *session, QIODevice *dev) const { unsigned char magic[2]; if (dev->read(reinterpret_cast(magic), 2) != 2 || magic[0] != 0xFA || magic[1] != 0xCE) { qWarning() << "Invalid magic"; return false; } char version = 0; if (!dev->getChar(&version)) { qWarning() << "Short read while getting version number"; return false; } switch (version) { case 3: return parseSessionInfoV3(session, dev); default: qWarning() << "Unknown version:" << version; return false; } } bool PaperReplay::parseSessionInfoV3(SessionData *session, QIODevice *dev) const { QDataStream s(dev); if (s.skipRawData(5) != 5) return false; PenTime startTime, endTime, creationTime; QString name; s >> startTime >> endTime >> creationTime; if (!readUtfString(s, name)) { return false; } session->name = name; session->start = startTime; session->end = endTime; qDebug() << " paperreplay" << QString("PRS-%1").arg(session->id, 0, 16) << name << Smartpen::fromPenTime(_userTime, session->start) << Smartpen::fromPenTime(_userTime, session->end); quint16 num_clips; s >> num_clips; Q_ASSERT(num_clips == 1); // TODO: We do not yet know how to handle this scenario (more than one audio file per session) for (uint i = 0; i < num_clips; ++i) { QString file; if (!readUtfString(s, file)) { return false; } s >> startTime >> endTime; qDebug() << " clip" << file << Smartpen::fromPenTime(_userTime, startTime) << Smartpen::fromPenTime(_userTime, endTime); session->file = file; } quint16 num_strokes; s >> num_strokes; // We are not doing anything with the stroke data stored here, // rather we match the paperreplay when we load the strokes from notebook files for (uint i = 0; i < num_strokes; ++i) { quint64 a, b, c; quint32 d; QString nbGuid; quint8 f, g; s >> a >> b >> c >> d; if (!readUtfString(s, nbGuid)) { return false; } s >> f >> g; qDebug() << " stroke" << a << b << c << d << nbGuid << f << g; } return true; } bool PaperReplay::parseSessionPages(SessionData *session, const QString &path) const { QFile f(path); if (f.open(QIODevice::ReadOnly)) { return parseSessionPages(session, &f); } else { return false; } } bool PaperReplay::parseSessionPages(SessionData *session, QIODevice *dev) const { unsigned char magic[2]; if (dev->read(reinterpret_cast(magic), 2) != 2 || magic[0] != 0xCA || magic[1] != 0xBE) { qWarning() << "Invalid magic"; return false; } char version = 0; if (!dev->getChar(&version)) { qWarning() << "Short read while getting version number"; return false; } switch (version) { case 1: return parseSessionPagesV1(session, dev); default: qWarning() << "Unknown version:" << version; return false; } } bool PaperReplay::parseSessionPagesV1(SessionData *session, QIODevice *dev) const { QDataStream s(dev); if (s.skipRawData(1) != 1) return false; quint16 num_pages = 0; s >> num_pages; session->pages.reserve(session->pages.size() + num_pages); for (uint i = 0; i < num_pages; ++i) { PageAddress address; PenTime time; s >> address >> time; session->pages.append(address); qDebug() << " page" << AfdPageAddress(address).toString() << Smartpen::fromPenTime(_userTime, time); } return true; }