mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
feat(tracker): add menu item to export .wav
Also refactor the common functions for .wav export into base package so that both sointu-play and tracker can use same functions.
This commit is contained in:
parent
7893c1d1ed
commit
1b4f1a8c5e
101
audioexport.go
Normal file
101
audioexport.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package sointu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Wav(buffer []float32, pcm16 bool) ([]byte, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
wavHeader(len(buffer), pcm16, buf)
|
||||||
|
err := rawToBuffer(buffer, pcm16, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Wav failed: %v", err)
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Raw(buffer []float32, 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 []float32, pcm16 bool, buf *bytes.Buffer) error {
|
||||||
|
var err error
|
||||||
|
if pcm16 {
|
||||||
|
int16data := make([]int16, len(data))
|
||||||
|
for i, v := range data {
|
||||||
|
int16data[i] = int16(clamp(int(v*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
|
||||||
|
}
|
@ -1,13 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -111,38 +108,22 @@ func main() {
|
|||||||
return fmt.Errorf("error playing: %v", err)
|
return fmt.Errorf("error playing: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var data interface{}
|
|
||||||
data = buffer
|
|
||||||
if *pcm {
|
|
||||||
int16buffer := make([]int16, len(buffer))
|
|
||||||
for i, v := range buffer {
|
|
||||||
int16buffer[i] = int16(clamp(int(v*math.MaxInt16), math.MinInt16, math.MaxInt16))
|
|
||||||
}
|
|
||||||
data = int16buffer
|
|
||||||
}
|
|
||||||
if *rawOut {
|
if *rawOut {
|
||||||
buf := new(bytes.Buffer)
|
raw, err := sointu.Raw(buffer, *pcm)
|
||||||
err := binary.Write(buf, binary.LittleEndian, data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
|
return fmt.Errorf("could not generate .raw file: %v", err)
|
||||||
}
|
}
|
||||||
if err := output(".raw", buf.Bytes()); err != nil {
|
if err := output(".raw", raw); err != nil {
|
||||||
return fmt.Errorf("error outputting raw audio file: %v", err)
|
return fmt.Errorf("error outputting .raw file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *wavOut {
|
if *wavOut {
|
||||||
buf := new(bytes.Buffer)
|
wav, err := sointu.Wav(buffer, *pcm)
|
||||||
header := createWavHeader(len(buffer), *pcm)
|
|
||||||
err := binary.Write(buf, binary.LittleEndian, header)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not binary write header to binary buffer: %v", err)
|
return fmt.Errorf("could not generate .wav file: %v", err)
|
||||||
}
|
}
|
||||||
err = binary.Write(buf, binary.LittleEndian, data)
|
if err := output(".wav", wav); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("error outputting .wav file: %v", err)
|
||||||
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
|
|
||||||
}
|
|
||||||
if err := output(".wav", buf.Bytes()); err != nil {
|
|
||||||
return fmt.Errorf("error outputting wav audio file: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -185,57 +166,3 @@ func printUsage() {
|
|||||||
fmt.Fprintf(os.Stderr, "Sointu command line utility for playing .asm/.json song files.\nUsage: %s [flags] [path ...]\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Sointu command line utility for playing .asm/.json song files.\nUsage: %s [flags] [path ...]\n", os.Args[0])
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWavHeader(bufferLength int, pcm bool) []byte {
|
|
||||||
// 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 pcm {
|
|
||||||
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 := new(bytes.Buffer)
|
|
||||||
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))
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func clamp(value, min, max int) int {
|
|
||||||
if value < min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
if value > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@ -82,3 +83,33 @@ func (t *Tracker) saveSong(filename string) bool {
|
|||||||
t.SetChangedSinceSave(false)
|
t.SetChangedSinceSave(false)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) exportWav(pcm16 bool) {
|
||||||
|
filename, err := dialog.File().Filter(".wav file", "wav").Title("Export .wav").Save()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var extension = filepath.Ext(filename)
|
||||||
|
if extension == "" {
|
||||||
|
filename = filename + ".wav"
|
||||||
|
}
|
||||||
|
synth, err := t.synthService.Compile(t.Song().Patch)
|
||||||
|
if err != nil {
|
||||||
|
t.Alert.Update(fmt.Sprintf("Error compiling the patch during export: %v", err), Error, time.Second*3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
synth.Release(i)
|
||||||
|
}
|
||||||
|
data, _, err := sointu.Play(synth, t.Song()) // render the song to calculate its length
|
||||||
|
if err != nil {
|
||||||
|
t.Alert.Update(fmt.Sprintf("Error rendering the song during export: %v", err), Error, time.Second*3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer, err := sointu.Wav(data, pcm16)
|
||||||
|
if err != nil {
|
||||||
|
t.Alert.Update(fmt.Sprintf("Error converting to .wav: %v", err), Error, time.Second*3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ioutil.WriteFile(filename, buffer, 0644)
|
||||||
|
}
|
||||||
|
@ -39,6 +39,22 @@ func (t *Tracker) Layout(gtx layout.Context) {
|
|||||||
for t.ConfirmSongDialog.BtnCancel.Clicked() {
|
for t.ConfirmSongDialog.BtnCancel.Clicked() {
|
||||||
t.ConfirmSongDialog.Visible = false
|
t.ConfirmSongDialog.Visible = false
|
||||||
}
|
}
|
||||||
|
dstyle = ConfirmDialog(t.Theme, t.WaveTypeDialog, "Export .wav in int16 or float32 sample format?")
|
||||||
|
dstyle.ShowAlt = true
|
||||||
|
dstyle.OkStyle.Text = "Int16"
|
||||||
|
dstyle.AltStyle.Text = "Float32"
|
||||||
|
dstyle.Layout(gtx)
|
||||||
|
for t.WaveTypeDialog.BtnOk.Clicked() {
|
||||||
|
t.exportWav(true)
|
||||||
|
t.WaveTypeDialog.Visible = false
|
||||||
|
}
|
||||||
|
for t.WaveTypeDialog.BtnAlt.Clicked() {
|
||||||
|
t.exportWav(false)
|
||||||
|
t.WaveTypeDialog.Visible = false
|
||||||
|
}
|
||||||
|
for t.WaveTypeDialog.BtnCancel.Clicked() {
|
||||||
|
t.WaveTypeDialog.Visible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) confirmedSongAction() {
|
func (t *Tracker) confirmedSongAction() {
|
||||||
|
@ -61,6 +61,8 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
|
|||||||
case 3:
|
case 3:
|
||||||
t.SaveSongAsFile()
|
t.SaveSongAsFile()
|
||||||
case 4:
|
case 4:
|
||||||
|
t.WaveTypeDialog.Visible = true
|
||||||
|
case 5:
|
||||||
t.TryQuit()
|
t.TryQuit()
|
||||||
}
|
}
|
||||||
clickedItem, hasClicked = t.Menus[0].Clicked()
|
clickedItem, hasClicked = t.Menus[0].Clicked()
|
||||||
@ -93,6 +95,7 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
|
|||||||
MenuItem{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
|
MenuItem{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
|
||||||
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
|
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
|
||||||
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song As..."},
|
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song As..."},
|
||||||
|
MenuItem{IconBytes: icons.ImageAudiotrack, Text: "Export Wav..."},
|
||||||
MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit"},
|
MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit"},
|
||||||
)),
|
)),
|
||||||
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(160),
|
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(160),
|
||||||
|
@ -70,6 +70,7 @@ type Tracker struct {
|
|||||||
PatternOrderScrollBar *ScrollBar
|
PatternOrderScrollBar *ScrollBar
|
||||||
ConfirmInstrDelete *Dialog
|
ConfirmInstrDelete *Dialog
|
||||||
ConfirmSongDialog *Dialog
|
ConfirmSongDialog *Dialog
|
||||||
|
WaveTypeDialog *Dialog
|
||||||
ConfirmSongActionType int
|
ConfirmSongActionType int
|
||||||
window *app.Window
|
window *app.Window
|
||||||
|
|
||||||
@ -82,6 +83,7 @@ type Tracker struct {
|
|||||||
errorChannel chan error
|
errorChannel chan error
|
||||||
quitted bool
|
quitted bool
|
||||||
audioContext sointu.AudioContext
|
audioContext sointu.AudioContext
|
||||||
|
synthService sointu.SynthService
|
||||||
|
|
||||||
*tracker.Model
|
*tracker.Model
|
||||||
}
|
}
|
||||||
@ -165,8 +167,10 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn
|
|||||||
PatternOrderScrollBar: &ScrollBar{Axis: layout.Vertical},
|
PatternOrderScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||||
ConfirmInstrDelete: new(Dialog),
|
ConfirmInstrDelete: new(Dialog),
|
||||||
ConfirmSongDialog: new(Dialog),
|
ConfirmSongDialog: new(Dialog),
|
||||||
|
WaveTypeDialog: new(Dialog),
|
||||||
errorChannel: make(chan error, 32),
|
errorChannel: make(chan error, 32),
|
||||||
window: window,
|
window: window,
|
||||||
|
synthService: synthService,
|
||||||
}
|
}
|
||||||
t.Model = tracker.NewModel()
|
t.Model = tracker.NewModel()
|
||||||
vuBufferObserver := make(chan []float32)
|
vuBufferObserver := make(chan []float32)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user