├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── audio ├── audio.go ├── audio_windows.go ├── encode_darwin.go ├── encode_darwin_test.go ├── encode_linux.go ├── encode_windows.go ├── encoding.go ├── filter.go ├── filter │ ├── data.go │ ├── encoding.go │ ├── fourier.go │ ├── guarantees.go │ ├── loop.go │ ├── pan.go │ ├── pitchshift.go │ ├── resample.go │ ├── sampleRate.go │ ├── supports │ │ ├── supports.go │ │ └── unsupported.go │ └── volume.go ├── format.go ├── manip │ ├── convert.go │ └── math.go ├── multi.go └── skip_devices.go ├── flac ├── flac.go ├── flac_test.go ├── test.flac └── test2.flac ├── font ├── audio.go ├── ceol │ ├── ceol.go │ ├── ceol_test.go │ └── test.ceol ├── dls │ ├── SanbikiSCC.dls │ ├── dls.go │ └── dls_test.go ├── font.go ├── font_test.go ├── riff │ ├── info.go │ └── riff.go └── sf2 │ └── sf2_test.go ├── load.go ├── load_test.go ├── mp3 ├── mp3.go └── mp3_test.go ├── sequence ├── chordPattern.go ├── generator.go ├── holdPattern.go ├── length.go ├── loop.go ├── pitchPattern.go ├── sequence.go ├── tick.go ├── volumePattern.go ├── waveFunction.go ├── waveGenerator.go └── waveGenerator_test.go ├── synth ├── option.go ├── pitch.go ├── source.go ├── synth_test.go └── waves.go └── wav ├── test.wav ├── wav.go └── wav_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # Test files that we can't commit because we don't have the right to 17 | *nolicense* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | 6 | os: 7 | - osx 8 | 9 | script: 10 | - go get -u github.com/stretchr/testify/assert 11 | - go test ./audio -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Patrick Stephen 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # klangsynthese 2 | Waveform and Audio Synthesis library in Go 3 | 4 | [![GoDoc](https://godoc.org/github.com/200sc/klangsynthese?status.svg)](https://godoc.org/github.com/200sc/klangsynthese) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/200sc/klangsynthese)](https://goreportcard.com/report/github.com/200sc/klangsynthese) 6 | 7 | Klangsynthese right now supports a number of features that will work regardless of OS, 8 | with further support planned for OSX as soon as we get our hands on one to test with. 9 | 10 | ## Usage 11 | 12 | See test files. 13 | 14 | ## OS specific features 15 | 16 | | OS | Load | Modify | Save | Play | 17 | | -------- | ---- | ------ | ------ | ---- | 18 | | Windows | X | X | | X | 19 | | Linux | X | X | | X | 20 | | Darwin | X | X | | | 21 | 22 | ## Quick recipe for testing on Linux 23 | 24 | This recipe should run the wav test on Linux: 25 | 26 | go get github.com/200sc/klangsynthese 27 | go get github.com/stretchr/testify/require 28 | go test github.com/200sc/klangsynthese/wav 29 | 30 | ## Other features 31 | 32 | - [x] Wav support 33 | - [x] Mp3 support 34 | - [x] Flac support 35 | - [ ] Ogg support 36 | - [x] Creating waveforms (Sin, Square, Saw, ...) 37 | - [x] Filtering audio samples 38 | - [x] Creating Sequences of audio samples to play music 39 | - [x] Importable Bosca Ceoil files (.ceol) 40 | - [x] Importable DLS files 41 | - [ ] Importable arbitrary instruments 42 | - [ ] Other Importable soundfonts (.sf2...) 43 | -------------------------------------------------------------------------------- /audio/audio.go: -------------------------------------------------------------------------------- 1 | // Package audio provides audio playing and encoding support 2 | package audio 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/200sc/klangsynthese/audio/filter/supports" 8 | ) 9 | 10 | // Audio represents playable, filterable audio data. 11 | type Audio interface { 12 | // Play returns a channel that will signal when it finishes playing. 13 | // Looping audio will never send on this channel! 14 | // The value sent will always be true. 15 | Play() <-chan error 16 | // Filter will return an audio with some desired filters applied 17 | Filter(...Filter) (Audio, error) 18 | MustFilter(...Filter) Audio 19 | // Stop will stop an ongoing audio 20 | Stop() error 21 | 22 | // Implementing struct-- encoding 23 | Copy() (Audio, error) 24 | MustCopy() Audio 25 | PlayLength() time.Duration 26 | 27 | // SetVolume sets the volume of an audio at an OS level, 28 | // post filters. It multiplies with any volume filters. 29 | // It takes a value from 0 to -10000, and can only reduce 30 | // volume from the raw input. 31 | SetVolume(int32) error 32 | } 33 | 34 | // FullAudio supports all the built in filters 35 | type FullAudio interface { 36 | Audio 37 | supports.Encoding 38 | supports.Loop 39 | } 40 | 41 | // Stream represents an audio stream. unlike Audio, the length of the 42 | // stream is unknown. Copy is also not supported. 43 | type Stream interface { 44 | // Play returns a channel that will signal when it finishes playing. 45 | // Looping audio will never send on this channel! 46 | // The value sent will always be true. 47 | Play() <-chan error 48 | // Filter will return an audio with some desired filters applied 49 | Filter(...Filter) (Audio, error) 50 | MustFilter(...Filter) Audio 51 | // Stop will stop an ongoing audio 52 | Stop() error 53 | } 54 | -------------------------------------------------------------------------------- /audio/audio_windows.go: -------------------------------------------------------------------------------- 1 | //+build windows 2 | 3 | package audio 4 | 5 | import ( 6 | "github.com/oov/directsound-go/dsound" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type dsAudio struct { 11 | *Encoding 12 | *dsound.IDirectSoundBuffer 13 | flags dsound.BufferPlayFlag 14 | } 15 | 16 | func (ds *dsAudio) Play() <-chan error { 17 | ch := make(chan error) 18 | if ds.Loop { 19 | ds.flags = dsound.DSBPLAY_LOOPING 20 | } 21 | go func(dsbuff *dsound.IDirectSoundBuffer, flags dsound.BufferPlayFlag, ch chan error) { 22 | err := dsbuff.SetCurrentPosition(0) 23 | if err != nil { 24 | select { 25 | case ch <- err: 26 | default: 27 | } 28 | } else { 29 | err = dsbuff.Play(0, flags) 30 | if err != nil { 31 | select { 32 | case ch <- err: 33 | default: 34 | } 35 | } else { 36 | select { 37 | case ch <- nil: 38 | default: 39 | } 40 | } 41 | } 42 | }(ds.IDirectSoundBuffer, ds.flags, ch) 43 | return ch 44 | } 45 | 46 | func (ds *dsAudio) Stop() error { 47 | err := ds.IDirectSoundBuffer.Stop() 48 | if err != nil { 49 | return err 50 | } 51 | return ds.IDirectSoundBuffer.SetCurrentPosition(0) 52 | } 53 | 54 | // SetVolume uses an underlying directsound command to set 55 | // the volume of the audio. Applies multiplicatively with volume 56 | // filters. Accepts int32s from -10000 to 0, 0 being the max and 57 | // default volume. 58 | func (ds *dsAudio) SetVolume(vol int32) error { 59 | return ds.IDirectSoundBuffer.SetVolume(vol) 60 | } 61 | 62 | func (ds *dsAudio) Filter(fs ...Filter) (Audio, error) { 63 | var a Audio = ds 64 | var err, consErr error 65 | for _, f := range fs { 66 | a, err = f.Apply(a) 67 | if err != nil { 68 | if consErr == nil { 69 | consErr = err 70 | } else { 71 | consErr = errors.New(err.Error() + ":" + consErr.Error()) 72 | } 73 | } 74 | } 75 | // Consider: this is a significant amount 76 | // of work to do just to make this an in-place filter. 77 | // would it be worth it to offer both in place and non-inplace 78 | // filter functions? 79 | a2, err2 := EncodeBytes(*ds.Encoding) 80 | if err2 != nil { 81 | return nil, err2 82 | } 83 | // reassign the contents of ds to be that of the 84 | // new audio, so that this filters in place 85 | *ds = *a2.(*dsAudio) 86 | return ds, consErr 87 | } 88 | 89 | // MustFilter acts like Filter, but ignores errors (it does not panic, 90 | // as filter errors are expected to be non-fatal) 91 | func (ds *dsAudio) MustFilter(fs ...Filter) Audio { 92 | a, _ := ds.Filter(fs...) 93 | return a 94 | } 95 | -------------------------------------------------------------------------------- /audio/encode_darwin.go: -------------------------------------------------------------------------------- 1 | //+build darwin 2 | 3 | package audio 4 | 5 | import "errors" 6 | 7 | type darwinNopAudio struct { 8 | Encoding 9 | } 10 | 11 | func (dna *darwinNopAudio) Play() <-chan error { 12 | ch := make(chan error) 13 | go func() { 14 | ch <- errors.New("Playback on Darwin is not supported") 15 | }() 16 | return ch 17 | } 18 | 19 | func (dna *darwinNopAudio) Stop() error { 20 | return errors.New("Playback on Darwin is not supported") 21 | } 22 | 23 | func (dna *darwinNopAudio) SetVolume(int32) error { 24 | return errors.New("SetVolume on Darwin is not supported") 25 | } 26 | 27 | func (dna *darwinNopAudio) Filter(fs ...Filter) (Audio, error) { 28 | var a Audio = dna 29 | var err, consErr error 30 | for _, f := range fs { 31 | a, err = f.Apply(a) 32 | if err != nil { 33 | if consErr == nil { 34 | consErr = err 35 | } else { 36 | consErr = errors.New(err.Error() + ":" + consErr.Error()) 37 | } 38 | } 39 | } 40 | return dna, consErr 41 | } 42 | 43 | func (dna *darwinNopAudio) MustFilter(fs ...Filter) Audio { 44 | a, _ := dna.Filter(fs...) 45 | return a 46 | } 47 | 48 | func EncodeBytes(enc Encoding) (Audio, error) { 49 | return &darwinNopAudio{enc}, nil 50 | } 51 | -------------------------------------------------------------------------------- /audio/encode_darwin_test.go: -------------------------------------------------------------------------------- 1 | //+build darwin 2 | 3 | package audio 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestOSXSupport(t *testing.T) { 12 | a, err := EncodeBytes(Encoding{ 13 | []byte{}, 14 | Format{}, 15 | CanLoop{}, 16 | }) 17 | assert.Nil(t, err) 18 | err = <-a.Play() 19 | assert.NotNil(t, err) 20 | err = a.Stop() 21 | assert.NotNil(t, err) 22 | } 23 | -------------------------------------------------------------------------------- /audio/encode_linux.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package audio 4 | 5 | import ( 6 | "strings" 7 | "sync" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/yobert/alsa" 11 | ) 12 | 13 | type alsaAudio struct { 14 | *Encoding 15 | *alsa.Device 16 | playAmount int 17 | playProgress int 18 | stopCh chan struct{} 19 | playing bool 20 | playCh chan error 21 | period int 22 | } 23 | 24 | func (aa *alsaAudio) Play() <-chan error { 25 | // If currently playing, restart 26 | if aa.playing { 27 | aa.playProgress = 0 28 | return aa.playCh 29 | } 30 | aa.playing = true 31 | aa.playCh = make(chan error) 32 | go func() { 33 | for { 34 | var data []byte 35 | if len(aa.Encoding.Data)-aa.playProgress <= aa.playAmount { 36 | data = aa.Encoding.Data[aa.playProgress:] 37 | if aa.Loop { 38 | delta := aa.playAmount - (len(aa.Encoding.Data) - aa.playProgress) 39 | data = append(data, aa.Encoding.Data[:delta]...) 40 | } 41 | } else { 42 | data = aa.Encoding.Data[aa.playProgress : aa.playProgress+aa.playAmount] 43 | } 44 | if len(data) != 0 { 45 | err := aa.Device.Write(data, aa.period) 46 | if err != nil { 47 | select { 48 | case aa.playCh <- err: 49 | default: 50 | } 51 | break 52 | } 53 | } 54 | aa.playProgress += aa.playAmount 55 | if aa.playProgress > len(aa.Encoding.Data) { 56 | if aa.Loop { 57 | aa.playProgress %= len(aa.Encoding.Data) 58 | } else { 59 | select { 60 | case aa.playCh <- nil: 61 | default: 62 | } 63 | break 64 | } 65 | } 66 | select { 67 | case <-aa.stopCh: 68 | select { 69 | case aa.playCh <- nil: 70 | default: 71 | } 72 | break 73 | default: 74 | } 75 | } 76 | aa.playing = false 77 | aa.playProgress = 0 78 | }() 79 | return aa.playCh 80 | } 81 | 82 | func (aa *alsaAudio) Stop() error { 83 | if aa.playing { 84 | go func() { 85 | aa.stopCh <- struct{}{} 86 | }() 87 | } else { 88 | return errors.New("Audio not playing, cannot stop") 89 | } 90 | return nil 91 | } 92 | 93 | func (aa *alsaAudio) SetVolume(int32) error { 94 | return errors.New("SetVolume on Linux is not supported") 95 | } 96 | 97 | func (aa *alsaAudio) Filter(fs ...Filter) (Audio, error) { 98 | var a Audio = aa 99 | var err, consErr error 100 | for _, f := range fs { 101 | a, err = f.Apply(a) 102 | if err != nil { 103 | if consErr == nil { 104 | consErr = err 105 | } else { 106 | consErr = errors.New(err.Error() + ":" + consErr.Error()) 107 | } 108 | } 109 | } 110 | return aa, consErr 111 | } 112 | 113 | // MustFilter acts like Filter, but ignores errors (it does not panic, 114 | // as filter errors are expected to be non-fatal) 115 | func (aa *alsaAudio) MustFilter(fs ...Filter) Audio { 116 | a, _ := aa.Filter(fs...) 117 | return a 118 | } 119 | 120 | func EncodeBytes(enc Encoding) (Audio, error) { 121 | handle, err := openDevice() 122 | if err != nil { 123 | return nil, err 124 | } 125 | // Todo: annotate these errors with more info 126 | format, err := alsaFormat(enc.Bits) 127 | if err != nil { 128 | return nil, err 129 | } 130 | _, err = handle.NegotiateFormat(format) 131 | if err != nil { 132 | return nil, err 133 | } 134 | _, err = handle.NegotiateRate(int(enc.SampleRate)) 135 | if err != nil { 136 | return nil, err 137 | } 138 | _, err = handle.NegotiateChannels(int(enc.Channels)) 139 | if err != nil { 140 | return nil, err 141 | } 142 | // Default value at recommendation of library 143 | period, err := handle.NegotiatePeriodSize(2048) 144 | if err != nil { 145 | return nil, err 146 | } 147 | _, err = handle.NegotiateBufferSize(4096) 148 | if err != nil { 149 | return nil, err 150 | } 151 | err = handle.Prepare() 152 | if err != nil { 153 | return nil, err 154 | } 155 | return &alsaAudio{ 156 | playAmount: period * int(enc.Bits) / 4, 157 | period: period, 158 | Encoding: &enc, 159 | Device: handle, 160 | stopCh: make(chan struct{}), 161 | }, nil 162 | } 163 | 164 | var ( 165 | // Todo: support more customized audio device usage 166 | openDeviceLock sync.Mutex 167 | openedDevice *alsa.Device 168 | ) 169 | 170 | func openDevice() (*alsa.Device, error) { 171 | openDeviceLock.Lock() 172 | defer openDeviceLock.Unlock() 173 | 174 | if openedDevice != nil { 175 | return openedDevice, nil 176 | } 177 | cards, err := alsa.OpenCards() 178 | if err != nil { 179 | return nil, err 180 | } 181 | defer alsa.CloseCards(cards) 182 | for i, c := range cards { 183 | dvcs, err := c.Devices() 184 | if err != nil { 185 | continue 186 | } 187 | for _, d := range dvcs { 188 | if d.Type != alsa.PCM || !d.Play { 189 | continue 190 | } 191 | if strings.Contains(d.Title, SkipDevicesContaining) { 192 | continue 193 | } 194 | d.Close() 195 | err := d.Open() 196 | if err != nil { 197 | continue 198 | } 199 | // We've a found a device we can hypothetically use 200 | cards = append(cards[:i], cards[i+1:]...) 201 | openedDevice = d 202 | return d, nil 203 | } 204 | } 205 | return nil, errors.New("No valid device found") 206 | } 207 | 208 | func alsaFormat(bits uint16) (alsa.FormatType, error) { 209 | switch bits { 210 | case 8: 211 | return alsa.S8, nil 212 | case 16: 213 | return alsa.S16_LE, nil 214 | } 215 | return 0, errors.New("Undefined alsa format for encoding bits") 216 | } 217 | -------------------------------------------------------------------------------- /audio/encode_windows.go: -------------------------------------------------------------------------------- 1 | //+build windows 2 | 3 | package audio 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/oov/directsound-go/dsound" 12 | ) 13 | 14 | var ( 15 | user32 = syscall.NewLazyDLL("user32") 16 | getDesktopWindow = user32.NewProc("GetDesktopWindow") 17 | ds *dsound.IDirectSound 18 | err error 19 | ) 20 | 21 | func init() { 22 | hasDefaultDevice := false 23 | dsound.DirectSoundEnumerate(func(guid *dsound.GUID, description string, module string) bool { 24 | if guid == nil { 25 | hasDefaultDevice = true 26 | return false 27 | } 28 | return true 29 | }) 30 | if !hasDefaultDevice { 31 | ds = nil 32 | err = errors.New("No default device available to play audio off of") 33 | return 34 | } 35 | 36 | ds, err = dsound.DirectSoundCreate(nil) 37 | if err != nil { 38 | return 39 | } 40 | // We don't check this error because Call() can return 41 | // "The operation was completed successfully" as an error! 42 | // Todo: type switch? Do we know the type of "success errors"? 43 | desktopWindow, _, err := getDesktopWindow.Call() 44 | if !strings.Contains(err.Error(), "success") { 45 | fmt.Println("Dsound initialization result:", err) 46 | } 47 | err = ds.SetCooperativeLevel(syscall.Handle(desktopWindow), dsound.DSSCL_PRIORITY) 48 | if err != nil { 49 | ds = nil 50 | } 51 | } 52 | 53 | // EncodeBytes converts an encoding to Audio 54 | func EncodeBytes(enc Encoding) (Audio, error) { 55 | // An error here would be an error from init() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | // Create the object which stores the wav data in a playable format 61 | blockAlign := enc.Channels * enc.Bits / 8 62 | dsbuff, err := ds.CreateSoundBuffer(&dsound.BufferDesc{ 63 | // These flags cover everything we should ever want to do 64 | Flags: dsound.DSBCAPS_GLOBALFOCUS | dsound.DSBCAPS_GETCURRENTPOSITION2 | dsound.DSBCAPS_CTRLVOLUME | dsound.DSBCAPS_CTRLPAN | dsound.DSBCAPS_CTRLFREQUENCY | dsound.DSBCAPS_LOCDEFER, 65 | Format: &dsound.WaveFormatEx{ 66 | FormatTag: dsound.WAVE_FORMAT_PCM, 67 | Channels: enc.Channels, 68 | SamplesPerSec: enc.SampleRate, 69 | BitsPerSample: enc.Bits, 70 | BlockAlign: blockAlign, 71 | AvgBytesPerSec: enc.SampleRate * uint32(blockAlign), 72 | ExtSize: 0, 73 | }, 74 | BufferBytes: uint32(len(enc.Data)), 75 | }) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | // Reserve some space in the sound buffer object to write to. 81 | // The Lock function (and by extension LockBytes) actually 82 | // reserves two spaces, but we ignore the second. 83 | by1, by2, err := dsbuff.LockBytes(0, uint32(len(enc.Data)), 0) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | // Write to the pointer we were given. 89 | copy(by1, enc.Data) 90 | 91 | // Update the buffer object with the new data. 92 | err = dsbuff.UnlockBytes(by1, by2) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return &dsAudio{ 97 | Encoding: &enc, 98 | IDirectSoundBuffer: dsbuff, 99 | }, nil 100 | } 101 | -------------------------------------------------------------------------------- /audio/encoding.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import "time" 4 | 5 | // Encoding contains all information required to convert raw data 6 | // (currently assumed PCM data but that may/will change) into playable Audio 7 | type Encoding struct { 8 | // Consider: non []byte data? 9 | // Consider: should Data be a type just like Format and CanLoop? 10 | Data []byte 11 | Format 12 | CanLoop 13 | } 14 | 15 | // Copy returns an audio encoded from this encoding. 16 | // Consider: Copy might be tied to HasEncoding 17 | func (enc *Encoding) Copy() (Audio, error) { 18 | return EncodeBytes(*enc.copy()) 19 | } 20 | 21 | // MustCopy acts like Copy, but will panic if err != nil 22 | func (enc *Encoding) MustCopy() Audio { 23 | a, err := EncodeBytes(*enc.copy()) 24 | if err != nil { 25 | panic(err) 26 | } 27 | return a 28 | } 29 | 30 | // GetData satisfies filter.SupportsData 31 | func (enc *Encoding) GetData() *[]byte { 32 | return &enc.Data 33 | } 34 | 35 | // PlayLength returns how long this encoding will play its data for 36 | func (enc *Encoding) PlayLength() time.Duration { 37 | return time.Duration( 38 | 1000000000*float64(len(enc.Data))/ 39 | float64(enc.SampleRate)/ 40 | float64(enc.Channels)/ 41 | float64(enc.Bits/8)) * time.Nanosecond 42 | } 43 | 44 | // copy for an encoding just copies the encoding data, 45 | // it does not return an audio. 46 | func (enc *Encoding) copy() *Encoding { 47 | newEnc := new(Encoding) 48 | newEnc.Format = enc.Format 49 | newEnc.CanLoop = enc.CanLoop 50 | newEnc.Data = make([]byte, len(enc.Data)) 51 | copy(newEnc.Data, enc.Data) 52 | return newEnc 53 | } 54 | -------------------------------------------------------------------------------- /audio/filter.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | // A Filter takes an input audio and returns some new Audio from them. 4 | // This usage implies that Audios can be copied, and that Audios have 5 | // available information to be generically modified by a Filter. The 6 | // functions for these capabilities are yet fleshed out. It's worth 7 | // considering whether a Filter modifies in place. The answer is 8 | // probably yes: 9 | // a.Filter(fs) would modify a in place 10 | // a.Copy().Filter(fs) would return a new audio 11 | // Specific audio implementations could not follow this, however. 12 | type Filter interface { 13 | Apply(Audio) (Audio, error) 14 | } 15 | 16 | // CanLoop offers composable looping 17 | type CanLoop struct { 18 | Loop bool 19 | } 20 | 21 | // GetLoop allows CanLoop to satisfy the SupportsLoop interface 22 | func (cl *CanLoop) GetLoop() *bool { 23 | return &cl.Loop 24 | } 25 | -------------------------------------------------------------------------------- /audio/filter/data.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/200sc/klangsynthese/audio" 5 | "github.com/200sc/klangsynthese/audio/filter/supports" 6 | ) 7 | 8 | // Data filters are functions on []byte types 9 | type Data func(*[]byte) 10 | 11 | // Apply checks that the given audio supports Data, filters if it 12 | // can, then returns 13 | func (df Data) Apply(a audio.Audio) (audio.Audio, error) { 14 | if sd, ok := a.(supports.Data); ok { 15 | df(sd.GetData()) 16 | return a, nil 17 | } 18 | return a, supports.NewUnsupported([]string{"Data"}) 19 | } 20 | -------------------------------------------------------------------------------- /audio/filter/encoding.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/200sc/klangsynthese/audio" 5 | "github.com/200sc/klangsynthese/audio/filter/supports" 6 | "github.com/200sc/klangsynthese/audio/manip" 7 | ) 8 | 9 | // Encoding filters are functions on any combination of the values 10 | // in an audio.Encoding 11 | type Encoding func(supports.Encoding) 12 | 13 | // Apply checks that the given audio supports Encoding, filters if it 14 | // can, then returns 15 | func (enc Encoding) Apply(a audio.Audio) (audio.Audio, error) { 16 | if senc, ok := a.(supports.Encoding); ok { 17 | enc(senc) 18 | return a, nil 19 | } 20 | return a, supports.NewUnsupported([]string{"Encoding"}) 21 | } 22 | 23 | // AssertStereo does nothing to audio that has two channels, but will convert 24 | // mono audio to two-channeled audio with the same data on both channels 25 | func AssertStereo() Encoding { 26 | return func(enc supports.Encoding) { 27 | chs := enc.GetChannels() 28 | if *chs > 1 { 29 | // We can't really do this for non-mono audio 30 | return 31 | } 32 | *chs = 2 33 | data := enc.GetData() 34 | d := *data 35 | newData := make([]byte, len(d)*2) 36 | byteDepth := int(*enc.GetBitDepth() / 8) 37 | for i := 0; i < len(d); i += 2 { 38 | for j := 0; j < byteDepth; j++ { 39 | newData[i*2+j] = d[i+j] 40 | newData[i*2+j+byteDepth] = d[i+j] 41 | } 42 | } 43 | *data = newData 44 | } 45 | } 46 | 47 | func mod(init, inc int, modFn func(float64) float64) Encoding { 48 | return func(enc supports.Encoding) { 49 | data := enc.GetData() 50 | d := *data 51 | byteDepth := int(*enc.GetBitDepth() / 8) 52 | switch byteDepth { 53 | case 2: 54 | for i := byteDepth * init; i < len(d); i += byteDepth * inc { 55 | manip.SetInt16(d, i, manip.Round(modFn(float64(manip.GetInt16(d, i))))) 56 | } 57 | default: 58 | // log unsupported byte depth 59 | } 60 | *data = d 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /audio/filter/fourier.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | // these fourier functions did not work for me. 4 | // In case I can fix them, I leave them here. 5 | // Credit Arnaud Gatouillat 6 | 7 | // fourier1 has a bad name 8 | // fourier1 is a helper function that does some kind of fourier transform math 9 | // What are nn and isign? 10 | // func fourier1(data []float64, nn, isign int) { 11 | // n := nn << 1 12 | // j := 1 13 | // for i := 1; i < n; i += 2 { 14 | // if j > i { 15 | // data[j], data[i] = data[i], data[j] 16 | // data[j+1], data[i+1] = data[i+1], data[j+1] 17 | // } 18 | // m := n >> 1 19 | // for m >= 2 && j > m { 20 | // j -= m 21 | // m >>= 1 22 | // } 23 | // j += m 24 | // } 25 | // mmax := 2 26 | // for n > mmax { 27 | // stp := 2 * mmax 28 | // theta := math.Pi * 2 / float64(isign*mmax) 29 | // wpr, wpi := wprWpi(theta) 30 | // wr := 1.0 31 | // wi := 0.0 32 | // for m := 1; m < mmax; m += 2 { 33 | // for i := m; i <= n; i += stp { 34 | // tr := wr*data[j] - wi*data[j+1] 35 | // ti := wr*data[j+1] - wi*data[i] 36 | // data[j] = data[i] - tr 37 | // data[j+1] = data[i+1] - ti 38 | // data[i] += tr 39 | // data[i+1] += ti 40 | // } 41 | // wt := wr 42 | // wr = wr*wpr - wi*wpi + wr 43 | // wi = wi*wpr + wt*wpi + wi 44 | // } 45 | // mmax = stp 46 | // } 47 | // } 48 | 49 | // func RealFourierTransform(data []float64, n, isign int) { 50 | // theta := math.Pi / float64(n) 51 | // var c2 float64 52 | // if isign == 1 { 53 | // c2 = -.5 54 | // fourier1(data, n, 1) 55 | // } else { 56 | // c2 = .5 57 | // theta *= -1 58 | // } 59 | // wpr, wpi := wprWpi(theta) 60 | // wr := 1.0 + wpr 61 | // wi := wpi 62 | // // Wow what a great name for this variable 63 | // n2p3 := 2*n + 3 64 | // for i := 2; i <= n/2; i++ { 65 | // i1 := i + i - 1 66 | // i2 := i1 + 1 67 | // i3 := n2p3 - i2 68 | // i4 := i3 + 1 69 | // h1r := .5 * (data[i1] + data[i3]) 70 | // h1i := .5 * (data[i2] - data[i4]) 71 | // h2r := -c2 * (data[i2] + data[i4]) 72 | // h2i := c2 * (data[i1] - data[i3]) 73 | // data[i1] = h1r + wr*h2r - wi*h2i 74 | // data[i2] = h1i + wr*h2i + wi*h2r 75 | // data[i3] = h1r - wr*h2r + wi*h2i 76 | // data[i4] = -h1i + wr*h2i + wi*h2r 77 | // wt := wr 78 | // wr = wr*wpr - wi*wpi + wr 79 | // wi = wi*wpr + wt*wpi + wi 80 | // } 81 | // if isign == 1 { 82 | // data[1], data[2] = (data[1] + data[2]), (data[1] - data[2]) 83 | // } else { 84 | // data[1], data[2] = .5*(data[1]+data[2]), .5*(data[1]-data[2]) 85 | // fourier1(data, n, -1) 86 | // } 87 | // } 88 | 89 | // func wprWpi(theta float64) (float64, float64) { 90 | // w := math.Sin(0.5 * theta) 91 | // wpr := -2 * math.Pow(w, 2) 92 | // wpi := math.Sin(theta) 93 | // return wpr, wpi 94 | // } 95 | -------------------------------------------------------------------------------- /audio/filter/guarantees.go: -------------------------------------------------------------------------------- 1 | // Package filter provides various audio filters to be applied to audios through the 2 | // Filter() function 3 | package filter 4 | 5 | import ( 6 | "github.com/200sc/klangsynthese/audio" 7 | "github.com/200sc/klangsynthese/audio/filter/supports" 8 | ) 9 | 10 | // These declarations guarantee that the filters in this package satisfy the filter interface 11 | var ( 12 | _ audio.Filter = SampleRate(func(*uint32) {}) 13 | _ audio.Filter = Data(func(*[]byte) {}) 14 | _ audio.Filter = Loop(func(*bool) {}) 15 | _ audio.Filter = Encoding(func(supports.Encoding) {}) 16 | ) 17 | -------------------------------------------------------------------------------- /audio/filter/loop.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/200sc/klangsynthese/audio" 5 | "github.com/200sc/klangsynthese/audio/filter/supports" 6 | ) 7 | 8 | // Loop functions modify a boolean, with the intention that that boolean 9 | // is a loop variable 10 | type Loop func(*bool) 11 | 12 | // Apply checks that the given audio supports Loop, filters if it 13 | // can, then returns 14 | func (lf Loop) Apply(a audio.Audio) (audio.Audio, error) { 15 | if sl, ok := a.(supports.Loop); ok { 16 | lf(sl.GetLoop()) 17 | return a, nil 18 | } 19 | return a, supports.NewUnsupported([]string{"Loop"}) 20 | } 21 | 22 | // LoopOn sets the loop to happen 23 | func LoopOn() Loop { 24 | return func(b *bool) { 25 | *b = true 26 | } 27 | } 28 | 29 | // LoopOff sets the loop to not happen 30 | func LoopOff() Loop { 31 | return func(b *bool) { 32 | *b = false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /audio/filter/pan.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "github.com/200sc/klangsynthese/audio/filter/supports" 4 | 5 | // LeftPan filters audio to only play on the left speaker 6 | func LeftPan() Encoding { 7 | return func(enc supports.Encoding) { 8 | data := enc.GetData() 9 | // Right/Left only makes sense for 2 channel 10 | if *enc.GetChannels() != 2 { 11 | return 12 | } 13 | // Zero out one channel 14 | swtch := int((*enc.GetBitDepth()) / 8) 15 | d := *data 16 | for i := 0; i < len(d); i += (2 * swtch) { 17 | for j := 0; j < swtch; j++ { 18 | d[i+j] = byte((int(d[i+j]) + int(d[i+j+swtch])) / 2) 19 | d[i+j+swtch] = 0 20 | } 21 | } 22 | *data = d 23 | } 24 | } 25 | 26 | // RightPan filters audio to only play on the right speaker 27 | func RightPan() Encoding { 28 | return func(enc supports.Encoding) { 29 | data := enc.GetData() 30 | // Right/Left only makes sense for 2 channel 31 | if *enc.GetChannels() != 2 { 32 | return 33 | } 34 | // Zero out one channel 35 | swtch := int((*enc.GetBitDepth()) / 8) 36 | d := *data 37 | for i := 0; i < len(d); i += (2 * swtch) { 38 | for j := 0; j < swtch; j++ { 39 | d[i+j+swtch] = byte((int(d[i+j]) + int(d[i+j+swtch])) / 2) 40 | d[i+j] = 0 41 | } 42 | } 43 | *data = d 44 | } 45 | } 46 | 47 | // Pan takes -1 <= f <= 1. 48 | // An f of -1 represents a full pan to the left, a pan of 1 represents 49 | // a full pan to the right. 50 | func Pan(f float64) Encoding { 51 | // Todo: test this is accurate 52 | if f > 0 { 53 | return VolumeBalance(1-f, 1) 54 | } else if f < 0 { 55 | return VolumeBalance(1, 1-(-1*f)) 56 | } else { 57 | return func(enc supports.Encoding) { 58 | data := enc.GetData() 59 | // Right/Left only makes sense for 2 channel 60 | if *enc.GetChannels() != 2 { 61 | return 62 | } 63 | // Zero out one channel 64 | swtch := int((*enc.GetBitDepth()) / 8) 65 | d := *data 66 | for i := 0; i < len(d); i += (2 * swtch) { 67 | for j := 0; j < swtch; j++ { 68 | v := byte((int(d[i+j]) + int(d[i+j+swtch])) / 2) 69 | d[i+j+swtch] = v 70 | d[i+j] = v 71 | } 72 | } 73 | *data = d 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /audio/filter/pitchshift.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/200sc/klangsynthese/audio/filter/supports" 7 | "github.com/200sc/klangsynthese/audio/manip" 8 | ) 9 | 10 | /***************************************************************************** 11 | * HOME URL: http://blogs.zynaptiq.com/bernsee 12 | * KNOWN BUGS: none 13 | * 14 | * SYNOPSIS: Routine for doing pitch shifting while maintaining 15 | * duration using the Short Time Fourier Transform. 16 | * 17 | * DESCRIPTION: The routine takes a pitchShift factor value which is between 0.5 18 | * (one octave down) and 2. (one octave up). A value of exactly 1 does not change 19 | * the pitch. numSampsToProcess tells the routine how many samples in indata[0... 20 | * numSampsToProcess-1] should be pitch shifted and moved to outdata[0 ... 21 | * numSampsToProcess-1]. The two buffers can be identical (ie. it can process the 22 | * data in-place). fftFrameSize defines the FFT frame size used for the 23 | * processing. Typical values are 1024, 2048 and 4096. It may be any value <= 24 | * MAX_FRAME_LENGTH but it MUST be a power of 2. osamp is the STFT 25 | * oversampling factor which also determines the overlap between adjacent STFT 26 | * frames. It should at least be 4 for moderate scaling ratios. A value of 32 is 27 | * recommended for best quality. sampleRate takes the sample rate for the signal 28 | * in unit Hz, ie. 44100 for 44.1 kHz audio. The data passed to the routine in 29 | * indata[] should be in the range [-1.0, 1.0), which is also the output range 30 | * for the data, make sure you scale the data accordingly (for 16bit signed integers 31 | * you would have to divide (and multiply) by 32768). 32 | * 33 | * COPYRIGHT 1999-2015 Stephan M. Bernsee 34 | * 35 | * The Wide Open License (WOL) 36 | * 37 | * Permission to use, copy, modify, distribute and sell this software and its 38 | * documentation for any purpose is hereby granted without fee, provided that 39 | * the above copyright notice and this license appear in all source copies. 40 | * THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY OF 41 | * ANY KIND. See http://www.dspguru.com/wol.htm for more information. 42 | * 43 | *****************************************************************************/ 44 | // As is standard with translations of this code to other languages, 45 | // Go translation copyright Patrick Stephen 2017 46 | // To be clear, the PitchShift function + FFT is what had to be translated 47 | 48 | // A PitchShifter has an encoding function that will shift 49 | // a pitch up to an octave up or down (0.5 -> octave down, 2.0 -> octave up) 50 | // these are for lower-level use, and a similar type that takes in steps to 51 | // shift by (and eventually pitches to set to) will follow. 52 | type PitchShifter interface { 53 | PitchShift(float64) Encoding 54 | } 55 | 56 | // FFTShifter holds buffers and settings for performing a pitch shift on PCM audio 57 | type FFTShifter struct { 58 | fftFrameSize int 59 | oversampling int 60 | step int 61 | latency int 62 | stack, frame []float64 63 | workBuffer []float64 64 | magnitudes, frequencies []float64 65 | synthMagnitudes, synthFrequencies []float64 66 | lastPhase, sumPhase []float64 67 | outAcc []float64 68 | expected float64 69 | window, windowFactors []float64 70 | } 71 | 72 | // These are built in shifters with some common inputs 73 | var ( 74 | LowQualityShifter, _ = NewFFTShifter(1024, 8) 75 | HighQualityShifter, _ = NewFFTShifter(1024, 32) 76 | ) 77 | 78 | // NewFFTShifter returns a pitch shifter that uses fast fourier transforms 79 | func NewFFTShifter(fftFrameSize int, oversampling int) (PitchShifter, error) { 80 | // Todo: check that the frame size and oversampling rate make sense 81 | ps := FFTShifter{} 82 | ps.fftFrameSize = fftFrameSize 83 | ps.oversampling = oversampling 84 | ps.step = fftFrameSize / oversampling 85 | ps.latency = fftFrameSize - ps.step 86 | ps.stack = make([]float64, fftFrameSize) 87 | ps.workBuffer = make([]float64, 2*fftFrameSize) 88 | ps.magnitudes = make([]float64, fftFrameSize) 89 | ps.frequencies = make([]float64, fftFrameSize) 90 | ps.synthMagnitudes = make([]float64, fftFrameSize) 91 | ps.synthFrequencies = make([]float64, fftFrameSize) 92 | ps.lastPhase = make([]float64, fftFrameSize/2+1) 93 | ps.sumPhase = make([]float64, fftFrameSize/2+1) 94 | ps.outAcc = make([]float64, 2*fftFrameSize) 95 | 96 | ps.expected = 2 * math.Pi * float64(ps.step) / float64(fftFrameSize) 97 | 98 | ps.window = make([]float64, fftFrameSize) 99 | ps.windowFactors = make([]float64, fftFrameSize) 100 | t := 0.0 101 | for i := 0; i < fftFrameSize; i++ { 102 | w := -0.5*math.Cos(t) + .5 103 | ps.window[i] = w 104 | ps.windowFactors[i] = w * (2.0 / float64(fftFrameSize*oversampling)) 105 | t += (math.Pi * 2) / float64(fftFrameSize) 106 | } 107 | 108 | ps.frame = make([]float64, fftFrameSize) 109 | return ps, nil 110 | } 111 | 112 | // PitchShift modifies filtered audio by the input float, between 0.5 and 2.0, 113 | // each end of the spectrum representing octave down and up respectively 114 | func (ps FFTShifter) PitchShift(shiftBy float64) Encoding { 115 | return func(senc supports.Encoding) { 116 | data := *senc.GetData() 117 | bitDepth := *senc.GetBitDepth() 118 | byteDepth := bitDepth / 8 119 | sampleRate := *senc.GetSampleRate() 120 | channels := *senc.GetChannels() 121 | 122 | // Jeeez 123 | out := make([]byte, len(data)) 124 | copy(out, data) 125 | 126 | freqPerBin := float64(sampleRate) / float64(ps.fftFrameSize) 127 | frameIndex := ps.latency 128 | 129 | // End jeeeez 130 | 131 | // for each channel individually 132 | for c := 0; c < int(channels); c++ { 133 | // convert this to a channel-specific float64 buffer 134 | f64in := manip.BytesToF64(data, channels, bitDepth, c) 135 | f64out := f64in 136 | 137 | for i := 0; i < len(f64in); i++ { 138 | // Get a frame 139 | ps.frame[frameIndex] = f64in[i] 140 | // Bug here for early i values: they'll all be 0! 141 | f64out[i] = ps.stack[frameIndex-ps.latency] 142 | frameIndex++ 143 | 144 | // A full frame has been obtained 145 | if frameIndex >= ps.fftFrameSize { 146 | frameIndex = ps.latency 147 | 148 | // Windowing 149 | for k := 0; k < ps.fftFrameSize; k++ { 150 | ps.workBuffer[2*k] = ps.frame[k] * ps.window[k] 151 | ps.workBuffer[(2*k)+1] = 0 152 | } 153 | 154 | ShortTimeFourierTransform(ps.workBuffer, ps.fftFrameSize, -1) 155 | 156 | // Analysis 157 | for k := 0; k <= ps.fftFrameSize/2; k++ { 158 | real := ps.workBuffer[2*k] 159 | imag := ps.workBuffer[(2*k)+1] 160 | 161 | magn := 2 * math.Sqrt(real*real+imag*imag) 162 | ps.magnitudes[k] = magn 163 | 164 | phase := math.Atan2(imag, real) 165 | 166 | diff := phase - ps.lastPhase[k] 167 | ps.lastPhase[k] = phase 168 | 169 | diff -= float64(k) * ps.expected 170 | 171 | deltaPhase := int(diff * (1 / math.Pi)) 172 | if deltaPhase >= 0 { 173 | deltaPhase += deltaPhase & 1 174 | } else { 175 | deltaPhase -= deltaPhase & 1 176 | } 177 | 178 | diff -= math.Pi * float64(deltaPhase) 179 | diff *= float64(ps.oversampling) / (math.Pi * 2) 180 | diff = (float64(k) + diff) * freqPerBin 181 | 182 | ps.frequencies[k] = diff 183 | } 184 | 185 | // Processing 186 | for k := 0; k < ps.fftFrameSize; k++ { 187 | ps.synthMagnitudes[k] = 0 188 | ps.synthFrequencies[k] = 0 189 | } 190 | 191 | for k := 0; k < ps.fftFrameSize/2; k++ { 192 | l := int(float64(k) * shiftBy) 193 | if l < ps.fftFrameSize/2 { 194 | ps.synthMagnitudes[l] += ps.magnitudes[k] 195 | ps.synthFrequencies[l] = ps.frequencies[k] * shiftBy 196 | } 197 | } 198 | 199 | // Synthesis 200 | for k := 0; k <= ps.fftFrameSize/2; k++ { 201 | magn := ps.synthMagnitudes[k] 202 | tmp := ps.synthFrequencies[k] 203 | tmp -= float64(k) * freqPerBin 204 | tmp /= freqPerBin 205 | tmp *= 2 * math.Pi / float64(ps.oversampling) 206 | tmp += float64(k) * ps.expected 207 | ps.sumPhase[k] += tmp 208 | 209 | ps.workBuffer[2*k] = magn * math.Cos(ps.sumPhase[k]) 210 | ps.workBuffer[(2*k)+1] = magn * math.Sin(ps.sumPhase[k]) 211 | } 212 | 213 | // Remove negative frequencies 214 | // I don't get how we know these ones are negative 215 | // also this looks like it's going to overflow the slice 216 | for k := ps.fftFrameSize + 2; k < 2*ps.fftFrameSize; k++ { 217 | ps.workBuffer[k] = 0.0 218 | } 219 | 220 | ShortTimeFourierTransform(ps.workBuffer, ps.fftFrameSize, 1) 221 | 222 | // Windowing 223 | for k := 0; k < ps.fftFrameSize; k++ { 224 | ps.outAcc[k] += ps.windowFactors[k] * ps.workBuffer[2*k] 225 | } 226 | for k := 0; k < ps.step; k++ { 227 | ps.stack[k] = ps.outAcc[k] 228 | } 229 | 230 | // Shift accumulator, shift frame 231 | for k := 0; k < ps.fftFrameSize; k++ { 232 | ps.outAcc[k] = ps.outAcc[k+ps.step] 233 | } 234 | 235 | for k := 0; k < ps.latency; k++ { 236 | ps.frame[k] = ps.frame[k+ps.step] 237 | } 238 | } 239 | } 240 | // remap this f64in to the output 241 | for i := c * int(byteDepth); i < len(data); i += int(byteDepth * 2) { 242 | manip.SetInt16_f64(out, i, f64in[i/int(byteDepth*2)]) 243 | } 244 | } 245 | datap := senc.GetData() 246 | *datap = out 247 | } 248 | } 249 | 250 | // ShortTimeFourierTransform : FFT routine, (C)1996 S.M.Bernsee. Sign = -1 is FFT, 1 is iFFT (inverse) 251 | // Fills fftBuffer[0...2*fftFrameSize-1] with the Fourier transform of the 252 | // time domain data in fftBuffer[0...2*fftFrameSize-1]. The FFT array takes 253 | // and returns the cosine and sine parts in an interleaved manner, ie. 254 | // fftBuffer[0] = cosPart[0], fftBuffer[1] = sinPart[0], asf. fftFrameSize 255 | // must be a power of 2. It expects a complex input signal (see footnote 2), 256 | // ie. when working with 'common' audio signals our input signal has to be 257 | // passed as {in[0],0.,in[1],0.,in[2],0.,...} asf. In that case, the transform 258 | // of the frequencies of interest is in fftBuffer[0...fftFrameSize]. 259 | func ShortTimeFourierTransform(data []float64, fftFrameSize, sign int) { 260 | for i := 2; i < 2*(fftFrameSize-2); i += 2 { 261 | j := 0 262 | for bitm := 2; bitm < 2*fftFrameSize; bitm <<= 1 { 263 | if (i & bitm) != 0 { 264 | j++ 265 | } 266 | j <<= 1 267 | } 268 | if i < j { 269 | data[j], data[i] = data[i], data[j] 270 | data[j+1], data[i+1] = data[i+1], data[j+1] 271 | } 272 | } 273 | max := int(math.Log(float64(fftFrameSize))/math.Log(2) + .5) 274 | le := 2 275 | for k := 0; k < max; k++ { 276 | le <<= 1 277 | le2 := le >> 1 278 | ur := 1.0 279 | ui := 0.0 280 | arg := math.Pi / float64(le2>>1) 281 | wr := math.Cos(arg) 282 | wi := float64(sign) * math.Sin(arg) 283 | for j := 0; j < le2; j += 2 { 284 | for i := j; i < 2*fftFrameSize; i += le { 285 | tr := data[i+le2]*ur - data[i+le2+1]*ui 286 | ti := data[i+le2]*ui + data[i+le2+1]*ur 287 | data[i+le2] = data[i] - tr 288 | data[i+le2+1] = data[i+1] - ti 289 | data[i] += tr 290 | data[i+1] += ti 291 | } 292 | tmp := ur*wr - ui*wi 293 | ui = ur*wi + ui*wr 294 | ur = tmp 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /audio/filter/resample.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/200sc/klangsynthese/audio/filter/supports" 7 | ) 8 | 9 | // Speed modifies the filtered audio by a speed ratio, changing its sample rate 10 | // in the process while maintaining pitch. 11 | func Speed(ratio float64, pitchShifter PitchShifter) Encoding { 12 | return func(senc supports.Encoding) { 13 | r := ratio 14 | fmt.Println(ratio) 15 | for r < .5 { 16 | r *= 2 17 | pitchShifter.PitchShift(.5)(senc) 18 | } 19 | for r > 2.0 { 20 | r /= 2 21 | pitchShifter.PitchShift(2.0)(senc) 22 | } 23 | pitchShifter.PitchShift(1 / r)(senc) 24 | ModSampleRate(ratio)(senc.GetSampleRate()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /audio/filter/sampleRate.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/200sc/klangsynthese/audio" 5 | "github.com/200sc/klangsynthese/audio/filter/supports" 6 | ) 7 | 8 | // A SampleRate is a function that takes in uint32 SampleRates 9 | type SampleRate func(*uint32) 10 | 11 | // Apply checks that the given audio supports SampleRate, filters if it 12 | // can, then returns 13 | func (srf SampleRate) Apply(a audio.Audio) (audio.Audio, error) { 14 | if ssr, ok := a.(supports.SampleRate); ok { 15 | srf(ssr.GetSampleRate()) 16 | return a, nil 17 | } 18 | return a, supports.NewUnsupported([]string{"SampleRate"}) 19 | } 20 | 21 | // ModSampleRate might slow down or speed up a sample, but this will 22 | // effect the perceived pitch of the sample. See Speed. 23 | func ModSampleRate(mult float64) SampleRate { 24 | return func(sr *uint32) { 25 | *sr = uint32(float64(*sr) * mult) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /audio/filter/supports/supports.go: -------------------------------------------------------------------------------- 1 | // Package supports holds interface types for filter supports 2 | package supports 3 | 4 | // Data types support filters that manipulate their raw audio data 5 | type Data interface { 6 | GetData() *[]byte 7 | } 8 | 9 | // Loop types support filters that manipulate whether they loop 10 | type Loop interface { 11 | GetLoop() *bool 12 | } 13 | 14 | // SampleRate types support filters that manipulate their SampleRate 15 | type SampleRate interface { 16 | GetSampleRate() *uint32 17 | } 18 | 19 | // BitDepth types support filters that manipulate bit depth. Probably 20 | // only useful in combination as an encoding 21 | type BitDepth interface { 22 | GetBitDepth() *uint16 23 | } 24 | 25 | // Channels types support filters that manipulate channels. Probably 26 | // only useful in combination as an encoding 27 | type Channels interface { 28 | GetChannels() *uint16 29 | } 30 | 31 | // Encoding types can get any variable on an audio.Encoding. They do 32 | // not just return an audio.Encoding because that would be an import 33 | // loop or another package to avoid said import loop. 34 | type Encoding interface { 35 | SampleRate 36 | BitDepth 37 | Data 38 | Channels 39 | } 40 | -------------------------------------------------------------------------------- /audio/filter/supports/unsupported.go: -------------------------------------------------------------------------------- 1 | package supports 2 | 3 | // Unsupported is an error type reporting that a filter was not supported 4 | // by the Audio type it was used on 5 | type Unsupported struct { 6 | filters []string 7 | } 8 | 9 | // NewUnsupported returns an Unsupported error with the input filters 10 | func NewUnsupported(filters []string) Unsupported { 11 | return Unsupported{filters} 12 | } 13 | 14 | func (un Unsupported) Error() string { 15 | s := "Unsupported filters: " 16 | for _, f := range un.filters { 17 | s += f + " " 18 | } 19 | return s 20 | } 21 | -------------------------------------------------------------------------------- /audio/filter/volume.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/200sc/klangsynthese/audio/filter/supports" 5 | "github.com/200sc/klangsynthese/audio/manip" 6 | ) 7 | 8 | // Volume will magnify the data by mult, increasing or reducing the volume 9 | // of the output sound. For mult <= 1 this should have no unexpected behavior, 10 | // although for mult ~= 1 it might not have any effect. More importantly for 11 | // mult > 1, values may result in the output data clipping over integer overflows, 12 | // which is presumably not desired behavior. 13 | func Volume(mult float64) Encoding { 14 | return vol(0, 1, mult) 15 | } 16 | 17 | // VolumeLeft acts like volume but reduces left channel volume only 18 | func VolumeLeft(mult float64) Encoding { 19 | return vol(0, 2, mult) 20 | } 21 | 22 | // VolumeRight acts like volume but reduces left channel volume only 23 | func VolumeRight(mult float64) Encoding { 24 | return vol(1, 2, mult) 25 | } 26 | 27 | func vol(init, inc int, mult float64) Encoding { 28 | return mod(init, inc, func(f float64) float64 { 29 | return f * mult 30 | }) 31 | } 32 | 33 | // VolumeBalance will filter audio on two channels such that the left channel 34 | // is (l+r)/2 * lMult, and the right channel is (l+r)/2 * rMult 35 | func VolumeBalance(lMult, rMult float64) Encoding { 36 | return func(enc supports.Encoding) { 37 | if *enc.GetChannels() != 2 { 38 | return 39 | } 40 | data := enc.GetData() 41 | d := *data 42 | byteDepth := int(*enc.GetBitDepth() / 8) 43 | switch byteDepth { 44 | case 2: 45 | for i := 0; i < len(d); i += (byteDepth * 2) { 46 | var v int16 47 | var shift uint16 48 | for j := 0; j < byteDepth; j++ { 49 | v += int16(int(d[i+j])+int(d[i+j+byteDepth])) / 2 << shift 50 | shift += 8 51 | } 52 | l := manip.Round(float64(v) * lMult) 53 | r := manip.Round(float64(v) * rMult) 54 | for j := 0; j < byteDepth; j++ { 55 | d[i+j] = byte(l & 255) 56 | d[i+j+byteDepth] = byte(r & 255) 57 | l >>= 8 58 | r >>= 8 59 | } 60 | } 61 | default: 62 | // log unsupported bit depth 63 | } 64 | *data = d 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /audio/format.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | // Format stores the variables which are presumably 4 | // constant for any given type of audio (wav / mp3 / flac ...) 5 | type Format struct { 6 | SampleRate uint32 7 | Channels uint16 8 | Bits uint16 9 | } 10 | 11 | // GetSampleRate satisfies supports.SampleRate 12 | func (f *Format) GetSampleRate() *uint32 { 13 | return &f.SampleRate 14 | } 15 | 16 | // GetChannels satisfies supports.Channels 17 | func (f *Format) GetChannels() *uint16 { 18 | return &f.Channels 19 | } 20 | 21 | // GetBitDepth satisfied supports.BitDepth 22 | func (f *Format) GetBitDepth() *uint16 { 23 | return &f.Bits 24 | } 25 | 26 | // Wave takes in raw bytes and encodes them according to this format 27 | func (f *Format) Wave(b []byte) (Audio, error) { 28 | return EncodeBytes(Encoding{b, *f, CanLoop{}}) 29 | } 30 | -------------------------------------------------------------------------------- /audio/manip/convert.go: -------------------------------------------------------------------------------- 1 | package manip 2 | 3 | func BytesToF64(data []byte, channels, bitRate uint16, channel int) []float64 { 4 | byteDepth := bitRate / 8 5 | out := make([]float64, (len(data)/int(byteDepth*channels))+1) 6 | for i := channel * int(byteDepth); i < len(data); i += int(byteDepth * channels) { 7 | out[i/int(byteDepth*channels)] = GetFloat64(data, i, byteDepth) 8 | } 9 | return out 10 | } 11 | -------------------------------------------------------------------------------- /audio/manip/math.go: -------------------------------------------------------------------------------- 1 | package manip 2 | 3 | func SetInt16(d []byte, i int, in int64) { 4 | for j := 0; j < 2; j++ { 5 | d[i+j] = byte(in & 255) 6 | in >>= 8 7 | } 8 | } 9 | 10 | func GetInt16(d []byte, i int) (out int16) { 11 | var shift uint16 12 | for j := 0; j < 2; j++ { 13 | out += int16(d[i+j]) << shift 14 | shift += 8 15 | } 16 | return 17 | } 18 | 19 | func GetFloat64(d []byte, i int, byteDepth uint16) float64 { 20 | switch byteDepth { 21 | case 1: 22 | return float64(int8(d[i])) / 128.0 23 | case 2: 24 | return float64(GetInt16(d, i)) / 32768.0 25 | } 26 | return 0.0 27 | } 28 | 29 | func SetInt16_f64(d []byte, i int, in float64) { 30 | SetInt16(d, i, int64(in*32768)) 31 | } 32 | 33 | func Round(f float64) int64 { 34 | if f < 0 { 35 | return int64(f - .5) 36 | } 37 | return int64(f + .5) 38 | } 39 | -------------------------------------------------------------------------------- /audio/multi.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // A Multi lets lists of audios be used simultaneously 10 | type Multi struct { 11 | Audios []Audio 12 | } 13 | 14 | // NewMulti returns a new multi 15 | func NewMulti(as ...Audio) *Multi { 16 | return &Multi{Audios: as} 17 | } 18 | 19 | // Play plays all audios in the Multi ASAP 20 | func (m *Multi) Play() <-chan error { 21 | extCh := make(chan error) 22 | go func() { 23 | // Todo: Propagating N errors? 24 | for _, a := range m.Audios { 25 | a.Play() 26 | } 27 | extCh <- nil 28 | }() 29 | return extCh 30 | } 31 | 32 | // Filter applies all the given filters on everything in the Multi 33 | func (m *Multi) Filter(fs ...Filter) (Audio, error) { 34 | var err, consErr error 35 | for i, a := range m.Audios { 36 | m.Audios[i], err = a.Filter(fs...) 37 | if err != nil { 38 | consErr = errors.New(err.Error() + ":" + consErr.Error()) 39 | } 40 | } 41 | return m, consErr 42 | } 43 | 44 | // MustFilter acts like filter but ignores errors. 45 | func (m *Multi) MustFilter(fs ...Filter) Audio { 46 | a, _ := m.Filter(fs...) 47 | return a 48 | } 49 | 50 | func (m *Multi) SetVolume(vol int32) error { 51 | for _, a := range m.Audios { 52 | err := a.SetVolume(vol) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | // Stop stops all audios in the Multi. Any that fail will report an error. 61 | func (m *Multi) Stop() error { 62 | var err, consErr error 63 | for _, a := range m.Audios { 64 | err = a.Stop() 65 | if err != nil { 66 | if consErr == nil { 67 | consErr = err 68 | } else { 69 | consErr = errors.New(err.Error() + ":" + consErr.Error()) 70 | } 71 | } 72 | } 73 | return consErr 74 | } 75 | 76 | // Copy returns a copy of this Multi 77 | func (m *Multi) Copy() (Audio, error) { 78 | var err error 79 | newAudios := make([]Audio, len(m.Audios)) 80 | for i, a := range m.Audios { 81 | newAudios[i], err = a.Copy() 82 | if err != nil { 83 | return nil, err 84 | } 85 | } 86 | return &Multi{newAudios}, nil 87 | 88 | } 89 | 90 | // MustCopy acts like Copy but panics if error != nil 91 | func (m *Multi) MustCopy() Audio { 92 | m2, err := m.Copy() 93 | if err != nil { 94 | panic(err) 95 | } 96 | return m2 97 | } 98 | 99 | // PlayLength returns how long this audio will play for 100 | func (m *Multi) PlayLength() time.Duration { 101 | var d time.Duration 102 | for _, a := range m.Audios { 103 | d2 := a.PlayLength() 104 | if d < d2 { 105 | d = d2 106 | } 107 | } 108 | return d 109 | } 110 | -------------------------------------------------------------------------------- /audio/skip_devices.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // SkipDevicesContaining is a environment variable controlled value 8 | // which will cause audio devices containing the given string to be 9 | // skipped when finding an audio device to play audio through. 10 | // Currently only supported on linux. 11 | // Todo: find a more elegant fix for bad audio devices being chosen 12 | var SkipDevicesContaining = "HDMI" 13 | 14 | 15 | func init() { 16 | skipDevices := os.Getenv("KGS_AUDIO_SKIP_DEVICES") 17 | if skipDevices != "" { 18 | SkipDevicesContaining = skipDevices 19 | } 20 | } -------------------------------------------------------------------------------- /flac/flac.go: -------------------------------------------------------------------------------- 1 | // Package flac provides functionality to handle .flac files and .flac encoded data 2 | package flac 3 | 4 | import ( 5 | "io" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/200sc/klangsynthese/audio" 10 | "github.com/eaburns/flac" 11 | ) 12 | 13 | // def wav format 14 | var format = audio.Format{ 15 | SampleRate: 44100, 16 | Bits: 16, 17 | Channels: 2, 18 | } 19 | 20 | // Load loads wav data from the incoming reader as an audio 21 | func Load(r io.Reader) (audio.Audio, error) { 22 | data, meta, err := flac.Decode(r) 23 | if err != nil { 24 | return nil, errors.Wrap(err, "Failed to load flac") 25 | } 26 | 27 | fformat := audio.Format{ 28 | SampleRate: uint32(meta.SampleRate), 29 | Channels: uint16(meta.NChannels), 30 | Bits: uint16(meta.BitsPerSample), 31 | } 32 | return audio.EncodeBytes( 33 | audio.Encoding{ 34 | Data: data, 35 | Format: fformat, 36 | }) 37 | } 38 | 39 | // Save will eventually save an audio encoded as a wav to the given writer 40 | func Save(r io.ReadWriter, a audio.Audio) error { 41 | return errors.New("Unsupported Functionality") 42 | } 43 | 44 | // Format returns the default wav formatting 45 | func Format() audio.Format { 46 | return format 47 | } 48 | -------------------------------------------------------------------------------- /flac/flac_test.go: -------------------------------------------------------------------------------- 1 | package flac 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestBasicFlac(t *testing.T) { 13 | fmt.Println("Opening Basic Flac") 14 | f, err := os.Open("test2.flac") 15 | fmt.Println(f) 16 | assert.Nil(t, err) 17 | a, err2 := Load(f) 18 | fmt.Println(a) 19 | assert.Nil(t, err2) 20 | fmt.Println("Now playing") 21 | err = <-a.Play() 22 | assert.Nil(t, err) 23 | time.Sleep(8 * time.Second) 24 | // In addition to the error tests here, this should play noise 25 | } 26 | -------------------------------------------------------------------------------- /flac/test.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200sc/klangsynthese/a0e14a8c862b10650b09ce186bdf4798e6e90e22/flac/test.flac -------------------------------------------------------------------------------- /flac/test2.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200sc/klangsynthese/a0e14a8c862b10650b09ce186bdf4798e6e90e22/flac/test2.flac -------------------------------------------------------------------------------- /font/audio.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import "github.com/200sc/klangsynthese/audio" 4 | 5 | // Audio is an ease-of-use wrapper around an audio 6 | // with an attached font, so that the audio can be played 7 | // with .Play() but can take in the remotely variable 8 | // font filter options. 9 | // 10 | // Note that it is a conscious choice for both Font and 11 | // Audio to have a Filter(...Filter) function, so that when 12 | // a FontAudio is in use the user needs to specify which 13 | // element they want to apply a filter on. The alternative would 14 | // be to have two similarly named functions, and its believed 15 | // that fa.Font.Filter(...) and fa.Audio.Filter(...) is 16 | // more or less equivalent to whatever those names would be. 17 | type Audio struct { 18 | *Font 19 | audio.FullAudio 20 | toStop audio.Audio 21 | } 22 | 23 | // NewAudio returns a *FontAudio. 24 | // For preparation against API changes, using NewAudio over Audio{} 25 | // is recommended. 26 | func NewAudio(f *Font, a audio.FullAudio) *Audio { 27 | return &Audio{f, a, nil} 28 | } 29 | 30 | // Play is equivalent to Audio.Font.Play(a.Audio) 31 | func (ad *Audio) Play() <-chan error { 32 | a2, err := ad.FullAudio.Copy() 33 | if err != nil { 34 | ch := make(chan error) 35 | go func() { 36 | ch <- err 37 | }() 38 | return ch 39 | } 40 | _, err = a2.Filter(ad.Font.Filters...) 41 | if err != nil { 42 | ch := make(chan error) 43 | go func() { 44 | ch <- err 45 | }() 46 | return ch 47 | } 48 | ad.toStop = a2 49 | return a2.Play() 50 | } 51 | 52 | // Stop stops a font.Audio's playback 53 | func (ad *Audio) Stop() error { 54 | if ad.toStop != nil { 55 | return ad.toStop.Stop() 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /font/ceol/ceol.go: -------------------------------------------------------------------------------- 1 | // Package ceol provides functionality to handle .ceol files and .ceol encoded data (Bosca Ceoil files) 2 | package ceol 3 | 4 | import ( 5 | "io" 6 | "io/ioutil" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/200sc/klangsynthese/sequence" 12 | "github.com/200sc/klangsynthese/synth" 13 | ) 14 | 15 | // Raw Ceol types, holds all information in ceol file 16 | 17 | // Ceol represents a complete .ceol file 18 | type Ceol struct { 19 | Version int 20 | Swing int 21 | Effect int 22 | EffectValue int 23 | Bpm int 24 | PatternLength int 25 | BarLength int 26 | Instruments []Instrument 27 | Patterns []Pattern 28 | LoopStart int 29 | LoopEnd int 30 | Arrangement [][8]int 31 | } 32 | 33 | // Instrument represents a single entry in a .ceol's instrument block 34 | type Instrument struct { 35 | Index int 36 | IsDrumkit int 37 | Palette int 38 | LPFCutoff int 39 | LPFResonance int 40 | Volume int 41 | } 42 | 43 | // Pattern represents a single entry in a .ceol's pattern block 44 | type Pattern struct { 45 | Key int 46 | Scale int 47 | Instrument int 48 | Palette int 49 | Notes []Note 50 | Filters []Filter 51 | } 52 | 53 | // Note represents a single entry in a .ceol's pattern's note block 54 | type Note struct { 55 | PitchIndex int // C4 = 60 56 | Length int 57 | Offset int 58 | } 59 | 60 | // Filter represents a single entry in a .ceol's pattern's filter block 61 | type Filter struct { 62 | Volume int 63 | LPFCutoff int 64 | LPFResonance int 65 | } 66 | 67 | // ChordPattern converts a Ceol's patterns and arrangement into a playable chord 68 | // pattern for sequences 69 | func (c Ceol) ChordPattern() sequence.ChordPattern { 70 | chp := sequence.ChordPattern{} 71 | chp.Pitches = make([][]synth.Pitch, c.PatternLength*len(c.Arrangement)) 72 | chp.Holds = make([][]time.Duration, c.PatternLength*len(c.Arrangement)) 73 | for i, m := range c.Arrangement { 74 | for _, p := range m { 75 | if p != -1 { 76 | for _, n := range c.Patterns[p].Notes { 77 | chp.Pitches[n.Offset+i*c.PatternLength] = 78 | append(chp.Pitches[n.Offset+i*c.PatternLength], synth.NoteFromIndex(n.PitchIndex)) 79 | chp.Holds[n.Offset+i*c.PatternLength] = 80 | append(chp.Holds[n.Offset+i*c.PatternLength], DurationFromQuarters(c.Bpm, n.Length)) 81 | } 82 | } 83 | } 84 | } 85 | return chp 86 | } 87 | 88 | // DurationFromQuarters should not be here, should be in a package 89 | // managing bpm and time 90 | // Duration from quarters expects four quarters to occur per beat, 91 | // (direct complaints at terry cavanagh), and returns a time.Duration 92 | // for n quarters in the given bpm. 93 | func DurationFromQuarters(bpm, quarters int) time.Duration { 94 | beatTime := time.Duration(60000/bpm) * time.Millisecond 95 | quarterTime := beatTime / 4 96 | return quarterTime * time.Duration(quarters) 97 | } 98 | 99 | // Open returns a Ceol from an io.Reader 100 | func Open(r io.Reader) (Ceol, error) { 101 | c := Ceol{} 102 | b, err := ioutil.ReadAll(r) 103 | if err != nil { 104 | return c, err 105 | } 106 | s := string(b) 107 | in := strings.Split(s, ",") 108 | ints := make([]int, len(in)) 109 | for i := 0; i < len(in)-1; i++ { 110 | ints[i], err = strconv.Atoi(in[i]) 111 | if err != nil { 112 | return c, err 113 | } 114 | } 115 | i := 0 116 | c.Version = ints[i] 117 | i++ 118 | c.Swing = ints[i] 119 | i++ 120 | c.Effect = ints[i] 121 | i++ 122 | c.EffectValue = ints[i] 123 | i++ 124 | c.Bpm = ints[i] 125 | i++ 126 | c.PatternLength = ints[i] 127 | i++ 128 | c.BarLength = ints[i] 129 | i++ 130 | nInstruments := ints[i] 131 | i++ 132 | c.Instruments = make([]Instrument, nInstruments) 133 | for j := 0; j < nInstruments; j++ { 134 | c.Instruments[j].Index = ints[i] 135 | i++ 136 | c.Instruments[j].IsDrumkit = ints[i] 137 | i++ 138 | c.Instruments[j].Palette = ints[i] 139 | i++ 140 | c.Instruments[j].LPFCutoff = ints[i] 141 | i++ 142 | c.Instruments[j].LPFResonance = ints[i] 143 | i++ 144 | c.Instruments[j].Volume = ints[i] 145 | i++ 146 | } 147 | nPatterns := ints[i] 148 | i++ 149 | c.Patterns = make([]Pattern, nPatterns) 150 | for j := 0; j < nPatterns; j++ { 151 | c.Patterns[j].Key = ints[i] 152 | i++ 153 | c.Patterns[j].Scale = ints[i] 154 | i++ 155 | c.Patterns[j].Instrument = ints[i] 156 | i++ 157 | c.Patterns[j].Palette = ints[i] 158 | i++ 159 | nNotes := ints[i] 160 | i++ 161 | c.Patterns[j].Notes = make([]Note, nNotes) 162 | for k := 0; k < nNotes; k++ { 163 | c.Patterns[j].Notes[k].PitchIndex = ints[i] 164 | i++ 165 | c.Patterns[j].Notes[k].Length = ints[i] 166 | i++ 167 | c.Patterns[j].Notes[k].Offset = ints[i] 168 | i++ 169 | i++ // Dummy value here 170 | } 171 | hasFilter := ints[i] 172 | i++ 173 | var nFilters int 174 | if hasFilter == 1 { 175 | nFilters = ints[i] 176 | i++ 177 | } 178 | c.Patterns[j].Filters = make([]Filter, nFilters) 179 | for k := 0; k < nFilters; k++ { 180 | c.Patterns[j].Filters[k].Volume = ints[i] 181 | i++ 182 | c.Patterns[j].Filters[k].LPFCutoff = ints[i] 183 | i++ 184 | c.Patterns[j].Filters[k].LPFResonance = ints[i] 185 | i++ 186 | } 187 | } 188 | songLength := ints[i] 189 | i++ 190 | c.LoopStart = ints[i] 191 | i++ 192 | c.LoopEnd = ints[i] 193 | i++ 194 | c.Arrangement = make([][8]int, songLength) 195 | for j := 0; j < songLength; j++ { 196 | for k := 0; k < 8; k++ { 197 | c.Arrangement[j][k] = ints[i] 198 | i++ 199 | } 200 | } 201 | return c, nil 202 | } 203 | -------------------------------------------------------------------------------- /font/ceol/ceol_test.go: -------------------------------------------------------------------------------- 1 | package ceol 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/200sc/klangsynthese/sequence" 10 | "github.com/200sc/klangsynthese/synth" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestReadCeol(t *testing.T) { 15 | f, err := os.Open("test.ceol") 16 | assert.Nil(t, err) 17 | c, err := Open(f) 18 | assert.Nil(t, err) 19 | wg := sequence.NewWaveGenerator( 20 | sequence.Chords(c.ChordPattern()), 21 | sequence.Volumes(2000), 22 | sequence.Ticks(DurationFromQuarters(c.Bpm, 1)), 23 | sequence.Waves(synth.Int16.Saw), 24 | ) 25 | sq := wg.Generate() 26 | sq.Play() 27 | fmt.Println("Playing sequence") 28 | time.Sleep(5 * time.Second) 29 | sq.Stop() 30 | } 31 | -------------------------------------------------------------------------------- /font/ceol/test.ceol: -------------------------------------------------------------------------------- 1 | 3,0,0,0,150,32,4,2,134,0,1,128,0,256,141,0,20,128,0,256,3,10,2,0,1,26,77,1,0,0,73,1,1,0,70,1,2,0,63,1,4,0,60,1,5,0,84,1,0,0,81,1,1,0,77,1,2,0,73,1,3,0,70,1,4,0,67,1,5,0,63,1,6,0,60,1,7,0,57,1,6,0,53,1,7,0,67,1,3,0,82,1,0,0,77,1,1,0,72,1,2,0,70,1,3,0,69,1,4,0,63,1,5,0,58,1,6,0,57,1,7,0,34,32,16,0,41,32,16,0,0,0,0,1,20,10,70,1,0,0,69,1,0,0,66,1,2,0,64,1,2,0,59,1,4,0,58,1,4,0,57,1,4,0,54,1,6,0,49,1,9,0,48,1,9,0,0,0,0,0,1,0,0,2,0,2,0,-1,-1,-1,-1,-1,-1,-1,2,-1,-1,-1,-1,-1,-1,-1, -------------------------------------------------------------------------------- /font/dls/SanbikiSCC.dls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200sc/klangsynthese/a0e14a8c862b10650b09ce186bdf4798e6e90e22/font/dls/SanbikiSCC.dls -------------------------------------------------------------------------------- /font/dls/dls.go: -------------------------------------------------------------------------------- 1 | // Package dls contains data structures for DLS (.dls) file types 2 | package dls 3 | 4 | import "github.com/200sc/klangsynthese/font/riff" 5 | 6 | // The DLS is the major struct we care about in this package 7 | // DLS files contain instrument and wave sample information, and 8 | // a bunch of other things users probably don't care about. 9 | type DLS struct { 10 | Dlid ID `riff:"dlid"` 11 | Colh uint32 `riff:"colh"` 12 | Vers int64 `riff:"vers"` 13 | Lins []Ins `riff:"lins"` 14 | Ptbl []byte `riff:"ptbl"` //PoolTable 15 | Wvpl []Wave `riff:"wvpl"` 16 | riff.INFO `riff:"INFO"` 17 | } 18 | 19 | // PoolTable is a goofy name for a thing that redirects references 20 | // between instruments and waves, I think. 21 | type PoolTable struct { 22 | CbSize uint32 23 | CCues uint32 24 | // CCues size 25 | PoolCues []uint32 26 | } 27 | 28 | // An ID is a unique identifer for a dls file or instrument (or wave). 29 | // This could just be written as a complex128. 30 | type ID struct { 31 | UlData1 uint32 32 | UlData2 uint16 33 | UlData3 uint16 34 | AbData4 [8]byte 35 | } 36 | 37 | // Wave is the underlying struct you'd also find in WAV files. It stores raw 38 | // audio information and headers describing how to play that information. The 39 | // DLS Wave struct can also have a DLSID. Todo: Consider moving this out of this 40 | // file entirely and into the WAV package, the downside of which would be that 41 | // if a user wanted access to a DLSID it would no longer be there to get. 42 | type Wave struct { 43 | Dlid ID `riff:"dlid"` 44 | Guid []byte `riff:"guid"` 45 | Wavu []byte `riff:"wavu"` 46 | Fmt PCMFormat `riff:"fmt "` 47 | Wavh []byte `riff:"wavh"` 48 | Smpl []byte `riff:"smpl"` 49 | Wsmp []byte `riff:"wsmp"` 50 | // Data is the stuff you actually care about 51 | Data []byte `riff:"data"` 52 | riff.INFO `riff:"INFO"` 53 | } 54 | 55 | // PCMFormat is a wave format that just know how many bits per sample a wave 56 | // takes, beyond common format fields 57 | // Really, there are two formats a Wave can take, this is just the one we hope 58 | // to see. Todo: Fix that 59 | type PCMFormat struct { 60 | AudioFormat uint16 61 | NumChannels uint16 62 | SampleRate uint32 63 | ByteRate uint32 64 | BlockAlign uint16 65 | BitsPerSample uint16 66 | WhoKnows uint16 // Test files show there are buffer bytes here? 67 | } 68 | 69 | // An Ins holds instrument data 70 | type Ins struct { 71 | Dlid ID `riff:"dlid"` 72 | Insh InsHeader `riff:"insh"` 73 | Lrgn []Rgn `riff:"lrgn"` 74 | Lart Art `riff:"lart"` 75 | riff.INFO `riff:"INFO"` 76 | } 77 | 78 | // InsHeader stores header information for an instrument, notably the number 79 | // of regions and the internal instrument bank and number 80 | type InsHeader struct { 81 | CRegions uint32 82 | Locale MIDILOCALE 83 | } 84 | 85 | // MIDILOCALE stores two of the fields in an instrument header 86 | type MIDILOCALE struct { 87 | UlBank uint32 88 | UlInstrument uint32 89 | } 90 | 91 | // An Art is something we need to look more into 92 | type Art struct { 93 | // Todo: art1 doesn't fit our unmarshaler's expectations, because 94 | // it's basically its own type of subchunk with two sizes following 95 | // 'art1' then a number of structs based on the second size 96 | Art1 []byte `riff:"art1"` 97 | // This []byte is really: 98 | // cbSize uint32 99 | // cConnectionBlocks uint32 100 | // ConnectionBlocks []ConnectionBlock 101 | // Also Art2, which is equivalent to Art1 102 | } 103 | 104 | // Rgn is a region linking instruments to waves 105 | type Rgn struct { 106 | Rgnh RgnHeader `riff:"rgnh"` 107 | Wsmp []byte `riff:"wsmp"` 108 | Wlnk WaveLink `riff:"wlnk"` 109 | Lart Art `riff:"lart"` 110 | } 111 | 112 | // An RgnHeader stores header information for regions, notably for one the valid range 113 | // of notes an instrument should be applied to 114 | // Todo: figure out how to distinguish between rgnhs with and without ulLayer 115 | type RgnHeader struct { 116 | RangeKey RGNRANGE 117 | RangeVelocity RGNRANGE 118 | FusOptions uint16 119 | UsKeyGroup uint16 120 | UsLayer uint16 // This field is optional 121 | } 122 | 123 | // An RGNRANGE just stores a low and a high value 124 | type RGNRANGE struct { 125 | UsLow uint16 126 | UsHigh uint16 127 | } 128 | 129 | // WaveLink stores things I don't know about 130 | type WaveLink struct { 131 | FusOptions uint16 132 | UsPhaseGroup uint16 133 | UlChannel uint32 134 | UlTableIndex uint32 135 | } 136 | 137 | // WaveSample also stores things I don't know about 138 | type WaveSample struct { 139 | CbSize uint32 140 | UsUnityNote uint16 141 | SFineTune int16 142 | LGain int32 143 | FulOptions uint32 144 | // As for art, WaveSampleLoop is CSampleLoops long 145 | CSampleLoops uint32 146 | WaveSampleLoop []WaveSampleLoop 147 | } 148 | 149 | // WaveSampleLoop also stores things I don't know about 150 | type WaveSampleLoop struct { 151 | CbSize uint32 152 | UlLoopType uint32 153 | UlLoopStart uint32 154 | UlLoopLength uint32 155 | } 156 | -------------------------------------------------------------------------------- /font/dls/dls_test.go: -------------------------------------------------------------------------------- 1 | package dls 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/200sc/klangsynthese/audio" 11 | "github.com/200sc/klangsynthese/audio/filter" 12 | "github.com/200sc/klangsynthese/font/riff" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | // SanbikiSCC.dls is the original soundfont from LA-MULANA, and is 17 | // provided with Nigoro's consent. SanbikiSCC was written by 18 | // Samieru: https://twitter.com/samieru_nigoro 19 | 20 | func TestDLSPrint(t *testing.T) { 21 | fl, err := os.Open("SanbikiSCC.dls") 22 | assert.Nil(t, err) 23 | data, err := ioutil.ReadAll(fl) 24 | assert.Nil(t, err) 25 | // Todo: There should be a way to not have to readAll this 26 | r := riff.NewReader(data) 27 | r.Print() 28 | //fmt.Println(s) 29 | } 30 | 31 | func TestDLSUnmarshal(t *testing.T) { 32 | fl, err := os.Open("SanbikiSCC.dls") 33 | assert.Nil(t, err) 34 | dls := &DLS{} 35 | by, err := ioutil.ReadAll(fl) 36 | assert.Nil(t, err) 37 | err = riff.Unmarshal(by, dls) 38 | assert.Nil(t, err) 39 | afmt := audio.Format{44100, 1, 16} 40 | fmt.Println(len(dls.Lins)) 41 | fmt.Println("Version", dls.Vers) 42 | for _, ins := range dls.Lins { 43 | fmt.Println(ins.Insh) 44 | for _, rgn := range ins.Lrgn { 45 | fmt.Println(rgn) 46 | } 47 | } 48 | fmt.Println(dls.INFO) 49 | // for i, w := range dls.Wvpl { 50 | // fmt.Println(i, w.Fmt) 51 | // a, err := afmt.Wave(w.Data) 52 | // assert.Nil(t, err) 53 | // a.Play() 54 | // time.Sleep(a.PlayLength()) 55 | // } 56 | wv := dls.Wvpl[25] 57 | shft, err := filter.NewFFTShifter(32, 16) 58 | assert.Nil(t, err) 59 | fmt.Println(len(wv.Data)) 60 | a, _ := afmt.Wave(wv.Data) 61 | a.Play() 62 | time.Sleep(a.PlayLength() * 12) 63 | for i := .5; i <= 2.0; i += .5 { 64 | a2 := a.MustCopy().MustFilter(shft.PitchShift(i)) 65 | a2.Play() 66 | time.Sleep(a2.PlayLength() * 12) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /font/font.go: -------------------------------------------------------------------------------- 1 | // Package font provides utilities to package together audio manipulations as 2 | // a 'font' 3 | package font 4 | 5 | import "github.com/200sc/klangsynthese/audio" 6 | 7 | // Font represents some group of settings which modify how an Audio 8 | // should be played. The name is derived from the concept of a SoundFont 9 | type Font struct { 10 | Filters []audio.Filter 11 | } 12 | 13 | // New returns a *Font. 14 | // It is recommended for future API changes to avoid &Font{} and use NewFont instead 15 | func New() *Font { 16 | return &Font{} 17 | } 18 | 19 | // Filter on a font is applied to all audios as they are played. 20 | // Each call of Filter will completely reset a Font's filters 21 | func (f *Font) Filter(fs ...audio.Filter) *Font { 22 | f.Filters = fs 23 | return f 24 | } 25 | 26 | // Play on a font is equivalent to Audio.Copy().Filter(Font.GetFilters()).Play() 27 | func (f *Font) Play(a audio.Audio) <-chan error { 28 | a2, err := a.Copy() 29 | if err != nil { 30 | ch := make(chan error) 31 | go func() { 32 | ch <- err 33 | }() 34 | return ch 35 | } 36 | _, err = a2.Filter(f.Filters...) 37 | if err != nil { 38 | ch := make(chan error) 39 | go func() { 40 | ch <- err 41 | }() 42 | return ch 43 | } 44 | return a2.Play() 45 | } 46 | -------------------------------------------------------------------------------- /font/font_test.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/200sc/klangsynthese/audio" 8 | "github.com/200sc/klangsynthese/audio/filter" 9 | "github.com/200sc/klangsynthese/synth" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestFont1(t *testing.T) { 14 | f := New().Filter(filter.Volume(.25)) 15 | a, err := synth.Int16.Sin() 16 | assert.Nil(t, err) 17 | fa := NewAudio(f, a.(audio.FullAudio)) 18 | fa.Play() 19 | fa.Font.Filter(filter.Volume(.5)) 20 | time.Sleep(2 * time.Second) 21 | fa.Play() 22 | fa.Font.Filter(filter.Volume(.75)) 23 | time.Sleep(2 * time.Second) 24 | fa.Play() 25 | time.Sleep(2 * time.Second) 26 | } 27 | -------------------------------------------------------------------------------- /font/riff/info.go: -------------------------------------------------------------------------------- 1 | package riff 2 | 3 | // INFO is a common RIFF component. Most of these fields will be absent on 4 | // any given INFO struct. Todo: consider if these should be given names 5 | // that are informative instead of representative of their structural tag 6 | type INFO struct { 7 | // Arhcival Location 8 | IARL string `riff:"IARL"` 9 | // Arist 10 | IART string `riff:"IART"` 11 | // Commissioned By 12 | ICMS string `riff:"ICMS"` 13 | // Comments 14 | ICMT string `riff:"ICMT"` 15 | // Copyright 16 | ICOP string `riff:"ICOP"` 17 | // Creation Date 18 | ICRD string `riff:"ICRD"` 19 | // Engineer 20 | IENG string `riff:"IENG"` 21 | // Genre 22 | IGNR string `riff:"IGNR"` 23 | // Keywords 24 | IKEY string `riff:"IKEY"` 25 | // Medium 26 | IMED string `riff:"IMED"` 27 | // Name 28 | INAM string `riff:"INAM"` 29 | // Product 30 | IPRD string `riff:"IPRD"` 31 | // Subject 32 | ISBJ string `riff:"ISBJ"` 33 | // Software 34 | ISFT string `riff:"ISFT"` 35 | // Source 36 | ISRC string `riff:"ISRC"` 37 | // Source Form 38 | ISRF string `riff:"ISRF"` 39 | // Technician 40 | ITCH string `riff:"ITCH"` 41 | } 42 | -------------------------------------------------------------------------------- /font/riff/riff.go: -------------------------------------------------------------------------------- 1 | // Package riff reads and umarshalls RIFF files 2 | package riff 3 | 4 | import ( 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "reflect" 11 | "strconv" 12 | ) 13 | 14 | // A Reader is a bytes reader with some helper functions to read IDs, Lens, and Data 15 | // from RIFF files. 16 | type Reader struct { 17 | *bytes.Reader 18 | } 19 | 20 | // NewReader returns an initial Reader 21 | func NewReader(data []byte) *Reader { 22 | return &Reader{ 23 | Reader: bytes.NewReader(data), 24 | } 25 | } 26 | 27 | // Print prints a reader without any knowledge of the structure of the reader, 28 | // so all values will be []bytes. 29 | // It assumes the reader has not advanced at all. Todo: Change that 30 | func (r *Reader) Print() { 31 | deepPrint(r, " ", -1) 32 | } 33 | 34 | func deepPrint(r *Reader, prefix string, readLimit int) { 35 | var err error 36 | var typ string 37 | var l uint32 38 | var data []byte 39 | var isList bool 40 | var read int 41 | for err == nil && readLimit == -1 || read < readLimit { 42 | typ, l, isList, err = r.NextIDLen() 43 | // There will be a bogus byte at the end of some prints. 44 | if l%2 != 0 { 45 | l++ 46 | } 47 | read += 8 48 | if err == nil { 49 | fmt.Print(prefix, typ, " Length:", l) 50 | if isList { 51 | typ2, err2 := r.NextID() 52 | read += 4 53 | if err2 == nil { 54 | fmt.Println(prefix+" ", typ2) 55 | deepPrint(r, prefix+" ", int(l)-4) 56 | } else { 57 | fmt.Println(prefix, err2) 58 | } 59 | } else if l < 40 { 60 | data = make([]byte, l) 61 | r.Read(data) 62 | fmt.Println(" Content:", data) 63 | } else { 64 | r.Seek(int64(l), io.SeekCurrent) 65 | fmt.Println(" Long Content") 66 | } 67 | read += int(l) 68 | } 69 | } 70 | if err != nil && err != io.EOF { 71 | fmt.Println(prefix, err) 72 | } 73 | } 74 | 75 | // Unmarshal is a mirror of json.Unmarshal, for RIFF files 76 | func Unmarshal(data []byte, v interface{}) error { 77 | return NewReader(data).unmarshal(v) 78 | } 79 | 80 | func (r *Reader) unmarshal(v interface{}) error { 81 | // Mirrors json.unmarshal 82 | rv := reflect.ValueOf(v) 83 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 84 | return errors.New("Invalid Unmarshal Struct") 85 | } 86 | // The first ID in the riff should be RIFF 87 | id, err := r.NextID() 88 | if err != nil { 89 | return err 90 | } 91 | if id != "RIFF" { 92 | return errors.New("RIFF format must begin with RIFF") 93 | } 94 | ln, err := r.NextLen() 95 | if err != nil { 96 | return err 97 | } 98 | // The next ID identifies this file type. We don't want it. 99 | _, err = r.NextID() 100 | if err != nil { 101 | return err 102 | } 103 | _, err = r.chunks(reflect.Indirect(rv), int(ln)) 104 | return err 105 | } 106 | 107 | // NextID returns the next four byte sof the reader as a string 108 | func (r *Reader) NextID() (string, error) { 109 | id := make([]byte, 4) 110 | l, err := r.Reader.Read(id) 111 | if l != 4 || err != nil { 112 | return "", errors.New("RIFF missing expected ID") 113 | } 114 | return string(id), nil 115 | } 116 | 117 | // NextIDLen returns NextID and NextLen 118 | func (r *Reader) NextIDLen() (string, uint32, bool, error) { 119 | id, err := r.NextID() 120 | if err != nil { 121 | return "", 0, false, err 122 | } 123 | ln, err := r.NextLen() 124 | if err != nil { 125 | return "", 0, false, err 126 | } 127 | return id, ln, id == "LIST" || id == "RIFF", nil 128 | } 129 | 130 | // NextLen returns the next four bytes of the reader as a length. 131 | func (r *Reader) NextLen() (uint32, error) { 132 | var ln uint32 133 | err := binary.Read(r.Reader, binary.LittleEndian, &ln) 134 | if err != nil { 135 | return ln, errors.New("RIFF missing expected length") 136 | } 137 | return ln, nil 138 | } 139 | 140 | func (r *Reader) chunks(rv reflect.Value, inLength int) (reflect.Value, error) { 141 | // Find chunkId in rv 142 | // If it can't be found, ignore it as a value the user does not want 143 | switch rv.Kind() { 144 | case reflect.Struct: 145 | return rv, r.structChunks(rv, inLength) 146 | case reflect.Slice: 147 | return r.sliceChunks(rv, inLength) 148 | default: 149 | return reflect.Value{}, errors.New("Unsupported unmarshal type") 150 | } 151 | } 152 | 153 | func (r *Reader) sliceChunks(rv reflect.Value, inLength int) (reflect.Value, error) { 154 | 155 | slTy := rv.Type() 156 | ty := slTy.Elem() 157 | newSlice := reflect.MakeSlice(slTy, 0, 10000) 158 | for inLength > 0 { 159 | _, ln, isList, err := r.NextIDLen() 160 | if err != nil { 161 | return reflect.Value{}, err 162 | } 163 | if !isList { 164 | return reflect.Value{}, errors.New("Slice structs need to be LISTs") 165 | } 166 | ln -= 4 167 | inLength -= 4 168 | if inLength <= 0 { 169 | break 170 | } 171 | _, err = r.NextID() 172 | if err != nil { 173 | return reflect.Value{}, err 174 | } 175 | 176 | inLength -= 8 177 | if inLength <= 0 { 178 | break 179 | } 180 | newStruct := reflect.New(ty) 181 | err = r.structChunks(reflect.Indirect(newStruct), int(ln)) 182 | if err != nil { 183 | return reflect.Value{}, err 184 | } 185 | newSlice = reflect.Append(newSlice, reflect.Indirect(newStruct)) 186 | if ln%2 != 0 { 187 | r.Reader.ReadByte() 188 | inLength-- 189 | } 190 | inLength -= int(ln) 191 | } 192 | return newSlice, nil 193 | } 194 | 195 | // structChunks reads chunks and matches them to fields on rv (which is a struct) 196 | // structChunks sets the fields of rv to be the output it gets 197 | func (r *Reader) structChunks(rv reflect.Value, inLength int) error { 198 | chunkID, ln, isList, err := r.NextIDLen() 199 | if err != nil { 200 | return err 201 | } 202 | if isList { 203 | ln -= 4 204 | inLength -= 4 205 | chunkID, err = r.NextID() 206 | if err != nil { 207 | return err 208 | } 209 | } 210 | inLength -= 8 211 | ty := reflect.TypeOf(rv.Interface()) 212 | fields := make([]reflect.Value, rv.NumField()) 213 | fieldTags := make([]reflect.StructTag, rv.NumField()) 214 | for i := range fields { 215 | fields[i] = rv.Field(i) 216 | fieldTags[i] = ty.Field(i).Tag 217 | } 218 | i := 0 219 | for inLength > 0 { 220 | tag := fieldTags[i].Get("riff") 221 | //spew.Dump(fields[i]) 222 | if tag == chunkID { 223 | // get contents from recursive call 224 | var content reflect.Value 225 | if isList { 226 | content, err = r.chunks(fields[i], int(ln)) 227 | } else { 228 | content, err = r.fieldValue(fields[i], ln) 229 | } 230 | if err != nil { 231 | return err 232 | } 233 | inLength -= int(ln) 234 | 235 | fields[i].Set(content) 236 | // if length is odd read one more 237 | if ln%2 != 0 { 238 | r.Reader.ReadByte() 239 | inLength-- 240 | } 241 | if inLength <= 0 { 242 | return nil 243 | } 244 | // next id 245 | chunkID, ln, isList, err = r.NextIDLen() 246 | if err != nil { 247 | return err 248 | } 249 | if isList { 250 | ln -= 4 251 | inLength -= 4 252 | chunkID, err = r.NextID() 253 | if err != nil { 254 | return err 255 | } 256 | } 257 | inLength -= 8 258 | i = -1 259 | } 260 | if inLength <= 0 { 261 | return nil 262 | } 263 | i++ 264 | if i >= len(fields) { 265 | // Skip this id 266 | // if length is odd read one more 267 | if ln%2 != 0 { 268 | ln++ 269 | } 270 | _, err = r.Reader.Seek(int64(ln), io.SeekCurrent) 271 | if err != nil { 272 | return err 273 | } 274 | inLength -= int(ln) 275 | if inLength <= 0 { 276 | return nil 277 | } 278 | // next id 279 | chunkID, ln, isList, err = r.NextIDLen() 280 | if err != nil { 281 | return err 282 | } 283 | if isList { 284 | ln -= 4 285 | inLength -= 4 286 | chunkID, err = r.NextID() 287 | if err != nil { 288 | return err 289 | } 290 | } 291 | inLength -= 8 292 | i = 0 293 | } 294 | } 295 | return nil 296 | } 297 | 298 | // Todo: the switch here should change to some separate functions, there's some 299 | // repetition here that is not necessary. 300 | func (r *Reader) fieldValue(rv reflect.Value, ln uint32) (reflect.Value, error) { 301 | switch rv.Kind() { 302 | case reflect.Struct: 303 | st := rv.Addr().Interface() 304 | err := binary.Read(r.Reader, binary.LittleEndian, st) 305 | if err != nil { 306 | // Something on this struct has an undefined size 307 | // Read each field in part by part. 308 | } 309 | return reflect.Indirect(reflect.ValueOf(st)), err 310 | case reflect.String: 311 | data := make([]byte, ln) 312 | n, err := r.Reader.Read(data) 313 | if n != int(ln) { 314 | return reflect.Value{}, errors.New("Insufficient data found in RIFF data block") 315 | } 316 | return reflect.ValueOf(string(data)), err 317 | case reflect.Slice: 318 | switch rv.Type().Elem().Kind() { 319 | case reflect.Uint8: 320 | data := make([]byte, ln) 321 | n, err := r.Reader.Read(data) 322 | if n != int(ln) { 323 | return reflect.Value{}, errors.New("Insufficient data found in RIFF data block") 324 | } 325 | return reflect.ValueOf(data), err 326 | default: 327 | return reflect.Value{}, errors.New("Unsupported type in input struct") 328 | } 329 | case reflect.Uint32: 330 | if ln != 4 { 331 | return reflect.Value{}, errors.New("Invalid length for uint32: " + strconv.Itoa(int(ln))) 332 | } 333 | data := make([]byte, ln) 334 | n, err := r.Reader.Read(data) 335 | if n != int(ln) { 336 | return reflect.Value{}, errors.New("Insufficient data found in RIFF data block") 337 | } 338 | if err != nil { 339 | return reflect.Value{}, err 340 | } 341 | val, n := binary.Uvarint(data) 342 | if n <= 0 { 343 | return reflect.Value{}, errors.New("Unable to decode int64 from data") 344 | } 345 | val32 := uint32(val) 346 | return reflect.ValueOf(val32), nil 347 | case reflect.Int64: 348 | if ln != 8 { 349 | return reflect.Value{}, errors.New("Invalid length for int64: " + strconv.Itoa(int(ln))) 350 | } 351 | data := make([]byte, ln) 352 | n, err := r.Reader.Read(data) 353 | if n != int(ln) { 354 | return reflect.Value{}, errors.New("Insufficient data found in RIFF data block") 355 | } 356 | if err != nil { 357 | return reflect.Value{}, err 358 | } 359 | val, n := binary.Varint(data) 360 | if n <= 0 { 361 | return reflect.Value{}, errors.New("Unable to decode int64 from data") 362 | } 363 | return reflect.ValueOf(val), nil 364 | } 365 | return reflect.Value{}, nil 366 | } 367 | -------------------------------------------------------------------------------- /font/sf2/sf2_test.go: -------------------------------------------------------------------------------- 1 | package sf2 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/200sc/klangsynthese/font/riff" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestReadSf2(t *testing.T) { 13 | fl, err := os.Open("nolicenseforthis.sf2") 14 | assert.Nil(t, err) 15 | data, err := ioutil.ReadAll(fl) 16 | assert.Nil(t, err) 17 | // Todo: There should be a way to not have to readAll this 18 | r := riff.NewReader(data) 19 | r.Print() 20 | } 21 | 22 | type Data struct { 23 | riff.INFO 24 | Sdta 25 | Pdta 26 | } 27 | 28 | type VersionTag struct { 29 | Major, Minor uint16 30 | } 31 | 32 | type Sdta struct { 33 | Smpls [][]int16 34 | Sm24s [][]int8 35 | } 36 | 37 | type Pdta struct { 38 | Phdr []PresetHeader 39 | Pbag []PresetBag 40 | Pmod []ModList 41 | Pgen []GenList 42 | Inst []Inst 43 | Ibag []InstBag 44 | Imod []ModList 45 | Igen []GenList 46 | Shdr []Sample 47 | } 48 | 49 | type PresetHeader struct { 50 | AchPresetName [20]int8 51 | Preset, Bank, PresetBagNdx uint16 52 | Library, Genre, Morphology uint32 53 | } 54 | 55 | type PresetBag struct { 56 | GenNdx, ModNdx uint16 57 | } 58 | 59 | type ModList struct { 60 | ModSrcOper Modulator 61 | ModDestOper Generator 62 | ModAmount int16 63 | ModAmtSrcOper Modulator 64 | ModTransOper Transform 65 | } 66 | 67 | type GenList struct { 68 | GenOper Generator 69 | GenAmount uint16 70 | } 71 | 72 | type Inst struct { 73 | AchInstName [20]int8 74 | InstBagNdx uint16 75 | } 76 | 77 | type InstBag struct { 78 | InstGenNdx, InstModNdx uint16 79 | } 80 | 81 | type Sample struct { 82 | AchSampleName [20]byte 83 | Start, End, StartLoop, EndLoop, SampleRate uint32 84 | ByOriginalKey byte 85 | ChCorrection int8 86 | SampleLink uint16 87 | SampleType SampleLink 88 | } 89 | 90 | type Modulator uint16 91 | 92 | // Modulator enum 93 | const () 94 | 95 | type Generator uint16 96 | 97 | // Generator enum 98 | const () 99 | 100 | type Transform uint16 101 | 102 | // Transform enum 103 | const () 104 | 105 | type SampleLink uint16 106 | 107 | // SampleLink enum 108 | const ( 109 | MonoSample SampleLink = 1 110 | RightSample SampleLink = 2 111 | LeftSample SampleLink = 4 112 | LinkedSample SampleLink = 8 113 | RomMonoSample SampleLink = 0x8001 114 | RomRightSample SampleLink = 0x8002 115 | RomLeftSample SampleLink = 0x8004 116 | RomLinkedSample SampleLink = 0x8008 117 | ) 118 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | package klangsynthese 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/200sc/klangsynthese/audio" 8 | "github.com/200sc/klangsynthese/flac" 9 | "github.com/200sc/klangsynthese/mp3" 10 | "github.com/200sc/klangsynthese/wav" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // LoadFile will parse the file name given and redirect to the appropriate 15 | // subpackage depending on the ending. 16 | func LoadFile(filename string) (audio.Audio, error) { 17 | f, err := os.Open(filename) 18 | if err != nil { 19 | return nil, errors.Wrap(err, "Failed to load file") 20 | } 21 | switch strings.ToLower(filename[len(filename)-4:]) { 22 | case ".wav": 23 | return wav.Load(f) 24 | case "flac": 25 | return flac.Load(f) 26 | case ".mp3": 27 | return mp3.Load(f) 28 | default: 29 | return nil, errors.New("Unsupported file type") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /load_test.go: -------------------------------------------------------------------------------- 1 | package klangsynthese 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // These tests do not test that we can play the given audio, see the 10 | // respective directory for each file type for those tests. These 11 | // tests just confirm that we can load the files. 12 | 13 | func TestLoadMp3(t *testing.T) { 14 | _, err := LoadFile("mp3/test.mp3") 15 | assert.Nil(t, err) 16 | } 17 | 18 | func TestLoadWav(t *testing.T) { 19 | _, err := LoadFile("wav/test.wav") 20 | assert.Nil(t, err) 21 | } 22 | 23 | func TestLoadFlac(t *testing.T) { 24 | _, err := LoadFile("flac/test.flac") 25 | assert.Nil(t, err) 26 | } 27 | -------------------------------------------------------------------------------- /mp3/mp3.go: -------------------------------------------------------------------------------- 1 | // Package mp3 provides functionality to handle .mp3 files and .mp3 encoded data 2 | package mp3 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "io" 8 | 9 | "github.com/200sc/klangsynthese/audio" 10 | 11 | haj "github.com/hajimehoshi/go-mp3" 12 | ) 13 | 14 | // Load loads an mp3-encoded reader into an audio 15 | func Load(r io.ReadCloser) (audio.Audio, error) { 16 | d, err := haj.NewDecoder(r) 17 | if err != nil { 18 | return nil, err 19 | } 20 | buf := bytes.NewBuffer(make([]byte, 0, d.Length())) 21 | _, err = io.Copy(buf, d) 22 | if err != nil { 23 | return nil, err 24 | } 25 | mformat := audio.Format{ 26 | SampleRate: uint32(d.SampleRate()), 27 | Bits: 16, 28 | Channels: 2, 29 | } 30 | return audio.EncodeBytes( 31 | audio.Encoding{ 32 | Data: buf.Bytes(), 33 | Format: mformat, 34 | }) 35 | } 36 | 37 | // Save will eventually save an audio encoded as an MP3 to r 38 | func Save(r io.ReadWriter, a audio.Audio) error { 39 | return errors.New("Unsupported Functionality") 40 | } 41 | -------------------------------------------------------------------------------- /mp3/mp3_test.go: -------------------------------------------------------------------------------- 1 | package mp3 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestBasicMp3(t *testing.T) { 13 | fmt.Println("Running Basic Mp3") 14 | f, err := os.Open("nolicenseforthis_test.mp3") 15 | fmt.Println(f) 16 | assert.Nil(t, err) 17 | a, err := Load(f) 18 | assert.Nil(t, err) 19 | err = <-a.Play() 20 | assert.Nil(t, err) 21 | fmt.Println("Starting playing") 22 | time.Sleep(5 * time.Second) 23 | // In addition to the error tests here, this should play noise 24 | } 25 | -------------------------------------------------------------------------------- /sequence/chordPattern.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/200sc/klangsynthese/synth" 7 | ) 8 | 9 | // A ChordPattern represents the order of pitches and holds 10 | // for each of those pitches over a sequence of (potential) 11 | // chords. Todo: pitchPattern is a subset of this, should 12 | // it even exist? 13 | type ChordPattern struct { 14 | Pitches [][]synth.Pitch 15 | Holds [][]time.Duration 16 | } 17 | 18 | // HasChords lets generators be built from chord Options 19 | // if they have a pointer to a chord pattern 20 | type HasChords interface { 21 | GetChordPattern() *ChordPattern 22 | } 23 | 24 | // GetChordPattern returns a pointer to a generator's chord pattern 25 | func (cp *ChordPattern) GetChordPattern() *ChordPattern { 26 | return cp 27 | } 28 | 29 | // Chords sets the generator's chord pattern 30 | func Chords(cp ChordPattern) Option { 31 | return func(g Generator) { 32 | if hcp, ok := g.(HasChords); ok { 33 | *(hcp.GetChordPattern()) = cp 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sequence/generator.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | // A Generator stores settings to create a sequence 4 | type Generator interface { 5 | Generate() *Sequence 6 | } 7 | 8 | // Option types are inserted into Constructors to create generators 9 | type Option func(Generator) 10 | 11 | // And combines any number of options into a single option. 12 | // And is a reminder that you can store combined settings to avoid 13 | // having to rewrite them 14 | func And(opts ...Option) Option { 15 | return func(g Generator) { 16 | for _, opt := range opts { 17 | opt(g) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sequence/holdPattern.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import "time" 4 | 5 | // A HoldPattern is a pattern that might loop on itself for how long notes 6 | // should be held 7 | type HoldPattern []time.Duration 8 | 9 | // HasHolds enables generators to be built from HoldPattern and use the 10 | // related option functions 11 | type HasHolds interface { 12 | GetHoldPattern() *[]time.Duration 13 | } 14 | 15 | // GetHoldPattern lets composing HoldPattern satisfy HasHolds 16 | func (hp *HoldPattern) GetHoldPattern() *HoldPattern { 17 | return hp 18 | } 19 | 20 | // Holds sets the generator's Hold pattern 21 | func Holds(vs ...time.Duration) Option { 22 | return func(g Generator) { 23 | if hhs, ok := g.(HasHolds); ok { 24 | *hhs.GetHoldPattern() = vs 25 | } 26 | } 27 | } 28 | 29 | // HoldAt sets the n'th value in the entire play sequence 30 | // to be Hold p. This could involve duplicating a pattern 31 | // until it is long enough to reach n. Meaningless if the 32 | // Hold pattern has not been set yet. 33 | func HoldAt(t time.Duration, n int) Option { 34 | return func(g Generator) { 35 | if hhs, ok := g.(HasHolds); ok { 36 | if hl, ok := hhs.(HasLength); ok { 37 | if hl.GetLength() < n { 38 | hp := hhs.GetHoldPattern() 39 | Holds := *hp 40 | if len(Holds) == 0 { 41 | return 42 | } 43 | // If the pattern is not long enough, there are two things 44 | // we could do-- 1. Extend the pattern and replace the 45 | // individual note, or 2. Replace the note that would be 46 | // played at n and thus all earlier and later plays within 47 | // the pattern as well. 48 | // 49 | // This uses approach 1. 50 | for len(Holds) <= n { 51 | Holds = append(Holds, Holds...) 52 | } 53 | Holds[n] = t 54 | *hp = Holds 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | // HoldPatternAt sets the n'th value in the Hold pattern 62 | // to be Hold p. Meaningless if the Hold pattern has not 63 | // been set yet. 64 | func HoldPatternAt(t time.Duration, n int) Option { 65 | return func(g Generator) { 66 | if hhs, ok := g.(HasHolds); ok { 67 | hp := hhs.GetHoldPattern() 68 | Holds := *hp 69 | if len(Holds) <= n { 70 | return 71 | } 72 | Holds[n] = t 73 | *hp = Holds 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sequence/length.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | type Length int 4 | 5 | type HasLength interface { 6 | GetLength() int 7 | SetLength(int) 8 | } 9 | 10 | func (l *Length) GetLength() int { 11 | return int(*l) 12 | } 13 | 14 | func (l *Length) SetLength(i int) { 15 | *l = Length(i) 16 | } 17 | 18 | func PlayLength(i int) Option { 19 | return func(g Generator) { 20 | if l, ok := g.(HasLength); ok { 21 | l.SetLength(i) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sequence/loop.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | type Loop bool 4 | 5 | type HasLoops interface { 6 | GetLoop() bool 7 | SetLoop(bool) 8 | } 9 | 10 | func (l *Loop) GetLoop() bool { 11 | return bool(*l) 12 | } 13 | 14 | func (l *Loop) SetLoop(b bool) { 15 | *l = Loop(b) 16 | } 17 | 18 | // Loops sets the generator's Loop 19 | func Loops(b bool) Option { 20 | return func(g Generator) { 21 | if ht, ok := g.(HasLoops); ok { 22 | ht.SetLoop(b) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sequence/pitchPattern.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import "github.com/200sc/klangsynthese/synth" 4 | 5 | type PitchPattern []synth.Pitch 6 | 7 | type HasPitches interface { 8 | GetPitchPattern() []synth.Pitch 9 | SetPitchPattern([]synth.Pitch) 10 | } 11 | 12 | func (pp *PitchPattern) GetPitchPattern() []synth.Pitch { 13 | return *pp 14 | } 15 | 16 | func (pp *PitchPattern) SetPitchPattern(ps []synth.Pitch) { 17 | *pp = ps 18 | } 19 | 20 | // Pitches sets the generator's pitch pattern 21 | func Pitches(ps ...synth.Pitch) Option { 22 | return func(g Generator) { 23 | if hpp, ok := g.(HasPitches); ok { 24 | hpp.SetPitchPattern(ps) 25 | } 26 | } 27 | } 28 | 29 | // PitchAt sets the n'th value in the entire play sequence 30 | // to be pitch p. This could involve duplicating a pattern 31 | // until it is long enough to reach n. Meaningless if the 32 | // pitch pattern has not been set yet. 33 | func PitchAt(p synth.Pitch, n int) Option { 34 | return func(g Generator) { 35 | if hpp, ok := g.(HasPitches); ok { 36 | if hl, ok := hpp.(HasLength); ok { 37 | if hl.GetLength() < n { 38 | pitches := hpp.GetPitchPattern() 39 | if len(pitches) == 0 { 40 | return 41 | } 42 | // If the pattern is not long enough, there are two things 43 | // we could do-- 1. Extend the pattern and replace the 44 | // individual note, or 2. Replace the note that would be 45 | // played at n and thus all earlier and later plays within 46 | // the pattern as well. 47 | // 48 | // This uses approach 1. 49 | for len(pitches) < n { 50 | pitches = append(pitches, pitches...) 51 | } 52 | pitches[n] = p 53 | hpp.SetPitchPattern(pitches) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | // PitchPatternAt sets the n'th value in the pitch pattern 61 | // to be pitch p. Meaningless if the pitch pattern has not 62 | // been set yet. 63 | func PitchPatternAt(p synth.Pitch, n int) Option { 64 | return func(g Generator) { 65 | if hpp, ok := g.(HasPitches); ok { 66 | pitches := hpp.GetPitchPattern() 67 | if len(pitches) < n { 68 | return 69 | } 70 | pitches[n] = p 71 | hpp.SetPitchPattern(pitches) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /sequence/sequence.go: -------------------------------------------------------------------------------- 1 | // Package sequence provides generators and options for creating audio sequences 2 | package sequence 3 | 4 | import ( 5 | "errors" 6 | "time" 7 | 8 | "github.com/200sc/klangsynthese/audio" 9 | ) 10 | 11 | // A Sequence is a timed pattern of simultaneously played audios. 12 | type Sequence struct { 13 | // Sequences play patterns of audio 14 | // everything at Pattern[0] will be simultaneously Play()ed at 15 | // Sequence.Play() 16 | Pattern []*audio.Multi 17 | patternIndex int 18 | // Every tick, the next index in Pattern will be played by a Sequence 19 | // until the pattern is over. 20 | Ticker *time.Ticker 21 | // needed to copy Ticker 22 | // consider: replacing ticker with dynamic ticker 23 | tickDuration time.Duration 24 | stopCh chan error 25 | loop bool 26 | } 27 | 28 | // Play on a sequence plays the pattern encoded in the sequence until stopped 29 | func (s *Sequence) Play() <-chan error { 30 | ch := make(chan error) 31 | go func() { 32 | for { 33 | s.patternIndex = 0 34 | for s.patternIndex < len(s.Pattern) { 35 | s.Pattern[s.patternIndex].Play() 36 | select { 37 | case <-s.stopCh: 38 | s.stopCh <- s.Pattern[s.patternIndex].Stop() 39 | ch <- nil 40 | return 41 | case <-s.Ticker.C: 42 | } 43 | s.patternIndex++ 44 | } 45 | if !s.loop { 46 | ch <- nil 47 | return 48 | } 49 | } 50 | }() 51 | return ch 52 | } 53 | 54 | // Filter for a sequence does nothing yet 55 | func (s *Sequence) Filter(fs ...audio.Filter) (audio.Audio, error) { 56 | // Filter on a sequence just applies the filter to all audios.. 57 | // but it can't do that always, what if the filter is Loop? 58 | // this implies two kinds of filters? 59 | // this doesn't work because FIlter is not an interface 60 | // for _, f := range fs { 61 | // if _, ok := f.(audio.Loop); ok { 62 | // s.loop = true 63 | // } else if _, ok := f.(audio.NoLoop); ok { 64 | // s.loop = false 65 | // } else { 66 | // for _, col := range s.Pattern { 67 | // for _, a := range col { 68 | // a.Filter(f) 69 | // } 70 | // } 71 | // } 72 | // } 73 | return s, nil 74 | } 75 | 76 | // MustFilter acts as filter, but does not respect errors. 77 | func (s *Sequence) MustFilter(fs ...audio.Filter) audio.Audio { 78 | a, _ := s.Filter(fs...) 79 | return a 80 | } 81 | 82 | // Stop stops a sequence 83 | func (s *Sequence) Stop() error { 84 | s.stopCh <- nil 85 | return <-s.stopCh 86 | } 87 | 88 | // Copy copies a sequence 89 | func (s *Sequence) Copy() (audio.Audio, error) { 90 | var err error 91 | s2 := &Sequence{ 92 | Pattern: make([]*audio.Multi, len(s.Pattern)), 93 | Ticker: time.NewTicker(s.tickDuration), 94 | tickDuration: s.tickDuration, 95 | stopCh: make(chan error), 96 | loop: s.loop, 97 | } 98 | for i := range s2.Pattern { 99 | s2.Pattern[i] = new(audio.Multi) 100 | s2.Pattern[i].Audios = make([]audio.Audio, len(s.Pattern[i].Audios)) 101 | for j := range s2.Pattern[i].Audios { 102 | // This could make a sequence that reuses the same 103 | // audio use a lot more memory when copied-- a better route 104 | // would involve identifying all unique audios 105 | // and making a copy for each of those, but that 106 | // requires producing unique IDs for each audio 107 | // (which would probably be a hash of their encoding? 108 | // but that raises issues for audios that don't want 109 | // to follow real encoding rules (like this one!)) 110 | s2.Pattern[i].Audios[j], err = s.Pattern[i].Audios[j].Copy() 111 | if err != nil { 112 | return nil, err 113 | } 114 | } 115 | } 116 | return s2, nil 117 | } 118 | 119 | // MustCopy acts as copy but panics on errors 120 | func (s *Sequence) MustCopy() audio.Audio { 121 | a, err := s.Copy() 122 | if err != nil { 123 | panic(err) 124 | } 125 | return a 126 | } 127 | 128 | // PlayLength returns how long this sequence will play before looping or stopping. 129 | // This does not include how long the last note is held beyond the tick duration 130 | func (s *Sequence) PlayLength() time.Duration { 131 | return time.Duration(len(s.Pattern)) * s.tickDuration 132 | } 133 | 134 | // Mix combines two sequences 135 | func (s *Sequence) Mix(s2 *Sequence) (*Sequence, error) { 136 | // Todo: we should be able to combine not-too-disparate 137 | // sequences like one that ticks on .5 seconds and one that ticks 138 | // on .25 seconds 139 | if s.tickDuration != s2.tickDuration { 140 | return nil, errors.New("Incompatible sequences") 141 | } 142 | seq, err := s.Copy() 143 | if err != nil { 144 | return nil, err 145 | } 146 | s3 := seq.(*Sequence) 147 | for i, col := range s2.Pattern { 148 | s3.Pattern[i].Audios = append(s3.Pattern[i].Audios, col.Audios...) 149 | } 150 | return s3, nil 151 | } 152 | 153 | // Append creates a sequence by combining two sequences in order 154 | func (s *Sequence) Append(s2 *Sequence) (*Sequence, error) { 155 | // Todo: we should be able to combine not-too-disparate 156 | // sequences like one that ticks on .5 seconds and one that ticks 157 | // on .25 seconds 158 | if s.tickDuration != s2.tickDuration { 159 | return nil, errors.New("Incompatible sequences") 160 | } 161 | seq, err := s.Copy() 162 | if err != nil { 163 | return nil, err 164 | } 165 | s3 := seq.(*Sequence) 166 | s3.Pattern = append(s3.Pattern, s2.Pattern...) 167 | return s3, nil 168 | } 169 | -------------------------------------------------------------------------------- /sequence/tick.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import "time" 4 | 5 | type Tick time.Duration 6 | 7 | type HasTicks interface { 8 | GetTick() time.Duration 9 | SetTick(time.Duration) 10 | } 11 | 12 | func (vp *Tick) GetTick() time.Duration { 13 | return time.Duration(*vp) 14 | } 15 | 16 | func (vp *Tick) SetTick(vs time.Duration) { 17 | *vp = Tick(vs) 18 | } 19 | 20 | // Ticks sets the generator's Tick 21 | func Ticks(t time.Duration) Option { 22 | return func(g Generator) { 23 | if ht, ok := g.(HasTicks); ok { 24 | ht.SetTick(t) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sequence/volumePattern.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | type VolumePattern []float64 4 | 5 | type HasVolumes interface { 6 | GetVolumePattern() []float64 7 | SetVolumePattern([]float64) 8 | } 9 | 10 | func (vp *VolumePattern) GetVolumePattern() []float64 { 11 | return *vp 12 | } 13 | 14 | func (vp *VolumePattern) SetVolumePattern(vs []float64) { 15 | *vp = vs 16 | } 17 | 18 | // Volumes sets the generator's Volume pattern 19 | func Volumes(vs ...float64) Option { 20 | return func(g Generator) { 21 | if hvs, ok := g.(HasVolumes); ok { 22 | hvs.SetVolumePattern(vs) 23 | } 24 | } 25 | } 26 | 27 | // VolumeAt sets the n'th value in the entire play sequence 28 | // to be Volume p. This could involve duplicating a pattern 29 | // until it is long enough to reach n. Meaningless if the 30 | // Volume pattern has not been set yet. 31 | func VolumeAt(v float64, n int) Option { 32 | return func(g Generator) { 33 | if hvs, ok := g.(HasVolumes); ok { 34 | if hl, ok := hvs.(HasLength); ok { 35 | if hl.GetLength() < n { 36 | volumes := hvs.GetVolumePattern() 37 | if len(volumes) == 0 { 38 | return 39 | } 40 | // If the pattern is not long enough, there are two things 41 | // we could do-- 1. Extend the pattern and replace the 42 | // individual note, or 2. Replace the note that would be 43 | // played at n and thus all earlier and later plays within 44 | // the pattern as well. 45 | // 46 | // This uses approach 1. 47 | for len(volumes) < n { 48 | volumes = append(volumes, volumes...) 49 | } 50 | volumes[n] = v 51 | hvs.SetVolumePattern(volumes) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | // VolumePatternAt sets the n'th value in the Volume pattern 59 | // to be Volume p. Meaningless if the Volume pattern has not 60 | // been set yet. 61 | func VolumePatternAt(v float64, n int) Option { 62 | return func(g Generator) { 63 | if hvs, ok := g.(HasVolumes); ok { 64 | volumes := hvs.GetVolumePattern() 65 | if len(volumes) < n { 66 | return 67 | } 68 | volumes[n] = v 69 | hvs.SetVolumePattern(volumes) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /sequence/waveFunction.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import "github.com/200sc/klangsynthese/synth" 4 | 5 | type WavePattern []synth.Wave 6 | 7 | type HasWaves interface { 8 | GetWavePattern() []synth.Wave 9 | SetWavePattern([]synth.Wave) 10 | } 11 | 12 | func (wp *WavePattern) GetWavePattern() []synth.Wave { 13 | return *wp 14 | } 15 | 16 | func (wp *WavePattern) SetWavePattern(ws []synth.Wave) { 17 | *wp = ws 18 | } 19 | 20 | // Waves sets the generator's Wave pattern 21 | func Waves(ws ...synth.Wave) Option { 22 | return func(g Generator) { 23 | if hw, ok := g.(HasWaves); ok { 24 | hw.SetWavePattern(ws) 25 | } 26 | } 27 | } 28 | 29 | // WaveAt sets the n'th value in the entire play sequence 30 | // to be Wave p. This could involve duplicating a pattern 31 | // until it is long enough to reach n. Meaningless if the 32 | // Wave pattern has not been set yet. 33 | func WaveAt(w synth.Wave, n int) Option { 34 | return func(g Generator) { 35 | if hw, ok := g.(HasWaves); ok { 36 | if hl, ok := hw.(HasLength); ok { 37 | if hl.GetLength() < n { 38 | Waves := hw.GetWavePattern() 39 | if len(Waves) == 0 { 40 | return 41 | } 42 | // If the pattern is not long enough, there are two things 43 | // we could do-- 1. Extend the pattern and replace the 44 | // individual note, or 2. Replace the note that would be 45 | // played at n and thus all earlier and later plays within 46 | // the pattern as well. 47 | // 48 | // This uses approach 1. 49 | for len(Waves) < n { 50 | Waves = append(Waves, Waves...) 51 | } 52 | Waves[n] = w 53 | hw.SetWavePattern(Waves) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | // WavePatternAt sets the n'th value in the Wave pattern 61 | // to be Wave p. Meaningless if the Wave pattern has not 62 | // been set yet. 63 | func WavePatternAt(w synth.Wave, n int) Option { 64 | return func(g Generator) { 65 | if hw, ok := g.(HasWaves); ok { 66 | Waves := hw.GetWavePattern() 67 | if len(Waves) < n { 68 | return 69 | } 70 | Waves[n] = w 71 | hw.SetWavePattern(Waves) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /sequence/waveGenerator.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/200sc/klangsynthese/audio" 7 | "github.com/200sc/klangsynthese/synth" 8 | ) 9 | 10 | // A WaveGenerator composes sets of simple waveforms as a sequence 11 | type WaveGenerator struct { 12 | ChordPattern 13 | PitchPattern 14 | WavePattern 15 | VolumePattern 16 | HoldPattern 17 | Length 18 | Tick 19 | Loop 20 | } 21 | 22 | // NewWaveGenerator uses optional variadic syntax to enable 23 | // any variant of a generator to be made 24 | func NewWaveGenerator(opts ...Option) *WaveGenerator { 25 | wg := &WaveGenerator{} 26 | for _, opt := range opts { 27 | opt(wg) 28 | } 29 | return wg 30 | } 31 | 32 | // Generate generates a sequence from this wave generator 33 | func (wg *WaveGenerator) Generate() *Sequence { 34 | sq := &Sequence{} 35 | sq.Ticker = time.NewTicker(time.Duration(wg.Tick)) 36 | sq.tickDuration = time.Duration(wg.Tick) 37 | sq.loop = bool(wg.Loop) 38 | sq.stopCh = make(chan error) 39 | if wg.Length == 0 { 40 | if len(wg.PitchPattern) != 0 { 41 | wg.Length = Length(len(wg.PitchPattern)) 42 | } else if len(wg.ChordPattern.Pitches) != 0 { 43 | wg.Length = Length(len(wg.ChordPattern.Pitches)) 44 | } 45 | // else whoops, there's no length 46 | } 47 | if len(wg.HoldPattern) == 0 { 48 | wg.HoldPattern = []time.Duration{sq.tickDuration} 49 | } 50 | sq.Pattern = make([]*audio.Multi, wg.Length) 51 | 52 | volumeIndex := 0 53 | waveIndex := 0 54 | if len(wg.PitchPattern) != 0 { 55 | pitchIndex := 0 56 | holdIndex := 0 57 | for i := range sq.Pattern { 58 | p := wg.PitchPattern[pitchIndex] 59 | if p != synth.Rest { 60 | a, _ := wg.WavePattern[waveIndex]( 61 | synth.AtPitch(p), 62 | synth.Duration(wg.HoldPattern[holdIndex]), 63 | synth.Volume(wg.VolumePattern[volumeIndex]), 64 | ) 65 | sq.Pattern[i] = audio.NewMulti(a) 66 | } else { 67 | sq.Pattern[i] = audio.NewMulti() 68 | } 69 | pitchIndex = (pitchIndex + 1) % len(wg.PitchPattern) 70 | volumeIndex = (volumeIndex + 1) % len(wg.VolumePattern) 71 | waveIndex = (waveIndex + 1) % len(wg.WavePattern) 72 | holdIndex = (holdIndex + 1) % len(wg.HoldPattern) 73 | } 74 | } else if len(wg.ChordPattern.Pitches) != 0 { 75 | chordIndex := 0 76 | for i := range sq.Pattern { 77 | mult := audio.NewMulti() 78 | for j, p := range wg.ChordPattern.Pitches[chordIndex] { 79 | a, _ := wg.WavePattern[waveIndex]( 80 | synth.AtPitch(p), 81 | synth.Duration(wg.ChordPattern.Holds[chordIndex][j]), 82 | synth.Volume(wg.VolumePattern[volumeIndex]), 83 | ) 84 | mult.Audios = append(mult.Audios, a) 85 | } 86 | sq.Pattern[i] = mult 87 | waveIndex = (waveIndex + 1) % len(wg.WavePattern) 88 | volumeIndex = (volumeIndex + 1) % len(wg.VolumePattern) 89 | chordIndex = (chordIndex + 1) % len(wg.ChordPattern.Pitches) 90 | } 91 | } 92 | return sq 93 | } 94 | -------------------------------------------------------------------------------- /sequence/waveGenerator_test.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/200sc/klangsynthese/synth" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestWaveGenerator(t *testing.T) { 13 | wg := NewWaveGenerator( 14 | Pitches( 15 | synth.A4, 16 | synth.A5, 17 | synth.A6, 18 | synth.G6, 19 | synth.Rest, 20 | synth.G4, 21 | ), 22 | Volumes(2000), 23 | Holds(time.Millisecond*150), 24 | HoldAt(time.Millisecond*400, 3), 25 | Ticks(time.Millisecond*200), 26 | Waves(synth.Int16.Sin, synth.Int16.Saw), 27 | Loops(true), 28 | PlayLength(7), 29 | ) 30 | sq := wg.Generate() 31 | sq.Play() 32 | fmt.Println("Playing sequence") 33 | time.Sleep(5 * time.Second) 34 | sq.Stop() 35 | } 36 | 37 | func TestMixSeq(t *testing.T) { 38 | loopsAndTicks := And( 39 | Ticks(time.Millisecond*200), 40 | Loops(true), 41 | ) 42 | wg := NewWaveGenerator( 43 | Pitches( 44 | synth.A4, 45 | synth.A5, 46 | synth.A6, 47 | synth.G6, 48 | synth.Rest, 49 | synth.G4, 50 | ), 51 | Volumes( 52 | 2000, 53 | 2500, 54 | 3000, 55 | ), 56 | Waves(synth.Int16.Sin), 57 | loopsAndTicks, 58 | ) 59 | sq := wg.Generate() 60 | wg = NewWaveGenerator( 61 | Pitches( 62 | synth.C4, 63 | synth.C5, 64 | synth.C6, 65 | synth.C6, 66 | synth.C5, 67 | synth.C4, 68 | ), 69 | Volumes(500), 70 | Waves( 71 | synth.Int16.Square, 72 | synth.Int16.Square, 73 | synth.Int16.Sin, 74 | synth.Int16.Saw, 75 | ), 76 | loopsAndTicks, 77 | ) 78 | sq2 := wg.Generate() 79 | sq3, err := sq.Mix(sq2) 80 | assert.Nil(t, err) 81 | sq3.Play() 82 | fmt.Println("Playing sequence") 83 | time.Sleep(5 * time.Second) 84 | assert.Nil(t, sq3.Stop()) 85 | } 86 | 87 | func TestAppendSeq(t *testing.T) { 88 | wg := NewWaveGenerator( 89 | Pitches( 90 | synth.A4, 91 | synth.A5, 92 | synth.A6, 93 | synth.G6, 94 | synth.Rest, 95 | synth.G4, 96 | ), 97 | Volumes( 98 | 2000, 99 | 2500, 100 | 3000, 101 | ), 102 | Ticks(time.Millisecond*200), 103 | Waves(synth.Int16.Sin), 104 | Loops(true), 105 | ) 106 | sq := wg.Generate() 107 | wg = NewWaveGenerator( 108 | Pitches( 109 | synth.C4, 110 | synth.C5, 111 | synth.C6, 112 | synth.C6, 113 | synth.C5, 114 | synth.C4, 115 | ), 116 | Volumes(500), 117 | Ticks(time.Millisecond*200), 118 | Waves( 119 | synth.Int16.Square, 120 | synth.Int16.Square, 121 | synth.Int16.Sin, 122 | synth.Int16.Saw, 123 | ), 124 | Loops(true), 125 | ) 126 | sq2 := wg.Generate() 127 | sq3, err := sq2.Append(sq) 128 | assert.Nil(t, err) 129 | sq3.Play() 130 | fmt.Println("Playing sequence") 131 | time.Sleep(5 * time.Second) 132 | assert.Nil(t, sq3.Stop()) 133 | } 134 | -------------------------------------------------------------------------------- /synth/option.go: -------------------------------------------------------------------------------- 1 | package synth 2 | 3 | import "time" 4 | 5 | // Option types modify waveform sources before they generate a waveform 6 | type Option func(Source) Source 7 | 8 | // Duration sets the duration of a generated waveform 9 | func Duration(t time.Duration) Option { 10 | return func(s Source) Source { 11 | s.Seconds = t.Seconds() 12 | return s 13 | } 14 | } 15 | 16 | // Volume sets the volume of a generated waveform. It guarantees that 0 <= v <= 1 17 | // (silent <= v <= max volume) 18 | func Volume(v float64) Option { 19 | return func(s Source) Source { 20 | if v > 1.0 { 21 | v = 1.0 22 | } else if v < 0 { 23 | v = 0 24 | } 25 | s.Volume = v 26 | return s 27 | } 28 | } 29 | 30 | // AtPitch sets the pitch of a generated waveform. 31 | func AtPitch(p Pitch) Option { 32 | return func(s Source) Source { 33 | s.Pitch = p 34 | return s 35 | } 36 | } 37 | 38 | // Mono sets the format to play mono audio. 39 | func Mono() Option { 40 | return func(s Source) Source { 41 | s.Channels = 1 42 | return s 43 | } 44 | } 45 | 46 | // Stereo sets the format to play stereo audio. 47 | func Stereo() Option { 48 | return func(s Source) Source { 49 | s.Channels = 2 50 | return s 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /synth/pitch.go: -------------------------------------------------------------------------------- 1 | package synth 2 | 3 | // A Pitch is a helper type for synth functions so 4 | // a user can write A4 instead of a frequency value 5 | // for a desired tone 6 | type Pitch uint16 7 | 8 | // Pitch frequencies 9 | // Values taken from http://peabody.sapp.org/class/st2/lab/notehz/ 10 | const ( 11 | Rest Pitch = 0 12 | C0 Pitch = 16 13 | C0s Pitch = 17 14 | D0b Pitch = 17 15 | D0 Pitch = 18 16 | D0s Pitch = 20 17 | E0b Pitch = 20 18 | E0 Pitch = 21 19 | F0 Pitch = 22 20 | F0s Pitch = 23 21 | G0b Pitch = 23 22 | G0 Pitch = 25 23 | G0s Pitch = 26 24 | A0b Pitch = 26 25 | A0 Pitch = 28 26 | A0s Pitch = 29 27 | B0b Pitch = 29 28 | B0 Pitch = 31 29 | C1 Pitch = 33 30 | C1s Pitch = 35 31 | D1b Pitch = 35 32 | D1 Pitch = 37 33 | D1s Pitch = 39 34 | E1b Pitch = 39 35 | E1 Pitch = 41 36 | F1 Pitch = 44 37 | F1s Pitch = 46 38 | G1b Pitch = 46 39 | G1 Pitch = 49 40 | G1s Pitch = 52 41 | A1b Pitch = 52 42 | A1 Pitch = 55 43 | A1s Pitch = 58 44 | B1b Pitch = 58 45 | B1 Pitch = 62 46 | C2 Pitch = 65 47 | C2s Pitch = 69 48 | D2b Pitch = 69 49 | D2 Pitch = 73 50 | D2s Pitch = 78 51 | E2b Pitch = 78 52 | E2 Pitch = 82 53 | F2 Pitch = 87 54 | F2s Pitch = 93 55 | G2b Pitch = 93 56 | G2 Pitch = 98 57 | G2s Pitch = 104 58 | A2b Pitch = 104 59 | A2 Pitch = 110 60 | A2s Pitch = 117 61 | B2b Pitch = 117 62 | B2 Pitch = 124 63 | C3 Pitch = 131 64 | C3s Pitch = 139 65 | D3b Pitch = 139 66 | D3 Pitch = 147 67 | D3s Pitch = 156 68 | E3b Pitch = 156 69 | E3 Pitch = 165 70 | F3 Pitch = 175 71 | F3s Pitch = 185 72 | G3b Pitch = 185 73 | G3 Pitch = 196 74 | G3s Pitch = 208 75 | A3b Pitch = 208 76 | A3 Pitch = 220 77 | A3s Pitch = 233 78 | B3b Pitch = 233 79 | B3 Pitch = 247 80 | C4 Pitch = 262 81 | C4s Pitch = 278 82 | D4b Pitch = 278 83 | D4 Pitch = 294 84 | D4s Pitch = 311 85 | E4b Pitch = 311 86 | E4 Pitch = 330 87 | F4 Pitch = 349 88 | F4s Pitch = 370 89 | G4b Pitch = 370 90 | G4 Pitch = 392 91 | G4s Pitch = 415 92 | A4b Pitch = 415 93 | A4 Pitch = 440 94 | A4s Pitch = 466 95 | B4b Pitch = 466 96 | B4 Pitch = 494 97 | C5 Pitch = 523 98 | C5s Pitch = 554 99 | D5b Pitch = 554 100 | D5 Pitch = 587 101 | D5s Pitch = 622 102 | E5b Pitch = 622 103 | E5 Pitch = 659 104 | F5 Pitch = 699 105 | F5s Pitch = 740 106 | G5b Pitch = 740 107 | G5 Pitch = 784 108 | G5s Pitch = 831 109 | A5b Pitch = 831 110 | A5 Pitch = 880 111 | A5s Pitch = 932 112 | B5b Pitch = 932 113 | B5 Pitch = 988 114 | C6 Pitch = 1047 115 | C6s Pitch = 1109 116 | D6b Pitch = 1109 117 | D6 Pitch = 1175 118 | D6s Pitch = 1245 119 | E6b Pitch = 1245 120 | E6 Pitch = 1319 121 | F6 Pitch = 1397 122 | F6s Pitch = 1475 123 | G6b Pitch = 1475 124 | G6 Pitch = 1568 125 | G6s Pitch = 1661 126 | A6b Pitch = 1661 127 | A6 Pitch = 1760 128 | A6s Pitch = 1865 129 | B6b Pitch = 1865 130 | B6 Pitch = 1976 131 | C7 Pitch = 2093 132 | C7s Pitch = 2218 133 | D7b Pitch = 2218 134 | D7 Pitch = 2349 135 | D7s Pitch = 2489 136 | E7b Pitch = 2489 137 | E7 Pitch = 2637 138 | F7 Pitch = 2794 139 | F7s Pitch = 2960 140 | G7b Pitch = 2960 141 | G7 Pitch = 3136 142 | G7s Pitch = 3322 143 | A7b Pitch = 3322 144 | A7 Pitch = 3520 145 | A7s Pitch = 3729 146 | B7b Pitch = 3729 147 | B7 Pitch = 3951 148 | C8 Pitch = 4186 149 | C8s Pitch = 4435 150 | D8b Pitch = 4435 151 | D8 Pitch = 4699 152 | D8s Pitch = 4978 153 | E8b Pitch = 4978 154 | E8 Pitch = 5274 155 | F8 Pitch = 5588 156 | F8s Pitch = 5920 157 | G8b Pitch = 5920 158 | G8 Pitch = 6272 159 | G8s Pitch = 6645 160 | A8b Pitch = 6645 161 | A8 Pitch = 7040 162 | A8s Pitch = 7459 163 | B8b Pitch = 7459 164 | B8 Pitch = 7902 165 | ) 166 | 167 | var ( 168 | allPitches = []Pitch{ 169 | C0, 170 | C0s, 171 | D0, 172 | D0s, 173 | E0, 174 | F0, 175 | F0s, 176 | G0, 177 | G0s, 178 | A0, 179 | A0s, 180 | B0, 181 | C1, 182 | C1s, 183 | D1, 184 | D1s, 185 | E1, 186 | F1, 187 | F1s, 188 | G1, 189 | G1s, 190 | A1, 191 | A1s, 192 | B1, 193 | C2, 194 | C2s, 195 | D2, 196 | D2s, 197 | E2, 198 | F2, 199 | F2s, 200 | G2, 201 | G2s, 202 | A2, 203 | A2s, 204 | B2, 205 | C3, 206 | C3s, 207 | D3, 208 | D3s, 209 | E3, 210 | F3, 211 | F3s, 212 | G3, 213 | G3s, 214 | A3, 215 | A3s, 216 | B3, 217 | C4, 218 | C4s, 219 | D4, 220 | D4s, 221 | E4, 222 | F4, 223 | F4s, 224 | G4, 225 | G4s, 226 | A4, 227 | A4s, 228 | B4, 229 | C5, 230 | C5s, 231 | D5, 232 | D5s, 233 | E5, 234 | F5, 235 | F5s, 236 | G5, 237 | G5s, 238 | A5, 239 | A5s, 240 | B5, 241 | C6, 242 | C6s, 243 | D6, 244 | D6s, 245 | E6, 246 | F6, 247 | F6s, 248 | G6, 249 | G6s, 250 | A6, 251 | A6s, 252 | B6, 253 | C7, 254 | C7s, 255 | D7, 256 | D7s, 257 | E7, 258 | F7, 259 | F7s, 260 | G7, 261 | G7s, 262 | A7, 263 | A7s, 264 | B7, 265 | C8, 266 | C8s, 267 | D8, 268 | D8s, 269 | E8, 270 | F8, 271 | F8s, 272 | G8, 273 | G8s, 274 | A8, 275 | A8s, 276 | B8, 277 | } 278 | 279 | // Reverse lookup for allPitches 280 | noteIndices = map[Pitch]int{ 281 | C0: 0, 282 | C0s: 1, 283 | D0: 2, 284 | D0s: 3, 285 | E0: 4, 286 | F0: 5, 287 | F0s: 6, 288 | G0: 7, 289 | G0s: 8, 290 | A0: 9, 291 | A0s: 10, 292 | B0: 11, 293 | C1: 12, 294 | C1s: 13, 295 | D1: 14, 296 | D1s: 15, 297 | E1: 16, 298 | F1: 17, 299 | F1s: 18, 300 | G1: 19, 301 | G1s: 20, 302 | A1: 21, 303 | A1s: 22, 304 | B1: 23, 305 | C2: 24, 306 | C2s: 25, 307 | D2: 26, 308 | D2s: 27, 309 | E2: 28, 310 | F2: 29, 311 | F2s: 30, 312 | G2: 31, 313 | G2s: 32, 314 | A2: 33, 315 | A2s: 34, 316 | B2: 35, 317 | C3: 36, 318 | C3s: 37, 319 | D3: 38, 320 | D3s: 39, 321 | E3: 40, 322 | F3: 41, 323 | F3s: 42, 324 | G3: 43, 325 | G3s: 44, 326 | A3: 45, 327 | A3s: 46, 328 | B3: 47, 329 | C4: 48, 330 | C4s: 49, 331 | D4: 50, 332 | D4s: 51, 333 | E4: 52, 334 | F4: 53, 335 | F4s: 54, 336 | G4: 55, 337 | G4s: 56, 338 | A4: 57, 339 | A4s: 58, 340 | B4: 59, 341 | C5: 60, 342 | C5s: 61, 343 | D5: 62, 344 | D5s: 63, 345 | E5: 64, 346 | F5: 65, 347 | F5s: 66, 348 | G5: 67, 349 | G5s: 68, 350 | A5: 69, 351 | A5s: 70, 352 | B5: 71, 353 | C6: 72, 354 | C6s: 73, 355 | D6: 74, 356 | D6s: 75, 357 | E6: 76, 358 | F6: 77, 359 | F6s: 78, 360 | G6: 79, 361 | G6s: 80, 362 | A6: 81, 363 | A6s: 82, 364 | B6: 83, 365 | C7: 84, 366 | C7s: 85, 367 | D7: 86, 368 | D7s: 87, 369 | E7: 88, 370 | F7: 89, 371 | F7s: 90, 372 | G7: 91, 373 | G7s: 92, 374 | A7: 93, 375 | A7s: 94, 376 | B7: 95, 377 | C8: 96, 378 | C8s: 97, 379 | D8: 98, 380 | D8s: 99, 381 | E8: 100, 382 | F8: 101, 383 | F8s: 102, 384 | G8: 103, 385 | G8s: 104, 386 | A8: 105, 387 | A8s: 106, 388 | B8: 107, 389 | } 390 | ) 391 | 392 | // A Step is an index offset on a pitch 393 | // to raise or lower it to a relative new pitch 394 | type Step int 395 | 396 | // Step values 397 | const ( 398 | HalfStep Step = 1 399 | WholeStep = 2 400 | Octave = 12 401 | ) 402 | 403 | // Up raises a pitch s steps 404 | func (p Pitch) Up(s Step) Pitch { 405 | i := noteIndices[p] 406 | if i+int(s) >= len(allPitches) { 407 | return allPitches[len(allPitches)-1] 408 | } 409 | return allPitches[i+int(s)] 410 | } 411 | 412 | // Down lowers a pitch s steps 413 | func (p Pitch) Down(s Step) Pitch { 414 | i := noteIndices[p] 415 | if i-int(s) < 0 { 416 | return allPitches[0] 417 | } 418 | return allPitches[i-int(s)] 419 | } 420 | 421 | // NoteFromIndex is a utility for pitch converters that for some reason have 422 | // integers representing their notes to get a pitch from said integer 423 | func NoteFromIndex(i int) Pitch { 424 | return allPitches[i] 425 | } 426 | -------------------------------------------------------------------------------- /synth/source.go: -------------------------------------------------------------------------------- 1 | package synth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/200sc/klangsynthese/audio" 7 | ) 8 | 9 | // A Source stores necessary information for generating audio and waveform data 10 | type Source struct { 11 | audio.Format 12 | Pitch Pitch 13 | Volume float64 14 | Seconds float64 15 | } 16 | 17 | // PlayLength returns the time it will take before audio generated from this 18 | // source will stop. 19 | func (s Source) PlayLength() time.Duration { 20 | return time.Duration(s.Seconds) * 1000 * time.Millisecond 21 | } 22 | 23 | // Phase is shorthand for phase(s.Pitch, i, s.SampleRate). 24 | // Some sources might have custom phase functions in the future, however. 25 | func (s Source) Phase(i int) float64 { 26 | return phase(s.Pitch, i, s.SampleRate) 27 | } 28 | 29 | // Update is shorthand for applying a set of options to a source 30 | func (s Source) Update(opts ...Option) Source { 31 | for _, opt := range opts { 32 | s = opt(s) 33 | } 34 | return s 35 | } 36 | 37 | var ( 38 | // Int16 is a default source for building 16-bit audio 39 | Int16 = Source{ 40 | Format: audio.Format{ 41 | SampleRate: 44100, 42 | Channels: 2, 43 | Bits: 16, 44 | }, 45 | Pitch: A4, 46 | Volume: .25, 47 | Seconds: 1, 48 | } 49 | ) 50 | -------------------------------------------------------------------------------- /synth/synth_test.go: -------------------------------------------------------------------------------- 1 | package synth 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/200sc/klangsynthese/audio/filter" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSinWav(t *testing.T) { 13 | a, err := Int16.Sin() 14 | assert.Nil(t, err) 15 | a.Play() 16 | time.Sleep(Int16.PlayLength()) 17 | } 18 | 19 | func TestSquareWav(t *testing.T) { 20 | a, err := Int16.Square() 21 | assert.Nil(t, err) 22 | a.Play() 23 | time.Sleep(Int16.PlayLength()) 24 | } 25 | 26 | func TestSawWav(t *testing.T) { 27 | a, err := Int16.Saw() 28 | assert.Nil(t, err) 29 | a.Play() 30 | time.Sleep(Int16.PlayLength()) 31 | } 32 | 33 | func TestTriangleWav(t *testing.T) { 34 | a, err := Int16.Triangle() 35 | assert.Nil(t, err) 36 | a.Play() 37 | time.Sleep(Int16.PlayLength()) 38 | } 39 | 40 | func TestPulseWav(t *testing.T) { 41 | a, err := Int16.Pulse(8)() 42 | assert.Nil(t, err) 43 | a.Play() 44 | time.Sleep(Int16.PlayLength()) 45 | } 46 | 47 | func TestVolume(t *testing.T) { 48 | a, _ := Int16.Sin() 49 | a2, err := a.MustCopy().Filter(filter.Volume(.25)) 50 | a3, _ := a.MustCopy().Filter(filter.VolumeRight(.5)) 51 | a4, _ := a.MustCopy().Filter(filter.VolumeLeft(.5)) 52 | assert.Nil(t, err) 53 | a.Play() 54 | time.Sleep(1 * time.Second) 55 | a2.Play() 56 | time.Sleep(1 * time.Second) 57 | a3.Play() 58 | time.Sleep(1 * time.Second) 59 | a4.Play() 60 | time.Sleep(1 * time.Second) 61 | } 62 | 63 | func TestPan(t *testing.T) { 64 | a, err := Int16.Sin() 65 | a2, err2 := a.MustCopy().Filter(filter.RightPan()) 66 | a3, err3 := a.MustCopy().Filter(filter.LeftPan()) 67 | assert.Nil(t, err) 68 | assert.Nil(t, err2) 69 | assert.Nil(t, err3) 70 | a.Play() 71 | fmt.Println(a.PlayLength()) 72 | time.Sleep(a.PlayLength()) 73 | a2.Play() 74 | time.Sleep(a2.PlayLength()) 75 | a3.Play() 76 | time.Sleep(a3.PlayLength()) 77 | a5, _ := Int16.Sin(Duration(100 * time.Millisecond)) 78 | for p := -1.0; p < 1; p += 0.04 { 79 | a6, _ := a5.MustCopy().Filter(filter.Pan(p)) 80 | a6.Play() 81 | time.Sleep(a6.PlayLength()) 82 | } 83 | } 84 | 85 | func TestStop(t *testing.T) { 86 | a, _ := Int16.Sin() 87 | <-a.Play() 88 | _ = a.Stop() 89 | time.Sleep(1 * time.Second) 90 | // assert that sound was not heard or was only heard very briefly 91 | } 92 | 93 | func TestLoop(t *testing.T) { 94 | a, _ := Int16.Sin() 95 | a, _ = a.Filter(filter.LoopOn()) 96 | <-a.Play() 97 | time.Sleep(3 * time.Second) 98 | } 99 | 100 | func TestModSampleRate(t *testing.T) { 101 | a, _ := Int16.Sin() 102 | a2, _ := a.MustCopy().Filter(filter.ModSampleRate(.5)) 103 | a.Play() 104 | time.Sleep(1 * time.Second) 105 | a2.Play() 106 | time.Sleep(2 * time.Second) 107 | } 108 | 109 | func TestPitchShift(t *testing.T) { 110 | a, _ := Int16.Sin(Duration(1 * time.Second)) 111 | a.Play() 112 | time.Sleep(a.PlayLength()) 113 | a = a.MustFilter(filter.LowQualityShifter.PitchShift(0.5)) 114 | a.Play() 115 | time.Sleep(a.PlayLength()) 116 | } 117 | 118 | func TestSpeed(t *testing.T) { 119 | a, _ := Int16.Sin(Duration(1 * time.Second)) 120 | a.Play() 121 | time.Sleep(a.PlayLength()) 122 | a = a.MustFilter(filter.Speed(.5, filter.HighQualityShifter)) 123 | a.Play() 124 | time.Sleep(a.PlayLength()) 125 | } 126 | -------------------------------------------------------------------------------- /synth/waves.go: -------------------------------------------------------------------------------- 1 | // Package synth provides functions and types to support waveform synthesis 2 | package synth 3 | 4 | import ( 5 | "math" 6 | 7 | "github.com/200sc/klangsynthese/audio" 8 | ) 9 | 10 | // Wave functions take a set of options and return an audio 11 | type Wave func(opts ...Option) (audio.Audio, error) 12 | 13 | // Thanks to https://en.wikibooks.org/wiki/Sound_Synthesis_Theory/Oscillators_and_Wavetables 14 | func phase(freq Pitch, i int, sampleRate uint32) float64 { 15 | return float64(freq) * (float64(i) / float64(sampleRate)) * 2 * math.Pi 16 | } 17 | 18 | func bytesFromInts(is []int16, channels int) []byte { 19 | wave := make([]byte, len(is)*channels*2) 20 | for i := 0; i < len(wave); i += channels * 2 { 21 | wave[i] = byte(is[i/4] % 256) 22 | wave[i+1] = byte(is[i/4] >> 8) 23 | // duplicate the contents across all channels 24 | for c := 1; c < channels; c++ { 25 | wave[i+(2*c)] = wave[i] 26 | wave[i+(2*c)+1] = wave[i+1] 27 | } 28 | } 29 | wave = append(wave, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 30 | return wave 31 | } 32 | 33 | // Sin produces a Sin wave 34 | // __ 35 | // -- -- 36 | // / \ 37 | //--__-- --__-- 38 | func (s Source) Sin(opts ...Option) (audio.Audio, error) { 39 | 40 | s = s.Update(opts...) 41 | 42 | var b []byte 43 | switch s.Bits { 44 | case 16: 45 | s.Volume *= 65535 / 2 46 | wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) 47 | for i := 0; i < len(wave); i++ { 48 | wave[i] = int16(s.Volume * math.Sin(s.Phase(i))) 49 | } 50 | b = bytesFromInts(wave, int(s.Channels)) 51 | } 52 | return s.Wave(b) 53 | } 54 | 55 | // Pulse acts like Square when given a pulse of 2, when given any lesser 56 | // pulse the time up and down will change so that 1/pulse time the wave will 57 | // be up. 58 | // 59 | // __ __ 60 | // || || 61 | // ____||____||____ 62 | func (s Source) Pulse(pulse float64) Wave { 63 | pulseSwitch := 1 - 2/pulse 64 | return func(opts ...Option) (audio.Audio, error) { 65 | s = s.Update(opts...) 66 | 67 | var b []byte 68 | switch s.Bits { 69 | case 16: 70 | s.Volume *= 65535 / 2 71 | wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) 72 | for i := range wave { 73 | // alternatively phase % 2pi 74 | if math.Sin(s.Phase(i)) > pulseSwitch { 75 | wave[i] = int16(s.Volume) 76 | } else { 77 | wave[i] = int16(-s.Volume) 78 | } 79 | } 80 | b = bytesFromInts(wave, int(s.Channels)) 81 | } 82 | return s.Wave(b) 83 | } 84 | } 85 | 86 | // Square produces a Square wave 87 | // 88 | // _________ 89 | // | | 90 | // ______| |________ 91 | func (s Source) Square(opts ...Option) (audio.Audio, error) { 92 | return s.Pulse(2)(opts...) 93 | } 94 | 95 | // Saw produces a saw wave 96 | // 97 | // ^ ^ ^ 98 | // / | / | / 99 | // / |/ |/ 100 | func (s Source) Saw(opts ...Option) (audio.Audio, error) { 101 | s = s.Update(opts...) 102 | 103 | var b []byte 104 | switch s.Bits { 105 | case 16: 106 | s.Volume *= 65535 / 2 107 | wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) 108 | for i := range wave { 109 | wave[i] = int16(s.Volume - (s.Volume / math.Pi * math.Mod(s.Phase(i), 2*math.Pi))) 110 | } 111 | b = bytesFromInts(wave, int(s.Channels)) 112 | } 113 | return s.Wave(b) 114 | } 115 | 116 | // Triangle produces a Triangle wave 117 | // 118 | // ^ ^ 119 | // / \ / \ 120 | // v v v 121 | func (s Source) Triangle(opts ...Option) (audio.Audio, error) { 122 | s = s.Update(opts...) 123 | 124 | var b []byte 125 | switch s.Bits { 126 | case 16: 127 | s.Volume *= 65535 / 2 128 | wave := make([]int16, int(s.Seconds*float64(s.SampleRate))) 129 | for i := range wave { 130 | p := math.Mod(s.Phase(i), 2*math.Pi) 131 | m := int16(p * (2 * s.Volume / math.Pi)) 132 | if math.Sin(p) > 0 { 133 | wave[i] = int16(-s.Volume) + m 134 | } else { 135 | wave[i] = 3*int16(s.Volume) - m 136 | } 137 | } 138 | b = bytesFromInts(wave, int(s.Channels)) 139 | } 140 | return s.Wave(b) 141 | } 142 | 143 | // Could have pulse triangle 144 | -------------------------------------------------------------------------------- /wav/test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200sc/klangsynthese/a0e14a8c862b10650b09ce186bdf4798e6e90e22/wav/test.wav -------------------------------------------------------------------------------- /wav/wav.go: -------------------------------------------------------------------------------- 1 | // Package wav provides functionality to handle .wav files and .wav encoded data 2 | package wav 3 | 4 | import ( 5 | "errors" 6 | "io" 7 | 8 | "encoding/binary" 9 | 10 | "github.com/200sc/klangsynthese/audio" 11 | ) 12 | 13 | // Load loads wav data from the incoming reader as an audio 14 | func Load(r io.Reader) (audio.Audio, error) { 15 | wav, err := Read(r) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return audio.EncodeBytes( 20 | audio.Encoding{ 21 | Data: wav.Data, 22 | Format: audio.Format{ 23 | SampleRate: wav.SampleRate, 24 | Channels: wav.NumChannels, 25 | Bits: wav.BitsPerSample, 26 | }, 27 | }) 28 | } 29 | 30 | // Save will eventually save an audio encoded as a wav to the given writer 31 | func Save(r io.ReadWriter, a audio.Audio) error { 32 | return errors.New("Unsupported Functionality") 33 | } 34 | 35 | // The following is a "fork" of verdverm's go-wav library 36 | 37 | // Data stores the raw information contained in a wav file 38 | type Data struct { 39 | bChunkID [4]byte // B 40 | ChunkSize uint32 // L 41 | bFormat [4]byte // B 42 | 43 | bSubchunk1ID [4]byte // B 44 | Subchunk1Size uint32 // L 45 | 46 | AudioFormat uint16 // L 47 | NumChannels uint16 // L 48 | SampleRate uint32 // L 49 | ByteRate uint32 // L 50 | BlockAlign uint16 // L 51 | BitsPerSample uint16 // L 52 | 53 | bSubchunk2ID [4]byte // B 54 | Subchunk2Size uint32 // L 55 | Data []byte // L 56 | } 57 | 58 | // Read returns raw wav data from an input reader 59 | func Read(r io.Reader) (Data, error) { 60 | wav := Data{} 61 | 62 | err := binary.Read(r, binary.BigEndian, &wav.bChunkID) 63 | if err != nil { 64 | return wav, err 65 | } 66 | err = binary.Read(r, binary.LittleEndian, &wav.ChunkSize) 67 | if err != nil { 68 | return wav, err 69 | } 70 | err = binary.Read(r, binary.BigEndian, &wav.bFormat) 71 | if err != nil { 72 | return wav, err 73 | } 74 | 75 | err = binary.Read(r, binary.BigEndian, &wav.bSubchunk1ID) 76 | if err != nil { 77 | return wav, err 78 | } 79 | err = binary.Read(r, binary.LittleEndian, &wav.Subchunk1Size) 80 | if err != nil { 81 | return wav, err 82 | } 83 | err = binary.Read(r, binary.LittleEndian, &wav.AudioFormat) 84 | if err != nil { 85 | return wav, err 86 | } 87 | err = binary.Read(r, binary.LittleEndian, &wav.NumChannels) 88 | if err != nil { 89 | return wav, err 90 | } 91 | err = binary.Read(r, binary.LittleEndian, &wav.SampleRate) 92 | if err != nil { 93 | return wav, err 94 | } 95 | err = binary.Read(r, binary.LittleEndian, &wav.ByteRate) 96 | if err != nil { 97 | return wav, err 98 | } 99 | err = binary.Read(r, binary.LittleEndian, &wav.BlockAlign) 100 | if err != nil { 101 | return wav, err 102 | } 103 | err = binary.Read(r, binary.LittleEndian, &wav.BitsPerSample) 104 | if err != nil { 105 | return wav, err 106 | } 107 | 108 | err = binary.Read(r, binary.BigEndian, &wav.bSubchunk2ID) 109 | if err != nil { 110 | return wav, err 111 | } 112 | err = binary.Read(r, binary.LittleEndian, &wav.Subchunk2Size) 113 | if err != nil { 114 | return wav, err 115 | } 116 | 117 | wav.Data = make([]byte, wav.Subchunk2Size) 118 | err = binary.Read(r, binary.LittleEndian, &wav.Data) 119 | 120 | return wav, err 121 | } 122 | -------------------------------------------------------------------------------- /wav/wav_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestBasicWav(t *testing.T) { 11 | fmt.Println("Running Basic Wav") 12 | f, err := os.Open("test.wav") 13 | fmt.Println(f) 14 | if err != nil { 15 | t.Fatal("expected open err to be nil, was", err) 16 | } 17 | a, err := Load(f) 18 | if err != nil { 19 | t.Fatal("expected load err to be nil, was", err) 20 | } 21 | err = <-a.Play() 22 | if err != nil { 23 | t.Fatal("expected play err to be nil, was", err) 24 | } 25 | time.Sleep(4 * time.Second) 26 | // In addition to the error tests here, this should play noise 27 | } 28 | --------------------------------------------------------------------------------