Initial commit

This commit is contained in:
DataHoarder 2022-02-22 10:24:49 +01:00
commit 00585aac13
13 changed files with 716 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/.idea

1
Kirika.go Normal file
View file

@ -0,0 +1 @@
package Kirika

62
Kirika_test.go Normal file
View file

@ -0,0 +1,62 @@
package Kirika
import (
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/mp3"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/opus"
"os"
"path"
"testing"
)
var TestSampleLocations = []string{
"resources/samples/cYsmix - Haunted House/",
"resources/samples/Babbe Music - RADIANT DANCEFLOOR/",
}
const BlockSize = 1024 * 64
func doTest(format format.Format, ext string, t *testing.T) {
for _, location := range TestSampleLocations {
entries, err := os.ReadDir(location)
if err != nil {
t.Error(err)
return
}
for _, f := range entries {
if path.Ext(f.Name()) == ext {
fullPath := path.Join(location, f.Name())
t.Run(f.Name(), func(t *testing.T) {
t.Parallel()
fp, err := os.Open(fullPath)
if err != nil {
t.Error(err)
return
}
defer fp.Close()
stream, err := format.Open(fp, BlockSize)
if err != nil {
t.Error(err)
return
}
//Decode
for range stream.GetAsBlockChannel() {
}
})
}
}
}
}
func TestFLACDecode(t *testing.T) {
doTest(flac.NewFormat(), ".flac", t)
}
func TestOpusDecode(t *testing.T) {
doTest(opus.NewFormat(), ".opus", t)
}
func TestMP3Decode(t *testing.T) {
doTest(mp3.NewFormat(), ".mp3", t)
}

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
Copyright (c) 2022 Kirika 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.

26
README.md Normal file
View file

@ -0,0 +1,26 @@
# [![](resources/kirikas.png)](resources/kirika.png) Kirika
Collection of audio utilities for decoding/encoding files and streams.
* channel-based audio consumption chain
* Audio resampler
* FLAC stream decoder and encoder
* MP3 stream decoder
* Opus stream decoder
## Dependencies
### Go >= 1.18
### [libFLAC](https://github.com/xiph/flac) (required by [goflac](https://github.com/cocoonlife/goflac))
```shell
sudo apt install libflac-dev
```
### [libopus](https://github.com/xiph/opus) and [libopusfile](https://github.com/xiph/opusfile) (required by [go-pus](https://git.gammaspectra.live/S.O.N.G/go-pus))
```shell
sudo apt install libopus-dev libopusfile-dev
```
### [libsamplerate](https://github.com/libsndfile/libsamplerate) (required by [gosamplerate](https://github.com/dh1tw/gosamplerate))
```shell
sudo apt install libsamplerate0-dev
```

144
audio/format/flac/flac.go Normal file
View file

