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

Re: ogg vorbis comment (tag)

Post by collectordave »

Hi Wilbert I can try

The first vorbis header is probably easiest.

It is allways 30bytes in length.

In the ogg it is like this.

4F 67 67 53 00 02 00 00 00 00 00 00 00 00 EC 0A 00 00 00 00 00 00 96 78 16 9D 01 1E 01 76 6F 72 62 69 73 00 00 00 00 02 44 AC 00 00 00 00 00 00 00 EE 02 00 00 00 00 00 B8

The bytes I have highlighted are tthe number of segments and the length 1E (30). The actual vorbis header is:

01 76 6F 72 62 69 73 00 00 00 00 02 44 AC 00 00 00 00 00 00 00 EE 02 00 00 00 00 00 B8 01

The first byte highlighted is the packet type (1) is the identity header. You can see this from your programme I think.

Code: Select all

Structure VorbisIdentification
  PacketType.a
  Identifier.a[6]
  VorbisVersion.l
  AudioChannnels.a
  AudioSampleRate.l
  BitrateMaximum.l
  BitrateNominal.l
  BitrateMinimum.l
  BlockSize01.a
  FramingFlag.a
EndStructure
The second header is the comments header it begins :-

03 76 6F 72 62 69 73

The first byte being the header type 03 means comments header. In the file I posted this first ogg page has all the segments set to $FF so the comments header is quite long and spans two ogg pages. I think your programme actually does retrieve this over the two pages, being an amateur I am not sure.

The third vorbis header begins:-

05 76 6F 72 62 69 73

05 being the header type which I think is the instructions for the codec?

So if these three could be read into their own buffers is what I am looking for.

The fourth section of a vorbis stream is the audio data this starts on the ogg page after the end of the third vorbis header. It seems to be split into lots of sections and reading a little about packing a vorbis stream into ogg each of the sections is a vorbis packet for use by the vorbis codec about which I do not have a clue.

Just those first three sections I need. Would be nice if the file offset for the first audio page could be kept as well.

I can see that writing vorbis comments, especially if someone wants to insert an image (not using the COVERART tag anymore) can force the comments header to cover more than one ogg page which I will have to deal with, this will mean reading in every ogg page after the comments header rewriting the page sequence number tand checksum and writing it back to a file.

The audio data, if we can extract it, could be one step towards solving the PlaySound() Bug viewtopic.php?f=4&t=73243

So three buffers each holding one vorbis header and then offset to first ogg page with audio would mean christmas has come early.

kind 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.
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 first byte being the header type 03 means comments header. In the file I posted this first ogg page has all the segments set to $FF so the comments header is quite long and spans two ogg pages. I think your programme actually does retrieve this over the two pages, being an amateur I am not sure.
The code I posted retrieves all comment pages; also if there are more than two.
collectordave wrote:05 being the header type which I think is the instructions for the codec?

So if these three could be read into their own buffers is what I am looking for.
While it would be possible to read this header, I doubt if the information it contains is very useful. That's why I skipped it.
collectordave wrote:The audio data, if we can extract it, could be one step towards solving the PlaySound() Bug viewtopic.php?f=4&t=73243
I wasn't aware of that.
I updated the code I posted before to display the duration in seconds.
viewtopic.php?p=539791#p539791
Can you check if the file that gave you problems reports the correct amount of seconds ?
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
Shardik
Addict
Addict
Posts: 1989
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: ogg vorbis comment (tag)

Post by Shardik »

In 2012 kenmo already demonstrated in this example how to determine the length of an ogg file. I tested it with this ogg file with a length of 6:40 minutes (using MacOS 10.6.8 Snow Leopard with PB 5.46 x86 in ASCII mode, Unicode mode didn't work!) and kenmo's example displayed the length correctly. So kenmo's code doesn't have the problem described in collectordave's linked PlaySound() bug.
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Tried both examples here is the outputs

Test File
Arlo Guthrie - Alice's Restaurant.ogg Length 18mins 38secs

kenmo post
0:00 Arlo Guthrie - Alice's Restaurant.ogg

wilbert code
Vorbis stream: 26493
Channels: 2
Sample rate: 44100
Duration: 1116 (18.6mins)
Vendor: AO; aoTuV [20110424] (based on Xiph.Org's libVorbis)
Comments:
TITLE=Alice's Restaurant
ARTIST=Arlo Guthrie
COMMENT=fre:ac - free audio converter <https://www.freac.org/>

wilbert gets very close. However the Playsound() bug is not just the length being reported incorrectly but also Playsound() gives up after 6mins 3sec and stops playing!

I imagine the sound decode follows this route

unpack .ogg file to a vorbis stream which is then passed to the vorbis codec which outputs raw sound data

So the problem with PlaySound() may actually be a problem with LoadSound()

I am not qualified nor do I have anywhere near the knowledge to comment further on the bug except to say that the bigg problem is playback length an 18min song should playback for 18min.
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:So the problem with PlaySound() may actually be a problem with LoadSound()
Did you try LoadSound with #PB_Sound_Streaming ?
It might be that there's a limit to loading the entire song in memory (which is what you do without the #PB_Sound_Streaming flag).
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
Shardik
Addict
Addict
Posts: 1989
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: ogg vorbis comment (tag)

Post by Shardik »

Shardik wrote:PB 5.46 x86 in ASCII mode, Unicode mode didn't work!
collectordave wrote:kenmo post
0:00 Arlo Guthrie - Alice's Restaurant.ogg
Did you test kenmo's example with PB 5.46 x86 in ASCII mode? The result "0:00" is typical when using Unicode mode!
collectordave
Addict
Addict
Posts: 1309
Joined: Fri Aug 28, 2015 6:10 pm
Location: Portugal

Re: ogg vorbis comment (tag)

Post by collectordave »

Hi All

Thanks for the replies.

Tested with PB 5.70 with unicode so fair comment missed that.

This post refers to soundstreaming flag viewtopic.php?f=13&t=59824 but as mentioned there Soundpos() is not available.

However thinking as I type I could start the song with the sttreaming flag so it all plays and set up a timer at the same time then using the wilbert code to get the length I can update the progress bar correctly. I will have to try that after I get my head round writing vorbis comments.

Note:-
Maybe these few posts should be moved to the PlaySound() bug topic.
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 »

Tested sound streaming and all plays ok.

Cannot pause or resume though so not much use for a media player.

It must be a restriction on the length in loadSound().

Can this be removed?

CD

PS Tried same song as a .wav file all played ok? The retriction seems to be for .ogg and .flac diles only?
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:Can this be removed?
You can ask Fred :)

I finally got my code to display the images inside your ogg file :)

