2022-11-19 18:18:50 +00:00
|
|
|
package xiami
|
2020-12-25 20:38:23 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
2022-11-18 23:25:43 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2022-11-18 23:25:40 +00:00
|
|
|
|
2022-11-18 23:44:44 +00:00
|
|
|
"unlock-music.dev/cli/algo/common"
|
2020-12-25 20:38:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2020-12-25 20:54:59 +00:00
|
|
|
magicHeader = []byte{'i', 'f', 'm', 't'}
|
|
|
|
magicHeader2 = []byte{0xfe, 0xfe, 0xfe, 0xfe}
|
|
|
|
typeMapping = map[string]string{
|
2020-12-25 20:38:23 +00:00
|
|
|
" WAV": "wav",
|
|
|
|
"FLAC": "flac",
|
|
|
|
" MP3": "mp3",
|
|
|
|
" A4M": "m4a",
|
|
|
|
}
|
2020-12-25 20:54:59 +00:00
|
|
|
ErrMagicHeader = errors.New("xm magic header not matched")
|
|
|
|
)
|
2020-12-25 20:38:23 +00:00
|
|
|
|
|
|
|
type Decoder struct {
|
2022-11-19 18:47:28 +00:00
|
|
|
rd io.ReadSeeker // rd is the original file reader
|
2022-11-18 23:25:43 +00:00
|
|
|
offset int
|
|
|
|
|
|
|
|
cipher common.StreamDecoder
|
2020-12-25 20:38:23 +00:00
|
|
|
outputExt string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Decoder) GetAudioExt() string {
|
2021-05-16 04:15:22 +00:00
|
|
|
if d.outputExt != "" {
|
|
|
|
return "." + d.outputExt
|
|
|
|
|
|
|
|
}
|
|
|
|
return ""
|
2020-12-25 20:38:23 +00:00
|
|
|
}
|
|
|
|
|
2022-12-04 15:05:38 +00:00
|
|
|
func NewDecoder(p *common.DecoderParams) common.Decoder {
|
|
|
|
return &Decoder{rd: p.Reader}
|
2020-12-25 20:38:23 +00:00
|
|
|
}
|
|
|
|
|
2022-11-19 18:47:28 +00:00
|
|
|
// Validate checks if the file is a valid xiami .xm file.
|
|
|
|
// rd will set to the beginning of the encrypted audio data.
|
2020-12-25 20:38:23 +00:00
|
|
|
func (d *Decoder) Validate() error {
|
2022-11-18 23:25:43 +00:00
|
|
|
header := make([]byte, 16) // xm header is fixed to 16 bytes
|
|
|
|
|
|
|
|
if _, err := io.ReadFull(d.rd, header); err != nil {
|
|
|
|
return fmt.Errorf("xm read header: %w", err)
|
2020-12-25 20:38:23 +00:00
|
|
|
}
|
2022-11-18 23:25:43 +00:00
|
|
|
|
|
|
|
// 0x00 - 0x03 and 0x08 - 0x0B: magic header
|
|
|
|
if !bytes.Equal(magicHeader, header[:4]) || !bytes.Equal(magicHeader2, header[8:12]) {
|
2020-12-25 20:54:59 +00:00
|
|
|
return ErrMagicHeader
|
2020-12-25 20:38:23 +00:00
|
|
|
}
|
|
|
|
|
2022-11-18 23:25:43 +00:00
|
|
|
// 0x04 - 0x07: Audio File Type
|
2020-12-25 20:38:23 +00:00
|
|
|
var ok bool
|
2022-11-18 23:25:43 +00:00
|
|
|
d.outputExt, ok = typeMapping[string(header[4:8])]
|
2020-12-25 20:38:23 +00:00
|
|
|
if !ok {
|
2022-11-18 23:25:43 +00:00
|
|
|
return fmt.Errorf("xm detect unknown audio type: %s", string(header[4:8]))
|
2020-12-25 20:38:23 +00:00
|
|
|
}
|
|
|
|
|
2022-11-18 23:25:43 +00:00
|
|
|
// 0x0C - 0x0E, Encrypt Start At, LittleEndian Unit24
|
|
|
|
encStartAt := uint32(header[12]) | uint32(header[13])<<8 | uint32(header[14])<<16
|
|
|
|
|
|
|
|
// 0x0F, XOR Mask
|
|
|
|
d.cipher = newXmCipher(header[15], int(encStartAt))
|
2020-12-25 20:38:23 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2020-12-26 08:07:48 +00:00
|
|
|
|
2022-11-18 23:25:43 +00:00
|
|
|
func (d *Decoder) Read(p []byte) (int, error) {
|
|
|
|
n, err := d.rd.Read(p)
|
|
|
|
if n > 0 {
|
|
|
|
d.cipher.Decrypt(p[:n], d.offset)
|
|
|
|
d.offset += n
|
2020-12-26 08:07:48 +00:00
|
|
|
}
|
2022-11-18 23:25:43 +00:00
|
|
|
return n, err
|
2020-12-26 08:07:48 +00:00
|
|
|
}
|
|
|
|
|
2020-12-26 07:47:10 +00:00
|
|
|
func init() {
|
|
|
|
// Xiami Wav/M4a/Mp3/Flac
|
2021-11-11 15:43:20 +00:00
|
|
|
common.RegisterDecoder("xm", false, NewDecoder)
|
2020-12-26 07:47:10 +00:00
|
|
|
// Xiami Typed Format
|
2022-11-18 23:25:43 +00:00
|
|
|
common.RegisterDecoder("wav", false, NewDecoder)
|
|
|
|
common.RegisterDecoder("mp3", false, NewDecoder)
|
|
|
|
common.RegisterDecoder("flac", false, NewDecoder)
|
|
|
|
common.RegisterDecoder("m4a", false, NewDecoder)
|
2020-12-26 07:47:10 +00:00
|
|
|
}
|