From d4d7f19fab090d944900462a629810164e49ea86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bara?= Date: Wed, 5 Jan 2011 20:23:53 +0000 Subject: [PATCH] better .cue spec conformity (allow many files in one sheet, use the SONGWRITER field as song's composer, ignore data files) --- src/playlistparsers/cueparser.cpp | 232 +++++++++++++++++------------- src/playlistparsers/cueparser.h | 9 +- tests/cueparser_test.cpp | 185 +++++++++++++++++++++++- tests/data/fullmetadata.cue | 13 ++ tests/data/manyfiles.cue | 25 ++++ tests/data/manyfilesbroken.cue | 33 +++++ tests/data/testdata.qrc | 24 ++-- tests/data/withdatafiles.cue | 28 ++++ 8 files changed, 438 insertions(+), 111 deletions(-) create mode 100644 tests/data/fullmetadata.cue create mode 100644 tests/data/manyfiles.cue create mode 100644 tests/data/manyfilesbroken.cue create mode 100644 tests/data/withdatafiles.cue diff --git a/src/playlistparsers/cueparser.cpp b/src/playlistparsers/cueparser.cpp index d22f79828..31e916444 100644 --- a/src/playlistparsers/cueparser.cpp +++ b/src/playlistparsers/cueparser.cpp @@ -29,6 +29,7 @@ const char* CueParser::kIndexRegExp = "(\\d{2}):(\\d{2}):(\\d{2})"; const char* CueParser::kPerformer = "performer"; const char* CueParser::kTitle = "title"; +const char* CueParser::kSongWriter = "songwriter"; const char* CueParser::kFile = "file"; const char* CueParser::kTrack = "track"; const char* CueParser::kIndex = "index"; @@ -46,123 +47,155 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const { text_stream.setCodec(QTextCodec::codecForUtfText(device->peek(1024), QTextCodec::codecForName("UTF-8"))); QString dir_path = dir.absolutePath(); - - QString line; - - QString album_artist; - QString album; - QString file; - - // header - while (!(line = text_stream.readLine()).isNull()) { - QStringList splitted = SplitCueLine(line); - - // uninteresting or incorrect line - if(splitted.size() < 2) { - continue; - } - - QString line_name = splitted[0].toLower(); - QString line_value = splitted[1]; - - // PERFORMER - if(line_name == kPerformer) { - - album_artist = line_value; - - // TITLE - } else if(line_name == kTitle) { - - album = line_value; - - // FILE - } else if(line_name == kFile) { - - file = QDir::isAbsolutePath(line_value) - ? line_value - : dir.absoluteFilePath(line_value); - - // end of the header -> go into the track mode - } else if(line_name == kTrack) { - - break; - - } - - // just ignore the rest of possible field types for now... - } - - if(line.isNull()) { - qWarning() << "the .cue file from " << dir_path << " defines no tracks!"; - return ret; - } - - QString track_type; - QString index; - QString artist; - QString title; + // read the first line already + QString line = text_stream.readLine(); QList entries; + int files = 0; - // tracks - do { - QStringList splitted = SplitCueLine(line); + // -- whole file + while (!text_stream.atEnd()) { - // uninteresting or incorrect line - if(splitted.size() < 2) { - continue; + QString album_artist; + QString album; + QString album_composer; + QString file; + QString file_type; + + // -- FILE section + do { + QStringList splitted = SplitCueLine(line); + + // uninteresting or incorrect line + if(splitted.size() < 2) { + continue; + } + + QString line_name = splitted[0].toLower(); + QString line_value = splitted[1]; + + // PERFORMER + if(line_name == kPerformer) { + + album_artist = line_value; + + // TITLE + } else if(line_name == kTitle) { + + album = line_value; + + // SONGWRITER + } else if(line_name == kSongWriter) { + + album_composer = line_value; + + // FILE + } else if(line_name == kFile) { + + file = QDir::isAbsolutePath(line_value) + ? line_value + : dir.absoluteFilePath(line_value); + + if(splitted.size() > 2) { + file_type = splitted[2]; + } + + // end of the header -> go into the track mode + } else if(line_name == kTrack) { + + files++; + break; + + } + + // just ignore the rest of possible field types for now... + } while(!(line = text_stream.readLine()).isNull()); + + if(line.isNull()) { + qWarning() << "the .cue file from " << dir_path << " defines no tracks!"; + return ret; } - QString line_name = splitted[0].toLower(); - QString line_value = splitted[1]; - QString line_additional = splitted.size() > 2 ? splitted[2].toLower() : ""; + // if this is a data file, all of it's tracks will be ignored + bool valid_file = file_type.compare("BINARY", Qt::CaseInsensitive) && + file_type.compare("MOTOROLA", Qt::CaseInsensitive); - if(line_name == kTrack) { + QString track_type; + QString index; + QString artist; + QString composer; + QString title; - // the beginning of another track's definition - we're saving the current one - // for later (if it's valid of course) - if(!index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) { - entries.append(CueEntry(file, index, title, artist, album_artist, album)); + // TRACK section + do { + QStringList splitted = SplitCueLine(line); + + // uninteresting or incorrect line + if(splitted.size() < 2) { + continue; } - // clear the state - track_type = index = artist = title = ""; + QString line_name = splitted[0].toLower(); + QString line_value = splitted[1]; + QString line_additional = splitted.size() > 2 ? splitted[2].toLower() : ""; - if(!line_additional.isEmpty()) { - track_type = line_additional; - } + if(line_name == kTrack) { - } else if(line_name == kIndex) { + // the beginning of another track's definition - we're saving the current one + // for later (if it's valid of course) + // please note that the same code is repeated just after this 'do-while' loop + if(valid_file && !index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) { + entries.append(CueEntry(file, index, title, artist, album_artist, album, composer, album_composer)); + } - // we need the index's position field - if(!line_additional.isEmpty()) { + // clear the state + track_type = index = artist = title = ""; - // if there's none "01" index, we'll just take the first one - // also, we'll take the "01" index even if it's the last one - if(line_value == "01" || index.isEmpty()) { + if(!line_additional.isEmpty()) { + track_type = line_additional; + } - index = line_additional; + } else if(line_name == kIndex) { + + // we need the index's position field + if(!line_additional.isEmpty()) { + + // if there's none "01" index, we'll just take the first one + // also, we'll take the "01" index even if it's the last one + if(line_value == "01" || index.isEmpty()) { + + index = line_additional; + + } } + } else if(line_name == kPerformer) { + + artist = line_value; + + } else if(line_name == kTitle) { + + title = line_value; + + } else if(line_name == kSongWriter) { + + composer = line_value; + + // end of track's for the current file -> parse next one + } else if(line_name == kFile) { + + break; + } - } else if(line_name == kPerformer) { - - artist = line_value; - - } else if(line_name == kTitle) { - - title = line_value; + // just ignore the rest of possible field types for now... + } while(!(line = text_stream.readLine()).isNull()); + // we didn't add the last song yet... + if(valid_file && !index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) { + entries.append(CueEntry(file, index, title, artist, album_artist, album, composer, album_composer)); } - - // just ignore the rest of possible field types for now... - } while(!(line = text_stream.readLine()).isNull()); - - // we didn't add the last song yet... - if(!index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) { - entries.append(CueEntry(file, index, title, artist, album_artist, album)); } // finalize parsing songs @@ -181,7 +214,12 @@ SongList CueParser::Load(QIODevice* device, const QDir& dir) const { // overwrite the stuff, we may have read from the file or library, using // the current .cue metadata - song.set_track(i + 1); + + // set track number only in single-file mode + if(files == 1) { + song.set_track(i + 1); + } + if(i + 1 < entries.size()) { // incorrect indices? if(!UpdateSong(entry, entries.at(i + 1).index, &song)) { @@ -227,6 +265,7 @@ bool CueParser::UpdateSong(const CueEntry& entry, const QString& next_index, Son song->Init(entry.title, entry.PrettyArtist(), entry.album, beginning, end); song->set_albumartist(entry.album_artist); + song->set_composer(entry.PrettyComposer()); return true; } @@ -245,6 +284,7 @@ bool CueParser::UpdateLastSong(const CueEntry& entry, Song* song) const { song->set_artist(entry.PrettyArtist()); song->set_album(entry.album); song->set_albumartist(entry.album_artist); + song->set_composer(entry.PrettyComposer()); // we don't do anything with the end here because it's already set to // the end of the media file (if it exists) diff --git a/src/playlistparsers/cueparser.h b/src/playlistparsers/cueparser.h index d933a8069..7b99576ce 100644 --- a/src/playlistparsers/cueparser.h +++ b/src/playlistparsers/cueparser.h @@ -34,6 +34,7 @@ class CueParser : public ParserBase { static const char* kPerformer; static const char* kTitle; + static const char* kSongWriter; static const char* kFile; static const char* kTrack; static const char* kIndex; @@ -62,16 +63,22 @@ class CueParser : public ParserBase { QString album_artist; QString album; + QString composer; + QString album_composer; + QString PrettyArtist() const { return artist.isEmpty() ? album_artist : artist; } + QString PrettyComposer() const { return composer.isEmpty() ? album_composer : composer; } CueEntry(QString& file, QString& index, QString& title, QString& artist, - QString& album_artist, QString& album) { + QString& album_artist, QString& album, QString& composer, QString& album_composer) { this->file = file; this->index = index; this->title = title; this->artist = artist; this->album_artist = album_artist; this->album = album; + this->composer = composer; + this->album_composer = album_composer; } }; diff --git a/tests/cueparser_test.cpp b/tests/cueparser_test.cpp index 46e0f0368..078887c46 100644 --- a/tests/cueparser_test.cpp +++ b/tests/cueparser_test.cpp @@ -53,6 +53,7 @@ TEST_F(CueParserTest, ParsesASong) { ASSERT_EQ("Zucchero himself", first_song.albumartist()); ASSERT_EQ("", first_song.album()); ASSERT_EQ(1, first_song.beginning()); + ASSERT_EQ(1, first_song.track()); } TEST_F(CueParserTest, ParsesTwoSongs) { @@ -66,19 +67,22 @@ TEST_F(CueParserTest, ParsesTwoSongs) { // with the specified metadata Song first_song = song_list.at(0); + Song second_song = song_list.at(1); + ASSERT_EQ("Un soffio caldo", first_song.title()); ASSERT_EQ("Chocabeck", first_song.album()); ASSERT_EQ("Zucchero himself", first_song.artist()); ASSERT_EQ("Zucchero himself", first_song.albumartist()); ASSERT_EQ(1, first_song.beginning()); - ASSERT_EQ((5 * 60 + 3) - 1, first_song.length()); + ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length()); + ASSERT_EQ(1, first_song.track()); - Song second_song = song_list.at(1); ASSERT_EQ("Somewon Else's Tears", second_song.title()); ASSERT_EQ("Chocabeck", second_song.album()); ASSERT_EQ("Zucchero himself", second_song.artist()); ASSERT_EQ("Zucchero himself", second_song.albumartist()); ASSERT_EQ(5 * 60 + 3, second_song.beginning()); + ASSERT_EQ(2, second_song.track()); } TEST_F(CueParserTest, SkipsBrokenSongs) { @@ -92,6 +96,8 @@ TEST_F(CueParserTest, SkipsBrokenSongs) { // with the specified metadata Song first_song = song_list.at(0); + Song second_song = song_list.at(1); + ASSERT_EQ("Un soffio caldo", first_song.title()); ASSERT_EQ("Chocabeck", first_song.album()); ASSERT_EQ("Zucchero himself", first_song.artist()); @@ -99,12 +105,183 @@ TEST_F(CueParserTest, SkipsBrokenSongs) { ASSERT_EQ(1, first_song.beginning()); // includes the broken song too; this entry will span from it's // INDEX (beginning) to the end of the next correct song - ASSERT_EQ((5 * 60) - 1, first_song.length()); + ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length()); + ASSERT_EQ(1, first_song.track()); - Song second_song = song_list.at(1); ASSERT_EQ("Somewon Else's Tears", second_song.title()); ASSERT_EQ("Chocabeck", second_song.album()); ASSERT_EQ("Zucchero himself", second_song.artist()); ASSERT_EQ("Zucchero himself", second_song.albumartist()); ASSERT_EQ(5 * 60, second_song.beginning()); + ASSERT_EQ(2, second_song.track()); +} + +TEST_F(CueParserTest, UsesAllMetadataInformation) { + QFile file(":testdata/fullmetadata.cue"); + file.open(QIODevice::ReadOnly); + + SongList song_list = parser_.Load(&file, QDir("")); + + // two songs + ASSERT_EQ(2, song_list.size()); + + // with the specified metadata + Song first_song = song_list.at(0); + Song second_song = song_list.at(1); + + ASSERT_TRUE(first_song.filename().endsWith("a_file.mp3")); + ASSERT_EQ("Un soffio caldo", first_song.title()); + ASSERT_EQ("Album", first_song.album()); + ASSERT_EQ("Zucchero", first_song.artist()); + ASSERT_EQ("Zucchero himself", first_song.albumartist()); + ASSERT_EQ("Some guy", first_song.composer()); + ASSERT_EQ(1, first_song.beginning()); + ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length()); + ASSERT_EQ(1, first_song.track()); + + ASSERT_TRUE(second_song.filename().endsWith("a_file.mp3")); + ASSERT_EQ("Hey you!", second_song.title()); + ASSERT_EQ("Album", second_song.album()); + ASSERT_EQ("Zucchero himself", second_song.artist()); + ASSERT_EQ("Zucchero himself", second_song.albumartist()); + ASSERT_EQ("Some other guy", second_song.composer()); + ASSERT_EQ(2, second_song.beginning()); + ASSERT_EQ(2, second_song.track()); +} + +TEST_F(CueParserTest, AcceptsMultipleFileBasedCues) { + QFile file(":testdata/manyfiles.cue"); + file.open(QIODevice::ReadOnly); + + SongList song_list = parser_.Load(&file, QDir("")); + + // five songs + ASSERT_EQ(5, song_list.size()); + + // with the specified metadata + Song first_song = song_list.at(0); + Song second_song = song_list.at(1); + Song third_song = song_list.at(2); + Song fourth_song = song_list.at(3); + Song fifth_song = song_list.at(4); + + ASSERT_TRUE(first_song.filename().endsWith("files/longer_one.mp3")); + ASSERT_EQ("A1Song1", first_song.title()); + ASSERT_EQ("Artist One Album", first_song.album()); + ASSERT_EQ("Artist One", first_song.artist()); + ASSERT_EQ("Artist One", first_song.albumartist()); + ASSERT_EQ(1, first_song.beginning()); + ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length()); + ASSERT_EQ(-1, first_song.track()); + + ASSERT_TRUE(second_song.filename().endsWith("files/longer_one.mp3")); + ASSERT_EQ("A1Song2", second_song.title()); + ASSERT_EQ("Artist One Album", second_song.album()); + ASSERT_EQ("Artist One", second_song.artist()); + ASSERT_EQ("Artist One", second_song.albumartist()); + ASSERT_EQ((5 * 60 + 3), second_song.beginning()); + ASSERT_EQ(-1, second_song.track()); + + ASSERT_TRUE(third_song.filename().endsWith("files/longer_two_p1.mp3")); + ASSERT_EQ("A2P1Song1", third_song.title()); + ASSERT_EQ("Artist Two Album", third_song.album()); + ASSERT_EQ("Artist X", third_song.artist()); + ASSERT_EQ("Artist Two", third_song.albumartist()); + ASSERT_EQ(0, third_song.beginning()); + ASSERT_EQ(fourth_song.beginning() - third_song.beginning(), third_song.length()); + ASSERT_EQ(-1, third_song.track()); + + ASSERT_TRUE(fourth_song.filename().endsWith("files/longer_two_p1.mp3")); + ASSERT_EQ("A2P1Song2", fourth_song.title()); + ASSERT_EQ("Artist Two Album", fourth_song.album()); + ASSERT_EQ("Artist Two", fourth_song.artist()); + ASSERT_EQ("Artist Two", fourth_song.albumartist()); + ASSERT_EQ(4 * 60, fourth_song.beginning()); + ASSERT_EQ(-1, fourth_song.track()); + + ASSERT_TRUE(fifth_song.filename().endsWith("files/longer_two_p2.mp3")); + ASSERT_EQ("A2P2Song1", fifth_song.title()); + ASSERT_EQ("Artist Two Album", fifth_song.album()); + ASSERT_EQ("Artist Two", fifth_song.artist()); + ASSERT_EQ("Artist Two", fifth_song.albumartist()); + ASSERT_EQ(1, fifth_song.beginning()); + ASSERT_EQ(-1, fifth_song.track()); +} + +TEST_F(CueParserTest, SkipsBrokenSongsInMultipleFileBasedCues) { + QFile file(":testdata/manyfilesbroken.cue"); + file.open(QIODevice::ReadOnly); + + SongList song_list = parser_.Load(&file, QDir("")); + + // four songs + ASSERT_EQ(4, song_list.size()); + + // with the specified metadata + Song first_song = song_list.at(0); + Song second_song = song_list.at(1); + Song third_song = song_list.at(2); + Song fourth_song = song_list.at(3); + + // A* - broken song in the middle + ASSERT_TRUE(first_song.filename().endsWith("file1.mp3")); + ASSERT_EQ("Artist One", first_song.artist()); + ASSERT_EQ("Artist One Album", first_song.album()); + ASSERT_EQ("A1", first_song.title()); + ASSERT_EQ(1, first_song.beginning()); + ASSERT_EQ(second_song.beginning() - first_song.beginning(), first_song.length()); + ASSERT_EQ(-1, first_song.track()); + + ASSERT_TRUE(second_song.filename().endsWith("file1.mp3")); + ASSERT_EQ("Artist One", second_song.artist()); + ASSERT_EQ("Artist One Album", second_song.album()); + ASSERT_EQ("A3", second_song.title()); + ASSERT_EQ(60, second_song.beginning()); + ASSERT_EQ(-1, second_song.track()); + + // all B* songs are broken + + // C* - broken song at the end + ASSERT_TRUE(third_song.filename().endsWith("file3.mp3")); + ASSERT_EQ("Artist Three", third_song.artist()); + ASSERT_EQ("Artist Three Album", third_song.album()); + ASSERT_EQ("C1", third_song.title()); + ASSERT_EQ(1, third_song.beginning()); + ASSERT_EQ(-1, third_song.track()); + + // D* - broken song at the beginning + ASSERT_TRUE(fourth_song.filename().endsWith("file4.mp3")); + ASSERT_EQ("Artist Four", fourth_song.artist()); + ASSERT_EQ("Artist Four Album", fourth_song.album()); + ASSERT_EQ("D2", fourth_song.title()); + ASSERT_EQ(61, fourth_song.beginning()); + ASSERT_EQ(-1, fourth_song.track()); +} + +TEST_F(CueParserTest, SkipsDataFiles) { + QFile file(":testdata/withdatafiles.cue"); + file.open(QIODevice::ReadOnly); + + SongList song_list = parser_.Load(&file, QDir("")); + + // two songs + ASSERT_EQ(2, song_list.size()); + + // with the specified metadata + Song first_song = song_list.at(0); + Song second_song = song_list.at(1); + + ASSERT_TRUE(first_song.filename().endsWith("file1.mp3")); + ASSERT_EQ("Artist One", first_song.artist()); + ASSERT_EQ("Artist One Album", first_song.album()); + ASSERT_EQ("A1", first_song.title()); + ASSERT_EQ(1, first_song.beginning()); + ASSERT_EQ(-1, first_song.track()); + + ASSERT_TRUE(second_song.filename().endsWith("file4.mp3")); + ASSERT_EQ("Artist Four", second_song.artist()); + ASSERT_EQ("Artist Four Album", second_song.album()); + ASSERT_EQ("D1", second_song.title()); + ASSERT_EQ(61, second_song.beginning()); + ASSERT_EQ(-1, second_song.track()); } diff --git a/tests/data/fullmetadata.cue b/tests/data/fullmetadata.cue new file mode 100644 index 000000000..be18ff0d6 --- /dev/null +++ b/tests/data/fullmetadata.cue @@ -0,0 +1,13 @@ +FILE a_file.mp3 WAVE +SONGWRITER "Some guy" +TITLE "Album" +PERFORMER "Zucchero himself" + TRACK 01 AUDIO + TITLE "Un soffio caldo" + PERFORMER Zucchero + INDEX 00 00:01:00 + INDEX 01 00:01:00 + TRACK 02 AUDIO + TITLE "Hey you!" + SONGWRITER "Some other guy" + INDEX 05 00:02:00 diff --git a/tests/data/manyfiles.cue b/tests/data/manyfiles.cue new file mode 100644 index 000000000..beb334b01 --- /dev/null +++ b/tests/data/manyfiles.cue @@ -0,0 +1,25 @@ +FILE files/longer_one.mp3 WAVE +PERFORMER "Artist One" +TITLE "Artist One Album" + TRACK 01 AUDIO + TITLE "A1Song1" + INDEX 01 00:01:00 + TRACK 02 AUDIO + TITLE "A1Song2" + INDEX 01 05:03:68 +FILE files/longer_two_p1.mp3 WAVE +PERFORMER "Artist Two" +TITLE "Artist Two Album" + TRACK 01 AUDIO + TITLE "A2P1Song1" + PERFORMER "Artist X" + INDEX 01 00:00:00 + TRACK 02 AUDIO + TITLE "A2P1Song2" + INDEX 01 04:00:00 +FILE files/longer_two_p2.mp3 WAVE +PERFORMER "Artist Two" +TITLE "Artist Two Album" + TRACK 01 AUDIO + TITLE "A2P2Song1" + INDEX 01 00:01:00 diff --git a/tests/data/manyfilesbroken.cue b/tests/data/manyfilesbroken.cue new file mode 100644 index 000000000..37f5265a7 --- /dev/null +++ b/tests/data/manyfilesbroken.cue @@ -0,0 +1,33 @@ +FILE file1.mp3 WAVE +PERFORMER "Artist One" +TITLE "Artist One Album" + TRACK 01 AUDIO + TITLE "A1" + INDEX 01 00:01:00 + TRACK 02 AUDIO + TITLE "A2" + TRACK 03 AUDIO + TITLE "A3" + INDEX 01 01:00:00 +FILE file2.mp3 WAVE +PERFORMER "Artist Two" +TITLE "Artist Two Album" + TRACK 01 CDG + TITLE "B1" + INDEX 00 00:01:00 +FILE file3.mp3 WAVE +PERFORMER "Artist Three" +TITLE "Artist Three Album" + TRACK 01 AUDIO + TITLE "C1" + INDEX 00 00:01:00 + TRACK 02 AUDIO + TITLE "C2" +FILE file4.mp3 WAVE +PERFORMER "Artist Four" +TITLE "Artist Four Album" + TRACK 01 AUDIO + TITLE "D1" + TRACK 02 AUDIO + TITLE "D2" + INDEX 00 01:01:00 diff --git a/tests/data/testdata.qrc b/tests/data/testdata.qrc index f322fa5ac..97d7dceb0 100644 --- a/tests/data/testdata.qrc +++ b/tests/data/testdata.qrc @@ -8,21 +8,25 @@ beep.wma beep.m4a brokensong.cue - onesong.cue - twosongs.cue - pls_one.pls - pls_somafm.pls - test.m3u - test.xspf - test.asx - secretagent.asx - secretagent.pls - test.asxini fmpsplaycount.mp3 fmpsplaycountboth.mp3 fmpsplaycountuser.mp3 fmpsrating.mp3 fmpsratingboth.mp3 fmpsratinguser.mp3 + fullmetadata.cue + manyfiles.cue + manyfilesbroken.cue + onesong.cue + pls_one.pls + pls_somafm.pls + secretagent.asx + secretagent.pls + test.m3u + test.xspf + test.asx + test.asxini + twosongs.cue + withdatafiles.cue diff --git a/tests/data/withdatafiles.cue b/tests/data/withdatafiles.cue new file mode 100644 index 000000000..4580fcb14 --- /dev/null +++ b/tests/data/withdatafiles.cue @@ -0,0 +1,28 @@ +FILE file1.mp3 +PERFORMER "Artist One" +TITLE "Artist One Album" + TRACK 01 AUDIO + TITLE "A1" + INDEX 01 00:01:00 + +FILE file2.mp3 BINARY +PERFORMER "Artist Two" +TITLE "Artist Two Album" + TRACK 01 AUDIO + TITLE "B1" + INDEX 00 00:01:00 + +FILE file3.mp3 MOTOROLA +PERFORMER "Artist Three" +TITLE "Artist Three Album" + + TRACK 01 AUDIO + TITLE "C1" + INDEX 00 00:01:00 + +FILE file4.mp3 MP3 +PERFORMER "Artist Four" +TITLE "Artist Four Album" + TRACK 01 AUDIO + TITLE "D1" + INDEX 00 01:01:00