@ -0,0 +1,144 @@
package flac
/*
#cgo CFLAGS: -I"${SRCDIR}/../../../cgo" -march=native -Ofast -std=c99
#include "audio.h"
*/
import "C"
import (
"bytes"
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format"
libflac "github.com/cocoonlife/goflac"
"io"
)
type Format struct {
}
func NewFormat() Format {
return Format{}
}
func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) {
decoder, err := libflac.NewDecoderReader(r)
if err != nil {
return nil, err
}
newChannel := make(chan []float32)
go func() {
defer close(newChannel)
defer decoder.Close()
frameNumber := 0
for {
currentFrame, err := decoder.ReadFrame()
if err != nil {
return
}
bitDepth := decoder.Depth
if currentFrame.Depth != 0 {
bitDepth = currentFrame.Depth
}
buf := make([]float32, len(currentFrame.Buffer))
C.audio_int32_to_float32((*C.int32_t)(&currentFrame.Buffer[0]), C.size_t(len(currentFrame.Buffer)), (*C.float)(&buf[0]), C.int(bitDepth))
newChannel <- buf
frameNumber++
}
}()
return audio.NewStream(newChannel, decoder.Channels, float64(decoder.Rate), blockSize), nil
}
func (f Format) OpenAnalyzer(r io.ReadSeekCloser, blockSize int) (*audio.Stream, chan *format.AnalyzerPacket, error) {
decoder, err := libflac.NewDecoderReader(r)
if err != nil {
return nil, nil, err
}
newChannel := make(chan []float32)
analyzerChannel := make(chan *format.AnalyzerPacket)
go func() {
defer close(newChannel)
defer close(analyzerChannel)
defer decoder.Close()
frameNumber := 0
for {
currentFrame, err := decoder.ReadFrame()
if err != nil {
return
}
bitDepth := decoder.Depth
channels := decoder.Channels
if currentFrame.Depth != 0 {
bitDepth = currentFrame.Depth
}
if currentFrame.Channels != 0 {
channels = currentFrame.Channels
}
buf := make([]float32, len(currentFrame.Buffer))
C.audio_int32_to_float32((*C.int32_t)(&currentFrame.Buffer[0]), C.size_t(len(currentFrame.Buffer)), (*C.float)(&buf[0]), C.int(bitDepth))
newChannel <- buf
analyzerChannel <- &format.AnalyzerPacket{
Samples: currentFrame.Buffer,
Channels: channels,
BitDepth: bitDepth,
SampleRate: decoder.Rate,
}
frameNumber++
}
}()
return audio.NewStream(newChannel, decoder.Channels, float64(decoder.Rate), blockSize), analyzerChannel, nil
}
func (f Format) Encode(stream *audio.Stream, writer format.WriteSeekCloser) error {
const bitsPerSample = 16
encoder, err := libflac.NewEncoderWriter(writer, stream.GetChannels(), bitsPerSample, int(stream.GetSampleRate()))
if err != nil {
return err
}
defer encoder.Close()
for block := range stream.GetAsBlockChannel() {
samples := make([]int32, len(block))
C.audio_float32_to_int32((*C.float)(&block[0]), C.size_t(len(block)), (*C.int32_t)(&samples[0]), C.int(bitsPerSample))
err = encoder.WriteFrame(libflac.Frame{
Rate: int(stream.GetSampleRate()),
Channels: stream.GetChannels(),
Depth: bitsPerSample,
Buffer: samples,
})
if err != nil {
return err
}
}
return nil
}
func (f Format) Identify(peek []byte, extension string) bool {
return bytes.Compare(peek[:4], []byte{'f', 'L', 'a', 'C'}) == 0 || extension == "flac"
}

39
audio/format/format.go Normal file
View file

@ -0,0 +1,39 @@
package format
import (
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
"io"
)
type Format interface {
// Identify checks whether a format is of a type. peek includes a few first bytes, extension is the lowercase file extension without a dot.
Identify(peek []byte, extension string) bool
// Open Opens a stream and decodes it into an audio.Stream
Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error)
}
type AnalyzerFormat interface {
Format
// OpenAnalyzer Opens a stream and decodes it into an audio.Stream, and additionally copy AnalyzerPacket back
OpenAnalyzer(r io.ReadSeekCloser, blockSize int) (*audio.Stream, chan *AnalyzerPacket, error)
}
type WriteSeekCloser interface {
io.Writer
io.Closer
io.Seeker
}
type Encoder interface {
// Encode Receives a stream and encodes it into a writer
Encode(stream *audio.Stream, writer WriteSeekCloser) error
}
type AnalyzerPacket struct {
//Samples interleaved samples
Samples []int32
Channels int
SampleRate int
BitDepth int
}

58
audio/format/mp3/mp3.go Normal file
View file

@ -0,0 +1,58 @@
package mp3
/*
#cgo CFLAGS: -I"${SRCDIR}/../../../cgo" -march=native -Ofast -std=c99
#include "audio.h"
*/
import "C"
import (
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
mp3Lib "github.com/kvark128/minimp3"
"io"
"unsafe"
)
type Format struct {
}
func NewFormat() Format {
return Format{}
}
func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) {
decoder := mp3Lib.NewDecoder(r)
_, err := decoder.Read([]byte{})
if err != nil {
return nil, err
}
newChannel := make(chan []float32)
go func() {
defer close(newChannel)
samples := make([]int16, blockSize*2)
const SizeofInt16 = int(unsafe.Sizeof(int16(0)))
byteSlice := unsafe.Slice((*byte)(unsafe.Pointer(&samples[0])), len(samples)*SizeofInt16)
for {
n, err := decoder.Read(byteSlice)
if err != nil {
return
}
n /= SizeofInt16
buf := make([]float32, n)
//convert 16-bit to f32 samples
C.audio_int16_to_float32((*C.int16_t)(&samples[0]), C.size_t(n), (*C.float)(&buf[0]), C.int(16))
newChannel <- buf
}
}()
return audio.NewStream(newChannel, decoder.Channels(), float64(decoder.SampleRate()), blockSize), nil
}
func (f Format) Identify(peek []byte, extension string) bool {
return /*bytes.Compare(peek[:4], []byte{'f', 'L', 'a', 'C'}) == 0 || */ extension == "mp3"
}

