Skip to content

Instantly share code, notes, and snippets.

@miy4
Created November 5, 2021 00:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save miy4/68e0221aecd8292d7f46cc015259b10b to your computer and use it in GitHub Desktop.
Save miy4/68e0221aecd8292d7f46cc015259b10b to your computer and use it in GitHub Desktop.
FLAC metadata parsing in Go
package main
// https://xiph.org/flac/format.html
// https://www.xiph.org/vorbis/doc/v-comment.html
import (
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"strings"
)
const (
BLOCK_TYPE_STREAMINFO = uint8(0) + iota
BLOCK_TYPE_PADDING
BLOCK_TYPE_APPLICATION
BLOCK_TYPE_SEEKTABLE
BLOCK_TYPE_VORBIS_COMMENT
BLOCK_TYPE_CUESHEET
BLOCK_TYPE_PICTURE
)
type MetadataBlock struct {
typ uint8
offset uint32
length uint32
isLast bool
chunk io.ReadSeeker
}
func (m MetadataBlock) String() string {
return fmt.Sprintf("MetadataBlock { typ: %d, offset: %d, length: %d, isLast: %t }", m.typ, m.offset, m.length, m.isLast)
}
type Parser struct {
pos uint32
tagKV map[string]string
}
func (p *Parser) Reset() {
p.pos = 0
p.tagKV = make(map[string]string)
}
func (p *Parser) readStreamMarker(in *os.File) (string, error) {
b := make([]byte, 4)
n, err := in.Read(b)
if err != nil {
return "", err
} else if n != 4 {
return "", errors.New("not enough stream marker")
}
p.pos += 4
return string(b), nil
}
func (p *Parser) readMetadataBlock(in *os.File) (*MetadataBlock, error) {
var header uint32
err := binary.Read(in, binary.BigEndian, &header)
if err != nil {
return nil, err
}
offset := p.pos
p.pos += 4
isLast := (header&0x80000000 == 0x80000000)
blockType := uint8((header >> 24) & 0x7F)
length := header & 0xFFFFFF
in.Seek(int64(length), 1)
p.pos += length
chunk := io.NewSectionReader(in, int64(offset), 4+int64(length))
block := &MetadataBlock{blockType, offset, 4 + length, isLast, chunk}
return block, nil
}
func (p *Parser) readVorbisComment(block *MetadataBlock) {
ch := block.chunk
ch.Seek(4, 0) // skip the header
var vendorLen uint32
binary.Read(ch, binary.LittleEndian, &vendorLen)
vendor := make([]byte, vendorLen)
ch.Read(vendor)
var tagListLen uint32
binary.Read(ch, binary.LittleEndian, &tagListLen)
for i := 0; i < int(tagListLen); i++ {
var tagLen uint32
binary.Read(ch, binary.LittleEndian, &tagLen)
tag := make([]byte, tagLen)
ch.Read(tag)
keyAndValue := strings.SplitN(string(tag), "=", 2)
p.tagKV[keyAndValue[0]] = keyAndValue[1]
}
}
func (p *Parser) PrintFlacTag(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
marker, err := p.readStreamMarker(file)
if err != nil {
return err
} else if marker != "fLaC" {
return fmt.Errorf("Unexpected stream marker: %s", marker)
}
var block *MetadataBlock
for {
block, err = p.readMetadataBlock(file)
if err != nil {
return err
}
if block.typ == BLOCK_TYPE_VORBIS_COMMENT {
p.readVorbisComment(block)
}
if block.isLast {
break
}
}
if len(p.tagKV) != 0 {
for k, v := range p.tagKV {
fmt.Printf("%s = \"%s\"\n", k, v)
}
}
return nil
}
func main() {
parser := &Parser{}
parser.Reset()
if err := parser.PrintFlacTag(os.Args[1]); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment