flacgo/cmd/go-metaflac/metaflac.go
2022-07-26 16:14:37 +02:00

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))
}