52
audio/format/opus/opus.go Normal file
View file

@ -0,0 +1,52 @@
package opus
import (
"bytes"
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
libopus "git.gammaspectra.live/S.O.N.G/go-pus"
"io"
)
const OPUS_SAMPLE_RATE int = 48000
type Format struct {
}
func NewFormat() Format {
return Format{}
}
func (f Format) Open(r io.ReadSeekCloser, blockSize int) (*audio.Stream, error) {
stream, err := libopus.NewStream(r)
if err != nil {
return nil, err
}
newChannel := make(chan []float32)
go func() {
defer stream.Close()
defer close(newChannel)
for {
buf := make([]float32, blockSize*2)
n, err := stream.ReadStereoFloat32(buf)
if err != nil {
return
}
if n > 0 {
newChannel <- buf[:n*2]
}
}
}()
//We always get two channels via stereo api
return audio.NewStream(newChannel, 2, float64(OPUS_SAMPLE_RATE), blockSize), nil
}
func (f Format) Identify(peek []byte, extension string) bool {
return bytes.Compare(peek[:4], []byte{'O', 'g', 'g', 'S'}) == 0 || extension == "opus"
}

181
audio/stream.go Normal file
View file

@ -0,0 +1,181 @@
package audio
/*
#cgo CFLAGS: -I"${SRCDIR}/../cgo" -march=native -Ofast -std=c99
#include "audio.h"
*/
import "C"
import (
"fmt"
"github.com/dh1tw/gosamplerate"
"log"
)
type Stream struct {
source chan []float32
channels int
sampleRate float64
samplesProcessed int
buffer []float32
blockSize int
}
func NewStream(source chan []float32, channels int, sampleRate float64, blockSize int) *Stream {
return &Stream{
source: source,
channels: channels,
sampleRate: sampleRate,
blockSize: blockSize,
}
}
func (s *Stream) GetChannels() int {
return s.channels
}
func (s *Stream) GetSampleRate() float64 {
return s.sampleRate
}
func (s *Stream) GetSamplesProcessed() int {
return s.samplesProcessed
}
func (s *Stream) GetAsChannel() chan float32 {
newChannel := make(chan float32)
go func() {
defer close(newChannel)
for {
v, more := s.Get()
if !more {
return
}
newChannel <- v
s.samplesProcessed++
}
}()
return newChannel
}
func (s *Stream) GetAsBlockChannel() chan []float32 {
newChannel := make(chan []float32)
go func() {
defer close(newChannel)
for buf := range s.source {
s.buffer = append(s.buffer, buf...)
for len(s.buffer) >= s.blockSize*s.channels {
newChannel <- s.buffer[0 : s.blockSize*s.channels]
s.samplesProcessed += s.blockSize * s.channels
s.buffer = s.buffer[s.blockSize*s.channels:]
}
}
if len(s.buffer) > 0 {
newChannel <- s.buffer
s.samplesProcessed += len(s.buffer)
}
s.buffer = nil
}()
return newChannel
}
func (s *Stream) Get() (float32, bool) {
var more bool
if len(s.buffer) == 0 {
s.buffer, more = <-s.source
if !more {
return 0, false
}
}
f := s.buffer[0]
s.buffer = s.buffer[1:]
s.samplesProcessed++
return f, true
}
func (s *Stream) AdvanceSeconds(seconds float64) bool {
stopAt := s.secondsIndex(seconds)
for i := 0; i < stopAt; i++ {
_, more := s.Get()
if !more {
return false
}
}
return true
}
func (s *Stream) secondsIndex(seconds float64) int {
return int(seconds * s.sampleRate)
}
const (
RESAMPLER_QUALITY_BANDLIMITED_BEST = gosamplerate.SRC_SINC_BEST_QUALITY
RESAMPLER_QUALITY_BANDLIMITED_MEDIUM = gosamplerate.SRC_SINC_MEDIUM_QUALITY
RESAMPLER_QUALITY_BANDLIMITED_FASTEST = gosamplerate.SRC_SINC_FASTEST
RESAMPLER_QUALITY_HOLD = gosamplerate.SRC_ZERO_ORDER_HOLD
RESAMPLER_QUALITY_LINEAR = gosamplerate.SRC_LINEAR
)
func (s *Stream) DoResample(channels int, sampleRate float64, quality int) (*Stream, error) {
if channels != 1 && channels != 2 && s.channels != channels {
return nil, fmt.Errorf("cannot convert from %d channels to %d", s.channels, channels)
}
if channels == s.channels && sampleRate == s.sampleRate {
return s, nil
}
samplerateConverter, err := gosamplerate.New(quality, channels, s.blockSize*s.channels)
if err != nil {
return nil, err
}
newChannel := make(chan []float32)
go func() {
defer gosamplerate.Delete(samplerateConverter)
defer close(newChannel)
ratio := sampleRate / s.sampleRate
for buf := range s.GetAsBlockChannel() {
if channels != s.channels && channels == 1 {
if channels == 1 {
//bring any number of channels to mono, equally weighted, reusing buffer backwards
C.audio_multiple_channels_to_mono((*C.float)(&buf[0]), C.size_t(len(buf)), C.int(s.channels))
buf = buf[0:(len(buf) / s.channels)]
} else if channels == 2 {
//bring any number of channels to stereo, using downmix formulas when necessary
out := make([]float32, (len(buf)/s.channels)*2)
C.audio_multiple_channels_to_stereo((*C.float)(&buf[0]), C.size_t(len(buf)), (*C.float)(&out[0]), C.int(s.channels))
buf = out
}
}
for len(buf) > 0 {
n := len(buf)
if n > s.blockSize*channels {
n = s.blockSize * channels
}
b, err := samplerateConverter.Process(buf[0:n], ratio, false)
if err != nil {
log.Panic(err)
}
if len(b) > 0 {
newChannel <- b
}
buf = buf[n:]
}
}
b, err := samplerateConverter.Process([]float32{}, ratio, true)
if err != nil {
log.Panic(err)
}
if len(b) > 0 {
newChannel <- b
}
}()
return NewStream(newChannel, channels, sampleRate, s.blockSize), nil
}

