Added working ALAC encoder into MP4

This commit is contained in:
DataHoarder 2022-04-20 22:32:15 +02:00
commit 97e71357be
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
11 changed files with 322 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/.idea
*.raw
*.alac
*.m4a

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
Copyright (c) 2022 go-alac Contributors All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

14
README.md Normal file
View file

@ -0,0 +1,14 @@
# go-alac
[libalac](https://github.com/mikebrady/alac) CGO bindings for encoding/decoding.
## Requirements
### [libalac](https://github.com/mikebrady/alac)
```shell
git clone --depth 1 https://github.com/mikebrady/alac.git
cd alac
autoreconf -fi
./configure --prefix /usr
make -j$(nproc)
sudo make install
```

62
encoder.go Normal file
View file

@ -0,0 +1,62 @@
package go_alac
/*
#cgo pkg-config: alac
#include <alac/libalac.h>
*/
import "C"
import (
"runtime"
"unsafe"
)
type FrameEncoder struct {
handle C.alac_encoder
}
func NewFrameEncoder(sampleRate, channels, bitDepth int, fastMode bool) *FrameEncoder {
fastInt := 0
if fastMode {
fastInt = 1
}
handle := C.alac_encoder_new(C.int(sampleRate), C.int(channels), C.int(bitDepth), C.int(fastInt))
if handle.ptr == nil {
return nil
}
e := &FrameEncoder{
handle: handle,
}
runtime.SetFinalizer(e, finalize)
return e
}
func (e *FrameEncoder) GetMagicCookie() []byte {
output := make([]byte, int(e.handle.magic_cookie_size))
outBytes := C.alac_encoder_get_magic_cookie(&e.handle, (*C.uchar)(unsafe.Pointer(&output[0])))
return output[:outBytes]
}
func (e *FrameEncoder) GetInputSize() int {
return int(e.handle.input_packet_size)
}
func (e *FrameEncoder) WritePacket(pcm []byte) []byte {
output := make([]byte, int(e.handle.output_max_packet_size))
outBytes := C.alac_encoder_write(&e.handle, (*C.uchar)(unsafe.Pointer(&pcm[0])), C.int(len(pcm)), (*C.uchar)(unsafe.Pointer(&output[0])))
if outBytes < 0 {
return nil
}
return output[:outBytes]
}
func finalize(e *FrameEncoder) {
e.Close()
}
func (e *FrameEncoder) Close() {
C.alac_encoder_delete(&e.handle)
}

47
encoder_test.go Normal file
View file

@ -0,0 +1,47 @@
package go_alac
import (
"io/ioutil"
"os"
"testing"
)
func TestEncode(t *testing.T) {
t.Parallel()
f, err := os.Open("sample/test_s16.raw")
if err != nil {
t.Error(err)
return
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
t.Error(err)
return
}
o, err := os.Create("sample/test_s16_output.alac")
if err != nil {
t.Error(err)
return
}
defer o.Close()
encoder := NewFrameEncoder(44100, 2, 16, false)
packetSize := encoder.GetInputSize()
for len(data) > packetSize {
resultPacket := encoder.WritePacket(data[:packetSize])
o.Write(resultPacket)
data = data[packetSize:]
}
if len(data) > 0 {
resultPacket := encoder.WritePacket(data)
o.Write(resultPacket)
}
o.Sync()
o.Close()
}

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module git.gammaspectra.live/S.O.N.G/go-alac
go 1.18
require github.com/edgeware/mp4ff v0.27.0

4
go.sum Normal file
View file

@ -0,0 +1,4 @@
github.com/edgeware/mp4ff v0.27.0 h1:Lv773H6c4pUt3Zj9z44TOICv6fjd6gKzi1sVl8GbcYE=
github.com/edgeware/mp4ff v0.27.0/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc=
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=

126
mp4.go Normal file
View file

@ -0,0 +1,126 @@
package go_alac
import (
"github.com/edgeware/mp4ff/bits"
"github.com/edgeware/mp4ff/mp4"
"io"
"time"
)
type FormatEncoder struct {
encoder *FrameEncoder
writer io.Writer
buffer []byte
segmentDuration int64
trackId uint32
seqNumber uint32
}
type alacBox struct {
cookie []byte
}
func (b *alacBox) Type() string {
return "alac"
}
// Size - calculated size of box
func (b *alacBox) Size() uint64 {
return uint64(12 + len(b.cookie))
}
// Encode - write box to w
func (b *alacBox) Encode(w io.Writer) error {
sw := bits.NewFixedSliceWriter(int(b.Size()))
err := b.EncodeSW(sw)
if err != nil {
return err
}
_, err = w.Write(sw.Bytes())
return err
}
func (b *alacBox) EncodeSW(sw bits.SliceWriter) error {
err := mp4.EncodeHeaderSW(b, sw)
if err != nil {
return err
}
sw.WriteUint32(0) //version
sw.WriteBytes(b.cookie)
return sw.AccError()
}
func (b *alacBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string) error {
return nil
}
func NewFormatEncoder(writer io.Writer, sampleRate, channels, bitDepth int, fastMode bool, segmentDuration time.Duration) *FormatEncoder {
e := &FormatEncoder{
encoder: NewFrameEncoder(sampleRate, channels, bitDepth, fastMode),
writer: writer,
segmentDuration: segmentDuration.Milliseconds(),
}
if e.encoder == nil {
return nil
}
init := mp4.CreateEmptyInit()
init.AddEmptyTrack(uint32(sampleRate), "audio", "en")
e.trackId = init.Moov.Mvhd.NextTrackID - 1
trak := init.Moov.Trak
stsd := trak.Mdia.Minf.Stbl.Stsd
//TODO: this does not work with 96kHz freq etc.
mp4a := mp4.CreateAudioSampleEntryBox("alac", uint16(channels), uint16(bitDepth), uint16(sampleRate), &alacBox{
cookie: e.encoder.GetMagicCookie(),
})
stsd.AddChild(mp4a)
init.Encode(writer)
return e
}
func (e *FormatEncoder) outputPacket(packet []byte) {
//TODO: more frames
seg := mp4.NewMediaSegment()
frag, _ := mp4.CreateFragment(e.seqNumber, e.trackId)
seg.AddFragment(frag)
frag.AddFullSampleToTrack(mp4.FullSample{
Sample: mp4.Sample{
Dur: 4096, //TODO
Size: uint32(len(packet)),
},
DecodeTime: uint64(4096 * e.seqNumber),
Data: packet,
}, e.trackId)
seg.Encode(e.writer)
e.seqNumber++
}
func (e *FormatEncoder) Write(pcm []byte) {
if e.encoder == nil {
return
}
e.buffer = append(e.buffer, pcm...)
inputSize := e.encoder.GetInputSize()
for len(e.buffer) >= inputSize {
e.outputPacket(e.encoder.WritePacket(e.buffer[:inputSize]))
e.buffer = e.buffer[inputSize:]
}
}
func (e *FormatEncoder) Flush() {
if e.encoder == nil {
return
}
if len(e.buffer) > 0 {
e.outputPacket(e.encoder.WritePacket(e.buffer))
e.buffer = nil
}
//TODO: flush end
}

47
mp4_test.go Normal file
View file

@ -0,0 +1,47 @@
package go_alac
import (
"io/ioutil"
"os"
"testing"
"time"
)
func TestEncodeMP4(t *testing.T) {
t.Parallel()
f, err := os.Open("sample/test_s16.raw")
if err != nil {
t.Error(err)
return
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
t.Error(err)
return
}
o, err := os.Create("sample/test_s16_output.m4a")
if err != nil {
t.Error(err)
return
}
defer o.Close()
encoder := NewFormatEncoder(o, 44100, 2, 16, false, time.Second)
iterationSize := 65536
for len(data) > iterationSize {
encoder.Write(data[:iterationSize])
data = data[iterationSize:]
}
if len(data) > 0 {
encoder.Write(data)
}
o.Sync()
o.Close()
}

Binary file not shown.

4
sample/README.md Normal file
View file

@ -0,0 +1,4 @@
`$ ffmpeg -i MooveKa_-_Rockabilly_Punk_Rock.mp3 -map 0:a:0 -ar 44100 -ac 2 -c:a pcm_f32le -f f32le test_f32.raw`
`$ ffmpeg -i MooveKa_-_Rockabilly_Punk_Rock.mp3 -map 0:a:0 -ar 44100 -ac 2 -c:a pcm_s16le -f s16le test_s16.raw`