commit ffc70c6395ea7cf4e10acb2e969181bcf3103586 Author: WeebDataHoarder <57538841+WeebDataHoarder@users.noreply.github.com> Date: Tue Mar 8 11:24:29 2022 +0100 Initial commit diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..b9139c5 --- /dev/null +++ b/.drone.yml @@ -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 + +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5071b87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +*.raw \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ba48b1 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4d1b5a --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fe18019 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.gammaspectra.live/S.O.N.G/go-ebur128 + +go 1.18 diff --git a/libebur128.go b/libebur128.go new file mode 100644 index 0000000..fbceec2 --- /dev/null +++ b/libebur128.go @@ -0,0 +1,163 @@ +package libebur128 + +/* +#cgo pkg-config: libebur128 +#include + +*/ +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) + } +} diff --git a/libebur128_test.go b/libebur128_test.go new file mode 100644 index 0000000..eed3da8 --- /dev/null +++ b/libebur128_test.go @@ -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) + } +} diff --git a/sample/MooveKa_-_Rockabilly_Punk_Rock.mp3 b/sample/MooveKa_-_Rockabilly_Punk_Rock.mp3 new file mode 100644 index 0000000..4b7e9ec Binary files /dev/null and b/sample/MooveKa_-_Rockabilly_Punk_Rock.mp3 differ diff --git a/sample/README.md b/sample/README.md new file mode 100644 index 0000000..6fcdda9 --- /dev/null +++ b/sample/README.md @@ -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`