├── .gitignore ├── LICENSE ├── README.md ├── analyzecq.py ├── cq ├── constantq.go ├── cqinverse.go ├── cqparams.go ├── kernel.go ├── resampler.go ├── spectrogram.go └── utils.go ├── cqspectrogram.go ├── demo.go ├── fakeflac └── flac.go ├── features └── peaks.go ├── file ├── cqfile.go └── soundfile.go ├── generatetest.go ├── mashapp ├── api.go ├── app.html ├── model.go ├── polymer │ ├── app │ │ └── elements │ │ │ ├── edit-track-dialog │ │ │ ├── edit-track-dialog-style.html │ │ │ ├── edit-track-dialog.html │ │ │ └── edit-track-dialog.js │ │ │ ├── loadfile-dialog │ │ │ ├── loadfile-dialog-style.html │ │ │ ├── loadfile-dialog.html │ │ │ └── loadfile-dialog.js │ │ │ ├── mash-app │ │ │ ├── mash-app-style.html │ │ │ ├── mash-app.html │ │ │ └── mash-app.js │ │ │ ├── play-controls │ │ │ ├── play-controls-style.html │ │ │ ├── play-controls.html │ │ │ └── play-controls.js │ │ │ ├── text-dialog │ │ │ ├── text-dialog-style.html │ │ │ ├── text-dialog.html │ │ │ └── text-dialog.js │ │ │ ├── track-line │ │ │ ├── track-line-style.html │ │ │ ├── track-line.html │ │ │ └── track-line.js │ │ │ └── util-src │ │ │ ├── util-src.html │ │ │ └── util-src.js │ └── bower.json ├── server.go ├── state.go └── static │ ├── app.css │ └── jquery-2.1.4.min.js ├── mashappserver.go ├── output ├── flacfile.go ├── jack.go ├── pulse.go ├── pulsegofork.go ├── screen.go └── wavfile.go ├── piano.wav ├── readcq.go ├── runcq.go ├── runthrough.go ├── showspec.py ├── sounds ├── adsrenvelope.go ├── basesound.go ├── channelsound.go ├── concat.go ├── delay.go ├── denseiir.go ├── flacfile.go ├── hzfromchannel.go ├── karplusstrong.go ├── midiinput.go ├── multiply.go ├── normalsum.go ├── repeater.go ├── sampler.go ├── silence.go ├── simplewaves.go ├── slicesound.go ├── sound.go ├── timedsound.go └── wavfile.go ├── spectrogramshift.go ├── test ├── adsr.wav ├── concat.wav ├── delay.wav ├── denseiir.wav ├── multiply.wav ├── normalsum.wav ├── repeat.wav ├── sampler.wav ├── samples.go ├── silence.wav └── sounds_test.go ├── types ├── buffer.go └── typedbuffer.go ├── util ├── livespectrogram.go ├── parser.go ├── samplecache.go └── screen.go └── writecq.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | 25 | # Personal scripts 26 | Makefile 27 | demo 28 | 29 | # CQ outputs 30 | *.cq 31 | *.peaks 32 | 33 | # Sound files 34 | *.wav 35 | *.mp3 36 | *.flac 37 | *.raw 38 | 39 | 40 | # MashApp bower polymer stuffs 41 | mashapp/polymer/.bowerrc 42 | mashapp/polymer/.editorconfig 43 | mashapp/polymer/.gitattributes 44 | mashapp/polymer/.gitignore 45 | mashapp/polymer/.jscsrc 46 | mashapp/polymer/.jshintrc 47 | mashapp/polymer/app/cache-config.json 48 | mashapp/polymer/app/favicon.ico 49 | mashapp/polymer/app/index.html 50 | mashapp/polymer/app/manifest.json 51 | mashapp/polymer/app/robots.txt 52 | mashapp/polymer/app/sw-import.js 53 | mashapp/polymer/docs/ 54 | mashapp/polymer/gulpfile.js 55 | mashapp/polymer/package.json 56 | mashapp/polymer/wct.conf.js 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-sound 2 | ====== 3 | 4 | go-sound is a go library for dealing with sound waves. 5 | At the fundamental level, the library models each sound as a channel of [-1, 1] samples that represent the 6 | [Compression Wave](https://en.wikipedia.org/wiki/Sound#Longitudinal_and_transverse_waves) that comprises the sound. 7 | To see it it action, check out [demo.go](https://github.com/padster/go-sound/blob/master/demo.go) or the examples 8 | provided by each file in the [sounds](https://github.com/padster/go-sound/tree/master/sounds) package. 9 | 10 | A tutorial explaining the basics behind sound wave modelling in code, and how it is implemented in go-sound, is available on my blog: http://padsterprogramming.blogspot.ch/2015/11/sounds-good-part-1.html 11 | 12 | ### Features : 13 | - A Sound interface, with a BaseSound implementation that makes it simpler to write your own. 14 | - Sound Math (play notes together to make chords, or in serial to form a melody, ...) 15 | - Utilities for dealing with sounds (repeat sounds, generate from text, ...) 16 | - Implementations for various inputs (silence, sinusoidal wave, .wav file, ...) 17 | - Implementations for various outputs (play via pulse audio, draw to screen, .wav file, ...) 18 | - Realtime input (via MIDI) - with delay though. 19 | - Sound -> Spectrogram -> Sound conversion using a [Constant Q transform](https://en.wikipedia.org/wiki/Constant_Q_transform) 20 | 21 | ### In progress: 22 | - MashApp, a golang server and polymer web app for manipulating sounds using the library. 23 | 24 | ### Future plans: 25 | - Inputs and Outputs integrating with [Jack Audio](http://jackaudio.org) 26 | - Realtime input from microphone, more efficient from MIDI 27 | - Effects algorithms (digitial processing like reverb, bandpass ...) 28 | 29 | #### Notes: 30 | This library requires pulse audio installed to play the sounds, libflac for reading/writing flac files, and OpenGL 3.3 / GLFW 3.1 for rendering a soundwave to screen. 31 | 32 | Some planned additions are included above, and include effects like those available in [Audacity](http://audacityteam.org/) 33 | (e.g. rewriting Nyquist, LADSPA plugins in Go), or ones explained [here](https://www.youtube.com/channel/UCchjpg1aaY91WubqAYRcNsg) 34 | or [here](https://christianfloisand.wordpress.com/2012/09/04/digital-reverberation). 35 | Additionally, some more complex instrument synthesizers could be added, and contributions are welcome. 36 | 37 | The example piano .wav C note came from: http://freewavesamples.com/ensoniq-sq-1-dyno-keys-c4 38 | 39 | Frequencies of notes are all obtained from: http://www.phy.mtu.edu/~suits/notefreqs.html 40 | 41 | For MIDI input, a number of things are required for portmidi: 42 | - Instructions to test the midi input device: https://wiki.archlinux.org/index.php/USB_MIDI_keyboards 43 | - Instructions to set linux up for realtime processing: http://tedfelix.com/linux/linux-midi.html 44 | - ALSA dev library required (libasound2-dev) 45 | - I needed to manually install portmidi: http://sourceforge.net/p/portmedia/wiki/Installing_portmidi_on_Linux/ 46 | - This also required removing the "WORKING_DIRECTORY pm_java)" in the ccmake configs 47 | - And to link against /usr/local/lib/libportmidi.so instead of /usr/lib/libporttime.so 48 | 49 | Overall quite a pain and there's still a noticeable delay in the MIDI input, patches to reduce that are welcome! 50 | 51 | Credit to [cryptix](//github.com/cryptix), [cocoonlife](//github.com/cocoonlife), [moriyoshi](//github.com/moriyoshi) and [rakyll](//github.com/rakyll) for their wavFile, pulseAudio and portmidi implementations respectively, used by go-sound. 52 | 53 | -------------------------------------------------------------------------------- /analyzecq.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import cmath, colorsys, math, sys, os 4 | 5 | columns = [] 6 | meta = [] 7 | bpo = 24 8 | octaves = 7 9 | MASK = 1 << (octaves - 1) 10 | TAU = 2 * np.pi 11 | 12 | def fixInput(m): 13 | print "%f to %f" % (np.max(m), np.min(m)) 14 | m = np.transpose(m) 15 | return m 16 | 17 | def display(mX, pX): 18 | mX = fixInput(mX) 19 | pX = fixInput(pX) 20 | fig, (ax0, ax1) = plt.subplots(nrows=2, sharex=True) 21 | ax0.imshow(mX, cmap='gray') 22 | ax1.imshow(pX, cmap='gist_rainbow') 23 | plt.show() 24 | 25 | def trailingZeros(num): 26 | if num % 2 == 0: 27 | return 1 + trailingZeros(num / 2) 28 | return 1 29 | 30 | def identity(x): 31 | return x 32 | 33 | def flatPhase(values): 34 | i = 1 35 | while i < len(values): 36 | delta = values[i] - values[i-1] 37 | # Move to the closest multiple of TAU 38 | delta = round(delta / TAU) * TAU 39 | values[i] -= delta 40 | i = i + 1 41 | return values 42 | 43 | def valuesForBin(octave, bin, f=identity): 44 | result = [] 45 | skip = 1 << octave 46 | bindex = octave * bpo + bin 47 | for i in range(0, len(columns), skip): 48 | result.append(f(columns[i][bindex])) 49 | return np.array(result) 50 | 51 | def pairwise(octave1, bin1, octave2, bin2, f=identity): 52 | r1, r2 = [], [] 53 | skip = 1 << max(octave1, octave2) 54 | bindex1 = octave1 * bpo + bin1 55 | bindex2 = octave2 * bpo + bin2 56 | for i in range(0, len(columns), skip): 57 | r1.append(f(columns[i][bindex1])) 58 | r2.append(f(columns[i][bindex2])) 59 | return r1, r2 60 | 61 | def running_mean(x, N): 62 | cumsum = np.cumsum(np.insert(x, 0, 0)) 63 | return (cumsum[N:] - cumsum[:-N]) / N 64 | 65 | def readFile(inputFile='out.cq'): 66 | global columns 67 | values = np.memmap(inputFile, dtype=np.complex64, mode="r") 68 | 69 | at = 0 70 | columnCounter = MASK 71 | columnsRead = 0 72 | while at < len(values): 73 | samplesInColumn = trailingZeros(columnCounter) * bpo 74 | columns.append(values[at:at+samplesInColumn]) 75 | columnCounter = (columnCounter % MASK) + 1 76 | at = at + samplesInColumn 77 | 78 | columnsRead = columnsRead + 1 79 | if columnsRead % 10000 == 0: 80 | print "%d columns read" % columnsRead 81 | 82 | # Normalize: find last full size column 83 | at = -1 84 | while len(columns[at]) != octaves * bpo: 85 | at -= 1 86 | columns = columns[:at] 87 | print "%d columns read" % (len(columns)) 88 | 89 | def readMeta(inputFile='out.meta'): 90 | global meta 91 | values = np.memmap(inputFile, dtype=np.int8, mode="r") 92 | 93 | at = 0 94 | columnCounter = MASK 95 | columnsRead = 0 96 | while at < len(values): 97 | samplesInColumn = trailingZeros(columnCounter) * bpo 98 | meta.append(values[at:at+samplesInColumn]) 99 | columnCounter = (columnCounter % MASK) + 1 100 | at = at + samplesInColumn 101 | 102 | columnsRead = columnsRead + 1 103 | if columnsRead % 10000 == 0: 104 | print "%d columns read" % columnsRead 105 | 106 | # Normalize: find last full size column 107 | at = -1 108 | while len(meta[at]) != octaves * bpo: 109 | at -= 1 110 | meta = meta[:at] 111 | print "%d columns read" % (len(meta)) 112 | 113 | def readFileAndMeta(): 114 | readFile() 115 | readMeta() 116 | if len(columns) != len(meta): 117 | print "CQ data and meta length do not match! %d vs %d" % (len(columns), len(meta)) 118 | raise BaseException("oops") 119 | 120 | def phaseScatter(octave, bin): 121 | p1, p2 = pairwise(octave, bin, octave + 1, bin, cmath.phase) 122 | p1, p2 = flatPhase(p1), flatPhase(p2) 123 | p1, p2 = np.diff(p1), np.diff(p2) * 2 124 | plt.scatter(p1, p2) 125 | plt.show() 126 | 127 | def phaseGraphsForBin(bin): 128 | for octave in range(octaves): 129 | phase = valuesForBin(octave, bin, cmath.phase) 130 | phase = flatPhase(phase) 131 | factor = 1 << octave 132 | x = range(0, len(phase) * factor, factor) 133 | plt.plot(x, phase * factor, label='octave ' + str(octave)) 134 | plt.legend( 135 | loc='upper center', bbox_to_anchor=(0.5, 1.0), 136 | ncol=3, fancybox=True, shadow=True) 137 | plt.show() 138 | 139 | def plotComplexSpectrogram(data, meta): 140 | data = data[:,::32] 141 | meta = meta[:,::32] 142 | fig, (ax0, ax1) = plt.subplots(nrows=2, sharex=True) 143 | power = np.abs(data) 144 | print "%f -> %f" % (np.min(meta), np.max(meta)) 145 | # power = 20 * np.log10(power + 1e-8) # convert to DB 146 | phase = np.angle(data) 147 | ax0.imshow(power, cmap='gray') 148 | ax0.imshow(meta, cmap='copper', alpha=0.4) 149 | ax1.imshow(phase, cmap='gist_rainbow') 150 | plt.show() 151 | 152 | def uninterpolatedSpectrogram(): 153 | maxHeight = octaves * bpo 154 | asMatrix = np.zeros((maxHeight, len(columns)), dtype=np.complex64) 155 | metaMatrix = np.zeros((maxHeight, len(columns)), dtype=np.int8) 156 | for (i, column) in enumerate(columns): 157 | asMatrix[:len(column), i] = column 158 | metaMatrix[:len(column), i] = meta[i] 159 | if i > 0: 160 | asMatrix[len(column):, i] = asMatrix[len(column):, i - 1] 161 | metaMatrix[len(column):, i] = metaMatrix[len(column):, i - 1] 162 | plotComplexSpectrogram(asMatrix, metaMatrix) 163 | 164 | def phaseVsPowerScatter(): 165 | # TODO: Should these phase peaks line up more? 166 | for octave in range(6): 167 | for i in range(24): 168 | r, g, b = colorsys.hsv_to_rgb(i / 24.0, 1.0, 1.0) 169 | c = '#%02x%02x%02x' % (int(r*255), int(g*255), int(b*255)) 170 | power = valuesForBin(octave, i, np.abs) 171 | phase = valuesForBin(octave, i, cmath.phase) 172 | 173 | # power = 20 * np.log10(np.abs(power) + 1e-8) 174 | ax1 = plt.subplot(3, 2, octave + 1) 175 | plt.title("octave %d" % octave) 176 | phase = flatPhase(phase) 177 | phase = np.diff(phase) 178 | phase = np.fmod(phase * (2 ** octave), 2 * np.pi) 179 | ax1.scatter(phase, power[1:], color=c, label='bin' + str(i)) 180 | plt.show() 181 | 182 | def notePower(): 183 | bins = 24 184 | power = np.zeros(bins) 185 | for octave in range(3): 186 | for bin in range(bins): 187 | power[bin] += np.sum(valuesForBin(octave + 2, bin, np.abs)) 188 | 189 | notes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'] 190 | noteLen = len(notes) 191 | notePowers = np.zeros(noteLen) 192 | width = 0.7 193 | for note in range(noteLen): 194 | notePowers[note] = power[2 * note] + (power[2 * note + 1] + power[2 * note - 1]) / 2.0 195 | plt.bar(range(noteLen), notePowers, width) 196 | plt.xticks(np.arange(noteLen) + width / 2, notes) 197 | plt.show() 198 | 199 | 200 | readFileAndMeta() 201 | # uninterpolatedSpectrogram() 202 | # phaseGraphsForBin(3) 203 | # phaseScatter(2, 4) 204 | # phaseVsPowerScatter() 205 | notePower() 206 | -------------------------------------------------------------------------------- /cq/constantq.go: -------------------------------------------------------------------------------- 1 | package cq 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/mjibson/go-dsp/fft" 8 | ) 9 | 10 | const DEBUG_CQ = false 11 | 12 | type ConstantQ struct { 13 | kernel *CQKernel 14 | 15 | bigBlockSize int 16 | OutputLatency int 17 | buffers [][]float64 18 | 19 | latencies []int 20 | decimators []*Resampler 21 | } 22 | 23 | func NewConstantQ(params CQParams) *ConstantQ { 24 | kernel := NewCQKernel(params) 25 | p := kernel.Properties 26 | 27 | // Use exact powers of two for resampling rates. They don't have 28 | // to be related to our actual samplerate: the resampler only 29 | // cares about the ratio, but it only accepts integer source and 30 | // target rates, and if we start from the actual samplerate we 31 | // risk getting non-integer rates for lower octaves 32 | sourceRate := unsafeShift(p.octaves) 33 | latencies := make([]int, p.octaves, p.octaves) 34 | decimators := make([]*Resampler, p.octaves, p.octaves) 35 | 36 | // top octave, no resampling 37 | latencies[0] = 0 38 | decimators[0] = nil 39 | 40 | for i := 1; i < p.octaves; i++ { 41 | factor := unsafeShift(i) 42 | 43 | r := NewResampler(sourceRate, sourceRate/factor, 50, 0.05) 44 | if DEBUG_CQ { 45 | fmt.Printf("Forward: octave %d: resample from %v to %v\n", i, sourceRate, sourceRate/factor) 46 | } 47 | 48 | // We need to adapt the latencies so as to get the first input 49 | // sample to be aligned, in time, at the decimator output 50 | // across all octaves. 51 | // 52 | // Our decimator uses a linear phase filter, but being causal 53 | // it is not zero phase: it has a latency that depends on the 54 | // decimation factor. Those latencies have been calculated 55 | // per-octave and are available to us in the latencies 56 | // array. Left to its own devices, the first input sample will 57 | // appear at output sample 0 in the highest octave (where no 58 | // decimation is needed), sample number latencies[1] in the 59 | // next octave down, latencies[2] in the next one, etc. We get 60 | // to apply some artificial per-octave latency after the 61 | // decimator in the processing chain, in order to compensate 62 | // for the differing latencies associated with different 63 | // decimation factors. How much should we insert? 64 | // 65 | // The outputs of the decimators are at different rates (in 66 | // terms of the relation between clock time and samples) and 67 | // we want them aligned in terms of time. So, for example, a 68 | // latency of 10 samples with a decimation factor of 2 is 69 | // equivalent to a latency of 20 with no decimation -- they 70 | // both result in the first output sample happening at the 71 | // same equivalent time in milliseconds. 72 | // 73 | // So here we record the latency added by the decimator, in 74 | // terms of the sample rate of the undecimated signal. Then we 75 | // use that to compensate in a moment, when we've discovered 76 | // what the longest latency across all octaves is. 77 | latencies[i] = r.GetLatency() * factor 78 | decimators[i] = r 79 | } 80 | 81 | bigBlockSize := p.fftSize * unsafeShift(p.octaves-1) 82 | 83 | // Now add in the extra padding and compensate for hops that must 84 | // be dropped in order to align the atom centres across 85 | // octaves. Again this is a bit trickier because we are doing it 86 | // at input rather than output and so must work in per-octave 87 | // sample rates rather than output blocks 88 | 89 | emptyHops := p.firstCentre / p.atomSpacing 90 | 91 | drops := make([]int, p.octaves, p.octaves) 92 | for i := 0; i < p.octaves; i++ { 93 | factor := unsafeShift(i) 94 | dropHops := emptyHops*unsafeShift(p.octaves-i-1) - emptyHops 95 | drops[i] = ((dropHops * p.fftHop) * factor) / p.atomsPerFrame 96 | } 97 | 98 | maxLatPlusDrop := 0 99 | for i := 0; i < p.octaves; i++ { 100 | latPlusDrop := latencies[i] + drops[i] 101 | if latPlusDrop > maxLatPlusDrop { 102 | maxLatPlusDrop = latPlusDrop 103 | } 104 | } 105 | 106 | totalLatency := maxLatPlusDrop 107 | 108 | lat0 := totalLatency - latencies[0] - drops[0] 109 | totalLatency = roundUp(float64((lat0/p.fftHop)*p.fftHop)) + latencies[0] + drops[0] 110 | 111 | // We want (totalLatency - latencies[i]) to be a multiple of 2^i 112 | // for each octave i, so that we do not end up with fractional 113 | // octave latencies below. In theory this is hard, in practice if 114 | // we ensure it for the last octave we should be OK. 115 | finalOctLat := float64(latencies[p.octaves-1]) 116 | finalOneFactInt := unsafeShift(p.octaves - 1) 117 | finalOctFact := float64(finalOneFactInt) 118 | 119 | totalLatency = int(finalOctLat + finalOctFact*math.Ceil((float64(totalLatency)-finalOctLat)/finalOctFact) + .5) 120 | 121 | if DEBUG_CQ { 122 | fmt.Printf("total latency = %v\n", totalLatency) 123 | } 124 | 125 | // Padding as in the reference (will be introduced with the 126 | // latency compensation in the loop below) 127 | outputLatency := totalLatency + bigBlockSize - p.firstCentre*unsafeShift(p.octaves-1) 128 | 129 | if DEBUG_CQ { 130 | fmt.Printf("bigBlockSize = %v, firstCentre = %v, octaves = %v, so outputLatency = %v\n", 131 | bigBlockSize, p.firstCentre, p.octaves, outputLatency) 132 | } 133 | 134 | buffers := make([][]float64, p.octaves, p.octaves) 135 | 136 | for i := 0; i < p.octaves; i++ { 137 | factor := unsafeShift(i) 138 | 139 | // Calculate the difference between the total latency applied 140 | // across all octaves, and the existing latency due to the 141 | // decimator for this octave, and then convert it back into 142 | // the sample rate appropriate for the output latency of this 143 | // decimator -- including one additional big block of padding 144 | // (as in the reference). 145 | 146 | octaveLatency := float64(totalLatency-latencies[i]-drops[i]+bigBlockSize) / float64(factor) 147 | 148 | if DEBUG_CQ { 149 | rounded := float64(round(octaveLatency)) 150 | fmt.Printf("octave %d: resampler latency = %v, drop = %v, (/factor = %v), octaveLatency = %v -> %v (diff * factor = %v * %v = %v)\n", 151 | i, latencies[i], drops[i], drops[i]/factor, octaveLatency, rounded, octaveLatency-rounded, factor, (octaveLatency-rounded)*float64(factor)) 152 | } 153 | 154 | sz := int(octaveLatency + 0.5) 155 | buffers[i] = make([]float64, sz, sz) 156 | } 157 | 158 | return &ConstantQ{ 159 | kernel, 160 | 161 | bigBlockSize, 162 | outputLatency, 163 | buffers, 164 | 165 | latencies, 166 | decimators, 167 | } 168 | } 169 | 170 | func (cq *ConstantQ) ProcessChannel(samples <-chan float64) <-chan []complex128 { 171 | result := make(chan []complex128) 172 | // required := cq.kernel.Properties.fftSize * unsafeShift(cq.octaves - 1) 173 | required := 4096 // HACK - actually figure this out properly. 174 | 175 | go func() { 176 | buffer := make([]float64, required, required) 177 | at := 0 178 | for s := range samples { 179 | if at == required { 180 | for _, c := range cq.Process(buffer) { 181 | result <- c 182 | } 183 | at = 0 184 | } 185 | buffer[at] = s 186 | at++ 187 | } 188 | for _, c := range cq.Process(buffer[:at]) { 189 | result <- c 190 | } 191 | for _, c := range cq.GetRemainingOutput() { 192 | result <- c 193 | } 194 | close(result) 195 | }() 196 | 197 | return result 198 | } 199 | 200 | func (cq *ConstantQ) Process(td []float64) [][]complex128 { 201 | apf := cq.kernel.Properties.atomsPerFrame 202 | bpo := cq.kernel.Properties.binsPerOctave 203 | octaves := cq.kernel.Properties.octaves 204 | fftSize := cq.kernel.Properties.fftSize 205 | 206 | cq.buffers[0] = append(cq.buffers[0], td...) 207 | for i := 1; i < octaves; i++ { 208 | decimated := cq.decimators[i].Process(td) 209 | cq.buffers[i] = append(cq.buffers[i], decimated...) 210 | } 211 | 212 | out := [][]complex128{} 213 | for { 214 | // We could have quite different remaining sample counts in 215 | // different octaves, because (apart from the predictable 216 | // added counts for decimator output on each block) we also 217 | // have variable additional latency per octave 218 | enough := true 219 | for i := 0; i < octaves; i++ { 220 | required := fftSize * unsafeShift(octaves-i-1) 221 | if len(cq.buffers[i]) < required { 222 | enough = false 223 | } 224 | } 225 | if !enough { 226 | break 227 | } 228 | 229 | base := len(out) 230 | totalColumns := unsafeShift(octaves-1) * apf 231 | 232 | // Pre-fill totalColumns number of empty arrays 233 | out = append(out, make([][]complex128, totalColumns, totalColumns)...) 234 | 235 | for octave := 0; octave < octaves; octave++ { 236 | blocksThisOctave := unsafeShift(octaves - octave - 1) 237 | 238 | for b := 0; b < blocksThisOctave; b++ { 239 | block := cq.processOctaveBlock(octave) 240 | 241 | for j := 0; j < apf; j++ { 242 | target := base + (b*(totalColumns/blocksThisOctave) + (j * ((totalColumns / blocksThisOctave) / apf))) 243 | 244 | toAppend := bpo*(octave+1) - len(out[target]) 245 | if toAppend > 0 { 246 | out[target] = append(out[target], make([]complex128, toAppend, toAppend)...) 247 | } 248 | 249 | for i := 0; i < bpo; i++ { 250 | out[target][bpo*octave+i] = block[j][bpo-i-1] 251 | } 252 | } 253 | } 254 | } 255 | } 256 | 257 | return out 258 | } 259 | 260 | func (cq *ConstantQ) GetRemainingOutput() [][]complex128 { 261 | // Same as padding added at start, though rounded up 262 | pad := roundUp(float64(cq.OutputLatency)/float64(cq.bigBlockSize)) * cq.bigBlockSize 263 | zeros := make([]float64, pad, pad) 264 | return cq.Process(zeros) 265 | } 266 | 267 | func (cq *ConstantQ) processOctaveBlock(octave int) [][]complex128 { 268 | apf := cq.kernel.Properties.atomsPerFrame 269 | bpo := cq.kernel.Properties.binsPerOctave 270 | fftHop := cq.kernel.Properties.fftHop 271 | fftSize := cq.kernel.Properties.fftSize 272 | 273 | cv := fft.FFTReal(cq.buffers[octave][:fftSize]) 274 | cq.buffers[octave] = cq.buffers[octave][fftHop:] 275 | 276 | cqrowvec := cq.kernel.processForward(cv) 277 | // Reform into a column matrix 278 | cqblock := make([][]complex128, apf, apf) 279 | for j := 0; j < apf; j++ { 280 | cqblock[j] = make([]complex128, bpo, bpo) 281 | for i := 0; i < bpo; i++ { 282 | cqblock[j][i] = cqrowvec[i*apf+j] 283 | } 284 | } 285 | 286 | return cqblock 287 | } 288 | 289 | func (cq *ConstantQ) BinCount() int { 290 | return cq.kernel.BinCount() 291 | } 292 | 293 | func (cq *ConstantQ) bpo() int { 294 | return cq.kernel.Properties.binsPerOctave 295 | } 296 | -------------------------------------------------------------------------------- /cq/cqparams.go: -------------------------------------------------------------------------------- 1 | package cq 2 | 3 | type Window int 4 | 5 | const ( 6 | SqrtBlackmanHarris Window = iota 7 | SqrtBlackman 8 | SqrtHann 9 | BlackmanHarris 10 | Blackman 11 | Hann 12 | ) 13 | 14 | type CQParams struct { 15 | sampleRate float64 16 | Octaves int 17 | minFrequency float64 18 | BinsPerOctave int 19 | 20 | // Spectral atom bandwidth scaling;1 is optimal for reconstruction, 21 | // q < 1 increases smearing in frequency domain but improves the time domain. 22 | q float64 23 | 24 | // Hop size between temporal atoms; 1 == no overlap, smaller = overlapping. 25 | atomHopFactor float64 26 | 27 | // Values smaller than this are zeroed in the kernel. 28 | threshold float64 29 | 30 | // Window shape for kernal atoms. 31 | window Window 32 | } 33 | 34 | func NewCQParams(sampleRate float64, octaves int, minFreq float64, binsPerOctave int) CQParams { 35 | if minFreq < 0 || octaves < 1 { 36 | panic("Requires frequencies 0 < min, octaves ") 37 | } 38 | 39 | return CQParams{ 40 | sampleRate, 41 | octaves, 42 | minFreq, 43 | binsPerOctave, 44 | 1.0, /* Q scaling factor */ 45 | 0.25, /* hop size of shortest temporal atom. */ 46 | 0.0005, /* sparcity threshold for resulting kernal. */ 47 | SqrtBlackmanHarris, /* window shape */ 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cq/kernel.go: -------------------------------------------------------------------------------- 1 | package cq 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/cmplx" 7 | 8 | "github.com/mjibson/go-dsp/fft" 9 | ) 10 | 11 | const DEBUG = false 12 | 13 | type Properties struct { 14 | sampleRate float64 15 | // NOTE: minFrequency of the kernel is *not* min frequency of the CQ. 16 | // It is the frequency of the lowest note in the top octave. 17 | minFrequency float64 18 | octaves int 19 | binsPerOctave int 20 | fftSize int 21 | fftHop int 22 | atomsPerFrame int 23 | atomSpacing int 24 | firstCentre int 25 | lastCentre int 26 | Q float64 27 | } 28 | 29 | type Kernel struct { 30 | origin []int 31 | data [][]complex128 32 | } 33 | 34 | type CQKernel struct { 35 | Properties Properties 36 | kernel *Kernel 37 | } 38 | 39 | // TODO - clean up a lot. 40 | func NewCQKernel(params CQParams) *CQKernel { 41 | // Constructor 42 | p := Properties{} 43 | p.sampleRate = params.sampleRate 44 | p.octaves = params.Octaves 45 | p.binsPerOctave = params.BinsPerOctave 46 | p.minFrequency = params.minFrequency * math.Pow(2.0, float64(p.octaves)-1.0+1.0/float64(p.binsPerOctave)) 47 | 48 | // GenerateKernel 49 | q := params.q 50 | atomHopFactor := params.atomHopFactor 51 | thresh := params.threshold 52 | bpo := params.BinsPerOctave 53 | 54 | p.Q = q / (math.Pow(2, 1.0/float64(bpo)) - 1.0) 55 | 56 | maxNK := float64(int(math.Floor(p.Q*p.sampleRate/p.minFrequency + 0.5))) 57 | minNK := float64(int(math.Floor(p.Q*p.sampleRate/ 58 | (p.minFrequency*math.Pow(2.0, (float64(bpo)-1.0)/float64(bpo))) + 0.5))) 59 | 60 | if minNK == 0 || maxNK == 0 { 61 | panic("Kernal minNK or maxNK is 0, can't make kernel") 62 | } 63 | 64 | p.atomSpacing = round(minNK*atomHopFactor + 0.5) 65 | p.firstCentre = p.atomSpacing * roundUp(math.Ceil(maxNK/2.0)/float64(p.atomSpacing)) 66 | p.fftSize = nextPowerOf2(p.firstCentre + roundUp(maxNK/2.0)) 67 | p.atomsPerFrame = roundDown(1.0 + (float64(p.fftSize)-math.Ceil(maxNK/2.0)-float64(p.firstCentre))/float64(p.atomSpacing)) 68 | 69 | if DEBUG { 70 | fmt.Printf("atomsPerFrame = %v (q = %v, Q = %v, atomHopFactor = %v, atomSpacing = %v, fftSize = %v, maxNK = %v, firstCentre = %v)\n", 71 | p.atomsPerFrame, q, p.Q, atomHopFactor, p.atomSpacing, p.fftSize, maxNK, p.firstCentre) 72 | } 73 | 74 | p.lastCentre = p.firstCentre + (p.atomsPerFrame-1)*p.atomSpacing 75 | p.fftHop = (p.lastCentre + p.atomSpacing) - p.firstCentre 76 | 77 | if DEBUG { 78 | fmt.Printf("fftHop = %v\n", p.fftHop) 79 | } 80 | 81 | dataSize := p.binsPerOctave * p.atomsPerFrame 82 | 83 | kernel := Kernel{ 84 | make([]int, 0, dataSize), 85 | make([][]complex128, 0, dataSize), 86 | } 87 | 88 | for k := 1; k <= p.binsPerOctave; k++ { 89 | nk := round(p.Q * p.sampleRate / (p.minFrequency * math.Pow(2, ((float64(k)-1.0)/float64(bpo))))) 90 | win := makeWindow(params.window, nk) 91 | fk := float64(p.minFrequency * math.Pow(2, ((float64(k)-1.0)/float64(bpo)))) 92 | 93 | cmplxs := make([]complex128, nk, nk) 94 | for i := 0; i < nk; i++ { 95 | arg := (2.0 * math.Pi * fk * float64(i)) / p.sampleRate 96 | cmplxs[i] = cmplx.Rect(win[i], arg) 97 | } 98 | 99 | atomOffset := p.firstCentre - roundUp(float64(nk)/2.0) 100 | 101 | for i := 0; i < p.atomsPerFrame; i++ { 102 | shift := atomOffset + (i * p.atomSpacing) 103 | cin := make([]complex128, p.fftSize, p.fftSize) 104 | for j := 0; j < nk; j++ { 105 | cin[j+shift] = cmplxs[j] 106 | } 107 | 108 | cout := fft.FFT(cin) 109 | 110 | // Keep this dense for the moment (until after normalisation calculations) 111 | for j := 0; j < p.fftSize; j++ { 112 | if cmplx.Abs(cout[j]) < thresh { 113 | cout[j] = complex(0, 0) 114 | } else { 115 | cout[j] = complexTimes(cout[j], 1.0/float64(p.fftSize)) 116 | } 117 | } 118 | 119 | kernel.origin = append(kernel.origin, 0) 120 | kernel.data = append(kernel.data, cout) 121 | } 122 | } 123 | 124 | if DEBUG { 125 | fmt.Printf("size = %v * %v (fft size = %v)\n", len(kernel.data), len(kernel.data[0]), p.fftSize) 126 | } 127 | 128 | // finalizeKernel 129 | 130 | // calculate weight for normalisation 131 | wx1 := maxidx(kernel.data[0]) 132 | wx2 := maxidx(kernel.data[len(kernel.data)-1]) 133 | 134 | subset := make([][]complex128, len(kernel.data), len(kernel.data)) 135 | for i := 0; i < len(kernel.data); i++ { 136 | subset[i] = make([]complex128, 0, wx2-wx1+1) 137 | } 138 | for j := wx1; j <= wx2; j++ { 139 | for i := 0; i < len(kernel.data); i++ { 140 | subset[i] = append(subset[i], kernel.data[i][j]) 141 | } 142 | } 143 | 144 | // Massive hack - precalculate above instead :( 145 | nrows, ncols := len(subset), len(subset[0]) 146 | 147 | square := make([][]complex128, ncols, ncols) // conjugate transpose of subset * subset 148 | for i := 0; i < ncols; i++ { 149 | square[i] = make([]complex128, ncols, ncols) 150 | } 151 | 152 | for j := 0; j < ncols; j++ { 153 | for i := 0; i < ncols; i++ { 154 | v := complex(0, 0) 155 | for k := 0; k < nrows; k++ { 156 | v += subset[k][i] * cmplx.Conj(subset[k][j]) 157 | } 158 | square[i][j] = v 159 | } 160 | } 161 | 162 | wK := []float64{} 163 | for i := int(1.0/q + 0.5); i < ncols-int(1.0/q+0.5)-2; i++ { 164 | wK = append(wK, cmplx.Abs(square[i][i])) 165 | } 166 | 167 | weight := float64(p.fftHop) / float64(p.fftSize) 168 | if len(wK) > 0 { 169 | weight /= mean(wK) 170 | } 171 | weight = math.Sqrt(weight) 172 | 173 | if DEBUG { 174 | fmt.Printf("weight = %v (from %v elements in wK, ncols = %v, q = %v)\n", 175 | weight, len(wK), ncols, q) 176 | } 177 | 178 | // apply normalisation weight, make sparse, and store conjugate 179 | // (we use the adjoint or conjugate transpose of the kernel matrix 180 | // for the forward transform, the plain kernel for the inverse 181 | // which we expect to be less common) 182 | 183 | sk := Kernel{ 184 | make([]int, len(kernel.data), len(kernel.data)), 185 | make([][]complex128, len(kernel.data), len(kernel.data)), 186 | } 187 | for i := 0; i < len(kernel.data); i++ { 188 | sk.origin[i] = 0 189 | sk.data[i] = []complex128{} 190 | 191 | lastNZ := 0 192 | for j := len(kernel.data[i]) - 1; j >= 0; j-- { 193 | if cmplx.Abs(kernel.data[i][j]) != 0 { 194 | lastNZ = j 195 | break 196 | } 197 | } 198 | 199 | haveNZ := false 200 | for j := 0; j <= lastNZ; j++ { 201 | if haveNZ || cmplx.Abs(kernel.data[i][j]) != 0 { 202 | if !haveNZ { 203 | sk.origin[i] = j 204 | } 205 | haveNZ = true 206 | sk.data[i] = append(sk.data[i], 207 | complexTimes(cmplx.Conj(kernel.data[i][j]), weight)) 208 | } 209 | } 210 | } 211 | 212 | return &CQKernel{p, &sk} 213 | } 214 | 215 | func (k *CQKernel) processForward(cv []complex128) []complex128 { 216 | // straightforward matrix multiply (taking into account m_kernel's 217 | // slightly-sparse representation) 218 | 219 | if len(k.kernel.data) == 0 { 220 | panic("Whoops - return empty array? is this even possible?") 221 | } 222 | 223 | nrows := k.Properties.binsPerOctave * k.Properties.atomsPerFrame 224 | 225 | rv := make([]complex128, nrows, nrows) 226 | for i := 0; i < nrows; i++ { 227 | // rv[i] = complex(0, 0) 228 | for j := 0; j < len(k.kernel.data[i]); j++ { 229 | rv[i] += cv[j+k.kernel.origin[i]] * k.kernel.data[i][j] 230 | } 231 | } 232 | return rv 233 | } 234 | 235 | func (k *CQKernel) ProcessInverse(cv []complex128) []complex128 { 236 | // matrix multiply by conjugate transpose of m_kernel. This is 237 | // actually the original kernel as calculated, we just stored the 238 | // conjugate-transpose of the kernel because we expect to be doing 239 | // more forward transforms than inverse ones. 240 | if len(k.kernel.data) == 0 { 241 | panic("Whoops - return empty array? is this even possible?") 242 | } 243 | 244 | ncols := k.Properties.binsPerOctave * k.Properties.atomsPerFrame 245 | nrows := k.Properties.fftSize 246 | 247 | rv := make([]complex128, nrows, nrows) 248 | for j := 0; j < ncols; j++ { 249 | i0 := k.kernel.origin[j] 250 | i1 := i0 + len(k.kernel.data[j]) 251 | for i := i0; i < i1; i++ { 252 | rv[i] += cv[j] * cmplx.Conj(k.kernel.data[j][i-i0]) 253 | } 254 | } 255 | return rv 256 | } 257 | 258 | func (k *CQKernel) BinCount() int { 259 | return k.Properties.octaves * k.Properties.binsPerOctave 260 | } 261 | 262 | func makeWindow(window Window, len int) []float64 { 263 | if len <= 0 { 264 | panic("Window too small!") 265 | } 266 | 267 | if window != SqrtBlackmanHarris { 268 | // HACK - support more? 269 | panic("Only SqrtBlackmanHarris window supported currently") 270 | } 271 | 272 | win := make([]float64, len-1, len-1) 273 | for i := 0; i < len-1; i++ { 274 | win[i] = 1.0 275 | } 276 | 277 | // Blackman Harris 278 | 279 | n := float64(len - 1) 280 | for i := 0; i < len-1; i++ { 281 | win[i] = win[i] * (0.35875 - 282 | 0.48829*math.Cos(2.0*math.Pi*float64(i)/n) + 283 | 0.14128*math.Cos(4.0*math.Pi*float64(i)/n) - 284 | 0.01168*math.Cos(6.0*math.Pi*float64(i)/n)) 285 | } 286 | 287 | win = append(win, win[0]) 288 | 289 | switch window { 290 | case SqrtBlackmanHarris: 291 | fallthrough 292 | case SqrtBlackman: 293 | fallthrough 294 | case SqrtHann: 295 | for i, v := range win { 296 | win[i] = math.Sqrt(v) / float64(len) 297 | } 298 | 299 | case BlackmanHarris: 300 | fallthrough 301 | case Blackman: 302 | fallthrough 303 | case Hann: 304 | for i, v := range win { 305 | win[i] = v / float64(len) 306 | } 307 | } 308 | 309 | return win 310 | } 311 | -------------------------------------------------------------------------------- /cq/spectrogram.go: -------------------------------------------------------------------------------- 1 | package cq 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/cmplx" 7 | ) 8 | 9 | const ( 10 | TAU = 2.0 * math.Pi 11 | ) 12 | 13 | type Spectrogram struct { 14 | cq *ConstantQ 15 | 16 | buffer [][]complex128 17 | prevColumn []complex128 18 | } 19 | 20 | func NewSpectrogram(params CQParams) *Spectrogram { 21 | return &Spectrogram{ 22 | NewConstantQ(params), 23 | make([][]complex128, 0, 128), 24 | make([]complex128, 0, 0), 25 | } 26 | } 27 | 28 | func (spec *Spectrogram) ProcessChannel(samples <-chan float64) <-chan []complex128 { 29 | return spec.InterpolateCQChannel(spec.cq.ProcessChannel(samples)) 30 | } 31 | 32 | func (spec *Spectrogram) InterpolateCQChannel(columns <-chan []complex128) <-chan []complex128 { 33 | result := make(chan []complex128) 34 | 35 | go func() { 36 | height := spec.cq.BinCount() 37 | buffer := make([][]complex128, 0, 128) // HACK - get the correct size 38 | 39 | first := false 40 | for column := range columns { 41 | buffer = append(buffer, column) 42 | if first { 43 | if len(column) != height { 44 | panic("First partial info must be for all values.") 45 | } 46 | first = false 47 | } else { 48 | if len(column) == height { 49 | full := spec.fullInterpolate(buffer) 50 | for _, ic := range full { 51 | result <- ic 52 | } 53 | buffer = buffer[len(buffer)-1:] 54 | } 55 | } 56 | } 57 | 58 | if len(buffer[0]) != height { 59 | panic("Oops - can't interpolate the ending part, wrong height :/") 60 | } 61 | for _, ic := range holdInterpolate(buffer) { 62 | result <- ic 63 | } 64 | close(result) 65 | }() 66 | 67 | return result 68 | } 69 | 70 | func (spec *Spectrogram) Process(values []float64) [][]complex128 { 71 | return spec.interpolate(spec.cq.Process(values), false) 72 | } 73 | 74 | func (spec *Spectrogram) GetRemainingOutput() [][]complex128 { 75 | return spec.interpolate(spec.cq.GetRemainingOutput(), true) 76 | } 77 | 78 | // Post process by writing to linear interpolator 79 | func (spec *Spectrogram) interpolate(cq [][]complex128, insist bool) [][]complex128 { 80 | // TODO: make copy here? currently we copy elsewhere. 81 | spec.buffer = append(spec.buffer, cq...) 82 | return spec.fetchInterpolated(insist) 83 | } 84 | 85 | // Interpolate by copying from the previous column 86 | func (spec *Spectrogram) fetchHold() [][]complex128 { 87 | width := len(spec.buffer) 88 | height := spec.cq.BinCount() 89 | 90 | out := make([][]complex128, width, width) 91 | 92 | for i := 0; i < width; i++ { 93 | col := spec.buffer[i] 94 | 95 | thisHeight, prevHeight := len(col), len(spec.prevColumn) 96 | for j := thisHeight; j < height; j++ { // TODO - collapse into two copies, not a for loop. 97 | if j < prevHeight { 98 | col = append(col, spec.prevColumn[j]) 99 | } else { 100 | col = append(col, complex(0, 0)) 101 | } 102 | } 103 | 104 | spec.prevColumn = col 105 | out[i] = col 106 | } 107 | 108 | spec.buffer = make([][]complex128, 0, 256) 109 | return out 110 | } 111 | 112 | func (spec *Spectrogram) fetchInterpolated(insist bool) [][]complex128 { 113 | width := len(spec.buffer) 114 | height := spec.cq.BinCount() 115 | 116 | if width == 0 { 117 | return make([][]complex128, 0, 0) 118 | } 119 | 120 | firstFullHeight, secondFullHeight := -1, -1 121 | 122 | for i := 0; i < width; i++ { 123 | if len(spec.buffer[i]) == height { 124 | if firstFullHeight == -1 { 125 | firstFullHeight = i 126 | } else if secondFullHeight == -1 { 127 | secondFullHeight = i 128 | break 129 | } 130 | } 131 | } 132 | 133 | if firstFullHeight > 0 { 134 | // Stuff at the start we can't interpolate. Copy that verbatim, and recurse 135 | out := spec.buffer[:firstFullHeight] 136 | spec.buffer = spec.buffer[firstFullHeight:] 137 | return append(out, spec.fetchInterpolated(insist)...) 138 | } else if firstFullHeight < 0 || secondFullHeight < 0 { 139 | // Wait until we have somethinng we can interpolate... 140 | if insist { 141 | return spec.fetchHold() 142 | } else { 143 | return make([][]complex128, 0, 0) 144 | } 145 | } else { 146 | // firstFullHeight == 0 and secondFullHeight also valid. Can interpolate 147 | out := spec.fullInterpolate(spec.buffer[:secondFullHeight+1]) 148 | spec.buffer = spec.buffer[secondFullHeight:] 149 | return append(out, spec.fetchInterpolated(insist)...) 150 | } 151 | } 152 | 153 | func (spec *Spectrogram) fullInterpolate(values [][]complex128) [][]complex128 { 154 | // Last entry is the interpolation end boundary, hence the -1 155 | width, height := len(values)-1, len(values[0]) 156 | bpo := spec.cq.bpo() 157 | 158 | if height != len(values[width]) { 159 | fmt.Printf("interpolateInPlace requires start and end arrays to be the same (full) size, %d != %d\n", 160 | len(values[0]), len(values[width])) 161 | panic("IAE to interpolateInPlace") 162 | } 163 | 164 | result := make([][]complex128, width, width) 165 | for i := 0; i < width; i++ { 166 | result[i] = make([]complex128, height, height) 167 | copy(result[i], values[i]) 168 | } 169 | 170 | // For each height... 171 | for y := 0; y < height; y++ { 172 | // spacing = index offset to next column bigger than that height (y) 173 | spacing := width 174 | for i := 1; i < width; i++ { 175 | thisHeight := len(values[i]) 176 | if thisHeight > height { 177 | panic("interpolateInPlace requires the first column to be the highest") 178 | } 179 | if thisHeight > y { 180 | spacing = i 181 | break // or: remove, and convert to i < spacing in for loop? 182 | } 183 | } 184 | 185 | if spacing < 2 { 186 | continue 187 | } 188 | thisOctave, lastOctave := y, y-bpo 189 | if lastOctave < 0 { 190 | panic("Oops, can't interpolate in the first octave?") 191 | } 192 | 193 | for i := 0; i+spacing <= width; i += spacing { 194 | // NOTE: can't use result[] instead of values[] here, as result[] doesn't include the full right column yet. 195 | thisStart, thisEnd := values[i][thisOctave], values[i+spacing][thisOctave] 196 | 197 | lastPhaseStart := cmplx.Phase(values[i][lastOctave]) 198 | lastPhaseAt := lastPhaseStart 199 | for j := 1; j < spacing; j++ { 200 | lastPhaseAt = makeCloser(lastPhaseAt, cmplx.Phase(result[i+j][lastOctave])) 201 | } 202 | totalLastPhaseDiff := lastPhaseAt - lastPhaseStart 203 | 204 | // Tweak this to allow the slope of the lower octave phase change to differ from the higher octave's 205 | upperScale := 0.5 206 | targetLastPhase := makeCloser(upperScale*totalLastPhaseDiff, cmplx.Phase(thisEnd)-cmplx.Phase(thisStart)) 207 | diffScale := 0.0 208 | if math.Abs(totalLastPhaseDiff) > 1e-5 { 209 | diffScale = targetLastPhase / totalLastPhaseDiff 210 | } 211 | 212 | lastPhaseAt = lastPhaseStart 213 | for j := 1; j < spacing; j++ { 214 | lastPhaseAt := makeCloser(lastPhaseAt, cmplx.Phase(result[i+j][lastOctave])) 215 | proportion := float64(j) / float64(spacing) 216 | interpolated := logInterpolate(thisStart, thisEnd, proportion, lastPhaseStart+(lastPhaseAt-lastPhaseStart)*diffScale) 217 | result[i+j][y] = interpolated 218 | } 219 | } 220 | } 221 | 222 | return result 223 | } 224 | 225 | func logInterpolate(this1, thisN complex128, proportion float64, interpolatedPhase float64) complex128 { 226 | if cmplx.Abs(this1) < cmplx.Abs(thisN) { 227 | return logInterpolate(thisN, this1, 1-proportion, interpolatedPhase) 228 | } 229 | 230 | // Simple linear interpolation for DB power. 231 | z := thisN / this1 232 | zLogAbs := math.Log(cmplx.Abs(z)) 233 | cLogAbs := zLogAbs * proportion 234 | cAbs := math.Exp(cLogAbs) 235 | 236 | return cmplx.Rect(cAbs*cmplx.Abs(this1), interpolatedPhase) 237 | } 238 | 239 | // Return the closest number X to toShift, such that X mod Tau == modTwoPi 240 | func makeCloser(toShift, modTau float64) float64 { 241 | if math.IsNaN(modTau) { 242 | modTau = 0.0 243 | } 244 | // Minimize |toShift - (modTau + tau * cyclesToAdd)| 245 | // toShift - modTau - tau * CTA = 0 246 | cyclesToAdd := (toShift - modTau) / TAU 247 | return modTau + float64(Round(cyclesToAdd))*TAU 248 | } 249 | 250 | func holdInterpolate(values [][]complex128) [][]complex128 { 251 | // Hmm...maybe instead interpolate to zeroes? 252 | width, height := len(values), len(values[0]) 253 | for i := 1; i < width; i++ { 254 | from := len(values[i]) 255 | if from >= height { 256 | panic("hold interpolate input has wrong structure :(") 257 | } 258 | values[i] = append(values[i], values[i-1][from:height]...) 259 | } 260 | return values 261 | } 262 | 263 | // HACK 264 | func Round(value float64) int64 { 265 | if value < 0.0 { 266 | value -= 0.5 267 | } else { 268 | value += 0.5 269 | } 270 | return int64(value) 271 | } 272 | -------------------------------------------------------------------------------- /cq/utils.go: -------------------------------------------------------------------------------- 1 | package cq 2 | 3 | import ( 4 | "encoding/binary" 5 | // "fmt" 6 | "io" 7 | "math" 8 | "math/cmplx" 9 | ) 10 | 11 | func GenerateHeights(octaves int) func() int { 12 | limit := unsafeShift(octaves - 1) 13 | at := limit 14 | return func() int { 15 | result := TerminalZeros(at) + 1 16 | if at == limit { 17 | at = 0 18 | } 19 | at++ 20 | return result 21 | } 22 | } 23 | 24 | func TerminalZeros(val int) int { 25 | cnt := 0 26 | for ; (val & 1) == 0; cnt++ { 27 | val /= 2 28 | } 29 | return cnt 30 | } 31 | 32 | // Math Utils 33 | 34 | func mean(fs []float64) float64 { 35 | s := 0.0 36 | for _, v := range fs { 37 | s += v 38 | } 39 | return s / float64(len(fs)) 40 | } 41 | 42 | func maxidx(row []complex128) int { 43 | idx, max := 0, cmplx.Abs(row[0]) 44 | for i, v := range row { 45 | vAbs := cmplx.Abs(v) 46 | if vAbs > max { 47 | idx, max = i, vAbs 48 | } 49 | } 50 | return idx 51 | } 52 | 53 | func complexTimes(c complex128, f float64) complex128 { 54 | return complex(real(c)*f, imag(c)*f) 55 | } 56 | 57 | // IsPowerOf2 returns true if x is a power of 2, else false. 58 | func isPowerOf2(x int) bool { 59 | return x&(x-1) == 0 60 | } 61 | 62 | // NextPowerOf2 returns the next power of 2 >= x. 63 | func nextPowerOf2(x int) int { 64 | if isPowerOf2(x) { 65 | return x 66 | } 67 | return int(math.Pow(2, math.Ceil(math.Log2(float64(x))))) 68 | } 69 | 70 | func round(x float64) int { 71 | return int(x + 0.5) 72 | } 73 | func roundUp(x float64) int { 74 | return int(math.Ceil(x)) 75 | } 76 | func roundDown(x float64) int { 77 | return int(x) 78 | } 79 | 80 | func unsafeShift(s int) int { 81 | return 1 << uint(s) 82 | } 83 | 84 | // TODO: Move into global exported utils 85 | func UnsafeShift(s int) int { 86 | return unsafeShift(s) 87 | } 88 | 89 | func clampUnit(v float64) float64 { 90 | switch { 91 | case v > 1.0: 92 | return 1.0 93 | case v < -1.0: 94 | return -1.0 95 | default: 96 | return v 97 | } 98 | } 99 | 100 | func realParts(values []complex128) []float64 { 101 | n := len(values) 102 | reals := make([]float64, n, n) 103 | for i, c := range values { 104 | reals[i] = real(c) 105 | } 106 | return reals 107 | } 108 | 109 | func minInt(a, b int) int { 110 | if a < b { 111 | return a 112 | } else { 113 | return b 114 | } 115 | } 116 | func maxInt(a, b int) int { 117 | if a > b { 118 | return a 119 | } else { 120 | return b 121 | } 122 | } 123 | 124 | func calcGcd(a int, b int) int { 125 | if b == 0 { 126 | return a 127 | } 128 | return calcGcd(b, a%b) 129 | } 130 | 131 | func bessel0(x float64) float64 { 132 | b := 0.0 133 | for i := 0; i < 20; i++ { 134 | b += besselTerm(x, i) 135 | } 136 | return b 137 | } 138 | 139 | func besselTerm(x float64, i int) float64 { 140 | if i == 0 { 141 | return 1.0 142 | } 143 | f := float64(factorial(i)) 144 | return math.Pow(x/2.0, float64(i)*2.0) / (f * f) 145 | } 146 | 147 | func factorial(i int) int { 148 | if i == 0 { 149 | return 1 150 | } 151 | return i * factorial(i-1) 152 | } 153 | 154 | // IO Utils 155 | 156 | func WriteComplexArray(w io.Writer, array []complex128) { 157 | // Uncomment for a weird effect going through the writecq -> readcq cycle 158 | // WriteInt32(w, int32(len(array))) 159 | for _, c := range array { 160 | WriteComplex(w, c) 161 | } 162 | } 163 | 164 | func WriteComplex(w io.Writer, c complex128) { 165 | WriteFloat32(w, float32(real(c))) 166 | WriteFloat32(w, float32(imag(c))) 167 | } 168 | 169 | func WriteInt32(w io.Writer, i int32) { 170 | binary.Write(w, binary.LittleEndian, i) 171 | } 172 | 173 | func WriteFloat32(w io.Writer, f float32) { 174 | binary.Write(w, binary.LittleEndian, f) 175 | } 176 | 177 | func WriteByte(w io.Writer, b byte) { 178 | binary.Write(w, binary.LittleEndian, b) 179 | } 180 | 181 | func ReadComplexArray(r io.Reader, size int) []complex128 { 182 | array := make([]complex128, size, size) 183 | for i := 0; i < size; i++ { 184 | array[i] = ReadComplex(r) 185 | } 186 | return array 187 | } 188 | 189 | func ReadComplex(r io.Reader) complex128 { 190 | f1, f2 := ReadFloat32(r), ReadFloat32(r) 191 | return complex(float64(f1), float64(f2)) 192 | } 193 | 194 | func ReadFloat32(r io.Reader) float32 { 195 | var f float32 196 | binary.Read(r, binary.LittleEndian, &f) 197 | return f 198 | } 199 | -------------------------------------------------------------------------------- /cqspectrogram.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/padster/go-sound/cq" 10 | f "github.com/padster/go-sound/file" 11 | "github.com/padster/go-sound/output" 12 | s "github.com/padster/go-sound/sounds" 13 | "github.com/padster/go-sound/util" 14 | ) 15 | 16 | // Generates the golden files. See test/sounds_test.go for actual test. 17 | func main() { 18 | // Needs to be at least 2 when doing openGL + sound output at the same time. 19 | runtime.GOMAXPROCS(6) 20 | 21 | sampleRate := s.CyclesPerSecond 22 | octaves := flag.Int("octaves", 7, "Range in octaves") 23 | minFreq := flag.Float64("minFreq", 55.0, "Minimum frequency") 24 | bpo := flag.Int("bpo", 24, "Buckets per octave") 25 | flag.Parse() 26 | 27 | remainingArgs := flag.Args() 28 | argCount := len(remainingArgs) 29 | if argCount < 1 || argCount > 2 { 30 | panic("Required: [] filename arguments") 31 | } 32 | inputFile := remainingArgs[0] 33 | outputFile := "" 34 | if argCount == 2 { 35 | outputFile = remainingArgs[1] 36 | } 37 | 38 | // minFreq, maxFreq, bpo := 110.0, 14080.0, 24 39 | params := cq.NewCQParams(sampleRate, *octaves, *minFreq, *bpo) 40 | spectrogram := cq.NewSpectrogram(params) 41 | 42 | inputSound := f.Read(inputFile) 43 | inputSound.Start() 44 | defer inputSound.Stop() 45 | 46 | startTime := time.Now() 47 | if outputFile != "" { 48 | columns := spectrogram.ProcessChannel(inputSound.GetSamples()) 49 | // Write to file 50 | f.WriteColumns(outputFile, columns) 51 | } else { 52 | // No file, so play and show instead: 53 | soundChannel, specChannel := splitChannel(inputSound.GetSamples()) 54 | go func() { 55 | columns := spectrogram.ProcessChannel(specChannel) 56 | toShow := util.NewSpectrogramScreen(882, *bpo**octaves, *bpo) 57 | toShow.Render(columns, 1) 58 | }() 59 | output.Play(s.WrapChannelAsSound(soundChannel)) 60 | } 61 | 62 | elapsedSeconds := time.Since(startTime).Seconds() 63 | fmt.Printf("elapsed time (not counting init): %f sec\n", elapsedSeconds) 64 | 65 | if outputFile == "" { 66 | // Hang around to the view can be looked at. 67 | for { 68 | } 69 | } 70 | } 71 | 72 | // HACK - move to utils, support in both main apps. 73 | func floatFrom16bit(input int32) float64 { 74 | return float64(input) / (float64(1<<15) - 1.0) // Hmmm..doesn't seem right? 75 | } 76 | func int16FromFloat(input float64) int32 { 77 | return int32(input * (float64(1<<15) - 1.0)) 78 | } 79 | 80 | func floatFrom24bit(input int32) float64 { 81 | return float64(input) / (float64(1<<23) - 1.0) // Hmmm..doesn't seem right? 82 | } 83 | func int24FromFloat(input float64) int32 { 84 | return int32(input * (float64(1<<23) - 1.0)) 85 | } 86 | 87 | func splitChannel(samples <-chan float64) (chan float64, chan float64) { 88 | r1, r2 := make(chan float64), make(chan float64) 89 | go func() { 90 | for s := range samples { 91 | r1 <- s 92 | r2 <- s 93 | } 94 | close(r1) 95 | close(r2) 96 | }() 97 | return r1, r2 98 | } 99 | -------------------------------------------------------------------------------- /demo.go: -------------------------------------------------------------------------------- 1 | // Demo usage of the go-sound Sounds library, to play Clair de Lune. 2 | package main 3 | 4 | import ( 5 | // "flag" 6 | "fmt" 7 | "math" 8 | "runtime" 9 | 10 | // "github.com/padster/go-sound/cq" 11 | // file "github.com/padster/go-sound/file" 12 | "github.com/padster/go-sound/output" 13 | s "github.com/padster/go-sound/sounds" 14 | "github.com/padster/go-sound/util" 15 | 16 | pm "github.com/rakyll/portmidi" 17 | ) 18 | 19 | const ( 20 | // ~65 bpm ~= 927 ms/b ~= 309 ms/quaver (in 9/8) 21 | q = float64(309) 22 | ) 23 | 24 | // Notes in a treble clef, centered on B (offset 8) 25 | var trebleMidi = [...]int{57, 59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81, 83, 84} 26 | 27 | // Key is D♭-Major = five flats: A♭, B♭, D♭, E♭, G♭ 28 | var trebleKeys = [...]int{-1, -1, 00, -1, -1, 00, -1, -1, -1, 00, -1, -1, 00, -1, -1, -1, 00} 29 | 30 | func main() { 31 | // NOTE: Not required, but shows how this can run on multiple threads. 32 | runtime.GOMAXPROCS(3) 33 | 34 | // Switch on whichever demo you'd like here: 35 | if false { 36 | renderMidi() 37 | } else { 38 | playClairDeLune() 39 | } 40 | } 41 | 42 | // renderMidi reads a midi device, converts it to a live sound, and renders the waveform to screen. 43 | func renderMidi() { 44 | fmt.Println("Loading portmidi...") 45 | pm.Initialize() 46 | 47 | midi := s.NewMidiInput(pm.DeviceID(3)) // ++See below 48 | fmt.Printf("Rendering midi...\n") 49 | 50 | /* 51 | // Note: to find your device id, use a version of this: 52 | for i := 1; i <= pm.CountDevices(); i++ { 53 | fmt.Printf("Reading device: %d\n", i) 54 | di := pm.GetDeviceInfo(pm.DeviceId(i)) 55 | fmt.Printf("Info = %v\n", di) 56 | } 57 | */ 58 | 59 | // Render the generated sine wave to screen: 60 | // output.Render(midi, 2000, 500, 1) 61 | // ...or, play it live: 62 | output.Play(midi) 63 | 64 | /* 65 | sampleRate := s.CyclesPerSecond 66 | octaves := 7 67 | minFreq := flag.Float64("minFreq", 55.0, "minimum frequency") 68 | maxFreq := flag.Float64("maxFreq", 55.0*float64(cq.UnsafeShift(octaves)), "maximum frequency") 69 | bpo := flag.Int("bpo", 48, "Buckets per octave") 70 | flag.Parse() 71 | 72 | params := cq.NewCQParams(sampleRate, *minFreq, *maxFreq, *bpo) 73 | spectrogram := cq.NewSpectrogram(params) 74 | 75 | midi.Start() 76 | defer midi.Stop() 77 | 78 | columns := spectrogram.ProcessChannel(midi.GetSamples()) 79 | toShow := util.NewSpectrogramScreen(882, *bpo*octaves, *bpo) 80 | toShow.Render(columns, 1) 81 | */ 82 | } 83 | 84 | // playClairDeLune builds then plays Clair de Lune (Debussey) 85 | // music from http://www.piano-midi.de/noten/debussy/deb_clai.pdf 86 | func playClairDeLune() { 87 | fmt.Println("Building sound.") 88 | 89 | finalNoteLength := float64(3 + 6) // 6 extra beats, just for effect 90 | 91 | // Left-hand split for a bit near the end. 92 | rh1 := s.ConcatSounds( 93 | notesT(7, fs(1)), 94 | notesTQRun(0, 1, 0, 3, 0, -1, 0), 95 | notesT(2, fs(-1)), notesTQRun(-2, -1), notesT(3, fs(-2)), notesT(finalNoteLength, fs(-3)), 96 | ) 97 | rh2 := s.ConcatSounds( 98 | notesT(6, fs(-1)), 99 | notesT(6, fs(-2)), notesT(3, fs(-2)), 100 | notesT(6, fs(-4)), notesT(finalNoteLength, fs(-4)), 101 | ) 102 | 103 | // Split of couplets over long Bb 104 | couplets := s.SumSounds( 105 | s.ConcatSounds(notesT(1.5, fs(2)), notesT(3, fs(4)), notesT(2.5, fs(2))), 106 | notesT(7, fs(0)), 107 | ) 108 | 109 | // Top half of the score: 110 | rightHand := s.ConcatSounds( 111 | rest(2), notesT(4, fs(4, 6)), notesT(4, fs(2, 4)), 112 | notesT(1, fs(1, 3)), notesT(1, fs(2, 4)), notesT(7, fs(1, 3)), 113 | notesT(1, fs(0, 2)), notesT(1, fs(1, 3)), couplets, 114 | notesT(1, fs(-1, 1)), notesT(1, fs(0, 2)), s.SumSounds(rh1, rh2), 115 | ) 116 | 117 | // Bottom half. 118 | leftHand := s.ConcatSounds( 119 | rest(1), notesT(8, fs(-1, -3)), 120 | notesT(9, fs(-0.5, -2)), 121 | notesT(9, fs(-1, -3)), 122 | notesT(9, fs(-2, -4)), 123 | notesT(6, fs(-4, -5)), 124 | notesT(3, fs(-4, -6)), 125 | notesT(6, fs(-5, -7)), // HACK: Actually in bass clef, but rewritten in treble for these two chords. 126 | notesT(finalNoteLength, fs(-6, -7.5)), 127 | ) 128 | 129 | clairDeLune := s.SumSounds(leftHand, rightHand) 130 | 131 | // toPlay := s.NewDenseIIR(clairDeLune, 132 | // []float64{0.8922, -2.677, 2.677, -0.8922}, 133 | // []float64{2.772, -2.57, 0.7961}, 134 | // ) 135 | toPlay := clairDeLune 136 | 137 | // hz := 440.0 138 | // toPlay := s.SumSounds( 139 | // s.NewSineWave(hz), 140 | // s.NewSquareWave(hz), 141 | // s.NewSawtoothWave(hz), 142 | // s.NewTriangleWave(hz), 143 | // ) 144 | // toPlay := s.NewJackInput("go-sound-in") 145 | // toPlay := s.NewTimedSound(s.NewSineWave(500), 1000) 146 | // toPlay := s.SumSounds(s1, s2) 147 | 148 | // toPlay := s.NewTimedSound(shephardTones(), 10000) 149 | // toPlay := file.Read("greatgig.flac") 150 | // file.Write(toPlay, "gg.wav") 151 | // fmt.Printf("Playing: \n\t%s\n", toPlay) 152 | // output.Render(toPlay, 2000, 400) 153 | // output.PlayJack(toPlay) 154 | output.Play(toPlay) 155 | 156 | // output.Play(s.LoadFlacAsSound("toneslide.flac")) 157 | 158 | // Optional: Write to a .wav file: 159 | // clairDeLune.Reset() 160 | // fmt.Println("Writing sound to file.") 161 | // file.Write(clairDeLune, "clairdelune.wav") 162 | 163 | // Optional: Draw to screen: 164 | // clairDeLune.Reset() 165 | // fmt.Println("Drawing sound to screen.") 166 | // output.Render(clairDeLune, 2000, 400) 167 | } 168 | 169 | // fs is a short way to write an array of floats. 170 | func fs(fs ...float64) []float64 { 171 | return fs 172 | } 173 | 174 | // The Sound of silence for quaverCount quavers 175 | func rest(quaverCount float64) s.Sound { 176 | return s.NewTimedSilence(q * quaverCount) 177 | } 178 | 179 | // A chord of notes in the treble clef, 0 = B, then notes up and down (e.g. -4 = E, 4 = F) 180 | // in the proper key (Db major), with +/- 0.5 signifying a sharp or flat. 181 | func notesT(quaverCount float64, notes []float64) s.Sound { 182 | sounds := make([]s.Sound, len(notes), len(notes)) 183 | for i, note := range notes { 184 | sounds[i] = noteTMidi(note, quaverCount) 185 | } 186 | return s.SumSounds(sounds...) 187 | } 188 | 189 | // A run of quavers in the treble clef 190 | func notesTQRun(notes ...float64) s.Sound { 191 | sounds := make([]s.Sound, len(notes), len(notes)) 192 | for i, note := range notes { 193 | sounds[i] = noteTMidi(note, 1.0) 194 | } 195 | return s.ConcatSounds(sounds...) 196 | } 197 | 198 | // Converts a treble note offset to a midi offset 199 | func noteTMidi(note float64, quaverCount float64) s.Sound { 200 | // NOTE: Only [-8, 8] allowed for 'note'. 201 | bFloat, sharp := math.Modf(note) 202 | base := int(bFloat) 203 | if sharp < 0 { 204 | sharp += 1.0 205 | base-- 206 | } 207 | 208 | // 0 = B = offset 8 209 | midi := trebleMidi[base+8] + trebleKeys[base+8] 210 | if sharp > 0.1 { 211 | midi++ 212 | } 213 | 214 | hz := util.MidiToHz(midi) 215 | 216 | // Simple waves: 217 | // asSound := s.NewSquareWave(hz) // Try different types... 218 | // asSound := s.NewSineWave(hz) 219 | // asSound = s.NewADSREnvelope(asSound, 15, 50, 0.5, 20) 220 | 221 | // String-like wave: 222 | asSound := s.NewKarplusStrong(hz, 0.9) 223 | return s.NewTimedSound(asSound, quaverCount*q) 224 | } 225 | 226 | // Shephard tones 227 | func shephardTones() s.Sound { 228 | octaves := 5 229 | base, mid := 110.0, 155.563491861 230 | 231 | tones := 2 * octaves 232 | bases := make([]float64, tones, tones) 233 | for i := 0; i < octaves; i++ { 234 | bases[2*i] = base * float64(unsafeShift(i)) 235 | bases[2*i+1] = mid * float64(unsafeShift(i)) 236 | } 237 | secondsPerOctave := 10 238 | 239 | maxHz := bases[0] * float64(unsafeShift(octaves)) 240 | downOctaves := 1.0 / float64(unsafeShift(octaves)) 241 | 242 | samplesPerOctave := int(secondsPerOctave * s.CyclesPerSecond) 243 | octavesPerSample := 1.0 / float64(samplesPerOctave) 244 | 245 | channels := make([]chan []float64, tones, tones) 246 | for i := 0; i < tones; i++ { 247 | channels[i] = make(chan []float64) 248 | } 249 | go func() { 250 | for { 251 | for sample := 0; sample < octaves*samplesPerOctave; sample++ { 252 | for i := 0; i < tones; i++ { 253 | hz := bases[i] * math.Pow(2.0, float64(sample)*octavesPerSample) 254 | if hz >= maxHz { 255 | hz *= downOctaves 256 | } 257 | channels[i] <- []float64{hz, gaussianAmplitude(hz, bases[0], maxHz)} 258 | } 259 | } 260 | } 261 | }() 262 | 263 | sounds := make([]s.Sound, tones, tones) 264 | for i, v := range channels { 265 | sounds[i] = s.NewHzFromChannelWithAmplitude(v) 266 | } 267 | return s.SumSounds(sounds...) 268 | } 269 | 270 | func gaussianAmplitude(at float64, minHz float64, maxHz float64) float64 { 271 | lHalf := 0.5 * (math.Log(minHz) + math.Log(maxHz)) 272 | diff := (math.Log(at) - lHalf) 273 | return math.Exp(-1.0 * diff * diff) 274 | } 275 | 276 | func unsafeShift(s int) int { 277 | return 1 << uint(s) 278 | } 279 | -------------------------------------------------------------------------------- /fakeflac/flac.go: -------------------------------------------------------------------------------- 1 | package fakeflac 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type Decoder struct { 8 | Rate int 9 | } 10 | 11 | type Encoder struct { 12 | Depth int 13 | Rate int 14 | } 15 | 16 | type Frame struct { 17 | Channels int 18 | Depth int 19 | Rate int 20 | Buffer []int32 21 | } 22 | 23 | func NewDecoder(name string) (d *Decoder, err error) { 24 | return nil, errors.New("Can't use flac decoder with fake flac") 25 | } 26 | 27 | func (d *Decoder) ReadFrame() (f *Frame, err error) { 28 | return nil, errors.New("Can't use flac decoder with fake flac") 29 | } 30 | 31 | func (d *Decoder) Close() { 32 | } 33 | 34 | func NewEncoder(name string, channels int, depth int, rate int) (e *Encoder, err error) { 35 | return nil, errors.New("Can't use flac encoder with fake flac") 36 | } 37 | 38 | func (e *Encoder) WriteFrame(f Frame) (err error) { 39 | return errors.New("Can't use flac encoder with fake flac") 40 | } 41 | 42 | func (e *Encoder) Close() { 43 | } 44 | -------------------------------------------------------------------------------- /features/peaks.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "math/cmplx" 8 | 9 | "github.com/padster/go-sound/cq" 10 | ) 11 | 12 | const ( 13 | THRESHOLD = 1.5 14 | ) 15 | 16 | // PeakDetector takes the constant Q output, and for each sample bin, returns 17 | // whether that sample is a 'peak' in the music. 18 | type PeakDetector struct { 19 | } 20 | 21 | func (pd *PeakDetector) ProcessChannel(samples <-chan []complex128) <-chan []byte { 22 | result := make(chan []byte) 23 | 24 | go func() { 25 | i := 0 26 | for sample := range samples { 27 | if i%10000 == 0 { 28 | fmt.Printf("Writing peaks %d\n", i) 29 | } 30 | i++ 31 | result <- pd.processColumn(sample) 32 | } 33 | close(result) 34 | }() 35 | 36 | return result 37 | } 38 | 39 | func (pd *PeakDetector) processColumn(column []complex128) []byte { 40 | size := len(column) 41 | result := make([]byte, size, size) 42 | 43 | for i, v := range column { 44 | power, _ := cmplx.Polar(v) 45 | if power > THRESHOLD { 46 | result[i] = 1 47 | } else { 48 | result[i] = 0 49 | } 50 | } 51 | 52 | // TODO 53 | return result 54 | } 55 | 56 | func WritePeaks(outputFile string, peaks <-chan []byte) { 57 | ioutil.WriteFile(outputFile, PeaksToBytes(peaks), 0644) 58 | } 59 | 60 | func PeaksToBytes(peaks <-chan []byte) []byte { 61 | outputBuffer := bytes.NewBuffer(make([]byte, 0, 1024)) 62 | width, height := 0, 0 63 | for col := range peaks { 64 | for _, c := range col { 65 | cq.WriteByte(outputBuffer, c) 66 | } 67 | width++ 68 | height = len(col) 69 | } 70 | fmt.Printf("Done! - %d by %d\n", width, height) 71 | return outputBuffer.Bytes() 72 | } 73 | -------------------------------------------------------------------------------- /file/cqfile.go: -------------------------------------------------------------------------------- 1 | package soundfile 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "github.com/padster/go-sound/cq" 9 | ) 10 | 11 | // Writes the result of a constant Q transform to file. 12 | func WriteColumns(outputFile string, columns <-chan []complex128) { 13 | ioutil.WriteFile(outputFile, ColumnsToBytes(columns), 0644) 14 | } 15 | 16 | // Converts the result of a constant Q transform to a byte stream. 17 | func ColumnsToBytes(columns <-chan []complex128) []byte { 18 | outputBuffer := bytes.NewBuffer(make([]byte, 0, 1024)) 19 | width, height := 0, 0 20 | for col := range columns { 21 | for _, c := range col { 22 | cq.WriteComplex(outputBuffer, c) 23 | } 24 | if width%10000 == 0 { 25 | fmt.Printf("At frame: %d\n", width) 26 | } 27 | width++ 28 | height = len(col) 29 | } 30 | fmt.Printf("Done! - %d by %d\n", width, height) 31 | return outputBuffer.Bytes() 32 | } 33 | 34 | // Reads a file and converts back into a CQ channel. 35 | func ReadCQColumns(inputFile string, params cq.CQParams) <-chan []complex128 { 36 | loaded, err := ioutil.ReadFile(inputFile) 37 | if err != nil { 38 | panic("Can't load file " + inputFile) 39 | } 40 | complexEntries := len(loaded) / 8 // complex stored as two float32s. 41 | fmt.Printf("Reading %d entries\n", complexEntries) 42 | 43 | asReader := bytes.NewReader(loaded) 44 | 45 | result := make(chan []complex128) 46 | go func() { 47 | heightGen := cq.GenerateHeights(params.Octaves) 48 | for at := 0; at < complexEntries; { 49 | nextSize := heightGen() * params.BinsPerOctave 50 | result <- cq.ReadComplexArray(asReader, nextSize) 51 | at += nextSize 52 | } 53 | close(result) 54 | }() 55 | return result 56 | } 57 | -------------------------------------------------------------------------------- /file/soundfile.go: -------------------------------------------------------------------------------- 1 | package soundfile 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/padster/go-sound/cq" 8 | o "github.com/padster/go-sound/output" 9 | s "github.com/padster/go-sound/sounds" 10 | ) 11 | 12 | func Read(path string) s.Sound { 13 | switch { 14 | case strings.HasSuffix(path, ".flac"): 15 | return s.LoadFlacAsSound(path) 16 | case strings.HasSuffix(path, ".wav"): 17 | return s.LoadWavAsSound(path, 0 /* TODO: average all channels. */) 18 | default: 19 | panic("Unsupported file type: " + path) 20 | } 21 | } 22 | 23 | func Write(sound s.Sound, path string) { 24 | switch { 25 | case strings.HasSuffix(path, ".flac"): 26 | panic("FLAC support currently broken, please use something else") 27 | o.WriteSoundToFlac(sound, path) 28 | case strings.HasSuffix(path, ".wav"): 29 | o.WriteSoundToWav(sound, path) 30 | default: 31 | panic("Unsupported file type: " + path) 32 | } 33 | } 34 | 35 | func ReadCQ(path string, params cq.CQParams, zip bool) s.Sound { 36 | if zip { 37 | // TODO: Implement 38 | panic("Zip read CQ unsupported for now.") 39 | } 40 | 41 | fmt.Printf("Reading columms from %s\n", path) 42 | cqChannel := ReadCQColumns(path, params) 43 | inverse := cq.NewCQInverse(params) 44 | invChannel := inverse.ProcessChannel(cqChannel) 45 | return s.WrapChannelAsSound(invChannel) 46 | } 47 | -------------------------------------------------------------------------------- /generatetest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/padster/go-sound/output" 7 | "github.com/padster/go-sound/sounds" 8 | "github.com/padster/go-sound/test" 9 | ) 10 | 11 | // Generates the golden files. See test/sounds_test.go for actual test. 12 | func main() { 13 | generate("test/timed_sine.wav", test.SampleTimedSineSound()) 14 | generate("test/timed_square.wav", test.SampleTimedSquareSound()) 15 | generate("test/timed_sawtooth.wav", test.SampleTimedSawtoothSound()) 16 | generate("test/timed_triangle.wav", test.SampleTimedTriangleSound()) 17 | generate("test/silence.wav", test.SampleSilence()) 18 | generate("test/concat.wav", test.SampleConcat()) 19 | generate("test/normalsum.wav", test.SampleNormalSum()) 20 | generate("test/multiply.wav", test.SampleMultiply()) 21 | generate("test/repeat.wav", test.SampleRepeater()) 22 | generate("test/adsr.wav", test.SampleAdsrEnvelope()) 23 | generate("test/sampler.wav", test.SampleSampler()) 24 | generate("test/delay.wav", test.SampleAddDelay()) 25 | generate("test/denseiir.wav", test.SampleDenseIIR()) 26 | } 27 | 28 | func generate(path string, sound sounds.Sound) { 29 | fmt.Printf("Generating sound at %s...\n", path) 30 | if err := output.WriteSoundToWav(sound, path); err != nil { 31 | fmt.Printf(" Skipped %s, path exists. Delete to regenerate.\n", path) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /mashapp/api.go: -------------------------------------------------------------------------------- 1 | package mashapp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | func (s *MashAppServer) serveRPCs() { 10 | s.serveRPC("input/load", s.wrapLoad) 11 | s.serveRPC("input/edit", s.wrapEdit) 12 | s.serveRPC("block/new", s.wrapCreateBlock) 13 | } 14 | 15 | func (s *MashAppServer) serveRPC(path string, handleFunc func(rw http.ResponseWriter, req *http.Request)) { 16 | rpcPath := fmt.Sprintf("/_/%s", path) 17 | fmt.Printf("Adding RPC handler for %s\n", rpcPath) 18 | http.HandleFunc(rpcPath, handleFunc) 19 | } 20 | 21 | // Load input RPC 22 | type LoadRequest struct { 23 | Path string `json:"path"` 24 | } 25 | type LoadResponse struct { 26 | Input InputMeta `json:"meta"` 27 | Samples JsonSamples `json:"samples"` 28 | } 29 | 30 | func (s *MashAppServer) wrapLoad(w http.ResponseWriter, r *http.Request) { 31 | var in LoadRequest 32 | err := json.NewDecoder(r.Body).Decode(&in) 33 | if err != nil { 34 | http.Error(w, err.Error(), http.StatusInternalServerError) 35 | } 36 | out := s.performLoad(in) 37 | js, err := json.Marshal(out) 38 | if err != nil { 39 | http.Error(w, err.Error(), http.StatusInternalServerError) 40 | return 41 | } 42 | w.Header().Set("Content-Type", "application/json") 43 | w.Write(js) 44 | } 45 | func (s *MashAppServer) performLoad(req LoadRequest) LoadResponse { 46 | // TODO: error handling 47 | id, sound := s.state.loadSound(fmt.Sprintf("%s/%s", s.filePath, req.Path)) 48 | 49 | meta := InputMeta{ 50 | id, req.Path, false, /* Muted */ 51 | len(sound.samples), len(sound.samples), /* Duration */ 52 | 0, 0, /* Pitch */ 53 | } 54 | return LoadResponse{meta, floatsToBase64(sound.samples)} 55 | } 56 | 57 | // Edit Input RPC 58 | type EditRequest struct { 59 | Meta InputMeta `json:"meta"` 60 | } 61 | type EditResponse struct { 62 | Input InputMeta `json:"meta"` 63 | Samples JsonSamples `json:"samples"` 64 | } 65 | 66 | func (s *MashAppServer) wrapEdit(w http.ResponseWriter, r *http.Request) { 67 | var in EditRequest 68 | err := json.NewDecoder(r.Body).Decode(&in) 69 | if err != nil { 70 | fmt.Printf("Decode error :(\n") 71 | http.Error(w, err.Error(), http.StatusInternalServerError) 72 | return 73 | } 74 | out := s.performEdit(in) 75 | js, err := json.Marshal(out) 76 | if err != nil { 77 | fmt.Printf("Marshal error :(\n") 78 | http.Error(w, err.Error(), http.StatusInternalServerError) 79 | return 80 | } 81 | w.Header().Set("Content-Type", "application/json") 82 | w.Write(js) 83 | } 84 | func (s *MashAppServer) performEdit(req EditRequest) EditResponse { 85 | // TODO: error handling 86 | newSamples := s.state.shiftInput(req.Meta).samples 87 | return EditResponse{req.Meta, floatsToBase64(newSamples)} 88 | } 89 | 90 | // Create Block RPC 91 | type CreateBlockRequest struct { 92 | Block Block `json:"block"` 93 | } 94 | type CreateBlockResponse struct { 95 | Block Block `json:"block"` 96 | } 97 | 98 | func (s *MashAppServer) wrapCreateBlock(w http.ResponseWriter, r *http.Request) { 99 | var in CreateBlockRequest 100 | err := json.NewDecoder(r.Body).Decode(&in) 101 | if err != nil { 102 | fmt.Printf("Decode error :(\n") 103 | http.Error(w, err.Error(), http.StatusInternalServerError) 104 | return 105 | } 106 | out := s.performCreateBlock(in) 107 | js, err := json.Marshal(out) 108 | if err != nil { 109 | fmt.Printf("Marshal error :(\n") 110 | http.Error(w, err.Error(), http.StatusInternalServerError) 111 | return 112 | } 113 | w.Header().Set("Content-Type", "application/json") 114 | w.Write(js) 115 | } 116 | func (s *MashAppServer) performCreateBlock(req CreateBlockRequest) CreateBlockResponse { 117 | block := s.state.createBlock(req.Block) 118 | return CreateBlockResponse{block} 119 | } 120 | -------------------------------------------------------------------------------- /mashapp/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{.JsonConfig}} 10 | 11 | 12 | -------------------------------------------------------------------------------- /mashapp/model.go: -------------------------------------------------------------------------------- 1 | package mashapp 2 | 3 | import ( 4 | b64 "encoding/base64" 5 | "encoding/binary" 6 | "math" 7 | ) 8 | 9 | type InputMeta struct { 10 | ID int `json:"id,string"` 11 | Path string `json:"path"` 12 | Muted bool `json:"muted"` 13 | 14 | // Init -> Final length in samples 15 | OriginalLength int `json:"originalLength,string"` 16 | FinalLength int `json:"finalLength,string"` 17 | 18 | // Init -> Final pitch in semitones 19 | OriginalPitch int `json:"originalPitch,string"` 20 | FinalPitch int `json:"finalPitch,string"` 21 | } 22 | 23 | type Block struct { 24 | ID int `json:"id"` 25 | InputID int `json:"inputId"` 26 | Name string `json:"name"` 27 | 28 | StartSample int `json:"startSample"` 29 | EndSample int `json:"endSample"` 30 | 31 | // TODO - use? 32 | Selected bool `json:"selected"` 33 | } 34 | 35 | type OutputMeta struct { 36 | ID int `json:"id,string"` 37 | BlockID int `json:"blockID,string"` 38 | 39 | Line int `json:"line,string"` 40 | Muted bool `json:"muted"` 41 | 42 | // NOTE: TimeShift if Duration() != Duration(Block) 43 | StartSample int `json:"startSample,string"` 44 | EndSample int `json:"endSample,string"` 45 | 46 | // TODO - use? 47 | Selected bool `json:"selected"` 48 | 49 | Changes []Modification `json:"changes"` 50 | } 51 | 52 | type Modification struct { 53 | Type int `json:"type,string"` 54 | Params []float64 `json:"params"` 55 | } 56 | 57 | type GoSamples []float64 58 | type JsonSamples string 59 | 60 | // UTIL 61 | func floatsToBase64(values GoSamples) JsonSamples { 62 | asFloats := ([]float64)(values) 63 | return JsonSamples(bytesToBase64(floatsToBytes(asFloats))) 64 | } 65 | 66 | func floatsToBytes(values []float64) []byte { 67 | bytes := make([]byte, 0, 4*len(values)) 68 | for _, v := range values { 69 | bytes = append(bytes, float32ToBytes(float32(v))...) 70 | } 71 | return bytes 72 | } 73 | 74 | func float32ToBytes(value float32) []byte { 75 | bits := math.Float32bits(value) 76 | bytes := make([]byte, 4) 77 | binary.LittleEndian.PutUint32(bytes, bits) 78 | return bytes 79 | } 80 | 81 | func bytesToBase64(values []byte) string { 82 | return b64.StdEncoding.EncodeToString(values) 83 | } 84 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/edit-track-dialog/edit-track-dialog-style.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/edit-track-dialog/edit-track-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | Edit track 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/edit-track-dialog/edit-track-dialog.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | Polymer({ 4 | is: 'edit-track-dialog', 5 | 6 | properties: { 7 | details: { 8 | type: Array, 9 | value: [], 10 | }, 11 | 12 | callback: Object, 13 | }, 14 | 15 | open: function(cb) { 16 | this.callback = cb; 17 | this.$.dialog.open(); 18 | }, 19 | 20 | handleSave: function() { 21 | // PICK: Whitelist properties? 22 | var dataClone = $.extend(true, {}, this.details); 23 | this.handleResult(dataClone); 24 | }, 25 | 26 | handleClose: function() { 27 | this.handleResult(null); 28 | }, 29 | 30 | handleResult: function(result) { 31 | this.callback(result); 32 | this.callback = null; 33 | this.$.dialog.close(); 34 | }, 35 | }); 36 | 37 | })(); 38 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/loadfile-dialog/loadfile-dialog-style.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/loadfile-dialog/loadfile-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | Load a file... 25 | 26 | 27 | 28 | 29 | 30 | [[item]] 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/loadfile-dialog/loadfile-dialog.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | Polymer({ 4 | is: 'loadfile-dialog', 5 | 6 | properties: { 7 | options: { 8 | type: Array, 9 | value: [], 10 | }, 11 | 12 | callback: Object, 13 | }, 14 | 15 | open: function(cb) { 16 | this.callback = cb; 17 | this.$.dialog.open(); 18 | }, 19 | 20 | handleUploadFile: function() { 21 | this.handleResult(this.$.filePath.selectedItem.innerText.trim()); 22 | }, 23 | 24 | handleClose: function() { 25 | this.handleResult(null); 26 | }, 27 | 28 | handleResult: function(result) { 29 | this.callback(result); 30 | this.callback = null; 31 | this.$.dialog.close(); 32 | }, 33 | }); 34 | 35 | })(); 36 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/mash-app/mash-app-style.html: -------------------------------------------------------------------------------- 1 | 90 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/mash-app/mash-app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Blocks 38 | 39 | 40 | 41 | 42 | [[item.name]] 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Inputs 51 | Output 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Load input track 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Add output track 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/play-controls/play-controls-style.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/play-controls/play-controls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Zoom: {{zoomValue}} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/play-controls/play-controls.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | Polymer({ 4 | is: 'play-controls', 5 | 6 | properties: { 7 | isPlaying: Boolean, 8 | zoomValue: { 9 | type: Number, 10 | value: 7, 11 | } 12 | }, 13 | 14 | attached: function() { 15 | this.handleZoomChange(); 16 | }, 17 | 18 | handleFastRewind: function(e) { 19 | util.performAction('fast-rewind', null, this); 20 | }, 21 | 22 | handlePlayButton: function(e) { 23 | util.performAction('play', null, this); 24 | }, 25 | 26 | handleZoomChange: function(e) { 27 | util.performAction('set-zoom', this.$.zoom.value, this); 28 | }, 29 | 30 | handleLoadFile: function(e) { 31 | util.performAction('load-file', null, this); 32 | }, 33 | }); 34 | 35 | })(); 36 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/text-dialog/text-dialog-style.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/text-dialog/text-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | [[title]] 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/text-dialog/text-dialog.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | Polymer({ 4 | is: 'text-dialog', 5 | 6 | properties: { 7 | value: String, 8 | title: String, 9 | callback: Object, 10 | }, 11 | 12 | open: function(cb) { 13 | this.callback = cb; 14 | this.$.dialog.open(); 15 | this.$.value.focus(); 16 | }, 17 | 18 | handleSave: function() { 19 | this.handleResult(this.value); 20 | }, 21 | 22 | handleClose: function() { 23 | this.handleResult(null); 24 | }, 25 | 26 | handleResult: function(result) { 27 | this.callback(result); 28 | this.callback = null; 29 | this.$.dialog.close(); 30 | }, 31 | 32 | handleKey: function(e) { 33 | // Save on Enter. NOTE: Close on Escape is automatic. 34 | if (e.which == 13) { 35 | this.handleSave(); 36 | } 37 | }, 38 | }); 39 | 40 | })(); 41 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/track-line/track-line-style.html: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/track-line/track-line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/util-src/util-src.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mashapp/polymer/app/elements/util-src/util-src.js: -------------------------------------------------------------------------------- 1 | // TODO: Not use a global util if possible with the new module stuff? 2 | window.util = {}; 3 | 4 | (function(M) { 5 | 6 | M.getService = function(serviceName, elt) { 7 | if (!elt) { 8 | return M.whoops("You forgot to pass 'this' to getService :("); 9 | } 10 | var service = elt.fire('get-service', {'service': serviceName}).detail.result; 11 | if (!service) { 12 | return M.whoops("Service " + serviceName + " not registered!"); 13 | } 14 | return service; 15 | } 16 | 17 | M.performAction = function(type, data, elt) { 18 | if (!elt) { 19 | return M.whoops("You forgot to pass 'this' to perform :("); 20 | } 21 | elt.fire('view-action', {'type': type, 'data': data}); 22 | } 23 | 24 | M.asClassAttribute = function(classSet) { 25 | var values = []; 26 | for (var key in classSet) { 27 | if (classSet[key]) { 28 | values.push(key); 29 | } 30 | } 31 | return values.join(" "); 32 | }; 33 | 34 | M.asStyleAttribute = function(styleObject) { 35 | var values = []; 36 | for (var key in styleObject) { 37 | if (styleObject[key] !== null) { 38 | values.push(key + ':' + styleObject[key]); 39 | } 40 | } 41 | return values.join(";"); 42 | }; 43 | 44 | M.momentInRange = function(toTest, startMoment, endMoment) { 45 | return !toTest.isBefore(startMoment) && toTest.isBefore(endMoment); 46 | }; 47 | 48 | M.filter = function(from, properties) { 49 | result = {}; 50 | properties.forEach(function(key) { 51 | result[key] = from[key]; 52 | }); 53 | return result; 54 | }; 55 | 56 | M.intersect = function(s1, e1, s2, e2) { 57 | return !(e1 <= s2 || e2 <= s1); 58 | }; 59 | 60 | M.mergeSamplesInPlace = function(s1, s2) { 61 | if (s2 === null) { return s1; } 62 | if (s1 === null) { return s2; } 63 | if (s1.length != s2.length) { 64 | console.error("Can't merge samples of different lengths..."); 65 | return null; 66 | } 67 | for (var i = 0; i < s1.length; i++) { 68 | s1[i] = (s1[i] + s2[i]) / 2.0; 69 | } 70 | return s1; 71 | }; 72 | 73 | M.dist = function(x1, y1, x2, y2) { 74 | var dx = x1 - x2, dy = y1 - y2; 75 | return Math.sqrt(dx * dx + dy * dy); 76 | }; 77 | 78 | M.deepEquals = function(a, b) { 79 | if (a === null != b === null) { return false; } 80 | if (a === null && b === null) { return true; } 81 | if (a === undefined != b === undefined) { return false; } 82 | if (a === undefined && b === undefined) { return true; } 83 | 84 | var aArr = (a instanceof Array ), bArr = (b instanceof Array ); 85 | var aObj = (a instanceof Object), bObj = (b instanceof Object); 86 | if (aArr != bArr || aObj != bObj) { return false; } 87 | 88 | if (aArr) { 89 | if (a.length != b.length) { return false; } 90 | for (var i in a) { if (!M.deepEquals(a[i], b[i])) { return false; } } 91 | return true; 92 | } else if (aObj) { 93 | var aLength = 0, bLength = 0; 94 | for (var i in a) { aLength++; } 95 | for (var i in b) { bLength++; } 96 | if (aLength != bLength) { return false; } 97 | for (var i in a) { if (!M.deepEquals(a[i], b[i])) { return false; } } 98 | return true; 99 | } else { // Primitives? 100 | return a === b; 101 | } 102 | }; 103 | 104 | M.clone = function(o) { 105 | if ($.isArray(o)) { 106 | return o.slice(); 107 | } 108 | return $.extend(true, {}, o); 109 | }; 110 | 111 | M.whoops = function(msg) { 112 | console.error(msg); 113 | debugger; 114 | return null; 115 | }; 116 | 117 | })(window.util); 118 | -------------------------------------------------------------------------------- /mashapp/polymer/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Go Sound MashApp", 3 | "private": true, 4 | "dependencies": { 5 | "polymer": "Polymer/polymer#^1.1.0", 6 | "iron-elements": "PolymerElements/iron-elements#^1.0.0", 7 | "paper-elements": "PolymerElements/paper-elements#^1.0.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /mashapp/server.go: -------------------------------------------------------------------------------- 1 | package mashapp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "html/template" 7 | "io/ioutil" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | type MashAppServer struct { 13 | // final 14 | port int 15 | rootPath string 16 | filePath string 17 | state *ServerState 18 | 19 | // Lazily loaded 20 | fileOptions []string 21 | } 22 | 23 | func NewServer(port int, rootPath string, filePath string) *MashAppServer { 24 | return &MashAppServer{ 25 | port, 26 | rootPath, 27 | filePath, 28 | NewServerState(), 29 | nil, 30 | } 31 | } 32 | 33 | func (s *MashAppServer) Serve() { 34 | addr := fmt.Sprintf(":%d", s.port) 35 | fmt.Printf("Serving http://localhost%s/\n", addr) 36 | 37 | serveStaticFiles(fmt.Sprintf("%s/static", s.rootPath), "static") 38 | serveStaticFiles(fmt.Sprintf("%s/polymer/app", s.rootPath), "polymer") 39 | 40 | s.serveRPCs() 41 | 42 | http.HandleFunc("/", s.appHandler) 43 | 44 | http.ListenAndServe(addr, nil) 45 | } 46 | 47 | type BootstrapData struct { 48 | Files []string `json:"files"` 49 | } 50 | type TemplateData struct { 51 | // Files []string 52 | JsonConfig template.HTML 53 | } 54 | 55 | func (s *MashAppServer) appHandler(w http.ResponseWriter, r *http.Request) { 56 | if s.fileOptions == nil { 57 | s.fileOptions = listMusicFiles(s.filePath) 58 | } 59 | 60 | bootstrap := BootstrapData{ 61 | s.fileOptions, 62 | } 63 | asJson, _ := json.Marshal(bootstrap) 64 | s.renderTemplate(w, "app.html", TemplateData{ 65 | // s.fileOptions, 66 | template.HTML(string(asJson)), 67 | }) 68 | } 69 | 70 | func (s *MashAppServer) renderTemplate(w http.ResponseWriter, templateName string, data interface{}) { 71 | asPath := fmt.Sprintf("%s/%s", s.rootPath, templateName) 72 | t, err := template.ParseFiles(asPath) 73 | if err != nil { 74 | http.Error(w, err.Error(), http.StatusInternalServerError) 75 | return 76 | } 77 | err = t.Execute(w, data) 78 | if err != nil { 79 | http.Error(w, err.Error(), http.StatusInternalServerError) 80 | } 81 | } 82 | 83 | func serveStaticFiles(fromDirectory string, toHttpPrefix string) { 84 | asPath := fmt.Sprintf("/%s/", toHttpPrefix) 85 | fs := http.FileServer(http.Dir(fromDirectory)) 86 | http.Handle(asPath, disableCache(http.StripPrefix(asPath, fs))) 87 | } 88 | 89 | func disableCache(h http.Handler) http.Handler { 90 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 91 | // PICK: Remove ever? 92 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") // HTTP 1.1. 93 | w.Header().Set("Pragma", "no-cache") // HTTP 1.0. 94 | w.Header().Set("Expires", "0") // Proxies. 95 | h.ServeHTTP(w, r) 96 | }) 97 | } 98 | 99 | func listMusicFiles(fromDirectory string) []string { 100 | infos, err := ioutil.ReadDir(fromDirectory) 101 | if err != nil { 102 | panic("Oops, can't read directory") 103 | } 104 | 105 | result := make([]string, 0) 106 | for _, info := range infos { 107 | if !info.IsDir() && isMusicFile(info.Name()) { 108 | result = append(result, info.Name()) 109 | } 110 | } 111 | return result 112 | } 113 | 114 | func isMusicFile(name string) bool { 115 | return strings.HasSuffix(name, ".wav") || strings.HasSuffix(name, ".flac") 116 | } 117 | -------------------------------------------------------------------------------- /mashapp/state.go: -------------------------------------------------------------------------------- 1 | package mashapp 2 | 3 | import ( 4 | // "fmt" 5 | 6 | "github.com/padster/go-sound/cq" 7 | "github.com/padster/go-sound/file" 8 | // "github.com/padster/go-sound/output" 9 | s "github.com/padster/go-sound/sounds" 10 | "github.com/padster/go-sound/util" 11 | ) 12 | 13 | const ( 14 | SAMPLE_RATE = s.CyclesPerSecond 15 | MIN_FREQ = 55.0 16 | OCTAVES = 7 17 | MAX_FREQ = 55.0 * (1 << OCTAVES) 18 | BINS_PER_SEMITONE = 4 19 | BPO = 12 * BINS_PER_SEMITONE 20 | ) 21 | 22 | type ServerState struct { 23 | nextId int 24 | inputs map[int]InputSound 25 | blocks map[int]Block 26 | } 27 | 28 | // HACK - remove, replace with structs from model.go 29 | type InputSound struct { 30 | samples GoSamples 31 | } 32 | 33 | func NewServerState() *ServerState { 34 | result := ServerState{ 35 | 0, 36 | make(map[int]InputSound), 37 | make(map[int]Block), 38 | } 39 | return &result 40 | } 41 | 42 | func (state *ServerState) loadSound(path string) (int, InputSound) { 43 | // TODO - lock 44 | id := state.nextId 45 | state.nextId++ 46 | 47 | loadedSound := soundfile.Read(path) 48 | inputSound := InputSound{ 49 | util.CacheSamples(loadedSound), 50 | } 51 | state.inputs[id] = inputSound 52 | return id, inputSound 53 | } 54 | 55 | func (state *ServerState) createBlock(block Block) Block { 56 | block.ID = state.nextId 57 | state.nextId++ 58 | return block 59 | } 60 | 61 | // Pitch- and/or Time-shift an input, return the resulting samples 62 | func (state *ServerState) shiftInput(input InputMeta) InputSound { 63 | beforeSamples := state.inputs[input.ID].samples 64 | beforeSound := s.WrapSliceAsSound(beforeSamples) 65 | beforeSound.Start() 66 | 67 | // HACK: Only pitch shift for now. 68 | paramsIn := cq.NewCQParams(SAMPLE_RATE, MIN_FREQ, MAX_FREQ, BPO) 69 | paramsOut := cq.NewCQParams(SAMPLE_RATE, MIN_FREQ, MAX_FREQ, BPO) 70 | spectrogram := cq.NewSpectrogram(paramsIn) 71 | cqInverse := cq.NewCQInverse(paramsOut) 72 | 73 | columns := spectrogram.ProcessChannel(beforeSound.GetSamples()) 74 | outColumns := shiftSpectrogram(input.FinalPitch*(BINS_PER_SEMITONE), 0, columns, OCTAVES, BPO) 75 | soundChannel := cqInverse.ProcessChannel(outColumns) 76 | resultSound := s.WrapChannelAsSound(soundChannel) 77 | 78 | afterSamples := util.CacheSamples(resultSound) 79 | 80 | // TODO - update all blocks created off this input. 81 | return InputSound{ 82 | afterSamples, 83 | } 84 | } 85 | 86 | func shiftSpectrogram(binOffset int, sampleOffset int, samples <-chan []complex128, octaves int, bpo int) <-chan []complex128 { 87 | result := make(chan []complex128) 88 | 89 | go func() { 90 | sRead, sWrite := 0, 0 91 | 92 | ignoreSamples := sampleOffset 93 | at := 0 94 | for s := range samples { 95 | sRead++ 96 | if ignoreSamples > 0 { 97 | ignoreSamples-- 98 | continue 99 | } 100 | 101 | octaveCount := octaves 102 | if at > 0 { 103 | octaveCount = numOctaves(at) 104 | if octaveCount == octaves { 105 | at = 0 106 | } 107 | } 108 | at++ 109 | 110 | toFill := octaveCount * bpo 111 | column := make([]complex128, toFill, toFill) 112 | 113 | // NOTE: Zero-padded, not the best... 114 | if binOffset >= 0 { 115 | copy(column, s[binOffset:]) 116 | } else { 117 | copy(column[-binOffset:], s) 118 | } 119 | sWrite++ 120 | result <- column 121 | } 122 | close(result) 123 | }() 124 | return result 125 | } 126 | 127 | func numOctaves(at int) int { 128 | result := 1 129 | for at%2 == 0 { 130 | at /= 2 131 | result++ 132 | } 133 | return result 134 | } 135 | -------------------------------------------------------------------------------- /mashapp/static/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | /* TODO: Customize to something else? Make sure that paper elements use it too. */ 7 | /* Copied from paper-styles. */ 8 | font-family: 'Roboto', 'Noto', sans-serif; 9 | -webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */ 10 | 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | canvas { 16 | border: 1px solid black; 17 | min-width: 100%; 18 | height: 100px; 19 | } 20 | 21 | .line { 22 | display: flex; 23 | flex-direction: row; 24 | } 25 | 26 | .lineButtons { 27 | width: 50px; 28 | } 29 | 30 | #playLine { 31 | position: absolute; 32 | height: 100px; 33 | border: 1px solid red; 34 | } 35 | 36 | #zoomSlider { 37 | width: 400px; 38 | } 39 | 40 | /* HACK - figure out why this isn't coming from polymer. */ 41 | [hidden] { 42 | display: none !important; 43 | } 44 | -------------------------------------------------------------------------------- /mashappserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/padster/go-sound/mashapp" 5 | ) 6 | 7 | func main() { 8 | mashapp.NewServer(8080, "mashapp", ".").Serve() 9 | } 10 | -------------------------------------------------------------------------------- /output/flacfile.go: -------------------------------------------------------------------------------- 1 | // Write a sound to a .wav file 2 | package output 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | flac "github.com/cocoonlife/goflac" 10 | // flac "github.com/padster/go-sound/fakeflac" 11 | s "github.com/padster/go-sound/sounds" 12 | ) 13 | 14 | // WriteSoundToFlac creates a file at a path, and writes the given sound in the .flac format. 15 | func WriteSoundToFlac(sound s.Sound, path string) error { 16 | if !strings.HasSuffix(path, ".flac") { 17 | panic("Output file must be .flac") 18 | } 19 | 20 | if _, err := os.Stat(path); err == nil { 21 | panic("File already exists! Please delete first") 22 | return os.ErrExist 23 | } 24 | 25 | sampleRate := int(s.CyclesPerSecond) 26 | depth := 24 27 | 28 | fileWriter, err := flac.NewEncoder(path, 1, depth, sampleRate) 29 | if err != nil { 30 | fmt.Printf("Error opening file to write to! %v\n", err) 31 | panic("Can't write file") 32 | } 33 | defer fileWriter.Close() 34 | 35 | // Starts the sound, and accesses its sample stream. 36 | sound.Start() 37 | samples := sound.GetSamples() 38 | defer sound.Stop() 39 | 40 | // TODO: Make a common utility for this, it's used here and in both CQ and CQI. 41 | frameSize := 44100 42 | buffer := make([]float64, frameSize, frameSize) 43 | at := 0 44 | for s := range samples { 45 | if at == frameSize { 46 | writeFrame(fileWriter, buffer) 47 | at = 0 48 | } 49 | buffer[at] = s 50 | at++ 51 | } 52 | writeFrame(fileWriter, buffer[:at]) 53 | 54 | return nil 55 | } 56 | 57 | func writeFrame(fileWriter *flac.Encoder, samples []float64) { 58 | n := len(samples) 59 | 60 | frameBuffer := make([]int32, n, n) 61 | for i, v := range samples { 62 | frameBuffer[i] = intFromFloatWithDepth(v, fileWriter.Depth) 63 | } 64 | 65 | frame := flac.Frame{ 66 | 1, /* channels */ 67 | fileWriter.Depth, /* depth */ 68 | fileWriter.Rate, /* rate */ 69 | frameBuffer, 70 | } 71 | 72 | if err := fileWriter.WriteFrame(frame); err != nil { 73 | fmt.Printf("Error writing frame to file :( %v\n", err) 74 | panic("Can't write to file") 75 | } 76 | } 77 | 78 | // HACK - should share with cq/utils.go 79 | func intFromFloatWithDepth(input float64, depth int) int32 { 80 | return int32(input * (float64(unsafeShift(depth)) - 1.0)) 81 | } 82 | func unsafeShift(s int) int { 83 | return 1 << uint(s) 84 | } 85 | -------------------------------------------------------------------------------- /output/jack.go: -------------------------------------------------------------------------------- 1 | // Write sounds to audio output via Jack (http://jackaudio.org) 2 | package output 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/xthexder/go-jack" 8 | 9 | s "github.com/padster/go-sound/sounds" 10 | ) 11 | 12 | type jackContext struct { 13 | sound s.Sound 14 | sampleChannel <-chan float64 15 | leftPort *jack.Port 16 | rightPort *jack.Port 17 | running bool 18 | } 19 | 20 | // Play plays a sound to audio out via jack. 21 | func PlayJack(sound s.Sound) { 22 | player := &jackContext{ 23 | sound, 24 | nil, /* sampleChannel */ 25 | nil, /* leftPort */ 26 | nil, /* rightPort */ 27 | false, /* running */ 28 | } 29 | 30 | // Setup copied from https://github.com/xthexder/go-jack readme. 31 | client, _ := jack.ClientOpen("GoSoundOut", jack.NoStartServer) 32 | if client == nil { 33 | fmt.Println("Could not connect to jack server.") 34 | return 35 | } 36 | defer client.Close() 37 | 38 | if code := client.SetProcessCallback(player.process); code != 0 { 39 | fmt.Println("Failed to set process callback.") 40 | return 41 | } 42 | client.OnShutdown(player.shutdown) 43 | 44 | sound.Start() 45 | defer player.sound.Stop() 46 | 47 | player.sampleChannel = sound.GetSamples() 48 | player.running = true 49 | 50 | if code := client.Activate(); code != 0 { 51 | fmt.Println("Failed to activate client.") 52 | return 53 | } 54 | 55 | player.leftPort = client.PortRegister("go-sound-left", jack.DEFAULT_AUDIO_TYPE, jack.PortIsOutput, 0) 56 | player.rightPort = client.PortRegister("go-sound-right", jack.DEFAULT_AUDIO_TYPE, jack.PortIsOutput, 0) 57 | for player.running { 58 | } 59 | } 60 | 61 | func (j *jackContext) process(nframes uint32) int { 62 | leftSamples := j.leftPort.GetBuffer(nframes) 63 | rightSamples := j.rightPort.GetBuffer(nframes) 64 | 65 | // fmt.Printf("Writing %d samples\n", len(samples)) 66 | for i := range leftSamples { 67 | sample, stream_ok := <-j.sampleChannel 68 | if !stream_ok { 69 | j.running = false 70 | return 1 71 | } 72 | leftSamples[i] = jack.AudioSample(sample) 73 | rightSamples[i] = jack.AudioSample(sample) 74 | } 75 | return 0 76 | } 77 | 78 | func (j *jackContext) shutdown() { 79 | } 80 | -------------------------------------------------------------------------------- /output/pulse.go: -------------------------------------------------------------------------------- 1 | // Package output it responsible for preparing the sounds for human consumption, 2 | // whether audio, visual or other means. 3 | package output 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/padster/go-sound/sounds" 9 | ) 10 | 11 | // Play plays a sound to audio out via pulseaudio. 12 | func Play(s sounds.Sound) { 13 | pa := NewPulseMainLoop() 14 | defer pa.Dispose() 15 | pa.Start() 16 | 17 | sync_ch := make(chan int) 18 | go playSamples(s, sync_ch, pa) 19 | <-sync_ch 20 | } 21 | 22 | // playSamples handles the writing of a sound's channel of samples to a pulse stream. 23 | func playSamples(s sounds.Sound, sync_ch chan int, pa *PulseMainLoop) { 24 | defer func() { 25 | sync_ch <- 0 26 | }() 27 | 28 | // Create a pulse audio context to play the sound. 29 | ctx := pa.NewContext("default", 0) 30 | if ctx == nil { 31 | fmt.Println("Failed to create a new context") 32 | return 33 | } 34 | defer ctx.Dispose() 35 | 36 | // Create a single-channel pulse audio stream to write the sound to. 37 | st := ctx.NewStream("default", &PulseSampleSpec{ 38 | Format: SAMPLE_FLOAT32LE, 39 | Rate: int(sounds.CyclesPerSecond), 40 | Channels: 1, 41 | }) 42 | if st == nil { 43 | fmt.Println("Failed to create a new stream") 44 | return 45 | } 46 | defer st.Dispose() 47 | 48 | // Starts the sound, and accesses its sample stream. 49 | s.Start() 50 | samples := s.GetSamples() 51 | defer s.Stop() 52 | 53 | // Continually buffers data from the stream and writes to audio. 54 | sampleCount := 0 55 | st.ConnectToSink() 56 | for { 57 | toAdd := st.WritableSize() 58 | if toAdd == 0 { 59 | continue 60 | } 61 | 62 | // No buffer - write immediately. 63 | // TODO(padster): Play with this to see if chunked writes actually reduce delay. 64 | if toAdd > 441 { 65 | toAdd = 441 66 | } 67 | 68 | // TODO: Reuse just one of these? 69 | buffer := make([]float32, toAdd) 70 | finishedAt := toAdd 71 | 72 | for i := range buffer { 73 | sample, stream_ok := <-samples 74 | if !stream_ok { 75 | finishedAt = i 76 | break 77 | } 78 | buffer[i] = float32(sample) 79 | } 80 | if finishedAt == 0 { 81 | st.Drain() 82 | break 83 | } 84 | sampleCount += finishedAt 85 | st.Write(buffer[0:finishedAt], SEEK_RELATIVE) 86 | } 87 | fmt.Printf("Samples written: %d\n", sampleCount) 88 | } 89 | -------------------------------------------------------------------------------- /output/screen.go: -------------------------------------------------------------------------------- 1 | // Renders a sound wave to screen. 2 | package output 3 | 4 | import ( 5 | "github.com/padster/go-sound/sounds" 6 | "github.com/padster/go-sound/util" 7 | ) 8 | 9 | // Render starts rendering a sound wave's samples to a screen of given height. 10 | func Render(s sounds.Sound, width int, height int, samplesPerPixel int) { 11 | screen := util.NewScreen(width, height, samplesPerPixel) 12 | s.Start() 13 | // TODO(padster): Currently this generates and renders the samples 14 | // as quickly as possible. Better instead to render close to realtime? 15 | screen.Render(s.GetSamples(), 2) 16 | } 17 | -------------------------------------------------------------------------------- /output/wavfile.go: -------------------------------------------------------------------------------- 1 | // Write a sound to a .wav file 2 | package output 3 | 4 | import ( 5 | "encoding/binary" 6 | "math" 7 | "os" 8 | 9 | wav "github.com/cryptix/wav" 10 | "github.com/padster/go-sound/sounds" 11 | ) 12 | 13 | const ( 14 | normScale = float64(math.MaxInt16) 15 | ) 16 | 17 | // WriteSoundToWav creates a file at a path, and writes the given sound in the .wav format. 18 | func WriteSoundToWav(s sounds.Sound, path string) error { 19 | // Create file first, only if it doesn't exist: 20 | if _, err := os.Stat(path); err == nil { 21 | panic("File already exists! Please delete first") 22 | return os.ErrExist 23 | } 24 | file, err := os.Create(path) 25 | if err != nil { 26 | return err 27 | } 28 | defer func() { 29 | file.Close() 30 | }() 31 | 32 | // Create a .wav writer for the file 33 | var wf = wav.File{ 34 | SampleRate: uint32(sounds.CyclesPerSecond), 35 | Channels: 1, 36 | SignificantBits: 16, 37 | } 38 | writer, err := wf.NewWriter(file) 39 | if err != nil { 40 | return err 41 | } 42 | defer writer.Close() 43 | 44 | // Starts the sound, and accesses its sample stream. 45 | s.Start() 46 | samples := s.GetSamples() 47 | defer s.Stop() 48 | 49 | // Write a single sample at a time, as per the .wav writer API. 50 | b := make([]byte, 2) 51 | for sample := range samples { 52 | toNumber := uint16(sample * normScale) // Inverse the read scaling 53 | binary.LittleEndian.PutUint16(b, uint16(toNumber)) 54 | writer.WriteSample(b) 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /piano.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/piano.wav -------------------------------------------------------------------------------- /readcq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "runtime" 7 | 8 | "github.com/padster/go-sound/cq" 9 | f "github.com/padster/go-sound/file" 10 | "github.com/padster/go-sound/output" 11 | s "github.com/padster/go-sound/sounds" 12 | "github.com/padster/go-sound/util" 13 | ) 14 | 15 | const ( 16 | SHOW_SPECTROGRAM = false 17 | ) 18 | 19 | // Reads the CQ columns from file, converts back into a sound. 20 | func main() { 21 | runtime.GOMAXPROCS(6) 22 | 23 | // Parse flags... 24 | sampleRate := s.CyclesPerSecond 25 | octaves := flag.Int("octaves", 7, "Range in octaves") 26 | minFreq := flag.Float64("minFreq", 55.0, "Minimum frequency") 27 | bpo := flag.Int("bpo", 24, "Buckets per octave") 28 | flag.Parse() 29 | 30 | remainingArgs := flag.Args() 31 | if len(remainingArgs) < 0 || len(remainingArgs) > 1 { 32 | panic("Only takes at most one arg: input file name.") 33 | } 34 | inputFile := "out.cq" 35 | if len(remainingArgs) == 1 { 36 | inputFile = remainingArgs[0] 37 | } 38 | 39 | params := cq.NewCQParams(sampleRate, *octaves, *minFreq, *bpo) 40 | 41 | if SHOW_SPECTROGRAM { 42 | cqChannel := f.ReadCQColumns(inputFile, params) 43 | spectrogram := cq.NewSpectrogram(params) 44 | columns := spectrogram.InterpolateCQChannel(cqChannel) 45 | toShow := util.NewSpectrogramScreen(882, *bpo**octaves, *bpo) 46 | toShow.Render(columns, 1) 47 | } else { 48 | asSound := f.ReadCQ(inputFile, params, false) 49 | fmt.Printf("Playing...\n") 50 | output.Play(asSound) 51 | fmt.Printf("Done...\n") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /runcq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math/cmplx" 7 | "runtime" 8 | "time" 9 | 10 | "github.com/padster/go-sound/cq" 11 | f "github.com/padster/go-sound/file" 12 | "github.com/padster/go-sound/output" 13 | s "github.com/padster/go-sound/sounds" 14 | ) 15 | 16 | // Runs CQ, applies some processing, and plays the result. 17 | func main() { 18 | runtime.GOMAXPROCS(4) 19 | 20 | // Parse flags... 21 | sampleRate := s.CyclesPerSecond 22 | octaves := flag.Int("octaves", 7, "Range in octaves") 23 | minFreq := flag.Float64("minFreq", 55.0, "Minimum frequency") 24 | bpo := flag.Int("bpo", 24, "Buckets per octave") 25 | flag.Parse() 26 | 27 | remainingArgs := flag.Args() 28 | if len(remainingArgs) < 1 || len(remainingArgs) > 2 { 29 | panic("Required: [] filename arguments") 30 | } 31 | inputFile := remainingArgs[0] 32 | inputFile2 := inputFile 33 | if len(remainingArgs) == 2 { 34 | inputFile2 = remainingArgs[1] 35 | } 36 | 37 | inputSound := f.Read(inputFile) 38 | // inputSound := s.NewTimedSound(s.NewSineWave(440.0), 1000) 39 | inputSound.Start() 40 | defer inputSound.Stop() 41 | 42 | params := cq.NewCQParams(sampleRate, *octaves, *minFreq, *bpo) 43 | constantQ := cq.NewConstantQ(params) 44 | cqInverse := cq.NewCQInverse(params) 45 | latency := constantQ.OutputLatency + cqInverse.OutputLatency 46 | 47 | // Two inputs version - TODO, switch back to input + output. 48 | inputSound2 := f.Read(inputFile2) 49 | inputSound2.Start() 50 | defer inputSound2.Stop() 51 | constantQ2 := cq.NewConstantQ(params) 52 | 53 | startTime := time.Now() 54 | // TODO: Skip the first 'latency' samples for the stream. 55 | fmt.Printf("TODO: Skip latency (= %d) samples)\n", latency) 56 | columns := constantQ.ProcessChannel(inputSound.GetSamples()) 57 | columns2 := constantQ2.ProcessChannel(inputSound2.GetSamples()) 58 | samples := cqInverse.ProcessChannel(mergeChannels(columns, columns2)) 59 | asSound := s.WrapChannelAsSound(samples) 60 | 61 | // if outputFile != "" { 62 | //f.Write(asSound, "brickwhite.wav") 63 | // } else { 64 | output.Play(asSound) 65 | // } 66 | 67 | elapsedSeconds := time.Since(startTime).Seconds() 68 | fmt.Printf("elapsed time (not counting init): %f sec\n", elapsedSeconds) 69 | } 70 | 71 | func mergeChannels(in1 <-chan []complex128, in2 <-chan []complex128) chan []complex128 { 72 | out := make(chan []complex128) 73 | go func() { 74 | fmt.Printf("Writing...\n") 75 | for cIn1 := range in1 { 76 | cIn2 := <-in2 77 | if len(cIn1) != len(cIn2) { 78 | panic("oops, columns don't match... :(") 79 | } 80 | cOut := make([]complex128, len(cIn1), len(cIn1)) 81 | for i := range cIn1 { 82 | power1, angle1 := cmplx.Polar(cIn1[i]) 83 | power1 = 1.0 84 | power2, angle2 := cmplx.Polar(cIn2[i]) 85 | cOut[i] = cmplx.Rect(power1, angle1) 86 | // if i > 48 && i <= 72 { 87 | // cOut[i] = 0 88 | // } 89 | // HACK variable to stop go complaining about unused variables :( 90 | cIn2[i] = cmplx.Rect(power2, angle2) 91 | } 92 | out <- cOut 93 | } 94 | close(out) 95 | }() 96 | return out 97 | } 98 | 99 | func shiftChannel(buckets int, in <-chan []complex128) chan []complex128 { 100 | out := make(chan []complex128) 101 | go func(b int) { 102 | for cIn := range in { 103 | s := len(cIn) 104 | cOut := make([]complex128, s, s) 105 | copy(cOut, cIn[buckets:]) 106 | out <- cOut 107 | } 108 | close(out) 109 | }(buckets) 110 | return out 111 | } 112 | -------------------------------------------------------------------------------- /runthrough.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | // "fmt" 5 | "runtime" 6 | 7 | "github.com/padster/go-sound/file" 8 | "github.com/padster/go-sound/output" 9 | s "github.com/padster/go-sound/sounds" 10 | "github.com/padster/go-sound/util" 11 | ) 12 | 13 | const ( 14 | ONE_SECOND_MS = 1000.0 15 | ) 16 | 17 | func sineWave() { 18 | // Example #1: Single tone = sine wave at a given frequency. 19 | sound := s.NewSineWave(440) 20 | demonstrateSound(sound, false) 21 | } 22 | 23 | func timedSineWave() { 24 | // Example #2: Single tone for a second. 25 | sound := s.NewTimedSound(s.NewSineWave(440), ONE_SECOND_MS) 26 | demonstrateSound(sound, true) 27 | } 28 | 29 | func silence() { 30 | // Example #3: The sound of silence. 31 | sound := s.NewTimedSilence(ONE_SECOND_MS) 32 | demonstrateSound(sound, false) 33 | } 34 | 35 | func oneNoteThenAnother() { 36 | // Example #4: One note (A 440) followed by a second (A 880) 37 | sound := s.ConcatSounds( 38 | s.NewTimedSound(s.NewSineWave(440), ONE_SECOND_MS/2), 39 | s.NewTimedSound(s.NewSineWave(880), ONE_SECOND_MS/2), 40 | ) 41 | demonstrateSound(sound, true) 42 | } 43 | 44 | func simpleWaveTypes() { 45 | // Example #5: The sound of four basic repeating wave types: Sine, Triangle, Sawtooth, Square 46 | sound := s.ConcatSounds( 47 | s.NewTimedSound(s.NewSineWave(440), ONE_SECOND_MS), 48 | s.NewTimedSound(s.NewTriangleWave(440), ONE_SECOND_MS), 49 | s.NewTimedSound(s.NewSawtoothWave(440), ONE_SECOND_MS), 50 | s.NewTimedSound(s.NewSquareWave(440), ONE_SECOND_MS), 51 | ) 52 | demonstrateSound(sound, true) 53 | } 54 | 55 | func twoNotesTogether() { 56 | // Example #6: Two notes at the same time! A 440 and E660 57 | sound := s.SumSounds( 58 | s.NewTimedSound(s.NewSineWave(440.00), ONE_SECOND_MS), 59 | s.NewTimedSound(s.NewSineWave(659.25), ONE_SECOND_MS), 60 | ) 61 | demonstrateSound(sound, true) 62 | } 63 | 64 | func amplify() { 65 | // Example #7: One note, and four different amplifications 0, 0.5, 1.0, 1.5 66 | sound := s.ConcatSounds( 67 | s.MultiplyWithClip(s.NewTimedSound(s.NewSineWave(440.00), ONE_SECOND_MS/2), 0.0), // Silence 68 | s.MultiplyWithClip(s.NewTimedSound(s.NewSineWave(440.00), ONE_SECOND_MS/2), 0.5), // Quiet 69 | s.MultiplyWithClip(s.NewTimedSound(s.NewSineWave(440.00), ONE_SECOND_MS/2), 1.0), // Loud 70 | s.MultiplyWithClip(s.NewTimedSound(s.NewSineWave(440.00), ONE_SECOND_MS/2), 1.5), // Clipped! 71 | ) 72 | demonstrateSound(sound, true) 73 | } 74 | 75 | func envelopes() { 76 | // Example #8: Play three notes normally, then silence, then repeat with envelopes. 77 | sound := s.ConcatSounds( 78 | s.NewTimedSound(util.MidiToSound(60), ONE_SECOND_MS/2), 79 | s.NewTimedSound(util.MidiToSound(62), ONE_SECOND_MS/2), 80 | s.NewTimedSound(util.MidiToSound(64), ONE_SECOND_MS/2), 81 | s.NewTimedSilence(ONE_SECOND_MS/2), 82 | s.NewADSREnvelope(s.NewTimedSound(util.MidiToSound(60), ONE_SECOND_MS/2), 50, 200, 0.5, 100), 83 | s.NewADSREnvelope(s.NewTimedSound(util.MidiToSound(62), ONE_SECOND_MS/2), 50, 200, 0.5, 100), 84 | s.NewADSREnvelope(s.NewTimedSound(util.MidiToSound(64), ONE_SECOND_MS/2), 50, 200, 0.5, 100), 85 | ) 86 | demonstrateSound(sound, true) 87 | } 88 | 89 | func pitchBending() { 90 | // Example #9: Smooth varying pitch. 91 | bendWave := s.NewTimedSound(s.NewSawtoothWave(2), 3*ONE_SECOND_MS) 92 | bendWave.Start() 93 | bendWaveSamples := bendWave.GetSamples() 94 | 95 | pitchBendChannel := make(chan float64) 96 | go func() { 97 | for s := range bendWaveSamples { 98 | // [-1, 1] -> [440, 880] 99 | pitchBendChannel <- 440.0 + (s+1.0)*220.0 100 | } 101 | }() 102 | 103 | sound := s.NewHzFromChannel(pitchBendChannel) 104 | demonstrateSound(sound, true) 105 | bendWave.Stop() 106 | } 107 | 108 | func delay() { 109 | bendWave := s.NewTimedSound(s.NewTriangleWave(1.0/4.0), 4*ONE_SECOND_MS) 110 | bendWave.Start() 111 | bendWaveSamples := bendWave.GetSamples() 112 | 113 | pitchBendChannel := make(chan float64) 114 | go func() { 115 | for s := range bendWaveSamples { 116 | pitchBendChannel <- 440.0 + (s+1.0)*220.0 117 | } 118 | }() 119 | 120 | sound := s.AddDelay(s.NewHzFromChannel(pitchBendChannel), ONE_SECOND_MS) 121 | // There appears to be a bug in writing this file :/ 122 | demonstrateSound(sound, false) 123 | } 124 | 125 | func main() { 126 | runtime.GOMAXPROCS(4) 127 | 128 | exampleToRun := 10 129 | 130 | switch exampleToRun { 131 | case 1: 132 | sineWave() 133 | case 2: 134 | timedSineWave() 135 | case 3: 136 | silence() 137 | case 4: 138 | oneNoteThenAnother() 139 | case 5: 140 | simpleWaveTypes() 141 | case 6: 142 | twoNotesTogether() 143 | case 7: 144 | amplify() 145 | case 8: 146 | envelopes() 147 | case 9: 148 | pitchBending() 149 | case 10: 150 | delay() 151 | } 152 | } 153 | 154 | // Ignore this section - just splits the sound into three: 155 | // One to be played, one to be rendered, and one to be saved if needed. 156 | func demonstrateSound(sound s.Sound, saveFile bool) { 157 | saveFile = false // HACK 158 | sound.Start() 159 | samples := sound.GetSamples() 160 | 161 | toDraw, toPlay := make(chan float64), make(chan float64) 162 | 163 | var toSave chan float64 164 | if saveFile { 165 | toSave = make(chan float64) 166 | } 167 | 168 | go func() { 169 | for s := range samples { 170 | toDraw <- s 171 | toPlay <- s 172 | if saveFile { 173 | toSave <- s 174 | } 175 | } 176 | close(toDraw) 177 | close(toPlay) 178 | if saveFile { 179 | close(toSave) 180 | } 181 | sound.Stop() 182 | }() 183 | 184 | go output.Play(s.WrapChannelAsSound(toPlay)) 185 | if saveFile { 186 | go soundfile.Write(s.WrapChannelAsSound(toSave), "out.wav") 187 | } 188 | output.Render(s.WrapChannelAsSound(toDraw), 600, 200, 18) 189 | } 190 | -------------------------------------------------------------------------------- /showspec.py: -------------------------------------------------------------------------------- 1 | # python showspec.py 2 | # Runs that through ConstantQ to produce a CQSpectrogram, 3 | # writes that to out.raw, and then draws the result using matplotlib. 4 | 5 | import matplotlib.pylab 6 | import numpy as np 7 | import sys 8 | import subprocess 9 | 10 | bins = 24 11 | octaves = 7 12 | # TODO: Pass bins to go run 13 | # subprocess.call(["go", "run", "cqspectrogram.go"] + sys.argv[1:2]) 14 | ys1 = np.memmap("out.raw", dtype=np.complex64, mode="r").reshape((-1, bins*octaves)).T 15 | ys1 = np.nan_to_num(ys1.copy()) 16 | 17 | # ys1[numpy.abs(ys1) < 1e-6] = 0 18 | 19 | def running_mean(x, N): 20 | cumsum = np.cumsum(np.insert(x, 0, np.zeros(N))) 21 | return (cumsum[N:] - cumsum[:-N]) / N 22 | 23 | def plot_complex_spectrogram(ys, ax0, ax1): 24 | rows, cols = ys.shape 25 | values = np.abs(ys) 26 | # values = 20 * np.log10(np.abs(values) + 1e-8) 27 | 28 | # for i in range(0, rows): 29 | # values[i, :] = running_mean(values[i, :], 32) 30 | 31 | # values = np.log(np.abs(ys) + 1e-8) 32 | # values = np.abs(ys) 33 | # values = np.abs(ys) 34 | # ax0.imshow(values, vmin=-12, vmax=5, cmap='gray') 35 | 36 | # colMax = np.mean(values, axis=1)[::-1] 37 | # colMax = np.insert(colMax, 0, 0) 38 | # ax1.plot(colMax) 39 | # for i in range(0, rows, 12): 40 | # ax1.axvline(i, color='r') 41 | # print "Max = %d" % np.argmax(colMax) 42 | 43 | # [0, 168) -> [84, 0) 44 | 45 | values = np.sum(values, axis=1) 46 | # ax0.plot(np.arange(84, 0, -0.5), values / 700.0) 47 | # ax0.imshow(values, cmap='gray') 48 | 49 | # ax1.plot(values[:, 256]) 50 | # colSum = np.std(values, axis=0) 51 | # colSumD = np.diff(colSum) 52 | # ax1.plot(running_mean(colSum, 5)) 53 | # notes = np.where((colSumD > 250) & (colSumD < 600))[0] 54 | # notes = np.insert(notes, 0, 0) 55 | # dupes = np.diff(notes) 56 | # notes = notes[np.where(dupes > 50)[0] + 1] 57 | # notes = notes[np.where(notes < 10500)[0]] + 18 58 | # notes = np.concatenate(( 59 | # np.arange(50, 300, 52), 60 | # np.arange(328, 600, 60), 61 | # np.arange(670, 1380, 54) 62 | # )) 63 | # print notes 64 | # i = 0 65 | # for note in notes[0:5]: 66 | # ax0.plot(values[:, note]) 67 | # i += 9 68 | # ax0.axvline(note, color='r') 69 | # ax1.axvline(note, color='r') 70 | # print "# notes = %d" % len(notes) 71 | # 50 changes 72 | ang = np.angle(ys) 73 | ang = np.diff(ang) 74 | ang[np.where(ang > np.pi)] -= 2 * np.pi 75 | ang[np.where(ang < -np.pi)] += 2 * np.pi 76 | 77 | 78 | # ax1.imshow(ang, cmap='gist_rainbow') 79 | # ax1.imshow(ang, cmap='gray') 80 | # ax1.plot(values[100]) 81 | ax1.plot(np.arange(84, 0, -0.5) - 6, values / 700.0) 82 | 83 | # ys1 = ys1[:,::16] 84 | ys1 = ys1[:,850*16:950*16] 85 | 86 | if len(sys.argv) < 3: 87 | fig, (ax0, ax1) = matplotlib.pylab.subplots(nrows=2, sharex=True) 88 | plot_complex_spectrogram(ys1, ax0, ax1) 89 | # else: 90 | # TODO: Support drawing two at a time 91 | # subprocess.call(["./makeSpectrogram", "-b %d" % bins] + sys.argv[2:3]) 92 | # ys2 = np.memmap("out.raw", dtype=np.complex64, mode="r").reshape((-1, bins*8)).T 93 | # ys2 = ys2[:, ::32] 94 | 95 | # fig, (ax0, ax1, ax2, ax3) = matplotlib.pylab.subplots(nrows=4, sharex=True) 96 | # plot_complex_spectrogram(ys1, ax0, ax2) 97 | # plot_complex_spectrogram(ys2, ax1, ax3). 98 | matplotlib.pylab.show() 99 | 100 | -------------------------------------------------------------------------------- /sounds/adsrenvelope.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // An adsrSound is parameters to the algorithm that applies an 9 | // Attack/Decay/Sustain/Release envelope over a sound. 10 | // 11 | // See https://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope 12 | type adsrEnvelope struct { 13 | wrapped Sound 14 | 15 | attackSamples uint64 16 | sustainStartSamples uint64 17 | sustainEndSamples uint64 18 | sampleCount uint64 19 | sustainLevel float64 20 | } 21 | 22 | // NewADSREnvelope wraps an existing sound with a parametric envelope. 23 | // 24 | // For details, read https://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope 25 | // 26 | // For example, to create an envelope around an A440 note 27 | // s := sounds.NewADSREnvelope( 28 | // sounds.NewTimedSound(sounds.NewSineWave(261.63), 1000), 29 | // 50, 200, 0.5, 100) 30 | func NewADSREnvelope(wrapped Sound, 31 | attackMs float64, delayMs float64, sustainLevel float64, releaseMs float64) Sound { 32 | // NOTE: params are is Ms - time.Duration is possible, but likely more verbose. 33 | 34 | sampleCount := wrapped.Length() 35 | attack := time.Duration(attackMs) * time.Millisecond 36 | delay := time.Duration(delayMs) * time.Millisecond 37 | release := time.Duration(releaseMs) * time.Millisecond 38 | 39 | data := adsrEnvelope{ 40 | wrapped, 41 | DurationToSamples(attack), /* attackSamples */ 42 | DurationToSamples(attack + delay), /* sustainStartMs */ 43 | sampleCount - DurationToSamples(release), /* sustainEndMs */ 44 | sampleCount, 45 | sustainLevel, 46 | } 47 | 48 | return NewBaseSound(&data, sampleCount) 49 | } 50 | 51 | // Run generates the samples by scaling the wrapped sound by the relevant envelope part. 52 | func (s *adsrEnvelope) Run(base *BaseSound) { 53 | s.wrapped.Start() 54 | 55 | attackDelta := 1.0 / float64(s.attackSamples) 56 | decayDelta := 1.0 / float64(s.sustainStartSamples-s.attackSamples) 57 | releaseDelta := 1.0 / float64(s.sampleCount-s.sustainEndSamples) 58 | 59 | for at := uint64(0); at < s.sampleCount; at++ { 60 | scale := float64(0) // [0, 1] 61 | 62 | // NOTE: this could be split into multiple loops but it doesn't seem worth optimizing currently. 63 | switch { 64 | case at < s.attackSamples: 65 | scale = float64(at) * attackDelta 66 | case at < s.sustainStartSamples: 67 | scale = 1 - (1-s.sustainLevel)*decayDelta*float64(at-s.attackSamples) 68 | case at < s.sustainEndSamples: 69 | scale = s.sustainLevel 70 | default: 71 | scale = s.sustainLevel * releaseDelta * float64(s.sampleCount-at) 72 | } 73 | 74 | next, ok := <-s.wrapped.GetSamples() 75 | if !ok || !base.WriteSample(next*scale) { 76 | return 77 | } 78 | } 79 | } 80 | 81 | // Stop cleans up the sound by stopping the underlying sound. 82 | func (s *adsrEnvelope) Stop() { 83 | s.wrapped.Stop() 84 | } 85 | 86 | // Reset resets the underlying sound, and zeroes the position in the envelope. 87 | func (s *adsrEnvelope) Reset() { 88 | s.wrapped.Reset() 89 | } 90 | 91 | // String returns the textual representation. 92 | func (s *adsrEnvelope) String() string { 93 | // NOTE: omit the parameters for brevity. 94 | return fmt.Sprintf("Adsr[%s]", s.wrapped) 95 | } 96 | -------------------------------------------------------------------------------- /sounds/basesound.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // A SoundDefinition represents the simplified requirements that BaseSound converts into a Sound. 9 | // If possible it is recommended that implementations be immutable, with all mutable state within Run(). 10 | type SoundDefinition interface { 11 | // Run executes the normal logic of the sound, writing to base.WriteSample until it is false. 12 | Run(base *BaseSound) 13 | 14 | // Stop cleans up at the end of the Sound. 15 | Stop() 16 | 17 | // Reset rewrites all state to the same as before Run() 18 | Reset() 19 | } 20 | 21 | // A BaseSound manages state around the definition, and adapts all the Sound methods. 22 | type BaseSound struct { 23 | samples chan float64 24 | running bool 25 | sampleCount uint64 26 | duration time.Duration 27 | definition SoundDefinition 28 | } 29 | 30 | // NewBaseSound takes a simpler definition of a sound, plus a duration, and 31 | // converts them into something that implements the Sound interface. 32 | func NewBaseSound(def SoundDefinition, sampleCount uint64) Sound { 33 | duration := SamplesToDuration(sampleCount) 34 | 35 | ret := BaseSound{ 36 | nil, /* samples */ 37 | false, /* running */ 38 | sampleCount, 39 | duration, 40 | def, 41 | } 42 | return &ret 43 | } 44 | 45 | // GetSamples returns the samples for this sound, valid between a Start() and Stop() 46 | func (s *BaseSound) GetSamples() <-chan float64 { 47 | // TODO(padster): Add some tracking here to make sure that GetSamples() is only called once 48 | // between each Start() and Stop(), if possible, to avoid re-using sounds. 49 | if !s.running { 50 | // panic("Can't get samples while sound is not running...") 51 | } 52 | return s.samples 53 | } 54 | 55 | // Length returns the provided number of samples for this sound. 56 | func (s *BaseSound) Length() uint64 { 57 | return s.sampleCount 58 | } 59 | 60 | // Duration returns the duration of time the sound runs for. 61 | func (s *BaseSound) Duration() time.Duration { 62 | return s.duration 63 | } 64 | 65 | // Start begins the Sound by initialzing the channel, running the definition 66 | // on a separate goroutine, and cleaning up once it has finished. 67 | func (s *BaseSound) Start() { 68 | s.running = true 69 | s.samples = make(chan float64) 70 | 71 | // NOTE: It may make sense to move things to the other side of this goroutine boundary. 72 | // e.g. Whether to start/stop child sounds are inside the goroutine, but can be moved 73 | // outside if Run() is split into two calls, one in and one out. 74 | go func() { 75 | s.definition.Run(s) 76 | s.Stop() 77 | close(s.samples) 78 | }() 79 | } 80 | 81 | // Stop ends the sound, preventing any more samples from being written. 82 | func (s *BaseSound) Stop() { 83 | s.running = false 84 | s.definition.Stop() 85 | } 86 | 87 | // Reset clears all the state in a stopped sound back to pre-Start values. 88 | func (s *BaseSound) Reset() { 89 | if s.running { 90 | panic("Must call Stop before reset!") 91 | } 92 | s.definition.Reset() 93 | } 94 | 95 | // WriteSample appends a sample to the channel, returning whether the write was successful. 96 | func (s *BaseSound) WriteSample(sample float64) bool { 97 | if s.running { 98 | s.samples <- sample 99 | } 100 | return s.running 101 | } 102 | 103 | // Running returns whether Sound is still generating samples. 104 | func (s *BaseSound) Running() bool { 105 | return s.running 106 | } 107 | 108 | // String returns the textual representation 109 | func (s *BaseSound) String() string { 110 | return fmt.Sprintf("%s", s.definition) // Simply delegate. 111 | } 112 | -------------------------------------------------------------------------------- /sounds/channelsound.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // A ChannelSound a sound of unknown length that is generated by a provided channel. 9 | type ChannelSound struct { 10 | samples <-chan float64 11 | running bool 12 | } 13 | 14 | // WrapChannelAsSound takes an input sample channel and adapts it to be a Sound. 15 | // 16 | // For example, to play a sample channel: 17 | // output.Play(sounds.WrapChannelAsSound(..samples..)) 18 | func WrapChannelAsSound(samples <-chan float64) Sound { 19 | s := ChannelSound{ 20 | samples, 21 | false, 22 | } 23 | 24 | return &s 25 | } 26 | 27 | func (s *ChannelSound) GetSamples() <-chan float64 { 28 | if !s.running { 29 | panic("Getting samples while a sound is not running") 30 | } 31 | return s.samples 32 | } 33 | 34 | func (s *ChannelSound) Length() uint64 { 35 | return MaxLength 36 | } 37 | 38 | func (s *ChannelSound) Duration() time.Duration { 39 | return MaxDuration 40 | } 41 | 42 | func (s *ChannelSound) Start() { 43 | s.running = true 44 | } 45 | 46 | func (s *ChannelSound) Running() bool { 47 | return s.running 48 | } 49 | 50 | func (s *ChannelSound) Stop() { 51 | s.running = false 52 | } 53 | 54 | func (s *ChannelSound) Reset() { 55 | // PICK: Support this by having a buffered version and replaying the buffer? 56 | panic("Can't reset a channel sound.") 57 | } 58 | 59 | func (s *ChannelSound) String() string { 60 | return fmt.Sprintf("Wrapped channel[%s]", s.samples) 61 | } 62 | -------------------------------------------------------------------------------- /sounds/concat.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // A concat is parameters to the algorithm that concatenates multiple sounds 8 | // one after the other, to allow playing them in series. 9 | type concat struct { 10 | wrapped []Sound 11 | } 12 | 13 | // ConcatSounds creates a sound by concatenating multiple sounds in series. 14 | // 15 | // For example, to create the 5-note sequence from Close Enounters: 16 | // s := sounds.ConcatSounds( 17 | // sounds.NewTimedSound(sounds.MidiToSound(74), 400), 18 | // sounds.NewTimedSound(sounds.MidiToSound(76), 400), 19 | // sounds.NewTimedSound(sounds.MidiToSound(72), 400), 20 | // sounds.NewTimedSound(sounds.MidiToSound(60), 400), 21 | // sounds.NewTimedSound(sounds.MidiToSound(67), 1200), 22 | // ) 23 | func ConcatSounds(wrapped ...Sound) Sound { 24 | sampleCount := uint64(0) 25 | for _, child := range wrapped { 26 | childLength := child.Length() 27 | if sampleCount+childLength < childLength { // Overflow, so cap out at max. 28 | childLength = MaxLength 29 | break 30 | } else { 31 | sampleCount += childLength 32 | } 33 | } 34 | 35 | data := concat{ 36 | wrapped, 37 | } 38 | 39 | return NewBaseSound(&data, sampleCount) 40 | } 41 | 42 | // Run generates the samples by copying each wrapped sound in turn. 43 | func (s *concat) Run(base *BaseSound) { 44 | cease := false 45 | 46 | // TODO(padster): The trivial implementation leads to bad sounds at the changeover points. 47 | // The sounds should be merged together more cleanly to avoid this. 48 | for _, wrapped := range s.wrapped { 49 | wrapped.Start() 50 | for sample := range wrapped.GetSamples() { 51 | if !base.WriteSample(sample) { 52 | cease = true 53 | break 54 | } 55 | } 56 | wrapped.Stop() 57 | if cease { 58 | break 59 | } 60 | } 61 | } 62 | 63 | // Stop cleans up the sound by stopping all underlying sounds. 64 | func (s *concat) Stop() { 65 | for _, wrapped := range s.wrapped { 66 | wrapped.Stop() 67 | } 68 | } 69 | 70 | // Reset resets the all underlying sounds, and lines up playing the first sound next. 71 | func (s *concat) Reset() { 72 | for _, wrapped := range s.wrapped { 73 | wrapped.Reset() 74 | } 75 | } 76 | 77 | // String returns the textual representation 78 | func (s *concat) String() string { 79 | result := "Concat[" 80 | for i, wrapped := range s.wrapped { 81 | if i > 0 { 82 | result += " + " 83 | } 84 | result += fmt.Sprintf("%s", wrapped) 85 | } 86 | return result + "]" 87 | } 88 | -------------------------------------------------------------------------------- /sounds/delay.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/padster/go-sound/types" 8 | ) 9 | 10 | // A delay is parameters to the algorithm that adds a sound to a delayed version of itself. 11 | type delay struct { 12 | wrapped Sound 13 | delaySamples uint64 14 | buffer *types.Buffer 15 | } 16 | 17 | // AddDelay takes a sound, and adds it with a delayed version of itself after a given duration. 18 | // 19 | // For example, to have a three note progression with a delay of 123ms: 20 | // s.AddDelay(s.ConcatSounds( 21 | // s.NewTimedSound(u.MidiToSound(55), 678), 22 | // s.NewTimedSound(u.MidiToSound(59), 678), 23 | // s.NewTimedSound(u.MidiToSound(62), 678), 24 | // ), 123) 25 | func AddDelay(wrapped Sound, delayMs float64) Sound { 26 | delayDuration := time.Duration(int64(delayMs*1e6)) * time.Nanosecond 27 | delaySamples := DurationToSamples(delayDuration) 28 | 29 | data := delay{ 30 | wrapped, 31 | delaySamples, 32 | types.NewBuffer(int(delaySamples)), 33 | } 34 | 35 | return NewBaseSound(&data, wrapped.Length()) 36 | } 37 | 38 | // Run generates the samples by adding the wrapped samples to a delayed version of the channel. 39 | func (s *delay) Run(base *BaseSound) { 40 | s.wrapped.Start() 41 | for sample := range s.wrapped.GetSamples() { 42 | // Add to buffer, and read the delayed version. 43 | delayed := s.buffer.Push(sample) 44 | 45 | value := (sample + delayed) * 0.5 46 | if !base.WriteSample(value) { 47 | break 48 | } 49 | } 50 | } 51 | 52 | // Stop cleans up the sound by stopping the underlying sound. 53 | func (s *delay) Stop() { 54 | s.wrapped.Stop() 55 | } 56 | 57 | // Reset resets the underlying sound, and clears out the buffer state. 58 | func (s *delay) Reset() { 59 | s.wrapped.Reset() 60 | s.buffer.Clear() 61 | } 62 | 63 | // String returns the textual representation 64 | func (s *delay) String() string { 65 | ms := float64(SamplesToDuration(s.delaySamples)) / float64(time.Millisecond) 66 | return fmt.Sprintf("Delay[%s with delay %.2fms]", s.wrapped, ms) 67 | } 68 | -------------------------------------------------------------------------------- /sounds/denseiir.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/padster/go-sound/types" 7 | ) 8 | 9 | // A denseIIR is parameters to an Infinite Impulse Response filter, which generates 10 | // new output samples through a linear combination of previous input and output samples. 11 | type denseIIR struct { 12 | wrapped Sound 13 | inCoef []float64 14 | outCoef []float64 15 | inBuffer *types.Buffer 16 | outBuffer *types.Buffer 17 | } 18 | 19 | // NewDenseIIR wrapps a sound in an IIR filter, as specified by the coefficients. 20 | // TODO(padster): Also implement the filter design algorithms, e.g: 21 | // http://engineerjs.com/?sidebar=docs/iir.html 22 | // http://www.mikroe.com/chapters/view/73/chapter-3-iir-filters/ 23 | // http://www-users.cs.york.ac.uk/~fisher/mkfilter/ 24 | // 25 | // For example, to use a high-pass filter for 800hz+ with sample rate of 44.1k: 26 | // sound := s.NewDenseIIR(...some sound..., 27 | // []float64{0.8922, -2.677, 2.677, -0.8922}, 28 | // []float64{2.772, -2.57, 0.7961}, 29 | // ) 30 | func NewDenseIIR(wrapped Sound, inCoef []float64, outCoef []float64) Sound { 31 | // TODO(padster): Verify this is doing what it should...hard to tell just by listening. 32 | data := denseIIR{ 33 | wrapped, 34 | inCoef, 35 | outCoef, 36 | types.NewBuffer(len(inCoef)), 37 | types.NewBuffer(len(outCoef)), 38 | } 39 | return NewBaseSound(&data, wrapped.Length()) 40 | } 41 | 42 | // Run generates the samples by applying the convolution of the coefs against input/output buffers. 43 | func (s *denseIIR) Run(base *BaseSound) { 44 | s.wrapped.Start() 45 | for sample := range s.wrapped.GetSamples() { 46 | s.inBuffer.Push(sample) 47 | 48 | value := 0.0 49 | for iX, coefX := range s.inCoef { 50 | value += coefX * s.inBuffer.GetFromEnd(iX) 51 | } 52 | for iY, coefY := range s.outCoef { 53 | value += coefY * s.outBuffer.GetFromEnd(iY) 54 | } 55 | if !base.WriteSample(value) { 56 | break 57 | } 58 | } 59 | } 60 | 61 | // Stop cleans up the sound by stopping the underlying sound. 62 | func (s *denseIIR) Stop() { 63 | s.wrapped.Stop() 64 | } 65 | 66 | // Reset resets the underlying sound, and clears the buffer state. 67 | func (s *denseIIR) Reset() { 68 | s.wrapped.Reset() 69 | s.inBuffer.Clear() 70 | s.outBuffer.Clear() 71 | } 72 | 73 | // String returns the textual representation 74 | func (s *denseIIR) String() string { 75 | // TODO(padster): Pass in and use e.g. "Lowpass" etc instead. 76 | return fmt.Sprintf("DenseIIR[%s]", s.wrapped) // Coefs omitted for brevity 77 | } 78 | -------------------------------------------------------------------------------- /sounds/flacfile.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | flac "github.com/cocoonlife/goflac" 10 | // flac "github.com/padster/go-sound/fakeflac" 11 | ) 12 | 13 | // A flacFileSound is parameters to the algorithm that converts a channel from a .flac file into a sound. 14 | type flacFileSound struct { 15 | path string 16 | fileReader *flac.Decoder 17 | } 18 | 19 | // LoadFlacAsSound loads a .flac file and converts the average of its channels to a Sound. 20 | // 21 | // For example, to read the first channel from a local file at 'piano.flac': 22 | // sounds.LoadFlacAsSound("piano.flac") 23 | func LoadFlacAsSound(path string) Sound { 24 | if !strings.HasSuffix(path, ".flac") { 25 | panic("Input file must be .flac") 26 | } 27 | 28 | flacReader := loadFlacReaderOrPanic(path) 29 | 30 | if flacReader.Rate != int(CyclesPerSecond) { 31 | // TODO(padster): Support more if there's a need. 32 | panic("Only wav files that are 44.1kHz are supported.") 33 | } 34 | 35 | // TODO: Precalculate the duration properly. 36 | durationMs := MaxLength 37 | 38 | data := flacFileSound{ 39 | path, 40 | flacReader, 41 | } 42 | 43 | return NewBaseSound(&data, durationMs) 44 | } 45 | 46 | // Run generates the samples by extracting them out of the .flac file. 47 | func (s *flacFileSound) Run(base *BaseSound) { 48 | frame, err := s.fileReader.ReadFrame() 49 | for err != io.EOF { 50 | count := len(frame.Buffer) / frame.Channels 51 | 52 | for i := 0; i < count; i++ { 53 | v := 0.0 54 | for _, c := range frame.Buffer[i*frame.Channels : (i+1)*frame.Channels] { 55 | v += floatFromBitWithDepth(c, frame.Depth) 56 | } 57 | v = v / float64(frame.Channels) 58 | if !base.WriteSample(v) { 59 | err = io.EOF 60 | } 61 | } 62 | 63 | if err != io.EOF { 64 | frame, err = s.fileReader.ReadFrame() 65 | } 66 | } 67 | } 68 | 69 | // Stop cleans up this sound, closing the reader. 70 | func (s *flacFileSound) Stop() { 71 | s.fileReader.Close() 72 | } 73 | 74 | // Reset reopens the file from the start. 75 | func (s *flacFileSound) Reset() { 76 | s.Stop() 77 | s.fileReader = loadFlacReaderOrPanic(s.path) 78 | } 79 | 80 | // String returns the textual representation 81 | func (s *flacFileSound) String() string { 82 | return fmt.Sprintf("Flac[path %s]", s.path) 83 | } 84 | 85 | // loadFlacReaderOrPanic reads a flac file and handles failure cases. 86 | func loadFlacReaderOrPanic(path string) *flac.Decoder { 87 | _, err := os.Stat(path) 88 | if err != nil { 89 | panic(err) 90 | } 91 | fileReader, err := flac.NewDecoder(path) 92 | if err != nil { 93 | panic(err) 94 | } 95 | return fileReader 96 | } 97 | 98 | // HACK - should share with cq/utils.go 99 | func floatFromBitWithDepth(input int32, depth int) float64 { 100 | return float64(input) / (float64(unsafeShift(depth)) - 1.0) // Hmmm..doesn't seem right? 101 | } 102 | func intFromFloatWithDepth(input float64, depth int) int32 { 103 | return int32(input * (float64(unsafeShift(depth)) - 1.0)) 104 | } 105 | func unsafeShift(s int) int { 106 | return 1 << uint(s) 107 | } 108 | -------------------------------------------------------------------------------- /sounds/hzfromchannel.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // A hzFromChannel is parameters to the algorithm that generates a variable tone. 8 | type hzFromChannel struct { 9 | wrapped <-chan float64 10 | wrappedWithAmplitute <-chan []float64 11 | } 12 | 13 | // NewHzFromChannel takes stream of hz values, and generates a tone that sounds 14 | // like those values over time. For a fixed tone, see NewSineWave. 15 | func NewHzFromChannel(wrapped <-chan float64) Sound { 16 | return NewBaseSound(&hzFromChannel{ 17 | wrapped, 18 | nil, 19 | }, MaxLength) 20 | } 21 | 22 | func NewHzFromChannelWithAmplitude(wrappedWithAmplitute <-chan []float64) Sound { 23 | return NewBaseSound(&hzFromChannel{ 24 | nil, 25 | wrappedWithAmplitute, 26 | }, MaxLength) 27 | } 28 | 29 | // Run generates the samples by adding the wrapped samples to a delayed version of the channel. 30 | func (s *hzFromChannel) Run(base *BaseSound) { 31 | timeAt := 0.0 32 | TAU := 2.0 * math.Pi 33 | 34 | if s.wrapped != nil { 35 | for currentHz := range s.wrapped { 36 | timeDelta := TAU * (currentHz * SecondsPerCycle) 37 | timeAt = math.Mod(timeAt+timeDelta, TAU) 38 | if !base.WriteSample(math.Sin(timeAt)) { 39 | return 40 | } 41 | } 42 | } else { 43 | for hzAndAmplitude := range s.wrappedWithAmplitute { 44 | currentHz := hzAndAmplitude[0] 45 | amplitude := hzAndAmplitude[1] 46 | timeDelta := TAU * (currentHz * SecondsPerCycle) 47 | timeAt = math.Mod(timeAt+timeDelta, TAU) 48 | if !base.WriteSample(amplitude * math.Sin(timeAt)) { 49 | return 50 | } 51 | } 52 | } 53 | } 54 | 55 | // Stop cleans up the sound by stopping the underlying sound. 56 | func (s *hzFromChannel) Stop() { 57 | // NO-OP 58 | } 59 | 60 | // Reset resets the underlying sound, and clears out the buffer state. 61 | func (s *hzFromChannel) Reset() { 62 | panic("Can't reset a stream-based sound.") 63 | } 64 | 65 | // String returns the textual representation 66 | func (s *hzFromChannel) String() string { 67 | return "HzFromChannel" 68 | } 69 | -------------------------------------------------------------------------------- /sounds/karplusstrong.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | 8 | "github.com/padster/go-sound/types" 9 | ) 10 | 11 | // A karplusStrong is parameters to the algorithm that synthesizes a string sound 12 | // by playing random noise at a repeated sampling rate, but averaging the samples over time. 13 | type karplusStrong struct { 14 | hz float64 15 | sampleOverhang float64 16 | buffer *types.Buffer 17 | // 1.0 = never gets quiter / flater (just repeated noise), 0.0 = immediately flat. 18 | sustain float64 19 | } 20 | 21 | // NewKarplusStrong creates a note at a given frequency by starting with white noise 22 | // then feeding that back into itself with a delay, which ends up sounding like a string. 23 | // See http://music.columbia.edu/cmc/MusicAndComputers/chapter4/04_09.php 24 | // 25 | // For example, to create a string sound at 440hz that never gets quieter: 26 | // stringA := sounds.NewKarplusStrong(440.0, 1.0) 27 | func NewKarplusStrong(hz float64, sustain float64) Sound { 28 | if 0.0 > sustain || sustain > 1.0 { 29 | panic("Sustain must be [0, 1]") 30 | } 31 | 32 | samplesPerCycle := CyclesPerSecond / hz 33 | bufferSize := int(math.Ceil(samplesPerCycle)) 34 | 35 | buffer := types.NewBuffer(bufferSize) 36 | for i := 0; i < bufferSize; i++ { 37 | buffer.Push(rand.Float64()*2.0 - 1.0) 38 | } 39 | 40 | data := karplusStrong{ 41 | hz, 42 | float64(bufferSize) - samplesPerCycle, 43 | buffer, 44 | sustain, 45 | } 46 | return NewBaseSound(&data, MaxLength) 47 | } 48 | 49 | // Run cycles through the buffer and keep adding it to itself, linearly interpolating 50 | // off the end to make sure to get the right cycle rate. 51 | func (s *karplusStrong) Run(base *BaseSound) { 52 | lastValue := 0.0 53 | 54 | for at := float64(0.0); true; at += 1.0 { 55 | // Linterpolate off the end. 56 | lastIndex := s.buffer.Size() - 1 57 | nextValue := s.sampleOverhang*s.buffer.GetFromEnd(lastIndex) + 58 | (1.0-s.sampleOverhang)*s.buffer.GetFromEnd(lastIndex-1) 59 | 60 | // This is the important part, smoothing the new value with the previous one. 61 | thisValue := s.sustain*nextValue + (1.0-s.sustain)*lastValue 62 | if !base.WriteSample(thisValue) { 63 | break 64 | } 65 | s.buffer.Push(thisValue) 66 | lastValue = thisValue 67 | } 68 | } 69 | 70 | // Stop cleans up the sound by stopping the underlying sound. 71 | func (s *karplusStrong) Stop() { 72 | // No-op 73 | } 74 | 75 | // Reset clears the buffer back to a white-noise state. 76 | func (s *karplusStrong) Reset() { 77 | for i := 0; i < s.buffer.Size(); i++ { 78 | s.buffer.Push(rand.Float64()*2.0 - 1.0) 79 | } 80 | } 81 | 82 | // String returns the textual representation 83 | func (s *karplusStrong) String() string { 84 | return fmt.Sprintf("KarplusStrong[%.2fhz]", s.hz) 85 | } 86 | -------------------------------------------------------------------------------- /sounds/midiinput.go: -------------------------------------------------------------------------------- 1 | // +build darwin,linux,windows 2 | 3 | package sounds 4 | 5 | import ( 6 | "fmt" 7 | "math" 8 | "time" 9 | 10 | pm "github.com/rakyll/portmidi" 11 | "gopkg.in/fatih/set.v0" 12 | ) 13 | 14 | const ( 15 | nsToSeconds = 1e-9 16 | noteStart = int64(144) 17 | noteEnd = int64(128) 18 | pitchBend = int64(224) 19 | nsPerCycle = SecondsPerCycle * 1e9 20 | outputSampleBuffer = 1 // how many output samples are written in the same loop 21 | tickerDuration = time.Duration(outputSampleBuffer) * DurationPerCycle 22 | MAX_CHANNELS = 8 23 | ) 24 | 25 | // Sample organ synth 26 | var organOffset = [...]int{0, 12, 19, 24, 31, 34, 36, 38, 40} 27 | var organVolume = [...]float64{0.18, 0.15, 0.7, 0.62, 1.0, 0.52, 0.4, 0.4, 0.4} 28 | var organSum = 4.37 29 | 30 | // A MidiInput is a sound that is wrapping a portmidi Midi input device. 31 | type MidiInput struct { 32 | samples chan float64 33 | deviceId pm.DeviceID 34 | running bool 35 | // TODO(padster): use a more efficient, less general data type. 36 | notes *set.Set 37 | } 38 | 39 | // NewMidiInput takes a given midi device and converts it into a sound that plays 40 | // what the device is playing (as sine waves), and stops once a pitch-bend is received. 41 | func NewMidiInput(deviceId pm.DeviceID) Sound { 42 | ret := MidiInput{ 43 | nil, /* samples */ 44 | deviceId, 45 | false, /* running */ 46 | set.New(), /* notes */ 47 | } 48 | return &ret 49 | } 50 | 51 | // GetSamples returns the samples for this sound, valid between a Start() and Stop() 52 | func (s *MidiInput) GetSamples() <-chan float64 { 53 | // TODO(padster): Add some tracking here to make sure that GetSamples() is only called once 54 | // between each Start() and Stop(), if possible, to avoid re-using sounds. 55 | return s.samples 56 | } 57 | 58 | // Length returns the number of samples - unknown in advance, so it returns MaxLength. 59 | func (s *MidiInput) Length() uint64 { 60 | return MaxLength 61 | } 62 | 63 | // Duration returns the duration of time the sound runs for, unknown as above. 64 | func (s *MidiInput) Duration() time.Duration { 65 | return MaxDuration 66 | } 67 | 68 | // Start begins the Sound by opening two goroutines - one to take a set of active notes and convert 69 | // it into sampled sine waves at the right frequencies, and the second to to listen to the midi input 70 | // stream of events and convert that into the live set of active notes. 71 | func (s *MidiInput) Start() { 72 | fmt.Println("Starting the MIDI sound's channel...") 73 | s.samples = make(chan float64) 74 | s.running = true 75 | 76 | // Goroutine to convert the s.notes set to samples. 77 | go func(midi *MidiInput) { 78 | fmt.Printf(" MIDI generation begun!\n") 79 | atNano := float64(time.Now().UnixNano()) 80 | 81 | ticker := time.NewTicker(tickerDuration) 82 | defer ticker.Stop() 83 | 84 | for now := range ticker.C { 85 | if !midi.running { 86 | break 87 | } 88 | 89 | nowNano := float64(now.UnixNano()) 90 | for ; atNano < nowNano && midi.running; atNano += nsPerCycle { 91 | if s.notes.IsEmpty() { 92 | if midi.running { 93 | midi.samples <- 0.0 94 | } 95 | } else { 96 | cycleAtMult := atNano * nsToSeconds 97 | value := 0.0 98 | for _, note := range s.notes.List() { 99 | // TODO(padster): Remove the * -> int64 -> int cast 100 | iNote := int(note.(int64)) 101 | noteValue := 0.0 102 | for i, oOff := range organOffset { 103 | cps := midiToHz(iNote + oOff) 104 | offset := math.Remainder(cps*cycleAtMult, 1.0) * math.Pi * 2.0 105 | noteValue += math.Sin(offset) * organVolume[i] 106 | } 107 | noteValue *= 1.0 / organSum 108 | value += noteValue 109 | } 110 | if midi.running { 111 | midi.samples <- value / float64(s.notes.Size()) 112 | } 113 | } 114 | } 115 | } 116 | close(s.samples) 117 | }(s) 118 | 119 | // Goroutine for reading from the input: 120 | go func() { 121 | fmt.Println(" Opening MIDI stream..") 122 | in, err := pm.NewInputStream(s.deviceId, 10) 123 | if err != nil { 124 | fmt.Printf("Error in reading midi device %d: Ensure portmidi is Initialized, and device is available.\n", s.deviceId) 125 | panic(err) 126 | } 127 | 128 | fmt.Println("Listening to stream") 129 | for event := range in.Listen() { 130 | // TODO - figure out what event.Data2 is (volumne?) and use it... 131 | fmt.Printf("Got: %v\n", event) 132 | if event.Status >= noteStart && event.Status < noteStart+MAX_CHANNELS { 133 | channel := event.Status - noteStart 134 | note := int64(event.Data1) 135 | // Drop channel 0 an octave 136 | if channel == 0 { 137 | note -= 12 138 | } 139 | s.notes.Add(note) 140 | } else if event.Status >= noteEnd && event.Status < noteEnd+MAX_CHANNELS { 141 | channel := event.Status - noteEnd 142 | note := int64(event.Data1) 143 | // Drop channel 0 an octave 144 | if channel == 0 { 145 | note -= 12 146 | } 147 | s.notes.Remove(note) 148 | } else if event.Status == pitchBend { 149 | s.Stop() 150 | } 151 | if !s.running { 152 | break 153 | } 154 | } 155 | }() 156 | // TODO(padster): Move goroutines into struct methods? 157 | } 158 | 159 | // Stop ends the sound, preventing any more samples from being written. 160 | func (s *MidiInput) Stop() { 161 | // TODO(padster): close midi stream, stop timer. 162 | s.running = false 163 | } 164 | 165 | // Reset unsupported for the MIDI stream. 166 | func (s *MidiInput) Reset() { 167 | panic("Can't reset live sound") 168 | } 169 | 170 | // Running returns whether Sound is still generating samples. 171 | func (s *MidiInput) Running() bool { 172 | return s.running 173 | } 174 | 175 | // String returns the textual representation 176 | func (s *MidiInput) String() string { 177 | return fmt.Sprintf("Midi[device #%d]", s.deviceId) 178 | } 179 | 180 | // TODO(padster): Merge this with the parser.go version once deps are sorted out. 181 | // Also, it'd be good to precalculate all of these, to make midiToHz just a lookup. 182 | var freq = []float64{ 183 | 16.35, // C 184 | 17.32, // C#/Db 185 | 18.35, // D 186 | 19.45, // D#/Eb 187 | 20.60, // E 188 | 21.83, // F 189 | 23.12, // F#/Gb 190 | 24.50, // G 191 | 25.96, // G#/Ab 192 | 27.50, // A 193 | 29.14, // A#/Bb 194 | 30.87, // B 195 | } 196 | 197 | func midiToHz(midiNote int) float64 { 198 | // Assuming C0 hz == 12 midi 199 | octave := midiNote/12 - 1 200 | semitone := midiNote % 12 201 | scale := 1 << uint(octave) 202 | return freq[semitone] * float64(scale) 203 | } 204 | -------------------------------------------------------------------------------- /sounds/multiply.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // A multiply is parameters to the algorithm that scales the amplitude of a sound. 8 | type multiply struct { 9 | wrapped Sound 10 | factor float64 11 | } 12 | 13 | // MultiplyWithClip wraps an existing sound and scales its amplitude by a given factory, 14 | // clipping the result to [-1, 1]. 15 | // 16 | // For example, to create a sound half as loud as the default E5 sine wave: 17 | // s := sounds.MultiplyWithClip(sounds.NewSineWave(659.25), 0.5) 18 | func MultiplyWithClip(wrapped Sound, factor float64) Sound { 19 | data := multiply{ 20 | wrapped, 21 | factor, 22 | } 23 | 24 | return NewBaseSound(&data, wrapped.Length()) 25 | } 26 | 27 | // Run generates the samples by scaling the wrapped sound's samples, clipping to the valid range. 28 | func (s *multiply) Run(base *BaseSound) { 29 | s.wrapped.Start() 30 | for sample := range s.wrapped.GetSamples() { 31 | scaled := s.factor * sample 32 | if scaled > 1 { 33 | scaled = 1.0 34 | } else if scaled < -1 { 35 | scaled = -1 36 | } 37 | if !base.WriteSample(scaled) { 38 | break 39 | } 40 | } 41 | } 42 | 43 | // Stop cleans up the sound by stopping the underlying sound. 44 | func (s *multiply) Stop() { 45 | s.wrapped.Stop() 46 | } 47 | 48 | // Reset resets the underlying sound. 49 | func (s *multiply) Reset() { 50 | s.wrapped.Reset() 51 | } 52 | 53 | // String returns the textual representation 54 | func (s *multiply) String() string { 55 | return fmt.Sprintf("Multiple[%s scaled by %.2f]", s.wrapped, s.factor) 56 | } 57 | -------------------------------------------------------------------------------- /sounds/normalsum.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // A normalSum is parameters to the algorithm that adds together sounds in parallel, 8 | // normalized to avoid going outside [-1, 1] 9 | type normalSum struct { 10 | wrapped []Sound 11 | normScalar float64 12 | } 13 | 14 | // SumSounds creates a sound by adding multiple sounds in parallel, playing them 15 | // at the same time and normalizing their volume. 16 | // 17 | // For example, to play a G7 chord for a second: 18 | // s := sounds.SumSounds( 19 | // sounds.NewTimedSound(sounds.MidiToSound(55), 1000), 20 | // sounds.NewTimedSound(sounds.MidiToSound(59), 1000), 21 | // sounds.NewTimedSound(sounds.MidiToSound(62), 1000), 22 | // sounds.NewTimedSound(sounds.MidiToSound(65), 1000), 23 | // sounds.NewTimedSound(sounds.MidiToSound(67), 1000), 24 | // ) 25 | func SumSounds(wrapped ...Sound) Sound { 26 | if len(wrapped) == 0 { 27 | panic("SumSounds can't take no sounds") 28 | } 29 | 30 | sampleCount := MaxLength 31 | for _, child := range wrapped { 32 | childLength := child.Length() 33 | if childLength < sampleCount { 34 | sampleCount = childLength 35 | } 36 | } 37 | 38 | data := normalSum{ 39 | wrapped, 40 | 1.0 / float64(len(wrapped)), /* normScalar */ 41 | } 42 | 43 | return NewBaseSound(&data, sampleCount) 44 | } 45 | 46 | // Run generates the samples by summing all the wrapped samples and normalizing. 47 | func (s *normalSum) Run(base *BaseSound) { 48 | for _, wrapped := range s.wrapped { 49 | wrapped.Start() 50 | } 51 | 52 | for { 53 | sum := 0.0 54 | for _, wrapped := range s.wrapped { 55 | sample, stream_ok := <-wrapped.GetSamples() 56 | if !stream_ok || !base.Running() { 57 | base.Stop() 58 | break 59 | } 60 | sum += sample 61 | } 62 | 63 | if !base.WriteSample(sum * s.normScalar) { 64 | break 65 | } 66 | } 67 | } 68 | 69 | // Stop cleans up the sound by stopping all underlyings sound. 70 | func (s *normalSum) Stop() { 71 | for _, wrapped := range s.wrapped { 72 | wrapped.Stop() 73 | } 74 | } 75 | 76 | // Reset resets all underlying sounds. 77 | func (s *normalSum) Reset() { 78 | for _, wrapped := range s.wrapped { 79 | wrapped.Reset() 80 | } 81 | } 82 | 83 | // String returns the textual representation 84 | func (s *normalSum) String() string { 85 | result := "Sum[" 86 | for i, wrapped := range s.wrapped { 87 | if i > 0 { 88 | result += " & " 89 | } 90 | result += fmt.Sprintf("%s", wrapped) 91 | } 92 | return result + "]" 93 | } 94 | -------------------------------------------------------------------------------- /sounds/repeater.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // A repeater is parameters to the algorithm that repeats a sound a given number of times. 9 | type repeater struct { 10 | wrapped Sound 11 | loopCount int32 12 | } 13 | 14 | // RepeatSound forms a sound by repeating a given sound a number of times in series. 15 | // 16 | // For example, for the cello part of Pachelbel's Canon in D: 17 | // sound := s.RepeatSound(s.ConcatSounds( 18 | // s.NewTimedSound(s.MidiToSound(50), 800), 19 | // s.NewTimedSound(s.MidiToSound(45), 800), 20 | // s.NewTimedSound(s.MidiToSound(47), 800), 21 | // s.NewTimedSound(s.MidiToSound(42), 800), 22 | // s.NewTimedSound(s.MidiToSound(43), 800), 23 | // s.NewTimedSound(s.MidiToSound(38), 800), 24 | // s.NewTimedSound(s.MidiToSound(43), 800), 25 | // s.NewTimedSound(s.MidiToSound(45), 800), 26 | // ), -1 /* repeat indefinitely */) 27 | func RepeatSound(wrapped Sound, loopCount int32) Sound { 28 | // Negative loop count == loop indefinitely 29 | if loopCount < 0 { 30 | loopCount = math.MaxInt32 31 | } 32 | 33 | sampleCount := wrapped.Length() 34 | if sampleCount != MaxLength { 35 | sampleCount *= uint64(loopCount) 36 | } 37 | 38 | data := repeater{ 39 | wrapped, 40 | loopCount, 41 | } 42 | return NewBaseSound(&data, sampleCount) 43 | } 44 | 45 | // Run generates the samples by copying from the wrapped sound multiple times. 46 | func (s *repeater) Run(base *BaseSound) { 47 | cease := false 48 | 49 | // NOTE: See concat, this leads to bad sounds at reset points. 50 | for loopAt := int32(0); !cease && loopAt < s.loopCount; loopAt++ { 51 | s.wrapped.Start() 52 | 53 | for sample := range s.wrapped.GetSamples() { 54 | if !base.WriteSample(sample) { 55 | cease = true 56 | } 57 | } 58 | 59 | s.wrapped.Stop() 60 | if !cease { 61 | s.wrapped.Reset() 62 | } 63 | } 64 | } 65 | 66 | // Stop cleans up the sound by stopping the underlying sound. 67 | func (s *repeater) Stop() { 68 | s.wrapped.Stop() 69 | } 70 | 71 | // Reset resets the underlying sound, plus the loop count tracking. 72 | func (s *repeater) Reset() { 73 | s.wrapped.Reset() 74 | } 75 | 76 | // String returns the textual representation 77 | func (s *repeater) String() string { 78 | return fmt.Sprintf("Repeat[%s, %d times]", s.wrapped, s.loopCount) 79 | } 80 | -------------------------------------------------------------------------------- /sounds/sampler.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // A linearSampler is parameters to the algorithm that forms a sound by 8 | // sampling a second sound, and linearly interpolating that to a different sample rate. 9 | type linearSampler struct { 10 | wrapped Sound 11 | pitchScale float64 12 | } 13 | 14 | // LinearSample wraps an existing sound and samples it at a different rate, 15 | // modifying both its pitch and duration. 16 | // 17 | // For example, to modulate a sound up an octave, and make it half as long: 18 | // s := ...some sound... 19 | // higher := sounds.LinearSample(s, 2.0) 20 | func LinearSample(wrapped Sound, pitchScale float64) Sound { 21 | newLength := MaxLength 22 | if wrapped.Length() < MaxLength { 23 | newLength = uint64(float64(wrapped.Length()) / pitchScale) 24 | } 25 | 26 | data := linearSampler{ 27 | wrapped, 28 | pitchScale, 29 | } 30 | return NewBaseSound(&data, newLength) 31 | } 32 | 33 | // Run generates the samples by iterating through the origin, and 34 | // resampling at the required rate, linearly interpolating to calculate the new samples. 35 | func (s *linearSampler) Run(base *BaseSound) { 36 | s.wrapped.Start() 37 | 38 | last := float64(0.0) 39 | 40 | for at := float64(0.0); true; at -= 1.0 { 41 | current, ok := <-s.wrapped.GetSamples() 42 | if !ok { 43 | break 44 | } 45 | 46 | for ; -1-1e-9 < at && at < 1e-9; at += s.pitchScale { 47 | // at == -1 -> last, at == 0 -> current, so: 48 | sample := current + at*(current-last) 49 | if !base.WriteSample(sample) { 50 | break 51 | } 52 | } 53 | 54 | last = current 55 | } 56 | } 57 | 58 | // Stop cleans up the sound by stopping the underlying sound. 59 | func (s *linearSampler) Stop() { 60 | s.wrapped.Stop() 61 | } 62 | 63 | // Reset resets the underlying sound, and restarts the sample tracker. 64 | func (s *linearSampler) Reset() { 65 | s.wrapped.Reset() 66 | } 67 | 68 | // String returns the textual representation 69 | func (s *linearSampler) String() string { 70 | return fmt.Sprintf("Sampled[%s at %.2f]", s.wrapped, s.pitchScale) 71 | } 72 | -------------------------------------------------------------------------------- /sounds/silence.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | // A silence is parameters to the algorithm that generates silence. 4 | type silence struct{} 5 | 6 | // NewSilence creates an unending sound that is inaudible. 7 | func NewSilence() Sound { 8 | data := silence{} 9 | return NewBaseSound(&data, MaxLength) 10 | } 11 | 12 | // NewTimedSilence creates a silence that lasts for a given duration. 13 | // 14 | // For example, Cage's 4'33" can be generated using: 15 | // s := sounds.NewTimedSilence(273000) 16 | func NewTimedSilence(durationMs float64) Sound { 17 | return NewTimedSound(NewSilence(), durationMs) 18 | } 19 | 20 | // Run generates the samples by continuously writing 0 (silence). 21 | func (s *silence) Run(base *BaseSound) { 22 | for base.WriteSample(0) { 23 | // No-op 24 | } 25 | } 26 | 27 | // Stop cleans up the silence, in this case doing nothing. 28 | func (s *silence) Stop() { 29 | // No-op 30 | } 31 | 32 | // Reset does nothing in the case of silence, as there is no state. 33 | func (s *silence) Reset() { 34 | // No-op 35 | } 36 | 37 | // String returns the textual representation, in this case fixed. 38 | func (s *silence) String() string { 39 | return "Silence" 40 | } 41 | -------------------------------------------------------------------------------- /sounds/simplewaves.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | type SimpleSampleMap func(float64) float64 9 | 10 | // A simpleWave is parameters to the algorithm that generates a sound wave by cycling a particular periodic 11 | // shape at a given frequency. 12 | type simpleWave struct { 13 | hz float64 14 | timeDelta float64 15 | mapper SimpleSampleMap 16 | } 17 | 18 | // NewSimpleWave creates an unending repeating sound based on cycles defined by a given mapping function. 19 | // For examples of usage, see sine/square/sawtooth/triangle waves below. 20 | func NewSimpleWave(hz float64, mapper SimpleSampleMap) Sound { 21 | return NewBaseSound(&simpleWave{hz, hz * SecondsPerCycle, mapper}, MaxLength) 22 | } 23 | 24 | // NewSineWave creates an unending sinusoid at a given pitch (in hz). 25 | // 26 | // For example, to create a sound represeting A440: 27 | // s := sounds.NewSineWave(440) 28 | func NewSineWave(hz float64) Sound { 29 | return NewSimpleWave(hz, SineMap) 30 | } 31 | 32 | // NewSquareWave creates an unending [-1, 1] square wave at a given pitch. 33 | func NewSquareWave(hz float64) Sound { 34 | return NewSimpleWave(hz, SquareMap) 35 | } 36 | 37 | // NewSawtoothWave creates an unending sawtooth pattern (-1->1 then resets to -1.) 38 | func NewSawtoothWave(hz float64) Sound { 39 | return NewSimpleWave(hz, SawtoothMap) 40 | } 41 | 42 | // NewTriangleWave creates an unending triangle pattern (-1->1->-1 linearly) 43 | func NewTriangleWave(hz float64) Sound { 44 | return NewSimpleWave(hz, TriangleMap) 45 | } 46 | 47 | // Run generates the samples by creating a sine wave at the desired frequency. 48 | func (s *simpleWave) Run(base *BaseSound) { 49 | // NOTE: Will overflow if run too long. 50 | for timeAt := float64(0); true; _, timeAt = math.Modf(timeAt + s.timeDelta) { 51 | if !base.WriteSample(s.mapper(timeAt)) { 52 | return 53 | } 54 | } 55 | } 56 | 57 | // Stop cleans up the sound, in this case doing nothing. 58 | func (s *simpleWave) Stop() { 59 | // No-op 60 | } 61 | 62 | // Reset sets the offset in the wavelength back to zero. 63 | func (s *simpleWave) Reset() { 64 | // No-op 65 | } 66 | 67 | // String returns the textual representation 68 | func (s *simpleWave) String() string { 69 | return fmt.Sprintf("Hz[%.2f]", s.hz) 70 | } 71 | 72 | // Below are some sample mappers used for generating various useful shapes. 73 | func SineMap(at float64) float64 { 74 | return math.Sin(at * 2.0 * math.Pi) 75 | } 76 | func SquareMap(at float64) float64 { 77 | if at < 0.5 { 78 | return -1.0 79 | } else { 80 | return 1.0 81 | } 82 | } 83 | func SawtoothMap(at float64) float64 { 84 | return at*2.0 - 1.0 85 | } 86 | func TriangleMap(at float64) float64 { 87 | if at > 0.5 { 88 | at = 1.0 - at 89 | } 90 | return at*2.0*2.0 - 1.0 91 | } 92 | -------------------------------------------------------------------------------- /sounds/slicesound.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // A sliceSound is the slice of samples that back the created Sound. 8 | type sliceSound struct { 9 | samples []float64 10 | } 11 | 12 | // WrapSliceAsSound wraps an already created slice of [-1, 1] as a sound. 13 | func WrapSliceAsSound(samples []float64) Sound { 14 | data := sliceSound{samples} 15 | return NewBaseSound(&data, uint64(len(samples))) 16 | } 17 | 18 | // Run generates the samples by simply iterating through the provided slice. 19 | func (s *sliceSound) Run(base *BaseSound) { 20 | for _, sample := range s.samples { 21 | if !base.WriteSample(sample) { 22 | break 23 | } 24 | } 25 | } 26 | 27 | // Stop cleans up the sound, in this case doing nothing. 28 | func (s *sliceSound) Stop() { 29 | // No-op all stopping is done in base. 30 | } 31 | 32 | // Reset resets the sound, in this case doing nothing. 33 | func (s *sliceSound) Reset() { 34 | // No-op, sound gets reset in Run() 35 | } 36 | 37 | // String returns the textual representation 38 | func (s *sliceSound) String() string { 39 | return fmt.Sprintf("SliceSound[%d samples]", len(s.samples)) 40 | } 41 | -------------------------------------------------------------------------------- /sounds/sound.go: -------------------------------------------------------------------------------- 1 | // Package sounds provides the basic types for Sounds within this system, 2 | // plus multiple implementations of different sounds that can be used. 3 | package sounds 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | const ( 10 | // The sample rate of each sound stream. 11 | CyclesPerSecond = 44100.0 12 | 13 | // Inverse sample rate. 14 | SecondsPerCycle = 1.0 / CyclesPerSecond 15 | 16 | // Inverse sample rate as a golang duration 17 | // DurationPerCycle = int64(SecondsPerCycle * 1e9) * time.Nanosecond 18 | DurationPerCycle = 22675 * time.Nanosecond // BIG HACK 19 | 20 | // The number of samples in the maximum duration. 21 | MaxLength = uint64(406750706825295) 22 | // HACK - Go doesn't allow uint64(float64(math.MaxInt64) * 0.000000001 * CyclesPerSecond) :( 23 | 24 | // Maximum duration, used for unending sounds. 25 | MaxDuration = time.Duration(int64(float64(MaxLength)*SecondsPerCycle*1e9)) * time.Nanosecond 26 | ) 27 | 28 | // A Sound is a model of a physical sound wave as a series of pressure changes over time. 29 | // 30 | // Each Sound contains a channel of samples in the range [-1, 1] of the intensity at each time step, 31 | // as well as a count of samples, which then also defines how long the sound lasts. 32 | // 33 | // Sounds also provide a way to start and stop when the samples are written, and reset to an initial state. 34 | type Sound interface { 35 | // Sound wave samples for the sound - only valid after Start() and before Stop() 36 | // NOTE: Only one sink should read from GetSamples(). Otherwise it will not receive every sample. 37 | GetSamples() <-chan float64 38 | 39 | // Number of samples in this sound, MaxLength if unlimited. 40 | Length() uint64 41 | 42 | // Length of time this goes for. Convenience method, should always be SamplesToDuration(Length()) 43 | Duration() time.Duration 44 | 45 | // Start begins writing the sound wave to the samples channel. 46 | Start() 47 | 48 | // Running indicates whether a sound has Start()'d but not yet Stop()'d 49 | Running() bool 50 | 51 | // Stop ceases writing samples, and closes the channel. 52 | Stop() 53 | 54 | // Reset converts the sound back to the pre-Start() state. Can only be called on a Stop()'d Sound. 55 | Reset() 56 | } 57 | 58 | // SamplesToDuration converts a sample count to a duration of time. 59 | func SamplesToDuration(sampleCount uint64) time.Duration { 60 | return time.Duration(int64(float64(sampleCount)*1e9*SecondsPerCycle)) * time.Nanosecond 61 | } 62 | 63 | func DurationToSamples(duration time.Duration) uint64 { 64 | return uint64(float64(duration.Nanoseconds()) * 1e-9 * CyclesPerSecond) 65 | } 66 | 67 | /* 68 | Likely TODO list order: 69 | - Pulseaudio (mic) input 70 | - Mathematically simple effects (chorus & reverb, from http://www.ti.com/lit/an/spraaa5/spraaa5.pdf) 71 | - Reduce MIDI input -> Wav output delay 72 | - Synchronize to allow output.Play() and output.Render() at the same time. 73 | - Sound based off cached float64 slice. 74 | - Add support for Travis CI or similar. 75 | - Pitch shifter (phased vocoder): http://www.guitarpitchshifter.com/algorithm.html or http://www.ee.columbia.edu/ln/labrosa/matlab/pvoc/ 76 | - Duration changer: Resample + Pitch shifter? vs. Time-domain harmonic scaling / PSOLA (http://research.spa.aalto.fi/publications/theses/lemmetty_mst/thesis.pdf) 77 | - (possibly): Replace Reset() with Clone(), and not allow anything post-Stop(). 78 | */ 79 | -------------------------------------------------------------------------------- /sounds/timedsound.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // A timedSound is parameters to the algorithm that limits a sound to a given duration. 9 | type timedSound struct { 10 | wrapped Sound 11 | sampleCount uint64 12 | } 13 | 14 | // NewSilence wraps an existing sound as something that stops after a given duration. 15 | // 16 | // For example, to create a sound of middle C that lasts a second: 17 | // s := sounds.NewTimedSound(sounds.NewSineWave(261.63), 1000) 18 | func NewTimedSound(wrapped Sound, durationMs float64) Sound { 19 | // NOTE: duration is Ms - time.Duration is possible, but likely more verbose. 20 | duration := time.Duration(int64(durationMs*1e6)) * time.Nanosecond 21 | sampleCount := DurationToSamples(duration) 22 | 23 | if wrapped.Length() < sampleCount { 24 | // TODO(padster) - perhaps instead pad out with timed silence? 25 | panic(fmt.Sprintf( 26 | "Can't time a sound longer than it starts out, %d < %d\n", 27 | wrapped.Length(), sampleCount)) 28 | } 29 | 30 | data := timedSound{ 31 | wrapped, 32 | sampleCount, 33 | } 34 | 35 | return NewBaseSound(&data, sampleCount) 36 | } 37 | 38 | // Run generates the samples by copying the wrapped sound, stopping after the set time. 39 | func (s *timedSound) Run(base *BaseSound) { 40 | s.wrapped.Start() 41 | for at := uint64(0); at < s.sampleCount; at++ { 42 | value, ok := <-s.wrapped.GetSamples() 43 | if !ok { 44 | // NOTE: should not happen. 45 | break 46 | } 47 | if !base.WriteSample(value) { 48 | break 49 | } 50 | } 51 | } 52 | 53 | // Stop cleans up the sound by stopping the underlying sound. 54 | func (s *timedSound) Stop() { 55 | s.wrapped.Stop() 56 | } 57 | 58 | // Reset resets the underlying sound, and restarts the sample tracker. 59 | func (s *timedSound) Reset() { 60 | s.wrapped.Reset() 61 | } 62 | 63 | // String returns the textual representation 64 | func (s *timedSound) String() string { 65 | ms := float64(SamplesToDuration(s.sampleCount)) / float64(time.Millisecond) 66 | return fmt.Sprintf("Timed[%s for %.2fms]", s.wrapped, ms) 67 | } 68 | -------------------------------------------------------------------------------- /sounds/wavfile.go: -------------------------------------------------------------------------------- 1 | package sounds 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | 8 | wav "github.com/cryptix/wav" 9 | ) 10 | 11 | const ( 12 | normScale = float64(1) / float64(math.MaxInt16) 13 | ) 14 | 15 | // A wavFileSound is parameters to the algorithm that converts a channel from a .wav file into a sound. 16 | type wavFileSound struct { 17 | path string 18 | channel uint16 19 | 20 | // TODO(padster): Clean this up, sounds shouldn't have mutable state beyond child sounds. 21 | // samplesLeft can be removed, the other two may require edits to the wav library. 22 | wavReader *wav.Reader 23 | meta wav.File 24 | samplesLeft uint32 25 | } 26 | 27 | // LoadWavAsSound loads a .wav file and converts one of its channels into a Sound. 28 | // 29 | // For example, to read the first channel from a local file at 'piano.wav': 30 | // sounds.LoadWavAsSound("piano.wav", 0) 31 | func LoadWavAsSound(path string, channel uint16) Sound { 32 | wavReader := loadWavReaderOrPanic(path) 33 | 34 | meta := wavReader.GetFile() 35 | if meta.Channels <= channel { 36 | panic("Unsupported channel number.") 37 | } 38 | if meta.SampleRate != uint32(CyclesPerSecond) { 39 | // TODO(padster): Support more if there's a need. 40 | panic("Only wav files that are 44.1kHz are supported.") 41 | } 42 | 43 | data := wavFileSound{ 44 | path, 45 | channel, 46 | wavReader, 47 | meta, 48 | wavReader.GetSampleCount(), /* samplesLeft */ 49 | } 50 | 51 | return NewBaseSound(&data, uint64(wavReader.GetSampleCount())) 52 | } 53 | 54 | // Run generates the samples by extracting them out of the .wav file. 55 | func (s *wavFileSound) Run(base *BaseSound) { 56 | for s.samplesLeft > 0 { 57 | // Read all channels, but pick just the one we want. 58 | selected := float64(0) 59 | for i := uint16(0); i < s.meta.Channels; i++ { 60 | n, err := s.wavReader.ReadSample() 61 | if err != nil { 62 | base.Stop() 63 | break 64 | } 65 | if i == s.channel { 66 | // Need this to convert the 16-bit integer into a [-1, 1] float sample. 67 | selected = float64(int16(n)) * normScale 68 | } 69 | } 70 | 71 | if !base.WriteSample(selected) { 72 | break 73 | } 74 | s.samplesLeft-- 75 | } 76 | } 77 | 78 | // Stop cleans up this sound, in this case doing nothing. 79 | func (s *wavFileSound) Stop() { 80 | // NOTE: It seems like the reader and file API have no Close cleanup. 81 | } 82 | 83 | // Reset reopens the file from the start. 84 | func (s *wavFileSound) Reset() { 85 | s.wavReader = loadWavReaderOrPanic(s.path) 86 | s.meta = s.wavReader.GetFile() 87 | s.samplesLeft = s.wavReader.GetSampleCount() 88 | } 89 | 90 | // String returns the textual representation 91 | func (s *wavFileSound) String() string { 92 | return fmt.Sprintf("Wav[channel %d from path %s]", s.channel, s.path) 93 | } 94 | 95 | // loadWavReaderOrPanic reads a wav file and handles failure cases. 96 | func loadWavReaderOrPanic(path string) *wav.Reader { 97 | testInfo, err := os.Stat(path) 98 | if err != nil { 99 | panic(err) 100 | } 101 | testWav, err := os.Open(path) 102 | if err != nil { 103 | panic(err) 104 | } 105 | result, err := wav.NewReader(testWav, testInfo.Size()) 106 | if err != nil { 107 | panic(err) 108 | } 109 | return result 110 | } 111 | -------------------------------------------------------------------------------- /spectrogramshift.go: -------------------------------------------------------------------------------- 1 | // go run spectrogramshift.go --shift= 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "math" 8 | "math/cmplx" 9 | "runtime" 10 | 11 | "github.com/padster/go-sound/cq" 12 | f "github.com/padster/go-sound/file" 13 | "github.com/padster/go-sound/output" 14 | s "github.com/padster/go-sound/sounds" 15 | ) 16 | 17 | // Takes a spectrogram, applies a shift, inverts back and plays the result. 18 | func main() { 19 | // Needs to be at least 2 when doing openGL + sound output at the same time. 20 | runtime.GOMAXPROCS(3) 21 | 22 | sampleRate := s.CyclesPerSecond 23 | octaves := flag.Int("octaves", 7, "Range in octaves") 24 | minFreq := flag.Float64("minFreq", 55.0, "Minimum frequency") 25 | semitones := flag.Int("shift", 0, "Semitones to shift") 26 | bpo := flag.Int("bpo", 24, "Buckets per octave") 27 | flag.Parse() 28 | 29 | remainingArgs := flag.Args() 30 | argCount := len(remainingArgs) 31 | if argCount < 1 || argCount > 2 { 32 | panic("Required: [output] argument") 33 | } 34 | inputFile := remainingArgs[0] 35 | 36 | // Note: scale the output frequency by this to change pitch dilation into time dilation 37 | // shift := math.Pow(2.0, float64(-*semitones) / 12.0) 38 | 39 | paramsIn := cq.NewCQParams(sampleRate, *octaves, *minFreq, *bpo) 40 | paramsOut := cq.NewCQParams(sampleRate, *octaves, *minFreq, *bpo) 41 | 42 | spectrogram := cq.NewSpectrogram(paramsIn) 43 | cqInverse := cq.NewCQInverse(paramsOut) 44 | 45 | inputSound := f.Read(inputFile) 46 | inputSound.Start() 47 | defer inputSound.Stop() 48 | 49 | fmt.Printf("Running...\n") 50 | columns := spectrogram.ProcessChannel(inputSound.GetSamples()) 51 | outColumns := shiftSpectrogram( 52 | *semitones*(*bpo/12), 0, flipSpectrogram(columns, *octaves, *bpo), *octaves, *bpo) 53 | soundChannel := cqInverse.ProcessChannel(outColumns) 54 | resultSound := s.WrapChannelAsSound(soundChannel) 55 | 56 | // HACK: Amplify for now. 57 | resultSound = s.MultiplyWithClip(resultSound, 3.0) 58 | 59 | if argCount == 2 { 60 | f.Write(resultSound, remainingArgs[1]) 61 | } else { 62 | output.Play(resultSound) 63 | } 64 | } 65 | 66 | func shiftSpectrogram(binOffset int, sampleOffset int, samples <-chan []complex128, octaves int, bpo int) <-chan []complex128 { 67 | result := make(chan []complex128) 68 | 69 | go func() { 70 | ignoreSamples := sampleOffset 71 | at := 0 72 | for s := range samples { 73 | if ignoreSamples > 0 { 74 | ignoreSamples-- 75 | continue 76 | } 77 | 78 | octaveCount := octaves 79 | if at > 0 { 80 | octaveCount = numOctaves(at) 81 | if octaveCount == octaves { 82 | at = 0 83 | } 84 | } 85 | at++ 86 | 87 | toFill := octaveCount * bpo 88 | column := make([]complex128, toFill, toFill) 89 | 90 | // NOTE: Zero-padded, not the best... 91 | if binOffset >= 0 { 92 | copy(column, s[binOffset:]) 93 | } else { 94 | copy(column[-binOffset:], s) 95 | } 96 | result <- column 97 | } 98 | close(result) 99 | }() 100 | return result 101 | } 102 | 103 | func numOctaves(at int) int { 104 | result := 1 105 | for at%2 == 0 { 106 | at /= 2 107 | result++ 108 | } 109 | return result 110 | } 111 | 112 | func clone(values []complex128) []complex128 { 113 | result := make([]complex128, len(values), len(values)) 114 | for i, v := range values { 115 | result[i] = v 116 | } 117 | return result 118 | } 119 | 120 | func flipSpectrogram(samples <-chan []complex128, octaves int, bpo int) <-chan []complex128 { 121 | result := make(chan []complex128) 122 | go func() { 123 | var phaseAt []float64 = nil 124 | for s := range samples { 125 | if phaseAt == nil { 126 | phaseAt = make([]float64, len(s), len(s)) 127 | } 128 | for i, v := range s { 129 | vp := cmplx.Phase(v) 130 | vp = makeCloser(phaseAt[i], vp) 131 | phaseAt[i] = vp 132 | } 133 | 134 | newSample := make([]complex128, len(s), len(s)) 135 | for i := 0; i < len(s); i++ { 136 | newSample[i] = s[i] 137 | } 138 | 139 | for i := 0; i < len(s); i++ { 140 | other := len(s) - 1 - i 141 | pFactor := float64(octaves) - float64(2*i+1)/float64(bpo) 142 | phase := phaseAt[other] / math.Pow(2.0, pFactor) 143 | newSample[i] = cmplx.Rect(cmplx.Abs(s[other]), phase) 144 | } 145 | 146 | result <- newSample 147 | } 148 | close(result) 149 | }() 150 | return result 151 | } 152 | 153 | // Return the closest number X to toShift, such that X mod Tau == modTwoPi 154 | func makeCloser(toShift, modTau float64) float64 { 155 | if math.IsNaN(modTau) { 156 | modTau = 0.0 157 | } 158 | // Minimize |toShift - (modTau + tau * cyclesToAdd)| 159 | // toShift - modTau - tau * CTA = 0 160 | cyclesToAdd := (toShift - modTau) / cq.TAU 161 | return modTau + float64(cq.Round(cyclesToAdd))*cq.TAU 162 | } 163 | -------------------------------------------------------------------------------- /test/adsr.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/adsr.wav -------------------------------------------------------------------------------- /test/concat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/concat.wav -------------------------------------------------------------------------------- /test/delay.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/delay.wav -------------------------------------------------------------------------------- /test/denseiir.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/denseiir.wav -------------------------------------------------------------------------------- /test/multiply.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/multiply.wav -------------------------------------------------------------------------------- /test/normalsum.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/normalsum.wav -------------------------------------------------------------------------------- /test/repeat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/repeat.wav -------------------------------------------------------------------------------- /test/sampler.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/sampler.wav -------------------------------------------------------------------------------- /test/samples.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | s "github.com/padster/go-sound/sounds" 5 | u "github.com/padster/go-sound/util" 6 | ) 7 | 8 | // samples.go includes all the example wavs used by the testing package, 9 | // and written .wav golden files to this folder. 10 | 11 | func SampleTimedSineSound() s.Sound { 12 | // Includes: SineWave 13 | return s.NewTimedSound(s.NewSineWave(261.63), 1000) 14 | } 15 | 16 | func SampleTimedSquareSound() s.Sound { 17 | // Includes: SquareWave 18 | return s.NewTimedSound(s.NewSquareWave(261.63), 1000) 19 | } 20 | 21 | func SampleTimedSawtoothSound() s.Sound { 22 | // Includes: SawtoothWave 23 | return s.NewTimedSound(s.NewSawtoothWave(261.63), 1000) 24 | } 25 | 26 | func SampleTimedTriangleSound() s.Sound { 27 | // Includes: SquareWave 28 | return s.NewTimedSound(s.NewSquareWave(261.63), 1000) 29 | } 30 | 31 | func SampleSilence() s.Sound { 32 | // Includes: TimedSound 33 | return s.NewTimedSilence(2000.0) 34 | } 35 | 36 | func SampleConcat() s.Sound { 37 | // Includes: TimedSound and MidiToSound 38 | return s.ConcatSounds( 39 | s.NewTimedSound(u.MidiToSound(72), 400), 40 | s.NewTimedSound(u.MidiToSound(74), 400), 41 | s.NewTimedSound(u.MidiToSound(76), 400), 42 | s.NewTimedSound(u.MidiToSound(60), 400), 43 | s.NewTimedSound(u.MidiToSound(67), 1200), 44 | ) 45 | } 46 | 47 | func SampleNormalSum() s.Sound { 48 | // Includes: TimedSound and MidiToSound 49 | return s.SumSounds( 50 | s.NewTimedSound(u.MidiToSound(55), 333), 51 | s.NewTimedSound(u.MidiToSound(59), 333), 52 | s.NewTimedSound(u.MidiToSound(62), 333), 53 | s.NewTimedSound(u.MidiToSound(65), 333), 54 | s.NewTimedSound(u.MidiToSound(67), 333), 55 | ) 56 | } 57 | 58 | func SampleMultiply() s.Sound { 59 | // Includes: TimedSound and SineWave 60 | all := make([]s.Sound, 20) 61 | for i := 0; i < len(all); i++ { 62 | all[i] = s.MultiplyWithClip(s.NewTimedSound(s.NewSineWave(659.25), 200), 0.2+float64(i)/10.0) 63 | } 64 | return s.ConcatSounds(all...) 65 | } 66 | 67 | func SampleRepeater() s.Sound { 68 | // Includes: Concat, TimedSound and MidiToSound 69 | return s.RepeatSound(s.ConcatSounds( 70 | s.NewTimedSound(u.MidiToSound(50), 400), 71 | s.NewTimedSound(u.MidiToSound(45), 400), 72 | s.NewTimedSound(u.MidiToSound(47), 400), 73 | s.NewTimedSound(u.MidiToSound(42), 400), 74 | s.NewTimedSound(u.MidiToSound(43), 400), 75 | s.NewTimedSound(u.MidiToSound(38), 400), 76 | s.NewTimedSound(u.MidiToSound(43), 400), 77 | s.NewTimedSound(u.MidiToSound(45), 400), 78 | ), 3) 79 | } 80 | 81 | func SampleAdsrEnvelope() s.Sound { 82 | // Includes: TimedSound and SineWave 83 | return s.NewADSREnvelope( 84 | s.NewTimedSound(s.NewSineWave(880.0), 875), 85 | 50, 200, 0.5, 100) 86 | } 87 | 88 | func SampleSampler() s.Sound { 89 | // Includes: TimedSound and SineWave 90 | return s.LinearSample(s.NewTimedSound(s.NewSineWave(392.00), 500), 2.0) 91 | } 92 | 93 | func SampleAddDelay() s.Sound { 94 | // Includes: Concat, TimedSound and MidiToSound 95 | return s.AddDelay(s.ConcatSounds( 96 | s.NewTimedSound(u.MidiToSound(55), 678), 97 | s.NewTimedSound(u.MidiToSound(59), 678), 98 | s.NewTimedSound(u.MidiToSound(62), 678), 99 | ), 123) 100 | } 101 | 102 | func SampleDenseIIR() s.Sound { 103 | // Includes: TimedSound and SineWave 104 | all := make([]s.Sound, 10) 105 | for i := 0; i < len(all); i++ { 106 | all[i] = s.NewTimedSound(s.NewSineWave(600*float64(i)/4), 200) 107 | } 108 | return s.NewDenseIIR(s.ConcatSounds(all...), 109 | []float64{0.8922, -2.677, 2.677, -0.8922}, 110 | []float64{2.772, -2.57, 0.7961}, 111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /test/silence.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padster/go-sound/0ccfc629d3a672312bf7bc1dd850b62c365136e0/test/silence.wav -------------------------------------------------------------------------------- /test/sounds_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | // go test github.com/padster/go-sound/test 4 | 5 | import ( 6 | "bytes" 7 | "io/ioutil" 8 | "os" 9 | "testing" 10 | 11 | "github.com/padster/go-sound/output" 12 | "github.com/padster/go-sound/sounds" 13 | ) 14 | 15 | // Generates multiple sample sounds, and compares against golden files generated in generatetest.go 16 | 17 | func TestTimedSine(t *testing.T) { 18 | compareFile(t, "timed_sine.wav", SampleTimedSineSound()) 19 | } 20 | 21 | func TestTimedSquare(t *testing.T) { 22 | compareFile(t, "timed_square.wav", SampleTimedSquareSound()) 23 | } 24 | 25 | func TestTimedSawtooth(t *testing.T) { 26 | compareFile(t, "timed_sawtooth.wav", SampleTimedSawtoothSound()) 27 | } 28 | 29 | func TestTimedTriangle(t *testing.T) { 30 | compareFile(t, "timed_triangle.wav", SampleTimedTriangleSound()) 31 | } 32 | 33 | func TestSilence(t *testing.T) { 34 | compareFile(t, "silence.wav", SampleSilence()) 35 | } 36 | 37 | func TestConcat(t *testing.T) { 38 | compareFile(t, "concat.wav", SampleConcat()) 39 | } 40 | 41 | func TestNormalSum(t *testing.T) { 42 | compareFile(t, "normalsum.wav", SampleNormalSum()) 43 | } 44 | 45 | func TestMultiply(t *testing.T) { 46 | compareFile(t, "multiply.wav", SampleMultiply()) 47 | } 48 | 49 | func TestRepeater(t *testing.T) { 50 | compareFile(t, "repeat.wav", SampleRepeater()) 51 | } 52 | 53 | func TestAdsrEnvelope(t *testing.T) { 54 | compareFile(t, "adsr.wav", SampleAdsrEnvelope()) 55 | } 56 | 57 | func TestSampler(t *testing.T) { 58 | compareFile(t, "sampler.wav", SampleSampler()) 59 | } 60 | 61 | func TestDelay(t *testing.T) { 62 | compareFile(t, "delay.wav", SampleAddDelay()) 63 | } 64 | 65 | func TestDenseIIR(t *testing.T) { 66 | compareFile(t, "denseiir.wav", SampleDenseIIR()) 67 | } 68 | 69 | // TODO(padster): Add tests for util/parser.go 70 | 71 | // compareFile writes a sound to file, compares it to a golden file, 72 | // and fails the test if anything goes wrong. 73 | func compareFile(t *testing.T, path string, sound sounds.Sound) { 74 | f, err := ioutil.TempFile("", "tmp_") 75 | os.Remove(f.Name()) 76 | t.Logf("Writing sound to %s\n", f.Name()) 77 | 78 | if err != nil { 79 | t.Fatalf("Error creating temp file: %s\n", err) 80 | return 81 | } 82 | if err = output.WriteSoundToWav(sound, f.Name()); err != nil { 83 | t.Fatalf("Error writing to temp file: %s\n", err) 84 | return 85 | } 86 | defer os.Remove(f.Name()) 87 | 88 | bE, errE := ioutil.ReadFile(path) 89 | if errE != nil { 90 | t.Fatalf("Error reading golden file: %s\n", errE) 91 | return 92 | } 93 | 94 | bA, errA := ioutil.ReadFile(f.Name()) 95 | if errA != nil { 96 | t.Fatalf("Error reading temp file: %s\n", errA) 97 | return 98 | } 99 | 100 | if !bytes.Equal(bE, bA) { 101 | t.Fatalf("File does not match %s! Please listen to the new version.\n", path) 102 | return 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /types/buffer.go: -------------------------------------------------------------------------------- 1 | // A circular buffer data type for floating point values. 2 | package types 3 | 4 | import ( 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | // Buffer holds the values within the buffer plus a collection of metadata. 10 | type Buffer struct { 11 | values []float64 12 | capacity int 13 | size int 14 | at int 15 | lock sync.Mutex 16 | finished bool 17 | } 18 | 19 | // NewBuffer creates a new circular buffer of a given maximum size. 20 | func NewBuffer(capacity int) *Buffer { 21 | b := Buffer{ 22 | make([]float64, capacity), 23 | capacity, 24 | 0, /* size */ 25 | 0, /* at */ 26 | sync.Mutex{}, 27 | false, /* finished */ 28 | } 29 | return &b 30 | } 31 | 32 | // Push adds a new value at the end of the buffer. 33 | func (b *Buffer) Push(value float64) float64 { 34 | b.lock.Lock() 35 | 36 | result := b.values[b.at] 37 | b.values[b.at] = value 38 | 39 | if b.size < b.capacity { 40 | b.size++ 41 | result = 0.0 42 | } 43 | 44 | if b.at+1 < b.capacity { 45 | b.at = b.at + 1 46 | } else { 47 | b.at = 0 48 | } 49 | 50 | b.lock.Unlock() 51 | return result 52 | } 53 | 54 | // GoPushChannel constantly pushes values from a channel, in a separate thread, 55 | // optionally only sampling 1 every sampleRate values. 56 | func (b *Buffer) GoPushChannel(values <-chan float64, sampleRate int) { 57 | val := 0.0 58 | ok := true 59 | b.finished = false 60 | go func() { 61 | for { 62 | if val, ok = <-values; !ok { 63 | break 64 | } 65 | b.Push(val) 66 | for i := 1; i < sampleRate; i++ { 67 | if _, ok = <-values; !ok { 68 | break 69 | } 70 | } 71 | } 72 | b.finished = true 73 | }() 74 | } 75 | 76 | // GetFromEnd returns the most recent buffer values. 77 | // 0 returns the most recently pushed, the least recent being b.size - 1 78 | func (b *Buffer) GetFromEnd(index int) float64 { 79 | b.lock.Lock() 80 | defer b.lock.Unlock() 81 | if index < 0 || index >= b.capacity { 82 | fmt.Printf("Index = %d, but size = %d and capacity = %d\n", index, b.size, b.capacity) 83 | panic("GetFromEnd index out of range") 84 | } else if index >= b.size { 85 | // Within range, just not filled yet, to default to zero. 86 | return 0.0 87 | } 88 | 89 | index = b.at - index 90 | if index < 0 { 91 | index = index + b.capacity 92 | } 93 | result := b.values[index] 94 | return result 95 | } 96 | 97 | // IsFull returns whether the buffer is full, 98 | // in that adding more entries will delete older ones. 99 | func (b *Buffer) IsFull() bool { 100 | return b.size == b.capacity 101 | } 102 | 103 | // IsFinished returns whether there is nothing more to be added to the buffer 104 | func (b *Buffer) IsFinished() bool { 105 | return b.finished 106 | } 107 | 108 | // Size returns how many entries are currently in the buffer. 109 | func (b *Buffer) Size() int { 110 | return b.size 111 | } 112 | 113 | // Clear resets the buffer to being empty 114 | func (b *Buffer) Clear() { 115 | // Simply clamp the size back to zero, don't worry about the existing values. 116 | b.lock.Lock() 117 | b.size = 0 118 | b.lock.Unlock() 119 | } 120 | 121 | // Each applies a given function to all the values in the buffer, 122 | // from least recent first, ending at the most recent. 123 | func (b *Buffer) Each(cb func(int, float64)) { 124 | b.lock.Lock() 125 | i := 0 126 | if !b.IsFull() { 127 | for i = 0; i < b.size; i++ { 128 | cb(i, b.values[i]) 129 | } 130 | } else { 131 | index := 0 132 | for i = b.at; i < b.capacity; i++ { 133 | cb(index, b.values[i]) 134 | index++ 135 | } 136 | for i = 0; i < b.at; i++ { 137 | cb(index, b.values[i]) 138 | index++ 139 | } 140 | } 141 | b.lock.Unlock() 142 | } 143 | -------------------------------------------------------------------------------- /types/typedbuffer.go: -------------------------------------------------------------------------------- 1 | // A circular buffer data type for generic values. 2 | package types 3 | 4 | import ( 5 | "sync" 6 | ) 7 | 8 | // Buffer holds the values within the buffer plus a collection of metadata. 9 | type TypedBuffer struct { 10 | values []interface{} 11 | capacity int 12 | size int 13 | at int 14 | lock sync.Mutex 15 | finished bool 16 | } 17 | 18 | // NewTypedBuffer creates a new circular buffer of a given maximum size. 19 | func NewTypedBuffer(capacity int) *TypedBuffer { 20 | b := TypedBuffer{ 21 | make([]interface{}, capacity), 22 | capacity, 23 | 0, /* size */ 24 | 0, /* at */ 25 | sync.Mutex{}, 26 | false, /* finished */ 27 | } 28 | return &b 29 | } 30 | 31 | // Push adds a new value at the end of the buffer. 32 | func (b *TypedBuffer) Push(value interface{}) interface{} { 33 | b.lock.Lock() 34 | 35 | result := b.values[b.at] 36 | b.values[b.at] = value 37 | 38 | if b.size < b.capacity { 39 | b.size++ 40 | result = 0.0 41 | } 42 | 43 | if b.at+1 < b.capacity { 44 | b.at = b.at + 1 45 | } else { 46 | b.at = 0 47 | } 48 | 49 | b.lock.Unlock() 50 | return result 51 | } 52 | 53 | // GoPushChannel constantly pushes values from a channel, in a separate thread, 54 | // optionally only sampling 1 every sampleRate values. 55 | func (b *TypedBuffer) GoPushChannel(values <-chan interface{}, sampleRate int) { 56 | var val interface{} 57 | ok := true 58 | b.finished = false 59 | go func() { 60 | for { 61 | if val, ok = <-values; !ok { 62 | break 63 | } 64 | b.Push(val) 65 | for i := 1; i < sampleRate; i++ { 66 | if _, ok = <-values; !ok { 67 | break 68 | } 69 | } 70 | } 71 | b.finished = true 72 | }() 73 | } 74 | 75 | // GetFromEnd returns the most recent buffer values. 76 | // 0 returns the most recently pushed, the least recent being b.size - 1 77 | func (b *TypedBuffer) GetFromEnd(index int) interface{} { 78 | b.lock.Lock() 79 | defer b.lock.Unlock() 80 | if index < 0 || index >= b.capacity { 81 | panic("GetFromEnd index out of range") 82 | } else if index >= b.size { 83 | // Within range, just not filled yet, to default to zero. 84 | return 0.0 85 | } 86 | 87 | index = b.at - index 88 | if index < 0 { 89 | index = index + b.capacity 90 | } 91 | result := b.values[index] 92 | return result 93 | } 94 | 95 | // IsFull returns whether the buffer is full, 96 | // in that adding more entries will delete older ones. 97 | func (b *TypedBuffer) IsFull() bool { 98 | return b.size == b.capacity 99 | } 100 | 101 | // Size returns the number of values in the buffer. 102 | func (b *TypedBuffer) Size() int { 103 | return b.size 104 | } 105 | 106 | // IsFinished returns whether there is nothing more to be added to the buffer 107 | func (b *TypedBuffer) IsFinished() bool { 108 | return b.finished 109 | } 110 | 111 | // Clear resets the buffer to being empty 112 | func (b *TypedBuffer) Clear() { 113 | // Simply clamp the size back to zero, don't worry about the existing values. 114 | b.lock.Lock() 115 | b.size = 0 116 | b.lock.Unlock() 117 | } 118 | 119 | // Each applies a given function to all the values in the buffer, 120 | // from least recent first, ending at the most recent. 121 | func (b *TypedBuffer) Each(cb func(int, interface{})) { 122 | b.lock.Lock() 123 | i := 0 124 | if !b.IsFull() { 125 | for i = 0; i < b.size; i++ { 126 | cb(i, b.values[i]) 127 | } 128 | } else { 129 | index := 0 130 | for i = b.at; i < b.capacity; i++ { 131 | cb(index, b.values[i]) 132 | index++ 133 | } 134 | for i = 0; i < b.at; i++ { 135 | cb(index, b.values[i]) 136 | index++ 137 | } 138 | } 139 | b.lock.Unlock() 140 | } 141 | -------------------------------------------------------------------------------- /util/livespectrogram.go: -------------------------------------------------------------------------------- 1 | // Renders various data from a channel of [-1, 1] onto screen. 2 | package util 3 | 4 | import ( 5 | "log" 6 | // "math" 7 | "math/cmplx" 8 | "runtime" 9 | 10 | "github.com/padster/go-sound/types" 11 | 12 | // TODO(padster) - migrate to core, not compat. 13 | gl "github.com/go-gl/gl/v3.3-compatibility/gl" 14 | glfw "github.com/go-gl/glfw/v3.1/glfw" 15 | ) 16 | 17 | // Line is the samples channel for the line, plus their color. 18 | type ComplexLine struct { 19 | Values <-chan []complex128 20 | R float32 21 | G float32 22 | B float32 23 | valueBuffer *types.TypedBuffer 24 | } 25 | 26 | // Screen is an on-screen opengl window that renders the channel. 27 | type SpectrogramScreen struct { 28 | width int 29 | height int 30 | bpo int 31 | pixelsPerSample float64 32 | eventBuffer *types.TypedBuffer 33 | 34 | // TODO(padster): Move this into constructor, and call much later. 35 | line *ComplexLine 36 | } 37 | 38 | // NewScreen creates a new output screen of a given size and sample density. 39 | func NewSpectrogramScreen(width int, height int, bpo int) *SpectrogramScreen { 40 | samplesPerPixel := 1 // HACK - parameterize? 41 | s := SpectrogramScreen{ 42 | width, 43 | height, 44 | bpo, 45 | 1.0 / float64(samplesPerPixel), 46 | types.NewTypedBuffer(width * samplesPerPixel), 47 | nil, // lines 48 | } 49 | return &s 50 | } 51 | 52 | // Render starts rendering a channel of waves samples to screen. 53 | func (s *SpectrogramScreen) Render(values <-chan []complex128, sampleRate int) { 54 | s.RenderLineWithEvents( 55 | &ComplexLine{values, 1.0, 1.0, 1.0, nil}, // Default to white 56 | nil, sampleRate) 57 | } 58 | 59 | // RenderLinesWithEvents renders multiple channels of samples to screen, and draws events. 60 | func (s *SpectrogramScreen) RenderLineWithEvents(line *ComplexLine, events <-chan interface{}, sampleRate int) { 61 | s.line = line 62 | 63 | runtime.LockOSThread() 64 | 65 | // NOTE: It appears that glfw 3.1 uses its own internal error callback. 66 | // glfw.SetErrorCallback(func(err glfw.ErrorCode, desc string) { 67 | // log.Fatalf("%v: %s\n", err, desc) 68 | // }) 69 | if err := glfw.Init(); err != nil { 70 | log.Fatalf("Can't init glfw: %v!", err) 71 | } 72 | defer glfw.Terminate() 73 | 74 | window, err := glfw.CreateWindow(s.width, s.height, "Spectrogram", nil, nil) 75 | if err != nil { 76 | log.Fatalf("CreateWindow failed: %s", err) 77 | } 78 | if aw, ah := window.GetSize(); aw != s.width || ah != s.height { 79 | log.Fatalf("Window doesn't have the requested size: want %d,%d got %d,%d", s.width, s.height, aw, ah) 80 | } 81 | window.MakeContextCurrent() 82 | 83 | // Must gl.Init() *after* MakeContextCurrent 84 | if err := gl.Init(); err != nil { 85 | log.Fatalf("Can't init gl: %v!", err) 86 | } 87 | 88 | // Set window up to be [0, 0] -> [width, height], black. 89 | gl.MatrixMode(gl.MODELVIEW) 90 | gl.LoadIdentity() 91 | gl.Translated(-1, -1, 0) 92 | gl.Scaled(2/float64(s.width), 2/float64(s.height), 1.0) 93 | gl.ClearColor(0.0, 0.0, 0.0, 0.0) 94 | 95 | gl.ShadeModel(gl.FLAT) 96 | 97 | // Actually start writing data to the buffer 98 | s.line.valueBuffer = types.NewTypedBuffer(int(float64(s.width) / s.pixelsPerSample)) 99 | s.line.valueBuffer.GoPushChannel(hackWrapChannel(s.line.Values), sampleRate) 100 | if events != nil { 101 | s.eventBuffer.GoPushChannel(events, sampleRate) 102 | } 103 | 104 | gl.Hint(gl.POINT_SMOOTH_HINT, gl.FASTEST) 105 | 106 | // Keep drawing while we still can (and should). 107 | for !window.ShouldClose() && !s.bufferFinished() { 108 | if window.GetKey(glfw.KeyEscape) == glfw.Press { 109 | break 110 | } 111 | gl.Clear(gl.COLOR_BUFFER_BIT) 112 | s.drawSignal() 113 | window.SwapBuffers() 114 | glfw.PollEvents() 115 | } 116 | 117 | // Keep window around, only close on esc. 118 | for !window.ShouldClose() && window.GetKey(glfw.KeyEscape) != glfw.Press { 119 | glfw.PollEvents() 120 | } 121 | } 122 | 123 | // bufferFinished returns whether any of the input channels have closed. 124 | func (s *SpectrogramScreen) bufferFinished() bool { 125 | if s.eventBuffer.IsFinished() { 126 | return true 127 | } 128 | return s.line.valueBuffer.IsFinished() 129 | } 130 | 131 | // drawSignal writes the input wave form(s) out to screen. 132 | func (s *SpectrogramScreen) drawSignal() { 133 | s.eventBuffer.Each(func(index int, value interface{}) { 134 | if value != nil { 135 | e := value.(Event) 136 | gl.Color3f(e.R, e.G, e.B) 137 | x := float64(index) * s.pixelsPerSample 138 | gl.Begin(gl.LINE_STRIP) 139 | gl.Vertex2d(x, -1.0) 140 | gl.Vertex2d(x, 1.0) 141 | gl.End() 142 | } 143 | }) 144 | 145 | spacing := 1 146 | 147 | gl.PointSize(1.0) 148 | gl.Begin(gl.POINTS) 149 | s.line.valueBuffer.Each(func(index int, value interface{}) { 150 | col := value.([]complex128) 151 | for i, v := range col { 152 | // p = log(|v|) = [-20, 5], so map to [0, 1] 153 | // p := math.Log(cmplx.Abs(v) + 1.e-8) 154 | // grey := (p + 20.0) / 25.0 155 | p := cmplx.Abs(v) 156 | grey := p / 15.0 157 | if grey > 1.0 { 158 | grey = 1.0 159 | } else if grey < 0.0 { 160 | grey = 0.0 161 | } 162 | 163 | // HACK: Stretch to make the darks darker and the whites whiter. 164 | // grey = grey * grey * grey * grey // more space at the top, [0, 1] 165 | // grey = 2.0 * grey - 1.0 // [-1, 1] 166 | // grey = math.Tanh(2.0 * grey) // streched, still [-1, 1] 167 | // grey = (grey + 1.0) / 2.0 168 | 169 | gl.Color3d(grey, grey, grey) 170 | // gl.Vertex2d(float64(index)*s.pixelsPerSample, float64(s.height - 1 - (3 * i + 0))) 171 | gl.Vertex2d(float64(index)*s.pixelsPerSample, float64(s.height-1-(spacing*i))) 172 | // gl.Vertex2d(float64(index)*s.pixelsPerSample, float64(s.height - 1 - (3 * i + 2))) 173 | } 174 | 175 | gl.Color3ub(255, 0, 0) 176 | for i := 1; i < 7; i++ { 177 | gl.Vertex2d(float64(index)*s.pixelsPerSample, float64(i*spacing*s.bpo)) 178 | } 179 | }) 180 | gl.End() 181 | } 182 | 183 | // Grr...no generics, but also chan T can't be a chan interface{} ??? 184 | func hackWrapChannel(in <-chan []complex128) <-chan interface{} { 185 | result := make(chan interface{}) 186 | go func(out chan interface{}) { 187 | for i := range in { 188 | out <- interface{}(i) 189 | } 190 | close(out) 191 | }(result) 192 | return result 193 | } 194 | -------------------------------------------------------------------------------- /util/parser.go: -------------------------------------------------------------------------------- 1 | // Package util provides a collection of utilities for dealing with Sounds, 2 | // generating them, displaying them, calculating their samples etc... 3 | package util 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | 9 | s "github.com/padster/go-sound/sounds" 10 | ) 11 | 12 | // Frequencies of notes in scale 0 - see: 13 | // http://www.phy.mtu.edu/~suits/notefreqs.html 14 | // http://newt.phys.unsw.edu.au/jw/notes.html 15 | var freq = []float64{ 16 | 16.35, // C 17 | 17.32, // C#/Db 18 | 18.35, // D 19 | 19.45, // D#/Eb 20 | 20.60, // E 21 | 21.83, // F 22 | 23.12, // F#/Gb 23 | 24.50, // G 24 | 25.96, // G#/Ab 25 | 27.50, // A 26 | 29.14, // A#/Bb 27 | 30.87, // B 28 | } 29 | 30 | // noteToMidi takes an offset into a string, parses a note, and returns 31 | // both the note's base midi value plus the end offset of the note. 32 | func noteToMidi(note string, offset int) (int, int) { 33 | resultSemi := 0 34 | 35 | switch note[offset] { 36 | case 'A': 37 | resultSemi = 9 38 | case 'B': 39 | resultSemi = 11 40 | case 'C': 41 | resultSemi = 0 42 | case 'D': 43 | resultSemi = 2 44 | case 'E': 45 | resultSemi = 4 46 | case 'F': 47 | resultSemi = 5 48 | case 'G': 49 | resultSemi = 7 50 | default: 51 | panic("Unknown note " + note) 52 | } 53 | offset++ 54 | 55 | // TODO(padster): Support bb, ##, etc if useful. 56 | if offset < len(note) { 57 | switch note[offset] { 58 | case 'b': 59 | resultSemi-- 60 | offset++ 61 | case '#': 62 | resultSemi++ 63 | offset++ 64 | } 65 | } 66 | 67 | return resultSemi, offset 68 | } 69 | 70 | // midiToHz returns the Hz of a given midi note. 71 | func MidiToHz(midiNote int) float64 { 72 | // Assuming C0 hz == 12 midi 73 | octave := midiNote/12 - 1 74 | semitone := midiNote % 12 75 | scale := 1 << uint(octave) 76 | return freq[semitone] * float64(scale) 77 | } 78 | 79 | // MidiToSound converts a midi note into a sound that plays its pitch. 80 | func MidiToSound(midiNote int) s.Sound { 81 | // NOTE: You can substitute here something that reads from .wav files 82 | // to synthesize the notes instead. 83 | return s.NewSineWave(MidiToHz(midiNote)) 84 | } 85 | 86 | // noteToHz reads a note starting at an offset, and returns the hz and the end offset. 87 | func noteToHz(note string, offset int, base uint) (float64, int) { 88 | midi, next := noteToMidi(note, offset) 89 | return MidiToHz(midi + 12*int(base+1)), next 90 | } 91 | 92 | // noteToHz reads a note starting at an offset, and returns its Sound and the end offset. 93 | func noteToSound(note string, offset int, base uint) (s.Sound, int) { 94 | baseHz, next := noteToHz(note, offset, base) 95 | return s.NewSineWave(baseHz), next 96 | } 97 | 98 | // ParseNotesToChord takes a collection of notes (e.g. "CEG") plus the base octave 99 | // and returns a sound of them all being played together. 100 | func ParseNotesToChord(notes string, base uint) s.Sound { 101 | asSounds := make([]s.Sound, 0, len(notes)) 102 | var sound s.Sound 103 | for at := 0; at < len(notes); { 104 | sound, at = noteToSound(notes, at, base) 105 | asSounds = append(asSounds, sound) 106 | } 107 | return s.SumSounds(asSounds...) 108 | } 109 | 110 | // ParseChord converts a chord string (e.g. "G#sus4") and base octave into a 111 | // Sound that contains the notes in the chord. 112 | func ParseChord(chord string, base uint) s.Sound { 113 | baseMidi, at := noteToMidi(chord, 0) 114 | baseMidi += 12 * int(base+1) 115 | 116 | modifier := chord[at:] 117 | var offsets []int 118 | 119 | // A subset of these, converted to integer notation: 120 | // https://en.wikibooks.org/wiki/Music_Theory/Complete_List_of_Chord_Patterns 121 | switch modifier { 122 | case "": 123 | offsets = []int{0, 4, 7} 124 | case "5": 125 | offsets = []int{0, 7} 126 | case "m": 127 | offsets = []int{0, 3, 7} 128 | case "dom": 129 | fallthrough 130 | case "7": 131 | offsets = []int{0, 4, 7, 10} 132 | case "M7": 133 | offsets = []int{0, 4, 7, 11} 134 | case "m7": 135 | offsets = []int{0, 3, 7, 10} 136 | case "6": 137 | offsets = []int{0, 4, 7, 9} 138 | case "m6": 139 | offsets = []int{0, 3, 7, 9} 140 | case "dim": 141 | offsets = []int{0, 3, 6} 142 | case "sus4": 143 | offsets = []int{0, 5, 7} 144 | case "sus2": 145 | offsets = []int{0, 2, 7} 146 | case "aug": 147 | offsets = []int{0, 4, 8} 148 | 149 | default: 150 | panic("Unsupported chord modifier: " + modifier) 151 | } 152 | 153 | asSounds := make([]s.Sound, len(offsets), len(offsets)) 154 | for i, offset := range offsets { 155 | asSounds[i] = MidiToSound(offset + baseMidi) 156 | } 157 | return s.SumSounds(asSounds...) 158 | } 159 | 160 | // GuitarChord converts a standard guitar representation (e.g. "2x0232") 161 | // into the sound of those notes being played, assuming standard tuning. 162 | func GuitarChord(chord string) s.Sound { 163 | // Standard guitar tuning: EADGBE 164 | stringMidi := []int{40, 45, 50, 55, 59, 64} 165 | noteMidi := []int{} 166 | 167 | for i, fret := range chord { 168 | if '0' <= fret && fret <= '9' { 169 | offset, _ := strconv.Atoi(fmt.Sprintf("%c", fret)) 170 | noteMidi = append(noteMidi, stringMidi[i]+offset) 171 | } 172 | } 173 | 174 | asSounds := make([]s.Sound, len(noteMidi), len(noteMidi)) 175 | for i, offset := range noteMidi { 176 | asSounds[i] = MidiToSound(offset) 177 | } 178 | return s.SumSounds(asSounds...) 179 | } 180 | -------------------------------------------------------------------------------- /util/samplecache.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | s "github.com/padster/go-sound/sounds" 5 | ) 6 | 7 | const ( 8 | LOAD_LIMIT = int(s.CyclesPerSecond * 10 * 60) /* 10 minutes */ 9 | ) 10 | 11 | func CacheSamples(sound s.Sound) []float64 { 12 | var result []float64 13 | 14 | sound.Start() 15 | for sample := range sound.GetSamples() { 16 | result = append(result, sample) 17 | if len(result) == LOAD_LIMIT { 18 | break 19 | } 20 | } 21 | sound.Stop() 22 | 23 | return result 24 | } 25 | -------------------------------------------------------------------------------- /util/screen.go: -------------------------------------------------------------------------------- 1 | // Renders various data from a channel of [-1, 1] onto screen. 2 | package util 3 | 4 | import ( 5 | "log" 6 | "runtime" 7 | 8 | "github.com/padster/go-sound/types" 9 | 10 | // TODO(padster) - migrate to core, not compat. 11 | gl "github.com/go-gl/gl/v3.3-compatibility/gl" 12 | glfw "github.com/go-gl/glfw/v3.1/glfw" 13 | ) 14 | 15 | // Event is something that occurred at a single sample of the values. 16 | type Event struct { 17 | // (r, g, b) colours for the event. 18 | R float32 19 | G float32 20 | B float32 21 | } 22 | 23 | // Line is the samples channel for the line, plus their color. 24 | type Line struct { 25 | Values <-chan float64 26 | R float32 27 | G float32 28 | B float32 29 | valueBuffer *types.Buffer 30 | } 31 | 32 | // NewLine creates a line from the exported fields. 33 | func NewLine(v <-chan float64, r float32, g float32, b float32) Line { 34 | return Line{v, r, g, b, nil} 35 | } 36 | 37 | // Screen is an on-screen opengl window that renders the channel. 38 | type Screen struct { 39 | width int 40 | height int 41 | pixelsPerSample float64 42 | eventBuffer *types.TypedBuffer 43 | 44 | // TODO(padster): Move this into constructor, and call much later. 45 | lines []Line 46 | } 47 | 48 | // NewScreen creates a new output screen of a given size and sample density. 49 | func NewScreen(width int, height int, samplesPerPixel int) *Screen { 50 | s := Screen{ 51 | width, 52 | height, 53 | 1.0 / float64(samplesPerPixel), 54 | types.NewTypedBuffer(width * samplesPerPixel), 55 | nil, // lines 56 | } 57 | return &s 58 | } 59 | 60 | // Render starts rendering a channel of waves samples to screen. 61 | func (s *Screen) Render(values <-chan float64, sampleRate int) { 62 | s.RenderLinesWithEvents([]Line{ 63 | Line{values, 1.0, 1.0, 1.0, nil}, // Default to white 64 | }, nil, sampleRate) 65 | } 66 | 67 | // RenderLinesWithEvents renders multiple channels of samples to screen, and draws events. 68 | func (s *Screen) RenderLinesWithEvents(lines []Line, events <-chan interface{}, sampleRate int) { 69 | s.lines = lines 70 | 71 | runtime.LockOSThread() 72 | 73 | // NOTE: It appears that glfw 3.1 uses its own internal error callback. 74 | // glfw.SetErrorCallback(func(err glfw.ErrorCode, desc string) { 75 | // log.Fatalf("%v: %s\n", err, desc) 76 | // }) 77 | if err := glfw.Init(); err != nil { 78 | log.Fatalf("Can't init glfw: %v!", err) 79 | } 80 | defer glfw.Terminate() 81 | 82 | window, err := glfw.CreateWindow(s.width, s.height, "Wave", nil, nil) 83 | if err != nil { 84 | log.Fatalf("CreateWindow failed: %s", err) 85 | } 86 | if aw, ah := window.GetSize(); aw != s.width || ah != s.height { 87 | log.Fatalf("Window doesn't have the requested size: want %d,%d got %d,%d", s.width, s.height, aw, ah) 88 | } 89 | window.MakeContextCurrent() 90 | 91 | // Must gl.Init() *after* MakeContextCurrent 92 | if err := gl.Init(); err != nil { 93 | log.Fatalf("Can't init gl: %v!", err) 94 | } 95 | 96 | // Set window up to be [0, -1.0] -> [width, 1.0], black. 97 | gl.MatrixMode(gl.MODELVIEW) 98 | gl.LoadIdentity() 99 | gl.Translated(-1, 0, 0) 100 | gl.Scaled(2/float64(s.width), 1.0, 1.0) 101 | gl.ClearColor(0.0, 0.0, 0.0, 0.0) 102 | 103 | // Actually start writing data to the buffer 104 | for i, _ := range s.lines { 105 | s.lines[i].valueBuffer = types.NewBuffer(int(float64(s.width) / s.pixelsPerSample)) 106 | s.lines[i].valueBuffer.GoPushChannel(s.lines[i].Values, sampleRate) 107 | } 108 | if events != nil { 109 | s.eventBuffer.GoPushChannel(events, sampleRate) 110 | } 111 | 112 | // Keep drawing while we still can (and should). 113 | for !window.ShouldClose() && !s.bufferFinished() { 114 | if window.GetKey(glfw.KeyEscape) == glfw.Press { 115 | break 116 | } 117 | gl.Clear(gl.COLOR_BUFFER_BIT) 118 | s.drawSignal() 119 | window.SwapBuffers() 120 | glfw.PollEvents() 121 | } 122 | 123 | // Keep window around, only close on esc. 124 | for !window.ShouldClose() && window.GetKey(glfw.KeyEscape) != glfw.Press { 125 | glfw.PollEvents() 126 | } 127 | } 128 | 129 | // bufferFinished returns whether any of the input channels have closed. 130 | func (s *Screen) bufferFinished() bool { 131 | if s.eventBuffer.IsFinished() { 132 | return true 133 | } 134 | for _, l := range s.lines { 135 | if l.valueBuffer.IsFinished() { 136 | return true 137 | } 138 | } 139 | return false 140 | } 141 | 142 | // drawSignal writes the input wave form(s) out to screen. 143 | func (s *Screen) drawSignal() { 144 | s.eventBuffer.Each(func(index int, value interface{}) { 145 | if value != nil { 146 | e := value.(Event) 147 | gl.Color3f(e.R, e.G, e.B) 148 | x := float64(index) * s.pixelsPerSample 149 | gl.Begin(gl.LINE_STRIP) 150 | gl.Vertex2d(x, -1.0) 151 | gl.Vertex2d(x, 1.0) 152 | gl.End() 153 | } 154 | }) 155 | 156 | for _, l := range s.lines { 157 | gl.Color3f(l.R, l.G, l.B) 158 | gl.Begin(gl.LINE_STRIP) 159 | l.valueBuffer.Each(func(index int, value float64) { 160 | gl.Vertex2d(float64(index)*s.pixelsPerSample, value) 161 | }) 162 | gl.End() 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /writecq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/zlib" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "runtime" 10 | "time" 11 | 12 | "github.com/padster/go-sound/cq" 13 | "github.com/padster/go-sound/features" 14 | f "github.com/padster/go-sound/file" 15 | s "github.com/padster/go-sound/sounds" 16 | ) 17 | 18 | // Runs CQ to generate the CQ columns and writes to file. 19 | func main() { 20 | runtime.GOMAXPROCS(6) 21 | 22 | // Parse flags... 23 | sampleRate := s.CyclesPerSecond 24 | octaves := flag.Int("octaves", 7, "Range in octaves") 25 | minFreq := flag.Float64("minFreq", 55.0, "Minimum frequency") 26 | bpo := flag.Int("bpo", 24, "Buckets per octave") 27 | zip := flag.Bool("zip", false, "Whether to zip the output") 28 | peaks := flag.Bool("peaks", false, "Whether to write CQ peaks rather than values") 29 | flag.Parse() 30 | 31 | remainingArgs := flag.Args() 32 | if len(remainingArgs) < 1 || len(remainingArgs) > 2 { 33 | panic("Required: [] filename arguments") 34 | } 35 | inputFile := remainingArgs[0] 36 | outputFile := "out.cq" 37 | if *peaks { 38 | outputFile = "out.meta" 39 | } 40 | if len(remainingArgs) == 2 { 41 | outputFile = remainingArgs[1] 42 | } 43 | 44 | inputSound := f.Read(inputFile) 45 | inputSound.Start() 46 | defer inputSound.Stop() 47 | 48 | // minFreq, maxFreq, bpo := 110.0, 14080.0, 24 49 | params := cq.NewCQParams(sampleRate, *octaves, *minFreq, *bpo) 50 | constantQ := cq.NewConstantQ(params) 51 | 52 | startTime := time.Now() 53 | columns := constantQ.ProcessChannel(inputSound.GetSamples()) 54 | 55 | if *peaks { 56 | pd := &features.PeakDetector{} 57 | asPeaks := pd.ProcessChannel(columns) 58 | features.WritePeaks(outputFile, asPeaks) 59 | } else { 60 | writeSamples(outputFile, *zip, constantQ.OutputLatency, columns) 61 | } 62 | elapsedSeconds := time.Since(startTime).Seconds() 63 | 64 | fmt.Printf("elapsed time (not counting init): %f sec\n", elapsedSeconds) 65 | } 66 | 67 | func writeSamples(outputFile string, compress bool, latency int, samples <-chan []complex128) { 68 | // BIG HACK 69 | latency = 0 70 | 71 | file, err := os.Create(outputFile) 72 | if err != nil { 73 | panic(err) 74 | } 75 | defer file.Close() 76 | 77 | framesWritten := 0 78 | maxHeight := 0 79 | 80 | var writer io.Writer 81 | if compress { 82 | zip := zlib.NewWriter(file) 83 | defer zip.Close() 84 | writer = zip 85 | } else { 86 | writer = file 87 | } 88 | 89 | fmt.Printf("Latency = %d\n", latency) 90 | 91 | totalNumbersWritten := 0 92 | for sample := range samples { 93 | if latency > 0 { 94 | latency-- 95 | } else { 96 | if len(sample) > maxHeight { 97 | maxHeight = len(sample) 98 | } 99 | cq.WriteComplexArray(writer, sample) 100 | framesWritten++ 101 | totalNumbersWritten += len(sample) 102 | if framesWritten%10000 == 0 { 103 | fmt.Printf("Written frame %d\n", framesWritten) 104 | } 105 | } 106 | } 107 | fmt.Printf("Result: %d numbers written, %d by %d\n", totalNumbersWritten, framesWritten, maxHeight) 108 | } 109 | --------------------------------------------------------------------------------