Initial commit
This commit is contained in:
commit
00585aac13
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/.idea
|
62
Kirika_test.go
Normal file
62
Kirika_test.go
Normal 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
9
LICENSE
Normal 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
26
README.md
Normal 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
144
audio/format/flac/flac.go
Normal 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)(¤tFrame.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)(¤tFrame.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
39
audio/format/format.go
Normal 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
58
audio/format/mp3/mp3.go
Normal 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
52
audio/format/opus/opus.go
Normal 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
181
audio/stream.go
Normal 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
121
cgo/audio.h
Normal 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
12
go.mod
Normal 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
10
go.sum
Normal 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=
|
Loading…
Reference in a new issue