Added working ALAC encoder into MP4
This commit is contained in:
commit
97e71357be
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/.idea
|
||||
*.raw
|
||||
*.alac
|
||||
*.m4a
|
9
LICENSE
Normal file
9
LICENSE
Normal 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
14
README.md
Normal 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
62
encoder.go
Normal 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
47
encoder_test.go
Normal 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
5
go.mod
Normal 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
4
go.sum
Normal 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
126
mp4.go
Normal 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
47
mp4_test.go
Normal 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()
|
||||
}
|
BIN
sample/MooveKa_-_Rockabilly_Punk_Rock.mp3
Normal file
BIN
sample/MooveKa_-_Rockabilly_Punk_Rock.mp3
Normal file
Binary file not shown.
4
sample/README.md
Normal file
4
sample/README.md
Normal 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`
|
Loading…
Reference in a new issue