121
cgo/audio.h Normal file
View file

@ -0,0 +1,121 @@
#include <stdint.h>
#include <string.h>
#include <math.h>
#define BITS_TO_DIV(b) (float)((1 << (b - 1)) - 1)
__attribute__((weak)) void audio_multiple_channels_to_mono(float* buffer, size_t buffer_len, int channels) {
float f;
switch(channels) {
case 1:
break;
case 2:
for (int i = 0; i < buffer_len; i += 2){
buffer[i/2] = buffer[i] + buffer[i+1];
}
break;
default:
for (int i = 0; i < buffer_len; i += channels){
f = buffer[i];
for (int j = 1; j < channels; ++j) {
f += buffer[i + j];
}
buffer[i/channels] = f / (float)channels;
}
}
}
__attribute__((weak)) void audio_multiple_channels_to_stereo(float* buffer, size_t buffer_len, float* out, int channels) {
float f;
int samples = 0;
float surroundMix = 1 / sqrt(2);
switch(channels) {
case 1: //mono, duplicate channels
for (int i = 0; i < buffer_len; i++){
out[i*2] = buffer[i];
out[i*2+1] = buffer[i];
}
break;
case 2: //copy
memcpy(out, buffer, sizeof(float) * buffer_len);
break;
case 3: //2.1, FL, FR, LFE
for (int i = 0; i < buffer_len; i += 3){
int FL = i;
int FR = i+1;
int LFE = i+2;
out[samples*2] = buffer[FL];
out[samples*2+1] = buffer[FR];
++samples;
}
case 5: //5.0, FL, FR, FC, RL, RR
for (int i = 0; i < buffer_len; i += 5){
int FL = i;
int FR = i+1;
int C = i+2;
int RL = i+3;
int RR = i+4;
out[samples*2] = buffer[FL] + surroundMix * buffer[C] + surroundMix * RL;
out[samples*2+1] = buffer[FR] + surroundMix * buffer[C] + surroundMix * RR;
++samples;
}
case 6: //5.1, FL, FR, FC, LFE, RL, RR
for (int i = 0; i < buffer_len; i += 6){
int FL = i;
int FR = i+1;
int C = i+2;
int LFE = i+3;
int RL = i+4;
int RR = i+5;
out[samples*2] = buffer[FL] + surroundMix * buffer[C] + surroundMix * RL;
out[samples*2+1] = buffer[FR] + surroundMix * buffer[C] + surroundMix * RR;
++samples;
}
break;
default: //no known formula, just take stereo out of it
for (int i = 0; i < buffer_len; i += channels){
out[samples*2] = buffer[i];
out[samples*2+1] = buffer[i+1];
++samples;
}
}
}
__attribute__((weak)) void audio_int32_to_float32(int32_t* restrict data, size_t data_len, float* restrict buffer, int bitDepth){
switch(bitDepth) {
case 16:
for (int i = 0; i < data_len; ++i){
buffer[i] = ((float)data[i]) / BITS_TO_DIV(16);
}
break;
case 24:
for (int i = 0; i < data_len; ++i){
buffer[i] = ((float)data[i]) / BITS_TO_DIV(24);
}
break;
default:
for (int i = 0; i < data_len; ++i){
buffer[i] = ((float)data[i]) / BITS_TO_DIV(bitDepth);
}
}
}
__attribute__((weak)) void audio_int16_to_float32(int16_t* restrict data, size_t data_len, float* restrict buffer, int bitDepth){
switch(bitDepth) {
case 16:
for (int i = 0; i < data_len; ++i){
buffer[i] = ((float)data[i]) / BITS_TO_DIV(16);
}
break;
default:
for (int i = 0; i < data_len; ++i){
buffer[i] = ((float)data[i]) / BITS_TO_DIV(bitDepth);
}
}
}
__attribute__((weak)) void audio_float32_to_int32(float* restrict data, size_t data_len, int32_t* restrict buffer, int bitDepth){
for (int i = 0; i < data_len; ++i){
buffer[i] = (int32_t)(data[i] * BITS_TO_DIV(bitDepth));
}
}

