355 lines
9.4 KiB
Go
355 lines
9.4 KiB
Go
// TODO(u): Prefix lines with file name if flag.NArg() > 1. Example:
|
|
// tone24bit.flac:METADATA block #2
|
|
// tone24bit.flac: type: 4 (VORBIS_COMMENT)
|
|
// tone24bit.flac: is last: false
|
|
// tone24bit.flac: length: 40
|
|
// tone24bit.flac: vendor string: reference libFLAC 1.1.4 20070213
|
|
// tone24bit.flac: comments: 0
|
|
// tone24bit.flac:METADATA block #3
|
|
// tone24bit.flac: type: 1 (PADDING)
|
|
// tone24bit.flac: is last: true
|
|
// tone24bit.flac: length: 8192
|
|
// zonophone-x28010-10407u.flac:METADATA block #0
|
|
// zonophone-x28010-10407u.flac: type: 0 (STREAMINFO)
|
|
// zonophone-x28010-10407u.flac: is last: false
|
|
// zonophone-x28010-10407u.flac: length: 34
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.gammaspectra.live/S.O.N.G/flacgo"
|
|
"git.gammaspectra.live/S.O.N.G/flacgo/meta"
|
|
)
|
|
|
|
// flagBlockNum contains an optional comma-separated list of block numbers to
|
|
// display.
|
|
var flagBlockNum string
|
|
|
|
func init() {
|
|
flag.StringVar(&flagBlockNum, "block-number", "", "An optional comma-separated list of block numbers to display.")
|
|
flag.Usage = usage
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, "Usage: metaflac [OPTION]... FILE...")
|
|
fmt.Fprintln(os.Stderr)
|
|
fmt.Fprintln(os.Stderr, "Flags:")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
if flag.NArg() < 1 {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
for _, path := range flag.Args() {
|
|
err := list(path)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func list(path string) error {
|
|
var blockNums []int
|
|
if flagBlockNum != "" {
|
|
// Parse "--block-number" command line flag.
|
|
rawBlockNums := strings.Split(flagBlockNum, ",")
|
|
for _, rawBlockNum := range rawBlockNums {
|
|
blockNum, err := strconv.Atoi(rawBlockNum)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
blockNums = append(blockNums, blockNum)
|
|
}
|
|
}
|
|
|
|
// Open FLAC stream.
|
|
stream, err := flac.ParseFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// List all blocks.
|
|
if blockNums == nil {
|
|
var isLast bool
|
|
if len(stream.Blocks) == 0 {
|
|
isLast = true
|
|
}
|
|
listStreamInfoHeader(isLast)
|
|
listStreamInfo(stream.Info)
|
|
for blockNum, block := range stream.Blocks {
|
|
// strea.Blocks doesn't contain StreamInfo, therefore the blockNum
|
|
// is one less.
|
|
blockNum--
|
|
listBlock(block, blockNum)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Only list blocks specified in the "--block-number" command line flag.
|
|
for _, blockNum := range blockNums {
|
|
if blockNum == 0 {
|
|
listStreamInfo(stream.Info)
|
|
} else {
|
|
// strea.Blocks doesn't contain StreamInfo, therefore the blockNum
|
|
// is one less.
|
|
blockNum--
|
|
}
|
|
if blockNum < len(stream.Blocks) {
|
|
listBlock(stream.Blocks[blockNum], blockNum)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func listBlock(block *meta.Block, blockNum int) {
|
|
listHeader(&block.Header, blockNum)
|
|
switch body := block.Body.(type) {
|
|
case *meta.Application:
|
|
listApplication(body)
|
|
case *meta.SeekTable:
|
|
listSeekTable(body)
|
|
case *meta.VorbisComment:
|
|
listVorbisComment(body)
|
|
case *meta.CueSheet:
|
|
listCueSheet(body)
|
|
case *meta.Picture:
|
|
listPicture(body)
|
|
}
|
|
}
|
|
|
|
// typeName maps from metadata block type to a string version of its name.
|
|
var typeName = map[meta.Type]string{
|
|
meta.TypeStreamInfo: "STREAMINFO",
|
|
meta.TypePadding: "PADDING",
|
|
meta.TypeApplication: "APPLICATION",
|
|
meta.TypeSeekTable: "SEEKTABLE",
|
|
meta.TypeVorbisComment: "VORBIS_COMMENT",
|
|
meta.TypeCueSheet: "CUESHEET",
|
|
meta.TypePicture: "PICTURE",
|
|
}
|
|
|
|
// Each field of the StreamInfo header is constant, with the exception of
|
|
// is_last.
|
|
//
|
|
// Example:
|
|
//
|
|
// METADATA block #0
|
|
// type: 0 (STREAMINFO)
|
|
// is last: false
|
|
// length: 34
|
|
func listStreamInfoHeader(isLast bool) {
|
|
fmt.Println("METADATA block #0")
|
|
fmt.Println(" type: 0 (STREAMINFO)")
|
|
fmt.Println(" is last:", isLast)
|
|
fmt.Println(" length: 34")
|
|
}
|
|
|
|
// Example:
|
|
//
|
|
// METADATA block #0
|
|
// type: 0 (STREAMINFO)
|
|
// is last: false
|
|
// length: 34
|
|
func listHeader(header *meta.Header, blockNum int) {
|
|
name, ok := typeName[header.Type]
|
|
if !ok {
|
|
name = "UNKNOWN"
|
|
}
|
|
fmt.Printf("METADATA block #%d\n", blockNum)
|
|
fmt.Printf(" type: %d (%s)\n", header.Type, name)
|
|
fmt.Printf(" is last: %t\n", header.IsLast)
|
|
fmt.Printf(" length: %d\n", header.Length)
|
|
}
|
|
|
|
// Example:
|
|
//
|
|
// minimum blocksize: 4608 samples
|
|
// maximum blocksize: 4608 samples
|
|
// minimum framesize: 0 bytes
|
|
// maximum framesize: 19024 bytes
|
|
// sample_rate: 44100 Hz
|
|
// channels: 2
|
|
// bits-per-sample: 16
|
|
// total samples: 151007220
|
|
// MD5 signature: 2e6238f5d9fe5c19f3ead628f750fd3d
|
|
func listStreamInfo(si *meta.StreamInfo) {
|
|
fmt.Printf(" minimum blocksize: %d samples\n", si.BlockSizeMin)
|
|
fmt.Printf(" maximum blocksize: %d samples\n", si.BlockSizeMax)
|
|
fmt.Printf(" minimum framesize: %d bytes\n", si.FrameSizeMin)
|
|
fmt.Printf(" maximum framesize: %d bytes\n", si.FrameSizeMax)
|
|
fmt.Printf(" sample_rate: %d Hz\n", si.SampleRate)
|
|
fmt.Printf(" channels: %d\n", si.NChannels)
|
|
fmt.Printf(" bits-per-sample: %d\n", si.BitsPerSample)
|
|
fmt.Printf(" total samples: %d\n", si.NSamples)
|
|
fmt.Printf(" MD5 signature: %x\n", si.MD5sum)
|
|
}
|
|
|
|
// Example:
|
|
//
|
|
// application ID: 46696361
|
|
// data contents:
|
|
// Medieval CUE Splitter (www.medieval.it)
|
|
func listApplication(app *meta.Application) {
|
|
fmt.Printf(" application ID: %d\n", app.ID)
|
|
fmt.Println(" data contents:")
|
|
if len(app.Data) > 0 {
|
|
fmt.Println(string(app.Data))
|
|
}
|
|
}
|
|
|
|
// Example:
|
|
//
|
|
// seek points: 17
|
|
// point 0: sample_number=0, stream_offset=0, frame_samples=4608
|
|
// point 1: sample_number=2419200, stream_offset=3733871, frame_samples=4608
|
|
// ...
|
|
func listSeekTable(st *meta.SeekTable) {
|
|
fmt.Printf(" seek points: %d\n", len(st.Points))
|
|
for pointNum, point := range st.Points {
|
|
if point.SampleNum == meta.PlaceholderPoint {
|
|
fmt.Printf(" point %d: PLACEHOLDER\n", pointNum)
|
|
} else {
|
|
fmt.Printf(" point %d: sample_number=%d, stream_offset=%d, frame_samples=%d\n", pointNum, point.SampleNum, point.Offset, point.NSamples)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Example:
|
|
//
|
|
// vendor string: reference libFLAC 1.2.1 20070917
|
|
// comments: 10
|
|
// comment[0]: ALBUM=「sugar sweet nightmare」 & 「化物語」劇伴音楽集 其の壹
|
|
// comment[1]: ARTIST=神前暁
|
|
// ...
|
|
func listVorbisComment(vc *meta.VorbisComment) {
|
|
fmt.Printf(" vendor string: %s\n", vc.Vendor)
|
|
fmt.Printf(" comments: %d\n", len(vc.Tags))
|
|
for tagNum, tag := range vc.Tags {
|
|
fmt.Printf(" comment[%d]: %s=%s\n", tagNum, tag[0], tag[1])
|
|
}
|
|
}
|
|
|
|
// Example:
|
|
//
|
|
// media catalog number:
|
|
// lead-in: 88200
|
|
// is CD: true
|
|
// number of tracks: 18
|
|
// track[0]
|
|
// offset: 0
|
|
// number: 1
|
|
// ISRC:
|
|
// type: AUDIO
|
|
// pre-emphasis: false
|
|
// number of index points: 1
|
|
// index[0]
|
|
// offset: 0
|
|
// number: 1
|
|
// track[1]
|
|
// offset: 2421384
|
|
// number: 2
|
|
// ISRC:
|
|
// type: AUDIO
|
|
// pre-emphasis: false
|
|
// number of index points: 1
|
|
// index[0]
|
|
// offset: 0
|
|
// number: 1
|
|
// ...
|
|
// track[17]
|
|
// offset: 151007220
|
|
// number: 170 (LEAD-OUT)
|
|
func listCueSheet(cs *meta.CueSheet) {
|
|
fmt.Printf(" media catalog number: %s\n", cs.MCN)
|
|
fmt.Printf(" lead-in: %d\n", cs.NLeadInSamples)
|
|
fmt.Printf(" is CD: %t\n", cs.IsCompactDisc)
|
|
fmt.Printf(" number of tracks: %d\n", len(cs.Tracks))
|
|
for trackNum, track := range cs.Tracks {
|
|
fmt.Printf(" track[%d]\n", trackNum)
|
|
fmt.Printf(" offset: %d\n", track.Offset)
|
|
if trackNum == len(cs.Tracks)-1 {
|
|
// Lead-out track.
|
|
fmt.Printf(" number: %d (LEAD-OUT)\n", track.Num)
|
|
continue
|
|
}
|
|
fmt.Printf(" number: %d\n", track.Num)
|
|
fmt.Printf(" ISRC: %s\n", track.ISRC)
|
|
var trackTypeName = map[bool]string{
|
|
false: "DATA",
|
|
true: "AUDIO",
|
|
}
|
|
fmt.Printf(" type: %s\n", trackTypeName[track.IsAudio])
|
|
fmt.Printf(" pre-emphasis: %t\n", track.HasPreEmphasis)
|
|
fmt.Printf(" number of index points: %d\n", len(track.Indicies))
|
|
for indexNum, index := range track.Indicies {
|
|
fmt.Printf(" index[%d]\n", indexNum)
|
|
fmt.Printf(" offset: %d\n", index.Offset)
|
|
fmt.Printf(" number: %d\n", index.Num)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Example:
|
|
//
|
|
// type: 3 (Cover (front))
|
|
// MIME type: image/jpeg
|
|
// description:
|
|
// width: 0
|
|
// height: 0
|
|
// depth: 0
|
|
// colors: 0 (unindexed)
|
|
// data length: 234569
|
|
// data:
|
|
// 00000000: FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 00 60 ......JFIF.....`
|
|
// 00000010: 00 60 00 00 FF DB 00 43 00 01 01 01 01 01 01 01 .`.....C........
|
|
func listPicture(pic *meta.Picture) {
|
|
typeName := map[uint32]string{
|
|
0: "Other",
|
|
1: "32x32 pixels 'file icon' (PNG only)",
|
|
2: "Other file icon",
|
|
3: "Cover (front)",
|
|
4: "Cover (back)",
|
|
5: "Leaflet page",
|
|
6: "Media (e.g. label side of CD)",
|
|
7: "Lead artist/lead performer/soloist",
|
|
8: "Artist/performer",
|
|
9: "Conductor",
|
|
10: "Band/Orchestra",
|
|
11: "Composer",
|
|
12: "Lyricist/text writer",
|
|
13: "Recording Location",
|
|
14: "During recording",
|
|
15: "During performance",
|
|
16: "Movie/video screen capture",
|
|
17: "A bright coloured fish",
|
|
18: "Illustration",
|
|
19: "Band/artist logotype",
|
|
20: "Publisher/Studio logotype",
|
|
}
|
|
fmt.Printf(" type: %d (%s)\n", pic.Type, typeName[pic.Type])
|
|
fmt.Printf(" MIME type: %s\n", pic.MIME)
|
|
fmt.Printf(" description: %s\n", pic.Desc)
|
|
fmt.Printf(" width: %d\n", pic.Width)
|
|
fmt.Printf(" height: %d\n", pic.Height)
|
|
fmt.Printf(" depth: %d\n", pic.Depth)
|
|
fmt.Printf(" colors: %d", pic.NPalColors)
|
|
if pic.NPalColors == 0 {
|
|
fmt.Print(" (unindexed)")
|
|
}
|
|
fmt.Println()
|
|
fmt.Printf(" data length: %d\n", len(pic.Data))
|
|
fmt.Printf(" data:\n")
|
|
fmt.Print(hex.Dump(pic.Data))
|
|
}
|