diff --git a/cmd/decode/decode.go b/cmd/decode/decode.go new file mode 100644 index 0000000..6bbd965 --- /dev/null +++ b/cmd/decode/decode.go @@ -0,0 +1,141 @@ +package main + +import ( + "crypto/md5" + "encoding/hex" + "flag" + "git.gammaspectra.live/S.O.N.G/Kirika/audio" + "git.gammaspectra.live/S.O.N.G/Kirika/audio/format" + "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess" + "git.gammaspectra.live/S.O.N.G/Kirika/hasher" + "hash" + "log" + "os" + "runtime" + "strconv" + "time" + "unsafe" +) + +func main() { + inputFile := flag.String("input", "", "Input file to decode") + + flag.Parse() + + fp, err := os.Open(*inputFile) + if err != nil { + log.Panic(err) + return + } + defer fp.Close() + log.Printf("File: %s\n", *inputFile) + + decoders, err := guess.GetDecoders(fp, *inputFile) + if err != nil { + log.Panic(err) + return + } + + log.Print("Available decoders:\n") + for _, d := range decoders { + if ad, ok := d.(format.AnalyzerDecoder); ok { + log.Printf("\t%s: %s (analyzer)", ad.Name(), ad.DecoderDescription()) + } else { + log.Printf("\t%s: %s", d.Name(), d.DecoderDescription()) + } + } + + startTime := time.Now() + + source, analyzer, err := guess.OpenAnalyzer(fp, decoders) + if err != nil { + source, err = guess.Open(fp, decoders) + } + + if err != nil { + log.Panic(err) + return + } + + var h *hasher.Hasher + + if analyzer != nil { + h = hasher.NewHasher(analyzer, hasher.HashtypeMd5) + } + + var rawFloat32Hasher hash.Hash + + var sampleCount, blockCount uint64 + if f32Source, ok := source.(audio.TypedSource[float32]); ok { + rawFloat32Hasher = md5.New() + log.Printf("Decoding as float32 @ %dHz %d channel(s)", f32Source.GetSampleRate(), f32Source.GetChannels()) + for b := range f32Source.GetBlocks() { + blockCount++ + sampleCount += uint64(len(b)) + + func() { + defer runtime.KeepAlive(b) + rawFloat32Hasher.Write(unsafe.Slice((*byte)(unsafe.Pointer(&b[0])), len(b)*int(unsafe.Sizeof(float32(0))))) + }() + } + sampleCount /= uint64(f32Source.GetChannels()) + } else if i16Source, ok := source.(audio.TypedSource[int16]); ok { + log.Printf("Decoding as int16 %d-bit @ %dHz %d channel(s)", i16Source.GetBitDepth(), i16Source.GetSampleRate(), i16Source.GetChannels()) + for b := range i16Source.GetBlocks() { + blockCount++ + sampleCount += uint64(len(b)) + } + sampleCount /= uint64(i16Source.GetChannels()) + } else if i32Source, ok := source.(audio.TypedSource[int32]); ok { + log.Printf("Decoding as int32 %d-bit @ %dHz %d channel(s)", i32Source.GetBitDepth(), i32Source.GetSampleRate(), i32Source.GetChannels()) + for b := range i32Source.GetBlocks() { + blockCount++ + sampleCount += uint64(len(b)) + } + sampleCount /= uint64(i32Source.GetChannels()) + } else { + rawFloat32Hasher = md5.New() + f32Source = source.ToFloat32() + log.Printf("Decoding as float32 (generic) %d-bit @ %dHz %d channel(s)", f32Source.GetBitDepth(), f32Source.GetSampleRate(), f32Source.GetChannels()) + for b := range f32Source.GetBlocks() { + blockCount++ + sampleCount += uint64(len(b)) + + func() { + defer runtime.KeepAlive(b) + rawFloat32Hasher.Write(unsafe.Slice((*byte)(unsafe.Pointer(&b[0])), len(b)*int(unsafe.Sizeof(float32(0))))) + }() + } + sampleCount /= uint64(f32Source.GetChannels()) + } + + log.Printf("Decoded %d sample(s)", sampleCount) + if blockCount > 0 { + log.Printf("Processed %d blocks(s) (avg %0.2f sample(s) per block)", blockCount, float64(sampleCount)/float64(blockCount)) + } + log.Printf("Duration %s", ((time.Duration(sampleCount) * time.Second) / time.Duration(source.GetSampleRate())).String()) + + if h != nil { + h.Wait() + log.Printf("MD5 hash: %s", hex.EncodeToString(h.GetResult())) + if _, ok := source.(audio.TypedSource[float32]); ok { + log.Printf("\tCheck via $ ffmpeg -hide_banner -loglevel error -i %s -vn -c:a pcm_f32le -f md5 -", strconv.Quote(*inputFile)) + } else { + switch source.GetBitDepth() { + case 8, 16, 24, 32: + log.Printf("\tCheck via $ ffmpeg -hide_banner -loglevel error -i %s -vn -c:a pcm_s%dle -f md5 -", strconv.Quote(*inputFile), source.GetBitDepth()) + default: + log.Printf("\tCheck via $ ffmpeg -hide_banner -loglevel error -i %s -vn -c:a pcm_s32le -f md5 -", strconv.Quote(*inputFile)) + } + } + } else if rawFloat32Hasher != nil { + log.Printf("MD5 hash: %s", hex.EncodeToString(rawFloat32Hasher.Sum(nil))) + if _, ok := source.(audio.TypedSource[float32]); ok { + log.Printf("\tCheck via $ ffmpeg -hide_banner -loglevel error -i %s -vn -c:a pcm_f32le -f md5 -", strconv.Quote(*inputFile)) + log.Printf("\tNote that you might need to specify a decoder with -c:a before -i to perfectly match the hash on lossy decoders, for example, libopus instead of opus") + } + } + + log.Printf("Took %s", time.Now().Sub(startTime).String()) + +}