diff --git a/audio.go b/audio.go index 2860a4b..93c96de 100644 --- a/audio.go +++ b/audio.go @@ -1,5 +1,12 @@ package sointu +import ( + "bytes" + "encoding/binary" + "fmt" + "math" +) + // 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 @@ -22,3 +29,107 @@ type 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 +} diff --git a/audioexport.go b/audioexport.go deleted file mode 100644 index b853552..0000000 --- a/audioexport.go +++ /dev/null @@ -1,112 +0,0 @@ -package sointu - -import ( - "bytes" - "encoding/binary" - "fmt" - "math" -) - -// 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 Wav(buffer AudioBuffer, pcm16 bool) ([]byte, error) { - buf := new(bytes.Buffer) - wavHeader(len(buffer)*2, pcm16, buf) - err := rawToBuffer(buffer, 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 Raw(buffer AudioBuffer, pcm16 bool) ([]byte, error) { - buf := new(bytes.Buffer) - err := rawToBuffer(buffer, pcm16, buf) - if err != nil { - return nil, fmt.Errorf("Raw failed: %v", err) - } - return buf.Bytes(), nil -} - -func rawToBuffer(data AudioBuffer, 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 -} diff --git a/cmd/sointu-play/main.go b/cmd/sointu-play/main.go index 018e779..fce0aa7 100644 --- a/cmd/sointu-play/main.go +++ b/cmd/sointu-play/main.go @@ -100,7 +100,7 @@ func main() { } } if *rawOut { - raw, err := sointu.Raw(buffer, *pcm) + raw, err := buffer.Raw(*pcm) if err != nil { return fmt.Errorf("could not generate .raw file: %v", err) } @@ -109,7 +109,7 @@ func main() { } } if *wavOut { - wav, err := sointu.Wav(buffer, *pcm) + wav, err := buffer.Wav(*pcm) if err != nil { return fmt.Errorf("could not generate .wav file: %v", err) } diff --git a/tracker/gioui/files.go b/tracker/gioui/files.go index df25d18..643de0f 100644 --- a/tracker/gioui/files.go +++ b/tracker/gioui/files.go @@ -145,7 +145,7 @@ func (t *Tracker) exportWav(w io.WriteCloser, pcm16 bool) { t.Alert.Update(fmt.Sprintf("Error rendering the song during export: %v", err), Error, time.Second*3) return } - buffer, err := sointu.Wav(data, pcm16) + buffer, err := data.Wav(pcm16) if err != nil { t.Alert.Update(fmt.Sprintf("Error converting to .wav: %v", err), Error, time.Second*3) return