From 8acfea10f4b7afd6334d143b753702ed7cb7b7d2 Mon Sep 17 00:00:00 2001 From: plandem Date: Tue, 25 Mar 2025 03:05:04 -0400 Subject: [PATCH 1/3] added `General Data Conversion` API that used internally by devices. --- _examples/convert/convert.go | 216 +++++++++++++++++++++++++++++++++++ converter.go | 135 ++++++++++++++++++++++ enumerations.go | 22 ++++ miniaudio.go | 5 + 4 files changed, 378 insertions(+) create mode 100644 _examples/convert/convert.go create mode 100644 converter.go diff --git a/_examples/convert/convert.go b/_examples/convert/convert.go new file mode 100644 index 0000000..31cec6a --- /dev/null +++ b/_examples/convert/convert.go @@ -0,0 +1,216 @@ +package main + +import ( + "encoding/binary" + "fmt" + "github.com/gen2brain/malgo" + "github.com/youpy/go-riff" + "github.com/youpy/go-wav" + "io" + "os" +) + +type Writer struct { + file *os.File + dataSize int + channels int + sampleRate int + bitDepth int + dataChunkPos int64 + format wav.WavFormat +} + +// NewWriter creates a new WAV writer for streaming audio +func NewWriter(filename string, format WavFormat) (*Writer, error) { + file, err := os.Create(filename) + if err != nil { + return nil, err + } + + // Create a RIFF writer with placeholder size + riffWriter := riff.NewWriter(file, []byte("WAVE"), 0) + + // Create WAV format chunk + format.BlockAlign = format.NumChannels * format.BitsPerSample / 8 + format.ByteRate = format.SampleRate * uint32(format.BlockAlign) + + // Write format chunk + err = riffWriter.WriteChunk([]byte("fmt "), 16, func(w io.Writer) { + binary.Write(w, binary.LittleEndian, format) + }) + if err != nil { + file.Close() + return nil, err + } + + // Write data chunk header with placeholder size + _, err = io.WriteString(file, "data") + if err != nil { + file.Close() + return nil, err + } + + // Remember position where we need to write the data size later + dataChunkPos, err := file.Seek(0, io.SeekCurrent) + if err != nil { + file.Close() + return nil, err + } + + // Write a placeholder size (0) + err = binary.Write(file, binary.LittleEndian, uint32(0)) + if err != nil { + file.Close() + return nil, err + } + + return &Writer{ + file: file, + dataSize: 0, + format: format, + dataChunkPos: dataChunkPos, + }, nil +} + +// Write implements io.Writer +func (w *Writer) Write(p []byte) (n int, err error) { + n, err = w.file.Write(p) + w.dataSize += n + return +} + +// Close finalizes the WAV file by updating headers with correct sizes +func (w *Writer) Close() error { + // Go back to data chunk size position and update it + _, err := w.file.Seek(w.dataChunkPos, io.SeekStart) + if err != nil { + return err + } + + // Write the actual data size + err = binary.Write(w.file, binary.LittleEndian, uint32(w.dataSize)) + if err != nil { + return err + } + + // Go to beginning of file to update the RIFF chunk size + _, err = w.file.Seek(4, io.SeekStart) + if err != nil { + return err + } + + // RIFF chunk size is: 4 (WAVE) + 8 (fmt chunk header) + 16 (fmt chunk) + 8 (data chunk header) + dataSize + riffSize := uint32(4 + 8 + 16 + 8 + w.dataSize) + err = binary.Write(w.file, binary.LittleEndian, riffSize) + if err != nil { + return err + } + + return w.file.Close() +} + +func BitsToType(bits int) malgo.FormatType { + switch bits { + case 8: + return malgo.FormatU8 + case 16: + return malgo.FormatS16 + case 24: + return malgo.FormatS24 + case 32: + return malgo.FormatS32 + default: + return malgo.FormatUnknown + } +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("No input wav file.") + os.Exit(1) + } + + file, err := os.Open(os.Args[1]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + defer file.Close() + + w := wav.NewReader(file) + inputFormat, err := w.Format() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + outputFormat := wav.WavFormat{ + AudioFormat: wav.AudioFormatPCM, + NumChannels: 1, + SampleRate: 48000, + BitsPerSample: 32, + } + + wavWriter, err := NewWriter("converted.wav", outputFormat) + if err != nil { + fmt.Println("Failed to create WAV file:", err) + os.Exit(1) + } + defer wavWriter.Close() + + formatTypeIn := BitsToType(int(inputFormat.BitsPerSample)) + if inputFormat.AudioFormat == wav.AudioFormatIEEEFloat { + formatTypeIn = malgo.FormatF32 + } + + formatTypeOut := BitsToType(outputFormat.BitsPerSample) + if outputFormat.AudioFormat == wav.AudioFormatIEEEFloat { + formatTypeOut = malgo.FormatF32 + } + + config := malgo.ConverterConfig{ + FormatIn: formatTypeIn, + FormatOut: formatTypeOut, + ChannelsIn: inputFormat.NumChannels, + ChannelsOut: outputFormat.NumChannels, + SampleRateIn: inputFormat.SampleRate, + SampleRateOut: outputFormat.SampleRate, + Resampling: malgo.ResampleConfig{ + Algorithm: malgo.ResampleAlgorithmLinear, + }, + DitherMode: malgo.DitherModeTriangle, + ChannelMixMode: malgo.ChannelMixModeSimple, + } + converter, err := malgo.InitConverter(config) + if err != nil { + fmt.Print(err) + os.Exit(-1) + } + + inFrameSize := malgo.FrameSizeInBytes(config.FormatIn, config.ChannelsIn) + outFrameSize := malgo.FrameSizeInBytes(config.FormatOut, config.ChannelsOut) + + inputFrames := 1000 + expectFrames, _ := converter.ExpectOutputFrameCount(inputFrames) + inBuffer := make([]byte, inFrameSize*inputFrames) + outBuffer := make([]byte, outFrameSize*expectFrames) + + for { + n, err := w.Read(inBuffer) + if err != nil { + break + } + + readFrameCount := n / inFrameSize + _, outFrameCount, err := converter.ProcessFrames(inBuffer, readFrameCount, outBuffer, expectFrames) + if err != nil { + fmt.Print(err) + continue + } else { + wavWriter.Write(outBuffer[:outFrameCount*outFrameSize]) + } + } + + converter.Uninit() +} diff --git a/converter.go b/converter.go new file mode 100644 index 0000000..5018dad --- /dev/null +++ b/converter.go @@ -0,0 +1,135 @@ +package malgo + +// #include "malgo.h" +import "C" +import ( + "unsafe" +) + +type ConverterConfig struct { + FormatIn FormatType + FormatOut FormatType + ChannelsIn int + ChannelsOut int + SampleRateIn int + SampleRateOut int + DitherMode DitherModeType + ChannelMixMode ChannelMixModeType + Resampling ResampleConfig + + // Unexposed: pChannelMapIn, pChannelMapOut, calculateLFEFromSpatialChannels, ppChannelWeights, allowDynamicSampleRate +} + +type Converter struct { + ptr *unsafe.Pointer +} + +// InitConverter initializes a converter. +// +// Converter can be used to wrap sample format conversion, channel conversion and +// resampling into one operation. This is what miniaudio uses internally to convert between the format +// requested when the device was initialized and the format of the backend's native device. +// +// It is very similar to the resampling API. +// +// The returned instance has to be cleaned up using Uninit(). +func InitConverter(config ConverterConfig) (*Converter, error) { + ptr := C.ma_malloc(C.sizeof_ma_data_converter, nil) + converter := Converter{ + ptr: &ptr, + } + if uintptr(*converter.ptr) == 0 { + return nil, ErrOutOfMemory + } + + configC := C.ma_data_converter_config_init_default() + configC.formatIn = C.ma_format(config.FormatIn) + configC.formatOut = C.ma_format(config.FormatOut) + configC.channelsIn = C.ma_uint32(config.ChannelsIn) + configC.channelsOut = C.ma_uint32(config.ChannelsOut) + configC.sampleRateIn = C.ma_uint32(config.SampleRateIn) + configC.sampleRateOut = C.ma_uint32(config.SampleRateOut) + configC.resampling.algorithm = C.ma_resample_algorithm(config.Resampling.Algorithm) + configC.resampling.linear.lpfOrder = C.uint(config.Resampling.Linear.LpfOrder) + + result := C.ma_data_converter_init(&configC, nil, converter.cptr()) + if result != 0 { + C.ma_free(ptr, nil) + return nil, errorFromResult(result) + } + + return &converter, nil +} + +// Uninit cleans up the ma_data_converter object. +func (c *Converter) Uninit() { + C.ma_data_converter_uninit(c.cptr(), nil) + c.free() +} + +func (c Converter) free() { + if c.ptr != nil { + C.ma_free(*c.ptr, nil) + } +} + +func (c Converter) cptr() *C.ma_data_converter { + return (*C.ma_data_converter)(*c.ptr) +} + +// RequiredInputFrameCount returns how many input frames you need to provide in order to output a specific number of output frames. +func (c *Converter) RequiredInputFrameCount(outputFrameCount int) (int, error) { + var cInputFrameCount C.ma_uint64 + var cOutputFrameCount C.ma_uint64 = C.ma_uint64(outputFrameCount) + + result := C.ma_data_converter_get_required_input_frame_count(c.cptr(), cOutputFrameCount, &cInputFrameCount) + if result != 0 { + return 0, errorFromResult(result) + } + + return int(cInputFrameCount), nil +} + +// ExpectOutputFrameCount returns how many output frames you can expect to get from a specific number of input frames. +func (c *Converter) ExpectOutputFrameCount(inputFrameCount int) (int, error) { + var cInputFrameCount C.ma_uint64 = C.ma_uint64(inputFrameCount) + var cOutputFrameCount C.ma_uint64 + + result := C.ma_data_converter_get_expected_output_frame_count(c.cptr(), cInputFrameCount, &cOutputFrameCount) + if result != 0 { + return 0, errorFromResult(result) + } + + return int(cOutputFrameCount), nil +} + +// ProcessFrames processes PCM frames using the data converter. +// +// Processing always happens on a per PCM frame basis and always assumes interleaved input and output. +// De-interleaved processing is not supported. On input, this function takes the number of output frames +// you can fit in the output buffer and the number of input frames contained in the input buffer. On +// output these variables contain the number of output frames that were written to the output buffer +// and the number of input frames that were consumed in the process. +// +// You can pass in nil for the input buffer in which case it will be treated as an infinitely large +// buffer of zeros. The output buffer can also be nil, in which case the processing will be treated +// as seek. +func (c *Converter) ProcessFrames(pFramesIn []byte, frameCountIn int, pFramesOut []byte, frameCountOut int) (int, int, error) { + if len(pFramesIn) == 0 || len(pFramesOut) == 0 || frameCountIn == 0 || frameCountOut == 0 { + return 0, 0, ErrInvalidArgs + } + + var cFrameCountIn C.ma_uint64 = C.ma_uint64(frameCountIn) + var cFrameCountOut C.ma_uint64 = C.ma_uint64(frameCountOut) + + result := C.ma_data_converter_process_pcm_frames(c.cptr(), + unsafe.Pointer(&pFramesIn[0]), &cFrameCountIn, + unsafe.Pointer(&pFramesOut[0]), &cFrameCountOut, + ) + + if result != 0 { + return 0, 0, errorFromResult(result) + } + + return int(cFrameCountIn), int(cFrameCountOut), nil +} diff --git a/enumerations.go b/enumerations.go index 332336c..8210c1f 100644 --- a/enumerations.go +++ b/enumerations.go @@ -86,6 +86,7 @@ type ResampleAlgorithm uint32 const ( ResampleAlgorithmLinear ResampleAlgorithm = 0 ResampleAlgorithmSpeex ResampleAlgorithm = 1 + ResampleAlgorithmCustom = ResampleAlgorithmSpeex ) // IOSSessionCategory type. @@ -116,3 +117,24 @@ const ( IOSSessionCategoryOptionAllowBluetoothA2dp = 0x20 // AVAudioSessionCategoryOptionAllowBluetoothA2DP IOSSessionCategoryOptionAllowAirPlay = 0x40 // AVAudioSessionCategoryOptionAllowAirPlay ) + +// DitherModeType type. +type DitherModeType uint32 + +// DitherModeType enumeration. +const ( + DitherModeNone DitherModeType = iota + DitherModeRectangle + DitherModeTriangle +) + +// ChannelMixModeType type. +type ChannelMixModeType uint32 + +// ChannelMixModeType enumeration. +const ( + ChannelMixModeRectangular ChannelMixModeType = iota + ChannelMixModeSimple + ChannelMixModeCustomWeights + ChannelMixModeDefault = ChannelMixModeRectangular +) diff --git a/miniaudio.go b/miniaudio.go index 482bacd..73bc54b 100644 --- a/miniaudio.go +++ b/miniaudio.go @@ -29,6 +29,11 @@ func SampleSizeInBytes(format FormatType) int { return int(ret) } +// FrameSizeInBytes retrieves the size of a frame in bytes for the given format. +func FrameSizeInBytes(format FormatType, channels int) int { + return SampleSizeInBytes(format) * channels +} + const ( rawDeviceInfoSize = C.sizeof_ma_device_info ) From d42f43311303f520c1405a622fc74e8d04e90590 Mon Sep 17 00:00:00 2001 From: plandem Date: Tue, 25 Mar 2025 03:16:38 -0400 Subject: [PATCH 2/3] added `General Data Conversion` API that used internally by devices. --- converter.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/converter.go b/converter.go index 5018dad..7f6631b 100644 --- a/converter.go +++ b/converter.go @@ -115,18 +115,24 @@ func (c *Converter) ExpectOutputFrameCount(inputFrameCount int) (int, error) { // buffer of zeros. The output buffer can also be nil, in which case the processing will be treated // as seek. func (c *Converter) ProcessFrames(pFramesIn []byte, frameCountIn int, pFramesOut []byte, frameCountOut int) (int, int, error) { - if len(pFramesIn) == 0 || len(pFramesOut) == 0 || frameCountIn == 0 || frameCountOut == 0 { - return 0, 0, ErrInvalidArgs + var cFramesIn unsafe.Pointer + if len(pFramesIn) == 0 || pFramesIn == nil { + cFramesIn = unsafe.Pointer(nil) + } else { + cFramesIn = unsafe.Pointer(&pFramesIn[0]) + } + + var cFramesOut unsafe.Pointer + if len(pFramesOut) == 0 || pFramesOut == nil { + cFramesOut = unsafe.Pointer(nil) + } else { + cFramesOut = unsafe.Pointer(&pFramesOut[0]) } var cFrameCountIn C.ma_uint64 = C.ma_uint64(frameCountIn) var cFrameCountOut C.ma_uint64 = C.ma_uint64(frameCountOut) - result := C.ma_data_converter_process_pcm_frames(c.cptr(), - unsafe.Pointer(&pFramesIn[0]), &cFrameCountIn, - unsafe.Pointer(&pFramesOut[0]), &cFrameCountOut, - ) - + result := C.ma_data_converter_process_pcm_frames(c.cptr(), cFramesIn, &cFrameCountIn, cFramesOut, &cFrameCountOut) if result != 0 { return 0, 0, errorFromResult(result) } From 82b44e868b968af8328f2edbe66cf52aa2390472 Mon Sep 17 00:00:00 2001 From: plandem Date: Tue, 25 Mar 2025 10:12:17 -0400 Subject: [PATCH 3/3] simplified example to keep only relevant code to demonstrate --- _examples/convert/convert.go | 195 ++++------------------------------- 1 file changed, 19 insertions(+), 176 deletions(-) diff --git a/_examples/convert/convert.go b/_examples/convert/convert.go index 31cec6a..724b182 100644 --- a/_examples/convert/convert.go +++ b/_examples/convert/convert.go @@ -1,179 +1,22 @@ package main import ( - "encoding/binary" - "fmt" "github.com/gen2brain/malgo" - "github.com/youpy/go-riff" - "github.com/youpy/go-wav" "io" - "os" ) -type Writer struct { - file *os.File - dataSize int - channels int - sampleRate int - bitDepth int - dataChunkPos int64 - format wav.WavFormat +type PCMFormat struct { + Type malgo.FormatType + Channels int + SampleRate int } -// NewWriter creates a new WAV writer for streaming audio -func NewWriter(filename string, format WavFormat) (*Writer, error) { - file, err := os.Create(filename) - if err != nil { - return nil, err - } - - // Create a RIFF writer with placeholder size - riffWriter := riff.NewWriter(file, []byte("WAVE"), 0) - - // Create WAV format chunk - format.BlockAlign = format.NumChannels * format.BitsPerSample / 8 - format.ByteRate = format.SampleRate * uint32(format.BlockAlign) - - // Write format chunk - err = riffWriter.WriteChunk([]byte("fmt "), 16, func(w io.Writer) { - binary.Write(w, binary.LittleEndian, format) - }) - if err != nil { - file.Close() - return nil, err - } - - // Write data chunk header with placeholder size - _, err = io.WriteString(file, "data") - if err != nil { - file.Close() - return nil, err - } - - // Remember position where we need to write the data size later - dataChunkPos, err := file.Seek(0, io.SeekCurrent) - if err != nil { - file.Close() - return nil, err - } - - // Write a placeholder size (0) - err = binary.Write(file, binary.LittleEndian, uint32(0)) - if err != nil { - file.Close() - return nil, err - } - - return &Writer{ - file: file, - dataSize: 0, - format: format, - dataChunkPos: dataChunkPos, - }, nil -} - -// Write implements io.Writer -func (w *Writer) Write(p []byte) (n int, err error) { - n, err = w.file.Write(p) - w.dataSize += n - return -} - -// Close finalizes the WAV file by updating headers with correct sizes -func (w *Writer) Close() error { - // Go back to data chunk size position and update it - _, err := w.file.Seek(w.dataChunkPos, io.SeekStart) - if err != nil { - return err - } - - // Write the actual data size - err = binary.Write(w.file, binary.LittleEndian, uint32(w.dataSize)) - if err != nil { - return err - } - - // Go to beginning of file to update the RIFF chunk size - _, err = w.file.Seek(4, io.SeekStart) - if err != nil { - return err - } - - // RIFF chunk size is: 4 (WAVE) + 8 (fmt chunk header) + 16 (fmt chunk) + 8 (data chunk header) + dataSize - riffSize := uint32(4 + 8 + 16 + 8 + w.dataSize) - err = binary.Write(w.file, binary.LittleEndian, riffSize) - if err != nil { - return err - } - - return w.file.Close() -} - -func BitsToType(bits int) malgo.FormatType { - switch bits { - case 8: - return malgo.FormatU8 - case 16: - return malgo.FormatS16 - case 24: - return malgo.FormatS24 - case 32: - return malgo.FormatS32 - default: - return malgo.FormatUnknown - } -} - -func main() { - if len(os.Args) < 2 { - fmt.Println("No input wav file.") - os.Exit(1) - } - - file, err := os.Open(os.Args[1]) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - defer file.Close() - - w := wav.NewReader(file) - inputFormat, err := w.Format() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - outputFormat := wav.WavFormat{ - AudioFormat: wav.AudioFormatPCM, - NumChannels: 1, - SampleRate: 48000, - BitsPerSample: 32, - } - - wavWriter, err := NewWriter("converted.wav", outputFormat) - if err != nil { - fmt.Println("Failed to create WAV file:", err) - os.Exit(1) - } - defer wavWriter.Close() - - formatTypeIn := BitsToType(int(inputFormat.BitsPerSample)) - if inputFormat.AudioFormat == wav.AudioFormatIEEEFloat { - formatTypeIn = malgo.FormatF32 - } - - formatTypeOut := BitsToType(outputFormat.BitsPerSample) - if outputFormat.AudioFormat == wav.AudioFormatIEEEFloat { - formatTypeOut = malgo.FormatF32 - } - +func convert(reader io.Reader, inputFormat PCMFormat, writer io.Writer, outputFormat PCMFormat) error { config := malgo.ConverterConfig{ - FormatIn: formatTypeIn, - FormatOut: formatTypeOut, - ChannelsIn: inputFormat.NumChannels, - ChannelsOut: outputFormat.NumChannels, + FormatIn: inputFormat.Type, + FormatOut: outputFormat.Type, + ChannelsIn: inputFormat.Channels, + ChannelsOut: outputFormat.Channels, SampleRateIn: inputFormat.SampleRate, SampleRateOut: outputFormat.SampleRate, Resampling: malgo.ResampleConfig{ @@ -184,9 +27,9 @@ func main() { } converter, err := malgo.InitConverter(config) if err != nil { - fmt.Print(err) - os.Exit(-1) + return err } + defer converter.Uninit() inFrameSize := malgo.FrameSizeInBytes(config.FormatIn, config.ChannelsIn) outFrameSize := malgo.FrameSizeInBytes(config.FormatOut, config.ChannelsOut) @@ -197,20 +40,20 @@ func main() { outBuffer := make([]byte, outFrameSize*expectFrames) for { - n, err := w.Read(inBuffer) + n, err := reader.Read(inBuffer) if err != nil { - break + return err } readFrameCount := n / inFrameSize _, outFrameCount, err := converter.ProcessFrames(inBuffer, readFrameCount, outBuffer, expectFrames) if err != nil { - fmt.Print(err) - continue - } else { - wavWriter.Write(outBuffer[:outFrameCount*outFrameSize]) + return err } - } - converter.Uninit() + _, err = writer.Write(outBuffer[:outFrameCount*outFrameSize]) + if err != nil { + return err + } + } }