12
go.mod Normal file
View file

@ -0,0 +1,12 @@
module git.gammaspectra.live/S.O.N.G/Kirika
go 1.18
require (
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220130003320-c9b07c6bec7a
github.com/cocoonlife/goflac v0.0.0-20170210142907-50ea06ed5a9d
github.com/dh1tw/gosamplerate v0.1.2
github.com/kvark128/minimp3 v0.0.0-20211109174940-101188771a65
)
require github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64 // indirect

10
go.sum Normal file
View file

@ -0,0 +1,10 @@
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220130003320-c9b07c6bec7a h1:LxrTp9gf4w5KnFHRPFLXYfoxC58GCSEmZrHI6Ogtrm0=
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220130003320-c9b07c6bec7a/go.mod h1:vkoHSHVM9p6vAUmXAik0gvaLcIfiQYrD6bQqVpOulUk=
github.com/cocoonlife/goflac v0.0.0-20170210142907-50ea06ed5a9d h1:utj98F6D5jVv2tHYMsYzM6Z5sG71/W12Ivkd/SnFiN0=
github.com/cocoonlife/goflac v0.0.0-20170210142907-50ea06ed5a9d/go.mod h1:swNVb00X8NOH/qeHuqnqiyfecAnWlThLX+NbH8r6yHw=
github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64 h1:LjPYdzoFSAJ5Tr/ElL8kzTJghXgpnOjJVbgd1UvZB1o=
github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64/go.mod h1:LoCAz53rbPcqs8Da2BjB/yDy4gxMtiSQmqnYI/DGH+U=
github.com/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/ktI=
github.com/dh1tw/gosamplerate v0.1.2/go.mod h1:zooTyHpoR7hE+FLfdE3yjLHb2QA2NpMusNfuaZqEACM=
github.com/kvark128/minimp3 v0.0.0-20211109174940-101188771a65 h1:8qfVQv7MSACDXadEwl1yjUKJ68yC9B7nR4cioEoCfH0=
github.com/kvark128/minimp3 v0.0.0-20211109174940-101188771a65/go.mod h1:hIq9nAqNcwTySvnFhCe1C8xC/STIr2Fe5vJ52zk1jkE=