ogg vorbis comment (tag)

Just starting out? Need help? Post your questions and find answers here.
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

ogg vorbis comment (tag)

Post by collectordave »

Has anyone written anything to read and write vorbis comment tags info to ogg files?

Need cross platform so native PB if poss.

Regards

CD

Written the comment editor now available here. https://www.dropbox.com/s/p7exmds88ymoo ... t.zip?dl=0

New editor written with batch etc.
Last edited by collectordave on Wed Aug 28, 2019 3:57 am, edited 3 times in total.
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Ok I have made a start not sure it is right but getting there. Just reading comments at the moment.

Each packet starts with 'OggS' and the comments are in the second packet.

1. open the file and search for the second 'OggS'
2. From this point search for 'vorbis'
3. The next four bytes are a little endian number so read these four bytes as a long number

In a HEX editor you will see as an example
76 6F 72 62 69 73 34 00 00 00
which is 'vorbis' followed by a four byte number in real life 00 00 00 34 (52 in decimal remember it is little endian)

4. Read in the next 52 bytes as a UTF-8 encoded string. This is the vendor string.

5. Read the next four bytes as a number this gives you the number of comments in the file.

6. The four bytes after this are the length of the comment.

7. Read in this number of bytes as a UTF-8 encoded string

Repeat steps 6 and 7 for the number of comments retrieved in step 5

All comments are in the form 'TITLE=Tell Me Why'

The first part before the = is the comment title (not TITLE) the second part is the comment content.

Some files have picture metadata in them I have not sorted this out yet but it is still read the same way.

Writing new comments I have not looked into yet but I think it is copy file to comment start to another temp file add new comments including total and length for each then from comments end, in original, copy all bytes to temp file, delete original file then rename temp to original.

Here is the code for a module and example use to read ogg comments please feel free to suggest improvements.

Code: Select all

DeclareModule oggTags
  
  Structure tag
    Title.s
    Value.s
  EndStructure
    
  Declare GetComments(FileName.s,List Comments.tag())
  
EndDeclareModule

