diff --git a/mixing/mixbuffer.go b/mixing/mixbuffer.go index 1621846..bf0eaad 100644 --- a/mixing/mixbuffer.go +++ b/mixing/mixbuffer.go @@ -2,7 +2,6 @@ package mixing import ( "bytes" - "encoding/binary" "time" "github.com/gotracker/gomixing/sampling" @@ -63,15 +62,14 @@ func (m *MixBuffer) Add(pos int, rhs *MixBuffer, volMtx volume.Matrix) { // ToRenderData converts a mixbuffer into a byte stream intended to be // output to the output sound device -func (m *MixBuffer) ToRenderData(samples int, bitsPerSample int, channels int, mixerVolume volume.Volume) []byte { +func (m *MixBuffer) ToRenderData(samples int, channels int, mixerVolume volume.Volume, formatter sampling.Formatter) []byte { writer := &bytes.Buffer{} - writer.Grow(samples * ((bitsPerSample + 7) / 8) * channels) + writer.Grow(samples * ((formatter.Size() + 7) / 8) * channels) for _, samp := range *m { buf := samp.Apply(mixerVolume) d := buf.ToChannels(channels) for i := 0; i < channels; i++ { - val := d.StaticMatrix[i].ToSample(bitsPerSample) - _ = binary.Write(writer, binary.LittleEndian, val) // lint + _ = formatter.Write(writer, d.StaticMatrix[i]) // lint } } return writer.Bytes() @@ -96,7 +94,7 @@ func (m *MixBuffer) ToIntStream(outputChannels int, samples int, bitsPerSample i // ToRenderDataWithBufs converts a mixbuffer into a byte stream intended to be // output to the output sound device -func (m *MixBuffer) ToRenderDataWithBufs(outBuffers [][]byte, samples int, bitsPerSample int, mixerVolume volume.Volume) { +func (m *MixBuffer) ToRenderDataWithBufs(outBuffers [][]byte, samples int, mixerVolume volume.Volume, formatter sampling.Formatter) { pos := 0 onum := 0 out := outBuffers[onum] @@ -111,22 +109,8 @@ func (m *MixBuffer) ToRenderDataWithBufs(outBuffers [][]byte, samples int, bitsP out = outBuffers[onum] pos = 0 } - val := buf.StaticMatrix[c].ToSample(bitsPerSample) - switch d := val.(type) { - case int8: - out[pos] = uint8(d) - pos++ - case int16: - binary.LittleEndian.PutUint16(out[pos:], uint16(d)) - pos += 2 - case int32: - binary.LittleEndian.PutUint32(out[pos:], uint32(d)) - pos += 4 - default: - writer := &bytes.Buffer{} - _ = binary.Write(writer, binary.LittleEndian, val) // lint - pos += copy(out[pos:], writer.Bytes()) - } + _ = formatter.WriteAt(out, int64(pos), buf.StaticMatrix[c]) // lint + pos += formatter.Size() } } } diff --git a/mixing/mixer.go b/mixing/mixer.go index 2fc521b..fc632f1 100644 --- a/mixing/mixer.go +++ b/mixing/mixer.go @@ -1,6 +1,9 @@ package mixing -import "github.com/gotracker/gomixing/volume" +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" +) // Mixer is a manager for mixing multiple single- and multi-channel samples into a single multi-channel output stream type Mixer struct { @@ -10,7 +13,7 @@ type Mixer struct { // NewMixBuffer returns a mixer buffer with a number of channels // of preallocated sample data -func (m *Mixer) NewMixBuffer(samples int) MixBuffer { +func (m Mixer) NewMixBuffer(samples int) MixBuffer { return make(MixBuffer, samples) } @@ -21,8 +24,9 @@ func GetDefaultMixerVolume(numMixedChannels int) volume.Volume { } // Flatten will to a final saturation mix of all the row's channel data into a single output buffer -func (m *Mixer) Flatten(panmixer PanMixer, samplesLen int, row []ChannelData, mixerVolume volume.Volume) []byte { +func (m Mixer) Flatten(panmixer PanMixer, samplesLen int, row []ChannelData, mixerVolume volume.Volume, sampleFormat sampling.Format) []byte { data := m.NewMixBuffer(samplesLen) + formatter := sampling.GetFormatter(sampleFormat) for _, rdata := range row { for _, cdata := range rdata { if cdata.Flush != nil { @@ -34,12 +38,12 @@ func (m *Mixer) Flatten(panmixer PanMixer, samplesLen int, row []ChannelData, mi } } } - return data.ToRenderData(samplesLen, m.BitsPerSample, m.Channels, mixerVolume) + return data.ToRenderData(samplesLen, m.Channels, mixerVolume, formatter) } // FlattenToInts runs a flatten on the channel data into separate channel data of int32 variety // these int32s still respect the BitsPerSample size -func (m *Mixer) FlattenToInts(panmixer PanMixer, samplesLen int, row []ChannelData, mixerVolume volume.Volume) [][]int32 { +func (m Mixer) FlattenToInts(panmixer PanMixer, samplesLen int, row []ChannelData, mixerVolume volume.Volume) [][]int32 { data := m.NewMixBuffer(samplesLen) for _, rdata := range row { for _, cdata := range rdata { @@ -56,8 +60,9 @@ func (m *Mixer) FlattenToInts(panmixer PanMixer, samplesLen int, row []ChannelDa } // FlattenTo will to a final saturation mix of all the row's channel data into a single output buffer -func (m *Mixer) FlattenTo(resultBuffers [][]byte, panmixer PanMixer, samplesLen int, row []ChannelData, mixerVolume volume.Volume) { +func (m Mixer) FlattenTo(resultBuffers [][]byte, panmixer PanMixer, samplesLen int, row []ChannelData, mixerVolume volume.Volume, sampleFormat sampling.Format) { data := m.NewMixBuffer(samplesLen) + formatter := sampling.GetFormatter(sampleFormat) for _, rdata := range row { for _, cdata := range rdata { if cdata.Flush != nil { @@ -69,5 +74,5 @@ func (m *Mixer) FlattenTo(resultBuffers [][]byte, panmixer PanMixer, samplesLen } } } - data.ToRenderDataWithBufs(resultBuffers, samplesLen, m.BitsPerSample, mixerVolume) + data.ToRenderDataWithBufs(resultBuffers, samplesLen, mixerVolume, formatter) } diff --git a/sampling/format.go b/sampling/format.go new file mode 100644 index 0000000..72b9902 --- /dev/null +++ b/sampling/format.go @@ -0,0 +1,27 @@ +package sampling + +// Format is the format of the sample data +type Format uint8 + +const ( + // Format8BitUnsigned is for unsigned 8-bit data + Format8BitUnsigned = Format(iota) + // Format8BitSigned is for signed 8-bit data + Format8BitSigned + // Format16BitLEUnsigned is for unsigned, little-endian, 16-bit data + Format16BitLEUnsigned + // Format16BitLESigned is for signed, little-endian, 16-bit data + Format16BitLESigned + // Format16BitBEUnsigned is for unsigned, big-endian, 16-bit data + Format16BitBEUnsigned + // Format16BitBESigned is for signed, big-endian, 16-bit data + Format16BitBESigned + // Format32BitLEFloat is for little-endian, 32-bit floating-point data + Format32BitLEFloat + // Format32BitBEFloat is for big-endian, 32-bit floating-point data + Format32BitBEFloat + // Format64BitLEFloat is for little-endian, 64-bit floating-point data + Format64BitLEFloat + // Format64BitBEFloat is for big-endian, 64-bit floating-point data + Format64BitBEFloat +) diff --git a/sampling/format_bit16.go b/sampling/format_bit16.go new file mode 100644 index 0000000..6cd27dc --- /dev/null +++ b/sampling/format_bit16.go @@ -0,0 +1,116 @@ +package sampling + +import ( + "encoding/binary" + "io" + + "github.com/gotracker/gomixing/volume" +) + +const ( + cSample16BitDataCoeff = 0x8000 + cSample16BitVolumeCoeff = volume.Volume(1) / cSample16BitDataCoeff + cSample16BitBytes = 2 +) + +// Sample16BitSigned is a signed 16-bit sample +type Sample16BitSigned struct { + byteOrder binary.ByteOrder +} + +// Volume returns the volume value for the sample +func (Sample16BitSigned) volume(v int16) volume.Volume { + return volume.Volume(v) * cSample16BitVolumeCoeff +} + +// fromVolume returns the volume value for the sample +func (Sample16BitSigned) fromVolume(v volume.Volume) int16 { + return int16(v.ToIntSample(16)) +} + +// Size returns the size of the sample in bytes +func (Sample16BitSigned) Size() int { + return cSample16BitBytes +} + +// ReadAt reads a value from the reader provided in the byte order provided +func (s Sample16BitSigned) ReadAt(data []byte, ofs int64) (volume.Volume, error) { + if len(data) <= int(ofs)+(cSample16BitBytes-1) { + return 0, io.EOF + } + if ofs < 0 { + ofs = 0 + } + + v := int16(s.byteOrder.Uint16(data[ofs:])) + return s.volume(v), nil +} + +// WriteAt writes a value to the slice provided in the byte order provided +func (s Sample16BitSigned) WriteAt(data []byte, ofs int64, v volume.Volume) error { + if len(data) <= int(ofs) { + return io.EOF + } + if ofs < 0 { + ofs = 0 + } + + s.byteOrder.PutUint16(data[ofs:], uint16(s.fromVolume(v))) + return nil +} + +// Write writes a value to the Writer provided in the byte order provided +func (s Sample16BitSigned) Write(out io.Writer, v volume.Volume) error { + return binary.Write(out, s.byteOrder, s.fromVolume(v)) +} + +// Sample16BitUnsigned is an unsigned 16-bit sample +type Sample16BitUnsigned struct { + byteOrder binary.ByteOrder +} + +// Volume returns the volume value for the sample +func (Sample16BitUnsigned) volume(v uint16) volume.Volume { + return volume.Volume(int16(v-cSample16BitDataCoeff)) * cSample16BitVolumeCoeff +} + +// fromVolume returns the volume value for the sample +func (Sample16BitUnsigned) fromVolume(v volume.Volume) uint16 { + return uint16(v.ToUintSample(16)) +} + +// Size returns the size of the sample in bytes +func (Sample16BitUnsigned) Size() int { + return cSample16BitBytes +} + +// ReadAt reads a value from the reader provided in the byte order provided +func (s Sample16BitUnsigned) ReadAt(data []byte, ofs int64) (volume.Volume, error) { + if len(data) <= int(ofs)+(cSample16BitBytes-1) { + return 0, io.EOF + } + if ofs < 0 { + ofs = 0 + } + + v := uint16(s.byteOrder.Uint16(data[ofs:])) + return s.volume(v), nil +} + +// WriteAt writes a value to the slice provided in the byte order provided +func (s Sample16BitUnsigned) WriteAt(data []byte, ofs int64, v volume.Volume) error { + if len(data) <= int(ofs) { + return io.EOF + } + if ofs < 0 { + ofs = 0 + } + + s.byteOrder.PutUint16(data[ofs:], s.fromVolume(v)) + return nil +} + +// Write writes a value to the Writer provided in the byte order provided +func (s Sample16BitUnsigned) Write(out io.Writer, v volume.Volume) error { + return binary.Write(out, s.byteOrder, s.fromVolume(v)) +} diff --git a/sampling/format_bit32float.go b/sampling/format_bit32float.go new file mode 100644 index 0000000..c25f59e --- /dev/null +++ b/sampling/format_bit32float.go @@ -0,0 +1,55 @@ +package sampling + +import ( + "encoding/binary" + "io" + "math" + + "github.com/gotracker/gomixing/volume" +) + +const ( + //cSample32BitFloatVolumeCoeff = volume.Volume(1) + cSample32BitFloatBytes = 4 +) + +// Sample32BitFloat is a 32-bit floating-point sample +type Sample32BitFloat struct { + byteOrder binary.ByteOrder +} + +// Size returns the size of the sample in bytes +func (Sample32BitFloat) Size() int { + return cSample32BitFloatBytes +} + +// ReadAt reads a value from the reader provided in the byte order provided +func (s Sample32BitFloat) ReadAt(data []byte, ofs int64) (volume.Volume, error) { + if len(data) <= int(ofs)+(cSample32BitFloatBytes-1) { + return 0, io.EOF + } + if ofs < 0 { + ofs = 0 + } + + v := math.Float32frombits(s.byteOrder.Uint32(data[ofs:])) + return volume.Volume(v), nil +} + +// WriteAt writes a value to the slice provided in the byte order provided +func (s Sample32BitFloat) WriteAt(data []byte, ofs int64, v volume.Volume) error { + if len(data) <= int(ofs) { + return io.EOF + } + if ofs < 0 { + ofs = 0 + } + + s.byteOrder.PutUint32(data[ofs:], math.Float32bits(float32(v.WithOverflowProtection()))) + return nil +} + +// Write writes a value to the Writer provided in the byte order provided +func (s Sample32BitFloat) Write(out io.Writer, v volume.Volume) error { + return binary.Write(out, s.byteOrder, math.Float32bits(float32(v.WithOverflowProtection()))) +} diff --git a/sampling/format_bit64float.go b/sampling/format_bit64float.go new file mode 100644 index 0000000..9030fb7 --- /dev/null +++ b/sampling/format_bit64float.go @@ -0,0 +1,55 @@ +package sampling + +import ( + "encoding/binary" + "io" + "math" + + "github.com/gotracker/gomixing/volume" +) + +const ( + //cSample64BitFloatVolumeCoeff = volume.Volume(1) + cSample64BitFloatBytes = 8 +) + +// Sample64BitFloat is a 64-bit floating-point sample +type Sample64BitFloat struct { + byteOrder binary.ByteOrder +} + +// Size returns the size of the sample in bytes +func (Sample64BitFloat) Size() int { + return cSample64BitFloatBytes +} + +// ReadAt reads a value from the reader provided in the byte order provided +func (s Sample64BitFloat) ReadAt(data []byte, ofs int64) (volume.Volume, error) { + if len(data) <= int(ofs)+(cSample64BitFloatBytes-1) { + return 0, io.EOF + } + if ofs < 0 { + ofs = 0 + } + + f := math.Float64frombits(s.byteOrder.Uint64(data[ofs:])) + return volume.Volume(f), nil +} + +// WriteAt writes a value to the slice provided in the byte order provided +func (s Sample64BitFloat) WriteAt(data []byte, ofs int64, v volume.Volume) error { + if len(data) <= int(ofs) { + return io.EOF + } + if ofs < 0 { + ofs = 0 + } + + s.byteOrder.PutUint64(data[ofs:], math.Float64bits(v.WithOverflowProtection())) + return nil +} + +// Write writes a value to the Writer provided in the byte order provided +func (s Sample64BitFloat) Write(out io.Writer, v volume.Volume) error { + return binary.Write(out, s.byteOrder, math.Float64bits(v.WithOverflowProtection())) +} diff --git a/sampling/format_bit8.go b/sampling/format_bit8.go new file mode 100644 index 0000000..6b2adee --- /dev/null +++ b/sampling/format_bit8.go @@ -0,0 +1,112 @@ +package sampling + +import ( + "encoding/binary" + "io" + + "github.com/gotracker/gomixing/volume" +) + +const ( + cSample8BitDataCoeff = 0x80 + cSample8BitVolumeCoeff = volume.Volume(1) / cSample8BitDataCoeff + cSample8BitBytes = 1 +) + +// Sample8BitSigned is a signed 8-bit sample +type Sample8BitSigned struct{} + +// toVolume returns the volume value for the sample +func (Sample8BitSigned) toVolume(v int8) volume.Volume { + return volume.Volume(v) * cSample8BitVolumeCoeff +} + +// fromVolume returns the volume value for the sample +func (Sample8BitSigned) fromVolume(v volume.Volume) int8 { + return int8(v.ToIntSample(8)) +} + +// Size returns the size of the sample in bytes +func (Sample8BitSigned) Size() int { + return cSample8BitBytes +} + +// ReadAt reads a value from the reader provided in the byte order provided +func (s Sample8BitSigned) ReadAt(data []byte, ofs int64) (volume.Volume, error) { + if len(data) <= int(ofs) { + return 0, io.EOF + } + if ofs < 0 { + ofs = 0 + } + + v := int8(data[ofs]) + return s.toVolume(v), nil +} + +// WriteAt writes a value to the slice provided in the byte order provided +func (s Sample8BitSigned) WriteAt(data []byte, ofs int64, v volume.Volume) error { + if len(data) <= int(ofs) { + return io.EOF + } + if ofs < 0 { + ofs = 0 + } + + data[ofs] = uint8(s.fromVolume(v)) + return nil +} + +// Write writes a value to the Writer provided in the byte order provided +func (s Sample8BitSigned) Write(out io.Writer, v volume.Volume) error { + return binary.Write(out, binary.LittleEndian, s.fromVolume(v)) +} + +// Sample8BitUnsigned is an unsigned 8-bit sample +type Sample8BitUnsigned struct{} + +// toVolume returns the volume value for the sample +func (Sample8BitUnsigned) toVolume(v uint8) volume.Volume { + return volume.Volume(int8(v-uint8(cSample8BitDataCoeff))) * cSample8BitVolumeCoeff +} + +// fromVolume returns the volume value for the sample +func (Sample8BitUnsigned) fromVolume(v volume.Volume) uint8 { + return uint8(v.ToUintSample(8)) +} + +// Size returns the size of the sample in bytes +func (Sample8BitUnsigned) Size() int { + return cSample8BitBytes +} + +// ReadAt reads a value from the slice provided in the byte order provided +func (s Sample8BitUnsigned) ReadAt(data []byte, ofs int64) (volume.Volume, error) { + if len(data) <= int(ofs) { + return 0, io.EOF + } + if ofs < 0 { + ofs = 0 + } + + v := uint8(data[ofs]) + return s.toVolume(v), nil +} + +// WriteAt writes a value to the slice provided in the byte order provided +func (s Sample8BitUnsigned) WriteAt(data []byte, ofs int64, v volume.Volume) error { + if len(data) <= int(ofs) { + return io.EOF + } + if ofs < 0 { + ofs = 0 + } + + data[ofs] = s.fromVolume(v) + return nil +} + +// Write writes a value to the Writer provided in the byte order provided +func (s Sample8BitUnsigned) Write(out io.Writer, v volume.Volume) error { + return binary.Write(out, binary.LittleEndian, s.fromVolume(v)) +} diff --git a/sampling/formatter.go b/sampling/formatter.go new file mode 100644 index 0000000..7c538ff --- /dev/null +++ b/sampling/formatter.go @@ -0,0 +1,52 @@ +package sampling + +import ( + "encoding/binary" + "io" + + "github.com/gotracker/gomixing/volume" +) + +type Formatter interface { + Size() int + ReadAt(data []byte, ofs int64) (volume.Volume, error) + WriteAt(data []byte, ofs int64, v volume.Volume) error + Write(out io.Writer, v volume.Volume) error +} + +func GetFormatter(format Format) Formatter { + switch format { + default: + return nil + case Format8BitUnsigned: + // Format8BitUnsigned is for unsigned 8-bit data + return Sample8BitUnsigned{} + case Format8BitSigned: + // Format8BitSigned is for signed 8-bit data + return Sample8BitSigned{} + case Format16BitLEUnsigned: + // Format16BitLEUnsigned is for unsigned, little-endian, 16-bit data + return Sample16BitUnsigned{byteOrder: binary.LittleEndian} + case Format16BitLESigned: + // Format16BitLESigned is for signed, little-endian, 16-bit data + return Sample16BitSigned{byteOrder: binary.LittleEndian} + case Format16BitBEUnsigned: + // Format16BitBEUnsigned is for unsigned, big-endian, 16-bit data + return Sample16BitUnsigned{byteOrder: binary.BigEndian} + case Format16BitBESigned: + // Format16BitBESigned is for signed, big-endian, 16-bit data + return Sample16BitSigned{byteOrder: binary.BigEndian} + case Format32BitLEFloat: + // Format32BitLEFloat is for little-endian, 32-bit floating-point data + return Sample32BitFloat{byteOrder: binary.LittleEndian} + case Format32BitBEFloat: + // Format32BitBEFloat is for big-endian, 32-bit floating-point data + return Sample32BitFloat{byteOrder: binary.BigEndian} + case Format64BitLEFloat: + // Format64BitLEFloat is for little-endian, 64-bit floating-point data + return Sample64BitFloat{byteOrder: binary.LittleEndian} + case Format64BitBEFloat: + // Format64BitBEFloat is for big-endian, 64-bit floating-point data + return Sample64BitFloat{byteOrder: binary.BigEndian} + } +} diff --git a/volume/volume.go b/volume/volume.go index 6b3ba87..09eb71b 100644 --- a/volume/volume.go +++ b/volume/volume.go @@ -21,7 +21,7 @@ type Int24 struct { // ToSample returns a volume as a typed value supporting the bits per sample provided func (v Volume) ToSample(bitsPerSample int) interface{} { - val := v.withOverflowProtection() + val := v.WithOverflowProtection() switch bitsPerSample { case 8: return int8(val * 128.0) @@ -38,7 +38,7 @@ func (v Volume) ToSample(bitsPerSample int) interface{} { // ToIntSample returns a volume as an int32 value ranged to the bits per sample provided func (v Volume) ToIntSample(bitsPerSample int) int32 { - val := v.withOverflowProtection() + val := v.WithOverflowProtection() switch bitsPerSample { case 8: return int32(val * 128.0) @@ -52,6 +52,22 @@ func (v Volume) ToIntSample(bitsPerSample int) int32 { return 0 } +// ToUintSample returns a volume as a uint32 value ranged to the bits per sample provided +func (v Volume) ToUintSample(bitsPerSample int) uint32 { + val := v.WithOverflowProtection() + switch bitsPerSample { + case 8: + return uint32(val*float64(math.MaxInt8)) ^ 0x80 + case 16: + return uint32(val*float64(math.MaxInt16)) ^ 0x8000 + case 24: + return uint32(val*8388608.0) ^ 0x800000 + case 32: + return uint32(val*float64(math.MaxInt32)) ^ 0x80000000 + } + return 0 +} + // Apply multiplies the volume to 1 sample, then returns the results func (v Volume) ApplySingle(samp Volume) Volume { return samp * v @@ -66,7 +82,7 @@ func (v Volume) ApplyMultiple(samp []Volume) []Volume { return vols } -func (v Volume) withOverflowProtection() float64 { +func (v Volume) WithOverflowProtection() float64 { val := float64(v) if math.Abs(val) <= 1.0 { // likely case