{"action":"created","comment":{"url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/comments/23619332","id":23619332,"diff_hunk":"@@ -0,0 +1,688 @@\n+#include \"metadata/trackmetadatataglib.h\"\n+\n+#include <taglib/tfile.h>\n+#include <taglib/tag.h>\n+#include <taglib/audioproperties.h>\n+#include <taglib/vorbisfile.h>\n+#include <taglib/id3v2frame.h>\n+#include <taglib/id3v2header.h>\n+#include <taglib/id3v1tag.h>\n+#include <taglib/tmap.h>\n+#include <taglib/tstringlist.h>\n+#include <taglib/textidentificationframe.h>\n+#include <taglib/wavpackfile.h>\n+#include <taglib/attachedpictureframe.h>\n+#include <taglib/flacpicture.h>\n+\n+#include <cmath>\n+\n+#include <QDebug>\n+\n+namespace Mixxx {\n+\n+// static\n+namespace {\n+const bool kDebugMetadata = false;\n+\n+// Taglib strings can be NULL and using it could cause some segfaults,\n+// so in this case it will return a QString()\n+inline QString toQString(const TagLib::String& tString) {\n+    if (tString.isNull()) {\n+        return QString();\n+    } else {\n+        return TStringToQString(tString);\n+    }\n+}\n+\n+// Returns the first element of TagLib string list.\n+inline QString toQStringFirst(const TagLib::StringList& strList) {\n+    if (strList.isEmpty()) {\n+        return QString();\n+    } else {\n+        return toQString(strList.front());\n+    }\n+}\n+\n+// Returns the text of an ID3v2 frame as a string.\n+inline QString toQString(const TagLib::ID3v2::Frame& frame) {\n+    return toQString(frame.toString());\n+}\n+\n+// Returns the first frame of an ID3v2 tag as a string.\n+inline QString toQStringFirst(const TagLib::ID3v2::FrameList& frameList) {\n+    if (frameList.isEmpty() || (NULL == frameList.front())) {\n+        return QString();\n+    } else {\n+        return toQString(*frameList.front());\n+    }\n+}\n+\n+// Returns the first value of an MP4 item as a string.\n+inline QString toQStringFirst(const TagLib::MP4::Item& mp4Item) {\n+    const TagLib::StringList strList(mp4Item.toStringList());\n+    if (strList.isEmpty()) {\n+        return QString();\n+    } else {\n+        return toQString(strList.front());\n+    }\n+}\n+\n+// Returns an APE item as a string.\n+inline QString toQString(const TagLib::APE::Item& apeItem) {\n+    return toQString(apeItem.toString());\n+}\n+\n+inline TagLib::String toTagLibString(const QString& str) {\n+    const QByteArray qba(str.toUtf8());\n+    return TagLib::String(qba.constData(), TagLib::String::UTF8);\n+}\n+\n+template<typename T>\n+inline QString formatString(const T& value) {\n+    return QString(\"%1\").arg(value);\n+}\n+\n+template<typename T>\n+inline QString formatString(const T& value, const T& emptyValue) {\n+    if (value == emptyValue) {\n+        return QString(); /// empty string\n+    } else {\n+        return formatString(value);\n+    }\n+}\n+\n+template<typename T>\n+inline QString formatBpmString(const T& bpm) {\n+    return formatString(bpm, TrackMetadata::BPM_UNDEFINED);\n+}\n+\n+} // anonymous namespace\n+\n+bool readAudioProperties(TrackMetadata* pTrackMetadata,\n+        const TagLib::File& file) {\n+    if (kDebugMetadata) {\n+        qDebug() << \"Parsing\" << file.name();\n+    }\n+\n+    if (file.isValid()) {\n+        const TagLib::AudioProperties *properties = file.audioProperties();\n+        if (properties) {\n+            pTrackMetadata->setChannels(properties->channels());\n+            pTrackMetadata->setSampleRate(properties->sampleRate());\n+            pTrackMetadata->setDuration(properties->length());\n+            pTrackMetadata->setBitrate(properties->bitrate());\n+\n+            if (kDebugMetadata) {\n+                qDebug() << \"TagLib\" << \"channels\"\n+                        << pTrackMetadata->getChannels() << \"sampleRate\"\n+                        << pTrackMetadata->getSampleRate() << \"bitrate\"\n+                        << pTrackMetadata->getBitrate() << \"duration\"\n+                        << pTrackMetadata->getDuration();\n+            }\n+\n+            return true;\n+        }\n+    }\n+    return false;\n+}\n+\n+void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) {\n+    pTrackMetadata->setTitle(toQString(tag.title()));\n+    pTrackMetadata->setArtist(toQString(tag.artist()));\n+    pTrackMetadata->setAlbum(toQString(tag.album()));\n+    pTrackMetadata->setComment(toQString(tag.comment()));\n+    pTrackMetadata->setGenre(toQString(tag.genre()));\n+\n+    int iYear = tag.year();\n+    if (iYear > 0) {\n+        pTrackMetadata->setYear(QString(\"%1\").arg(iYear));\n+    }\n+\n+    int iTrack = tag.track();\n+    if (iTrack > 0) {\n+        pTrackMetadata->setTrackNumber(QString(\"%1\").arg(iTrack));\n+    }\n+\n+    if (kDebugMetadata) {\n+        qDebug() << \"TagLib\" << \"title\" << pTrackMetadata->getTitle()\n+                << \"artist\" << pTrackMetadata->getArtist() << \"album\"\n+                << pTrackMetadata->getAlbum() << \"comment\"\n+                << pTrackMetadata->getComment() << \"genre\"\n+                << pTrackMetadata->getGenre() << \"year\"\n+                << pTrackMetadata->getYear() << \"trackNumber\"\n+                << pTrackMetadata->getTrackNumber();\n+    }\n+}\n+\n+void readID3v2Tag(TrackMetadata* pTrackMetadata,\n+        const TagLib::ID3v2::Tag& tag) {\n+\n+    // Print every frame in the file.\n+    if (kDebugMetadata) {\n+        for (TagLib::ID3v2::FrameList::ConstIterator it(\n+                tag.frameList().begin()); it != tag.frameList().end(); ++it) {\n+            qDebug() << \"ID3V2\" << (*it)->frameID().data() << \"-\"\n+                    << toQString((*it)->toString());\n+        }\n+    }\n+\n+    readTag(pTrackMetadata, tag);\n+\n+    const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()[\"TBPM\"]);\n+    if (!bpmFrame.isEmpty()) {\n+        pTrackMetadata->setBpmString(toQStringFirst(bpmFrame));\n+    }\n+\n+    const TagLib::ID3v2::FrameList keyFrame(tag.frameListMap()[\"TKEY\"]);\n+    if (!keyFrame.isEmpty()) {\n+        pTrackMetadata->setKey(toQStringFirst(keyFrame));\n+    }\n+\n+    // Foobar2000-style ID3v2.3.0 tags\n+    // TODO: Check if everything is ok.\n+    TagLib::ID3v2::FrameList textFrames(tag.frameListMap()[\"TXXX\"]);\n+    for (TagLib::ID3v2::FrameList::ConstIterator it(textFrames.begin());\n+            it != textFrames.end(); ++it) {\n+        TagLib::ID3v2::UserTextIdentificationFrame* replaygainFrame =\n+                dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(*it);\n+        if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) {\n+            const QString desc(\n+                    toQString(replaygainFrame->description()).toLower());\n+            if (desc == \"replaygain_album_gain\") {\n+                pTrackMetadata->setReplayGainDbString(\n+                        toQString(replaygainFrame->fieldList()[1]));\n+            }\n+            // Prefer track gain over album gain.\n+            if (desc == \"replaygain_track_gain\") {\n+                pTrackMetadata->setReplayGainDbString(\n+                        toQString(replaygainFrame->fieldList()[1]));\n+            }\n+        }\n+    }\n+\n+    const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()[\"TPE2\"]);\n+    if (!albumArtistFrame.isEmpty()) {\n+        pTrackMetadata->setAlbumArtist(toQStringFirst(albumArtistFrame));\n+    }\n+\n+    if (pTrackMetadata->getAlbum().isEmpty()) {\n+        const TagLib::ID3v2::FrameList originalAlbumFrame(\n+                tag.frameListMap()[\"TOAL\"]);\n+        pTrackMetadata->setAlbum(toQStringFirst(originalAlbumFrame));\n+    }\n+\n+    const TagLib::ID3v2::FrameList composerFrame(tag.frameListMap()[\"TCOM\"]);\n+    if (!composerFrame.isEmpty()) {\n+        pTrackMetadata->setComposer(toQStringFirst(composerFrame));\n+    }\n+\n+    const TagLib::ID3v2::FrameList groupingFrame(tag.frameListMap()[\"TIT1\"]);\n+    if (!groupingFrame.isEmpty()) {\n+        pTrackMetadata->setGrouping(toQStringFirst(groupingFrame));\n+    }\n+\n+    // ID3v2.4.0: TDRC replaces TYER + TDAT\n+    const TagLib::ID3v2::FrameList recordingDateFrame(\n+            tag.frameListMap()[\"TDRC\"]);\n+    if (!recordingDateFrame.isEmpty()) {\n+        pTrackMetadata->setYear(toQStringFirst(recordingDateFrame));\n+    }\n+}\n+\n+void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) {\n+    if (kDebugMetadata) {\n+        for (TagLib::APE::ItemListMap::ConstIterator it(\n+                tag.itemListMap().begin()); it != tag.itemListMap().end();\n+                ++it) {\n+            qDebug() << \"APE\" << toQString((*it).first) << \"-\"\n+                    << toQString((*it).second.toString());\n+        }\n+    }\n+\n+    readTag(pTrackMetadata, tag);\n+\n+    if (tag.itemListMap().contains(\"BPM\")) {\n+        pTrackMetadata->setBpmString(toQString(tag.itemListMap()[\"BPM\"]));\n+    }\n+\n+    if (tag.itemListMap().contains(\"REPLAYGAIN_ALBUM_GAIN\")) {\n+        pTrackMetadata->setReplayGainDbString(\n+                toQString(tag.itemListMap()[\"REPLAYGAIN_ALBUM_GAIN\"]));\n+    }\n+    //Prefer track gain over album gain.\n+    if (tag.itemListMap().contains(\"REPLAYGAIN_TRACK_GAIN\")) {\n+        pTrackMetadata->setReplayGainDbString(\n+                toQString(tag.itemListMap()[\"REPLAYGAIN_TRACK_GAIN\"]));\n+    }\n+\n+    if (tag.itemListMap().contains(\"Album Artist\")) {\n+        pTrackMetadata->setAlbumArtist(\n+                toQString(tag.itemListMap()[\"Album Artist\"]));\n+    }\n+\n+    if (tag.itemListMap().contains(\"Composer\")) {\n+        pTrackMetadata->setComposer(toQString(tag.itemListMap()[\"Composer\"]));\n+    }\n+\n+    if (tag.itemListMap().contains(\"Grouping\")) {\n+        pTrackMetadata->setGrouping(toQString(tag.itemListMap()[\"Grouping\"]));\n+    }\n+\n+    if (tag.itemListMap().contains(\"Year\")) {\n+        pTrackMetadata->setYear(toQString(tag.itemListMap()[\"Year\"]));\n+    }\n+}\n+\n+void readXiphComment(TrackMetadata* pTrackMetadata,\n+        const TagLib::Ogg::XiphComment& tag) {\n+    if (kDebugMetadata) {\n+        for (TagLib::Ogg::FieldListMap::ConstIterator it(\n+                tag.fieldListMap().begin()); it != tag.fieldListMap().end();\n+                ++it) {\n+            qDebug() << \"XIPH\" << toQString((*it).first) << \"-\"\n+                    << toQString((*it).second.toString());\n+        }\n+    }\n+\n+    readTag(pTrackMetadata, tag);\n+\n+    // Some applications (like puddletag up to version 1.0.5) write COMMENT\n+    // instead DESCRIPTION. If the comment field (correctly populated by TagLib\n+    // from DESCRIPTION) is still empty we will additionally read this field.\n+    // Reference: http://www.xiph.org/vorbis/doc/v-comment.html\n+    if (pTrackMetadata->getComment().isEmpty()\n+            && tag.fieldListMap().contains(\"COMMENT\")) {\n+        pTrackMetadata->setComment(\n+                toQStringFirst(tag.fieldListMap()[\"COMMENT\"]));\n+    }\n+\n+    // Some tags use \"BPM\" so check for that.\n+    if (tag.fieldListMap().contains(\"BPM\")) {\n+        pTrackMetadata->setBpmString(toQStringFirst(tag.fieldListMap()[\"BPM\"]));\n+    }\n+\n+    // Give preference to the \"TEMPO\" tag which seems to be more standard\n+    if (tag.fieldListMap().contains(\"TEMPO\")) {\n+        pTrackMetadata->setBpmString(\n+                toQStringFirst(tag.fieldListMap()[\"TEMPO\"]));\n+    }\n+\n+    if (tag.fieldListMap().contains(\"REPLAYGAIN_ALBUM_GAIN\")) {\n+        pTrackMetadata->setReplayGainDbString(\n+                toQStringFirst(tag.fieldListMap()[\"REPLAYGAIN_ALBUM_GAIN\"]));\n+    }\n+    //Prefer track gain over album gain.\n+    if (tag.fieldListMap().contains(\"REPLAYGAIN_TRACK_GAIN\")) {\n+        pTrackMetadata->setReplayGainDbString(\n+                toQStringFirst(tag.fieldListMap()[\"REPLAYGAIN_TRACK_GAIN\"]));\n+    }\n+\n+    /*\n+     * Reading key code information\n+     * Unlike, ID3 tags, there's no standard or recommendation on how to store 'key' code\n+     *\n+     * Luckily, there are only a few tools for that, e.g., Rapid Evolution (RE).\n+     * Assuming no distinction between start and end key, RE uses a \"INITIALKEY\"\n+     * or a \"KEY\" vorbis comment.\n+     */\n+    if (tag.fieldListMap().contains(\"KEY\")) {\n+        pTrackMetadata->setKey(toQStringFirst(tag.fieldListMap()[\"KEY\"]));\n+    }\n+    if (pTrackMetadata->getKey().isEmpty()\n+            && tag.fieldListMap().contains(\"INITIALKEY\")) {\n+        // try alternative field name\n+        pTrackMetadata->setKey(\n+                toQStringFirst(tag.fieldListMap()[\"INITIALKEY\"]));\n+    }\n+\n+    if (tag.fieldListMap().contains(\"ALBUMARTIST\")) {\n+        pTrackMetadata->setAlbumArtist(\n+                toQStringFirst(tag.fieldListMap()[\"ALBUMARTIST\"]));\n+    }\n+    if (pTrackMetadata->getAlbumArtist().isEmpty()\n+            && tag.fieldListMap().contains(\"ALBUM_ARTIST\")) {\n+        // try alternative field name\n+        pTrackMetadata->setAlbumArtist(\n+                toQStringFirst(tag.fieldListMap()[\"ALBUM_ARTIST\"]));\n+    }\n+    if (pTrackMetadata->getAlbumArtist().isEmpty()\n+            && tag.fieldListMap().contains(\"ALBUM ARTIST\")) {\n+        // try alternative field name\n+        pTrackMetadata->setAlbumArtist(\n+                toQStringFirst(tag.fieldListMap()[\"ALBUM ARTIST\"]));\n+    }\n+\n+    if (tag.fieldListMap().contains(\"COMPOSER\")) {\n+        pTrackMetadata->setComposer(\n+                toQStringFirst(tag.fieldListMap()[\"COMPOSER\"]));\n+    }\n+\n+    if (tag.fieldListMap().contains(\"GROUPING\")) {\n+        pTrackMetadata->setGrouping(\n+                toQStringFirst(tag.fieldListMap()[\"GROUPING\"]));\n+    }\n+\n+    if (tag.fieldListMap().contains(\"DATE\")) {\n+        pTrackMetadata->setYear(toQStringFirst(tag.fieldListMap()[\"DATE\"]));\n+    }\n+}\n+\n+void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) {\n+    if (kDebugMetadata) {\n+        for (TagLib::MP4::ItemListMap::ConstIterator it(\n+                tag.itemListMap().begin()); it != tag.itemListMap().end();\n+                ++it) {\n+            qDebug() << \"MP4\" << toQString((*it).first) << \"-\"\n+                    << toQStringFirst((*it).second);\n+        }\n+    }\n+\n+    readTag(pTrackMetadata, tag);\n+\n+    // Get BPM\n+    if (tag.itemListMap().contains(\"tmpo\")) {\n+        // Read the BPM as an integer value.\n+        pTrackMetadata->setBpm(tag.itemListMap()[\"tmpo\"].toInt());\n+    }\n+    if (tag.itemListMap().contains(\"----:com.apple.iTunes:BPM\")) {\n+        // This is the preferred field for storing the BPM\n+        // with fractional digits as a floating-point value.\n+        // If this field contains a valid value the integer\n+        // BPM value that might have been read before is\n+        // overwritten.\n+        pTrackMetadata->setBpmString(\n+                toQStringFirst(tag.itemListMap()[\"----:com.apple.iTunes:BPM\"]));\n+    }\n+\n+    // Get Album Artist\n+    if (tag.itemListMap().contains(\"aART\")) {\n+        pTrackMetadata->setAlbumArtist(\n+                toQStringFirst(tag.itemListMap()[\"aART\"]));\n+    }\n+\n+    // Get Composer\n+    if (tag.itemListMap().contains(\"\\251wrt\")) {\n+        pTrackMetadata->setComposer(\n+                toQStringFirst(tag.itemListMap()[\"\\251wrt\"]));\n+    }\n+\n+    // Get Grouping\n+    if (tag.itemListMap().contains(\"\\251grp\")) {\n+        pTrackMetadata->setGrouping(\n+                toQStringFirst(tag.itemListMap()[\"\\251grp\"]));\n+    }\n+\n+    // Get date/year as string\n+    if (tag.itemListMap().contains(\"\\251day\")) {\n+        pTrackMetadata->setYear(toQStringFirst(tag.itemListMap()[\"\\251day\"]));\n+    }\n+\n+    // Get KEY (conforms to Rapid Evolution)\n+    if (tag.itemListMap().contains(\"----:com.apple.iTunes:KEY\")) {\n+        pTrackMetadata->setKey(\n+                toQStringFirst(tag.itemListMap()[\"----:com.apple.iTunes:KEY\"]));\n+    }\n+\n+    // Apparently iTunes stores replaygain in this property.\n+    if (tag.itemListMap().contains(\n+            \"----:com.apple.iTunes:replaygain_album_gain\")) {\n+        // TODO(XXX) find tracks with this property and check what it looks\n+    }\n+    //Prefer track gain over album gain.\n+    if (tag.itemListMap().contains(\n+            \"----:com.apple.iTunes:replaygain_track_gain\")) {\n+        // TODO(XXX) find tracks with this property and check what it looks\n+    }\n+}\n+\n+QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag) {\n+    QImage coverArt;\n+    TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()[\"APIC\"];\n+    if (!covertArtFrame.isEmpty()) {\n+        TagLib::ID3v2::AttachedPictureFrame* picframe =\n+                static_cast<TagLib::ID3v2::AttachedPictureFrame*>(covertArtFrame.front());\n+        TagLib::ByteVector data = picframe->picture();\n+        coverArt = QImage::fromData(\n+                reinterpret_cast<const uchar *>(data.data()), data.size());\n+    }\n+    return coverArt;\n+}\n+\n+QImage readAPETagCover(const TagLib::APE::Tag& tag) {\n+    QImage coverArt;\n+    if (tag.itemListMap().contains(\"COVER ART (FRONT)\")) {\n+        const TagLib::ByteVector nullStringTerminator(1, 0);\n+        TagLib::ByteVector item =\n+                tag.itemListMap()[\"COVER ART (FRONT)\"].value();\n+        int pos = item.find(nullStringTerminator);  // skip the filename\n+        if (++pos > 0) {\n+            const TagLib::ByteVector& data = item.mid(pos);\n+            coverArt = QImage::fromData(\n+                    reinterpret_cast<const uchar *>(data.data()), data.size());\n+        }\n+    }\n+    return coverArt;\n+}\n+\n+QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag) {\n+    QImage coverArt;\n+    if (tag.fieldListMap().contains(\"METADATA_BLOCK_PICTURE\")) {\n+        QByteArray data(\n+                QByteArray::fromBase64(\n+                        tag.fieldListMap()[\"METADATA_BLOCK_PICTURE\"].front().toCString()));\n+        TagLib::ByteVector tdata(data.data(), data.size());\n+        TagLib::FLAC::Picture p(tdata);\n+        data = QByteArray(p.data().data(), p.data().size());\n+        coverArt = QImage::fromData(data);\n+    } else if (tag.fieldListMap().contains(\"COVERART\")) {\n+        QByteArray data(\n+                QByteArray::fromBase64(\n+                        tag.fieldListMap()[\"COVERART\"].toString().toCString()));\n+        coverArt = QImage::fromData(data);\n+    }\n+    return coverArt;\n+}\n+\n+QImage readMP4TagCover(/*const*/TagLib::MP4::Tag& tag) {\n+    QImage coverArt;\n+    if (tag.itemListMap().contains(\"covr\")) {\n+        TagLib::MP4::CoverArtList coverArtList =\n+                tag.itemListMap()[\"covr\"].toCoverArtList();\n+        TagLib::ByteVector data = coverArtList.front().data();\n+        coverArt = QImage::fromData(\n+                reinterpret_cast<const uchar *>(data.data()), data.size());\n+    }\n+    return coverArt;\n+}\n+\n+namespace {\n+\n+void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) {\n+    pTag->removeFrames(pFrame->frameID());\n+    pTag->addFrame(pFrame);\n+}\n+\n+void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag,\n+        const TagLib::ByteVector &id, const QString& text) {\n+    TagLib::String::Type textType;\n+    QByteArray textData;\n+    if (4 <= pTag->header()->majorVersion()) {\n+        // prefer UTF-8 for ID3v2.4.0 or higher\n+        textType = TagLib::String::UTF8;\n+        textData = text.toUtf8();\n+    } else {\n+        textType = TagLib::String::Latin1;\n+        textData = text.toLatin1();\n+    }\n+    QScopedPointer<TagLib::ID3v2::TextIdentificationFrame> pNewFrame(\n+            new TagLib::ID3v2::TextIdentificationFrame(id, textType));\n+    pNewFrame->setText(toTagLibString(text));\n+    replaceID3v2Frame(pTag, pNewFrame.data());\n+    pNewFrame.take(); // release ownership\n+}\n+\n+} // anonymous namespace\n+\n+bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) {\n+    if (NULL == pTag) {\n+        return false;\n+    }\n+\n+    pTag->setArtist(toTagLibString(trackMetadata.getArtist()));\n+    pTag->setTitle(toTagLibString(trackMetadata.getTitle()));\n+    pTag->setAlbum(toTagLibString(trackMetadata.getAlbum()));\n+    pTag->setGenre(toTagLibString(trackMetadata.getGenre()));\n+    pTag->setComment(toTagLibString(trackMetadata.getComment()));\n+    bool yearValid = false;\n+    uint year = trackMetadata.getYear().toUInt(&yearValid);","path":"src/metadata/trackmetadatataglib.cpp","position":537,"original_position":537,"commit_id":"d7b834e704e57570768e39d7b6dafa5edda01fc8","original_commit_id":"d7b834e704e57570768e39d7b6dafa5edda01fc8","user":{"login":"daschuer","id":1777442,"avatar_url":"https://avatars.githubusercontent.com/u/1777442?v=3","gravatar_id":"","url":"https://api.github.com/users/daschuer","html_url":"https://github.com/daschuer","followers_url":"https://api.github.com/users/daschuer/followers","following_url":"https://api.github.com/users/daschuer/following{/other_user}","gists_url":"https://api.github.com/users/daschuer/gists{/gist_id}","starred_url":"https://api.github.com/users/daschuer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/daschuer/subscriptions","organizations_url":"https://api.github.com/users/daschuer/orgs","repos_url":"https://api.github.com/users/daschuer/repos","events_url":"https://api.github.com/users/daschuer/events{/privacy}","received_events_url":"https://api.github.com/users/daschuer/received_events","type":"User","site_admin":false},"body":"Enable Date format support? ","created_at":"2015-01-27T16:14:25Z","updated_at":"2015-01-27T16:14:25Z","html_url":"https://github.com/mixxxdj/mixxx/pull/411#discussion_r23619332","pull_request_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411","_links":{"self":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/comments/23619332"},"html":{"href":"https://github.com/mixxxdj/mixxx/pull/411#discussion_r23619332"},"pull_request":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411"}}},"pull_request":{"url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411","id":25228389,"html_url":"https://github.com/mixxxdj/mixxx/pull/411","diff_url":"https://github.com/mixxxdj/mixxx/pull/411.diff","patch_url":"https://github.com/mixxxdj/mixxx/pull/411.patch","issue_url":"https://api.github.com/repos/mixxxdj/mixxx/issues/411","number":411,"state":"open","locked":false,"title":"New SoundSource/AudioSource API","user":{"login":"uklotzde","id":1474154,"avatar_url":"https://avatars.githubusercontent.com/u/1474154?v=3","gravatar_id":"","url":"https://api.github.com/users/uklotzde","html_url":"https://github.com/uklotzde","followers_url":"https://api.github.com/users/uklotzde/followers","following_url":"https://api.github.com/users/uklotzde/following{/other_user}","gists_url":"https://api.github.com/users/uklotzde/gists{/gist_id}","starred_url":"https://api.github.com/users/uklotzde/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/uklotzde/subscriptions","organizations_url":"https://api.github.com/users/uklotzde/orgs","repos_url":"https://api.github.com/users/uklotzde/repos","events_url":"https://api.github.com/users/uklotzde/events{/privacy}","received_events_url":"https://api.github.com/users/uklotzde/received_events","type":"User","site_admin":false},"body":"This is the first step towards a better/cleaner SoundSource API.\r\nThe operations for reading audio data have been moved into the\r\nnew abstract base class AudioSource.\r\n\r\nCurrently SoundSource inherits from AudioSource for technical reasons.\r\nThis is too much much coupling that needs to be broken up. I have\r\nalready some ideas on how to resolve it, but first this work needs to\r\nbe integrated.\r\n\r\nDuring the refactoring I've noticed, that the quality and maturity of the\r\naudio decoders for different file types greatly varies:\r\n\r\nGood/mature:\r\n- FLAC\r\n- Ogg\r\n- Opus\r\n- WavPack\r\n- libsndfile\r\n\r\nMediocre:\r\n- MP3\r\n\r\nUgly/inefficient:\r\n- M4A (2014-12-04: Code has been rewritten without legacy code borrowed from other projects)\r\n- ModPlug\r\n\r\nUntested AudioSource API:\r\n- Apple CoreAudio\r\n- Windows Media Foundation\r\n\r\nUnfinished AudioSource API (just a hack):\r\n- FFmpeg\r\n\r\nEspecially the code for the M4A decoder should be revised and cleaned up as\r\nsoon as possible, followed by a review of the MP3 decoder. Reading audio\r\ndata must work as stable and efficient as possible. This has priority over\r\nthe improvement of the internal software architecture!","created_at":"2014-11-29T23:03:10Z","updated_at":"2015-01-27T16:14:25Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4f4f84d0687b0bc0fa133ff7aeb04a3122137e9a","assignee":null,"milestone":{"url":"https://api.github.com/repos/mixxxdj/mixxx/milestones/2","labels_url":"https://api.github.com/repos/mixxxdj/mixxx/milestones/2/labels","id":754553,"number":2,"title":"1.13","description":"","creator":{"login":"ywwg","id":888940,"avatar_url":"https://avatars.githubusercontent.com/u/888940?v=3","gravatar_id":"","url":"https://api.github.com/users/ywwg","html_url":"https://github.com/ywwg","followers_url":"https://api.github.com/users/ywwg/followers","following_url":"https://api.github.com/users/ywwg/following{/other_user}","gists_url":"https://api.github.com/users/ywwg/gists{/gist_id}","starred_url":"https://api.github.com/users/ywwg/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ywwg/subscriptions","organizations_url":"https://api.github.com/users/ywwg/orgs","repos_url":"https://api.github.com/users/ywwg/repos","events_url":"https://api.github.com/users/ywwg/events{/privacy}","received_events_url":"https://api.github.com/users/ywwg/received_events","type":"User","site_admin":false},"open_issues":8,"closed_issues":4,"state":"open","created_at":"2014-08-14T23:06:10Z","updated_at":"2014-11-29T23:05:51Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411/commits","review_comments_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411/comments","review_comment_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/comments/{number}","comments_url":"https://api.github.com/repos/mixxxdj/mixxx/issues/411/comments","statuses_url":"https://api.github.com/repos/mixxxdj/mixxx/statuses/d7b834e704e57570768e39d7b6dafa5edda01fc8","head":{"label":"uklotzde:NewSoundSourceAPI","ref":"NewSoundSourceAPI","sha":"d7b834e704e57570768e39d7b6dafa5edda01fc8","user":{"login":"uklotzde","id":1474154,"avatar_url":"https://avatars.githubusercontent.com/u/1474154?v=3","gravatar_id":"","url":"https://api.github.com/users/uklotzde","html_url":"https://github.com/uklotzde","followers_url":"https://api.github.com/users/uklotzde/followers","following_url":"https://api.github.com/users/uklotzde/following{/other_user}","gists_url":"https://api.github.com/users/uklotzde/gists{/gist_id}","starred_url":"https://api.github.com/users/uklotzde/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/uklotzde/subscriptions","organizations_url":"https://api.github.com/users/uklotzde/orgs","repos_url":"https://api.github.com/users/uklotzde/repos","events_url":"https://api.github.com/users/uklotzde/events{/privacy}","received_events_url":"https://api.github.com/users/uklotzde/received_events","type":"User","site_admin":false},"repo":{"id":11108787,"name":"mixxx","full_name":"uklotzde/mixxx","owner":{"login":"uklotzde","id":1474154,"avatar_url":"https://avatars.githubusercontent.com/u/1474154?v=3","gravatar_id":"","url":"https://api.github.com/users/uklotzde","html_url":"https://github.com/uklotzde","followers_url":"https://api.github.com/users/uklotzde/followers","following_url":"https://api.github.com/users/uklotzde/following{/other_user}","gists_url":"https://api.github.com/users/uklotzde/gists{/gist_id}","starred_url":"https://api.github.com/users/uklotzde/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/uklotzde/subscriptions","organizations_url":"https://api.github.com/users/uklotzde/orgs","repos_url":"https://api.github.com/users/uklotzde/repos","events_url":"https://api.github.com/users/uklotzde/events{/privacy}","received_events_url":"https://api.github.com/users/uklotzde/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/uklotzde/mixxx","description":"Mixxx is Free DJ software that gives you everything you need to perform live mixes.","fork":true,"url":"https://api.github.com/repos/uklotzde/mixxx","forks_url":"https://api.github.com/repos/uklotzde/mixxx/forks","keys_url":"https://api.github.com/repos/uklotzde/mixxx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/uklotzde/mixxx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/uklotzde/mixxx/teams","hooks_url":"https://api.github.com/repos/uklotzde/mixxx/hooks","issue_events_url":"https://api.github.com/repos/uklotzde/mixxx/issues/events{/number}","events_url":"https://api.github.com/repos/uklotzde/mixxx/events","assignees_url":"https://api.github.com/repos/uklotzde/mixxx/assignees{/user}","branches_url":"https://api.github.com/repos/uklotzde/mixxx/branches{/branch}","tags_url":"https://api.github.com/repos/uklotzde/mixxx/tags","blobs_url":"https://api.github.com/repos/uklotzde/mixxx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/uklotzde/mixxx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/uklotzde/mixxx/git/refs{/sha}","trees_url":"https://api.github.com/repos/uklotzde/mixxx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/uklotzde/mixxx/statuses/{sha}","languages_url":"https://api.github.com/repos/uklotzde/mixxx/languages","stargazers_url":"https://api.github.com/repos/uklotzde/mixxx/stargazers","contributors_url":"https://api.github.com/repos/uklotzde/mixxx/contributors","subscribers_url":"https://api.github.com/repos/uklotzde/mixxx/subscribers","subscription_url":"https://api.github.com/repos/uklotzde/mixxx/subscription","commits_url":"https://api.github.com/repos/uklotzde/mixxx/commits{/sha}","git_commits_url":"https://api.github.com/repos/uklotzde/mixxx/git/commits{/sha}","comments_url":"https://api.github.com/repos/uklotzde/mixxx/comments{/number}","issue_comment_url":"https://api.github.com/repos/uklotzde/mixxx/issues/comments/{number}","contents_url":"https://api.github.com/repos/uklotzde/mixxx/contents/{+path}","compare_url":"https://api.github.com/repos/uklotzde/mixxx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/uklotzde/mixxx/merges","archive_url":"https://api.github.com/repos/uklotzde/mixxx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/uklotzde/mixxx/downloads","issues_url":"https://api.github.com/repos/uklotzde/mixxx/issues{/number}","pulls_url":"https://api.github.com/repos/uklotzde/mixxx/pulls{/number}","milestones_url":"https://api.github.com/repos/uklotzde/mixxx/milestones{/number}","notifications_url":"https://api.github.com/repos/uklotzde/mixxx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/uklotzde/mixxx/labels{/name}","releases_url":"https://api.github.com/repos/uklotzde/mixxx/releases{/id}","created_at":"2013-07-01T22:22:18Z","updated_at":"2015-01-12T23:41:03Z","pushed_at":"2015-01-26T23:19:01Z","git_url":"git://github.com/uklotzde/mixxx.git","ssh_url":"git@github.com:uklotzde/mixxx.git","clone_url":"https://github.com/uklotzde/mixxx.git","svn_url":"https://github.com/uklotzde/mixxx","homepage":"http://mixxx.org","size":237449,"stargazers_count":0,"watchers_count":0,"language":"C++","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"mixxxdj:master","ref":"master","sha":"060f6b083ed493b88e50a477a94a3fdc36dac48a","user":{"login":"mixxxdj","id":1743769,"avatar_url":"https://avatars.githubusercontent.com/u/1743769?v=3","gravatar_id":"","url":"https://api.github.com/users/mixxxdj","html_url":"https://github.com/mixxxdj","followers_url":"https://api.github.com/users/mixxxdj/followers","following_url":"https://api.github.com/users/mixxxdj/following{/other_user}","gists_url":"https://api.github.com/users/mixxxdj/gists{/gist_id}","starred_url":"https://api.github.com/users/mixxxdj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mixxxdj/subscriptions","organizations_url":"https://api.github.com/users/mixxxdj/orgs","repos_url":"https://api.github.com/users/mixxxdj/repos","events_url":"https://api.github.com/users/mixxxdj/events{/privacy}","received_events_url":"https://api.github.com/users/mixxxdj/received_events","type":"Organization","site_admin":false},"repo":{"id":10126031,"name":"mixxx","full_name":"mixxxdj/mixxx","owner":{"login":"mixxxdj","id":1743769,"avatar_url":"https://avatars.githubusercontent.com/u/1743769?v=3","gravatar_id":"","url":"https://api.github.com/users/mixxxdj","html_url":"https://github.com/mixxxdj","followers_url":"https://api.github.com/users/mixxxdj/followers","following_url":"https://api.github.com/users/mixxxdj/following{/other_user}","gists_url":"https://api.github.com/users/mixxxdj/gists{/gist_id}","starred_url":"https://api.github.com/users/mixxxdj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mixxxdj/subscriptions","organizations_url":"https://api.github.com/users/mixxxdj/orgs","repos_url":"https://api.github.com/users/mixxxdj/repos","events_url":"https://api.github.com/users/mixxxdj/events{/privacy}","received_events_url":"https://api.github.com/users/mixxxdj/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mixxxdj/mixxx","description":"Mixxx is Free DJ software that gives you everything you need to perform live mixes.","fork":false,"url":"https://api.github.com/repos/mixxxdj/mixxx","forks_url":"https://api.github.com/repos/mixxxdj/mixxx/forks","keys_url":"https://api.github.com/repos/mixxxdj/mixxx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mixxxdj/mixxx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mixxxdj/mixxx/teams","hooks_url":"https://api.github.com/repos/mixxxdj/mixxx/hooks","issue_events_url":"https://api.github.com/repos/mixxxdj/mixxx/issues/events{/number}","events_url":"https://api.github.com/repos/mixxxdj/mixxx/events","assignees_url":"https://api.github.com/repos/mixxxdj/mixxx/assignees{/user}","branches_url":"https://api.github.com/repos/mixxxdj/mixxx/branches{/branch}","tags_url":"https://api.github.com/repos/mixxxdj/mixxx/tags","blobs_url":"https://api.github.com/repos/mixxxdj/mixxx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mixxxdj/mixxx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mixxxdj/mixxx/git/refs{/sha}","trees_url":"https://api.github.com/repos/mixxxdj/mixxx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mixxxdj/mixxx/statuses/{sha}","languages_url":"https://api.github.com/repos/mixxxdj/mixxx/languages","stargazers_url":"https://api.github.com/repos/mixxxdj/mixxx/stargazers","contributors_url":"https://api.github.com/repos/mixxxdj/mixxx/contributors","subscribers_url":"https://api.github.com/repos/mixxxdj/mixxx/subscribers","subscription_url":"https://api.github.com/repos/mixxxdj/mixxx/subscription","commits_url":"https://api.github.com/repos/mixxxdj/mixxx/commits{/sha}","git_commits_url":"https://api.github.com/repos/mixxxdj/mixxx/git/commits{/sha}","comments_url":"https://api.github.com/repos/mixxxdj/mixxx/comments{/number}","issue_comment_url":"https://api.github.com/repos/mixxxdj/mixxx/issues/comments/{number}","contents_url":"https://api.github.com/repos/mixxxdj/mixxx/contents/{+path}","compare_url":"https://api.github.com/repos/mixxxdj/mixxx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mixxxdj/mixxx/merges","archive_url":"https://api.github.com/repos/mixxxdj/mixxx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mixxxdj/mixxx/downloads","issues_url":"https://api.github.com/repos/mixxxdj/mixxx/issues{/number}","pulls_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls{/number}","milestones_url":"https://api.github.com/repos/mixxxdj/mixxx/milestones{/number}","notifications_url":"https://api.github.com/repos/mixxxdj/mixxx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mixxxdj/mixxx/labels{/name}","releases_url":"https://api.github.com/repos/mixxxdj/mixxx/releases{/id}","created_at":"2013-05-17T15:14:38Z","updated_at":"2015-01-26T15:30:51Z","pushed_at":"2015-01-26T15:30:51Z","git_url":"git://github.com/mixxxdj/mixxx.git","ssh_url":"git@github.com:mixxxdj/mixxx.git","clone_url":"https://github.com/mixxxdj/mixxx.git","svn_url":"https://github.com/mixxxdj/mixxx","homepage":"http://mixxx.org","size":311129,"stargazers_count":227,"watchers_count":227,"language":"C++","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":188,"mirror_url":null,"open_issues_count":34,"forks":188,"open_issues":34,"watchers":227,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411"},"html":{"href":"https://github.com/mixxxdj/mixxx/pull/411"},"issue":{"href":"https://api.github.com/repos/mixxxdj/mixxx/issues/411"},"comments":{"href":"https://api.github.com/repos/mixxxdj/mixxx/issues/411/comments"},"review_comments":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411/comments"},"review_comment":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411/commits"},"statuses":{"href":"https://api.github.com/repos/mixxxdj/mixxx/statuses/d7b834e704e57570768e39d7b6dafa5edda01fc8"}}}}