sointu/audio.go
5684185+vsariola@users.noreply.github.com e4a2ed9f32 style: group types into fewer, logical files
2023-10-18 15:02:25 +03:00

138 lines
4.6 KiB
Go

package sointu
import (
"bytes"
"encoding/binary"
"fmt"
"math"
)
type (
// AudioBuffer is a buffer of stereo audio samples of variable length, each
// sample represented by a slice of [2]float32. [0] is left channel, [1] is
// right
AudioBuffer [][2]float32
// AudioOutput represents something where we can send audio e.g. audio output.
// WriteAudio should block if not ready to accept audio e.g. buffer full.
AudioOutput interface {
WriteAudio(buffer AudioBuffer) error
Close() error
}
// AudioContext represents the low-level audio drivers. There should be at most
// one AudioContext at a time. The interface is implemented at least by
// oto.OtoContext, but in future we could also mock it.
//
// AudioContext is used to create one or more AudioOutputs with Output(); each
// can be used to output separate sound & closed when done.
AudioContext interface {
Output() AudioOutput
Close() error
}
)
// Wav converts a stereo signal of 32-bit floats (L R L R..., length should be
// divisible by 2) into a valid WAV-file, returned as a []byte array.
//
// If pcm16 is set to true, the samples in the WAV-file will be 16-bit signed
// integers; otherwise the samples will be 32-bit floats
func (buffer AudioBuffer) Wav(pcm16 bool) ([]byte, error) {
buf := new(bytes.Buffer)
wavHeader(len(buffer)*2, pcm16, buf)
err := buffer.rawToBuffer(pcm16, buf)
if err != nil {
return nil, fmt.Errorf("Wav failed: %v", err)
}
return buf.Bytes(), nil
}
// Raw converts a stereo signal of 32-bit floats (L R L R..., length should be
// divisible by 2) into a raw audio file, returned as a []byte array.
//
// If pcm16 is set to true, the samples will be 16-bit signed integers;
// otherwise the samples will be 32-bit floats
func (buffer AudioBuffer) Raw(pcm16 bool) ([]byte, error) {
buf := new(bytes.Buffer)
err := buffer.rawToBuffer(pcm16, buf)
if err != nil {
return nil, fmt.Errorf("Raw failed: %v", err)
}
return buf.Bytes(), nil
}
func (data AudioBuffer) rawToBuffer(pcm16 bool, buf *bytes.Buffer) error {
var err error
if pcm16 {
int16data := make([][2]int16, len(data))
for i, v := range data {
int16data[i][0] = int16(clamp(int(v[0]*math.MaxInt16), math.MinInt16, math.MaxInt16))
int16data[i][1] = int16(clamp(int(v[1]*math.MaxInt16), math.MinInt16, math.MaxInt16))
}
err = binary.Write(buf, binary.LittleEndian, int16data)
} else {
err = binary.Write(buf, binary.LittleEndian, data)
}
if err != nil {
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
}
return nil
}
// wavHeader writes a wave header for either float32 or int16 .wav file into the
// bytes.buffer. It needs to know the length of the buffer and assumes stereo
// sound, so the length in stereo samples (L + R) is bufferlength / 2. If pcm16
// = true, then the header is for int16 audio; pcm16 = false means the header is
// for float32 audio. Assumes 44100 Hz sample rate.
func wavHeader(bufferLength int, pcm16 bool, buf *bytes.Buffer) {
// Refer to: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
numChannels := 2
sampleRate := 44100
var bytesPerSample, chunkSize, fmtChunkSize, waveFormat int
var factChunk bool
if pcm16 {
bytesPerSample = 2
chunkSize = 36 + bytesPerSample*bufferLength
fmtChunkSize = 16
waveFormat = 1 // PCM
factChunk = false
} else {
bytesPerSample = 4
chunkSize = 50 + bytesPerSample*bufferLength
fmtChunkSize = 18
waveFormat = 3 // IEEE float
factChunk = true
}
buf.Write([]byte("RIFF"))
binary.Write(buf, binary.LittleEndian, uint32(chunkSize))
buf.Write([]byte("WAVE"))
buf.Write([]byte("fmt "))
binary.Write(buf, binary.LittleEndian, uint32(fmtChunkSize))
binary.Write(buf, binary.LittleEndian, uint16(waveFormat))
binary.Write(buf, binary.LittleEndian, uint16(numChannels))
binary.Write(buf, binary.LittleEndian, uint32(sampleRate))
binary.Write(buf, binary.LittleEndian, uint32(sampleRate*numChannels*bytesPerSample)) // avgBytesPerSec
binary.Write(buf, binary.LittleEndian, uint16(numChannels*bytesPerSample)) // blockAlign
binary.Write(buf, binary.LittleEndian, uint16(8*bytesPerSample)) // bits per sample
if fmtChunkSize > 16 {
binary.Write(buf, binary.LittleEndian, uint16(0)) // size of extension
}
if factChunk {
buf.Write([]byte("fact"))
binary.Write(buf, binary.LittleEndian, uint32(4)) // fact chunk size
binary.Write(buf, binary.LittleEndian, uint32(bufferLength)) // sample length
}
buf.Write([]byte("data"))
binary.Write(buf, binary.LittleEndian, uint32(bytesPerSample*bufferLength))
}
func clamp(value, min, max int) int {
if value < min {
return min
}
if value > max {
return max
}
return value
}