Module oggTags
  
  Structure ByteArray
    byte.b[0]
  EndStructure
    
  Procedure.i QuickSearch (*mainMem.ByteArray, mainSize.i, *findMem.ByteArray, findSize.i, startOff.i=0)
    ; -- Simplification of the Boyer-Moore algorithm;
    ;    searches for a sequence of bytes in memory
    ;    (not for characters, so it works in ASCII mode and Unicode mode)
    ; in : *mainMem: pointer to memory area where to search
    ;      mainSize: size of memory area where to search (bytes)
    ;      *findMem: pointer to byte sequence to search for
    ;      findSize: number of bytes to search for
    ;      startOff: offset in <mainMem>, where the search begins (bytes)
    ; out: offset in <mainMem>, where <findMem> was found (bytes);
    ;      -1 if not found
    ; Note: The first offset is 0 (not 1)!
    ;
    ; after <http://www-igm.univ-mlv.fr/~lecroq/string/node19.html#SECTION00190>, 31.8.2008
    ; (translated from C to PureBasic by Little John)
    Protected i.i, diff.i
    Protected Dim badByte.i(255)
   
    ; Preprocessing
    For i = 0 To 255
      badByte(i) = findSize + 1
    Next
    For i = 0 To findSize - 1
      badByte(*findMem\byte[i] & #FF) = findSize - i
    Next
   
    ; Searching
    diff = mainSize - findSize
    While startOff <= diff
      If CompareMemory(*mainMem + startOff, *findMem, findSize) = 1
        ProcedureReturn startOff
      EndIf
      startOff + badByte(*mainMem\byte[startOff + findSize] & #FF)  ; shift
    Wend
   
    ProcedureReturn -1                                               ; not found
EndProcedure
  
  Procedure.q FindInFile (infile.i, *find, findSize.i, startOff.q=0, bufferSize.i=4096)
    ;Code From Purebasic Forum By littleJohn
    ; -- Looks in <infile> for byte sequence at *find;
    ;    works in ASCII mode and Unicode mode.
    ; in : infile    : number of a file, that was opened for reading
    ;      *find     : pointer to byte sequence to search for
    ;      findSize  : number of bytes to search for
    ;      startOff  : offset in the file where the search begins (bytes)
    ;      bufferSize: size of used memory buffer (bytes)
    ; out: offset in the file, where byte sequence at *find was found (bytes),
    ;      -1 if byte sequence at *find was not found in <infile>,
    ;      -2 on error
    ; Note: The first offset is 0 (not 1)!
    Protected *buffer
    Protected offset.q, move.i, bytes.i
   
    move = bufferSize - findSize + 1
    If move < 1
      ProcedureReturn -2                 ; error
    EndIf
   
    *buffer = AllocateMemory(bufferSize)
    If *buffer = 0
      ProcedureReturn -2                 ; error
    EndIf
   
    Repeat
      FileSeek(infile, startOff)
      bytes = ReadData(infile, *buffer, bufferSize)
      ; QuickSearch returns the offset in the buffer (bytes),
      ; or -1 if not found:
     
      offset = QuickSearch(*buffer, bytes, *find, findSize)
      If offset <> -1                    ; found
        offset + startOff
        Break
      EndIf
      startOff + move
    Until bytes < bufferSize
   
    FreeMemory(*buffer)
    ProcedureReturn offset
   
  EndProcedure
    
  Procedure GetComments(FileName.s,List Comments.tag())
    
    Define SearchedFile.i, SearchFor.s,format.i, numBytes.i, *searchBuffer, found.q

    Define TempString.s,TagStart.i,iLoop.i,TagLength.i

    Define HeaderNumber.i,StartPos.i

    SearchFor = "OggS"

    ClearList(Comments())

    AddElement(Comments())

    SearchedFile = ReadFile(#PB_Any, FileName)
    If SearchedFile
      format = #PB_UTF8
      numBytes = StringByteLength(SearchFor, format)
    
      *searchBuffer = AllocateMemory(numBytes+2)

     If *searchBuffer
       PokeS(*searchBuffer, SearchFor, -1, format)
       StartPos = 0
       While HeaderNumber < 2

      found = FindInFile(SearchedFile, *searchBuffer, numBytes,StartPos)
      Select found
         Case -2
            Debug "Error."
         Case -1
            Debug "'" + search$ + "' not found in file '" + file$ + "'."
          Default
            
            HeaderNumber = HeaderNumber + 1
            StartPos = Found + 4 ;Add On Bytes for "OggS"

        EndSelect
        
      Wend

      FreeMemory(*searchBuffer)
      
      ;Now Find "vorbis"
      SearchFor = "vorbis"
     numBytes = StringByteLength(SearchFor, format)
    
    *searchBuffer = AllocateMemory(numBytes+2)

   If *searchBuffer
     PokeS(*searchBuffer, SearchFor, -1, format)     
      
     found = FindInFile(SearchedFile, *searchBuffer, numBytes,StartPos) 
      Select found
         Case -2
            Debug "Error."
         Case -1
            Debug "'" + SearchFor + "' not found in file '" + file$ + "'."
          Default
            
            StartPos = found + 6 ;Add On Bytes for "vorbis"
            FileSeek(SearchedFile,StartPos)
            
            ;Get Vendor String
            TagLength =  ReadLong(SearchedFile) ;Vendor String Length
            ;Debug  "Vendor String " +  ReadString(SearchedFile,#PB_UTF8,TagLength) ;Vendor String
            Comments()\Title = "Vendor String"
            Comments()\Value = ReadString(SearchedFile,#PB_UTF8,TagLength)
            
            
            ;Get Number Of Comments
            TagStart =  ReadLong(SearchedFile)
            ;Debug "Number Of Comments " + Hex(TagStart)
            
            ;Loop To Get Each Comment(Tag)
            For iLoop = 0 To TagStart - 1
              TagLength = ReadLong(SearchedFile)
              AddElement(Comments())
              TempString = ReadString(SearchedFile,#PB_UTF8,TagLength)
              Comments()\Title = StringField(TempString,1,"=")
              Comments()\Value = StringField(TempString,2,"=")
            Next iLoop
    
        EndSelect     
     
     EndIf

      FreeMemory(*searchBuffer)
   Else   
      Debug "Error allocating memory for search string."
   EndIf
   CloseFile(SearchedFile)
Else
   Debug "Error reading from file '" + file$ + "'."
EndIf
  
    
    EndProcedure 
    
  
 EndModule
 
 Global NewList SongTags.oggTags::tag()
 
 Global SongFile.s
 
  SongFile = OpenFileRequester("Please choose file to load", "", "Songs (*.ogg)|*.ogg", 0)
  If SongFile
    oggTags::getComments(SongFile,SongTags())
  Else
    MessageRequester("Information", "Operation canceled.", 0) 
  EndIf

  ForEach SongTags()
    
    Debug SongTags()\Title
    Debug SongTags()\Value 
    
  Next
regards

CD
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Answer from another thread.

Quicker than mine!

Getting the idea of what to do writing ogg comments now.

Just need to work out a quick way to:-

1. Copy file up to a point
2. Miss comment section
3. Add my comment page
4. Copy rest of file

Basically copy ogg file to Number of segments, add my comment segment then copy rest of file.
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: ogg vorbis comment (tag)

Post by wilbert »

After seeing your posts I was curious about the ogg format.

What's not entirely clear to me is how to handle ogg files with multiple streams inside them.
Do you know if an ogg file can contain multiple vorbis streams ?

I also read a comment header can span multiple pages.
Do you know if the multiple pages from a comment header always follow each other directly when there are multiple streams or can a page from another stream be multiplexed in between them ?
Windows (x64)
Raspberry Pi OS (Arm64)
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Hi Wilbert

I am no expert but ogg can handle multiple streams.

The biggest problem I have had is the terminology used it differs from one explanation to another so this is my explanation.

An ogg file is split into pages every page begins with "OggS" and a version number byte after (allways 0). Each page ends when another page starts.

The little routine I wrote to look for a pattern in a file returning the position in the file of each occurrence of the pattern returns a list of offsets.

So for example the third offset returned is the start of the third page and is also the end of the second page.

Now each page actually contains two parts, a header and segments.

First the segments. A segment is a block of 0 to 255 bytes. Each page can contain up to 255 segments.

Now the header. The header is of variable length, you will read that the header is fixed at 27 bytes but it also has a segment table after the 27 bytes whose length is determined
by the number of segments used.

It is the 27 bytes you are interested in for the streams. Here is a header structure to explain.

Byte order: Little-endian

Offset Length Contents
[ 0 4 bytes "OggS"
4 1 byte Stream structure version (0x00)
5 1 byte Packet flag:
bit 0: true if page continued
bit 1: true if first page
bit 2: true if last page
bit 3..7: reserved
6 8 bytes The end pcm sample position (64bit integer)
14 4 bytes Stream serial number
18 4 bytes Page number
22 4 bytes Check sum
26 1 byte Number of segments(s)
27 (s)bytes Segment table
27+(s) (b)bytes Body (b := header[27] + header[27+1] + ... + header[27+s-1])
]*

The last line is the varable part depending on the value of byte 26

The interesting parts are the Stream serial number and the page number (I find it easier to call this sequence it counts from 1 to number of pages in stream in increments of 1).

To assemble a stream completely you would have to go through the whole file finding all the pages with the same Stream serial number and then assemble them
in the order dictated by the page number (sequence).

If there is a second stream there will be pages with a different stream serial number.

Now vorbis. As I understand it a vorbis stream is comments followed by stream data comments are not mixed up in the vorbis stream thay are only at the beginning.

So for any vorbis stream find the length of the comments then you have the start of the stream data.

Of course we are talking vorbis in an ogg file here. I can find nothing which guarantees the position of an ogg page so with stream serial and sequence they can be anywhere in any order.

Except page 0 of course. Pages of different streams can then be mixed sorted when read.

Comments can logically span multiple pages this is worked out using the segment table in a page header.

vorbis comments are allways first in a vorbis stream so find the page of the stream you are interested in then calculate the length of the comments section.

Here is a typical segment table

10 D3 FF FF FF FF FF FF FF FF FF FF FF FF FF FF

The first byte is 10 (16) showing 16 segments used in this page. (Confused here seems 16 - 1 are used)

From the 10 you start with zero and read each byte in turn if it is FF add to previous byte(0 at first not 10) if less than FF add to previous and this is the end of this section i.e. comments in this case.

In the example the next byte is D3 which is < FF so comments uses D3 bytes.

The next byte in a vorbis stream is the start of the stream data.

If the whole segment table was FF then you have to move to page 2 in the stream and keep reading to get the comments length.

Hope this helps

CD
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Trying to make it easier to understand.

When talking about ogg vorbis we are talking about two separate things.

ogg is a container which can hold many types of file. I imagine it as having a piece of paper with writing on it. this can be seen as a standard text file extension .txt I can put this sheet into an envelope, now to read the text I have to open the envelope to get at the piece of paper. The envelope in this case is called .ogg so to read the text file I have to take it out of the ogg.

Confusion starts as vorbis have hogged the ogg envelope (forgive the pun).

You can pack any file into an ogg container.

vorbis is a data compression format especially for audio files I think. You could have a .vorbis file the inside of which would look like

vorbis comments|vorbis bitstream

ogg vorbis is a vorbis bit stream packed into an ogg container.

Ah more mud.

CD
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: ogg vorbis comment (tag)

Post by wilbert »

I more or less understand the structure now. :wink:

I think the biggest problem when editing comments would be handling comment packets that span multiple pages.
Especially if the updated comment packet consists of a different number of pages compared to the original one.
Since the crc also seems to include the page number, you would have to recalculate the crc for all pages were the page number changes.
Windows (x64)
Raspberry Pi OS (Arm64)
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Oh yes looked at doing that had a small brain overload.

Luckily comments on an audio file rarely exceed 2Kb but I am looking at taking it easy and padding the comments page to get 8Kb of comment space then add the original or edited comments back. Only one page to deal with then. Have to remember that a cover art image can be encoded as a comment so limited size available (resized images not whole hidef images).

The first page of a stream allways seems to be loaded with 15 segments of preamble for the bitstream the comments normally taking up just one or two segments so with 255 segments available in the page padding the comments section to use 32 segments should not be a problem. I cannot see comments taking up more than this in every ogg vorbis file.

I am doing this to extend the ogg player I am writing here viewtopic.php?f=12&t=73239 and have thought about when people want more info than comments can deliver and maybe add a link to a relevant CDDB page as a comment and add a link to a personal music database on the clients machine where they can store as much as they like. Needs to be a balance between what is easily achievable and client expectations.

Most other ogg players will simply ignore these comments some may display them but they will only have meaning in my little player.
CD
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

To help see streams in an ogg file cobbled this together:-

Code: Select all

Structure OGG_STRUCT
  capture_pattern.l
  stream_structure_version.a
  header_type_flag.a
  granule_position.q
  bitstream_serial_number.l
  page_sequence_number.l
  crc_checksum.l
  number_page_segments.a
EndStructure





Global NewList OffSets.l() ;List to hold all positions where serched for string is found
    
    Global StringToFind.s


Procedure FindPatternInFile(FileName.s,*SearchBuffer,SLength.i,List FoundAt.l())
    
    Define *FileBuffer

    Define CommentStart.l,CommentEnd.i,CurrentPos.i
    
    Define BufferSize.i,SearchPosition.i
    
    Define DataAmount.i
    
    ;BufferSize determines the maximum amount of data to read each time 
    BufferSize = 10000
    
    ;DataAmount is the actual amount of data read from the file at any point
    DataAmount = 0
    
    ;SearchPosition is the position in the file reached by the search or file pointer
    SearchPosition = 0
    
    ;create the file buffer
    *FileBuffer = AllocateMemory(BufferSize)
    
    ;Ensure list of offsets is cleared
    ClearList(FoundAt())
    
    ;Open The File
    If ReadFile(0, FileName)
      
      While Not Eof(0)
        
        ;Read the first BufferSize chunk of data. DataRead is less than BufferSize when EOF reached
        DataRead = ReadData(0,*FileBuffer,BufferSize)

        ;Go Through Buffer Looking For String. SLength - 1 is last position to search as looking for whole string in less bytes cannot succeed
        For iLoop = 0 To Dataread - (SLength - 1)
          
          ;Add iLoop to *FileBuffer address and check for pattern
          If CompareMemory(*FileBuffer + iLoop,*SearchBuffer,SLength) = 1 ;If Pattern found
            
            ;Add an element to the list      
            AddElement(FoundAt())
            
            ;The offset is iLoop i.e. where in this loop we are at plus the amount of data allready read
            FoundAt() = iLoop + DataAmount

          EndIf
          
    
        Next iLoop

        If DataRead = BufferSize  ;Not End Of File
          If SearchPosition = 0 ;If this is the first block to stop negative FileSeek()
            SearchPosition = BufferSize - (SLength- 1)
            ;SLength- 1 just in case string found in last few bytes so next block read will not include whole string
            ;but will include the whole string if the string crosses the read boundary
          Else
            ;Same as above but totaling all data read
            SearchPosition = SearchPosition + (BufferSize - (SLength- 1))
          EndIf
          
          ;Set DataAmount to be actual amount of dataread for this loop
          DataAmount = SearchPosition
          
          ;Move the file pointer to the new position
          FileSeek(0,SearchPosition)
          
        EndIf 
      
    Wend
    
    EndIf
    
    CloseFile(0)
  
  EndProcedure
  
  Procedure ShowStreamSerial(FileName.s)
    
    Define ThisFile.i,PageNumber.i
    
    Protected ogg.OGG_STRUCT
    
    ResetList(OffSets())
    
    
    ;Open The File
    If ReadFile(ThisFile, FileName)
      
      ForEach OffSets()
      
        FileSeek(ThisFile,OffSets())
        ClearStructure(@ogg,OGG_STRUCT)
        If ReadData(handle,@ogg,SizeOf(OGG_STRUCT)) = SizeOf(OGG_STRUCT)
        
          Debug "Page Number = " + Str(PageNumber)
          PageNumber = PageNumber + 1
          Debug "Stream Serial Number = " + Hex(ogg\bitstream_serial_number)
          Debug "Page sequence Number = " + Hex(ogg\page_sequence_number)
          Debug "Number of Segments = " + Hex(ogg\number_page_segments)
        EndIf
          
      Next
          
    EndIf

  EndProcedure
  

  
  ;Searching For Pages
  FileToSearch.s = OpenFileRequester("Please choose file to load", "", "Ogg Files (*.ogg)|*.ogg", 0)

  If FileToSearch
  
    StringToFind.s = "OggS"
    
    ;SLength is the length of the buffer required for the string in a particular format
    SLength = StringByteLength(StringToFind,#PB_UTF8) ;Zero string terminator not counted
    *SearchBuffer = AllocateMemory(SLength)
    PokeS(*SearchBuffer,StringToFind,SLength,#PB_UTF8|#PB_String_NoZero)
    
    ;Used to search in a .txt file  
    FindPatternInFile(FileToSearch,*SearchBuffer,SLength,OffSets())

    ShowStreamSerial(FileToSearch)
    
  EndIf
Select an ogg file

debug shows each page found with Stream serial and segments used.

I do not have a multistream ogg file so my serial is allways the same but if you have one then two different serials will show on the page where they are. Not counting number of streams or saving offsets to load each stream.

Cd
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: ogg vorbis comment (tag)

Post by wilbert »

After some more reading online ...
If I understand correctly, an ogg file can have multiple streams but only 1 vorbis stream at a time.
Chaining however is allowed. So you can have multiple vorbis streams sequentially in one ogg file that should be played one after the other.

Do you have an ogg file with embedded artwork ?
I tried to find one online to test with but can't find one.
Windows (x64)
Raspberry Pi OS (Arm64)
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Read that as well I think it is a vorbis restriction not an ogg restriction.

I Have one with cover art here https://www.dropbox.com/s/ke4ousu8b4qjm ... y.ogg?dl=0

According to vorbis it should be either png or jpg.

Interested if you extract the image as I haven't tried yet.
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: ogg vorbis comment (tag)

Post by wilbert »

collectordave wrote:According to vorbis it should be either png or jpg.

Interested if you extract the image as I haven't tried yet.
The code below is what I came up with so far.
It does show a Base64 (RFC 4648) encoded METADATA_BLOCK_PICTURE but I haven't tried to decode it yet.

Code: Select all

EnableExplicit

; >> Structures <<

Structure OGGPage
  CapturePattern.l
  Version.a
  HeaderType.a
  GranulePosition.q
  BitstreamSerialNumber.l
  PageSequenceNumber.l
  Checksum.l
  PageSegments.a
  SegmentTable.a[255]
EndStructure  

Structure VorbisIdentification
  PacketType.a
  Identifier.a[6]
  VorbisVersion.l
  AudioChannnels.a
  AudioSampleRate.l
  BitrateMaximum.l
  BitrateNominal.l
  BitrateMinimum.l
  BlockSize01.a
  FramingFlag.a
EndStructure

Structure VorbisStream
  BitstreamSerialNumber.l
  AudioChannels.l
  AudioSampleRate.l
  Duration.l
  Vendor.s
  List Comment.s()
EndStructure


; >> ParseOGG procedure <<

Procedure ParseOGG(Filename.s, List VorbisStreams.VorbisStream())
  
  Protected OGGFile.i, NextPageLoc.q, Page.OGGPage, Id.VorbisIdentification
  Protected *Mem.Long, *CommentPacket, BitstreamSerialNumber.l
  Protected.i i, NextPage, LastSegment, BytesToRead, PacketSize, Length
  
  OGGFile = ReadFile(#PB_Any, Filename)
  If OGGFile
    ClearList(VorbisStreams())
    
    ; >> Process pages <<
    While ReadData(OGGFile, @Page, 27) = 27 And 
          Page\CapturePattern = $5367674F And
          ReadData(OGGFile, @Page + 27, Page\PageSegments) = Page\PageSegments
      
      ; >> Calculate file position of next page <<
      LastSegment = Page\PageSegments - 1
      NextPageLoc = Loc(OGGFile)
      For i = 0 To LastSegment
        NextPageLoc + Page\SegmentTable[i]
      Next
      
      ; >> Check for new vorbis stream <<
      If Page\HeaderType & 2 And Page\SegmentTable[0] = 30
        If ReadData(OGGFile, @Id, 30) = 30 And PeekS(@Id\Identifier, 6, #PB_Ascii) = "vorbis"
          ; Beginning of vorbis stream
          BitstreamSerialNumber = Page\BitstreamSerialNumber
          PacketSize = 0
          NextPage = 1
          ; Add new vorbis stream to list
          AddElement(VorbisStreams())
          VorbisStreams()\BitstreamSerialNumber = BitstreamSerialNumber
          VorbisStreams()\AudioChannels = Id\AudioChannnels
          VorbisStreams()\AudioSampleRate = Id\AudioSampleRate
        EndIf
      EndIf
      
      If BitstreamSerialNumber = Page\BitstreamSerialNumber
        
        ; Set duration when end of stream is reached
        If Page\HeaderType & 4
          VorbisStreams()\Duration = Page\GranulePosition / Id\AudioSampleRate
        EndIf
                
        ; Process a comment page
        If Page\PageSequenceNumber = NextPage
          ; Calculate how many bytes to read
          BytesToRead = 0
          For i = 0 To LastSegment
            If Page\SegmentTable[i] = 255
              BytesToRead + 255
            Else
              BytesToRead + Page\SegmentTable[i]
              NextPage = 0; No more comment pages
              Break
            EndIf
          Next
          ; Reallocate memory for comment packet
          *Mem = ReAllocateMemory(*CommentPacket, PacketSize + BytesToRead, #PB_Memory_NoClear)
          If *Mem
            *CommentPacket = *Mem
            ; Read bytes
            If ReadData(OGGFile, *CommentPacket + PacketSize, BytesToRead) = BytesToRead
              PacketSize + BytesToRead
              If NextPage
                NextPage + 1
              Else              
                ; Done reading comment packet
                *Mem + 7
                Length = *Mem\l : *Mem + 4
                ; Set vendor
                VorbisStreams()\Vendor = PeekS(*Mem, Length, #PB_UTF8) : *Mem + Length
                ; Clear the comments list
                ClearList(VorbisStreams()\Comment())
                i = *Mem\l : *Mem + 4
                ; Loop through comments and add to list
                While i
                  AddElement(VorbisStreams()\Comment())
                  Length = *Mem\l : *Mem + 4
                  VorbisStreams()\Comment() = PeekS(*Mem, Length, #PB_UTF8) : *Mem + Length
                  i - 1
                Wend
              EndIf
            EndIf
          EndIf
        EndIf
        
      EndIf
      
      ; Set the file pointer to the next page
      FileSeek(OGGFile, NextPageLoc) 
      
    Wend
    If *CommentPacket
      ; Free allocated memory
      FreeMemory(*CommentPacket)
    EndIf
    CloseFile(OGGFile)
  EndIf

EndProcedure


; Test the code

NewList VorbisStreams.VorbisStream()

ParseOGG("Queen - Action This Day.ogg", VorbisStreams())

ForEach VorbisStreams()
  Debug "Vorbis stream: " + Str(VorbisStreams()\BitstreamSerialNumber)
  Debug "Channels: " + Str(VorbisStreams()\AudioChannels)
  Debug "Sample rate: " + Str(VorbisStreams()\AudioSampleRate)
  Debug "Duration: " + Str(VorbisStreams()\Duration)
  Debug "Vendor: " + VorbisStreams()\Vendor
  Debug "Comments: "
  ForEach VorbisStreams()\Comment()
    Debug VorbisStreams()\Comment()  
  Next
Next
Last edited by wilbert on Tue Jul 30, 2019 4:06 pm, edited 2 times in total.
Windows (x64)
Raspberry Pi OS (Arm64)
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Genius!

Adapting bits to get what I think I want.

If I am reading the vorbis spec correctly there are three required vorbis (not ogg) headers each beginning with 'vorbis' the last of these finishes the ogg page on which it resides with the audio data begining on the next ogg page.

The file I sent you does indeed have comments spanning two pages so with a variable number of pages depending on the length of the comments I will have redo all pages in the ogg file if the number of pages changes Argh!

Thanks wilbert great
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Going out my head!

With the help of wilberts code above and numerous other hints and tips managed to cobble the following together.

The idea is that a vorbis stream in an ogg file can be extracted. It has four sections in total.

1. An Identity header
2. A comments header
3. A header for the codec
4. The audio stream

Each of the first three start with a single byte identity then 'vorbis', the identity bytes are 01 03 and 05

The fourth just gets on with it however in section four each section is significant. It starts on the next ogg page after section 3 ends.

I cannot get the idea of saving each section to a buffer for checking. wilbert did it with the comments header in his code.

My programme outputs

Page Number = 0
Bytes To read = 1E
New Section

For each section read. Where a section spans a page no new page number will be shown. So the bytes to read maybe a collection of bytes from a previous page or pages.

Any help appreciated.

Code: Select all

EnableExplicit

; >> Structures <<

Structure OGGPage
  CapturePattern.l
  Version.a
  HeaderType.a
  GranulePosition.q
  BitstreamSerialNumber.l
  PageSequenceNumber.l
  Checksum.l
  PageSegments.a
  SegmentTable.a[255]
EndStructure 

Structure VorbisIdentification
  PacketType.a
  Identifier.a[6]
  VorbisVersion.l
  AudioChannnels.a
  AudioSampleRate.l
  BitrateMaximum.l
  BitrateNominal.l
  BitrateMinimum.l
  BlockSize01.a
  FramingFlag.a
EndStructure

Structure VorbisStream
  BitstreamSerialNumber.l
  AudioChannels.l
  AudioSampleRate.l
  Vendor.s
  List Comment.s()
EndStructure

Structure Section
  Page.l
  Boundary.s
  NumBytes.i
EndStructure

Global NewList BytesPerSec.Section()

Global PageNo.i

 
Global OGGFile.i, NextPageLoc.q, Page.OGGPage, Id.VorbisIdentification
  Global *Mem.Long, *CommentPacket, BitstreamSerialNumber.l
  Global.i i, NextPage, LastSegment, BytesToRead, PacketSize, Length
 
  OGGFile = ReadFile(#PB_Any, "C:\CD Media\Queen\Queen - Action This Day.ogg")
  If OGGFile
   ; ClearList(VorbisStreams())
    
    PageNo = 0
    
    
    ; >> Process pages <<
    While ReadData(OGGFile, @Page, 27) = 27 And
          Page\CapturePattern = $5367674F And
          ReadData(OGGFile, @Page + 27, Page\PageSegments) = Page\PageSegments
      
      
      
      
      PageNo + 1
      
      
      ; >> Calculate file position of next page <<
      LastSegment = Page\PageSegments - 1
      NextPageLoc = Loc(OGGFile)
      For i = 0 To LastSegment
        NextPageLoc + Page\SegmentTable[i]
      Next
      
      ;Debug NextPageLoc
      
      
      ; Calculate how many bytes to read
      BytesToRead = 0
      For i = 0 To LastSegment
        If Page\SegmentTable[i] = 255
          BytesToRead + 255
        Else
          BytesToRead + Page\SegmentTable[i]
                    
          AddElement(BytesPerSec())
          BytesPerSec()\Page = Page\PageSequenceNumber
          BytesPerSec()\NumBytes = BytesToRead
          BytesPerSec()\Boundary = "New Section "
          ;No more For This Section

          ;Ready For next section       
          BytesToRead = 0
        EndIf
      Next
      
      
      
      
      FileSeek(OGGFile,NextPageLoc)
      
    Wend
    
  EndIf
  
  ForEach BytesPerSec()

    Debug "Page Number = " + Str(BytesPerSec()\Page)
    Debug "Bytes To read = " + Hex(BytesPerSec()\NumBytes)
    Debug BytesPerSec()\Boundary
    Debug ""
    
  Next
  
CD
Any intelligent fool can make things bigger and more complex. It takes a touch of genius — and a lot of courage to move in the opposite direction.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: ogg vorbis comment (tag)

Post by wilbert »

collectordave wrote:The idea is that a vorbis stream in an ogg file can be extracted. It has four sections in total.

1. An Identity header
2. A comments header
3. A header for the codec
4. The audio stream
Can you explain a bit more what you exactly want ?
Extracting a stream if the ogg file has multiple streams should be as simple as extracting all pages with the same bitstream serial number.
For your initial idea of editing a comment tag, this shouldn't be required.
Editing the comments header and updating the page number of all following pages should be sufficient. :?
Windows (x64)
Raspberry Pi OS (Arm64)
Post Reply