Added MP4 encoding/decoding to AAC, ALAC, change README and format description
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
DataHoarder 2022-04-23 20:41:14 +02:00
parent fe503892ba
commit 3a37b7cf3e
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
20 changed files with 717 additions and 110 deletions

View file

@ -8,6 +8,7 @@ steps:
image: alpine/git
commands:
- apk update && apk add --no-cache git-lfs && git lfs install
- git lfs pull
- git submodule update --init --recursive
- name: build-repo
image: golang:1.18-bullseye
@ -30,6 +31,7 @@ steps:
image: alpine/git
commands:
- apk update && apk add --no-cache git-lfs && git lfs install
- git lfs pull
- git submodule update --init --recursive
- name: build-repo
image: golang:1.18-bullseye

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.m4a filter=lfs diff=lfs merge=lfs -text

View file

@ -13,15 +13,15 @@ Collection of audio utilities for decoding/encoding files and streams.
## Codecs supported
| Codec | Containers | Decoder | Analyzer | Encoder | Notes |
|:----------:|:----------------------------------------------------------------------------------------:|:-------:|:--------:|:-------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **FLAC** | [FLAC](https://xiph.org/flac/format.html), [Ogg](https://xiph.org/flac/ogg_mapping.html) | ✅ | ✅ | ✅ | Adjustable encoding compression level and block size.<br/>Decoding/encoding by [libFLAC](https://github.com/xiph/flac) via [goflac](https://git.gammaspectra.live/S.O.N.G/goflac). |
| **TTA** | [TTA](https://www.tausoft.org/en/true_audio_codec_format/) | ✅ | ✅ | ✅ | Decoding/encoding via [go-tta](https://git.gammaspectra.live/S.O.N.G/go-tta). |
| **MP3** | [MP3](http://mpgedit.org/mpgedit/mpeg_format/MP3Format.html) | ✅ | - | ✅ | Adjustable encoding bitrate and mode.<br/>Decoding via [minimp3](https://github.com/kvark128/minimp3), encoding by [LAME](https://lame.sourceforge.io/) via [go-lame](https://github.com/viert/go-lame). |
| **Opus** | [Ogg](https://www.xiph.org/ogg/doc/framing.html) | ✅ | - | ✅ | Adjustable encoding bitrate.<br/>Decoding/encoding by [libopus](https://github.com/xiph/opus) via [go-pus](https://git.gammaspectra.live/S.O.N.G/go-pus). |
| **Vorbis** | [Ogg](https://www.xiph.org/ogg/doc/framing.html) | ✅ | - | ❌ | Decoding by [jfreymuth/vorbis](https://github.com/jfreymuth/vorbis) via [jfreymuth/oggvorbis](https://github.com/jfreymuth/oggvorbis). |
| **AAC** | [ADTS](https://wiki.multimedia.cx/index.php/ADTS) | ✅ | - | ✅ | Adjustable encoding bitrate and mode (LC, HEv2).<br/>Decoding/encoding by [FDK-AAC](https://github.com/mstorsjo/fdk-aac) via [go-fdkaac](https://git.gammaspectra.live/S.O.N.G/go-fdkaac). |
| **ALAC** | MP4* | ✅ | ✅ | ✅ | Decoding/encoding by [libalac](https://git.gammaspectra.live/S.O.N.G/alac) via [go-alac](https://git.gammaspectra.live/S.O.N.G/go-alac).<br/>*MP4 Decoding/encoding only supported on fragmented MP4 currently. |
| Codec | Containers | Decoder | Analyzer | Encoder | Notes |
|:----------:|:----------------------------------------------------------------------------------------:|:-------:|:--------:|:-------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **FLAC** | [FLAC](https://xiph.org/flac/format.html), [Ogg](https://xiph.org/flac/ogg_mapping.html) | ✅ | ✅ | ✅ | Adjustable encoding compression level and block size.<br/>Decoding/encoding by [libFLAC](https://github.com/xiph/flac) via [goflac](https://git.gammaspectra.live/S.O.N.G/goflac). |
| **TTA** | [TTA](https://www.tausoft.org/en/true_audio_codec_format/) | ✅ | ✅ | ✅ | Decoding/encoding via [go-tta](https://git.gammaspectra.live/S.O.N.G/go-tta). |
| **MP3** | [MP3](http://mpgedit.org/mpgedit/mpeg_format/MP3Format.html) | ✅ | - | ✅ | Adjustable encoding bitrate and mode.<br/>Decoding via [minimp3](https://github.com/kvark128/minimp3), encoding by [LAME](https://lame.sourceforge.io/) via [go-lame](https://github.com/viert/go-lame). |
| **Opus** | [Ogg](https://www.xiph.org/ogg/doc/framing.html) | ✅ | - | ✅ | Adjustable encoding bitrate.<br/>Decoding/encoding by [libopus](https://github.com/xiph/opus) via [go-pus](https://git.gammaspectra.live/S.O.N.G/go-pus). |
| **Vorbis** | [Ogg](https://www.xiph.org/ogg/doc/framing.html) | ✅ | - | ❌ | Decoding by [jfreymuth/vorbis](https://github.com/jfreymuth/vorbis) via [jfreymuth/oggvorbis](https://github.com/jfreymuth/oggvorbis). |
| **AAC** | [ADTS](https://wiki.multimedia.cx/index.php/ADTS), ADIF*, MP4** | ✅ | - | ✅ | Adjustable encoding bitrate and mode (LC, HEv2).<br/>Decoding/encoding by [FDK-AAC](https://github.com/mstorsjo/fdk-aac) via [go-fdkaac](https://git.gammaspectra.live/S.O.N.G/go-fdkaac).<br/>If [go-fdkaac](https://git.gammaspectra.live/S.O.N.G/go-fdkaac) codec is disabled, [VisualOn AAC encoder](https://github.com/gen2brain/aac-go) will be used for limited encoding support.<br/>*ADIF only supported on encoding.<br/>**MP4 encoding only supported on fragmented MP4 currently. |
| **ALAC** | MP4* | ✅ | ✅ | ✅ | Decoding/encoding by [libalac](https://git.gammaspectra.live/S.O.N.G/alac) via [go-alac](https://git.gammaspectra.live/S.O.N.G/go-alac).<br/>Disabled by default.<br/>*MP4 encoding only supported on fragmented MP4 currently. |
## Container packetizers supported
@ -33,6 +33,7 @@ Collection of audio utilities for decoding/encoding files and streams.
| **Ogg** | ✅ | ✅ | ✅* | *Sample numbers (absolute granule position in Ogg) depend on underlying codec implementing it.<br/>Has been tested as working for Opus |
| **ADTS** | ✅ | ✅ | ✅ | Uses [edgeware/mp4ff](https://github.com/edgeware/mp4ff) for its ADTS frame parser. |
| **MP4** | ❌ | - | - | |
| **ADIF** | ❌ | - | - | |
## Dependencies
### Go >= 1.18

View file

@ -4,12 +4,16 @@
package aac
import (
"bytes"
"errors"
"fmt"
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
"git.gammaspectra.live/S.O.N.G/Kirika/cgo"
"git.gammaspectra.live/S.O.N.G/go-fdkaac/fdkaac"
aac_adts "github.com/edgeware/mp4ff/aac"
"github.com/edgeware/mp4ff/mp4"
"io"
"time"
"unsafe"
)
@ -25,18 +29,16 @@ func (f Format) Name() string {
}
func (f Format) Description() string {
return "libfdk-aac (go-fdkaac)"
return "libfdk-aac (S.O.N.G/go-fdkaac)"
}
func decodeFrame(decoder *fdkaac.AacDecoder, r io.Reader) ([]float32, error) {
pcm, err := decoder.Decode()
pcm, err := tryDecodeFrame(decoder)
if err != nil {
return nil, err
}
if pcm != nil {
return cgo.Int32ToFloat32(cgo.BytesToInt32(pcm, 16), 16), nil
return pcm, err
}
header, _, err := aac_adts.DecodeADTSHeader(r)
@ -51,8 +53,11 @@ func decodeFrame(decoder *fdkaac.AacDecoder, r io.Reader) ([]float32, error) {
return nil, err
}
_, err = decoder.Fill(append(header.Encode(), data...))
var n int
fullData := append(header.Encode(), data...)
if n, err = decoder.Fill(fullData); n != 0 {
return nil, errors.New("buffer under read")
}
if err != nil {
return nil, err
}
@ -60,54 +65,146 @@ func decodeFrame(decoder *fdkaac.AacDecoder, r io.Reader) ([]float32, error) {
return decodeFrame(decoder, r)
}
func tryDecodeFrame(decoder *fdkaac.AacDecoder) ([]float32, error) {
pcm, err := decoder.Decode()
if err != nil {
return nil, err
}
if pcm != nil {
return cgo.Int32ToFloat32(cgo.BytesToInt32(pcm, 16), 16), nil
}
return nil, nil
}
func decodeFrameMP4(decoder *fdkaac.AacDecoder, demuxer *mp4Decoder) (result []float32, err error) {
pcm, err := tryDecodeFrame(decoder)
if err != nil {
return nil, err
}
if pcm != nil {
return pcm, err
}
samples := demuxer.Read()
if samples == nil {
return nil, io.EOF
}
var n int
for _, sample := range samples {
if n, err = decoder.Fill(sample); n != 0 {
return nil, errors.New("buffer under read")
}
if err != nil {
return nil, err
}
pcm, err = tryDecodeFrame(decoder)
if pcm != nil {
result = append(result, pcm...)
}
}
return result, nil
}
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
decoder := fdkaac.NewAacDecoder()
err := decoder.InitAdts()
if err != nil {
return audio.Source{}, err
}
buf, err := decodeFrame(decoder, r)
if err != nil {
decoder.Close()
return audio.Source{}, err
}
newChannel := make(chan []float32)
go func() {
defer close(newChannel)
defer decoder.Close()
if len(buf) > 0 {
newChannel <- buf
mp4Demuxer, err := tryDecodeMP4(r)
if err != nil { //try ADTS
r.Seek(0, io.SeekStart)
err = decoder.InitAdts()
if err != nil {
return audio.Source{}, err
}
for {
buf, err = decodeFrame(decoder, r)
buf, err := decodeFrame(decoder, r)
if err != nil {
return
}
if err != nil {
decoder.Close()
return audio.Source{}, err
}
newChannel := make(chan []float32)
go func() {
defer close(newChannel)
defer decoder.Close()
if len(buf) > 0 {
newChannel <- buf
}
}
}()
return audio.Source{
Channels: decoder.NumChannels(),
SampleRate: decoder.SampleRate(),
Blocks: newChannel,
}, nil
for {
buf, err = decodeFrame(decoder, r)
if err != nil {
return
}
if len(buf) > 0 {
newChannel <- buf
}
}
}()
return audio.Source{
Channels: decoder.NumChannels(),
SampleRate: decoder.SampleRate(),
Blocks: newChannel,
}, nil
} else {
return audio.Source{}, fmt.Errorf("unsupported format mp4")
err = decoder.InitRaw(mp4Demuxer.cookie)
if err != nil {
return audio.Source{}, err
}
buf, err := decodeFrameMP4(decoder, mp4Demuxer)
if err != nil {
decoder.Close()
return audio.Source{}, err
}
newChannel := make(chan []float32)
go func() {
defer close(newChannel)
defer decoder.Close()
if len(buf) > 0 {
newChannel <- buf
}
for {
buf, err = decodeFrameMP4(decoder, mp4Demuxer)
if err != nil {
return
}
if len(buf) > 0 {
newChannel <- buf
}
}
}()
return audio.Source{
Channels: decoder.NumChannels(),
SampleRate: decoder.SampleRate(),
Blocks: newChannel,
}, nil
}
}
func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[string]interface{}) error {
var bitrate = 128
var isHEv2 bool
var format = "adts"
if options != nil {
var val interface{}
@ -146,16 +243,33 @@ func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[s
}
}
}
if val, ok = options["format"]; ok {
if strVal, ok = val.(string); ok {
format = strVal
}
}
}
muxingMode := fdkaac.MuxingModeADTS
if format == "adts" {
muxingMode = fdkaac.MuxingModeADTS
} else if format == "mp4" {
muxingMode = fdkaac.MuxingModeRAW
} else if format == "adif" {
muxingMode = fdkaac.MuxingModeADIF
} else {
return fmt.Errorf("unsupported format %s", format)
}
encoder := fdkaac.NewAacEncoder()
if isHEv2 {
err := encoder.InitHEv2(source.Channels, source.SampleRate, bitrate*1024, fdkaac.MuxingModeADTS)
err := encoder.InitHEv2(source.Channels, source.SampleRate, bitrate*1024, muxingMode)
if err != nil {
return err
}
} else {
err := encoder.InitLc(source.Channels, source.SampleRate, bitrate*1024, fdkaac.MuxingModeADTS)
err := encoder.InitLc(source.Channels, source.SampleRate, bitrate*1024, muxingMode)
if err != nil {
return err
}
@ -164,14 +278,172 @@ func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[s
frameSize := encoder.FrameSize() * encoder.Channels()
var buffer []int16
for block := range source.Blocks {
if format == "mp4" {
init := mp4.CreateEmptyInit()
init.AddEmptyTrack(uint32(source.SampleRate), "audio", "en")
trackId := init.Moov.Mvhd.NextTrackID - 1
trak := init.Moov.Trak
buffer = append(buffer, cgo.Float32ToInt16(block)...)
objType := aac_adts.AAClc
if isHEv2 {
objType = aac_adts.HEAACv2
}
for len(buffer) >= frameSize {
sl := buffer[:frameSize]
buf, err := encoder.Encode(unsafe.Slice((*byte)(unsafe.Pointer(&sl[0])), len(sl)*2))
{
stsd := trak.Mdia.Minf.Stbl.Stsd
asc := &aac_adts.AudioSpecificConfig{
ObjectType: byte(objType),
ChannelConfiguration: byte(source.Channels),
SamplingFrequency: source.SampleRate,
ExtensionFrequency: 0,
SBRPresentFlag: false,
PSPresentFlag: false,
}
switch objType {
case aac_adts.HEAACv1:
asc.ExtensionFrequency = 2 * source.SampleRate
asc.SBRPresentFlag = true
case aac_adts.HEAACv2:
asc.ExtensionFrequency = 2 * source.SampleRate
asc.SBRPresentFlag = true
asc.ChannelConfiguration = 1
asc.PSPresentFlag = true
}
buf := &bytes.Buffer{}
err := asc.Encode(buf)
if err != nil {
return err
}
ascBytes := buf.Bytes()
esds := mp4.CreateEsdsBox(ascBytes)
mp4a := mp4.CreateAudioSampleEntryBox("mp4a",
uint16(asc.ChannelConfiguration),
16, uint16(source.SampleRate), esds)
stsd.AddChild(mp4a)
}
init.Encode(writer)
var seqNumber uint32
var packetsWritten uint64
var outputBuffer [][]byte
segmentDuration := time.Millisecond * 100
outputSegment := func() {
seg := mp4.NewMediaSegment()
frag, _ := mp4.CreateFragment(seqNumber, trackId)
seg.AddFragment(frag)
for _, b := range outputBuffer {
frag.AddFullSampleToTrack(mp4.FullSample{
Sample: mp4.Sample{
Dur: uint32(frameSize),
Size: uint32(len(b)),
},
DecodeTime: uint64(frameSize) * packetsWritten,
Data: b,
}, trackId)
packetsWritten++
}
seg.Encode(writer)
seqNumber++
outputBuffer = nil
}
outputPacket := func(packet []byte) {
outputBuffer = append(outputBuffer, packet)
if time.Duration(float64(time.Second)*(float64(frameSize*len(outputBuffer))/float64(source.SampleRate))) >= segmentDuration {
outputSegment()
}
}
var buffer []int16
for block := range source.Blocks {
buffer = append(buffer, cgo.Float32ToInt16(block)...)
for len(buffer) >= frameSize {
sl := buffer[:frameSize]
buf, err := encoder.Encode(unsafe.Slice((*byte)(unsafe.Pointer(&sl[0])), len(sl)*2))
if err != nil {
return err
}
if len(buf) > 0 {
outputPacket(buf)
}
buffer = buffer[frameSize:]
}
}
if len(buffer) > 0 {
//pad
buffer = append(buffer, make([]int16, frameSize-len(buffer))...)
buf, err := encoder.Encode(unsafe.Slice((*byte)(unsafe.Pointer(&buffer[0])), len(buffer)*2))
if err != nil {
return err
}
if len(buf) > 0 {
outputPacket(buf)
}
}
//Do flush
for {
buf, err := encoder.Flush()
if err != nil {
return err
}
if len(buf) > 0 {
outputPacket(buf)
} else {
break
}
}
if len(outputBuffer) > 0 {
outputSegment()
}
} else {
var buffer []int16
for block := range source.Blocks {
buffer = append(buffer, cgo.Float32ToInt16(block)...)
for len(buffer) >= frameSize {
sl := buffer[:frameSize]
buf, err := encoder.Encode(unsafe.Slice((*byte)(unsafe.Pointer(&sl[0])), len(sl)*2))
if err != nil {
return err
}
if len(buf) > 0 {
_, err = writer.Write(buf)
if err != nil {
return err
}
}
buffer = buffer[frameSize:]
}
}
if len(buffer) > 0 {
//pad
buffer = append(buffer, make([]int16, frameSize-len(buffer))...)
buf, err := encoder.Encode(unsafe.Slice((*byte)(unsafe.Pointer(&buffer[0])), len(buffer)*2))
if err != nil {
return err
@ -183,43 +455,24 @@ func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[s
return err
}
}
buffer = buffer[frameSize:]
}
}
if len(buffer) > 0 {
//pad
buffer = append(buffer, make([]int16, frameSize-len(buffer))...)
buf, err := encoder.Encode(unsafe.Slice((*byte)(unsafe.Pointer(&buffer[0])), len(buffer)*2))
if err != nil {
return err
}
if len(buf) > 0 {
_, err = writer.Write(buf)
//Do flush
for {
buf, err := encoder.Flush()
if err != nil {
return err
}
}
}
//Do flush
for {
buf, err := encoder.Flush()
if err != nil {
return err
}
if len(buf) > 0 {
_, err = writer.Write(buf)
if err != nil {
return err
if len(buf) > 0 {
_, err = writer.Write(buf)
if err != nil {
return err
}
} else {
break
}
} else {
break
}
}

View file

@ -96,6 +96,77 @@ func TestDecodeAAC(t *testing.T) {
}
}
/*
func TestDecodeAACMP4(t *testing.T) {
t.Parallel()
fp, err := os.Open(test.TestSingleSample24)
if err != nil {
t.Error(err)
return
}
defer fp.Close()
source, err := flac.NewFormat().Open(fp)
if err != nil {
t.Error(err)
return
}
target, err := os.Open(test.TestSingleSampleAACMP4)
if err != nil {
t.Error(err)
return
}
defer target.Close()
source, err = NewFormat().Open(target)
if err != nil {
t.Error(err)
return
}
for range source.Blocks {
}
}
*/
func TestEncodeAACMP4(t *testing.T) {
t.Parallel()
fp, err := os.Open(test.TestSingleSample24)
if err != nil {
t.Error(err)
return
}
defer fp.Close()
source, err := flac.NewFormat().Open(fp)
if err != nil {
t.Error(err)
return
}
target, err := os.CreateTemp("/tmp", "encode_test_*.m4a")
if err != nil {
t.Error(err)
return
}
defer func() {
name := target.Name()
target.Close()
os.Remove(name)
}()
options := make(map[string]interface{})
options["bitrate"] = "256k"
options["format"] = "mp4"
err = NewFormat().Encode(source, target, options)
if err != nil {
t.Error(err)
return
}
}
func TestEncodeAACHE(t *testing.T) {
t.Parallel()
fp, err := os.Open(test.TestSingleSample24)

107
audio/format/aac/mp4.go Normal file
View file

@ -0,0 +1,107 @@
//go:build !disable_format_mp4
// +build !disable_format_mp4
package aac
import (
"bytes"
"github.com/edgeware/mp4ff/mp4"
"io"
)
import (
"errors"
)
type mp4Decoder struct {
mp4 *mp4.File
reader io.ReadSeekCloser
trackId uint32
cookie []byte
currentSegment int
currentFragment int
currentSample uint32
}
func tryDecodeMP4(r io.ReadSeekCloser) (*mp4Decoder, error) {
//TODO: mp4.DecModeLazyMdat errors in segmented files
parsedMp4, err := mp4.DecodeFile(r, mp4.WithDecodeMode(mp4.DecModeNormal))
if err != nil {
return nil, err
}
var trackId uint32
var magicCookie []byte
for _, trak := range parsedMp4.Moov.Traks {
if box, err := trak.Mdia.Minf.Stbl.Stsd.GetSampleDescription(0); err == nil && box.Type() == "mp4a" {
if aseb, ok := box.(*mp4.AudioSampleEntryBox); ok {
trackId = trak.Tkhd.TrackID
magicCookie = aseb.Esds.DecConfig
break
}
}
}
if magicCookie == nil {
return nil, errors.New("could not find track entry")
}
return &mp4Decoder{
reader: r,
mp4: parsedMp4,
cookie: magicCookie,
trackId: trackId,
currentSample: 1,
}, nil
}
func (d *mp4Decoder) Read() (samples [][]byte) {
if d.mp4.IsFragmented() {
if d.currentSegment >= len(d.mp4.Segments) {
//EOF
return nil
}
segment := d.mp4.Segments[d.currentSegment]
if d.currentFragment >= len(segment.Fragments) {
d.currentSegment++
d.currentFragment = 0
return d.Read()
}
frag := segment.Fragments[d.currentFragment]
fullSamples, err := frag.GetFullSamples(&mp4.TrexBox{
TrackID: d.trackId,
})
if err != nil {
return nil
}
for _, sample := range fullSamples {
samples = append(samples, sample.Data)
}
d.currentFragment++
return
} else {
if d.mp4.Mdat.Data == nil {
return nil
}
for _, trak := range d.mp4.Moov.Traks {
if trak.Tkhd.TrackID == d.trackId {
buf := new(bytes.Buffer)
if err := d.mp4.CopySampleData(buf, d.reader, trak, d.currentSample, d.currentSample); err != nil || buf.Len() == 0 {
return nil
}
d.currentSample++
return [][]byte{buf.Bytes()}
}
}
return
}
}

View file

@ -23,7 +23,7 @@ func (f Format) Name() string {
}
func (f Format) Description() string {
return "vo-aacenc (aac-go)"
return "vo-aacenc (gen2brain/aac-go)"
}
func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[string]interface{}) error {
@ -67,6 +67,13 @@ func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[s
}
}
}
if val, ok = options["format"]; ok {
if strVal, ok = val.(string); ok {
if strVal != "adts" {
return fmt.Errorf("format %s not supported", strVal)
}
}
}
}
encoder, err := aac.NewEncoder(writer, &aac.Options{

View file

@ -26,11 +26,16 @@ func (f Format) Name() string {
}
func (f Format) Description() string {
return "libalac (go-alac)"
return "libalac (S.O.N.G/go-alac)"
}
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
decoder := go_alac.NewFormatDecoder(r)
mp4Demuxer, err := tryDecodeMP4(r)
if err != nil {
return audio.Source{}, err
}
decoder := go_alac.NewFrameDecoder(mp4Demuxer.cookie)
if decoder == nil {
return audio.Source{}, errors.New("could not decode")
}
@ -41,11 +46,17 @@ func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
defer close(newChannel)
for {
pcm := decoder.Read()
if pcm == nil {
samples := mp4Demuxer.Read()
if samples == nil {
return
}
newChannel <- cgo.Int32ToFloat32(cgo.BytesToInt32(pcm, decoder.GetBitDepth()), decoder.GetBitDepth())
for _, sample := range samples {
_, pcm := decoder.ReadPacket(sample)
if pcm == nil {
return
}
newChannel <- cgo.Int32ToFloat32(cgo.BytesToInt32(pcm, decoder.GetBitDepth()), decoder.GetBitDepth())
}
}
}()
@ -57,7 +68,12 @@ func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {
}
func (f Format) OpenAnalyzer(r io.ReadSeekCloser) (audio.Source, format.AnalyzerChannel, error) {
decoder := go_alac.NewFormatDecoder(r)
mp4Demuxer, err := tryDecodeMP4(r)
if err != nil {
return audio.Source{}, nil, err
}
decoder := go_alac.NewFrameDecoder(mp4Demuxer.cookie)
if decoder == nil {
return audio.Source{}, nil, errors.New("could not decode")
}
@ -70,19 +86,25 @@ func (f Format) OpenAnalyzer(r io.ReadSeekCloser) (audio.Source, format.Analyzer
defer close(analyzerChannel)
for {
pcm := decoder.Read()
if pcm == nil {
samples := mp4Demuxer.Read()
if samples == nil {
return
}
for _, sample := range samples {
_, pcm := decoder.ReadPacket(sample)
if pcm == nil {
return
}
intSamples := cgo.BytesToInt32(pcm, decoder.GetBitDepth())
newChannel <- cgo.Int32ToFloat32(intSamples, decoder.GetBitDepth())
intSamples := cgo.BytesToInt32(pcm, decoder.GetBitDepth())
newChannel <- cgo.Int32ToFloat32(intSamples, decoder.GetBitDepth())
analyzerChannel <- &format.AnalyzerPacket{
Samples: intSamples,
Channels: decoder.GetChannels(),
BitDepth: decoder.GetBitDepth(),
SampleRate: decoder.GetSampleRate(),
analyzerChannel <- &format.AnalyzerPacket{
Samples: intSamples,
Channels: decoder.GetChannels(),
BitDepth: decoder.GetBitDepth(),
SampleRate: decoder.GetSampleRate(),
}
}
}
}()

View file

@ -45,3 +45,33 @@ func TestEncodeALAC(t *testing.T) {
return
}
}
func TestDecodeALAC(t *testing.T) {
t.Parallel()
fp, err := os.Open(test.TestSingleSample24)
if err != nil {
t.Error(err)
return
}
defer fp.Close()
source, err := flac.NewFormat().Open(fp)
if err != nil {
t.Error(err)
return
}
target, err := os.Open(test.TestSingleSampleALACMP4)
if err != nil {
t.Error(err)
return
}
source, err = NewFormat().Open(target)
if err != nil {
t.Error(err)
return
}
for range source.Blocks {
}
}

107
audio/format/alac/mp4.go Normal file
View file

@ -0,0 +1,107 @@
//go:build !disable_format_alac
// +build !disable_format_alac
package alac
import (
"bytes"
"errors"
"github.com/edgeware/mp4ff/mp4"
"io"
)
type mp4Decoder struct {
mp4 *mp4.File
reader io.ReadSeekCloser
trackId uint32
cookie []byte
currentSegment int
currentFragment int
currentSample uint32
}
func tryDecodeMP4(r io.ReadSeekCloser) (*mp4Decoder, error) {
//TODO: mp4.DecModeLazyMdat errors in segmented files
parsedMp4, err := mp4.DecodeFile(r, mp4.WithDecodeMode(mp4.DecModeNormal))
if err != nil {
return nil, err
}
var trackId uint32
var magicCookie []byte
for _, trak := range parsedMp4.Moov.Traks {
if box, err := trak.Mdia.Minf.Stbl.Stsd.GetSampleDescription(0); err == nil && box.Type() == "alac" {
trackId = trak.Tkhd.TrackID
buf := new(bytes.Buffer)
box.Encode(buf)
boxBytes := buf.Bytes()
boxOffset := 36 + 12
magicCookie = boxBytes[boxOffset:]
break
}
}
if magicCookie == nil {
return nil, errors.New("could not find track entry")
}
return &mp4Decoder{
reader: r,
mp4: parsedMp4,
cookie: magicCookie,
trackId: trackId,
currentSample: 1,
}, nil
}
func (d *mp4Decoder) Read() (samples [][]byte) {
if d.mp4.IsFragmented() {
if d.currentSegment >= len(d.mp4.Segments) {
//EOF
return nil
}
segment := d.mp4.Segments[d.currentSegment]
if d.currentFragment >= len(segment.Fragments) {
d.currentSegment++
d.currentFragment = 0
return d.Read()
}
frag := segment.Fragments[d.currentFragment]
fullSamples, err := frag.GetFullSamples(&mp4.TrexBox{
TrackID: d.trackId,
})
if err != nil {
return nil
}
for _, sample := range fullSamples {
samples = append(samples, sample.Data)
}
d.currentFragment++
return
} else {
for _, trak := range d.mp4.Moov.Traks {
if trak.Tkhd.TrackID == d.trackId {
buf := new(bytes.Buffer)
if err := d.mp4.CopySampleData(buf, d.reader, trak, d.currentSample, d.currentSample); err != nil {
return nil
}
d.currentSample++
return [][]byte{buf.Bytes()}
}
}
return
}
}

View file

@ -24,7 +24,7 @@ func (f Format) Name() string {
}
func (f Format) Description() string {
return "libFLAC (goflac)"
return "libFLAC (S.O.N.G/goflac)"
}
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {

View file

@ -14,7 +14,7 @@ import (
)
func (f Format) Description() string {
return "minimp3 / LAME (go-lame)"
return "kvark128/minimp3, LAME (viert/go-lame)"
}
func (f Format) Encode(source audio.Source, writer io.WriteCloser, options map[string]interface{}) error {

View file

@ -4,5 +4,5 @@
package mp3
func (f Format) Description() string {
return "minimp3 / LAME"
return "kvark128/minimp3"
}

View file

@ -27,7 +27,7 @@ func (f Format) Name() string {
}
func (f Format) Description() string {
return "libopus (go-pus)"
return "libopus (S.O.N.G/go-pus)"
}
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {

View file

@ -25,7 +25,7 @@ func (f Format) Name() string {
}
func (f Format) Description() string {
return "go-tta"
return "S.O.N.G/go-tta"
}
func NewFormat() Format {

View file

@ -22,7 +22,7 @@ func (f Format) Name() string {
}
func (f Format) Description() string {
return "oggvorvis"
return "jfreymuth/oggvorvis"
}
func (f Format) Open(r io.ReadSeekCloser) (audio.Source, error) {

2
go.sum
View file

@ -1,5 +1,3 @@
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1 h1:D1VyacBGUBfvFD4Fq2eO6RQ5eZPUdC2YsIv9gXun9aQ=
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1/go.mod h1:f1+h7KOnuM9zcEQp7ri4UaVvgX4m1NFFIXgReIyjGMA=
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f h1:CxN7zlk5FdAieyRKQSbwBGBsvQ2cDF8JVCODZpzcRkA=
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f/go.mod h1:f1+h7KOnuM9zcEQp7ri4UaVvgX4m1NFFIXgReIyjGMA=
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255 h1:BWRx2ZFyhp5+rsXhdDZtk5Gld+L44lxlN9ASqB9Oj0M=

BIN
resources/aac.m4a (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/alac.m4a (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -25,3 +25,5 @@ const TestSingleSample24 = "resources/samples/cYsmix - Haunted House/11. The Gre
const TestSingleSample16 = "resources/samples/Babbe Music - RADIANT DANCEFLOOR/01. ENTER.flac"
const TestSingleSample16TTA = "resources/samples/Babbe Music - RADIANT DANCEFLOOR/01. ENTER.tta"
const TestSingleSampleAACMP4 = "resources/aac.m4a"
const TestSingleSampleALACMP4 = "resources/alac.m4a"