├── cmd ├── .gitignore ├── MidiSound │ ├── out.bin │ └── main.go ├── tuningfork │ ├── out.bin │ └── readme.md ├── extractbrk │ ├── extractbrk │ └── main.go ├── sine │ ├── readme.md │ ├── main.go │ └── out.txt └── inspect │ └── main.go ├── examples ├── stereopan │ ├── testpan.brk │ ├── pan.brk │ ├── readme.md │ └── main.go ├── table_oscillator │ ├── amps.brk │ ├── freqs.brk │ ├── table_oscillator │ └── main.go ├── adsr │ ├── readme.md │ └── main.go ├── spectral │ └── main.go ├── amplitude │ └── main.go ├── wavetable │ └── main.go └── oscillator │ └── main.go ├── go.mod ├── wave ├── golden │ └── maybe-next-time.wav ├── readme.md ├── writer_test.go ├── utils.go ├── reader_test.go ├── utils_test.go ├── writer.go ├── wavefile.go └── reader.go ├── .travis.yml ├── .gitignore ├── readme.md ├── LICENSE ├── math └── dft.go ├── synthesizer ├── wavetable.go ├── filters_test.go ├── filters.go ├── guardtable.go ├── synth_test.go ├── oscil.go ├── lookuposcil.go ├── synth.go └── constants.go └── breakpoint ├── breakpoint_test.go └── breakpoint.go /cmd/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/stereopan/testpan.brk: -------------------------------------------------------------------------------- 1 | 0:1 2 | 1:-1 3 | -------------------------------------------------------------------------------- /examples/table_oscillator/amps.brk: -------------------------------------------------------------------------------- 1 | 0:1 2 | 10:1 3 | 0:1 4 | 5 | -------------------------------------------------------------------------------- /examples/table_oscillator/freqs.brk: -------------------------------------------------------------------------------- 1 | 0:440 2 | 5:200 3 | 10:600 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DylanMeeus/GoAudio 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /examples/stereopan/pan.brk: -------------------------------------------------------------------------------- 1 | 0:-1 2 | 2:1 3 | 4:-1 4 | 6:1 5 | 8:-1 6 | 10:1 7 | -------------------------------------------------------------------------------- /cmd/MidiSound/out.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanMeeus/GoAudio/HEAD/cmd/MidiSound/out.bin -------------------------------------------------------------------------------- /cmd/tuningfork/out.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanMeeus/GoAudio/HEAD/cmd/tuningfork/out.bin -------------------------------------------------------------------------------- /cmd/extractbrk/extractbrk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanMeeus/GoAudio/HEAD/cmd/extractbrk/extractbrk -------------------------------------------------------------------------------- /wave/golden/maybe-next-time.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanMeeus/GoAudio/HEAD/wave/golden/maybe-next-time.wav -------------------------------------------------------------------------------- /examples/table_oscillator/table_oscillator: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DylanMeeus/GoAudio/HEAD/examples/table_oscillator/table_oscillator -------------------------------------------------------------------------------- /examples/adsr/readme.md: -------------------------------------------------------------------------------- 1 | # ADSR 2 | 3 | This example demonstrates the use of an ADSR (Attack Decay Sustain Release) envelope applies to a 4 | chosen signal. 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13" 5 | - "1.14" 6 | - "1.15" 7 | - "1.16" 8 | 9 | script: 10 | - cd wave && go test ./... 11 | 12 | -------------------------------------------------------------------------------- /examples/stereopan/readme.md: -------------------------------------------------------------------------------- 1 | # stereo pan 2 | 3 | This applies a set stereo panning to a mono input file. Turning it into a stereo output file with 4 | left/right panning applied per the given panfactor (-1..1) 5 | -------------------------------------------------------------------------------- /cmd/sine/readme.md: -------------------------------------------------------------------------------- 1 | # Sine wave 2 | 3 | Small program to generate float values for a sine wave 4 | 5 | usage: 6 | 7 | ``` 8 | go run main.go > out.txt 9 | // in gnuplot 10 | plot "out.txt" using($1) with lines 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /wave/readme.md: -------------------------------------------------------------------------------- 1 | # GoWave 2 | 3 | This library is intended to help me with converting C code from "The Audio Programming Book" into Go 4 | code. The format for the wave file can be found: 5 | https://web.archive.org/web/20141213140451/https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ 6 | 7 | -------------------------------------------------------------------------------- /examples/spectral/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | audiomath "github.com/DylanMeeus/GoAudio/math" 7 | "github.com/DylanMeeus/GoAudio/wave" 8 | "math" 9 | ) 10 | 11 | var _ = audiomath.HFFT 12 | 13 | func main() { 14 | w, err := wave.ReadWaveFile("frerejacques.wav") 15 | if err != nil { 16 | panic(err) 17 | } 18 | fmt.Println(2 * math.Pi) 19 | c128 := audiomath.FFT(w.Frames) 20 | fmt.Printf("%v\n", c128) 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Vim ### 2 | # Swap 3 | [._]*.s[a-v][a-z] 4 | !*.svg # comment out if you don't need vector files 5 | [._]*.sw[a-p] 6 | [._]s[a-rt-v][a-z] 7 | [._]ss[a-gi-z] 8 | [._]sw[a-p] 9 | 10 | # Session 11 | Session.vim 12 | Sessionx.vim 13 | 14 | # Temporary 15 | .netrwhist 16 | *~ 17 | # Auto-generated tag files 18 | tags 19 | # Persistent undo 20 | [._]*.un~ 21 | 22 | 23 | # Audio editing files 24 | *.wav 25 | 26 | # debugging files 27 | *.hex 28 | -------------------------------------------------------------------------------- /cmd/sine/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* program to create a pitch perfect (440Hz) sound */ 4 | 5 | import ( 6 | "fmt" 7 | "math" 8 | "os" 9 | ) 10 | 11 | const ( 12 | nsamps = 50 13 | ) 14 | 15 | var ( 16 | tau = math.Pi * 2 17 | ) 18 | 19 | func main() { 20 | fmt.Fprintf(os.Stderr, "generating sine wave..\n") 21 | generate() 22 | fmt.Fprintf(os.Stderr, "done") 23 | } 24 | 25 | func generate() { 26 | var angleincr float64 = tau / nsamps 27 | for i := 0; i < nsamps; i++ { 28 | samp := math.Sin(angleincr * float64(i)) 29 | fmt.Printf("%.81f\t%81.f\n", samp, samp) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/adsr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | synth "github.com/DylanMeeus/GoAudio/synthesizer" 8 | "github.com/DylanMeeus/GoAudio/wave" 9 | ) 10 | 11 | func main() { 12 | flag.Parse() 13 | 14 | osc, err := synth.NewOscillator(44100, synth.SINE) 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | sr := 44100 20 | duration := sr * 10 21 | 22 | frames := []wave.Frame{} 23 | var adsrtime int 24 | for i := 0; i < duration; i++ { 25 | value := synth.ADSR(1, 10, 1, 1, 0.7, 5, float64(sr), adsrtime) 26 | adsrtime++ 27 | frames = append(frames, wave.Frame(value*osc.Tick(440))) 28 | } 29 | 30 | wfmt := wave.NewWaveFmt(1, 1, sr, 16, nil) 31 | wave.WriteFrames(frames, wfmt, "output.wav") 32 | 33 | fmt.Println("done writing to output.wav") 34 | } 35 | -------------------------------------------------------------------------------- /cmd/tuningfork/readme.md: -------------------------------------------------------------------------------- 1 | # Tuning fork 2 | 3 | This small sample programs allows you to generate a sine-wave signal of variable duration, 4 | frequency, sample rate and amplitude. 5 | 6 | A 32bit floating-point (raw) binary file will be created, this can be listened to with ffplay / Audacity or anything else supporting these files. 7 | 8 | 9 | **Only works on LittleEndian systems, change the binary encoding to BigEndian for BigEndian 10 | systems!** 11 | 12 | An exponential decay is applied to the signal. 13 | 14 | ``` 15 | go run main.go [duration] [frequency] [samplerate] [amplitude] [outfile] 16 | go run main.go 2 440 44100 1 out.bin 17 | ``` 18 | 19 | ## ffplay 20 | 21 | Play with ffplay: 22 | 23 | ``` 24 | ffplay -f f32le -ar 44100 out.bin 25 | ``` 26 | 27 | Or visualize with: 28 | 29 | ``` 30 | ffplay -f f32le -ar 44100 -showmode 1 out.bin 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # GoAudio 🎶 2 | [![Build 3 | Status](https://travis-ci.com/DylanMeeus/GoAudio.svg?branch=master)](https://travis-ci.com/DylanMeeus/GoAudio) 4 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 5 | 6 | 7 | GoAudio is an audio processing library, currently supporting `WAVE` files, although some tools such 8 | as the synth and breakpoints are encoding-agnostic, so you could combine them with a different 9 | library for storing the data and using GoAudio only as a means to generate the waveforms. 10 | 11 | # Features 12 | 13 | - [Wave file handling](wave)(READ / WRITE Wave files) 14 | - [Synthesizer](synthesizer) - Create different waveforms using different types of oscillators 15 | - [Breakpoints](breakpoint) (create automation tracks / envelopes) 16 | 17 | 18 | # Blog 19 | 20 | If you want to know more about how this code works and what you can do with it, I write about this code and other audio related programs over on my [blog: 21 | dylanmeeus.github.io](https://dylanmeeus.github.io). 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/amplitude/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | pkg "github.com/DylanMeeus/GoAudio/wave" 7 | ) 8 | 9 | var ( 10 | input = flag.String("i", "", "input file") 11 | output = flag.String("o", "", "output file") 12 | amp = flag.Float64("a", 1.0, "amp mod factor") 13 | ) 14 | 15 | func main() { 16 | fmt.Println("Parsing wave file..") 17 | flag.Parse() 18 | infile := *input 19 | outfile := *output 20 | scale := *amp 21 | wave, err := pkg.ReadWaveFile(infile) 22 | if err != nil { 23 | panic("Could not parse wave file") 24 | } 25 | 26 | fmt.Printf("Read %v samples\n", len(wave.Frames)) 27 | 28 | // now try to write this file 29 | scaledSamples := changeAmplitude(wave.Frames, scale) 30 | if err := pkg.WriteFrames(scaledSamples, wave.WaveFmt, outfile); err != nil { 31 | panic(err) 32 | } 33 | 34 | fmt.Println("done") 35 | } 36 | 37 | func changeAmplitude(samples []pkg.Frame, scalefactor float64) []pkg.Frame { 38 | for i, s := range samples { 39 | samples[i] = pkg.Frame(float64(s) * scalefactor) 40 | } 41 | return samples 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dylan Meeus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /wave/writer_test.go: -------------------------------------------------------------------------------- 1 | package wave 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var ( 8 | rescaleFrameTests = []struct { 9 | input Frame 10 | bits int 11 | out int 12 | }{ 13 | { 14 | Frame(1), 15 | 8, 16 | 127, 17 | }, 18 | { 19 | Frame(-1), 20 | 8, 21 | -127, 22 | }, 23 | { 24 | Frame(1), 25 | 16, 26 | 32_767, 27 | }, 28 | { 29 | Frame(-1), 30 | 16, 31 | -32_767, 32 | }, 33 | } 34 | ) 35 | 36 | func TestRescaleFrames(t *testing.T) { 37 | for _, test := range rescaleFrameTests { 38 | t.Run("", func(t *testing.T) { 39 | res := rescaleFrame(test.input, test.bits) 40 | if res != test.out { 41 | t.Fatalf("expected %v, got %v", test.out, res) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | // TestWriteWave reads wave file and writes it, ensuring nothing is different between the two 48 | func TestWriteWave(t *testing.T) { 49 | goldenfile := "./golden/maybe-next-time.wav" 50 | wav, err := ReadWaveFile(goldenfile) 51 | if err != nil { 52 | t.Fatalf("Should be able to read wave file: %v", err) 53 | } 54 | 55 | if err := WriteFrames(wav.Frames, wav.WaveFmt, "output.wav"); err != nil { 56 | t.Fatalf("Should be able to write file: %v", err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /wave/utils.go: -------------------------------------------------------------------------------- 1 | package wave 2 | 3 | // utility functions for dealing with wave files. 4 | 5 | // BatchSamples batches the samples per requested timespan expressed in seconds 6 | func BatchSamples(data Wave, seconds float64) [][]Frame { 7 | if seconds == 0 { 8 | return [][]Frame{ 9 | data.Frames, 10 | } 11 | } 12 | 13 | samples := data.Frames 14 | 15 | sampleSize := int(float64(data.SampleRate*data.NumChannels) * float64(seconds)) 16 | 17 | batches := len(samples) / sampleSize 18 | if len(samples)%sampleSize != 0 { 19 | batches++ 20 | } 21 | 22 | batched := make([][]Frame, batches) // this should be round up.. 23 | for i := 0; i < len(batched); i++ { 24 | start := i * sampleSize 25 | if start > len(samples) { 26 | return batched 27 | } 28 | maxTake := i*sampleSize + sampleSize 29 | if maxTake >= len(samples)-1 { 30 | maxTake = len(samples) 31 | } 32 | subs := samples[start:maxTake] 33 | batched[i] = subs 34 | } 35 | // figure out how many samples per duration? 36 | // depends on the samplerate, which is 'samples per second' 37 | return batched 38 | } 39 | 40 | // FloatsToFrames turns a slice of float64 to a slice of frames 41 | func FloatsToFrames(fs []float64) []Frame { 42 | frames := make([]Frame, len(fs)) 43 | for i, f := range fs { 44 | frames[i] = Frame(f) 45 | } 46 | return frames 47 | } 48 | -------------------------------------------------------------------------------- /cmd/extractbrk/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "strconv" 7 | "strings" 8 | 9 | wav "github.com/DylanMeeus/GoAudio/wave" 10 | ) 11 | 12 | // program to extract breakpoint data from an input source. 13 | 14 | var ( 15 | input = flag.String("i", "", "input file") 16 | output = flag.String("o", "", "output file") 17 | window = flag.Int("w", 15, "window of time for capturing breakpoint data") 18 | ) 19 | 20 | func main() { 21 | flag.Parse() 22 | infile := *input 23 | outfile := *output 24 | wave, err := wav.ReadWaveFile(infile) 25 | if err != nil { 26 | panic("Could not parse wave file") 27 | } 28 | 29 | ticks := float64(*window) / 1000.0 30 | batches := wav.BatchSamples(wave, ticks) 31 | 32 | strout := strings.Builder{} 33 | elapsed := 0.0 34 | for _, b := range batches { 35 | maxa := maxAmp(b) 36 | es := strconv.FormatFloat(elapsed, 'f', 8, 64) 37 | fs := strconv.FormatFloat(maxa, 'f', 8, 64) 38 | strout.WriteString(es + ":" + fs + "\n") 39 | elapsed += ticks 40 | } 41 | 42 | err = ioutil.WriteFile(outfile, []byte(strout.String()), 0644) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | } 48 | 49 | // return the maximum amplitude of these samples 50 | func maxAmp(ss []wav.Frame) float64 { 51 | if len(ss) == 0 { 52 | return 0 53 | } 54 | max := -1.0 // because they are in range -1 .. 1 55 | for _, a := range ss { 56 | if float64(a) > max { 57 | max = float64(a) 58 | } 59 | } 60 | return float64(max) 61 | } 62 | -------------------------------------------------------------------------------- /examples/wavetable/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | synth "github.com/DylanMeeus/GoAudio/synthesizer" 7 | "github.com/DylanMeeus/GoAudio/wave" 8 | ) 9 | 10 | // example use of the oscillator to generate different waveforms 11 | var ( 12 | duration = flag.Int("d", 10, "duration of signal in seconds") 13 | shape = flag.String("s", "square", "waveshape to generate") 14 | amp = flag.Float64("a", 1, "Amplitude of signal") 15 | harms = flag.Int("h", 1, "Number of harmonics of signal") 16 | freq = flag.Float64("f", 440, "Frequency of signal") 17 | output = flag.String("o", "", "output file") 18 | ) 19 | 20 | var ( 21 | shapefunc = map[string]func(int, int) []float64{ 22 | "triangle": synth.TriangleTable, 23 | "square": synth.SquareTable, 24 | "saw": synth.SawTable, 25 | } 26 | ) 27 | 28 | func main() { 29 | flag.Parse() 30 | 31 | wfmt := wave.NewWaveFmt(1, 1, 44100, 16, nil) 32 | 33 | waveform := shapefunc[*shape] 34 | if waveform == nil { 35 | waveform = synth.SquareTable 36 | } 37 | table := synth.NewGtable(waveform(*harms, *duration*wfmt.SampleRate)) 38 | 39 | osc, err := synth.NewLookupOscillator(wfmt.SampleRate, table, 0.0) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | samples := osc.BatchTruncateTick(*freq, *duration*wfmt.SampleRate) 45 | frames := []wave.Frame{} 46 | for _, s := range samples { 47 | frames = append(frames, wave.Frame(s)) 48 | } 49 | if err := wave.WriteFrames(frames, wfmt, *output); err != nil { 50 | panic(err) 51 | } 52 | fmt.Println("done") 53 | } 54 | -------------------------------------------------------------------------------- /math/dft.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "github.com/DylanMeeus/GoAudio/wave" 5 | "math" 6 | "math/cmplx" 7 | ) 8 | 9 | const ( 10 | tau = 2. * math.Pi 11 | ) 12 | 13 | // DFT is a discrete fourier transformation on the input frames 14 | // DEPRECATED 15 | // Please use FFT unless you are sure you want this one.. 16 | func DFT(input []wave.Frame) []complex128 { 17 | N := len(input) 18 | 19 | output := make([]complex128, len(input)) 20 | 21 | reals := make([]float64, len(input)) 22 | imgs := make([]float64, len(input)) 23 | for i, frame := range input { 24 | for n := 0; n < N; n++ { 25 | reals[i] += float64(frame) * math.Cos(float64(i*n)*tau/float64(N)) 26 | imgs[i] += float64(frame) * math.Sin(float64(i*n)*tau/float64(N)) 27 | } 28 | 29 | reals[i] /= float64(N) 30 | imgs[i] /= float64(N) 31 | } 32 | 33 | for i := 0; i < len(reals); i++ { 34 | output[i] = complex(reals[i], imgs[i]) 35 | } 36 | 37 | return output 38 | } 39 | 40 | // HFFT mutates freqs! 41 | func HFFT(input []wave.Frame, freqs []complex128, n, step int) { 42 | if n == 1 { 43 | freqs[0] = complex(input[0], 0) 44 | return 45 | } 46 | 47 | h := n / 2 48 | 49 | HFFT(input, freqs, h, 2*step) 50 | HFFT(input[step:], freqs[h:], h, 2*step) 51 | 52 | for k := 0; k < h; k++ { 53 | a := -2 * math.Pi * float64(k) * float64(n) 54 | e := cmplx.Rect(1, a) * freqs[k+h] 55 | freqs[k], freqs[k+h] = freqs[k]+e, freqs[k]-e 56 | } 57 | } 58 | 59 | // FFT (Fast Fourier Transform) implementation 60 | func FFT(input []wave.Frame) []complex128 { 61 | freqs := make([]complex128, len(input)) 62 | HFFT(input, freqs, len(input), 1) 63 | return freqs 64 | } 65 | -------------------------------------------------------------------------------- /synthesizer/wavetable.go: -------------------------------------------------------------------------------- 1 | // wavetable implementation 2 | package synthesizer 3 | 4 | import "math" 5 | 6 | // FourierTable constructs a lookup table based on fourier addition with 'nharmns' harmonics 7 | // If amps is provided, scales the harmonics by the provided amp 8 | func FourierTable(nharms int, amps []float64, length int, phase float64) []float64 { 9 | table := make([]float64, length+2) 10 | phase *= tau 11 | 12 | for i := 0; i < nharms; i++ { 13 | for n := 0; n < len(table); n++ { 14 | amp := 1.0 15 | if i < len(amps) { 16 | amp = amps[i] 17 | } 18 | angle := float64(i+1) * (float64(n) * tau / float64(length)) 19 | table[n] += (amp * math.Cos(angle+phase)) 20 | } 21 | } 22 | return normalize(table) 23 | } 24 | 25 | // SawTable creates a sawtooth wavetable using Fourier addition 26 | func SawTable(nharms, length int) []float64 { 27 | amps := make([]float64, nharms) 28 | for i := 0; i < len(amps); i++ { 29 | amps[i] = 1.0 / float64(i+1) 30 | } 31 | return FourierTable(nharms, amps, length, -0.25) 32 | } 33 | 34 | // SquareTable uses fourier addition to create a square waveform 35 | func SquareTable(nharms, length int) []float64 { 36 | amps := make([]float64, nharms) 37 | for i := 0; i < len(amps); i += 2 { 38 | amps[i] = 1.0 / float64(i+1) 39 | } 40 | return FourierTable(nharms, amps, length, -0.25) 41 | } 42 | 43 | // TriangleTable uses fourier addition to create a triangle waveform 44 | func TriangleTable(nharms, length int) []float64 { 45 | amps := make([]float64, nharms) 46 | for i := 0; i < nharms; i += 2 { 47 | amps[i] = 1.0 / (float64(i+1) * float64(i+1)) 48 | } 49 | return FourierTable(nharms, amps, length, 0) 50 | } 51 | -------------------------------------------------------------------------------- /synthesizer/filters_test.go: -------------------------------------------------------------------------------- 1 | package synthesizer_test 2 | 3 | import ( 4 | synth "github.com/DylanMeeus/GoAudio/synthesizer" 5 | 6 | "testing" 7 | ) 8 | 9 | var ( 10 | lowpassTests = []struct { 11 | input []float64 12 | }{ 13 | { 14 | input: []float64{}, 15 | }, 16 | { 17 | input: []float64{1, 2, 3}, 18 | }, 19 | } 20 | 21 | highpassTests = []struct { 22 | input []float64 23 | }{ 24 | { 25 | input: []float64{}, 26 | }, 27 | { 28 | input: []float64{1, 2, 3}, 29 | }, 30 | } 31 | ) 32 | 33 | func TestLowpassFilter(t *testing.T) { 34 | // Test to make sure that the function is non-destructive to the input 35 | for _, test := range lowpassTests { 36 | t.Run("", func(t *testing.T) { 37 | c := make([]float64, len(test.input)) 38 | copy(test.input, c) 39 | _ = synth.Lowpass(test.input, 440, 1, 44100) 40 | // make sure that the input is not modified 41 | if !floatsEqual(test.input, c) { 42 | t.Fatal("Function modified source!") 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestHighpassFilter(t *testing.T) { 49 | // Test to make sure that the function is non-destructive to the input 50 | for _, test := range highpassTests { 51 | t.Run("", func(t *testing.T) { 52 | c := make([]float64, len(test.input)) 53 | copy(test.input, c) 54 | _ = synth.Highpass(test.input, 440, 1, 44100) 55 | // make sure that the input is not modified 56 | if !floatsEqual(test.input, c) { 57 | t.Fatal("Function modified source!") 58 | } 59 | }) 60 | } 61 | } 62 | func floatsEqual(fs1, fs2 []float64) bool { 63 | if len(fs1) != len(fs2) { 64 | return false 65 | } 66 | 67 | for i := range fs1 { 68 | if fs2[i] != fs2[i] { 69 | return false 70 | } 71 | } 72 | return true 73 | } 74 | -------------------------------------------------------------------------------- /synthesizer/filters.go: -------------------------------------------------------------------------------- 1 | package synthesizer 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Lowpass applies a low-pass filter to the frames 8 | // Does not modify the input signal 9 | func Lowpass(fs []float64, freq, delay, sr float64) []float64 { 10 | output := make([]float64, len(fs)) 11 | copy(output, fs) 12 | 13 | costh := 2. - math.Cos((tau*freq)/sr) 14 | coef := math.Sqrt(costh*costh-1.) - costh 15 | 16 | for i, a := range output { 17 | output[i] = a*(1+coef) - delay*coef 18 | delay = output[i] 19 | } 20 | 21 | return output 22 | } 23 | 24 | // Highpass applies a high-pass filter to the frames. 25 | // Does not modify the input signal 26 | func Highpass(fs []float64, freq, delay, sr float64) []float64 { 27 | output := make([]float64, len(fs)) 28 | copy(output, fs) 29 | 30 | b := 2. - math.Cos(tau*freq/sr) 31 | coef := b - math.Sqrt(b*b-1.) 32 | 33 | for i, a := range output { 34 | output[i] = a*(1.-coef) - delay*coef 35 | delay = output[i] 36 | } 37 | 38 | return output 39 | } 40 | 41 | // Balance a signal (rescale output signal) 42 | func Balance(signal, comparator, delay []float64, frequency, samplerate float64) []float64 { 43 | c := make([]float64, len(signal)) 44 | copy(signal, c) 45 | 46 | costh := 2. - math.Cos(tau*frequency/samplerate) 47 | coef := math.Sqrt(costh*costh-1.) - costh 48 | 49 | for i, s := range signal { 50 | ss := signal[i] 51 | if signal[i] < 0 { 52 | ss = -s 53 | } 54 | delay[0] = ss*(1+coef) - (delay[0] * coef) 55 | 56 | if comparator[i] < 0 { 57 | comparator[i] = -comparator[i] 58 | } 59 | delay[1] = comparator[i]*(1+coef) - (delay[1] * coef) 60 | if delay[0] != 0 { 61 | c[i] = s * (delay[0] / delay[1]) 62 | } else { 63 | c[i] = s * delay[1] 64 | } 65 | } 66 | return c 67 | } 68 | -------------------------------------------------------------------------------- /synthesizer/guardtable.go: -------------------------------------------------------------------------------- 1 | package synthesizer 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | ) 7 | 8 | // Gtable is a Guard-table for oscillator lookup 9 | type Gtable struct { 10 | data []float64 11 | } 12 | 13 | // Len returns the length of the data segment without the guard point 14 | func Len(g *Gtable) int { 15 | return len(g.data) - 1 16 | } 17 | 18 | func NewGtable(data []float64) *Gtable { 19 | return &Gtable{data} 20 | } 21 | 22 | // NewSineTable returns a lookup table populated for sine-wave generation. 23 | func NewSineTable(length int) *Gtable { 24 | g := &Gtable{} 25 | if length == 0 { 26 | return g 27 | } 28 | g.data = make([]float64, length+1) // one extra for the guard point. 29 | step := tau / float64(Len(g)) 30 | for i := 0; i < Len(g); i++ { 31 | g.data[i] = math.Sin(step * float64(i)) 32 | } 33 | // store a guard point 34 | g.data[len(g.data)-1] = g.data[0] 35 | return g 36 | } 37 | 38 | // NewTriangleTable generates a lookup table for a triangle wave 39 | // of the specified length and with the requested number of harmonics. 40 | func NewTriangleTable(length int, nharmonics int) (*Gtable, error) { 41 | if length == 0 || nharmonics == 0 || nharmonics >= length/2 { 42 | return nil, errors.New("Invalid arguments for creation of Triangle Table") 43 | } 44 | 45 | g := &Gtable{} 46 | g.data = make([]float64, length+1) 47 | 48 | step := tau / float64(length) 49 | 50 | // generate triangle waveform 51 | harmonic := 1.0 52 | for i := 0; i < nharmonics; i++ { 53 | amp := 1.0 / (harmonic * harmonic) 54 | for j := 0; j < length; j++ { 55 | g.data[j] += amp * math.Cos(step*harmonic*float64(j)) 56 | } 57 | harmonic += 2 // triangle wave has only odd harmonics 58 | } 59 | // normalize the values to be in the [-1;1] range 60 | g.data = normalize(g.data) 61 | return g, nil 62 | } 63 | 64 | // normalize the functions to the range -1, 1 65 | func normalize(xs []float64) []float64 { 66 | length := len(xs) 67 | maxamp := 0.0 68 | for i := 0; i < length; i++ { 69 | amp := math.Abs(xs[i]) 70 | if amp > maxamp { 71 | maxamp = amp 72 | } 73 | } 74 | 75 | maxamp = 1.0 / maxamp 76 | for i := 0; i < length; i++ { 77 | xs[i] *= maxamp 78 | } 79 | xs[len(xs)-1] = xs[0] 80 | return xs 81 | } 82 | -------------------------------------------------------------------------------- /cmd/inspect/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // tool to print the interpreted the content of a .wave file 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/DylanMeeus/GoAudio/wave" 11 | wav "github.com/DylanMeeus/GoAudio/wave" 12 | "strings" 13 | ) 14 | 15 | var ( 16 | input = flag.String("i", "", "input file") 17 | withSamples = flag.Bool("s", false, "with raw audio samples") 18 | ) 19 | 20 | // printHeader 21 | func printHeader(h wave.WaveHeader) { 22 | fmt.Println("Header") 23 | fmt.Printf("Chunk ID: %v\n", string(h.ChunkID)) 24 | fmt.Printf("Chunk Size: %v\n", h.ChunkSize) 25 | fmt.Printf("Format: %v\n", string(h.Format)) 26 | 27 | } 28 | 29 | func printFormat(f wave.WaveFmt) { 30 | fmt.Println("Wave FMT") 31 | fmt.Printf("SubchunkID: %v\n", string(f.Subchunk1ID)) 32 | fmt.Printf("SubchunkSize: %v\n", f.Subchunk1Size) 33 | fmt.Printf("AudioFormat: %v\n", f.AudioFormat) 34 | fmt.Printf("Channels: %v\n", f.NumChannels) 35 | fmt.Printf("SampleRate: %v\n", f.SampleRate) 36 | fmt.Printf("ByteRate: %v\n", f.ByteRate) 37 | fmt.Printf("BlockAlign: %v\n", f.BlockAlign) 38 | fmt.Printf("BitsPerSample: %v\n", f.BitsPerSample) 39 | } 40 | 41 | // print some information derived from the wave file content 42 | func printDerivedData(w wave.Wave) { 43 | bps := w.BitsPerSample * w.SampleRate 44 | fmt.Printf("Bits / second: %v\n", bps) 45 | 46 | // duration = number of samples / samplerate 47 | duration := (len(w.Frames) / w.NumChannels) / w.SampleRate 48 | fmt.Printf("Duration: %v\n", duration) 49 | 50 | } 51 | 52 | // validFile makes sure we can inspect this file based on the extension 53 | func validFile(file string) bool { 54 | parts := strings.Split(strings.ToLower(file), ".") 55 | if len(parts) == 0 { 56 | return false 57 | } 58 | 59 | ext := parts[len(parts)-1] 60 | return ext == "wav" || ext == "wave" 61 | } 62 | 63 | func main() { 64 | flag.Parse() 65 | as := os.Args[1:] 66 | if len(as) == 0 { 67 | panic("Please provide file") 68 | } 69 | 70 | infile := as[0] 71 | if !validFile(infile) { 72 | panic("Please provide valid file (.wav or .wave supported)") 73 | } 74 | ws := *withSamples 75 | wave, err := wav.ReadWaveFile(infile) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | printHeader(wave.WaveHeader) 81 | fmt.Println("===============") 82 | printFormat(wave.WaveFmt) 83 | fmt.Println("===============") 84 | printDerivedData(wave) 85 | 86 | if ws { 87 | fmt.Printf("===============\nSamples:\n") 88 | fmt.Printf("%v\n", wave.RawData) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /synthesizer/synth_test.go: -------------------------------------------------------------------------------- 1 | package synthesizer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | synth "github.com/DylanMeeus/GoAudio/synthesizer" 7 | ) 8 | 9 | var ( 10 | // we take a random sampling of notes -> frequencies to avoid dealing with odd Floating Point 11 | // values 12 | noteFrequencyTests = []struct { 13 | note string 14 | octave int 15 | output float64 16 | }{ 17 | {"g", 2, 98.}, 18 | {"bb", 5, 932.32}, 19 | {"G", 2, 98.}, 20 | {" G ", 2, 98.}, 21 | {"a", 7, 3520.}, 22 | {"a", 4, 440.}, 23 | {"a", 2, 110.}, 24 | } 25 | 26 | parseNoteFrequencyTests = []struct { 27 | in string 28 | out float64 29 | }{ 30 | { 31 | "g2", 32 | 98., 33 | }, 34 | { 35 | "a4", 36 | 440., 37 | }, 38 | { 39 | "a7", 40 | 3520., 41 | }, 42 | } 43 | ) 44 | 45 | // TestNoteToFrequency tests a selection of notes + octaves and verifies their frequency 46 | func TestNoteToFrequency(t *testing.T) { 47 | for _, test := range noteFrequencyTests { 48 | t.Run("", func(t *testing.T) { 49 | freq := synth.NoteToFrequency(test.note, test.octave) 50 | if !floatFuzzyEquals(freq, test.output) { 51 | t.Fatalf("Expected %v but got %v for (%s%v)", test.output, freq, test.note, test.octave) 52 | } 53 | }) 54 | 55 | } 56 | } 57 | 58 | // TestParseNoteFrequency tests a selection of strings containing note+octave and ensures that the 59 | // output is the expected frequency 60 | func TestParseNoteFrequency(t *testing.T) { 61 | for _, test := range parseNoteFrequencyTests { 62 | t.Run("", func(t *testing.T) { 63 | freq, err := synth.ParseNoteToFrequency(test.in) 64 | if err != nil { 65 | t.Fatalf("Unexpected error occurred: %v", err) 66 | } 67 | if !floatFuzzyEquals(freq, test.out) { 68 | t.Fatalf("Expected %v but got %v for (%s)", test.out, freq, test.in) 69 | } 70 | }) 71 | 72 | } 73 | } 74 | 75 | // BenchmarkNoteToFrequency benchmarks the lookup of a frequency given a note & string 76 | func BenchmarkNoteToFrequency(b *testing.B) { 77 | for i := 0; i < b.N; i++ { 78 | synth.NoteToFrequency("A", 4) 79 | } 80 | } 81 | 82 | // BenchmarkParseNoteFrequency benchmarks the 'translation' of a note/octave given as a string 83 | // to a frequency 84 | func BenchmarkParseNoteFrequency(b *testing.B) { 85 | for i := 0; i < b.N; i++ { 86 | synth.ParseNoteToFrequency("A4") 87 | } 88 | } 89 | 90 | // are the floats equal, within some grace region? 91 | // to deal with floating point representation errors 92 | func floatFuzzyEquals(f1, f2 float64) bool { 93 | return f1 > f2-10e-2 && f1 < f2+10e-2 94 | } 95 | -------------------------------------------------------------------------------- /cmd/MidiSound/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Play sound based on midi notes.. 5 | */ 6 | 7 | import ( 8 | "encoding/binary" 9 | "fmt" 10 | "math" 11 | "os" 12 | "strconv" 13 | ) 14 | 15 | type config struct { 16 | Duration int 17 | MidiNote int 18 | SampleRate int 19 | Amplitude float64 20 | } 21 | 22 | // Constants for generating our output 23 | const ( 24 | DURATION = iota 25 | MIDI 26 | SR 27 | AMP 28 | OUTFILE 29 | TOTAL_ARGS 30 | ) 31 | 32 | const concertA = float64(440) 33 | 34 | // usage: go run main.go [dur] [hz] [sr] [amp] 35 | // e.g: go run main.go 2 440 44100 1 36 | func main() { 37 | fmt.Printf("Generating..\n") 38 | generate(parseInput()) 39 | fmt.Printf("Done\n") 40 | } 41 | 42 | // midi2hertz turns a midi note into a frequency 43 | func midi2hertz(n int) float64 { 44 | middleC := concertA // oft used default for middleC 45 | return math.Pow(2, (float64(n)-69)/12) * middleC 46 | } 47 | 48 | // generate the sample based on the config 49 | func generate(c config) { 50 | var ( 51 | start float64 = 1.0 52 | end float64 = 1.0e-4 53 | tau = math.Pi * 2 54 | ) 55 | 56 | hertz := midi2hertz(c.MidiNote) 57 | fmt.Printf("don't hertz me: %v\n", hertz) 58 | 59 | // setup output file 60 | file := os.Args[1:][OUTFILE] 61 | f, err := os.Create(file) 62 | if err != nil { 63 | panic(err) 64 | } 65 | defer f.Close() 66 | 67 | nsamples := c.Duration * c.SampleRate 68 | angleincr := tau * hertz / float64(nsamples) 69 | decayfac := math.Pow(end/start, 1.0/float64(nsamples)) 70 | 71 | for i := 0; i < nsamples; i++ { 72 | sample := c.Amplitude * math.Sin(angleincr*float64(i)) 73 | sample *= start 74 | start *= decayfac 75 | var buf [8]byte 76 | // I know my system is LittleEndian, use BigEndian if yours is not.. 77 | binary.LittleEndian.PutUint32(buf[:], math.Float32bits(float32(sample))) 78 | bw, err := f.Write(buf[:]) 79 | if err != nil { 80 | panic(err) 81 | } 82 | fmt.Printf("\rWrote: %v bytes to %s", bw, file) 83 | } 84 | fmt.Printf("\n") 85 | } 86 | 87 | // will ignore error handling for now.. ;-) 88 | func parseInput() config { 89 | args := os.Args[1:] 90 | if len(args) != TOTAL_ARGS { 91 | return config{} 92 | } 93 | dur, _ := strconv.Atoi(args[DURATION]) 94 | midi, _ := strconv.Atoi(args[MIDI]) 95 | sr, _ := strconv.Atoi(args[SR]) 96 | amp, _ := strconv.ParseFloat(args[AMP], 64) 97 | return config{ 98 | Duration: dur, 99 | MidiNote: midi, 100 | SampleRate: sr, 101 | Amplitude: amp, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/oscillator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "github.com/DylanMeeus/GoAudio/breakpoint" 8 | synth "github.com/DylanMeeus/GoAudio/synthesizer" 9 | "github.com/DylanMeeus/GoAudio/wave" 10 | "io/ioutil" 11 | ) 12 | 13 | var stringToShape = map[string]synth.Shape{ 14 | "sine": 0, 15 | "square": 1, 16 | "downsaw": 2, 17 | "upsaw": 3, 18 | "triangle": 4, 19 | } 20 | 21 | // example use of the oscillator to generate different waveforms 22 | var ( 23 | duration = flag.Int("d", 10, "duration of signal") 24 | shape = flag.String("s", "sine", "One of: sine, square, triangle, downsaw, upsaw") 25 | amppoints = flag.String("a", "", "amplitude breakpoints file") 26 | freqpoints = flag.String("f", "", "frequency breakpoints file") 27 | output = flag.String("o", "", "output file") 28 | ) 29 | 30 | // Generate an oscillator of a given shape and duration 31 | // Modified by amplitude / frequency breakpoints 32 | func main() { 33 | flag.Parse() 34 | fmt.Println("usage: go run main -d {dur} -s {shape} -a {amps} -f {freqs} -o {output}") 35 | if output == nil { 36 | panic("please provide an output file") 37 | } 38 | 39 | wfmt := wave.NewWaveFmt(1, 1, 44100, 16, nil) 40 | amps, err := ioutil.ReadFile(*amppoints) 41 | if err != nil { 42 | panic(err) 43 | } 44 | ampPoints, err := breakpoint.ParseBreakpoints(bytes.NewReader(amps)) 45 | if err != nil { 46 | panic(err) 47 | } 48 | ampStream, err := breakpoint.NewBreakpointStream(ampPoints, wfmt.SampleRate) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | freqs, err := ioutil.ReadFile(*freqpoints) 54 | if err != nil { 55 | panic(err) 56 | } 57 | freqPoints, err := breakpoint.ParseBreakpoints(bytes.NewReader(freqs)) 58 | if err != nil { 59 | panic(err) 60 | } 61 | freqStream, err := breakpoint.NewBreakpointStream(freqPoints, wfmt.SampleRate) 62 | if err != nil { 63 | panic(err) 64 | } 65 | // create wave file sampled at 44.1Khz w/ 16-bit frames 66 | 67 | frames := generate(*duration, stringToShape[*shape], ampStream, freqStream, wfmt) 68 | wave.WriteFrames(frames, wfmt, *output) 69 | fmt.Println("done") 70 | } 71 | 72 | func generate(dur int, shape synth.Shape, ampStream, freqStream *breakpoint.BreakpointStream, wfmt wave.WaveFmt) []wave.Frame { 73 | reqFrames := dur * wfmt.SampleRate 74 | frames := make([]wave.Frame, reqFrames) 75 | osc, err := synth.NewOscillator(wfmt.SampleRate, shape) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | for i := range frames { 81 | amp := ampStream.Tick() 82 | freq := freqStream.Tick() 83 | frames[i] = wave.Frame(amp * osc.Tick(freq)) 84 | } 85 | 86 | return frames 87 | } 88 | -------------------------------------------------------------------------------- /wave/reader_test.go: -------------------------------------------------------------------------------- 1 | package wave 2 | 3 | import ( 4 | "runtime/debug" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | scaleFrameTests = []struct { 10 | input int 11 | bits int 12 | out Frame 13 | }{ 14 | { 15 | 0, 16 | 16, 17 | 0, 18 | }, 19 | { 20 | 127, 21 | 8, 22 | 1, 23 | }, 24 | { 25 | -127, 26 | 8, 27 | -1, 28 | }, 29 | { 30 | 32_767, 31 | 16, 32 | 1, 33 | }, 34 | { 35 | 32_767, 36 | 16, 37 | 1, 38 | }, 39 | { 40 | 0, 41 | 16, 42 | 0, 43 | }, 44 | { 45 | -32_767, 46 | 16, 47 | -1, 48 | }, 49 | } 50 | 51 | // readFileTest to iterate over various files and make sure the output meets 52 | // the expected Wave struct. 53 | readFileTests = []struct { 54 | file string 55 | expected Wave 56 | }{} 57 | ) 58 | 59 | // TestReadFile reads a golden file and ensures that is parsed as expected 60 | func TestReadFile(t *testing.T) { 61 | defer func() { 62 | if r := recover(); r != nil { 63 | t.Fatalf("Should not have panic'd!\n%v", r) 64 | } 65 | }() 66 | goldenfile := "./golden/maybe-next-time.wav" 67 | wav, err := ReadWaveFile(goldenfile) 68 | if err != nil { 69 | t.Fatalf("Should be able to read wave file: %v", err) 70 | } 71 | 72 | // assert that the wav file looks as expected. 73 | if wav.SampleRate != 44100 { 74 | t.Fatalf("Expected SR 44100, got: %v", wav.SampleRate) 75 | } 76 | 77 | if wav.NumChannels != 2 { 78 | t.Fatalf("Expected 2 channels, got: %v", wav.NumChannels) 79 | } 80 | } 81 | 82 | func TestScaleFrames(t *testing.T) { 83 | for _, test := range scaleFrameTests { 84 | t.Run("", func(t *testing.T) { 85 | res := scaleFrame(test.input, test.bits) 86 | if res != test.out { 87 | t.Fatalf("expected %v, got %v", test.out, res) 88 | } 89 | }) 90 | } 91 | } 92 | 93 | // TestJunkChunkFile tests that .wave files with a JUNK chunk can be read 94 | // https://www.daubnet.com/en/file-format-riff 95 | func TestJunkChunkFile(t *testing.T) { 96 | defer func() { 97 | if r := recover(); r != nil { 98 | t.Fatalf("Should not have panic'd reading JUNK files\n%v", string(debug.Stack())) 99 | } 100 | }() 101 | 102 | goldenfile := "./golden/chunk_junk.wav" 103 | 104 | wav, err := ReadWaveFile(goldenfile) 105 | if err != nil { 106 | t.Fatalf("should be able to read wave file: %v", err) 107 | } 108 | // assert that the wav file looks as expected. 109 | if wav.SampleRate != 48000 { 110 | t.Fatalf("Expected SR 48000, got: %v", wav.SampleRate) 111 | } 112 | 113 | if wav.NumChannels != 2 { 114 | t.Fatalf("Expected 2 channels, got: %v", wav.NumChannels) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /synthesizer/oscil.go: -------------------------------------------------------------------------------- 1 | package synthesizer 2 | 3 | // package to generate oscillators of various shapes 4 | 5 | import ( 6 | "fmt" 7 | "math" 8 | ) 9 | 10 | const tau = (2 * math.Pi) 11 | 12 | // Shape for defining the different possible waveform shapes for use with the Oscillator 13 | type Shape int 14 | 15 | // Shapes for which we can generate waveforms 16 | const ( 17 | SINE Shape = iota 18 | SQUARE 19 | DOWNWARD_SAWTOOTH 20 | UPWARD_SAWTOOTH 21 | TRIANGLE 22 | ) 23 | 24 | var ( 25 | shapeCalcFunc = map[Shape]func(float64) float64{ 26 | SINE: sineCalc, 27 | SQUARE: squareCalc, 28 | TRIANGLE: triangleCalc, 29 | DOWNWARD_SAWTOOTH: downSawtoothCalc, 30 | UPWARD_SAWTOOTH: upwSawtoothCalc, 31 | } 32 | ) 33 | 34 | // Oscillator represents a wave-oscillator where each tick is calculated in the moment. 35 | type Oscillator struct { 36 | curfreq float64 37 | curphase float64 38 | incr float64 39 | twopiosr float64 // (2*PI) / samplerate 40 | tickfunc func(float64) float64 41 | } 42 | 43 | // NewOscillator set to a given sample rate 44 | func NewOscillator(sr int, shape Shape) (*Oscillator, error) { 45 | cf, ok := shapeCalcFunc[shape] 46 | if !ok { 47 | return nil, fmt.Errorf("Shape type %v not supported", shape) 48 | } 49 | return &Oscillator{ 50 | twopiosr: tau / float64(sr), 51 | tickfunc: cf, 52 | }, nil 53 | } 54 | 55 | // NewPhaseOscillator creates a new oscillator where the initial phase is offset 56 | // by a given phase 57 | func NewPhaseOscillator(sr int, phase float64, shape Shape) (*Oscillator, error) { 58 | cf, ok := shapeCalcFunc[shape] 59 | if !ok { 60 | return nil, fmt.Errorf("Shape type %v not supported", shape) 61 | } 62 | return &Oscillator{ 63 | twopiosr: tau / float64(sr), 64 | tickfunc: cf, 65 | curphase: tau * phase, 66 | }, nil 67 | } 68 | 69 | // Tick generates the next value of the oscillator waveform at a given frequency in Hz 70 | func (o *Oscillator) Tick(freq float64) float64 { 71 | if o.curfreq != freq { 72 | o.curfreq = freq 73 | o.incr = o.twopiosr * freq 74 | } 75 | val := o.tickfunc(o.curphase) 76 | o.curphase += o.incr 77 | if o.curphase >= tau { 78 | o.curphase -= tau 79 | } 80 | if o.curphase < 0 { 81 | o.curphase = tau 82 | } 83 | return val 84 | 85 | } 86 | 87 | func triangleCalc(phase float64) float64 { 88 | val := 2.0*(phase*(1.0/tau)) - 1.0 89 | if val < 0.0 { 90 | val = -val 91 | } 92 | val = 2.0 * (val - 0.5) 93 | return val 94 | } 95 | 96 | func upwSawtoothCalc(phase float64) float64 { 97 | val := 2.0*(phase*(1.0/tau)) - 1.0 98 | return val 99 | } 100 | 101 | func downSawtoothCalc(phase float64) float64 { 102 | val := 1.0 - 2.0*(phase*(1.0/tau)) 103 | return val 104 | } 105 | 106 | func squareCalc(phase float64) float64 { 107 | val := -1.0 108 | if phase <= math.Pi { 109 | val = 1.0 110 | } 111 | return val 112 | } 113 | 114 | func sineCalc(phase float64) float64 { 115 | return math.Sin(phase) 116 | } 117 | -------------------------------------------------------------------------------- /synthesizer/lookuposcil.go: -------------------------------------------------------------------------------- 1 | package synthesizer 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // LookupOscillator is an oscillator that's more gentle on your CPU 8 | // By performing a table lookup to generate the required waveform.. 9 | type LookupOscillator struct { 10 | Oscillator 11 | Table *Gtable 12 | SizeOverSr float64 // convenience variable for calculations 13 | } 14 | 15 | // NewLookupOscillator creates a new oscillator which 16 | // performs a table-lookup to generate the required waveform 17 | func NewLookupOscillator(sr int, t *Gtable, phase float64) (*LookupOscillator, error) { 18 | if t == nil || len(t.data) == 0 { 19 | return nil, errors.New("Invalid table provided for lookup oscillator") 20 | } 21 | 22 | return &LookupOscillator{ 23 | Oscillator: Oscillator{ 24 | curfreq: 0.0, 25 | curphase: float64(Len(t)) * phase, 26 | incr: 0.0, 27 | }, 28 | Table: t, 29 | SizeOverSr: float64(Len(t)) / float64(sr), 30 | }, nil 31 | 32 | } 33 | 34 | // TruncateTick performs a lookup and truncates the value 35 | // index down (if the index for lookup = 10.5, return index 10) 36 | func (l *LookupOscillator) TruncateTick(freq float64) float64 { 37 | return l.BatchTruncateTick(freq, 1)[0] 38 | } 39 | 40 | // BatchTruncateTick returns a slice of samples from the oscillator of the requested length 41 | func (l *LookupOscillator) BatchTruncateTick(freq float64, nframes int) []float64 { 42 | out := make([]float64, nframes) 43 | for i := 0; i < nframes; i++ { 44 | index := l.curphase 45 | if l.curfreq != freq { 46 | l.curfreq = freq 47 | l.incr = l.SizeOverSr * l.curfreq 48 | } 49 | curphase := l.curphase 50 | curphase += l.incr 51 | for curphase > float64(Len(l.Table)) { 52 | curphase -= float64(Len(l.Table)) 53 | } 54 | for curphase < 0.0 { 55 | curphase += float64(Len(l.Table)) 56 | } 57 | l.curphase = curphase 58 | out[i] = l.Table.data[int(index)] 59 | } 60 | return out 61 | } 62 | 63 | // InterpolateTick performs a lookup but interpolates the value if the 64 | // requested index does not appear in the table. 65 | func (l *LookupOscillator) InterpolateTick(freq float64) float64 { 66 | return l.BatchInterpolateTick(freq, 1)[0] 67 | } 68 | 69 | // BatchInterpolateTick performs a lookup for N frames, and interpolates the value if the 70 | // requested index does not appear in the table. 71 | func (l *LookupOscillator) BatchInterpolateTick(freq float64, nframes int) []float64 { 72 | out := make([]float64, nframes) 73 | for i := 0; i < nframes; i++ { 74 | baseIndex := int(l.curphase) 75 | nextIndex := baseIndex + 1 76 | if l.curfreq != freq { 77 | l.curfreq = freq 78 | l.incr = l.SizeOverSr * l.curfreq 79 | } 80 | curphase := l.curphase 81 | frac := curphase - float64(baseIndex) 82 | val := l.Table.data[baseIndex] 83 | slope := l.Table.data[nextIndex] - val 84 | val += frac * slope 85 | curphase += l.incr 86 | 87 | for curphase > float64(Len(l.Table)) { 88 | curphase -= float64(Len(l.Table)) 89 | } 90 | for curphase < 0.0 { 91 | curphase += float64(Len(l.Table)) 92 | } 93 | 94 | l.curphase = curphase 95 | out[i] = val 96 | } 97 | return out 98 | } 99 | -------------------------------------------------------------------------------- /examples/table_oscillator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "github.com/DylanMeeus/GoAudio/breakpoint" 8 | synth "github.com/DylanMeeus/GoAudio/synthesizer" 9 | "github.com/DylanMeeus/GoAudio/wave" 10 | "io/ioutil" 11 | ) 12 | 13 | var stringToShape = map[string]synth.Shape{ 14 | "sine": 0, 15 | "square": 1, 16 | "downsaw": 2, 17 | "upsaw": 3, 18 | "triangle": 4, 19 | } 20 | 21 | // example use of the oscillator to generate different waveforms 22 | var ( 23 | duration = flag.Int("d", 10, "duration of signal") 24 | shape = flag.String("s", "sine", "One of: sine, square, triangle, downsaw, upsaw") 25 | amppoints = flag.String("a", "", "amplitude breakpoints file") 26 | freqpoints = flag.String("f", "", "frequency breakpoints file") 27 | output = flag.String("o", "", "output file") 28 | ) 29 | 30 | // Generate an oscillator of a given shape and duration 31 | // Modified by amplitude / frequency breakpoints 32 | func main() { 33 | flag.Parse() 34 | fmt.Println("usage: go run main -d {dur} -s {shape} -a {amps} -f {freqs} -o {output}") 35 | if output == nil { 36 | panic("please provide an output file") 37 | } 38 | 39 | wfmt := wave.NewWaveFmt(1, 1, 44100, 16, nil) 40 | amps, err := ioutil.ReadFile(*amppoints) 41 | if err != nil { 42 | panic(err) 43 | } 44 | ampPoints, err := breakpoint.ParseBreakpoints(bytes.NewReader(amps)) 45 | if err != nil { 46 | panic(err) 47 | } 48 | ampStream, err := breakpoint.NewBreakpointStream(ampPoints, wfmt.SampleRate) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | freqs, err := ioutil.ReadFile(*freqpoints) 54 | if err != nil { 55 | panic(err) 56 | } 57 | freqPoints, err := breakpoint.ParseBreakpoints(bytes.NewReader(freqs)) 58 | if err != nil { 59 | panic(err) 60 | } 61 | freqStream, err := breakpoint.NewBreakpointStream(freqPoints, wfmt.SampleRate) 62 | if err != nil { 63 | panic(err) 64 | } 65 | // create wave file sampled at 44.1Khz w/ 16-bit frames 66 | 67 | frames := generate(*duration, stringToShape[*shape], ampStream, freqStream, wfmt) 68 | wave.WriteFrames(frames, wfmt, *output) 69 | fmt.Println("done") 70 | } 71 | 72 | func generate(dur int, shape synth.Shape, ampStream, freqStream *breakpoint.BreakpointStream, wfmt wave.WaveFmt) []wave.Frame { 73 | reqFrames := dur * wfmt.SampleRate 74 | frames := make([]wave.Frame, reqFrames) 75 | 76 | // create table 77 | // 8k size 78 | var lookupTable *synth.Gtable 79 | switch shape { 80 | case synth.SINE: 81 | lookupTable = synth.NewSineTable(8 * 1024) 82 | case synth.TRIANGLE: 83 | triangleTable, err := synth.NewTriangleTable(8*1024, 4000) 84 | if err != nil { 85 | panic("Could not create triangle table") 86 | } 87 | lookupTable = triangleTable 88 | default: 89 | panic(fmt.Sprintf("Shape: %v not yet supported", shape)) 90 | } 91 | 92 | osc, err := synth.NewLookupOscillator(wfmt.SampleRate, lookupTable, 0.0) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | for i := range frames { 98 | amp := ampStream.Tick() 99 | freq := freqStream.Tick() 100 | frames[i] = wave.Frame(amp * osc.InterpolateTick(freq)) 101 | } 102 | 103 | return frames 104 | } 105 | -------------------------------------------------------------------------------- /wave/utils_test.go: -------------------------------------------------------------------------------- 1 | package wave 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var ( 8 | testBatchSamples = []struct { 9 | wave Wave 10 | timespan float64 11 | out [][]Frame 12 | }{ 13 | { 14 | Wave{}, 15 | 0, 16 | [][]Frame{{}}, 17 | }, 18 | { 19 | Wave{ 20 | WaveFmt: WaveFmt{ 21 | SampleRate: 2, // 2 seconds per sample 22 | NumChannels: 1, 23 | }, 24 | WaveData: WaveData{ 25 | Frames: makeSampleSlice(1, 2, 3, 4, 5, 6, 7, 8), 26 | }, 27 | }, 28 | 2, 29 | [][]Frame{makeSampleSlice(1, 2, 3, 4), makeSampleSlice(5, 6, 7, 8)}, 30 | }, 31 | { 32 | Wave{ 33 | WaveFmt: WaveFmt{ 34 | SampleRate: 2, // 2 seconds per sample 35 | NumChannels: 1, 36 | }, 37 | WaveData: WaveData{ 38 | Frames: makeSampleSlice(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 39 | }, 40 | }, 41 | 2, 42 | [][]Frame{makeSampleSlice(1, 2, 3, 4), makeSampleSlice(5, 6, 7, 8), makeSampleSlice(9, 10)}, 43 | }, 44 | { 45 | // test for multi-cuannel wave files 46 | Wave{ 47 | WaveFmt: WaveFmt{ 48 | SampleRate: 2, // 2 seconds per sample 49 | NumChannels: 2, 50 | }, 51 | WaveData: WaveData{ 52 | Frames: makeSampleSlice(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 53 | }, 54 | }, 55 | 0.5, 56 | [][]Frame{makeSampleSlice(1, 2), makeSampleSlice(3, 4), makeSampleSlice(5, 6), makeSampleSlice(7, 8), makeSampleSlice(9, 10)}, 57 | }, 58 | } 59 | 60 | testFloatsToFrames = []struct { 61 | in []float64 62 | out []Frame 63 | }{ 64 | { 65 | in: []float64{}, 66 | out: []Frame{}, 67 | }, 68 | { 69 | in: []float64{1, 2, 3}, 70 | out: makeSampleSlice(1, 2, 3), 71 | }, 72 | { 73 | in: []float64{1}, 74 | out: makeSampleSlice(1), 75 | }, 76 | } 77 | ) 78 | 79 | func TestBatching(t *testing.T) { 80 | t.Logf("Testing batching of samples per time slice") 81 | for _, test := range testBatchSamples { 82 | t.Run("", func(t *testing.T) { 83 | res := BatchSamples(test.wave, test.timespan) 84 | if !compareSampleSlices(res, test.out) { 85 | t.Fatalf("expected %v, got %v", test.out, res) 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func TestFloatsToFrames(t *testing.T) { 92 | t.Logf("Testing batching of samples per time slice") 93 | for _, test := range testFloatsToFrames { 94 | t.Run("", func(t *testing.T) { 95 | out := FloatsToFrames(test.in) 96 | if !framesEquals(out, test.out) { 97 | t.Fatalf("expected %v, got %v", test.out, out) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func framesEquals(f1, f2 []Frame) bool { 104 | if len(f1) != len(f2) { 105 | return false 106 | } 107 | for i := range f1 { 108 | if f1[i] != f2[i] { 109 | return false 110 | } 111 | } 112 | return true 113 | } 114 | 115 | // helper functions for testing 116 | func makeSampleSlice(input ...float64) (out []Frame) { 117 | for _, f := range input { 118 | out = append(out, Frame(f)) 119 | } 120 | return 121 | } 122 | 123 | // compareSampleSlices makes sure both slices are the same 124 | func compareSampleSlices(a, b [][]Frame) bool { 125 | if len(a) != len(b) { 126 | return false 127 | } 128 | for i, x := range a { 129 | for j, v := range x { 130 | if b[i][j] != v { 131 | return false 132 | } 133 | } 134 | } 135 | return true 136 | } 137 | -------------------------------------------------------------------------------- /synthesizer/synth.go: -------------------------------------------------------------------------------- 1 | package synthesizer 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | // noteIndex for use in calculations where a user passes a note 11 | noteIndex = map[string]int{ 12 | "a": 0, 13 | "a#": 1, 14 | "bb": 1, 15 | "b": 2, 16 | "c": 3, 17 | "c#": 4, 18 | "db": 4, 19 | "d": 5, 20 | "d#": 6, 21 | "eb": 6, 22 | "e": 7, 23 | "f": 8, 24 | "f#": 9, 25 | "gb": 9, 26 | "g": 10, 27 | "g#": 11, 28 | "ab": 11, 29 | } 30 | ) 31 | 32 | var ( 33 | s = struct{}{} 34 | valid = map[string]interface{}{"a": s, "b": s, "c": s, "d": s, "e": s, "f": s, "g": s, "#": s} 35 | digits = map[string]interface{}{"0": s, "1": s, "2": s, "3": s, "4": s, "5": s, "6": s, "7": s, "8": s, "9": s} 36 | ) 37 | 38 | // ADSR creates an attack -> decay -> sustain -> release envelope 39 | // time durations are passes as seconds. 40 | // returns the value + the current time 41 | func ADSR(maxamp, duration, attacktime, decaytime, sus, releasetime, controlrate float64, currentframe int) float64 { 42 | dur := duration * controlrate 43 | at := attacktime * controlrate 44 | dt := decaytime * controlrate 45 | rt := releasetime * controlrate 46 | cnt := float64(currentframe) 47 | 48 | amp := 0.0 49 | if cnt < dur { 50 | if cnt <= at { 51 | // attack 52 | amp = cnt * (maxamp / at) 53 | } else if cnt <= (at + dt) { 54 | // decay 55 | amp = ((sus-maxamp)/dt)*(cnt-at) + maxamp 56 | } else if cnt <= dur-rt { 57 | // sustain 58 | amp = sus 59 | } else if cnt > (dur - rt) { 60 | // release 61 | amp = -(sus/rt)*(cnt-(dur-rt)) + sus 62 | } 63 | } 64 | 65 | return amp 66 | } 67 | 68 | // NoteToFrequency turns a given note & octave into a frequency 69 | // using Equal-Tempered tuning with reference pitch = A440 70 | func NoteToFrequency(note string, octave int) float64 { 71 | // TODO: Allow for tuning systems other than Equal-Tempered A440? 72 | // clean the input 73 | note = strings.ToLower(strings.TrimSpace(note)) 74 | ni := noteIndex[note] 75 | if ni >= 3 { 76 | // correct for octaves starting at C, not A. 77 | octave-- 78 | } 79 | FR := 440. 80 | // we adjust the octave (-4) as the reference frequency is in the fourth octave 81 | // this effectively allows us to generate any octave above or below the reference octave 82 | return FR * math.Pow(2, float64(octave-4)+(float64(ni)/12.)) 83 | } 84 | 85 | // parseNoteOctave returns the note + octave value 86 | func parseNoteOctave(note string) (string, int, error) { 87 | note = strings.ToLower(note) 88 | notePart := strings.Map(func(r rune) rune { 89 | if _, ok := valid[string(r)]; !ok { 90 | return rune(-1) 91 | } 92 | return r 93 | }, note) 94 | 95 | digitPart := strings.Map(func(r rune) rune { 96 | if _, ok := digits[string(r)]; !ok { 97 | return rune(-1) 98 | } 99 | return r 100 | }, note[len(notePart):]) 101 | 102 | octave, err := strconv.Atoi(digitPart) 103 | if err != nil { 104 | return "", 0, err 105 | } 106 | 107 | return notePart, octave, nil 108 | } 109 | 110 | // ParseNoteToFrequency tries to parse a string representation of a note+octave (e.g C#4) 111 | // and will return a float64 frequency value using 'NoteToFrequency' 112 | func ParseNoteToFrequency(note string) (float64, error) { 113 | nt, oct, err := parseNoteOctave(note) 114 | if err != nil { 115 | return -1, err 116 | } 117 | return NoteToFrequency(nt, oct), nil 118 | } 119 | -------------------------------------------------------------------------------- /examples/stereopan/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | brk "github.com/DylanMeeus/GoAudio/breakpoint" 7 | wav "github.com/DylanMeeus/GoAudio/wave" 8 | "math" 9 | "os" 10 | ) 11 | 12 | var ( 13 | input = flag.String("i", "", "input file") 14 | output = flag.String("o", "", "output file") 15 | pan = flag.Float64("p", 0.0, "pan in range of -1 (left) to 1 (right)") 16 | brkpnt = flag.String("b", "", "breakpoint file") 17 | ) 18 | 19 | type panposition struct { 20 | left, right float64 21 | } 22 | 23 | // calculateConstantPowerPosition finds the position of each speaker using a constant power function 24 | func calculateConstantPowerPosition(position float64) panposition { 25 | // half a sinusoid cycle 26 | var halfpi float64 = math.Pi / 2 27 | r := math.Sqrt(2.0) / 2 28 | 29 | // scale position to fit in this range 30 | scaled := position * halfpi 31 | 32 | // each channel uses 1/4 of a cycle 33 | angle := scaled / 2 34 | pos := panposition{} 35 | pos.left = r * (math.Cos(angle) - math.Sin(angle)) 36 | pos.right = r * (math.Cos(angle) + math.Sin(angle)) 37 | return pos 38 | } 39 | 40 | func calculatePosition(position float64) panposition { 41 | position *= 0.5 42 | return panposition{ 43 | left: position - 0.5, 44 | right: position + 0.5, 45 | } 46 | } 47 | 48 | // set a single pan (without breakpoints) 49 | func setPan() { 50 | fmt.Println("Parsing wave file..") 51 | flag.Parse() 52 | infile := *input 53 | outfile := *output 54 | panfac := *pan 55 | wave, err := wav.ReadWaveFile(infile) 56 | if err != nil { 57 | panic("Could not parse wave file") 58 | } 59 | 60 | fmt.Printf("Read %v samples\n", len(wave.Frames)) 61 | 62 | // now try to write this file 63 | pos := calculatePosition(panfac) 64 | fmt.Printf("panfac: %v\npanpos: %v\n", panfac, pos) 65 | scaledFrames := applyPan(wave.Frames, calculatePosition(panfac)) 66 | wave.NumChannels = 2 // samples are now stereo, so we need dual channels 67 | if err := wav.WriteFrames(scaledFrames, wave.WaveFmt, outfile); err != nil { 68 | panic(err) 69 | } 70 | fmt.Println("done") 71 | } 72 | 73 | func withBreakpointFile() { 74 | // apply a stereo pan by applying a breakpoint file using linear interpolation 75 | flag.Parse() 76 | 77 | file, err := os.Open(*brkpnt) 78 | if err != nil { 79 | panic(err) 80 | } 81 | pnts, err := brk.ParseBreakpoints(file) 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | // verify that the breakpoints are valid 87 | if brk.Breakpoints(pnts).Any(func(b brk.Breakpoint) bool { 88 | return b.Value > 1 || b.Value < -1 89 | }) { 90 | panic("Breakpoint file contains invalid values") 91 | } 92 | 93 | // read the content file 94 | infile := *input 95 | wave, err := wav.ReadWaveFile(infile) 96 | if err != nil { 97 | panic(err) 98 | } 99 | 100 | // now we want to apply the pan per sample 101 | // for this we need to know the time at each sample. 102 | // which is the reciprocal of sample rate 103 | timeincr := 1.0 / float64(wave.SampleRate) 104 | var frametime float64 105 | inframes := wave.Frames 106 | var out []wav.Frame 107 | 108 | wave.WaveFmt.SetChannels(2) 109 | for _, s := range inframes { 110 | // apply pan 111 | _, pos := brk.ValueAt(pnts, frametime, 0) 112 | pan := calculatePosition(pos) 113 | out = append(out, wav.Frame(float64(s)*pan.left)) 114 | out = append(out, wav.Frame(float64(s)*pan.right)) 115 | frametime += timeincr 116 | } 117 | 118 | wav.WriteFrames(out, wave.WaveFmt, *output) 119 | } 120 | 121 | func main() { 122 | //setPan() 123 | withBreakpointFile() 124 | } 125 | 126 | func applyPan(samples []wav.Frame, p panposition) []wav.Frame { 127 | out := []wav.Frame{} 128 | for _, s := range samples { 129 | out = append(out, wav.Frame(float64(s)*p.left)) 130 | out = append(out, wav.Frame(float64(s)*p.right)) 131 | } 132 | return out 133 | } 134 | -------------------------------------------------------------------------------- /synthesizer/constants.go: -------------------------------------------------------------------------------- 1 | package synthesizer 2 | 3 | // EqualTemperedNote 4 | type EqualTemperedNote float64 5 | 6 | // These constants represent the frequncies for the equal-tempered scale tuned to A4 = 440Hz 7 | const ( 8 | C0 EqualTemperedNote = 16.35 9 | C0S EqualTemperedNote = 17.32 10 | D0 EqualTemperedNote = 18.35 11 | D0S EqualTemperedNote = 19.45 12 | E0 EqualTemperedNote = 20.60 13 | F0 EqualTemperedNote = 21.83 14 | F0S EqualTemperedNote = 23.12 15 | G0 EqualTemperedNote = 24.50 16 | G0S EqualTemperedNote = 25.96 17 | A0 EqualTemperedNote = 27.50 18 | A0S EqualTemperedNote = 29.14 19 | B0 EqualTemperedNote = 30.87 20 | C1 EqualTemperedNote = 32.70 21 | C1S EqualTemperedNote = 34.65 22 | D1 EqualTemperedNote = 36.71 23 | D1S EqualTemperedNote = 38.89 24 | E1 EqualTemperedNote = 41.20 25 | F1 EqualTemperedNote = 43.65 26 | F1S EqualTemperedNote = 46.25 27 | G1 EqualTemperedNote = 49.00 28 | G1S EqualTemperedNote = 51.91 29 | A1 EqualTemperedNote = 55.00 30 | A1S EqualTemperedNote = 58.27 31 | B1 EqualTemperedNote = 61.74 32 | C2 EqualTemperedNote = 65.41 33 | C2S EqualTemperedNote = 69.30 34 | D2 EqualTemperedNote = 73.42 35 | D2S EqualTemperedNote = 77.78 36 | E2 EqualTemperedNote = 82.41 37 | F2 EqualTemperedNote = 87.31 38 | F2S EqualTemperedNote = 92.50 39 | G2 EqualTemperedNote = 98.00 40 | G2S EqualTemperedNote = 103.83 41 | A2 EqualTemperedNote = 110.00 42 | A2S EqualTemperedNote = 116.54 43 | B2 EqualTemperedNote = 123.47 44 | C3 EqualTemperedNote = 130.81 45 | C3S EqualTemperedNote = 138.59 46 | D3 EqualTemperedNote = 146.83 47 | D3S EqualTemperedNote = 155.56 48 | E3 EqualTemperedNote = 164.81 49 | F3 EqualTemperedNote = 174.61 50 | F3S EqualTemperedNote = 185.00 51 | G3 EqualTemperedNote = 196.00 52 | G3S EqualTemperedNote = 207.65 53 | A3 EqualTemperedNote = 220.00 54 | A3S EqualTemperedNote = 233.08 55 | B3 EqualTemperedNote = 246.94 56 | C4 EqualTemperedNote = 261.63 57 | C4S EqualTemperedNote = 277.18 58 | D4 EqualTemperedNote = 293.66 59 | D4S EqualTemperedNote = 311.13 60 | E4 EqualTemperedNote = 329.63 61 | F4 EqualTemperedNote = 349.23 62 | F4S EqualTemperedNote = 369.99 63 | G4 EqualTemperedNote = 392.00 64 | G4S EqualTemperedNote = 415.30 65 | A4 EqualTemperedNote = 440.00 66 | A4S EqualTemperedNote = 466.16 67 | B4 EqualTemperedNote = 493.88 68 | C5 EqualTemperedNote = 523.25 69 | C5S EqualTemperedNote = 554.37 70 | D5 EqualTemperedNote = 587.33 71 | D5S EqualTemperedNote = 622.25 72 | E5 EqualTemperedNote = 659.25 73 | F5 EqualTemperedNote = 698.46 74 | F5S EqualTemperedNote = 739.99 75 | G5 EqualTemperedNote = 783.99 76 | G5S EqualTemperedNote = 830.61 77 | A5 EqualTemperedNote = 880.00 78 | A5S EqualTemperedNote = 932.33 79 | B5 EqualTemperedNote = 987.77 80 | C6 EqualTemperedNote = 1046.50 81 | C6S EqualTemperedNote = 1108.73 82 | D6 EqualTemperedNote = 1174.66 83 | D6S EqualTemperedNote = 1244.51 84 | E6 EqualTemperedNote = 1318.51 85 | F6 EqualTemperedNote = 1396.91 86 | F6S EqualTemperedNote = 1479.98 87 | G6 EqualTemperedNote = 1567.98 88 | G6S EqualTemperedNote = 1661.22 89 | A6 EqualTemperedNote = 1760.00 90 | A6S EqualTemperedNote = 1864.66 91 | B6 EqualTemperedNote = 1975.53 92 | C7 EqualTemperedNote = 2093.00 93 | C7S EqualTemperedNote = 2217.46 94 | D7 EqualTemperedNote = 2349.32 95 | D7S EqualTemperedNote = 2489.02 96 | E7 EqualTemperedNote = 2636.02 97 | F7 EqualTemperedNote = 2793.83 98 | F7S EqualTemperedNote = 2959.96 99 | G7 EqualTemperedNote = 3135.96 100 | G7S EqualTemperedNote = 3322.44 101 | A7 EqualTemperedNote = 3520.00 102 | A7S EqualTemperedNote = 3729.31 103 | B7 EqualTemperedNote = 3951.07 104 | C8 EqualTemperedNote = 4186.01 105 | C8S EqualTemperedNote = 4434.92 106 | D8 EqualTemperedNote = 4698.63 107 | D8S EqualTemperedNote = 4978.03 108 | E8 EqualTemperedNote = 5274.04 109 | F8 EqualTemperedNote = 5587.65 110 | F8S EqualTemperedNote = 5919.91 111 | G8 EqualTemperedNote = 6271.93 112 | G8S EqualTemperedNote = 6644.88 113 | A8 EqualTemperedNote = 7040.00 114 | A8S EqualTemperedNote = 7458.62 115 | B8 EqualTemperedNote = 7902.13 116 | ) 117 | -------------------------------------------------------------------------------- /wave/writer.go: -------------------------------------------------------------------------------- 1 | package wave 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "math" 7 | "os" 8 | ) 9 | 10 | // Consts that appear in the .WAVE file format 11 | var ( 12 | ChunkID = []byte{0x52, 0x49, 0x46, 0x46} // RIFF 13 | BigEndianChunkID = []byte{0x52, 0x49, 0x46, 0x58} // RIFX 14 | WaveID = []byte{0x57, 0x41, 0x56, 0x45} // WAVE 15 | Format = []byte{0x66, 0x6d, 0x74, 0x20} // FMT 16 | Subchunk2ID = []byte{0x64, 0x61, 0x74, 0x61} // DATA 17 | ) 18 | 19 | type intsToBytesFunc func(i int) []byte 20 | 21 | var ( 22 | // intsToBytesFm to map X-bit int to byte functions 23 | intsToBytesFm = map[int]intsToBytesFunc{ 24 | 16: int16ToBytes, 25 | 32: int32ToBytes, 26 | } 27 | ) 28 | 29 | // WriteFrames writes the slice to disk as a .wav file 30 | // the WaveFmt metadata needs to be correct 31 | // WaveData and WaveHeader are inferred from the samples however.. 32 | func WriteFrames(samples []Frame, wfmt WaveFmt, file string) error { 33 | return WriteWaveFile(samples, wfmt, file) 34 | } 35 | 36 | func WriteWaveFile(samples []Frame, wfmt WaveFmt, file string) error { 37 | f, err := os.Create(file) 38 | if err != nil { 39 | return err 40 | } 41 | defer f.Close() 42 | 43 | return WriteWaveToWriter(samples, wfmt, f) 44 | } 45 | 46 | func WriteWaveToWriter(samples []Frame, wfmt WaveFmt, writer io.Writer) error { 47 | wfb := fmtToBytes(wfmt) 48 | data, databits := framesToData(samples, wfmt) 49 | hdr := createHeader(data) 50 | 51 | _, err := writer.Write(hdr) 52 | if err != nil { 53 | return err 54 | } 55 | _, err = writer.Write(wfb) 56 | if err != nil { 57 | return err 58 | } 59 | _, err = writer.Write(databits) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func int16ToBytes(i int) []byte { 68 | b := make([]byte, 2) 69 | in := uint16(i) 70 | binary.LittleEndian.PutUint16(b, in) 71 | return b 72 | } 73 | 74 | func int32ToBytes(i int) []byte { 75 | b := make([]byte, 4) 76 | in := uint32(i) 77 | binary.LittleEndian.PutUint32(b, in) 78 | return b 79 | } 80 | 81 | func framesToData(frames []Frame, wfmt WaveFmt) (WaveData, []byte) { 82 | b := []byte{} 83 | raw := samplesToRawData(frames, wfmt) 84 | 85 | // We receive frames but have to store the size of the samples 86 | // The size of the samples is frames / channels.. 87 | subchunksize := (len(frames) * wfmt.NumChannels * wfmt.BitsPerSample) / 8 88 | subBytes := int32ToBytes(subchunksize) 89 | 90 | // construct the data part.. 91 | b = append(b, Subchunk2ID...) 92 | b = append(b, subBytes...) 93 | b = append(b, raw...) 94 | 95 | wd := WaveData{ 96 | Subchunk2ID: Subchunk2ID, 97 | Subchunk2Size: subchunksize, 98 | RawData: raw, 99 | Frames: frames, 100 | } 101 | return wd, b 102 | } 103 | 104 | func floatToBytes(f float64, nBytes int) []byte { 105 | bits := math.Float64bits(f) 106 | bs := make([]byte, 8) 107 | binary.LittleEndian.PutUint64(bs, bits) 108 | // trim padding 109 | switch nBytes { 110 | case 2: 111 | return bs[:2] 112 | case 4: 113 | return bs[:4] 114 | } 115 | return bs 116 | } 117 | 118 | // Turn the samples into raw data... 119 | func samplesToRawData(samples []Frame, props WaveFmt) []byte { 120 | raw := []byte{} 121 | for _, s := range samples { 122 | // the samples are scaled - rescale them? 123 | rescaled := rescaleFrame(s, props.BitsPerSample) 124 | bits := intsToBytesFm[props.BitsPerSample](rescaled) 125 | raw = append(raw, bits...) 126 | } 127 | return raw 128 | } 129 | 130 | // rescale frames back to the original values.. 131 | func rescaleFrame(s Frame, bits int) int { 132 | rescaled := float64(s) * float64(maxValues[bits]) 133 | return int(rescaled) 134 | } 135 | 136 | func fmtToBytes(wfmt WaveFmt) []byte { 137 | b := []byte{} 138 | 139 | subchunksize := int32ToBytes(wfmt.Subchunk1Size) 140 | audioformat := int16ToBytes(wfmt.AudioFormat) 141 | numchans := int16ToBytes(wfmt.NumChannels) 142 | sr := int32ToBytes(wfmt.SampleRate) 143 | br := int32ToBytes(wfmt.ByteRate) 144 | blockalign := int16ToBytes(wfmt.BlockAlign) 145 | bitsPerSample := int16ToBytes(wfmt.BitsPerSample) 146 | 147 | b = append(b, wfmt.Subchunk1ID...) 148 | b = append(b, subchunksize...) 149 | b = append(b, audioformat...) 150 | b = append(b, numchans...) 151 | b = append(b, sr...) 152 | b = append(b, br...) 153 | b = append(b, blockalign...) 154 | b = append(b, bitsPerSample...) 155 | 156 | return b 157 | } 158 | 159 | // turn the sample to a valid header 160 | func createHeader(wd WaveData) []byte { 161 | // write chunkID 162 | bits := []byte{} 163 | 164 | chunksize := 36 + wd.Subchunk2Size 165 | cb := int32ToBytes(chunksize) 166 | 167 | bits = append(bits, ChunkID...) // in theory switch on endianness.. 168 | bits = append(bits, cb...) 169 | bits = append(bits, WaveID...) 170 | 171 | return bits 172 | } 173 | -------------------------------------------------------------------------------- /breakpoint/breakpoint_test.go: -------------------------------------------------------------------------------- 1 | package breakpoint 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | valueAtTests = []struct { 10 | breaks []Breakpoint 11 | time float64 12 | out float64 13 | }{ 14 | { 15 | []Breakpoint{}, 16 | 0, 17 | 0, 18 | }, 19 | { 20 | []Breakpoint{}, 21 | 0, 22 | 0, 23 | }, 24 | { 25 | []Breakpoint{ 26 | { 27 | Time: 0, 28 | Value: 1, 29 | }, 30 | }, 31 | 0, 32 | 1, 33 | }, 34 | { 35 | []Breakpoint{ 36 | { 37 | Time: 0, 38 | Value: 1, 39 | }, 40 | }, 41 | 2, 42 | 1, 43 | }, 44 | { 45 | []Breakpoint{ 46 | { 47 | Time: 0, 48 | Value: 2, 49 | }, 50 | { 51 | Time: 1, 52 | Value: 0, 53 | }, 54 | }, 55 | 2, 56 | 0, 57 | }, 58 | { 59 | []Breakpoint{ 60 | { 61 | Time: 1, 62 | Value: 0, 63 | }, 64 | { 65 | Time: 2, 66 | Value: 10, 67 | }, 68 | { 69 | Time: 3, 70 | Value: 100, 71 | }, 72 | }, 73 | 1.5, // linear interpolation should give this result 74 | 5, 75 | }, 76 | } 77 | 78 | minMaxTests = []struct { 79 | in []Breakpoint 80 | min float64 81 | max float64 82 | }{ 83 | { 84 | in: []Breakpoint{}, 85 | min: 0.0, 86 | max: 0.0, 87 | }, 88 | { 89 | in: []Breakpoint{Breakpoint{0, 10}}, 90 | min: 10, 91 | max: 10, 92 | }, 93 | { 94 | in: []Breakpoint{Breakpoint{0, 10}, Breakpoint{1, 5}, Breakpoint{2, 3}}, 95 | min: 3, 96 | max: 10, 97 | }, 98 | } 99 | 100 | anyTests = []struct { 101 | in Breakpoints 102 | anyf func(Breakpoint) bool 103 | out bool 104 | }{ 105 | { 106 | in: Breakpoints{Breakpoint{1, 0}, Breakpoint{2, 5}, Breakpoint{3, 10}}, 107 | anyf: func(b Breakpoint) bool { return b.Value > 5 }, 108 | out: true, 109 | }, 110 | { 111 | in: Breakpoints{Breakpoint{1, 0}, Breakpoint{2, 5}, Breakpoint{3, 10}}, 112 | anyf: func(b Breakpoint) bool { return b.Value > 15 }, 113 | out: false, 114 | }, 115 | } 116 | ) 117 | 118 | func TestBreakpoint(t *testing.T) { 119 | // TODO: these breakpoints are invalid as long as we can't timetravel. Time should be strictly 120 | // increasing.. 121 | input := ` 122 | 3.0:1.31415 123 | -1:1.32134 124 | 0:0 125 | ` 126 | brk, err := ParseBreakpoints(strings.NewReader(input)) 127 | if err != nil { 128 | t.Fatalf("Should be able to parse breakpoints: %v", err) 129 | } 130 | 131 | if brk[0].Time != 3.0 { 132 | t.Fatalf("Breakpoint does not match input, expected %v, got %v", 3.0, brk[0].Time) 133 | } 134 | if brk[1].Time != -1 { 135 | t.Fatal("Breakpoint does not match input") 136 | } 137 | if brk[2].Time != 0 { 138 | t.Fatal("Breakpoint does not match input") 139 | } 140 | 141 | if brk[0].Value != 1.31415 { 142 | t.Fatal("Breakpoint does not match input") 143 | } 144 | if brk[1].Value != 1.32134 { 145 | t.Fatal("Breakpoint does not match input") 146 | } 147 | if brk[2].Value != 0 { 148 | t.Fatal("Breakpoint does not match input") 149 | } 150 | } 151 | 152 | // TestbreakpointStream tests various assumptions of how ticks should be handled 153 | // We test at 10 samples per second for convenience (easier to verify correctness) 154 | // Thus each -tick- should update our position by 0.1 155 | func TestBreakpointStream(t *testing.T) { 156 | input := ` 157 | 0:100 158 | 10:50 159 | 20:100 160 | ` 161 | samplerate := 10 // 10 sample per second 162 | brks, err := ParseBreakpoints(strings.NewReader(input)) 163 | if err != nil { 164 | t.Fatalf("Should be able to parse breakpoints: %v", err) 165 | } 166 | stream, err := NewBreakpointStream(brks, samplerate) 167 | if err != nil { 168 | t.Fatalf("Should be able to create breakpoint stream: %v", err) 169 | } 170 | 171 | if stream.Increment != 1.0/float64(samplerate) { 172 | t.Fatal("Incorrect increment") 173 | } 174 | value := stream.Tick() 175 | if stream.CurrentPosition != 0.1 { 176 | t.Fatal("Incorrectly incremented stream") 177 | } 178 | // move to 5 seconds (50 ticks) 179 | for i := 0; i < 50; i++ { 180 | value = stream.Tick() 181 | } 182 | if value != 75 { 183 | t.Fatalf("Expected 75 but got %v", value) 184 | } 185 | 186 | // move to second 15 187 | for i := 0; i < 100; i++ { 188 | value = stream.Tick() 189 | } 190 | 191 | t.Log("Should be able to get values beyond the end of the stream") 192 | for i := 0; i < 10e5; i++ { 193 | value = stream.Tick() 194 | } 195 | if value != 100 { 196 | t.Fatalf("Value should be 100, but got %v", value) 197 | } 198 | } 199 | 200 | func TestValueAt(t *testing.T) { 201 | for _, test := range valueAtTests { 202 | t.Run("", func(t *testing.T) { 203 | _, res := ValueAt(test.breaks, test.time, 0) 204 | if res != test.out { 205 | t.Fatalf("expected %v, got %v", test.out, res) 206 | } 207 | }) 208 | } 209 | } 210 | 211 | func TestAny(t *testing.T) { 212 | for _, test := range anyTests { 213 | t.Run("", func(t *testing.T) { 214 | out := test.in.Any(test.anyf) 215 | if out != test.out { 216 | t.Fatalf("Expected %v but got %v", test.out, out) 217 | } 218 | }) 219 | } 220 | } 221 | 222 | func TestMinMax(t *testing.T) { 223 | for _, test := range minMaxTests { 224 | t.Run("", func(t *testing.T) { 225 | min, max := MinMaxValue(test.in) 226 | if min != test.min || max != test.max { 227 | t.Fatalf("Expected (min,max) = (%v,%v) but got (%v,%v)", test.min, test.max, min, max) 228 | } 229 | }) 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /breakpoint/breakpoint.go: -------------------------------------------------------------------------------- 1 | package breakpoint 2 | 3 | // convenience functions for dealing with breakpoints 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // Breakpoints is a collection of Breakpoint (time:value) pairs 14 | type Breakpoints []Breakpoint 15 | 16 | // Breakpoint represents a time:value pair 17 | type Breakpoint struct { 18 | Time, Value float64 19 | } 20 | 21 | // BreakpointStream can be used to to treat breakpoints as a stream of data 22 | // Each 'tick' can manipulate the state of the breakpoint stream 23 | type BreakpointStream struct { 24 | Breakpoints Breakpoints 25 | Left Breakpoint 26 | Right Breakpoint 27 | IndexLeft int 28 | IndexRight int 29 | CurrentPosition float64 // current position in timeframes 30 | Increment float64 31 | Width float64 32 | Height float64 33 | HasMore bool 34 | } 35 | 36 | // Tick returns the next value in the breakpoint stream 37 | func (b *BreakpointStream) Tick() (out float64) { 38 | if !b.HasMore { 39 | // permanently the last value 40 | return b.Right.Value 41 | } 42 | if b.Width == 0.0 { 43 | out = b.Right.Value 44 | } else { 45 | // figure out value from linear interpolation 46 | frac := (float64(b.CurrentPosition) - b.Left.Time) / b.Width 47 | out = b.Left.Value + (b.Height * frac) 48 | } 49 | 50 | // prepare for next frame 51 | b.CurrentPosition += b.Increment 52 | if b.CurrentPosition > b.Right.Time { 53 | // move to next span 54 | b.IndexLeft++ 55 | b.IndexRight++ 56 | if b.IndexRight < len(b.Breakpoints) { 57 | b.Left = b.Breakpoints[b.IndexLeft] 58 | b.Right = b.Breakpoints[b.IndexRight] 59 | b.Width = b.Right.Time - b.Left.Time 60 | b.Height = b.Right.Value - b.Left.Value 61 | } else { 62 | // no more points 63 | b.HasMore = false 64 | } 65 | } 66 | return out 67 | } 68 | 69 | // NewBreakpointStream represents a slice of breakpoints streamed at a given sample rate 70 | func NewBreakpointStream(bs []Breakpoint, sr int) (*BreakpointStream, error) { 71 | if len(bs) == 0 { 72 | return nil, errors.New("Need at least two points to create a stream") 73 | } 74 | left, right := bs[0], bs[1] 75 | return &BreakpointStream{ 76 | Breakpoints: Breakpoints(bs), 77 | Increment: 1.0 / float64(sr), 78 | IndexLeft: 0, 79 | IndexRight: 1, 80 | CurrentPosition: 0, 81 | Left: left, 82 | Right: right, 83 | Width: right.Time - left.Time, // first span 84 | Height: right.Value - left.Value, // diff of first span 85 | HasMore: len(bs) > 0, 86 | }, nil 87 | } 88 | 89 | // ParseBreakpoints reads the breakpoints from an io.Reader 90 | // and turns them into a slice. 91 | // A file is expected to be [time: value] formatted 92 | // Will panic if file format is wrong 93 | // TODO: don't panic 94 | func ParseBreakpoints(in io.Reader) ([]Breakpoint, error) { 95 | data, err := ioutil.ReadAll(in) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | lines := strings.Split(string(data), "\n") 101 | 102 | brkpnts := []Breakpoint{} 103 | for _, line := range lines { 104 | line = strings.TrimSpace(line) 105 | if line == "" { 106 | continue 107 | } 108 | parts := strings.Split(line, ":") 109 | if len(parts) != 2 { 110 | return brkpnts, err 111 | } 112 | time := parts[0] 113 | value := parts[1] 114 | 115 | tf, err := strconv.ParseFloat(time, 64) 116 | if err != nil { 117 | return brkpnts, err 118 | } 119 | vf, err := strconv.ParseFloat(value, 64) 120 | if err != nil { 121 | return brkpnts, err 122 | } 123 | 124 | brkpnts = append(brkpnts, Breakpoint{ 125 | Time: tf, 126 | Value: vf, 127 | }) 128 | 129 | } 130 | return brkpnts, nil 131 | } 132 | 133 | // ValueAt returns the expected value at a given time (expressed as float64) by linear interpolation 134 | // Returns the index at which we found our value as well as the value itself. 135 | func ValueAt(bs []Breakpoint, time float64, startIndex int) (index int, value float64) { 136 | if len(bs) == 0 { 137 | return 0, 0 138 | } 139 | npoints := len(bs) 140 | 141 | // first we need to find a span containing our timeslot 142 | startSpan := startIndex // start of span 143 | for _, b := range bs[startSpan:] { 144 | if b.Time > time { 145 | break 146 | } 147 | startSpan++ 148 | } 149 | 150 | // Our span is never-ending (the last point in our breakpoint file was hit) 151 | if startSpan == npoints { 152 | return startSpan, bs[startSpan-1].Value 153 | } 154 | 155 | left := bs[startSpan-1] 156 | right := bs[startSpan] 157 | 158 | // check for instant jump 159 | // 2 points having the same time... 160 | width := right.Time - left.Time 161 | 162 | if width == 0 { 163 | return startSpan, right.Value 164 | } 165 | 166 | frac := (time - left.Time) / width 167 | 168 | val := left.Value + ((right.Value - left.Value) * frac) 169 | 170 | return startSpan, val 171 | } 172 | 173 | // MinMaxValue returns the smallest and largest value found in the breakpoint file 174 | func MinMaxValue(bs []Breakpoint) (smallest float64, largest float64) { 175 | // TODO: implement as SORT and return the first and last element 176 | if len(bs) == 0 { 177 | return 178 | } 179 | smallest = bs[0].Value 180 | largest = bs[0].Value 181 | for _, b := range bs[1:] { 182 | if b.Value < smallest { 183 | smallest = b.Value 184 | } else if b.Value > largest { 185 | largest = b.Value 186 | } else { 187 | // no op 188 | } 189 | } 190 | return 191 | } 192 | 193 | // Any returns true if any breakpoint matches the filter. 194 | func (bs Breakpoints) Any(f func(Breakpoint) bool) bool { 195 | for _, b := range bs { 196 | if f(b) { 197 | return true 198 | } 199 | } 200 | return false 201 | } 202 | -------------------------------------------------------------------------------- /wave/wavefile.go: -------------------------------------------------------------------------------- 1 | package wave 2 | 3 | // representation of the wave file, used by reader.go and writer.go 4 | 5 | // Frame is a single float64 value of raw audio data 6 | type Frame float64 7 | 8 | /* 9 | 10 | ╔════════╤════════════════╤══════╤═══════════════════════════════════════════════════╗ 11 | ║ Offset │ Field │ Size │ -- start of header ║ 12 | ╠════════╪════════════════╪══════╪═══════════════════════════════════════════════════╣ 13 | ║ 0 │ ChunkID │ 4 │ ║ 14 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 15 | ║ 4 │ ChunkSize │ 4 │ ║ 16 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 17 | ║ 8 │ Format │ 8 │ ║ 18 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 19 | ║ -- │ -- │ -- │ -- start of fmt ║ 20 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 21 | ║ 12 │ SubchunkID │ 4 │ ║ 22 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 23 | ║ 16 │ SubchunkSize │ 4 │ ║ 24 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 25 | ║ 20 │ AudioFormat │ 2 │ ║ 26 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 27 | ║ 22 │ NumChannels │ 2 │ ║ 28 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 29 | ║ 24 │ SampleRate │ 4 │ ║ 30 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 31 | ║ 28 │ ByteRate │ 4 │ ║ 32 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 33 | ║ 32 │ BlockAlign │ 2 │ ║ 34 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 35 | ║ 34 │ BitsPerSample │ 2 │ ║ 36 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 37 | ║ * 36 │ ExtraParamSize │ 2 │ Optional! Only when not PCM ║ 38 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 39 | ║ * 38 │ ExtraParams │ * │ Optional! Only when not PCM ║ 40 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 41 | ║ -- │ -- │ -- │ -- start of data, assuming PCM ║ 42 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 43 | ║ 36 │ Subchunk2ID │ 4 │ (offset by extra params of subchunk 1 if not PCM) ║ 44 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 45 | ║ 40 │ SubchunkSize │ 4 │ (offset by extra params of subchunk 1 if not PCM) ║ 46 | ╟────────┼────────────────┼──────┼───────────────────────────────────────────────────╢ 47 | ║ 44 │ Data │ * │ (offset by extra params of subchunk 1 if not PCM) ║ 48 | ╚════════╧════════════════╧══════╧═══════════════════════════════════════════════════╝ 49 | 50 | 51 | */ 52 | 53 | // Wave represents an entire .wav audio file 54 | type Wave struct { 55 | WaveHeader 56 | WaveFmt 57 | WaveData 58 | } 59 | 60 | // WaveHeader describes the header each WAVE file should start with 61 | type WaveHeader struct { 62 | ChunkID []byte // should be RIFF on little-endian or RIFX on big-endian systems.. 63 | ChunkSize int 64 | Format string // sanity-check, should be WAVE (//TODO: keep as []byte?) 65 | } 66 | 67 | // WaveFmt describes the format of the sound-information in the data subchunks 68 | type WaveFmt struct { 69 | Subchunk1ID []byte // should contain "fmt" 70 | Subchunk1Size int // 16 for PCM 71 | AudioFormat int // PCM = 1 (Linear Quantization), if not 1, compression was used. 72 | NumChannels int // Mono 1, Stereo = 2, .. 73 | SampleRate int // 44100 for CD-Quality, etc.. 74 | ByteRate int // SampleRate * NumChannels * BitsPerSample / 8 75 | BlockAlign int // NumChannels * BitsPerSample / 8 (number of bytes per sample) 76 | BitsPerSample int // 8 bits = 8, 16 bits = 16, .. :-) 77 | ExtraParamSize int // if not PCM, can contain extra params 78 | ExtraParams []byte // the actual extra params. 79 | } 80 | 81 | // WaveData contains the raw sound data 82 | type WaveData struct { 83 | Subchunk2ID []byte // Identifier of subchunk 84 | Subchunk2Size int // size of raw sound data 85 | RawData []byte // raw sound data itself 86 | Frames []Frame 87 | } 88 | 89 | // NewWaveFmt can be used to generate a complete WaveFmt by calculating the remaining props 90 | func NewWaveFmt(format, channels, samplerate, bitspersample int, extraparams []byte) WaveFmt { 91 | return WaveFmt{ 92 | Subchunk1ID: Format, 93 | Subchunk1Size: 16, // assume PCM for now 94 | AudioFormat: format, 95 | NumChannels: channels, 96 | SampleRate: samplerate, 97 | ByteRate: samplerate * channels * (bitspersample / 8.0), 98 | BlockAlign: channels * (bitspersample / 8), 99 | BitsPerSample: bitspersample, 100 | ExtraParamSize: len(extraparams), 101 | ExtraParams: extraparams, 102 | } 103 | } 104 | 105 | // SetChannels changes the FMT to adapt to a new amount of channels 106 | func (wfmt *WaveFmt) SetChannels(n uint) { 107 | wfmt.NumChannels = int(n) 108 | wfmt.ByteRate = (wfmt.SampleRate * wfmt.NumChannels * wfmt.BitsPerSample) / 8 109 | wfmt.BlockAlign = (wfmt.NumChannels * wfmt.BitsPerSample) / 8 110 | } 111 | -------------------------------------------------------------------------------- /wave/reader.go: -------------------------------------------------------------------------------- 1 | package wave 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "io/ioutil" 8 | "math" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | // type aliases for conversion functions 14 | type ( 15 | bytesToIntF func([]byte) int 16 | bytesToFloatF func([]byte) float64 17 | ) 18 | 19 | var ( 20 | // figure out which 'to int' function to use.. 21 | byteSizeToIntFunc = map[int]bytesToIntF{ 22 | 16: bits16ToInt, 23 | 24: bits24ToInt, 24 | 32: bits32ToInt, 25 | } 26 | 27 | byteSizeToFloatFunc = map[int]bytesToFloatF{ 28 | 16: bitsToFloat, 29 | 32: bitsToFloat, 30 | 64: bitsToFloat, 31 | } 32 | 33 | // max value depending on the bit size 34 | maxValues = map[int]int{ 35 | 8: math.MaxInt8, 36 | 16: math.MaxInt16, 37 | 32: math.MaxInt32, 38 | 64: math.MaxInt64, 39 | } 40 | ) 41 | 42 | // ReadWaveFile parses a .wave file into a Wave struct 43 | func ReadWaveFile(f string) (Wave, error) { 44 | // open as read-only file 45 | file, err := os.Open(f) 46 | if err != nil { 47 | return Wave{}, err 48 | } 49 | defer file.Close() 50 | 51 | return ReadWaveFromReader(file) 52 | } 53 | 54 | // ReadWaveFromReader parses an io.Reader into a Wave struct 55 | func ReadWaveFromReader(reader io.Reader) (Wave, error) { 56 | data, err := ioutil.ReadAll(reader) 57 | if err != nil { 58 | return Wave{}, err 59 | } 60 | 61 | data = deleteJunk(data) 62 | 63 | hdr := readHeader(data) 64 | 65 | wfmt := readFmt(data) 66 | 67 | wavdata := readData(data, wfmt) 68 | 69 | frames := parseRawData(wfmt, wavdata.RawData) 70 | wavdata.Frames = frames 71 | 72 | return Wave{ 73 | WaveHeader: hdr, 74 | WaveFmt: wfmt, 75 | WaveData: wavdata, 76 | }, nil 77 | } 78 | 79 | // for our wave format we expect double precision floats 80 | func bitsToFloat(b []byte) float64 { 81 | var bits uint64 82 | switch len(b) { 83 | case 2: 84 | bits = uint64(binary.LittleEndian.Uint16(b)) 85 | case 4: 86 | bits = uint64(binary.LittleEndian.Uint32(b)) 87 | case 8: 88 | bits = binary.LittleEndian.Uint64(b) 89 | default: 90 | panic("Can't parse to float..") 91 | } 92 | float := math.Float64frombits(bits) 93 | return float 94 | } 95 | 96 | func bits16ToInt(b []byte) int { 97 | if len(b) != 2 { 98 | panic("Expected size 4!") 99 | } 100 | var payload int16 101 | buf := bytes.NewReader(b) 102 | err := binary.Read(buf, binary.LittleEndian, &payload) 103 | if err != nil { 104 | // TODO: make safe 105 | panic(err) 106 | } 107 | return int(payload) // easier to work with ints 108 | } 109 | 110 | func bits24ToInt(b []byte) int { 111 | if len(b) != 3 { 112 | panic("Expected size 3!") 113 | } 114 | // add some padding to turn a 24-bit integer into a 32-bit integer 115 | b = append([]byte{0x00}, b...) 116 | var payload int32 117 | buf := bytes.NewReader(b) 118 | err := binary.Read(buf, binary.LittleEndian, &payload) 119 | if err != nil { 120 | // TODO: make safe 121 | panic(err) 122 | } 123 | return int(payload) // easier to work with ints 124 | } 125 | 126 | // turn a 32-bit byte array into an int 127 | func bits32ToInt(b []byte) int { 128 | if len(b) != 4 { 129 | panic("Expected size 4!") 130 | } 131 | var payload int32 132 | buf := bytes.NewReader(b) 133 | err := binary.Read(buf, binary.LittleEndian, &payload) 134 | if err != nil { 135 | // TODO: make safe 136 | panic(err) 137 | } 138 | return int(payload) // easier to work with ints 139 | } 140 | 141 | func readData(b []byte, wfmt WaveFmt) WaveData { 142 | wd := WaveData{} 143 | 144 | start := 36 + wfmt.ExtraParamSize 145 | subchunk2ID := b[start : start+4] 146 | wd.Subchunk2ID = subchunk2ID 147 | 148 | subsize := bits32ToInt(b[start+4 : start+8]) 149 | wd.Subchunk2Size = subsize 150 | 151 | wd.RawData = b[start+8:] 152 | 153 | return wd 154 | } 155 | 156 | // Should we do n-channel separation at this point? 157 | func parseRawData(wfmt WaveFmt, rawdata []byte) []Frame { 158 | bytesSampleSize := wfmt.BitsPerSample / 8 159 | // TODO: sanity-check that this is a power of 2? I think only those sample sizes are 160 | // possible 161 | 162 | frames := []Frame{} 163 | // read the chunks 164 | for i := 0; i < len(rawdata); i += bytesSampleSize { 165 | rawFrame := rawdata[i : i+bytesSampleSize] 166 | unscaledFrame := byteSizeToIntFunc[wfmt.BitsPerSample](rawFrame) 167 | scaled := scaleFrame(unscaledFrame, wfmt.BitsPerSample) 168 | frames = append(frames, scaled) 169 | } 170 | return frames 171 | } 172 | 173 | func scaleFrame(unscaled, bits int) Frame { 174 | maxV := maxValues[bits] 175 | return Frame(float64(unscaled) / float64(maxV)) 176 | 177 | } 178 | 179 | // deleteJunk will remove the JUNK chunks if they are present 180 | func deleteJunk(b []byte) []byte { 181 | var junkStart, junkEnd int 182 | 183 | for i := 0; i < len(b)-4; i++ { 184 | if strings.ToLower(string(b[i:i+4])) == "junk" { 185 | junkStart = i 186 | } 187 | 188 | if strings.ToLower(string(b[i:i+3])) == "fmt" { 189 | junkEnd = i 190 | } 191 | } 192 | 193 | if junkStart != 0 { 194 | cpy := make([]byte, len(b[0:junkStart])) 195 | copy(cpy, b[0:junkStart]) 196 | cpy = append(cpy, b[junkEnd:]...) 197 | return cpy 198 | } 199 | 200 | return b 201 | } 202 | 203 | // readFmt parses the FMT portion of the WAVE file 204 | // assumes the entire binary representation is passed! 205 | func readFmt(b []byte) WaveFmt { 206 | wfmt := WaveFmt{} 207 | subchunk1ID := b[12:16] 208 | wfmt.Subchunk1ID = subchunk1ID 209 | 210 | subchunksize := bits32ToInt(b[16:20]) 211 | wfmt.Subchunk1Size = subchunksize 212 | 213 | format := bits16ToInt(b[20:22]) 214 | wfmt.AudioFormat = format 215 | 216 | numChannels := bits16ToInt(b[22:24]) 217 | wfmt.NumChannels = numChannels 218 | 219 | sr := bits32ToInt(b[24:28]) 220 | wfmt.SampleRate = sr 221 | 222 | br := bits32ToInt(b[28:32]) 223 | wfmt.ByteRate = br 224 | 225 | ba := bits16ToInt(b[32:34]) 226 | wfmt.BlockAlign = ba 227 | 228 | bps := bits16ToInt(b[34:36]) 229 | wfmt.BitsPerSample = bps 230 | 231 | // parse extra (optional) elements.. 232 | 233 | if subchunksize != 16 { 234 | // only for compressed files (non-PCM) 235 | extraSize := bits16ToInt(b[36:38]) 236 | wfmt.ExtraParamSize = extraSize 237 | wfmt.ExtraParams = b[38 : 38+extraSize] 238 | } 239 | 240 | return wfmt 241 | } 242 | 243 | // TODO: make safe. 244 | func readHeader(b []byte) WaveHeader { 245 | // the start of the bte slice.. 246 | hdr := WaveHeader{} 247 | hdr.ChunkID = b[0:4] 248 | if string(hdr.ChunkID) != "RIFF" { 249 | panic("Invalid file") 250 | } 251 | 252 | chunkSize := b[4:8] 253 | var size uint32 254 | buf := bytes.NewReader(chunkSize) 255 | err := binary.Read(buf, binary.LittleEndian, &size) 256 | if err != nil { 257 | panic(err) 258 | } 259 | hdr.ChunkSize = int(size) // easier to work with ints 260 | 261 | format := b[8:12] 262 | if string(format) != "WAVE" { 263 | panic("Format should be WAVE") 264 | } 265 | hdr.Format = string(format) 266 | return hdr 267 | } 268 | -------------------------------------------------------------------------------- /cmd/sine/out.txt: -------------------------------------------------------------------------------- 1 | 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000 0 2 | 0.125333233564304258322863461216911673545837402343750000000000000000000000000000000 0 3 | 0.248689887164854794843193985798279754817485809326171875000000000000000000000000000 0 4 | 0.368124552684677974756510820952826179563999176025390625000000000000000000000000000 0 5 | 0.481753674101715323452310713037149980664253234863281250000000000000000000000000000 0 6 | 0.587785252292473137103456792829092592000961303710937500000000000000000000000000000 1 7 | 0.684547105928688726095288075157441198825836181640625000000000000000000000000000000 1 8 | 0.770513242775789253258267308410722762346267700195312500000000000000000000000000000 1 9 | 0.844327925502015075309714120521675795316696166992187500000000000000000000000000000 1 10 | 0.904827052466019465803981347562512382864952087402343750000000000000000000000000000 1 11 | 0.951056516295153531181938433292089030146598815917968750000000000000000000000000000 1 12 | 0.982287250728688721146397710981545969843864440917968750000000000000000000000000000 1 13 | 0.998026728428271558968276622181292623281478881835937500000000000000000000000000000 1 14 | 0.998026728428271558968276622181292623281478881835937500000000000000000000000000000 1 15 | 0.982287250728688721146397710981545969843864440917968750000000000000000000000000000 1 16 | 0.951056516295153531181938433292089030146598815917968750000000000000000000000000000 1 17 | 0.904827052466019465803981347562512382864952087402343750000000000000000000000000000 1 18 | 0.844327925502014964287411658006021752953529357910156250000000000000000000000000000 1 19 | 0.770513242775789253258267308410722762346267700195312500000000000000000000000000000 1 20 | 0.684547105928688504050683150126133114099502563476562500000000000000000000000000000 1 21 | 0.587785252292473248125759255344746634364128112792968750000000000000000000000000000 1 22 | 0.481753674101715212430008250521495938301086425781250000000000000000000000000000000 0 23 | 0.368124552684677697200754664663691073656082153320312500000000000000000000000000000 0 24 | 0.248689887164854794843193985798279754817485809326171875000000000000000000000000000 0 25 | 0.125333233564304091789409767443430610001087188720703125000000000000000000000000000 0 26 | -0.000000000000000321624529935327468015399281386820898353054443141729734634282067418 -0 27 | -0.125333233564304313834014692474738694727420806884765625000000000000000000000000000 -0 28 | -0.248689887164855016887798910829587839543819427490234375000000000000000000000000000 -0 29 | -0.368124552684678363334569439757615327835083007812500000000000000000000000000000000 -0 30 | -0.481753674101715378963461944294977001845836639404296875000000000000000000000000000 -0 31 | -0.587785252292473359148061717860400676727294921875000000000000000000000000000000000 -1 32 | -0.684547105928688726095288075157441198825836181640625000000000000000000000000000000 -1 33 | -0.770513242775789253258267308410722762346267700195312500000000000000000000000000000 -1 34 | -0.844327925502015297354319045552983880043029785156250000000000000000000000000000000 -1 35 | -0.904827052466019798870888735109474509954452514648437500000000000000000000000000000 -1 36 | -0.951056516295153531181938433292089030146598815917968750000000000000000000000000000 -1 37 | -0.982287250728688721146397710981545969843864440917968750000000000000000000000000000 -1 38 | -0.998026728428271558968276622181292623281478881835937500000000000000000000000000000 -1 39 | -0.998026728428271558968276622181292623281478881835937500000000000000000000000000000 -1 40 | -0.982287250728688610124095248465891927480697631835937500000000000000000000000000000 -1 41 | -0.951056516295153642204240895807743072509765625000000000000000000000000000000000000 -1 42 | -0.904827052466019465803981347562512382864952087402343750000000000000000000000000000 -1 43 | -0.844327925502015075309714120521675795316696166992187500000000000000000000000000000 -1 44 | -0.770513242775789031213662383379414677619934082031250000000000000000000000000000000 -1 45 | -0.684547105928688282006078225094825029373168945312500000000000000000000000000000000 -1 46 | -0.587785252292472581991944480250822380185127258300781250000000000000000000000000000 -1 47 | -0.481753674101715323452310713037149980664253234863281250000000000000000000000000000 -0 48 | -0.368124552684677808223057127179345116019248962402343750000000000000000000000000000 -0 49 | -0.248689887164854489531862213880231138318777084350585937500000000000000000000000000 -0 50 | -0.125333233564303786478077995525381993502378463745117187500000000000000000000000000 -0 51 | --------------------------------------------------------------------------------