Initial commit
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details

This commit is contained in:
DataHoarder 2022-03-08 11:24:29 +01:00
commit ffc70c6395
Signed by: DataHoarder
SSH Key Fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
9 changed files with 351 additions and 0 deletions

18
.drone.yml Normal file
View File

@ -0,0 +1,18 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: test
image: golang:1.18rc1-bullseye
commands:
- DEBIAN_FRONTEND=noninteractive apt update
- DEBIAN_FRONTEND=noninteractive apt install -y libebur128-dev ffmpeg
- cd sample
- 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
- cd ..
- go test -cover -v
...

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.idea
*.raw

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
Copyright (c) 2022 go-ebur128 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-ebur128
libebur128 CGO bindings. Only a few calls are implemented, as needed.
libebur128 is a library that implements the EBU R 128 standard for loudness normalisation.
## Requirements
### [libebur128](https://github.com/jiixyj/libebur128)
```shell
sudo apt install libebur128-dev
```
Alternatively you can install the [ebur128 rust port](https://github.com/sdroege/ebur128#c-api) C API.

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.gammaspectra.live/S.O.N.G/go-ebur128
go 1.18

163
libebur128.go Normal file
View File

@ -0,0 +1,163 @@
package libebur128
/*
#cgo pkg-config: libebur128
#include <ebur128.h>
*/
import "C"
import (
"fmt"
)
type Ebur128Mode int
const (
LoudnessMomentary Ebur128Mode = C.EBUR128_MODE_M
LoudnessShortTerm Ebur128Mode = C.EBUR128_MODE_S
LoudnessGlobalMomentary Ebur128Mode = C.EBUR128_MODE_I
LoudnessRange Ebur128Mode = C.EBUR128_MODE_LRA
SamplePeak Ebur128Mode = C.EBUR128_MODE_SAMPLE_PEAK
TruePeak Ebur128Mode = C.EBUR128_MODE_TRUE_PEAK
Histogram Ebur128Mode = C.EBUR128_MODE_HISTOGRAM
)
func GetVersion() (major, minor, patch int) {
var i, j, k C.int
C.ebur128_get_version(&i, &j, &k)
return int(i), int(j), int(k)
}
type State struct {
p *C.ebur128_state
channels int
}
func NewState(channels, sampleRate int, mode Ebur128Mode) *State {
p := C.ebur128_init(C.uint(channels), C.ulong(sampleRate), C.int(mode))
if p == nil {
return nil
}
return &State{
p: p,
channels: channels,
}
}
//GetSamplePeak Get maximum sample peak from all frames that have been processed.
func (s *State) GetSamplePeak() (out []float64, err error) {
var j C.double
for i := 0; i < s.channels; i++ {
ret := C.ebur128_sample_peak(s.p, C.uint(i), &j)
if ret != C.EBUR128_SUCCESS {
return nil, fmt.Errorf("error calculating peak: %d", ret)
}
out = append(out, float64(j))
}
return
}
//GetPreviousSamplePeak Get maximum sample peak from the last call to Add*()
func (s *State) GetPreviousSamplePeak() (out []float64, err error) {
var j C.double
for i := 0; i < s.channels; i++ {
ret := C.ebur128_prev_sample_peak(s.p, C.uint(i), &j)
if ret != C.EBUR128_SUCCESS {
return nil, fmt.Errorf("error calculating peak: %d", ret)
}
out = append(out, float64(j))
}
return
}
//GetTruePeak Get maximum true peak from all frames that have been processed.
func (s *State) GetTruePeak() (out []float64, err error) {
var j C.double
for i := 0; i < s.channels; i++ {
ret := C.ebur128_true_peak(s.p, C.uint(i), &j)
if ret != C.EBUR128_SUCCESS {
return nil, fmt.Errorf("error calculating peak: %d", ret)
}
out = append(out, float64(j))
}
return
}
//GetPreviousTruePeak Get maximum true peak from the last call to Add*()
func (s *State) GetPreviousTruePeak() (out []float64, err error) {
var j C.double
for i := 0; i < s.channels; i++ {
ret := C.ebur128_prev_true_peak(s.p, C.uint(i), &j)
if ret != C.EBUR128_SUCCESS {
return nil, fmt.Errorf("error calculating peak: %d", ret)
}
out = append(out, float64(j))
}
return
}
//GetLoudnessGlobal Get global integrated loudness in LUFS.
func (s *State) GetLoudnessGlobal() (out float64, err error) {
var i C.double
ret := C.ebur128_loudness_global(s.p, &i)
if ret != C.EBUR128_SUCCESS {
return float64(i), fmt.Errorf("error calculating loudness: %d", ret)
}
return float64(i), nil
}
//GetLoudnessGlobalMultiple Get global integrated loudness in LUFS across multiple instances.
func GetLoudnessGlobalMultiple(states []*State) (out float64, err error) {
slice := make([]*C.ebur128_state, len(states))
for i, s := range states {
slice[i] = s.p
}
var i C.double
ret := C.ebur128_loudness_global_multiple(&slice[0], C.ulong(len(slice)), &i)
if ret != C.EBUR128_SUCCESS {
return float64(i), fmt.Errorf("error calculating loudness: %d", ret)
}
return float64(i), nil
}
//GetLoudnessMomentary Get momentary loudness (last 400ms) in LUFS.
func (s *State) GetLoudnessMomentary() (out float64, err error) {
var i C.double
ret := C.ebur128_loudness_momentary(s.p, &i)
if ret != C.EBUR128_SUCCESS {
return float64(i), fmt.Errorf("error calculating loudness: %d", ret)
}
return float64(i), nil
}
//GetLoudnessShortTerm Get short-term loudness (last 3s) in LUFS.
func (s *State) GetLoudnessShortTerm() (out float64, err error) {
var i C.double
ret := C.ebur128_loudness_shortterm(s.p, &i)
if ret != C.EBUR128_SUCCESS {
return float64(i), fmt.Errorf("error calculating loudness: %d", ret)
}
return float64(i), nil
}
func (s *State) AddFloat(src []float32) error {
ret := C.ebur128_add_frames_float(s.p, (*C.float)(&src[0]), C.size_t(len(src)))
if ret != C.EBUR128_SUCCESS {
return fmt.Errorf("error adding frames: %d", ret)
}
return nil
}
func (s *State) AddShort(src []int16) error {
ret := C.ebur128_add_frames_short(s.p, (*C.short)(&src[0]), C.size_t(len(src)))
if ret != C.EBUR128_SUCCESS {
return fmt.Errorf("error adding frames: %d", ret)
}
return nil
}
func (s *State) Close() {
if s.p != nil {
C.ebur128_destroy(&s.p)
}
}

138
libebur128_test.go Normal file
View File

@ -0,0 +1,138 @@
package libebur128
import (
"errors"
"fmt"
"io/ioutil"
"os"
"testing"
"unsafe"
)
func TestFloat(t *testing.T) {
t.Parallel()
state := NewState(2, 44100, LoudnessGlobalMomentary|SamplePeak)
if state == nil {
t.Error(errors.New("could not create state"))
return
}
defer state.Close()
f, err := os.Open("sample/test_f32.raw")
if err != nil {
t.Error(err)
return
}
data, err := ioutil.ReadAll(f)
if err != nil {
t.Error(err)
return
}
const blockSize = 1024
floatData := unsafe.Slice((*float32)(unsafe.Pointer(&data[0])), len(data)/4)
for len(floatData) >= blockSize {
if err = state.AddFloat(floatData[:blockSize]); err != nil {
t.Error(err)
return
}
floatData = floatData[blockSize:]
}
if len(floatData) > 0 {
if err = state.AddFloat(floatData); err != nil {
t.Error(err)
return
}
}
var loudness float64
var peak []float64
if loudness, err = state.GetLoudnessGlobal(); err != nil {
t.Error(err)
return
}
if peak, err = state.GetSamplePeak(); err != nil {
t.Error(err)
return
}
var maxPeak float64
for _, p := range peak {
if p > maxPeak {
maxPeak = p
}
}
result := fmt.Sprintf("%f dB, %f", loudness, maxPeak)
expected := "-14.927264 dB, 0.666256"
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}
func TestShort(t *testing.T) {
t.Parallel()
state := NewState(2, 44100, LoudnessGlobalMomentary|SamplePeak)
if state == nil {
t.Error(errors.New("could not create state"))
return
}
defer state.Close()
f, err := os.Open("sample/test_s16.raw")
if err != nil {
t.Error(err)
return
}
data, err := ioutil.ReadAll(f)
if err != nil {
t.Error(err)
return
}
const blockSize = 1024
intData := unsafe.Slice((*int16)(unsafe.Pointer(&data[0])), len(data)/2)
for len(intData) >= blockSize {
if err = state.AddShort(intData[:blockSize]); err != nil {
t.Error(err)
return
}
intData = intData[blockSize:]
}
if len(intData) > 0 {
if err = state.AddShort(intData); err != nil {
t.Error(err)
return
}
}
var loudness float64
var peak []float64
if loudness, err = state.GetLoudnessGlobal(); err != nil {
t.Error(err)
return
}
if peak, err = state.GetSamplePeak(); err != nil {
t.Error(err)
return
}
var maxPeak float64
for _, p := range peak {
if p > maxPeak {
maxPeak = p
}
}
result := fmt.Sprintf("%f dB, %f", loudness, maxPeak)
expected := "-14.927263 dB, 0.666260"
if result != expected {
t.Errorf("expected %s, got %s", expected, result)
}
}

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`