package ncmcrypt import ( "bytes" "encoding/binary" "fmt" "github.com/go-flac/go-flac" ) func EmbedMetadata(audio []byte, format NcmFormat, title, artist, album string, coverData []byte) ([]byte, error) { switch format { case Mp3: return embedMP3(audio, title, artist, album, coverData) case Flac: return embedFLAC(audio, title, artist, album, coverData) default: return audio, nil } } func synchSafe(v int) []byte { b := make([]byte, 4) b[0] = byte((v >> 21) & 0x7F) b[1] = byte((v >> 14) & 0x7F) b[2] = byte((v >> 7) & 0x7F) b[3] = byte(v & 0x7F) return b } func textFrame(id string, text string) []byte { if text == "" { return nil } data := append([]byte{0x03}, []byte(text)...) size := len(data) frame := make([]byte, 10+size) copy(frame[0:4], id) copy(frame[4:8], synchSafe(size)) frame[8] = 0x00 frame[9] = 0x00 copy(frame[10:], data) return frame } func apicFrame(mime string, picType byte, description string, data []byte) []byte { if len(data) == 0 { return nil } inner := make([]byte, 0, 1+len(mime)+1+1+len(description)+1+len(data)) inner = append(inner, 0x00) inner = append(inner, []byte(mime)...) inner = append(inner, 0x00) inner = append(inner, picType) inner = append(inner, []byte(description)...) inner = append(inner, 0x00) inner = append(inner, data...) size := len(inner) frame := make([]byte, 10+size) copy(frame[0:4], "APIC") copy(frame[4:8], synchSafe(size)) frame[8] = 0x00 frame[9] = 0x00 copy(frame[10:], inner) return frame } func findMpegSync(data []byte) int { for i := 0; i < len(data)-1; i++ { if data[i] == 0xFF && (data[i+1]&0xE0) == 0xE0 { return i } } return len(data) } func skipID3v2(data []byte) int { if len(data) >= 10 && string(data[0:3]) == "ID3" { size := int(data[6])<<21 | int(data[7])<<14 | int(data[8])<<7 | int(data[9]) return 10 + size } return 0 } func imageMime(data []byte) string { if len(data) >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 { return "image/png" } return "image/jpeg" } func embedMP3(audio []byte, title, artist, album string, coverData []byte) ([]byte, error) { id3End := skipID3v2(audio) frames := bytes.NewBuffer(nil) if f := textFrame("TIT2", title); f != nil { frames.Write(f) } if f := textFrame("TPE1", artist); f != nil { frames.Write(f) } if f := textFrame("TALB", album); f != nil { frames.Write(f) } if f := apicFrame(imageMime(coverData), 0x03, "", coverData); f != nil { frames.Write(f) } if frames.Len() == 0 { return audio, nil } body := frames.Bytes() tagSize := len(body) tag := make([]byte, 10+tagSize) copy(tag[0:3], "ID3") tag[3] = 0x03 tag[4] = 0x00 tag[5] = 0x00 copy(tag[6:10], synchSafe(tagSize)) copy(tag[10:], body) out := make([]byte, 0, len(tag)+len(audio)-id3End) out = append(out, tag...) out = append(out, audio[id3End:]...) return out, nil } func embedFLAC(audio []byte, title, artist, album string, coverData []byte) ([]byte, error) { f, err := flac.ParseBytes(bytes.NewReader(audio)) if err != nil { return nil, fmt.Errorf("parse FLAC: %w", err) } metaOffset := 4 for _, block := range f.Meta { metaOffset += 4 + len(block.Data) } audioFrames := audio[metaOffset:] var comments map[string]string if title != "" || artist != "" || album != "" { comments = make(map[string]string) if title != "" { comments["TITLE"] = title } if artist != "" { comments["ARTIST"] = artist } if album != "" { comments["ALBUM"] = album } } newMeta := make([]*flac.MetaDataBlock, 0, len(f.Meta)+2) for _, block := range f.Meta { if block.Type == flac.Picture || block.Type == flac.VorbisComment { continue } newMeta = append(newMeta, block) } if comments != nil { newMeta = append(newMeta, buildVorbisComment(comments)) } if len(coverData) > 0 { newMeta = append(newMeta, buildPictureBlock(coverData)) } var buf bytes.Buffer buf.WriteString("fLaC") for i, block := range newMeta { header := byte(block.Type) if i == len(newMeta)-1 { header |= 0x80 } buf.WriteByte(header) size := make([]byte, 3) size[0] = byte(len(block.Data) >> 16) size[1] = byte(len(block.Data) >> 8) size[2] = byte(len(block.Data)) buf.Write(size) buf.Write(block.Data) } buf.Write(audioFrames) return buf.Bytes(), nil } func buildVorbisComment(comments map[string]string) *flac.MetaDataBlock { vendorLen := 0 var vendor string dataLen := 4 + vendorLen + 4 for key, val := range comments { dataLen += 4 + len(key) + 1 + len(val) } data := make([]byte, dataLen) pos := 0 pos += putU32le(data[pos:], uint32(vendorLen)) pos += copy(data[pos:], vendor) pos += putU32le(data[pos:], uint32(len(comments))) for key, val := range comments { entry := key + "=" + val pos += putU32le(data[pos:], uint32(len(entry))) pos += copy(data[pos:], entry) } return &flac.MetaDataBlock{ Type: flac.VorbisComment, Data: data, } } func buildPictureBlock(imageData []byte) *flac.MetaDataBlock { mime := imageMime(imageData) picType := uint32(3) width := uint32(0) height := uint32(0) depth := uint32(24) colors := uint32(0) dataLen := 4 + len(mime) + 4 + 4 + 4 + 4 + 4 + len(imageData) data := make([]byte, dataLen) pos := 0 pos += putU32be(data[pos:], picType) pos += putU32be(data[pos:], uint32(len(mime))) pos += copy(data[pos:], mime) pos += putU32be(data[pos:], uint32(len(""))) pos += copy(data[pos:], "") pos += putU32be(data[pos:], width) pos += putU32be(data[pos:], height) pos += putU32be(data[pos:], depth) pos += putU32be(data[pos:], colors) pos += putU32be(data[pos:], uint32(len(imageData))) copy(data[pos:], imageData) return &flac.MetaDataBlock{ Type: flac.Picture, Data: data, } } func putU32le(b []byte, v uint32) int { binary.LittleEndian.PutUint32(b, v) return 4 } func putU32be(b []byte, v uint32) int { binary.BigEndian.PutUint32(b, v) return 4 }