Code: Select all

EnableExplicit

UsePNGImageDecoder()
UseJPEGImageDecoder()

; >> 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 VorbisPictureInfo
  Type.l
  MIMEType.s
  Description.s
EndStructure

Structure VorbisComment
  FieldName.s
  Value.s
EndStructure

Structure VorbisStream
  BitstreamSerialNumber.l
  AudioChannels.l
  AudioSampleRate.l
  BitrateMaximum.l
  BitrateNominal.l
  BitrateMinimum.l  
  Duration.l
  Vendor.s
  List Comment.VorbisComment()
EndStructure


; >> ParseOGG procedure <<

Procedure ParseOGG(Filename.s, List VorbisStreams.VorbisStream())
  
  Protected OGGFile.i, NextPageLoc.q, Page.OGGPage, Id.VorbisIdentification
  Protected *Mem.Long, *MemEnd, *a.Ascii, *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
          CopyMemory(@Id\AudioSampleRate, @VorbisStreams()\AudioSampleRate, 16)
        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
                  ; Find '='
                  *a = *Mem : *MemEnd = *Mem + Length
                  While *a < *MemEnd And *a\a <> $3D
                    *a + 1
                  Wend
                  VorbisStreams()\Comment()\FieldName = PeekS(*Mem, *a - *Mem, #PB_UTF8 | #PB_ByteLength)
                  If *a < *MemEnd
                    VorbisStreams()\Comment()\Value = PeekS(*a + 1, *MemEnd - *a - 1, #PB_UTF8 | #PB_ByteLength)
                  EndIf
                  *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)
    ProcedureReturn ListSize(VorbisStreams())
  Else
    ProcedureReturn #False
  EndIf
  
EndProcedure

; >> DecodeVorbisPicture procedure <<

Procedure.i DecodeVorbisPicture(Base64String.s, *Info.VorbisPictureInfo = #Null)
  
  Protected.i i, n = Len(Base64String)
  Protected Dim InputBuffer.a((n >> 6) * 66 + n & 63)
  Protected Dim P.a((3 * n) >> 2)
  Protected *c.Character = @Base64String
  Protected *a.Ascii = @InputBuffer()
  
  ; Decode Base64
  i = 0
  While *c\c
    *a\a = *c\c : i + 1
    *a + 1 : *c + SizeOf(Character)
    If i = 64
      *a\a = #CR : *a + 1
      *a\a = #LF : *a + 1
      i = 0  
    EndIf    
  Wend
  Base64DecoderBuffer(@InputBuffer(), ArraySize(InputBuffer()), @P(), ArraySize(P()))
  
  ; Type
  n = P(0)<<24 | P(1)<<16 | P(2)<<8 | P(3) : i = 4
  If *Info : *Info\Type = n : EndIf
  
  ; MIMEType
  n = P(i)<<24 | P(i+1)<<16 | P(i+2)<<8 | P(i+3) : i + 4
  If *Info : *Info\MIMEType = PeekS(@P(i), n, #PB_Ascii) : EndIf
  i + n
  
  ; Description  
  n = P(i)<<24 | P(i+1)<<16 | P(i+2)<<8 | P(i+3) : i + 4
  If *Info : *Info\Description = PeekS(@P(i), n, #PB_UTF8 | #PB_ByteLength) : EndIf
  i + n + 16
  
  ; Return image
  n = P(i)<<24 | P(i+1)<<16 | P(i+2)<<8 | P(i+3) : i + 4
  If n
    ProcedureReturn CatchImage(#PB_Any, @P(i), n)
  Else
    ProcedureReturn #Null
  EndIf
  
EndProcedure



; Test the code

NewList VorbisStreams.VorbisStream()
Define.i w, h, i, p, Info.VorbisPictureInfo

OpenWindow(0, 0, 0, 420, 320, "OGG Vorbis", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
EditorGadget(0, 10, 10, 400, 300, #PB_Editor_ReadOnly | #PB_Editor_WordWrap)

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

ForEach VorbisStreams()
  AddGadgetItem(0, -1, "Vorbis stream: " + Str(VorbisStreams()\BitstreamSerialNumber))
  AddGadgetItem(0, -1, "Channels: " + Str(VorbisStreams()\AudioChannels))
  AddGadgetItem(0, -1, "Sample rate: " + Str(VorbisStreams()\AudioSampleRate))
  AddGadgetItem(0, -1, "Duration: " + Str(VorbisStreams()\Duration) + " seconds")
  AddGadgetItem(0, -1, "Vendor: " + VorbisStreams()\Vendor)
  ForEach VorbisStreams()\Comment()
    If VorbisStreams()\Comment()\FieldName = "METADATA_BLOCK_PICTURE"
      p = DecodeVorbisPicture(VorbisStreams()\Comment()\Value, @Info)
      If p
        i + 1
        w = ImageWidth(p)
        h = ImageHeight(p)
        OpenWindow(i, 0, 0, w, h, "OGG Vorbis " + Info\MIMEType, #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
        ImageGadget(i, 0, 0, w, h, ImageID(p))
      EndIf
    Else
      AddGadgetItem(0, -1, VorbisStreams()\Comment()\FieldName + " => " + VorbisStreams()\Comment()\Value)
    EndIf
  Next
Next

Repeat
Until WaitWindowEvent() = #PB_Event_CloseWindow
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

Brilliant.

I will get used to you being 5 steps ahead of what I am thinking!

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 »

In case you are also interested in flac files, I read the comments of a flac file are also stored using VORBIS_COMMENT. :)
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 »

Finally understanding a little.

Put together the vorbis comment reader into a module. A good mix of code from wilbert and others.

Thinking ahaead to writing the edited comments back to a file as well. The parseoggfile() procedure provides segment addresses for the three vorbis headers and also the audio start page needed to write comments. Working on that now.

I will add this to the simple ogg player in another thread. I intend vorbiscomment to be an include file.

Here is the code with a quick example of use:-

Code: Select all

EnableExplicit

DeclareModule VorbisComment
  
  ;Only jpeg and png Supported In Comments
  UseJPEGImageDecoder()
  UsePNGImageDecoder()

  ;The SongTags() List Is Available After Using getComments()
  Structure tag
    Length.i
    Title.s
    Value.s
    ImageID.i
  EndStructure
  Global NewList SongTags.tag()
  
  Structure VorbisPictureInfo
    Type.l
    MIMEType.s
    Description.s
  EndStructure
  
  ;Header Information Shared With Module User
  Global Version.i,AudioChannels.i,SampleRate.i
  
  ;The Length of the song and the number of images in comments are not in header
  Global Length.s,ImagesFound.i

  ;Procedures Available
  ;GetComments() Fetches The Comments
  Declare GetComments(FileName.s)
  
  ;If A Picture Is found The Module User Can Have It Decoded Output is the ImageID
  Declare.i DecodeVorbisPicture(Base64String.s, *Info.VorbisPictureInfo = #Null)

EndDeclareModule

Module VorbisComment
  
  ;Structure To Hold OGG page Header Information
  Structure OGGPage
    CapturePattern.l
    Version.a
    HeaderType.a
    GranulePosition.q
    BitstreamSerialNumber.l
    PageSequenceNumber.l
    Checksum.l
    PageSegments.a
    SegmentTable.a[255]
  EndStructure
  Global Page.OGGPage
  
  ;Structure To Hold Section Information From File
  ;Section 1 Is The vorbis Identification Header
  ;Section 2 Is The vorbis Comment Header
  ;Section 3 Is the vorbis codec Instructions
  Structure Section
    IDNumber.i
    PageNumber.i
    FileOffset.i
    BytesToRead.i
  EndStructure
  Global NewList Segments.Section()
  
  ;Structure to hold File Offsets For Each Page Used When Writing Comments To New File
  Structure FileData
    PageNumber.i
    FileOffset.l
  EndStructure
  Global NewList PageOffsets.FileData()

  ;Buffers To Hold Header And Comments
  Global *Header,*Comments
  
  ;Variables Used to Calculate The Length Of The Track
  Global AudioSampleRate.i,LastGranule.i
  
  Procedure.i DecodeVorbisPicture(Base64String.s, *Info.VorbisPictureInfo = #Null)
    
    ;Decode vorbis picture by wilbert    
    
    
    Protected.i i, n = Len(Base64String)
    Protected Dim InputBuffer.a((n >> 6) * 66 + n & 63)
    Protected Dim P.a((3 * n) >> 2)
    Protected *c.Character = @Base64String
    Protected *a.Ascii = @InputBuffer()
 
    ; Decode Base64
    i = 0
    While *c\c
      *a\a = *c\c : i + 1
      *a + 1 : *c + SizeOf(Character)
      If i = 64
        *a\a = #CR : *a + 1
        *a\a = #LF : *a + 1
        i = 0 
      EndIf   
    Wend
    Base64DecoderBuffer(@InputBuffer(), ArraySize(InputBuffer()), @P(), ArraySize(P()))
  
    ; Type
    n = P(0)<<24 | P(1)<<16 | P(2)<<8 | P(3) : i = 4
    If *Info : *Info\Type = n : EndIf
 
    ; MIMEType
    n = P(i)<<24 | P(i+1)<<16 | P(i+2)<<8 | P(i+3) : i + 4
    If *Info 
      *Info\MIMEType = PeekS(@P(i), n, #PB_Ascii) 
    EndIf
    i + n
 
    ; Description 
    n = P(i)<<24 | P(i+1)<<16 | P(i+2)<<8 | P(i+3) 
    i + 4
    If *Info 
      *Info\Description = PeekS(@P(i), n, #PB_UTF8 | #PB_ByteLength) 
    EndIf
    i + n + 16
 
    ; Return image
    n = P(i)<<24 | P(i+1)<<16 | P(i+2)<<8 | P(i+3) 
    i + 4
    If n
      ProcedureReturn CatchImage(#PB_Any, @P(i), n)
    Else
      ProcedureReturn #Null
    EndIf
 
EndProcedure 

  Procedure ProcessComments()
    
    Define VendorLength.i,NumberOfComments.i,CommentLength.i,Picture.i,PictureWidth.i,PictureHeight.i,imgWin.i
    Define ThisComment.s
    Define Info.VorbisPictureInfo
    Protected CurrentPosition.i
    
    ;Get Ready For list
    ClearList(SongTags())
    AddElement(SongTags())
    CurrentPosition = 7
    
    ;Length Of Vendor String
    VendorLength =   PeekL(*Comments + CurrentPosition)
    
    CurrentPosition + 4
    
    SongTags()\Title = "VENDOR" ;Never Set In file
    SongTags()\Value = PeekS(*Comments + CurrentPosition,VendorLength,#PB_UTF8)

    ;Add Vendor string length
    CurrentPosition + VendorLength

    ;Get Number Of Comments
    NumberOfComments = PeekL(*Comments + CurrentPosition)
    CurrentPosition + 4

    ;Fetch Each comment
    For iLoop = 1 To NumberOfComments

      CommentLength = PeekL(*Comments + CurrentPosition)

      CurrentPosition + 4
      ThisComment = PeekS(*Comments + CurrentPosition,CommentLength,#PB_UTF8)
      CurrentPosition + CommentLength
      
      ;Break Up Comment Into Title and Value
      AddElement(SongTags())
      SongTags()\Title = StringField(ThisComment,1,"=")
      SongTags()\Value = StringField(ThisComment,2,"=")    

    Next

  EndProcedure
  
  Procedure ProcessHeader()

    AudioChannels = PeekA(*Header + 11)
    
    SampleRate = PeekL(*Header + 12)
    Duration = LastGranule / SampleRate
    
    Length = Str(Duration / 60) + " Minutes"
    
    Length = Length +  " " + Str(Duration % 60) + " Seconds"

  EndProcedure

  Procedure ParseOGGFile(FileName.s)
    
    Define OGGFile.i,ThisPacket.i
    Define *Mem
    
    ClearList(Segments())
    
    OGGFile = ReadFile(#PB_Any, FileName)

    If OGGFile

      SectionNumber = 1
      PageNumber = 0
      
              ;New Page Add Element To PageOffsets()
        AddElement(PageOffsets())
        PageOffsets()\PageNumber = PageNumber
        PageOffsets()\FileOffset = 0 
      
      
      
      ;Process pages
      While ReadData(OGGFile, @Page, 27) = 27 And
        Page\CapturePattern = $5367674F And
        ReadData(OGGFile, @Page + 27, Page\PageSegments) = Page\PageSegments

        ;Get last Granule Position from end of stream 
        If Page\HeaderType & 4 ;Last Page
          LastGranule = Page\GranulePosition
        EndIf

        ;New Page So New Element Foe A Segment (Section)
        PageNumber + 1
        AddElement(Segments())
        Segments()\IDNumber = SectionNumber
        Segments()\PageNumber = PageNumber
        Segments()\FileOffset = Loc(OGGFile)
      
        ;Audio Section Start?
        If SectionNumber = 4
          AudioStartPage = PageNumber
        EndIf   

        ;Calculate file position of next page
        LastSegment = Page\PageSegments - 1
        NextPageLoc = Loc(OGGFile)
        For i = 0 To LastSegment
          NextPageLoc + Page\SegmentTable[i]
        Next     
        
        ;Another OGG Page Needed
        AddElement(PageOffsets())
        PageOffsets()\PageNumber = PageNumber
        PageOffsets()\FileOffset = NextPageLoc      

        ;Calculate how many bytes to read
        BytesToRead = 0
        For i = 0 To LastSegment
          If Page\SegmentTable[i] = 255
            BytesToRead + 255
          Else
          
            ;End Of A section
            BytesToRead + Page\SegmentTable[i]
          
            Segments()\BytesToRead = BytesToRead
          
            *Segment = ReAllocateMemory(*Segment,BytesToRead)
          
            ReadData(OGGFile,*Segment,BytesToRead)

            ;New Section New entry If Not end of segments
            SectionNumber + 1
            If i < LastSegment
              AddElement(Segments())
              Segments()\IDNumber = SectionNumber
              Segments()\PageNumber = PageNumber
              Segments()\FileOffset = Loc(OGGFile)
            EndIf
          
            BytesToRead = 0
          
          EndIf
      
        Next
      
        If BytesToRead > 0
          Segments()\BytesToRead = BytesToRead
        EndIf

        ;Goto Next Page
        FileSeek(OGGFile,NextPageLoc)
  
      Wend
    
    EndIf
 
  
    ;Put Header Buffer Together
    ThisPacket = 0
  
    ForEach Segments()
    
      If Segments()\IDNumber = 1
      
        FileSeek(OGGFile,Segments()\FileOffset)
      
        *mem = ReAllocateMemory(*Header,ThisPacket + Segments()\BytesToRead,#PB_Memory_NoClear)
      
        *Header = *mem
      
        ReadData(OGGFile,*Header + ThisPacket,Segments()\BytesToRead)
      
        ThisPacket + Segments()\BytesToRead
      
      EndIf
    
    Next
  
    ThisPacket = 0
  
    *Mem = #Null
  
    ForEach Segments()
    
      If Segments()\IDNumber = 2
      
        FileSeek(OGGFile,Segments()\FileOffset)

        *Mem = ReAllocateMemory(*Comments,ThisPacket + Segments()\BytesToRead,#PB_Memory_NoClear)
      
        *Comments = *Mem
      
        ReadData(OGGFile,*Comments + ThisPacket,Segments()\BytesToRead)
      
        ThisPacket + Segments()\BytesToRead
      
      EndIf
    
    Next
    
    CloseFile(OGGFile)
  
  
  EndProcedure

  Procedure GetComments(FileName.s)

    ;Set Everything Back To Start
    *Comments = #Null
    *Header = #Null
    *Mem = #Null
    ClearList(Segments())
    ClearList(SongTags())
    
    ;Now Fill Them For This Song
    ParseOGGFile(FileName)
    ProcessComments()
    ProcessHeader()

  EndProcedure

EndModule

;Start Of Demo Code


;Array To Hold ImageID's
Global Dim vorbisImage.l(0)


Global NewList SongTags.VorbisComment::tag()

Global Info.VorbisComment::VorbisPictureInfo

Global dlgvorbisComment.i,Quit.i, FileName.s

Global edtSongStats, txtFileSpec, txtAvailableComments, txtSelectComments, cmbComments, edtComment, txtImages, cnvPicture, txtImageStatus, btnPrevious, btnNext, btnDone, btnSelectFile,btnSaveComments

Global ImageIndex.i, ImagesFound.i

Define Event.i

Enumeration FormFont
  #Font_Window_0_0
EndEnumeration

LoadFont(#Font_Window_0_0,"Consolas", 10)

Procedure ShowComments(FileName.s)
  
  Protected Picture.i,iLoop.i
  
  
  ;Clear The Gadgets
  StartDrawing(CanvasOutput(cnvPicture))
    Box(0,0,220,220, RGB(255,255,255))
  StopDrawing()
  ClearGadgetItems(edtComment)
  ClearGadgetItems(edtSongStats)    
              
  ;Clear Image Array
  ReDim vorbisImage(0)
  vorbisImage(0) = 0
              
  ;Get The Comments
  VorbisComment::GetComments(FileName)

  ;Show Header Information
  AddGadgetItem(edtSongStats,0,"")  
  AddGadgetItem(edtSongStats,1,"Audio Channels")
  AddGadgetItem(edtSongStats,2,Str(VorbisComment::AudioChannels))
  AddGadgetItem(edtSongStats,3,"") 
  AddGadgetItem(edtSongStats,4,"Sample Rate")
  AddGadgetItem(edtSongStats,5,Str(VorbisComment::SampleRate) + "Hz")  
  AddGadgetItem(edtSongStats,6,"") 
  AddGadgetItem(edtSongStats,7,"Duration")             
  AddGadgetItem(edtSongStats,8, VorbisComment::Length) 
  
  ImagesFound = 0
           
  ForEach VorbisComment::SongTags()

    If VorbisComment::SongTags()\Title = "METADATA_BLOCK_PICTURE"
        
      Picture = VorbisComment::DecodeVorbisPicture(VorbisComment::SongTags()\Value, @Info)
      If Picture
        VorbisComment::SongTags()\ImageID = Picture
        vorbisImage(ImagesFound) = Picture
        ImagesFound + 1
        ReDim vorbisImage(ImagesFound)
      EndIf
    EndIf 
            
  Next
    
  ResetList(VorbisComment::SongTags())

  ClearGadgetItems(cmbComments)
    
  ForEach VorbisComment::SongTags()
  
    If VorbisComment::SongTags()\Title <> "METADATA_BLOCK_PICTURE"
      AddGadgetItem(cmbComments,-1,VorbisComment::SongTags()\Title)
    EndIf
  Next

  If vorbisImage(iLoop) > 0
    ;Show First Image
    ImageIndex = 0
    StartDrawing(CanvasOutput(cnvPicture))
      DrawImage(ImageID(vorbisImage(0)),0,0,220,220)
      StopDrawing()
      SetGadgetText(txtImageStatus,"Image 1 of " + Str(ImagesFound) + " Images")   
    Else
      SetGadgetText(txtImageStatus,"Image 0 of " + Str(ImagesFound) + " Images")
    EndIf
  DisableGadget(btnPrevious,#True)
  
  If ImagesFound > 1
    DisableGadget(btnNext,#False)  
  EndIf
    
EndProcedure

dlgvorbisComment = OpenWindow(#PB_Any, 0, 0, 810, 380, "OGG vorbis Comments", #PB_Window_SystemMenu)
edtSongStats = EditorGadget(#PB_Any, 10, 40, 200, 280, #PB_Editor_ReadOnly)
txtFileSpec = TextGadget(#PB_Any, 10, 10, 200, 20, "File Specification")
txtAvailableComments = TextGadget(#PB_Any, 230, 10, 200, 20, "Available Comments")
txtSelectComments = TextGadget(#PB_Any, 230, 40, 200, 20, "Select Comment")
cmbComments = ComboBoxGadget(#PB_Any, 230, 70, 200, 20)
edtComment = EditorGadget(#PB_Any, 230, 100, 340, 220,#PB_Editor_WordWrap)
txtImages = TextGadget(#PB_Any, 580, 10, 200, 20, "Images")
cnvPicture = CanvasGadget(#PB_Any, 580, 40, 220, 220, #PB_Canvas_Border)
txtImageStatus = TextGadget(#PB_Any, 580, 270, 220, 20, "Image 0 of 0 Images")
btnPrevious = ButtonGadget(#PB_Any, 580, 300, 60, 20, "Previous")
DisableGadget(btnPrevious,#True)
btnNext = ButtonGadget(#PB_Any, 740, 300, 60, 20, "Next")
DisableGadget(btnNext,#True)
btnDone = ButtonGadget(#PB_Any, 670, 340, 130, 25, "Done")
btnSaveComments = ButtonGadget(#PB_Any, 510, 340, 130, 25, "Save Comments")
btnSelectFile = ButtonGadget(#PB_Any, 110, 340, 130, 25, "Select File")

SetGadgetFont(edtSongStats, FontID(#Font_Window_0_0))
SetGadgetFont(edtComment, FontID(#Font_Window_0_0))

Quit = #False  
  
  Repeat
    
    Event = WaitWindowEvent()
    Select Event
      Case #PB_Event_CloseWindow
        Quit = #True
        CloseWindow(dlgvorbisComment)

      Case #PB_Event_Menu
        Select EventMenu()
        EndSelect

      Case #PB_Event_Gadget
        Select EventGadget()
              
          Case btnSelectFile
      
            Filename = OpenFileRequester("Select OGG File", "", "Songs (*.ogg)|*.ogg", 0)  
            If FileName
              ShowComments(FileName)
            EndIf
 
          Case btnNext
   
            If ImageIndex + 1 < ImagesFound 
              ImageIndex + 1
              
              StartDrawing(CanvasOutput(cnvPicture))
                DrawImage(ImageID(vorbisImage(ImageIndex)),0,0,220,220)
              StopDrawing()
           
              SetGadgetText(txtImageStatus,"Image " + Str(ImageIndex + 1) +" of " + Str(ImagesFound) + " Images")
              If ImageIndex > 0
                DisableGadget(btnPrevious,#False)
              EndIf
               If ImageIndex = ArraySize(vorbisImage()) - 1
                DisableGadget(btnNext,#True)
              EndIf           
            
            EndIf
            
          Case btnPrevious
            
            ImageIndex - 1
            
            If ImageIndex > -1 
              StartDrawing(CanvasOutput(cnvPicture))
                DrawImage(ImageID(vorbisImage(ImageIndex)),0,0,220,220)
              StopDrawing()
            EndIf
            SetGadgetText(txtImageStatus,"Image " + Str(ImageIndex + 1) +" of " + Str(ImagesFound) + " Images")
            If ImageIndex = 0
              DisableGadget(btnPrevious,#True)
            EndIf
            If ImageIndex < ArraySize(vorbisImage()) - 1
              DisableGadget(btnNext,#False)
            EndIf            
   
          Case btnDone
            Quit = #True
            CloseWindow(dlgvorbisComment)
              
          Case cmbComments
              
            ClearGadgetItems(edtComment)

            ForEach VorbisComment::SongTags()
                
              If VorbisComment::SongTags()\Title = GetGadgetText(cmbComments)
                  
                ClearGadgetItems(edtComment)
                AddGadgetItem(edtComment,-1,VorbisComment::SongTags()\Value)
                Break
                
              EndIf
                
            Next
   
        EndSelect
      EndSelect
  
    Until Quit = #True
Think I am getting there just in information overload at the moment.

Kind 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 »

Hi All

Reading comments seems to be working great thanks to wilbert and others.

Moving on to writing comments.

The first thing is to ensure we are reading the page sequence and checksums correctly.

I have modyfird infratecs checksum routine to calculate a checksum from a buffer and written a smal program to check an og vorbis files integrity as a start:-

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 Page
  PageNumber.i
  Start.i
  Finish.i
EndStructure

Structure Error
  PageNumber.i
  Error.s
EndStructure

Global NewList Errors.Error()

Global PageCount.i,OriginalCheckSum.s, CalculatedChecksum.s

Global NewList PageOffsets.Page()

Global *Buffer

Procedure.i checksumbuffer(*Buffer,BufferLength.i)
  
  Define iLoop.i
  
  Protected crc_reg.l, b.a
 
  For iLoop = 0 To BufferLength - 1
    
    b = PeekA(*Buffer + iLoop)

    crc_reg = (crc_reg << 8) ! PeekL(?crc_lookup + (((crc_reg >> 24) & $ff) ! b) * 4)
    
    Next iLoop
 
  ProcedureReturn crc_reg
 
EndProcedure

Procedure CheckPage(FileHandle.i,Start.i,Finish.i)
  
  Protected Result.i
  
  Define BufferSize.i 
  Define BufferStart.i,BufferEnd.i
  

  If IsFile(FileHandle)

    BufferSize =  Finish - Start
      
    FileSeek(FileHandle,Start)
   
    If Finish > start
        
      If *buffer
        FreeMemory(*Buffer)
      EndIf
        
      *Buffer = AllocateMemory(BufferSize)
      
      ReadData(FileHandle,*Buffer,BufferSize)

      OriginalCheckSum = Hex(PeekL(*Buffer + 22),#PB_Long)

      ;Zero Checksum Bytes
      PokeL(*buffer + 22,0)
  
      ;Get Checksum Of Buffer
      result = checksumbuffer(*Buffer,BufferSize)
    
      CalculatedCheckSum =  Hex(result,#PB_Long)

      If CalculatedCheckSum <> OriginalCheckSum
      
        AddElement(Errors())
        Errors()\PageNumber = PageOffsets()\PageNumber
        Errors()\Error = "Bad Checksum"
      
      EndIf
    
    
    EndIf
    
  EndIf
  
EndProcedure

Procedure ParseOGG(Filename.s)
 
  Protected OGGFile.i, NextPageLoc.q, Page.OGGPage
  Protected *Mem.Long, *CommentPacket, BitstreamSerialNumber.l
  Protected.i i, NextPage, LastSegment, BytesToRead, PacketSize, Length
 
  OGGFile = ReadFile(#PB_Any, Filename)
  If OGGFile
    ClearList(PageOffsets())
    
    PageCount = 0
    AddElement(PageOffsets())
    PageOffsets()\PageNumber = PageCount
    PageOffsets()\Start = 0

    ; >> 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
      
      PageOffsets()\Finish = NextPageLoc
   
      If PageCount <> Page\PageSequenceNumber
        
        AddElement(Errors())
        Errors()\PageNumber = Page\PageSequenceNumber
        Errors()\Error = "Page sequence Error"
        
      EndIf
      
      PageCount + 1
      
      AddElement(PageOffsets())
      PageOffsets()\PageNumber = PageCount
      PageOffsets()\Start = NextPageLoc
     
      ; Set the file pointer to the next page
      FileSeek(OGGFile, NextPageLoc)
     
    Wend
    CloseFile(OGGFile)
    
  EndIf

EndProcedure

Define FileHandle.i,FileToSearch.s

FileToSearch = OpenFileRequester("Please choose ogg file", "", "Song Files (*.ogg)|*.ogg", 0)

If FileToSearch
  
  ParseOGG(FileToSearch)

  FileHandle = ReadFile(#PB_Any,FileToSearch)
  
  If IsFile(FileHandle)

    ForEach PageOffsets()
  
      CheckPage(FileHandle,PageOffsets()\Start,PageOffsets()\Finish)  
   
    Next

  EndIf

  If ListSize(Errors()) > 0

    ForEach Errors()
  
      Debug Errors()\PageNumber 
      Debug Errors()\Error
    Next

  Else
  
    Debug "No Errors Found"

  EndIf
  
EndIf

End

DataSection
  crc_lookup:
  Data.l $00000000,$04c11db7,$09823b6e,$0d4326d9
  Data.l $130476dc,$17c56b6b,$1a864db2,$1e475005
  Data.l $2608edb8,$22c9f00f,$2f8ad6d6,$2b4bcb61
  Data.l $350c9b64,$31cd86d3,$3c8ea00a,$384fbdbd
  Data.l $4c11db70,$48d0c6c7,$4593e01e,$4152fda9
  Data.l $5f15adac,$5bd4b01b,$569796c2,$52568b75
  Data.l $6a1936c8,$6ed82b7f,$639b0da6,$675a1011
  Data.l $791d4014,$7ddc5da3,$709f7b7a,$745e66cd
  Data.l $9823b6e0,$9ce2ab57,$91a18d8e,$95609039
  Data.l $8b27c03c,$8fe6dd8b,$82a5fb52,$8664e6e5
  Data.l $be2b5b58,$baea46ef,$b7a96036,$b3687d81
  Data.l $ad2f2d84,$a9ee3033,$a4ad16ea,$a06c0b5d
  Data.l $d4326d90,$d0f37027,$ddb056fe,$d9714b49
  Data.l $c7361b4c,$c3f706fb,$ceb42022,$ca753d95
  Data.l $f23a8028,$f6fb9d9f,$fbb8bb46,$ff79a6f1
  Data.l $e13ef6f4,$e5ffeb43,$e8bccd9a,$ec7dd02d
  Data.l $34867077,$30476dc0,$3d044b19,$39c556ae
  Data.l $278206ab,$23431b1c,$2e003dc5,$2ac12072
  Data.l $128e9dcf,$164f8078,$1b0ca6a1,$1fcdbb16
  Data.l $018aeb13,$054bf6a4,$0808d07d,$0cc9cdca
  Data.l $7897ab07,$7c56b6b0,$71159069,$75d48dde
  Data.l $6b93dddb,$6f52c06c,$6211e6b5,$66d0fb02
  Data.l $5e9f46bf,$5a5e5b08,$571d7dd1,$53dc6066
  Data.l $4d9b3063,$495a2dd4,$44190b0d,$40d816ba
  Data.l $aca5c697,$a864db20,$a527fdf9,$a1e6e04e
  Data.l $bfa1b04b,$bb60adfc,$b6238b25,$b2e29692
  Data.l $8aad2b2f,$8e6c3698,$832f1041,$87ee0df6
  Data.l $99a95df3,$9d684044,$902b669d,$94ea7b2a
  Data.l $e0b41de7,$e4750050,$e9362689,$edf73b3e
  Data.l $f3b06b3b,$f771768c,$fa325055,$fef34de2
  Data.l $c6bcf05f,$c27dede8,$cf3ecb31,$cbffd686
  Data.l $d5b88683,$d1799b34,$dc3abded,$d8fba05a
  Data.l $690ce0ee,$6dcdfd59,$608edb80,$644fc637
  Data.l $7a089632,$7ec98b85,$738aad5c,$774bb0eb
  Data.l $4f040d56,$4bc510e1,$46863638,$42472b8f
  Data.l $5c007b8a,$58c1663d,$558240e4,$51435d53
  Data.l $251d3b9e,$21dc2629,$2c9f00f0,$285e1d47
  Data.l $36194d42,$32d850f5,$3f9b762c,$3b5a6b9b
  Data.l $0315d626,$07d4cb91,$0a97ed48,$0e56f0ff
  Data.l $1011a0fa,$14d0bd4d,$19939b94,$1d528623
  Data.l $f12f560e,$f5ee4bb9,$f8ad6d60,$fc6c70d7
  Data.l $e22b20d2,$e6ea3d65,$eba91bbc,$ef68060b
  Data.l $d727bbb6,$d3e6a601,$dea580d8,$da649d6f
  Data.l $c423cd6a,$c0e2d0dd,$cda1f604,$c960ebb3
  Data.l $bd3e8d7e,$b9ff90c9,$b4bcb610,$b07daba7
  Data.l $ae3afba2,$aafbe615,$a7b8c0cc,$a379dd7b
  Data.l $9b3660c6,$9ff77d71,$92b45ba8,$9675461f
  Data.l $8832161a,$8cf30bad,$81b02d74,$857130c3
  Data.l $5d8a9099,$594b8d2e,$5408abf7,$50c9b640
  Data.l $4e8ee645,$4a4ffbf2,$470cdd2b,$43cdc09c
  Data.l $7b827d21,$7f436096,$7200464f,$76c15bf8
  Data.l $68860bfd,$6c47164a,$61043093,$65c52d24
  Data.l $119b4be9,$155a565e,$18197087,$1cd86d30
  Data.l $029f3d35,$065e2082,$0b1d065b,$0fdc1bec
  Data.l $3793a651,$3352bbe6,$3e119d3f,$3ad08088
  Data.l $2497d08d,$2056cd3a,$2d15ebe3,$29d4f654
  Data.l $c5a92679,$c1683bce,$cc2b1d17,$c8ea00a0
  Data.l $d6ad50a5,$d26c4d12,$df2f6bcb,$dbee767c
  Data.l $e3a1cbc1,$e760d676,$ea23f0af,$eee2ed18
  Data.l $f0a5bd1d,$f464a0aa,$f9278673,$fde69bc4
  Data.l $89b8fd09,$8d79e0be,$803ac667,$84fbdbd0
  Data.l $9abc8bd5,$9e7d9662,$933eb0bb,$97ffad0c
  Data.l $afb010b1,$ab710d06,$a6322bdf,$a2f33668
  Data.l $bcb4666d,$b8757bda,$b5365d03,$b1f740b4
EndDataSection
Run the programme and select an ogg file the file will be checked for page sequence errors and every pages checksum will be checked.

Output is any errors found or "No Errors"

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.
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 first thing is to ensure we are reading the page sequence and checksums correctly.

I have modyfird infratecs checksum routine to calculate a checksum from a buffer and written a smal program to check an og vorbis files integrity as a start:
Seems to work fine :)

If desired, you could speed up the crc calculation by using pointers instead of Peek.
(or of course use an assembly based crc routine. :wink: )
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

Thanks for testing.

I am moving on to firstly attempt to split the first three vorbis headers onto an ogg page each instead of scrunching them up. So that ogg page 1 is vorbis header 1 (no problem) section 2 comments on to page or pages, can be multiple, then vorbis header 3 on the next page after comments. Only 1 vorbis section to pack in the ogg file then comments. All the audio data is after vorbis header 3 so the most I will have to do is rewrite the page sequence number calculate the CRC for each page and add to the new file.

My big worry for later is encodeing images ready to be placed in the comment pages.

Of course anything that can be done to speed this all up would be welcome.

Kind 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.
Post Reply