433 lines
14 KiB
C++
433 lines
14 KiB
C++
/***************************************************************************
|
|
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 *>::ConstIterator 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.");
|
|
}
|
|
}
|
|
}
|