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