From da3abdc095d9921e7a4adca663a9107f3b7aba96 Mon Sep 17 00:00:00 2001 From: birkeh Date: Wed, 19 Oct 2016 19:21:47 +0200 Subject: [PATCH] first commit --- README.md | Bin 0 -> 28 bytes cdatabase.cpp | 137 ++ cdatabase.h | 31 + cimage.cpp | 26 + cimage.h | 58 + cmainwindow.cpp | 212 ++ cmainwindow.h | 27 + cmainwindow.ui | 24 + cmediainfo.cpp | 1282 +++++++++++ cmediainfo.h | 292 +++ cstring.cpp | 46 + cstring.h | 56 + ctag.cpp | 48 + ctag.h | 38 + main.cpp | 17 + qtJukeBox.pro | 32 + taglib/AUTHORS | 17 + taglib/CMakeLists.txt | 120 + taglib/COPYING.LGPL | 502 ++++ taglib/COPYING.MPL | 470 ++++ taglib/ConfigureChecks.cmake | 226 ++ taglib/Doxyfile.cmake | 210 ++ taglib/INSTALL | 157 ++ taglib/NEWS | 189 ++ taglib/bindings/CMakeLists.txt | 1 + taglib/bindings/README | 6 + taglib/bindings/c/CMakeLists.txt | 68 + taglib/bindings/c/tag_c.cpp | 290 +++ taglib/bindings/c/tag_c.h | 299 +++ taglib/bindings/c/taglib_c.pc.cmake | 12 + taglib/cmake/modules/FindCppUnit.cmake | 69 + taglib/cmake/modules/MacroEnsureVersion.cmake | 71 + taglib/cmake_uninstall.cmake.in | 21 + taglib/config.h.cmake | 37 + taglib/doc/README | 1 + taglib/doc/api-footer.html | 4 + taglib/doc/api-header.html | 41 + taglib/doc/taglib-api.css | 395 ++++ taglib/doc/taglib.png | Bin 0 -> 2748 bytes taglib/examples/CMakeLists.txt | 50 + taglib/examples/framelist.cpp | 109 + taglib/examples/strip-id3v1.cpp | 40 + taglib/examples/tagreader.cpp | 96 + taglib/examples/tagreader_c.c | 81 + taglib/examples/tagwriter.cpp | 139 ++ taglib/taglib-config.cmake | 55 + taglib/taglib-config.cmd.cmake | 36 + taglib/taglib.pc.cmake | 11 + taglib/taglib/CMakeLists.txt | 335 +++ taglib/taglib/ape/ape-tag-format.txt | 170 ++ taglib/taglib/ape/apefile.cpp | 314 +++ taglib/taglib/ape/apefile.h | 229 ++ taglib/taglib/ape/apefooter.cpp | 236 ++ taglib/taglib/ape/apefooter.h | 173 ++ taglib/taglib/ape/apeitem.cpp | 286 +++ taglib/taglib/ape/apeitem.h | 219 ++ taglib/taglib/ape/apeproperties.cpp | 232 ++ taglib/taglib/ape/apeproperties.h | 99 + taglib/taglib/ape/apetag.cpp | 383 ++++ taglib/taglib/ape/apetag.h | 208 ++ taglib/taglib/asf/asfattribute.cpp | 350 +++ taglib/taglib/asf/asfattribute.h | 203 ++ taglib/taglib/asf/asffile.cpp | 630 +++++ taglib/taglib/asf/asffile.h | 148 ++ taglib/taglib/asf/asfpicture.cpp | 179 ++ taglib/taglib/asf/asfpicture.h | 217 ++ taglib/taglib/asf/asfproperties.cpp | 111 + taglib/taglib/asf/asfproperties.h | 76 + taglib/taglib/asf/asftag.cpp | 354 +++ taglib/taglib/asf/asftag.h | 190 ++ taglib/taglib/audioproperties.cpp | 51 + taglib/taglib/audioproperties.h | 110 + taglib/taglib/fileref.cpp | 286 +++ taglib/taglib/fileref.h | 263 +++ taglib/taglib/flac/flacfile.cpp | 568 +++++ taglib/taglib/flac/flacfile.h | 305 +++ taglib/taglib/flac/flacmetadatablock.cpp | 47 + taglib/taglib/flac/flacmetadatablock.h | 75 + taglib/taglib/flac/flacpicture.cpp | 217 ++ taglib/taglib/flac/flacpicture.h | 208 ++ taglib/taglib/flac/flacproperties.cpp | 169 ++ taglib/taglib/flac/flacproperties.h | 103 + .../taglib/flac/flacunknownmetadatablock.cpp | 79 + taglib/taglib/flac/flacunknownmetadatablock.h | 81 + taglib/taglib/it/itfile.cpp | 330 +++ taglib/taglib/it/itfile.h | 109 + taglib/taglib/it/itproperties.cpp | 245 ++ taglib/taglib/it/itproperties.h | 101 + taglib/taglib/mod/modfile.cpp | 187 ++ taglib/taglib/mod/modfile.h | 110 + taglib/taglib/mod/modfilebase.cpp | 120 + taglib/taglib/mod/modfilebase.h | 62 + taglib/taglib/mod/modfileprivate.h | 67 + taglib/taglib/mod/modproperties.cpp | 96 + taglib/taglib/mod/modproperties.h | 65 + taglib/taglib/mod/modtag.cpp | 168 ++ taglib/taglib/mod/modtag.h | 190 ++ taglib/taglib/mp4/mp4atom.cpp | 194 ++ taglib/taglib/mp4/mp4atom.h | 111 + taglib/taglib/mp4/mp4coverart.cpp | 83 + taglib/taglib/mp4/mp4coverart.h | 75 + taglib/taglib/mp4/mp4file.cpp | 163 ++ taglib/taglib/mp4/mp4file.h | 130 ++ taglib/taglib/mp4/mp4item.cpp | 206 ++ taglib/taglib/mp4/mp4item.h | 83 + taglib/taglib/mp4/mp4properties.cpp | 199 ++ taglib/taglib/mp4/mp4properties.h | 71 + taglib/taglib/mp4/mp4tag.cpp | 917 ++++++++ taglib/taglib/mp4/mp4tag.h | 118 + taglib/taglib/mpc/mpcfile.cpp | 367 +++ taglib/taglib/mpc/mpcfile.h | 231 ++ taglib/taglib/mpc/mpcproperties.cpp | 316 +++ taglib/taglib/mpc/mpcproperties.h | 122 + taglib/taglib/mpeg/id3v1/id3v1genres.cpp | 219 ++ taglib/taglib/mpeg/id3v1/id3v1genres.h | 66 + taglib/taglib/mpeg/id3v1/id3v1tag.cpp | 265 +++ taglib/taglib/mpeg/id3v1/id3v1tag.h | 202 ++ .../id3v2/frames/attachedpictureframe.cpp | 222 ++ .../mpeg/id3v2/frames/attachedpictureframe.h | 230 ++ .../mpeg/id3v2/frames/commentsframe.cpp | 197 ++ .../taglib/mpeg/id3v2/frames/commentsframe.h | 179 ++ .../frames/generalencapsulatedobjectframe.cpp | 176 ++ .../frames/generalencapsulatedobjectframe.h | 178 ++ .../mpeg/id3v2/frames/ownershipframe.cpp | 162 ++ .../taglib/mpeg/id3v2/frames/ownershipframe.h | 151 ++ .../mpeg/id3v2/frames/popularimeterframe.cpp | 137 ++ .../mpeg/id3v2/frames/popularimeterframe.h | 132 ++ .../taglib/mpeg/id3v2/frames/privateframe.cpp | 128 ++ .../taglib/mpeg/id3v2/frames/privateframe.h | 111 + .../mpeg/id3v2/frames/relativevolumeframe.cpp | 236 ++ .../mpeg/id3v2/frames/relativevolumeframe.h | 274 +++ .../id3v2/frames/textidentificationframe.cpp | 423 ++++ .../id3v2/frames/textidentificationframe.h | 313 +++ .../frames/uniquefileidentifierframe.cpp | 148 ++ .../id3v2/frames/uniquefileidentifierframe.h | 123 + .../taglib/mpeg/id3v2/frames/unknownframe.cpp | 84 + .../taglib/mpeg/id3v2/frames/unknownframe.h | 79 + .../frames/unsynchronizedlyricsframe.cpp | 193 ++ .../id3v2/frames/unsynchronizedlyricsframe.h | 179 ++ .../taglib/mpeg/id3v2/frames/urllinkframe.cpp | 230 ++ .../taglib/mpeg/id3v2/frames/urllinkframe.h | 189 ++ taglib/taglib/mpeg/id3v2/id3v2.2.0.txt | 1660 ++++++++++++++ taglib/taglib/mpeg/id3v2/id3v2.3.0.txt | 2022 +++++++++++++++++ taglib/taglib/mpeg/id3v2/id3v2.4.0-frames.txt | 1734 ++++++++++++++ .../taglib/mpeg/id3v2/id3v2.4.0-structure.txt | 733 ++++++ .../taglib/mpeg/id3v2/id3v2extendedheader.cpp | 71 + .../taglib/mpeg/id3v2/id3v2extendedheader.h | 93 + taglib/taglib/mpeg/id3v2/id3v2footer.cpp | 60 + taglib/taglib/mpeg/id3v2/id3v2footer.h | 82 + taglib/taglib/mpeg/id3v2/id3v2frame.cpp | 844 +++++++ taglib/taglib/mpeg/id3v2/id3v2frame.h | 518 +++++ .../taglib/mpeg/id3v2/id3v2framefactory.cpp | 458 ++++ taglib/taglib/mpeg/id3v2/id3v2framefactory.h | 166 ++ taglib/taglib/mpeg/id3v2/id3v2header.cpp | 243 ++ taglib/taglib/mpeg/id3v2/id3v2header.h | 175 ++ taglib/taglib/mpeg/id3v2/id3v2synchdata.cpp | 84 + taglib/taglib/mpeg/id3v2/id3v2synchdata.h | 70 + taglib/taglib/mpeg/id3v2/id3v2tag.cpp | 732 ++++++ taglib/taglib/mpeg/id3v2/id3v2tag.h | 412 ++++ taglib/taglib/mpeg/mpegfile.cpp | 663 ++++++ taglib/taglib/mpeg/mpegfile.h | 390 ++++ taglib/taglib/mpeg/mpegheader.cpp | 277 +++ taglib/taglib/mpeg/mpegheader.h | 166 ++ taglib/taglib/mpeg/mpegproperties.cpp | 254 +++ taglib/taglib/mpeg/mpegproperties.h | 118 + taglib/taglib/mpeg/xingheader.cpp | 115 + taglib/taglib/mpeg/xingheader.h | 100 + taglib/taglib/ogg/flac/oggflacfile.cpp | 294 +++ taglib/taglib/ogg/flac/oggflacfile.h | 162 ++ taglib/taglib/ogg/oggfile.cpp | 432 ++++ taglib/taglib/ogg/oggfile.h | 122 + taglib/taglib/ogg/oggpage.cpp | 340 +++ taglib/taglib/ogg/oggpage.h | 211 ++ taglib/taglib/ogg/oggpageheader.cpp | 323 +++ taglib/taglib/ogg/oggpageheader.h | 232 ++ taglib/taglib/ogg/opus/opusfile.cpp | 139 ++ taglib/taglib/ogg/opus/opusfile.h | 124 + taglib/taglib/ogg/opus/opusproperties.cpp | 161 ++ taglib/taglib/ogg/opus/opusproperties.h | 96 + taglib/taglib/ogg/speex/speexfile.cpp | 132 ++ taglib/taglib/ogg/speex/speexfile.h | 126 + taglib/taglib/ogg/speex/speexproperties.cpp | 170 ++ taglib/taglib/ogg/speex/speexproperties.h | 89 + taglib/taglib/ogg/vorbis/vorbisfile.cpp | 138 ++ taglib/taglib/ogg/vorbis/vorbisfile.h | 144 ++ taglib/taglib/ogg/vorbis/vorbisproperties.cpp | 183 ++ taglib/taglib/ogg/vorbis/vorbisproperties.h | 125 + taglib/taglib/ogg/xiphcomment.cpp | 382 ++++ taglib/taglib/ogg/xiphcomment.h | 225 ++ taglib/taglib/riff/aiff/aifffile.cpp | 144 ++ taglib/taglib/riff/aiff/aifffile.h | 142 ++ taglib/taglib/riff/aiff/aiffproperties.cpp | 160 ++ taglib/taglib/riff/aiff/aiffproperties.h | 83 + taglib/taglib/riff/rifffile.cpp | 321 +++ taglib/taglib/riff/rifffile.h | 157 ++ taglib/taglib/riff/wav/infotag.cpp | 266 +++ taglib/taglib/riff/wav/infotag.h | 192 ++ taglib/taglib/riff/wav/wavfile.cpp | 244 ++ taglib/taglib/riff/wav/wavfile.h | 189 ++ taglib/taglib/riff/wav/wavproperties.cpp | 129 ++ taglib/taglib/riff/wav/wavproperties.h | 92 + taglib/taglib/s3m/s3mfile.cpp | 243 ++ taglib/taglib/s3m/s3mfile.h | 108 + taglib/taglib/s3m/s3mproperties.cpp | 204 ++ taglib/taglib/s3m/s3mproperties.h | 88 + taglib/taglib/tag.cpp | 180 ++ taglib/taglib/tag.h | 201 ++ taglib/taglib/taglib_config.h.cmake | 4 + taglib/taglib/taglib_export.h | 43 + taglib/taglib/tagunion.cpp | 186 ++ taglib/taglib/tagunion.h | 95 + taglib/taglib/toolkit/taglib.h | 170 ++ taglib/taglib/toolkit/tbytevector.cpp | 857 +++++++ taglib/taglib/toolkit/tbytevector.h | 518 +++++ taglib/taglib/toolkit/tbytevectorlist.cpp | 102 + taglib/taglib/toolkit/tbytevectorlist.h | 91 + taglib/taglib/toolkit/tbytevectorstream.cpp | 167 ++ taglib/taglib/toolkit/tbytevectorstream.h | 145 ++ taglib/taglib/toolkit/tdebug.cpp | 99 + taglib/taglib/toolkit/tdebug.h | 62 + taglib/taglib/toolkit/tdebuglistener.cpp | 85 + taglib/taglib/toolkit/tdebuglistener.h | 74 + taglib/taglib/toolkit/tfile.cpp | 503 ++++ taglib/taglib/toolkit/tfile.h | 306 +++ taglib/taglib/toolkit/tfilestream.cpp | 492 ++++ taglib/taglib/toolkit/tfilestream.h | 154 ++ taglib/taglib/toolkit/tiostream.cpp | 150 ++ taglib/taglib/toolkit/tiostream.h | 169 ++ taglib/taglib/toolkit/tlist.h | 257 +++ taglib/taglib/toolkit/tlist.tcc | 324 +++ taglib/taglib/toolkit/tmap.h | 199 ++ taglib/taglib/toolkit/tmap.tcc | 199 ++ taglib/taglib/toolkit/tpropertymap.cpp | 173 ++ taglib/taglib/toolkit/tpropertymap.h | 233 ++ taglib/taglib/toolkit/trefcounter.cpp | 108 + taglib/taglib/toolkit/trefcounter.h | 111 + taglib/taglib/toolkit/tstring.cpp | 880 +++++++ taglib/taglib/toolkit/tstring.h | 543 +++++ taglib/taglib/toolkit/tstringlist.cpp | 123 + taglib/taglib/toolkit/tstringlist.h | 115 + taglib/taglib/toolkit/tutils.h | 190 ++ taglib/taglib/toolkit/unicode.cpp | 303 +++ taglib/taglib/toolkit/unicode.h | 154 ++ taglib/taglib/trueaudio/trueaudiofile.cpp | 325 +++ taglib/taglib/trueaudio/trueaudiofile.h | 252 ++ .../taglib/trueaudio/trueaudioproperties.cpp | 150 ++ taglib/taglib/trueaudio/trueaudioproperties.h | 98 + taglib/taglib/wavpack/wavpackfile.cpp | 311 +++ taglib/taglib/wavpack/wavpackfile.h | 216 ++ taglib/taglib/wavpack/wavpackproperties.cpp | 203 ++ taglib/taglib/wavpack/wavpackproperties.h | 110 + taglib/taglib/xm/xmfile.cpp | 650 ++++++ taglib/taglib/xm/xmfile.h | 108 + taglib/taglib/xm/xmproperties.cpp | 180 ++ taglib/taglib/xm/xmproperties.h | 79 + taglib/tests/CMakeLists.txt | 76 + taglib/tests/data/005411.id3 | Bin 0 -> 38402 bytes taglib/tests/data/64bit.mp4 | Bin 0 -> 85 bytes taglib/tests/data/broken-tenc.id3 | Bin 0 -> 400 bytes taglib/tests/data/changed.mod | Bin 0 -> 3132 bytes taglib/tests/data/changed.s3m | Bin 0 -> 544 bytes taglib/tests/data/changed.xm | Bin 0 -> 5471 bytes taglib/tests/data/click.mpc | Bin 0 -> 1588 bytes taglib/tests/data/click.wv | Bin 0 -> 3176 bytes taglib/tests/data/compressed_id3_frame.mp3 | Bin 0 -> 5000 bytes .../data/correctness_gain_silent_output.opus | Bin 0 -> 35506 bytes taglib/tests/data/covr-junk.m4a | Bin 0 -> 5108 bytes taglib/tests/data/empty.aiff | Bin 0 -> 5936 bytes taglib/tests/data/empty.ogg | Bin 0 -> 4328 bytes taglib/tests/data/empty.spx | Bin 0 -> 24301 bytes taglib/tests/data/empty.tta | Bin 0 -> 79538 bytes taglib/tests/data/empty.wav | Bin 0 -> 14744 bytes taglib/tests/data/empty_alac.m4a | Bin 0 -> 5380 bytes taglib/tests/data/empty_flac.oga | Bin 0 -> 9113 bytes taglib/tests/data/empty_vorbis.oga | Bin 0 -> 4328 bytes taglib/tests/data/gnre.m4a | Bin 0 -> 5026 bytes taglib/tests/data/has-tags.m4a | Bin 0 -> 5108 bytes taglib/tests/data/id3v22-tda.mp3 | Bin 0 -> 8192 bytes taglib/tests/data/ilst-is-last.m4a | Bin 0 -> 32768 bytes taglib/tests/data/mac-390-hdr.ape | Bin 0 -> 128 bytes taglib/tests/data/mac-396.ape | Bin 0 -> 104 bytes taglib/tests/data/mac-399.ape | Bin 0 -> 172 bytes taglib/tests/data/mpeg2.mp3 | Bin 0 -> 16384 bytes taglib/tests/data/multiple-vc.flac | Bin 0 -> 4754 bytes taglib/tests/data/no-extension | Bin 0 -> 256 bytes taglib/tests/data/no-tags.3g2 | Bin 0 -> 68335 bytes taglib/tests/data/no-tags.flac | Bin 0 -> 4692 bytes taglib/tests/data/no-tags.m4a | Bin 0 -> 2898 bytes taglib/tests/data/no_length.wv | Bin 0 -> 532 bytes taglib/tests/data/noise.aif | Bin 0 -> 4400 bytes taglib/tests/data/noise_odd.aif | Bin 0 -> 4399 bytes taglib/tests/data/rare_frames.mp3 | Bin 0 -> 8320 bytes taglib/tests/data/silence-1.wma | Bin 0 -> 35416 bytes taglib/tests/data/silence-44-s.flac | Bin 0 -> 50904 bytes taglib/tests/data/stripped.xm | Bin 0 -> 602 bytes taglib/tests/data/sv4_header.mpc | Bin 0 -> 128 bytes taglib/tests/data/sv5_header.mpc | Bin 0 -> 128 bytes taglib/tests/data/sv8_header.mpc | Bin 0 -> 114 bytes taglib/tests/data/test.it | Bin 0 -> 644 bytes taglib/tests/data/test.mod | Bin 0 -> 3132 bytes taglib/tests/data/test.ogg | Bin 0 -> 4408 bytes taglib/tests/data/test.s3m | Bin 0 -> 544 bytes taglib/tests/data/test.xm | Bin 0 -> 5471 bytes taglib/tests/data/unsupported-extension.xxx | Bin 0 -> 256 bytes taglib/tests/data/unsynch.id3 | Bin 0 -> 320 bytes taglib/tests/data/w000.mp3 | Bin 0 -> 512 bytes taglib/tests/data/xing.mp3 | Bin 0 -> 8208 bytes taglib/tests/data/zero-size-chunk.wav | Bin 0 -> 1024 bytes taglib/tests/main.cpp | 57 + taglib/tests/test_aiff.cpp | 31 + taglib/tests/test_ape.cpp | 52 + taglib/tests/test_apetag.cpp | 122 + taglib/tests/test_asf.cpp | 255 +++ taglib/tests/test_bytevector.cpp | 246 ++ taglib/tests/test_bytevectorlist.cpp | 38 + taglib/tests/test_bytevectorstream.cpp | 92 + taglib/tests/test_fileref.cpp | 163 ++ taglib/tests/test_flac.cpp | 248 ++ taglib/tests/test_flacpicture.cpp | 52 + .../tests/test_flacunknownmetadatablock.cpp | 38 + taglib/tests/test_id3v1.cpp | 40 + taglib/tests/test_id3v2.cpp | 758 ++++++ taglib/tests/test_info.cpp | 49 + taglib/tests/test_it.cpp | 136 ++ taglib/tests/test_list.cpp | 58 + taglib/tests/test_map.cpp | 27 + taglib/tests/test_mod.cpp | 126 + taglib/tests/test_mp4.cpp | 282 +++ taglib/tests/test_mp4coverart.cpp | 49 + taglib/tests/test_mp4item.cpp | 37 + taglib/tests/test_mpc.cpp | 66 + taglib/tests/test_mpeg.cpp | 88 + taglib/tests/test_ogg.cpp | 104 + taglib/tests/test_oggflac.cpp | 44 + taglib/tests/test_opus.cpp | 61 + taglib/tests/test_propertymap.cpp | 32 + taglib/tests/test_riff.cpp | 198 ++ taglib/tests/test_s3m.cpp | 123 + taglib/tests/test_string.cpp | 233 ++ taglib/tests/test_synchdata.cpp | 107 + taglib/tests/test_trueaudio.cpp | 27 + taglib/tests/test_wav.cpp | 36 + taglib/tests/test_wavpack.cpp | 40 + taglib/tests/test_xiphcomment.cpp | 79 + taglib/tests/test_xm.cpp | 216 ++ taglib/tests/utils.h | 104 + 346 files changed, 59904 insertions(+) create mode 100644 README.md create mode 100644 cdatabase.cpp create mode 100644 cdatabase.h create mode 100644 cimage.cpp create mode 100644 cimage.h create mode 100644 cmainwindow.cpp create mode 100644 cmainwindow.h create mode 100644 cmainwindow.ui create mode 100644 cmediainfo.cpp create mode 100644 cmediainfo.h create mode 100644 cstring.cpp create mode 100644 cstring.h create mode 100644 ctag.cpp create mode 100644 ctag.h create mode 100644 main.cpp create mode 100644 qtJukeBox.pro create mode 100644 taglib/AUTHORS create mode 100644 taglib/CMakeLists.txt create mode 100644 taglib/COPYING.LGPL create mode 100644 taglib/COPYING.MPL create mode 100644 taglib/ConfigureChecks.cmake create mode 100644 taglib/Doxyfile.cmake create mode 100644 taglib/INSTALL create mode 100644 taglib/NEWS create mode 100644 taglib/bindings/CMakeLists.txt create mode 100644 taglib/bindings/README create mode 100644 taglib/bindings/c/CMakeLists.txt create mode 100644 taglib/bindings/c/tag_c.cpp create mode 100644 taglib/bindings/c/tag_c.h create mode 100644 taglib/bindings/c/taglib_c.pc.cmake create mode 100644 taglib/cmake/modules/FindCppUnit.cmake create mode 100644 taglib/cmake/modules/MacroEnsureVersion.cmake create mode 100644 taglib/cmake_uninstall.cmake.in create mode 100644 taglib/config.h.cmake create mode 100644 taglib/doc/README create mode 100644 taglib/doc/api-footer.html create mode 100644 taglib/doc/api-header.html create mode 100644 taglib/doc/taglib-api.css create mode 100644 taglib/doc/taglib.png create mode 100644 taglib/examples/CMakeLists.txt create mode 100644 taglib/examples/framelist.cpp create mode 100644 taglib/examples/strip-id3v1.cpp create mode 100644 taglib/examples/tagreader.cpp create mode 100644 taglib/examples/tagreader_c.c create mode 100644 taglib/examples/tagwriter.cpp create mode 100644 taglib/taglib-config.cmake create mode 100644 taglib/taglib-config.cmd.cmake create mode 100644 taglib/taglib.pc.cmake create mode 100644 taglib/taglib/CMakeLists.txt create mode 100644 taglib/taglib/ape/ape-tag-format.txt create mode 100644 taglib/taglib/ape/apefile.cpp create mode 100644 taglib/taglib/ape/apefile.h create mode 100644 taglib/taglib/ape/apefooter.cpp create mode 100644 taglib/taglib/ape/apefooter.h create mode 100644 taglib/taglib/ape/apeitem.cpp create mode 100644 taglib/taglib/ape/apeitem.h create mode 100644 taglib/taglib/ape/apeproperties.cpp create mode 100644 taglib/taglib/ape/apeproperties.h create mode 100644 taglib/taglib/ape/apetag.cpp create mode 100644 taglib/taglib/ape/apetag.h create mode 100644 taglib/taglib/asf/asfattribute.cpp create mode 100644 taglib/taglib/asf/asfattribute.h create mode 100644 taglib/taglib/asf/asffile.cpp create mode 100644 taglib/taglib/asf/asffile.h create mode 100644 taglib/taglib/asf/asfpicture.cpp create mode 100644 taglib/taglib/asf/asfpicture.h create mode 100644 taglib/taglib/asf/asfproperties.cpp create mode 100644 taglib/taglib/asf/asfproperties.h create mode 100644 taglib/taglib/asf/asftag.cpp create mode 100644 taglib/taglib/asf/asftag.h create mode 100644 taglib/taglib/audioproperties.cpp create mode 100644 taglib/taglib/audioproperties.h create mode 100644 taglib/taglib/fileref.cpp create mode 100644 taglib/taglib/fileref.h create mode 100644 taglib/taglib/flac/flacfile.cpp create mode 100644 taglib/taglib/flac/flacfile.h create mode 100644 taglib/taglib/flac/flacmetadatablock.cpp create mode 100644 taglib/taglib/flac/flacmetadatablock.h create mode 100644 taglib/taglib/flac/flacpicture.cpp create mode 100644 taglib/taglib/flac/flacpicture.h create mode 100644 taglib/taglib/flac/flacproperties.cpp create mode 100644 taglib/taglib/flac/flacproperties.h create mode 100644 taglib/taglib/flac/flacunknownmetadatablock.cpp create mode 100644 taglib/taglib/flac/flacunknownmetadatablock.h create mode 100644 taglib/taglib/it/itfile.cpp create mode 100644 taglib/taglib/it/itfile.h create mode 100644 taglib/taglib/it/itproperties.cpp create mode 100644 taglib/taglib/it/itproperties.h create mode 100644 taglib/taglib/mod/modfile.cpp create mode 100644 taglib/taglib/mod/modfile.h create mode 100644 taglib/taglib/mod/modfilebase.cpp create mode 100644 taglib/taglib/mod/modfilebase.h create mode 100644 taglib/taglib/mod/modfileprivate.h create mode 100644 taglib/taglib/mod/modproperties.cpp create mode 100644 taglib/taglib/mod/modproperties.h create mode 100644 taglib/taglib/mod/modtag.cpp create mode 100644 taglib/taglib/mod/modtag.h create mode 100644 taglib/taglib/mp4/mp4atom.cpp create mode 100644 taglib/taglib/mp4/mp4atom.h create mode 100644 taglib/taglib/mp4/mp4coverart.cpp create mode 100644 taglib/taglib/mp4/mp4coverart.h create mode 100644 taglib/taglib/mp4/mp4file.cpp create mode 100644 taglib/taglib/mp4/mp4file.h create mode 100644 taglib/taglib/mp4/mp4item.cpp create mode 100644 taglib/taglib/mp4/mp4item.h create mode 100644 taglib/taglib/mp4/mp4properties.cpp create mode 100644 taglib/taglib/mp4/mp4properties.h create mode 100644 taglib/taglib/mp4/mp4tag.cpp create mode 100644 taglib/taglib/mp4/mp4tag.h create mode 100644 taglib/taglib/mpc/mpcfile.cpp create mode 100644 taglib/taglib/mpc/mpcfile.h create mode 100644 taglib/taglib/mpc/mpcproperties.cpp create mode 100644 taglib/taglib/mpc/mpcproperties.h create mode 100644 taglib/taglib/mpeg/id3v1/id3v1genres.cpp create mode 100644 taglib/taglib/mpeg/id3v1/id3v1genres.h create mode 100644 taglib/taglib/mpeg/id3v1/id3v1tag.cpp create mode 100644 taglib/taglib/mpeg/id3v1/id3v1tag.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/attachedpictureframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/attachedpictureframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/commentsframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/commentsframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/generalencapsulatedobjectframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/generalencapsulatedobjectframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/ownershipframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/ownershipframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/popularimeterframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/popularimeterframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/privateframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/privateframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/relativevolumeframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/relativevolumeframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/textidentificationframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/textidentificationframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/uniquefileidentifierframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/uniquefileidentifierframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/unknownframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/unknownframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.h create mode 100644 taglib/taglib/mpeg/id3v2/frames/urllinkframe.cpp create mode 100644 taglib/taglib/mpeg/id3v2/frames/urllinkframe.h create mode 100644 taglib/taglib/mpeg/id3v2/id3v2.2.0.txt create mode 100644 taglib/taglib/mpeg/id3v2/id3v2.3.0.txt create mode 100644 taglib/taglib/mpeg/id3v2/id3v2.4.0-frames.txt create mode 100644 taglib/taglib/mpeg/id3v2/id3v2.4.0-structure.txt create mode 100644 taglib/taglib/mpeg/id3v2/id3v2extendedheader.cpp create mode 100644 taglib/taglib/mpeg/id3v2/id3v2extendedheader.h create mode 100644 taglib/taglib/mpeg/id3v2/id3v2footer.cpp create mode 100644 taglib/taglib/mpeg/id3v2/id3v2footer.h create mode 100644 taglib/taglib/mpeg/id3v2/id3v2frame.cpp create mode 100644 taglib/taglib/mpeg/id3v2/id3v2frame.h create mode 100644 taglib/taglib/mpeg/id3v2/id3v2framefactory.cpp create mode 100644 taglib/taglib/mpeg/id3v2/id3v2framefactory.h create mode 100644 taglib/taglib/mpeg/id3v2/id3v2header.cpp create mode 100644 taglib/taglib/mpeg/id3v2/id3v2header.h create mode 100644 taglib/taglib/mpeg/id3v2/id3v2synchdata.cpp create mode 100644 taglib/taglib/mpeg/id3v2/id3v2synchdata.h create mode 100644 taglib/taglib/mpeg/id3v2/id3v2tag.cpp create mode 100644 taglib/taglib/mpeg/id3v2/id3v2tag.h create mode 100644 taglib/taglib/mpeg/mpegfile.cpp create mode 100644 taglib/taglib/mpeg/mpegfile.h create mode 100644 taglib/taglib/mpeg/mpegheader.cpp create mode 100644 taglib/taglib/mpeg/mpegheader.h create mode 100644 taglib/taglib/mpeg/mpegproperties.cpp create mode 100644 taglib/taglib/mpeg/mpegproperties.h create mode 100644 taglib/taglib/mpeg/xingheader.cpp create mode 100644 taglib/taglib/mpeg/xingheader.h create mode 100644 taglib/taglib/ogg/flac/oggflacfile.cpp create mode 100644 taglib/taglib/ogg/flac/oggflacfile.h create mode 100644 taglib/taglib/ogg/oggfile.cpp create mode 100644 taglib/taglib/ogg/oggfile.h create mode 100644 taglib/taglib/ogg/oggpage.cpp create mode 100644 taglib/taglib/ogg/oggpage.h create mode 100644 taglib/taglib/ogg/oggpageheader.cpp create mode 100644 taglib/taglib/ogg/oggpageheader.h create mode 100644 taglib/taglib/ogg/opus/opusfile.cpp create mode 100644 taglib/taglib/ogg/opus/opusfile.h create mode 100644 taglib/taglib/ogg/opus/opusproperties.cpp create mode 100644 taglib/taglib/ogg/opus/opusproperties.h create mode 100644 taglib/taglib/ogg/speex/speexfile.cpp create mode 100644 taglib/taglib/ogg/speex/speexfile.h create mode 100644 taglib/taglib/ogg/speex/speexproperties.cpp create mode 100644 taglib/taglib/ogg/speex/speexproperties.h create mode 100644 taglib/taglib/ogg/vorbis/vorbisfile.cpp create mode 100644 taglib/taglib/ogg/vorbis/vorbisfile.h create mode 100644 taglib/taglib/ogg/vorbis/vorbisproperties.cpp create mode 100644 taglib/taglib/ogg/vorbis/vorbisproperties.h create mode 100644 taglib/taglib/ogg/xiphcomment.cpp create mode 100644 taglib/taglib/ogg/xiphcomment.h create mode 100644 taglib/taglib/riff/aiff/aifffile.cpp create mode 100644 taglib/taglib/riff/aiff/aifffile.h create mode 100644 taglib/taglib/riff/aiff/aiffproperties.cpp create mode 100644 taglib/taglib/riff/aiff/aiffproperties.h create mode 100644 taglib/taglib/riff/rifffile.cpp create mode 100644 taglib/taglib/riff/rifffile.h create mode 100644 taglib/taglib/riff/wav/infotag.cpp create mode 100644 taglib/taglib/riff/wav/infotag.h create mode 100644 taglib/taglib/riff/wav/wavfile.cpp create mode 100644 taglib/taglib/riff/wav/wavfile.h create mode 100644 taglib/taglib/riff/wav/wavproperties.cpp create mode 100644 taglib/taglib/riff/wav/wavproperties.h create mode 100644 taglib/taglib/s3m/s3mfile.cpp create mode 100644 taglib/taglib/s3m/s3mfile.h create mode 100644 taglib/taglib/s3m/s3mproperties.cpp create mode 100644 taglib/taglib/s3m/s3mproperties.h create mode 100644 taglib/taglib/tag.cpp create mode 100644 taglib/taglib/tag.h create mode 100644 taglib/taglib/taglib_config.h.cmake create mode 100644 taglib/taglib/taglib_export.h create mode 100644 taglib/taglib/tagunion.cpp create mode 100644 taglib/taglib/tagunion.h create mode 100644 taglib/taglib/toolkit/taglib.h create mode 100644 taglib/taglib/toolkit/tbytevector.cpp create mode 100644 taglib/taglib/toolkit/tbytevector.h create mode 100644 taglib/taglib/toolkit/tbytevectorlist.cpp create mode 100644 taglib/taglib/toolkit/tbytevectorlist.h create mode 100644 taglib/taglib/toolkit/tbytevectorstream.cpp create mode 100644 taglib/taglib/toolkit/tbytevectorstream.h create mode 100644 taglib/taglib/toolkit/tdebug.cpp create mode 100644 taglib/taglib/toolkit/tdebug.h create mode 100644 taglib/taglib/toolkit/tdebuglistener.cpp create mode 100644 taglib/taglib/toolkit/tdebuglistener.h create mode 100644 taglib/taglib/toolkit/tfile.cpp create mode 100644 taglib/taglib/toolkit/tfile.h create mode 100644 taglib/taglib/toolkit/tfilestream.cpp create mode 100644 taglib/taglib/toolkit/tfilestream.h create mode 100644 taglib/taglib/toolkit/tiostream.cpp create mode 100644 taglib/taglib/toolkit/tiostream.h create mode 100644 taglib/taglib/toolkit/tlist.h create mode 100644 taglib/taglib/toolkit/tlist.tcc create mode 100644 taglib/taglib/toolkit/tmap.h create mode 100644 taglib/taglib/toolkit/tmap.tcc create mode 100644 taglib/taglib/toolkit/tpropertymap.cpp create mode 100644 taglib/taglib/toolkit/tpropertymap.h create mode 100644 taglib/taglib/toolkit/trefcounter.cpp create mode 100644 taglib/taglib/toolkit/trefcounter.h create mode 100644 taglib/taglib/toolkit/tstring.cpp create mode 100644 taglib/taglib/toolkit/tstring.h create mode 100644 taglib/taglib/toolkit/tstringlist.cpp create mode 100644 taglib/taglib/toolkit/tstringlist.h create mode 100644 taglib/taglib/toolkit/tutils.h create mode 100644 taglib/taglib/toolkit/unicode.cpp create mode 100644 taglib/taglib/toolkit/unicode.h create mode 100644 taglib/taglib/trueaudio/trueaudiofile.cpp create mode 100644 taglib/taglib/trueaudio/trueaudiofile.h create mode 100644 taglib/taglib/trueaudio/trueaudioproperties.cpp create mode 100644 taglib/taglib/trueaudio/trueaudioproperties.h create mode 100644 taglib/taglib/wavpack/wavpackfile.cpp create mode 100644 taglib/taglib/wavpack/wavpackfile.h create mode 100644 taglib/taglib/wavpack/wavpackproperties.cpp create mode 100644 taglib/taglib/wavpack/wavpackproperties.h create mode 100644 taglib/taglib/xm/xmfile.cpp create mode 100644 taglib/taglib/xm/xmfile.h create mode 100644 taglib/taglib/xm/xmproperties.cpp create mode 100644 taglib/taglib/xm/xmproperties.h create mode 100644 taglib/tests/CMakeLists.txt create mode 100644 taglib/tests/data/005411.id3 create mode 100644 taglib/tests/data/64bit.mp4 create mode 100644 taglib/tests/data/broken-tenc.id3 create mode 100644 taglib/tests/data/changed.mod create mode 100644 taglib/tests/data/changed.s3m create mode 100644 taglib/tests/data/changed.xm create mode 100644 taglib/tests/data/click.mpc create mode 100644 taglib/tests/data/click.wv create mode 100644 taglib/tests/data/compressed_id3_frame.mp3 create mode 100644 taglib/tests/data/correctness_gain_silent_output.opus create mode 100644 taglib/tests/data/covr-junk.m4a create mode 100644 taglib/tests/data/empty.aiff create mode 100644 taglib/tests/data/empty.ogg create mode 100644 taglib/tests/data/empty.spx create mode 100644 taglib/tests/data/empty.tta create mode 100644 taglib/tests/data/empty.wav create mode 100644 taglib/tests/data/empty_alac.m4a create mode 100644 taglib/tests/data/empty_flac.oga create mode 100644 taglib/tests/data/empty_vorbis.oga create mode 100644 taglib/tests/data/gnre.m4a create mode 100644 taglib/tests/data/has-tags.m4a create mode 100644 taglib/tests/data/id3v22-tda.mp3 create mode 100644 taglib/tests/data/ilst-is-last.m4a create mode 100644 taglib/tests/data/mac-390-hdr.ape create mode 100644 taglib/tests/data/mac-396.ape create mode 100644 taglib/tests/data/mac-399.ape create mode 100644 taglib/tests/data/mpeg2.mp3 create mode 100644 taglib/tests/data/multiple-vc.flac create mode 100644 taglib/tests/data/no-extension create mode 100644 taglib/tests/data/no-tags.3g2 create mode 100644 taglib/tests/data/no-tags.flac create mode 100644 taglib/tests/data/no-tags.m4a create mode 100644 taglib/tests/data/no_length.wv create mode 100644 taglib/tests/data/noise.aif create mode 100644 taglib/tests/data/noise_odd.aif create mode 100644 taglib/tests/data/rare_frames.mp3 create mode 100644 taglib/tests/data/silence-1.wma create mode 100644 taglib/tests/data/silence-44-s.flac create mode 100644 taglib/tests/data/stripped.xm create mode 100644 taglib/tests/data/sv4_header.mpc create mode 100644 taglib/tests/data/sv5_header.mpc create mode 100644 taglib/tests/data/sv8_header.mpc create mode 100644 taglib/tests/data/test.it create mode 100644 taglib/tests/data/test.mod create mode 100644 taglib/tests/data/test.ogg create mode 100644 taglib/tests/data/test.s3m create mode 100644 taglib/tests/data/test.xm create mode 100644 taglib/tests/data/unsupported-extension.xxx create mode 100644 taglib/tests/data/unsynch.id3 create mode 100644 taglib/tests/data/w000.mp3 create mode 100644 taglib/tests/data/xing.mp3 create mode 100644 taglib/tests/data/zero-size-chunk.wav create mode 100644 taglib/tests/main.cpp create mode 100644 taglib/tests/test_aiff.cpp create mode 100644 taglib/tests/test_ape.cpp create mode 100644 taglib/tests/test_apetag.cpp create mode 100644 taglib/tests/test_asf.cpp create mode 100644 taglib/tests/test_bytevector.cpp create mode 100644 taglib/tests/test_bytevectorlist.cpp create mode 100644 taglib/tests/test_bytevectorstream.cpp create mode 100644 taglib/tests/test_fileref.cpp create mode 100644 taglib/tests/test_flac.cpp create mode 100644 taglib/tests/test_flacpicture.cpp create mode 100644 taglib/tests/test_flacunknownmetadatablock.cpp create mode 100644 taglib/tests/test_id3v1.cpp create mode 100644 taglib/tests/test_id3v2.cpp create mode 100644 taglib/tests/test_info.cpp create mode 100644 taglib/tests/test_it.cpp create mode 100644 taglib/tests/test_list.cpp create mode 100644 taglib/tests/test_map.cpp create mode 100644 taglib/tests/test_mod.cpp create mode 100644 taglib/tests/test_mp4.cpp create mode 100644 taglib/tests/test_mp4coverart.cpp create mode 100644 taglib/tests/test_mp4item.cpp create mode 100644 taglib/tests/test_mpc.cpp create mode 100644 taglib/tests/test_mpeg.cpp create mode 100644 taglib/tests/test_ogg.cpp create mode 100644 taglib/tests/test_oggflac.cpp create mode 100644 taglib/tests/test_opus.cpp create mode 100644 taglib/tests/test_propertymap.cpp create mode 100644 taglib/tests/test_riff.cpp create mode 100644 taglib/tests/test_s3m.cpp create mode 100644 taglib/tests/test_string.cpp create mode 100644 taglib/tests/test_synchdata.cpp create mode 100644 taglib/tests/test_trueaudio.cpp create mode 100644 taglib/tests/test_wav.cpp create mode 100644 taglib/tests/test_wavpack.cpp create mode 100644 taglib/tests/test_xiphcomment.cpp create mode 100644 taglib/tests/test_xm.cpp create mode 100644 taglib/tests/utils.h diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7b0b77f3c0972c9565c918db72666383ff2f68fc GIT binary patch literal 28 jcmezWPnki1p^%}3!Hc1kA)6tU!HFTCp@M;zfr|kEa9joT literal 0 HcmV?d00001 diff --git a/cdatabase.cpp b/cdatabase.cpp new file mode 100644 index 0000000..9f2ab5a --- /dev/null +++ b/cdatabase.cpp @@ -0,0 +1,137 @@ +#include "cdatabase.h" +#include +#include + + +cDatabase::cDatabase(QObject *parent) : + QObject(parent), m_iVersion(-1) +{ + init(); +} + +cDatabase::~cDatabase() +{ + if(m_DB.isOpen()) + m_DB.close(); +} + +QSqlDatabase cDatabase::getDB() +{ + return(m_DB); +} + +void cDatabase::init() +{ + QDir d; + QString szDB; + QSettings settings; + + m_DB = QSqlDatabase::addDatabase("QSQLITE"); + szDB = settings.value("application/data").toString()+QDir::separator()+settings.value("database/database").toString(); + m_DB.setDatabaseName(szDB); + m_DB.open(); + + QSqlQuery version("select version from version", m_DB); + if(!version.exec()) + m_iVersion = createDB(); + else + { + version.first(); + m_iVersion = version.value(0).toInt(); + } +} + +int cDatabase::createDB() +{ + QSqlQuery query(m_DB); + + if(!query.exec("CREATE TABLE version\n" + "(version integer)")) + return(0); + if(!query.exec("INSERT INTO version (version) VALUES (1)")) + return(0); + + if(!query.exec("CREATE TABLE file\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " filename TEXT NOT NULL,\n" + " filetype INTEGER DEFAULT (0),\n" + " length INTEGER DEFAULT (-1),\n" + " bitrate INTEGER DEFAULT (-1),\n" + " samplerate INTEGER DEFAULT (-1),\n" + " channels INTEGER DEFAULT (-1),\n" + " bitspersample INTEGER DEFAULT (-1),\n" + " layer INTEGER DEFAULT (-1),\n" + " version INTEGER DEFAULT (-1),\n" + " samplewidth INTEGER DEFAULT (-1),\n" + " sampleframes INTEGER DEFAULT (0),\n" + " isencrypted INTEGER DEFAULT (0),\n" + " trackgain INTEGER DEFAULT (-1),\n" + " albumgain INTEGER DEFAULT (-1),\n" + " trackpeak INTEGER DEFAULT (-1),\n" + " albumpeak INTEGER DEFAULT (-1),\n" + " protectionenabled INTEGER DEFAULT (0),\n" + " channelmode INTEGER DEFAULT (0),\n" + " iscopyrighted INTEGER DEFAULT (0),\n" + " isoriginal INTEGER DEFAULT (0),\n" + " album TEXT,\n" + " comment TEXT,\n" + " title TEXT\n," + " rating TEXT,\n" + " copyright TEXT,\n" + " track TEXT,\n" + " year INTEGER)")) + return(0); + + if(!query.exec("CREATE TABLE id3v1artist\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " fileid INTEGER NOT NULL,\n" + " person TEXT NOT NULL)")) + return(0); + + if(!query.exec("CREATE TABLE id3v1genre\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " fileid INTEGER NOT NULL,\n" + " genre TEXT NOT NULL)")) + return(0); + + if(!query.exec("CREATE TABLE id3v2\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " fileid INTEGER NOT NULL,\n" + " version INTEGER DEFAULT (-1),\n" + " revision INTEGER DEFAULT (-1),\n" + " size INTEGER DEFAULT (-1))")) + return(0); + + if(!query.exec("CREATE TABLE id3v2tag\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " id3v2id INTEGER NOT NULL,\n" + " tag TEXT NOT NULL,\n" + " value TEXT)")) + return(0); + + if(!query.exec("CREATE TABLE ape\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " fileid INTEGER NOT NULL\n)")) + return(0); + + if(!query.exec("CREATE TABLE apetag\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " apeid INTEGER NOT NULL,\n" + " tag TEXT NOT NULL,\n" + " value TEXT)")) + return(0); + + if(!query.exec("CREATE TABLE properties\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " fileid INTEGER NOT NULL)")) + return(0); + + if(!query.exec("CREATE TABLE propertiestag\n" + "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n" + " propertiesid INTEGER NOT NULL,\n" + " tag TEXT NOT NULL,\n" + " value TEXT)")) + return(0); + + return(1); +} diff --git a/cdatabase.h b/cdatabase.h new file mode 100644 index 0000000..ddc227c --- /dev/null +++ b/cdatabase.h @@ -0,0 +1,31 @@ +#ifndef CDATABASE_H +#define CDATABASE_H + +#include +#include +#include +#include +#include + + +class cDatabase : public QObject +{ + Q_OBJECT +public: + explicit cDatabase(QObject *parent = 0); + ~cDatabase(); + + QSqlDatabase getDB(); +signals: + +public slots: + +protected: + QSqlDatabase m_DB; + qint16 m_iVersion; + + void init(); + int createDB(); +}; + +#endif // CDATABASE_H diff --git a/cimage.cpp b/cimage.cpp new file mode 100644 index 0000000..b16d411 --- /dev/null +++ b/cimage.cpp @@ -0,0 +1,26 @@ +#include "cimage.h" + + +cImage::cImage() : + QImage() +{ +} + +cImage::cImage(const QByteArray& array, const QString& szFileName, const cImage::ImageType& imageType, const QString& szDescription) : + QImage(QImage::fromData(array)), + m_szFileName(szFileName), + m_ImageType(imageType), + m_szDescription(szDescription) +{ +} + +cImageList::cImageList() +{ +} + +cImage* cImageList::add(const QByteArray& array, const QString& szFileName, const cImage::ImageType& imageType, const QString& szDescription) +{ + cImage* lpNew = new cImage(array, szFileName, imageType, szDescription); + append(lpNew); + return(lpNew); +} diff --git a/cimage.h b/cimage.h new file mode 100644 index 0000000..be72145 --- /dev/null +++ b/cimage.h @@ -0,0 +1,58 @@ +#ifndef CIMAGE_H +#define CIMAGE_H + +#include +#include +#include + + +class cImage : public QImage +{ +public: + enum ImageType + { + Other = 0x00, + FileIcon = 0x01, + OtherFileIcon = 0x02, + FrontCover = 0x03, + BackCover = 0x04, + LeafletPage = 0x05, + Media = 0x06, + LeadArtist = 0x07, + Artist = 0x08, + Conductor = 0x09, + Band = 0x0A, + Composer = 0x0B, + Lyricist = 0x0C, + RecordingLocation = 0x0D, + DuringRecording = 0x0E, + DuringPerformance = 0x0F, + MovieScreenCapture = 0x10, + ColouredFish = 0x11, + Illustration = 0x12, + BandLogo = 0x13, + PublisherLogo = 0x14 + }; + explicit cImage(); + explicit cImage(const QByteArray& array, const QString& szFileName, const cImage::ImageType& imageType, const QString& szDescription); + +protected: + QString m_szFileName; + ImageType m_ImageType; + QString m_szDescription; +signals: + +public slots: + +}; + +Q_DECLARE_METATYPE(cImage) + +class cImageList : public QList +{ +public: + cImageList(); + cImage* add(const QByteArray& array, const QString& szFileName, const cImage::ImageType& imageType, const QString& szDescription); +}; + +#endif // CIMAGE_H diff --git a/cmainwindow.cpp b/cmainwindow.cpp new file mode 100644 index 0000000..ede24ec --- /dev/null +++ b/cmainwindow.cpp @@ -0,0 +1,212 @@ +#include + +#include "cmainwindow.h" +#include "ui_cmainwindow.h" + +#include "cdatabase.h" +#include "cmediainfo.h" + + +void addFile(const QString& szFile, const QSqlDatabase db) +{ + if(szFile.isEmpty()) + return; + cMediaInfo* lpMediaInfo = new cMediaInfo; + lpMediaInfo->readFromFile(szFile); + if(lpMediaInfo->isValid()) + lpMediaInfo->writeToDB(db); + delete lpMediaInfo; +} + +void addPath(const QString& szPath, const QSqlDatabase db) +{ + QDir Dir(szPath); + QStringList Dirs = Dir.entryList(QDir::Dirs); + QStringList Files = Dir.entryList(QDir::Files); + int z; + + if(!Files.isEmpty()) + { + for(z = 0;z < Files.count();z++) + addFile(szPath + QString("/") + Files.at(z), db); + } + + Dirs.removeAll("."); + Dirs.removeAll(("..")); + + if(!Dirs.isEmpty()) + { + for(z = 0;z < Dirs.count();z++) + addPath(szPath + QString("/") + Dirs.at(z), db); + } +} + +cMainWindow::cMainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::cMainWindow) +{ + QSettings settings; + + if(settings.value("application/version", QVariant(0.0)).toDouble() == 0.0) + initSettings(); + + cDatabase* lpDatabase; + lpDatabase = new cDatabase(this); +/* + bool bRet; + bRet = lpDatabase->getDB().transaction(); + addPath("C:\\Users\\birkeh\\Music", lpDatabase->getDB()); + bRet = lpDatabase->getDB().commit(); +*/ + + cMediaInfo* lpMediaInfo; + lpMediaInfo = new cMediaInfo(this); +// lpMediaInfo->readFromFile("C:\\Users\\birkeh\\Music\\Wig\\A Day In The Life\\CD1\\Wig - A Day In The Life - Part 1 - Wake Up.mp3"); +// lpMediaInfo->readFromFile("C:\\Users\\birkeh\\Music\\Amy MacDonald\\Life In A Beautiful Light\\CD1\\Amy Macdonald - Life In A Beautiful Light - 05 - The Game.mp3"); +// lpMediaInfo->readFromFile("C:\\Users\\birkeh\\Music\\Amy MacDonald\\A Curious Thing\\CD1\\Amy MacDonald - A Curious Thing - 01 - Don't tell me it's over.mp3"); +// lpMediaInfo->readFromFile("C:\\Users\\birkeh\\Music\\id3v1\\id3v1_140_genre_W.mp3"); + lpMediaInfo->readFromFile("C:\\Users\\birkeh\\Documents\\__P__\\MP3Test\\From-The-Machine-World.mp3"); + + QString sz; + QStringList szL; + qint16 i; + qint32 i32; + qint64 i64; + QDateTime dt; + bool b; + cImageList images; + cStringTimeList szTL; + cMediaInfo::MEDIA_TYPE mt; + cMediaInfo::CHANNEL_MODE cm; + + sz = lpMediaInfo->fileName(); + mt = lpMediaInfo->fileType1(); + sz = lpMediaInfo->fileType1Text(); + + i32 = lpMediaInfo->length1(); + sz = lpMediaInfo->length1Text(); + i = lpMediaInfo->bitrate(); + sz = lpMediaInfo->bitrateText(); + i32 = lpMediaInfo->sampleRate(); + sz = lpMediaInfo->sampleRateText(); + i = lpMediaInfo->channels(); + sz = lpMediaInfo->channelsText(); + i = lpMediaInfo->bitsPerSample(); + sz = lpMediaInfo->bitsPerSampleText(); + i = lpMediaInfo->layer(); + sz = lpMediaInfo->layerText(); + i = lpMediaInfo->version(); + sz = lpMediaInfo->versionText(); + i = lpMediaInfo->sampleWidth(); + sz = lpMediaInfo->sampleWidthText(); + i64 = lpMediaInfo->sampleFrames(); + sz = lpMediaInfo->sampleFramesText(); + b = lpMediaInfo->isEncrypted(); + i = lpMediaInfo->trackGain(); + i = lpMediaInfo->albumGain(); + i = lpMediaInfo->trackPeak(); + i = lpMediaInfo->albumPeak(); + b = lpMediaInfo->protectionEnabled(); + cm = lpMediaInfo->channelMode(); + sz = lpMediaInfo->channelModeText(); + b = lpMediaInfo->isCopyrighted(); + b = lpMediaInfo->isOriginal(); + + sz = lpMediaInfo->contentGroupDescription(); + sz = lpMediaInfo->title(); + sz = lpMediaInfo->subTitle(); + sz = lpMediaInfo->album(); + sz = lpMediaInfo->originalAlbum(); + sz = lpMediaInfo->trackNumber(); + sz = lpMediaInfo->partOfSet(); + sz = lpMediaInfo->subTitleOfSet(); + sz = lpMediaInfo->internationalStandardRecordingCode(); + + sz = lpMediaInfo->leadArtist(); + sz = lpMediaInfo->band(); + sz = lpMediaInfo->conductor(); + szL = lpMediaInfo->interpret(); + sz = lpMediaInfo->originalArtist(); + sz = lpMediaInfo->textWriter(); + sz = lpMediaInfo->originalTextWriter(); + sz = lpMediaInfo->composer(); + sz = lpMediaInfo->encodedBy(); + + i = lpMediaInfo->beatsPerMinute(); + i = lpMediaInfo->length(); + szL = lpMediaInfo->language(); + szL = lpMediaInfo->contentType(); + sz = lpMediaInfo->fileType(); + szL = lpMediaInfo->mediaType(); + sz = lpMediaInfo->mood(); + + sz = lpMediaInfo->copyright(); + sz = lpMediaInfo->producedNotice(); + sz = lpMediaInfo->publisher(); + sz = lpMediaInfo->fileOwner(); + sz = lpMediaInfo->internetRadioStationName(); + sz = lpMediaInfo->internetRadioStationOwner(); + + sz = lpMediaInfo->originalFilename(); + i = lpMediaInfo->playlistDelay(); + dt = lpMediaInfo->encodingTime(); + dt = lpMediaInfo->originalReleaseTime(); + dt = lpMediaInfo->recordingTime(); + dt = lpMediaInfo->releaseTime(); + dt = lpMediaInfo->taggingTime(); + szL = lpMediaInfo->swhwSettings(); + sz = lpMediaInfo->albumSortOrder(); + sz = lpMediaInfo->performerSortOrder(); + sz = lpMediaInfo->titleSortOrder(); + + szL = lpMediaInfo->unsynchronizedLyrics(); + szTL = lpMediaInfo->synchronizedLyrics(); + + images = lpMediaInfo->images(); + +// lpMediaInfo->writeToDB(lpDatabase->getDB()); + delete lpMediaInfo; + +/* + lpMediaInfo = new cMediaInfo(this); + lpMediaInfo->readFromFile("/data/Musik/MP3/Wig/First/CD1/Wig - First - 01 - Arie.flac"); + lpMediaInfo->writeToDB(lpDatabase->getDB()); + delete lpMediaInfo; + + lpMediaInfo = new cMediaInfo(this); + lpMediaInfo->readFromFile("/data/Musik/MP3/Wig/First/CD1/Wig - First - 01 - Arie.m4a"); + lpMediaInfo->writeToDB(lpDatabase->getDB()); + delete lpMediaInfo; + + lpMediaInfo = new cMediaInfo(this); + lpMediaInfo->readFromFile("/data/Musik/MP3/Wig/First/CD1/Wig - First - 01 - Arie.mp3"); + lpMediaInfo->writeToDB(lpDatabase->getDB()); + delete lpMediaInfo; + + lpMediaInfo = new cMediaInfo(this); + lpMediaInfo->readFromFile("/data/Musik/MP3/Wig/First/CD1/sh3.ape"); + lpMediaInfo->writeToDB(lpDatabase->getDB()); + delete lpMediaInfo; +*/ + ui->setupUi(this); + + delete (lpDatabase); +} + +cMainWindow::~cMainWindow() +{ + delete ui; +} + +void cMainWindow::initSettings() +{ + QSettings settings; + QDir dir; + + settings.setValue("application/version", QVariant(0.1)); + settings.setValue("application/data", QDir::homePath()+QDir::separator()+".qtjukebox"); + settings.setValue("database/type", "SQLITE"); + settings.setValue("database/database", "qtjukebox.db"); + + dir.mkdir(settings.value("application/data").toString()); +} diff --git a/cmainwindow.h b/cmainwindow.h new file mode 100644 index 0000000..7b76e8b --- /dev/null +++ b/cmainwindow.h @@ -0,0 +1,27 @@ +#ifndef CMAINWINDOW_H +#define CMAINWINDOW_H + +#include + + +namespace Ui +{ + class cMainWindow; +} + +class cMainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit cMainWindow(QWidget *parent = 0); + ~cMainWindow(); + +private: + Ui::cMainWindow *ui; + +protected: + void initSettings(); +}; + +#endif // CMAINWINDOW_H diff --git a/cmainwindow.ui b/cmainwindow.ui new file mode 100644 index 0000000..2f025dc --- /dev/null +++ b/cmainwindow.ui @@ -0,0 +1,24 @@ + + cMainWindow + + + + 0 + 0 + 400 + 300 + + + + cMainWindow + + + + + + + + + + + diff --git a/cmediainfo.cpp b/cmediainfo.cpp new file mode 100644 index 0000000..ea1604a --- /dev/null +++ b/cmediainfo.cpp @@ -0,0 +1,1282 @@ +#include "cmediainfo.h" + +#include +#include +#include + +#include +#include + +#include + + +cMediaInfo::cMediaInfo(bool bAPE, bool bID3V1, bool bID3V2, bool bProperties, QObject *parent) : + QObject(parent), m_bAPE(bAPE), m_bID3V1(bID3V1), m_bID3V2(bID3V2), m_bProperties(bProperties), m_bIsValid(false), m_FileType(MEDIA_TYPE_UNKNOWN) +{ +} + +cMediaInfo::~cMediaInfo() +{ + clear(); +} + +void cMediaInfo::clear() +{ + m_szFileName = ""; + m_FileType = MEDIA_TYPE_UNKNOWN; + m_bIsValid = false; + + m_iLength = -1; + m_iBitrate = -1; + m_iSampleRate = -1; + m_iChannels = -1; + m_iBitsPerSample = -1; + m_iLayer = -1; + m_iVersion = -1; + m_iSampleWidth = -1; + m_ullSampleFrames = 0; + m_bIsEncrypted = false; + m_iTrackGain = -1; + m_iAlbumGain = -1; + m_iTrackPeak = -1; + m_iAlbumPeak = -1; + m_bProtectionEnabled = false; + m_channelMode = CHANNEL_MODE_UNKNOWN; + m_bIsCopyrighted = false; + m_bIsOriginal = false; + + m_szContentGroupDescription = ""; + m_szTitle = ""; + m_szSubTitle = ""; + m_szAlbum = ""; + m_szOriginalAlbum = ""; + m_szTrackNumber = ""; + m_szPartOfSet = ""; + m_szSubTitleOfSet = ""; + m_szInternationalStandardRecordingCode = ""; + + m_szContentGroupDescription = ""; + m_szTitle = ""; + m_szSubTitle = ""; + m_szAlbum = ""; + m_szOriginalAlbum = ""; + m_szTrackNumber = ""; + m_szPartOfSet = ""; + m_szSubTitleOfSet = ""; + m_szInternationalStandardRecordingCode = ""; + + m_szLeadArtist = ""; + m_szBand = ""; + m_szConductor = ""; + m_szInterpret.clear(); + m_szOriginalArtist = ""; + m_szTextWriter = ""; + m_szOriginalTextWriter = ""; + m_szComposer = ""; + m_szEncodedBy = ""; + + m_iBeatsPerMinute = -1; + m_szLanguage.clear(); + m_szContentType.clear(); + m_szFileType = ""; + m_szMediaType.clear(); + m_szMood = ""; + + m_szCopyright = ""; + m_szProducedNotice = ""; + m_szPublisher = ""; + m_szFileOwner = ""; + m_szInternetRadioStationName = ""; + m_szInternetRadioStationOwner = ""; + + m_szOriginalFilename = ""; + m_iPlaylistDelay = -1; + m_szswhwSettings.clear(); + m_szAlbumSortOrder = ""; + m_szPerformerSortOrder = ""; + m_szTitleSortOrder = ""; + + m_szSynchronizedLyrics.clear(); + m_szSynchronizedLyricsLanguage = ""; + m_szSynchronizedLyricsDescription = ""; + + m_szUnsynchronizedLyrics.clear(); + m_szUnsynchronizedLyricsLanguage = ""; + m_szUnsynchronizedLyricsDescription = ""; + + m_imageList.clear(); + + m_szAlbum = ""; + m_szArtistList.clear(); + m_szAlbumArtistList.clear(); + m_szComposerList.clear(); + m_szComment = ""; + m_szGenreList.clear(); + m_szTitle = ""; + m_szRating = ""; + m_szCopyright = ""; + m_szTrackNumber = ""; + m_iYear = -1; + + m_iID3v2Version = -1; + m_iID3v2Revision = -1; + m_iID3v2Size = -1; + + m_TAGAPEList.clear(); + m_TAGPropertiesList.clear(); + + m_imageList.clear(); +} + +bool cMediaInfo::readFromFile(const QString& szFileName) +{ + clear(); + + QFileInfo FileInfo(szFileName); + if(!FileInfo.exists()) + return(false); + + if(!FileInfo.suffix().compare("APE", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_APE; + else if(!FileInfo.suffix().compare("WMA", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_ASF; + else if(!FileInfo.suffix().compare("FLAC", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_FLAC; + else if(!FileInfo.suffix().compare("AAC", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_MP4; + else if(!FileInfo.suffix().compare("MP4", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_MP4; + else if(!FileInfo.suffix().compare("M4A", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_MP4; + else if(!FileInfo.suffix().compare("MPC", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_MPC; + else if(!FileInfo.suffix().compare("MP1", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_MPEG; + else if(!FileInfo.suffix().compare("MP2", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_MPEG; + else if(!FileInfo.suffix().compare("MP3", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_MPEG; + else if(!FileInfo.suffix().compare("TTA", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_TRUEAUDIO; + else if(!FileInfo.suffix().compare("WV", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_WAVPACK; + else if(!FileInfo.suffix().compare("WAV", Qt::CaseInsensitive)) + m_FileType = MEDIA_TYPE_WAV; + + if(m_FileType == MEDIA_TYPE_UNKNOWN) + return(false); + + m_szFileName = szFileName; + ID3v1::Tag* lpTagV1 = 0; + ID3v2::Tag* lpTagV2 = 0; + APE::Tag* lpTagAPE = 0; + APE::File* lpApe = 0; + ASF::File* lpASF = 0; + FLAC::File* lpFlac = 0; + MP4::File* lpMP4 = 0; + MPC::File* lpMPC = 0; + MPEG::File* lpMPEG = 0; + TrueAudio::File* lpTrueAudio = 0; + WavPack::File* lpWavPack = 0; + RIFF::WAV::File* lpWav = 0; + TagLib::PropertyMap tags; + + QString szTmp; + + switch(m_FileType) + { + case MEDIA_TYPE_APE: + { + lpApe = new APE::File(m_szFileName.toLocal8Bit().data()); + + lpTagV1 = lpApe->ID3v1Tag(); + lpTagAPE = lpApe->APETag(); + tags = lpApe->properties(); + + APE::Properties* lpAudioProperties = lpApe->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_iBitsPerSample = lpAudioProperties->bitsPerSample(); + m_iVersion = lpAudioProperties->version(); + break; + } + case MEDIA_TYPE_ASF: + { + lpASF = new ASF::File(m_szFileName.toLocal8Bit().data()); + TagLib::ASF::Tag* lpTag = lpASF->tag(); + m_szTitle = QString(lpTag->title().toCString()); + szTmp = QString(lpTag->artist().toCString()); + if(!szTmp.isEmpty() && m_bID3V1) + m_szArtistList = szTmp.split("\n"); + m_szAlbum = QString(lpTag->album().toCString()); + m_szComment = QString(lpTag->comment().toCString()); + szTmp = QString(lpTag->genre().toCString()); + if(!szTmp.isEmpty() && m_bID3V1) + m_szGenreList = szTmp.split("\n"); + m_szRating = QString(lpTag->genre().toCString()); + m_szCopyright = QString(lpTag->copyright().toCString()); + m_iYear = lpTag->year(); + m_szTrackNumber = QString("%1").arg(lpTag->track()); + + tags = lpASF->properties(); + + ASF::Properties* lpAudioProperties = lpASF->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_bIsEncrypted = lpAudioProperties->isEncrypted(); + break; + } + case MEDIA_TYPE_FLAC: + { + lpFlac = new FLAC::File(m_szFileName.toLocal8Bit().data()); + lpTagV1 = lpFlac->ID3v1Tag(); + lpTagV2 = lpFlac->ID3v2Tag(); + tags = lpFlac->properties(); + + FLAC::Properties* lpAudioProperties = lpFlac->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_iSampleWidth = lpAudioProperties->sampleWidth(); + m_ullSampleFrames = lpAudioProperties->sampleFrames(); + break; + } + case MEDIA_TYPE_MP4: + { + lpMP4 = new MP4::File(m_szFileName.toLocal8Bit().data()); + TagLib::MP4::Tag* lpTag = lpMP4->tag(); + m_szTitle = QString(lpTag->title().toCString()); + szTmp = QString(lpTag->artist().toCString()); + if(!szTmp.isEmpty() && m_bID3V1) + m_szArtistList = szTmp.split("\n"); + m_szAlbum = QString(lpTag->album().toCString()); + m_szComment = QString(lpTag->comment().toCString()); + szTmp = QString(lpTag->genre().toCString()); + if(!szTmp.isEmpty() && m_bID3V1) + m_szGenreList = szTmp.split("\n"); + m_iYear = lpTag->year(); + m_szTrackNumber = QString("%1").arg(lpTag->track()); + + tags = lpMP4->properties(); + + MP4::Properties* lpAudioProperties = lpMP4->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_iBitsPerSample = lpAudioProperties->bitsPerSample(); + m_bIsEncrypted = lpAudioProperties->isEncrypted(); + break; + } + case MEDIA_TYPE_MPC: + { + lpMPC = new MPC::File(m_szFileName.toLocal8Bit().data()); + lpTagV1 = lpMPC->ID3v1Tag(); + lpTagAPE = lpMPC->APETag(); + tags = lpMPC->properties(); + + MPC::Properties* lpAudioProperties = lpMPC->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_iVersion = lpAudioProperties->mpcVersion(); + m_ullSampleFrames = lpAudioProperties->sampleFrames(); + m_iTrackGain = lpAudioProperties->trackGain(); + m_iAlbumGain = lpAudioProperties->albumGain(); + m_iTrackPeak = lpAudioProperties->trackPeak(); + m_iAlbumPeak = lpAudioProperties->albumPeak(); + break; + } + case MEDIA_TYPE_MPEG: + { + lpMPEG = new MPEG::File(m_szFileName.toLocal8Bit().data()); + lpTagV1 = lpMPEG->ID3v1Tag(); + lpTagV2 = lpMPEG->ID3v2Tag(); + lpTagAPE = lpMPEG->APETag(); + tags = lpMPEG->properties(); + + MPEG::Properties* lpAudioProperties = lpMPEG->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_iLayer = lpAudioProperties->layer(); + switch(lpAudioProperties->version()) + { + case MPEG::Header::Version1: + m_iVersion = 10; + break; + case MPEG::Header::Version2: + m_iVersion = 20; + break; + case MPEG::Header::Version2_5: + m_iVersion = 25; + break; + } + m_bProtectionEnabled = lpAudioProperties->protectionEnabled(); + switch(lpAudioProperties->channelMode()) + { + case MPEG::Header::Stereo: + m_channelMode = CHANNEL_MODE_STEREO; + break; + case MPEG::Header::JointStereo: + m_channelMode = CHANNEL_MODE_JOINTSTEREO; + break; + case MPEG::Header::DualChannel: + m_channelMode = CHANNEL_MODE_DUALMONO; + break; + case MPEG::Header::SingleChannel: + m_channelMode = CHANNEL_MODE_MONO; + break; + } + m_bIsCopyrighted = lpAudioProperties->isCopyrighted(); + m_bIsOriginal = lpAudioProperties->isOriginal(); + break; + } + case MEDIA_TYPE_TRUEAUDIO: + { + lpTrueAudio = new TrueAudio::File(m_szFileName.toLocal8Bit().data()); + lpTagV1 = lpTrueAudio->ID3v1Tag(); + lpTagV2 = lpTrueAudio->ID3v2Tag(); + tags = lpTrueAudio->properties(); + + TrueAudio::Properties* lpAudioProperties = lpTrueAudio->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_iBitsPerSample = lpAudioProperties->bitsPerSample(); + m_iVersion = lpAudioProperties->ttaVersion(); + break; + } + case MEDIA_TYPE_WAVPACK: + { + lpWavPack = new WavPack::File(m_szFileName.toLocal8Bit().data()); + lpTagV1 = lpWavPack->ID3v1Tag(); + lpTagAPE = lpWavPack->APETag(); + tags = lpWavPack->properties(); + + WavPack::Properties* lpAudioProperties = lpWavPack->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_iBitsPerSample = lpAudioProperties->bitsPerSample(); + m_ullSampleFrames = lpAudioProperties->sampleFrames(); + m_iVersion = lpAudioProperties->version(); + break; + } + case MEDIA_TYPE_WAV: + { + lpWav = new RIFF::WAV::File(m_szFileName.toLocal8Bit().data()); + lpTagV2 = lpWav->tag(); + tags = lpWav->properties(); + + RIFF::WAV::Properties* lpAudioProperties = lpWav->audioProperties(); + m_iLength = lpAudioProperties->length(); + m_iBitrate = lpAudioProperties->bitrate(); + m_iSampleRate = lpAudioProperties->sampleRate(); + m_iChannels = lpAudioProperties->channels(); + m_iSampleWidth = lpAudioProperties->sampleWidth(); + m_ullSampleFrames = lpAudioProperties->sampleFrames(); + break; + } + default: + break; + } + + if(lpTagV2 && m_bID3V2) + readTagV2(lpTagV2); + if(lpTagV1 && m_bID3V1) + readTagV1(lpTagV1); + if(lpTagAPE && m_bAPE) + readTagAPE(lpTagAPE); + if(m_bProperties) + readTagProperties(tags); + + if(lpApe) + delete lpApe; + if(lpASF) + delete lpASF; + if(lpFlac) + delete lpFlac; + if(lpMP4) + delete lpMP4; + if(lpMPC) + delete lpMPC; + if(lpMPEG) + delete lpMPEG; + if(lpTrueAudio) + delete lpTrueAudio; + if(lpWavPack) + delete lpWavPack; + if(lpWav) + delete lpWav; + + m_bIsValid = true; + return(true); +} + +bool cMediaInfo::readFromDB(const QSqlDatabase& db) +{ + return(true); +} +/* +qint32 cMediaInfo::writeFilename(const QSqlDatabase& db) +{ + QSqlQuery query(db); + QString sz; + + sz = QString("SELECT id FROM file WHERE filename = \"%1\"").arg(m_szFileName); + if(!query.exec(sz)) + return(-1); + query.first(); + if(query.isValid()) + return(-1); + + sz = QString("INSERT INTO file (filename, filetype, length, bitrate, samplerate, channels, bitspersample, layer, version, samplewidth, sampleframes, isencrypted, trackgain, albumgain, trackpeak, albumpeak, protectionenabled, channelmode, iscopyrighted, isoriginal, album, comment, title, rating, copyright, track, year)\n" + "values\n" + "(\"%1\", %2, %3, %4, %5, %6, %7, %8, %9, %10,\n" + " %11, %12, %13, %14, %15, %16, %17, %18, %19, %20, \"%21\", \"%22\", \"%23\", \"%24\", \"%25\", \"%26\", %27)").arg(m_szFileName).arg(m_FileType).arg(m_iLength).arg(m_iBitrate).arg(m_iSampleRate).arg(m_iChannels).arg(m_iBitsPerSample).arg(m_iLayer).arg(m_iVersion).arg(m_iSampleWidth).arg(m_ullSampleFrames).arg(m_bIsEncrypted).arg(m_iTrackGain).arg(m_iAlbumGain).arg(m_iTrackPeak).arg(m_iAlbumPeak).arg(m_bProtectionEnabled).arg(m_channelMode).arg(m_bIsCopyrighted).arg(m_bIsOriginal).arg(m_szAlbum).arg(m_szComment).arg(m_szTitle).arg(m_szRating).arg(m_szCopyright).arg(m_szTrackNumber).arg(m_iYear); + + if(!query.exec(sz)) + return(-1); + + sz = QString("SELECT id FROM file WHERE filename = \"%1\"").arg(m_szFileName); + if(!query.exec(sz)) + return(-1); + query.first(); + if(!query.isValid()) + return(-1); + return(query.value(0).toInt()); +} + +qint32 cMediaInfo::writeArtist(const QSqlDatabase& db, qint32 idFile, const QString& szArtist) +{ + QSqlQuery query(db); + QString sz; + + sz = QString("SELECT id FROM id3v1artist WHERE fileid = %1 AND person = \"%2\"").arg(idFile).arg(szArtist); + if(!query.exec(sz)) + return(-1); + query.first(); + if(query.isValid()) + return(-1); + + for(int z = 0;z < m_szArtistList.count();z++) + { + QString szArtist = m_szArtistList.at(z); + sz = QString("INSERT INTO id3v1artist (fileid, person) VALUES (%1, \"%2\")").arg(idFile).arg(szArtist); + if(!query.exec(sz)) + return(-1); + } +} +*/ +bool cMediaInfo::writeToDB(const QSqlDatabase& db) +{ +/* + QSqlQuery query(db); + QString sz; + qint32 idFile; + qint32 id3v2; + qint32 idAPE; + qint32 idProperties; + + idFile = writeFilename(db); + if(idFile == -1) + return(false); + + if(m_szArtistList.count()) + { + sz = QString("SELECT id FROM id3v1artist WHERE fileid = %1").arg(idFile); + if(!query.exec(sz)) + return(false); + query.first(); + if(query.isValid()) + return(false); + + for(int z = 0;z < m_szArtistList.count();z++) + { + QString szArtist = m_szArtistList.at(z); + sz = QString("INSERT INTO id3v1artist (fileid, person) VALUES (%1, \"%2\")").arg(idFile).arg(szArtist); + if(!query.exec(sz)) + return(false); + } + } + + if(m_szGenreList.count()) + { + sz = QString("SELECT id FROM id3v1genre WHERE fileid = %1").arg(idFile); + if(!query.exec(sz)) + return(false); + query.first(); + if(query.isValid()) + return(false); + + for(int z = 0;z < m_szGenreList.count();z++) + { + QString szGenre = m_szGenreList.at(z); + sz = QString("INSERT INTO id3v1genre (fileid, genre) VALUES (%1, \"%2\")").arg(idFile).arg(szGenre); + if(!query.exec(sz)) + return(false); + } + } + + if(m_TAGv2List.count()) + { + sz = QString("SELECT id FROM id3v2 WHERE fileid = %1").arg(idFile); + if(!query.exec(sz)) + return(false); + query.first(); + if(query.isValid()) + return(false); + + sz = QString("INSERT INTO id3v2 (fileid, version, revision, size) VALUES (%1, %2, %3, %4)").arg(idFile).arg(m_iID3v2Version).arg(m_iID3v2Revision).arg(m_iID3v2Size); + if(!query.exec(sz)) + return(false); + sz = QString("SELECT id FROM id3v2 WHERE fileid = %1").arg(idFile); + if(!query.exec(sz)) + return(false); + query.first(); + id3v2 = query.value(0).toInt(); + + for(int z = 0;z < m_TAGv2List.count();z++) + { + cTAG* lpTag = m_TAGv2List.at(z); + QString szTag = lpTag->tag(); + QStringList szValue = lpTag->valueList(); + for(int y = 0;y < szValue.count();y++) + { + sz = QString("INSERT INTO id3v2tag (id3v2id, tag, value) VALUES (%1, \"%2\", \"%3\")").arg(id3v2).arg(szTag).arg(szValue.at(y)); + if(!query.exec(sz)) + return(false); + } + } + } + + if(m_TAGAPEList.count()) + { + sz = QString("SELECT id FROM ape WHERE fileid = %1").arg(idFile); + if(!query.exec(sz)) + return(false); + query.first(); + if(query.isValid()) + return(false); + + sz = QString("INSERT INTO ape (fileid) VALUES (%1)").arg(idFile); + if(!query.exec(sz)) + return(false); + sz = QString("SELECT id FROM ape WHERE fileid = %1").arg(idFile); + if(!query.exec(sz)) + return(false); + query.first(); + idAPE = query.value(0).toInt(); + + for(int z = 0;z < m_TAGAPEList.count();z++) + { + cTAG* lpTag = m_TAGAPEList.at(z); + QString szTag = lpTag->tag(); + QStringList szValue = lpTag->valueList(); + for(int y = 0;y < szValue.count();y++) + { + sz = QString("INSERT INTO apetag (apeid, tag, value) VALUES (%1, \"%2\", \"%3\")").arg(idAPE).arg(szTag).arg(szValue.at(y)); + if(!query.exec(sz)) + return(false); + } + } + } + + if(m_TAGPropertiesList.count()) + { + sz = QString("SELECT id FROM properties WHERE fileid = %1").arg(idFile); + if(!query.exec(sz)) + return(false); + query.first(); + if(query.isValid()) + return(false); + + sz = QString("INSERT INTO properties (fileid) VALUES (%1)").arg(idFile); + if(!query.exec(sz)) + return(false); + sz = QString("SELECT id FROM properties WHERE fileid = %1").arg(idFile); + if(!query.exec(sz)) + return(false); + query.first(); + idProperties = query.value(0).toInt(); + + for(int z = 0;z < m_TAGPropertiesList.count();z++) + { + cTAG* lpTag = m_TAGPropertiesList.at(z); + QString szTag = lpTag->tag(); + QStringList szValue = lpTag->valueList(); + for(int y = 0;y < szValue.count();y++) + { + sz = QString("INSERT INTO propertiestag (propertiesid, tag, value) VALUES (%1, \"%2\", \"%3\")").arg(idProperties).arg(szTag).arg(szValue.at(y)); + if(!query.exec(sz)) + return(false); + } + } + } +*/ + return(true); +} + +void cMediaInfo::readTagV1(ID3v1::Tag* lpTag) +{ + QString szTmp; + m_szAlbum = QString(lpTag->album().toCString()); + szTmp = QString(lpTag->artist().toCString()); + if(!szTmp.isEmpty()) + m_szArtistList = szTmp.split("\n"); + m_szAlbum = QString(lpTag->album().toCString()); + m_szComment = QString(lpTag->comment().toCString()); + szTmp = QString(lpTag->genre().toCString()); + if(!szTmp.isEmpty()) + m_szGenreList = szTmp.split("\n"); + m_szTitle = QString(lpTag->title().toCString()); + m_szTrackNumber = QString("%1").arg(lpTag->track()); + m_iYear = lpTag->year(); +} + +void cMediaInfo::readTagV2(ID3v2::Tag* lpTag) +{ + m_iID3v2Version = lpTag->header()->majorVersion(); + m_iID3v2Revision = lpTag->header()->revisionNumber(); + m_iID3v2Size = lpTag->header()->tagSize(); + + for(ID3v2::FrameList::ConstIterator it = lpTag->frameList().begin();it != lpTag->frameList().end();it++) + { + QString szID = QString("%1").arg((*it)->frameID().data()).left(4); + + if(!szID.compare("TIT1")) + m_szContentGroupDescription = (*it)->toString().toCString(); + else if(!szID.compare("TIT2")) + m_szTitle = (*it)->toString().toCString(); + else if(!szID.compare("TIT3")) + m_szSubTitle = (*it)->toString().toCString(); + else if(!szID.compare("TALB")) + m_szAlbum = (*it)->toString().toCString(); + else if(!szID.compare("TOAL")) + m_szOriginalAlbum = (*it)->toString().toCString(); + else if(!szID.compare("TRCK")) + m_szTrackNumber = (*it)->toString().toCString(); + else if(!szID.compare("TPOS")) + m_szPartOfSet = (*it)->toString().toCString(); + else if(!szID.compare("TSST")) + m_szSubTitleOfSet = (*it)->toString().toCString(); + else if(!szID.compare("TSRC")) + m_szInternationalStandardRecordingCode = (*it)->toString().toCString(); + else if(!szID.compare("TPE1")) + m_szLeadArtist = (*it)->toString().toCString(); + else if(!szID.compare("TPE2")) + m_szBand = (*it)->toString().toCString(); + else if(!szID.compare("TPE3")) + m_szConductor = (*it)->toString().toCString(); + else if(!szID.compare("TPE4")) + m_szInterpret = QString((*it)->toString().toCString()).split("\r\n"); + else if(!szID.compare("TOPE")) + m_szOriginalArtist = (*it)->toString().toCString(); + else if(!szID.compare("TEXT")) + m_szTextWriter = (*it)->toString().toCString(); + else if(!szID.compare("TOLY")) + m_szOriginalTextWriter = (*it)->toString().toCString(); + else if(!szID.compare("TCOM")) + m_szComposer = (*it)->toString().toCString(); + else if(!szID.compare("TENC")) + m_szEncodedBy = (*it)->toString().toCString(); + else if(!szID.compare("TBPM")) + m_iBeatsPerMinute = QString((*it)->toString().toCString()).toInt(); + else if(!szID.compare("TLEN") && m_iLength == 0) + m_iLength = QString((*it)->toString().toCString()).toInt(); + else if(!szID.compare("TLAN")) + m_szLanguage = QString((*it)->toString().toCString()).split("\r\n"); + else if(!szID.compare("TCON")) + m_szContentType = QString((*it)->toString().toCString()).split("\r\n"); + else if(!szID.compare("TFLT")) + m_szFileType = (*it)->toString().toCString(); + else if(!szID.compare("TMED")) + m_szMediaType = QString((*it)->toString().toCString()).split("\r\n"); + else if(!szID.compare("TMOO")) + m_szMood = (*it)->toString().toCString(); + else if(!szID.compare("TCOP")) + m_szCopyright = (*it)->toString().toCString(); + else if(!szID.compare("TPRO")) + m_szProducedNotice = (*it)->toString().toCString(); + else if(!szID.compare("TPUB")) + m_szPublisher = (*it)->toString().toCString(); + else if(!szID.compare("TOWN")) + m_szFileOwner = (*it)->toString().toCString(); + else if(!szID.compare("TRSN")) + m_szInternetRadioStationName = (*it)->toString().toCString(); + else if(!szID.compare("TRSO")) + m_szInternetRadioStationOwner = (*it)->toString().toCString(); + else if(!szID.compare("TOFN")) + m_szOriginalFilename = (*it)->toString().toCString(); + else if(!szID.compare("TDLY")) + m_iPlaylistDelay = QString((*it)->toString().toCString()).toInt(); + else if(!szID.compare("TDEN")) + m_EncodingTime = str2TS((*it)->toString().toCString()); + else if(!szID.compare("TDOR")) + m_OriginalReleaseTime = str2TS((*it)->toString().toCString()); + else if(!szID.compare("TDRC")) + m_RecordingTime = str2TS((*it)->toString().toCString()); + else if(!szID.compare("TDRL")) + m_ReleaseTime = str2TS((*it)->toString().toCString()); + else if(!szID.compare("TDTG")) + m_TaggingTime = str2TS((*it)->toString().toCString()); + else if(!szID.compare("TSSE")) + m_szswhwSettings = QString((*it)->toString().toCString()).split("\r\n"); + else if(!szID.compare("TSOA")) + m_szAlbumSortOrder = (*it)->toString().toCString(); + else if(!szID.compare("TSOP")) + m_szPerformerSortOrder = (*it)->toString().toCString(); + else if(!szID.compare("TSOT")) + m_szTitleSortOrder = (*it)->toString().toCString(); + else if(!szID.compare("SYLT")) + { + TagLib::ID3v2::SynchronizedLyricsFrame* lpLyrics = static_cast (*it); + + m_szSynchronizedLyricsLanguage = QString(lpLyrics->language().data()).left(3); + String::Type type = lpLyrics->textEncoding(); + m_szSynchronizedLyricsDescription = lpLyrics->description().toCString(); + + TagLib::ID3v2::SynchronizedLyricsFrame::SynchedTextList list = lpLyrics->synchedText(); + + for(ID3v2::SynchronizedLyricsFrame::SynchedTextList::ConstIterator it1 = list.begin(); it1 != list.end();it1++) + { + ID3v2::SynchronizedLyricsFrame::SynchedText t = *(it1); + m_szSynchronizedLyrics.add(t.time, QString(t.text.toCString())); + } + } + else if(!szID.compare("USLT")) + { + TagLib::ID3v2::UnsynchronizedLyricsFrame* lpLyrics = static_cast (*it); + + m_szUnsynchronizedLyricsLanguage = QString(lpLyrics->language().data()).left(3); + String::Type type = lpLyrics->textEncoding(); + m_szUnsynchronizedLyricsDescription = lpLyrics->description().toCString(); + QString szText = lpLyrics->text().toCString(); + if(szText.contains("\r\n")) + m_szUnsynchronizedLyrics = szText.split("\r\n"); + else if(szText.contains("\r")) + m_szUnsynchronizedLyrics = szText.split("\r"); + else if(szText.contains("\n")) + m_szUnsynchronizedLyrics = szText.split("\n"); + else + m_szUnsynchronizedLyrics.append(szText); + } + else if(!szID.compare("APIC")) + { + TagLib::ID3v2::AttachedPictureFrame* lpPicture = static_cast (*it); + TagLib::ID3v2::AttachedPictureFrame::Type t = lpPicture->type(); + QString szDescription; + szDescription = lpPicture->description().toCString(); + + QByteArray pictureData = QByteArray(lpPicture->picture().data(), lpPicture->picture().size()); + m_imageList.add(pictureData, "", (cImage::ImageType)t, szDescription); + } + } +} + +void cMediaInfo::readTagAPE(APE::Tag* lpTag) +{ + for(APE::ItemListMap::ConstIterator it = lpTag->itemListMap().begin();it != lpTag->itemListMap().end();++it) + { + QString szID = (*it).first.toCString(); + QString szVal = (*it).second.toString().toCString(); + if(!szID.isEmpty() && !szVal.isEmpty()) + m_TAGAPEList.add(szID.left(4), szVal); + } +} + +void cMediaInfo::readTagProperties(TagLib::PropertyMap& tags) +{ + QString szID; + + for(TagLib::PropertyMap::ConstIterator i = tags.begin(); i != tags.end(); ++i) + { + QStringList szValue; + szID = i->first.toCString(); + if(!szID.isEmpty()) + { + for(TagLib::StringList::ConstIterator j = i->second.begin(); j != i->second.end(); ++j) + { + QString sz = QString((*j).toCString()); + if(!sz.isEmpty()) + { + if(!szID.compare("LYRICS")) + { + sz.replace("\"", "'"); + sz.replace("\r\n", "\n"); + szValue.append(sz); + } + else + szValue.append(sz.split("\n")); + } + } + if(!szID.isEmpty() && !szValue.isEmpty()) + m_TAGPropertiesList.add(szID, szValue); + } + } +} + +QDateTime cMediaInfo::str2TS(const QString& sz) +{ + QDateTime dt; + + switch(sz.length()) + { + case 4: + dt = QDateTime::fromString(sz, QString("yyyy")); + break; + case 7: + dt = QDateTime::fromString(sz, QString("yyyy-MM")); + break; + case 10: + dt = QDateTime::fromString(sz, QString("yyyy-MM-dd")); + break; + case 13: + dt = QDateTime::fromString(sz, QString("yyyy-MM-ddTHH")); + break; + case 16: + dt = QDateTime::fromString(sz, QString("yyyy-MM-ddTHH:mm")); + break; + case 19: + dt = QDateTime::fromString(sz, QString("yyyy-MM-ddTHH:mm:ss")); + break; + default: + return(QDateTime()); + } + return(dt); +} + +bool cMediaInfo::isValid() +{ + return(m_bIsValid); +} + +QString cMediaInfo::fileName() +{ + return(m_szFileName); +} + +cMediaInfo::MEDIA_TYPE cMediaInfo::fileType1() +{ + return(m_FileType); +} + +QString cMediaInfo::fileType1Text() +{ + switch(m_FileType) + { + case MEDIA_TYPE_APE: + return("APE"); + case MEDIA_TYPE_ASF: + return("ASF"); + case MEDIA_TYPE_FLAC: + return("FLAC"); + case MEDIA_TYPE_MP4: + return("MP4"); + case MEDIA_TYPE_MPC: + return("MPC"); + case MEDIA_TYPE_MPEG: + return("MPEG"); + case MEDIA_TYPE_TRUEAUDIO: + return("TrueAudio"); + case MEDIA_TYPE_WAVPACK: + return("WavPack"); + case MEDIA_TYPE_WAV: + return("Wave"); + default: + return("unknown"); + } +} + +qint32 cMediaInfo::length1() +{ + return(m_iLength); +} + +QString cMediaInfo::length1Text() +{ + return("not implemented"); +} + +qint16 cMediaInfo::bitrate() +{ + return(m_iBitrate); +} + +QString cMediaInfo::bitrateText() +{ + return("not implemented"); +} + +qint32 cMediaInfo::sampleRate() +{ + return(m_iSampleRate); +} + +QString cMediaInfo::sampleRateText() +{ + return("not implemented"); +} + +qint16 cMediaInfo::channels() +{ + return(m_iChannels); +} + +QString cMediaInfo::channelsText() +{ + return("not implemented"); +} + +qint16 cMediaInfo::bitsPerSample() +{ + return(m_iBitsPerSample); +} + +QString cMediaInfo::bitsPerSampleText() +{ + return("not implemented"); +} + +qint16 cMediaInfo::layer() +{ + return(m_iLayer); +} + +QString cMediaInfo::layerText() +{ + return("not implemented"); +} + +qint16 cMediaInfo::version() +{ + return(m_iVersion); +} + +QString cMediaInfo::versionText() +{ + return("not implemented"); +} + +qint16 cMediaInfo::sampleWidth() +{ + return(m_iSampleWidth); +} + +QString cMediaInfo::sampleWidthText() +{ + return("not implemented"); +} + +qint64 cMediaInfo::sampleFrames() +{ + return(m_ullSampleFrames); +} + +QString cMediaInfo::sampleFramesText() +{ + return("not implemented"); +} + +bool cMediaInfo::isEncrypted() +{ + return(m_bIsEncrypted); +} + +qint16 cMediaInfo::trackGain() +{ + return(m_iTrackGain); +} + +qint16 cMediaInfo::albumGain() +{ + return(m_iAlbumGain); +} + +qint16 cMediaInfo::trackPeak() +{ + return(m_iTrackPeak); +} + +qint16 cMediaInfo::albumPeak() +{ + return(m_iAlbumPeak); +} + +bool cMediaInfo::protectionEnabled() +{ + return(m_bProtectionEnabled); +} + +cMediaInfo::CHANNEL_MODE cMediaInfo::channelMode() +{ + return(m_channelMode); +} + +QString cMediaInfo::channelModeText() +{ + switch(m_channelMode) + { + case CHANNEL_MODE_STEREO: + return("stereo"); + case CHANNEL_MODE_JOINTSTEREO: + return("joint stereo"); + case CHANNEL_MODE_MONO: + return("mono"); + case CHANNEL_MODE_DUALMONO: + return("dual mono"); + default: + return("unknown"); + } +} + +bool cMediaInfo::isCopyrighted() +{ + return(m_bIsCopyrighted); +} + +bool cMediaInfo::isOriginal() +{ + return(m_bIsOriginal); +} + +QString cMediaInfo::contentGroupDescription() +{ + return(m_szContentGroupDescription); +} + +QString cMediaInfo::title() +{ + return(m_szTitle); +} + +QString cMediaInfo::subTitle() +{ + return(m_szSubTitle); +} + +QString cMediaInfo::album() +{ + return(m_szAlbum); +} + +QString cMediaInfo::originalAlbum() +{ + return(m_szOriginalAlbum); +} + +QString cMediaInfo::trackNumber() +{ + return(m_szTrackNumber); +} + +QString cMediaInfo::partOfSet() +{ + return(m_szPartOfSet); +} + +QString cMediaInfo::subTitleOfSet() +{ + return(m_szSubTitleOfSet); +} + +QString cMediaInfo::internationalStandardRecordingCode() +{ + return(m_szInternationalStandardRecordingCode); +} + +QString cMediaInfo::leadArtist() +{ + return(m_szLeadArtist); +} + +QString cMediaInfo::band() +{ + return(m_szBand); +} + +QString cMediaInfo::conductor() +{ + return(m_szConductor); +} + +QStringList cMediaInfo::interpret() +{ + return(m_szInterpret); +} + +QString cMediaInfo::originalArtist() +{ + return(m_szOriginalArtist); +} + +QString cMediaInfo::textWriter() +{ + return(m_szTextWriter); +} + +QString cMediaInfo::originalTextWriter() +{ + return(m_szOriginalTextWriter); +} + +QString cMediaInfo::composer() +{ + return(m_szComposer); +} + +QString cMediaInfo::encodedBy() +{ + return(m_szEncodedBy); +} + +qint16 cMediaInfo::beatsPerMinute() +{ + return(m_iBeatsPerMinute); +} + +qint32 cMediaInfo::length() +{ + return(m_iLength); +} + +QStringList cMediaInfo::language() +{ + return(m_szLanguage); +} + +QStringList cMediaInfo::contentType() +{ + return(m_szContentType); +} + +QString cMediaInfo::fileType() +{ + return(m_szFileType); +} + +QStringList cMediaInfo::mediaType() +{ + return(m_szMediaType); +} + +QString cMediaInfo::mood() +{ + return(m_szMood); +} + +QString cMediaInfo::copyright() +{ + return(m_szCopyright); +} + +QString cMediaInfo::producedNotice() +{ + return(m_szProducedNotice); +} + +QString cMediaInfo::publisher() +{ + return(m_szPublisher); +} + +QString cMediaInfo::fileOwner() +{ + return(m_szFileOwner); +} + +QString cMediaInfo::internetRadioStationName() +{ + return(m_szInternetRadioStationName); +} + +QString cMediaInfo::internetRadioStationOwner() +{ + return(m_szInternetRadioStationOwner); +} + +QString cMediaInfo::originalFilename() +{ + return(m_szOriginalFilename); +} + +qint32 cMediaInfo::playlistDelay() +{ + return(m_iPlaylistDelay); +} + +QDateTime cMediaInfo::encodingTime() +{ + return(m_EncodingTime); +} + +QDateTime cMediaInfo::originalReleaseTime() +{ + return(m_OriginalReleaseTime); +} + +QDateTime cMediaInfo::recordingTime() +{ + return(m_RecordingTime); +} + +QDateTime cMediaInfo::releaseTime() +{ + return(m_ReleaseTime); +} + +QDateTime cMediaInfo::taggingTime() +{ + return(m_TaggingTime); +} + +QStringList cMediaInfo::swhwSettings() +{ + return(m_szswhwSettings); +} + +QString cMediaInfo::albumSortOrder() +{ + return(m_szAlbumSortOrder); +} + +QString cMediaInfo::performerSortOrder() +{ + return(m_szPerformerSortOrder); +} + +QString cMediaInfo::titleSortOrder() +{ + return(m_szTitleSortOrder); +} + +cStringTimeList cMediaInfo::synchronizedLyrics() +{ + return(m_szSynchronizedLyrics); +} + +QStringList cMediaInfo::unsynchronizedLyrics() +{ + return(m_szUnsynchronizedLyrics); +} + +cImageList cMediaInfo::images() +{ + return(m_imageList); +} + +/* + * http://de.wikipedia.org/wiki/ID3-Tag + * http://id3.org/id3v2.4.0-frames + * http://id3.org/id3v2.4.0-structure +*/ diff --git a/cmediainfo.h b/cmediainfo.h new file mode 100644 index 0000000..9a88dd3 --- /dev/null +++ b/cmediainfo.h @@ -0,0 +1,292 @@ +#ifndef CMEDIAINFO_H +#define CMEDIAINFO_H + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "ctag.h" +#include "cstring.h" +#include "cimage.h" + + +using namespace TagLib; + +class cMediaInfo : public QObject +{ + Q_OBJECT +public: + enum MEDIA_TYPE + { + MEDIA_TYPE_UNKNOWN = 0, + MEDIA_TYPE_APE, // APE + MEDIA_TYPE_ASF, // WMA + MEDIA_TYPE_FLAC, // FLAC + MEDIA_TYPE_MP4, // AAC, MP4, M4A + MEDIA_TYPE_MPC, // MPC + MEDIA_TYPE_MPEG, // MP1, MP2, MP3 + MEDIA_TYPE_TRUEAUDIO, // TTA + MEDIA_TYPE_WAVPACK, // WV + MEDIA_TYPE_WAV // WAV + }; + enum CHANNEL_MODE + { + CHANNEL_MODE_UNKNOWN = 0, + CHANNEL_MODE_STEREO, + CHANNEL_MODE_JOINTSTEREO, + CHANNEL_MODE_MONO, + CHANNEL_MODE_DUALMONO + }; + + explicit cMediaInfo(bool bAPE = true, bool bID3V1 = true, bool bID3V2 = true, bool bProperties = true, QObject *parent = 0); + ~cMediaInfo(); + + bool readFromFile(const QString& szFileName); + bool readFromDB(const QSqlDatabase& db); + bool writeToDB(const QSqlDatabase& db); + + bool isValid(); + + // + QString fileName(); + MEDIA_TYPE fileType1(); + QString fileType1Text(); + + // + qint32 length1(); + QString length1Text(); + qint16 bitrate(); + QString bitrateText(); + qint32 sampleRate(); + QString sampleRateText(); + qint16 channels(); + QString channelsText(); + qint16 bitsPerSample(); + QString bitsPerSampleText(); + qint16 layer(); + QString layerText(); + qint16 version(); + QString versionText(); + qint16 sampleWidth(); + QString sampleWidthText(); + qint64 sampleFrames(); + QString sampleFramesText(); + bool isEncrypted(); + qint16 trackGain(); + qint16 albumGain(); + qint16 trackPeak(); + qint16 albumPeak(); + bool protectionEnabled(); + CHANNEL_MODE channelMode(); + QString channelModeText(); + bool isCopyrighted(); + bool isOriginal(); + + // Identification Frames + QString contentGroupDescription(); // TIT1 + QString title(); // TIT2 + QString subTitle(); // TIT3 + QString album(); // TALB + QString originalAlbum(); // TOAL + QString trackNumber(); // TRCK + QString partOfSet(); // TPOS + QString subTitleOfSet(); // TSST + QString internationalStandardRecordingCode(); // TSRC + + // Involved Persons Frames + QString leadArtist(); // TPE1 + QString band(); // TPE2 + QString conductor(); // TPE3 + QStringList interpret(); // TPE4 + QString originalArtist(); // TOPE + QString textWriter(); // TEXT + QString originalTextWriter(); // TOLY + QString composer(); // TCOM + //cString21List musicianCredits(); // TMCL + //cString21List involvedPeople(); // TIPL + QString encodedBy(); // TENC + + // Derived and subjective properties frames + qint16 beatsPerMinute(); // TBPM + qint32 length(); // TLEN + // TKEY + QStringList language(); // TLAN + QStringList contentType(); // TCON + QString fileType(); // TFLT + QStringList mediaType(); // TMED + QString mood(); // TMOO + + // Rights and license frames + QString copyright(); // TCOP + QString producedNotice(); // TPRO + QString publisher(); // TPUB + QString fileOwner(); // TOWN + QString internetRadioStationName(); // TRSN + QString internetRadioStationOwner(); // TRSO + + // Other text frames + QString originalFilename(); // TOFN + qint32 playlistDelay(); // TDLY + QDateTime encodingTime(); // TDEN + QDateTime originalReleaseTime(); // TDOR + QDateTime recordingTime(); // TDRC + QDateTime releaseTime(); // TDRL + QDateTime taggingTime(); // TDTG + QStringList swhwSettings(); // TSSE + QString albumSortOrder(); // TSOA + QString performerSortOrder(); // TSOP + QString titleSortOrder(); // TSOT + + cStringTimeList synchronizedLyrics(); // SYLT + QStringList unsynchronizedLyrics(); // USLT + + cImageList images(); // APIC +signals: + +public slots: + +protected: + bool m_bAPE; + bool m_bID3V1; + bool m_bID3V2; + bool m_bProperties; + bool m_bIsValid; + QString m_szFileName; + MEDIA_TYPE m_FileType; + + qint32 m_iLength; + qint16 m_iBitrate; + qint32 m_iSampleRate; + qint16 m_iChannels; + qint16 m_iBitsPerSample; + qint16 m_iLayer; + qint16 m_iVersion; + qint16 m_iSampleWidth; + qint64 m_ullSampleFrames; + bool m_bIsEncrypted; + qint16 m_iTrackGain; + qint16 m_iAlbumGain; + qint16 m_iTrackPeak; + qint16 m_iAlbumPeak; + bool m_bProtectionEnabled; + CHANNEL_MODE m_channelMode; + bool m_bIsCopyrighted; + bool m_bIsOriginal; + + // ID3V1 + QStringList m_szAlbumArtistList; + QStringList m_szArtistList; + QString m_szComment; + QStringList m_szComposerList; + QStringList m_szGenreList; + QString m_szRating; + qint16 m_iYear; + + // Identification Frames + QString m_szContentGroupDescription; // TIT1 + QString m_szTitle; // TIT2 + QString m_szSubTitle; // TIT3 + QString m_szAlbum; // TALB + QString m_szOriginalAlbum; // TOAL + QString m_szTrackNumber; // TRCK + QString m_szPartOfSet; // TPOS + QString m_szSubTitleOfSet; // TSST + QString m_szInternationalStandardRecordingCode; // TSRC + + // Involved Persons Frames + QString m_szLeadArtist; // TPE1 + QString m_szBand; // TPE2 + QString m_szConductor; // TPE3 + QStringList m_szInterpret; // TPE4 + QString m_szOriginalArtist; // TOPE + QString m_szTextWriter; // TEXT + QString m_szOriginalTextWriter; // TOLY + QString m_szComposer; // TCOM + QString m_szEncodedBy; // TENC + + // Derived and subjective properties frames + qint16 m_iBeatsPerMinute; // TBPM +// qint16 m_iLength; // TLEN + // TKEY + QStringList m_szLanguage; // TLAN + QStringList m_szContentType; // TCON + QString m_szFileType; // TFLT + QStringList m_szMediaType; // TMED + QString m_szMood; // TMOO + + // Rights and license frames + QString m_szCopyright; // TCOP + QString m_szProducedNotice; // TPRO + QString m_szPublisher; // TPUB + QString m_szFileOwner; // TOWN + QString m_szInternetRadioStationName; // TRSN + QString m_szInternetRadioStationOwner; // TRSO + + // Other text frames + QString m_szOriginalFilename; // TOFN + qint32 m_iPlaylistDelay; // TDLY + QDateTime m_EncodingTime; // TDEN + QDateTime m_OriginalReleaseTime; // TDOR + QDateTime m_RecordingTime; // TDRC + QDateTime m_ReleaseTime; // TDRL + QDateTime m_TaggingTime; // TDTG + QStringList m_szswhwSettings; // TSSE + QString m_szAlbumSortOrder; // TSOA + QString m_szPerformerSortOrder; // TSOP + QString m_szTitleSortOrder; // TSOT + + cStringTimeList m_szSynchronizedLyrics; // SYLT + QString m_szSynchronizedLyricsLanguage; + QString m_szSynchronizedLyricsDescription; + QStringList m_szUnsynchronizedLyrics; // USLT + QString m_szUnsynchronizedLyricsLanguage; + QString m_szUnsynchronizedLyricsDescription; + + //ID3V2 + qint32 m_iID3v2Version; + qint32 m_iID3v2Revision; + qint32 m_iID3v2Size; + //cTAGList m_TAGv2List; + cTAGList m_TAGAPEList; + cTAGList m_TAGPropertiesList; + + cImageList m_imageList; + + void clear(); + + void readTagV1(ID3v1::Tag* lpTag); + void readTagV2(ID3v2::Tag* lpTag); + void readTagAPE(APE::Tag* lpTag); + void readTagProperties(TagLib::PropertyMap& tags); + + QDateTime str2TS(const QString& sz); +}; + +#endif // CMEDIAINFO_H diff --git a/cstring.cpp b/cstring.cpp new file mode 100644 index 0000000..dea0148 --- /dev/null +++ b/cstring.cpp @@ -0,0 +1,46 @@ +#include "cstring.h" + + +cStringTime::cStringTime() +{ +} + +cStringTime::cStringTime(const qint32& iTime, const QString& szString) : + m_iTime(iTime), + m_szString(szString) +{ +} + +cStringTimeList::cStringTimeList() +{ +} + +cStringTime* cStringTimeList::add(const qint32& iTime, const QString& szString) +{ + cStringTime* lpNew = new cStringTime(iTime, szString); + append(lpNew); + return(lpNew); +} + +cString21::cString21() : + m_szLeft(QStringList()), + m_szRight("") +{ +} + +cString21::cString21(const QStringList& szLeft, const QString& szRight) : + m_szLeft(szLeft), + m_szRight(szRight) +{ +} + +cString21List::cString21List() +{ +} + +cString21* cString21List::add(const QStringList& szLeft, const QString& szRight) +{ + cString21* lpNew = new cString21(szLeft, szRight); + append(lpNew); + return(lpNew); +} diff --git a/cstring.h b/cstring.h new file mode 100644 index 0000000..0853035 --- /dev/null +++ b/cstring.h @@ -0,0 +1,56 @@ +#ifndef CSTRING_H +#define CSTRING_H + +#include +#include +#include +#include + + +class cStringTime +{ +public: + explicit cStringTime(); + explicit cStringTime(const qint32& iTime, const QString& szString); +protected: + qint32 m_iTime; + QString m_szString; +signals: + +public slots: +}; + +Q_DECLARE_METATYPE(cStringTime) + +class cStringTimeList : public QList +{ +public: + cStringTimeList(); + cStringTime* add(const qint32& iTime, const QString& szString); +}; + + +class cString21 +{ +public: + explicit cString21(); + explicit cString21(const QStringList& szLeft, const QString& szRight); +protected: + QStringList m_szLeft; + QString m_szRight; +signals: + +public slots: + +}; + +Q_DECLARE_METATYPE(cString21) + +class cString21List : public QList +{ +public: + cString21List(); + cString21* add(const QStringList& szLeft, const QString& szRight); +}; + +#endif // CSTRING_H diff --git a/ctag.cpp b/ctag.cpp new file mode 100644 index 0000000..e075c0d --- /dev/null +++ b/ctag.cpp @@ -0,0 +1,48 @@ +#include "ctag.h" + + +cTAG::cTAG() +{ + m_szTAG = ""; + m_szValue.clear(); +} + +cTAG::cTAG(const QString& szTAG, const QString& szValue) +{ + m_szTAG = szTAG; + m_szValue = szValue.split("\n"); +} + +cTAG::cTAG(const QString& szTAG, const QStringList& szValue) +{ + m_szTAG = szTAG; + m_szValue = szValue; +} + +QString cTAG::tag() +{ + return(m_szTAG); +} + +QStringList cTAG::valueList() +{ + return(m_szValue); +} + +cTAGList::cTAGList() +{ +} + +cTAG* cTAGList::add(const QString& szTAG, const QString& szValue) +{ + cTAG* lpTag = new cTAG(szTAG, szValue); + append(lpTag); + return(lpTag); +} + +cTAG* cTAGList::add(const QString& szTAG, const QStringList& szValue) +{ + cTAG* lpTag = new cTAG(szTAG, szValue); + append(lpTag); + return(lpTag); +} diff --git a/ctag.h b/ctag.h new file mode 100644 index 0000000..e7d0055 --- /dev/null +++ b/ctag.h @@ -0,0 +1,38 @@ +#ifndef CTAG_H +#define CTAG_H + +#include +#include +#include +#include + + +class cTAG +{ +public: + explicit cTAG(); + explicit cTAG(const QString& szTAG, const QString& szValue); + explicit cTAG(const QString& szTAG, const QStringList& szValue); + + QString tag(); + QStringList valueList(); +protected: + QString m_szTAG; + QStringList m_szValue; +signals: + +public slots: + +}; + +Q_DECLARE_METATYPE(cTAG) + +class cTAGList : public QList +{ +public: + cTAGList(); + cTAG* add(const QString& szTAG, const QString& szValue); + cTAG* add(const QString& szTAG, const QStringList& szValue); +}; + +#endif // CTAG_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..899172d --- /dev/null +++ b/main.cpp @@ -0,0 +1,17 @@ +#include "cmainwindow.h" +#include + + +int main(int argc, char *argv[]) +{ + QCoreApplication::setOrganizationName("WIN-DESIGN"); + QCoreApplication::setOrganizationDomain("windesign.at"); + QCoreApplication::setApplicationName("qtJukeBox"); + + QApplication a(argc, argv); + cMainWindow w; + + w.show(); + + return a.exec(); +} diff --git a/qtJukeBox.pro b/qtJukeBox.pro new file mode 100644 index 0000000..3cba2aa --- /dev/null +++ b/qtJukeBox.pro @@ -0,0 +1,32 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2013-04-11T16:58:13 +# +#------------------------------------------------- + +QT += core gui sql + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = qtJukeBox +TEMPLATE = app + + +SOURCES += main.cpp\ + cmainwindow.cpp \ + cmediainfo.cpp \ + ctag.cpp \ + cdatabase.cpp \ + cstring.cpp \ + cimage.cpp + +HEADERS += cmainwindow.h \ + cmediainfo.h \ + ctag.h \ + cdatabase.h \ + cstring.h \ + cimage.h + +FORMS += cmainwindow.ui + +LIBS += -ltaglib diff --git a/taglib/AUTHORS b/taglib/AUTHORS new file mode 100644 index 0000000..e996d94 --- /dev/null +++ b/taglib/AUTHORS @@ -0,0 +1,17 @@ +Scott Wheeler + Author, maintainer +Lukas Lalinsky + Implementation of multiple new file formats, many bug fixes, maintainer +Ismael Orenstein + Xing header implementation +Allan Sandfeld Jensen + FLAC metadata implementation +Teemu Tervo + Numerous bug reports and fixes +Mathias Panzenböck + Mod, S3M, IT and XM metadata implementations +Tsuda Kageyu + A lot of fixes and improvements, i.e. memory copy reduction etc. + +Please send all patches and questions to taglib-devel@kde.org rather than to +individual developers! diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt new file mode 100644 index 0000000..317ffa1 --- /dev/null +++ b/taglib/CMakeLists.txt @@ -0,0 +1,120 @@ +project(taglib) + +cmake_minimum_required(VERSION 2.6.0 FATAL_ERROR) + +option(ENABLE_STATIC "Make static version of libtag" OFF) +if(ENABLE_STATIC) + add_definitions(-DTAGLIB_STATIC) + set(BUILD_SHARED_LIBS OFF) +else() + set(BUILD_SHARED_LIBS ON) +endif() +OPTION(ENABLE_STATIC_RUNTIME "Visual Studio, link with runtime statically" OFF) + +option(VISIBILITY_HIDDEN "Build with -fvisibility=hidden" OFF) +if(VISIBILITY_HIDDEN) + add_definitions (-fvisibility=hidden) +endif() + +option(BUILD_TESTS "Build the test suite" OFF) +option(BUILD_EXAMPLES "Build the examples" OFF) + +option(NO_ITUNES_HACKS "Disable workarounds for iTunes bugs" OFF) + +add_definitions(-DHAVE_CONFIG_H) +set(TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests/") + +## the following are directories where stuff will be installed to +set(LIB_SUFFIX "" CACHE STRING "Define suffix of directory name (32/64)") +set(EXEC_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" CACHE PATH "Base directory for executables and libraries" FORCE) +set(BIN_INSTALL_DIR "${EXEC_INSTALL_PREFIX}/bin" CACHE PATH "The subdirectory to the binaries prefix (default prefix/bin)" FORCE) +set(LIB_INSTALL_DIR "${EXEC_INSTALL_PREFIX}/lib${LIB_SUFFIX}" CACHE PATH "The subdirectory relative to the install prefix where libraries will be installed (default is /lib${LIB_SUFFIX})" FORCE) +set(INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/include" CACHE PATH "The subdirectory to the header prefix" FORCE) + +if(APPLE) + option(BUILD_FRAMEWORK "Build an OS X framework" OFF) + set(FRAMEWORK_INSTALL_DIR "/Library/Frameworks" CACHE STRING "Directory to install frameworks to.") +endif() + +if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") +endif() +if (MSVC AND ENABLE_STATIC_RUNTIME) + foreach(flag_var CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO) + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + endforeach(flag_var) +endif() + +set(TAGLIB_LIB_MAJOR_VERSION "1") +set(TAGLIB_LIB_MINOR_VERSION "9") +set(TAGLIB_LIB_PATCH_VERSION "1") + +set(TAGLIB_LIB_VERSION_STRING "${TAGLIB_LIB_MAJOR_VERSION}.${TAGLIB_LIB_MINOR_VERSION}.${TAGLIB_LIB_PATCH_VERSION}") + +# 1. If the library source code has changed at all since the last update, then increment revision. +# 2. If any interfaces have been added, removed, or changed since the last update, increment current, and set revision to 0. +# 3. If any interfaces have been added since the last public release, then increment age. +# 4. If any interfaces have been removed since the last public release, then set age to 0. +set(TAGLIB_SOVERSION_CURRENT 15) +set(TAGLIB_SOVERSION_REVISION 0) +set(TAGLIB_SOVERSION_AGE 14) + +math(EXPR TAGLIB_SOVERSION_MAJOR "${TAGLIB_SOVERSION_CURRENT} - ${TAGLIB_SOVERSION_AGE}") +math(EXPR TAGLIB_SOVERSION_MINOR "${TAGLIB_SOVERSION_AGE}") +math(EXPR TAGLIB_SOVERSION_PATCH "${TAGLIB_SOVERSION_REVISION}") + +include(ConfigureChecks.cmake) + +if(NOT WIN32) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/taglib-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/taglib-config ) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/taglib-config DESTINATION ${BIN_INSTALL_DIR}) +endif() + +if(WIN32) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/taglib-config.cmd.cmake ${CMAKE_CURRENT_BINARY_DIR}/taglib-config.cmd ) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/taglib-config.cmd DESTINATION ${BIN_INSTALL_DIR}) +endif() + +if(NOT WIN32 AND NOT BUILD_FRAMEWORK) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/taglib.pc.cmake ${CMAKE_CURRENT_BINARY_DIR}/taglib.pc ) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/taglib.pc DESTINATION ${LIB_INSTALL_DIR}/pkgconfig) +endif() + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +configure_file(config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h) + +if(WITH_ASF) + set(TAGLIB_WITH_ASF TRUE) +endif() +if(WITH_MP4) + set(TAGLIB_WITH_MP4 TRUE) +endif() + +option(TRACE_IN_RELEASE "Output debug messages even in release mode" OFF) +if(TRACE_IN_RELEASE) + set(TRACE_IN_RELEASE TRUE) +endif() + +configure_file(taglib/taglib_config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/taglib_config.h) + +add_subdirectory(taglib) +add_subdirectory(bindings) +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif(BUILD_TESTS) +add_subdirectory(examples) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.cmake ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) +file(COPY doc/taglib.png DESTINATION doc) +add_custom_target(docs doxygen) + +# uninstall target +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" + IMMEDIATE @ONLY) + +add_custom_target(uninstall + COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake") diff --git a/taglib/COPYING.LGPL b/taglib/COPYING.LGPL new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/taglib/COPYING.LGPL @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/taglib/COPYING.MPL b/taglib/COPYING.MPL new file mode 100644 index 0000000..7714141 --- /dev/null +++ b/taglib/COPYING.MPL @@ -0,0 +1,470 @@ + MOZILLA PUBLIC LICENSE + Version 1.1 + + --------------- + +1. Definitions. + + 1.0.1. "Commercial Use" means distribution or otherwise making the + Covered Code available to a third party. + + 1.1. "Contributor" means each entity that creates or contributes to + the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Code, prior Modifications used by a Contributor, and the Modifications + made by that particular Contributor. + + 1.3. "Covered Code" means the Original Code or Modifications or the + combination of the Original Code and Modifications, in each case + including portions thereof. + + 1.4. "Electronic Distribution Mechanism" means a mechanism generally + accepted in the software development community for the electronic + transfer of data. + + 1.5. "Executable" means Covered Code in any form other than Source + Code. + + 1.6. "Initial Developer" means the individual or entity identified + as the Initial Developer in the Source Code notice required by Exhibit + A. + + 1.7. "Larger Work" means a work which combines Covered Code or + portions thereof with code not governed by the terms of this License. + + 1.8. "License" means this document. + + 1.8.1. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed herein. + + 1.9. "Modifications" means any addition to or deletion from the + substance or structure of either the Original Code or any previous + Modifications. When Covered Code is released as a series of files, a + Modification is: + A. Any addition to or deletion from the contents of a file + containing Original Code or previous Modifications. + + B. Any new file that contains any part of the Original Code or + previous Modifications. + + 1.10. "Original Code" means Source Code of computer software code + which is described in the Source Code notice required by Exhibit A as + Original Code, and which, at the time of its release under this + License is not already Covered Code governed by this License. + + 1.10.1. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, process, + and apparatus claims, in any patent Licensable by grantor. + + 1.11. "Source Code" means the preferred form of the Covered Code for + making modifications to it, including all modules it contains, plus + any associated interface definition files, scripts used to control + compilation and installation of an Executable, or source code + differential comparisons against either the Original Code or another + well known, available Covered Code of the Contributor's choice. The + Source Code can be in a compressed or archival form, provided the + appropriate decompression or de-archiving software is widely available + for no charge. + + 1.12. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms of, this + License or a future version of this License issued under Section 6.1. + For legal entities, "You" includes any entity which controls, is + controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, + to cause the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty percent + (50%) of the outstanding shares or beneficial ownership of such + entity. + +2. Source Code License. + + 2.1. The Initial Developer Grant. + The Initial Developer hereby grants You a world-wide, royalty-free, + non-exclusive license, subject to third party intellectual property + claims: + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer to use, reproduce, + modify, display, perform, sublicense and distribute the Original + Code (or portions thereof) with or without Modifications, and/or + as part of a Larger Work; and + + (b) under Patents Claims infringed by the making, using or + selling of Original Code, to make, have made, use, practice, + sell, and offer for sale, and/or otherwise dispose of the + Original Code (or portions thereof). + + (c) the licenses granted in this Section 2.1(a) and (b) are + effective on the date Initial Developer first distributes + Original Code under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: 1) for code that You delete from the Original Code; 2) + separate from the Original Code; or 3) for infringements caused + by: i) the modification of the Original Code or ii) the + combination of the Original Code with other software or devices. + + 2.2. Contributor Grant. + Subject to third party intellectual property claims, each Contributor + hereby grants You a world-wide, royalty-free, non-exclusive license + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor, to use, reproduce, modify, + display, perform, sublicense and distribute the Modifications + created by such Contributor (or portions thereof) either on an + unmodified basis, with other Modifications, as Covered Code + and/or as part of a Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either alone + and/or in combination with its Contributor Version (or portions + of such combination), to make, use, sell, offer for sale, have + made, and/or otherwise dispose of: 1) Modifications made by that + Contributor (or portions thereof); and 2) the combination of + Modifications made by that Contributor with its Contributor + Version (or portions of such combination). + + (c) the licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first makes Commercial Use of + the Covered Code. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: 1) for any code that Contributor has deleted from the + Contributor Version; 2) separate from the Contributor Version; + 3) for infringements caused by: i) third party modifications of + Contributor Version or ii) the combination of Modifications made + by that Contributor with other software (except as part of the + Contributor Version) or other devices; or 4) under Patent Claims + infringed by Covered Code in the absence of Modifications made by + that Contributor. + +3. Distribution Obligations. + + 3.1. Application of License. + The Modifications which You create or to which You contribute are + governed by the terms of this License, including without limitation + Section 2.2. The Source Code version of Covered Code may be + distributed only under the terms of this License or a future version + of this License released under Section 6.1, and You must include a + copy of this License with every copy of the Source Code You + distribute. You may not offer or impose any terms on any Source Code + version that alters or restricts the applicable version of this + License or the recipients' rights hereunder. However, You may include + an additional document offering the additional rights described in + Section 3.5. + + 3.2. Availability of Source Code. + Any Modification which You create or to which You contribute must be + made available in Source Code form under the terms of this License + either on the same media as an Executable version or via an accepted + Electronic Distribution Mechanism to anyone to whom you made an + Executable version available; and if made available via Electronic + Distribution Mechanism, must remain available for at least twelve (12) + months after the date it initially became available, or at least six + (6) months after a subsequent version of that particular Modification + has been made available to such recipients. You are responsible for + ensuring that the Source Code version remains available even if the + Electronic Distribution Mechanism is maintained by a third party. + + 3.3. Description of Modifications. + You must cause all Covered Code to which You contribute to contain a + file documenting the changes You made to create that Covered Code and + the date of any change. You must include a prominent statement that + the Modification is derived, directly or indirectly, from Original + Code provided by the Initial Developer and including the name of the + Initial Developer in (a) the Source Code, and (b) in any notice in an + Executable version or related documentation in which You describe the + origin or ownership of the Covered Code. + + 3.4. Intellectual Property Matters + (a) Third Party Claims. + If Contributor has knowledge that a license under a third party's + intellectual property rights is required to exercise the rights + granted by such Contributor under Sections 2.1 or 2.2, + Contributor must include a text file with the Source Code + distribution titled "LEGAL" which describes the claim and the + party making the claim in sufficient detail that a recipient will + know whom to contact. If Contributor obtains such knowledge after + the Modification is made available as described in Section 3.2, + Contributor shall promptly modify the LEGAL file in all copies + Contributor makes available thereafter and shall take other steps + (such as notifying appropriate mailing lists or newsgroups) + reasonably calculated to inform those who received the Covered + Code that new knowledge has been obtained. + + (b) Contributor APIs. + If Contributor's Modifications include an application programming + interface and Contributor has knowledge of patent licenses which + are reasonably necessary to implement that API, Contributor must + also include this information in the LEGAL file. + + (c) Representations. + Contributor represents that, except as disclosed pursuant to + Section 3.4(a) above, Contributor believes that Contributor's + Modifications are Contributor's original creation(s) and/or + Contributor has sufficient rights to grant the rights conveyed by + this License. + + 3.5. Required Notices. + You must duplicate the notice in Exhibit A in each file of the Source + Code. If it is not possible to put such notice in a particular Source + Code file due to its structure, then You must include such notice in a + location (such as a relevant directory) where a user would be likely + to look for such a notice. If You created one or more Modification(s) + You may add your name as a Contributor to the notice described in + Exhibit A. You must also duplicate this License in any documentation + for the Source Code where You describe recipients' rights or ownership + rights relating to Covered Code. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Code. However, You + may do so only on Your own behalf, and not on behalf of the Initial + Developer or any Contributor. You must make it absolutely clear than + any such warranty, support, indemnity or liability obligation is + offered by You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, + support, indemnity or liability terms You offer. + + 3.6. Distribution of Executable Versions. + You may distribute Covered Code in Executable form only if the + requirements of Section 3.1-3.5 have been met for that Covered Code, + and if You include a notice stating that the Source Code version of + the Covered Code is available under the terms of this License, + including a description of how and where You have fulfilled the + obligations of Section 3.2. The notice must be conspicuously included + in any notice in an Executable version, related documentation or + collateral in which You describe recipients' rights relating to the + Covered Code. You may distribute the Executable version of Covered + Code or ownership rights under a license of Your choice, which may + contain terms different from this License, provided that You are in + compliance with the terms of this License and that the license for the + Executable version does not attempt to limit or alter the recipient's + rights in the Source Code version from the rights set forth in this + License. If You distribute the Executable version under a different + license You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial + Developer or any Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by + the Initial Developer or such Contributor as a result of any such + terms You offer. + + 3.7. Larger Works. + You may create a Larger Work by combining Covered Code with other code + not governed by the terms of this License and distribute the Larger + Work as a single product. In such a case, You must make sure the + requirements of this License are fulfilled for the Covered Code. + +4. Inability to Comply Due to Statute or Regulation. + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Code due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description + must be included in the LEGAL file described in Section 3.4 and must + be included with all distributions of the Source Code. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Application of this License. + + This License applies to code to which the Initial Developer has + attached the notice in Exhibit A and to related Covered Code. + +6. Versions of the License. + + 6.1. New Versions. + Netscape Communications Corporation ("Netscape") may publish revised + and/or new versions of the License from time to time. Each version + will be given a distinguishing version number. + + 6.2. Effect of New Versions. + Once Covered Code has been published under a particular version of the + License, You may always continue to use it under the terms of that + version. You may also choose to use such Covered Code under the terms + of any subsequent version of the License published by Netscape. No one + other than Netscape has the right to modify the terms applicable to + Covered Code created under this License. + + 6.3. Derivative Works. + If You create or use a modified version of this License (which you may + only do in order to apply it to code which is not already Covered Code + governed by this License), You must (a) rename Your license so that + the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", + "MPL", "NPL" or any confusingly similar phrase do not appear in your + license (except to note that your license differs from this License) + and (b) otherwise make it clear that Your version of the license + contains terms which differ from the Mozilla Public License and + Netscape Public License. (Filling in the name of the Initial + Developer, Original Code or Contributor in the notice described in + Exhibit A shall not of themselves be deemed to be modifications of + this License.) + +7. DISCLAIMER OF WARRANTY. + + COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, + WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF + DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. + THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE + IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE + COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +8. TERMINATION. + + 8.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. All + sublicenses to the Covered Code which are properly granted shall + survive any termination of this License. Provisions which, by their + nature, must remain in effect beyond the termination of this License + shall survive. + + 8.2. If You initiate litigation by asserting a patent infringement + claim (excluding declatory judgment actions) against Initial Developer + or a Contributor (the Initial Developer or Contributor against whom + You file such action is referred to as "Participant") alleging that: + + (a) such Participant's Contributor Version directly or indirectly + infringes any patent, then any and all rights granted by such + Participant to You under Sections 2.1 and/or 2.2 of this License + shall, upon 60 days notice from Participant terminate prospectively, + unless if within 60 days after receipt of notice You either: (i) + agree in writing to pay Participant a mutually agreeable reasonable + royalty for Your past and future use of Modifications made by such + Participant, or (ii) withdraw Your litigation claim with respect to + the Contributor Version against such Participant. If within 60 days + of notice, a reasonable royalty and payment arrangement are not + mutually agreed upon in writing by the parties or the litigation claim + is not withdrawn, the rights granted by Participant to You under + Sections 2.1 and/or 2.2 automatically terminate at the expiration of + the 60 day notice period specified above. + + (b) any software, hardware, or device, other than such Participant's + Contributor Version, directly or indirectly infringes any patent, then + any rights granted to You by such Participant under Sections 2.1(b) + and 2.2(b) are revoked effective as of the date You first made, used, + sold, distributed, or had made, Modifications made by that + Participant. + + 8.3. If You assert a patent infringement claim against Participant + alleging that such Participant's Contributor Version directly or + indirectly infringes any patent where such claim is resolved (such as + by license or settlement) prior to the initiation of patent + infringement litigation, then the reasonable value of the licenses + granted by such Participant under Sections 2.1 or 2.2 shall be taken + into account in determining the amount or value of any payment or + license. + + 8.4. In the event of termination under Sections 8.1 or 8.2 above, + all end user license agreements (excluding distributors and resellers) + which have been validly granted by You or any distributor hereunder + prior to termination shall survive termination. + +9. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL + DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, + OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR + ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY + CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, + WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO + THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +10. U.S. GOVERNMENT END USERS. + + The Covered Code is a "commercial item," as that term is defined in + 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer + software" and "commercial computer software documentation," as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 + C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), + all U.S. Government End Users acquire Covered Code with only those + rights set forth herein. + +11. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed by + California law provisions (except to the extent applicable law, if + any, provides otherwise), excluding its conflict-of-law provisions. + With respect to disputes in which at least one party is a citizen of, + or an entity chartered or registered to do business in the United + States of America, any litigation relating to this License shall be + subject to the jurisdiction of the Federal Courts of the Northern + District of California, with venue lying in Santa Clara County, + California, with the losing party responsible for costs, including + without limitation, court costs and reasonable attorneys' fees and + expenses. The application of the United Nations Convention on + Contracts for the International Sale of Goods is expressly excluded. + Any law or regulation which provides that the language of a contract + shall be construed against the drafter shall not apply to this + License. + +12. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or indirectly, + out of its utilization of rights under this License and You agree to + work with Initial Developer and Contributors to distribute such + responsibility on an equitable basis. Nothing herein is intended or + shall be deemed to constitute any admission of liability. + +13. MULTIPLE-LICENSED CODE. + + Initial Developer may designate portions of the Covered Code as + "Multiple-Licensed". "Multiple-Licensed" means that the Initial + Developer permits you to utilize portions of the Covered Code under + Your choice of the NPL or the alternative licenses, if any, specified + by the Initial Developer in the file described in Exhibit A. + +EXHIBIT A -Mozilla Public License. + + ``The contents of this file are subject to the Mozilla Public License + Version 1.1 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + http://www.mozilla.org/MPL/ + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + License for the specific language governing rights and limitations + under the License. + + The Original Code is ______________________________________. + + The Initial Developer of the Original Code is ________________________. + Portions created by ______________________ are Copyright (C) ______ + _______________________. All Rights Reserved. + + Contributor(s): ______________________________________. + + Alternatively, the contents of this file may be used under the terms + of the _____ license (the "[___] License"), in which case the + provisions of [______] License are applicable instead of those + above. If you wish to allow use of your version of this file only + under the terms of the [____] License and not to allow others to use + your version of this file under the MPL, indicate your decision by + deleting the provisions above and replace them with the notice and + other provisions required by the [___] License. If you do not delete + the provisions above, a recipient may use your version of this file + under either the MPL or the [___] License." + + [NOTE: The text of this Exhibit A may differ slightly from the text of + the notices in the Source Code files of the Original Code. You should + use the text of this Exhibit A rather than the text found in the + Original Code Source Code for Your Modifications.] + diff --git a/taglib/ConfigureChecks.cmake b/taglib/ConfigureChecks.cmake new file mode 100644 index 0000000..b0f164a --- /dev/null +++ b/taglib/ConfigureChecks.cmake @@ -0,0 +1,226 @@ +include(CheckIncludeFile) +include(CheckIncludeFiles) +include(CheckSymbolExists) +include(CheckFunctionExists) +include(CheckLibraryExists) +include(CheckTypeSize) +include(CheckCXXSourceCompiles) +include(TestBigEndian) + +# Check if the size of integral types are suitable. + +check_type_size("short" SIZEOF_SHORT) +if(NOT ${SIZEOF_SHORT} EQUAL 2) + MESSAGE(FATAL_ERROR "TagLib requires that short is 16-bit wide.") +endif() + +check_type_size("int" SIZEOF_INT) +if(NOT ${SIZEOF_INT} EQUAL 4) + MESSAGE(FATAL_ERROR "TagLib requires that int is 32-bit wide.") +endif() + +check_type_size("long long" SIZEOF_LONGLONG) +if(NOT ${SIZEOF_LONGLONG} EQUAL 8) + MESSAGE(FATAL_ERROR "TagLib requires that long long is 64-bit wide.") +endif() + +check_type_size("wchar_t" SIZEOF_WCHAR_T) +if(${SIZEOF_WCHAR_T} LESS 2) + MESSAGE(FATAL_ERROR "TagLib requires that wchar_t is sufficient to store a UTF-16 char.") +endif() + +# Determine the CPU byte order. + +test_big_endian(IS_BIG_ENDIAN) + +if(NOT IS_BIG_ENDIAN) + set(SYSTEM_BYTEORDER 1) +else() + set(SYSTEM_BYTEORDER 2) +endif() + +# Determine which kind of atomic operations your compiler supports. + +check_cxx_source_compiles(" + #include + int main() { + std::atomic x; + x.fetch_add(1); + x.fetch_sub(1); + return 0; + } +" HAVE_STD_ATOMIC) + +if(NOT HAVE_STD_ATOMIC) + check_cxx_source_compiles(" + #include + int main() { + boost::atomic x(1); + x.fetch_add(1); + x.fetch_sub(1); + return 0; + } + " HAVE_BOOST_ATOMIC) + + if(NOT HAVE_BOOST_ATOMIC) + check_cxx_source_compiles(" + int main() { + volatile int x; + __sync_add_and_fetch(&x, 1); + int y = __sync_sub_and_fetch(&x, 1); + return 0; + } + " HAVE_GCC_ATOMIC) + + if(NOT HAVE_GCC_ATOMIC) + check_cxx_source_compiles(" + #include + int main() { + volatile int32_t x; + OSAtomicIncrement32Barrier(&x); + int32_t y = OSAtomicDecrement32Barrier(&x); + return 0; + } + " HAVE_MAC_ATOMIC) + + if(NOT HAVE_MAC_ATOMIC) + check_cxx_source_compiles(" + #include + int main() { + volatile LONG x; + InterlockedIncrement(&x); + LONG y = InterlockedDecrement(&x); + return 0; + } + " HAVE_WIN_ATOMIC) + + if(NOT HAVE_WIN_ATOMIC) + check_cxx_source_compiles(" + #include + int main() { + volatile int x; + __sync_add_and_fetch(&x, 1); + int y = __sync_sub_and_fetch(&x, 1); + return 0; + } + " HAVE_IA64_ATOMIC) + endif() + endif() + endif() + endif() +endif() + +# Determine which kind of byte swap functions your compiler supports. + +# GCC's __builtin_bswap* should be checked individually +# because some of them can be missing depends on the GCC version. +check_cxx_source_compiles(" + int main() { + __builtin_bswap16(0); + return 0; + } +" HAVE_GCC_BYTESWAP_16) + +check_cxx_source_compiles(" + int main() { + __builtin_bswap32(0); + return 0; + } +" HAVE_GCC_BYTESWAP_32) + +check_cxx_source_compiles(" + int main() { + __builtin_bswap64(0); + return 0; + } +" HAVE_GCC_BYTESWAP_64) + +if(NOT HAVE_GCC_BYTESWAP_16 OR NOT HAVE_GCC_BYTESWAP_32 OR NOT HAVE_GCC_BYTESWAP_64) + check_cxx_source_compiles(" + #include + int main() { + __bswap_16(0); + __bswap_32(0); + __bswap_64(0); + return 0; + } + " HAVE_GLIBC_BYTESWAP) + + if(NOT HAVE_GLIBC_BYTESWAP) + check_cxx_source_compiles(" + #include + int main() { + _byteswap_ushort(0); + _byteswap_ulong(0); + _byteswap_uint64(0); + return 0; + } + " HAVE_MSC_BYTESWAP) + + if(NOT HAVE_MSC_BYTESWAP) + check_cxx_source_compiles(" + #include + int main() { + OSSwapInt16(0); + OSSwapInt32(0); + OSSwapInt64(0); + return 0; + } + " HAVE_MAC_BYTESWAP) + + if(NOT HAVE_MAC_BYTESWAP) + check_cxx_source_compiles(" + #include + int main() { + swap16(0); + swap32(0); + swap64(0); + return 0; + } + " HAVE_OPENBSD_BYTESWAP) + endif() + endif() + endif() +endif() + +# Determine whether your compiler supports some safer version of sprintf. + +check_cxx_source_compiles(" + #include + int main() { char buf[20]; snprintf(buf, 20, \"%d\", 1); return 0; } +" HAVE_SNPRINTF) + +if(NOT HAVE_SNPRINTF) + check_cxx_source_compiles(" + #include + int main() { char buf[20]; sprintf_s(buf, \"%d\", 1); return 0; } + " HAVE_SPRINTF_S) +endif() + +# Determine whether your compiler supports codecvt. + +check_cxx_source_compiles(" + #include + int main() { + std::codecvt_utf8_utf16 x; + return 0; + } +" HAVE_STD_CODECVT) + +# Check for libz using the cmake supplied FindZLIB.cmake + +find_package(ZLIB) +if(ZLIB_FOUND) + set(HAVE_ZLIB 1) +else() + set(HAVE_ZLIB 0) +endif() + + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) + +find_package(CppUnit) +if(NOT CppUnit_FOUND AND BUILD_TESTS) + message(STATUS "CppUnit not found, disabling tests.") + set(BUILD_TESTS OFF) +endif() diff --git a/taglib/Doxyfile.cmake b/taglib/Doxyfile.cmake new file mode 100644 index 0000000..6da30bb --- /dev/null +++ b/taglib/Doxyfile.cmake @@ -0,0 +1,210 @@ +# Doxyfile 1.3.4 + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- +PROJECT_NAME = TagLib +PROJECT_NUMBER = ${TAGLIB_LIB_VERSION_STRING} +OUTPUT_DIRECTORY = doc +OUTPUT_LANGUAGE = English +USE_WINDOWS_ENCODING = NO +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = YES +ALWAYS_DETAILED_SEC = NO +INLINE_INHERITED_MEMB = NO +FULL_PATH_NAMES = NO +STRIP_FROM_PATH = +SHORT_NAMES = NO +JAVADOC_AUTOBRIEF = NO +MULTILINE_CPP_IS_BRIEF = NO +DETAILS_AT_TOP = NO +INHERIT_DOCS = YES +DISTRIBUTE_GROUP_DOC = NO +TAB_SIZE = 4 +ALIASES = +OPTIMIZE_OUTPUT_FOR_C = NO +OPTIMIZE_OUTPUT_JAVA = NO +SUBGROUPING = YES +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- +EXTRACT_ALL = YES +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = NO +EXTRACT_LOCAL_CLASSES = NO +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +HIDE_FRIEND_COMPOUNDS = NO +HIDE_IN_BODY_DOCS = NO +INTERNAL_DOCS = YES +CASE_SENSE_NAMES = YES +HIDE_SCOPE_NAMES = NO +SHOW_INCLUDE_FILES = YES +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES +GENERATE_TODOLIST = NO +GENERATE_TESTLIST = NO +GENERATE_BUGLIST = NO +GENERATE_DEPRECATEDLIST= NO +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +SHOW_USED_FILES = YES +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- +QUIET = NO +WARNINGS = YES +WARN_IF_UNDOCUMENTED = NO +WARN_IF_DOC_ERROR = YES +WARN_FORMAT = "$file:$line: $text" +WARN_LOGFILE = +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- +INPUT = @CMAKE_SOURCE_DIR@/taglib +FILE_PATTERNS = *.h \ + *.hh \ + *.H +RECURSIVE = YES +EXCLUDE = +EXCLUDE_SYMLINKS = NO +EXCLUDE_PATTERNS = +EXAMPLE_PATH = +EXAMPLE_PATTERNS = +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = +FILTER_SOURCE_FILES = NO +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- +SOURCE_BROWSER = NO +INLINE_SOURCES = NO +STRIP_CODE_COMMENTS = YES +REFERENCED_BY_RELATION = YES +REFERENCES_RELATION = YES +VERBATIM_HEADERS = YES +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- +ALPHABETICAL_INDEX = YES +COLS_IN_ALPHA_INDEX = 5 +IGNORE_PREFIX = +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_FILE_EXTENSION = .html +HTML_HEADER = @CMAKE_SOURCE_DIR@/doc/api-header.html +HTML_FOOTER = @CMAKE_SOURCE_DIR@/doc/api-footer.html +HTML_STYLESHEET = @CMAKE_SOURCE_DIR@/doc/taglib-api.css +HTML_ALIGN_MEMBERS = YES +GENERATE_HTMLHELP = NO +CHM_FILE = +HHC_LOCATION = +GENERATE_CHI = NO +BINARY_TOC = NO +TOC_EXPAND = NO +DISABLE_INDEX = YES +ENUM_VALUES_PER_LINE = 4 +GENERATE_TREEVIEW = NO +TREEVIEW_WIDTH = 250 +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- +GENERATE_LATEX = NO +LATEX_OUTPUT = latex +LATEX_CMD_NAME = latex +MAKEINDEX_CMD_NAME = makeindex +COMPACT_LATEX = NO +PAPER_TYPE = letter +EXTRA_PACKAGES = +LATEX_HEADER = +PDF_HYPERLINKS = YES +USE_PDFLATEX = YES +LATEX_BATCHMODE = NO +LATEX_HIDE_INDICES = NO +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- +GENERATE_RTF = NO +RTF_OUTPUT = rtf +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- +GENERATE_MAN = NO +MAN_OUTPUT = man +MAN_EXTENSION = .3 +MAN_LINKS = NO +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- +GENERATE_XML = NO +XML_OUTPUT = xml +XML_SCHEMA = +XML_DTD = +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- +GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- +GENERATE_PERLMOD = NO +PERLMOD_LATEX = NO +PERLMOD_PRETTY = YES +PERLMOD_MAKEVAR_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = NO +EXPAND_ONLY_PREDEF = NO +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = DO_NOT_DOCUMENT \ + DOXYGEN \ + WITH_MP4 \ + WITH_ASF +EXPAND_AS_DEFINED = +SKIP_FUNCTION_MACROS = YES +#--------------------------------------------------------------------------- +# Configuration::addtions related to external references +#--------------------------------------------------------------------------- +TAGFILES = +GENERATE_TAGFILE = +ALLEXTERNALS = NO +EXTERNAL_GROUPS = YES +PERL_PATH = /usr/bin/perl +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- +CLASS_DIAGRAMS = YES +HIDE_UNDOC_RELATIONS = YES +HAVE_DOT = YES +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +UML_LOOK = NO +TEMPLATE_RELATIONS = YES +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +CALL_GRAPH = NO +GRAPHICAL_HIERARCHY = YES +DOT_IMAGE_FORMAT = png +DOT_PATH = +DOTFILE_DIRS = +MAX_DOT_GRAPH_WIDTH = 1024 +MAX_DOT_GRAPH_HEIGHT = 1024 +MAX_DOT_GRAPH_DEPTH = 0 +GENERATE_LEGEND = YES +DOT_CLEANUP = YES +#--------------------------------------------------------------------------- +# Configuration::addtions related to the search engine +#--------------------------------------------------------------------------- +SEARCHENGINE = NO diff --git a/taglib/INSTALL b/taglib/INSTALL new file mode 100644 index 0000000..9e6e807 --- /dev/null +++ b/taglib/INSTALL @@ -0,0 +1,157 @@ +TagLib Installation +=================== + +TagLib uses the CMake build system. As a user, you will most likely want to +build TagLib in release mode and install it into a system-wide location. +This can be done using the following commands: + + cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_RELEASE_TYPE=Release . + make + sudo make install + +In order to build the included examples, use the BUILD_EXAMPLES option: + + cmake -DBUILD_EXAMPLES=ON [...] + +See http://www.cmake.org/cmake/help/runningcmake.html for generic help on +running CMake. + +Mac OS X +-------- + +On Mac OS X, you might want to build a framework that can be easily integrated +into your application. If you set the BUILD_FRAMEWORK option on, it will compile +TagLib as a framework. For example, the following command can be used to build +an Universal Binary framework with Mac OS X 10.4 as the deployment target: + + cmake -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_FRAMEWORK=ON \ + -DCMAKE_C_COMPILER=/usr/bin/gcc-4.0 \ + -DCMAKE_CXX_COMPILER=/usr/bin/c++-4.0 \ + -DCMAKE_OSX_SYSROOT=/Developer/SDKs/MacOSX10.4u.sdk/ \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.4 \ + -DCMAKE_OSX_ARCHITECTURES="ppc;i386;x86_64" + +For a 10.6 Snow Leopard static library with both 32-bit and 64-bit code, use: + + cmake -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.6 \ + -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" \ + -DENABLE_STATIC=ON \ + -DCMAKE_INSTALL_PREFIX="" + +After 'make', and 'make install', add libtag.a to your XCode project, and add +the include folder to the project's User Header Search Paths. + +Windows +------- + +It's Windows ... Systems vary! +This means you need to adjust things to suit your system, especially paths. + +Tested with: + Microsoft Visual Studio 2010 + Gcc by mingw-w64.sf.net v4.6.3 (Strawberry Perl 32b) + MinGW32-4.8.0 + +Requirements: + 1. Tool chain, Build Environment, Whatever ya want to call it ... + Installed and working. + 2. CMake program. (Available at: www.cmake.org) + Installed and working. + +Optional: + 1. Zlib library. + Available in some Tool Chains, Not all. + Search the web, Take your choice. + +Useful configuration options used with CMake (GUI and/or Command line): + Any of the ZLIB_ variables may be used at the command line, ZLIB_ROOT is only + available on the Command line. + ZLIB_ROOT= Where to find ZLib's root directory. + Assumes parent of: \include and \lib. + ZLIB_INCLUDE_DIR= Where to find ZLib's Include directory. + ZLIB_LIBRARY= Where to find ZLib's Library. + CMAKE_INSTALL_PREFIX= Where to install Taglib. + CMAKE_BUILD_TYPE= Release, Debug, etc ... (Not available in MSVC) + +The easiest way is at the Command Prompt. + MSVS Command Prompt for MSVS Users. + (Batch file and/or Shortcuts are your friends) + + 1. Build the Makefiles: + Replace "GENERATOR" with your needs. + For MSVS : "Visual Studio X" where X is the single or two digit version. + For MinGW: "MinGW Makefiles" + + C:\GitRoot\taglib> cmake -G "GENERATOR" -DCMAKE_INSTALL_PREFIX=C:\Libraries\taglib + + Or use the CMake GUI: + 1. Open CMake GUI. + 2. Set Paths. + "Where is the source code" and "Where to build the binaries" + Example, Both would be: C:\GitRoot\taglib + 3. Tick: Advanced + 4. Select: Configure + 5. Select: Generator + 6. Tick: Use default native compilers + 7. Select: Finish + Wait until done. + 5. If using ZLib, Scroll down. + (to the bottom of the list of options ... should go over them all) + 1. Edit: ZLIB_INCLUDE_DIR + 2. Edit: ZLIB_LIBRARY + 6. Select: Generate + + 2. Build the project: + MSVS: + C:\GitRoot\taglib> msbuild all_build.vcxproj /p:Configuration=Release + OR (Depending on MSVS version or personal choice) + C:\GitRoot\taglib> devenv all_build.vcxproj /build Release + MinGW: + C:\GitRoot\taglib> gmake + OR (Depending on MinGW install) + C:\GitRoot\taglib> mingw32-make + + Or in the MSVS GUI: + 1. Open MSVS. + 2. Open taglib solution. + 3. Set build type to: Release (look in the tool bars) + 2. Hit F7 to build the solution. (project) + + 3. Install the project: + (Change 'install' to 'uninstall' to uninstall the project) + MSVS: + C:\GitRoot\taglib> msbuild install.vcxproj + OR (Depending on MSVC version or personal choice) + C:\GitRoot\taglib> devenv install.vcxproj + MinGW: + C:\GitRoot\taglib> gmake install + OR (Depending on MinGW install) + C:\GitRoot\taglib> mingw32-make install + + Or in the MSVS GUI: + 1. Open project. + 2. Open Solution Explorer. + 3. Right Click: INSTALL + 4. Select: Project Only + 5. Select: Build Only INSTALL + +To build a static library enable the following two options with CMake. + -DENABLE_STATIC=ON -DENABLE_STATIC_RUNTIME=ON + +Including ENABLE_STATIC_RUNTIME=ON indicates you want TagLib built using the +static runtime library, rather than the DLL form of the runtime. + +Unit Tests +---------- + +If you want to run the test suite to make sure TagLib works properly on your +system, you need to have cppunit installed. To build the tests, include +the option -DBUILD_TESTS=on when running cmake. + +The test suite has a custom target in the build system, so you can run +the tests using make: + + make check + diff --git a/taglib/NEWS b/taglib/NEWS new file mode 100644 index 0000000..72d41fb --- /dev/null +++ b/taglib/NEWS @@ -0,0 +1,189 @@ +TagLib 1.9.1 (Oct 8, 2013) +========================== + + * Fixed binary incompatible change in TagLib::Map and TagLib::List. + * Fixed constructing String from ByteVector. + * Fixed compilation on MSVC with the /Zc:wchar_t- option. + * Fixed detecting of RIFF files with invalid chunk sizes. + * Added TagLib::MP4::PropertyMap::codec(). + +TagLib 1.9 (Oct 6, 2013) +======================== + + * Added support for the Ogg Opus file format. + * Added support for INFO tags in WAV files. + * Changed FileStream to use Windows file API. + * Included taglib-config.cmd script for Windows. + * New ID3v1::Tag methods for working directly with genre numbers. + * New MPEG::File methods for checking which tags are saved in the file. + * Added support for the PropertyMap API to ASF and MP4 files. + * Added MusicBrainz identifiers to the PropertyMap API. + * Allowed reading of MP4 cover art without an explicitly specified format. + * Better parsing of corrupted FLAC files. + * Fixed saving of PropertyMap comments without description into ID3v2 tags. + * Fixed crash when parsing certain XM files. + * Fixed compilation of unit test with clang. + * Better handling of files that can't be open or have read-only permissions. + * Improved atomic reference counting. + * New hookable API for debug messages. + * More complete Windows install instructions. + * Many smaller bug fixes and performance improvements. + +TagLib 1.8 (Sep 6, 2012) +======================== + +1.8: + + * Added support for OWNE ID3 frames. + * Changed key validation in the new PropertyMap API. + * ID3v1::Tag::setStringHandler will no londer delete the previous handler, + the caller is responsible for this. + * File objects will also no longer delete the passed IOStream objects. It + should be done in the caller code after the File object is no longer + used. + * Added ID3v2::Tag::setLatin1StringHandler for custom handling of + latin1-encoded text in ID3v2 frames. + * Fixed validation of ID3v2 frame IDs (IDs with '0' were ignored). + +1.8 BETA: + + * New API for accessing tags by name. + * New abstract I/O stream layer to allow custom I/O handlers. + * Support for writing ID3v2.3 tags. + * Support for various module file formats (MOD, S3M, IT, XM). + * Support for MP4 and ASF is now enabled by default. + * Started using atomic int operations for reference counting. + * Added methods for checking if WMA and MP4 files are DRM-protected. + * Added taglib_free to the C bindings. + * New method to allow removing pictures from FLAC files. + * Support for reading audio properties from ALAC and Musepack SV8 files. + * Added replay-gain information to Musepack audio properties. + * Support for APEv2 binary tags. + * Many AudioProperties subclasses now provide information about the total number of samples. + * Various small bug fixes. + +TagLib 1.7.2 (Apr 20, 2012) +=========================== + + * Fixed division by zero while parsing corrupted MP4 files (CVE-2012-2396). + * Fixed compilation on Haiku. + +TagLib 1.7.1 (Mar 17, 2012) +=========================== + + * Improved parsing of corrupted WMA, RIFF and OGG files. + * Fixed a memory leak in the WMA parser. + * Fixed a memory leak in the FLAC parser. + * Fixed a possible division by zero in the APE parser. + * Added detection of TTA2 files. + * Fixed saving of multiple identically named tags to Vorbis Comments. + +TagLib 1.7 (Mar 11, 2011) +========================= + +1.7: + + * Fixed memory leaks in the FLAC file format parser. + * Fixed bitrate calculation for WAV files. + +1.7 RC1: + + * Support for reading/writing tags from Monkey's Audio files. (BUG:210404) + * Support for reading/writing embedded pictures from WMA files. + * Support for reading/writing embedded pictures from FLAC files (BUG:218696). + * Implemented APE::Tag::isEmpty() to check for all APE tags, not just the + basic ones. + * Added reading of WAV audio length. (BUG:116033) + * Exposed FLAC MD5 signature of the uncompressed audio stream via + FLAC::Properties::signature(). (BUG:160172) + * Added function ByteVector::toHex() for hex-encoding of byte vectors. + * WavPack reader now tries to get the audio length by finding the final + block, if the header doesn't have the information. (BUG:258016) + * Fixed a memory leak in the ID3v2.2 PIC frame parser. (BUG:257007) + * Fixed writing of RIFF files with even chunk sizes. (BUG:243954) + * Fixed compilation on MSVC 2010. + * Removed support for building using autoconf/automake. + * API docs can be now built using "make docs". + +TagLib 1.6.3 (Apr 17, 2010) +=========================== + + * Fixed definitions of the TAGLIB_WITH_MP4 and TAGLIB_WITH_ASF macros. + * Fixed upgrading of ID3v2.3 genre frame with ID3v1 code 0 (Blues). + * New method `int String::toInt(bool *ok)` which can return whether the + conversion to a number was successfull. + * Fixed parsing of incorrectly written lengths in ID3v2 (affects mainly + compressed frames). (BUG:231075) + +TagLib 1.6.2 (Apr 9, 2010) +========================== + + * Read Vorbis Comments from the first FLAC metadata block, if there are + multipe ones. (BUG:211089) + * Fixed a memory leak in FileRef's OGA format detection. + * Fixed compilation with the Sun Studio compiler. (BUG:215225) + * Handle WM/TrackNumber attributes with DWORD content in WMA files. + (BUG:218526) + * More strict check if something is a valid MP4 file. (BUG:216819) + * Correctly save MP4 int-pair atoms with flags set to 0. + * Fixed compilation of the test runner on Windows. + * Store ASF attributes larger than 64k in the metadata library object. + * Ignore trailing non-data atoms when parsing MP4 covr atoms. + * Don't upgrade ID3v2.2 frame TDA to TDRC. (BUG:228968) + +TagLib 1.6.1 (Oct 31, 2009) +=========================== + + * Better detection of the audio codec of .oga files in FileRef. + * Fixed saving of Vorbis comments to Ogg FLAC files. TagLib tried to + include the Vorbis framing bit, which is only correct for Ogg Vorbis. + * Public symbols now have explicitly set visibility to "default" on GCC. + * Added missing exports for static ID3v1 functions. + * Fixed a typo in taglib_c.pc + * Fixed a failing test on ppc64. + * Support for binary 'covr' atom in MP4 files. TagLib 1.6 treated them + as text atoms, which corrupted them in some cases. + * Fixed ID3v1-style genre to string conversion in MP4 files. + +TagLib 1.6 (Sep 13, 2009) +========================= + +1.6: + + * New CMake option to build a static version - ENABLE_STATIC. + * Added support for disabling dllimport/dllexport on Windows using the + TAGLIB_STATIC macro. + * Support for parsing the obsolete 'gnre' MP4 atom. + * New cpp macros TAGLIB_WITH_MP4 and TAGLIB_WITH_ASF to determin if + TagLib was built with MP4/ASF support. + +1.6 RC1: + + * Split Ogg packets larger than 64k into multiple pages. (BUG:171957) + * TagLib can now use FLAC padding block. (BUG:107659) + * ID3v2.2 frames are now not incorrectly saved. (BUG:176373) + * Support for ID3v2.2 PIC frames. (BUG:167786) + * Fixed a bug in ByteVectorList::split(). + * XiphComment::year() now falls back to YEAR if DATE doesn't exist + and XiphComment::year() falls back to TRACKNUM if TRACKNUMBER doesn't + exist. (BUG:144396) + * Improved ID3v2.3 genre parsing. (BUG:188578) + * Better checking of corrupted ID3v2 APIC data. (BUG:168382) + * Bitrate calculating using the Xing header now uses floating point + numbers. (BUG:172556) + * New TagLib::String method rfind(). + * Added support for MP4 file format with iTunes-style metadata [optional]. + * Added support for ASF (WMA) file format [optional]. + * Fixed crash when saving a Locator APEv2 tag. (BUG:169810) + * Fixed a possible crash in the non-const version of String::operator[] + and in String::operator+=. (BUG:169389) + * Added support for PRIV ID3v2 frames. + * Empty ID3v2 genres are no longer treated as numeric ID3v1 genres. + * Added support for the POPM (rating/playcount) ID3v2 frame. + * Generic RIFF file format support: + * Support for AIFF files with ID3v2 tags. + * Support for WAV files with ID3v2 tags. + * Fixed crash on handling unsupported ID3v2 frames, e.g. on encrypted + frames. (BUG:161721) + * Fixed overflow while calculating bitrate of FLAC files with a very + high bitrate. diff --git a/taglib/bindings/CMakeLists.txt b/taglib/bindings/CMakeLists.txt new file mode 100644 index 0000000..d019843 --- /dev/null +++ b/taglib/bindings/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(c) diff --git a/taglib/bindings/README b/taglib/bindings/README new file mode 100644 index 0000000..bfbd44a --- /dev/null +++ b/taglib/bindings/README @@ -0,0 +1,6 @@ +There are a few other people that have done bindings externally that I have +been made aware of. I have not personally reviewed these bindings, but I'm +listing them here so that those who find them useful are able to find them: + +http://developer.kde.org/~wheeler/taglib.html#bindings + diff --git a/taglib/bindings/c/CMakeLists.txt b/taglib/bindings/c/CMakeLists.txt new file mode 100644 index 0000000..2d9dafc --- /dev/null +++ b/taglib/bindings/c/CMakeLists.txt @@ -0,0 +1,68 @@ +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/toolkit + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/asf + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/mpeg + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/ogg + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/ogg/vorbis + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/ogg/flac + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/flac + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/mpc + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/mp4 + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/mpeg/id3v2 + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/mpeg/id3v2/frames + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/wavpack + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/ogg/speex + ${CMAKE_CURRENT_SOURCE_DIR}/../../taglib/trueaudio +) + +set(tag_c_HDRS tag_c.h) + +add_library(tag_c tag_c.cpp ${tag_c_HDRS}) + +target_link_libraries(tag_c tag) +set_target_properties(tag_c PROPERTIES PUBLIC_HEADER "${tag_c_HDRS}") +if(BUILD_FRAMEWORK) + set_target_properties(tag_c PROPERTIES FRAMEWORK TRUE) +endif() + +# On Solaris we need to explicitly add the C++ standard and runtime +# libraries to the libs used by the C bindings, because those C bindings +# themselves won't pull in the C++ libs -- and if a C application is +# using the C bindings then we get link errors. +check_library_exists(Crun __RTTI___ "" HAVE_CRUN_LIB) +if(HAVE_CRUN_LIB) + # Which libraries to link depends critically on which + # STL version is going to be used by your application + # and which runtime is in use. While Crun is pretty much + # the only game in town, the three available STLs -- Cstd, + # stlport4 and stdcxx -- make this a mess. The KDE-Solaris + # team supports stdcxx (Apache RogueWave stdcxx 4.1.3). + + # According to http://bugs.kde.org/show_bug.cgi?id=215225 the library can have the following two names: + find_library(ROGUEWAVE_STDCXX_LIBRARY NAMES stdcxx4 stdcxx) + if(NOT ROGUEWAVE_STDCXX_LIBRARY) + message(FATAL_ERROR "Did not find supported STL library (tried stdcxx4 and stdcxx)") + endif() + target_link_libraries(tag_c ${ROGUEWAVE_STDCXX_LIBRARY} Crun) +endif() + +set_target_properties(tag_c PROPERTIES + VERSION 0.0.0 + SOVERSION 0 + DEFINE_SYMBOL MAKE_TAGLIB_C_LIB + INSTALL_NAME_DIR ${LIB_INSTALL_DIR} +) +install(TARGETS tag_c + FRAMEWORK DESTINATION ${FRAMEWORK_INSTALL_DIR} + LIBRARY DESTINATION ${LIB_INSTALL_DIR} + RUNTIME DESTINATION ${BIN_INSTALL_DIR} + ARCHIVE DESTINATION ${LIB_INSTALL_DIR} + PUBLIC_HEADER DESTINATION ${INCLUDE_INSTALL_DIR}/taglib +) + +if(NOT WIN32 AND NOT BUILD_FRAMEWORK) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/taglib_c.pc.cmake ${CMAKE_CURRENT_BINARY_DIR}/taglib_c.pc) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/taglib_c.pc DESTINATION ${LIB_INSTALL_DIR}/pkgconfig) +endif() + diff --git a/taglib/bindings/c/tag_c.cpp b/taglib/bindings/c/tag_c.cpp new file mode 100644 index 0000000..2ae8c7d --- /dev/null +++ b/taglib/bindings/c/tag_c.cpp @@ -0,0 +1,290 @@ +/*************************************************************************** + copyright : (C) 2003 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * + * USA * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tag_c.h" + +using namespace TagLib; + +static List strings; +static bool unicodeStrings = true; +static bool stringManagementEnabled = true; + +void taglib_set_strings_unicode(BOOL unicode) +{ + unicodeStrings = bool(unicode); +} + +void taglib_set_string_management_enabled(BOOL management) +{ + stringManagementEnabled = bool(management); +} + +void taglib_free(void* pointer) +{ + free(pointer); +} + +//////////////////////////////////////////////////////////////////////////////// +// TagLib::File wrapper +//////////////////////////////////////////////////////////////////////////////// + +TagLib_File *taglib_file_new(const char *filename) +{ + return reinterpret_cast(FileRef::create(filename)); +} + +TagLib_File *taglib_file_new_type(const char *filename, TagLib_File_Type type) +{ + switch(type) { + case TagLib_File_MPEG: + return reinterpret_cast(new MPEG::File(filename)); + case TagLib_File_OggVorbis: + return reinterpret_cast(new Ogg::Vorbis::File(filename)); + case TagLib_File_FLAC: + return reinterpret_cast(new FLAC::File(filename)); + case TagLib_File_MPC: + return reinterpret_cast(new MPC::File(filename)); + case TagLib_File_OggFlac: + return reinterpret_cast(new Ogg::FLAC::File(filename)); + case TagLib_File_WavPack: + return reinterpret_cast(new WavPack::File(filename)); + case TagLib_File_Speex: + return reinterpret_cast(new Ogg::Speex::File(filename)); + case TagLib_File_TrueAudio: + return reinterpret_cast(new TrueAudio::File(filename)); + case TagLib_File_MP4: + return reinterpret_cast(new MP4::File(filename)); + case TagLib_File_ASF: + return reinterpret_cast(new ASF::File(filename)); + default: + return 0; + } + + return 0; +} + +void taglib_file_free(TagLib_File *file) +{ + delete reinterpret_cast(file); +} + +BOOL taglib_file_is_valid(const TagLib_File *file) +{ + return reinterpret_cast(file)->isValid(); +} + +TagLib_Tag *taglib_file_tag(const TagLib_File *file) +{ + const File *f = reinterpret_cast(file); + return reinterpret_cast(f->tag()); +} + +const TagLib_AudioProperties *taglib_file_audioproperties(const TagLib_File *file) +{ + const File *f = reinterpret_cast(file); + return reinterpret_cast(f->audioProperties()); +} + +BOOL taglib_file_save(TagLib_File *file) +{ + return reinterpret_cast(file)->save(); +} + +//////////////////////////////////////////////////////////////////////////////// +// TagLib::Tag wrapper +//////////////////////////////////////////////////////////////////////////////// + +char *taglib_tag_title(const TagLib_Tag *tag) +{ + const Tag *t = reinterpret_cast(tag); + char *s = ::strdup(t->title().toCString(unicodeStrings)); + if(stringManagementEnabled) + strings.append(s); + return s; +} + +char *taglib_tag_artist(const TagLib_Tag *tag) +{ + const Tag *t = reinterpret_cast(tag); + char *s = ::strdup(t->artist().toCString(unicodeStrings)); + if(stringManagementEnabled) + strings.append(s); + return s; +} + +char *taglib_tag_album(const TagLib_Tag *tag) +{ + const Tag *t = reinterpret_cast(tag); + char *s = ::strdup(t->album().toCString(unicodeStrings)); + if(stringManagementEnabled) + strings.append(s); + return s; +} + +char *taglib_tag_comment(const TagLib_Tag *tag) +{ + const Tag *t = reinterpret_cast(tag); + char *s = ::strdup(t->comment().toCString(unicodeStrings)); + if(stringManagementEnabled) + strings.append(s); + return s; +} + +char *taglib_tag_genre(const TagLib_Tag *tag) +{ + const Tag *t = reinterpret_cast(tag); + char *s = ::strdup(t->genre().toCString(unicodeStrings)); + if(stringManagementEnabled) + strings.append(s); + return s; +} + +unsigned int taglib_tag_year(const TagLib_Tag *tag) +{ + const Tag *t = reinterpret_cast(tag); + return t->year(); +} + +unsigned int taglib_tag_track(const TagLib_Tag *tag) +{ + const Tag *t = reinterpret_cast(tag); + return t->track(); +} + +void taglib_tag_set_title(TagLib_Tag *tag, const char *title) +{ + Tag *t = reinterpret_cast(tag); + t->setTitle(String(title, unicodeStrings ? String::UTF8 : String::Latin1)); +} + +void taglib_tag_set_artist(TagLib_Tag *tag, const char *artist) +{ + Tag *t = reinterpret_cast(tag); + t->setArtist(String(artist, unicodeStrings ? String::UTF8 : String::Latin1)); +} + +void taglib_tag_set_album(TagLib_Tag *tag, const char *album) +{ + Tag *t = reinterpret_cast(tag); + t->setAlbum(String(album, unicodeStrings ? String::UTF8 : String::Latin1)); +} + +void taglib_tag_set_comment(TagLib_Tag *tag, const char *comment) +{ + Tag *t = reinterpret_cast(tag); + t->setComment(String(comment, unicodeStrings ? String::UTF8 : String::Latin1)); +} + +void taglib_tag_set_genre(TagLib_Tag *tag, const char *genre) +{ + Tag *t = reinterpret_cast(tag); + t->setGenre(String(genre, unicodeStrings ? String::UTF8 : String::Latin1)); +} + +void taglib_tag_set_year(TagLib_Tag *tag, unsigned int year) +{ + Tag *t = reinterpret_cast(tag); + t->setYear(year); +} + +void taglib_tag_set_track(TagLib_Tag *tag, unsigned int track) +{ + Tag *t = reinterpret_cast(tag); + t->setTrack(track); +} + +void taglib_tag_free_strings() +{ + if(!stringManagementEnabled) + return; + + for(List::Iterator it = strings.begin(); it != strings.end(); ++it) + free(*it); + strings.clear(); +} + +//////////////////////////////////////////////////////////////////////////////// +// TagLib::AudioProperties wrapper +//////////////////////////////////////////////////////////////////////////////// + +int taglib_audioproperties_length(const TagLib_AudioProperties *audioProperties) +{ + const AudioProperties *p = reinterpret_cast(audioProperties); + return p->length(); +} + +int taglib_audioproperties_bitrate(const TagLib_AudioProperties *audioProperties) +{ + const AudioProperties *p = reinterpret_cast(audioProperties); + return p->bitrate(); +} + +int taglib_audioproperties_samplerate(const TagLib_AudioProperties *audioProperties) +{ + const AudioProperties *p = reinterpret_cast(audioProperties); + return p->sampleRate(); +} + +int taglib_audioproperties_channels(const TagLib_AudioProperties *audioProperties) +{ + const AudioProperties *p = reinterpret_cast(audioProperties); + return p->channels(); +} + +void taglib_id3v2_set_default_text_encoding(TagLib_ID3v2_Encoding encoding) +{ + String::Type type = String::Latin1; + + switch(encoding) + { + case TagLib_ID3v2_Latin1: + type = String::Latin1; + break; + case TagLib_ID3v2_UTF16: + type = String::UTF16; + break; + case TagLib_ID3v2_UTF16BE: + type = String::UTF16BE; + break; + case TagLib_ID3v2_UTF8: + type = String::UTF8; + break; + } + + ID3v2::FrameFactory::instance()->setDefaultTextEncoding(type); +} diff --git a/taglib/bindings/c/tag_c.h b/taglib/bindings/c/tag_c.h new file mode 100644 index 0000000..2e8b752 --- /dev/null +++ b/taglib/bindings/c/tag_c.h @@ -0,0 +1,299 @@ +/*************************************************************************** + copyright : (C) 2003 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * + * USA * + ***************************************************************************/ + +#ifndef TAGLIB_TAG_C +#define TAGLIB_TAG_C + +/* Do not include this in the main TagLib documentation. */ +#ifndef DO_NOT_DOCUMENT + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(TAGLIB_STATIC) +#define TAGLIB_C_EXPORT +#elif defined(_WIN32) || defined(_WIN64) +#ifdef MAKE_TAGLIB_C_LIB +#define TAGLIB_C_EXPORT __declspec(dllexport) +#else +#define TAGLIB_C_EXPORT __declspec(dllimport) +#endif +#elif defined(__GNUC__) && (__GNUC__ > 4 || __GNUC__ == 4 && __GNUC_MINOR__ >= 1) +#define TAGLIB_C_EXPORT __attribute__ ((visibility("default"))) +#else +#define TAGLIB_C_EXPORT +#endif + +#ifndef BOOL +#define BOOL int +#endif + +/******************************************************************************* + * [ TagLib C Binding ] + * + * This is an interface to TagLib's "simple" API, meaning that you can read and + * modify media files in a generic, but not specialized way. This is a rough + * representation of TagLib::File and TagLib::Tag, for which the documentation + * is somewhat more complete and worth consulting. + *******************************************************************************/ + +/* + * These are used for type provide some type safety to the C API (as opposed to + * using void *, but pointers to them are simply cast to the corresponding C++ + * types in the implementation. + */ + +typedef struct { int dummy; } TagLib_File; +typedef struct { int dummy; } TagLib_Tag; +typedef struct { int dummy; } TagLib_AudioProperties; + +/*! + * By default all strings coming into or out of TagLib's C API are in UTF8. + * However, it may be desirable for TagLib to operate on Latin1 (ISO-8859-1) + * strings in which case this should be set to FALSE. + */ +TAGLIB_C_EXPORT void taglib_set_strings_unicode(BOOL unicode); + +/*! + * TagLib can keep track of strings that are created when outputting tag values + * and clear them using taglib_tag_clear_strings(). This is enabled by default. + * However if you wish to do more fine grained management of strings, you can do + * so by setting \a management to FALSE. + */ +TAGLIB_C_EXPORT void taglib_set_string_management_enabled(BOOL management); + +/*! + * Explicitly free a string returned from TagLib + */ +TAGLIB_C_EXPORT void taglib_free(void* pointer); + +/******************************************************************************* + * File API + ******************************************************************************/ + +typedef enum { + TagLib_File_MPEG, + TagLib_File_OggVorbis, + TagLib_File_FLAC, + TagLib_File_MPC, + TagLib_File_OggFlac, + TagLib_File_WavPack, + TagLib_File_Speex, + TagLib_File_TrueAudio, + TagLib_File_MP4, + TagLib_File_ASF +} TagLib_File_Type; + +/*! + * Creates a TagLib file based on \a filename. TagLib will try to guess the file + * type. + * + * \returns NULL if the file type cannot be determined or the file cannot + * be opened. + */ +TAGLIB_C_EXPORT TagLib_File *taglib_file_new(const char *filename); + +/*! + * Creates a TagLib file based on \a filename. Rather than attempting to guess + * the type, it will use the one specified by \a type. + */ +TAGLIB_C_EXPORT TagLib_File *taglib_file_new_type(const char *filename, TagLib_File_Type type); + +/*! + * Frees and closes the file. + */ +TAGLIB_C_EXPORT void taglib_file_free(TagLib_File *file); + +/*! + * Returns true if the file is open and readble and valid information for + * the Tag and / or AudioProperties was found. + */ + +TAGLIB_C_EXPORT BOOL taglib_file_is_valid(const TagLib_File *file); + +/*! + * Returns a pointer to the tag associated with this file. This will be freed + * automatically when the file is freed. + */ +TAGLIB_C_EXPORT TagLib_Tag *taglib_file_tag(const TagLib_File *file); + +/*! + * Returns a pointer to the the audio properties associated with this file. This + * will be freed automatically when the file is freed. + */ +TAGLIB_C_EXPORT const TagLib_AudioProperties *taglib_file_audioproperties(const TagLib_File *file); + +/*! + * Saves the \a file to disk. + */ +TAGLIB_C_EXPORT BOOL taglib_file_save(TagLib_File *file); + +/****************************************************************************** + * Tag API + ******************************************************************************/ + +/*! + * Returns a string with this tag's title. + * + * \note By default this string should be UTF8 encoded and its memory should be + * freed using taglib_tag_free_strings(). + */ +TAGLIB_C_EXPORT char *taglib_tag_title(const TagLib_Tag *tag); + +/*! + * Returns a string with this tag's artist. + * + * \note By default this string should be UTF8 encoded and its memory should be + * freed using taglib_tag_free_strings(). + */ +TAGLIB_C_EXPORT char *taglib_tag_artist(const TagLib_Tag *tag); + +/*! + * Returns a string with this tag's album name. + * + * \note By default this string should be UTF8 encoded and its memory should be + * freed using taglib_tag_free_strings(). + */ +TAGLIB_C_EXPORT char *taglib_tag_album(const TagLib_Tag *tag); + +/*! + * Returns a string with this tag's comment. + * + * \note By default this string should be UTF8 encoded and its memory should be + * freed using taglib_tag_free_strings(). + */ +TAGLIB_C_EXPORT char *taglib_tag_comment(const TagLib_Tag *tag); + +/*! + * Returns a string with this tag's genre. + * + * \note By default this string should be UTF8 encoded and its memory should be + * freed using taglib_tag_free_strings(). + */ +TAGLIB_C_EXPORT char *taglib_tag_genre(const TagLib_Tag *tag); + +/*! + * Returns the tag's year or 0 if year is not set. + */ +TAGLIB_C_EXPORT unsigned int taglib_tag_year(const TagLib_Tag *tag); + +/*! + * Returns the tag's track number or 0 if track number is not set. + */ +TAGLIB_C_EXPORT unsigned int taglib_tag_track(const TagLib_Tag *tag); + +/*! + * Sets the tag's title. + * + * \note By default this string should be UTF8 encoded. + */ +TAGLIB_C_EXPORT void taglib_tag_set_title(TagLib_Tag *tag, const char *title); + +/*! + * Sets the tag's artist. + * + * \note By default this string should be UTF8 encoded. + */ +TAGLIB_C_EXPORT void taglib_tag_set_artist(TagLib_Tag *tag, const char *artist); + +/*! + * Sets the tag's album. + * + * \note By default this string should be UTF8 encoded. + */ +TAGLIB_C_EXPORT void taglib_tag_set_album(TagLib_Tag *tag, const char *album); + +/*! + * Sets the tag's comment. + * + * \note By default this string should be UTF8 encoded. + */ +TAGLIB_C_EXPORT void taglib_tag_set_comment(TagLib_Tag *tag, const char *comment); + +/*! + * Sets the tag's genre. + * + * \note By default this string should be UTF8 encoded. + */ +TAGLIB_C_EXPORT void taglib_tag_set_genre(TagLib_Tag *tag, const char *genre); + +/*! + * Sets the tag's year. 0 indicates that this field should be cleared. + */ +TAGLIB_C_EXPORT void taglib_tag_set_year(TagLib_Tag *tag, unsigned int year); + +/*! + * Sets the tag's track number. 0 indicates that this field should be cleared. + */ +TAGLIB_C_EXPORT void taglib_tag_set_track(TagLib_Tag *tag, unsigned int track); + +/*! + * Frees all of the strings that have been created by the tag. + */ +TAGLIB_C_EXPORT void taglib_tag_free_strings(void); + +/****************************************************************************** + * Audio Properties API + ******************************************************************************/ + +/*! + * Returns the length of the file in seconds. + */ +TAGLIB_C_EXPORT int taglib_audioproperties_length(const TagLib_AudioProperties *audioProperties); + +/*! + * Returns the bitrate of the file in kb/s. + */ +TAGLIB_C_EXPORT int taglib_audioproperties_bitrate(const TagLib_AudioProperties *audioProperties); + +/*! + * Returns the sample rate of the file in Hz. + */ +TAGLIB_C_EXPORT int taglib_audioproperties_samplerate(const TagLib_AudioProperties *audioProperties); + +/*! + * Returns the number of channels in the audio stream. + */ +TAGLIB_C_EXPORT int taglib_audioproperties_channels(const TagLib_AudioProperties *audioProperties); + +/******************************************************************************* + * Special convenience ID3v2 functions + *******************************************************************************/ + +typedef enum { + TagLib_ID3v2_Latin1, + TagLib_ID3v2_UTF16, + TagLib_ID3v2_UTF16BE, + TagLib_ID3v2_UTF8 +} TagLib_ID3v2_Encoding; + +/*! + * This sets the default encoding for ID3v2 frames that are written to tags. + */ + +TAGLIB_C_EXPORT void taglib_id3v2_set_default_text_encoding(TagLib_ID3v2_Encoding encoding); + +#ifdef __cplusplus +} +#endif +#endif /* DO_NOT_DOCUMENT */ +#endif diff --git a/taglib/bindings/c/taglib_c.pc.cmake b/taglib/bindings/c/taglib_c.pc.cmake new file mode 100644 index 0000000..61764fc --- /dev/null +++ b/taglib/bindings/c/taglib_c.pc.cmake @@ -0,0 +1,12 @@ +prefix=${CMAKE_INSTALL_PREFIX} +exec_prefix=${CMAKE_INSTALL_PREFIX} +libdir=${LIB_INSTALL_DIR} +includedir=${INCLUDE_INSTALL_DIR} + + +Name: TagLib C Bindings +Description: Audio meta-data library (C bindings) +Requires: taglib +Version: ${TAGLIB_LIB_MAJOR_VERSION}.${TAGLIB_LIB_MINOR_VERSION}.${TAGLIB_LIB_PATCH_VERSION} +Libs: -L${LIB_INSTALL_DIR} -ltag_c +Cflags: -I${INCLUDE_INSTALL_DIR}/taglib diff --git a/taglib/cmake/modules/FindCppUnit.cmake b/taglib/cmake/modules/FindCppUnit.cmake new file mode 100644 index 0000000..adaaeb6 --- /dev/null +++ b/taglib/cmake/modules/FindCppUnit.cmake @@ -0,0 +1,69 @@ +# - Try to find the libcppunit libraries +# Once done this will define +# +# CppUnit_FOUND - system has libcppunit +# CPPUNIT_INCLUDE_DIR - the libcppunit include directory +# CPPUNIT_LIBRARIES - libcppunit library + +include (MacroEnsureVersion) + +if(NOT CPPUNIT_MIN_VERSION) + SET(CPPUNIT_MIN_VERSION 1.12.0) +endif(NOT CPPUNIT_MIN_VERSION) + +FIND_PROGRAM(CPPUNIT_CONFIG_EXECUTABLE cppunit-config ) + +IF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES) + + # in cache already + SET(CppUnit_FOUND TRUE) + +ELSE(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES) + + SET(CPPUNIT_INCLUDE_DIR) + SET(CPPUNIT_LIBRARIES) + + IF(CPPUNIT_CONFIG_EXECUTABLE) + EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --cflags RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_CFLAGS) + EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --libs RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_LIBRARIES) + EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_INSTALLED_VERSION) + STRING(REGEX REPLACE "-I(.+)" "\\1" CPPUNIT_CFLAGS "${CPPUNIT_CFLAGS}") + ELSE(CPPUNIT_CONFIG_EXECUTABLE) + # in case win32 needs to find it the old way? + FIND_PATH(CPPUNIT_CFLAGS cppunit/TestRunner.h PATHS /usr/include /usr/local/include ) + FIND_LIBRARY(CPPUNIT_LIBRARIES NAMES cppunit PATHS /usr/lib /usr/local/lib ) + # how can we find cppunit version? + MESSAGE (STATUS "Ensure you cppunit installed version is at least ${CPPUNIT_MIN_VERSION}") + SET (CPPUNIT_INSTALLED_VERSION ${CPPUNIT_MIN_VERSION}) + ENDIF(CPPUNIT_CONFIG_EXECUTABLE) + + SET(CPPUNIT_INCLUDE_DIR ${CPPUNIT_CFLAGS} "${CPPUNIT_CFLAGS}/cppunit") + +ENDIF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES) + +IF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES) + + SET(CppUnit_FOUND TRUE) + + if(NOT CppUnit_FIND_QUIETLY) + MESSAGE (STATUS "Found cppunit: ${CPPUNIT_LIBRARIES}") + endif(NOT CppUnit_FIND_QUIETLY) + + IF(CPPUNIT_CONFIG_EXECUTABLE) + EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_INSTALLED_VERSION) + ENDIF(CPPUNIT_CONFIG_EXECUTABLE) + + macro_ensure_version( ${CPPUNIT_MIN_VERSION} ${CPPUNIT_INSTALLED_VERSION} CPPUNIT_INSTALLED_VERSION_OK ) + + IF(NOT CPPUNIT_INSTALLED_VERSION_OK) + MESSAGE ("** CppUnit version is too old: found ${CPPUNIT_INSTALLED_VERSION} installed, ${CPPUNIT_MIN_VERSION} or major is required") + SET(CppUnit_FOUND FALSE) + ENDIF(NOT CPPUNIT_INSTALLED_VERSION_OK) + +ELSE(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES) + + SET(CppUnit_FOUND FALSE CACHE BOOL "Not found cppunit library") + +ENDIF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES) + +MARK_AS_ADVANCED(CPPUNIT_INCLUDE_DIR CPPUNIT_LIBRARIES) diff --git a/taglib/cmake/modules/MacroEnsureVersion.cmake b/taglib/cmake/modules/MacroEnsureVersion.cmake new file mode 100644 index 0000000..c6df537 --- /dev/null +++ b/taglib/cmake/modules/MacroEnsureVersion.cmake @@ -0,0 +1,71 @@ +# This macro compares version numbers of the form "x.y.z" +# MACRO_ENSURE_VERSION( FOO_MIN_VERSION FOO_VERSION_FOUND FOO_VERSION_OK) +# will set FOO_VERSIN_OK to true if FOO_VERSION_FOUND >= FOO_MIN_VERSION +# where both have to be in a 3-part-version format, leading and trailing +# text is ok, e.g. +# MACRO_ENSURE_VERSION( "2.5.31" "flex 2.5.4a" VERSION_OK) +# which means 2.5.31 is required and "flex 2.5.4a" is what was found on the system + +# Copyright (c) 2006, David Faure, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +MACRO(MACRO_ENSURE_VERSION requested_version found_version var_too_old) + + # parse the parts of the version string + STRING(REGEX REPLACE "([0-9]+)\\.[0-9]+\\.[0-9]+" "\\1" req_major_vers "${requested_version}") + STRING(REGEX REPLACE "[0-9]+\\.([0-9]+)\\.[0-9]+" "\\1" req_minor_vers "${requested_version}") + STRING(REGEX REPLACE "[0-9]+\\.[0-9]+\\.([0-9]+)" "\\1" req_patch_vers "${requested_version}") + + STRING(REGEX REPLACE "[^0-9]*([0-9]+)\\.[0-9]+\\.[0-9]+.*" "\\1" found_major_vers "${found_version}") + STRING(REGEX REPLACE "[^0-9]*[0-9]+\\.([0-9]+)\\.[0-9]+.*" "\\1" found_minor_vers "${found_version}") + STRING(REGEX REPLACE "[^0-9]*[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" found_patch_vers "${found_version}") + + # compute an overall version number which can be compared at once + MATH(EXPR req_vers_num "${req_major_vers}*10000 + ${req_minor_vers}*100 + ${req_patch_vers}") + MATH(EXPR found_vers_num "${found_major_vers}*10000 + ${found_minor_vers}*100 + ${found_patch_vers}") + + if (found_vers_num LESS req_vers_num) + set( ${var_too_old} FALSE ) + else (found_vers_num LESS req_vers_num) + set( ${var_too_old} TRUE ) + endif (found_vers_num LESS req_vers_num) + +ENDMACRO(MACRO_ENSURE_VERSION) + + +# This macro compares version numbers of the form "x.y" +# MACRO_ENSURE_VERSION( FOO_MIN_VERSION FOO_VERSION_FOUND FOO_VERSION_OK) +# will set FOO_VERSIN_OK to true if FOO_VERSION_FOUND >= FOO_MIN_VERSION +# where both have to be in a 2-part-version format, leading and trailing +# text is ok, e.g. +# MACRO_ENSURE_VERSION( "0.5" "foo 0.6" VERSION_OK) +# which means 0.5 is required and "foo 0.6" is what was found on the system + +# Copyright (c) 2006, David Faure, +# Copyright (c) 2007, Pino Toscano, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +MACRO(MACRO_ENSURE_VERSION2 requested_version found_version var_too_old) + + # parse the parts of the version string + STRING(REGEX REPLACE "([0-9]+)\\.[0-9]+" "\\1" req_major_vers "${requested_version}") + STRING(REGEX REPLACE "[0-9]+\\.([0-9]+)" "\\1" req_minor_vers "${requested_version}") + + STRING(REGEX REPLACE "[^0-9]*([0-9]+)\\.[0-9]+.*" "\\1" found_major_vers "${found_version}") + STRING(REGEX REPLACE "[^0-9]*[0-9]+\\.([0-9]+).*" "\\1" found_minor_vers "${found_version}") + + # compute an overall version number which can be compared at once + MATH(EXPR req_vers_num "${req_major_vers}*100 + ${req_minor_vers}") + MATH(EXPR found_vers_num "${found_major_vers}*100 + ${found_minor_vers}") + + if (found_vers_num LESS req_vers_num) + set( ${var_too_old} FALSE ) + else (found_vers_num LESS req_vers_num) + set( ${var_too_old} TRUE ) + endif (found_vers_num LESS req_vers_num) + +ENDMACRO(MACRO_ENSURE_VERSION2) diff --git a/taglib/cmake_uninstall.cmake.in b/taglib/cmake_uninstall.cmake.in new file mode 100644 index 0000000..72e030f --- /dev/null +++ b/taglib/cmake_uninstall.cmake.in @@ -0,0 +1,21 @@ +if (NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + message(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"") +endif() + +file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) +string(REGEX REPLACE "\n" ";" files "${files}") +foreach (file ${files}) + message(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"") + if (EXISTS "$ENV{DESTDIR}${file}") + execute_process( + COMMAND @CMAKE_COMMAND@ -E remove "$ENV{DESTDIR}${file}" + OUTPUT_VARIABLE rm_out + RESULT_VARIABLE rm_retval + ) + if(NOT ${rm_retval} EQUAL 0) + message(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") + endif () + else () + message(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.") + endif () +endforeach() diff --git a/taglib/config.h.cmake b/taglib/config.h.cmake new file mode 100644 index 0000000..f8dcbbd --- /dev/null +++ b/taglib/config.h.cmake @@ -0,0 +1,37 @@ +/* config.h. Generated by cmake from config.h.cmake */ + +/* Indicates the byte order of your target system */ +/* 1 if little-endian, 2 if big-endian. */ +#cmakedefine SYSTEM_BYTEORDER ${SYSTEM_BYTEORDER} + +/* Defined if your compiler supports some byte swap functions */ +#cmakedefine HAVE_GCC_BYTESWAP_16 1 +#cmakedefine HAVE_GCC_BYTESWAP_32 1 +#cmakedefine HAVE_GCC_BYTESWAP_64 1 +#cmakedefine HAVE_GLIBC_BYTESWAP 1 +#cmakedefine HAVE_MSC_BYTESWAP 1 +#cmakedefine HAVE_MAC_BYTESWAP 1 +#cmakedefine HAVE_OPENBSD_BYTESWAP 1 + +/* Defined if your compiler supports codecvt */ +#cmakedefine HAVE_STD_CODECVT 1 + +/* Defined if your compiler supports some atomic operations */ +#cmakedefine HAVE_STD_ATOMIC 1 +#cmakedefine HAVE_BOOST_ATOMIC 1 +#cmakedefine HAVE_GCC_ATOMIC 1 +#cmakedefine HAVE_MAC_ATOMIC 1 +#cmakedefine HAVE_WIN_ATOMIC 1 +#cmakedefine HAVE_IA64_ATOMIC 1 + +/* Defined if your compiler supports some safer version of sprintf */ +#cmakedefine HAVE_SNPRINTF 1 +#cmakedefine HAVE_SPRINTF_S 1 + +/* Defined if you have libz */ +#cmakedefine HAVE_ZLIB 1 + +/* Indicates whether debug messages are shown even in release mode */ +#cmakedefine TRACE_IN_RELEASE 1 + +#cmakedefine TESTS_DIR "@TESTS_DIR@" diff --git a/taglib/doc/README b/taglib/doc/README new file mode 100644 index 0000000..b2ebd36 --- /dev/null +++ b/taglib/doc/README @@ -0,0 +1 @@ +Run "make docs" in the parent directory to generate the TagLib API documentation. diff --git a/taglib/doc/api-footer.html b/taglib/doc/api-footer.html new file mode 100644 index 0000000..9b151ee --- /dev/null +++ b/taglib/doc/api-footer.html @@ -0,0 +1,4 @@ + + + + diff --git a/taglib/doc/api-header.html b/taglib/doc/api-header.html new file mode 100644 index 0000000..ab133c2 --- /dev/null +++ b/taglib/doc/api-header.html @@ -0,0 +1,41 @@ + + + + $title ($projectname) + + + + + +
+ + + + + + +
+ + +
+ + + + + +

TagLib $projectnumber ($title)

+ +
+
+
+ +
diff --git a/taglib/doc/taglib-api.css b/taglib/doc/taglib-api.css new file mode 100644 index 0000000..3fa820e --- /dev/null +++ b/taglib/doc/taglib-api.css @@ -0,0 +1,395 @@ +body { + font-family: sans-serif; + background: white; + color: black; + margin: 0px; + padding: 15px; +} + +a:link { + font-weight: bold; + text-decoration: none; + color: gray; +} + +a:visited { + font-weight: bold; + text-decoration: none; + color: gray; +} + +a:hover { + color: #cccccc; + text-decoration: underline; +} + +a:active { + color: #cccccc; + text-decoration: underline; +} + +img { + border-style: none; +} + +h1 { + font-family: sans-serif; +} + +h2 { + font-family: sans-serif; +} + +h3 { + font-family: sans-serif; +} + +/* container */ + +#container { + position: absolute; + border-width: thin; + border-style: solid; + width: 95%; +} + +/* intro */ + +#intro { + padding: 5px; + margin: 0px; + background: #cccccc; + border-width: medium; + border-style: solid; +} + +#intro h1 { + margin: 5px; + padding: 5px; +} + +/* links */ + +#links { + font-size: x-small; + vertical-align: bottom; +} + +#links a { + border-width: thin; + border-style: dotted; + border-color: white; + /* margin: 0px 10px 0px 0px; */ + margin: 1px; + padding: 3px; + line-height: 230% +} + +#links a:hover { + color: black; + text-decoration: underline; +} + +#links h3 { + outline-width: thin; + border-style: solid; + padding: 2px; + margin: 3px 0px 3px 0px; +} + +/* menu */ + +#menu h3 { + text-align: center; +} + +/* text */ + +#text { + margin: 0px; + padding: 5px 5px 0px 5px; + float: left; +} + +#text h3 { + border-width: thin; + border-style: solid; + padding: 2px; + margin: 3px 0px 3px 0px; +} + +#text li { + margin: 0px 0px 10px 0px; +} + +#text ul { + margin: 5px; + padding: 0px 0px 0px 20px; +} + +#leftcolumn { + float: left; + width: 300px; + margin: 0px 10px 0px 0px; + padding: 0px; +} + +#rightcolumn { + float: right; + width: 210px; + margin: 0px; + padding: 0px; +} + +/* vspacer */ + +.vspacer { + height: 10px; +} + +.silver { + border-width: thin; + border-color: black; + border-style: solid; + background: #cccccc; +} + +a.code { + text-decoration: none; + font-weight: normal; + color: #4444ee +} + +a.codeRef { + font-weight: normal; + color: #4444ee +} + +div.fragment { + width: 98%; + border: 1px solid #CCCCCC; + background-color: #f5f5f5; + padding-left: 4px; + margin: 4px; +} + +div.ah { + background-color: black; + font-weight: bold; color: #ffffff; + margin-bottom: 3px; + margin-top: 3px +} + +#text td { + width: auto; +} + +div.memdoc { + margin-top: 0px; + margin-bottom: 20px; + padding: 10px 10px 10px 40px; +} + +div.memproto { + border: thin solid black; + background-color: #f2f2ff; + width: 100%; + margin-top: 20px; + padding-top: 10px; + padding-bottom: 10px; +} + +td.paramtype { + color: #602020; +} + +table.memname { + font-weight: bold; +} + +div.groupHeader { + margin-left: 16px; + margin-top: 12px; + margin-bottom: 6px; + font-weight: bold +} + +div.groupText { + margin-left: 16px; + font-style: italic; + font-size: smaller +} + +body { + background: white; + color: black; + margin-right: 20px; + margin-left: 20px; +} + +td.indexkey { + background-color: #eeeeff; + font-weight: bold; + padding-right : 10px; + padding-top : 2px; + padding-left : 10px; + padding-bottom : 2px; + margin-left : 0px; + margin-right : 0px; + margin-top : 2px; + margin-bottom : 2px +} + +td.indexvalue { + background-color: #eeeeff; + font-style: italic; + padding-right : 10px; + padding-top : 2px; + padding-left : 10px; + padding-bottom : 2px; + margin-left : 0px; + margin-right : 0px; + margin-top : 2px; + margin-bottom : 2px +} + +tr.memlist { + background-color: #f0f0f0; +} + +p.formulaDsp { + text-align: center; +} + +img.formulaDsp { + +} + +img.formulaInl { + vertical-align: middle; +} + +span.keyword { + color: #008000 +} + +span.keywordtype { + color: #604020 +} + +span.keywordflow { + color: #e08000 +} + +span.comment { + color: #800000 +} + +span.preprocessor { + color: #806020 +} + +span.stringliteral { + color: #002080 +} + +span.charliteral { + color: #008080 +} + +.mdTable { + border: 1px solid #868686; + background-color: #f2f2ff; +} + +.mdRow { + padding: 8px 20px; +} + +.mdescLeft { + font-size: smaller; + font-family: Arial, Helvetica, sans-serif; + background-color: #FAFAFA; + padding-left: 8px; + border-top: 1px none #E0E0E0; + border-right: 1px none #E0E0E0; + border-bottom: 1px none #E0E0E0; + border-left: 1px none #E0E0E0; + margin: 0px; +} + +.mdescRight { + font-size: smaller; + font-family: Arial, Helvetica, sans-serif; + font-style: italic; + background-color: #FAFAFA; + padding-left: 4px; + border-top: 1px none #E0E0E0; + border-right: 1px none #E0E0E0; + border-bottom: 1px none #E0E0E0; + border-left: 1px none #E0E0E0; + margin: 0px; + padding-bottom: 0px; + padding-right: 8px; +} + +.memItemLeft { + padding: 1px 0px 0px 8px; + margin: 4px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-style: solid; + border-top-color: #E0E0E0; + border-right-color: #E0E0E0; + border-bottom-color: #E0E0E0; + border-left-color: #E0E0E0; + border-right-style: none; + border-bottom-style: none; + border-left-style: none; + background-color: #FAFAFA; + font-family: Geneva, Arial, Helvetica, sans-serif; + font-size: 12px; +} + +.memItemRight { + padding: 1px 0px 0px 8px; + margin: 4px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-style: solid; + border-top-color: #E0E0E0; + border-right-color: #E0E0E0; + border-bottom-color: #E0E0E0; + border-left-color: #E0E0E0; + border-right-style: none; + border-bottom-style: none; + border-left-style: none; + background-color: #FAFAFA; + font-family: Geneva, Arial, Helvetica, sans-serif; + font-size: 13px; +} + +.search { + color: #0000ee; + font-weight: bold; +} + +form.search { + margin-bottom: 0px; + margin-top: 0px; +} + +input.search { + font-size: 75%; + color: #000080; + font-weight: normal; + background-color: #eeeeff; +} + +td.tiny { + font-size: 75%; +} diff --git a/taglib/doc/taglib.png b/taglib/doc/taglib.png new file mode 100644 index 0000000000000000000000000000000000000000..2791cc887670ef22450bb7f35686912c8ebf1af3 GIT binary patch literal 2748 zcmeHJ`!^Ge8(;2WY-L&`>?<+KB}AdomW>SC3yDgZEawX$K{m4<5!+inxP@llC zU1rZ5-~s?5U5@7osNJD|;vWb8e;w%B$_m}xeu<33;sJX^M8(7#3n_E~Stj{7*=WNgz zTf6i2SO-TZ9G>8O!PU*(<0A1A>9Uu%kFTG9Kwwbtm8&74Vd0TcH=-#wV`6{16&FuU zNK8sjrKhFe&iMTfBQq;ICpYhI{=LGY;*!$)56W2O6_r&FA63^puC1$o^0c9`srgw; zYg_wsc1LGd_a8mIFZ%iiUJkx~!{H7Mza4r1;UjNs{NEFwCa0!nX6NQVFDx!Ce_2^w z`}SQR{IULXb8GuoK}4+#03eZ#L0S=R@K%a8TnN62-U#cQ zxshDVk#FbxU`Lz@(~J1&+eu_Ot!Zb!u*!iy;VrIT89^4V-MpyV4f}5Z6)S#xz44_nH)ZwLEX1m@h{jx#s^%-I12Uo{L;lYU>S6TgGhdGB z-86#$!MWVF=DePxCFXUv3MhxFgQId@QM>wAZS7>TD&n;AB}hM;sMl-S_gyQtOMwwaMxEZ(SGTen$NAH(Dr;;wTMLm2rm%dEFDtTox@-@yo_8=#PTFHV7~^?_z$R zxYn}=x)!a^d?VBXN`@a&+4V)@HnEI?SU4HG803Max{73rO=A^u6V+|LA$#@%CB=*IUN&JV>%TnPaWua;K8=X) zpSvd-Td5$wDKjt8&kuf+pJ>TL+qB&R?R2y>PS1{tx#f1xCAyl)A(-a{^sG4}pTUpl ztg3d6gXi+mVt)PemeH1eoYGiIzLX?oAlxv0CO2Tt2$3mtQ6CBPvdnoJ@5=iiQW;6p zc>9sOuYQ&Ylk4RZvdLE0c#sqmBw7x|aAJkrfxun-y0rEm@2t7Il^velx+l*`Qw)L2 z)`j*t2l*aEy9oZ)V-~)2jw||+ifBAL3^{cV3xm5QqFn!1=#fWOGe<@0y>#7aOb9Jd z!s*C3e4j~aj#e^~#w3Az7ur%2)4Wbpj*fhVUN6Wxbu4;*IfD{0% zQoa$@Udr-hi3Y79kklmJ=+Y4wsH+lFP@NgTzB48X%f#J#Nl6_S`s?6_dSO6N;J+5P zu^UPj*dEX5-;B6#=a%-SM}qHPr}dZ-ykfMyaF`YuAyW>Vm7?dh$lnh8;Nq`WWR{&b zBBsfxt^F7=KRqz~+RzDK#!s)vj zsatPb7)ztssHyH0VO`s+bIM6K6o8->F0jXseoN3oW9__3hbWIj{JeF=U4}s5lSVi# zw#t+TqrY&FE*YfDoD}>?lf~0)V&KF4hJpzmTg#Ul{||Qvj_p^UPjnwVWj4Ru^F)Xv zLGjvWoABrk%7>zpsxf$<+mDnCq9BWrcdZYyA%PApFWC2Z%3WSOK?QL$I3(B|F3WH? z4WfZQyeVn5U4GqoK{c)PD!)VUOcm3K|6JJ^PhZK+@oaI_KdM+%MOa|b23AVKQInP^ zYGNnIs!l>mJ>D7==NK<_P6bJ}Z}6~cAe#><8Q*GOw&NI@heJfQF$9%LP_DFP-r zcOWM=?B30%IXuiaEYmisp4m}nFn(~G&yB}u3|IbQ*v^zb9NK<*e+SU)oHl8-*H!25p8|uzA|G3m GsDA+=UdN#T literal 0 HcmV?d00001 diff --git a/taglib/examples/CMakeLists.txt b/taglib/examples/CMakeLists.txt new file mode 100644 index 0000000..17d4bd6 --- /dev/null +++ b/taglib/examples/CMakeLists.txt @@ -0,0 +1,50 @@ +if(BUILD_EXAMPLES) +INCLUDE_DIRECTORIES( ${CMAKE_CURRENT_SOURCE_DIR}/../taglib + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/toolkit + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ape + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v1 + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v2 + ${CMAKE_CURRENT_SOURCE_DIR}/../bindings/c/ ) + +if(ENABLE_STATIC) + add_definitions(-DTAGLIB_STATIC) +endif(ENABLE_STATIC) + +########### next target ############### + +ADD_EXECUTABLE(tagreader tagreader.cpp) + +TARGET_LINK_LIBRARIES(tagreader tag ) + + +########### next target ############### + +ADD_EXECUTABLE(tagreader_c tagreader_c.c) + +TARGET_LINK_LIBRARIES(tagreader_c tag_c ) + + +########### next target ############### + +ADD_EXECUTABLE(tagwriter tagwriter.cpp) + +TARGET_LINK_LIBRARIES(tagwriter tag ) + + +########### next target ############### + +ADD_EXECUTABLE(framelist framelist.cpp) + +TARGET_LINK_LIBRARIES(framelist tag ) + + +########### next target ############### + +ADD_EXECUTABLE(strip-id3v1 strip-id3v1.cpp) + +TARGET_LINK_LIBRARIES(strip-id3v1 tag ) + + +endif(BUILD_EXAMPLES) + diff --git a/taglib/examples/framelist.cpp b/taglib/examples/framelist.cpp new file mode 100644 index 0000000..679aa39 --- /dev/null +++ b/taglib/examples/framelist.cpp @@ -0,0 +1,109 @@ +/* Copyright (C) 2003 Scott Wheeler + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include + +#include + +#include +#include +#include + +#include + +#include + +using namespace std; +using namespace TagLib; + +int main(int argc, char *argv[]) +{ + // process the command line args + + + for(int i = 1; i < argc; i++) { + + cout << "******************** \"" << argv[i] << "\"********************" << endl; + + MPEG::File f(argv[i]); + + ID3v2::Tag *id3v2tag = f.ID3v2Tag(); + + if(id3v2tag) { + + cout << "ID3v2." + << id3v2tag->header()->majorVersion() + << "." + << id3v2tag->header()->revisionNumber() + << ", " + << id3v2tag->header()->tagSize() + << " bytes in tag" + << endl; + + ID3v2::FrameList::ConstIterator it = id3v2tag->frameList().begin(); + for(; it != id3v2tag->frameList().end(); it++) + cout << (*it)->frameID() << " - \"" << (*it)->toString() << "\"" << endl; + } + else + cout << "file does not have a valid id3v2 tag" << endl; + + cout << endl << "ID3v1" << endl; + + ID3v1::Tag *id3v1tag = f.ID3v1Tag(); + + if(id3v1tag) { + cout << "title - \"" << id3v1tag->title() << "\"" << endl; + cout << "artist - \"" << id3v1tag->artist() << "\"" << endl; + cout << "album - \"" << id3v1tag->album() << "\"" << endl; + cout << "year - \"" << id3v1tag->year() << "\"" << endl; + cout << "comment - \"" << id3v1tag->comment() << "\"" << endl; + cout << "track - \"" << id3v1tag->track() << "\"" << endl; + cout << "genre - \"" << id3v1tag->genre() << "\"" << endl; + } + else + cout << "file does not have a valid id3v1 tag" << endl; + + APE::Tag *ape = f.APETag(); + + cout << endl << "APE" << endl; + + if(ape) { + for(APE::ItemListMap::ConstIterator it = ape->itemListMap().begin(); + it != ape->itemListMap().end(); ++it) + { + if((*it).second.type() != APE::Item::Binary) + cout << (*it).first << " - \"" << (*it).second.toString() << "\"" << endl; + else + cout << (*it).first << " - Binary data (" << (*it).second.binaryData().size() << " bytes)" << endl; + } + } + else + cout << "file does not have a valid APE tag" << endl; + + cout << endl; + } +} diff --git a/taglib/examples/strip-id3v1.cpp b/taglib/examples/strip-id3v1.cpp new file mode 100644 index 0000000..ab36d71 --- /dev/null +++ b/taglib/examples/strip-id3v1.cpp @@ -0,0 +1,40 @@ +/* Copyright (C) 2003 Scott Wheeler + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +using namespace TagLib; + +int main(int argc, char *argv[]) +{ + for(int i = 1; i < argc; i++) { + + std::cout << "******************** Stripping ID3v1 Tag From: \"" << argv[i] << "\"********************" << std::endl; + + MPEG::File f(argv[i]); + f.strip(MPEG::File::ID3v1); + } +} diff --git a/taglib/examples/tagreader.cpp b/taglib/examples/tagreader.cpp new file mode 100644 index 0000000..cc978eb --- /dev/null +++ b/taglib/examples/tagreader.cpp @@ -0,0 +1,96 @@ +/* Copyright (C) 2003 Scott Wheeler + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include +#include +#include + +using namespace std; + +TagLib::String formatSeconds(int seconds) +{ + char secondsString[3]; + sprintf(secondsString, "%02i", seconds); + return secondsString; +} + +int main(int argc, char *argv[]) +{ + for(int i = 1; i < argc; i++) { + + cout << "******************** \"" << argv[i] << "\" ********************" << endl; + + TagLib::FileRef f(argv[i]); + + if(!f.isNull() && f.tag()) { + + TagLib::Tag *tag = f.tag(); + + cout << "-- TAG (basic) --" << endl; + cout << "title - \"" << tag->title() << "\"" << endl; + cout << "artist - \"" << tag->artist() << "\"" << endl; + cout << "album - \"" << tag->album() << "\"" << endl; + cout << "year - \"" << tag->year() << "\"" << endl; + cout << "comment - \"" << tag->comment() << "\"" << endl; + cout << "track - \"" << tag->track() << "\"" << endl; + cout << "genre - \"" << tag->genre() << "\"" << endl; + + TagLib::PropertyMap tags = f.file()->properties(); + + unsigned int longest = 0; + for(TagLib::PropertyMap::ConstIterator i = tags.begin(); i != tags.end(); ++i) { + if (i->first.size() > longest) { + longest = i->first.size(); + } + } + + cout << "-- TAG (properties) --" << endl; + for(TagLib::PropertyMap::ConstIterator i = tags.begin(); i != tags.end(); ++i) { + for(TagLib::StringList::ConstIterator j = i->second.begin(); j != i->second.end(); ++j) { + cout << left << std::setw(longest) << i->first << " - " << '"' << *j << '"' << endl; + } + } + + } + + if(!f.isNull() && f.audioProperties()) { + + TagLib::AudioProperties *properties = f.audioProperties(); + + int seconds = properties->length() % 60; + int minutes = (properties->length() - seconds) / 60; + + cout << "-- AUDIO --" << endl; + cout << "bitrate - " << properties->bitrate() << endl; + cout << "sample rate - " << properties->sampleRate() << endl; + cout << "channels - " << properties->channels() << endl; + cout << "length - " << minutes << ":" << formatSeconds(seconds) << endl; + } + } + return 0; +} diff --git a/taglib/examples/tagreader_c.c b/taglib/examples/tagreader_c.c new file mode 100644 index 0000000..0436992 --- /dev/null +++ b/taglib/examples/tagreader_c.c @@ -0,0 +1,81 @@ +/* Copyright (C) 2003 Scott Wheeler + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#ifndef FALSE +#define FALSE 0 +#endif + +int main(int argc, char *argv[]) +{ + int i; + int seconds; + int minutes; + TagLib_File *file; + TagLib_Tag *tag; + const TagLib_AudioProperties *properties; + + taglib_set_strings_unicode(FALSE); + + for(i = 1; i < argc; i++) { + printf("******************** \"%s\" ********************\n", argv[i]); + + file = taglib_file_new(argv[i]); + + if(file == NULL) + break; + + tag = taglib_file_tag(file); + properties = taglib_file_audioproperties(file); + + if(tag != NULL) { + printf("-- TAG --\n"); + printf("title - \"%s\"\n", taglib_tag_title(tag)); + printf("artist - \"%s\"\n", taglib_tag_artist(tag)); + printf("album - \"%s\"\n", taglib_tag_album(tag)); + printf("year - \"%i\"\n", taglib_tag_year(tag)); + printf("comment - \"%s\"\n", taglib_tag_comment(tag)); + printf("track - \"%i\"\n", taglib_tag_track(tag)); + printf("genre - \"%s\"\n", taglib_tag_genre(tag)); + } + + if(properties != NULL) { + seconds = taglib_audioproperties_length(properties) % 60; + minutes = (taglib_audioproperties_length(properties) - seconds) / 60; + + printf("-- AUDIO --\n"); + printf("bitrate - %i\n", taglib_audioproperties_bitrate(properties)); + printf("sample rate - %i\n", taglib_audioproperties_samplerate(properties)); + printf("channels - %i\n", taglib_audioproperties_channels(properties)); + printf("length - %i:%02i\n", minutes, seconds); + } + + taglib_tag_free_strings(); + taglib_file_free(file); + } + + return 0; +} diff --git a/taglib/examples/tagwriter.cpp b/taglib/examples/tagwriter.cpp new file mode 100644 index 0000000..63163fb --- /dev/null +++ b/taglib/examples/tagwriter.cpp @@ -0,0 +1,139 @@ +/* Copyright (C) 2004 Scott Wheeler + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace std; + +bool isArgument(const char *s) +{ + return strlen(s) == 2 && s[0] == '-'; +} + +bool isFile(const char *s) +{ + struct stat st; +#ifdef _WIN32 + return ::stat(s, &st) == 0 && (st.st_mode & (S_IFREG)); +#else + return ::stat(s, &st) == 0 && (st.st_mode & (S_IFREG | S_IFLNK)); +#endif +} + +void usage() +{ + cout << endl; + cout << "Usage: tagwriter " << endl; + cout << endl; + cout << "Where the valid fields are:" << endl; + cout << " -t " << endl; + cout << " -a <artist>" << endl; + cout << " -A <album>" << endl; + cout << " -c <comment>" << endl; + cout << " -g <genre>" << endl; + cout << " -y <year>" << endl; + cout << " -T <track>" << endl; + cout << endl; + + exit(1); +} + +int main(int argc, char *argv[]) +{ + TagLib::List<TagLib::FileRef> fileList; + + while(argc > 0 && isFile(argv[argc - 1])) { + + TagLib::FileRef f(argv[argc - 1]); + + if(!f.isNull() && f.tag()) + fileList.append(f); + + argc--; + } + + if(fileList.isEmpty()) + usage(); + + for(int i = 1; i < argc - 1; i += 2) { + + if(isArgument(argv[i]) && i + 1 < argc && !isArgument(argv[i + 1])) { + + char field = argv[i][1]; + TagLib::String value = argv[i + 1]; + + TagLib::List<TagLib::FileRef>::Iterator it; + for(it = fileList.begin(); it != fileList.end(); ++it) { + + TagLib::Tag *t = (*it).tag(); + + switch (field) { + case 't': + t->setTitle(value); + break; + case 'a': + t->setArtist(value); + break; + case 'A': + t->setAlbum(value); + break; + case 'c': + t->setComment(value); + break; + case 'g': + t->setGenre(value); + break; + case 'y': + t->setYear(value.toInt()); + break; + case 'T': + t->setTrack(value.toInt()); + break; + default: + usage(); + break; + } + } + } + else + usage(); + } + + TagLib::List<TagLib::FileRef>::Iterator it; + for(it = fileList.begin(); it != fileList.end(); ++it) + (*it).file()->save(); + + return 0; +} diff --git a/taglib/taglib-config.cmake b/taglib/taglib-config.cmake new file mode 100644 index 0000000..2c2d2db --- /dev/null +++ b/taglib/taglib-config.cmake @@ -0,0 +1,55 @@ +#!/bin/sh + +usage() +{ + echo "usage: $0 [OPTIONS]" +cat << EOH + +options: + [--libs] + [--cflags] + [--version] + [--prefix] +EOH + exit 1; +} + +prefix=${CMAKE_INSTALL_PREFIX} +exec_prefix=${CMAKE_INSTALL_PREFIX} +libdir=${LIB_INSTALL_DIR} +includedir=${INCLUDE_INSTALL_DIR} + +flags="" + +if test $# -eq 0 ; then + usage +fi + +while test $# -gt 0 +do + case $1 in + --libs) + flags="$flags -L$libdir -ltag" + ;; + --cflags) + flags="$flags -I$includedir/taglib" + ;; + --version) + echo ${TAGLIB_LIB_MAJOR_VERSION}.${TAGLIB_LIB_MINOR_VERSION}.${TAGLIB_LIB_PATCH_VERSION} + ;; + --prefix) + echo $prefix + ;; + *) + echo "$0: unknown option $1" + echo + usage + ;; + esac + shift +done + +if test -n "$flags" +then + echo $flags +fi diff --git a/taglib/taglib-config.cmd.cmake b/taglib/taglib-config.cmd.cmake new file mode 100644 index 0000000..3e2b1cd --- /dev/null +++ b/taglib/taglib-config.cmd.cmake @@ -0,0 +1,36 @@ +@echo off +goto beginning + * + * It is what it is, you can do with it as you please. + * + * Just don't blame me if it teaches your computer to smoke! + * + * -Enjoy + * fh :)_~ + * +:beginning +if /i "%1#" == "--libs#" goto doit +if /i "%1#" == "--cflags#" goto doit +if /i "%1#" == "--version#" goto doit +if /i "%1#" == "--prefix#" goto doit + +echo "usage: %0 [OPTIONS]" +echo [--libs] +echo [--cflags] +echo [--version] +echo [--prefix] +goto theend + + * + * NOTE: Windows does not assume libraries are prefixed with 'lib'. + * NOTE: If '-llibtag' is the last element, it is easily appended in the users installation/makefile process + * to allow for static, shared or debug builds. + * It would be preferable if the top level CMakeLists.txt provided the library name during config. ?? +:doit +if /i "%1#" == "--libs#" echo -L${LIB_INSTALL_DIR} -llibtag +if /i "%1#" == "--cflags#" echo -I${INCLUDE_INSTALL_DIR}/taglib +if /i "%1#" == "--version#" echo ${TAGLIB_LIB_MAJOR_VERSION}.${TAGLIB_LIB_MINOR_VERSION}.${TAGLIB_LIB_PATCH_VERSION} +if /i "%1#" == "--prefix#" echo ${CMAKE_INSTALL_PREFIX} + +:theend + diff --git a/taglib/taglib.pc.cmake b/taglib/taglib.pc.cmake new file mode 100644 index 0000000..909b8fc --- /dev/null +++ b/taglib/taglib.pc.cmake @@ -0,0 +1,11 @@ +prefix=${CMAKE_INSTALL_PREFIX} +exec_prefix=${CMAKE_INSTALL_PREFIX} +libdir=${LIB_INSTALL_DIR} +includedir=${INCLUDE_INSTALL_DIR} + +Name: TagLib +Description: Audio meta-data library +Requires: +Version: ${TAGLIB_LIB_MAJOR_VERSION}.${TAGLIB_LIB_MINOR_VERSION}.${TAGLIB_LIB_PATCH_VERSION} +Libs: -L${LIB_INSTALL_DIR} -ltag +Cflags: -I${INCLUDE_INSTALL_DIR}/taglib diff --git a/taglib/taglib/CMakeLists.txt b/taglib/taglib/CMakeLists.txt new file mode 100644 index 0000000..23fa60d --- /dev/null +++ b/taglib/taglib/CMakeLists.txt @@ -0,0 +1,335 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/toolkit + ${CMAKE_CURRENT_SOURCE_DIR}/asf + ${CMAKE_CURRENT_SOURCE_DIR}/mpeg + ${CMAKE_CURRENT_SOURCE_DIR}/ogg + ${CMAKE_CURRENT_SOURCE_DIR}/ogg/flac + ${CMAKE_CURRENT_SOURCE_DIR}/flac + ${CMAKE_CURRENT_SOURCE_DIR}/mpc + ${CMAKE_CURRENT_SOURCE_DIR}/mp4 + ${CMAKE_CURRENT_SOURCE_DIR}/ogg/vorbis + ${CMAKE_CURRENT_SOURCE_DIR}/ogg/speex + ${CMAKE_CURRENT_SOURCE_DIR}/ogg/opus + ${CMAKE_CURRENT_SOURCE_DIR}/mpeg/id3v2 + ${CMAKE_CURRENT_SOURCE_DIR}/mpeg/id3v2/frames + ${CMAKE_CURRENT_SOURCE_DIR}/mpeg/id3v1 + ${CMAKE_CURRENT_SOURCE_DIR}/ape + ${CMAKE_CURRENT_SOURCE_DIR}/wavpack + ${CMAKE_CURRENT_SOURCE_DIR}/trueaudio + ${CMAKE_CURRENT_SOURCE_DIR}/riff + ${CMAKE_CURRENT_SOURCE_DIR}/riff/aiff + ${CMAKE_CURRENT_SOURCE_DIR}/riff/wav + ${CMAKE_CURRENT_SOURCE_DIR}/mod + ${CMAKE_CURRENT_SOURCE_DIR}/s3m + ${CMAKE_CURRENT_SOURCE_DIR}/it + ${CMAKE_CURRENT_SOURCE_DIR}/xm +) + +if(ZLIB_FOUND) + include_directories(${ZLIB_INCLUDE_DIR}) +endif() + +set(tag_HDRS + tag.h + fileref.h + audioproperties.h + taglib_export.h + ${CMAKE_BINARY_DIR}/taglib_config.h + toolkit/taglib.h + toolkit/tstring.h + toolkit/tlist.h + toolkit/tlist.tcc + toolkit/tstringlist.h + toolkit/tbytevector.h + toolkit/tbytevectorlist.h + toolkit/tbytevectorstream.h + toolkit/tiostream.h + toolkit/tfile.h + toolkit/tfilestream.h + toolkit/tmap.h + toolkit/tmap.tcc + toolkit/tpropertymap.h + toolkit/trefcounter.h + toolkit/tdebuglistener.h + mpeg/mpegfile.h + mpeg/mpegproperties.h + mpeg/mpegheader.h + mpeg/xingheader.h + mpeg/id3v1/id3v1tag.h + mpeg/id3v1/id3v1genres.h + mpeg/id3v2/id3v2extendedheader.h + mpeg/id3v2/id3v2frame.h + mpeg/id3v2/id3v2header.h + mpeg/id3v2/id3v2synchdata.h + mpeg/id3v2/id3v2footer.h + mpeg/id3v2/id3v2framefactory.h + mpeg/id3v2/id3v2tag.h + mpeg/id3v2/frames/attachedpictureframe.h + mpeg/id3v2/frames/commentsframe.h + mpeg/id3v2/frames/generalencapsulatedobjectframe.h + mpeg/id3v2/frames/ownershipframe.h + mpeg/id3v2/frames/popularimeterframe.h + mpeg/id3v2/frames/privateframe.h + mpeg/id3v2/frames/relativevolumeframe.h + mpeg/id3v2/frames/textidentificationframe.h + mpeg/id3v2/frames/uniquefileidentifierframe.h + mpeg/id3v2/frames/unknownframe.h + mpeg/id3v2/frames/unsynchronizedlyricsframe.h + mpeg/id3v2/frames/urllinkframe.h + ogg/oggfile.h + ogg/oggpage.h + ogg/oggpageheader.h + ogg/xiphcomment.h + ogg/vorbis/vorbisfile.h + ogg/vorbis/vorbisproperties.h + ogg/flac/oggflacfile.h + ogg/speex/speexfile.h + ogg/speex/speexproperties.h + ogg/opus/opusfile.h + ogg/opus/opusproperties.h + flac/flacfile.h + flac/flacpicture.h + flac/flacproperties.h + flac/flacmetadatablock.h + ape/apefile.h + ape/apeproperties.h + ape/apetag.h + ape/apefooter.h + ape/apeitem.h + mpc/mpcfile.h + mpc/mpcproperties.h + wavpack/wavpackfile.h + wavpack/wavpackproperties.h + trueaudio/trueaudiofile.h + trueaudio/trueaudioproperties.h + riff/rifffile.h + riff/aiff/aifffile.h + riff/aiff/aiffproperties.h + riff/wav/wavfile.h + riff/wav/wavproperties.h + riff/wav/infotag.h + asf/asffile.h + asf/asfproperties.h + asf/asftag.h + asf/asfattribute.h + asf/asfpicture.h + mp4/mp4file.h + mp4/mp4atom.h + mp4/mp4tag.h + mp4/mp4item.h + mp4/mp4properties.h + mp4/mp4coverart.h + mod/modfilebase.h + mod/modfile.h + mod/modtag.h + mod/modproperties.h + it/itfile.h + it/itproperties.h + s3m/s3mfile.h + s3m/s3mproperties.h + xm/xmfile.h + xm/xmproperties.h +) + +set(mpeg_SRCS + mpeg/mpegfile.cpp + mpeg/mpegproperties.cpp + mpeg/mpegheader.cpp + mpeg/xingheader.cpp +) + +set(id3v1_SRCS + mpeg/id3v1/id3v1tag.cpp + mpeg/id3v1/id3v1genres.cpp +) + +set(id3v2_SRCS + mpeg/id3v2/id3v2framefactory.cpp + mpeg/id3v2/id3v2synchdata.cpp + mpeg/id3v2/id3v2tag.cpp + mpeg/id3v2/id3v2header.cpp + mpeg/id3v2/id3v2frame.cpp + mpeg/id3v2/id3v2footer.cpp + mpeg/id3v2/id3v2extendedheader.cpp + ) + +set(frames_SRCS + mpeg/id3v2/frames/attachedpictureframe.cpp + mpeg/id3v2/frames/commentsframe.cpp + mpeg/id3v2/frames/generalencapsulatedobjectframe.cpp + mpeg/id3v2/frames/ownershipframe.cpp + mpeg/id3v2/frames/popularimeterframe.cpp + mpeg/id3v2/frames/privateframe.cpp + mpeg/id3v2/frames/relativevolumeframe.cpp + mpeg/id3v2/frames/textidentificationframe.cpp + mpeg/id3v2/frames/uniquefileidentifierframe.cpp + mpeg/id3v2/frames/unknownframe.cpp + mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp + mpeg/id3v2/frames/urllinkframe.cpp +) + +set(ogg_SRCS + ogg/oggfile.cpp + ogg/oggpage.cpp + ogg/oggpageheader.cpp + ogg/xiphcomment.cpp +) + +set(vorbis_SRCS + ogg/vorbis/vorbisfile.cpp + ogg/vorbis/vorbisproperties.cpp +) + +set(flacs_SRCS + flac/flacfile.cpp + flac/flacpicture.cpp + flac/flacproperties.cpp + flac/flacmetadatablock.cpp + flac/flacunknownmetadatablock.cpp +) + +set(oggflacs_SRCS + ogg/flac/oggflacfile.cpp +) + +set(mpc_SRCS + mpc/mpcfile.cpp + mpc/mpcproperties.cpp +) + +set(mp4_SRCS + mp4/mp4file.cpp + mp4/mp4atom.cpp + mp4/mp4tag.cpp + mp4/mp4item.cpp + mp4/mp4properties.cpp + mp4/mp4coverart.cpp +) + +set(ape_SRCS + ape/apetag.cpp + ape/apefooter.cpp + ape/apeitem.cpp + ape/apefile.cpp + ape/apeproperties.cpp +) + +set(wavpack_SRCS + wavpack/wavpackfile.cpp + wavpack/wavpackproperties.cpp +) + +set(speex_SRCS + ogg/speex/speexfile.cpp + ogg/speex/speexproperties.cpp +) + +set(opus_SRCS + ogg/opus/opusfile.cpp + ogg/opus/opusproperties.cpp +) + +set(trueaudio_SRCS + trueaudio/trueaudiofile.cpp + trueaudio/trueaudioproperties.cpp +) + +set(asf_SRCS + asf/asftag.cpp + asf/asffile.cpp + asf/asfproperties.cpp + asf/asfattribute.cpp + asf/asfpicture.cpp +) + +set(riff_SRCS + riff/rifffile.cpp +) + +set(aiff_SRCS + riff/aiff/aifffile.cpp + riff/aiff/aiffproperties.cpp +) + +set(wav_SRCS + riff/wav/wavfile.cpp + riff/wav/wavproperties.cpp + riff/wav/infotag.cpp +) + +set(mod_SRCS + mod/modfilebase.cpp + mod/modfile.cpp + mod/modtag.cpp + mod/modproperties.cpp +) + +set(s3m_SRCS + s3m/s3mfile.cpp + s3m/s3mproperties.cpp +) + +set(it_SRCS + it/itfile.cpp + it/itproperties.cpp +) + +set(xm_SRCS + xm/xmfile.cpp + xm/xmproperties.cpp +) + +set(toolkit_SRCS + toolkit/tstring.cpp + toolkit/tstringlist.cpp + toolkit/tbytevector.cpp + toolkit/tbytevectorlist.cpp + toolkit/tbytevectorstream.cpp + toolkit/tiostream.cpp + toolkit/tfile.cpp + toolkit/tfilestream.cpp + toolkit/tdebug.cpp + toolkit/tpropertymap.cpp + toolkit/trefcounter.cpp + toolkit/tdebuglistener.cpp + toolkit/unicode.cpp +) + +set(tag_LIB_SRCS + ${mpeg_SRCS} ${id3v1_SRCS} ${id3v2_SRCS} ${frames_SRCS} ${ogg_SRCS} + ${vorbis_SRCS} ${oggflacs_SRCS} ${mpc_SRCS} ${ape_SRCS} ${toolkit_SRCS} ${flacs_SRCS} + ${wavpack_SRCS} ${speex_SRCS} ${trueaudio_SRCS} ${riff_SRCS} ${aiff_SRCS} ${wav_SRCS} + ${asf_SRCS} ${mp4_SRCS} ${mod_SRCS} ${s3m_SRCS} ${it_SRCS} ${xm_SRCS} ${opus_SRCS} + tag.cpp + tagunion.cpp + fileref.cpp + audioproperties.cpp +) + +add_library(tag ${tag_LIB_SRCS} ${tag_HDRS}) + +if(ZLIB_FOUND) + target_link_libraries(tag ${ZLIB_LIBRARIES}) +endif() + +set_target_properties(tag PROPERTIES + VERSION ${TAGLIB_SOVERSION_MAJOR}.${TAGLIB_SOVERSION_MINOR}.${TAGLIB_SOVERSION_PATCH} + SOVERSION ${TAGLIB_SOVERSION_MAJOR} + INSTALL_NAME_DIR ${LIB_INSTALL_DIR} + DEFINE_SYMBOL MAKE_TAGLIB_LIB + LINK_INTERFACE_LIBRARIES "" + PUBLIC_HEADER "${tag_HDRS}" +) +if(BUILD_FRAMEWORK) + set_target_properties(tag PROPERTIES FRAMEWORK TRUE) +endif() + +install(TARGETS tag + FRAMEWORK DESTINATION ${FRAMEWORK_INSTALL_DIR} + LIBRARY DESTINATION ${LIB_INSTALL_DIR} + RUNTIME DESTINATION ${BIN_INSTALL_DIR} + ARCHIVE DESTINATION ${LIB_INSTALL_DIR} + PUBLIC_HEADER DESTINATION ${INCLUDE_INSTALL_DIR}/taglib +) + diff --git a/taglib/taglib/ape/ape-tag-format.txt b/taglib/taglib/ape/ape-tag-format.txt new file mode 100644 index 0000000..21ff1c8 --- /dev/null +++ b/taglib/taglib/ape/ape-tag-format.txt @@ -0,0 +1,170 @@ +================================================================================ += APE Tag Specification, Version 2.000 +================================================================================ + +Original Content (C) 2004, Frank Klemm <frank.klemm@elster.offl.uni-jena.de> +Formatting / Editing (C) 2004, Scott Wheeler <wheeler@kde.org> + +================================================================================ += Contents +================================================================================ + +1 - APE Tag General Structure +2 - APE Tag Header / Footer Format +3 - APE Tag Flags +4 - APE Tag Item Format +5 - APE Tag Item Supported Keys +6 - APE Tag Item Content +7 - Data Types +7.1 - Data Types / UTF-8 +7.2 - Data Types / Dates +7.3 - Data Types / Timestamps + +================================================================================ += 1 - APE Tag General Structure +================================================================================ + +Member of Basic Components of SV8 Stream Note: + +It is strongly recommended that the data size be stored in the tags. The size +should normally be in the roughly one kilobyte, never more than 8 kilobytes. + +Larger data should be stored externally using link entries. Linked data is much +easier to process by normal programs, so for instance JPEG data should not be +included inside the audio file. + +APE Tag Version 2.000 (with header, recommended): + +/================================\ +| APE Tag Header | 32 bytes | +|-------------------|------------| +| APE Tag Item 1 | > 10 bytes | +| APE Tag Item 2 | > 10 bytes | +| APE Tag Item n-1 | > 10 bytes | +| APE Tag Item n | > 10 bytes | +|-------------------|------------| +| APE Tag Footer | 32 bytes | +\================================/ + + +APE tag items should be sorted ascending by size. When streaming, parts of the +APE tag may be dropped to reduce the danger of drop outs between tracks. This +is not required, but is strongly recommended. It would be desirable for the i +tems to be sorted by importance / size, but this is not feasible. This +convention should only be broken when adding less important small items and it +is not desirable to rewrite the entire tag. An APE tag at the end of a file +(the recommended location) must have at least a footer; an APE tag at the +beginning of a file (strongly discouraged) must have at least a header. + +APE Tag Version 1.000 (without header, deprecated) + +/================================\ +| APE Tag Item 1 | > 10 bytes | +| APE Tag Item 2 | > 10 bytes | +| APE Tag Item n-1 | > 10 bytes | +| APE Tag Item n | > 10 bytes | +|-------------------|------------| +| APE Tag Footer | 32 bytes | +\================================/ + +================================================================================ += 2 - APE Tag Header / Footer Format +================================================================================ + +Contains number, length and attributes of all tag items + +Header and Footer are different in 1 bit in the Tags Flags to distinguish +between them. + +Member of APE Tag 2.0 + +/===========================================================================\ +| Preamble | 8 bytes | { 'A', 'P', 'E', 'T', 'A', 'G', 'E', 'X' } | +|----------------|---------|------------------------------------------------| +| Version Number | 4 bytes | 1000 = Version 1.000, 2000 = Version 2.000 | +|----------------|---------|------------------------------------------------| +| Tag Size | 4 bytes | Tag size in bytes including footer and all tag | +| | | items excluding the header (for 1.000 | +| | | compatibility) | +|----------------|---------|------------------------------------------------| +| Item Count | 4 bytes | Number of items in the tag | +|----------------|---------|------------------------------------------------| +| Tag Flags | 4 bytes | Global flags | +|----------------|---------|------------------------------------------------| +| Reserved | 8 bytes | Must be zeroed | +\===========================================================================/ + +================================================================================ += 3 - APE Tag Flags +================================================================================ + +The general flag structure for either items or headers / footers is the same. +Bits 31, 30 and 29 are specific to headers / footers, whereas 2 through 0 are +item specific. + +Note: APE Tags from Version 1.0 do not use any of the following. All flags in +that version are zeroed and ignored when reading. + +/=================================================================\ +| Contains Header | Bit 31 | 1 - has header | 0 - no header | +|-----------------|-------------|---------------------------------| +| Contains Footer | Bit 30 | 1 - has footer | 0 - no footer | +|-----------------|-------------|---------------------------------| +| Is Header | Bit 29 | 1 - is header | 0 - is footer | +|-----------------|-------------|---------------------------------| +| Undefined | Bits 28 - 3 | Undefined, must be zeroed | +|-----------------|-------------|---------------------------------| +| Encoding | Bits 2 - 1 | 00 - UTF-8 | +| | | 01 - Binary Data * | +| | | 10 - External Reference ** | +| | | 11 - Reserved | +|-----------------|-------------|---------------------------------| +| Read Only | Bit 0 | 1 - read only | 0 - read/write | +\=================================================================/ + + (*) Should be ignored by tools for editing text values +(**) Allowed external reference formats: + - http://host/directory/filename.ext + - ftp://host/directory/filename.ext + - filename.ext + - /directory/filename.ext + - DRIVE:/directory/filename.ext + + Note: External references are also UTF-8 encoded. + +================================================================================ += 4 - APE Tag Item Format +================================================================================ + +APE Tag Items are stored as key-value pairs. APE Tags Item Key are case +sensitive, however it is illegal to use keys which only differ in case and +it is recommended that tag reading not be case sensitive. + +Every key can only occur (at most) once. It is not possible to repeat a key +to signify updated contents. + +Tags can be partially or completely repeated in the streaming format. This +makes it possible to display an artist and / or title if it was missed at the +beginning of the stream. It is recommended that the important information like +artist, album and title should occur approximately every 2 minutes in the +stream and again 5 to 10 seconds before the end. However, care should be tak +en not to replicate this information too often or during passages with high +bitrate demands to avoid unnecessary drop-outs. + +/==============================================================================\ +| Content Size | 4 bytes | Length of the value in bytes | +|----------------|---------------|---------------------------------------------| +| Flags | 4 bytes | Item flags | +|----------------|---------------|---------------------------------------------| +| Key | 2 - 255 bytes | Item key | +|----------------|---------------|---------------------------------------------| +| Key Terminator | 1 byte | Null byte that indicates the end of the key | +|----------------|---------------|---------------------------------------------| +| Value | variable | Content (formatted according to the flags) | +\==============================================================================/ + +================================================================================ + +Sections 5 - 7 haven't yet been converted from: + +http://www.personal.uni-jena.de/~pfk/mpp/sv8/apetag.html diff --git a/taglib/taglib/ape/apefile.cpp b/taglib/taglib/ape/apefile.cpp new file mode 100644 index 0000000..3cb9d9a --- /dev/null +++ b/taglib/taglib/ape/apefile.cpp @@ -0,0 +1,314 @@ +/*************************************************************************** + copyright : (C) 2010 by Alex Novichkov + email : novichko@atnet.ru + + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + (original WavPack implementation) + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tstring.h> +#include <tdebug.h> +#include <tagunion.h> +#include <id3v1tag.h> +#include <tpropertymap.h> + +#include "apefile.h" + +#include "apetag.h" +#include "apefooter.h" + +using namespace TagLib; + +namespace +{ + enum { ApeAPEIndex = 0, ApeID3v1Index = 1 }; +} + +class APE::File::FilePrivate +{ +public: + FilePrivate() : + APELocation(-1), + APESize(0), + ID3v1Location(-1), + properties(0), + hasAPE(false), + hasID3v1(false) {} + + ~FilePrivate() + { + delete properties; + } + + long APELocation; + uint APESize; + + long ID3v1Location; + + TagUnion tag; + + Properties *properties; + + // These indicate whether the file *on disk* has these tags, not if + // this data structure does. This is used in computing offsets. + + bool hasAPE; + bool hasID3v1; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +APE::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +APE::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +APE::File::~File() +{ + delete d; +} + +TagLib::Tag *APE::File::tag() const +{ + return &d->tag; +} + +PropertyMap APE::File::properties() const +{ + if(d->hasAPE) + return d->tag.access<APE::Tag>(ApeAPEIndex, false)->properties(); + if(d->hasID3v1) + return d->tag.access<ID3v1::Tag>(ApeID3v1Index, false)->properties(); + return PropertyMap(); +} + +void APE::File::removeUnsupportedProperties(const StringList &properties) +{ + if(d->hasAPE) + d->tag.access<APE::Tag>(ApeAPEIndex, false)->removeUnsupportedProperties(properties); + if(d->hasID3v1) + d->tag.access<ID3v1::Tag>(ApeID3v1Index, false)->removeUnsupportedProperties(properties); +} + +PropertyMap APE::File::setProperties(const PropertyMap &properties) +{ + if(d->hasID3v1) + d->tag.access<ID3v1::Tag>(ApeID3v1Index, false)->setProperties(properties); + return d->tag.access<APE::Tag>(ApeAPEIndex, true)->setProperties(properties); +} + +APE::Properties *APE::File::audioProperties() const +{ + return d->properties; +} + +bool APE::File::save() +{ + if(readOnly()) { + debug("APE::File::save() -- File is read only."); + return false; + } + + // Update ID3v1 tag + + if(ID3v1Tag()) { + if(d->hasID3v1) { + seek(d->ID3v1Location); + writeBlock(ID3v1Tag()->render()); + } + else { + seek(0, End); + d->ID3v1Location = tell(); + writeBlock(ID3v1Tag()->render()); + d->hasID3v1 = true; + } + } + else { + if(d->hasID3v1) { + removeBlock(d->ID3v1Location, 128); + d->hasID3v1 = false; + if(d->hasAPE) { + if(d->APELocation > d->ID3v1Location) + d->APELocation -= 128; + } + } + } + + // Update APE tag + + if(APETag()) { + if(d->hasAPE) + insert(APETag()->render(), d->APELocation, d->APESize); + else { + if(d->hasID3v1) { + insert(APETag()->render(), d->ID3v1Location, 0); + d->APESize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + d->APELocation = d->ID3v1Location; + d->ID3v1Location += d->APESize; + } + else { + seek(0, End); + d->APELocation = tell(); + writeBlock(APETag()->render()); + d->APESize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + } + } + } + else { + if(d->hasAPE) { + removeBlock(d->APELocation, d->APESize); + d->hasAPE = false; + if(d->hasID3v1) { + if(d->ID3v1Location > d->APELocation) { + d->ID3v1Location -= d->APESize; + } + } + } + } + + return true; +} + +ID3v1::Tag *APE::File::ID3v1Tag(bool create) +{ + return d->tag.access<ID3v1::Tag>(ApeID3v1Index, create); +} + +APE::Tag *APE::File::APETag(bool create) +{ + return d->tag.access<APE::Tag>(ApeAPEIndex, create); +} + +void APE::File::strip(int tags) +{ + if(tags & ID3v1) { + d->tag.set(ApeID3v1Index, 0); + APETag(true); + } + + if(tags & APE) { + d->tag.set(ApeAPEIndex, 0); + + if(!ID3v1Tag()) + APETag(true); + } +} + +bool APE::File::hasAPETag() const +{ + return d->hasAPE; +} + +bool APE::File::hasID3v1Tag() const +{ + return d->hasID3v1; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void APE::File::read(bool readProperties, Properties::ReadStyle /* propertiesStyle */) +{ + // Look for an ID3v1 tag + + d->ID3v1Location = findID3v1(); + + if(d->ID3v1Location >= 0) { + d->tag.set(ApeID3v1Index, new ID3v1::Tag(this, d->ID3v1Location)); + d->hasID3v1 = true; + } + + // Look for an APE tag + + d->APELocation = findAPE(); + + if(d->APELocation >= 0) { + d->tag.set(ApeAPEIndex, new APE::Tag(this, d->APELocation)); + d->APESize = APETag()->footer()->completeTagSize(); + d->APELocation = d->APELocation + APETag()->footer()->size() - d->APESize; + d->hasAPE = true; + } + + if(!d->hasID3v1) + APETag(true); + + // Look for APE audio properties + + if(readProperties) { + d->properties = new Properties(this); + } +} + +long APE::File::findAPE() +{ + if(!isValid()) + return -1; + + if(d->hasID3v1) + seek(-160, End); + else + seek(-32, End); + + long p = tell(); + + if(readBlock(8) == APE::Tag::fileIdentifier()) + return p; + + return -1; +} + +long APE::File::findID3v1() +{ + if(!isValid()) + return -1; + + seek(-128, End); + long p = tell(); + + if(readBlock(3) == ID3v1::Tag::fileIdentifier()) + return p; + + return -1; +} diff --git a/taglib/taglib/ape/apefile.h b/taglib/taglib/ape/apefile.h new file mode 100644 index 0000000..f7b509f --- /dev/null +++ b/taglib/taglib/ape/apefile.h @@ -0,0 +1,229 @@ +/*************************************************************************** + copyright : (C) 2010 by Alex Novichkov + email : novichko@atnet.ru + + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + (original WavPack implementation) + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_APEFILE_H +#define TAGLIB_APEFILE_H + +#include "tfile.h" +#include "taglib_export.h" +#include "apeproperties.h" + +namespace TagLib { + + class Tag; + + namespace ID3v1 { class Tag; } + namespace APE { class Tag; } + + //! An implementation of APE metadata + + /*! + * This is implementation of APE metadata. + * + * This supports ID3v1 and APE (v1 and v2) style comments as well as reading stream + * properties from the file. + */ + + namespace APE { + + //! An implementation of TagLib::File with APE specific methods + + /*! + * This implements and provides an interface for APE files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to APE files. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * This set of flags is used for various operations and is suitable for + * being OR-ed together. + */ + enum TagTypes { + //! Empty set. Matches no tag types. + NoTags = 0x0000, + //! Matches ID3v1 tags. + ID3v1 = 0x0001, + //! Matches APE tags. + APE = 0x0002, + //! Matches all tag types. + AllTags = 0xffff + }; + + /*! + * Constructs an APE file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an APE file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the Tag for this file. This will be an APE tag, an ID3v1 tag + * or a combination of the two. + */ + virtual TagLib::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * If the file contains both an APE and an ID3v1 tag, only APE + * will be converted to the PropertyMap. + */ + PropertyMap properties() const; + + /*! + * Removes unsupported properties. Forwards to the actual Tag's + * removeUnsupportedProperties() function. + */ + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * Creates an APEv2 tag if necessary. A pontentially existing ID3v1 + * tag will be updated as well. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the APE::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + /*! + * Saves the file. + * + * \note According to the official Monkey's Audio SDK, an APE file + * can only have either ID3V1 or APE tags, so a parameter is used here. + */ + virtual bool save(); + + /*! + * Returns a pointer to the ID3v1 tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid ID3v1 tag. If \a create is true it will create + * an ID3v1 tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v1 tag. Use hasID3v1Tag() to check if the file + * on disk actually has an ID3v1 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v1Tag() + */ + ID3v1::Tag *ID3v1Tag(bool create = false); + + /*! + * Returns a pointer to the APE tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid APE tag. If \a create is true it will create + * an APE tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an APE tag. Use hasAPETag() to check if the file + * on disk actually has an APE tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasAPETag() + */ + APE::Tag *APETag(bool create = false); + + /*! + * This will remove the tags that match the OR-ed together TagTypes from the + * file. By default it removes all tags. + * + * \note This will also invalidate pointers to the tags + * as their memory will be freed. + * \note In order to make the removal permanent save() still needs to be called + */ + void strip(int tags = AllTags); + + /*! + * Returns whether or not the file on disk actually has an APE tag. + * + * \see APETag() + */ + bool hasAPETag() const; + + /*! + * Returns whether or not the file on disk actually has an ID3v1 tag. + * + * \see ID3v1Tag() + */ + bool hasID3v1Tag() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + void scan(); + long findID3v1(); + long findAPE(); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/ape/apefooter.cpp b/taglib/taglib/ape/apefooter.cpp new file mode 100644 index 0000000..539832b --- /dev/null +++ b/taglib/taglib/ape/apefooter.cpp @@ -0,0 +1,236 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + (C) 2002 - 2008 by Scott Wheeler (id3v2header.cpp) + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <iostream> +#include <bitset> + +#include <tstring.h> +#include <tdebug.h> + +#include "apefooter.h" + +using namespace TagLib; +using namespace APE; + +class APE::Footer::FooterPrivate +{ +public: + FooterPrivate() : version(0), + footerPresent(true), + headerPresent(false), + isHeader(false), + itemCount(0), + tagSize(0) {} + + ~FooterPrivate() {} + + uint version; + + bool footerPresent; + bool headerPresent; + + bool isHeader; + + uint itemCount; + uint tagSize; + + static const uint size = 32; +}; + +//////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +TagLib::uint APE::Footer::size() +{ + return FooterPrivate::size; +} + +ByteVector APE::Footer::fileIdentifier() +{ + return ByteVector::fromCString("APETAGEX"); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +APE::Footer::Footer() +{ + d = new FooterPrivate; +} + +APE::Footer::Footer(const ByteVector &data) +{ + d = new FooterPrivate; + parse(data); +} + +APE::Footer::~Footer() +{ + delete d; +} + +TagLib::uint APE::Footer::version() const +{ + return d->version; +} + +bool APE::Footer::headerPresent() const +{ + return d->headerPresent; +} + +bool APE::Footer::footerPresent() const +{ + return d->footerPresent; +} + +bool APE::Footer::isHeader() const +{ + return d->isHeader; +} + +void APE::Footer::setHeaderPresent(bool b) const +{ + d->headerPresent = b; +} + +TagLib::uint APE::Footer::itemCount() const +{ + return d->itemCount; +} + +void APE::Footer::setItemCount(uint s) +{ + d->itemCount = s; +} + +TagLib::uint APE::Footer::tagSize() const +{ + return d->tagSize; +} + +TagLib::uint APE::Footer::completeTagSize() const +{ + if(d->headerPresent) + return d->tagSize + d->size; + else + return d->tagSize; +} + +void APE::Footer::setTagSize(uint s) +{ + d->tagSize = s; +} + +void APE::Footer::setData(const ByteVector &data) +{ + parse(data); +} + +ByteVector APE::Footer::renderFooter() const +{ + return render(false); +} + +ByteVector APE::Footer::renderHeader() const +{ + if (!d->headerPresent) return ByteVector(); + + return render(true); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void APE::Footer::parse(const ByteVector &data) +{ + if(data.size() < size()) + return; + + // The first eight bytes, data[0..7], are the File Identifier, "APETAGEX". + + // Read the version number + + d->version = data.toUInt(8, false); + + // Read the tag size + + d->tagSize = data.toUInt(12, false); + + // Read the item count + + d->itemCount = data.toUInt(16, false); + + // Read the flags + + std::bitset<32> flags(TAGLIB_CONSTRUCT_BITSET(data.toUInt(20, false))); + + d->headerPresent = flags[31]; + d->footerPresent = !flags[30]; + d->isHeader = flags[29]; + +} + +ByteVector APE::Footer::render(bool isHeader) const +{ + ByteVector v; + + // add the file identifier -- "APETAGEX" + + v.append(fileIdentifier()); + + // add the version number -- we always render a 2.000 tag regardless of what + // the tag originally was. + + v.append(ByteVector::fromUInt(2000, false)); + + // add the tag size + + v.append(ByteVector::fromUInt(d->tagSize, false)); + + // add the item count + + v.append(ByteVector::fromUInt(d->itemCount, false)); + + // render and add the flags + + std::bitset<32> flags; + + flags[31] = d->headerPresent; + flags[30] = false; // footer is always present + flags[29] = isHeader; + + v.append(ByteVector::fromUInt(flags.to_ulong(), false)); + + // add the reserved 64bit + + v.append(ByteVector::fromLongLong(0)); + + return v; +} diff --git a/taglib/taglib/ape/apefooter.h b/taglib/taglib/ape/apefooter.h new file mode 100644 index 0000000..080f930 --- /dev/null +++ b/taglib/taglib/ape/apefooter.h @@ -0,0 +1,173 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_APEFOOTER_H +#define TAGLIB_APEFOOTER_H + +#include "tbytevector.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace APE { + + //! An implementation of APE footers + + /*! + * This class implements APE footers (and headers). It attempts to follow, both + * semantically and programatically, the structure specified in + * the APE v2.0 standard. The API is based on the properties of APE footer and + * headers specified there. + */ + + class TAGLIB_EXPORT Footer + { + public: + /*! + * Constructs an empty APE footer. + */ + Footer(); + + /*! + * Constructs an APE footer based on \a data. parse() is called + * immediately. + */ + Footer(const ByteVector &data); + + /*! + * Destroys the footer. + */ + virtual ~Footer(); + + /*! + * Returns the version number. (Note: This is the 1000 or 2000.) + */ + uint version() const; + + /*! + * Returns true if a header is present in the tag. + */ + bool headerPresent() const; + + /*! + * Returns true if a footer is present in the tag. + */ + bool footerPresent() const; + + /*! + * Returns true this is actually the header. + */ + bool isHeader() const; + + /*! + * Sets whether the header should be rendered or not + */ + void setHeaderPresent(bool b) const; + + /*! + * Returns the number of items in the tag. + */ + uint itemCount() const; + + /*! + * Set the item count to \a s. + * \see itemCount() + */ + void setItemCount(uint s); + + /*! + * Returns the tag size in bytes. This is the size of the frame content and footer. + * The size of the \e entire tag will be this plus the header size, if present. + * + * \see completeTagSize() + */ + uint tagSize() const; + + /*! + * Returns the tag size, including if present, the header + * size. + * + * \see tagSize() + */ + uint completeTagSize() const; + + /*! + * Set the tag size to \a s. + * \see tagSize() + */ + void setTagSize(uint s); + + /*! + * Returns the size of the footer. Presently this is always 32 bytes. + */ + static uint size(); + + /*! + * Returns the string used to identify an APE tag inside of a file. + * Presently this is always "APETAGEX". + */ + static ByteVector fileIdentifier(); + + /*! + * Sets the data that will be used as the footer. 32 bytes, + * starting from \a data will be used. + */ + void setData(const ByteVector &data); + + /*! + * Renders the footer back to binary format. + */ + ByteVector renderFooter() const; + + /*! + * Renders the header corresponding to the footer. If headerPresent is + * set to false, it returns an empty ByteVector. + */ + ByteVector renderHeader() const; + + protected: + /*! + * Called by setData() to parse the footer data. It makes this information + * available through the public API. + */ + void parse(const ByteVector &data); + + /*! + * Called by renderFooter and renderHeader + */ + ByteVector render(bool isHeader) const; + + private: + Footer(const Footer &); + Footer &operator=(const Footer &); + + class FooterPrivate; + FooterPrivate *d; + }; + + } +} + +#endif diff --git a/taglib/taglib/ape/apeitem.cpp b/taglib/taglib/ape/apeitem.cpp new file mode 100644 index 0000000..3490173 --- /dev/null +++ b/taglib/taglib/ape/apeitem.cpp @@ -0,0 +1,286 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevectorlist.h> +#include <tdebug.h> + +#include "apeitem.h" + +using namespace TagLib; +using namespace APE; + +class APE::Item::ItemPrivate +{ +public: + ItemPrivate() : type(Text), readOnly(false) {} + + Item::ItemTypes type; + String key; + ByteVector value; + StringList text; + bool readOnly; +}; + +APE::Item::Item() +{ + d = new ItemPrivate; +} + +APE::Item::Item(const String &key, const String &value) +{ + d = new ItemPrivate; + d->key = key; + d->text.append(value); +} + +APE::Item::Item(const String &key, const StringList &values) +{ + d = new ItemPrivate; + d->key = key; + d->text = values; +} + +APE::Item::Item(const String &key, const ByteVector &value, bool binary) +{ + d = new ItemPrivate; + d->key = key; + if(binary) { + d->type = Binary; + d->value = value; + } + else + d->text.append(value); +} + +APE::Item::Item(const Item &item) +{ + d = new ItemPrivate(*item.d); +} + +APE::Item::~Item() +{ + delete d; +} + +Item &APE::Item::operator=(const Item &item) +{ + delete d; + d = new ItemPrivate(*item.d); + return *this; +} + +void APE::Item::setReadOnly(bool readOnly) +{ + d->readOnly = readOnly; +} + +bool APE::Item::isReadOnly() const +{ + return d->readOnly; +} + +void APE::Item::setType(APE::Item::ItemTypes val) +{ + d->type = val; +} + +APE::Item::ItemTypes APE::Item::type() const +{ + return d->type; +} + +String APE::Item::key() const +{ + return d->key; +} + +ByteVector APE::Item::binaryData() const +{ + return d->value; +} + +void APE::Item::setBinaryData(const ByteVector &value) +{ + d->type = Binary; + d->value = value; + d->text.clear(); +} + +ByteVector APE::Item::value() const +{ + // This seems incorrect as it won't be actually rendering the value to keep it + // up to date. + + return d->value; +} + +void APE::Item::setKey(const String &key) +{ + d->key = key; +} + +void APE::Item::setValue(const String &value) +{ + d->type = Text; + d->text = value; + d->value.clear(); +} + +void APE::Item::setValues(const StringList &value) +{ + d->type = Text; + d->text = value; + d->value.clear(); +} + +void APE::Item::appendValue(const String &value) +{ + d->type = Text; + d->text.append(value); + d->value.clear(); +} + +void APE::Item::appendValues(const StringList &values) +{ + d->type = Text; + d->text.append(values); + d->value.clear(); +} + +int APE::Item::size() const +{ + // SFB: Why is d->key.size() used when size() returns the length in UniChars and not UTF-8? + int result = 8 + d->key.size() /* d->key.data(String::UTF8).size() */ + 1; + switch (d->type) { + case Text: + if(d->text.size()) { + StringList::ConstIterator it = d->text.begin(); + + result += it->data(String::UTF8).size(); + it++; + for(; it != d->text.end(); ++it) + result += 1 + it->data(String::UTF8).size(); + } + break; + + case Binary: + case Locator: + result += d->value.size(); + break; + } + return result; +} + +StringList APE::Item::toStringList() const +{ + return d->text; +} + +StringList APE::Item::values() const +{ + return d->text; +} + +String APE::Item::toString() const +{ + if(d->type == Text && !isEmpty()) + return d->text.front(); + else + return String::null; +} + +bool APE::Item::isEmpty() const +{ + switch(d->type) { + case Text: + if(d->text.isEmpty()) + return true; + if(d->text.size() == 1 && d->text.front().isEmpty()) + return true; + return false; + case Binary: + case Locator: + return d->value.isEmpty(); + default: + return false; + } +} + +void APE::Item::parse(const ByteVector &data) +{ + // 11 bytes is the minimum size for an APE item + + if(data.size() < 11) { + debug("APE::Item::parse() -- no data in item"); + return; + } + + const uint valueLength = data.toUInt(0, false); + const uint flags = data.toUInt(4, false); + + d->key = String(data.mid(8), String::UTF8); + + const ByteVector value = data.mid(8 + d->key.size() + 1, valueLength); + + setReadOnly(flags & 1); + setType(ItemTypes((flags >> 1) & 3)); + + if(Text == d->type) + d->text = StringList(ByteVectorList::split(value, '\0'), String::UTF8); + else + d->value = value; +} + +ByteVector APE::Item::render() const +{ + ByteVector data; + TagLib::uint flags = ((d->readOnly) ? 1 : 0) | (d->type << 1); + ByteVector value; + + if(isEmpty()) + return data; + + if(d->type == Text) { + StringList::ConstIterator it = d->text.begin(); + + value.append(it->data(String::UTF8)); + it++; + for(; it != d->text.end(); ++it) { + value.append('\0'); + value.append(it->data(String::UTF8)); + } + d->value = value; + } + else + value.append(d->value); + + data.append(ByteVector::fromUInt(value.size(), false)); + data.append(ByteVector::fromUInt(flags, false)); + data.append(d->key.data(String::UTF8)); + data.append(ByteVector('\0')); + data.append(value); + + return data; +} diff --git a/taglib/taglib/ape/apeitem.h b/taglib/taglib/ape/apeitem.h new file mode 100644 index 0000000..0588d18 --- /dev/null +++ b/taglib/taglib/ape/apeitem.h @@ -0,0 +1,219 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_APEITEM_H +#define TAGLIB_APEITEM_H + +#include "tbytevector.h" +#include "tstring.h" +#include "tstringlist.h" + +namespace TagLib { + + namespace APE { + + //! An implementation of APE-items + + /*! + * This class provides the features of items in the APEv2 standard. + */ + class TAGLIB_EXPORT Item + { + public: + /*! + * Enum of types an Item can have. The value of 3 is reserved. + */ + enum ItemTypes { + //! Item contains text information coded in UTF-8 + Text = 0, + //! Item contains binary information + Binary = 1, + //! Item is a locator of external stored information + Locator = 2 + }; + /*! + * Constructs an empty item. + */ + Item(); + + /*! + * Constructs a text item with \a key and \a value. + */ + // BIC: Remove this, StringList has a constructor from a single string + Item(const String &key, const String &value); + + /*! + * Constructs a text item with \a key and \a values. + */ + Item(const String &key, const StringList &values); + + /*! + * Constructs an item with \a key and \a value. + * If \a binary is true a Binary item will be created, otherwise \a value will be interpreted as text + */ + Item(const String &key, const ByteVector &value, bool binary); + + /*! + * Construct an item as a copy of \a item. + */ + Item(const Item &item); + + /*! + * Destroys the item. + */ + virtual ~Item(); + + /*! + * Copies the contents of \a item into this item. + */ + Item &operator=(const Item &item); + + /*! + * Returns the key. + */ + String key() const; + + /*! + * Returns the binary value. + * If the item type is not \a Binary, always returns an empty ByteVector. + */ + ByteVector binaryData() const; + + /*! + * Set the binary value to \a value + * The item's type will also be set to \a Binary + */ + void setBinaryData(const ByteVector &value); + +#ifndef DO_NOT_DOCUMENT + /* Remove in next binary incompatible release */ + ByteVector value() const; +#endif + + /*! + * Sets the key for the item to \a key. + */ + void setKey(const String &key); + + /*! + * Sets the text value of the item to \a value and clears any previous contents. + * + * \see toString() + */ + void setValue(const String &value); + + /*! + * Sets the text value of the item to the list of values in \a value and clears + * any previous contents. + * + * \see toStringList() + */ + void setValues(const StringList &values); + + /*! + * Appends \a value to create (or extend) the current list of text values. + * + * \see toString() + */ + void appendValue(const String &value); + + /*! + * Appends \a values to extend the current list of text values. + * + * \see toStringList() + */ + void appendValues(const StringList &values); + + /*! + * Returns the size of the full item. + */ + int size() const; + + /*! + * Returns the value as a single string. In case of multiple strings, + * the first is returned. If the data type is not \a Text, always returns + * an empty String. + */ + String toString() const; + +#ifndef DO_NOT_DOCUMENT + /* Remove in next binary incompatible release */ + StringList toStringList() const; +#endif + + /*! + * Returns the list of text values. If the data type is not \a Text, always + * returns an empty StringList. + */ + StringList values() const; + + /*! + * Render the item to a ByteVector. + */ + ByteVector render() const; + + /*! + * Parse the item from the ByteVector \a data. + */ + void parse(const ByteVector& data); + + /*! + * Set the item to read-only. + */ + void setReadOnly(bool readOnly); + + /*! + * Return true if the item is read-only. + */ + bool isReadOnly() const; + + /*! + * Sets the type of the item to \a type. + * + * \see ItemTypes + */ + void setType(ItemTypes type); + + /*! + * Returns the type of the item. + */ + ItemTypes type() const; + + /*! + * Returns if the item has any real content. + */ + bool isEmpty() const; + + private: + class ItemPrivate; + ItemPrivate *d; + }; + } + +} + +#endif + + diff --git a/taglib/taglib/ape/apeproperties.cpp b/taglib/taglib/ape/apeproperties.cpp new file mode 100644 index 0000000..4940ede --- /dev/null +++ b/taglib/taglib/ape/apeproperties.cpp @@ -0,0 +1,232 @@ +/*************************************************************************** + copyright : (C) 2010 by Alex Novichkov + email : novichko@atnet.ru + + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + (original WavPack implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> +#include <bitset> +#include "id3v2tag.h" +#include "apeproperties.h" +#include "apefile.h" + +using namespace TagLib; + +class APE::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(File *file, long streamLength) : + length(0), + bitrate(0), + sampleRate(0), + channels(0), + version(0), + bitsPerSample(0), + sampleFrames(0), + file(file), + streamLength(streamLength) {} + + int length; + int bitrate; + int sampleRate; + int channels; + int version; + int bitsPerSample; + uint sampleFrames; + File *file; + long streamLength; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +APE::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(file, file->length()); + read(); +} + +APE::Properties::~Properties() +{ + delete d; +} + +int APE::Properties::length() const +{ + return d->length; +} + +int APE::Properties::bitrate() const +{ + return d->bitrate; +} + +int APE::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int APE::Properties::channels() const +{ + return d->channels; +} + +int APE::Properties::version() const +{ + return d->version; +} + +int APE::Properties::bitsPerSample() const +{ + return d->bitsPerSample; +} + +TagLib::uint APE::Properties::sampleFrames() const +{ + return d->sampleFrames; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + + +void APE::Properties::read() +{ + // First we are searching the descriptor + long offset = findDescriptor(); + if(offset < 0) + return; + + // Then we read the header common for all versions of APE + d->file->seek(offset); + ByteVector commonHeader = d->file->readBlock(6); + if(!commonHeader.startsWith("MAC ")) + return; + d->version = commonHeader.toUShort(4, false); + + if(d->version >= 3980) { + analyzeCurrent(); + } + else { + analyzeOld(); + } +} + +long APE::Properties::findDescriptor() +{ + long ID3v2Location = findID3v2(); + long ID3v2OriginalSize = 0; + bool hasID3v2 = false; + if(ID3v2Location >= 0) { + ID3v2::Tag tag(d->file, ID3v2Location); + ID3v2OriginalSize = tag.header()->completeTagSize(); + if(tag.header()->tagSize() > 0) + hasID3v2 = true; + } + + long offset = 0; + if(hasID3v2) + offset = d->file->find("MAC ", ID3v2Location + ID3v2OriginalSize); + else + offset = d->file->find("MAC "); + + if(offset < 0) { + debug("APE::Properties::findDescriptor() -- APE descriptor not found"); + return -1; + } + + return offset; +} + +long APE::Properties::findID3v2() +{ + if(!d->file->isValid()) + return -1; + + d->file->seek(0); + + if(d->file->readBlock(3) == ID3v2::Header::fileIdentifier()) + return 0; + + return -1; +} + +void APE::Properties::analyzeCurrent() +{ + // Read the descriptor + d->file->seek(2, File::Current); + ByteVector descriptor = d->file->readBlock(44); + const uint descriptorBytes = descriptor.toUInt(0, false); + + if ((descriptorBytes - 52) > 0) + d->file->seek(descriptorBytes - 52, File::Current); + + // Read the header + ByteVector header = d->file->readBlock(24); + + // Get the APE info + d->channels = header.toShort(18, false); + d->sampleRate = header.toUInt(20, false); + d->bitsPerSample = header.toShort(16, false); + //d->compressionLevel = + + const uint totalFrames = header.toUInt(12, false); + const uint blocksPerFrame = header.toUInt(4, false); + const uint finalFrameBlocks = header.toUInt(8, false); + d->sampleFrames = totalFrames > 0 ? (totalFrames - 1) * blocksPerFrame + finalFrameBlocks : 0; + d->length = d->sampleRate > 0 ? d->sampleFrames / d->sampleRate : 0; + d->bitrate = d->length > 0 ? ((d->streamLength * 8L) / d->length) / 1000 : 0; +} + +void APE::Properties::analyzeOld() +{ + ByteVector header = d->file->readBlock(26); + const uint totalFrames = header.toUInt(18, false); + + // Fail on 0 length APE files (catches non-finalized APE files) + if(totalFrames == 0) + return; + + const short compressionLevel = header.toShort(0, false); + uint blocksPerFrame; + if(d->version >= 3950) + blocksPerFrame = 73728 * 4; + else if(d->version >= 3900 || (d->version >= 3800 && compressionLevel == 4000)) + blocksPerFrame = 73728; + else + blocksPerFrame = 9216; + d->channels = header.toShort(4, false); + d->sampleRate = header.toUInt(6, false); + const uint finalFrameBlocks = header.toUInt(22, false); + const uint totalBlocks + = totalFrames > 0 ? (totalFrames - 1) * blocksPerFrame + finalFrameBlocks : 0; + d->length = totalBlocks / d->sampleRate; + d->bitrate = d->length > 0 ? ((d->streamLength * 8L) / d->length) / 1000 : 0; +} + diff --git a/taglib/taglib/ape/apeproperties.h b/taglib/taglib/ape/apeproperties.h new file mode 100644 index 0000000..f154ec3 --- /dev/null +++ b/taglib/taglib/ape/apeproperties.h @@ -0,0 +1,99 @@ +/*************************************************************************** + copyright : (C) 2010 by Alex Novichkov + email : novichko@atnet.ru + + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + (original WavPack implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_APEPROPERTIES_H +#define TAGLIB_APEPROPERTIES_H + +#include "taglib_export.h" +#include "audioproperties.h" + +namespace TagLib { + + namespace APE { + + class File; + + //! An implementation of audio property reading for APE + + /*! + * This reads the data from an APE stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of APE::Properties with the data read from the + * ByteVector \a data. + */ + Properties(File *f, ReadStyle style = Average); + + /*! + * Destroys this APE::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + /*! + * Returns number of bits per sample. + */ + int bitsPerSample() const; + uint sampleFrames() const; + + /*! + * Returns APE version. + */ + int version() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(); + + long findDescriptor(); + long findID3v2(); + + void analyzeCurrent(); + void analyzeOld(); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/ape/apetag.cpp b/taglib/taglib/ape/apetag.cpp new file mode 100644 index 0000000..664ee3a --- /dev/null +++ b/taglib/taglib/ape/apetag.cpp @@ -0,0 +1,383 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifdef __SUNPRO_CC +// Sun Studio finds multiple specializations of Map because +// it considers specializations with and without class types +// to be different; this define forces Map to use only the +// specialization with the class keyword. +#define WANT_CLASS_INSTANTIATION_OF_MAP (1) +#endif + +#include <tfile.h> +#include <tstring.h> +#include <tmap.h> +#include <tpropertymap.h> + +#include "apetag.h" +#include "apefooter.h" +#include "apeitem.h" + +using namespace TagLib; +using namespace APE; + +class APE::Tag::TagPrivate +{ +public: + TagPrivate() : file(0), footerLocation(-1), tagLength(0) {} + + TagLib::File *file; + long footerLocation; + long tagLength; + + Footer footer; + + ItemListMap itemListMap; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +APE::Tag::Tag() : TagLib::Tag() +{ + d = new TagPrivate; +} + +APE::Tag::Tag(TagLib::File *file, long footerLocation) : TagLib::Tag() +{ + d = new TagPrivate; + d->file = file; + d->footerLocation = footerLocation; + + read(); +} + +APE::Tag::~Tag() +{ + delete d; +} + +ByteVector APE::Tag::fileIdentifier() +{ + return ByteVector::fromCString("APETAGEX"); +} + +String APE::Tag::title() const +{ + if(d->itemListMap["TITLE"].isEmpty()) + return String::null; + return d->itemListMap["TITLE"].toString(); +} + +String APE::Tag::artist() const +{ + if(d->itemListMap["ARTIST"].isEmpty()) + return String::null; + return d->itemListMap["ARTIST"].toString(); +} + +String APE::Tag::album() const +{ + if(d->itemListMap["ALBUM"].isEmpty()) + return String::null; + return d->itemListMap["ALBUM"].toString(); +} + +String APE::Tag::comment() const +{ + if(d->itemListMap["COMMENT"].isEmpty()) + return String::null; + return d->itemListMap["COMMENT"].toString(); +} + +String APE::Tag::genre() const +{ + if(d->itemListMap["GENRE"].isEmpty()) + return String::null; + return d->itemListMap["GENRE"].toString(); +} + +TagLib::uint APE::Tag::year() const +{ + if(d->itemListMap["YEAR"].isEmpty()) + return 0; + return d->itemListMap["YEAR"].toString().toInt(); +} + +TagLib::uint APE::Tag::track() const +{ + if(d->itemListMap["TRACK"].isEmpty()) + return 0; + return d->itemListMap["TRACK"].toString().toInt(); +} + +void APE::Tag::setTitle(const String &s) +{ + addValue("TITLE", s, true); +} + +void APE::Tag::setArtist(const String &s) +{ + addValue("ARTIST", s, true); +} + +void APE::Tag::setAlbum(const String &s) +{ + addValue("ALBUM", s, true); +} + +void APE::Tag::setComment(const String &s) +{ + addValue("COMMENT", s, true); +} + +void APE::Tag::setGenre(const String &s) +{ + addValue("GENRE", s, true); +} + +void APE::Tag::setYear(uint i) +{ + if(i <= 0) + removeItem("YEAR"); + else + addValue("YEAR", String::number(i), true); +} + +void APE::Tag::setTrack(uint i) +{ + if(i <= 0) + removeItem("TRACK"); + else + addValue("TRACK", String::number(i), true); +} + +// conversions of tag keys between what we use in PropertyMap and what's usual +// for APE tags +static const TagLib::uint keyConversionsSize = 5; //usual, APE +static const char *keyConversions[][2] = {{"TRACKNUMBER", "TRACK" }, + {"DATE", "YEAR" }, + {"ALBUMARTIST", "ALBUM ARTIST"}, + {"DISCNUMBER", "DISC" }, + {"REMIXER", "MIXARTIST" }}; + +PropertyMap APE::Tag::properties() const +{ + PropertyMap properties; + ItemListMap::ConstIterator it = itemListMap().begin(); + for(; it != itemListMap().end(); ++it) { + String tagName = it->first.upper(); + // if the item is Binary or Locator, or if the key is an invalid string, + // add to unsupportedData + if(it->second.type() != Item::Text || tagName.isNull()) + properties.unsupportedData().append(it->first); + else { + // Some tags need to be handled specially + for(uint i = 0; i < keyConversionsSize; ++i) + if(tagName == keyConversions[i][1]) + tagName = keyConversions[i][0]; + properties[tagName].append(it->second.toStringList()); + } + } + return properties; +} + +void APE::Tag::removeUnsupportedProperties(const StringList &properties) +{ + StringList::ConstIterator it = properties.begin(); + for(; it != properties.end(); ++it) + removeItem(*it); +} + +PropertyMap APE::Tag::setProperties(const PropertyMap &origProps) +{ + PropertyMap properties(origProps); // make a local copy that can be modified + + // see comment in properties() + for(uint i = 0; i < keyConversionsSize; ++i) + if(properties.contains(keyConversions[i][0])) { + properties.insert(keyConversions[i][1], properties[keyConversions[i][0]]); + properties.erase(keyConversions[i][0]); + } + + // first check if tags need to be removed completely + StringList toRemove; + ItemListMap::ConstIterator remIt = itemListMap().begin(); + for(; remIt != itemListMap().end(); ++remIt) { + String key = remIt->first.upper(); + // only remove if a) key is valid, b) type is text, c) key not contained in new properties + if(!key.isNull() && remIt->second.type() == APE::Item::Text && !properties.contains(key)) + toRemove.append(remIt->first); + } + + for (StringList::Iterator removeIt = toRemove.begin(); removeIt != toRemove.end(); removeIt++) + removeItem(*removeIt); + + // now sync in the "forward direction" + PropertyMap::ConstIterator it = properties.begin(); + PropertyMap invalid; + for(; it != properties.end(); ++it) { + const String &tagName = it->first; + if(!checkKey(tagName)) + invalid.insert(it->first, it->second); + else if(!(itemListMap().contains(tagName)) || !(itemListMap()[tagName].values() == it->second)) { + if(it->second.size() == 0) + removeItem(tagName); + else { + StringList::ConstIterator valueIt = it->second.begin(); + addValue(tagName, *valueIt, true); + ++valueIt; + for(; valueIt != it->second.end(); ++valueIt) + addValue(tagName, *valueIt, false); + } + } + } + return invalid; +} + +bool APE::Tag::checkKey(const String &key) +{ + if(key.size() < 2 || key.size() > 16) + return false; + for(String::ConstIterator it = key.begin(); it != key.end(); it++) + // only allow printable ASCII including space (32..127) + if (*it < 32 || *it >= 128) + return false; + String upperKey = key.upper(); + if (upperKey=="ID3" || upperKey=="TAG" || upperKey=="OGGS" || upperKey=="MP+") + return false; + return true; +} + +APE::Footer *APE::Tag::footer() const +{ + return &d->footer; +} + +const APE::ItemListMap& APE::Tag::itemListMap() const +{ + return d->itemListMap; +} + +void APE::Tag::removeItem(const String &key) +{ + Map<const String, Item>::Iterator it = d->itemListMap.find(key.upper()); + if(it != d->itemListMap.end()) + d->itemListMap.erase(it); +} + +void APE::Tag::addValue(const String &key, const String &value, bool replace) +{ + if(replace) + removeItem(key); + if(!key.isEmpty() && !value.isEmpty()) { + if(!replace && d->itemListMap.contains(key)) { + // Text items may contain more than one value + if(APE::Item::Text == d->itemListMap.begin()->second.type()) + d->itemListMap[key.upper()].appendValue(value); + // Binary or locator items may have only one value + else + setItem(key, Item(key, value)); + } + else + setItem(key, Item(key, value)); + } +} + +void APE::Tag::setData(const String &key, const ByteVector &value) +{ + removeItem(key); + if(!key.isEmpty() && !value.isEmpty()) + setItem(key, Item(key, value, true)); +} + +void APE::Tag::setItem(const String &key, const Item &item) +{ + if(!key.isEmpty()) + d->itemListMap.insert(key.upper(), item); +} + +bool APE::Tag::isEmpty() const +{ + return d->itemListMap.isEmpty(); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected methods +//////////////////////////////////////////////////////////////////////////////// + +void APE::Tag::read() +{ + if(d->file && d->file->isValid()) { + + d->file->seek(d->footerLocation); + d->footer.setData(d->file->readBlock(Footer::size())); + + if(d->footer.tagSize() <= Footer::size() || + d->footer.tagSize() > uint(d->file->length())) + return; + + d->file->seek(d->footerLocation + Footer::size() - d->footer.tagSize()); + parse(d->file->readBlock(d->footer.tagSize() - Footer::size())); + } +} + +ByteVector APE::Tag::render() const +{ + ByteVector data; + uint itemCount = 0; + + { + for(Map<const String, Item>::ConstIterator it = d->itemListMap.begin(); + it != d->itemListMap.end(); ++it) + { + data.append(it->second.render()); + itemCount++; + } + } + + d->footer.setItemCount(itemCount); + d->footer.setTagSize(data.size() + Footer::size()); + d->footer.setHeaderPresent(true); + + return d->footer.renderHeader() + data + d->footer.renderFooter(); +} + +void APE::Tag::parse(const ByteVector &data) +{ + uint pos = 0; + + // 11 bytes is the minimum size for an APE item + + for(uint i = 0; i < d->footer.itemCount() && pos <= data.size() - 11; i++) { + APE::Item item; + item.parse(data.mid(pos)); + + d->itemListMap.insert(item.key().upper(), item); + + pos += item.size(); + } +} diff --git a/taglib/taglib/ape/apetag.h b/taglib/taglib/ape/apetag.h new file mode 100644 index 0000000..c1f12e2 --- /dev/null +++ b/taglib/taglib/ape/apetag.h @@ -0,0 +1,208 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_APETAG_H +#define TAGLIB_APETAG_H + +#include "tag.h" +#include "tbytevector.h" +#include "tmap.h" +#include "tstring.h" +#include "taglib_export.h" + +#include "apeitem.h" + +namespace TagLib { + + class File; + + //! An implementation of the APE tagging format + + namespace APE { + + class Footer; + + /*! + * A mapping between a list of item names, or keys, and the associated item. + * + * \see APE::Tag::itemListMap() + */ + typedef Map<const String, Item> ItemListMap; + + + //! An APE tag implementation + + class TAGLIB_EXPORT Tag : public TagLib::Tag + { + public: + /*! + * Create an APE tag with default values. + */ + Tag(); + + /*! + * Create an APE tag and parse the data in \a file with APE footer at + * \a tagOffset. + */ + Tag(TagLib::File *file, long footerLocation); + + /*! + * Destroys this Tag instance. + */ + virtual ~Tag(); + + /*! + * Renders the in memory values to a ByteVector suitable for writing to + * the file. + */ + ByteVector render() const; + + /*! + * Returns the string "APETAGEX" suitable for usage in locating the tag in a + * file. + */ + static ByteVector fileIdentifier(); + + // Reimplementations. + + virtual String title() const; + virtual String artist() const; + virtual String album() const; + virtual String comment() const; + virtual String genre() const; + virtual uint year() const; + virtual uint track() const; + + virtual void setTitle(const String &s); + virtual void setArtist(const String &s); + virtual void setAlbum(const String &s); + virtual void setComment(const String &s); + virtual void setGenre(const String &s); + virtual void setYear(uint i); + virtual void setTrack(uint i); + + /*! + * Implements the unified tag dictionary interface -- export function. + * APE tags are perfectly compatible with the dictionary interface because they + * support both arbitrary tag names and multiple values. Currently only + * APE items of type *Text* are handled by the dictionary interface; all *Binary* + * and *Locator* items will be put into the unsupportedData list and can be + * deleted on request using removeUnsupportedProperties(). The same happens + * to Text items if their key is invalid for PropertyMap (which should actually + * never happen). + * + * The only conversion done by this export function is to rename the APE tags + * TRACK to TRACKNUMBER, YEAR to DATE, and ALBUM ARTIST to ALBUMARTIST, respectively, + * in order to be compliant with the names used in other formats. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified tag dictionary interface -- import function. The same + * comments as for the export function apply; additionally note that the APE tag + * specification requires keys to have between 2 and 16 printable ASCII characters + * with the exception of the fixed strings "ID3", "TAG", "OGGS", and "MP+". + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Check if the given String is a valid APE tag key. + */ + static bool checkKey(const String&); + + /*! + * Returns a pointer to the tag's footer. + */ + Footer *footer() const; + + /*! + * Returns a reference to the item list map. This is an ItemListMap of + * all of the items in the tag. + * + * This is the most powerfull structure for accessing the items of the tag. + * + * APE tags are case-insensitive, all keys in this map have been converted + * to upper case. + * + * \warning You should not modify this data structure directly, instead + * use setItem() and removeItem(). + */ + const ItemListMap &itemListMap() const; + + /*! + * Removes the \a key item from the tag + */ + void removeItem(const String &key); + + /*! + * Adds to the text item specified by \a key the data \a value. If \a replace + * is true, then all of the other values on the same key will be removed + * first. If a binary item exists for \a key it will be removed first. + */ + void addValue(const String &key, const String &value, bool replace = true); + + /*! + * Set the binary data for the key specified by \a item to \a value + * This will convert the item to type \a Binary if it isn't already and + * all of the other values on the same key will be removed. + */ + void setData(const String &key, const ByteVector &value); + + /*! + * Sets the \a key item to the value of \a item. If an item with the \a key is already + * present, it will be replaced. + */ + void setItem(const String &key, const Item &item); + + /*! + * Returns true if the tag does not contain any data. + */ + bool isEmpty() const; + + protected: + + /*! + * Reads from the file specified in the constructor. + */ + void read(); + + /*! + * Parses the body of the tag in \a data. + */ + void parse(const ByteVector &data); + + private: + Tag(const Tag &); + Tag &operator=(const Tag &); + + class TagPrivate; + TagPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/asf/asfattribute.cpp b/taglib/taglib/asf/asfattribute.cpp new file mode 100644 index 0000000..4ee2d0a --- /dev/null +++ b/taglib/taglib/asf/asfattribute.cpp @@ -0,0 +1,350 @@ +/************************************************************************** + copyright : (C) 2005-2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <taglib.h> +#include <tdebug.h> +#include "trefcounter.h" +#include "asfattribute.h" +#include "asffile.h" + +using namespace TagLib; + +class ASF::Attribute::AttributePrivate : public RefCounter +{ +public: + AttributePrivate() + : pictureValue(ASF::Picture::fromInvalid()), + stream(0), + language(0) {} + AttributeTypes type; + String stringValue; + ByteVector byteVectorValue; + ASF::Picture pictureValue; + union { + unsigned int intValue; + unsigned short shortValue; + unsigned long long longLongValue; + bool boolValue; + }; + int stream; + int language; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +ASF::Attribute::Attribute() +{ + d = new AttributePrivate; + d->type = UnicodeType; +} + +ASF::Attribute::Attribute(const ASF::Attribute &other) + : d(other.d) +{ + d->ref(); +} + +ASF::Attribute &ASF::Attribute::operator=(const ASF::Attribute &other) +{ + if(d->deref()) + delete d; + d = other.d; + d->ref(); + return *this; +} + +ASF::Attribute::~Attribute() +{ + if(d->deref()) + delete d; +} + +ASF::Attribute::Attribute(const String &value) +{ + d = new AttributePrivate; + d->type = UnicodeType; + d->stringValue = value; +} + +ASF::Attribute::Attribute(const ByteVector &value) +{ + d = new AttributePrivate; + d->type = BytesType; + d->byteVectorValue = value; +} + +ASF::Attribute::Attribute(const ASF::Picture &value) +{ + d = new AttributePrivate; + d->type = BytesType; + d->pictureValue = value; +} + +ASF::Attribute::Attribute(unsigned int value) +{ + d = new AttributePrivate; + d->type = DWordType; + d->intValue = value; +} + +ASF::Attribute::Attribute(unsigned long long value) +{ + d = new AttributePrivate; + d->type = QWordType; + d->longLongValue = value; +} + +ASF::Attribute::Attribute(unsigned short value) +{ + d = new AttributePrivate; + d->type = WordType; + d->shortValue = value; +} + +ASF::Attribute::Attribute(bool value) +{ + d = new AttributePrivate; + d->type = BoolType; + d->boolValue = value; +} + +ASF::Attribute::AttributeTypes ASF::Attribute::type() const +{ + return d->type; +} + +String ASF::Attribute::toString() const +{ + return d->stringValue; +} + +ByteVector ASF::Attribute::toByteVector() const +{ + if(d->pictureValue.isValid()) + return d->pictureValue.render(); + return d->byteVectorValue; +} + +unsigned short ASF::Attribute::toBool() const +{ + return d->shortValue; +} + +unsigned short ASF::Attribute::toUShort() const +{ + return d->shortValue; +} + +unsigned int ASF::Attribute::toUInt() const +{ + return d->intValue; +} + +unsigned long long ASF::Attribute::toULongLong() const +{ + return d->longLongValue; +} + +ASF::Picture ASF::Attribute::toPicture() const +{ + return d->pictureValue; +} + +String ASF::Attribute::parse(ASF::File &f, int kind) +{ + uint size, nameLength; + String name; + d->pictureValue = Picture::fromInvalid(); + // extended content descriptor + if(kind == 0) { + nameLength = f.readWORD(); + name = f.readString(nameLength); + d->type = ASF::Attribute::AttributeTypes(f.readWORD()); + size = f.readWORD(); + } + // metadata & metadata library + else { + int temp = f.readWORD(); + // metadata library + if(kind == 2) { + d->language = temp; + } + d->stream = f.readWORD(); + nameLength = f.readWORD(); + d->type = ASF::Attribute::AttributeTypes(f.readWORD()); + size = f.readDWORD(); + name = f.readString(nameLength); + } + + if(kind != 2 && size > 65535) { + debug("ASF::Attribute::parse() -- Value larger than 64kB"); + } + + switch(d->type) { + case WordType: + d->shortValue = f.readWORD(); + break; + + case BoolType: + if(kind == 0) { + d->boolValue = f.readDWORD() == 1; + } + else { + d->boolValue = f.readWORD() == 1; + } + break; + + case DWordType: + d->intValue = f.readDWORD(); + break; + + case QWordType: + d->longLongValue = f.readQWORD(); + break; + + case UnicodeType: + d->stringValue = f.readString(size); + break; + + case BytesType: + case GuidType: + d->byteVectorValue = f.readBlock(size); + break; + } + + if(d->type == BytesType && name == "WM/Picture") { + d->pictureValue.parse(d->byteVectorValue); + if(d->pictureValue.isValid()) { + d->byteVectorValue.clear(); + } + } + + return name; +} + +int ASF::Attribute::dataSize() const +{ + switch (d->type) { + case WordType: + return 2; + case BoolType: + return 4; + case DWordType: + return 4; + case QWordType: + return 5; + case UnicodeType: + return d->stringValue.size() * 2 + 2; + case BytesType: + if(d->pictureValue.isValid()) + return d->pictureValue.dataSize(); + case GuidType: + return d->byteVectorValue.size(); + } + return 0; +} + +ByteVector ASF::Attribute::render(const String &name, int kind) const +{ + ByteVector data; + + switch (d->type) { + case WordType: + data.append(ByteVector::fromShort(d->shortValue, false)); + break; + + case BoolType: + if(kind == 0) { + data.append(ByteVector::fromUInt(d->boolValue ? 1 : 0, false)); + } + else { + data.append(ByteVector::fromShort(d->boolValue ? 1 : 0, false)); + } + break; + + case DWordType: + data.append(ByteVector::fromUInt(d->intValue, false)); + break; + + case QWordType: + data.append(ByteVector::fromLongLong(d->longLongValue, false)); + break; + + case UnicodeType: + data.append(File::renderString(d->stringValue)); + break; + + case BytesType: + if(d->pictureValue.isValid()) { + data.append(d->pictureValue.render()); + break; + } + case GuidType: + data.append(d->byteVectorValue); + break; + } + + if(kind == 0) { + data = File::renderString(name, true) + + ByteVector::fromShort((int)d->type, false) + + ByteVector::fromShort(data.size(), false) + + data; + } + else { + ByteVector nameData = File::renderString(name); + data = ByteVector::fromShort(kind == 2 ? d->language : 0, false) + + ByteVector::fromShort(d->stream, false) + + ByteVector::fromShort(nameData.size(), false) + + ByteVector::fromShort((int)d->type, false) + + ByteVector::fromUInt(data.size(), false) + + nameData + + data; + } + + return data; +} + +int ASF::Attribute::language() const +{ + return d->language; +} + +void ASF::Attribute::setLanguage(int value) +{ + d->language = value; +} + +int ASF::Attribute::stream() const +{ + return d->stream; +} + +void ASF::Attribute::setStream(int value) +{ + d->stream = value; +} + diff --git a/taglib/taglib/asf/asfattribute.h b/taglib/taglib/asf/asfattribute.h new file mode 100644 index 0000000..54eb0c7 --- /dev/null +++ b/taglib/taglib/asf/asfattribute.h @@ -0,0 +1,203 @@ +/************************************************************************** + copyright : (C) 2005-2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ASFATTRIBUTE_H +#define TAGLIB_ASFATTRIBUTE_H + +#include "tstring.h" +#include "tbytevector.h" +#include "taglib_export.h" +#include "asfpicture.h" + +namespace TagLib +{ + + namespace ASF + { + + class File; + class Picture; + + class TAGLIB_EXPORT Attribute + { + public: + + /*! + * Enum of types an Attribute can have. + */ + enum AttributeTypes { + UnicodeType = 0, + BytesType = 1, + BoolType = 2, + DWordType = 3, + QWordType = 4, + WordType = 5, + GuidType = 6 + }; + + /*! + * Constructs an empty attribute. + */ + Attribute(); + + /*! + * Constructs an attribute with \a key and a UnicodeType \a value. + */ + Attribute(const String &value); + + /*! + * Constructs an attribute with \a key and a BytesType \a value. + */ + Attribute(const ByteVector &value); + + /*! + * Constructs an attribute with \a key and a Picture \a value. + * + * This attribute is compatible with the ID3 frame, APIC. The ID3 specification for the APIC frame stipulates that, + * while there may be any number of APIC frames associated with a file, + * only one may be of type 1 and only one may be of type 2. + * + * The specification also states that the description of the picture can be no longer than 64 characters, but can be empty. + * WM/Picture attributes added with TagLib::ASF are not automatically validated to conform to ID3 specifications. + * You must add code in your application to perform validations if you want to maintain complete compatibility with ID3. + */ + Attribute(const Picture &value); + + /*! + * Constructs an attribute with \a key and a DWordType \a value. + */ + Attribute(unsigned int value); + + /*! + * Constructs an attribute with \a key and a QWordType \a value. + */ + Attribute(unsigned long long value); + + /*! + * Constructs an attribute with \a key and a WordType \a value. + */ + Attribute(unsigned short value); + + /*! + * Constructs an attribute with \a key and a BoolType \a value. + */ + Attribute(bool value); + + /*! + * Construct an attribute as a copy of \a other. + */ + Attribute(const Attribute &item); + + /*! + * Copies the contents of \a other into this item. + */ + ASF::Attribute &operator=(const Attribute &other); + + /*! + * Destroys the attribute. + */ + virtual ~Attribute(); + + /*! + * Returns type of the value. + */ + AttributeTypes type() const; + + /*! + * Returns the BoolType \a value. + */ + unsigned short toBool() const; + + /*! + * Returns the WordType \a value. + */ + unsigned short toUShort() const; + + /*! + * Returns the DWordType \a value. + */ + unsigned int toUInt() const; + + /*! + * Returns the QWordType \a value. + */ + unsigned long long toULongLong() const; + + /*! + * Returns the UnicodeType \a value. + */ + String toString() const; + + /*! + * Returns the BytesType \a value. + */ + ByteVector toByteVector() const; + + /*! + * Returns the Picture \a value. + */ + Picture toPicture() const; + + /*! + * Returns the language number, or 0 is no stream number was set. + */ + int language() const; + + /*! + * Sets the language number. + */ + void setLanguage(int value); + + /*! + * Returns the stream number, or 0 is no stream number was set. + */ + int stream() const; + + /*! + * Sets the stream number. + */ + void setStream(int value); + +#ifndef DO_NOT_DOCUMENT + /* THIS IS PRIVATE, DON'T TOUCH IT! */ + String parse(ASF::File &file, int kind = 0); +#endif + + //! Returns the size of the stored data + int dataSize() const; + + private: + friend class File; + + ByteVector render(const String &name, int kind = 0) const; + + class AttributePrivate; + AttributePrivate *d; + }; + } + +} + +#endif diff --git a/taglib/taglib/asf/asffile.cpp b/taglib/taglib/asf/asffile.cpp new file mode 100644 index 0000000..241998c --- /dev/null +++ b/taglib/taglib/asf/asffile.cpp @@ -0,0 +1,630 @@ +/************************************************************************** + copyright : (C) 2005-2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tbytevectorlist.h> +#include <tpropertymap.h> +#include <tstring.h> +#include "asffile.h" +#include "asftag.h" +#include "asfproperties.h" + +using namespace TagLib; + +class ASF::File::FilePrivate +{ +public: + FilePrivate(): + size(0), + tag(0), + properties(0), + contentDescriptionObject(0), + extendedContentDescriptionObject(0), + headerExtensionObject(0), + metadataObject(0), + metadataLibraryObject(0) {} + unsigned long long size; + ASF::Tag *tag; + ASF::Properties *properties; + List<ASF::File::BaseObject *> objects; + ASF::File::ContentDescriptionObject *contentDescriptionObject; + ASF::File::ExtendedContentDescriptionObject *extendedContentDescriptionObject; + ASF::File::HeaderExtensionObject *headerExtensionObject; + ASF::File::MetadataObject *metadataObject; + ASF::File::MetadataLibraryObject *metadataLibraryObject; +}; + +static ByteVector headerGuid("\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C", 16); +static ByteVector filePropertiesGuid("\xA1\xDC\xAB\x8C\x47\xA9\xCF\x11\x8E\xE4\x00\xC0\x0C\x20\x53\x65", 16); +static ByteVector streamPropertiesGuid("\x91\x07\xDC\xB7\xB7\xA9\xCF\x11\x8E\xE6\x00\xC0\x0C\x20\x53\x65", 16); +static ByteVector contentDescriptionGuid("\x33\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C", 16); +static ByteVector extendedContentDescriptionGuid("\x40\xA4\xD0\xD2\x07\xE3\xD2\x11\x97\xF0\x00\xA0\xC9\x5E\xA8\x50", 16); +static ByteVector headerExtensionGuid("\xb5\x03\xbf_.\xa9\xcf\x11\x8e\xe3\x00\xc0\x0c Se", 16); +static ByteVector metadataGuid("\xEA\xCB\xF8\xC5\xAF[wH\204g\xAA\214D\xFAL\xCA", 16); +static ByteVector metadataLibraryGuid("\224\034#D\230\224\321I\241A\x1d\x13NEpT", 16); +static ByteVector contentEncryptionGuid("\xFB\xB3\x11\x22\x23\xBD\xD2\x11\xB4\xB7\x00\xA0\xC9\x55\xFC\x6E", 16); +static ByteVector extendedContentEncryptionGuid("\x14\xE6\x8A\x29\x22\x26 \x17\x4C\xB9\x35\xDA\xE0\x7E\xE9\x28\x9C", 16); +static ByteVector advancedContentEncryptionGuid("\xB6\x9B\x07\x7A\xA4\xDA\x12\x4E\xA5\xCA\x91\xD3\x8D\xC1\x1A\x8D", 16); + +class ASF::File::BaseObject +{ +public: + ByteVector data; + virtual ~BaseObject() {} + virtual ByteVector guid() = 0; + virtual void parse(ASF::File *file, unsigned int size); + virtual ByteVector render(ASF::File *file); +}; + +class ASF::File::UnknownObject : public ASF::File::BaseObject +{ + ByteVector myGuid; +public: + UnknownObject(const ByteVector &guid); + ByteVector guid(); +}; + +class ASF::File::FilePropertiesObject : public ASF::File::BaseObject +{ +public: + ByteVector guid(); + void parse(ASF::File *file, uint size); +}; + +class ASF::File::StreamPropertiesObject : public ASF::File::BaseObject +{ +public: + ByteVector guid(); + void parse(ASF::File *file, uint size); +}; + +class ASF::File::ContentDescriptionObject : public ASF::File::BaseObject +{ +public: + ByteVector guid(); + void parse(ASF::File *file, uint size); + ByteVector render(ASF::File *file); +}; + +class ASF::File::ExtendedContentDescriptionObject : public ASF::File::BaseObject +{ +public: + ByteVectorList attributeData; + ByteVector guid(); + void parse(ASF::File *file, uint size); + ByteVector render(ASF::File *file); +}; + +class ASF::File::MetadataObject : public ASF::File::BaseObject +{ +public: + ByteVectorList attributeData; + ByteVector guid(); + void parse(ASF::File *file, uint size); + ByteVector render(ASF::File *file); +}; + +class ASF::File::MetadataLibraryObject : public ASF::File::BaseObject +{ +public: + ByteVectorList attributeData; + ByteVector guid(); + void parse(ASF::File *file, uint size); + ByteVector render(ASF::File *file); +}; + +class ASF::File::HeaderExtensionObject : public ASF::File::BaseObject +{ +public: + List<ASF::File::BaseObject *> objects; + ~HeaderExtensionObject(); + ByteVector guid(); + void parse(ASF::File *file, uint size); + ByteVector render(ASF::File *file); +}; + +ASF::File::HeaderExtensionObject::~HeaderExtensionObject() +{ + for(unsigned int i = 0; i < objects.size(); i++) { + delete objects[i]; + } +} + +void ASF::File::BaseObject::parse(ASF::File *file, unsigned int size) +{ + data.clear(); + if (size > 24 && size <= (unsigned int)(file->length())) + data = file->readBlock(size - 24); + else + data = ByteVector::null; +} + +ByteVector ASF::File::BaseObject::render(ASF::File * /*file*/) +{ + return guid() + ByteVector::fromLongLong(data.size() + 24, false) + data; +} + +ASF::File::UnknownObject::UnknownObject(const ByteVector &guid) : myGuid(guid) +{ +} + +ByteVector ASF::File::UnknownObject::guid() +{ + return myGuid; +} + +ByteVector ASF::File::FilePropertiesObject::guid() +{ + return filePropertiesGuid; +} + +void ASF::File::FilePropertiesObject::parse(ASF::File *file, uint size) +{ + BaseObject::parse(file, size); + file->d->properties->setLength( + (int)(data.toLongLong(40, false) / 10000000L - data.toLongLong(56, false) / 1000L)); +} + +ByteVector ASF::File::StreamPropertiesObject::guid() +{ + return streamPropertiesGuid; +} + +void ASF::File::StreamPropertiesObject::parse(ASF::File *file, uint size) +{ + BaseObject::parse(file, size); + file->d->properties->setChannels(data.toShort(56, false)); + file->d->properties->setSampleRate(data.toUInt(58, false)); + file->d->properties->setBitrate(data.toUInt(62, false) * 8 / 1000); +} + +ByteVector ASF::File::ContentDescriptionObject::guid() +{ + return contentDescriptionGuid; +} + +void ASF::File::ContentDescriptionObject::parse(ASF::File *file, uint /*size*/) +{ + file->d->contentDescriptionObject = this; + int titleLength = file->readWORD(); + int artistLength = file->readWORD(); + int copyrightLength = file->readWORD(); + int commentLength = file->readWORD(); + int ratingLength = file->readWORD(); + file->d->tag->setTitle(file->readString(titleLength)); + file->d->tag->setArtist(file->readString(artistLength)); + file->d->tag->setCopyright(file->readString(copyrightLength)); + file->d->tag->setComment(file->readString(commentLength)); + file->d->tag->setRating(file->readString(ratingLength)); +} + +ByteVector ASF::File::ContentDescriptionObject::render(ASF::File *file) +{ + ByteVector v1 = file->renderString(file->d->tag->title()); + ByteVector v2 = file->renderString(file->d->tag->artist()); + ByteVector v3 = file->renderString(file->d->tag->copyright()); + ByteVector v4 = file->renderString(file->d->tag->comment()); + ByteVector v5 = file->renderString(file->d->tag->rating()); + data.clear(); + data.append(ByteVector::fromShort(v1.size(), false)); + data.append(ByteVector::fromShort(v2.size(), false)); + data.append(ByteVector::fromShort(v3.size(), false)); + data.append(ByteVector::fromShort(v4.size(), false)); + data.append(ByteVector::fromShort(v5.size(), false)); + data.append(v1); + data.append(v2); + data.append(v3); + data.append(v4); + data.append(v5); + return BaseObject::render(file); +} + +ByteVector ASF::File::ExtendedContentDescriptionObject::guid() +{ + return extendedContentDescriptionGuid; +} + +void ASF::File::ExtendedContentDescriptionObject::parse(ASF::File *file, uint /*size*/) +{ + file->d->extendedContentDescriptionObject = this; + int count = file->readWORD(); + while(count--) { + ASF::Attribute attribute; + String name = attribute.parse(*file); + file->d->tag->addAttribute(name, attribute); + } +} + +ByteVector ASF::File::ExtendedContentDescriptionObject::render(ASF::File *file) +{ + data.clear(); + data.append(ByteVector::fromShort(attributeData.size(), false)); + data.append(attributeData.toByteVector(ByteVector::null)); + return BaseObject::render(file); +} + +ByteVector ASF::File::MetadataObject::guid() +{ + return metadataGuid; +} + +void ASF::File::MetadataObject::parse(ASF::File *file, uint /*size*/) +{ + file->d->metadataObject = this; + int count = file->readWORD(); + while(count--) { + ASF::Attribute attribute; + String name = attribute.parse(*file, 1); + file->d->tag->addAttribute(name, attribute); + } +} + +ByteVector ASF::File::MetadataObject::render(ASF::File *file) +{ + data.clear(); + data.append(ByteVector::fromShort(attributeData.size(), false)); + data.append(attributeData.toByteVector(ByteVector::null)); + return BaseObject::render(file); +} + +ByteVector ASF::File::MetadataLibraryObject::guid() +{ + return metadataLibraryGuid; +} + +void ASF::File::MetadataLibraryObject::parse(ASF::File *file, uint /*size*/) +{ + file->d->metadataLibraryObject = this; + int count = file->readWORD(); + while(count--) { + ASF::Attribute attribute; + String name = attribute.parse(*file, 2); + file->d->tag->addAttribute(name, attribute); + } +} + +ByteVector ASF::File::MetadataLibraryObject::render(ASF::File *file) +{ + data.clear(); + data.append(ByteVector::fromShort(attributeData.size(), false)); + data.append(attributeData.toByteVector(ByteVector::null)); + return BaseObject::render(file); +} + +ByteVector ASF::File::HeaderExtensionObject::guid() +{ + return headerExtensionGuid; +} + +void ASF::File::HeaderExtensionObject::parse(ASF::File *file, uint /*size*/) +{ + file->d->headerExtensionObject = this; + file->seek(18, File::Current); + long long dataSize = file->readDWORD(); + long long dataPos = 0; + while(dataPos < dataSize) { + ByteVector guid = file->readBlock(16); + if(guid.size() != 16) { + file->setValid(false); + break; + } + bool ok; + long long size = file->readQWORD(&ok); + if(!ok) { + file->setValid(false); + break; + } + BaseObject *obj; + if(guid == metadataGuid) { + obj = new MetadataObject(); + } + else if(guid == metadataLibraryGuid) { + obj = new MetadataLibraryObject(); + } + else { + obj = new UnknownObject(guid); + } + obj->parse(file, (unsigned int)size); + objects.append(obj); + dataPos += size; + } +} + +ByteVector ASF::File::HeaderExtensionObject::render(ASF::File *file) +{ + data.clear(); + for(unsigned int i = 0; i < objects.size(); i++) { + data.append(objects[i]->render(file)); + } + data = ByteVector("\x11\xD2\xD3\xAB\xBA\xA9\xcf\x11\x8E\xE6\x00\xC0\x0C\x20\x53\x65\x06\x00", 18) + ByteVector::fromUInt(data.size(), false) + data; + return BaseObject::render(file); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +ASF::File::File(FileName file, bool readProperties, Properties::ReadStyle propertiesStyle) + : TagLib::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +ASF::File::File(IOStream *stream, bool readProperties, Properties::ReadStyle propertiesStyle) + : TagLib::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +ASF::File::~File() +{ + for(unsigned int i = 0; i < d->objects.size(); i++) { + delete d->objects[i]; + } + if(d->tag) { + delete d->tag; + } + if(d->properties) { + delete d->properties; + } + delete d; +} + +ASF::Tag *ASF::File::tag() const +{ + return d->tag; +} + +PropertyMap ASF::File::properties() const +{ + return d->tag->properties(); +} + +void ASF::File::removeUnsupportedProperties(const StringList &properties) +{ + d->tag->removeUnsupportedProperties(properties); +} + +PropertyMap ASF::File::setProperties(const PropertyMap &properties) +{ + return d->tag->setProperties(properties); +} + +ASF::Properties *ASF::File::audioProperties() const +{ + return d->properties; +} + +void ASF::File::read(bool /*readProperties*/, Properties::ReadStyle /*propertiesStyle*/) +{ + if(!isValid()) + return; + + ByteVector guid = readBlock(16); + if(guid != headerGuid) { + debug("ASF: Not an ASF file."); + setValid(false); + return; + } + + d->tag = new ASF::Tag(); + d->properties = new ASF::Properties(); + + bool ok; + d->size = readQWORD(&ok); + if(!ok) { + setValid(false); + return; + } + int numObjects = readDWORD(&ok); + if(!ok) { + setValid(false); + return; + } + seek(2, Current); + + for(int i = 0; i < numObjects; i++) { + ByteVector guid = readBlock(16); + if(guid.size() != 16) { + setValid(false); + break; + } + long size = (long)readQWORD(&ok); + if(!ok) { + setValid(false); + break; + } + BaseObject *obj; + if(guid == filePropertiesGuid) { + obj = new FilePropertiesObject(); + } + else if(guid == streamPropertiesGuid) { + obj = new StreamPropertiesObject(); + } + else if(guid == contentDescriptionGuid) { + obj = new ContentDescriptionObject(); + } + else if(guid == extendedContentDescriptionGuid) { + obj = new ExtendedContentDescriptionObject(); + } + else if(guid == headerExtensionGuid) { + obj = new HeaderExtensionObject(); + } + else { + if(guid == contentEncryptionGuid || + guid == extendedContentEncryptionGuid || + guid == advancedContentEncryptionGuid) { + d->properties->setEncrypted(true); + } + obj = new UnknownObject(guid); + } + obj->parse(this, size); + d->objects.append(obj); + } +} + +bool ASF::File::save() +{ + if(readOnly()) { + debug("ASF::File::save() -- File is read only."); + return false; + } + + if(!isValid()) { + debug("ASF::File::save() -- Trying to save invalid file."); + return false; + } + + if(!d->contentDescriptionObject) { + d->contentDescriptionObject = new ContentDescriptionObject(); + d->objects.append(d->contentDescriptionObject); + } + if(!d->extendedContentDescriptionObject) { + d->extendedContentDescriptionObject = new ExtendedContentDescriptionObject(); + d->objects.append(d->extendedContentDescriptionObject); + } + if(!d->headerExtensionObject) { + d->headerExtensionObject = new HeaderExtensionObject(); + d->objects.append(d->headerExtensionObject); + } + if(!d->metadataObject) { + d->metadataObject = new MetadataObject(); + d->headerExtensionObject->objects.append(d->metadataObject); + } + if(!d->metadataLibraryObject) { + d->metadataLibraryObject = new MetadataLibraryObject(); + d->headerExtensionObject->objects.append(d->metadataLibraryObject); + } + + ASF::AttributeListMap::ConstIterator it = d->tag->attributeListMap().begin(); + for(; it != d->tag->attributeListMap().end(); it++) { + const String &name = it->first; + const AttributeList &attributes = it->second; + bool inExtendedContentDescriptionObject = false; + bool inMetadataObject = false; + for(unsigned int j = 0; j < attributes.size(); j++) { + const Attribute &attribute = attributes[j]; + bool largeValue = attribute.dataSize() > 65535; + if(!inExtendedContentDescriptionObject && !largeValue && attribute.language() == 0 && attribute.stream() == 0) { + d->extendedContentDescriptionObject->attributeData.append(attribute.render(name)); + inExtendedContentDescriptionObject = true; + } + else if(!inMetadataObject && !largeValue && attribute.language() == 0 && attribute.stream() != 0) { + d->metadataObject->attributeData.append(attribute.render(name, 1)); + inMetadataObject = true; + } + else { + d->metadataLibraryObject->attributeData.append(attribute.render(name, 2)); + } + } + } + + ByteVector data; + for(unsigned int i = 0; i < d->objects.size(); i++) { + data.append(d->objects[i]->render(this)); + } + data = headerGuid + ByteVector::fromLongLong(data.size() + 30, false) + ByteVector::fromUInt(d->objects.size(), false) + ByteVector("\x01\x02", 2) + data; + insert(data, 0, (TagLib::ulong)d->size); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +int ASF::File::readBYTE(bool *ok) +{ + ByteVector v = readBlock(1); + if(v.size() != 1) { + if(ok) *ok = false; + return 0; + } + if(ok) *ok = true; + return v[0]; +} + +int ASF::File::readWORD(bool *ok) +{ + ByteVector v = readBlock(2); + if(v.size() != 2) { + if(ok) *ok = false; + return 0; + } + if(ok) *ok = true; + return v.toUShort(false); +} + +unsigned int ASF::File::readDWORD(bool *ok) +{ + ByteVector v = readBlock(4); + if(v.size() != 4) { + if(ok) *ok = false; + return 0; + } + if(ok) *ok = true; + return v.toUInt(false); +} + +long long ASF::File::readQWORD(bool *ok) +{ + ByteVector v = readBlock(8); + if(v.size() != 8) { + if(ok) *ok = false; + return 0; + } + if(ok) *ok = true; + return v.toLongLong(false); +} + +String ASF::File::readString(int length) +{ + ByteVector data = readBlock(length); + unsigned int size = data.size(); + while (size >= 2) { + if(data[size - 1] != '\0' || data[size - 2] != '\0') { + break; + } + size -= 2; + } + if(size != data.size()) { + data.resize(size); + } + return String(data, String::UTF16LE); +} + +ByteVector ASF::File::renderString(const String &str, bool includeLength) +{ + ByteVector data = str.data(String::UTF16LE) + ByteVector::fromShort(0, false); + if(includeLength) { + data = ByteVector::fromShort(data.size(), false) + data; + } + return data; +} + diff --git a/taglib/taglib/asf/asffile.h b/taglib/taglib/asf/asffile.h new file mode 100644 index 0000000..5ccf2fd --- /dev/null +++ b/taglib/taglib/asf/asffile.h @@ -0,0 +1,148 @@ +/************************************************************************** + copyright : (C) 2005-2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ASFFILE_H +#define TAGLIB_ASFFILE_H + +#include "tag.h" +#include "tfile.h" +#include "taglib_export.h" +#include "asfproperties.h" +#include "asftag.h" + +namespace TagLib { + + //! An implementation of ASF (WMA) metadata + namespace ASF { + + /*! + * This implements and provides an interface for ASF files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to ASF files. + */ + class TAGLIB_EXPORT File : public TagLib::File + { + public: + + /*! + * Constructs an ASF file from \a file. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an ASF file from \a stream. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns a pointer to the ASF tag of the file. + * + * ASF::Tag implements the tag interface, so this serves as the + * reimplementation of TagLib::File::tag(). + * + * \note The Tag <b>is still</b> owned by the ASF::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + */ + virtual Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + */ + PropertyMap properties() const; + + /*! + * Removes unsupported properties. Forwards to the actual Tag's + * removeUnsupportedProperties() function. + */ + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the ASF audio properties for this file. + */ + virtual Properties *audioProperties() const; + + /*! + * Save the file. + * + * This returns true if the save was successful. + */ + virtual bool save(); + + private: + int readBYTE(bool *ok = 0); + int readWORD(bool *ok = 0); + unsigned int readDWORD(bool *ok = 0); + long long readQWORD(bool *ok = 0); + static ByteVector renderString(const String &str, bool includeLength = false); + String readString(int len); + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + + friend class Attribute; + friend class Picture; + + class BaseObject; + class UnknownObject; + class FilePropertiesObject; + class StreamPropertiesObject; + class ContentDescriptionObject; + class ExtendedContentDescriptionObject; + class HeaderExtensionObject; + class MetadataObject; + class MetadataLibraryObject; + + class FilePrivate; + FilePrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/asf/asfpicture.cpp b/taglib/taglib/asf/asfpicture.cpp new file mode 100644 index 0000000..999f920 --- /dev/null +++ b/taglib/taglib/asf/asfpicture.cpp @@ -0,0 +1,179 @@ +/************************************************************************** + copyright : (C) 2010 by Anton Sergunov + email : setosha@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <taglib.h> +#include <tdebug.h> +#include "trefcounter.h" +#include "asfattribute.h" +#include "asffile.h" +#include "asfpicture.h" + +using namespace TagLib; + +class ASF::Picture::PicturePrivate : public RefCounter +{ +public: + bool valid; + Type type; + String mimeType; + String description; + ByteVector picture; +}; + +//////////////////////////////////////////////////////////////////////////////// +// Picture class members +//////////////////////////////////////////////////////////////////////////////// + +ASF::Picture::Picture() +{ + d = new PicturePrivate(); + d->valid = true; +} + +ASF::Picture::Picture(const Picture& other) + : d(other.d) +{ + d->ref(); +} + +ASF::Picture::~Picture() +{ + if(d->deref()) + delete d; +} + +bool ASF::Picture::isValid() const +{ + return d->valid; +} + +String ASF::Picture::mimeType() const +{ + return d->mimeType; +} + +void ASF::Picture::setMimeType(const String &value) +{ + d->mimeType = value; +} + +ASF::Picture::Type ASF::Picture::type() const +{ + return d->type; +} + +void ASF::Picture::setType(const ASF::Picture::Type& t) +{ + d->type = t; +} + +String ASF::Picture::description() const +{ + return d->description; +} + +void ASF::Picture::setDescription(const String &desc) +{ + d->description = desc; +} + +ByteVector ASF::Picture::picture() const +{ + return d->picture; +} + +void ASF::Picture::setPicture(const ByteVector &p) +{ + d->picture = p; +} + +int ASF::Picture::dataSize() const +{ + return + 9 + (d->mimeType.length() + d->description.length()) * 2 + + d->picture.size(); +} + +ASF::Picture& ASF::Picture::operator=(const ASF::Picture& other) +{ + if(other.d != d) { + if(d->deref()) + delete d; + d = other.d; + d->ref(); + } + return *this; +} + +ByteVector ASF::Picture::render() const +{ + if(!isValid()) + return ByteVector::null; + return + ByteVector((char)d->type) + + ByteVector::fromUInt(d->picture.size(), false) + + ASF::File::renderString(d->mimeType) + + ASF::File::renderString(d->description) + + d->picture; +} + +void ASF::Picture::parse(const ByteVector& bytes) +{ + d->valid = false; + if(bytes.size() < 9) + return; + int pos = 0; + d->type = (Type)bytes[0]; ++pos; + const uint dataLen = bytes.toUInt(pos, false); pos+=4; + + const ByteVector nullStringTerminator(2, 0); + + int endPos = bytes.find(nullStringTerminator, pos, 2); + if(endPos < 0) + return; + d->mimeType = String(bytes.mid(pos, endPos - pos), String::UTF16LE); + pos = endPos+2; + + endPos = bytes.find(nullStringTerminator, pos, 2); + if(endPos < 0) + return; + d->description = String(bytes.mid(pos, endPos - pos), String::UTF16LE); + pos = endPos+2; + + if(dataLen + pos != bytes.size()) + return; + + d->picture = bytes.mid(pos, dataLen); + d->valid = true; + return; +} + +ASF::Picture ASF::Picture::fromInvalid() +{ + Picture ret; + ret.d->valid = false; + return ret; +} + diff --git a/taglib/taglib/asf/asfpicture.h b/taglib/taglib/asf/asfpicture.h new file mode 100644 index 0000000..aa0a060 --- /dev/null +++ b/taglib/taglib/asf/asfpicture.h @@ -0,0 +1,217 @@ +/************************************************************************** + copyright : (C) 2010 by Anton Sergunov + email : setosha@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef ASFPICTURE_H +#define ASFPICTURE_H + +#include "tstring.h" +#include "tbytevector.h" +#include "taglib_export.h" +#include "attachedpictureframe.h" + +namespace TagLib +{ + namespace ASF + { + + //! An ASF attached picture interface implementation + + /*! + * This is an implementation of ASF attached pictures interface. Pictures may be + * included in attributes, one per WM/Picture attribute (but there may be multiple WM/Picture + * attribute in a single tag). These pictures are usually in either JPEG or + * PNG format. + * \see Attribute::toPicture() + * \see Attribute::Attribute(const Picture& picture) + */ + class TAGLIB_EXPORT Picture { + public: + + /*! + * This describes the function or content of the picture. + */ + enum Type { + //! A type not enumerated below + Other = 0x00, + //! 32x32 PNG image that should be used as the file icon + FileIcon = 0x01, + //! File icon of a different size or format + OtherFileIcon = 0x02, + //! Front cover image of the album + FrontCover = 0x03, + //! Back cover image of the album + BackCover = 0x04, + //! Inside leaflet page of the album + LeafletPage = 0x05, + //! Image from the album itself + Media = 0x06, + //! Picture of the lead artist or soloist + LeadArtist = 0x07, + //! Picture of the artist or performer + Artist = 0x08, + //! Picture of the conductor + Conductor = 0x09, + //! Picture of the band or orchestra + Band = 0x0A, + //! Picture of the composer + Composer = 0x0B, + //! Picture of the lyricist or text writer + Lyricist = 0x0C, + //! Picture of the recording location or studio + RecordingLocation = 0x0D, + //! Picture of the artists during recording + DuringRecording = 0x0E, + //! Picture of the artists during performance + DuringPerformance = 0x0F, + //! Picture from a movie or video related to the track + MovieScreenCapture = 0x10, + //! Picture of a large, coloured fish + ColouredFish = 0x11, + //! Illustration related to the track + Illustration = 0x12, + //! Logo of the band or performer + BandLogo = 0x13, + //! Logo of the publisher (record company) + PublisherLogo = 0x14 + }; + + /*! + * Constructs an empty picture. + */ + Picture(); + + /*! + * Construct an picture as a copy of \a other. + */ + Picture(const Picture& other); + + /*! + * Destroys the picture. + */ + virtual ~Picture(); + + /*! + * Copies the contents of \a other into this picture. + */ + Picture& operator=(const Picture& other); + + /*! + * Returns true if Picture stores valid picture + */ + bool isValid() const; + + /*! + * Returns the mime type of the image. This should in most cases be + * "image/png" or "image/jpeg". + * \see setMimeType(const String &) + * \see picture() + * \see setPicture(const ByteArray&) + */ + String mimeType() const; + + /*! + * Sets the mime type of the image. This should in most cases be + * "image/png" or "image/jpeg". + * \see setMimeType(const String &) + * \see picture() + * \see setPicture(const ByteArray&) + */ + void setMimeType(const String &value); + + /*! + * Returns the type of the image. + * + * \see Type + * \see setType() + */ + Type type() const; + + /*! + * Sets the type for the image. + * + * \see Type + * \see type() + */ + void setType(const ASF::Picture::Type& t); + + /*! + * Returns a text description of the image. + * + * \see setDescription() + */ + String description() const; + + /*! + * Sets a textual description of the image to \a desc. + * + * \see description() + */ + void setDescription(const String &desc); + + /*! + * Returns the image data as a ByteVector. + * + * \note ByteVector has a data() method that returns a const char * which + * should make it easy to export this data to external programs. + * + * \see setPicture() + * \see mimeType() + */ + ByteVector picture() const; + + /*! + * Sets the image data to \a p. \a p should be of the type specified in + * this frame's mime-type specification. + * + * \see picture() + * \see mimeType() + * \see setMimeType() + */ + void setPicture(const ByteVector &p); + + /*! + * Returns picture as binary raw data \a value + */ + ByteVector render() const; + + /*! + * Returns picture as binary raw data \a value + */ + int dataSize() const; + +#ifndef DO_NOT_DOCUMENT + /* THIS IS PRIVATE, DON'T TOUCH IT! */ + void parse(const ByteVector& ); + static Picture fromInvalid(); + friend class Attribute; +#endif + private: + class PicturePrivate; + PicturePrivate *d; + }; + } +} + +#endif // ASFPICTURE_H diff --git a/taglib/taglib/asf/asfproperties.cpp b/taglib/taglib/asf/asfproperties.cpp new file mode 100644 index 0000000..b82c131 --- /dev/null +++ b/taglib/taglib/asf/asfproperties.cpp @@ -0,0 +1,111 @@ +/************************************************************************** + copyright : (C) 2005-2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tstring.h> +#include "asfproperties.h" + +using namespace TagLib; + +class ASF::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(): length(0), bitrate(0), sampleRate(0), channels(0), encrypted(false) {} + int length; + int bitrate; + int sampleRate; + int channels; + bool encrypted; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +ASF::Properties::Properties() : AudioProperties(AudioProperties::Average) +{ + d = new PropertiesPrivate; +} + +ASF::Properties::~Properties() +{ + if(d) + delete d; +} + +int ASF::Properties::length() const +{ + return d->length; +} + +int ASF::Properties::bitrate() const +{ + return d->bitrate; +} + +int ASF::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int ASF::Properties::channels() const +{ + return d->channels; +} + +bool ASF::Properties::isEncrypted() const +{ + return d->encrypted; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void ASF::Properties::setLength(int length) +{ + d->length = length; +} + +void ASF::Properties::setBitrate(int length) +{ + d->bitrate = length; +} + +void ASF::Properties::setSampleRate(int length) +{ + d->sampleRate = length; +} + +void ASF::Properties::setChannels(int length) +{ + d->channels = length; +} + +void ASF::Properties::setEncrypted(bool encrypted) +{ + d->encrypted = encrypted; +} + diff --git a/taglib/taglib/asf/asfproperties.h b/taglib/taglib/asf/asfproperties.h new file mode 100644 index 0000000..95730d8 --- /dev/null +++ b/taglib/taglib/asf/asfproperties.h @@ -0,0 +1,76 @@ +/************************************************************************** + copyright : (C) 2005-2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ASFPROPERTIES_H +#define TAGLIB_ASFPROPERTIES_H + +#include "audioproperties.h" +#include "tstring.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ASF { + + //! An implementation of ASF audio properties + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + + /*! + * Create an instance of ASF::Properties. + */ + Properties(); + + /*! + * Destroys this ASF::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + bool isEncrypted() const; + +#ifndef DO_NOT_DOCUMENT + void setLength(int value); + void setBitrate(int value); + void setSampleRate(int value); + void setChannels(int value); + void setEncrypted(bool value); +#endif + + private: + class PropertiesPrivate; + PropertiesPrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/asf/asftag.cpp b/taglib/taglib/asf/asftag.cpp new file mode 100644 index 0000000..c07abe9 --- /dev/null +++ b/taglib/taglib/asf/asftag.cpp @@ -0,0 +1,354 @@ +/************************************************************************** + copyright : (C) 2005-2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tpropertymap.h> +#include "asftag.h" + +using namespace TagLib; + +class ASF::Tag::TagPrivate +{ +public: + String title; + String artist; + String copyright; + String comment; + String rating; + AttributeListMap attributeListMap; +}; + +ASF::Tag::Tag() +: TagLib::Tag() +{ + d = new TagPrivate; +} + +ASF::Tag::~Tag() +{ + if(d) + delete d; +} + +String ASF::Tag::title() const +{ + return d->title; +} + +String ASF::Tag::artist() const +{ + return d->artist; +} + +String ASF::Tag::album() const +{ + if(d->attributeListMap.contains("WM/AlbumTitle")) + return d->attributeListMap["WM/AlbumTitle"][0].toString(); + return String::null; +} + +String ASF::Tag::copyright() const +{ + return d->copyright; +} + +String ASF::Tag::comment() const +{ + return d->comment; +} + +String ASF::Tag::rating() const +{ + return d->rating; +} + +unsigned int ASF::Tag::year() const +{ + if(d->attributeListMap.contains("WM/Year")) + return d->attributeListMap["WM/Year"][0].toString().toInt(); + return 0; +} + +unsigned int ASF::Tag::track() const +{ + if(d->attributeListMap.contains("WM/TrackNumber")) { + const ASF::Attribute attr = d->attributeListMap["WM/TrackNumber"][0]; + if(attr.type() == ASF::Attribute::DWordType) + return attr.toUInt(); + else + return attr.toString().toInt(); + } + if(d->attributeListMap.contains("WM/Track")) + return d->attributeListMap["WM/Track"][0].toUInt(); + return 0; +} + +String ASF::Tag::genre() const +{ + if(d->attributeListMap.contains("WM/Genre")) + return d->attributeListMap["WM/Genre"][0].toString(); + return String::null; +} + +void ASF::Tag::setTitle(const String &value) +{ + d->title = value; +} + +void ASF::Tag::setArtist(const String &value) +{ + d->artist = value; +} + +void ASF::Tag::setCopyright(const String &value) +{ + d->copyright = value; +} + +void ASF::Tag::setComment(const String &value) +{ + d->comment = value; +} + +void ASF::Tag::setRating(const String &value) +{ + d->rating = value; +} + +void ASF::Tag::setAlbum(const String &value) +{ + setAttribute("WM/AlbumTitle", value); +} + +void ASF::Tag::setGenre(const String &value) +{ + setAttribute("WM/Genre", value); +} + +void ASF::Tag::setYear(uint value) +{ + setAttribute("WM/Year", String::number(value)); +} + +void ASF::Tag::setTrack(uint value) +{ + setAttribute("WM/TrackNumber", String::number(value)); +} + +ASF::AttributeListMap& ASF::Tag::attributeListMap() +{ + return d->attributeListMap; +} + +void ASF::Tag::removeItem(const String &key) +{ + AttributeListMap::Iterator it = d->attributeListMap.find(key); + if(it != d->attributeListMap.end()) + d->attributeListMap.erase(it); +} + +void ASF::Tag::setAttribute(const String &name, const Attribute &attribute) +{ + AttributeList value; + value.append(attribute); + d->attributeListMap.insert(name, value); +} + +void ASF::Tag::addAttribute(const String &name, const Attribute &attribute) +{ + if(d->attributeListMap.contains(name)) { + d->attributeListMap[name].append(attribute); + } + else { + setAttribute(name, attribute); + } +} + +bool ASF::Tag::isEmpty() const +{ + return TagLib::Tag::isEmpty() && + copyright().isEmpty() && + rating().isEmpty() && + d->attributeListMap.isEmpty(); +} + +static const char *keyTranslation[][2] = { + { "WM/AlbumTitle", "ALBUM" }, + { "WM/Composer", "COMPOSER" }, + { "WM/Writer", "WRITER" }, + { "WM/Conductor", "CONDUCTOR" }, + { "WM/ModifiedBy", "REMIXER" }, + { "WM/Year", "DATE" }, + { "WM/OriginalReleaseYear", "ORIGINALDATE" }, + { "WM/Producer", "PRODUCER" }, + { "WM/ContentGroupDescription", "GROUPING" }, + { "WM/SubTitle", "SUBTITLE" }, + { "WM/SetSubTitle", "DISCSUBTITLE" }, + { "WM/TrackNumber", "TRACKNUMBER" }, + { "WM/PartOfSet", "DISCNUMBER" }, + { "WM/Genre", "GENRE" }, + { "WM/BeatsPerMinute", "BPM" }, + { "WM/Mood", "MOOD" }, + { "WM/ISRC", "ISRC" }, + { "WM/Lyrics", "LYRICS" }, + { "WM/Media", "MEDIA" }, + { "WM/Publisher", "LABEL" }, + { "WM/CatalogNo", "CATALOGNUMBER" }, + { "WM/Barcode", "BARCODE" }, + { "WM/EncodedBy", "ENCODEDBY" }, + { "WM/AlbumSortOrder", "ALBUMSORT" }, + { "WM/AlbumArtistSortOrder", "ALBUMARTISTSORT" }, + { "WM/ArtistSortOrder", "ARTISTSORT" }, + { "WM/TitleSortOrder", "TITLESORT" }, + { "WM/Script", "SCRIPT" }, + { "WM/Language", "LANGUAGE" }, + { "MusicBrainz/Track Id", "MUSICBRAINZ_TRACKID" }, + { "MusicBrainz/Artist Id", "MUSICBRAINZ_ARTISTID" }, + { "MusicBrainz/Album Id", "MUSICBRAINZ_ALBUMID" }, + { "MusicBrainz/Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID" }, + { "MusicBrainz/Release Group Id", "MUSICBRAINZ_RELEASEGROUPID" }, + { "MusicBrainz/Work Id", "MUSICBRAINZ_WORKID" }, + { "MusicIP/PUID", "MUSICIP_PUID" }, + { "Acoustid/Id", "ACOUSTID_ID" }, + { "Acoustid/Fingerprint", "ACOUSTID_FINGERPRINT" }, +}; + +PropertyMap ASF::Tag::properties() const +{ + static Map<String, String> keyMap; + if(keyMap.isEmpty()) { + int numKeys = sizeof(keyTranslation) / sizeof(keyTranslation[0]); + for(int i = 0; i < numKeys; i++) { + keyMap[keyTranslation[i][0]] = keyTranslation[i][1]; + } + } + + PropertyMap props; + + if(!d->title.isEmpty()) { + props["TITLE"] = d->title; + } + if(!d->artist.isEmpty()) { + props["ARTIST"] = d->artist; + } + if(!d->copyright.isEmpty()) { + props["COPYRIGHT"] = d->copyright; + } + if(!d->comment.isEmpty()) { + props["COMMENT"] = d->comment; + } + + ASF::AttributeListMap::ConstIterator it = d->attributeListMap.begin(); + for(; it != d->attributeListMap.end(); ++it) { + if(keyMap.contains(it->first)) { + String key = keyMap[it->first]; + AttributeList::ConstIterator it2 = it->second.begin(); + for(; it2 != it->second.end(); ++it2) { + if(key == "TRACKNUMBER") { + if(it2->type() == ASF::Attribute::DWordType) + props.insert(key, String::number(it2->toUInt())); + else + props.insert(key, it2->toString()); + } + else { + props.insert(key, it2->toString()); + } + } + } + else { + props.unsupportedData().append(it->first); + } + } + return props; +} + +void ASF::Tag::removeUnsupportedProperties(const StringList &props) +{ + StringList::ConstIterator it = props.begin(); + for(; it != props.end(); ++it) + d->attributeListMap.erase(*it); +} + +PropertyMap ASF::Tag::setProperties(const PropertyMap &props) +{ + static Map<String, String> reverseKeyMap; + if(reverseKeyMap.isEmpty()) { + int numKeys = sizeof(keyTranslation) / sizeof(keyTranslation[0]); + for(int i = 0; i < numKeys; i++) { + reverseKeyMap[keyTranslation[i][1]] = keyTranslation[i][0]; + } + } + + PropertyMap origProps = properties(); + PropertyMap::ConstIterator it = origProps.begin(); + for(; it != origProps.end(); ++it) { + if(!props.contains(it->first) || props[it->first].isEmpty()) { + if(it->first == "TITLE") { + d->title = String::null; + } + else if(it->first == "ARTIST") { + d->artist = String::null; + } + else if(it->first == "COMMENT") { + d->comment = String::null; + } + else if(it->first == "COPYRIGHT") { + d->copyright = String::null; + } + else { + d->attributeListMap.erase(reverseKeyMap[it->first]); + } + } + } + + PropertyMap ignoredProps; + it = props.begin(); + for(; it != props.end(); ++it) { + if(reverseKeyMap.contains(it->first)) { + String name = reverseKeyMap[it->first]; + removeItem(name); + StringList::ConstIterator it2 = it->second.begin(); + for(; it2 != it->second.end(); ++it2) { + addAttribute(name, *it2); + } + } + else if(it->first == "TITLE") { + d->title = it->second.toString(); + } + else if(it->first == "ARTIST") { + d->artist = it->second.toString(); + } + else if(it->first == "COMMENT") { + d->comment = it->second.toString(); + } + else if(it->first == "COPYRIGHT") { + d->copyright = it->second.toString(); + } + else { + ignoredProps.insert(it->first, it->second); + } + } + + return ignoredProps; +} diff --git a/taglib/taglib/asf/asftag.h b/taglib/taglib/asf/asftag.h new file mode 100644 index 0000000..f68579d --- /dev/null +++ b/taglib/taglib/asf/asftag.h @@ -0,0 +1,190 @@ +/************************************************************************** + copyright : (C) 2005-2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ASFTAG_H +#define TAGLIB_ASFTAG_H + +#include "tag.h" +#include "tlist.h" +#include "tmap.h" +#include "taglib_export.h" +#include "asfattribute.h" + +namespace TagLib { + + namespace ASF { + + typedef List<Attribute> AttributeList; + typedef Map<String, AttributeList> AttributeListMap; + + class TAGLIB_EXPORT Tag : public TagLib::Tag { + + friend class File; + + public: + + Tag(); + + virtual ~Tag(); + + /*! + * Returns the track name. + */ + virtual String title() const; + + /*! + * Returns the artist name. + */ + virtual String artist() const; + + /*! + * Returns the album name; if no album name is present in the tag + * String::null will be returned. + */ + virtual String album() const; + + /*! + * Returns the track comment. + */ + virtual String comment() const; + + /*! + * Returns the genre name; if no genre is present in the tag String::null + * will be returned. + */ + virtual String genre() const; + + /*! + * Returns the rating. + */ + virtual String rating() const; + + /*! + * Returns the genre name; if no genre is present in the tag String::null + * will be returned. + */ + virtual String copyright() const; + + /*! + * Returns the year; if there is no year set, this will return 0. + */ + virtual uint year() const; + + /*! + * Returns the track number; if there is no track number set, this will + * return 0. + */ + virtual uint track() const; + + /*! + * Sets the title to \a s. + */ + virtual void setTitle(const String &s); + + /*! + * Sets the artist to \a s. + */ + virtual void setArtist(const String &s); + + /*! + * Sets the album to \a s. If \a s is String::null then this value will be + * cleared. + */ + virtual void setAlbum(const String &s); + + /*! + * Sets the comment to \a s. + */ + virtual void setComment(const String &s); + + /*! + * Sets the rating to \a s. + */ + virtual void setRating(const String &s); + + /*! + * Sets the copyright to \a s. + */ + virtual void setCopyright(const String &s); + + /*! + * Sets the genre to \a s. + */ + virtual void setGenre(const String &s); + + /*! + * Sets the year to \a i. If \a s is 0 then this value will be cleared. + */ + virtual void setYear(uint i); + + /*! + * Sets the track to \a i. If \a s is 0 then this value will be cleared. + */ + virtual void setTrack(uint i); + + /*! + * Returns true if the tag does not contain any data. This should be + * reimplemented in subclasses that provide more than the basic tagging + * abilities in this class. + */ + virtual bool isEmpty() const; + + /*! + * Returns a reference to the item list map. This is an AttributeListMap of + * all of the items in the tag. + * + * This is the most powerfull structure for accessing the items of the tag. + */ + AttributeListMap &attributeListMap(); + + /*! + * Removes the \a key attribute from the tag + */ + void removeItem(const String &name); + + /*! + * Sets the \a key attribute to the value of \a attribute. If an attribute + * with the \a key is already present, it will be replaced. + */ + void setAttribute(const String &name, const Attribute &attribute); + + /*! + * Sets the \a key attribute to the value of \a attribute. If an attribute + * with the \a key is already present, it will be added to the list. + */ + void addAttribute(const String &name, const Attribute &attribute); + + PropertyMap properties() const; + void removeUnsupportedProperties(const StringList& properties); + PropertyMap setProperties(const PropertyMap &properties); + + private: + + class TagPrivate; + TagPrivate *d; + }; + } +} +#endif diff --git a/taglib/taglib/audioproperties.cpp b/taglib/taglib/audioproperties.cpp new file mode 100644 index 0000000..298b97d --- /dev/null +++ b/taglib/taglib/audioproperties.cpp @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "audioproperties.h" + +using namespace TagLib; + +class AudioProperties::AudioPropertiesPrivate +{ + +}; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +AudioProperties::~AudioProperties() +{ + +} + +//////////////////////////////////////////////////////////////////////////////// +// protected methods +//////////////////////////////////////////////////////////////////////////////// + +AudioProperties::AudioProperties(ReadStyle) +{ + +} diff --git a/taglib/taglib/audioproperties.h b/taglib/taglib/audioproperties.h new file mode 100644 index 0000000..e9844fa --- /dev/null +++ b/taglib/taglib/audioproperties.h @@ -0,0 +1,110 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_AUDIOPROPERTIES_H +#define TAGLIB_AUDIOPROPERTIES_H + +#include "taglib_export.h" + +namespace TagLib { + + //! A simple, abstract interface to common audio properties + + /*! + * The values here are common to most audio formats. For more specific, codec + * dependant values, please see see the subclasses APIs. This is meant to + * compliment the TagLib::File and TagLib::Tag APIs in providing a simple + * interface that is sufficient for most applications. + */ + + class TAGLIB_EXPORT AudioProperties + { + public: + + /*! + * Reading audio properties from a file can sometimes be very time consuming + * and for the most accurate results can often involve reading the entire + * file. Because in many situations speed is critical or the accuracy of the + * values is not particularly important this allows the level of desired + * accuracy to be set. + */ + enum ReadStyle { + //! Read as little of the file as possible + Fast, + //! Read more of the file and make better values guesses + Average, + //! Read as much of the file as needed to report accurate values + Accurate + }; + + /*! + * Destroys this AudioProperties instance. + */ + virtual ~AudioProperties(); + + /*! + * Returns the length of the file in seconds. + */ + virtual int length() const = 0; + + /*! + * Returns the most appropriate bit rate for the file in kb/s. For constant + * bitrate formats this is simply the bitrate of the file. For variable + * bitrate formats this is either the average or nominal bitrate. + */ + virtual int bitrate() const = 0; + + /*! + * Returns the sample rate in Hz. + */ + virtual int sampleRate() const = 0; + + /*! + * Returns the number of audio channels. + */ + virtual int channels() const = 0; + + protected: + + /*! + * Construct an audio properties instance. This is protected as this class + * should not be instantiated directly, but should be instantiated via its + * subclasses and can be fetched from the FileRef or File APIs. + * + * \see ReadStyle + */ + AudioProperties(ReadStyle style); + + private: + AudioProperties(const AudioProperties &); + AudioProperties &operator=(const AudioProperties &); + + class AudioPropertiesPrivate; + AudioPropertiesPrivate *d; + }; + +} + +#endif diff --git a/taglib/taglib/fileref.cpp b/taglib/taglib/fileref.cpp new file mode 100644 index 0000000..4403a5f --- /dev/null +++ b/taglib/taglib/fileref.cpp @@ -0,0 +1,286 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + + copyright : (C) 2010 by Alex Novichkov + email : novichko@atnet.ru + (added APE file support) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tfile.h> +#include <tstring.h> +#include <tdebug.h> +#include "trefcounter.h" + +#include "fileref.h" +#include "asffile.h" +#include "mpegfile.h" +#include "vorbisfile.h" +#include "flacfile.h" +#include "oggflacfile.h" +#include "mpcfile.h" +#include "mp4file.h" +#include "wavpackfile.h" +#include "speexfile.h" +#include "opusfile.h" +#include "trueaudiofile.h" +#include "aifffile.h" +#include "wavfile.h" +#include "apefile.h" +#include "modfile.h" +#include "s3mfile.h" +#include "itfile.h" +#include "xmfile.h" + +using namespace TagLib; + +class FileRef::FileRefPrivate : public RefCounter +{ +public: + FileRefPrivate(File *f) : RefCounter(), file(f) {} + ~FileRefPrivate() { + delete file; + } + + File *file; + static List<const FileTypeResolver *> fileTypeResolvers; +}; + +List<const FileRef::FileTypeResolver *> FileRef::FileRefPrivate::fileTypeResolvers; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +FileRef::FileRef() +{ + d = new FileRefPrivate(0); +} + +FileRef::FileRef(FileName fileName, bool readAudioProperties, + AudioProperties::ReadStyle audioPropertiesStyle) +{ + d = new FileRefPrivate(create(fileName, readAudioProperties, audioPropertiesStyle)); +} + +FileRef::FileRef(File *file) +{ + d = new FileRefPrivate(file); +} + +FileRef::FileRef(const FileRef &ref) : d(ref.d) +{ + d->ref(); +} + +FileRef::~FileRef() +{ + if(d->deref()) + delete d; +} + +Tag *FileRef::tag() const +{ + if(isNull()) { + debug("FileRef::tag() - Called without a valid file."); + return 0; + } + return d->file->tag(); +} + +AudioProperties *FileRef::audioProperties() const +{ + if(isNull()) { + debug("FileRef::audioProperties() - Called without a valid file."); + return 0; + } + return d->file->audioProperties(); +} + +File *FileRef::file() const +{ + return d->file; +} + +bool FileRef::save() +{ + if(isNull()) { + debug("FileRef::save() - Called without a valid file."); + return false; + } + return d->file->save(); +} + +const FileRef::FileTypeResolver *FileRef::addFileTypeResolver(const FileRef::FileTypeResolver *resolver) // static +{ + FileRefPrivate::fileTypeResolvers.prepend(resolver); + return resolver; +} + +StringList FileRef::defaultFileExtensions() +{ + StringList l; + + l.append("ogg"); + l.append("flac"); + l.append("oga"); + l.append("mp3"); + l.append("mpc"); + l.append("wv"); + l.append("spx"); + l.append("tta"); + l.append("m4a"); + l.append("m4r"); + l.append("m4b"); + l.append("m4p"); + l.append("3g2"); + l.append("mp4"); + l.append("wma"); + l.append("asf"); + l.append("aif"); + l.append("aiff"); + l.append("wav"); + l.append("ape"); + l.append("mod"); + l.append("module"); // alias for "mod" + l.append("nst"); // alias for "mod" + l.append("wow"); // alias for "mod" + l.append("s3m"); + l.append("it"); + l.append("xm"); + + return l; +} + +bool FileRef::isNull() const +{ + return !d->file || !d->file->isValid(); +} + +FileRef &FileRef::operator=(const FileRef &ref) +{ + if(&ref == this) + return *this; + + if(d->deref()) + delete d; + + d = ref.d; + d->ref(); + + return *this; +} + +bool FileRef::operator==(const FileRef &ref) const +{ + return ref.d->file == d->file; +} + +bool FileRef::operator!=(const FileRef &ref) const +{ + return ref.d->file != d->file; +} + +File *FileRef::create(FileName fileName, bool readAudioProperties, + AudioProperties::ReadStyle audioPropertiesStyle) // static +{ + + List<const FileTypeResolver *>::ConstIterator it = FileRefPrivate::fileTypeResolvers.begin(); + + for(; it != FileRefPrivate::fileTypeResolvers.end(); ++it) { + File *file = (*it)->createFile(fileName, readAudioProperties, audioPropertiesStyle); + if(file) + return file; + } + + // Ok, this is really dumb for now, but it works for testing. + + String ext; + { +#ifdef _WIN32 + + String s = fileName.toString(); + +#else + + String s = fileName; + + #endif + + const int pos = s.rfind("."); + if(pos != -1) + ext = s.substr(pos + 1).upper(); + } + + // If this list is updated, the method defaultFileExtensions() should also be + // updated. However at some point that list should be created at the same time + // that a default file type resolver is created. + + if(!ext.isEmpty()) { + if(ext == "MP3") + return new MPEG::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "OGG") + return new Ogg::Vorbis::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "OGA") { + /* .oga can be any audio in the Ogg container. First try FLAC, then Vorbis. */ + File *file = new Ogg::FLAC::File(fileName, readAudioProperties, audioPropertiesStyle); + if (file->isValid()) + return file; + delete file; + return new Ogg::Vorbis::File(fileName, readAudioProperties, audioPropertiesStyle); + } + if(ext == "FLAC") + return new FLAC::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "MPC") + return new MPC::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "WV") + return new WavPack::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "SPX") + return new Ogg::Speex::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "OPUS") + return new Ogg::Opus::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "TTA") + return new TrueAudio::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2") + return new MP4::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "WMA" || ext == "ASF") + return new ASF::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "AIF" || ext == "AIFF") + return new RIFF::AIFF::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "WAV") + return new RIFF::WAV::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "APE") + return new APE::File(fileName, readAudioProperties, audioPropertiesStyle); + // module, nst and wow are possible but uncommon extensions + if(ext == "MOD" || ext == "MODULE" || ext == "NST" || ext == "WOW") + return new Mod::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "S3M") + return new S3M::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "IT") + return new IT::File(fileName, readAudioProperties, audioPropertiesStyle); + if(ext == "XM") + return new XM::File(fileName, readAudioProperties, audioPropertiesStyle); + } + + return 0; +} diff --git a/taglib/taglib/fileref.h b/taglib/taglib/fileref.h new file mode 100644 index 0000000..0f0c21a --- /dev/null +++ b/taglib/taglib/fileref.h @@ -0,0 +1,263 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_FILEREF_H +#define TAGLIB_FILEREF_H + +#include "tfile.h" +#include "tstringlist.h" + +#include "taglib_export.h" +#include "audioproperties.h" + +namespace TagLib { + + class Tag; + + //! This class provides a simple abstraction for creating and handling files + + /*! + * FileRef exists to provide a minimal, generic and value-based wrapper around + * a File. It is lightweight and implicitly shared, and as such suitable for + * pass-by-value use. This hides some of the uglier details of TagLib::File + * and the non-generic portions of the concrete file implementations. + * + * This class is useful in a "simple usage" situation where it is desirable + * to be able to get and set some of the tag information that is similar + * across file types. + * + * Also note that it is probably a good idea to plug this into your mime + * type system rather than using the constructor that accepts a file name using + * the FileTypeResolver. + * + * \see FileTypeResolver + * \see addFileTypeResolver() + */ + + class TAGLIB_EXPORT FileRef + { + public: + + //! A class for pluggable file type resolution. + + /*! + * This class is used to add extend TagLib's very basic file name based file + * type resolution. + * + * This can be accomplished with: + * + * \code + * + * class MyFileTypeResolver : FileTypeResolver + * { + * TagLib::File *createFile(TagLib::FileName *fileName, bool, AudioProperties::ReadStyle) + * { + * if(someCheckForAnMP3File(fileName)) + * return new TagLib::MPEG::File(fileName); + * return 0; + * } + * } + * + * FileRef::addFileTypeResolver(new MyFileTypeResolver); + * + * \endcode + * + * Naturally a less contrived example would be slightly more complex. This + * can be used to plug in mime-type detection systems or to add new file types + * to TagLib. + */ + + class TAGLIB_EXPORT FileTypeResolver + { + TAGLIB_IGNORE_MISSING_DESTRUCTOR + public: + /*! + * This method must be overridden to provide an additional file type + * resolver. If the resolver is able to determine the file type it should + * return a valid File object; if not it should return 0. + * + * \note The created file is then owned by the FileRef and should not be + * deleted. Deletion will happen automatically when the FileRef passes + * out of scope. + */ + virtual File *createFile(FileName fileName, + bool readAudioProperties = true, + AudioProperties::ReadStyle + audioPropertiesStyle = AudioProperties::Average) const = 0; + }; + + /*! + * Creates a null FileRef. + */ + FileRef(); + + /*! + * Create a FileRef from \a fileName. If \a readAudioProperties is true then + * the audio properties will be read using \a audioPropertiesStyle. If + * \a readAudioProperties is false then \a audioPropertiesStyle will be + * ignored. + * + * Also see the note in the class documentation about why you may not want to + * use this method in your application. + */ + explicit FileRef(FileName fileName, + bool readAudioProperties = true, + AudioProperties::ReadStyle + audioPropertiesStyle = AudioProperties::Average); + + /*! + * Contruct a FileRef using \a file. The FileRef now takes ownership of the + * pointer and will delete the File when it passes out of scope. + */ + explicit FileRef(File *file); + + /*! + * Make a copy of \a ref. + */ + FileRef(const FileRef &ref); + + /*! + * Destroys this FileRef instance. + */ + virtual ~FileRef(); + + /*! + * Returns a pointer to represented file's tag. + * + * \warning This pointer will become invalid when this FileRef and all + * copies pass out of scope. + * + * \warning Do not cast it to any subclasses of \class Tag. + * Use tag returning methods of appropriate subclasses of \class File instead. + * + * \see File::tag() + */ + Tag *tag() const; + + /*! + * Returns the audio properties for this FileRef. If no audio properties + * were read then this will returns a null pointer. + */ + AudioProperties *audioProperties() const; + + /*! + * Returns a pointer to the file represented by this handler class. + * + * As a general rule this call should be avoided since if you need to work + * with file objects directly, you are probably better served instantiating + * the File subclasses (i.e. MPEG::File) manually and working with their APIs. + * + * This <i>handle</i> exists to provide a minimal, generic and value-based + * wrapper around a File. Accessing the file directly generally indicates + * a moving away from this simplicity (and into things beyond the scope of + * FileRef). + * + * \warning This pointer will become invalid when this FileRef and all + * copies pass out of scope. + */ + File *file() const; + + /*! + * Saves the file. Returns true on success. + */ + bool save(); + + /*! + * Adds a FileTypeResolver to the list of those used by TagLib. Each + * additional FileTypeResolver is added to the front of a list of resolvers + * that are tried. If the FileTypeResolver returns zero the next resolver + * is tried. + * + * Returns a pointer to the added resolver (the same one that's passed in -- + * this is mostly so that static inialializers have something to use for + * assignment). + * + * \see FileTypeResolver + */ + static const FileTypeResolver *addFileTypeResolver(const FileTypeResolver *resolver); + + /*! + * As is mentioned elsewhere in this class's documentation, the default file + * type resolution code provided by TagLib only works by comparing file + * extensions. + * + * This method returns the list of file extensions that are used by default. + * + * The extensions are all returned in lowercase, though the comparison used + * by TagLib for resolution is case-insensitive. + * + * \note This does not account for any additional file type resolvers that + * are plugged in. Also note that this is not intended to replace a propper + * mime-type resolution system, but is just here for reference. + * + * \see FileTypeResolver + */ + static StringList defaultFileExtensions(); + + /*! + * Returns true if the file (and as such other pointers) are null. + */ + bool isNull() const; + + /*! + * Assign the file pointed to by \a ref to this FileRef. + */ + FileRef &operator=(const FileRef &ref); + + /*! + * Returns true if this FileRef and \a ref point to the same File object. + */ + bool operator==(const FileRef &ref) const; + + /*! + * Returns true if this FileRef and \a ref do not point to the same File + * object. + */ + bool operator!=(const FileRef &ref) const; + + /*! + * A simple implementation of file type guessing. If \a readAudioProperties + * is true then the audio properties will be read using + * \a audioPropertiesStyle. If \a readAudioProperties is false then + * \a audioPropertiesStyle will be ignored. + * + * \note You generally shouldn't use this method, but instead the constructor + * directly. + * + * \deprecated + */ + static File *create(FileName fileName, + bool readAudioProperties = true, + AudioProperties::ReadStyle audioPropertiesStyle = AudioProperties::Average); + + + private: + class FileRefPrivate; + FileRefPrivate *d; + }; + +} // namespace TagLib + +#endif diff --git a/taglib/taglib/flac/flacfile.cpp b/taglib/taglib/flac/flacfile.cpp new file mode 100644 index 0000000..823170a --- /dev/null +++ b/taglib/taglib/flac/flacfile.cpp @@ -0,0 +1,568 @@ +/*************************************************************************** + copyright : (C) 2003-2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tstring.h> +#include <tlist.h> +#include <tdebug.h> +#include <tagunion.h> +#include <tpropertymap.h> + +#include <id3v2header.h> +#include <id3v2tag.h> +#include <id3v1tag.h> +#include <xiphcomment.h> + +#include "flacpicture.h" +#include "flacfile.h" +#include "flacmetadatablock.h" +#include "flacunknownmetadatablock.h" + +using namespace TagLib; + +namespace +{ + enum { FlacXiphIndex = 0, FlacID3v2Index = 1, FlacID3v1Index = 2 }; + enum { MinPaddingLength = 4096 }; + enum { LastBlockFlag = 0x80 }; +} + +class FLAC::File::FilePrivate +{ +public: + FilePrivate() : + ID3v2FrameFactory(ID3v2::FrameFactory::instance()), + ID3v2Location(-1), + ID3v2OriginalSize(0), + ID3v1Location(-1), + properties(0), + flacStart(0), + streamStart(0), + streamLength(0), + scanned(false), + hasXiphComment(false), + hasID3v2(false), + hasID3v1(false) + { + } + + ~FilePrivate() + { + uint size = blocks.size(); + for(uint i = 0; i < size; i++) { + delete blocks[i]; + } + delete properties; + } + + const ID3v2::FrameFactory *ID3v2FrameFactory; + long ID3v2Location; + uint ID3v2OriginalSize; + + long ID3v1Location; + + TagUnion tag; + + Properties *properties; + ByteVector streamInfoData; + ByteVector xiphCommentData; + List<MetadataBlock *> blocks; + + long flacStart; + long streamStart; + long streamLength; + bool scanned; + + bool hasXiphComment; + bool hasID3v2; + bool hasID3v1; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +FLAC::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : + TagLib::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +FLAC::File::File(FileName file, ID3v2::FrameFactory *frameFactory, + bool readProperties, Properties::ReadStyle propertiesStyle) : + TagLib::File(file) +{ + d = new FilePrivate; + d->ID3v2FrameFactory = frameFactory; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +FLAC::File::File(IOStream *stream, ID3v2::FrameFactory *frameFactory, + bool readProperties, Properties::ReadStyle propertiesStyle) : + TagLib::File(stream) +{ + d = new FilePrivate; + d->ID3v2FrameFactory = frameFactory; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +FLAC::File::~File() +{ + delete d; +} + +TagLib::Tag *FLAC::File::tag() const +{ + return &d->tag; +} + +PropertyMap FLAC::File::properties() const +{ + // once Tag::properties() is virtual, this case distinction could actually be done + // within TagUnion. + if(d->hasXiphComment) + return d->tag.access<Ogg::XiphComment>(FlacXiphIndex, false)->properties(); + if(d->hasID3v2) + return d->tag.access<ID3v2::Tag>(FlacID3v2Index, false)->properties(); + if(d->hasID3v1) + return d->tag.access<ID3v1::Tag>(FlacID3v1Index, false)->properties(); + return PropertyMap(); +} + +void FLAC::File::removeUnsupportedProperties(const StringList &unsupported) +{ + if(d->hasXiphComment) + d->tag.access<Ogg::XiphComment>(FlacXiphIndex, false)->removeUnsupportedProperties(unsupported); + if(d->hasID3v2) + d->tag.access<ID3v2::Tag>(FlacID3v2Index, false)->removeUnsupportedProperties(unsupported); + if(d->hasID3v1) + d->tag.access<ID3v1::Tag>(FlacID3v1Index, false)->removeUnsupportedProperties(unsupported); +} + +PropertyMap FLAC::File::setProperties(const PropertyMap &properties) +{ + return d->tag.access<Ogg::XiphComment>(FlacXiphIndex, true)->setProperties(properties); +} + +FLAC::Properties *FLAC::File::audioProperties() const +{ + return d->properties; +} + + +bool FLAC::File::save() +{ + if(readOnly()) { + debug("FLAC::File::save() - Cannot save to a read only file."); + return false; + } + + if(!isValid()) { + debug("FLAC::File::save() -- Trying to save invalid file."); + return false; + } + + // Create new vorbis comments + + Tag::duplicate(&d->tag, xiphComment(true), false); + + d->xiphCommentData = xiphComment()->render(false); + + // Replace metadata blocks + + bool foundVorbisCommentBlock = false; + List<MetadataBlock *> newBlocks; + for(uint i = 0; i < d->blocks.size(); i++) { + MetadataBlock *block = d->blocks[i]; + if(block->code() == MetadataBlock::VorbisComment) { + // Set the new Vorbis Comment block + delete block; + block = new UnknownMetadataBlock(MetadataBlock::VorbisComment, d->xiphCommentData); + foundVorbisCommentBlock = true; + } + if(block->code() == MetadataBlock::Padding) { + delete block; + continue; + } + newBlocks.append(block); + } + if(!foundVorbisCommentBlock) { + newBlocks.append(new UnknownMetadataBlock(MetadataBlock::VorbisComment, d->xiphCommentData)); + foundVorbisCommentBlock = true; + } + d->blocks = newBlocks; + + // Render data for the metadata blocks + + ByteVector data; + for(uint i = 0; i < newBlocks.size(); i++) { + FLAC::MetadataBlock *block = newBlocks[i]; + ByteVector blockData = block->render(); + ByteVector blockHeader = ByteVector::fromUInt(blockData.size()); + blockHeader[0] = block->code(); + data.append(blockHeader); + data.append(blockData); + } + + // Adjust the padding block(s) + + long originalLength = d->streamStart - d->flacStart; + int paddingLength = originalLength - data.size() - 4; + if (paddingLength < 0) { + paddingLength = MinPaddingLength; + } + ByteVector padding = ByteVector::fromUInt(paddingLength); + padding.resize(paddingLength + 4); + padding[0] = (char)(FLAC::MetadataBlock::Padding | LastBlockFlag); + data.append(padding); + + // Write the data to the file + + insert(data, d->flacStart, originalLength); + d->hasXiphComment = true; + + // Update ID3 tags + + if(ID3v2Tag()) { + if(d->hasID3v2) { + if(d->ID3v2Location < d->flacStart) + debug("FLAC::File::save() -- This can't be right -- an ID3v2 tag after the " + "start of the FLAC bytestream? Not writing the ID3v2 tag."); + else + insert(ID3v2Tag()->render(), d->ID3v2Location, d->ID3v2OriginalSize); + } + else + insert(ID3v2Tag()->render(), 0, 0); + } + + if(ID3v1Tag()) { + seek(-128, End); + writeBlock(ID3v1Tag()->render()); + } + + return true; +} + +ID3v2::Tag *FLAC::File::ID3v2Tag(bool create) +{ + if(!create || d->tag[FlacID3v2Index]) + return static_cast<ID3v2::Tag *>(d->tag[FlacID3v2Index]); + + d->tag.set(FlacID3v2Index, new ID3v2::Tag); + return static_cast<ID3v2::Tag *>(d->tag[FlacID3v2Index]); +} + +ID3v1::Tag *FLAC::File::ID3v1Tag(bool create) +{ + return d->tag.access<ID3v1::Tag>(FlacID3v1Index, create); +} + +Ogg::XiphComment *FLAC::File::xiphComment(bool create) +{ + return d->tag.access<Ogg::XiphComment>(FlacXiphIndex, create); +} + +void FLAC::File::setID3v2FrameFactory(const ID3v2::FrameFactory *factory) +{ + d->ID3v2FrameFactory = factory; +} + + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void FLAC::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + // Look for an ID3v2 tag + + d->ID3v2Location = findID3v2(); + + if(d->ID3v2Location >= 0) { + + d->tag.set(FlacID3v2Index, new ID3v2::Tag(this, d->ID3v2Location, d->ID3v2FrameFactory)); + + d->ID3v2OriginalSize = ID3v2Tag()->header()->completeTagSize(); + + if(ID3v2Tag()->header()->tagSize() <= 0) + d->tag.set(FlacID3v2Index, 0); + else + d->hasID3v2 = true; + } + + // Look for an ID3v1 tag + + d->ID3v1Location = findID3v1(); + + if(d->ID3v1Location >= 0) { + d->tag.set(FlacID3v1Index, new ID3v1::Tag(this, d->ID3v1Location)); + d->hasID3v1 = true; + } + + // Look for FLAC metadata, including vorbis comments + + scan(); + + if(!isValid()) + return; + + if(d->hasXiphComment) + d->tag.set(FlacXiphIndex, new Ogg::XiphComment(xiphCommentData())); + else + d->tag.set(FlacXiphIndex, new Ogg::XiphComment); + + if(readProperties) + d->properties = new Properties(streamInfoData(), streamLength(), propertiesStyle); +} + +ByteVector FLAC::File::streamInfoData() +{ + return isValid() ? d->streamInfoData : ByteVector(); +} + +ByteVector FLAC::File::xiphCommentData() const +{ + return (isValid() && d->hasXiphComment) ? d->xiphCommentData : ByteVector(); +} + +long FLAC::File::streamLength() +{ + return d->streamLength; +} + +void FLAC::File::scan() +{ + // Scan the metadata pages + + if(d->scanned) + return; + + if(!isValid()) + return; + + long nextBlockOffset; + + if(d->hasID3v2) + nextBlockOffset = find("fLaC", d->ID3v2Location + d->ID3v2OriginalSize); + else + nextBlockOffset = find("fLaC"); + + if(nextBlockOffset < 0) { + debug("FLAC::File::scan() -- FLAC stream not found"); + setValid(false); + return; + } + + nextBlockOffset += 4; + d->flacStart = nextBlockOffset; + + seek(nextBlockOffset); + + ByteVector header = readBlock(4); + + // Header format (from spec): + // <1> Last-metadata-block flag + // <7> BLOCK_TYPE + // 0 : STREAMINFO + // 1 : PADDING + // .. + // 4 : VORBIS_COMMENT + // .. + // <24> Length of metadata to follow + + char blockType = header[0] & 0x7f; + bool isLastBlock = (header[0] & 0x80) != 0; + uint length = header.toUInt(1U, 3U); + + // First block should be the stream_info metadata + + if(blockType != MetadataBlock::StreamInfo) { + debug("FLAC::File::scan() -- invalid FLAC stream"); + setValid(false); + return; + } + + d->streamInfoData = readBlock(length); + d->blocks.append(new UnknownMetadataBlock(blockType, d->streamInfoData)); + nextBlockOffset += length + 4; + + // Search through the remaining metadata + while(!isLastBlock) { + + header = readBlock(4); + blockType = header[0] & 0x7f; + isLastBlock = (header[0] & 0x80) != 0; + length = header.toUInt(1U, 3U); + + ByteVector data = readBlock(length); + if(data.size() != length || length == 0) { + debug("FLAC::File::scan() -- FLAC stream corrupted"); + setValid(false); + return; + } + + MetadataBlock *block = 0; + + // Found the vorbis-comment + if(blockType == MetadataBlock::VorbisComment) { + if(!d->hasXiphComment) { + d->xiphCommentData = data; + d->hasXiphComment = true; + } + else { + debug("FLAC::File::scan() -- multiple Vorbis Comment blocks found, using the first one"); + } + } + else if(blockType == MetadataBlock::Picture) { + FLAC::Picture *picture = new FLAC::Picture(); + if(picture->parse(data)) { + block = picture; + } + else { + debug("FLAC::File::scan() -- invalid picture found, discarting"); + delete picture; + } + } + + if(!block) { + block = new UnknownMetadataBlock(blockType, data); + } + if(block->code() != MetadataBlock::Padding) { + d->blocks.append(block); + } + else { + delete block; + } + + nextBlockOffset += length + 4; + + if(nextBlockOffset >= File::length()) { + debug("FLAC::File::scan() -- FLAC stream corrupted"); + setValid(false); + return; + } + seek(nextBlockOffset); + } + + // End of metadata, now comes the datastream + + d->streamStart = nextBlockOffset; + d->streamLength = File::length() - d->streamStart; + + if(d->hasID3v1) + d->streamLength -= 128; + + d->scanned = true; +} + +long FLAC::File::findID3v1() +{ + if(!isValid()) + return -1; + + seek(-128, End); + long p = tell(); + + if(readBlock(3) == ID3v1::Tag::fileIdentifier()) + return p; + + return -1; +} + +long FLAC::File::findID3v2() +{ + if(!isValid()) + return -1; + + seek(0); + + if(readBlock(3) == ID3v2::Header::fileIdentifier()) + return 0; + + return -1; +} + +List<FLAC::Picture *> FLAC::File::pictureList() +{ + List<Picture *> pictures; + for(uint i = 0; i < d->blocks.size(); i++) { + Picture *picture = dynamic_cast<Picture *>(d->blocks[i]); + if(picture) { + pictures.append(picture); + } + } + return pictures; +} + +void FLAC::File::addPicture(Picture *picture) +{ + d->blocks.append(picture); +} + +void FLAC::File::removePicture(Picture *picture, bool del) +{ + MetadataBlock *block = picture; + List<MetadataBlock *>::Iterator it = d->blocks.find(block); + if(it != d->blocks.end()) + d->blocks.erase(it); + + if(del) + delete picture; +} + +void FLAC::File::removePictures() +{ + List<MetadataBlock *> newBlocks; + for(uint i = 0; i < d->blocks.size(); i++) { + Picture *picture = dynamic_cast<Picture *>(d->blocks[i]); + if(picture) { + delete picture; + } + else { + newBlocks.append(d->blocks[i]); + } + } + d->blocks = newBlocks; +} + +bool FLAC::File::hasXiphComment() const +{ + return d->hasXiphComment; +} + +bool FLAC::File::hasID3v1Tag() const +{ + return d->hasID3v1; +} + +bool FLAC::File::hasID3v2Tag() const +{ + return d->hasID3v2; +} diff --git a/taglib/taglib/flac/flacfile.h b/taglib/taglib/flac/flacfile.h new file mode 100644 index 0000000..0963f4a --- /dev/null +++ b/taglib/taglib/flac/flacfile.h @@ -0,0 +1,305 @@ +/*************************************************************************** + copyright : (C) 2003 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_FLACFILE_H +#define TAGLIB_FLACFILE_H + +#include "taglib_export.h" +#include "tfile.h" +#include "tlist.h" +#include "tag.h" + +#include "flacpicture.h" +#include "flacproperties.h" + +namespace TagLib { + + class Tag; + namespace ID3v2 { class FrameFactory; class Tag; } + namespace ID3v1 { class Tag; } + namespace Ogg { class XiphComment; } + + //! An implementation of FLAC metadata + + /*! + * This is implementation of FLAC metadata for non-Ogg FLAC files. At some + * point when Ogg / FLAC is more common there will be a similar implementation + * under the Ogg hiearchy. + * + * This supports ID3v1, ID3v2 and Xiph style comments as well as reading stream + * properties from the file. + */ + + namespace FLAC { + + //! An implementation of TagLib::File with FLAC specific methods + + /*! + * This implements and provides an interface for FLAC files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to FLAC files. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * Constructs a FLAC file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + * + * \deprecated This constructor will be dropped in favor of the one below + * in a future version. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an APE file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * If this file contains and ID3v2 tag the frames will be created using + * \a frameFactory. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + // BIC: merge with the above constructor + File(FileName file, ID3v2::FrameFactory *frameFactory, + bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs a FLAC file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * If this file contains and ID3v2 tag the frames will be created using + * \a frameFactory. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + // BIC: merge with the above constructor + File(IOStream *stream, ID3v2::FrameFactory *frameFactory, + bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the Tag for this file. This will be a union of XiphComment, + * ID3v1 and ID3v2 tags. + * + * \see ID3v2Tag() + * \see ID3v1Tag() + * \see XiphComment() + */ + virtual TagLib::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * If the file contains more than one tag (e.g. XiphComment and ID3v1), + * only the first one (in the order XiphComment, ID3v2, ID3v1) will be + * converted to the PropertyMap. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &); + + /*! + * Implements the unified property interface -- import function. + * This always creates a Xiph comment, if none exists. The return value + * relates to the Xiph comment only. + * Ignores any changes to ID3v1 or ID3v2 comments since they are not allowed + * in the FLAC specification. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the FLAC::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + /*! + * Save the file. This will primarily save the XiphComment, but + * will also keep any old ID3-tags up to date. If the file + * has no XiphComment, one will be constructed from the ID3-tags. + * + * This returns true if the save was successful. + */ + virtual bool save(); + + /*! + * Returns a pointer to the ID3v2 tag of the file. + * + * If \a create is false (the default) this returns a null pointer + * if there is no valid ID3v2 tag. If \a create is true it will create + * an ID3v2 tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v2 tag. Use hasID3v2Tag() to check if the file + * on disk actually has an ID3v2 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v2Tag() + */ + ID3v2::Tag *ID3v2Tag(bool create = false); + + /*! + * Returns a pointer to the ID3v1 tag of the file. + * + * If \a create is false (the default) this returns a null pointer + * if there is no valid APE tag. If \a create is true it will create + * an APE tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v1 tag. Use hasID3v1Tag() to check if the file + * on disk actually has an ID3v1 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v1Tag() + */ + ID3v1::Tag *ID3v1Tag(bool create = false); + + /*! + * Returns a pointer to the XiphComment for the file. + * + * If \a create is false (the default) this returns a null pointer + * if there is no valid XiphComment. If \a create is true it will create + * a XiphComment if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has a XiphComment. Use hasXiphComment() to check if the + * file on disk actually has a XiphComment. + * + * \note The Tag <b>is still</b> owned by the FLAC::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasXiphComment() + */ + Ogg::XiphComment *xiphComment(bool create = false); + + /*! + * Set the ID3v2::FrameFactory to something other than the default. This + * can be used to specify the way that ID3v2 frames will be interpreted + * when + * + * \see ID3v2FrameFactory + */ + void setID3v2FrameFactory(const ID3v2::FrameFactory *factory); + + /*! + * Returns the block of data used by FLAC::Properties for parsing the + * stream properties. + * + * \deprecated This method will not be public in a future release. + */ + ByteVector streamInfoData(); // BIC: remove + + /*! + * Returns the length of the audio-stream, used by FLAC::Properties for + * calculating the bitrate. + * + * \deprecated This method will not be public in a future release. + */ + long streamLength(); // BIC: remove + + /*! + * Returns a list of pictures attached to the FLAC file. + */ + List<Picture *> pictureList(); + + /*! + * Removes an attached picture. If \a del is true the picture's memory + * will be freed; if it is false, it must be deleted by the user. + */ + void removePicture(Picture *picture, bool del = true); + + /*! + * Remove all attached images. + */ + void removePictures(); + + /*! + * Add a new picture to the file. The file takes ownership of the + * picture and will handle freeing its memory. + * + * \note The file will be saved only after calling save(). + */ + void addPicture(Picture *picture); + + /*! + * Returns whether or not the file on disk actually has a XiphComment. + * + * \see xiphComment() + */ + bool hasXiphComment() const; + + /*! + * Returns whether or not the file on disk actually has an ID3v1 tag. + * + * \see ID3v1Tag() + */ + bool hasID3v1Tag() const; + + /*! + * Returns whether or not the file on disk actually has an ID3v2 tag. + * + * \see ID3v2Tag() + */ + bool hasID3v2Tag() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + void scan(); + long findID3v2(); + long findID3v1(); + ByteVector xiphCommentData() const; + long findPaddingBreak(long nextPageOffset, long targetOffset, bool *isLast); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/flac/flacmetadatablock.cpp b/taglib/taglib/flac/flacmetadatablock.cpp new file mode 100644 index 0000000..17ab05f --- /dev/null +++ b/taglib/taglib/flac/flacmetadatablock.cpp @@ -0,0 +1,47 @@ +/************************************************************************** + copyright : (C) 2010 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <taglib.h> +#include <tdebug.h> +#include "flacmetadatablock.h" + +using namespace TagLib; + +class FLAC::MetadataBlock::MetadataBlockPrivate +{ +public: + MetadataBlockPrivate() {} + +}; + +FLAC::MetadataBlock::MetadataBlock() +{ + d = 0; +} + +FLAC::MetadataBlock::~MetadataBlock() +{ +} + diff --git a/taglib/taglib/flac/flacmetadatablock.h b/taglib/taglib/flac/flacmetadatablock.h new file mode 100644 index 0000000..41bca31 --- /dev/null +++ b/taglib/taglib/flac/flacmetadatablock.h @@ -0,0 +1,75 @@ +/************************************************************************** + copyright : (C) 2010 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_FLACMETADATABLOCK_H +#define TAGLIB_FLACMETADATABLOCK_H + +#include "tlist.h" +#include "tbytevector.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace FLAC { + + class TAGLIB_EXPORT MetadataBlock + { + public: + MetadataBlock(); + virtual ~MetadataBlock(); + + enum BlockType { + StreamInfo = 0, + Padding, + Application, + SeekTable, + VorbisComment, + CueSheet, + Picture + }; + + /*! + * Returns the FLAC metadata block type. + */ + virtual int code() const = 0; + + /*! + * Render the content of the block. + */ + virtual ByteVector render() const = 0; + + private: + MetadataBlock(const MetadataBlock &item); + MetadataBlock &operator=(const MetadataBlock &item); + + class MetadataBlockPrivate; + MetadataBlockPrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/flac/flacpicture.cpp b/taglib/taglib/flac/flacpicture.cpp new file mode 100644 index 0000000..a2a9000 --- /dev/null +++ b/taglib/taglib/flac/flacpicture.cpp @@ -0,0 +1,217 @@ +/************************************************************************** + copyright : (C) 2010 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <taglib.h> +#include <tdebug.h> +#include "flacpicture.h" + +using namespace TagLib; + +class FLAC::Picture::PicturePrivate +{ +public: + PicturePrivate() : + type(FLAC::Picture::Other), + width(0), + height(0), + colorDepth(0), + numColors(0) + {} + + Type type; + String mimeType; + String description; + int width; + int height; + int colorDepth; + int numColors; + ByteVector data; +}; + +FLAC::Picture::Picture() +{ + d = new PicturePrivate; +} + +FLAC::Picture::Picture(const ByteVector &data) +{ + d = new PicturePrivate; + parse(data); +} + +FLAC::Picture::~Picture() +{ + delete d; +} + +int FLAC::Picture::code() const +{ + return FLAC::MetadataBlock::Picture; +} + +bool FLAC::Picture::parse(const ByteVector &data) +{ + if(data.size() < 32) { + debug("A picture block must contain at least 5 bytes."); + return false; + } + + uint pos = 0; + d->type = FLAC::Picture::Type(data.toUInt(pos)); + pos += 4; + uint mimeTypeLength = data.toUInt(pos); + pos += 4; + if(pos + mimeTypeLength + 24 > data.size()) { + debug("Invalid picture block."); + return false; + } + d->mimeType = String(data.mid(pos, mimeTypeLength), String::UTF8); + pos += mimeTypeLength; + uint descriptionLength = data.toUInt(pos); + pos += 4; + if(pos + descriptionLength + 20 > data.size()) { + debug("Invalid picture block."); + return false; + } + d->description = String(data.mid(pos, descriptionLength), String::UTF8); + pos += descriptionLength; + d->width = data.toUInt(pos); + pos += 4; + d->height = data.toUInt(pos); + pos += 4; + d->colorDepth = data.toUInt(pos); + pos += 4; + d->numColors = data.toUInt(pos); + pos += 4; + uint dataLength = data.toUInt(pos); + pos += 4; + if(pos + dataLength > data.size()) { + debug("Invalid picture block."); + return false; + } + d->data = data.mid(pos, dataLength); + + return true; +} + +ByteVector FLAC::Picture::render() const +{ + ByteVector result; + result.append(ByteVector::fromUInt(d->type)); + ByteVector mimeTypeData = d->mimeType.data(String::UTF8); + result.append(ByteVector::fromUInt(mimeTypeData.size())); + result.append(mimeTypeData); + ByteVector descriptionData = d->description.data(String::UTF8); + result.append(ByteVector::fromUInt(descriptionData.size())); + result.append(descriptionData); + result.append(ByteVector::fromUInt(d->width)); + result.append(ByteVector::fromUInt(d->height)); + result.append(ByteVector::fromUInt(d->colorDepth)); + result.append(ByteVector::fromUInt(d->numColors)); + result.append(ByteVector::fromUInt(d->data.size())); + result.append(d->data); + return result; +} + +FLAC::Picture::Type FLAC::Picture::type() const +{ + return d->type; +} + +void FLAC::Picture::setType(FLAC::Picture::Type type) +{ + d->type = type; +} + +String FLAC::Picture::mimeType() const +{ + return d->mimeType; +} + +void FLAC::Picture::setMimeType(const String &mimeType) +{ + d->mimeType = mimeType; +} + +String FLAC::Picture::description() const +{ + return d->description; +} + +void FLAC::Picture::setDescription(const String &description) +{ + d->description = description; +} + +int FLAC::Picture::width() const +{ + return d->width; +} + +void FLAC::Picture::setWidth(int width) +{ + d->width = width; +} + +int FLAC::Picture::height() const +{ + return d->height; +} + +void FLAC::Picture::setHeight(int height) +{ + d->height = height; +} + +int FLAC::Picture::colorDepth() const +{ + return d->colorDepth; +} + +void FLAC::Picture::setColorDepth(int colorDepth) +{ + d->colorDepth = colorDepth; +} + +int FLAC::Picture::numColors() const +{ + return d->numColors; +} + +void FLAC::Picture::setNumColors(int numColors) +{ + d->numColors = numColors; +} + +ByteVector FLAC::Picture::data() const +{ + return d->data; +} + +void FLAC::Picture::setData(const ByteVector &data) +{ + d->data = data; +} + diff --git a/taglib/taglib/flac/flacpicture.h b/taglib/taglib/flac/flacpicture.h new file mode 100644 index 0000000..b6def57 --- /dev/null +++ b/taglib/taglib/flac/flacpicture.h @@ -0,0 +1,208 @@ +/************************************************************************** + copyright : (C) 2010 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_FLACPICTURE_H +#define TAGLIB_FLACPICTURE_H + +#include "tlist.h" +#include "tstring.h" +#include "tbytevector.h" +#include "taglib_export.h" +#include "flacmetadatablock.h" + +namespace TagLib { + + namespace FLAC { + + class TAGLIB_EXPORT Picture : public MetadataBlock + { + public: + + /*! + * This describes the function or content of the picture. + */ + enum Type { + //! A type not enumerated below + Other = 0x00, + //! 32x32 PNG image that should be used as the file icon + FileIcon = 0x01, + //! File icon of a different size or format + OtherFileIcon = 0x02, + //! Front cover image of the album + FrontCover = 0x03, + //! Back cover image of the album + BackCover = 0x04, + //! Inside leaflet page of the album + LeafletPage = 0x05, + //! Image from the album itself + Media = 0x06, + //! Picture of the lead artist or soloist + LeadArtist = 0x07, + //! Picture of the artist or performer + Artist = 0x08, + //! Picture of the conductor + Conductor = 0x09, + //! Picture of the band or orchestra + Band = 0x0A, + //! Picture of the composer + Composer = 0x0B, + //! Picture of the lyricist or text writer + Lyricist = 0x0C, + //! Picture of the recording location or studio + RecordingLocation = 0x0D, + //! Picture of the artists during recording + DuringRecording = 0x0E, + //! Picture of the artists during performance + DuringPerformance = 0x0F, + //! Picture from a movie or video related to the track + MovieScreenCapture = 0x10, + //! Picture of a large, coloured fish + ColouredFish = 0x11, + //! Illustration related to the track + Illustration = 0x12, + //! Logo of the band or performer + BandLogo = 0x13, + //! Logo of the publisher (record company) + PublisherLogo = 0x14 + }; + + Picture(); + Picture(const ByteVector &data); + ~Picture(); + + /*! + * Returns the type of the image. + */ + Type type() const; + + /*! + * Sets the type of the image. + */ + void setType(Type type); + + /*! + * Returns the mime type of the image. This should in most cases be + * "image/png" or "image/jpeg". + */ + String mimeType() const; + + /*! + * Sets the mime type of the image. This should in most cases be + * "image/png" or "image/jpeg". + */ + void setMimeType(const String &m); + + /*! + * Returns a text description of the image. + */ + + String description() const; + + /*! + * Sets a textual description of the image to \a desc. + */ + + void setDescription(const String &desc); + + /*! + * Returns the width of the image. + */ + int width() const; + + /*! + * Sets the width of the image. + */ + void setWidth(int w); + + /*! + * Returns the height of the image. + */ + int height() const; + + /*! + * Sets the height of the image. + */ + void setHeight(int h); + + /*! + * Returns the color depth (in bits-per-pixel) of the image. + */ + int colorDepth() const; + + /*! + * Sets the color depth (in bits-per-pixel) of the image. + */ + void setColorDepth(int depth); + + /*! + * Returns the number of colors used on the image.. + */ + int numColors() const; + + /*! + * Sets the number of colors used on the image (for indexed images). + */ + void setNumColors(int numColors); + + /*! + * Returns the image data. + */ + ByteVector data() const; + + /*! + * Sets the image data. + */ + void setData(const ByteVector &data); + + /*! + * Returns the FLAC metadata block type. + */ + int code() const; + + /*! + * Render the content to the FLAC picture block format. + */ + ByteVector render() const; + + /*! + * Parse the picture data in the FLAC picture block format. + */ + bool parse(const ByteVector &rawData); + + private: + Picture(const Picture &item); + Picture &operator=(const Picture &item); + + class PicturePrivate; + PicturePrivate *d; + }; + + typedef List<Picture> PictureList; + + } + +} + +#endif diff --git a/taglib/taglib/flac/flacproperties.cpp b/taglib/taglib/flac/flacproperties.cpp new file mode 100644 index 0000000..e591193 --- /dev/null +++ b/taglib/taglib/flac/flacproperties.cpp @@ -0,0 +1,169 @@ +/*************************************************************************** + copyright : (C) 2003 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> + +#include "flacproperties.h" +#include "flacfile.h" + +using namespace TagLib; + +class FLAC::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(ByteVector d, long st, ReadStyle s) : + data(d), + streamLength(st), + style(s), + length(0), + bitrate(0), + sampleRate(0), + sampleWidth(0), + channels(0), + sampleFrames(0) {} + + ByteVector data; + long streamLength; + ReadStyle style; + int length; + int bitrate; + int sampleRate; + int sampleWidth; + int channels; + unsigned long long sampleFrames; + ByteVector signature; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +FLAC::Properties::Properties(ByteVector data, long streamLength, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(data, streamLength, style); + read(); +} + +FLAC::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(file->streamInfoData(), file->streamLength(), style); + read(); +} + +FLAC::Properties::~Properties() +{ + delete d; +} + +int FLAC::Properties::length() const +{ + return d->length; +} + +int FLAC::Properties::bitrate() const +{ + return d->bitrate; +} + +int FLAC::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int FLAC::Properties::sampleWidth() const +{ + return d->sampleWidth; +} + +int FLAC::Properties::channels() const +{ + return d->channels; +} + +unsigned long long FLAC::Properties::sampleFrames() const +{ + return d->sampleFrames; +} + +ByteVector FLAC::Properties::signature() const +{ + return d->signature; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void FLAC::Properties::read() +{ + if(d->data.size() < 18) { + debug("FLAC::Properties::read() - FLAC properties must contain at least 18 bytes."); + return; + } + + uint pos = 0; + + // Minimum block size (in samples) + pos += 2; + + // Maximum block size (in samples) + pos += 2; + + // Minimum frame size (in bytes) + pos += 3; + + // Maximum frame size (in bytes) + pos += 3; + + uint flags = d->data.toUInt(pos, true); + pos += 4; + + d->sampleRate = flags >> 12; + d->channels = ((flags >> 9) & 7) + 1; + d->sampleWidth = ((flags >> 4) & 31) + 1; + + // The last 4 bits are the most significant 4 bits for the 36 bit + // stream length in samples. (Audio files measured in days) + + unsigned long long hi = flags & 0xf; + unsigned long long lo = d->data.toUInt(pos, true); + pos += 4; + + d->sampleFrames = (hi << 32) | lo; + + if(d->sampleRate > 0) + d->length = int(d->sampleFrames / d->sampleRate); + + // Uncompressed bitrate: + + //d->bitrate = ((d->sampleRate * d->channels) / 1000) * d->sampleWidth; + + // Real bitrate: + + d->bitrate = d->length > 0 ? ((d->streamLength * 8UL) / d->length) / 1000 : 0; + + d->signature = d->data.mid(pos, 32); +} diff --git a/taglib/taglib/flac/flacproperties.h b/taglib/taglib/flac/flacproperties.h new file mode 100644 index 0000000..c145898 --- /dev/null +++ b/taglib/taglib/flac/flacproperties.h @@ -0,0 +1,103 @@ +/*************************************************************************** + copyright : (C) 2003 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_FLACPROPERTIES_H +#define TAGLIB_FLACPROPERTIES_H + +#include "taglib_export.h" +#include "audioproperties.h" + +namespace TagLib { + + namespace FLAC { + + class File; + + //! An implementation of audio property reading for FLAC + + /*! + * This reads the data from an FLAC stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of FLAC::Properties with the data read from the + * ByteVector \a data. + */ + // BIC: switch to const reference + Properties(ByteVector data, long streamLength, ReadStyle style = Average); + + /*! + * Create an instance of FLAC::Properties with the data read from the + * FLAC::File \a file. + */ + // BIC: remove + Properties(File *file, ReadStyle style = Average); + + /*! + * Destroys this FLAC::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + /*! + * Returns the sample width as read from the FLAC identification + * header. + */ + int sampleWidth() const; + + /*! + * Return the number of sample frames + */ + unsigned long long sampleFrames() const; + + /*! + * Returns the MD5 signature of the uncompressed audio stream as read + * from the stream info header header. + */ + ByteVector signature() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/flac/flacunknownmetadatablock.cpp b/taglib/taglib/flac/flacunknownmetadatablock.cpp new file mode 100644 index 0000000..dcd5d65 --- /dev/null +++ b/taglib/taglib/flac/flacunknownmetadatablock.cpp @@ -0,0 +1,79 @@ +/************************************************************************** + copyright : (C) 2010 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <taglib.h> +#include <tdebug.h> +#include <tstring.h> +#include "flacunknownmetadatablock.h" + +using namespace TagLib; + +class FLAC::UnknownMetadataBlock::UnknownMetadataBlockPrivate +{ +public: + UnknownMetadataBlockPrivate() : code(0) {} + + int code; + ByteVector data; +}; + +FLAC::UnknownMetadataBlock::UnknownMetadataBlock(int code, const ByteVector &data) +{ + d = new UnknownMetadataBlockPrivate; + d->code = code; + //debug(String(data.toHex())); + d->data = data; +} + +FLAC::UnknownMetadataBlock::~UnknownMetadataBlock() +{ + delete d; +} + +int FLAC::UnknownMetadataBlock::code() const +{ + return d->code; +} + +void FLAC::UnknownMetadataBlock::setCode(int code) +{ + d->code = code; +} + +ByteVector FLAC::UnknownMetadataBlock::data() const +{ + return d->data; +} + +void FLAC::UnknownMetadataBlock::setData(const ByteVector &data) +{ + d->data = data; +} + +ByteVector FLAC::UnknownMetadataBlock::render() const +{ + return d->data; +} + diff --git a/taglib/taglib/flac/flacunknownmetadatablock.h b/taglib/taglib/flac/flacunknownmetadatablock.h new file mode 100644 index 0000000..3030254 --- /dev/null +++ b/taglib/taglib/flac/flacunknownmetadatablock.h @@ -0,0 +1,81 @@ +/************************************************************************** + copyright : (C) 2010 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_FLACUNKNOWNMETADATABLOCK_H +#define TAGLIB_FLACUNKNOWNMETADATABLOCK_H + +#include "tlist.h" +#include "tbytevector.h" +#include "taglib_export.h" +#include "flacmetadatablock.h" + +namespace TagLib { + + namespace FLAC { + + class TAGLIB_EXPORT UnknownMetadataBlock : public MetadataBlock + { + public: + UnknownMetadataBlock(int blockType, const ByteVector &data); + ~UnknownMetadataBlock(); + + /*! + * Returns the FLAC metadata block type. + */ + int code() const; + + /*! + * Sets the FLAC metadata block type. + */ + void setCode(int code); + + /*! + * Returns the FLAC metadata block type. + */ + ByteVector data() const; + + /*! + * Sets the FLAC metadata block type. + */ + void setData(const ByteVector &data); + + /*! + * Render the content of the block. + */ + ByteVector render() const; + + private: + UnknownMetadataBlock(const MetadataBlock &item); + UnknownMetadataBlock &operator=(const MetadataBlock &item); + + class UnknownMetadataBlockPrivate; + UnknownMetadataBlockPrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/it/itfile.cpp b/taglib/taglib/it/itfile.cpp new file mode 100644 index 0000000..ad5cf0b --- /dev/null +++ b/taglib/taglib/it/itfile.cpp @@ -0,0 +1,330 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "tstringlist.h" +#include "itfile.h" +#include "tdebug.h" +#include "modfileprivate.h" +#include "tpropertymap.h" + +using namespace TagLib; +using namespace IT; + +class IT::File::FilePrivate +{ +public: + FilePrivate(AudioProperties::ReadStyle propertiesStyle) + : tag(), properties(propertiesStyle) + { + } + + Mod::Tag tag; + IT::Properties properties; +}; + +IT::File::File(FileName file, bool readProperties, + AudioProperties::ReadStyle propertiesStyle) : + Mod::FileBase(file), + d(new FilePrivate(propertiesStyle)) +{ + if(isOpen()) + read(readProperties); +} + +IT::File::File(IOStream *stream, bool readProperties, + AudioProperties::ReadStyle propertiesStyle) : + Mod::FileBase(stream), + d(new FilePrivate(propertiesStyle)) +{ + if(isOpen()) + read(readProperties); +} + +IT::File::~File() +{ + delete d; +} + +Mod::Tag *IT::File::tag() const +{ + return &d->tag; +} + +PropertyMap IT::File::properties() const +{ + return d->tag.properties(); +} + +PropertyMap IT::File::setProperties(const PropertyMap &properties) +{ + return d->tag.setProperties(properties); +} + +IT::Properties *IT::File::audioProperties() const +{ + return &d->properties; +} + +bool IT::File::save() +{ + if(readOnly()) + { + debug("IT::File::save() - Cannot save to a read only file."); + return false; + } + seek(4); + writeString(d->tag.title(), 25); + writeByte(0); + + seek(2, Current); + + ushort length = 0; + ushort instrumentCount = 0; + ushort sampleCount = 0; + + if(!readU16L(length) || !readU16L(instrumentCount) || !readU16L(sampleCount)) + return false; + + seek(15, Current); + + // write comment as instrument and sample names: + StringList lines = d->tag.comment().split("\n"); + for(ushort i = 0; i < instrumentCount; ++ i) { + seek(192L + length + ((long)i << 2)); + ulong instrumentOffset = 0; + if(!readU32L(instrumentOffset)) + return false; + + seek(instrumentOffset + 32); + + if(i < lines.size()) + writeString(lines[i], 25); + else + writeString(String::null, 25); + writeByte(0); + } + + for(ushort i = 0; i < sampleCount; ++ i) { + seek(192L + length + ((long)instrumentCount << 2) + ((long)i << 2)); + ulong sampleOffset = 0; + if(!readU32L(sampleOffset)) + return false; + + seek(sampleOffset + 20); + + if((TagLib::uint)(i + instrumentCount) < lines.size()) + writeString(lines[i + instrumentCount], 25); + else + writeString(String::null, 25); + writeByte(0); + } + + // write rest as message: + StringList messageLines; + for(uint i = instrumentCount + sampleCount; i < lines.size(); ++ i) + messageLines.append(lines[i]); + ByteVector message = messageLines.toString("\r").data(String::Latin1); + + // it's actually not really stated if the message needs a + // terminating NUL but it does not hurt to add one: + if(message.size() > 7999) + message.resize(7999); + message.append((char)0); + + ushort special = 0; + ushort messageLength = 0; + ulong messageOffset = 0; + + seek(46); + if(!readU16L(special)) + return false; + + ulong fileSize = File::length(); + if(special & Properties::MessageAttached) { + seek(54); + if(!readU16L(messageLength) || !readU32L(messageOffset)) + return false; + + if(messageLength == 0) + messageOffset = fileSize; + } + else + { + messageOffset = fileSize; + seek(46); + writeU16L(special | 0x1); + } + + if(messageOffset + messageLength >= fileSize) { + // append new message + seek(54); + writeU16L(message.size()); + writeU32L(messageOffset); + seek(messageOffset); + writeBlock(message); + truncate(messageOffset + message.size()); + } + else { + // Only overwrite existing message. + // I'd need to parse (understand!) the whole file for more. + // Although I could just move the message to the end of file + // and let the existing one be, but that would waste space. + message.resize(messageLength, 0); + seek(messageOffset); + writeBlock(message); + } + return true; +} + +void IT::File::read(bool) +{ + if(!isOpen()) + return; + + seek(0); + READ_ASSERT(readBlock(4) == "IMPM"); + READ_STRING(d->tag.setTitle, 26); + + seek(2, Current); + + READ_U16L_AS(length); + READ_U16L_AS(instrumentCount); + READ_U16L_AS(sampleCount); + + d->properties.setInstrumentCount(instrumentCount); + d->properties.setSampleCount(sampleCount); + READ_U16L(d->properties.setPatternCount); + READ_U16L(d->properties.setVersion); + READ_U16L(d->properties.setCompatibleVersion); + READ_U16L(d->properties.setFlags); + READ_U16L_AS(special); + d->properties.setSpecial(special); + READ_BYTE(d->properties.setGlobalVolume); + READ_BYTE(d->properties.setMixVolume); + READ_BYTE(d->properties.setBpmSpeed); + READ_BYTE(d->properties.setTempo); + READ_BYTE(d->properties.setPanningSeparation); + READ_BYTE(d->properties.setPitchWheelDepth); + + // IT supports some kind of comment tag. Still, the + // sample/instrument names are abused as comments so + // I just add all together. + String message; + if(special & Properties::MessageAttached) { + READ_U16L_AS(messageLength); + READ_U32L_AS(messageOffset); + seek(messageOffset); + ByteVector messageBytes = readBlock(messageLength); + READ_ASSERT(messageBytes.size() == messageLength); + int index = messageBytes.find((char) 0); + if(index > -1) + messageBytes.resize(index, 0); + messageBytes.replace('\r', '\n'); + message = messageBytes; + } + + seek(64); + + ByteVector pannings = readBlock(64); + ByteVector volumes = readBlock(64); + READ_ASSERT(pannings.size() == 64 && volumes.size() == 64); + int channels = 0; + for(int i = 0; i < 64; ++ i) { + // Strictly speaking an IT file has always 64 channels, but + // I don't count disabled and muted channels. + // But this always gives 64 channels for all my files anyway. + // Strangely VLC does report other values. I wonder how VLC + // gets it's values. + if((unsigned char) pannings[i] < 128 && volumes[i] > 0) + ++channels; + } + d->properties.setChannels(channels); + + // real length might be shorter because of skips and terminator + ushort realLength = 0; + for(ushort i = 0; i < length; ++ i) { + READ_BYTE_AS(order); + if(order == 255) break; + if(order != 254) ++ realLength; + } + d->properties.setLengthInPatterns(realLength); + + StringList comment; + // Note: I found files that have nil characters somewhere + // in the instrument/sample names and more characters + // afterwards. The spec does not mention such a case. + // Currently I just discard anything after a nil, but + // e.g. VLC seems to interprete a nil as a space. I + // don't know what is the proper behaviour. + for(ushort i = 0; i < instrumentCount; ++ i) { + seek(192L + length + ((long)i << 2)); + READ_U32L_AS(instrumentOffset); + seek(instrumentOffset); + + ByteVector instrumentMagic = readBlock(4); + READ_ASSERT(instrumentMagic == "IMPI"); + + READ_STRING_AS(dosFileName, 13); + + seek(15, Current); + + READ_STRING_AS(instrumentName, 26); + comment.append(instrumentName); + } + + for(ushort i = 0; i < sampleCount; ++ i) { + seek(192L + length + ((long)instrumentCount << 2) + ((long)i << 2)); + READ_U32L_AS(sampleOffset); + + seek(sampleOffset); + + ByteVector sampleMagic = readBlock(4); + READ_ASSERT(sampleMagic == "IMPS"); + + READ_STRING_AS(dosFileName, 13); + READ_BYTE_AS(globalVolume); + READ_BYTE_AS(sampleFlags); + READ_BYTE_AS(sampleVolume); + READ_STRING_AS(sampleName, 26); + /* + READ_BYTE_AS(sampleCvt); + READ_BYTE_AS(samplePanning); + READ_U32L_AS(sampleLength); + READ_U32L_AS(loopStart); + READ_U32L_AS(loopStop); + READ_U32L_AS(c5speed); + READ_U32L_AS(sustainLoopStart); + READ_U32L_AS(sustainLoopEnd); + READ_U32L_AS(sampleDataOffset); + READ_BYTE_AS(vibratoSpeed); + READ_BYTE_AS(vibratoDepth); + READ_BYTE_AS(vibratoRate); + READ_BYTE_AS(vibratoType); + */ + + comment.append(sampleName); + } + + if(message.size() > 0) + comment.append(message); + d->tag.setComment(comment.toString("\n")); + d->tag.setTrackerName("Impulse Tracker"); +} diff --git a/taglib/taglib/it/itfile.h b/taglib/taglib/it/itfile.h new file mode 100644 index 0000000..19327dc --- /dev/null +++ b/taglib/taglib/it/itfile.h @@ -0,0 +1,109 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_ITFILE_H +#define TAGLIB_ITFILE_H + +#include "tfile.h" +#include "audioproperties.h" +#include "taglib_export.h" +#include "modfilebase.h" +#include "modtag.h" +#include "itproperties.h" + +namespace TagLib { + + namespace IT { + + class TAGLIB_EXPORT File : public Mod::FileBase { + public: + /*! + * Constructs a Impulse Tracker file from \a file. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + */ + File(FileName file, bool readProperties = true, + AudioProperties::ReadStyle propertiesStyle = + AudioProperties::Average); + + /*! + * Constructs a Impulse Tracker file from \a stream. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + */ + File(IOStream *stream, bool readProperties = true, + AudioProperties::ReadStyle propertiesStyle = + AudioProperties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + Mod::Tag *tag() const; + + /*! + * Forwards to Mod::Tag::properties(). + * BIC: will be removed once File::toDict() is made virtual + */ + PropertyMap properties() const; + + /*! + * Forwards to Mod::Tag::setProperties(). + * BIC: will be removed once File::setProperties() is made virtual + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the IT::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + IT::Properties *audioProperties() const; + + /*! + * Save the file. + * This is the same as calling save(AllTags); + * + * \note Saving Impulse Tracker tags is not supported. + */ + bool save(); + + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/it/itproperties.cpp b/taglib/taglib/it/itproperties.cpp new file mode 100644 index 0000000..7a328fb --- /dev/null +++ b/taglib/taglib/it/itproperties.cpp @@ -0,0 +1,245 @@ +/*************************************************************************** + copyright :(C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "itproperties.h" + +using namespace TagLib; +using namespace IT; + +class IT::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate() : + channels(0), + lengthInPatterns(0), + instrumentCount(0), + sampleCount(0), + patternCount(0), + version(0), + compatibleVersion(0), + flags(0), + special(0), + globalVolume(0), + mixVolume(0), + tempo(0), + bpmSpeed(0), + panningSeparation(0), + pitchWheelDepth(0) + { + } + + int channels; + ushort lengthInPatterns; + ushort instrumentCount; + ushort sampleCount; + ushort patternCount; + ushort version; + ushort compatibleVersion; + ushort flags; + ushort special; + uchar globalVolume; + uchar mixVolume; + uchar tempo; + uchar bpmSpeed; + uchar panningSeparation; + uchar pitchWheelDepth; +}; + +IT::Properties::Properties(AudioProperties::ReadStyle propertiesStyle) : + AudioProperties(propertiesStyle), + d(new PropertiesPrivate) +{ +} + +IT::Properties::~Properties() +{ + delete d; +} + +int IT::Properties::length() const +{ + return 0; +} + +int IT::Properties::bitrate() const +{ + return 0; +} + +int IT::Properties::sampleRate() const +{ + return 0; +} + +int IT::Properties::channels() const +{ + return d->channels; +} + +TagLib::ushort IT::Properties::lengthInPatterns() const +{ + return d->lengthInPatterns; +} + +bool IT::Properties::stereo() const +{ + return d->flags & Stereo; +} + +TagLib::ushort IT::Properties::instrumentCount() const +{ + return d->instrumentCount; +} + +TagLib::ushort IT::Properties::sampleCount() const +{ + return d->sampleCount; +} + +TagLib::ushort IT::Properties::patternCount() const +{ + return d->patternCount; +} + +TagLib::ushort IT::Properties::version() const +{ + return d->version; +} + +TagLib::ushort IT::Properties::compatibleVersion() const +{ + return d->compatibleVersion; +} + +TagLib::ushort IT::Properties::flags() const +{ + return d->flags; +} + +TagLib::ushort IT::Properties::special() const +{ + return d->special; +} + +uchar IT::Properties::globalVolume() const +{ + return d->globalVolume; +} + +uchar IT::Properties::mixVolume() const +{ + return d->mixVolume; +} + +uchar IT::Properties::tempo() const +{ + return d->tempo; +} + +uchar IT::Properties::bpmSpeed() const +{ + return d->bpmSpeed; +} + +uchar IT::Properties::panningSeparation() const +{ + return d->panningSeparation; +} + +uchar IT::Properties::pitchWheelDepth() const +{ + return d->pitchWheelDepth; +} + +void IT::Properties::setChannels(int channels) +{ + d->channels = channels; +} + +void IT::Properties::setLengthInPatterns(ushort lengthInPatterns) +{ + d->lengthInPatterns = lengthInPatterns; +} + +void IT::Properties::setInstrumentCount(ushort instrumentCount) +{ + d->instrumentCount = instrumentCount; +} + +void IT::Properties::setSampleCount(ushort sampleCount) +{ + d->sampleCount = sampleCount; +} + +void IT::Properties::setPatternCount(ushort patternCount) +{ + d->patternCount = patternCount; +} + +void IT::Properties::setFlags(ushort flags) +{ + d->flags = flags; +} + +void IT::Properties::setSpecial(ushort special) +{ + d->special = special; +} + +void IT::Properties::setCompatibleVersion(ushort compatibleVersion) +{ + d->compatibleVersion = compatibleVersion; +} + +void IT::Properties::setVersion(ushort version) +{ + d->version = version; +} + +void IT::Properties::setGlobalVolume(uchar globalVolume) +{ + d->globalVolume = globalVolume; +} + +void IT::Properties::setMixVolume(uchar mixVolume) +{ + d->mixVolume = mixVolume; +} + +void IT::Properties::setTempo(uchar tempo) +{ + d->tempo = tempo; +} + +void IT::Properties::setBpmSpeed(uchar bpmSpeed) +{ + d->bpmSpeed = bpmSpeed; +} + +void IT::Properties::setPanningSeparation(uchar panningSeparation) +{ + d->panningSeparation = panningSeparation; +} + +void IT::Properties::setPitchWheelDepth(uchar pitchWheelDepth) +{ + d->pitchWheelDepth = pitchWheelDepth; +} diff --git a/taglib/taglib/it/itproperties.h b/taglib/taglib/it/itproperties.h new file mode 100644 index 0000000..da2a7c4 --- /dev/null +++ b/taglib/taglib/it/itproperties.h @@ -0,0 +1,101 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_ITPROPERTIES_H +#define TAGLIB_ITPROPERTIES_H + +#include "taglib.h" +#include "audioproperties.h" + +namespace TagLib { + namespace IT { + class TAGLIB_EXPORT Properties : public AudioProperties { + friend class File; + public: + /*! Flag bits. */ + enum { + Stereo = 1, + Vol0MixOptimizations = 2, + UseInstruments = 4, + LinearSlides = 8, + OldEffects = 16, + LinkEffects = 32, + UseMidiPitchController = 64, + RequestEmbeddedMidiConf = 128 + }; + + /*! Special bits. */ + enum { + MessageAttached = 1, + MidiConfEmbedded = 8 + }; + + Properties(AudioProperties::ReadStyle propertiesStyle); + virtual ~Properties(); + + int length() const; + int bitrate() const; + int sampleRate() const; + int channels() const; + + ushort lengthInPatterns() const; + bool stereo() const; + ushort instrumentCount() const; + ushort sampleCount() const; + ushort patternCount() const; + ushort version() const; + ushort compatibleVersion() const; + ushort flags() const; + ushort special() const; + uchar globalVolume() const; + uchar mixVolume() const; + uchar tempo() const; + uchar bpmSpeed() const; + uchar panningSeparation() const; + uchar pitchWheelDepth() const; + + void setChannels(int channels); + void setLengthInPatterns(ushort lengthInPatterns); + void setInstrumentCount(ushort instrumentCount); + void setSampleCount (ushort sampleCount); + void setPatternCount(ushort patternCount); + void setVersion (ushort version); + void setCompatibleVersion(ushort compatibleVersion); + void setFlags (ushort flags); + void setSpecial (ushort special); + void setGlobalVolume(uchar globalVolume); + void setMixVolume (uchar mixVolume); + void setTempo (uchar tempo); + void setBpmSpeed (uchar bpmSpeed); + void setPanningSeparation(uchar panningSeparation); + void setPitchWheelDepth (uchar pitchWheelDepth); + + private: + Properties(const Properties&); + Properties &operator=(const Properties&); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mod/modfile.cpp b/taglib/taglib/mod/modfile.cpp new file mode 100644 index 0000000..ce974c1 --- /dev/null +++ b/taglib/taglib/mod/modfile.cpp @@ -0,0 +1,187 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "modfile.h" +#include "tstringlist.h" +#include "tdebug.h" +#include "modfileprivate.h" +#include "tpropertymap.h" + +using namespace TagLib; +using namespace Mod; + +class Mod::File::FilePrivate +{ +public: + FilePrivate(AudioProperties::ReadStyle propertiesStyle) + : properties(propertiesStyle) + { + } + + Mod::Tag tag; + Mod::Properties properties; +}; + +Mod::File::File(FileName file, bool readProperties, + AudioProperties::ReadStyle propertiesStyle) : + Mod::FileBase(file), + d(new FilePrivate(propertiesStyle)) +{ + if(isOpen()) + read(readProperties); +} + +Mod::File::File(IOStream *stream, bool readProperties, + AudioProperties::ReadStyle propertiesStyle) : + Mod::FileBase(stream), + d(new FilePrivate(propertiesStyle)) +{ + if(isOpen()) + read(readProperties); +} + +Mod::File::~File() +{ + delete d; +} + +Mod::Tag *Mod::File::tag() const +{ + return &d->tag; +} + +Mod::Properties *Mod::File::audioProperties() const +{ + return &d->properties; +} + +PropertyMap Mod::File::properties() const +{ + return d->tag.properties(); +} + +PropertyMap Mod::File::setProperties(const PropertyMap &properties) +{ + return d->tag.setProperties(properties); +} + +bool Mod::File::save() +{ + if(readOnly()) { + debug("Mod::File::save() - Cannot save to a read only file."); + return false; + } + seek(0); + writeString(d->tag.title(), 20); + StringList lines = d->tag.comment().split("\n"); + uint n = std::min(lines.size(), d->properties.instrumentCount()); + for(uint i = 0; i < n; ++ i) { + writeString(lines[i], 22); + seek(8, Current); + } + + for(uint i = n; i < d->properties.instrumentCount(); ++ i) { + writeString(String::null, 22); + seek(8, Current); + } + return true; +} + +void Mod::File::read(bool) +{ + if(!isOpen()) + return; + + seek(1080); + ByteVector modId = readBlock(4); + READ_ASSERT(modId.size() == 4); + + int channels = 4; + uint instruments = 31; + if(modId == "M.K." || modId == "M!K!" || modId == "M&K!" || modId == "N.T.") { + d->tag.setTrackerName("ProTracker"); + channels = 4; + } + else if(modId.startsWith("FLT") || modId.startsWith("TDZ")) { + d->tag.setTrackerName("StarTrekker"); + char digit = modId[3]; + READ_ASSERT(digit >= '0' && digit <= '9'); + channels = digit - '0'; + } + else if(modId.endsWith("CHN")) { + d->tag.setTrackerName("StarTrekker"); + char digit = modId[0]; + READ_ASSERT(digit >= '0' && digit <= '9'); + channels = digit - '0'; + } + else if(modId == "CD81" || modId == "OKTA") { + d->tag.setTrackerName("Atari Oktalyzer"); + channels = 8; + } + else if(modId.endsWith("CH") || modId.endsWith("CN")) { + d->tag.setTrackerName("TakeTracker"); + char digit = modId[0]; + READ_ASSERT(digit >= '0' && digit <= '9'); + channels = (digit - '0') * 10; + digit = modId[1]; + READ_ASSERT(digit >= '0' && digit <= '9'); + channels += digit - '0'; + } + else { + // Not sure if this is correct. I'd need a file + // created with NoiseTracker to check this. + d->tag.setTrackerName("NoiseTracker"); // probably + channels = 4; + instruments = 15; + } + d->properties.setChannels(channels); + d->properties.setInstrumentCount(instruments); + + seek(0); + READ_STRING(d->tag.setTitle, 20); + + StringList comment; + for(uint i = 0; i < instruments; ++ i) { + READ_STRING_AS(instrumentName, 22); + // value in words, * 2 (<< 1) for bytes: + READ_U16B_AS(sampleLength); + + READ_BYTE_AS(fineTuneByte); + int fineTune = fineTuneByte & 0xF; + // > 7 means negative value + if(fineTune > 7) fineTune -= 16; + + READ_BYTE_AS(volume); + if(volume > 64) volume = 64; + // volume in decibels: 20 * log10(volume / 64) + + // value in words, * 2 (<< 1) for bytes: + READ_U16B_AS(repeatStart); + // value in words, * 2 (<< 1) for bytes: + READ_U16B_AS(repatLength); + + comment.append(instrumentName); + } + + READ_BYTE(d->properties.setLengthInPatterns); + + d->tag.setComment(comment.toString("\n")); +} diff --git a/taglib/taglib/mod/modfile.h b/taglib/taglib/mod/modfile.h new file mode 100644 index 0000000..c45ede2 --- /dev/null +++ b/taglib/taglib/mod/modfile.h @@ -0,0 +1,110 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_MODFILE_H +#define TAGLIB_MODFILE_H + +#include "tfile.h" +#include "audioproperties.h" +#include "taglib_export.h" +#include "modfilebase.h" +#include "modtag.h" +#include "modproperties.h" + +namespace TagLib { + + namespace Mod { + + class TAGLIB_EXPORT File : public TagLib::Mod::FileBase + { + public: + /*! + * Constructs a Protracker file from \a file. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + */ + File(FileName file, bool readProperties = true, + AudioProperties::ReadStyle propertiesStyle = + AudioProperties::Average); + + /*! + * Constructs a Protracker file from \a stream. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + */ + File(IOStream *stream, bool readProperties = true, + AudioProperties::ReadStyle propertiesStyle = + AudioProperties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + Mod::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * Forwards to Mod::Tag::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Forwards to Mod::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + /*! + * Returns the Mod::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + Mod::Properties *audioProperties() const; + + /*! + * Save the file. + * This is the same as calling save(AllTags); + * + * \note Saving Protracker tags is not supported. + */ + bool save(); + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties); + + class FilePrivate; + FilePrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/mod/modfilebase.cpp b/taglib/taglib/mod/modfilebase.cpp new file mode 100644 index 0000000..e074dac --- /dev/null +++ b/taglib/taglib/mod/modfilebase.cpp @@ -0,0 +1,120 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "tdebug.h" +#include "modfilebase.h" + +using namespace TagLib; +using namespace Mod; + +Mod::FileBase::FileBase(FileName file) : TagLib::File(file) +{ +} + +Mod::FileBase::FileBase(IOStream *stream) : TagLib::File(stream) +{ +} + +void Mod::FileBase::writeString(const String &s, ulong size, char padding) +{ + ByteVector data(s.data(String::Latin1)); + data.resize(size, padding); + writeBlock(data); +} + +bool Mod::FileBase::readString(String &s, ulong size) +{ + ByteVector data(readBlock(size)); + if(data.size() < size) return false; + int index = data.find((char) 0); + if(index > -1) + { + data.resize(index); + } + data.replace((char) 0xff, ' '); + + s = data; + return true; +} + +void Mod::FileBase::writeByte(uchar byte) +{ + ByteVector data(1, byte); + writeBlock(data); +} + +void Mod::FileBase::writeU16L(ushort number) +{ + writeBlock(ByteVector::fromShort(number, false)); +} + +void Mod::FileBase::writeU32L(ulong number) +{ + writeBlock(ByteVector::fromUInt(number, false)); +} + +void Mod::FileBase::writeU16B(ushort number) +{ + writeBlock(ByteVector::fromShort(number, true)); +} + +void Mod::FileBase::writeU32B(ulong number) +{ + writeBlock(ByteVector::fromUInt(number, true)); +} + +bool Mod::FileBase::readByte(uchar &byte) +{ + ByteVector data(readBlock(1)); + if(data.size() < 1) return false; + byte = data[0]; + return true; +} + +bool Mod::FileBase::readU16L(ushort &number) +{ + ByteVector data(readBlock(2)); + if(data.size() < 2) return false; + number = data.toUShort(false); + return true; +} + +bool Mod::FileBase::readU32L(ulong &number) { + ByteVector data(readBlock(4)); + if(data.size() < 4) return false; + number = data.toUInt(false); + return true; +} + +bool Mod::FileBase::readU16B(ushort &number) +{ + ByteVector data(readBlock(2)); + if(data.size() < 2) return false; + number = data.toUShort(true); + return true; +} + +bool Mod::FileBase::readU32B(ulong &number) { + ByteVector data(readBlock(4)); + if(data.size() < 4) return false; + number = data.toUInt(true); + return true; +} diff --git a/taglib/taglib/mod/modfilebase.h b/taglib/taglib/mod/modfilebase.h new file mode 100644 index 0000000..383bde1 --- /dev/null +++ b/taglib/taglib/mod/modfilebase.h @@ -0,0 +1,62 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_MODFILEBASE_H +#define TAGLIB_MODFILEBASE_H + +#include "taglib.h" +#include "tfile.h" +#include "tstring.h" +#include "tlist.h" +#include "taglib_export.h" + +#include <algorithm> + +namespace TagLib { + + namespace Mod { + + class TAGLIB_EXPORT FileBase : public TagLib::File + { + protected: + FileBase(FileName file); + FileBase(IOStream *stream); + + void writeString(const String &s, ulong size, char padding = 0); + void writeByte(uchar byte); + void writeU16L(ushort number); + void writeU32L(ulong number); + void writeU16B(ushort number); + void writeU32B(ulong number); + + bool readString(String &s, ulong size); + bool readByte(uchar &byte); + bool readU16L(ushort &number); + bool readU32L(ulong &number); + bool readU16B(ushort &number); + bool readU32B(ulong &number); + }; + + } + +} + +#endif diff --git a/taglib/taglib/mod/modfileprivate.h b/taglib/taglib/mod/modfileprivate.h new file mode 100644 index 0000000..19a3001 --- /dev/null +++ b/taglib/taglib/mod/modfileprivate.h @@ -0,0 +1,67 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_MODFILEPRIVATE_H +#define TAGLIB_MODFILEPRIVATE_H + +// some helper-macros only used internally by (s3m|it|xm)file.cpp +#define READ_ASSERT(cond) \ + if(!(cond)) \ + { \ + setValid(false); \ + return; \ + } + +#define READ(setter,type,read) \ + { \ + type number; \ + READ_ASSERT(read(number)); \ + setter(number); \ + } + +#define READ_BYTE(setter) READ(setter,uchar,readByte) +#define READ_U16L(setter) READ(setter,ushort,readU16L) +#define READ_U32L(setter) READ(setter,ulong,readU32L) +#define READ_U16B(setter) READ(setter,ushort,readU16B) +#define READ_U32B(setter) READ(setter,ulong,readU32B) + +#define READ_STRING(setter,size) \ + { \ + String s; \ + READ_ASSERT(readString(s, size)); \ + setter(s); \ + } + +#define READ_AS(type,name,read) \ + type name = 0; \ + READ_ASSERT(read(name)); + +#define READ_BYTE_AS(name) READ_AS(uchar,name,readByte) +#define READ_U16L_AS(name) READ_AS(ushort,name,readU16L) +#define READ_U32L_AS(name) READ_AS(ulong,name,readU32L) +#define READ_U16B_AS(name) READ_AS(ushort,name,readU16B) +#define READ_U32B_AS(name) READ_AS(ulong,name,readU32B) + +#define READ_STRING_AS(name,size) \ + String name; \ + READ_ASSERT(readString(name, size)); + +#endif diff --git a/taglib/taglib/mod/modproperties.cpp b/taglib/taglib/mod/modproperties.cpp new file mode 100644 index 0000000..4d3c354 --- /dev/null +++ b/taglib/taglib/mod/modproperties.cpp @@ -0,0 +1,96 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "modproperties.h" + +using namespace TagLib; +using namespace Mod; + +class Mod::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate() : + channels(0), + instrumentCount(0), + lengthInPatterns(0) + { + } + + int channels; + uint instrumentCount; + uchar lengthInPatterns; +}; + +Mod::Properties::Properties(AudioProperties::ReadStyle propertiesStyle) : + AudioProperties(propertiesStyle), + d(new PropertiesPrivate) +{ +} + +Mod::Properties::~Properties() +{ + delete d; +} + +int Mod::Properties::length() const +{ + return 0; +} + +int Mod::Properties::bitrate() const +{ + return 0; +} + +int Mod::Properties::sampleRate() const +{ + return 0; +} + +int Mod::Properties::channels() const +{ + return d->channels; +} + +TagLib::uint Mod::Properties::instrumentCount() const +{ + return d->instrumentCount; +} + +uchar Mod::Properties::lengthInPatterns() const +{ + return d->lengthInPatterns; +} + +void Mod::Properties::setChannels(int channels) +{ + d->channels = channels; +} + +void Mod::Properties::setInstrumentCount(uint instrumentCount) +{ + d->instrumentCount = instrumentCount; +} + +void Mod::Properties::setLengthInPatterns(uchar lengthInPatterns) +{ + d->lengthInPatterns = lengthInPatterns; +} diff --git a/taglib/taglib/mod/modproperties.h b/taglib/taglib/mod/modproperties.h new file mode 100644 index 0000000..d50353a --- /dev/null +++ b/taglib/taglib/mod/modproperties.h @@ -0,0 +1,65 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_MODPROPERTIES_H +#define TAGLIB_MODPROPERTIES_H + +#include "taglib.h" +#include "audioproperties.h" + +namespace TagLib { + + namespace Mod { + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + Properties(AudioProperties::ReadStyle propertiesStyle); + virtual ~Properties(); + + int length() const; + int bitrate() const; + int sampleRate() const; + int channels() const; + + uint instrumentCount() const; + uchar lengthInPatterns() const; + + void setChannels(int channels); + + void setInstrumentCount(uint sampleCount); + void setLengthInPatterns(uchar lengthInPatterns); + + private: + friend class File; + + Properties(const Properties&); + Properties &operator=(const Properties&); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/mod/modtag.cpp b/taglib/taglib/mod/modtag.cpp new file mode 100644 index 0000000..14c4c74 --- /dev/null +++ b/taglib/taglib/mod/modtag.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "modtag.h" +#include "tstringlist.h" +#include "tpropertymap.h" + +using namespace TagLib; +using namespace Mod; + +class Mod::Tag::TagPrivate +{ +public: + TagPrivate() + { + } + + String title; + String comment; + String trackerName; +}; + +Mod::Tag::Tag() : TagLib::Tag() +{ + d = new TagPrivate; +} + +Mod::Tag::~Tag() +{ + delete d; +} + +String Mod::Tag::title() const +{ + return d->title; +} + +String Mod::Tag::artist() const +{ + return String::null; +} + +String Mod::Tag::album() const +{ + return String::null; +} + +String Mod::Tag::comment() const +{ + return d->comment; +} + +String Mod::Tag::genre() const +{ + return String::null; +} + +TagLib::uint Mod::Tag::year() const +{ + return 0; +} + +TagLib::uint Mod::Tag::track() const +{ + return 0; +} + +String Mod::Tag::trackerName() const +{ + return d->trackerName; +} + +void Mod::Tag::setTitle(const String &title) +{ + d->title = title; +} + +void Mod::Tag::setArtist(const String &) +{ +} + +void Mod::Tag::setAlbum(const String &) +{ +} + +void Mod::Tag::setComment(const String &comment) +{ + d->comment = comment; +} + +void Mod::Tag::setGenre(const String &) +{ +} + +void Mod::Tag::setYear(uint) +{ +} + +void Mod::Tag::setTrack(uint) +{ +} + +void Mod::Tag::setTrackerName(const String &trackerName) +{ + d->trackerName = trackerName; +} + +PropertyMap Mod::Tag::properties() const +{ + PropertyMap properties; + properties["TITLE"] = d->title; + properties["COMMENT"] = d->comment; + if(!(d->trackerName.isNull())) + properties["TRACKERNAME"] = d->trackerName; + return properties; +} + +PropertyMap Mod::Tag::setProperties(const PropertyMap &origProps) +{ + PropertyMap properties(origProps); + properties.removeEmpty(); + StringList oneValueSet; + if(properties.contains("TITLE")) { + d->title = properties["TITLE"].front(); + oneValueSet.append("TITLE"); + } else + d->title = String::null; + + if(properties.contains("COMMENT")) { + d->comment = properties["COMMENT"].front(); + oneValueSet.append("COMMENT"); + } else + d->comment = String::null; + + if(properties.contains("TRACKERNAME")) { + d->trackerName = properties["TRACKERNAME"].front(); + oneValueSet.append("TRACKERNAME"); + } else + d->trackerName = String::null; + + // for each tag that has been set above, remove the first entry in the corresponding + // value list. The others will be returned as unsupported by this format. + for(StringList::Iterator it = oneValueSet.begin(); it != oneValueSet.end(); ++it) { + if(properties[*it].size() == 1) + properties.erase(*it); + else + properties[*it].erase( properties[*it].begin() ); + } + return properties; +} diff --git a/taglib/taglib/mod/modtag.h b/taglib/taglib/mod/modtag.h new file mode 100644 index 0000000..f33e33f --- /dev/null +++ b/taglib/taglib/mod/modtag.h @@ -0,0 +1,190 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_MODTAG_H +#define TAGLIB_MODTAG_H + +#include "tag.h" + +namespace TagLib { + + namespace Mod { + + /*! + * Tags for module files (Mod, S3M, IT, XM). + * + * Note that only the \a title is supported as such by most + * module file formats. Except for XM files the \a trackerName + * is derived from the file format or the flavour of the file + * format. For XM files it is stored in the file. + * + * The \a comment tag is not strictly supported by module files, + * but it is common practice to abuse instrument/sample/pattern + * names as multiline comments. TagLib does so as well. + */ + class TAGLIB_EXPORT Tag : public TagLib::Tag + { + public: + Tag(); + virtual ~Tag(); + + /*! + * Returns the track name; if no track name is present in the tag + * String::null will be returned. + */ + String title() const; + + /*! + * Not supported by module files. Therefore always returns String::null. + */ + String artist() const; + + /*! + * Not supported by module files. Therefore always returns String::null. + */ + String album() const; + + /*! + * Returns the track comment derived from the instrument/sample/pattern + * names; if no comment is present in the tag String::null will be + * returned. + */ + String comment() const; + + /*! + * Not supported by module files. Therefore always returns String::null. + */ + String genre() const; + + /*! + * Not supported by module files. Therefore always returns 0. + */ + uint year() const; + + /*! + * Not supported by module files. Therefore always returns 0. + */ + uint track() const; + + /*! + * Returns the name of the tracker used to create/edit the module file. + * Only XM files store this tag to the file as such, for other formats + * (Mod, S3M, IT) this is derived from the file type or the flavour of + * the file type. Therefore only XM files might have an empty + * (String::null) tracker name. + */ + String trackerName() const; + + /*! + * Sets the title to \a title. If \a title is String::null then this + * value will be cleared. + * + * The length limits per file type are (1 characetr = 1 byte): + * Mod 20 characters, S3M 27 characters, IT 25 characters and XM 20 + * characters. + */ + void setTitle(const String &title); + + /*! + * Not supported by module files and therefore ignored. + */ + void setArtist(const String &artist); + + /*! + * Not supported by module files and therefore ignored. + */ + void setAlbum(const String &album); + + /*! + * Sets the comment to \a comment. If \a comment is String::null then + * this value will be cleared. + * + * Note that module file formats don't actually support a comment tag. + * Instead the names of instruments/patterns/samples are abused as + * a multiline comment. Because of this the number of lines in a + * module file is fixed to the number of instruments/patterns/samples. + * + * Also note that the instrument/pattern/sample name length is limited + * an thus the line length in comments are limited. Too big comments + * will be truncated. + * + * The line length limits per file type are (1 characetr = 1 byte): + * Mod 22 characters, S3M 27 characters, IT 25 characters and XM 22 + * characters. + */ + void setComment(const String &comment); + + /*! + * Not supported by module files and therefore ignored. + */ + void setGenre(const String &genre); + + /*! + * Not supported by module files and therefore ignored. + */ + void setYear(uint year); + + /*! + * Not supported by module files and therefore ignored. + */ + void setTrack(uint track); + + /*! + * Sets the tracker name to \a trackerName. If \a trackerName is + * String::null then this value will be cleared. + * + * Note that only XM files support this tag. Setting the + * tracker name for other module file formats will be ignored. + * + * The length of this tag is limited to 20 characters (1 character + * = 1 byte). + */ + void setTrackerName(const String &trackerName); + + /*! + * Implements the unified property interface -- export function. + * Since the module tag is very limited, the exported map is as well. + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Because of the limitations of the module file tag, any tags besides + * COMMENT, TITLE and, if it is an XM file, TRACKERNAME, will be + * returened. Additionally, if the map contains tags with multiple values, + * all but the first will be contained in the returned map of unsupported + * properties. + */ + PropertyMap setProperties(const PropertyMap &); + + private: + Tag(const Tag &); + Tag &operator=(const Tag &); + + class TagPrivate; + TagPrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/mp4/mp4atom.cpp b/taglib/taglib/mp4/mp4atom.cpp new file mode 100644 index 0000000..7b87a47 --- /dev/null +++ b/taglib/taglib/mp4/mp4atom.cpp @@ -0,0 +1,194 @@ +/************************************************************************** + copyright : (C) 2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tstring.h> +#include "mp4atom.h" + +using namespace TagLib; + +const char *MP4::Atom::containers[11] = { + "moov", "udta", "mdia", "meta", "ilst", + "stbl", "minf", "moof", "traf", "trak", + "stsd" +}; + +MP4::Atom::Atom(File *file) +{ + offset = file->tell(); + ByteVector header = file->readBlock(8); + if (header.size() != 8) { + // The atom header must be 8 bytes long, otherwise there is either + // trailing garbage or the file is truncated + debug("MP4: Couldn't read 8 bytes of data for atom header"); + length = 0; + file->seek(0, File::End); + return; + } + + length = header.toUInt(); + + if (length == 1) { + const long long longLength = file->readBlock(8).toLongLong(); + if (longLength >= 8 && longLength <= 0xFFFFFFFF) { + // The atom has a 64-bit length, but it's actually a 32-bit value + length = (long)longLength; + } + else { + debug("MP4: 64-bit atoms are not supported"); + length = 0; + file->seek(0, File::End); + return; + } + } + if (length < 8) { + debug("MP4: Invalid atom size"); + length = 0; + file->seek(0, File::End); + return; + } + + name = header.mid(4, 4); + + for(int i = 0; i < numContainers; i++) { + if(name == containers[i]) { + if(name == "meta") { + file->seek(4, File::Current); + } + else if(name == "stsd") { + file->seek(8, File::Current); + } + while(file->tell() < offset + length) { + MP4::Atom *child = new MP4::Atom(file); + children.append(child); + if (child->length == 0) + return; + } + return; + } + } + + file->seek(offset + length); +} + +MP4::Atom::~Atom() +{ + for(unsigned int i = 0; i < children.size(); i++) { + delete children[i]; + } + children.clear(); +} + +MP4::Atom * +MP4::Atom::find(const char *name1, const char *name2, const char *name3, const char *name4) +{ + if(name1 == 0) { + return this; + } + for(unsigned int i = 0; i < children.size(); i++) { + if(children[i]->name == name1) { + return children[i]->find(name2, name3, name4); + } + } + return 0; +} + +MP4::AtomList +MP4::Atom::findall(const char *name, bool recursive) +{ + MP4::AtomList result; + for(unsigned int i = 0; i < children.size(); i++) { + if(children[i]->name == name) { + result.append(children[i]); + } + if(recursive) { + result.append(children[i]->findall(name, recursive)); + } + } + return result; +} + +bool +MP4::Atom::path(MP4::AtomList &path, const char *name1, const char *name2, const char *name3) +{ + path.append(this); + if(name1 == 0) { + return true; + } + for(unsigned int i = 0; i < children.size(); i++) { + if(children[i]->name == name1) { + return children[i]->path(path, name2, name3); + } + } + return false; +} + +MP4::Atoms::Atoms(File *file) +{ + file->seek(0, File::End); + long end = file->tell(); + file->seek(0); + while(file->tell() + 8 <= end) { + MP4::Atom *atom = new MP4::Atom(file); + atoms.append(atom); + if (atom->length == 0) + break; + } +} + +MP4::Atoms::~Atoms() +{ + for(unsigned int i = 0; i < atoms.size(); i++) { + delete atoms[i]; + } + atoms.clear(); +} + +MP4::Atom * +MP4::Atoms::find(const char *name1, const char *name2, const char *name3, const char *name4) +{ + for(unsigned int i = 0; i < atoms.size(); i++) { + if(atoms[i]->name == name1) { + return atoms[i]->find(name2, name3, name4); + } + } + return 0; +} + +MP4::AtomList +MP4::Atoms::path(const char *name1, const char *name2, const char *name3, const char *name4) +{ + MP4::AtomList path; + for(unsigned int i = 0; i < atoms.size(); i++) { + if(atoms[i]->name == name1) { + if(!atoms[i]->path(path, name2, name3, name4)) { + path.clear(); + } + return path; + } + } + return path; +} + diff --git a/taglib/taglib/mp4/mp4atom.h b/taglib/taglib/mp4/mp4atom.h new file mode 100644 index 0000000..ea5091a --- /dev/null +++ b/taglib/taglib/mp4/mp4atom.h @@ -0,0 +1,111 @@ +/************************************************************************** + copyright : (C) 2007,2011 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +// This file is not part of the public API! + +#ifndef DO_NOT_DOCUMENT + +#ifndef TAGLIB_MP4ATOM_H +#define TAGLIB_MP4ATOM_H + +#include "tfile.h" +#include "tlist.h" + +namespace TagLib { + + namespace MP4 { + + class Atom; + typedef TagLib::List<Atom *> AtomList; + + enum AtomDataType + { + TypeImplicit = 0, // for use with tags for which no type needs to be indicated because only one type is allowed + TypeUTF8 = 1, // without any count or null terminator + TypeUTF16 = 2, // also known as UTF-16BE + TypeSJIS = 3, // deprecated unless it is needed for special Japanese characters + TypeHTML = 6, // the HTML file header specifies which HTML version + TypeXML = 7, // the XML header must identify the DTD or schemas + TypeUUID = 8, // also known as GUID; stored as 16 bytes in binary (valid as an ID) + TypeISRC = 9, // stored as UTF-8 text (valid as an ID) + TypeMI3P = 10, // stored as UTF-8 text (valid as an ID) + TypeGIF = 12, // (deprecated) a GIF image + TypeJPEG = 13, // a JPEG image + TypePNG = 14, // a PNG image + TypeURL = 15, // absolute, in UTF-8 characters + TypeDuration = 16, // in milliseconds, 32-bit integer + TypeDateTime = 17, // in UTC, counting seconds since midnight, January 1, 1904; 32 or 64-bits + TypeGenred = 18, // a list of enumerated values + TypeInteger = 21, // a signed big-endian integer with length one of { 1,2,3,4,8 } bytes + TypeRIAAPA = 24, // RIAA parental advisory; { -1=no, 1=yes, 0=unspecified }, 8-bit ingteger + TypeUPC = 25, // Universal Product Code, in text UTF-8 format (valid as an ID) + TypeBMP = 27, // Windows bitmap image + TypeUndefined = 255 // undefined + }; + + struct AtomData { + AtomData(AtomDataType type, ByteVector data) : type(type), locale(0), data(data) {} + AtomDataType type; + int locale; + ByteVector data; + }; + + typedef TagLib::List<AtomData> AtomDataList; + + class Atom + { + public: + Atom(File *file); + ~Atom(); + Atom *find(const char *name1, const char *name2 = 0, const char *name3 = 0, const char *name4 = 0); + bool path(AtomList &path, const char *name1, const char *name2 = 0, const char *name3 = 0); + AtomList findall(const char *name, bool recursive = false); + long offset; + long length; + TagLib::ByteVector name; + AtomList children; + private: + static const int numContainers = 11; + static const char *containers[11]; + }; + + //! Root-level atoms + class Atoms + { + public: + Atoms(File *file); + ~Atoms(); + Atom *find(const char *name1, const char *name2 = 0, const char *name3 = 0, const char *name4 = 0); + AtomList path(const char *name1, const char *name2 = 0, const char *name3 = 0, const char *name4 = 0); + AtomList atoms; + }; + + } + +} + +#endif + +#endif diff --git a/taglib/taglib/mp4/mp4coverart.cpp b/taglib/taglib/mp4/mp4coverart.cpp new file mode 100644 index 0000000..2746469 --- /dev/null +++ b/taglib/taglib/mp4/mp4coverart.cpp @@ -0,0 +1,83 @@ +/************************************************************************** + copyright : (C) 2009 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <taglib.h> +#include <tdebug.h> +#include "trefcounter.h" +#include "mp4coverart.h" + +using namespace TagLib; + +class MP4::CoverArt::CoverArtPrivate : public RefCounter +{ +public: + CoverArtPrivate() : RefCounter(), format(MP4::CoverArt::JPEG) {} + + Format format; + ByteVector data; +}; + +MP4::CoverArt::CoverArt(Format format, const ByteVector &data) +{ + d = new CoverArtPrivate; + d->format = format; + d->data = data; +} + +MP4::CoverArt::CoverArt(const CoverArt &item) : d(item.d) +{ + d->ref(); +} + +MP4::CoverArt & +MP4::CoverArt::operator=(const CoverArt &item) +{ + if(d->deref()) { + delete d; + } + d = item.d; + d->ref(); + return *this; +} + +MP4::CoverArt::~CoverArt() +{ + if(d->deref()) { + delete d; + } +} + +MP4::CoverArt::Format +MP4::CoverArt::format() const +{ + return d->format; +} + +ByteVector +MP4::CoverArt::data() const +{ + return d->data; +} + diff --git a/taglib/taglib/mp4/mp4coverart.h b/taglib/taglib/mp4/mp4coverart.h new file mode 100644 index 0000000..64115b4 --- /dev/null +++ b/taglib/taglib/mp4/mp4coverart.h @@ -0,0 +1,75 @@ +/************************************************************************** + copyright : (C) 2009 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MP4COVERART_H +#define TAGLIB_MP4COVERART_H + +#include "tlist.h" +#include "tbytevector.h" +#include "taglib_export.h" +#include "mp4atom.h" + +namespace TagLib { + + namespace MP4 { + + class TAGLIB_EXPORT CoverArt + { + public: + /*! + * This describes the image type. + */ + enum Format { + JPEG = TypeJPEG, + PNG = TypePNG, + BMP = TypeBMP, + GIF = TypeGIF, + Unknown = TypeImplicit, + }; + + CoverArt(Format format, const ByteVector &data); + ~CoverArt(); + + CoverArt(const CoverArt &item); + CoverArt &operator=(const CoverArt &item); + + //! Format of the image + Format format() const; + + //! The image data + ByteVector data() const; + + private: + class CoverArtPrivate; + CoverArtPrivate *d; + }; + + typedef List<CoverArt> CoverArtList; + + } + +} + +#endif diff --git a/taglib/taglib/mp4/mp4file.cpp b/taglib/taglib/mp4/mp4file.cpp new file mode 100644 index 0000000..aab1a2b --- /dev/null +++ b/taglib/taglib/mp4/mp4file.cpp @@ -0,0 +1,163 @@ +/************************************************************************** + copyright : (C) 2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tstring.h> +#include <tpropertymap.h> +#include "mp4atom.h" +#include "mp4tag.h" +#include "mp4file.h" + +using namespace TagLib; + +class MP4::File::FilePrivate +{ +public: + FilePrivate() : tag(0), atoms(0), properties(0) + { + } + + ~FilePrivate() + { + if(atoms) { + delete atoms; + atoms = 0; + } + if(tag) { + delete tag; + tag = 0; + } + if(properties) { + delete properties; + properties = 0; + } + } + + MP4::Tag *tag; + MP4::Atoms *atoms; + MP4::Properties *properties; +}; + +MP4::File::File(FileName file, bool readProperties, AudioProperties::ReadStyle audioPropertiesStyle) + : TagLib::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, audioPropertiesStyle); +} + +MP4::File::File(IOStream *stream, bool readProperties, AudioProperties::ReadStyle audioPropertiesStyle) + : TagLib::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, audioPropertiesStyle); +} + +MP4::File::~File() +{ + delete d; +} + +MP4::Tag * +MP4::File::tag() const +{ + return d->tag; +} + +PropertyMap MP4::File::properties() const +{ + return d->tag->properties(); +} + +void MP4::File::removeUnsupportedProperties(const StringList &properties) +{ + d->tag->removeUnsupportedProperties(properties); +} + +PropertyMap MP4::File::setProperties(const PropertyMap &properties) +{ + return d->tag->setProperties(properties); +} + +MP4::Properties * +MP4::File::audioProperties() const +{ + return d->properties; +} + +bool +MP4::File::checkValid(const MP4::AtomList &list) +{ + for(uint i = 0; i < list.size(); i++) { + if(list[i]->length == 0) + return false; + if(!checkValid(list[i]->children)) + return false; + } + return true; +} + +void +MP4::File::read(bool readProperties, Properties::ReadStyle audioPropertiesStyle) +{ + if(!isValid()) + return; + + d->atoms = new Atoms(this); + if (!checkValid(d->atoms->atoms)) { + setValid(false); + return; + } + + // must have a moov atom, otherwise consider it invalid + MP4::Atom *moov = d->atoms->find("moov"); + if(!moov) { + setValid(false); + return; + } + + d->tag = new Tag(this, d->atoms); + if(readProperties) { + d->properties = new Properties(this, d->atoms, audioPropertiesStyle); + } +} + +bool +MP4::File::save() +{ + if(readOnly()) { + debug("MP4::File::save() -- File is read only."); + return false; + } + + if(!isValid()) { + debug("MP4::File::save() -- Trying to save invalid file."); + return false; + } + + return d->tag->save(); +} + diff --git a/taglib/taglib/mp4/mp4file.h b/taglib/taglib/mp4/mp4file.h new file mode 100644 index 0000000..39693d3 --- /dev/null +++ b/taglib/taglib/mp4/mp4file.h @@ -0,0 +1,130 @@ +/************************************************************************** + copyright : (C) 2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MP4FILE_H +#define TAGLIB_MP4FILE_H + +#include "tag.h" +#include "tfile.h" +#include "taglib_export.h" +#include "mp4properties.h" +#include "mp4tag.h" + +namespace TagLib { + + //! An implementation of MP4 (AAC, ALAC, ...) metadata + namespace MP4 { + + class Atoms; + + /*! + * This implements and provides an interface for MP4 files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to MP4 files. + */ + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * Constructs an MP4 file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle audioPropertiesStyle = Properties::Average); + + /*! + * Constructs an MP4 file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle audioPropertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns a pointer to the MP4 tag of the file. + * + * MP4::Tag implements the tag interface, so this serves as the + * reimplementation of TagLib::File::tag(). + * + * \note The Tag <b>is still</b> owned by the MP4::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + */ + Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + */ + PropertyMap properties() const; + + /*! + * Removes unsupported properties. Forwards to the actual Tag's + * removeUnsupportedProperties() function. + */ + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the MP4 audio properties for this file. + */ + Properties *audioProperties() const; + + /*! + * Save the file. + * + * This returns true if the save was successful. + */ + bool save(); + + private: + + void read(bool readProperties, Properties::ReadStyle audioPropertiesStyle); + bool checkValid(const MP4::AtomList &list); + + class FilePrivate; + FilePrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/mp4/mp4item.cpp b/taglib/taglib/mp4/mp4item.cpp new file mode 100644 index 0000000..671f26b --- /dev/null +++ b/taglib/taglib/mp4/mp4item.cpp @@ -0,0 +1,206 @@ +/************************************************************************** + copyright : (C) 2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <taglib.h> +#include <tdebug.h> +#include "trefcounter.h" +#include "mp4item.h" + +using namespace TagLib; + +class MP4::Item::ItemPrivate : public RefCounter +{ +public: + ItemPrivate() : RefCounter(), valid(true), atomDataType(TypeUndefined) {} + + bool valid; + AtomDataType atomDataType; + union { + bool m_bool; + int m_int; + IntPair m_intPair; + uchar m_byte; + uint m_uint; + long long m_longlong; + }; + StringList m_stringList; + ByteVectorList m_byteVectorList; + MP4::CoverArtList m_coverArtList; +}; + +MP4::Item::Item() +{ + d = new ItemPrivate; + d->valid = false; +} + +MP4::Item::Item(const Item &item) : d(item.d) +{ + d->ref(); +} + +MP4::Item & +MP4::Item::operator=(const Item &item) +{ + if(d->deref()) { + delete d; + } + d = item.d; + d->ref(); + return *this; +} + +MP4::Item::~Item() +{ + if(d->deref()) { + delete d; + } +} + +MP4::Item::Item(bool value) +{ + d = new ItemPrivate; + d->m_bool = value; +} + +MP4::Item::Item(int value) +{ + d = new ItemPrivate; + d->m_int = value; +} + +MP4::Item::Item(uchar value) +{ + d = new ItemPrivate; + d->m_byte = value; +} + +MP4::Item::Item(uint value) +{ + d = new ItemPrivate; + d->m_uint = value; +} + +MP4::Item::Item(long long value) +{ + d = new ItemPrivate; + d->m_longlong = value; +} + +MP4::Item::Item(int value1, int value2) +{ + d = new ItemPrivate; + d->m_intPair.first = value1; + d->m_intPair.second = value2; +} + +MP4::Item::Item(const ByteVectorList &value) +{ + d = new ItemPrivate; + d->m_byteVectorList = value; +} + +MP4::Item::Item(const StringList &value) +{ + d = new ItemPrivate; + d->m_stringList = value; +} + +MP4::Item::Item(const MP4::CoverArtList &value) +{ + d = new ItemPrivate; + d->m_coverArtList = value; +} + +void MP4::Item::setAtomDataType(MP4::AtomDataType type) +{ + d->atomDataType = type; +} + +MP4::AtomDataType MP4::Item::atomDataType() const +{ + return d->atomDataType; +} + +bool +MP4::Item::toBool() const +{ + return d->m_bool; +} + +int +MP4::Item::toInt() const +{ + return d->m_int; +} + +uchar +MP4::Item::toByte() const +{ + return d->m_byte; +} + +TagLib::uint +MP4::Item::toUInt() const +{ + return d->m_uint; +} + +long long +MP4::Item::toLongLong() const +{ + return d->m_longlong; +} + +MP4::Item::IntPair +MP4::Item::toIntPair() const +{ + return d->m_intPair; +} + +StringList +MP4::Item::toStringList() const +{ + return d->m_stringList; +} + +ByteVectorList +MP4::Item::toByteVectorList() const +{ + return d->m_byteVectorList; +} + +MP4::CoverArtList +MP4::Item::toCoverArtList() const +{ + return d->m_coverArtList; +} + +bool +MP4::Item::isValid() const +{ + return d->valid; +} + diff --git a/taglib/taglib/mp4/mp4item.h b/taglib/taglib/mp4/mp4item.h new file mode 100644 index 0000000..be7aa1a --- /dev/null +++ b/taglib/taglib/mp4/mp4item.h @@ -0,0 +1,83 @@ +/************************************************************************** + copyright : (C) 2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MP4ITEM_H +#define TAGLIB_MP4ITEM_H + +#include "tstringlist.h" +#include "mp4coverart.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace MP4 { + + class TAGLIB_EXPORT Item + { + public: + struct IntPair { + int first, second; + }; + + Item(); + Item(const Item &item); + Item &operator=(const Item &item); + ~Item(); + + Item(int value); + Item(uchar value); + Item(uint value); + Item(long long value); + Item(bool value); + Item(int first, int second); + Item(const StringList &value); + Item(const ByteVectorList &value); + Item(const CoverArtList &value); + + void setAtomDataType(AtomDataType type); + AtomDataType atomDataType() const; + + int toInt() const; + uchar toByte() const; + uint toUInt() const; + long long toLongLong() const; + bool toBool() const; + IntPair toIntPair() const; + StringList toStringList() const; + ByteVectorList toByteVectorList() const; + CoverArtList toCoverArtList() const; + + bool isValid() const; + + private: + class ItemPrivate; + ItemPrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/mp4/mp4properties.cpp b/taglib/taglib/mp4/mp4properties.cpp new file mode 100644 index 0000000..5a41c08 --- /dev/null +++ b/taglib/taglib/mp4/mp4properties.cpp @@ -0,0 +1,199 @@ +/************************************************************************** + copyright : (C) 2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tstring.h> +#include "mp4file.h" +#include "mp4atom.h" +#include "mp4properties.h" + +using namespace TagLib; + +class MP4::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate() : length(0), bitrate(0), sampleRate(0), channels(0), bitsPerSample(0), encrypted(false), codec(MP4::Properties::Unknown) {} + + int length; + int bitrate; + int sampleRate; + int channels; + int bitsPerSample; + bool encrypted; + Codec codec; +}; + +MP4::Properties::Properties(File *file, MP4::Atoms *atoms, ReadStyle style) + : AudioProperties(style) +{ + d = new PropertiesPrivate; + + MP4::Atom *moov = atoms->find("moov"); + if(!moov) { + debug("MP4: Atom 'moov' not found"); + return; + } + + MP4::Atom *trak = 0; + ByteVector data; + + MP4::AtomList trakList = moov->findall("trak"); + for (unsigned int i = 0; i < trakList.size(); i++) { + trak = trakList[i]; + MP4::Atom *hdlr = trak->find("mdia", "hdlr"); + if(!hdlr) { + debug("MP4: Atom 'trak.mdia.hdlr' not found"); + return; + } + file->seek(hdlr->offset); + data = file->readBlock(hdlr->length); + if(data.mid(16, 4) == "soun") { + break; + } + trak = 0; + } + if (!trak) { + debug("MP4: No audio tracks"); + return; + } + + MP4::Atom *mdhd = trak->find("mdia", "mdhd"); + if(!mdhd) { + debug("MP4: Atom 'trak.mdia.mdhd' not found"); + return; + } + + file->seek(mdhd->offset); + data = file->readBlock(mdhd->length); + uint version = data[8]; + if(version == 1) { + if (data.size() < 36 + 8) { + debug("MP4: Atom 'trak.mdia.mdhd' is smaller than expected"); + return; + } + const long long unit = data.toLongLong(28U); + const long long length = data.toLongLong(36U); + d->length = unit ? int(length / unit) : 0; + } + else { + if (data.size() < 24 + 4) { + debug("MP4: Atom 'trak.mdia.mdhd' is smaller than expected"); + return; + } + const unsigned int unit = data.toUInt(20U); + const unsigned int length = data.toUInt(24U); + d->length = unit ? length / unit : 0; + } + + MP4::Atom *atom = trak->find("mdia", "minf", "stbl", "stsd"); + if(!atom) { + return; + } + + file->seek(atom->offset); + data = file->readBlock(atom->length); + if(data.mid(20, 4) == "mp4a") { + d->codec = AAC; + d->channels = data.toShort(40U); + d->bitsPerSample = data.toShort(42U); + d->sampleRate = data.toUInt(46U); + if(data.mid(56, 4) == "esds" && data[64] == 0x03) { + uint pos = 65; + if(data.mid(pos, 3) == "\x80\x80\x80") { + pos += 3; + } + pos += 4; + if(data[pos] == 0x04) { + pos += 1; + if(data.mid(pos, 3) == "\x80\x80\x80") { + pos += 3; + } + pos += 10; + d->bitrate = (data.toUInt(pos) + 500) / 1000; + } + } + } + else if (data.mid(20, 4) == "alac") { + if (atom->length == 88 && data.mid(56, 4) == "alac") { + d->codec = ALAC; + d->bitsPerSample = data.at(69); + d->channels = data.at(73); + d->bitrate = data.toUInt(80U) / 1000; + d->sampleRate = data.toUInt(84U); + } + } + + MP4::Atom *drms = atom->find("drms"); + if(drms) { + d->encrypted = true; + } +} + +MP4::Properties::~Properties() +{ + delete d; +} + +int +MP4::Properties::channels() const +{ + return d->channels; +} + +int +MP4::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int +MP4::Properties::length() const +{ + return d->length; +} + +int +MP4::Properties::bitrate() const +{ + return d->bitrate; +} + +int +MP4::Properties::bitsPerSample() const +{ + return d->bitsPerSample; +} + +bool +MP4::Properties::isEncrypted() const +{ + return d->encrypted; +} + +MP4::Properties::Codec MP4::Properties::codec() const +{ + return d->codec; +} + diff --git a/taglib/taglib/mp4/mp4properties.h b/taglib/taglib/mp4/mp4properties.h new file mode 100644 index 0000000..2607c36 --- /dev/null +++ b/taglib/taglib/mp4/mp4properties.h @@ -0,0 +1,71 @@ +/************************************************************************** + copyright : (C) 2007 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MP4PROPERTIES_H +#define TAGLIB_MP4PROPERTIES_H + +#include "taglib_export.h" +#include "audioproperties.h" + +namespace TagLib { + + namespace MP4 { + + class Atoms; + class File; + + //! An implementation of MP4 audio properties + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + enum Codec { + Unknown = 0, + AAC, + ALAC + }; + + Properties(File *file, Atoms *atoms, ReadStyle style = Average); + virtual ~Properties(); + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + virtual int bitsPerSample() const; + bool isEncrypted() const; + + //! Audio codec used in the MP4 file + Codec codec() const; + + private: + class PropertiesPrivate; + PropertiesPrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/mp4/mp4tag.cpp b/taglib/taglib/mp4/mp4tag.cpp new file mode 100644 index 0000000..bbe3301 --- /dev/null +++ b/taglib/taglib/mp4/mp4tag.cpp @@ -0,0 +1,917 @@ +/************************************************************************** + copyright : (C) 2007,2011 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tstring.h> +#include <tpropertymap.h> +#include "mp4atom.h" +#include "mp4tag.h" +#include "id3v1genres.h" + +using namespace TagLib; + +class MP4::Tag::TagPrivate +{ +public: + TagPrivate() : file(0), atoms(0) {} + ~TagPrivate() {} + TagLib::File *file; + Atoms *atoms; + ItemListMap items; +}; + +MP4::Tag::Tag() +{ + d = new TagPrivate; +} + +MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms) +{ + d = new TagPrivate; + d->file = file; + d->atoms = atoms; + + MP4::Atom *ilst = atoms->find("moov", "udta", "meta", "ilst"); + if(!ilst) { + //debug("Atom moov.udta.meta.ilst not found."); + return; + } + + for(unsigned int i = 0; i < ilst->children.size(); i++) { + MP4::Atom *atom = ilst->children[i]; + file->seek(atom->offset + 8); + if(atom->name == "----") { + parseFreeForm(atom, file); + } + else if(atom->name == "trkn" || atom->name == "disk") { + parseIntPair(atom, file); + } + else if(atom->name == "cpil" || atom->name == "pgap" || atom->name == "pcst" || + atom->name == "hdvd") { + parseBool(atom, file); + } + else if(atom->name == "tmpo") { + parseInt(atom, file); + } + else if(atom->name == "tvsn" || atom->name == "tves" || atom->name == "cnID" || + atom->name == "sfID" || atom->name == "atID" || atom->name == "geID") { + parseUInt(atom, file); + } + else if(atom->name == "plID") { + parseLongLong(atom, file); + } + else if(atom->name == "stik" || atom->name == "rtng" || atom->name == "akID") { + parseByte(atom, file); + } + else if(atom->name == "gnre") { + parseGnre(atom, file); + } + else if(atom->name == "covr") { + parseCovr(atom, file); + } + else { + parseText(atom, file); + } + } +} + +MP4::Tag::~Tag() +{ + delete d; +} + +MP4::AtomDataList +MP4::Tag::parseData2(MP4::Atom *atom, TagLib::File *file, int expectedFlags, bool freeForm) +{ + AtomDataList result; + ByteVector data = file->readBlock(atom->length - 8); + int i = 0; + unsigned int pos = 0; + while(pos < data.size()) { + const int length = static_cast<int>(data.toUInt(pos)); + ByteVector name = data.mid(pos + 4, 4); + const int flags = static_cast<int>(data.toUInt(pos + 8)); + if(freeForm && i < 2) { + if(i == 0 && name != "mean") { + debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\""); + return result; + } + else if(i == 1 && name != "name") { + debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\""); + return result; + } + result.append(AtomData(AtomDataType(flags), data.mid(pos + 12, length - 12))); + } + else { + if(name != "data") { + debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\""); + return result; + } + if(expectedFlags == -1 || flags == expectedFlags) { + result.append(AtomData(AtomDataType(flags), data.mid(pos + 16, length - 16))); + } + } + pos += length; + i++; + } + return result; +} + +ByteVectorList +MP4::Tag::parseData(MP4::Atom *atom, TagLib::File *file, int expectedFlags, bool freeForm) +{ + AtomDataList data = parseData2(atom, file, expectedFlags, freeForm); + ByteVectorList result; + for(uint i = 0; i < data.size(); i++) { + result.append(data[i].data); + } + return result; +} + +void +MP4::Tag::parseInt(MP4::Atom *atom, TagLib::File *file) +{ + ByteVectorList data = parseData(atom, file); + if(data.size()) { + addItem(atom->name, (int)data[0].toShort()); + } +} + +void +MP4::Tag::parseUInt(MP4::Atom *atom, TagLib::File *file) +{ + ByteVectorList data = parseData(atom, file); + if(data.size()) { + addItem(atom->name, data[0].toUInt()); + } +} + +void +MP4::Tag::parseLongLong(MP4::Atom *atom, TagLib::File *file) +{ + ByteVectorList data = parseData(atom, file); + if(data.size()) { + addItem(atom->name, data[0].toLongLong()); + } +} + +void +MP4::Tag::parseByte(MP4::Atom *atom, TagLib::File *file) +{ + ByteVectorList data = parseData(atom, file); + if(data.size()) { + addItem(atom->name, (uchar)data[0].at(0)); + } +} + +void +MP4::Tag::parseGnre(MP4::Atom *atom, TagLib::File *file) +{ + ByteVectorList data = parseData(atom, file); + if(data.size()) { + int idx = (int)data[0].toShort(); + if(idx > 0) { + addItem("\251gen", StringList(ID3v1::genre(idx - 1))); + } + } +} + +void +MP4::Tag::parseIntPair(MP4::Atom *atom, TagLib::File *file) +{ + ByteVectorList data = parseData(atom, file); + if(data.size()) { + const int a = data[0].toShort(2U); + const int b = data[0].toShort(4U); + addItem(atom->name, MP4::Item(a, b)); + } +} + +void +MP4::Tag::parseBool(MP4::Atom *atom, TagLib::File *file) +{ + ByteVectorList data = parseData(atom, file); + if(data.size()) { + bool value = data[0].size() ? data[0][0] != '\0' : false; + addItem(atom->name, value); + } +} + +void +MP4::Tag::parseText(MP4::Atom *atom, TagLib::File *file, int expectedFlags) +{ + ByteVectorList data = parseData(atom, file, expectedFlags); + if(data.size()) { + StringList value; + for(unsigned int i = 0; i < data.size(); i++) { + value.append(String(data[i], String::UTF8)); + } + addItem(atom->name, value); + } +} + +void +MP4::Tag::parseFreeForm(MP4::Atom *atom, TagLib::File *file) +{ + AtomDataList data = parseData2(atom, file, -1, true); + if(data.size() > 2) { + String name = "----:" + String(data[0].data, String::UTF8) + ':' + String(data[1].data, String::UTF8); + AtomDataType type = data[2].type; + for(uint i = 2; i < data.size(); i++) { + if(data[i].type != type) { + debug("MP4: We currently don't support values with multiple types"); + break; + } + } + if(type == TypeUTF8) { + StringList value; + for(uint i = 2; i < data.size(); i++) { + value.append(String(data[i].data, String::UTF8)); + } + Item item(value); + item.setAtomDataType(type); + addItem(name, item); + } + else { + ByteVectorList value; + for(uint i = 2; i < data.size(); i++) { + value.append(data[i].data); + } + Item item(value); + item.setAtomDataType(type); + addItem(name, item); + } + } +} + +void +MP4::Tag::parseCovr(MP4::Atom *atom, TagLib::File *file) +{ + MP4::CoverArtList value; + ByteVector data = file->readBlock(atom->length - 8); + unsigned int pos = 0; + while(pos < data.size()) { + const int length = static_cast<int>(data.toUInt(pos)); + ByteVector name = data.mid(pos + 4, 4); + const int flags = static_cast<int>(data.toUInt(pos + 8)); + if(name != "data") { + debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\""); + break; + } + if(flags == TypeJPEG || flags == TypePNG || flags == TypeBMP || flags == TypeGIF || flags == TypeImplicit) { + value.append(MP4::CoverArt(MP4::CoverArt::Format(flags), + data.mid(pos + 16, length - 16))); + } + else { + debug("MP4: Unknown covr format " + String::number(flags)); + } + pos += length; + } + if(value.size() > 0) + addItem(atom->name, value); +} + +ByteVector +MP4::Tag::padIlst(const ByteVector &data, int length) +{ + if (length == -1) { + length = ((data.size() + 1023) & ~1023) - data.size(); + } + return renderAtom("free", ByteVector(length, '\1')); +} + +ByteVector +MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data) +{ + return ByteVector::fromUInt(data.size() + 8) + name + data; +} + +ByteVector +MP4::Tag::renderData(const ByteVector &name, int flags, const ByteVectorList &data) +{ + ByteVector result; + for(unsigned int i = 0; i < data.size(); i++) { + result.append(renderAtom("data", ByteVector::fromUInt(flags) + ByteVector(4, '\0') + data[i])); + } + return renderAtom(name, result); +} + +ByteVector +MP4::Tag::renderBool(const ByteVector &name, MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector(1, item.toBool() ? '\1' : '\0')); + return renderData(name, TypeInteger, data); +} + +ByteVector +MP4::Tag::renderInt(const ByteVector &name, MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector::fromShort(item.toInt())); + return renderData(name, TypeInteger, data); +} + +ByteVector +MP4::Tag::renderUInt(const ByteVector &name, MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector::fromUInt(item.toUInt())); + return renderData(name, TypeInteger, data); +} + +ByteVector +MP4::Tag::renderLongLong(const ByteVector &name, MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector::fromLongLong(item.toLongLong())); + return renderData(name, TypeInteger, data); +} + +ByteVector +MP4::Tag::renderByte(const ByteVector &name, MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector(1, item.toByte())); + return renderData(name, TypeInteger, data); +} + +ByteVector +MP4::Tag::renderIntPair(const ByteVector &name, MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector(2, '\0') + + ByteVector::fromShort(item.toIntPair().first) + + ByteVector::fromShort(item.toIntPair().second) + + ByteVector(2, '\0')); + return renderData(name, TypeImplicit, data); +} + +ByteVector +MP4::Tag::renderIntPairNoTrailing(const ByteVector &name, MP4::Item &item) +{ + ByteVectorList data; + data.append(ByteVector(2, '\0') + + ByteVector::fromShort(item.toIntPair().first) + + ByteVector::fromShort(item.toIntPair().second)); + return renderData(name, TypeImplicit, data); +} + +ByteVector +MP4::Tag::renderText(const ByteVector &name, MP4::Item &item, int flags) +{ + ByteVectorList data; + StringList value = item.toStringList(); + for(unsigned int i = 0; i < value.size(); i++) { + data.append(value[i].data(String::UTF8)); + } + return renderData(name, flags, data); +} + +ByteVector +MP4::Tag::renderCovr(const ByteVector &name, MP4::Item &item) +{ + ByteVector data; + MP4::CoverArtList value = item.toCoverArtList(); + for(unsigned int i = 0; i < value.size(); i++) { + data.append(renderAtom("data", ByteVector::fromUInt(value[i].format()) + + ByteVector(4, '\0') + value[i].data())); + } + return renderAtom(name, data); +} + +ByteVector +MP4::Tag::renderFreeForm(const String &name, MP4::Item &item) +{ + StringList header = StringList::split(name, ":"); + if (header.size() != 3) { + debug("MP4: Invalid free-form item name \"" + name + "\""); + return ByteVector::null; + } + ByteVector data; + data.append(renderAtom("mean", ByteVector::fromUInt(0) + header[1].data(String::UTF8))); + data.append(renderAtom("name", ByteVector::fromUInt(0) + header[2].data(String::UTF8))); + AtomDataType type = item.atomDataType(); + if(type == TypeUndefined) { + if(!item.toStringList().isEmpty()) { + type = TypeUTF8; + } + else { + type = TypeImplicit; + } + } + if(type == TypeUTF8) { + StringList value = item.toStringList(); + for(unsigned int i = 0; i < value.size(); i++) { + data.append(renderAtom("data", ByteVector::fromUInt(type) + ByteVector(4, '\0') + value[i].data(String::UTF8))); + } + } + else { + ByteVectorList value = item.toByteVectorList(); + for(unsigned int i = 0; i < value.size(); i++) { + data.append(renderAtom("data", ByteVector::fromUInt(type) + ByteVector(4, '\0') + value[i])); + } + } + return renderAtom("----", data); +} + +bool +MP4::Tag::save() +{ + ByteVector data; + for(MP4::ItemListMap::Iterator i = d->items.begin(); i != d->items.end(); i++) { + const String name = i->first; + if(name.startsWith("----")) { + data.append(renderFreeForm(name, i->second)); + } + else if(name == "trkn") { + data.append(renderIntPair(name.data(String::Latin1), i->second)); + } + else if(name == "disk") { + data.append(renderIntPairNoTrailing(name.data(String::Latin1), i->second)); + } + else if(name == "cpil" || name == "pgap" || name == "pcst" || name == "hdvd") { + data.append(renderBool(name.data(String::Latin1), i->second)); + } + else if(name == "tmpo") { + data.append(renderInt(name.data(String::Latin1), i->second)); + } + else if(name == "tvsn" || name == "tves" || name == "cnID" || + name == "sfID" || name == "atID" || name == "geID") { + data.append(renderUInt(name.data(String::Latin1), i->second)); + } + else if(name == "plID") { + data.append(renderLongLong(name.data(String::Latin1), i->second)); + } + else if(name == "stik" || name == "rtng" || name == "akID") { + data.append(renderByte(name.data(String::Latin1), i->second)); + } + else if(name == "covr") { + data.append(renderCovr(name.data(String::Latin1), i->second)); + } + else if(name.size() == 4){ + data.append(renderText(name.data(String::Latin1), i->second)); + } + else { + debug("MP4: Unknown item name \"" + name + "\""); + } + } + data = renderAtom("ilst", data); + + AtomList path = d->atoms->path("moov", "udta", "meta", "ilst"); + if(path.size() == 4) { + saveExisting(data, path); + } + else { + saveNew(data); + } + + return true; +} + +void +MP4::Tag::updateParents(AtomList &path, long delta, int ignore) +{ + for(unsigned int i = 0; i < path.size() - ignore; i++) { + d->file->seek(path[i]->offset); + long size = d->file->readBlock(4).toUInt(); + // 64-bit + if (size == 1) { + d->file->seek(4, File::Current); // Skip name + long long longSize = d->file->readBlock(8).toLongLong(); + // Seek the offset of the 64-bit size + d->file->seek(path[i]->offset + 8); + d->file->writeBlock(ByteVector::fromLongLong(longSize + delta)); + } + // 32-bit + else { + d->file->seek(path[i]->offset); + d->file->writeBlock(ByteVector::fromUInt(size + delta)); + } + } +} + +void +MP4::Tag::updateOffsets(long delta, long offset) +{ + MP4::Atom *moov = d->atoms->find("moov"); + if(moov) { + MP4::AtomList stco = moov->findall("stco", true); + for(unsigned int i = 0; i < stco.size(); i++) { + MP4::Atom *atom = stco[i]; + if(atom->offset > offset) { + atom->offset += delta; + } + d->file->seek(atom->offset + 12); + ByteVector data = d->file->readBlock(atom->length - 12); + unsigned int count = data.toUInt(); + d->file->seek(atom->offset + 16); + uint pos = 4; + while(count--) { + long o = static_cast<long>(data.toUInt(pos)); + if(o > offset) { + o += delta; + } + d->file->writeBlock(ByteVector::fromUInt(o)); + pos += 4; + } + } + + MP4::AtomList co64 = moov->findall("co64", true); + for(unsigned int i = 0; i < co64.size(); i++) { + MP4::Atom *atom = co64[i]; + if(atom->offset > offset) { + atom->offset += delta; + } + d->file->seek(atom->offset + 12); + ByteVector data = d->file->readBlock(atom->length - 12); + unsigned int count = data.toUInt(); + d->file->seek(atom->offset + 16); + uint pos = 4; + while(count--) { + long long o = data.toLongLong(pos); + if(o > offset) { + o += delta; + } + d->file->writeBlock(ByteVector::fromLongLong(o)); + pos += 8; + } + } + } + + MP4::Atom *moof = d->atoms->find("moof"); + if(moof) { + MP4::AtomList tfhd = moof->findall("tfhd", true); + for(unsigned int i = 0; i < tfhd.size(); i++) { + MP4::Atom *atom = tfhd[i]; + if(atom->offset > offset) { + atom->offset += delta; + } + d->file->seek(atom->offset + 9); + ByteVector data = d->file->readBlock(atom->length - 9); + const unsigned int flags = data.toUInt(0, 3, true); + if(flags & 1) { + long long o = data.toLongLong(7U); + if(o > offset) { + o += delta; + } + d->file->seek(atom->offset + 16); + d->file->writeBlock(ByteVector::fromLongLong(o)); + } + } + } +} + +void +MP4::Tag::saveNew(ByteVector &data) +{ + data = renderAtom("meta", TagLib::ByteVector(4, '\0') + + renderAtom("hdlr", TagLib::ByteVector(8, '\0') + TagLib::ByteVector("mdirappl") + TagLib::ByteVector(9, '\0')) + + data + padIlst(data)); + + AtomList path = d->atoms->path("moov", "udta"); + if(path.size() != 2) { + path = d->atoms->path("moov"); + data = renderAtom("udta", data); + } + + long offset = path[path.size() - 1]->offset + 8; + d->file->insert(data, offset, 0); + + updateParents(path, data.size()); + updateOffsets(data.size(), offset); +} + +void +MP4::Tag::saveExisting(ByteVector &data, AtomList &path) +{ + MP4::Atom *ilst = path[path.size() - 1]; + long offset = ilst->offset; + long length = ilst->length; + + MP4::Atom *meta = path[path.size() - 2]; + AtomList::Iterator index = meta->children.find(ilst); + + // check if there is an atom before 'ilst', and possibly use it as padding + if(index != meta->children.begin()) { + AtomList::Iterator prevIndex = index; + prevIndex--; + MP4::Atom *prev = *prevIndex; + if(prev->name == "free") { + offset = prev->offset; + length += prev->length; + } + } + // check if there is an atom after 'ilst', and possibly use it as padding + AtomList::Iterator nextIndex = index; + nextIndex++; + if(nextIndex != meta->children.end()) { + MP4::Atom *next = *nextIndex; + if(next->name == "free") { + length += next->length; + } + } + + long delta = data.size() - length; + if(delta > 0 || (delta < 0 && delta > -8)) { + data.append(padIlst(data)); + delta = data.size() - length; + } + else if(delta < 0) { + data.append(padIlst(data, -delta - 8)); + delta = 0; + } + + d->file->insert(data, offset, length); + + if(delta) { + updateParents(path, delta, 1); + updateOffsets(delta, offset); + } +} + +String +MP4::Tag::title() const +{ + if(d->items.contains("\251nam")) + return d->items["\251nam"].toStringList().toString(", "); + return String::null; +} + +String +MP4::Tag::artist() const +{ + if(d->items.contains("\251ART")) + return d->items["\251ART"].toStringList().toString(", "); + return String::null; +} + +String +MP4::Tag::album() const +{ + if(d->items.contains("\251alb")) + return d->items["\251alb"].toStringList().toString(", "); + return String::null; +} + +String +MP4::Tag::comment() const +{ + if(d->items.contains("\251cmt")) + return d->items["\251cmt"].toStringList().toString(", "); + return String::null; +} + +String +MP4::Tag::genre() const +{ + if(d->items.contains("\251gen")) + return d->items["\251gen"].toStringList().toString(", "); + return String::null; +} + +unsigned int +MP4::Tag::year() const +{ + if(d->items.contains("\251day")) + return d->items["\251day"].toStringList().toString().toInt(); + return 0; +} + +unsigned int +MP4::Tag::track() const +{ + if(d->items.contains("trkn")) + return d->items["trkn"].toIntPair().first; + return 0; +} + +void +MP4::Tag::setTitle(const String &value) +{ + d->items["\251nam"] = StringList(value); +} + +void +MP4::Tag::setArtist(const String &value) +{ + d->items["\251ART"] = StringList(value); +} + +void +MP4::Tag::setAlbum(const String &value) +{ + d->items["\251alb"] = StringList(value); +} + +void +MP4::Tag::setComment(const String &value) +{ + d->items["\251cmt"] = StringList(value); +} + +void +MP4::Tag::setGenre(const String &value) +{ + d->items["\251gen"] = StringList(value); +} + +void +MP4::Tag::setYear(uint value) +{ + d->items["\251day"] = StringList(String::number(value)); +} + +void +MP4::Tag::setTrack(uint value) +{ + d->items["trkn"] = MP4::Item(value, 0); +} + +MP4::ItemListMap & +MP4::Tag::itemListMap() +{ + return d->items; +} + +static const char *keyTranslation[][2] = { + { "\251nam", "TITLE" }, + { "\251ART", "ARTIST" }, + { "\251alb", "ALBUM" }, + { "\251cmt", "COMMENT" }, + { "\251gen", "GENRE" }, + { "\251day", "DATE" }, + { "\251wrt", "COMPOSER" }, + { "\251grp", "GROUPING" }, + { "trkn", "TRACKNUMBER" }, + { "disk", "DISCNUMBER" }, + { "cpil", "COMPILATION" }, + { "tmpo", "BPM" }, + { "cprt", "COPYRIGHT" }, + { "\251lyr", "LYRICS" }, + { "\251too", "ENCODEDBY" }, + { "soal", "ALBUMSORT" }, + { "soaa", "ALBUMARTISTSORT" }, + { "soar", "ARTISTSORT" }, + { "sonm", "TITLESORT" }, + { "soco", "COMPOSERSORT" }, + { "sosn", "SHOWSORT" }, + { "----:com.apple.iTunes:MusicBrainz Track Id", "MUSICBRAINZ_TRACKID" }, + { "----:com.apple.iTunes:MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID" }, + { "----:com.apple.iTunes:MusicBrainz Album Id", "MUSICBRAINZ_ALBUMID" }, + { "----:com.apple.iTunes:MusicBrainz Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID" }, + { "----:com.apple.iTunes:MusicBrainz Release Group Id", "MUSICBRAINZ_RELEASEGROUPID" }, + { "----:com.apple.iTunes:MusicBrainz Work Id", "MUSICBRAINZ_WORKID" }, + { "----:com.apple.iTunes:ASIN", "ASIN" }, + { "----:com.apple.iTunes:LABEL", "LABEL" }, + { "----:com.apple.iTunes:LYRICIST", "LYRICIST" }, + { "----:com.apple.iTunes:CONDUCTOR", "CONDUCTOR" }, + { "----:com.apple.iTunes:REMIXER", "REMIXER" }, + { "----:com.apple.iTunes:ENGINEER", "ENGINEER" }, + { "----:com.apple.iTunes:PRODUCER", "PRODUCER" }, + { "----:com.apple.iTunes:DJMIXER", "DJMIXER" }, + { "----:com.apple.iTunes:MIXER", "MIXER" }, + { "----:com.apple.iTunes:SUBTITLE", "SUBTITLE" }, + { "----:com.apple.iTunes:DISCSUBTITLE", "DISCSUBTITLE" }, + { "----:com.apple.iTunes:MOOD", "MOOD" }, + { "----:com.apple.iTunes:ISRC", "ISRC" }, + { "----:com.apple.iTunes:CATALOGNUMBER", "CATALOGNUMBER" }, + { "----:com.apple.iTunes:BARCODE", "BARCODE" }, + { "----:com.apple.iTunes:SCRIPT", "SCRIPT" }, + { "----:com.apple.iTunes:LANGUAGE", "LANGUAGE" }, + { "----:com.apple.iTunes:LICENSE", "LICENSE" }, + { "----:com.apple.iTunes:MEDIA", "MEDIA" }, +}; + +PropertyMap MP4::Tag::properties() const +{ + static Map<String, String> keyMap; + if(keyMap.isEmpty()) { + int numKeys = sizeof(keyTranslation) / sizeof(keyTranslation[0]); + for(int i = 0; i < numKeys; i++) { + keyMap[keyTranslation[i][0]] = keyTranslation[i][1]; + } + } + + PropertyMap props; + MP4::ItemListMap::ConstIterator it = d->items.begin(); + for(; it != d->items.end(); ++it) { + if(keyMap.contains(it->first)) { + String key = keyMap[it->first]; + if(key == "TRACKNUMBER" || key == "DISCNUMBER") { + MP4::Item::IntPair ip = it->second.toIntPair(); + String value = String::number(ip.first); + if(ip.second) { + value += "/" + String::number(ip.second); + } + props[key] = value; + } + else if(key == "BPM") { + props[key] = String::number(it->second.toInt()); + } + else if(key == "COMPILATION") { + props[key] = String::number(it->second.toBool()); + } + else { + props[key] = it->second.toStringList(); + } + } + else { + props.unsupportedData().append(it->first); + } + } + return props; +} + +void MP4::Tag::removeUnsupportedProperties(const StringList &props) +{ + StringList::ConstIterator it = props.begin(); + for(; it != props.end(); ++it) + d->items.erase(*it); +} + +PropertyMap MP4::Tag::setProperties(const PropertyMap &props) +{ + static Map<String, String> reverseKeyMap; + if(reverseKeyMap.isEmpty()) { + int numKeys = sizeof(keyTranslation) / sizeof(keyTranslation[0]); + for(int i = 0; i < numKeys; i++) { + reverseKeyMap[keyTranslation[i][1]] = keyTranslation[i][0]; + } + } + + PropertyMap origProps = properties(); + PropertyMap::ConstIterator it = origProps.begin(); + for(; it != origProps.end(); ++it) { + if(!props.contains(it->first) || props[it->first].isEmpty()) { + d->items.erase(reverseKeyMap[it->first]); + } + } + + PropertyMap ignoredProps; + it = props.begin(); + for(; it != props.end(); ++it) { + if(reverseKeyMap.contains(it->first)) { + String name = reverseKeyMap[it->first]; + if(it->first == "TRACKNUMBER" || it->first == "DISCNUMBER") { + int first = 0, second = 0; + StringList parts = StringList::split(it->second.front(), "/"); + if(parts.size() > 0) { + first = parts[0].toInt(); + if(parts.size() > 1) { + second = parts[1].toInt(); + } + d->items[name] = MP4::Item(first, second); + } + } + else if(it->first == "BPM") { + int value = it->second.front().toInt(); + d->items[name] = MP4::Item(value); + } + else if(it->first == "COMPILATION") { + bool value = (it->second.front().toInt() != 0); + d->items[name] = MP4::Item(value); + } + else { + d->items[name] = it->second; + } + } + else { + ignoredProps.insert(it->first, it->second); + } + } + + return ignoredProps; +} + +void MP4::Tag::addItem(const String &name, const Item &value) +{ + if(!d->items.contains(name)) { + d->items.insert(name, value); + } + else { + debug("MP4: Ignoring duplicate atom \"" + name + "\""); + } +} diff --git a/taglib/taglib/mp4/mp4tag.h b/taglib/taglib/mp4/mp4tag.h new file mode 100644 index 0000000..48d71fc --- /dev/null +++ b/taglib/taglib/mp4/mp4tag.h @@ -0,0 +1,118 @@ +/************************************************************************** + copyright : (C) 2007,2011 by Lukáš Lalinský + email : lalinsky@gmail.com + **************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MP4TAG_H +#define TAGLIB_MP4TAG_H + +#include "tag.h" +#include "tbytevectorlist.h" +#include "tfile.h" +#include "tmap.h" +#include "tstringlist.h" +#include "taglib_export.h" +#include "mp4atom.h" +#include "mp4item.h" + +namespace TagLib { + + namespace MP4 { + + typedef TagLib::Map<String, Item> ItemListMap; + + class TAGLIB_EXPORT Tag: public TagLib::Tag + { + public: + Tag(); + Tag(TagLib::File *file, Atoms *atoms); + ~Tag(); + bool save(); + + String title() const; + String artist() const; + String album() const; + String comment() const; + String genre() const; + uint year() const; + uint track() const; + + void setTitle(const String &value); + void setArtist(const String &value); + void setAlbum(const String &value); + void setComment(const String &value); + void setGenre(const String &value); + void setYear(uint value); + void setTrack(uint value); + + ItemListMap &itemListMap(); + + PropertyMap properties() const; + void removeUnsupportedProperties(const StringList& properties); + PropertyMap setProperties(const PropertyMap &properties); + + private: + AtomDataList parseData2(Atom *atom, TagLib::File *file, int expectedFlags = -1, bool freeForm = false); + TagLib::ByteVectorList parseData(Atom *atom, TagLib::File *file, int expectedFlags = -1, bool freeForm = false); + void parseText(Atom *atom, TagLib::File *file, int expectedFlags = 1); + void parseFreeForm(Atom *atom, TagLib::File *file); + void parseInt(Atom *atom, TagLib::File *file); + void parseByte(Atom *atom, TagLib::File *file); + void parseUInt(Atom *atom, TagLib::File *file); + void parseLongLong(Atom *atom, TagLib::File *file); + void parseGnre(Atom *atom, TagLib::File *file); + void parseIntPair(Atom *atom, TagLib::File *file); + void parseBool(Atom *atom, TagLib::File *file); + void parseCovr(Atom *atom, TagLib::File *file); + + TagLib::ByteVector padIlst(const ByteVector &data, int length = -1); + TagLib::ByteVector renderAtom(const ByteVector &name, const TagLib::ByteVector &data); + TagLib::ByteVector renderData(const ByteVector &name, int flags, const TagLib::ByteVectorList &data); + TagLib::ByteVector renderText(const ByteVector &name, Item &item, int flags = TypeUTF8); + TagLib::ByteVector renderFreeForm(const String &name, Item &item); + TagLib::ByteVector renderBool(const ByteVector &name, Item &item); + TagLib::ByteVector renderInt(const ByteVector &name, Item &item); + TagLib::ByteVector renderByte(const ByteVector &name, Item &item); + TagLib::ByteVector renderUInt(const ByteVector &name, Item &item); + TagLib::ByteVector renderLongLong(const ByteVector &name, Item &item); + TagLib::ByteVector renderIntPair(const ByteVector &name, Item &item); + TagLib::ByteVector renderIntPairNoTrailing(const ByteVector &name, Item &item); + TagLib::ByteVector renderCovr(const ByteVector &name, Item &item); + + void updateParents(AtomList &path, long delta, int ignore = 0); + void updateOffsets(long delta, long offset); + + void saveNew(TagLib::ByteVector &data); + void saveExisting(TagLib::ByteVector &data, AtomList &path); + + void addItem(const String &name, const Item &value); + + class TagPrivate; + TagPrivate *d; + }; + + } + +} + +#endif diff --git a/taglib/taglib/mpc/mpcfile.cpp b/taglib/taglib/mpc/mpcfile.cpp new file mode 100644 index 0000000..979c035 --- /dev/null +++ b/taglib/taglib/mpc/mpcfile.cpp @@ -0,0 +1,367 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tstring.h> +#include <tagunion.h> +#include <tdebug.h> +#include <tpropertymap.h> + +#include "mpcfile.h" +#include "id3v1tag.h" +#include "id3v2header.h" +#include "apetag.h" +#include "apefooter.h" + +using namespace TagLib; + +namespace +{ + enum { MPCAPEIndex = 0, MPCID3v1Index = 1 }; +} + +class MPC::File::FilePrivate +{ +public: + FilePrivate() : + APELocation(-1), + APESize(0), + ID3v1Location(-1), + ID3v2Header(0), + ID3v2Location(-1), + ID3v2Size(0), + properties(0), + scanned(false), + hasAPE(false), + hasID3v1(false), + hasID3v2(false) {} + + ~FilePrivate() + { + delete ID3v2Header; + delete properties; + } + + long APELocation; + uint APESize; + + long ID3v1Location; + + ID3v2::Header *ID3v2Header; + long ID3v2Location; + uint ID3v2Size; + + TagUnion tag; + + Properties *properties; + bool scanned; + + // These indicate whether the file *on disk* has these tags, not if + // this data structure does. This is used in computing offsets. + + bool hasAPE; + bool hasID3v1; + bool hasID3v2; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +MPC::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +MPC::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +MPC::File::~File() +{ + delete d; +} + +TagLib::Tag *MPC::File::tag() const +{ + return &d->tag; +} + +PropertyMap MPC::File::properties() const +{ + if(d->hasAPE) + return d->tag.access<APE::Tag>(MPCAPEIndex, false)->properties(); + if(d->hasID3v1) + return d->tag.access<ID3v1::Tag>(MPCID3v1Index, false)->properties(); + return PropertyMap(); +} + +void MPC::File::removeUnsupportedProperties(const StringList &properties) +{ + if(d->hasAPE) + d->tag.access<APE::Tag>(MPCAPEIndex, false)->removeUnsupportedProperties(properties); + if(d->hasID3v1) + d->tag.access<ID3v1::Tag>(MPCID3v1Index, false)->removeUnsupportedProperties(properties); +} + +PropertyMap MPC::File::setProperties(const PropertyMap &properties) +{ + if(d->hasID3v1) + d->tag.access<APE::Tag>(MPCID3v1Index, false)->setProperties(properties); + return d->tag.access<APE::Tag>(MPCAPEIndex, true)->setProperties(properties); +} + +MPC::Properties *MPC::File::audioProperties() const +{ + return d->properties; +} + +bool MPC::File::save() +{ + if(readOnly()) { + debug("MPC::File::save() -- File is read only."); + return false; + } + + // Possibly strip ID3v2 tag + + if(d->hasID3v2 && !d->ID3v2Header) { + removeBlock(d->ID3v2Location, d->ID3v2Size); + d->hasID3v2 = false; + if(d->hasID3v1) + d->ID3v1Location -= d->ID3v2Size; + if(d->hasAPE) + d->APELocation -= d->ID3v2Size; + } + + // Update ID3v1 tag + + if(ID3v1Tag()) { + if(d->hasID3v1) { + seek(d->ID3v1Location); + writeBlock(ID3v1Tag()->render()); + } + else { + seek(0, End); + d->ID3v1Location = tell(); + writeBlock(ID3v1Tag()->render()); + d->hasID3v1 = true; + } + } else + if(d->hasID3v1) { + removeBlock(d->ID3v1Location, 128); + d->hasID3v1 = false; + if(d->hasAPE) { + if(d->APELocation > d->ID3v1Location) + d->APELocation -= 128; + } + } + + // Update APE tag + + if(APETag()) { + if(d->hasAPE) + insert(APETag()->render(), d->APELocation, d->APESize); + else { + if(d->hasID3v1) { + insert(APETag()->render(), d->ID3v1Location, 0); + d->APESize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + d->APELocation = d->ID3v1Location; + d->ID3v1Location += d->APESize; + } + else { + seek(0, End); + d->APELocation = tell(); + writeBlock(APETag()->render()); + d->APESize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + } + } + } + else + if(d->hasAPE) { + removeBlock(d->APELocation, d->APESize); + d->hasAPE = false; + if(d->hasID3v1) { + if(d->ID3v1Location > d->APELocation) + d->ID3v1Location -= d->APESize; + } + } + + return true; +} + +ID3v1::Tag *MPC::File::ID3v1Tag(bool create) +{ + return d->tag.access<ID3v1::Tag>(MPCID3v1Index, create); +} + +APE::Tag *MPC::File::APETag(bool create) +{ + return d->tag.access<APE::Tag>(MPCAPEIndex, create); +} + +void MPC::File::strip(int tags) +{ + if(tags & ID3v1) { + d->tag.set(MPCID3v1Index, 0); + APETag(true); + } + + if(tags & ID3v2) { + delete d->ID3v2Header; + d->ID3v2Header = 0; + } + + if(tags & APE) { + d->tag.set(MPCAPEIndex, 0); + + if(!ID3v1Tag()) + APETag(true); + } +} + +void MPC::File::remove(int tags) +{ + strip(tags); +} + +bool MPC::File::hasID3v1Tag() const +{ + return d->hasID3v1; +} + +bool MPC::File::hasAPETag() const +{ + return d->hasAPE; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void MPC::File::read(bool readProperties, Properties::ReadStyle /* propertiesStyle */) +{ + // Look for an ID3v1 tag + + d->ID3v1Location = findID3v1(); + + if(d->ID3v1Location >= 0) { + d->tag.set(MPCID3v1Index, new ID3v1::Tag(this, d->ID3v1Location)); + d->hasID3v1 = true; + } + + // Look for an APE tag + + findAPE(); + + d->APELocation = findAPE(); + + if(d->APELocation >= 0) { + d->tag.set(MPCAPEIndex, new APE::Tag(this, d->APELocation)); + + d->APESize = APETag()->footer()->completeTagSize(); + d->APELocation = d->APELocation + APETag()->footer()->size() - d->APESize; + d->hasAPE = true; + } + + if(!d->hasID3v1) + APETag(true); + + // Look for and skip an ID3v2 tag + + d->ID3v2Location = findID3v2(); + + if(d->ID3v2Location >= 0) { + seek(d->ID3v2Location); + d->ID3v2Header = new ID3v2::Header(readBlock(ID3v2::Header::size())); + d->ID3v2Size = d->ID3v2Header->completeTagSize(); + d->hasID3v2 = true; + } + + if(d->hasID3v2) + seek(d->ID3v2Location + d->ID3v2Size); + else + seek(0); + + // Look for MPC metadata + + if(readProperties) { + d->properties = new Properties(this, length() - d->ID3v2Size - d->APESize); + } +} + +long MPC::File::findAPE() +{ + if(!isValid()) + return -1; + + if(d->hasID3v1) + seek(-160, End); + else + seek(-32, End); + + long p = tell(); + + if(readBlock(8) == APE::Tag::fileIdentifier()) + return p; + + return -1; +} + +long MPC::File::findID3v1() +{ + if(!isValid()) + return -1; + + seek(-128, End); + long p = tell(); + + if(readBlock(3) == ID3v1::Tag::fileIdentifier()) + return p; + + return -1; +} + +long MPC::File::findID3v2() +{ + if(!isValid()) + return -1; + + seek(0); + + if(readBlock(3) == ID3v2::Header::fileIdentifier()) + return 0; + + return -1; +} diff --git a/taglib/taglib/mpc/mpcfile.h b/taglib/taglib/mpc/mpcfile.h new file mode 100644 index 0000000..1eef810 --- /dev/null +++ b/taglib/taglib/mpc/mpcfile.h @@ -0,0 +1,231 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MPCFILE_H +#define TAGLIB_MPCFILE_H + +#include "taglib_export.h" +#include "tfile.h" +#include "tag.h" + +#include "mpcproperties.h" + +#include "tlist.h" + +namespace TagLib { + + class Tag; + + namespace ID3v1 { class Tag; } + namespace APE { class Tag; } + + //! An implementation of MPC metadata + + /*! + * This is implementation of MPC metadata. + * + * This supports ID3v1 and APE (v1 and v2) style comments as well as reading stream + * properties from the file. ID3v2 tags are invalid in MPC-files, but will be skipped + * and ignored. + */ + + namespace MPC { + + //! An implementation of TagLib::File with MPC specific methods + + /*! + * This implements and provides an interface for MPC files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to MPC files. + * The only invalid tag combination supported is an ID3v1 tag after an APE tag. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * This set of flags is used for various operations and is suitable for + * being OR-ed together. + */ + enum TagTypes { + //! Empty set. Matches no tag types. + NoTags = 0x0000, + //! Matches ID3v1 tags. + ID3v1 = 0x0001, + //! Matches ID3v2 tags. + ID3v2 = 0x0002, + //! Matches APE tags. + APE = 0x0004, + //! Matches all tag types. + AllTags = 0xffff + }; + + /*! + * Constructs an MPC file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an MPC file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the Tag for this file. This will be an APE tag, an ID3v1 tag + * or a combination of the two. + */ + virtual TagLib::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * If the file contains both an APE and an ID3v1 tag, only the APE + * tag will be converted to the PropertyMap. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * Affects only the APEv2 tag which will be created if necessary. + * If an ID3v1 tag exists, it will be updated as well. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the MPC::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + /*! + * Saves the file. + */ + virtual bool save(); + + /*! + * Returns a pointer to the ID3v1 tag of the file. + * + * If \a create is false (the default) this returns a null pointer + * if there is no valid APE tag. If \a create is true it will create + * an APE tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v1 tag. Use hasID3v1Tag() to check if the file + * on disk actually has an ID3v1 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v1Tag() + */ + ID3v1::Tag *ID3v1Tag(bool create = false); + + /*! + * Returns a pointer to the APE tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid APE tag. If \a create is true it will create + * an APE tag if one does not exist and returns a valid pointer. If + * there already be an ID3v1 tag, the new APE tag will be placed before it. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an APE tag. Use hasAPETag() to check if the file + * on disk actually has an APE tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasAPETag() + */ + APE::Tag *APETag(bool create = false); + + /*! + * This will remove the tags that match the OR-ed together TagTypes from the + * file. By default it removes all tags. + * + * \warning This will also invalidate pointers to the tags + * as their memory will be freed. + * + * \note In order to make the removal permanent save() still needs to be called. + */ + void strip(int tags = AllTags); + + /*! + * \deprecated + * \see strip + */ + void remove(int tags = AllTags); + + /*! + * Returns whether or not the file on disk actually has an ID3v1 tag. + * + * \see ID3v1Tag() + */ + bool hasID3v1Tag() const; + + /*! + * Returns whether or not the file on disk actually has an APE tag. + * + * \see APETag() + */ + bool hasAPETag() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + void scan(); + long findAPE(); + long findID3v1(); + long findID3v2(); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mpc/mpcproperties.cpp b/taglib/taglib/mpc/mpcproperties.cpp new file mode 100644 index 0000000..d406f8d --- /dev/null +++ b/taglib/taglib/mpc/mpcproperties.cpp @@ -0,0 +1,316 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> +#include <bitset> +#include <math.h> + +#include "mpcproperties.h" +#include "mpcfile.h" + +using namespace TagLib; + +class MPC::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(long length, ReadStyle s) : + streamLength(length), + style(s), + version(0), + length(0), + bitrate(0), + sampleRate(0), + channels(0), + totalFrames(0), + sampleFrames(0), + trackGain(0), + trackPeak(0), + albumGain(0), + albumPeak(0) {} + + long streamLength; + ReadStyle style; + int version; + int length; + int bitrate; + int sampleRate; + int channels; + uint totalFrames; + uint sampleFrames; + uint trackGain; + uint trackPeak; + uint albumGain; + uint albumPeak; + String flags; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +MPC::Properties::Properties(const ByteVector &data, long streamLength, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(streamLength, style); + readSV7(data); +} + +MPC::Properties::Properties(File *file, long streamLength, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(streamLength, style); + ByteVector magic = file->readBlock(4); + if(magic == "MPCK") { + // Musepack version 8 + readSV8(file); + } + else { + // Musepack version 7 or older, fixed size header + readSV7(magic + file->readBlock(MPC::HeaderSize - 4)); + } +} + +MPC::Properties::~Properties() +{ + delete d; +} + +int MPC::Properties::length() const +{ + return d->length; +} + +int MPC::Properties::bitrate() const +{ + return d->bitrate; +} + +int MPC::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int MPC::Properties::channels() const +{ + return d->channels; +} + +int MPC::Properties::mpcVersion() const +{ + return d->version; +} + +TagLib::uint MPC::Properties::totalFrames() const +{ + return d->totalFrames; +} + +TagLib::uint MPC::Properties::sampleFrames() const +{ + return d->sampleFrames; +} + +int MPC::Properties::trackGain() const +{ + return d->trackGain; +} + +int MPC::Properties::trackPeak() const +{ + return d->trackPeak; +} + +int MPC::Properties::albumGain() const +{ + return d->albumGain; +} + +int MPC::Properties::albumPeak() const +{ + return d->albumPeak; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +unsigned long readSize(File *file, TagLib::uint &sizelength) +{ + unsigned char tmp; + unsigned long size = 0; + + do { + ByteVector b = file->readBlock(1); + tmp = b[0]; + size = (size << 7) | (tmp & 0x7F); + sizelength++; + } while((tmp & 0x80)); + return size; +} + +unsigned long readSize(const ByteVector &data, TagLib::uint &sizelength) +{ + unsigned char tmp; + unsigned long size = 0; + unsigned long pos = 0; + + do { + tmp = data[pos++]; + size = (size << 7) | (tmp & 0x7F); + sizelength++; + } while((tmp & 0x80) && (pos < data.size())); + return size; +} + +static const unsigned short sftable [4] = { 44100, 48000, 37800, 32000 }; + +void MPC::Properties::readSV8(File *file) +{ + bool readSH = false, readRG = false; + + while(!readSH && !readRG) { + ByteVector packetType = file->readBlock(2); + uint packetSizeLength = 0; + unsigned long packetSize = readSize(file, packetSizeLength); + unsigned long dataSize = packetSize - 2 - packetSizeLength; + + if(packetType == "SH") { + // Stream Header + // http://trac.musepack.net/wiki/SV8Specification#StreamHeaderPacket + ByteVector data = file->readBlock(dataSize); + readSH = true; + + TagLib::uint pos = 4; + d->version = data[pos]; + pos += 1; + d->sampleFrames = readSize(data.mid(pos), pos); + ulong begSilence = readSize(data.mid(pos), pos); + + std::bitset<16> flags(TAGLIB_CONSTRUCT_BITSET(data.toUShort(pos, true))); + pos += 2; + + d->sampleRate = sftable[flags[15] * 4 + flags[14] * 2 + flags[13]]; + d->channels = flags[7] * 8 + flags[6] * 4 + flags[5] * 2 + flags[4] + 1; + + if((d->sampleFrames - begSilence) != 0) + d->bitrate = (int)(d->streamLength * 8.0 * d->sampleRate / (d->sampleFrames - begSilence)); + d->bitrate = d->bitrate / 1000; + + d->length = (d->sampleFrames - begSilence) / d->sampleRate; + } + + else if (packetType == "RG") { + // Replay Gain + // http://trac.musepack.net/wiki/SV8Specification#ReplaygainPacket + ByteVector data = file->readBlock(dataSize); + readRG = true; + + int replayGainVersion = data[0]; + if(replayGainVersion == 1) { + d->trackGain = data.toShort(1, true); + d->trackPeak = data.toShort(3, true); + d->albumGain = data.toShort(5, true); + d->albumPeak = data.toShort(7, true); + } + } + + else if(packetType == "SE") { + break; + } + + else { + file->seek(dataSize, File::Current); + } + } +} + +void MPC::Properties::readSV7(const ByteVector &data) +{ + if(data.startsWith("MP+")) { + d->version = data[3] & 15; + if(d->version < 7) + return; + + d->totalFrames = data.toUInt(4, false); + + std::bitset<32> flags(TAGLIB_CONSTRUCT_BITSET(data.toUInt(8, false))); + d->sampleRate = sftable[flags[17] * 2 + flags[16]]; + d->channels = 2; + + uint gapless = data.toUInt(5, false); + + d->trackGain = data.toShort(14, false); + d->trackPeak = data.toShort(12, false); + d->albumGain = data.toShort(18, false); + d->albumPeak = data.toShort(16, false); + + // convert gain info + if(d->trackGain != 0) { + int tmp = (int)((64.82 - (short)d->trackGain / 100.) * 256. + .5); + if(tmp >= (1 << 16) || tmp < 0) tmp = 0; + d->trackGain = tmp; + } + + if(d->albumGain != 0) { + int tmp = (int)((64.82 - d->albumGain / 100.) * 256. + .5); + if(tmp >= (1 << 16) || tmp < 0) tmp = 0; + d->albumGain = tmp; + } + + if (d->trackPeak != 0) + d->trackPeak = (int)(log10((double)d->trackPeak) * 20 * 256 + .5); + + if (d->albumPeak != 0) + d->albumPeak = (int)(log10((double)d->albumPeak) * 20 * 256 + .5); + + bool trueGapless = (gapless >> 31) & 0x0001; + if(trueGapless) { + uint lastFrameSamples = (gapless >> 20) & 0x07FF; + d->sampleFrames = d->totalFrames * 1152 - lastFrameSamples; + } + else + d->sampleFrames = d->totalFrames * 1152 - 576; + } + else { + uint headerData = data.toUInt(0, false); + + d->bitrate = (headerData >> 23) & 0x01ff; + d->version = (headerData >> 11) & 0x03ff; + d->sampleRate = 44100; + d->channels = 2; + + if(d->version >= 5) + d->totalFrames = data.toUInt(4, false); + else + d->totalFrames = data.toUShort(6, false); + + d->sampleFrames = d->totalFrames * 1152 - 576; + } + + d->length = d->sampleRate > 0 ? (d->sampleFrames + (d->sampleRate / 2)) / d->sampleRate : 0; + + if(!d->bitrate) + d->bitrate = d->length > 0 ? ((d->streamLength * 8L) / d->length) / 1000 : 0; +} + diff --git a/taglib/taglib/mpc/mpcproperties.h b/taglib/taglib/mpc/mpcproperties.h new file mode 100644 index 0000000..adf40d8 --- /dev/null +++ b/taglib/taglib/mpc/mpcproperties.h @@ -0,0 +1,122 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MPCPROPERTIES_H +#define TAGLIB_MPCPROPERTIES_H + +#include "taglib_export.h" +#include "audioproperties.h" + +namespace TagLib { + + namespace MPC { + + class File; + + static const uint HeaderSize = 8*7; + + //! An implementation of audio property reading for MPC + + /*! + * This reads the data from an MPC stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of MPC::Properties with the data read from the + * ByteVector \a data. + * + * This constructor is deprecated. It only works for MPC version up to 7. + */ + Properties(const ByteVector &data, long streamLength, ReadStyle style = Average); + + /*! + * Create an instance of MPC::Properties with the data read directly + * from a MPC::File. + */ + Properties(File *file, long streamLength, ReadStyle style = Average); + + /*! + * Destroys this MPC::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + /*! + * Returns the version of the bitstream (SV4-SV8) + */ + int mpcVersion() const; + uint totalFrames() const; + uint sampleFrames() const; + + /*! + * Returns the track gain as an integer value, + * to convert to dB: trackGain in dB = 64.82 - (trackGain / 256) + */ + int trackGain() const; + + /*! + * Returns the track peak as an integer value, + * to convert to dB: trackPeak in dB = trackPeak / 256 + * to convert to floating [-1..1]: trackPeak = 10^(trackPeak / 256 / 20)/32768 + */ + int trackPeak() const; + + /*! + * Returns the album gain as an integer value, + * to convert to dB: albumGain in dB = 64.82 - (albumGain / 256) + */ + int albumGain() const; + + /*! + * Returns the album peak as an integer value, + * to convert to dB: albumPeak in dB = albumPeak / 256 + * to convert to floating [-1..1]: albumPeak = 10^(albumPeak / 256 / 20)/32768 + */ + int albumPeak() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void readSV7(const ByteVector &data); + void readSV8(File *file); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v1/id3v1genres.cpp b/taglib/taglib/mpeg/id3v1/id3v1genres.cpp new file mode 100644 index 0000000..eba4c52 --- /dev/null +++ b/taglib/taglib/mpeg/id3v1/id3v1genres.cpp @@ -0,0 +1,219 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "id3v1genres.h" + +using namespace TagLib; + +namespace TagLib { + namespace ID3v1 { + + static const int genresSize = 148; + static const String genres[] = { + "Blues", + "Classic Rock", + "Country", + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", + "Metal", + "New Age", + "Oldies", + "Other", + "Pop", + "R&B", + "Rap", + "Reggae", + "Rock", + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "Alternative Rock", + "Bass", + "Soul", + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", + "Psychedelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", + "Folk", + "Folk/Rock", + "National Folk", + "Swing", + "Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A Cappella", + "Euro-House", + "Dance Hall", + "Goa", + "Drum & Bass", + "Club-House", + "Hardcore", + "Terror", + "Indie", + "BritPop", + "Negerpunk", + "Polsk Punk", + "Beat", + "Christian Gangsta Rap", + "Heavy Metal", + "Black Metal", + "Crossover", + "Contemporary Christian", + "Christian Rock", + "Merengue", + "Salsa", + "Thrash Metal", + "Anime", + "Jpop", + "Synthpop" + }; + } +} + +StringList ID3v1::genreList() +{ + static StringList l; + if(l.isEmpty()) { + for(int i = 0; i < genresSize; i++) + l.append(genres[i]); + } + return l; +} + +ID3v1::GenreMap ID3v1::genreMap() +{ + static GenreMap m; + if(m.isEmpty()) { + for(int i = 0; i < genresSize; i++) + m.insert(genres[i], i); + } + return m; +} + +String ID3v1::genre(int i) +{ + if(i >= 0 && i < genresSize) + return genres[i] + String::null; // always make a copy + return String::null; +} + +int ID3v1::genreIndex(const String &name) +{ + if(genreMap().contains(name)) + return genreMap()[name]; + return 255; +} diff --git a/taglib/taglib/mpeg/id3v1/id3v1genres.h b/taglib/taglib/mpeg/id3v1/id3v1genres.h new file mode 100644 index 0000000..271f725 --- /dev/null +++ b/taglib/taglib/mpeg/id3v1/id3v1genres.h @@ -0,0 +1,66 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V1GENRE_H +#define TAGLIB_ID3V1GENRE_H + +#include "tmap.h" +#include "tstringlist.h" +#include "taglib_export.h" + +namespace TagLib { + namespace ID3v1 { + + typedef Map<String, int> GenreMap; + + /*! + * Returns the list of canonical ID3v1 genre names in the order that they + * are listed in the standard. + */ + StringList TAGLIB_EXPORT genreList(); + + /*! + * A "reverse mapping" that goes from the canonical ID3v1 genre name to the + * respective genre number. genreMap()["Rock"] == + */ + GenreMap TAGLIB_EXPORT genreMap(); + + /*! + * Returns the name of the genre at \a index in the ID3v1 genre list. If + * \a index is out of range -- less than zero or greater than 146 -- a null + * string will be returned. + */ + String TAGLIB_EXPORT genre(int index); + + /*! + * Returns the genre index for the (case sensitive) genre \a name. If the + * genre is not in the list 255 (which signifies an unknown genre in ID3v1) + * will be returned. + */ + int TAGLIB_EXPORT genreIndex(const String &name); + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v1/id3v1tag.cpp b/taglib/taglib/mpeg/id3v1/id3v1tag.cpp new file mode 100644 index 0000000..9fc8cfd --- /dev/null +++ b/taglib/taglib/mpeg/id3v1/id3v1tag.cpp @@ -0,0 +1,265 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tfile.h> + +#include "id3v1tag.h" +#include "id3v1genres.h" + +using namespace TagLib; +using namespace ID3v1; + +class ID3v1::Tag::TagPrivate +{ +public: + TagPrivate() : file(0), tagOffset(-1), track(0), genre(255) {} + + File *file; + long tagOffset; + + String title; + String artist; + String album; + String year; + String comment; + uchar track; + uchar genre; + + static const StringHandler *stringHandler; +}; + +static const StringHandler defaultStringHandler; +const ID3v1::StringHandler *ID3v1::Tag::TagPrivate::stringHandler = &defaultStringHandler; + +//////////////////////////////////////////////////////////////////////////////// +// StringHandler implementation +//////////////////////////////////////////////////////////////////////////////// + +StringHandler::StringHandler() +{ +} + +String ID3v1::StringHandler::parse(const ByteVector &data) const +{ + return String(data, String::Latin1).stripWhiteSpace(); +} + +ByteVector ID3v1::StringHandler::render(const String &s) const +{ + if(!s.isLatin1()) + { + return ByteVector(); + } + + return s.data(String::Latin1); +} + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +ID3v1::Tag::Tag() : TagLib::Tag() +{ + d = new TagPrivate; +} + +ID3v1::Tag::Tag(File *file, long tagOffset) : TagLib::Tag() +{ + d = new TagPrivate; + d->file = file; + d->tagOffset = tagOffset; + + read(); +} + +ID3v1::Tag::~Tag() +{ + delete d; +} + +ByteVector ID3v1::Tag::render() const +{ + ByteVector data; + + data.append(fileIdentifier()); + data.append(TagPrivate::stringHandler->render(d->title).resize(30)); + data.append(TagPrivate::stringHandler->render(d->artist).resize(30)); + data.append(TagPrivate::stringHandler->render(d->album).resize(30)); + data.append(TagPrivate::stringHandler->render(d->year).resize(4)); + data.append(TagPrivate::stringHandler->render(d->comment).resize(28)); + data.append(char(0)); + data.append(char(d->track)); + data.append(char(d->genre)); + + return data; +} + +ByteVector ID3v1::Tag::fileIdentifier() +{ + return ByteVector::fromCString("TAG"); +} + +String ID3v1::Tag::title() const +{ + return d->title; +} + +String ID3v1::Tag::artist() const +{ + return d->artist; +} + +String ID3v1::Tag::album() const +{ + return d->album; +} + +String ID3v1::Tag::comment() const +{ + return d->comment; +} + +String ID3v1::Tag::genre() const +{ + return ID3v1::genre(d->genre); +} + +TagLib::uint ID3v1::Tag::year() const +{ + return d->year.toInt(); +} + +TagLib::uint ID3v1::Tag::track() const +{ + return d->track; +} + +void ID3v1::Tag::setTitle(const String &s) +{ + d->title = s; +} + +void ID3v1::Tag::setArtist(const String &s) +{ + d->artist = s; +} + +void ID3v1::Tag::setAlbum(const String &s) +{ + d->album = s; +} + +void ID3v1::Tag::setComment(const String &s) +{ + d->comment = s; +} + +void ID3v1::Tag::setGenre(const String &s) +{ + d->genre = ID3v1::genreIndex(s); +} + +void ID3v1::Tag::setYear(TagLib::uint i) +{ + d->year = i > 0 ? String::number(i) : String::null; +} + +void ID3v1::Tag::setTrack(TagLib::uint i) +{ + d->track = i < 256 ? i : 0; +} + +TagLib::uint ID3v1::Tag::genreNumber() const +{ + return d->genre; +} + +void ID3v1::Tag::setGenreNumber(TagLib::uint i) +{ + d->genre = i < 256 ? i : 255; +} + +void ID3v1::Tag::setStringHandler(const StringHandler *handler) +{ + if (handler) + TagPrivate::stringHandler = handler; + else + TagPrivate::stringHandler = &defaultStringHandler; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected methods +//////////////////////////////////////////////////////////////////////////////// + +void ID3v1::Tag::read() +{ + if(d->file && d->file->isValid()) { + d->file->seek(d->tagOffset); + // read the tag -- always 128 bytes + ByteVector data = d->file->readBlock(128); + + // some initial sanity checking + if(data.size() == 128 && data.startsWith("TAG")) + parse(data); + else + debug("ID3v1 tag is not valid or could not be read at the specified offset."); + } +} + +void ID3v1::Tag::parse(const ByteVector &data) +{ + int offset = 3; + + d->title = TagPrivate::stringHandler->parse(data.mid(offset, 30)); + offset += 30; + + d->artist = TagPrivate::stringHandler->parse(data.mid(offset, 30)); + offset += 30; + + d->album = TagPrivate::stringHandler->parse(data.mid(offset, 30)); + offset += 30; + + d->year = TagPrivate::stringHandler->parse(data.mid(offset, 4)); + offset += 4; + + // Check for ID3v1.1 -- Note that ID3v1 *does not* support "track zero" -- this + // is not a bug in TagLib. Since a zeroed byte is what we would expect to + // indicate the end of a C-String, specifically the comment string, a value of + // zero must be assumed to be just that. + + if(data[offset + 28] == 0 && data[offset + 29] != 0) { + // ID3v1.1 detected + + d->comment = TagPrivate::stringHandler->parse(data.mid(offset, 28)); + d->track = uchar(data[offset + 29]); + } + else + d->comment = data.mid(offset, 30); + + offset += 30; + + d->genre = uchar(data[offset]); +} diff --git a/taglib/taglib/mpeg/id3v1/id3v1tag.h b/taglib/taglib/mpeg/id3v1/id3v1tag.h new file mode 100644 index 0000000..7b26d02 --- /dev/null +++ b/taglib/taglib/mpeg/id3v1/id3v1tag.h @@ -0,0 +1,202 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V1TAG_H +#define TAGLIB_ID3V1TAG_H + +#include "tag.h" +#include "tbytevector.h" +#include "taglib_export.h" + +namespace TagLib { + + class File; + + //! An ID3v1 implementation + + namespace ID3v1 { + + //! A abstraction for the string to data encoding in ID3v1 tags. + + /*! + * ID3v1 should in theory always contain ISO-8859-1 (Latin1) data. In + * practice it does not. TagLib by default only supports ISO-8859-1 data + * in ID3v1 tags. + * + * However by subclassing this class and reimplementing parse() and render() + * and setting your reimplementation as the default with + * ID3v1::Tag::setStringHandler() you can define how you would like these + * transformations to be done. + * + * \warning It is advisable <b>not</b> to write non-ISO-8859-1 data to ID3v1 + * tags. Please consider disabling the writing of ID3v1 tags in the case + * that the data is not ISO-8859-1. + * + * \see ID3v1::Tag::setStringHandler() + */ + + class TAGLIB_EXPORT StringHandler + { + TAGLIB_IGNORE_MISSING_DESTRUCTOR + public: + // BIC: Add virtual destructor. + StringHandler(); + + /*! + * Decode a string from \a data. The default implementation assumes that + * \a data is an ISO-8859-1 (Latin1) character array. + */ + virtual String parse(const ByteVector &data) const; + + /*! + * Encode a ByteVector with the data from \a s. The default implementation + * assumes that \a s is an ISO-8859-1 (Latin1) string. If the string is + * does not conform to ISO-8859-1, no value is written. + * + * \warning It is recommended that you <b>not</b> override this method, but + * instead do not write an ID3v1 tag in the case that the data is not + * ISO-8859-1. + */ + virtual ByteVector render(const String &s) const; + }; + + //! The main class in the ID3v1 implementation + + /*! + * This is an implementation of the ID3v1 format. ID3v1 is both the simplist + * and most common of tag formats but is rather limited. Because of its + * pervasiveness and the way that applications have been written around the + * fields that it provides, the generic TagLib::Tag API is a mirror of what is + * provided by ID3v1. + * + * ID3v1 tags should generally only contain Latin1 information. However because + * many applications do not follow this rule there is now support for overriding + * the ID3v1 string handling using the ID3v1::StringHandler class. Please see + * the documentation for that class for more information. + * + * \see StringHandler + * + * \note Most fields are truncated to a maximum of 28-30 bytes. The + * truncation happens automatically when the tag is rendered. + */ + + class TAGLIB_EXPORT Tag : public TagLib::Tag + { + public: + /*! + * Create an ID3v1 tag with default values. + */ + Tag(); + + /*! + * Create an ID3v1 tag and parse the data in \a file starting at + * \a tagOffset. + */ + Tag(File *file, long tagOffset); + + /*! + * Destroys this Tag instance. + */ + virtual ~Tag(); + + /*! + * Renders the in memory values to a ByteVector suitable for writing to + * the file. + */ + ByteVector render() const; + + /*! + * Returns the string "TAG" suitable for usage in locating the tag in a + * file. + */ + static ByteVector fileIdentifier(); + + // Reimplementations. + + virtual String title() const; + virtual String artist() const; + virtual String album() const; + virtual String comment() const; + virtual String genre() const; + virtual TagLib::uint year() const; + virtual TagLib::uint track() const; + + virtual void setTitle(const String &s); + virtual void setArtist(const String &s); + virtual void setAlbum(const String &s); + virtual void setComment(const String &s); + virtual void setGenre(const String &s); + virtual void setYear(TagLib::uint i); + virtual void setTrack(TagLib::uint i); + + /*! + * Returns the genre in number. + * + * /note Normally 255 indicates that this tag contains no genre. + */ + TagLib::uint genreNumber() const; + + /*! + * Sets the genre in number to \a i. + * + * /note Valid value is from 0 up to 255. Normally 255 indicates that + * this tag contains no genre. + */ + void setGenreNumber(TagLib::uint i); + + /*! + * Sets the string handler that decides how the ID3v1 data will be + * converted to and from binary data. + * If the parameter \a handler is null, the previous handler is + * released and default ISO-8859-1 handler is restored. + * + * \note The caller is responsible for deleting the previous handler + * as needed after it is released. + * + * \see StringHandler + */ + static void setStringHandler(const StringHandler *handler); + + protected: + /*! + * Reads from the file specified in the constructor. + */ + void read(); + /*! + * Pareses the body of the tag in \a data. + */ + void parse(const ByteVector &data); + + private: + Tag(const Tag &); + Tag &operator=(const Tag &); + + class TagPrivate; + TagPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/attachedpictureframe.cpp b/taglib/taglib/mpeg/id3v2/frames/attachedpictureframe.cpp new file mode 100644 index 0000000..86f8e35 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/attachedpictureframe.cpp @@ -0,0 +1,222 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "attachedpictureframe.h" + +#include <tstringlist.h> +#include <tdebug.h> + +using namespace TagLib; +using namespace ID3v2; + +class AttachedPictureFrame::AttachedPictureFramePrivate +{ +public: + AttachedPictureFramePrivate() : textEncoding(String::Latin1), + type(AttachedPictureFrame::Other) {} + + String::Type textEncoding; + String mimeType; + AttachedPictureFrame::Type type; + String description; + ByteVector data; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +AttachedPictureFrame::AttachedPictureFrame() : Frame("APIC") +{ + d = new AttachedPictureFramePrivate; +} + +AttachedPictureFrame::AttachedPictureFrame(const ByteVector &data) : Frame(data) +{ + d = new AttachedPictureFramePrivate; + setData(data); +} + +AttachedPictureFrame::~AttachedPictureFrame() +{ + delete d; +} + +String AttachedPictureFrame::toString() const +{ + String s = "[" + d->mimeType + "]"; + return d->description.isEmpty() ? s : d->description + " " + s; +} + +String::Type AttachedPictureFrame::textEncoding() const +{ + return d->textEncoding; +} + +void AttachedPictureFrame::setTextEncoding(String::Type t) +{ + d->textEncoding = t; +} + +String AttachedPictureFrame::mimeType() const +{ + return d->mimeType; +} + +void AttachedPictureFrame::setMimeType(const String &m) +{ + d->mimeType = m; +} + +AttachedPictureFrame::Type AttachedPictureFrame::type() const +{ + return d->type; +} + +void AttachedPictureFrame::setType(Type t) +{ + d->type = t; +} + +String AttachedPictureFrame::description() const +{ + return d->description; +} + +void AttachedPictureFrame::setDescription(const String &desc) +{ + d->description = desc; +} + +ByteVector AttachedPictureFrame::picture() const +{ + return d->data; +} + +void AttachedPictureFrame::setPicture(const ByteVector &p) +{ + d->data = p; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void AttachedPictureFrame::parseFields(const ByteVector &data) +{ + if(data.size() < 5) { + debug("A picture frame must contain at least 5 bytes."); + return; + } + + d->textEncoding = String::Type(data[0]); + + int pos = 1; + + d->mimeType = readStringField(data, String::Latin1, &pos); + /* Now we need at least two more bytes available */ + if (uint(pos) + 1 >= data.size()) { + debug("Truncated picture frame."); + return; + } + + d->type = (TagLib::ID3v2::AttachedPictureFrame::Type)data[pos++]; + d->description = readStringField(data, d->textEncoding, &pos); + + d->data = data.mid(pos); +} + +ByteVector AttachedPictureFrame::renderFields() const +{ + ByteVector data; + + String::Type encoding = checkTextEncoding(d->description, d->textEncoding); + + data.append(char(encoding)); + data.append(d->mimeType.data(String::Latin1)); + data.append(textDelimiter(String::Latin1)); + data.append(char(d->type)); + data.append(d->description.data(encoding)); + data.append(textDelimiter(encoding)); + data.append(d->data); + + return data; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +AttachedPictureFrame::AttachedPictureFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new AttachedPictureFramePrivate; + parseFields(fieldData(data)); +} + +//////////////////////////////////////////////////////////////////////////////// +// support for ID3v2.2 PIC frames +//////////////////////////////////////////////////////////////////////////////// + +void AttachedPictureFrameV22::parseFields(const ByteVector &data) +{ + if(data.size() < 5) { + debug("A picture frame must contain at least 5 bytes."); + return; + } + + d->textEncoding = String::Type(data[0]); + + int pos = 1; + + String fixedString = String(data.mid(pos, 3), String::Latin1); + pos += 3; + // convert fixed string image type to mime string + if (fixedString.upper() == "JPG") { + d->mimeType = "image/jpeg"; + } else if (fixedString.upper() == "PNG") { + d->mimeType = "image/png"; + } else { + debug("probably unsupported image type"); + d->mimeType = "image/" + fixedString; + } + + d->type = (TagLib::ID3v2::AttachedPictureFrame::Type)data[pos++]; + d->description = readStringField(data, d->textEncoding, &pos); + + d->data = data.mid(pos); +} + +AttachedPictureFrameV22::AttachedPictureFrameV22(const ByteVector &data, Header *h) +{ + // set v2.2 header to make fieldData work correctly + setHeader(h, true); + + parseFields(fieldData(data)); + + // now set the v2.4 header + Frame::Header *newHeader = new Frame::Header("APIC"); + newHeader->setFrameSize(h->frameSize()); + setHeader(newHeader, true); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/attachedpictureframe.h b/taglib/taglib/mpeg/id3v2/frames/attachedpictureframe.h new file mode 100644 index 0000000..55067bd --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/attachedpictureframe.h @@ -0,0 +1,230 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ATTACHEDPICTUREFRAME_H +#define TAGLIB_ATTACHEDPICTUREFRAME_H + +#include "id3v2frame.h" +#include "id3v2header.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! An ID3v2 attached picture frame implementation + + /*! + * This is an implementation of ID3v2 attached pictures. Pictures may be + * included in tags, one per APIC frame (but there may be multiple APIC + * frames in a single tag). These pictures are usually in either JPEG or + * PNG format. + */ + + class TAGLIB_EXPORT AttachedPictureFrame : public Frame + { + friend class FrameFactory; + + public: + + /*! + * This describes the function or content of the picture. + */ + enum Type { + //! A type not enumerated below + Other = 0x00, + //! 32x32 PNG image that should be used as the file icon + FileIcon = 0x01, + //! File icon of a different size or format + OtherFileIcon = 0x02, + //! Front cover image of the album + FrontCover = 0x03, + //! Back cover image of the album + BackCover = 0x04, + //! Inside leaflet page of the album + LeafletPage = 0x05, + //! Image from the album itself + Media = 0x06, + //! Picture of the lead artist or soloist + LeadArtist = 0x07, + //! Picture of the artist or performer + Artist = 0x08, + //! Picture of the conductor + Conductor = 0x09, + //! Picture of the band or orchestra + Band = 0x0A, + //! Picture of the composer + Composer = 0x0B, + //! Picture of the lyricist or text writer + Lyricist = 0x0C, + //! Picture of the recording location or studio + RecordingLocation = 0x0D, + //! Picture of the artists during recording + DuringRecording = 0x0E, + //! Picture of the artists during performance + DuringPerformance = 0x0F, + //! Picture from a movie or video related to the track + MovieScreenCapture = 0x10, + //! Picture of a large, coloured fish + ColouredFish = 0x11, + //! Illustration related to the track + Illustration = 0x12, + //! Logo of the band or performer + BandLogo = 0x13, + //! Logo of the publisher (record company) + PublisherLogo = 0x14 + }; + + /*! + * Constructs an empty picture frame. The description, content and text + * encoding should be set manually. + */ + AttachedPictureFrame(); + + /*! + * Constructs an AttachedPicture frame based on \a data. + */ + explicit AttachedPictureFrame(const ByteVector &data); + + /*! + * Destroys the AttahcedPictureFrame instance. + */ + virtual ~AttachedPictureFrame(); + + /*! + * Returns a string containing the description and mime-type + */ + virtual String toString() const; + + /*! + * Returns the text encoding used for the description. + * + * \see setTextEncoding() + * \see description() + */ + String::Type textEncoding() const; + + /*! + * Set the text encoding used for the description. + * + * \see description() + */ + void setTextEncoding(String::Type t); + + /*! + * Returns the mime type of the image. This should in most cases be + * "image/png" or "image/jpeg". + */ + String mimeType() const; + + /*! + * Sets the mime type of the image. This should in most cases be + * "image/png" or "image/jpeg". + */ + void setMimeType(const String &m); + + /*! + * Returns the type of the image. + * + * \see Type + * \see setType() + */ + Type type() const; + + /*! + * Sets the type for the image. + * + * \see Type + * \see type() + */ + void setType(Type t); + + /*! + * Returns a text description of the image. + * + * \see setDescription() + * \see textEncoding() + * \see setTextEncoding() + */ + + String description() const; + + /*! + * Sets a textual description of the image to \a desc. + * + * \see description() + * \see textEncoding() + * \see setTextEncoding() + */ + + void setDescription(const String &desc); + + /*! + * Returns the image data as a ByteVector. + * + * \note ByteVector has a data() method that returns a const char * which + * should make it easy to export this data to external programs. + * + * \see setPicture() + * \see mimeType() + */ + ByteVector picture() const; + + /*! + * Sets the image data to \a p. \a p should be of the type specified in + * this frame's mime-type specification. + * + * \see picture() + * \see mimeType() + * \see setMimeType() + */ + void setPicture(const ByteVector &p); + + protected: + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + class AttachedPictureFramePrivate; + AttachedPictureFramePrivate *d; + + private: + AttachedPictureFrame(const AttachedPictureFrame &); + AttachedPictureFrame &operator=(const AttachedPictureFrame &); + AttachedPictureFrame(const ByteVector &data, Header *h); + + }; + + //! support for ID3v2.2 PIC frames + class TAGLIB_EXPORT AttachedPictureFrameV22 : public AttachedPictureFrame + { + protected: + virtual void parseFields(const ByteVector &data); + private: + AttachedPictureFrameV22(const ByteVector &data, Header *h); + friend class FrameFactory; + }; + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/commentsframe.cpp b/taglib/taglib/mpeg/id3v2/frames/commentsframe.cpp new file mode 100644 index 0000000..deaa9d9 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/commentsframe.cpp @@ -0,0 +1,197 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevectorlist.h> +#include <id3v2tag.h> +#include <tdebug.h> +#include <tstringlist.h> + +#include "commentsframe.h" +#include "tpropertymap.h" + +using namespace TagLib; +using namespace ID3v2; + +class CommentsFrame::CommentsFramePrivate +{ +public: + CommentsFramePrivate() : textEncoding(String::Latin1) {} + String::Type textEncoding; + ByteVector language; + String description; + String text; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +CommentsFrame::CommentsFrame(String::Type encoding) : Frame("COMM") +{ + d = new CommentsFramePrivate; + d->textEncoding = encoding; +} + +CommentsFrame::CommentsFrame(const ByteVector &data) : Frame(data) +{ + d = new CommentsFramePrivate; + setData(data); +} + +CommentsFrame::~CommentsFrame() +{ + delete d; +} + +String CommentsFrame::toString() const +{ + return d->text; +} + +ByteVector CommentsFrame::language() const +{ + return d->language; +} + +String CommentsFrame::description() const +{ + return d->description; +} + +String CommentsFrame::text() const +{ + return d->text; +} + +void CommentsFrame::setLanguage(const ByteVector &languageEncoding) +{ + d->language = languageEncoding.mid(0, 3); +} + +void CommentsFrame::setDescription(const String &s) +{ + d->description = s; +} + +void CommentsFrame::setText(const String &s) +{ + d->text = s; +} + +String::Type CommentsFrame::textEncoding() const +{ + return d->textEncoding; +} + +void CommentsFrame::setTextEncoding(String::Type encoding) +{ + d->textEncoding = encoding; +} + +PropertyMap CommentsFrame::asProperties() const +{ + String key = description().upper(); + PropertyMap map; + if(key.isEmpty() || key == "COMMENT") + map.insert("COMMENT", text()); + else if(key.isNull()) + map.unsupportedData().append(L"COMM/" + description()); + else + map.insert("COMMENT:" + key, text()); + return map; +} + +CommentsFrame *CommentsFrame::findByDescription(const ID3v2::Tag *tag, const String &d) // static +{ + ID3v2::FrameList comments = tag->frameList("COMM"); + + for(ID3v2::FrameList::ConstIterator it = comments.begin(); + it != comments.end(); + ++it) + { + CommentsFrame *frame = dynamic_cast<CommentsFrame *>(*it); + if(frame && frame->description() == d) + return frame; + } + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void CommentsFrame::parseFields(const ByteVector &data) +{ + if(data.size() < 5) { + debug("A comment frame must contain at least 5 bytes."); + return; + } + + d->textEncoding = String::Type(data[0]); + d->language = data.mid(1, 3); + + int byteAlign = d->textEncoding == String::Latin1 || d->textEncoding == String::UTF8 ? 1 : 2; + + ByteVectorList l = ByteVectorList::split(data.mid(4), textDelimiter(d->textEncoding), byteAlign, 2); + + if(l.size() == 2) { + if(d->textEncoding == String::Latin1) { + d->description = Tag::latin1StringHandler()->parse(l.front()); + d->text = Tag::latin1StringHandler()->parse(l.back()); + } else { + d->description = String(l.front(), d->textEncoding); + d->text = String(l.back(), d->textEncoding); + } + } +} + +ByteVector CommentsFrame::renderFields() const +{ + ByteVector v; + + String::Type encoding = d->textEncoding; + + encoding = checkTextEncoding(d->description, encoding); + encoding = checkTextEncoding(d->text, encoding); + + v.append(char(encoding)); + v.append(d->language.size() == 3 ? d->language : "XXX"); + v.append(d->description.data(encoding)); + v.append(textDelimiter(encoding)); + v.append(d->text.data(encoding)); + + return v; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +CommentsFrame::CommentsFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new CommentsFramePrivate(); + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/commentsframe.h b/taglib/taglib/mpeg/id3v2/frames/commentsframe.h new file mode 100644 index 0000000..f65f6f0 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/commentsframe.h @@ -0,0 +1,179 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_COMMENTSFRAME_H +#define TAGLIB_COMMENTSFRAME_H + +#include "id3v2frame.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! An implementation of ID3v2 comments + + /*! + * This implements the ID3v2 comment format. An ID3v2 comment concists of + * a language encoding, a description and a single text field. + */ + + class TAGLIB_EXPORT CommentsFrame : public Frame + { + friend class FrameFactory; + + public: + /*! + * Construct an empty comment frame that will use the text encoding + * \a encoding. + */ + explicit CommentsFrame(String::Type encoding = String::Latin1); + + /*! + * Construct a comment based on the data in \a data. + */ + explicit CommentsFrame(const ByteVector &data); + + /*! + * Destroys this CommentFrame instance. + */ + virtual ~CommentsFrame(); + + /*! + * Returns the text of this comment. + * + * \see text() + */ + virtual String toString() const; + + /*! + * Returns the language encoding as a 3 byte encoding as specified by + * <a href="http://en.wikipedia.org/wiki/ISO_639">ISO-639-2</a>. + * + * \note Most taggers simply ignore this value. + * + * \see setLanguage() + */ + ByteVector language() const; + + /*! + * Returns the description of this comment. + * + * \note Most taggers simply ignore this value. + * + * \see setDescription() + */ + String description() const; + + /*! + * Returns the text of this comment. + * + * \see setText() + */ + String text() const; + + /*! + * Set the language using the 3 byte language code from + * <a href="http://en.wikipedia.org/wiki/ISO_639">ISO-639-2</a> to + * \a languageCode. + * + * \see language() + */ + void setLanguage(const ByteVector &languageCode); + + /*! + * Sets the description of the comment to \a s. + * + * \see decription() + */ + void setDescription(const String &s); + + /*! + * Sets the text portion of the comment to \a s. + * + * \see text() + */ + virtual void setText(const String &s); + + /*! + * Returns the text encoding that will be used in rendering this frame. + * This defaults to the type that was either specified in the constructor + * or read from the frame when parsed. + * + * \see setTextEncoding() + * \see render() + */ + String::Type textEncoding() const; + + /*! + * Sets the text encoding to be used when rendering this frame to + * \a encoding. + * + * \see textEncoding() + * \see render() + */ + void setTextEncoding(String::Type encoding); + + /*! + * Parses this frame as PropertyMap with a single key. + * - if description() is empty or "COMMENT", the key will be "COMMENT" + * - if description() is not a valid PropertyMap key, the frame will be + * marked unsupported by an entry "COMM/<description>" in the unsupportedData() + * attribute of the returned map. + * - otherwise, the key will be "COMMENT:<description>" + * - The single value will be the frame's text(). + */ + PropertyMap asProperties() const; + + /*! + * Comments each have a unique description. This searches for a comment + * frame with the decription \a d and returns a pointer to it. If no + * frame is found that matches the given description null is returned. + * + * \see description() + */ + static CommentsFrame *findByDescription(const Tag *tag, const String &d); + + protected: + // Reimplementations. + + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + /*! + * The constructor used by the FrameFactory. + */ + CommentsFrame(const ByteVector &data, Header *h); + CommentsFrame(const CommentsFrame &); + CommentsFrame &operator=(const CommentsFrame &); + + class CommentsFramePrivate; + CommentsFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/generalencapsulatedobjectframe.cpp b/taglib/taglib/mpeg/id3v2/frames/generalencapsulatedobjectframe.cpp new file mode 100644 index 0000000..fa3509b --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/generalencapsulatedobjectframe.cpp @@ -0,0 +1,176 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + copyright : (C) 2006 by Aaron VonderHaar + email : avh4@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> + +#include "generalencapsulatedobjectframe.h" + +using namespace TagLib; +using namespace ID3v2; + +class GeneralEncapsulatedObjectFrame::GeneralEncapsulatedObjectFramePrivate +{ +public: + GeneralEncapsulatedObjectFramePrivate() : textEncoding(String::Latin1) {} + + String::Type textEncoding; + String mimeType; + String fileName; + String description; + ByteVector data; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +GeneralEncapsulatedObjectFrame::GeneralEncapsulatedObjectFrame() : Frame("GEOB") +{ + d = new GeneralEncapsulatedObjectFramePrivate; +} + +GeneralEncapsulatedObjectFrame::GeneralEncapsulatedObjectFrame(const ByteVector &data) : Frame(data) +{ + d = new GeneralEncapsulatedObjectFramePrivate; + setData(data); +} + +GeneralEncapsulatedObjectFrame::~GeneralEncapsulatedObjectFrame() +{ + delete d; +} + +String GeneralEncapsulatedObjectFrame::toString() const +{ + String text = "[" + d->mimeType + "]"; + + if(!d->fileName.isEmpty()) + text += " " + d->fileName; + + if(!d->description.isEmpty()) + text += " \"" + d->description + "\""; + + return text; +} + +String::Type GeneralEncapsulatedObjectFrame::textEncoding() const +{ + return d->textEncoding; +} + +void GeneralEncapsulatedObjectFrame::setTextEncoding(String::Type encoding) +{ + d->textEncoding = encoding; +} + +String GeneralEncapsulatedObjectFrame::mimeType() const +{ + return d->mimeType; +} + +void GeneralEncapsulatedObjectFrame::setMimeType(const String &type) +{ + d->mimeType = type; +} + +String GeneralEncapsulatedObjectFrame::fileName() const +{ + return d->fileName; +} + +void GeneralEncapsulatedObjectFrame::setFileName(const String &name) +{ + d->fileName = name; +} + +String GeneralEncapsulatedObjectFrame::description() const +{ + return d->description; +} + +void GeneralEncapsulatedObjectFrame::setDescription(const String &desc) +{ + d->description = desc; +} + +ByteVector GeneralEncapsulatedObjectFrame::object() const +{ + return d->data; +} + +void GeneralEncapsulatedObjectFrame::setObject(const ByteVector &data) +{ + d->data = data; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void GeneralEncapsulatedObjectFrame::parseFields(const ByteVector &data) +{ + if(data.size() < 4) { + debug("An object frame must contain at least 4 bytes."); + return; + } + + d->textEncoding = String::Type(data[0]); + + int pos = 1; + + d->mimeType = readStringField(data, String::Latin1, &pos); + d->fileName = readStringField(data, d->textEncoding, &pos); + d->description = readStringField(data, d->textEncoding, &pos); + + d->data = data.mid(pos); +} + +ByteVector GeneralEncapsulatedObjectFrame::renderFields() const +{ + ByteVector data; + + data.append(char(d->textEncoding)); + data.append(d->mimeType.data(String::Latin1)); + data.append(textDelimiter(String::Latin1)); + data.append(d->fileName.data(d->textEncoding)); + data.append(textDelimiter(d->textEncoding)); + data.append(d->description.data(d->textEncoding)); + data.append(textDelimiter(d->textEncoding)); + data.append(d->data); + + return data; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +GeneralEncapsulatedObjectFrame::GeneralEncapsulatedObjectFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new GeneralEncapsulatedObjectFramePrivate; + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/generalencapsulatedobjectframe.h b/taglib/taglib/mpeg/id3v2/frames/generalencapsulatedobjectframe.h new file mode 100644 index 0000000..42f854c --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/generalencapsulatedobjectframe.h @@ -0,0 +1,178 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + copyright : (C) 2006 by Aaron VonderHaar + email : avh4@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_GENERALENCAPSULATEDOBJECT_H +#define TAGLIB_GENERALENCAPSULATEDOBJECT_H + +#include "id3v2frame.h" +#include "id3v2header.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! An ID3v2 general encapsulated object frame implementation + + /*! + * This is an implementation of ID3v2 general encapsulated objects. + * Arbitrary binary data may be included in tags, stored in GEOB frames. + * There may be multiple GEOB frames in a single tag. Each GEOB it + * labelled with a content description (which may be blank), a required + * mime-type, and a file name (may be blank). The content description + * uniquely identifies the GEOB frame in the tag. + */ + + class TAGLIB_EXPORT GeneralEncapsulatedObjectFrame : public Frame + { + friend class FrameFactory; + + public: + + /*! + * Constructs an empty object frame. The description, file name and text + * encoding should be set manually. + */ + GeneralEncapsulatedObjectFrame(); + + /*! + * Constructs a GeneralEncapsulatedObjectFrame frame based on \a data. + * + * \warning This is \em not data for the encapsulated object, for that use + * setObject(). This constructor is used when reading the frame from the + * disk. + */ + explicit GeneralEncapsulatedObjectFrame(const ByteVector &data); + + /*! + * Destroys the GeneralEncapsulatedObjectFrame instance. + */ + virtual ~GeneralEncapsulatedObjectFrame(); + + /*! + * Returns a string containing the description, file name and mime-type + */ + virtual String toString() const; + + /*! + * Returns the text encoding used for the description and file name. + * + * \see setTextEncoding() + * \see description() + * \see fileName() + */ + String::Type textEncoding() const; + + /*! + * Set the text encoding used for the description and file name. + * + * \see description() + * \see fileName() + */ + void setTextEncoding(String::Type encoding); + + /*! + * Returns the mime type of the object. + */ + String mimeType() const; + + /*! + * Sets the mime type of the object. + */ + void setMimeType(const String &type); + + /*! + * Returns the file name of the object. + * + * \see setFileName() + */ + String fileName() const; + + /*! + * Sets the file name for the object. + * + * \see fileName() + */ + void setFileName(const String &name); + + /*! + * Returns the content description of the object. + * + * \see setDescription() + * \see textEncoding() + * \see setTextEncoding() + */ + + String description() const; + + /*! + * Sets the content description of the object to \a desc. + * + * \see description() + * \see textEncoding() + * \see setTextEncoding() + */ + + void setDescription(const String &desc); + + /*! + * Returns the object data as a ByteVector. + * + * \note ByteVector has a data() method that returns a const char * which + * should make it easy to export this data to external programs. + * + * \see setObject() + * \see mimeType() + */ + ByteVector object() const; + + /*! + * Sets the object data to \a data. \a data should be of the type specified in + * this frame's mime-type specification. + * + * \see object() + * \see mimeType() + * \see setMimeType() + */ + void setObject(const ByteVector &object); + + protected: + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + GeneralEncapsulatedObjectFrame(const ByteVector &data, Header *h); + GeneralEncapsulatedObjectFrame(const GeneralEncapsulatedObjectFrame &); + GeneralEncapsulatedObjectFrame &operator=(const GeneralEncapsulatedObjectFrame &); + + class GeneralEncapsulatedObjectFramePrivate; + GeneralEncapsulatedObjectFramePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/ownershipframe.cpp b/taglib/taglib/mpeg/id3v2/frames/ownershipframe.cpp new file mode 100644 index 0000000..9451c4c --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/ownershipframe.cpp @@ -0,0 +1,162 @@ +/*************************************************************************** + copyright : (C) 2012 by Rupert Daniel + email : rupert@cancelmonday.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> + +#include "ownershipframe.h" +#include <id3v2tag.h> + +using namespace TagLib; +using namespace ID3v2; + +class OwnershipFrame::OwnershipFramePrivate +{ +public: + String pricePaid; + String datePurchased; + String seller; + String::Type textEncoding; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +OwnershipFrame::OwnershipFrame(String::Type encoding) : Frame("OWNE") +{ + d = new OwnershipFramePrivate; + d->textEncoding = encoding; +} + +OwnershipFrame::OwnershipFrame(const ByteVector &data) : Frame(data) +{ + d = new OwnershipFramePrivate; + setData(data); +} + +OwnershipFrame::~OwnershipFrame() +{ + delete d; +} + +String OwnershipFrame::toString() const +{ + return "pricePaid=" + d->pricePaid + " datePurchased=" + d->datePurchased + " seller=" + d->seller; +} + +String OwnershipFrame::pricePaid() const +{ + return d->pricePaid; +} + +void OwnershipFrame::setPricePaid(const String &s) +{ + d->pricePaid = s; +} + +String OwnershipFrame::datePurchased() const +{ + return d->datePurchased; +} + +void OwnershipFrame::setDatePurchased(const String &s) +{ + d->datePurchased = s; +} + +String OwnershipFrame::seller() const +{ + return d->seller; +} + +void OwnershipFrame::setSeller(const String &s) +{ + d->seller = s; +} + +String::Type OwnershipFrame::textEncoding() const +{ + return d->textEncoding; +} + +void OwnershipFrame::setTextEncoding(String::Type encoding) +{ + d->textEncoding = encoding; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void OwnershipFrame::parseFields(const ByteVector &data) +{ + int pos = 0; + + // Get the text encoding + d->textEncoding = String::Type(data[0]); + pos += 1; + + // Read the price paid this is a null terminate string + d->pricePaid = readStringField(data, String::Latin1, &pos); + + // If we don't have at least 8 bytes left then don't parse the rest of the + // data + if(data.size() - pos < 8) { + return; + } + + // Read the date purchased YYYYMMDD + d->datePurchased = String(data.mid(pos, 8)); + pos += 8; + + // Read the seller + if(d->textEncoding == String::Latin1) + d->seller = Tag::latin1StringHandler()->parse(data.mid(pos)); + else + d->seller = String(data.mid(pos), d->textEncoding); +} + +ByteVector OwnershipFrame::renderFields() const +{ + ByteVector v; + + v.append(char(d->textEncoding)); + v.append(d->pricePaid.data(String::Latin1)); + v.append(textDelimiter(String::Latin1)); + v.append(d->datePurchased.data(String::Latin1)); + v.append(d->seller.data(d->textEncoding)); + + return v; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +OwnershipFrame::OwnershipFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new OwnershipFramePrivate; + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/ownershipframe.h b/taglib/taglib/mpeg/id3v2/frames/ownershipframe.h new file mode 100644 index 0000000..34fc912 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/ownershipframe.h @@ -0,0 +1,151 @@ +/*************************************************************************** + copyright : (C) 2012 by Rupert Daniel + email : rupert@cancelmonday.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_OWNERSHIPFRAME_H +#define TAGLIB_OWNERSHIPFRAME_H + +#include "id3v2frame.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! An implementation of ID3v2 "ownership" + + /*! + * This implements the ID3v2 ownership (OWNE frame). It consists of + * a price paid, a date purchased (YYYYMMDD) and the name of the seller. + */ + + class TAGLIB_EXPORT OwnershipFrame : public Frame + { + friend class FrameFactory; + + public: + /*! + * Construct an empty ownership frame. + */ + explicit OwnershipFrame(String::Type encoding = String::Latin1); + + /*! + * Construct a ownership based on the data in \a data. + */ + explicit OwnershipFrame(const ByteVector &data); + + /*! + * Destroys this OwnershipFrame instance. + */ + virtual ~OwnershipFrame(); + + /*! + * Returns the text of this popularimeter. + * + * \see text() + */ + virtual String toString() const; + + /*! + * Returns the date purchased. + * + * \see setDatePurchased() + */ + String datePurchased() const; + + /*! + * Set the date purchased. + * + * \see datePurchased() + */ + void setDatePurchased(const String &datePurchased); + + /*! + * Returns the price paid. + * + * \see setPricePaid() + */ + String pricePaid() const; + + /*! + * Set the price paid. + * + * \see pricePaid() + */ + void setPricePaid(const String &pricePaid); + + /*! + * Returns the seller. + * + * \see setSeller() + */ + String seller() const; + + /*! + * Set the seller. + * + * \see seller() + */ + void setSeller(const String &seller); + + /*! + * Returns the text encoding that will be used in rendering this frame. + * This defaults to the type that was either specified in the constructor + * or read from the frame when parsed. + * + * \see setTextEncoding() + * \see render() + */ + String::Type textEncoding() const; + + /*! + * Sets the text encoding to be used when rendering this frame to + * \a encoding. + * + * \see textEncoding() + * \see render() + */ + void setTextEncoding(String::Type encoding); + + protected: + // Reimplementations. + + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + /*! + * The constructor used by the FrameFactory. + */ + OwnershipFrame(const ByteVector &data, Header *h); + OwnershipFrame(const OwnershipFrame &); + OwnershipFrame &operator=(const OwnershipFrame &); + + class OwnershipFramePrivate; + OwnershipFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/popularimeterframe.cpp b/taglib/taglib/mpeg/id3v2/frames/popularimeterframe.cpp new file mode 100644 index 0000000..3d4429f --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/popularimeterframe.cpp @@ -0,0 +1,137 @@ +/*************************************************************************** + copyright : (C) 2008 by Lukas Lalinsky + email : lalinsky@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> + +#include "popularimeterframe.h" + +using namespace TagLib; +using namespace ID3v2; + +class PopularimeterFrame::PopularimeterFramePrivate +{ +public: + PopularimeterFramePrivate() : rating(0), counter(0) {} + String email; + int rating; + TagLib::uint counter; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +PopularimeterFrame::PopularimeterFrame() : Frame("POPM") +{ + d = new PopularimeterFramePrivate; +} + +PopularimeterFrame::PopularimeterFrame(const ByteVector &data) : Frame(data) +{ + d = new PopularimeterFramePrivate; + setData(data); +} + +PopularimeterFrame::~PopularimeterFrame() +{ + delete d; +} + +String PopularimeterFrame::toString() const +{ + return d->email + " rating=" + String::number(d->rating) + " counter=" + String::number(d->counter); +} + +String PopularimeterFrame::email() const +{ + return d->email; +} + +void PopularimeterFrame::setEmail(const String &s) +{ + d->email = s; +} + +int PopularimeterFrame::rating() const +{ + return d->rating; +} + +void PopularimeterFrame::setRating(int s) +{ + d->rating = s; +} + +TagLib::uint PopularimeterFrame::counter() const +{ + return d->counter; +} + +void PopularimeterFrame::setCounter(TagLib::uint s) +{ + d->counter = s; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void PopularimeterFrame::parseFields(const ByteVector &data) +{ + int pos = 0, size = int(data.size()); + + d->email = readStringField(data, String::Latin1, &pos); + + d->rating = 0; + d->counter = 0; + if(pos < size) { + d->rating = (unsigned char)(data[pos++]); + if(pos < size) { + d->counter = data.toUInt(static_cast<uint>(pos)); + } + } +} + +ByteVector PopularimeterFrame::renderFields() const +{ + ByteVector data; + + data.append(d->email.data(String::Latin1)); + data.append(textDelimiter(String::Latin1)); + data.append(char(d->rating)); + data.append(ByteVector::fromUInt(d->counter)); + + return data; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +PopularimeterFrame::PopularimeterFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new PopularimeterFramePrivate; + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/popularimeterframe.h b/taglib/taglib/mpeg/id3v2/frames/popularimeterframe.h new file mode 100644 index 0000000..d39f1aa --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/popularimeterframe.h @@ -0,0 +1,132 @@ +/*************************************************************************** + copyright : (C) 2008 by Lukas Lalinsky + email : lalinsky@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_POPULARIMETERFRAME_H +#define TAGLIB_POPULARIMETERFRAME_H + +#include "id3v2frame.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! An implementation of ID3v2 "popularimeter" + + /*! + * This implements the ID3v2 popularimeter (POPM frame). It concists of + * an email, a rating and an optional counter. + */ + + class TAGLIB_EXPORT PopularimeterFrame : public Frame + { + friend class FrameFactory; + + public: + /*! + * Construct an empty popularimeter frame. + */ + explicit PopularimeterFrame(); + + /*! + * Construct a popularimeter based on the data in \a data. + */ + explicit PopularimeterFrame(const ByteVector &data); + + /*! + * Destroys this PopularimeterFrame instance. + */ + virtual ~PopularimeterFrame(); + + /*! + * Returns the text of this popularimeter. + * + * \see text() + */ + virtual String toString() const; + + /*! + * Returns the email. + * + * \see setEmail() + */ + String email() const; + + /*! + * Set the email. + * + * \see email() + */ + void setEmail(const String &email); + + /*! + * Returns the rating. + * + * \see setRating() + */ + int rating() const; + + /*! + * Set the rating. + * + * \see rating() + */ + void setRating(int rating); + + /*! + * Returns the counter. + * + * \see setCounter() + */ + uint counter() const; + + /*! + * Set the counter. + * + * \see counter() + */ + void setCounter(uint counter); + + protected: + // Reimplementations. + + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + /*! + * The constructor used by the FrameFactory. + */ + PopularimeterFrame(const ByteVector &data, Header *h); + PopularimeterFrame(const PopularimeterFrame &); + PopularimeterFrame &operator=(const PopularimeterFrame &); + + class PopularimeterFramePrivate; + PopularimeterFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/privateframe.cpp b/taglib/taglib/mpeg/id3v2/frames/privateframe.cpp new file mode 100644 index 0000000..24ee0f3 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/privateframe.cpp @@ -0,0 +1,128 @@ +/*************************************************************************** + copyright : (C) 2008 by Serkan Kalyoncu + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevectorlist.h> +#include <id3v2tag.h> +#include <tdebug.h> + +#include "privateframe.h" + +using namespace TagLib; +using namespace ID3v2; + + +class PrivateFrame::PrivateFramePrivate +{ +public: + ByteVector data; + String owner; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +PrivateFrame::PrivateFrame() : Frame("PRIV") +{ + d = new PrivateFramePrivate; +} + +PrivateFrame::PrivateFrame(const ByteVector &data) : Frame(data) +{ + d = new PrivateFramePrivate; + setData(data); +} + +PrivateFrame::~PrivateFrame() +{ + delete d; +} + +String PrivateFrame::toString() const +{ + return d->owner; +} + +String PrivateFrame::owner() const +{ + return d->owner; +} + +ByteVector PrivateFrame::data() const +{ + return d->data; +} + +void PrivateFrame::setOwner(const String &s) +{ + d->owner = s; +} + +void PrivateFrame::setData(const ByteVector & data) +{ + d->data = data; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void PrivateFrame::parseFields(const ByteVector &data) +{ + if(data.size() < 2) { + debug("A private frame must contain at least 2 bytes."); + return; + } + + // Owner identifier is assumed to be Latin1 + + const int byteAlign = 1; + const int endOfOwner = data.find(textDelimiter(String::Latin1), 0, byteAlign); + + d->owner = String(data.mid(0, endOfOwner)); + d->data = data.mid(endOfOwner + 1); +} + +ByteVector PrivateFrame::renderFields() const +{ + ByteVector v; + + v.append(d->owner.data(String::Latin1)); + v.append(textDelimiter(String::Latin1)); + v.append(d->data); + + return v; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +PrivateFrame::PrivateFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new PrivateFramePrivate(); + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/privateframe.h b/taglib/taglib/mpeg/id3v2/frames/privateframe.h new file mode 100644 index 0000000..06f82f3 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/privateframe.h @@ -0,0 +1,111 @@ +/*************************************************************************** + copyright : (C) 2008 by Serkan Kalyoncu + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_PRIVATEFRAME_H +#define TAGLIB_PRIVATEFRAME_H + +#include "id3v2frame.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! An implementation of ID3v2 privateframe + + class TAGLIB_EXPORT PrivateFrame : public Frame + { + friend class FrameFactory; + + public: + /*! + * Construct an empty private frame. + */ + PrivateFrame(); + + /*! + * Construct a private frame based on the data in \a data. + * + * \note This is the constructor used when parsing the frame from a file. + */ + explicit PrivateFrame(const ByteVector &data); + + /*! + * Destroys this private frame instance. + */ + virtual ~PrivateFrame(); + + /*! + * Returns the text of this private frame, currently just the owner. + * + * \see text() + */ + virtual String toString() const; + + /*! + * \return The owner of the private frame. + * \note This should contain an email address or link to a website. + */ + String owner() const; + + /*! + * + */ + ByteVector data() const; + + /*! + * Sets the owner of the frame to \a s. + * \note This should contain an email address or link to a website. + */ + void setOwner(const String &s); + + /*! + * + */ + void setData(const ByteVector &v); + + protected: + // Reimplementations. + + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + /*! + * The constructor used by the FrameFactory. + */ + PrivateFrame(const ByteVector &data, Header *h); + + PrivateFrame(const PrivateFrame &); + PrivateFrame &operator=(const PrivateFrame &); + + class PrivateFramePrivate; + PrivateFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/relativevolumeframe.cpp b/taglib/taglib/mpeg/id3v2/frames/relativevolumeframe.cpp new file mode 100644 index 0000000..e3efbc3 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/relativevolumeframe.cpp @@ -0,0 +1,236 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tmap.h> + +#include "relativevolumeframe.h" + +using namespace TagLib; +using namespace ID3v2; + +static inline int bitsToBytes(int i) +{ + return i % 8 == 0 ? i / 8 : (i - i % 8) / 8 + 1; +} + +struct ChannelData +{ + ChannelData() : channelType(RelativeVolumeFrame::Other), volumeAdjustment(0) {} + + RelativeVolumeFrame::ChannelType channelType; + short volumeAdjustment; + RelativeVolumeFrame::PeakVolume peakVolume; +}; + +class RelativeVolumeFrame::RelativeVolumeFramePrivate +{ +public: + String identification; + Map<ChannelType, ChannelData> channels; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +RelativeVolumeFrame::RelativeVolumeFrame() : Frame("RVA2") +{ + d = new RelativeVolumeFramePrivate; +} + +RelativeVolumeFrame::RelativeVolumeFrame(const ByteVector &data) : Frame(data) +{ + d = new RelativeVolumeFramePrivate; + setData(data); +} + +RelativeVolumeFrame::~RelativeVolumeFrame() +{ + delete d; +} + +String RelativeVolumeFrame::toString() const +{ + return d->identification; +} + +List<RelativeVolumeFrame::ChannelType> RelativeVolumeFrame::channels() const +{ + List<ChannelType> l; + + Map<ChannelType, ChannelData>::ConstIterator it = d->channels.begin(); + for(; it != d->channels.end(); ++it) + l.append((*it).first); + + return l; +} + +// deprecated + +RelativeVolumeFrame::ChannelType RelativeVolumeFrame::channelType() const +{ + return MasterVolume; +} + +// deprecated + +void RelativeVolumeFrame::setChannelType(ChannelType) +{ + +} + +short RelativeVolumeFrame::volumeAdjustmentIndex(ChannelType type) const +{ + return d->channels.contains(type) ? d->channels[type].volumeAdjustment : 0; +} + +short RelativeVolumeFrame::volumeAdjustmentIndex() const +{ + return volumeAdjustmentIndex(MasterVolume); +} + +void RelativeVolumeFrame::setVolumeAdjustmentIndex(short index, ChannelType type) +{ + d->channels[type].volumeAdjustment = index; +} + +void RelativeVolumeFrame::setVolumeAdjustmentIndex(short index) +{ + setVolumeAdjustmentIndex(index, MasterVolume); +} + +float RelativeVolumeFrame::volumeAdjustment(ChannelType type) const +{ + return d->channels.contains(type) ? float(d->channels[type].volumeAdjustment) / float(512) : 0; +} + +float RelativeVolumeFrame::volumeAdjustment() const +{ + return volumeAdjustment(MasterVolume); +} + +void RelativeVolumeFrame::setVolumeAdjustment(float adjustment, ChannelType type) +{ + d->channels[type].volumeAdjustment = short(adjustment * float(512)); +} + +void RelativeVolumeFrame::setVolumeAdjustment(float adjustment) +{ + setVolumeAdjustment(adjustment, MasterVolume); +} + +RelativeVolumeFrame::PeakVolume RelativeVolumeFrame::peakVolume(ChannelType type) const +{ + return d->channels.contains(type) ? d->channels[type].peakVolume : PeakVolume(); +} + +RelativeVolumeFrame::PeakVolume RelativeVolumeFrame::peakVolume() const +{ + return peakVolume(MasterVolume); +} + +void RelativeVolumeFrame::setPeakVolume(const PeakVolume &peak, ChannelType type) +{ + d->channels[type].peakVolume = peak; +} + +void RelativeVolumeFrame::setPeakVolume(const PeakVolume &peak) +{ + setPeakVolume(peak, MasterVolume); +} + +String RelativeVolumeFrame::identification() const +{ + return d->identification; +} + +void RelativeVolumeFrame::setIdentification(const String &s) +{ + d->identification = s; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void RelativeVolumeFrame::parseFields(const ByteVector &data) +{ + int pos = 0; + d->identification = readStringField(data, String::Latin1, &pos); + + // Each channel is at least 4 bytes. + + while(pos <= (int)data.size() - 4) { + + + ChannelType type = ChannelType(data[pos]); + pos += 1; + + ChannelData &channel = d->channels[type]; + + channel.volumeAdjustment = data.toShort(static_cast<uint>(pos)); + pos += 2; + + channel.peakVolume.bitsRepresentingPeak = data[pos]; + pos += 1; + + int bytes = bitsToBytes(channel.peakVolume.bitsRepresentingPeak); + channel.peakVolume.peakVolume = data.mid(pos, bytes); + pos += bytes; + } +} + +ByteVector RelativeVolumeFrame::renderFields() const +{ + ByteVector data; + + data.append(d->identification.data(String::Latin1)); + data.append(textDelimiter(String::Latin1)); + + Map<ChannelType, ChannelData>::ConstIterator it = d->channels.begin(); + + for(; it != d->channels.end(); ++it) { + ChannelType type = (*it).first; + const ChannelData &channel = (*it).second; + + data.append(char(type)); + data.append(ByteVector::fromShort(channel.volumeAdjustment)); + data.append(char(channel.peakVolume.bitsRepresentingPeak)); + data.append(channel.peakVolume.peakVolume); + } + + return data; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +RelativeVolumeFrame::RelativeVolumeFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new RelativeVolumeFramePrivate; + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/relativevolumeframe.h b/taglib/taglib/mpeg/id3v2/frames/relativevolumeframe.h new file mode 100644 index 0000000..dad4e7d --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/relativevolumeframe.h @@ -0,0 +1,274 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_RELATIVEVOLUMEFRAME_H +#define TAGLIB_RELATIVEVOLUMEFRAME_H + +#include "tlist.h" +#include "id3v2frame.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! An ID3v2 relative volume adjustment frame implementation + + /*! + * This is an implementation of ID3v2 relative volume adjustment. The + * presence of this frame makes it possible to specify an increase in volume + * for an audio file or specific audio tracks in that file. + * + * Multiple relative volume adjustment frames may be present in the tag + * each with a unique identification and describing volume adjustment for + * different channel types. + */ + + class TAGLIB_EXPORT RelativeVolumeFrame : public Frame + { + friend class FrameFactory; + + public: + + /*! + * This indicates the type of volume adjustment that should be applied. + */ + enum ChannelType { + //! A type not enumerated below + Other = 0x00, + //! The master volume for the track + MasterVolume = 0x01, + //! The front right audio channel + FrontRight = 0x02, + //! The front left audio channel + FrontLeft = 0x03, + //! The back right audio channel + BackRight = 0x04, + //! The back left audio channel + BackLeft = 0x05, + //! The front center audio channel + FrontCentre = 0x06, + //! The back center audio channel + BackCentre = 0x07, + //! The subwoofer audio channel + Subwoofer = 0x08 + }; + + //! Struct that stores the relevant values for ID3v2 peak volume + + /*! + * The peak volume is described as a series of bits that is padded to fill + * a block of bytes. These two values should always be updated in tandem. + */ + struct PeakVolume + { + /*! + * Constructs an empty peak volume description. + */ + PeakVolume() : bitsRepresentingPeak(0) {} + /*! + * The number of bits (in the range of 0 to 255) used to describe the + * peak volume. + */ + unsigned char bitsRepresentingPeak; + /*! + * The array of bits (represented as a series of bytes) used to describe + * the peak volume. + */ + ByteVector peakVolume; + }; + + /*! + * Constructs a RelativeVolumeFrame. The relevant data should be set + * manually. + */ + RelativeVolumeFrame(); + + /*! + * Constructs a RelativeVolumeFrame based on the contents of \a data. + */ + RelativeVolumeFrame(const ByteVector &data); + + /*! + * Destroys the RelativeVolumeFrame instance. + */ + virtual ~RelativeVolumeFrame(); + + /*! + * Returns the frame's identification. + * + * \see identification() + */ + virtual String toString() const; + + /*! + * Returns a list of channels with information currently in the frame. + */ + List<ChannelType> channels() const; + + /*! + * \deprecated Always returns master volume. + */ + ChannelType channelType() const; + + /*! + * \deprecated This method no longer has any effect. + */ + void setChannelType(ChannelType t); + + /* + * There was a terrible API goof here, and while this can't be changed to + * the way it appears below for binary compaibility reasons, let's at + * least pretend that it looks clean. + */ + +#ifdef DOXYGEN + + /*! + * Returns the relative volume adjustment "index". As indicated by the + * ID3v2 standard this is a 16-bit signed integer that reflects the + * decibils of adjustment when divided by 512. + * + * This defaults to returning the value for the master volume channel if + * available and returns 0 if the specified channel does not exist. + * + * \see setVolumeAdjustmentIndex() + * \see volumeAjustment() + */ + short volumeAdjustmentIndex(ChannelType type = MasterVolume) const; + + /*! + * Set the volume adjustment to \a index. As indicated by the ID3v2 + * standard this is a 16-bit signed integer that reflects the decibils of + * adjustment when divided by 512. + * + * By default this sets the value for the master volume. + * + * \see volumeAdjustmentIndex() + * \see setVolumeAjustment() + */ + void setVolumeAdjustmentIndex(short index, ChannelType type = MasterVolume); + + /*! + * Returns the relative volume adjustment in decibels. + * + * \note Because this is actually stored internally as an "index" to this + * value the value returned by this method may not be identical to the + * value set using setVolumeAdjustment(). + * + * This defaults to returning the value for the master volume channel if + * available and returns 0 if the specified channel does not exist. + * + * \see setVolumeAdjustment() + * \see volumeAdjustmentIndex() + */ + float volumeAdjustment(ChannelType type = MasterVolume) const; + + /*! + * Set the relative volume adjustment in decibels to \a adjustment. + * + * By default this sets the value for the master volume. + * + * \note Because this is actually stored internally as an "index" to this + * value the value set by this method may not be identical to the one + * returned by volumeAdjustment(). + * + * \see setVolumeAdjustment() + * \see volumeAdjustmentIndex() + */ + void setVolumeAdjustment(float adjustment, ChannelType type = MasterVolume); + + /*! + * Returns the peak volume (represented as a length and a string of bits). + * + * This defaults to returning the value for the master volume channel if + * available and returns 0 if the specified channel does not exist. + * + * \see setPeakVolume() + */ + PeakVolume peakVolume(ChannelType type = MasterVolume) const; + + /*! + * Sets the peak volume to \a peak. + * + * By default this sets the value for the master volume. + * + * \see peakVolume() + */ + void setPeakVolume(const PeakVolume &peak, ChannelType type = MasterVolume); + +#else + + // BIC: Combine each of the following pairs of functions (or maybe just + // rework this junk altogether). + + short volumeAdjustmentIndex(ChannelType type) const; + short volumeAdjustmentIndex() const; + + void setVolumeAdjustmentIndex(short index, ChannelType type); + void setVolumeAdjustmentIndex(short index); + + float volumeAdjustment(ChannelType type) const; + float volumeAdjustment() const; + + void setVolumeAdjustment(float adjustment, ChannelType type); + void setVolumeAdjustment(float adjustment); + + PeakVolume peakVolume(ChannelType type) const; + PeakVolume peakVolume() const; + + void setPeakVolume(const PeakVolume &peak, ChannelType type); + void setPeakVolume(const PeakVolume &peak); + +#endif + + /*! + * Returns the identification for this frame. + */ + String identification() const; + + /*! + * Sets the identification of the frame to \a s. The string + * is used to identify the situation and/or device where this + * adjustment should apply. + */ + void setIdentification(const String &s); + + protected: + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + RelativeVolumeFrame(const ByteVector &data, Header *h); + RelativeVolumeFrame(const RelativeVolumeFrame &); + RelativeVolumeFrame &operator=(const RelativeVolumeFrame &); + + class RelativeVolumeFramePrivate; + RelativeVolumeFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/textidentificationframe.cpp b/taglib/taglib/mpeg/id3v2/frames/textidentificationframe.cpp new file mode 100644 index 0000000..70ea50f --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/textidentificationframe.cpp @@ -0,0 +1,423 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevectorlist.h> +#include <id3v2tag.h> +#include "textidentificationframe.h" +#include "tpropertymap.h" +#include "id3v1genres.h" + +using namespace TagLib; +using namespace ID3v2; + +class TextIdentificationFrame::TextIdentificationFramePrivate +{ +public: + TextIdentificationFramePrivate() : textEncoding(String::Latin1) {} + String::Type textEncoding; + StringList fieldList; +}; + +//////////////////////////////////////////////////////////////////////////////// +// TextIdentificationFrame public members +//////////////////////////////////////////////////////////////////////////////// + +TextIdentificationFrame::TextIdentificationFrame(const ByteVector &type, String::Type encoding) : + Frame(type) +{ + d = new TextIdentificationFramePrivate; + d->textEncoding = encoding; +} + +TextIdentificationFrame::TextIdentificationFrame(const ByteVector &data) : + Frame(data) +{ + d = new TextIdentificationFramePrivate; + setData(data); +} + +TextIdentificationFrame *TextIdentificationFrame::createTIPLFrame(const PropertyMap &properties) // static +{ + TextIdentificationFrame *frame = new TextIdentificationFrame("TIPL"); + StringList l; + for(PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it){ + l.append(it->first); + l.append(it->second.toString(",")); // comma-separated list of names + } + frame->setText(l); + return frame; +} + +TextIdentificationFrame *TextIdentificationFrame::createTMCLFrame(const PropertyMap &properties) // static +{ + TextIdentificationFrame *frame = new TextIdentificationFrame("TMCL"); + StringList l; + for(PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it){ + if(!it->first.startsWith(instrumentPrefix)) // should not happen + continue; + l.append(it->first.substr(instrumentPrefix.size())); + l.append(it->second.toString(",")); + } + frame->setText(l); + return frame; +} + +TextIdentificationFrame::~TextIdentificationFrame() +{ + delete d; +} + +void TextIdentificationFrame::setText(const StringList &l) +{ + d->fieldList = l; +} + +void TextIdentificationFrame::setText(const String &s) +{ + d->fieldList = s; +} + +String TextIdentificationFrame::toString() const +{ + return d->fieldList.toString(); +} + +StringList TextIdentificationFrame::fieldList() const +{ + return d->fieldList; +} + +String::Type TextIdentificationFrame::textEncoding() const +{ + return d->textEncoding; +} + +void TextIdentificationFrame::setTextEncoding(String::Type encoding) +{ + d->textEncoding = encoding; +} + +// array of allowed TIPL prefixes and their corresponding key value +static const TagLib::uint involvedPeopleSize = 5; +static const char* involvedPeople[][2] = { + {"ARRANGER", "ARRANGER"}, + {"ENGINEER", "ENGINEER"}, + {"PRODUCER", "PRODUCER"}, + {"DJ-MIX", "DJMIXER"}, + {"MIX", "MIXER"}, +}; + +const KeyConversionMap &TextIdentificationFrame::involvedPeopleMap() // static +{ + static KeyConversionMap m; + if(m.isEmpty()) + for(uint i = 0; i < involvedPeopleSize; ++i) + m.insert(involvedPeople[i][1], involvedPeople[i][0]); + return m; +} + +PropertyMap TextIdentificationFrame::asProperties() const +{ + if(frameID() == "TIPL") + return makeTIPLProperties(); + if(frameID() == "TMCL") + return makeTMCLProperties(); + PropertyMap map; + String tagName = frameIDToKey(frameID()); + if(tagName.isNull()) { + map.unsupportedData().append(frameID()); + return map; + } + StringList values = fieldList(); + if(tagName == "GENRE") { + // Special case: Support ID3v1-style genre numbers. They are not officially supported in + // ID3v2, however it seems that still a lot of programs use them. + for(StringList::Iterator it = values.begin(); it != values.end(); ++it) { + bool ok = false; + int test = it->toInt(&ok); // test if the genre value is an integer + if(ok) + *it = ID3v1::genre(test); + } + } else if(tagName == "DATE") { + for(StringList::Iterator it = values.begin(); it != values.end(); ++it) { + // ID3v2 specifies ISO8601 timestamps which contain a 'T' as separator between date and time. + // Since this is unusual in other formats, the T is removed. + int tpos = it->find("T"); + if(tpos != -1) + (*it)[tpos] = ' '; + } + } + PropertyMap ret; + ret.insert(tagName, values); + return ret; +} + +//////////////////////////////////////////////////////////////////////////////// +// TextIdentificationFrame protected members +//////////////////////////////////////////////////////////////////////////////// + +void TextIdentificationFrame::parseFields(const ByteVector &data) +{ + // Don't try to parse invalid frames + + if(data.size() < 2) + return; + + // read the string data type (the first byte of the field data) + + d->textEncoding = String::Type(data[0]); + + // split the byte array into chunks based on the string type (two byte delimiter + // for unicode encodings) + + int byteAlign = d->textEncoding == String::Latin1 || d->textEncoding == String::UTF8 ? 1 : 2; + + // build a small counter to strip nulls off the end of the field + + int dataLength = data.size() - 1; + + while(dataLength > 0 && data[dataLength] == 0) + dataLength--; + + while(dataLength % byteAlign != 0) + dataLength++; + + ByteVectorList l = ByteVectorList::split(data.mid(1, dataLength), textDelimiter(d->textEncoding), byteAlign); + + d->fieldList.clear(); + + // append those split values to the list and make sure that the new string's + // type is the same specified for this frame + + for(ByteVectorList::Iterator it = l.begin(); it != l.end(); it++) { + if(!(*it).isEmpty()) { + if(d->textEncoding == String::Latin1) + d->fieldList.append(Tag::latin1StringHandler()->parse(*it)); + else + d->fieldList.append(String(*it, d->textEncoding)); + } + } +} + +ByteVector TextIdentificationFrame::renderFields() const +{ + String::Type encoding = checkTextEncoding(d->fieldList, d->textEncoding); + + ByteVector v; + + v.append(char(encoding)); + + for(StringList::ConstIterator it = d->fieldList.begin(); it != d->fieldList.end(); it++) { + + // Since the field list is null delimited, if this is not the first + // element in the list, append the appropriate delimiter for this + // encoding. + + if(it != d->fieldList.begin()) + v.append(textDelimiter(encoding)); + + v.append((*it).data(encoding)); + } + + return v; +} + +//////////////////////////////////////////////////////////////////////////////// +// TextIdentificationFrame private members +//////////////////////////////////////////////////////////////////////////////// + +TextIdentificationFrame::TextIdentificationFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new TextIdentificationFramePrivate; + parseFields(fieldData(data)); +} + +PropertyMap TextIdentificationFrame::makeTIPLProperties() const +{ + PropertyMap map; + if(fieldList().size() % 2 != 0){ + // according to the ID3 spec, TIPL must contain an even number of entries + map.unsupportedData().append(frameID()); + return map; + } + StringList l = fieldList(); + for(StringList::ConstIterator it = l.begin(); it != l.end(); ++it) { + bool found = false; + for(uint i = 0; i < involvedPeopleSize; ++i) + if(*it == involvedPeople[i][0]) { + map.insert(involvedPeople[i][1], (++it)->split(",")); + found = true; + break; + } + if(!found){ + // invalid involved role -> mark whole frame as unsupported in order to be consisten with writing + map.clear(); + map.unsupportedData().append(frameID()); + return map; + } + } + return map; +} + +PropertyMap TextIdentificationFrame::makeTMCLProperties() const +{ + PropertyMap map; + if(fieldList().size() % 2 != 0){ + // according to the ID3 spec, TMCL must contain an even number of entries + map.unsupportedData().append(frameID()); + return map; + } + StringList l = fieldList(); + for(StringList::ConstIterator it = l.begin(); it != l.end(); ++it) { + String instrument = it->upper(); + if(instrument.isNull()) { + // instrument is not a valid key -> frame unsupported + map.clear(); + map.unsupportedData().append(frameID()); + return map; + } + map.insert(L"PERFORMER:" + instrument, (++it)->split(",")); + } + return map; +} + +//////////////////////////////////////////////////////////////////////////////// +// UserTextIdentificationFrame public members +//////////////////////////////////////////////////////////////////////////////// + +UserTextIdentificationFrame::UserTextIdentificationFrame(String::Type encoding) : + TextIdentificationFrame("TXXX", encoding), + d(0) +{ + StringList l; + l.append(String::null); + l.append(String::null); + setText(l); +} + + +UserTextIdentificationFrame::UserTextIdentificationFrame(const ByteVector &data) : + TextIdentificationFrame(data) +{ + checkFields(); +} + +UserTextIdentificationFrame::UserTextIdentificationFrame(const String &description, const StringList &values, String::Type encoding) : + TextIdentificationFrame("TXXX", encoding), + d(0) +{ + setDescription(description); + setText(values); +} + +String UserTextIdentificationFrame::toString() const +{ + return "[" + description() + "] " + fieldList().toString(); +} + +String UserTextIdentificationFrame::description() const +{ + return !TextIdentificationFrame::fieldList().isEmpty() + ? TextIdentificationFrame::fieldList().front() + : String::null; +} + +StringList UserTextIdentificationFrame::fieldList() const +{ + // TODO: remove this function + + return TextIdentificationFrame::fieldList(); +} + +void UserTextIdentificationFrame::setText(const String &text) +{ + if(description().isEmpty()) + setDescription(String::null); + + TextIdentificationFrame::setText(StringList(description()).append(text)); +} + +void UserTextIdentificationFrame::setText(const StringList &fields) +{ + if(description().isEmpty()) + setDescription(String::null); + + TextIdentificationFrame::setText(StringList(description()).append(fields)); +} + +void UserTextIdentificationFrame::setDescription(const String &s) +{ + StringList l = fieldList(); + + if(l.isEmpty()) + l.append(s); + else + l[0] = s; + + TextIdentificationFrame::setText(l); +} + +PropertyMap UserTextIdentificationFrame::asProperties() const +{ + PropertyMap map; + String tagName = txxxToKey(description()); + StringList v = fieldList(); + for(StringList::ConstIterator it = v.begin(); it != v.end(); ++it) + if(it != v.begin()) + map.insert(tagName, *it); + return map; +} + +UserTextIdentificationFrame *UserTextIdentificationFrame::find( + ID3v2::Tag *tag, const String &description) // static +{ + FrameList l = tag->frameList("TXXX"); + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) { + UserTextIdentificationFrame *f = dynamic_cast<UserTextIdentificationFrame *>(*it); + if(f && f->description() == description) + return f; + } + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// UserTextIdentificationFrame private members +//////////////////////////////////////////////////////////////////////////////// + +UserTextIdentificationFrame::UserTextIdentificationFrame(const ByteVector &data, Header *h) : + TextIdentificationFrame(data, h) +{ + checkFields(); +} + +void UserTextIdentificationFrame::checkFields() +{ + int fields = fieldList().size(); + + if(fields == 0) + setDescription(String::null); + if(fields <= 1) + setText(String::null); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/textidentificationframe.h b/taglib/taglib/mpeg/id3v2/frames/textidentificationframe.h new file mode 100644 index 0000000..58ed63e --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/textidentificationframe.h @@ -0,0 +1,313 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_TEXTIDENTIFICATIONFRAME_H +#define TAGLIB_TEXTIDENTIFICATIONFRAME_H + +#include "tstringlist.h" +#include "tmap.h" +#include "taglib_export.h" + +#include "id3v2frame.h" + +namespace TagLib { + + namespace ID3v2 { + + class Tag; + typedef Map<String, String> KeyConversionMap; + + //! An ID3v2 text identification frame implementation + + /*! + * This is an implementation of the most common type of ID3v2 frame -- text + * identification frames. There are a number of variations on this. Those + * enumerated in the ID3v2.4 standard are: + * + * <ul> + * <li><b>TALB</b> Album/Movie/Show title</li> + * <li><b>TBPM</b> BPM (beats per minute)</li> + * <li><b>TCOM</b> Composer</li> + * <li><b>TCON</b> Content type</li> + * <li><b>TCOP</b> Copyright message</li> + * <li><b>TDEN</b> Encoding time</li> + * <li><b>TDLY</b> Playlist delay</li> + * <li><b>TDOR</b> Original release time</li> + * <li><b>TDRC</b> Recording time</li> + * <li><b>TDRL</b> Release time</li> + * <li><b>TDTG</b> Tagging time</li> + * <li><b>TENC</b> Encoded by</li> + * <li><b>TEXT</b> Lyricist/Text writer</li> + * <li><b>TFLT</b> File type</li> + * <li><b>TIPL</b> Involved people list</li> + * <li><b>TIT1</b> Content group description</li> + * <li><b>TIT2</b> Title/songname/content description</li> + * <li><b>TIT3</b> Subtitle/Description refinement</li> + * <li><b>TKEY</b> Initial key</li> + * <li><b>TLAN</b> Language(s)</li> + * <li><b>TLEN</b> Length</li> + * <li><b>TMCL</b> Musician credits list</li> + * <li><b>TMED</b> Media type</li> + * <li><b>TMOO</b> Mood</li> + * <li><b>TOAL</b> Original album/movie/show title</li> + * <li><b>TOFN</b> Original filename</li> + * <li><b>TOLY</b> Original lyricist(s)/text writer(s)</li> + * <li><b>TOPE</b> Original artist(s)/performer(s)</li> + * <li><b>TOWN</b> File owner/licensee</li> + * <li><b>TPE1</b> Lead performer(s)/Soloist(s)</li> + * <li><b>TPE2</b> Band/orchestra/accompaniment</li> + * <li><b>TPE3</b> Conductor/performer refinement</li> + * <li><b>TPE4</b> Interpreted, remixed, or otherwise modified by</li> + * <li><b>TPOS</b> Part of a set</li> + * <li><b>TPRO</b> Produced notice</li> + * <li><b>TPUB</b> Publisher</li> + * <li><b>TRCK</b> Track number/Position in set</li> + * <li><b>TRSN</b> Internet radio station name</li> + * <li><b>TRSO</b> Internet radio station owner</li> + * <li><b>TSOA</b> Album sort order</li> + * <li><b>TSOP</b> Performer sort order</li> + * <li><b>TSOT</b> Title sort order</li> + * <li><b>TSRC</b> ISRC (international standard recording code)</li> + * <li><b>TSSE</b> Software/Hardware and settings used for encoding</li> + * <li><b>TSST</b> Set subtitle</li> + * </ul> + * + * The ID3v2 Frames document gives a description of each of these formats + * and the expected order of strings in each. ID3v2::Header::frameID() can + * be used to determine the frame type. + * + * \note If non-Latin1 compatible strings are used with this class, even if + * the text encoding is set to Latin1, the frame will be written using UTF8 + * (with the encoding flag appropriately set in the output). + */ + + class TAGLIB_EXPORT TextIdentificationFrame : public Frame + { + friend class FrameFactory; + + public: + /*! + * Construct an empty frame of type \a type. Uses \a encoding as the + * default text encoding. + * + * \note In this case you must specify the text encoding as it + * resolves the ambiguity between constructors. + * + * \note Please see the note in the class description regarding Latin1. + */ + TextIdentificationFrame(const ByteVector &type, String::Type encoding); + + /*! + * This is a dual purpose constructor. \a data can either be binary data + * that should be parsed or (at a minimum) the frame ID. + */ + explicit TextIdentificationFrame(const ByteVector &data); + + /*! + * This is a special factory method to create a TIPL (involved people list) + * frame from the given \a properties. Will parse key=[list of values] data + * into the TIPL format as specified in the ID3 standard. + */ + static TextIdentificationFrame *createTIPLFrame(const PropertyMap &properties); + + /*! + * This is a special factory method to create a TMCL (musician credits list) + * frame from the given \a properties. Will parse key=[list of values] data + * into the TMCL format as specified in the ID3 standard, where key should be + * of the form instrumentPrefix:instrument. + */ + static TextIdentificationFrame *createTMCLFrame(const PropertyMap &properties); + /*! + * Destroys this TextIdentificationFrame instance. + */ + virtual ~TextIdentificationFrame(); + + /*! + * Text identification frames are a list of string fields. + * + * This function will accept either a StringList or a String (using the + * StringList constructor that accepts a single String). + * + * \note This will not change the text encoding of the frame even if the + * strings passed in are not of the same encoding. Please use + * setEncoding(s.type()) if you wish to change the encoding of the frame. + */ + void setText(const StringList &l); + + // Reimplementations. + + virtual void setText(const String &s); + virtual String toString() const; + + /*! + * Returns the text encoding that will be used in rendering this frame. + * This defaults to the type that was either specified in the constructor + * or read from the frame when parsed. + * + * \note Please see the note in the class description regarding Latin1. + * + * \see setTextEncoding() + * \see render() + */ + String::Type textEncoding() const; + + /*! + * Sets the text encoding to be used when rendering this frame to + * \a encoding. + * + * \note Please see the note in the class description regarding Latin1. + * + * \see textEncoding() + * \see render() + */ + void setTextEncoding(String::Type encoding); + + /*! + * Returns a list of the strings in this frame. + */ + StringList fieldList() const; + + /*! + * Returns a KeyConversionMap mapping a role as it would be used in a PropertyMap + * to the corresponding key used in a TIPL ID3 frame to describe that role. + */ + static const KeyConversionMap &involvedPeopleMap(); + + PropertyMap asProperties() const; + + protected: + // Reimplementations. + + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + /*! + * The constructor used by the FrameFactory. + */ + TextIdentificationFrame(const ByteVector &data, Header *h); + + private: + TextIdentificationFrame(const TextIdentificationFrame &); + TextIdentificationFrame &operator=(const TextIdentificationFrame &); + + /*! + * Parses the special structure of a TIPL frame + * Only the whitelisted roles "ARRANGER", "ENGINEER", "PRODUCER", + * "DJMIXER" (ID3: "DJ-MIX") and "MIXER" (ID3: "MIX") are allowed. + */ + PropertyMap makeTIPLProperties() const; + /*! + * Parses the special structure of a TMCL frame. + */ + PropertyMap makeTMCLProperties() const; + class TextIdentificationFramePrivate; + TextIdentificationFramePrivate *d; + }; + + /*! + * This is a specialization of text identification frames that allows for + * user defined entries. Each entry has a description in addition to the + * normal list of fields that a text identification frame has. + * + * This description identifies the frame and must be unique. + */ + + //! An ID3v2 custom text identification frame implementationx + + class TAGLIB_EXPORT UserTextIdentificationFrame : public TextIdentificationFrame + { + friend class FrameFactory; + + public: + /*! + * Constructs an empty user defined text identification frame. For this to be + * a useful frame both a description and text must be set. + */ + explicit UserTextIdentificationFrame(String::Type encoding = String::Latin1); + + /*! + * Creates a frame based on \a data. + */ + explicit UserTextIdentificationFrame(const ByteVector &data); + + /*! + * Creates a user defined text identification frame with the given \a description + * and \a values. + */ + UserTextIdentificationFrame(const String &description, const StringList &values, String::Type encoding = String::UTF8); + + virtual String toString() const; + + /*! + * Returns the description for this frame. + */ + String description() const; + + /*! + * Sets the description of the frame to \a s. \a s must be unique. You can + * check for the presence of another user defined text frame of the same type + * using find() and testing for null. + */ + void setDescription(const String &s); + + StringList fieldList() const; + void setText(const String &text); + void setText(const StringList &fields); + + /*! + * A UserTextIdentificationFrame is parsed into a PropertyMap as follows: + * - the key is the frame's description, uppercased + * - if the description contains '::', only the substring after that + * separator is considered as key (compatibility with exfalso) + * - if the above rules don't yield a valid key (e.g. containing non-ASCII + * characters), the returned map will contain an entry "TXXX/<description>" + * in its unsupportedData() list. + * - The values will be copies of the fieldList(). + * - If the description() appears as value in fieldList(), it will be omitted + * in the value list, in order to be compatible with TagLib which copies + * the description() into the fieldList(). + */ + PropertyMap asProperties() const; + + /*! + * Searches for the user defined text frame with the description \a description + * in \a tag. This returns null if no matching frames were found. + */ + static UserTextIdentificationFrame *find(Tag *tag, const String &description); + + private: + UserTextIdentificationFrame(const ByteVector &data, Header *h); + UserTextIdentificationFrame(const TextIdentificationFrame &); + UserTextIdentificationFrame &operator=(const UserTextIdentificationFrame &); + + void checkFields(); + + class UserTextIdentificationFramePrivate; + UserTextIdentificationFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/uniquefileidentifierframe.cpp b/taglib/taglib/mpeg/id3v2/frames/uniquefileidentifierframe.cpp new file mode 100644 index 0000000..a0e842e --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/uniquefileidentifierframe.cpp @@ -0,0 +1,148 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevectorlist.h> +#include <tpropertymap.h> +#include <tdebug.h> + +#include "id3v2tag.h" +#include "uniquefileidentifierframe.h" + +using namespace TagLib; +using namespace ID3v2; + +class UniqueFileIdentifierFrame::UniqueFileIdentifierFramePrivate +{ +public: + String owner; + ByteVector identifier; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +UniqueFileIdentifierFrame::UniqueFileIdentifierFrame(const ByteVector &data) : + ID3v2::Frame(data) +{ + d = new UniqueFileIdentifierFramePrivate; + setData(data); +} + +UniqueFileIdentifierFrame::UniqueFileIdentifierFrame(const String &owner, const ByteVector &id) : + ID3v2::Frame("UFID") +{ + d = new UniqueFileIdentifierFramePrivate; + d->owner = owner; + d->identifier = id; +} + +UniqueFileIdentifierFrame::~UniqueFileIdentifierFrame() +{ + delete d; +} + +String UniqueFileIdentifierFrame::owner() const +{ + return d->owner; +} + +ByteVector UniqueFileIdentifierFrame::identifier() const +{ + return d->identifier; +} + +void UniqueFileIdentifierFrame::setOwner(const String &s) +{ + d->owner = s; +} + +void UniqueFileIdentifierFrame::setIdentifier(const ByteVector &v) +{ + d->identifier = v; +} + +String UniqueFileIdentifierFrame::toString() const +{ + return String::null; +} + +PropertyMap UniqueFileIdentifierFrame::asProperties() const +{ + PropertyMap map; + if(d->owner == "http://musicbrainz.org") { + map.insert("MUSICBRAINZ_TRACKID", String(d->identifier)); + } + else { + map.unsupportedData().append(frameID() + String("/") + d->owner); + } + return map; +} + +UniqueFileIdentifierFrame *UniqueFileIdentifierFrame::findByOwner(const ID3v2::Tag *tag, const String &o) // static +{ + ID3v2::FrameList comments = tag->frameList("UFID"); + + for(ID3v2::FrameList::ConstIterator it = comments.begin(); + it != comments.end(); + ++it) + { + UniqueFileIdentifierFrame *frame = dynamic_cast<UniqueFileIdentifierFrame *>(*it); + if(frame && frame->owner() == o) + return frame; + } + + return 0; +} + +void UniqueFileIdentifierFrame::parseFields(const ByteVector &data) +{ + if(data.size() < 1) { + debug("An UFID frame must contain at least 1 byte."); + return; + } + + int pos = 0; + d->owner = readStringField(data, String::Latin1, &pos); + d->identifier = data.mid(pos); +} + +ByteVector UniqueFileIdentifierFrame::renderFields() const +{ + ByteVector data; + + data.append(d->owner.data(String::Latin1)); + data.append(char(0)); + data.append(d->identifier); + + return data; +} + +UniqueFileIdentifierFrame::UniqueFileIdentifierFrame(const ByteVector &data, Header *h) : + Frame(h) +{ + d = new UniqueFileIdentifierFramePrivate; + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/uniquefileidentifierframe.h b/taglib/taglib/mpeg/id3v2/frames/uniquefileidentifierframe.h new file mode 100644 index 0000000..add5a2e --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/uniquefileidentifierframe.h @@ -0,0 +1,123 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_UNIQUEFILEIDENTIFIERFRAME +#define TAGLIB_UNIQUEFILEIDENTIFIERFRAME + +#include "id3v2frame.h" + +namespace TagLib { + + namespace ID3v2 { + + /*! + * This is an implementation of ID3v2 unique file identifier frames. This + * frame is used to identify the file in an arbitrary database identified + * by the owner field. + */ + + //! An implementation of ID3v2 unique identifier frames + + class TAGLIB_EXPORT UniqueFileIdentifierFrame : public ID3v2::Frame + { + friend class FrameFactory; + + public: + /*! + * Creates a uniqe file identifier frame based on \a data. + */ + UniqueFileIdentifierFrame(const ByteVector &data); + + /*! + * Creates a unique file identifier frame with the owner \a owner and + * the identification \a id. + */ + UniqueFileIdentifierFrame(const String &owner, const ByteVector &id); + + /*! + * Destroys the frame. + */ + ~UniqueFileIdentifierFrame(); + + /*! + * Returns the owner for the frame; essentially this is the key for + * determining which identification scheme this key belongs to. This + * will usually either be an email address or URL for the person or tool + * used to create the unique identifier. + * + * \see setOwner() + */ + String owner() const; + + /*! + * Returns the unique identifier. Though sometimes this is a text string + * it also may be binary data and as much should be assumed when handling + * it. + */ + ByteVector identifier() const; + + /*! + * Sets the owner of the identification scheme to \a s. + * + * \see owner() + */ + void setOwner(const String &s); + + /*! + * Sets the unique file identifier to \a v. + * + * \see identifier() + */ + void setIdentifier(const ByteVector &v); + + virtual String toString() const; + + PropertyMap asProperties() const; + + /*! + * UFID frames each have a unique owner. This searches for a UFID + * frame with the owner \a o and returns a pointer to it. + * + * \see owner() + */ + static UniqueFileIdentifierFrame *findByOwner(const Tag *tag, const String &o); + + protected: + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + UniqueFileIdentifierFrame(const UniqueFileIdentifierFrame &); + UniqueFileIdentifierFrame &operator=(const UniqueFileIdentifierFrame &); + + UniqueFileIdentifierFrame(const ByteVector &data, Header *h); + + class UniqueFileIdentifierFramePrivate; + UniqueFileIdentifierFramePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/unknownframe.cpp b/taglib/taglib/mpeg/id3v2/frames/unknownframe.cpp new file mode 100644 index 0000000..4def028 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/unknownframe.cpp @@ -0,0 +1,84 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "unknownframe.h" + +using namespace TagLib; +using namespace ID3v2; + +class UnknownFrame::UnknownFramePrivate +{ +public: + ByteVector fieldData; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +UnknownFrame::UnknownFrame(const ByteVector &data) : Frame(data) +{ + d = new UnknownFramePrivate; + setData(data); +} + +UnknownFrame::~UnknownFrame() +{ + delete d; +} + +String UnknownFrame::toString() const +{ + return String::null; +} + +ByteVector UnknownFrame::data() const +{ + return d->fieldData; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void UnknownFrame::parseFields(const ByteVector &data) +{ + d->fieldData = data; +} + +ByteVector UnknownFrame::renderFields() const +{ + return d->fieldData; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +UnknownFrame::UnknownFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new UnknownFramePrivate; + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/unknownframe.h b/taglib/taglib/mpeg/id3v2/frames/unknownframe.h new file mode 100644 index 0000000..6559f4e --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/unknownframe.h @@ -0,0 +1,79 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_UNKNOWNFRAME_H +#define TAGLIB_UNKNOWNFRAME_H + +#include "id3v2frame.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! A frame type \e unknown to TagLib. + + /*! + * This class represents a frame type not known (or more often simply + * unimplemented) in TagLib. This is here provide a basic API for + * manipulating the binary data of unknown frames and to provide a means + * of rendering such \e unknown frames. + * + * Please note that a cleaner way of handling frame types that TagLib + * does not understand is to subclass ID3v2::Frame and ID3v2::FrameFactory + * to have your frame type supported through the standard ID3v2 mechanism. + */ + + class TAGLIB_EXPORT UnknownFrame : public Frame + { + friend class FrameFactory; + + public: + UnknownFrame(const ByteVector &data); + virtual ~UnknownFrame(); + + virtual String toString() const; + + /*! + * Returns the field data (everything but the header) for this frame. + */ + ByteVector data() const; + + protected: + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + UnknownFrame(const ByteVector &data, Header *h); + UnknownFrame(const UnknownFrame &); + UnknownFrame &operator=(const UnknownFrame &); + + class UnknownFramePrivate; + UnknownFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp b/taglib/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp new file mode 100644 index 0000000..8f96cb8 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp @@ -0,0 +1,193 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + copyright : (C) 2006 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "unsynchronizedlyricsframe.h" +#include <tbytevectorlist.h> +#include <id3v2tag.h> +#include <tdebug.h> +#include <tpropertymap.h> + +using namespace TagLib; +using namespace ID3v2; + +class UnsynchronizedLyricsFrame::UnsynchronizedLyricsFramePrivate +{ +public: + UnsynchronizedLyricsFramePrivate() : textEncoding(String::Latin1) {} + String::Type textEncoding; + ByteVector language; + String description; + String text; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +UnsynchronizedLyricsFrame::UnsynchronizedLyricsFrame(String::Type encoding) : + Frame("USLT") +{ + d = new UnsynchronizedLyricsFramePrivate; + d->textEncoding = encoding; +} + +UnsynchronizedLyricsFrame::UnsynchronizedLyricsFrame(const ByteVector &data) : + Frame(data) +{ + d = new UnsynchronizedLyricsFramePrivate; + setData(data); +} + +UnsynchronizedLyricsFrame::~UnsynchronizedLyricsFrame() +{ + delete d; +} + +String UnsynchronizedLyricsFrame::toString() const +{ + return d->text; +} + +ByteVector UnsynchronizedLyricsFrame::language() const +{ + return d->language; +} + +String UnsynchronizedLyricsFrame::description() const +{ + return d->description; +} + +String UnsynchronizedLyricsFrame::text() const +{ + return d->text; +} + +void UnsynchronizedLyricsFrame::setLanguage(const ByteVector &languageEncoding) +{ + d->language = languageEncoding.mid(0, 3); +} + +void UnsynchronizedLyricsFrame::setDescription(const String &s) +{ + d->description = s; +} + +void UnsynchronizedLyricsFrame::setText(const String &s) +{ + d->text = s; +} + + +String::Type UnsynchronizedLyricsFrame::textEncoding() const +{ + return d->textEncoding; +} + +void UnsynchronizedLyricsFrame::setTextEncoding(String::Type encoding) +{ + d->textEncoding = encoding; +} + +PropertyMap UnsynchronizedLyricsFrame::asProperties() const +{ + PropertyMap map; + String key = description().upper(); + if(key.isEmpty() || key.upper() == "LYRICS") + map.insert("LYRICS", text()); + else if(key.isNull()) + map.unsupportedData().append(L"USLT/" + description()); + else + map.insert("LYRICS:" + key, text()); + return map; +} + +UnsynchronizedLyricsFrame *UnsynchronizedLyricsFrame::findByDescription(const ID3v2::Tag *tag, const String &d) // static +{ + ID3v2::FrameList lyrics = tag->frameList("USLT"); + + for(ID3v2::FrameList::ConstIterator it = lyrics.begin(); it != lyrics.end(); ++it){ + UnsynchronizedLyricsFrame *frame = dynamic_cast<UnsynchronizedLyricsFrame *>(*it); + if(frame && frame->description() == d) + return frame; + } + return 0; +} +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void UnsynchronizedLyricsFrame::parseFields(const ByteVector &data) +{ + if(data.size() < 5) { + debug("An unsynchronized lyrics frame must contain at least 5 bytes."); + return; + } + + d->textEncoding = String::Type(data[0]); + d->language = data.mid(1, 3); + + int byteAlign + = d->textEncoding == String::Latin1 || d->textEncoding == String::UTF8 ? 1 : 2; + + ByteVectorList l = + ByteVectorList::split(data.mid(4), textDelimiter(d->textEncoding), byteAlign, 2); + + if(l.size() == 2) { + if(d->textEncoding == String::Latin1) { + d->description = Tag::latin1StringHandler()->parse(l.front()); + d->text = Tag::latin1StringHandler()->parse(l.back()); + } else { + d->description = String(l.front(), d->textEncoding); + d->text = String(l.back(), d->textEncoding); + } + } +} + +ByteVector UnsynchronizedLyricsFrame::renderFields() const +{ + ByteVector v; + + v.append(char(d->textEncoding)); + v.append(d->language.size() == 3 ? d->language : "XXX"); + v.append(d->description.data(d->textEncoding)); + v.append(textDelimiter(d->textEncoding)); + v.append(d->text.data(d->textEncoding)); + + return v; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +UnsynchronizedLyricsFrame::UnsynchronizedLyricsFrame(const ByteVector &data, Header *h) + : Frame(h) +{ + d = new UnsynchronizedLyricsFramePrivate(); + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.h b/taglib/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.h new file mode 100644 index 0000000..3af354f --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.h @@ -0,0 +1,179 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + copyright : (C) 2006 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_UNSYNCHRONIZEDLYRICSFRAME_H +#define TAGLIB_UNSYNCHRONIZEDLYRICSFRAME_H + +#include "id3v2frame.h" + +namespace TagLib { + + namespace ID3v2 { + + //! ID3v2 unsynchronized lyrics frame + /*! + * An implementation of ID3v2 unsynchronized lyrics. + */ + class TAGLIB_EXPORT UnsynchronizedLyricsFrame : public Frame + { + friend class FrameFactory; + + public: + /*! + * Construct an empty unsynchronized lyrics frame that will use the text encoding + * \a encoding. + */ + explicit UnsynchronizedLyricsFrame(String::Type encoding = String::Latin1); + + /*! + * Construct a unsynchronized lyrics frame based on the data in \a data. + */ + explicit UnsynchronizedLyricsFrame(const ByteVector &data); + + /*! + * Destroys this UnsynchronizedLyricsFrame instance. + */ + virtual ~UnsynchronizedLyricsFrame(); + + /*! + * Returns the text of this unsynchronized lyrics frame. + * + * \see text() + */ + virtual String toString() const; + + /*! + * Returns the language encoding as a 3 byte encoding as specified by + * <a href="http://en.wikipedia.org/wiki/ISO_639">ISO-639-2</a>. + * + * \note Most taggers simply ignore this value. + * + * \see setLanguage() + */ + ByteVector language() const; + + /*! + * Returns the description of this unsynchronized lyrics frame. + * + * \note Most taggers simply ignore this value. + * + * \see setDescription() + */ + String description() const; + + /*! + * Returns the text of this unsynchronized lyrics frame. + * + * \see setText() + */ + String text() const; + + /*! + * Set the language using the 3 byte language code from + * <a href="http://en.wikipedia.org/wiki/ISO_639">ISO-639-2</a> to + * \a languageCode. + * + * \see language() + */ + void setLanguage(const ByteVector &languageCode); + + /*! + * Sets the description of the unsynchronized lyrics frame to \a s. + * + * \see decription() + */ + void setDescription(const String &s); + + /*! + * Sets the text portion of the unsynchronized lyrics frame to \a s. + * + * \see text() + */ + virtual void setText(const String &s); + + /*! + * Returns the text encoding that will be used in rendering this frame. + * This defaults to the type that was either specified in the constructor + * or read from the frame when parsed. + * + * \see setTextEncoding() + * \see render() + */ + String::Type textEncoding() const; + + /*! + * Sets the text encoding to be used when rendering this frame to + * \a encoding. + * + * \see textEncoding() + * \see render() + */ + void setTextEncoding(String::Type encoding); + + + /*! Parses this frame as PropertyMap with a single key. + * - if description() is empty or "LYRICS", the key will be "LYRICS" + * - if description() is not a valid PropertyMap key, the frame will be + * marked unsupported by an entry "USLT/<description>" in the unsupportedData() + * attribute of the returned map. + * - otherwise, the key will be "LYRICS:<description>" + * - The single value will be the frame's text(). + * Note that currently the language() field is not supported by the PropertyMap + * interface. + */ + PropertyMap asProperties() const; + + /*! + * LyricsFrames each have a unique description. This searches for a lyrics + * frame with the decription \a d and returns a pointer to it. If no + * frame is found that matches the given description null is returned. + * + * \see description() + */ + static UnsynchronizedLyricsFrame *findByDescription(const Tag *tag, const String &d); + + protected: + // Reimplementations. + + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + private: + /*! + * The constructor used by the FrameFactory. + */ + UnsynchronizedLyricsFrame(const ByteVector &data, Header *h); + UnsynchronizedLyricsFrame(const UnsynchronizedLyricsFrame &); + UnsynchronizedLyricsFrame &operator=(const UnsynchronizedLyricsFrame &); + + class UnsynchronizedLyricsFramePrivate; + UnsynchronizedLyricsFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/frames/urllinkframe.cpp b/taglib/taglib/mpeg/id3v2/frames/urllinkframe.cpp new file mode 100644 index 0000000..6bcbbda --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/urllinkframe.cpp @@ -0,0 +1,230 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + copyright : (C) 2006 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "urllinkframe.h" +#include "id3v2tag.h" +#include <tdebug.h> +#include <tstringlist.h> +#include <tpropertymap.h> + +using namespace TagLib; +using namespace ID3v2; + +class UrlLinkFrame::UrlLinkFramePrivate +{ +public: + String url; +}; + +class UserUrlLinkFrame::UserUrlLinkFramePrivate +{ +public: + UserUrlLinkFramePrivate() : textEncoding(String::Latin1) {} + String::Type textEncoding; + String description; +}; + +UrlLinkFrame::UrlLinkFrame(const ByteVector &data) : + Frame(data) +{ + d = new UrlLinkFramePrivate; + setData(data); +} + +UrlLinkFrame::~UrlLinkFrame() +{ + delete d; +} + +void UrlLinkFrame::setUrl(const String &s) +{ + d->url = s; +} + +String UrlLinkFrame::url() const +{ + return d->url; +} + +void UrlLinkFrame::setText(const String &s) +{ + setUrl(s); +} + +String UrlLinkFrame::toString() const +{ + return url(); +} + +PropertyMap UrlLinkFrame::asProperties() const +{ + String key = frameIDToKey(frameID()); + PropertyMap map; + if(key.isNull()) + // unknown W*** frame - this normally shouldn't happen + map.unsupportedData().append(frameID()); + else + map.insert(key, url()); + return map; +} + +void UrlLinkFrame::parseFields(const ByteVector &data) +{ + d->url = String(data); +} + +ByteVector UrlLinkFrame::renderFields() const +{ + return d->url.data(String::Latin1); +} + +UrlLinkFrame::UrlLinkFrame(const ByteVector &data, Header *h) : Frame(h) +{ + d = new UrlLinkFramePrivate; + parseFields(fieldData(data)); +} + + +UserUrlLinkFrame::UserUrlLinkFrame(String::Type encoding) : + UrlLinkFrame("WXXX") +{ + d = new UserUrlLinkFramePrivate; + d->textEncoding = encoding; +} + +UserUrlLinkFrame::UserUrlLinkFrame(const ByteVector &data) : + UrlLinkFrame(data) +{ + d = new UserUrlLinkFramePrivate; + setData(data); +} + +UserUrlLinkFrame::~UserUrlLinkFrame() +{ + delete d; +} + +String UserUrlLinkFrame::toString() const +{ + return "[" + description() + "] " + url(); +} + +String::Type UserUrlLinkFrame::textEncoding() const +{ + return d->textEncoding; +} + +void UserUrlLinkFrame::setTextEncoding(String::Type encoding) +{ + d->textEncoding = encoding; +} + +String UserUrlLinkFrame::description() const +{ + return d->description; +} + +void UserUrlLinkFrame::setDescription(const String &s) +{ + d->description = s; +} + +PropertyMap UserUrlLinkFrame::asProperties() const +{ + PropertyMap map; + String key = description().upper(); + if(key.isEmpty() || key.upper() == "URL") + map.insert("URL", url()); + else if(key.isNull()) + map.unsupportedData().append(L"WXXX/" + description()); + else + map.insert("URL:" + key, url()); + return map; +} + +UserUrlLinkFrame *UserUrlLinkFrame::find(ID3v2::Tag *tag, const String &description) // static +{ + FrameList l = tag->frameList("WXXX"); + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) { + UserUrlLinkFrame *f = dynamic_cast<UserUrlLinkFrame *>(*it); + if(f && f->description() == description) + return f; + } + return 0; +} + +void UserUrlLinkFrame::parseFields(const ByteVector &data) +{ + if(data.size() < 2) { + debug("A user URL link frame must contain at least 2 bytes."); + return; + } + + int pos = 0; + + d->textEncoding = String::Type(data[0]); + pos += 1; + + if(d->textEncoding == String::Latin1 || d->textEncoding == String::UTF8) { + int offset = data.find(textDelimiter(d->textEncoding), pos); + if(offset < pos) + return; + + d->description = String(data.mid(pos, offset - pos), d->textEncoding); + pos = offset + 1; + } + else { + int len = data.mid(pos).find(textDelimiter(d->textEncoding), 0, 2); + if(len < 0) + return; + + d->description = String(data.mid(pos, len), d->textEncoding); + pos += len + 2; + } + + setUrl(String(data.mid(pos))); +} + +ByteVector UserUrlLinkFrame::renderFields() const +{ + ByteVector v; + + String::Type encoding = checkTextEncoding(d->description, d->textEncoding); + + v.append(char(encoding)); + v.append(d->description.data(encoding)); + v.append(textDelimiter(encoding)); + v.append(url().data(String::Latin1)); + + return v; +} + +UserUrlLinkFrame::UserUrlLinkFrame(const ByteVector &data, Header *h) : UrlLinkFrame(data, h) +{ + d = new UserUrlLinkFramePrivate; + parseFields(fieldData(data)); +} diff --git a/taglib/taglib/mpeg/id3v2/frames/urllinkframe.h b/taglib/taglib/mpeg/id3v2/frames/urllinkframe.h new file mode 100644 index 0000000..7ac966b --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/frames/urllinkframe.h @@ -0,0 +1,189 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + copyright : (C) 2006 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_URLLINKFRAME_H +#define TAGLIB_URLLINKFRAME_H + +#include "id3v2frame.h" + +namespace TagLib { + + namespace ID3v2 { + + //! ID3v2 URL frame + /*! + * An implementation of ID3v2 URL link frames. + */ + class TAGLIB_EXPORT UrlLinkFrame : public Frame + { + friend class FrameFactory; + + public: + /*! + * This is a dual purpose constructor. \a data can either be binary data + * that should be parsed or (at a minimum) the frame ID. + */ + explicit UrlLinkFrame(const ByteVector &data); + + /*! + * Destroys this UrlLinkFrame instance. + */ + virtual ~UrlLinkFrame(); + + /*! + * Returns the URL. + */ + virtual String url() const; + + /*! + * Sets the URL to \a s. + */ + virtual void setUrl(const String &s); + + // Reimplementations. + + virtual void setText(const String &s); + virtual String toString() const; + PropertyMap asProperties() const; + + protected: + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + /*! + * The constructor used by the FrameFactory. + */ + UrlLinkFrame(const ByteVector &data, Header *h); + + private: + UrlLinkFrame(const UrlLinkFrame &); + UrlLinkFrame &operator=(const UrlLinkFrame &); + + class UrlLinkFramePrivate; + UrlLinkFramePrivate *d; + }; + + //! ID3v2 User defined URL frame + + /*! + * This is a specialization of URL link frames that allows for + * user defined entries. Each entry has a description in addition to the + * normal list of fields that a URL link frame has. + * + * This description identifies the frame and must be unique. + */ + class TAGLIB_EXPORT UserUrlLinkFrame : public UrlLinkFrame + { + friend class FrameFactory; + + public: + /*! + * Constructs an empty user defined URL link frame. For this to be + * a useful frame both a description and text must be set. + */ + explicit UserUrlLinkFrame(String::Type encoding = String::Latin1); + + /*! + * This is a dual purpose constructor. \a data can either be binary data + * that should be parsed or (at a minimum) the frame ID. + */ + explicit UserUrlLinkFrame(const ByteVector &data); + + /*! + * Destroys this UserUrlLinkFrame instance. + */ + virtual ~UserUrlLinkFrame(); + + // Reimplementations. + + virtual String toString() const; + + /*! + * Returns the text encoding that will be used in rendering this frame. + * This defaults to the type that was either specified in the constructor + * or read from the frame when parsed. + * + * \see setTextEncoding() + * \see render() + */ + String::Type textEncoding() const; + + /*! + * Sets the text encoding to be used when rendering this frame to + * \a encoding. + * + * \see textEncoding() + * \see render() + */ + void setTextEncoding(String::Type encoding); + + /*! + * Returns the description for this frame. + */ + String description() const; + + /*! + * Sets the description of the frame to \a s. \a s must be unique. + */ + void setDescription(const String &s); + + /*! + * Parses the UserUrlLinkFrame as PropertyMap. The description() is taken as key, + * and the URL as single value. + * - if description() is empty, the key will be "URL". + * - otherwise, if description() is not a valid key (e.g. containing non-ASCII + * characters), the returned map will contain an entry "WXXX/<description>" + * in its unsupportedData() list. + */ + PropertyMap asProperties() const; + + /*! + * Searches for the user defined url frame with the description \a description + * in \a tag. This returns null if no matching frames were found. + */ + static UserUrlLinkFrame *find(Tag *tag, const String &description); + + protected: + virtual void parseFields(const ByteVector &data); + virtual ByteVector renderFields() const; + + /*! + * The constructor used by the FrameFactory. + */ + UserUrlLinkFrame(const ByteVector &data, Header *h); + + private: + UserUrlLinkFrame(const UserUrlLinkFrame &); + UserUrlLinkFrame &operator=(const UserUrlLinkFrame &); + + class UserUrlLinkFramePrivate; + UserUrlLinkFramePrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/id3v2.2.0.txt b/taglib/taglib/mpeg/id3v2/id3v2.2.0.txt new file mode 100644 index 0000000..a69bddd --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2.2.0.txt @@ -0,0 +1,1660 @@ + +Informal standard M. Nilsson +Document: id3v2-00.txt 26th March 1998 + + + ID3 tag version 2 + +Status of this document + + This document is an Informal standard and is released so that + implementors could have a set standard before the formal standard is + set. The formal standard will use another version number if not + identical to what is described in this document. The contents in this + document may change for clarifications but never for added or altered + functionallity. + + Distribution of this document is unlimited. + + +Abstract + + The recent gain of popularity for MPEG layer III audio files on the + internet forced a standardised way of storing information about an + audio file within itself to determinate its origin and contents. + + Today the most accepted way to do this is with the so called ID3 tag, + which is simple but very limited and in some cases very unsuitable. + The ID3 tag has very limited space in every field, very limited + numbers of fields, not expandable or upgradeable and is placed at the + end of a the file, which is unsuitable for streaming audio. This draft + is an attempt to answer these issues with a new version of the ID3 + tag. + + +1. Table of contents + + 2. Conventions in this document + 3. ID3v2 overview + 3.1. ID3v2 header + 3.2. ID3v2 frames overview + 4. Declared ID3v2 frames + 4.1. Unique file identifier + 4.2. Text information frames + 4.2.1. Text information frames - details + 4.2.2. User defined text information frame + 4.3. URL link frames + 4.3.1. URL link frames - details + 4.3.2. User defined URL link frame + 4.4. Involved people list + 4.5. Music CD Identifier + 4.6. Event timing codes + 4.7. MPEG location lookup table + 4.8. Synced tempo codes + 4.9. Unsychronised lyrics/text transcription + 4.10. Synchronised lyrics/text + 4.11. Comments + 4.12. Relative volume adjustment + 4.13. Equalisation + 4.14. Reverb + 4.15. Attached picture + 4.16. General encapsulated object + 4.17. Play counter + 4.18. Popularimeter + 4.19. Recommended buffer size + 4.20. Encrypted meta frame + 4.21. Audio encryption + 4.22. Linked information + 5. The 'unsynchronisation scheme' + 6. Copyright + 7. References + 8. Appendix + A. Appendix A - ID3-Tag Specification V1.1 + A.1. Overview + A.2. ID3v1 Implementation + A.3. Genre List + A.4. Track addition - ID3v1.1 + 9. Author's Address + + +2. Conventions in this document + + In the examples, text within "" is a text string exactly as it appears + in a file. Numbers preceded with $ are hexadecimal and numbers + preceded with % are binary. $xx is used to indicate a byte with + unknown content. %x is used to indicate a bit with unknown content. + The most significant bit (MSB) of a byte is called 'bit 7' and the + least significant bit (LSB) is called 'bit 0'. + + A tag is the whole tag described in this document. A frame is a block + of information in the tag. The tag consists of a header, frames and + optional padding. A field is a piece of information; one value, a + string etc. A numeric string is a string that consists of the + characters 0-9 only. + + +3. ID3v2 overview + + The two biggest design goals were to be able to implement ID3v2 + without disturbing old software too much and that ID3v2 should be + expandable. + + The first criterion is met by the simple fact that the MPEG [MPEG] + decoding software uses a syncsignal, embedded in the audiostream, to + 'lock on to' the audio. Since the ID3v2 tag doesn't contain a valid + syncsignal, no software will attempt to play the tag. If, for any + reason, coincidence make a syncsignal appear within the tag it will be + taken care of by the 'unsynchronisation scheme' described in section + 5. + + The second criterion has made a more noticeable impact on the design + of the ID3v2 tag. It is constructed as a container for several + information blocks, called frames, whose format need not be known to + the software that encounters them. At the start of every frame there + is an identifier that explains the frames's format and content, and a + size descriptor that allows software to skip unknown frames. + + If a total revision of the ID3v2 tag should be needed, there is a + version number and a size descriptor in the ID3v2 header. + + The ID3 tag described in this document is mainly targeted to files + encoded with MPEG-2 layer I, MPEG-2 layer II, MPEG-2 layer III and + MPEG-2.5, but may work with other types of encoded audio. + + The bitorder in ID3v2 is most significant bit first (MSB). The + byteorder in multibyte numbers is most significant byte first (e.g. + $12345678 would be encoded $12 34 56 78). + + It is permitted to include padding after all the final frame (at the + end of the ID3 tag), making the size of all the frames together + smaller than the size given in the head of the tag. A possible purpose + of this padding is to allow for adding a few additional frames or + enlarge existing frames within the tag without having to rewrite the + entire file. The value of the padding bytes must be $00. + + +3.1. ID3v2 header + + The ID3v2 tag header, which should be the first information in the + file, is 10 bytes as follows: + + ID3/file identifier "ID3" + ID3 version $02 00 + ID3 flags %xx000000 + ID3 size 4 * %0xxxxxxx + + The first three bytes of the tag are always "ID3" to indicate that + this is an ID3 tag, directly followed by the two version bytes. The + first byte of ID3 version is it's major version, while the second byte + is its revision number. All revisions are backwards compatible while + major versions are not. If software with ID3v2 and below support + should encounter version three or higher it should simply ignore the + whole tag. Version and revision will never be $FF. + + The first bit (bit 7) in the 'ID3 flags' is indicating whether or not + unsynchronisation is used (see section 5 for details); a set bit + indicates usage. + + The second bit (bit 6) is indicating whether or not compression is + used; a set bit indicates usage. Since no compression scheme has been + decided yet, the ID3 decoder (for now) should just ignore the entire + tag if the compression bit is set. + + The ID3 tag size is encoded with four bytes where the first bit (bit + 7) is set to zero in every byte, making a total of 28 bits. The zeroed + bits are ignored, so a 257 bytes long tag is represented as $00 00 02 + 01. + + The ID3 tag size is the size of the complete tag after + unsychronisation, including padding, excluding the header (total tag + size - 10). The reason to use 28 bits (representing up to 256MB) for + size description is that we don't want to run out of space here. + + A ID3v2 tag can be detected with the following pattern: + $49 44 33 yy yy xx zz zz zz zz + Where yy is less than $FF, xx is the 'flags' byte and zz is less than + $80. + + +3.2. ID3v2 frames overview + + The headers of the frames are similar in their construction. They + consist of one three character identifier (capital A-Z and 0-9) and + one three byte size field, making a total of six bytes. The header is + excluded from the size. Identifiers beginning with "X", "Y" and "Z" + are for experimental use and free for everyone to use. Have in mind + that someone else might have used the same identifier as you. All + other identifiers are either used or reserved for future use. + + The three character frame identifier is followed by a three byte size + descriptor, making a total header size of six bytes in every frame. + The size is calculated as framesize excluding frame identifier and + size descriptor (frame size - 6). + + There is no fixed order of the frames' appearance in the tag, although + it is desired that the frames are arranged in order of significance + concerning the recognition of the file. An example of such order: + UFI, MCI, TT2 ... + + A tag must contain at least one frame. A frame must be at least 1 byte + big, excluding the 6-byte header. + + If nothing else is said a string is represented as ISO-8859-1 + [ISO-8859-1] characters in the range $20 - $FF. All unicode strings + [UNICODE] use 16-bit unicode 2.0 (ISO/IEC 10646-1:1993, UCS-2). All + numeric strings are always encoded as ISO-8859-1. Terminated strings + are terminated with $00 if encoded with ISO-8859-1 and $00 00 if + encoded as unicode. If nothing else is said newline character is + forbidden. In ISO-8859-1 a new line is represented, when allowed, with + $0A only. Frames that allow different types of text encoding have a + text encoding description byte directly after the frame size. If + ISO-8859-1 is used this byte should be $00, if unicode is used it + should be $01. + + The three byte language field is used to describe the language of the + frame's content, according to ISO-639-2 [ISO-639-2]. + + All URLs [URL] may be relative, e.g. "picture.png", "../doc.txt". + + If a frame is longer than it should be, e.g. having more fields than + specified in this document, that indicates that additions to the + frame have been made in a later version of the ID3 standard. This + is reflected by the revision number in the header of the tag. + + +4. Declared ID3v2 frames + + The following frames are declared in this draft. + + 4.19 BUF Recommended buffer size + + 4.17 CNT Play counter + 4.11 COM Comments + 4.21 CRA Audio encryption + 4.20 CRM Encrypted meta frame + + 4.6 ETC Event timing codes + 4.13 EQU Equalization + + 4.16 GEO General encapsulated object + + 4.4 IPL Involved people list + + 4.22 LNK Linked information + + 4.5 MCI Music CD Identifier + 4.7 MLL MPEG location lookup table + + 4.15 PIC Attached picture + 4.18 POP Popularimeter + + 4.14 REV Reverb + 4.12 RVA Relative volume adjustment + + 4.10 SLT Synchronized lyric/text + 4.8 STC Synced tempo codes + + 4.2.1 TAL Album/Movie/Show title + 4.2.1 TBP BPM (Beats Per Minute) + 4.2.1 TCM Composer + 4.2.1 TCO Content type + 4.2.1 TCR Copyright message + 4.2.1 TDA Date + 4.2.1 TDY Playlist delay + 4.2.1 TEN Encoded by + 4.2.1 TFT File type + 4.2.1 TIM Time + 4.2.1 TKE Initial key + 4.2.1 TLA Language(s) + 4.2.1 TLE Length + 4.2.1 TMT Media type + 4.2.1 TOA Original artist(s)/performer(s) + 4.2.1 TOF Original filename + 4.2.1 TOL Original Lyricist(s)/text writer(s) + 4.2.1 TOR Original release year + 4.2.1 TOT Original album/Movie/Show title + 4.2.1 TP1 Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group + 4.2.1 TP2 Band/Orchestra/Accompaniment + 4.2.1 TP3 Conductor/Performer refinement + 4.2.1 TP4 Interpreted, remixed, or otherwise modified by + 4.2.1 TPA Part of a set + 4.2.1 TPB Publisher + 4.2.1 TRC ISRC (International Standard Recording Code) + 4.2.1 TRD Recording dates + 4.2.1 TRK Track number/Position in set + 4.2.1 TSI Size + 4.2.1 TSS Software/hardware and settings used for encoding + 4.2.1 TT1 Content group description + 4.2.1 TT2 Title/Songname/Content description + 4.2.1 TT3 Subtitle/Description refinement + 4.2.1 TXT Lyricist/text writer + 4.2.2 TXX User defined text information frame + 4.2.1 TYE Year + + 4.1 UFI Unique file identifier + 4.9 ULT Unsychronized lyric/text transcription + + 4.3.1 WAF Official audio file webpage + 4.3.1 WAR Official artist/performer webpage + 4.3.1 WAS Official audio source webpage + 4.3.1 WCM Commercial information + 4.3.1 WCP Copyright/Legal information + 4.3.1 WPB Publishers official webpage + 4.3.2 WXX User defined URL link frame + + +4.1. Unique file identifier + + This frame's purpose is to be able to identify the audio file in a + database that may contain more information relevant to the content. + Since standardisation of such a database is beyond this document, all + frames begin with a null-terminated string with a URL [URL] containing + an email address, or a link to a location where an email address can + be found, that belongs to the organisation responsible for this + specific database implementation. Questions regarding the database + should be sent to the indicated email address. The URL should not be + used for the actual database queries. If a $00 is found directly after + the 'Frame size' the whole frame should be ignored, and preferably be + removed. The 'Owner identifier' is then followed by the actual + identifier, which may be up to 64 bytes. There may be more than one + "UFI" frame in a tag, but only one with the same 'Owner identifier'. + + Unique file identifier "UFI" + Frame size $xx xx xx + Owner identifier <textstring> $00 + Identifier <up to 64 bytes binary data> + + +4.2. Text information frames + + The text information frames are the most important frames, containing + information like artist, album and more. There may only be one text + information frame of its kind in an tag. If the textstring is followed + by a termination ($00 (00)) all the following information should be + ignored and not be displayed. All the text information frames have the + following format: + + Text information identifier "T00" - "TZZ" , excluding "TXX", + described in 4.2.2. + Frame size $xx xx xx + Text encoding $xx + Information <textstring> + + +4.2.1. Text information frames - details + + TT1 + The 'Content group description' frame is used if the sound belongs to + a larger category of sounds/music. For example, classical music is + often sorted in different musical sections (e.g. "Piano Concerto", + "Weather - Hurricane"). + + TT2 + The 'Title/Songname/Content description' frame is the actual name of + the piece (e.g. "Adagio", "Hurricane Donna"). + + TT3 + The 'Subtitle/Description refinement' frame is used for information + directly related to the contents title (e.g. "Op. 16" or "Performed + live at wembley"). + + TP1 + The 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group' is + used for the main artist(s). They are seperated with the "/" + character. + + TP2 + The 'Band/Orchestra/Accompaniment' frame is used for additional + information about the performers in the recording. + + TP3 + The 'Conductor' frame is used for the name of the conductor. + + TP4 + The 'Interpreted, remixed, or otherwise modified by' frame contains + more information about the people behind a remix and similar + interpretations of another existing piece. + + TCM + The 'Composer(s)' frame is intended for the name of the composer(s). + They are seperated with the "/" character. + + TXT + The 'Lyricist(s)/text writer(s)' frame is intended for the writer(s) + of the text or lyrics in the recording. They are seperated with the + "/" character. + + TLA + The 'Language(s)' frame should contain the languages of the text or + lyrics in the audio file. The language is represented with three + characters according to ISO-639-2. If more than one language is used + in the text their language codes should follow according to their + usage. + + TCO + The content type, which previously (in ID3v1.1, see appendix A) was + stored as a one byte numeric value only, is now a numeric string. You + may use one or several of the types as ID3v1.1 did or, since the + category list would be impossible to maintain with accurate and up to + date categories, define your own. + References to the ID3v1 genres can be made by, as first byte, enter + "(" followed by a number from the genres list (section A.3.) and + ended with a ")" character. This is optionally followed by a + refinement, e.g. "(21)" or "(4)Eurodisco". Several references can be + made in the same frame, e.g. "(51)(39)". If the refinement should + begin with a "(" character it should be replaced with "((", e.g. "((I + can figure out any genre)" or "(55)((I think...)". The following new + content types is defined in ID3v2 and is implemented in the same way + as the numerig content types, e.g. "(RX)". + + RX Remix + CR Cover + + TAL + The 'Album/Movie/Show title' frame is intended for the title of the + recording(/source of sound) which the audio in the file is taken from. + + TPA + The 'Part of a set' frame is a numeric string that describes which + part of a set the audio came from. This frame is used if the source + described in the "TAL" frame is divided into several mediums, e.g. a + double CD. The value may be extended with a "/" character and a + numeric string containing the total number of parts in the set. E.g. + "1/2". + + TRK + The 'Track number/Position in set' frame is a numeric string + containing the order number of the audio-file on its original + recording. This may be extended with a "/" character and a numeric + string containing the total numer of tracks/elements on the original + recording. E.g. "4/9". + + TRC + The 'ISRC' frame should contian the International Standard Recording + Code [ISRC]. + + TYE + The 'Year' frame is a numeric string with a year of the recording. + This frames is always four characters long (until the year 10000). + + TDA + The 'Date' frame is a numeric string in the DDMM format containing + the date for the recording. This field is always four characters + long. + + TIM + The 'Time' frame is a numeric string in the HHMM format containing + the time for the recording. This field is always four characters + long. + + TRD + The 'Recording dates' frame is a intended to be used as complement to + the "TYE", "TDA" and "TIM" frames. E.g. "4th-7th June, 12th June" in + combination with the "TYE" frame. + + TMT + The 'Media type' frame describes from which media the sound + originated. This may be a textstring or a reference to the predefined + media types found in the list below. References are made within "(" + and ")" and are optionally followed by a text refinement, e.g. "(MC) + with four channels". If a text refinement should begin with a "(" + character it should be replaced with "((" in the same way as in the + "TCO" frame. Predefined refinements is appended after the media type, + e.g. "(CD/S)" or "(VID/PAL/VHS)". + + DIG Other digital media + /A Analog transfer from media + + ANA Other analog media + /WAC Wax cylinder + /8CA 8-track tape cassette + + CD CD + /A Analog transfer from media + /DD DDD + /AD ADD + /AA AAD + + LD Laserdisc + /A Analog transfer from media + + TT Turntable records + /33 33.33 rpm + /45 45 rpm + /71 71.29 rpm + /76 76.59 rpm + /78 78.26 rpm + /80 80 rpm + + MD MiniDisc + /A Analog transfer from media + + DAT DAT + /A Analog transfer from media + /1 standard, 48 kHz/16 bits, linear + /2 mode 2, 32 kHz/16 bits, linear + /3 mode 3, 32 kHz/12 bits, nonlinear, low speed + /4 mode 4, 32 kHz/12 bits, 4 channels + /5 mode 5, 44.1 kHz/16 bits, linear + /6 mode 6, 44.1 kHz/16 bits, 'wide track' play + + DCC DCC + /A Analog transfer from media + + DVD DVD + /A Analog transfer from media + + TV Television + /PAL PAL + /NTSC NTSC + /SECAM SECAM + + VID Video + /PAL PAL + /NTSC NTSC + /SECAM SECAM + /VHS VHS + /SVHS S-VHS + /BETA BETAMAX + + RAD Radio + /FM FM + /AM AM + /LW LW + /MW MW + + TEL Telephone + /I ISDN + + MC MC (normal cassette) + /4 4.75 cm/s (normal speed for a two sided cassette) + /9 9.5 cm/s + /I Type I cassette (ferric/normal) + /II Type II cassette (chrome) + /III Type III cassette (ferric chrome) + /IV Type IV cassette (metal) + + REE Reel + /9 9.5 cm/s + /19 19 cm/s + /38 38 cm/s + /76 76 cm/s + /I Type I cassette (ferric/normal) + /II Type II cassette (chrome) + /III Type III cassette (ferric chrome) + /IV Type IV cassette (metal) + + TFT + The 'File type' frame indicates which type of audio this tag defines. + The following type and refinements are defined: + + MPG MPEG Audio + /1 MPEG 2 layer I + /2 MPEG 2 layer II + /3 MPEG 2 layer III + /2.5 MPEG 2.5 + /AAC Advanced audio compression + + but other types may be used, not for these types though. This is used + in a similar way to the predefined types in the "TMT" frame, but + without parenthesis. If this frame is not present audio type is + assumed to be "MPG". + + TBP + BPM is short for beats per minute, and is easily computed by + dividing the number of beats in a musical piece with its length. To + get a more accurate result, do the BPM calculation on the main-part + only. To acquire best result measure the time between each beat and + calculate individual BPM for each beat and use the median value as + result. BPM is an integer and represented as a numerical string. + + TCR + The 'Copyright message' frame, which must begin with a year and a + space character (making five characters), is intended for the + copyright holder of the original sound, not the audio file itself. The + absence of this frame means only that the copyright information is + unavailable or has been removed, and must not be interpreted to mean + that the sound is public domain. Every time this field is displayed + the field must be preceded with "Copyright " (C) " ", where (C) is one + character showing a C in a circle. + + TPB + The 'Publisher' frame simply contains the name of the label or + publisher. + + TEN + The 'Encoded by' frame contains the name of the person or + organisation that encoded the audio file. This field may contain a + copyright message, if the audio file also is copyrighted by the + encoder. + + TSS + The 'Software/hardware and settings used for encoding' frame + includes the used audio encoder and its settings when the file was + encoded. Hardware refers to hardware encoders, not the computer on + which a program was run. + + TOF + The 'Original filename' frame contains the preferred filename for the + file, since some media doesn't allow the desired length of the + filename. The filename is case sensitive and includes its suffix. + + TLE + The 'Length' frame contains the length of the audiofile in + milliseconds, represented as a numeric string. + + TSI + The 'Size' frame contains the size of the audiofile in bytes + excluding the tag, represented as a numeric string. + + TDY + The 'Playlist delay' defines the numbers of milliseconds of silence + between every song in a playlist. The player should use the "ETC" + frame, if present, to skip initial silence and silence at the end of + the audio to match the 'Playlist delay' time. The time is represented + as a numeric string. + + TKE + The 'Initial key' frame contains the musical key in which the sound + starts. It is represented as a string with a maximum length of three + characters. The ground keys are represented with "A","B","C","D","E", + "F" and "G" and halfkeys represented with "b" and "#". Minor is + represented as "m". Example "Cbm". Off key is represented with an "o" + only. + + TOT + The 'Original album/Movie/Show title' frame is intended for the title + of the original recording(/source of sound), if for example the music + in the file should be a cover of a previously released song. + + TOA + The 'Original artist(s)/performer(s)' frame is intended for the + performer(s) of the original recording, if for example the music in + the file should be a cover of a previously released song. The + performers are seperated with the "/" character. + + TOL + The 'Original Lyricist(s)/text writer(s)' frame is intended for the + text writer(s) of the original recording, if for example the music in + the file should be a cover of a previously released song. The text + writers are seperated with the "/" character. + + TOR + The 'Original release year' frame is intended for the year when the + original recording, if for example the music in the file should be a + cover of a previously released song, was released. The field is + formatted as in the "TDY" frame. + + +4.2.2. User defined text information frame + + This frame is intended for one-string text information concerning the + audiofile in a similar way to the other "T"xx frames. The frame body + consists of a description of the string, represented as a terminated + string, followed by the actual string. There may be more than one + "TXX" frame in each tag, but only one with the same description. + + User defined... "TXX" + Frame size $xx xx xx + Text encoding $xx + Description <textstring> $00 (00) + Value <textstring> + + +4.3. URL link frames + + With these frames dynamic data such as webpages with touring + information, price information or plain ordinary news can be added to + the tag. There may only be one URL [URL] link frame of its kind in an + tag, except when stated otherwise in the frame description. If the + textstring is followed by a termination ($00 (00)) all the following + information should be ignored and not be displayed. All URL link + frames have the following format: + + URL link frame "W00" - "WZZ" , excluding "WXX" + (described in 4.3.2.) + Frame size $xx xx xx + URL <textstring> + + +4.3.1. URL link frames - details + + WAF + The 'Official audio file webpage' frame is a URL pointing at a file + specific webpage. + + WAR + The 'Official artist/performer webpage' frame is a URL pointing at + the artists official webpage. There may be more than one "WAR" frame + in a tag if the audio contains more than one performer. + + WAS + The 'Official audio source webpage' frame is a URL pointing at the + official webpage for the source of the audio file, e.g. a movie. + + WCM + The 'Commercial information' frame is a URL pointing at a webpage + with information such as where the album can be bought. There may be + more than one "WCM" frame in a tag. + + WCP + The 'Copyright/Legal information' frame is a URL pointing at a + webpage where the terms of use and ownership of the file is described. + + WPB + The 'Publishers official webpage' frame is a URL pointing at the + official wepage for the publisher. + + +4.3.2. User defined URL link frame + + This frame is intended for URL [URL] links concerning the audiofile in + a similar way to the other "W"xx frames. The frame body consists of a + description of the string, represented as a terminated string, + followed by the actual URL. The URL is always encoded with ISO-8859-1 + [ISO-8859-1]. There may be more than one "WXX" frame in each tag, but + only one with the same description. + + User defined... "WXX" + Frame size $xx xx xx + Text encoding $xx + Description <textstring> $00 (00) + URL <textstring> + + +4.4. Involved people list + + Since there might be a lot of people contributing to an audio file in + various ways, such as musicians and technicians, the 'Text + information frames' are often insufficient to list everyone involved + in a project. The 'Involved people list' is a frame containing the + names of those involved, and how they were involved. The body simply + contains a terminated string with the involvement directly followed by + a terminated string with the involvee followed by a new involvement + and so on. There may only be one "IPL" frame in each tag. + + Involved people list "IPL" + Frame size $xx xx xx + Text encoding $xx + People list strings <textstrings> + + +4.5. Music CD Identifier + + This frame is intended for music that comes from a CD, so that the CD + can be identified in databases such as the CDDB [CDDB]. The frame + consists of a binary dump of the Table Of Contents, TOC, from the CD, + which is a header of 4 bytes and then 8 bytes/track on the CD making a + maximum of 804 bytes. This frame requires a present and valid "TRK" + frame. There may only be one "MCI" frame in each tag. + + Music CD identifier "MCI" + Frame size $xx xx xx + CD TOC <binary data> + + +4.6. Event timing codes + + This frame allows synchronisation with key events in a song or sound. + The head is: + + Event timing codes "ETC" + Frame size $xx xx xx + Time stamp format $xx + + Where time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Abolute time means that every stamp contains the time from the + beginning of the file. + + Followed by a list of key events in the following format: + + Type of event $xx + Time stamp $xx (xx ...) + + The 'Time stamp' is set to zero if directly at the beginning of the + sound or after the previous event. All events should be sorted in + chronological order. The type of event is as follows: + + $00 padding (has no meaning) + $01 end of initial silence + $02 intro start + $03 mainpart start + $04 outro start + $05 outro end + $06 verse begins + $07 refrain begins + $08 interlude + $09 theme start + $0A variation + $0B key change + $0C time change + $0D unwanted noise (Snap, Crackle & Pop) + + $0E-$DF reserved for future use + + $E0-$EF not predefined sync 0-F + + $F0-$FC reserved for future use + + $FD audio end (start of silence) + $FE audio file ends + $FF one more byte of events follows (all the following bytes with + the value $FF have the same function) + + The 'Not predefined sync's ($E0-EF) are for user events. You might + want to synchronise your music to something, like setting of an + explosion on-stage, turning on your screensaver etc. + + There may only be one "ETC" frame in each tag. + + +4.7. MPEG location lookup table + + To increase performance and accuracy of jumps within a MPEG [MPEG] + audio file, frames with timecodes in different locations in the file + might be useful. The ID3 frame includes references that the software + can use to calculate positions in the file. After the frame header is + a descriptor of how much the 'frame counter' should increase for every + reference. If this value is two then the first reference points out + the second frame, the 2nd reference the 4th frame, the 3rd reference + the 6th frame etc. In a similar way the 'bytes between reference' and + 'milliseconds between reference' points out bytes and milliseconds + respectively. + + Each reference consists of two parts; a certain number of bits, as + defined in 'bits for bytes deviation', that describes the difference + between what is said in 'bytes between reference' and the reality and + a certain number of bits, as defined in 'bits for milliseconds + deviation', that describes the difference between what is said in + 'milliseconds between reference' and the reality. The number of bits + in every reference, i.e. 'bits for bytes deviation'+'bits for + milliseconds deviation', must be a multiple of four. There may only be + one "MLL" frame in each tag. + + Location lookup table "MLL" + ID3 frame size $xx xx xx + MPEG frames between reference $xx xx + Bytes between reference $xx xx xx + Milliseconds between reference $xx xx xx + Bits for bytes deviation $xx + Bits for milliseconds dev. $xx + + Then for every reference the following data is included; + + Deviation in bytes %xxx.... + Deviation in milliseconds %xxx.... + + +4.8. Synced tempo codes + + For a more accurate description of the tempo of a musical piece this + frame might be used. After the header follows one byte describing + which time stamp format should be used. Then follows one or more tempo + codes. Each tempo code consists of one tempo part and one time part. + The tempo is in BPM described with one or two bytes. If the first byte + has the value $FF, one more byte follows, which is added to the first + giving a range from 2 - 510 BPM, since $00 and $01 is reserved. $00 is + used to describe a beat-free time period, which is not the same as a + music-free time period. $01 is used to indicate one single beat-stroke + followed by a beat-free period. + + The tempo descriptor is followed by a time stamp. Every time the tempo + in the music changes, a tempo descriptor may indicate this for the + player. All tempo descriptors should be sorted in chronological order. + The first beat-stroke in a time-period is at the same time as the beat + description occurs. There may only be one "STC" frame in each tag. + + Synced tempo codes "STC" + Frame size $xx xx xx + Time stamp format $xx + Tempo data <binary data> + + Where time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Abolute time means that every stamp contains the time from the + beginning of the file. + + +4.9. Unsychronised lyrics/text transcription + + This frame contains the lyrics of the song or a text transcription of + other vocal activities. The head includes an encoding descriptor and + a content descriptor. The body consists of the actual text. The + 'Content descriptor' is a terminated string. If no descriptor is + entered, 'Content descriptor' is $00 (00) only. Newline characters + are allowed in the text. Maximum length for the descriptor is 64 + bytes. There may be more than one lyrics/text frame in each tag, but + only one with the same language and content descriptor. + + Unsynced lyrics/text "ULT" + Frame size $xx xx xx + Text encoding $xx + Language $xx xx xx + Content descriptor <textstring> $00 (00) + Lyrics/text <textstring> + + +4.10. Synchronised lyrics/text + + This is another way of incorporating the words, said or sung lyrics, + in the audio file as text, this time, however, in sync with the audio. + It might also be used to describing events e.g. occurring on a stage + or on the screen in sync with the audio. The header includes a content + descriptor, represented with as terminated textstring. If no + descriptor is entered, 'Content descriptor' is $00 (00) only. + + Synced lyrics/text "SLT" + Frame size $xx xx xx + Text encoding $xx + Language $xx xx xx + Time stamp format $xx + Content type $xx + Content descriptor <textstring> $00 (00) + + + Encoding: $00 ISO-8859-1 [ISO-8859-1] character set is used => $00 + is sync identifier. + $01 Unicode [UNICODE] character set is used => $00 00 is + sync identifier. + + Content type: $00 is other + $01 is lyrics + $02 is text transcription + $03 is movement/part name (e.g. "Adagio") + $04 is events (e.g. "Don Quijote enters the stage") + $05 is chord (e.g. "Bb F Fsus") + + Time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Abolute time means that every stamp contains the time from the + beginning of the file. + + The text that follows the frame header differs from that of the + unsynchronised lyrics/text transcription in one major way. Each + syllable (or whatever size of text is considered to be convenient by + the encoder) is a null terminated string followed by a time stamp + denoting where in the sound file it belongs. Each sync thus has the + following structure: + + Terminated text to be synced (typically a syllable) + Sync identifier (terminator to above string) $00 (00) + Time stamp $xx (xx ...) + + The 'time stamp' is set to zero or the whole sync is omitted if + located directly at the beginning of the sound. All time stamps should + be sorted in chronological order. The sync can be considered as a + validator of the subsequent string. + + Newline characters are allowed in all "SLT" frames and should be used + after every entry (name, event etc.) in a frame with the content type + $03 - $04. + + A few considerations regarding whitespace characters: Whitespace + separating words should mark the beginning of a new word, thus + occurring in front of the first syllable of a new word. This is also + valid for new line characters. A syllable followed by a comma should + not be broken apart with a sync (both the syllable and the comma + should be before the sync). + + An example: The "ULT" passage + + "Strangers in the night" $0A "Exchanging glances" + + would be "SLT" encoded as: + + "Strang" $00 xx xx "ers" $00 xx xx " in" $00 xx xx " the" $00 xx xx + " night" $00 xx xx 0A "Ex" $00 xx xx "chang" $00 xx xx "ing" $00 xx + xx "glan" $00 xx xx "ces" $00 xx xx + + There may be more than one "SLT" frame in each tag, but only one with + the same language and content descriptor. + + +4.11. Comments + + This frame replaces the old 30-character comment field in ID3v1. It + consists of a frame head followed by encoding, language and content + descriptors and is ended with the actual comment as a text string. + Newline characters are allowed in the comment text string. There may + be more than one comment frame in each tag, but only one with the same + language and content descriptor. + + Comment "COM" + Frame size $xx xx xx + Text encoding $xx + Language $xx xx xx + Short content description <textstring> $00 (00) + The actual text <textstring> + + +4.12. Relative volume adjustment + + This is a more subjective function than the previous ones. It allows + the user to say how much he wants to increase/decrease the volume on + each channel while the file is played. The purpose is to be able to + align all files to a reference volume, so that you don't have to + change the volume constantly. This frame may also be used to balance + adjust the audio. If the volume peak levels are known then this could + be described with the 'Peak volume right' and 'Peak volume left' + field. If Peakvolume is not known these fields could be left zeroed + or completely omitted. There may only be one "RVA" frame in each + tag. + + Relative volume adjustment "RVA" + Frame size $xx xx xx + Increment/decrement %000000xx + Bits used for volume descr. $xx + Relative volume change, right $xx xx (xx ...) + Relative volume change, left $xx xx (xx ...) + Peak volume right $xx xx (xx ...) + Peak volume left $xx xx (xx ...) + + In the increment/decrement field bit 0 is used to indicate the right + channel and bit 1 is used to indicate the left channel. 1 is + increment and 0 is decrement. + + The 'bits used for volume description' field is normally $10 (16 bits) + for MPEG 2 layer I, II and III [MPEG] and MPEG 2.5. This value may not + be $00. The volume is always represented with whole bytes, padded in + the beginning (highest bits) when 'bits used for volume description' + is not a multiple of eight. + + +4.13. Equalisation + + This is another subjective, alignment frame. It allows the user to + predefine an equalisation curve within the audio file. There may only + be one "EQU" frame in each tag. + + Equalisation "EQU" + Frame size $xx xx xx + Adjustment bits $xx + + The 'adjustment bits' field defines the number of bits used for + representation of the adjustment. This is normally $10 (16 bits) for + MPEG 2 layer I, II and III [MPEG] and MPEG 2.5. This value may not be + $00. + + This is followed by 2 bytes + ('adjustment bits' rounded up to the + nearest byte) for every equalisation band in the following format, + giving a frequency range of 0 - 32767Hz: + + Increment/decrement %x (MSB of the Frequency) + Frequency (lower 15 bits) + Adjustment $xx (xx ...) + + The increment/decrement bit is 1 for increment and 0 for decrement. + The equalisation bands should be ordered increasingly with reference + to frequency. All frequencies don't have to be declared. Adjustments + with the value $00 should be omitted. A frequency should only be + described once in the frame. + + +4.14. Reverb + + Yet another subjective one. You may here adjust echoes of different + kinds. Reverb left/right is the delay between every bounce in ms. + Reverb bounces left/right is the number of bounces that should be + made. $FF equals an infinite number of bounces. Feedback is the amount + of volume that should be returned to the next echo bounce. $00 is 0%, + $FF is 100%. If this value were $7F, there would be 50% volume + reduction on the first bounce, yet 50% on the second and so on. Left + to left means the sound from the left bounce to be played in the left + speaker, while left to right means sound from the left bounce to be + played in the right speaker. + + 'Premix left to right' is the amount of left sound to be mixed in the + right before any reverb is applied, where $00 id 0% and $FF is 100%. + 'Premix right to left' does the same thing, but right to left. Setting + both premix to $FF would result in a mono output (if the reverb is + applied symmetric). There may only be one "REV" frame in each tag. + + Reverb settings "REV" + Frame size $00 00 0C + Reverb left (ms) $xx xx + Reverb right (ms) $xx xx + Reverb bounces, left $xx + Reverb bounces, right $xx + Reverb feedback, left to left $xx + Reverb feedback, left to right $xx + Reverb feedback, right to right $xx + Reverb feedback, right to left $xx + Premix left to right $xx + Premix right to left $xx + + +4.15. Attached picture + + This frame contains a picture directly related to the audio file. + Image format is preferably "PNG" [PNG] or "JPG" [JFIF]. Description + is a short description of the picture, represented as a terminated + textstring. The description has a maximum length of 64 characters, + but may be empty. There may be several pictures attached to one file, + each in their individual "PIC" frame, but only one with the same + content descriptor. There may only be one picture with the picture + type declared as picture type $01 and $02 respectively. There is a + possibility to put only a link to the image file by using the 'image + format' "-->" and having a complete URL [URL] instead of picture data. + The use of linked files should however be used restrictively since + there is the risk of separation of files. + + Attached picture "PIC" + Frame size $xx xx xx + Text encoding $xx + Image format $xx xx xx + Picture type $xx + Description <textstring> $00 (00) + Picture data <binary data> + + + Picture type: $00 Other + $01 32x32 pixels 'file icon' (PNG only) + $02 Other file icon + $03 Cover (front) + $04 Cover (back) + $05 Leaflet page + $06 Media (e.g. lable side of CD) + $07 Lead artist/lead performer/soloist + $08 Artist/performer + $09 Conductor + $0A Band/Orchestra + $0B Composer + $0C Lyricist/text writer + $0D Recording Location + $0E During recording + $0F During performance + $10 Movie/video screen capture + $11 A bright coloured fish + $12 Illustration + $13 Band/artist logotype + $14 Publisher/Studio logotype + + +4.16. General encapsulated object + + In this frame any type of file can be encapsulated. After the header, + 'Frame size' and 'Encoding' follows 'MIME type' [MIME] and 'Filename' + for the encapsulated object, both represented as terminated strings + encoded with ISO 8859-1 [ISO-8859-1]. The filename is case sensitive. + Then follows a content description as terminated string, encoded as + 'Encoding'. The last thing in the frame is the actual object. The + first two strings may be omitted, leaving only their terminations. + MIME type is always an ISO-8859-1 text string. There may be more than + one "GEO" frame in each tag, but only one with the same content + descriptor. + + General encapsulated object "GEO" + Frame size $xx xx xx + Text encoding $xx + MIME type <textstring> $00 + Filename <textstring> $00 (00) + Content description <textstring> $00 (00) + Encapsulated object <binary data> + + +4.17. Play counter + + This is simply a counter of the number of times a file has been + played. The value is increased by one every time the file begins to + play. There may only be one "CNT" frame in each tag. When the counter + reaches all one's, one byte is inserted in front of the counter thus + making the counter eight bits bigger. The counter must be at least + 32-bits long to begin with. + + Play counter "CNT" + Frame size $xx xx xx + Counter $xx xx xx xx (xx ...) + + +4.18. Popularimeter + + The purpose of this frame is to specify how good an audio file is. + Many interesting applications could be found to this frame such as a + playlist that features better audiofiles more often than others or it + could be used to profile a persons taste and find other 'good' files + by comparing people's profiles. The frame is very simple. It contains + the email address to the user, one rating byte and a four byte play + counter, intended to be increased with one for every time the file is + played. The email is a terminated string. The rating is 1-255 where + 1 is worst and 255 is best. 0 is unknown. If no personal counter is + wanted it may be omitted. When the counter reaches all one's, one + byte is inserted in front of the counter thus making the counter + eight bits bigger in the same away as the play counter ("CNT"). + There may be more than one "POP" frame in each tag, but only one with + the same email address. + + Popularimeter "POP" + Frame size $xx xx xx + Email to user <textstring> $00 + Rating $xx + Counter $xx xx xx xx (xx ...) + + +4.19. Recommended buffer size + + Sometimes the server from which a audio file is streamed is aware of + transmission or coding problems resulting in interruptions in the + audio stream. In these cases, the size of the buffer can be + recommended by the server using this frame. If the 'embedded info + flag' is true (1) then this indicates that an ID3 tag with the + maximum size described in 'Buffer size' may occur in the audiostream. + In such case the tag should reside between two MPEG [MPEG] frames, if + the audio is MPEG encoded. If the position of the next tag is known, + 'offset to next tag' may be used. The offset is calculated from the + end of tag in which this frame resides to the first byte of the header + in the next. This field may be omitted. Embedded tags is currently not + recommended since this could render unpredictable behaviour from + present software/hardware. The 'Buffer size' should be kept to a + minimum. There may only be one "BUF" frame in each tag. + + Recommended buffer size "BUF" + Frame size $xx xx xx + Buffer size $xx xx xx + Embedded info flag %0000000x + Offset to next tag $xx xx xx xx + + +4.20. Encrypted meta frame + + This frame contains one or more encrypted frames. This enables + protection of copyrighted information such as pictures and text, that + people might want to pay extra for. Since standardisation of such an + encryption scheme is beyond this document, all "CRM" frames begin with + a terminated string with a URL [URL] containing an email address, or a + link to a location where an email adress can be found, that belongs to + the organisation responsible for this specific encrypted meta frame. + + Questions regarding the encrypted frame should be sent to the + indicated email address. If a $00 is found directly after the 'Frame + size', the whole frame should be ignored, and preferably be removed. + The 'Owner identifier' is then followed by a short content description + and explanation as to why it's encrypted. After the + 'content/explanation' description, the actual encrypted block follows. + + When an ID3v2 decoder encounters a "CRM" frame, it should send the + datablock to the 'plugin' with the corresponding 'owner identifier' + and expect to receive either a datablock with one or several ID3v2 + frames after each other or an error. There may be more than one "CRM" + frames in a tag, but only one with the same 'owner identifier'. + + Encrypted meta frame "CRM" + Frame size $xx xx xx + Owner identifier <textstring> $00 (00) + Content/explanation <textstring> $00 (00) + Encrypted datablock <binary data> + + +4.21. Audio encryption + + This frame indicates if the actual audio stream is encrypted, and by + whom. Since standardisation of such encrypion scheme is beyond this + document, all "CRA" frames begin with a terminated string with a + URL containing an email address, or a link to a location where an + email address can be found, that belongs to the organisation + responsible for this specific encrypted audio file. Questions + regarding the encrypted audio should be sent to the email address + specified. If a $00 is found directly after the 'Frame size' and the + audiofile indeed is encrypted, the whole file may be considered + useless. + + After the 'Owner identifier', a pointer to an unencrypted part of the + audio can be specified. The 'Preview start' and 'Preview length' is + described in frames. If no part is unencrypted, these fields should be + left zeroed. After the 'preview length' field follows optionally a + datablock required for decryption of the audio. There may be more than + one "CRA" frames in a tag, but only one with the same 'Owner + identifier'. + + Audio encryption "CRA" + Frame size $xx xx xx + Owner identifier <textstring> $00 (00) + Preview start $xx xx + Preview length $xx xx + Encryption info <binary data> + + +4.22. Linked information + + To keep space waste as low as possible this frame may be used to link + information from another ID3v2 tag that might reside in another audio + file or alone in a binary file. It is recommended that this method is + only used when the files are stored on a CD-ROM or other circumstances + when the risk of file seperation is low. The frame contains a frame + identifier, which is the frame that should be linked into this tag, a + URL [URL] field, where a reference to the file where the frame is + given, and additional ID data, if needed. Data should be retrieved + from the first tag found in the file to which this link points. There + may be more than one "LNK" frame in a tag, but only one with the same + contents. A linked frame is to be considered as part of the tag and + has the same restrictions as if it was a physical part of the tag + (i.e. only one "REV" frame allowed, whether it's linked or not). + + Linked information "LNK" + Frame size $xx xx xx + Frame identifier $xx xx xx + URL <textstring> $00 (00) + Additional ID data <textstring(s)> + + Frames that may be linked and need no additional data are "IPL", + "MCI", "ETC", "LLT", "STC", "RVA", "EQU", "REV", "BUF", the text + information frames and the URL link frames. + + The "TXX", "PIC", "GEO", "CRM" and "CRA" frames may be linked with the + content descriptor as additional ID data. + + The "COM", "SLT" and "ULT" frames may be linked with three bytes of + language descriptor directly followed by a content descriptor as + additional ID data. + + +5. The 'unsynchronisation scheme' + + The only purpose of the 'unsychronisation scheme' is to make the ID3v2 + tag as compatible as possible with existing software. There is no use + in 'unsynchronising' tags if the file is only to be processed by new + software. Unsynchronisation may only be made with MPEG 2 layer I, II + and III and MPEG 2.5 files. + + Whenever a false synchronisation is found within the tag, one zeroed + byte is inserted after the first false synchronisation byte. The + format of a correct sync that should be altered by ID3 encoders is as + follows: + + %11111111 111xxxxx + + And should be replaced with: + + %11111111 00000000 111xxxxx + + This has the side effect that all $FF 00 combinations have to be + altered, so they won't be affected by the decoding process. Therefore + all the $FF 00 combinations have to be replaced with the $FF 00 00 + combination during the unsynchonisation. + + To indicate usage of the unsynchronisation, the first bit in 'ID3 + flags' should be set. This bit should only be set if the tag + contained a, now corrected, false synchronisation. The bit should + only be clear if the tag does not contain any false synchronisations. + + Do bear in mind, that if a compression scheme is used by the encoder, + the unsyncronisation scheme should be applied *afterwards*. When + decoding a compressed, 'unsyncronised' file, the 'unsyncronisation + scheme' should be parsed first, compression afterwards. + + +6. Copyright + + Copyright (C) Martin Nilsson 1998. All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that a reference to this document is included on all + such copies and derivative works. However, this document itself may + not be modified in any way and reissued as the original document. + + The limited permissions granted above are perpetual and will not be + revoked. + + This document and the information contained herein is provided on an + "AS IS" basis and THE AUTHORS DISCLAIMS ALL WARRANTIES, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE + INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + + +7. References + + [CDDB] Compact Disc Data Base + + <url:http://www.cddb.com> + + [ISO-639-2] ISO/FDIS 639-2. + Codes for the representation of names of languages, Part 2: Alpha-3 + code. Technical committee / subcommittee: TC 37 / SC 2 + + [ISO-8859-1] ISO/IEC DIS 8859-1. + 8-bit single-byte coded graphic character sets, Part 1: Latin + alphabet No. 1. Technical committee / subcommittee: JTC 1 / SC 2 + + [ISRC] ISO 3901:1986 + International Standard Recording Code (ISRC). + Technical committee / subcommittee: TC 46 / SC 9 + + [JFIF] JPEG File Interchange Format, version 1.02 + + <url:http://www.w3.org/Graphics/JPEG/jfif.txt> + + [MIME] Freed, N. and N. Borenstein, "Multipurpose Internet Mail + Extensions (MIME) Part One: Format of Internet Message Bodies", + RFC 2045, November 1996. + + <url:ftp://ftp.isi.edu/in-notes/rfc2045.txt> + + [MPEG] ISO/IEC 11172-3:1993. + Coding of moving pictures and associated audio for digital storage + media at up to about 1,5 Mbit/s, Part 3: Audio. + Technical committee / subcommittee: JTC 1 / SC 29 + and + ISO/IEC 13818-3:1995 + Generic coding of moving pictures and associated audio information, + Part 3: Audio. + Technical committee / subcommittee: JTC 1 / SC 29 + and + ISO/IEC DIS 13818-3 + Generic coding of moving pictures and associated audio information, + Part 3: Audio (Revision of ISO/IEC 13818-3:1995) + + + [PNG] Portable Network Graphics, version 1.0 + + <url:http://www.w3.org/TR/REC-png-multi.html> + + [UNICODE] ISO/IEC 10646-1:1993. + Universal Multiple-Octet Coded Character Set (UCS), Part 1: + Architecture and Basic Multilingual Plane. Technical committee + / subcommittee: JTC 1 / SC 2 + + <url:http://www.unicode.org> + + [URL] T. Berners-Lee, L. Masinter & M. McCahill, "Uniform Resource + Locators (URL).", RFC 1738, December 1994. + + <url:ftp://ftp.isi.edu/in-notes/rfc1738.txt> + + +8. Appendix + + +A. Appendix A - ID3-Tag Specification V1.1 + + ID3-Tag Specification V1.1 (12 dec 1997) by Michael Mutschler + <amiga2@info2.rus.uni-stuttgart.de>, edited for space and clarity + reasons. + + +A.1. Overview + + The ID3-Tag is an information field for MPEG Layer 3 audio files. + Since a standalone MP3 doesn't provide a method of storing other + information than those directly needed for replay reasons, the + ID3-tag was invented by Eric Kemp in 1996. + + A revision from ID3v1 to ID3v1.1 was made by Michael Mutschler to + support track number information is described in A.4. + + +A.2. ID3v1 Implementation + + The Information is stored in the last 128 bytes of an MP3. The Tag + has got the following fields, and the offsets given here, are from + 0-127. + + Field Length Offsets + Tag 3 0-2 + Songname 30 3-32 + Artist 30 33-62 + Album 30 63-92 + Year 4 93-96 + Comment 30 97-126 + Genre 1 127 + + + The string-fields contain ASCII-data, coded in ISO-Latin 1 codepage. + Strings which are smaller than the field length are padded with zero- + bytes. + + Tag: The tag is valid if this field contains the string "TAG". This + has to be uppercase! + + Songname: This field contains the title of the MP3 (string as + above). + + Artist: This field contains the artist of the MP3 (string as above). + + Album: this field contains the album where the MP3 comes from + (string as above). + + Year: this field contains the year when this song has originally + been released (string as above). + + Comment: this field contains a comment for the MP3 (string as + above). Revision to this field has been made in ID3v1.1. See + A.4. + + Genre: this byte contains the offset of a genre in a predefined + list the byte is treated as an unsigned byte. The offset is + starting from 0. See A.3. + + +A.3. Genre List + + The following genres is defined in ID3v1 + + 0.Blues + 1.Classic Rock + 2.Country + 3.Dance + 4.Disco + 5.Funk + 6.Grunge + 7.Hip-Hop + 8.Jazz + 9.Metal + 10.New Age + 11.Oldies + 12.Other + 13.Pop + 14.R&B + 15.Rap + 16.Reggae + 17.Rock + 18.Techno + 19.Industrial + 20.Alternative + 21.Ska + 22.Death Metal + 23.Pranks + 24.Soundtrack + 25.Euro-Techno + 26.Ambient + 27.Trip-Hop + 28.Vocal + 29.Jazz+Funk + 30.Fusion + 31.Trance + 32.Classical + 33.Instrumental + 34.Acid + 35.House + 36.Game + 37.Sound Clip + 38.Gospel + 39.Noise + 40.AlternRock + 41.Bass + 42.Soul + 43.Punk + 44.Space + 45.Meditative + 46.Instrumental Pop + 47.Instrumental Rock + 48.Ethnic + 49.Gothic + 50.Darkwave + 51.Techno-Industrial + 52.Electronic + 53.Pop-Folk + 54.Eurodance + 55.Dream + 56.Southern Rock + 57.Comedy + 58.Cult + 59.Gangsta + 60.Top 40 + 61.Christian Rap + 62.Pop/Funk + 63.Jungle + 64.Native American + 65.Cabaret + 66.New Wave + 67.Psychadelic + 68.Rave + 69.Showtunes + 70.Trailer + 71.Lo-Fi + 72.Tribal + 73.Acid Punk + 74.Acid Jazz + 75.Polka + 76.Retro + 77.Musical + 78.Rock & Roll + 79.Hard Rock + + The following genres are Winamp extensions + + 80.Folk + 81.Folk-Rock + 82.National Folk + 83.Swing + 84.Fast Fusion + 85.Bebob + 86.Latin + 87.Revival + 88.Celtic + 89.Bluegrass + 90.Avantgarde + 91.Gothic Rock + 92.Progressive Rock + 93.Psychedelic Rock + 94.Symphonic Rock + 95.Slow Rock + 96.Big Band + 97.Chorus + 98.Easy Listening + 99.Acoustic + 100.Humour + 101.Speech + 102.Chanson + 103.Opera + 104.Chamber Music + 105.Sonata + 106.Symphony + 107.Booty Bass + 108.Primus + 109.Porn Groove + 110.Satire + 111.Slow Jam + 112.Club + 113.Tango + 114.Samba + 115.Folklore + 116.Ballad + 117.Power Ballad + 118.Rhythmic Soul + 119.Freestyle + 120.Duet + 121.Punk Rock + 122.Drum Solo + 123.A capella + 124.Euro-House + 125.Dance Hall + + +A.4. Track addition - ID3v1.1 + + In ID3v1.1, Michael Mutschler revised the specification of the + comment field in order to implement the track number. The new format + of the comment field is a 28 character string followed by a mandatory + null ($00) character and the original album tracknumber stored as an + unsigned byte-size integer. In such cases where the 29th byte is not + the null character or when the 30th is a null character, the + tracknumber is to be considered undefined. + + +9. Author's Address + + Martin Nilsson + Rydsvägen 246 C. 30 + S-584 34 Linköping + Sweden + + Email: nilsson@id3.org + + Co-authors: + + Johan Sundström Email: johan@id3.org + + diff --git a/taglib/taglib/mpeg/id3v2/id3v2.3.0.txt b/taglib/taglib/mpeg/id3v2/id3v2.3.0.txt new file mode 100644 index 0000000..b4ed763 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2.3.0.txt @@ -0,0 +1,2022 @@ +Informal standard M. Nilsson +Document: id3v2.3.0.txt 3rd February 1999 + + + ID3 tag version 2.3.0 + +Status of this document + + This document is an informal standard and replaces the ID3v2.2.0 + standard [ID3v2]. The informal standard is released so that + implementors could have a set standard before a formal standard is + set. The formal standard will use another version or revision number + if not identical to what is described in this document. The contents + in this document may change for clarifications but never for added or + altered functionallity. + + Distribution of this document is unlimited. + + +Abstract + + This document describes the ID3v2.3.0, which is a more developed + version of the ID3v2 informal standard [ID3v2] (version 2.2.0), + evolved from the ID3 tagging system. The ID3v2 offers a flexible way + of storing information about an audio file within itself to determine + its origin and contents. The information may be technical + information, such as equalisation curves, as well as related meta + information, such as title, performer, copyright etc. + + +1. Table of contents + + 2. Conventions in this document + 3. ID3v2 overview + 3.1. ID3v2 header + 3.2. ID3v2 extended header + 3.3. ID3v2 frames overview + 3.3.1. Frame header flags + 3.3.2. Default flags + 4. Declared ID3v2 frames + 4.1. Unique file identifier + 4.2. Text information frames + 4.2.1. Text information frames - details + 4.2.2. User defined text information frame + 4.3. URL link frames + 4.3.1. URL link frames - details + 4.3.2. User defined URL link frame + 4.4. Involved people list + 4.5. Music CD Identifier + 4.6. Event timing codes + 4.7. MPEG location lookup table + 4.8. Synced tempo codes + 4.9. Unsychronised lyrics/text transcription + 4.10. Synchronised lyrics/text + 4.11. Comments + 4.12. Relative volume adjustment + 4.13. Equalisation + 4.14. Reverb + 4.15. Attached picture + 4.16. General encapsulated object + 4.17. Play counter + 4.18. Popularimeter + 4.19. Recommended buffer size + 4.20. Audio encryption + 4.21. Linked information + 4.22. Position synchronisation frame + 4.23. Terms of use + 4.24. Ownership frame + 4.25. Commercial frame + 4.26. Encryption method registration + 4.27. Group identification registration + 4.28. Private frame + 5. The 'unsynchronisation scheme' + 6. Copyright + 7. References + 8. Appendix + A. Appendix A - Genre List from ID3v1 + 9. Author's Address + + +2. Conventions in this document + + In the examples, text within "" is a text string exactly as it + appears in a file. Numbers preceded with $ are hexadecimal and + numbers preceded with % are binary. $xx is used to indicate a byte + with unknown content. %x is used to indicate a bit with unknown + content. The most significant bit (MSB) of a byte is called 'bit 7' + and the least significant bit (LSB) is called 'bit 0'. + + A tag is the whole tag described in this document. A frame is a block + of information in the tag. The tag consists of a header, frames and + optional padding. A field is a piece of information; one value, a + string etc. A numeric string is a string that consists of the + characters 0-9 only. + + +3. ID3v2 overview + + The two biggest design goals were to be able to implement ID3v2 + without disturbing old software too much and that ID3v2 should be + as flexible and expandable as possible. + + The first criterion is met by the simple fact that the MPEG [MPEG] + decoding software uses a syncsignal, embedded in the audiostream, to + 'lock on to' the audio. Since the ID3v2 tag doesn't contain a valid + syncsignal, no software will attempt to play the tag. If, for any + reason, coincidence make a syncsignal appear within the tag it will + be taken care of by the 'unsynchronisation scheme' described in + section 5. + + The second criterion has made a more noticeable impact on the design + of the ID3v2 tag. It is constructed as a container for several + information blocks, called frames, whose format need not be known to + the software that encounters them. At the start of every frame there + is an identifier that explains the frames' format and content, and a + size descriptor that allows software to skip unknown frames. + + If a total revision of the ID3v2 tag should be needed, there is a + version number and a size descriptor in the ID3v2 header. + + The ID3 tag described in this document is mainly targeted at files + encoded with MPEG-1/2 layer I, MPEG-1/2 layer II, MPEG-1/2 layer III + and MPEG-2.5, but may work with other types of encoded audio. + + The bitorder in ID3v2 is most significant bit first (MSB). The + byteorder in multibyte numbers is most significant byte first (e.g. + $12345678 would be encoded $12 34 56 78). + + It is permitted to include padding after all the final frame (at the + end of the ID3 tag), making the size of all the frames together + smaller than the size given in the head of the tag. A possible + purpose of this padding is to allow for adding a few additional + frames or enlarge existing frames within the tag without having to + rewrite the entire file. The value of the padding bytes must be $00. + + +3.1. ID3v2 header + + The ID3v2 tag header, which should be the first information in the + file, is 10 bytes as follows: + + ID3v2/file identifier "ID3" + ID3v2 version $03 00 + ID3v2 flags %abc00000 + ID3v2 size 4 * %0xxxxxxx + + The first three bytes of the tag are always "ID3" to indicate that + this is an ID3v2 tag, directly followed by the two version bytes. The + first byte of ID3v2 version is it's major version, while the second + byte is its revision number. In this case this is ID3v2.3.0. All + revisions are backwards compatible while major versions are not. If + software with ID3v2.2.0 and below support should encounter version + three or higher it should simply ignore the whole tag. Version and + revision will never be $FF. + + The version is followed by one the ID3v2 flags field, of which + currently only three flags are used. + + + a - Unsynchronisation + + Bit 7 in the 'ID3v2 flags' indicates whether or not + unsynchronisation is used (see section 5 for details); a set bit + indicates usage. + + + b - Extended header + + The second bit (bit 6) indicates whether or not the header is + followed by an extended header. The extended header is described in + section 3.2. + + + c - Experimental indicator + + The third bit (bit 5) should be used as an 'experimental + indicator'. This flag should always be set when the tag is in an + experimental stage. + + All the other flags should be cleared. If one of these undefined + flags are set that might mean that the tag is not readable for a + parser that does not know the flags function. + + The ID3v2 tag size is encoded with four bytes where the most + significant bit (bit 7) is set to zero in every byte, making a total + of 28 bits. The zeroed bits are ignored, so a 257 bytes long tag is + represented as $00 00 02 01. + + The ID3v2 tag size is the size of the complete tag after + unsychronisation, including padding, excluding the header but not + excluding the extended header (total tag size - 10). Only 28 bits + (representing up to 256MB) are used in the size description to avoid + the introducuction of 'false syncsignals'. + + An ID3v2 tag can be detected with the following pattern: + $49 44 33 yy yy xx zz zz zz zz + Where yy is less than $FF, xx is the 'flags' byte and zz is less than + $80. + + +3.2. ID3v2 extended header + + The extended header contains information that is not vital to the + correct parsing of the tag information, hence the extended header is + optional. + + Extended header size $xx xx xx xx + Extended Flags $xx xx + Size of padding $xx xx xx xx + + Where the 'Extended header size', currently 6 or 10 bytes, excludes + itself. The 'Size of padding' is simply the total tag size excluding + the frames and the headers, in other words the padding. The extended + header is considered separate from the header proper, and as such is + subject to unsynchronisation. + + The extended flags are a secondary flag set which describes further + attributes of the tag. These attributes are currently defined as + follows + + %x0000000 00000000 + + + x - CRC data present + + If this flag is set four bytes of CRC-32 data is appended to the + extended header. The CRC should be calculated before + unsynchronisation on the data between the extended header and the + padding, i.e. the frames and only the frames. + + Total frame CRC $xx xx xx xx + + +3.3. ID3v2 frame overview + + As the tag consists of a tag header and a tag body with one or more + frames, all the frames consists of a frame header followed by one or + more fields containing the actual information. The layout of the + frame header: + + Frame ID $xx xx xx xx (four characters) + Size $xx xx xx xx + Flags $xx xx + + The frame ID made out of the characters capital A-Z and 0-9. + Identifiers beginning with "X", "Y" and "Z" are for experimental use + and free for everyone to use, without the need to set the + experimental bit in the tag header. Have in mind that someone else + might have used the same identifier as you. All other identifiers are + either used or reserved for future use. + + The frame ID is followed by a size descriptor, making a total header + size of ten bytes in every frame. The size is calculated as frame + size excluding frame header (frame size - 10). + + In the frame header the size descriptor is followed by two flags + bytes. These flags are described in section 3.3.1. + + There is no fixed order of the frames' appearance in the tag, + although it is desired that the frames are arranged in order of + significance concerning the recognition of the file. An example of + such order: UFID, TIT2, MCDI, TRCK ... + + A tag must contain at least one frame. A frame must be at least 1 + byte big, excluding the header. + + If nothing else is said a string is represented as ISO-8859-1 + [ISO-8859-1] characters in the range $20 - $FF. Such strings are + represented as <text string>, or <full text string> if newlines are + allowed, in the frame descriptions. All Unicode strings [UNICODE] use + 16-bit unicode 2.0 (ISO/IEC 10646-1:1993, UCS-2). Unicode strings + must begin with the Unicode BOM ($FF FE or $FE FF) to identify the + byte order. + + All numeric strings and URLs [URL] are always encoded as ISO-8859-1. + Terminated strings are terminated with $00 if encoded with ISO-8859-1 + and $00 00 if encoded as unicode. If nothing else is said newline + character is forbidden. In ISO-8859-1 a new line is represented, when + allowed, with $0A only. Frames that allow different types of text + encoding have a text encoding description byte directly after the + frame size. If ISO-8859-1 is used this byte should be $00, if Unicode + is used it should be $01. Strings dependent on encoding is + represented as <text string according to encoding>, or <full text + string according to encoding> if newlines are allowed. Any empty + Unicode strings which are NULL-terminated may have the Unicode BOM + followed by a Unicode NULL ($FF FE 00 00 or $FE FF 00 00). + + The three byte language field is used to describe the language of the + frame's content, according to ISO-639-2 [ISO-639-2]. + + All URLs [URL] may be relative, e.g. "picture.png", "../doc.txt". + + If a frame is longer than it should be, e.g. having more fields than + specified in this document, that indicates that additions to the + frame have been made in a later version of the ID3v2 standard. This + is reflected by the revision number in the header of the tag. + + +3.3.1. Frame header flags + + In the frame header the size descriptor is followed by two flags + bytes. All unused flags must be cleared. The first byte is for + 'status messages' and the second byte is for encoding purposes. If an + unknown flag is set in the first byte the frame may not be changed + without the bit cleared. If an unknown flag is set in the second byte + it is likely to not be readable. The flags field is defined as + follows. + + %abc00000 %ijk00000 + + + a - Tag alter preservation + + This flag tells the software what to do with this frame if it is + unknown and the tag is altered in any way. This applies to all + kinds of alterations, including adding more padding and reordering + the frames. + + 0 Frame should be preserved. + 1 Frame should be discarded. + + + b - File alter preservation + + This flag tells the software what to do with this frame if it is + unknown and the file, excluding the tag, is altered. This does not + apply when the audio is completely replaced with other audio data. + + 0 Frame should be preserved. + 1 Frame should be discarded. + + + c - Read only + + This flag, if set, tells the software that the contents of this + frame is intended to be read only. Changing the contents might + break something, e.g. a signature. If the contents are changed, + without knowledge in why the frame was flagged read only and + without taking the proper means to compensate, e.g. recalculating + the signature, the bit should be cleared. + + + i - Compression + + This flag indicates whether or not the frame is compressed. + + 0 Frame is not compressed. + 1 Frame is compressed using zlib [zlib] with 4 bytes for + 'decompressed size' appended to the frame header. + + + j - Encryption + + This flag indicates wether or not the frame is enrypted. If set + one byte indicating with which method it was encrypted will be + appended to the frame header. See section 4.26. for more + information about encryption method registration. + + 0 Frame is not encrypted. + 1 Frame is encrypted. + + + k - Grouping identity + + This flag indicates whether or not this frame belongs in a group + with other frames. If set a group identifier byte is added to the + frame header. Every frame with the same group identifier belongs + to the same group. + + 0 Frame does not contain group information + 1 Frame contains group information + + + Some flags indicates that the frame header is extended with + additional information. This information will be added to the frame + header in the same order as the flags indicating the additions. I.e. + the four bytes of decompressed size will preceed the encryption + method byte. These additions to the frame header, while not included + in the frame header size but are included in the 'frame size' field, + are not subject to encryption or compression. + + +3.3.2. Default flags + + The default settings for the frames described in this document can be + divided into the following classes. The flags may be set differently + if found more suitable by the software. + + 1. Discarded if tag is altered, discarded if file is altered. + + None. + + 2. Discarded if tag is altered, preserved if file is altered. + + None. + + 3. Preserved if tag is altered, discarded if file is altered. + + AENC, ETCO, EQUA, MLLT, POSS, SYLT, SYTC, RVAD, TENC, TLEN, TSIZ + + 4. Preserved if tag is altered, preserved if file is altered. + + The rest of the frames. + + +4. Declared ID3v2 frames + + The following frames are declared in this draft. + + 4.21 AENC Audio encryption + 4.15 APIC Attached picture + + 4.11 COMM Comments + 4.25 COMR Commercial frame + + 4.26 ENCR Encryption method registration + 4.13 EQUA Equalization + 4.6 ETCO Event timing codes + + 4.16 GEOB General encapsulated object + 4.27 GRID Group identification registration + + 4.4 IPLS Involved people list + + 4.21 LINK Linked information + + 4.5 MCDI Music CD identifier + 4.7 MLLT MPEG location lookup table + + 4.24 OWNE Ownership frame + + 4.28. PRIV Private frame + 4.17 PCNT Play counter + 4.18 POPM Popularimeter + 4.22 POSS Position synchronisation frame + + 4.19 RBUF Recommended buffer size + 4.12 RVAD Relative volume adjustment + 4.14 RVRB Reverb + + 4.10 SYLT Synchronized lyric/text + 4.8 SYTC Synchronized tempo codes + + 4.2.1 TALB Album/Movie/Show title + 4.2.1 TBPM BPM (beats per minute) + 4.2.1 TCOM Composer + 4.2.1 TCON Content type + 4.2.1 TCOP Copyright message + 4.2.1 TDAT Date + 4.2.1 TDLY Playlist delay + 4.2.1 TENC Encoded by + 4.2.1 TEXT Lyricist/Text writer + 4.2.1 TFLT File type + 4.2.1 TIME Time + 4.2.1 TIT1 Content group description + 4.2.1 TIT2 Title/songname/content description + 4.2.1 TIT3 Subtitle/Description refinement + 4.2.1 TKEY Initial key + 4.2.1 TLAN Language(s) + 4.2.1 TLEN Length + 4.2.1 TMED Media type + 4.2.1 TOAL Original album/movie/show title + 4.2.1 TOFN Original filename + 4.2.1 TOLY Original lyricist(s)/text writer(s) + 4.2.1 TOPE Original artist(s)/performer(s) + 4.2.1 TORY Original release year + 4.2.1 TOWN File owner/licensee + 4.2.1 TPE1 Lead performer(s)/Soloist(s) + 4.2.1 TPE2 Band/orchestra/accompaniment + 4.2.1 TPE3 Conductor/performer refinement + 4.2.1 TPE4 Interpreted, remixed, or otherwise modified by + 4.2.1 TPOS Part of a set + 4.2.1 TPUB Publisher + 4.2.1 TRCK Track number/Position in set + 4.2.1 TRDA Recording dates + 4.2.1 TRSN Internet radio station name + 4.2.1 TRSO Internet radio station owner + 4.2.1 TSIZ Size + 4.2.1 TSRC ISRC (international standard recording code) + 4.2.1 TSSE Software/Hardware and settings used for encoding + 4.2.1 TYER Year + 4.2.2 TXXX User defined text information frame + + 4.1 UFID Unique file identifier + 4.23 USER Terms of use + 4.9 USLT Unsychronized lyric/text transcription + + 4.3.1 WCOM Commercial information + 4.3.1 WCOP Copyright/Legal information + 4.3.1 WOAF Official audio file webpage + 4.3.1 WOAR Official artist/performer webpage + 4.3.1 WOAS Official audio source webpage + 4.3.1 WORS Official internet radio station homepage + 4.3.1 WPAY Payment + 4.3.1 WPUB Publishers official webpage + 4.3.2 WXXX User defined URL link frame + + +4.1. Unique file identifier + + This frame's purpose is to be able to identify the audio file in a + database that may contain more information relevant to the content. + Since standardisation of such a database is beyond this document, all + frames begin with a null-terminated string with a URL [URL] + containing an email address, or a link to a location where an email + address can be found, that belongs to the organisation responsible + for this specific database implementation. Questions regarding the + database should be sent to the indicated email address. The URL + should not be used for the actual database queries. The string + "<a href="http://www.id3.org/dummy/ufid.html">http://www.id3.org/dummy/ufid.html</a>" should be used for tests. + Software that isn't told otherwise may safely remove such frames. The + 'Owner identifier' must be non-empty (more than just a termination). + The 'Owner identifier' is then followed by the actual identifier, + which may be up to 64 bytes. There may be more than one "UFID" frame + in a tag, but only one with the same 'Owner identifier'. + + <Header for 'Unique file identifier', ID: "UFID"> + Owner identifier <text string> $00 + Identifier <up to 64 bytes binary data> + + +4.2. Text information frames + + The text information frames are the most important frames, containing + information like artist, album and more. There may only be one text + information frame of its kind in an tag. If the textstring is + followed by a termination ($00 (00)) all the following information + should be ignored and not be displayed. All text frame identifiers + begin with "T". Only text frame identifiers begin with "T", with the + exception of the "TXXX" frame. All the text information frames have + the following format: + + <Header for 'Text information frame', ID: "T000" - "TZZZ", + excluding "TXXX" described in 4.2.2.> + Text encoding $xx + Information <text string according to encoding> + + +4.2.1. Text information frames - details + + TALB + The 'Album/Movie/Show title' frame is intended for the title of the + recording(/source of sound) which the audio in the file is taken + from. + + TBPM + The 'BPM' frame contains the number of beats per minute in the + mainpart of the audio. The BPM is an integer and represented as a + numerical string. + + TCOM + The 'Composer(s)' frame is intended for the name of the composer(s). + They are seperated with the "/" character. + + TCON + The 'Content type', which previously was stored as a one byte numeric + value only, is now a numeric string. You may use one or several of + the types as ID3v1.1 did or, since the category list would be + impossible to maintain with accurate and up to date categories, + define your own. + + References to the ID3v1 genres can be made by, as first byte, enter + "(" followed by a number from the genres list (appendix A.) and + ended with a ")" character. This is optionally followed by a + refinement, e.g. "(21)" or "(4)Eurodisco". Several references can be + made in the same frame, e.g. "(51)(39)". If the refinement should + begin with a "(" character it should be replaced with "((", e.g. "((I + can figure out any genre)" or "(55)((I think...)". The following new + content types is defined in ID3v2 and is implemented in the same way + as the numerig content types, e.g. "(RX)". + + RX Remix + CR Cover + + TCOP + The 'Copyright message' frame, which must begin with a year and a + space character (making five characters), is intended for the + copyright holder of the original sound, not the audio file itself. + The absence of this frame means only that the copyright information + is unavailable or has been removed, and must not be interpreted to + mean that the sound is public domain. Every time this field is + displayed the field must be preceded with "Copyright " (C) " ", where + (C) is one character showing a C in a circle. + + TDAT + The 'Date' frame is a numeric string in the DDMM format containing + the date for the recording. This field is always four characters + long. + + TDLY + The 'Playlist delay' defines the numbers of milliseconds of silence + between every song in a playlist. The player should use the "ETC" + frame, if present, to skip initial silence and silence at the end of + the audio to match the 'Playlist delay' time. The time is represented + as a numeric string. + + TENC + The 'Encoded by' frame contains the name of the person or + organisation that encoded the audio file. This field may contain a + copyright message, if the audio file also is copyrighted by the + encoder. + + TEXT + The 'Lyricist(s)/Text writer(s)' frame is intended for the writer(s) + of the text or lyrics in the recording. They are seperated with the + "/" character. + + TFLT + The 'File type' frame indicates which type of audio this tag defines. + The following type and refinements are defined: + + MPG MPEG Audio + /1 MPEG 1/2 layer I + /2 MPEG 1/2 layer II + /3 MPEG 1/2 layer III + /2.5 MPEG 2.5 + /AAC Advanced audio compression + VQF Transform-domain Weighted Interleave Vector Quantization + PCM Pulse Code Modulated audio + + but other types may be used, not for these types though. This is used + in a similar way to the predefined types in the "TMED" frame, but + without parentheses. If this frame is not present audio type is + assumed to be "MPG". + + TIME + The 'Time' frame is a numeric string in the HHMM format containing + the time for the recording. This field is always four characters + long. + + TIT1 + The 'Content group description' frame is used if the sound belongs to + a larger category of sounds/music. For example, classical music is + often sorted in different musical sections (e.g. "Piano Concerto", + "Weather - Hurricane"). + + TIT2 + The 'Title/Songname/Content description' frame is the actual name of + the piece (e.g. "Adagio", "Hurricane Donna"). + + TIT3 + The 'Subtitle/Description refinement' frame is used for information + directly related to the contents title (e.g. "Op. 16" or "Performed + live at Wembley"). + + TKEY + The 'Initial key' frame contains the musical key in which the sound + starts. It is represented as a string with a maximum length of three + characters. The ground keys are represented with "A","B","C","D","E", + "F" and "G" and halfkeys represented with "b" and "#". Minor is + represented as "m". Example "Cbm". Off key is represented with an "o" + only. + + TLAN + The 'Language(s)' frame should contain the languages of the text or + lyrics spoken or sung in the audio. The language is represented with + three characters according to ISO-639-2. If more than one language is + used in the text their language codes should follow according to + their usage. + + TLEN + The 'Length' frame contains the length of the audiofile in + milliseconds, represented as a numeric string. + + TMED + The 'Media type' frame describes from which media the sound + originated. This may be a text string or a reference to the + predefined media types found in the list below. References are made + within "(" and ")" and are optionally followed by a text refinement, + e.g. "(MC) with four channels". If a text refinement should begin + with a "(" character it should be replaced with "((" in the same way + as in the "TCO" frame. Predefined refinements is appended after the + media type, e.g. "(CD/A)" or "(VID/PAL/VHS)". + + DIG Other digital media + /A Analog transfer from media + + ANA Other analog media + /WAC Wax cylinder + /8CA 8-track tape cassette + + CD CD + /A Analog transfer from media + /DD DDD + /AD ADD + /AA AAD + + LD Laserdisc + /A Analog transfer from media + + TT Turntable records + /33 33.33 rpm + /45 45 rpm + /71 71.29 rpm + /76 76.59 rpm + /78 78.26 rpm + /80 80 rpm + + MD MiniDisc + /A Analog transfer from media + + DAT DAT + /A Analog transfer from media + /1 standard, 48 kHz/16 bits, linear + /2 mode 2, 32 kHz/16 bits, linear + /3 mode 3, 32 kHz/12 bits, nonlinear, low speed + /4 mode 4, 32 kHz/12 bits, 4 channels + /5 mode 5, 44.1 kHz/16 bits, linear + /6 mode 6, 44.1 kHz/16 bits, 'wide track' play + + DCC DCC + /A Analog transfer from media + + DVD DVD + /A Analog transfer from media + + TV Television + /PAL PAL + /NTSC NTSC + /SECAM SECAM + + VID Video + /PAL PAL + /NTSC NTSC + /SECAM SECAM + /VHS VHS + /SVHS S-VHS + /BETA BETAMAX + + RAD Radio + /FM FM + /AM AM + /LW LW + /MW MW + + TEL Telephone + /I ISDN + + MC MC (normal cassette) + /4 4.75 cm/s (normal speed for a two sided cassette) + /9 9.5 cm/s + /I Type I cassette (ferric/normal) + /II Type II cassette (chrome) + /III Type III cassette (ferric chrome) + /IV Type IV cassette (metal) + + REE Reel + /9 9.5 cm/s + /19 19 cm/s + /38 38 cm/s + /76 76 cm/s + /I Type I cassette (ferric/normal) + /II Type II cassette (chrome) + /III Type III cassette (ferric chrome) + /IV Type IV cassette (metal) + + TOAL + The 'Original album/movie/show title' frame is intended for the title + of the original recording (or source of sound), if for example the + music in the file should be a cover of a previously released song. + + TOFN + The 'Original filename' frame contains the preferred filename for the + file, since some media doesn't allow the desired length of the + filename. The filename is case sensitive and includes its suffix. + + TOLY + The 'Original lyricist(s)/text writer(s)' frame is intended for the + text writer(s) of the original recording, if for example the music in + the file should be a cover of a previously released song. The text + writers are seperated with the "/" character. + + TOPE + The 'Original artist(s)/performer(s)' frame is intended for the + performer(s) of the original recording, if for example the music in + the file should be a cover of a previously released song. The + performers are seperated with the "/" character. + + TORY + The 'Original release year' frame is intended for the year when the + original recording, if for example the music in the file should be a + cover of a previously released song, was released. The field is + formatted as in the "TYER" frame. + + TOWN + The 'File owner/licensee' frame contains the name of the owner or + licensee of the file and it's contents. + + TPE1 + The 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group' is + used for the main artist(s). They are seperated with the "/" + character. + + TPE2 + The 'Band/Orchestra/Accompaniment' frame is used for additional + information about the performers in the recording. + + TPE3 + The 'Conductor' frame is used for the name of the conductor. + + TPE4 + The 'Interpreted, remixed, or otherwise modified by' frame contains + more information about the people behind a remix and similar + interpretations of another existing piece. + + TPOS + The 'Part of a set' frame is a numeric string that describes which + part of a set the audio came from. This frame is used if the source + described in the "TALB" frame is divided into several mediums, e.g. a + double CD. The value may be extended with a "/" character and a + numeric string containing the total number of parts in the set. E.g. + "1/2". + + TPUB + The 'Publisher' frame simply contains the name of the label or + publisher. + + TRCK + The 'Track number/Position in set' frame is a numeric string + containing the order number of the audio-file on its original + recording. This may be extended with a "/" character and a numeric + string containing the total numer of tracks/elements on the original + recording. E.g. "4/9". + + TRDA + The 'Recording dates' frame is a intended to be used as complement to + the "TYER", "TDAT" and "TIME" frames. E.g. "4th-7th June, 12th June" + in combination with the "TYER" frame. + + TRSN + The 'Internet radio station name' frame contains the name of the + internet radio station from which the audio is streamed. + + TRSO + The 'Internet radio station owner' frame contains the name of the + owner of the internet radio station from which the audio is + streamed. + + TSIZ + The 'Size' frame contains the size of the audiofile in bytes, + excluding the ID3v2 tag, represented as a numeric string. + + TSRC + The 'ISRC' frame should contain the International Standard Recording + Code [ISRC] (12 characters). + + TSSE + The 'Software/Hardware and settings used for encoding' frame + includes the used audio encoder and its settings when the file was + encoded. Hardware refers to hardware encoders, not the computer on + which a program was run. + + TYER + The 'Year' frame is a numeric string with a year of the recording. + This frames is always four characters long (until the year 10000). + + +4.2.2. User defined text information frame + + This frame is intended for one-string text information concerning the + audiofile in a similar way to the other "T"-frames. The frame body + consists of a description of the string, represented as a terminated + string, followed by the actual string. There may be more than one + "TXXX" frame in each tag, but only one with the same description. + + <Header for 'User defined text information frame', ID: "TXXX"> + Text encoding $xx + Description <text string according to encoding> $00 (00) + Value <text string according to encoding> + + +4.3. URL link frames + + With these frames dynamic data such as webpages with touring + information, price information or plain ordinary news can be added to + the tag. There may only be one URL [URL] link frame of its kind in an + tag, except when stated otherwise in the frame description. If the + textstring is followed by a termination ($00 (00)) all the following + information should be ignored and not be displayed. All URL link + frame identifiers begins with "W". Only URL link frame identifiers + begins with "W". All URL link frames have the following format: + + <Header for 'URL link frame', ID: "W000" - "WZZZ", excluding "WXXX" + described in 4.3.2.> + URL <text string> + + +4.3.1. URL link frames - details + + WCOM + The 'Commercial information' frame is a URL pointing at a webpage + with information such as where the album can be bought. There may be + more than one "WCOM" frame in a tag, but not with the same content. + + WCOP + The 'Copyright/Legal information' frame is a URL pointing at a + webpage where the terms of use and ownership of the file is + described. + + WOAF + The 'Official audio file webpage' frame is a URL pointing at a file + specific webpage. + + WOAR + The 'Official artist/performer webpage' frame is a URL pointing at + the artists official webpage. There may be more than one "WOAR" frame + in a tag if the audio contains more than one performer, but not with + the same content. + + WOAS + The 'Official audio source webpage' frame is a URL pointing at the + official webpage for the source of the audio file, e.g. a movie. + + WORS + The 'Official internet radio station homepage' contains a URL + pointing at the homepage of the internet radio station. + + WPAY + The 'Payment' frame is a URL pointing at a webpage that will handle + the process of paying for this file. + + WPUB + The 'Publishers official webpage' frame is a URL pointing at the + official wepage for the publisher. + + +4.3.2. User defined URL link frame + + This frame is intended for URL [URL] links concerning the audiofile + in a similar way to the other "W"-frames. The frame body consists + of a description of the string, represented as a terminated string, + followed by the actual URL. The URL is always encoded with ISO-8859-1 + [ISO-8859-1]. There may be more than one "WXXX" frame in each tag, + but only one with the same description. + + <Header for 'User defined URL link frame', ID: "WXXX"> + Text encoding $xx + Description <text string according to encoding> $00 (00) + URL <text string> + + +4.4. Involved people list + + Since there might be a lot of people contributing to an audio file in + various ways, such as musicians and technicians, the 'Text + information frames' are often insufficient to list everyone involved + in a project. The 'Involved people list' is a frame containing the + names of those involved, and how they were involved. The body simply + contains a terminated string with the involvement directly followed + by a terminated string with the involvee followed by a new + involvement and so on. There may only be one "IPLS" frame in each + tag. + + <Header for 'Involved people list', ID: "IPLS"> + Text encoding $xx + People list strings <text strings according to encoding> + + +4.5. Music CD identifier + + This frame is intended for music that comes from a CD, so that the CD + can be identified in databases such as the CDDB [CDDB]. The frame + consists of a binary dump of the Table Of Contents, TOC, from the CD, + which is a header of 4 bytes and then 8 bytes/track on the CD plus 8 + bytes for the 'lead out' making a maximum of 804 bytes. The offset to + the beginning of every track on the CD should be described with a + four bytes absolute CD-frame address per track, and not with absolute + time. This frame requires a present and valid "TRCK" frame, even if + the CD's only got one track. There may only be one "MCDI" frame in + each tag. + + <Header for 'Music CD identifier', ID: "MCDI"> + CD TOC <binary data> + + +4.6. Event timing codes + + This frame allows synchronisation with key events in a song or sound. + The header is: + + <Header for 'Event timing codes', ID: "ETCO"> + Time stamp format $xx + + Where time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Abolute time means that every stamp contains the time from the + beginning of the file. + + Followed by a list of key events in the following format: + + Type of event $xx + Time stamp $xx (xx ...) + + The 'Time stamp' is set to zero if directly at the beginning of the + sound or after the previous event. All events should be sorted in + chronological order. The type of event is as follows: + + $00 padding (has no meaning) + $01 end of initial silence + $02 intro start + $03 mainpart start + $04 outro start + $05 outro end + $06 verse start + $07 refrain start + $08 interlude start + $09 theme start + $0A variation start + $0B key change + $0C time change + $0D momentary unwanted noise (Snap, Crackle & Pop) + $0E sustained noise + $0F sustained noise end + $10 intro end + $11 mainpart end + $12 verse end + $13 refrain end + $14 theme end + + $15-$DF reserved for future use + + $E0-$EF not predefined sync 0-F + + $F0-$FC reserved for future use + + $FD audio end (start of silence) + $FE audio file ends + $FF one more byte of events follows (all the following bytes with + the value $FF have the same function) + + Terminating the start events such as "intro start" is not required. + The 'Not predefined sync's ($E0-EF) are for user events. You might + want to synchronise your music to something, like setting of an + explosion on-stage, turning on your screensaver etc. + + There may only be one "ETCO" frame in each tag. + + +4.7. MPEG location lookup table + + To increase performance and accuracy of jumps within a MPEG [MPEG] + audio file, frames with timecodes in different locations in the file + might be useful. The ID3v2 frame includes references that the + software can use to calculate positions in the file. After the frame + header is a descriptor of how much the 'frame counter' should + increase for every reference. If this value is two then the first + reference points out the second frame, the 2nd reference the 4th + frame, the 3rd reference the 6th frame etc. In a similar way the + 'bytes between reference' and 'milliseconds between reference' points + out bytes and milliseconds respectively. + + Each reference consists of two parts; a certain number of bits, as + defined in 'bits for bytes deviation', that describes the difference + between what is said in 'bytes between reference' and the reality and + a certain number of bits, as defined in 'bits for milliseconds + deviation', that describes the difference between what is said in + 'milliseconds between reference' and the reality. The number of bits + in every reference, i.e. 'bits for bytes deviation'+'bits for + milliseconds deviation', must be a multiple of four. There may only + be one "MLLT" frame in each tag. + + <Header for 'Location lookup table', ID: "MLLT"> + MPEG frames between reference $xx xx + Bytes between reference $xx xx xx + Milliseconds between reference $xx xx xx + Bits for bytes deviation $xx + Bits for milliseconds dev. $xx + + Then for every reference the following data is included; + + Deviation in bytes %xxx.... + Deviation in milliseconds %xxx.... + + +4.8. Synchronised tempo codes + + For a more accurate description of the tempo of a musical piece this + frame might be used. After the header follows one byte describing + which time stamp format should be used. Then follows one or more + tempo codes. Each tempo code consists of one tempo part and one time + part. The tempo is in BPM described with one or two bytes. If the + first byte has the value $FF, one more byte follows, which is added + to the first giving a range from 2 - 510 BPM, since $00 and $01 is + reserved. $00 is used to describe a beat-free time period, which is + not the same as a music-free time period. $01 is used to indicate one + single beat-stroke followed by a beat-free period. + + The tempo descriptor is followed by a time stamp. Every time the + tempo in the music changes, a tempo descriptor may indicate this for + the player. All tempo descriptors should be sorted in chronological + order. The first beat-stroke in a time-period is at the same time as + the beat description occurs. There may only be one "SYTC" frame in + each tag. + + <Header for 'Synchronised tempo codes', ID: "SYTC"> + Time stamp format $xx + Tempo data <binary data> + + Where time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Abolute time means that every stamp contains the time from the + beginning of the file. + + +4.9. Unsychronised lyrics/text transcription + + This frame contains the lyrics of the song or a text transcription of + other vocal activities. The head includes an encoding descriptor and + a content descriptor. The body consists of the actual text. The + 'Content descriptor' is a terminated string. If no descriptor is + entered, 'Content descriptor' is $00 (00) only. Newline characters + are allowed in the text. There may be more than one 'Unsynchronised + lyrics/text transcription' frame in each tag, but only one with the + same language and content descriptor. + + <Header for 'Unsynchronised lyrics/text transcription', ID: "USLT"> + Text encoding $xx + Language $xx xx xx + Content descriptor <text string according to encoding> $00 (00) + Lyrics/text <full text string according to encoding> + + +4.10. Synchronised lyrics/text + + This is another way of incorporating the words, said or sung lyrics, + in the audio file as text, this time, however, in sync with the + audio. It might also be used to describing events e.g. occurring on a + stage or on the screen in sync with the audio. The header includes a + content descriptor, represented with as terminated textstring. If no + descriptor is entered, 'Content descriptor' is $00 (00) only. + + <Header for 'Synchronised lyrics/text', ID: "SYLT"> + Text encoding $xx + Language $xx xx xx + Time stamp format $xx + Content type $xx + Content descriptor <text string according to encoding> $00 (00) + + + Encoding: $00 ISO-8859-1 [ISO-8859-1] character set is used => $00 + is sync identifier. + $01 Unicode [UNICODE] character set is used => $00 00 is + sync identifier. + + Content type: $00 is other + $01 is lyrics + $02 is text transcription + $03 is movement/part name (e.g. "Adagio") + $04 is events (e.g. "Don Quijote enters the stage") + $05 is chord (e.g. "Bb F Fsus") + $06 is trivia/'pop up' information + + Time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Abolute time means that every stamp contains the time from the + beginning of the file. + + The text that follows the frame header differs from that of the + unsynchronised lyrics/text transcription in one major way. Each + syllable (or whatever size of text is considered to be convenient by + the encoder) is a null terminated string followed by a time stamp + denoting where in the sound file it belongs. Each sync thus has the + following structure: + + Terminated text to be synced (typically a syllable) + Sync identifier (terminator to above string) $00 (00) + Time stamp $xx (xx ...) + + The 'time stamp' is set to zero or the whole sync is omitted if + located directly at the beginning of the sound. All time stamps + should be sorted in chronological order. The sync can be considered + as a validator of the subsequent string. + + Newline ($0A) characters are allowed in all "SYLT" frames and should + be used after every entry (name, event etc.) in a frame with the + content type $03 - $04. + + A few considerations regarding whitespace characters: Whitespace + separating words should mark the beginning of a new word, thus + occurring in front of the first syllable of a new word. This is also + valid for new line characters. A syllable followed by a comma should + not be broken apart with a sync (both the syllable and the comma + should be before the sync). + + An example: The "USLT" passage + + "Strangers in the night" $0A "Exchanging glances" + + would be "SYLT" encoded as: + + "Strang" $00 xx xx "ers" $00 xx xx " in" $00 xx xx " the" $00 xx xx + " night" $00 xx xx 0A "Ex" $00 xx xx "chang" $00 xx xx "ing" $00 xx + xx "glan" $00 xx xx "ces" $00 xx xx + + There may be more than one "SYLT" frame in each tag, but only one + with the same language and content descriptor. + + +4.11. Comments + + This frame is indended for any kind of full text information that + does not fit in any other frame. It consists of a frame header + followed by encoding, language and content descriptors and is ended + with the actual comment as a text string. Newline characters are + allowed in the comment text string. There may be more than one + comment frame in each tag, but only one with the same language and + content descriptor. + + <Header for 'Comment', ID: "COMM"> + Text encoding $xx + Language $xx xx xx + Short content descrip. <text string according to encoding> $00 (00) + The actual text <full text string according to encoding> + + +4.12. Relative volume adjustment + + This is a more subjective function than the previous ones. It allows + the user to say how much he wants to increase/decrease the volume on + each channel while the file is played. The purpose is to be able to + align all files to a reference volume, so that you don't have to + change the volume constantly. This frame may also be used to balance + adjust the audio. If the volume peak levels are known then this could + be described with the 'Peak volume right' and 'Peak volume left' + field. If Peakvolume is not known these fields could be left zeroed + or, if no other data follows, be completely omitted. There may only + be one "RVAD" frame in each tag. + + <Header for 'Relative volume adjustment', ID: "RVAD"> + Increment/decrement %00xxxxxx + Bits used for volume descr. $xx + Relative volume change, right $xx xx (xx ...) + Relative volume change, left $xx xx (xx ...) + Peak volume right $xx xx (xx ...) + Peak volume left $xx xx (xx ...) + + In the increment/decrement field bit 0 is used to indicate the right + channel and bit 1 is used to indicate the left channel. 1 is + increment and 0 is decrement. + + The 'bits used for volume description' field is normally $10 (16 + bits) for MPEG 2 layer I, II and III [MPEG] and MPEG 2.5. This value + may not be $00. The volume is always represented with whole bytes, + padded in the beginning (highest bits) when 'bits used for volume + description' is not a multiple of eight. + + This datablock is then optionally followed by a volume definition for + the left and right back channels. If this information is appended to + the frame the first two channels will be treated as front channels. + In the increment/decrement field bit 2 is used to indicate the right + back channel and bit 3 for the left back channel. + + Relative volume change, right back $xx xx (xx ...) + Relative volume change, left back $xx xx (xx ...) + Peak volume right back $xx xx (xx ...) + Peak volume left back $xx xx (xx ...) + + If the center channel adjustment is present the following is appended + to the existing frame, after the left and right back channels. The + center channel is represented by bit 4 in the increase/decrease + field. + + Relative volume change, center $xx xx (xx ...) + Peak volume center $xx xx (xx ...) + + If the bass channel adjustment is present the following is appended + to the existing frame, after the center channel. The bass channel is + represented by bit 5 in the increase/decrease field. + + Relative volume change, bass $xx xx (xx ...) + Peak volume bass $xx xx (xx ...) + + +4.13. Equalisation + + This is another subjective, alignment frame. It allows the user to + predefine an equalisation curve within the audio file. There may only + be one "EQUA" frame in each tag. + + <Header of 'Equalisation', ID: "EQUA"> + Adjustment bits $xx + + The 'adjustment bits' field defines the number of bits used for + representation of the adjustment. This is normally $10 (16 bits) for + MPEG 2 layer I, II and III [MPEG] and MPEG 2.5. This value may not be + $00. + + This is followed by 2 bytes + ('adjustment bits' rounded up to the + nearest byte) for every equalisation band in the following format, + giving a frequency range of 0 - 32767Hz: + + Increment/decrement %x (MSB of the Frequency) + Frequency (lower 15 bits) + Adjustment $xx (xx ...) + + The increment/decrement bit is 1 for increment and 0 for decrement. + The equalisation bands should be ordered increasingly with reference + to frequency. All frequencies don't have to be declared. The + equalisation curve in the reading software should be interpolated + between the values in this frame. Three equal adjustments for three + subsequent frequencies. A frequency should only be described once in + the frame. + + +4.14. Reverb + + Yet another subjective one. You may here adjust echoes of different + kinds. Reverb left/right is the delay between every bounce in ms. + Reverb bounces left/right is the number of bounces that should be + made. $FF equals an infinite number of bounces. Feedback is the + amount of volume that should be returned to the next echo bounce. $00 + is 0%, $FF is 100%. If this value were $7F, there would be 50% volume + reduction on the first bounce, 50% of that on the second and so on. + Left to left means the sound from the left bounce to be played in the + left speaker, while left to right means sound from the left bounce to + be played in the right speaker. + + 'Premix left to right' is the amount of left sound to be mixed in the + right before any reverb is applied, where $00 id 0% and $FF is 100%. + 'Premix right to left' does the same thing, but right to left. + Setting both premix to $FF would result in a mono output (if the + reverb is applied symmetric). There may only be one "RVRB" frame in + each tag. + + <Header for 'Reverb', ID: "RVRB"> + Reverb left (ms) $xx xx + Reverb right (ms) $xx xx + Reverb bounces, left $xx + Reverb bounces, right $xx + Reverb feedback, left to left $xx + Reverb feedback, left to right $xx + Reverb feedback, right to right $xx + Reverb feedback, right to left $xx + Premix left to right $xx + Premix right to left $xx + + +4.15. Attached picture + + This frame contains a picture directly related to the audio file. + Image format is the MIME type and subtype [MIME] for the image. In + the event that the MIME media type name is omitted, "image/" will be + implied. The "image/png" [PNG] or "image/jpeg" [JFIF] picture format + should be used when interoperability is wanted. Description is a + short description of the picture, represented as a terminated + textstring. The description has a maximum length of 64 characters, + but may be empty. There may be several pictures attached to one file, + each in their individual "APIC" frame, but only one with the same + content descriptor. There may only be one picture with the picture + type declared as picture type $01 and $02 respectively. There is the + possibility to put only a link to the image file by using the 'MIME + type' "-->" and having a complete URL [URL] instead of picture data. + The use of linked files should however be used sparingly since there + is the risk of separation of files. + + <Header for 'Attached picture', ID: "APIC"> + Text encoding $xx + MIME type <text string> $00 + Picture type $xx + Description <text string according to encoding> $00 (00) + Picture data <binary data> + + + Picture type: $00 Other + $01 32x32 pixels 'file icon' (PNG only) + $02 Other file icon + $03 Cover (front) + $04 Cover (back) + $05 Leaflet page + $06 Media (e.g. lable side of CD) + $07 Lead artist/lead performer/soloist + $08 Artist/performer + $09 Conductor + $0A Band/Orchestra + $0B Composer + $0C Lyricist/text writer + $0D Recording Location + $0E During recording + $0F During performance + $10 Movie/video screen capture + $11 A bright coloured fish + $12 Illustration + $13 Band/artist logotype + $14 Publisher/Studio logotype + + +4.16. General encapsulated object + + In this frame any type of file can be encapsulated. After the header, + 'Frame size' and 'Encoding' follows 'MIME type' [MIME] represented as + as a terminated string encoded with ISO 8859-1 [ISO-8859-1]. The + filename is case sensitive and is encoded as 'Encoding'. Then follows + a content description as terminated string, encoded as 'Encoding'. + The last thing in the frame is the actual object. The first two + strings may be omitted, leaving only their terminations. MIME type is + always an ISO-8859-1 text string. There may be more than one "GEOB" + frame in each tag, but only one with the same content descriptor. + + <Header for 'General encapsulated object', ID: "GEOB"> + Text encoding $xx + MIME type <text string> $00 + Filename <text string according to encoding> $00 (00) + Content description <text string according to encóding> $00 (00) + Encapsulated object <binary data> + + +4.17. Play counter + + This is simply a counter of the number of times a file has been + played. The value is increased by one every time the file begins to + play. There may only be one "PCNT" frame in each tag. When the + counter reaches all one's, one byte is inserted in front of the + counter thus making the counter eight bits bigger. The counter must + be at least 32-bits long to begin with. + + <Header for 'Play counter', ID: "PCNT"> + Counter $xx xx xx xx (xx ...) + + +4.18. Popularimeter + + The purpose of this frame is to specify how good an audio file is. + Many interesting applications could be found to this frame such as a + playlist that features better audiofiles more often than others or it + could be used to profile a person's taste and find other 'good' files + by comparing people's profiles. The frame is very simple. It contains + the email address to the user, one rating byte and a four byte play + counter, intended to be increased with one for every time the file is + played. The email is a terminated string. The rating is 1-255 where + 1 is worst and 255 is best. 0 is unknown. If no personal counter is + wanted it may be omitted. When the counter reaches all one's, one + byte is inserted in front of the counter thus making the counter + eight bits bigger in the same away as the play counter ("PCNT"). + There may be more than one "POPM" frame in each tag, but only one + with the same email address. + + <Header for 'Popularimeter', ID: "POPM"> + Email to user <text string> $00 + Rating $xx + Counter $xx xx xx xx (xx ...) + + +4.19. Recommended buffer size + + Sometimes the server from which a audio file is streamed is aware of + transmission or coding problems resulting in interruptions in the + audio stream. In these cases, the size of the buffer can be + recommended by the server using this frame. If the 'embedded info + flag' is true (1) then this indicates that an ID3 tag with the + maximum size described in 'Buffer size' may occur in the audiostream. + In such case the tag should reside between two MPEG [MPEG] frames, if + the audio is MPEG encoded. If the position of the next tag is known, + 'offset to next tag' may be used. The offset is calculated from the + end of tag in which this frame resides to the first byte of the + header in the next. This field may be omitted. Embedded tags are + generally not recommended since this could render unpredictable + behaviour from present software/hardware. + + For applications like streaming audio it might be an idea to embed + tags into the audio stream though. If the clients connects to + individual connections like HTTP and there is a possibility to begin + every transmission with a tag, then this tag should include a + 'recommended buffer size' frame. If the client is connected to a + arbitrary point in the stream, such as radio or multicast, then the + 'recommended buffer size' frame should be included in every tag. + Every tag that is picked up after the initial/first tag is to be + considered as an update of the previous one. E.g. if there is a + "TIT2" frame in the first received tag and one in the second tag, + then the first should be 'replaced' with the second. + + The 'Buffer size' should be kept to a minimum. There may only be one + "RBUF" frame in each tag. + + <Header for 'Recommended buffer size', ID: "RBUF"> + Buffer size $xx xx xx + Embedded info flag %0000000x + Offset to next tag $xx xx xx xx + + +4.20. Audio encryption + + This frame indicates if the actual audio stream is encrypted, and by + whom. Since standardisation of such encrypion scheme is beyond this + document, all "AENC" frames begin with a terminated string with a + URL containing an email address, or a link to a location where an + email address can be found, that belongs to the organisation + responsible for this specific encrypted audio file. Questions + regarding the encrypted audio should be sent to the email address + specified. If a $00 is found directly after the 'Frame size' and the + audiofile indeed is encrypted, the whole file may be considered + useless. + + After the 'Owner identifier', a pointer to an unencrypted part of the + audio can be specified. The 'Preview start' and 'Preview length' is + described in frames. If no part is unencrypted, these fields should + be left zeroed. After the 'preview length' field follows optionally a + datablock required for decryption of the audio. There may be more + than one "AENC" frames in a tag, but only one with the same 'Owner + identifier'. + + <Header for 'Audio encryption', ID: "AENC"> + Owner identifier <text string> $00 + Preview start $xx xx + Preview length $xx xx + Encryption info <binary data> + + +4.21. Linked information + + To keep space waste as low as possible this frame may be used to link + information from another ID3v2 tag that might reside in another audio + file or alone in a binary file. It is recommended that this method is + only used when the files are stored on a CD-ROM or other + circumstances when the risk of file seperation is low. The frame + contains a frame identifier, which is the frame that should be linked + into this tag, a URL [URL] field, where a reference to the file where + the frame is given, and additional ID data, if needed. Data should be + retrieved from the first tag found in the file to which this link + points. There may be more than one "LINK" frame in a tag, but only + one with the same contents. A linked frame is to be considered as + part of the tag and has the same restrictions as if it was a physical + part of the tag (i.e. only one "RVRB" frame allowed, whether it's + linked or not). + + <Header for 'Linked information', ID: "LINK"> + Frame identifier $xx xx xx + URL <text string> $00 + ID and additional data <text string(s)> + + Frames that may be linked and need no additional data are "IPLS", + "MCID", "ETCO", "MLLT", "SYTC", "RVAD", "EQUA", "RVRB", "RBUF", the + text information frames and the URL link frames. + + The "TXXX", "APIC", "GEOB" and "AENC" frames may be linked with + the content descriptor as additional ID data. + + The "COMM", "SYLT" and "USLT" frames may be linked with three bytes + of language descriptor directly followed by a content descriptor as + additional ID data. + + +4.22. Position synchronisation frame + + This frame delivers information to the listener of how far into the + audio stream he picked up; in effect, it states the time offset of + the first frame in the stream. The frame layout is: + + <Head for 'Position synchronisation', ID: "POSS"> + Time stamp format $xx + Position $xx (xx ...) + + Where time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + and position is where in the audio the listener starts to receive, + i.e. the beginning of the next frame. If this frame is used in the + beginning of a file the value is always 0. There may only be one + "POSS" frame in each tag. + + +4.23. Terms of use frame + + This frame contains a brief description of the terms of use and + ownership of the file. More detailed information concerning the legal + terms might be available through the "WCOP" frame. Newlines are + allowed in the text. There may only be one "USER" frame in a tag. + + <Header for 'Terms of use frame', ID: "USER"> + Text encoding $xx + Language $xx xx xx + The actual text <text string according to encoding> + + +4.24. Ownership frame + + The ownership frame might be used as a reminder of a made transaction + or, if signed, as proof. Note that the "USER" and "TOWN" frames are + good to use in conjunction with this one. The frame begins, after the + frame ID, size and encoding fields, with a 'price payed' field. The + first three characters of this field contains the currency used for + the transaction, encoded according to ISO 4217 [ISO-4217] alphabetic + currency code. Concatenated to this is the actual price payed, as a + numerical string using "." as the decimal separator. Next is an 8 + character date string (YYYYMMDD) followed by a string with the name + of the seller as the last field in the frame. There may only be one + "OWNE" frame in a tag. + + <Header for 'Ownership frame', ID: "OWNE"> + Text encoding $xx + Price payed <text string> $00 + Date of purch. <text string> + Seller <text string according to encoding> + + +4.25. Commercial frame + + This frame enables several competing offers in the same tag by + bundling all needed information. That makes this frame rather complex + but it's an easier solution than if one tries to achieve the same + result with several frames. The frame begins, after the frame ID, + size and encoding fields, with a price string field. A price is + constructed by one three character currency code, encoded according + to ISO 4217 [ISO-4217] alphabetic currency code, followed by a + numerical value where "." is used as decimal seperator. In the price + string several prices may be concatenated, seperated by a "/" + character, but there may only be one currency of each type. + + The price string is followed by an 8 character date string in the + format YYYYMMDD, describing for how long the price is valid. After + that is a contact URL, with which the user can contact the seller, + followed by a one byte 'received as' field. It describes how the + audio is delivered when bought according to the following list: + + $00 Other + $01 Standard CD album with other songs + $02 Compressed audio on CD + $03 File over the Internet + $04 Stream over the Internet + $05 As note sheets + $06 As note sheets in a book with other sheets + $07 Music on other media + $08 Non-musical merchandise + + Next follows a terminated string with the name of the seller followed + by a terminated string with a short description of the product. The + last thing is the ability to include a company logotype. The first of + them is the 'Picture MIME type' field containing information about + which picture format is used. In the event that the MIME media type + name is omitted, "image/" will be implied. Currently only "image/png" + and "image/jpeg" are allowed. This format string is followed by the + binary picture data. This two last fields may be omitted if no + picture is to attach. + + <Header for 'Commercial frame', ID: "COMR"> + Text encoding $xx + Price string <text string> $00 + Valid until <text string> + Contact URL <text string> $00 + Received as $xx + Name of seller <text string according to encoding> $00 (00) + Description <text string according to encoding> $00 (00) + Picture MIME type <string> $00 + Seller logo <binary data> + + +4.26. Encryption method registration + + To identify with which method a frame has been encrypted the + encryption method must be registered in the tag with this frame. The + 'Owner identifier' is a null-terminated string with a URL [URL] + containing an email address, or a link to a location where an email + address can be found, that belongs to the organisation responsible + for this specific encryption method. Questions regarding the + encryption method should be sent to the indicated email address. The + 'Method symbol' contains a value that is associated with this method + throughout the whole tag. Values below $80 are reserved. The 'Method + symbol' may optionally be followed by encryption specific data. There + may be several "ENCR" frames in a tag but only one containing the + same symbol and only one containing the same owner identifier. The + method must be used somewhere in the tag. See section 3.3.1, flag j + for more information. + + <Header for 'Encryption method registration', ID: "ENCR"> + Owner identifier <text string> $00 + Method symbol $xx + Encryption data <binary data> + + +4.27. Group identification registration + + This frame enables grouping of otherwise unrelated frames. This can + be used when some frames are to be signed. To identify which frames + belongs to a set of frames a group identifier must be registered in + the tag with this frame. The 'Owner identifier' is a null-terminated + string with a URL [URL] containing an email address, or a link to a + location where an email address can be found, that belongs to the + organisation responsible for this grouping. Questions regarding the + grouping should be sent to the indicated email address. The 'Group + symbol' contains a value that associates the frame with this group + throughout the whole tag. Values below $80 are reserved. The 'Group + symbol' may optionally be followed by some group specific data, e.g. + a digital signature. There may be several "GRID" frames in a tag but + only one containing the same symbol and only one containing the same + owner identifier. The group symbol must be used somewhere in the tag. + See section 3.3.1, flag j for more information. + + <Header for 'Group ID registration', ID: "GRID"> + Owner identifier <text string> $00 + Group symbol $xx + Group dependent data <binary data> + + +4.28. Private frame + + This frame is used to contain information from a software producer + that its program uses and does not fit into the other frames. The + frame consists of an 'Owner identifier' string and the binary data. + The 'Owner identifier' is a null-terminated string with a URL [URL] + containing an email address, or a link to a location where an email + address can be found, that belongs to the organisation responsible + for the frame. Questions regarding the frame should be sent to the + indicated email address. The tag may contain more than one "PRIV" + frame but only with different contents. It is recommended to keep the + number of "PRIV" frames as low as possible. + + <Header for 'Private frame', ID: "PRIV"> + Owner identifier <text string> $00 + The private data <binary data> + + +5. The 'unsynchronisation scheme' + + The only purpose of the 'unsynchronisation scheme' is to make the + ID3v2 tag as compatible as possible with existing software. There is + no use in 'unsynchronising' tags if the file is only to be processed + by new software. Unsynchronisation may only be made with MPEG 2 layer + I, II and III and MPEG 2.5 files. + + Whenever a false synchronisation is found within the tag, one zeroed + byte is inserted after the first false synchronisation byte. The + format of a correct sync that should be altered by ID3 encoders is as + follows: + + %11111111 111xxxxx + + And should be replaced with: + + %11111111 00000000 111xxxxx + + This has the side effect that all $FF 00 combinations have to be + altered, so they won't be affected by the decoding process. Therefore + all the $FF 00 combinations have to be replaced with the $FF 00 00 + combination during the unsynchronisation. + + To indicate usage of the unsynchronisation, the first bit in 'ID3 + flags' should be set. This bit should only be set if the tag + contains a, now corrected, false synchronisation. The bit should + only be clear if the tag does not contain any false synchronisations. + + Do bear in mind, that if a compression scheme is used by the encoder, + the unsynchronisation scheme should be applied *afterwards*. When + decoding a compressed, 'unsynchronised' file, the 'unsynchronisation + scheme' should be parsed first, decompression afterwards. + + If the last byte in the tag is $FF, and there is a need to eliminate + false synchronisations in the tag, at least one byte of padding + should be added. + + +6. Copyright + + Copyright (C) Martin Nilsson 1998. All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that a reference to this document is included on all + such copies and derivative works. However, this document itself may + not be modified in any way and reissued as the original document. + + The limited permissions granted above are perpetual and will not be + revoked. + + This document and the information contained herein is provided on an + "AS IS" basis and THE AUTHORS DISCLAIMS ALL WARRANTIES, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF + THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + + +7. References + + [CDDB] Compact Disc Data Base + + http://www.cddb.com + + [ID3v2] Martin Nilsson, "ID3v2 informal standard". + + http://www.id3lib.org/id3/id3v2-00.txt + + [ISO-639-2] ISO/FDIS 639-2. + Codes for the representation of names of languages, Part 2: Alpha-3 + code. Technical committee / subcommittee: TC 37 / SC 2 + + [ISO-4217] ISO 4217:1995. + Codes for the representation of currencies and funds. + Technical committee / subcommittee: TC 68 + + [ISO-8859-1] ISO/IEC DIS 8859-1. + 8-bit single-byte coded graphic character sets, Part 1: Latin + alphabet No. 1. Technical committee / subcommittee: JTC 1 / SC 2 + + [ISRC] ISO 3901:1986 + International Standard Recording Code (ISRC). + Technical committee / subcommittee: TC 46 / SC 9 + + [JFIF] JPEG File Interchange Format, version 1.02 + + http://www.w3.org/Graphics/JPEG/jfif.txt">http://www.w3.org/Graphics/JPEG/jfif.txt + + [MIME] Freed, N. and N. Borenstein, "Multipurpose Internet Mail + Extensions (MIME) Part One: Format of Internet Message Bodies", + RFC 2045, November 1996. + + ftp://ftp.isi.edu/in-notes/rfc2045.txt">ftp://ftp.isi.edu/in-notes/rfc2045.txt + + [MPEG] ISO/IEC 11172-3:1993. + Coding of moving pictures and associated audio for digital storage + media at up to about 1,5 Mbit/s, Part 3: Audio. + Technical committee / subcommittee: JTC 1 / SC 29 + and + ISO/IEC 13818-3:1995 + Generic coding of moving pictures and associated audio information, + Part 3: Audio. + Technical committee / subcommittee: JTC 1 / SC 29 + and + ISO/IEC DIS 13818-3 + Generic coding of moving pictures and associated audio information, + Part 3: Audio (Revision of ISO/IEC 13818-3:1995) + + + [PNG] Portable Network Graphics, version 1.0 + + http://www.w3.org/TR/REC-png-multi.html + + [UNICODE] ISO/IEC 10646-1:1993. + Universal Multiple-Octet Coded Character Set (UCS), Part 1: + Architecture and Basic Multilingual Plane. + Technical committee / subcommittee: JTC 1 / SC 2 + + http://www.unicode.org/ + + [URL] T. Berners-Lee, L. Masinter & M. McCahill, "Uniform Resource + Locators (URL).", RFC 1738, December 1994. + + ftp://ftp.isi.edu/in-notes/rfc1738.txt + + [ZLIB] P. Deutsch, Aladdin Enterprises & J-L. Gailly, "ZLIB + Compressed + Data Format Specification version 3.3", RFC 1950, May 1996. + + ftp://ftp.isi.edu/in-notes/rfc1950.txt + + +8. Appendix + + +A. Appendix A - Genre List from ID3v1 + + The following genres is defined in ID3v1 + + 0.Blues + 1.Classic Rock + 2.Country + 3.Dance + 4.Disco + 5.Funk + 6.Grunge + 7.Hip-Hop + 8.Jazz + 9.Metal + 10.New Age + 11.Oldies + 12.Other + 13.Pop + 14.R&B + 15.Rap + 16.Reggae + 17.Rock + 18.Techno + 19.Industrial + 20.Alternative + 21.Ska + 22.Death Metal + 23.Pranks + 24.Soundtrack + 25.Euro-Techno + 26.Ambient + 27.Trip-Hop + 28.Vocal + 29.Jazz+Funk + 30.Fusion + 31.Trance + 32.Classical + 33.Instrumental + 34.Acid + 35.House + 36.Game + 37.Sound Clip + 38.Gospel + 39.Noise + 40.AlternRock + 41.Bass + 42.Soul + 43.Punk + 44.Space + 45.Meditative + 46.Instrumental Pop + 47.Instrumental Rock + 48.Ethnic + 49.Gothic + 50.Darkwave + 51.Techno-Industrial + 52.Electronic + 53.Pop-Folk + 54.Eurodance + 55.Dream + 56.Southern Rock + 57.Comedy + 58.Cult + 59.Gangsta + 60.Top 40 + 61.Christian Rap + 62.Pop/Funk + 63.Jungle + 64.Native American + 65.Cabaret + 66.New Wave + 67.Psychadelic + 68.Rave + 69.Showtunes + 70.Trailer + 71.Lo-Fi + 72.Tribal + 73.Acid Punk + 74.Acid Jazz + 75.Polka + 76.Retro + 77.Musical + 78.Rock & Roll + 79.Hard Rock + + The following genres are Winamp extensions + + 80.Folk + 81.Folk-Rock + 82.National Folk + 83.Swing + 84.Fast Fusion + 85.Bebob + 86.Latin + 87.Revival + 88.Celtic + 89.Bluegrass + 90.Avantgarde + 91.Gothic Rock + 92.Progressive Rock + 93.Psychedelic Rock + 94.Symphonic Rock + 95.Slow Rock + 96.Big Band + 97.Chorus + 98.Easy Listening + 99.Acoustic + 100.Humour + 101.Speech + 102.Chanson + 103.Opera + 104.Chamber Music + 105.Sonata + 106.Symphony + 107.Booty Bass + 108.Primus + 109.Porn Groove + 110.Satire + 111.Slow Jam + 112.Club + 113.Tango + 114.Samba + 115.Folklore + 116.Ballad + 117.Power Ballad + 118.Rhythmic Soul + 119.Freestyle + 120.Duet + 121.Punk Rock + 122.Drum Solo + 123.Acapella + 124.Euro-House + 125.Dance Hall + + +9. Author's Address + + Written by + + Martin Nilsson + Rydsvägen 246 C. 30 + S-584 34 Linköping + Sweden + + Email: nilsson@id3.org + + + Edited by + + Dirk Mahoney + 57 Pechey Street + Chermside Q + Australia 4032 + + Email: dirk@id3.org + + + Johan Sundström + Alsättersgatan 5 A. 34 + S-584 35 Linköping + Sweden + + Email: johan@id3.org diff --git a/taglib/taglib/mpeg/id3v2/id3v2.4.0-frames.txt b/taglib/taglib/mpeg/id3v2/id3v2.4.0-frames.txt new file mode 100644 index 0000000..74a21be --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2.4.0-frames.txt @@ -0,0 +1,1734 @@ +$Id$ + +Informal standard M. Nilsson +Document: id3v2.4.0-frames.txt 1st November 2000 + + + ID3 tag version 2.4.0 - Native Frames + +Status of this document + + This document is an informal standard and replaces the ID3v2.3.0 + standard [ID3v2]. A formal standard will use another revision number + even if the content is identical to document. The contents in this + document may change for clarifications but never for added or altered + functionallity. + + Distribution of this document is unlimited. + + +Abstract + + This document describes the frames natively supported by ID3v2.4.0, + which is a revised version of the ID3v2 informal standard [ID3v2.3.0] + version 2.3.0. The ID3v2 offers a flexible way of storing audio meta + information within audio file itself. The information may be + technical information, such as equalisation curves, as well as title, + performer, copyright etc. + + ID3v2.4.0 is meant to be as close as possible to ID3v2.3.0 in order + to allow for implementations to be revised as easily as possible. + + +1. Table of contents + + 2. Conventions in this document + 3. Default flags + 4. Declared ID3v2 frames + 4.1. Unique file identifier + 4.2. Text information frames + 4.2.1. Identification frames + 4.2.2. Involved persons frames + 4.2.3. Derived and subjective properties frames + 4.2.4. Rights and license frames + 4.2.5. Other text frames + 4.2.6. User defined text information frame + 4.3. URL link frames + 4.3.1. URL link frames - details + 4.3.2. User defined URL link frame + 4.4. Music CD Identifier + 4.5. Event timing codes + 4.6. MPEG location lookup table + 4.7. Synced tempo codes + 4.8. Unsynchronised lyrics/text transcription + 4.9. Synchronised lyrics/text + 4.10. Comments + 4.11. Relative volume adjustment (2) + 4.12. Equalisation (2) + 4.13. Reverb + 4.14. Attached picture + 4.15. General encapsulated object + 4.16. Play counter + 4.17. Popularimeter + 4.18. Recommended buffer size + 4.19. Audio encryption + 4.20. Linked information + 4.21. Position synchronisation frame + 4.22. Terms of use + 4.23. Ownership frame + 4.24. Commercial frame + 4.25. Encryption method registration + 4.26. Group identification registration + 4.27. Private frame + 4.28. Signature frame + 4.29. Seek frame + 4.30. Audio seek point index + 5. Copyright + 6. References + 7. Appendix + A. Appendix A - Genre List from ID3v1 + 8. Author's Address + + +2. Conventions in this document + + Text within "" is a text string exactly as it appears in a tag. + Numbers preceded with $ are hexadecimal and numbers preceded with % + are binary. $xx is used to indicate a byte with unknown content. %x + is used to indicate a bit with unknown content. The most significant + bit (MSB) of a byte is called 'bit 7' and the least significant bit + (LSB) is called 'bit 0'. + + A tag is the whole tag described the ID3v2 main structure document + [ID3v2-strct]. A frame is a block of information in the tag. The tag + consists of a header, frames and optional padding. A field is a piece + of information; one value, a string etc. A numeric string is a string + that consists of the characters "0123456789" only. + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in RFC 2119 [KEYWORDS]. + + +3. Default flags + + The default settings for the frames described in this document can be + divided into the following classes. The flags may be set differently + if found more suitable by the software. + + 1. Discarded if tag is altered, discarded if file is altered. + + None. + + 2. Discarded if tag is altered, preserved if file is altered. + + None. + + 3. Preserved if tag is altered, discarded if file is altered. + + ASPI, AENC, ETCO, EQU2, MLLT, POSS, SEEK, SYLT, SYTC, RVA2, TENC, + TLEN + + 4. Preserved if tag is altered, preserved if file is altered. + + The rest of the frames. + + +4. Declared ID3v2 frames + + The following frames are declared in this draft. + + 4.19 AENC Audio encryption + 4.14 APIC Attached picture + 4.30 ASPI Audio seek point index + + 4.10 COMM Comments + 4.24 COMR Commercial frame + + 4.25 ENCR Encryption method registration + 4.12 EQU2 Equalisation (2) + 4.5 ETCO Event timing codes + + 4.15 GEOB General encapsulated object + 4.26 GRID Group identification registration + + 4.20 LINK Linked information + + 4.4 MCDI Music CD identifier + 4.6 MLLT MPEG location lookup table + + 4.23 OWNE Ownership frame + + 4.27 PRIV Private frame + 4.16 PCNT Play counter + 4.17 POPM Popularimeter + 4.21 POSS Position synchronisation frame + + 4.18 RBUF Recommended buffer size + 4.11 RVA2 Relative volume adjustment (2) + 4.13 RVRB Reverb + + 4.29 SEEK Seek frame + 4.28 SIGN Signature frame + 4.9 SYLT Synchronised lyric/text + 4.7 SYTC Synchronised tempo codes + + 4.2.1 TALB Album/Movie/Show title + 4.2.3 TBPM BPM (beats per minute) + 4.2.2 TCOM Composer + 4.2.3 TCON Content type + 4.2.4 TCOP Copyright message + 4.2.5 TDEN Encoding time + 4.2.5 TDLY Playlist delay + 4.2.5 TDOR Original release time + 4.2.5 TDRC Recording time + 4.2.5 TDRL Release time + 4.2.5 TDTG Tagging time + 4.2.2 TENC Encoded by + 4.2.2 TEXT Lyricist/Text writer + 4.2.3 TFLT File type + 4.2.2 TIPL Involved people list + 4.2.1 TIT1 Content group description + 4.2.1 TIT2 Title/songname/content description + 4.2.1 TIT3 Subtitle/Description refinement + 4.2.3 TKEY Initial key + 4.2.3 TLAN Language(s) + 4.2.3 TLEN Length + 4.2.2 TMCL Musician credits list + 4.2.3 TMED Media type + 4.2.3 TMOO Mood + 4.2.1 TOAL Original album/movie/show title + 4.2.5 TOFN Original filename + 4.2.2 TOLY Original lyricist(s)/text writer(s) + 4.2.2 TOPE Original artist(s)/performer(s) + 4.2.4 TOWN File owner/licensee + 4.2.2 TPE1 Lead performer(s)/Soloist(s) + 4.2.2 TPE2 Band/orchestra/accompaniment + 4.2.2 TPE3 Conductor/performer refinement + 4.2.2 TPE4 Interpreted, remixed, or otherwise modified by + 4.2.1 TPOS Part of a set + 4.2.4 TPRO Produced notice + 4.2.4 TPUB Publisher + 4.2.1 TRCK Track number/Position in set + 4.2.4 TRSN Internet radio station name + 4.2.4 TRSO Internet radio station owner + 4.2.5 TSOA Album sort order + 4.2.5 TSOP Performer sort order + 4.2.5 TSOT Title sort order + 4.2.1 TSRC ISRC (international standard recording code) + 4.2.5 TSSE Software/Hardware and settings used for encoding + 4.2.1 TSST Set subtitle + 4.2.2 TXXX User defined text information frame + + 4.1 UFID Unique file identifier + 4.22 USER Terms of use + 4.8 USLT Unsynchronised lyric/text transcription + + 4.3.1 WCOM Commercial information + 4.3.1 WCOP Copyright/Legal information + 4.3.1 WOAF Official audio file webpage + 4.3.1 WOAR Official artist/performer webpage + 4.3.1 WOAS Official audio source webpage + 4.3.1 WORS Official Internet radio station homepage + 4.3.1 WPAY Payment + 4.3.1 WPUB Publishers official webpage + 4.3.2 WXXX User defined URL link frame + + +4.1. Unique file identifier + + This frame's purpose is to be able to identify the audio file in a + database, that may provide more information relevant to the content. + Since standardisation of such a database is beyond this document, all + UFID frames begin with an 'owner identifier' field. It is a null- + terminated string with a URL [URL] containing an email address, or a + link to a location where an email address can be found, that belongs + to the organisation responsible for this specific database + implementation. Questions regarding the database should be sent to + the indicated email address. The URL should not be used for the + actual database queries. The string + "http://www.id3.org/dummy/ufid.html" should be used for tests. The + 'Owner identifier' must be non-empty (more than just a termination). + The 'Owner identifier' is then followed by the actual identifier, + which may be up to 64 bytes. There may be more than one "UFID" frame + in a tag, but only one with the same 'Owner identifier'. + + <Header for 'Unique file identifier', ID: "UFID"> + Owner identifier <text string> $00 + Identifier <up to 64 bytes binary data> + + +4.2. Text information frames + + The text information frames are often the most important frames, + containing information like artist, album and more. There may only be + one text information frame of its kind in an tag. All text + information frames supports multiple strings, stored as a null + separated list, where null is reperesented by the termination code + for the charater encoding. All text frame identifiers begin with "T". + Only text frame identifiers begin with "T", with the exception of the + "TXXX" frame. All the text information frames have the following + format: + + <Header for 'Text information frame', ID: "T000" - "TZZZ", + excluding "TXXX" described in 4.2.6.> + Text encoding $xx + Information <text string(s) according to encoding> + + +4.2.1. Identification frames + + TIT1 + The 'Content group description' frame is used if the sound belongs to + a larger category of sounds/music. For example, classical music is + often sorted in different musical sections (e.g. "Piano Concerto", + "Weather - Hurricane"). + + TIT2 + The 'Title/Songname/Content description' frame is the actual name of + the piece (e.g. "Adagio", "Hurricane Donna"). + + TIT3 + The 'Subtitle/Description refinement' frame is used for information + directly related to the contents title (e.g. "Op. 16" or "Performed + live at Wembley"). + + TALB + The 'Album/Movie/Show title' frame is intended for the title of the + recording (or source of sound) from which the audio in the file is + taken. + + TOAL + The 'Original album/movie/show title' frame is intended for the title + of the original recording (or source of sound), if for example the + music in the file should be a cover of a previously released song. + + TRCK + The 'Track number/Position in set' frame is a numeric string + containing the order number of the audio-file on its original + recording. This MAY be extended with a "/" character and a numeric + string containing the total number of tracks/elements on the original + recording. E.g. "4/9". + + TPOS + The 'Part of a set' frame is a numeric string that describes which + part of a set the audio came from. This frame is used if the source + described in the "TALB" frame is divided into several mediums, e.g. a + double CD. The value MAY be extended with a "/" character and a + numeric string containing the total number of parts in the set. E.g. + "1/2". + + TSST + The 'Set subtitle' frame is intended for the subtitle of the part of + a set this track belongs to. + + TSRC + The 'ISRC' frame should contain the International Standard Recording + Code [ISRC] (12 characters). + + +4.2.2. Involved persons frames + + TPE1 + The 'Lead artist/Lead performer/Soloist/Performing group' is + used for the main artist. + + TPE2 + The 'Band/Orchestra/Accompaniment' frame is used for additional + information about the performers in the recording. + + TPE3 + The 'Conductor' frame is used for the name of the conductor. + + TPE4 + The 'Interpreted, remixed, or otherwise modified by' frame contains + more information about the people behind a remix and similar + interpretations of another existing piece. + + TOPE + The 'Original artist/performer' frame is intended for the performer + of the original recording, if for example the music in the file + should be a cover of a previously released song. + + TEXT + The 'Lyricist/Text writer' frame is intended for the writer of the + text or lyrics in the recording. + + TOLY + The 'Original lyricist/text writer' frame is intended for the + text writer of the original recording, if for example the music in + the file should be a cover of a previously released song. + + TCOM + The 'Composer' frame is intended for the name of the composer. + + TMCL + The 'Musician credits list' is intended as a mapping between + instruments and the musician that played it. Every odd field is an + instrument and every even is an artist or a comma delimited list of + artists. + + TIPL + The 'Involved people list' is very similar to the musician credits + list, but maps between functions, like producer, and names. + + TENC + The 'Encoded by' frame contains the name of the person or + organisation that encoded the audio file. This field may contain a + copyright message, if the audio file also is copyrighted by the + encoder. + + +4.2.3. Derived and subjective properties frames + + TBPM + The 'BPM' frame contains the number of beats per minute in the + main part of the audio. The BPM is an integer and represented as a + numerical string. + + TLEN + The 'Length' frame contains the length of the audio file in + milliseconds, represented as a numeric string. + + TKEY + The 'Initial key' frame contains the musical key in which the sound + starts. It is represented as a string with a maximum length of three + characters. The ground keys are represented with "A","B","C","D","E", + "F" and "G" and halfkeys represented with "b" and "#". Minor is + represented as "m", e.g. "Dbm" $00. Off key is represented with an + "o" only. + + TLAN + The 'Language' frame should contain the languages of the text or + lyrics spoken or sung in the audio. The language is represented with + three characters according to ISO-639-2 [ISO-639-2]. If more than one + language is used in the text their language codes should follow + according to the amount of their usage, e.g. "eng" $00 "sve" $00. + + TCON + The 'Content type', which ID3v1 was stored as a one byte numeric + value only, is now a string. You may use one or several of the ID3v1 + types as numerical strings, or, since the category list would be + impossible to maintain with accurate and up to date categories, + define your own. Example: "21" $00 "Eurodisco" $00 + + You may also use any of the following keywords: + + RX Remix + CR Cover + + TFLT + The 'File type' frame indicates which type of audio this tag defines. + The following types and refinements are defined: + + MIME MIME type follows + MPG MPEG Audio + /1 MPEG 1/2 layer I + /2 MPEG 1/2 layer II + /3 MPEG 1/2 layer III + /2.5 MPEG 2.5 + /AAC Advanced audio compression + VQF Transform-domain Weighted Interleave Vector Quantisation + PCM Pulse Code Modulated audio + + but other types may be used, but not for these types though. This is + used in a similar way to the predefined types in the "TMED" frame, + but without parentheses. If this frame is not present audio type is + assumed to be "MPG". + + TMED + The 'Media type' frame describes from which media the sound + originated. This may be a text string or a reference to the + predefined media types found in the list below. Example: + "VID/PAL/VHS" $00. + + DIG Other digital media + /A Analogue transfer from media + + ANA Other analogue media + /WAC Wax cylinder + /8CA 8-track tape cassette + + CD CD + /A Analogue transfer from media + /DD DDD + /AD ADD + /AA AAD + + LD Laserdisc + + TT Turntable records + /33 33.33 rpm + /45 45 rpm + /71 71.29 rpm + /76 76.59 rpm + /78 78.26 rpm + /80 80 rpm + + MD MiniDisc + /A Analogue transfer from media + + DAT DAT + /A Analogue transfer from media + /1 standard, 48 kHz/16 bits, linear + /2 mode 2, 32 kHz/16 bits, linear + /3 mode 3, 32 kHz/12 bits, non-linear, low speed + /4 mode 4, 32 kHz/12 bits, 4 channels + /5 mode 5, 44.1 kHz/16 bits, linear + /6 mode 6, 44.1 kHz/16 bits, 'wide track' play + + DCC DCC + /A Analogue transfer from media + + DVD DVD + /A Analogue transfer from media + + TV Television + /PAL PAL + /NTSC NTSC + /SECAM SECAM + + VID Video + /PAL PAL + /NTSC NTSC + /SECAM SECAM + /VHS VHS + /SVHS S-VHS + /BETA BETAMAX + + RAD Radio + /FM FM + /AM AM + /LW LW + /MW MW + + TEL Telephone + /I ISDN + + MC MC (normal cassette) + /4 4.75 cm/s (normal speed for a two sided cassette) + /9 9.5 cm/s + /I Type I cassette (ferric/normal) + /II Type II cassette (chrome) + /III Type III cassette (ferric chrome) + /IV Type IV cassette (metal) + + REE Reel + /9 9.5 cm/s + /19 19 cm/s + /38 38 cm/s + /76 76 cm/s + /I Type I cassette (ferric/normal) + /II Type II cassette (chrome) + /III Type III cassette (ferric chrome) + /IV Type IV cassette (metal) + + TMOO + The 'Mood' frame is intended to reflect the mood of the audio with a + few keywords, e.g. "Romantic" or "Sad". + + +4.2.4. Rights and license frames + + TCOP + The 'Copyright message' frame, in which the string must begin with a + year and a space character (making five characters), is intended for + the copyright holder of the original sound, not the audio file + itself. The absence of this frame means only that the copyright + information is unavailable or has been removed, and must not be + interpreted to mean that the audio is public domain. Every time this + field is displayed the field must be preceded with "Copyright " (C) " + ", where (C) is one character showing a C in a circle. + + TPRO + The 'Produced notice' frame, in which the string must begin with a + year and a space character (making five characters), is intended for + the production copyright holder of the original sound, not the audio + file itself. The absence of this frame means only that the production + copyright information is unavailable or has been removed, and must + not be interpreted to mean that the audio is public domain. Every + time this field is displayed the field must be preceded with + "Produced " (P) " ", where (P) is one character showing a P in a + circle. + + TPUB + The 'Publisher' frame simply contains the name of the label or + publisher. + + TOWN + The 'File owner/licensee' frame contains the name of the owner or + licensee of the file and it's contents. + + TRSN + The 'Internet radio station name' frame contains the name of the + internet radio station from which the audio is streamed. + + TRSO + The 'Internet radio station owner' frame contains the name of the + owner of the internet radio station from which the audio is + streamed. + +4.2.5. Other text frames + + TOFN + The 'Original filename' frame contains the preferred filename for the + file, since some media doesn't allow the desired length of the + filename. The filename is case sensitive and includes its suffix. + + TDLY + The 'Playlist delay' defines the numbers of milliseconds of silence + that should be inserted before this audio. The value zero indicates + that this is a part of a multifile audio track that should be played + continuously. + + TDEN + The 'Encoding time' frame contains a timestamp describing when the + audio was encoded. Timestamp format is described in the ID3v2 + structure document [ID3v2-strct]. + + TDOR + The 'Original release time' frame contains a timestamp describing + when the original recording of the audio was released. Timestamp + format is described in the ID3v2 structure document [ID3v2-strct]. + + TDRC + The 'Recording time' frame contains a timestamp describing when the + audio was recorded. Timestamp format is described in the ID3v2 + structure document [ID3v2-strct]. + + TDRL + The 'Release time' frame contains a timestamp describing when the + audio was first released. Timestamp format is described in the ID3v2 + structure document [ID3v2-strct]. + + TDTG + The 'Tagging time' frame contains a timestamp describing then the + audio was tagged. Timestamp format is described in the ID3v2 + structure document [ID3v2-strct]. + + TSSE + The 'Software/Hardware and settings used for encoding' frame + includes the used audio encoder and its settings when the file was + encoded. Hardware refers to hardware encoders, not the computer on + which a program was run. + + TSOA + The 'Album sort order' frame defines a string which should be used + instead of the album name (TALB) for sorting purposes. E.g. an album + named "A Soundtrack" might preferably be sorted as "Soundtrack". + + TSOP + The 'Performer sort order' frame defines a string which should be + used instead of the performer (TPE2) for sorting purposes. + + TSOT + The 'Title sort order' frame defines a string which should be used + instead of the title (TIT2) for sorting purposes. + + +4.2.6. User defined text information frame + + This frame is intended for one-string text information concerning the + audio file in a similar way to the other "T"-frames. The frame body + consists of a description of the string, represented as a terminated + string, followed by the actual string. There may be more than one + "TXXX" frame in each tag, but only one with the same description. + + <Header for 'User defined text information frame', ID: "TXXX"> + Text encoding $xx + Description <text string according to encoding> $00 (00) + Value <text string according to encoding> + + +4.3. URL link frames + + With these frames dynamic data such as webpages with touring + information, price information or plain ordinary news can be added to + the tag. There may only be one URL [URL] link frame of its kind in an + tag, except when stated otherwise in the frame description. If the + text string is followed by a string termination, all the following + information should be ignored and not be displayed. All URL link + frame identifiers begins with "W". Only URL link frame identifiers + begins with "W", except for "WXXX". All URL link frames have the + following format: + + <Header for 'URL link frame', ID: "W000" - "WZZZ", excluding "WXXX" + described in 4.3.2.> + URL <text string> + + +4.3.1. URL link frames - details + + WCOM + The 'Commercial information' frame is a URL pointing at a webpage + with information such as where the album can be bought. There may be + more than one "WCOM" frame in a tag, but not with the same content. + + WCOP + The 'Copyright/Legal information' frame is a URL pointing at a + webpage where the terms of use and ownership of the file is + described. + + WOAF + The 'Official audio file webpage' frame is a URL pointing at a file + specific webpage. + + WOAR + The 'Official artist/performer webpage' frame is a URL pointing at + the artists official webpage. There may be more than one "WOAR" frame + in a tag if the audio contains more than one performer, but not with + the same content. + + WOAS + The 'Official audio source webpage' frame is a URL pointing at the + official webpage for the source of the audio file, e.g. a movie. + + WORS + The 'Official Internet radio station homepage' contains a URL + pointing at the homepage of the internet radio station. + + WPAY + The 'Payment' frame is a URL pointing at a webpage that will handle + the process of paying for this file. + + WPUB + The 'Publishers official webpage' frame is a URL pointing at the + official webpage for the publisher. + + +4.3.2. User defined URL link frame + + This frame is intended for URL [URL] links concerning the audio file + in a similar way to the other "W"-frames. The frame body consists + of a description of the string, represented as a terminated string, + followed by the actual URL. The URL is always encoded with ISO-8859-1 + [ISO-8859-1]. There may be more than one "WXXX" frame in each tag, + but only one with the same description. + + <Header for 'User defined URL link frame', ID: "WXXX"> + Text encoding $xx + Description <text string according to encoding> $00 (00) + URL <text string> + + +4.4. Music CD identifier + + This frame is intended for music that comes from a CD, so that the CD + can be identified in databases such as the CDDB [CDDB]. The frame + consists of a binary dump of the Table Of Contents, TOC, from the CD, + which is a header of 4 bytes and then 8 bytes/track on the CD plus 8 + bytes for the 'lead out', making a maximum of 804 bytes. The offset + to the beginning of every track on the CD should be described with a + four bytes absolute CD-frame address per track, and not with absolute + time. When this frame is used the presence of a valid "TRCK" frame is + REQUIRED, even if the CD's only got one track. It is recommended that + this frame is always added to tags originating from CDs. There may + only be one "MCDI" frame in each tag. + + <Header for 'Music CD identifier', ID: "MCDI"> + CD TOC <binary data> + + +4.5. Event timing codes + + This frame allows synchronisation with key events in the audio. The + header is: + + <Header for 'Event timing codes', ID: "ETCO"> + Time stamp format $xx + + Where time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Absolute time means that every stamp contains the time from the + beginning of the file. + + Followed by a list of key events in the following format: + + Type of event $xx + Time stamp $xx (xx ...) + + The 'Time stamp' is set to zero if directly at the beginning of the + sound or after the previous event. All events MUST be sorted in + chronological order. The type of event is as follows: + + $00 padding (has no meaning) + $01 end of initial silence + $02 intro start + $03 main part start + $04 outro start + $05 outro end + $06 verse start + $07 refrain start + $08 interlude start + $09 theme start + $0A variation start + $0B key change + $0C time change + $0D momentary unwanted noise (Snap, Crackle & Pop) + $0E sustained noise + $0F sustained noise end + $10 intro end + $11 main part end + $12 verse end + $13 refrain end + $14 theme end + $15 profanity + $16 profanity end + + $17-$DF reserved for future use + + $E0-$EF not predefined synch 0-F + + $F0-$FC reserved for future use + + $FD audio end (start of silence) + $FE audio file ends + $FF one more byte of events follows (all the following bytes with + the value $FF have the same function) + + Terminating the start events such as "intro start" is OPTIONAL. The + 'Not predefined synch's ($E0-EF) are for user events. You might want + to synchronise your music to something, like setting off an explosion + on-stage, activating a screensaver etc. + + There may only be one "ETCO" frame in each tag. + + +4.6. MPEG location lookup table + + To increase performance and accuracy of jumps within a MPEG [MPEG] + audio file, frames with time codes in different locations in the file + might be useful. This ID3v2 frame includes references that the + software can use to calculate positions in the file. After the frame + header follows a descriptor of how much the 'frame counter' should be + increased for every reference. If this value is two then the first + reference points out the second frame, the 2nd reference the 4th + frame, the 3rd reference the 6th frame etc. In a similar way the + 'bytes between reference' and 'milliseconds between reference' points + out bytes and milliseconds respectively. + + Each reference consists of two parts; a certain number of bits, as + defined in 'bits for bytes deviation', that describes the difference + between what is said in 'bytes between reference' and the reality and + a certain number of bits, as defined in 'bits for milliseconds + deviation', that describes the difference between what is said in + 'milliseconds between reference' and the reality. The number of bits + in every reference, i.e. 'bits for bytes deviation'+'bits for + milliseconds deviation', must be a multiple of four. There may only + be one "MLLT" frame in each tag. + + <Header for 'Location lookup table', ID: "MLLT"> + MPEG frames between reference $xx xx + Bytes between reference $xx xx xx + Milliseconds between reference $xx xx xx + Bits for bytes deviation $xx + Bits for milliseconds dev. $xx + + Then for every reference the following data is included; + + Deviation in bytes %xxx.... + Deviation in milliseconds %xxx.... + + +4.7. Synchronised tempo codes + + For a more accurate description of the tempo of a musical piece, this + frame might be used. After the header follows one byte describing + which time stamp format should be used. Then follows one or more + tempo codes. Each tempo code consists of one tempo part and one time + part. The tempo is in BPM described with one or two bytes. If the + first byte has the value $FF, one more byte follows, which is added + to the first giving a range from 2 - 510 BPM, since $00 and $01 is + reserved. $00 is used to describe a beat-free time period, which is + not the same as a music-free time period. $01 is used to indicate one + single beat-stroke followed by a beat-free period. + + The tempo descriptor is followed by a time stamp. Every time the + tempo in the music changes, a tempo descriptor may indicate this for + the player. All tempo descriptors MUST be sorted in chronological + order. The first beat-stroke in a time-period is at the same time as + the beat description occurs. There may only be one "SYTC" frame in + each tag. + + <Header for 'Synchronised tempo codes', ID: "SYTC"> + Time stamp format $xx + Tempo data <binary data> + + Where time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Absolute time means that every stamp contains the time from the + beginning of the file. + + +4.8. Unsynchronised lyrics/text transcription + + This frame contains the lyrics of the song or a text transcription of + other vocal activities. The head includes an encoding descriptor and + a content descriptor. The body consists of the actual text. The + 'Content descriptor' is a terminated string. If no descriptor is + entered, 'Content descriptor' is $00 (00) only. Newline characters + are allowed in the text. There may be more than one 'Unsynchronised + lyrics/text transcription' frame in each tag, but only one with the + same language and content descriptor. + + <Header for 'Unsynchronised lyrics/text transcription', ID: "USLT"> + Text encoding $xx + Language $xx xx xx + Content descriptor <text string according to encoding> $00 (00) + Lyrics/text <full text string according to encoding> + + +4.9. Synchronised lyrics/text + + This is another way of incorporating the words, said or sung lyrics, + in the audio file as text, this time, however, in sync with the + audio. It might also be used to describing events e.g. occurring on a + stage or on the screen in sync with the audio. The header includes a + content descriptor, represented with as terminated text string. If no + descriptor is entered, 'Content descriptor' is $00 (00) only. + + <Header for 'Synchronised lyrics/text', ID: "SYLT"> + Text encoding $xx + Language $xx xx xx + Time stamp format $xx + Content type $xx + Content descriptor <text string according to encoding> $00 (00) + + Content type: $00 is other + $01 is lyrics + $02 is text transcription + $03 is movement/part name (e.g. "Adagio") + $04 is events (e.g. "Don Quijote enters the stage") + $05 is chord (e.g. "Bb F Fsus") + $06 is trivia/'pop up' information + $07 is URLs to webpages + $08 is URLs to images + + Time stamp format: + + $01 Absolute time, 32 bit sized, using MPEG [MPEG] frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + Absolute time means that every stamp contains the time from the + beginning of the file. + + The text that follows the frame header differs from that of the + unsynchronised lyrics/text transcription in one major way. Each + syllable (or whatever size of text is considered to be convenient by + the encoder) is a null terminated string followed by a time stamp + denoting where in the sound file it belongs. Each sync thus has the + following structure: + + Terminated text to be synced (typically a syllable) + Sync identifier (terminator to above string) $00 (00) + Time stamp $xx (xx ...) + + The 'time stamp' is set to zero or the whole sync is omitted if + located directly at the beginning of the sound. All time stamps + should be sorted in chronological order. The sync can be considered + as a validator of the subsequent string. + + Newline characters are allowed in all "SYLT" frames and MUST be used + after every entry (name, event etc.) in a frame with the content type + $03 - $04. + + A few considerations regarding whitespace characters: Whitespace + separating words should mark the beginning of a new word, thus + occurring in front of the first syllable of a new word. This is also + valid for new line characters. A syllable followed by a comma should + not be broken apart with a sync (both the syllable and the comma + should be before the sync). + + An example: The "USLT" passage + + "Strangers in the night" $0A "Exchanging glances" + + would be "SYLT" encoded as: + + "Strang" $00 xx xx "ers" $00 xx xx " in" $00 xx xx " the" $00 xx xx + " night" $00 xx xx 0A "Ex" $00 xx xx "chang" $00 xx xx "ing" $00 xx + xx "glan" $00 xx xx "ces" $00 xx xx + + There may be more than one "SYLT" frame in each tag, but only one + with the same language and content descriptor. + + +4.10. Comments + + This frame is intended for any kind of full text information that + does not fit in any other frame. It consists of a frame header + followed by encoding, language and content descriptors and is ended + with the actual comment as a text string. Newline characters are + allowed in the comment text string. There may be more than one + comment frame in each tag, but only one with the same language and + content descriptor. + + <Header for 'Comment', ID: "COMM"> + Text encoding $xx + Language $xx xx xx + Short content descrip. <text string according to encoding> $00 (00) + The actual text <full text string according to encoding> + + +4.11. Relative volume adjustment (2) + + This is a more subjective frame than the previous ones. It allows the + user to say how much he wants to increase/decrease the volume on each + channel when the file is played. The purpose is to be able to align + all files to a reference volume, so that you don't have to change the + volume constantly. This frame may also be used to balance adjust the + audio. The volume adjustment is encoded as a fixed point decibel + value, 16 bit signed integer representing (adjustment*512), giving + +/- 64 dB with a precision of 0.001953125 dB. E.g. +2 dB is stored as + $04 00 and -2 dB is $FC 00. There may be more than one "RVA2" frame + in each tag, but only one with the same identification string. + + <Header for 'Relative volume adjustment (2)', ID: "RVA2"> + Identification <text string> $00 + + The 'identification' string is used to identify the situation and/or + device where this adjustment should apply. The following is then + repeated for every channel + + Type of channel $xx + Volume adjustment $xx xx + Bits representing peak $xx + Peak volume $xx (xx ...) + + + Type of channel: $00 Other + $01 Master volume + $02 Front right + $03 Front left + $04 Back right + $05 Back left + $06 Front centre + $07 Back centre + $08 Subwoofer + + Bits representing peak can be any number between 0 and 255. 0 means + that there is no peak volume field. The peak volume field is always + padded to whole bytes, setting the most significant bits to zero. + + +4.12. Equalisation (2) + + This is another subjective, alignment frame. It allows the user to + predefine an equalisation curve within the audio file. There may be + more than one "EQU2" frame in each tag, but only one with the same + identification string. + + <Header of 'Equalisation (2)', ID: "EQU2"> + Interpolation method $xx + Identification <text string> $00 + + The 'interpolation method' describes which method is preferred when + an interpolation between the adjustment point that follows. The + following methods are currently defined: + + $00 Band + No interpolation is made. A jump from one adjustment level to + another occurs in the middle between two adjustment points. + $01 Linear + Interpolation between adjustment points is linear. + + The 'identification' string is used to identify the situation and/or + device where this adjustment should apply. The following is then + repeated for every adjustment point + + Frequency $xx xx + Volume adjustment $xx xx + + The frequency is stored in units of 1/2 Hz, giving it a range from 0 + to 32767 Hz. + + The volume adjustment is encoded as a fixed point decibel value, 16 + bit signed integer representing (adjustment*512), giving +/- 64 dB + with a precision of 0.001953125 dB. E.g. +2 dB is stored as $04 00 + and -2 dB is $FC 00. + + Adjustment points should be ordered by frequency and one frequency + should only be described once in the frame. + + +4.13. Reverb + + Yet another subjective frame, with which you can adjust echoes of + different kinds. Reverb left/right is the delay between every bounce + in ms. Reverb bounces left/right is the number of bounces that should + be made. $FF equals an infinite number of bounces. Feedback is the + amount of volume that should be returned to the next echo bounce. $00 + is 0%, $FF is 100%. If this value were $7F, there would be 50% volume + reduction on the first bounce, 50% of that on the second and so on. + Left to left means the sound from the left bounce to be played in the + left speaker, while left to right means sound from the left bounce to + be played in the right speaker. + + 'Premix left to right' is the amount of left sound to be mixed in the + right before any reverb is applied, where $00 id 0% and $FF is 100%. + 'Premix right to left' does the same thing, but right to left. + Setting both premix to $FF would result in a mono output (if the + reverb is applied symmetric). There may only be one "RVRB" frame in + each tag. + + <Header for 'Reverb', ID: "RVRB"> + Reverb left (ms) $xx xx + Reverb right (ms) $xx xx + Reverb bounces, left $xx + Reverb bounces, right $xx + Reverb feedback, left to left $xx + Reverb feedback, left to right $xx + Reverb feedback, right to right $xx + Reverb feedback, right to left $xx + Premix left to right $xx + Premix right to left $xx + + +4.14. Attached picture + + This frame contains a picture directly related to the audio file. + Image format is the MIME type and subtype [MIME] for the image. In + the event that the MIME media type name is omitted, "image/" will be + implied. The "image/png" [PNG] or "image/jpeg" [JFIF] picture format + should be used when interoperability is wanted. Description is a + short description of the picture, represented as a terminated + text string. There may be several pictures attached to one file, each + in their individual "APIC" frame, but only one with the same content + descriptor. There may only be one picture with the picture type + declared as picture type $01 and $02 respectively. There is the + possibility to put only a link to the image file by using the 'MIME + type' "-->" and having a complete URL [URL] instead of picture data. + The use of linked files should however be used sparingly since there + is the risk of separation of files. + + <Header for 'Attached picture', ID: "APIC"> + Text encoding $xx + MIME type <text string> $00 + Picture type $xx + Description <text string according to encoding> $00 (00) + Picture data <binary data> + + + Picture type: $00 Other + $01 32x32 pixels 'file icon' (PNG only) + $02 Other file icon + $03 Cover (front) + $04 Cover (back) + $05 Leaflet page + $06 Media (e.g. label side of CD) + $07 Lead artist/lead performer/soloist + $08 Artist/performer + $09 Conductor + $0A Band/Orchestra + $0B Composer + $0C Lyricist/text writer + $0D Recording Location + $0E During recording + $0F During performance + $10 Movie/video screen capture + $11 A bright coloured fish + $12 Illustration + $13 Band/artist logotype + $14 Publisher/Studio logotype + + +4.15. General encapsulated object + + In this frame any type of file can be encapsulated. After the header, + 'Frame size' and 'Encoding' follows 'MIME type' [MIME] represented as + as a terminated string encoded with ISO 8859-1 [ISO-8859-1]. The + filename is case sensitive and is encoded as 'Encoding'. Then follows + a content description as terminated string, encoded as 'Encoding'. + The last thing in the frame is the actual object. The first two + strings may be omitted, leaving only their terminations. MIME type is + always an ISO-8859-1 text string. There may be more than one "GEOB" + frame in each tag, but only one with the same content descriptor. + + <Header for 'General encapsulated object', ID: "GEOB"> + Text encoding $xx + MIME type <text string> $00 + Filename <text string according to encoding> $00 (00) + Content description <text string according to encoding> $00 (00) + Encapsulated object <binary data> + + +4.16. Play counter + + This is simply a counter of the number of times a file has been + played. The value is increased by one every time the file begins to + play. There may only be one "PCNT" frame in each tag. When the + counter reaches all one's, one byte is inserted in front of the + counter thus making the counter eight bits bigger. The counter must + be at least 32-bits long to begin with. + + <Header for 'Play counter', ID: "PCNT"> + Counter $xx xx xx xx (xx ...) + + +4.17. Popularimeter + + The purpose of this frame is to specify how good an audio file is. + Many interesting applications could be found to this frame such as a + playlist that features better audio files more often than others or + it could be used to profile a person's taste and find other 'good' + files by comparing people's profiles. The frame contains the email + address to the user, one rating byte and a four byte play counter, + intended to be increased with one for every time the file is played. + The email is a terminated string. The rating is 1-255 where 1 is + worst and 255 is best. 0 is unknown. If no personal counter is wanted + it may be omitted. When the counter reaches all one's, one byte is + inserted in front of the counter thus making the counter eight bits + bigger in the same away as the play counter ("PCNT"). There may be + more than one "POPM" frame in each tag, but only one with the same + email address. + + <Header for 'Popularimeter', ID: "POPM"> + Email to user <text string> $00 + Rating $xx + Counter $xx xx xx xx (xx ...) + + +4.18. Recommended buffer size + + Sometimes the server from which an audio file is streamed is aware of + transmission or coding problems resulting in interruptions in the + audio stream. In these cases, the size of the buffer can be + recommended by the server using this frame. If the 'embedded info + flag' is true (1) then this indicates that an ID3 tag with the + maximum size described in 'Buffer size' may occur in the audio + stream. In such case the tag should reside between two MPEG [MPEG] + frames, if the audio is MPEG encoded. If the position of the next tag + is known, 'offset to next tag' may be used. The offset is calculated + from the end of tag in which this frame resides to the first byte of + the header in the next. This field may be omitted. Embedded tags are + generally not recommended since this could render unpredictable + behaviour from present software/hardware. + + For applications like streaming audio it might be an idea to embed + tags into the audio stream though. If the clients connects to + individual connections like HTTP and there is a possibility to begin + every transmission with a tag, then this tag should include a + 'recommended buffer size' frame. If the client is connected to a + arbitrary point in the stream, such as radio or multicast, then the + 'recommended buffer size' frame SHOULD be included in every tag. + + The 'Buffer size' should be kept to a minimum. There may only be one + "RBUF" frame in each tag. + + <Header for 'Recommended buffer size', ID: "RBUF"> + Buffer size $xx xx xx + Embedded info flag %0000000x + Offset to next tag $xx xx xx xx + + +4.19. Audio encryption + + This frame indicates if the actual audio stream is encrypted, and by + whom. Since standardisation of such encryption scheme is beyond this + document, all "AENC" frames begin with a terminated string with a + URL containing an email address, or a link to a location where an + email address can be found, that belongs to the organisation + responsible for this specific encrypted audio file. Questions + regarding the encrypted audio should be sent to the email address + specified. If a $00 is found directly after the 'Frame size' and the + audio file indeed is encrypted, the whole file may be considered + useless. + + After the 'Owner identifier', a pointer to an unencrypted part of the + audio can be specified. The 'Preview start' and 'Preview length' is + described in frames. If no part is unencrypted, these fields should + be left zeroed. After the 'preview length' field follows optionally a + data block required for decryption of the audio. There may be more + than one "AENC" frames in a tag, but only one with the same 'Owner + identifier'. + + <Header for 'Audio encryption', ID: "AENC"> + Owner identifier <text string> $00 + Preview start $xx xx + Preview length $xx xx + Encryption info <binary data> + + +4.20. Linked information + + To keep information duplication as low as possible this frame may be + used to link information from another ID3v2 tag that might reside in + another audio file or alone in a binary file. It is RECOMMENDED that + this method is only used when the files are stored on a CD-ROM or + other circumstances when the risk of file separation is low. The + frame contains a frame identifier, which is the frame that should be + linked into this tag, a URL [URL] field, where a reference to the + file where the frame is given, and additional ID data, if needed. + Data should be retrieved from the first tag found in the file to + which this link points. There may be more than one "LINK" frame in a + tag, but only one with the same contents. A linked frame is to be + considered as part of the tag and has the same restrictions as if it + was a physical part of the tag (i.e. only one "RVRB" frame allowed, + whether it's linked or not). + + <Header for 'Linked information', ID: "LINK"> + Frame identifier $xx xx xx xx + URL <text string> $00 + ID and additional data <text string(s)> + + Frames that may be linked and need no additional data are "ASPI", + "ETCO", "EQU2", "MCID", "MLLT", "OWNE", "RVA2", "RVRB", "SYTC", the + text information frames and the URL link frames. + + The "AENC", "APIC", "GEOB" and "TXXX" frames may be linked with + the content descriptor as additional ID data. + + The "USER" frame may be linked with the language field as additional + ID data. + + The "PRIV" frame may be linked with the owner identifier as + additional ID data. + + The "COMM", "SYLT" and "USLT" frames may be linked with three bytes + of language descriptor directly followed by a content descriptor as + additional ID data. + + +4.21. Position synchronisation frame + + This frame delivers information to the listener of how far into the + audio stream he picked up; in effect, it states the time offset from + the first frame in the stream. The frame layout is: + + <Head for 'Position synchronisation', ID: "POSS"> + Time stamp format $xx + Position $xx (xx ...) + + Where time stamp format is: + + $01 Absolute time, 32 bit sized, using MPEG frames as unit + $02 Absolute time, 32 bit sized, using milliseconds as unit + + and position is where in the audio the listener starts to receive, + i.e. the beginning of the next frame. If this frame is used in the + beginning of a file the value is always 0. There may only be one + "POSS" frame in each tag. + + +4.22. Terms of use frame + + This frame contains a brief description of the terms of use and + ownership of the file. More detailed information concerning the legal + terms might be available through the "WCOP" frame. Newlines are + allowed in the text. There may be more than one 'Terms of use' frame + in a tag, but only one with the same 'Language'. + + <Header for 'Terms of use frame', ID: "USER"> + Text encoding $xx + Language $xx xx xx + The actual text <text string according to encoding> + + +4.23. Ownership frame + + The ownership frame might be used as a reminder of a made transaction + or, if signed, as proof. Note that the "USER" and "TOWN" frames are + good to use in conjunction with this one. The frame begins, after the + frame ID, size and encoding fields, with a 'price paid' field. The + first three characters of this field contains the currency used for + the transaction, encoded according to ISO 4217 [ISO-4217] alphabetic + currency code. Concatenated to this is the actual price paid, as a + numerical string using "." as the decimal separator. Next is an 8 + character date string (YYYYMMDD) followed by a string with the name + of the seller as the last field in the frame. There may only be one + "OWNE" frame in a tag. + + <Header for 'Ownership frame', ID: "OWNE"> + Text encoding $xx + Price paid <text string> $00 + Date of purch. <text string> + Seller <text string according to encoding> + + +4.24. Commercial frame + + This frame enables several competing offers in the same tag by + bundling all needed information. That makes this frame rather complex + but it's an easier solution than if one tries to achieve the same + result with several frames. The frame begins, after the frame ID, + size and encoding fields, with a price string field. A price is + constructed by one three character currency code, encoded according + to ISO 4217 [ISO-4217] alphabetic currency code, followed by a + numerical value where "." is used as decimal separator. In the price + string several prices may be concatenated, separated by a "/" + character, but there may only be one currency of each type. + + The price string is followed by an 8 character date string in the + format YYYYMMDD, describing for how long the price is valid. After + that is a contact URL, with which the user can contact the seller, + followed by a one byte 'received as' field. It describes how the + audio is delivered when bought according to the following list: + + $00 Other + $01 Standard CD album with other songs + $02 Compressed audio on CD + $03 File over the Internet + $04 Stream over the Internet + $05 As note sheets + $06 As note sheets in a book with other sheets + $07 Music on other media + $08 Non-musical merchandise + + Next follows a terminated string with the name of the seller followed + by a terminated string with a short description of the product. The + last thing is the ability to include a company logotype. The first of + them is the 'Picture MIME type' field containing information about + which picture format is used. In the event that the MIME media type + name is omitted, "image/" will be implied. Currently only "image/png" + and "image/jpeg" are allowed. This format string is followed by the + binary picture data. This two last fields may be omitted if no + picture is attached. There may be more than one 'commercial frame' in + a tag, but no two may be identical. + + <Header for 'Commercial frame', ID: "COMR"> + Text encoding $xx + Price string <text string> $00 + Valid until <text string> + Contact URL <text string> $00 + Received as $xx + Name of seller <text string according to encoding> $00 (00) + Description <text string according to encoding> $00 (00) + Picture MIME type <string> $00 + Seller logo <binary data> + + +4.25. Encryption method registration + + To identify with which method a frame has been encrypted the + encryption method must be registered in the tag with this frame. The + 'Owner identifier' is a null-terminated string with a URL [URL] + containing an email address, or a link to a location where an email + address can be found, that belongs to the organisation responsible + for this specific encryption method. Questions regarding the + encryption method should be sent to the indicated email address. The + 'Method symbol' contains a value that is associated with this method + throughout the whole tag, in the range $80-F0. All other values are + reserved. The 'Method symbol' may optionally be followed by + encryption specific data. There may be several "ENCR" frames in a tag + but only one containing the same symbol and only one containing the + same owner identifier. The method must be used somewhere in the tag. + See the description of the frame encryption flag in the ID3v2 + structure document [ID3v2-strct] for more information. + + <Header for 'Encryption method registration', ID: "ENCR"> + Owner identifier <text string> $00 + Method symbol $xx + Encryption data <binary data> + + +4.26. Group identification registration + + This frame enables grouping of otherwise unrelated frames. This can + be used when some frames are to be signed. To identify which frames + belongs to a set of frames a group identifier must be registered in + the tag with this frame. The 'Owner identifier' is a null-terminated + string with a URL [URL] containing an email address, or a link to a + location where an email address can be found, that belongs to the + organisation responsible for this grouping. Questions regarding the + grouping should be sent to the indicated email address. The 'Group + symbol' contains a value that associates the frame with this group + throughout the whole tag, in the range $80-F0. All other values are + reserved. The 'Group symbol' may optionally be followed by some group + specific data, e.g. a digital signature. There may be several "GRID" + frames in a tag but only one containing the same symbol and only one + containing the same owner identifier. The group symbol must be used + somewhere in the tag. See the description of the frame grouping flag + in the ID3v2 structure document [ID3v2-strct] for more information. + + <Header for 'Group ID registration', ID: "GRID"> + Owner identifier <text string> $00 + Group symbol $xx + Group dependent data <binary data> + + +4.27. Private frame + + This frame is used to contain information from a software producer + that its program uses and does not fit into the other frames. The + frame consists of an 'Owner identifier' string and the binary data. + The 'Owner identifier' is a null-terminated string with a URL [URL] + containing an email address, or a link to a location where an email + address can be found, that belongs to the organisation responsible + for the frame. Questions regarding the frame should be sent to the + indicated email address. The tag may contain more than one "PRIV" + frame but only with different contents. + + <Header for 'Private frame', ID: "PRIV"> + Owner identifier <text string> $00 + The private data <binary data> + + +4.28. Signature frame + + This frame enables a group of frames, grouped with the 'Group + identification registration', to be signed. Although signatures can + reside inside the registration frame, it might be desired to store + the signature elsewhere, e.g. in watermarks. There may be more than + one 'signature frame' in a tag, but no two may be identical. + + <Header for 'Signature frame', ID: "SIGN"> + Group symbol $xx + Signature <binary data> + + +4.29. Seek frame + + This frame indicates where other tags in a file/stream can be found. + The 'minimum offset to next tag' is calculated from the end of this + tag to the beginning of the next. There may only be one 'seek frame' + in a tag. + + <Header for 'Seek frame', ID: "SEEK"> + Minimum offset to next tag $xx xx xx xx + + +4.30. Audio seek point index + + Audio files with variable bit rates are intrinsically difficult to + deal with in the case of seeking within the file. The ASPI frame + makes seeking easier by providing a list a seek points within the + audio file. The seek points are a fractional offset within the audio + data, providing a starting point from which to find an appropriate + point to start decoding. The presence of an ASPI frame requires the + existence of a TLEN frame, indicating the duration of the file in + milliseconds. There may only be one 'audio seek point index' frame in + a tag. + + <Header for 'Seek Point Index', ID: "ASPI"> + Indexed data start (S) $xx xx xx xx + Indexed data length (L) $xx xx xx xx + Number of index points (N) $xx xx + Bits per index point (b) $xx + + Then for every index point the following data is included; + + Fraction at index (Fi) $xx (xx) + + 'Indexed data start' is a byte offset from the beginning of the file. + 'Indexed data length' is the byte length of the audio data being + indexed. 'Number of index points' is the number of index points, as + the name implies. The recommended number is 100. 'Bits per index + point' is 8 or 16, depending on the chosen precision. 8 bits works + well for short files (less than 5 minutes of audio), while 16 bits is + advantageous for long files. 'Fraction at index' is the numerator of + the fraction representing a relative position in the data. The + denominator is 2 to the power of b. + + Here are the algorithms to be used in the calculation. The known data + must be the offset of the start of the indexed data (S), the offset + of the end of the indexed data (E), the number of index points (N), + the offset at index i (Oi). We calculate the fraction at index i + (Fi). + + Oi is the offset of the frame whose start is soonest after the point + for which the time offset is (i/N * duration). + + The frame data should be calculated as follows: + + Fi = Oi/L * 2^b (rounded down to the nearest integer) + + Offset calculation should be calculated as follows from data in the + frame: + + Oi = (Fi/2^b)*L (rounded up to the nearest integer) + + +5. Copyright + + Copyright (C) Martin Nilsson 2000. All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that a reference to this document is included on all + such copies and derivative works. However, this document itself may + not be modified in any way and reissued as the original document. + + The limited permissions granted above are perpetual and will not be + revoked. + + This document and the information contained herein is provided on an + "AS IS" basis and THE AUTHORS DISCLAIMS ALL WARRANTIES, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF + THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + + +6. References + + [CDDB] Compact Disc Data Base + + <url:http://www.cddb.com> + + [ID3v2.3.0] Martin Nilsson, "ID3v2 informal standard". + + <url:http://www.id3.org/id3v2.3.0.txt> + + [ID3v2-strct] Martin Nilsson, + "ID3 tag version 2.4.0 - Main Structure" + + <url:http//www.id3.org/id3v2.4.0-structure.txt> + + [ISO-639-2] ISO/FDIS 639-2. + Codes for the representation of names of languages, Part 2: Alpha-3 + code. Technical committee / subcommittee: TC 37 / SC 2 + + [ISO-4217] ISO 4217:1995. + Codes for the representation of currencies and funds. + Technical committee / subcommittee: TC 68 + + [ISO-8859-1] ISO/IEC DIS 8859-1. + 8-bit single-byte coded graphic character sets, Part 1: Latin + alphabet No. 1. Technical committee / subcommittee: JTC 1 / SC 2 + + [ISRC] ISO 3901:1986 + International Standard Recording Code (ISRC). + Technical committee / subcommittee: TC 46 / SC 9 + + [JFIF] JPEG File Interchange Format, version 1.02 + + <url:http://www.w3.org/Graphics/JPEG/jfif.txt> + + [KEYWORDS] S. Bradner, 'Key words for use in RFCs to Indicate + Requirement Levels', RFC 2119, March 1997. + + <url:ftp://ftp.isi.edu/in-notes/rfc2119.txt> + + [MIME] Freed, N. and N. Borenstein, "Multipurpose Internet Mail + Extensions (MIME) Part One: Format of Internet Message Bodies", + RFC 2045, November 1996. + + <url:ftp://ftp.isi.edu/in-notes/rfc2045.txt> + + [MPEG] ISO/IEC 11172-3:1993. + Coding of moving pictures and associated audio for digital storage + media at up to about 1,5 Mbit/s, Part 3: Audio. + Technical committee / subcommittee: JTC 1 / SC 29 + and + ISO/IEC 13818-3:1995 + Generic coding of moving pictures and associated audio information, + Part 3: Audio. + Technical committee / subcommittee: JTC 1 / SC 29 + and + ISO/IEC DIS 13818-3 + Generic coding of moving pictures and associated audio information, + Part 3: Audio (Revision of ISO/IEC 13818-3:1995) + + + [PNG] Portable Network Graphics, version 1.0 + + <url:http://www.w3.org/TR/REC-png-multi.html> + + [URL] T. Berners-Lee, L. Masinter & M. McCahill, "Uniform Resource + Locators (URL).", RFC 1738, December 1994. + + <url:ftp://ftp.isi.edu/in-notes/rfc1738.txt> + + [ZLIB] P. Deutsch, Aladdin Enterprises & J-L. Gailly, "ZLIB + Compressed + Data Format Specification version 3.3", RFC 1950, May 1996. + + <url:ftp://ftp.isi.edu/in-notes/rfc1950.txt> + + +7. Appendix + + +A. Appendix A - Genre List from ID3v1 + + The following genres is defined in ID3v1 + + 0.Blues + 1.Classic Rock + 2.Country + 3.Dance + 4.Disco + 5.Funk + 6.Grunge + 7.Hip-Hop + 8.Jazz + 9.Metal + 10.New Age + 11.Oldies + 12.Other + 13.Pop + 14.R&B + 15.Rap + 16.Reggae + 17.Rock + 18.Techno + 19.Industrial + 20.Alternative + 21.Ska + 22.Death Metal + 23.Pranks + 24.Soundtrack + 25.Euro-Techno + 26.Ambient + 27.Trip-Hop + 28.Vocal + 29.Jazz+Funk + 30.Fusion + 31.Trance + 32.Classical + 33.Instrumental + 34.Acid + 35.House + 36.Game + 37.Sound Clip + 38.Gospel + 39.Noise + 40.AlternRock + 41.Bass + 42.Soul + 43.Punk + 44.Space + 45.Meditative + 46.Instrumental Pop + 47.Instrumental Rock + 48.Ethnic + 49.Gothic + 50.Darkwave + 51.Techno-Industrial + 52.Electronic + 53.Pop-Folk + 54.Eurodance + 55.Dream + 56.Southern Rock + 57.Comedy + 58.Cult + 59.Gangsta + 60.Top 40 + 61.Christian Rap + 62.Pop/Funk + 63.Jungle + 64.Native American + 65.Cabaret + 66.New Wave + 67.Psychadelic + 68.Rave + 69.Showtunes + 70.Trailer + 71.Lo-Fi + 72.Tribal + 73.Acid Punk + 74.Acid Jazz + 75.Polka + 76.Retro + 77.Musical + 78.Rock & Roll + 79.Hard Rock + + +8. Author's Address + + Written by + + Martin Nilsson + Rydsvägen 246 C. 30 + SE-584 34 Linköping + Sweden + + Email: nilsson@id3.org diff --git a/taglib/taglib/mpeg/id3v2/id3v2.4.0-structure.txt b/taglib/taglib/mpeg/id3v2/id3v2.4.0-structure.txt new file mode 100644 index 0000000..5fa156a --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2.4.0-structure.txt @@ -0,0 +1,733 @@ + +Informal standard M. Nilsson +Document: id3v2.4.0-structure.txt 16 September 2001 + + + ID3 tag version 2.4.0 - Main Structure + +Status of this document + + This document is an informal standard and replaces the ID3v2.3.0 + standard [ID3v2]. A formal standard will use another revision number + even if the content is identical to document. The contents in this + document may change for clarifications but never for added or altered + functionallity. + + Distribution of this document is unlimited. + + +Abstract + + This document describes the main structure of ID3v2.4.0, which is a + revised version of the ID3v2 informal standard [ID3v2] version + 2.3.0. The ID3v2 offers a flexible way of storing audio meta + information within the audio file itself. The information may be + technical information, such as equalisation curves, as well as + title, performer, copyright etc. + + ID3v2.4.0 is meant to be as close as possible to ID3v2.3.0 in order + to allow for implementations to be revised as easily as possible. + + +1. Table of contents + + Status of this document + Abstract + 1. Table of contents + 2. Conventions in this document + 2. Standard overview + 3. ID3v2 overview + 3.1. ID3v2 header + 3.2. ID3v2 extended header + 3.3. Padding + 3.4. ID3v2 footer + 4. ID3v2 frames overview + 4.1. Frame header flags + 4.1.1. Frame status flags + 4.1.2. Frame format flags + 5. Tag location + 6. Unsynchronisation + 6.1. The unsynchronisation scheme + 6.2. Synchsafe integers + 7. Copyright + 8. References + 9. Author's Address + + +2. Conventions in this document + + Text within "" is a text string exactly as it appears in a tag. + Numbers preceded with $ are hexadecimal and numbers preceded with % + are binary. $xx is used to indicate a byte with unknown content. %x + is used to indicate a bit with unknown content. The most significant + bit (MSB) of a byte is called 'bit 7' and the least significant bit + (LSB) is called 'bit 0'. + + A tag is the whole tag described in this document. A frame is a block + of information in the tag. The tag consists of a header, frames and + optional padding. A field is a piece of information; one value, a + string etc. A numeric string is a string that consists of the + characters "0123456789" only. + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in RFC 2119 [KEYWORDS]. + + +3. ID3v2 overview + + ID3v2 is a general tagging format for audio, which makes it possible + to store meta data about the audio inside the audio file itself. The + ID3 tag described in this document is mainly targeted at files + encoded with MPEG-1/2 layer I, MPEG-1/2 layer II, MPEG-1/2 layer III + and MPEG-2.5, but may work with other types of encoded audio or as a + stand alone format for audio meta data. + + ID3v2 is designed to be as flexible and expandable as possible to + meet new meta information needs that might arise. To achieve that + ID3v2 is constructed as a container for several information blocks, + called frames, whose format need not be known to the software that + encounters them. At the start of every frame is an unique and + predefined identifier, a size descriptor that allows software to skip + unknown frames and a flags field. The flags describes encoding + details and if the frame should remain in the tag, should it be + unknown to the software, if the file is altered. + + The bitorder in ID3v2 is most significant bit first (MSB). The + byteorder in multibyte numbers is most significant byte first (e.g. + $12345678 would be encoded $12 34 56 78), also known as big endian + and network byte order. + + Overall tag structure: + + +-----------------------------+ + | Header (10 bytes) | + +-----------------------------+ + | Extended Header | + | (variable length, OPTIONAL) | + +-----------------------------+ + | Frames (variable length) | + +-----------------------------+ + | Padding | + | (variable length, OPTIONAL) | + +-----------------------------+ + | Footer (10 bytes, OPTIONAL) | + +-----------------------------+ + + In general, padding and footer are mutually exclusive. See details in + sections 3.3, 3.4 and 5. + + +3.1. ID3v2 header + + The first part of the ID3v2 tag is the 10 byte tag header, laid out + as follows: + + ID3v2/file identifier "ID3" + ID3v2 version $04 00 + ID3v2 flags %abcd0000 + ID3v2 size 4 * %0xxxxxxx + + The first three bytes of the tag are always "ID3", to indicate that + this is an ID3v2 tag, directly followed by the two version bytes. The + first byte of ID3v2 version is its major version, while the second + byte is its revision number. In this case this is ID3v2.4.0. All + revisions are backwards compatible while major versions are not. If + software with ID3v2.4.0 and below support should encounter version + five or higher it should simply ignore the whole tag. Version or + revision will never be $FF. + + The version is followed by the ID3v2 flags field, of which currently + four flags are used. + + + a - Unsynchronisation + + Bit 7 in the 'ID3v2 flags' indicates whether or not + unsynchronisation is applied on all frames (see section 6.1 for + details); a set bit indicates usage. + + + b - Extended header + + The second bit (bit 6) indicates whether or not the header is + followed by an extended header. The extended header is described in + section 3.2. A set bit indicates the presence of an extended + header. + + + c - Experimental indicator + + The third bit (bit 5) is used as an 'experimental indicator'. This + flag SHALL always be set when the tag is in an experimental stage. + + + d - Footer present + + Bit 4 indicates that a footer (section 3.4) is present at the very + end of the tag. A set bit indicates the presence of a footer. + + + All the other flags MUST be cleared. If one of these undefined flags + are set, the tag might not be readable for a parser that does not + know the flags function. + + The ID3v2 tag size is stored as a 32 bit synchsafe integer (section + 6.2), making a total of 28 effective bits (representing up to 256MB). + + The ID3v2 tag size is the sum of the byte length of the extended + header, the padding and the frames after unsynchronisation. If a + footer is present this equals to ('total size' - 20) bytes, otherwise + ('total size' - 10) bytes. + + An ID3v2 tag can be detected with the following pattern: + $49 44 33 yy yy xx zz zz zz zz + Where yy is less than $FF, xx is the 'flags' byte and zz is less than + $80. + + +3.2. Extended header + + The extended header contains information that can provide further + insight in the structure of the tag, but is not vital to the correct + parsing of the tag information; hence the extended header is + optional. + + Extended header size 4 * %0xxxxxxx + Number of flag bytes $01 + Extended Flags $xx + + Where the 'Extended header size' is the size of the whole extended + header, stored as a 32 bit synchsafe integer. An extended header can + thus never have a size of fewer than six bytes. + + The extended flags field, with its size described by 'number of flag + bytes', is defined as: + + %0bcd0000 + + Each flag that is set in the extended header has data attached, which + comes in the order in which the flags are encountered (i.e. the data + for flag 'b' comes before the data for flag 'c'). Unset flags cannot + have any attached data. All unknown flags MUST be unset and their + corresponding data removed when a tag is modified. + + Every set flag's data starts with a length byte, which contains a + value between 0 and 127 ($00 - $7f), followed by data that has the + field length indicated by the length byte. If a flag has no attached + data, the value $00 is used as length byte. + + + b - Tag is an update + + If this flag is set, the present tag is an update of a tag found + earlier in the present file or stream. If frames defined as unique + are found in the present tag, they are to override any + corresponding ones found in the earlier tag. This flag has no + corresponding data. + + Flag data length $00 + + c - CRC data present + + If this flag is set, a CRC-32 [ISO-3309] data is included in the + extended header. The CRC is calculated on all the data between the + header and footer as indicated by the header's tag length field, + minus the extended header. Note that this includes the padding (if + there is any), but excludes the footer. The CRC-32 is stored as an + 35 bit synchsafe integer, leaving the upper four bits always + zeroed. + + Flag data length $05 + Total frame CRC 5 * %0xxxxxxx + + d - Tag restrictions + + For some applications it might be desired to restrict a tag in more + ways than imposed by the ID3v2 specification. Note that the + presence of these restrictions does not affect how the tag is + decoded, merely how it was restricted before encoding. If this flag + is set the tag is restricted as follows: + + Flag data length $01 + Restrictions %ppqrrstt + + p - Tag size restrictions + + 00 No more than 128 frames and 1 MB total tag size. + 01 No more than 64 frames and 128 KB total tag size. + 10 No more than 32 frames and 40 KB total tag size. + 11 No more than 32 frames and 4 KB total tag size. + + q - Text encoding restrictions + + 0 No restrictions + 1 Strings are only encoded with ISO-8859-1 [ISO-8859-1] or + UTF-8 [UTF-8]. + + r - Text fields size restrictions + + 00 No restrictions + 01 No string is longer than 1024 characters. + 10 No string is longer than 128 characters. + 11 No string is longer than 30 characters. + + Note that nothing is said about how many bytes is used to + represent those characters, since it is encoding dependent. If a + text frame consists of more than one string, the sum of the + strungs is restricted as stated. + + s - Image encoding restrictions + + 0 No restrictions + 1 Images are encoded only with PNG [PNG] or JPEG [JFIF]. + + t - Image size restrictions + + 00 No restrictions + 01 All images are 256x256 pixels or smaller. + 10 All images are 64x64 pixels or smaller. + 11 All images are exactly 64x64 pixels, unless required + otherwise. + + +3.3. Padding + + It is OPTIONAL to include padding after the final frame (at the end + of the ID3 tag), making the size of all the frames together smaller + than the size given in the tag header. A possible purpose of this + padding is to allow for adding a few additional frames or enlarge + existing frames within the tag without having to rewrite the entire + file. The value of the padding bytes must be $00. A tag MUST NOT have + any padding between the frames or between the tag header and the + frames. Furthermore it MUST NOT have any padding when a tag footer is + added to the tag. + + +3.4. ID3v2 footer + + To speed up the process of locating an ID3v2 tag when searching from + the end of a file, a footer can be added to the tag. It is REQUIRED + to add a footer to an appended tag, i.e. a tag located after all + audio data. The footer is a copy of the header, but with a different + identifier. + + ID3v2 identifier "3DI" + ID3v2 version $04 00 + ID3v2 flags %abcd0000 + ID3v2 size 4 * %0xxxxxxx + + +4. ID3v2 frame overview + + All ID3v2 frames consists of one frame header followed by one or more + fields containing the actual information. The header is always 10 + bytes and laid out as follows: + + Frame ID $xx xx xx xx (four characters) + Size 4 * %0xxxxxxx + Flags $xx xx + + The frame ID is made out of the characters capital A-Z and 0-9. + Identifiers beginning with "X", "Y" and "Z" are for experimental + frames and free for everyone to use, without the need to set the + experimental bit in the tag header. Bear in mind that someone else + might have used the same identifier as you. All other identifiers are + either used or reserved for future use. + + The frame ID is followed by a size descriptor containing the size of + the data in the final frame, after encryption, compression and + unsynchronisation. The size is excluding the frame header ('total + frame size' - 10 bytes) and stored as a 32 bit synchsafe integer. + + In the frame header the size descriptor is followed by two flag + bytes. These flags are described in section 4.1. + + There is no fixed order of the frames' appearance in the tag, + although it is desired that the frames are arranged in order of + significance concerning the recognition of the file. An example of + such order: UFID, TIT2, MCDI, TRCK ... + + A tag MUST contain at least one frame. A frame must be at least 1 + byte big, excluding the header. + + If nothing else is said, strings, including numeric strings and URLs + [URL], are represented as ISO-8859-1 [ISO-8859-1] characters in the + range $20 - $FF. Such strings are represented in frame descriptions + as <text string>, or <full text string> if newlines are allowed. If + nothing else is said newline character is forbidden. In ISO-8859-1 a + newline is represented, when allowed, with $0A only. + + Frames that allow different types of text encoding contains a text + encoding description byte. Possible encodings: + + $00 ISO-8859-1 [ISO-8859-1]. Terminated with $00. + $01 UTF-16 [UTF-16] encoded Unicode [UNICODE] with BOM. All + strings in the same frame SHALL have the same byteorder. + Terminated with $00 00. + $02 UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM. + Terminated with $00 00. + $03 UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00. + + Strings dependent on encoding are represented in frame descriptions + as <text string according to encoding>, or <full text string + according to encoding> if newlines are allowed. Any empty strings of + type $01 which are NULL-terminated may have the Unicode BOM followed + by a Unicode NULL ($FF FE 00 00 or $FE FF 00 00). + + The timestamp fields are based on a subset of ISO 8601. When being as + precise as possible the format of a time string is + yyyy-MM-ddTHH:mm:ss (year, "-", month, "-", day, "T", hour (out of + 24), ":", minutes, ":", seconds), but the precision may be reduced by + removing as many time indicators as wanted. Hence valid timestamps + are + yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm and + yyyy-MM-ddTHH:mm:ss. All time stamps are UTC. For durations, use + the slash character as described in 8601, and for multiple non- + contiguous dates, use multiple strings, if allowed by the frame + definition. + + The three byte language field, present in several frames, is used to + describe the language of the frame's content, according to ISO-639-2 + [ISO-639-2]. The language should be represented in lower case. If the + language is not known the string "XXX" should be used. + + All URLs [URL] MAY be relative, e.g. "picture.png", "../doc.txt". + + If a frame is longer than it should be, e.g. having more fields than + specified in this document, that indicates that additions to the + frame have been made in a later version of the ID3v2 standard. This + is reflected by the revision number in the header of the tag. + + +4.1. Frame header flags + + In the frame header the size descriptor is followed by two flag + bytes. All unused flags MUST be cleared. The first byte is for + 'status messages' and the second byte is a format description. If an + unknown flag is set in the first byte the frame MUST NOT be changed + without that bit cleared. If an unknown flag is set in the second + byte the frame is likely to not be readable. Some flags in the second + byte indicates that extra information is added to the header. These + fields of extra information is ordered as the flags that indicates + them. The flags field is defined as follows (l and o left out because + ther resemblence to one and zero): + + %0abc0000 %0h00kmnp + + Some frame format flags indicate that additional information fields + are added to the frame. This information is added after the frame + header and before the frame data in the same order as the flags that + indicates them. I.e. the four bytes of decompressed size will precede + the encryption method byte. These additions affects the 'frame size' + field, but are not subject to encryption or compression. + + The default status flags setting for a frame is, unless stated + otherwise, 'preserved if tag is altered' and 'preserved if file is + altered', i.e. %00000000. + + +4.1.1. Frame status flags + + a - Tag alter preservation + + This flag tells the tag parser what to do with this frame if it is + unknown and the tag is altered in any way. This applies to all + kinds of alterations, including adding more padding and reordering + the frames. + + 0 Frame should be preserved. + 1 Frame should be discarded. + + + b - File alter preservation + + This flag tells the tag parser what to do with this frame if it is + unknown and the file, excluding the tag, is altered. This does not + apply when the audio is completely replaced with other audio data. + + 0 Frame should be preserved. + 1 Frame should be discarded. + + + c - Read only + + This flag, if set, tells the software that the contents of this + frame are intended to be read only. Changing the contents might + break something, e.g. a signature. If the contents are changed, + without knowledge of why the frame was flagged read only and + without taking the proper means to compensate, e.g. recalculating + the signature, the bit MUST be cleared. + + +4.1.2. Frame format flags + + h - Grouping identity + + This flag indicates whether or not this frame belongs in a group + with other frames. If set, a group identifier byte is added to the + frame. Every frame with the same group identifier belongs to the + same group. + + 0 Frame does not contain group information + 1 Frame contains group information + + + k - Compression + + This flag indicates whether or not the frame is compressed. + A 'Data Length Indicator' byte MUST be included in the frame. + + 0 Frame is not compressed. + 1 Frame is compressed using zlib [zlib] deflate method. + If set, this requires the 'Data Length Indicator' bit + to be set as well. + + + m - Encryption + + This flag indicates whether or not the frame is encrypted. If set, + one byte indicating with which method it was encrypted will be + added to the frame. See description of the ENCR frame for more + information about encryption method registration. Encryption + should be done after compression. Whether or not setting this flag + requires the presence of a 'Data Length Indicator' depends on the + specific algorithm used. + + 0 Frame is not encrypted. + 1 Frame is encrypted. + + n - Unsynchronisation + + This flag indicates whether or not unsynchronisation was applied + to this frame. See section 6 for details on unsynchronisation. + If this flag is set all data from the end of this header to the + end of this frame has been unsynchronised. Although desirable, the + presence of a 'Data Length Indicator' is not made mandatory by + unsynchronisation. + + 0 Frame has not been unsynchronised. + 1 Frame has been unsyrchronised. + + p - Data length indicator + + This flag indicates that a data length indicator has been added to + the frame. The data length indicator is the value one would write + as the 'Frame length' if all of the frame format flags were + zeroed, represented as a 32 bit synchsafe integer. + + 0 There is no Data Length Indicator. + 1 A data length Indicator has been added to the frame. + + +5. Tag location + + The default location of an ID3v2 tag is prepended to the audio so + that players can benefit from the information when the data is + streamed. It is however possible to append the tag, or make a + prepend/append combination. When deciding upon where an unembedded + tag should be located, the following order of preference SHOULD be + considered. + + 1. Prepend the tag. + + 2. Prepend a tag with all vital information and add a second tag at + the end of the file, before tags from other tagging systems. The + first tag is required to have a SEEK frame. + + 3. Add a tag at the end of the file, before tags from other tagging + systems. + + In case 2 and 3 the tag can simply be appended if no other known tags + are present. The suggested method to find ID3v2 tags are: + + 1. Look for a prepended tag using the pattern found in section 3.1. + + 2. If a SEEK frame was found, use its values to guide further + searching. + + 3. Look for a tag footer, scanning from the back of the file. + + For every new tag that is found, the old tag should be discarded + unless the update flag in the extended header (section 3.2) is set. + + +6. Unsynchronisation + + The only purpose of unsynchronisation is to make the ID3v2 tag as + compatible as possible with existing software and hardware. There is + no use in 'unsynchronising' tags if the file is only to be processed + only by ID3v2 aware software and hardware. Unsynchronisation is only + useful with tags in MPEG 1/2 layer I, II and III, MPEG 2.5 and AAC + files. + + +6.1. The unsynchronisation scheme + + Whenever a false synchronisation is found within the tag, one zeroed + byte is inserted after the first false synchronisation byte. The + format of synchronisations that should be altered by ID3 encoders is + as follows: + + %11111111 111xxxxx + + and should be replaced with: + + %11111111 00000000 111xxxxx + + This has the side effect that all $FF 00 combinations have to be + altered, so they will not be affected by the decoding process. + Therefore all the $FF 00 combinations have to be replaced with the + $FF 00 00 combination during the unsynchronisation. + + To indicate usage of the unsynchronisation, the unsynchronisation + flag in the frame header should be set. This bit MUST be set if the + frame was altered by the unsynchronisation and SHOULD NOT be set if + unaltered. If all frames in the tag are unsynchronised the + unsynchronisation flag in the tag header SHOULD be set. It MUST NOT + be set if the tag has a frame which is not unsynchronised. + + Assume the first byte of the audio to be $FF. The special case when + the last byte of the last frame is $FF and no padding nor footer is + used will then introduce a false synchronisation. This can be solved + by adding a footer, adding padding or unsynchronising the frame and + add $00 to the end of the frame data, thus adding more byte to the + frame size than a normal unsynchronisation would. Although not + preferred, it is allowed to apply the last method on all frames + ending with $FF. + + It is preferred that the tag is either completely unsynchronised or + not unsynchronised at all. A completely unsynchronised tag has no + false synchonisations in it, as defined above, and does not end with + $FF. A completely non-unsynchronised tag contains no unsynchronised + frames, and thus the unsynchronisation flag in the header is cleared. + + Do bear in mind, that if compression or encryption is used, the + unsynchronisation scheme MUST be applied afterwards. When decoding an + unsynchronised frame, the unsynchronisation scheme MUST be reversed + first, encryption and decompression afterwards. + + +6.2. Synchsafe integers + + In some parts of the tag it is inconvenient to use the + unsychronisation scheme because the size of unsynchronised data is + not known in advance, which is particularly problematic with size + descriptors. The solution in ID3v2 is to use synchsafe integers, in + which there can never be any false synchs. Synchsafe integers are + integers that keep its highest bit (bit 7) zeroed, making seven bits + out of eight available. Thus a 32 bit synchsafe integer can store 28 + bits of information. + + Example: + + 255 (%11111111) encoded as a 16 bit synchsafe integer is 383 + (%00000001 01111111). + + +7. Copyright + + Copyright (C) Martin Nilsson 2000. All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that a reference to this document is included on all + such copies and derivative works. However, this document itself may + not be modified in any way and reissued as the original document. + + The limited permissions granted above are perpetual and will not be + revoked. + + This document and the information contained herein is provided on an + 'AS IS' basis and THE AUTHORS DISCLAIMS ALL WARRANTIES, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF + THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + + +8. References + + [ID3v2] Martin Nilsson, 'ID3v2 informal standard'. + + <url:http://www.id3.org/id3v2.3.0.txt> + + [ISO-639-2] ISO/FDIS 639-2. + 'Codes for the representation of names of languages, Part 2: Alpha-3 + code.' Technical committee / subcommittee: TC 37 / SC 2 + + [ISO-3309] ISO 3309 + 'Information Processing Systems--Data Communication High-Level Data + Link Control Procedure--Frame Structure', IS 3309, October 1984, 3rd + Edition. + + [ISO-8859-1] ISO/IEC DIS 8859-1. + '8-bit single-byte coded graphic character sets, Part 1: Latin + alphabet No. 1.' Technical committee / subcommittee: JTC 1 / SC 2 + + [JFIF] 'JPEG File Interchange Format, version 1.02' + + <url:http://www.w3.org/Graphics/JPEG/jfif.txt> + + [KEYWORDS] S. Bradner, 'Key words for use in RFCs to Indicate + Requirement Levels', RFC 2119, March 1997. + + <url:ftp://ftp.isi.edu/in-notes/rfc2119.txt> + + [MPEG] ISO/IEC 11172-3:1993. + 'Coding of moving pictures and associated audio for digital storage + media at up to about 1,5 Mbit/s, Part 3: Audio.' + Technical committee / subcommittee: JTC 1 / SC 29 + and + ISO/IEC 13818-3:1995 + 'Generic coding of moving pictures and associated audio information, + Part 3: Audio.' + Technical committee / subcommittee: JTC 1 / SC 29 + and + ISO/IEC DIS 13818-3 + 'Generic coding of moving pictures and associated audio information, + Part 3: Audio (Revision of ISO/IEC 13818-3:1995)' + + [PNG] 'Portable Network Graphics, version 1.0' + + <url:http://www.w3.org/TR/REC-png-multi.html> + + [UNICODE] The Unicode Consortium, + 'The Unicode Standard Version 3.0', ISBN 0-201-61633-5. + + <url:http://www.unicode.org/unicode/standard/versions/Unicode3.0.htm> + + [URL] T. Berners-Lee, L. Masinter & M. McCahill, 'Uniform Resource + Locators (URL)', RFC 1738, December 1994. + + <url:ftp://ftp.isi.edu/in-notes/rfc1738.txt> + + [UTF-8] F. Yergeau, 'UTF-8, a transformation format of ISO 10646', + RFC 2279, January 1998. + + <url:ftp://ftp.isi.edu/in-notes/rfc2279.txt> + + [UTF-16] F. Yergeau, 'UTF-16, an encoding of ISO 10646', RFC 2781, + February 2000. + + <url:ftp://ftp.isi.edu/in-notes/rfc2781.txt> + + [ZLIB] P. Deutsch, Aladdin Enterprises & J-L. Gailly, 'ZLIB + Compressed Data Format Specification version 3.3', RFC 1950, + May 1996. + + <url:ftp://ftp.isi.edu/in-notes/rfc1950.txt> + + +9. Author's Address + + Written by + + Martin Nilsson + Rydsvägen 246 C. 30 + SE-584 34 Linköping + Sweden + + Email: nilsson@id3.org + diff --git a/taglib/taglib/mpeg/id3v2/id3v2extendedheader.cpp b/taglib/taglib/mpeg/id3v2/id3v2extendedheader.cpp new file mode 100644 index 0000000..ddcfd45 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2extendedheader.cpp @@ -0,0 +1,71 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "id3v2extendedheader.h" +#include "id3v2synchdata.h" + +using namespace TagLib; +using namespace ID3v2; + +class ExtendedHeader::ExtendedHeaderPrivate +{ +public: + ExtendedHeaderPrivate() : size(0) {} + + uint size; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +ExtendedHeader::ExtendedHeader() +{ + d = new ExtendedHeaderPrivate(); +} + +ExtendedHeader::~ExtendedHeader() +{ + delete d; +} + +TagLib::uint ExtendedHeader::size() const +{ + return d->size; +} + +void ExtendedHeader::setData(const ByteVector &data) +{ + parse(data); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void ExtendedHeader::parse(const ByteVector &data) +{ + d->size = SynchData::toUInt(data.mid(0, 4)); // (structure 3.2 "Extended header size") +} diff --git a/taglib/taglib/mpeg/id3v2/id3v2extendedheader.h b/taglib/taglib/mpeg/id3v2/id3v2extendedheader.h new file mode 100644 index 0000000..d7227e9 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2extendedheader.h @@ -0,0 +1,93 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V2EXTENDEDHEADER_H +#define TAGLIB_ID3V2EXTENDEDHEADER_H + +#include "taglib_export.h" +#include "tbytevector.h" +#include "taglib.h" + +namespace TagLib { + + namespace ID3v2 { + + //! ID3v2 extended header implementation + + /*! + * This class implements ID3v2 extended headers. It attempts to follow, + * both semantically and programatically, the structure specified in + * the ID3v2 standard. The API is based on the properties of ID3v2 extended + * headers specified there. If any of the terms used in this documentation + * are unclear please check the specification in the linked section. + * (Structure, <a href="id3v2-structure.html#3.2">3.2</a>) + */ + + class TAGLIB_EXPORT ExtendedHeader + { + public: + /*! + * Constructs an empty ID3v2 extended header. + */ + ExtendedHeader(); + + /*! + * Destroys the extended header. + */ + virtual ~ExtendedHeader(); + + /*! + * Returns the size of the extended header. This is variable for the + * extended header. + */ + uint size() const; + + /*! + * Sets the data that will be used as the extended header. Since the + * length is not known before the extended header has been parsed, this + * should just be a pointer to the first byte of the extended header. It + * will determine the length internally and make that available through + * size(). + */ + void setData(const ByteVector &data); + + protected: + /*! + * Called by setData() to parse the extended header data. It makes this + * information available through the public API. + */ + void parse(const ByteVector &data); + + private: + ExtendedHeader(const ExtendedHeader &); + ExtendedHeader &operator=(const ExtendedHeader &); + + class ExtendedHeaderPrivate; + ExtendedHeaderPrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/id3v2footer.cpp b/taglib/taglib/mpeg/id3v2/id3v2footer.cpp new file mode 100644 index 0000000..defbb17 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2footer.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "id3v2footer.h" +#include "id3v2header.h" + +using namespace TagLib; +using namespace ID3v2; + +class Footer::FooterPrivate +{ +public: + static const uint size = 10; +}; + +Footer::Footer() +{ + +} + +Footer::~Footer() +{ + +} + +TagLib::uint Footer::size() +{ + return FooterPrivate::size; +} + +ByteVector Footer::render(const Header *header) const +{ + ByteVector headerData = header->render(); + headerData[0] = '3'; + headerData[1] = 'D'; + headerData[2] = 'I'; + return headerData; +} diff --git a/taglib/taglib/mpeg/id3v2/id3v2footer.h b/taglib/taglib/mpeg/id3v2/id3v2footer.h new file mode 100644 index 0000000..1374a14 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2footer.h @@ -0,0 +1,82 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V2FOOTER_H +#define TAGLIB_ID3V2FOOTER_H + +#include "taglib_export.h" +#include "tbytevector.h" + +namespace TagLib { + + namespace ID3v2 { + + class Header; + + //! ID3v2 footer implementation + + /*! + * Per the ID3v2 specification, the tag's footer is just a copy of the + * information in the header. As such there is no API for reading the + * data from the header, it can just as easily be done from the header. + * + * In fact, at this point, TagLib does not even parse the footer since + * it is not useful internally. However, if the flag to include a footer + * has been set in the ID3v2::Tag, TagLib will render a footer. + */ + + class TAGLIB_EXPORT Footer + { + public: + /*! + * Constructs an empty ID3v2 footer. + */ + Footer(); + /*! + * Destroys the footer. + */ + virtual ~Footer(); + + /*! + * Returns the size of the footer. Presently this is always 10 bytes. + */ + static uint size(); + + /*! + * Renders the footer based on the data in \a header. + */ + ByteVector render(const Header *header) const; + + private: + Footer(const Footer &); + Footer &operator=(const Footer &); + + class FooterPrivate; + FooterPrivate *d; + }; + + } +} +#endif diff --git a/taglib/taglib/mpeg/id3v2/id3v2frame.cpp b/taglib/taglib/mpeg/id3v2/id3v2frame.cpp new file mode 100644 index 0000000..3cafcff --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2frame.cpp @@ -0,0 +1,844 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#if HAVE_ZLIB +#include <zlib.h> +#endif + +#include <bitset> + +#include <tdebug.h> +#include <tstringlist.h> + +#include "id3v2tag.h" +#include "id3v2frame.h" +#include "id3v2synchdata.h" +#include "tpropertymap.h" +#include "frames/textidentificationframe.h" +#include "frames/urllinkframe.h" +#include "frames/unsynchronizedlyricsframe.h" +#include "frames/commentsframe.h" +#include "frames/uniquefileidentifierframe.h" +#include "frames/unknownframe.h" + +using namespace TagLib; +using namespace ID3v2; + +class Frame::FramePrivate +{ +public: + FramePrivate() : + header(0) + {} + + ~FramePrivate() + { + delete header; + } + + Frame::Header *header; +}; + +namespace +{ + bool isValidFrameID(const ByteVector &frameID) + { + if(frameID.size() != 4) + return false; + + for(ByteVector::ConstIterator it = frameID.begin(); it != frameID.end(); it++) { + if( (*it < 'A' || *it > 'Z') && (*it < '0' || *it > '9') ) { + return false; + } + } + return true; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// static methods +//////////////////////////////////////////////////////////////////////////////// + +TagLib::uint Frame::headerSize() +{ + return Header::size(); +} + +TagLib::uint Frame::headerSize(uint version) +{ + return Header::size(version); +} + +ByteVector Frame::textDelimiter(String::Type t) +{ + ByteVector d = char(0); + if(t == String::UTF16 || t == String::UTF16BE || t == String::UTF16LE) + d.append(char(0)); + return d; +} + +const String Frame::instrumentPrefix("PERFORMER:"); +const String Frame::commentPrefix("COMMENT:"); +const String Frame::lyricsPrefix("LYRICS:"); +const String Frame::urlPrefix("URL:"); + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Frame *Frame::createTextualFrame(const String &key, const StringList &values) //static +{ + // check if the key is contained in the key<=>frameID mapping + ByteVector frameID = keyToFrameID(key); + if(!frameID.isNull()) { + if(frameID[0] == 'T'){ // text frame + TextIdentificationFrame *frame = new TextIdentificationFrame(frameID, String::UTF8); + frame->setText(values); + return frame; + } else if((frameID[0] == 'W') && (values.size() == 1)){ // URL frame (not WXXX); support only one value + UrlLinkFrame* frame = new UrlLinkFrame(frameID); + frame->setUrl(values.front()); + return frame; + } + } + if(key == "MUSICBRAINZ_TRACKID" && values.size() == 1) { + UniqueFileIdentifierFrame *frame = new UniqueFileIdentifierFrame("http://musicbrainz.org", values.front().data(String::UTF8)); + return frame; + } + // now we check if it's one of the "special" cases: + // -LYRICS: depending on the number of values, use USLT or TXXX (with description=LYRICS) + if((key == "LYRICS" || key.startsWith(lyricsPrefix)) && values.size() == 1){ + UnsynchronizedLyricsFrame *frame = new UnsynchronizedLyricsFrame(String::UTF8); + frame->setDescription(key == "LYRICS" ? key : key.substr(lyricsPrefix.size())); + frame->setText(values.front()); + return frame; + } + // -URL: depending on the number of values, use WXXX or TXXX (with description=URL) + if((key == "URL" || key.startsWith(urlPrefix)) && values.size() == 1){ + UserUrlLinkFrame *frame = new UserUrlLinkFrame(String::UTF8); + frame->setDescription(key == "URL" ? key : key.substr(urlPrefix.size())); + frame->setUrl(values.front()); + return frame; + } + // -COMMENT: depending on the number of values, use COMM or TXXX (with description=COMMENT) + if((key == "COMMENT" || key.startsWith(commentPrefix)) && values.size() == 1){ + CommentsFrame *frame = new CommentsFrame(String::UTF8); + if (key != "COMMENT"){ + frame->setDescription(key.substr(commentPrefix.size())); + } + frame->setText(values.front()); + return frame; + } + // if non of the above cases apply, we use a TXXX frame with the key as description + return new UserTextIdentificationFrame(keyToTXXX(key), values, String::UTF8); +} + +Frame::~Frame() +{ + delete d; +} + +ByteVector Frame::frameID() const +{ + if(d->header) + return d->header->frameID(); + else + return ByteVector::null; +} + +TagLib::uint Frame::size() const +{ + if(d->header) + return d->header->frameSize(); + else + return 0; +} + +void Frame::setData(const ByteVector &data) +{ + parse(data); +} + +void Frame::setText(const String &) +{ + +} + +ByteVector Frame::render() const +{ + ByteVector fieldData = renderFields(); + d->header->setFrameSize(fieldData.size()); + ByteVector headerData = d->header->render(); + + return headerData + fieldData; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +Frame::Frame(const ByteVector &data) +{ + d = new FramePrivate; + d->header = new Header(data); +} + +Frame::Frame(Header *h) +{ + d = new FramePrivate; + d->header = h; +} + +Frame::Header *Frame::header() const +{ + return d->header; +} + +void Frame::setHeader(Header *h, bool deleteCurrent) +{ + if(deleteCurrent) + delete d->header; + + d->header = h; +} + +void Frame::parse(const ByteVector &data) +{ + if(d->header) + d->header->setData(data); + else + d->header = new Header(data); + + parseFields(fieldData(data)); +} + +ByteVector Frame::fieldData(const ByteVector &frameData) const +{ + uint headerSize = Header::size(d->header->version()); + + uint frameDataOffset = headerSize; + uint frameDataLength = size(); + + if(d->header->compression() || d->header->dataLengthIndicator()) { + frameDataLength = SynchData::toUInt(frameData.mid(headerSize, 4)); + frameDataOffset += 4; + } + +#if HAVE_ZLIB + if(d->header->compression() && + !d->header->encryption()) + { + ByteVector data(frameDataLength); + uLongf uLongTmp = frameDataLength; + ::uncompress((Bytef *) data.data(), + (uLongf *) &uLongTmp, + (Bytef *) frameData.data() + frameDataOffset, + size()); + return data; + } + else +#endif + return frameData.mid(frameDataOffset, frameDataLength); +} + +String Frame::readStringField(const ByteVector &data, String::Type encoding, int *position) +{ + int start = 0; + + if(!position) + position = &start; + + ByteVector delimiter = textDelimiter(encoding); + + int end = data.find(delimiter, *position, delimiter.size()); + + if(end < *position) + return String::null; + + String str; + if(encoding == String::Latin1) + str = Tag::latin1StringHandler()->parse(data.mid(*position, end - *position)); + else + str = String(data.mid(*position, end - *position), encoding); + + *position = end + delimiter.size(); + + return str; +} + +String::Type Frame::checkEncoding(const StringList &fields, String::Type encoding) // static +{ + return checkEncoding(fields, encoding, 4); +} + +String::Type Frame::checkEncoding(const StringList &fields, String::Type encoding, uint version) // static +{ + if((encoding == String::UTF8 || encoding == String::UTF16BE) && version != 4) + return String::UTF16; + + if(encoding != String::Latin1) + return encoding; + + for(StringList::ConstIterator it = fields.begin(); it != fields.end(); ++it) { + if(!(*it).isLatin1()) { + if(version == 4) { + debug("Frame::checkEncoding() -- Rendering using UTF8."); + return String::UTF8; + } + else { + debug("Frame::checkEncoding() -- Rendering using UTF16."); + return String::UTF16; + } + } + } + + return String::Latin1; +} + +String::Type Frame::checkTextEncoding(const StringList &fields, String::Type encoding) const +{ + return checkEncoding(fields, encoding, header()->version()); +} + +static const TagLib::uint frameTranslationSize = 51; +static const char *frameTranslation[][2] = { + // Text information frames + { "TALB", "ALBUM"}, + { "TBPM", "BPM" }, + { "TCOM", "COMPOSER" }, + { "TCON", "GENRE" }, + { "TCOP", "COPYRIGHT" }, + { "TDEN", "ENCODINGTIME" }, + { "TDLY", "PLAYLISTDELAY" }, + { "TDOR", "ORIGINALDATE" }, + { "TDRC", "DATE" }, + // { "TRDA", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4 + // { "TDAT", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4 + // { "TYER", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4 + // { "TIME", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4 + { "TDRL", "RELEASEDATE" }, + { "TDTG", "TAGGINGDATE" }, + { "TENC", "ENCODEDBY" }, + { "TEXT", "LYRICIST" }, + { "TFLT", "FILETYPE" }, + //{ "TIPL", "INVOLVEDPEOPLE" }, handled separately + { "TIT1", "CONTENTGROUP" }, + { "TIT2", "TITLE"}, + { "TIT3", "SUBTITLE" }, + { "TKEY", "INITIALKEY" }, + { "TLAN", "LANGUAGE" }, + { "TLEN", "LENGTH" }, + //{ "TMCL", "MUSICIANCREDITS" }, handled separately + { "TMED", "MEDIA" }, + { "TMOO", "MOOD" }, + { "TOAL", "ORIGINALALBUM" }, + { "TOFN", "ORIGINALFILENAME" }, + { "TOLY", "ORIGINALLYRICIST" }, + { "TOPE", "ORIGINALARTIST" }, + { "TOWN", "OWNER" }, + { "TPE1", "ARTIST"}, + { "TPE2", "ALBUMARTIST" }, // id3's spec says 'PERFORMER', but most programs use 'ALBUMARTIST' + { "TPE3", "CONDUCTOR" }, + { "TPE4", "REMIXER" }, // could also be ARRANGER + { "TPOS", "DISCNUMBER" }, + { "TPRO", "PRODUCEDNOTICE" }, + { "TPUB", "LABEL" }, + { "TRCK", "TRACKNUMBER" }, + { "TRSN", "RADIOSTATION" }, + { "TRSO", "RADIOSTATIONOWNER" }, + { "TSOA", "ALBUMSORT" }, + { "TSOP", "ARTISTSORT" }, + { "TSOT", "TITLESORT" }, + { "TSO2", "ALBUMARTISTSORT" }, // non-standard, used by iTunes + { "TSRC", "ISRC" }, + { "TSSE", "ENCODING" }, + // URL frames + { "WCOP", "COPYRIGHTURL" }, + { "WOAF", "FILEWEBPAGE" }, + { "WOAR", "ARTISTWEBPAGE" }, + { "WOAS", "AUDIOSOURCEWEBPAGE" }, + { "WORS", "RADIOSTATIONWEBPAGE" }, + { "WPAY", "PAYMENTWEBPAGE" }, + { "WPUB", "PUBLISHERWEBPAGE" }, + //{ "WXXX", "URL"}, handled specially + // Other frames + { "COMM", "COMMENT" }, + //{ "USLT", "LYRICS" }, handled specially +}; + +static const TagLib::uint txxxFrameTranslationSize = 8; +static const char *txxxFrameTranslation[][2] = { + { "MusicBrainz Album Id", "MUSICBRAINZ_ALBUMID" }, + { "MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID" }, + { "MusicBrainz Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID" }, + { "MusicBrainz Release Group Id", "MUSICBRAINZ_RELEASEGROUPID" }, + { "MusicBrainz Work Id", "MUSICBRAINZ_WORKID" }, + { "Acoustid Id", "ACOUSTID_ID" }, + { "Acoustid Fingerprint", "ACOUSTID_FINGERPRINT" }, + { "MusicIP PUID", "MUSICIP_PUID" }, +}; + +Map<ByteVector, String> &idMap() +{ + static Map<ByteVector, String> m; + if(m.isEmpty()) + for(size_t i = 0; i < frameTranslationSize; ++i) + m[frameTranslation[i][0]] = frameTranslation[i][1]; + return m; +} + +Map<String, String> &txxxMap() +{ + static Map<String, String> m; + if(m.isEmpty()) { + for(size_t i = 0; i < txxxFrameTranslationSize; ++i) { + String key = String(txxxFrameTranslation[i][0]).upper(); + m[key] = txxxFrameTranslation[i][1]; + } + } + return m; +} + +// list of deprecated frames and their successors +static const TagLib::uint deprecatedFramesSize = 4; +static const char *deprecatedFrames[][2] = { + {"TRDA", "TDRC"}, // 2.3 -> 2.4 (http://en.wikipedia.org/wiki/ID3) + {"TDAT", "TDRC"}, // 2.3 -> 2.4 + {"TYER", "TDRC"}, // 2.3 -> 2.4 + {"TIME", "TDRC"}, // 2.3 -> 2.4 +}; + +Map<ByteVector,ByteVector> &deprecationMap() +{ + static Map<ByteVector,ByteVector> depMap; + if(depMap.isEmpty()) + for(TagLib::uint i = 0; i < deprecatedFramesSize; ++i) + depMap[deprecatedFrames[i][0]] = deprecatedFrames[i][1]; + return depMap; +} + +String Frame::frameIDToKey(const ByteVector &id) +{ + Map<ByteVector, String> &m = idMap(); + if(m.contains(id)) + return m[id]; + if(deprecationMap().contains(id)) + return m[deprecationMap()[id]]; + return String::null; +} + +ByteVector Frame::keyToFrameID(const String &s) +{ + static Map<String, ByteVector> m; + if(m.isEmpty()) + for(size_t i = 0; i < frameTranslationSize; ++i) + m[frameTranslation[i][1]] = frameTranslation[i][0]; + if(m.contains(s.upper())) + return m[s]; + return ByteVector::null; +} + +String Frame::txxxToKey(const String &description) +{ + Map<String, String> &m = txxxMap(); + String d = description.upper(); + if(m.contains(d)) + return m[d]; + return d; +} + +String Frame::keyToTXXX(const String &s) +{ + static Map<String, String> m; + if(m.isEmpty()) + for(size_t i = 0; i < txxxFrameTranslationSize; ++i) + m[txxxFrameTranslation[i][1]] = txxxFrameTranslation[i][0]; + if(m.contains(s.upper())) + return m[s]; + return s; +} + +PropertyMap Frame::asProperties() const +{ + if(dynamic_cast< const UnknownFrame *>(this)) { + PropertyMap m; + m.unsupportedData().append("UNKNOWN/" + frameID()); + return m; + } + const ByteVector &id = frameID(); + // workaround until this function is virtual + if(id == "TXXX") + return dynamic_cast< const UserTextIdentificationFrame* >(this)->asProperties(); + else if(id[0] == 'T') + return dynamic_cast< const TextIdentificationFrame* >(this)->asProperties(); + else if(id == "WXXX") + return dynamic_cast< const UserUrlLinkFrame* >(this)->asProperties(); + else if(id[0] == 'W') + return dynamic_cast< const UrlLinkFrame* >(this)->asProperties(); + else if(id == "COMM") + return dynamic_cast< const CommentsFrame* >(this)->asProperties(); + else if(id == "USLT") + return dynamic_cast< const UnsynchronizedLyricsFrame* >(this)->asProperties(); + else if(id == "UFID") + return dynamic_cast< const UniqueFileIdentifierFrame* >(this)->asProperties(); + PropertyMap m; + m.unsupportedData().append(id); + return m; +} + +void Frame::splitProperties(const PropertyMap &original, PropertyMap &singleFrameProperties, + PropertyMap &tiplProperties, PropertyMap &tmclProperties) +{ + + singleFrameProperties.clear(); + tiplProperties.clear(); + tmclProperties.clear(); + for(PropertyMap::ConstIterator it = original.begin(); it != original.end(); ++it) { + if(TextIdentificationFrame::involvedPeopleMap().contains(it->first)) + tiplProperties.insert(it->first, it->second); + else if(it->first.startsWith(TextIdentificationFrame::instrumentPrefix)) + tmclProperties.insert(it->first, it->second); + else + singleFrameProperties.insert(it->first, it->second); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Frame::Header class +//////////////////////////////////////////////////////////////////////////////// + +class Frame::Header::HeaderPrivate +{ +public: + HeaderPrivate() : + frameSize(0), + version(4), + tagAlterPreservation(false), + fileAlterPreservation(false), + readOnly(false), + groupingIdentity(false), + compression(false), + encryption(false), + unsynchronisation(false), + dataLengthIndicator(false) + {} + + ByteVector frameID; + uint frameSize; + uint version; + + // flags + + bool tagAlterPreservation; + bool fileAlterPreservation; + bool readOnly; + bool groupingIdentity; + bool compression; + bool encryption; + bool unsynchronisation; + bool dataLengthIndicator; +}; + +//////////////////////////////////////////////////////////////////////////////// +// static members (Frame::Header) +//////////////////////////////////////////////////////////////////////////////// + +TagLib::uint Frame::Header::size() +{ + return size(4); +} + +TagLib::uint Frame::Header::size(uint version) +{ + switch(version) { + case 0: + case 1: + case 2: + return 6; + case 3: + case 4: + default: + return 10; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// public members (Frame::Header) +//////////////////////////////////////////////////////////////////////////////// + +Frame::Header::Header(const ByteVector &data, bool synchSafeInts) +{ + d = new HeaderPrivate; + setData(data, synchSafeInts); +} + +Frame::Header::Header(const ByteVector &data, uint version) +{ + d = new HeaderPrivate; + setData(data, version); +} + +Frame::Header::~Header() +{ + delete d; +} + +void Frame::Header::setData(const ByteVector &data, bool synchSafeInts) +{ + setData(data, uint(synchSafeInts ? 4 : 3)); +} + +void Frame::Header::setData(const ByteVector &data, uint version) +{ + d->version = version; + + switch(version) { + case 0: + case 1: + case 2: + { + // ID3v2.2 + + if(data.size() < 3) { + debug("You must at least specify a frame ID."); + return; + } + + // Set the frame ID -- the first three bytes + + d->frameID = data.mid(0, 3); + + // If the full header information was not passed in, do not continue to the + // steps to parse the frame size and flags. + + if(data.size() < 6) { + d->frameSize = 0; + return; + } + + d->frameSize = data.toUInt(3, 3, true); + + break; + } + case 3: + { + // ID3v2.3 + + if(data.size() < 4) { + debug("You must at least specify a frame ID."); + return; + } + + // Set the frame ID -- the first four bytes + + d->frameID = data.mid(0, 4); + + // If the full header information was not passed in, do not continue to the + // steps to parse the frame size and flags. + + if(data.size() < 10) { + d->frameSize = 0; + return; + } + + // Set the size -- the frame size is the four bytes starting at byte four in + // the frame header (structure 4) + + d->frameSize = data.toUInt(4U); + + { // read the first byte of flags + std::bitset<8> flags(data[8]); + d->tagAlterPreservation = flags[7]; // (structure 3.3.1.a) + d->fileAlterPreservation = flags[6]; // (structure 3.3.1.b) + d->readOnly = flags[5]; // (structure 3.3.1.c) + } + + { // read the second byte of flags + std::bitset<8> flags(data[9]); + d->compression = flags[7]; // (structure 3.3.1.i) + d->encryption = flags[6]; // (structure 3.3.1.j) + d->groupingIdentity = flags[5]; // (structure 3.3.1.k) + } + break; + } + case 4: + default: + { + // ID3v2.4 + + if(data.size() < 4) { + debug("You must at least specify a frame ID."); + return; + } + + // Set the frame ID -- the first four bytes + + d->frameID = data.mid(0, 4); + + // If the full header information was not passed in, do not continue to the + // steps to parse the frame size and flags. + + if(data.size() < 10) { + d->frameSize = 0; + return; + } + + // Set the size -- the frame size is the four bytes starting at byte four in + // the frame header (structure 4) + + d->frameSize = SynchData::toUInt(data.mid(4, 4)); +#ifndef NO_ITUNES_HACKS + // iTunes writes v2.4 tags with v2.3-like frame sizes + if(d->frameSize > 127) { + if(!isValidFrameID(data.mid(d->frameSize + 10, 4))) { + unsigned int uintSize = data.toUInt(4U); + if(isValidFrameID(data.mid(uintSize + 10, 4))) { + d->frameSize = uintSize; + } + } + } +#endif + + { // read the first byte of flags + std::bitset<8> flags(data[8]); + d->tagAlterPreservation = flags[6]; // (structure 4.1.1.a) + d->fileAlterPreservation = flags[5]; // (structure 4.1.1.b) + d->readOnly = flags[4]; // (structure 4.1.1.c) + } + + { // read the second byte of flags + std::bitset<8> flags(data[9]); + d->groupingIdentity = flags[6]; // (structure 4.1.2.h) + d->compression = flags[3]; // (structure 4.1.2.k) + d->encryption = flags[2]; // (structure 4.1.2.m) + d->unsynchronisation = flags[1]; // (structure 4.1.2.n) + d->dataLengthIndicator = flags[0]; // (structure 4.1.2.p) + } + break; + } + } +} + +ByteVector Frame::Header::frameID() const +{ + return d->frameID; +} + +void Frame::Header::setFrameID(const ByteVector &id) +{ + d->frameID = id.mid(0, 4); +} + +TagLib::uint Frame::Header::frameSize() const +{ + return d->frameSize; +} + +void Frame::Header::setFrameSize(uint size) +{ + d->frameSize = size; +} + +TagLib::uint Frame::Header::version() const +{ + return d->version; +} + +void Frame::Header::setVersion(TagLib::uint version) +{ + d->version = version; +} + +bool Frame::Header::tagAlterPreservation() const +{ + return d->tagAlterPreservation; +} + +void Frame::Header::setTagAlterPreservation(bool preserve) +{ + d->tagAlterPreservation = preserve; +} + +bool Frame::Header::fileAlterPreservation() const +{ + return d->fileAlterPreservation; +} + +bool Frame::Header::readOnly() const +{ + return d->readOnly; +} + +bool Frame::Header::groupingIdentity() const +{ + return d->groupingIdentity; +} + +bool Frame::Header::compression() const +{ + return d->compression; +} + +bool Frame::Header::encryption() const +{ + return d->encryption; +} + +bool Frame::Header::unsycronisation() const +{ + return unsynchronisation(); +} + +bool Frame::Header::unsynchronisation() const +{ + return d->unsynchronisation; +} + +bool Frame::Header::dataLengthIndicator() const +{ + return d->dataLengthIndicator; +} + +ByteVector Frame::Header::render() const +{ + ByteVector flags(2, char(0)); // just blank for the moment + + ByteVector v = d->frameID + + (d->version == 3 + ? ByteVector::fromUInt(d->frameSize) + : SynchData::fromUInt(d->frameSize)) + + flags; + + return v; +} + +bool Frame::Header::frameAlterPreservation() const +{ + return fileAlterPreservation(); +} diff --git a/taglib/taglib/mpeg/id3v2/id3v2frame.h b/taglib/taglib/mpeg/id3v2/id3v2frame.h new file mode 100644 index 0000000..3e257d4 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2frame.h @@ -0,0 +1,518 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V2FRAME_H +#define TAGLIB_ID3V2FRAME_H + +#include "tstring.h" +#include "tbytevector.h" +#include "taglib_export.h" + +namespace TagLib { + + class StringList; + class PropertyMap; + + namespace ID3v2 { + + class Tag; + class FrameFactory; + + //! ID3v2 frame implementation + + /*! + * This class is the main ID3v2 frame implementation. In ID3v2, a tag is + * split between a collection of frames (which are in turn split into fields + * (Structure, <a href="id3v2-structure.html#4">4</a>) + * (<a href="id3v2-frames.html">Frames</a>). This class provides an API for + * gathering information about and modifying ID3v2 frames. Funtionallity + * specific to a given frame type is handed in one of the many subclasses. + */ + + class TAGLIB_EXPORT Frame + { + friend class Tag; + friend class FrameFactory; + + public: + + /*! + * Creates a textual frame which corresponds to a single key in the PropertyMap + * interface. These are all (User)TextIdentificationFrames except TIPL and TMCL, + * all (User)URLLinkFrames, CommentsFrames, and UnsynchronizedLyricsFrame. + */ + static Frame *createTextualFrame(const String &key, const StringList &values); + + /*! + * Destroys this Frame instance. + */ + virtual ~Frame(); + + /*! + * Returns the Frame ID (Structure, <a href="id3v2-structure.html#4">4</a>) + * (Frames, <a href="id3v2-frames.html#4">4</a>) + */ + ByteVector frameID() const; + + /*! + * Returns the size of the frame. + */ + uint size() const; + + /*! + * Returns the size of the frame header + * + * \deprecated This is only accurate for ID3v2.3 or ID3v2.4. Please use + * the call below which accepts an ID3v2 version number. In the next + * non-binary compatible release this will be made into a non-static + * member that checks the internal ID3v2 version. + */ + static uint headerSize(); // BIC: remove and make non-static + + /*! + * Returns the size of the frame header for the given ID3v2 version. + * + * \deprecated Please see the explanation above. + */ + static uint headerSize(uint version); // BIC: remove and make non-static + + /*! + * Sets the data that will be used as the frame. Since the length is not + * known before the frame has been parsed, this should just be a pointer to + * the first byte of the frame. It will determine the length internally + * and make that available through size(). + */ + void setData(const ByteVector &data); + + /*! + * Set the text of frame in the sanest way possible. This should only be + * reimplemented in frames where there is some logical mapping to text. + * + * \note If the frame type supports multiple text encodings, this will not + * change the text encoding of the frame; the string will be converted to + * that frame's encoding. Please use the specific APIs of the frame types + * to set the encoding if that is desired. + */ + virtual void setText(const String &text); + + /*! + * This returns the textual representation of the data in the frame. + * Subclasses must reimplement this method to provide a string + * representation of the frame's data. + */ + virtual String toString() const = 0; + + /*! + * Render the frame back to its binary format in a ByteVector. + */ + ByteVector render() const; + + /*! + * Returns the text delimiter that is used between fields for the string + * type \a t. + */ + static ByteVector textDelimiter(String::Type t); + + /*! + * The string with which an instrument name is prefixed to build a key in a PropertyMap; + * used to translate PropertyMaps to TMCL frames. In the current implementation, this + * is "PERFORMER:". + */ + static const String instrumentPrefix; + /*! + * The PropertyMap key prefix which triggers the use of a COMM frame instead of a TXXX + * frame for a non-standard key. In the current implementation, this is "COMMENT:". + */ + static const String commentPrefix; + /*! + * The PropertyMap key prefix which triggers the use of a USLT frame instead of a TXXX + * frame for a non-standard key. In the current implementation, this is "LYRICS:". + */ + static const String lyricsPrefix; + /*! + * The PropertyMap key prefix which triggers the use of a WXXX frame instead of a TXX + * frame for a non-standard key. In the current implementation, this is "URL:". + */ + static const String urlPrefix; + + protected: + class Header; + + /*! + * Constructs an ID3v2 frame using \a data to read the header information. + * All other processing of \a data should be handled in a subclass. + * + * \note This need not contain anything more than a frame ID, but + * \e must constain at least that. + */ + explicit Frame(const ByteVector &data); + + /*! + * This creates an Frame using the header \a h. + * + * The ownership of this header will be assigned to the frame and the + * header will be deleted when the frame is destroyed. + */ + Frame(Header *h); + + /*! + * Returns a pointer to the frame header. + */ + Header *header() const; + + /*! + * Sets the header to \a h. If \a deleteCurrent is true, this will free + * the memory of the current header. + * + * The ownership of this header will be assigned to the frame and the + * header will be deleted when the frame is destroyed. + */ + void setHeader(Header *h, bool deleteCurrent = true); + + /*! + * Called by setData() to parse the frame data. It makes this information + * available through the public API. + */ + void parse(const ByteVector &data); + + /*! + * Called by parse() to parse the field data. It makes this information + * available through the public API. This must be overridden by the + * subclasses. + */ + virtual void parseFields(const ByteVector &data) = 0; + + /*! + * Render the field data back to a binary format in a ByteVector. This + * must be overridden by subclasses. + */ + virtual ByteVector renderFields() const = 0; + + /*! + * Returns a ByteVector containing the field data given the frame data. + * This correctly adjusts for the header size plus any additional frame + * data that's specified in the frame header flags. + */ + ByteVector fieldData(const ByteVector &frameData) const; + + /*! + * Reads a String of type \a encodiong from the ByteVector \a data. If \a + * position is passed in it is used both as the starting point and is + * updated to replect the position just after the string that has been read. + * This is useful for reading strings sequentially. + */ + String readStringField(const ByteVector &data, String::Type encoding, + int *positon = 0); + + /*! + * Checks a the list of string values to see if they can be used with the + * specified encoding and returns the recommended encoding. + */ + // BIC: remove and make non-static + static String::Type checkEncoding(const StringList &fields, + String::Type encoding); + + /*! + * Checks a the list of string values to see if they can be used with the + * specified encoding and returns the recommended encoding. This method + * also checks the ID3v2 version and makes sure the encoding can be used + * in the specified version. + */ + // BIC: remove and make non-static + static String::Type checkEncoding(const StringList &fields, + String::Type encoding, uint version); + + /*! + * Checks a the list of string values to see if they can be used with the + * specified encoding and returns the recommended encoding. This method + * also checks the ID3v2 version and makes sure the encoding can be used + * in the version specified by the frame's header. + */ + String::Type checkTextEncoding(const StringList &fields, + String::Type encoding) const; + + + /*! + * Parses the contents of this frame as PropertyMap. If that fails, the returend + * PropertyMap will be empty, and its unsupportedData() will contain this frame's + * ID. + * BIC: Will be a virtual function in future releases. + */ + PropertyMap asProperties() const; + + /*! + * Returns an appropriate ID3 frame ID for the given free-form tag key. This method + * will return ByteVector::null if no specialized translation is found. + */ + static ByteVector keyToFrameID(const String &); + + /*! + * Returns a free-form tag name for the given ID3 frame ID. Note that this does not work + * for general frame IDs such as TXXX or WXXX; in such a case String::null is returned. + */ + static String frameIDToKey(const ByteVector &); + + /*! + * Returns an appropriate TXXX frame description for the given free-form tag key. + */ + static String keyToTXXX(const String &); + + /*! + * Returns a free-form tag name for the given ID3 frame description. + */ + static String txxxToKey(const String &); + + /*! + * This helper function splits the PropertyMap \a original into three ProperytMaps + * \a singleFrameProperties, \a tiplProperties, and \a tmclProperties, such that: + * - \a singleFrameProperties contains only of keys which can be represented with + * exactly one ID3 frame per key. In the current implementation + * this is everything except for the fixed "involved people" keys and keys of the + * form "TextIdentificationFrame::instrumentPrefix" + "instrument", which are + * mapped to a TMCL frame. + * - \a tiplProperties will consist of those keys that are present in + * TextIdentificationFrame::involvedPeopleMap() + * - \a tmclProperties contains the "musician credits" keys which should be mapped + * to a TMCL frame + */ + static void splitProperties(const PropertyMap &original, PropertyMap &singleFrameProperties, + PropertyMap &tiplProperties, PropertyMap &tmclProperties); + + private: + Frame(const Frame &); + Frame &operator=(const Frame &); + + class FramePrivate; + friend class FramePrivate; + FramePrivate *d; + }; + + //! ID3v2 frame header implementation + + /*! + * The ID3v2 Frame Header (Structure, <a href="id3v2-structure.html#4">4</a>) + * + * Every ID3v2::Frame has an associated header that gives some general + * properties of the frame and also makes it possible to identify the frame + * type. + * + * As such when reading an ID3v2 tag ID3v2::FrameFactory first creates the + * frame headers and then creates the appropriate Frame subclass based on + * the type and attaches the header. + */ + + class TAGLIB_EXPORT Frame::Header + { + public: + /*! + * Construct a Frame Header based on \a data. \a data must at least + * contain a 4 byte frame ID, and optionally can contain flag data and the + * frame size. i.e. Just the frame id -- "TALB" -- is a valid value. + * + * \deprecated Please use the constructor below that accepts a version + * number. + */ + Header(const ByteVector &data, bool synchSafeInts); + + /*! + * Construct a Frame Header based on \a data. \a data must at least + * contain a 4 byte frame ID, and optionally can contain flag data and the + * frame size. i.e. Just the frame id -- "TALB" -- is a valid value. + * + * \a version should be the ID3v2 version of the tag. + */ + explicit Header(const ByteVector &data, uint version = 4); + + /*! + * Destroys this Header instance. + */ + virtual ~Header(); + + /*! + * Sets the data for the Header. + * + * \deprecated Please use the version below that accepts an ID3v2 version + * number. + */ + void setData(const ByteVector &data, bool synchSafeInts); + + /*! + * Sets the data for the Header. \a version should indicate the ID3v2 + * version number of the tag that this frame is contained in. + */ + void setData(const ByteVector &data, uint version = 4); + + /*! + * Returns the Frame ID (Structure, <a href="id3v2-structure.html#4">4</a>) + * (Frames, <a href="id3v2-frames.html#4">4</a>) + */ + ByteVector frameID() const; + + /*! + * Sets the frame's ID to \a id. Only the first four bytes of \a id will + * be used. + * + * \warning This method should in general be avoided. It exists simply to + * provide a mechanism for transforming frames from a deprecated frame type + * to a newer one -- i.e. TYER to TDRC from ID3v2.3 to ID3v2.4. + */ + void setFrameID(const ByteVector &id); + + /*! + * Returns the size of the frame data portion, as set when setData() was + * called or set explicitly via setFrameSize(). + */ + uint frameSize() const; + + /*! + * Sets the size of the frame data portion. + */ + void setFrameSize(uint size); + + /*! + * Returns the ID3v2 version of the header, as passed in from the + * construction of the header or set via setVersion(). + */ + uint version() const; + + /*! + * Sets the ID3v2 version of the header, changing has impact on the + * correct parsing/rendering of frame data. + */ + void setVersion(uint version); + + /*! + * Returns the size of the frame header in bytes. + * + * \deprecated Please use the version of this method that accepts a + * version. This is only accurate for ID3v2.3 and ID3v2.4. This will be + * removed in the next binary incompatible release (2.0) and will be + * replaced with a non-static method that checks the frame version. + */ + static uint size(); + + /*! + * Returns the size of the frame header in bytes for the ID3v2 version + * that's given. + * + * \deprecated Please see the explanation in the version above. + */ + static uint size(uint version); + + /*! + * Returns true if the flag for tag alter preservation is set. + * + * The semantics are a little backwards from what would seem natural + * (setting the preservation flag to throw away the frame), but this + * follows the ID3v2 standard. + * + * \see setTagAlterPreservation() + */ + bool tagAlterPreservation() const; + + /*! + * Sets the flag for preservation of this frame if the tag is set. If + * this is set to true the frame will not be written when the tag is + * saved. + * + * The semantics are a little backwards from what would seem natural + * (setting the preservation flag to throw away the frame), but this + * follows the ID3v2 standard. + * + * \see tagAlterPreservation() + */ + void setTagAlterPreservation(bool discard); + + /*! + * Returns true if the flag for file alter preservation is set. + * + * \note This flag is currently ignored internally in TagLib. + */ + bool fileAlterPreservation() const; + + /*! + * Returns true if the frame is meant to be read only. + * + * \note This flag is currently ignored internally in TagLib. + */ + bool readOnly() const; + + /*! + * Returns true if the flag for the grouping identifity is set. + * + * \note This flag is currently ignored internally in TagLib. + */ + bool groupingIdentity() const; + + /*! + * Returns true if compression is enabled for this frame. + * + * \note This flag is currently ignored internally in TagLib. + */ + bool compression() const; + + /*! + * Returns true if encryption is enabled for this frame. + * + * \note This flag is currently ignored internally in TagLib. + */ + bool encryption() const; + +#ifndef DO_NOT_DOCUMENT + bool unsycronisation() const; +#endif + + /*! + * Returns true if unsynchronisation is enabled for this frame. + */ + bool unsynchronisation() const; + + /*! + * Returns true if the flag for a data length indicator is set. + */ + bool dataLengthIndicator() const; + + /*! + * Render the Header back to binary format in a ByteVector. + */ + ByteVector render() const; + + /*! + * \deprecated + */ + bool frameAlterPreservation() const; + + private: + Header(const Header &); + Header &operator=(const Header &); + + class HeaderPrivate; + HeaderPrivate *d; + }; + + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v2/id3v2framefactory.cpp b/taglib/taglib/mpeg/id3v2/id3v2framefactory.cpp new file mode 100644 index 0000000..3371ca7 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2framefactory.cpp @@ -0,0 +1,458 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <tdebug.h> + +#include "id3v2framefactory.h" +#include "id3v2synchdata.h" +#include "id3v1genres.h" + +#include "frames/attachedpictureframe.h" +#include "frames/commentsframe.h" +#include "frames/relativevolumeframe.h" +#include "frames/textidentificationframe.h" +#include "frames/uniquefileidentifierframe.h" +#include "frames/unknownframe.h" +#include "frames/generalencapsulatedobjectframe.h" +#include "frames/urllinkframe.h" +#include "frames/unsynchronizedlyricsframe.h" +#include "frames/popularimeterframe.h" +#include "frames/privateframe.h" +#include "frames/ownershipframe.h" + +using namespace TagLib; +using namespace ID3v2; + +class FrameFactory::FrameFactoryPrivate +{ +public: + FrameFactoryPrivate() : + defaultEncoding(String::Latin1), + useDefaultEncoding(false) {} + + String::Type defaultEncoding; + bool useDefaultEncoding; + + template <class T> void setTextEncoding(T *frame) + { + if(useDefaultEncoding) + frame->setTextEncoding(defaultEncoding); + } +}; + +FrameFactory FrameFactory::factory; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +FrameFactory *FrameFactory::instance() +{ + return &factory; +} + +Frame *FrameFactory::createFrame(const ByteVector &data, bool synchSafeInts) const +{ + return createFrame(data, uint(synchSafeInts ? 4 : 3)); +} + +Frame *FrameFactory::createFrame(const ByteVector &data, uint version) const +{ + Header tagHeader; + tagHeader.setMajorVersion(version); + return createFrame(data, &tagHeader); +} + +Frame *FrameFactory::createFrame(const ByteVector &origData, Header *tagHeader) const +{ + ByteVector data = origData; + uint version = tagHeader->majorVersion(); + Frame::Header *header = new Frame::Header(data, version); + ByteVector frameID = header->frameID(); + + // A quick sanity check -- make sure that the frameID is 4 uppercase Latin1 + // characters. Also make sure that there is data in the frame. + + if(frameID.size() != (version < 3 ? 3 : 4) || + header->frameSize() <= uint(header->dataLengthIndicator() ? 4 : 0) || + header->frameSize() > data.size()) + { + delete header; + return 0; + } + +#ifndef NO_ITUNES_HACKS + if(version == 3 && frameID.size() == 4 && frameID[3] == '\0') { + // iTunes v2.3 tags store v2.2 frames - convert now + frameID = frameID.mid(0, 3); + header->setFrameID(frameID); + header->setVersion(2); + updateFrame(header); + header->setVersion(3); + } +#endif + + for(ByteVector::ConstIterator it = frameID.begin(); it != frameID.end(); it++) { + if( (*it < 'A' || *it > 'Z') && (*it < '0' || *it > '9') ) { + delete header; + return 0; + } + } + + if(version > 3 && (tagHeader->unsynchronisation() || header->unsynchronisation())) { + // Data lengths are not part of the encoded data, but since they are synch-safe + // integers they will be never actually encoded. + ByteVector frameData = data.mid(Frame::Header::size(version), header->frameSize()); + frameData = SynchData::decode(frameData); + data = data.mid(0, Frame::Header::size(version)) + frameData; + } + + // TagLib doesn't mess with encrypted frames, so just treat them + // as unknown frames. + +#if !defined(HAVE_ZLIB) || HAVE_ZLIB == 0 + if(header->compression()) { + debug("Compressed frames are currently not supported."); + return new UnknownFrame(data, header); + } +#endif + if(header->encryption()) { + debug("Encrypted frames are currently not supported."); + return new UnknownFrame(data, header); + } + + if(!updateFrame(header)) { + header->setTagAlterPreservation(true); + return new UnknownFrame(data, header); + } + + // updateFrame() might have updated the frame ID. + + frameID = header->frameID(); + + // This is where things get necissarily nasty. Here we determine which + // Frame subclass (or if none is found simply an Frame) based + // on the frame ID. Since there are a lot of possibilities, that means + // a lot of if blocks. + + // Text Identification (frames 4.2) + + if(frameID.startsWith("T")) { + + TextIdentificationFrame *f = frameID != "TXXX" + ? new TextIdentificationFrame(data, header) + : new UserTextIdentificationFrame(data, header); + + d->setTextEncoding(f); + + if(frameID == "TCON") + updateGenre(f); + + return f; + } + + // Comments (frames 4.10) + + if(frameID == "COMM") { + CommentsFrame *f = new CommentsFrame(data, header); + d->setTextEncoding(f); + return f; + } + + // Attached Picture (frames 4.14) + + if(frameID == "APIC") { + AttachedPictureFrame *f = new AttachedPictureFrame(data, header); + d->setTextEncoding(f); + return f; + } + + // ID3v2.2 Attached Picture + + if(frameID == "PIC") { + AttachedPictureFrame *f = new AttachedPictureFrameV22(data, header); + d->setTextEncoding(f); + return f; + } + + // Relative Volume Adjustment (frames 4.11) + + if(frameID == "RVA2") + return new RelativeVolumeFrame(data, header); + + // Unique File Identifier (frames 4.1) + + if(frameID == "UFID") + return new UniqueFileIdentifierFrame(data, header); + + // General Encapsulated Object (frames 4.15) + + if(frameID == "GEOB") { + GeneralEncapsulatedObjectFrame *f = new GeneralEncapsulatedObjectFrame(data, header); + d->setTextEncoding(f); + return f; + } + + // URL link (frames 4.3) + + if(frameID.startsWith("W")) { + if(frameID != "WXXX") { + return new UrlLinkFrame(data, header); + } + else { + UserUrlLinkFrame *f = new UserUrlLinkFrame(data, header); + d->setTextEncoding(f); + return f; + } + } + + // Unsynchronized lyric/text transcription (frames 4.8) + + if(frameID == "USLT") { + UnsynchronizedLyricsFrame *f = new UnsynchronizedLyricsFrame(data, header); + if(d->useDefaultEncoding) + f->setTextEncoding(d->defaultEncoding); + return f; + } + + // Popularimeter (frames 4.17) + + if(frameID == "POPM") + return new PopularimeterFrame(data, header); + + // Private (frames 4.27) + + if(frameID == "PRIV") + return new PrivateFrame(data, header); + + // Ownership (frames 4.22) + + if(frameID == "OWNE") { + OwnershipFrame *f = new OwnershipFrame(data, header); + d->setTextEncoding(f); + return f; + } + + return new UnknownFrame(data, header); +} + +String::Type FrameFactory::defaultTextEncoding() const +{ + return d->defaultEncoding; +} + +void FrameFactory::setDefaultTextEncoding(String::Type encoding) +{ + d->useDefaultEncoding = true; + d->defaultEncoding = encoding; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +FrameFactory::FrameFactory() +{ + d = new FrameFactoryPrivate; +} + +FrameFactory::~FrameFactory() +{ + delete d; +} + +bool FrameFactory::updateFrame(Frame::Header *header) const +{ + TagLib::ByteVector frameID = header->frameID(); + + switch(header->version()) { + + case 2: // ID3v2.2 + { + if(frameID == "CRM" || + frameID == "EQU" || + frameID == "LNK" || + frameID == "RVA" || + frameID == "TIM" || + frameID == "TSI" || + frameID == "TDA") + { + debug("ID3v2.4 no longer supports the frame type " + String(frameID) + + ". It will be discarded from the tag."); + return false; + } + + // ID3v2.2 only used 3 bytes for the frame ID, so we need to convert all of + // the frames to their 4 byte ID3v2.4 equivalent. + + convertFrame("BUF", "RBUF", header); + convertFrame("CNT", "PCNT", header); + convertFrame("COM", "COMM", header); + convertFrame("CRA", "AENC", header); + convertFrame("ETC", "ETCO", header); + convertFrame("GEO", "GEOB", header); + convertFrame("IPL", "TIPL", header); + convertFrame("MCI", "MCDI", header); + convertFrame("MLL", "MLLT", header); + convertFrame("POP", "POPM", header); + convertFrame("REV", "RVRB", header); + convertFrame("SLT", "SYLT", header); + convertFrame("STC", "SYTC", header); + convertFrame("TAL", "TALB", header); + convertFrame("TBP", "TBPM", header); + convertFrame("TCM", "TCOM", header); + convertFrame("TCO", "TCON", header); + convertFrame("TCP", "TCMP", header); + convertFrame("TCR", "TCOP", header); + convertFrame("TDY", "TDLY", header); + convertFrame("TEN", "TENC", header); + convertFrame("TFT", "TFLT", header); + convertFrame("TKE", "TKEY", header); + convertFrame("TLA", "TLAN", header); + convertFrame("TLE", "TLEN", header); + convertFrame("TMT", "TMED", header); + convertFrame("TOA", "TOAL", header); + convertFrame("TOF", "TOFN", header); + convertFrame("TOL", "TOLY", header); + convertFrame("TOR", "TDOR", header); + convertFrame("TOT", "TOAL", header); + convertFrame("TP1", "TPE1", header); + convertFrame("TP2", "TPE2", header); + convertFrame("TP3", "TPE3", header); + convertFrame("TP4", "TPE4", header); + convertFrame("TPA", "TPOS", header); + convertFrame("TPB", "TPUB", header); + convertFrame("TRC", "TSRC", header); + convertFrame("TRD", "TDRC", header); + convertFrame("TRK", "TRCK", header); + convertFrame("TS2", "TSO2", header); + convertFrame("TSA", "TSOA", header); + convertFrame("TSC", "TSOC", header); + convertFrame("TSP", "TSOP", header); + convertFrame("TSS", "TSSE", header); + convertFrame("TST", "TSOT", header); + convertFrame("TT1", "TIT1", header); + convertFrame("TT2", "TIT2", header); + convertFrame("TT3", "TIT3", header); + convertFrame("TXT", "TOLY", header); + convertFrame("TXX", "TXXX", header); + convertFrame("TYE", "TDRC", header); + convertFrame("UFI", "UFID", header); + convertFrame("ULT", "USLT", header); + convertFrame("WAF", "WOAF", header); + convertFrame("WAR", "WOAR", header); + convertFrame("WAS", "WOAS", header); + convertFrame("WCM", "WCOM", header); + convertFrame("WCP", "WCOP", header); + convertFrame("WPB", "WPUB", header); + convertFrame("WXX", "WXXX", header); + + break; + } + + case 3: // ID3v2.3 + { + if(frameID == "EQUA" || + frameID == "RVAD" || + frameID == "TIME" || + frameID == "TRDA" || + frameID == "TSIZ" || + frameID == "TDAT") + { + debug("ID3v2.4 no longer supports the frame type " + String(frameID) + + ". It will be discarded from the tag."); + return false; + } + + convertFrame("TORY", "TDOR", header); + convertFrame("TYER", "TDRC", header); + convertFrame("IPLS", "TIPL", header); + + break; + } + + default: + + // This should catch a typo that existed in TagLib up to and including + // version 1.1 where TRDC was used for the year rather than TDRC. + + convertFrame("TRDC", "TDRC", header); + break; + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void FrameFactory::convertFrame(const char *from, const char *to, + Frame::Header *header) const +{ + if(header->frameID() != from) + return; + + // debug("ID3v2.4 no longer supports the frame type " + String(from) + " It has" + + // "been converted to the type " + String(to) + "."); + + header->setFrameID(to); +} + +void FrameFactory::updateGenre(TextIdentificationFrame *frame) const +{ + StringList fields = frame->fieldList(); + StringList newfields; + + for(StringList::Iterator it = fields.begin(); it != fields.end(); ++it) { + String s = *it; + int end = s.find(")"); + + if(s.startsWith("(") && end > 0) { + // "(12)Genre" + String text = s.substr(end + 1); + bool ok; + int number = s.substr(1, end - 1).toInt(&ok); + if(ok && number >= 0 && number <= 255 && !(ID3v1::genre(number) == text)) + newfields.append(s.substr(1, end - 1)); + if(!text.isEmpty()) + newfields.append(text); + } + else { + // "Genre" or "12" + newfields.append(s); + } + } + + if(newfields.isEmpty()) + fields.append(String::null); + + frame->setText(newfields); + +} diff --git a/taglib/taglib/mpeg/id3v2/id3v2framefactory.h b/taglib/taglib/mpeg/id3v2/id3v2framefactory.h new file mode 100644 index 0000000..762d7eb --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2framefactory.h @@ -0,0 +1,166 @@ + /*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V2FRAMEFACTORY_H +#define TAGLIB_ID3V2FRAMEFACTORY_H + +#include "taglib_export.h" +#include "tbytevector.h" +#include "id3v2frame.h" +#include "id3v2header.h" + +namespace TagLib { + + namespace ID3v2 { + + class TextIdentificationFrame; + + //! A factory for creating ID3v2 frames during parsing + + /*! + * This factory abstracts away the frame creation process and instantiates + * the appropriate ID3v2::Frame subclasses based on the contents of the + * data. + * + * Reimplementing this factory is the key to adding support for frame types + * not directly supported by TagLib to your application. To do so you would + * subclass this factory reimplement createFrame(). Then by setting your + * factory to be the default factory in ID3v2::Tag constructor or with + * MPEG::File::setID3v2FrameFactory() you can implement behavior that will + * allow for new ID3v2::Frame subclasses (also provided by you) to be used. + * + * This implements both <i>abstract factory</i> and <i>singleton</i> patterns + * of which more information is available on the web and in software design + * textbooks (Notably <i>Design Patters</i>). + * + * \note You do not need to use this factory to create new frames to add to + * an ID3v2::Tag. You can instantiate frame subclasses directly (with new) + * and add them to a tag using ID3v2::Tag::addFrame() + * + * \see ID3v2::Tag::addFrame() + */ + + class TAGLIB_EXPORT FrameFactory + { + public: + static FrameFactory *instance(); + /*! + * Create a frame based on \a data. \a synchSafeInts should only be set + * false if we are parsing an old tag (v2.3 or older) that does not support + * synchsafe ints. + * + * \deprecated Please use the method below that accepts a ID3v2::Header + * instance in new code. + */ + Frame *createFrame(const ByteVector &data, bool synchSafeInts) const; + + /*! + * Create a frame based on \a data. \a version should indicate the ID3v2 + * version of the tag. As ID3v2.4 is the most current version of the + * standard 4 is the default. + * + * \deprecated Please use the method below that accepts a ID3v2::Header + * instance in new code. + */ + Frame *createFrame(const ByteVector &data, uint version = 4) const; + + /*! + * Create a frame based on \a data. \a tagHeader should be a valid + * ID3v2::Header instance. + */ + // BIC: make virtual + Frame *createFrame(const ByteVector &data, Header *tagHeader) const; + + /*! + * Returns the default text encoding for text frames. If setTextEncoding() + * has not been explicitly called this will only be used for new text + * frames. However, if this value has been set explicitly all frames will be + * converted to this type (unless it's explitly set differently for the + * individual frame) when being rendered. + * + * \see setDefaultTextEncoding() + */ + String::Type defaultTextEncoding() const; + + /*! + * Set the default text encoding for all text frames that are created to + * \a encoding. If no value is set the frames with either default to the + * encoding type that was parsed and new frames default to Latin1. + * + * Valid string types for ID3v2 tags are Latin1, UTF8, UTF16 and UTF16BE. + * + * \see defaultTextEncoding() + */ + void setDefaultTextEncoding(String::Type encoding); + + protected: + /*! + * Constructs a frame factory. Because this is a singleton this method is + * protected, but may be used for subclasses. + */ + FrameFactory(); + + /*! + * Destroys the frame factory. + */ + virtual ~FrameFactory(); + + /*! + * This method checks for compliance to the current ID3v2 standard (2.4) + * and does nothing in the common case. However if a frame is found that + * is not compatible with the current standard, this method either updates + * the frame or indicates that it should be discarded. + * + * This method with return true (with or without changes to the frame) if + * this frame should be kept or false if it should be discarded. + * + * See the id3v2.4.0-changes.txt document for further information. + */ + virtual bool updateFrame(Frame::Header *header) const; + + private: + FrameFactory(const FrameFactory &); + FrameFactory &operator=(const FrameFactory &); + + /*! + * This method is used internally to convert a frame from ID \a from to ID + * \a to. If the frame matches the \a from pattern and converts the frame + * ID in the \a header or simply does nothing if the frame ID does not match. + */ + void convertFrame(const char *from, const char *to, + Frame::Header *header) const; + + void updateGenre(TextIdentificationFrame *frame) const; + + static FrameFactory factory; + + class FrameFactoryPrivate; + FrameFactoryPrivate *d; + }; + + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v2/id3v2header.cpp b/taglib/taglib/mpeg/id3v2/id3v2header.cpp new file mode 100644 index 0000000..ad6b3aa --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2header.cpp @@ -0,0 +1,243 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <iostream> +#include <bitset> + +#include <tstring.h> +#include <tdebug.h> + +#include "id3v2header.h" +#include "id3v2footer.h" +#include "id3v2synchdata.h" + +using namespace TagLib; +using namespace ID3v2; + +class Header::HeaderPrivate +{ +public: + HeaderPrivate() : majorVersion(4), + revisionNumber(0), + unsynchronisation(false), + extendedHeader(false), + experimentalIndicator(false), + footerPresent(false), + tagSize(0) {} + + ~HeaderPrivate() {} + + uint majorVersion; + uint revisionNumber; + + bool unsynchronisation; + bool extendedHeader; + bool experimentalIndicator; + bool footerPresent; + + uint tagSize; + + static const uint size = 10; +}; + +//////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +TagLib::uint Header::size() +{ + return HeaderPrivate::size; +} + +ByteVector Header::fileIdentifier() +{ + return ByteVector::fromCString("ID3"); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Header::Header() +{ + d = new HeaderPrivate; +} + +Header::Header(const ByteVector &data) +{ + d = new HeaderPrivate; + parse(data); +} + +Header::~Header() +{ + delete d; +} + +TagLib::uint Header::majorVersion() const +{ + return d->majorVersion; +} + +void Header::setMajorVersion(TagLib::uint version) +{ + d->majorVersion = version; +} + +TagLib::uint Header::revisionNumber() const +{ + return d->revisionNumber; +} + +bool Header::unsynchronisation() const +{ + return d->unsynchronisation; +} + +bool Header::extendedHeader() const +{ + return d->extendedHeader; +} + +bool Header::experimentalIndicator() const +{ + return d->experimentalIndicator; +} + +bool Header::footerPresent() const +{ + return d->footerPresent; +} + +TagLib::uint Header::tagSize() const +{ + return d->tagSize; +} + +TagLib::uint Header::completeTagSize() const +{ + if(d->footerPresent) + return d->tagSize + d->size + Footer::size(); + else + return d->tagSize + d->size; +} + +void Header::setTagSize(uint s) +{ + d->tagSize = s; +} + +void Header::setData(const ByteVector &data) +{ + parse(data); +} + +ByteVector Header::render() const +{ + ByteVector v; + + // add the file identifier -- "ID3" + v.append(fileIdentifier()); + + // add the version number -- we always render a 2.4.0 tag regardless of what + // the tag originally was. + + v.append(char(majorVersion())); + v.append(char(0)); + + // Currently we don't actually support writing extended headers, footers or + // unsynchronized tags, make sure that the flags are set accordingly. + + d->extendedHeader = false; + d->footerPresent = false; + d->unsynchronisation = false; + + // render and add the flags + std::bitset<8> flags; + + flags[7] = d->unsynchronisation; + flags[6] = d->extendedHeader; + flags[5] = d->experimentalIndicator; + flags[4] = d->footerPresent; + + v.append(char(flags.to_ulong())); + + // add the size + v.append(SynchData::fromUInt(d->tagSize)); + + return v; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void Header::parse(const ByteVector &data) +{ + if(data.size() < size()) + return; + + + // do some sanity checking -- even in ID3v2.3.0 and less the tag size is a + // synch-safe integer, so all bytes must be less than 128. If this is not + // true then this is an invalid tag. + + // note that we're doing things a little out of order here -- the size is + // later in the bytestream than the version + + ByteVector sizeData = data.mid(6, 4); + + if(sizeData.size() != 4) { + d->tagSize = 0; + debug("TagLib::ID3v2::Header::parse() - The tag size as read was 0 bytes!"); + return; + } + + for(ByteVector::Iterator it = sizeData.begin(); it != sizeData.end(); it++) { + if(uchar(*it) >= 128) { + d->tagSize = 0; + debug("TagLib::ID3v2::Header::parse() - One of the size bytes in the id3v2 header was greater than the allowed 128."); + return; + } + } + + // The first three bytes, data[0..2], are the File Identifier, "ID3". (structure 3.1 "file identifier") + + // Read the version number from the fourth and fifth bytes. + d->majorVersion = data[3]; // (structure 3.1 "major version") + d->revisionNumber = data[4]; // (structure 3.1 "revision number") + + // Read the flags, the first four bits of the sixth byte. + std::bitset<8> flags(data[5]); + + d->unsynchronisation = flags[7]; // (structure 3.1.a) + d->extendedHeader = flags[6]; // (structure 3.1.b) + d->experimentalIndicator = flags[5]; // (structure 3.1.c) + d->footerPresent = flags[4]; // (structure 3.1.d) + + // Get the size from the remaining four bytes (read above) + + d->tagSize = SynchData::toUInt(sizeData); // (structure 3.1 "size") +} diff --git a/taglib/taglib/mpeg/id3v2/id3v2header.h b/taglib/taglib/mpeg/id3v2/id3v2header.h new file mode 100644 index 0000000..307ba96 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2header.h @@ -0,0 +1,175 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V2HEADER_H +#define TAGLIB_ID3V2HEADER_H + +#include "tbytevector.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace ID3v2 { + + //! An implementation of ID3v2 headers + + /*! + * This class implements ID3v2 headers. It attempts to follow, both + * semantically and programatically, the structure specified in + * the ID3v2 standard. The API is based on the properties of ID3v2 headers + * specified there. If any of the terms used in this documentation are + * unclear please check the specification in the linked section. + * (Structure, <a href="id3v2-structure.html#3.1">3.1</a>) + */ + + class TAGLIB_EXPORT Header + { + public: + /*! + * Constructs an empty ID3v2 header. + */ + Header(); + + /*! + * Constructs an ID3v2 header based on \a data. parse() is called + * immediately. + */ + Header(const ByteVector &data); + + /*! + * Destroys the header. + */ + virtual ~Header(); + + /*! + * Returns the major version number. (Note: This is the 4, not the 2 in + * ID3v2.4.0. The 2 is implied.) + */ + uint majorVersion() const; + + /*! + * Set the the major version number to \a version. (Note: This is + * the 4, not the 2 in ID3v2.4.0. The 2 is implied.) + * \see majorVersion() + * + * \note This is used by the internal parser; this will not change the + * version which is written and in general should not be called by API + * users. + */ + void setMajorVersion(uint version); + + /*! + * Returns the revision number. (Note: This is the 0, not the 4 in + * ID3v2.4.0. The 2 is implied.) + */ + uint revisionNumber() const; + + /*! + * Returns true if unsynchronisation has been applied to all frames. + */ + bool unsynchronisation() const; + + /*! + * Returns true if an extended header is present in the tag. + */ + bool extendedHeader() const; + + /*! + * Returns true if the experimental indicator flag is set. + */ + bool experimentalIndicator() const; + + /*! + * Returns true if a footer is present in the tag. + */ + bool footerPresent() const; + /*! + * Returns the tag size in bytes. This is the size of the frame content. + * The size of the \e entire tag will be this plus the header size (10 + * bytes) and, if present, the footer size (potentially another 10 bytes). + * + * \note This is the value as read from the header to which TagLib attempts + * to provide an API to; it was not a design decision on the part of TagLib + * to not include the mentioned portions of the tag in the \e size. + * + * \see completeTagSize() + */ + uint tagSize() const; + + /*! + * Returns the tag size, including the header and, if present, the footer + * size. + * + * \see tagSize() + */ + uint completeTagSize() const; + + /*! + * Set the tag size to \a s. + * \see tagSize() + */ + void setTagSize(uint s); + + /*! + * Returns the size of the header. Presently this is always 10 bytes. + */ + static uint size(); + + /*! + * Returns the string used to identify and ID3v2 tag inside of a file. + * Presently this is always "ID3". + */ + static ByteVector fileIdentifier(); + + /*! + * Sets the data that will be used as the header. 10 bytes, starting from + * the beginning of \a data are used. + */ + void setData(const ByteVector &data); + + /*! + * Renders the Header back to binary format. + */ + ByteVector render() const; + + protected: + /*! + * Called by setData() to parse the header data. It makes this information + * available through the public API. + */ + void parse(const ByteVector &data); + + private: + Header(const Header &); + Header &operator=(const Header &); + + class HeaderPrivate; + HeaderPrivate *d; + }; + + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v2/id3v2synchdata.cpp b/taglib/taglib/mpeg/id3v2/id3v2synchdata.cpp new file mode 100644 index 0000000..4acfd91 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2synchdata.cpp @@ -0,0 +1,84 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <iostream> + +#include "id3v2synchdata.h" + +using namespace TagLib; +using namespace ID3v2; + +TagLib::uint SynchData::toUInt(const ByteVector &data) +{ + uint sum = 0; + bool notSynchSafe = false; + int last = data.size() > 4 ? 3 : data.size() - 1; + + for(int i = 0; i <= last; i++) { + if(data[i] & 0x80) { + notSynchSafe = true; + break; + } + + sum |= (data[i] & 0x7f) << ((last - i) * 7); + } + + if(notSynchSafe) { + // Invalid data; assume this was created by some buggy software that just + // put normal integers here rather than syncsafe ones, and try it that + // way. + if(data.size() >= 4) { + sum = data.toUInt(0, true); + } + else { + ByteVector tmp(data); + tmp.resize(4); + sum = tmp.toUInt(0, true); + } + } + + return sum; +} + +ByteVector SynchData::fromUInt(uint value) +{ + ByteVector v(4, 0); + + for(int i = 0; i < 4; i++) + v[i] = uchar(value >> ((3 - i) * 7) & 0x7f); + + return v; +} + +ByteVector SynchData::decode(const ByteVector &data) +{ + ByteVector result = data; + + ByteVector pattern(2, char(0)); + pattern[0] = '\xFF'; + pattern[1] = '\x00'; + + return result.replace(pattern, '\xFF'); +} diff --git a/taglib/taglib/mpeg/id3v2/id3v2synchdata.h b/taglib/taglib/mpeg/id3v2/id3v2synchdata.h new file mode 100644 index 0000000..4a1f596 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2synchdata.h @@ -0,0 +1,70 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V2SYNCHDATA_H +#define TAGLIB_ID3V2SYNCHDATA_H + +#include "tbytevector.h" +#include "taglib.h" + +namespace TagLib { + + namespace ID3v2 { + + //! A few functions for ID3v2 synch safe integer conversion + + /*! + * In the ID3v2.4 standard most integer values are encoded as "synch safe" + * integers which are encoded in such a way that they will not give false + * MPEG syncs and confuse MPEG decoders. This namespace provides some + * methods for converting to and from these values to ByteVectors for + * things rendering and parsing ID3v2 data. + */ + + namespace SynchData + { + /*! + * This returns the unsigned integer value of \a data where \a data is a + * ByteVector that contains a \e synchsafe integer (Structure, + * <a href="id3v2-structure.html#6.2">6.2</a>). The default \a length of + * 4 is used if another value is not specified. + */ + TAGLIB_EXPORT uint toUInt(const ByteVector &data); + + /*! + * Returns a 4 byte (32 bit) synchsafe integer based on \a value. + */ + TAGLIB_EXPORT ByteVector fromUInt(uint value); + + /*! + * Convert the data from unsynchronized data to its original format. + */ + TAGLIB_EXPORT ByteVector decode(const ByteVector &input); + } + + } +} + +#endif diff --git a/taglib/taglib/mpeg/id3v2/id3v2tag.cpp b/taglib/taglib/mpeg/id3v2/id3v2tag.cpp new file mode 100644 index 0000000..57637a3 --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2tag.cpp @@ -0,0 +1,732 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <tfile.h> + +#include "id3v2tag.h" +#include "id3v2header.h" +#include "id3v2extendedheader.h" +#include "id3v2footer.h" +#include "id3v2synchdata.h" +#include "tbytevector.h" +#include "id3v1genres.h" +#include "tpropertymap.h" +#include <tdebug.h> + +#include "frames/textidentificationframe.h" +#include "frames/commentsframe.h" +#include "frames/urllinkframe.h" +#include "frames/uniquefileidentifierframe.h" +#include "frames/unsynchronizedlyricsframe.h" +#include "frames/unknownframe.h" + +using namespace TagLib; +using namespace ID3v2; + +class ID3v2::Tag::TagPrivate +{ +public: + TagPrivate() : file(0), tagOffset(-1), extendedHeader(0), footer(0), paddingSize(0) + { + frameList.setAutoDelete(true); + } + ~TagPrivate() + { + delete extendedHeader; + delete footer; + } + + File *file; + long tagOffset; + const FrameFactory *factory; + + Header header; + ExtendedHeader *extendedHeader; + Footer *footer; + + int paddingSize; + + FrameListMap frameListMap; + FrameList frameList; + + static const Latin1StringHandler *stringHandler; +}; + +static const Latin1StringHandler defaultStringHandler; +const ID3v2::Latin1StringHandler *ID3v2::Tag::TagPrivate::stringHandler = &defaultStringHandler; + +//////////////////////////////////////////////////////////////////////////////// +// StringHandler implementation +//////////////////////////////////////////////////////////////////////////////// + +Latin1StringHandler::Latin1StringHandler() +{ +} + +Latin1StringHandler::~Latin1StringHandler() +{ +} + +String Latin1StringHandler::parse(const ByteVector &data) const +{ + return String(data, String::Latin1); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +ID3v2::Tag::Tag() : TagLib::Tag() +{ + d = new TagPrivate; + d->factory = FrameFactory::instance(); +} + +ID3v2::Tag::Tag(File *file, long tagOffset, const FrameFactory *factory) : + TagLib::Tag() +{ + d = new TagPrivate; + + d->file = file; + d->tagOffset = tagOffset; + d->factory = factory; + + read(); +} + +ID3v2::Tag::~Tag() +{ + delete d; +} + + +String ID3v2::Tag::title() const +{ + if(!d->frameListMap["TIT2"].isEmpty()) + return d->frameListMap["TIT2"].front()->toString(); + return String::null; +} + +String ID3v2::Tag::artist() const +{ + if(!d->frameListMap["TPE1"].isEmpty()) + return d->frameListMap["TPE1"].front()->toString(); + return String::null; +} + +String ID3v2::Tag::album() const +{ + if(!d->frameListMap["TALB"].isEmpty()) + return d->frameListMap["TALB"].front()->toString(); + return String::null; +} + +String ID3v2::Tag::comment() const +{ + const FrameList &comments = d->frameListMap["COMM"]; + + if(comments.isEmpty()) + return String::null; + + for(FrameList::ConstIterator it = comments.begin(); it != comments.end(); ++it) + { + CommentsFrame *frame = dynamic_cast<CommentsFrame *>(*it); + + if(frame && frame->description().isEmpty()) + return (*it)->toString(); + } + + return comments.front()->toString(); +} + +String ID3v2::Tag::genre() const +{ + // TODO: In the next major version (TagLib 2.0) a list of multiple genres + // should be separated by " / " instead of " ". For the moment to keep + // the behavior the same as released versions it is being left with " ". + + if(d->frameListMap["TCON"].isEmpty() || + !dynamic_cast<TextIdentificationFrame *>(d->frameListMap["TCON"].front())) + { + return String::null; + } + + // ID3v2.4 lists genres as the fields in its frames field list. If the field + // is simply a number it can be assumed that it is an ID3v1 genre number. + // Here was assume that if an ID3v1 string is present that it should be + // appended to the genre string. Multiple fields will be appended as the + // string is built. + + TextIdentificationFrame *f = static_cast<TextIdentificationFrame *>( + d->frameListMap["TCON"].front()); + + StringList fields = f->fieldList(); + + StringList genres; + + for(StringList::Iterator it = fields.begin(); it != fields.end(); ++it) { + + if((*it).isEmpty()) + continue; + + bool ok; + int number = (*it).toInt(&ok); + if(ok && number >= 0 && number <= 255) { + *it = ID3v1::genre(number); + } + + if(std::find(genres.begin(), genres.end(), *it) == genres.end()) + genres.append(*it); + } + + return genres.toString(); +} + +TagLib::uint ID3v2::Tag::year() const +{ + if(!d->frameListMap["TDRC"].isEmpty()) + return d->frameListMap["TDRC"].front()->toString().substr(0, 4).toInt(); + return 0; +} + +TagLib::uint ID3v2::Tag::track() const +{ + if(!d->frameListMap["TRCK"].isEmpty()) + return d->frameListMap["TRCK"].front()->toString().toInt(); + return 0; +} + +void ID3v2::Tag::setTitle(const String &s) +{ + setTextFrame("TIT2", s); +} + +void ID3v2::Tag::setArtist(const String &s) +{ + setTextFrame("TPE1", s); +} + +void ID3v2::Tag::setAlbum(const String &s) +{ + setTextFrame("TALB", s); +} + +void ID3v2::Tag::setComment(const String &s) +{ + if(s.isEmpty()) { + removeFrames("COMM"); + return; + } + + if(!d->frameListMap["COMM"].isEmpty()) + d->frameListMap["COMM"].front()->setText(s); + else { + CommentsFrame *f = new CommentsFrame(d->factory->defaultTextEncoding()); + addFrame(f); + f->setText(s); + } +} + +void ID3v2::Tag::setGenre(const String &s) +{ + if(s.isEmpty()) { + removeFrames("TCON"); + return; + } + + // iTunes can't handle correctly encoded ID3v2.4 numerical genres. Just use + // strings until iTunes sucks less. + +#ifdef NO_ITUNES_HACKS + + int index = ID3v1::genreIndex(s); + + if(index != 255) + setTextFrame("TCON", String::number(index)); + else + setTextFrame("TCON", s); + +#else + + setTextFrame("TCON", s); + +#endif +} + +void ID3v2::Tag::setYear(uint i) +{ + if(i <= 0) { + removeFrames("TDRC"); + return; + } + setTextFrame("TDRC", String::number(i)); +} + +void ID3v2::Tag::setTrack(uint i) +{ + if(i <= 0) { + removeFrames("TRCK"); + return; + } + setTextFrame("TRCK", String::number(i)); +} + +bool ID3v2::Tag::isEmpty() const +{ + return d->frameList.isEmpty(); +} + +Header *ID3v2::Tag::header() const +{ + return &(d->header); +} + +ExtendedHeader *ID3v2::Tag::extendedHeader() const +{ + return d->extendedHeader; +} + +Footer *ID3v2::Tag::footer() const +{ + return d->footer; +} + +const FrameListMap &ID3v2::Tag::frameListMap() const +{ + return d->frameListMap; +} + +const FrameList &ID3v2::Tag::frameList() const +{ + return d->frameList; +} + +const FrameList &ID3v2::Tag::frameList(const ByteVector &frameID) const +{ + return d->frameListMap[frameID]; +} + +void ID3v2::Tag::addFrame(Frame *frame) +{ + d->frameList.append(frame); + d->frameListMap[frame->frameID()].append(frame); +} + +void ID3v2::Tag::removeFrame(Frame *frame, bool del) +{ + // remove the frame from the frame list + FrameList::Iterator it = d->frameList.find(frame); + d->frameList.erase(it); + + // ...and from the frame list map + it = d->frameListMap[frame->frameID()].find(frame); + d->frameListMap[frame->frameID()].erase(it); + + // ...and delete as desired + if(del) + delete frame; +} + +void ID3v2::Tag::removeFrames(const ByteVector &id) +{ + FrameList l = d->frameListMap[id]; + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) + removeFrame(*it, true); +} + +PropertyMap ID3v2::Tag::properties() const +{ + PropertyMap properties; + for(FrameList::ConstIterator it = frameList().begin(); it != frameList().end(); ++it) { + PropertyMap props = (*it)->asProperties(); + properties.merge(props); + } + return properties; +} + +void ID3v2::Tag::removeUnsupportedProperties(const StringList &properties) +{ + for(StringList::ConstIterator it = properties.begin(); it != properties.end(); ++it){ + if(it->startsWith("UNKNOWN/")) { + String frameID = it->substr(String("UNKNOWN/").size()); + if(frameID.size() != 4) + continue; // invalid specification + ByteVector id = frameID.data(String::Latin1); + // delete all unknown frames of given type + FrameList l = frameList(id); + for(FrameList::ConstIterator fit = l.begin(); fit != l.end(); fit++) + if (dynamic_cast<const UnknownFrame *>(*fit) != 0) + removeFrame(*fit); + } + else if(it->size() == 4){ + ByteVector id = it->data(String::Latin1); + removeFrames(id); + } + else { + ByteVector id = it->substr(0,4).data(String::Latin1); + if(it->size() <= 5) + continue; // invalid specification + String description = it->substr(5); + Frame *frame = 0; + if(id == "TXXX") + frame = UserTextIdentificationFrame::find(this, description); + else if(id == "WXXX") + frame = UserUrlLinkFrame::find(this, description); + else if(id == "COMM") + frame = CommentsFrame::findByDescription(this, description); + else if(id == "USLT") + frame = UnsynchronizedLyricsFrame::findByDescription(this, description); + else if(id == "UFID") + frame = UniqueFileIdentifierFrame::findByOwner(this, description); + if(frame) + removeFrame(frame); + } + } +} + +PropertyMap ID3v2::Tag::setProperties(const PropertyMap &origProps) +{ + FrameList framesToDelete; + // we split up the PropertyMap into the "normal" keys and the "complicated" ones, + // which are those according to TIPL or TMCL frames. + PropertyMap properties; + PropertyMap tiplProperties; + PropertyMap tmclProperties; + Frame::splitProperties(origProps, properties, tiplProperties, tmclProperties); + for(FrameListMap::ConstIterator it = frameListMap().begin(); it != frameListMap().end(); ++it){ + for(FrameList::ConstIterator lit = it->second.begin(); lit != it->second.end(); ++lit){ + PropertyMap frameProperties = (*lit)->asProperties(); + if(it->first == "TIPL") { + if (tiplProperties != frameProperties) + framesToDelete.append(*lit); + else + tiplProperties.erase(frameProperties); + } else if(it->first == "TMCL") { + if (tmclProperties != frameProperties) + framesToDelete.append(*lit); + else + tmclProperties.erase(frameProperties); + } else if(!properties.contains(frameProperties)) + framesToDelete.append(*lit); + else + properties.erase(frameProperties); + } + } + for(FrameList::ConstIterator it = framesToDelete.begin(); it != framesToDelete.end(); ++it) + removeFrame(*it); + + // now create remaining frames: + // start with the involved people list (TIPL) + if(!tiplProperties.isEmpty()) + addFrame(TextIdentificationFrame::createTIPLFrame(tiplProperties)); + // proceed with the musician credit list (TMCL) + if(!tmclProperties.isEmpty()) + addFrame(TextIdentificationFrame::createTMCLFrame(tmclProperties)); + // now create the "one key per frame" frames + for(PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it) + addFrame(Frame::createTextualFrame(it->first, it->second)); + return PropertyMap(); // ID3 implements the complete PropertyMap interface, so an empty map is returned +} + +ByteVector ID3v2::Tag::render() const +{ + return render(4); +} + +void ID3v2::Tag::downgradeFrames(FrameList *frames, FrameList *newFrames) const +{ + const char *unsupportedFrames[] = { + "ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDRL", "TDTG", + "TMOO", "TPRO", "TSOA", "TSOT", "TSST", "TSOP", 0 + }; + ID3v2::TextIdentificationFrame *frameTDOR = 0; + ID3v2::TextIdentificationFrame *frameTDRC = 0; + ID3v2::TextIdentificationFrame *frameTIPL = 0; + ID3v2::TextIdentificationFrame *frameTMCL = 0; + for(FrameList::Iterator it = d->frameList.begin(); it != d->frameList.end(); it++) { + ID3v2::Frame *frame = *it; + ByteVector frameID = frame->header()->frameID(); + for(int i = 0; unsupportedFrames[i]; i++) { + if(frameID == unsupportedFrames[i]) { + debug("A frame that is not supported in ID3v2.3 \'" + + String(frameID) + "\' has been discarded"); + frame = 0; + break; + } + } + if(frame && frameID == "TDOR") { + frameTDOR = dynamic_cast<ID3v2::TextIdentificationFrame *>(frame); + frame = 0; + } + if(frame && frameID == "TDRC") { + frameTDRC = dynamic_cast<ID3v2::TextIdentificationFrame *>(frame); + frame = 0; + } + if(frame && frameID == "TIPL") { + frameTIPL = dynamic_cast<ID3v2::TextIdentificationFrame *>(frame); + frame = 0; + } + if(frame && frameID == "TMCL") { + frameTMCL = dynamic_cast<ID3v2::TextIdentificationFrame *>(frame); + frame = 0; + } + if(frame) { + frames->append(frame); + } + } + if(frameTDOR) { + String content = frameTDOR->toString(); + if(content.size() >= 4) { + ID3v2::TextIdentificationFrame *frameTORY = new ID3v2::TextIdentificationFrame("TORY", String::Latin1); + frameTORY->setText(content.substr(0, 4)); + frames->append(frameTORY); + newFrames->append(frameTORY); + } + } + if(frameTDRC) { + String content = frameTDRC->toString(); + if(content.size() >= 4) { + ID3v2::TextIdentificationFrame *frameTYER = new ID3v2::TextIdentificationFrame("TYER", String::Latin1); + frameTYER->setText(content.substr(0, 4)); + frames->append(frameTYER); + newFrames->append(frameTYER); + if(content.size() >= 10 && content[4] == '-' && content[7] == '-') { + ID3v2::TextIdentificationFrame *frameTDAT = new ID3v2::TextIdentificationFrame("TDAT", String::Latin1); + frameTDAT->setText(content.substr(8, 2) + content.substr(5, 2)); + frames->append(frameTDAT); + newFrames->append(frameTDAT); + if(content.size() >= 16 && content[10] == 'T' && content[13] == ':') { + ID3v2::TextIdentificationFrame *frameTIME = new ID3v2::TextIdentificationFrame("TIME", String::Latin1); + frameTIME->setText(content.substr(11, 2) + content.substr(14, 2)); + frames->append(frameTIME); + newFrames->append(frameTIME); + } + } + } + } + if(frameTIPL || frameTMCL) { + ID3v2::TextIdentificationFrame *frameIPLS = new ID3v2::TextIdentificationFrame("IPLS", String::Latin1); + StringList people; + if(frameTMCL) { + StringList v24People = frameTMCL->fieldList(); + for(uint i = 0; i + 1 < v24People.size(); i += 2) { + people.append(v24People[i]); + people.append(v24People[i+1]); + } + } + if(frameTIPL) { + StringList v24People = frameTIPL->fieldList(); + for(uint i = 0; i + 1 < v24People.size(); i += 2) { + people.append(v24People[i]); + people.append(v24People[i+1]); + } + } + frameIPLS->setText(people); + frames->append(frameIPLS); + newFrames->append(frameIPLS); + } +} + + +ByteVector ID3v2::Tag::render(int version) const +{ + // We need to render the "tag data" first so that we have to correct size to + // render in the tag's header. The "tag data" -- everything that is included + // in ID3v2::Header::tagSize() -- includes the extended header, frames and + // padding, but does not include the tag's header or footer. + + ByteVector tagData; + + if(version != 3 && version != 4) { + debug("Unknown ID3v2 version, using ID3v2.4"); + version = 4; + } + + // TODO: Render the extended header. + + // Loop through the frames rendering them and adding them to the tagData. + + FrameList newFrames; + newFrames.setAutoDelete(true); + + FrameList frameList; + if(version == 4) { + frameList = d->frameList; + } + else { + downgradeFrames(&frameList, &newFrames); + } + + for(FrameList::Iterator it = frameList.begin(); it != frameList.end(); it++) { + (*it)->header()->setVersion(version); + if((*it)->header()->frameID().size() != 4) { + debug("A frame of unsupported or unknown type \'" + + String((*it)->header()->frameID()) + "\' has been discarded"); + continue; + } + if(!(*it)->header()->tagAlterPreservation()) + tagData.append((*it)->render()); + } + + // Compute the amount of padding, and append that to tagData. + + uint paddingSize = 0; + uint originalSize = d->header.tagSize(); + + if(tagData.size() < originalSize) + paddingSize = originalSize - tagData.size(); + else + paddingSize = 1024; + + tagData.append(ByteVector(paddingSize, char(0))); + + // Set the version and data size. + d->header.setMajorVersion(version); + d->header.setTagSize(tagData.size()); + + // TODO: This should eventually include d->footer->render(). + return d->header.render() + tagData; +} + +Latin1StringHandler const *ID3v2::Tag::latin1StringHandler() +{ + return TagPrivate::stringHandler; +} + +void ID3v2::Tag::setLatin1StringHandler(const Latin1StringHandler *handler) +{ + if(handler) + TagPrivate::stringHandler = handler; + else + TagPrivate::stringHandler = &defaultStringHandler; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void ID3v2::Tag::read() +{ + if(d->file && d->file->isOpen()) { + + d->file->seek(d->tagOffset); + d->header.setData(d->file->readBlock(Header::size())); + + // if the tag size is 0, then this is an invalid tag (tags must contain at + // least one frame) + + if(d->header.tagSize() == 0) + return; + + parse(d->file->readBlock(d->header.tagSize())); + } +} + +void ID3v2::Tag::parse(const ByteVector &origData) +{ + ByteVector data = origData; + + if(d->header.unsynchronisation() && d->header.majorVersion() <= 3) + data = SynchData::decode(data); + + uint frameDataPosition = 0; + uint frameDataLength = data.size(); + + // check for extended header + + if(d->header.extendedHeader()) { + if(!d->extendedHeader) + d->extendedHeader = new ExtendedHeader; + d->extendedHeader->setData(data); + if(d->extendedHeader->size() <= data.size()) { + frameDataPosition += d->extendedHeader->size(); + frameDataLength -= d->extendedHeader->size(); + } + } + + // check for footer -- we don't actually need to parse it, as it *must* + // contain the same data as the header, but we do need to account for its + // size. + + if(d->header.footerPresent() && Footer::size() <= frameDataLength) + frameDataLength -= Footer::size(); + + // parse frames + + // Make sure that there is at least enough room in the remaining frame data for + // a frame header. + + while(frameDataPosition < frameDataLength - Frame::headerSize(d->header.majorVersion())) { + + // If the next data is position is 0, assume that we've hit the padding + // portion of the frame data. + + if(data.at(frameDataPosition) == 0) { + if(d->header.footerPresent()) { + debug("Padding *and* a footer found. This is not allowed by the spec."); + } + + d->paddingSize = frameDataLength - frameDataPosition; + return; + } + + Frame *frame = d->factory->createFrame(data.mid(frameDataPosition), + &d->header); + + if(!frame) + return; + + // Checks to make sure that frame parsed correctly. + + if(frame->size() <= 0) { + delete frame; + return; + } + + frameDataPosition += frame->size() + Frame::headerSize(d->header.majorVersion()); + addFrame(frame); + } +} + +void ID3v2::Tag::setTextFrame(const ByteVector &id, const String &value) +{ + if(value.isEmpty()) { + removeFrames(id); + return; + } + + if(!d->frameListMap[id].isEmpty()) + d->frameListMap[id].front()->setText(value); + else { + const String::Type encoding = d->factory->defaultTextEncoding(); + TextIdentificationFrame *f = new TextIdentificationFrame(id, encoding); + addFrame(f); + f->setText(value); + } +} diff --git a/taglib/taglib/mpeg/id3v2/id3v2tag.h b/taglib/taglib/mpeg/id3v2/id3v2tag.h new file mode 100644 index 0000000..5fd5c1f --- /dev/null +++ b/taglib/taglib/mpeg/id3v2/id3v2tag.h @@ -0,0 +1,412 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_ID3V2TAG_H +#define TAGLIB_ID3V2TAG_H + +#include "tag.h" +#include "tbytevector.h" +#include "tstring.h" +#include "tlist.h" +#include "tmap.h" +#include "taglib_export.h" + +#include "id3v2framefactory.h" + +namespace TagLib { + + class File; + + //! An ID3v2 implementation + + /*! + * This is a relatively complete and flexible framework for working with ID3v2 + * tags. + * + * \see ID3v2::Tag + */ + + namespace ID3v2 { + + class Header; + class ExtendedHeader; + class Footer; + + typedef List<Frame *> FrameList; + typedef Map<ByteVector, FrameList> FrameListMap; + + //! An abstraction for the ISO-8859-1 string to data encoding in ID3v2 tags. + + /*! + * ID3v2 tag can store strings in ISO-8859-1 (Latin1), and TagLib only + * supports genuine ISO-8859-1 by default. However, in practice, non + * ISO-8859-1 encodings are often used instead of ISO-8859-1, such as + * Windows-1252 for western languages, Shift_JIS for Japanese and so on. + * + * Here is an option to read such tags by subclassing this class, + * reimplementing parse() and setting your reimplementation as the default + * with ID3v2::Tag::setStringHandler(). + * + * \note Writing non-ISO-8859-1 tags is not implemented intentionally. + * Use UTF-16 or UTF-8 instead. + * + * \see ID3v2::Tag::setStringHandler() + */ + class TAGLIB_EXPORT Latin1StringHandler + { + public: + Latin1StringHandler(); + virtual ~Latin1StringHandler(); + + /*! + * Decode a string from \a data. The default implementation assumes that + * \a data is an ISO-8859-1 (Latin1) character array. + */ + virtual String parse(const ByteVector &data) const; + }; + + //! The main class in the ID3v2 implementation + + /*! + * This is the main class in the ID3v2 implementation. It serves two + * functions. This first, as is obvious from the public API, is to provide a + * container for the other ID3v2 related classes. In addition, through the + * read() and parse() protected methods, it provides the most basic level of + * parsing. In these methods the ID3v2 tag is extracted from the file and + * split into data components. + * + * ID3v2 tags have several parts, TagLib attempts to provide an interface + * for them all. header(), footer() and extendedHeader() corespond to those + * data structures in the ID3v2 standard and the APIs for the classes that + * they return attempt to reflect this. + * + * Also ID3v2 tags are built up from a list of frames, which are in turn + * have a header and a list of fields. TagLib provides two ways of accessing + * the list of frames that are in a given ID3v2 tag. The first is simply + * via the frameList() method. This is just a list of pointers to the frames. + * The second is a map from the frame type -- i.e. "COMM" for comments -- and + * a list of frames of that type. (In some cases ID3v2 allows for multiple + * frames of the same type, hence this being a map to a list rather than just + * a map to an individual frame.) + * + * More information on the structure of frames can be found in the ID3v2::Frame + * class. + * + * read() and parse() pass binary data to the other ID3v2 class structures, + * they do not handle parsing of flags or fields, for instace. Those are + * handled by similar functions within those classes. + * + * \note All pointers to data structures within the tag will become invalid + * when the tag is destroyed. + * + * \warning Dealing with the nasty details of ID3v2 is not for the faint of + * heart and should not be done without much meditation on the spec. It's + * rather long, but if you're planning on messing with this class and others + * that deal with the details of ID3v2 (rather than the nice, safe, abstract + * TagLib::Tag and friends), it's worth your time to familiarize yourself + * with said spec (which is distrubuted with the TagLib sources). TagLib + * tries to do most of the work, but with a little luck, you can still + * convince it to generate invalid ID3v2 tags. The APIs for ID3v2 assume a + * working knowledge of ID3v2 structure. You're been warned. + */ + + class TAGLIB_EXPORT Tag : public TagLib::Tag + { + public: + /*! + * Constructs an empty ID3v2 tag. + * + * \note You must create at least one frame for this tag to be valid. + */ + Tag(); + + /*! + * Constructs an ID3v2 tag read from \a file starting at \a tagOffset. + * \a factory specifies which FrameFactory will be used for the + * construction of new frames. + * + * \note You should be able to ignore the \a factory parameter in almost + * all situations. You would want to specify your own FrameFactory + * subclass in the case that you are extending TagLib to support additional + * frame types, which would be incorperated into your factory. + * + * \see FrameFactory + */ + Tag(File *file, long tagOffset, + const FrameFactory *factory = FrameFactory::instance()); + + /*! + * Destroys this Tag instance. + */ + virtual ~Tag(); + + // Reimplementations. + + virtual String title() const; + virtual String artist() const; + virtual String album() const; + virtual String comment() const; + virtual String genre() const; + virtual uint year() const; + virtual uint track() const; + + virtual void setTitle(const String &s); + virtual void setArtist(const String &s); + virtual void setAlbum(const String &s); + virtual void setComment(const String &s); + virtual void setGenre(const String &s); + virtual void setYear(uint i); + virtual void setTrack(uint i); + + virtual bool isEmpty() const; + + /*! + * Returns a pointer to the tag's header. + */ + Header *header() const; + + /*! + * Returns a pointer to the tag's extended header or null if there is no + * extended header. + */ + ExtendedHeader *extendedHeader() const; + + /*! + * Returns a pointer to the tag's footer or null if there is no footer. + * + * \deprecated I don't see any reason to keep this around since there's + * nothing useful to be retrieved from the footer, but well, again, I'm + * prone to change my mind, so this gets to stay around until near a + * release. + */ + Footer *footer() const; + + /*! + * Returns a reference to the frame list map. This is an FrameListMap of + * all of the frames in the tag. + * + * This is the most convenient structure for accessing the tag's frames. + * Many frame types allow multiple instances of the same frame type so this + * is a map of lists. In most cases however there will only be a single + * frame of a certain type. + * + * Let's say for instance that you wanted to access the frame for total + * beats per minute -- the TBPM frame. + * + * \code + * TagLib::MPEG::File f("foo.mp3"); + * + * // Check to make sure that it has an ID3v2 tag + * + * if(f.ID3v2Tag()) { + * + * // Get the list of frames for a specific frame type + * + * TagLib::ID3v2::FrameList l = f.ID3v2Tag()->frameListMap()["TBPM"]; + * + * if(!l.isEmpty()) + * std::cout << l.front()->toString() << std::endl; + * } + * + * \endcode + * + * \warning You should not modify this data structure directly, instead + * use addFrame() and removeFrame(). + * + * \see frameList() + */ + const FrameListMap &frameListMap() const; + + /*! + * Returns a reference to the frame list. This is an FrameList of all of + * the frames in the tag in the order that they were parsed. + * + * This can be useful if for example you want iterate over the tag's frames + * in the order that they occur in the tag. + * + * \warning You should not modify this data structure directly, instead + * use addFrame() and removeFrame(). + */ + const FrameList &frameList() const; + + /*! + * Returns the frame list for frames with the id \a frameID or an empty + * list if there are no frames of that type. This is just a convenience + * and is equivalent to: + * + * \code + * frameListMap()[frameID]; + * \endcode + * + * \see frameListMap() + */ + const FrameList &frameList(const ByteVector &frameID) const; + + /*! + * Add a frame to the tag. At this point the tag takes ownership of + * the frame and will handle freeing its memory. + * + * \note Using this method will invalidate any pointers on the list + * returned by frameList() + */ + void addFrame(Frame *frame); + + /*! + * Remove a frame from the tag. If \a del is true the frame's memory + * will be freed; if it is false, it must be deleted by the user. + * + * \note Using this method will invalidate any pointers on the list + * returned by frameList() + */ + void removeFrame(Frame *frame, bool del = true); + + /*! + * Remove all frames of type \a id from the tag and free their memory. + * + * \note Using this method will invalidate any pointers on the list + * returned by frameList() + */ + void removeFrames(const ByteVector &id); + + /*! + * Implements the unified property interface -- export function. + * This function does some work to translate the hard-specified ID3v2 + * frame types into a free-form string-to-stringlist PropertyMap: + * - if ID3v2 frame ID is known by Frame::frameIDToKey(), the returned + * key is used + * - if the frame ID is "TXXX" (user text frame), the description() is + * used as key + * - if the frame ID is "WXXX" (user url frame), + * - if the description is empty or "URL", the key "URL" is used + * - otherwise, the key "URL:<description>" is used; + * - if the frame ID is "COMM" (comments frame), + * - if the description is empty or "COMMENT", the key "COMMENT" + * is used + * - otherwise, the key "COMMENT:<description>" is used; + * - if the frame ID is "USLT" (unsynchronized lyrics), + * - if the description is empty or "LYRICS", the key "LYRICS" is used + * - otherwise, the key "LYRICS:<description>" is used; + * - if the frame ID is "TIPL" (involved peoples list), and if all the + * roles defined in the frame are known in TextIdentificationFrame::involvedPeopleMap(), + * then "<role>=<name>" will be contained in the returned obejct for each + * - if the frame ID is "TMCL" (musician credit list), then + * "PERFORMER:<instrument>=<name>" will be contained in the returned + * PropertyMap for each defined musician + * In any other case, the unsupportedData() of the returned object will contain + * the frame's ID and, in case of a frame ID which is allowed to appear more than + * once, the description, separated by a "/". + * + */ + PropertyMap properties() const; + + /*! + * Removes unsupported frames given by \a properties. The elements of + * \a properties must be taken from properties().unsupportedData(); they + * are of one of the following forms: + * - a four-character frame ID, if the ID3 specification allows only one + * frame with that ID (thus, the frame is uniquely determined) + * - frameID + "/" + description(), when the ID is one of "TXXX", "WXXX", + * "COMM", or "USLT", + * - "UNKNOWN/" + frameID, for frames that could not be parsed by TagLib. + * In that case, *all* unknown frames with the given ID will be removed. + */ + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * See the comments in properties(). + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Render the tag back to binary data, suitable to be written to disk. + */ + ByteVector render() const; + + /*! + * Render the tag back to binary data, suitable to be written to disk. + * + * The \a version parameter specifies the version of the rendered + * ID3v2 tag. It can be either 4 or 3. + */ + // BIC: combine with the above method + ByteVector render(int version) const; + + /*! + * Gets the current string handler that decides how the "Latin-1" data + * will be converted to and from binary data. + * + * \see Latin1StringHandler + */ + static Latin1StringHandler const *latin1StringHandler(); + + /*! + * Sets the string handler that decides how the "Latin-1" data will be + * converted to and from binary data. + * If the parameter \a handler is null, the previous handler is + * released and default ISO-8859-1 handler is restored. + * + * \note The caller is responsible for deleting the previous handler + * as needed after it is released. + * + * \see Latin1StringHandler + */ + static void setLatin1StringHandler(const Latin1StringHandler *handler); + + protected: + /*! + * Reads data from the file specified in the constructor. It does basic + * parsing of the data in the largest chunks. It partitions the tag into + * the Header, the body of the tag (which contains the ExtendedHeader and + * frames) and Footer. + */ + void read(); + + /*! + * This is called by read to parse the body of the tag. It determines if an + * extended header exists and adds frames to the FrameListMap. + */ + void parse(const ByteVector &data); + + /*! + * Sets the value of the text frame with the Frame ID \a id to \a value. + * If the frame does not exist, it is created. + */ + void setTextFrame(const ByteVector &id, const String &value); + + void downgradeFrames(FrameList *existingFrames, FrameList *newFrames) const; + + private: + Tag(const Tag &); + Tag &operator=(const Tag &); + + class TagPrivate; + TagPrivate *d; + }; + + } +} + +#endif diff --git a/taglib/taglib/mpeg/mpegfile.cpp b/taglib/taglib/mpeg/mpegfile.cpp new file mode 100644 index 0000000..f765bef --- /dev/null +++ b/taglib/taglib/mpeg/mpegfile.cpp @@ -0,0 +1,663 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tagunion.h> +#include <id3v2tag.h> +#include <id3v2header.h> +#include <id3v1tag.h> +#include <apefooter.h> +#include <apetag.h> +#include <tdebug.h> + +#include <bitset> + +#include "mpegfile.h" +#include "mpegheader.h" +#include "tpropertymap.h" + +using namespace TagLib; + +namespace +{ + enum { ID3v2Index = 0, APEIndex = 1, ID3v1Index = 2 }; +} + +class MPEG::File::FilePrivate +{ +public: + FilePrivate(ID3v2::FrameFactory *frameFactory = ID3v2::FrameFactory::instance()) : + ID3v2FrameFactory(frameFactory), + ID3v2Location(-1), + ID3v2OriginalSize(0), + APELocation(-1), + APEFooterLocation(-1), + APEOriginalSize(0), + ID3v1Location(-1), + hasID3v2(false), + hasID3v1(false), + hasAPE(false), + properties(0) + { + + } + + ~FilePrivate() + { + delete properties; + } + + const ID3v2::FrameFactory *ID3v2FrameFactory; + + long ID3v2Location; + uint ID3v2OriginalSize; + + long APELocation; + long APEFooterLocation; + uint APEOriginalSize; + + long ID3v1Location; + + TagUnion tag; + + // These indicate whether the file *on disk* has these tags, not if + // this data structure does. This is used in computing offsets. + + bool hasID3v2; + bool hasID3v1; + bool hasAPE; + + Properties *properties; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +MPEG::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(file) +{ + d = new FilePrivate; + + if(isOpen()) + read(readProperties, propertiesStyle); +} + +MPEG::File::File(FileName file, ID3v2::FrameFactory *frameFactory, + bool readProperties, Properties::ReadStyle propertiesStyle) : + TagLib::File(file) +{ + d = new FilePrivate(frameFactory); + + if(isOpen()) + read(readProperties, propertiesStyle); +} + +MPEG::File::File(IOStream *stream, ID3v2::FrameFactory *frameFactory, + bool readProperties, Properties::ReadStyle propertiesStyle) : + TagLib::File(stream) +{ + d = new FilePrivate(frameFactory); + + if(isOpen()) + read(readProperties, propertiesStyle); +} + +MPEG::File::~File() +{ + delete d; +} + +TagLib::Tag *MPEG::File::tag() const +{ + return &d->tag; +} + +PropertyMap MPEG::File::properties() const +{ + // once Tag::properties() is virtual, this case distinction could actually be done + // within TagUnion. + if(d->hasID3v2) + return d->tag.access<ID3v2::Tag>(ID3v2Index, false)->properties(); + if(d->hasAPE) + return d->tag.access<APE::Tag>(APEIndex, false)->properties(); + if(d->hasID3v1) + return d->tag.access<ID3v1::Tag>(ID3v1Index, false)->properties(); + return PropertyMap(); +} + +void MPEG::File::removeUnsupportedProperties(const StringList &properties) +{ + if(d->hasID3v2) + d->tag.access<ID3v2::Tag>(ID3v2Index, false)->removeUnsupportedProperties(properties); + else if(d->hasAPE) + d->tag.access<APE::Tag>(APEIndex, false)->removeUnsupportedProperties(properties); + else if(d->hasID3v1) + d->tag.access<ID3v1::Tag>(ID3v1Index, false)->removeUnsupportedProperties(properties); +} + +PropertyMap MPEG::File::setProperties(const PropertyMap &properties) +{ + if(d->hasID3v1) + // update ID3v1 tag if it exists, but ignore the return value + d->tag.access<ID3v1::Tag>(ID3v1Index, false)->setProperties(properties); + return d->tag.access<ID3v2::Tag>(ID3v2Index, true)->setProperties(properties); +} + +MPEG::Properties *MPEG::File::audioProperties() const +{ + return d->properties; +} + +bool MPEG::File::save() +{ + return save(AllTags); +} + +bool MPEG::File::save(int tags) +{ + return save(tags, true); +} + +bool MPEG::File::save(int tags, bool stripOthers) +{ + return save(tags, stripOthers, 4); +} + +bool MPEG::File::save(int tags, bool stripOthers, int id3v2Version) +{ + return save(tags, stripOthers, id3v2Version, true); +} + +bool MPEG::File::save(int tags, bool stripOthers, int id3v2Version, bool duplicateTags) +{ + if(tags == NoTags && stripOthers) + return strip(AllTags); + + if(!ID3v2Tag() && !ID3v1Tag() && !APETag()) { + + if((d->hasID3v1 || d->hasID3v2 || d->hasAPE) && stripOthers) + return strip(AllTags); + + return true; + } + + if(readOnly()) { + debug("MPEG::File::save() -- File is read only."); + return false; + } + + // Create the tags if we've been asked to. + + if (duplicateTags) { + + // Copy the values from the tag that does exist into the new tag, + // except if the existing tag is to be stripped. + + if((tags & ID3v2) && ID3v1Tag() && !(stripOthers && !(tags & ID3v1))) + Tag::duplicate(ID3v1Tag(), ID3v2Tag(true), false); + + if((tags & ID3v1) && d->tag[ID3v2Index] && !(stripOthers && !(tags & ID3v2))) + Tag::duplicate(ID3v2Tag(), ID3v1Tag(true), false); + } + + bool success = true; + + if(ID3v2 & tags) { + + if(ID3v2Tag() && !ID3v2Tag()->isEmpty()) { + + if(!d->hasID3v2) + d->ID3v2Location = 0; + + insert(ID3v2Tag()->render(id3v2Version), d->ID3v2Location, d->ID3v2OriginalSize); + + d->hasID3v2 = true; + + // v1 tag location has changed, update if it exists + + if(ID3v1Tag()) + d->ID3v1Location = findID3v1(); + + // APE tag location has changed, update if it exists + + if(APETag()) + findAPE(); + } + else if(stripOthers) + success = strip(ID3v2, false) && success; + } + else if(d->hasID3v2 && stripOthers) + success = strip(ID3v2) && success; + + if(ID3v1 & tags) { + if(ID3v1Tag() && !ID3v1Tag()->isEmpty()) { + int offset = d->hasID3v1 ? -128 : 0; + seek(offset, End); + writeBlock(ID3v1Tag()->render()); + d->hasID3v1 = true; + d->ID3v1Location = findID3v1(); + } + else if(stripOthers) + success = strip(ID3v1) && success; + } + else if(d->hasID3v1 && stripOthers) + success = strip(ID3v1, false) && success; + + // Dont save an APE-tag unless one has been created + + if((APE & tags) && APETag()) { + if(d->hasAPE) + insert(APETag()->render(), d->APELocation, d->APEOriginalSize); + else { + if(d->hasID3v1) { + insert(APETag()->render(), d->ID3v1Location, 0); + d->APEOriginalSize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + d->APELocation = d->ID3v1Location; + d->ID3v1Location += d->APEOriginalSize; + } + else { + seek(0, End); + d->APELocation = tell(); + APE::Tag *apeTag = d->tag.access<APE::Tag>(APEIndex, false); + d->APEFooterLocation = d->APELocation + + apeTag->footer()->completeTagSize() + - APE::Footer::size(); + writeBlock(APETag()->render()); + d->APEOriginalSize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + } + } + } + else if(d->hasAPE && stripOthers) + success = strip(APE, false) && success; + + return success; +} + +ID3v2::Tag *MPEG::File::ID3v2Tag(bool create) +{ + return d->tag.access<ID3v2::Tag>(ID3v2Index, create); +} + +ID3v1::Tag *MPEG::File::ID3v1Tag(bool create) +{ + return d->tag.access<ID3v1::Tag>(ID3v1Index, create); +} + +APE::Tag *MPEG::File::APETag(bool create) +{ + return d->tag.access<APE::Tag>(APEIndex, create); +} + +bool MPEG::File::strip(int tags) +{ + return strip(tags, true); +} + +bool MPEG::File::strip(int tags, bool freeMemory) +{ + if(readOnly()) { + debug("MPEG::File::strip() - Cannot strip tags from a read only file."); + return false; + } + + if((tags & ID3v2) && d->hasID3v2) { + removeBlock(d->ID3v2Location, d->ID3v2OriginalSize); + d->ID3v2Location = -1; + d->ID3v2OriginalSize = 0; + d->hasID3v2 = false; + + if(freeMemory) + d->tag.set(ID3v2Index, 0); + + // v1 tag location has changed, update if it exists + + if(ID3v1Tag()) + d->ID3v1Location = findID3v1(); + + // APE tag location has changed, update if it exists + + if(APETag()) + findAPE(); + } + + if((tags & ID3v1) && d->hasID3v1) { + truncate(d->ID3v1Location); + d->ID3v1Location = -1; + d->hasID3v1 = false; + + if(freeMemory) + d->tag.set(ID3v1Index, 0); + } + + if((tags & APE) && d->hasAPE) { + removeBlock(d->APELocation, d->APEOriginalSize); + d->APELocation = -1; + d->APEFooterLocation = -1; + d->hasAPE = false; + if(d->hasID3v1) { + if(d->ID3v1Location > d->APELocation) + d->ID3v1Location -= d->APEOriginalSize; + } + + if(freeMemory) + d->tag.set(APEIndex, 0); + } + + return true; +} + +void MPEG::File::setID3v2FrameFactory(const ID3v2::FrameFactory *factory) +{ + d->ID3v2FrameFactory = factory; +} + +long MPEG::File::nextFrameOffset(long position) +{ + bool foundLastSyncPattern = false; + + ByteVector buffer; + + while(true) { + seek(position); + buffer = readBlock(bufferSize()); + + if(buffer.size() <= 0) + return -1; + + if(foundLastSyncPattern && secondSynchByte(buffer[0])) + return position - 1; + + for(uint i = 0; i < buffer.size() - 1; i++) { + if(uchar(buffer[i]) == 0xff && secondSynchByte(buffer[i + 1])) + return position + i; + } + + foundLastSyncPattern = uchar(buffer[buffer.size() - 1]) == 0xff; + position += buffer.size(); + } +} + +long MPEG::File::previousFrameOffset(long position) +{ + bool foundFirstSyncPattern = false; + ByteVector buffer; + + while (position > 0) { + long size = ulong(position) < bufferSize() ? position : bufferSize(); + position -= size; + + seek(position); + buffer = readBlock(size); + + if(buffer.size() <= 0) + break; + + if(foundFirstSyncPattern && uchar(buffer[buffer.size() - 1]) == 0xff) + return position + buffer.size() - 1; + + for(int i = buffer.size() - 2; i >= 0; i--) { + if(uchar(buffer[i]) == 0xff && secondSynchByte(buffer[i + 1])) + return position + i; + } + + foundFirstSyncPattern = secondSynchByte(buffer[0]); + } + return -1; +} + +long MPEG::File::firstFrameOffset() +{ + long position = 0; + + if(ID3v2Tag()) + position = d->ID3v2Location + ID3v2Tag()->header()->completeTagSize(); + + return nextFrameOffset(position); +} + +long MPEG::File::lastFrameOffset() +{ + return previousFrameOffset(ID3v1Tag() ? d->ID3v1Location - 1 : length()); +} + +bool MPEG::File::hasID3v1Tag() const +{ + return d->hasID3v1; +} + +bool MPEG::File::hasID3v2Tag() const +{ + return d->hasID3v2; +} + +bool MPEG::File::hasAPETag() const +{ + return d->hasAPE; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void MPEG::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + // Look for an ID3v2 tag + + d->ID3v2Location = findID3v2(); + + if(d->ID3v2Location >= 0) { + + d->tag.set(ID3v2Index, new ID3v2::Tag(this, d->ID3v2Location, d->ID3v2FrameFactory)); + + d->ID3v2OriginalSize = ID3v2Tag()->header()->completeTagSize(); + + if(ID3v2Tag()->header()->tagSize() <= 0) + d->tag.set(ID3v2Index, 0); + else + d->hasID3v2 = true; + } + + // Look for an ID3v1 tag + + d->ID3v1Location = findID3v1(); + + if(d->ID3v1Location >= 0) { + d->tag.set(ID3v1Index, new ID3v1::Tag(this, d->ID3v1Location)); + d->hasID3v1 = true; + } + + // Look for an APE tag + + findAPE(); + + if(d->APELocation >= 0) { + + d->tag.set(APEIndex, new APE::Tag(this, d->APEFooterLocation)); + d->APEOriginalSize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + } + + if(readProperties) + d->properties = new Properties(this, propertiesStyle); + + // Make sure that we have our default tag types available. + + ID3v2Tag(true); + ID3v1Tag(true); +} + +long MPEG::File::findID3v2() +{ + // This method is based on the contents of TagLib::File::find(), but because + // of some subtlteies -- specifically the need to look for the bit pattern of + // an MPEG sync, it has been modified for use here. + + if(isValid() && ID3v2::Header::fileIdentifier().size() <= bufferSize()) { + + // The position in the file that the current buffer starts at. + + long bufferOffset = 0; + ByteVector buffer; + + // These variables are used to keep track of a partial match that happens at + // the end of a buffer. + + int previousPartialMatch = -1; + bool previousPartialSynchMatch = false; + + // Save the location of the current read pointer. We will restore the + // position using seek() before all returns. + + long originalPosition = tell(); + + // Start the search at the beginning of the file. + + seek(0); + + // This loop is the crux of the find method. There are three cases that we + // want to account for: + // (1) The previously searched buffer contained a partial match of the search + // pattern and we want to see if the next one starts with the remainder of + // that pattern. + // + // (2) The search pattern is wholly contained within the current buffer. + // + // (3) The current buffer ends with a partial match of the pattern. We will + // note this for use in the next itteration, where we will check for the rest + // of the pattern. + + for(buffer = readBlock(bufferSize()); buffer.size() > 0; buffer = readBlock(bufferSize())) { + + // (1) previous partial match + + if(previousPartialSynchMatch && secondSynchByte(buffer[0])) + return -1; + + if(previousPartialMatch >= 0 && int(bufferSize()) > previousPartialMatch) { + const int patternOffset = (bufferSize() - previousPartialMatch); + if(buffer.containsAt(ID3v2::Header::fileIdentifier(), 0, patternOffset)) { + seek(originalPosition); + return bufferOffset - bufferSize() + previousPartialMatch; + } + } + + // (2) pattern contained in current buffer + + long location = buffer.find(ID3v2::Header::fileIdentifier()); + if(location >= 0) { + seek(originalPosition); + return bufferOffset + location; + } + + int firstSynchByte = buffer.find(char(uchar(255))); + + // Here we have to loop because there could be several of the first + // (11111111) byte, and we want to check all such instances until we find + // a full match (11111111 111) or hit the end of the buffer. + + while(firstSynchByte >= 0) { + + // if this *is not* at the end of the buffer + + if(firstSynchByte < int(buffer.size()) - 1) { + if(secondSynchByte(buffer[firstSynchByte + 1])) { + // We've found the frame synch pattern. + seek(originalPosition); + return -1; + } + else { + + // We found 11111111 at the end of the current buffer indicating a + // partial match of the synch pattern. The find() below should + // return -1 and break out of the loop. + + previousPartialSynchMatch = true; + } + } + + // Check in the rest of the buffer. + + firstSynchByte = buffer.find(char(uchar(255)), firstSynchByte + 1); + } + + // (3) partial match + + previousPartialMatch = buffer.endsWithPartialMatch(ID3v2::Header::fileIdentifier()); + + bufferOffset += bufferSize(); + } + + // Since we hit the end of the file, reset the status before continuing. + + clear(); + + seek(originalPosition); + } + + return -1; +} + +long MPEG::File::findID3v1() +{ + if(isValid()) { + seek(-128, End); + long p = tell(); + + if(readBlock(3) == ID3v1::Tag::fileIdentifier()) + return p; + } + return -1; +} + +void MPEG::File::findAPE() +{ + if(isValid()) { + seek(d->hasID3v1 ? -160 : -32, End); + + long p = tell(); + + if(readBlock(8) == APE::Tag::fileIdentifier()) { + d->APEFooterLocation = p; + seek(d->APEFooterLocation); + APE::Footer footer(readBlock(APE::Footer::size())); + d->APELocation = d->APEFooterLocation - footer.completeTagSize() + + APE::Footer::size(); + return; + } + } + + d->APELocation = -1; + d->APEFooterLocation = -1; +} + +bool MPEG::File::secondSynchByte(char byte) +{ + std::bitset<8> b(byte); + + // check to see if the byte matches 111xxxxx + return b.test(7) && b.test(6) && b.test(5); +} diff --git a/taglib/taglib/mpeg/mpegfile.h b/taglib/taglib/mpeg/mpegfile.h new file mode 100644 index 0000000..3fc01e6 --- /dev/null +++ b/taglib/taglib/mpeg/mpegfile.h @@ -0,0 +1,390 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MPEGFILE_H +#define TAGLIB_MPEGFILE_H + +#include "taglib_export.h" +#include "tfile.h" +#include "tag.h" + +#include "mpegproperties.h" + +namespace TagLib { + + namespace ID3v2 { class Tag; class FrameFactory; } + namespace ID3v1 { class Tag; } + namespace APE { class Tag; } + + //! An implementation of TagLib::File with MPEG (MP3) specific methods + + namespace MPEG { + + //! An MPEG file class with some useful methods specific to MPEG + + /*! + * This implements the generic TagLib::File API and additionally provides + * access to properties that are distinct to MPEG files, notably access + * to the different ID3 tags. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * This set of flags is used for various operations and is suitable for + * being OR-ed together. + */ + enum TagTypes { + //! Empty set. Matches no tag types. + NoTags = 0x0000, + //! Matches ID3v1 tags. + ID3v1 = 0x0001, + //! Matches ID3v2 tags. + ID3v2 = 0x0002, + //! Matches APE tags. + APE = 0x0004, + //! Matches all tag types. + AllTags = 0xffff + }; + + /*! + * Constructs an MPEG file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + * + * \deprecated This constructor will be dropped in favor of the one below + * in a future version. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an MPEG file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * If this file contains and ID3v2 tag the frames will be created using + * \a frameFactory. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + // BIC: merge with the above constructor + File(FileName file, ID3v2::FrameFactory *frameFactory, + bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an MPEG file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * If this file contains and ID3v2 tag the frames will be created using + * \a frameFactory. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, ID3v2::FrameFactory *frameFactory, + bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns a pointer to a tag that is the union of the ID3v2 and ID3v1 + * tags. The ID3v2 tag is given priority in reading the information -- if + * requested information exists in both the ID3v2 tag and the ID3v1 tag, + * the information from the ID3v2 tag will be returned. + * + * If you would like more granular control over the content of the tags, + * with the concession of generality, use the tag-type specific calls. + * + * \note As this tag is not implemented as an ID3v2 tag or an ID3v1 tag, + * but a union of the two this pointer may not be cast to the specific + * tag types. + * + * \see ID3v1Tag() + * \see ID3v2Tag() + * \see APETag() + */ + virtual Tag *tag() const; + + /*! + * Implements the reading part of the unified property interface. + * If the file contains more than one tag, only the + * first one (in the order ID3v2, APE, ID3v1) will be converted to the + * PropertyMap. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the writing part of the unified tag dictionary interface. + * In order to avoid problems with deprecated tag formats, this method + * always creates an ID3v2 tag if necessary. + * If an ID3v1 tag exists, it will be updated as well, within the + * limitations of that format. + * The returned PropertyMap refers to the ID3v2 tag only. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the MPEG::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + /*! + * Save the file. If at least one tag -- ID3v1 or ID3v2 -- exists this + * will duplicate its content into the other tag. This returns true + * if saving was successful. + * + * If neither exists or if both tags are empty, this will strip the tags + * from the file. + * + * This is the same as calling save(AllTags); + * + * If you would like more granular control over the content of the tags, + * with the concession of generality, use paramaterized save call below. + * + * \see save(int tags) + */ + virtual bool save(); + + /*! + * Save the file. This will attempt to save all of the tag types that are + * specified by OR-ing together TagTypes values. The save() method above + * uses AllTags. This returns true if saving was successful. + * + * This strips all tags not included in the mask, but does not modify them + * in memory, so later calls to save() which make use of these tags will + * remain valid. This also strips empty tags. + */ + bool save(int tags); + + /*! + * Save the file. This will attempt to save all of the tag types that are + * specified by OR-ing together TagTypes values. The save() method above + * uses AllTags. This returns true if saving was successful. + * + * If \a stripOthers is true this strips all tags not included in the mask, + * but does not modify them in memory, so later calls to save() which make + * use of these tags will remain valid. This also strips empty tags. + */ + // BIC: combine with the above method + bool save(int tags, bool stripOthers); + + /*! + * Save the file. This will attempt to save all of the tag types that are + * specified by OR-ing together TagTypes values. The save() method above + * uses AllTags. This returns true if saving was successful. + * + * If \a stripOthers is true this strips all tags not included in the mask, + * but does not modify them in memory, so later calls to save() which make + * use of these tags will remain valid. This also strips empty tags. + * + * The \a id3v2Version parameter specifies the version of the saved + * ID3v2 tag. It can be either 4 or 3. + */ + // BIC: combine with the above method + bool save(int tags, bool stripOthers, int id3v2Version); + + /*! + * Save the file. This will attempt to save all of the tag types that are + * specified by OR-ing together TagTypes values. The save() method above + * uses AllTags. This returns true if saving was successful. + * + * If \a stripOthers is true this strips all tags not included in the mask, + * but does not modify them in memory, so later calls to save() which make + * use of these tags will remain valid. This also strips empty tags. + * + * The \a id3v2Version parameter specifies the version of the saved + * ID3v2 tag. It can be either 4 or 3. + * + * If \a duplicateTags is true and at least one tag -- ID3v1 or ID3v2 -- + * exists this will duplicate its content into the other tag. + */ + // BIC: combine with the above method + bool save(int tags, bool stripOthers, int id3v2Version, bool duplicateTags); + + /*! + * Returns a pointer to the ID3v2 tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid ID3v2 tag. If \a create is true it will create + * an ID3v2 tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v2 tag. Use hasID3v2Tag() to check if the file + * on disk actually has an ID3v2 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v2Tag() + */ + ID3v2::Tag *ID3v2Tag(bool create = false); + + /*! + * Returns a pointer to the ID3v1 tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid ID3v1 tag. If \a create is true it will create + * an ID3v1 tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v1 tag. Use hasID3v1Tag() to check if the file + * on disk actually has an ID3v1 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v1Tag() + */ + ID3v1::Tag *ID3v1Tag(bool create = false); + + /*! + * Returns a pointer to the APE tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid APE tag. If \a create is true it will create + * an APE tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an APE tag. Use hasAPETag() to check if the file + * on disk actually has an APE tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasAPETag() + */ + APE::Tag *APETag(bool create = false); + + /*! + * This will strip the tags that match the OR-ed together TagTypes from the + * file. By default it strips all tags. It returns true if the tags are + * successfully stripped. + * + * This is equivalent to strip(tags, true) + * + * \note This will also invalidate pointers to the ID3 and APE tags + * as their memory will be freed. + */ + bool strip(int tags = AllTags); + + /*! + * This will strip the tags that match the OR-ed together TagTypes from the + * file. By default it strips all tags. It returns true if the tags are + * successfully stripped. + * + * If \a freeMemory is true the ID3 and APE tags will be deleted and + * pointers to them will be invalidated. + */ + // BIC: merge with the method above + bool strip(int tags, bool freeMemory); + + /*! + * Set the ID3v2::FrameFactory to something other than the default. + * + * \see ID3v2FrameFactory + */ + void setID3v2FrameFactory(const ID3v2::FrameFactory *factory); + + /*! + * Returns the position in the file of the first MPEG frame. + */ + long firstFrameOffset(); + + /*! + * Returns the position in the file of the next MPEG frame, + * using the current position as start + */ + long nextFrameOffset(long position); + + /*! + * Returns the position in the file of the previous MPEG frame, + * using the current position as start + */ + long previousFrameOffset(long position); + + /*! + * Returns the position in the file of the last MPEG frame. + */ + long lastFrameOffset(); + + /*! + * Returns whether or not the file on disk actually has an ID3v1 tag. + * + * \see ID3v1Tag() + */ + bool hasID3v1Tag() const; + + /*! + * Returns whether or not the file on disk actually has an ID3v2 tag. + * + * \see ID3v2Tag() + */ + bool hasID3v2Tag() const; + + /*! + * Returns whether or not the file on disk actually has an APE tag. + * + * \see APETag() + */ + bool hasAPETag() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + long findID3v2(); + long findID3v1(); + void findAPE(); + + /*! + * MPEG frames can be recognized by the bit pattern 11111111 111, so the + * first byte is easy to check for, however checking to see if the second byte + * starts with \e 111 is a bit more tricky, hence this member function. + */ + static bool secondSynchByte(char byte); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mpeg/mpegheader.cpp b/taglib/taglib/mpeg/mpegheader.cpp new file mode 100644 index 0000000..a582f75 --- /dev/null +++ b/taglib/taglib/mpeg/mpegheader.cpp @@ -0,0 +1,277 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <bitset> + +#include <tbytevector.h> +#include <tstring.h> +#include <tdebug.h> +#include "trefcounter.h" + +#include "mpegheader.h" + +using namespace TagLib; + +class MPEG::Header::HeaderPrivate : public RefCounter +{ +public: + HeaderPrivate() : + isValid(false), + version(Version1), + layer(0), + protectionEnabled(false), + sampleRate(0), + isPadded(false), + channelMode(Stereo), + isCopyrighted(false), + isOriginal(false), + frameLength(0), + samplesPerFrame(0) {} + + bool isValid; + Version version; + int layer; + bool protectionEnabled; + int bitrate; + int sampleRate; + bool isPadded; + ChannelMode channelMode; + bool isCopyrighted; + bool isOriginal; + int frameLength; + int samplesPerFrame; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +MPEG::Header::Header(const ByteVector &data) +{ + d = new HeaderPrivate; + parse(data); +} + +MPEG::Header::Header(const Header &h) : d(h.d) +{ + d->ref(); +} + +MPEG::Header::~Header() +{ + if (d->deref()) + delete d; +} + +bool MPEG::Header::isValid() const +{ + return d->isValid; +} + +MPEG::Header::Version MPEG::Header::version() const +{ + return d->version; +} + +int MPEG::Header::layer() const +{ + return d->layer; +} + +bool MPEG::Header::protectionEnabled() const +{ + return d->protectionEnabled; +} + +int MPEG::Header::bitrate() const +{ + return d->bitrate; +} + +int MPEG::Header::sampleRate() const +{ + return d->sampleRate; +} + +bool MPEG::Header::isPadded() const +{ + return d->isPadded; +} + +MPEG::Header::ChannelMode MPEG::Header::channelMode() const +{ + return d->channelMode; +} + +bool MPEG::Header::isCopyrighted() const +{ + return d->isCopyrighted; +} + +bool MPEG::Header::isOriginal() const +{ + return d->isOriginal; +} + +int MPEG::Header::frameLength() const +{ + return d->frameLength; +} + +int MPEG::Header::samplesPerFrame() const +{ + return d->samplesPerFrame; +} + +MPEG::Header &MPEG::Header::operator=(const Header &h) +{ + if(&h == this) + return *this; + + if(d->deref()) + delete d; + + d = h.d; + d->ref(); + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void MPEG::Header::parse(const ByteVector &data) +{ + if(data.size() < 4 || uchar(data[0]) != 0xff) { + debug("MPEG::Header::parse() -- First byte did not match MPEG synch."); + return; + } + + std::bitset<32> flags(TAGLIB_CONSTRUCT_BITSET(data.toUInt())); + + // Check for the second byte's part of the MPEG synch + + if(!flags[23] || !flags[22] || !flags[21]) { + debug("MPEG::Header::parse() -- Second byte did not match MPEG synch."); + return; + } + + // Set the MPEG version + + if(!flags[20] && !flags[19]) + d->version = Version2_5; + else if(flags[20] && !flags[19]) + d->version = Version2; + else if(flags[20] && flags[19]) + d->version = Version1; + + // Set the MPEG layer + + if(!flags[18] && flags[17]) + d->layer = 3; + else if(flags[18] && !flags[17]) + d->layer = 2; + else if(flags[18] && flags[17]) + d->layer = 1; + + d->protectionEnabled = !flags[16]; + + // Set the bitrate + + static const int bitrates[2][3][16] = { + { // Version 1 + { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0 }, // layer 1 + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0 }, // layer 2 + { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 } // layer 3 + }, + { // Version 2 or 2.5 + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 }, // layer 1 + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // layer 2 + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 } // layer 3 + } + }; + + const int versionIndex = d->version == Version1 ? 0 : 1; + const int layerIndex = d->layer > 0 ? d->layer - 1 : 0; + + // The bitrate index is encoded as the first 4 bits of the 3rd byte, + // i.e. 1111xxxx + + int i = uchar(data[2]) >> 4; + + d->bitrate = bitrates[versionIndex][layerIndex][i]; + + // Set the sample rate + + static const int sampleRates[3][4] = { + { 44100, 48000, 32000, 0 }, // Version 1 + { 22050, 24000, 16000, 0 }, // Version 2 + { 11025, 12000, 8000, 0 } // Version 2.5 + }; + + // The sample rate index is encoded as two bits in the 3nd byte, i.e. xxxx11xx + + i = uchar(data[2]) >> 2 & 0x03; + + d->sampleRate = sampleRates[d->version][i]; + + if(d->sampleRate == 0) { + debug("MPEG::Header::parse() -- Invalid sample rate."); + return; + } + + // The channel mode is encoded as a 2 bit value at the end of the 3nd byte, + // i.e. xxxxxx11 + + d->channelMode = ChannelMode((uchar(data[3]) & 0xC0) >> 6); + + // TODO: Add mode extension for completeness + + d->isOriginal = flags[2]; + d->isCopyrighted = flags[3]; + d->isPadded = flags[9]; + + // Calculate the frame length + + if(d->layer == 1) + d->frameLength = 24000 * 2 * d->bitrate / d->sampleRate + int(d->isPadded); + else + d->frameLength = 72000 * d->bitrate / d->sampleRate + int(d->isPadded); + + // Samples per frame + + static const int samplesPerFrame[3][2] = { + // MPEG1, 2/2.5 + { 384, 384 }, // Layer I + { 1152, 1152 }, // Layer II + { 1152, 576 } // Layer III + }; + + d->samplesPerFrame = samplesPerFrame[layerIndex][versionIndex]; + + // Now that we're done parsing, set this to be a valid frame. + + d->isValid = true; +} diff --git a/taglib/taglib/mpeg/mpegheader.h b/taglib/taglib/mpeg/mpegheader.h new file mode 100644 index 0000000..020ebd0 --- /dev/null +++ b/taglib/taglib/mpeg/mpegheader.h @@ -0,0 +1,166 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MPEGHEADER_H +#define TAGLIB_MPEGHEADER_H + +#include "taglib_export.h" + +namespace TagLib { + + class ByteVector; + + namespace MPEG { + + //! An implementation of MP3 frame headers + + /*! + * This is an implementation of MPEG Layer III headers. The API follows more + * or less the binary format of these headers. I've used + * <a href="http://www.mp3-tech.org/programmer/frame_header.html">this</a> + * document as a reference. + */ + + class TAGLIB_EXPORT Header + { + public: + /*! + * Parses an MPEG header based on \a data. + */ + Header(const ByteVector &data); + + /*! + * Does a shallow copy of \a h. + */ + Header(const Header &h); + + /*! + * Destroys this Header instance. + */ + virtual ~Header(); + + /*! + * Returns true if the frame is at least an appropriate size and has + * legal values. + */ + bool isValid() const; + + /*! + * The MPEG Version. + */ + enum Version { + //! MPEG Version 1 + Version1 = 0, + //! MPEG Version 2 + Version2 = 1, + //! MPEG Version 2.5 + Version2_5 = 2 + }; + + /*! + * Returns the MPEG Version of the header. + */ + Version version() const; + + /*! + * Returns the layer version. This will be between the values 1-3. + */ + int layer() const; + + /*! + * Returns true if the MPEG protection bit is enabled. + */ + bool protectionEnabled() const; + + /*! + * Returns the bitrate encoded in the header. + */ + int bitrate() const; + + /*! + * Returns the sample rate in Hz. + */ + int sampleRate() const; + + /*! + * Returns true if the frame is padded. + */ + bool isPadded() const; + + /*! + * There are a few combinations or one or two channel audio that are + * possible: + */ + enum ChannelMode { + //! Stereo + Stereo = 0, + //! Stereo + JointStereo = 1, + //! Dual Mono + DualChannel = 2, + //! Mono + SingleChannel = 3 + }; + + /*! + * Returns the channel mode for this frame. + */ + ChannelMode channelMode() const; + + /*! + * Returns true if the copyrighted bit is set. + */ + bool isCopyrighted() const; + + /*! + * Returns true if the "original" bit is set. + */ + bool isOriginal() const; + + /*! + * Returns the frame length. + */ + int frameLength() const; + + /*! + * Returns the number of frames per sample. + */ + int samplesPerFrame() const; + + /*! + * Makes a shallow copy of the header. + */ + Header &operator=(const Header &h); + + private: + void parse(const ByteVector &data); + + class HeaderPrivate; + HeaderPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mpeg/mpegproperties.cpp b/taglib/taglib/mpeg/mpegproperties.cpp new file mode 100644 index 0000000..3af9566 --- /dev/null +++ b/taglib/taglib/mpeg/mpegproperties.cpp @@ -0,0 +1,254 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tstring.h> + +#include "mpegproperties.h" +#include "mpegfile.h" +#include "xingheader.h" + +using namespace TagLib; + +class MPEG::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(File *f, ReadStyle s) : + file(f), + xingHeader(0), + style(s), + length(0), + bitrate(0), + sampleRate(0), + channels(0), + layer(0), + version(Header::Version1), + channelMode(Header::Stereo), + protectionEnabled(false), + isCopyrighted(false), + isOriginal(false) {} + + ~PropertiesPrivate() + { + delete xingHeader; + } + + File *file; + XingHeader *xingHeader; + ReadStyle style; + int length; + int bitrate; + int sampleRate; + int channels; + int layer; + Header::Version version; + Header::ChannelMode channelMode; + bool protectionEnabled; + bool isCopyrighted; + bool isOriginal; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +MPEG::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(file, style); + + if(file && file->isOpen()) + read(); +} + +MPEG::Properties::~Properties() +{ + delete d; +} + +int MPEG::Properties::length() const +{ + return d->length; +} + +int MPEG::Properties::bitrate() const +{ + return d->bitrate; +} + +int MPEG::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int MPEG::Properties::channels() const +{ + return d->channels; +} + +const MPEG::XingHeader *MPEG::Properties::xingHeader() const +{ + return d->xingHeader; +} + +MPEG::Header::Version MPEG::Properties::version() const +{ + return d->version; +} + +int MPEG::Properties::layer() const +{ + return d->layer; +} + +bool MPEG::Properties::protectionEnabled() const +{ + return d->protectionEnabled; +} + +MPEG::Header::ChannelMode MPEG::Properties::channelMode() const +{ + return d->channelMode; +} + +bool MPEG::Properties::isCopyrighted() const +{ + return d->isCopyrighted; +} + +bool MPEG::Properties::isOriginal() const +{ + return d->isOriginal; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void MPEG::Properties::read() +{ + // Since we've likely just looked for the ID3v1 tag, start at the end of the + // file where we're least likely to have to have to move the disk head. + + long last = d->file->lastFrameOffset(); + + if(last < 0) { + debug("MPEG::Properties::read() -- Could not find a valid last MPEG frame in the stream."); + return; + } + + d->file->seek(last); + Header lastHeader(d->file->readBlock(4)); + + long first = d->file->firstFrameOffset(); + + if(first < 0) { + debug("MPEG::Properties::read() -- Could not find a valid first MPEG frame in the stream."); + return; + } + + if(!lastHeader.isValid()) { + + long pos = last; + + while(pos > first) { + + pos = d->file->previousFrameOffset(pos); + + if(pos < 0) + break; + + d->file->seek(pos); + Header header(d->file->readBlock(4)); + + if(header.isValid()) { + lastHeader = header; + last = pos; + break; + } + } + } + + // Now jump back to the front of the file and read what we need from there. + + d->file->seek(first); + Header firstHeader(d->file->readBlock(4)); + + if(!firstHeader.isValid() || !lastHeader.isValid()) { + debug("MPEG::Properties::read() -- Page headers were invalid."); + return; + } + + // Check for a Xing header that will help us in gathering information about a + // VBR stream. + + int xingHeaderOffset = MPEG::XingHeader::xingHeaderOffset(firstHeader.version(), + firstHeader.channelMode()); + + d->file->seek(first + xingHeaderOffset); + d->xingHeader = new XingHeader(d->file->readBlock(16)); + + // Read the length and the bitrate from the Xing header. + + if(d->xingHeader->isValid() && + firstHeader.sampleRate() > 0 && + d->xingHeader->totalFrames() > 0) + { + double timePerFrame = + double(firstHeader.samplesPerFrame()) / firstHeader.sampleRate(); + + double length = timePerFrame * d->xingHeader->totalFrames(); + + d->length = int(length); + d->bitrate = d->length > 0 ? (int)(d->xingHeader->totalSize() * 8 / length / 1000) : 0; + } + else { + // Since there was no valid Xing header found, we hope that we're in a constant + // bitrate file. + + delete d->xingHeader; + d->xingHeader = 0; + + // TODO: Make this more robust with audio property detection for VBR without a + // Xing header. + + if(firstHeader.frameLength() > 0 && firstHeader.bitrate() > 0) { + int frames = (last - first) / firstHeader.frameLength() + 1; + + d->length = int(float(firstHeader.frameLength() * frames) / + float(firstHeader.bitrate() * 125) + 0.5); + d->bitrate = firstHeader.bitrate(); + } + } + + + d->sampleRate = firstHeader.sampleRate(); + d->channels = firstHeader.channelMode() == Header::SingleChannel ? 1 : 2; + d->version = firstHeader.version(); + d->layer = firstHeader.layer(); + d->protectionEnabled = firstHeader.protectionEnabled(); + d->channelMode = firstHeader.channelMode(); + d->isCopyrighted = firstHeader.isCopyrighted(); + d->isOriginal = firstHeader.isOriginal(); +} diff --git a/taglib/taglib/mpeg/mpegproperties.h b/taglib/taglib/mpeg/mpegproperties.h new file mode 100644 index 0000000..72e594f --- /dev/null +++ b/taglib/taglib/mpeg/mpegproperties.h @@ -0,0 +1,118 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MPEGPROPERTIES_H +#define TAGLIB_MPEGPROPERTIES_H + +#include "taglib_export.h" +#include "audioproperties.h" + +#include "mpegheader.h" + +namespace TagLib { + + namespace MPEG { + + class File; + class XingHeader; + + //! An implementation of audio property reading for MP3 + + /*! + * This reads the data from an MPEG Layer III stream found in the + * AudioProperties API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of MPEG::Properties with the data read from the + * MPEG::File \a file. + */ + Properties(File *file, ReadStyle style = Average); + + /*! + * Destroys this MPEG Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + /*! + * Returns a pointer to the XingHeader if one exists or null if no + * XingHeader was found. + */ + + const XingHeader *xingHeader() const; + + /*! + * Returns the MPEG Version of the file. + */ + Header::Version version() const; + + /*! + * Returns the layer version. This will be between the values 1-3. + */ + int layer() const; + + /*! + * Returns true if the MPEG protection bit is enabled. + */ + bool protectionEnabled() const; + + /*! + * Returns the channel mode for this frame. + */ + Header::ChannelMode channelMode() const; + + /*! + * Returns true if the copyrighted bit is set. + */ + bool isCopyrighted() const; + + /*! + * Returns true if the "original" bit is set. + */ + bool isOriginal() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/mpeg/xingheader.cpp b/taglib/taglib/mpeg/xingheader.cpp new file mode 100644 index 0000000..9e20127 --- /dev/null +++ b/taglib/taglib/mpeg/xingheader.cpp @@ -0,0 +1,115 @@ +/*************************************************************************** + copyright : (C) 2003 by Ismael Orenstein + email : orenstein@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tstring.h> +#include <tdebug.h> + +#include "xingheader.h" + +using namespace TagLib; + +class MPEG::XingHeader::XingHeaderPrivate +{ +public: + XingHeaderPrivate() : + frames(0), + size(0), + valid(false) + {} + + uint frames; + uint size; + bool valid; +}; + +MPEG::XingHeader::XingHeader(const ByteVector &data) +{ + d = new XingHeaderPrivate; + parse(data); +} + +MPEG::XingHeader::~XingHeader() +{ + delete d; +} + +bool MPEG::XingHeader::isValid() const +{ + return d->valid; +} + +TagLib::uint MPEG::XingHeader::totalFrames() const +{ + return d->frames; +} + +TagLib::uint MPEG::XingHeader::totalSize() const +{ + return d->size; +} + +int MPEG::XingHeader::xingHeaderOffset(TagLib::MPEG::Header::Version v, + TagLib::MPEG::Header::ChannelMode c) +{ + if(v == MPEG::Header::Version1) { + if(c == MPEG::Header::SingleChannel) + return 0x15; + else + return 0x24; + } + else { + if(c == MPEG::Header::SingleChannel) + return 0x0D; + else + return 0x15; + } +} + +void MPEG::XingHeader::parse(const ByteVector &data) +{ + // Check to see if a valid Xing header is available. + + if(!data.startsWith("Xing") && !data.startsWith("Info")) + return; + + // If the XingHeader doesn't contain the number of frames and the total stream + // info it's invalid. + + if(!(data[7] & 0x01)) { + debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the total number of frames."); + return; + } + + if(!(data[7] & 0x02)) { + debug("MPEG::XingHeader::parse() -- Xing header doesn't contain the total stream size."); + return; + } + + d->frames = data.toUInt(8U); + d->size = data.toUInt(12U); + + d->valid = true; +} diff --git a/taglib/taglib/mpeg/xingheader.h b/taglib/taglib/mpeg/xingheader.h new file mode 100644 index 0000000..ffe7494 --- /dev/null +++ b/taglib/taglib/mpeg/xingheader.h @@ -0,0 +1,100 @@ +/*************************************************************************** + copyright : (C) 2003 by Ismael Orenstein + email : orenstein@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_XINGHEADER_H +#define TAGLIB_XINGHEADER_H + +#include "mpegheader.h" +#include "taglib_export.h" + +namespace TagLib { + + class ByteVector; + + namespace MPEG { + + //! An implementation of the Xing VBR headers + + /*! + * This is a minimalistic implementation of the Xing VBR headers. Xing + * headers are often added to VBR (variable bit rate) MP3 streams to make it + * easy to compute the length and quality of a VBR stream. Our implementation + * is only concerned with the total size of the stream (so that we can + * calculate the total playing time and the average bitrate). It uses + * <a href="http://home.pcisys.net/~melanson/codecs/mp3extensions.txt">this text</a> + * and the XMMS sources as references. + */ + + class TAGLIB_EXPORT XingHeader + { + public: + /*! + * Parses a Xing header based on \a data. The data must be at least 16 + * bytes long (anything longer than this is discarded). + */ + XingHeader(const ByteVector &data); + + /*! + * Destroy this XingHeader instance. + */ + virtual ~XingHeader(); + + /*! + * Returns true if the data was parsed properly and if there is a valid + * Xing header present. + */ + bool isValid() const; + + /*! + * Returns the total number of frames. + */ + uint totalFrames() const; + + /*! + * Returns the total size of stream in bytes. + */ + uint totalSize() const; + + /*! + * Returns the offset for the start of this Xing header, given the + * version and channels of the frame + */ + // BIC: rename to offset() + static int xingHeaderOffset(TagLib::MPEG::Header::Version v, + TagLib::MPEG::Header::ChannelMode c); + + private: + XingHeader(const XingHeader &); + XingHeader &operator=(const XingHeader &); + + void parse(const ByteVector &data); + + class XingHeaderPrivate; + XingHeaderPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/ogg/flac/oggflacfile.cpp b/taglib/taglib/ogg/flac/oggflacfile.cpp new file mode 100644 index 0000000..bdf8245 --- /dev/null +++ b/taglib/taglib/ogg/flac/oggflacfile.cpp @@ -0,0 +1,294 @@ +/*************************************************************************** + copyright : (C) 2004-2005 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tstring.h> +#include <tdebug.h> +#include <tpropertymap.h> + +#include <xiphcomment.h> +#include "oggflacfile.h" + +using namespace TagLib; +using TagLib::FLAC::Properties; + +class Ogg::FLAC::File::FilePrivate +{ +public: + FilePrivate() : + comment(0), + properties(0), + streamStart(0), + streamLength(0), + scanned(false), + hasXiphComment(false), + commentPacket(0) {} + + ~FilePrivate() + { + delete comment; + delete properties; + } + + Ogg::XiphComment *comment; + + Properties *properties; + ByteVector streamInfoData; + ByteVector xiphCommentData; + long streamStart; + long streamLength; + bool scanned; + + bool hasXiphComment; + int commentPacket; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Ogg::FLAC::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : Ogg::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +Ogg::FLAC::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : Ogg::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +Ogg::FLAC::File::~File() +{ + delete d; +} + +Ogg::XiphComment *Ogg::FLAC::File::tag() const +{ + return d->comment; +} + +PropertyMap Ogg::FLAC::File::properties() const +{ + return d->comment->properties(); +} + +PropertyMap Ogg::FLAC::File::setProperties(const PropertyMap &properties) +{ + return d->comment->setProperties(properties); +} + +Properties *Ogg::FLAC::File::audioProperties() const +{ + return d->properties; +} + + +bool Ogg::FLAC::File::save() +{ + d->xiphCommentData = d->comment->render(false); + + // Create FLAC metadata-block: + + // Put the size in the first 32 bit (I assume no more than 24 bit are used) + + ByteVector v = ByteVector::fromUInt(d->xiphCommentData.size()); + + // Set the type of the metadata-block to be a Xiph / Vorbis comment + + v[0] = 4; + + // Append the comment-data after the 32 bit header + + v.append(d->xiphCommentData); + + // Save the packet at the old spot + // FIXME: Use padding if size is increasing + + setPacket(d->commentPacket, v); + + return Ogg::File::save(); +} + +bool Ogg::FLAC::File::hasXiphComment() const +{ + return d->hasXiphComment; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Ogg::FLAC::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + // Sanity: Check if we really have an Ogg/FLAC file + +/* + ByteVector oggHeader = packet(0); + + if (oggHeader.mid(28,4) != "fLaC") { + debug("Ogg::FLAC::File::read() -- Not an Ogg/FLAC file"); + setValid(false); + return; + }*/ + + // Look for FLAC metadata, including vorbis comments + + scan(); + + if (!d->scanned) { + setValid(false); + return; + } + + + if(d->hasXiphComment) + d->comment = new Ogg::XiphComment(xiphCommentData()); + else + d->comment = new Ogg::XiphComment; + + + if(readProperties) + d->properties = new Properties(streamInfoData(), streamLength(), propertiesStyle); +} + +ByteVector Ogg::FLAC::File::streamInfoData() +{ + scan(); + return d->streamInfoData; +} + +ByteVector Ogg::FLAC::File::xiphCommentData() +{ + scan(); + return d->xiphCommentData; +} + +long Ogg::FLAC::File::streamLength() +{ + scan(); + return d->streamLength; +} + +void Ogg::FLAC::File::scan() +{ + // Scan the metadata pages + + if(d->scanned) + return; + + if(!isValid()) + return; + + int ipacket = 0; + long overhead = 0; + + ByteVector metadataHeader = packet(ipacket); + if(metadataHeader.isNull()) + return; + + ByteVector header; + + if (!metadataHeader.startsWith("fLaC")) { + // FLAC 1.1.2+ + if (metadataHeader.mid(1,4) != "FLAC") return; + + if (metadataHeader[5] != 1) return; // not version 1 + + metadataHeader = metadataHeader.mid(13); + } + else { + // FLAC 1.1.0 & 1.1.1 + metadataHeader = packet(++ipacket); + + if(metadataHeader.isNull()) + return; + + } + + header = metadataHeader.mid(0,4); + // Header format (from spec): + // <1> Last-metadata-block flag + // <7> BLOCK_TYPE + // 0 : STREAMINFO + // 1 : PADDING + // .. + // 4 : VORBIS_COMMENT + // .. + // <24> Length of metadata to follow + + char blockType = header[0] & 0x7f; + bool lastBlock = (header[0] & 0x80) != 0; + uint length = header.toUInt(1, 3, true); + overhead += length; + + // Sanity: First block should be the stream_info metadata + + if(blockType != 0) { + debug("Ogg::FLAC::File::scan() -- Invalid Ogg/FLAC stream"); + return; + } + + d->streamInfoData = metadataHeader.mid(4, length); + + // Search through the remaining metadata + + while(!lastBlock) { + metadataHeader = packet(++ipacket); + + if(metadataHeader.isNull()) + return; + + header = metadataHeader.mid(0, 4); + blockType = header[0] & 0x7f; + lastBlock = (header[0] & 0x80) != 0; + length = header.toUInt(1, 3, true); + overhead += length; + + if(blockType == 1) { + // debug("Ogg::FLAC::File::scan() -- Padding found"); + } + else if(blockType == 4) { + // debug("Ogg::FLAC::File::scan() -- Vorbis-comments found"); + d->xiphCommentData = metadataHeader.mid(4, length); + d->hasXiphComment = true; + d->commentPacket = ipacket; + } + else if(blockType > 5) { + debug("Ogg::FLAC::File::scan() -- Unknown metadata block"); + } + } + + // End of metadata, now comes the datastream + d->streamStart = overhead; + d->streamLength = File::length() - d->streamStart; + + d->scanned = true; +} diff --git a/taglib/taglib/ogg/flac/oggflacfile.h b/taglib/taglib/ogg/flac/oggflacfile.h new file mode 100644 index 0000000..8d87e48 --- /dev/null +++ b/taglib/taglib/ogg/flac/oggflacfile.h @@ -0,0 +1,162 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_OGGFLACFILE_H +#define TAGLIB_OGGFLACFILE_H + +#include "taglib_export.h" +#include "oggfile.h" +#include "xiphcomment.h" + +#include "flacproperties.h" + +namespace TagLib { + + class Tag; + + namespace Ogg { + + //! An implementation of Ogg FLAC metadata + + /*! + * This is implementation of FLAC metadata for Ogg FLAC files. For "pure" + * FLAC files look under the FLAC hiearchy. + * + * Unlike "pure" FLAC-files, Ogg FLAC only supports Xiph-comments, + * while the audio-properties are the same. + */ + namespace FLAC { + + using TagLib::FLAC::Properties; + + //! An implementation of TagLib::File with Ogg/FLAC specific methods + + /*! + * This implements and provides an interface for Ogg/FLAC files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to Ogg FLAC files. + */ + + class TAGLIB_EXPORT File : public Ogg::File + { + public: + /*! + * Constructs an Ogg/FLAC file from \a file. If \a readProperties is true + * the file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an Ogg/FLAC file from \a stream. If \a readProperties is true + * the file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the Tag for this file. This will always be a XiphComment. + * + * \note This always returns a valid pointer regardless of whether or not + * the file on disk has a XiphComment. Use hasXiphComment() to check if + * the file on disk actually has a XiphComment. + * + * \note The Tag <b>is still</b> owned by the FLAC::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasXiphComment() + */ + virtual XiphComment *tag() const; + + /*! + * Returns the FLAC::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + + /*! + * Implements the unified property interface -- export function. + * This forwards directly to XiphComment::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified tag dictionary interface -- import function. + * Like properties(), this is a forwarder to the file's XiphComment. + */ + PropertyMap setProperties(const PropertyMap &); + + + /*! + * Save the file. This will primarily save and update the XiphComment. + * Returns true if the save is successful. + */ + virtual bool save(); + + /*! + * Returns the length of the audio-stream, used by FLAC::Properties for + * calculating the bitrate. + */ + long streamLength(); + + /*! + * Returns whether or not the file on disk actually has a XiphComment. + * + * \see tag() + */ + bool hasXiphComment() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + void scan(); + ByteVector streamInfoData(); + ByteVector xiphCommentData(); + + class FilePrivate; + FilePrivate *d; + }; + } // namespace FLAC + } // namespace Ogg +} // namespace TagLib + +#endif diff --git a/taglib/taglib/ogg/oggfile.cpp b/taglib/taglib/ogg/oggfile.cpp new file mode 100644 index 0000000..cc30f9c --- /dev/null +++ b/taglib/taglib/ogg/oggfile.cpp @@ -0,0 +1,432 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevectorlist.h> +#include <tmap.h> +#include <tstring.h> +#include <tdebug.h> + +#include "oggfile.h" +#include "oggpage.h" +#include "oggpageheader.h" + +using namespace TagLib; + +class Ogg::File::FilePrivate +{ +public: + FilePrivate() : + streamSerialNumber(0), + firstPageHeader(0), + lastPageHeader(0), + currentPage(0), + currentPacketPage(0) + { + pages.setAutoDelete(true); + } + + ~FilePrivate() + { + delete firstPageHeader; + delete lastPageHeader; + } + + uint streamSerialNumber; + List<Page *> pages; + PageHeader *firstPageHeader; + PageHeader *lastPageHeader; + std::vector< List<int> > packetToPageMap; + Map<int, ByteVector> dirtyPackets; + List<int> dirtyPages; + + //! The current page for the reader -- used by nextPage() + Page *currentPage; + //! The current page for the packet parser -- used by packet() + Page *currentPacketPage; + //! The packets for the currentPacketPage -- used by packet() + ByteVectorList currentPackets; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Ogg::File::~File() +{ + delete d; +} + +ByteVector Ogg::File::packet(uint i) +{ + // Check to see if we're called setPacket() for this packet since the last + // save: + + if(d->dirtyPackets.contains(i)) + return d->dirtyPackets[i]; + + // If we haven't indexed the page where the packet we're interested in starts, + // begin reading pages until we have. + + while(d->packetToPageMap.size() <= i) { + if(!nextPage()) { + debug("Ogg::File::packet() -- Could not find the requested packet."); + return ByteVector::null; + } + } + + // Start reading at the first page that contains part (or all) of this packet. + // If the last read stopped at the packet that we're interested in, don't + // reread its packet list. (This should make sequential packet reads fast.) + + uint pageIndex = d->packetToPageMap[i].front(); + if(d->currentPacketPage != d->pages[pageIndex]) { + d->currentPacketPage = d->pages[pageIndex]; + d->currentPackets = d->currentPacketPage->packets(); + } + + // If the packet is completely contained in the first page that it's in, then + // just return it now. + + if(d->currentPacketPage->containsPacket(i) & Page::CompletePacket) + return d->currentPackets[i - d->currentPacketPage->firstPacketIndex()]; + + // If the packet is *not* completely contained in the first page that it's a + // part of then that packet trails off the end of the page. Continue appending + // the pages' packet data until we hit a page that either does not end with the + // packet that we're fetching or where the last packet is complete. + + ByteVector packet = d->currentPackets.back(); + while(d->currentPacketPage->containsPacket(i) & Page::EndsWithPacket && + !d->currentPacketPage->header()->lastPacketCompleted()) + { + pageIndex++; + if(pageIndex == d->pages.size()) { + if(!nextPage()) { + debug("Ogg::File::packet() -- Could not find the requested packet."); + return ByteVector::null; + } + } + d->currentPacketPage = d->pages[pageIndex]; + d->currentPackets = d->currentPacketPage->packets(); + packet.append(d->currentPackets.front()); + } + + return packet; +} + +void Ogg::File::setPacket(uint i, const ByteVector &p) +{ + while(d->packetToPageMap.size() <= i) { + if(!nextPage()) { + debug("Ogg::File::setPacket() -- Could not set the requested packet."); + return; + } + } + + List<int>::ConstIterator it = d->packetToPageMap[i].begin(); + for(; it != d->packetToPageMap[i].end(); ++it) + d->dirtyPages.sortedInsert(*it, true); + + d->dirtyPackets.insert(i, p); +} + +const Ogg::PageHeader *Ogg::File::firstPageHeader() +{ + if(d->firstPageHeader) + return d->firstPageHeader->isValid() ? d->firstPageHeader : 0; + + long firstPageHeaderOffset = find("OggS"); + + if(firstPageHeaderOffset < 0) + return 0; + + d->firstPageHeader = new PageHeader(this, firstPageHeaderOffset); + return d->firstPageHeader->isValid() ? d->firstPageHeader : 0; +} + +const Ogg::PageHeader *Ogg::File::lastPageHeader() +{ + if(d->lastPageHeader) + return d->lastPageHeader->isValid() ? d->lastPageHeader : 0; + + long lastPageHeaderOffset = rfind("OggS"); + + if(lastPageHeaderOffset < 0) + return 0; + + d->lastPageHeader = new PageHeader(this, lastPageHeaderOffset); + return d->lastPageHeader->isValid() ? d->lastPageHeader : 0; +} + +bool Ogg::File::save() +{ + if(readOnly()) { + debug("Ogg::File::save() - Cannot save to a read only file."); + return false; + } + + List<int> pageGroup; + + for(List<int>::ConstIterator it = d->dirtyPages.begin(); it != d->dirtyPages.end(); ++it) { + if(!pageGroup.isEmpty() && pageGroup.back() + 1 != *it) { + writePageGroup(pageGroup); + pageGroup.clear(); + } + else + pageGroup.append(*it); + } + writePageGroup(pageGroup); + d->dirtyPages.clear(); + d->dirtyPackets.clear(); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +Ogg::File::File(FileName file) : TagLib::File(file) +{ + d = new FilePrivate; +} + +Ogg::File::File(IOStream *stream) : TagLib::File(stream) +{ + d = new FilePrivate; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +bool Ogg::File::nextPage() +{ + long nextPageOffset; + int currentPacket; + + if(d->pages.isEmpty()) { + currentPacket = 0; + nextPageOffset = find("OggS"); + if(nextPageOffset < 0) + return false; + } + else { + if(d->currentPage->header()->lastPageOfStream()) + return false; + + if(d->currentPage->header()->lastPacketCompleted()) + currentPacket = d->currentPage->firstPacketIndex() + d->currentPage->packetCount(); + else + currentPacket = d->currentPage->firstPacketIndex() + d->currentPage->packetCount() - 1; + + nextPageOffset = d->currentPage->fileOffset() + d->currentPage->size(); + } + + // Read the next page and add it to the page list. + + d->currentPage = new Page(this, nextPageOffset); + + if(!d->currentPage->header()->isValid()) { + delete d->currentPage; + d->currentPage = 0; + return false; + } + + d->currentPage->setFirstPacketIndex(currentPacket); + + if(d->pages.isEmpty()) + d->streamSerialNumber = d->currentPage->header()->streamSerialNumber(); + + d->pages.append(d->currentPage); + + // Loop through the packets in the page that we just read appending the + // current page number to the packet to page map for each packet. + + for(uint i = 0; i < d->currentPage->packetCount(); i++) { + uint currentPacket = d->currentPage->firstPacketIndex() + i; + if(d->packetToPageMap.size() <= currentPacket) + d->packetToPageMap.push_back(List<int>()); + d->packetToPageMap[currentPacket].append(d->pages.size() - 1); + } + + return true; +} + +void Ogg::File::writePageGroup(const List<int> &thePageGroup) +{ + if(thePageGroup.isEmpty()) + return; + + + // pages in the pageGroup and packets must be equivalent + // (originalSize and size of packets would not work together), + // therefore we sometimes have to add pages to the group + List<int> pageGroup(thePageGroup); + while (!d->pages[pageGroup.back()]->header()->lastPacketCompleted()) { + if (d->currentPage->header()->pageSequenceNumber() == pageGroup.back()) { + if (nextPage() == false) { + debug("broken ogg file"); + return; + } + pageGroup.append(d->currentPage->header()->pageSequenceNumber()); + } else { + pageGroup.append(pageGroup.back() + 1); + } + } + + ByteVectorList packets; + + // If the first page of the group isn't dirty, append its partial content here. + + if(!d->dirtyPages.contains(d->pages[pageGroup.front()]->firstPacketIndex())) + packets.append(d->pages[pageGroup.front()]->packets().front()); + + int previousPacket = -1; + int originalSize = 0; + + for(List<int>::ConstIterator it = pageGroup.begin(); it != pageGroup.end(); ++it) { + uint firstPacket = d->pages[*it]->firstPacketIndex(); + uint lastPacket = firstPacket + d->pages[*it]->packetCount() - 1; + + List<int>::ConstIterator last = --pageGroup.end(); + + for(uint i = firstPacket; i <= lastPacket; i++) { + + if(it == last && i == lastPacket && !d->dirtyPages.contains(i)) + packets.append(d->pages[*it]->packets().back()); + else if(int(i) != previousPacket) { + previousPacket = i; + packets.append(packet(i)); + } + } + originalSize += d->pages[*it]->size(); + } + + const bool continued = d->pages[pageGroup.front()]->header()->firstPacketContinued(); + const bool completed = d->pages[pageGroup.back()]->header()->lastPacketCompleted(); + + // TODO: This pagination method isn't accurate for what's being done here. + // This should account for real possibilities like non-aligned packets and such. + + List<Page *> pages = Page::paginate(packets, Page::SinglePagePerGroup, + d->streamSerialNumber, pageGroup.front(), + continued, completed); + + List<Page *> renumberedPages; + + // Correct the page numbering of following pages + + if (pages.back()->header()->pageSequenceNumber() != pageGroup.back()) { + + // TODO: change the internal data structure so that we don't need to hold the + // complete file in memory (is unavoidable at the moment) + + // read the complete stream + while(!d->currentPage->header()->lastPageOfStream()) { + if(nextPage() == false) { + debug("broken ogg file"); + break; + } + } + + // create a gap for the new pages + int numberOfNewPages = pages.back()->header()->pageSequenceNumber() - pageGroup.back(); + List<Page *>::Iterator pageIter = d->pages.begin(); + for(int i = 0; i < pageGroup.back(); i++) { + if(pageIter != d->pages.end()) { + ++pageIter; + } + else { + debug("Ogg::File::writePageGroup() -- Page sequence is broken in original file."); + break; + } + } + + ++pageIter; + for(; pageIter != d->pages.end(); ++pageIter) { + Ogg::Page *newPage = + (*pageIter)->getCopyWithNewPageSequenceNumber( + (*pageIter)->header()->pageSequenceNumber() + numberOfNewPages); + + ByteVector data; + data.append(newPage->render()); + insert(data, newPage->fileOffset(), data.size()); + + renumberedPages.append(newPage); + } + } + + // insert the new data + + ByteVector data; + for(List<Page *>::ConstIterator it = pages.begin(); it != pages.end(); ++it) + data.append((*it)->render()); + + // The insertion algorithms could also be improve to queue and prioritize data + // on the way out. Currently it requires rewriting the file for every page + // group rather than just once; however, for tagging applications there will + // generally only be one page group, so it's not worth the time for the + // optimization at the moment. + + insert(data, d->pages[pageGroup.front()]->fileOffset(), originalSize); + + // Update the page index to include the pages we just created and to delete the + // old pages. + + // First step: Pages that contain the comment data + + for(List<Page *>::ConstIterator it = pages.begin(); it != pages.end(); ++it) { + const unsigned int index = (*it)->header()->pageSequenceNumber(); + if(index < d->pages.size()) { + delete d->pages[index]; + d->pages[index] = *it; + } + else if(index == d->pages.size()) { + d->pages.append(*it); + } + else { + // oops - there's a hole in the sequence + debug("Ogg::File::writePageGroup() -- Page sequence is broken."); + } + } + + // Second step: the renumbered pages + + for(List<Page *>::ConstIterator it = renumberedPages.begin(); it != renumberedPages.end(); ++it) { + const unsigned int index = (*it)->header()->pageSequenceNumber(); + if(index < d->pages.size()) { + delete d->pages[index]; + d->pages[index] = *it; + } + else if(index == d->pages.size()) { + d->pages.append(*it); + } + else { + // oops - there's a hole in the sequence + debug("Ogg::File::writePageGroup() -- Page sequence is broken."); + } + } +} diff --git a/taglib/taglib/ogg/oggfile.h b/taglib/taglib/ogg/oggfile.h new file mode 100644 index 0000000..8fed4ba --- /dev/null +++ b/taglib/taglib/ogg/oggfile.h @@ -0,0 +1,122 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "taglib_export.h" +#include "tfile.h" +#include "tbytevectorlist.h" + +#ifndef TAGLIB_OGGFILE_H +#define TAGLIB_OGGFILE_H + +namespace TagLib { + + //! A namespace for the classes used by Ogg-based metadata files + + namespace Ogg { + + class PageHeader; + + //! An implementation of TagLib::File with some helpers for Ogg based formats + + /*! + * This is an implementation of Ogg file page and packet rendering and is of + * use to Ogg based formats. While the API is small this handles the + * non-trivial details of breaking up an Ogg stream into packets and makes + * these available (via subclassing) to the codec meta data implementations. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + virtual ~File(); + + /*! + * Returns the packet contents for the i-th packet (starting from zero) + * in the Ogg bitstream. + * + * \warning The requires reading at least the packet header for every page + * up to the requested page. + */ + ByteVector packet(uint i); + + /*! + * Sets the packet with index \a i to the value \a p. + */ + void setPacket(uint i, const ByteVector &p); + + /*! + * Returns a pointer to the PageHeader for the first page in the stream or + * null if the page could not be found. + */ + const PageHeader *firstPageHeader(); + + /*! + * Returns a pointer to the PageHeader for the last page in the stream or + * null if the page could not be found. + */ + const PageHeader *lastPageHeader(); + + virtual bool save(); + + protected: + /*! + * Constructs an Ogg file from \a file. + * + * \note This constructor is protected since Ogg::File shouldn't be + * instantiated directly but rather should be used through the codec + * specific subclasses. + */ + File(FileName file); + + /*! + * Constructs an Ogg file from \a stream. + * + * \note This constructor is protected since Ogg::File shouldn't be + * instantiated directly but rather should be used through the codec + * specific subclasses. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + */ + File(IOStream *stream); + + private: + File(const File &); + File &operator=(const File &); + + /*! + * Reads the next page and updates the internal "current page" pointer. + */ + bool nextPage(); + void writePageGroup(const List<int> &group); + + class FilePrivate; + FilePrivate *d; + }; + + } +} + +#endif diff --git a/taglib/taglib/ogg/oggpage.cpp b/taglib/taglib/ogg/oggpage.cpp new file mode 100644 index 0000000..e5f767e --- /dev/null +++ b/taglib/taglib/ogg/oggpage.cpp @@ -0,0 +1,340 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> + +#include "oggpage.h" +#include "oggpageheader.h" +#include "oggfile.h" + +using namespace TagLib; + +class Ogg::Page::PagePrivate +{ +public: + PagePrivate(File *f = 0, long pageOffset = -1) : + file(f), + fileOffset(pageOffset), + packetOffset(0), + header(f, pageOffset), + firstPacketIndex(-1) + { + if(file) { + packetOffset = fileOffset + header.size(); + packetSizes = header.packetSizes(); + dataSize = header.dataSize(); + } + } + + File *file; + long fileOffset; + long packetOffset; + int dataSize; + List<int> packetSizes; + PageHeader header; + int firstPacketIndex; + ByteVectorList packets; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Ogg::Page::Page(Ogg::File *file, long pageOffset) +{ + d = new PagePrivate(file, pageOffset); +} + +Ogg::Page::~Page() +{ + delete d; +} + +long Ogg::Page::fileOffset() const +{ + return d->fileOffset; +} + +const Ogg::PageHeader *Ogg::Page::header() const +{ + return &d->header; +} + +int Ogg::Page::firstPacketIndex() const +{ + return d->firstPacketIndex; +} + +void Ogg::Page::setFirstPacketIndex(int index) +{ + d->firstPacketIndex = index; +} + +Ogg::Page::ContainsPacketFlags Ogg::Page::containsPacket(int index) const +{ + int lastPacketIndex = d->firstPacketIndex + packetCount() - 1; + if(index < d->firstPacketIndex || index > lastPacketIndex) + return DoesNotContainPacket; + + ContainsPacketFlags flags = DoesNotContainPacket; + + if(index == d->firstPacketIndex) + flags = ContainsPacketFlags(flags | BeginsWithPacket); + + if(index == lastPacketIndex) + flags = ContainsPacketFlags(flags | EndsWithPacket); + + // If there's only one page and it's complete: + + if(packetCount() == 1 && + !d->header.firstPacketContinued() && + d->header.lastPacketCompleted()) + { + flags = ContainsPacketFlags(flags | CompletePacket); + } + + // Or if there is more than one page and the page is + // (a) the first page and it's complete or + // (b) the last page and it's complete or + // (c) a page in the middle. + else if(packetCount() > 1 && + ((flags & BeginsWithPacket && !d->header.firstPacketContinued()) || + (flags & EndsWithPacket && d->header.lastPacketCompleted()) || + (!(flags & BeginsWithPacket) && !(flags & EndsWithPacket)))) + { + flags = ContainsPacketFlags(flags | CompletePacket); + } + + return flags; +} + +TagLib::uint Ogg::Page::packetCount() const +{ + return d->header.packetSizes().size(); +} + +ByteVectorList Ogg::Page::packets() const +{ + if(!d->packets.isEmpty()) + return d->packets; + + ByteVectorList l; + + if(d->file && d->header.isValid()) { + + d->file->seek(d->packetOffset); + + List<int> packetSizes = d->header.packetSizes(); + + List<int>::ConstIterator it = packetSizes.begin(); + for(; it != packetSizes.end(); ++it) + l.append(d->file->readBlock(*it)); + } + else + debug("Ogg::Page::packets() -- attempting to read packets from an invalid page."); + + return l; +} + +int Ogg::Page::size() const +{ + return d->header.size() + d->header.dataSize(); +} + +ByteVector Ogg::Page::render() const +{ + ByteVector data; + + data.append(d->header.render()); + + if(d->packets.isEmpty()) { + if(d->file) { + d->file->seek(d->packetOffset); + data.append(d->file->readBlock(d->dataSize)); + } + else + debug("Ogg::Page::render() -- this page is empty!"); + } + else { + ByteVectorList::ConstIterator it = d->packets.begin(); + for(; it != d->packets.end(); ++it) + data.append(*it); + } + + // Compute and set the checksum for the Ogg page. The checksum is taken over + // the entire page with the 4 bytes reserved for the checksum zeroed and then + // inserted in bytes 22-25 of the page header. + + ByteVector checksum = ByteVector::fromUInt(data.checksum(), false); + for(int i = 0; i < 4; i++) + data[i + 22] = checksum[i]; + + return data; +} + +List<Ogg::Page *> Ogg::Page::paginate(const ByteVectorList &packets, + PaginationStrategy strategy, + uint streamSerialNumber, + int firstPage, + bool firstPacketContinued, + bool lastPacketCompleted, + bool containsLastPacket) +{ + List<Page *> l; + + int totalSize = 0; + + for(ByteVectorList::ConstIterator it = packets.begin(); it != packets.end(); ++it) + totalSize += (*it).size(); + + // Handle creation of multiple pages with appropriate pagination. + if(strategy == Repaginate || totalSize + packets.size() > 255 * 255) { + + // SPLITSIZE must be a multiple of 255 in order to get the lacing values right + // create pages of about 8KB each +#define SPLITSIZE (32*255) + + int pageIndex = 0; + + for(ByteVectorList::ConstIterator it = packets.begin(); it != packets.end(); ++it) { + bool continued = false; + + // mark very first packet? + if(firstPacketContinued && it==packets.begin()) { + continued = true; + } + + // append to buf + ByteVector packetBuf; + packetBuf.append(*it); + + while(packetBuf.size() > SPLITSIZE) { + // output a Page + ByteVector packetForOnePage; + packetForOnePage.resize(SPLITSIZE); + std::copy(packetBuf.begin(), packetBuf.begin() + SPLITSIZE, packetForOnePage.begin()); + + ByteVectorList packetList; + packetList.append(packetForOnePage); + Page *p = new Page(packetList, streamSerialNumber, firstPage+pageIndex, continued, false, false); + l.append(p); + + pageIndex++; + continued = true; + packetBuf = packetBuf.mid(SPLITSIZE); + } + + ByteVectorList::ConstIterator jt = it; + ++jt; + bool lastPacketInList = (jt == packets.end()); + + // output a page for the rest (we output one packet per page, so this one should be completed) + ByteVectorList packetList; + packetList.append(packetBuf); + + bool isVeryLastPacket = false; + if(containsLastPacket) { + // mark the very last output page as last of stream + ByteVectorList::ConstIterator jt = it; + ++jt; + if(jt == packets.end()) { + isVeryLastPacket = true; + } + } + + Page *p = new Page(packetList, streamSerialNumber, firstPage+pageIndex, continued, + lastPacketInList ? lastPacketCompleted : true, + isVeryLastPacket); + pageIndex++; + + l.append(p); + } + } + else { + Page *p = new Page(packets, streamSerialNumber, firstPage, firstPacketContinued, + lastPacketCompleted, containsLastPacket); + l.append(p); + } + + return l; +} + +Ogg::Page* Ogg::Page::getCopyWithNewPageSequenceNumber(int sequenceNumber) +{ + Page *pResultPage = NULL; + + // TODO: a copy constructor would be helpful + + if(d->file == 0) { + pResultPage = new Page( + d->packets, + d->header.streamSerialNumber(), + sequenceNumber, + d->header.firstPacketContinued(), + d->header.lastPacketCompleted(), + d->header.lastPageOfStream()); + } + else + { + pResultPage = new Page(d->file, d->fileOffset); + pResultPage->d->header.setPageSequenceNumber(sequenceNumber); + } + return pResultPage; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +Ogg::Page::Page(const ByteVectorList &packets, + uint streamSerialNumber, + int pageNumber, + bool firstPacketContinued, + bool lastPacketCompleted, + bool containsLastPacket) +{ + d = new PagePrivate; + + ByteVector data; + List<int> packetSizes; + + d->header.setFirstPageOfStream(pageNumber == 0 && !firstPacketContinued); + d->header.setLastPageOfStream(containsLastPacket); + d->header.setFirstPacketContinued(firstPacketContinued); + d->header.setLastPacketCompleted(lastPacketCompleted); + d->header.setStreamSerialNumber(streamSerialNumber); + d->header.setPageSequenceNumber(pageNumber); + + // Build a page from the list of packets. + + for(ByteVectorList::ConstIterator it = packets.begin(); it != packets.end(); ++it) { + packetSizes.append((*it).size()); + data.append(*it); + } + d->packets = packets; + d->header.setPacketSizes(packetSizes); +} + diff --git a/taglib/taglib/ogg/oggpage.h b/taglib/taglib/ogg/oggpage.h new file mode 100644 index 0000000..e9f4840 --- /dev/null +++ b/taglib/taglib/ogg/oggpage.h @@ -0,0 +1,211 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_OGGPAGE_H +#define TAGLIB_OGGPAGE_H + +#include "taglib_export.h" +#include "tbytevectorlist.h" + +namespace TagLib { + + namespace Ogg { + + class File; + class PageHeader; + + //! An implementation of Ogg pages + + /*! + * This is an implementation of the pages that make up an Ogg stream. + * This handles parsing pages and breaking them down into packets and handles + * the details of packets spanning multiple pages and pages that contiain + * multiple packets. + * + * In most Xiph.org formats the comments are found in the first few packets, + * this however is a reasonably complete implementation of Ogg pages that + * could potentially be useful for non-meta data purposes. + */ + + class TAGLIB_EXPORT Page + { + public: + /*! + * Read an Ogg page from the \a file at the position \a pageOffset. + */ + Page(File *file, long pageOffset); + + virtual ~Page(); + + /*! + * Returns the page's position within the file (in bytes). + */ + long fileOffset() const; + + /*! + * Returns a pointer to the header for this page. This pointer will become + * invalid when the page is deleted. + */ + const PageHeader *header() const; + + /*! + * Returns a copy of the page with \a sequenceNumber set as sequence number. + * + * \see header() + * \see PageHeader::setPageSequenceNumber() + */ + Page* getCopyWithNewPageSequenceNumber(int sequenceNumber); + + /*! + * Returns the index of the first packet wholly or partially contained in + * this page. + * + * \see setFirstPacketIndex() + */ + int firstPacketIndex() const; + + /*! + * Sets the index of the first packet in the page. + * + * \see firstPacketIndex() + */ + void setFirstPacketIndex(int index); + + /*! + * When checking to see if a page contains a given packet this set of flags + * represents the possible values for that packets status in the page. + * + * \see containsPacket() + */ + enum ContainsPacketFlags { + //! No part of the packet is contained in the page + DoesNotContainPacket = 0x0000, + //! The packet is wholly contained in the page + CompletePacket = 0x0001, + //! The page starts with the given packet + BeginsWithPacket = 0x0002, + //! The page ends with the given packet + EndsWithPacket = 0x0004 + }; + + /*! + * Checks to see if the specified \a packet is contained in the current + * page. + * + * \see ContainsPacketFlags + */ + ContainsPacketFlags containsPacket(int index) const; + + /*! + * Returns the number of packets (whole or partial) in this page. + */ + uint packetCount() const; + + /*! + * Returns a list of the packets in this page. + * + * \note Either or both the first and last packets may be only partial. + * \see PageHeader::firstPacketContinued() + */ + ByteVectorList packets() const; + + /*! + * Returns the size of the page in bytes. + */ + int size() const; + + ByteVector render() const; + + /*! + * Defines a strategy for pagination, or grouping pages into Ogg packets, + * for use with pagination methods. + * + * \note Yes, I'm aware that this is not a canonical "Strategy Pattern", + * the term was simply convenient. + */ + enum PaginationStrategy { + /*! + * Attempt to put the specified set of packets into a single Ogg packet. + * If the sum of the packet data is greater than will fit into a single + * Ogg page -- 65280 bytes -- this will fall back to repagination using + * the recommended page sizes. + */ + SinglePagePerGroup, + /*! + * Split the packet or group of packets into pages that conform to the + * sizes recommended in the Ogg standard. + */ + Repaginate + }; + + /*! + * Pack \a packets into Ogg pages using the \a strategy for pagination. + * The page number indicater inside of the rendered packets will start + * with \a firstPage and be incremented for each page rendered. + * \a containsLastPacket should be set to true if \a packets contains the + * last page in the stream and will set the appropriate flag in the last + * rendered Ogg page's header. \a streamSerialNumber should be set to + * the serial number for this stream. + * + * \note The "absolute granule position" is currently always zeroed using + * this method as this suffices for the comment headers. + * + * \warning The pages returned by this method must be deleted by the user. + * You can use List<T>::setAutoDelete(true) to set these pages to be + * automatically deleted when this list passes out of scope. + * + * \see PaginationStrategy + * \see List::setAutoDelete() + */ + static List<Page *> paginate(const ByteVectorList &packets, + PaginationStrategy strategy, + uint streamSerialNumber, + int firstPage, + bool firstPacketContinued = false, + bool lastPacketCompleted = true, + bool containsLastPacket = false); + + protected: + /*! + * Creates an Ogg packet based on the data in \a packets. The page number + * for each page will be set to \a pageNumber. + */ + Page(const ByteVectorList &packets, + uint streamSerialNumber, + int pageNumber, + bool firstPacketContinued = false, + bool lastPacketCompleted = true, + bool containsLastPacket = false); + + private: + Page(const Page &); + Page &operator=(const Page &); + + class PagePrivate; + PagePrivate *d; + }; + } +} +#endif diff --git a/taglib/taglib/ogg/oggpageheader.cpp b/taglib/taglib/ogg/oggpageheader.cpp new file mode 100644 index 0000000..b933313 --- /dev/null +++ b/taglib/taglib/ogg/oggpageheader.cpp @@ -0,0 +1,323 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <stdlib.h> + +#include <bitset> + +#include <tstring.h> +#include <tdebug.h> +#include <taglib.h> + +#include "oggpageheader.h" +#include "oggfile.h" + +using namespace TagLib; + +class Ogg::PageHeader::PageHeaderPrivate +{ +public: + PageHeaderPrivate(File *f, long pageOffset) : + file(f), + fileOffset(pageOffset), + isValid(false), + firstPacketContinued(false), + lastPacketCompleted(false), + firstPageOfStream(false), + lastPageOfStream(false), + absoluteGranularPosition(0), + streamSerialNumber(0), + pageSequenceNumber(-1), + size(0), + dataSize(0) + {} + + File *file; + long fileOffset; + bool isValid; + List<int> packetSizes; + bool firstPacketContinued; + bool lastPacketCompleted; + bool firstPageOfStream; + bool lastPageOfStream; + long long absoluteGranularPosition; + uint streamSerialNumber; + int pageSequenceNumber; + int size; + int dataSize; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Ogg::PageHeader::PageHeader(Ogg::File *file, long pageOffset) +{ + d = new PageHeaderPrivate(file, pageOffset); + if(file && pageOffset >= 0) + read(); +} + +Ogg::PageHeader::~PageHeader() +{ + delete d; +} + +bool Ogg::PageHeader::isValid() const +{ + return d->isValid; +} + +List<int> Ogg::PageHeader::packetSizes() const +{ + return d->packetSizes; +} + +void Ogg::PageHeader::setPacketSizes(const List<int> &sizes) +{ + d->packetSizes = sizes; +} + +bool Ogg::PageHeader::firstPacketContinued() const +{ + return d->firstPacketContinued; +} + +void Ogg::PageHeader::setFirstPacketContinued(bool continued) +{ + d->firstPacketContinued = continued; +} + +bool Ogg::PageHeader::lastPacketCompleted() const +{ + return d->lastPacketCompleted; +} + +void Ogg::PageHeader::setLastPacketCompleted(bool completed) +{ + d->lastPacketCompleted = completed; +} + +bool Ogg::PageHeader::firstPageOfStream() const +{ + return d->firstPageOfStream; +} + +void Ogg::PageHeader::setFirstPageOfStream(bool first) +{ + d->firstPageOfStream = first; +} + +bool Ogg::PageHeader::lastPageOfStream() const +{ + return d->lastPageOfStream; +} + +void Ogg::PageHeader::setLastPageOfStream(bool last) +{ + d->lastPageOfStream = last; +} + +long long Ogg::PageHeader::absoluteGranularPosition() const +{ + return d->absoluteGranularPosition; +} + +void Ogg::PageHeader::setAbsoluteGranularPosition(long long agp) +{ + d->absoluteGranularPosition = agp; +} + +int Ogg::PageHeader::pageSequenceNumber() const +{ + return d->pageSequenceNumber; +} + +void Ogg::PageHeader::setPageSequenceNumber(int sequenceNumber) +{ + d->pageSequenceNumber = sequenceNumber; +} + +TagLib::uint Ogg::PageHeader::streamSerialNumber() const +{ + return d->streamSerialNumber; +} + +void Ogg::PageHeader::setStreamSerialNumber(uint n) +{ + d->streamSerialNumber = n; +} + +int Ogg::PageHeader::size() const +{ + return d->size; +} + +int Ogg::PageHeader::dataSize() const +{ + return d->dataSize; +} + +ByteVector Ogg::PageHeader::render() const +{ + ByteVector data; + + // capture patern + + data.append("OggS"); + + // stream structure version + + data.append(char(0)); + + // header type flag + + std::bitset<8> flags; + flags[0] = d->firstPacketContinued; + flags[1] = d->pageSequenceNumber == 0; + flags[2] = d->lastPageOfStream; + + data.append(char(flags.to_ulong())); + + // absolute granular position + + data.append(ByteVector::fromLongLong(d->absoluteGranularPosition, false)); + + // stream serial number + + data.append(ByteVector::fromUInt(d->streamSerialNumber, false)); + + // page sequence number + + data.append(ByteVector::fromUInt(d->pageSequenceNumber, false)); + + // checksum -- this is left empty and should be filled in by the Ogg::Page + // class + + data.append(ByteVector(4, 0)); + + // page segment count and page segment table + + ByteVector pageSegments = lacingValues(); + + data.append(char(uchar(pageSegments.size()))); + data.append(pageSegments); + + return data; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Ogg::PageHeader::read() +{ + d->file->seek(d->fileOffset); + + // An Ogg page header is at least 27 bytes, so we'll go ahead and read that + // much and then get the rest when we're ready for it. + + ByteVector data = d->file->readBlock(27); + + // Sanity check -- make sure that we were in fact able to read as much data as + // we asked for and that the page begins with "OggS". + + if(data.size() != 27 || !data.startsWith("OggS")) { + debug("Ogg::PageHeader::read() -- error reading page header"); + return; + } + + std::bitset<8> flags(data[5]); + + d->firstPacketContinued = flags.test(0); + d->firstPageOfStream = flags.test(1); + d->lastPageOfStream = flags.test(2); + + d->absoluteGranularPosition = data.toLongLong(6, false); + d->streamSerialNumber = data.toUInt(14, false); + d->pageSequenceNumber = data.toUInt(18, false); + + // Byte number 27 is the number of page segments, which is the only variable + // length portion of the page header. After reading the number of page + // segments we'll then read in the corresponding data for this count. + + int pageSegmentCount = uchar(data[26]); + + ByteVector pageSegments = d->file->readBlock(pageSegmentCount); + + // Another sanity check. + + if(pageSegmentCount < 1 || int(pageSegments.size()) != pageSegmentCount) + return; + + // The base size of an Ogg page 27 bytes plus the number of lacing values. + + d->size = 27 + pageSegmentCount; + + int packetSize = 0; + + for(int i = 0; i < pageSegmentCount; i++) { + d->dataSize += uchar(pageSegments[i]); + packetSize += uchar(pageSegments[i]); + + if(uchar(pageSegments[i]) < 255) { + d->packetSizes.append(packetSize); + packetSize = 0; + } + } + + if(packetSize > 0) { + d->packetSizes.append(packetSize); + d->lastPacketCompleted = false; + } + else + d->lastPacketCompleted = true; + + d->isValid = true; +} + +ByteVector Ogg::PageHeader::lacingValues() const +{ + ByteVector data; + + List<int> sizes = d->packetSizes; + for(List<int>::ConstIterator it = sizes.begin(); it != sizes.end(); ++it) { + + // The size of a packet in an Ogg page is indicated by a series of "lacing + // values" where the sum of the values is the packet size in bytes. Each of + // these values is a byte. A value of less than 255 (0xff) indicates the end + // of the packet. + + div_t n = div(*it, 255); + + for(int i = 0; i < n.quot; i++) + data.append(char(uchar(255))); + + if(it != --sizes.end() || d->lastPacketCompleted) + data.append(char(uchar(n.rem))); + } + + return data; +} diff --git a/taglib/taglib/ogg/oggpageheader.h b/taglib/taglib/ogg/oggpageheader.h new file mode 100644 index 0000000..742710a --- /dev/null +++ b/taglib/taglib/ogg/oggpageheader.h @@ -0,0 +1,232 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_OGGPAGEHEADER_H +#define TAGLIB_OGGPAGEHEADER_H + +#include "tlist.h" +#include "tbytevector.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace Ogg { + + class File; + + //! An implementation of the page headers associated with each Ogg::Page + + /*! + * This class implements Ogg page headers which contain the information + * about Ogg pages needed to break them into packets which can be passed on + * to the codecs. + */ + + class TAGLIB_EXPORT PageHeader + { + public: + /*! + * Reads a PageHeader from \a file starting at \a pageOffset. The defaults + * create a page with no (and as such, invalid) data that must be set + * later. + */ + PageHeader(File *file = 0, long pageOffset = -1); + + /*! + * Deletes this instance of the PageHeader. + */ + virtual ~PageHeader(); + + /*! + * Returns true if the header parsed properly and is valid. + */ + bool isValid() const; + + /*! + * Ogg pages contain a list of packets (which are used by the contained + * codecs). The sizes of these pages is encoded in the page header. This + * returns a list of the packet sizes in bytes. + * + * \see setPacketSizes() + */ + List<int> packetSizes() const; + + /*! + * Sets the sizes of the packets in this page to \a sizes. Internally this + * updates the lacing values in the header. + * + * \see packetSizes() + */ + void setPacketSizes(const List<int> &sizes); + + /*! + * Some packets can be <i>continued</i> across multiple pages. If the + * first packet in the current page is a continuation this will return + * true. If this is page starts with a new packet this will return false. + * + * \see lastPacketCompleted() + * \see setFirstPacketContinued() + */ + bool firstPacketContinued() const; + + /*! + * Sets the internal flag indicating if the first packet in this page is + * continued to \a continued. + * + * \see firstPacketContinued() + */ + void setFirstPacketContinued(bool continued); + + /*! + * Returns true if the last packet of this page is completely contained in + * this page. + * + * \see firstPacketContinued() + * \see setLastPacketCompleted() + */ + bool lastPacketCompleted() const; + + /*! + * Sets the internal flag indicating if the last packet in this page is + * complete to \a completed. + * + * \see lastPacketCompleted() + */ + void setLastPacketCompleted(bool completed); + + /*! + * This returns true if this is the first page of the Ogg (logical) stream. + * + * \see setFirstPageOfStream() + */ + bool firstPageOfStream() const; + + /*! + * Marks this page as the first page of the Ogg stream. + * + * \see firstPageOfStream() + */ + void setFirstPageOfStream(bool first); + + /*! + * This returns true if this is the last page of the Ogg (logical) stream. + * + * \see setLastPageOfStream() + */ + bool lastPageOfStream() const; + + /*! + * Marks this page as the last page of the Ogg stream. + * + * \see lastPageOfStream() + */ + void setLastPageOfStream(bool last); + + /*! + * A special value of containing the position of the packet to be + * interpreted by the codec. In the case of Vorbis this contains the PCM + * value and is used to calculate the length of the stream. + * + * \see setAbsoluteGranularPosition() + */ + long long absoluteGranularPosition() const; + + /*! + * A special value of containing the position of the packet to be + * interpreted by the codec. It is only supported here so that it may be + * coppied from one page to another. + * + * \see absoluteGranularPosition() + */ + void setAbsoluteGranularPosition(long long agp); + + /*! + * Every Ogg logical stream is given a random serial number which is common + * to every page in that logical stream. This returns the serial number of + * the stream associated with this packet. + * + * \see setStreamSerialNumber() + */ + uint streamSerialNumber() const; + + /*! + * Every Ogg logical stream is given a random serial number which is common + * to every page in that logical stream. This sets this pages serial + * number. This method should be used when adding new pages to a logical + * stream. + * + * \see streamSerialNumber() + */ + void setStreamSerialNumber(uint n); + + /*! + * Returns the index of the page within the Ogg stream. This helps make it + * possible to determine if pages have been lost. + * + * \see setPageSequenceNumber() + */ + int pageSequenceNumber() const; + + /*! + * Sets the page's position in the stream to \a sequenceNumber. + * + * \see pageSequenceNumber() + */ + void setPageSequenceNumber(int sequenceNumber); + + /*! + * Returns the complete header size. + */ + int size() const; + + /*! + * Returns the size of the data portion of the page -- i.e. the size of the + * page less the header size. + */ + int dataSize() const; + + /*! + * Render the page header to binary data. + * + * \note The checksum -- bytes 22 - 25 -- will be left empty and must be + * filled in when rendering the entire page. + */ + ByteVector render() const; + + private: + PageHeader(const PageHeader &); + PageHeader &operator=(const PageHeader &); + + void read(); + ByteVector lacingValues() const; + + class PageHeaderPrivate; + PageHeaderPrivate *d; + }; + + } +} + +#endif diff --git a/taglib/taglib/ogg/opus/opusfile.cpp b/taglib/taglib/ogg/opus/opusfile.cpp new file mode 100644 index 0000000..cb81a32 --- /dev/null +++ b/taglib/taglib/ogg/opus/opusfile.cpp @@ -0,0 +1,139 @@ +/*************************************************************************** + copyright : (C) 2012 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + (original Vorbis implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <bitset> + +#include <tstring.h> +#include <tdebug.h> +#include <tpropertymap.h> + +#include "opusfile.h" + +using namespace TagLib; +using namespace TagLib::Ogg; + +class Opus::File::FilePrivate +{ +public: + FilePrivate() : + comment(0), + properties(0) {} + + ~FilePrivate() + { + delete comment; + delete properties; + } + + Ogg::XiphComment *comment; + Properties *properties; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Opus::File::File(FileName file, bool readProperties, Properties::ReadStyle propertiesStyle) : + Ogg::File(file), + d(new FilePrivate()) +{ + if(isOpen()) + read(readProperties, propertiesStyle); +} + +Opus::File::File(IOStream *stream, bool readProperties, Properties::ReadStyle propertiesStyle) : + Ogg::File(stream), + d(new FilePrivate()) +{ + if(isOpen()) + read(readProperties, propertiesStyle); +} + +Opus::File::~File() +{ + delete d; +} + +Ogg::XiphComment *Opus::File::tag() const +{ + return d->comment; +} + +PropertyMap Opus::File::properties() const +{ + return d->comment->properties(); +} + +PropertyMap Opus::File::setProperties(const PropertyMap &properties) +{ + return d->comment->setProperties(properties); +} + +Opus::Properties *Opus::File::audioProperties() const +{ + return d->properties; +} + +bool Opus::File::save() +{ + if(!d->comment) + d->comment = new Ogg::XiphComment; + + setPacket(1, ByteVector("OpusTags", 8) + d->comment->render(false)); + + return Ogg::File::save(); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Opus::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + ByteVector opusHeaderData = packet(0); + + if(!opusHeaderData.startsWith("OpusHead")) { + setValid(false); + debug("Opus::File::read() -- invalid Opus identification header"); + return; + } + + ByteVector commentHeaderData = packet(1); + + if(!commentHeaderData.startsWith("OpusTags")) { + setValid(false); + debug("Opus::File::read() -- invalid Opus tags header"); + return; + } + + d->comment = new Ogg::XiphComment(commentHeaderData.mid(8)); + + if(readProperties) + d->properties = new Properties(this, propertiesStyle); +} diff --git a/taglib/taglib/ogg/opus/opusfile.h b/taglib/taglib/ogg/opus/opusfile.h new file mode 100644 index 0000000..275167e --- /dev/null +++ b/taglib/taglib/ogg/opus/opusfile.h @@ -0,0 +1,124 @@ +/*************************************************************************** + copyright : (C) 2012 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + (original Vorbis implementation) +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_OPUSFILE_H +#define TAGLIB_OPUSFILE_H + +#include "oggfile.h" +#include "xiphcomment.h" + +#include "opusproperties.h" + +namespace TagLib { + + namespace Ogg { + + //! A namespace containing classes for Opus metadata + + namespace Opus { + + //! An implementation of Ogg::File with Opus specific methods + + /*! + * This is the central class in the Ogg Opus metadata processing collection + * of classes. It's built upon Ogg::File which handles processing of the Ogg + * logical bitstream and breaking it down into pages which are handled by + * the codec implementations, in this case Opus specifically. + */ + + class TAGLIB_EXPORT File : public Ogg::File + { + public: + /*! + * Constructs an Opus file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an Opus file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the XiphComment for this file. XiphComment implements the tag + * interface, so this serves as the reimplementation of + * TagLib::File::tag(). + */ + virtual Ogg::XiphComment *tag() const; + + /*! + * Implements the unified property interface -- export function. + * This forwards directly to XiphComment::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified tag dictionary interface -- import function. + * Like properties(), this is a forwarder to the file's XiphComment. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the Opus::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + virtual bool save(); + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + + class FilePrivate; + FilePrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/taglib/ogg/opus/opusproperties.cpp b/taglib/taglib/ogg/opus/opusproperties.cpp new file mode 100644 index 0000000..7bdcd39 --- /dev/null +++ b/taglib/taglib/ogg/opus/opusproperties.cpp @@ -0,0 +1,161 @@ +/*************************************************************************** + copyright : (C) 2012 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + (original Vorbis implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> + +#include <oggpageheader.h> + +#include "opusproperties.h" +#include "opusfile.h" + +using namespace TagLib; +using namespace TagLib::Ogg; + +class Opus::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(File *f, ReadStyle s) : + file(f), + style(s), + length(0), + inputSampleRate(0), + channels(0), + opusVersion(0) {} + + File *file; + ReadStyle style; + int length; + int inputSampleRate; + int channels; + int opusVersion; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Opus::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(file, style); + read(); +} + +Opus::Properties::~Properties() +{ + delete d; +} + +int Opus::Properties::length() const +{ + return d->length; +} + +int Opus::Properties::bitrate() const +{ + return 0; +} + +int Opus::Properties::sampleRate() const +{ + // Opus can decode any stream at a sample rate of 8, 12, 16, 24, or 48 kHz, + // so there is no single sample rate. Let's assume it's the highest + // possible. + return 48000; +} + +int Opus::Properties::channels() const +{ + return d->channels; +} + +int Opus::Properties::inputSampleRate() const +{ + return d->inputSampleRate; +} + +int Opus::Properties::opusVersion() const +{ + return d->opusVersion; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Opus::Properties::read() +{ + // Get the identification header from the Ogg implementation. + + // http://tools.ietf.org/html/draft-terriberry-oggopus-01#section-5.1 + + ByteVector data = d->file->packet(0); + + // *Magic Signature* + uint pos = 8; + + // *Version* (8 bits, unsigned) + d->opusVersion = uchar(data.at(pos)); + pos += 1; + + // *Output Channel Count* 'C' (8 bits, unsigned) + d->channels = uchar(data.at(pos)); + pos += 1; + + // *Pre-skip* (16 bits, unsigned, little endian) + const ushort preSkip = data.toUShort(pos, false); + pos += 2; + + // *Input Sample Rate* (32 bits, unsigned, little endian) + d->inputSampleRate = data.toUInt(pos, false); + pos += 4; + + // *Output Gain* (16 bits, signed, little endian) + pos += 2; + + // *Channel Mapping Family* (8 bits, unsigned) + pos += 1; + + const Ogg::PageHeader *first = d->file->firstPageHeader(); + const Ogg::PageHeader *last = d->file->lastPageHeader(); + + if(first && last) { + long long start = first->absoluteGranularPosition(); + long long end = last->absoluteGranularPosition(); + + if(start >= 0 && end >= 0) + d->length = (int) ((end - start - preSkip) / 48000); + else { + debug("Opus::Properties::read() -- The PCM values for the start or " + "end of this file was incorrect."); + } + } + else + debug("Opus::Properties::read() -- Could not find valid first and last Ogg pages."); +} diff --git a/taglib/taglib/ogg/opus/opusproperties.h b/taglib/taglib/ogg/opus/opusproperties.h new file mode 100644 index 0000000..946f167 --- /dev/null +++ b/taglib/taglib/ogg/opus/opusproperties.h @@ -0,0 +1,96 @@ +/*************************************************************************** + copyright : (C) 2012 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + (original Vorbis implementation) +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_OPUSPROPERTIES_H +#define TAGLIB_OPUSPROPERTIES_H + +#include "audioproperties.h" + +namespace TagLib { + + namespace Ogg { + + namespace Opus { + + class File; + + //! An implementation of audio property reading for Ogg Opus + + /*! + * This reads the data from an Ogg Opus stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of Opus::Properties with the data read from the + * Opus::File \a file. + */ + Properties(File *file, ReadStyle style = Average); + + /*! + * Destroys this Opus::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + /*! + * The Opus codec supports decoding at multiple sample rates, there is no + * single sample rate of the encoded stream. This returns the sample rate + * of the original audio stream. + */ + int inputSampleRate() const; + + /*! + * Returns the Opus version, currently "0" (as specified by the spec). + */ + int opusVersion() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/taglib/ogg/speex/speexfile.cpp b/taglib/taglib/ogg/speex/speexfile.cpp new file mode 100644 index 0000000..e83f0ad --- /dev/null +++ b/taglib/taglib/ogg/speex/speexfile.cpp @@ -0,0 +1,132 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + (original Vorbis implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <bitset> + +#include <tstring.h> +#include <tdebug.h> +#include <tpropertymap.h> + +#include "speexfile.h" + +using namespace TagLib; +using namespace TagLib::Ogg; + +class Speex::File::FilePrivate +{ +public: + FilePrivate() : + comment(0), + properties(0) {} + + ~FilePrivate() + { + delete comment; + delete properties; + } + + Ogg::XiphComment *comment; + Properties *properties; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Speex::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : Ogg::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +Speex::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : Ogg::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +Speex::File::~File() +{ + delete d; +} + +Ogg::XiphComment *Speex::File::tag() const +{ + return d->comment; +} + +PropertyMap Speex::File::properties() const +{ + return d->comment->properties(); +} + +PropertyMap Speex::File::setProperties(const PropertyMap &properties) +{ + return d->comment->setProperties(properties); +} + +Speex::Properties *Speex::File::audioProperties() const +{ + return d->properties; +} + +bool Speex::File::save() +{ + if(!d->comment) + d->comment = new Ogg::XiphComment; + + setPacket(1, d->comment->render()); + + return Ogg::File::save(); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Speex::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + ByteVector speexHeaderData = packet(0); + + if(!speexHeaderData.startsWith("Speex ")) { + debug("Speex::File::read() -- invalid Speex identification header"); + return; + } + + ByteVector commentHeaderData = packet(1); + + d->comment = new Ogg::XiphComment(commentHeaderData); + + if(readProperties) + d->properties = new Properties(this, propertiesStyle); +} diff --git a/taglib/taglib/ogg/speex/speexfile.h b/taglib/taglib/ogg/speex/speexfile.h new file mode 100644 index 0000000..880e689 --- /dev/null +++ b/taglib/taglib/ogg/speex/speexfile.h @@ -0,0 +1,126 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + (original Vorbis implementation) +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_SPEEXFILE_H +#define TAGLIB_SPEEXFILE_H + +#include "oggfile.h" +#include "xiphcomment.h" + +#include "speexproperties.h" + +namespace TagLib { + + namespace Ogg { + + //! A namespace containing classes for Speex metadata + + namespace Speex { + + //! An implementation of Ogg::File with Speex specific methods + + /*! + * This is the central class in the Ogg Speex metadata processing collection + * of classes. It's built upon Ogg::File which handles processing of the Ogg + * logical bitstream and breaking it down into pages which are handled by + * the codec implementations, in this case Speex specifically. + */ + + class TAGLIB_EXPORT File : public Ogg::File + { + public: + /*! + * Constructs a Speex file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs a Speex file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the XiphComment for this file. XiphComment implements the tag + * interface, so this serves as the reimplementation of + * TagLib::File::tag(). + */ + virtual Ogg::XiphComment *tag() const; + + /*! + * Implements the unified property interface -- export function. + * This forwards directly to XiphComment::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified tag dictionary interface -- import function. + * Like properties(), this is a forwarder to the file's XiphComment. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the Speex::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + + + virtual bool save(); + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + + class FilePrivate; + FilePrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/taglib/ogg/speex/speexproperties.cpp b/taglib/taglib/ogg/speex/speexproperties.cpp new file mode 100644 index 0000000..5aaa915 --- /dev/null +++ b/taglib/taglib/ogg/speex/speexproperties.cpp @@ -0,0 +1,170 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + (original Vorbis implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> + +#include <oggpageheader.h> + +#include "speexproperties.h" +#include "speexfile.h" + +using namespace TagLib; +using namespace TagLib::Ogg; + +class Speex::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(File *f, ReadStyle s) : + file(f), + style(s), + length(0), + bitrate(0), + sampleRate(0), + channels(0), + speexVersion(0), + vbr(false), + mode(0) {} + + File *file; + ReadStyle style; + int length; + int bitrate; + int sampleRate; + int channels; + int speexVersion; + bool vbr; + int mode; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Speex::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(file, style); + read(); +} + +Speex::Properties::~Properties() +{ + delete d; +} + +int Speex::Properties::length() const +{ + return d->length; +} + +int Speex::Properties::bitrate() const +{ + return int(float(d->bitrate) / float(1000) + 0.5); +} + +int Speex::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int Speex::Properties::channels() const +{ + return d->channels; +} + +int Speex::Properties::speexVersion() const +{ + return d->speexVersion; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Speex::Properties::read() +{ + // Get the identification header from the Ogg implementation. + + ByteVector data = d->file->packet(0); + + uint pos = 28; + + // speex_version_id; /**< Version for Speex (for checking compatibility) */ + d->speexVersion = data.toUInt(pos, false); + pos += 4; + + // header_size; /**< Total size of the header ( sizeof(SpeexHeader) ) */ + pos += 4; + + // rate; /**< Sampling rate used */ + d->sampleRate = data.toUInt(pos, false); + pos += 4; + + // mode; /**< Mode used (0 for narrowband, 1 for wideband) */ + d->mode = data.toUInt(pos, false); + pos += 4; + + // mode_bitstream_version; /**< Version ID of the bit-stream */ + pos += 4; + + // nb_channels; /**< Number of channels encoded */ + d->channels = data.toUInt(pos, false); + pos += 4; + + // bitrate; /**< Bit-rate used */ + d->bitrate = data.toUInt(pos, false); + pos += 4; + + // frame_size; /**< Size of frames */ + // unsigned int frameSize = data.mid(pos, 4).toUInt(false); + pos += 4; + + // vbr; /**< 1 for a VBR encoding, 0 otherwise */ + d->vbr = data.toUInt(pos, false) == 1; + pos += 4; + + // frames_per_packet; /**< Number of frames stored per Ogg packet */ + // unsigned int framesPerPacket = data.mid(pos, 4).toUInt(false); + + const Ogg::PageHeader *first = d->file->firstPageHeader(); + const Ogg::PageHeader *last = d->file->lastPageHeader(); + + if(first && last) { + long long start = first->absoluteGranularPosition(); + long long end = last->absoluteGranularPosition(); + + if(start >= 0 && end >= 0 && d->sampleRate > 0) + d->length = (int) ((end - start) / (long long) d->sampleRate); + else + debug("Speex::Properties::read() -- Either the PCM values for the start or " + "end of this file was incorrect or the sample rate is zero."); + } + else + debug("Speex::Properties::read() -- Could not find valid first and last Ogg pages."); +} diff --git a/taglib/taglib/ogg/speex/speexproperties.h b/taglib/taglib/ogg/speex/speexproperties.h new file mode 100644 index 0000000..4720bd8 --- /dev/null +++ b/taglib/taglib/ogg/speex/speexproperties.h @@ -0,0 +1,89 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + (original Vorbis implementation) +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_SPEEXPROPERTIES_H +#define TAGLIB_SPEEXPROPERTIES_H + +#include "audioproperties.h" + +namespace TagLib { + + namespace Ogg { + + namespace Speex { + + class File; + + //! An implementation of audio property reading for Ogg Speex + + /*! + * This reads the data from an Ogg Speex stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of Speex::Properties with the data read from the + * Speex::File \a file. + */ + Properties(File *file, ReadStyle style = Average); + + /*! + * Destroys this Speex::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + /*! + * Returns the Speex version, currently "0" (as specified by the spec). + */ + int speexVersion() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/taglib/ogg/vorbis/vorbisfile.cpp b/taglib/taglib/ogg/vorbis/vorbisfile.cpp new file mode 100644 index 0000000..8298453 --- /dev/null +++ b/taglib/taglib/ogg/vorbis/vorbisfile.cpp @@ -0,0 +1,138 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <bitset> + +#include <tstring.h> +#include <tdebug.h> +#include <tpropertymap.h> + +#include "vorbisfile.h" + + +using namespace TagLib; + +class Vorbis::File::FilePrivate +{ +public: + FilePrivate() : + comment(0), + properties(0) {} + + ~FilePrivate() + { + delete comment; + delete properties; + } + + Ogg::XiphComment *comment; + Properties *properties; +}; + +namespace TagLib { + /*! + * Vorbis headers can be found with one type ID byte and the string "vorbis" in + * an Ogg stream. 0x03 indicates the comment header. + */ + static const char vorbisCommentHeaderID[] = { 0x03, 'v', 'o', 'r', 'b', 'i', 's', 0 }; +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Vorbis::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : Ogg::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +Vorbis::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : Ogg::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +Vorbis::File::~File() +{ + delete d; +} + +Ogg::XiphComment *Vorbis::File::tag() const +{ + return d->comment; +} + +PropertyMap Vorbis::File::properties() const +{ + return d->comment->properties(); +} + +PropertyMap Vorbis::File::setProperties(const PropertyMap &properties) +{ + return d->comment->setProperties(properties); +} + +Vorbis::Properties *Vorbis::File::audioProperties() const +{ + return d->properties; +} + +bool Vorbis::File::save() +{ + ByteVector v(vorbisCommentHeaderID); + + if(!d->comment) + d->comment = new Ogg::XiphComment; + v.append(d->comment->render()); + + setPacket(1, v); + + return Ogg::File::save(); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Vorbis::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + ByteVector commentHeaderData = packet(1); + + if(commentHeaderData.mid(0, 7) != vorbisCommentHeaderID) { + debug("Vorbis::File::read() - Could not find the Vorbis comment header."); + setValid(false); + return; + } + + d->comment = new Ogg::XiphComment(commentHeaderData.mid(7)); + + if(readProperties) + d->properties = new Properties(this, propertiesStyle); +} diff --git a/taglib/taglib/ogg/vorbis/vorbisfile.h b/taglib/taglib/ogg/vorbis/vorbisfile.h new file mode 100644 index 0000000..7735a11 --- /dev/null +++ b/taglib/taglib/ogg/vorbis/vorbisfile.h @@ -0,0 +1,144 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_VORBISFILE_H +#define TAGLIB_VORBISFILE_H + +#include "taglib_export.h" +#include "oggfile.h" +#include "xiphcomment.h" + +#include "vorbisproperties.h" + +namespace TagLib { + +/* + * This is just to make this appear to be in the Ogg namespace in the + * documentation. The typedef below will make this work with the current code. + * In the next BIC version of TagLib this will be really moved into the Ogg + * namespace. + */ + +#ifdef DOXYGEN + namespace Ogg { +#endif + + //! A namespace containing classes for Vorbis metadata + + namespace Vorbis { + + + //! An implementation of Ogg::File with Vorbis specific methods + + /*! + * This is the central class in the Ogg Vorbis metadata processing collection + * of classes. It's built upon Ogg::File which handles processing of the Ogg + * logical bitstream and breaking it down into pages which are handled by + * the codec implementations, in this case Vorbis specifically. + */ + + class TAGLIB_EXPORT File : public Ogg::File + { + public: + /*! + * Constructs a Vorbis file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs a Vorbis file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the XiphComment for this file. XiphComment implements the tag + * interface, so this serves as the reimplementation of + * TagLib::File::tag(). + */ + virtual Ogg::XiphComment *tag() const; + + + /*! + * Implements the unified property interface -- export function. + * This forwards directly to XiphComment::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified tag dictionary interface -- import function. + * Like properties(), this is a forwarder to the file's XiphComment. + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the Vorbis::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + virtual bool save(); + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + + class FilePrivate; + FilePrivate *d; + }; + } + +/* + * To keep compatibility with the current version put Vorbis in the Ogg namespace + * only in the docs and provide a typedef to make it work. In the next BIC + * version this will be removed and it will only exist in the Ogg namespace. + */ + +#ifdef DOXYGEN + } +#else + namespace Ogg { namespace Vorbis { typedef TagLib::Vorbis::File File; } } +#endif + +} + +#endif diff --git a/taglib/taglib/ogg/vorbis/vorbisproperties.cpp b/taglib/taglib/ogg/vorbis/vorbisproperties.cpp new file mode 100644 index 0000000..b5e88bf --- /dev/null +++ b/taglib/taglib/ogg/vorbis/vorbisproperties.cpp @@ -0,0 +1,183 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> + +#include <oggpageheader.h> + +#include "vorbisproperties.h" +#include "vorbisfile.h" + +using namespace TagLib; + +class Vorbis::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(File *f, ReadStyle s) : + file(f), + style(s), + length(0), + bitrate(0), + sampleRate(0), + channels(0), + vorbisVersion(0), + bitrateMaximum(0), + bitrateNominal(0), + bitrateMinimum(0) {} + + File *file; + ReadStyle style; + int length; + int bitrate; + int sampleRate; + int channels; + int vorbisVersion; + int bitrateMaximum; + int bitrateNominal; + int bitrateMinimum; +}; + +namespace TagLib { + /*! + * Vorbis headers can be found with one type ID byte and the string "vorbis" in + * an Ogg stream. 0x01 indicates the setup header. + */ + static const char vorbisSetupHeaderID[] = { 0x01, 'v', 'o', 'r', 'b', 'i', 's', 0 }; +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Vorbis::Properties::Properties(File *file, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(file, style); + read(); +} + +Vorbis::Properties::~Properties() +{ + delete d; +} + +int Vorbis::Properties::length() const +{ + return d->length; +} + +int Vorbis::Properties::bitrate() const +{ + return int(float(d->bitrate) / float(1000) + 0.5); +} + +int Vorbis::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int Vorbis::Properties::channels() const +{ + return d->channels; +} + +int Vorbis::Properties::vorbisVersion() const +{ + return d->vorbisVersion; +} + +int Vorbis::Properties::bitrateMaximum() const +{ + return d->bitrateMaximum; +} + +int Vorbis::Properties::bitrateNominal() const +{ + return d->bitrateNominal; +} + +int Vorbis::Properties::bitrateMinimum() const +{ + return d->bitrateMinimum; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Vorbis::Properties::read() +{ + // Get the identification header from the Ogg implementation. + + ByteVector data = d->file->packet(0); + + uint pos = 0; + + if(data.mid(pos, 7) != vorbisSetupHeaderID) { + debug("Vorbis::Properties::read() -- invalid Vorbis identification header"); + return; + } + + pos += 7; + + d->vorbisVersion = data.toUInt(pos, false); + pos += 4; + + d->channels = uchar(data[pos]); + pos += 1; + + d->sampleRate = data.toUInt(pos, false); + pos += 4; + + d->bitrateMaximum = data.toUInt(pos, false); + pos += 4; + + d->bitrateNominal = data.toUInt(pos, false); + pos += 4; + + d->bitrateMinimum = data.toUInt(pos, false); + + // TODO: Later this should be only the "fast" mode. + d->bitrate = d->bitrateNominal; + + // Find the length of the file. See http://wiki.xiph.org/VorbisStreamLength/ + // for my notes on the topic. + + const Ogg::PageHeader *first = d->file->firstPageHeader(); + const Ogg::PageHeader *last = d->file->lastPageHeader(); + + if(first && last) { + long long start = first->absoluteGranularPosition(); + long long end = last->absoluteGranularPosition(); + + if(start >= 0 && end >= 0 && d->sampleRate > 0) + d->length = (int)((end - start) / (long long) d->sampleRate); + else + debug("Vorbis::Properties::read() -- Either the PCM values for the start or " + "end of this file was incorrect or the sample rate is zero."); + } + else + debug("Vorbis::Properties::read() -- Could not find valid first and last Ogg pages."); +} diff --git a/taglib/taglib/ogg/vorbis/vorbisproperties.h b/taglib/taglib/ogg/vorbis/vorbisproperties.h new file mode 100644 index 0000000..de46985 --- /dev/null +++ b/taglib/taglib/ogg/vorbis/vorbisproperties.h @@ -0,0 +1,125 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_VORBISPROPERTIES_H +#define TAGLIB_VORBISPROPERTIES_H + +#include "taglib_export.h" +#include "audioproperties.h" + +namespace TagLib { + +/* + * This is just to make this appear to be in the Ogg namespace in the + * documentation. The typedef below will make this work with the current code. + * In the next BIC version of TagLib this will be really moved into the Ogg + * namespace. + */ + +#ifdef DOXYGEN + namespace Ogg { +#endif + + namespace Vorbis { + + class File; + + //! An implementation of audio property reading for Ogg Vorbis + + /*! + * This reads the data from an Ogg Vorbis stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of Vorbis::Properties with the data read from the + * Vorbis::File \a file. + */ + Properties(File *file, ReadStyle style = Average); + + /*! + * Destroys this VorbisProperties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + /*! + * Returns the Vorbis version, currently "0" (as specified by the spec). + */ + int vorbisVersion() const; + + /*! + * Returns the maximum bitrate as read from the Vorbis identification + * header. + */ + int bitrateMaximum() const; + + /*! + * Returns the nominal bitrate as read from the Vorbis identification + * header. + */ + int bitrateNominal() const; + + /*! + * Returns the minimum bitrate as read from the Vorbis identification + * header. + */ + int bitrateMinimum() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } + +/* + * To keep compatibility with the current version put Vorbis in the Ogg namespace + * only in the docs and provide a typedef to make it work. In the next BIC + * version this will be removed and it will only exist in the Ogg namespace. + */ + +#ifdef DOXYGEN + } +#else + namespace Ogg { namespace Vorbis { typedef TagLib::AudioProperties AudioProperties; } } +#endif + +} + +#endif diff --git a/taglib/taglib/ogg/xiphcomment.cpp b/taglib/taglib/ogg/xiphcomment.cpp new file mode 100644 index 0000000..51c2f9f --- /dev/null +++ b/taglib/taglib/ogg/xiphcomment.cpp @@ -0,0 +1,382 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tdebug.h> + +#include <xiphcomment.h> +#include <tpropertymap.h> + +using namespace TagLib; + +class Ogg::XiphComment::XiphCommentPrivate +{ +public: + FieldListMap fieldListMap; + String vendorID; + String commentField; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +Ogg::XiphComment::XiphComment() : TagLib::Tag() +{ + d = new XiphCommentPrivate; +} + +Ogg::XiphComment::XiphComment(const ByteVector &data) : TagLib::Tag() +{ + d = new XiphCommentPrivate; + parse(data); +} + +Ogg::XiphComment::~XiphComment() +{ + delete d; +} + +String Ogg::XiphComment::title() const +{ + if(d->fieldListMap["TITLE"].isEmpty()) + return String::null; + return d->fieldListMap["TITLE"].front(); +} + +String Ogg::XiphComment::artist() const +{ + if(d->fieldListMap["ARTIST"].isEmpty()) + return String::null; + return d->fieldListMap["ARTIST"].front(); +} + +String Ogg::XiphComment::album() const +{ + if(d->fieldListMap["ALBUM"].isEmpty()) + return String::null; + return d->fieldListMap["ALBUM"].front(); +} + +String Ogg::XiphComment::comment() const +{ + if(!d->fieldListMap["DESCRIPTION"].isEmpty()) { + d->commentField = "DESCRIPTION"; + return d->fieldListMap["DESCRIPTION"].front(); + } + + if(!d->fieldListMap["COMMENT"].isEmpty()) { + d->commentField = "COMMENT"; + return d->fieldListMap["COMMENT"].front(); + } + + return String::null; +} + +String Ogg::XiphComment::genre() const +{ + if(d->fieldListMap["GENRE"].isEmpty()) + return String::null; + return d->fieldListMap["GENRE"].front(); +} + +TagLib::uint Ogg::XiphComment::year() const +{ + if(!d->fieldListMap["DATE"].isEmpty()) + return d->fieldListMap["DATE"].front().toInt(); + if(!d->fieldListMap["YEAR"].isEmpty()) + return d->fieldListMap["YEAR"].front().toInt(); + return 0; +} + +TagLib::uint Ogg::XiphComment::track() const +{ + if(!d->fieldListMap["TRACKNUMBER"].isEmpty()) + return d->fieldListMap["TRACKNUMBER"].front().toInt(); + if(!d->fieldListMap["TRACKNUM"].isEmpty()) + return d->fieldListMap["TRACKNUM"].front().toInt(); + return 0; +} + +void Ogg::XiphComment::setTitle(const String &s) +{ + addField("TITLE", s); +} + +void Ogg::XiphComment::setArtist(const String &s) +{ + addField("ARTIST", s); +} + +void Ogg::XiphComment::setAlbum(const String &s) +{ + addField("ALBUM", s); +} + +void Ogg::XiphComment::setComment(const String &s) +{ + addField(d->commentField.isEmpty() ? "DESCRIPTION" : d->commentField, s); +} + +void Ogg::XiphComment::setGenre(const String &s) +{ + addField("GENRE", s); +} + +void Ogg::XiphComment::setYear(uint i) +{ + removeField("YEAR"); + if(i == 0) + removeField("DATE"); + else + addField("DATE", String::number(i)); +} + +void Ogg::XiphComment::setTrack(uint i) +{ + removeField("TRACKNUM"); + if(i == 0) + removeField("TRACKNUMBER"); + else + addField("TRACKNUMBER", String::number(i)); +} + +bool Ogg::XiphComment::isEmpty() const +{ + FieldListMap::ConstIterator it = d->fieldListMap.begin(); + for(; it != d->fieldListMap.end(); ++it) + if(!(*it).second.isEmpty()) + return false; + + return true; +} + +TagLib::uint Ogg::XiphComment::fieldCount() const +{ + uint count = 0; + + FieldListMap::ConstIterator it = d->fieldListMap.begin(); + for(; it != d->fieldListMap.end(); ++it) + count += (*it).second.size(); + + return count; +} + +const Ogg::FieldListMap &Ogg::XiphComment::fieldListMap() const +{ + return d->fieldListMap; +} + +PropertyMap Ogg::XiphComment::properties() const +{ + return d->fieldListMap; +} + +PropertyMap Ogg::XiphComment::setProperties(const PropertyMap &properties) +{ + // check which keys are to be deleted + StringList toRemove; + for(FieldListMap::ConstIterator it = d->fieldListMap.begin(); it != d->fieldListMap.end(); ++it) + if (!properties.contains(it->first)) + toRemove.append(it->first); + + for(StringList::ConstIterator it = toRemove.begin(); it != toRemove.end(); ++it) + removeField(*it); + + // now go through keys in \a properties and check that the values match those in the xiph comment + PropertyMap invalid; + PropertyMap::ConstIterator it = properties.begin(); + for(; it != properties.end(); ++it) + { + if(!checkKey(it->first)) + invalid.insert(it->first, it->second); + else if(!d->fieldListMap.contains(it->first) || !(it->second == d->fieldListMap[it->first])) { + const StringList &sl = it->second; + if(sl.size() == 0) + // zero size string list -> remove the tag with all values + removeField(it->first); + else { + // replace all strings in the list for the tag + StringList::ConstIterator valueIterator = sl.begin(); + addField(it->first, *valueIterator, true); + ++valueIterator; + for(; valueIterator != sl.end(); ++valueIterator) + addField(it->first, *valueIterator, false); + } + } + } + return invalid; +} + +bool Ogg::XiphComment::checkKey(const String &key) +{ + if(key.size() < 1) + return false; + for(String::ConstIterator it = key.begin(); it != key.end(); it++) + // forbid non-printable, non-ascii, '=' (#61) and '~' (#126) + if (*it < 32 || *it >= 128 || *it == 61 || *it == 126) + return false; + return true; +} + +String Ogg::XiphComment::vendorID() const +{ + return d->vendorID; +} + +void Ogg::XiphComment::addField(const String &key, const String &value, bool replace) +{ + if(replace) + removeField(key.upper()); + + if(!key.isEmpty() && !value.isEmpty()) + d->fieldListMap[key.upper()].append(value); +} + +void Ogg::XiphComment::removeField(const String &key, const String &value) +{ + if(!value.isNull()) { + StringList::Iterator it = d->fieldListMap[key].begin(); + while(it != d->fieldListMap[key].end()) { + if(value == *it) + it = d->fieldListMap[key].erase(it); + else + it++; + } + } + else + d->fieldListMap.erase(key); +} + +bool Ogg::XiphComment::contains(const String &key) const +{ + return d->fieldListMap.contains(key) && !d->fieldListMap[key].isEmpty(); +} + +ByteVector Ogg::XiphComment::render() const +{ + return render(true); +} + +ByteVector Ogg::XiphComment::render(bool addFramingBit) const +{ + ByteVector data; + + // Add the vendor ID length and the vendor ID. It's important to use the + // length of the data(String::UTF8) rather than the length of the the string + // since this is UTF8 text and there may be more characters in the data than + // in the UTF16 string. + + ByteVector vendorData = d->vendorID.data(String::UTF8); + + data.append(ByteVector::fromUInt(vendorData.size(), false)); + data.append(vendorData); + + // Add the number of fields. + + data.append(ByteVector::fromUInt(fieldCount(), false)); + + // Iterate over the the field lists. Our iterator returns a + // std::pair<String, StringList> where the first String is the field name and + // the StringList is the values associated with that field. + + FieldListMap::ConstIterator it = d->fieldListMap.begin(); + for(; it != d->fieldListMap.end(); ++it) { + + // And now iterate over the values of the current list. + + String fieldName = (*it).first; + StringList values = (*it).second; + + StringList::ConstIterator valuesIt = values.begin(); + for(; valuesIt != values.end(); ++valuesIt) { + ByteVector fieldData = fieldName.data(String::UTF8); + fieldData.append('='); + fieldData.append((*valuesIt).data(String::UTF8)); + + data.append(ByteVector::fromUInt(fieldData.size(), false)); + data.append(fieldData); + } + } + + // Append the "framing bit". + + if(addFramingBit) + data.append(char(1)); + + return data; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void Ogg::XiphComment::parse(const ByteVector &data) +{ + // The first thing in the comment data is the vendor ID length, followed by a + // UTF8 string with the vendor ID. + + uint pos = 0; + + const uint vendorLength = data.toUInt(0, false); + pos += 4; + + d->vendorID = String(data.mid(pos, vendorLength), String::UTF8); + pos += vendorLength; + + // Next the number of fields in the comment vector. + + const uint commentFields = data.toUInt(pos, false); + pos += 4; + + if(commentFields > (data.size() - 8) / 4) { + return; + } + + for(uint i = 0; i < commentFields; i++) { + + // Each comment field is in the format "KEY=value" in a UTF8 string and has + // 4 bytes before the text starts that gives the length. + + const uint commentLength = data.toUInt(pos, false); + pos += 4; + + String comment = String(data.mid(pos, commentLength), String::UTF8); + pos += commentLength; + if(pos > data.size()) { + break; + } + + int commentSeparatorPosition = comment.find("="); + if(commentSeparatorPosition == -1) { + break; + } + + String key = comment.substr(0, commentSeparatorPosition); + String value = comment.substr(commentSeparatorPosition + 1); + + addField(key, value, false); + } +} diff --git a/taglib/taglib/ogg/xiphcomment.h b/taglib/taglib/ogg/xiphcomment.h new file mode 100644 index 0000000..54f3070 --- /dev/null +++ b/taglib/taglib/ogg/xiphcomment.h @@ -0,0 +1,225 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_VORBISCOMMENT_H +#define TAGLIB_VORBISCOMMENT_H + +#include "tag.h" +#include "tlist.h" +#include "tmap.h" +#include "tstring.h" +#include "tstringlist.h" +#include "tbytevector.h" +#include "taglib_export.h" + +namespace TagLib { + + namespace Ogg { + + /*! + * A mapping between a list of field names, or keys, and a list of values + * associated with that field. + * + * \see XiphComment::fieldListMap() + */ + typedef Map<String, StringList> FieldListMap; + + //! Ogg Vorbis comment implementation + + /*! + * This class is an implementation of the Ogg Vorbis comment specification, + * to be found in section 5 of the Ogg Vorbis specification. Because this + * format is also used in other (currently unsupported) Xiph.org formats, it + * has been made part of a generic implementation rather than being limited + * to strictly Vorbis. + * + * Vorbis comments are a simple vector of keys and values, called fields. + * Multiple values for a given key are supported. + * + * \see fieldListMap() + */ + + class TAGLIB_EXPORT XiphComment : public TagLib::Tag + { + public: + /*! + * Constructs an empty Vorbis comment. + */ + XiphComment(); + + /*! + * Constructs a Vorbis comment from \a data. + */ + XiphComment(const ByteVector &data); + + /*! + * Destroys this instance of the XiphComment. + */ + virtual ~XiphComment(); + + virtual String title() const; + virtual String artist() const; + virtual String album() const; + virtual String comment() const; + virtual String genre() const; + virtual uint year() const; + virtual uint track() const; + + virtual void setTitle(const String &s); + virtual void setArtist(const String &s); + virtual void setAlbum(const String &s); + virtual void setComment(const String &s); + virtual void setGenre(const String &s); + virtual void setYear(uint i); + virtual void setTrack(uint i); + + virtual bool isEmpty() const; + + /*! + * Returns the number of fields present in the comment. + */ + uint fieldCount() const; + + /*! + * Returns a reference to the map of field lists. Because Xiph comments + * support multiple fields with the same key, a pure Map would not work. + * As such this is a Map of string lists, keyed on the comment field name. + * + * The standard set of Xiph/Vorbis fields (which may or may not be + * contained in any specific comment) is: + * + * <ul> + * <li>TITLE</li> + * <li>VERSION</li> + * <li>ALBUM</li> + * <li>ARTIST</li> + * <li>PERFORMER</li> + * <li>COPYRIGHT</li> + * <li>ORGANIZATION</li> + * <li>DESCRIPTION</li> + * <li>GENRE</li> + * <li>DATE</li> + * <li>LOCATION</li> + * <li>CONTACT</li> + * <li>ISRC</li> + * </ul> + * + * For a more detailed description of these fields, please see the Ogg + * Vorbis specification, section 5.2.2.1. + * + * \note The Ogg Vorbis comment specification does allow these key values + * to be either upper or lower case. However, it is conventional for them + * to be upper case. As such, TagLib, when parsing a Xiph/Vorbis comment, + * converts all fields to uppercase. When you are using this data + * structure, you will need to specify the field name in upper case. + * + * \warning You should not modify this data structure directly, instead + * use addField() and removeField(). + */ + const FieldListMap &fieldListMap() const; + + /*! + * Implements the unified property interface -- export function. + * The result is a one-to-one match of the Xiph comment, since it is + * completely compatible with the property interface (in fact, a Xiph + * comment is nothing more than a map from tag names to list of values, + * as is the dict interface). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * The tags from the given map will be stored one-to-one in the file, + * except for invalid keys (less than one character, non-ASCII, or + * containing '=' or '~') in which case the according values will + * be contained in the returned PropertyMap. + */ + PropertyMap setProperties(const PropertyMap&); + + /*! + * Check if the given String is a valid Xiph comment key. + */ + static bool checkKey(const String&); + + /*! + * Returns the vendor ID of the Ogg Vorbis encoder. libvorbis 1.0 as the + * most common case always returns "Xiph.Org libVorbis I 20020717". + */ + String vendorID() const; + + /*! + * Add the field specified by \a key with the data \a value. If \a replace + * is true, then all of the other fields with the same key will be removed + * first. + * + * If the field value is empty, the field will be removed. + */ + void addField(const String &key, const String &value, bool replace = true); + + /*! + * Remove the field specified by \a key with the data \a value. If + * \a value is null, all of the fields with the given key will be removed. + */ + void removeField(const String &key, const String &value = String::null); + + /*! + * Returns true if the field is contained within the comment. + * + * \note This is safer than checking for membership in the FieldListMap. + */ + bool contains(const String &key) const; + + /*! + * Renders the comment to a ByteVector suitable for inserting into a file. + */ + ByteVector render() const; // BIC: remove and merge with below + + /*! + * Renders the comment to a ByteVector suitable for inserting into a file. + * + * If \a addFramingBit is true the standard Vorbis comment framing bit will + * be appended. However some formats (notably FLAC) do not work with this + * in place. + */ + ByteVector render(bool addFramingBit) const; + + protected: + /*! + * Reads the tag from the file specified in the constructor and fills the + * FieldListMap. + */ + void parse(const ByteVector &data); + + private: + XiphComment(const XiphComment &); + XiphComment &operator=(const XiphComment &); + + class XiphCommentPrivate; + XiphCommentPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/riff/aiff/aifffile.cpp b/taglib/taglib/riff/aiff/aifffile.cpp new file mode 100644 index 0000000..d20c148 --- /dev/null +++ b/taglib/taglib/riff/aiff/aifffile.cpp @@ -0,0 +1,144 @@ +/*************************************************************************** + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tdebug.h> +#include <id3v2tag.h> +#include <tstringlist.h> +#include <tpropertymap.h> + +#include "aifffile.h" + +using namespace TagLib; + +class RIFF::AIFF::File::FilePrivate +{ +public: + FilePrivate() : + properties(0), + tag(0), + tagChunkID("ID3 ") + { + + } + + ~FilePrivate() + { + delete properties; + delete tag; + } + + Properties *properties; + ID3v2::Tag *tag; + ByteVector tagChunkID; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +RIFF::AIFF::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : RIFF::File(file, BigEndian) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +RIFF::AIFF::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : RIFF::File(stream, BigEndian) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +RIFF::AIFF::File::~File() +{ + delete d; +} + +ID3v2::Tag *RIFF::AIFF::File::tag() const +{ + return d->tag; +} + +PropertyMap RIFF::AIFF::File::properties() const +{ + return d->tag->properties(); +} + +void RIFF::AIFF::File::removeUnsupportedProperties(const StringList &unsupported) +{ + d->tag->removeUnsupportedProperties(unsupported); +} + +PropertyMap RIFF::AIFF::File::setProperties(const PropertyMap &properties) +{ + return d->tag->setProperties(properties); +} + + +RIFF::AIFF::Properties *RIFF::AIFF::File::audioProperties() const +{ + return d->properties; +} + +bool RIFF::AIFF::File::save() +{ + if(readOnly()) { + debug("RIFF::AIFF::File::save() -- File is read only."); + return false; + } + + if(!isValid()) { + debug("RIFF::AIFF::File::save() -- Trying to save invalid file."); + return false; + } + + setChunkData(d->tagChunkID, d->tag->render()); + + return true; +} + + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void RIFF::AIFF::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + for(uint i = 0; i < chunkCount(); i++) { + if(chunkName(i) == "ID3 " || chunkName(i) == "id3 ") { + d->tagChunkID = chunkName(i); + d->tag = new ID3v2::Tag(this, chunkOffset(i)); + } + else if(chunkName(i) == "COMM" && readProperties) + d->properties = new Properties(chunkData(i), propertiesStyle); + } + + if(!d->tag) + d->tag = new ID3v2::Tag; +} diff --git a/taglib/taglib/riff/aiff/aifffile.h b/taglib/taglib/riff/aiff/aifffile.h new file mode 100644 index 0000000..c75b94b --- /dev/null +++ b/taglib/taglib/riff/aiff/aifffile.h @@ -0,0 +1,142 @@ +/*************************************************************************** + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_AIFFFILE_H +#define TAGLIB_AIFFFILE_H + +#include "rifffile.h" +#include "id3v2tag.h" +#include "aiffproperties.h" + +namespace TagLib { + + namespace RIFF { + + //! An implementation of AIFF metadata + + /*! + * This is implementation of AIFF metadata. + * + * This supports an ID3v2 tag as well as reading stream from the ID3 RIFF + * chunk as well as properties from the file. + */ + + namespace AIFF { + + //! An implementation of TagLib::File with AIFF specific methods + + /*! + * This implements and provides an interface for AIFF files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to AIFF files. + */ + + class TAGLIB_EXPORT File : public TagLib::RIFF::File + { + public: + /*! + * Constructs an AIFF file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an AIFF file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the Tag for this file. + * + * \note This always returns a valid pointer regardless of whether or not + * the file on disk has an ID3v2 tag. Use hasID3v2Tag() to check if the file + * on disk actually has an ID3v2 tag. + * + * \see hasID3v2Tag() + */ + virtual ID3v2::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * This method forwards to ID3v2::Tag::properties(). + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * This method forwards to ID3v2::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the AIFF::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + /*! + * Saves the file. + */ + virtual bool save(); + + /*! + * Returns whether or not the file on disk actually has an ID3v2 tag. + * + * \see ID3v2Tag() + */ + bool hasID3v2Tag() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + + class FilePrivate; + FilePrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/taglib/riff/aiff/aiffproperties.cpp b/taglib/taglib/riff/aiff/aiffproperties.cpp new file mode 100644 index 0000000..18b973e --- /dev/null +++ b/taglib/taglib/riff/aiff/aiffproperties.cpp @@ -0,0 +1,160 @@ +/*************************************************************************** + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> +#include <cmath> +// ldexp is a c99 function, which might not be defined in <cmath> +// so we pull in math.h too and hope it does the right (wrong) thing +// wrt. c99 functions in C++ +#include <math.h> + +#include "aiffproperties.h" + +//////////////////////////////////////////////////////////////////////////////// +// nasty 80-bit float helpers +//////////////////////////////////////////////////////////////////////////////// + +#define UnsignedToFloat(u) (((double)((long)(u - 2147483647L - 1))) + 2147483648.0) + +static double ConvertFromIeeeExtended(const TagLib::uchar *bytes) +{ + double f; + int expon; + unsigned long hiMant, loMant; + + expon = ((bytes[0] & 0x7F) << 8) | (bytes[1] & 0xFF); + + hiMant = ((unsigned long)(bytes[2] & 0xFF) << 24) | + ((unsigned long)(bytes[3] & 0xFF) << 16) | + ((unsigned long)(bytes[4] & 0xFF) << 8) | + ((unsigned long)(bytes[5] & 0xFF)); + + loMant = ((unsigned long)(bytes[6] & 0xFF) << 24) | + ((unsigned long)(bytes[7] & 0xFF) << 16) | + ((unsigned long)(bytes[8] & 0xFF) << 8) | + ((unsigned long)(bytes[9] & 0xFF)); + + if (expon == 0 && hiMant == 0 && loMant == 0) + f = 0; + else { + if(expon == 0x7FFF) /* Infinity or NaN */ + f = HUGE_VAL; + else { + expon -= 16383; + f = ldexp(UnsignedToFloat(hiMant), expon -= 31); + f += ldexp(UnsignedToFloat(loMant), expon -= 32); + } + } + + if(bytes[0] & 0x80) + return -f; + else + return f; +} + +using namespace TagLib; + +class RIFF::AIFF::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate() : + length(0), + bitrate(0), + sampleRate(0), + channels(0), + sampleWidth(0), + sampleFrames(0) + { + + } + + int length; + int bitrate; + int sampleRate; + int channels; + int sampleWidth; + uint sampleFrames; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +RIFF::AIFF::Properties::Properties(const ByteVector &data, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate; + read(data); +} + +RIFF::AIFF::Properties::~Properties() +{ + delete d; +} + +int RIFF::AIFF::Properties::length() const +{ + return d->length; +} + +int RIFF::AIFF::Properties::bitrate() const +{ + return d->bitrate; +} + +int RIFF::AIFF::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int RIFF::AIFF::Properties::channels() const +{ + return d->channels; +} + +int RIFF::AIFF::Properties::sampleWidth() const +{ + return d->sampleWidth; +} + +TagLib::uint RIFF::AIFF::Properties::sampleFrames() const +{ + return d->sampleFrames; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void RIFF::AIFF::Properties::read(const ByteVector &data) +{ + d->channels = data.toShort(0U); + d->sampleFrames = data.toUInt(2U); + d->sampleWidth = data.toShort(6U); + double sampleRate = ConvertFromIeeeExtended(reinterpret_cast<const uchar *>(data.data() + 8)); + d->sampleRate = (int)sampleRate; + d->bitrate = (int)((sampleRate * d->sampleWidth * d->channels) / 1000.0); + d->length = d->sampleRate > 0 ? d->sampleFrames / d->sampleRate : 0; +} diff --git a/taglib/taglib/riff/aiff/aiffproperties.h b/taglib/taglib/riff/aiff/aiffproperties.h new file mode 100644 index 0000000..68e90b7 --- /dev/null +++ b/taglib/taglib/riff/aiff/aiffproperties.h @@ -0,0 +1,83 @@ +/*************************************************************************** + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_AIFFPROPERTIES_H +#define TAGLIB_AIFFPROPERTIES_H + +#include "audioproperties.h" + +namespace TagLib { + + namespace RIFF { + + namespace AIFF { + + class File; + + //! An implementation of audio property reading for AIFF + + /*! + * This reads the data from an AIFF stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of AIFF::Properties with the data read from the + * ByteVector \a data. + */ + Properties(const ByteVector &data, ReadStyle style); + + /*! + * Destroys this AIFF::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + int sampleWidth() const; + uint sampleFrames() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(const ByteVector &data); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/taglib/riff/rifffile.cpp b/taglib/taglib/riff/rifffile.cpp new file mode 100644 index 0000000..a39fce6 --- /dev/null +++ b/taglib/taglib/riff/rifffile.cpp @@ -0,0 +1,321 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tdebug.h> +#include <tstring.h> + +#include "rifffile.h" +#include <algorithm> +#include <vector> + +using namespace TagLib; + +struct Chunk +{ + ByteVector name; + TagLib::uint offset; + TagLib::uint size; + char padding; +}; + +class RIFF::File::FilePrivate +{ +public: + FilePrivate() : + endianness(BigEndian), + size(0) + { + + } + Endianness endianness; + ByteVector type; + TagLib::uint size; + ByteVector format; + + std::vector<Chunk> chunks; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +RIFF::File::~File() +{ + delete d; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +RIFF::File::File(FileName file, Endianness endianness) : TagLib::File(file) +{ + d = new FilePrivate; + d->endianness = endianness; + + if(isOpen()) + read(); +} + +RIFF::File::File(IOStream *stream, Endianness endianness) : TagLib::File(stream) +{ + d = new FilePrivate; + d->endianness = endianness; + + if(isOpen()) + read(); +} + +TagLib::uint RIFF::File::riffSize() const +{ + return d->size; +} + +TagLib::uint RIFF::File::chunkCount() const +{ + return d->chunks.size(); +} + +TagLib::uint RIFF::File::chunkDataSize(uint i) const +{ + return d->chunks[i].size; +} + +TagLib::uint RIFF::File::chunkOffset(uint i) const +{ + return d->chunks[i].offset; +} + +TagLib::uint RIFF::File::chunkPadding(uint i) const +{ + return d->chunks[i].padding; +} + +ByteVector RIFF::File::chunkName(uint i) const +{ + if(i >= chunkCount()) + return ByteVector::null; + + return d->chunks[i].name; +} + +ByteVector RIFF::File::chunkData(uint i) +{ + if(i >= chunkCount()) + return ByteVector::null; + + // Offset for the first subchunk's data + + long begin = 12 + 8; + + for(uint it = 0; it < i; it++) + begin += 8 + d->chunks[it].size + d->chunks[it].padding; + + seek(begin); + + return readBlock(d->chunks[i].size); +} + +void RIFF::File::setChunkData(uint i, const ByteVector &data) +{ + // First we update the global size + + d->size += ((data.size() + 1) & ~1) - (d->chunks[i].size + d->chunks[i].padding); + insert(ByteVector::fromUInt(d->size, d->endianness == BigEndian), 4, 4); + + // Now update the specific chunk + + writeChunk(chunkName(i), data, d->chunks[i].offset - 8, d->chunks[i].size + d->chunks[i].padding + 8); + + d->chunks[i].size = data.size(); + d->chunks[i].padding = (data.size() & 0x01) ? 1 : 0; + + // Now update the internal offsets + + for(i++; i < d->chunks.size(); i++) + d->chunks[i].offset = d->chunks[i-1].offset + 8 + d->chunks[i-1].size + d->chunks[i-1].padding; +} + +void RIFF::File::setChunkData(const ByteVector &name, const ByteVector &data) +{ + setChunkData(name, data, false); +} + +void RIFF::File::setChunkData(const ByteVector &name, const ByteVector &data, bool alwaysCreate) +{ + if(d->chunks.size() == 0) { + debug("RIFF::File::setChunkData - No valid chunks found."); + return; + } + + if(alwaysCreate && name != "LIST") { + debug("RIFF::File::setChunkData - alwaysCreate should be used for only \"LIST\" chunks."); + return; + } + + if(!alwaysCreate) { + for(uint i = 0; i < d->chunks.size(); i++) { + if(d->chunks[i].name == name) { + setChunkData(i, data); + return; + } + } + } + + // Couldn't find an existing chunk, so let's create a new one. + + uint i = d->chunks.size() - 1; + ulong offset = d->chunks[i].offset + d->chunks[i].size; + + // First we update the global size + + d->size += (offset & 1) + data.size() + 8; + insert(ByteVector::fromUInt(d->size, d->endianness == BigEndian), 4, 4); + + // Now add the chunk to the file + + writeChunk(name, data, offset, std::max<long>(0, length() - offset), (offset & 1) ? 1 : 0); + + // And update our internal structure + + if (offset & 1) { + d->chunks[i].padding = 1; + offset++; + } + + Chunk chunk; + chunk.name = name; + chunk.size = data.size(); + chunk.offset = offset + 8; + chunk.padding = (data.size() & 0x01) ? 1 : 0; + + d->chunks.push_back(chunk); +} + +void RIFF::File::removeChunk(uint i) +{ + if(i >= d->chunks.size()) + return; + + removeBlock(d->chunks[i].offset - 8, d->chunks[i].size + 8); + d->chunks.erase(d->chunks.begin() + i); +} + +void RIFF::File::removeChunk(const ByteVector &name) +{ + std::vector<Chunk> newChunks; + for(size_t i = 0; i < d->chunks.size(); ++i) { + if(d->chunks[i].name == name) + removeBlock(d->chunks[i].offset - 8, d->chunks[i].size + 8); + else + newChunks.push_back(d->chunks[i]); + } + + d->chunks.swap(newChunks); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +static bool isValidChunkID(const ByteVector &name) +{ + if(name.size() != 4) { + return false; + } + for(int i = 0; i < 4; i++) { + if(name[i] < 32 || name[i] > 127) { + return false; + } + } + return true; +} + +void RIFF::File::read() +{ + bool bigEndian = (d->endianness == BigEndian); + + d->type = readBlock(4); + d->size = readBlock(4).toUInt(bigEndian); + d->format = readBlock(4); + + // + 8: chunk header at least, fix for additional junk bytes + while(tell() + 8 <= length()) { + ByteVector chunkName = readBlock(4); + uint chunkSize = readBlock(4).toUInt(bigEndian); + + if(!isValidChunkID(chunkName)) { + debug("RIFF::File::read() -- Chunk '" + chunkName + "' has invalid ID"); + setValid(false); + break; + } + + if(static_cast<ulonglong>(tell()) + chunkSize > static_cast<ulonglong>(length())) { + debug("RIFF::File::read() -- Chunk '" + chunkName + "' has invalid size (larger than the file size)"); + setValid(false); + break; + } + + Chunk chunk; + chunk.name = chunkName; + chunk.size = chunkSize; + chunk.offset = tell(); + + seek(chunk.size, Current); + + // check padding + chunk.padding = 0; + long uPosNotPadded = tell(); + if((uPosNotPadded & 0x01) != 0) { + ByteVector iByte = readBlock(1); + if((iByte.size() != 1) || (iByte[0] != 0)) { + // not well formed, re-seek + seek(uPosNotPadded, Beginning); + } + else { + chunk.padding = 1; + } + } + d->chunks.push_back(chunk); + + } +} + +void RIFF::File::writeChunk(const ByteVector &name, const ByteVector &data, + ulong offset, ulong replace, uint leadingPadding) +{ + ByteVector combined; + if(leadingPadding) { + combined.append(ByteVector(leadingPadding, '\x00')); + } + combined.append(name); + combined.append(ByteVector::fromUInt(data.size(), d->endianness == BigEndian)); + combined.append(data); + if((data.size() & 0x01) != 0) { + combined.append('\x00'); + } + insert(combined, offset, replace); +} diff --git a/taglib/taglib/riff/rifffile.h b/taglib/taglib/riff/rifffile.h new file mode 100644 index 0000000..6b8fe19 --- /dev/null +++ b/taglib/taglib/riff/rifffile.h @@ -0,0 +1,157 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_RIFFFILE_H +#define TAGLIB_RIFFFILE_H + +#include "taglib_export.h" +#include "tfile.h" + +namespace TagLib { + + //! An implementation of TagLib::File with RIFF specific methods + + namespace RIFF { + + //! An RIFF file class with some useful methods specific to RIFF + + /*! + * This implements the generic TagLib::File API and additionally provides + * access to properties that are distinct to RIFF files, notably access + * to the different ID3 tags. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + protected: + + enum Endianness { BigEndian, LittleEndian }; + + File(FileName file, Endianness endianness); + File(IOStream *stream, Endianness endianness); + + /*! + * \return The size of the main RIFF chunk. + */ + uint riffSize() const; + + /*! + * \return The number of chunks in the file. + */ + uint chunkCount() const; + + /*! + * \return The offset within the file for the selected chunk number. + */ + uint chunkOffset(uint i) const; + + /*! + * \return The size of the chunk data. + */ + uint chunkDataSize(uint i) const; + + /*! + * \return The size of the padding after the chunk (can be either 0 or 1). + */ + uint chunkPadding(uint i) const; + + /*! + * \return The name of the specified chunk, for instance, "COMM" or "ID3 " + */ + ByteVector chunkName(uint i) const; + + /*! + * Reads the chunk data from the file and returns it. + * + * \note This \e will move the read pointer for the file. + */ + ByteVector chunkData(uint i); + + /*! + * Sets the data for the the specified chunk to \a data. + * + * \warning This will update the file immediately. + */ + void setChunkData(uint i, const ByteVector &data); + + /*! + * Sets the data for the chunk \a name to \a data. If a chunk with the + * given name already exists it will be overwritten, otherwise it will be + * created after the existing chunks. + * + * \warning This will update the file immediately. + */ + void setChunkData(const ByteVector &name, const ByteVector &data); + + /*! + * Sets the data for the chunk \a name to \a data. If a chunk with the + * given name already exists it will be overwritten, otherwise it will be + * created after the existing chunks. + * + * \note If \a alwaysCreate is true, a new chunk is created regardless of + * whether or not the chunk \a name exists. It should only be used for + * "LIST" chunks. + * + * \warning This will update the file immediately. + */ + void setChunkData(const ByteVector &name, const ByteVector &data, bool alwaysCreate); + + /*! + * Removes the specified chunk. + * + * \warning This will update the file immediately. + */ + void removeChunk(uint i); + + /*! + * Removes the chunk \a name. + * + * \warning This will update the file immediately. + * \warning This removes all the chunks with the given name. + */ + void removeChunk(const ByteVector &name); + + private: + File(const File &); + File &operator=(const File &); + + void read(); + void writeChunk(const ByteVector &name, const ByteVector &data, + ulong offset, ulong replace = 0, + uint leadingPadding = 0); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/riff/wav/infotag.cpp b/taglib/taglib/riff/wav/infotag.cpp new file mode 100644 index 0000000..09edfa5 --- /dev/null +++ b/taglib/taglib/riff/wav/infotag.cpp @@ -0,0 +1,266 @@ +/*************************************************************************** + copyright : (C) 2012 by Tsuda Kageyu + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tdebug.h> +#include <tfile.h> + +#include "infotag.h" + +using namespace TagLib; +using namespace RIFF::Info; + +namespace { + static bool isValidChunkID(const ByteVector &name) + { + if(name.size() != 4) + return false; + + for(int i = 0; i < 4; i++) { + if(name[i] < 32 || name[i] > 127) + return false; + } + + return true; + } +} + +class RIFF::Info::Tag::TagPrivate +{ +public: + TagPrivate() + {} + + FieldListMap fieldListMap; + + static const StringHandler *stringHandler; +}; + +//////////////////////////////////////////////////////////////////////////////// +// StringHandler implementation +//////////////////////////////////////////////////////////////////////////////// + +StringHandler::StringHandler() +{ +} + +StringHandler::~StringHandler() +{ +} + +String RIFF::Info::StringHandler::parse(const ByteVector &data) const +{ + return String(data, String::UTF8); +} + +ByteVector RIFF::Info::StringHandler::render(const String &s) const +{ + return s.data(String::UTF8); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +static const StringHandler defaultStringHandler; +const RIFF::Info::StringHandler *RIFF::Info::Tag::TagPrivate::stringHandler = &defaultStringHandler; + +RIFF::Info::Tag::Tag(const ByteVector &data) + : TagLib::Tag() + , d(new TagPrivate()) +{ + parse(data); +} + +RIFF::Info::Tag::Tag() + : TagLib::Tag() + , d(new TagPrivate()) +{ +} + +RIFF::Info::Tag::~Tag() +{ + delete d; +} + +String RIFF::Info::Tag::title() const +{ + return fieldText("INAM"); +} + +String RIFF::Info::Tag::artist() const +{ + return fieldText("IART"); +} + +String RIFF::Info::Tag::album() const +{ + return fieldText("IPRD"); +} + +String RIFF::Info::Tag::comment() const +{ + return fieldText("ICMT"); +} + +String RIFF::Info::Tag::genre() const +{ + return fieldText("IGNR"); +} + +TagLib::uint RIFF::Info::Tag::year() const +{ + return fieldText("ICRD").substr(0, 4).toInt(); +} + +TagLib::uint RIFF::Info::Tag::track() const +{ + return fieldText("IPRT").toInt(); +} + +void RIFF::Info::Tag::setTitle(const String &s) +{ + setFieldText("INAM", s); +} + +void RIFF::Info::Tag::setArtist(const String &s) +{ + setFieldText("IART", s); +} + +void RIFF::Info::Tag::setAlbum(const String &s) +{ + setFieldText("IPRD", s); +} + +void RIFF::Info::Tag::setComment(const String &s) +{ + setFieldText("ICMT", s); +} + +void RIFF::Info::Tag::setGenre(const String &s) +{ + setFieldText("IGNR", s); +} + +void RIFF::Info::Tag::setYear(uint i) +{ + if(i != 0) + setFieldText("ICRD", String::number(i)); + else + d->fieldListMap.erase("ICRD"); +} + +void RIFF::Info::Tag::setTrack(uint i) +{ + if(i != 0) + setFieldText("IPRT", String::number(i)); + else + d->fieldListMap.erase("IPRT"); +} + +bool RIFF::Info::Tag::isEmpty() const +{ + return d->fieldListMap.isEmpty(); +} + +FieldListMap RIFF::Info::Tag::fieldListMap() const +{ + return d->fieldListMap; +} + +String RIFF::Info::Tag::fieldText(const ByteVector &id) const +{ + if(d->fieldListMap.contains(id)) + return String(d->fieldListMap[id]); + else + return String(); +} + +void RIFF::Info::Tag::setFieldText(const ByteVector &id, const String &s) +{ + // id must be four-byte long pure ascii string. + if(!isValidChunkID(id)) + return; + + if(!s.isEmpty()) + d->fieldListMap[id] = s; + else + removeField(id); +} + +void RIFF::Info::Tag::removeField(const ByteVector &id) +{ + if(d->fieldListMap.contains(id)) + d->fieldListMap.erase(id); +} + +ByteVector RIFF::Info::Tag::render() const +{ + ByteVector data("INFO"); + + FieldListMap::ConstIterator it = d->fieldListMap.begin(); + for(; it != d->fieldListMap.end(); ++it) { + ByteVector text = TagPrivate::stringHandler->render(it->second); + if(text.isEmpty()) + continue; + + data.append(it->first); + data.append(ByteVector::fromUInt(text.size() + 1, false)); + data.append(text); + + do { + data.append('\0'); + } while(data.size() & 1); + } + + if(data.size() == 4) + return ByteVector(); + else + return data; +} + +void RIFF::Info::Tag::setStringHandler(const StringHandler *handler) +{ + if(handler) + TagPrivate::stringHandler = handler; + else + TagPrivate::stringHandler = &defaultStringHandler; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void RIFF::Info::Tag::parse(const ByteVector &data) +{ + uint p = 4; + while(p < data.size()) { + const uint size = data.toUInt(p + 4, false); + d->fieldListMap[data.mid(p, 4)] = TagPrivate::stringHandler->parse(data.mid(p + 8, size)); + + p += ((size + 1) & ~1) + 8; + } +} + diff --git a/taglib/taglib/riff/wav/infotag.h b/taglib/taglib/riff/wav/infotag.h new file mode 100644 index 0000000..4007ae6 --- /dev/null +++ b/taglib/taglib/riff/wav/infotag.h @@ -0,0 +1,192 @@ +/*************************************************************************** + copyright : (C) 2012 by Tsuda Kageyu + email : tsuda.kageyu@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_INFOTAG_H +#define TAGLIB_INFOTAG_H + +#include "tag.h" +#include "tmap.h" +#include "tstring.h" +#include "tstringlist.h" +#include "tbytevector.h" +#include "taglib_export.h" + +namespace TagLib { + + class File; + + //! A RIFF Info tag implementation. + namespace RIFF { + namespace Info { + + typedef Map<ByteVector, String> FieldListMap; + + //! A abstraction for the string to data encoding in Info tags. + + /*! + * RIFF Info tag has no clear definitions about character encodings. + * In practice, local encoding of each system is largely used and UTF-8 is + * popular too. + * + * Here is an option to read and write tags in your preferrd encoding + * by subclassing this class, reimplementing parse() and render() and setting + * your reimplementation as the default with Info::Tag::setStringHandler(). + * + * \see ID3v1::Tag::setStringHandler() + */ + + class TAGLIB_EXPORT StringHandler + { + public: + StringHandler(); + ~StringHandler(); + + /*! + * Decode a string from \a data. The default implementation assumes that + * \a data is an UTF-8 character array. + */ + virtual String parse(const ByteVector &data) const; + + /*! + * Encode a ByteVector with the data from \a s. The default implementation + * assumes that \a s is an UTF-8 string. + */ + virtual ByteVector render(const String &s) const; + }; + + //! The main class in the ID3v2 implementation + + /*! + * This is the main class in the INFO tag implementation. RIFF INFO tag is a + * metadata format found in WAV audio and AVI video files. Though it is a part + * of Microsoft/IBM's RIFF specification, the author could not find the official + * documents about it. So, this implementation is referring to unofficial documents + * online and some applications' behaviors especially Windows Explorer. + */ + class TAGLIB_EXPORT Tag : public TagLib::Tag + { + public: + /*! + * Constructs an empty Info tag. + */ + Tag(); + + /*! + * Constructs an Info tag read from \a data which is contents of "LIST" chunk. + */ + Tag(const ByteVector &data); + + virtual ~Tag(); + + // Reimplementations + + virtual String title() const; + virtual String artist() const; + virtual String album() const; + virtual String comment() const; + virtual String genre() const; + virtual uint year() const; + virtual uint track() const; + + virtual void setTitle(const String &s); + virtual void setArtist(const String &s); + virtual void setAlbum(const String &s); + virtual void setComment(const String &s); + virtual void setGenre(const String &s); + virtual void setYear(uint i); + virtual void setTrack(uint i); + + virtual bool isEmpty() const; + + /*! + * Returns a copy of the internal fields of the tag. The returned map directly + * reflects the contents of the "INFO" chunk. + * + * \note Modifying this map does not affect the tag's internal data. + * Use setFieldText() and removeField() instead. + * + * \see setFieldText() + * \see removeField() + */ + FieldListMap fieldListMap() const; + + /* + * Gets the value of the field with the ID \a id. + */ + String fieldText(const ByteVector &id) const; + + /* + * Sets the value of the field with the ID \a id to \a s. + * If the field does not exist, it is created. + * If \s is empty, the field is removed. + * + * \note fieldId must be four-byte long pure ASCII string. This function + * performs nothing if fieldId is invalid. + */ + void setFieldText(const ByteVector &id, const String &s); + + /* + * Removes the field with the ID \a id. + */ + void removeField(const ByteVector &id); + + /*! + * Render the tag back to binary data, suitable to be written to disk. + * + * \note Returns empty ByteVector is the tag contains no fields. + */ + ByteVector render() const; + + /*! + * Sets the string handler that decides how the text data will be + * converted to and from binary data. + * If the parameter \a handler is null, the previous handler is + * released and default UTF-8 handler is restored. + * + * \note The caller is responsible for deleting the previous handler + * as needed after it is released. + * + * \see StringHandler + */ + static void setStringHandler(const StringHandler *handler); + + protected: + /*! + * Pareses the body of the tag in \a data. + */ + void parse(const ByteVector &data); + + + private: + Tag(const Tag &); + Tag &operator=(const Tag &); + + class TagPrivate; + TagPrivate *d; + }; + }} +} + +#endif diff --git a/taglib/taglib/riff/wav/wavfile.cpp b/taglib/taglib/riff/wav/wavfile.cpp new file mode 100644 index 0000000..abecea3 --- /dev/null +++ b/taglib/taglib/riff/wav/wavfile.cpp @@ -0,0 +1,244 @@ +/*************************************************************************** + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tbytevector.h" +#include "tdebug.h" +#include "tstringlist.h" +#include "tpropertymap.h" + +#include "wavfile.h" +#include "id3v2tag.h" +#include "infotag.h" +#include "tagunion.h" + +using namespace TagLib; + +namespace +{ + enum { ID3v2Index = 0, InfoIndex = 1 }; +} + +class RIFF::WAV::File::FilePrivate +{ +public: + FilePrivate() : + properties(0), + tagChunkID("ID3 "), + hasID3v2(false), + hasInfo(false) + { + } + + ~FilePrivate() + { + delete properties; + } + + Properties *properties; + + ByteVector tagChunkID; + + TagUnion tag; + + bool hasID3v2; + bool hasInfo; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +RIFF::WAV::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : RIFF::File(file, LittleEndian) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +RIFF::WAV::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : RIFF::File(stream, LittleEndian) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +RIFF::WAV::File::~File() +{ + delete d; +} + +ID3v2::Tag *RIFF::WAV::File::tag() const +{ + return ID3v2Tag(); +} + +ID3v2::Tag *RIFF::WAV::File::ID3v2Tag() const +{ + return d->tag.access<ID3v2::Tag>(ID3v2Index, false); +} + +RIFF::Info::Tag *RIFF::WAV::File::InfoTag() const +{ + return d->tag.access<RIFF::Info::Tag>(InfoIndex, false); +} + +PropertyMap RIFF::WAV::File::properties() const +{ + return tag()->properties(); +} + +void RIFF::WAV::File::removeUnsupportedProperties(const StringList &unsupported) +{ + tag()->removeUnsupportedProperties(unsupported); +} + +PropertyMap RIFF::WAV::File::setProperties(const PropertyMap &properties) +{ + return tag()->setProperties(properties); +} + +RIFF::WAV::Properties *RIFF::WAV::File::audioProperties() const +{ + return d->properties; +} + +bool RIFF::WAV::File::save() +{ + return RIFF::WAV::File::save(AllTags); +} + +bool RIFF::WAV::File::save(TagTypes tags, bool stripOthers, int id3v2Version) +{ + if(readOnly()) { + debug("RIFF::WAV::File::save() -- File is read only."); + return false; + } + + if(!isValid()) { + debug("RIFF::WAV::File::save() -- Trying to save invalid file."); + return false; + } + + if(stripOthers) + strip(static_cast<TagTypes>(AllTags & ~tags)); + + ID3v2::Tag *id3v2tag = d->tag.access<ID3v2::Tag>(ID3v2Index, false); + if(!id3v2tag->isEmpty()) { + if(tags & ID3v2) { + setChunkData(d->tagChunkID, id3v2tag->render(id3v2Version)); + d->hasID3v2 = true; + } + } + + Info::Tag *infotag = d->tag.access<Info::Tag>(InfoIndex, false); + if(!infotag->isEmpty()) { + if(tags & Info) { + int chunkId = findInfoTagChunk(); + if(chunkId != -1) + setChunkData(chunkId, infotag->render()); + else + setChunkData("LIST", infotag->render(), true); + + d->hasInfo = true; + } + } + + return true; +} + +bool RIFF::WAV::File::hasID3v2Tag() const +{ + return d->hasID3v2; +} + +bool RIFF::WAV::File::hasInfoTag() const +{ + return d->hasInfo; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void RIFF::WAV::File::read(bool readProperties, Properties::ReadStyle propertiesStyle) +{ + ByteVector formatData; + uint streamLength = 0; + for(uint i = 0; i < chunkCount(); i++) { + String name = chunkName(i); + if(name == "ID3 " || name == "id3 ") { + d->tagChunkID = chunkName(i); + d->tag.set(ID3v2Index, new ID3v2::Tag(this, chunkOffset(i))); + d->hasID3v2 = true; + } + else if(name == "fmt " && readProperties) + formatData = chunkData(i); + else if(name == "data" && readProperties) + streamLength = chunkDataSize(i); + else if(name == "LIST") { + ByteVector data = chunkData(i); + ByteVector type = data.mid(0, 4); + + if(type == "INFO") { + d->tag.set(InfoIndex, new RIFF::Info::Tag(data)); + d->hasInfo = true; + } + } + } + + if (!d->tag[ID3v2Index]) + d->tag.set(ID3v2Index, new ID3v2::Tag); + + if (!d->tag[InfoIndex]) + d->tag.set(InfoIndex, new RIFF::Info::Tag); + + if(!formatData.isEmpty()) + d->properties = new Properties(formatData, streamLength, propertiesStyle); +} + +void RIFF::WAV::File::strip(TagTypes tags) +{ + if(tags & ID3v2) + removeChunk(d->tagChunkID); + + if(tags & Info){ + TagLib::uint chunkId = findInfoTagChunk(); + if(chunkId != TagLib::uint(-1)) + removeChunk(chunkId); + } +} + +TagLib::uint RIFF::WAV::File::findInfoTagChunk() +{ + for(uint i = 0; i < chunkCount(); ++i) { + if(chunkName(i) == "LIST" && chunkData(i).mid(0, 4) == "INFO") { + return i; + } + } + + return TagLib::uint(-1); +} diff --git a/taglib/taglib/riff/wav/wavfile.h b/taglib/taglib/riff/wav/wavfile.h new file mode 100644 index 0000000..2f83332 --- /dev/null +++ b/taglib/taglib/riff/wav/wavfile.h @@ -0,0 +1,189 @@ +/*************************************************************************** + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_WAVFILE_H +#define TAGLIB_WAVFILE_H + +#include "rifffile.h" +#include "id3v2tag.h" +#include "infotag.h" +#include "wavproperties.h" + +namespace TagLib { + + namespace RIFF { + + //! An implementation of WAV metadata + + /*! + * This is implementation of WAV metadata. + * + * This supports an ID3v2 tag as well as reading stream from the ID3 RIFF + * chunk as well as properties from the file. + */ + + namespace WAV { + + //! An implementation of TagLib::File with WAV specific methods + + /*! + * This implements and provides an interface for WAV files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to WAV files. + */ + + class TAGLIB_EXPORT File : public TagLib::RIFF::File + { + public: + enum TagTypes { + //! Empty set. Matches no tag types. + NoTags = 0x0000, + //! Matches ID3v2 tags. + ID3v2 = 0x0001, + //! Matches Info tags. + Info = 0x0002, + //! Matches all tag types. + AllTags = 0xffff + }; + + /*! + * Constructs a WAV file from \a file. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs a WAV file from \a stream. If \a readProperties is true the + * file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the ID3v2 Tag for this file. + * + * \note This method does not return all the tags for this file for + * backward compatibility. Will be fixed in TagLib 2.0. + */ + ID3v2::Tag *tag() const; + + /*! + * Returns the ID3v2 Tag for this file. + * + * \note This always returns a valid pointer regardless of whether or not + * the file on disk has an ID3v2 tag. Use hasID3v2Tag() to check if the + * file on disk actually has an ID3v2 tag. + * + * \see hasID3v2Tag() + */ + ID3v2::Tag *ID3v2Tag() const; + + /*! + * Returns the RIFF INFO Tag for this file. + * + * \note This always returns a valid pointer regardless of whether or not + * the file on disk has a RIFF INFO tag. Use hasInfoTag() to check if the + * file on disk actually has a RIFF INFO tag. + * + * \see hasInfoTag() + */ + Info::Tag *InfoTag() const; + + /*! + * Implements the unified property interface -- export function. + * This method forwards to ID3v2::Tag::properties(). + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * This method forwards to ID3v2::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the WAV::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + /*! + * Saves the file. + */ + virtual bool save(); + + bool save(TagTypes tags, bool stripOthers = true, int id3v2Version = 4); + + /*! + * Returns whether or not the file on disk actually has an ID3v2 tag. + * + * \see ID3v2Tag() + */ + bool hasID3v2Tag() const; + + /*! + * Returns whether or not the file on disk actually has a RIFF INFO tag. + * + * \see InfoTag() + */ + bool hasInfoTag() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + + void strip(TagTypes tags); + + /*! + * Returns the index of the chunk that its name is "LIST" and list type is "INFO". + */ + uint findInfoTagChunk(); + + class FilePrivate; + FilePrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/taglib/riff/wav/wavproperties.cpp b/taglib/taglib/riff/wav/wavproperties.cpp new file mode 100644 index 0000000..8062df5 --- /dev/null +++ b/taglib/taglib/riff/wav/wavproperties.cpp @@ -0,0 +1,129 @@ +/*************************************************************************** + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "wavproperties.h" + +#include <tstring.h> +#include <tdebug.h> +#include <cmath> +#include <math.h> + +using namespace TagLib; + +class RIFF::WAV::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(uint streamLength = 0) : + format(0), + length(0), + bitrate(0), + sampleRate(0), + channels(0), + sampleWidth(0), + sampleFrames(0), + streamLength(streamLength) + { + + } + + short format; + int length; + int bitrate; + int sampleRate; + int channels; + int sampleWidth; + uint sampleFrames; + uint streamLength; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +RIFF::WAV::Properties::Properties(const ByteVector &data, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(); + read(data); +} + +RIFF::WAV::Properties::Properties(const ByteVector &data, uint streamLength, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(streamLength); + read(data); +} + +RIFF::WAV::Properties::~Properties() +{ + delete d; +} + +int RIFF::WAV::Properties::length() const +{ + return d->length; +} + +int RIFF::WAV::Properties::bitrate() const +{ + return d->bitrate; +} + +int RIFF::WAV::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int RIFF::WAV::Properties::channels() const +{ + return d->channels; +} + +int RIFF::WAV::Properties::sampleWidth() const +{ + return d->sampleWidth; +} + +TagLib::uint RIFF::WAV::Properties::sampleFrames() const +{ + return d->sampleFrames; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void RIFF::WAV::Properties::read(const ByteVector &data) +{ + d->format = data.toShort(0, false); + d->channels = data.toShort(2, false); + d->sampleRate = data.toUInt(4, false); + d->sampleWidth = data.toShort(14, false); + + const uint byteRate = data.toUInt(8, false); + d->bitrate = byteRate * 8 / 1000; + + d->length = byteRate > 0 ? d->streamLength / byteRate : 0; + if(d->channels > 0 && d->sampleWidth > 0) + d->sampleFrames = d->streamLength / (d->channels * ((d->sampleWidth + 7) / 8)); +} diff --git a/taglib/taglib/riff/wav/wavproperties.h b/taglib/taglib/riff/wav/wavproperties.h new file mode 100644 index 0000000..2023f53 --- /dev/null +++ b/taglib/taglib/riff/wav/wavproperties.h @@ -0,0 +1,92 @@ +/*************************************************************************** + copyright : (C) 2008 by Scott Wheeler + email : wheeler@kde.org +***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_WAVPROPERTIES_H +#define TAGLIB_WAVPROPERTIES_H + +#include "taglib.h" +#include "audioproperties.h" + +namespace TagLib { + + class ByteVector; + + namespace RIFF { + + namespace WAV { + + class File; + + //! An implementation of audio property reading for WAV + + /*! + * This reads the data from an WAV stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of WAV::Properties with the data read from the + * ByteVector \a data. + */ + Properties(const ByteVector &data, ReadStyle style); + + /*! + * Create an instance of WAV::Properties with the data read from the + * ByteVector \a data and the length calculated using \a streamLength. + */ + Properties(const ByteVector &data, uint streamLength, ReadStyle style); + + /*! + * Destroys this WAV::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + int sampleWidth() const; + uint sampleFrames() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(const ByteVector &data); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } + } +} + +#endif diff --git a/taglib/taglib/s3m/s3mfile.cpp b/taglib/taglib/s3m/s3mfile.cpp new file mode 100644 index 0000000..371340a --- /dev/null +++ b/taglib/taglib/s3m/s3mfile.cpp @@ -0,0 +1,243 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "s3mfile.h" +#include "tstringlist.h" +#include "tdebug.h" +#include "modfileprivate.h" +#include "tpropertymap.h" + +#include <iostream> + +using namespace TagLib; +using namespace S3M; + +class S3M::File::FilePrivate +{ +public: + FilePrivate(AudioProperties::ReadStyle propertiesStyle) + : properties(propertiesStyle) + { + } + + Mod::Tag tag; + S3M::Properties properties; +}; + +S3M::File::File(FileName file, bool readProperties, + AudioProperties::ReadStyle propertiesStyle) : + Mod::FileBase(file), + d(new FilePrivate(propertiesStyle)) +{ + if(isOpen()) + read(readProperties); +} + +S3M::File::File(IOStream *stream, bool readProperties, + AudioProperties::ReadStyle propertiesStyle) : + Mod::FileBase(stream), + d(new FilePrivate(propertiesStyle)) +{ + if(isOpen()) + read(readProperties); +} + +S3M::File::~File() +{ + delete d; +} + +Mod::Tag *S3M::File::tag() const +{ + return &d->tag; +} + +PropertyMap S3M::File::properties() const +{ + return d->tag.properties(); +} + +PropertyMap S3M::File::setProperties(const PropertyMap &properties) +{ + return d->tag.setProperties(properties); +} + +S3M::Properties *S3M::File::audioProperties() const +{ + return &d->properties; +} + +bool S3M::File::save() +{ + if(readOnly()) { + debug("S3M::File::save() - Cannot save to a read only file."); + return false; + } + // note: if title starts with "Extended Module: " + // the file would look like an .xm file + seek(0); + writeString(d->tag.title(), 27); + // string terminating NUL is not optional: + writeByte(0); + + seek(32); + + ushort length = 0; + ushort sampleCount = 0; + + if(!readU16L(length) || !readU16L(sampleCount)) + return false; + + seek(28, Current); + + int channels = 0; + for(int i = 0; i < 32; ++ i) { + uchar setting = 0; + if(!readByte(setting)) + return false; + // or if(setting >= 128)? + // or channels = i + 1;? + // need a better spec! + if(setting != 0xff) ++ channels; + } + + seek(channels, Current); + + StringList lines = d->tag.comment().split("\n"); + // write comment as sample names: + for(ushort i = 0; i < sampleCount; ++ i) { + seek(96L + length + ((long)i << 1)); + + ushort instrumentOffset = 0; + if(!readU16L(instrumentOffset)) + return false; + seek(((long)instrumentOffset << 4) + 48); + + if(i < lines.size()) + writeString(lines[i], 27); + else + writeString(String::null, 27); + // string terminating NUL is not optional: + writeByte(0); + } + return true; +} + +void S3M::File::read(bool) +{ + if(!isOpen()) + return; + + READ_STRING(d->tag.setTitle, 28); + READ_BYTE_AS(mark); + READ_BYTE_AS(type); + + READ_ASSERT(mark == 0x1A && type == 0x10); + + seek(32); + + READ_U16L_AS(length); + READ_U16L_AS(sampleCount); + + d->properties.setSampleCount(sampleCount); + + READ_U16L(d->properties.setPatternCount); + READ_U16L(d->properties.setFlags); + READ_U16L(d->properties.setTrackerVersion); + READ_U16L(d->properties.setFileFormatVersion); + + READ_ASSERT(readBlock(4) == "SCRM"); + + READ_BYTE(d->properties.setGlobalVolume); + READ_BYTE(d->properties.setBpmSpeed); + READ_BYTE(d->properties.setTempo); + + READ_BYTE_AS(masterVolume); + d->properties.setMasterVolume(masterVolume & 0x7f); + d->properties.setStereo((masterVolume & 0x80) != 0); + + // I've seen players who call the next two bytes + // "ultra click" and "use panning values" (if == 0xFC). + // I don't see them in any spec, though. + // Hm, but there is "UltraClick-removal" and some other + // variables in ScreamTracker IIIs GUI. + + seek(12, Current); + + int channels = 0; + for(int i = 0; i < 32; ++ i) { + READ_BYTE_AS(setting); + // or if(setting >= 128)? + // or channels = i + 1;? + // need a better spec! + if(setting != 0xff) ++ channels; + } + d->properties.setChannels(channels); + + seek(96); + ushort realLength = 0; + for(ushort i = 0; i < length; ++ i) { + READ_BYTE_AS(order); + if(order == 255) break; + if(order != 254) ++ realLength; + } + d->properties.setLengthInPatterns(realLength); + + seek(channels, Current); + + // Note: The S3M spec mentions samples and instruments, but in + // the header there are only pointers to instruments. + // However, there I never found instruments (SCRI) but + // instead samples (SCRS). + StringList comment; + for(ushort i = 0; i < sampleCount; ++ i) { + seek(96L + length + ((long)i << 1)); + + READ_U16L_AS(sampleHeaderOffset); + seek((long)sampleHeaderOffset << 4); + + READ_BYTE_AS(sampleType); + READ_STRING_AS(dosFileName, 13); + READ_U16L_AS(sampleDataOffset); + READ_U32L_AS(sampleLength); + READ_U32L_AS(repeatStart); + READ_U32L_AS(repeatStop); + READ_BYTE_AS(sampleVolume); + + seek(1, Current); + + READ_BYTE_AS(packing); + READ_BYTE_AS(sampleFlags); + READ_U32L_AS(baseFrequency); + + seek(12, Current); + + READ_STRING_AS(sampleName, 28); + // The next 4 bytes should be "SCRS", but I've found + // files that are otherwise ok with 4 nils instead. + // READ_ASSERT(readBlock(4) == "SCRS"); + + comment.append(sampleName); + } + + d->tag.setComment(comment.toString("\n")); + d->tag.setTrackerName("ScreamTracker III"); +} diff --git a/taglib/taglib/s3m/s3mfile.h b/taglib/taglib/s3m/s3mfile.h new file mode 100644 index 0000000..5f45297 --- /dev/null +++ b/taglib/taglib/s3m/s3mfile.h @@ -0,0 +1,108 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_S3MFILE_H +#define TAGLIB_S3MFILE_H + +#include "tfile.h" +#include "audioproperties.h" +#include "taglib_export.h" +#include "modfilebase.h" +#include "modtag.h" +#include "s3mproperties.h" + +namespace TagLib { + + namespace S3M { + + class TAGLIB_EXPORT File : public Mod::FileBase { + public: + /*! + * Constructs a ScreamTracker III from \a file. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + */ + File(FileName file, bool readProperties = true, + AudioProperties::ReadStyle propertiesStyle = + AudioProperties::Average); + + /*! + * Constructs a ScreamTracker III file from \a stream. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + */ + File(IOStream *stream, bool readProperties = true, + AudioProperties::ReadStyle propertiesStyle = + AudioProperties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + Mod::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * Forwards to Mod::Tag::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Forwards to Mod::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the S3M::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + S3M::Properties *audioProperties() const; + + /*! + * Save the file. + * This is the same as calling save(AllTags); + * + * \note Saving ScreamTracker III tags is not supported. + */ + bool save(); + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/s3m/s3mproperties.cpp b/taglib/taglib/s3m/s3mproperties.cpp new file mode 100644 index 0000000..a1a010a --- /dev/null +++ b/taglib/taglib/s3m/s3mproperties.cpp @@ -0,0 +1,204 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "s3mproperties.h" + +using namespace TagLib; +using namespace S3M; + +class S3M::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate() : + lengthInPatterns(0), + channels(0), + stereo(false), + sampleCount(0), + patternCount(0), + flags(0), + trackerVersion(0), + fileFormatVersion(0), + globalVolume(0), + masterVolume(0), + tempo(0), + bpmSpeed(0) + { + } + + ushort lengthInPatterns; + int channels; + bool stereo; + ushort sampleCount; + ushort patternCount; + ushort flags; + ushort trackerVersion; + ushort fileFormatVersion; + uchar globalVolume; + uchar masterVolume; + uchar tempo; + uchar bpmSpeed; +}; + +S3M::Properties::Properties(AudioProperties::ReadStyle propertiesStyle) : + AudioProperties(propertiesStyle), + d(new PropertiesPrivate) +{ +} + +S3M::Properties::~Properties() +{ + delete d; +} + +int S3M::Properties::length() const +{ + return 0; +} + +int S3M::Properties::bitrate() const +{ + return 0; +} + +int S3M::Properties::sampleRate() const +{ + return 0; +} + +int S3M::Properties::channels() const +{ + return d->channels; +} + +TagLib::ushort S3M::Properties::lengthInPatterns() const +{ + return d->lengthInPatterns; +} + +bool S3M::Properties::stereo() const +{ + return d->stereo; +} + +TagLib::ushort S3M::Properties::sampleCount() const +{ + return d->sampleCount; +} + +TagLib::ushort S3M::Properties::patternCount() const +{ + return d->patternCount; +} + +TagLib::ushort S3M::Properties::flags() const +{ + return d->flags; +} + +TagLib::ushort S3M::Properties::trackerVersion() const +{ + return d->trackerVersion; +} + +TagLib::ushort S3M::Properties::fileFormatVersion() const +{ + return d->fileFormatVersion; +} + +uchar S3M::Properties::globalVolume() const +{ + return d->globalVolume; +} + +uchar S3M::Properties::masterVolume() const +{ + return d->masterVolume; +} + +uchar S3M::Properties::tempo() const +{ + return d->tempo; +} + +uchar S3M::Properties::bpmSpeed() const +{ + return d->bpmSpeed; +} + +void S3M::Properties::setLengthInPatterns(ushort lengthInPatterns) +{ + d->lengthInPatterns = lengthInPatterns; +} + +void S3M::Properties::setChannels(int channels) +{ + d->channels = channels; +} + +void S3M::Properties::setStereo(bool stereo) +{ + d->stereo = stereo; +} + +void S3M::Properties::setSampleCount(ushort sampleCount) +{ + d->sampleCount = sampleCount; +} + +void S3M::Properties::setPatternCount(ushort patternCount) +{ + d->patternCount = patternCount; +} + +void S3M::Properties::setFlags(ushort flags) +{ + d->flags = flags; +} + +void S3M::Properties::setTrackerVersion(ushort trackerVersion) +{ + d->trackerVersion = trackerVersion; +} + +void S3M::Properties::setFileFormatVersion(ushort fileFormatVersion) +{ + d->fileFormatVersion = fileFormatVersion; +} + +void S3M::Properties::setGlobalVolume(uchar globalVolume) +{ + d->globalVolume = globalVolume; +} + +void S3M::Properties::setMasterVolume(uchar masterVolume) +{ + d->masterVolume = masterVolume; +} + +void S3M::Properties::setTempo(uchar tempo) +{ + d->tempo = tempo; +} + +void S3M::Properties::setBpmSpeed(uchar bpmSpeed) +{ + d->bpmSpeed = bpmSpeed; +} diff --git a/taglib/taglib/s3m/s3mproperties.h b/taglib/taglib/s3m/s3mproperties.h new file mode 100644 index 0000000..6e9f1d2 --- /dev/null +++ b/taglib/taglib/s3m/s3mproperties.h @@ -0,0 +1,88 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_S3MPROPERTIES_H +#define TAGLIB_S3MPROPERTIES_H + +#include "taglib.h" +#include "audioproperties.h" + +namespace TagLib { + namespace S3M { + class TAGLIB_EXPORT Properties : public AudioProperties { + friend class File; + public: + /*! Flag bits. */ + enum { + ST2Vibrato = 1, + ST2Tempo = 2, + AmigaSlides = 4, + Vol0MixOptimizations = 8, + AmigaLimits = 16, + EnableFilter = 32, + CustomData = 128 + }; + + Properties(AudioProperties::ReadStyle propertiesStyle); + virtual ~Properties(); + + int length() const; + int bitrate() const; + int sampleRate() const; + int channels() const; + + ushort lengthInPatterns() const; + bool stereo() const; + ushort sampleCount() const; + ushort patternCount() const; + ushort flags() const; + ushort trackerVersion() const; + ushort fileFormatVersion() const; + uchar globalVolume() const; + uchar masterVolume() const; + uchar tempo() const; + uchar bpmSpeed() const; + + void setChannels(int channels); + + void setLengthInPatterns (ushort lengthInPatterns); + void setStereo (bool stereo); + void setSampleCount (ushort sampleCount); + void setPatternCount (ushort patternCount); + void setFlags (ushort flags); + void setTrackerVersion (ushort trackerVersion); + void setFileFormatVersion(ushort fileFormatVersion); + void setGlobalVolume (uchar globalVolume); + void setMasterVolume (uchar masterVolume); + void setTempo (uchar tempo); + void setBpmSpeed (uchar bpmSpeed); + + private: + Properties(const Properties&); + Properties &operator=(const Properties&); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/tag.cpp b/taglib/taglib/tag.cpp new file mode 100644 index 0000000..6763408 --- /dev/null +++ b/taglib/taglib/tag.cpp @@ -0,0 +1,180 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tag.h" +#include "tstringlist.h" +#include "tpropertymap.h" + +using namespace TagLib; + +class Tag::TagPrivate +{ + +}; + +Tag::Tag() +{ + +} + +Tag::~Tag() +{ + +} + +bool Tag::isEmpty() const +{ + return (title().isEmpty() && + artist().isEmpty() && + album().isEmpty() && + comment().isEmpty() && + genre().isEmpty() && + year() == 0 && + track() == 0); +} + +PropertyMap Tag::properties() const +{ + PropertyMap map; + if(!(title().isNull())) + map["TITLE"].append(title()); + if(!(artist().isNull())) + map["ARTIST"].append(artist()); + if(!(album().isNull())) + map["ALBUM"].append(album()); + if(!(comment().isNull())) + map["COMMENT"].append(comment()); + if(!(genre().isNull())) + map["GENRE"].append(genre()); + if(!(year() == 0)) + map["DATE"].append(String::number(year())); + if(!(track() == 0)) + map["TRACKNUMBER"].append(String::number(track())); + return map; +} + +void Tag::removeUnsupportedProperties(const StringList&) +{ +} + +PropertyMap Tag::setProperties(const PropertyMap &origProps) +{ + PropertyMap properties(origProps); + properties.removeEmpty(); + StringList oneValueSet; + // can this be simplified by using some preprocessor defines / function pointers? + if(properties.contains("TITLE")) { + setTitle(properties["TITLE"].front()); + oneValueSet.append("TITLE"); + } else + setTitle(String::null); + + if(properties.contains("ARTIST")) { + setArtist(properties["ARTIST"].front()); + oneValueSet.append("ARTIST"); + } else + setArtist(String::null); + + if(properties.contains("ALBUM")) { + setAlbum(properties["ALBUM"].front()); + oneValueSet.append("ALBUM"); + } else + setAlbum(String::null); + + if(properties.contains("COMMENT")) { + setComment(properties["COMMENT"].front()); + oneValueSet.append("COMMENT"); + } else + setComment(String::null); + + if(properties.contains("GENRE")) { + setGenre(properties["GENRE"].front()); + oneValueSet.append("GENRE"); + } else + setGenre(String::null); + + if(properties.contains("DATE")) { + bool ok; + int date = properties["DATE"].front().toInt(&ok); + if(ok) { + setYear(date); + oneValueSet.append("DATE"); + } else + setYear(0); + } + else + setYear(0); + + if(properties.contains("TRACKNUMBER")) { + bool ok; + int track = properties["TRACKNUMBER"].front().toInt(&ok); + if(ok) { + setTrack(track); + oneValueSet.append("TRACKNUMBER"); + } else + setTrack(0); + } + else + setYear(0); + + // for each tag that has been set above, remove the first entry in the corresponding + // value list. The others will be returned as unsupported by this format. + for(StringList::Iterator it = oneValueSet.begin(); it != oneValueSet.end(); ++it) { + if(properties[*it].size() == 1) + properties.erase(*it); + else + properties[*it].erase( properties[*it].begin() ); + } + return properties; +} + +void Tag::duplicate(const Tag *source, Tag *target, bool overwrite) // static +{ + if(overwrite) { + target->setTitle(source->title()); + target->setArtist(source->artist()); + target->setAlbum(source->album()); + target->setComment(source->comment()); + target->setGenre(source->genre()); + target->setYear(source->year()); + target->setTrack(source->track()); + } + else { + if(target->title().isEmpty()) + target->setTitle(source->title()); + if(target->artist().isEmpty()) + target->setArtist(source->artist()); + if(target->album().isEmpty()) + target->setAlbum(source->album()); + if(target->comment().isEmpty()) + target->setComment(source->comment()); + if(target->genre().isEmpty()) + target->setGenre(source->genre()); + if(target->year() <= 0) + target->setYear(source->year()); + if(target->track() <= 0) + target->setTrack(source->track()); + } +} diff --git a/taglib/taglib/tag.h b/taglib/taglib/tag.h new file mode 100644 index 0000000..76c9a82 --- /dev/null +++ b/taglib/taglib/tag.h @@ -0,0 +1,201 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_TAG_H +#define TAGLIB_TAG_H + +#include "taglib_export.h" +#include "tstring.h" + +namespace TagLib { + + //! A simple, generic interface to common audio meta data fields + + /*! + * This is an attempt to abstract away the difference in the meta data formats + * of various audio codecs and tagging schemes. As such it is generally a + * subset of what is available in the specific formats but should be suitable + * for most applications. This is meant to compliment the generic APIs found + * in TagLib::AudioProperties, TagLib::File and TagLib::FileRef. + */ + + class PropertyMap; + + class TAGLIB_EXPORT Tag + { + public: + + /*! + * Detroys this Tag instance. + */ + virtual ~Tag(); + + /*! + * Exports the tags of the file as dictionary mapping (human readable) tag + * names (Strings) to StringLists of tag values. + * The default implementation in this class considers only the usual built-in + * tags (artist, album, ...) and only one value per key. + */ + PropertyMap properties() const; + + /*! + * Removes unsupported properties, or a subset of them, from the tag. + * The parameter \a properties must contain only entries from + * properties().unsupportedData(). + * BIC: Will become virtual in future releases. Currently the non-virtual + * standard implementation of TagLib::Tag does nothing, since there are + * no unsupported elements. + */ + void removeUnsupportedProperties(const StringList& properties); + + /*! + * Sets the tags of this File to those specified in \a properties. This default + * implementation sets only the tags for which setter methods exist in this class + * (artist, album, ...), and only one value per key; the rest will be contained + * in the returned PropertyMap. + */ + PropertyMap setProperties(const PropertyMap &properties); + + /*! + * Returns the track name; if no track name is present in the tag + * String::null will be returned. + */ + virtual String title() const = 0; + + /*! + * Returns the artist name; if no artist name is present in the tag + * String::null will be returned. + */ + virtual String artist() const = 0; + + /*! + * Returns the album name; if no album name is present in the tag + * String::null will be returned. + */ + virtual String album() const = 0; + + /*! + * Returns the track comment; if no comment is present in the tag + * String::null will be returned. + */ + virtual String comment() const = 0; + + /*! + * Returns the genre name; if no genre is present in the tag String::null + * will be returned. + */ + virtual String genre() const = 0; + + /*! + * Returns the year; if there is no year set, this will return 0. + */ + virtual uint year() const = 0; + + /*! + * Returns the track number; if there is no track number set, this will + * return 0. + */ + virtual uint track() const = 0; + + /*! + * Sets the title to \a s. If \a s is String::null then this value will be + * cleared. + */ + virtual void setTitle(const String &s) = 0; + + /*! + * Sets the artist to \a s. If \a s is String::null then this value will be + * cleared. + */ + virtual void setArtist(const String &s) = 0; + + /*! + * Sets the album to \a s. If \a s is String::null then this value will be + * cleared. + */ + virtual void setAlbum(const String &s) = 0; + + /*! + * Sets the comment to \a s. If \a s is String::null then this value will be + * cleared. + */ + virtual void setComment(const String &s) = 0; + + /*! + * Sets the genre to \a s. If \a s is String::null then this value will be + * cleared. For tag formats that use a fixed set of genres, the appropriate + * value will be selected based on a string comparison. A list of available + * genres for those formats should be available in that type's + * implementation. + */ + virtual void setGenre(const String &s) = 0; + + /*! + * Sets the year to \a i. If \a s is 0 then this value will be cleared. + */ + virtual void setYear(uint i) = 0; + + /*! + * Sets the track to \a i. If \a s is 0 then this value will be cleared. + */ + virtual void setTrack(uint i) = 0; + + /*! + * Returns true if the tag does not contain any data. This should be + * reimplemented in subclasses that provide more than the basic tagging + * abilities in this class. + */ + virtual bool isEmpty() const; + + /*! + * Copies the generic data from one tag to another. + * + * \note This will no affect any of the lower level details of the tag. For + * instance if any of the tag type specific data (maybe a URL for a band) is + * set, this will not modify or copy that. This just copies using the API + * in this class. + * + * If \a overwrite is true then the values will be unconditionally copied. + * If false only empty values will be overwritten. + */ + static void duplicate(const Tag *source, Tag *target, bool overwrite = true); + + protected: + /*! + * Construct a Tag. This is protected since tags should only be instantiated + * through subclasses. + */ + Tag(); + + private: + Tag(const Tag &); + Tag &operator=(const Tag &); + + class TagPrivate; + TagPrivate *d; + }; +} + +#endif diff --git a/taglib/taglib/taglib_config.h.cmake b/taglib/taglib/taglib_config.h.cmake new file mode 100644 index 0000000..0f499e2 --- /dev/null +++ b/taglib/taglib/taglib_config.h.cmake @@ -0,0 +1,4 @@ +/* taglib_config.h. Generated by cmake from taglib_config.h.cmake */ + +#define TAGLIB_WITH_ASF 1 +#define TAGLIB_WITH_MP4 1 diff --git a/taglib/taglib/taglib_export.h b/taglib/taglib/taglib_export.h new file mode 100644 index 0000000..737ae64 --- /dev/null +++ b/taglib/taglib/taglib_export.h @@ -0,0 +1,43 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_EXPORT_H +#define TAGLIB_EXPORT_H + +#if defined(TAGLIB_STATIC) +#define TAGLIB_EXPORT +#elif (defined(_WIN32) || defined(_WIN64)) +#ifdef MAKE_TAGLIB_LIB +#define TAGLIB_EXPORT __declspec(dllexport) +#else +#define TAGLIB_EXPORT __declspec(dllimport) +#endif +#elif defined(__GNUC__) && (__GNUC__ > 4 || __GNUC__ == 4 && __GNUC_MINOR__ >= 1) +#define TAGLIB_EXPORT __attribute__ ((visibility("default"))) +#else +#define TAGLIB_EXPORT +#endif + +#endif diff --git a/taglib/taglib/tagunion.cpp b/taglib/taglib/tagunion.cpp new file mode 100644 index 0000000..52d7136 --- /dev/null +++ b/taglib/taglib/tagunion.cpp @@ -0,0 +1,186 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tagunion.h" +#include "tstringlist.h" + +using namespace TagLib; + +#define stringUnion(method) \ + if(tag(0) && !tag(0)->method().isEmpty()) \ + return tag(0)->method(); \ + if(tag(1) && !tag(1)->method().isEmpty()) \ + return tag(1)->method(); \ + if(tag(2) && !tag(2)->method().isEmpty()) \ + return tag(2)->method(); \ + return String::null \ + +#define numberUnion(method) \ + if(tag(0) && tag(0)->method() > 0) \ + return tag(0)->method(); \ + if(tag(1) && tag(1)->method() > 0) \ + return tag(1)->method(); \ + if(tag(2) && tag(2)->method() > 0) \ + return tag(2)->method(); \ + return 0 + +#define setUnion(method, value) \ + if(tag(0)) \ + tag(0)->set##method(value); \ + if(tag(1)) \ + tag(1)->set##method(value); \ + if(tag(2)) \ + tag(2)->set##method(value); \ + +class TagUnion::TagUnionPrivate +{ +public: + TagUnionPrivate() : tags(3, static_cast<Tag *>(0)) + { + + } + + ~TagUnionPrivate() + { + delete tags[0]; + delete tags[1]; + delete tags[2]; + } + + std::vector<Tag *> tags; +}; + +TagUnion::TagUnion(Tag *first, Tag *second, Tag *third) +{ + d = new TagUnionPrivate; + + d->tags[0] = first; + d->tags[1] = second; + d->tags[2] = third; +} + +TagUnion::~TagUnion() +{ + delete d; +} + +Tag *TagUnion::operator[](int index) const +{ + return tag(index); +} + +Tag *TagUnion::tag(int index) const +{ + return d->tags[index]; +} + +void TagUnion::set(int index, Tag *tag) +{ + delete d->tags[index]; + d->tags[index] = tag; +} + +String TagUnion::title() const +{ + stringUnion(title); +} + +String TagUnion::artist() const +{ + stringUnion(artist); +} + +String TagUnion::album() const +{ + stringUnion(album); +} + +String TagUnion::comment() const +{ + stringUnion(comment); +} + +String TagUnion::genre() const +{ + stringUnion(genre); +} + +TagLib::uint TagUnion::year() const +{ + numberUnion(year); +} + +TagLib::uint TagUnion::track() const +{ + numberUnion(track); +} + +void TagUnion::setTitle(const String &s) +{ + setUnion(Title, s); +} + +void TagUnion::setArtist(const String &s) +{ + setUnion(Artist, s); +} + +void TagUnion::setAlbum(const String &s) +{ + setUnion(Album, s); +} + +void TagUnion::setComment(const String &s) +{ + setUnion(Comment, s); +} + +void TagUnion::setGenre(const String &s) +{ + setUnion(Genre, s); +} + +void TagUnion::setYear(uint i) +{ + setUnion(Year, i); +} + +void TagUnion::setTrack(uint i) +{ + setUnion(Track, i); +} + +bool TagUnion::isEmpty() const +{ + if(d->tags[0] && !d->tags[0]->isEmpty()) + return false; + if(d->tags[1] && !d->tags[1]->isEmpty()) + return false; + if(d->tags[2] && !d->tags[2]->isEmpty()) + return false; + + return true; +} + diff --git a/taglib/taglib/tagunion.h b/taglib/taglib/tagunion.h new file mode 100644 index 0000000..e94d523 --- /dev/null +++ b/taglib/taglib/tagunion.h @@ -0,0 +1,95 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_TAGUNION_H +#define TAGLIB_TAGUNION_H + +#include "tag.h" + +#ifndef DO_NOT_DOCUMENT + +namespace TagLib { + + /*! + * \internal + */ + + class TagUnion : public Tag + { + public: + + enum AccessType { Read, Write }; + + /*! + * Creates a TagLib::Tag that is the union of \a first, \a second, and + * \a third. The TagUnion takes ownership of these tags and will handle + * their deletion. + */ + TagUnion(Tag *first = 0, Tag *second = 0, Tag *third = 0); + + virtual ~TagUnion(); + + Tag *operator[](int index) const; + Tag *tag(int index) const; + + void set(int index, Tag *tag); + + virtual String title() const; + virtual String artist() const; + virtual String album() const; + virtual String comment() const; + virtual String genre() const; + virtual uint year() const; + virtual uint track() const; + + virtual void setTitle(const String &s); + virtual void setArtist(const String &s); + virtual void setAlbum(const String &s); + virtual void setComment(const String &s); + virtual void setGenre(const String &s); + virtual void setYear(uint i); + virtual void setTrack(uint i); + virtual bool isEmpty() const; + + template <class T> T *access(int index, bool create) + { + if(!create || tag(index)) + return static_cast<T *>(tag(index)); + + set(index, new T); + return static_cast<T *>(tag(index)); + } + + private: + TagUnion(const Tag &); + TagUnion &operator=(const Tag &); + + class TagUnionPrivate; + TagUnionPrivate *d; + }; +} + +#endif +#endif diff --git a/taglib/taglib/toolkit/taglib.h b/taglib/taglib/toolkit/taglib.h new file mode 100644 index 0000000..cbca107 --- /dev/null +++ b/taglib/taglib/toolkit/taglib.h @@ -0,0 +1,170 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_H +#define TAGLIB_H + +#include "taglib_config.h" + +#define TAGLIB_MAJOR_VERSION 1 +#define TAGLIB_MINOR_VERSION 9 +#define TAGLIB_PATCH_VERSION 1 + +#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 1)) +#define TAGLIB_IGNORE_MISSING_DESTRUCTOR _Pragma("GCC diagnostic ignored \"-Wnon-virtual-dtor\"") +#else +#define TAGLIB_IGNORE_MISSING_DESTRUCTOR +#endif + +#if (defined(_MSC_VER) && _MSC_VER >= 1600) +#define TAGLIB_CONSTRUCT_BITSET(x) static_cast<unsigned long long>(x) +#else +#define TAGLIB_CONSTRUCT_BITSET(x) static_cast<unsigned long>(x) +#endif + +#include <string> + +//! A namespace for all TagLib related classes and functions + +/*! + * This namespace contains everything in TagLib. For projects working with + * TagLib extensively it may be convenient to add a + * \code + * using namespace TagLib; + * \endcode + */ + +namespace TagLib { + + class String; + + typedef wchar_t wchar; // Assumed to be sufficient to store a UTF-16 char. + typedef unsigned char uchar; + typedef unsigned short ushort; + typedef unsigned int uint; + typedef unsigned long long ulonglong; + + // long/ulong can be either 32-bit or 64-bit wide. + typedef unsigned long ulong; + + /*! + * Unfortunately std::wstring isn't defined on some systems, (i.e. GCC < 3) + * so I'm providing something here that should be constant. + */ + typedef std::basic_string<wchar> wstring; +} + +/*! + * \mainpage TagLib + * + * \section intro Introduction + * + * TagLib is a library for reading and editing audio meta data, commonly know as \e tags. + * + * Features: + * - A clean, high level, C++ API to handling audio meta data. + * - Format specific APIs for advanced API users. + * - ID3v1, ID3v2, APE, FLAC, Xiph, iTunes-style MP4 and WMA tag formats. + * - MP3, MPC, FLAC, MP4, ASF, AIFF, WAV, TrueAudio, WavPack, Ogg FLAC, Ogg Vorbis, Speex and Opus file formats. + * - Basic audio file properties such as length, sample rate, etc. + * - Long term binary and source compatibility. + * - Extensible design, notably the ability to add other formats or extend current formats as a library user. + * - Full support for unicode and internationalized tags. + * - Dual <a href="http://www.mozilla.org/MPL/MPL-1.1.html">MPL</a> and + * <a href="http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html">LGPL</a> licenses. + * - No external toolkit dependancies. + * + * \section why Why TagLib? + * + * TagLib originally was written to provide an updated and improved ID3v2 implementation in C++ for use + * in a variety of Open Source projects. Since development began in 2002 and the 1.0 release in 2004 + * it has expanded to cover a wide variety of tag and file formats and is used in a wide variety of + * Open Source and proprietary applications. It now supports a variety of UNIXes, including Apple's OS + * X, as well as Microsoft Windows. + * + * \section commercial Usage in Commercial Applications + * + * TagLib's licenses \e do allow usage within propriety (\e closed) applications, however TagLib is \e not + * public domain. Please note the requirements of the LGPL or MPL, and adhere to at least one of them. + * In simple terms, you must at a minimum note your usage of TagLib, note the licensing terms of TagLib and + * if you make changes to TagLib publish them. Please review the licenses above before using TagLib in your + * software. Note that you may choose either the MPL or the LGPL, you do not have to fulfill the + * requirements of both. + * + * \section installing Installing TagLib + * + * Please see the <a href="http://developer.kde.org/~wheeler/taglib.html">TagLib website</a> for the latest + * downloads. + * + * TagLib can be built using the CMake build system. TagLib installs a taglib-config and pkg-config file to + * make it easier to integrate into various build systems. Note that TagLib's include install directory \e must + * be included in the header include path. Simply adding <taglib/tag.h> will \e not work. + * + * \section start Getting Started + * + * TagLib provides both simple, abstract APIs which make it possible to ignore the differences between tagging + * formats and format specific APIs which allow programmers to work with the features of specific tagging + * schemes. There is a similar abstraction mechanism for AudioProperties. + * + * The best place to start is with the <b>Class Hierarchy</b> linked at the top of the page. The File and + * AudioProperties classes and their subclasses are the core of TagLib. The FileRef class is also a convenient + * way for using a value-based handle. + * + * \note When working with FileRef please consider that it has only the most basic (extension-based) file + * type resolution. Please see its documentation on how to plug in more advanced file type resolution. (Such + * resolution may be part of later TagLib releases by default.) + * + * Here's a very simple example with TagLib: + * + * \code + * + * TagLib::FileRef f("Latex Solar Beef.mp3"); + * TagLib::String artist = f.tag()->artist(); // artist == "Frank Zappa" + * + * f.tag()->setAlbum("Fillmore East"); + * f.save(); + * + * TagLib::FileRef g("Free City Rhymes.ogg"); + * TagLib::String album = g.tag()->album(); // album == "NYC Ghosts & Flowers" + * + * g.tag()->setTrack(1); + * g.save(); + * + * \endcode + * + * More examples can be found in the \e examples directory of the source distribution. + * + * \section Contact + * + * Questions about TagLib should be directed to the TagLib mailing list, not directly to the author. + * + * - <a href="http://developer.kde.org/~wheeler/taglib/">TagLib Homepage</a> + * - <a href="https://mail.kde.org/mailman/listinfo/taglib-devel">TagLib Mailing List (taglib-devel@kde.org)</a> + * + * \author Scott Wheeler <wheeler@kde.org> et al. + * + */ + +#endif diff --git a/taglib/taglib/toolkit/tbytevector.cpp b/taglib/taglib/toolkit/tbytevector.cpp new file mode 100644 index 0000000..b658246 --- /dev/null +++ b/taglib/taglib/toolkit/tbytevector.cpp @@ -0,0 +1,857 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <algorithm> +#include <iostream> +#include <cstdio> +#include <cstring> + +#include <tstring.h> +#include <tdebug.h> +#include "trefcounter.h" +#include "tutils.h" + +#include "tbytevector.h" + +// This is a bit ugly to keep writing over and over again. + +// A rather obscure feature of the C++ spec that I hadn't thought of that makes +// working with C libs much more efficient. There's more here: +// +// http://www.informit.com/isapi/product_id~{9C84DAB4-FE6E-49C5-BB0A-FB50331233EA}/content/index.asp + +#define DATA(x) (&(x->data->data[0])) + +namespace TagLib { + +static const char hexTable[17] = "0123456789abcdef"; + +static const uint crcTable[256] = { + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, + 0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 0x4c11db70, 0x48d0c6c7, + 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, + 0x709f7b7a, 0x745e66cd, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0xbe2b5b58, 0xbaea46ef, + 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, + 0xceb42022, 0xca753d95, 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0x34867077, 0x30476dc0, + 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, + 0x0808d07d, 0x0cc9cdca, 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 0x5e9f46bf, 0x5a5e5b08, + 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, + 0xb6238b25, 0xb2e29692, 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 0xe0b41de7, 0xe4750050, + 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, + 0xdc3abded, 0xd8fba05a, 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 0x4f040d56, 0x4bc510e1, + 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, + 0x3f9b762c, 0x3b5a6b9b, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 0xf12f560e, 0xf5ee4bb9, + 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, + 0xcda1f604, 0xc960ebb3, 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 0x9b3660c6, 0x9ff77d71, + 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, + 0x470cdd2b, 0x43cdc09c, 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 0x119b4be9, 0x155a565e, + 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, + 0x2d15ebe3, 0x29d4f654, 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 0xe3a1cbc1, 0xe760d676, + 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, + 0x933eb0bb, 0x97ffad0c, 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 +}; + +/*! + * A templatized straightforward find that works with the types + * std::vector<char>::iterator and std::vector<char>::reverse_iterator. + */ +template <class TIterator> +int findChar( + const TIterator dataBegin, const TIterator dataEnd, + char c, uint offset, int byteAlign) +{ + const size_t dataSize = dataEnd - dataBegin; + if(dataSize == 0 || offset > dataSize - 1) + return -1; + + // n % 0 is invalid + + if(byteAlign == 0) + return -1; + + for(TIterator it = dataBegin + offset; it < dataEnd; it += byteAlign) { + if(*it == c) + return (it - dataBegin); + } + + return -1; +} + +/*! + * A templatized KMP find that works with the types + * std::vector<char>::iterator and std::vector<char>::reverse_iterator. + */ +template <class TIterator> +int findVector( + const TIterator dataBegin, const TIterator dataEnd, + const TIterator patternBegin, const TIterator patternEnd, + uint offset, int byteAlign) +{ + const size_t dataSize = dataEnd - dataBegin; + const size_t patternSize = patternEnd - patternBegin; + if(patternSize > dataSize || offset > dataSize - 1) + return -1; + + // n % 0 is invalid + + if(byteAlign == 0) + return -1; + + // Special case that pattern contains just single char. + + if(patternSize == 1) + return findChar(dataBegin, dataEnd, *patternBegin, offset, byteAlign); + + size_t lastOccurrence[256]; + + for(size_t i = 0; i < 256; ++i) + lastOccurrence[i] = patternSize; + + for(size_t i = 0; i < patternSize - 1; ++i) + lastOccurrence[static_cast<uchar>(*(patternBegin + i))] = patternSize - i - 1; + + TIterator it = dataBegin + patternSize - 1 + offset; + while(true) + { + TIterator itBuffer = it; + TIterator itPattern = patternBegin + patternSize - 1; + + while(*itBuffer == *itPattern) + { + if(itPattern == patternBegin) + { + if((itBuffer - dataBegin - offset) % byteAlign == 0) + return (itBuffer - dataBegin); + else + break; + } + + --itBuffer; + --itPattern; + } + + const size_t step = lastOccurrence[static_cast<uchar>(*it)]; + if(dataEnd - step <= it) + break; + + it += step; + } + + return -1; +} + +template <class T> +T toNumber(const ByteVector &v, size_t offset, size_t length, bool mostSignificantByteFirst) +{ + if(offset >= v.size()) { + debug("toNumber<T>() -- No data to convert. Returning 0."); + return 0; + } + + length = std::min(length, v.size() - offset); + + T sum = 0; + for(size_t i = 0; i < length; i++) { + const size_t shift = (mostSignificantByteFirst ? length - 1 - i : i) * 8; + sum |= static_cast<T>(static_cast<uchar>(v[offset + i])) << shift; + } + + return sum; +} + +template <class T> +T toNumber(const ByteVector &v, size_t offset, bool mostSignificantByteFirst) +{ + static const bool isBigEndian = (Utils::SystemByteOrder == Utils::BigEndian); + const bool swap = (mostSignificantByteFirst != isBigEndian); + + if(offset + sizeof(T) > v.size()) + return toNumber<T>(v, offset, v.size() - offset, mostSignificantByteFirst); + + // Uses memcpy instead of reinterpret_cast to avoid an alignment exception. + T tmp; + ::memcpy(&tmp, v.data() + offset, sizeof(T)); + + if(swap) + return Utils::byteSwap(tmp); + else + return tmp; +} + +template <class T> +ByteVector fromNumber(T value, bool mostSignificantByteFirst) +{ + static const bool isBigEndian = (Utils::SystemByteOrder == Utils::BigEndian); + const bool swap = (mostSignificantByteFirst != isBigEndian); + + if(swap) + value = Utils::byteSwap(value); + + return ByteVector(reinterpret_cast<const char *>(&value), sizeof(T)); +} + +class DataPrivate : public RefCounter +{ +public: + DataPrivate() + { + } + + DataPrivate(const std::vector<char> &v, uint offset, uint length) + : data(v.begin() + offset, v.begin() + offset + length) + { + } + + // A char* can be an iterator. + DataPrivate(const char *begin, const char *end) + : data(begin, end) + { + } + + DataPrivate(uint len, char c) + : data(len, c) + { + } + + std::vector<char> data; +}; + +class ByteVector::ByteVectorPrivate : public RefCounter +{ +public: + ByteVectorPrivate() + : RefCounter() + , data(new DataPrivate()) + , offset(0) + , length(0) + { + } + + ByteVectorPrivate(ByteVectorPrivate *d, uint o, uint l) + : RefCounter() + , data(d->data) + , offset(d->offset + o) + , length(l) + { + data->ref(); + } + + ByteVectorPrivate(const std::vector<char> &v, uint o, uint l) + : RefCounter() + , data(new DataPrivate(v, o, l)) + , offset(0) + , length(l) + { + } + + ByteVectorPrivate(uint l, char c) + : RefCounter() + , data(new DataPrivate(l, c)) + , offset(0) + , length(l) + { + } + + ByteVectorPrivate(const char *s, uint l) + : RefCounter() + , data(new DataPrivate(s, s + l)) + , offset(0) + , length(l) + { + } + + void detach() + { + if(data->count() > 1) { + data->deref(); + data = new DataPrivate(data->data, offset, length); + offset = 0; + } + } + + ~ByteVectorPrivate() + { + if(data->deref()) + delete data; + } + + ByteVectorPrivate &operator=(const ByteVectorPrivate &x) + { + if(&x != this) + { + if(data->deref()) + delete data; + + data = x.data; + data->ref(); + } + + return *this; + } + + DataPrivate *data; + uint offset; + uint length; +}; + +//////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +ByteVector ByteVector::null; + +ByteVector ByteVector::fromCString(const char *s, uint length) +{ + if(length == 0xffffffff) + return ByteVector(s, ::strlen(s)); + else + return ByteVector(s, length); +} + +ByteVector ByteVector::fromUInt(uint value, bool mostSignificantByteFirst) +{ + return fromNumber<uint>(value, mostSignificantByteFirst); +} + +ByteVector ByteVector::fromShort(short value, bool mostSignificantByteFirst) +{ + return fromNumber<ushort>(value, mostSignificantByteFirst); +} + +ByteVector ByteVector::fromLongLong(long long value, bool mostSignificantByteFirst) +{ + return fromNumber<unsigned long long>(value, mostSignificantByteFirst); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +ByteVector::ByteVector() + : d(new ByteVectorPrivate()) +{ +} + +ByteVector::ByteVector(uint size, char value) + : d(new ByteVectorPrivate(size, value)) +{ +} + +ByteVector::ByteVector(const ByteVector &v) + : d(v.d) +{ + d->ref(); +} + +ByteVector::ByteVector(const ByteVector &v, uint offset, uint length) + : d(new ByteVectorPrivate(v.d, offset, length)) +{ +} + +ByteVector::ByteVector(char c) + : d(new ByteVectorPrivate(1, c)) +{ +} + +ByteVector::ByteVector(const char *data, uint length) + : d(new ByteVectorPrivate(data, length)) +{ +} + +ByteVector::ByteVector(const char *data) + : d(new ByteVectorPrivate(data, ::strlen(data))) +{ +} + +ByteVector::~ByteVector() +{ + if(d->deref()) + delete d; +} + +ByteVector &ByteVector::setData(const char *s, uint length) +{ + *this = ByteVector(s, length); + return *this; +} + +ByteVector &ByteVector::setData(const char *data) +{ + *this = ByteVector(data); + return *this; +} + +char *ByteVector::data() +{ + detach(); + return size() > 0 ? (DATA(d) + d->offset) : 0; +} + +const char *ByteVector::data() const +{ + return size() > 0 ? (DATA(d) + d->offset) : 0; +} + +ByteVector ByteVector::mid(uint index, uint length) const +{ + index = std::min(index, size()); + length = std::min(length, size() - index); + + return ByteVector(*this, index, length); +} + +char ByteVector::at(uint index) const +{ + return index < size() ? DATA(d)[d->offset + index] : 0; +} + +int ByteVector::find(const ByteVector &pattern, uint offset, int byteAlign) const +{ + return findVector<ConstIterator>( + begin(), end(), pattern.begin(), pattern.end(), offset, byteAlign); +} + +int ByteVector::find(char c, uint offset, int byteAlign) const +{ + return findChar<ConstIterator>(begin(), end(), c, offset, byteAlign); +} + +int ByteVector::rfind(const ByteVector &pattern, uint offset, int byteAlign) const +{ + if(offset > 0) { + offset = size() - offset - pattern.size(); + if(offset >= size()) + offset = 0; + } + + const int pos = findVector<ConstReverseIterator>( + rbegin(), rend(), pattern.rbegin(), pattern.rend(), offset, byteAlign); + + if(pos == -1) + return -1; + else + return size() - pos - pattern.size(); +} + +bool ByteVector::containsAt(const ByteVector &pattern, uint offset, uint patternOffset, uint patternLength) const +{ + if(pattern.size() < patternLength) + patternLength = pattern.size(); + + // do some sanity checking -- all of these things are needed for the search to be valid + const uint compareLength = patternLength - patternOffset; + if(offset + compareLength > size() || patternOffset >= pattern.size() || patternLength == 0) + return false; + + return (::memcmp(data() + offset, pattern.data() + patternOffset, compareLength) == 0); +} + +bool ByteVector::startsWith(const ByteVector &pattern) const +{ + return containsAt(pattern, 0); +} + +bool ByteVector::endsWith(const ByteVector &pattern) const +{ + return containsAt(pattern, size() - pattern.size()); +} + +ByteVector &ByteVector::replace(const ByteVector &pattern, const ByteVector &with) +{ + if(pattern.size() == 0 || pattern.size() > size()) + return *this; + + const uint withSize = with.size(); + const uint patternSize = pattern.size(); + int offset = 0; + + if(withSize == patternSize) { + // I think this case might be common enough to optimize it + detach(); + offset = find(pattern); + while(offset >= 0) { + ::memcpy(data() + offset, with.data(), withSize); + offset = find(pattern, offset + withSize); + } + return *this; + } + + // calculate new size: + uint newSize = 0; + for(;;) { + int next = find(pattern, offset); + if(next < 0) { + if(offset == 0) + // pattern not found, do nothing: + return *this; + newSize += size() - offset; + break; + } + newSize += (next - offset) + withSize; + offset = next + patternSize; + } + + // new private data of appropriate size: + ByteVectorPrivate *newData = new ByteVectorPrivate(newSize, 0); + char *target = DATA(newData); + const char *source = data(); + + // copy modified data into new private data: + offset = 0; + for(;;) { + int next = find(pattern, offset); + if(next < 0) { + ::memcpy(target, source + offset, size() - offset); + break; + } + int chunkSize = next - offset; + ::memcpy(target, source + offset, chunkSize); + target += chunkSize; + ::memcpy(target, with.data(), withSize); + target += withSize; + offset += chunkSize + patternSize; + } + + // replace private data: + if(d->deref()) + delete d; + + d = newData; + + return *this; +} + +int ByteVector::endsWithPartialMatch(const ByteVector &pattern) const +{ + if(pattern.size() > size()) + return -1; + + const int startIndex = size() - pattern.size(); + + // try to match the last n-1 bytes from the vector (where n is the pattern + // size) -- continue trying to match n-2, n-3...1 bytes + + for(uint i = 1; i < pattern.size(); i++) { + if(containsAt(pattern, startIndex + i, 0, pattern.size() - i)) + return startIndex + i; + } + + return -1; +} + +ByteVector &ByteVector::append(const ByteVector &v) +{ + if(v.d->length != 0) + { + detach(); + + uint originalSize = size(); + resize(originalSize + v.size()); + ::memcpy(data() + originalSize, v.data(), v.size()); + } + + return *this; +} + +ByteVector &ByteVector::clear() +{ + *this = ByteVector(); + return *this; +} + +TagLib::uint ByteVector::size() const +{ + return d->length; +} + +ByteVector &ByteVector::resize(uint size, char padding) +{ + if(size != d->length) { + detach(); + d->data->data.resize(d->offset + size, padding); + d->length = size; + } + + return *this; +} + +ByteVector::Iterator ByteVector::begin() +{ + return d->data->data.begin() + d->offset; +} + +ByteVector::ConstIterator ByteVector::begin() const +{ + return d->data->data.begin() + d->offset; +} + +ByteVector::Iterator ByteVector::end() +{ + return d->data->data.begin() + d->offset + d->length; +} + +ByteVector::ConstIterator ByteVector::end() const +{ + return d->data->data.begin() + d->offset + d->length; +} + +ByteVector::ReverseIterator ByteVector::rbegin() +{ + std::vector<char> &v = d->data->data; + return v.rbegin() + (v.size() - (d->offset + d->length)); +} + +ByteVector::ConstReverseIterator ByteVector::rbegin() const +{ + std::vector<char> &v = d->data->data; + return v.rbegin() + (v.size() - (d->offset + d->length)); +} + +ByteVector::ReverseIterator ByteVector::rend() +{ + std::vector<char> &v = d->data->data; + return v.rbegin() + (v.size() - d->offset); +} + +ByteVector::ConstReverseIterator ByteVector::rend() const +{ + std::vector<char> &v = d->data->data; + return v.rbegin() + (v.size() - d->offset); +} + +bool ByteVector::isNull() const +{ + return (d == null.d); +} + +bool ByteVector::isEmpty() const +{ + return (d->length == 0); +} + +TagLib::uint ByteVector::checksum() const +{ + uint sum = 0; + for(ByteVector::ConstIterator it = begin(); it != end(); ++it) + sum = (sum << 8) ^ crcTable[((sum >> 24) & 0xff) ^ uchar(*it)]; + return sum; +} + +TagLib::uint ByteVector::toUInt(bool mostSignificantByteFirst) const +{ + return toNumber<uint>(*this, 0, mostSignificantByteFirst); +} + +TagLib::uint ByteVector::toUInt(uint offset, bool mostSignificantByteFirst) const +{ + return toNumber<uint>(*this, offset, mostSignificantByteFirst); +} + +TagLib::uint ByteVector::toUInt(uint offset, uint length, bool mostSignificantByteFirst) const +{ + return toNumber<uint>(*this, offset, length, mostSignificantByteFirst); +} + +short ByteVector::toShort(bool mostSignificantByteFirst) const +{ + return toNumber<unsigned short>(*this, 0, mostSignificantByteFirst); +} + +short ByteVector::toShort(uint offset, bool mostSignificantByteFirst) const +{ + return toNumber<unsigned short>(*this, offset, mostSignificantByteFirst); +} + +unsigned short ByteVector::toUShort(bool mostSignificantByteFirst) const +{ + return toNumber<unsigned short>(*this, 0, mostSignificantByteFirst); +} + +unsigned short ByteVector::toUShort(uint offset, bool mostSignificantByteFirst) const +{ + return toNumber<unsigned short>(*this, offset, mostSignificantByteFirst); +} + +long long ByteVector::toLongLong(bool mostSignificantByteFirst) const +{ + return toNumber<unsigned long long>(*this, 0, mostSignificantByteFirst); +} + +long long ByteVector::toLongLong(uint offset, bool mostSignificantByteFirst) const +{ + return toNumber<unsigned long long>(*this, offset, mostSignificantByteFirst); +} + +const char &ByteVector::operator[](int index) const +{ + return d->data->data[d->offset + index]; +} + +char &ByteVector::operator[](int index) +{ + detach(); + return d->data->data[d->offset + index]; +} + +bool ByteVector::operator==(const ByteVector &v) const +{ + if(size() != v.size()) + return false; + + return (::memcmp(data(), v.data(), size()) == 0); +} + +bool ByteVector::operator!=(const ByteVector &v) const +{ + return !operator==(v); +} + +bool ByteVector::operator==(const char *s) const +{ + if(size() != ::strlen(s)) + return false; + + return (::memcmp(data(), s, size()) == 0); +} + +bool ByteVector::operator!=(const char *s) const +{ + return !operator==(s); +} + +bool ByteVector::operator<(const ByteVector &v) const +{ + const int result = ::memcmp(data(), v.data(), std::min(size(), v.size())); + if(result != 0) + return result < 0; + else + return size() < v.size(); +} + +bool ByteVector::operator>(const ByteVector &v) const +{ + return v < *this; +} + +ByteVector ByteVector::operator+(const ByteVector &v) const +{ + ByteVector sum(*this); + sum.append(v); + return sum; +} + +ByteVector &ByteVector::operator=(const ByteVector &v) +{ + if(&v == this) + return *this; + + if(d->deref()) + delete d; + + d = v.d; + d->ref(); + return *this; +} + +ByteVector &ByteVector::operator=(char c) +{ + *this = ByteVector(c); + return *this; +} + +ByteVector &ByteVector::operator=(const char *data) +{ + *this = ByteVector(data); + return *this; +} + +ByteVector ByteVector::toHex() const +{ + ByteVector encoded(size() * 2); + char *p = encoded.data(); + + for(uint i = 0; i < size(); i++) { + unsigned char c = data()[i]; + *p++ = hexTable[(c >> 4) & 0x0F]; + *p++ = hexTable[(c ) & 0x0F]; + } + + return encoded; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void ByteVector::detach() +{ + if(d->data->count() > 1) { + d->data->deref(); + d->data = new DataPrivate(d->data->data, d->offset, d->length); + d->offset = 0; + } + + if(d->count() > 1) { + d->deref(); + d = new ByteVectorPrivate(d->data->data, d->offset, d->length); + } +} +} + +//////////////////////////////////////////////////////////////////////////////// +// related functions +//////////////////////////////////////////////////////////////////////////////// + +std::ostream &operator<<(std::ostream &s, const TagLib::ByteVector &v) +{ + for(TagLib::uint i = 0; i < v.size(); i++) + s << v[i]; + return s; +} diff --git a/taglib/taglib/toolkit/tbytevector.h b/taglib/taglib/toolkit/tbytevector.h new file mode 100644 index 0000000..538565b --- /dev/null +++ b/taglib/taglib/toolkit/tbytevector.h @@ -0,0 +1,518 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_BYTEVECTOR_H +#define TAGLIB_BYTEVECTOR_H + +#include "taglib.h" +#include "taglib_export.h" + +#include <vector> +#include <iostream> + +namespace TagLib { + + //! A byte vector + + /*! + * This class provides a byte vector with some methods that are useful for + * tagging purposes. Many of the search functions are tailored to what is + * useful for finding tag related paterns in a data array. + */ + + class TAGLIB_EXPORT ByteVector + { + public: +#ifndef DO_NOT_DOCUMENT + typedef std::vector<char>::iterator Iterator; + typedef std::vector<char>::const_iterator ConstIterator; + typedef std::vector<char>::reverse_iterator ReverseIterator; + typedef std::vector<char>::const_reverse_iterator ConstReverseIterator; +#endif + + /*! + * Constructs an empty byte vector. + */ + ByteVector(); + + /*! + * Construct a vector of size \a size with all values set to \a value by + * default. + */ + ByteVector(uint size, char value = 0); + + /*! + * Constructs a byte vector that is a copy of \a v. + */ + ByteVector(const ByteVector &v); + + /*! + * Constructs a byte vector that is a copy of \a v. + */ + ByteVector(const ByteVector &v, uint offset, uint length); + + /*! + * Constructs a byte vector that contains \a c. + */ + ByteVector(char c); + + /*! + * Constructs a byte vector that copies \a data for up to \a length bytes. + */ + ByteVector(const char *data, uint length); + + /*! + * Constructs a byte vector that copies \a data up to the first null + * byte. The behavior is undefined if \a data is not null terminated. + * This is particularly useful for constructing byte arrays from string + * constants. + */ + ByteVector(const char *data); + + /*! + * Destroys this ByteVector instance. + */ + virtual ~ByteVector(); + + /*! + * Sets the data for the byte array using the first \a length bytes of \a data + */ + ByteVector &setData(const char *data, uint length); + + /*! + * Sets the data for the byte array copies \a data up to the first null + * byte. The behavior is undefined if \a data is not null terminated. + */ + ByteVector &setData(const char *data); + + /*! + * Returns a pointer to the internal data structure. + * + * \warning Care should be taken when modifying this data structure as it is + * easy to corrupt the ByteVector when doing so. Specifically, while the + * data may be changed, its length may not be. + */ + char *data(); + + /*! + * Returns a pointer to the internal data structure which may not be modified. + */ + const char *data() const; + + /*! + * Returns a byte vector made up of the bytes starting at \a index and + * for \a length bytes. If \a length is not specified it will return the bytes + * from \a index to the end of the vector. + */ + ByteVector mid(uint index, uint length = 0xffffffff) const; + + /*! + * This essentially performs the same as operator[](), but instead of causing + * a runtime error if the index is out of bounds, it will return a null byte. + */ + char at(uint index) const; + + /*! + * Searches the ByteVector for \a pattern starting at \a offset and returns + * the offset. Returns -1 if the pattern was not found. If \a byteAlign is + * specified the pattern will only be matched if it starts on a byte divisible + * by \a byteAlign (starting from \a offset). + */ + int find(const ByteVector &pattern, uint offset = 0, int byteAlign = 1) const; + + /*! + * Searches the char for \a c starting at \a offset and returns + * the offset. Returns \a npos if the pattern was not found. If \a byteAlign is + * specified the pattern will only be matched if it starts on a byte divisible + * by \a byteAlign (starting from \a offset). + */ + int find(char c, uint offset = 0, int byteAlign = 1) const; + + /*! + * Searches the ByteVector for \a pattern starting from either the end of the + * vector or \a offset and returns the offset. Returns -1 if the pattern was + * not found. If \a byteAlign is specified the pattern will only be matched + * if it starts on a byte divisible by \a byteAlign (starting from \a offset). + */ + int rfind(const ByteVector &pattern, uint offset = 0, int byteAlign = 1) const; + + /*! + * Checks to see if the vector contains the \a pattern starting at position + * \a offset. Optionally, if you only want to search for part of the pattern + * you can specify an offset within the pattern to start from. Also, you can + * specify to only check for the first \a patternLength bytes of \a pattern with + * the \a patternLength argument. + */ + bool containsAt(const ByteVector &pattern, uint offset, uint patternOffset = 0, uint patternLength = 0xffffffff) const; + + /*! + * Returns true if the vector starts with \a pattern. + */ + bool startsWith(const ByteVector &pattern) const; + + /*! + * Returns true if the vector ends with \a pattern. + */ + bool endsWith(const ByteVector &pattern) const; + + /*! + * Replaces \a pattern with \a with and returns a reference to the ByteVector + * after the operation. This \e does modify the vector. + */ + ByteVector &replace(const ByteVector &pattern, const ByteVector &with); + + /*! + * Checks for a partial match of \a pattern at the end of the vector. It + * returns the offset of the partial match within the vector, or -1 if the + * pattern is not found. This method is particularly useful when searching for + * patterns that start in one vector and end in another. When combined with + * startsWith() it can be used to find a pattern that overlaps two buffers. + * + * \note This will not match the complete pattern at the end of the string; use + * endsWith() for that. + */ + int endsWithPartialMatch(const ByteVector &pattern) const; + + /*! + * Appends \a v to the end of the ByteVector. + */ + ByteVector &append(const ByteVector &v); + + /*! + * Clears the data. + */ + ByteVector &clear(); + + /*! + * Returns the size of the array. + */ + uint size() const; + + /*! + * Resize the vector to \a size. If the vector is currently less than + * \a size, pad the remaining spaces with \a padding. Returns a reference + * to the resized vector. + */ + ByteVector &resize(uint size, char padding = 0); + + /*! + * Returns an Iterator that points to the front of the vector. + */ + Iterator begin(); + + /*! + * Returns a ConstIterator that points to the front of the vector. + */ + ConstIterator begin() const; + + /*! + * Returns an Iterator that points to the back of the vector. + */ + Iterator end(); + + /*! + * Returns a ConstIterator that points to the back of the vector. + */ + ConstIterator end() const; + + /*! + * Returns a ReverseIterator that points to the front of the vector. + */ + ReverseIterator rbegin(); + + /*! + * Returns a ConstReverseIterator that points to the front of the vector. + */ + ConstReverseIterator rbegin() const; + + /*! + * Returns a ReverseIterator that points to the back of the vector. + */ + ReverseIterator rend(); + + /*! + * Returns a ConstReverseIterator that points to the back of the vector. + */ + ConstReverseIterator rend() const; + + /*! + * Returns true if the vector is null. + * + * \note A vector may be empty without being null. + * \see isEmpty() + */ + bool isNull() const; + + /*! + * Returns true if the ByteVector is empty. + * + * \see size() + * \see isNull() + */ + bool isEmpty() const; + + /*! + * Returns a CRC checksum of the byte vector's data. + */ + uint checksum() const; + + /*! + * Converts the first 4 bytes of the vector to an unsigned integer. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 $00 $00 $01 == 0x00000001 == 1, if false, $01 00 00 00 == + * 0x01000000 == 1. + * + * \see fromUInt() + */ + uint toUInt(bool mostSignificantByteFirst = true) const; + + /*! + * Converts the 4 bytes at \a offset of the vector to an unsigned integer. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 $00 $00 $01 == 0x00000001 == 1, if false, $01 00 00 00 == + * 0x01000000 == 1. + * + * \see fromUInt() + */ + uint toUInt(uint offset, bool mostSignificantByteFirst = true) const; + + /*! + * Converts the \a length bytes at \a offset of the vector to an unsigned + * integer. If \a length is larger than 4, the excess is ignored. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 $00 $00 $01 == 0x00000001 == 1, if false, $01 00 00 00 == + * 0x01000000 == 1. + * + * \see fromUInt() + */ + uint toUInt(uint offset, uint length, bool mostSignificantByteFirst = true) const; + + /*! + * Converts the first 2 bytes of the vector to a (signed) short. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 $01 == 0x0001 == 1, if false, $01 00 == 0x01000000 == 1. + * + * \see fromShort() + */ + short toShort(bool mostSignificantByteFirst = true) const; + + /*! + * Converts the 2 bytes at \a offset of the vector to a (signed) short. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 $01 == 0x0001 == 1, if false, $01 00 == 0x01000000 == 1. + * + * \see fromShort() + */ + short toShort(uint offset, bool mostSignificantByteFirst = true) const; + + /*! + * Converts the first 2 bytes of the vector to a unsigned short. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 $01 == 0x0001 == 1, if false, $01 00 == 0x01000000 == 1. + * + * \see fromShort() + */ + unsigned short toUShort(bool mostSignificantByteFirst = true) const; + + /*! + * Converts the 2 bytes at \a offset of the vector to a unsigned short. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 $01 == 0x0001 == 1, if false, $01 00 == 0x01000000 == 1. + * + * \see fromShort() + */ + unsigned short toUShort(uint offset, bool mostSignificantByteFirst = true) const; + + /*! + * Converts the first 8 bytes of the vector to a (signed) long long. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 00 00 00 00 00 00 01 == 0x0000000000000001 == 1, + * if false, $01 00 00 00 00 00 00 00 == 0x0100000000000000 == 1. + * + * \see fromUInt() + */ + long long toLongLong(bool mostSignificantByteFirst = true) const; + + /*! + * Converts the 8 bytes at \a offset of the vector to a (signed) long long. + * + * If \a mostSignificantByteFirst is true this will operate left to right + * evaluating the integer. For example if \a mostSignificantByteFirst is + * true then $00 00 00 00 00 00 00 01 == 0x0000000000000001 == 1, + * if false, $01 00 00 00 00 00 00 00 == 0x0100000000000000 == 1. + * + * \see fromUInt() + */ + long long toLongLong(uint offset, bool mostSignificantByteFirst = true) const; + + /*! + * Creates a 4 byte ByteVector based on \a value. If + * \a mostSignificantByteFirst is true, then this will operate left to right + * in building the ByteVector. For example if \a mostSignificantByteFirst is + * true then $00 00 00 01 == 0x00000001 == 1, if false, $01 00 00 00 == + * 0x01000000 == 1. + * + * \see toUInt() + */ + static ByteVector fromUInt(uint value, bool mostSignificantByteFirst = true); + + /*! + * Creates a 2 byte ByteVector based on \a value. If + * \a mostSignificantByteFirst is true, then this will operate left to right + * in building the ByteVector. For example if \a mostSignificantByteFirst is + * true then $00 01 == 0x0001 == 1, if false, $01 00 == 0x0100 == 1. + * + * \see toShort() + */ + static ByteVector fromShort(short value, bool mostSignificantByteFirst = true); + + /*! + * Creates a 8 byte ByteVector based on \a value. If + * \a mostSignificantByteFirst is true, then this will operate left to right + * in building the ByteVector. For example if \a mostSignificantByteFirst is + * true then $00 00 00 01 == 0x0000000000000001 == 1, if false, + * $01 00 00 00 00 00 00 00 == 0x0100000000000000 == 1. + * + * \see toLongLong() + */ + static ByteVector fromLongLong(long long value, bool mostSignificantByteFirst = true); + + /*! + * Returns a ByteVector based on the CString \a s. + */ + static ByteVector fromCString(const char *s, uint length = 0xffffffff); + + /*! + * Returns a const refernence to the byte at \a index. + */ + const char &operator[](int index) const; + + /*! + * Returns a reference to the byte at \a index. + */ + char &operator[](int index); + + /*! + * Returns true if this ByteVector and \a v are equal. + */ + bool operator==(const ByteVector &v) const; + + /*! + * Returns true if this ByteVector and \a v are not equal. + */ + bool operator!=(const ByteVector &v) const; + + /*! + * Returns true if this ByteVector and the null terminated C string \a s + * contain the same data. + */ + bool operator==(const char *s) const; + + /*! + * Returns true if this ByteVector and the null terminated C string \a s + * do not contain the same data. + */ + bool operator!=(const char *s) const; + + /*! + * Returns true if this ByteVector is less than \a v. The value of the + * vectors is determined by evaluating the character from left to right, and + * in the event one vector is a superset of the other, the size is used. + */ + bool operator<(const ByteVector &v) const; + + /*! + * Returns true if this ByteVector is greater than \a v. + */ + bool operator>(const ByteVector &v) const; + + /*! + * Returns a vector that is \a v appended to this vector. + */ + ByteVector operator+(const ByteVector &v) const; + + /*! + * Copies ByteVector \a v. + */ + ByteVector &operator=(const ByteVector &v); + + /*! + * Copies ByteVector \a v. + */ + ByteVector &operator=(char c); + + /*! + * Copies ByteVector \a v. + */ + ByteVector &operator=(const char *data); + + /*! + * A static, empty ByteVector which is convenient and fast (since returning + * an empty or "null" value does not require instantiating a new ByteVector). + */ + static ByteVector null; + + /*! + * Returns a hex-encoded copy of the byte vector. + */ + ByteVector toHex() const; + + protected: + /* + * If this ByteVector is being shared via implicit sharing, do a deep copy + * of the data and separate from the shared members. This should be called + * by all non-const subclass members. + */ + void detach(); + + private: + class ByteVectorPrivate; + ByteVectorPrivate *d; + }; +} + +/*! + * \relates TagLib::ByteVector + * Streams the ByteVector \a v to the output stream \a s. + */ +TAGLIB_EXPORT std::ostream &operator<<(std::ostream &s, const TagLib::ByteVector &v); + +#endif diff --git a/taglib/taglib/toolkit/tbytevectorlist.cpp b/taglib/taglib/toolkit/tbytevectorlist.cpp new file mode 100644 index 0000000..7ea893f --- /dev/null +++ b/taglib/taglib/toolkit/tbytevectorlist.cpp @@ -0,0 +1,102 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tbytevectorlist.h" + +using namespace TagLib; + +class ByteVectorListPrivate +{ + +}; + +//////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +ByteVectorList ByteVectorList::split(const ByteVector &v, const ByteVector &pattern, + int byteAlign) +{ + return split(v, pattern, byteAlign, 0); +} + +ByteVectorList ByteVectorList::split(const ByteVector &v, const ByteVector &pattern, + int byteAlign, int max) +{ + ByteVectorList l; + + uint previousOffset = 0; + for(int offset = v.find(pattern, 0, byteAlign); + offset != -1 && (max == 0 || max > int(l.size()) + 1); + offset = v.find(pattern, offset + pattern.size(), byteAlign)) + { + if(offset - previousOffset >= 1) + l.append(v.mid(previousOffset, offset - previousOffset)); + else + l.append(ByteVector::null); + + previousOffset = offset + pattern.size(); + } + + if(previousOffset < v.size()) + l.append(v.mid(previousOffset, v.size() - previousOffset)); + + return l; +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +ByteVectorList::ByteVectorList() : List<ByteVector>() +{ + +} + +ByteVectorList::ByteVectorList(const ByteVectorList &l) : List<ByteVector>(l) +{ + +} + +ByteVectorList::~ByteVectorList() +{ + +} + +ByteVector ByteVectorList::toByteVector(const ByteVector &separator) const +{ + ByteVector v; + + ConstIterator it = begin(); + + while(it != end()) { + v.append(*it); + it++; + if(it != end()) + v.append(separator); + } + + return v; +} diff --git a/taglib/taglib/toolkit/tbytevectorlist.h b/taglib/taglib/toolkit/tbytevectorlist.h new file mode 100644 index 0000000..5852583 --- /dev/null +++ b/taglib/taglib/toolkit/tbytevectorlist.h @@ -0,0 +1,91 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_BYTEVECTORLIST_H +#define TAGLIB_BYTEVECTORLIST_H + +#include "taglib_export.h" +#include "tbytevector.h" +#include "tlist.h" + +namespace TagLib { + + //! A list of ByteVectors + + /*! + * A List specialization with some handy features useful for ByteVectors. + */ + + class TAGLIB_EXPORT ByteVectorList : public List<ByteVector> + { + public: + + /*! + * Construct an empty ByteVectorList. + */ + ByteVectorList(); + + /*! + * Destroys this ByteVectorList instance. + */ + virtual ~ByteVectorList(); + + /*! + * Make a shallow, implicitly shared, copy of \a l. Because this is + * implicitly shared, this method is lightweight and suitable for + * pass-by-value usage. + */ + ByteVectorList(const ByteVectorList &l); + + /*! + * Convert the ByteVectorList to a ByteVector separated by \a separator. By + * default a space is used. + */ + ByteVector toByteVector(const ByteVector &separator = " ") const; + + /*! + * Splits the ByteVector \a v into several strings at \a pattern. This will + * not include the pattern in the returned ByteVectors. + */ + static ByteVectorList split(const ByteVector &v, const ByteVector &pattern, + int byteAlign = 1); + /*! + * Splits the ByteVector \a v into several strings at \a pattern. This will + * not include the pattern in the returned ByteVectors. \a max is the + * maximum number of entries that will be separated. If \a max for instance + * is 2 then a maximum of 1 match will be found and the vector will be split + * on that match. + */ + // BIC: merge with the function above + static ByteVectorList split(const ByteVector &v, const ByteVector &pattern, + int byteAlign, int max); + private: + class ByteVectorListPrivate; + ByteVectorListPrivate *d; + }; + +} + +#endif diff --git a/taglib/taglib/toolkit/tbytevectorstream.cpp b/taglib/taglib/toolkit/tbytevectorstream.cpp new file mode 100644 index 0000000..dc480a2 --- /dev/null +++ b/taglib/taglib/toolkit/tbytevectorstream.cpp @@ -0,0 +1,167 @@ +/*************************************************************************** + copyright : (C) 2011 by Lukas Lalinsky + email : lalinsky@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tbytevectorstream.h" +#include "tstring.h" +#include "tdebug.h" + +#include <stdio.h> +#include <string.h> + +#include <stdlib.h> + +using namespace TagLib; + +class ByteVectorStream::ByteVectorStreamPrivate +{ +public: + ByteVectorStreamPrivate(const ByteVector &data); + + ByteVector data; + long position; +}; + +ByteVectorStream::ByteVectorStreamPrivate::ByteVectorStreamPrivate(const ByteVector &data) : + data(data), + position(0) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +ByteVectorStream::ByteVectorStream(const ByteVector &data) +{ + d = new ByteVectorStreamPrivate(data); +} + +ByteVectorStream::~ByteVectorStream() +{ + delete d; +} + +FileName ByteVectorStream::name() const +{ + return FileName(""); // XXX do we need a name? +} + +ByteVector ByteVectorStream::readBlock(ulong length) +{ + if(length == 0) + return ByteVector::null; + + ByteVector v = d->data.mid(d->position, length); + d->position += v.size(); + return v; +} + +void ByteVectorStream::writeBlock(const ByteVector &data) +{ + uint size = data.size(); + if(long(d->position + size) > length()) { + truncate(d->position + size); + } + memcpy(d->data.data() + d->position, data.data(), size); + d->position += size; +} + +void ByteVectorStream::insert(const ByteVector &data, ulong start, ulong replace) +{ + long sizeDiff = data.size() - replace; + if(sizeDiff < 0) { + removeBlock(start + data.size(), -sizeDiff); + } + else if(sizeDiff > 0) { + truncate(length() + sizeDiff); + ulong readPosition = start + replace; + ulong writePosition = start + data.size(); + memmove(d->data.data() + writePosition, d->data.data() + readPosition, length() - sizeDiff - readPosition); + } + seek(start); + writeBlock(data); +} + +void ByteVectorStream::removeBlock(ulong start, ulong length) +{ + ulong readPosition = start + length; + ulong writePosition = start; + if(readPosition < ulong(ByteVectorStream::length())) { + ulong bytesToMove = ByteVectorStream::length() - readPosition; + memmove(d->data.data() + writePosition, d->data.data() + readPosition, bytesToMove); + writePosition += bytesToMove; + } + d->position = writePosition; + truncate(writePosition); +} + +bool ByteVectorStream::readOnly() const +{ + return false; +} + +bool ByteVectorStream::isOpen() const +{ + return true; +} + +void ByteVectorStream::seek(long offset, Position p) +{ + switch(p) { + case Beginning: + d->position = offset; + break; + case Current: + d->position += offset; + break; + case End: + d->position = length() - offset; + break; + } +} + +void ByteVectorStream::clear() +{ +} + +long ByteVectorStream::tell() const +{ + return d->position; +} + +long ByteVectorStream::length() +{ + return d->data.size(); +} + +void ByteVectorStream::truncate(long length) +{ + d->data.resize(length); +} + +ByteVector *ByteVectorStream::data() +{ + return &d->data; +} diff --git a/taglib/taglib/toolkit/tbytevectorstream.h b/taglib/taglib/toolkit/tbytevectorstream.h new file mode 100644 index 0000000..456b854 --- /dev/null +++ b/taglib/taglib/toolkit/tbytevectorstream.h @@ -0,0 +1,145 @@ +/*************************************************************************** + copyright : (C) 2011 by Lukas Lalinsky + email : lalinsky@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_BYTEVECTORSTREAM_H +#define TAGLIB_BYTEVECTORSTREAM_H + +#include "taglib_export.h" +#include "taglib.h" +#include "tbytevector.h" +#include "tiostream.h" + +namespace TagLib { + + class String; + class Tag; + class AudioProperties; + + //! In-memory Stream class using ByteVector for its storage. + + class TAGLIB_EXPORT ByteVectorStream : public IOStream + { + public: + /*! + * Construct a File object and opens the \a file. \a file should be a + * be a C-string in the local file system encoding. + */ + ByteVectorStream(const ByteVector &data); + + /*! + * Destroys this ByteVectorStream instance. + */ + virtual ~ByteVectorStream(); + + /*! + * Returns the file name in the local file system encoding. + */ + FileName name() const; + + /*! + * Reads a block of size \a length at the current get pointer. + */ + ByteVector readBlock(ulong length); + + /*! + * Attempts to write the block \a data at the current get pointer. If the + * file is currently only opened read only -- i.e. readOnly() returns true -- + * this attempts to reopen the file in read/write mode. + * + * \note This should be used instead of using the streaming output operator + * for a ByteVector. And even this function is significantly slower than + * doing output with a char[]. + */ + void writeBlock(const ByteVector &data); + + /*! + * Insert \a data at position \a start in the file overwriting \a replace + * bytes of the original content. + * + * \note This method is slow since it requires rewriting all of the file + * after the insertion point. + */ + void insert(const ByteVector &data, ulong start = 0, ulong replace = 0); + + /*! + * Removes a block of the file starting a \a start and continuing for + * \a length bytes. + * + * \note This method is slow since it involves rewriting all of the file + * after the removed portion. + */ + void removeBlock(ulong start = 0, ulong length = 0); + + /*! + * Returns true if the file is read only (or if the file can not be opened). + */ + bool readOnly() const; + + /*! + * Since the file can currently only be opened as an argument to the + * constructor (sort-of by design), this returns if that open succeeded. + */ + bool isOpen() const; + + /*! + * Move the I/O pointer to \a offset in the file from position \a p. This + * defaults to seeking from the beginning of the file. + * + * \see Position + */ + void seek(long offset, Position p = Beginning); + + /*! + * Reset the end-of-file and error flags on the file. + */ + void clear(); + + /*! + * Returns the current offset within the file. + */ + long tell() const; + + /*! + * Returns the length of the file. + */ + long length(); + + /*! + * Truncates the file to a \a length. + */ + void truncate(long length); + + ByteVector *data(); + + protected: + + private: + class ByteVectorStreamPrivate; + ByteVectorStreamPrivate *d; + }; + +} + +#endif diff --git a/taglib/taglib/toolkit/tdebug.cpp b/taglib/taglib/toolkit/tdebug.cpp new file mode 100644 index 0000000..71350af --- /dev/null +++ b/taglib/taglib/toolkit/tdebug.cpp @@ -0,0 +1,99 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "tdebug.h" +#include "tstring.h" +#include "tdebuglistener.h" + +#include <bitset> +#include <cstdio> +#include <cstdarg> + +using namespace TagLib; + +namespace +{ + String format(const char *fmt, ...) + { + va_list args; + va_start(args, fmt); + + char buf[256]; + +#if defined(HAVE_SNPRINTF) + + vsnprintf(buf, sizeof(buf), fmt, args); + +#elif defined(HAVE_SPRINTF_S) + + vsprintf_s(buf, fmt, args); + +#else + + // Be careful. May cause a buffer overflow. + vsprintf(buf, fmt, args); + +#endif + + va_end(args); + + return String(buf); + } +} + +namespace TagLib +{ + // The instance is defined in tdebuglistener.cpp. + extern DebugListener *debugListener; + + void debug(const String &s) + { +#if !defined(NDEBUG) || defined(TRACE_IN_RELEASE) + + debugListener->printMessage("TagLib: " + s + "\n"); + +#endif + } + + void debugData(const ByteVector &v) + { +#if !defined(NDEBUG) || defined(TRACE_IN_RELEASE) + + for(size_t i = 0; i < v.size(); ++i) + { + std::string bits = std::bitset<8>(v[i]).to_string(); + String msg = format("*** [%d] - char '%c' - int %d, 0x%02x, 0b%s\n", + i, v[i], v[i], v[i], bits.c_str()); + + debugListener->printMessage(msg); + } + +#endif + } +} diff --git a/taglib/taglib/toolkit/tdebug.h b/taglib/taglib/toolkit/tdebug.h new file mode 100644 index 0000000..bd94d15 --- /dev/null +++ b/taglib/taglib/toolkit/tdebug.h @@ -0,0 +1,62 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_DEBUG_H +#define TAGLIB_DEBUG_H + +namespace TagLib { + + class String; + class ByteVector; + +#ifndef DO_NOT_DOCUMENT + + /*! + * A simple function that outputs the debug messages to the listener. + * The default listener redirects the messages to \a stderr when NDEBUG is + * not defined. + * + * \warning Do not use this outside of TagLib, it could lead to undefined + * symbols in your build if TagLib is built with NDEBUG defined and your + * application is not. + * + * \internal + */ + void debug(const String &s); + + /*! + * For debugging binary data. + * + * \warning Do not use this outside of TagLib, it could lead to undefined + * symbols in your build if TagLib is built with NDEBUG defined and your + * application is not. + * + * \internal + */ + void debugData(const ByteVector &v); +} + +#endif +#endif diff --git a/taglib/taglib/toolkit/tdebuglistener.cpp b/taglib/taglib/toolkit/tdebuglistener.cpp new file mode 100644 index 0000000..4891222 --- /dev/null +++ b/taglib/taglib/toolkit/tdebuglistener.cpp @@ -0,0 +1,85 @@ +/*************************************************************************** + copyright : (C) 2013 by Tsuda Kageyu + email : tsuda.kageyu@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tdebuglistener.h" + +#include <iostream> +#include <bitset> + +#ifdef _WIN32 +# include <windows.h> +#endif + +using namespace TagLib; + +namespace +{ + class DefaultListener : public DebugListener + { + public: + virtual void printMessage(const String &msg) + { +#ifdef _WIN32 + + const wstring wstr = msg.toWString(); + const int len = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL); + if(len != 0) { + std::vector<char> buf(len); + WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &buf[0], len, NULL, NULL); + + std::cerr << std::string(&buf[0]); + } + +#else + + std::cerr << msg; + +#endif + } + }; + + DefaultListener defaultListener; +} + +namespace TagLib +{ + DebugListener *debugListener = &defaultListener; + + DebugListener::DebugListener() + { + } + + DebugListener::~DebugListener() + { + } + + void setDebugListener(DebugListener *listener) + { + if(listener) + debugListener = listener; + else + debugListener = &defaultListener; + } +} diff --git a/taglib/taglib/toolkit/tdebuglistener.h b/taglib/taglib/toolkit/tdebuglistener.h new file mode 100644 index 0000000..a32f285 --- /dev/null +++ b/taglib/taglib/toolkit/tdebuglistener.h @@ -0,0 +1,74 @@ +/*************************************************************************** + copyright : (C) 2013 by Tsuda Kageyu + email : tsuda.kageyu@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_DEBUGLISTENER_H +#define TAGLIB_DEBUGLISTENER_H + +#include "taglib_export.h" +#include "tstring.h" + +namespace TagLib +{ + //! An abstraction for the listener to the debug messages. + + /*! + * This class enables you to handle the debug messages in your preferred + * way by subclassing this class, reimplementing printMessage() and setting + * your reimplementation as the default with setDebugListener(). + * + * \see setDebugListener() + */ + class TAGLIB_EXPORT DebugListener + { + public: + DebugListener(); + virtual ~DebugListener(); + + /*! + * When overridden in a derived class, redirects \a msg to your preferred + * channel such as stderr, Windows debugger or so forth. + */ + virtual void printMessage(const String &msg) = 0; + + private: + // Noncopyable + DebugListener(const DebugListener &); + DebugListener &operator=(const DebugListener &); + }; + + /*! + * Sets the listener that decides how the debug messages are redirected. + * If the parameter \a listener is null, the previous listener is released + * and default stderr listener is restored. + * + * \note The caller is responsible for deleting the previous listener + * as needed after it is released. + * + * \see DebugListener + */ + TAGLIB_EXPORT void setDebugListener(DebugListener *listener); +} + +#endif diff --git a/taglib/taglib/toolkit/tfile.cpp b/taglib/taglib/toolkit/tfile.cpp new file mode 100644 index 0000000..4a05b05 --- /dev/null +++ b/taglib/taglib/toolkit/tfile.cpp @@ -0,0 +1,503 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tfile.h" +#include "tfilestream.h" +#include "tstring.h" +#include "tdebug.h" +#include "tpropertymap.h" + +#ifdef _WIN32 +# include <windows.h> +# include <io.h> +#else +# include <stdio.h> +# include <unistd.h> +#endif + +#ifndef R_OK +# define R_OK 4 +#endif +#ifndef W_OK +# define W_OK 2 +#endif + +#include "asffile.h" +#include "mpegfile.h" +#include "vorbisfile.h" +#include "flacfile.h" +#include "oggflacfile.h" +#include "mpcfile.h" +#include "mp4file.h" +#include "wavpackfile.h" +#include "speexfile.h" +#include "opusfile.h" +#include "trueaudiofile.h" +#include "aifffile.h" +#include "wavfile.h" +#include "apefile.h" +#include "modfile.h" +#include "s3mfile.h" +#include "itfile.h" +#include "xmfile.h" +#include "mp4file.h" + +using namespace TagLib; + +namespace +{ +#ifdef _WIN32 + const TagLib::uint BufferSize = 8192; +#else + const TagLib::uint BufferSize = 1024; +#endif +} + +class File::FilePrivate +{ +public: + FilePrivate(IOStream *stream, bool owner); + + IOStream *stream; + bool streamOwner; + bool valid; +}; + +File::FilePrivate::FilePrivate(IOStream *stream, bool owner) : + stream(stream), + streamOwner(owner), + valid(true) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +File::File(FileName fileName) +{ + IOStream *stream = new FileStream(fileName); + d = new FilePrivate(stream, true); +} + +File::File(IOStream *stream) +{ + d = new FilePrivate(stream, false); +} + +File::~File() +{ + if(d->stream && d->streamOwner) + delete d->stream; + delete d; +} + +FileName File::name() const +{ + return d->stream->name(); +} + +PropertyMap File::properties() const +{ + // ugly workaround until this method is virtual + if(dynamic_cast<const APE::File* >(this)) + return dynamic_cast<const APE::File* >(this)->properties(); + if(dynamic_cast<const FLAC::File* >(this)) + return dynamic_cast<const FLAC::File* >(this)->properties(); + if(dynamic_cast<const IT::File* >(this)) + return dynamic_cast<const IT::File* >(this)->properties(); + if(dynamic_cast<const Mod::File* >(this)) + return dynamic_cast<const Mod::File* >(this)->properties(); + if(dynamic_cast<const MPC::File* >(this)) + return dynamic_cast<const MPC::File* >(this)->properties(); + if(dynamic_cast<const MPEG::File* >(this)) + return dynamic_cast<const MPEG::File* >(this)->properties(); + if(dynamic_cast<const Ogg::FLAC::File* >(this)) + return dynamic_cast<const Ogg::FLAC::File* >(this)->properties(); + if(dynamic_cast<const Ogg::Speex::File* >(this)) + return dynamic_cast<const Ogg::Speex::File* >(this)->properties(); + if(dynamic_cast<const Ogg::Opus::File* >(this)) + return dynamic_cast<const Ogg::Opus::File* >(this)->properties(); + if(dynamic_cast<const Ogg::Vorbis::File* >(this)) + return dynamic_cast<const Ogg::Vorbis::File* >(this)->properties(); + if(dynamic_cast<const RIFF::AIFF::File* >(this)) + return dynamic_cast<const RIFF::AIFF::File* >(this)->properties(); + if(dynamic_cast<const RIFF::WAV::File* >(this)) + return dynamic_cast<const RIFF::WAV::File* >(this)->properties(); + if(dynamic_cast<const S3M::File* >(this)) + return dynamic_cast<const S3M::File* >(this)->properties(); + if(dynamic_cast<const TrueAudio::File* >(this)) + return dynamic_cast<const TrueAudio::File* >(this)->properties(); + if(dynamic_cast<const WavPack::File* >(this)) + return dynamic_cast<const WavPack::File* >(this)->properties(); + if(dynamic_cast<const XM::File* >(this)) + return dynamic_cast<const XM::File* >(this)->properties(); + if(dynamic_cast<const MP4::File* >(this)) + return dynamic_cast<const MP4::File* >(this)->properties(); + if(dynamic_cast<const ASF::File* >(this)) + return dynamic_cast<const ASF::File* >(this)->properties(); + return tag()->properties(); +} + +void File::removeUnsupportedProperties(const StringList &properties) +{ + // here we only consider those formats that could possibly contain + // unsupported properties + if(dynamic_cast<APE::File* >(this)) + dynamic_cast<APE::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<FLAC::File* >(this)) + dynamic_cast<FLAC::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<MPC::File* >(this)) + dynamic_cast<MPC::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<MPEG::File* >(this)) + dynamic_cast<MPEG::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<Ogg::Vorbis::File* >(this)) + dynamic_cast<Ogg::Vorbis::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<RIFF::AIFF::File* >(this)) + dynamic_cast<RIFF::AIFF::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<RIFF::WAV::File* >(this)) + dynamic_cast<RIFF::WAV::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<TrueAudio::File* >(this)) + dynamic_cast<TrueAudio::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<WavPack::File* >(this)) + dynamic_cast<WavPack::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<MP4::File* >(this)) + dynamic_cast<MP4::File* >(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast<ASF::File* >(this)) + dynamic_cast<ASF::File* >(this)->removeUnsupportedProperties(properties); + else + tag()->removeUnsupportedProperties(properties); +} + +PropertyMap File::setProperties(const PropertyMap &properties) +{ + if(dynamic_cast<APE::File* >(this)) + return dynamic_cast<APE::File* >(this)->setProperties(properties); + else if(dynamic_cast<FLAC::File* >(this)) + return dynamic_cast<FLAC::File* >(this)->setProperties(properties); + else if(dynamic_cast<IT::File* >(this)) + return dynamic_cast<IT::File* >(this)->setProperties(properties); + else if(dynamic_cast<Mod::File* >(this)) + return dynamic_cast<Mod::File* >(this)->setProperties(properties); + else if(dynamic_cast<MPC::File* >(this)) + return dynamic_cast<MPC::File* >(this)->setProperties(properties); + else if(dynamic_cast<MPEG::File* >(this)) + return dynamic_cast<MPEG::File* >(this)->setProperties(properties); + else if(dynamic_cast<Ogg::FLAC::File* >(this)) + return dynamic_cast<Ogg::FLAC::File* >(this)->setProperties(properties); + else if(dynamic_cast<Ogg::Speex::File* >(this)) + return dynamic_cast<Ogg::Speex::File* >(this)->setProperties(properties); + else if(dynamic_cast<Ogg::Opus::File* >(this)) + return dynamic_cast<Ogg::Opus::File* >(this)->setProperties(properties); + else if(dynamic_cast<Ogg::Vorbis::File* >(this)) + return dynamic_cast<Ogg::Vorbis::File* >(this)->setProperties(properties); + else if(dynamic_cast<RIFF::AIFF::File* >(this)) + return dynamic_cast<RIFF::AIFF::File* >(this)->setProperties(properties); + else if(dynamic_cast<RIFF::WAV::File* >(this)) + return dynamic_cast<RIFF::WAV::File* >(this)->setProperties(properties); + else if(dynamic_cast<S3M::File* >(this)) + return dynamic_cast<S3M::File* >(this)->setProperties(properties); + else if(dynamic_cast<TrueAudio::File* >(this)) + return dynamic_cast<TrueAudio::File* >(this)->setProperties(properties); + else if(dynamic_cast<WavPack::File* >(this)) + return dynamic_cast<WavPack::File* >(this)->setProperties(properties); + else if(dynamic_cast<XM::File* >(this)) + return dynamic_cast<XM::File* >(this)->setProperties(properties); + else if(dynamic_cast<MP4::File* >(this)) + return dynamic_cast<MP4::File* >(this)->setProperties(properties); + else if(dynamic_cast<ASF::File* >(this)) + return dynamic_cast<ASF::File* >(this)->setProperties(properties); + else + return tag()->setProperties(properties); +} + +ByteVector File::readBlock(ulong length) +{ + return d->stream->readBlock(length); +} + +void File::writeBlock(const ByteVector &data) +{ + d->stream->writeBlock(data); +} + +long File::find(const ByteVector &pattern, long fromOffset, const ByteVector &before) +{ + if(!d->stream || pattern.size() > bufferSize()) + return -1; + + // The position in the file that the current buffer starts at. + + long bufferOffset = fromOffset; + ByteVector buffer; + + // These variables are used to keep track of a partial match that happens at + // the end of a buffer. + + int previousPartialMatch = -1; + int beforePreviousPartialMatch = -1; + + // Save the location of the current read pointer. We will restore the + // position using seek() before all returns. + + long originalPosition = tell(); + + // Start the search at the offset. + + seek(fromOffset); + + // This loop is the crux of the find method. There are three cases that we + // want to account for: + // + // (1) The previously searched buffer contained a partial match of the search + // pattern and we want to see if the next one starts with the remainder of + // that pattern. + // + // (2) The search pattern is wholly contained within the current buffer. + // + // (3) The current buffer ends with a partial match of the pattern. We will + // note this for use in the next itteration, where we will check for the rest + // of the pattern. + // + // All three of these are done in two steps. First we check for the pattern + // and do things appropriately if a match (or partial match) is found. We + // then check for "before". The order is important because it gives priority + // to "real" matches. + + for(buffer = readBlock(bufferSize()); buffer.size() > 0; buffer = readBlock(bufferSize())) { + + // (1) previous partial match + + if(previousPartialMatch >= 0 && int(bufferSize()) > previousPartialMatch) { + const int patternOffset = (bufferSize() - previousPartialMatch); + if(buffer.containsAt(pattern, 0, patternOffset)) { + seek(originalPosition); + return bufferOffset - bufferSize() + previousPartialMatch; + } + } + + if(!before.isNull() && beforePreviousPartialMatch >= 0 && int(bufferSize()) > beforePreviousPartialMatch) { + const int beforeOffset = (bufferSize() - beforePreviousPartialMatch); + if(buffer.containsAt(before, 0, beforeOffset)) { + seek(originalPosition); + return -1; + } + } + + // (2) pattern contained in current buffer + + long location = buffer.find(pattern); + if(location >= 0) { + seek(originalPosition); + return bufferOffset + location; + } + + if(!before.isNull() && buffer.find(before) >= 0) { + seek(originalPosition); + return -1; + } + + // (3) partial match + + previousPartialMatch = buffer.endsWithPartialMatch(pattern); + + if(!before.isNull()) + beforePreviousPartialMatch = buffer.endsWithPartialMatch(before); + + bufferOffset += bufferSize(); + } + + // Since we hit the end of the file, reset the status before continuing. + + clear(); + + seek(originalPosition); + + return -1; +} + + +long File::rfind(const ByteVector &pattern, long fromOffset, const ByteVector &before) +{ + if(!d->stream || pattern.size() > bufferSize()) + return -1; + + // The position in the file that the current buffer starts at. + + ByteVector buffer; + + // These variables are used to keep track of a partial match that happens at + // the end of a buffer. + + /* + int previousPartialMatch = -1; + int beforePreviousPartialMatch = -1; + */ + + // Save the location of the current read pointer. We will restore the + // position using seek() before all returns. + + long originalPosition = tell(); + + // Start the search at the offset. + + long bufferOffset; + if(fromOffset == 0) { + seek(-1 * int(bufferSize()), End); + bufferOffset = tell(); + } + else { + seek(fromOffset + -1 * int(bufferSize()), Beginning); + bufferOffset = tell(); + } + + // See the notes in find() for an explanation of this algorithm. + + for(buffer = readBlock(bufferSize()); buffer.size() > 0; buffer = readBlock(bufferSize())) { + + // TODO: (1) previous partial match + + // (2) pattern contained in current buffer + + long location = buffer.rfind(pattern); + if(location >= 0) { + seek(originalPosition); + return bufferOffset + location; + } + + if(!before.isNull() && buffer.find(before) >= 0) { + seek(originalPosition); + return -1; + } + + // TODO: (3) partial match + + bufferOffset -= bufferSize(); + seek(bufferOffset); + } + + // Since we hit the end of the file, reset the status before continuing. + + clear(); + + seek(originalPosition); + + return -1; +} + +void File::insert(const ByteVector &data, ulong start, ulong replace) +{ + d->stream->insert(data, start, replace); +} + +void File::removeBlock(ulong start, ulong length) +{ + d->stream->removeBlock(start, length); +} + +bool File::readOnly() const +{ + return d->stream->readOnly(); +} + +bool File::isOpen() const +{ + return d->stream->isOpen(); +} + +bool File::isValid() const +{ + return isOpen() && d->valid; +} + +void File::seek(long offset, Position p) +{ + d->stream->seek(offset, IOStream::Position(p)); +} + +void File::truncate(long length) +{ + d->stream->truncate(length); +} + +void File::clear() +{ + d->stream->clear(); +} + +long File::tell() const +{ + return d->stream->tell(); +} + +long File::length() +{ + return d->stream->length(); +} + +bool File::isReadable(const char *file) +{ + +#if defined(_MSC_VER) && (_MSC_VER >= 1400) // VC++2005 or later + + return _access_s(file, R_OK) == 0; + +#else + + return access(file, R_OK) == 0; + +#endif + +} + +bool File::isWritable(const char *file) +{ + +#if defined(_MSC_VER) && (_MSC_VER >= 1400) // VC++2005 or later + + return _access_s(file, W_OK) == 0; + +#else + + return access(file, W_OK) == 0; + +#endif + +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +TagLib::uint File::bufferSize() +{ + return BufferSize; +} + +void File::setValid(bool valid) +{ + d->valid = valid; +} + diff --git a/taglib/taglib/toolkit/tfile.h b/taglib/taglib/toolkit/tfile.h new file mode 100644 index 0000000..67f6f80 --- /dev/null +++ b/taglib/taglib/toolkit/tfile.h @@ -0,0 +1,306 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_FILE_H +#define TAGLIB_FILE_H + +#include "taglib_export.h" +#include "taglib.h" +#include "tag.h" +#include "tbytevector.h" +#include "tiostream.h" + +namespace TagLib { + + class String; + class Tag; + class AudioProperties; + class PropertyMap; + + //! A file class with some useful methods for tag manipulation + + /*! + * This class is a basic file class with some methods that are particularly + * useful for tag editors. It has methods to take advantage of + * ByteVector and a binary search method for finding patterns in a file. + */ + + class TAGLIB_EXPORT File + { + public: + /*! + * Position in the file used for seeking. + */ + enum Position { + //! Seek from the beginning of the file. + Beginning, + //! Seek from the current position in the file. + Current, + //! Seek from the end of the file. + End + }; + + /*! + * Destroys this File instance. + */ + virtual ~File(); + + /*! + * Returns the file name in the local file system encoding. + */ + FileName name() const; + + /*! + * Returns a pointer to this file's tag. This should be reimplemented in + * the concrete subclasses. + */ + virtual Tag *tag() const = 0; + + /*! + * Exports the tags of the file as dictionary mapping (human readable) tag + * names (uppercase Strings) to StringLists of tag values. Calls the according + * specialization in the File subclasses. + * For each metadata object of the file that could not be parsed into the PropertyMap + * format, the returend map's unsupportedData() list will contain one entry identifying + * that object (e.g. the frame type for ID3v2 tags). Use removeUnsupportedProperties() + * to remove (a subset of) them. + * For files that contain more than one tag (e.g. an MP3 with both an ID3v2 and an ID3v2 + * tag) only the most "modern" one will be exported (ID3v2 in this case). + * BIC: Will be made virtual in future releases. + */ + PropertyMap properties() const; + + /*! + * Removes unsupported properties, or a subset of them, from the file's metadata. + * The parameter \a properties must contain only entries from + * properties().unsupportedData(). + * BIC: Will be mad virtual in future releases. + */ + void removeUnsupportedProperties(const StringList& properties); + + /*! + * Sets the tags of this File to those specified in \a properties. Calls the + * according specialization method in the subclasses of File to do the translation + * into the format-specific details. + * If some value(s) could not be written imported to the specific metadata format, + * the returned PropertyMap will contain those value(s). Otherwise it will be empty, + * indicating that no problems occured. + * With file types that support several tag formats (for instance, MP3 files can have + * ID3v1, ID3v2, and APEv2 tags), this function will create the most appropriate one + * (ID3v2 for MP3 files). Older formats will be updated as well, if they exist, but won't + * be taken into account for the return value of this function. + * See the documentation of the subclass implementations for detailed descriptions. + * BIC: will become pure virtual in the future + */ + PropertyMap setProperties(const PropertyMap &properties); + + /*! + * Returns a pointer to this file's audio properties. This should be + * reimplemented in the concrete subclasses. If no audio properties were + * read then this will return a null pointer. + */ + virtual AudioProperties *audioProperties() const = 0; + + /*! + * Save the file and its associated tags. This should be reimplemented in + * the concrete subclasses. Returns true if the save succeeds. + * + * \warning On UNIX multiple processes are able to write to the same file at + * the same time. This can result in serious file corruption. If you are + * developing a program that makes use of TagLib from multiple processes you + * must insure that you are only doing writes to a particular file from one + * of them. + */ + virtual bool save() = 0; + + /*! + * Reads a block of size \a length at the current get pointer. + */ + ByteVector readBlock(ulong length); + + /*! + * Attempts to write the block \a data at the current get pointer. If the + * file is currently only opened read only -- i.e. readOnly() returns true -- + * this attempts to reopen the file in read/write mode. + * + * \note This should be used instead of using the streaming output operator + * for a ByteVector. And even this function is significantly slower than + * doing output with a char[]. + */ + void writeBlock(const ByteVector &data); + + /*! + * Returns the offset in the file that \a pattern occurs at or -1 if it can + * not be found. If \a before is set, the search will only continue until the + * pattern \a before is found. This is useful for tagging purposes to search + * for a tag before the synch frame. + * + * Searching starts at \a fromOffset, which defaults to the beginning of the + * file. + * + * \note This has the practial limitation that \a pattern can not be longer + * than the buffer size used by readBlock(). Currently this is 1024 bytes. + */ + long find(const ByteVector &pattern, + long fromOffset = 0, + const ByteVector &before = ByteVector::null); + + /*! + * Returns the offset in the file that \a pattern occurs at or -1 if it can + * not be found. If \a before is set, the search will only continue until the + * pattern \a before is found. This is useful for tagging purposes to search + * for a tag before the synch frame. + * + * Searching starts at \a fromOffset and proceeds from the that point to the + * beginning of the file and defaults to the end of the file. + * + * \note This has the practial limitation that \a pattern can not be longer + * than the buffer size used by readBlock(). Currently this is 1024 bytes. + */ + long rfind(const ByteVector &pattern, + long fromOffset = 0, + const ByteVector &before = ByteVector::null); + + /*! + * Insert \a data at position \a start in the file overwriting \a replace + * bytes of the original content. + * + * \note This method is slow since it requires rewriting all of the file + * after the insertion point. + */ + void insert(const ByteVector &data, ulong start = 0, ulong replace = 0); + + /*! + * Removes a block of the file starting a \a start and continuing for + * \a length bytes. + * + * \note This method is slow since it involves rewriting all of the file + * after the removed portion. + */ + void removeBlock(ulong start = 0, ulong length = 0); + + /*! + * Returns true if the file is read only (or if the file can not be opened). + */ + bool readOnly() const; + + /*! + * Since the file can currently only be opened as an argument to the + * constructor (sort-of by design), this returns if that open succeeded. + */ + bool isOpen() const; + + /*! + * Returns true if the file is open and readable. + */ + bool isValid() const; + + /*! + * Move the I/O pointer to \a offset in the file from position \a p. This + * defaults to seeking from the beginning of the file. + * + * \see Position + */ + void seek(long offset, Position p = Beginning); + + /*! + * Reset the end-of-file and error flags on the file. + */ + void clear(); + + /*! + * Returns the current offset within the file. + */ + long tell() const; + + /*! + * Returns the length of the file. + */ + long length(); + + /*! + * Returns true if \a file can be opened for reading. If the file does not + * exist, this will return false. + * + * \deprecated + */ + static bool isReadable(const char *file); + + /*! + * Returns true if \a file can be opened for writing. + * + * \deprecated + */ + static bool isWritable(const char *name); + + protected: + /*! + * Construct a File object and opens the \a file. \a file should be a + * be a C-string in the local file system encoding. + * + * \note Constructor is protected since this class should only be + * instantiated through subclasses. + */ + File(FileName file); + + /*! + * Construct a File object and use the \a stream instance. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note Constructor is protected since this class should only be + * instantiated through subclasses. + */ + File(IOStream *stream); + + /*! + * Marks the file as valid or invalid. + * + * \see isValid() + */ + void setValid(bool valid); + + /*! + * Truncates the file to a \a length. + */ + void truncate(long length); + + /*! + * Returns the buffer size that is used for internal buffering. + */ + static uint bufferSize(); + + private: + File(const File &); + File &operator=(const File &); + + class FilePrivate; + FilePrivate *d; + }; + +} + +#endif diff --git a/taglib/taglib/toolkit/tfilestream.cpp b/taglib/taglib/toolkit/tfilestream.cpp new file mode 100644 index 0000000..eae5257 --- /dev/null +++ b/taglib/taglib/toolkit/tfilestream.cpp @@ -0,0 +1,492 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tfilestream.h" +#include "tstring.h" +#include "tdebug.h" + +#ifdef _WIN32 +# include <windows.h> +#else +# include <stdio.h> +# include <unistd.h> +#endif + +using namespace TagLib; + +namespace +{ +#ifdef _WIN32 + + // Uses Win32 native API instead of POSIX API to reduce the resource consumption. + + typedef FileName FileNameHandle; + typedef HANDLE FileHandle; + + const TagLib::uint BufferSize = 8192; + const FileHandle InvalidFileHandle = INVALID_HANDLE_VALUE; + + inline FileHandle openFile(const FileName &path, bool readOnly) + { + const DWORD access = readOnly ? GENERIC_READ : (GENERIC_READ | GENERIC_WRITE); + + if(!path.wstr().empty()) + return CreateFileW(path.wstr().c_str(), access, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + else if(!path.str().empty()) + return CreateFileA(path.str().c_str(), access, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + else + return InvalidFileHandle; + } + + inline void closeFile(FileHandle file) + { + CloseHandle(file); + } + + inline size_t readFile(FileHandle file, ByteVector &buffer) + { + DWORD length; + if(ReadFile(file, buffer.data(), static_cast<DWORD>(buffer.size()), &length, NULL)) + return static_cast<size_t>(length); + else + return 0; + } + + inline size_t writeFile(FileHandle file, const ByteVector &buffer) + { + DWORD length; + if(WriteFile(file, buffer.data(), static_cast<DWORD>(buffer.size()), &length, NULL)) + return static_cast<size_t>(length); + else + return 0; + } + +#else // _WIN32 + + struct FileNameHandle : public std::string + { + FileNameHandle(FileName name) : std::string(name) {} + operator FileName () const { return c_str(); } + }; + + typedef FILE* FileHandle; + + const TagLib::uint BufferSize = 8192; + const FileHandle InvalidFileHandle = 0; + + inline FileHandle openFile(const FileName &path, bool readOnly) + { + return fopen(path, readOnly ? "rb" : "rb+"); + } + + inline void closeFile(FileHandle file) + { + fclose(file); + } + + inline size_t readFile(FileHandle file, ByteVector &buffer) + { + return fread(buffer.data(), sizeof(char), buffer.size(), file); + } + + inline size_t writeFile(FileHandle file, const ByteVector &buffer) + { + return fwrite(buffer.data(), sizeof(char), buffer.size(), file); + } + +#endif // _WIN32 +} + +class FileStream::FileStreamPrivate +{ +public: + FileStreamPrivate(const FileName &fileName) + : file(InvalidFileHandle) + , name(fileName) + , readOnly(true) + { + } + + FileHandle file; + FileNameHandle name; + bool readOnly; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +FileStream::FileStream(FileName fileName, bool openReadOnly) + : d(new FileStreamPrivate(fileName)) +{ + // First try with read / write mode, if that fails, fall back to read only. + + if(!openReadOnly) + d->file = openFile(fileName, false); + + if(d->file != InvalidFileHandle) + d->readOnly = false; + else + d->file = openFile(fileName, true); + + if(d->file == InvalidFileHandle) + { +# ifdef _WIN32 + debug("Could not open file " + fileName.toString()); +# else + debug("Could not open file " + String(static_cast<const char *>(d->name))); +# endif + } +} + +FileStream::~FileStream() +{ + if(isOpen()) + closeFile(d->file); + + delete d; +} + +FileName FileStream::name() const +{ + return d->name; +} + +ByteVector FileStream::readBlock(ulong length) +{ + if(!isOpen()) { + debug("File::readBlock() -- invalid file."); + return ByteVector::null; + } + + if(length == 0) + return ByteVector::null; + + const ulong streamLength = static_cast<ulong>(FileStream::length()); + if(length > bufferSize() && length > streamLength) + length = streamLength; + + ByteVector buffer(static_cast<uint>(length)); + + const size_t count = readFile(d->file, buffer); + buffer.resize(static_cast<uint>(count)); + + return buffer; +} + +void FileStream::writeBlock(const ByteVector &data) +{ + if(!isOpen()) { + debug("File::writeBlock() -- invalid file."); + return; + } + + if(readOnly()) { + debug("File::writeBlock() -- read only file."); + return; + } + + writeFile(d->file, data); +} + +void FileStream::insert(const ByteVector &data, ulong start, ulong replace) +{ + if(!isOpen()) { + debug("File::insert() -- invalid file."); + return; + } + + if(readOnly()) { + debug("File::insert() -- read only file."); + return; + } + + if(data.size() == replace) { + seek(start); + writeBlock(data); + return; + } + else if(data.size() < replace) { + seek(start); + writeBlock(data); + removeBlock(start + data.size(), replace - data.size()); + return; + } + + // Woohoo! Faster (about 20%) than id3lib at last. I had to get hardcore + // and avoid TagLib's high level API for rendering just copying parts of + // the file that don't contain tag data. + // + // Now I'll explain the steps in this ugliness: + + // First, make sure that we're working with a buffer that is longer than + // the *differnce* in the tag sizes. We want to avoid overwriting parts + // that aren't yet in memory, so this is necessary. + + ulong bufferLength = bufferSize(); + + while(data.size() - replace > bufferLength) + bufferLength += bufferSize(); + + // Set where to start the reading and writing. + + long readPosition = start + replace; + long writePosition = start; + + ByteVector buffer = data; + ByteVector aboutToOverwrite(static_cast<uint>(bufferLength)); + + while(true) + { + // Seek to the current read position and read the data that we're about + // to overwrite. Appropriately increment the readPosition. + + seek(readPosition); + const size_t bytesRead = readFile(d->file, aboutToOverwrite); + aboutToOverwrite.resize(bytesRead); + readPosition += bufferLength; + + // Check to see if we just read the last block. We need to call clear() + // if we did so that the last write succeeds. + + if(bytesRead < bufferLength) + clear(); + + // Seek to the write position and write our buffer. Increment the + // writePosition. + + seek(writePosition); + writeBlock(buffer); + + // We hit the end of the file. + + if(bytesRead == 0) + break; + + writePosition += buffer.size(); + + // Make the current buffer the data that we read in the beginning. + + buffer = aboutToOverwrite; + } +} + +void FileStream::removeBlock(ulong start, ulong length) +{ + if(!isOpen()) { + debug("File::removeBlock() -- invalid file."); + return; + } + + ulong bufferLength = bufferSize(); + + long readPosition = start + length; + long writePosition = start; + + ByteVector buffer(static_cast<uint>(bufferLength)); + + for(size_t bytesRead = -1; bytesRead != 0;) + { + seek(readPosition); + bytesRead = readFile(d->file, buffer); + readPosition += bytesRead; + + // Check to see if we just read the last block. We need to call clear() + // if we did so that the last write succeeds. + + if(bytesRead < buffer.size()) { + clear(); + buffer.resize(bytesRead); + } + + seek(writePosition); + writeFile(d->file, buffer); + + writePosition += bytesRead; + } + + truncate(writePosition); +} + +bool FileStream::readOnly() const +{ + return d->readOnly; +} + +bool FileStream::isOpen() const +{ + return (d->file != InvalidFileHandle); +} + +void FileStream::seek(long offset, Position p) +{ + if(!isOpen()) { + debug("File::seek() -- invalid file."); + return; + } + +#ifdef _WIN32 + + DWORD whence; + switch(p) { + case Beginning: + whence = FILE_BEGIN; + break; + case Current: + whence = FILE_CURRENT; + break; + case End: + whence = FILE_END; + break; + default: + debug("FileStream::seek() -- Invalid Position value."); + return; + } + + SetFilePointer(d->file, offset, NULL, whence); + if(GetLastError() != NO_ERROR) { + debug("File::seek() -- Failed to set the file pointer."); + } + +#else + + int whence; + switch(p) { + case Beginning: + whence = SEEK_SET; + break; + case Current: + whence = SEEK_CUR; + break; + case End: + whence = SEEK_END; + break; + default: + debug("FileStream::seek() -- Invalid Position value."); + return; + } + + fseek(d->file, offset, whence); + +#endif +} + +void FileStream::clear() +{ +#ifdef _WIN32 + + // NOP + +#else + + clearerr(d->file); + +#endif +} + +long FileStream::tell() const +{ +#ifdef _WIN32 + + const DWORD position = SetFilePointer(d->file, 0, NULL, FILE_CURRENT); + if(GetLastError() == NO_ERROR) { + return static_cast<long>(position); + } + else { + debug("File::tell() -- Failed to get the file pointer."); + return 0; + } + +#else + + return ftell(d->file); + +#endif +} + +long FileStream::length() +{ + if(!isOpen()) { + debug("File::length() -- invalid file."); + return 0; + } + +#ifdef _WIN32 + + const DWORD fileSize = GetFileSize(d->file, NULL); + if(GetLastError() == NO_ERROR) { + return static_cast<ulong>(fileSize); + } + else { + debug("File::length() -- Failed to get the file size."); + return 0; + } + +#else + + const long curpos = tell(); + + seek(0, End); + const long endpos = tell(); + + seek(curpos, Beginning); + + return endpos; + +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void FileStream::truncate(long length) +{ +#ifdef _WIN32 + + const long currentPos = tell(); + + seek(length); + SetEndOfFile(d->file); + if(GetLastError() != NO_ERROR) { + debug("File::truncate() -- Failed to truncate the file."); + } + + seek(currentPos); + +#else + + const int error = ftruncate(fileno(d->file), length); + if(error != 0) { + debug("FileStream::truncate() -- Coundn't truncate the file."); + } + +#endif +} + +TagLib::uint FileStream::bufferSize() +{ + return BufferSize; +} diff --git a/taglib/taglib/toolkit/tfilestream.h b/taglib/taglib/toolkit/tfilestream.h new file mode 100644 index 0000000..fa113b7 --- /dev/null +++ b/taglib/taglib/toolkit/tfilestream.h @@ -0,0 +1,154 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_FILESTREAM_H +#define TAGLIB_FILESTREAM_H + +#include "taglib_export.h" +#include "taglib.h" +#include "tbytevector.h" +#include "tiostream.h" + +namespace TagLib { + + class String; + class Tag; + class AudioProperties; + + //! A file class with some useful methods for tag manipulation + + /*! + * This class is a basic file class with some methods that are particularly + * useful for tag editors. It has methods to take advantage of + * ByteVector and a binary search method for finding patterns in a file. + */ + + class TAGLIB_EXPORT FileStream : public IOStream + { + public: + /*! + * Construct a File object and opens the \a file. \a file should be a + * be a C-string in the local file system encoding. + */ + FileStream(FileName file, bool openReadOnly = false); + + /*! + * Destroys this FileStream instance. + */ + virtual ~FileStream(); + + /*! + * Returns the file name in the local file system encoding. + */ + FileName name() const; + + /*! + * Reads a block of size \a length at the current get pointer. + */ + ByteVector readBlock(ulong length); + + /*! + * Attempts to write the block \a data at the current get pointer. If the + * file is currently only opened read only -- i.e. readOnly() returns true -- + * this attempts to reopen the file in read/write mode. + * + * \note This should be used instead of using the streaming output operator + * for a ByteVector. And even this function is significantly slower than + * doing output with a char[]. + */ + void writeBlock(const ByteVector &data); + + /*! + * Insert \a data at position \a start in the file overwriting \a replace + * bytes of the original content. + * + * \note This method is slow since it requires rewriting all of the file + * after the insertion point. + */ + void insert(const ByteVector &data, ulong start = 0, ulong replace = 0); + + /*! + * Removes a block of the file starting a \a start and continuing for + * \a length bytes. + * + * \note This method is slow since it involves rewriting all of the file + * after the removed portion. + */ + void removeBlock(ulong start = 0, ulong length = 0); + + /*! + * Returns true if the file is read only (or if the file can not be opened). + */ + bool readOnly() const; + + /*! + * Since the file can currently only be opened as an argument to the + * constructor (sort-of by design), this returns if that open succeeded. + */ + bool isOpen() const; + + /*! + * Move the I/O pointer to \a offset in the file from position \a p. This + * defaults to seeking from the beginning of the file. + * + * \see Position + */ + void seek(long offset, Position p = Beginning); + + /*! + * Reset the end-of-file and error flags on the file. + */ + void clear(); + + /*! + * Returns the current offset within the file. + */ + long tell() const; + + /*! + * Returns the length of the file. + */ + long length(); + + /*! + * Truncates the file to a \a length. + */ + void truncate(long length); + + protected: + + /*! + * Returns the buffer size that is used for internal buffering. + */ + static uint bufferSize(); + + private: + class FileStreamPrivate; + FileStreamPrivate *d; + }; + +} + +#endif diff --git a/taglib/taglib/toolkit/tiostream.cpp b/taglib/taglib/toolkit/tiostream.cpp new file mode 100644 index 0000000..c2ad291 --- /dev/null +++ b/taglib/taglib/toolkit/tiostream.cpp @@ -0,0 +1,150 @@ +/*************************************************************************** + copyright : (C) 2011 by Lukas Lalinsky + email : lalinsky@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tiostream.h" + +using namespace TagLib; + +#ifdef _WIN32 + +# include "tstring.h" +# include "tdebug.h" +# include <windows.h> + +namespace +{ + // Check if the running system has CreateFileW() function. + // Windows9x systems don't have CreateFileW() or can't accept Unicode file names. + + bool supportsUnicode() + { + const FARPROC p = GetProcAddress(GetModuleHandleA("kernel32"), "CreateFileW"); + return (p != NULL); + } + + // Indicates whether the system supports Unicode file names. + + const bool SystemSupportsUnicode = supportsUnicode(); + + // Converts a UTF-16 string into a local encoding. + // This function should only be used in Windows9x systems which don't support + // Unicode file names. + + std::string unicodeToAnsi(const wchar_t *wstr) + { + if(SystemSupportsUnicode) { + debug("unicodeToAnsi() - Should not be used on WinNT systems."); + } + + const int len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL); + if(len == 0) + return std::string(); + + std::string str(len, '\0'); + WideCharToMultiByte(CP_ACP, 0, wstr, -1, &str[0], len, NULL, NULL); + + return str; + } +} + +// If WinNT, stores a Unicode string into m_wname directly. +// If Win9x, converts and stores it into m_name to avoid calling Unicode version functions. + +FileName::FileName(const wchar_t *name) + : m_name (SystemSupportsUnicode ? "" : unicodeToAnsi(name)) + , m_wname(SystemSupportsUnicode ? name : L"") +{ +} + +FileName::FileName(const char *name) + : m_name(name) +{ +} + +FileName::FileName(const FileName &name) + : m_name (name.m_name) + , m_wname(name.m_wname) +{ +} + +FileName::operator const wchar_t *() const +{ + return m_wname.c_str(); +} + +FileName::operator const char *() const +{ + return m_name.c_str(); +} + +const std::wstring &FileName::wstr() const +{ + return m_wname; +} + +const std::string &FileName::str() const +{ + return m_name; +} + +String FileName::toString() const +{ + if(!m_wname.empty()) { + return String(m_wname); + } + else if(!m_name.empty()) { + const int len = MultiByteToWideChar(CP_ACP, 0, m_name.c_str(), -1, NULL, 0); + if(len == 0) + return String::null; + + std::vector<wchar_t> buf(len); + MultiByteToWideChar(CP_ACP, 0, m_name.c_str(), -1, &buf[0], len); + + return String(&buf[0]); + } + else { + return String::null; + } +} + + +#endif // _WIN32 + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +IOStream::IOStream() +{ +} + +IOStream::~IOStream() +{ +} + +void IOStream::clear() +{ +} + diff --git a/taglib/taglib/toolkit/tiostream.h b/taglib/taglib/toolkit/tiostream.h new file mode 100644 index 0000000..8682696 --- /dev/null +++ b/taglib/taglib/toolkit/tiostream.h @@ -0,0 +1,169 @@ +/*************************************************************************** + copyright : (C) 2011 by Lukas Lalinsky + email : lalinsky@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_IOSTREAM_H +#define TAGLIB_IOSTREAM_H + +#include "taglib_export.h" +#include "taglib.h" +#include "tbytevector.h" + +namespace TagLib { + +#ifdef _WIN32 + class TAGLIB_EXPORT FileName + { + public: + FileName(const wchar_t *name); + FileName(const char *name); + + FileName(const FileName &name); + + operator const wchar_t *() const; + operator const char *() const; + + const std::wstring &wstr() const; + const std::string &str() const; + + String toString() const; + + private: + const std::string m_name; + const std::wstring m_wname; + }; +#else + typedef const char *FileName; +#endif + + //! An abstract class that provides operations on a sequence of bytes + + class TAGLIB_EXPORT IOStream + { + public: + /*! + * Position in the file used for seeking. + */ + enum Position { + //! Seek from the beginning of the file. + Beginning, + //! Seek from the current position in the file. + Current, + //! Seek from the end of the file. + End + }; + + IOStream(); + + /*! + * Destroys this IOStream instance. + */ + virtual ~IOStream(); + + /*! + * Returns the stream name in the local file system encoding. + */ + virtual FileName name() const = 0; + + /*! + * Reads a block of size \a length at the current get pointer. + */ + virtual ByteVector readBlock(ulong length) = 0; + + /*! + * Attempts to write the block \a data at the current get pointer. If the + * file is currently only opened read only -- i.e. readOnly() returns true -- + * this attempts to reopen the file in read/write mode. + * + * \note This should be used instead of using the streaming output operator + * for a ByteVector. And even this function is significantly slower than + * doing output with a char[]. + */ + virtual void writeBlock(const ByteVector &data) = 0; + + /*! + * Insert \a data at position \a start in the file overwriting \a replace + * bytes of the original content. + * + * \note This method is slow since it requires rewriting all of the file + * after the insertion point. + */ + virtual void insert(const ByteVector &data, ulong start = 0, ulong replace = 0) = 0; + + /*! + * Removes a block of the file starting a \a start and continuing for + * \a length bytes. + * + * \note This method is slow since it involves rewriting all of the file + * after the removed portion. + */ + virtual void removeBlock(ulong start = 0, ulong length = 0) = 0; + + /*! + * Returns true if the file is read only (or if the file can not be opened). + */ + virtual bool readOnly() const = 0; + + /*! + * Since the file can currently only be opened as an argument to the + * constructor (sort-of by design), this returns if that open succeeded. + */ + virtual bool isOpen() const = 0; + + /*! + * Move the I/O pointer to \a offset in the stream from position \a p. This + * defaults to seeking from the beginning of the stream. + * + * \see Position + */ + virtual void seek(long offset, Position p = Beginning) = 0; + + /*! + * Reset the end-of-stream and error flags on the stream. + */ + virtual void clear(); + + /*! + * Returns the current offset within the stream. + */ + virtual long tell() const = 0; + + /*! + * Returns the length of the stream. + */ + virtual long length() = 0; + + /*! + * Truncates the stream to a \a length. + */ + virtual void truncate(long length) = 0; + + private: + IOStream(const IOStream &); + IOStream &operator=(const IOStream &); + }; + +} + +#endif diff --git a/taglib/taglib/toolkit/tlist.h b/taglib/taglib/toolkit/tlist.h new file mode 100644 index 0000000..0099dad --- /dev/null +++ b/taglib/taglib/toolkit/tlist.h @@ -0,0 +1,257 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_LIST_H +#define TAGLIB_LIST_H + +#include "taglib.h" + +#include <list> + +namespace TagLib { + + //! A generic, implicitly shared list. + + /*! + * This is basic generic list that's somewhere between a std::list and a + * QValueList. This class is implicitly shared. For example: + * + * \code + * + * TagLib::List<int> l = someOtherIntList; + * + * \endcode + * + * The above example is very cheap. This also makes lists suitable for the + * return types of functions. The above example will just copy a pointer rather + * than copying the data in the list. When your \e shared list's data changes, + * only \e then will the data be copied. + */ + + template <class T> class List + { + public: +#ifndef DO_NOT_DOCUMENT + typedef typename std::list<T>::iterator Iterator; + typedef typename std::list<T>::const_iterator ConstIterator; +#endif + + /*! + * Constructs an empty list. + */ + List(); + + /*! + * Make a shallow, implicitly shared, copy of \a l. Because this is + * implicitly shared, this method is lightweight and suitable for + * pass-by-value usage. + */ + List(const List<T> &l); + + /*! + * Destroys this List instance. If auto deletion is enabled and this list + * contains a pointer type all of the memebers are also deleted. + */ + virtual ~List(); + + /*! + * Returns an STL style iterator to the beginning of the list. See + * std::list::const_iterator for the semantics. + */ + Iterator begin(); + + /*! + * Returns an STL style constant iterator to the beginning of the list. See + * std::list::iterator for the semantics. + */ + ConstIterator begin() const; + + /*! + * Returns an STL style iterator to the end of the list. See + * std::list::iterator for the semantics. + */ + Iterator end(); + + /*! + * Returns an STL style constant iterator to the end of the list. See + * std::list::const_iterator for the semantics. + */ + ConstIterator end() const; + + /*! + * Inserts a copy of \a value before \a it. + */ + Iterator insert(Iterator it, const T &value); + + /*! + * Inserts the \a value into the list. This assumes that the list is + * currently sorted. If \a unique is true then the value will not + * be inserted if it is already in the list. + */ + List<T> &sortedInsert(const T &value, bool unique = false); + + /*! + * Appends \a item to the end of the list and returns a reference to the + * list. + */ + List<T> &append(const T &item); + + /*! + * Appends all of the values in \a l to the end of the list and returns a + * reference to the list. + */ + List<T> &append(const List<T> &l); + + /*! + * Prepends \a item to the beginning list and returns a reference to the + * list. + */ + List<T> &prepend(const T &item); + + /*! + * Prepends all of the items in \a l to the beginning list and returns a + * reference to the list. + */ + List<T> &prepend(const List<T> &l); + + /*! + * Clears the list. If auto deletion is enabled and this list contains a + * pointer type the members are also deleted. + * + * \see setAutoDelete() + */ + List<T> &clear(); + + /*! + * Returns the number of elements in the list. + */ + uint size() const; + bool isEmpty() const; + + /*! + * Find the first occurrence of \a value. + */ + Iterator find(const T &value); + + /*! + * Find the first occurrence of \a value. + */ + ConstIterator find(const T &value) const; + + /*! + * Returns true if the list contains \a value. + */ + bool contains(const T &value) const; + + /*! + * Erase the item at \a it from the list. + */ + Iterator erase(Iterator it); + + /*! + * Returns a reference to the first item in the list. + */ + const T &front() const; + + /*! + * Returns a reference to the first item in the list. + */ + T &front(); + + /*! + * Returns a reference to the last item in the list. + */ + const T &back() const; + + /*! + * Returns a reference to the last item in the list. + */ + T &back(); + + /*! + * Auto delete the members of the list when the last reference to the list + * passes out of scope. This will have no effect on lists which do not + * contain a pointer type. + * + * \note This relies on partial template instantiation -- most modern C++ + * compilers should now support this. + */ + void setAutoDelete(bool autoDelete); + + /*! + * Returns a reference to item \a i in the list. + * + * \warning This method is slow. Use iterators to loop through the list. + */ + T &operator[](uint i); + + /*! + * Returns a const reference to item \a i in the list. + * + * \warning This method is slow. Use iterators to loop through the list. + */ + const T &operator[](uint i) const; + + /*! + * Make a shallow, implicitly shared, copy of \a l. Because this is + * implicitly shared, this method is lightweight and suitable for + * pass-by-value usage. + */ + List<T> &operator=(const List<T> &l); + + /*! + * Compares this list with \a l and returns true if all of the elements are + * the same. + */ + bool operator==(const List<T> &l) const; + + /*! + * Compares this list with \a l and returns true if the lists differ. + */ + bool operator!=(const List<T> &l) const; + + protected: + /* + * If this List is being shared via implicit sharing, do a deep copy of the + * data and separate from the shared members. This should be called by all + * non-const subclass members. + */ + void detach(); + + private: +#ifndef DO_NOT_DOCUMENT + template <class TP> class ListPrivate; + ListPrivate<T> *d; +#endif + }; + +} + +// Since GCC doesn't support the "export" keyword, we have to include the +// implementation. + +#include "tlist.tcc" + +#endif diff --git a/taglib/taglib/toolkit/tlist.tcc b/taglib/taglib/toolkit/tlist.tcc new file mode 100644 index 0000000..72d4767 --- /dev/null +++ b/taglib/taglib/toolkit/tlist.tcc @@ -0,0 +1,324 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <algorithm> +#include "trefcounter.h" + +namespace TagLib { + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +// The functionality of List<T>::setAutoDelete() is implemented here partial +// template specialization. This is implemented in such a way that calling +// setAutoDelete() on non-pointer types will simply have no effect. + +// A base for the generic and specialized private class types. New +// non-templatized members should be added here. + +// BIC change to RefCounter +class ListPrivateBase : public RefCounterOld +{ +public: + ListPrivateBase() : autoDelete(false) {} + bool autoDelete; +}; + +// A generic implementation + +template <class T> +template <class TP> class List<T>::ListPrivate : public ListPrivateBase +{ +public: + ListPrivate() : ListPrivateBase() {} + ListPrivate(const std::list<TP> &l) : ListPrivateBase(), list(l) {} + void clear() { + list.clear(); + } + std::list<TP> list; +}; + +// A partial specialization for all pointer types that implements the +// setAutoDelete() functionality. + +template <class T> +template <class TP> class List<T>::ListPrivate<TP *> : public ListPrivateBase +{ +public: + ListPrivate() : ListPrivateBase() {} + ListPrivate(const std::list<TP *> &l) : ListPrivateBase(), list(l) {} + ~ListPrivate() { + clear(); + } + void clear() { + if(autoDelete) { + typename std::list<TP *>::const_iterator it = list.begin(); + for(; it != list.end(); ++it) + delete *it; + } + list.clear(); + } + std::list<TP *> list; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +template <class T> +List<T>::List() +{ + d = new ListPrivate<T>; +} + +template <class T> +List<T>::List(const List<T> &l) : d(l.d) +{ + d->ref(); +} + +template <class T> +List<T>::~List() +{ + if(d->deref()) + delete d; +} + +template <class T> +typename List<T>::Iterator List<T>::begin() +{ + detach(); + return d->list.begin(); +} + +template <class T> +typename List<T>::ConstIterator List<T>::begin() const +{ + return d->list.begin(); +} + +template <class T> +typename List<T>::Iterator List<T>::end() +{ + detach(); + return d->list.end(); +} + +template <class T> +typename List<T>::ConstIterator List<T>::end() const +{ + return d->list.end(); +} + +template <class T> +typename List<T>::Iterator List<T>::insert(Iterator it, const T &item) +{ + detach(); + return d->list.insert(it, item); +} + +template <class T> +List<T> &List<T>::sortedInsert(const T &value, bool unique) +{ + detach(); + Iterator it = begin(); + while(it != end() && *it < value) + ++it; + if(unique && it != end() && *it == value) + return *this; + insert(it, value); + return *this; +} + +template <class T> +List<T> &List<T>::append(const T &item) +{ + detach(); + d->list.push_back(item); + return *this; +} + +template <class T> +List<T> &List<T>::append(const List<T> &l) +{ + detach(); + d->list.insert(d->list.end(), l.begin(), l.end()); + return *this; +} + +template <class T> +List<T> &List<T>::prepend(const T &item) +{ + detach(); + d->list.push_front(item); + return *this; +} + +template <class T> +List<T> &List<T>::prepend(const List<T> &l) +{ + detach(); + d->list.insert(d->list.begin(), l.begin(), l.end()); + return *this; +} + +template <class T> +List<T> &List<T>::clear() +{ + detach(); + d->clear(); + return *this; +} + +template <class T> +TagLib::uint List<T>::size() const +{ + return d->list.size(); +} + +template <class T> +bool List<T>::isEmpty() const +{ + return d->list.empty(); +} + +template <class T> +typename List<T>::Iterator List<T>::find(const T &value) +{ + return std::find(d->list.begin(), d->list.end(), value); +} + +template <class T> +typename List<T>::ConstIterator List<T>::find(const T &value) const +{ + return std::find(d->list.begin(), d->list.end(), value); +} + +template <class T> +bool List<T>::contains(const T &value) const +{ + return std::find(d->list.begin(), d->list.end(), value) != d->list.end(); +} + +template <class T> +typename List<T>::Iterator List<T>::erase(Iterator it) +{ + return d->list.erase(it); +} + +template <class T> +const T &List<T>::front() const +{ + return d->list.front(); +} + +template <class T> +T &List<T>::front() +{ + detach(); + return d->list.front(); +} + +template <class T> +const T &List<T>::back() const +{ + return d->list.back(); +} + +template <class T> +void List<T>::setAutoDelete(bool autoDelete) +{ + d->autoDelete = autoDelete; +} + +template <class T> +T &List<T>::back() +{ + detach(); + return d->list.back(); +} + +template <class T> +T &List<T>::operator[](uint i) +{ + Iterator it = d->list.begin(); + + for(uint j = 0; j < i; j++) + ++it; + + return *it; +} + +template <class T> +const T &List<T>::operator[](uint i) const +{ + ConstIterator it = d->list.begin(); + + for(uint j = 0; j < i; j++) + ++it; + + return *it; +} + +template <class T> +List<T> &List<T>::operator=(const List<T> &l) +{ + if(&l == this) + return *this; + + if(d->deref()) + delete d; + d = l.d; + d->ref(); + return *this; +} + +template <class T> +bool List<T>::operator==(const List<T> &l) const +{ + return d->list == l.d->list; +} + +template <class T> +bool List<T>::operator!=(const List<T> &l) const +{ + return d->list != l.d->list; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +template <class T> +void List<T>::detach() +{ + if(d->count() > 1) { + d->deref(); + d = new ListPrivate<T>(d->list); + } +} + +} // namespace TagLib diff --git a/taglib/taglib/toolkit/tmap.h b/taglib/taglib/toolkit/tmap.h new file mode 100644 index 0000000..a7d9930 --- /dev/null +++ b/taglib/taglib/toolkit/tmap.h @@ -0,0 +1,199 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_MAP_H +#define TAGLIB_MAP_H + +#include <map> + +#include "taglib.h" + +namespace TagLib { + + //! A generic, implicitly shared map. + + /*! + * This implements a standard map container that associates a key with a value + * and has fast key-based lookups. This map is also implicitly shared making + * it suitable for pass-by-value usage. + */ + + template <class Key, class T> class Map + { + public: +#ifndef DO_NOT_DOCUMENT +#ifdef WANT_CLASS_INSTANTIATION_OF_MAP + // Some STL implementations get snippy over the use of the + // class keyword to distinguish different templates; Sun Studio + // in particular finds multiple specializations in certain rare + // cases and complains about that. GCC doesn't seem to mind, + // and uses the typedefs further below without the class keyword. + // Not all the specializations of Map can use the class keyword + // (when T is not actually a class type), so don't apply this + // generally. + typedef typename std::map<class Key, class T>::iterator Iterator; + typedef typename std::map<class Key, class T>::const_iterator ConstIterator; +#else + typedef typename std::map<Key, T>::iterator Iterator; + typedef typename std::map<Key, T>::const_iterator ConstIterator; +#endif +#endif + + /*! + * Constructs an empty Map. + */ + Map(); + + /*! + * Make a shallow, implicitly shared, copy of \a m. Because this is + * implicitly shared, this method is lightweight and suitable for + * pass-by-value usage. + */ + Map(const Map<Key, T> &m); + + /*! + * Destroys this instance of the Map. + */ + virtual ~Map(); + + /*! + * Returns an STL style iterator to the beginning of the map. See + * std::map::iterator for the semantics. + */ + Iterator begin(); + + /*! + * Returns an STL style iterator to the beginning of the map. See + * std::map::const_iterator for the semantics. + */ + ConstIterator begin() const; + + /*! + * Returns an STL style iterator to the end of the map. See + * std::map::iterator for the semantics. + */ + Iterator end(); + + /*! + * Returns an STL style iterator to the end of the map. See + * std::map::const_iterator for the semantics. + */ + ConstIterator end() const; + + /*! + * Inserts \a value under \a key in the map. If a value for \a key already + * exists it will be overwritten. + */ + Map<Key, T> &insert(const Key &key, const T &value); + + /*! + * Removes all of the elements from elements from the map. This however + * will not delete pointers if the mapped type is a pointer type. + */ + Map<Key, T> &clear(); + + /*! + * The number of elements in the map. + * + * \see isEmpty() + */ + uint size() const; + + /*! + * Returns true if the map is empty. + * + * \see size() + */ + bool isEmpty() const; + + /*! + * Find the first occurrence of \a key. + */ + Iterator find(const Key &key); + + /*! + * Find the first occurrence of \a key. + */ + ConstIterator find(const Key &key) const; + + /*! + * Returns true if the map contains an instance of \a key. + */ + bool contains(const Key &key) const; + + /*! + * Erase the item at \a it from the list. + */ + Map<Key, T> &erase(Iterator it); + + /*! + * Erase the item with \a key from the list. + */ + Map<Key, T> &erase(const Key &key); + + /*! + * Returns a reference to the value associated with \a key. + * + * \note This has undefined behavior if the key is not present in the map. + */ + const T &operator[](const Key &key) const; + + /*! + * Returns a reference to the value associated with \a key. + * + * \note This has undefined behavior if the key is not present in the map. + */ + T &operator[](const Key &key); + + /*! + * Make a shallow, implicitly shared, copy of \a m. Because this is + * implicitly shared, this method is lightweight and suitable for + * pass-by-value usage. + */ + Map<Key, T> &operator=(const Map<Key, T> &m); + + protected: + /* + * If this List is being shared via implicit sharing, do a deep copy of the + * data and separate from the shared members. This should be called by all + * non-const subclass members. + */ + void detach(); + + private: +#ifndef DO_NOT_DOCUMENT + template <class KeyP, class TP> class MapPrivate; + MapPrivate<Key, T> *d; +#endif + }; + +} + +// Since GCC doesn't support the "export" keyword, we have to include the +// implementation. + +#include "tmap.tcc" + +#endif diff --git a/taglib/taglib/toolkit/tmap.tcc b/taglib/taglib/toolkit/tmap.tcc new file mode 100644 index 0000000..5d3abca --- /dev/null +++ b/taglib/taglib/toolkit/tmap.tcc @@ -0,0 +1,199 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "trefcounter.h" + +namespace TagLib { + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +// BIC change to RefCounter +template <class Key, class T> +template <class KeyP, class TP> +class Map<Key, T>::MapPrivate : public RefCounterOld +{ +public: + MapPrivate() : RefCounterOld() {} +#ifdef WANT_CLASS_INSTANTIATION_OF_MAP + MapPrivate(const std::map<class KeyP, class TP>& m) : RefCounterOld(), map(m) {} + std::map<class KeyP, class TP> map; +#else + MapPrivate(const std::map<KeyP, TP>& m) : RefCounterOld(), map(m) {} + std::map<KeyP, TP> map; +#endif +}; + +template <class Key, class T> +Map<Key, T>::Map() +{ + d = new MapPrivate<Key, T>; +} + +template <class Key, class T> +Map<Key, T>::Map(const Map<Key, T> &m) : d(m.d) +{ + d->ref(); +} + +template <class Key, class T> +Map<Key, T>::~Map() +{ + if(d->deref()) + delete(d); +} + +template <class Key, class T> +typename Map<Key, T>::Iterator Map<Key, T>::begin() +{ + detach(); + return d->map.begin(); +} + +template <class Key, class T> +typename Map<Key, T>::ConstIterator Map<Key, T>::begin() const +{ + return d->map.begin(); +} + +template <class Key, class T> +typename Map<Key, T>::Iterator Map<Key, T>::end() +{ + detach(); + return d->map.end(); +} + +template <class Key, class T> +typename Map<Key, T>::ConstIterator Map<Key, T>::end() const +{ + return d->map.end(); +} + +template <class Key, class T> +Map<Key, T> &Map<Key, T>::insert(const Key &key, const T &value) +{ + detach(); + d->map[key] = value; + return *this; +} + +template <class Key, class T> +Map<Key, T> &Map<Key, T>::clear() +{ + detach(); + d->map.clear(); + return *this; +} + +template <class Key, class T> +bool Map<Key, T>::isEmpty() const +{ + return d->map.empty(); +} + +template <class Key, class T> +typename Map<Key, T>::Iterator Map<Key, T>::find(const Key &key) +{ + detach(); + return d->map.find(key); +} + +template <class Key, class T> +typename Map<Key,T>::ConstIterator Map<Key, T>::find(const Key &key) const +{ + return d->map.find(key); +} + +template <class Key, class T> +bool Map<Key, T>::contains(const Key &key) const +{ + return d->map.find(key) != d->map.end(); +} + +template <class Key, class T> +Map<Key, T> &Map<Key,T>::erase(Iterator it) +{ + detach(); + d->map.erase(it); + return *this; +} + +template <class Key, class T> +Map<Key, T> &Map<Key,T>::erase(const Key &key) +{ + detach(); + Iterator it = d->map.find(key); + if(it != d->map.end()) + d->map.erase(it); + return *this; +} + +template <class Key, class T> +TagLib::uint Map<Key, T>::size() const +{ + return d->map.size(); +} + +template <class Key, class T> +const T &Map<Key, T>::operator[](const Key &key) const +{ + return d->map[key]; +} + +template <class Key, class T> +T &Map<Key, T>::operator[](const Key &key) +{ + detach(); + return d->map[key]; +} + +template <class Key, class T> +Map<Key, T> &Map<Key, T>::operator=(const Map<Key, T> &m) +{ + if(&m == this) + return *this; + + if(d->deref()) + delete(d); + d = m.d; + d->ref(); + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +template <class Key, class T> +void Map<Key, T>::detach() +{ + if(d->count() > 1) { + d->deref(); + d = new MapPrivate<Key, T>(d->map); + } +} + +} // namespace TagLib diff --git a/taglib/taglib/toolkit/tpropertymap.cpp b/taglib/taglib/toolkit/tpropertymap.cpp new file mode 100644 index 0000000..3317cc9 --- /dev/null +++ b/taglib/taglib/toolkit/tpropertymap.cpp @@ -0,0 +1,173 @@ +/*************************************************************************** + copyright : (C) 2012 by Michael Helmling + email : helmling@mathematik.uni-kl.de + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "tpropertymap.h" +using namespace TagLib; + + +PropertyMap::PropertyMap() : SimplePropertyMap() +{ +} + +PropertyMap::PropertyMap(const PropertyMap &m) : SimplePropertyMap(m), unsupported(m.unsupported) +{ +} + +PropertyMap::PropertyMap(const SimplePropertyMap &m) +{ + for(SimplePropertyMap::ConstIterator it = m.begin(); it != m.end(); ++it){ + String key = it->first.upper(); + if(!key.isNull()) + insert(it->first, it->second); + else + unsupported.append(it->first); + } +} + +PropertyMap::~PropertyMap() +{ +} + +bool PropertyMap::insert(const String &key, const StringList &values) +{ + String realKey = key.upper(); + Iterator result = SimplePropertyMap::find(realKey); + if(result == end()) + SimplePropertyMap::insert(realKey, values); + else + SimplePropertyMap::operator[](realKey).append(values); + return true; +} + +bool PropertyMap::replace(const String &key, const StringList &values) +{ + String realKey = key.upper(); + SimplePropertyMap::erase(realKey); + SimplePropertyMap::insert(realKey, values); + return true; +} + +PropertyMap::Iterator PropertyMap::find(const String &key) +{ + return SimplePropertyMap::find(key.upper()); +} + +PropertyMap::ConstIterator PropertyMap::find(const String &key) const +{ + return SimplePropertyMap::find(key.upper()); +} + +bool PropertyMap::contains(const String &key) const +{ + return SimplePropertyMap::contains(key.upper()); +} + +bool PropertyMap::contains(const PropertyMap &other) const +{ + for(ConstIterator it = other.begin(); it != other.end(); ++it) { + if(!SimplePropertyMap::contains(it->first)) + return false; + if ((*this)[it->first] != it->second) + return false; + } + return true; +} + +PropertyMap &PropertyMap::erase(const String &key) +{ + SimplePropertyMap::erase(key.upper()); + return *this; +} + +PropertyMap &PropertyMap::erase(const PropertyMap &other) +{ + for(ConstIterator it = other.begin(); it != other.end(); ++it) + erase(it->first); + return *this; +} + +PropertyMap &PropertyMap::merge(const PropertyMap &other) +{ + for(PropertyMap::ConstIterator it = other.begin(); it != other.end(); ++it) + insert(it->first, it->second); + unsupported.append(other.unsupported); + return *this; +} + +const StringList &PropertyMap::operator[](const String &key) const +{ + return SimplePropertyMap::operator[](key.upper()); +} + +StringList &PropertyMap::operator[](const String &key) +{ + return SimplePropertyMap::operator[](key.upper()); +} + +bool PropertyMap::operator==(const PropertyMap &other) const +{ + for(ConstIterator it = other.begin(); it != other.end(); ++it) { + ConstIterator thisFind = find(it->first); + if( thisFind == end() || (thisFind->second != it->second) ) + return false; + } + for(ConstIterator it = begin(); it != end(); ++it) { + ConstIterator otherFind = other.find(it->first); + if( otherFind == other.end() || (otherFind->second != it->second) ) + return false; + } + return unsupported == other.unsupported; +} + +bool PropertyMap::operator!=(const PropertyMap &other) const +{ + return !(*this == other); +} + +String PropertyMap::toString() const +{ + String ret = ""; + for(ConstIterator it = begin(); it != end(); ++it) + ret += it->first+"="+it->second.toString(", ") + "\n"; + if(!unsupported.isEmpty()) + ret += "Unsupported Data: " + unsupported.toString(", ") + "\n"; + return ret; +} + +void PropertyMap::removeEmpty() +{ + StringList emptyKeys; + for(Iterator it = begin(); it != end(); ++it) + if(it->second.isEmpty()) + emptyKeys.append(it->first); + for(StringList::Iterator emptyIt = emptyKeys.begin(); emptyIt != emptyKeys.end(); emptyIt++ ) + erase(*emptyIt); +} + +StringList &PropertyMap::unsupportedData() +{ + return unsupported; +} + +const StringList &PropertyMap::unsupportedData() const +{ + return unsupported; +} diff --git a/taglib/taglib/toolkit/tpropertymap.h b/taglib/taglib/toolkit/tpropertymap.h new file mode 100644 index 0000000..2be49dd --- /dev/null +++ b/taglib/taglib/toolkit/tpropertymap.h @@ -0,0 +1,233 @@ +/*************************************************************************** + copyright : (C) 2012 by Michael Helmling + email : helmling@mathematik.uni-kl.de + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef PROPERTYMAP_H_ +#define PROPERTYMAP_H_ + +#include "tmap.h" +#include "tstringlist.h" + +namespace TagLib { + + typedef Map<String,StringList> SimplePropertyMap; + + //! A map for format-independent <key,valuelist> tag representations. + + /*! + * This map implements a generic representation of textual audio metadata + * ("tags") realized as pairs of a case-insensitive key + * and a nonempty list of corresponding values, each value being an an arbitrary + * unicode String. + * + * Note that most metadata formats pose additional conditions on the tag keys. The + * most popular ones (Vorbis, APE, ID3v2) should support all ASCII only words of + * length between 2 and 16. + * + * This class can contain any tags, but here is a list of "well-known" tags that + * you might want to use: + * + * Basic tags: + * + * - TITLE + * - ALBUM + * - ARTIST + * - ALBUMARTIST + * - SUBTITLE + * - TRACKNUMBER + * - DISCNUMBER + * - DATE + * - ORIGINALDATE + * - GENRE + * - COMMENT + * + * Sort names: + * + * - TITLESORT + * - ALBUMSORT + * - ARTISTSORT + * - ALBUMARTISTSORT + * + * Credits: + * + * - COMPOSER + * - LYRICIST + * - CONDUCTOR + * - REMIXER + * - PERFORMER:<XXXX> + * + * Other tags: + * + * - ISRC + * - ASIN + * - BPM + * - COPYRIGHT + * - ENCODEDBY + * - MOOD + * - COMMENT + * - MEDIA + * - LABEL + * - CATALOGNUMBER + * - BARCODE + * + * MusicBrainz identifiers: + * + * - MUSICBRAINZ_TRACKID + * - MUSICBRAINZ_ALBUMID + * - MUSICBRAINZ_RELEASEGROUPID + * - MUSICBRAINZ_WORKID + * - MUSICBRAINZ_ARTISTID + * - MUSICBRAINZ_ALBUMARTISTID + * - ACOUSTID_ID + * - ACOUSTID_FINGERPRINT + * - MUSICIP_PUID + * + */ + + class TAGLIB_EXPORT PropertyMap: public SimplePropertyMap + { + public: + + typedef SimplePropertyMap::Iterator Iterator; + typedef SimplePropertyMap::ConstIterator ConstIterator; + + PropertyMap(); + + PropertyMap(const PropertyMap &m); + + /*! + * Creates a PropertyMap initialized from a SimplePropertyMap. Copies all + * entries from \a m that have valid keys. + * Invalid keys will be appended to the unsupportedData() list. + */ + PropertyMap(const SimplePropertyMap &m); + + virtual ~PropertyMap(); + + /*! + * Inserts \a values under \a key in the map. If \a key already exists, + * then \values will be appended to the existing StringList. + * The returned value indicates success, i.e. whether \a key is a + * valid key. + */ + bool insert(const String &key, const StringList &values); + + /*! + * Replaces any existing values for \a key with the given \a values, + * and simply insert them if \a key did not exist before. + * The returned value indicates success, i.e. whether \a key is a + * valid key. + */ + bool replace(const String &key, const StringList &values); + + /*! + * Find the first occurrence of \a key. + */ + Iterator find(const String &key); + + /*! + * Find the first occurrence of \a key. + */ + ConstIterator find(const String &key) const; + + /*! + * Returns true if the map contains values for \a key. + */ + bool contains(const String &key) const; + + /*! + * Returns true if this map contains all keys of \a other + * and the values coincide for that keys. Does not take + * the unsupportedData list into account. + */ + bool contains(const PropertyMap &other) const; + + /*! + * Erase the \a key and its values from the map. + */ + PropertyMap &erase(const String &key); + + /*! + * Erases from this map all keys that appear in \a other. + */ + PropertyMap &erase(const PropertyMap &other); + + /*! + * Merge the contents of \a other into this PropertyMap. + * If a key is contained in both maps, the values of the second + * are appended to that of the first. + * The unsupportedData() lists are concatenated as well. + */ + PropertyMap &merge(const PropertyMap &other); + + /*! + * Returns a reference to the value associated with \a key. + * + * \note: If \a key is not contained in the map, an empty + * StringList is returned without error. + */ + const StringList &operator[](const String &key) const; + + /*! + * Returns a reference to the value associated with \a key. + * + * \note: If \a key is not contained in the map, an empty + * StringList is returned. You can also directly add entries + * by using this function as an lvalue. + */ + StringList &operator[](const String &key); + + /*! + * Returns true if and only if \other has the same contents as this map. + */ + bool operator==(const PropertyMap &other) const; + + /*! + * Returns false if and only \other has the same contents as this map. + */ + bool operator!=(const PropertyMap &other) const; + + /*! + * If a PropertyMap is read from a File object using File::properties(), + * the StringList returned from this function will represent metadata + * that could not be parsed into the PropertyMap representation. This could + * be e.g. binary data, unknown ID3 frames, etc. + * You can remove items from the returned list, which tells TagLib to remove + * those unsupported elements if you call File::setProperties() with the + * same PropertyMap as argument. + */ + StringList &unsupportedData(); + const StringList &unsupportedData() const; + + /*! + * Removes all entries which have an empty value list. + */ + void removeEmpty(); + + String toString() const; + + private: + + + StringList unsupported; + }; + +} +#endif /* PROPERTYMAP_H_ */ diff --git a/taglib/taglib/toolkit/trefcounter.cpp b/taglib/taglib/toolkit/trefcounter.cpp new file mode 100644 index 0000000..71f3c2f --- /dev/null +++ b/taglib/taglib/toolkit/trefcounter.cpp @@ -0,0 +1,108 @@ +/*************************************************************************** + copyright : (C) 2013 by Tsuda Kageyu + email : tsuda.kageyu@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "trefcounter.h" + +#if defined(HAVE_STD_ATOMIC) +# include <atomic> +# define ATOMIC_INT std::atomic<unsigned int> +# define ATOMIC_INC(x) x.fetch_add(1) +# define ATOMIC_DEC(x) (x.fetch_sub(1) - 1) +#elif defined(HAVE_BOOST_ATOMIC) +# include <boost/atomic.hpp> +# define ATOMIC_INT boost::atomic<unsigned int> +# define ATOMIC_INC(x) x.fetch_add(1) +# define ATOMIC_DEC(x) (x.fetch_sub(1) - 1) +#elif defined(HAVE_GCC_ATOMIC) +# define ATOMIC_INT int +# define ATOMIC_INC(x) __sync_add_and_fetch(&x, 1) +# define ATOMIC_DEC(x) __sync_sub_and_fetch(&x, 1) +#elif defined(HAVE_WIN_ATOMIC) +# if !defined(NOMINMAX) +# define NOMINMAX +# endif +# include <windows.h> +# define ATOMIC_INT long +# define ATOMIC_INC(x) InterlockedIncrement(&x) +# define ATOMIC_DEC(x) InterlockedDecrement(&x) +#elif defined(HAVE_MAC_ATOMIC) +# include <libkern/OSAtomic.h> +# define ATOMIC_INT int32_t +# define ATOMIC_INC(x) OSAtomicIncrement32Barrier(&x) +# define ATOMIC_DEC(x) OSAtomicDecrement32Barrier(&x) +#elif defined(HAVE_IA64_ATOMIC) +# include <ia64intrin.h> +# define ATOMIC_INT int +# define ATOMIC_INC(x) __sync_add_and_fetch(&x, 1) +# define ATOMIC_DEC(x) __sync_sub_and_fetch(&x, 1) +#else +# define ATOMIC_INT int +# define ATOMIC_INC(x) (++x) +# define ATOMIC_DEC(x) (--x) +#endif + +namespace TagLib +{ + class RefCounter::RefCounterPrivate + { + public: + RefCounterPrivate() : refCount(1) {} + + void ref() { ATOMIC_INC(refCount); } + bool deref() { return (ATOMIC_DEC(refCount) == 0); } + int count() const { return refCount; } + + volatile ATOMIC_INT refCount; + }; + + RefCounter::RefCounter() + : d(new RefCounterPrivate()) + { + } + + RefCounter::~RefCounter() + { + delete d; + } + + void RefCounter::ref() + { + d->ref(); + } + + bool RefCounter::deref() + { + return d->deref(); + } + + int RefCounter::count() const + { + return d->count(); + } +} diff --git a/taglib/taglib/toolkit/trefcounter.h b/taglib/taglib/toolkit/trefcounter.h new file mode 100644 index 0000000..75388b1 --- /dev/null +++ b/taglib/taglib/toolkit/trefcounter.h @@ -0,0 +1,111 @@ +/*************************************************************************** + copyright : (C) 2013 by Tsuda Kageyu + email : tsuda.kageyu@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_REFCOUNTER_H +#define TAGLIB_REFCOUNTER_H + +#include "taglib_export.h" +#include "taglib.h" + +#ifdef __APPLE__ +# include <libkern/OSAtomic.h> +# define TAGLIB_ATOMIC_MAC +#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__CYGWIN__) +# define NOMINMAX +# include <windows.h> +# define TAGLIB_ATOMIC_WIN +#elif defined (__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ >= 401) \ + && (defined(__i386__) || defined(__i486__) || defined(__i586__) || \ + defined(__i686__) || defined(__x86_64) || defined(__ia64)) \ + && !defined(__INTEL_COMPILER) +# define TAGLIB_ATOMIC_GCC +#elif defined(__ia64) && defined(__INTEL_COMPILER) +# include <ia64intrin.h> +# define TAGLIB_ATOMIC_GCC +#endif + +#ifndef DO_NOT_DOCUMENT // Tell Doxygen to skip this class. +/*! + * \internal + * This is just used as a base class for shared classes in TagLib. + * + * \warning This <b>is not</b> part of the TagLib public API! + */ +namespace TagLib +{ + + class TAGLIB_EXPORT RefCounter + { + public: + RefCounter(); + virtual ~RefCounter(); + + void ref(); + bool deref(); + int count() const; + + private: + class RefCounterPrivate; + RefCounterPrivate *d; + }; + + // BIC this old class is needed by tlist.tcc and tmap.tcc + class RefCounterOld + { + public: + RefCounterOld() : refCount(1) {} + +#ifdef TAGLIB_ATOMIC_MAC + void ref() { OSAtomicIncrement32Barrier(const_cast<int32_t*>(&refCount)); } + bool deref() { return ! OSAtomicDecrement32Barrier(const_cast<int32_t*>(&refCount)); } + int32_t count() { return refCount; } + private: + volatile int32_t refCount; +#elif defined(TAGLIB_ATOMIC_WIN) + void ref() { InterlockedIncrement(&refCount); } + bool deref() { return ! InterlockedDecrement(&refCount); } + long count() { return refCount; } + private: + volatile long refCount; +#elif defined(TAGLIB_ATOMIC_GCC) + void ref() { __sync_add_and_fetch(&refCount, 1); } + bool deref() { return ! __sync_sub_and_fetch(&refCount, 1); } + int count() { return refCount; } + private: + volatile int refCount; +#else + void ref() { refCount++; } + bool deref() { return ! --refCount; } + int count() { return refCount; } + private: + uint refCount; +#endif + }; + +} + +#endif // DO_NOT_DOCUMENT +#endif + diff --git a/taglib/taglib/toolkit/tstring.cpp b/taglib/taglib/toolkit/tstring.cpp new file mode 100644 index 0000000..75a9833 --- /dev/null +++ b/taglib/taglib/toolkit/tstring.cpp @@ -0,0 +1,880 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +// This class assumes that std::basic_string<T> has a contiguous and null-terminated buffer. + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "tstring.h" +#include "tdebug.h" +#include "tstringlist.h" +#include "trefcounter.h" +#include "tutils.h" + +#include <iostream> +#include <cstdio> +#include <cstring> + +#ifdef HAVE_STD_CODECVT +# include <codecvt> +#else +# include "unicode.h" +#endif + +namespace +{ + + inline unsigned short combine(unsigned char c1, unsigned char c2) + { + return (c1 << 8) | c2; + } + + void UTF16toUTF8(const wchar_t *src, size_t srcLength, char *dst, size_t dstLength) + { +#ifdef HAVE_STD_CODECVT + + typedef std::codecvt_utf8_utf16<wchar_t> utf8_utf16_t; + + using namespace TagLib; + + const wchar_t *srcBegin = src; + const wchar_t *srcEnd = srcBegin + srcLength; + + char *dstBegin = dst; + char *dstEnd = dstBegin + dstLength; + + std::mbstate_t st; + const wchar_t *source; + char *target; + memset(&st, 0, sizeof(st)); + std::codecvt_base::result result = utf8_utf16_t().out( + st, srcBegin, srcEnd, source, dstBegin, dstEnd, target); + + if(result != utf8_utf16_t::ok) { + debug("String::copyFromUTF8() - Unicode conversion error."); + } + +#else + + using namespace Unicode; + using namespace TagLib; + + const Unicode::UTF16 *srcBegin = src; + const Unicode::UTF16 *srcEnd = srcBegin + srcLength; + + Unicode::UTF8 *dstBegin = reinterpret_cast<Unicode::UTF8*>(dst); + Unicode::UTF8 *dstEnd = dstBegin + dstLength; + + Unicode::ConversionResult result = Unicode::ConvertUTF16toUTF8( + &srcBegin, srcEnd, &dstBegin, dstEnd, Unicode::lenientConversion); + + if(result != Unicode::conversionOK) { + debug("String::to8Bit() - Unicode conversion error."); + } + +#endif + } + + void UTF8toUTF16(const char *src, size_t srcLength, wchar_t *dst, size_t dstLength) + { +#ifdef HAVE_STD_CODECVT + + typedef std::codecvt_utf8_utf16<wchar_t> utf8_utf16_t; + + using namespace TagLib; + + const char *srcBegin = src; + const char *srcEnd = srcBegin + srcLength; + + wchar_t *dstBegin = dst; + wchar_t *dstEnd = dstBegin + dstLength; + + std::mbstate_t st; + const char *source; + wchar_t *target; + memset(&st, 0, sizeof(st)); + std::codecvt_base::result result = utf8_utf16_t().in( + st, srcBegin, srcEnd, source, dstBegin, dstEnd, target); + + if(result != utf8_utf16_t::ok) { + debug("String::copyFromUTF8() - Unicode conversion error."); + } + +#else + + using namespace Unicode; + using namespace TagLib; + + const Unicode::UTF8 *srcBegin = reinterpret_cast<const Unicode::UTF8*>(src); + const Unicode::UTF8 *srcEnd = srcBegin + srcLength; + + Unicode::UTF16 *dstBegin = dst; + Unicode::UTF16 *dstEnd = dstBegin + dstLength; + + Unicode::ConversionResult result = Unicode::ConvertUTF8toUTF16( + &srcBegin, srcEnd, &dstBegin, dstEnd, Unicode::lenientConversion); + + if(result != Unicode::conversionOK) { + debug("String::copyFromUTF8() - Unicode conversion error."); + } + +#endif + } +} + +namespace TagLib { + +class String::StringPrivate : public RefCounter +{ +public: + StringPrivate() + : RefCounter() + { + } + + StringPrivate(const wstring &s) + : RefCounter() + , data(s) + { + } + + StringPrivate(uint n, wchar_t c) + : RefCounter() + , data(static_cast<size_t>(n), c) + { + } + + /*! + * Stores string in UTF-16. The byte order depends on the CPU endian. + */ + TagLib::wstring data; + + /*! + * This is only used to hold the the most recent value of toCString(). + */ + std::string cstring; +}; + +String String::null; + +//////////////////////////////////////////////////////////////////////////////// + +String::String() + : d(new StringPrivate()) +{ +} + +String::String(const String &s) + : d(s.d) +{ + d->ref(); +} + +String::String(const std::string &s, Type t) + : d(new StringPrivate()) +{ + if(t == Latin1) + copyFromLatin1(&s[0], s.length()); + else if(t == String::UTF8) + copyFromUTF8(&s[0], s.length()); + else { + debug("String::String() -- A std::string should not contain UTF16."); + } +} + +String::String(const wstring &s, Type t) + : d(new StringPrivate()) +{ + if(t == UTF16 || t == UTF16BE || t == UTF16LE) + copyFromUTF16(s.c_str(), s.length(), t); + else { + debug("String::String() -- A TagLib::wstring should not contain Latin1 or UTF-8."); + } +} + +String::String(const wchar_t *s, Type t) + : d(new StringPrivate()) +{ + if(t == UTF16 || t == UTF16BE || t == UTF16LE) + copyFromUTF16(s, ::wcslen(s), t); + else { + debug("String::String() -- A const wchar_t * should not contain Latin1 or UTF-8."); + } +} + +String::String(const char *s, Type t) + : d(new StringPrivate()) +{ + if(t == Latin1) + copyFromLatin1(s, ::strlen(s)); + else if(t == String::UTF8) + copyFromUTF8(s, ::strlen(s)); + else { + debug("String::String() -- A const char * should not contain UTF16."); + } +} + +String::String(wchar_t c, Type t) + : d(new StringPrivate()) +{ + if(t == UTF16 || t == UTF16BE || t == UTF16LE) + copyFromUTF16(&c, 1, t); + else { + debug("String::String() -- A const wchar_t should not contain Latin1 or UTF-8."); + } +} + +String::String(char c, Type t) + : d(new StringPrivate(1, static_cast<uchar>(c))) +{ + if(t != Latin1 && t != UTF8) { + debug("String::String() -- A char should not contain UTF16."); + } +} + +String::String(const ByteVector &v, Type t) + : d(new StringPrivate()) +{ + if(v.isEmpty()) + return; + + if(t == Latin1) + copyFromLatin1(v.data(), v.size()); + else if(t == UTF8) + copyFromUTF8(v.data(), v.size()); + else + copyFromUTF16(v.data(), v.size(), t); + + // If we hit a null in the ByteVector, shrink the string again. + d->data.resize(::wcslen(d->data.c_str())); +} + +//////////////////////////////////////////////////////////////////////////////// + +String::~String() +{ + if(d->deref()) + delete d; +} + +std::string String::to8Bit(bool unicode) const +{ + std::string s; + + if(!unicode) { + s.resize(d->data.size()); + + std::string::iterator targetIt = s.begin(); + for(wstring::const_iterator it = d->data.begin(); it != d->data.end(); it++) { + *targetIt = static_cast<char>(*it); + ++targetIt; + } + } + else { + s.resize(d->data.size() * 4 + 1); + + UTF16toUTF8(&d->data[0], d->data.size(), &s[0], s.size()); + s.resize(::strlen(s.c_str())); + } + + return s; +} + +TagLib::wstring String::toWString() const +{ + return d->data; +} + +const char *String::toCString(bool unicode) const +{ + d->cstring = to8Bit(unicode); + return d->cstring.c_str(); +} + +const wchar_t *String::toCWString() const +{ + return d->data.c_str(); +} + +String::Iterator String::begin() +{ + return d->data.begin(); +} + +String::ConstIterator String::begin() const +{ + return d->data.begin(); +} + +String::Iterator String::end() +{ + return d->data.end(); +} + +String::ConstIterator String::end() const +{ + return d->data.end(); +} + +int String::find(const String &s, int offset) const +{ + return d->data.find(s.d->data, offset); +} + +int String::rfind(const String &s, int offset) const +{ + return d->data.rfind(s.d->data, offset); +} + +StringList String::split(const String &separator) const +{ + StringList list; + for(int index = 0;;) + { + int sep = find(separator, index); + if(sep < 0) + { + list.append(substr(index, size() - index)); + break; + } + else + { + list.append(substr(index, sep - index)); + index = sep + separator.size(); + } + } + return list; +} + +bool String::startsWith(const String &s) const +{ + if(s.length() > length()) + return false; + + return substr(0, s.length()) == s; +} + +String String::substr(uint position, uint n) const +{ + return String(d->data.substr(position, n)); +} + +String &String::append(const String &s) +{ + detach(); + d->data += s.d->data; + return *this; +} + +String String::upper() const +{ + String s; + + static int shift = 'A' - 'a'; + + for(wstring::const_iterator it = d->data.begin(); it != d->data.end(); ++it) { + if(*it >= 'a' && *it <= 'z') + s.d->data.push_back(*it + shift); + else + s.d->data.push_back(*it); + } + + return s; +} + +TagLib::uint String::size() const +{ + return d->data.size(); +} + +TagLib::uint String::length() const +{ + return size(); +} + +bool String::isEmpty() const +{ + return d->data.size() == 0; +} + +bool String::isNull() const +{ + return d == null.d; +} + +ByteVector String::data(Type t) const +{ + switch(t) + { + case Latin1: + { + ByteVector v(size(), 0); + char *p = v.data(); + + for(wstring::const_iterator it = d->data.begin(); it != d->data.end(); it++) + *p++ = static_cast<char>(*it); + + return v; + } + case UTF8: + { + ByteVector v(size() * 4 + 1, 0); + + UTF16toUTF8(&d->data[0], d->data.size(), v.data(), v.size()); + v.resize(::strlen(v.data())); + + return v; + } + case UTF16: + { + ByteVector v(2 + size() * 2, 0); + char *p = v.data(); + + // Assume that if we're doing UTF16 and not UTF16BE that we want little + // endian encoding. (Byte Order Mark) + + *p++ = '\xff'; + *p++ = '\xfe'; + + for(wstring::const_iterator it = d->data.begin(); it != d->data.end(); it++) { + *p++ = static_cast<char>(*it & 0xff); + *p++ = static_cast<char>(*it >> 8); + } + + return v; + } + case UTF16BE: + { + ByteVector v(size() * 2, 0); + char *p = v.data(); + + for(wstring::const_iterator it = d->data.begin(); it != d->data.end(); it++) { + *p++ = static_cast<char>(*it >> 8); + *p++ = static_cast<char>(*it & 0xff); + } + + return v; + } + case UTF16LE: + { + ByteVector v(size() * 2, 0); + char *p = v.data(); + + for(wstring::const_iterator it = d->data.begin(); it != d->data.end(); it++) { + *p++ = static_cast<char>(*it & 0xff); + *p++ = static_cast<char>(*it >> 8); + } + + return v; + } + default: + { + debug("String::data() - Invalid Type value."); + return ByteVector(); + } + } +} + +int String::toInt() const +{ + return toInt(0); +} + +int String::toInt(bool *ok) const +{ + int value = 0; + + uint size = d->data.size(); + bool negative = size > 0 && d->data[0] == '-'; + uint start = negative ? 1 : 0; + uint i = start; + + for(; i < size && d->data[i] >= '0' && d->data[i] <= '9'; i++) + value = value * 10 + (d->data[i] - '0'); + + if(negative) + value = value * -1; + + if(ok) + *ok = (size > start && i == size); + + return value; +} + +String String::stripWhiteSpace() const +{ + wstring::const_iterator begin = d->data.begin(); + wstring::const_iterator end = d->data.end(); + + while(begin != end && + (*begin == '\t' || *begin == '\n' || *begin == '\f' || + *begin == '\r' || *begin == ' ')) + { + ++begin; + } + + if(begin == end) + return null; + + // There must be at least one non-whitespace character here for us to have + // gotten this far, so we should be safe not doing bounds checking. + + do { + --end; + } while(*end == '\t' || *end == '\n' || + *end == '\f' || *end == '\r' || *end == ' '); + + return String(wstring(begin, end + 1)); +} + +bool String::isLatin1() const +{ + for(wstring::const_iterator it = d->data.begin(); it != d->data.end(); it++) { + if(*it >= 256) + return false; + } + return true; +} + +bool String::isAscii() const +{ + for(wstring::const_iterator it = d->data.begin(); it != d->data.end(); it++) { + if(*it >= 128) + return false; + } + return true; +} + +String String::number(int n) // static +{ + static const size_t BufferSize = 11; // Sufficient to store "-214748364". + static const char *Format = "%d"; + + char buffer[BufferSize]; + int length; + +#if defined(HAVE_SNPRINTF) + + length = snprintf(buffer, BufferSize, Format, n); + +#elif defined(HAVE_SPRINTF_S) + + length = sprintf_s(buffer, Format, n); + +#else + + length = sprintf(buffer, Format, n); + +#endif + + if(length > 0) + return String(buffer); + else + return String::null; +} + +TagLib::wchar &String::operator[](int i) +{ + detach(); + return d->data[i]; +} + +const TagLib::wchar &String::operator[](int i) const +{ + return d->data[i]; +} + +bool String::operator==(const String &s) const +{ + return d == s.d || d->data == s.d->data; +} + +bool String::operator!=(const String &s) const +{ + return !operator==(s); +} + +String &String::operator+=(const String &s) +{ + detach(); + + d->data += s.d->data; + return *this; +} + +String &String::operator+=(const wchar_t *s) +{ + detach(); + + d->data += s; + return *this; +} + +String &String::operator+=(const char *s) +{ + detach(); + + for(int i = 0; s[i] != 0; i++) + d->data += uchar(s[i]); + return *this; +} + +String &String::operator+=(wchar_t c) +{ + detach(); + + d->data += c; + return *this; +} + +String &String::operator+=(char c) +{ + detach(); + + d->data += uchar(c); + return *this; +} + +String &String::operator=(const String &s) +{ + if(&s == this) + return *this; + + if(d->deref()) + delete d; + d = s.d; + d->ref(); + return *this; +} + +String &String::operator=(const std::string &s) +{ + if(d->deref()) + delete d; + + d = new StringPrivate; + copyFromLatin1(s.c_str(), s.length()); + + return *this; +} + +String &String::operator=(const wstring &s) +{ + if(d->deref()) + delete d; + d = new StringPrivate(s); + return *this; +} + +String &String::operator=(const wchar_t *s) +{ + if(d->deref()) + delete d; + + d = new StringPrivate(s); + return *this; +} + +String &String::operator=(char c) +{ + if(d->deref()) + delete d; + + d = new StringPrivate(1, static_cast<uchar>(c)); + return *this; +} + +String &String::operator=(wchar_t c) +{ + if(d->deref()) + delete d; + + d = new StringPrivate(1, c); + return *this; +} + +String &String::operator=(const char *s) +{ + if(d->deref()) + delete d; + + d = new StringPrivate; + copyFromLatin1(s, ::strlen(s)); + + return *this; +} + +String &String::operator=(const ByteVector &v) +{ + if(d->deref()) + delete d; + + d = new StringPrivate; + copyFromLatin1(v.data(), v.size()); + + // If we hit a null in the ByteVector, shrink the string again. + d->data.resize(::wcslen(d->data.c_str())); + + return *this; +} + +bool String::operator<(const String &s) const +{ + return d->data < s.d->data; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void String::detach() +{ + if(d->count() > 1) { + d->deref(); + d = new StringPrivate(d->data); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void String::copyFromLatin1(const char *s, size_t length) +{ + d->data.resize(length); + + for(size_t i = 0; i < length; ++i) + d->data[i] = static_cast<uchar>(s[i]); +} + +void String::copyFromUTF8(const char *s, size_t length) +{ + d->data.resize(length); + + UTF8toUTF16(s, length, &d->data[0], d->data.size()); + d->data.resize(::wcslen(d->data.c_str())); +} + +void String::copyFromUTF16(const wchar_t *s, size_t length, Type t) +{ + bool swap; + if(t == UTF16) { + if(length >= 1 && s[0] == 0xfeff) + swap = false; // Same as CPU endian. No need to swap bytes. + else if(length >= 1 && s[0] == 0xfffe) + swap = true; // Not same as CPU endian. Need to swap bytes. + else { + debug("String::copyFromUTF16() - Invalid UTF16 string."); + return; + } + + s++; + length--; + } + else + swap = (t != WCharByteOrder); + + d->data.resize(length); + memcpy(&d->data[0], s, length * sizeof(wchar_t)); + + if(swap) { + for(size_t i = 0; i < length; ++i) + d->data[i] = Utils::byteSwap(static_cast<ushort>(s[i])); + } +} + +void String::copyFromUTF16(const char *s, size_t length, Type t) +{ + bool swap; + if(t == UTF16) { + if(length < 2) { + debug("String::copyFromUTF16() - Invalid UTF16 string."); + return; + } + + // Uses memcpy instead of reinterpret_cast to avoid an alignment exception. + ushort bom; + ::memcpy(&bom, s, 2); + + if(bom == 0xfeff) + swap = false; // Same as CPU endian. No need to swap bytes. + else if(bom == 0xfffe) + swap = true; // Not same as CPU endian. Need to swap bytes. + else { + debug("String::copyFromUTF16() - Invalid UTF16 string."); + return; + } + + s += 2; + length -= 2; + } + else + swap = (t != WCharByteOrder); + + d->data.resize(length / 2); + for(size_t i = 0; i < length / 2; ++i) { + d->data[i] = swap ? combine(*s, *(s + 1)) : combine(*(s + 1), *s); + s += 2; + } +} + +const String::Type String::WCharByteOrder + = (Utils::SystemByteOrder == Utils::BigEndian) ? String::UTF16BE : String::UTF16LE; + +} + +//////////////////////////////////////////////////////////////////////////////// +// related functions +//////////////////////////////////////////////////////////////////////////////// + +const TagLib::String operator+(const TagLib::String &s1, const TagLib::String &s2) +{ + TagLib::String s(s1); + s.append(s2); + return s; +} + +const TagLib::String operator+(const char *s1, const TagLib::String &s2) +{ + TagLib::String s(s1); + s.append(s2); + return s; +} + +const TagLib::String operator+(const TagLib::String &s1, const char *s2) +{ + TagLib::String s(s1); + s.append(s2); + return s; +} + +std::ostream &operator<<(std::ostream &s, const TagLib::String &str) +{ + s << str.to8Bit(); + return s; +} + diff --git a/taglib/taglib/toolkit/tstring.h b/taglib/taglib/toolkit/tstring.h new file mode 100644 index 0000000..57945be --- /dev/null +++ b/taglib/taglib/toolkit/tstring.h @@ -0,0 +1,543 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_STRING_H +#define TAGLIB_STRING_H + +#include "taglib_export.h" +#include "taglib.h" +#include "tbytevector.h" + +#include <string> +#include <iostream> + +/*! + * \relates TagLib::String + * + * Converts a QString to a TagLib::String without a requirement to link to Qt. + * + * \note consider conversion via usual char-by-char for loop to avoid UTF16->UTF8->UTF16 + * conversion happening in the background + */ +#define QStringToTString(s) TagLib::String(s.utf8().data(), TagLib::String::UTF8) + +/*! + * \relates TagLib::String + * + * Converts a TagLib::String to a QString without a requirement to link to Qt. + * + * \note consider conversion via usual char-by-char for loop to avoid UTF16->UTF8->UTF16 + * conversion happening in the background + * + */ +#define TStringToQString(s) QString::fromUtf8(s.toCString(true)) + +namespace TagLib { + + class StringList; + + //! A \e wide string class suitable for unicode. + + /*! + * This is an implicitly shared \e wide string. For storage it uses + * TagLib::wstring, but as this is an <i>implementation detail</i> this of + * course could change. Strings are stored internally as UTF-16(without BOM/ + * CPU byte order) + * + * The use of implicit sharing means that copying a string is cheap, the only + * \e cost comes into play when the copy is modified. Prior to that the string + * just has a pointer to the data of the \e parent String. This also makes + * this class suitable as a function return type. + * + * In addition to adding implicit sharing, this class keeps track of four + * possible encodings, which are the four supported by the ID3v2 standard. + */ + + class TAGLIB_EXPORT String + { + public: + +#ifndef DO_NOT_DOCUMENT + typedef std::basic_string<wchar>::iterator Iterator; + typedef std::basic_string<wchar>::const_iterator ConstIterator; +#endif + + /** + * The four types of string encodings supported by the ID3v2 specification. + * ID3v1 is assumed to be Latin1 and Ogg Vorbis comments use UTF8. + */ + enum Type { + /*! + * IS08859-1, or <i>Latin1</i> encoding. 8 bit characters. + */ + Latin1 = 0, + /*! + * UTF16 with a <i>byte order mark</i>. 16 bit characters. + */ + UTF16 = 1, + /*! + * UTF16 <i>big endian</i>. 16 bit characters. This is the encoding used + * internally by TagLib. + */ + UTF16BE = 2, + /*! + * UTF8 encoding. Characters are usually 8 bits but can be up to 32. + */ + UTF8 = 3, + /*! + * UTF16 <i>little endian</i>. 16 bit characters. + */ + UTF16LE = 4 + }; + + /*! + * Constructs an empty String. + */ + String(); + + /*! + * Make a shallow, implicitly shared, copy of \a s. Because this is + * implicitly shared, this method is lightweight and suitable for + * pass-by-value usage. + */ + String(const String &s); + + /*! + * Makes a deep copy of the data in \a s. + * + * \note This should only be used with the 8-bit codecs Latin1 and UTF8, when + * used with other codecs it will simply print a warning and exit. + */ + String(const std::string &s, Type t = Latin1); + + /*! + * Makes a deep copy of the data in \a s. + */ + String(const wstring &s, Type t = WCharByteOrder); + + /*! + * Makes a deep copy of the data in \a s. + */ + String(const wchar_t *s, Type t = WCharByteOrder); + + /*! + * Makes a deep copy of the data in \a c. + * + * \note This should only be used with the 8-bit codecs Latin1 and UTF8, when + * used with other codecs it will simply print a warning and exit. + */ + String(char c, Type t = Latin1); + + /*! + * Makes a deep copy of the data in \a c. + */ + String(wchar_t c, Type t = Latin1); + + + /*! + * Makes a deep copy of the data in \a s. + * + * \note This should only be used with the 8-bit codecs Latin1 and UTF8, when + * used with other codecs it will simply print a warning and exit. + */ + String(const char *s, Type t = Latin1); + + /*! + * Makes a deep copy of the data in \a s. + * + * \note This should only be used with the 8-bit codecs Latin1 and UTF8, when + * used with other codecs it will simply print a warning and exit. + */ + String(const ByteVector &v, Type t = Latin1); + + /*! + * Destroys this String instance. + */ + virtual ~String(); + + /*! + * Returns a deep copy of this String as an std::string. The returned string + * is encoded in UTF8 if \a unicode is true, otherwise Latin1. + * + * \see toCString() + */ + std::string to8Bit(bool unicode = false) const; + + /*! + * Returns a deep copy of this String as a wstring. The returned string is + * encoded in UTF-16 (without BOM/CPU byte order). + * + * \see toCWString() + */ + wstring toWString() const; + + /*! + * Creates and returns a standard C-style (null-terminated) version of this + * String. The returned string is encoded in UTF8 if \a unicode is true, + * otherwise Latin1. + * + * The returned string is still owned by this String and should not be deleted + * by the user. + * + * The returned pointer remains valid until this String instance is destroyed + * or toCString() is called again. + * + * \warning This however has the side effect that the returned string will remain + * in memory <b>in addition to</b> other memory that is consumed by this + * String instance. So, this method should not be used on large strings or + * where memory is critical. Consider using to8Bit() instead to avoid it. + * + * \see to8Bit() + */ + const char *toCString(bool unicode = false) const; + + /*! + * Returns a standard C-style (null-terminated) wide character version of + * this String. The returned string is encoded in UTF-16 (without BOM/CPU byte + * order). + * + * The returned string is still owned by this String and should not be deleted + * by the user. + * + * The returned pointer remains valid until this String instance is destroyed + * or any other method of this String is called. + * + * /note This returns a pointer to the String's internal data without any + * conversions. + * + * \see toWString() + */ + const wchar_t *toCWString() const; + + /*! + * Returns an iterator pointing to the beginning of the string. + */ + Iterator begin(); + + /*! + * Returns a const iterator pointing to the beginning of the string. + */ + ConstIterator begin() const; + + /*! + * Returns an iterator pointing to the end of the string (the position + * after the last character). + */ + Iterator end(); + + /*! + * Returns a const iterator pointing to the end of the string (the position + * after the last character). + */ + ConstIterator end() const; + + /*! + * Finds the first occurrence of pattern \a s in this string starting from + * \a offset. If the pattern is not found, -1 is returned. + */ + int find(const String &s, int offset = 0) const; + + /*! + * Finds the last occurrence of pattern \a s in this string, searched backwards, + * either from the end of the string or starting from \a offset. If the pattern + * is not found, -1 is returned. + */ + int rfind(const String &s, int offset = -1) const; + + /*! + * Splits the string on each occurrence of \a separator. + */ + StringList split(const String &separator = " ") const; + + /*! + * Returns true if the strings starts with the substring \a s. + */ + bool startsWith(const String &s) const; + + /*! + * Extract a substring from this string starting at \a position and + * continuing for \a n characters. + */ + String substr(uint position, uint n = 0xffffffff) const; + + /*! + * Append \a s to the current string and return a reference to the current + * string. + */ + String &append(const String &s); + + /*! + * Returns an upper case version of the string. + * + * \warning This only works for the characters in US-ASCII, i.e. A-Z. + */ + String upper() const; + + /*! + * Returns the size of the string. + */ + uint size() const; + + /*! + * Returns the length of the string. Equivalent to size(). + */ + uint length() const; + + /*! + * Returns true if the string is empty. + * + * \see isNull() + */ + bool isEmpty() const; + + /*! + * Returns true if this string is null -- i.e. it is a copy of the + * String::null string. + * + * \note A string can be empty and not null. + * \see isEmpty() + */ + bool isNull() const; + + /*! + * Returns a ByteVector containing the string's data. If \a t is Latin1 or + * UTF8, this will return a vector of 8 bit characters, otherwise it will use + * 16 bit characters. + */ + ByteVector data(Type t) const; + + /*! + * Convert the string to an integer. + * + * Returns the integer if the conversion was successful or 0 if the + * string does not represent a number. + */ + // BIC: merge with the method below + int toInt() const; + + /*! + * Convert the string to an integer. + * + * If the conversion was successfull, it sets the value of \a *ok to + * true and returns the integer. Otherwise it sets \a *ok to false + * and the result is undefined. + */ + int toInt(bool *ok) const; + + /*! + * Returns a string with the leading and trailing whitespace stripped. + */ + String stripWhiteSpace() const; + + /*! + * Returns true if the file only uses characters required by Latin1. + */ + bool isLatin1() const; + + /*! + * Returns true if the file only uses characters required by (7-bit) ASCII. + */ + bool isAscii() const; + + /*! + * Converts the base-10 integer \a n to a string. + */ + static String number(int n); + + /*! + * Returns a reference to the character at position \a i. + */ + wchar &operator[](int i); + + /*! + * Returns a const reference to the character at position \a i. + */ + const wchar &operator[](int i) const; + + /*! + * Compares each character of the String with each character of \a s and + * returns true if the strings match. + */ + bool operator==(const String &s) const; + + /*! + * Compares each character of the String with each character of \a s and + * returns false if the strings match. + */ + bool operator!=(const String &s) const; + + /*! + * Appends \a s to the end of the String. + */ + String &operator+=(const String &s); + + /*! + * Appends \a s to the end of the String. + */ + String &operator+=(const wchar_t* s); + + /*! + * Appends \a s to the end of the String. + */ + String &operator+=(const char* s); + + /*! + * Appends \a s to the end of the String. + */ + String &operator+=(wchar_t c); + + /*! + * Appends \a c to the end of the String. + */ + String &operator+=(char c); + + /*! + * Performs a shallow, implicitly shared, copy of \a s, overwriting the + * String's current data. + */ + String &operator=(const String &s); + + /*! + * Performs a deep copy of the data in \a s. + */ + String &operator=(const std::string &s); + + /*! + * Performs a deep copy of the data in \a s. + */ + String &operator=(const wstring &s); + + /*! + * Performs a deep copy of the data in \a s. + */ + String &operator=(const wchar_t *s); + + /*! + * Performs a deep copy of the data in \a s. + */ + String &operator=(char c); + + /*! + * Performs a deep copy of the data in \a s. + */ + String &operator=(wchar_t c); + + /*! + * Performs a deep copy of the data in \a s. + */ + String &operator=(const char *s); + + /*! + * Performs a deep copy of the data in \a v. + */ + String &operator=(const ByteVector &v); + + /*! + * To be able to use this class in a Map, this operator needed to be + * implemented. Returns true if \a s is less than this string in a bytewise + * comparison. + */ + bool operator<(const String &s) const; + + /*! + * A null string provided for convenience. + */ + static String null; + + protected: + /*! + * If this String is being shared via implicit sharing, do a deep copy of the + * data and separate from the shared members. This should be called by all + * non-const subclass members. + */ + void detach(); + + private: + /*! + * Converts a \e Latin-1 string into \e UTF-16(without BOM/CPU byte order) + * and copies it to the internal buffer. + */ + void copyFromLatin1(const char *s, size_t length); + + /*! + * Converts a \e UTF-8 string into \e UTF-16(without BOM/CPU byte order) + * and copies it to the internal buffer. + */ + void copyFromUTF8(const char *s, size_t length); + + /*! + * Converts a \e UTF-16 (with BOM), UTF-16LE or UTF16-BE string into + * \e UTF-16(without BOM/CPU byte order) and copies it to the internal buffer. + */ + void copyFromUTF16(const wchar_t *s, size_t length, Type t); + + /*! + * Converts a \e UTF-16 (with BOM), UTF-16LE or UTF16-BE string into + * \e UTF-16(without BOM/CPU byte order) and copies it to the internal buffer. + */ + void copyFromUTF16(const char *s, size_t length, Type t); + + /*! + * Indicates which byte order of UTF-16 is used to store strings internally. + * + * \note \e String::UTF16BE or \e String::UTF16LE + */ + static const Type WCharByteOrder; + + class StringPrivate; + StringPrivate *d; + }; +} + +/*! + * \relates TagLib::String + * + * Concatenates \a s1 and \a s2 and returns the result as a string. + */ +TAGLIB_EXPORT const TagLib::String operator+(const TagLib::String &s1, const TagLib::String &s2); + +/*! + * \relates TagLib::String + * + * Concatenates \a s1 and \a s2 and returns the result as a string. + */ +TAGLIB_EXPORT const TagLib::String operator+(const char *s1, const TagLib::String &s2); + +/*! + * \relates TagLib::String + * + * Concatenates \a s1 and \a s2 and returns the result as a string. + */ +TAGLIB_EXPORT const TagLib::String operator+(const TagLib::String &s1, const char *s2); + + +/*! + * \relates TagLib::String + * + * Send the string to an output stream. + */ +TAGLIB_EXPORT std::ostream &operator<<(std::ostream &s, const TagLib::String &str); + +#endif diff --git a/taglib/taglib/toolkit/tstringlist.cpp b/taglib/taglib/toolkit/tstringlist.cpp new file mode 100644 index 0000000..655fe6c --- /dev/null +++ b/taglib/taglib/toolkit/tstringlist.cpp @@ -0,0 +1,123 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tstringlist.h" + +using namespace TagLib; + +class StringListPrivate +{ + +}; + +//////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +StringList StringList::split(const String &s, const String &pattern) +{ + StringList l; + + int previousOffset = 0; + for(int offset = s.find(pattern); offset != -1; offset = s.find(pattern, offset + 1)) { + l.append(s.substr(previousOffset, offset - previousOffset)); + previousOffset = offset + 1; + } + + l.append(s.substr(previousOffset, s.size() - previousOffset)); + + return l; +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +StringList::StringList() : List<String>() +{ + +} + +StringList::StringList(const StringList &l) : List<String>(l) +{ + +} + +StringList::StringList(const String &s) : List<String>() +{ + append(s); +} + +StringList::StringList(const ByteVectorList &bl, String::Type t) : List<String>() +{ + ByteVectorList::ConstIterator i = bl.begin(); + for(;i != bl.end(); i++) { + append(String(*i, t)); + } +} + +StringList::~StringList() +{ + +} + +String StringList::toString(const String &separator) const +{ + String s; + + ConstIterator it = begin(); + ConstIterator itEnd = end(); + + while(it != itEnd) { + s += *it; + it++; + if(it != itEnd) + s += separator; + } + + return s; +} + +StringList &StringList::append(const String &s) +{ + List<String>::append(s); + return *this; +} + +StringList &StringList::append(const StringList &l) +{ + List<String>::append(l); + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +// related functions +//////////////////////////////////////////////////////////////////////////////// + +std::ostream &operator<<(std::ostream &s, const StringList &l) +{ + s << l.toString(); + return s; +} diff --git a/taglib/taglib/toolkit/tstringlist.h b/taglib/taglib/toolkit/tstringlist.h new file mode 100644 index 0000000..f94b50f --- /dev/null +++ b/taglib/taglib/toolkit/tstringlist.h @@ -0,0 +1,115 @@ +/*************************************************************************** + copyright : (C) 2002 - 2008 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_STRINGLIST_H +#define TAGLIB_STRINGLIST_H + +#include "tstring.h" +#include "tlist.h" +#include "tbytevectorlist.h" +#include "taglib_export.h" + +#include <iostream> + +namespace TagLib { + + //! A list of strings + + /*! + * This is a spcialization of the List class with some members convention for + * string operations. + */ + + class TAGLIB_EXPORT StringList : public List<String> + { + public: + + /*! + * Constructs an empty StringList. + */ + StringList(); + + /*! + * Make a shallow, implicitly shared, copy of \a l. Because this is + * implicitly shared, this method is lightweight and suitable for + * pass-by-value usage. + */ + StringList(const StringList &l); + + /*! + * Constructs a StringList with \a s as a member. + */ + StringList(const String &s); + + /*! + * Makes a deep copy of the data in \a vl. + * + * \note This should only be used with the 8-bit codecs Latin1 and UTF8, when + * used with other codecs it will simply print a warning and exit. + */ + StringList(const ByteVectorList &vl, String::Type t = String::Latin1); + + /*! + * Destroys this StringList instance. + */ + virtual ~StringList(); + + /*! + * Concatenate the list of strings into one string separated by \a separator. + */ + String toString(const String &separator = " ") const; + + /*! + * Appends \a s to the end of the list and returns a reference to the + * list. + */ + StringList &append(const String &s); + + /*! + * Appends all of the values in \a l to the end of the list and returns a + * reference to the list. + */ + StringList &append(const StringList &l); + + /*! + * Splits the String \a s into several strings at \a pattern. This will not include + * the pattern in the returned strings. + */ + static StringList split(const String &s, const String &pattern); + + private: + class StringListPrivate; + StringListPrivate *d; + }; + +} + +/*! + * \related TagLib::StringList + * Send the StringList to an output stream. + */ +std::ostream &operator<<(std::ostream &s, const TagLib::StringList &l); + +#endif diff --git a/taglib/taglib/toolkit/tutils.h b/taglib/taglib/toolkit/tutils.h new file mode 100644 index 0000000..4a398d2 --- /dev/null +++ b/taglib/taglib/toolkit/tutils.h @@ -0,0 +1,190 @@ +/*************************************************************************** + copyright : (C) 2013 by Tsuda Kageyu + email : tsuda.kageyu@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_TUTILS_H +#define TAGLIB_TUTILS_H + +// THIS FILE IS NOT A PART OF THE TAGLIB API + +#ifndef DO_NOT_DOCUMENT // tell Doxygen not to document this header + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#if defined(HAVE_MSC_BYTESWAP) +# include <stdlib.h> +#elif defined(HAVE_GLIBC_BYTESWAP) +# include <byteswap.h> +#elif defined(HAVE_MAC_BYTESWAP) +# include <libkern/OSByteOrder.h> +#elif defined(HAVE_OPENBSD_BYTESWAP) +# include <sys/endian.h> +#endif + +namespace TagLib +{ + namespace Utils + { + inline ushort byteSwap(ushort x) + { +#if defined(HAVE_GCC_BYTESWAP_16) + + return __builtin_bswap16(x); + +#elif defined(HAVE_MSC_BYTESWAP) + + return _byteswap_ushort(x); + +#elif defined(HAVE_GLIBC_BYTESWAP) + + return __bswap_16(x); + +#elif defined(HAVE_MAC_BYTESWAP) + + return OSSwapInt16(x); + +#elif defined(HAVE_OPENBSD_BYTESWAP) + + return swap16(x); + +#else + + return ((x >> 8) & 0xff) | ((x & 0xff) << 8); + +#endif + } + + inline uint byteSwap(uint x) + { +#if defined(HAVE_GCC_BYTESWAP_32) + + return __builtin_bswap32(x); + +#elif defined(HAVE_MSC_BYTESWAP) + + return _byteswap_ulong(x); + +#elif defined(HAVE_GLIBC_BYTESWAP) + + return __bswap_32(x); + +#elif defined(HAVE_MAC_BYTESWAP) + + return OSSwapInt32(x); + +#elif defined(HAVE_OPENBSD_BYTESWAP) + + return swap32(x); + +#else + + return ((x & 0xff000000u) >> 24) + | ((x & 0x00ff0000u) >> 8) + | ((x & 0x0000ff00u) << 8) + | ((x & 0x000000ffu) << 24); + +#endif + } + + inline ulonglong byteSwap(ulonglong x) + { +#if defined(HAVE_GCC_BYTESWAP_64) + + return __builtin_bswap64(x); + +#elif defined(HAVE_MSC_BYTESWAP) + + return _byteswap_uint64(x); + +#elif defined(HAVE_GLIBC_BYTESWAP) + + return __bswap_64(x); + +#elif defined(HAVE_MAC_BYTESWAP) + + return OSSwapInt64(x); + +#elif defined(HAVE_OPENBSD_BYTESWAP) + + return swap64(x); + +#else + + return ((x & 0xff00000000000000ull) >> 56) + | ((x & 0x00ff000000000000ull) >> 40) + | ((x & 0x0000ff0000000000ull) >> 24) + | ((x & 0x000000ff00000000ull) >> 8) + | ((x & 0x00000000ff000000ull) << 8) + | ((x & 0x0000000000ff0000ull) << 24) + | ((x & 0x000000000000ff00ull) << 40) + | ((x & 0x00000000000000ffull) << 56); + +#endif + } + + enum ByteOrder + { + LittleEndian, + BigEndian + }; + +#ifdef SYSTEM_BYTEORDER + +# if SYSTEM_BYTEORDER == 1 + + const ByteOrder SystemByteOrder = LittleEndian; + +# else + + const ByteOrder SystemByteOrder = BigEndian; + +# endif + +#else + + inline ByteOrder systemByteOrder() + { + union { + int i; + char c; + } u; + + u.i = 1; + if(u.c == 1) + return LittleEndian; + else + return BigEndian; + } + + const ByteOrder SystemByteOrder = systemByteOrder(); + +#endif + } +} + +#endif + +#endif diff --git a/taglib/taglib/toolkit/unicode.cpp b/taglib/taglib/toolkit/unicode.cpp new file mode 100644 index 0000000..1b26977 --- /dev/null +++ b/taglib/taglib/toolkit/unicode.cpp @@ -0,0 +1,303 @@ +/******************************************************************************* + * * + * THIS FILE IS INCLUDED IN TAGLIB, BUT IS NOT COPYRIGHTED BY THE TAGLIB * + * AUTHORS, NOT PART OF THE TAGLIB API AND COULD GO AWAY AT ANY POINT IN TIME. * + * AS SUCH IT SHOULD BE CONSIERED FOR INTERNAL USE ONLY. * + * * + *******************************************************************************/ + +/* + * Copyright 2001 Unicode, Inc. + * + * Disclaimer + * + * This source code is provided as is by Unicode, Inc. No claims are + * made as to fitness for any particular purpose. No warranties of any + * kind are expressed or implied. The recipient agrees to determine + * applicability of information provided. If this file has been + * purchased on magnetic or optical media from Unicode, Inc., the + * sole remedy for any claim will be exchange of defective media + * within 90 days of receipt. + * + * Limitations on Rights to Redistribute This Code + * + * Unicode, Inc. hereby grants the right to freely use the information + * supplied in this file in the creation of products supporting the + * Unicode Standard, and to make copies of this file in any form + * for internal or external distribution as long as this notice + * remains attached. + */ + +/* + * This file has been modified by Scott Wheeler <wheeler@kde.org> to remove + * the UTF32 conversion functions and to place the appropriate functions + * in their own C++ namespace. + */ + +/* --------------------------------------------------------------------- + + Conversions between UTF32, UTF-16, and UTF-8. Source code file. + Author: Mark E. Davis, 1994. + Rev History: Rick McGowan, fixes & updates May 2001. + Sept 2001: fixed const & error conditions per + mods suggested by S. Parent & A. Lillich. + + See the header file "ConvertUTF.h" for complete documentation. + +------------------------------------------------------------------------ */ + + +#include "unicode.h" +#include <stdio.h> + +#define UNI_SUR_HIGH_START (UTF32)0xD800 +#define UNI_SUR_HIGH_END (UTF32)0xDBFF +#define UNI_SUR_LOW_START (UTF32)0xDC00 +#define UNI_SUR_LOW_END (UTF32)0xDFFF +#define false 0 +#define true 1 + +namespace Unicode { + +static const int halfShift = 10; /* used for shifting by 10 bits */ + +static const UTF32 halfBase = 0x0010000UL; +static const UTF32 halfMask = 0x3FFUL; + +/* + * Index into the table below with the first byte of a UTF-8 sequence to + * get the number of trailing bytes that are supposed to follow it. + */ +static const char trailingBytesForUTF8[256] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5 +}; + +/* + * Magic values subtracted from a buffer value during UTF8 conversion. + * This table contains as many values as there might be trailing bytes + * in a UTF-8 sequence. + */ +static const UTF32 offsetsFromUTF8[6] = { 0x00000000UL, 0x00003080UL, 0x000E2080UL, + 0x03C82080UL, 0xFA082080UL, 0x82082080UL }; + +/* + * Once the bits are split out into bytes of UTF-8, this is a mask OR-ed + * into the first byte, depending on how many bytes follow. There are + * as many entries in this table as there are UTF-8 sequence types. + * (I.e., one byte sequence, two byte... six byte sequence.) + */ +static const UTF8 firstByteMark[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; + +/* --------------------------------------------------------------------- */ + +/* The interface converts a whole buffer to avoid function-call overhead. + * Constants have been gathered. Loops & conditionals have been removed as + * much as possible for efficiency, in favor of drop-through switches. + * (See "Note A" at the bottom of the file for equivalent code.) + * If your compiler supports it, the "isLegalUTF8" call can be turned + * into an inline function. + */ + +/* --------------------------------------------------------------------- */ + +ConversionResult ConvertUTF16toUTF8 ( + const UTF16** sourceStart, const UTF16* sourceEnd, + UTF8** targetStart, UTF8* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF16* source = *sourceStart; + UTF8* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch; + unsigned short bytesToWrite = 0; + const UTF32 byteMask = 0xBF; + const UTF32 byteMark = 0x80; + const UTF16* oldSource = source; /* In case we have to back up because of target overflow. */ + ch = *source++; + /* If we have a surrogate pair, convert to UTF32 first. */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_HIGH_END && source < sourceEnd) { + UTF32 ch2 = *source; + if (ch2 >= UNI_SUR_LOW_START && ch2 <= UNI_SUR_LOW_END) { + ch = ((ch - UNI_SUR_HIGH_START) << halfShift) + + (ch2 - UNI_SUR_LOW_START) + halfBase; + ++source; + } else if (flags == strictConversion) { /* it's an unpaired high surrogate */ + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } else if ((flags == strictConversion) && (ch >= UNI_SUR_LOW_START && ch <= UNI_SUR_LOW_END)) { + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + /* Figure out how many bytes the result will require */ + if (ch < (UTF32)0x80) { bytesToWrite = 1; + } else if (ch < (UTF32)0x800) { bytesToWrite = 2; + } else if (ch < (UTF32)0x10000) { bytesToWrite = 3; + } else if (ch < (UTF32)0x200000) { bytesToWrite = 4; + } else { bytesToWrite = 2; + ch = UNI_REPLACEMENT_CHAR; + } + // printf("bytes to write = %i\n", bytesToWrite); + target += bytesToWrite; + if (target > targetEnd) { + source = oldSource; /* Back up source pointer! */ + target -= bytesToWrite; result = targetExhausted; break; + } + switch (bytesToWrite) { /* note: everything falls through. */ + case 4: *--target = (ch | byteMark) & byteMask; ch >>= 6; + case 3: *--target = (ch | byteMark) & byteMask; ch >>= 6; + case 2: *--target = (ch | byteMark) & byteMask; ch >>= 6; + case 1: *--target = (UTF8)(ch | firstByteMark[bytesToWrite]); + } + target += bytesToWrite; + } + *sourceStart = source; + *targetStart = target; + return result; +} + +/* --------------------------------------------------------------------- */ + +/* + * Utility routine to tell whether a sequence of bytes is legal UTF-8. + * This must be called with the length pre-determined by the first byte. + * If not calling this from ConvertUTF8to*, then the length can be set by: + * length = trailingBytesForUTF8[*source]+1; + * and the sequence is illegal right away if there aren't that many bytes + * available. + * If presented with a length > 4, this returns false. The Unicode + * definition of UTF-8 goes up to 4-byte sequences. + */ + +static Boolean isLegalUTF8(const UTF8 *source, int length) { + UTF8 a; + const UTF8 *srcptr = source+length; + switch (length) { + default: return false; + /* Everything else falls through when "true"... */ + case 4: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false; + case 3: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false; + case 2: if ((a = (*--srcptr)) > 0xBF) return false; + switch (*source) { + /* no fall-through in this inner switch */ + case 0xE0: if (a < 0xA0) return false; break; + case 0xF0: if (a < 0x90) return false; break; + case 0xF4: if (a > 0x8F) return false; break; + default: if (a < 0x80) return false; + } + case 1: if (*source >= 0x80 && *source < 0xC2) return false; + if (*source > 0xF4) return false; + } + return true; +} + +/* --------------------------------------------------------------------- */ + +/* + * Exported function to return whether a UTF-8 sequence is legal or not. + * This is not used here; it's just exported. + */ +Boolean isLegalUTF8Sequence(const UTF8 *source, const UTF8 *sourceEnd) { + int length = trailingBytesForUTF8[*source]+1; + if (source+length > sourceEnd) { + return false; + } + return isLegalUTF8(source, length); +} + +/* --------------------------------------------------------------------- */ + +ConversionResult ConvertUTF8toUTF16 ( + const UTF8** sourceStart, const UTF8* sourceEnd, + UTF16** targetStart, UTF16* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF8* source = *sourceStart; + UTF16* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch = 0; + unsigned short extraBytesToRead = trailingBytesForUTF8[*source]; + if (source + extraBytesToRead >= sourceEnd) { + result = sourceExhausted; break; + } + /* Do this check whether lenient or strict */ + if (! isLegalUTF8(source, extraBytesToRead+1)) { + result = sourceIllegal; + break; + } + /* + * The cases all fall through. See "Note A" below. + */ + switch (extraBytesToRead) { + case 3: ch += *source++; ch <<= 6; + case 2: ch += *source++; ch <<= 6; + case 1: ch += *source++; ch <<= 6; + case 0: ch += *source++; + } + ch -= offsetsFromUTF8[extraBytesToRead]; + + if (target >= targetEnd) { + source -= (extraBytesToRead+1); /* Back up source pointer! */ + result = targetExhausted; break; + } + if (ch <= UNI_MAX_BMP) { /* Target is a character <= 0xFFFF */ + if ((flags == strictConversion) && (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END)) { + source -= (extraBytesToRead+1); /* return to the illegal value itself */ + result = sourceIllegal; + break; + } else { + *target++ = (UTF16)ch; /* normal case */ + } + } else if (ch > UNI_MAX_UTF16) { + if (flags == strictConversion) { + result = sourceIllegal; + source -= (extraBytesToRead+1); /* return to the start */ + break; /* Bail out; shouldn't continue */ + } else { + *target++ = UNI_REPLACEMENT_CHAR; + } + } else { + /* target is a character in range 0xFFFF - 0x10FFFF. */ + if (target + 1 >= targetEnd) { + source -= (extraBytesToRead+1); /* Back up source pointer! */ + result = targetExhausted; break; + } + ch -= halfBase; + *target++ = (UTF16)((ch >> halfShift) + UNI_SUR_HIGH_START); + *target++ = (ch & halfMask) + UNI_SUR_LOW_START; + } + } + *sourceStart = source; + *targetStart = target; + return result; +} + +} + +/* --------------------------------------------------------------------- + + Note A. + The fall-through switches in UTF-8 reading code save a + temp variable, some decrements & conditionals. The switches + are equivalent to the following loop: + { + int tmpBytesToRead = extraBytesToRead+1; + do { + ch += *source++; + --tmpBytesToRead; + if (tmpBytesToRead) ch <<= 6; + } while (tmpBytesToRead > 0); + } + In UTF-8 writing code, the switches on "bytesToWrite" are + similarly unrolled loops. + + --------------------------------------------------------------------- */ + + diff --git a/taglib/taglib/toolkit/unicode.h b/taglib/taglib/toolkit/unicode.h new file mode 100644 index 0000000..ebf1915 --- /dev/null +++ b/taglib/taglib/toolkit/unicode.h @@ -0,0 +1,154 @@ +#ifndef TAGLIB_UNICODE_H +#define TAGLIB_UNICODE_H + +/******************************************************************************* + * * + * THIS FILE IS INCLUDED IN TAGLIB, BUT IS NOT COPYRIGHTED BY THE TAGLIB * + * AUTHORS, NOT PART OF THE TAGLIB API AND COULD GO AWAY AT ANY POINT IN TIME. * + * AS SUCH IT SHOULD BE CONSIERED FOR INTERNAL USE ONLY. * + * * + *******************************************************************************/ + +#ifndef DO_NOT_DOCUMENT // tell Doxygen not to document this header + +/* + * Copyright 2001 Unicode, Inc. + * + * Disclaimer + * + * This source code is provided as is by Unicode, Inc. No claims are + * made as to fitness for any particular purpose. No warranties of any + * kind are expressed or implied. The recipient agrees to determine + * applicability of information provided. If this file has been + * purchased on magnetic or optical media from Unicode, Inc., the + * sole remedy for any claim will be exchange of defective media + * within 90 days of receipt. + * + * Limitations on Rights to Redistribute This Code + * + * Unicode, Inc. hereby grants the right to freely use the information + * supplied in this file in the creation of products supporting the + * Unicode Standard, and to make copies of this file in any form + * for internal or external distribution as long as this notice + * remains attached. + */ + +/* + * This file has been modified by Scott Wheeler <wheeler@kde.org> to remove + * the UTF32 conversion functions and to place the appropriate functions + * in their own C++ namespace. + */ + +/* --------------------------------------------------------------------- + + Conversions between UTF32, UTF-16, and UTF-8. Header file. + + Several functions are included here, forming a complete set of + conversions between the three formats. UTF-7 is not included + here, but is handled in a separate source file. + + Each of these routines takes pointers to input buffers and output + buffers. The input buffers are const. + + Each routine converts the text between *sourceStart and sourceEnd, + putting the result into the buffer between *targetStart and + targetEnd. Note: the end pointers are *after* the last item: e.g. + *(sourceEnd - 1) is the last item. + + The return result indicates whether the conversion was successful, + and if not, whether the problem was in the source or target buffers. + (Only the first encountered problem is indicated.) + + After the conversion, *sourceStart and *targetStart are both + updated to point to the end of last text successfully converted in + the respective buffers. + + Input parameters: + sourceStart - pointer to a pointer to the source buffer. + The contents of this are modified on return so that + it points at the next thing to be converted. + targetStart - similarly, pointer to pointer to the target buffer. + sourceEnd, targetEnd - respectively pointers to the ends of the + two buffers, for overflow checking only. + + These conversion functions take a ConversionFlags argument. When this + flag is set to strict, both irregular sequences and isolated surrogates + will cause an error. When the flag is set to lenient, both irregular + sequences and isolated surrogates are converted. + + Whether the flag is strict or lenient, all illegal sequences will cause + an error return. This includes sequences such as: <F4 90 80 80>, <C0 80>, + or <A0> in UTF-8, and values above 0x10FFFF in UTF-32. Conformant code + must check for illegal sequences. + + When the flag is set to lenient, characters over 0x10FFFF are converted + to the replacement character; otherwise (when the flag is set to strict) + they constitute an error. + + Output parameters: + The value "sourceIllegal" is returned from some routines if the input + sequence is malformed. When "sourceIllegal" is returned, the source + value will point to the illegal value that caused the problem. E.g., + in UTF-8 when a sequence is malformed, it points to the start of the + malformed sequence. + + Author: Mark E. Davis, 1994. + Rev History: Rick McGowan, fixes & updates May 2001. + Fixes & updates, Sept 2001. + +------------------------------------------------------------------------ */ + +/* --------------------------------------------------------------------- + The following 4 definitions are compiler-specific. + The C standard does not guarantee that wchar_t has at least + 16 bits, so wchar_t is no less portable than unsigned short! + All should be unsigned values to avoid sign extension during + bit mask & shift operations. +------------------------------------------------------------------------ */ + +// Workaround for when MSVC doesn't have wchar_t as a built-in type. +#if defined(_MSC_VER) && !defined(_WCHAR_T_DEFINED) +# include <wchar.h> +#endif + +/* Some fundamental constants */ +#define UNI_REPLACEMENT_CHAR (UTF32)0x0000FFFD +#define UNI_MAX_BMP (UTF32)0x0000FFFF +#define UNI_MAX_UTF16 (UTF32)0x0010FFFF +#define UNI_MAX_UTF32 (UTF32)0x7FFFFFFF + +namespace Unicode { + +typedef unsigned long UTF32; /* at least 32 bits */ +typedef wchar_t UTF16; /* TagLib assumes that wchar_t is sufficient for UTF-16. */ +typedef unsigned char UTF8; /* typically 8 bits */ +typedef unsigned char Boolean; /* 0 or 1 */ + +typedef enum { + conversionOK = 0, /* conversion successful */ + sourceExhausted = 1, /* partial character in source, but hit end */ + targetExhausted = 2, /* insuff. room in target for conversion */ + sourceIllegal = 3 /* source sequence is illegal/malformed */ +} ConversionResult; + +typedef enum { + strictConversion = 0, + lenientConversion +} ConversionFlags; + +ConversionResult ConvertUTF8toUTF16 ( + const UTF8** sourceStart, const UTF8* sourceEnd, + UTF16** targetStart, UTF16* targetEnd, ConversionFlags flags); + +ConversionResult ConvertUTF16toUTF8 ( + const UTF16** sourceStart, const UTF16* sourceEnd, + UTF8** targetStart, UTF8* targetEnd, ConversionFlags flags); + +Boolean isLegalUTF8Sequence(const UTF8 *source, const UTF8 *sourceEnd); + +} // namespace Unicode + +/* --------------------------------------------------------------------- */ + +#endif +#endif diff --git a/taglib/taglib/trueaudio/trueaudiofile.cpp b/taglib/taglib/trueaudio/trueaudiofile.cpp new file mode 100644 index 0000000..f98d6ad --- /dev/null +++ b/taglib/taglib/trueaudio/trueaudiofile.cpp @@ -0,0 +1,325 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tstring.h> +#include <tdebug.h> +#include <tagunion.h> +#include <tstringlist.h> +#include <tpropertymap.h> + +#include "trueaudiofile.h" +#include "id3v1tag.h" +#include "id3v2tag.h" +#include "id3v2header.h" + +using namespace TagLib; + +namespace +{ + enum { TrueAudioID3v2Index = 0, TrueAudioID3v1Index = 1 }; +} + +class TrueAudio::File::FilePrivate +{ +public: + FilePrivate(const ID3v2::FrameFactory *frameFactory = ID3v2::FrameFactory::instance()) : + ID3v2FrameFactory(frameFactory), + ID3v2Location(-1), + ID3v2OriginalSize(0), + ID3v1Location(-1), + properties(0), + hasID3v1(false), + hasID3v2(false) {} + + ~FilePrivate() + { + delete properties; + } + + const ID3v2::FrameFactory *ID3v2FrameFactory; + long ID3v2Location; + uint ID3v2OriginalSize; + + long ID3v1Location; + + TagUnion tag; + + Properties *properties; + + // These indicate whether the file *on disk* has these tags, not if + // this data structure does. This is used in computing offsets. + + bool hasID3v1; + bool hasID3v2; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +TrueAudio::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +TrueAudio::File::File(FileName file, ID3v2::FrameFactory *frameFactory, + bool readProperties, Properties::ReadStyle propertiesStyle) : + TagLib::File(file) +{ + d = new FilePrivate(frameFactory); + if(isOpen()) + read(readProperties, propertiesStyle); +} + +TrueAudio::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +TrueAudio::File::File(IOStream *stream, ID3v2::FrameFactory *frameFactory, + bool readProperties, Properties::ReadStyle propertiesStyle) : + TagLib::File(stream) +{ + d = new FilePrivate(frameFactory); + if(isOpen()) + read(readProperties, propertiesStyle); +} + +TrueAudio::File::~File() +{ + delete d; +} + +TagLib::Tag *TrueAudio::File::tag() const +{ + return &d->tag; +} + +PropertyMap TrueAudio::File::properties() const +{ + // once Tag::properties() is virtual, this case distinction could actually be done + // within TagUnion. + if(d->hasID3v2) + return d->tag.access<ID3v2::Tag>(TrueAudioID3v2Index, false)->properties(); + if(d->hasID3v1) + return d->tag.access<ID3v1::Tag>(TrueAudioID3v1Index, false)->properties(); + return PropertyMap(); +} + +void TrueAudio::File::removeUnsupportedProperties(const StringList &unsupported) +{ + if(d->hasID3v2) + d->tag.access<ID3v2::Tag>(TrueAudioID3v2Index, false)->removeUnsupportedProperties(unsupported); +} + +PropertyMap TrueAudio::File::setProperties(const PropertyMap &properties) +{ + if(d->hasID3v1) + d->tag.access<ID3v1::Tag>(TrueAudioID3v1Index, false)->setProperties(properties); + return d->tag.access<ID3v2::Tag>(TrueAudioID3v2Index, true)->setProperties(properties); +} + +TrueAudio::Properties *TrueAudio::File::audioProperties() const +{ + return d->properties; +} + +void TrueAudio::File::setID3v2FrameFactory(const ID3v2::FrameFactory *factory) +{ + d->ID3v2FrameFactory = factory; +} + +bool TrueAudio::File::save() +{ + if(readOnly()) { + debug("TrueAudio::File::save() -- File is read only."); + return false; + } + + // Update ID3v2 tag + + if(ID3v2Tag() && !ID3v2Tag()->isEmpty()) { + if(!d->hasID3v2) { + d->ID3v2Location = 0; + d->ID3v2OriginalSize = 0; + } + ByteVector data = ID3v2Tag()->render(); + insert(data, d->ID3v2Location, d->ID3v2OriginalSize); + d->ID3v1Location -= d->ID3v2OriginalSize - data.size(); + d->ID3v2OriginalSize = data.size(); + d->hasID3v2 = true; + } + else if(d->hasID3v2) { + removeBlock(d->ID3v2Location, d->ID3v2OriginalSize); + d->ID3v1Location -= d->ID3v2OriginalSize; + d->ID3v2Location = -1; + d->ID3v2OriginalSize = 0; + d->hasID3v2 = false; + } + + // Update ID3v1 tag + + if(ID3v1Tag() && !ID3v1Tag()->isEmpty()) { + if(!d->hasID3v1) { + seek(0, End); + d->ID3v1Location = tell(); + } + else + seek(d->ID3v1Location); + writeBlock(ID3v1Tag()->render()); + d->hasID3v1 = true; + } + else if(d->hasID3v1) { + removeBlock(d->ID3v1Location, 128); + d->ID3v1Location = -1; + d->hasID3v1 = false; + } + + return true; +} + +ID3v1::Tag *TrueAudio::File::ID3v1Tag(bool create) +{ + return d->tag.access<ID3v1::Tag>(TrueAudioID3v1Index, create); +} + +ID3v2::Tag *TrueAudio::File::ID3v2Tag(bool create) +{ + return d->tag.access<ID3v2::Tag>(TrueAudioID3v2Index, create); +} + +void TrueAudio::File::strip(int tags) +{ + if(tags & ID3v1) { + d->tag.set(TrueAudioID3v1Index, 0); + ID3v2Tag(true); + } + + if(tags & ID3v2) { + d->tag.set(TrueAudioID3v2Index, 0); + + if(!ID3v1Tag()) + ID3v2Tag(true); + } +} + +bool TrueAudio::File::hasID3v1Tag() const +{ + return d->hasID3v1; +} + +bool TrueAudio::File::hasID3v2Tag() const +{ + return d->hasID3v2; +} + + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void TrueAudio::File::read(bool readProperties, Properties::ReadStyle /* propertiesStyle */) +{ + // Look for an ID3v2 tag + + d->ID3v2Location = findID3v2(); + + if(d->ID3v2Location >= 0) { + + d->tag.set(TrueAudioID3v2Index, new ID3v2::Tag(this, d->ID3v2Location, d->ID3v2FrameFactory)); + + d->ID3v2OriginalSize = ID3v2Tag()->header()->completeTagSize(); + + if(ID3v2Tag()->header()->tagSize() <= 0) + d->tag.set(TrueAudioID3v2Index, 0); + else + d->hasID3v2 = true; + } + + // Look for an ID3v1 tag + + d->ID3v1Location = findID3v1(); + + if(d->ID3v1Location >= 0) { + d->tag.set(TrueAudioID3v1Index, new ID3v1::Tag(this, d->ID3v1Location)); + d->hasID3v1 = true; + } + + if(!d->hasID3v1) + ID3v2Tag(true); + + // Look for TrueAudio metadata + + if(readProperties) { + if(d->ID3v2Location >= 0) { + seek(d->ID3v2Location + d->ID3v2OriginalSize); + d->properties = new Properties(readBlock(TrueAudio::HeaderSize), + length() - d->ID3v2OriginalSize); + } + else { + seek(0); + d->properties = new Properties(readBlock(TrueAudio::HeaderSize), + length()); + } + } +} + +long TrueAudio::File::findID3v1() +{ + if(!isValid()) + return -1; + + seek(-128, End); + long p = tell(); + + if(readBlock(3) == ID3v1::Tag::fileIdentifier()) + return p; + + return -1; +} + +long TrueAudio::File::findID3v2() +{ + if(!isValid()) + return -1; + + seek(0); + + if(readBlock(3) == ID3v2::Header::fileIdentifier()) + return 0; + + return -1; +} diff --git a/taglib/taglib/trueaudio/trueaudiofile.h b/taglib/taglib/trueaudio/trueaudiofile.h new file mode 100644 index 0000000..dbaafcd --- /dev/null +++ b/taglib/taglib/trueaudio/trueaudiofile.h @@ -0,0 +1,252 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_TRUEAUDIOFILE_H +#define TAGLIB_TRUEAUDIOFILE_H + +#include "tfile.h" +#include "trueaudioproperties.h" + +namespace TagLib { + + class Tag; + + namespace ID3v2 { class Tag; class FrameFactory; } + namespace ID3v1 { class Tag; } + + //! An implementation of TrueAudio metadata + + /*! + * This is implementation of TrueAudio metadata. + * + * This supports ID3v1 and ID3v2 tags as well as reading stream + * properties from the file. + */ + + namespace TrueAudio { + + //! An implementation of TagLib::File with TrueAudio specific methods + + /*! + * This implements and provides an interface for TrueAudio files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to TrueAudio files. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * This set of flags is used for various operations and is suitable for + * being OR-ed together. + */ + enum TagTypes { + //! Empty set. Matches no tag types. + NoTags = 0x0000, + //! Matches ID3v1 tags. + ID3v1 = 0x0001, + //! Matches ID3v2 tags. + ID3v2 = 0x0002, + //! Matches all tag types. + AllTags = 0xffff + }; + + /*! + * Constructs a TrueAudio file from \a file. If \a readProperties is true + * the file's audio properties will also be read. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs a TrueAudio file from \a file. If \a readProperties is true + * the file's audio properties will also be read. + * + * If this file contains and ID3v2 tag the frames will be created using + * \a frameFactory. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(FileName file, ID3v2::FrameFactory *frameFactory, + bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs a TrueAudio file from \a stream. If \a readProperties is true + * the file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs a TrueAudio file from \a stream. If \a readProperties is true + * the file's audio properties will also be read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + * + * If this file contains and ID3v2 tag the frames will be created using + * \a frameFactory. + * + * \note In the current implementation, \a propertiesStyle is ignored. + */ + File(IOStream *stream, ID3v2::FrameFactory *frameFactory, + bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the Tag for this file. + */ + virtual TagLib::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * If the file contains both ID3v1 and v2 tags, only ID3v2 will be + * converted to the PropertyMap. + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Creates in ID3v2 tag if necessary. If an ID3v1 tag exists, it will + * be updated as well, within the limitations of ID3v1. + */ + PropertyMap setProperties(const PropertyMap &); + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Returns the TrueAudio::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + /*! + * Set the ID3v2::FrameFactory to something other than the default. + * + * \see ID3v2FrameFactory + */ + void setID3v2FrameFactory(const ID3v2::FrameFactory *factory); + + /*! + * Saves the file. + */ + virtual bool save(); + + /*! + * Returns a pointer to the ID3v1 tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid ID3v1 tag. If \a create is true it will create + * an ID3v1 tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v1 tag. Use hasID3v1Tag() to check if the file + * on disk actually has an ID3v1 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v1Tag() + */ + ID3v1::Tag *ID3v1Tag(bool create = false); + + /*! + * Returns a pointer to the ID3v2 tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid ID3v2 tag. If \a create is true it will create + * an ID3v2 tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v2 tag. Use hasID3v2Tag() to check if the file + * on disk actually has an ID3v2 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v2Tag() + */ + ID3v2::Tag *ID3v2Tag(bool create = false); + + /*! + * This will remove the tags that match the OR-ed together TagTypes from the + * file. By default it removes all tags. + * + * \note This will also invalidate pointers to the tags + * as their memory will be freed. + * \note In order to make the removal permanent save() still needs to be called + */ + void strip(int tags = AllTags); + + /*! + * Returns whether or not the file on disk actually has an ID3v1 tag. + * + * \see ID3v1Tag() + */ + bool hasID3v1Tag() const; + + /*! + * Returns whether or not the file on disk actually has an ID3v2 tag. + * + * \see ID3v2Tag() + */ + bool hasID3v2Tag() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + void scan(); + long findID3v1(); + long findID3v2(); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/trueaudio/trueaudioproperties.cpp b/taglib/taglib/trueaudio/trueaudioproperties.cpp new file mode 100644 index 0000000..dedd74e --- /dev/null +++ b/taglib/taglib/trueaudio/trueaudioproperties.cpp @@ -0,0 +1,150 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> +#include <bitset> + +#include "trueaudioproperties.h" +#include "trueaudiofile.h" + +using namespace TagLib; + +class TrueAudio::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(const ByteVector &d, long length, ReadStyle s) : + data(d), + streamLength(length), + style(s), + version(0), + length(0), + bitrate(0), + sampleRate(0), + channels(0), + bitsPerSample(0), + sampleFrames(0) {} + + ByteVector data; + long streamLength; + ReadStyle style; + int version; + int length; + int bitrate; + int sampleRate; + int channels; + int bitsPerSample; + uint sampleFrames; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +TrueAudio::Properties::Properties(const ByteVector &data, long streamLength, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(data, streamLength, style); + read(); +} + +TrueAudio::Properties::~Properties() +{ + delete d; +} + +int TrueAudio::Properties::length() const +{ + return d->length; +} + +int TrueAudio::Properties::bitrate() const +{ + return d->bitrate; +} + +int TrueAudio::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int TrueAudio::Properties::bitsPerSample() const +{ + return d->bitsPerSample; +} + +int TrueAudio::Properties::channels() const +{ + return d->channels; +} + +TagLib::uint TrueAudio::Properties::sampleFrames() const +{ + return d->sampleFrames; +} + +int TrueAudio::Properties::ttaVersion() const +{ + return d->version; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void TrueAudio::Properties::read() +{ + if(!d->data.startsWith("TTA")) + return; + + int pos = 3; + + d->version = d->data[pos] - '0'; + pos += 1; + + // According to http://en.true-audio.com/TTA_Lossless_Audio_Codec_-_Format_Description + // TTA2 headers are in development, and have a different format + if(1 == d->version) { + // Skip the audio format + pos += 2; + + d->channels = d->data.toShort(pos, false); + pos += 2; + + d->bitsPerSample = d->data.toShort(pos, false); + pos += 2; + + d->sampleRate = d->data.toUInt(pos, false); + pos += 4; + + d->sampleFrames = d->data.toUInt(pos, false); + d->length = d->sampleRate > 0 ? d->sampleFrames / d->sampleRate : 0; + + d->bitrate = d->length > 0 ? ((d->streamLength * 8L) / d->length) / 1000 : 0; + } +} diff --git a/taglib/taglib/trueaudio/trueaudioproperties.h b/taglib/taglib/trueaudio/trueaudioproperties.h new file mode 100644 index 0000000..126b678 --- /dev/null +++ b/taglib/taglib/trueaudio/trueaudioproperties.h @@ -0,0 +1,98 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_TRUEAUDIOPROPERTIES_H +#define TAGLIB_TRUEAUDIOPROPERTIES_H + +#include "audioproperties.h" + +namespace TagLib { + + namespace TrueAudio { + + class File; + + static const uint HeaderSize = 18; + + //! An implementation of audio property reading for TrueAudio + + /*! + * This reads the data from an TrueAudio stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of TrueAudio::Properties with the data read from the + * ByteVector \a data. + */ + Properties(const ByteVector &data, long streamLength, ReadStyle style = Average); + + /*! + * Destroys this TrueAudio::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + virtual int sampleRate() const; + virtual int channels() const; + + /*! + * Returns number of bits per sample. + */ + int bitsPerSample() const; + + /*! + * Returns the total number of sample frames + */ + uint sampleFrames() const; + + /*! + * Returns the major version number. + */ + int ttaVersion() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/wavpack/wavpackfile.cpp b/taglib/taglib/wavpack/wavpackfile.cpp new file mode 100644 index 0000000..13b8c57 --- /dev/null +++ b/taglib/taglib/wavpack/wavpackfile.cpp @@ -0,0 +1,311 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tbytevector.h> +#include <tstring.h> +#include <tdebug.h> +#include <tagunion.h> +#include <tpropertymap.h> + +#include "wavpackfile.h" +#include "id3v1tag.h" +#include "id3v2header.h" +#include "apetag.h" +#include "apefooter.h" + +using namespace TagLib; + +namespace +{ + enum { WavAPEIndex, WavID3v1Index }; +} + +class WavPack::File::FilePrivate +{ +public: + FilePrivate() : + APELocation(-1), + APESize(0), + ID3v1Location(-1), + properties(0), + hasAPE(false), + hasID3v1(false) {} + + ~FilePrivate() + { + delete properties; + } + + long APELocation; + uint APESize; + + long ID3v1Location; + + TagUnion tag; + + Properties *properties; + + // These indicate whether the file *on disk* has these tags, not if + // this data structure does. This is used in computing offsets. + + bool hasAPE; + bool hasID3v1; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +WavPack::File::File(FileName file, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(file) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +WavPack::File::File(IOStream *stream, bool readProperties, + Properties::ReadStyle propertiesStyle) : TagLib::File(stream) +{ + d = new FilePrivate; + if(isOpen()) + read(readProperties, propertiesStyle); +} + +WavPack::File::~File() +{ + delete d; +} + +TagLib::Tag *WavPack::File::tag() const +{ + return &d->tag; +} + +PropertyMap WavPack::File::properties() const +{ + if(d->hasAPE) + return d->tag.access<APE::Tag>(WavAPEIndex, false)->properties(); + if(d->hasID3v1) + return d->tag.access<ID3v1::Tag>(WavID3v1Index, false)->properties(); + return PropertyMap(); +} + + +void WavPack::File::removeUnsupportedProperties(const StringList &unsupported) +{ + if(d->hasAPE) + d->tag.access<APE::Tag>(WavAPEIndex, false)->removeUnsupportedProperties(unsupported); +} + + +PropertyMap WavPack::File::setProperties(const PropertyMap &properties) +{ + if(d->hasID3v1) + d->tag.access<ID3v1::Tag>(WavID3v1Index, false)->setProperties(properties); + return d->tag.access<APE::Tag>(WavAPEIndex, true)->setProperties(properties); +} + +WavPack::Properties *WavPack::File::audioProperties() const +{ + return d->properties; +} + +bool WavPack::File::save() +{ + if(readOnly()) { + debug("WavPack::File::save() -- File is read only."); + return false; + } + + // Update ID3v1 tag + + if(ID3v1Tag()) { + if(d->hasID3v1) { + seek(d->ID3v1Location); + writeBlock(ID3v1Tag()->render()); + } + else { + seek(0, End); + d->ID3v1Location = tell(); + writeBlock(ID3v1Tag()->render()); + d->hasID3v1 = true; + } + } + else { + if(d->hasID3v1) { + removeBlock(d->ID3v1Location, 128); + d->hasID3v1 = false; + if(d->hasAPE) { + if(d->APELocation > d->ID3v1Location) + d->APELocation -= 128; + } + } + } + + // Update APE tag + + if(APETag()) { + if(d->hasAPE) + insert(APETag()->render(), d->APELocation, d->APESize); + else { + if(d->hasID3v1) { + insert(APETag()->render(), d->ID3v1Location, 0); + d->APESize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + d->APELocation = d->ID3v1Location; + d->ID3v1Location += d->APESize; + } + else { + seek(0, End); + d->APELocation = tell(); + writeBlock(APETag()->render()); + d->APESize = APETag()->footer()->completeTagSize(); + d->hasAPE = true; + } + } + } + else { + if(d->hasAPE) { + removeBlock(d->APELocation, d->APESize); + d->hasAPE = false; + if(d->hasID3v1) { + if(d->ID3v1Location > d->APELocation) { + d->ID3v1Location -= d->APESize; + } + } + } + } + + return true; +} + +ID3v1::Tag *WavPack::File::ID3v1Tag(bool create) +{ + return d->tag.access<ID3v1::Tag>(WavID3v1Index, create); +} + +APE::Tag *WavPack::File::APETag(bool create) +{ + return d->tag.access<APE::Tag>(WavAPEIndex, create); +} + +void WavPack::File::strip(int tags) +{ + if(tags & ID3v1) { + d->tag.set(WavID3v1Index, 0); + APETag(true); + } + + if(tags & APE) { + d->tag.set(WavAPEIndex, 0); + + if(!ID3v1Tag()) + APETag(true); + } +} + +bool WavPack::File::hasID3v1Tag() const +{ + return d->hasID3v1; +} + +bool WavPack::File::hasAPETag() const +{ + return d->hasAPE; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void WavPack::File::read(bool readProperties, Properties::ReadStyle /* propertiesStyle */) +{ + // Look for an ID3v1 tag + + d->ID3v1Location = findID3v1(); + + if(d->ID3v1Location >= 0) { + d->tag.set(WavID3v1Index, new ID3v1::Tag(this, d->ID3v1Location)); + d->hasID3v1 = true; + } + + // Look for an APE tag + + d->APELocation = findAPE(); + + if(d->APELocation >= 0) { + d->tag.set(WavAPEIndex, new APE::Tag(this, d->APELocation)); + d->APESize = APETag()->footer()->completeTagSize(); + d->APELocation = d->APELocation + APETag()->footer()->size() - d->APESize; + d->hasAPE = true; + } + + if(!d->hasID3v1) + APETag(true); + + // Look for WavPack audio properties + + if(readProperties) { + seek(0); + d->properties = new Properties(this, length() - d->APESize); + } +} + +long WavPack::File::findAPE() +{ + if(!isValid()) + return -1; + + if(d->hasID3v1) + seek(-160, End); + else + seek(-32, End); + + long p = tell(); + + if(readBlock(8) == APE::Tag::fileIdentifier()) + return p; + + return -1; +} + +long WavPack::File::findID3v1() +{ + if(!isValid()) + return -1; + + seek(-128, End); + long p = tell(); + + if(readBlock(3) == ID3v1::Tag::fileIdentifier()) + return p; + + return -1; +} diff --git a/taglib/taglib/wavpack/wavpackfile.h b/taglib/taglib/wavpack/wavpackfile.h new file mode 100644 index 0000000..c85c75f --- /dev/null +++ b/taglib/taglib/wavpack/wavpackfile.h @@ -0,0 +1,216 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_WVFILE_H +#define TAGLIB_WVFILE_H + +#include "tfile.h" +#include "taglib_export.h" +#include "wavpackproperties.h" + +namespace TagLib { + + class Tag; + + namespace ID3v1 { class Tag; } + namespace APE { class Tag; } + + //! An implementation of WavPack metadata + + /*! + * This is implementation of WavPack metadata. + * + * This supports ID3v1 and APE (v1 and v2) style comments as well as reading stream + * properties from the file. + */ + + namespace WavPack { + + //! An implementation of TagLib::File with WavPack specific methods + + /*! + * This implements and provides an interface for WavPack files to the + * TagLib::Tag and TagLib::AudioProperties interfaces by way of implementing + * the abstract TagLib::File API as well as providing some additional + * information specific to WavPack files. + */ + + class TAGLIB_EXPORT File : public TagLib::File + { + public: + /*! + * This set of flags is used for various operations and is suitable for + * being OR-ed together. + */ + enum TagTypes { + //! Empty set. Matches no tag types. + NoTags = 0x0000, + //! Matches ID3v1 tags. + ID3v1 = 0x0001, + //! Matches APE tags. + APE = 0x0002, + //! Matches all tag types. + AllTags = 0xffff + }; + + /*! + * Constructs a WavPack file from \a file. If \a readProperties is true the + * file's audio properties will also be read using \a propertiesStyle. If + * false, \a propertiesStyle is ignored + */ + File(FileName file, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Constructs an WavPack file from \a file. If \a readProperties is true the + * file's audio properties will also be read using \a propertiesStyle. If + * false, \a propertiesStyle is ignored. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + */ + File(IOStream *stream, bool readProperties = true, + Properties::ReadStyle propertiesStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + /*! + * Returns the Tag for this file. This will be an APE tag, an ID3v1 tag + * or a combination of the two. + */ + virtual TagLib::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * If the file contains both an APE and an ID3v1 tag, only APE + * will be converted to the PropertyMap. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * Creates an APE tag if it does not exists and calls setProperties() on + * that. Any existing ID3v1 tag will be updated as well. + */ + PropertyMap setProperties(const PropertyMap&); + + /*! + * Returns the MPC::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + virtual Properties *audioProperties() const; + + /*! + * Saves the file. + */ + virtual bool save(); + + /*! + * Returns a pointer to the ID3v1 tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid ID3v1 tag. If \a create is true it will create + * an ID3v1 tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an ID3v1 tag. Use hasID3v1Tag() to check if the file + * on disk actually has an ID3v1 tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasID3v1Tag() + */ + ID3v1::Tag *ID3v1Tag(bool create = false); + + /*! + * Returns a pointer to the APE tag of the file. + * + * If \a create is false (the default) this may return a null pointer + * if there is no valid APE tag. If \a create is true it will create + * an APE tag if one does not exist and returns a valid pointer. + * + * \note This may return a valid pointer regardless of whether or not the + * file on disk has an APE tag. Use hasAPETag() to check if the file + * on disk actually has an APE tag. + * + * \note The Tag <b>is still</b> owned by the MPEG::File and should not be + * deleted by the user. It will be deleted when the file (object) is + * destroyed. + * + * \see hasAPETag() + */ + APE::Tag *APETag(bool create = false); + + /*! + * This will remove the tags that match the OR-ed together TagTypes from the + * file. By default it removes all tags. + * + * \note This will also invalidate pointers to the tags + * as their memory will be freed. + * \note In order to make the removal permanent save() still needs to be called + */ + void strip(int tags = AllTags); + + /*! + * Returns whether or not the file on disk actually has an ID3v1 tag. + * + * \see ID3v1Tag() + */ + bool hasID3v1Tag() const; + + /*! + * Returns whether or not the file on disk actually has an APE tag. + * + * \see APETag() + */ + bool hasAPETag() const; + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties, Properties::ReadStyle propertiesStyle); + void scan(); + long findID3v1(); + long findAPE(); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/wavpack/wavpackproperties.cpp b/taglib/taglib/wavpack/wavpackproperties.cpp new file mode 100644 index 0000000..085ddf8 --- /dev/null +++ b/taglib/taglib/wavpack/wavpackproperties.cpp @@ -0,0 +1,203 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include <tstring.h> +#include <tdebug.h> + +#include "wavpackproperties.h" +#include "wavpackfile.h" + +using namespace TagLib; + +class WavPack::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate(const ByteVector &d, long length, ReadStyle s) : + data(d), + streamLength(length), + style(s), + length(0), + bitrate(0), + sampleRate(0), + channels(0), + version(0), + bitsPerSample(0), + sampleFrames(0), + file(0) {} + + ByteVector data; + long streamLength; + ReadStyle style; + int length; + int bitrate; + int sampleRate; + int channels; + int version; + int bitsPerSample; + uint sampleFrames; + File *file; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +WavPack::Properties::Properties(const ByteVector &data, long streamLength, ReadStyle style) : AudioProperties(style) +{ + d = new PropertiesPrivate(data, streamLength, style); + read(); +} + +WavPack::Properties::Properties(File *file, long streamLength, ReadStyle style) : AudioProperties(style) +{ + ByteVector data = file->readBlock(32); + d = new PropertiesPrivate(data, streamLength, style); + d->file = file; + read(); +} + +WavPack::Properties::~Properties() +{ + delete d; +} + +int WavPack::Properties::length() const +{ + return d->length; +} + +int WavPack::Properties::bitrate() const +{ + return d->bitrate; +} + +int WavPack::Properties::sampleRate() const +{ + return d->sampleRate; +} + +int WavPack::Properties::channels() const +{ + return d->channels; +} + +int WavPack::Properties::version() const +{ + return d->version; +} + +int WavPack::Properties::bitsPerSample() const +{ + return d->bitsPerSample; +} + +TagLib::uint WavPack::Properties::sampleFrames() const +{ + return d->sampleFrames; +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +static const unsigned int sample_rates[] = { + 6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, + 32000, 44100, 48000, 64000, 88200, 96000, 192000, 0 }; + +#define BYTES_STORED 3 +#define MONO_FLAG 4 + +#define SHIFT_LSB 13 +#define SHIFT_MASK (0x1fL << SHIFT_LSB) + +#define SRATE_LSB 23 +#define SRATE_MASK (0xfL << SRATE_LSB) + +#define MIN_STREAM_VERS 0x402 +#define MAX_STREAM_VERS 0x410 + +#define FINAL_BLOCK 0x1000 + +void WavPack::Properties::read() +{ + if(!d->data.startsWith("wvpk")) + return; + + d->version = d->data.toShort(8, false); + if(d->version < MIN_STREAM_VERS || d->version > MAX_STREAM_VERS) + return; + + const unsigned int flags = d->data.toUInt(24, false); + d->bitsPerSample = ((flags & BYTES_STORED) + 1) * 8 - + ((flags & SHIFT_MASK) >> SHIFT_LSB); + d->sampleRate = sample_rates[(flags & SRATE_MASK) >> SRATE_LSB]; + d->channels = (flags & MONO_FLAG) ? 1 : 2; + + unsigned int samples = d->data.toUInt(12, false); + if(samples == ~0u) { + if(d->file && d->style != Fast) { + samples = seekFinalIndex(); + } + else { + samples = 0; + } + } + d->length = d->sampleRate > 0 ? (samples + (d->sampleRate / 2)) / d->sampleRate : 0; + d->sampleFrames = samples; + + d->bitrate = d->length > 0 ? ((d->streamLength * 8L) / d->length) / 1000 : 0; +} + +unsigned int WavPack::Properties::seekFinalIndex() +{ + ByteVector blockID("wvpk", 4); + + long offset = d->streamLength; + while(offset > 0) { + offset = d->file->rfind(blockID, offset); + if(offset == -1) + return 0; + d->file->seek(offset); + ByteVector data = d->file->readBlock(32); + if(data.size() != 32) + return 0; + const int version = data.toShort(8, false); + if(version < MIN_STREAM_VERS || version > MAX_STREAM_VERS) + continue; + const unsigned int flags = data.toUInt(24, false); + if(!(flags & FINAL_BLOCK)) + return 0; + const unsigned int blockIndex = data.toUInt(16, false); + const unsigned int blockSamples = data.toUInt(20, false); + return blockIndex + blockSamples; + } + + return 0; +} + diff --git a/taglib/taglib/wavpack/wavpackproperties.h b/taglib/taglib/wavpack/wavpackproperties.h new file mode 100644 index 0000000..c788fdd --- /dev/null +++ b/taglib/taglib/wavpack/wavpackproperties.h @@ -0,0 +1,110 @@ +/*************************************************************************** + copyright : (C) 2006 by Lukáš Lalinský + email : lalinsky@gmail.com + + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.org + (original MPC implementation) + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_WVPROPERTIES_H +#define TAGLIB_WVPROPERTIES_H + +#include "taglib_export.h" +#include "audioproperties.h" + +namespace TagLib { + + namespace WavPack { + + class File; + + static const uint HeaderSize = 32; + + //! An implementation of audio property reading for WavPack + + /*! + * This reads the data from an WavPack stream found in the AudioProperties + * API. + */ + + class TAGLIB_EXPORT Properties : public AudioProperties + { + public: + /*! + * Create an instance of WavPack::Properties with the data read from the + * ByteVector \a data. + * + * \deprecated This constructor will be dropped in favor of the one below + * in a future version. + */ + Properties(const ByteVector &data, long streamLength, ReadStyle style = Average); + + /*! + * Create an instance of WavPack::Properties. + */ + // BIC: merge with the above constructor + Properties(File *file, long streamLength, ReadStyle style = Average); + + /*! + * Destroys this WavPack::Properties instance. + */ + virtual ~Properties(); + + // Reimplementations. + + virtual int length() const; + virtual int bitrate() const; + + /*! + * Returns the sample rate in Hz. 0 means unknown or custom. + */ + virtual int sampleRate() const; + + virtual int channels() const; + + /*! + * Returns number of bits per sample. + */ + int bitsPerSample() const; + uint sampleFrames() const; + + /*! + * Returns WavPack version. + */ + int version() const; + + private: + Properties(const Properties &); + Properties &operator=(const Properties &); + + void read(); + unsigned int seekFinalIndex(); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/xm/xmfile.cpp b/taglib/taglib/xm/xmfile.cpp new file mode 100644 index 0000000..d8373a7 --- /dev/null +++ b/taglib/taglib/xm/xmfile.cpp @@ -0,0 +1,650 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "tstringlist.h" +#include "tdebug.h" +#include "xmfile.h" +#include "modfileprivate.h" +#include "tpropertymap.h" + +#include <string.h> +#include <algorithm> + +using namespace TagLib; +using namespace XM; +using TagLib::uint; +using TagLib::ushort; +using TagLib::ulong; + +/*! + * The Reader classes are helpers to make handling of the stripped XM + * format more easy. In the stripped XM format certain header sizes might + * be smaller than one would expect. The fields that are not included + * are then just some predefined valued (e.g. 0). + * + * Using these classes this code: + * + * if(headerSize >= 4) { + * if(!readU16L(value1)) ERROR(); + * if(headerSize >= 8) { + * if(!readU16L(value2)) ERROR(); + * if(headerSize >= 12) { + * if(!readString(value3, 22)) ERROR(); + * ... + * } + * } + * } + * + * Becomes: + * + * StructReader header; + * header.u16L(value1).u16L(value2).string(value3, 22). ...; + * if(header.read(*this, headerSize) < std::min(header.size(), headerSize)) + * ERROR(); + * + * Maybe if this is useful to other formats these classes can be moved to + * their own public files. + */ +class Reader +{ +public: + virtual ~Reader() + { + } + + /*! + * Reads associated values from \a file, but never reads more + * then \a limit bytes. + */ + virtual uint read(TagLib::File &file, uint limit) = 0; + + /*! + * Returns the number of bytes this reader would like to read. + */ + virtual uint size() const = 0; +}; + +class SkipReader : public Reader +{ +public: + SkipReader(uint size) : m_size(size) + { + } + + uint read(TagLib::File &file, uint limit) + { + uint count = std::min(m_size, limit); + file.seek(count, TagLib::File::Current); + return count; + } + + uint size() const + { + return m_size; + } + +private: + uint m_size; +}; + +template<typename T> +class ValueReader : public Reader +{ +public: + ValueReader(T &value) : value(value) + { + } + +protected: + T &value; +}; + +class StringReader : public ValueReader<String> +{ +public: + StringReader(String &string, uint size) : + ValueReader<String>(string), m_size(size) + { + } + + uint read(TagLib::File &file, uint limit) + { + ByteVector data = file.readBlock(std::min(m_size, limit)); + uint count = data.size(); + int index = data.find((char) 0); + if(index > -1) { + data.resize(index); + } + data.replace((char) 0xff, ' '); + value = data; + return count; + } + + uint size() const + { + return m_size; + } + +private: + uint m_size; +}; + +class ByteReader : public ValueReader<uchar> +{ +public: + ByteReader(uchar &byte) : ValueReader<uchar>(byte) {} + + uint read(TagLib::File &file, uint limit) + { + ByteVector data = file.readBlock(std::min(1U,limit)); + if(data.size() > 0) { + value = data[0]; + } + return data.size(); + } + + uint size() const + { + return 1; + } +}; + +template<typename T> +class NumberReader : public ValueReader<T> +{ +public: + NumberReader(T &value, bool bigEndian) : + ValueReader<T>(value), bigEndian(bigEndian) + { + } + +protected: + bool bigEndian; +}; + +class U16Reader : public NumberReader<ushort> +{ +public: + U16Reader(ushort &value, bool bigEndian) + : NumberReader<ushort>(value, bigEndian) {} + + uint read(TagLib::File &file, uint limit) + { + ByteVector data = file.readBlock(std::min(2U,limit)); + value = data.toUShort(bigEndian); + return data.size(); + } + + uint size() const + { + return 2; + } +}; + +class U32Reader : public NumberReader<ulong> +{ +public: + U32Reader(ulong &value, bool bigEndian = true) : + NumberReader<ulong>(value, bigEndian) + { + } + + uint read(TagLib::File &file, uint limit) + { + ByteVector data = file.readBlock(std::min(4U,limit)); + value = data.toUInt(bigEndian); + return data.size(); + } + + uint size() const + { + return 4; + } +}; + +class StructReader : public Reader +{ +public: + StructReader() + { + m_readers.setAutoDelete(true); + } + + /*! + * Add a nested reader. This reader takes ownership. + */ + StructReader &reader(Reader *reader) + { + m_readers.append(reader); + return *this; + } + + /*! + * Don't read anything but skip \a size bytes. + */ + StructReader &skip(uint size) + { + m_readers.append(new SkipReader(size)); + return *this; + } + + /*! + * Read a string of \a size characters (bytes) into \a string. + */ + StructReader &string(String &string, uint size) + { + m_readers.append(new StringReader(string, size)); + return *this; + } + + /*! + * Read a byte into \a byte. + */ + StructReader &byte(uchar &byte) + { + m_readers.append(new ByteReader(byte)); + return *this; + } + + /*! + * Read a unsigned 16 Bit integer into \a number. The byte order + * is controlled by \a bigEndian. + */ + StructReader &u16(ushort &number, bool bigEndian) + { + m_readers.append(new U16Reader(number, bigEndian)); + return *this; + } + + /*! + * Read a unsigned 16 Bit little endian integer into \a number. + */ + StructReader &u16L(ushort &number) + { + return u16(number, false); + } + + /*! + * Read a unsigned 16 Bit big endian integer into \a number. + */ + StructReader &u16B(ushort &number) + { + return u16(number, true); + } + + /*! + * Read a unsigned 32 Bit integer into \a number. The byte order + * is controlled by \a bigEndian. + */ + StructReader &u32(ulong &number, bool bigEndian) + { + m_readers.append(new U32Reader(number, bigEndian)); + return *this; + } + + /*! + * Read a unsigned 32 Bit little endian integer into \a number. + */ + StructReader &u32L(ulong &number) + { + return u32(number, false); + } + + /*! + * Read a unsigned 32 Bit big endian integer into \a number. + */ + StructReader &u32B(ulong &number) + { + return u32(number, true); + } + + uint size() const + { + uint size = 0; + for(List<Reader*>::ConstIterator i = m_readers.begin(); + i != m_readers.end(); ++ i) { + size += (*i)->size(); + } + return size; + } + + uint read(TagLib::File &file, uint limit) + { + uint sumcount = 0; + for(List<Reader*>::Iterator i = m_readers.begin(); + limit > 0 && i != m_readers.end(); ++ i) { + uint count = (*i)->read(file, limit); + limit -= count; + sumcount += count; + } + return sumcount; + } + +private: + List<Reader*> m_readers; +}; + +class XM::File::FilePrivate +{ +public: + FilePrivate(AudioProperties::ReadStyle propertiesStyle) + : tag(), properties(propertiesStyle) + { + } + + Mod::Tag tag; + XM::Properties properties; +}; + +XM::File::File(FileName file, bool readProperties, + AudioProperties::ReadStyle propertiesStyle) : + Mod::FileBase(file), + d(new FilePrivate(propertiesStyle)) +{ + if(isOpen()) + read(readProperties); +} + +XM::File::File(IOStream *stream, bool readProperties, + AudioProperties::ReadStyle propertiesStyle) : + Mod::FileBase(stream), + d(new FilePrivate(propertiesStyle)) +{ + if(isOpen()) + read(readProperties); +} + +XM::File::~File() +{ + delete d; +} + +Mod::Tag *XM::File::tag() const +{ + return &d->tag; +} + +PropertyMap XM::File::properties() const +{ + return d->tag.properties(); +} + +PropertyMap XM::File::setProperties(const PropertyMap &properties) +{ + return d->tag.setProperties(properties); +} + +XM::Properties *XM::File::audioProperties() const +{ + return &d->properties; +} + +bool XM::File::save() +{ + if(readOnly()) { + debug("XM::File::save() - Cannot save to a read only file."); + return false; + } + seek(17); + writeString(d->tag.title(), 20); + seek(1, Current); + writeString(d->tag.trackerName(), 20); + seek(2, Current); + ulong headerSize = 0; + if(!readU32L(headerSize)) + return false; + seek(2+2+2, Current); + + ushort patternCount = 0; + ushort instrumentCount = 0; + if(!readU16L(patternCount) || !readU16L(instrumentCount)) + return false; + + seek(60 + headerSize); + + // need to read patterns again in order to seek to the instruments: + for(ushort i = 0; i < patternCount; ++ i) { + ulong patternHeaderLength = 0; + if(!readU32L(patternHeaderLength) || patternHeaderLength < 4) + return false; + + ushort dataSize = 0; + StructReader pattern; + pattern.skip(3).u16L(dataSize); + + uint count = pattern.read(*this, patternHeaderLength - 4U); + if(count != std::min(patternHeaderLength - 4U, (ulong)pattern.size())) + return false; + + seek(patternHeaderLength - (4 + count) + dataSize, Current); + } + + StringList lines = d->tag.comment().split("\n"); + uint sampleNameIndex = instrumentCount; + for(ushort i = 0; i < instrumentCount; ++ i) { + ulong instrumentHeaderSize = 0; + if(!readU32L(instrumentHeaderSize) || instrumentHeaderSize < 4) + return false; + + uint len = std::min(22UL, instrumentHeaderSize - 4U); + if(i >= lines.size()) + writeString(String::null, len); + else + writeString(lines[i], len); + + long offset = 0; + if(instrumentHeaderSize >= 29U) { + ushort sampleCount = 0; + seek(1, Current); + if(!readU16L(sampleCount)) + return false; + + if(sampleCount > 0) { + ulong sampleHeaderSize = 0; + if(instrumentHeaderSize < 33U || !readU32L(sampleHeaderSize)) + return false; + // skip unhandeled header proportion: + seek(instrumentHeaderSize - 33, Current); + + for(ushort j = 0; j < sampleCount; ++ j) { + if(sampleHeaderSize > 4U) { + ulong sampleLength = 0; + if(!readU32L(sampleLength)) + return false; + offset += sampleLength; + + seek(std::min(sampleHeaderSize, 14UL), Current); + if(sampleHeaderSize > 18U) { + uint len = std::min(sampleHeaderSize - 18U, 22UL); + if(sampleNameIndex >= lines.size()) + writeString(String::null, len); + else + writeString(lines[sampleNameIndex ++], len); + seek(sampleHeaderSize - (18U + len), Current); + } + } + else { + seek(sampleHeaderSize, Current); + } + } + } + else { + offset = instrumentHeaderSize - 29; + } + } + else { + offset = instrumentHeaderSize - (4 + len); + } + seek(offset, Current); + } + + return true; +} + +void XM::File::read(bool) +{ + if(!isOpen()) + return; + + seek(0); + ByteVector magic = readBlock(17); + // it's all 0x00 for stripped XM files: + READ_ASSERT(magic == "Extended Module: " || magic == ByteVector(17, 0)); + + READ_STRING(d->tag.setTitle, 20); + READ_BYTE_AS(escape); + // in stripped XM files this is 0x00: + READ_ASSERT(escape == 0x1A || escape == 0x00); + + READ_STRING(d->tag.setTrackerName, 20); + READ_U16L(d->properties.setVersion); + + READ_U32L_AS(headerSize); + READ_ASSERT(headerSize >= 4); + + ushort length = 0; + ushort restartPosition = 0; + ushort channels = 0; + ushort patternCount = 0; + ushort instrumentCount = 0; + ushort flags = 0; + ushort tempo = 0; + ushort bpmSpeed = 0; + + StructReader header; + header.u16L(length) + .u16L(restartPosition) + .u16L(channels) + .u16L(patternCount) + .u16L(instrumentCount) + .u16L(flags) + .u16L(tempo) + .u16L(bpmSpeed); + + uint count = header.read(*this, headerSize - 4U); + uint size = std::min(headerSize - 4U, (ulong)header.size()); + + READ_ASSERT(count == size); + + d->properties.setLengthInPatterns(length); + d->properties.setRestartPosition(restartPosition); + d->properties.setChannels(channels); + d->properties.setPatternCount(patternCount); + d->properties.setInstrumentCount(instrumentCount); + d->properties.setFlags(flags); + d->properties.setTempo(tempo); + d->properties.setBpmSpeed(bpmSpeed); + + seek(60 + headerSize); + + // read patterns: + for(ushort i = 0; i < patternCount; ++ i) { + READ_U32L_AS(patternHeaderLength); + READ_ASSERT(patternHeaderLength >= 4); + + uchar packingType = 0; + ushort rowCount = 0; + ushort dataSize = 0; + StructReader pattern; + pattern.byte(packingType).u16L(rowCount).u16L(dataSize); + + uint count = pattern.read(*this, patternHeaderLength - 4U); + READ_ASSERT(count == std::min(patternHeaderLength - 4U, (ulong)pattern.size())); + + seek(patternHeaderLength - (4 + count) + dataSize, Current); + } + + StringList intrumentNames; + StringList sampleNames; + uint sumSampleCount = 0; + + // read instruments: + for(ushort i = 0; i < instrumentCount; ++ i) { + READ_U32L_AS(instrumentHeaderSize); + READ_ASSERT(instrumentHeaderSize >= 4); + + String instrumentName; + uchar instrumentType = 0; + ushort sampleCount = 0; + + StructReader instrument; + instrument.string(instrumentName, 22).byte(instrumentType).u16L(sampleCount); + + // 4 for instrumentHeaderSize + uint count = 4 + instrument.read(*this, instrumentHeaderSize - 4U); + READ_ASSERT(count == std::min(instrumentHeaderSize, (ulong)instrument.size() + 4)); + + ulong sampleHeaderSize = 0; + long offset = 0; + if(sampleCount > 0) { + sumSampleCount += sampleCount; + // wouldn't know which header size to assume otherwise: + READ_ASSERT(instrumentHeaderSize >= count + 4 && readU32L(sampleHeaderSize)); + // skip unhandeled header proportion: + seek(instrumentHeaderSize - count - 4, Current); + + for(ushort j = 0; j < sampleCount; ++ j) { + ulong sampleLength = 0; + ulong loopStart = 0; + ulong loopLength = 0; + uchar volume = 0; + uchar finetune = 0; + uchar sampleType = 0; + uchar panning = 0; + uchar noteNumber = 0; + uchar compression = 0; + String sampleName; + StructReader sample; + sample.u32L(sampleLength) + .u32L(loopStart) + .u32L(loopLength) + .byte(volume) + .byte(finetune) + .byte(sampleType) + .byte(panning) + .byte(noteNumber) + .byte(compression) + .string(sampleName, 22); + + uint count = sample.read(*this, sampleHeaderSize); + READ_ASSERT(count == std::min(sampleHeaderSize, (ulong)sample.size())); + // skip unhandeled header proportion: + seek(sampleHeaderSize - count, Current); + + offset += sampleLength; + sampleNames.append(sampleName); + } + } + else { + offset = instrumentHeaderSize - count; + } + intrumentNames.append(instrumentName); + seek(offset, Current); + } + + d->properties.setSampleCount(sumSampleCount); + String comment(intrumentNames.toString("\n")); + if(sampleNames.size() > 0) { + comment += "\n"; + comment += sampleNames.toString("\n"); + } + d->tag.setComment(comment); +} diff --git a/taglib/taglib/xm/xmfile.h b/taglib/taglib/xm/xmfile.h new file mode 100644 index 0000000..ae48a51 --- /dev/null +++ b/taglib/taglib/xm/xmfile.h @@ -0,0 +1,108 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_XMFILE_H +#define TAGLIB_XMFILE_H + +#include "tfile.h" +#include "audioproperties.h" +#include "taglib_export.h" +#include "modfilebase.h" +#include "modtag.h" +#include "xmproperties.h" + +namespace TagLib { + + namespace XM { + + class TAGLIB_EXPORT File : public Mod::FileBase { + public: + /*! + * Constructs an Extended Module file from \a file. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + */ + File(FileName file, bool readProperties = true, + AudioProperties::ReadStyle propertiesStyle = + AudioProperties::Average); + + /*! + * Constructs an Extended Module file from \a stream. + * + * \note In the current implementation, both \a readProperties and + * \a propertiesStyle are ignored. The audio properties are always + * read. + * + * \note TagLib will *not* take ownership of the stream, the caller is + * responsible for deleting it after the File object. + */ + File(IOStream *stream, bool readProperties = true, + AudioProperties::ReadStyle propertiesStyle = + AudioProperties::Average); + + /*! + * Destroys this instance of the File. + */ + virtual ~File(); + + Mod::Tag *tag() const; + + /*! + * Implements the unified property interface -- export function. + * Forwards to Mod::Tag::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Forwards to Mod::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + + /*! + * Returns the XM::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + XM::Properties *audioProperties() const; + + /*! + * Save the file. + * This is the same as calling save(AllTags); + * + * \note Saving Extended Module tags is not supported. + */ + bool save(); + + private: + File(const File &); + File &operator=(const File &); + + void read(bool readProperties); + + class FilePrivate; + FilePrivate *d; + }; + } +} + +#endif diff --git a/taglib/taglib/xm/xmproperties.cpp b/taglib/taglib/xm/xmproperties.cpp new file mode 100644 index 0000000..45da02b --- /dev/null +++ b/taglib/taglib/xm/xmproperties.cpp @@ -0,0 +1,180 @@ +/*************************************************************************** + copyright :(C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "xmproperties.h" + +using namespace TagLib; +using namespace XM; + +class XM::Properties::PropertiesPrivate +{ +public: + PropertiesPrivate() : + lengthInPatterns(0), + channels(0), + version(0), + restartPosition(0), + patternCount(0), + instrumentCount(0), + sampleCount(0), + flags(0), + tempo(0), + bpmSpeed(0) + { + } + + ushort lengthInPatterns; + int channels; + ushort version; + ushort restartPosition; + ushort patternCount; + ushort instrumentCount; + uint sampleCount; + ushort flags; + ushort tempo; + ushort bpmSpeed; +}; + +XM::Properties::Properties(AudioProperties::ReadStyle propertiesStyle) : + AudioProperties(propertiesStyle), + d(new PropertiesPrivate) +{ +} + +XM::Properties::~Properties() +{ + delete d; +} + +int XM::Properties::length() const +{ + return 0; +} + +int XM::Properties::bitrate() const +{ + return 0; +} + +int XM::Properties::sampleRate() const +{ + return 0; +} + +int XM::Properties::channels() const +{ + return d->channels; +} + +TagLib::ushort XM::Properties::lengthInPatterns() const +{ + return d->lengthInPatterns; +} + +TagLib::ushort XM::Properties::version() const +{ + return d->version; +} + +TagLib::ushort XM::Properties::restartPosition() const +{ + return d->restartPosition; +} + +TagLib::ushort XM::Properties::patternCount() const +{ + return d->patternCount; +} + +TagLib::ushort XM::Properties::instrumentCount() const +{ + return d->instrumentCount; +} + +TagLib::uint XM::Properties::sampleCount() const +{ + return d->sampleCount; +} + +TagLib::ushort XM::Properties::flags() const +{ + return d->flags; +} + +TagLib::ushort XM::Properties::tempo() const +{ + return d->tempo; +} + +TagLib::ushort XM::Properties::bpmSpeed() const +{ + return d->bpmSpeed; +} + +void XM::Properties::setLengthInPatterns(ushort lengthInPatterns) +{ + d->lengthInPatterns = lengthInPatterns; +} + +void XM::Properties::setChannels(int channels) +{ + d->channels = channels; +} + +void XM::Properties::setVersion(ushort version) +{ + d->version = version; +} + +void XM::Properties::setRestartPosition(ushort restartPosition) +{ + d->restartPosition = restartPosition; +} + +void XM::Properties::setPatternCount(ushort patternCount) +{ + d->patternCount = patternCount; +} + +void XM::Properties::setInstrumentCount(ushort instrumentCount) +{ + d->instrumentCount = instrumentCount; +} + +void XM::Properties::setSampleCount(uint sampleCount) +{ + d->sampleCount = sampleCount; +} + +void XM::Properties::setFlags(ushort flags) +{ + d->flags = flags; +} + +void XM::Properties::setTempo(ushort tempo) +{ + d->tempo = tempo; +} + +void XM::Properties::setBpmSpeed(ushort bpmSpeed) +{ + d->bpmSpeed = bpmSpeed; +} diff --git a/taglib/taglib/xm/xmproperties.h b/taglib/taglib/xm/xmproperties.h new file mode 100644 index 0000000..9b6624a --- /dev/null +++ b/taglib/taglib/xm/xmproperties.h @@ -0,0 +1,79 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef TAGLIB_XMPROPERTIES_H +#define TAGLIB_XMPROPERTIES_H + +#include "taglib.h" +#include "tstring.h" +#include "audioproperties.h" + +namespace TagLib { + namespace XM { + class Properties : public AudioProperties { + friend class File; + public: + /*! Flag bits. */ + enum { + LinearFreqTable = 1 // otherwise its the amiga freq. table + }; + + Properties(AudioProperties::ReadStyle propertiesStyle); + virtual ~Properties(); + + int length() const; + int bitrate() const; + int sampleRate() const; + int channels() const; + + ushort lengthInPatterns() const; + ushort version() const; + ushort restartPosition() const; + ushort patternCount() const; + ushort instrumentCount() const; + uint sampleCount() const; + ushort flags() const; + ushort tempo() const; + ushort bpmSpeed() const; + + void setChannels(int channels); + + void setLengthInPatterns(ushort lengthInPatterns); + void setVersion(ushort version); + void setRestartPosition(ushort restartPosition); + void setPatternCount(ushort patternCount); + void setInstrumentCount(ushort instrumentCount); + void setSampleCount(uint sampleCount); + void setFlags(ushort flags); + void setTempo(ushort tempo); + void setBpmSpeed(ushort bpmSpeed); + + private: + Properties(const Properties&); + Properties &operator=(const Properties&); + + class PropertiesPrivate; + PropertiesPrivate *d; + }; + } +} + +#endif diff --git a/taglib/tests/CMakeLists.txt b/taglib/tests/CMakeLists.txt new file mode 100644 index 0000000..7a3d0bd --- /dev/null +++ b/taglib/tests/CMakeLists.txt @@ -0,0 +1,76 @@ +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/toolkit + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ape + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/asf + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v1 + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v2 + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg/id3v2/frames + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpeg + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mpc + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mp4 + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/riff + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/riff/aiff + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/riff/wav + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/trueaudio + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ogg + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ogg/vorbis + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ogg/flac + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ogg/speex + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/ogg/opus + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/flac + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/wavpack + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/mod + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/s3m + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/it + ${CMAKE_CURRENT_SOURCE_DIR}/../taglib/xm +) + +SET(test_runner_SRCS + main.cpp + test_list.cpp + test_map.cpp + test_mpeg.cpp + test_synchdata.cpp + test_trueaudio.cpp + test_bytevector.cpp + test_bytevectorlist.cpp + test_bytevectorstream.cpp + test_string.cpp + test_propertymap.cpp + test_fileref.cpp + test_id3v1.cpp + test_id3v2.cpp + test_xiphcomment.cpp + test_aiff.cpp + test_riff.cpp + test_ogg.cpp + test_oggflac.cpp + test_flac.cpp + test_flacpicture.cpp + test_flacunknownmetadatablock.cpp + test_ape.cpp + test_apetag.cpp + test_wav.cpp + test_info.cpp + test_wavpack.cpp + test_mp4.cpp + test_mp4item.cpp + test_mp4coverart.cpp + test_asf.cpp + test_mod.cpp + test_s3m.cpp + test_it.cpp + test_xm.cpp + test_mpc.cpp + test_opus.cpp +) + +INCLUDE_DIRECTORIES(${CPPUNIT_INCLUDE_DIR}) + +ADD_EXECUTABLE(test_runner ${test_runner_SRCS}) +TARGET_LINK_LIBRARIES(test_runner tag ${CPPUNIT_LIBRARIES}) + +ADD_TEST(test_runner test_runner) +ADD_CUSTOM_TARGET(check COMMAND ${CMAKE_CTEST_COMMAND} -V + DEPENDS test_runner) diff --git a/taglib/tests/data/005411.id3 b/taglib/tests/data/005411.id3 new file mode 100644 index 0000000000000000000000000000000000000000..ab2e0997a0bcbf27aa169a09fd3f730d8319de9d GIT binary patch literal 38402 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 AdjJ3c literal 0 HcmV?d00001 diff --git a/taglib/tests/data/64bit.mp4 b/taglib/tests/data/64bit.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..0bd7f9f33dfa28317996eab962cce5cb2d2afc0b GIT binary patch literal 85 zcmZQzU|`J6&o5(u0AC=*SejCj2<6*?__?WYzAjW;F*B#Q1jv?5F38LQ(t;_8U^Sv3 Gc}4)(BMUG9 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/broken-tenc.id3 b/taglib/tests/data/broken-tenc.id3 new file mode 100644 index 0000000000000000000000000000000000000000..809040506f29b83c48e055251e164c807d3b2997 GIT binary patch literal 400 zcmcJL!Ab)$5QhKNDhg73gj{+po36XAH`8gLrDn4%1ManlMT@WqrFWmnC-A*I)X6sX z4f=<ld^3LtldKlm2%?KI+lmAdmEg5)+t5P-F!?&PWwEbqMDorFzKgs5ZOi+<@{NN^ zgR+7m*y0Ft(*KYJ&}Cus2xN!o!H^jsH|1=Iw0=xaAal4d$E8?<S3DEA6Z7VyQ@&TO z1J_nd=UQykasxIm^H}SrG6y%bmO5UsXu@;D;bTa?6220b=?;hA?`PoV9_ugJfZNaT Q2S}!pD`4^ij1FheA4Fn><NyEw literal 0 HcmV?d00001 diff --git a/taglib/tests/data/changed.mod b/taglib/tests/data/changed.mod new file mode 100644 index 0000000000000000000000000000000000000000..13dcea8bc98054152611f31515822de7c1254add GIT binary patch literal 3132 zcmYe!NX$!5O;ISxEXhe_fPj#U%wmO{%)C^E^30qZg``x4lA_W)pqv8`Ga_Vxs`9h- zAbMc3a2`6t0Vu$TE(qt3%EE&J$QTWc(a=B+0c18fZ7|j&3k?L@!r8-bAnYBrb2J1- TLtr!nMnhmU1V%$(U_$@^D@Pm& literal 0 HcmV?d00001 diff --git a/taglib/tests/data/changed.s3m b/taglib/tests/data/changed.s3m new file mode 100644 index 0000000000000000000000000000000000000000..37bd49cdd47c365f265f3f56bf53f0cac67be090 GIT binary patch literal 544 zcmYe!NX$!5O;ISxEXhe_Km}3)3=B*RtPG4mVFh6(hG6F)UkA2Y1A#wC>NprVnYfs_ zS$J4^+4$J`|6>4720jL11_=gPBu%)O4Gp+tK;jNSyjlTBAc2sK%wh!~O3YKp%quP_ zD$PyJD^bWx%uQ89QiYxA02D?y&o@7%G$&OdEfYlK7v(0F6l2$nMHHKPiAkl!spw84 o(nzf46)WVH=9FaSWagzRB<JUXyj!e?#W7ffu$iZe8-Ps!07j!oy8r+H literal 0 HcmV?d00001 diff --git a/taglib/tests/data/changed.xm b/taglib/tests/data/changed.xm new file mode 100644 index 0000000000000000000000000000000000000000..bb5db3ddd45e3a5bda8b016e0ae867179f26b4fc GIT binary patch literal 5471 zcmeH}%WA_g5JktP^s&z>5YV!#en6q%O&5V?)%?IDR#Jf_6-$P0O7rLKjODm-Uu07V zcm`V!&sd<ty|V9LqIRV&)xwuerEk^x3Eh_F(Zotais=%!4`$WPy6!BSWB_!*CAB-c z&*61gGQ!yv@3{cTcM_u}a6!(PyAbQfY8RD5t3&699Im_3|8@l^mg3Q&s9qgio#(Cl zX`3AHZ>j&QEr5;*ctrbL=>d>0FQq!HwF=(qXg<jSFjmgsstI&T;hrfik&PNFR>6O# z!noGnvIK)N?hy32Rk(+8bBJUxE?Tt?9`%zMjcohDR2xHL-uT*L3~4b=-u)Pq;N-6% oNkStK1VVC%C_$7UN=O7m38Dm1LQg>KAa>}*ApZ$shZD8K2XpUN-T(jq literal 0 HcmV?d00001 diff --git a/taglib/tests/data/click.mpc b/taglib/tests/data/click.mpc new file mode 100644 index 0000000000000000000000000000000000000000..a41f14e9ea2175684ff2ab74d3682ddbd0785765 GIT binary patch literal 1588 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/click.wv b/taglib/tests/data/click.wv new file mode 100644 index 0000000000000000000000000000000000000000..f8bd1a8513ca97bae632a2a144fa90c9582bcd48 GIT binary patch literal 3176 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 Gx%FRQMM#za literal 0 HcmV?d00001 diff --git a/taglib/tests/data/compressed_id3_frame.mp3 b/taglib/tests/data/compressed_id3_frame.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..60bc895633b1ce32894e54800f6aa65d4e2cb546 GIT binary patch literal 5000 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 D0Kmmp literal 0 HcmV?d00001 diff --git a/taglib/tests/data/correctness_gain_silent_output.opus b/taglib/tests/data/correctness_gain_silent_output.opus new file mode 100644 index 0000000000000000000000000000000000000000..00972c42f64f285610c17ab548ac0bfa6960bdff GIT binary patch literal 35506 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/covr-junk.m4a b/taglib/tests/data/covr-junk.m4a new file mode 100644 index 0000000000000000000000000000000000000000..ac80cb29d2585e9d728a86b5027973690f51d174 GIT binary patch literal 5108 zcmeHK-)qxw5Wgf{e{Jnj?c7gBwr+EX>$Gmjbg*@+oe|w+6ciC=)}*ekX-1O`6_MhT zBErBui7@d&7>dvOAXFF#J}Wo};-i0nuM+Rl)U8DXVW1EBw8`D~^8Mc3_ru*cEdaD+ zj2lbp(ohT#B(u7v0@S@$Wy$#B0?1|OGm?}Rjz(ic02A;~aK&J%3&d5n6(U7PaY%3~ z!RZ303;u^L00>4@O<TdUJg2TK$aJWezMtB9dsY;InOlXr=Ka6`eRohbKy%G_d}rqG zG!~65SxwgIAzv&OJuqXMjiw3UGK_+>h~~@2qLmBtE0;<X%4Zj>&^7V@oR5oF<*bA* z1XZrtFT6+qd%I-2?^=*^g-R%0Th3Q>TXOHzY(9enURTSG><2w^+1?=+6naje6dRTc zIiaj`_jO}Fhx%3B&@Ix-cDR6VR7#Gpvn>{QGGKayV~SoTk3;gh(cnTE9&Z9@!yw-P z>gVgw!tUeUC>Y{f&<(@l8+975<02Fkf-T{-hH2a+gN-LB(HZ!h%i-W3)j&ViWEx`= zsAj1h4%?E7x5L)4w$mEw1hNY_^$h?g;_xGl)77<)eyrNZ9w7K4%d#P%Y*!VM7OHw5 z&x?Yzv_$P%0lSvX=?0FT4j@o=sETAA5NS<~(qtu?O)cjYYPxV<QWaWBmg$+<bEsa! zAIlUBlfev+*w}Lg13XJ>D|C#_*%5;j@!t9Aser%Dk7__XotQ;Q6tiBs|M>8T(mo(2 z;;HM8)3?^&mk^tDD*=Rj%*E$-w=KrSQ`3o;6IbqFm_gfp09S(f$@~iZnWUHmhG9U) z518992|l;S>v8+MUa#Nps}D9e1_Oa$N4P1}+}#<8bae_s@1fy=-h;7zL5PkG#*UAS zj*j*WOeEtclEWuQN5~1|_xpnl!S=?+_K`lJZ{)9GmLTNC{jwa>4Qz<vLX5cyO_(a> zz6%~ShxuA7&}7<H6<ltb9ICP6wm(Ay$6y#P1Y_`^_^Q?1!i9XLCTu5>jreQ|_4eI$ yky3O+h!>2kRcc)uhDNNn?w*p2$0zN7TJB4kf}&tz4jcv?1{?+)1{?<VfPtT=7Y1Aa literal 0 HcmV?d00001 diff --git a/taglib/tests/data/empty.aiff b/taglib/tests/data/empty.aiff new file mode 100644 index 0000000000000000000000000000000000000000..849b762da5a8d2b1a366ea8f9d3daa87be7a194c GIT binary patch literal 5936 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/empty.ogg b/taglib/tests/data/empty.ogg new file mode 100644 index 0000000000000000000000000000000000000000..aa533104d6fde177772775137cb7660a3be11418 GIT binary patch literal 4328 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/empty.spx b/taglib/tests/data/empty.spx new file mode 100644 index 0000000000000000000000000000000000000000..70572b4587f26b1dfa1b93035b65a90f47ffe511 GIT binary patch literal 24301 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/empty.tta b/taglib/tests/data/empty.tta new file mode 100644 index 0000000000000000000000000000000000000000..9cc00ba8156b8ef1b16401b0efe65b6d64a1b1de GIT binary patch literal 79538 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/empty.wav b/taglib/tests/data/empty.wav new file mode 100644 index 0000000000000000000000000000000000000000..74b5a6de7b02e17ed293c677b873f446f77d84d9 GIT binary patch literal 14744 zcmeIup$&sj07cOU#g`Gf2C@Q@S_J5yG^o}w2qQ2aW56DqtFJoaXBf6Fncq{t{MT2T zq)MIRk?hZ;Nt<t3%kO$bfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ gfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5V*6z34A#SivR!s literal 0 HcmV?d00001 diff --git a/taglib/tests/data/empty_alac.m4a b/taglib/tests/data/empty_alac.m4a new file mode 100644 index 0000000000000000000000000000000000000000..8c678321847428247a7776afb210b362301c32c1 GIT binary patch literal 5380 zcmeHL&1(}u6o1)T`#~t_he&F`p%l!;kk;J1L{RWj6;bdY>STA5Zo9L)-I)aHLE_1O zL%c}AqemeMLPh)|?4RJxLt%YyKiqAsB2-XeUNU)a-g`5@dB1&|F92X&wHs`-t}s9$ zErQm%BYi=0fO1v%{s0YE3^pzDd^}s)$MFH+)m1Q_w4ns_%;m@g-DC51o_U;Q02o^; z<l7j$ueM{%sT?!ijc^MRLyY148rL{2EQjM3OjyU)b@zP?u(D~nVFo4rzL(jVF1-<s z*Tq>yie#rWvT7y%nibj<C!sTz`=QGcyEK%l<Dz{}DjBnz_~ANtxrsJOUY7wCav<*{ zt9cH(EA`nq10wPR%zjF<m{dx}*+?tu3z$e#+;gstx`wz#9KxDR;_w<{kI~Mgjt2XO z2i6TusZ1X=$#ha{<vGB78^>pWg(1Kq!|@hi=@G!p<-Vmj>Px~V9m|m`j$IP+AfRiM z$QLB<rQ=G4)0I7s(|xjR@pv_{2et^*?!)xjRL#JlwexAl8Ok8320b32{voieL_ifR zjIV(Dp7JTeifGQAzGq9e)@-cfW37ep-bEDDHDU9(TTNdys0O={WMk|c9^6I^*F4k5 zKJBqvJmi8}rJin2Q5e`E`AXdEPzz5CI^hoaQC|E8%u)tJz&CE*-oSNR#pAPPDu3Jn zn0n9+?YPMJy!u5mpc&8%Xa+O`nt>BDFfr1ft|ul@Z=f0YI}A*g1%3}_0H<JadgkfZ vchL}k7Jav(XVK^&I@EE^PoXPxe`I%xzCZqx_Xqs9t2X@Ed))ca!T#YliGbjW literal 0 HcmV?d00001 diff --git a/taglib/tests/data/empty_flac.oga b/taglib/tests/data/empty_flac.oga new file mode 100644 index 0000000000000000000000000000000000000000..444587fd0cf950ce092cc5540b81f973b52136d1 GIT binary patch literal 9113 zcmeI&Pe_w-9LMqB<7R)X=Hpsx*{roTTWGc!ViMF+=unb}K}D3HHir-v=4Ay7v44~u zIut>N3=gqOk|2nUC`n|P{Q)8B5O$FcQ6nW*B<uO?vxd;6YxMnHp5N=?_j{i4zCAzI z6p4gXfy#|@6U`?uEsfefX*<sEYuwwQDP?MJY-up=x^<-+7mHGQ(X;xLDu@MV5A;p% z9~+2|mW6&sAI4rc;c@wa#~J&Tcl=CTt28U+a~bLiw}-pJoo!*)$&OaM!WF2ht_rxS z{r*~iU7!}*|9lR9W2(NtTeF*#vSQ8Rq33pFaszKR)~UhM!%s?n-^^colxy*3nM(mF zAO)m=6p#W^Knh3!DIf);fE17dQs7@NkUdPnIZSroXv8>*QSMf=N6#OLaN^0Wk&6_N z0#ZN<NC7Dz1*Cu!kOERb3P^$fuR!+u*&K}VZxZ-^E;J%^nrjApmcJW%CY^MsF{T^& z3~A%k_B&9)bIPtkrb()&8#2GA!ZlFgTk2~NTJnmTd<QLkLmeB0mi?f5k3-8BD4P!| zny0o6K`Z8{(FthfEY*AoTJ?cS&Ow%sRH_3i{z65ZkUm7cyaJUBQ%4pcYl3QtK&u~7 z->i`B0c8zAYvR<!a>(9KB@)ou`&2~|a$Ke6cR}l}Q*8&K(hJnlX=r^fHFXbi-lDe7 zKxH?m$2Xx3x2em;(8jxzeim}MsY7mPlb3pW7;;xq=_zP)1$Al!^6aLreTTetl(P>i T_fuYbCLMKbvFzDNznJ+2futKO literal 0 HcmV?d00001 diff --git a/taglib/tests/data/empty_vorbis.oga b/taglib/tests/data/empty_vorbis.oga new file mode 100644 index 0000000000000000000000000000000000000000..aa533104d6fde177772775137cb7660a3be11418 GIT binary patch literal 4328 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/gnre.m4a b/taglib/tests/data/gnre.m4a new file mode 100644 index 0000000000000000000000000000000000000000..f925ea9eb5d1cfc12a1effb51e3208737a97aeb9 GIT binary patch literal 5026 zcmeHL%Wl(95Iu1mS_G(8d9|erCuP9`Arblk4Np}<;-N}(frOmcX|39E@V%)73zR*Z zO0b7*C4PVfQg;0U7HnAa0qnyWKS&y(Zb9OXV$IC)&CK{{=H@y8mb&^?KklzLK@lCQ zBnE1qW8c%?Z6FHUUC-;dH*Yl8f!lcJ+D$yDWAngT#@f&rh6K|IW(v#{{0mdSJ$zm^ zZs6Q@oFx17qj-PUm(6|j^WL|kg}Dfo7YC?aK936IGm&Es;TeBpvhq)sMQ|xhd?h!X z;cz&IN8|CrcnsKDr`{gn=Xx)zrSiL$GK<tRC0Xj3{3q9AJFy>n4B^K9%=6;YU082M zMfm!zAEjj~l?-~NUFhmj9QL|&92Mt*iW^t`9AEX*K<<l3iDw6C<mPtut<vp?_Kwmj zBR$9QjK8c>oXF&{EI0zNJds<0%E_yeyaoZA`ohLVV1-G30B71YBG#gFimD=fyGrYf zZ`$I2;FqW*g*D>!o@KlippcK$42IL@G93J+9=OJy>@c^8^sJ1-^+HSYt`;_XyOYos z5!~g{FF<vZ?pN_U$9tb~vfk$mh|F&feC<&`jsww|G^S^t`$g*Y`!cS%JmG3|(;z4T zqYVRw0mFb{z%XDK_%9e}gpuOg6IOP5shk5A`7%|W9)L?kUdjpK#B&4}JuJf14|O7k knB^i24=KR?R_jjjcN*3XTTi!$U72366)5etQXMM&3nGA@LjV8( literal 0 HcmV?d00001 diff --git a/taglib/tests/data/has-tags.m4a b/taglib/tests/data/has-tags.m4a new file mode 100644 index 0000000000000000000000000000000000000000..f48a28b520399cdd2184f965f4b690c68f6702d6 GIT binary patch literal 5108 zcmeHJ%WD&15TD(od9`_IYG0vjnzk0Psm2zG2HLbSHKMJQfF6Xp&2DVkY_?>#MMWft z2StQ};z5Lpx1xB~gAk!0cvi3$#G`+JS6OE^8=C|Xgn}OSm&eTf_S>2HW@o+x06KH( zwH0Y)Gztikc}120T3$;6uYR!t6!Hr>p3ibeBhgWSNqE3nqc9zU^g7!G;j*ciMKC+T zoC0$S{)Z_5@Ps8<Uc<AzAgwJ5bf{OppW1$VHk}4$ejQpm_5%a--bI6fbdx=?yYhD$ zi)3eB78JVZE0@c5nAJ3|rU6)0wZtzYeMw!`b76kvQj0?Q)Iks36#vipSbdU^=g|cx z2~GQj7fE1mRt)!{MWIltg;M0zVof)s<(-r-=FspdQq_@hp<k%-L7^nlbpoZ>wpuE1 zRo!w=Q5OoRUr|&=FM5@S3;2sl$q{z9#e$CrOpkC(RI0_ZVDb8pu%Zl1v;%ZwkZ%C3 z3oXd7`+PPO4Aq-cR8_Ax>SS-=O(-Y?TfxsmOyfEc2A-lsXW%qHW)1#P4-8>XW-&H_ zdX~oFupy~=gNBa1ot0515YOS#HvpD2njgu|HugUH(R&|ffZz<T3M!AXM-oZuXqbIG zFG~E%3XN+G?CpF(QPB!*sxk%d!au)sGdc%IB@$D{-(LuCCgw88_0%uOh>FT3N@`yB z@oX4@OM;Tf>xWEMmLe2TjN~(`MUe(KP~;^MB@i<+J9iG%rFxg#5d(X!;HDmB<uy7A z=Z%OS%H;0(nQ6DH+l8t-eL6XZk{%Q5pzj|a9#h%}(#b^T>XYn^jrSGA7JXF!{7&ZL z^E*5GQ%j$kNxq!Cd<(;j81CJ8CupCvui!hAN~eHf7!dFU+73*C(`I+rZBB>7;c_`! zJsn<;+wBPk+Wj4Uz2R`Em*WNwjSUYRj1F;JWPBugJQk0~`-dk}i4&=@lkpfiVO%bk zr_Iyj_4dRDxxv_9L#u$_f%|1GOdqg*#^PtREojG7Dfc?qkq-EpS)j$Vtp-?awAkOo v;<i6Sn}xwJ7C(%`{qn0WZ5zP0lq-pPPc%3&4YLAf1<VSV6)-EXCl&Y!Uf%vX literal 0 HcmV?d00001 diff --git a/taglib/tests/data/id3v22-tda.mp3 b/taglib/tests/data/id3v22-tda.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d89ae1c4473cfeec3d8c9a1ecf8c2fe0f4570506 GIT binary patch literal 8192 zcmeHLbyQSeyFLRm<PcI)N+VqZ5`y%=(A^+Iw}60%B11O<!VoImB|S8Vq;x1q2!bFj zQWE0a@%z4Y|GMk`{=RG7z1P|KzVGwC&)(<ka}GiUh6mshp`)^{HUJ2L07n4(2f$qh zfGgmQ%Z`8pzzWy_PjICXpbi)US^xqt0aS5X|I%>*9KD=fjr_fIbPcotR+0ate{P6N z{IiUR$bZWKMn+-)zycsxm62YorbrKWR^7j~pQA79zsKMnPXHr5Q2>AfO2|i#T>QOo zbAXYGA^;EoA}|qgz(`RG0H}fgLCruD0Pq1(z{ngI7y%$AA}RtHDeD3N1mJ=3{-x;u z)BiK@f1LsBRX+HiVlYuMKmZ`JI^2D4x0nI|Y%)Lu00Qs;h0MCnvi7@e<L#1d6K&4z zk{xsHxos1z0__s*QXNF?xvlA4s~tqGmu>bf7R|b?SFJBG$6XZd;+UT;eVEub`!@U5 z-yNPcr^SaQ=B0c^jkO|;syJ(RJG?sxar%;7tG%oPnY}SXQvJ4rlY=e;BK<?dM1wAa zlLI0{QbSUMz(D5UB(9Y=2;k;%61dt0*PHCw>X>g$!+C&i8*P8np4;KwHqsQ_Vum?v zy3s0tIcyujdFs^W(#qH7*kaX0hdJ$Kec#h}KJ5M1`rrK5@_%ZVzjg;Q2QvTS;7})r z@^CHO=&<+jd>>D{<UcX^e_<Ybc|&Ck^e;~}Q2;;$0L0z^ZkYj)5N?na0JwSp08E7d zP^4j$N(3OWXL<nOi2?|+->IVjyo*D<$r>yx7>^W-y;!}RE7dtj_o@Qf+KPY^^8iVn z;O<3i;2K`*o4+_L<ZL*b!2g0HzudZP;x>R%007bMy2>k)<rTrRlydZsB@`5f4?M(n zW3ktr0%OD73Sm4w4Bc<vY+;Ao0}^w0WxrTMV<-UOzc|~BO8<aEU)(OQv*>kZA99&e z%?02k;N)2Wiak>-7W?PPvs)S<6rco<U4>w=mRt7NEBbZ-7dAN_&M$&vT%iEqa2!hS z+vD{99S_;6D-F~h1dQTyqEG<q9aLfRApn`f*~400$2ZC8fdC>b_H+*Wt5`?MnoPCn zf<r6pyFY1;e@IuR(LLMN+?ji(3Wpzi`wd*ZitULsLwZ#9yPBH+;&`F`P{4CwpbQ7b zXv-kS3;=i@AbJ$8D}!=a^#;I;PJWUU@`{ToMg1R3O}_A|m8%zvr<6C2u|_or4&>2B z3FL3aqBohtN~oes*rU0r!%L_lxw(l3NP$i6cy5|nwZAwY3LgRh8l<jh3lLGGc!>l6 z5S|vka6XP3WmHiS4u=;`&~ppUK^R2^3Imw?*=z?2^gbZxd-AH5S$8NuFYU^{m)-In zXU#O^Y$6w2ALg&tT63yEDJ0mU*YBl^HQ}rO#mQiE2Y@@kNS*>fM1bN2GXo&dDugv0 z$D1-xM3V=ATb}f7aEqVRST@gX{kVVd+jFU?k}PH|Mlme32M?h1j|Oc#{YGL$LU<4s zb8xl9PQqY`#a`;$eQ$YLeVgLdqSqblrKp32VgLaA{5Nk*c|04k06^z(#hbKoL$Mgm zg{;o^zU|v7wq?JbLst@?#|(<Ib&u4~<N%5Rg+4%qJvK)h%{iwuq=bm+52KtNR96Pl z6z7mHd0pslfY8>_ybd}K9ta^N<)6IwIk2CHfFPQe4kYxk#fD#gboSE?Wrq|vFsz0u z=Wfuf_G&XS{`ix|u+>vn$BMnIbEwgVpupedXEe?2J(7q>Je`Rj(tt!YMr)r3F%(od zMlo(u7UZjugr<_*(5hcx-F~4Fc#c<(Uj`XeaE%(qxMnE8lBd+bq*U!TU#Uu!l(iVd zBk4m|SG9Nu>{tondi48DT*fZM8uaV(EM@Qsl0t*88l+0NY>k$N+GLY9?U$0RB#pFu z*`D?}l%|Op8jigR%FDa{mgpEySn@;;%>4X3VCnIYX*cXx8sxtq=lG(lTZqm<nyWpy z3eeMY!nfcZ&h;6hF{@2&JW#}Yb@EyhhzHV1G8so`h(bQ|CgjgJleq@I{e(tq00J>V z<ktHgI>eZpW)<7#-uqoiFUfp<V)$cK9iTa#uKd5$u`_+U{oC{dOY-l031*G$;ATlR za8?C2NzEOFP(Q?%uN;(|HjiwonV#le_**y`D6Gr^CfkZSdP+wG<Olh8d4Bf+L-in9 z`-oUO$2|Hau}$Uqyd3-zV=fQ>8%@BEil68P_*(J=?8~bf_bCeH#Kg<EahZvFU4D<k z@A(hgDKdmi|2Uon<ebM}F*esyq}3tXXeOXtWZ^t{JQM<vfswnK!f$yi1zC9E%WUBZ ze$zEQ9p6r)M!S(g;03KlJ(Ef53kp#`>7Fu&r{}tn73*geVNKT4Jgr6zBeBNw%W2k% zvtu^=4z~QCUHCCcw^E2yNz&?LNZ&S;6z+spy%%aY_*69I_>y?WS?P3_av@lZvBtL2 zm6z(YFmh<w6{Ew*o?EK+AiAc0Vp?y0{hhoClajSah2@n$8wn#+U{`#;rSHp{=-!xv z*bm_b7P(Pc>O^6L^}09@3ZbF-cgJm$;-SD#;4`}d+7(gT3r`%w2UDwW8Ee(f|2*g9 zD~}=%=)4kTf2>8Dg85wNQ{uGEy%hE<{Dlz0L3`3-I_PGT&^whE_>3xNrL11nS+7LS z8V7C97oBb6_a!NcBAj-VHU{i`--C+WZnn_EA27tvGnus^-iJOAh)kA^Q=Jw|3yzYe zUlXbQeNAt4L>|Y>-FPq_JlA=DV_ZC~t0}h${Iz}a^4Ip*xlhjiG$1hNo4~C=gQ9tV zC&1Bt6NrbWbczi}>fIb$3C-lFkI3+0UZ92<2wnv!+33%SoeXRR9Al0J?%ZqnKu1WN z!IGQ7Y#C9K5Wk}foxAwOhITcM2%K1|;8-a2To$5N(lJq}rh8T)mmg8MW*fzfm@Gmm zM8-VletPd1c62Hnw?DyqMY5fm?ulVm50`$gfxMy9r{BU6p-y8#mGey|S|JipAWO@K zoSCFc3$^Q;(CJLC_5aY#49ju#&>mD$CO70#lnU2j@AHus&LAVyPzjuSNt;hlAQc5M z<y7(NfnU%0fA~JuY#wPB3yTzuy@+Y<nUuU)-?7(htsL{n^M@z%oy5_((X1hfHV~Ly zy5{nlg{iA40s8RhNQ{hg-IevTQni^Y4Vgt&Va4f5?L(;4aGvosxq^m1sav3G<{olD zetJ<HPn0;~$VekTc6?>t*2)q|mW4DqT)is*6!I*Nh+5i^B~DxPSD3U4X`4HKxLYB) z@jCl`AVllT`7xk4+*<@IQE_s9ELKq|H70AGxvO+nY`PbeUM<OlfNu1E?IxF@xauR4 z0=FmA3TQcG8kgHFjN)b6lo6N|<Ib%*hw^c16okXr#87}m_5~s+8yVhixGZc~RlBew zc)Ox0y7)SdY--V-I~mbq;jI^cncf_QRR6NrMOMYOZW1@+o`JZW;jL7hj!#jQ_I6n< zdnKP;wp^C0PXDO@tfbLG`k~R?`+}(XgivE5{C!cRb(+}h4cWCZt;KPQDLY+c>;TJl zXZ+Y@Vq}}6=gW{ntX$4AOJS;n2*@CS#d3r|p=_?b`Es&e8)_*1^!!*|O=<3Pc6h53 zLk5U{=8YK#^cLV+Crg5-(4i%oJ@nwr@0+;QhR?Q>a5_oeq?L>ArcP*P%ual?JDT4) zZs)6U7GCt4O<nK8nI>b@v$~Lvf2<4elseh%_+q`DuN32dwqzDoq;;lGZ_I^*AV$lT z8`Hw2<gI=~=GXV}m8ImQxsvrHmN5&_(rv|U?<F86&kf3qAP6#|XweDsv|*lS7~?yp zkZ&H*yym}uOGOnz%VUF0onA{k()cY)W@9q(U|^jj)IpTzxGmKXrmvQAYFY8Zv^S!( z>L>$oND}c;JNAm0delPIqSNRnwRXaVZ~xousuv9=pF!@9X34Q8NCJ%GUTpoyEh_tB z)rTAJo{&)}^AR0*=^qsn%UD>~D+6JB?buU_wm%Q1rHBvT6^Zed6ilh>NN@(a9<|Zw ziB;VtYayU_O`@bx>Iu@1e3@DdFXzjw?Rq7?_?c;M^J#101EG&`XT7n1Zmag^YrMN{ z80?*E+F+CJuX^7p#L$hOVC<IaR!ttJ$kPw%PaI=gs;fGU*5}Xvc{m;M<GU#g97)=a z{|oQ2ryJyrB$FrcDbp*c#i-FPcu!f7K<e*~d$>x10$*u-GNf>jjdLZHLq@f}RQRbc z@fwi1m*aX?dk9I$3n`B=xbt(H<HJxB9%qClM>L<fIHISEvTnYxBGTbaCQ047L$iYH zTBdT$p0}Duki7UaF_xrvnJRZ6dvB?->bV4Y{M^~38NR|<9st+Kk~tZU!6c=r6Hl@A z4dZzq(HpI4gkH{eEvpW_H(NMuNX1g_+sIBIeSEujM=hhT39~L1{SvXmXfAY@T6@^5 zS)H?{>`<{heXdA<_jkH@EsuJ#&D<YJ>#7Ean)vD=$Y-xq%)9h2@0@z-&aEm?cu-xF z=#jnJ!)#CcKFBVsD=dC49el-jw|T1kHjkgMm4{nDd8@Pei5uU~dZ#smk4+MDth;fm z?!SJY)(DHHh^{RYue|LdTUlU8@iESTk*xP&3O-Q=VbkFKD9Ud>kqS9A9W;(qL)Ail zPP)DhTE5JWy-@-iVoVze9~Yfl(;~PY<a3ob1e8SyN+6BWT5T6R3i>K`_sKOe3cKT& zpIIuz{J+jpB{-Vw%tHi<r{<hgCGi_90!w^s`rm$fvtBM|{;5e$!e&CWRd9rXju|~} z2tRyFG2f{h!icF)`~0&k`&7%Wfj)Zzd!0pGI|SyvgWXkzI}{Qr@oEv;>S=M4xCsSK z3m@TY<tfc>-!LrGkd_EnPZvVG>Z@xDffW-Ik6Kz7mPu-9`Yc-B#M46Si1HY2oSGex z+~v8{hrv4gf(%GyO#(~X+=ZF)hQ5>5*H-anJ&?Xrr`FiuSGU_aqg>Op4Mou$!)0 z)_zVRWf5K8^l}+%_#gp5fjIHBlq8xKu-&Wp*vl!0CozNo;UnECuZIf2K<uT=*cO21 zg&A8X0DQhh{QPw&h@O#1{*A(8Dq>3IOxsMJGBfoa#Xc@ltKl2ayvabLtsB$l8}GIK zwsunlQu3WhIPH|_{YofByc>_jQBd=`!gr9-_N=E7S|W|xXY_JN<HJeLan)9;9ejN4 zK;&dJT(M)EeKb%roLqY(7k)?V>pk+5dNP9z29LP?8wO2}rN@fs=8)%UCpLO}`hw@g zjxs`*8MBEabK%Z~u~La+(I0Zv*#pRQ0+xRVf0>h{%Y{@E@hNOnerE17O!Qx9B5gn2 zx7hzIm$4vqB0fBDl!O*9tx=q5)nuE$Ep*+#{E811gVKxjyu0Yr1IkX$h}lI$S!wo- zM)mY5(VqiSr1WT-;dUAXOd?=*JA99F*rd@?5IOzyD&%8~GUwvOeUuvCEd_ggj~Bbo zYqK4}ig;hYK@Th`Ltn?6b^WrvK0rLl8SiI*ajiX{_`zMha@^o%J-8_Ty`j!icW0Ad zLMuLr-QFin7Z`0d8V}!reQlnGg^OSM2`sIui0U23#jcat2(PDxUfN^}7ArPmuh1UI z<3|Ca)$)%>W(4s>HF<lZ?r~OXs%VfbVmv!377u^aM3L0i$ce0)$O0l9A;jZstMK_p zH|=Y;pUmcv5>=4p>!IG=-5wzr?QvaQfoIsIKBM`qA0S03i<}8T@*M;;jz)Pd=0sJE zzBkw&X%O4Ku=3Jt-w3d7^=Kx{FN($z!N?!%nO}C7FL5!yMVMN|%(gn6(DI{4ziLa+ z=C|K-W`q`4XDmN2&R~NXnmr<gE$5b}4LWTcXN@5*HYy+gBqD5@W~rWwSP{CyhP??7 z+x%j?ix0}5>Q1fC=o!Sfi`N@0)S1~4;V#17T=tu3czh#LR?zX<6_%vRQMJVFQt)Fo z+b*7d&-gmr!IXDpdr{~3r!IZCpw8}blN+>MH&JlO?W}*%^tn4!tXq_?T+XTKwa4<Z zkiX}~b2n=KK@9JX!B@2?SnzMy#J!W1dsjcOKG^p^zsqArCHTdak&D=|HZw0v?3bAO zG6;LR=Q`A#qMZ{_+QZAy9!3KU(|vW5S^hppgnT3MuNPBY8UO-T`)CN&kx6o0=tOc4 z(nzrBS2@P-Fy3-=jV|HDvm7~%fk~Y2H(*3x_Xsw?Z4Ayz!M(_6-z3Ic(I_*on4U-C z7We2T2?S}?lnv-J7!Oygq*%NQ>Z}f6-`vqedr3}I%*#(b-_!`avEgX5b2Xq{+I--~ z6^*F(GJ)G<57(%K={|2PT1<ipR1)=?!3cjaI$e1^X2gA3&tb1Ar{(cAZfZtd8VZ!c zZ+pMyN?7{5Xb}rHTlk?)_u3kg*D*4M9JZY|XmGo&Sz{>EV1V&yF6T+pA~c(w$`GbP zHVT3#KSyfcC&ts`21i>x=SifdmfINClff4tN<FlI$EIf86faJujgc`!3fd@_EhOcD zqsm2}^QBI2iuwHN2r<X{+fDZ90VH1g&8P;Dcg1TtLP>CklanAB+X6|8$7UlX+a@ds zN)v%GOO_&jng>BTj?}$XvNhYy{3Cjk;g^AalTrHI?04t%>x=@o>XzG<3J0n2VYPO$ z=)jYUSxuu?w{j=xcqoz^^Y%Xd>@dG26}GOkJFym5bd+5>nmkS{>KPGj8^rU*INq0P z`a9n-&#t_C&Wiab!Hbh%a^2@CtuN+IEfYaYi@`2F3(CB%<1@uCCuyjm-Xlg?YbXZ? zU7yUQoi0<~Z~2~Z`Iy$*>7*wroL_5_k`Q_%k6O?2rxJ!OlWH3)4PadO$ArowsnbmJ zQ*4gODK6gW4#sxGFfN3ChJMi(*Rbt!yx!p1k`r%zdm{RWl<QlZ+~!VEV=)LOO*IhQ zY9cdT=@!DP8ALhyStS3N0+|o11wZno814tmkOg<t-yJt9PmThgYa7i#;Vt1VB+JOC zexpc)c~zZEg<f%`L=^WZS)5)q`?UVe;n#%)PK_E{8qhc1>XS5M?jo_I%_-12wX9L& zOrh2Zv#dIY!@|;>*Hz1$^A<h^MMdGG6D-nJKc$9Q|A@Fk%epnm8$Gtv&5e53KNN9Q z!_uSNY4~9YtR><u0f|lfGtKZM0(iC(q06TRzXD7~V1IV?#*hAY5SO^Sq}rX`zgFqg zgHw)T^CW+)#mT*0Jo`ZuQC)sF<#2+rpcI)|ynq-iE3gbNG!skIow5HSt6Op4^!EDc z9a4onamNd*+AZ6_`OW9kXr^z|JCAeutRC_4qT`Zw?j6JoF$me~^>kN{$~&cFU#!dT zpZ32I5A$GEu{+k>q+<bv$py2)dsU?$t~}#Bsabn=cf0{wVMZjg6sv(ZDIhfx$~2f- zo<1WGnPnb}cv!Q_GqF(7RSoiBO9<1c-Sai-u{(Gvn-zR#dp5YY*U(q>?t*s`YrVxC zyBl7dVT$h#l{yYh4O2f3^YRS2o4HhlkmNo}(&S6MpIxh?#`z9#E8-W6aD%2cOmA_= zM-a`swm&}j%&b`VrBL%#q_B0z9>!dt^BU7FmJgv>!aQuEVDL9k{if3&Rp8-OS=LID zM}S&gn-mNDO}UpzH_QoXG34$|I_M1-`sDkkG0>-|<)K|-i;1Xk=z9+1DzrwULOJSD z5a`fbA8d`-O)lg_4rt_qUA6;x%hOfEDAkP$*N~43%pHp@_%dT*rkZ(i8kGI;knoix zQQ}_n=gpT%oyW_6uI~VO!M|RbSF6TqpM8m8kfRT(P(foa-#I8M69N!QEn`DrEsLbs z51n#RZ)kYgjH~2eZqkKrCuijQmeD@p80$#12%fW|nu23Z@wszE96oDUlPwe5Wkdl) zyh{9|BL{elaNFO3|JI$EQgB&;s>m<XSxmC>m=I$AOLhehq<0uXHB$IH;vDLMPn_vN zy_=3C(#t0jPaeW{xl#|gDUC$Gu1`Sw()XrwG~w+TnCAV_H%+DLns6v97)p=1o#>#> zS>?Kt{GB-QT4DENTmcbRYuDA}IjzzhvtL*g0|Sh<{3|Obh>IwwmHe6PV42u(S!n8s zxK$YNja>yn6|bF@KCYe7cuO$0FH-sIU>e+rS+G~oMc6>yV!EYDnNpvd>qScL##mSN z&+z_sb@ssQEM}W+<4<{oR6a#w%A@ED=VlDg@5>a~#oyts{1R`A>f<sdK4bSf^NHJD zsfQK43OR+W@)*>YMOa+btg#9b>v>n<-);o;-!A~``joSmSs}8UG{5z|hM|adsCoz7 zI!KR>#KW5VrI`!U883eFSyiM*S;yl`Xbl~`F8jogr{cZX%2BRuZdiP7Q?xW4z3bp_ zN9#gDML2eYDtXD7BP{UVRwkc12MHUTywN)IC3_2ru2%gN2JtE5*>K3-UIX*>v$sqk z{Dbc2Xqiba25#(i=r>sqIUCyty~;i-Oa@0@FqH^P<PvS4O9{}y1r*8Jmr|~CnlZt* zr%TzYIVp(xLG*B0<C}MT^GJdsi7Ail>X6_k%$<Ci_daRmP(vL?KYH{nC+ND_(=OZ8 z?RWUh<PU0!*4FXw?0$@r(De9K|J@htKh+Y&VqwCEmr&VTbHQgq(YAvoU?+x@dw%L^ zv;dU6NzGkU-goSA#YjO$#d%av0OZh-U(4lVLFFXp*K*~@rM6_aqf}ARNgBV%Do0dj zr77yAOnur~&<EnpK|m+N!e=GyZpUQA_hD#c9FyP&rHTf5tG^FZw52EjVb}iXE^vD& zR6VZRG3E_TFv7G-PQs3-xRN)Dy}Dz+-uB=_!MpB`>tS9U1!;z(wz>6N98Z*bzhsmJ zZczs%@)VM=yXL+rJG9<fG;3A89vRy>c@UDREBdAD!j^({!8`(mfQ)$5(iQDiM-OGe zC`)}<G7^v7jevIy>$EBvj!qJ3vmO&}Rc#klAM%w>h0tcVbj=xn_a9a_HHgT`H|oaB z=YNcqn(_(WmUcGt45~mwf8Y&DTsnJdt=vm&oV>VZiN4KAlPUPN<<%FNU#zSIml~YL zb9Tw})h9$WX5**=)-WoxwLfKZU)|R<S6wyhnTN`xvmX?u4l_--h&3}q$Nls<t!YC? zXgU}Rot4~qjk4@R1-1mW;NJVXIi5l!kB$lGJ=DUYWpt<?mwpes$mV2hT}yB;aF5wb zdYDe#RBK3ZkOc!}w)3pkSKKrd@+%M8)Ll@I>8B7#Ko9QZEm@DNOR&1C5{mi}E3HSz z_;pC-p=0Hiqyt`>3uwQ6y4NpJ&Chc>r%zVy5oIv6Xf}fvWf2<;wiWRVE<9$ggf8B= gmDHtvN}cDCqAKF<0a+$?5x+?`8dJRcf}i@o0H*sZg#Z8m literal 0 HcmV?d00001 diff --git a/taglib/tests/data/ilst-is-last.m4a b/taglib/tests/data/ilst-is-last.m4a new file mode 100644 index 0000000000000000000000000000000000000000..c56c80498027ecb7113d0ec56042277233361ccd GIT binary patch literal 32768 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/mac-390-hdr.ape b/taglib/tests/data/mac-390-hdr.ape new file mode 100644 index 0000000000000000000000000000000000000000..c703e2e2fd595469973c4162cfbebcc0722ab829 GIT binary patch literal 128 zcmeZubXKt8U%)TMz{KFPhJitc0Rp&yR9*@L!%cMt1`s>Q)6MOUpcX^8W0-4NZi#{b zP>d0(M_?lp0}F!yLrP*v;trsi8lap{9TP*cE-ORIPIiXhwp<L~F7Yr#X7e*VV;5q0 QIah=s$V;5zOy1sd0HO&R=>Px# literal 0 HcmV?d00001 diff --git a/taglib/tests/data/mac-396.ape b/taglib/tests/data/mac-396.ape new file mode 100644 index 0000000000000000000000000000000000000000..fa7ae4149460cc0a94174a1e3ce72fa4f1106138 GIT binary patch literal 104 zcmeZubXKU~zrZfWz{KFPhJitc0Rk9-)PX7{FbBjA@^o`kdCkcX?il8pmRq7A02E_{ h>Jiw;#K6KJz>tzylE?s569JSH7VO>34%f}V003n#561ui literal 0 HcmV?d00001 diff --git a/taglib/tests/data/mac-399.ape b/taglib/tests/data/mac-399.ape new file mode 100644 index 0000000000000000000000000000000000000000..ae895ba21cb8d6ca9561db087d4373d976503e3e GIT binary patch literal 172 zcmeZubXJ(g&%j{9z`!5@#5_Q(1H>9Y3<JOCbl$m`R%)zJn7KqqYVpYn>_8a>Mur1c z3=GUbrT_yIgUcEQh87@uA`s65;vi2qH<j0%4B?Jpu4%a?3IaeeMyNW0jZ6$IKwT+` iC5a3`H9bi(y&xk&Du7H7qX)=jpYR>TWVi@qf*Alqb{*XS literal 0 HcmV?d00001 diff --git a/taglib/tests/data/mpeg2.mp3 b/taglib/tests/data/mpeg2.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..13e8d53df6f32dc924cf5b153eb62b92fc13e65b GIT binary patch literal 16384 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 AumAu6 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/multiple-vc.flac b/taglib/tests/data/multiple-vc.flac new file mode 100644 index 0000000000000000000000000000000000000000..93d9a8a14a2eb60b75cbb1be5c7975a095407b18 GIT binary patch literal 4754 zcmeI$%PWLY9LDi8jLQr&USo`5#&u@nGP#vYk!EC~rrc(MTpAkESO~c-l#s&SWrvl8 zVkZfOg(N#EJ3D28gq86=9=861bL#Z=>381Eclqo01!{#5IVNFJKSm)G{ki8^L`<J< z?z=zwALlpD?v<h~ebt0k2%GvuXw;jaUU!SXq19jQ9vK@P9m^{Ie_S#vlmh%Mk1QYy z$O5u}EFcTW0<wTCAPdL>vVbfg3&;Y01;U^0RuQ6<B}g+wEry|pAaxvsB73P^1=3DX z*UeDWFxBCNqNk|7V<={Zdb@&RTdDj?Nav@He2~72TDXPcIw;dN6yHd_KR||hDzFL} z-Bg<aO7K!QlaTVWPZ46^2}-nc_Ph?6lBvr`C<)6M^>mLz<{0X*9Qu7ITpq}x<IH>q zrKk%XoVi`d>YzG%pj0Pyl?SD{sMkHnmPZ9IAbSlpI}fE-P?i@cqnIiTL78RLK?any dOnrHw>=kOT6v|nkns&mUQ&xxeWQ}gC`vy%NpK<^I literal 0 HcmV?d00001 diff --git a/taglib/tests/data/no-extension b/taglib/tests/data/no-extension new file mode 100644 index 0000000000000000000000000000000000000000..65f57c2ee985713476ac0b6e3483e6fe472e2176 GIT binary patch literal 256 LcmZQz7})>-0RR92 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/no-tags.3g2 b/taglib/tests/data/no-tags.3g2 new file mode 100644 index 0000000000000000000000000000000000000000..d31a6ce96d7cb701216e29bf17e15903e3fca5c6 GIT binary patch literal 68335 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%0DOmT!Moqh7w1Y^2~HTN>C$n{#(z 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 AGXMYp literal 0 HcmV?d00001 diff --git a/taglib/tests/data/no-tags.flac b/taglib/tests/data/no-tags.flac new file mode 100644 index 0000000000000000000000000000000000000000..41714416760792085717cb85fa1542e5a7e28981 GIT binary patch literal 4692 zcmeI$&nv@m9LMpGZGLQwubCN}`LSJ$g_Iwqw8?T<Ar7<rXtYTjoG=HqY$@FRxZ&iW zxJep?gO=Q++}xDINbF?x>Lq{0=Y9F~=<_~3kMFQA*eisnHwZ)aDHcM}-FQ+$wQ{w8 z>i!PD#`dnCm6iit_fk|SP1zMWvVbfg3&;YpfGi*j$O5u}EFcTW0<wTCAPf8#$fQTj zVvbZcpqzPXJp-viR3Zear>F)6(kxN;Ls0Gl6>va#5o$UC<*!noNvL3$YV3lvBh-Zt z(v4H`N2oAB84jVM0qW}o()UxrZK&8y`SnnVmwH%+6bm);29;W=)GlNwr|#5H8O|%& znOKC3`P6v_^tUKn9>}ESpYaK*$ev^{YsZk;PK`}Ml@2QDgsNQB#|dO{QlVSO+CxQS vP<1C|dWUSSRP!8E(@ve)pxRC9#|zbMQL}AOeViIR%A_M^yXJC-_V@h)*-@2S literal 0 HcmV?d00001 diff --git a/taglib/tests/data/no-tags.m4a b/taglib/tests/data/no-tags.m4a new file mode 100644 index 0000000000000000000000000000000000000000..ba4e92baef225c0204347186c4aaf42e0259112f GIT binary patch literal 2898 zcmeHJJ#W)s5Psq~5L6^AKGYUEIFx|_Arblmn$m(QRYIyNu^?4WUWZ!sTL-^yD#1`? z?^3DV=~{_Dz<|icFJORynIF(SJZHO2S|L^#{3zDlJ+FOtKHZ%IV5V!|^>lBc2|{FK z5)IToX%*O?E)YlUZV+_*Cy$#8z!DC9w}}@GtR6VCm>-l%S%k6^$|)$P;D49`?(n)S zZZ7b;PLf^PvEJQMvfPKKE8mBwYhO^=JV0&w7Alb67#9rEm+Zyyoqy6Ck~2}Fj2wD{ z!Qd)hjz%{|Bfzyb4Yo<Yw%b`Ql{}Y~ALQ~VlFtM(@>2Z2=i_=>MFCy-T1~#M#Q3-j zXKz@zZ)~YJJqu-${?3_R$m&-e?R05)M(2)-g9j?-=TsWXxj<69-cMsc*Q@)+wqxpV zj5S%&a~`kwWtHRv<JWTF3BmLPPeYRzugc=xC*e}wUc3X`WsqOMRJ%sTx#?Y@s7Sqi zV{KM%>SBN3LlhFi8S*+}8t;WDa7;;OxK>_e4gOINJY-LH7+WAcE93B}kkY(!h0fmY zB-91M`@HotP+g_@P3+Fa-lw0e_c;R@>V0JcYHzz~7*5VS_e2`>dNQWxSc_uArHRIg nO(f_ncc?r}r@-@8>sj&l7xO2r^$l|OCU4jXjrCipjg0*b4AhjE literal 0 HcmV?d00001 diff --git a/taglib/tests/data/no_length.wv b/taglib/tests/data/no_length.wv new file mode 100644 index 0000000000000000000000000000000000000000..c06d1071d5ac76d9be9d6eff977a5f8cd4c82863 GIT binary patch literal 532 zcmXRfE6C1ZU|?WpVPN<V1VAQ(QWyh+qC^ABdZ+S_ief>YZf+_+IT^wo!(7vHOB4iv zDi|4<7+lscFbHg9VqjqqU`R<UNn`-3VPaqg@>$tHroccd6NuFXB>(^Kh$sizX9DDd z>~pAN0^5abA2t(k=>@rfT)k;bjAZNWh#+6@s&(Y+J;TDpu*(J#H}CIsO5qHv`jVgM K;oRm(yj}nfOJeE( literal 0 HcmV?d00001 diff --git a/taglib/tests/data/noise.aif b/taglib/tests/data/noise.aif new file mode 100644 index 0000000000000000000000000000000000000000..310b995e3c64878ca14d096b8e219f59389e6ef9 GIT binary patch literal 4400 zcmZ?s5AtPT5L9>cbaQj|_XV;UgculsWGaJ%1K%1KAPWfGe0+i!82ArCc%$TK2#kin zXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQQzsf#6_2V4t4R9Og=<u37ARma=Fp z{r|b*Yx0M`OLf`XqgZ_&au@QmO@GKX&0C_whUKRX@6{-2M_q2#X&izT3_(%<Y#7-8 zd|=+g|7&mNpR?0`w=Dh1eV93d|5x^<-`(w>v!*?M<o(ok%lnwaZ>Bu_cG%{JsSW!C zT}EbahC3BZKKwj0_&NXcGl(r^iH`c+{NdX@hCeqieQ&j4Ui^?<b_>7xLvG;;9x)r9 ztO_m#Z}I$guEn~%jO}di4~t7zaP9iQuHpT=j{nc^rECfh*#h{5PBQ$;KFoGim$AH^ zdD1j?&Zu8*TYf%S`b+Z?vr!b6l{aIl&5s+G{)O0l)|30SLhk33Ex&f@GBz${`}l!z z*N1P`6%3O6e^sV&>OSP+l@pvX?Z46`4woolcklnjx~%CBzp!O~dzSet@-T~~Cj$T^ C(vHsn literal 0 HcmV?d00001 diff --git a/taglib/tests/data/noise_odd.aif b/taglib/tests/data/noise_odd.aif new file mode 100644 index 0000000000000000000000000000000000000000..bccfd7283c4be54cb38d869161148a620cc0647b GIT binary patch literal 4399 zcmZ?s5AtPT5L9>cbaQj|_XV;UgculsWGaJ%1K%1KAPWfGe0+i!82ArCc%$TK2#kin zXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQQzsf#6_2V4t4R9Og=<u37ARma=Fp z{r|b*Yx0M`OLf`XqgZ_&au@QmO@GKX&0C_whUKRX@6{-2M_q2#X&izT3_(%<Y#7-8 zd|=+g|7&mNpR?0`w=Dh1eV93d|5x^<-`(w>v!*?M<o(ok%lnwaZ>Bu_cG%{JsSW!C zT}EbahC3BZKKwj0_&NXcGl(r^iH`c+{NdX@hCeqieQ&j4Ui^?<b_>7xLvG;;9x)r9 ztO_m#Z}I$guEn~%jO}di4~t7zaP9iQuHpT=j{nc^rECfh*#h{5PBQ$;KFoGim$AH^ zdD1j?&Zu8*TYf%S`b+Z?vr!b6l{aIl&5s+G{)O0l)|30SLhk33Ex&f@GBz${`}l!z z*N1P`6%3O6e^sV&>OSP+l@pvX?Z46`4woolcklnjx~%CBzp!O~dzSet@-T~~Cjgc7 Bj?Vx9 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/rare_frames.mp3 b/taglib/tests/data/rare_frames.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e485337f9e489dea7fecc899c7418cadc923c4b4 GIT binary patch literal 8320 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 N691C_K>+}&{{TD^OUM8K literal 0 HcmV?d00001 diff --git a/taglib/tests/data/silence-1.wma b/taglib/tests/data/silence-1.wma new file mode 100644 index 0000000000000000000000000000000000000000..e06f9176648836ab73928f0e15af92abd5c82f8c GIT binary patch literal 35416 zcmeI*U2Icj7zgnG>1H=(g&i@v5Dd+t1e`W(kcGvMY<7&vhmhfvIB*UrvIw%Q8zV>} zOfZXlh+a4_MsZHvT2QiJNYDfmBXL`#V~Pm|k$@p;Oi*LyoEZJT=bQ#M>y;Pl#s75Y z^z=O+@7v$$#gnJcS!Ye}Y2LFYVZU<*F|0bdZqsBqQx=z&Z}yn2bKH%{fzNw1A%ZBH zyms^S=GpkJ`P~V7&v*DZ5L^~n>un1Ub^q}0lDikQ9y~m=aY5ec<uAA!_LhX&OaC}w zUGMF8I59cHtR5J+$7i17*O?{QAJCWM&WEl5A7mV_4Viu}y8V2e>+A-5@Qe5{(|*Zq z_v0Y9bXSD8oWFc!Vb9Ex`_7fE*b#Df=2`twJjEA*2%g4WXy&^CR_^>|`u2H0&3~hI zVq{uoRqvGK-qw%(!=Jui(^9$p$=I&)kwtx*lUw*~A>gP05AZAx<9<|mTP+wlKj(6K zZA3ku;By!o*ykE98?hdBTsN%`abAxGtYeGO1s0)*%cd{=dEfR;nAdsi)p-9avF{ru zu~Cx*Bp?9^NI(J-kbndvAOQ(TKmz|up#SYlH#MFp3O~2MyS~a_^XT&bI9v9})f~mO z>v85POwPjMBh-=1AOOL0u`{=9d2Pu@Gj>#ev+qJj{=d#!+Bq<ob#c&c{}~5*AM0A$ z>Ftq$sdxm7QGsR5BP?WwA;Qc-l-UH6M=;Y=6k--aoUbltqnO!;Vs4!>>gWe~bOfuN z*&KVbcZ@mOJ%c$56C<Ot89rp1ol#nK>iJs>2kf^xImV32q4%6tplnRL#daU^*mFI{ zxSAtv;fRBL7pCue<~V{VW7fs2Z{T_f`!3`$w{hK!JILhBDrQ5XxRdL1FxP3R#AfHU z$vhc@h;wWa^EVAVW7O#(?B!p4sIy+mI(zi!H94V1Z?vVa*AFkQN!VR~VQcM*@`9zG ze{%fAx`e&6huN?r7lPhybOo|GTlj3~*UUQ0hpiv-y-wqm&&g#r3~j(99J+&h#B<^~ z7I3n8_?g`mr|jLWc)Y#0qbGSP^Jy^ItKu=4weuU@zQ$(Bl36j!YUL1CI{4KV2f!)Y ze8fz8Yyyz=?C-Y+^Ya6jfBW<0y|(Rd8M*gJTd*}d$ip;AKmrnwfCMBU0SQQ8JO$iH zwwh5gBU$IWUMG@0&F@F^fXv)7{*CAO)VBmAAOQ(TKmrnwfCN$txRGp(*;q!hPHxtT zWDA%po}oxKwV7HaAOQ(TKmrnwfCSP(z>Q@8W;T|QtaHbW6UlC4X~A4YvgvSzGzJMs zKmrnwfCMBUfz$$Cbpc~oMzZFXYqw7JE7r<ZDw0iYrdA0^KmrnwfCMBUfpie?stXvy zGLkj_kjaf?3jzRdMs(|Bb;nmaTq%t~0uqpb1SB8<2}oeHfLC3>7?zPNi(|b=cH0fW z+GHd<`Ut&}fCMBU0SQPz0uqowItX~x1uTnYB+F&8x?m!(Ns(+iTp^7?0uqpb1SB8< z2}mHdfLC3>vRFp4Tqdgv_+v?JieyupsZ|0JkbndvAOQ(TARPp}>H_{$0wY=H2NT@? zU0?(I6v?K;719_aAOQ(TKmrnwfCN$tc+~|gi)AFs+q{z11vdg+$w*dre5JNis{|w< P0SQPz0uqqGcnbUjXqG|4 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/silence-44-s.flac b/taglib/tests/data/silence-44-s.flac new file mode 100644 index 0000000000000000000000000000000000000000..24e15deb8c85ef82baf22a231901d646397dd8c4 GIT binary patch literal 50904 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/stripped.xm b/taglib/tests/data/stripped.xm new file mode 100644 index 0000000000000000000000000000000000000000..57055f5f192d5858341a8a9235d0f642481d7180 GIT binary patch literal 602 zcmZQzKm#S2B{``I`DqHp`FZJRa%fypAfFM4Ilv?%0~<pv0|O^Wkplw*Q^P12(IEf; DpT-63 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/sv4_header.mpc b/taglib/tests/data/sv4_header.mpc new file mode 100644 index 0000000000000000000000000000000000000000..214f7ac4eb710e8e21de093ee7959dab8fe86bca GIT binary patch literal 128 zcmV-`0Du3%Cm;Z)0P+K!E+_X8&%*;!>;D5wsil<Pw|`SHW?4+jVwPoDmSr)-7(xuW zvBWGKXl@{?UfGo!nyhFu>6)pQDvY{}wzBH4T&wTPvrFTA(>cv{&D-_Dah=cBT5|n@ iE3UO<#~#&>*ohqgTB!C%gDl*OOm-ISqH9VS7Q>v%RY5rb literal 0 HcmV?d00001 diff --git a/taglib/tests/data/sv5_header.mpc b/taglib/tests/data/sv5_header.mpc new file mode 100644 index 0000000000000000000000000000000000000000..6d17e65f0f865303397cdb08632cca5a2aa0365a GIT binary patch literal 128 zcmV-`0Du3%FCYN&0{{RVcBcU78pg2y2S5XsQc5XpYAK~=`TMqG7PBld%R&|wvn-27 z)(~SG+b-oV^~l!Gc|2h@R~)T+MB#OHHdkBzU)gt`zIpg}+1_WT^O~=6<x2Zku3Wju i%9txZJ@=^oqLdzMqz%Mg>6~40@oh*DH|+sQguls;y*_RL literal 0 HcmV?d00001 diff --git a/taglib/tests/data/sv8_header.mpc b/taglib/tests/data/sv8_header.mpc new file mode 100644 index 0000000000000000000000000000000000000000..3405545a29460883251a5ccd6135ae440bca5815 GIT binary patch literal 114 zcmeYbaP|)N;Crv7*~rn-9LK;a8RX8x=vo_f28=z~0~zHQg8eyM7=XYrfJKH(ED#*R L#wc_k*p(Ro^T!wZ literal 0 HcmV?d00001 diff --git a/taglib/tests/data/test.it b/taglib/tests/data/test.it new file mode 100644 index 0000000000000000000000000000000000000000..379444b91869139a9ca1b73d5b2b9bed98370e9f GIT binary patch literal 644 zcmebD4e%{VEiO?g&d*C%$V<#kWk3Qf0!%<2D+42gIFkqyCj%Qpg8^G@15|}y3da)# z3V{QK;Q#+GK-ai1GB8X5;wM0C0&+4CFf&}Yi&S_j#sH*WDj9Qv==Zx`3otNv`UV6; z)IdoG28WQ0%wh!~N>nIL%q_@C1qX#5iYhQ?wE{w{1!@yE^E~qua`RJ4fd-{zf{6U0 z+{BV%gkBhnSo4rv1$1OlszPE?X>n=_%osRLta*vW3b~~@C7C&yd8rD?`MJ5Nc_qaZ O_*Vh$0{jeY?gId>FJKJ- literal 0 HcmV?d00001 diff --git a/taglib/tests/data/test.mod b/taglib/tests/data/test.mod new file mode 100644 index 0000000000000000000000000000000000000000..136b6119187bf574bcfdca2c4a4e9155880d553a GIT binary patch literal 3132 zcmeH@u?hk)3_#mI$u2rO9=Pb@KU6O#1)D<C&i=Z&DA?il4km>V@)AN{jrEC;7vM|} z_lXN}EiEBcppcNiZRaMbd?*Ov));39KO{uyJ*Rb%A(FJZn56b8&rQxPaZS0TQ2C%U uO82|1CtRt{Z6%|<6|?aNHvFsq<cRusI$uZJwVP(Z4445kU<S;<SOae~S1CaN literal 0 HcmV?d00001 diff --git a/taglib/tests/data/test.ogg b/taglib/tests/data/test.ogg new file mode 100644 index 0000000000000000000000000000000000000000..220f76f0cef41c7e50736c9134ffb20f0d297961 GIT binary patch literal 4408 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/test.s3m b/taglib/tests/data/test.s3m new file mode 100644 index 0000000000000000000000000000000000000000..668250bb7876f31762bb3c2d4a2cafb93cff2f9c GIT binary patch literal 544 zcmXR(EiO?g&d*C%$V<#kWk3N^0t^gH46F={Kv4x@CWc_=AYTWzS_6SUNXj@EIhnYa zxmkEvdD-~b`Tt`8P6j>(VFn2XStL!km<<iMWI*B$K)hN3NFafbjLc#MAWF<r$jmD) zDJsoP%_{->MGr|8cBTVR7~MSI{FKt1RE4xm5RqS$n^;neT{9L@Z002<l@_O>JB>&q lv6@$`kXxEll9`j4m#UDQp9}JCF=4NQU5m{;1+WZm0sx-yM0EfF literal 0 HcmV?d00001 diff --git a/taglib/tests/data/test.xm b/taglib/tests/data/test.xm new file mode 100644 index 0000000000000000000000000000000000000000..b09d91324bb3e6edcdae4f659965e1a87b5e1954 GIT binary patch literal 5471 zcmeH}Jx{|h5QZ-i;$vq*Lb?$N!4F`{(t)89`~lNA$f`do+mT>E^q<2yPHTn$T@XX= z68kLqJowT3+FrawX<OOi(zU&ocNkUF5?zPE*=NXcu~d5f`sl@KEj^BLRGn7<^usCj zTWZJfHZGWNI!jw`0O~ErtO=ZuGwyAOzBkfFw8F?R+M<T5arC#T0ZdXnIFRb`;Oe>_ z+)vv~@$sJXPkjS)Ou&OM8!dmY`%0`-C#=Ft6i<DiQ%mfaQbjdutO-hRMn+YH29R*A zoJkT4sBGa#C)B)g>ENbBR0uVLT6gen%LI)sshQ!z#7@z5**R}S47n&x-ds<8<4Q`? tkyd^iJV|I60z=3SVI{B<SP7W`D}j~3O4t)%JFp%0V&HcI+u{H0@Cp8IS;+tZ literal 0 HcmV?d00001 diff --git a/taglib/tests/data/unsupported-extension.xxx b/taglib/tests/data/unsupported-extension.xxx new file mode 100644 index 0000000000000000000000000000000000000000..65f57c2ee985713476ac0b6e3483e6fe472e2176 GIT binary patch literal 256 LcmZQz7})>-0RR92 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/unsynch.id3 b/taglib/tests/data/unsynch.id3 new file mode 100644 index 0000000000000000000000000000000000000000..cfe6ee1a6f13da6901627a4543f7849dc2d99ed9 GIT binary patch literal 320 zcmeZtF=l3HU|?W02=NRtVqjn}1+xGBXJGJUsAN!JNMc9?;#43ji=mXEn4tv7N(PD* z0mX`e>@<daAX|YUmmxJIz||0_UlOX{k0FyG52#XsAsEQY1**#fs&Vvj0;&;(sxf3R z076xu8ZV&ADu$|%AZKr&JUdk0fWbJ#$JGxg!tVfal@Ws>gBe(t!T;YAco_IU9AokT z$}`9?cnSb1W(H0j5I2rd^#G9ij|`svU-M@HkGx&2`|172;{QKAZewCMWnwU>eR}Y_ dH>%vO**}Anyl%v>9odbd=d!cx(>YD)697C+bpQYW literal 0 HcmV?d00001 diff --git a/taglib/tests/data/w000.mp3 b/taglib/tests/data/w000.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f9c2261764526ff40aba618a1e9b9933acada5fd GIT binary patch literal 512 zcmdUqQA@)x6ore5`rwm4K<|s750-TE!M*9)BBSeCW@Pvz=}fz@G#PD5?Z5XFd=USE z2R@Q>a&K<3h^EILbb1qJX^PDTNK<)vV{1#9(F5I6-}nAu3@p6AwKcLxwXRV`=ae;M zOL9BaQmM2gtmDzvp%+~{RVXJveLl~}0Vg1sJn}GG{kYrd9gkNb+$H!mI5SISBwz3e z@(Yj{+bdJiyX|-yM@Y^9V-IXL<9QswT!UBHRLate&~^}pN}sr>8$pSbMi`#I%~5m? z*0v~=X*Y~9xGT@o-Cnf578>b(c#>bDK4>}q_ygkWFhSiB2>A1IyyefUko+%w1F}q5 ADgXcg literal 0 HcmV?d00001 diff --git a/taglib/tests/data/xing.mp3 b/taglib/tests/data/xing.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0c880151b6b60b5f5ac4823a064320710b687449 GIT binary patch literal 8208 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 literal 0 HcmV?d00001 diff --git a/taglib/tests/data/zero-size-chunk.wav b/taglib/tests/data/zero-size-chunk.wav new file mode 100644 index 0000000000000000000000000000000000000000..8517e797dc803bfbcea502588f70d68e643a6aad GIT binary patch literal 1024 zcmWIYbaUfiU|<M$40BD(Em05vvKbke7+lscFbHg9VqjqqU`R<UNdyXyg3%Bd4S``6 F0sw5+2R;A* literal 0 HcmV?d00001 diff --git a/taglib/tests/main.cpp b/taglib/tests/main.cpp new file mode 100644 index 0000000..ab89dc3 --- /dev/null +++ b/taglib/tests/main.cpp @@ -0,0 +1,57 @@ +#include <stdlib.h> +#include <string.h> +#include <fstream> +#include <stdexcept> +#include <cppunit/TestResult.h> +#include <cppunit/TestResultCollector.h> +#include <cppunit/TestRunner.h> +#include <cppunit/extensions/TestFactoryRegistry.h> +#include <cppunit/BriefTestProgressListener.h> +#include <cppunit/CompilerOutputter.h> +#include <cppunit/XmlOutputter.h> + +int main(int argc, char* argv[]) +{ + std::string testPath = (argc > 1) ? std::string(argv[1]) : ""; + + // Create the event manager and test controller + CppUnit::TestResult controller; + + // Add a listener that colllects test result + CppUnit::TestResultCollector result; + controller.addListener(&result); + + // Add a listener that print dots as test run. + CppUnit::BriefTestProgressListener progress; + controller.addListener(&progress); + + // Add the top suite to the test runner + CppUnit::TestRunner runner; + runner.addTest(CppUnit::TestFactoryRegistry::getRegistry().makeTest()); + + try { + std::cout << "Running " << testPath; + runner.run(controller, testPath); + + std::cerr << std::endl; + + // Print test in a compiler compatible format. + CppUnit::CompilerOutputter outputter(&result, std::cerr); + outputter.write(); + + char *xml = ::getenv("CPPUNIT_XML"); + if(xml && !::strcmp(xml, "1")) { + std::ofstream xmlfileout("cpptestresults.xml"); + CppUnit::XmlOutputter xmlout(&result, xmlfileout); + xmlout.write(); + } + } + catch(std::invalid_argument &e){ + std::cerr << std::endl + << "ERROR: " << e.what() + << std::endl; + return 0; + } + + return result.wasSuccessful() ? 0 : 1; +} diff --git a/taglib/tests/test_aiff.cpp b/taglib/tests/test_aiff.cpp new file mode 100644 index 0000000..df1c5ac --- /dev/null +++ b/taglib/tests/test_aiff.cpp @@ -0,0 +1,31 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tbytevectorlist.h> +#include <aifffile.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestAIFF : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestAIFF); + CPPUNIT_TEST(testReading); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testReading() + { + ScopedFileCopy copy("empty", ".aiff"); + string filename = copy.fileName(); + + RIFF::AIFF::File *f = new RIFF::AIFF::File(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(705, f->audioProperties()->bitrate()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestAIFF); diff --git a/taglib/tests/test_ape.cpp b/taglib/tests/test_ape.cpp new file mode 100644 index 0000000..c95ff0c --- /dev/null +++ b/taglib/tests/test_ape.cpp @@ -0,0 +1,52 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <apefile.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestAPE : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestAPE); + CPPUNIT_TEST(testProperties399); + CPPUNIT_TEST(testProperties396); + CPPUNIT_TEST(testProperties390); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testProperties399() + { + APE::File f(TEST_FILE_PATH_C("mac-399.ape")); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + } + + void testProperties396() + { + APE::File f(TEST_FILE_PATH_C("mac-396.ape")); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + } + + void testProperties390() + { + APE::File f(TEST_FILE_PATH_C("mac-390-hdr.ape")); + CPPUNIT_ASSERT_EQUAL(15, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestAPE); diff --git a/taglib/tests/test_apetag.cpp b/taglib/tests/test_apetag.cpp new file mode 100644 index 0000000..845828f --- /dev/null +++ b/taglib/tests/test_apetag.cpp @@ -0,0 +1,122 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <tpropertymap.h> +#include <apetag.h> +#include <tdebug.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestAPETag : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestAPETag); + CPPUNIT_TEST(testIsEmpty); + CPPUNIT_TEST(testIsEmpty2); + CPPUNIT_TEST(testPropertyInterface1); + CPPUNIT_TEST(testPropertyInterface2); + CPPUNIT_TEST(testInvalidKeys); + CPPUNIT_TEST(testTextBinary); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testIsEmpty() + { + APE::Tag tag; + CPPUNIT_ASSERT(tag.isEmpty()); + tag.addValue("COMPOSER", "Mike Oldfield"); + CPPUNIT_ASSERT(!tag.isEmpty()); + } + + void testIsEmpty2() + { + APE::Tag tag; + CPPUNIT_ASSERT(tag.isEmpty()); + tag.setArtist("Mike Oldfield"); + CPPUNIT_ASSERT(!tag.isEmpty()); + } + + void testPropertyInterface1() + { + APE::Tag tag; + PropertyMap dict = tag.properties(); + CPPUNIT_ASSERT(dict.isEmpty()); + dict["ARTIST"] = String("artist 1"); + dict["ARTIST"].append("artist 2"); + dict["TRACKNUMBER"].append("17"); + tag.setProperties(dict); + CPPUNIT_ASSERT_EQUAL(String("17"), tag.itemListMap()["TRACK"].values()[0]); + CPPUNIT_ASSERT_EQUAL(2u, tag.itemListMap()["ARTIST"].values().size()); + CPPUNIT_ASSERT_EQUAL(String("artist 1"), tag.artist()); + CPPUNIT_ASSERT_EQUAL(17u, tag.track()); + } + + void testPropertyInterface2() + { + APE::Tag tag; + APE::Item item1 = APE::Item("TRACK", "17"); + tag.setItem("TRACK", item1); + + APE::Item item2 = APE::Item(); + item2.setType(APE::Item::Binary); + tag.setItem("TESTBINARY", item2); + + PropertyMap properties = tag.properties(); + CPPUNIT_ASSERT_EQUAL(1u, properties.unsupportedData().size()); + CPPUNIT_ASSERT(properties.contains("TRACKNUMBER")); + CPPUNIT_ASSERT(!properties.contains("TRACK")); + CPPUNIT_ASSERT(tag.itemListMap().contains("TESTBINARY")); + + tag.removeUnsupportedProperties(properties.unsupportedData()); + CPPUNIT_ASSERT(!tag.itemListMap().contains("TESTBINARY")); + + APE::Item item3 = APE::Item("TRACKNUMBER", "29"); + tag.setItem("TRACKNUMBER", item3); + properties = tag.properties(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), properties["TRACKNUMBER"].size()); + CPPUNIT_ASSERT_EQUAL(String("17"), properties["TRACKNUMBER"][0]); + CPPUNIT_ASSERT_EQUAL(String("29"), properties["TRACKNUMBER"][1]); + + } + + void testInvalidKeys() + { + PropertyMap properties; + properties["A"] = String("invalid key: one character"); + properties["MP+"] = String("invalid key: forbidden string"); + properties["A B~C"] = String("valid key: space and tilde"); + properties["ARTIST"] = String("valid key: normal one"); + + APE::Tag tag; + PropertyMap unsuccessful = tag.setProperties(properties); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), unsuccessful.size()); + CPPUNIT_ASSERT(unsuccessful.contains("A")); + CPPUNIT_ASSERT(unsuccessful.contains("MP+")); + } + + void testTextBinary() + { + APE::Item item = APE::Item("DUMMY", "Test Text"); + CPPUNIT_ASSERT_EQUAL(String("Test Text"), item.toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector::null, item.binaryData()); + + ByteVector data("Test Data"); + item.setBinaryData(data); + CPPUNIT_ASSERT(item.values().isEmpty()); + CPPUNIT_ASSERT_EQUAL(String::null, item.toString()); + CPPUNIT_ASSERT_EQUAL(data, item.binaryData()); + + item.setValue("Test Text 2"); + CPPUNIT_ASSERT_EQUAL(String("Test Text 2"), item.toString()); + CPPUNIT_ASSERT_EQUAL(ByteVector::null, item.binaryData()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestAPETag); + diff --git a/taglib/tests/test_asf.cpp b/taglib/tests/test_asf.cpp new file mode 100644 index 0000000..8610c24 --- /dev/null +++ b/taglib/tests/test_asf.cpp @@ -0,0 +1,255 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <tpropertymap.h> +#include <asffile.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestASF : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestASF); + CPPUNIT_TEST(testAudioProperties); + CPPUNIT_TEST(testRead); + CPPUNIT_TEST(testSaveMultipleValues); + CPPUNIT_TEST(testSaveStream); + CPPUNIT_TEST(testSaveLanguage); + CPPUNIT_TEST(testDWordTrackNumber); + CPPUNIT_TEST(testSaveLargeValue); + CPPUNIT_TEST(testSavePicture); + CPPUNIT_TEST(testSaveMultiplePictures); + CPPUNIT_TEST(testProperties); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testAudioProperties() + { + ASF::File f(TEST_FILE_PATH_C("silence-1.wma")); + CPPUNIT_ASSERT_EQUAL(4, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(64, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(48000, f.audioProperties()->sampleRate()); + } + + void testRead() + { + ASF::File f(TEST_FILE_PATH_C("silence-1.wma")); + CPPUNIT_ASSERT_EQUAL(String("test"), f.tag()->title()); + } + + void testSaveMultipleValues() + { + ScopedFileCopy copy("silence-1", ".wma"); + string newname = copy.fileName(); + + ASF::File *f = new ASF::File(newname.c_str()); + ASF::AttributeList values; + values.append("Foo"); + values.append("Bar"); + f->tag()->attributeListMap()["WM/AlbumTitle"] = values; + f->save(); + delete f; + + f = new ASF::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(2, (int)f->tag()->attributeListMap()["WM/AlbumTitle"].size()); + delete f; + } + + void testDWordTrackNumber() + { + ScopedFileCopy copy("silence-1", ".wma"); + string newname = copy.fileName(); + + ASF::File *f = new ASF::File(newname.c_str()); + CPPUNIT_ASSERT(!f->tag()->attributeListMap().contains("WM/TrackNumber")); + f->tag()->setAttribute("WM/TrackNumber", (unsigned int)(123)); + f->save(); + delete f; + + f = new ASF::File(newname.c_str()); + CPPUNIT_ASSERT(f->tag()->attributeListMap().contains("WM/TrackNumber")); + CPPUNIT_ASSERT_EQUAL(ASF::Attribute::DWordType, f->tag()->attributeListMap()["WM/TrackNumber"].front().type()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(123), f->tag()->track()); + f->tag()->setTrack(234); + f->save(); + delete f; + + f = new ASF::File(newname.c_str()); + CPPUNIT_ASSERT(f->tag()->attributeListMap().contains("WM/TrackNumber")); + CPPUNIT_ASSERT_EQUAL(ASF::Attribute::UnicodeType, f->tag()->attributeListMap()["WM/TrackNumber"].front().type()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(234), f->tag()->track()); + delete f; + } + + void testSaveStream() + { + ScopedFileCopy copy("silence-1", ".wma"); + string newname = copy.fileName(); + + ASF::File *f = new ASF::File(newname.c_str()); + ASF::AttributeList values; + ASF::Attribute attr("Foo"); + attr.setStream(43); + values.append(attr); + f->tag()->attributeListMap()["WM/AlbumTitle"] = values; + f->save(); + delete f; + + f = new ASF::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(43, f->tag()->attributeListMap()["WM/AlbumTitle"][0].stream()); + delete f; + } + + void testSaveLanguage() + { + ScopedFileCopy copy("silence-1", ".wma"); + string newname = copy.fileName(); + + ASF::File *f = new ASF::File(newname.c_str()); + ASF::AttributeList values; + ASF::Attribute attr("Foo"); + attr.setStream(32); + attr.setLanguage(56); + values.append(attr); + f->tag()->attributeListMap()["WM/AlbumTitle"] = values; + f->save(); + delete f; + + f = new ASF::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(32, f->tag()->attributeListMap()["WM/AlbumTitle"][0].stream()); + CPPUNIT_ASSERT_EQUAL(56, f->tag()->attributeListMap()["WM/AlbumTitle"][0].language()); + delete f; + } + + void testSaveLargeValue() + { + ScopedFileCopy copy("silence-1", ".wma"); + string newname = copy.fileName(); + + ASF::File *f = new ASF::File(newname.c_str()); + ASF::AttributeList values; + ASF::Attribute attr(ByteVector(70000, 'x')); + values.append(attr); + f->tag()->attributeListMap()["WM/Blob"] = values; + f->save(); + delete f; + + f = new ASF::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(ByteVector(70000, 'x'), f->tag()->attributeListMap()["WM/Blob"][0].toByteVector()); + delete f; + } + + void testSavePicture() + { + ScopedFileCopy copy("silence-1", ".wma"); + string newname = copy.fileName(); + + ASF::File *f = new ASF::File(newname.c_str()); + ASF::AttributeList values; + ASF::Picture picture; + picture.setMimeType("image/jpeg"); + picture.setType(ASF::Picture::FrontCover); + picture.setDescription("description"); + picture.setPicture("data"); + ASF::Attribute attr(picture); + values.append(attr); + f->tag()->attributeListMap()["WM/Picture"] = values; + f->save(); + delete f; + + f = new ASF::File(newname.c_str()); + ASF::AttributeList values2 = f->tag()->attributeListMap()["WM/Picture"]; + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), values2.size()); + ASF::Attribute attr2 = values2.front(); + ASF::Picture picture2 = attr2.toPicture(); + CPPUNIT_ASSERT(picture2.isValid()); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), picture2.mimeType()); + CPPUNIT_ASSERT_EQUAL(ASF::Picture::FrontCover, picture2.type()); + CPPUNIT_ASSERT_EQUAL(String("description"), picture2.description()); + CPPUNIT_ASSERT_EQUAL(ByteVector("data"), picture2.picture()); + delete f; + } + + void testSaveMultiplePictures() + { + ScopedFileCopy copy("silence-1", ".wma"); + string newname = copy.fileName(); + + ASF::File *f = new ASF::File(newname.c_str()); + ASF::AttributeList values; + ASF::Picture picture; + picture.setMimeType("image/jpeg"); + picture.setType(ASF::Picture::FrontCover); + picture.setDescription("description"); + picture.setPicture("data"); + values.append(ASF::Attribute(picture)); + ASF::Picture picture2; + picture2.setMimeType("image/png"); + picture2.setType(ASF::Picture::BackCover); + picture2.setDescription("back cover"); + picture2.setPicture("PNG data"); + values.append(ASF::Attribute(picture2)); + f->tag()->attributeListMap()["WM/Picture"] = values; + f->save(); + delete f; + + f = new ASF::File(newname.c_str()); + ASF::AttributeList values2 = f->tag()->attributeListMap()["WM/Picture"]; + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), values2.size()); + ASF::Picture picture3 = values2[1].toPicture(); + CPPUNIT_ASSERT(picture3.isValid()); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), picture3.mimeType()); + CPPUNIT_ASSERT_EQUAL(ASF::Picture::FrontCover, picture3.type()); + CPPUNIT_ASSERT_EQUAL(String("description"), picture3.description()); + CPPUNIT_ASSERT_EQUAL(ByteVector("data"), picture3.picture()); + ASF::Picture picture4 = values2[0].toPicture(); + CPPUNIT_ASSERT(picture4.isValid()); + CPPUNIT_ASSERT_EQUAL(String("image/png"), picture4.mimeType()); + CPPUNIT_ASSERT_EQUAL(ASF::Picture::BackCover, picture4.type()); + CPPUNIT_ASSERT_EQUAL(String("back cover"), picture4.description()); + CPPUNIT_ASSERT_EQUAL(ByteVector("PNG data"), picture4.picture()); + delete f; + } + + void testProperties() + { + ASF::File f(TEST_FILE_PATH_C("silence-1.wma")); + + PropertyMap tags = f.properties(); + + tags["TRACKNUMBER"] = StringList("2"); + tags["DISCNUMBER"] = StringList("3"); + tags["BPM"] = StringList("123"); + tags["ARTIST"] = StringList("Foo Bar"); + f.setProperties(tags); + + tags = f.properties(); + + CPPUNIT_ASSERT_EQUAL(String("Foo Bar"), f.tag()->artist()); + CPPUNIT_ASSERT_EQUAL(StringList("Foo Bar"), tags["ARTIST"]); + + CPPUNIT_ASSERT(f.tag()->attributeListMap().contains("WM/BeatsPerMinute")); + CPPUNIT_ASSERT_EQUAL(1u, f.tag()->attributeListMap()["WM/BeatsPerMinute"].size()); + CPPUNIT_ASSERT_EQUAL(String("123"), f.tag()->attributeListMap()["WM/BeatsPerMinute"].front().toString()); + CPPUNIT_ASSERT_EQUAL(StringList("123"), tags["BPM"]); + + CPPUNIT_ASSERT(f.tag()->attributeListMap().contains("WM/TrackNumber")); + CPPUNIT_ASSERT_EQUAL(1u, f.tag()->attributeListMap()["WM/TrackNumber"].size()); + CPPUNIT_ASSERT_EQUAL(String("2"), f.tag()->attributeListMap()["WM/TrackNumber"].front().toString()); + CPPUNIT_ASSERT_EQUAL(StringList("2"), tags["TRACKNUMBER"]); + + CPPUNIT_ASSERT(f.tag()->attributeListMap().contains("WM/PartOfSet")); + CPPUNIT_ASSERT_EQUAL(1u, f.tag()->attributeListMap()["WM/PartOfSet"].size()); + CPPUNIT_ASSERT_EQUAL(String("3"), f.tag()->attributeListMap()["WM/PartOfSet"].front().toString()); + CPPUNIT_ASSERT_EQUAL(StringList("3"), tags["DISCNUMBER"]); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestASF); diff --git a/taglib/tests/test_bytevector.cpp b/taglib/tests/test_bytevector.cpp new file mode 100644 index 0000000..9efd23a --- /dev/null +++ b/taglib/tests/test_bytevector.cpp @@ -0,0 +1,246 @@ +/* Copyright (C) 2003 Scott Wheeler <wheeler@kde.org> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <tbytevector.h> +#include <tbytevectorlist.h> +#include <cppunit/extensions/HelperMacros.h> + +using namespace std; +using namespace TagLib; + +class TestByteVector : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestByteVector); + CPPUNIT_TEST(testByteVector); + CPPUNIT_TEST(testFind1); + CPPUNIT_TEST(testFind2); + CPPUNIT_TEST(testRfind1); + CPPUNIT_TEST(testRfind2); + CPPUNIT_TEST(testToHex); + CPPUNIT_TEST(testToUShort); + CPPUNIT_TEST(testReplace); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testConversion(unsigned int i, unsigned char a, unsigned char b, unsigned char c, unsigned char d) + { + ByteVector v(4, 0); + + v[3] = a; + v[2] = b; + v[1] = c; + v[0] = d; + CPPUNIT_ASSERT(v.toUInt(false) == i); + + v[0] = a; + v[1] = b; + v[2] = c; + v[3] = d; + CPPUNIT_ASSERT(v.toUInt() == i); + } + + void testByteVector() + { + ByteVector v("foobar"); + + CPPUNIT_ASSERT(v.find("ob") == 2); + CPPUNIT_ASSERT(v.find('b') == 3); + + ByteVector n(4, 0); + n[0] = 1; + CPPUNIT_ASSERT(n.toUInt(true) == 16777216); + CPPUNIT_ASSERT(n.toUInt(false) == 1); + CPPUNIT_ASSERT(ByteVector::fromUInt(16777216, true) == n); + CPPUNIT_ASSERT(ByteVector::fromUInt(1, false) == n); + + CPPUNIT_ASSERT(ByteVector::fromUInt(0xa0).toUInt() == 0xa0); + + testConversion(0x000000a0, 0x00, 0x00, 0x00, 0xa0); + testConversion(0xd50bf072, 0xd5, 0x0b, 0xf0, 0x72); + + ByteVector intVector(2, 0); + intVector[0] = char(0xfc); + intVector[1] = char(0x00); + CPPUNIT_ASSERT(intVector.toShort() == -1024); + intVector[0] = char(0x04); + intVector[1] = char(0x00); + CPPUNIT_ASSERT(intVector.toShort() == 1024); + + CPPUNIT_ASSERT(ByteVector::fromLongLong(1).toLongLong() == 1); + CPPUNIT_ASSERT(ByteVector::fromLongLong(0).toLongLong() == 0); + CPPUNIT_ASSERT(ByteVector::fromLongLong(0xffffffffffffffffLL).toLongLong() == -1); + CPPUNIT_ASSERT(ByteVector::fromLongLong(0xfffffffffffffffeLL).toLongLong() == -2); + CPPUNIT_ASSERT(ByteVector::fromLongLong(1024).toLongLong() == 1024); + + ByteVector a1("foo"); + a1.append("bar"); + CPPUNIT_ASSERT(a1 == "foobar"); + + ByteVector a2("foo"); + a2.append("b"); + CPPUNIT_ASSERT(a2 == "foob"); + + ByteVector a3; + a3.append("b"); + CPPUNIT_ASSERT(a3 == "b"); + + ByteVector s1("foo"); + CPPUNIT_ASSERT(ByteVectorList::split(s1, " ").size() == 1); + + ByteVector s2("f"); + CPPUNIT_ASSERT(ByteVectorList::split(s2, " ").size() == 1); + + CPPUNIT_ASSERT(ByteVector().size() == 0); + CPPUNIT_ASSERT(ByteVector("asdf").clear().size() == 0); + CPPUNIT_ASSERT(ByteVector("asdf").clear() == ByteVector()); + + ByteVector i("blah blah"); + ByteVector j("blah"); + CPPUNIT_ASSERT(i.containsAt(j, 5, 0)); + CPPUNIT_ASSERT(i.containsAt(j, 6, 1)); + CPPUNIT_ASSERT(i.containsAt(j, 6, 1, 3)); + } + + void testFind1() + { + CPPUNIT_ASSERT_EQUAL(4, ByteVector("....SggO."). find("SggO")); + CPPUNIT_ASSERT_EQUAL(4, ByteVector("....SggO."). find("SggO", 0)); + CPPUNIT_ASSERT_EQUAL(4, ByteVector("....SggO."). find("SggO", 1)); + CPPUNIT_ASSERT_EQUAL(4, ByteVector("....SggO."). find("SggO", 2)); + CPPUNIT_ASSERT_EQUAL(4, ByteVector("....SggO."). find("SggO", 3)); + CPPUNIT_ASSERT_EQUAL(4, ByteVector("....SggO."). find("SggO", 4)); + CPPUNIT_ASSERT_EQUAL(-1, ByteVector("....SggO."). find("SggO", 5)); + CPPUNIT_ASSERT_EQUAL(-1, ByteVector("....SggO."). find("SggO", 6)); + CPPUNIT_ASSERT_EQUAL(-1, ByteVector("....SggO."). find("SggO", 7)); + CPPUNIT_ASSERT_EQUAL(-1, ByteVector("....SggO."). find("SggO", 8)); + } + + void testFind2() + { + CPPUNIT_ASSERT_EQUAL(0, ByteVector("\x01", 1).find("\x01")); + CPPUNIT_ASSERT_EQUAL(0, ByteVector("\x01\x02", 2).find("\x01\x02")); + CPPUNIT_ASSERT_EQUAL(-1, ByteVector("\x01", 1).find("\x02")); + CPPUNIT_ASSERT_EQUAL(-1, ByteVector("\x01\x02", 2).find("\x01\x03")); + } + + void testRfind1() + { + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 0)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 1)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 2)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 3)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 4)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 5)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 6)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 7)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS", 8)); + CPPUNIT_ASSERT_EQUAL(1, ByteVector(".OggS....").rfind("OggS")); + } + + void testRfind2() + { + ByteVector r0("**************"); + ByteVector r1("OggS**********"); + ByteVector r2("**********OggS"); + ByteVector r3("OggS******OggS"); + ByteVector r4("OggS*OggS*OggS"); + + CPPUNIT_ASSERT_EQUAL(-1, r0.find("OggS")); + CPPUNIT_ASSERT_EQUAL(-1, r0.rfind("OggS")); + CPPUNIT_ASSERT_EQUAL(0, r1.find("OggS")); + CPPUNIT_ASSERT_EQUAL(0, r1.rfind("OggS")); + CPPUNIT_ASSERT_EQUAL(10, r2.find("OggS")); + CPPUNIT_ASSERT_EQUAL(10, r2.rfind("OggS")); + CPPUNIT_ASSERT_EQUAL(0, r3.find("OggS")); + CPPUNIT_ASSERT_EQUAL(10, r3.rfind("OggS")); + CPPUNIT_ASSERT_EQUAL(10, r4.rfind("OggS")); + CPPUNIT_ASSERT_EQUAL(10, r4.rfind("OggS", 0)); + CPPUNIT_ASSERT_EQUAL(5, r4.rfind("OggS", 7)); + CPPUNIT_ASSERT_EQUAL(10, r4.rfind("OggS", 12)); + } + + void testToHex() + { + ByteVector v("\xf0\xe1\xd2\xc3\xb4\xa5\x96\x87\x78\x69\x5a\x4b\x3c\x2d\x1e\x0f", 16); + + CPPUNIT_ASSERT_EQUAL(ByteVector("f0e1d2c3b4a5968778695a4b3c2d1e0f"), v.toHex()); + } + + void testToUShort() + { + CPPUNIT_ASSERT_EQUAL((unsigned short)0xFFFF, ByteVector("\xff\xff", 2).toUShort()); + CPPUNIT_ASSERT_EQUAL((unsigned short)0x0001, ByteVector("\x00\x01", 2).toUShort()); + CPPUNIT_ASSERT_EQUAL((unsigned short)0x0100, ByteVector("\x00\x01", 2).toUShort(false)); + CPPUNIT_ASSERT_EQUAL((unsigned short)0xFF01, ByteVector("\xFF\x01", 2).toUShort()); + CPPUNIT_ASSERT_EQUAL((unsigned short)0x01FF, ByteVector("\xFF\x01", 2).toUShort(false)); + } + + void testReplace() + { + { + ByteVector a("abcdabf"); + a.replace(ByteVector(""), ByteVector("<a>")); + CPPUNIT_ASSERT_EQUAL(ByteVector("abcdabf"), a); + } + { + ByteVector a("abcdabf"); + a.replace(ByteVector("foobartoolong"), ByteVector("<a>")); + CPPUNIT_ASSERT_EQUAL(ByteVector("abcdabf"), a); + } + { + ByteVector a("abcdabf"); + a.replace(ByteVector("xx"), ByteVector("yy")); + CPPUNIT_ASSERT_EQUAL(ByteVector("abcdabf"), a); + } + { + ByteVector a("abcdabf"); + a.replace(ByteVector("a"), ByteVector("x")); + CPPUNIT_ASSERT_EQUAL(ByteVector("xbcdxbf"), a); + } + { + ByteVector a("abcdabf"); + a.replace(ByteVector("ab"), ByteVector("xy")); + CPPUNIT_ASSERT_EQUAL(ByteVector("xycdxyf"), a); + } + { + ByteVector a("abcdabf"); + a.replace(ByteVector("a"), ByteVector("<a>")); + CPPUNIT_ASSERT_EQUAL(ByteVector("<a>bcd<a>bf"), a); + } + { + ByteVector a("abcdabf"); + a.replace(ByteVector("ab"), ByteVector("x")); + CPPUNIT_ASSERT_EQUAL(ByteVector("xcdxf"), a); + } + { + ByteVector a("abcdabf"); + a.replace(ByteVector("ab"), ByteVector()); + CPPUNIT_ASSERT_EQUAL(ByteVector("cdf"), a); + } + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestByteVector); diff --git a/taglib/tests/test_bytevectorlist.cpp b/taglib/tests/test_bytevectorlist.cpp new file mode 100644 index 0000000..7e4a9fd --- /dev/null +++ b/taglib/tests/test_bytevectorlist.cpp @@ -0,0 +1,38 @@ +#include <tbytevector.h> +#include <tbytevectorlist.h> +#include <cppunit/extensions/HelperMacros.h> + +using namespace std; +using namespace TagLib; + +class TestByteVectorList : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestByteVectorList); + CPPUNIT_TEST(testSplitSingleChar); + CPPUNIT_TEST(testSplitSingleChar_2); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testSplitSingleChar() + { + ByteVector v("a b"); + + ByteVectorList l = ByteVectorList::split(v, " "); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), l.size()); + CPPUNIT_ASSERT_EQUAL(ByteVector("a"), l[0]); + CPPUNIT_ASSERT_EQUAL(ByteVector("b"), l[1]); + } + + void testSplitSingleChar_2() + { + ByteVector v("a"); + + ByteVectorList l = ByteVectorList::split(v, " "); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), l.size()); + CPPUNIT_ASSERT_EQUAL(ByteVector("a"), l[0]); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestByteVectorList); diff --git a/taglib/tests/test_bytevectorstream.cpp b/taglib/tests/test_bytevectorstream.cpp new file mode 100644 index 0000000..b0fec90 --- /dev/null +++ b/taglib/tests/test_bytevectorstream.cpp @@ -0,0 +1,92 @@ +#include <tbytevectorstream.h> +#include <cppunit/extensions/HelperMacros.h> + +using namespace std; +using namespace TagLib; + +class TestByteVectorStream : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestByteVectorStream); + CPPUNIT_TEST(testInitialData); + CPPUNIT_TEST(testWriteBlock); + CPPUNIT_TEST(testWriteBlockResize); + CPPUNIT_TEST(testReadBlock); + CPPUNIT_TEST(testRemoveBlock); + CPPUNIT_TEST(testInsert); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testInitialData() + { + ByteVector v("abcd"); + ByteVectorStream stream(v); + + CPPUNIT_ASSERT_EQUAL(ByteVector("abcd"), *stream.data()); + } + + void testWriteBlock() + { + ByteVector v("abcd"); + ByteVectorStream stream(v); + + stream.seek(1); + stream.writeBlock(ByteVector("xx")); + CPPUNIT_ASSERT_EQUAL(ByteVector("axxd"), *stream.data()); + } + + void testWriteBlockResize() + { + ByteVector v("abcd"); + ByteVectorStream stream(v); + + stream.seek(3); + stream.writeBlock(ByteVector("xx")); + CPPUNIT_ASSERT_EQUAL(ByteVector("abcxx"), *stream.data()); + stream.seek(5); + stream.writeBlock(ByteVector("yy")); + CPPUNIT_ASSERT_EQUAL(ByteVector("abcxxyy"), *stream.data()); + } + + void testReadBlock() + { + ByteVector v("abcd"); + ByteVectorStream stream(v); + + CPPUNIT_ASSERT_EQUAL(ByteVector("a"), stream.readBlock(1)); + CPPUNIT_ASSERT_EQUAL(ByteVector("bc"), stream.readBlock(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("d"), stream.readBlock(3)); + CPPUNIT_ASSERT_EQUAL(ByteVector::null, stream.readBlock(3)); + } + + void testRemoveBlock() + { + ByteVector v("abcd"); + ByteVectorStream stream(v); + + stream.removeBlock(1, 1); + CPPUNIT_ASSERT_EQUAL(ByteVector("acd"), *stream.data()); + stream.removeBlock(0, 2); + CPPUNIT_ASSERT_EQUAL(ByteVector("d"), *stream.data()); + stream.removeBlock(0, 2); + CPPUNIT_ASSERT_EQUAL(ByteVector(""), *stream.data()); + } + + void testInsert() + { + ByteVector v("abcd"); + ByteVectorStream stream(v); + + stream.insert(ByteVector("xx"), 1, 1); + CPPUNIT_ASSERT_EQUAL(ByteVector("axxcd"), *stream.data()); + stream.insert(ByteVector("yy"), 0, 2); + CPPUNIT_ASSERT_EQUAL(ByteVector("yyxcd"), *stream.data()); + stream.insert(ByteVector("foa"), 3, 2); + CPPUNIT_ASSERT_EQUAL(ByteVector("yyxfoa"), *stream.data()); + stream.insert(ByteVector("123"), 3, 0); + CPPUNIT_ASSERT_EQUAL(ByteVector("yyx123foa"), *stream.data()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestByteVectorStream); diff --git a/taglib/tests/test_fileref.cpp b/taglib/tests/test_fileref.cpp new file mode 100644 index 0000000..197a921 --- /dev/null +++ b/taglib/tests/test_fileref.cpp @@ -0,0 +1,163 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <fileref.h> +#include <oggflacfile.h> +#include <vorbisfile.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestFileRef : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestFileRef); + CPPUNIT_TEST(testASF); + CPPUNIT_TEST(testMusepack); + CPPUNIT_TEST(testVorbis); + CPPUNIT_TEST(testSpeex); + CPPUNIT_TEST(testFLAC); + CPPUNIT_TEST(testMP3); + CPPUNIT_TEST(testOGA_FLAC); + CPPUNIT_TEST(testOGA_Vorbis); + CPPUNIT_TEST(testMP4_1); + CPPUNIT_TEST(testMP4_2); + CPPUNIT_TEST(testMP4_3); + CPPUNIT_TEST(testTrueAudio); + CPPUNIT_TEST(testAPE); + CPPUNIT_TEST(testWav); + CPPUNIT_TEST(testUnsupported); + CPPUNIT_TEST_SUITE_END(); + +public: + + void fileRefSave(const string &filename, const string &ext) + { + ScopedFileCopy copy(filename, ext); + string newname = copy.fileName(); + + FileRef *f = new FileRef(newname.c_str()); + CPPUNIT_ASSERT(!f->isNull()); + f->tag()->setArtist("test artist"); + f->tag()->setTitle("test title"); + f->tag()->setGenre("Test!"); + f->tag()->setAlbum("albummmm"); + f->tag()->setTrack(5); + f->tag()->setYear(2020); + f->save(); + delete f; + + f = new FileRef(newname.c_str()); + CPPUNIT_ASSERT(!f->isNull()); + CPPUNIT_ASSERT_EQUAL(f->tag()->artist(), String("test artist")); + CPPUNIT_ASSERT_EQUAL(f->tag()->title(), String("test title")); + CPPUNIT_ASSERT_EQUAL(f->tag()->genre(), String("Test!")); + CPPUNIT_ASSERT_EQUAL(f->tag()->album(), String("albummmm")); + CPPUNIT_ASSERT_EQUAL(f->tag()->track(), TagLib::uint(5)); + CPPUNIT_ASSERT_EQUAL(f->tag()->year(), TagLib::uint(2020)); + f->tag()->setArtist("ttest artist"); + f->tag()->setTitle("ytest title"); + f->tag()->setGenre("uTest!"); + f->tag()->setAlbum("ialbummmm"); + f->tag()->setTrack(7); + f->tag()->setYear(2080); + f->save(); + delete f; + + f = new FileRef(newname.c_str()); + CPPUNIT_ASSERT(!f->isNull()); + CPPUNIT_ASSERT_EQUAL(f->tag()->artist(), String("ttest artist")); + CPPUNIT_ASSERT_EQUAL(f->tag()->title(), String("ytest title")); + CPPUNIT_ASSERT_EQUAL(f->tag()->genre(), String("uTest!")); + CPPUNIT_ASSERT_EQUAL(f->tag()->album(), String("ialbummmm")); + CPPUNIT_ASSERT_EQUAL(f->tag()->track(), TagLib::uint(7)); + CPPUNIT_ASSERT_EQUAL(f->tag()->year(), TagLib::uint(2080)); + delete f; + } + + void testMusepack() + { + fileRefSave("click", ".mpc"); + } + + void testASF() + { + fileRefSave("silence-1", ".wma"); + } + + void testVorbis() + { + fileRefSave("empty", ".ogg"); + } + + void testSpeex() + { + fileRefSave("empty", ".spx"); + } + + void testFLAC() + { + fileRefSave("no-tags", ".flac"); + } + + void testMP3() + { + fileRefSave("xing", ".mp3"); + } + + void testTrueAudio() + { + fileRefSave("empty", ".tta"); + } + + void testMP4_1() + { + fileRefSave("has-tags", ".m4a"); + } + + void testMP4_2() + { + fileRefSave("no-tags", ".m4a"); + } + + void testMP4_3() + { + fileRefSave("no-tags", ".3g2"); + } + + void testWav() + { + fileRefSave("empty", ".wav"); + } + + void testOGA_FLAC() + { + FileRef *f = new FileRef(TEST_FILE_PATH_C("empty_flac.oga")); + CPPUNIT_ASSERT(dynamic_cast<Ogg::Vorbis::File *>(f->file()) == NULL); + CPPUNIT_ASSERT(dynamic_cast<Ogg::FLAC::File *>(f->file()) != NULL); + } + + void testOGA_Vorbis() + { + FileRef *f = new FileRef(TEST_FILE_PATH_C("empty_vorbis.oga")); + CPPUNIT_ASSERT(dynamic_cast<Ogg::Vorbis::File *>(f->file()) != NULL); + CPPUNIT_ASSERT(dynamic_cast<Ogg::FLAC::File *>(f->file()) == NULL); + } + + void testAPE() + { + fileRefSave("mac-399", ".ape"); + } + + void testUnsupported() + { + FileRef f1(TEST_FILE_PATH_C("no-extension")); + CPPUNIT_ASSERT(f1.isNull()); + + FileRef f2(TEST_FILE_PATH_C("unsupported-extension.xxx")); + CPPUNIT_ASSERT(f2.isNull()); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestFileRef); diff --git a/taglib/tests/test_flac.cpp b/taglib/tests/test_flac.cpp new file mode 100644 index 0000000..caec715 --- /dev/null +++ b/taglib/tests/test_flac.cpp @@ -0,0 +1,248 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <tpropertymap.h> +#include <flacfile.h> +#include <xiphcomment.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestFLAC : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestFLAC); + CPPUNIT_TEST(testSignature); + CPPUNIT_TEST(testMultipleCommentBlocks); + CPPUNIT_TEST(testReadPicture); + CPPUNIT_TEST(testAddPicture); + CPPUNIT_TEST(testReplacePicture); + CPPUNIT_TEST(testRemoveAllPictures); + CPPUNIT_TEST(testRepeatedSave); + CPPUNIT_TEST(testSaveMultipleValues); + CPPUNIT_TEST(testDict); + CPPUNIT_TEST(testInvalid); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testSignature() + { + FLAC::File f(TEST_FILE_PATH_C("no-tags.flac")); + CPPUNIT_ASSERT_EQUAL(ByteVector("a1b141f766e9849ac3db1030a20a3c77"), f.audioProperties()->signature().toHex()); + } + + void testMultipleCommentBlocks() + { + ScopedFileCopy copy("multiple-vc", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String("Artist 1"), f->tag()->artist()); + f->tag()->setArtist("The Artist"); + f->save(); + delete f; + + f = new FLAC::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String("The Artist"), f->tag()->artist()); + delete f; + } + + void testReadPicture() + { + ScopedFileCopy copy("silence-44-s", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + List<FLAC::Picture *> lst = f->pictureList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), lst.size()); + + FLAC::Picture *pic = lst.front(); + CPPUNIT_ASSERT_EQUAL(FLAC::Picture::FrontCover, pic->type()); + CPPUNIT_ASSERT_EQUAL(1, pic->width()); + CPPUNIT_ASSERT_EQUAL(1, pic->height()); + CPPUNIT_ASSERT_EQUAL(24, pic->colorDepth()); + CPPUNIT_ASSERT_EQUAL(0, pic->numColors()); + CPPUNIT_ASSERT_EQUAL(String("image/png"), pic->mimeType()); + CPPUNIT_ASSERT_EQUAL(String("A pixel."), pic->description()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(150), pic->data().size()); + } + + void testAddPicture() + { + ScopedFileCopy copy("silence-44-s", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + List<FLAC::Picture *> lst = f->pictureList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), lst.size()); + + FLAC::Picture *newpic = new FLAC::Picture(); + newpic->setType(FLAC::Picture::BackCover); + newpic->setWidth(5); + newpic->setHeight(6); + newpic->setColorDepth(16); + newpic->setNumColors(7); + newpic->setMimeType("image/jpeg"); + newpic->setDescription("new image"); + newpic->setData("JPEG data"); + f->addPicture(newpic); + f->save(); + + f = new FLAC::File(newname.c_str()); + lst = f->pictureList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), lst.size()); + + FLAC::Picture *pic = lst[0]; + CPPUNIT_ASSERT_EQUAL(FLAC::Picture::FrontCover, pic->type()); + CPPUNIT_ASSERT_EQUAL(1, pic->width()); + CPPUNIT_ASSERT_EQUAL(1, pic->height()); + CPPUNIT_ASSERT_EQUAL(24, pic->colorDepth()); + CPPUNIT_ASSERT_EQUAL(0, pic->numColors()); + CPPUNIT_ASSERT_EQUAL(String("image/png"), pic->mimeType()); + CPPUNIT_ASSERT_EQUAL(String("A pixel."), pic->description()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(150), pic->data().size()); + + pic = lst[1]; + CPPUNIT_ASSERT_EQUAL(FLAC::Picture::BackCover, pic->type()); + CPPUNIT_ASSERT_EQUAL(5, pic->width()); + CPPUNIT_ASSERT_EQUAL(6, pic->height()); + CPPUNIT_ASSERT_EQUAL(16, pic->colorDepth()); + CPPUNIT_ASSERT_EQUAL(7, pic->numColors()); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), pic->mimeType()); + CPPUNIT_ASSERT_EQUAL(String("new image"), pic->description()); + CPPUNIT_ASSERT_EQUAL(ByteVector("JPEG data"), pic->data()); + } + + void testReplacePicture() + { + ScopedFileCopy copy("silence-44-s", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + List<FLAC::Picture *> lst = f->pictureList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), lst.size()); + + FLAC::Picture *newpic = new FLAC::Picture(); + newpic->setType(FLAC::Picture::BackCover); + newpic->setWidth(5); + newpic->setHeight(6); + newpic->setColorDepth(16); + newpic->setNumColors(7); + newpic->setMimeType("image/jpeg"); + newpic->setDescription("new image"); + newpic->setData("JPEG data"); + f->removePictures(); + f->addPicture(newpic); + f->save(); + + f = new FLAC::File(newname.c_str()); + lst = f->pictureList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), lst.size()); + + FLAC::Picture *pic = lst[0]; + CPPUNIT_ASSERT_EQUAL(FLAC::Picture::BackCover, pic->type()); + CPPUNIT_ASSERT_EQUAL(5, pic->width()); + CPPUNIT_ASSERT_EQUAL(6, pic->height()); + CPPUNIT_ASSERT_EQUAL(16, pic->colorDepth()); + CPPUNIT_ASSERT_EQUAL(7, pic->numColors()); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), pic->mimeType()); + CPPUNIT_ASSERT_EQUAL(String("new image"), pic->description()); + CPPUNIT_ASSERT_EQUAL(ByteVector("JPEG data"), pic->data()); + } + + void testRemoveAllPictures() + { + ScopedFileCopy copy("silence-44-s", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + List<FLAC::Picture *> lst = f->pictureList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), lst.size()); + + f->removePictures(); + f->save(); + + f = new FLAC::File(newname.c_str()); + lst = f->pictureList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), lst.size()); + } + + void testRepeatedSave() + { + ScopedFileCopy copy("silence-44-s", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + Tag *tag = f->tag(); + CPPUNIT_ASSERT_EQUAL(String("Silence"), tag->title()); + tag->setTitle("NEW TITLE"); + f->save(); + CPPUNIT_ASSERT_EQUAL(String("NEW TITLE"), tag->title()); + tag->setTitle("NEW TITLE 2"); + f->save(); + CPPUNIT_ASSERT_EQUAL(String("NEW TITLE 2"), tag->title()); + + f = new FLAC::File(newname.c_str()); + tag = f->tag(); + CPPUNIT_ASSERT_EQUAL(String("NEW TITLE 2"), tag->title()); + } + + void testSaveMultipleValues() + { + ScopedFileCopy copy("silence-44-s", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + Ogg::XiphComment* c = f->xiphComment(true); + c->addField("ARTIST", "artist 1", true); + c->addField("ARTIST", "artist 2", false); + f->save(); + delete f; + + f = new FLAC::File(newname.c_str()); + c = f->xiphComment(true); + Ogg::FieldListMap m = c->fieldListMap(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), m["ARTIST"].size()); + CPPUNIT_ASSERT_EQUAL(String("artist 1"), m["ARTIST"][0]); + CPPUNIT_ASSERT_EQUAL(String("artist 2"), m["ARTIST"][1]); + } + + void testDict() + { + // test unicode & multiple values with dict interface + ScopedFileCopy copy("silence-44-s", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + PropertyMap dict; + dict["ARTIST"].append("artøst 1"); + dict["ARTIST"].append("artöst 2"); + f->setProperties(dict); + f->save(); + delete f; + + f = new FLAC::File(newname.c_str()); + dict = f->properties(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), dict["ARTIST"].size()); + CPPUNIT_ASSERT_EQUAL(String("artøst 1"), dict["ARTIST"][0]); + CPPUNIT_ASSERT_EQUAL(String("artöst 2"), dict["ARTIST"][1]); + } + + void testInvalid() + { + ScopedFileCopy copy("silence-44-s", ".flac"); + PropertyMap map; + map["HÄÖ"] = String("bla"); + FLAC::File f(copy.fileName().c_str()); + PropertyMap invalid = f.setProperties(map); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), invalid.size()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f.properties().size()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestFLAC); diff --git a/taglib/tests/test_flacpicture.cpp b/taglib/tests/test_flacpicture.cpp new file mode 100644 index 0000000..eefa930 --- /dev/null +++ b/taglib/tests/test_flacpicture.cpp @@ -0,0 +1,52 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <flacfile.h> +#include <flacmetadatablock.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestFLACPicture : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestFLACPicture); + CPPUNIT_TEST(testParse); + CPPUNIT_TEST(testPassThrough); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testParse() + { + const unsigned char data[] = { 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x09, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x70, 0x6E, 0x67, 0x00, 0x00, 0x00, 0x08, 0x41, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6C, 0x2E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0B, 0x13, 0x00, 0x00, 0x0B, 0x13, 0x01, 0x00, 0x9A, 0x9C, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4D, 0x45, 0x07, 0xD6, 0x0B, 0x1C, 0x0A, 0x36, 0x06, 0x08, 0x44, 0x3D, 0x32, 0x00, 0x00, 0x00, 0x1D, 0x74, 0x45, 0x58, 0x74, 0x43, 0x6F, 0x6D, 0x6D, 0x65, 0x6E, 0x74, 0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x54, 0x68, 0x65, 0x20, 0x47, 0x49, 0x4D, 0x50, 0xEF, 0x64, 0x25, 0x6E, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xFF, 0xFF, 0x3F, 0x00, 0x05, 0xFE, 0x02, 0xFE, 0xDC, 0xCC, 0x59, 0xE7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; + const char *pdata = reinterpret_cast<const char*>(data); + + FLAC::Picture pic(ByteVector(pdata, 199)); + + CPPUNIT_ASSERT_EQUAL(3, int(pic.type())); + CPPUNIT_ASSERT_EQUAL(1, pic.width()); + CPPUNIT_ASSERT_EQUAL(1, pic.height()); + CPPUNIT_ASSERT_EQUAL(24, pic.colorDepth()); + CPPUNIT_ASSERT_EQUAL(0, pic.numColors()); + CPPUNIT_ASSERT_EQUAL(String("image/png"), pic.mimeType()); + CPPUNIT_ASSERT_EQUAL(String("A pixel."), pic.description()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(150), pic.data().size()); + } + + void testPassThrough() + { + const unsigned char data[] = { 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x09, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x70, 0x6E, 0x67, 0x00, 0x00, 0x00, 0x08, 0x41, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6C, 0x2E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0B, 0x13, 0x00, 0x00, 0x0B, 0x13, 0x01, 0x00, 0x9A, 0x9C, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4D, 0x45, 0x07, 0xD6, 0x0B, 0x1C, 0x0A, 0x36, 0x06, 0x08, 0x44, 0x3D, 0x32, 0x00, 0x00, 0x00, 0x1D, 0x74, 0x45, 0x58, 0x74, 0x43, 0x6F, 0x6D, 0x6D, 0x65, 0x6E, 0x74, 0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x54, 0x68, 0x65, 0x20, 0x47, 0x49, 0x4D, 0x50, 0xEF, 0x64, 0x25, 0x6E, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xFF, 0xFF, 0x3F, 0x00, 0x05, 0xFE, 0x02, 0xFE, 0xDC, 0xCC, 0x59, 0xE7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; + const char *pdata = reinterpret_cast<const char*>(data); + + FLAC::Picture pic(ByteVector(pdata, 199)); + CPPUNIT_ASSERT_EQUAL(ByteVector(pdata, 199), pic.render()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestFLACPicture); + diff --git a/taglib/tests/test_flacunknownmetadatablock.cpp b/taglib/tests/test_flacunknownmetadatablock.cpp new file mode 100644 index 0000000..881078d --- /dev/null +++ b/taglib/tests/test_flacunknownmetadatablock.cpp @@ -0,0 +1,38 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <flacunknownmetadatablock.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestFLACUnknownMetadataBlock : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestFLACUnknownMetadataBlock); + CPPUNIT_TEST(testAccessors); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testAccessors() + { + ByteVector data("abc\x01", 4); + FLAC::UnknownMetadataBlock block(42, data); + CPPUNIT_ASSERT_EQUAL(42, block.code()); + CPPUNIT_ASSERT_EQUAL(data, block.data()); + CPPUNIT_ASSERT_EQUAL(data, block.render()); + ByteVector data2("xxx", 3); + block.setCode(13); + block.setData(data2); + CPPUNIT_ASSERT_EQUAL(13, block.code()); + CPPUNIT_ASSERT_EQUAL(data2, block.data()); + CPPUNIT_ASSERT_EQUAL(data2, block.render()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestFLACUnknownMetadataBlock); diff --git a/taglib/tests/test_id3v1.cpp b/taglib/tests/test_id3v1.cpp new file mode 100644 index 0000000..7db2dbb --- /dev/null +++ b/taglib/tests/test_id3v1.cpp @@ -0,0 +1,40 @@ +#include <string> +#include <stdio.h> +#include <tstring.h> +#include <mpegfile.h> +#include <id3v1tag.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestID3v1 : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestID3v1); + CPPUNIT_TEST(testStripWhiteSpace); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testStripWhiteSpace() + { + ScopedFileCopy copy("xing", ".mp3"); + string newname = copy.fileName(); + + { + MPEG::File f(newname.c_str()); + f.ID3v1Tag(true)->setArtist("Artist "); + f.save(); + } + + { + MPEG::File f(newname.c_str()); + CPPUNIT_ASSERT(f.ID3v1Tag(false)); + CPPUNIT_ASSERT_EQUAL(String("Artist"), f.ID3v1Tag(false)->artist()); + } + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestID3v1); diff --git a/taglib/tests/test_id3v2.cpp b/taglib/tests/test_id3v2.cpp new file mode 100644 index 0000000..9f5ffe0 --- /dev/null +++ b/taglib/tests/test_id3v2.cpp @@ -0,0 +1,758 @@ +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string> +#include <stdio.h> +// so evil :( +#define protected public +#include <id3v2tag.h> +#include <mpegfile.h> +#include <id3v2frame.h> +#undef protected +#include <uniquefileidentifierframe.h> +#include <textidentificationframe.h> +#include <attachedpictureframe.h> +#include <unsynchronizedlyricsframe.h> +#include <generalencapsulatedobjectframe.h> +#include <relativevolumeframe.h> +#include <popularimeterframe.h> +#include <urllinkframe.h> +#include <ownershipframe.h> +#include <unknownframe.h> +#include <tdebug.h> +#include <tpropertymap.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class PublicFrame : public ID3v2::Frame +{ + public: + PublicFrame() : ID3v2::Frame(ByteVector("XXXX\0\0\0\0\0\0", 10)) {} + String readStringField(const ByteVector &data, String::Type encoding, + int *positon = 0) + { return ID3v2::Frame::readStringField(data, encoding, positon); } + virtual String toString() const { return String::null; } + virtual void parseFields(const ByteVector &) {} + virtual ByteVector renderFields() const { return ByteVector::null; } +}; + +class TestID3v2 : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestID3v2); + CPPUNIT_TEST(testUnsynchDecode); + CPPUNIT_TEST(testDowngradeUTF8ForID3v23); + CPPUNIT_TEST(testUTF16BEDelimiter); + CPPUNIT_TEST(testUTF16Delimiter); + CPPUNIT_TEST(testReadStringField); + CPPUNIT_TEST(testParseAPIC); + CPPUNIT_TEST(testParseAPIC_UTF16_BOM); + CPPUNIT_TEST(testParseAPICv22); + CPPUNIT_TEST(testDontRender22); + CPPUNIT_TEST(testParseGEOB); + CPPUNIT_TEST(testPOPMtoString); + CPPUNIT_TEST(testParsePOPM); + CPPUNIT_TEST(testParsePOPMWithoutCounter); + CPPUNIT_TEST(testRenderPOPM); + CPPUNIT_TEST(testPOPMFromFile); + CPPUNIT_TEST(testParseRelativeVolumeFrame); + CPPUNIT_TEST(testParseUniqueFileIdentifierFrame); + CPPUNIT_TEST(testParseEmptyUniqueFileIdentifierFrame); + CPPUNIT_TEST(testBrokenFrame1); + CPPUNIT_TEST(testItunes24FrameSize); + CPPUNIT_TEST(testParseUrlLinkFrame); + CPPUNIT_TEST(testRenderUrlLinkFrame); + CPPUNIT_TEST(testParseUserUrlLinkFrame); + CPPUNIT_TEST(testRenderUserUrlLinkFrame); + CPPUNIT_TEST(testParseOwnershipFrame); + CPPUNIT_TEST(testRenderOwnershipFrame); + CPPUNIT_TEST(testSaveUTF16Comment); + CPPUNIT_TEST(testUpdateGenre23_1); + CPPUNIT_TEST(testUpdateGenre23_2); + CPPUNIT_TEST(testUpdateGenre24); + CPPUNIT_TEST(testUpdateDate22); + CPPUNIT_TEST(testDowngradeTo23); + // CPPUNIT_TEST(testUpdateFullDate22); TODO TYE+TDA should be upgraded to TDRC together + CPPUNIT_TEST(testCompressedFrameWithBrokenLength); + CPPUNIT_TEST(testW000); + CPPUNIT_TEST(testPropertyInterface); + CPPUNIT_TEST(testPropertyInterface2); + CPPUNIT_TEST(testDeleteFrame); + CPPUNIT_TEST(testSaveAndStripID3v1ShouldNotAddFrameFromID3v1ToId3v2); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testUnsynchDecode() + { + MPEG::File f(TEST_FILE_PATH_C("unsynch.id3"), false); + CPPUNIT_ASSERT(f.tag()); + CPPUNIT_ASSERT_EQUAL(String("My babe just cares for me"), f.tag()->title()); + } + + void testDowngradeUTF8ForID3v23() + { + ID3v2::TextIdentificationFrame f(ByteVector("TPE1"), String::UTF8); + StringList sl; + sl.append("Foo"); + f.setText(sl); + f.header()->setVersion(3); + ByteVector data = f.render(); + CPPUNIT_ASSERT_EQUAL((unsigned int)(4+4+2+1+6+2), data.size()); + ID3v2::TextIdentificationFrame f2(data); + CPPUNIT_ASSERT_EQUAL(sl, f2.fieldList()); + CPPUNIT_ASSERT_EQUAL(String::UTF16, f2.textEncoding()); + } + + void testUTF16BEDelimiter() + { + ID3v2::TextIdentificationFrame f(ByteVector("TPE1"), String::UTF16BE); + StringList sl; + sl.append("Foo"); + sl.append("Bar"); + f.setText(sl); + CPPUNIT_ASSERT_EQUAL((unsigned int)(4+4+2+1+6+2+6), f.render().size()); + } + + void testUTF16Delimiter() + { + ID3v2::TextIdentificationFrame f(ByteVector("TPE1"), String::UTF16); + StringList sl; + sl.append("Foo"); + sl.append("Bar"); + f.setText(sl); + CPPUNIT_ASSERT_EQUAL((unsigned int)(4+4+2+1+8+2+8), f.render().size()); + } + + void testBrokenFrame1() + { + MPEG::File f(TEST_FILE_PATH_C("broken-tenc.id3"), false); + CPPUNIT_ASSERT(f.tag()); + CPPUNIT_ASSERT(!f.ID3v2Tag()->frameListMap().contains("TENC")); + } + + void testReadStringField() + { + PublicFrame f; + ByteVector data("abc\0", 4); + String str = f.readStringField(data, String::Latin1); + CPPUNIT_ASSERT_EQUAL(String("abc"), str); + } + + // http://bugs.kde.org/show_bug.cgi?id=151078 + void testParseAPIC() + { + ID3v2::AttachedPictureFrame f(ByteVector("APIC" + "\x00\x00\x00\x07" + "\x00\x00" + "\x00" + "m\x00" + "\x01" + "d\x00" + "\x00", 17)); + CPPUNIT_ASSERT_EQUAL(String("m"), f.mimeType()); + CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::FileIcon, f.type()); + CPPUNIT_ASSERT_EQUAL(String("d"), f.description()); + } + + void testParseAPIC_UTF16_BOM() + { + ID3v2::AttachedPictureFrame f(ByteVector( + "\x41\x50\x49\x43\x00\x02\x0c\x59\x00\x00\x01\x69\x6d\x61\x67\x65" + "\x2f\x6a\x70\x65\x67\x00\x00\xfe\xff\x00\x63\x00\x6f\x00\x76\x00" + "\x65\x00\x72\x00\x2e\x00\x6a\x00\x70\x00\x67\x00\x00\xff\xd8\xff", + 16 * 3)); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), f.mimeType()); + CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::Other, f.type()); + CPPUNIT_ASSERT_EQUAL(String("cover.jpg"), f.description()); + CPPUNIT_ASSERT_EQUAL(ByteVector("\xff\xd8\xff", 3), f.picture()); + } + + void testParseAPICv22() + { + ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance(); + ByteVector data = ByteVector("PIC" + "\x00\x00\x08" + "\x00" + "JPG" + "\x01" + "d\x00" + "\x00", 18); + ID3v2::AttachedPictureFrame *frame = + static_cast<TagLib::ID3v2::AttachedPictureFrame*>(factory->createFrame(data, TagLib::uint(2))); + + CPPUNIT_ASSERT(frame); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), frame->mimeType()); + CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::FileIcon, frame->type()); + CPPUNIT_ASSERT_EQUAL(String("d"), frame->description()); + } + + void testDontRender22() + { + ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance(); + ByteVector data = ByteVector("FOO" + "\x00\x00\x08" + "\x00" + "JPG" + "\x01" + "d\x00" + "\x00", 18); + ID3v2::AttachedPictureFrame *frame = + static_cast<TagLib::ID3v2::AttachedPictureFrame*>(factory->createFrame(data, TagLib::uint(2))); + + CPPUNIT_ASSERT(frame); + + ID3v2::Tag tag; + tag.addFrame(frame); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1034), tag.render().size()); + } + + // http://bugs.kde.org/show_bug.cgi?id=151078 + void testParseGEOB() + { + ID3v2::GeneralEncapsulatedObjectFrame f(ByteVector("GEOB" + "\x00\x00\x00\x08" + "\x00\x00" + "\x00" + "m\x00" + "f\x00" + "d\x00" + "\x00", 18)); + CPPUNIT_ASSERT_EQUAL(String("m"), f.mimeType()); + CPPUNIT_ASSERT_EQUAL(String("f"), f.fileName()); + CPPUNIT_ASSERT_EQUAL(String("d"), f.description()); + } + + void testParsePOPM() + { + ID3v2::PopularimeterFrame f(ByteVector("POPM" + "\x00\x00\x00\x17" + "\x00\x00" + "email@example.com\x00" + "\x02" + "\x00\x00\x00\x03", 33)); + CPPUNIT_ASSERT_EQUAL(String("email@example.com"), f.email()); + CPPUNIT_ASSERT_EQUAL(2, f.rating()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), f.counter()); + } + + void testParsePOPMWithoutCounter() + { + ID3v2::PopularimeterFrame f(ByteVector("POPM" + "\x00\x00\x00\x13" + "\x00\x00" + "email@example.com\x00" + "\x02", 29)); + CPPUNIT_ASSERT_EQUAL(String("email@example.com"), f.email()); + CPPUNIT_ASSERT_EQUAL(2, f.rating()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f.counter()); + } + + void testRenderPOPM() + { + ID3v2::PopularimeterFrame f; + f.setEmail("email@example.com"); + f.setRating(2); + f.setCounter(3); + CPPUNIT_ASSERT_EQUAL( + ByteVector("POPM" + "\x00\x00\x00\x17" + "\x00\x00" + "email@example.com\x00" + "\x02" + "\x00\x00\x00\x03", 33), + f.render()); + } + + void testPOPMtoString() + { + ID3v2::PopularimeterFrame f; + f.setEmail("email@example.com"); + f.setRating(2); + f.setCounter(3); + CPPUNIT_ASSERT_EQUAL( + String("email@example.com rating=2 counter=3"), f.toString()); + } + + void testPOPMFromFile() + { + ScopedFileCopy copy("xing", ".mp3"); + string newname = copy.fileName(); + + ID3v2::PopularimeterFrame *f = new ID3v2::PopularimeterFrame(); + f->setEmail("email@example.com"); + f->setRating(200); + f->setCounter(3); + + MPEG::File foo(newname.c_str()); + foo.ID3v2Tag()->addFrame(f); + foo.save(); + + MPEG::File bar(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String("email@example.com"), dynamic_cast<ID3v2::PopularimeterFrame *>(bar.ID3v2Tag()->frameList("POPM").front())->email()); + CPPUNIT_ASSERT_EQUAL(200, dynamic_cast<ID3v2::PopularimeterFrame *>(bar.ID3v2Tag()->frameList("POPM").front())->rating()); + } + + // http://bugs.kde.org/show_bug.cgi?id=150481 + void testParseRelativeVolumeFrame() + { + ID3v2::RelativeVolumeFrame f( + ByteVector("RVA2" // Frame ID + "\x00\x00\x00\x0B" // Frame size + "\x00\x00" // Frame flags + "ident\x00" // Identification + "\x02" // Type of channel + "\x00\x0F" // Volume adjustment + "\x08" // Bits representing peak + "\x45", 21)); // Peak volume + CPPUNIT_ASSERT_EQUAL(String("ident"), f.identification()); + CPPUNIT_ASSERT_EQUAL(15.0f / 512.0f, + f.volumeAdjustment(ID3v2::RelativeVolumeFrame::FrontRight)); + CPPUNIT_ASSERT_EQUAL((uchar)8, + f.peakVolume(ID3v2::RelativeVolumeFrame::FrontRight).bitsRepresentingPeak); + CPPUNIT_ASSERT_EQUAL(ByteVector("\x45"), + f.peakVolume(ID3v2::RelativeVolumeFrame::FrontRight).peakVolume); + } + + void testParseUniqueFileIdentifierFrame() + { + ID3v2::UniqueFileIdentifierFrame f( + ByteVector("UFID" // Frame ID + "\x00\x00\x00\x09" // Frame size + "\x00\x00" // Frame flags + "owner\x00" // Owner identifier + "\x00\x01\x02", 19)); // Identifier + CPPUNIT_ASSERT_EQUAL(String("owner"), + f.owner()); + CPPUNIT_ASSERT_EQUAL(ByteVector("\x00\x01\x02", 3), + f.identifier()); + } + + void testParseEmptyUniqueFileIdentifierFrame() + { + ID3v2::UniqueFileIdentifierFrame f( + ByteVector("UFID" // Frame ID + "\x00\x00\x00\x01" // Frame size + "\x00\x00" // Frame flags + "\x00" // Owner identifier + "", 11)); // Identifier + CPPUNIT_ASSERT_EQUAL(String(), + f.owner()); + CPPUNIT_ASSERT_EQUAL(ByteVector(), + f.identifier()); + } + + void testParseUrlLinkFrame() + { + ID3v2::UrlLinkFrame f( + ByteVector("WOAF" // Frame ID + "\x00\x00\x00\x12" // Frame size + "\x00\x00" // Frame flags + "http://example.com", 28)); // URL + CPPUNIT_ASSERT_EQUAL(String("http://example.com"), f.url()); + } + + void testRenderUrlLinkFrame() + { + ID3v2::UrlLinkFrame f("WOAF"); + f.setUrl("http://example.com"); + CPPUNIT_ASSERT_EQUAL( + ByteVector("WOAF" // Frame ID + "\x00\x00\x00\x12" // Frame size + "\x00\x00" // Frame flags + "http://example.com", 28), // URL + f.render()); + } + + void testParseUserUrlLinkFrame() + { + ID3v2::UserUrlLinkFrame f( + ByteVector("WXXX" // Frame ID + "\x00\x00\x00\x17" // Frame size + "\x00\x00" // Frame flags + "\x00" // Text encoding + "foo\x00" // Description + "http://example.com", 33)); // URL + CPPUNIT_ASSERT_EQUAL(String("foo"), f.description()); + CPPUNIT_ASSERT_EQUAL(String("http://example.com"), f.url()); + } + + void testRenderUserUrlLinkFrame() + { + ID3v2::UserUrlLinkFrame f; + f.setDescription("foo"); + f.setUrl("http://example.com"); + CPPUNIT_ASSERT_EQUAL( + ByteVector("WXXX" // Frame ID + "\x00\x00\x00\x17" // Frame size + "\x00\x00" // Frame flags + "\x00" // Text encoding + "foo\x00" // Description + "http://example.com", 33), // URL + f.render()); + } + + void testParseOwnershipFrame() + { + ID3v2::OwnershipFrame f( + ByteVector("OWNE" // Frame ID + "\x00\x00\x00\x19" // Frame size + "\x00\x00" // Frame flags + "\x00" // Text encoding + "GBP1.99\x00" // Price paid + "20120905" // Date of purchase + "Beatport", 35)); // Seller + CPPUNIT_ASSERT_EQUAL(String("GBP1.99"), f.pricePaid()); + CPPUNIT_ASSERT_EQUAL(String("20120905"), f.datePurchased()); + CPPUNIT_ASSERT_EQUAL(String("Beatport"), f.seller()); + } + + void testRenderOwnershipFrame() + { + ID3v2::OwnershipFrame f; + f.setPricePaid("GBP1.99"); + f.setDatePurchased("20120905"); + f.setSeller("Beatport"); + CPPUNIT_ASSERT_EQUAL( + ByteVector("OWNE" // Frame ID + "\x00\x00\x00\x19" // Frame size + "\x00\x00" // Frame flags + "\x00" // Text encoding + "GBP1.99\x00" // Price paid + "20120905" // Date of purchase + "Beatport", 35), // URL + f.render()); + } + + void testItunes24FrameSize() + { + MPEG::File f(TEST_FILE_PATH_C("005411.id3"), false); + CPPUNIT_ASSERT(f.tag()); + CPPUNIT_ASSERT(f.ID3v2Tag()->frameListMap().contains("TIT2")); + CPPUNIT_ASSERT_EQUAL(String("Sunshine Superman"), f.ID3v2Tag()->frameListMap()["TIT2"].front()->toString()); + } + + void testSaveUTF16Comment() + { + String::Type defaultEncoding = ID3v2::FrameFactory::instance()->defaultTextEncoding(); + ScopedFileCopy copy("xing", ".mp3"); + string newname = copy.fileName(); + ID3v2::FrameFactory::instance()->setDefaultTextEncoding(String::UTF16); + MPEG::File foo(newname.c_str()); + foo.strip(); + foo.tag()->setComment("Test comment!"); + foo.save(); + MPEG::File bar(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String("Test comment!"), bar.tag()->comment()); + ID3v2::FrameFactory::instance()->setDefaultTextEncoding(defaultEncoding); + } + + void testUpdateGenre23_1() + { + // "Refinement" is the same as the ID3v1 genre - duplicate + ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance(); + ByteVector data = ByteVector("TCON" // Frame ID + "\x00\x00\x00\x10" // Frame size + "\x00\x00" // Frame flags + "\x00" // Encoding + "(22)Death Metal", 26); // Text + ID3v2::TextIdentificationFrame *frame = + static_cast<TagLib::ID3v2::TextIdentificationFrame*>(factory->createFrame(data, TagLib::uint(3))); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), frame->fieldList().size()); + CPPUNIT_ASSERT_EQUAL(String("Death Metal"), frame->fieldList()[0]); + + ID3v2::Tag tag; + tag.addFrame(frame); + CPPUNIT_ASSERT_EQUAL(String("Death Metal"), tag.genre()); + } + + void testUpdateGenre23_2() + { + // "Refinement" is different from the ID3v1 genre + ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance(); + ByteVector data = ByteVector("TCON" // Frame ID + "\x00\x00\x00\x13" // Frame size + "\x00\x00" // Frame flags + "\x00" // Encoding + "(4)Eurodisco", 23); // Text + ID3v2::TextIdentificationFrame *frame = + static_cast<TagLib::ID3v2::TextIdentificationFrame*>(factory->createFrame(data, TagLib::uint(3))); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), frame->fieldList().size()); + CPPUNIT_ASSERT_EQUAL(String("4"), frame->fieldList()[0]); + CPPUNIT_ASSERT_EQUAL(String("Eurodisco"), frame->fieldList()[1]); + + ID3v2::Tag tag; + tag.addFrame(frame); + CPPUNIT_ASSERT_EQUAL(String("Disco Eurodisco"), tag.genre()); + } + + void testUpdateGenre24() + { + ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance(); + ByteVector data = ByteVector("TCON" // Frame ID + "\x00\x00\x00\x0D" // Frame size + "\x00\x00" // Frame flags + "\0" // Encoding + "14\0Eurodisco", 23); // Text + ID3v2::TextIdentificationFrame *frame = + static_cast<TagLib::ID3v2::TextIdentificationFrame*>(factory->createFrame(data, TagLib::uint(4))); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), frame->fieldList().size()); + CPPUNIT_ASSERT_EQUAL(String("14"), frame->fieldList()[0]); + CPPUNIT_ASSERT_EQUAL(String("Eurodisco"), frame->fieldList()[1]); + + ID3v2::Tag tag; + tag.addFrame(frame); + CPPUNIT_ASSERT_EQUAL(String("R&B Eurodisco"), tag.genre()); + } + + void testUpdateDate22() + { + MPEG::File f(TEST_FILE_PATH_C("id3v22-tda.mp3"), false); + CPPUNIT_ASSERT(f.tag()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2010), f.tag()->year()); + } + + void testUpdateFullDate22() + { + MPEG::File f(TEST_FILE_PATH_C("id3v22-tda.mp3"), false); + CPPUNIT_ASSERT(f.tag()); + CPPUNIT_ASSERT_EQUAL(String("2010-04-03"), f.ID3v2Tag()->frameListMap()["TDRC"].front()->toString()); + } + + void testDowngradeTo23() + { + ScopedFileCopy copy("xing", ".mp3"); + string newname = copy.fileName(); + + ID3v2::TextIdentificationFrame *tf; + MPEG::File foo(newname.c_str()); + tf = new ID3v2::TextIdentificationFrame("TDOR", String::Latin1); + tf->setText("2011-03-16"); + foo.ID3v2Tag()->addFrame(tf); + tf = new ID3v2::TextIdentificationFrame("TDRC", String::Latin1); + tf->setText("2012-04-17T12:01"); + foo.ID3v2Tag()->addFrame(tf); + tf = new ID3v2::TextIdentificationFrame("TMCL", String::Latin1); + tf->setText(StringList().append("Guitar").append("Artist 1").append("Drums").append("Artist 2")); + foo.ID3v2Tag()->addFrame(tf); + tf = new ID3v2::TextIdentificationFrame("TIPL", String::Latin1); + tf->setText(StringList().append("Producer").append("Artist 3").append("Mastering").append("Artist 4")); + foo.ID3v2Tag()->addFrame(tf); + foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TDRL", String::Latin1)); + foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TDTG", String::Latin1)); + foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TMOO", String::Latin1)); + foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TPRO", String::Latin1)); + foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TSOA", String::Latin1)); + foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TSOT", String::Latin1)); + foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TSST", String::Latin1)); + foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TSOP", String::Latin1)); + foo.save(MPEG::File::AllTags, true, 3); + + MPEG::File bar(newname.c_str()); + tf = static_cast<ID3v2::TextIdentificationFrame *>(bar.ID3v2Tag()->frameList("TDOR").front()); + CPPUNIT_ASSERT(tf); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), tf->fieldList().size()); + CPPUNIT_ASSERT_EQUAL(String("2011"), tf->fieldList().front()); + tf = static_cast<ID3v2::TextIdentificationFrame *>(bar.ID3v2Tag()->frameList("TDRC").front()); + CPPUNIT_ASSERT(tf); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), tf->fieldList().size()); + CPPUNIT_ASSERT_EQUAL(String("2012"), tf->fieldList().front()); + tf = dynamic_cast<ID3v2::TextIdentificationFrame *>(bar.ID3v2Tag()->frameList("TIPL").front()); + CPPUNIT_ASSERT(tf); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(8), tf->fieldList().size()); + CPPUNIT_ASSERT_EQUAL(String("Guitar"), tf->fieldList()[0]); + CPPUNIT_ASSERT_EQUAL(String("Artist 1"), tf->fieldList()[1]); + CPPUNIT_ASSERT_EQUAL(String("Drums"), tf->fieldList()[2]); + CPPUNIT_ASSERT_EQUAL(String("Artist 2"), tf->fieldList()[3]); + CPPUNIT_ASSERT_EQUAL(String("Producer"), tf->fieldList()[4]); + CPPUNIT_ASSERT_EQUAL(String("Artist 3"), tf->fieldList()[5]); + CPPUNIT_ASSERT_EQUAL(String("Mastering"), tf->fieldList()[6]); + CPPUNIT_ASSERT_EQUAL(String("Artist 4"), tf->fieldList()[7]); + CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TDRL")); + CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TDTG")); + CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TMOO")); + CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TPRO")); + CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TSOA")); + CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TSOT")); + CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TSST")); + CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TSOP")); + } + + void testCompressedFrameWithBrokenLength() + { + MPEG::File f(TEST_FILE_PATH_C("compressed_id3_frame.mp3"), false); + CPPUNIT_ASSERT(f.ID3v2Tag()->frameListMap().contains("APIC")); + +#ifdef HAVE_ZLIB + + ID3v2::AttachedPictureFrame *frame + = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(f.ID3v2Tag()->frameListMap()["APIC"].front()); + CPPUNIT_ASSERT(frame); + CPPUNIT_ASSERT_EQUAL(String("image/bmp"), frame->mimeType()); + CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::Other, frame->type()); + CPPUNIT_ASSERT_EQUAL(String(""), frame->description()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(86414), frame->picture().size()); + +#else + + // Skip the test if ZLIB is not installed. + // The message "Compressed frames are currently not supported." will be displayed. + + ID3v2::UnknownFrame *frame + = dynamic_cast<TagLib::ID3v2::UnknownFrame*>(f.ID3v2Tag()->frameListMap()["APIC"].front()); + CPPUNIT_ASSERT(frame); + +#endif + } + + void testW000() + { + MPEG::File f(TEST_FILE_PATH_C("w000.mp3"), false); + CPPUNIT_ASSERT(f.ID3v2Tag()->frameListMap().contains("W000")); + ID3v2::UrlLinkFrame *frame = + dynamic_cast<TagLib::ID3v2::UrlLinkFrame*>(f.ID3v2Tag()->frameListMap()["W000"].front()); + CPPUNIT_ASSERT(frame); + CPPUNIT_ASSERT_EQUAL(String("lukas.lalinsky@example.com____"), frame->url()); + } + + void testPropertyInterface() + { + ScopedFileCopy copy("rare_frames", ".mp3"); + string newname = copy.fileName(); + MPEG::File f(newname.c_str()); + PropertyMap dict = f.ID3v2Tag(false)->properties(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(6), dict.size()); + + CPPUNIT_ASSERT(dict.contains("USERTEXTDESCRIPTION1")); + CPPUNIT_ASSERT(dict.contains("QuodLibet::USERTEXTDESCRIPTION2")); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), dict["USERTEXTDESCRIPTION1"].size()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), dict["QuodLibet::USERTEXTDESCRIPTION2"].size()); + CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["USERTEXTDESCRIPTION1"][0]); + CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["USERTEXTDESCRIPTION1"][1]); + CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["QuodLibet::USERTEXTDESCRIPTION2"][0]); + CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["QuodLibet::USERTEXTDESCRIPTION2"][1]); + + CPPUNIT_ASSERT_EQUAL(String("Pop"), dict["GENRE"].front()); + + CPPUNIT_ASSERT_EQUAL(String("http://a.user.url"), dict["URL:USERURL"].front()); + + CPPUNIT_ASSERT_EQUAL(String("http://a.user.url/with/empty/description"), dict["URL"].front()); + CPPUNIT_ASSERT_EQUAL(String("A COMMENT"), dict["COMMENT"].front()); + + CPPUNIT_ASSERT_EQUAL(1u, dict.unsupportedData().size()); + CPPUNIT_ASSERT_EQUAL(String("UFID/supermihi@web.de"), dict.unsupportedData().front()); + } + + void testPropertyInterface2() + { + ID3v2::Tag tag; + ID3v2::UnsynchronizedLyricsFrame *frame1 = new ID3v2::UnsynchronizedLyricsFrame(); + frame1->setDescription("test"); + frame1->setText("la-la-la test"); + tag.addFrame(frame1); + + ID3v2::UnsynchronizedLyricsFrame *frame2 = new ID3v2::UnsynchronizedLyricsFrame(); + frame2->setDescription(""); + frame2->setText("la-la-la nodescription"); + tag.addFrame(frame2); + + ID3v2::AttachedPictureFrame *frame3 = new ID3v2::AttachedPictureFrame(); + frame3->setDescription("test picture"); + tag.addFrame(frame3); + + ID3v2::TextIdentificationFrame *frame4 = new ID3v2::TextIdentificationFrame("TIPL"); + frame4->setText("single value is invalid for TIPL"); + tag.addFrame(frame4); + + ID3v2::TextIdentificationFrame *frame5 = new ID3v2::TextIdentificationFrame("TMCL"); + StringList tmclData; + tmclData.append("VIOLIN"); + tmclData.append("a violinist"); + tmclData.append("PIANO"); + tmclData.append("a pianist"); + frame5->setText(tmclData); + tag.addFrame(frame5); + + ID3v2::UniqueFileIdentifierFrame *frame6 = new ID3v2::UniqueFileIdentifierFrame("http://musicbrainz.org", "152454b9-19ba-49f3-9fc9-8fc26545cf41"); + tag.addFrame(frame6); + + ID3v2::UniqueFileIdentifierFrame *frame7 = new ID3v2::UniqueFileIdentifierFrame("http://example.com", "123"); + tag.addFrame(frame7); + + ID3v2::UserTextIdentificationFrame *frame8 = new ID3v2::UserTextIdentificationFrame(); + frame8->setDescription("MusicBrainz Album Id"); + frame8->setText("95c454a5-d7e0-4d8f-9900-db04aca98ab3"); + tag.addFrame(frame8); + + PropertyMap properties = tag.properties(); + + CPPUNIT_ASSERT_EQUAL(3u, properties.unsupportedData().size()); + CPPUNIT_ASSERT(properties.unsupportedData().contains("TIPL")); + CPPUNIT_ASSERT(properties.unsupportedData().contains("APIC")); + CPPUNIT_ASSERT(properties.unsupportedData().contains("UFID/http://example.com")); + + CPPUNIT_ASSERT(properties.contains("PERFORMER:VIOLIN")); + CPPUNIT_ASSERT(properties.contains("PERFORMER:PIANO")); + CPPUNIT_ASSERT_EQUAL(String("a violinist"), properties["PERFORMER:VIOLIN"].front()); + CPPUNIT_ASSERT_EQUAL(String("a pianist"), properties["PERFORMER:PIANO"].front()); + + CPPUNIT_ASSERT(properties.contains("LYRICS")); + CPPUNIT_ASSERT(properties.contains("LYRICS:TEST")); + + CPPUNIT_ASSERT(properties.contains("MUSICBRAINZ_TRACKID")); + CPPUNIT_ASSERT_EQUAL(String("152454b9-19ba-49f3-9fc9-8fc26545cf41"), properties["MUSICBRAINZ_TRACKID"].front()); + + CPPUNIT_ASSERT(properties.contains("MUSICBRAINZ_ALBUMID")); + CPPUNIT_ASSERT_EQUAL(String("95c454a5-d7e0-4d8f-9900-db04aca98ab3"), properties["MUSICBRAINZ_ALBUMID"].front()); + + tag.removeUnsupportedProperties(properties.unsupportedData()); + CPPUNIT_ASSERT(tag.frameList("APIC").isEmpty()); + CPPUNIT_ASSERT(tag.frameList("TIPL").isEmpty()); + CPPUNIT_ASSERT_EQUAL((ID3v2::UniqueFileIdentifierFrame *)0, ID3v2::UniqueFileIdentifierFrame::findByOwner(&tag, "http://example.com")); + CPPUNIT_ASSERT_EQUAL(frame6, ID3v2::UniqueFileIdentifierFrame::findByOwner(&tag, "http://musicbrainz.org")); + } + + void testDeleteFrame() + { + ScopedFileCopy copy("rare_frames", ".mp3"); + string newname = copy.fileName(); + MPEG::File f(newname.c_str()); + ID3v2::Tag *t = f.ID3v2Tag(); + ID3v2::Frame *frame = t->frameList("TCON")[0]; + CPPUNIT_ASSERT_EQUAL(1u, t->frameList("TCON").size()); + t->removeFrame(frame, true); + f.save(MPEG::File::ID3v2); + + MPEG::File f2(newname.c_str()); + t = f2.ID3v2Tag(); + CPPUNIT_ASSERT(t->frameList("TCON").isEmpty()); + } + + void testSaveAndStripID3v1ShouldNotAddFrameFromID3v1ToId3v2() + { + ScopedFileCopy copy("xing", ".mp3"); + string newname = copy.fileName(); + + { + MPEG::File foo(newname.c_str()); + foo.tag()->setArtist("Artist"); + foo.save(MPEG::File::ID3v1 | MPEG::File::ID3v2); + } + + { + MPEG::File bar(newname.c_str()); + bar.ID3v2Tag()->removeFrames("TPE1"); + // Should strip ID3v1 here and not add old values to ID3v2 again + bar.save(MPEG::File::ID3v2, true); + } + + MPEG::File f(newname.c_str()); + CPPUNIT_ASSERT(!f.ID3v2Tag()->frameListMap().contains("TPE1")); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestID3v2); diff --git a/taglib/tests/test_info.cpp b/taglib/tests/test_info.cpp new file mode 100644 index 0000000..8e0d715 --- /dev/null +++ b/taglib/tests/test_info.cpp @@ -0,0 +1,49 @@ +#include <string> +#include <stdio.h> +#include <infotag.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestInfoTag : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestInfoTag); + CPPUNIT_TEST(testTitle); + CPPUNIT_TEST(testNumericFields); + CPPUNIT_TEST_SUITE_END(); + +public: + void testTitle() + { + RIFF::Info::Tag tag; + + CPPUNIT_ASSERT_EQUAL(String(""), tag.title()); + tag.setTitle("Test title 1"); + tag.setFieldText("TEST", "Dummy Text"); + + CPPUNIT_ASSERT_EQUAL(String("Test title 1"), tag.title()); + + RIFF::Info::FieldListMap map = tag.fieldListMap(); + CPPUNIT_ASSERT_EQUAL(String("Test title 1"), map["INAM"]); + CPPUNIT_ASSERT_EQUAL(String("Dummy Text"), map["TEST"]); + } + + void testNumericFields() + { + RIFF::Info::Tag tag; + + CPPUNIT_ASSERT_EQUAL((uint)0, tag.track()); + tag.setTrack(1234); + CPPUNIT_ASSERT_EQUAL((uint)1234, tag.track()); + CPPUNIT_ASSERT_EQUAL(String("1234"), tag.fieldText("IPRT")); + + CPPUNIT_ASSERT_EQUAL((uint)0, tag.year()); + tag.setYear(1234); + CPPUNIT_ASSERT_EQUAL((uint)1234, tag.year()); + CPPUNIT_ASSERT_EQUAL(String("1234"), tag.fieldText("ICRD")); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestInfoTag); diff --git a/taglib/tests/test_it.cpp b/taglib/tests/test_it.cpp new file mode 100644 index 0000000..be5680e --- /dev/null +++ b/taglib/tests/test_it.cpp @@ -0,0 +1,136 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include <itfile.h> +#include <tstringlist.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; +using TagLib::uint; + +static const String titleBefore("test song name"); +static const String titleAfter("changed title"); + +static const String commentBefore( + "This is a sample name.\n" + "In module file formats\n" + "sample names are abused\n" + "as multiline comments.\n" + " "); + +static const String newComment( + "This is a sample name!\n" + "In module file formats\n" + "sample names are abused\n" + "as multiline comments.\n" + "-----------------------------------\n" + "The previous line is truncated but starting with this line\n" + "the comment is not limeted in the line length but to 8000\n" + "additional characters (bytes).\n" + "\n" + "This is because it is saved in the 'message' proportion of\n" + "IT files."); + +static const String commentAfter( + "This is a sample name!\n" + "In module file formats\n" + "sample names are abused\n" + "as multiline comments.\n" + "-------------------------\n" + "The previous line is truncated but starting with this line\n" + "the comment is not limeted in the line length but to 8000\n" + "additional characters (bytes).\n" + "\n" + "This is because it is saved in the 'message' proportion of\n" + "IT files."); + +class TestIT : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestIT); + CPPUNIT_TEST(testReadTags); + CPPUNIT_TEST(testWriteTags); + CPPUNIT_TEST_SUITE_END(); + +public: + void testReadTags() + { + testRead(TEST_FILE_PATH_C("test.it"), titleBefore, commentBefore); + } + + void testWriteTags() + { + ScopedFileCopy copy("test", ".it"); + { + IT::File file(copy.fileName().c_str()); + CPPUNIT_ASSERT(file.tag() != 0); + file.tag()->setTitle(titleAfter); + file.tag()->setComment(newComment); + file.tag()->setTrackerName("won't be saved"); + CPPUNIT_ASSERT(file.save()); + } + testRead(copy.fileName().c_str(), titleAfter, commentAfter); + } + +private: + void testRead(FileName fileName, const String &title, const String &comment) + { + IT::File file(fileName); + + CPPUNIT_ASSERT(file.isValid()); + + IT::Properties *p = file.audioProperties(); + Mod::Tag *t = file.tag(); + + CPPUNIT_ASSERT(0 != p); + CPPUNIT_ASSERT(0 != t); + + CPPUNIT_ASSERT_EQUAL( 0, p->length()); + CPPUNIT_ASSERT_EQUAL( 0, p->bitrate()); + CPPUNIT_ASSERT_EQUAL( 0, p->sampleRate()); + CPPUNIT_ASSERT_EQUAL(64, p->channels()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 0, p->lengthInPatterns()); + CPPUNIT_ASSERT_EQUAL(true, p->stereo()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 0, p->instrumentCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 5, p->sampleCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 1, p->patternCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort)535, p->version()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort)532, p->compatibleVersion()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 9, p->flags()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar)128, p->globalVolume()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar) 48, p->mixVolume()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar)125, p->tempo()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar) 6, p->bpmSpeed()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar)128, p->panningSeparation()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar) 0, p->pitchWheelDepth()); + CPPUNIT_ASSERT_EQUAL(title, t->title()); + CPPUNIT_ASSERT_EQUAL(String::null, t->artist()); + CPPUNIT_ASSERT_EQUAL(String::null, t->album()); + CPPUNIT_ASSERT_EQUAL(comment, t->comment()); + CPPUNIT_ASSERT_EQUAL(String::null, t->genre()); + CPPUNIT_ASSERT_EQUAL(0U, t->year()); + CPPUNIT_ASSERT_EQUAL(0U, t->track()); + CPPUNIT_ASSERT_EQUAL(String("Impulse Tracker"), t->trackerName()); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestIT); diff --git a/taglib/tests/test_list.cpp b/taglib/tests/test_list.cpp new file mode 100644 index 0000000..39cbaf0 --- /dev/null +++ b/taglib/tests/test_list.cpp @@ -0,0 +1,58 @@ +/* Copyright (C) 2003 Scott Wheeler <wheeler@kde.org> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <cppunit/extensions/HelperMacros.h> +#include <tlist.h> + +using namespace std; +using namespace TagLib; + +class TestList : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestList); + CPPUNIT_TEST(testList); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testList() + { + List<int> l1; + List<int> l2; + List<int> l3; + l1.append(2); + l2.append(3); + l2.append(4); + l1.append(l2); + l1.prepend(1); + l3.append(1); + l3.append(2); + l3.append(3); + l3.append(4); + CPPUNIT_ASSERT(l1 == l3); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestList); diff --git a/taglib/tests/test_map.cpp b/taglib/tests/test_map.cpp new file mode 100644 index 0000000..b6f77aa --- /dev/null +++ b/taglib/tests/test_map.cpp @@ -0,0 +1,27 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <tstring.h> +#include <tmap.h> + +using namespace std; +using namespace TagLib; + +class TestMap : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestMap); + CPPUNIT_TEST(testInsert); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testInsert() + { + Map<String, int> m; + m.insert("foo", 3); + CPPUNIT_ASSERT_EQUAL(3, m["foo"]); + m.insert("foo", 7); + CPPUNIT_ASSERT_EQUAL(7, m["foo"]); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestMap); diff --git a/taglib/tests/test_mod.cpp b/taglib/tests/test_mod.cpp new file mode 100644 index 0000000..0a233c9 --- /dev/null +++ b/taglib/tests/test_mod.cpp @@ -0,0 +1,126 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include <modfile.h> +#include <tpropertymap.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +static const String titleBefore("title of song"); +static const String titleAfter("changed title"); + +static const String commentBefore( + "Instrument names\n" + "are abused as\n" + "comments in\n" + "module file formats.\n" + "-+-+-+-+-+-+-+-+-+-+-+\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); + +static const String newComment( + "This line will be truncated because it is too long for a mod instrument name.\n" + "This line is ok."); + +static const String commentAfter( + "This line will be trun\n" + "This line is ok.\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); + +class TestMod : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestMod); + CPPUNIT_TEST(testReadTags); + CPPUNIT_TEST(testWriteTags); + CPPUNIT_TEST(testPropertyInterface); + CPPUNIT_TEST_SUITE_END(); + +public: + void testReadTags() + { + testRead(TEST_FILE_PATH_C("test.mod"), titleBefore, commentBefore); + } + + void testWriteTags() + { + ScopedFileCopy copy("test", ".mod"); + { + Mod::File file(copy.fileName().c_str()); + CPPUNIT_ASSERT(file.tag() != 0); + file.tag()->setTitle(titleAfter); + file.tag()->setComment(newComment); + CPPUNIT_ASSERT(file.save()); + } + testRead(copy.fileName().c_str(), titleAfter, commentAfter); + CPPUNIT_ASSERT(fileEqual( + copy.fileName(), + TEST_FILE_PATH_C("changed.mod"))); + } + + void testPropertyInterface() + { + Mod::Tag t; + PropertyMap properties; + properties["BLA"] = String("bla"); + properties["ARTIST"] = String("artist1"); + properties["ARTIST"].append("artist2"); + properties["TITLE"] = String("title"); + + PropertyMap unsupported = t.setProperties(properties); + CPPUNIT_ASSERT(unsupported.contains("BLA")); + CPPUNIT_ASSERT(unsupported.contains("ARTIST")); + CPPUNIT_ASSERT_EQUAL(properties["ARTIST"], unsupported["ARTIST"]); + CPPUNIT_ASSERT(!unsupported.contains("TITLE")); + } + +private: + void testRead(FileName fileName, const String &title, const String &comment) + { + Mod::File file(fileName); + + CPPUNIT_ASSERT(file.isValid()); + + Mod::Properties *p = file.audioProperties(); + Mod::Tag *t = file.tag(); + + CPPUNIT_ASSERT(0 != p); + CPPUNIT_ASSERT(0 != t); + + CPPUNIT_ASSERT_EQUAL(0, p->length()); + CPPUNIT_ASSERT_EQUAL(0, p->bitrate()); + CPPUNIT_ASSERT_EQUAL(0, p->sampleRate()); + CPPUNIT_ASSERT_EQUAL(8, p->channels()); + CPPUNIT_ASSERT_EQUAL(31U, p->instrumentCount()); + CPPUNIT_ASSERT_EQUAL((uchar)1, p->lengthInPatterns()); + CPPUNIT_ASSERT_EQUAL(title, t->title()); + CPPUNIT_ASSERT_EQUAL(String::null, t->artist()); + CPPUNIT_ASSERT_EQUAL(String::null, t->album()); + CPPUNIT_ASSERT_EQUAL(comment, t->comment()); + CPPUNIT_ASSERT_EQUAL(String::null, t->genre()); + CPPUNIT_ASSERT_EQUAL(0U, t->year()); + CPPUNIT_ASSERT_EQUAL(0U, t->track()); + CPPUNIT_ASSERT_EQUAL(String("StarTrekker"), t->trackerName()); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestMod); diff --git a/taglib/tests/test_mp4.cpp b/taglib/tests/test_mp4.cpp new file mode 100644 index 0000000..56b6052 --- /dev/null +++ b/taglib/tests/test_mp4.cpp @@ -0,0 +1,282 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <mp4tag.h> +#include <tbytevectorlist.h> +#include <tpropertymap.h> +#include <mp4atom.h> +#include <mp4file.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestMP4 : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestMP4); + CPPUNIT_TEST(testPropertiesAAC); + CPPUNIT_TEST(testPropertiesALAC); + CPPUNIT_TEST(testFreeForm); + CPPUNIT_TEST(testCheckValid); + CPPUNIT_TEST(testUpdateStco); + CPPUNIT_TEST(testSaveExisingWhenIlstIsLast); + CPPUNIT_TEST(test64BitAtom); + CPPUNIT_TEST(testGnre); + CPPUNIT_TEST(testCovrRead); + CPPUNIT_TEST(testCovrWrite); + CPPUNIT_TEST(testCovrRead2); + CPPUNIT_TEST(testProperties); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testPropertiesAAC() + { + MP4::File f(TEST_FILE_PATH_C("has-tags.m4a")); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, ((MP4::Properties *)f.audioProperties())->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(MP4::Properties::AAC, ((MP4::Properties *)f.audioProperties())->codec()); + } + + void testPropertiesALAC() + { + MP4::File f(TEST_FILE_PATH_C("empty_alac.m4a")); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, ((MP4::Properties *)f.audioProperties())->bitsPerSample()); + CPPUNIT_ASSERT_EQUAL(MP4::Properties::ALAC, ((MP4::Properties *)f.audioProperties())->codec()); + } + + void testCheckValid() + { + MP4::File f(TEST_FILE_PATH_C("empty.aiff")); + CPPUNIT_ASSERT(!f.isValid()); + MP4::File f2(TEST_FILE_PATH_C("has-tags.m4a")); + CPPUNIT_ASSERT(f2.isValid()); + } + + void testUpdateStco() + { + ScopedFileCopy copy("no-tags", ".3g2"); + string filename = copy.fileName(); + + MP4::File *f = new MP4::File(filename.c_str()); + f->tag()->setArtist(ByteVector(3000, 'x')); + + ByteVectorList data1; + { + MP4::Atoms a(f); + MP4::Atom *stco = a.find("moov")->findall("stco", true)[0]; + f->seek(stco->offset + 12); + ByteVector data = f->readBlock(stco->length - 12); + unsigned int count = data.mid(0, 4).toUInt(); + int pos = 4; + while (count--) { + unsigned int offset = data.mid(pos, 4).toUInt(); + f->seek(offset); + data1.append(f->readBlock(20)); + pos += 4; + } + } + + f->save(); + delete f; + f = new MP4::File(filename.c_str()); + + { + MP4::Atoms a(f); + MP4::Atom *stco = a.find("moov")->findall("stco", true)[0]; + f->seek(stco->offset + 12); + ByteVector data = f->readBlock(stco->length - 12); + unsigned int count = data.mid(0, 4).toUInt(); + int pos = 4, i = 0; + while (count--) { + unsigned int offset = data.mid(pos, 4).toUInt(); + f->seek(offset); + CPPUNIT_ASSERT_EQUAL(data1[i], f->readBlock(20)); + pos += 4; + i++; + } + } + + delete f; + } + + void testFreeForm() + { + ScopedFileCopy copy("has-tags", ".m4a"); + string filename = copy.fileName(); + + MP4::File *f = new MP4::File(filename.c_str()); + CPPUNIT_ASSERT(f->tag()->itemListMap().contains("----:com.apple.iTunes:iTunNORM")); + f->tag()->itemListMap()["----:org.kde.TagLib:Foo"] = StringList("Bar"); + f->save(); + delete f; + + f = new MP4::File(filename.c_str()); + CPPUNIT_ASSERT(f->tag()->itemListMap().contains("----:org.kde.TagLib:Foo")); + CPPUNIT_ASSERT_EQUAL(String("Bar"), f->tag()->itemListMap()["----:org.kde.TagLib:Foo"].toStringList()[0]); + f->save(); + delete f; + } + + void testSaveExisingWhenIlstIsLast() + { + ScopedFileCopy copy("ilst-is-last", ".m4a"); + string filename = copy.fileName(); + + MP4::File *f = new MP4::File(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(String("82,164"), f->tag()->itemListMap()["----:com.apple.iTunes:replaygain_track_minmax"].toStringList()[0]); + CPPUNIT_ASSERT_EQUAL(String("Pearl Jam"), f->tag()->artist()); + f->tag()->setComment("foo"); + f->save(); + delete f; + + f = new MP4::File(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(String("82,164"), f->tag()->itemListMap()["----:com.apple.iTunes:replaygain_track_minmax"].toStringList()[0]); + CPPUNIT_ASSERT_EQUAL(String("Pearl Jam"), f->tag()->artist()); + CPPUNIT_ASSERT_EQUAL(String("foo"), f->tag()->comment()); + } + + void test64BitAtom() + { + ScopedFileCopy copy("64bit", ".mp4"); + string filename = copy.fileName(); + + MP4::File *f = new MP4::File(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(true, f->tag()->itemListMap()["cpil"].toBool()); + + MP4::Atoms *atoms = new MP4::Atoms(f); + MP4::Atom *moov = atoms->atoms[0]; + CPPUNIT_ASSERT_EQUAL(long(77), moov->length); + + f->tag()->itemListMap()["pgap"] = true; + f->save(); + + f = new MP4::File(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(true, f->tag()->itemListMap()["cpil"].toBool()); + CPPUNIT_ASSERT_EQUAL(true, f->tag()->itemListMap()["pgap"].toBool()); + + atoms = new MP4::Atoms(f); + moov = atoms->atoms[0]; + // original size + 'pgap' size + padding + CPPUNIT_ASSERT_EQUAL(long(77 + 25 + 974), moov->length); + } + + void testGnre() + { + MP4::File *f = new MP4::File(TEST_FILE_PATH_C("gnre.m4a")); + CPPUNIT_ASSERT_EQUAL(TagLib::String("Ska"), f->tag()->genre()); + delete f; + } + + void testCovrRead() + { + MP4::File *f = new MP4::File(TEST_FILE_PATH_C("has-tags.m4a")); + CPPUNIT_ASSERT(f->tag()->itemListMap().contains("covr")); + MP4::CoverArtList l = f->tag()->itemListMap()["covr"].toCoverArtList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), l.size()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, l[0].format()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(79), l[0].data().size()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::JPEG, l[1].format()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(287), l[1].data().size()); + delete f; + } + + void testCovrWrite() + { + ScopedFileCopy copy("has-tags", ".m4a"); + string filename = copy.fileName(); + + MP4::File *f = new MP4::File(filename.c_str()); + CPPUNIT_ASSERT(f->tag()->itemListMap().contains("covr")); + MP4::CoverArtList l = f->tag()->itemListMap()["covr"].toCoverArtList(); + l.append(MP4::CoverArt(MP4::CoverArt::PNG, "foo")); + f->tag()->itemListMap()["covr"] = l; + f->save(); + delete f; + + f = new MP4::File(filename.c_str()); + CPPUNIT_ASSERT(f->tag()->itemListMap().contains("covr")); + l = f->tag()->itemListMap()["covr"].toCoverArtList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), l.size()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, l[0].format()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(79), l[0].data().size()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::JPEG, l[1].format()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(287), l[1].data().size()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, l[2].format()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), l[2].data().size()); + delete f; + } + + void testCovrRead2() + { + MP4::File *f = new MP4::File(TEST_FILE_PATH_C("covr-junk.m4a")); + CPPUNIT_ASSERT(f->tag()->itemListMap().contains("covr")); + MP4::CoverArtList l = f->tag()->itemListMap()["covr"].toCoverArtList(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), l.size()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, l[0].format()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(79), l[0].data().size()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::JPEG, l[1].format()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(287), l[1].data().size()); + delete f; + } + + void testProperties() + { + MP4::File f(TEST_FILE_PATH_C("has-tags.m4a")); + + PropertyMap tags = f.properties(); + + CPPUNIT_ASSERT_EQUAL(StringList("Test Artist"), tags["ARTIST"]); + + tags["TRACKNUMBER"] = StringList("2/4"); + tags["DISCNUMBER"] = StringList("3/5"); + tags["BPM"] = StringList("123"); + tags["ARTIST"] = StringList("Foo Bar"); + tags["COMPILATION"] = StringList("1"); + f.setProperties(tags); + + tags = f.properties(); + + CPPUNIT_ASSERT(f.tag()->itemListMap().contains("trkn")); + CPPUNIT_ASSERT_EQUAL(2, f.tag()->itemListMap()["trkn"].toIntPair().first); + CPPUNIT_ASSERT_EQUAL(4, f.tag()->itemListMap()["trkn"].toIntPair().second); + CPPUNIT_ASSERT_EQUAL(StringList("2/4"), tags["TRACKNUMBER"]); + + CPPUNIT_ASSERT(f.tag()->itemListMap().contains("disk")); + CPPUNIT_ASSERT_EQUAL(3, f.tag()->itemListMap()["disk"].toIntPair().first); + CPPUNIT_ASSERT_EQUAL(5, f.tag()->itemListMap()["disk"].toIntPair().second); + CPPUNIT_ASSERT_EQUAL(StringList("3/5"), tags["DISCNUMBER"]); + + CPPUNIT_ASSERT(f.tag()->itemListMap().contains("tmpo")); + CPPUNIT_ASSERT_EQUAL(123, f.tag()->itemListMap()["tmpo"].toInt()); + CPPUNIT_ASSERT_EQUAL(StringList("123"), tags["BPM"]); + + CPPUNIT_ASSERT(f.tag()->itemListMap().contains("\251ART")); + CPPUNIT_ASSERT_EQUAL(StringList("Foo Bar"), f.tag()->itemListMap()["\251ART"].toStringList()); + CPPUNIT_ASSERT_EQUAL(StringList("Foo Bar"), tags["ARTIST"]); + + CPPUNIT_ASSERT(f.tag()->itemListMap().contains("cpil")); + CPPUNIT_ASSERT_EQUAL(true, f.tag()->itemListMap()["cpil"].toBool()); + CPPUNIT_ASSERT_EQUAL(StringList("1"), tags["COMPILATION"]); + + tags["COMPILATION"] = StringList("0"); + f.setProperties(tags); + + tags = f.properties(); + + CPPUNIT_ASSERT(f.tag()->itemListMap().contains("cpil")); + CPPUNIT_ASSERT_EQUAL(false, f.tag()->itemListMap()["cpil"].toBool()); + CPPUNIT_ASSERT_EQUAL(StringList("0"), tags["COMPILATION"]); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4); diff --git a/taglib/tests/test_mp4coverart.cpp b/taglib/tests/test_mp4coverart.cpp new file mode 100644 index 0000000..812a2e5 --- /dev/null +++ b/taglib/tests/test_mp4coverart.cpp @@ -0,0 +1,49 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <mp4coverart.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestMP4CoverArt : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestMP4CoverArt); + CPPUNIT_TEST(testSimple); + CPPUNIT_TEST(testList); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testSimple() + { + MP4::CoverArt c(MP4::CoverArt::PNG, "foo"); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, c.format()); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), c.data()); + + MP4::CoverArt c2(c); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, c2.format()); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), c2.data()); + + MP4::CoverArt c3 = c; + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, c3.format()); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), c3.data()); + } + + void testList() + { + MP4::CoverArtList l; + l.append(MP4::CoverArt(MP4::CoverArt::PNG, "foo")); + l.append(MP4::CoverArt(MP4::CoverArt::JPEG, "bar")); + + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, l[0].format()); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), l[0].data()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::JPEG, l[1].format()); + CPPUNIT_ASSERT_EQUAL(ByteVector("bar"), l[1].data()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4CoverArt); diff --git a/taglib/tests/test_mp4item.cpp b/taglib/tests/test_mp4item.cpp new file mode 100644 index 0000000..37d3f71 --- /dev/null +++ b/taglib/tests/test_mp4item.cpp @@ -0,0 +1,37 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <mp4coverart.h> +#include <mp4item.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestMP4Item : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestMP4Item); + CPPUNIT_TEST(testCoverArtList); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testCoverArtList() + { + MP4::CoverArtList l; + l.append(MP4::CoverArt(MP4::CoverArt::PNG, "foo")); + l.append(MP4::CoverArt(MP4::CoverArt::JPEG, "bar")); + + MP4::Item i(l); + MP4::CoverArtList l2 = i.toCoverArtList(); + + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::PNG, l[0].format()); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), l[0].data()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::JPEG, l[1].format()); + CPPUNIT_ASSERT_EQUAL(ByteVector("bar"), l[1].data()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4Item); diff --git a/taglib/tests/test_mpc.cpp b/taglib/tests/test_mpc.cpp new file mode 100644 index 0000000..12da6ed --- /dev/null +++ b/taglib/tests/test_mpc.cpp @@ -0,0 +1,66 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <mpcfile.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestMPC : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestMPC); + CPPUNIT_TEST(testPropertiesSV8); + CPPUNIT_TEST(testPropertiesSV7); + CPPUNIT_TEST(testPropertiesSV5); + CPPUNIT_TEST(testPropertiesSV4); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testPropertiesSV8() + { + MPC::File f(TEST_FILE_PATH_C("sv8_header.mpc")); + CPPUNIT_ASSERT_EQUAL(8, f.audioProperties()->mpcVersion()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + } + + void testPropertiesSV7() + { + MPC::File f(TEST_FILE_PATH_C("click.mpc")); + CPPUNIT_ASSERT_EQUAL(7, f.audioProperties()->mpcVersion()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + } + + void testPropertiesSV5() + { + MPC::File f(TEST_FILE_PATH_C("sv5_header.mpc")); + CPPUNIT_ASSERT_EQUAL(5, f.audioProperties()->mpcVersion()); + CPPUNIT_ASSERT_EQUAL(26, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + } + + void testPropertiesSV4() + { + MPC::File f(TEST_FILE_PATH_C("sv4_header.mpc")); + CPPUNIT_ASSERT_EQUAL(4, f.audioProperties()->mpcVersion()); + CPPUNIT_ASSERT_EQUAL(26, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(2, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestMPC); diff --git a/taglib/tests/test_mpeg.cpp b/taglib/tests/test_mpeg.cpp new file mode 100644 index 0000000..ad6acc4 --- /dev/null +++ b/taglib/tests/test_mpeg.cpp @@ -0,0 +1,88 @@ +#include <string> +#include <stdio.h> +#include <tstring.h> +#include <mpegfile.h> +#include <id3v2tag.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestMPEG : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestMPEG); + CPPUNIT_TEST(testVersion2DurationWithXingHeader); + CPPUNIT_TEST(testSaveID3v24); + CPPUNIT_TEST(testSaveID3v24WrongParam); + CPPUNIT_TEST(testSaveID3v23); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testVersion2DurationWithXingHeader() + { + MPEG::File f(TEST_FILE_PATH_C("mpeg2.mp3")); + CPPUNIT_ASSERT_EQUAL(5387, f.audioProperties()->length()); + } + + void testSaveID3v24() + { + ScopedFileCopy copy("xing", ".mp3"); + string newname = copy.fileName(); + + String xxx = ByteVector(254, 'X'); + MPEG::File f(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(false, f.hasID3v2Tag()); + + f.tag()->setTitle(xxx); + f.tag()->setArtist("Artist A"); + f.save(MPEG::File::AllTags, true, 4); + CPPUNIT_ASSERT_EQUAL(true, f.hasID3v2Tag()); + + MPEG::File f2(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4), f2.ID3v2Tag()->header()->majorVersion()); + CPPUNIT_ASSERT_EQUAL(String("Artist A"), f2.tag()->artist()); + CPPUNIT_ASSERT_EQUAL(xxx, f2.tag()->title()); + } + + void testSaveID3v24WrongParam() + { + ScopedFileCopy copy("xing", ".mp3"); + string newname = copy.fileName(); + + String xxx = ByteVector(254, 'X'); + MPEG::File f(newname.c_str()); + f.tag()->setTitle(xxx); + f.tag()->setArtist("Artist A"); + f.save(MPEG::File::AllTags, true, 8); + + MPEG::File f2(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4), f2.ID3v2Tag()->header()->majorVersion()); + CPPUNIT_ASSERT_EQUAL(String("Artist A"), f2.tag()->artist()); + CPPUNIT_ASSERT_EQUAL(xxx, f2.tag()->title()); + } + + void testSaveID3v23() + { + ScopedFileCopy copy("xing", ".mp3"); + string newname = copy.fileName(); + + String xxx = ByteVector(254, 'X'); + MPEG::File f(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(false, f.hasID3v2Tag()); + + f.tag()->setTitle(xxx); + f.tag()->setArtist("Artist A"); + f.save(MPEG::File::AllTags, true, 3); + CPPUNIT_ASSERT_EQUAL(true, f.hasID3v2Tag()); + + MPEG::File f2(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), f2.ID3v2Tag()->header()->majorVersion()); + CPPUNIT_ASSERT_EQUAL(String("Artist A"), f2.tag()->artist()); + CPPUNIT_ASSERT_EQUAL(xxx, f2.tag()->title()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestMPEG); diff --git a/taglib/tests/test_ogg.cpp b/taglib/tests/test_ogg.cpp new file mode 100644 index 0000000..c89586a --- /dev/null +++ b/taglib/tests/test_ogg.cpp @@ -0,0 +1,104 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <tpropertymap.h> +#include <oggfile.h> +#include <vorbisfile.h> +#include <oggpageheader.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestOGG : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestOGG); + CPPUNIT_TEST(testSimple); + CPPUNIT_TEST(testSplitPackets); + CPPUNIT_TEST(testDictInterface1); + CPPUNIT_TEST(testDictInterface2); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testSimple() + { + ScopedFileCopy copy("empty", ".ogg"); + string newname = copy.fileName(); + + Vorbis::File *f = new Vorbis::File(newname.c_str()); + f->tag()->setArtist("The Artist"); + f->save(); + delete f; + + f = new Vorbis::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String("The Artist"), f->tag()->artist()); + delete f; + } + + void testSplitPackets() + { + ScopedFileCopy copy("empty", ".ogg"); + string newname = copy.fileName(); + + Vorbis::File *f = new Vorbis::File(newname.c_str()); + f->tag()->addField("test", ByteVector(128 * 1024, 'x') + ByteVector(1, '\0')); + f->save(); + delete f; + + f = new Vorbis::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(19, f->lastPageHeader()->pageSequenceNumber()); + delete f; + } + + void testDictInterface1() + { + ScopedFileCopy copy("empty", ".ogg"); + string newname = copy.fileName(); + + Vorbis::File *f = new Vorbis::File(newname.c_str()); + + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f->tag()->properties().size()); + + PropertyMap newTags; + StringList values("value 1"); + values.append("value 2"); + newTags["ARTIST"] = values; + f->tag()->setProperties(newTags); + + PropertyMap map = f->tag()->properties(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), map.size()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), map["ARTIST"].size()); + CPPUNIT_ASSERT_EQUAL(String("value 1"), map["ARTIST"][0]); + delete f; + + } + + void testDictInterface2() + { + ScopedFileCopy copy("test", ".ogg"); + string newname = copy.fileName(); + + Vorbis::File *f = new Vorbis::File(newname.c_str()); + PropertyMap tags = f->tag()->properties(); + + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), tags["UNUSUALTAG"].size()); + CPPUNIT_ASSERT_EQUAL(String("usual value"), tags["UNUSUALTAG"][0]); + CPPUNIT_ASSERT_EQUAL(String("another value"), tags["UNUSUALTAG"][1]); + CPPUNIT_ASSERT_EQUAL(String("öäüoΣø", String::UTF8), tags["UNICODETAG"][0]); + + tags["UNICODETAG"][0] = String("νεω ναλυε", String::UTF8); + tags.erase("UNUSUALTAG"); + f->tag()->setProperties(tags); + CPPUNIT_ASSERT_EQUAL(String("νεω ναλυε", String::UTF8), f->tag()->properties()["UNICODETAG"][0]); + CPPUNIT_ASSERT_EQUAL(false, f->tag()->properties().contains("UNUSUALTAG")); + + delete f; + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestOGG); diff --git a/taglib/tests/test_oggflac.cpp b/taglib/tests/test_oggflac.cpp new file mode 100644 index 0000000..1cdb24b --- /dev/null +++ b/taglib/tests/test_oggflac.cpp @@ -0,0 +1,44 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tstringlist.h> +#include <tbytevectorlist.h> +#include <oggfile.h> +#include <oggflacfile.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestOggFLAC : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestOggFLAC); + CPPUNIT_TEST(testFramingBit); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testFramingBit() + { + ScopedFileCopy copy("empty_flac", ".oga"); + string newname = copy.fileName(); + + Ogg::FLAC::File *f = new Ogg::FLAC::File(newname.c_str()); + f->tag()->setArtist("The Artist"); + f->save(); + delete f; + + f = new Ogg::FLAC::File(newname.c_str()); + CPPUNIT_ASSERT_EQUAL(String("The Artist"), f->tag()->artist()); + + f->seek(0, File::End); + int size = f->tell(); + CPPUNIT_ASSERT_EQUAL(9134, size); + + delete f; + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestOggFLAC); diff --git a/taglib/tests/test_opus.cpp b/taglib/tests/test_opus.cpp new file mode 100644 index 0000000..769d398 --- /dev/null +++ b/taglib/tests/test_opus.cpp @@ -0,0 +1,61 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tbytevectorlist.h> +#include <opusfile.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestOpus : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestOpus); + CPPUNIT_TEST(testProperties); + CPPUNIT_TEST(testReadComments); + CPPUNIT_TEST(testWriteComments); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testProperties() + { + Ogg::Opus::File f(TEST_FILE_PATH_C("correctness_gain_silent_output.opus")); + CPPUNIT_ASSERT_EQUAL(7, f.audioProperties()->length()); + CPPUNIT_ASSERT_EQUAL(0, f.audioProperties()->bitrate()); + CPPUNIT_ASSERT_EQUAL(1, f.audioProperties()->channels()); + CPPUNIT_ASSERT_EQUAL(48000, f.audioProperties()->sampleRate()); + CPPUNIT_ASSERT_EQUAL(48000, ((Ogg::Opus::Properties *)f.audioProperties())->inputSampleRate()); + } + + void testReadComments() + { + Ogg::Opus::File f(TEST_FILE_PATH_C("correctness_gain_silent_output.opus")); + CPPUNIT_ASSERT_EQUAL(StringList("Xiph.Org Opus testvectormaker"), f.tag()->fieldListMap()["ENCODER"]); + CPPUNIT_ASSERT(f.tag()->fieldListMap().contains("TESTDESCRIPTION")); + CPPUNIT_ASSERT(!f.tag()->fieldListMap().contains("ARTIST")); + CPPUNIT_ASSERT_EQUAL(String("libopus 0.9.11-66-g64c2dd7"), f.tag()->vendorID()); + } + + void testWriteComments() + { + ScopedFileCopy copy("correctness_gain_silent_output", ".opus"); + string filename = copy.fileName(); + + Ogg::Opus::File *f = new Ogg::Opus::File(filename.c_str()); + f->tag()->setArtist("Your Tester"); + f->save(); + delete f; + + f = new Ogg::Opus::File(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(StringList("Xiph.Org Opus testvectormaker"), f->tag()->fieldListMap()["ENCODER"]); + CPPUNIT_ASSERT(f->tag()->fieldListMap().contains("TESTDESCRIPTION")); + CPPUNIT_ASSERT_EQUAL(StringList("Your Tester"), f->tag()->fieldListMap()["ARTIST"]); + CPPUNIT_ASSERT_EQUAL(String("libopus 0.9.11-66-g64c2dd7"), f->tag()->vendorID()); + delete f; + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestOpus); diff --git a/taglib/tests/test_propertymap.cpp b/taglib/tests/test_propertymap.cpp new file mode 100644 index 0000000..d0b2fbb --- /dev/null +++ b/taglib/tests/test_propertymap.cpp @@ -0,0 +1,32 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <tpropertymap.h> +class TestPropertyMap : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestPropertyMap); + CPPUNIT_TEST(testInvalidKeys); + CPPUNIT_TEST_SUITE_END(); + +public: + void testInvalidKeys() + { + TagLib::PropertyMap map1; + CPPUNIT_ASSERT(map1.isEmpty()); + map1["ÄÖÜ"].append("test"); + CPPUNIT_ASSERT_EQUAL(map1.size(), 1u); + + TagLib::PropertyMap map2; + map2["ÄÖÜ"].append("test"); + CPPUNIT_ASSERT(map1 == map2); + CPPUNIT_ASSERT(map1.contains(map2)); + + map2["ARTIST"] = TagLib::String("Test Artist"); + CPPUNIT_ASSERT(map1 != map2); + CPPUNIT_ASSERT(map2.contains(map1)); + + map2["ÄÖÜ"].append("test 2"); + CPPUNIT_ASSERT(!map2.contains(map1)); + + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestPropertyMap); diff --git a/taglib/tests/test_riff.cpp b/taglib/tests/test_riff.cpp new file mode 100644 index 0000000..8e120d0 --- /dev/null +++ b/taglib/tests/test_riff.cpp @@ -0,0 +1,198 @@ +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tbytevectorlist.h> +#include <rifffile.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class PublicRIFF : public RIFF::File +{ +public: + PublicRIFF(FileName file) : RIFF::File(file, BigEndian) {}; + TagLib::uint riffSize() { return RIFF::File::riffSize(); }; + TagLib::uint chunkCount() { return RIFF::File::chunkCount(); }; + TagLib::uint chunkOffset(TagLib::uint i) { return RIFF::File::chunkOffset(i); }; + TagLib::uint chunkPadding(TagLib::uint i) { return RIFF::File::chunkPadding(i); }; + TagLib::uint chunkDataSize(TagLib::uint i) { return RIFF::File::chunkDataSize(i); }; + ByteVector chunkName(TagLib::uint i) { return RIFF::File::chunkName(i); }; + ByteVector chunkData(TagLib::uint i) { return RIFF::File::chunkData(i); }; + void setChunkData(const ByteVector &name, const ByteVector &data) { + RIFF::File::setChunkData(name, data); + }; + virtual TagLib::Tag* tag() const { return 0; }; + virtual TagLib::AudioProperties* audioProperties() const { return 0;}; + virtual bool save() { return false; }; +}; + +class TestRIFF : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestRIFF); + CPPUNIT_TEST(testPadding); + CPPUNIT_TEST(testLastChunkAtEvenPosition); + CPPUNIT_TEST(testLastChunkAtEvenPosition2); + CPPUNIT_TEST(testLastChunkAtEvenPosition3); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testPadding() + { + ScopedFileCopy copy("empty", ".aiff"); + string filename = copy.fileName(); + + PublicRIFF *f = new PublicRIFF(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0x1728 + 8), f->chunkOffset(2)); + + f->setChunkData("TEST", "foo"); + delete f; + + f = new PublicRIFF(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), f->chunkData(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0x1728 + 8), f->chunkOffset(2)); + + f->setChunkData("SSND", "abcd"); + + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(1)); + CPPUNIT_ASSERT_EQUAL(ByteVector("abcd"), f->chunkData(1)); + + f->seek(f->chunkOffset(1)); + CPPUNIT_ASSERT_EQUAL(ByteVector("abcd"), f->readBlock(4)); + + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), f->chunkData(2)); + + f->seek(f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), f->readBlock(3)); + + delete f; + + f = new PublicRIFF(filename.c_str()); + + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(1)); + CPPUNIT_ASSERT_EQUAL(ByteVector("abcd"), f->chunkData(1)); + + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("foo"), f->chunkData(2)); + } + + void testLastChunkAtEvenPosition() + { + ScopedFileCopy copy("noise", ".aif"); + string filename = copy.fileName(); + + PublicRIFF *f = new PublicRIFF(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0xff0 + 8), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(long(4400), f->length()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4399 - 8), f->riffSize()); + f->setChunkData("TEST", "abcd"); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4088), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4408), f->chunkOffset(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4), f->chunkDataSize(3)); + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f->chunkPadding(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4412 - 8), f->riffSize()); + delete f; + + f = new PublicRIFF(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4088), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4408), f->chunkOffset(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4), f->chunkDataSize(3)); + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f->chunkPadding(3)); + CPPUNIT_ASSERT_EQUAL(long(4412), f->length()); + delete f; + } + + void testLastChunkAtEvenPosition2() + { + ScopedFileCopy copy("noise_odd", ".aif"); + string filename = copy.fileName(); + + PublicRIFF *f = new PublicRIFF(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0xff0 + 8), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(long(4399), f->length()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4399 - 8), f->riffSize()); + f->setChunkData("TEST", "abcd"); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4088), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4408), f->chunkOffset(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4), f->chunkDataSize(3)); + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f->chunkPadding(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4412 - 8), f->riffSize()); + delete f; + + f = new PublicRIFF(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4088), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4408), f->chunkOffset(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4), f->chunkDataSize(3)); + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f->chunkPadding(3)); + CPPUNIT_ASSERT_EQUAL(long(4412), f->length()); + delete f; + } + + void testLastChunkAtEvenPosition3() + { + ScopedFileCopy copy("noise_odd", ".aif"); + string filename = copy.fileName(); + + PublicRIFF *f = new PublicRIFF(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0xff0 + 8), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(long(4399), f->length()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4399 - 8), f->riffSize()); + f->setChunkData("TEST", "abc"); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4088), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4408), f->chunkOffset(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), f->chunkDataSize(3)); + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4411 - 8), f->riffSize()); + delete f; + + f = new PublicRIFF(filename.c_str()); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4088), f->chunkOffset(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(311), f->chunkDataSize(2)); + CPPUNIT_ASSERT_EQUAL(ByteVector("SSND"), f->chunkName(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(2)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(4408), f->chunkOffset(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), f->chunkDataSize(3)); + CPPUNIT_ASSERT_EQUAL(ByteVector("TEST"), f->chunkName(3)); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(1), f->chunkPadding(3)); + CPPUNIT_ASSERT_EQUAL(long(4412), f->length()); + delete f; + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestRIFF); diff --git a/taglib/tests/test_s3m.cpp b/taglib/tests/test_s3m.cpp new file mode 100644 index 0000000..24a4c6e --- /dev/null +++ b/taglib/tests/test_s3m.cpp @@ -0,0 +1,123 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include <s3mfile.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +static const String titleBefore("test song name"); +static const String titleAfter("changed title"); + +static const String commentBefore( + "This is an instrument name.\n" + "Module file formats\n" + "abuse instrument names\n" + "as multiline comments.\n" + " "); + +static const String newComment( + "This is an instrument name!\n" + "Module file formats\n" + "abuse instrument names\n" + "as multiline comments.\n" + "-----------------------------------\n" + "This line will be dropped and the previous is truncated."); + +static const String commentAfter( + "This is an instrument name!\n" + "Module file formats\n" + "abuse instrument names\n" + "as multiline comments.\n" + "---------------------------"); + +class TestS3M : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestS3M); + CPPUNIT_TEST(testReadTags); + CPPUNIT_TEST(testWriteTags); + CPPUNIT_TEST_SUITE_END(); + +public: + void testReadTags() + { + testRead(TEST_FILE_PATH_C("test.s3m"), titleBefore, commentBefore); + } + + void testWriteTags() + { + ScopedFileCopy copy("test", ".s3m"); + { + S3M::File file(copy.fileName().c_str()); + CPPUNIT_ASSERT(file.tag() != 0); + file.tag()->setTitle(titleAfter); + file.tag()->setComment(newComment); + file.tag()->setTrackerName("won't be saved"); + CPPUNIT_ASSERT(file.save()); + } + testRead(copy.fileName().c_str(), titleAfter, commentAfter); + CPPUNIT_ASSERT(fileEqual( + copy.fileName(), + TEST_FILE_PATH_C("changed.s3m"))); + } + +private: + void testRead(FileName fileName, const String &title, const String &comment) + { + S3M::File file(fileName); + + CPPUNIT_ASSERT(file.isValid()); + + S3M::Properties *p = file.audioProperties(); + Mod::Tag *t = file.tag(); + + CPPUNIT_ASSERT(0 != p); + CPPUNIT_ASSERT(0 != t); + + CPPUNIT_ASSERT_EQUAL( 0, p->length()); + CPPUNIT_ASSERT_EQUAL( 0, p->bitrate()); + CPPUNIT_ASSERT_EQUAL( 0, p->sampleRate()); + CPPUNIT_ASSERT_EQUAL(16, p->channels()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 0, p->lengthInPatterns()); + CPPUNIT_ASSERT_EQUAL(false, p->stereo()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 5, p->sampleCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 1, p->patternCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 0, p->flags()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort)4896, p->trackerVersion()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 2, p->fileFormatVersion()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar) 64, p->globalVolume()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar) 48, p->masterVolume()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar)125, p->tempo()); + CPPUNIT_ASSERT_EQUAL((TagLib::uchar) 6, p->bpmSpeed()); + CPPUNIT_ASSERT_EQUAL(title, t->title()); + CPPUNIT_ASSERT_EQUAL(String::null, t->artist()); + CPPUNIT_ASSERT_EQUAL(String::null, t->album()); + CPPUNIT_ASSERT_EQUAL(comment, t->comment()); + CPPUNIT_ASSERT_EQUAL(String::null, t->genre()); + CPPUNIT_ASSERT_EQUAL(0U, t->year()); + CPPUNIT_ASSERT_EQUAL(0U, t->track()); + CPPUNIT_ASSERT_EQUAL(String("ScreamTracker III"), t->trackerName()); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestS3M); diff --git a/taglib/tests/test_string.cpp b/taglib/tests/test_string.cpp new file mode 100644 index 0000000..a815a0b --- /dev/null +++ b/taglib/tests/test_string.cpp @@ -0,0 +1,233 @@ +/* Copyright (C) 2003 Scott Wheeler <wheeler@kde.org> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <tstring.h> +#include <string.h> +#include <cppunit/extensions/HelperMacros.h> + +using namespace std; +using namespace TagLib; + +class TestString : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestString); + CPPUNIT_TEST(testString); + CPPUNIT_TEST(testRfind); + CPPUNIT_TEST(testUTF16Encode); + CPPUNIT_TEST(testUTF16Decode); + CPPUNIT_TEST(testUTF16DecodeInvalidBOM); + CPPUNIT_TEST(testUTF16DecodeEmptyWithBOM); + CPPUNIT_TEST(testAppendCharDetach); + CPPUNIT_TEST(testAppendStringDetach); + CPPUNIT_TEST(testToInt); + CPPUNIT_TEST(testSubstr); + CPPUNIT_TEST(testNewline); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testString() + { + String s = "taglib string"; + ByteVector v = "taglib string"; + CPPUNIT_ASSERT(v == s.data(String::Latin1)); + + char str[] = "taglib string"; + CPPUNIT_ASSERT(strcmp(s.toCString(), str) == 0); + + String unicode("José Carlos", String::UTF8); + CPPUNIT_ASSERT(strcmp(unicode.toCString(), "Jos\xe9 Carlos") == 0); + + String latin = "Jos\xe9 Carlos"; + CPPUNIT_ASSERT(strcmp(latin.toCString(true), "José Carlos") == 0); + + String c; + c = "1"; + CPPUNIT_ASSERT(c == L"1"); + + c = L'\u4E00'; + CPPUNIT_ASSERT(c == L"\u4E00"); + + String unicode2(unicode.to8Bit(true), String::UTF8); + CPPUNIT_ASSERT(unicode == unicode2); + + String unicode3(L"\u65E5\u672C\u8A9E"); + CPPUNIT_ASSERT(*(unicode3.toCWString() + 1) == L'\u672C'); + + CPPUNIT_ASSERT(strcmp(String::number(0).toCString(), "0") == 0); + CPPUNIT_ASSERT(strcmp(String::number(12345678).toCString(), "12345678") == 0); + CPPUNIT_ASSERT(strcmp(String::number(-12345678).toCString(), "-12345678") == 0); + + String n = "123"; + CPPUNIT_ASSERT(n.toInt() == 123); + + n = "-123"; + CPPUNIT_ASSERT(n.toInt() == -123); + + CPPUNIT_ASSERT(String("0").toInt() == 0); + CPPUNIT_ASSERT(String("1").toInt() == 1); + + CPPUNIT_ASSERT(String(" foo ").stripWhiteSpace() == String("foo")); + CPPUNIT_ASSERT(String("foo ").stripWhiteSpace() == String("foo")); + CPPUNIT_ASSERT(String(" foo").stripWhiteSpace() == String("foo")); + + CPPUNIT_ASSERT(memcmp(String("foo").data(String::Latin1).data(), "foo", 3) == 0); + CPPUNIT_ASSERT(memcmp(String("f").data(String::Latin1).data(), "f", 1) == 0); + + ByteVector utf16 = unicode.data(String::UTF16); + + // Check to make sure that the BOM is there and that the data size is correct + + CPPUNIT_ASSERT(utf16.size() == 2 + (unicode.size() * 2)); + + CPPUNIT_ASSERT(unicode == String(utf16, String::UTF16)); + } + + void testUTF16Encode() + { + String a("foo"); + ByteVector b("\0f\0o\0o", 6); + ByteVector c("f\0o\0o\0", 6); + ByteVector d("\377\376f\0o\0o\0", 8); + CPPUNIT_ASSERT(a.data(String::UTF16BE) != a.data(String::UTF16LE)); + CPPUNIT_ASSERT(b == a.data(String::UTF16BE)); + CPPUNIT_ASSERT(c == a.data(String::UTF16LE)); + CPPUNIT_ASSERT_EQUAL(d, a.data(String::UTF16)); + } + + void testUTF16Decode() + { + String a("foo"); + ByteVector b("\0f\0o\0o", 6); + ByteVector c("f\0o\0o\0", 6); + ByteVector d("\377\376f\0o\0o\0", 8); + CPPUNIT_ASSERT_EQUAL(a, String(b, String::UTF16BE)); + CPPUNIT_ASSERT_EQUAL(a, String(c, String::UTF16LE)); + CPPUNIT_ASSERT_EQUAL(a, String(d, String::UTF16)); + } + + // this test is expected to print "TagLib: String::prepare() - + // Invalid UTF16 string." on the console 3 times + void testUTF16DecodeInvalidBOM() + { + ByteVector b(" ", 1); + ByteVector c(" ", 2); + ByteVector d(" \0f\0o\0o", 8); + CPPUNIT_ASSERT_EQUAL(String(), String(b, String::UTF16)); + CPPUNIT_ASSERT_EQUAL(String(), String(c, String::UTF16)); + CPPUNIT_ASSERT_EQUAL(String(), String(d, String::UTF16)); + } + + void testUTF16DecodeEmptyWithBOM() + { + ByteVector a("\377\376", 2); + ByteVector b("\376\377", 2); + CPPUNIT_ASSERT_EQUAL(String(), String(a, String::UTF16)); + CPPUNIT_ASSERT_EQUAL(String(), String(b, String::UTF16)); + } + + void testAppendStringDetach() + { + String a("a"); + String b = a; + a += "b"; + CPPUNIT_ASSERT_EQUAL(String("ab"), a); + CPPUNIT_ASSERT_EQUAL(String("a"), b); + } + + void testAppendCharDetach() + { + String a("a"); + String b = a; + a += 'b'; + CPPUNIT_ASSERT_EQUAL(String("ab"), a); + CPPUNIT_ASSERT_EQUAL(String("a"), b); + } + + void testRfind() + { + CPPUNIT_ASSERT_EQUAL(-1, String("foo.bar").rfind(".", 0)); + CPPUNIT_ASSERT_EQUAL(-1, String("foo.bar").rfind(".", 1)); + CPPUNIT_ASSERT_EQUAL(-1, String("foo.bar").rfind(".", 2)); + CPPUNIT_ASSERT_EQUAL(3, String("foo.bar").rfind(".", 3)); + CPPUNIT_ASSERT_EQUAL(3, String("foo.bar").rfind(".", 4)); + CPPUNIT_ASSERT_EQUAL(3, String("foo.bar").rfind(".", 5)); + CPPUNIT_ASSERT_EQUAL(3, String("foo.bar").rfind(".", 6)); + CPPUNIT_ASSERT_EQUAL(3, String("foo.bar").rfind(".", 7)); + CPPUNIT_ASSERT_EQUAL(3, String("foo.bar").rfind(".")); + } + + void testToInt() + { + bool ok; + CPPUNIT_ASSERT_EQUAL(String("123").toInt(&ok), 123); + CPPUNIT_ASSERT_EQUAL(ok, true); + + CPPUNIT_ASSERT_EQUAL(String("-123").toInt(&ok), -123); + CPPUNIT_ASSERT_EQUAL(ok, true); + + CPPUNIT_ASSERT_EQUAL(String("abc").toInt(&ok), 0); + CPPUNIT_ASSERT_EQUAL(ok, false); + + CPPUNIT_ASSERT_EQUAL(String("1x").toInt(&ok), 1); + CPPUNIT_ASSERT_EQUAL(ok, false); + + CPPUNIT_ASSERT_EQUAL(String("").toInt(&ok), 0); + CPPUNIT_ASSERT_EQUAL(ok, false); + + CPPUNIT_ASSERT_EQUAL(String("-").toInt(&ok), 0); + CPPUNIT_ASSERT_EQUAL(ok, false); + + CPPUNIT_ASSERT_EQUAL(String("123").toInt(), 123); + CPPUNIT_ASSERT_EQUAL(String("-123").toInt(), -123); + CPPUNIT_ASSERT_EQUAL(String("123aa").toInt(), 123); + CPPUNIT_ASSERT_EQUAL(String("-123aa").toInt(), -123); + } + + void testSubstr() + { + CPPUNIT_ASSERT_EQUAL(String("01"), String("0123456").substr(0, 2)); + CPPUNIT_ASSERT_EQUAL(String("12"), String("0123456").substr(1, 2)); + CPPUNIT_ASSERT_EQUAL(String("123456"), String("0123456").substr(1, 200)); + } + + void testNewline() + { + ByteVector cr("abc\x0dxyz", 7); + ByteVector lf("abc\x0axyz", 7); + ByteVector crlf("abc\x0d\x0axyz", 8); + + CPPUNIT_ASSERT_EQUAL(uint(7), String(cr).size()); + CPPUNIT_ASSERT_EQUAL(uint(7), String(lf).size()); + CPPUNIT_ASSERT_EQUAL(uint(8), String(crlf).size()); + + CPPUNIT_ASSERT_EQUAL(L'\x0d', String(cr)[3]); + CPPUNIT_ASSERT_EQUAL(L'\x0a', String(lf)[3]); + CPPUNIT_ASSERT_EQUAL(L'\x0d', String(crlf)[3]); + CPPUNIT_ASSERT_EQUAL(L'\x0a', String(crlf)[4]); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestString); diff --git a/taglib/tests/test_synchdata.cpp b/taglib/tests/test_synchdata.cpp new file mode 100644 index 0000000..2e3021e --- /dev/null +++ b/taglib/tests/test_synchdata.cpp @@ -0,0 +1,107 @@ +/* Copyright (C) 2003 Scott Wheeler <wheeler@kde.org> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <id3v2synchdata.h> +#include <cppunit/extensions/HelperMacros.h> + +using namespace std; +using namespace TagLib; + +class TestID3v2SynchData : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestID3v2SynchData); + CPPUNIT_TEST(test1); + CPPUNIT_TEST(test2); + CPPUNIT_TEST(test3); + CPPUNIT_TEST(testToUIntBroken); + CPPUNIT_TEST(testToUIntBrokenAndTooLarge); + CPPUNIT_TEST(testDecode1); + CPPUNIT_TEST(testDecode2); + CPPUNIT_TEST_SUITE_END(); + +public: + + void test1() + { + char data[] = { 0, 0, 0, 127 }; + ByteVector v(data, 4); + + CPPUNIT_ASSERT_EQUAL(ID3v2::SynchData::toUInt(v), TagLib::uint(127)); + CPPUNIT_ASSERT_EQUAL(ID3v2::SynchData::fromUInt(127), v); + } + + void test2() + { + char data[] = { 0, 0, 1, 0 }; + ByteVector v(data, 4); + + CPPUNIT_ASSERT_EQUAL(ID3v2::SynchData::toUInt(v), TagLib::uint(128)); + CPPUNIT_ASSERT_EQUAL(ID3v2::SynchData::fromUInt(128), v); + } + + void test3() + { + char data[] = { 0, 0, 1, 1 }; + ByteVector v(data, 4); + + CPPUNIT_ASSERT_EQUAL(ID3v2::SynchData::toUInt(v), TagLib::uint(129)); + CPPUNIT_ASSERT_EQUAL(ID3v2::SynchData::fromUInt(129), v); + } + + void testToUIntBroken() + { + char data[] = { 0, 0, 0, -1 }; + char data2[] = { 0, 0, -1, -1 }; + + CPPUNIT_ASSERT_EQUAL(TagLib::uint(255), ID3v2::SynchData::toUInt(ByteVector(data, 4))); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(65535), ID3v2::SynchData::toUInt(ByteVector(data2, 4))); + } + + void testToUIntBrokenAndTooLarge() + { + char data[] = { 0, 0, 0, -1, 0 }; + ByteVector v(data, 5); + + CPPUNIT_ASSERT_EQUAL(TagLib::uint(255), ID3v2::SynchData::toUInt(v)); + } + + void testDecode1() + { + ByteVector a("\xff\x00\x00", 3); + a = ID3v2::SynchData::decode(a); + CPPUNIT_ASSERT_EQUAL((unsigned int)2, a.size()); + CPPUNIT_ASSERT_EQUAL(ByteVector("\xff\x00", 2), a); + } + + void testDecode2() + { + ByteVector a("\xff\x44", 2); + a = ID3v2::SynchData::decode(a); + CPPUNIT_ASSERT_EQUAL((unsigned int)2, a.size()); + CPPUNIT_ASSERT_EQUAL(ByteVector("\xff\x44", 2), a); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestID3v2SynchData); diff --git a/taglib/tests/test_trueaudio.cpp b/taglib/tests/test_trueaudio.cpp new file mode 100644 index 0000000..5ff114c --- /dev/null +++ b/taglib/tests/test_trueaudio.cpp @@ -0,0 +1,27 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <string> +#include <stdio.h> +#include <trueaudiofile.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestTrueAudio : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestTrueAudio); + CPPUNIT_TEST(testReadPropertiesWithoutID3v2); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testReadPropertiesWithoutID3v2() + { + TrueAudio::File f(TEST_FILE_PATH_C("empty.tta")); + CPPUNIT_ASSERT(f.audioProperties()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestTrueAudio); diff --git a/taglib/tests/test_wav.cpp b/taglib/tests/test_wav.cpp new file mode 100644 index 0000000..d17ce2c --- /dev/null +++ b/taglib/tests/test_wav.cpp @@ -0,0 +1,36 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tbytevectorlist.h> +#include <wavfile.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestWAV : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestWAV); + CPPUNIT_TEST(testLength); + CPPUNIT_TEST(testZeroSizeDataChunk); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testLength() + { + RIFF::WAV::File f(TEST_FILE_PATH_C("empty.wav")); + CPPUNIT_ASSERT_EQUAL(true, f.isValid()); + CPPUNIT_ASSERT_EQUAL(3, f.audioProperties()->length()); + } + + void testZeroSizeDataChunk() + { + RIFF::WAV::File f(TEST_FILE_PATH_C("zero-size-chunk.wav")); + CPPUNIT_ASSERT_EQUAL(false, f.isValid()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestWAV); diff --git a/taglib/tests/test_wavpack.cpp b/taglib/tests/test_wavpack.cpp new file mode 100644 index 0000000..085fa2d --- /dev/null +++ b/taglib/tests/test_wavpack.cpp @@ -0,0 +1,40 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <string> +#include <stdio.h> +#include <tag.h> +#include <tbytevectorlist.h> +#include <wavpackfile.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestWavPack : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestWavPack); + CPPUNIT_TEST(testBasic); + CPPUNIT_TEST(testLengthScan); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testBasic() + { + WavPack::File f(TEST_FILE_PATH_C("no_length.wv")); + WavPack::Properties *props = f.audioProperties(); + CPPUNIT_ASSERT_EQUAL(44100, props->sampleRate()); + CPPUNIT_ASSERT_EQUAL(2, props->channels()); + CPPUNIT_ASSERT_EQUAL(1, props->bitrate()); + CPPUNIT_ASSERT_EQUAL(0x407, props->version()); + } + + void testLengthScan() + { + WavPack::File f(TEST_FILE_PATH_C("no_length.wv")); + WavPack::Properties *props = f.audioProperties(); + CPPUNIT_ASSERT_EQUAL(4, props->length()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestWavPack); diff --git a/taglib/tests/test_xiphcomment.cpp b/taglib/tests/test_xiphcomment.cpp new file mode 100644 index 0000000..6526229 --- /dev/null +++ b/taglib/tests/test_xiphcomment.cpp @@ -0,0 +1,79 @@ +#include <string> +#include <stdio.h> +#include <xiphcomment.h> +#include <tpropertymap.h> +#include <tdebug.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +class TestXiphComment : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestXiphComment); + CPPUNIT_TEST(testYear); + CPPUNIT_TEST(testSetYear); + CPPUNIT_TEST(testTrack); + CPPUNIT_TEST(testSetTrack); + CPPUNIT_TEST(testInvalidKeys); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testYear() + { + Ogg::XiphComment cmt; + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), cmt.year()); + cmt.addField("YEAR", "2009"); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2009), cmt.year()); + cmt.addField("DATE", "2008"); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2008), cmt.year()); + } + + void testSetYear() + { + Ogg::XiphComment cmt; + cmt.addField("YEAR", "2009"); + cmt.addField("DATE", "2008"); + cmt.setYear(1995); + CPPUNIT_ASSERT(cmt.fieldListMap()["YEAR"].isEmpty()); + CPPUNIT_ASSERT_EQUAL(String("1995"), cmt.fieldListMap()["DATE"].front()); + } + + void testTrack() + { + Ogg::XiphComment cmt; + CPPUNIT_ASSERT_EQUAL(TagLib::uint(0), cmt.track()); + cmt.addField("TRACKNUM", "7"); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(7), cmt.track()); + cmt.addField("TRACKNUMBER", "8"); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(8), cmt.track()); + } + + void testSetTrack() + { + Ogg::XiphComment cmt; + cmt.addField("TRACKNUM", "7"); + cmt.addField("TRACKNUMBER", "8"); + cmt.setTrack(3); + CPPUNIT_ASSERT(cmt.fieldListMap()["TRACKNUM"].isEmpty()); + CPPUNIT_ASSERT_EQUAL(String("3"), cmt.fieldListMap()["TRACKNUMBER"].front()); + } + + void testInvalidKeys() + { + PropertyMap map; + map[""] = String("invalid key: empty string"); + map["A=B"] = String("invalid key: contains '='"); + map["A~B"] = String("invalid key: contains '~'"); + + Ogg::XiphComment cmt; + PropertyMap unsuccessful = cmt.setProperties(map); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(3), unsuccessful.size()); + CPPUNIT_ASSERT(cmt.properties().isEmpty()); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestXiphComment); diff --git a/taglib/tests/test_xm.cpp b/taglib/tests/test_xm.cpp new file mode 100644 index 0000000..2808653 --- /dev/null +++ b/taglib/tests/test_xm.cpp @@ -0,0 +1,216 @@ +/*************************************************************************** + copyright : (C) 2011 by Mathias Panzenböck + email : grosser.meister.morti@gmx.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include <xmfile.h> +#include <cppunit/extensions/HelperMacros.h> +#include "utils.h" + +using namespace std; +using namespace TagLib; + +static const String titleBefore("title of song"); +static const String titleAfter("changed title"); + +static const String trackerNameBefore("MilkyTracker "); +static const String trackerNameAfter("TagLib"); + +static const String commentBefore( + "Instrument names\n" + "are abused as\n" + "comments in\n" + "module file formats.\n" + "-+-+-+-+-+-+-+-+-+-+-+\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n" + "Sample\n" + "names\n" + "are sometimes\n" + "also abused as\n" + "comments."); + +static const String newCommentShort( + "Instrument names\n" + "are abused as\n" + "comments in\n" + "module file formats.\n" + "======================\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n" + "Sample names\n" + "are sometimes\n" + "also abused as\n" + "comments."); + +static const String newCommentLong( + "Instrument names\n" + "are abused as\n" + "comments in\n" + "module file formats.\n" + "======================\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n" + "Sample names\n" + "are sometimes\n" + "also abused as\n" + "comments.\n" + "\n\n\n\n\n\n\n" + "TEST"); + +static const String commentAfter( + "Instrument names\n" + "are abused as\n" + "comments in\n" + "module file formats.\n" + "======================\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n" + "Sample names\n" + "are sometimes\n" + "also abused as\n" + "comments.\n"); + +class TestXM : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestXM); + CPPUNIT_TEST(testReadTags); + CPPUNIT_TEST(testReadStrippedTags); + CPPUNIT_TEST(testWriteTagsShort); + CPPUNIT_TEST(testWriteTagsLong); + CPPUNIT_TEST_SUITE_END(); + +public: + void testReadTags() + { + testRead(TEST_FILE_PATH_C("test.xm"), titleBefore, + commentBefore, trackerNameBefore); + } + + void testReadStrippedTags() + { + XM::File file(TEST_FILE_PATH_C("stripped.xm")); + CPPUNIT_ASSERT(file.isValid()); + + XM::Properties *p = file.audioProperties(); + Mod::Tag *t = file.tag(); + + CPPUNIT_ASSERT(0 != p); + CPPUNIT_ASSERT(0 != t); + + CPPUNIT_ASSERT_EQUAL(0, p->length()); + CPPUNIT_ASSERT_EQUAL(0, p->bitrate()); + CPPUNIT_ASSERT_EQUAL(0, p->sampleRate()); + CPPUNIT_ASSERT_EQUAL(8, p->channels()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 1, p->lengthInPatterns()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 0, p->version()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 0 , p->restartPosition()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 1, p->patternCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 0, p->instrumentCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 1, p->flags()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 6, p->tempo()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort)125, p->bpmSpeed()); + CPPUNIT_ASSERT_EQUAL(titleBefore, t->title()); + CPPUNIT_ASSERT_EQUAL(String::null, t->artist()); + CPPUNIT_ASSERT_EQUAL(String::null, t->album()); + CPPUNIT_ASSERT_EQUAL(String::null, t->comment()); + CPPUNIT_ASSERT_EQUAL(String::null, t->genre()); + CPPUNIT_ASSERT_EQUAL(0U, t->year()); + CPPUNIT_ASSERT_EQUAL(0U, t->track()); + CPPUNIT_ASSERT_EQUAL(String::null, t->trackerName()); + } + + void testWriteTagsShort() + { + testWriteTags(newCommentShort); + } + + void testWriteTagsLong() + { + testWriteTags(newCommentLong); + } + +private: + void testRead(FileName fileName, const String &title, + const String &comment, const String &trackerName) + { + XM::File file(fileName); + + CPPUNIT_ASSERT(file.isValid()); + + XM::Properties *p = file.audioProperties(); + Mod::Tag *t = file.tag(); + + CPPUNIT_ASSERT(0 != p); + CPPUNIT_ASSERT(0 != t); + + CPPUNIT_ASSERT_EQUAL(0, p->length()); + CPPUNIT_ASSERT_EQUAL(0, p->bitrate()); + CPPUNIT_ASSERT_EQUAL(0, p->sampleRate()); + CPPUNIT_ASSERT_EQUAL(8, p->channels()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 1, p->lengthInPatterns()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort)260, p->version()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 0, p->restartPosition()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 1, p->patternCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort)128, p->instrumentCount()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 1, p->flags()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort) 6, p->tempo()); + CPPUNIT_ASSERT_EQUAL((TagLib::ushort)125, p->bpmSpeed()); + CPPUNIT_ASSERT_EQUAL(title, t->title()); + CPPUNIT_ASSERT_EQUAL(String::null, t->artist()); + CPPUNIT_ASSERT_EQUAL(String::null, t->album()); + CPPUNIT_ASSERT_EQUAL(comment, t->comment()); + CPPUNIT_ASSERT_EQUAL(String::null, t->genre()); + CPPUNIT_ASSERT_EQUAL(0U, t->year()); + CPPUNIT_ASSERT_EQUAL(0U, t->track()); + CPPUNIT_ASSERT_EQUAL(trackerName, t->trackerName()); + } + + void testWriteTags(const String &comment) + { + ScopedFileCopy copy("test", ".xm"); + { + XM::File file(copy.fileName().c_str()); + CPPUNIT_ASSERT(file.tag() != 0); + file.tag()->setTitle(titleAfter); + file.tag()->setComment(comment); + file.tag()->setTrackerName(trackerNameAfter); + CPPUNIT_ASSERT(file.save()); + } + testRead(copy.fileName().c_str(), titleAfter, + commentAfter, trackerNameAfter); + CPPUNIT_ASSERT(fileEqual( + copy.fileName(), + TEST_FILE_PATH_C("changed.xm"))); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestXM); diff --git a/taglib/tests/utils.h b/taglib/tests/utils.h new file mode 100644 index 0000000..00cef62 --- /dev/null +++ b/taglib/tests/utils.h @@ -0,0 +1,104 @@ +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#ifdef _WIN32 +#include <windows.h> +#else +#include <unistd.h> +#include <fcntl.h> +#include <sys/fcntl.h> +#include <sys/stat.h> +#endif +#include <stdio.h> +#include <string.h> +#include <string> +#include <fstream> + +using namespace std; + +inline string testFilePath(const string &filename) +{ + return string(TESTS_DIR "data/") + filename; +} + +#define TEST_FILE_PATH_C(f) testFilePath(f).c_str() + +inline string copyFile(const string &filename, const string &ext) +{ + string newname = string(tempnam(NULL, NULL)) + ext; + string oldname = testFilePath(filename) + ext; +#ifdef _WIN32 + CopyFile(oldname.c_str(), newname.c_str(), FALSE); + SetFileAttributes(newname.c_str(), GetFileAttributes(newname.c_str()) & ~FILE_ATTRIBUTE_READONLY); +#else + char buffer[4096]; + int bytes; + int inf = open(oldname.c_str(), O_RDONLY); + int outf = open(newname.c_str(), O_CREAT | O_EXCL | O_RDWR, S_IRUSR | S_IWUSR); + while((bytes = read(inf, buffer, sizeof(buffer))) > 0) + write(outf, buffer, bytes); + close(outf); + close(inf); +#endif + return newname; +} + +inline void deleteFile(const string &filename) +{ + remove(filename.c_str()); +} + +inline bool fileEqual(const string &filename1, const string &filename2) +{ + char buf1[BUFSIZ]; + char buf2[BUFSIZ]; + + ifstream stream1(filename1.c_str(), ios_base::in | ios_base::binary); + ifstream stream2(filename2.c_str(), ios_base::in | ios_base::binary); + + if(!stream1 && !stream2) return true; + if(!stream1 || !stream2) return false; + + for(;;) + { + stream1.read(buf1, BUFSIZ); + stream2.read(buf2, BUFSIZ); + + streamsize n1 = stream1.gcount(); + streamsize n2 = stream2.gcount(); + + if(n1 != n2) return false; + + if(n1 == 0) break; + + if(memcmp(buf1, buf2, n1) != 0) return false; + } + + return stream1.good() == stream2.good(); +} + +class ScopedFileCopy +{ +public: + ScopedFileCopy(const string &filename, const string &ext, bool deleteFile=true) + { + m_deleteFile = deleteFile; + m_filename = copyFile(filename, ext); + } + + ~ScopedFileCopy() + { + if(m_deleteFile) + deleteFile(m_filename); + } + + string fileName() + { + return m_filename; + } + +private: + bool m_deleteFile; + string m_filename; +};