Implemented basic FLAC support (no artwork, yet!)

This commit is contained in:
David Howden 2015-04-15 00:09:58 +10:00
parent 8aff503453
commit 4cebcae157
3 changed files with 233 additions and 0 deletions

223
flac.go Normal file
View File

@ -0,0 +1,223 @@
// Copyright 2015, David Howden
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tag
import (
"bytes"
"errors"
"io"
"log"
"os"
"strconv"
"strings"
)
// BlockType is a type which represents an enumeration of valid FLAC blocks
type BlockType byte
const (
StreamInfoBlock BlockType = 0
PaddingBlock = 1
ApplicationBlock = 2
SeektableBlock = 3
VorbisCommentBlock = 4 // Supported
CueSheetBlock = 5
PictureBlock = 6 // Supported
)
// ReadFLACTags reads FLAC metadata from the io.ReadSeeker, returning the resulting
// metadata in a Metadata implementation, or non-nil error if there was a problem.
func ReadFLACTags(r io.ReadSeeker) (Metadata, error) {
_, err := r.Seek(0, os.SEEK_SET)
if err != nil {
return nil, err
}
flac, err := readString(r, 4)
if err != nil {
return nil, err
}
if flac != "fLaC" {
return nil, errors.New("expected 'fLaC'")
}
m := &metadataFLAC{
c: make(map[string]string),
}
for {
last, err := m.readFLACMetadataBlock(r)
if err != nil {
return nil, err
}
if last {
break
}
}
return m, nil
}
type metadataFLAC struct {
c map[string]string // the vorbis comments
p *Picture
}
func (m *metadataFLAC) readFLACMetadataBlock(rs io.ReadSeeker) (last bool, err error) {
blockHeader, err := readBytes(rs, 1)
if err != nil {
log.Println(err)
return
}
if getBit(blockHeader[0], 7) {
blockHeader[0] ^= (1 << 7)
last = true
}
blockLen, err := readInt(rs, 3)
if err != nil {
return
}
if BlockType(blockHeader[0]) != VorbisCommentBlock {
_, err = rs.Seek(int64(blockLen), os.SEEK_CUR)
return
}
commentBytes := make([]byte, blockLen)
_, err = rs.Read(commentBytes)
if err != nil {
return
}
r := bytes.NewReader(commentBytes)
m.readVorbisComment(r)
return
}
func (m *metadataFLAC) readVorbisComment(r io.Reader) error {
vendorLen, err := readInt32LittleEndian(r)
if err != nil {
return err
}
vendor, err := readString(r, vendorLen)
if err != nil {
return err
}
m.c["vendor"] = vendor
commentsLen, err := readInt32LittleEndian(r)
if err != nil {
return err
}
for i := 0; i < commentsLen; i++ {
l, err := readInt32LittleEndian(r)
if err != nil {
return err
}
s, err := readString(r, l)
if err != nil {
return err
}
k, v, err := parseComment(s)
if err != nil {
return err
}
m.c[strings.ToLower(k)] = v
}
return nil
}
func parseComment(c string) (k, v string, err error) {
kv := strings.SplitN(c, "=", 2)
if len(kv) != 2 {
err = errors.New("vorbis comment must contain '='")
return
}
k = kv[0]
v = kv[1]
return
}
func (m *metadataFLAC) Format() Format {
return FLAC
}
func (m *metadataFLAC) Raw() map[string]interface{} {
raw := make(map[string]interface{}, len(m.c))
for k, v := range m.c {
raw[k] = v
}
return raw
}
func (m *metadataFLAC) Title() string {
return m.c["title"]
}
func (m *metadataFLAC) Artist() string {
// PERFORMER
// The artist(s) who performed the work. In classical music this would be the
// conductor, orchestra, soloists. In an audio book it would be the actor who
// did the reading. In popular music this is typically the same as the ARTIST
// and is omitted.
if m.c["performer"] != "" {
return m.c["performer"]
}
return m.c["artist"]
}
func (m *metadataFLAC) Album() string {
return m.c["album"]
}
func (m *metadataFLAC) AlbumArtist() string {
// This field isn't included in the standard.
return ""
}
func (m *metadataFLAC) Composer() string {
// ARTIST
// The artist generally considered responsible for the work. In popular music
// this is usually the performing band or singer. For classical music it would
// be the composer. For an audio book it would be the author of the original text.
if m.c["composer"] != "" {
return m.c["composer"]
}
if m.c["performer"] == "" {
return ""
}
return m.c["artist"]
}
func (m *metadataFLAC) Genre() string {
return m.c["genre"]
}
func (m *metadataFLAC) Year() int {
// FIXME: try to parse the date in m.c["date"] to extract this
return 0
}
func (m *metadataFLAC) Track() (int, int) {
x, _ := strconv.Atoi(m.c["tracknumber"])
// https://wiki.xiph.org/Field_names
n, _ := strconv.Atoi(m.c["tracktotal"])
return x, n
}
func (m *metadataFLAC) Disc() (int, int) {
// https://wiki.xiph.org/Field_names
x, _ := strconv.Atoi(m.c["discnumber"])
n, _ := strconv.Atoi(m.c["disctotal"])
return x, n
}
func (m *metadataFLAC) Picture() *Picture {
return m.p
}

3
tag.go
View File

@ -26,6 +26,8 @@ func ReadFrom(r io.ReadSeeker) (Metadata, error) {
}
switch {
case string(b[0:4]) == "fLaC":
return ReadFLACTags(r)
case string(b[4:11]) == "ftypM4A":
return ReadAtoms(r)
@ -53,6 +55,7 @@ const (
ID3v2_3 = "ID3v2.3" // ID3v2.3 tag format (most common).
ID3v2_4 = "ID3v2.4" // ID3v2.4 tag format.
MP4 = "MP4" // MP4 tag (atom) format.
FLAC = "FLAC" // FLAC (Vorbis Comment) tag format.
)
// Metadata is an interface which is used to describe metadata retrieved by this package.

View File

@ -5,6 +5,7 @@
package tag
import (
"encoding/binary"
"io"
)
@ -63,3 +64,9 @@ func read7BitChunkedInt(r io.Reader, n int) (int, error) {
}
return get7BitChunkedInt(b), nil
}
func readInt32LittleEndian(r io.Reader) (int, error) {
var n int32
err := binary.Read(r, binary.LittleEndian, &n)
return int(n), err
}