├── .gitignore ├── demo ├── .gitignore ├── sound │ └── 808 │ │ ├── tom1.wav │ │ ├── claves.wav │ │ ├── conga1.wav │ │ ├── cowbell.wav │ │ ├── hightom.wav │ │ ├── kick1.wav │ │ ├── kick2.wav │ │ ├── maracas.wav │ │ ├── open_hh.wav │ │ ├── rimshot.wav │ │ ├── snare.wav │ │ ├── cl_hihat.wav │ │ ├── crashcym.wav │ │ ├── handclap.wav │ │ └── hi_conga.wav └── demo.go ├── bind ├── debug │ ├── debug_test.go │ └── debug.go ├── opt │ ├── option_test.go │ └── option.go ├── wav │ ├── testdata │ │ └── .gitignore │ ├── wav_test.go │ ├── format_test.go │ ├── reader_test.go │ ├── wav.go │ ├── writer.go │ ├── writer_test.go │ ├── format.go │ └── reader.go ├── spec │ ├── spec_test.go │ └── spec.go ├── sample │ ├── sample_test.go │ ├── sample.go │ ├── out_test.go │ ├── out.go │ ├── value_test.go │ └── value.go ├── hardware │ └── null │ │ ├── out_test.go │ │ └── out.go ├── sox │ └── sox.go ├── api_test.go └── api.go ├── docs ├── attack-decay-sustain-release.png └── LogarithmicDynamicRangeCompression-PaulVogler.pdf ├── lib ├── source │ ├── testdata │ │ ├── Signed16bitLittleEndian44100HzMono.wav │ │ └── Float32bitLittleEndian48000HzEstéreo.wav │ ├── storage_test.go │ ├── storage.go │ ├── source.go │ └── source_test.go ├── fire │ ├── fire_test.go │ └── fire.go └── mix │ ├── mix_test.go │ └── mix.go ├── .travis.yml ├── go.mod ├── .editorconfig ├── Makefile ├── go.sum ├── LICENSE ├── mix_test.go ├── README.md └── mix.go /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | *.iml 3 | *.out 4 | .idea/ 5 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore any output files in the root dir 2 | /*.wav 3 | -------------------------------------------------------------------------------- /bind/debug/debug_test.go: -------------------------------------------------------------------------------- 1 | // Package debug for debugging 2 | package debug 3 | -------------------------------------------------------------------------------- /bind/opt/option_test.go: -------------------------------------------------------------------------------- 1 | // Package opt specifies valid options 2 | package opt 3 | -------------------------------------------------------------------------------- /bind/wav/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | # Test writes temporary files 2 | tmp/ 3 | *.tmp 4 | -------------------------------------------------------------------------------- /bind/spec/spec_test.go: -------------------------------------------------------------------------------- 1 | // Package spec specifies valid audio formats 2 | package spec 3 | -------------------------------------------------------------------------------- /demo/sound/808/tom1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/tom1.wav -------------------------------------------------------------------------------- /demo/sound/808/claves.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/claves.wav -------------------------------------------------------------------------------- /demo/sound/808/conga1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/conga1.wav -------------------------------------------------------------------------------- /demo/sound/808/cowbell.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/cowbell.wav -------------------------------------------------------------------------------- /demo/sound/808/hightom.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/hightom.wav -------------------------------------------------------------------------------- /demo/sound/808/kick1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/kick1.wav -------------------------------------------------------------------------------- /demo/sound/808/kick2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/kick2.wav -------------------------------------------------------------------------------- /demo/sound/808/maracas.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/maracas.wav -------------------------------------------------------------------------------- /demo/sound/808/open_hh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/open_hh.wav -------------------------------------------------------------------------------- /demo/sound/808/rimshot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/rimshot.wav -------------------------------------------------------------------------------- /demo/sound/808/snare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/snare.wav -------------------------------------------------------------------------------- /demo/sound/808/cl_hihat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/cl_hihat.wav -------------------------------------------------------------------------------- /demo/sound/808/crashcym.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/crashcym.wav -------------------------------------------------------------------------------- /demo/sound/808/handclap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/handclap.wav -------------------------------------------------------------------------------- /demo/sound/808/hi_conga.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/demo/sound/808/hi_conga.wav -------------------------------------------------------------------------------- /bind/sample/sample_test.go: -------------------------------------------------------------------------------- 1 | package sample 2 | 3 | type TestSample struct { 4 | Values []float64 5 | } 6 | -------------------------------------------------------------------------------- /docs/attack-decay-sustain-release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/docs/attack-decay-sustain-release.png -------------------------------------------------------------------------------- /docs/LogarithmicDynamicRangeCompression-PaulVogler.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/docs/LogarithmicDynamicRangeCompression-PaulVogler.pdf -------------------------------------------------------------------------------- /lib/source/testdata/Signed16bitLittleEndian44100HzMono.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/lib/source/testdata/Signed16bitLittleEndian44100HzMono.wav -------------------------------------------------------------------------------- /lib/source/testdata/Float32bitLittleEndian48000HzEstéreo.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-mix/mix/HEAD/lib/source/testdata/Float32bitLittleEndian48000HzEstéreo.wav -------------------------------------------------------------------------------- /bind/wav/wav_test.go: -------------------------------------------------------------------------------- 1 | // Package wav is direct WAV filo I/O 2 | package wav 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | func TestLoad(t *testing.T) { 9 | // TODO 10 | } 11 | -------------------------------------------------------------------------------- /bind/sample/sample.go: -------------------------------------------------------------------------------- 1 | package sample 2 | 3 | type Sample struct { 4 | Values []Value 5 | } 6 | 7 | func New(values []Value) Sample { 8 | return Sample{ 9 | Values: values, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | dist: trusty 3 | 4 | go: 5 | - 1.11 6 | 7 | install: 8 | - sudo apt-get install -y libsox-dev 9 | - export GO111MODULE="on" 10 | - go get ./... 11 | 12 | script: 13 | - go test ./... 14 | -------------------------------------------------------------------------------- /bind/hardware/null/out_test.go: -------------------------------------------------------------------------------- 1 | // Package null is for modular binding of mix to a null (mock) audio interface 2 | package null 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | func TestSetup(t *testing.T) { 9 | // TODO 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-mix/mix 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/krig/go-sox v0.0.0-20180617124112-7d2f8ae31981 7 | github.com/stretchr/testify v1.4.0 8 | github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb 9 | gopkg.in/pkg/profile.v1 v1.3.0 10 | ) 11 | -------------------------------------------------------------------------------- /bind/wav/format_test.go: -------------------------------------------------------------------------------- 1 | // Package wav is direct WAV filo I/O 2 | package wav 3 | 4 | import ( 5 | // "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestFormat(t *testing.T) { 10 | // TODO 11 | } 12 | 13 | func TestFormatBlockAlign(t *testing.T) { 14 | // TODO 15 | } 16 | 17 | func TestByteRate(t *testing.T) { 18 | // TODO 19 | } 20 | -------------------------------------------------------------------------------- /bind/hardware/null/out.go: -------------------------------------------------------------------------------- 1 | // Package null is for modular binding of mix to a null (mock) audio interface 2 | package null 3 | 4 | import ( 5 | "github.com/go-mix/mix/bind/sample" 6 | "github.com/go-mix/mix/bind/spec" 7 | ) 8 | 9 | func ConfigureOutput(s spec.AudioSpec) { 10 | go func() { 11 | for { 12 | sample.OutNextBytes() 13 | } 14 | }() 15 | // nothing to do 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.go] 13 | indent_style = tab 14 | indent_size = 4 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | [Makefile] 20 | indent_style = tab 21 | indent_size = 4 22 | -------------------------------------------------------------------------------- /bind/sample/out_test.go: -------------------------------------------------------------------------------- 1 | // Package sample models an audio sample 2 | package sample 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | func TestOut_useWAV(t *testing.T) { 9 | // TODO 10 | } 11 | 12 | func TestOut_useOutput(t *testing.T) { 13 | // TODO 14 | } 15 | 16 | func TestOut_outSpec(t *testing.T) { 17 | // TODO 18 | } 19 | 20 | func TestOut_outCallbackSample(t *testing.T) { 21 | // TODO 22 | } 23 | 24 | func TestOut_outCallbackFunc(t *testing.T) { 25 | // TODO 26 | } 27 | 28 | func TestOut_outNextBytes(t *testing.T) { 29 | // TODO 30 | } 31 | -------------------------------------------------------------------------------- /bind/opt/option.go: -------------------------------------------------------------------------------- 1 | // Package opt specifies valid options 2 | package opt 3 | 4 | // OptLoader represents an audio input option 5 | type Input string 6 | 7 | // OptLoadWav to use Go-Native WAV file I/O 8 | const ( 9 | InputWAV Input = "wav" 10 | InputSOX Input = "sox" 11 | ) 12 | 13 | // OptOutput represents an audio output option 14 | type Output string 15 | 16 | // OptOutputNull for benchmarking/profiling, because those tools are unable to sample to C-go callback tree 17 | const OutputNull Output = "null" 18 | 19 | // OptOutputWAV to use WAV directly for []byte to stdout 20 | const OutputWAV Output = "wav" 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | TESTS := expr unrecognised 3 | 4 | .PHONY: test profile fmt demo clean cover 5 | 6 | fmt: 7 | go fmt ./... 8 | 9 | test: 10 | go get -v ./... && go test ./... 11 | 12 | demo: 13 | cd demo && go get -v && go run demo.go 14 | 15 | demo.wav: 16 | cd demo && go get -v && go run demo.go -out wav > output.wav 17 | 18 | profile: 19 | cd demo && go get -v && go run demo.go --profile cpu 20 | 21 | clean: 22 | rm *.out bind/*.out 23 | 24 | cover: # test coverage 25 | go test -coverprofile cover.out && go tool cover -html=cover.out 26 | cd bind && go test -coverprofile cover.out && go tool cover -html=cover.out 27 | -------------------------------------------------------------------------------- /lib/source/storage_test.go: -------------------------------------------------------------------------------- 1 | // Package source models a single audio source 2 | package source 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | func TestPrepare(t *testing.T) { 9 | // TODO: test Prepare a source by ensuring it is stored in memory. 10 | } 11 | 12 | func TestGet(t *testing.T) { 13 | // TODO: test Get a source from storage 14 | } 15 | 16 | func TestGetLength(t *testing.T) { 17 | // TODO: test Get a source from storage 18 | } 19 | 20 | func TestPrune(t *testing.T) { 21 | // TODO: test Prune to keep only the sources in this list 22 | } 23 | 24 | func TestCount(t *testing.T) { 25 | // TODO: test Count the number of sources in memory 26 | } 27 | -------------------------------------------------------------------------------- /bind/wav/reader_test.go: -------------------------------------------------------------------------------- 1 | // Package wav is direct WAV filo I/O 2 | package wav 3 | 4 | import ( 5 | // "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestReaderOpen(t *testing.T) { 10 | // TODO 11 | } 12 | 13 | func TestReaderFormat(t *testing.T) { 14 | // TODO 15 | } 16 | 17 | func TestReaderReadSamples(t *testing.T) { 18 | // TODO 19 | } 20 | 21 | func TestReaderReadSamplesIntoBuffer(t *testing.T) { 22 | // TODO 23 | } 24 | 25 | func TestReaderSampleFromBytes(t *testing.T) { 26 | // TODO 27 | } 28 | 29 | func TestReaderOpenAndParse(t *testing.T) { 30 | // TODO 31 | } 32 | 33 | func TestReaderReadData(t *testing.T) { 34 | // TODO 35 | } 36 | -------------------------------------------------------------------------------- /bind/debug/debug.go: -------------------------------------------------------------------------------- 1 | // Package debug for debugging 2 | package debug 3 | 4 | import ( 5 | "log" 6 | "os" 7 | ) 8 | 9 | // Debug ON/OFF (ripples down to all sub-modules) 10 | func Configure(active bool) { 11 | isActive = active 12 | } 13 | 14 | // Printf only when debug is active 15 | func Printf(format string, args ...interface{}) { 16 | if isActive { 17 | logger.Printf(format, args...) 18 | } 19 | } 20 | 21 | // Active returns current state of debug 22 | func Active() bool { 23 | return isActive 24 | } 25 | 26 | // 27 | // Private 28 | // 29 | 30 | var ( 31 | isActive bool 32 | logger *log.Logger 33 | ) 34 | 35 | func init() { 36 | logger = log.New(os.Stderr, "", 0) 37 | } 38 | -------------------------------------------------------------------------------- /bind/wav/wav.go: -------------------------------------------------------------------------------- 1 | // Package wav is direct WAV filo I/O 2 | package wav 3 | 4 | import ( 5 | "io" 6 | "os" 7 | 8 | "github.com/go-mix/mix/bind/sample" 9 | "github.com/go-mix/mix/bind/spec" 10 | ) 11 | 12 | // Load a WAV file into memory 13 | func Load(path string) (out []sample.Sample, specs *spec.AudioSpec) { 14 | if _, err := os.Stat(path); os.IsNotExist(err) { 15 | panic("File not found: " + path) 16 | } 17 | file, _ := os.Open(path) 18 | reader, err := NewReader(file) 19 | if err != nil { 20 | panic(err) 21 | } 22 | specs = &spec.AudioSpec{ 23 | Freq: float64(reader.Format.SampleRate), 24 | Format: reader.AudioFormat, 25 | Channels: int(reader.Format.NumChannels), 26 | } 27 | for { 28 | samples, err := reader.ReadSamples() 29 | if err == io.EOF { 30 | break 31 | } 32 | out = append(out, samples...) 33 | } 34 | return 35 | } 36 | 37 | // 38 | // Private 39 | // 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/krig/go-sox v0.0.0-20180617124112-7d2f8ae31981 h1:ir4NRMjkkSP63kAOiFDTQN3dcs0o6c0f1cfpe9V8JT0= 3 | github.com/krig/go-sox v0.0.0-20180617124112-7d2f8ae31981/go.mod h1:0uPmTzngejep+JBRxvlmijKKMexuskpMCzWFp+oFzc4= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 7 | github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb h1:RDh7U5Di6o7fblIBe7rVi9KnrcOXUbLwvvLLdP2InSI= 8 | github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/pkg/profile.v1 v1.3.0/go.mod h1:knhHpoyiu3zB9bR/uG9+s8jTFrCOFA3g9Xkh/NCDzJ4= 11 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Outright Mental Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /bind/sox/sox.go: -------------------------------------------------------------------------------- 1 | // Package sox is for file I/O via go-sox package 2 | package sox 3 | 4 | import ( 5 | "github.com/go-mix/mix/bind/sample" 6 | "github.com/go-mix/mix/bind/spec" 7 | sox "github.com/krig/go-sox" 8 | ) 9 | 10 | const ChunkSize = 2048 11 | 12 | // Load sound file into memory 13 | func Load(path string) (out []sample.Sample, specs *spec.AudioSpec) { 14 | file := sox.OpenRead(path) 15 | if file == nil { 16 | panic("Sox can't open file: " + path) 17 | } 18 | defer file.Release() 19 | info := file.Signal() 20 | specs = &spec.AudioSpec{ 21 | Freq: info.Rate(), 22 | Format: spec.AudioS32, 23 | Channels: int(info.Channels()), 24 | } 25 | buffer := make([]sox.Sample, ChunkSize*specs.Channels) 26 | for { 27 | size := file.Read(buffer, uint(len(buffer))) 28 | if size == 0 || size == sox.EOF { 29 | break 30 | } 31 | outBuffer := make([]sample.Value, size) 32 | for offset := 0; offset < int(size); offset += specs.Channels { 33 | values := outBuffer[offset : offset+specs.Channels] 34 | for c := 0; c < specs.Channels; c++ { 35 | values[c] = sample.Value(sox.SampleToFloat64(buffer[offset+c])) 36 | } 37 | out = append(out, sample.New(values)) 38 | } 39 | } 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /bind/api_test.go: -------------------------------------------------------------------------------- 1 | // Package bind is for modular binding of mix to audio interface 2 | package bind 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/go-mix/mix/bind/opt" 10 | ) 11 | 12 | func TestAPI(t *testing.T) { 13 | // TODO 14 | } 15 | 16 | func TestAPI_UseWAV(t *testing.T) { 17 | UseLoader(opt.InputWAV) 18 | assert.Equal(t, opt.InputWAV, useLoader) 19 | } 20 | 21 | func TestAPI_UseWAVString(t *testing.T) { 22 | UseLoaderString("wav") 23 | assert.Equal(t, opt.InputWAV, useLoader) 24 | } 25 | 26 | func TestAPI_UseWAVString_Fail(t *testing.T) { 27 | defer func() { 28 | msg := recover() 29 | assert.IsType(t, "", msg) 30 | assert.Equal(t, "No such Loader: this-will-panic", msg) 31 | }() 32 | UseLoaderString("this-will-panic") 33 | } 34 | 35 | func TestAPI_UseOutput(t *testing.T) { 36 | UseOutput(opt.OutputNull) 37 | assert.Equal(t, opt.OutputNull, useOutput) 38 | } 39 | 40 | func TestAPI_UseOutputString(t *testing.T) { 41 | UseOutputString("wav") 42 | assert.Equal(t, opt.OutputWAV, useOutput) 43 | } 44 | 45 | func TestAPI_UseOutputString_Fail(t *testing.T) { 46 | defer func() { 47 | msg := recover() 48 | assert.IsType(t, "", msg) 49 | assert.Equal(t, "No such Output: this-will-panic", msg) 50 | }() 51 | UseOutputString("this-will-panic") 52 | } 53 | 54 | func TestAPI_noErr(t *testing.T) { 55 | //TODO: Test 56 | } 57 | -------------------------------------------------------------------------------- /lib/source/storage.go: -------------------------------------------------------------------------------- 1 | // Package source models a single audio source 2 | package source 3 | 4 | import ( 5 | "github.com/go-mix/mix/bind/spec" 6 | "sync" 7 | ) 8 | 9 | // Prepare a source by ensuring it is stored in memory. 10 | func Prepare(src string) { 11 | storageMutex.Lock() 12 | defer storageMutex.Unlock() 13 | if _, exists := storage[src]; !exists { 14 | storage[src] = New(src) 15 | } 16 | } 17 | 18 | // Get a source from storage 19 | func Get(src string) *Source { 20 | storageMutex.Lock() 21 | defer storageMutex.Unlock() 22 | if _, ok := storage[src]; ok { 23 | return storage[src] 24 | } else { 25 | return nil 26 | } 27 | } 28 | 29 | func GetLength(src string) spec.Tz { 30 | source := Get(src) 31 | if source != nil { 32 | return source.Length() 33 | } else { 34 | return spec.Tz(0) 35 | } 36 | } 37 | 38 | // Prune to keep only the sources in this list 39 | func Prune(keep map[string]bool) { 40 | storageMutex.Lock() 41 | defer storageMutex.Unlock() 42 | for key, _ := range storage { 43 | if _, exists := keep[key]; !exists { 44 | delete(storage, key) 45 | } 46 | } 47 | } 48 | 49 | // Count the number of sources in memory 50 | func Count() int { 51 | storageMutex.Lock() 52 | defer storageMutex.Unlock() 53 | return len(storage) 54 | } 55 | 56 | // 57 | // Private 58 | // 59 | 60 | var ( 61 | storage map[string]*Source 62 | storageMutex = &sync.Mutex{} 63 | ) 64 | 65 | func init() { 66 | storage = make(map[string]*Source, 0) 67 | } 68 | -------------------------------------------------------------------------------- /bind/wav/writer.go: -------------------------------------------------------------------------------- 1 | // Package wav is direct WAV filo I/O 2 | package wav 3 | 4 | import ( 5 | "encoding/binary" 6 | "io" 7 | 8 | riff "github.com/youpy/go-riff" 9 | 10 | "time" 11 | 12 | "github.com/go-mix/mix/bind/sample" 13 | "github.com/go-mix/mix/bind/spec" 14 | ) 15 | 16 | func ConfigureOutput(s spec.AudioSpec) { 17 | outputSpec = &s 18 | } 19 | 20 | func OutputStart(length time.Duration, out io.Writer) { 21 | writer = NewWriter(out, FormatFromSpec(outputSpec), length) 22 | } 23 | 24 | func TeardownOutput() { 25 | // nothing to do 26 | } 27 | 28 | type Writer struct { 29 | io.Writer 30 | Format *Format 31 | } 32 | 33 | func NewWriter(w io.Writer, format Format, length time.Duration) (writer *Writer) { 34 | dataSize := uint32(float64(length/time.Second)*float64(format.SampleRate)) * uint32(format.BlockAlign) 35 | riffSize := 4 + 8 + 16 + 8 + dataSize 36 | riffWriter := riff.NewWriter(w, []byte("WAVE"), riffSize) 37 | 38 | writer = &Writer{riffWriter, &format} 39 | riffWriter.WriteChunk([]byte("fmt "), 16, func(w io.Writer) { 40 | binary.Write(w, binary.LittleEndian, format) 41 | }) 42 | riffWriter.WriteChunk([]byte("data"), dataSize, func(w io.Writer) {}) 43 | 44 | return writer 45 | } 46 | 47 | func OutputNext(numSamples spec.Tz) (err error) { 48 | for n := spec.Tz(0); n < numSamples; n++ { 49 | writer.Write(sample.OutNextBytes()) 50 | } 51 | return 52 | } 53 | 54 | // 55 | // Private 56 | // 57 | 58 | var ( 59 | writer *Writer 60 | outputSpec *spec.AudioSpec 61 | ) 62 | -------------------------------------------------------------------------------- /bind/sample/out.go: -------------------------------------------------------------------------------- 1 | // Package sample models an audio sample 2 | package sample 3 | 4 | import ( 5 | "github.com/go-mix/mix/bind/spec" 6 | ) 7 | 8 | // OutNextCallbackFunc to stream mix out from mix 9 | type OutNextCallbackFunc func() []Value 10 | 11 | func ConfigureOutput(s spec.AudioSpec) { 12 | outSpec = &s 13 | } 14 | 15 | // SetOutNextCallback to set streaming callback function 16 | func SetOutputCallback(fn OutNextCallbackFunc) { 17 | outNextCallback = fn 18 | } 19 | 20 | // OutNext to mix the next sample for all channels, in []float64 21 | func OutNext() []Value { 22 | return outNextCallback() 23 | } 24 | 25 | // OutNextBytes to mix the next sample for all channels, in bytes 26 | func OutNextBytes() (out []byte) { 27 | in := outNextCallback() 28 | for ch := 0; ch < outSpec.Channels; ch++ { 29 | switch outSpec.Format { 30 | case spec.AudioU8: 31 | out = append(out, in[ch].ToByteU8()) 32 | case spec.AudioS8: 33 | out = append(out, in[ch].ToByteS8()) 34 | case spec.AudioS16: 35 | out = append(out, in[ch].ToBytesS16LSB()...) 36 | case spec.AudioU16: 37 | out = append(out, in[ch].ToBytesU16LSB()...) 38 | case spec.AudioS32: 39 | out = append(out, in[ch].ToBytesS32LSB()...) 40 | case spec.AudioF32: 41 | out = append(out, in[ch].ToBytesF32LSB()...) 42 | case spec.AudioF64: 43 | out = append(out, in[ch].ToBytesF64LSB()...) 44 | } 45 | } 46 | return 47 | } 48 | 49 | // 50 | // Private 51 | // 52 | 53 | var ( 54 | outSpec *spec.AudioSpec 55 | outNextCallback OutNextCallbackFunc 56 | ) 57 | -------------------------------------------------------------------------------- /bind/spec/spec.go: -------------------------------------------------------------------------------- 1 | // Package spec specifies valid audio formats 2 | package spec 3 | 4 | import ( 5 | "time" 6 | ) 7 | 8 | // Tz is the unit of measurement of samples-over-time, e.g. for 48000Hz playback there are 48,000 Tz in 1 second. 9 | type Tz uint64 10 | 11 | // AudioSpec represents the frequency, format, # channels and sample rate of any audio I/O 12 | type AudioSpec struct { 13 | Freq float64 14 | Format AudioFormat 15 | Channels int 16 | Length time.Duration 17 | } 18 | 19 | // Validate these specs 20 | func (spec *AudioSpec) Validate() { 21 | if spec.Freq == 0 { 22 | panic("Must specify Frequency") 23 | } 24 | if spec.Freq < 0 { 25 | panic("Must specify a mixing frequency greater than zero.") 26 | } 27 | if spec.Format == "" { 28 | panic("Must specify Format") 29 | } 30 | if spec.Channels == 0 { 31 | panic("Must specify Channels") 32 | } 33 | } 34 | 35 | // AudioFormat represents the bit allocation for a single sample of audio 36 | type AudioFormat string 37 | 38 | // AudioU8 is unsigned-integer 8-bit sample (per channel) 39 | const AudioU8 AudioFormat = "U8" 40 | 41 | // AudioS8 is signed-integer 8-bit sample (per channel) 42 | const AudioS8 AudioFormat = "S8" 43 | 44 | // AudioU16 is unsigned-integer 16-bit sample (per channel) 45 | const AudioU16 AudioFormat = "U16" 46 | 47 | // AudioS16 is signed-integer 16-bit sample (per channel) 48 | const AudioS16 AudioFormat = "S16" 49 | 50 | // AudioS32 is signed-integer 32-bit sample (per channel) 51 | const AudioS32 AudioFormat = "S32" 52 | 53 | // AudioF32 is floating-point 32-bit sample (per channel) 54 | const AudioF32 AudioFormat = "F32" 55 | 56 | // AudioF64 is floating-point 64-bit sample (per channel) 57 | const AudioF64 AudioFormat = "F64" 58 | -------------------------------------------------------------------------------- /bind/wav/writer_test.go: -------------------------------------------------------------------------------- 1 | // Package wav is direct WAV filo I/O 2 | package wav 3 | 4 | import ( 5 | //"io/ioutil" 6 | //"os" 7 | //"log" 8 | "testing" 9 | //"github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestConfigureOutput(t *testing.T) { 13 | // TODO 14 | } 15 | 16 | func TestTeardownOutput(t *testing.T) { 17 | // TODO 18 | } 19 | 20 | func TestWrite(t *testing.T) { 21 | //outfile, err := ioutil.TempFile("/tmp", "outfile") 22 | //if err != nil { 23 | // t.Fatal(err) 24 | //} 25 | //log.Printf("TempFile: %+v", outfile.Name()) 26 | // 27 | //defer func() { 28 | // outfile.Close() 29 | // os.Remove(outfile.Name()) 30 | //}() 31 | // 32 | //var numSamples uint32 = 2 33 | //var numChannels uint16 = 2 34 | //var sampleRate uint32 = 44100 35 | //var bitsPerSample uint16 = 32 36 | // 37 | //writer := NewWriter(outfile, AudioFormatIEEEFloat, numSamples, numChannels, sampleRate, bitsPerSample) 38 | //if writer == nil { 39 | // t.Fatal(err) 40 | //} 41 | // 42 | //outfile.Close() 43 | //file, err := os.Open(outfile.Name()) 44 | //if err != nil { 45 | // t.Fatal(err) 46 | //} 47 | // 48 | //defer func() { 49 | // file.Close() 50 | // os.Remove(outfile.Name()) 51 | //}() 52 | // 53 | //reader, err := NewReader(file) 54 | //if err != nil { 55 | // t.Fatal(err) 56 | //} 57 | // 58 | //assert.Equal(t, reader.Format.SampleFormat, AudioFormatIEEEFloat) 59 | //assert.Equal(t, reader.Format.NumChannels, numChannels) 60 | //assert.Equal(t, reader.Format.SampleRate, sampleRate) 61 | //assert.Equal(t, reader.Format.ByteRate, sampleRate * 8) 62 | //assert.Equal(t, reader.Format.BlockAlign, numChannels*(bitsPerSample/8)) 63 | //assert.Equal(t, reader.Format.BitsPerSample, bitsPerSample) 64 | 65 | } 66 | -------------------------------------------------------------------------------- /lib/fire/fire_test.go: -------------------------------------------------------------------------------- 1 | // Package fire model an audio source playing at a specific time 2 | package fire 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/go-mix/mix/bind/spec" 10 | ) 11 | 12 | func TestBase(t *testing.T) { 13 | testLengthTz := spec.Tz(100) 14 | src := "sound.wav" 15 | bgnTz := spec.Tz(5984) 16 | endTz := bgnTz + testLengthTz 17 | vol := float64(1) 18 | pan := float64(0) 19 | fire := New(src, bgnTz, endTz, vol, pan) 20 | // before start: 21 | assert.Equal(t, spec.Tz(0), fire.At(bgnTz-2)) 22 | assert.Equal(t, spec.Tz(0), fire.At(bgnTz-1)) 23 | assert.Equal(t, fireStateReady, fire.state) 24 | assert.Equal(t, true, fire.IsAlive()) 25 | // start: 26 | assert.Equal(t, spec.Tz(0), fire.At(bgnTz)) 27 | assert.Equal(t, fireStatePlay, fire.state) 28 | assert.Equal(t, true, fire.IsAlive()) 29 | // after start / before end: 30 | for n := spec.Tz(1); n < testLengthTz; n++ { 31 | assert.Equal(t, spec.Tz(n), fire.At(bgnTz+n)) 32 | } 33 | // end: 34 | assert.Equal(t, testLengthTz, fire.At(endTz)) 35 | assert.Equal(t, fireStateDone, fire.state) 36 | assert.Equal(t, false, fire.IsAlive()) 37 | // after end: 38 | assert.Equal(t, spec.Tz(0), fire.At(endTz+1)) 39 | } 40 | 41 | func TestNewFire(t *testing.T) { 42 | // TODO 43 | } 44 | 45 | func TestAt(t *testing.T) { 46 | // TODO 47 | } 48 | 49 | func TestState(t *testing.T) { 50 | // TODO 51 | } 52 | 53 | func TestIsAlive(t *testing.T) { 54 | // TODO 55 | } 56 | 57 | func TestIsPlaying(t *testing.T) { 58 | // TODO 59 | } 60 | 61 | func TestSetState(t *testing.T) { 62 | // TODO 63 | } 64 | 65 | func TestSourceLength(t *testing.T) { 66 | // TODO 67 | } 68 | 69 | func TestTeardown(t *testing.T) { 70 | // TODO 71 | } 72 | -------------------------------------------------------------------------------- /bind/wav/format.go: -------------------------------------------------------------------------------- 1 | // Package wav is direct WAV filo I/O 2 | package wav 3 | 4 | import ( 5 | "github.com/go-mix/mix/bind/spec" 6 | "io" 7 | ) 8 | 9 | // the Format struct must be in the exact order according 10 | // to WAV specifications, such that a binary.Read(...) 11 | // can assign the WAV specified "fmt" header bytes 12 | // to the correct Format properties. 13 | type Format struct { 14 | SampleFormat SampleFormat 15 | NumChannels uint16 16 | SampleRate uint32 17 | ByteRate uint32 18 | BlockAlign uint16 19 | BitsPerSample uint16 20 | } 21 | 22 | func FormatFromSpec(s *spec.AudioSpec) Format { 23 | format := Format{} 24 | switch s.Format { 25 | case spec.AudioU8: 26 | format.SampleFormat = AudioFormatLinearPCM 27 | format.BitsPerSample = 8 28 | case spec.AudioS8: 29 | format.SampleFormat = AudioFormatLinearPCM 30 | format.BitsPerSample = 8 31 | case spec.AudioU16: 32 | format.SampleFormat = AudioFormatLinearPCM 33 | format.BitsPerSample = 16 34 | case spec.AudioS16: 35 | format.SampleFormat = AudioFormatLinearPCM 36 | format.BitsPerSample = 16 37 | case spec.AudioS32: 38 | format.SampleFormat = AudioFormatLinearPCM 39 | format.BitsPerSample = 32 40 | case spec.AudioF32: 41 | format.SampleFormat = AudioFormatIEEEFloat 42 | format.BitsPerSample = 32 43 | case spec.AudioF64: 44 | format.SampleFormat = AudioFormatIEEEFloat 45 | format.BitsPerSample = 64 46 | } 47 | format.NumChannels = uint16(s.Channels) 48 | format.SampleRate = uint32(s.Freq) 49 | if format.ByteRate == 0 { 50 | format.ByteRate = format.SampleRate * uint32(format.NumChannels*format.BitsPerSample/8) 51 | } 52 | if format.BlockAlign == 0 { 53 | format.BlockAlign = format.NumChannels * format.BitsPerSample / 8 54 | } 55 | return format 56 | } 57 | 58 | type Data struct { 59 | io.Reader 60 | Size uint32 61 | pos uint32 62 | } 63 | 64 | type SampleFormat uint16 65 | 66 | const ( 67 | AudioFormatLinearPCM SampleFormat = 0x0001 68 | AudioFormatIEEEFloat SampleFormat = 0x0003 69 | ) 70 | -------------------------------------------------------------------------------- /bind/sample/value_test.go: -------------------------------------------------------------------------------- 1 | // Package sample models an audio sample 2 | package sample 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | func TestValueFromByteU8(t *testing.T) { 9 | //TODO: Test 10 | } 11 | 12 | func TestValueFromByteS8(t *testing.T) { 13 | //TODO: Test 14 | } 15 | 16 | func TestValueFromBytesU16LSB(t *testing.T) { 17 | //TODO: Test 18 | } 19 | 20 | func TestValueFromBytesU16MSB(t *testing.T) { 21 | //TODO: Test 22 | } 23 | 24 | func TestValueFromBytesS16LSB(t *testing.T) { 25 | //TODO: Test 26 | } 27 | 28 | func TestValueFromBytesS16MSB(t *testing.T) { 29 | //TODO: Test 30 | } 31 | 32 | func TestValueFromBytesS32LSB(t *testing.T) { 33 | //TODO: Test 34 | } 35 | 36 | func TestValueFromBytesS32MSB(t *testing.T) { 37 | //TODO: Test 38 | } 39 | 40 | func TestValueFromBytesF32LSB(t *testing.T) { 41 | //TODO: Test 42 | } 43 | 44 | func TestValueFromBytesF32MSB(t *testing.T) { 45 | //TODO: Test 46 | } 47 | 48 | func TestValueFromBytesF64LSB(t *testing.T) { 49 | //TODO: Test 50 | } 51 | 52 | func TestValueFromBytesF64MSB(t *testing.T) { 53 | //TODO: Test 54 | } 55 | 56 | func TestValueToByteU8(t *testing.T) { 57 | // TODO 58 | } 59 | 60 | func TestValueToByteS8(t *testing.T) { 61 | // TODO 62 | } 63 | 64 | func TestValueToBytesU16LSB(t *testing.T) { 65 | // TODO 66 | } 67 | 68 | func TestValueToBytesS16LSB(t *testing.T) { 69 | // TODO 70 | } 71 | 72 | func TestValueToBytesS32LSB(t *testing.T) { 73 | // TODO 74 | } 75 | 76 | func TestValueToBytesF32LSB(t *testing.T) { 77 | // TODO 78 | } 79 | 80 | func TestValueToBytesF64LSB(t *testing.T) { 81 | // TODO 82 | } 83 | 84 | func TestValueToUint8(t *testing.T) { 85 | // TODO 86 | } 87 | 88 | func TestValueToInt8(t *testing.T) { 89 | // TODO 90 | } 91 | 92 | func TestValueToUint16(t *testing.T) { 93 | // TODO 94 | } 95 | 96 | func TestValueToInt16(t *testing.T) { 97 | // TODO 98 | } 99 | 100 | func TestValueToInt32(t *testing.T) { 101 | // TODO 102 | } 103 | -------------------------------------------------------------------------------- /lib/fire/fire.go: -------------------------------------------------------------------------------- 1 | // Package fire model an audio source playing at a specific time 2 | package fire 3 | 4 | import ( 5 | "github.com/go-mix/mix/bind/spec" 6 | 7 | "github.com/go-mix/mix/lib/source" 8 | ) 9 | 10 | // New Fire to represent a single audio source playing at a specific time in the future. 11 | func New(source string, beginTz spec.Tz, endTz spec.Tz, volume float64, pan float64) *Fire { 12 | // debug.Printf("NewFire(%v, %v, %v, %v, %v)\n", source, beginTz, endTz, volume, pan) 13 | s := &Fire{ 14 | /* setup */ 15 | Source: source, 16 | Volume: volume, 17 | Pan: pan, 18 | BeginTz: beginTz, 19 | EndTz: endTz, 20 | /* playback */ 21 | state: fireStateReady, 22 | } 23 | return s 24 | } 25 | 26 | // Fire represents a single audio source playing at a specific time in the future. 27 | type Fire struct { 28 | /* setup */ 29 | BeginTz spec.Tz 30 | EndTz spec.Tz 31 | Source string 32 | Volume float64 // 0 to 1 33 | Pan float64 // -1 to +1 34 | /* playback */ 35 | nowTz spec.Tz 36 | state fireStateEnum 37 | } 38 | 39 | // At the series of Tz it's playing for, return the series of Tz corresponding to source audio. 40 | func (f *Fire) At(at spec.Tz) (t spec.Tz) { 41 | // debug.Printf("*Fire[%s].At(%v vs %v)\n", f.Source, at, f.BeginTz) 42 | switch f.state { 43 | case fireStateReady: 44 | if at >= f.BeginTz { 45 | f.state = fireStatePlay 46 | f.nowTz++ 47 | } 48 | case fireStatePlay: 49 | t = f.nowTz 50 | f.nowTz++ 51 | if f.EndTz != 0 { 52 | if at >= f.EndTz { 53 | f.state = fireStateDone 54 | } 55 | } else { 56 | f.EndTz = f.BeginTz + f.sourceLength() 57 | } 58 | case fireStateDone: 59 | // garbage collection 60 | } 61 | return 62 | } 63 | 64 | // IsAlive the Fire? 65 | func (f *Fire) IsAlive() bool { 66 | return f.state < fireStateDone 67 | } 68 | 69 | // IsPlaying the Fire? 70 | func (f *Fire) IsPlaying() bool { 71 | return f.state == fireStatePlay 72 | } 73 | 74 | // Teardown the Fire and release its memory 75 | func (f *Fire) Teardown() { 76 | // TODO: confirm that all memory of this object is released when its pointer is deleted from the *Mixer.fires slice, else make sure it does get released somehow 77 | } 78 | 79 | // 80 | // Private 81 | // 82 | 83 | type fireStateEnum uint 84 | 85 | const ( 86 | fireStateReady fireStateEnum = 1 87 | fireStatePlay fireStateEnum = 2 88 | // it is assumed that all alive states are < SOURCE_FINISHED 89 | fireStateDone fireStateEnum = 6 90 | ) 91 | 92 | func (f *Fire) sourceLength() spec.Tz { 93 | return source.GetLength(f.Source) 94 | } 95 | -------------------------------------------------------------------------------- /demo/demo.go: -------------------------------------------------------------------------------- 1 | /** Author: Charney Kaye */ 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "math/rand" 9 | "os" 10 | "time" 11 | 12 | "gopkg.in/pkg/profile.v1" 13 | 14 | "github.com/go-mix/mix" 15 | "github.com/go-mix/mix/bind" 16 | "github.com/go-mix/mix/bind/spec" 17 | ) 18 | 19 | var ( 20 | loader, out string 21 | profileMode string 22 | sampleHz = float64(48000) 23 | specs = spec.AudioSpec{ 24 | Freq: sampleHz, 25 | Format: spec.AudioF32, 26 | Channels: 2, 27 | } 28 | bpm = 120 29 | step = time.Minute / time.Duration(bpm*4) 30 | loops = 8 31 | prefix = "sound/808/" 32 | kick1 = "kick1.wav" 33 | kick2 = "kick2.wav" 34 | marac = "maracas.wav" 35 | snare = "snare.wav" 36 | hitom = "hightom.wav" 37 | lotom = "tom1.wav" 38 | clhat = "cl_hihat.wav" 39 | pattern = []string{ 40 | kick2, 41 | marac, 42 | clhat, 43 | marac, 44 | snare, 45 | marac, 46 | clhat, 47 | kick2, 48 | marac, 49 | marac, 50 | hitom, 51 | marac, 52 | snare, 53 | kick1, 54 | clhat, 55 | marac, 56 | } 57 | ) 58 | 59 | func main() { 60 | // command-line arguments 61 | flag.StringVar(&out, "out", "null", "playback binding [null] _OR_ [wav] for direct stdout (e.g. >file or |aplay)") 62 | flag.StringVar(&profileMode, "profile", "", "enable profiling [cpu, mem, block]") 63 | flag.StringVar(&loader, "loader", "wav", "input loading interface [wav, sox]") 64 | flag.Parse() 65 | 66 | // CPU/Memory/Block profiling 67 | if len(profileMode) > 0 { 68 | out = "null" // TODO: evaluate whether profiling is actually working 69 | switch profileMode { 70 | case "cpu": 71 | defer profile.Start(profile.CPUProfile).Stop() 72 | case "mem": 73 | defer profile.Start(profile.MemProfile, profile.MemProfileRate(4096)).Stop() 74 | case "block": 75 | defer profile.Start(profile.BlockProfile).Stop() 76 | default: 77 | // do nothing 78 | } 79 | } 80 | 81 | // configure mix 82 | bind.UseOutputString(out) 83 | bind.UseLoaderString(loader) 84 | defer mix.Teardown() 85 | mix.Configure(specs) 86 | mix.SetSoundsPath(prefix) 87 | 88 | // setup the music 89 | t := 1 * time.Second // buffer before music 90 | for n := 0; n < loops; n++ { 91 | for s := 0; s < len(pattern); s++ { 92 | mix.SetFire( 93 | pattern[s], t+time.Duration(s)*step, 0, 1.0, rand.Float64()*2-1) 94 | } 95 | t += time.Duration(len(pattern)) * step 96 | } 97 | t += 5 * time.Second // buffer after music 98 | 99 | // 100 | if bind.IsDirectOutput() { 101 | out := os.Stdout 102 | mix.Debug(true) 103 | mix.OutputStart(t, out) 104 | for p := time.Duration(0); p <= t; p += t / 4 { 105 | mix.OutputContinueTo(p) 106 | } 107 | mix.OutputClose() 108 | } else { 109 | mix.Debug(true) 110 | mix.StartAt(time.Now().Add(1 * time.Second)) 111 | fmt.Printf("Mix: 808 Example - pid:%v playback:%v spec:%v\n", os.Getpid(), out, specs) 112 | for mix.FireCount() > 0 { 113 | time.Sleep(1 * time.Second) 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /bind/api.go: -------------------------------------------------------------------------------- 1 | // Package bind is for modular binding of mix to audio interface 2 | package bind 3 | 4 | import ( 5 | "io" 6 | "time" 7 | 8 | "github.com/go-mix/mix/bind/hardware/null" 9 | "github.com/go-mix/mix/bind/opt" 10 | "github.com/go-mix/mix/bind/sample" 11 | "github.com/go-mix/mix/bind/sox" 12 | "github.com/go-mix/mix/bind/spec" 13 | "github.com/go-mix/mix/bind/wav" 14 | ) 15 | 16 | // Configure begins streaming to the bound out audio interface, via a callback function 17 | func Configure(s spec.AudioSpec) { 18 | sample.ConfigureOutput(s) 19 | switch useOutput { 20 | case opt.OutputWAV: 21 | wav.ConfigureOutput(s) 22 | case opt.OutputNull: 23 | null.ConfigureOutput(s) 24 | } 25 | } 26 | 27 | func IsDirectOutput() bool { 28 | return useOutput == opt.OutputWAV 29 | } 30 | 31 | // SetMixNextOutFunc to stream mix out from mix 32 | func SetOutputCallback(fn sample.OutNextCallbackFunc) { 33 | sample.SetOutputCallback(fn) 34 | } 35 | 36 | // OutputStart requires a known length 37 | func OutputStart(length time.Duration, out io.Writer) { 38 | switch useOutput { 39 | case opt.OutputWAV: 40 | wav.OutputStart(length, out) 41 | case opt.OutputNull: 42 | // do nothing 43 | } 44 | } 45 | 46 | // OutputNext using the configured writer. 47 | func OutputNext(numSamples spec.Tz) { 48 | switch useOutput { 49 | case opt.OutputWAV: 50 | wav.OutputNext(numSamples) 51 | case opt.OutputNull: 52 | // do nothing 53 | } 54 | } 55 | 56 | // LoadWAV into a buffer 57 | func LoadWAV(file string) ([]sample.Sample, *spec.AudioSpec) { 58 | switch useLoader { 59 | case opt.InputWAV: 60 | return wav.Load(file) 61 | case opt.InputSOX: 62 | return sox.Load(file) 63 | default: 64 | return make([]sample.Sample, 0), &spec.AudioSpec{} 65 | } 66 | } 67 | 68 | // Teardown to close all hardware bindings 69 | func Teardown() { 70 | switch useOutput { 71 | case opt.OutputWAV: 72 | wav.TeardownOutput() 73 | case opt.OutputNull: 74 | // do nothing 75 | } 76 | } 77 | 78 | // UseLoader to select the file loading interface 79 | func UseLoader(opt opt.Input) { 80 | useLoader = opt 81 | } 82 | 83 | // UseLoaderString to select the file loading interface by string 84 | func UseLoaderString(loader string) { 85 | switch loader { 86 | case string(opt.InputWAV): 87 | useLoader = opt.InputWAV 88 | case string(opt.InputSOX): 89 | useLoader = opt.InputSOX 90 | default: 91 | panic("No such Loader: " + loader) 92 | } 93 | } 94 | 95 | // UseOutput to select the outback interface 96 | func UseOutput(opt opt.Output) { 97 | useOutput = opt 98 | } 99 | 100 | // UseOutputString to select the outback interface by string 101 | func UseOutputString(output string) { 102 | switch output { 103 | case string(opt.OutputWAV): 104 | useOutput = opt.OutputWAV 105 | case string(opt.OutputNull): 106 | useOutput = opt.OutputNull 107 | default: 108 | panic("No such Output: " + output) 109 | } 110 | } 111 | 112 | // 113 | // Private 114 | // 115 | 116 | var ( 117 | useLoader = opt.InputWAV 118 | useOutput = opt.OutputNull 119 | ) 120 | -------------------------------------------------------------------------------- /mix_test.go: -------------------------------------------------------------------------------- 1 | // Sequence-based Go-native audio mixer for music apps 2 | package mix 3 | 4 | import ( 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/go-mix/mix/bind/spec" 11 | "github.com/go-mix/mix/lib/mix" 12 | ) 13 | 14 | func TestDebug(t *testing.T) { 15 | // TODO: Test API Debug 16 | } 17 | 18 | func TestConfigure(t *testing.T) { 19 | // TODO: Test API Configure 20 | } 21 | 22 | func TestConfigure_FailureFreqNotGreaterThanZero(t *testing.T) { 23 | defer func() { 24 | msg := recover() 25 | assert.IsType(t, "", msg) 26 | assert.Equal(t, "Must specify a mixing frequency greater than zero.", msg) 27 | }() 28 | Configure(spec.AudioSpec{ 29 | Freq: -100, 30 | Format: spec.AudioS16, 31 | Channels: 2, 32 | }) 33 | } 34 | 35 | func TestTeardown(t *testing.T) { 36 | testAPISetup() 37 | Teardown() 38 | } 39 | 40 | func TestSpec(t *testing.T) { 41 | testAPISetup() 42 | assert.Equal(t, &spec.AudioSpec{ 43 | Freq: 44100, 44 | Format: spec.AudioF32, 45 | Channels: 1, 46 | }, Spec()) 47 | Teardown() 48 | } 49 | 50 | func TestSetFire(t *testing.T) { 51 | testAPISetup() 52 | fire := SetFire("lib/source/testdata/Signed16bitLittleEndian44100HzMono.wav", time.Duration(0), 0, 1.0, 0) 53 | assert.NotNil(t, fire) 54 | } 55 | 56 | func TestFireCount(t *testing.T) { 57 | testAPISetup() 58 | assert.Equal(t, 0, FireCount()) 59 | SetFire("lib/source/testdata/Float32bitLittleEndian48000HzEstéreo.wav", time.Duration(0), 0, 1.0, 0) 60 | assert.Equal(t, 1, FireCount()) 61 | SetFire("lib/source/testdata/Signed16bitLittleEndian44100HzMono.wav", time.Duration(0), 0, 1.0, 0) 62 | assert.Equal(t, 2, FireCount()) 63 | // TODO: assert count drains during back to 0 as a result of playback 64 | } 65 | 66 | func TestClearAllFires(t *testing.T) { 67 | testAPISetup() 68 | SetFire("lib/source/testdata/Signed16bitLittleEndian44100HzMono.wav", time.Duration(0), 0, 1.0, 0) 69 | ClearAllFires() 70 | assert.Equal(t, 0, FireCount()) 71 | } 72 | 73 | func TestSetSoundsPath(t *testing.T) { 74 | // TODO: Test API SetSoundsPath 75 | } 76 | 77 | func TestSetGetMixCycleDuration(t *testing.T) { 78 | testAPISetup() 79 | SetMixCycleDuration(2 * time.Second) 80 | assert.Equal(t, spec.Tz(88200), mix.GetCycleDurationTz()) 81 | } 82 | 83 | func TestStart(t *testing.T) { 84 | Start() 85 | } 86 | 87 | func TestStartAt(t *testing.T) { 88 | StartAt(time.Now().Add(1 * time.Second)) 89 | } 90 | 91 | func TestGetStartTime(t *testing.T) { 92 | startExpect := time.Now().Add(1 * time.Second) 93 | StartAt(startExpect) 94 | startActual := GetStartTime() 95 | assert.Equal(t, startExpect, startActual) 96 | } 97 | 98 | func TestGetNowAt(t *testing.T) { 99 | // TODO 100 | } 101 | 102 | func TestOutputStart(t *testing.T) { 103 | // TODO: Test 104 | } 105 | 106 | func TestOutputContinueTo(t *testing.T) { 107 | // TODO: Test 108 | } 109 | 110 | func TestOutputClose(t *testing.T) { 111 | // TODO: Test 112 | } 113 | 114 | func TestAudioCallback(t *testing.T) { 115 | // TODO: Test API AudioCallback 116 | } 117 | 118 | // 119 | // Test Components 120 | // 121 | 122 | func testAPISetup() { 123 | ClearAllFires() 124 | Configure(spec.AudioSpec{ 125 | Freq: 44100, 126 | Format: spec.AudioF32, 127 | Channels: 1, 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /bind/sample/value.go: -------------------------------------------------------------------------------- 1 | // Package sample models an audio sample 2 | package sample 3 | 4 | import ( 5 | "encoding/binary" 6 | "math" 7 | ) 8 | 9 | type Value float64 10 | 11 | func (this Value) Abs() Value { 12 | return Value(math.Abs(float64(this))) 13 | } 14 | 15 | func (this Value) ToByteU8() byte { 16 | return byte(this.ToUint8()) 17 | } 18 | 19 | func (this Value) ToByteS8() byte { 20 | return byte(this.ToInt8()) 21 | } 22 | 23 | func (this Value) ToBytesU16LSB() (out []byte) { 24 | out = make([]byte, 2) 25 | binary.LittleEndian.PutUint16(out, this.ToUint16()) 26 | return 27 | } 28 | 29 | func (this Value) ToBytesS16LSB() (out []byte) { 30 | out = make([]byte, 2) 31 | binary.LittleEndian.PutUint16(out, uint16(this.ToInt16())) 32 | return 33 | } 34 | 35 | func (this Value) ToBytesS32LSB() (out []byte) { 36 | out = make([]byte, 4) 37 | binary.LittleEndian.PutUint32(out, uint32(this.ToInt32())) 38 | return 39 | } 40 | 41 | func (this Value) ToBytesF32LSB() (out []byte) { 42 | out = make([]byte, 4) 43 | binary.LittleEndian.PutUint32(out, math.Float32bits(float32(this))) 44 | return 45 | } 46 | 47 | func (this Value) ToBytesF64LSB() (out []byte) { 48 | out = make([]byte, 4) 49 | binary.LittleEndian.PutUint64(out, math.Float64bits(float64(this))) 50 | return 51 | } 52 | 53 | func (this Value) ToUint8() uint8 { 54 | return uint8(0x80 * (this + 1)) 55 | } 56 | 57 | func (this Value) ToInt8() int8 { 58 | return int8(0x80 * this) 59 | } 60 | 61 | func (this Value) ToUint16() uint16 { 62 | return uint16(0x8000 * (this + 1)) 63 | } 64 | 65 | func (this Value) ToInt16() int16 { 66 | return int16(0x8000 * this) 67 | } 68 | 69 | func (this Value) ToInt32() int32 { 70 | return int32(0x80000000 * this) 71 | } 72 | 73 | func ValueOfByteU8(sample byte) Value { 74 | return Value(int8(sample))/Value(0x7F) - Value(1) 75 | } 76 | 77 | func ValueOfByteS8(sample byte) Value { 78 | return Value(int8(sample)) / Value(0x7F) 79 | } 80 | 81 | func ValueOfBytesU16LSB(sample []byte) Value { 82 | return Value(binary.LittleEndian.Uint16(sample))/Value(0x8000) - Value(1) 83 | } 84 | 85 | //func ValueOfBytesU16MSB(sample []byte) Value { 86 | // return Value(binary.BigEndian.Uint16(sample))/Value(0x8000) - Value(1) 87 | //} 88 | 89 | func ValueOfBytesS16LSB(sample []byte) Value { 90 | return Value(int16(binary.LittleEndian.Uint16(sample))) / Value(0x7FFF) 91 | } 92 | 93 | //func ValueOfBytesS16MSB(sample []byte) Value { 94 | // return Value(int16(binary.BigEndian.Uint16(sample))) / Value(0x7FFF) 95 | //} 96 | 97 | func ValueOfBytesS32LSB(sample []byte) Value { 98 | return Value(int32(binary.LittleEndian.Uint32(sample))) / Value(0x7FFFFFFF) 99 | } 100 | 101 | //func ValueOfBytesS32MSB(sample []byte) Value { 102 | // return Value(int32(binary.BigEndian.Uint32(sample))) / Value(0x7FFFFFFF) 103 | //} 104 | 105 | func ValueOfBytesF32LSB(sample []byte) Value { 106 | return Value(math.Float32frombits(binary.LittleEndian.Uint32(sample))) 107 | } 108 | 109 | //func ValueOfBytesF32MSB(sample []byte) Value { 110 | // return Value(math.Float32frombits(binary.BigEndian.Uint32(sample))) 111 | //} 112 | 113 | func ValueOfBytesF64LSB(sample []byte) Value { 114 | return Value(math.Float64frombits(binary.LittleEndian.Uint64(sample))) 115 | } 116 | 117 | //func ValueOfBytesF64MSB(sample []byte) Value { 118 | // return Value(math.Float64frombits(binary.BigEndian.Uint64(sample))) 119 | //} 120 | -------------------------------------------------------------------------------- /lib/source/source.go: -------------------------------------------------------------------------------- 1 | // Package source models a single audio source 2 | package source 3 | 4 | import ( 5 | "math" 6 | 7 | "github.com/go-mix/mix/bind" 8 | "github.com/go-mix/mix/bind/debug" 9 | "github.com/go-mix/mix/bind/sample" 10 | "github.com/go-mix/mix/bind/spec" 11 | ) 12 | 13 | func Configure(s spec.AudioSpec) { 14 | masterChannelsFloat = float64(s.Channels) 15 | masterSpec = &s 16 | } 17 | 18 | // New Source from a "URL" (which is actually only a file path for now) 19 | func New(URL string) *Source { 20 | // TODO: implement true URL (for now, it's being used as a path) 21 | s := &Source{ 22 | state: STAGED, 23 | URL: URL, 24 | } 25 | s.load() 26 | return s 27 | } 28 | 29 | // Source stores a series of Samples in Channels across Time, for audio playback. 30 | type Source struct { 31 | URL string 32 | // private 33 | sample []sample.Sample 34 | maxTz spec.Tz 35 | audioSpec *spec.AudioSpec 36 | state stateEnum 37 | } 38 | 39 | // SampleAt at a specific Tz, volume (0 to 1), and pan (-1 to +1) 40 | func (s *Source) SampleAt(at spec.Tz, vol float64, pan float64) (out []sample.Value) { 41 | out = make([]sample.Value, masterSpec.Channels) 42 | if at < s.maxTz { 43 | // if s.sample[at] != 0 { 44 | // debug.Printf("*Source[%v].SampleAt(%v): %v\n", s.URL, at, s.sample[at]) 45 | // } 46 | if masterSpec.Channels == s.audioSpec.Channels { // same # channels; easier maths 47 | for c := int(0); c < masterSpec.Channels; c++ { 48 | out[c] = volume(float64(c), vol, pan) * s.sample[at].Values[c] 49 | } 50 | } else { // need to map # source channels to # destination channels 51 | tc := float64(s.audioSpec.Channels) 52 | for c := int(0); c < masterSpec.Channels; c++ { 53 | out[c] = volume(float64(c), vol, pan) * s.sample[at].Values[int(math.Floor(tc*float64(c)/masterChannelsFloat))] 54 | } 55 | } 56 | } 57 | return 58 | } 59 | 60 | // Length of the source audio in Tz 61 | func (s *Source) Length() spec.Tz { 62 | return s.maxTz 63 | } 64 | 65 | // Spec of the source audio 66 | func (s *Source) Spec() *spec.AudioSpec { 67 | return s.audioSpec 68 | } 69 | 70 | // Teardown the source audio and release its memory. 71 | func (s *Source) Teardown() { 72 | s.sample = nil 73 | } 74 | 75 | // 76 | // Private 77 | // 78 | 79 | var ( 80 | masterChannelsFloat float64 81 | masterSpec *spec.AudioSpec 82 | ) 83 | 84 | type stateEnum uint 85 | 86 | const ( 87 | STAGED stateEnum = iota 88 | LOADING 89 | READY 90 | // it is assumed that all alive states are < FINISHED 91 | // FINISHED 92 | // FAILED 93 | ) 94 | 95 | func (s *Source) load() { 96 | s.state = LOADING 97 | s.sample, s.audioSpec = bind.LoadWAV(s.URL) 98 | if s.audioSpec == nil { 99 | // TODO: handle errors loading file 100 | debug.Printf("could not load WAV %s\n", s.URL) 101 | } 102 | s.maxTz = spec.Tz(len(s.sample)) 103 | s.state = READY 104 | } 105 | 106 | // volume (0 to 1), and pan (-1 to +1) 107 | // TODO: ensure implicit panning of source channels! e.g. 2 channels is full left, full right. 108 | func volume(channel float64, volume float64, pan float64) sample.Value { 109 | if pan == 0 { 110 | return sample.Value(volume) 111 | } else if pan < 0 { 112 | return sample.Value(math.Max(0, 1+pan*channel/masterChannelsFloat)) 113 | } else { // pan > 0 114 | return sample.Value(math.Max(0, 1-pan*channel/masterChannelsFloat)) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/mix/mix_test.go: -------------------------------------------------------------------------------- 1 | // Package mix combines sources into an output audio stream 2 | package mix 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/go-mix/mix/bind/spec" 10 | "time" 11 | ) 12 | 13 | // 14 | // Tests 15 | // 16 | 17 | func TestBase(t *testing.T) { 18 | Configure(spec.AudioSpec{ 19 | Freq: 44100, 20 | Format: spec.AudioU16, 21 | Channels: 2, 22 | }) 23 | assert.NotNil(t, Spec()) 24 | } 25 | 26 | func TestRequiresProperAudioSpec(t *testing.T) { 27 | assert.Panics(t, func() { 28 | Configure(spec.AudioSpec{}) 29 | }) 30 | } 31 | 32 | func TestInitialize(t *testing.T) { 33 | // TODO: Test Mixer Initialize 34 | } 35 | 36 | func TestDebug(t *testing.T) { 37 | // TODO: Test Mixer Debug 38 | } 39 | 40 | func TestDebugf(t *testing.T) { 41 | // TODO: Test Mixer debug.Printf 42 | } 43 | 44 | func TestStart(t *testing.T) { 45 | // TODO: Test Mixer Start 46 | } 47 | 48 | func TestStartAt(t *testing.T) { 49 | // TODO: Test Mixer StartAt 50 | } 51 | 52 | func TestGetStartTime(t *testing.T) { 53 | // TODO: Test Mixer GetStartTime 54 | } 55 | 56 | func TestSetFire(t *testing.T) { 57 | // TODO: Test Mixer SetFire 58 | } 59 | 60 | func TestSetSoundsPath(t *testing.T) { 61 | // TODO: Test Mixer SetSoundsPath 62 | } 63 | 64 | func TestNextOut(t *testing.T) { 65 | // TODO: Test Mixer NextOut 66 | } 67 | 68 | func TestTeardown(t *testing.T) { 69 | // TODO: Test Mixer Teardown 70 | } 71 | 72 | func TestNextSample(t *testing.T) { 73 | // TODO: Test Mixer nextSample 74 | } 75 | 76 | func TestOutputStart(t *testing.T) { 77 | // TODO: Test 78 | } 79 | 80 | func TestOutputContinueTo(t *testing.T) { 81 | // TODO: Test 82 | } 83 | 84 | func TestOutputClose(t *testing.T) { 85 | // TODO: Test 86 | } 87 | 88 | func TestSourceAtTz(t *testing.T) { 89 | // TODO: Test Mixer sourceAt 90 | } 91 | 92 | func TestSetSpec(t *testing.T) { 93 | // TODO: Test Mixer setSpec 94 | } 95 | 96 | func TestGetSpec(t *testing.T) { 97 | // TODO: Test Mixer getSpec 98 | } 99 | 100 | func TestPrepareSource(t *testing.T) { 101 | // TODO: Test Mixer prepareSource 102 | } 103 | 104 | func TestMixCleanup(t *testing.T) { 105 | // TODO: Test 106 | } 107 | 108 | func TestMixSetSpec(t *testing.T) { 109 | // TODO: Test success passing in a bind.AudioSpec 110 | // TODO: Test sets the default mixCycleDurTz 111 | } 112 | 113 | func TestSetCycleDuration(t *testing.T) { 114 | masterFreq = 0 // simulates never having set a mix frequency 115 | defer func() { 116 | msg := recover() 117 | assert.IsType(t, "", msg) 118 | assert.Equal(t, "Must specify mixing frequency before setting cycle duration!", msg) 119 | }() 120 | SetCycleDuration(5 * time.Second) 121 | } 122 | 123 | func TestGetSource(t *testing.T) { 124 | // TODO: Test Mixer getSource 125 | } 126 | 127 | func TestMixCycle(t *testing.T) { 128 | // TODO: Test garbage collection of unused sources 129 | // TODO: Test garbage collection of unused fires 130 | } 131 | 132 | // TODO: test mix.GetSpec() 133 | 134 | // TODO: test mix.Debug(true) and mix.Debug(false) 135 | 136 | // TODO: test mix.Play("filename", time, duration, volume) 137 | 138 | // TODO: test sources are queued and loaded properly 139 | 140 | // TODO: test audio sources are mixed properly into buffer 141 | 142 | // TODO: test different timing of ^ 143 | 144 | // TODO: test different audio format / bit rate / samples of ^ 145 | 146 | // TODO: test buffer properly reported to AudioCallback 147 | -------------------------------------------------------------------------------- /lib/source/source_test.go: -------------------------------------------------------------------------------- 1 | // Package source models a single audio source 2 | package source 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/go-mix/mix/bind/debug" 10 | "github.com/go-mix/mix/bind/sample" 11 | "github.com/go-mix/mix/bind/spec" 12 | ) 13 | 14 | // TODO: test multi-channel source audio files 15 | 16 | func TestBase(t *testing.T) { 17 | // TODO: Test Source Base 18 | } 19 | 20 | func TestLoad_IntVsFloat(t *testing.T) { 21 | debug.Configure(true) 22 | testSourceSetup(44100, 1) 23 | sourceFloat := New("testdata/Float32bitLittleEndian48000HzEstéreo.wav") 24 | assert.NotNil(t, sourceFloat) 25 | assert.Equal(t, spec.AudioF32, sourceFloat.Spec().Format) 26 | sourceInt := New("testdata/Signed16bitLittleEndian44100HzMono.wav") 27 | assert.NotNil(t, sourceInt) 28 | assert.Equal(t, spec.AudioS16, sourceInt.Spec().Format) 29 | } 30 | 31 | func TestLoad_FAIL(t *testing.T) { 32 | pathFail := "testdata/ThisShouldFailBecauseItDoesNotExist.wav" 33 | defer func() { 34 | msg := recover() 35 | assert.IsType(t, "", msg) 36 | assert.Equal(t, "File not found: "+pathFail, msg) 37 | }() 38 | debug.Configure(true) 39 | testSourceSetup(44100, 1) 40 | source := New(pathFail) 41 | assert.NotNil(t, source) 42 | } 43 | 44 | func TestLoadSigned16bitLittleEndian44100HzMono(t *testing.T) { 45 | debug.Configure(true) 46 | testSourceSetup(44100, 1) 47 | source := New("testdata/Signed16bitLittleEndian44100HzMono.wav") 48 | assert.NotNil(t, source) 49 | totalSoundMovement := testSourceAssertSound(t, source, 1) 50 | assert.True(t, totalSoundMovement > .001) 51 | } 52 | 53 | func TestLoadFloat32bitLittleEndian48000HzEstéreo(t *testing.T) { 54 | debug.Configure(true) 55 | testSourceSetup(48000, 2) 56 | source := New("testdata/Float32bitLittleEndian48000HzEstéreo.wav") 57 | assert.NotNil(t, source) 58 | totalSoundMovement := testSourceAssertSound(t, source, 2) 59 | assert.True(t, totalSoundMovement > .001) 60 | } 61 | 62 | func TestOutput(t *testing.T) { 63 | // TODO: Test Source plays audio 64 | } 65 | 66 | func TestSampleAt(t *testing.T) { 67 | // TODO: Test Source SampleAt 68 | } 69 | 70 | func TestState(t *testing.T) { 71 | // TODO: Test Source State 72 | } 73 | 74 | func TestStateName(t *testing.T) { 75 | // TODO: Test Source StateName 76 | } 77 | 78 | func TestLength(t *testing.T) { 79 | // TODO: Test Source reports length 80 | } 81 | 82 | func TestTeardown(t *testing.T) { 83 | // TODO: Test Source Teardown 84 | } 85 | 86 | func TestMixer_mixVolume(t *testing.T) { 87 | masterChannelsFloat = 1 88 | assert.Equal(t, sample.Value(0), volume(0, 0, 0)) 89 | assert.Equal(t, sample.Value(1), volume(0, 1, .5)) 90 | masterChannelsFloat = 2 91 | assert.Equal(t, sample.Value(1), volume(0, 1, -.5)) 92 | assert.Equal(t, sample.Value(.75), volume(1, 1, .5)) 93 | assert.Equal(t, sample.Value(.5), volume(0, .5, 0)) 94 | assert.Equal(t, sample.Value(.5), volume(1, .5, 1)) 95 | masterChannelsFloat = 3 96 | assert.Equal(t, sample.Value(1), volume(0, 1, 0)) 97 | assert.Equal(t, sample.Value(0.6666666666666667), volume(1, 1, -1)) 98 | assert.Equal(t, sample.Value(0.6666666666666667), volume(2, .5, -.5)) 99 | assert.Equal(t, sample.Value(0.6666666666666667), volume(1, .5, 1)) 100 | masterChannelsFloat = 4 101 | assert.Equal(t, sample.Value(1), volume(0, 1, -1)) 102 | assert.Equal(t, sample.Value(1), volume(1, 1, 0)) 103 | assert.Equal(t, sample.Value(.75), volume(2, .5, .5)) 104 | assert.Equal(t, sample.Value(.625), volume(3, .5, -.5)) 105 | } 106 | 107 | // 108 | // Private 109 | // 110 | 111 | func testSourceSetup(freq float64, channels int) { 112 | Configure(spec.AudioSpec{ 113 | Freq: freq, 114 | Format: spec.AudioF32, 115 | Channels: channels, 116 | }) 117 | } 118 | 119 | func testSourceAssertSound(t *testing.T, source *Source, channels int) (totalSoundMovement sample.Value) { 120 | for tz := spec.Tz(0); tz < source.Length(); tz++ { 121 | smp := source.SampleAt(tz, 1, 0) 122 | assert.Equal(t, channels, len(smp)) 123 | for c := 0; c < channels; c++ { 124 | totalSoundMovement += smp[c].Abs() 125 | } 126 | } 127 | return 128 | } 129 | -------------------------------------------------------------------------------- /bind/wav/reader.go: -------------------------------------------------------------------------------- 1 | // Package wav is direct WAV filo I/O 2 | package wav 3 | 4 | import ( 5 | "bufio" 6 | "encoding/binary" 7 | "errors" 8 | 9 | riff "github.com/youpy/go-riff" 10 | 11 | "fmt" 12 | "github.com/go-mix/mix/bind/sample" 13 | "github.com/go-mix/mix/bind/spec" 14 | ) 15 | 16 | type Reader struct { 17 | Format *Format 18 | AudioFormat spec.AudioFormat 19 | *Data 20 | // private 21 | riffReader *riff.Reader 22 | riffChunk *riff.RIFFChunk 23 | } 24 | 25 | func NewReader(file riff.RIFFReader) (reader *Reader, err error) { 26 | reader = &Reader{riffReader: riff.NewReader(file)} 27 | format, audioFormat, err := reader.openAndParse() 28 | if err != nil { 29 | return 30 | } 31 | reader.Format = format 32 | reader.AudioFormat = audioFormat 33 | return 34 | } 35 | 36 | func (r *Reader) ReadSamples(params ...uint32) (out []sample.Sample, err error) { 37 | var buffer []byte 38 | var numSamples, n int 39 | 40 | if len(params) > 0 { 41 | numSamples = int(params[0]) 42 | } else { 43 | numSamples = 2048 44 | } 45 | 46 | numChannels := int(r.Format.NumChannels) 47 | bytesPerSample := int(r.Format.BitsPerSample) / 8 48 | blockAlign := int(r.Format.NumChannels) * bytesPerSample 49 | 50 | buffer = make([]byte, numSamples*blockAlign) 51 | n, err = r.readSamplesIntoBuffer(buffer) 52 | 53 | if err != nil { 54 | return 55 | } 56 | 57 | numSamples = n / blockAlign 58 | r.Data.pos += uint32(numSamples * blockAlign) 59 | 60 | for offset := 0; offset < len(buffer)-numChannels-bytesPerSample; offset += blockAlign { 61 | values := make([]sample.Value, numChannels) 62 | for c := 0; c < int(numChannels); c++ { 63 | offsetCh := offset + c*bytesPerSample 64 | bytes := buffer[offsetCh : offsetCh+bytesPerSample] 65 | values[c] = r.sampleFromBytes(r.AudioFormat, bytes) 66 | } 67 | out = append(out, sample.New(values)) 68 | } 69 | 70 | return 71 | } 72 | 73 | // 74 | // Private 75 | // 76 | 77 | func (r *Reader) readSamplesIntoBuffer(p []byte) (n int, err error) { 78 | if r.Data == nil { 79 | data, err := r.readData() 80 | if err != nil { 81 | return n, err 82 | } 83 | r.Data = data 84 | } 85 | 86 | return r.Data.Read(p) 87 | } 88 | 89 | func (r *Reader) sampleFromBytes(audio spec.AudioFormat, bytes []byte) sample.Value { 90 | // TODO: big-endian or little-endian? 91 | switch audio { 92 | case spec.AudioU8: 93 | return sample.ValueOfByteU8(bytes[0]) 94 | case spec.AudioS8: 95 | return sample.ValueOfByteS8(bytes[0]) 96 | case spec.AudioU16: 97 | return sample.ValueOfBytesU16LSB(bytes) 98 | case spec.AudioS16: 99 | return sample.ValueOfBytesS16LSB(bytes) 100 | case spec.AudioS32: 101 | return sample.ValueOfBytesS32LSB(bytes) 102 | case spec.AudioF32: 103 | return sample.ValueOfBytesF32LSB(bytes) 104 | case spec.AudioF64: 105 | return sample.ValueOfBytesF64LSB(bytes) 106 | default: 107 | panic("Unhandled format!") 108 | } 109 | } 110 | 111 | func (r *Reader) openAndParse() (format *Format, audio spec.AudioFormat, err error) { 112 | var riffChunk *riff.RIFFChunk 113 | 114 | format = new(Format) 115 | 116 | if r.riffChunk == nil { 117 | riffChunk, err = r.riffReader.Read() 118 | if err != nil { 119 | return 120 | } 121 | 122 | r.riffChunk = riffChunk 123 | } else { 124 | riffChunk = r.riffChunk 125 | } 126 | 127 | for _, ch := range riffChunk.Chunks { 128 | var data []byte 129 | switch string(ch.ChunkID[:]) { 130 | case "fmt ": 131 | err = binary.Read(ch, binary.LittleEndian, format) 132 | if err != nil { 133 | return 134 | } 135 | switch SampleFormat(format.SampleFormat) { 136 | case AudioFormatLinearPCM: // Linear PCM 137 | switch format.BitsPerSample { 138 | case 8: 139 | audio = spec.AudioS8 140 | case 16: 141 | audio = spec.AudioS16 142 | default: 143 | panic(fmt.Sprintf("Unhandled Linear PCM bitrate: %+v", format.BitsPerSample)) 144 | } 145 | case AudioFormatIEEEFloat: // IEEE Float 146 | switch format.BitsPerSample { 147 | case 32: 148 | audio = spec.AudioF32 149 | case 64: 150 | audio = spec.AudioF64 151 | default: 152 | panic(fmt.Sprintf("Unhandled IEEE Float bitrate: %+v", format.BitsPerSample)) 153 | } 154 | default: 155 | panic("Unhandled format") 156 | } 157 | case "fact": 158 | data = make([]byte, ch.ChunkSize) 159 | err = binary.Read(ch, binary.LittleEndian, data) 160 | if err != nil { 161 | return 162 | } 163 | case "PEAK": 164 | data = make([]byte, ch.ChunkSize) 165 | err = binary.Read(ch, binary.LittleEndian, data) 166 | if err != nil { 167 | return 168 | } 169 | } 170 | } 171 | 172 | if format == nil && err == nil { 173 | err = errors.New("Format chunk is not found") 174 | } 175 | return 176 | } 177 | 178 | func (r *Reader) readData() (data *Data, err error) { 179 | var riffChunk *riff.RIFFChunk 180 | 181 | if r.riffChunk == nil { 182 | riffChunk, err = r.riffReader.Read() 183 | if err != nil { 184 | return 185 | } 186 | 187 | r.riffChunk = riffChunk 188 | } else { 189 | riffChunk = r.riffChunk 190 | } 191 | 192 | for _, ch := range riffChunk.Chunks { 193 | if string(ch.ChunkID[:]) == "data" { 194 | data = &Data{bufio.NewReader(ch), ch.ChunkSize, 0} 195 | return 196 | } 197 | } 198 | 199 | err = errors.New("Data chunk is not found") 200 | return 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mix 2 | 3 | [![Build Status](https://travis-ci.org/go-mix/mix.svg?branch=master)](https://travis-ci.org/go-mix/mix) [![GoDoc](https://godoc.org/github.com/go-mix/mix?status.svg)](https://godoc.org/github.com/go-mix/mix) [![codebeat badge](https://codebeat.co/badges/008a2ecc-76ac-4ef5-9baa-6ee99501cacc)](https://codebeat.co/projects/github-com-go-mix-mix) [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) 4 | 5 | https://github.com/go-mix/mix 6 | 7 | #### Sequence-based Go-native audio mixer for music apps 8 | 9 | See `demo/demo.go`: 10 | 11 | package main 12 | 13 | import ( 14 | "fmt" 15 | "os" 16 | "time" 17 | 18 | "github.com/go-mix/mix" 19 | "github.com/go-mix/mix/bind" 20 | ) 21 | 22 | var ( 23 | sampleHz = float64(48000) 24 | spec = bind.AudioSpec{ 25 | Freq: sampleHz, 26 | Format: bind.AudioF32, 27 | Channels: 2, 28 | } 29 | bpm = 120 30 | step = time.Minute / time.Duration(bpm*4) 31 | loops = 16 32 | prefix = "sound/808/" 33 | kick1 = "kick1.wav" 34 | kick2 = "kick2.wav" 35 | marac = "maracas.wav" 36 | snare = "snare.wav" 37 | hitom = "hightom.wav" 38 | clhat = "cl_hihat.wav" 39 | pattern = []string{ 40 | kick2, 41 | marac, 42 | clhat, 43 | marac, 44 | snare, 45 | marac, 46 | clhat, 47 | kick2, 48 | marac, 49 | marac, 50 | hitom, 51 | marac, 52 | snare, 53 | kick1, 54 | clhat, 55 | marac, 56 | } 57 | ) 58 | 59 | func main() { 60 | defer mix.Teardown() 61 | 62 | mix.Debug(true) 63 | mix.Configure(spec) 64 | mix.SetSoundsPath(prefix) 65 | mix.StartAt(time.Now().Add(1 * time.Second)) 66 | 67 | t := 2 * time.Second // padding before music 68 | for n := 0; n < loops; n++ { 69 | for s := 0; s < len(pattern); s++ { 70 | mix.SetFire(pattern[s], t+time.Duration(s)*step, 0, 1.0, 0) 71 | } 72 | t += time.Duration(len(pattern)) * step 73 | } 74 | 75 | fmt.Printf("Mix, pid:%v, spec:%v\n", os.Getpid(), spec) 76 | for mix.FireCount() > 0 { 77 | time.Sleep(1 * time.Second) 78 | } 79 | } 80 | 81 | Play this Demo from the root of the project, with no actual audio playback: 82 | 83 | make demo 84 | 85 | Or export WAV via stdout `> demo/output.wav`: 86 | 87 | make demo.wav 88 | 89 | ##### Credit 90 | 91 | [Charney Kaye](https://charneykaye.com) 92 | 93 | [XJ Music Inc.](https://xj.io) 94 | 95 | ### What? 96 | 97 | Game audio mixers are designed to play audio spontaneously, but when the timing is known in advance (e.g. sequence-based music apps) there is a demand for much greater accuracy in playback timing. 98 | 99 | Read the API documentation at [godoc.org/github.com/go-mix/mix](https://godoc.org/github.com/go-mix/mix) 100 | 101 | **Mix** seeks to solve the problem of audio mixing for the purpose of the playback of sequences where audio files and their playback timing is known in advance. 102 | 103 | Mix stores and mixes audio in native Go `[]float64` and natively implements Paul Vögler's "Loudness Normalization by Logarithmic Dynamic Range Compression" (details below) 104 | 105 | Best efforts will be made to preserve each API version in a release tag that can be parsed, e.g. **[github.com/go-mix/mix](http://github.com/go-mix/mix)** 106 | 107 | ### Why? 108 | 109 | Even after selecting a hardware interface library such as [PortAudio](http://www.portaudio.com/) or [C++ SDL 2.0](https://www.libsdl.org/), there remains a critical design problem to be solved. 110 | 111 | This design is a **music application mixer**. Most available options are geared towards Game development. 112 | 113 | Game audio mixers offer playback timing accuracy +/- 2 milliseconds. But that's totally unacceptable for music, specifically sequence-based sample playback. 114 | 115 | The design pattern particular to Game design is that the timing of the audio is not know in advance- the timing that really matters is that which is assembled in near-real-time in response to user interaction. 116 | 117 | In the field of Music development, often the timing is known in advance, e.g. a **sequencer**, the composition of music by specifying exactly how, when and which audio files will be played relative to the beginning of playback. 118 | 119 | Ergo, **mix** seeks to solve the problem of audio mixing for the purpose of the playback of sequences where audio files and their playback timing is known in advance. It seeks to do this with the absolute minimal logical overhead on top of the audio interface. 120 | 121 | Mix takes maximum advantage of Go by storing and mixing audio in native Go `[]float64` and natively implementing Paul Vögler's "Loudness Normalization by Logarithmic Dynamic Range Compression" 122 | 123 | ### Time 124 | 125 | To the Mix API, time is specified as a time.Duration-since-epoch, where the epoch is the moment that mix.Start() was called. 126 | 127 | Internally, time is tracked as samples-since-epoch at the master out playback frequency (e.g. 48000 Hz). This is most efficient because source audio is pre-converted to the master out playback frequency, and all audio maths are performed in terms of samples. 128 | 129 | ### The Mixing Algorithm 130 | 131 | Inspired by the theory paper "Mixing two digital audio streams with on the fly Loudness Normalization by Logarithmic Dynamic Range Compression" by Paul Vögler, 2012-04-20. A .PDF has been included [here](docs/LogarithmicDynamicRangeCompression-PaulVogler.pdf), from the paper originally published [here](http://www.voegler.eu/pub/audio/digital-audio-mixing-and-normalization.html). 132 | 133 | ### Usage 134 | 135 | There's a demo implementation of **mix** included in the `demo/` folder in this repository. Run it using the defaults: 136 | 137 | cd demo && go get && go run demo.go 138 | 139 | Or specify options, e.g. using WAV bytes to stdout for playback (piped to system native `aplay`) 140 | 141 | go run demo.go --out wav | aplay 142 | 143 | To show the help screen: 144 | 145 | go run demo.go --help 146 | -------------------------------------------------------------------------------- /lib/mix/mix.go: -------------------------------------------------------------------------------- 1 | // Package mix combines sources into an output audio stream 2 | package mix 3 | 4 | import ( 5 | "io" 6 | "math" 7 | "time" 8 | 9 | "github.com/go-mix/mix/bind/spec" 10 | 11 | "github.com/go-mix/mix/bind" 12 | "github.com/go-mix/mix/bind/debug" 13 | "github.com/go-mix/mix/bind/sample" 14 | "github.com/go-mix/mix/lib/fire" 15 | "github.com/go-mix/mix/lib/source" 16 | ) 17 | 18 | // NextSample returns the next sample mixed in all channels 19 | func NextSample() []sample.Value { 20 | smp := make([]sample.Value, masterSpec.Channels) 21 | var fireSample []sample.Value 22 | for _, fire := range mixLiveFires { 23 | if fireTz := fire.At(nowTz); fireTz > 0 { 24 | fireSample = mixSourceAt(fire.Source, fire.Volume, fire.Pan, fireTz) 25 | for c := 0; c < masterSpec.Channels; c++ { 26 | smp[c] += fireSample[c] 27 | } 28 | } 29 | } 30 | // debug.Printf("*Mixer.nextSample %+v\n", sample) 31 | nowTz++ 32 | out := make([]sample.Value, masterSpec.Channels) 33 | for c := 0; c < masterSpec.Channels; c++ { 34 | out[c] = mixLogarithmicRangeCompression(smp[c]) 35 | } 36 | if nowTz > nextCycleTz { 37 | mixCycle() 38 | } 39 | return out 40 | } 41 | 42 | // Configure the mixer frequency, format, channels & sample rate. 43 | func Configure(s spec.AudioSpec) { 44 | masterSpec = &s 45 | masterFreq = float64(s.Freq) 46 | masterTzDur = time.Second / time.Duration(masterFreq) 47 | masterCycleDurTz = spec.Tz(masterFreq) 48 | source.Configure(s) 49 | } 50 | 51 | // Spec spec returns the current audio specification. 52 | func Spec() *spec.AudioSpec { 53 | return masterSpec 54 | } 55 | 56 | // Teardown everything and release all memory. 57 | func Teardown() { 58 | ClearAllFires() 59 | outputToDur = time.Duration(0) 60 | nextCycleTz = 0 61 | nowTz = 0 62 | } 63 | 64 | // SetFire to represent a single audio source playing at a specific time in the future (in time.Duration from play start), with sustain time.Duration, volume from 0 to 1, and pan from -1 to +1 65 | func SetFire(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64) *fire.Fire { 66 | mixPrepareSource(mixSourcePrefix + source) 67 | beginTz := spec.Tz(begin.Nanoseconds() / masterTzDur.Nanoseconds()) 68 | var endTz spec.Tz 69 | if sustain != 0 { 70 | endTz = beginTz + spec.Tz(sustain.Nanoseconds()/masterTzDur.Nanoseconds()) 71 | } 72 | f := fire.New(mixSourcePrefix+source, beginTz, endTz, volume, pan) 73 | mixReadyFires = append(mixReadyFires, f) 74 | return f 75 | } 76 | 77 | // FireCount returns the current total ready fires + live fires. 78 | func FireCount() int { 79 | return len(mixLiveFires) + len(mixReadyFires) 80 | } 81 | 82 | // StartAt to specify what time to begin mixing. 83 | func StartAt(t time.Time) { 84 | startAtTime = t 85 | } 86 | 87 | // GetStartTime returns the time mixing began. 88 | func GetStartTime() time.Time { 89 | return startAtTime 90 | } 91 | 92 | // GetNowAt returns current mix position 93 | func GetNowAt() time.Duration { 94 | return time.Duration(nowTz) * masterTzDur 95 | } 96 | 97 | // ClearAllFires to remove all ready & live fires. 98 | func ClearAllFires() { 99 | mixReadyFires = make([]*fire.Fire, 0) 100 | mixLiveFires = make([]*fire.Fire, 0) 101 | } 102 | 103 | // SetSoundsPath to set the sound path prefix. 104 | func SetSoundsPath(prefix string) { 105 | mixSourcePrefix = prefix 106 | } 107 | 108 | // GetCycleDurationTz sets the duration of a mix cycle. 109 | func SetCycleDuration(d time.Duration) { 110 | if masterFreq == 0 { 111 | panic("Must specify mixing frequency before setting cycle duration!") 112 | } 113 | masterCycleDurTz = spec.Tz((d / time.Second) * time.Duration(masterFreq)) 114 | } 115 | 116 | // GetCycleDurationTz returns the duration of a mix cycle. 117 | func GetCycleDurationTz() spec.Tz { 118 | return masterCycleDurTz 119 | } 120 | 121 | // OutputStart requires a known length 122 | func OutputStart(length time.Duration, out io.Writer) { 123 | bind.OutputStart(length, out) 124 | } 125 | 126 | // OutputContinueTo to mix and output as []byte via stdout, up to a specified duration-since-start 127 | func OutputContinueTo(t time.Duration) { 128 | deltaDur := t - outputToDur 129 | deltaTz := spec.Tz(masterFreq * float64((deltaDur)/time.Second)) 130 | debug.Printf("mix.OutputContinueTo(%+v) deltaDur:%+v nowTz:%+v deltaTz:%+v begin...", t, deltaDur, nowTz, deltaTz) 131 | bind.OutputNext(deltaTz) 132 | outputToDur = t 133 | debug.Printf("mix.OutputContinueTo(%+v) ...done! nowTz:%+v outputToDur:%+v", t, nowTz, outputToDur) 134 | } 135 | 136 | // OutputBegin to output WAV closer as []byte via stdout 137 | func OutputClose() { 138 | // nothing to do 139 | } 140 | 141 | // 142 | // Private 143 | // 144 | 145 | var ( 146 | outputToDur time.Duration 147 | startAtTime time.Time 148 | nowTz spec.Tz 149 | nextCycleTz spec.Tz 150 | masterCycleDurTz spec.Tz 151 | masterTzDur time.Duration 152 | // TODO: implement mixFreq float64 153 | mixSourcePrefix string 154 | mixReadyFires []*fire.Fire 155 | mixLiveFires []*fire.Fire 156 | masterSpec *spec.AudioSpec 157 | masterFreq float64 158 | ) 159 | 160 | func init() { 161 | startAtTime = time.Now().Add(0xFFFF * time.Hour) // this gets reset by Start() or StartAt() 162 | } 163 | 164 | func mixSourceAt(src string, volume float64, pan float64, at spec.Tz) []sample.Value { 165 | s := mixGetSource(src) 166 | if s == nil { 167 | return make([]sample.Value, masterSpec.Channels) 168 | } 169 | // if at != 0 { 170 | // debug.Printf("About to source.SampleAt %v in %v\n", at, s.URL) 171 | // } 172 | return s.SampleAt(at, volume, pan) 173 | } 174 | 175 | func mixPrepareSource(src string) { 176 | source.Prepare(src) 177 | } 178 | 179 | func mixGetSource(src string) *source.Source { 180 | return source.Get(src) 181 | } 182 | 183 | // TODO: 184 | // Make a new empty map[string]*Source, e.g. keepSource 185 | // While iterating over the ready & active fires (see issues #11 and #18; implemented as of pull #29) copy any used *Source to the new keepSource 186 | // Replace the mixSource with keepSource 187 | func mixCycle() { 188 | var f *fire.Fire 189 | // for garbage collection of unused sources: 190 | keepSource := make(map[string]bool) 191 | // if a fire is near-to-playback, move it to the live fire queue 192 | keepReadyFires := make([]*fire.Fire, 0) 193 | for _, f = range mixReadyFires { 194 | keepSource[f.Source] = true 195 | if f.BeginTz < nowTz+masterCycleDurTz*2 { // for now, double a mix cycle is consider near-playback 196 | mixLiveFires = append(mixLiveFires, f) 197 | } else { 198 | keepReadyFires = append(keepReadyFires, f) 199 | } 200 | } 201 | mixReadyFires = keepReadyFires 202 | // keep only active fires 203 | keepLiveFires := make([]*fire.Fire, 0) 204 | for _, f = range mixLiveFires { 205 | if f.IsAlive() { 206 | keepSource[f.Source] = true 207 | keepLiveFires = append(keepLiveFires, f) 208 | } else { 209 | f.Teardown() 210 | } 211 | } 212 | mixLiveFires = keepLiveFires 213 | source.Prune(keepSource) 214 | nextCycleTz = nowTz + masterCycleDurTz 215 | if debug.Active() && source.Count() > 0 { 216 | debug.Printf("mix [%dz] fire-ready:%d fire-active:%d sources:%d\n", nowTz, len(mixReadyFires), len(mixLiveFires), source.Count()) 217 | } 218 | } 219 | 220 | func mixLogarithmicRangeCompression(i sample.Value) sample.Value { 221 | if i < -1 { 222 | return sample.Value(-math.Log(-float64(i)-0.85)/14 - 0.75) 223 | } else if i > 1 { 224 | return sample.Value(math.Log(float64(i)-0.85)/14 + 0.75) 225 | } else { 226 | return sample.Value(i / 1.61803398875) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /mix.go: -------------------------------------------------------------------------------- 1 | // Sequence-based Go-native audio mixer for music apps 2 | // 3 | // Go-native audio mixer for Music apps 4 | // 5 | // See `demo/demo.go`: 6 | // 7 | // package main 8 | // 9 | // import ( 10 | // "fmt" 11 | // "os" 12 | // "time" 13 | // 14 | // "github.com/go-mix/mix" 15 | // "github.com/go-mix/mix/bind" 16 | // ) 17 | // 18 | // var ( 19 | // sampleHz = float64(48000) 20 | // spec = bind.AudioSpec{ 21 | // Freq: sampleHz, 22 | // Format: bind.AudioF32, 23 | // Channels: 2, 24 | // } 25 | // bpm = 120 26 | // step = time.Minute / time.Duration(bpm*4) 27 | // loops = 16 28 | // prefix = "sound/808/" 29 | // kick1 = "kick1.wav" 30 | // kick2 = "kick2.wav" 31 | // marac = "maracas.wav" 32 | // snare = "snare.wav" 33 | // hitom = "hightom.wav" 34 | // clhat = "cl_hihat.wav" 35 | // pattern = []string{ 36 | // kick2, 37 | // marac, 38 | // clhat, 39 | // marac, 40 | // snare, 41 | // marac, 42 | // clhat, 43 | // kick2, 44 | // marac, 45 | // marac, 46 | // hitom, 47 | // marac, 48 | // snare, 49 | // kick1, 50 | // clhat, 51 | // marac, 52 | // } 53 | // ) 54 | // 55 | // func main() { 56 | // defer mix.Teardown() 57 | // 58 | // mix.Debug(true) 59 | // mix.Configure(spec) 60 | // mix.SetSoundsPath(prefix) 61 | // mix.StartAt(time.Now().Add(1 * time.Second)) 62 | // 63 | // t := 2 * time.Second // padding before music 64 | // for n := 0; n < loops; n++ { 65 | // for s := 0; s < len(pattern); s++ { 66 | // mix.SetFire(pattern[s], t+time.Duration(s)*step, 0, 1.0, 0) 67 | // } 68 | // t += time.Duration(len(pattern)) * step 69 | // } 70 | // 71 | // fmt.Printf("Mix, pid:%v, spec:%v\n", os.Getpid(), spec) 72 | // for mix.FireCount() > 0 { 73 | // time.Sleep(1 * time.Second) 74 | // } 75 | // } 76 | // 77 | // Play this Demo from the root of the project, with no actual audio playback 78 | // 79 | // make demo 80 | // 81 | // Or export WAV via stdout `> demo/output.wav`: 82 | // 83 | // make demo.wav 84 | // 85 | // 86 | // What 87 | // 88 | // Game audio mixers are designed to play audio spontaneously, but when the timing is known in advance (e.g. sequence-based music apps) there is a demand for much greater accuracy in playback timing. 89 | // 90 | // Read the API documentation at https://godoc.org/github.com/go-mix/mix 91 | // 92 | // Mix seeks to solve the problem of audio mixing for the purpose of the playback of sequences where audio files and their playback timing is known in advance. 93 | // 94 | // Mix stores and mixes audio in native Go `[]float64` and natively implements Paul Vögler's "Loudness Normalization by Logarithmic Dynamic Range Compression" (details below) 95 | // 96 | // 97 | // Credit 98 | // 99 | // Charney Kaye 100 | // https://charneykaye.com 101 | // 102 | // XJ Music Inc. 103 | // https://xj.io 104 | // 105 | // 106 | // Why 107 | // 108 | // Even after selecting a hardware interface library such as http://www.portaudio.com/ or https://www.libsdl.org/, there remains a critical design problem to be solved. 109 | // 110 | // This design is a music application mixer. Most available options are geared towards Game development. 111 | // 112 | // Game audio mixers offer playback timing accuracy +/- 2 milliseconds. But that's totally unacceptable for music, specifically sequence-based sample playback. 113 | // 114 | // The design pattern particular to Game design is that the timing of the audio is not know in advance- the timing that really matters is that which is assembled in near-real-time in response to user interaction. 115 | // 116 | // In the field of Music development, often the timing is known in advance, e.g. a sequencer, the composition of music by specifying exactly how, when and which audio files will be played relative to the beginning of playback. 117 | // 118 | // Ergo, mix seeks to solve the problem of audio mixing for the purpose of the playback of sequences where audio files and their playback timing is known in advance. It seeks to do this with the absolute minimal logical overhead on top of the audio interface. 119 | // 120 | // Mix takes maximum advantage of Go by storing and mixing audio in native Go `[]float64` and natively implementing Paul Vögler's "Loudness Normalization by Logarithmic Dynamic Range Compression" (see The Mixing Algorithm below) 121 | // 122 | // 123 | // Time 124 | // 125 | // To the Mix API, time is specified as a time.Duration-since-epoch, where the epoch is the moment that mix.Start() was called. 126 | // 127 | // Internally, time is tracked as samples-since-epoch at the master out playback frequency (e.g. 48000 Hz). This is most efficient because source audio is pre-converted to the master out playback frequency, and all audio maths are performed in terms of samples. 128 | // 129 | // The Mixing Algorithm 130 | // 131 | // Inspired by the theory paper "Mixing two digital audio streams with on the fly Loudness Normalization by Logarithmic Dynamic Range Compression" by Paul Vögler, 2012-04-20. This paper is published at http://www.voegler.eu/pub/audio/digital-audio-mixing-and-normalization.html. 132 | // 133 | // 134 | // Usage 135 | // 136 | // There's a demo implementation of **mix** included in the `demo/` folder in this repository. Run it using the defaults: 137 | // 138 | // cd demo && go get && go run demo.go 139 | // 140 | // Or specify options, e.g. using WAV bytes to stdout for playback (piped to system native `aplay`) 141 | // 142 | // go run demo.go --out wav | aplay 143 | // 144 | // To show the help screen: 145 | // 146 | // go run demo.go --help 147 | // 148 | // Best efforts will be made to preserve each API version in a release tag that can be parsed, e.g. http://github.com/go-mix/mix 149 | // 150 | // Mix in good health! 151 | // 152 | package mix 153 | 154 | import ( 155 | "io" 156 | "time" 157 | 158 | "github.com/go-mix/mix/bind" 159 | "github.com/go-mix/mix/bind/debug" 160 | "github.com/go-mix/mix/bind/spec" 161 | 162 | "github.com/go-mix/mix/lib/fire" 163 | "github.com/go-mix/mix/lib/mix" 164 | ) 165 | 166 | // VERSION # of this mix source code 167 | // const VERSION = "0.0.3" 168 | 169 | // Debug ON/OFF (ripples down to all sub-modules) 170 | func Debug(isOn bool) { 171 | debug.Configure(isOn) 172 | } 173 | 174 | // Configure the mixer frequency, format, channels & sample rate. 175 | func Configure(s spec.AudioSpec) { 176 | s.Validate() 177 | bind.SetOutputCallback(mix.NextSample) 178 | bind.Configure(s) 179 | mix.Configure(s) 180 | } 181 | 182 | // Teardown everything and release all memory. 183 | func Teardown() { 184 | mix.Teardown() 185 | bind.Teardown() 186 | } 187 | 188 | // Spec for the mixer, which may include callback functions, e.g. portaudio 189 | func Spec() *spec.AudioSpec { 190 | return mix.Spec() 191 | } 192 | 193 | // SetFire to represent a single audio source playing at a specific time in the future (in time.Duration from play start), with sustain time.Duration, volume from 0 to 1, and pan from -1 to +1 194 | func SetFire(source string, begin time.Duration, sustain time.Duration, volume float64, pan float64) *fire.Fire { 195 | return mix.SetFire(source, begin, sustain, volume, pan) 196 | } 197 | 198 | // FireCount to check the number of fires currently scheduled for playback 199 | func FireCount() int { 200 | return mix.FireCount() 201 | } 202 | 203 | // ClearAllFires to clear all fires currently ready, or live 204 | func ClearAllFires() { 205 | mix.ClearAllFires() 206 | } 207 | 208 | // SetSoundsPath prefix 209 | func SetSoundsPath(prefix string) { 210 | mix.SetSoundsPath(prefix) 211 | } 212 | 213 | // Set the duration between "mix cycles", wherein garbage collection is performed. 214 | func SetMixCycleDuration(d time.Duration) { 215 | mix.SetCycleDuration(d) 216 | } 217 | 218 | // Start the mixer now 219 | func Start() { 220 | mix.StartAt(time.Now()) 221 | } 222 | 223 | // StartAt a specific time in the future 224 | func StartAt(t time.Time) { 225 | mix.StartAt(t) 226 | } 227 | 228 | // GetStartTime the mixer was started at 229 | func GetStartTime() time.Time { 230 | return mix.GetStartTime() 231 | } 232 | 233 | // GetNowAt returns current mix position 234 | func GetNowAt() time.Duration { 235 | return mix.GetNowAt() 236 | } 237 | 238 | // OutputStart requires a known length 239 | func OutputStart(length time.Duration, out io.Writer) { 240 | mix.OutputStart(length, out) 241 | } 242 | 243 | // OutputContinueTo output as []byte via stdout, up to a specified duration-since-start 244 | func OutputContinueTo(t time.Duration) { 245 | mix.OutputContinueTo(t) 246 | } 247 | 248 | // OutputBegin to output WAV closer as []byte via stdout 249 | func OutputClose() { 250 | mix.OutputClose() 251 | } 252 | --------------------------------------------------------------------------------