diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9d56f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +dsp +dsp-* +dist/ +keys/ + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d69bead --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +CGO_FLAGS_AARCH64 := "" +CGO_FLAGS_AMD64 := "-m64" +CGO_FLAGS_ARM := "" +CGO_FLAGS_I686 := "-m32" +GCFLAGS := '-N -l' +GOPATH := `pwd`/../../../.. +PACKAGE_BIN := config ir keys webroot `ls dsp*` + +all: dsp + +.PHONY: clean clean-all fmt keys test + +clean: + rm -rf dist/ + rm -f dsp + +clean-all: + rm -rf dist/ + rm -f dsp dsp-linux-aarch64 dsp-linux-amd64 dsp-linux-arm dsp-win-amd64.exe dsp-win-i686.exe + +dsp: + GOPATH=$(GOPATH) go build -o dsp -gcflags $(GCFLAGS) + +dsp-linux-aarch64: + GOPATH=$(GOPATH) CGO_ENABLED=1 CGO_CFLAGS=$(CGO_FLAGS_AARCH64) CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 go build -o dsp-linux-aarch64 -gcflags $(GCFLAGS) + +dsp-linux-amd64: + GOPATH=$(GOPATH) CGO_ENABLED=1 CGO_CFLAGS=$(CGO_FLAGS_AMD64) CC=x86_64-linux-gnu-gcc GOOS=linux GOARCH=amd64 go build -o dsp-linux-amd64 -gcflags $(GCFLAGS) + +dsp-linux-arm: + GOPATH=$(GOPATH) CGO_ENABLED=1 CGO_CFLAGS=$(CGO_FLAGS_ARM) CC=arm-linux-gnu-gcc GOOS=linux GOARCH=arm GOARM=7 go build -o dsp-linux-arm -gcflags $(GCFLAGS) + +dsp-win-amd64.exe: + GOPATH=$(GOPATH) CGO_ENABLED=1 CGO_CFLAGS=$(CGO_FLAGS_AMD64) CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 go build -o dsp-win-amd64.exe -gcflags $(GCFLAGS) + +dsp-win-i686.exe: + GOPATH=$(GOPATH) CGO_ENABLED=1 CGO_CFLAGS=$(CGO_FLAGS_I686) CC=i686-w64-mingw32-gcc GOOS=windows GOARCH=386 go build -o dsp-win-i686.exe -gcflags $(GCFLAGS) + +dist: dsp-linux-amd64 dsp-linux-arm dsp-win-amd64.exe + mkdir dist + mkdir dist/bin + mkdir dist/bin/go-dsp-guitar + cp -r $(PACKAGE_BIN) dist/bin/go-dsp-guitar/ + mkdir dist/src + mkdir dist/src/go-dsp-guitar + rsync -rlpv . dist/src/go-dsp-guitar/ --exclude dist/ --exclude ".*" --exclude "dsp*" + cd dist/bin/ && tar cvzf go-dsp-guitar-vX.X.X.tar.gz --exclude=".[^/]*" go-dsp-guitar && cd ../../ + cd dist/src/ && tar cvzf go-dsp-guitar-vX.X.X.src.tar.gz --exclude=".[^/]*" go-dsp-guitar && cd ../../ + +fmt: + GOPATH=$(GOPATH) gofmt -w . + +keys: + openssl genrsa -out keys/private.pem 4096 + openssl req -new -x509 -days 365 -sha512 -key keys/private.pem -out keys/public.pem -subj "/C=DE/ST=Berlin/L=Berlin/O=None/OU=None/CN=localhost" + +test: + GOPATH=$(GOPATH) go test -cover github.com/andrepxx/go-dsp-guitar/circular + GOPATH=$(GOPATH) go test -cover github.com/andrepxx/go-dsp-guitar/fft + GOPATH=$(GOPATH) go test -cover github.com/andrepxx/go-dsp-guitar/level + GOPATH=$(GOPATH) go test -cover github.com/andrepxx/go-dsp-guitar/random + GOPATH=$(GOPATH) go test -cover github.com/andrepxx/go-dsp-guitar/resample + GOPATH=$(GOPATH) go test -cover github.com/andrepxx/go-dsp-guitar/tuner + GOPATH=$(GOPATH) go test -cover github.com/andrepxx/go-dsp-guitar/wave + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3d45ee --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +# go-dsp-guitar + +This project implements a cross-platform multichannel multi-effects processor for electric guitars and other instruments, based upon concepts and algorithms originating from the field of circuit simulation. + +The software takes the signals from N audio input channels, processes them and provides N + 3 audio output channels. The user may, for example, connect the signal from individual instruments to separate input channels of his / her sound card / audio interface. The input signals are then taken and put through dedicated signal chains for processing. The software provides one dedicated signal chain for each input. The output from the last processing element in each chain is then sent to one of the output channels, providing one output channel for each of the input channels. The remaining three output channels include a dedicated metronome, which creates a monophonic click track, as well as a pair of "master output" channels providing a stereo mixdown of all processed signals, say for monitoring purposes. + +To manipulate the signal, the user may choose from a variety of highly customizable signal processing units, including the following. + +- signal / function generator +- noise gate +- bandpass filter +- auto-wah / envelope follower +- (multi-)octaver +- excess (distortion by phase-modulation) +- fuzz (asymmetric hard or soft saturation) +- overdrive (symmetric soft saturation) +- distortion (symmetric hard saturation) +- tone stack (four-band equalizer) +- (multi-)chorus +- flanger (simple LFO-driven comb filter) +- phaser (complex LFO-driven comb filter) +- tremolo (amplitude modulation) +- ring modulator +- delay (echo) +- power amplifier and speaker simulation + +In addition, the software provides ... + +- a means to dynamically control the latency of the audio hardware / JACK server +- a highly sensitive, fully chromatic instrument tuner based on the auto-correlation function +- a room simulation (spatializer) to create a stereo mixdown from all (processed) instrument signals +- a metronome to generate a click track for the performing musician for synchronization +- sampled peak programme meters (SPPMs) for controlling the signal level of each input and output channel + +... and much more. + +The software itself runs in headless mode and is entirely controlled via a modern, web-based user interface, accessible either from the same machine or remotely over the network. It may operate either in real-time mode (default), where it takes signals from either the computer's audio hardware or other applications (e. g. a software synth) and delivers signals to either the computer's audio hardware or other applications (e. g. a DAW), via JACK, or in batch processing mode, where it reads signals from and writes generated output to audio files in either RIFF WAVE or RF64 format. It currently supports files in 8-bit, 16-bit, 24-bit and 32-bit linear PCM (LPCM), as well as 32-bit and 64-bit IEEE 754 floating-point format. Supported sample rates include 22.05 kHz, 32 kHz, 44.1 kHz, 48 kHz, 88.2 kHz, 96 kHz and 192 kHz. The simulation engine will adjust its internal time discretization to the selected sample rate. It will also use the highest precision available from the processor's floating-point implementation for all intermediate results. Only when the results are written to file or handed back to the JACK audio server, the (amplitude) resolution of the audio signal may be reduced, if required. + +## Screenshots + +![Screenshot 01](/doc/img/screenshot-01-thumb.png) + +[View full resolution image](/doc/img/screenshot-01.png) + +## Running the software (from a binary package) + +Just download the binary (non-`src`) tarball from our *Releases* page, extract it somewhere, start JACK (e. g. via `qjackctl`), `cd` into the directory where you extracted *go-dsp-guitar* and run the `./dsp-*` executable which matches your target architecture. For example, on an x86-64 system running Linux, you may do the following. + +``` +cd go-dsp-guitar/ +./dsp-linux-amd64 +``` + +If you want to run the software in batch processing mode (without JACK) instead, replace the last line with the following. + +``` +./dsp-linux-amd64 -channels 1 +``` + +Replace the number `1` with the actual number of input channels you want to process, then enter the sample rate (time discretization) you want the simulation engine to operate at. + +No matter if you run the software in real-time (JACK-aware) or batch processing mode, you should finally get the following message in your terminal emulator / console. + +``` +Web interface ready: https://localhost:8443/ +``` + +Point your browser to the following URL to fire up the web interface: https://localhost:8443/ + +You will find more documentation inside the web interface. + +## Building the software from source locally + +To download and build the software from source for your system, run the following commands in a shell (assuming that `~/go` is your `$GOPATH`). + +``` +cd ~/go/src/ +go get -v github.com/andrepxx/go-dsp-guitar +cd github.com/andrepxx/go-dsp-guitar/ +make keys +make +``` + +This will create an RSA key pair for the TLS connection between the user-interface and the actual signal processing service (`make keys`) and then build the software for your system (`make`). The executable is called `dsp`, but you may re-name it to match your architecture. For example, on an x86-64 system running on Linux, you may rename the executable as follows. + +``` +mv dsp dsp-linux-amd64 +``` + +## Building the software from source for other architectures (cross-compilation) + +In addition, you may cross-compile the software from source for other architectures. Currently, the following targets are supported for cross-compilation. + +``` +make dsp-linux-aarch64 +make dsp-linux-amd64 +make dsp-linux-arm +make dsp-win-amd64.exe +make dsp-win-i686.exe +``` + +In order to cross-compile the software, you will need a cross-compilation toolchain and a populated `sysroot` for your target architecture. You may find it by invoking your cross-compiler with the `-v` option. For example, the `sysroot` may be one of the following. + +``` +/usr/aarch64-linux-gnu/sys-root/ +/usr/arm-linux-gnu/sys-root/ +/usr/x86_64-linux-gnu/sys-root/ +/usr/x86_64-w64-mingw32/sys-root/ +/usr/i686-w64-mingw32/sys-root/ +``` + +## Packaging the software for distribution + +After you build either a binary for your system or cross-compiled binaries for different systems (or both), you can bundle your binaries, along with scripts and auxiliary data, into packages for distribution. + +``` +make dist +``` + +This will create a binary package under `dist/bin/go-dsp-guitar-vX.X.X.tar.gz`, as well as a source package under `dist/src/go-dsp-guitar-src-vX.X.X.tar.gz`. Rename these for proper semantic versioning. + +## Other build targets + +There are other build targets in the `Makefile`. + +- `make clean`: Removes the `dist/` directory and the `dsp` executable built for your local system. +- `make clean-all`: Removes the `dist/` directory, as well as all `dsp` executables built for your local system and cross-compiled for other systems. +- `make fmt`: Format the source code. Run this build target immediately before committing source code to version control. +- `make test`: Run automated tests to ensure the software functions correctly on your system. You should also run this before committing source code to version control to ensure that there are no regressions. + +## Build requirements + +You may need the following packages in order to build the software on your system. + +- `gcc-aarch64-linux-gnu` +- `gcc-arm-linux-gnu` +- `gcc-x86_64-linux` +- `git` +- `glibc-arm-linux-gnu` +- `glibc-devel.i686` +- `glibc-devel.x86_64` +- `glibc-headers.i686` +- `glibc-headers.x86_64` +- `golang-bin` (Fedora / RHEL) +- `golang-go` (Debian / Ubuntu) +- `jack-audio-connection-kit` (Fedora / RHEL) +- `jack-audio-connection-kit-devel` (Fedora / RHEL) +- `libjack-jackd2-dev` (Debian / Ubuntu) +- `mingw32-gcc` +- `mingw32-gcc-c++` +- `mingw32-pkg-config` +- `mingw64-gcc` +- `mingw64-gcc-c++` +- `mingw64-pkg-config` +- `openssl` +- `rsync` + +## Q and A + +**Q: To the project initiator: Why the hell did you create this software?** + +**A (short):** I created this software because I wanted to do things that I was unable to do without it. + +**A (long):** I'm a fanatic music lover and concert-goer. Learning to play electric guitar was one of my greatest dreams back in the day when I was like thirteen years old and has remained it ever since. I always wanted to establish a band, stand on a stage, play my instrument in front of a large audience, you get the idea. I've seen hundreds of bands live, and I'm very passionate about audio engineering in general since back in the day when I went to school. However, for a long time, electric guitar required a lot of expensive and large and heavy and especially LOUD equipment, stuff that one simply could not justify to operate inside the parents' home or later inside a small flat in the city. While this became less of an issue since decently-sounding (mostly solid-state) pre-amplifier pedals (and rack units) are available, I always felt the desire to create my own audio equipment, which would sound and behave exactly like I wanted. Being a computer scientist, not an electrical engineer, it was clear that, instead of designing and building an actual circuit for, say, a guitar effects pedal, I had to create a piece of software that would behave LIKE such a circuit. While we currently reach a point where we begin to see some decent computer-based audio effects on the horizon, they simply weren't there back in the day when I started creating this software. Also, most of these effects are still not designed with precision as a priority - at least not the kind of precision you'd expect if you have experience with (scientific) simulation software, like I did. They therefore lack the ultimate "realism", so to speak. Also, these effects are normally not THAT customizable that you could ever hope to recreate, say, the sound of a very specific and rare pedal, which was one of the things I always wanted to be able to do, but until now simply couldn't. + +**Q: How long did it take you to build this thing?** + +**A (short):** Overall it took about five years from the first concept to the first release. Our actual implementation in Golang took about three years until the first release. + +**A (long):** The project started in October 2013 as a small tool, written in Python, which could perform some simple processing (especially non-linear distortion and filtering) on RIFF WAVE files. At this point, our vision was born, to take concepts and algorithms originating from the field of circuit simulation and create an effects processor based upon them. We weren't certain if we could ever hope to achieve sufficient performance for real-time processing with this approach, but if we couldn't, at least we could fall back to file-based processing. We first tried to combine Python (for control) with C (for the actual processing) in order to achieve the performance necessary to perform any kind of real-time processing. We tried several audio APIs and libraries, first "raw" ALSA, then PortAudio, and (very briefly) also rtAudio, but with each of these APIs, there was always a point where we hit a roadblock, so development began to stagnate. Almost two years later, in summer 2015, we found that Golang was finally mature enough and we might give this language a chance and try to implement our algorithms in it instead. In addition, we were also at some sort of tipping point, where we could hope that hardware, which is fast enough to run our algorithms in real-time at an acceptable latency, would begin to become commonplace. We also chose to transition to JACK as our audio API. All of these changes finally gave us the required performance and, after countless nights of coding, countless cups of coffee, a lot of (original) research including measurements on actual audio equipment and analysis, as well as countless chords strummed on the guitar for testing, we finally arrived at what we released in December 2018 as *go-dsp-guitar*. So all in all, it took us slightly more than five years from the first prototype to what we finally considered stable and "polished" enough to be released. + +**Q: Which platforms can I run this software on?** + +**A:** We currently provide binaries for Linux on x86-64 / amd64 (typically PCs), Linux on ARM (typically embedded devices, like a Raspberry Pi), Windows on x86-64 / amd64 (64-bit CPUs, basically all current machines) and Windows on i686 (32-bit CPUs, PCs from pre-2004 or very ressource-limited devices like netbooks from pre-2010). Note that even though the 32-bit variant of the software will typically run on a 64-bit CPU (but not the other way round), using the native (64-bit) variant on a 64-bit machine will be both faster and able to process larger files due to the larger address space of the process. We highly recommend Linux on x86-64 / amd64 for real-time use with JACK. Based upon our own testing, current ARM-based devices will not nearly be fast enough for real-time processing, and Linux currently performs better on x86-64 / amd64 for real-time use than Windows does. We still chose to support Windows as well due to its high market share on the desktop. When running this software on Windows, use the x86-64 / amd64 binary if possible. (It should run, unless your're on a very old machine.) And, of course, you can always use the file-based batch processing mode on slower machines. + +**Q: Why do I run out of memory when batch-processing files?** + +**A:** The batch processing mode currently requires a lot of (virtual) memory, especially when processing large files and / or many channels, since it has to load the entire files into memory, resample all audio material to the target sampling rate and, finally, extend (zero-pad) it to the same length. During this entire process, the entire high-resolution audio data has to reside in (virtual) memory. This is a technical limitation of how *go-dsp-guitar* currently operates when in this particular mode. If you want to process large files, but are unable to provide a lot of (virtual) memory, you might consider running *go-dsp-guitar* in real-time mode with a very high latency setting instead, then feed audio streams from a DAW through *go-dsp-guitar* and back into the DAW, where the result gets recorded. Real-time operation of *go-dsp-guitar* requires a certain amount of processing power from the CPU though. + +**Q: Why don't you support macOS?** + +**A:** We're well aware of the fact that macOS has a high market share among the creative folks. However, we currently neither have a device for building and testing nor do we know, which changes we'd have to make to our software so that it builds for macOS. Feel free to fork our project and try to port it to macOS though. When you're done, submit a pull request and we might merge your changes into mainline. (We still won't be able to provide binaries though.) Keep in mind that we will only accept changes which do not break functionality on our currently supported platforms. + diff --git a/circular/circular.go b/circular/circular.go new file mode 100644 index 0000000..de6e9f6 --- /dev/null +++ b/circular/circular.go @@ -0,0 +1,124 @@ +package circular + +import ( + "fmt" + "sync" +) + +/* + * Data structure implementing a circular buffer. + */ +type bufferStruct struct { + mutex sync.RWMutex + values []float64 + pointer int +} + +/* + * A circular buffer. + */ +type Buffer interface { + Enqueue(elems ...float64) + Length() int + Retrieve(buf []float64) error +} + +/* + * Add elements to the circular buffer, potentially overwriting unread elements. + * + * Semantics: First write to buffer, then increment pointer. + * + * Pointer points to "oldest" element, or next element to be overwritten. + */ +func (this *bufferStruct) Enqueue(elems ...float64) { + numElems := len(elems) + values := this.values + n := len(values) + + /* + * If there are more elements than fit into the buffer, simply copy + * the tail of the element array into the buffer, otherwise perform + * circular write operation. + */ + if numElems >= n { + idx := numElems - n + this.mutex.Lock() + copy(values, elems[idx:numElems]) + this.pointer = 0 + this.mutex.Unlock() + } else { + this.mutex.Lock() + ptr := this.pointer + ptrInc := ptr + numElems + + /* + * Check whether the write operation stays within the array bounds. + */ + if ptrInc < n { + copy(values[ptr:ptrInc], elems) + this.pointer = ptrInc + } else { + head := ptrInc - n + tail := n - ptr + copy(values[ptr:n], elems[0:tail]) + copy(values[0:head], elems[tail:numElems]) + this.pointer = head + } + + this.mutex.Unlock() + } + +} + +/* + * Returns the size of this buffer. + */ +func (this *bufferStruct) Length() int { + vals := this.values + n := len(vals) + return n +} + +/* + * Retrieve all elements from the circular buffer. + */ +func (this *bufferStruct) Retrieve(buf []float64) error { + values := this.values + n := len(values) + m := len(buf) + + /* + * Ensure the target buffer is of equal size. + */ + if n != m { + return fmt.Errorf("%s", "Target buffer must be of the same size as source buffer.") + } else { + this.mutex.RLock() + ptr := this.pointer + tailSize := n - ptr + copy(buf[0:tailSize], values[ptr:n]) + copy(buf[tailSize:n], values[0:ptr]) + this.mutex.RUnlock() + return nil + } + +} + +/* + * Creates a circular buffer of a certain size. + */ +func CreateBuffer(size int) Buffer { + values := make([]float64, size) + m := sync.RWMutex{} + + /* + * Create circular buffer. + */ + buf := bufferStruct{ + mutex: m, + values: values, + pointer: 0, + } + + return &buf +} diff --git a/circular/circular_test.go b/circular/circular_test.go new file mode 100644 index 0000000..cc0cb8a --- /dev/null +++ b/circular/circular_test.go @@ -0,0 +1,166 @@ +package circular + +import ( + "testing" +) + +/* + * Compare two slices to check whether their contents are equal. + */ +func areSlicesEqual(a []float64, b []float64) bool { + eq := true + + /* + * Check whether the two slices are of the same size. + */ + if len(a) != len(b) { + eq = false + } else { + + /* + * Iterate over the arrays to compare values. + */ + for i, elem := range a { + + /* + * Check if we found a mismatch. + */ + if b[i] != elem { + eq = false + } + + } + + } + + return eq +} + +/* + * Perform a unit test on the circular buffer. + */ +func TestBuffer(t *testing.T) { + bufSize := 5 + buf := CreateBuffer(bufSize) + + /* + * Make sure the buffer is non-nil. + */ + if buf == nil { + t.Errorf("Buffer returned from circular.CreateBuffer(5) == nil.") + } else { + + /* + * Describe the elements to be enqueued. + */ + in := [][]float64{ + []float64{1.0}, + []float64{2.0, 3.0}, + []float64{4.0, 5.0, 6.0}, + []float64{7.0, 8.0}, + []float64{9.0, 10.0}, + []float64{11.0, 12.0, 13.0, 14.0, 15.0}, + []float64{16.0, 17.0, 18.0, 19.0, 20.0, 21.0}, + []float64{31.0, 32.0, 33.0, 34.0}, + []float64{35.0, 36.0, 37.0, 38.0}, + []float64{39.0, 40.0, 41.0, 42.0}, + []float64{43.0}, + []float64{44.0}, + } + + out := make([][]float64, 7) + + /* + * Initialize inner slices. + */ + for i, _ := range out { + out[i] = make([]float64, bufSize) + } + + errs := make([]error, 7) + buf.Enqueue(in[0]...) + errs[0] = buf.Retrieve(out[0]) + buf.Enqueue(in[1]...) + errs[1] = buf.Retrieve(out[1]) + buf.Enqueue(in[2]...) + errs[2] = buf.Retrieve(out[2]) + buf.Enqueue(in[3]...) + buf.Enqueue(in[4]...) + errs[3] = buf.Retrieve(out[3]) + buf.Enqueue(in[5]...) + errs[4] = buf.Retrieve(out[4]) + buf.Enqueue(in[6]...) + errs[5] = buf.Retrieve(out[5]) + buf.Enqueue(in[7]...) + buf.Enqueue(in[8]...) + buf.Enqueue(in[9]...) + buf.Enqueue(in[10][0]) + buf.Enqueue(in[11][0]) + errs[6] = buf.Retrieve(out[6]) + + /* + * Describe the elements we expect to be retrieved. + */ + expected := [][]float64{ + []float64{0.0, 0.0, 0.0, 0.0, 1.0}, + []float64{0.0, 0.0, 1.0, 2.0, 3.0}, + []float64{2.0, 3.0, 4.0, 5.0, 6.0}, + []float64{6.0, 7.0, 8.0, 9.0, 10.0}, + []float64{11.0, 12.0, 13.0, 14.0, 15.0}, + []float64{17.0, 18.0, 19.0, 20.0, 21.0}, + []float64{40.0, 41.0, 42.0, 43.0, 44.0}, + } + + /* + * Compare the results. + */ + for i, elem := range out { + ex := expected[i] + pass := areSlicesEqual(elem, ex) + + /* + * If slices differ, report that. + */ + if !pass { + t.Errorf("Array number %d retrieved from circular buffer does not match expectations. Expected: %v Got: %v", i, ex, elem) + } + + } + + /* + * Check for errors. + */ + for i, err := range errs { + + /* + * Check if an error occured. + */ + if err != nil { + msg := err.Error() + t.Errorf("Error while retrieving array %d: %s", i, msg) + } + + } + + tooSmall := make([]float64, bufSize-1) + err := buf.Retrieve(tooSmall) + + /* + * Verify that an error was returned when retrieving contents into a too small buffer. + */ + if err == nil { + t.Errorf("Did not return an error when retrieving into too small buffer.") + } + + l := buf.Length() + + /* + * Check if returned length is correct. + */ + if l != bufSize { + t.Errorf("Circular buffer did not return correct length.") + } + + } + +} diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..85b80fe --- /dev/null +++ b/config/config.json @@ -0,0 +1,30 @@ +{ + "ImpulseResponses": "ir/index.json", + + "WebServer": { + "Name": "go-dsp-guitar/1.0.0", + "Port": "8080", + "TLSPort": "8443", + "TLSPrivateKey": "keys/private.pem", + "TLSPublicKey": "keys/public.pem", + "WebRoot": "webroot/", + "Index": "/index.xhtml", + + "MimeTypes": { + "css": "text/css; charset=utf-8", + "js": "text/javascript; charset=utf-8", + "json": "application/json; charset=utf-8", + "png": "image/png", + "svg": "image/svg+xml; charset=utf-8", + "txt": "text/plain; charset=utf-8", + "webm": "video/webm", + "xhtml": "application/xhtml+xml; charset=utf-8", + "xml": "application/xml; charset=utf-8" + }, + + "DefaultMime": "application/octet-stream", + "ErrorMime": "text/plain; charset=utf-8" + } + +} + diff --git a/controller/controller.go b/controller/controller.go new file mode 100644 index 0000000..81b0db8 --- /dev/null +++ b/controller/controller.go @@ -0,0 +1,2847 @@ +package controller + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "github.com/andrepxx/go-dsp-guitar/effects" + "github.com/andrepxx/go-dsp-guitar/filter" + "github.com/andrepxx/go-dsp-guitar/hwio" + "github.com/andrepxx/go-dsp-guitar/level" + "github.com/andrepxx/go-dsp-guitar/metronome" + "github.com/andrepxx/go-dsp-guitar/resample" + "github.com/andrepxx/go-dsp-guitar/signal" + "github.com/andrepxx/go-dsp-guitar/spatializer" + "github.com/andrepxx/go-dsp-guitar/tuner" + "github.com/andrepxx/go-dsp-guitar/wave" + "github.com/andrepxx/go-dsp-guitar/webserver" + "io/ioutil" + "math" + "os" + "runtime" + "strconv" +) + +/* + * Constants for the controller. + */ +const ( + CONFIG_PATH = "config/config.json" + DEFAULT_SAMPLE_RATE = 96000 + BLOCK_SIZE = 8192 + MORE_OUTPUTS_THAN_INPUTS = 3 +) + +/* + * The configuration for the controller. + */ +type configStruct struct { + ImpulseResponses string + WebServer webserver.Config +} + +/* + * A data structure that tells whether an operation was successful or not. + */ +type webResponseStruct struct { + Success bool + Reason string +} + +/* + * A data structure encoding a parameter for an effects unit. + */ +type webParameterStruct struct { + Name string + Type string + Minimum int32 + Maximum int32 + NumericValue int32 + DiscreteValueIndex int + DiscreteValues []string +} + +/* + * A data structure encoding an effects unit. + */ +type webUnitStruct struct { + Type int + Bypass bool + Parameters []webParameterStruct +} + +/* + * A data structure encoding a signal chain. + */ +type webChainStruct struct { + Units []webUnitStruct +} + +/* + * A data structure encoding the configuration of a single spatializer channel. + */ +type webSpatializerChannelStruct struct { + Azimuth float64 + Distance float64 + Level float64 +} + +/* + * A data structure encoding the spatializer configuration. + */ +type webSpatializerStruct struct { + Channels []webSpatializerChannelStruct +} + +/* + * A data structure encoding the metronome configuration. + */ +type webMetronomeStruct struct { + BeatsPerPeriod uint32 + MasterOutput bool + Speed uint32 + Sounds []string + TickSound string + TockSound string +} + +/* + * A data structure encoding the tuner configuration. + */ +type webTunerStruct struct { + Channel int +} + +/* + * A data structure encoding the results of the analysis performed by a tuner. + */ +type webTunerResultStruct struct { + Cents int8 + Frequency float64 + Note string +} + +/* + * A data structure encoding the results of the analysis performed by a level meter. + */ +type webLevelMeterResultStruct struct { + ChannelName string + Level int32 + Peak int32 +} + +/* + * A data structure encoding the results of the analysis performed by the level meters. + */ +type webLevelMetersResultStruct struct { + DSPLoad int32 + Inputs []webLevelMeterResultStruct + Outputs []webLevelMeterResultStruct + Metronome []webLevelMeterResultStruct + Master []webLevelMeterResultStruct +} + +/* + * A data structure encoding the entire DSP configuration. + */ +type webConfigurationStruct struct { + FramesPerPeriod uint32 + Chains []webChainStruct + Tuner webTunerStruct + Spatializer webSpatializerStruct + Metronome webMetronomeStruct + BatchProcessing bool +} + +/* + * A task for asynchronous signal processing. + */ +type processingTask struct { + chain signal.Chain + inputBuffer []float64 + outputBuffer []float64 + sampleRate uint32 +} + +/* + * The controller for the DSP. + */ +type controllerStruct struct { + binding *hwio.Binding + config configStruct + effects []signal.Chain + impulseResponses filter.ImpulseResponses + levelMetersInput []level.Meter + levelMetersMaster []level.Meter + levelMetersMetr []level.Meter + levelMetersOutput []level.Meter + metr metronome.Metronome + metrMasterOutput bool + running bool + sampleRate uint32 + spat spatializer.Spatializer + tuner tuner.Tuner + tunerChannel int + processingTaskChannel chan processingTask + processingResultChannel chan bool +} + +/* + * The controller interface. + */ +type Controller interface { + Operate(numChannels int) +} + +/* + * Marshals an object into a JSON representation or an error. + * Returns the appropriate MIME type and binary representation. + */ +func (this *controllerStruct) createJSON(obj interface{}) (string, []byte) { + buffer, err := json.MarshalIndent(obj, "", "\t") + + /* + * Check if we got an error during marshalling. + */ + if err != nil { + errString := err.Error() + bufString := bytes.NewBufferString(errString) + bufBytes := bufString.Bytes() + return this.config.WebServer.ErrorMime, bufBytes + } else { + return "application/json; charset=utf-8", buffer + } + +} + +/* + * Adds a new unit to a rack. + */ +func (this *controllerStruct) addUnitHandler(request webserver.HttpRequest) webserver.HttpResponse { + unitTypeString := request.Params["type"] + unitType64, errUnitType := strconv.ParseUint(unitTypeString, 10, 64) + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + webResponse := webResponseStruct{} + + /* + * Check if unit type and chain ID are valid. + */ + if errUnitType != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode unit type.", + } + + } else if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else { + unitType := int(unitType64) + chainId := int(chainId64) + fx := this.effects + nChains := len(fx) + + /* + * Check if chain ID is out of range. + */ + if (chainId < 0) || (chainId >= nChains) { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Chain ID out of range.", + } + + } else { + _, err := fx[chainId].AppendUnit(unitType) + + /* + * Check if unit was successfully appended. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Returns the current rack configuration. + */ +func (this *controllerStruct) getConfigurationHandler(request webserver.HttpRequest) webserver.HttpResponse { + numChannels := len(this.effects) + framesPerPeriod := uint32(0) + + /* + * If we are bound to a hardware interface, query frames per period. + */ + if this.binding != nil { + framesPerPeriod = hwio.FramesPerPeriod() + } + + webChains := make([]webChainStruct, numChannels) + spatChannels := make([]webSpatializerChannelStruct, numChannels) + paramTypes := effects.ParameterTypes() + + /* + * Iterate over the channels and the associated signal chains. + */ + for idChannel, chain := range this.effects { + numUnits := chain.Length() + webUnits := make([]webUnitStruct, numUnits) + + /* + * Iterate over the units in each channel. + */ + for idUnit := 0; idUnit < numUnits; idUnit++ { + unitType, _ := chain.UnitType(idUnit) + bypass, _ := chain.GetBypass(idUnit) + params, _ := chain.Parameters(idUnit) + numParams := len(params) + webParams := make([]webParameterStruct, numParams) + + /* + * Iterate over the parameters and copy all values. + */ + for idParam, param := range params { + webParams[idParam].Name = param.Name + webParams[idParam].Type = paramTypes[param.Type] + webParams[idParam].Minimum = param.Minimum + webParams[idParam].Maximum = param.Maximum + webParams[idParam].NumericValue = param.NumericValue + webParams[idParam].DiscreteValueIndex = param.DiscreteValueIndex + nValues := len(param.DiscreteValues) + webParams[idParam].DiscreteValues = make([]string, nValues) + copy(webParams[idParam].DiscreteValues, param.DiscreteValues) + } + + webUnits[idUnit].Type = unitType + webUnits[idUnit].Bypass = bypass + webUnits[idUnit].Parameters = webParams + } + + webChains[idChannel].Units = webUnits + + /* + * Check if spatializer exists. + */ + if this.spat != nil { + azimuth, _ := this.spat.GetAzimuth(idChannel) + spatChannels[idChannel].Azimuth = azimuth + distance, _ := this.spat.GetDistance(idChannel) + spatChannels[idChannel].Distance = distance + level, _ := this.spat.GetLevel(idChannel) + spatChannels[idChannel].Level = level + } + + } + + tunerChannel := this.tunerChannel + + /* + * Create tuner structure. + */ + tuner := webTunerStruct{ + Channel: tunerChannel, + } + + /* + * Create spatializer structure. + */ + spat := webSpatializerStruct{ + Channels: spatChannels, + } + + currentMetronome := this.metr + irs := this.impulseResponses + beatsPerPeriod := uint32(0) + speed := uint32(0) + preSounds := irs.Names() + numSounds := len(preSounds) + sounds := make([]string, numSounds+1) + sounds[0] = "- NONE -" + copy(sounds[1:], preSounds) + tickSound := "" + tockSound := "" + metrMasterOutput := this.metrMasterOutput + + /* + * Check if we have a metronome. + */ + if this.metr != nil { + beatsPerPeriod = currentMetronome.BeatsPerPeriod() + speed = currentMetronome.Speed() + tickSound, _ = currentMetronome.Tick() + tockSound, _ = currentMetronome.Tock() + } + + /* + * Create metronome structure. + */ + metr := webMetronomeStruct{ + BeatsPerPeriod: beatsPerPeriod, + MasterOutput: metrMasterOutput, + Speed: speed, + Sounds: sounds, + TickSound: tickSound, + TockSound: tockSound, + } + + batchProcessing := (this.binding == nil) + + /* + * Create configuration structure. + */ + cfg := webConfigurationStruct{ + Chains: webChains, + FramesPerPeriod: framesPerPeriod, + Tuner: tuner, + Spatializer: spat, + Metronome: metr, + BatchProcessing: batchProcessing, + } + + mimeType, buffer := this.createJSON(cfg) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Returns the results of the level analysis of the channels. + */ +func (this *controllerStruct) getLevelAnalysisHandler(request webserver.HttpRequest) webserver.HttpResponse { + dspLoad := hwio.DSPLoad() + dspLoad64 := float64(dspLoad) + dspLoadRounded := math.Round(dspLoad64) + dspLoad32 := int32(dspLoadRounded) + levelMetersInput := this.levelMetersInput + levelMetersOutput := this.levelMetersOutput + levelMetersMetr := this.levelMetersMetr + levelMetersMaster := this.levelMetersMaster + inputCount := len(levelMetersInput) + outputCount := len(levelMetersOutput) + metrCount := len(levelMetersMetr) + masterChannelCount := len(levelMetersMaster) + inputResultStructs := make([]webLevelMeterResultStruct, inputCount) + outputResultStructs := make([]webLevelMeterResultStruct, outputCount) + metrResultStructs := make([]webLevelMeterResultStruct, metrCount) + masterResultStructs := make([]webLevelMeterResultStruct, masterChannelCount) + + /* + * Iterate over all input level meters and obtain results. + */ + for i, meter := range levelMetersInput { + i64 := uint64(i) + channelNumber := strconv.FormatUint(i64, 10) + channelName := "in_" + channelNumber + result := meter.Analyze() + level := result.Level() + peak := result.Peak() + + /* + * Fill in web result data structure. + */ + r := webLevelMeterResultStruct{ + ChannelName: channelName, + Level: level, + Peak: peak, + } + + inputResultStructs[i] = r + } + + /* + * Iterate over all output level meters and obtain results. + */ + for i, meter := range levelMetersOutput { + i64 := uint64(i) + channelNumber := strconv.FormatUint(i64, 10) + channelName := "out_" + channelNumber + result := meter.Analyze() + level := result.Level() + peak := result.Peak() + + /* + * Fill in web result data structure. + */ + r := webLevelMeterResultStruct{ + ChannelName: channelName, + Level: level, + Peak: peak, + } + + outputResultStructs[i] = r + } + + /* + * The names of the metronome output channels. + */ + metrChannelNames := []string{ + "metronome", + } + + numMetrChannelNames := len(metrChannelNames) + + /* + * Iterate over all metronome level meters and obtain results. + */ + for i, meter := range levelMetersMetr { + i64 := uint64(i) + channelNumber := strconv.FormatUint(i64, 10) + channelName := "metr_" + channelNumber + + /* + * Check if this channel has a canonical name. + */ + if i < numMetrChannelNames { + channelName = metrChannelNames[i] + } + + result := meter.Analyze() + level := result.Level() + peak := result.Peak() + + /* + * Fill in web result data structure. + */ + r := webLevelMeterResultStruct{ + ChannelName: channelName, + Level: level, + Peak: peak, + } + + metrResultStructs[i] = r + } + + /* + * The names of the master output channels. + */ + masterChannelNames := []string{ + "master_left", + "master_right", + } + + numMasterChannelNames := len(masterChannelNames) + + /* + * Iterate over all master output level meters and obtain results. + */ + for i, meter := range levelMetersMaster { + i64 := uint64(i) + channelNumber := strconv.FormatUint(i64, 10) + channelName := "master_" + channelNumber + + /* + * Check if this channel has a canonical name. + */ + if i < numMasterChannelNames { + channelName = masterChannelNames[i] + } + + result := meter.Analyze() + level := result.Level() + peak := result.Peak() + + /* + * Fill in web result data structure. + */ + r := webLevelMeterResultStruct{ + ChannelName: channelName, + Level: level, + Peak: peak, + } + + masterResultStructs[i] = r + } + + /* + * Create level meters result structure. + */ + result := webLevelMetersResultStruct{ + DSPLoad: dspLoad32, + Inputs: inputResultStructs, + Outputs: outputResultStructs, + Metronome: metrResultStructs, + Master: masterResultStructs, + } + + mimeType, buffer := this.createJSON(result) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Returns a list of all supported types of effects units. + */ +func (this *controllerStruct) getUnitTypesHandler(request webserver.HttpRequest) webserver.HttpResponse { + unitTypes := effects.UnitTypes() + mimeType, buffer := this.createJSON(unitTypes) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Perform a pitch analysis via the tuner and return the results. + */ +func (this *controllerStruct) getTunerAnalysisHandler(request webserver.HttpRequest) webserver.HttpResponse { + analysis, err := this.tuner.Analyze() + response := webserver.HttpResponse{} + + /* + * Check if analysis was successful. + */ + if err != nil { + message := err.Error() + reason := "Failed to perform analysis: " + message + + /* + * Indicate failure. + */ + errResponse := webResponseStruct{ + Success: false, + Reason: reason, + } + + mimeType, buffer := this.createJSON(errResponse) + + /* + * Create HTTP response. + */ + response = webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + } else { + cents := analysis.Cents() + frequency := analysis.Frequency() + note := analysis.Note() + + /* + * Fill the results of the tuner into a data structure. + */ + result := webTunerResultStruct{ + Cents: cents, + Frequency: frequency, + Note: note, + } + + mimeType, buffer := this.createJSON(result) + + /* + * Create HTTP response. + */ + response = webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + } + + return response +} + +/* + * Moves a unit down in a rack. + */ +func (this *controllerStruct) moveDownHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + unitIdString := request.Params["unit"] + unitId64, errUnitId := strconv.ParseUint(unitIdString, 10, 64) + webResponse := webResponseStruct{} + + /* + * Check if chain and unit ID are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errUnitId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode unit ID.", + } + + } else { + chainId := int(chainId64) + unitId := int(unitId64) + fx := this.effects + nChains := len(fx) + + /* + * Check if chain ID is out of range. + */ + if (chainId < 0) || (chainId >= nChains) { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Chain ID out of range.", + } + + } else { + err := fx[chainId].MoveDown(unitId) + + /* + * Check if unit was successfully moved downwards. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Moves a unit up in a rack. + */ +func (this *controllerStruct) moveUpHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + unitIdString := request.Params["unit"] + unitId64, errUnitId := strconv.ParseUint(unitIdString, 10, 64) + webResponse := webResponseStruct{} + + /* + * Check if chain and unit ID are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errUnitId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode unit ID.", + } + + } else { + chainId := int(chainId64) + unitId := int(unitId64) + fx := this.effects + nChains := len(fx) + + /* + * Check if chain ID is out of range. + */ + if (chainId < 0) || (chainId >= nChains) { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Chain ID out of range.", + } + + } else { + err := fx[chainId].MoveUp(unitId) + + /* + * Check if unit was successfully moved upwards. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Cause processing of a file in batch mode. + */ +func (this *controllerStruct) processHandler(request webserver.HttpRequest) webserver.HttpResponse { + this.running = false + + /* + * Indicate success. + */ + webResponse := webResponseStruct{ + Success: true, + Reason: "", + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Removes a unit from a rack. + */ +func (this *controllerStruct) removeUnitHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + unitIdString := request.Params["unit"] + unitId64, errUnitId := strconv.ParseUint(unitIdString, 10, 64) + webResponse := webResponseStruct{} + + /* + * Check if chain and unit ID are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errUnitId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode unit ID.", + } + + } else { + chainId := int(chainId64) + unitId := int(unitId64) + fx := this.effects + nChains := len(fx) + + /* + * Check if chain ID is out of range. + */ + if (chainId < 0) || (chainId >= nChains) { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Chain ID out of range.", + } + + } else { + err := fx[chainId].RemoveUnit(unitId) + + /* + * Check if unit was successfully removed. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Sets the azimuth of a channel in the spatializer. + */ +func (this *controllerStruct) setAzimuthHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + valueString := request.Params["value"] + valueInt, errValue := strconv.ParseInt(valueString, 10, 64) + webResponse := webResponseStruct{} + + /* + * Check if chain ID and azimuth value are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errValue != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode azimuth value.", + } + + } else { + chainId := int(chainId64) + value := float64(valueInt) + err := this.spat.SetAzimuth(chainId, value) + + /* + * Check if azimuth was set successfully. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Enables or disables bypass for an effects unit. + */ +func (this *controllerStruct) setBypassHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + unitIdString := request.Params["unit"] + unitId64, errUnitId := strconv.ParseUint(unitIdString, 10, 64) + valueString := request.Params["value"] + value, errValue := strconv.ParseBool(valueString) + webResponse := webResponseStruct{} + + /* + * Check if chain ID, unit ID and value are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errUnitId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode unit ID.", + } + + } else if errValue != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode value.", + } + + } else { + chainId := int(chainId64) + unitId := int(unitId64) + fx := this.effects + nChains := len(fx) + + /* + * Check if chain ID is out of range. + */ + if (chainId < 0) || (chainId >= nChains) { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Chain ID out of range.", + } + + } else { + err := fx[chainId].SetBypass(unitId, value) + + /* + * Check if bypass value was successfully set. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Sets a discrete value as a parameter in an effects unit. + */ +func (this *controllerStruct) setDiscreteValueHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + unitIdString := request.Params["unit"] + unitId64, errUnitId := strconv.ParseUint(unitIdString, 10, 64) + param := request.Params["param"] + value := request.Params["value"] + webResponse := webResponseStruct{} + + /* + * Check if chain ID, unit ID and value are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errUnitId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode unit ID.", + } + + } else { + chainId := int(chainId64) + unitId := int(unitId64) + fx := this.effects + nChains := len(fx) + + /* + * Check if chain ID is out of range. + */ + if (chainId < 0) || (chainId >= nChains) { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Chain ID out of range.", + } + + } else { + err := fx[chainId].SetDiscreteValue(unitId, param, value) + + /* + * Check if bypass value was successfully set. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Sets the distance of a channel in the spatializer. + */ +func (this *controllerStruct) setDistanceHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + valueString := request.Params["value"] + value, errDistance := strconv.ParseFloat(valueString, 64) + webResponse := webResponseStruct{} + + /* + * Check if chain ID and distance value are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errDistance != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode distance value.", + } + + } else { + chainId := int(chainId64) + err := this.spat.SetDistance(chainId, value) + + /* + * Check if distance was set successfully. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Sets the frames per period for the hardware interface. + */ +func (this *controllerStruct) setFramesPerPeriodHandler(request webserver.HttpRequest) webserver.HttpResponse { + valueString := request.Params["value"] + value64, err := strconv.ParseUint(valueString, 10, 32) + webResponse := webResponseStruct{} + + /* + * Check if value is valid. + */ + if err != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to parse frame count.", + } + + } else { + value32 := uint32(value64) + hwio.SetFramesPerPeriod(value32) + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Sets the level of a channel in the spatializer. + */ +func (this *controllerStruct) setLevelHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + valueString := request.Params["value"] + value, errDistance := strconv.ParseFloat(valueString, 64) + webResponse := webResponseStruct{} + + /* + * Check if chain ID and distance value are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errDistance != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode level value.", + } + + } else { + chainId := int(chainId64) + err := this.spat.SetLevel(chainId, value) + + /* + * Check if distance was set successfully. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Sets a value for the metronome. + */ +func (this *controllerStruct) setMetronomeValueHandler(request webserver.HttpRequest) webserver.HttpResponse { + metr := this.metr + webResponse := webResponseStruct{} + + /* + * Check if we have a metronome. + */ + if metr != nil { + param := request.Params["param"] + value := request.Params["value"] + + /* + * Check which parameter should be edited. + */ + switch param { + case "beats-per-period": + rawValue, err := strconv.ParseUint(value, 10, 32) + + /* + * Check if value failed to parse. + */ + if err != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode metronome beats per minute.", + } + + } else { + value := uint32(rawValue) + metr.SetBeatsPerPeriod(value) + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + case "master-output": + value, err := strconv.ParseBool(value) + + /* + * Check if value failed to parse. + */ + if err != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode metronome master output flag.", + } + + } else { + this.metrMasterOutput = value + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + case "speed": + rawValue, err := strconv.ParseUint(value, 10, 32) + + /* + * Check if value failed to parse. + */ + if err != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode metronome speed.", + } + + } else { + value := uint32(rawValue) + metr.SetSpeed(value) + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + case "tick-sound": + + /* + * Check if we should disable the tick sound. + */ + if value == "- NONE -" { + this.metr.SetTick(value, nil) + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } else { + sampleRate := this.sampleRate + flt := this.impulseResponses.CreateFilter(value, sampleRate) + + /* + * Check if filter was successfully loaded. + */ + if flt == nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to load impulse response for metronome tick sound.", + } + + } else { + coeffs := flt.Coefficients() + this.metr.SetTick(value, coeffs) + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + case "tock-sound": + + /* + * Check if we should disable the tock sound. + */ + if value == "- NONE -" { + this.metr.SetTock(value, nil) + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } else { + sampleRate := this.sampleRate + flt := this.impulseResponses.CreateFilter(value, sampleRate) + + /* + * Check if filter was successfully loaded. + */ + if flt == nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to load impulse response for metronome tick sound.", + } + + } else { + coeffs := flt.Coefficients() + this.metr.SetTock(value, coeffs) + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + default: + reason := fmt.Sprintf("Unknown metronome parameter: '%s'", param) + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Sets a value for the tuner. + */ +func (this *controllerStruct) setTunerValueHandler(request webserver.HttpRequest) webserver.HttpResponse { + metr := this.metr + webResponse := webResponseStruct{} + + /* + * Check if we have a metronome. + */ + if metr != nil { + param := request.Params["param"] + value := request.Params["value"] + + /* + * Check which parameter should be edited. + */ + switch param { + case "channel": + rawValue, err := strconv.ParseInt(value, 10, 64) + + /* + * Check if value failed to parse. + */ + if err != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode tuner channel.", + } + + } else { + this.tunerChannel = int(rawValue) + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + default: + reason := fmt.Sprintf("Unknown tuner parameter: '%s'", param) + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Sets a numeric value as a parameter in an effects unit. + */ +func (this *controllerStruct) setNumericValueHandler(request webserver.HttpRequest) webserver.HttpResponse { + chainIdString := request.Params["chain"] + chainId64, errChainId := strconv.ParseUint(chainIdString, 10, 64) + unitIdString := request.Params["unit"] + unitId64, errUnitId := strconv.ParseUint(unitIdString, 10, 64) + param := request.Params["param"] + valueString := request.Params["value"] + value64, errValue := strconv.ParseInt(valueString, 10, 32) + webResponse := webResponseStruct{} + + /* + * Check if chain ID, unit ID and value are valid. + */ + if errChainId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode chain ID.", + } + + } else if errUnitId != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode unit ID.", + } + + } else if errValue != nil { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Failed to decode value.", + } + + } else { + chainId := int(chainId64) + unitId := int(unitId64) + value := int32(value64) + fx := this.effects + nChains := len(fx) + + /* + * Check if chain ID is out of range. + */ + if (chainId < 0) || (chainId >= nChains) { + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: "Chain ID out of range.", + } + + } else { + err := fx[chainId].SetNumericValue(unitId, param, value) + + /* + * Check if bypass value was successfully set. + */ + if err != nil { + reason := err.Error() + + /* + * Indicate failure. + */ + webResponse = webResponseStruct{ + Success: false, + Reason: reason, + } + + } else { + + /* + * Indicate success. + */ + webResponse = webResponseStruct{ + Success: true, + Reason: "", + } + + } + + } + + } + + mimeType, buffer := this.createJSON(webResponse) + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": mimeType}, + Body: buffer, + } + + return response +} + +/* + * Handles CGI requests that could not be dispatched to other CGIs. + */ +func (this *controllerStruct) errorHandler(request webserver.HttpRequest) webserver.HttpResponse { + + /* + * Create HTTP response. + */ + response := webserver.HttpResponse{ + Header: map[string]string{"Content-type": this.config.WebServer.ErrorMime}, + Body: bytes.NewBufferString("This CGI call is not implemented.").Bytes(), + } + + return response +} + +/* + * Dispatch CGI requests to the corresponding CGI handlers. + */ +func (this *controllerStruct) dispatch(request webserver.HttpRequest) webserver.HttpResponse { + cgi := request.Params["cgi"] + response := webserver.HttpResponse{} + + /* + * Find the right CGI to handle the request. + */ + switch cgi { + case "add-unit": + response = this.addUnitHandler(request) + case "get-configuration": + response = this.getConfigurationHandler(request) + case "get-level-analysis": + response = this.getLevelAnalysisHandler(request) + case "get-unit-types": + response = this.getUnitTypesHandler(request) + case "get-tuner-analysis": + response = this.getTunerAnalysisHandler(request) + case "move-down": + response = this.moveDownHandler(request) + case "move-up": + response = this.moveUpHandler(request) + case "process": + response = this.processHandler(request) + case "remove-unit": + response = this.removeUnitHandler(request) + case "set-azimuth": + response = this.setAzimuthHandler(request) + case "set-bypass": + response = this.setBypassHandler(request) + case "set-discrete-value": + response = this.setDiscreteValueHandler(request) + case "set-distance": + response = this.setDistanceHandler(request) + case "set-frames-per-period": + response = this.setFramesPerPeriodHandler(request) + case "set-level": + response = this.setLevelHandler(request) + case "set-metronome-value": + response = this.setMetronomeValueHandler(request) + case "set-tuner-value": + response = this.setTunerValueHandler(request) + case "set-numeric-value": + response = this.setNumericValueHandler(request) + default: + response = this.errorHandler(request) + } + + return response +} + +/* + * Perform asynchronous signal processing. + */ +func (this *controllerStruct) processAsync() { + requests := this.processingTaskChannel + responses := this.processingResultChannel + + /* + * Process tasks as long as channel is open. + */ + for task := range requests { + chain := task.chain + inputBuffer := task.inputBuffer + outputBuffer := task.outputBuffer + sampleRate := task.sampleRate + chain.Process(inputBuffer, outputBuffer, sampleRate) + responses <- true + } + + close(responses) +} + +/* + * Process audio data. + */ +func (this *controllerStruct) process(inputBuffers [][]float64, outputBuffers [][]float64, sampleRate uint32) { + nIn := len(inputBuffers) + nOut := len(outputBuffers) + nMinOut := nIn + (spatializer.OUTPUT_COUNT + metronome.OUTPUT_COUNT) + tunerChannel := this.tunerChannel + + /* + * Check if an input channel should be passed to the tuner. + */ + if (tunerChannel >= 0) && (tunerChannel < nIn) { + tunerInput := inputBuffers[tunerChannel] + this.tuner.Process(tunerInput, sampleRate) + } + + /* + * Ensure that there are at least as many outputs as inputs registered. + */ + if (nOut >= nIn) && (nIn >= 0) { + + /* + * Start processing for each input channel. + */ + for i := 0; i < nIn; i++ { + + /* + * Create a new signal processing task. + */ + task := processingTask{ + chain: this.effects[i], + inputBuffer: inputBuffers[i], + outputBuffer: outputBuffers[i], + sampleRate: sampleRate, + } + + this.processingTaskChannel <- task + } + + /* + * Wait for processing of each channel to finish. + */ + for i := 0; i < nIn; i++ { + <-this.processingResultChannel + } + + levelMetersInput := this.levelMetersInput + + /* + * Perform level analysis for each input. + */ + for i, meter := range levelMetersInput { + buffer := inputBuffers[i] + meter.Process(buffer, sampleRate) + } + + levelMetersOutput := this.levelMetersOutput + + /* + * Perform level analysis for each output. + */ + for i, meter := range levelMetersOutput { + buffer := outputBuffers[i] + meter.Process(buffer, sampleRate) + } + + } + + /* + * Check if there are enough output channels for a spatializer and a metronome. + */ + if nOut >= nMinOut { + lastIdx := nOut - 1 + auxBuffer := outputBuffers[lastIdx] + + /* + * Check if there is a metronome. + */ + if this.metr == nil { + auxBuffer = nil + } else { + this.metr.Process(auxBuffer) + levelMetersMetr := this.levelMetersMetr + + /* + * Perform level analysis for the metronome. + */ + for _, meter := range levelMetersMetr { + meter.Process(auxBuffer, sampleRate) + } + + } + + /* + * Check if there is a spatializer. + */ + if this.spat != nil { + + /* + * Check if metronome output should be excluded from the master output. + */ + if !this.metrMasterOutput { + auxBuffer = nil + } + + uBound := nIn + spatializer.OUTPUT_COUNT + spatializerInputs := outputBuffers[0:nIn] + spatializerOutputs := outputBuffers[nIn:uBound] + this.spat.Process(spatializerInputs, auxBuffer, spatializerOutputs) + levelMetersMaster := this.levelMetersMaster + + /* + * Perform level analysis for each master output. + */ + for i, meter := range levelMetersMaster { + buffer := spatializerOutputs[i] + meter.Process(buffer, sampleRate) + } + + } + + } + +} + +/* + * This is called when the hardware changes the sample rate. + */ +func (this *controllerStruct) sampleRateListener(rate uint32) { + this.sampleRate = rate + this.spat.SetSampleRate(rate) + this.metr.SetSampleRate(rate) +} + +/* + * Get input from the user. + */ +func (this *controllerStruct) getInput(scanner *bufio.Scanner, prompt string) string { + fmt.Printf("%s", prompt) + scanner.Scan() + s := scanner.Text() + return s +} + +/* + * Process files for batch processing. + */ +func (this *controllerStruct) processFiles(scanner *bufio.Scanner, targetRate uint32) { + numChannels := len(this.effects) + fmt.Printf("Web interface initiated batch processing for %d channels.\n", numChannels) + inputs := make([][]float64, numChannels) + sampleRates := make([]uint32, numChannels) + outputFormat := uint16(wave.AUDIO_PCM) + validFormat := false + + /* + * Query the user for a target format. + */ + for !validFormat { + targetFormat := this.getInput(scanner, "Please enter target format ('lpcm' or 'float'): ") + + /* + * Find out about the target format. + */ + switch targetFormat { + case "lpcm": + outputFormat = wave.AUDIO_PCM + validFormat = true + case "float": + outputFormat = wave.AUDIO_IEEE_FLOAT + validFormat = true + } + + } + + bitDepth := uint16(wave.DEFAULT_BIT_DEPTH) + validBitDepth := false + + /* + * Query the user for a target bit depth. + */ + for !validBitDepth { + + /* + * Different formats support different bit depths. + */ + switch outputFormat { + case wave.AUDIO_PCM: + targetBitDepthString := this.getInput(scanner, "Please enter target bit depth (8 or 16 or 24 or 32): ") + targetBitDepth64, _ := strconv.ParseUint(targetBitDepthString, 10, 64) + + /* + * Check if the target bit depth is valid. + */ + if targetBitDepth64 == 8 || targetBitDepth64 == 16 || targetBitDepth64 == 24 || targetBitDepth64 == 32 { + bitDepth = uint16(targetBitDepth64) + validBitDepth = true + } + + case wave.AUDIO_IEEE_FLOAT: + targetBitDepthString := this.getInput(scanner, "Please enter target bit depth (32 or 64): ") + targetBitDepth64, _ := strconv.ParseUint(targetBitDepthString, 10, 64) + + /* + * Check if the target bit depth is valid. + */ + if targetBitDepth64 == 32 || targetBitDepth64 == 64 { + bitDepth = uint16(targetBitDepth64) + validBitDepth = true + } + + default: + fmt.Printf("WARNING! Unrecognized format code: %#04x\n - Continuing with default bit depth: %d (This should not happen!)\n", outputFormat, bitDepth) + validBitDepth = true + } + + } + + /* + * Query file name and channel number for each input. + */ + for fileId := 0; fileId < numChannels; fileId++ { + fmt.Printf("%s\n", "Enter name/path of the wave file for input.") + prompt := fmt.Sprintf("File for input %d: ", fileId) + fileName := this.getInput(scanner, prompt) + + /* + * Abort if file name is empty. + */ + if fileName == "" { + fmt.Printf("Leaving channel %d empty.\n", fileId) + inputs[fileId] = make([]float64, 0) + sampleRates[fileId] = DEFAULT_SAMPLE_RATE + } else { + buf, err := ioutil.ReadFile(fileName) + + /* + * Check if file could be read. + */ + if err != nil { + fmt.Printf("Failed to read wave file. Leaving channel %d empty.\n", fileId) + inputs[fileId] = make([]float64, 0) + sampleRates[fileId] = DEFAULT_SAMPLE_RATE + } else { + f, err := wave.FromBuffer(buf) + + /* + * Check if file could be parsed. + */ + if err != nil { + msg := err.Error() + fmt.Printf("Failed to parse wave file: %s\n", msg) + inputs[fileId] = make([]float64, 0) + sampleRates[fileId] = DEFAULT_SAMPLE_RATE + } else { + numChannels := f.ChannelCount() + + /* + * If file contains only one channel, take first, + * otherwise ask which one to use. + */ + if numChannels == 1 { + c, err := f.Channel(0) + + /* + * Check if channel could be loaded. + */ + if err != nil { + buf := c.Floats() + inputs = append(inputs, buf) + } + + } else { + loadedChan := false + + /* + * Do this until the channel has been loaded. + */ + for !loadedChan { + uBound := numChannels - 1 + prompt := fmt.Sprintf("File contains %d channels. Which channel [%d, %d] to use? ", numChannels, 0, uBound) + channelString := this.getInput(scanner, prompt) + n, err := strconv.ParseUint(channelString, 10, 16) + + /* + * If input is valid, load this channel. + */ + if err != nil { + fmt.Printf("%s\n", "Not a valid channel number.") + } else { + id := uint16(n) + c, err := f.Channel(id) + + /* + * Check if channel could be loaded. + */ + if err != nil { + msg := err.Error() + fmt.Printf("Failed to load channel: %s\n", msg) + inputs[fileId] = make([]float64, 0) + sampleRates[fileId] = DEFAULT_SAMPLE_RATE + } else { + inputs[fileId] = c.Floats() + sampleRates[fileId] = f.SampleRate() + loadedChan = true + } + + } + + } + + } + + } + + } + + } + + } + + /* + * Resample all inputs to the target sample rate. + */ + for i, input := range inputs { + sampleRate := sampleRates[i] + + /* + * Check if resampling is necessary. + */ + if sampleRate != targetRate { + fmt.Printf("Resampling input channel %d from %d Hz to %d Hz, please wait ...\n", i, sampleRate, targetRate) + inputs[i] = resample.Time(input, sampleRate, targetRate) + runtime.GC() + } + + } + + maxLength := int(0) + + /* + * Find the length of the longest input stream. + */ + for _, input := range inputs { + size := len(input) + + /* + * If we found a longer input stream, store its length. + */ + if size > maxLength { + maxLength = size + } + + } + + /* + * Length must be a multiple of the block size. + */ + if (maxLength % BLOCK_SIZE) != 0 { + maxLength = BLOCK_SIZE * ((maxLength / BLOCK_SIZE) + 1) + } + + /* + * Extend each input stream to equal length. + */ + for i, input := range inputs { + size := len(input) + + /* + * If size of input stream doesn't already match, extend it. + */ + if size != maxLength { + inputNew := make([]float64, maxLength) + copy(inputNew, input) + inputs[i] = inputNew + runtime.GC() + } + + } + + numInputs := len(inputs) + numOutputs := numInputs + MORE_OUTPUTS_THAN_INPUTS + outputs := make([][]float64, numOutputs) + inputBuffers := make([][]float64, numInputs) + outputBuffers := make([][]float64, numOutputs) + + /* + * Create each inner output buffer. + */ + for i := 0; i < numOutputs; i++ { + outputs[i] = make([]float64, maxLength) + outputBuffers[i] = make([]float64, BLOCK_SIZE) + } + + /* + * Create each inner input buffer. + */ + for i := 0; i < numInputs; i++ { + inputBuffers[i] = make([]float64, BLOCK_SIZE) + } + + numBlocks := maxLength / BLOCK_SIZE + numBlocksFloat := float64(numBlocks) + fmt.Printf("%s\n", "Processing audio data ...") + oldPercents := int(0) + + /* + * Process each block. + */ + for block := 0; block < numBlocks; block++ { + blockFloat := float64(block) + percents := int((100.0 * blockFloat) / numBlocksFloat) + + /* + * Check if percentage changed. + */ + if percents != oldPercents { + fmt.Printf(" %d", percents) + oldPercents = percents + } + + offsetStart := BLOCK_SIZE * block + offsetEnd := offsetStart + BLOCK_SIZE + + /* + * Copy part of each input stream into the input buffers. + */ + for i, input := range inputs { + copy(inputBuffers[i], input[offsetStart:offsetEnd]) + } + + this.process(inputBuffers, outputBuffers, targetRate) + + /* + * Copy the output buffers into the right place in the output streams. + */ + for i, output := range outputs { + copy(output[offsetStart:offsetEnd], outputBuffers[i]) + } + + } + + fmt.Printf("\n") + + /* + * Discard the input streams to free memory. + */ + for i := 0; i < numInputs; i++ { + inputs[i] = nil + } + + runtime.GC() + + /* + * Write each output into a wave file. + */ + for i, output := range outputs { + f, err := wave.CreateEmpty(targetRate, outputFormat, bitDepth, 1) + + /* + * Check whether we were able to create a wave file. + */ + if err != nil { + msg := err.Error() + fmt.Printf("Failed to create wave file: %s", msg) + } else { + c, err := f.Channel(0) + + /* + * Check whether we were able to obtain the channel. + */ + if err != nil { + msg := err.Error() + fmt.Printf("Failed to create output %d: %s\n", i, msg) + } else { + c.WriteFloats(output) + buf, err := f.Bytes() + f = nil + runtime.GC() + + /* + * Check whether we were able to serialize the channel. + */ + if err != nil { + msg := err.Error() + fmt.Printf("Failed to serialize output %d: %s\n", i, msg) + } else { + iLong := uint64(i) + iString := strconv.FormatUint(iLong, 10) + channelName := "out_" + iString + + /* + * Check whether output channel is "special". + */ + switch i { + case numInputs: + channelName = "master_left" + case numInputs + 1: + channelName = "master_right" + case numInputs + 2: + channelName = "metronome" + } + + prompt := fmt.Sprintf("Output file for channel '%s': ", channelName) + path := this.getInput(scanner, prompt) + fd, err := os.Create(path) + + /* + * Check if file was successfully created. + */ + if err != nil { + fmt.Printf("%s\n", "Failed to create output file.") + } else { + _, err = fd.Write(buf) + + /* + * Check if buffer was written successfully. + */ + if err != nil { + fmt.Printf("%s\n", "Failed to write to output file.") + } + + err = fd.Close() + buf = nil + runtime.GC() + + /* + * Check if file was closed successfully. + */ + if err != nil { + msg := err.Error() + fmt.Printf("%s\n", "Failed to close output file.", msg) + } + + } + + } + + } + + } + + } + + /* + * Discard the output streams to free memory. + */ + for i := 0; i < numOutputs; i++ { + outputs[i] = nil + runtime.GC() + } + +} + +/* + * Initialize the controller. + */ +func (this *controllerStruct) initialize(nInputs int, useHardware bool) error { + content, err := ioutil.ReadFile(CONFIG_PATH) + + /* + * Check if file could be read. + */ + if err != nil { + return fmt.Errorf("Could not open config file: '%s'", CONFIG_PATH) + } else { + config := configStruct{} + err = json.Unmarshal(content, &config) + this.config = config + + /* + * Check if file failed to unmarshal. + */ + if err != nil { + return fmt.Errorf("Could not decode config file: '%s'", CONFIG_PATH) + } else { + ir, err := filter.Import(config.ImpulseResponses) + + /* + * Check if impulse responses failed to load. + */ + if err != nil { + return err + } else { + this.impulseResponses = ir + fx := make([]signal.Chain, nInputs) + + /* + * Create an effects chain for each input. + */ + for i := 0; i < nInputs; i++ { + fx[i] = signal.CreateChain(ir) + } + + this.effects = fx + this.sampleRate = DEFAULT_SAMPLE_RATE + this.spat = spatializer.Create(nInputs) + this.metr = metronome.Create() + this.tuner = tuner.Create() + this.tunerChannel = -1 + levelMetersInput := make([]level.Meter, nInputs) + + /* + * Create level meter for each input channel. + */ + for i := range levelMetersInput { + levelMetersInput[i] = level.CreateMeter() + } + + this.levelMetersInput = levelMetersInput + levelMetersOutput := make([]level.Meter, nInputs) + + /* + * Create level meter for each output channel. + */ + for i := range levelMetersOutput { + levelMetersOutput[i] = level.CreateMeter() + } + + this.levelMetersOutput = levelMetersOutput + levelMetersMetr := make([]level.Meter, 1) + + /* + * Create level meter for each metronome channel. + */ + for i := range levelMetersMetr { + levelMetersMetr[i] = level.CreateMeter() + } + this.levelMetersMetr = levelMetersMetr + masterOutputs := int(spatializer.OUTPUT_COUNT) + levelMetersMaster := make([]level.Meter, masterOutputs) + + /* + * Create level meter for each master output channel. + */ + for i := range levelMetersMaster { + levelMetersMaster[i] = level.CreateMeter() + } + + this.levelMetersMaster = levelMetersMaster + this.processingTaskChannel = make(chan processingTask, nInputs) + this.processingResultChannel = make(chan bool, nInputs) + + /* + * Start a worker thread for each input channel. + */ + for i := 0; i < nInputs; i++ { + go this.processAsync() + } + + /* + * If we don't use hardware I/O, we are done, otherwise register hardware binding. + */ + if !useHardware { + return nil + } else { + this.binding, err = hwio.Register(this.process, this.sampleRateListener) + return err + } + + } + + } + + } + +} + +/* + * Finalize the controller, freeing allocated ressources. + */ +func (this *controllerStruct) finalize() { + this.running = false + hwio.Unregister(this.binding) + close(this.processingTaskChannel) +} + +/* + * Main routine of our controller. Performs initialization, then runs the message pump. + */ +func (this *controllerStruct) Operate(numChannels int) { + batch := numChannels > 0 + err := fmt.Errorf("") + + /* + * If we are not in batch processing mode, acquire hardware channels. + */ + if !batch { + err = this.initialize(hwio.INPUT_CHANNELS, true) + } else { + err = this.initialize(numChannels, false) + } + + /* + * Check if initialization was successful. + */ + if err != nil { + msg := err.Error() + msgNew := "Initialization failed: " + msg + fmt.Printf("%s\n", msgNew) + } else { + serverCfg := this.config.WebServer + server := webserver.CreateWebServer(serverCfg) + + /* + * Check if we got a web server. + */ + if server == nil { + fmt.Printf("%s\n", "Web server did not enter message loop.") + } else { + channels := server.RegisterCgi("/cgi-bin/dsp") + server.Run() + in := os.Stdin + scanner := bufio.NewScanner(in) + sampleRate := uint32(DEFAULT_SAMPLE_RATE) + sampleRates := filter.SampleRates() + + /* + * If we are in batch mode, prepare file processing. + */ + if batch { + sampleRate64 := uint64(0) + correctRate := false + + /* + * Ask user to enter sample rate. + */ + for !correctRate { + sampleRateString := this.getInput(scanner, "Target sample rate: ") + sampleRate64, err = strconv.ParseUint(sampleRateString, 10, 64) + sampleRate = uint32(sampleRate64) + + /* + * Check if sample rate could be parsed. + */ + if err == nil { + + /* + * Iterate over the supported sample rates. + */ + for _, currentRate := range sampleRates { + + /* + * Check if sample rate is supported. + */ + if currentRate == sampleRate { + correctRate = true + } + + } + + } + + /* + * If rate is not supported, output error message. + */ + if !correctRate { + fmt.Printf("%s\n", "Sample rate not supported.") + } + + } + + } + + this.sampleRate = sampleRate + tlsPort := serverCfg.TLSPort + fmt.Printf("Web interface ready: https://localhost:%s/\n", tlsPort) + + /* + * We should not terminate. + */ + for { + this.running = true + + /* + * This is the actual message pump. + */ + for this.running { + request := <-channels.Requests + response := this.dispatch(request) + channels.Responses <- response + } + + /* + * If we are in batch mode, process files. + */ + if batch { + this.processFiles(scanner, sampleRate) + } + + } + + } + + this.finalize() + } + +} + +/* + * Creates a new controller. + */ +func CreateController() Controller { + controller := controllerStruct{} + return &controller +} diff --git a/doc/changelog.txt b/doc/changelog.txt new file mode 100644 index 0000000..24c66c9 --- /dev/null +++ b/doc/changelog.txt @@ -0,0 +1,42 @@ +Changelog for go-dsp-guitar +--------------------------- + +v1.0.0 + +- Reimplement DSP code in Golang using JACK instead of ALSA. +- Implement improved filter simulation using fast convolution. +- Pay attention to the order in which filters were compiled by applying sequence numbers to the data structure in the compilation channel. +- Implement four-band equalizer using IIR filters. +- Implement web-based user interface. +- Fix deadlock, which occurs, when a filter is recompiled while the associated "power amp" effect is in bypass mode. +- Implement metronome. +- Get rid of remaining hard-coded sample rate values in the controller. +- Implement phaser effect. +- Implement auto-wah effect using envelope-follower. +- Optimize the FFT implementation. + - Optimize generation of Fourier coefficients. + - Optimize generation of permutation coefficients. + - Calculate 2N-point FFT of real and/or Hermitian functions using N-point complex FFT. + - Make use of "math/bits" for bit-level operations. These functions get translated into specialized machine instructions. +- Process all channels concurrently. +- Enable the envelope follower to track RMS power levels instead of peak levels only. +- Implement an instrument tuner based on the auto-correlation function. +- Improve the accuracy of the tuner using quadratic interpolation of the auto-correlation function. +- Enable processing of WAV files for simpler "re-amping". +- Implement support for 64-bit "RF64"-files. +- Replace the legacy "JQuery knob" with "pure-knob". +- Optimize the algorithm for fast convolution for the case that the filter order is smaller than the buffer size. +- Implement a (peak) level meter according to DIN IEC 60268-18. +- Implement a method to change vectorization (frames per period) of the simulation algorithm, allowing the user to trade off simulation complexity (and therefore possible accuracy) and latency. +- Use Lanczos interpolation in the Fourier domain to reduce filter order. This preserves the original frequency response of the filter much better than the previous method while also preserving phase response, minimizing perceived latency. +- Get entirely rid of JQuery as a dependency using an associative data storage backed by a WeakMap instead of the '$(element).data(key, value)' function. +- Implement unit-tests with good coverage for several packages. + - "circular" + - "fft" + - "level" + - "random" + - "resample" + - "tuner" + - "wave" +- Create user documentation. + diff --git a/doc/img/screenshot-01-thumb.png b/doc/img/screenshot-01-thumb.png new file mode 100644 index 0000000..6bb51e3 Binary files /dev/null and b/doc/img/screenshot-01-thumb.png differ diff --git a/doc/img/screenshot-01.png b/doc/img/screenshot-01.png new file mode 100644 index 0000000..48980db Binary files /dev/null and b/doc/img/screenshot-01.png differ diff --git a/doc/jack-setup.txt b/doc/jack-setup.txt new file mode 100644 index 0000000..680c041 --- /dev/null +++ b/doc/jack-setup.txt @@ -0,0 +1,106 @@ +## Setting up JACK under Windows. + +I will assume you have an ASIO-capable card in your system and the audio drivers installed and working. + +Go to http://jackaudio.org/downloads, download *JACK 2 for Windows (64-bit)* and install it in the default location. + +Default location: `C:\Program Files (x86)\Jack\` + +Open up *JACK Control*, set the following options. + +--- Parameters --- + +Driver: portaudio +Realtime: yes +Sample rate: 96000 +Frames/Period: 1024 + +--- Advanced --- + +Output Device: *[Your ASIO driver]* (For me: "ASIO::UMC ASIO Driver") +Input Device: *[Your ASIO driver]* (For me: "ASIO::UMC ASIO Driver") + +## Setting up Go under Windows. + +Go to https://golang.org/dl/, download latest version of Go for Windows and install it in the default location. + +Default location: `C:\Go\` + +## Setting up MSYS and MinGW. + +Download *MinGW-w64* from: https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/installer/mingw-w64-install.exe/download + +Run the installer. + +Change the *architecture* value to *x86_64* for a 64-bit system. + +By default, *MinGW-w64* will install to `C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64`. Change that! The space will cause problems later on. I'm serious! Install it to `C:\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64` instead. + +We will call that last path the *MinGW-w64 root directory*. + +Download *MSYS-20111123.zip* from: https://sourceforge.net/projects/mingw-w64/files/External%20binary%20packages%20%28Win64%20hosted%29/MSYS%20%2832-bit%29/. + +Exctract the MSYS archive and it will contain a folder called `msys`. + +Take everything that's inside that folder and copy it into the *MinGW-w64 root directory*. (Do not copy the `msys` folder itself, but its contents.) + +When asked whether folders should be merged, say: "Yes, always." + +## Now, let's build it. :-) + +Run `msys.bat` and a `/home` directory will be created (inside your *MinGW-w64 root directory*) and a POSIX shell will be opened. + +Create `~/go` directory and directory structure. + +``` +$ mkdir go +$ cd go/ +$ mkdir src +$ cd src/ +$ mkdir github.com +$ cd github.com/ +$ mkdir andrepxx +$ cd andrepxx/ +$ mkdir go-jack +$ cd go-jack/ +``` + +Download source of *github.com/andrepxx/go-jack* as ZIP file and extract the contents of the `go-jack-master/` directory (contained within the Archive) to the directory created above (inside MinGW). + +(No, you cannot just `go get ...` it, since there is no `git` in *MinGW*.) + +Set your `$GOPATH`. (Replace *username* with your actual user name. And no, you cannot use the tilde `~` shortcut.) + +`$ GOPATH="/home/username/go"` + +Build passthrough example. + +First, edit `passthrough.go` and `midipassthrough.go` to `import "github.com/andrepxx/go-jack"` instead of `import "github.com/xthexder/go-jack"`, then try to build them. + +``` +$ cd example/ +$ make +``` + +It will complain, that it cannot find the include `jack/jack.h`. + +Copy the `jack` folder from `C:\Program Files (x86)\Jack\includes` into `/x86_64-w64-mingw32/include` inside your *MinGW root directory*. + +Copy the contents of `C:\Program Files (x86)\Jack\lib` into `/x86_64-w64-mingw32/lib` inside your *MinGW root directory*. + +Now, run `make` again. + +`$ make` + +This time, the code should build successfully. Open up *Jack Control* (from the start menu), "Start" your JACK server, then go back to your *MinGW shell*. + +`$ ./passthrough.exe` + +**And it works!** + +*Go Passthrough* will appear as a *JACK client* in the *Connection* window and pass audio signals through. :-) + +Have fun! :-) + +(I said it's painful, but it works. :-) ) + diff --git a/doc/packages.txt b/doc/packages.txt new file mode 100644 index 0000000..d9f2b7c --- /dev/null +++ b/doc/packages.txt @@ -0,0 +1,26 @@ +Commonly required packages for building go-dsp-guitar +----------------------------------------------------- + +gcc-aarch64-linux-gnu +gcc-arm-linux-gnu +gcc-x86_64-linux +git +glibc-arm-linux-gnu +glibc-devel.i686 +glibc-devel.x86_64 +glibc-headers.i686 +glibc-headers.x86_64 +golang-bin (Fedora / RHEL) +golang-go (Debian / Ubuntu) +jack-audio-connection-kit (Fedora / RHEL) +jack-audio-connection-kit-devel (Fedora / RHEL) +libjack-jackd2-dev (Debian / Ubuntu) +mingw32-gcc +mingw32-gcc-c++ +mingw32-pkg-config +mingw64-gcc +mingw64-gcc-c++ +mingw64-pkg-config +openssl +rsync + diff --git a/doc/riff-64.txt b/doc/riff-64.txt new file mode 100644 index 0000000..a67388f --- /dev/null +++ b/doc/riff-64.txt @@ -0,0 +1,27 @@ +"RF64" = 52 46 36 34 = 0x34364652 +"ds64" = 64 73 36 34 = 0x34367364 + +struct ChunkRF64 { + chunkId uint32, // 0x34364652 = "RF64" + size uint32, // 0xffffffff (= MAX_UINT32) means "don't use" + type uint32, // "WAVE" +} + +struct ChunkDataSize64 { + chunkId uint32, // 0x34367364 = "ds64" + chunkSize uint32, // size of the "ds64" chunk (min. 36 bytes) + riffSize uint64, // size of the "RF64" chunk + dataSize uint64, // size of the "data" chunk + sampleCount uint64, // number of samples (per channel) in file + tableLength uint32, // number of table entries (each 12 bytes in size) to skip after "ds64" chunk - this should normally be zero! +} + +type dataSizeHeader struct { + ChunkID uint32 + ChunkSize uint32 + SizeRIFF uint64 + SizeData uint64 + SampleCount uint64 + TableLength uint32 +} + diff --git a/effects/autowah.go b/effects/autowah.go new file mode 100644 index 0000000..8ada619 --- /dev/null +++ b/effects/autowah.go @@ -0,0 +1,197 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing an auto wah effect. + */ +type autowah struct { + unitStruct + envelope float64 + highpassCapVoltages [NUM_FILTERS]float64 + lowpassCapVoltages [NUM_FILTERS]float64 +} + +/* + * Auto wah audio processing. + */ +func (this *autowah) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + follow, _ := this.getDiscreteValue("follow") + levelA, _ := this.getNumericValue("level_1") + levelB, _ := this.getNumericValue("level_2") + frequencyA, _ := this.getNumericValue("frequency_1") + frequencyB, _ := this.getNumericValue("frequency_2") + this.mutex.RUnlock() + + /* + * If the first level is higher than the second, swap both levels and frequencies around. + */ + if levelA > levelB { + levelA, levelB = levelB, levelA + frequencyA, frequencyB = frequencyB, frequencyA + } + + levelAFloat := float64(levelA) + levelBFloat := float64(levelB) + frequencyAFloat := float64(frequencyA) + frequencyBFloat := float64(frequencyB) + frequencySlope := (frequencyBFloat - frequencyAFloat) / (levelBFloat - levelAFloat) + sampleRateFloat := float64(sampleRate) + dischargePerSampleEnvelopeArg := -20.0 / sampleRateFloat + dischargePerSampleEnvelopeInv := math.Exp(dischargePerSampleEnvelopeArg) + dischargePerSampleEnvelope := 1.0 - dischargePerSampleEnvelopeInv + envelope := this.envelope + hcvs := this.highpassCapVoltages + lcvs := this.lowpassCapVoltages + gainCompensation := math.Pow(2.0, NUM_FILTERS) + + /* + * Process each sample. + */ + for i, sample := range in { + sampleAbs := math.Abs(sample) + + /* + * Follow either level or envelope. + */ + switch follow { + case "envelope": + envelope *= dischargePerSampleEnvelopeInv + + /* + * If the absolute value of the current sample exceeds the + * current envelope value, make it the new envelope value. + */ + if sampleAbs > envelope { + envelope = sampleAbs + } + + case "level": + diff := sampleAbs - envelope + envelope += diff * dischargePerSampleEnvelope + default: + envelope = 1.0 + } + + level := factorToDecibels(envelope) + frequency := 0.0 + + /* + * Calculate the current limit frequency of the filter as a piecewise + * linear function of the signal level. + */ + if level <= levelAFloat { + frequency = frequencyAFloat + } else if level >= levelBFloat { + frequency = frequencyBFloat + } else { + excess := level - levelAFloat + frequency = frequencyAFloat + (frequencySlope * excess) + } + + arg := -frequency / sampleRateFloat + dischargePerSampleInv := 1.0 - math.Exp(arg) + lcv := sample + + /* + * Evaluate the response of all filters. + */ + for j := 0; j < NUM_FILTERS; j++ { + hcv := hcvs[j] + diff := lcv - hcv + hcv += diff * dischargePerSampleInv + hcvs[j] = hcv + lcv = lcvs[j] + diff -= lcv + lcv += diff * dischargePerSampleInv + lcvs[j] = lcv + } + + pre := gainCompensation * lcv + + /* + * Limit the output signal to the appropriate range. + */ + if pre < -1.0 { + pre = -1.0 + } else if pre > 1.0 { + pre = 1.0 + } + + out[i] = pre + } + + this.envelope = envelope + this.highpassCapVoltages = hcvs + this.lowpassCapVoltages = lcvs +} + +/* + * Create an auto-wah effects unit. + */ +func createAutoWah() Unit { + + /* + * Create effects unit. + */ + u := autowah{ + unitStruct: unitStruct{ + unitType: UNIT_AUTOWAH, + params: []Parameter{ + Parameter{ + Name: "follow", + Type: PARAMETER_TYPE_DISCRETE, + Minimum: -1, + Maximum: -1, + NumericValue: -1, + DiscreteValueIndex: 1, + DiscreteValues: []string{ + "envelope", + "level", + }, + }, + Parameter{ + Name: "level_1", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -40, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level_2", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -10, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "frequency_1", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 20000, + NumericValue: 300, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "frequency_2", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 20000, + NumericValue: 6000, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/bandpass.go b/effects/bandpass.go new file mode 100644 index 0000000..c4bca05 --- /dev/null +++ b/effects/bandpass.go @@ -0,0 +1,149 @@ +package effects + +import ( + "math" + "strconv" +) + +/* + * Data structure representing a bandpass effect. + */ +type bandpass struct { + unitStruct + highpassCapVoltages []float64 + lowpassCapVoltages []float64 +} + +/* + * Bandpass audio processing. + */ +func (this *bandpass) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + filterOrderString, _ := this.getDiscreteValue("filter_order") + frequencyA, _ := this.getNumericValue("frequency_1") + frequencyB, _ := this.getNumericValue("frequency_2") + this.mutex.RUnlock() + filterOrder, _ := strconv.ParseUint(filterOrderString, 10, 32) + halfOrderUint := filterOrder >> 1 + halfOrder := int(halfOrderUint) + + /* + * If the first frequency is higher than the second, swap them around. + */ + if frequencyA > frequencyB { + frequencyA, frequencyB = frequencyB, frequencyA + } + + /* + * Allocate storage for highpass capacitor voltages if needed. + */ + if len(this.highpassCapVoltages) != halfOrder { + this.highpassCapVoltages = make([]float64, halfOrder) + } + + /* + * Allocate storage for lowpass capacitor voltages if needed. + */ + if len(this.lowpassCapVoltages) != halfOrder { + this.lowpassCapVoltages = make([]float64, halfOrder) + } + + sampleRateFloat := float64(sampleRate) + minusTwoPiOverSampleRate := -MATH_TWO_PI / sampleRateFloat + frequencyAFloat := float64(frequencyA) + frequencyBFloat := float64(frequencyB) + dischargePerSampleHPArg := minusTwoPiOverSampleRate * frequencyAFloat + dischargePerSampleHP := math.Exp(dischargePerSampleHPArg) + dischargePerSampleHPInv := 1.0 - dischargePerSampleHP + dischargePerSampleLPArg := minusTwoPiOverSampleRate * frequencyBFloat + dischargePerSampleLP := math.Exp(dischargePerSampleLPArg) + dischargePerSampleLPInv := 1.0 - dischargePerSampleLP + + /* + * Process each sample. + */ + for i, sample := range in { + pre := sample + + /* + * Filter as many times as required by the filter order. + */ + for j := 0; j < halfOrder; j++ { + hcv := this.highpassCapVoltages[j] + diff := pre - hcv + hcv += diff * dischargePerSampleHPInv + this.highpassCapVoltages[j] = hcv + lcv := this.lowpassCapVoltages[j] + diff -= lcv + iv := lcv + lcv += diff * dischargePerSampleLPInv + this.lowpassCapVoltages[j] = lcv + + /* + * Limit the output signal to the appropriate range. + */ + if iv < -1.0 { + pre = -1.0 + } else if iv > 1.0 { + pre = 1.0 + } else { + pre = iv + } + + } + + out[i] = pre + } + +} + +/* + * Create a bandpass effects unit. + */ +func createBandpass() Unit { + + /* + * Create effects unit. + */ + u := bandpass{ + unitStruct: unitStruct{ + unitType: UNIT_BANDPASS, + params: []Parameter{ + Parameter{ + Name: "filter_order", + Type: PARAMETER_TYPE_DISCRETE, + Minimum: -1, + Maximum: -1, + NumericValue: -1, + DiscreteValueIndex: 0, + DiscreteValues: []string{ + "2", + "4", + "6", + "8", + }, + }, + Parameter{ + Name: "frequency_1", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 20000, + NumericValue: 300, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "frequency_2", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 20000, + NumericValue: 3000, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/chorus.go b/effects/chorus.go new file mode 100644 index 0000000..cb6cb6e --- /dev/null +++ b/effects/chorus.go @@ -0,0 +1,167 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing a chorus effect. + */ +type chorus struct { + unitStruct + buffer []float64 + previousPhase float64 +} + +/* + * Chorus audio processing. + */ +func (this *chorus) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + depth, _ := this.getNumericValue("depth") + speed, _ := this.getNumericValue("speed") + this.mutex.RUnlock() + depthFloat := 0.1 * float64(depth) + + /* + * Limit depth to [0.0; 10.0]. + */ + if depthFloat < 0.0 { + depthFloat = 0.0 + } else if depthFloat > 10.0 { + depthFloat = 10.0 + } + + speedFloat := float64(speed) + angularSpeed := MATH_PI_THOUSANDTH * speedFloat + sampleRateFloat := float64(sampleRate) + maxDelaySamplesFloat := math.Floor((0.05 * sampleRateFloat) + 0.5) + maxDelaySamples := int(maxDelaySamplesFloat) + bufferSize := len(this.buffer) + previousPhase := this.previousPhase + + /* + * Make sure the buffer has the appropriate size. + */ + if bufferSize != maxDelaySamples { + this.buffer = make([]float64, maxDelaySamples) + bufferSize = maxDelaySamples + } + + /* + * Process each sample. + */ + for i, sample := range in { + iFloat := float64(i) + time := iFloat / sampleRateFloat + phaseChange := angularSpeed * time + phaseChanged := previousPhase + phaseChange + zeroPhase := math.Mod(phaseChanged, MATH_TWO_PI) + effectedSample := 0.0 + + /* + * Generate five sub-signals. + */ + for j := 0; j < 5; j++ { + jFloat := float64(j) + phaseOffset := MATH_TWO_PI_FIFTH * jFloat + updatedPhase := zeroPhase + phaseOffset + phase := math.Mod(updatedPhase, MATH_TWO_PI) + offset := depthFloat * math.Sin(phase) + currentDelayTime := 0.001 * (40.0 + offset) + currentDelaySamples := currentDelayTime * sampleRateFloat + currentDelaySamplesEarly := math.Floor(currentDelaySamples) + currentDelaySamplesEarlyInt := int(currentDelaySamplesEarly) + currentDelaySamplesLate := math.Ceil(currentDelaySamples) + currentDelaySamplesLateInt := int(currentDelaySamplesLate) + delayedIdxEarly := i - currentDelaySamplesEarlyInt + delayedIdxLate := i - currentDelaySamplesLateInt + delayedSampleEarly := float64(0.0) + delayedSampleLate := float64(0.0) + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdxEarly >= 0 { + delayedSampleEarly = in[delayedIdxEarly] + } else { + bufferPtr := bufferSize + delayedIdxEarly + delayedSampleEarly = this.buffer[bufferPtr] + } + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdxLate >= 0 { + delayedSampleLate = in[delayedIdxLate] + } else { + bufferPtr := bufferSize + delayedIdxLate + delayedSampleLate = this.buffer[bufferPtr] + } + + weightEarly := 1.0 - (currentDelaySamples - currentDelaySamplesEarly) + weightLate := 1.0 - (currentDelaySamplesLate - currentDelaySamples) + effectedSample += 0.2 * ((weightEarly * delayedSampleEarly) + (weightLate * delayedSampleLate)) + } + + out[i] = (0.5 * sample) + (0.5 * effectedSample) + } + + bufferSizeFloat := float64(bufferSize) + bufferTime := bufferSizeFloat / sampleRateFloat + phaseChange := angularSpeed * bufferTime + phaseChanged := previousPhase + phaseChange + this.previousPhase = math.Mod(phaseChanged, MATH_TWO_PI) + numSamples := len(in) + boundary := bufferSize - numSamples + + /* + * Check whether our buffer is larger than the number of samples processed. + */ + if boundary >= 0 { + copy(this.buffer[0:boundary], this.buffer[numSamples:bufferSize]) + copy(this.buffer[boundary:bufferSize], in) + } else { + copy(this.buffer, in[-boundary:numSamples]) + } + +} + +/* + * Create a chorus effects unit. + */ +func createChorus() Unit { + + /* + * Create effects unit. + */ + u := chorus{ + unitStruct: unitStruct{ + unitType: UNIT_CHORUS, + params: []Parameter{ + Parameter{ + Name: "depth", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "speed", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 100, + NumericValue: 30, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/delay.go b/effects/delay.go new file mode 100644 index 0000000..c87359d --- /dev/null +++ b/effects/delay.go @@ -0,0 +1,133 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing a delay effect. + */ +type delay struct { + unitStruct + buffer []float64 +} + +/* + * Delay audio processing. + */ +func (this *delay) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + delayTime, _ := this.getNumericValue("delay_time") + feedback, _ := this.getNumericValue("feedback") + level, _ := this.getNumericValue("level") + this.mutex.RUnlock() + delayTimeFloat := float64(delayTime) + delayTimeSeconds := 0.001 * delayTimeFloat + sampleRateFloat := float64(sampleRate) + delaySamplesFloat := math.Floor((delayTimeSeconds * sampleRateFloat) + 0.5) + delaySamples := int(delaySamplesFloat) + feedbackFactor := decibelsToFactor(feedback) + levelFactor := decibelsToFactor(level) + bufferSize := len(this.buffer) + + /* + * Make sure the buffer has the appropriate size. + */ + if bufferSize != delaySamples { + this.buffer = make([]float64, delaySamples) + bufferSize = delaySamples + } + + /* + * Mix the straight output with the delayed signal. + */ + for i, sample := range in { + delayedIdx := i - delaySamples + delayedSample := float64(0.0) + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdx >= 0 { + delayedSample = in[delayedIdx] + } else { + bufferPtr := bufferSize + delayedIdx + delayedSample = this.buffer[bufferPtr] + } + + pre := levelFactor * (sample + (feedbackFactor * delayedSample)) + + /* + * Limit the output signal to the appropriate range. + */ + if pre < -1.0 { + out[i] = -1.0 + } else if pre > 1.0 { + out[i] = 1.0 + } else { + out[i] = pre + } + + } + + numSamples := len(in) + boundary := bufferSize - numSamples + + /* + * Check whether our buffer is larger than the number of samples processed. + */ + if boundary >= 0 { + copy(this.buffer[0:boundary], this.buffer[numSamples:bufferSize]) + copy(this.buffer[boundary:bufferSize], in) + } else { + copy(this.buffer, in[-boundary:numSamples]) + } + +} + +/* + * Create a delay effects unit. + */ +func createDelay() Unit { + + /* + * Create effects unit. + */ + u := delay{ + unitStruct: unitStruct{ + unitType: UNIT_DELAY, + params: []Parameter{ + Parameter{ + Name: "delay_time", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 1000, + NumericValue: 200, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "feedback", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -5, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: -5, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/distortion.go b/effects/distortion.go new file mode 100644 index 0000000..56adfc1 --- /dev/null +++ b/effects/distortion.go @@ -0,0 +1,87 @@ +package effects + +/* + * Data structure representing a distortion effect. + */ +type distortion struct { + unitStruct +} + +/* + * Distortion audio processing. + */ +func (this *distortion) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + boost, _ := this.getNumericValue("boost") + gain, _ := this.getNumericValue("gain") + level, _ := this.getNumericValue("level") + this.mutex.RUnlock() + totalGain := boost + gain + gainFactor := decibelsToFactor(totalGain) + levelFactor := decibelsToFactor(level) + + /* + * Process each sample. + */ + for i, sample := range in { + pre := gainFactor * sample + + /* + * Limit the output signal to the appropriate range. + */ + if pre < -1.0 { + pre = -1.0 + } else if pre > 1.0 { + pre = 1.0 + } + + out[i] = levelFactor * pre + } + +} + +/* + * Create a distortion effects unit. + */ +func createDistortion() Unit { + + /* + * Create effects unit. + */ + u := distortion{ + unitStruct: unitStruct{ + unitType: UNIT_DISTORTION, + params: []Parameter{ + Parameter{ + Name: "boost", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 30, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "gain", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 30, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/effects.go b/effects/effects.go new file mode 100644 index 0000000..5df573a --- /dev/null +++ b/effects/effects.go @@ -0,0 +1,555 @@ +package effects + +import ( + "fmt" + "github.com/andrepxx/go-dsp-guitar/filter" + "math" + "sync" +) + +/* + * Parameter types. + */ +const ( + PARAMETER_TYPE_INVALID = iota + PARAMETER_TYPE_DISCRETE + PARAMETER_TYPE_NUMERIC +) + +/* + * Effect unit types. + */ +const ( + UNIT_SIGNALGENERATOR = iota + UNIT_NOISEGATE + UNIT_BANDPASS + UNIT_AUTOWAH + UNIT_OCTAVER + UNIT_EXCESS + UNIT_FUZZ + UNIT_OVERDRIVE + UNIT_DISTORTION + UNIT_TONESTACK + UNIT_CHORUS + UNIT_FLANGER + UNIT_PHASER + UNIT_TREMOLO + UNIT_RINGMODULATOR + UNIT_DELAY + UNIT_POWERAMP +) + +/* + * Mathematical constants. + */ +const ( + MATH_DEGREE_TO_RADIANS = math.Pi / 180.0 + MATH_PI_THOUSANDTH = 0.001 * math.Pi + MATH_TWO_OVER_PI = 2.0 / math.Pi + MATH_TWO_PI = 2.0 * math.Pi + MATH_TWO_PI_FIFTH = 0.4 * math.Pi + MATH_TWO_PI_HUNDREDTH = 0.02 * math.Pi +) + +/* + * Other constants. + */ +const ( + NUM_FILTERS = 8 + STRING_NONE = "- NONE -" +) + +/* + * Data structure representing the result of a filter compilation. + */ +type compilationResult struct { + id uint64 + result filter.Filter +} + +/* + * Data structure representing a parameter for an effects unit. + */ +type Parameter struct { + Name string + Type int32 + Minimum int32 + Maximum int32 + NumericValue int32 + DiscreteValueIndex int + DiscreteValues []string +} + +/* + * Interface type for an effects unit. + */ +type Unit interface { + Parameters() []Parameter + Process(in []float64, out []float64, sampleRate uint32) + Type() int + SetDiscreteValue(name string, value string) error + GetDiscreteValue(name string) (string, error) + SetNumericValue(name string, value int32) error + GetNumericValue(name string) (int32, error) +} + +/* + * Data structure representing a generic effects unit. + */ +type unitStruct struct { + unitType int + mutex sync.RWMutex + params []Parameter +} + +/* + * Returns the parameters of an effects unit. + */ +func (this *unitStruct) parameters() []Parameter { + n := len(this.params) + params := make([]Parameter, n) + copy(params, this.params) + + /* + * Copy the discrete value slices. + */ + for i, param := range params { + values := param.DiscreteValues + k := len(values) + valuesCopy := make([]string, k) + copy(valuesCopy, values) + params[i].DiscreteValues = valuesCopy + } + + return params +} + +/* + * Returns the parameters of an effects unit. + */ +func (this *unitStruct) Parameters() []Parameter { + this.mutex.RLock() + params := this.parameters() + this.mutex.RUnlock() + return params +} + +/* + * Returns the type of this effects unit. + */ +func (this *unitStruct) Type() int { + return this.unitType +} + +/* + * Sets a discrete parameter value for an effects unit. + */ +func (this *unitStruct) setDiscreteValue(name string, value string) error { + idx := int(-1) + + /* + * Iterate over all parameters. + */ + for i, param := range this.params { + + /* + * If we got the right one, store its index. + */ + if param.Name == name { + idx = i + } + + } + + /* + * Check if parameter was found. + */ + if idx == -1 { + return fmt.Errorf("Failed to set discrete value: Could not find parameter with name '%s'.", name) + } else { + param := this.params[idx] + + /* + * Check if parameter is discrete. + */ + if param.Type != PARAMETER_TYPE_DISCRETE { + return fmt.Errorf("Failed to set discrete value: Parameter '%s' is not discrete.", name) + } else { + values := param.DiscreteValues + valIdx := int(-1) + + /* + * Iterate over all values. + */ + for i, currentValue := range values { + + /* + * If we got the right one, store its index. + */ + if currentValue == value { + valIdx = i + } + + } + + /* + * Check if discrete value was found. + */ + if valIdx == -1 { + return fmt.Errorf("Failed to set discrete value: Value '%s' is not valid for parameter '%s'.", value, name) + } else { + this.params[idx].DiscreteValueIndex = valIdx + return nil + } + + } + + } + +} + +/* + * Sets a discrete parameter value for an effects unit. + */ +func (this *unitStruct) SetDiscreteValue(name string, value string) error { + this.mutex.Lock() + err := this.setDiscreteValue(name, value) + this.mutex.Unlock() + return err +} + +/* + * Gets a discrete parameter value from an effects unit. + */ +func (this *unitStruct) getDiscreteValue(name string) (string, error) { + idx := int(-1) + + /* + * Iterate over all parameters. + */ + for i, param := range this.params { + + /* + * If we got the right one, store its index. + */ + if param.Name == name { + idx = i + } + + } + + /* + * Check if parameter was found. + */ + if idx == -1 { + return "", fmt.Errorf("Failed to get discrete value: Could not find parameter with name '%s'.", name) + } else { + param := this.params[idx] + + /* + * Check if parameter is discrete. + */ + if param.Type != PARAMETER_TYPE_DISCRETE { + return "", fmt.Errorf("Failed to get discrete value: Parameter '%s' is not discrete.", name) + } else { + valIdx := param.DiscreteValueIndex + value := param.DiscreteValues[valIdx] + return value, nil + } + + } + +} + +/* + * Gets a discrete parameter value from an effects unit. + */ +func (this *unitStruct) GetDiscreteValue(name string) (string, error) { + this.mutex.RLock() + val, err := this.getDiscreteValue(name) + this.mutex.RUnlock() + return val, err +} + +/* + * Sets a numeric parameter value for an effects unit. + */ +func (this *unitStruct) setNumericValue(name string, value int32) error { + idx := int(-1) + + /* + * Iterate over all parameters. + */ + for i, param := range this.params { + + /* + * If we got the right one, store its index. + */ + if param.Name == name { + idx = i + } + + } + + /* + * Check if parameter was found. + */ + if idx == -1 { + return fmt.Errorf("Failed to set numeric value: Could not find parameter with name '%s'.", name) + } else { + param := this.params[idx] + + /* + * Check if parameter is numeric. + */ + if param.Type != PARAMETER_TYPE_NUMERIC { + return fmt.Errorf("Failed to set numeric value: Parameter '%s' is not numeric.", name) + } else { + min := param.Minimum + max := param.Maximum + + /* + * Check if value is out of range. + */ + if (value < min) || (value > max) { + return fmt.Errorf("Failed to set numeric value: Parameter '%s' must be between '%d' and '%d' - got '%d'.", name, min, max, value) + } else { + this.params[idx].NumericValue = value + return nil + } + + } + + } + +} + +/* + * Sets a numeric parameter value for an effects unit. + */ +func (this *unitStruct) SetNumericValue(name string, value int32) error { + this.mutex.Lock() + err := this.setNumericValue(name, value) + this.mutex.Unlock() + return err +} + +/* + * Gets a numeric parameter value from an effects unit. + */ +func (this *unitStruct) getNumericValue(name string) (int32, error) { + idx := int(-1) + + /* + * Iterate over all parameters. + */ + for i, param := range this.params { + + /* + * If we got the right one, store its index. + */ + if param.Name == name { + idx = i + } + + } + + /* + * Check if parameter was found. + */ + if idx == -1 { + return 0, fmt.Errorf("Failed to get numeric value: Could not find parameter with name '%s'.", name) + } else { + param := this.params[idx] + + /* + * Check if parameter is numeric. + */ + if param.Type != PARAMETER_TYPE_NUMERIC { + return 0, fmt.Errorf("Failed to get numeric value: Parameter '%s' is not numeric.", name) + } else { + val := param.NumericValue + return val, nil + } + + } + +} + +/* + * Gets a numeric parameter value from an effects unit. + */ +func (this *unitStruct) GetNumericValue(name string) (int32, error) { + this.mutex.RLock() + val, err := this.getNumericValue(name) + this.mutex.RUnlock() + return val, err +} + +/* + * Turn gain (or attenuation) in decibels into a (linear) factor. + */ +func decibelsToFactor(decibels int32) float64 { + decibelsFloat := float64(decibels) + exp := 0.05 * decibelsFloat + result := math.Pow(10.0, exp) + return result +} + +/* + * Turn a linear factor into a gain (or attenuation) value in decibels. + */ +func factorToDecibels(factor float64) float64 { + result := 20.0 * math.Log10(factor) + return result +} + +/* + * Returns the sign of an integer. + */ +func signInt(number int32) float64 { + + /* + * Check, whether number is negative, positive or zero. + */ + if number < 0 { + return -1.0 + } else if number > 0 { + return 1.0 + } else { + return 0 + } + +} + +/* + * Returns the sign of a floating-point number. + */ +func signFloat(number float64) float64 { + + /* + * Check, whether number is negative, positive or zero. + */ + if number < 0.0 { + return -1.0 + } else if number > 0.0 { + return 1.0 + } else { + return 0.0 + } + +} + +/* + * Create a new effects unit. + */ +func CreateUnit(unitType int) Unit { + + /* + * Lookup, which effect unit to create. + */ + switch unitType { + case UNIT_SIGNALGENERATOR: + u := createSignalGenerator() + return u + case UNIT_NOISEGATE: + u := createNoiseGate() + return u + case UNIT_BANDPASS: + u := createBandpass() + return u + case UNIT_AUTOWAH: + u := createAutoWah() + return u + case UNIT_OCTAVER: + u := createOctaver() + return u + case UNIT_EXCESS: + u := createExcess() + return u + case UNIT_FUZZ: + u := createFuzz() + return u + case UNIT_OVERDRIVE: + u := createOverdrive() + return u + case UNIT_DISTORTION: + u := createDistortion() + return u + case UNIT_TONESTACK: + u := createToneStack() + return u + case UNIT_CHORUS: + u := createChorus() + return u + case UNIT_FLANGER: + u := createFlanger() + return u + case UNIT_PHASER: + u := createPhaser() + return u + case UNIT_TREMOLO: + u := createTremolo() + return u + case UNIT_RINGMODULATOR: + u := createRingModulator() + return u + case UNIT_DELAY: + u := createDelay() + return u + case UNIT_POWERAMP: + u := createPowerAmp() + return u + default: + return nil + } + +} + +/* + * Returns a list of supported parameter types. + */ +func ParameterTypes() []string { + + /* + * List of all supported parameter types. + */ + paramTypes := []string{ + "invalid", + "discrete", + "numeric", + } + + return paramTypes +} + +/* + * Returns a list of supported unit types. + */ +func UnitTypes() []string { + + /* + * List of all supported unit types. + */ + unitTypes := []string{ + "signal_generator", + "noise_gate", + "bandpass", + "auto_wah", + "octaver", + "excess", + "fuzz", + "overdrive", + "distortion", + "tone_stack", + "chorus", + "flanger", + "phaser", + "tremolo", + "ring_modulator", + "delay", + "power_amp", + } + + return unitTypes +} diff --git a/effects/excess.go b/effects/excess.go new file mode 100644 index 0000000..c9b6a7e --- /dev/null +++ b/effects/excess.go @@ -0,0 +1,98 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing an excess effect. + */ +type excess struct { + unitStruct +} + +/* + * Excess audio processing. + */ +func (this *excess) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + gain, _ := this.getNumericValue("gain") + level, _ := this.getNumericValue("level") + this.mutex.RUnlock() + gainFactor := decibelsToFactor(gain) + levelFactor := decibelsToFactor(level) + + /* + * Process each sample. + */ + for i, sample := range in { + pre := gainFactor * sample + absPre := math.Abs(pre) + exceeded := absPre > 1.0 + negative := pre < 0.0 + absPreBiased := absPre + 1.0 + preBiasedFloor := math.Floor(absPreBiased) + section := int32(0.5 * preBiasedFloor) + sectionLSB := section % 2 + sectionOdd := sectionLSB != 0 + inverted := sectionOdd != (exceeded && negative) + absPreInc := absPre + 1.0 + excess := math.Mod(absPreInc, 2.0) + + /* + * Check if range has been exceeded. + */ + if exceeded { + + /* + * Decide, whether we're in range, go from top to bottom or from bottom to top. + */ + if inverted { + pre = 1.0 - excess + } else { + pre = excess - 1.0 + } + + } + + out[i] = levelFactor * pre + } + +} + +/* + * Create an excess effects unit. + */ +func createExcess() Unit { + + /* + * Create effects unit. + */ + u := excess{ + unitStruct: unitStruct{ + unitType: UNIT_EXCESS, + params: []Parameter{ + Parameter{ + Name: "gain", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 30, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/flanger.go b/effects/flanger.go new file mode 100644 index 0000000..c26545e --- /dev/null +++ b/effects/flanger.go @@ -0,0 +1,153 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing a flanger effect. + */ +type flanger struct { + unitStruct + buffer []float64 + previousPhase float64 +} + +/* + * Flanger audio processing. + */ +func (this *flanger) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + depth, _ := this.getNumericValue("depth") + speed, _ := this.getNumericValue("speed") + this.mutex.RUnlock() + depthFloat := 0.01 * float64(depth) + + /* + * Limit depth to [0.0; 1.0]. + */ + if depthFloat < 0.0 { + depthFloat = 0.0 + } else if depthFloat > 1.0 { + depthFloat = 1.0 + } + + speedFloat := float64(speed) + angularSpeed := MATH_TWO_PI_HUNDREDTH * speedFloat + sampleRateFloat := float64(sampleRate) + sampleRateFloatInv := 1.0 / sampleRateFloat + maxDelaySamplesFloat := math.Floor((0.002 * sampleRateFloat) + 0.5) + maxDelaySamples := int(maxDelaySamplesFloat) + bufferSize := len(this.buffer) + previousPhase := this.previousPhase + + /* + * Make sure the buffer has the appropriate size. + */ + if bufferSize != maxDelaySamples { + this.buffer = make([]float64, maxDelaySamples) + bufferSize = maxDelaySamples + } + + /* + * Mix the straight output with the delayed signal. + */ + for i, sample := range in { + iFloat := float64(i) + time := iFloat * sampleRateFloatInv + phaseChange := angularSpeed * time + phaseChanged := previousPhase + phaseChange + phase := math.Mod(phaseChanged, MATH_TWO_PI) + offset := depthFloat * math.Sin(phase) + currentDelayTime := 0.001 * (depthFloat + offset) + currentDelaySamples := currentDelayTime * sampleRateFloat + currentDelaySamplesEarly := math.Floor(currentDelaySamples) + currentDelaySamplesLate := math.Ceil(currentDelaySamples) + delayedIdxEarly := i - int(currentDelaySamplesEarly) + delayedIdxLate := i - int(currentDelaySamplesLate) + delayedSampleEarly := float64(0.0) + delayedSampleLate := float64(0.0) + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdxEarly >= 0 { + delayedSampleEarly = in[delayedIdxEarly] + } else { + bufferPtr := bufferSize + delayedIdxEarly + delayedSampleEarly = this.buffer[bufferPtr] + } + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdxLate >= 0 { + delayedSampleLate = in[delayedIdxLate] + } else { + bufferPtr := bufferSize + delayedIdxLate + delayedSampleLate = this.buffer[bufferPtr] + } + + weightEarly := 1.0 - (currentDelaySamples - currentDelaySamplesEarly) + weightLate := 1.0 - (currentDelaySamplesLate - currentDelaySamples) + delayedSample := (weightEarly * delayedSampleEarly) + (weightLate * delayedSampleLate) + out[i] = (0.5 * sample) + (0.5 * delayedSample) + } + + bufferSizeFloat := float64(bufferSize) + duration := bufferSizeFloat * sampleRateFloatInv + phaseIncrement := angularSpeed * duration + this.previousPhase = math.Mod(previousPhase+phaseIncrement, MATH_TWO_PI) + numSamples := len(in) + boundary := bufferSize - numSamples + + /* + * Check whether our buffer is larger than the number of samples processed. + */ + if boundary >= 0 { + copy(this.buffer[0:boundary], this.buffer[numSamples:bufferSize]) + copy(this.buffer[boundary:bufferSize], in) + } else { + copy(this.buffer, in[-boundary:numSamples]) + } + +} + +/* + * Create a flanger effects unit. + */ +func createFlanger() Unit { + + /* + * Create effects unit. + */ + u := flanger{ + unitStruct: unitStruct{ + unitType: UNIT_FLANGER, + params: []Parameter{ + Parameter{ + Name: "depth", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "speed", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 100, + NumericValue: 10, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/fuzz.go b/effects/fuzz.go new file mode 100644 index 0000000..6ee6d8b --- /dev/null +++ b/effects/fuzz.go @@ -0,0 +1,179 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing a fuzz effect. + */ +type fuzz struct { + unitStruct + envelope float64 + couplingCapacitorVoltage float64 +} + +/* + * Fuzz audio processing. + */ +func (this *fuzz) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + follow, _ := this.getDiscreteValue("follow") + bias, _ := this.getNumericValue("bias") + boost, _ := this.getNumericValue("boost") + gain, _ := this.getNumericValue("gain") + fuzz, _ := this.getNumericValue("fuzz") + level, _ := this.getNumericValue("level") + this.mutex.RUnlock() + biasFloat := float64(bias) + biasFactor := 0.01 * biasFloat + gainFactor := decibelsToFactor(boost + gain) + fuzzFloat := float64(fuzz) + fuzzFactor := 0.01 * fuzzFloat + fuzzFactorInv := 1.0 - fuzzFactor + levelFactor := decibelsToFactor(level) + envelope := this.envelope + couplingCapacitorVoltage := this.couplingCapacitorVoltage + sampleRateFloat := float64(sampleRate) + dischargePerSampleArg := -20.0 / sampleRateFloat + dischargePerSampleInv := math.Exp(dischargePerSampleArg) + dischargePerSample := 1.0 - dischargePerSampleInv + + /* + * Process each sample. + */ + for i, sample := range in { + sampleAbs := math.Abs(sample) + + /* + * Follow either level or envelope. + */ + switch follow { + case "envelope": + envelope *= dischargePerSampleInv + + /* + * If the absolute value of the current sample exceeds the + * current envelope value, make it the new envelope value. + */ + if sampleAbs > envelope { + envelope = sampleAbs + } + + case "level": + diff := sampleAbs - envelope + envelope += diff * dischargePerSample + default: + envelope = 1.0 + } + + biasVoltage := biasFactor * envelope + pre := gainFactor * (sample - biasVoltage) + + /* + * Clip the waveform. + */ + if pre < -1.0 { + pre = -1.0 + } else if pre > 1.0 { + pre = 1.0 + } + + fuzzFraction := fuzzFactor * pre + cleanFraction := fuzzFactorInv * sample + pre = fuzzFraction + cleanFraction + diff := pre - couplingCapacitorVoltage + couplingCapacitorVoltage += diff * dischargePerSample + pre -= couplingCapacitorVoltage + + /* + * Limit the signal to the appropriate range. + */ + if pre < -1.0 { + pre = -1.0 + } else if pre > 1.0 { + pre = 1.0 + } + + out[i] = levelFactor * pre + } + + this.envelope = envelope + this.couplingCapacitorVoltage = couplingCapacitorVoltage +} + +/* + * Create a fuzz effects unit. + */ +func createFuzz() Unit { + + /* + * Create effects unit. + */ + u := fuzz{ + unitStruct: unitStruct{ + unitType: UNIT_FUZZ, + params: []Parameter{ + Parameter{ + Name: "follow", + Type: PARAMETER_TYPE_DISCRETE, + Minimum: -1, + Maximum: -1, + NumericValue: -1, + DiscreteValueIndex: 1, + DiscreteValues: []string{ + "envelope", + "level", + }, + }, + Parameter{ + Name: "bias", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -100, + Maximum: 100, + NumericValue: 50, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "boost", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 30, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "gain", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 30, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "fuzz", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/noisegate.go b/effects/noisegate.go new file mode 100644 index 0000000..9a297c4 --- /dev/null +++ b/effects/noisegate.go @@ -0,0 +1,142 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing a noise gate effect. + */ +type noiseGate struct { + unitStruct + gateOpen bool + onHoldSince uint32 +} + +/* + * Noise gate audio processing. + */ +func (this *noiseGate) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + levelOpen, _ := this.getNumericValue("threshold_open") + levelClose, _ := this.getNumericValue("threshold_close") + holdTime, _ := this.getNumericValue("hold_time") + this.mutex.RUnlock() + facOpen := decibelsToFactor(levelOpen) + facClose := decibelsToFactor(levelClose) + + /* + * If opening threshold lies BELOW closing threshold, bypass the gate altogether, + * but still keep it open. + */ + if levelOpen < levelClose { + copy(out, in) + this.gateOpen = true + this.onHoldSince = 0 + } else { + holdTimeFloat := float64(holdTime) + holdTimeSeconds := 0.001 * holdTimeFloat + sampleRateFloat := float64(sampleRate) + holdSamplesFloat := math.Floor((holdTimeSeconds * sampleRateFloat) + 0.5) + holdSamples := uint32(holdSamplesFloat) + gateOpen := this.gateOpen + onHoldSince := this.onHoldSince + + /* + * Process each sample. + */ + for i, sample := range in { + amplitude := math.Abs(sample) + + /* + * Check if amplitude is above opening threshold. + */ + if amplitude > facOpen { + gateOpen = true + } + + /* + * Check if amplitude is above closing threshold. + */ + if amplitude > facClose { + onHoldSince = 0 + } + + /* + * If we're on hold for too long, close the gate. + */ + if onHoldSince >= holdSamples { + gateOpen = false + } + + fac := float64(0.0) + + /* + * Check if gate is open. + */ + if gateOpen { + fac = 1.0 + } + + out[i] = fac * sample + + /* + * Increment time on hold, unless it overflows. + */ + if onHoldSince < math.MaxUint32 { + onHoldSince++ + } + + } + + this.gateOpen = gateOpen + this.onHoldSince = onHoldSince + } + +} + +/* + * Create a noise gate effects unit. + */ +func createNoiseGate() Unit { + + /* + * Create effects unit. + */ + u := noiseGate{ + unitStruct: unitStruct{ + unitType: UNIT_NOISEGATE, + params: []Parameter{ + Parameter{ + Name: "threshold_open", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -20, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "threshold_close", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -40, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "hold_time", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 1000, + NumericValue: 50, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/octaver.go b/effects/octaver.go new file mode 100644 index 0000000..2fbcdc9 --- /dev/null +++ b/effects/octaver.go @@ -0,0 +1,224 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing an octaver effect. + */ +type octaver struct { + unitStruct + previousPolarity float64 + octaveRegister uint32 + envelope float64 + couplingCapacitorVoltage float64 +} + +/* + * Octaver audio processing. + */ +func (this *octaver) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + follow, _ := this.getDiscreteValue("follow") + levelOctaveUp, _ := this.getNumericValue("level_octave_up") + levelClean, _ := this.getNumericValue("level_clean") + levelDist, _ := this.getNumericValue("level_dist") + levelOctaveDownFirst, _ := this.getNumericValue("level_octave_down_first") + levelOctaveDownSecond, _ := this.getNumericValue("level_octave_down_second") + levelHysteresis, _ := this.getNumericValue("level_hysteresis") + this.mutex.RUnlock() + facOctaveUp := decibelsToFactor(levelOctaveUp) + facClean := decibelsToFactor(levelClean) + facDist := decibelsToFactor(levelDist) + facOctaveDownFirst := decibelsToFactor(levelOctaveDownFirst) + facOctaveDownSecond := decibelsToFactor(levelOctaveDownSecond) + facHysteresis := decibelsToFactor(levelHysteresis) + previousPolarity := this.previousPolarity + octaveRegister := this.octaveRegister + envelope := this.envelope + couplingCapacitorVoltage := this.couplingCapacitorVoltage + sampleRateFloat := float64(sampleRate) + dischargePerSampleArg := -20.0 / sampleRateFloat + dischargePerSampleInv := math.Exp(dischargePerSampleArg) + dischargePerSample := 1.0 - dischargePerSampleInv + + /* + * Process each sample. + */ + for i, sample := range in { + sampleAbs := math.Abs(sample) + + /* + * Follow either level or envelope. + */ + switch follow { + case "envelope": + envelope *= dischargePerSampleInv + + /* + * If the absolute value of the current sample exceeds the + * current envelope value, make it the new envelope value. + */ + if sampleAbs > envelope { + envelope = sampleAbs + } + + case "level": + diff := sampleAbs - envelope + envelope += diff * dischargePerSample + default: + envelope = 1.0 + } + + square := sample * sample + sign := signFloat(sample) + hysteresis := envelope * facHysteresis + + /* + * If signal changes polarity and is above the hysteresis, increment + * two-bit octave register. + */ + if (sign != 0.0) && (sign != previousPolarity) && (sampleAbs > hysteresis) { + octaveRegister = (octaveRegister + 1) & 0x7 + previousPolarity = sign + } + + firstDown := float64(1.0) + + /* + * Invert polarity of first octave down, depending on the contents of the + * octave register. + */ + if (octaveRegister & 0x2) != 0 { + firstDown = -1.0 + } + + secondDown := float64(1.0) + + /* + * Invert polarity of second octave down, depending on the contents of the + * octave register. + */ + if (octaveRegister & 0x4) != 0 { + secondDown = -1.0 + } + + pre := facClean * sample + + /* + * Check that envelope is not too small. + */ + if envelope > 0.0001 { + pre += facOctaveUp * (square / envelope) + } + + pre += facDist * (sign * envelope) + pre += facOctaveDownFirst * (firstDown * envelope) + pre += facOctaveDownSecond * (secondDown * envelope) + couplingCapacitorVoltage += (pre - couplingCapacitorVoltage) * dischargePerSample + pre -= couplingCapacitorVoltage + + /* + * Limit the output signal to the appropriate range. + */ + if pre < -1.0 { + out[i] = -1.0 + } else if pre > 1.0 { + out[i] = 1.0 + } else { + out[i] = pre + } + + } + + this.previousPolarity = previousPolarity + this.octaveRegister = octaveRegister + this.envelope = envelope + this.couplingCapacitorVoltage = couplingCapacitorVoltage +} + +/* + * Create an octaver effects unit. + */ +func createOctaver() Unit { + + /* + * Create effects unit. + */ + u := octaver{ + unitStruct: unitStruct{ + unitType: UNIT_OCTAVER, + params: []Parameter{ + Parameter{ + Name: "follow", + Type: PARAMETER_TYPE_DISCRETE, + Minimum: -1, + Maximum: -1, + NumericValue: -1, + DiscreteValueIndex: 1, + DiscreteValues: []string{ + "envelope", + "level", + }, + }, + Parameter{ + Name: "level_octave_up", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -20, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level_clean", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -20, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level_dist", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -20, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level_octave_down_first", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -20, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level_octave_down_second", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -20, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level_hysteresis", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -20, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/overdrive.go b/effects/overdrive.go new file mode 100644 index 0000000..6f519e3 --- /dev/null +++ b/effects/overdrive.go @@ -0,0 +1,97 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing an overdrive effect. + */ +type overdrive struct { + unitStruct +} + +/* + * Overdrive audio processing. + */ +func (this *overdrive) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + boost, _ := this.getNumericValue("boost") + gain, _ := this.getNumericValue("gain") + drive, _ := this.getNumericValue("drive") + level, _ := this.getNumericValue("level") + this.mutex.RUnlock() + totalGain := boost + gain + gainFactor := decibelsToFactor(totalGain) + driveFloat := float64(drive) + driveFactor := 0.01 * driveFloat + cleanFactor := 1.0 - driveFactor + levelFactor := decibelsToFactor(level) + + /* + * Process each sample. + */ + for i, sample := range in { + arg := -gainFactor * sample + x := math.Exp(arg) + dist := (2.0 / (1.0 + x)) - 1.0 + mix := (driveFactor * dist) + (cleanFactor * sample) + out[i] = levelFactor * mix + } + +} + +/* + * Create an overdrive effects unit. + */ +func createOverdrive() Unit { + + /* + * Create effects unit. + */ + u := overdrive{ + unitStruct: unitStruct{ + unitType: UNIT_OVERDRIVE, + params: []Parameter{ + Parameter{ + Name: "boost", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 30, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "gain", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 30, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "drive", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "level", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/phaser.go b/effects/phaser.go new file mode 100644 index 0000000..9f51716 --- /dev/null +++ b/effects/phaser.go @@ -0,0 +1,168 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing a phaser effect. + */ +type phaser struct { + unitStruct + buffer []float64 + previousPhase float64 +} + +/* + * Phaser audio processing. + */ +func (this *phaser) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + depth, _ := this.getNumericValue("depth") + speed, _ := this.getNumericValue("speed") + phase, _ := this.getNumericValue("phase") + this.mutex.RUnlock() + depthFloat := float64(depth) + depthValue := 0.001 * depthFloat + + /* + * Limit depth to [0.0; 1.0]. + */ + if depthValue < 0.0 { + depthValue = 0.0 + } else if depthValue > 1.0 { + depthValue = 1.0 + } + + speedFloat := float64(speed) + angularSpeed := MATH_TWO_PI_HUNDREDTH * speedFloat + phaseFloat := float64(phase) + phaseFloatRadians := MATH_DEGREE_TO_RADIANS * phaseFloat + phaseFac := 0.5 * math.Sin(phaseFloatRadians) + phaseFacInv := 1.0 - math.Abs(phaseFac) + sampleRateFloat := float64(sampleRate) + sampleRateFloatInv := 1.0 / sampleRateFloat + maxDelaySamplesFloat := math.Floor((0.002 * sampleRateFloat) + 0.5) + maxDelaySamples := int(maxDelaySamplesFloat) + bufferSize := len(this.buffer) + previousPhase := this.previousPhase + + /* + * Make sure the buffer has the appropriate size. + */ + if bufferSize != maxDelaySamples { + this.buffer = make([]float64, maxDelaySamples) + bufferSize = maxDelaySamples + } + + /* + * Mix the straight output with the delayed signal. + */ + for i, sample := range in { + iFloat := float64(i) + time := iFloat * sampleRateFloatInv + phaseChange := angularSpeed * time + phaseChanged := previousPhase + phaseChange + phase := math.Mod(phaseChanged, MATH_TWO_PI) + offset := depthValue * math.Sin(phase) + currentDelayTime := 0.001 * (depthValue + offset) + currentDelaySamples := currentDelayTime * sampleRateFloat + currentDelaySamplesEarly := math.Floor(currentDelaySamples) + currentDelaySamplesLate := math.Ceil(currentDelaySamples) + delayedIdxEarly := i - int(currentDelaySamplesEarly) + delayedIdxLate := i - int(currentDelaySamplesLate) + delayedSampleEarly := float64(0.0) + delayedSampleLate := float64(0.0) + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdxEarly >= 0 { + delayedSampleEarly = in[delayedIdxEarly] + } else { + bufferPtr := bufferSize + delayedIdxEarly + delayedSampleEarly = this.buffer[bufferPtr] + } + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdxLate >= 0 { + delayedSampleLate = in[delayedIdxLate] + } else { + bufferPtr := bufferSize + delayedIdxLate + delayedSampleLate = this.buffer[bufferPtr] + } + + weightEarly := 1.0 - (currentDelaySamples - currentDelaySamplesEarly) + weightLate := 1.0 - (currentDelaySamplesLate - currentDelaySamples) + delayedSample := (weightEarly * delayedSampleEarly) + (weightLate * delayedSampleLate) + out[i] = (phaseFacInv * sample) + (phaseFac * delayedSample) + } + + bufferSizeFloat := float64(bufferSize) + duration := bufferSizeFloat * sampleRateFloatInv + phaseIncrement := angularSpeed * duration + this.previousPhase = math.Mod(previousPhase+phaseIncrement, MATH_TWO_PI) + numSamples := len(in) + boundary := bufferSize - numSamples + + /* + * Check whether our buffer is larger than the number of samples processed. + */ + if boundary >= 0 { + copy(this.buffer[0:boundary], this.buffer[numSamples:bufferSize]) + copy(this.buffer[boundary:bufferSize], in) + } else { + copy(this.buffer, in[-boundary:numSamples]) + } + +} + +/* + * Create a phaser effects unit. + */ +func createPhaser() Unit { + + /* + * Create effects unit. + */ + u := phaser{ + unitStruct: unitStruct{ + unitType: UNIT_PHASER, + params: []Parameter{ + Parameter{ + Name: "depth", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "speed", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 100, + NumericValue: 10, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "phase", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -90, + Maximum: 90, + NumericValue: 45, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/poweramp.go b/effects/poweramp.go new file mode 100644 index 0000000..7b61d54 --- /dev/null +++ b/effects/poweramp.go @@ -0,0 +1,367 @@ +package effects + +import ( + "fmt" + "github.com/andrepxx/go-dsp-guitar/fft" + "github.com/andrepxx/go-dsp-guitar/filter" + "strconv" +) + +/* + * Data structure representing a power amplifier. + */ +type poweramp struct { + unitStruct + sampleRate uint32 + fltChannel chan compilationResult + impulseResponses filter.ImpulseResponses + idCompiled uint64 + idReceived uint64 + currentFilter filter.Filter +} + +/* + * Post compilation result into the channel. + */ +func (this *poweramp) postCompilationResult(result compilationResult) { + + /* + * Post result asynchronously. + */ + post := func(result compilationResult, c chan compilationResult) { + c <- result + } + + go post(result, this.fltChannel) +} + +/* + * Compile a new filter for this power amplifier. + */ +func (this *poweramp) compile(sampleRate uint32, id uint64) error { + irs := this.impulseResponses + + /* + * Verify that impulse responses are loaded. + */ + if irs == nil { + return fmt.Errorf("%s", "Could not compile filter: No impulse responses were loaded.") + } else { + targetOrder := uint32(0) + targetOrderString, err := this.getDiscreteValue("filter_order") + + /* + * Set target filter order. + */ + if err == nil { + targetOrder64, err := strconv.ParseUint(targetOrderString, 10, 32) + + /* + * Abort if error occured during parsing. + */ + if err != nil { + return fmt.Errorf("Could not parse filter target order: '%s'", targetOrderString) + } else { + targetOrder = uint32(targetOrder64) + } + + } + + filters := make([]filter.Filter, NUM_FILTERS) + + /* + * Populate each filter. + */ + for i := 0; i < NUM_FILTERS; i++ { + iInc := int64(i + 1) + sIdxInc := strconv.FormatInt(iInc, 10) + paramFilter := "filter_" + sIdxInc + paramLevel := "level_" + sIdxInc + name, errName := this.getDiscreteValue(paramFilter) + level, errLevel := this.getNumericValue(paramLevel) + + /* + * Check if an error occured. + */ + if errName != nil || errLevel != nil { + return fmt.Errorf("Error parsing values for filter %d.", i) + } else { + + /* + * Verify that this is actually a valid filter and not a dummy value. + */ + if name != STRING_NONE { + fac := decibelsToFactor(level) + flt := irs.CreateFilter(name, sampleRate) + + /* + * Check if filter was found. + */ + if flt == nil { + return fmt.Errorf("Failed to load filter '%s' for sample rate '%d'.", name, sampleRate) + } else { + + /* + * Check if target order makes sense. + */ + if targetOrder > 0 { + flt = flt.Reduce(targetOrder) + } + + flt = flt.Normalize() + flt = flt.Multiply(fac) + filters[i] = flt + } + + } + + } + + } + + fltComposite := filter.Empty(sampleRate) + + /* + * Add all other filters. + */ + for _, flt := range filters { + fltComposite, err = fltComposite.Add(flt) + + /* + * Check for errors. + */ + if err != nil { + return fmt.Errorf("Failed to add filter: %s", err.Error()) + } + + } + + /* + * Create compilation result. + */ + result := compilationResult{ + id: id, + result: fltComposite, + } + + this.postCompilationResult(result) + return nil + } + +} + +/* + * Sets a discrete parameter value for a power amplifier. + */ +func (this *poweramp) SetDiscreteValue(name string, value string) error { + this.mutex.Lock() + err := this.unitStruct.setDiscreteValue(name, value) + + /* + * If value was set, recompile filter. + */ + if err == nil { + sr := this.sampleRate + id := this.idCompiled + 1 + this.idCompiled = id + err = this.compile(sr, id) + } + + this.mutex.Unlock() + return err +} + +/* + * Sets a numeric parameter value for a power amplifier. + */ +func (this *poweramp) SetNumericValue(name string, value int32) error { + this.mutex.Lock() + err := this.unitStruct.setNumericValue(name, value) + + /* + * If value was set, recompile filter. + */ + if err == nil { + sr := this.sampleRate + id := this.idCompiled + 1 + this.idCompiled = id + err = this.compile(sr, id) + } + + this.mutex.Unlock() + return err +} + +/* + * Power amplifier audio processing. + */ +func (this *poweramp) Process(in []float64, out []float64, sampleRate uint32) { + + /* + * Check if sampling rate changed. + */ + if sampleRate != this.sampleRate { + this.sampleRate = sampleRate + sr := this.sampleRate + id := this.idCompiled + 1 + this.idCompiled = id + this.compile(sr, id) + } + + noFilter := false + + /* + * Do this as long as we have new filters in the queue. + */ + for !noFilter { + + /* + * Check if new filter has been compiled. + */ + select { + case r := <-this.fltChannel: + flt := r.result + id := r.id + + /* + * Accept this filter if it is newer than the newest I have. + */ + if id > this.idReceived { + this.currentFilter = flt + this.idReceived = id + } + + default: + noFilter = true + } + + } + + flt := this.currentFilter + + /* + * If there is a filter, put the signal through it, otherwise write zeros to output. + */ + if flt != nil { + flt.Process(in, out) + } else { + fft.ZeroFloat(out) + } + +} + +/* + * Populate the parameters of a power amplifier. + */ +func PreparePowerAmp(unit Unit, responses filter.ImpulseResponses) error { + isPowerAmp := false + + /* + * Check if unit is a power amplifier. + */ + switch unit.(type) { + case *poweramp: + isPowerAmp = true + } + + /* + * Check if the unit is a power amp. + */ + if !isPowerAmp { + return fmt.Errorf("%s", "Cannot prepare power amp: Unit is not a power amp.") + } else if responses == nil { + return fmt.Errorf("%s", "Cannot prepare power amp: Impulse responses are nil.") + } else { + amp := unit.(*poweramp) + names := responses.Names() + params := amp.unitStruct.params + + /* + * Create name and gain values for each filter. + */ + for i := 0; i < NUM_FILTERS; i++ { + iInc := int64(i + 1) + sIdxInc := strconv.FormatInt(iInc, 10) + namesExtended := []string{STRING_NONE} + namesExtended = append(namesExtended, names...) + + /* + * Parameter for power amp type. + */ + paramType := Parameter{ + Name: "filter_" + sIdxInc, + Type: PARAMETER_TYPE_DISCRETE, + Minimum: -1, + Maximum: -1, + NumericValue: -1, + DiscreteValueIndex: 0, + DiscreteValues: namesExtended, + } + + /* + * Parameter for power amp level. + */ + paramLevel := Parameter{ + Name: "level_" + sIdxInc, + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + } + + params = append(params, paramType, paramLevel) + } + + amp.unitStruct.params = params + amp.fltChannel = make(chan compilationResult) + amp.impulseResponses = responses + return nil + } + +} + +/* + * Create a power amp effects unit. + */ +func createPowerAmp() Unit { + + /* + * Create effects unit. + */ + u := poweramp{ + unitStruct: unitStruct{ + unitType: UNIT_POWERAMP, + params: []Parameter{ + Parameter{ + Name: "filter_order", + Type: PARAMETER_TYPE_DISCRETE, + Minimum: -1, + Maximum: -1, + NumericValue: -1, + DiscreteValueIndex: 14, + DiscreteValues: []string{ + "64", + "128", + "256", + "512", + "1024", + "2048", + "4096", + "8192", + "16384", + "32768", + "65536", + "131072", + "262144", + "524288", + "1048576", + }, + }, + }, + }, + } + + return &u +} diff --git a/effects/ringmodulator.go b/effects/ringmodulator.go new file mode 100644 index 0000000..7b9537c --- /dev/null +++ b/effects/ringmodulator.go @@ -0,0 +1,73 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing a ring modulator effect. + */ +type ringModulator struct { + unitStruct + phase float64 +} + +/* + * Ring modulator audio processing. + */ +func (this *ringModulator) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + frequency, _ := this.getNumericValue("frequency") + this.mutex.RUnlock() + phase := this.phase + sampleRateFloat := float64(sampleRate) + frequencyFloat := float64(frequency) + angularFrequency := MATH_TWO_PI * frequencyFloat + phaseFraction := angularFrequency / sampleRateFloat + + /* + * Process each sample. + */ + for i, sample := range in { + iFloat := float64(i) + phaseOffset := iFloat * phaseFraction + phaseUpdate := phase + phaseOffset + currentPhase := math.Mod(phaseUpdate, MATH_TWO_PI) + carrierWave := math.Sin(currentPhase) + out[i] = carrierWave * sample + } + + n := len(in) + nFloat := float64(n) + phaseOffset := nFloat * phaseFraction + phaseUpdate := phase + phaseOffset + this.phase = math.Mod(phaseUpdate, MATH_TWO_PI) +} + +/* + * Create a ring modulator effects unit. + */ +func createRingModulator() Unit { + + /* + * Create effects unit. + */ + u := ringModulator{ + unitStruct: unitStruct{ + unitType: UNIT_RINGMODULATOR, + params: []Parameter{ + Parameter{ + Name: "frequency", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/signalgenerator.go b/effects/signalgenerator.go new file mode 100644 index 0000000..996f678 --- /dev/null +++ b/effects/signalgenerator.go @@ -0,0 +1,232 @@ +package effects + +import ( + "github.com/andrepxx/go-dsp-guitar/random" + "math" +) + +/* + * Data structure representing a signal generator. + */ +type signalGenerator struct { + unitStruct + phase float64 + prng random.PseudoRandomNumberGenerator +} + +/* + * Signal generator audio processing. + */ +func (this *signalGenerator) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + inputAmplitude, _ := this.getNumericValue("input_amplitude") + inputGain, _ := this.getNumericValue("input_gain") + signalType, _ := this.getDiscreteValue("signal_type") + signalFrequency, _ := this.getNumericValue("signal_frequency") + signalAmplitude, _ := this.getNumericValue("signal_amplitude") + signalGain, _ := this.getNumericValue("signal_gain") + this.mutex.RUnlock() + inputAmplitudeFloat := float64(inputAmplitude) + facInputGain := decibelsToFactor(inputGain) + facInput := (0.01 * inputAmplitudeFloat) * facInputGain + facSignalGain := decibelsToFactor(signalGain) + signalAmplitudeFloat := float64(signalAmplitude) + facSignal := (0.01 * signalAmplitudeFloat) * facSignalGain + phase := this.phase + signalFrequencyFloat := float64(signalFrequency) + sampleRateFloat := float64(sampleRate) + phaseIncrement := MATH_TWO_PI * (signalFrequencyFloat / sampleRateFloat) + twoOverPi := 2.0 / math.Pi + n := len(in) + nFloat := float64(n) + + /* + * Generate the appropriate signal. + */ + switch signalType { + case "sine": + + /* + * Process each sample. + */ + for i, sample := range in { + iFloat := float64(i) + updatedPhase := phase + (iFloat * phaseIncrement) + currentPhase := math.Mod(updatedPhase, MATH_TWO_PI) + signal := math.Sin(currentPhase) + out[i] = (facInput * sample) + (facSignal * signal) + } + + phase += nFloat * phaseIncrement + phase = math.Mod(phase, MATH_TWO_PI) + break + case "triangle": + + /* + * Process each sample. + */ + for i, sample := range in { + iFloat := float64(i) + updatedPhase := phase + (iFloat * phaseIncrement) + currentPhase := math.Mod(updatedPhase, MATH_TWO_PI) + signal := 0.0 + + /* + * Check whether the waveform is rising or falling. + */ + if currentPhase < math.Pi { + signal = (twoOverPi * currentPhase) - 1.0 + } else { + signal = 3.0 - (twoOverPi * currentPhase) + } + + out[i] = (facInput * sample) + (facSignal * signal) + } + + phase += nFloat * phaseIncrement + phase = math.Mod(phase, MATH_TWO_PI) + break + case "square": + + /* + * Process each sample. + */ + for i, sample := range in { + iFloat := float64(i) + updatedPhase := phase + (iFloat * phaseIncrement) + currentPhase := math.Mod(updatedPhase, MATH_TWO_PI) + signal := signFloat(math.Pi - currentPhase) + out[i] = (facInput * sample) + (facSignal * signal) + } + + phase += nFloat * phaseIncrement + phase = math.Mod(phase, MATH_TWO_PI) + break + case "sawtooth": + + /* + * Process each sample. + */ + for i, sample := range in { + iFloat := float64(i) + updatedPhase := phase + (iFloat * phaseIncrement) + currentPhase := math.Mod(updatedPhase, MATH_TWO_PI) + signal := currentPhase / math.Pi + + /* + * Check whether we're after the phase jump. + */ + if currentPhase > math.Pi { + signal -= 2.0 + } + + out[i] = (facInput * sample) + (facSignal * signal) + } + + phase += nFloat * phaseIncrement + phase = math.Mod(phase, MATH_TWO_PI) + break + case "noise": + prng := this.prng + + /* + * Check if pseudo-random number generator is initialized. + */ + if prng == nil { + prng = random.CreatePRNG(1337) + this.prng = prng + } + + /* + * Process each sample. + */ + for i, sample := range in { + r := prng.NextFloat() + uniform := (1.0 - (2.0 * r)) + out[i] = (facInput * sample) + (facSignal * uniform) + } + + break + } + + this.phase = phase +} + +/* + * Create a signal generator effects unit. + */ +func createSignalGenerator() Unit { + + /* + * Create effects unit. + */ + u := signalGenerator{ + unitStruct: unitStruct{ + unitType: UNIT_SIGNALGENERATOR, + params: []Parameter{ + Parameter{ + Name: "input_amplitude", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "input_gain", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "signal_type", + Type: PARAMETER_TYPE_DISCRETE, + Minimum: -1, + Maximum: -1, + NumericValue: -1, + DiscreteValueIndex: 0, + DiscreteValues: []string{ + "sine", + "triangle", + "square", + "sawtooth", + "noise", + }, + }, + Parameter{ + Name: "signal_frequency", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 1, + Maximum: 20000, + NumericValue: 440, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "signal_amplitude", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "signal_gain", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/tonestack.go b/effects/tonestack.go new file mode 100644 index 0000000..a131a14 --- /dev/null +++ b/effects/tonestack.go @@ -0,0 +1,155 @@ +package effects + +import ( + "math" +) + +/* + * Data structure representing a tone stack effect. + */ +type toneStack struct { + unitStruct + highpassCapVoltages []float64 + lowpassCapVoltages []float64 +} + +/* + * Tone stack audio processing. + */ +func (this *toneStack) Process(in []float64, out []float64, sampleRate uint32) { + frequencies := [...]float64{20.0, 300.0, 3000.0, 6000.0, 20000.0} + facs := [...]float64{0.0, 0.0, 0.0, 0.0} + names := [...]string{"low", "middle", "presence", "high"} + numBands := len(facs) + this.mutex.RLock() + + /* + * Read in levels and calculate factors. + */ + for i := 0; i < numBands; i++ { + name := names[i] + level, _ := this.getNumericValue(name) + facs[i] = decibelsToFactor(level) + } + + this.mutex.RUnlock() + + /* + * Allocate storage for highpass capacitor voltages if needed. + */ + if len(this.highpassCapVoltages) != numBands { + this.highpassCapVoltages = make([]float64, numBands) + } + + /* + * Allocate storage for lowpass capacitor voltages if needed. + */ + if len(this.lowpassCapVoltages) != numBands { + this.lowpassCapVoltages = make([]float64, numBands) + } + + sampleRateFloat := float64(sampleRate) + minusTwoPiOverSampleRate := -MATH_TWO_PI / sampleRateFloat + + /* + * Process each sample. + */ + for i, sample := range in { + sum := float64(0.0) + + /* + * Process each band and sum them all up. + */ + for j := 0; j < numBands; j++ { + jInc := j + 1 + hcv := this.highpassCapVoltages[j] + lcv := this.lowpassCapVoltages[j] + frequencyA := frequencies[j] + frequencyAFloat := float64(frequencyA) + argHP := minusTwoPiOverSampleRate * frequencyAFloat + dischargePerSampleHP := math.Exp(argHP) + dischargePerSampleHPInv := 1.0 - dischargePerSampleHP + frequencyB := frequencies[jInc] + frequencyBFloat := float64(frequencyB) + argLP := minusTwoPiOverSampleRate * frequencyBFloat + dischargePerSampleLP := math.Exp(argLP) + dischargePerSampleLPInv := 1.0 - dischargePerSampleLP + diff := sample - hcv + hcv += diff * dischargePerSampleHPInv + diff -= lcv + pre := lcv + lcv += diff * dischargePerSampleLPInv + this.highpassCapVoltages[j] = hcv + this.lowpassCapVoltages[j] = lcv + sum += facs[j] * pre + } + + /* + * Limit the output signal to the appropriate range. + */ + if sum < -1.0 { + out[i] = -1.0 + } else if sum > 1.0 { + out[i] = 1.0 + } else { + out[i] = sum + } + + } + +} + +/* + * Create a tone stack effects unit. + */ +func createToneStack() Unit { + + /* + * Create effects unit. + */ + u := toneStack{ + unitStruct: unitStruct{ + unitType: UNIT_TONESTACK, + params: []Parameter{ + Parameter{ + Name: "low", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: 0, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "middle", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: -2, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "presence", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: -5, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "high", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -30, + Maximum: 0, + NumericValue: -5, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/effects/tremolo.go b/effects/tremolo.go new file mode 100644 index 0000000..1dde6b2 --- /dev/null +++ b/effects/tremolo.go @@ -0,0 +1,111 @@ +package effects + +/* + * Data structure representing a tremolo effect. + */ +type tremolo struct { + unitStruct + attenuated bool + inStateSince uint32 +} + +/* + * Tremolo audio processing. + */ +func (this *tremolo) Process(in []float64, out []float64, sampleRate uint32) { + this.mutex.RLock() + frequency, _ := this.getNumericValue("frequency") + phase, _ := this.getNumericValue("phase") + depth, _ := this.getNumericValue("depth") + this.mutex.RUnlock() + sampleRateFloat := float64(sampleRate) + frequencyFloat := float64(frequency) + frequencyValue := 0.1 * frequencyFloat + periodLengthFloat := sampleRateFloat / frequencyValue + periodLength := uint32(periodLengthFloat) + phaseFloat := float64(phase) + phaseValue := 0.01 * phaseFloat + samplesUnattenuatedFloat := periodLengthFloat * phaseValue + samplesUnattenuated := uint32(samplesUnattenuatedFloat) + samplesAttenuated := periodLength - samplesUnattenuated + fac := decibelsToFactor(depth) + attenuated := this.attenuated + inStateSince := this.inStateSince + + /* + * Process each sample. + */ + for i, sample := range in { + result := sample + + /* + * Perform state transitions. + */ + if attenuated && (inStateSince >= samplesAttenuated) { + attenuated = false + inStateSince = 0 + } else if !attenuated && (inStateSince >= samplesUnattenuated) { + attenuated = true + inStateSince = 0 + } + + /* + * Check if signal should be attenuated. + */ + if attenuated { + result *= fac + } + + out[i] = result + inStateSince++ + } + + this.attenuated = attenuated + this.inStateSince = inStateSince +} + +/* + * Create a tremolo effects unit. + */ +func createTremolo() Unit { + + /* + * Create effects unit. + */ + u := tremolo{ + unitStruct: unitStruct{ + unitType: UNIT_TREMOLO, + params: []Parameter{ + Parameter{ + Name: "frequency", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 10, + Maximum: 100, + NumericValue: 100, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "phase", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: 0, + Maximum: 100, + NumericValue: 50, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + Parameter{ + Name: "depth", + Type: PARAMETER_TYPE_NUMERIC, + Minimum: -60, + Maximum: 0, + NumericValue: -10, + DiscreteValueIndex: -1, + DiscreteValues: nil, + }, + }, + }, + } + + return &u +} diff --git a/fft/fft.go b/fft/fft.go new file mode 100644 index 0000000..307cbf5 --- /dev/null +++ b/fft/fft.go @@ -0,0 +1,943 @@ +package fft + +import ( + "fmt" + "math" + "math/bits" + "math/cmplx" + "sync" +) + +/* + * Global constants. + */ +const ( + SCALING_DEFAULT = iota + SCALING_ORTHONORMAL + MODE_STANDARD + MODE_INPLACE +) + +/* + * Mathematical constants + */ +const ( + MATH_MINUS_TWO_PI = -2.0 * math.Pi +) + +/* + * Global variables. + */ +var g_mutex sync.Mutex +var g_coefficientsLarge map[int][]complex128 +var g_coefficientsSmall []complex128 +var g_permutationLarge map[int][]int +var g_permutationSmall []int +var g_scrapspace []complex128 + +/* + * Generates Fourier coefficients for n = 1, 2, 4, 8, ..., 8192. + */ +func generateFourierCoefficients() []complex128 { + coefficients := make([]complex128, 16384) + coefficients[0] = complex(0.0, 0.0) + + /* + * Generate coefficients for n = 2^k with k = [0, 13]. + */ + for k := uint(0); k <= 13; k++ { + n := 1 << k + nFloat := float64(n) + + /* + * Generate n coefficients. + */ + for i := 0; i < n; i++ { + idx := n + i + iFloat := float64(i) + argImag := (MATH_MINUS_TWO_PI * iFloat) / nFloat + arg := complex(0.0, argImag) + coefficients[idx] = cmplx.Exp(arg) + } + + } + + return coefficients +} + +/* + * Generates permutation coefficients for n = 1, 2, 4, 8, ..., 8192. + */ +func generatePermutationCoefficients() []int { + coefficients := make([]int, 16384) + coefficients[0] = 0 + + /* + * Generate coefficients for n = 2^p with p = [0, 13]. + */ + for p := 0; p <= 13; p++ { + shiftP := uint(p) + n := 1 << shiftP + coefficients[n] = 0 + + /* + * Do this for every power of two. + */ + for k := 0; k < p; k++ { + shiftM := uint(k) + m := 1 << shiftM + + /* + * Copy the coefficients, shifted by one place, then increment + * them by one. + */ + for i := 0; i < m; i++ { + idxA := n + i + idxB := idxA + m + value := coefficients[idxA] + value <<= 1 + coefficients[idxA] = value + coefficients[idxB] = value + 1 + } + + } + + } + + return coefficients +} + +/* + * Returns the Fourier coefficients for a Fourier transform of the specified size. + */ +func fourierCoefficients(n int) []complex128 { + + /* + * Ensure that the number of coefficients is positive, then fetch them either + * from a slice or generate them and store them in a map. + */ + if n < 0 { + return nil + } else if n <= 8192 { + uBound := n << 1 + return g_coefficientsSmall[n:uBound] + } else { + g_mutex.Lock() + coefficients, ok := g_coefficientsLarge[n] + + /* + * If coefficients aren't already calculated, calculate them now. + */ + if !ok { + coefficients = make([]complex128, n) + nFloat := float64(n) + + /* + * Calculate the Fourier coefficients. + */ + for j := 0; j < n; j++ { + jFloat := float64(j) + argImag := (MATH_MINUS_TWO_PI * jFloat) / nFloat + arg := complex(0.0, argImag) + coefficients[j] = cmplx.Exp(arg) + } + + g_coefficientsLarge[n] = coefficients + } + + g_mutex.Unlock() + return coefficients + } + +} + +/* + * Returns the permutation coefficients for an in-place Fourier transform of the + * specified size. + */ +func permutationCoefficients(n int) []int { + + /* + * Ensure that the number of coefficients is positive, then fetch them either + * from a slice or generate them and store them in a map. + */ + if n < 0 { + return nil + } else if n <= 8192 { + uBound := n << 1 + return g_permutationSmall[n:uBound] + } else { + g_mutex.Lock() + coefficients, ok := g_permutationLarge[n] + + /* + * If coefficients aren't already calculated, calculate them now. + */ + if !ok { + n64 := uint64(n) + num, p := NextPowerOfTwo(n64) + coefficients = make([]int, num) + coefficients[0] = 0 + + /* + * Do this for every power of two. + */ + for i := uint32(0); i < p; i++ { + m := 1 << i + + /* + * Copy the coefficients, shifted by one place, then + * increment them by one. + */ + for j := 0; j < m; j++ { + value := coefficients[j] + value <<= 1 + coefficients[j] = value + idx := j + m + coefficients[idx] = value + 1 + } + + } + + g_permutationLarge[n] = coefficients + } + + g_mutex.Unlock() + return coefficients + } + +} + +/* + * Compute the fast Fourier transform using the recursive Cooley-Tukey algorithm. + */ +func cooleyTukey(vec []complex128) []complex128 { + n := len(vec) + + /* + * Abort recursion when only a single element is left. + */ + if n <= 1 { + return vec + } else { + nHalf := n / 2 + even := make([]complex128, nHalf) + odd := make([]complex128, nHalf) + result := make([]complex128, n) + + /* + * Split vector into even and odd half. + */ + for i := 0; i < nHalf; i++ { + idxEven := i << 1 + idxOdd := idxEven + 1 + even[i] = vec[idxEven] + odd[i] = vec[idxOdd] + } + + lower := cooleyTukey(even) + upper := cooleyTukey(odd) + coefficients := fourierCoefficients(n) + + /* + * Perform the "twiddling". + */ + for i, elem := range lower { + product := coefficients[i] * upper[i] + lower[i] = elem + product + upper[i] = elem - product + } + + copy(result[0:nHalf], lower) + copy(result[nHalf:n], upper) + return result + } + +} + +/* + * Perform the Fourier input permutation on a vector. + */ +func permute(vec []complex128) { + n := len(vec) + coeff := permutationCoefficients(n) + g_mutex.Lock() + + /* + * Check if size for scrapspace is sufficient. + */ + if g_scrapspace == nil || len(g_scrapspace) < n { + g_scrapspace = make([]complex128, n) + } + + copy(g_scrapspace, vec) + + /* + * Permute the elements. + */ + for i := 0; i < n; i++ { + idx := coeff[i] + vec[i] = g_scrapspace[idx] + } + + g_mutex.Unlock() +} + +/* + * Compute the fast Fourier transform using an (unnamed?) in-place algorithm. + */ +func inplaceTransform(vec []complex128) { + permute(vec) + n := len(vec) + coeffs := fourierCoefficients(n) + size := 1 + stride := n + n64 := uint64(n) + npp := n64 + 1 + _, p := NextPowerOfTwo(npp) + pmm := int(p - 1) + + /* + * Fourier rounds. + */ + for i := 1; i <= pmm; i++ { + size <<= 1 + stride >>= 1 + blocks := n / size // The number of blocks. + + /* + * Process each block. + */ + for j := 0; j < blocks; j++ { + halfBlocks := blocks << 1 + half := n / halfBlocks // The length of a half-block. + dj := j << 1 + offset := dj * half // The offset into the current block. + + /* + * Perform the butterfly operations. + */ + for k := 0; k < half; k++ { + i := k + offset + j := i + half + vi := vec[i] + vj := vec[j] + l := k * stride + m := half * stride + n := l + m + cl := coeffs[l] + cn := coeffs[n] + left := vi + (cl * vj) + right := vi + (cn * vj) + vec[i] = left + vec[j] = right + } + + } + + } + +} + +/* + * Initialize the computation of a Fourier transform. + */ +func initialize() { + g_mutex.Lock() + + /* + * Generate the global Fourier coefficient slice. + */ + if g_coefficientsSmall == nil { + g_coefficientsSmall = generateFourierCoefficients() + } + + /* + * Initialize the global Fourier coefficient map. + */ + if g_coefficientsLarge == nil { + g_coefficientsLarge = make(map[int][]complex128) + } + + /* + * Initialize the global permutation coefficient slice. + */ + if g_permutationSmall == nil { + g_permutationSmall = generatePermutationCoefficients() + } + + /* + * Initialize the global permutation coefficient map. + */ + if g_permutationLarge == nil { + g_permutationLarge = make(map[int][]int) + } + + g_mutex.Unlock() +} + +/* + * Swap the real and imaginary parts of a complex-valued vector and return the new + * vector. + */ +func swapComplex(vec []complex128) []complex128 { + n := len(vec) + result := make([]complex128, n) + + /* + * Swap real and imaginary part for each element of the vector. + */ + for i, elem := range vec { + elemReal := real(elem) + elemImag := imag(elem) + result[i] = complex(elemImag, elemReal) + } + + return result +} + +/* + * Swap two elements in a complex vector. + */ +func swapComplexElements(vec []complex128, i int, j int) { + tmp := vec[i] + vec[i] = vec[j] + vec[j] = tmp +} + +/* + * Swap the real and imaginary parts of a complex-valued vector in-place. + */ +func swapComplexInPlace(vec []complex128) { + + /* + * Swap real and imaginary part for each element of the vector. + */ + for i, elem := range vec { + elemReal := real(elem) + elemImag := imag(elem) + result := complex(elemImag, elemReal) + vec[i] = result + } + +} + +/* + * Find the next higher power of two. + * + * If value is already a power of two, the same value is returned. + */ +func NextPowerOfTwo(value uint64) (uint64, uint32) { + digit := bits.Len64(value) + digit32 := uint32(digit) + exp := digit32 - 1 + pow := uint64(1) + pow <<= exp + + /* + * If we are still below the threshold, we need an extra bit. + */ + if pow < value { + exp++ + pow <<= 1 + } + + return pow, exp +} + +/* + * Write zeroes to a complex-valued buffer. + */ +func ZeroComplex(buffer []complex128) { + + /* + * Iterate over the buffer to zero it. + */ + for i, _ := range buffer { + buffer[i] = complex(0.0, 0.0) + } + +} + +/* + * Write zeroes to a floating-point buffer. + */ +func ZeroFloat(buffer []float64) { + + /* + * Iterate over the buffer to zero it. + */ + for i, _ := range buffer { + buffer[i] = float64(0.0) + } + +} + +/* + * Calculates the Fourier transform of a vector. + */ +func Fourier(vec []complex128, scaling int, mode int) []complex128 { + initialize() + result := vec + + /* + * Decide on which mode to operate. + */ + switch mode { + + /* + * Standard mode - copies data elements, slower. + */ + case MODE_STANDARD: + result = cooleyTukey(vec) + + /* + * In-place mode - avoids copies of data elements, faster. + */ + case MODE_INPLACE: + inplaceTransform(result) + + /* + * This should never happen. + */ + default: + result = nil + } + + /* + * Check if we should apply orthonormal scaling. + */ + if scaling == SCALING_ORTHONORMAL { + + /* + * Make sure that we got a result. + */ + if result != nil { + n := len(vec) + nFloat := float64(n) + sqrtN := math.Sqrt(nFloat) + r := 1.0 / sqrtN + fac := complex(r, 0.0) + + /* + * Scale the result vector. + */ + for i := 0; i < n; i++ { + result[i] *= fac + } + + } + + } + + return result +} + +/* + * Calculates the inverse Fourier transform of a vector. + */ +func InverseFourier(vec []complex128, scaling int, mode int) []complex128 { + initialize() + n := len(vec) + nFloat := float64(n) + r := float64(0.0) + + /* + * Check which kind of scaling should be applied. + */ + switch scaling { + case SCALING_DEFAULT: + r = 1.0 / nFloat + break + case SCALING_ORTHONORMAL: + sqrtN := math.Sqrt(nFloat) + r = 1.0 / sqrtN + break + } + + scalingFac := complex(r, 0.0) + + /* + * Decide on which mode to operate. + */ + switch mode { + + /* + * Standard mode - copies data elements, slower. + */ + case MODE_STANDARD: + swapped := swapComplex(vec) + swappedResult := cooleyTukey(swapped) + result := swapComplex(swappedResult) + + /* + * Apply scaling to the result vector. + */ + for i, elem := range result { + result[i] = scalingFac * elem + } + + return result + + /* + * In-place mode - avoids copies of data elements, faster. + */ + case MODE_INPLACE: + swapComplexInPlace(vec) + inplaceTransform(vec) + swapComplexInPlace(vec) + + /* + * Apply scaling to the vector. + */ + for i, elem := range vec { + vec[i] = scalingFac * elem + } + + return vec + + /* + * This should never happen. + */ + default: + return nil + } + +} + +/* + * Performs a (forward) Fourier transform of a real-valued vector. + */ +func RealFourier(in []float64, out []complex128, scaling int) error { + nIn := len(in) + nOut := len(out) + + /* + * Verify that input and output sequences are of equal size. + */ + if nIn != nOut { + return fmt.Errorf("%s", "Input and output sequences must be of equal length.") + } else { + m := nIn % 2 + + /* + * Check if the number of elements in the vector is odd or even. + */ + if m != 0 { + + /* + * If the number of elements is odd, there may only be a single + * element. + */ + if nIn == 1 { + elem := in[0] + out[0] = complex(elem, 0.0) + return nil + } else { + return fmt.Errorf("%s", "The number of elements in the vector must be even or one.") + } + + } else { + nHalf := nIn / 2 + + /* + * Iterate over the lower half of the output sequence and put + * even elements into the real part, odd elements into the + * imaginary part of a complex sequence of half the length. + */ + for i := 0; i < nHalf; i++ { + idxEven := i << 1 + idxOdd := idxEven + 1 + even := in[idxEven] + odd := in[idxOdd] + out[i] = complex(even, odd) + } + + lower := out[0:nHalf] + upper := out[nHalf:nOut] + Fourier(lower, scaling, MODE_INPLACE) + copy(upper, lower) + j := complex(0.0, 1.0) + coeffs := fourierCoefficients(nIn) + + /* + * Iterate over the upper half of the output sequence to perform + * an additional butterfly pass and store the result in the lower + * half. + */ + for i := 0; i < nHalf; i++ { + idxLow := nHalf + i + idxHigh := nOut - i + + /* + * out[idxHigh] = upper[nHalf - i], but we need to handle + * i == 0 specially to stay within the slice bounds. + */ + if i == 0 { + idxHigh = nHalf + } + + low := out[idxLow] + high := out[idxHigh] + highConj := cmplx.Conj(high) + coeff := j * coeffs[i] + out[i] = 0.5 * ((low + highConj) - (coeff * (low - highConj))) + } + + /* + * Calculate the remaining parts of the output sequence. + */ + for i := 1; i < nHalf; i++ { + elem := out[i] + idx := nOut - i + out[idx] = cmplx.Conj(elem) + } + + centerElem := out[nHalf] + centerElemConj := cmplx.Conj(centerElem) + out[nHalf] = 0.5 * ((centerElem + centerElemConj) + (j * (centerElem - centerElemConj))) + + /* + * If we need to apply orthonormal scaling, multiply by inverse + * square root of two, to compensate for the larger size of the + * transform. + */ + if scaling == SCALING_ORTHONORMAL { + invSqrt2 := complex(1.0/math.Sqrt2, 0.0) + + /* + * Multiply each element in the output vector by a square + * root of two. + */ + for i, elem := range out { + out[i] = invSqrt2 * elem + } + + } + + return nil + } + + } + +} + +/* + * Performs an inverse Fourier transform resulting in a real-valued vector. + * + * This function will destroy the contents of the input vector in the process. + */ +func RealInverseFourier(in []complex128, out []float64, scaling int) error { + nIn := len(in) + nOut := len(out) + + /* + * Verify that input and output sequences are of equal size. + */ + if nIn != nOut { + return fmt.Errorf("%s", "Input and output sequences must be of equal length.") + } else { + m := nIn % 2 + + /* + * Check if the number of elements in the vector is odd or even. + */ + if m != 0 { + + /* + * If the number of elements is odd, there may only be a single + * element. + */ + if nIn == 1 { + elem := in[0] + out[0] = real(elem) + return nil + } else { + return fmt.Errorf("%s", "The number of elements in the vector must be even or one.") + } + + } else { + nHalf := nIn / 2 + + /* + * Ensure that the input array is conjugate symmetric and store + * the relevant data in its lower half. + */ + for i := 1; i < nHalf; i++ { + lowValue := in[i] + idx := nIn - i + highValue := in[idx] + highValueConj := cmplx.Conj(highValue) + average := 0.5 * (lowValue + highValueConj) + in[i] = average + } + + /* BEGIN MAGIC */ + dc := in[0] + dcReal := real(dc) + nyquist := in[nHalf] + nyquistReal := real(nyquist) + /* END MAGIC */ + + lower := in[0:nHalf] + upper := in[nHalf:nIn] + copy(upper, lower) + coeffs := fourierCoefficients(nIn) + j := complex(0.0, 1.0) + + /* + * Calculate an inverse butterfly pass on the upper half and + * store the results in the lower half of the spectrum. + */ + for i := 0; i < nHalf; i++ { + idxLow := nHalf + i + idxHigh := nOut - i + + /* + * in[idxHigh] = upper[nHalf - i], but we need to handle + * i == 0 specially to stay within the slice bounds. + */ + if i == 0 { + idxHigh = nHalf + } + + low := in[idxLow] + high := in[idxHigh] + highConj := cmplx.Conj(high) + even := low + highConj + coeff := coeffs[i] + coeffConj := cmplx.Conj(coeff) + odd := (low - highConj) * coeffConj + in[i] = 0.5 * (even + (j * odd)) + } + + /* BEGIN MAGIC */ + firstNewReal := 0.5 * (dcReal + nyquistReal) + firstNewImag := 0.5 * (dcReal - nyquistReal) + lower[0] = complex(firstNewReal, firstNewImag) + /* END MAGIC */ + + ZeroComplex(upper) + InverseFourier(lower, scaling, MODE_INPLACE) + + /* + * Extract the real components from the lower half of the + * spectrum. + */ + for i := 0; i < nHalf; i++ { + value := in[i] + idx := i << 1 + idxInc := idx + 1 + out[idx] = real(value) + out[idxInc] = imag(value) + } + + /* + * If we need to apply orthonormal scaling, multiply by inverse + * square root of two, to compensate for the larger size of the + * transform. + */ + if scaling == SCALING_ORTHONORMAL { + + /* + * Multiply each element in the output vector by a square + * root of two. + */ + for i, elem := range out { + out[i] = math.Sqrt2 * elem + } + + } + + return nil + } + + } + +} + +/* + * Shift negative frequencies to lower indices than the DC component or invert the + * shifting process. + */ +func Shift(vec []complex128, inverse bool) { + n := len(vec) + nNegative := n >> 1 + nPositive := nNegative + isOdd := (n & 1) != 0 + + /* + * If the number of frequency bins is odd, there is one more bin for positive + * frequencies than there are bins for negative frequencies. + */ + if isOdd { + nPositive++ + } + + ptrA := 0 + ptrB := 0 + + /* + * During the forward operation, the second pointer is offset from the first + * pointer by the number of positive coefficients. + * + * During the inverse operation, the second pointer is offset from the first + * pointer by the number of negative coefficients. + */ + if inverse { + ptrB = nNegative + } else { + ptrB = nPositive + } + + /* + * Do this until the second pointer reaches the end of the slice. + */ + for ptrB < n { + swapComplexElements(vec, ptrA, ptrB) + ptrA++ + ptrB++ + } + + /* + * If the number of frequency bins is odd, we have to perform further post- + * processing. We have to rotate the entire "right half" of the vector, + * including the central element, by one position to the left (during the + * forward transform) or to the right (during the inverse transform). + */ + if isOdd { + + /* + * During the forward transform, we have to rotate to the left. + * During the inverse transform, we have to rotate to the right. + */ + if inverse { + ptrB = n - 1 + ptrA = ptrB - 1 + + /* + * Do this until the first pointer reaches the positive elements. + */ + for ptrA >= nPositive { + swapComplexElements(vec, ptrA, ptrB) + ptrA-- + ptrB-- + } + + } else { + ptrB = ptrA + 1 + + /* + * Do this until the second pointer reaches the end of the slice. + */ + for ptrB < n { + swapComplexElements(vec, ptrA, ptrB) + ptrA++ + ptrB++ + } + + } + + } + +} diff --git a/fft/fft_test.go b/fft/fft_test.go new file mode 100644 index 0000000..bdc74ce --- /dev/null +++ b/fft/fft_test.go @@ -0,0 +1,831 @@ +package fft + +import ( + "math" + "testing" +) + +/* + * Compare two real-valued slices to check whether their components are close. + */ +func areSlicesClose(a []float64, b []float64) (bool, []float64) { + + /* + * Check whether the two slices are of the same size. + */ + if len(a) != len(b) { + return false, nil + } else { + c := true + n := len(a) + diffs := make([]float64, n) + + /* + * Iterate over the arrays to compare values. + */ + for i, elem := range a { + diff := elem - b[i] + diffAbs := math.Abs(diff) + + /* + * Check if we found a significant difference. + */ + if diffAbs > 0.00000001 { + c = false + } + + diffs[i] = diff + } + + return c, diffs + } + +} + +/* + * Compare two complex-valued slices to check whether their components are equal. + */ +func areSlicesEqual(a []complex128, b []complex128) bool { + + /* + * Check whether the two slices are of the same size. + */ + if len(a) != len(b) { + return false + } else { + c := true + + /* + * Iterate over the arrays to compare values. + */ + for i, elem := range a { + c = c && (elem == b[i]) + } + + return c + } + +} + +/* + * Perform a unit test on the power-of-two function. + */ +func TestNextPowerOfTwo(t *testing.T) { + + /* + * Input values. + */ + in := []uint64{ + 6493615572477977987, + 183778605738611348, + 1211049956568627, + 877784, + 65537, + 65536, + 63128, + 255, + 2, + 1, + } + + /* + * Next higher powers of two. + */ + powers := []uint64{ + 9223372036854775808, + 288230376151711744, + 2251799813685248, + 1048576, + 131072, + 65536, + 65536, + 256, + 2, + 1, + } + + /* + * Exponents. + */ + exponents := []uint32{ + 63, + 58, + 51, + 20, + 17, + 16, + 16, + 8, + 1, + 0, + } + + /* + * Test the function for each input value. + */ + for i, val := range in { + expectedP := powers[i] + expectedE := exponents[i] + p, e := NextPowerOfTwo(val) + + /* + * Check if we got the expected power. + */ + if p != expectedP { + t.Errorf("Power of two number %d: Resulting power is incorrect. Expected %d (%x), got %d (%x).", i, expectedP, expectedP, p, p) + } + + /* + * Check if we got the expected exponent. + */ + if e != expectedE { + t.Errorf("Power of two number %d: Resulting exponent is incorrect. Expected %d (%x), got %d (%x).", i, expectedE, expectedE, e, e) + } + + } + +} + +/* + * Perform a unit test on the zeroes a complex-valued vector. + */ +func TestZeroComplex(t *testing.T) { + sizes := []int{1, 2, 4, 8, 15, 16} + + /* + * Create buffers of different size. + */ + for _, size := range sizes { + ones := make([]complex128, size) + + /* + * Initialize both real and imaginary part to one. + */ + for i, _ := range ones { + ones[i] = complex(1.0, 1.0) + } + + ZeroComplex(ones) + + /* + * Verify that all elements are now zeroed. + */ + for i, elem := range ones { + re := real(elem) + ie := imag(elem) + + /* + * If either real or imaginary part is non-zero, fail. + */ + if re != 0.0 || ie != 0.0 { + t.Errorf("Failed to zero complex-valued buffer of size %d. Element %d is non-zero.", size, i) + } + + } + + } + +} + +/* + * Perform a unit test on the function that zeroes a real-valued vector. + */ +func TestZeroFloat(t *testing.T) { + sizes := []int{1, 2, 4, 8, 15, 16} + + /* + * Create buffers of different size. + */ + for _, size := range sizes { + ones := make([]float64, size) + + /* + * Initialize each element to one. + */ + for i, _ := range ones { + ones[i] = 1.0 + } + + ZeroFloat(ones) + + /* + * Verify that all elements are now zeroed. + */ + for i, elem := range ones { + + /* + * If element is non-zero, fail. + */ + if elem != 0.0 { + t.Errorf("Failed to zero real-valued buffer of size %d. Element %d is non-zero.", size, i) + } + + } + + } + +} + +/* + * Perform a unit test on the real-valued FFT. + */ +func TestRealFFT(t *testing.T) { + + /* + * Input vectors. + */ + in := [][]float64{ + []float64{0.0, 1.0, 0.0, 0.0}, + []float64{1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0}, + []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}, + []float64{1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0}, + []float64{0.93990505, 0.20043027, 0.24328743, 0.39466036, 0.62847371, 0.29570877, 0.30114516, 0.7491788}, + } + + /* + * Real components of expected output vectors. + */ + outRealExpected := [][]float64{ + []float64{1.0, 0.0, -1.0, 0.0}, + []float64{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}, + []float64{1.0, 0.70710678, 0.0, -0.70710678, -1.0, -0.70710678, 0.0, 0.70710678}, + []float64{10.0, -0.41421356, -2.00000000, 2.41421356, -2.0, 2.41421356, -2.0, -0.41421356}, + []float64{36.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0}, + []float64{0.0, 0.0, 0.0, 0.0, 8.0, 0.0, 0.0, 0.0}, + []float64{3.75278955, 0.49474166, 1.02394617, 0.12812102, 0.47283315, 0.12812102, 1.02394617, 0.49474166}, + } + + /* + * Imaginary components of expected output vectors. + */ + outImagExpected := [][]float64{ + []float64{0.0, -1.0, 0.0, 1.0}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, -0.70710678, -1.0, -0.70710678, 0.0, 0.70710678, 1.0, 0.70710678}, + []float64{0.0, -7.24264069, 2.0, -1.24264069, 0.0, 1.24264069, -2.0, 7.24264069}, + []float64{0.0, 9.65685425, 4.0, 1.65685425, 0.0, -1.65685425, -4., -9.65685425}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, 0.3759122, 0.64770012, 0.26019674, 0.0, -0.26019674, -0.64770012, -0.3759122}, + } + + /* + * Test with each input vector. + */ + for i, currentIn := range in { + n := len(currentIn) + expectedReal := outRealExpected[i] + nReal := len(expectedReal) + expectedImag := outImagExpected[i] + nImag := len(expectedImag) + + /* + * Verify that expected result vectors are of correct size. + */ + if nReal != n { + t.Errorf("Expected real result vector %d is of incorrect size: Expected %d, got %d.", i, n, nReal) + } else if nImag != n { + t.Errorf("Expected imaginary result vector %d is of incorrect size: Expected %d, got %d.", i, n, nImag) + } else { + currentResult := make([]complex128, nReal) + err := RealFourier(currentIn, currentResult, SCALING_DEFAULT) + + /* + * Check if forward transform was calculated successfully. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to calculate real FFT: %s", msg) + } else { + currentResultReal := make([]float64, nReal) + currentResultImag := make([]float64, nImag) + + /* + * Extract real and imaginary components from result vector. + */ + for i, elem := range currentResult { + currentResultReal[i] = real(elem) + currentResultImag[i] = imag(elem) + } + + okReal, diffReal := areSlicesClose(currentResultReal, expectedReal) + + /* + * Verify real components of result vector. + */ + if !okReal { + t.Errorf("Real FFT number %d: Real part of result is incorrect. Expected %v, got %v, difference: %v", i, expectedReal, currentResultReal, diffReal) + } + + okImag, diffImag := areSlicesClose(currentResultImag, expectedImag) + + /* + * Verify imaginary components of result vector. + */ + if !okImag { + t.Errorf("Real FFT number %d: Imaginary part of result is incorrect. Expected %v, got %v, difference: %v", i, expectedImag, currentResultImag, diffImag) + } + + } + + currentInverse := make([]float64, nReal) + err = RealInverseFourier(currentResult, currentInverse, SCALING_DEFAULT) + + /* + * Check if inverse transform was calculated successfully. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to calculate real IFFT: %s", msg) + } else { + okInverse, diffInverse := areSlicesClose(currentInverse, currentIn) + + /* + * Verify components of IFFT result vector. + */ + if !okInverse { + t.Errorf("Real IFFT number %d: Result is incorrect. Expected %v, got %v, difference: %v", i, currentIn, currentInverse, diffInverse) + } + + } + + } + + } + +} + +/* + * Perform a unit test on the complex-valued FFT. + */ +func TestComplexFFT(t *testing.T) { + + /* + * Real components fo the input vectors. + */ + inReal := [][]float64{ + []float64{0.0, 1.0, 0.0, 0.0}, + []float64{1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0}, + []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}, + []float64{1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0}, + []float64{0.93811391, 0.12498467, 0.65156107, 0.68689968, 0.04341771, 0.29019219, 0.89338032, 0.44420547}, + } + + /* + * Imaginary components fo the input vectors. + */ + inImag := [][]float64{ + []float64{0.0, 0.0, 0.0, 0.0}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.00579331, 0.57801897, 0.69192584, 0.60747351, 0.75338567, 0.24053831, 0.12623075, 0.01731368}, + } + + /* + * Real components of expected output vectors. + */ + outRealExpected := [][]float64{ + []float64{1.0, 0.0, -1.0, 0.0}, + []float64{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}, + []float64{1.0, 0.70710678, 0.0, -0.70710678, -1.0, -0.70710678, 0.0, 0.70710678}, + []float64{10.0, -0.41421356, -2.00000000, 2.41421356, -2.0, 2.41421356, -2.0, -0.41421356}, + []float64{36.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0}, + []float64{0.0, 0.0, 0.0, 0.0, 8.0, 0.0, 0.0, 0.0}, + []float64{4.07275502, 1.82790209, -0.36963968, 1.27337207, 0.98019100, 1.09288049, -0.75717986, -0.61536985}, + } + + /* + * Imaginary components of expected output vectors. + */ + outImagExpected := [][]float64{ + []float64{0.0, -1.0, 0.0, 1.0}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{0.0, -0.70710678, -1.0, -0.70710678, 0.0, 0.70710678, 1.0, 0.70710678}, + []float64{0.0, -7.24264069, 2.0, -1.24264069, 0.0, 1.24264069, -2.0, 7.24264069}, + []float64{0.0, 9.65685425, 4.0, 1.65685425, 0.0, -1.65685425, -4., -9.65685425}, + []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + []float64{3.02068004, -0.73923563, 0.65695068, -0.86553182, 0.1339911, -0.27231059, -0.7749059, -1.1132914}, + } + + /* + * Possible modes of operation. + */ + modes := []int{ + MODE_STANDARD, + MODE_INPLACE, + } + + /* + * Test with each mode of operation. + */ + for _, mode := range modes { + + /* + * Test with each input vector. + */ + for i, currentInReal := range inReal { + n := len(currentInReal) + currentInImag := inImag[i] + m := len(currentInImag) + + /* + * Verify that the input vectors are of correct size. + */ + if n != m { + t.Errorf("Components of vector %d are of unequal size: %d real components and %d imaginary components.", i, n, m) + } else { + currentComplexIn := make([]complex128, n) + + /* + * Form a complex vector out of real and imaginary components. + */ + for j, cr := range currentInReal { + ci := currentInImag[j] + currentComplexIn[j] = complex(cr, ci) + } + + expectedReal := outRealExpected[i] + nReal := len(expectedReal) + expectedImag := outImagExpected[i] + nImag := len(expectedImag) + + /* + * Verify that expected result vectors are of correct size. + */ + if nReal != n { + t.Errorf("Expected real result vector %d is of incorrect size: Expected %d, got %d.", i, n, nReal) + } else if nImag != n { + t.Errorf("Expected imaginary result vector %d is of incorrect size: Expected %d, got %d.", i, n, nImag) + } else { + currentResult := make([]complex128, n) + copy(currentResult, currentComplexIn) + currentResult = Fourier(currentResult, SCALING_DEFAULT, mode) + currentResultReal := make([]float64, nReal) + currentResultImag := make([]float64, nImag) + + /* + * Extract real and imaginary components from result vector. + */ + for i, elem := range currentResult { + currentResultReal[i] = real(elem) + currentResultImag[i] = imag(elem) + } + + okReal, diffReal := areSlicesClose(currentResultReal, expectedReal) + + /* + * Verify real components of result vector. + */ + if !okReal { + t.Errorf("Complex FFT number %d (mode %d): Real part of result is incorrect. Expected %v, got %v, difference: %v", i, mode, expectedReal, currentResultReal, diffReal) + } + + okImag, diffImag := areSlicesClose(currentResultImag, expectedImag) + + /* + * Verify imaginary components of result vector. + */ + if !okImag { + t.Errorf("Complex FFT number %d (mode %d): Imaginary part of result is incorrect. Expected %v, got %v, difference: %v", i, mode, expectedImag, currentResultImag, diffImag) + } + + currentInverse := make([]complex128, n) + copy(currentInverse, currentResult) + currentInverse = InverseFourier(currentInverse, SCALING_DEFAULT, mode) + currentInverseReal := make([]float64, nReal) + currentInverseImag := make([]float64, nImag) + + /* + * Extract real and imaginary components from inverse vector. + */ + for i, elem := range currentInverse { + currentInverseReal[i] = real(elem) + currentInverseImag[i] = imag(elem) + } + + okInverseReal, diffInverseReal := areSlicesClose(currentInverseReal, currentInReal) + + /* + * Verify real components of IFFT result vector. + */ + if !okInverseReal { + t.Errorf("Complex IFFT number %d (mode %d): Real part of result is incorrect. Expected %v, got %v, difference: %v", i, mode, currentInReal, currentInverseReal, diffInverseReal) + } + + okInverseImag, diffInverseImag := areSlicesClose(currentInverseImag, currentInImag) + + /* + * Verify real components of IFFT result vector. + */ + if !okInverseImag { + t.Errorf("Complex IFFT number %d (mode %d): Imaginary part of result is incorrect. Expected %v, got %v, difference: %v", i, mode, currentInImag, currentInverseImag, diffInverseImag) + } + + } + + } + + } + + } + +} + +/* + * Test (real) FFT with orthonormal scaling. + */ +func TestOrthonormalScaling(t *testing.T) { + + /* + * Real input vector. + */ + in := []float64{ + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + } + + /* + * Real part of expected output vector. + */ + expectedReal := []float64{ + 0.35355339, 0.25, 0.0, -0.25, -0.35355339, -0.25, 0.0, 0.25, + } + + /* + * Imaginary part of expected output vector. + */ + expectedImag := []float64{ + 0.0, -0.25, -0.35355339, -0.25, 0.0, 0.25, 0.35355339, 0.25, + } + + n := len(in) + result := make([]complex128, n) + err := RealFourier(in, result, SCALING_ORTHONORMAL) + + /* + * Check if forward transform was calculated successfully. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to calculate real orthonormal FFT: %s", msg) + } else { + resultReal := make([]float64, n) + resultImag := make([]float64, n) + + /* + * Extract real and imaginary components from result vector. + */ + for i, elem := range result { + resultReal[i] = real(elem) + resultImag[i] = imag(elem) + } + + okReal, diffReal := areSlicesClose(resultReal, expectedReal) + + /* + * Verify real components of result vector. + */ + if !okReal { + t.Errorf("Real orthonormal FFT: Real part of result is incorrect. Expected %v, got %v, difference: %v", expectedReal, resultReal, diffReal) + } + + okImag, diffImag := areSlicesClose(resultImag, expectedImag) + + /* + * Verify imaginary components of result vector. + */ + if !okImag { + t.Errorf("Real orthonormal FFT: Imaginary part of result is incorrect. Expected %v, got %v, difference: %v", expectedImag, resultImag, diffImag) + } + + } + + inverse := make([]float64, n) + err = RealInverseFourier(result, inverse, SCALING_ORTHONORMAL) + + /* + * Check if inverse transform was calculated successfully. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to calculate real orthonormal IFFT: %s", msg) + } else { + okInverse, diffInverse := areSlicesClose(inverse, in) + + /* + * Verify components of IFFT result vector. + */ + if !okInverse { + t.Errorf("Real orthonormal IFFT: Result is incorrect. Expected %v, got %v, difference: %v", in, inverse, diffInverse) + } + + } + +} + +/* + * Test the edge-case of an input vector containing only a single element. + */ +func TestSingleElementFFT(t *testing.T) { + + /* + * Real input. + */ + inReal := []float64{ + 3.14, + } + + /* + * Complex input. + */ + inComplex := []complex128{ + complex(3.14, 0.0), + } + + outReal := make([]float64, 1) + outComplex := make([]complex128, 1) + err := RealFourier(inReal, outComplex, SCALING_DEFAULT) + + /* + * Check if forward transform was calculated successfully. + */ + if err != nil { + msg := err.Error() + t.Errorf("Single-element real FFT failed: %s", msg) + } else { + + /* + * Check if we got the expected result for the forward transform. + */ + if real(outComplex[0]) != 3.14 || imag(outComplex[0]) != 0.0 { + t.Errorf("Single-element real FFT did not return expected result.") + } + + err = RealInverseFourier(inComplex, outReal, SCALING_DEFAULT) + + /* + * Check if inverse transform was calculated successfully. + */ + if err != nil { + msg := err.Error() + t.Errorf("Single-element real IFFT failed: %s", msg) + } else { + + /* + * Check if we got the expected result for the inverse transform. + */ + if outReal[0] != 3.14 { + t.Errorf("Single-element real IFFT did not return expected result.") + } + + } + + } + +} + +/* + * Test cases where the transforms should fail. + */ +func TestFailureCases(t *testing.T) { + rThree := make([]float64, 3) + rEight := make([]float64, 8) + cThree := make([]complex128, 3) + cFour := make([]complex128, 4) + err := RealFourier(rThree, cThree, SCALING_DEFAULT) + + /* + * Verify that the transform failed. + */ + if err == nil { + t.Errorf("Real FFT of size three did not fail!") + } + + err = RealInverseFourier(cThree, rThree, SCALING_DEFAULT) + + /* + * Verify that the transform failed. + */ + if err == nil { + t.Errorf("Real IFFT of size three did not fail!") + } + + err = RealFourier(rEight, cFour, SCALING_DEFAULT) + + /* + * Verify that the transform failed. + */ + if err == nil { + t.Errorf("Real FFT of unequal size did not fail!") + } + + err = RealInverseFourier(cFour, rEight, SCALING_DEFAULT) + + /* + * Verify that the transform failed. + */ + if err == nil { + t.Errorf("Real IFFT of unequal size did not fail!") + } + +} + +/* + * Test spectral shifting functions. + */ +func TestShift(t *testing.T) { + + /* + * Input vector with even number of elements. + */ + inEven := []complex128{ + complex(1.0, 2.0), + complex(3.0, 4.0), + complex(5.0, 6.0), + complex(7.0, 8.0), + } + + /* + * Expected output vector with even number of elements. + */ + outEven := []complex128{ + complex(5.0, 6.0), + complex(7.0, 8.0), + complex(1.0, 2.0), + complex(3.0, 4.0), + } + + /* + * Input vector with odd number of elements. + */ + inOdd := []complex128{ + complex(1.0, 2.0), + complex(3.0, 4.0), + complex(5.0, 6.0), + complex(7.0, 8.0), + complex(9.0, 10.0), + } + + /* + * Expected output vector with even number of elements. + */ + outOdd := []complex128{ + complex(7.0, 8.0), + complex(9.0, 10.0), + complex(1.0, 2.0), + complex(3.0, 4.0), + complex(5.0, 6.0), + } + + nEven := len(inEven) + nOdd := len(inOdd) + shiftEven := make([]complex128, nEven) + shiftOdd := make([]complex128, nOdd) + copy(shiftEven, inEven) + copy(shiftOdd, inOdd) + Shift(shiftEven, false) + Shift(shiftOdd, false) + evenCorrect := areSlicesEqual(shiftEven, outEven) + + /* + * Check if array with even element-count was permuted correctly. + */ + if !evenCorrect { + t.Errorf("Even-valued array was not permuted correctly. Expected %v, got %v.", outEven, shiftEven) + } + + oddCorrect := areSlicesEqual(shiftOdd, outOdd) + + /* + * Check if array with odd element-count was permuted correctly. + */ + if !oddCorrect { + t.Errorf("Odd-valued array was not permuted correctly. Expected %v, got %v.", outOdd, shiftOdd) + } + + Shift(shiftEven, true) + Shift(shiftOdd, true) + evenCorrect = areSlicesEqual(shiftEven, inEven) + + /* + * Check if array with even element-count was inverse-permuted correctly. + */ + if !evenCorrect { + t.Errorf("Even-valued array was not inverse-permuted correctly. Expected %v, got %v.", inEven, shiftEven) + } + + oddCorrect = areSlicesEqual(shiftOdd, inOdd) + + /* + * Check if array with even element-count was inverse-permuted correctly. + */ + if !evenCorrect { + t.Errorf("Odd-valued array was not inverse-permuted correctly. Expected %v, got %v.", inOdd, shiftOdd) + } + +} diff --git a/filter/filter.go b/filter/filter.go new file mode 100644 index 0000000..e65673f --- /dev/null +++ b/filter/filter.go @@ -0,0 +1,875 @@ +package filter + +import ( + "encoding/json" + "fmt" + "github.com/andrepxx/go-dsp-guitar/fft" + "github.com/andrepxx/go-dsp-guitar/resample" + "github.com/andrepxx/go-dsp-guitar/wave" + "io/ioutil" + "math" + "math/cmplx" + "strconv" +) + +/* + * Global constants. + */ +const ( + CHANNEL_COUNT = 1 +) + +/* + * Global variables. + */ +var g_sampleRates = []uint32{ + 22050, + 32000, + 44100, + 48000, + 88200, + 96000, + 192000, +} + +/* + * Data structure describing an FIR filter. + */ +type filterDescriptorStruct struct { + Name string + Path string + Compensation int32 +} + +/* + * Data structure containing the coefficients for an FIR filter. + */ +type impulseResponseStruct struct { + name string + sampleRate uint32 + gainCompensation float64 + data []float64 +} + +/* + * A collection of impulse responses. + */ +type impulseResponsesStruct struct { + responses []impulseResponseStruct +} + +/* + * Interface type representing a collection of impulse responses. + */ +type ImpulseResponses interface { + CreateFilter(name string, sampleRate uint32) Filter + Names() []string +} + +/* + * Data structure implementing an FIR filter. + */ +type filterStruct struct { + impulseResponse impulseResponseStruct + filterComplex []complex128 + filteredComplex []complex128 + inputBuffer []float64 + inputBufferComplex []complex128 + outputBuffer []float64 + outputBufferComplex []complex128 + tailBuffer []float64 +} + +/* + * Interface type representing an FIR filter. + */ +type Filter interface { + Add(other Filter) (Filter, error) + Coefficients() []float64 + Multiply(scalar float64) Filter + Normalize() Filter + Process(inputBuffer []float64, outputBuffer []float64) error + Reduce(order uint32) Filter + SampleRate() uint32 +} + +/* + * Convert a buffer of floating-point numbers to a buffer of complex numbers. + */ +func floatToComplex(out []complex128, in []float64) error { + M := len(in) + N := len(out) + + /* + * Verify that buffers are the same size. + */ + if M != N { + return fmt.Errorf("%s", "Failed to convert float to complex: Input and output buffers must be the same size.") + } else { + + /* + * Iterate over the buffer and fill in the real values. + */ + for i, val := range in { + out[i] = complex(val, 0.0) + } + + return nil + } + +} + +/* + * Convert a buffer of complex numbers to a buffer of floating-point numbers. + */ +func complexToFloat(out []float64, in []complex128) error { + M := len(in) + N := len(out) + + /* + * Verify that buffers are the same size. + */ + if M != N { + return fmt.Errorf("%s", "Failed to convert float to complex: Input and output buffers must be the same size.") + } else { + + /* + * Iterate over the buffer and extract the real values. + */ + for i, val := range in { + out[i] = real(val) + } + + return nil + } + +} + +/* + * Calculate the complex hadamard product of two vectors. + */ +func hadamardComplex(result []complex128, a []complex128, b []complex128) error { + L := len(result) + N := len(a) + M := len(b) + + /* + * Check if buffers are the same size. + */ + if (N != M) || (L != N) { + return fmt.Errorf("%s", "Failed to calculate hadamard product: All buffers must be the same size.") + } else { + + /* + * Multiply the contents of the buffer + */ + for i, _ := range result { + result[i] = a[i] * b[i] + } + + return nil + } + +} + +/* + * Estimate the gain of an FIR filter. + */ +func estimateGain(coefficients []float64) float64 { + sum := 0.0 + + /* + * Sum the squares of the filter coefficients. + */ + for _, coefficient := range coefficients { + sum += coefficient * coefficient + } + + return math.Sqrt(sum) +} + +/* + * Find the maximum absolute value in a vector. + */ +func peakValue(vec []float64) float64 { + peak := 0.0 + + /* + * Iterate over all values in the vector. + */ + for _, value := range vec { + abs := math.Abs(value) + + /* + * If we found a larger absolute value, keep it. + */ + if abs > peak { + peak = abs + } + + } + + return peak +} + +/* + * Add another filter to this one. + */ +func (this *filterStruct) Add(other Filter) (Filter, error) { + + /* + * Check if the filter to be added is nil. + */ + if other == nil { + return this, nil + } else { + otherStruct := other.(*filterStruct) + irA := this.impulseResponse + irB := otherStruct.impulseResponse + rateA := irA.sampleRate + rateB := irB.sampleRate + + /* + * Only filters with the same sample rate can be added. + */ + if rateA != rateB { + return nil, fmt.Errorf("%s", "Cannot add filters: Sample rates do not match.") + } else { + coeffsA := irA.data + coeffsB := irB.data + nA := len(coeffsA) + nB := len(coeffsB) + nResult := nA + + /* + * Check if the other filter is larger. + */ + if nB > nResult { + nResult = nB + } + + coeffsResult := make([]float64, nResult) + copy(coeffsResult, coeffsA) + + /* + * Add filter coefficients of other filter. + */ + for i, coeff := range coeffsB { + coeffsResult[i] += coeff + } + + nameA := irA.name + nameB := irB.name + nameResult := nameA + " + " + nameB + + /* + * Create the resulting impulse response. + */ + irResult := impulseResponseStruct{ + name: nameResult, + sampleRate: rateA, + gainCompensation: 0.0, + data: coeffsResult, + } + + bufFilterC := make([]complex128, 0) + bufFilteredC := make([]complex128, 0) + bufInput := make([]float64, 0) + bufInputC := make([]complex128, 0) + bufOutput := make([]float64, 0) + bufOutputC := make([]complex128, 0) + bufTail := make([]float64, 0) + + /* + * Create a new filter. + */ + fltFilter := filterStruct{ + impulseResponse: irResult, + filterComplex: bufFilterC, + filteredComplex: bufFilteredC, + inputBuffer: bufInput, + inputBufferComplex: bufInputC, + outputBuffer: bufOutput, + outputBufferComplex: bufOutputC, + tailBuffer: bufTail, + } + + return &fltFilter, nil + } + + } + +} + +/* + * Return filter coefficients. + */ +func (this *filterStruct) Coefficients() []float64 { + coeff := this.impulseResponse.data + size := len(coeff) + coeffCopy := make([]float64, size) + copy(coeffCopy, coeff) + return coeffCopy +} + +/* + * Multiply the filter with a scalar factor. + */ +func (this *filterStruct) Multiply(scalar float64) Filter { + scalarString := strconv.FormatFloat(scalar, 'f', -1, 64) + ir := this.impulseResponse + coeffs := ir.data + n := len(coeffs) + coeffsResult := make([]float64, n) + + /* + * Calculate the new filter coefficients with compensated gain. + */ + for i, coeff := range coeffs { + coeffsResult[i] = scalar * coeff + } + + /* + * Create a new impulse response structure. + */ + irResult := impulseResponseStruct{ + name: scalarString + " * (" + ir.name + ")", + sampleRate: ir.sampleRate, + gainCompensation: 0.0, + data: coeffsResult, + } + + bufFilterC := make([]complex128, 0) + bufFilteredC := make([]complex128, 0) + bufInput := make([]float64, 0) + bufInputC := make([]complex128, 0) + bufOutput := make([]float64, 0) + bufOutputC := make([]complex128, 0) + bufTail := make([]float64, 0) + + /* + * Create a new filter. + */ + fltFilter := filterStruct{ + impulseResponse: irResult, + filterComplex: bufFilterC, + filteredComplex: bufFilteredC, + inputBuffer: bufInput, + inputBufferComplex: bufInputC, + outputBuffer: bufOutput, + outputBufferComplex: bufOutputC, + tailBuffer: bufTail, + } + + return &fltFilter +} + +/* + * Normalize the filter to compensate for gain. + */ +func (this *filterStruct) Normalize() Filter { + ir := this.impulseResponse + coeffs := ir.data + gain := estimateGain(coeffs) + compensation := ir.gainCompensation + fac := compensation / gain + fltFilter := this.Multiply(fac) + return fltFilter +} + +/* + * Reads samples from the input buffer, passes them through the filter and writes + * samples to the output buffer. + */ +func (this *filterStruct) Process(inputBuffer []float64, outputBuffer []float64) error { + N := len(inputBuffer) + M := len(outputBuffer) + + /* + * Check if output and input buffer are the same size. + */ + if M != N { + return fmt.Errorf("%s", "Output and input buffer must be of the same size.") + } else { + coefficients := this.impulseResponse.data + + /* + * Check if impulse response exists. + */ + if coefficients == nil { + return fmt.Errorf("%s", "Impulse response must not be nil.") + } else { + L := len(coefficients) + + /* + * Check if filter is empty. + */ + if L == 0 { + fft.ZeroFloat(outputBuffer) + } else { + N64 := uint64(N) + L64 := uint64(L) + Npower, _ := fft.NextPowerOfTwo(N64) + blockSize, _ := fft.NextPowerOfTwo(L64) + numBlocks := Npower / blockSize + overflow := Npower % blockSize + + /* + * If there is overflow, add another block. + */ + if overflow != 0 { + numBlocks++ + } + + /* + * Process each block + */ + for i := uint64(0); i < numBlocks; i++ { + fftSize64 := blockSize << 1 + fftSize := int(fftSize64) + + /* + * Pre-calculate the FFT of the filter. + */ + if len(this.filterComplex) != fftSize { + coefficientsPadded := make([]float64, fftSize) + copy(coefficientsPadded[0:L], coefficients) + this.filterComplex = make([]complex128, fftSize) + fft.RealFourier(coefficientsPadded, this.filterComplex, fft.SCALING_DEFAULT) + } + + /* + * Check if complex-valued filtered (FFT) buffer is of correct size. + */ + if len(this.filteredComplex) != fftSize { + this.filteredComplex = make([]complex128, fftSize) + } + + /* + * Check if real-valued input buffer is of the correct size. + */ + if len(this.inputBuffer) != fftSize { + this.inputBuffer = make([]float64, fftSize) + } + + /* + * Check if real-valued output buffer is of the correct size. + */ + if len(this.outputBuffer) != fftSize { + this.outputBuffer = make([]float64, fftSize) + } + + /* + * Check if real-valued tail buffer is of the correct size. + */ + if len(this.tailBuffer) != fftSize { + this.tailBuffer = make([]float64, fftSize) + } + + lBound := i * blockSize + uBound := lBound + blockSize + + /* + * Prevent exceeding upper bound. + */ + if uBound > N64 { + uBound = N64 + } + + currentInputBuffer := inputBuffer[lBound:uBound] + currentOutputBuffer := outputBuffer[lBound:uBound] + numSamples := uBound - lBound + copy(this.inputBuffer[0:numSamples], currentInputBuffer) + fft.ZeroFloat(this.inputBuffer[numSamples:]) + fft.RealFourier(this.inputBuffer, this.filteredComplex, fft.SCALING_DEFAULT) + err := hadamardComplex(this.filteredComplex, this.filteredComplex, this.filterComplex) + + /* + * Check if hadamard product was calculated successfully. + */ + if err != nil { + return err + } else { + fft.RealInverseFourier(this.filteredComplex, this.outputBuffer, fft.SCALING_DEFAULT) + + /* + * Calculate the total output by overlapping with the tail of the + * previous calculation. + */ + for j, elem := range this.outputBuffer { + tailElem := this.tailBuffer[j] + pre := elem + tailElem + j64 := uint64(j) + + /* + * Write a portion to the current output buffer + * and update tail buffer. + */ + if j64 < numSamples { + + /* + * Ensure that the output is in range. + */ + if pre > 1.0 { + currentOutputBuffer[j] = 1.0 + } else if pre < -1.0 { + currentOutputBuffer[j] = -1.0 + } else { + currentOutputBuffer[j] = pre + } + + } else { + idx := j64 - numSamples + this.tailBuffer[idx] = pre + } + + } + + tailSize64 := fftSize64 - numSamples + fft.ZeroFloat(this.tailBuffer[tailSize64:]) + } + + } + + } + + return nil + } + + } + +} + +/* + * Approximate this filter by one of the given order. + */ +func (this *filterStruct) Reduce(order uint32) Filter { + ir := this.impulseResponse + coefficients := ir.data + orderWord := uint64(order) + n := len(coefficients) + nWord := uint64(n) + nFftSource, _ := fft.NextPowerOfTwo(nWord) + nFftTarget, _ := fft.NextPowerOfTwo(orderWord) + coefficientsPadded := make([]float64, nFftSource) + copy(coefficientsPadded, coefficients) + nFftSourceWord := uint32(nFftSource) + nFftTargetWord := uint32(nFftTarget) + + /* + * Check if the requested order is exceeded. + */ + if nWord <= orderWord { + return this + } else { + fr := make([]complex128, nFftSource) + floatToComplex(fr, coefficientsPadded) + fft.Fourier(fr, fft.SCALING_DEFAULT, fft.MODE_INPLACE) + numPositiveFreqsSource := (nFftSourceWord >> 1) + 1 + frPos := fr[:numPositiveFreqsSource] + nFftTargetHalf := nFftTargetWord >> 1 + numPositiveFreqsTarget := nFftTargetHalf + 1 + frPosNew := resample.Frequency(frPos, numPositiveFreqsTarget) + frNew := make([]complex128, nFftTarget) + copy(frNew, frPosNew) + + /* + * Generate negative frequency values. + */ + for i := uint32(1); i < nFftTargetHalf; i++ { + elem := frPosNew[i] + elemConj := cmplx.Conj(elem) + idx := nFftTargetWord - i + frNew[idx] = elemConj + } + + fft.InverseFourier(frNew, fft.SCALING_DEFAULT, fft.MODE_INPLACE) + targetResponse := make([]float64, nFftTarget) + complexToFloat(targetResponse, frNew) + coeffsNew := targetResponse[:order] + nameNew := ir.name + " (" + string(order) + ")" + rate := ir.sampleRate + compensation := ir.gainCompensation + + /* + * Create a new impulse response structure. + */ + irNew := impulseResponseStruct{ + name: nameNew, + gainCompensation: compensation, + sampleRate: rate, + data: coeffsNew, + } + + bufFilterC := make([]complex128, 0) + bufFilteredC := make([]complex128, 0) + bufInput := make([]float64, 0) + bufInputC := make([]complex128, 0) + bufOutput := make([]float64, 0) + bufOutputC := make([]complex128, 0) + bufTail := make([]float64, 0) + + /* + * Create a new filter. + */ + fltFilter := filterStruct{ + impulseResponse: irNew, + filterComplex: bufFilterC, + filteredComplex: bufFilteredC, + inputBuffer: bufInput, + inputBufferComplex: bufInputC, + outputBuffer: bufOutput, + outputBufferComplex: bufOutputC, + tailBuffer: bufTail, + } + + return &fltFilter + } + +} + +/* + * Returns the sample rate this filter is designed to operate at. + */ +func (this *filterStruct) SampleRate() uint32 { + ir := this.impulseResponse + return ir.sampleRate +} + +/* + * Retrieves an impulse response filter from a collection of impulse responses and + * creates an FIR filter from it. + */ +func (this *impulseResponsesStruct) CreateFilter(name string, sampleRate uint32) Filter { + + /* + * Iterate over the filter collection. + */ + for _, ir := range this.responses { + + /* + * Check if both name and sample rate match. + */ + if (ir.name == name) && (ir.sampleRate == sampleRate) { + bufFilterC := make([]complex128, 0) + bufFilteredC := make([]complex128, 0) + bufInput := make([]float64, 0) + bufInputC := make([]complex128, 0) + bufOutput := make([]float64, 0) + bufOutputC := make([]complex128, 0) + bufTail := make([]float64, 0) + + /* + * Create a new filter. + */ + fltFilter := filterStruct{ + impulseResponse: ir, + filterComplex: bufFilterC, + filteredComplex: bufFilteredC, + inputBuffer: bufInput, + inputBufferComplex: bufInputC, + outputBuffer: bufOutput, + outputBufferComplex: bufOutputC, + tailBuffer: bufTail, + } + + return &fltFilter + } + + } + + return nil +} + +/* + * Retrieves the names of all impulse responses. + */ +func (this *impulseResponsesStruct) Names() []string { + names := make([]string, 0) + + /* + * Iterate over the filter collection. + */ + for _, ir := range this.responses { + name := ir.name + contained := false + + /* + * Iterate over the names to check whether it's still there. + */ + for _, currentName := range names { + + /* + * If names match, we already know a version of this impulse response. + */ + if currentName == name { + contained = true + } + + } + + /* + * If this name is not already known, add it to the list. + */ + if !contained { + names = append(names, name) + } + + } + + return names +} + +/* + * Imports a set of impulse responses using a descriptor file. + */ +func Import(descriptorFilePath string) (ImpulseResponses, error) { + content, err := ioutil.ReadFile(descriptorFilePath) + + /* + * Check if file could be red. + */ + if err != nil { + return nil, fmt.Errorf("Failed to read descriptor file: '%s'", descriptorFilePath) + } else { + descriptors := []filterDescriptorStruct{} + err = json.Unmarshal(content, &descriptors) + + /* + * Check if file failed to unmarshal. + */ + if err != nil { + return nil, fmt.Errorf("Failed to decode descriptor file: '%s'", descriptorFilePath) + } else { + impulseResponseList := []impulseResponseStruct{} + + /* + * Iterate over all filter descriptors and load the corresponding + * FIR filter coefficients. + */ + for _, descriptor := range descriptors { + filterName := descriptor.Name + wavePath := descriptor.Path + dc := descriptor.Compensation + dcFloat := float64(dc) + compensation := 0.05 * dcFloat + fac := math.Pow(10.0, compensation) + waveBuffer, err := ioutil.ReadFile(wavePath) + + /* + * Check if file was read successfully. + */ + if err != nil { + fmt.Printf("WARNING: During filter import: Could not read file '%s'. - Skipping.\n", wavePath) + } else { + waveFile, err := wave.FromBuffer(waveBuffer) + + /* + * Check if file was parsed successfully. + */ + if err != nil { + fmt.Printf("WARNING: During filter import (file '%s'): %s\n", wavePath, err.Error()) + } else { + channelCount := waveFile.ChannelCount() + + /* + * An FIR filter should have exactly one channel. + */ + if channelCount != CHANNEL_COUNT { + fmt.Printf("WARNING: During filter import: File '%s' contains %d channels, expected: %d - Skipping.\n", wavePath, channelCount, CHANNEL_COUNT) + } else { + sampleRate := waveFile.SampleRate() + channel, _ := waveFile.Channel(0) + content := channel.Floats() + + /* + * Iterate over the supported sample rates. + */ + for _, targetSampleRate := range g_sampleRates { + coefficients := resample.Time(content, sampleRate, targetSampleRate) + + /* + * Create impulse response structure. + */ + ir := impulseResponseStruct{ + name: filterName, + gainCompensation: fac, + sampleRate: targetSampleRate, + data: coefficients, + } + + impulseResponseList = append(impulseResponseList, ir) + } + + } + + } + + } + + } + + /* + * Create data structure for impulse responses. + */ + impulseResponses := impulseResponsesStruct{ + responses: impulseResponseList, + } + + return &impulseResponses, nil + } + + } + +} + +/* + * Create an empty filter, which does not pass any signal. + */ +func Empty(sampleRate uint32) Filter { + coeffs := make([]float64, 0) + + /* + * Create impulse response. + */ + ir := impulseResponseStruct{ + name: "(EMPTY)", + gainCompensation: 0.0, + sampleRate: sampleRate, + data: coeffs, + } + + bufFilterC := make([]complex128, 0) + bufFilteredC := make([]complex128, 0) + bufInput := make([]float64, 0) + bufInputC := make([]complex128, 0) + bufOutput := make([]float64, 0) + bufOutputC := make([]complex128, 0) + bufTail := make([]float64, 0) + + /* + * Create a new filter. + */ + fltFilter := filterStruct{ + impulseResponse: ir, + filterComplex: bufFilterC, + filteredComplex: bufFilteredC, + inputBuffer: bufInput, + inputBufferComplex: bufInputC, + outputBuffer: bufOutput, + outputBufferComplex: bufOutputC, + tailBuffer: bufTail, + } + + return &fltFilter +} + +/* + * Returns the supported sample rates. + */ +func SampleRates() []uint32 { + numRates := len(g_sampleRates) + sampleRates := make([]uint32, numRates) + copy(sampleRates, g_sampleRates) + return sampleRates +} diff --git a/hwio/hwio.go b/hwio/hwio.go new file mode 100644 index 0000000..6ecb1af --- /dev/null +++ b/hwio/hwio.go @@ -0,0 +1,439 @@ +package hwio + +import ( + "fmt" + "github.com/andrepxx/go-jack" + "strconv" + "sync" +) + +/* + * Function pointer for implementing signal processors. + */ +type Processor func([][]float64, [][]float64, uint32) + +/* + * Function pointer for implementing sample rate listeners. + */ +type SampleRateListener func(uint32) + +/* + * Data structure representing a handle to a hardware input and output and an + * associated signal processor. + */ +type Binding struct { + inputs []*jack.Port + outputs []*jack.Port + processor Processor + listener SampleRateListener +} + +/* + * Global constants. + */ +const ( + INPUT_CHANNELS = 2 + OUTPUT_CHANNELS = INPUT_CHANNELS + 3 +) + +/* + * Global variables. + */ +var g_client *jack.Client // JACK client handle. +var g_mutex sync.RWMutex // Mutex for bindings. +var g_bindings []*Binding = nil // All currently active bindings. +var g_inputBuffers [][]float64 // Input buffers. +var g_outputBuffers [][]float64 // Output buffers. +var g_sampleRate uint32 // Sample rate. + +/* + * Convert audio samples to floating-point numbers. + */ +func samplesToFloats(in []jack.AudioSample, out []float64) error { + + /* + * Verify that the output buffer has an appropriate size + */ + if len(out) < len(in) { + return fmt.Errorf("%s", "Cannot convert samples to floats: Output buffer is too small.") + } else { + + /* + * Convert each audio sample to a floating-point number. + */ + for i, sample := range in { + out[i] = float64(sample) + } + + return nil + } + +} + +/* + * Convert floating-point numbers to audio samples. + */ +func floatsToSamples(in []float64, out []jack.AudioSample) error { + + /* + * Verify that the output buffer has an appropriate size + */ + if len(out) < len(in) { + return fmt.Errorf("%s", "Cannot convert floats to samples: Output buffer is too small.") + } else { + + /* + * Convert each floating-point number to an audio sample. + */ + for i, sample := range in { + out[i] = jack.AudioSample(sample) + } + + return nil + } + +} + +/* + * Interrupt handler called when the hardware has audio to process. + */ +func process(nframes uint32) int { + g_mutex.RLock() + + /* + * Process audio for each binding. + */ + for _, binding := range g_bindings { + inputs := binding.inputs + outputs := binding.outputs + + /* + * Read audio from each input channel. + */ + for i, input := range inputs { + hwInputBuffer := input.GetBuffer(nframes) + bufferSize := len(hwInputBuffer) + + /* + * Ensure the size of the current input buffer matches the size of the hardware buffer. + */ + if len(g_inputBuffers[i]) != bufferSize { + g_inputBuffers[i] = make([]float64, bufferSize) + } + + err := samplesToFloats(hwInputBuffer, g_inputBuffers[i]) + + /* + * If conversion failed, log error, otherwise perform processing. + */ + if err != nil { + msg := err.Error() + fmt.Printf("Error in real-time thread: %s", msg) + } + + } + + /* + * Prepare output buffer for each output channel. + */ + for i, output := range outputs { + hwOutputBuffer := output.GetBuffer(nframes) + bufferSize := len(hwOutputBuffer) + + /* + * Ensure the size of the current output buffer matches the size of the hardware buffer. + */ + if len(g_outputBuffers[i]) != bufferSize { + g_outputBuffers[i] = make([]float64, bufferSize) + } + + } + + binding.processor(g_inputBuffers, g_outputBuffers, g_sampleRate) + + /* + * Write audio to each output channel. + */ + for i, output := range outputs { + hwOutputBuffer := output.GetBuffer(nframes) + err := floatsToSamples(g_outputBuffers[i], hwOutputBuffer) + + /* + * If conversion failed, log error. + */ + if err != nil { + msg := err.Error() + fmt.Printf("Error in real-time thread: %s", msg) + } + + } + + } + + g_mutex.RUnlock() + return 0 +} + +/* + * Interrupt handler called when the hardware adjusts the sample rate. + */ +func sampleRate(rate uint32) int { + g_sampleRate = rate + + /* + * Notify each binding about the change. + */ + for _, binding := range g_bindings { + binding.listener(rate) + } + + return 0 +} + +/* + * Initialize the hardware for signal processing. + */ +func initialize() (*jack.Client, error) { + client, _ := jack.ClientOpen("go-dsp-guitar", jack.NoStartServer) + + /* + * Check if we are connected to the JACK server. + */ + if client == nil { + return nil, fmt.Errorf("%s", "Could not connect to JACK server.") + } else { + statusProcess := client.SetProcessCallback(process) + + /* + * Check if we could register our application as a signal processor. + */ + if statusProcess != 0 { + return nil, fmt.Errorf("%s", "Failed to set process callback.") + } else { + statusSampleRate := client.SetSampleRateCallback(sampleRate) + + /* + * Check if we could register a sample rate callback. + */ + if statusSampleRate != 0 { + return nil, fmt.Errorf("%s", "Failed to set sample rate callback.") + } else { + statusActivate := client.Activate() + + /* + * Check if we could activate JACK. + */ + if statusActivate != 0 { + return nil, fmt.Errorf("%s", "Failed to activate client.") + } else { + return client, nil + } + + } + + } + + } + +} + +/* + * Get DSP load. + */ +func DSPLoad() float32 { + res := float32(0.0) + g_mutex.RLock() + + /* + * Check if client is registered. + */ + if g_client != nil { + res = g_client.CPULoad() + } + + g_mutex.RUnlock() + return res +} + +/* + * Get frames per period. + */ +func FramesPerPeriod() uint32 { + res := uint32(0) + g_mutex.RLock() + + /* + * Check if client is registered. + */ + if g_client != nil { + res = g_client.GetBufferSize() + } + + g_mutex.RUnlock() + return res +} + +/* + * Register a binding to a hardware interface. + */ +func Register(processor Processor, listener SampleRateListener) (*Binding, error) { + err := error(nil) + g_mutex.RLock() + + /* + * If no bindings exist yet, initialize hardware first. + */ + if g_bindings == nil { + g_mutex.RUnlock() + g_mutex.Lock() + g_client, err = initialize() + g_bindings = []*Binding{} + g_inputBuffers = make([][]float64, INPUT_CHANNELS) + g_outputBuffers = make([][]float64, OUTPUT_CHANNELS) + g_mutex.Unlock() + g_mutex.RLock() + } + + g_mutex.RUnlock() + + /* + * Check, whether hardware was initialized successfully. + */ + if err != nil { + return nil, err + } else { + inputs := make([]*jack.Port, INPUT_CHANNELS) + outputs := make([]*jack.Port, OUTPUT_CHANNELS) + + /* + * Create input and output for each input channel. + */ + for idx, _ := range inputs { + idxLong := int64(idx) + sChannelNumber := strconv.FormatInt(idxLong, 10) + inputName := "in_" + sChannelNumber + inputs[idx] = g_client.PortRegister(inputName, jack.DEFAULT_AUDIO_TYPE, jack.PortIsInput, 0) + outputName := "out_" + sChannelNumber + outputs[idx] = g_client.PortRegister(outputName, jack.DEFAULT_AUDIO_TYPE, jack.PortIsOutput, 0) + } + + /* + * Names of additional channels to register. + */ + additionalChannels := []string{ + "master_left", + "master_right", + "metronome", + } + + nAdditional := len(additionalChannels) + baseIdx := OUTPUT_CHANNELS - nAdditional + + /* + * Register additional channels. + */ + for i, additionalChannel := range additionalChannels { + idx := baseIdx + i + outputs[idx] = g_client.PortRegister(additionalChannel, jack.DEFAULT_AUDIO_TYPE, jack.PortIsOutput, 0) + } + + /* + * Create hardware binding. + */ + binding := &Binding{ + inputs: inputs, + outputs: outputs, + processor: processor, + listener: listener, + } + + g_mutex.Lock() + g_bindings = append(g_bindings, binding) + g_mutex.Unlock() + sampleRate(g_sampleRate) + return binding, nil + } + +} + +/* + * Set frames per period. + */ +func SetFramesPerPeriod(n uint32) { + g_mutex.RLock() + + /* + * Check if client is registered. + */ + if g_client != nil { + g_client.SetBufferSize(n) + } + + g_mutex.RUnlock() +} + +/* + * Unregister a binding to a hardware interface. + */ +func Unregister(binding *Binding) { + idx := int(-1) + g_mutex.RLock() + + /* + * Iterate over the bindings. + */ + for i, b := range g_bindings { + + /* + * Check if we have the binding we're about to remove. + */ + if b == binding { + idx = i + } + + } + + /* + * If we found the binding, remove it. + */ + if idx > 0 { + inputs := binding.inputs + outputs := binding.outputs + idxInc := idx + 1 + g_mutex.RUnlock() + g_mutex.Lock() + + /* + * Unregister all input ports. + */ + for _, port := range inputs { + g_client.PortUnregister(port) + } + + /* + * Unregister all output ports. + */ + for _, port := range outputs { + g_client.PortUnregister(port) + } + + g_bindings = append(g_bindings[:idx], g_bindings[idxInc:]...) + g_mutex.Unlock() + g_mutex.RLock() + } + + /* + * If no bindings exist, terminate connection to JACK. + */ + if len(g_bindings) == 0 { + g_mutex.RUnlock() + g_mutex.Lock() + g_client.Close() + g_client = nil + g_bindings = nil + g_mutex.Unlock() + g_mutex.RLock() + } + + g_mutex.RUnlock() +} diff --git a/ir/guitar/bm1-neutral.wav b/ir/guitar/bm1-neutral.wav new file mode 100644 index 0000000..9eceeff Binary files /dev/null and b/ir/guitar/bm1-neutral.wav differ diff --git a/ir/guitar/brit-m-center.wav b/ir/guitar/brit-m-center.wav new file mode 100644 index 0000000..9be73a6 Binary files /dev/null and b/ir/guitar/brit-m-center.wav differ diff --git a/ir/guitar/brit-m-classic.wav b/ir/guitar/brit-m-classic.wav new file mode 100644 index 0000000..d0cba3a Binary files /dev/null and b/ir/guitar/brit-m-classic.wav differ diff --git a/ir/guitar/brit-m-offax.wav b/ir/guitar/brit-m-offax.wav new file mode 100644 index 0000000..cdbfa43 Binary files /dev/null and b/ir/guitar/brit-m-offax.wav differ diff --git a/ir/guitar/custom-bp-neutral.wav b/ir/guitar/custom-bp-neutral.wav new file mode 100644 index 0000000..00af332 Binary files /dev/null and b/ir/guitar/custom-bp-neutral.wav differ diff --git a/ir/guitar/custom-hc450-neutral.wav b/ir/guitar/custom-hc450-neutral.wav new file mode 100644 index 0000000..62546c0 Binary files /dev/null and b/ir/guitar/custom-hc450-neutral.wav differ diff --git a/ir/guitar/rfier-center.wav b/ir/guitar/rfier-center.wav new file mode 100644 index 0000000..9d3c4ab Binary files /dev/null and b/ir/guitar/rfier-center.wav differ diff --git a/ir/guitar/rfier-classic.wav b/ir/guitar/rfier-classic.wav new file mode 100644 index 0000000..0f9e58f Binary files /dev/null and b/ir/guitar/rfier-classic.wav differ diff --git a/ir/guitar/rfier-offax.wav b/ir/guitar/rfier-offax.wav new file mode 100644 index 0000000..fede251 Binary files /dev/null and b/ir/guitar/rfier-offax.wav differ diff --git a/ir/guitar/tweed-center.wav b/ir/guitar/tweed-center.wav new file mode 100644 index 0000000..5345817 Binary files /dev/null and b/ir/guitar/tweed-center.wav differ diff --git a/ir/guitar/tweed-classic.wav b/ir/guitar/tweed-classic.wav new file mode 100644 index 0000000..aa293b4 Binary files /dev/null and b/ir/guitar/tweed-classic.wav differ diff --git a/ir/guitar/tweed-offax.wav b/ir/guitar/tweed-offax.wav new file mode 100644 index 0000000..28c54ab Binary files /dev/null and b/ir/guitar/tweed-offax.wav differ diff --git a/ir/guitar/vh4-neutral.wav b/ir/guitar/vh4-neutral.wav new file mode 100644 index 0000000..5c36e51 Binary files /dev/null and b/ir/guitar/vh4-neutral.wav differ diff --git a/ir/index.json b/ir/index.json new file mode 100644 index 0000000..c4f6662 --- /dev/null +++ b/ir/index.json @@ -0,0 +1,108 @@ +[ + { + "Name": "Guitar: American Modern (Center)", + "Path": "ir/guitar/rfier-center.wav", + "Compensation": -25 + }, + { + "Name": "Guitar: American Modern (Classic)", + "Path": "ir/guitar/rfier-classic.wav", + "Compensation": -25 + }, + { + "Name": "Guitar: American Modern (Off-Axis)", + "Path": "ir/guitar/rfier-offax.wav", + "Compensation": -25 + }, + { + "Name": "Guitar: American Vintage (Center)", + "Path": "ir/guitar/tweed-center.wav", + "Compensation": -20 + }, + { + "Name": "Guitar: American Vintage (Classic)", + "Path": "ir/guitar/tweed-classic.wav", + "Compensation": -20 + }, + { + "Name": "Guitar: American Vintage (Off-Axis)", + "Path": "ir/guitar/tweed-offax.wav", + "Compensation": -20 + }, + { + "Name": "Guitar: British Modern (Center)", + "Path": "ir/guitar/brit-m-center.wav", + "Compensation": -20 + }, + { + "Name": "Guitar: British Modern (Classic)", + "Path": "ir/guitar/brit-m-classic.wav", + "Compensation": -20 + }, + { + "Name": "Guitar: British Modern (Off-Axis)", + "Path": "ir/guitar/brit-m-offax.wav", + "Compensation": -20 + }, + { + "Name": "Guitar: Custom BP", + "Path": "ir/guitar/custom-bp-neutral.wav", + "Compensation": -25 + }, + { + "Name": "Guitar: Custom HC-450", + "Path": "ir/guitar/custom-hc450-neutral.wav", + "Compensation": -25 + }, + { + "Name": "Guitar: German Modern", + "Path": "ir/guitar/vh4-neutral.wav", + "Compensation": -20 + }, + { + "Name": "Guitar: Scandinavian Modern", + "Path": "ir/guitar/bm1-neutral.wav", + "Compensation": -20 + }, + { + "Name": "PA: HC 2-way (High)", + "Path": "ir/pa/hc-2way-high.wav", + "Compensation": -10 + }, + { + "Name": "PA: HC 2-way (Low)", + "Path": "ir/pa/hc-2way-low.wav", + "Compensation": -30 + }, + { + "Name": "PA: HC 4-way (High)", + "Path": "ir/pa/hc-4way-high.wav", + "Compensation": -10 + }, + { + "Name": "PA: HC 4-way (Mid)", + "Path": "ir/pa/hc-4way-mid.wav", + "Compensation": -30 + }, + { + "Name": "PA: HC 4-way (Low)", + "Path": "ir/pa/hc-4way-low.wav", + "Compensation": -30 + }, + { + "Name": "PA: HC 4-way (Sub)", + "Path": "ir/pa/hc-4way-sub.wav", + "Compensation": -30 + }, + { + "Name": "PA: ISP 2-way (High)", + "Path": "ir/pa/isp-2way-high.wav", + "Compensation": -10 + }, + { + "Name": "PA: ISP 2-way (Low)", + "Path": "ir/pa/isp-2way-low.wav", + "Compensation": -30 + } +] + diff --git a/ir/pa/hc-2way-high.wav b/ir/pa/hc-2way-high.wav new file mode 100644 index 0000000..8ac12e2 Binary files /dev/null and b/ir/pa/hc-2way-high.wav differ diff --git a/ir/pa/hc-2way-low.wav b/ir/pa/hc-2way-low.wav new file mode 100644 index 0000000..a8300d3 Binary files /dev/null and b/ir/pa/hc-2way-low.wav differ diff --git a/ir/pa/hc-4way-high.wav b/ir/pa/hc-4way-high.wav new file mode 100644 index 0000000..ac8ec8d Binary files /dev/null and b/ir/pa/hc-4way-high.wav differ diff --git a/ir/pa/hc-4way-low.wav b/ir/pa/hc-4way-low.wav new file mode 100644 index 0000000..5ce356c Binary files /dev/null and b/ir/pa/hc-4way-low.wav differ diff --git a/ir/pa/hc-4way-mid.wav b/ir/pa/hc-4way-mid.wav new file mode 100644 index 0000000..75c9f89 Binary files /dev/null and b/ir/pa/hc-4way-mid.wav differ diff --git a/ir/pa/hc-4way-sub.wav b/ir/pa/hc-4way-sub.wav new file mode 100644 index 0000000..a671cf3 Binary files /dev/null and b/ir/pa/hc-4way-sub.wav differ diff --git a/ir/pa/isp-2way-high.wav b/ir/pa/isp-2way-high.wav new file mode 100644 index 0000000..1407657 Binary files /dev/null and b/ir/pa/isp-2way-high.wav differ diff --git a/ir/pa/isp-2way-low.wav b/ir/pa/isp-2way-low.wav new file mode 100644 index 0000000..20a7223 Binary files /dev/null and b/ir/pa/isp-2way-low.wav differ diff --git a/level/level.go b/level/level.go new file mode 100644 index 0000000..36fa3a2 --- /dev/null +++ b/level/level.go @@ -0,0 +1,189 @@ +package level + +import ( + "math" + "sync" +) + +/* + * Global constants. + */ +const ( + PEAK_HOLD_TIME_SECONDS = 2 + TIME_CONSTANT = 1.7 // DIN IEC 60268-18 + MIN_LEVEL = -200.0 + OUTPUT_COUNT = 1 +) + +/* + * Data structure representing the result of a level analysis. + */ +type resultStruct struct { + level int32 + peak int32 +} + +/* + * The result of a level analysis. + */ +type Result interface { + Level() int32 + Peak() int32 +} + +/* + * Data structure representing a level meter. + */ +type meterStruct struct { + mutex sync.RWMutex + currentValue float64 + peakValue float64 + sampleCounter uint64 +} + +/* + * Interface type representing a level meter. + */ +type Meter interface { + Process(inputBuffer []float64, sampleRate uint32) + Analyze() Result +} + +/* + * Turn a linear factor into a gain (or attenuation) value in decibels. + */ +func factorToDecibels(factor float64) float64 { + result := 20.0 * math.Log10(factor) + return result +} + +/* + * Returns the current signal level. + */ +func (this *resultStruct) Level() int32 { + value := this.level + return value +} + +/* + * Returns the current peak level. + */ +func (this *resultStruct) Peak() int32 { + value := this.peak + return value +} + +/* + * Feed the signal from an input buffer through the level meter. + */ +func (this *meterStruct) Process(inputBuffer []float64, sampleRate uint32) { + this.mutex.Lock() + currentValue := this.currentValue + peakValue := this.peakValue + sampleCounter := this.sampleCounter + sampleRateFloat := float64(sampleRate) + holdTimeSamples := uint64(PEAK_HOLD_TIME_SECONDS * sampleRateFloat) + decayExp := -1.0 / (TIME_CONSTANT * sampleRateFloat) + decayFactor := math.Pow(10.0, decayExp) + + /* + * Process each sample. + */ + for _, sample := range inputBuffer { + currentValue *= decayFactor + + /* + * If we're above the hold time, let the peak indicator decay, + * otherwise increment sample counter. + */ + if sampleCounter > holdTimeSamples { + peakValue *= decayFactor + } else { + sampleCounter++ + } + + sampleAbs := math.Abs(sample) + + /* + * If we got a sample with larger amplitude, update current value. + */ + if sampleAbs > currentValue { + currentValue = sampleAbs + } + + /* + * If we got a sample with larger or equal amplitude, update peak value. + */ + if sampleAbs >= peakValue { + peakValue = sampleAbs + sampleCounter = 0 + } + + } + + this.currentValue = currentValue + this.peakValue = peakValue + this.sampleCounter = sampleCounter + this.mutex.Unlock() +} + +/* + * Perform analysis of signal level. + */ +func (this *meterStruct) Analyze() Result { + this.mutex.RLock() + currentValue := this.currentValue + peakValue := this.peakValue + this.mutex.RUnlock() + currentLevel := factorToDecibels(currentValue) + currentLevelNaN := math.IsNaN(currentLevel) + + /* + * Ensure that the minimum level is not exceeded. + */ + if currentLevelNaN || currentLevel < MIN_LEVEL { + currentLevel = MIN_LEVEL + } + + currentLevelRounded := math.Round(currentLevel) + currentLevelInt := int32(currentLevelRounded) + peakLevel := factorToDecibels(peakValue) + peakLevelNaN := math.IsNaN(peakLevel) + + /* + * Ensure that the minimum level is not exceeded. + */ + if peakLevelNaN || peakLevel < MIN_LEVEL { + peakLevel = MIN_LEVEL + } + + peakLevelRounded := math.Round(peakLevel) + peakLevelInt := int32(peakLevelRounded) + + /* + * Create result structure. + */ + result := resultStruct{ + level: currentLevelInt, + peak: peakLevelInt, + } + + return &result +} + +/* + * Creates a new level meter. + */ +func CreateMeter() Meter { + + /* + * Create a new meter struct. + */ + m := meterStruct{ + currentValue: 0.0, + peakValue: 0.0, + sampleCounter: 0, + } + + return &m +} diff --git a/level/level_test.go b/level/level_test.go new file mode 100644 index 0000000..39ab90b --- /dev/null +++ b/level/level_test.go @@ -0,0 +1,55 @@ +package level + +import ( + "math" + "testing" +) + +const ( + DEFAULT_SAMPLE_RATE = 96000 + TESTING_FREQUENCY = 440 + TWO_PI = 2.0 * math.Pi +) + +/* + * Perform a unit test on the level meter. + */ +func TestMeter(t *testing.T) { + sampleRate := uint32(DEFAULT_SAMPLE_RATE) + sampleRateFloat := float64(sampleRate) + buf := make([]float64, sampleRate) + + /* + * Generate data series. + */ + for i := range buf { + iFloat := float64(i) + t := iFloat / sampleRateFloat + arg := TWO_PI * t + elem := math.Sin(arg) + buf[i] = elem + } + + m := CreateMeter() + m.Process(buf, sampleRate) + res := m.Analyze() + level := res.Level() + peak := res.Peak() + expectedLevel := int32(-3) + expectedPeak := int32(0) + + /* + * Check if the current level matches our expectations. + */ + if level != expectedLevel { + t.Errorf("Current level does not match! Expected %d, got %d.\n", expectedLevel, level) + } + + /* + * Check if the peak level matches our expectations. + */ + if peak != expectedPeak { + t.Errorf("Peak level does not match! Expected %d, got %d.\n", expectedPeak, peak) + } + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ec21765 --- /dev/null +++ b/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "flag" + "github.com/andrepxx/go-dsp-guitar/controller" +) + +/* + * The entry point of our program. + */ +func main() { + numChannels := flag.Int("channels", 0, "Number of channels for batch processing") + flag.Parse() + cn := controller.CreateController() + cn.Operate(*numChannels) +} diff --git a/metronome/metronome.go b/metronome/metronome.go new file mode 100644 index 0000000..30e232f --- /dev/null +++ b/metronome/metronome.go @@ -0,0 +1,282 @@ +package metronome + +import ( + "sync" +) + +/* + * Global constants. + */ +const ( + DEFAULT_SAMPLE_RATE = 96000 + OUTPUT_COUNT = 1 +) + +/* + * Data structure representing a metronome. + */ +type metronomeStruct struct { + sampleCounter uint32 + tickCounter uint32 + mutex sync.RWMutex + beatsPerPeriod uint32 + bpmSpeed uint32 + coefficientsTick []float64 + coefficientsTock []float64 + nameTick string + nameTock string + sampleRate uint32 +} + +/* + * Interface type representing a metronome. + */ +type Metronome interface { + BeatsPerPeriod() uint32 + Process(outputBuffer []float64) + SampleRate() uint32 + SetBeatsPerPeriod(count uint32) + SetSampleRate(rate uint32) + SetSpeed(speed uint32) + SetTick(name string, coefficients []float64) + SetTock(name string, coefficients []float64) + Tick() (string, []float64) + Tock() (string, []float64) + Speed() uint32 +} + +/* + * Returns the number of beats per period for this metronome. + */ +func (this *metronomeStruct) BeatsPerPeriod() uint32 { + this.mutex.RLock() + bpm := this.beatsPerPeriod + this.mutex.RUnlock() + return bpm +} + +/* + * Generates the metronome signal and writes it into a buffer. + */ +func (this *metronomeStruct) Process(outputBuffer []float64) { + this.mutex.RLock() + tickBuf := this.coefficientsTick + tockBuf := this.coefficientsTock + bpm := this.bpmSpeed + beatsPerPeriod := this.beatsPerPeriod + this.mutex.RUnlock() + + /* + * Prevent division by zero. + */ + if beatsPerPeriod == 0 { + beatsPerPeriod = 1 + } + + sampleCounter := this.sampleCounter + tickCounter := this.tickCounter + sampleRate := this.sampleRate + tickSize := len(tickBuf) + unsignedTickSize := uint32(tickSize) + tockSize := len(tockBuf) + unsignedTockSize := uint32(tockSize) + samplesPerBeat := (60 * sampleRate) / bpm + + /* + * Generate the output samples. + */ + for i, _ := range outputBuffer { + sample := float64(0.0) + + /* + * Decide whether a tick or a tock should be produced. + */ + if tickCounter == 0 { + + /* + * Check if buffer is allocated and part of the tick must be output. + */ + if (tickBuf != nil) && (sampleCounter < unsignedTickSize) { + sample = tickBuf[sampleCounter] + } + + } else { + + /* + * Check if buffer is allocated and part of the tock must be output. + */ + if (tockBuf != nil) && (sampleCounter < unsignedTockSize) { + sample = tockBuf[sampleCounter] + } + + } + + outputBuffer[i] = sample + sampleCounter++ + + /* + * Reset sample counter on every beat. + */ + if sampleCounter > samplesPerBeat { + sampleCounter = 0 + tickCounter = (tickCounter + 1) % beatsPerPeriod + } + + } + + this.sampleCounter = sampleCounter + this.tickCounter = tickCounter +} + +/* + * Return the sample rate the metronome is supposed to operate at. + */ +func (this *metronomeStruct) SampleRate() uint32 { + this.mutex.RLock() + rate := this.sampleRate + this.mutex.RUnlock() + return rate +} + +/* + * Sets the number of beats per period. + */ +func (this *metronomeStruct) SetBeatsPerPeriod(count uint32) { + this.mutex.Lock() + this.beatsPerPeriod = count + this.mutex.Unlock() +} + +/* + * Sets the sample rate. Note that the coefficients will also need to be + * updated on a sample rate change. + */ +func (this *metronomeStruct) SetSampleRate(rate uint32) { + this.mutex.Lock() + this.sampleRate = rate + this.mutex.Unlock() +} + +/* + * Sets the speed in beats per minute. + */ +func (this *metronomeStruct) SetSpeed(speed uint32) { + this.mutex.Lock() + this.bpmSpeed = speed + this.mutex.Unlock() +} + +/* + * Set the name and the coefficients for the 'tick' signal. + */ +func (this *metronomeStruct) SetTick(name string, coefficients []float64) { + this.mutex.Lock() + this.nameTick = name + + /* + * Check if coefficients were passed into the function. + */ + if coefficients == nil { + this.coefficientsTick = nil + } else { + size := len(coefficients) + + /* + * If size of the coefficient buffer does not match, allocate + * a new one. + */ + if size != len(this.coefficientsTick) { + this.coefficientsTick = make([]float64, size) + } + + copy(this.coefficientsTick, coefficients) + } + + this.mutex.Unlock() +} + +/* + * Set the name and the coefficients for the 'tock' signal. + */ +func (this *metronomeStruct) SetTock(name string, coefficients []float64) { + this.mutex.Lock() + this.nameTock = name + + /* + * Check if coefficients were passed into the function. + */ + if coefficients == nil { + this.coefficientsTock = nil + } else { + size := len(coefficients) + + /* + * If size of the coefficient buffer does not match, allocate + * a new one. + */ + if size != len(this.coefficientsTock) { + this.coefficientsTock = make([]float64, size) + } + + copy(this.coefficientsTock, coefficients) + } + + this.mutex.Unlock() +} + +/* + * Returns the name and the coefficients of the metronome 'tick' sound. + */ +func (this *metronomeStruct) Tick() (string, []float64) { + this.mutex.RLock() + coeffs := this.coefficientsTick + size := len(coeffs) + coeffsCopy := make([]float64, size) + copy(coeffsCopy, coeffs) + this.mutex.RUnlock() + return this.nameTick, coeffsCopy +} + +/* + * Returns the name and the coefficients of the metronome 'tock' sound. + */ +func (this *metronomeStruct) Tock() (string, []float64) { + this.mutex.RLock() + coeffs := this.coefficientsTock + size := len(coeffs) + coeffsCopy := make([]float64, size) + copy(coeffsCopy, coeffs) + this.mutex.RUnlock() + return this.nameTock, coeffsCopy +} + +/* + * Returns the metronome speed in beats per minute. + */ +func (this *metronomeStruct) Speed() uint32 { + this.mutex.RLock() + bpm := this.bpmSpeed + this.mutex.RUnlock() + return bpm +} + +/* + * Creates a new metronome. + */ +func Create() Metronome { + + /* + * Create a new metronome struct. + */ + m := metronomeStruct{ + beatsPerPeriod: 4, + bpmSpeed: 120, + coefficientsTick: nil, + coefficientsTock: nil, + sampleCounter: 0, + sampleRate: DEFAULT_SAMPLE_RATE, + tickCounter: 0, + } + + return &m +} diff --git a/random/random.go b/random/random.go new file mode 100644 index 0000000..bb4b0fe --- /dev/null +++ b/random/random.go @@ -0,0 +1,55 @@ +package random + +/* + * Interface type for a pseudo random number generator. + */ +type PseudoRandomNumberGenerator interface { + NextFloat() float64 +} + +/* + * Data structure representing a linear congruency generator. + */ +type linearCongruencyGenerator struct { + a uint64 + b uint64 + n uint64 + x uint64 +} + +/* + * Samples a new random number in the interval [0, 1] from a uniform distribution. + */ +func (this *linearCongruencyGenerator) NextFloat() float64 { + a := this.a + b := this.b + n := this.n + x := this.x + x = ((a * x) + b) % n + this.x = x + xFloat := float64(x) + xMax := n - 1 + xMaxFloat := float64(xMax) + result := xFloat / xMaxFloat + return result +} + +/* + * Creates a new pseudo random number generator. + */ +func CreatePRNG(seed uint64) PseudoRandomNumberGenerator { + n := uint64((1 << 31) - 1) + x := ((64979 * seed) + 83) % n + + /* + * Initialize a new LCG. + */ + generator := linearCongruencyGenerator{ + a: 16807, + b: 0, + n: n, + x: x, + } + + return &generator +} diff --git a/random/random_test.go b/random/random_test.go new file mode 100644 index 0000000..ed47815 --- /dev/null +++ b/random/random_test.go @@ -0,0 +1,112 @@ +package random + +import ( + "math" + "testing" +) + +/* + * Compare two real-valued slices to check whether their components are close. + */ +func areSlicesClose(a []float64, b []float64) (bool, []float64) { + + /* + * Check whether the two slices are of the same size. + */ + if len(a) != len(b) { + return false, nil + } else { + c := true + n := len(a) + diffs := make([]float64, n) + + /* + * Iterate over the arrays to compare values. + */ + for i, elem := range a { + diff := elem - b[i] + diffAbs := math.Abs(diff) + + /* + * Check if we found a significant difference. + */ + if diffAbs > 0.00000001 { + c = false + } + + diffs[i] = diff + } + + return c, diffs + } + +} + +/* + * Perform a unit test on the random number generator. + */ +func TestRNG(t *testing.T) { + + /* + * The seeds with which the PRNG is tested. + */ + seeds := []uint64{ + 0, + 1, + 1337, + 0xffffffffffffffff, + } + + /* + * The values we expect from the PRNG output. + */ + expectedOutputs := [][]float64{ + []float64{0.000649588648834814, 0.9176364163101058, 0.7152417425208183, 0.06796094967793762, 0.2196807053123421, 0.17361246531234353, 0.9047031462236337, 0.34577150023148534}, + []float64{0.5091992369938635, 0.11157217073400708, 0.1934726533419198, 0.6948832037811011, 0.9020005109738564, 0.92258087864386, 0.8168201472766885, 0.29620888670553347}, + []float64{0.931529109768131, 0.20974058258323053, 0.10996983489950173, 0.26301429538336984, 0.48126045007376045, 0.5443806234229176, 0.405133608640296, 0.08055724676750343}, + []float64{0.4921312462465197, 0.24985181377255528, 0.25943212002462906, 0.27563922365721244, 0.6684298498261998, 0.3004807977010317, 0.18076460965048952, 0.11079298109821321}, + } + + outputBuffer := make([]float64, 8) + + /* + * Initialize the PRNG with each of the seeds and obtain its output. + */ + for i, seed := range seeds { + rng := CreatePRNG(seed) + + /* + * Fill the output buffer with values from the PRNG. + */ + for i, _ := range outputBuffer { + outputBuffer[i] = rng.NextFloat() + } + + expectedOutput := expectedOutputs[i] + valid, diff := areSlicesClose(outputBuffer, expectedOutput) + + /* + * Check if we got the expected result. + */ + if !valid { + t.Errorf("PRNG test number %d: Result is incorrect. Seeded with %d, expected %v, got %v, difference: %v", i, seed, expectedOutput, outputBuffer, diff) + } + + /* + * Obtain 10k more values from the PRNG and verify that they are all within the unit interval. + */ + for j := 0; j < 10000; j++ { + value := rng.NextFloat() + + /* + * Check if the interval is exceeded. + */ + if value < 0.0 || value > 1.0 { + t.Errorf("PRNG test number %d, seeded with %d exceeded unit interval [0; 1] at the %d-th sample. Output: %f", i, seed, j, value) + } + + } + + } + +} diff --git a/resample/resample.go b/resample/resample.go new file mode 100644 index 0000000..41b2f50 --- /dev/null +++ b/resample/resample.go @@ -0,0 +1,142 @@ +package resample + +import ( + "math" +) + +/* + * The Lanczos kernel function L(x, a). + */ +func lanczosKernel(x float64, a float64) float64 { + + /* + * Calculate the sections of the Lanczos kernel. + */ + if x == 0 { + return 1.0 + } else if (-a < x) && (x < a) { + piX := math.Pi * x + piXa := piX / a + piXsquared := piX * piX + xSin := math.Sin(piX) + xaSin := math.Sin(piXa) + prodSins := xSin * xaSin + arg := a * prodSins + result := arg / piXsquared + return result + } else { + return 0.0 + } + +} + +/* + * The Lanczos interpolation function S(s, x, a). + */ +func lanczosInterpolate(s []float64, x float64, a uint16) float64 { + floorX := math.Floor(x) + idx := int(floorX) + aInt := int(a) + aInc := aInt + 1 + lBound := idx - aInc + uBound := idx + aInc + n := len(s) + aFloat := float64(a) + sum := float64(0.0) + + /* + * Calculate the Lanczos sum. + */ + for i := lBound; i < uBound; i++ { + + /* + * Check if we are still within the bounds of the slice. + */ + if (i >= 0) && (i < n) { + iFloat := float64(i) + diff := x - iFloat + fac := s[i] + val := lanczosKernel(diff, aFloat) + sum += fac * val + } + + } + + return sum +} + +/* + * Resample time series data from a source to a target sampling rate using the + * Lanczos resampling method. + */ +func Time(samples []float64, sourceRate uint32, targetRate uint32) []float64 { + inputLength := len(samples) + inputLengthFloat := float64(inputLength) + sourceRateFloat := float64(sourceRate) + targetRateFloat := float64(targetRate) + expansion := targetRateFloat / sourceRateFloat + outputLengthFloat := inputLengthFloat * expansion + outputLengthFloor := math.Floor(outputLengthFloat) + outputLength := int(outputLengthFloor) + + /* + * If we exactly hit the last sample, do not expand the sequence. + */ + if outputLengthFloor == outputLengthFloat { + outputLength-- + } + + outputBuffer := make([]float64, outputLength) + dx := sourceRateFloat / targetRateFloat + + /* + * Calculate output samples using Lanczos interpolation. + */ + for i, _ := range outputBuffer { + iFloat := float64(i) + x := iFloat * dx + val := lanczosInterpolate(samples, x, 3) + outputBuffer[i] = val + } + + return outputBuffer +} + +/* + * Resample frequency domain data to a different number of target bins using + * the Lanczos resampling method. + */ +func Frequency(bins []complex128, numTargetBins uint32) []complex128 { + numSourceBins := len(bins) + sourceReal := make([]float64, numSourceBins) + sourceImag := make([]float64, numSourceBins) + + /* + * Extract real and imaginary sequences from complex sequence. + */ + for i, elem := range bins { + elemReal := real(elem) + sourceReal[i] = elemReal + elemImag := imag(elem) + sourceImag[i] = elemImag + } + + targetBins := make([]complex128, numTargetBins) + numSourceBinsFloat := float64(numSourceBins) + numTargetBinsFloat := float64(numTargetBins) + dx := numSourceBinsFloat / numTargetBinsFloat + + /* + * Calculate output samples using Lanczos interpolation. + */ + for i, _ := range targetBins { + iFloat := float64(i) + x := iFloat * dx + targetReal := lanczosInterpolate(sourceReal, x, 3) + targetImag := lanczosInterpolate(sourceImag, x, 3) + targetComplex := complex(targetReal, targetImag) + targetBins[i] = targetComplex + } + + return targetBins +} diff --git a/resample/resample_test.go b/resample/resample_test.go new file mode 100644 index 0000000..07762c5 --- /dev/null +++ b/resample/resample_test.go @@ -0,0 +1,175 @@ +package resample + +import ( + "math" + "testing" +) + +/* + * Compare two real-valued slices to check whether their components are close. + */ +func areSlicesClose(a []float64, b []float64) (bool, []float64) { + + /* + * Check whether the two slices are of the same size. + */ + if len(a) != len(b) { + return false, nil + } else { + c := true + n := len(a) + diffs := make([]float64, n) + + /* + * Iterate over the arrays to compare values. + */ + for i, elem := range a { + diff := elem - b[i] + diffAbs := math.Abs(diff) + + /* + * Check if we found a significant difference. + */ + if diffAbs > 0.00000001 { + c = false + } + + diffs[i] = diff + } + + return c, diffs + } + +} + +/* + * Perform a unit test on the Lanczos algorithm for time series data. + */ +func TestTimeSeries(t *testing.T) { + + /* + * Input vectors. + */ + in := [][]float64{ + []float64{0.87622011, 0.41920066, 0.56935138, 0.56090797, 0.0485888, 0.89798242, 0.94420837, 0.89861948}, + } + + /* + * Expected output vectors for upsampling. + */ + outExpectedUp := [][]float64{ + []float64{0.87622011, 0.72424457, 0.41920066, 0.40800042, 0.56935138, 0.66706275, 0.56090797, 0.20545441, 0.0485888, 0.40780951, 0.89798242, 1.00559434, 0.94420837, 1.00017368, 0.89861948}, + } + + /* + * Expected output vectors for downsampling. + */ + outExpectedDown := [][]float64{ + []float64{0.87622011, 0.61602851, 0.25912048}, + } + + /* + * Test with each input vector. + */ + for i, currentIn := range in { + expectedUp := outExpectedUp[i] + expectedDown := outExpectedDown[i] + currentResultUp := Time(currentIn, 96000, 192000) + currentResultDown := Time(currentIn, 96000, 44100) + okUp, diffUp := areSlicesClose(currentResultUp, expectedUp) + + /* + * Verify components of upsampled vector. + */ + if !okUp { + t.Errorf("Upsampling vector number %d: Result is incorrect. Expected %v, got %v, difference: %v", i, expectedUp, currentResultUp, diffUp) + } + + okDown, diffDown := areSlicesClose(currentResultDown, expectedDown) + + /* + * Verify components of downsampled vector. + */ + if !okDown { + t.Errorf("Downsampling vector number %d: Result is incorrect. Expected %v, got %v, difference: %v", i, expectedDown, currentResultDown, diffDown) + } + + } + +} + +/* + * Perform a unit test on the Lanczos algorithm for frequency series data. + */ +func TestFrequencySeries(t *testing.T) { + + /* + * Complex input vectors. + */ + in := [][]complex128{ + []complex128{ + complex(0.34233881, 0.25689662), + complex(0.04731972, 0.70090472), + complex(0.6126194, 0.21446363), + complex(0.4184522, 0.44984173), + complex(0.58391517, 0.93459223), + complex(0.52775765, 0.05379716), + complex(0.13449256, 0.70627374), + complex(0.05077271, 0.49363423), + }, + } + + /* + * Real part of expected output vectors for downsampling. + */ + outExpectedReal := [][]float64{ + []float64{0.34233881, 0.6126194, 0.58391517, 0.13449256}, + } + + /* + * Imaginary part of expected output vectors for downsampling. + */ + outExpectedImag := [][]float64{ + []float64{0.25689662, 0.21446363, 0.93459223, 0.70627374}, + } + + /* + * Test with each input vector. + */ + for i, currentIn := range in { + expectedReal := outExpectedReal[i] + expectedImag := outExpectedImag[i] + currentResult := Frequency(currentIn, 4) + nResult := len(currentResult) + currentResultReal := make([]float64, nResult) + currentResultImag := make([]float64, nResult) + + /* + * Extract real and imaginary part from result. + */ + for i, elem := range currentResult { + currentResultReal[i] = real(elem) + currentResultImag[i] = imag(elem) + } + + okReal, diffReal := areSlicesClose(currentResultReal, expectedReal) + + /* + * Verify components of upsampled vector. + */ + if !okReal { + t.Errorf("Real vector %d: Result is incorrect. Expected %v, got %v, difference: %v", i, expectedReal, currentResultReal, diffReal) + } + + okImag, diffImag := areSlicesClose(currentResultImag, expectedImag) + + /* + * Verify components of downsampled vector. + */ + if !okImag { + t.Errorf("Imaginary vector %d: Result is incorrect. Expected %v, got %v, difference: %v", i, expectedImag, currentResultImag, diffImag) + } + + } + +} diff --git a/signal/signal.go b/signal/signal.go new file mode 100644 index 0000000..bf16502 --- /dev/null +++ b/signal/signal.go @@ -0,0 +1,406 @@ +package signal + +import ( + "fmt" + "github.com/andrepxx/go-dsp-guitar/effects" + "github.com/andrepxx/go-dsp-guitar/filter" + "sync" +) + +/* + * Data structure representing a slot in a signal chain. + */ +type slotStruct struct { + unit effects.Unit + bypass bool +} + +/* + * Interface type for a signal chain. + */ +type Chain interface { + AppendUnit(unitType int) (int, error) + RemoveUnit(id int) error + MoveUp(id int) error + MoveDown(id int) error + UnitType(id int) (int, error) + SetBypass(id int, bypass bool) error + GetBypass(id int) (bool, error) + SetDiscreteValue(id int, name string, value string) error + GetDiscreteValue(id int, name string) (string, error) + SetNumericValue(id int, name string, value int32) error + GetNumericValue(id int, name string) (int32, error) + Parameters(id int) ([]effects.Parameter, error) + Length() int + Process(in []float64, out []float64, sampleRate uint32) +} + +/* + * Data structure representing a signal chain. + */ +type chainStruct struct { + bufferIn []float64 + bufferOut []float64 + responses filter.ImpulseResponses + mutex sync.RWMutex + slots []slotStruct +} + +/* + * Appends a new effects unit to the end of the signal chain. + */ +func (this *chainStruct) AppendUnit(unitType int) (int, error) { + unit := effects.CreateUnit(unitType) + + /* + * Check whether unit was successfully created. + */ + if unit == nil { + return -1, fmt.Errorf("%s", "Failed to create effects unit.") + } else { + + /* + * If unit is a power amp, prepare it. + */ + if unitType == effects.UNIT_POWERAMP { + effects.PreparePowerAmp(unit, this.responses) + } + + /* + * Create new slot in the signal chain. + */ + slot := slotStruct{ + unit: unit, + bypass: true, + } + + this.mutex.Lock() + nPre := len(this.slots) + this.slots = append(this.slots, slot) + this.mutex.Unlock() + return nPre, nil + } + +} + +/* + * Removes an effects unit from the signal chain. + */ +func (this *chainStruct) RemoveUnit(id int) error { + this.mutex.Lock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + this.mutex.Unlock() + return fmt.Errorf("Cannot remove unit %d.", id) + } else { + idInc := id + 1 + this.slots = append(this.slots[:id], this.slots[idInc:]...) + this.mutex.Unlock() + return nil + } + +} + +/* + * Moves an effects unit up (towards the front of) the signal chain. + */ +func (this *chainStruct) MoveUp(id int) error { + this.mutex.Lock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id <= 0 || id >= n { + this.mutex.Unlock() + return fmt.Errorf("Cannot move unit %d up.", id) + } else { + idDec := id - 1 + this.slots[id], this.slots[idDec] = this.slots[idDec], this.slots[id] + this.mutex.Unlock() + return nil + } + +} + +/* + * Moves an effects unit down (towards the end of) the signal chain. + */ +func (this *chainStruct) MoveDown(id int) error { + this.mutex.Lock() + n := len(this.slots) + nDec := n - 1 + + /* + * Check if index is out of range. + */ + if id < 0 || id >= nDec { + this.mutex.Unlock() + return fmt.Errorf("Cannot move unit %d down.", id) + } else { + idInc := id + 1 + this.slots[id], this.slots[idInc] = this.slots[idInc], this.slots[id] + this.mutex.Unlock() + return nil + } + +} + +/* + * Returns the type of an effects unit. + */ +func (this *chainStruct) UnitType(id int) (int, error) { + this.mutex.RLock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + this.mutex.RUnlock() + return -1, fmt.Errorf("Cannot get unit type: No unit %d.", id) + } else { + unit := this.slots[id].unit + unitType := unit.Type() + this.mutex.RUnlock() + return unitType, nil + } +} + +/* + * Enables or disables bypass of an effects unit inside the signal chain. + */ +func (this *chainStruct) SetBypass(id int, bypass bool) error { + this.mutex.Lock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + this.mutex.Unlock() + action := "disable" + + /* + * Check whether bypass should be enabled. + */ + if bypass { + action = "enable" + } + + return fmt.Errorf("Cannot %s bypass: No unit %d.", action, id) + } else { + this.slots[id].bypass = bypass + this.mutex.Unlock() + return nil + } + +} + +/* + * Retrieves whether an effects unit inside the signal chain is in bypass mode or not. + */ +func (this *chainStruct) GetBypass(id int) (bool, error) { + this.mutex.RLock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + this.mutex.RUnlock() + return false, fmt.Errorf("Cannot get bypass value: No unit %d.", id) + } else { + bypass := this.slots[id].bypass + this.mutex.RUnlock() + return bypass, nil + } + +} + +/* + * Sets a discrete value for an effects unit inside the signal chain. + */ +func (this *chainStruct) SetDiscreteValue(id int, name string, value string) error { + this.mutex.RLock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + this.mutex.RUnlock() + return fmt.Errorf("Cannot set discrete value: No unit %d.", id) + } else { + unit := this.slots[id].unit + this.mutex.RUnlock() + err := unit.SetDiscreteValue(name, value) + return err + } + +} + +/* + * Retrieves a discrete value from an effects unit inside the signal chain. + */ +func (this *chainStruct) GetDiscreteValue(id int, name string) (string, error) { + this.mutex.RLock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + this.mutex.RUnlock() + return "", fmt.Errorf("Cannot get discrete value: No unit %d.", id) + } else { + unit := this.slots[id].unit + this.mutex.RUnlock() + value, err := unit.GetDiscreteValue(name) + return value, err + } + +} + +/* + * Sets a numeric value for an effects unit inside the signal chain. + */ +func (this *chainStruct) SetNumericValue(id int, name string, value int32) error { + this.mutex.RLock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + return fmt.Errorf("Cannot set numeric value: No unit %d.", id) + } else { + unit := this.slots[id].unit + this.mutex.RUnlock() + err := unit.SetNumericValue(name, value) + return err + } + +} + +/* + * Retrieves a numeric value from an effects unit inside the signal chain. + */ +func (this *chainStruct) GetNumericValue(id int, name string) (int32, error) { + this.mutex.RLock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + return 0, fmt.Errorf("Cannot get numeric value: No unit %d.", id) + } else { + unit := this.slots[id].unit + this.mutex.RUnlock() + value, err := unit.GetNumericValue(name) + return value, err + } + +} + +/* + * Returns the parameters of an effects unit inside a signal chain. + */ +func (this *chainStruct) Parameters(id int) ([]effects.Parameter, error) { + this.mutex.RLock() + n := len(this.slots) + + /* + * Check if index is out of range. + */ + if id < 0 || id >= n { + return nil, fmt.Errorf("Cannot get parameters: No unit %d.", id) + } else { + unit := this.slots[id].unit + this.mutex.RUnlock() + params := unit.Parameters() + return params, nil + } + +} + +/* + * Returns the number of units inside this signal chain. + */ +func (this *chainStruct) Length() int { + this.mutex.RLock() + n := len(this.slots) + this.mutex.RUnlock() + return n +} + +/* + * Passes a signal through the signal chain. + */ +func (this *chainStruct) Process(in []float64, out []float64, sampleRate uint32) { + + /* + * Verify that input and output buffers are the same size. + */ + if len(in) == len(out) { + n := len(in) + + /* + * If size of input buffer does not match, reallocate it. + */ + if len(this.bufferIn) != n { + this.bufferIn = make([]float64, n) + } + + /* + * If size of output buffer does not match, reallocate it. + */ + if len(this.bufferOut) != n { + this.bufferOut = make([]float64, n) + } + + copy(this.bufferIn, in) + this.mutex.RLock() + + /* + * Iterate over the slots. + */ + for _, slot := range this.slots { + + /* + * Verify that slot is not in bypass mode. + */ + if !slot.bypass { + slot.unit.Process(this.bufferIn, this.bufferOut, sampleRate) + this.bufferIn, this.bufferOut = this.bufferOut, this.bufferIn + } + + } + + this.mutex.RUnlock() + copy(out, this.bufferIn) + } + +} + +/* + * Creates a new signal chain. + */ +func CreateChain(responses filter.ImpulseResponses) Chain { + slots := make([]slotStruct, 0) + + /* + * The new signal chain. + */ + chain := chainStruct{ + responses: responses, + slots: slots, + } + + return &chain +} diff --git a/spatializer/spatializer.go b/spatializer/spatializer.go new file mode 100644 index 0000000..4da8774 --- /dev/null +++ b/spatializer/spatializer.go @@ -0,0 +1,468 @@ +package spatializer + +import ( + "fmt" + "math" + "sync" +) + +/* + * Mathematical constants. + */ +const ( + MATH_DEGREE_TO_RADIANS = math.Pi / 180.0 +) + +/* + * Other global constants. + */ +const ( + DEFAULT_SAMPLE_RATE = 96000 + EFFECTIVE_DISTANCE = 0.215 + HALF_EFFECTIVE_DISTANCE = 0.5 * EFFECTIVE_DISTANCE + GROUP_DELAY = 6.3e-4 + OUTPUT_COUNT = 2 +) + +/* + * Interface type for a spatializer. + */ +type Spatializer interface { + GetAzimuth(inputChannel int) (float64, error) + GetDistance(inputChannel int) (float64, error) + GetLevel(inputChannel int) (float64, error) + GetInputCount() int + GetOutputCount() int + Process(inputBuffers [][]float64, auxInputBuffer []float64, outputBuffers [][]float64) + SetAzimuth(inputChannel int, azimuth float64) error + SetDistance(inputChannel int, distance float64) error + SetLevel(inputChannel int, level float64) error + SetSampleRate(rate uint32) +} + +/* + * Data structure representing the position of an audio source in space. + */ +type position struct { + azimuth float64 + distance float64 + level float64 +} + +/* + * Data structure representing a spatializer. + */ +type spatializerStruct struct { + buffers [][]float64 + inputCount int + sampleRate uint32 + mutex sync.RWMutex + positions []position +} + +/* + * Returns the azimuth value associated with a channel. + */ +func (this *spatializerStruct) GetAzimuth(inputChannel int) (float64, error) { + inputCount := this.inputCount + + /* + * Verify that the channel exists. + */ + if inputChannel > inputCount { + return 0.0, fmt.Errorf("Cannot get azimuth for channel %d: Only %d channels exist.", inputChannel, inputCount) + } else { + this.mutex.RLock() + az := this.positions[inputChannel].azimuth + this.mutex.RUnlock() + return az, nil + } + +} + +/* + * Returns the distance value associated with a channel. + */ +func (this *spatializerStruct) GetDistance(inputChannel int) (float64, error) { + inputCount := this.inputCount + + /* + * Verify that the channel exists. + */ + if inputChannel > inputCount { + return 0.0, fmt.Errorf("Cannot get distance for channel %d: Only %d channels exist.", inputChannel, inputCount) + } else { + this.mutex.RLock() + dist := this.positions[inputChannel].distance + this.mutex.RUnlock() + return dist, nil + } + +} + +/* + * Returns the level value associated with a channel. + */ +func (this *spatializerStruct) GetLevel(inputChannel int) (float64, error) { + inputCount := this.inputCount + + /* + * Verify that the channel exists. + */ + if inputChannel > inputCount { + return 0.0, fmt.Errorf("Cannot get level for channel %d: Only %d channels exist.", inputChannel, inputCount) + } else { + this.mutex.RLock() + level := this.positions[inputChannel].level + this.mutex.RUnlock() + return level, nil + } + +} + +/* + * Returns the number of input streams this spatializer processes. + */ +func (this *spatializerStruct) GetInputCount() int { + return this.inputCount +} + +/* + * Returns the number of output streams this spatializer generates. + */ +func (this *spatializerStruct) GetOutputCount() int { + return OUTPUT_COUNT +} + +/* + * Perform the spatializer audio processing. + */ +func (this *spatializerStruct) Process(inputBuffers [][]float64, auxInputBuffer []float64, outputBuffers [][]float64) { + nInputBuffers := len(inputBuffers) + nOutputBuffers := len(outputBuffers) + + /* + * Verify that we have as many input and output buffers as we expect. + */ + if (nInputBuffers == this.inputCount) && (nOutputBuffers == OUTPUT_COUNT) { + sampleRateFloat := float64(this.sampleRate) + + /* + * Iterate over the output buffers. + */ + for _, buffer := range outputBuffers { + + /* + * Iterate over the current buffer and zero it. + */ + for i, _ := range buffer { + buffer[i] = 0.0 + } + + } + + this.mutex.RLock() + + /* + * Iterate over the input channels. + */ + for i, inputBuffer := range inputBuffers { + position := this.positions[i] + azimuth := MATH_DEGREE_TO_RADIANS * position.azimuth + distance := position.distance + level := position.level + currentBuffer := this.buffers[i] + bufferSize := len(currentBuffer) + sinAz, cosAz := math.Sincos(azimuth) + xPosition := distance * sinAz + yPosition := distance * cosAz + xDistLeft := math.Abs(xPosition + (HALF_EFFECTIVE_DISTANCE)) + xDistRight := math.Abs(xPosition - (HALF_EFFECTIVE_DISTANCE)) + yDist := math.Abs(yPosition) + yDistSquared := yDist * yDist + xDistLeftSquared := xDistLeft * xDistLeft + distLeft := math.Sqrt(xDistLeftSquared + yDistSquared) + preLeft := 1.0 / distLeft + + /* + * Factors should not exceed unity. + */ + if preLeft > 1.0 { + preLeft = 1.0 + } + + facLeft := level * preLeft + xDistRightSquared := xDistRight * xDistRight + distRight := math.Sqrt(xDistRightSquared + yDistSquared) + preRight := 1.0 / distRight + + /* + * Factors should not exceed unity. + */ + if preRight > 1.0 { + preRight = 1.0 + } + + facRight := level * preRight + distDiff := distLeft - distRight + delayTime := (GROUP_DELAY / EFFECTIVE_DISTANCE) * distDiff + delayTimeAbs := math.Abs(delayTime) + delaySamples := delayTimeAbs * sampleRateFloat + delaySamplesEarly := math.Floor(delaySamples) + delaySamplesEarlyInt := int(delaySamplesEarly) + + /* + * Ensure that the delay does not exceed the buffer size. + */ + if delaySamplesEarlyInt >= bufferSize { + delaySamplesEarlyInt = bufferSize - 1 + } + + delaySamplesLate := math.Ceil(delaySamples) + delaySamplesLateInt := int(delaySamplesLate) + + /* + * Ensure that the delay does not exceed the buffer size. + */ + if delaySamplesLateInt >= bufferSize { + delaySamplesLateInt = bufferSize - 1 + } + + /* + * Process each sample. + */ + for j, currentSample := range inputBuffer { + + /* + * Perform simplified processing if delay time is exactly zero. + */ + if delayTime == 0.0 { + outputBuffers[0][j] += facLeft * currentSample + outputBuffers[1][j] += facRight * currentSample + } else { + delayedIdxEarly := j - delaySamplesEarlyInt + delayedIdxLate := j - delaySamplesLateInt + delayedSampleEarly := float64(0.0) + delayedSampleLate := float64(0.0) + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdxEarly >= 0 { + delayedSampleEarly = inputBuffer[delayedIdxEarly] + } else { + bufferPtr := bufferSize + delayedIdxEarly + delayedSampleEarly = currentBuffer[bufferPtr] + } + + /* + * Check whether the delayed sample can be found in the current input + * signal or the delay buffer. + */ + if delayedIdxLate >= 0 { + delayedSampleLate = inputBuffer[delayedIdxLate] + } else { + bufferPtr := bufferSize + delayedIdxLate + delayedSampleLate = currentBuffer[bufferPtr] + } + + weightEarly := 1.0 - (delaySamples - delaySamplesEarly) + weightLate := 1.0 - (delaySamplesLate - delaySamples) + earlySample := weightEarly * delayedSampleEarly + lateSample := weightLate * delayedSampleLate + delayedSample := earlySample + lateSample + + /* + * When the delay time is positive, the left channel is delayed. + * When the delay time is negative, the right channel is delayed. + */ + if delayTime > 0.0 { + outputBuffers[0][j] += facLeft * delayedSample + outputBuffers[1][j] += facRight * inputBuffer[j] + } else { + outputBuffers[0][j] += facLeft * inputBuffer[j] + outputBuffers[1][j] += facRight * delayedSample + } + + } + + } + + } + + this.mutex.RUnlock() + + /* + * If we have an aux input, mix it in as well. + */ + if auxInputBuffer != nil { + + /* + * Process each sample. + */ + for j, sample := range auxInputBuffer { + outputBuffers[0][j] += sample + outputBuffers[1][j] += sample + } + + } + + /* + * Iterate over the input channels again to update all buffers. + */ + for i, inputBuffer := range inputBuffers { + numSamples := len(inputBuffer) + currentBuffer := this.buffers[i] + bufferSize := len(currentBuffer) + boundary := bufferSize - numSamples + + /* + * Check whether our buffer is larger than the number of samples processed. + */ + if boundary >= 0 { + copy(currentBuffer[0:boundary], currentBuffer[numSamples:bufferSize]) + copy(currentBuffer[boundary:bufferSize], inputBuffer) + } else { + copy(currentBuffer, inputBuffer[-boundary:numSamples]) + } + + } + + } + +} + +/* + * Sets the azimuth of the audio source associated with a certain channel. + */ +func (this *spatializerStruct) SetAzimuth(inputChannel int, azimuth float64) error { + inputCount := this.inputCount + + /* + * Verify that the channel exists. + */ + if inputChannel > inputCount { + return fmt.Errorf("Cannot set azimuth for channel %d: Only %d channels exist.", inputChannel, inputCount) + } else { + this.mutex.Lock() + this.positions[inputChannel].azimuth = azimuth + this.mutex.Unlock() + return nil + } + +} + +/* + * Sets the distance of the audio source associated with a certain channel. + */ +func (this *spatializerStruct) SetDistance(inputChannel int, distance float64) error { + inputCount := this.inputCount + + /* + * Verify that the channel exists. + */ + if inputChannel > inputCount { + return fmt.Errorf("Cannot set distance for channel %d: Only %d channels exist.", inputChannel, inputCount) + } else { + + /* + * Verify that the distance is within limits. + */ + if distance < 0.0 || distance > 10.0 { + return fmt.Errorf("%s", "Failed to set distance: Value must be within [0, 10].") + } else { + this.mutex.Lock() + this.positions[inputChannel].distance = distance + this.mutex.Unlock() + return nil + } + + } + +} + +/* + * Sets the level of the audio source associated with a certain channel. + */ +func (this *spatializerStruct) SetLevel(inputChannel int, level float64) error { + inputCount := this.inputCount + + /* + * Verify that the channel exists. + */ + if inputChannel > inputCount { + return fmt.Errorf("Cannot set distance for channel %d: Only %d channels exist.", inputChannel, inputCount) + } else { + + /* + * Verify that the distance is within limits. + */ + if level < 0.0 || level > 1.0 { + return fmt.Errorf("%s", "Failed to set level: Value must be within [0, 1].") + } else { + this.mutex.Lock() + this.positions[inputChannel].level = level + this.mutex.Unlock() + return nil + } + + } + +} + +/* + * Changes the sample rate and recreates all inner buffers. + */ +func (this *spatializerStruct) SetSampleRate(rate uint32) { + sampleRateFloat := float64(rate) + bufferSizeFloat := math.Ceil(sampleRateFloat * GROUP_DELAY) + bufferSize := int(bufferSizeFloat) + inputChannels := this.inputCount + + /* + * Create each inner buffer. + */ + for i := 0; i < inputChannels; i++ { + this.buffers[i] = make([]float64, bufferSize) + } + +} + +/* + * Creates a new spatializer. + */ +func Create(inputChannels int) Spatializer { + positions := make([]position, inputChannels) + + /* + * Set the levels to one by default. + */ + for i, _ := range positions { + positions[i].level = 1.0 + } + + buffers := make([][]float64, inputChannels) + sampleRateFloat := float64(DEFAULT_SAMPLE_RATE) + bufferSizeFloat := math.Ceil(sampleRateFloat * GROUP_DELAY) + bufferSize := int(bufferSizeFloat) + + /* + * Create each inner buffer. + */ + for i, _ := range buffers { + buffers[i] = make([]float64, bufferSize) + } + + /* + * Create the new spatializer. + */ + s := spatializerStruct{ + inputCount: inputChannels, + sampleRate: DEFAULT_SAMPLE_RATE, + positions: positions, + buffers: buffers, + } + + return &s +} diff --git a/tuner/samples/A2.wav b/tuner/samples/A2.wav new file mode 100644 index 0000000..7e8c5f1 Binary files /dev/null and b/tuner/samples/A2.wav differ diff --git a/tuner/samples/D2.wav b/tuner/samples/D2.wav new file mode 100644 index 0000000..7487df5 Binary files /dev/null and b/tuner/samples/D2.wav differ diff --git a/tuner/samples/D3.wav b/tuner/samples/D3.wav new file mode 100644 index 0000000..7fb4546 Binary files /dev/null and b/tuner/samples/D3.wav differ diff --git a/tuner/samples/E4.wav b/tuner/samples/E4.wav new file mode 100644 index 0000000..3ec259d Binary files /dev/null and b/tuner/samples/E4.wav differ diff --git a/tuner/samples/G3.wav b/tuner/samples/G3.wav new file mode 100644 index 0000000..b6ea509 Binary files /dev/null and b/tuner/samples/G3.wav differ diff --git a/tuner/samples/H3.wav b/tuner/samples/H3.wav new file mode 100644 index 0000000..3a01cbc Binary files /dev/null and b/tuner/samples/H3.wav differ diff --git a/tuner/tuner.go b/tuner/tuner.go new file mode 100644 index 0000000..0af5912 --- /dev/null +++ b/tuner/tuner.go @@ -0,0 +1,603 @@ +package tuner + +import ( + "fmt" + "github.com/andrepxx/go-dsp-guitar/circular" + "github.com/andrepxx/go-dsp-guitar/fft" + "math" + "math/cmplx" + "sync" +) + +/* + * Global constants. + */ +const ( + NUM_SAMPLES = 96000 +) + +/* + * Data structure representing a musical note. + */ +type noteStruct struct { + name string + frequency float64 +} + +/* + * Data structure representing the result of a spectral analysis. + */ +type resultStruct struct { + cents int8 + frequency float64 + note string +} + +/* + * The result of a spectral analysis. + */ +type Result interface { + Cents() int8 + Frequency() float64 + Note() string +} + +/* + * Data structure representing a tuner. + */ +type tunerStruct struct { + notes []noteStruct + mutexBuffer sync.RWMutex + buffer circular.Buffer + sampleRate uint32 + mutexAnalyze sync.Mutex + bufCorrelation []float64 + bufFFT []complex128 +} + +/* + * A chromatic instrument tuner. + */ +type Tuner interface { + Analyze() (Result, error) + Process(samples []float64, sampleRate uint32) +} + +/* + * Generates a list of notes and their frequencies. + * + * f(n) = 2^(n / 12) * 440 + * + * Where n is the number of half-tone steps relative to A4. + */ +func generateNotes() []noteStruct { + + /* + * Create a list of appropriate notes. + */ + notes := []noteStruct{ + noteStruct{ + name: "H1", + frequency: 61.7354, + }, + noteStruct{ + name: "C2", + frequency: 65.4064, + }, + noteStruct{ + name: "C#2", + frequency: 69.2957, + }, + noteStruct{ + name: "D2", + frequency: 73.4162, + }, + noteStruct{ + name: "D#2", + frequency: 77.7817, + }, + noteStruct{ + name: "E2", + frequency: 82.4069, + }, + noteStruct{ + name: "F2", + frequency: 87.3071, + }, + noteStruct{ + name: "F#2", + frequency: 92.4986, + }, + noteStruct{ + name: "G2", + frequency: 97.9989, + }, + noteStruct{ + name: "G#2", + frequency: 103.8262, + }, + noteStruct{ + name: "A2", + frequency: 110.0000, + }, + noteStruct{ + name: "A#2", + frequency: 116.5409, + }, + noteStruct{ + name: "H2", + frequency: 123.4708, + }, + noteStruct{ + name: "C3", + frequency: 130.8128, + }, + noteStruct{ + name: "C#3", + frequency: 138.5913, + }, + noteStruct{ + name: "D3", + frequency: 146.8324, + }, + noteStruct{ + name: "D#3", + frequency: 155.5635, + }, + noteStruct{ + name: "E3", + frequency: 164.8138, + }, + noteStruct{ + name: "F3", + frequency: 174.6141, + }, + noteStruct{ + name: "F#3", + frequency: 184.9972, + }, + noteStruct{ + name: "G3", + frequency: 195.9978, + }, + noteStruct{ + name: "G#3", + frequency: 207.6523, + }, + noteStruct{ + name: "A3", + frequency: 220.0000, + }, + noteStruct{ + name: "A#3", + frequency: 233.0819, + }, + noteStruct{ + name: "H3", + frequency: 246.9417, + }, + noteStruct{ + name: "C4", + frequency: 261.6256, + }, + noteStruct{ + name: "C#4", + frequency: 277.1826, + }, + noteStruct{ + name: "D4", + frequency: 293.6648, + }, + noteStruct{ + name: "D#4", + frequency: 311.1270, + }, + noteStruct{ + name: "E4", + frequency: 329.6276, + }, + noteStruct{ + name: "F4", + frequency: 349.2282, + }, + noteStruct{ + name: "F#4", + frequency: 369.9944, + }, + noteStruct{ + name: "G4", + frequency: 391.9954, + }, + noteStruct{ + name: "G#4", + frequency: 415.3047, + }, + noteStruct{ + name: "A4", + frequency: 440.0000, + }, + noteStruct{ + name: "A#4", + frequency: 466.1638, + }, + noteStruct{ + name: "H4", + frequency: 493.8833, + }, + noteStruct{ + name: "C5", + frequency: 523.2511, + }, + noteStruct{ + name: "C#5", + frequency: 554.3653, + }, + noteStruct{ + name: "D5", + frequency: 587.3295, + }, + noteStruct{ + name: "D#5", + frequency: 622.2540, + }, + noteStruct{ + name: "E5", + frequency: 659.2551, + }, + noteStruct{ + name: "F5", + frequency: 698.4565, + }, + noteStruct{ + name: "F#5", + frequency: 739.9888, + }, + noteStruct{ + name: "G5", + frequency: 783.9909, + }, + noteStruct{ + name: "G#5", + frequency: 830.6094, + }, + noteStruct{ + name: "A5", + frequency: 880.0000, + }, + noteStruct{ + name: "A#5", + frequency: 932.3275, + }, + noteStruct{ + name: "H5", + frequency: 987.7666, + }, + noteStruct{ + name: "C6", + frequency: 1046.5023, + }, + noteStruct{ + name: "C#6", + frequency: 1108.7305, + }, + noteStruct{ + name: "D6", + frequency: 1174.6591, + }, + noteStruct{ + name: "D#6", + frequency: 1244.5079, + }, + noteStruct{ + name: "E6", + frequency: 1318.5102, + }, + noteStruct{ + name: "F6", + frequency: 1396.9129, + }, + noteStruct{ + name: "F#6", + frequency: 1479.9777, + }, + noteStruct{ + name: "G6", + frequency: 1567.9817, + }, + noteStruct{ + name: "G#6", + frequency: 1661.2188, + }, + noteStruct{ + name: "A6", + frequency: 1760.0000, + }, + noteStruct{ + name: "A#6", + frequency: 1864.6550, + }, + noteStruct{ + name: "H6", + frequency: 1975.5332, + }, + } + + return notes +} + +/* + * Find the maximum value in a buffer. + */ +func findMaximum(buf []float64) (float64, int) { + maxVal := math.Inf(-1) + maxIdx := int(-1) + + /* + * Iterate over the buffer and find the maximum value. + */ + for idx, value := range buf { + + /* + * If we found a value which is greater than any value we + * encountered so far, make it the new candidate. + */ + if value > maxVal { + maxVal = value + maxIdx = idx + } + + } + + return maxVal, maxIdx +} + +/* + * Returns the deviation from the reference note in cents. + */ +func (this *resultStruct) Cents() int8 { + return this.cents +} + +/* + * Returns the fundamental frequency of the signal. + */ +func (this *resultStruct) Frequency() float64 { + return this.frequency +} + +/* + * Returns the name of the closest note on the chromatic scale. + */ +func (this *resultStruct) Note() string { + return this.note +} + +/* + * Analyze buffered stream for spectral content. + */ +func (this *tunerStruct) Analyze() (Result, error) { + this.mutexAnalyze.Lock() + circularBuffer := this.buffer + bufCorrelation := this.bufCorrelation + bufCorrrlationLength := len(bufCorrelation) + bufCorrelationLength64 := uint64(bufCorrrlationLength) + bufFFT := this.bufFFT + bufFFTLength := len(bufFFT) + bufFFTLength64 := uint64(bufFFTLength) + n := circularBuffer.Length() + twoN := uint64(2 * n) + fftSize, _ := fft.NextPowerOfTwo(twoN) + + /* + * Ensure that correlation buffer is of correct length. + */ + if bufCorrelationLength64 != fftSize { + bufCorrelation = make([]float64, fftSize) + this.bufCorrelation = bufCorrelation + } + + /* + * Ensure that FFT buffer is of correct length. + */ + if bufFFTLength64 != fftSize { + bufFFT = make([]complex128, fftSize) + this.bufFFT = bufFFT + } + + signalBuffer := bufCorrelation[0:n] + this.mutexBuffer.RLock() + sampleRate := this.sampleRate + err := circularBuffer.Retrieve(signalBuffer) + this.mutexBuffer.RUnlock() + + /* + * Verify that buffer contents could be retrieved. + */ + if err != nil { + msg := err.Error() + this.mutexAnalyze.Unlock() + return nil, fmt.Errorf("Failed to retrieve contents of circular buffer: %s", msg) + } else { + tailBuffer := bufCorrelation[n:fftSize] + fft.ZeroFloat(tailBuffer) + err = fft.RealFourier(bufCorrelation, bufFFT, fft.SCALING_DEFAULT) + + /* + * Verify that the forward FFT was calculated successfully. + */ + if err != nil { + msg := err.Error() + this.mutexAnalyze.Unlock() + return nil, fmt.Errorf("Failed to calculate forward FFT: %s", msg) + } else { + + /* + * Multiply each element of the spectrum with its complex conjugate. + */ + for i, elem := range bufFFT { + elemConj := cmplx.Conj(elem) + bufFFT[i] = elem * elemConj + } + + err = fft.RealInverseFourier(bufFFT, bufCorrelation, fft.SCALING_DEFAULT) + + /* + * Verify that the inverse FFT was calculated successfully. + */ + if err != nil { + msg := err.Error() + this.mutexAnalyze.Unlock() + return nil, fmt.Errorf("Failed to calculate inverse FFT: %s", msg) + } else { + notes := this.notes + noteCount := len(notes) + lastNote := noteCount - 1 + lowFreq := notes[0].frequency + highFreq := notes[lastNote].frequency + sampleRateFloat := float64(sampleRate) + lowIdx := int((sampleRateFloat / highFreq) + 0.5) + lowIdx64 := uint64(lowIdx) + + /* + * This might happen when the float value is infinite. + */ + if (lowIdx < 0) || (lowIdx64 >= twoN) { + lowIdx = 0 + lowIdx64 = 0 + } + + highIdx := int((sampleRateFloat / lowFreq) + 0.5) + highIdx64 := uint64(highIdx) + + /* + * This might happen when the float value is infinite. + */ + if (highIdx < 0) || (highIdx64 >= twoN) { + maxIdx := twoN - 1 + highIdx = int(maxIdx) + highIdx64 = maxIdx + } + + subCorrelation := bufCorrelation[lowIdx:highIdx] + maxVal, maxIdx := findMaximum(subCorrelation) + idx := lowIdx + maxIdx + idxUp := idx + 1 + + /* + * Prevent overrun. + */ + if idxUp > n { + idxUp = n + } + + idxDown := idx - 1 + + /* + * Prevent underrun. + */ + if idxDown < 0 { + idxDown = 0 + } + + valueLeft := bufCorrelation[idxDown] + valueRight := bufCorrelation[idxUp] + idxFloat := float64(idx) + valueDiff := valueRight - valueLeft + valueSum := valueRight + valueLeft + halfDiff := 0.5 * valueDiff + doubleMaxVal := 2.0 * maxVal + denominatorDiff := doubleMaxVal - valueSum + shiftEstimation := halfDiff / denominatorDiff + + /* + * Limit shift estimation to plus/minus half a sample. + */ + if shiftEstimation < -0.5 { + shiftEstimation = -0.5 + } else if shiftEstimation > 0.5 { + shiftEstimation = 0.5 + } + + idxFloat += shiftEstimation + actualFrequency := sampleRateFloat / idxFloat + actualNote := "Unknown" + actualCents := math.Inf(1) + actualCentsAbs := math.Abs(actualCents) + + /* + * Iterate over all notes and find the closest match. + */ + for _, note := range notes { + freq := note.frequency + freqRatio := actualFrequency / freq + diffCents := 1200.0 * math.Log2(freqRatio) + diffCentsAbs := math.Abs(diffCents) + + /* + * If this is the closest we've seen so far, make this the best match. + */ + if diffCentsAbs < actualCentsAbs { + actualNote = note.name + actualCents = diffCents + actualCentsAbs = diffCentsAbs + } + + } + + actualCentsInfinite := math.IsInf(actualCents, 0) + actualCentsNaN := math.IsNaN(actualCents) + actualCentsInt := int8(0) + + /* + * If cents are finite, use them. + */ + if !(actualCentsInfinite || actualCentsNaN) { + actualCentsInt = int8(actualCents) + } + + /* + * Create result of signal analysis. + */ + result := resultStruct{ + cents: actualCentsInt, + frequency: actualFrequency, + note: actualNote, + } + + this.mutexAnalyze.Unlock() + return &result, nil + } + + } + + } + +} + +/* + * Stream samples for later analysis. + */ +func (this *tunerStruct) Process(samples []float64, sampleRate uint32) { + this.mutexBuffer.Lock() + this.buffer.Enqueue(samples...) + this.sampleRate = sampleRate + this.mutexBuffer.Unlock() +} + +/* + * Creates an instrument tuner. + */ +func Create() Tuner { + notes := generateNotes() + buffer := circular.CreateBuffer(NUM_SAMPLES) + + /* + * Create data structure for a guitar tuner. + */ + t := tunerStruct{ + notes: notes, + buffer: buffer, + } + + return &t +} diff --git a/tuner/tuner_test.go b/tuner/tuner_test.go new file mode 100644 index 0000000..e624712 --- /dev/null +++ b/tuner/tuner_test.go @@ -0,0 +1,131 @@ +package tuner + +import ( + "github.com/andrepxx/go-dsp-guitar/wave" + "io/ioutil" + "math" + "testing" +) + +/* + * Perform a unit test on the tuner. + */ +func TestTuner(t *testing.T) { + tn := Create() + + /* + * Paths to test wave files. + */ + wavePaths := []string{ + "samples/D2.wav", + "samples/A2.wav", + "samples/D3.wav", + "samples/G3.wav", + "samples/H3.wav", + "samples/E4.wav", + } + + /* + * Notes contained in files. + */ + notes := []string{ + "D2", + "A2", + "D3", + "G3", + "H3", + "E4", + } + + /* + * Import each wave file into a buffer. + */ + for i, path := range wavePaths { + currentNote := notes[i] + buf, err := ioutil.ReadFile(path) + + /* + * Check if file was successfully read. + */ + if err != nil { + t.Errorf("Failed to read wave file from '%s'.", path) + } else { + file, err := wave.FromBuffer(buf) + + /* + * Check if file was successfully parsed. + */ + if err != nil { + t.Errorf("Failed to parse wave file from '%s'.", path) + } else { + sampleRate := file.SampleRate() + numChannels := file.ChannelCount() + + /* + * Check if file has a single channel. + */ + if numChannels != 1 { + t.Errorf("Wave file '%s' has %d channels, expected %d.", path, numChannels, 1) + } else { + c, err := file.Channel(0) + + /* + * Check if channel could be obtained. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to obtain channel %d from wave file '%s': %s", 1, path, msg) + } else { + samples := c.Floats() + tn.Process(samples, sampleRate) + res, err := tn.Analyze() + + /* + * Check if analysis could be performed. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to analyze wave file '%s': %s", path, msg) + } else { + note := res.Note() + + /* + * Check if note was determined correctly. + */ + if note != currentNote { + t.Errorf("Tuner failed to determine correct note. Expected '%s', got '%s'.", currentNote, note) + } + + cents := res.Cents() + + /* + * Check if deviation is large. + */ + if cents < -5 || cents > 5 { + t.Errorf("Tuner exhibits large deviation for note '%s'.", currentNote) + } + + freq := res.Frequency() + freqInfinite := math.IsInf(freq, 0) + freqNaN := math.IsNaN(freq) + + /* + * Check if frequency is infinite or not a number. + */ + if freqInfinite || freqNaN { + t.Errorf("Tuner reported invalid frequency ('%e') for note '%s'.", freq, currentNote) + } + + } + + } + + } + + } + + } + + } + +} diff --git a/wave/wave.go b/wave/wave.go new file mode 100644 index 0000000..5c976cf --- /dev/null +++ b/wave/wave.go @@ -0,0 +1,1435 @@ +package wave + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math" +) + +/* + * Global constants. + */ +const ( + BITS_PER_BYTE = 8 + MIN_CHUNK_HEADER_SIZE = 8 + MIN_DATASIZE_CHUNK_SIZE = 36 + LENGTH_DATASIZE_TABLE_ENTRIES = 12 +) + +/* + * Constants for handling of 24-bit integers. + */ +const ( + MAX_INT24 = 0x007fffff // int32 + MIN_INT24 = -(MAX_INT24 + 1) // int32 + SIGN_BIT_INT24 = 0x00800000 // int32 + SIZE_INT24 = 3 +) + +/* + * RIFF header constants. + */ +const ( + AUDIO_PCM = 0x0001 // uint16 + AUDIO_IEEE_FLOAT = 0x0003 // uint16 + DEFAULT_BIT_DEPTH = 0x0010 // uint16 + FORMAT_WAVE = 0x45564157 // uint32 + ID_DATA = 0x61746164 // uint32 + ID_DATASIZE = 0x34367364 // uint32 + ID_FORMAT = 0x20746d66 // uint32 + ID_RIFF = 0x46464952 // uint32 + ID_RIFF64 = 0x34364652 // uint32 + MIN_CHUNK_SIZE_FORMAT = 0x00000010 // uint32 + MIN_TOTAL_HEADER_SIZE = 0x0000002c // uint32 +) + +/* + * An interface type representing the channels inside a RIFF wave file. + */ +type Channel interface { + Clear() + Floats() []float64 + WriteFloats(samples []float64) +} + +/* + * The internal data structure representing a channel of a RIFF wave file. + */ +type channelStruct struct { + samples []float64 +} + +/* + * An interface type representing a RIFF wave file. + */ +type File interface { + BitDepth() uint16 + Bytes() ([]byte, error) + Channel(id uint16) (Channel, error) + ChannelCount() uint16 + SampleFormat() uint16 + SampleRate() uint32 +} + +/* + * The internal data structure representing a RIFF wave file. + */ +type fileStruct struct { + bitDepth uint16 + sampleFormat uint16 + sampleRate uint32 + channels []Channel +} + +/* + * The structure of a wave file's RIFF header. + */ +type riffHeader struct { + ChunkID uint32 + ChunkSize uint32 + Format uint32 +} + +/* + * The structure of a wave file's data size header. + */ +type dataSizeHeader struct { + ChunkID uint32 + ChunkSize uint32 + SizeRIFF uint64 + SizeData uint64 + SampleCount uint64 + TableLength uint32 +} + +/* + * The structure of a chunk header for pre-parsing. + */ +type chunkHeader struct { + ChunkID uint32 + ChunkSize uint32 +} + +/* + * The structure of a wave file's format header. + */ +type formatHeader struct { + ChunkID uint32 + ChunkSize uint32 + AudioFormat uint16 + ChannelCount uint16 + SampleRate uint32 + ByteRate uint32 + BlockAlign uint16 + BitDepth uint16 +} + +/* + * The structure of a wave file's data header. + */ +type dataHeader struct { + ChunkID uint32 + ChunkSize uint32 +} + +/* + * Clears all samples from the channel. + */ +func (this *channelStruct) Clear() { + this.samples = make([]float64, 0) +} + +/* + * Returns all samples inside this channel in floating-point representation. + */ +func (this *channelStruct) Floats() []float64 { + size := len(this.samples) + samples := make([]float64, size) + copy(samples, this.samples) + return samples +} + +/* + * Writes (appends) samples in floating-point representation to this channel. + */ +func (this *channelStruct) WriteFloats(samples []float64) { + this.samples = append(this.samples, samples...) +} + +/* + * Utility function for creating an empty buffer. + */ +func createBuffer() *bytes.Buffer { + buf := bytes.Buffer{} + return &buf +} + +/* + * Converts a slice of channels into a slice of samples. + */ +func channelsToSamples(channels []Channel) []float64 { + channelCount := len(channels) + channelCount16 := uint16(channelCount) + channelCount32 := uint32(channelCount) + samplesByChannel := make([][]float64, channelCount) + maxSampleCount := uint32(0) + + /* + * Iterate over all channels and extract the samples for each. + */ + for i, currentChannel := range channels { + currentSamples := currentChannel.Floats() + sampleCount := len(currentSamples) + sampleCount32 := uint32(sampleCount) + samplesByChannel[i] = currentSamples + + /* + * If we found a channel with more samples, make its sample + * count the new longest channel sample count. + */ + if sampleCount32 > maxSampleCount { + maxSampleCount = sampleCount32 + } + + } + + totalSampleCount := channelCount32 * maxSampleCount + data := make([]float64, totalSampleCount) + + /* + * Iterate over the samples to reorder them by time. + */ + for i := uint32(0); i < maxSampleCount; i++ { + + /* + * Iterate over the channels and extract the current sample. + */ + for j := uint16(0); j < channelCount16; j++ { + currentChannel := samplesByChannel[j] + currentChannelLength := len(currentChannel) + currentChannelLength32 := uint32(currentChannelLength) + currentSample := float64(0.0) + + /* + * If the channel is long enough, read the sample from it, + * otherwise pad with zeroes. + */ + if i < currentChannelLength32 { + currentSample = currentChannel[i] + } + + j32 := uint32(j) + offset := (channelCount32 * i) + j32 + data[offset] = currentSample + } + + } + + return data +} + +/* + * Converts a slice of samples into a slice of channels. + */ +func samplesToChannels(samples []float64, channelCount uint16) []Channel { + channels := make([]Channel, channelCount) + channelCount32 := uint32(channelCount) + size := len(samples) + size32 := uint32(size) + samplesPerChannel := size32 / channelCount32 + + /* + * Extract each channel from the sample data. + */ + for i := uint16(0); i < channelCount; i++ { + currentSamples := make([]float64, samplesPerChannel) + i32 := uint32(i) + + /* + * Extract each sample for this channel. + */ + for j := uint32(0); j < samplesPerChannel; j++ { + idx := (j * channelCount32) + i32 + currentSamples[j] = samples[idx] + } + + /* + * Data structure representing this channel. + */ + channel := channelStruct{ + samples: currentSamples, + } + + channels[i] = &channel + } + + return channels +} + +/* + * Convert samples to bytes, encoding them as 8-bit LPCM values. + */ +func samplesToBytesLPCM8(samples []float64) ([]byte, error) { + numSamples := len(samples) + data := make([]byte, numSamples) + scale := float64(math.MaxInt8) + + /* + * Iterate over the samples and encode them as 8-bit LPCM values. + */ + for i, sample := range samples { + + /* + * Make sure that limits are not exceeded. + */ + if sample < -1.0 { + sample = -1.0 + } else if sample > 1.0 { + sample = 1.0 + } + + temp := int16(scale * sample) + res := temp - math.MinInt8 + + /* + * Make sure that limits are not exceeded. + */ + if res < 0 { + data[i] = 0 + } else if res > math.MaxUint8 { + data[i] = math.MaxUint8 + } else { + data[i] = byte(res) + } + + } + + return data, nil +} + +/* + * Convert bytes, encoded as 8-bit LPCM values, to samples. + */ +func bytesToSamplesLPCM8(data []byte) ([]float64, error) { + numSamples := len(data) + samples := make([]float64, numSamples) + scale := 1.0 / float64(math.MaxInt8) + + /* + * Iterate over the samples and decode the 8-bit LPCM values. + */ + for i, byt := range data { + temp := int16(byt) + math.MinInt8 + res := scale * float64(temp) + + /* + * Make sure that limits are not exceeded. + */ + if res < -1.0 { + samples[i] = -1.0 + } else if res > 1.0 { + samples[i] = 1.0 + } else { + samples[i] = res + } + + } + + return samples, nil +} + +/* + * Convert samples to bytes, encoding them as 16-bit LPCM values. + */ +func samplesToBytesLPCM16(samples []float64) ([]byte, error) { + numSamples := len(samples) + samplesInt := make([]int16, numSamples) + const delta = math.MaxInt16 - math.MinInt16 + scale := 0.5 * float64(delta) + + /* + * Iterate over the samples and convert them into integer representation. + */ + for i, sample := range samples { + + /* + * Make sure that limits are not exceeded. + */ + if sample < -1.0 { + sample = -1.0 + } else if sample > 1.0 { + sample = 1.0 + } + + tmp := int32(scale * sample) + + /* + * Make sure that limits are not exceeded. + */ + if tmp > math.MaxInt16 { + tmp = math.MaxInt16 + } else if tmp < math.MinInt16 { + tmp = math.MinInt16 + } + + samplesInt[i] = int16(tmp) + } + + buf := createBuffer() + err := binary.Write(buf, binary.LittleEndian, samplesInt) + + /* + * Check if conversion was successful. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to convert samples: %s", msg) + } else { + data := buf.Bytes() + return data, nil + } + +} + +/* + * Convert bytes, encoded as 16-bit LPCM values, to samples. + */ +func bytesToSamplesLPCM16(data []byte) ([]float64, error) { + numBytes := len(data) + numBytes64 := uint64(numBytes) + numSamples := numBytes64 >> 1 + samplesInt := make([]int16, numSamples) + reader := bytes.NewReader(data) + err := binary.Read(reader, binary.LittleEndian, samplesInt) + + /* + * Check if conversion was successful. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to decode LPCM16 data: %s", msg) + } else { + samplesFloat := make([]float64, numSamples) + scaling := 2.0 / (math.MaxInt16 - math.MinInt16) + + /* + * Convert samples to floating-point representation. + */ + for i, sample := range samplesInt { + samplesFloat[i] = scaling * float64(sample) + } + + return samplesFloat, nil + } + +} + +/* + * Convert samples to bytes, encoding them as 24-bit LPCM values. + */ +func samplesToBytesLPCM24(samples []float64) ([]byte, error) { + const delta = MAX_INT24 - MIN_INT24 + scale := 0.5 * float64(delta) + buf := createBuffer() + + /* + * Iterate over the samples and convert them into integer representation. + */ + for _, sample := range samples { + + /* + * Make sure that limits are not exceeded. + */ + if sample < -1.0 { + sample = -1.0 + } else if sample > 1.0 { + sample = 1.0 + } + + tmp := int32(scale * sample) + + /* + * Make sure that limits are not exceeded. + */ + if tmp > MAX_INT24 { + tmp = MAX_INT24 + } else if tmp < MIN_INT24 { + tmp = MIN_INT24 + } + + sampleUint := uint32(tmp) + + /* + * Write each byte to the buffer. + */ + for j := 0; j < SIZE_INT24; j++ { + shift := BITS_PER_BYTE * uint32(j) + byt := byte((sampleUint >> shift) & 0xff) + buf.WriteByte(byt) + } + + } + + data := buf.Bytes() + return data, nil +} + +/* + * Convert bytes, encoded as 24-bit LPCM values, to samples. + */ +func bytesToSamplesLPCM24(data []byte) ([]float64, error) { + numBytes := len(data) + numBytes64 := uint64(numBytes) + numSamples := numBytes64 / SIZE_INT24 + samplesFloat := make([]float64, numSamples) + scaling := 2.0 / (MAX_INT24 - MIN_INT24) + reader := bytes.NewReader(data) + buf := make([]byte, SIZE_INT24) + words := make([]uint32, SIZE_INT24) + + /* + * Read samples from input stream. + */ + for idx := range samplesFloat { + reader.Read(buf) + + /* + * Turn the single bytes from the buffer into machine words. + */ + for i, byt := range buf { + words[i] = uint32(byt) + } + + sampleWord := uint32(0) + + /* + * Combine the extracted words into a single machine word. + */ + for i, word := range words { + shift := BITS_PER_BYTE * uint32(i) + sampleWord |= word << shift + } + + sampleInt := int32(sampleWord) + signBit := (sampleWord & SIGN_BIT_INT24) != 0 + + /* + * Handle negative values in two's complement representation. + */ + if signBit { + offset := sampleInt & MAX_INT24 + sampleInt = MIN_INT24 + offset + } + + samplesFloat[idx] = scaling * float64(sampleInt) + } + + return samplesFloat, nil +} + +/* + * Convert samples to bytes, encoding them as 32-bit LPCM values. + */ +func samplesToBytesLPCM32(samples []float64) ([]byte, error) { + numSamples := len(samples) + samplesInt := make([]int32, numSamples) + const delta = math.MaxInt32 - math.MinInt32 + scale := 0.5 * float64(delta) + + /* + * Iterate over the samples and convert them into integer representation. + */ + for i, sample := range samples { + + /* + * Make sure that limits are not exceeded. + */ + if sample < -1.0 { + sample = -1.0 + } else if sample > 1.0 { + sample = 1.0 + } + + tmp := int64(scale * sample) + + /* + * Make sure that limits are not exceeded. + */ + if tmp > math.MaxInt32 { + tmp = math.MaxInt32 + } else if tmp < math.MinInt32 { + tmp = math.MinInt32 + } + + samplesInt[i] = int32(tmp) + } + + buf := createBuffer() + err := binary.Write(buf, binary.LittleEndian, samplesInt) + + /* + * Check if conversion was successful. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to convert samples: %s", msg) + } else { + data := buf.Bytes() + return data, nil + } + +} + +/* + * Convert bytes, encoded as 32-bit LPCM values, to samples. + */ +func bytesToSamplesLPCM32(data []byte) ([]float64, error) { + numBytes := len(data) + numBytes64 := uint64(numBytes) + numSamples := numBytes64 >> 2 + samplesInt := make([]int32, numSamples) + reader := bytes.NewReader(data) + err := binary.Read(reader, binary.LittleEndian, samplesInt) + + /* + * Check if conversion was successful. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to decode LPCM32 data: %s", msg) + } else { + samplesFloat := make([]float64, numSamples) + scaling := 2.0 / (math.MaxInt32 - math.MinInt32) + + /* + * Convert samples to floating-point representation. + */ + for i, sample := range samplesInt { + samplesFloat[i] = scaling * float64(sample) + } + + return samplesFloat, nil + } + +} + +/* + * Convert samples to bytes, encoding them as 32-bit IEEE floating-point values. + */ +func samplesToBytesIEEE32(samples []float64) ([]byte, error) { + numSamples := len(samples) + samples32 := make([]float32, numSamples) + + /* + * Iterate over the samples and convert them into integer representation. + */ + for i, sample := range samples { + + /* + * Make sure that limits are not exceeded. + */ + if sample < -1.0 { + sample = -1.0 + } else if sample > 1.0 { + sample = 1.0 + } + + samples32[i] = float32(sample) + } + + buf := createBuffer() + err := binary.Write(buf, binary.LittleEndian, samples32) + + /* + * Check if conversion was successful. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to convert samples: %s", msg) + } else { + data := buf.Bytes() + return data, nil + } + +} + +/* + * Convert bytes, encoded as 32-bit IEEE floating-point values, to samples. + */ +func bytesToSamplesIEEE32(data []byte) ([]float64, error) { + numBytes := len(data) + numBytes64 := uint64(numBytes) + numSamples := numBytes64 >> 2 + samplesFloat32 := make([]float32, numSamples) + reader := bytes.NewReader(data) + err := binary.Read(reader, binary.LittleEndian, samplesFloat32) + + /* + * Check if conversion was successful. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to decode 32-bit IEEE floating-point data: %s", msg) + } else { + samplesFloat := make([]float64, numSamples) + + /* + * Convert samples to 64-bit floating-point representation. + */ + for i, sample := range samplesFloat32 { + samplesFloat[i] = float64(sample) + } + + return samplesFloat, nil + } + +} + +/* + * Convert samples to bytes, encoding them as 64-bit IEEE floating-point values. + */ +func samplesToBytesIEEE64(samples []float64) ([]byte, error) { + buf := createBuffer() + err := binary.Write(buf, binary.LittleEndian, samples) + + /* + * Check if conversion was successful. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to convert samples: %s", msg) + } else { + data := buf.Bytes() + return data, nil + } + +} + +/* + * Convert bytes, encoded as 64-bit IEEE floating-point values, to samples. + */ +func bytesToSamplesIEEE64(data []byte) ([]float64, error) { + numBytes := len(data) + numBytes64 := uint64(numBytes) + numSamples := numBytes64 >> 3 + samplesFloat64 := make([]float64, numSamples) + reader := bytes.NewReader(data) + err := binary.Read(reader, binary.LittleEndian, samplesFloat64) + + /* + * Check if conversion was successful. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to decode 64-bit IEEE floating-point data: %s", msg) + } else { + return samplesFloat64, nil + } + +} + +/* + * Convert samples to bytes, given a sample format and bit depth. + */ +func samplesToBytes(samples []float64, sampleFormat uint16, bitDepth uint16) ([]byte, error) { + + /* + * Decide on the sample format. + */ + switch sampleFormat { + case AUDIO_PCM: + + /* + * Decide on the bit depth. + */ + switch bitDepth { + case 8: + res, err := samplesToBytesLPCM8(samples) + return res, err + case 16: + res, err := samplesToBytesLPCM16(samples) + return res, err + case 24: + res, err := samplesToBytesLPCM24(samples) + return res, err + case 32: + res, err := samplesToBytesLPCM32(samples) + return res, err + default: + return nil, fmt.Errorf("Unsupported bit depth for audio in LPCM format: %d", bitDepth) + } + + case AUDIO_IEEE_FLOAT: + + /* + * Decide on the bit depth. + */ + switch bitDepth { + case 32: + res, err := samplesToBytesIEEE32(samples) + return res, err + case 64: + res, err := samplesToBytesIEEE64(samples) + return res, err + default: + return nil, fmt.Errorf("Unsupported bit depth for audio in IEEE floating-point format: %d", bitDepth) + } + + default: + return nil, fmt.Errorf("Unknown sample format: %#04x", sampleFormat) + } + +} + +/* + * Convert bytes to samples, given a sample format and bit depth. + */ +func bytesToSamples(data []byte, sampleFormat uint16, bitDepth uint16) ([]float64, error) { + + /* + * Decide on the sample format. + */ + switch sampleFormat { + case AUDIO_PCM: + + /* + * Decide on the bit depth. + */ + switch bitDepth { + case 8: + res, err := bytesToSamplesLPCM8(data) + return res, err + case 16: + res, err := bytesToSamplesLPCM16(data) + return res, err + case 24: + res, err := bytesToSamplesLPCM24(data) + return res, err + case 32: + res, err := bytesToSamplesLPCM32(data) + return res, err + default: + return nil, fmt.Errorf("Unsupported bit depth for audio in LPCM format: %d", bitDepth) + } + + case AUDIO_IEEE_FLOAT: + + /* + * Decide on the bit depth. + */ + switch bitDepth { + case 32: + res, err := bytesToSamplesIEEE32(data) + return res, err + case 64: + res, err := bytesToSamplesIEEE64(data) + return res, err + default: + return nil, fmt.Errorf("Unsupported bit depth for audio in IEEE floating-point format: %d", bitDepth) + } + + default: + return nil, fmt.Errorf("Unknown sample format: %#04x", sampleFormat) + } + +} + +/* + * Returns the sample depth of this wave file in bits. + */ +func (this *fileStruct) BitDepth() uint16 { + return this.bitDepth +} + +/* + * Returns the contents of this wave file as a byte slice. + */ +func (this *fileStruct) Bytes() ([]byte, error) { + channelCount := len(this.channels) + channelCount16 := uint16(channelCount) + channelCount32 := uint32(channelCount) + bitDepth := this.bitDepth + sampleFormat := this.sampleFormat + sampleRate := this.sampleRate + sampleSize32 := uint32(bitDepth / BITS_PER_BYTE) + sampleSize64 := uint64(sampleSize32) + blockAlign := sampleSize32 * channelCount32 + blockAlign16 := uint16(blockAlign) + byteRate := sampleRate * blockAlign + samples := channelsToSamples(this.channels) + numSamples := len(samples) + data, err := samplesToBytes(samples, sampleFormat, bitDepth) + + /* + * Check if conversion was successful. + */ + if err != nil { + return nil, err + } else { + idRIFF := uint32(ID_RIFF) + numSamples32 := uint32(numSamples) + numSamples64 := uint64(numSamples) + dataBytes32 := sampleSize32 * numSamples32 + dataBytes64 := sampleSize64 * numSamples64 + riffSize64 := dataBytes64 + (MIN_TOTAL_HEADER_SIZE - MIN_CHUNK_HEADER_SIZE) + riffSize32 := uint32(riffSize64) + requiresRF64 := riffSize64 > math.MaxUint32 + + /* + * If we write an RF64 file, replace RIFF chunk ID with 'RF64' and set 32-bit size to math.MaxUint32 (0xffffffff). + */ + if requiresRF64 { + idRIFF = uint32(ID_RIFF64) + riffSize32 = math.MaxUint32 + } + + /* + * Create RIFF header. + */ + hdrRiff := riffHeader{ + ChunkID: idRIFF, + ChunkSize: riffSize32, + Format: FORMAT_WAVE, + } + + /* + * Create data size header. + */ + hdrDataSize := dataSizeHeader{ + ChunkID: ID_DATASIZE, + ChunkSize: MIN_DATASIZE_CHUNK_SIZE, + SizeRIFF: riffSize64, + SizeData: dataBytes64, + SampleCount: numSamples64, + TableLength: 0, + } + + /* + * Create format header. + */ + hdrFormat := formatHeader{ + ChunkID: ID_FORMAT, + ChunkSize: MIN_CHUNK_SIZE_FORMAT, + AudioFormat: sampleFormat, + ChannelCount: channelCount16, + SampleRate: sampleRate, + ByteRate: byteRate, + BlockAlign: blockAlign16, + BitDepth: bitDepth, + } + + /* + * Create data header. + */ + hdrData := dataHeader{ + ChunkID: ID_DATA, + ChunkSize: dataBytes32, + } + + buf := createBuffer() + binary.Write(buf, binary.LittleEndian, hdrRiff) + + /* + * If we write an RF64 file, write mandatory data size chunk. + */ + if requiresRF64 { + binary.Write(buf, binary.LittleEndian, hdrDataSize) + } + + binary.Write(buf, binary.LittleEndian, hdrFormat) + binary.Write(buf, binary.LittleEndian, hdrData) + buf.Write(data) + content := buf.Bytes() + return content, nil + } + +} + +/* + * Returns a reference to the requested channel. + */ +func (this *fileStruct) Channel(id uint16) (Channel, error) { + channelCount := this.ChannelCount() + + /* + * Check whether the requested channel is available in this wave file. + */ + if id >= channelCount { + return nil, fmt.Errorf("No channel with id = %d in this wave file with channel count %d.", id, channelCount) + } else { + return this.channels[id], nil + } + +} + +/* + * Returns the number of channels available in this wave file. + */ +func (this *fileStruct) ChannelCount() uint16 { + n := len(this.channels) + n16 := uint16(n) + return n16 +} + +/* + * Returns the format code of the sample format of this wave file. + */ +func (this *fileStruct) SampleFormat() uint16 { + return this.sampleFormat +} + +/* + * Returns the sample rate of this wave file in Hertz. + */ +func (this *fileStruct) SampleRate() uint32 { + return this.sampleRate +} + +/* + * Creates an empty channel. + */ +func createChannel() Channel { + channel := channelStruct{} + return &channel +} + +/* + * Skips over a number of bytes in the file. + */ +func skipData(reader *bytes.Reader, numBytes uint64) error { + max := uint64(math.MaxInt32) + + /* + * Check if we can seek this far. + */ + if numBytes > max { + return fmt.Errorf("Cannot skip more than %d bytes.", max) + } else { + signedBytes := int64(numBytes) + mode := io.SeekCurrent + reader.Seek(signedBytes, mode) + return nil + } + +} + +/* + * Look ahead to the next chunk. + */ +func lookaheadChunk(reader *bytes.Reader) (*chunkHeader, error) { + hdrChunk := chunkHeader{} + err := binary.Read(reader, binary.LittleEndian, &hdrChunk) + + /* + * Check if chunk header was read. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to read chunk header: %s", msg) + } else { + mode := io.SeekCurrent + _, err = reader.Seek(-MIN_CHUNK_HEADER_SIZE, mode) + return &hdrChunk, err + } + +} + +/* + * Skip over chunks until you find one with a certain ID. + */ +func skipToChunk(reader *bytes.Reader, chunkId uint32) error { + abort := false + + /* + * Skip over chunks until we find the one we expect. + */ + for !abort { + hdrChunk, err := lookaheadChunk(reader) + + /* + * Check if lookahead was successful. + */ + if err != nil { + return err + } else { + id := hdrChunk.ChunkID + + /* + * If we found the right chunk, abort, otherwise skip over it. + */ + if id == chunkId { + abort = true + } else { + size := hdrChunk.ChunkSize + sizeLSB := size % 2 + + /* + * If chunk size is not even, we have to read one + * additional byte of padding. + */ + if sizeLSB != 0 { + size += 1 + } + + amount := uint64(size) + MIN_CHUNK_HEADER_SIZE + err = skipData(reader, amount) + + /* + * Check if skipping failed. + */ + if err != nil { + return err + } + + } + + } + + } + + return nil +} + +/* + * Read RIFF header from file and validate it. + */ +func readHeaderRIFF(reader *bytes.Reader, totalSize uint64) (*riffHeader, error) { + hdrRiff := riffHeader{} + err := binary.Read(reader, binary.LittleEndian, &hdrRiff) + + /* + * Check if RIFF header was read. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to read RIFF header: %s", msg) + } else { + expectedRiffChunkSize64 := totalSize - 8 + expectedRiffChunkSize32 := uint32(expectedRiffChunkSize64) + + /* + * Check RIFF header for validity. + */ + if hdrRiff.ChunkID != ID_RIFF { + return nil, fmt.Errorf("RIFF header contains invalid chunk id. Expected %#08x or %#08x, found %#08x.", ID_RIFF, ID_RIFF64, hdrRiff.ChunkID) + } else if (expectedRiffChunkSize64 < math.MaxUint32 && hdrRiff.ChunkSize != expectedRiffChunkSize32) || (hdrRiff.ChunkID == ID_RIFF64 && hdrRiff.ChunkSize != math.MaxUint32) { + return nil, fmt.Errorf("RIFF header contains invalid chunk size. Expected %#08x (or %#08x for 'RF64'), found %#08x.", expectedRiffChunkSize32, uint32(math.MaxUint32), hdrRiff.ChunkSize) + } else if hdrRiff.Format != FORMAT_WAVE { + return nil, fmt.Errorf("RIFF header contains invalid format. Expected %#08x, found %#08x.", FORMAT_WAVE, hdrRiff.Format) + } else { + return &hdrRiff, nil + } + + } + +} + +/* + * Read data size header from file and validate it. + */ +func readHeaderDataSize(reader *bytes.Reader, totalSize uint64) (*dataSizeHeader, error) { + hdrDataSize := dataSizeHeader{} + err := binary.Read(reader, binary.LittleEndian, &hdrDataSize) + + /* + * Check if data size header was read. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to read data size header: %s", msg) + } else { + expectedRiffChunkSize := totalSize - 8 + + /* + * Check data size header for validity. + */ + if hdrDataSize.ChunkID != ID_DATASIZE { + return nil, fmt.Errorf("Data size header contains invalid chunk id. Expected %#08x, found %#08x.", ID_DATASIZE, hdrDataSize.ChunkID) + } else if hdrDataSize.ChunkSize < MIN_DATASIZE_CHUNK_SIZE { + return nil, fmt.Errorf("Data size header has too small size. Expected at least %#08x, found %#08x.", MIN_DATASIZE_CHUNK_SIZE, hdrDataSize.ChunkSize) + } else if hdrDataSize.SizeRIFF != expectedRiffChunkSize { + return nil, fmt.Errorf("Unexpected RIFF chunk size in data size header. Expected %#08x, found %0#8x.", expectedRiffChunkSize, hdrDataSize.SizeRIFF) + } else { + return &hdrDataSize, nil + } + + } + +} + +/* + * Read format header from file and validate it. + */ +func readHeaderFormat(reader *bytes.Reader) (*formatHeader, error) { + hdrFormat := formatHeader{} + err := binary.Read(reader, binary.LittleEndian, &hdrFormat) + + /* + * Check if format header was read. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to read format header: %s", msg) + } else { + channelCount := hdrFormat.ChannelCount + bitDepth := hdrFormat.BitDepth + sampleRate := hdrFormat.SampleRate + frameSize := channelCount * bitDepth + expectedBlockAlign32 := uint32(frameSize / BITS_PER_BYTE) + expectedBlockAlign16 := uint16(expectedBlockAlign32) + expectedByteRate := expectedBlockAlign32 * sampleRate + chunkSize := int64(hdrFormat.ChunkSize) + numBytesSkip := chunkSize - MIN_CHUNK_SIZE_FORMAT + + /* + * Skip optional fields in the format header. + */ + if numBytesSkip > 0 { + + /* + * If this is even, we need to skip one more. + */ + if (numBytesSkip % 2) == 0 { + numBytesSkip += 1 + } + + amount := uint64(numBytesSkip) + skipData(reader, amount) + } + + /* + * Check format header for validity. + */ + if hdrFormat.ChunkID != ID_FORMAT { + return nil, fmt.Errorf("Format header contains invalid chunk id. Expected %#08x, found %#08x.", ID_FORMAT, hdrFormat.ChunkID) + } else if hdrFormat.ChunkSize < MIN_CHUNK_SIZE_FORMAT { + return nil, fmt.Errorf("Format header contains invalid chunk size. Expected at least %#08x, found %#08x.", MIN_CHUNK_SIZE_FORMAT, hdrFormat.ChunkSize) + } else if hdrFormat.AudioFormat != AUDIO_PCM && hdrFormat.AudioFormat != AUDIO_IEEE_FLOAT { + return nil, fmt.Errorf("Format header contains invalid audio format. Expected %#04x or %#04x, found %#04x.", AUDIO_PCM, AUDIO_IEEE_FLOAT, hdrFormat.AudioFormat) + } else if hdrFormat.ByteRate != expectedByteRate { + return nil, fmt.Errorf("Format header contains invalid byte rate. Expected %#08x, found %#08x.", expectedByteRate, hdrFormat.ByteRate) + } else if hdrFormat.BlockAlign != expectedBlockAlign16 { + return nil, fmt.Errorf("Format header contains invalid block align. Expected %#04x, found %#04x.", expectedBlockAlign16, hdrFormat.BlockAlign) + } else if hdrFormat.AudioFormat == AUDIO_PCM && hdrFormat.BitDepth != 8 && hdrFormat.BitDepth != 16 && hdrFormat.BitDepth != 24 && hdrFormat.BitDepth != 32 { + return nil, fmt.Errorf("Format header contains invalid bit depth for PCM format. Expected %#04x or %#04x or %#04x or %#04x, found %#04x.", 8, 16, 24, 32, hdrFormat.BitDepth) + } else if hdrFormat.AudioFormat == AUDIO_IEEE_FLOAT && hdrFormat.BitDepth != 32 && hdrFormat.BitDepth != 64 { + return nil, fmt.Errorf("Format header contains invalid bit depth for IEEE floating-point format. Expected %#04x or %#04x, found %#04x.", 32, 64, hdrFormat.BitDepth) + } else { + return &hdrFormat, nil + } + + } + +} + +/* + * Read data header from file and validate it. + */ +func readHeaderData(reader *bytes.Reader, totalSize uint64) (*dataHeader, error) { + hdrData := dataHeader{} + err := binary.Read(reader, binary.LittleEndian, &hdrData) + + /* + * Check if data header was read. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to read data header: %s", msg) + } else { + maxDataLength := totalSize - MIN_TOTAL_HEADER_SIZE + maxDataLength32 := uint32(maxDataLength) + chunkId := hdrData.ChunkID + chunkSize := hdrData.ChunkSize + + /* + * Check data header for validity. + */ + if chunkId != ID_DATA { + return nil, fmt.Errorf("Data header contains invalid chunk id. Expected %#08x, found %#08x.", ID_DATA, chunkId) + } else if (chunkSize > maxDataLength32) && (chunkSize != math.MaxUint32) { + return nil, fmt.Errorf("Data header contains invalid chunk size. Expected at most %#08x (or %#08x), found %#08x.", maxDataLength32, uint32(math.MaxUint32), chunkSize) + } else { + return &hdrData, nil + } + + } + +} + +/* + * Create an empty wave file with the desired sample rate, sample format, bit depth and channel count. + */ +func CreateEmpty(sampleRate uint32, sampleFormat uint16, bitDepth uint16, channelCount uint16) (File, error) { + + /* + * Check if sample format is valid. + */ + if sampleFormat != AUDIO_PCM && sampleFormat != AUDIO_IEEE_FLOAT { + return nil, fmt.Errorf("Unknown sample format: %#04x - Expected either %#04x or %#04x.", sampleFormat, AUDIO_PCM, AUDIO_IEEE_FLOAT) + } else { + + /* + * Check if bit depth is valid for sample format. + */ + if sampleFormat == AUDIO_PCM && bitDepth != 8 && bitDepth != 16 && bitDepth != 24 && bitDepth != 32 { + return nil, fmt.Errorf("Bit depth must be either %d or %d or %d or %d for audio in PCM format.", 8, 16, 24, 32) + } else if sampleFormat == AUDIO_IEEE_FLOAT && bitDepth != 32 && bitDepth != 64 { + return nil, fmt.Errorf("Bit depth must be either %d or %d for audio in IEEE floating-point format.", 32, 64) + } else { + channels := make([]Channel, channelCount) + + /* + * Create channels for this wave file. + */ + for i := uint16(0); i < channelCount; i++ { + channels[i] = createChannel() + } + + /* + * Create wave file structure. + */ + file := fileStruct{ + bitDepth: bitDepth, + sampleFormat: sampleFormat, + sampleRate: sampleRate, + channels: channels, + } + + return &file, nil + } + + } + +} + +/* + * Creates a wave file from the contents of a byte buffer. + */ +func FromBuffer(buffer []byte) (File, error) { + totalSize := len(buffer) + totalSize64 := uint64(totalSize) + reader := bytes.NewReader(buffer) + hdrRiff, err := readHeaderRIFF(reader, totalSize64) + + /* + * Check if RIFF header was successfully read. + */ + if err != nil { + return nil, err + } else { + hdrDataSize := &dataSizeHeader{} + + /* + * If this is an 'RF64' file, read data size header. + */ + if hdrRiff.ChunkID == ID_RIFF64 { + hdrDataSize, err = readHeaderDataSize(reader, totalSize64) + + /* + * If data size header was successfully read, skip over optional table entries. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to read data size chunk: %s", msg) + } else { + numEntries := hdrDataSize.TableLength + numEntries64 := uint64(numEntries) + bytesToSkip := LENGTH_DATASIZE_TABLE_ENTRIES * numEntries64 + err := skipData(reader, bytesToSkip) + + /* + * Check if we successfully skipped over the table entries. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to skip over data size table entries: %s", msg) + } + + } + + } + + hdrFormat, err := readHeaderFormat(reader) + + /* + * Check if format header was successfully read. + */ + if err != nil { + return nil, err + } else { + bitDepth := hdrFormat.BitDepth + sampleFormat := hdrFormat.AudioFormat + err = skipToChunk(reader, ID_DATA) + + /* + * Check if we successfully arrived at the data chunk. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to locate data chunk: %s", msg) + } else { + hdrData, err := readHeaderData(reader, totalSize64) + chunkSize32 := hdrData.ChunkSize + chunkSize64 := uint64(chunkSize32) + + /* + * If this is an 'RF64' file, take chunk size from data size header. + */ + if hdrRiff.ChunkID == ID_RIFF64 { + chunkSize64 = hdrDataSize.SizeData + } + + /* + * Check if data header was successfully read. + */ + if err != nil { + return nil, err + } else { + sampleData := make([]byte, chunkSize64) + _, err = reader.Read(sampleData) + + /* + * Check if sample data was read. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to read sample data: %s", msg) + } else { + samples, err := bytesToSamples(sampleData, sampleFormat, bitDepth) + + /* + * Check if sample data was decoded. + */ + if err != nil { + msg := err.Error() + return nil, fmt.Errorf("Failed to decode sample data: %s", msg) + } else { + channelCount := hdrFormat.ChannelCount + channels := samplesToChannels(samples, channelCount) + + /* + * Create a new data structure representing the contents of the wave file. + */ + file := fileStruct{ + bitDepth: bitDepth, + sampleFormat: sampleFormat, + sampleRate: hdrFormat.SampleRate, + channels: channels, + } + + return &file, nil + } + + } + + } + + } + + } + + } + +} diff --git a/wave/wave_test.go b/wave/wave_test.go new file mode 100644 index 0000000..b2c92a6 --- /dev/null +++ b/wave/wave_test.go @@ -0,0 +1,1630 @@ +package wave + +import ( + "fmt" + "math" + "testing" +) + +/* + * Compare two real-valued slices to check whether their components are close. + */ +func areSlicesClose(a []float64, b []float64, err float64) (bool, []float64) { + + /* + * Check whether the two slices are of the same size. + */ + if len(a) != len(b) { + return false, nil + } else { + c := true + n := len(a) + diffs := make([]float64, n) + + /* + * Iterate over the arrays to compare values. + */ + for i, elem := range a { + diff := elem - b[i] + diffAbs := math.Abs(diff) + + /* + * Check if we found a significant difference. + */ + if diffAbs > err { + c = false + } + + diffs[i] = diff + } + + return c, diffs + } + +} + +/* + * Compare two byte slices to check whether they are equal. + */ +func areSlicesEqual(a []byte, b []byte) bool { + + /* + * Check whether the two slices are of the same size. + */ + if len(a) != len(b) { + return false + } else { + c := true + + /* + * Iterate over the arrays to compare values. + */ + for i, elem := range a { + c = c && (elem == b[i]) + } + + return c + } + +} + +/* + * Convert buffer to hex string. + */ +func bufferToHex(buf []byte) string { + s := "[" + + /* + * Serialize all bytes into their hexadecimal representation. + */ + for i, b := range buf { + + /* + * Prepend comma if this is not the first element. + */ + if i > 0 { + s += ", " + } + + s += fmt.Sprintf("0x%02x", b) + } + + s += "]" + return s +} + +/* + * Test creating an 8-bit mono PCM wave file. + */ +func TestExportPCM8Mono(t *testing.T) { + + /* + * Sample data for testing. + */ + samples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + /* + * Expected output buffer. + */ + expectedOutput := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x38, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0x77, 0x01, 0x00, + 0x01, 0x00, 0x08, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x14, 0x00, 0x00, 0x00, 0x57, 0x87, 0x01, 0x20, + 0xd5, 0x5c, 0xea, 0x34, 0x06, 0x40, 0x6d, 0xff, + 0x9a, 0x8d, 0x94, 0xa6, 0x80, 0x76, 0xd6, 0x8c, + } + + w, err := CreateEmpty(96000, AUDIO_PCM, 8, 1) + + /* + * Check if wave file was successfully created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to create wave file: %s", msg) + } else { + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + c.WriteFloats(samples) + buf, err := w.Bytes() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain byte buffer failed.") + } else { + + /* + * Make sure that buffer is non-nil. + */ + if buf == nil { + t.Errorf("%s", "Byte buffer is nil.") + } else { + equal := areSlicesEqual(buf, expectedOutput) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + expectedOutputString := bufferToHex(expectedOutput) + actualOutputString := bufferToHex(buf) + t.Errorf("Byte buffers are not equal. Expected: %s Got: %s", expectedOutputString, actualOutputString) + } + + } + + } + + } + + } + + } + +} + +/* + * Test reading an 8-bit mono PCM wave file. + */ +func TestImportPCM8Mono(t *testing.T) { + + /* + * Input buffer. + */ + buf := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x38, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0x77, 0x01, 0x00, + 0x01, 0x00, 0x08, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x14, 0x00, 0x00, 0x00, 0x57, 0x87, 0x01, 0x20, + 0xd5, 0x5c, 0xea, 0x34, 0x06, 0x40, 0x6d, 0xff, + 0x9a, 0x8d, 0x94, 0xa6, 0x80, 0x76, 0xd6, 0x8c, + } + + /* + * Expected sample data. + */ + expectedSamples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + w, err := FromBuffer(buf) + + /* + * Check if wave file was read created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to read wave file: %s", msg) + } else { + sampleRate := w.SampleRate() + + /* + * Check if sample rate was determined successfully. + */ + if sampleRate != 96000 { + t.Errorf("Attempt to determine sample rate failed. Expected %d, got %d.", 96000, sampleRate) + } + + numChannels := w.ChannelCount() + + /* + * Check if sample rate was determined successfully. + */ + if numChannels != 1 { + t.Errorf("Attempt to determine channel count failed. Expected %d, got %d.", 1, numChannels) + } + + sampleFormat := w.SampleFormat() + + /* + * Check if sample format was determined successfully. + */ + if sampleFormat != AUDIO_PCM { + t.Errorf("Attempt to determine sample format failed. Expected %d, got %d.", AUDIO_PCM, sampleFormat) + } + + bitDepth := w.BitDepth() + + /* + * Check if bit depth was determined successfully. + */ + if bitDepth != 8 { + t.Errorf("Attempt to determine bit depth failed. Expected %d, got %d.", 8, bitDepth) + } + + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + samples := c.Floats() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if samples == nil { + t.Errorf("%s", "Sample buffer is nil.") + } else { + equal, diff := areSlicesClose(samples, expectedSamples, 0.078125) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + t.Errorf("Sample buffers are not similar. Expected: %v Got: %v Difference: %v", expectedSamples, samples, diff) + } + + } + + } + + } + + } + +} + +/* + * Test creating a 16-bit mono PCM wave file. + */ +func TestExportPCM16Mono(t *testing.T) { + + /* + * Sample data for testing. + */ + samples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + /* + * Expected output buffer. + */ + expectedOutput := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x4c, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0xee, 0x02, 0x00, + 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x28, 0x00, 0x00, 0x00, 0xfc, 0xd5, 0xe5, 0x07, + 0x01, 0x80, 0x6a, 0x9e, 0x3d, 0x56, 0x34, 0xdb, + 0x68, 0x6b, 0x04, 0xb3, 0xb9, 0x84, 0x49, 0xbf, + 0x5d, 0xec, 0xff, 0x7f, 0xf0, 0x1a, 0x74, 0x0d, + 0x1a, 0x15, 0x20, 0x27, 0x00, 0x00, 0xbc, 0xf5, + 0xa9, 0x57, 0x54, 0x0c, + } + + w, err := CreateEmpty(96000, AUDIO_PCM, 16, 1) + + /* + * Check if wave file was successfully created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to create wave file: %s", msg) + } else { + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + c.WriteFloats(samples) + buf, err := w.Bytes() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain byte buffer failed.") + } else { + + /* + * Make sure that buffer is non-nil. + */ + if buf == nil { + t.Errorf("%s", "Byte buffer is nil.") + } else { + equal := areSlicesEqual(buf, expectedOutput) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + expectedOutputString := bufferToHex(expectedOutput) + actualOutputString := bufferToHex(buf) + t.Errorf("Byte buffers are not equal. Expected: %s Got: %s", expectedOutputString, actualOutputString) + } + + } + + } + + } + + } + + } + +} + +/* + * Test reading an 16-bit mono PCM wave file. + */ +func TestImportPCM16Mono(t *testing.T) { + + /* + * Input buffer. + */ + buf := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x4c, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0xee, 0x02, 0x00, + 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x28, 0x00, 0x00, 0x00, 0xfc, 0xd5, 0xe5, 0x07, + 0x01, 0x80, 0x6a, 0x9e, 0x3d, 0x56, 0x34, 0xdb, + 0x68, 0x6b, 0x04, 0xb3, 0xb9, 0x84, 0x49, 0xbf, + 0x5d, 0xec, 0xff, 0x7f, 0xf0, 0x1a, 0x74, 0x0d, + 0x1a, 0x15, 0x20, 0x27, 0x00, 0x00, 0xbc, 0xf5, + 0xa9, 0x57, 0x54, 0x0c, + } + + /* + * Expected sample data. + */ + expectedSamples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + w, err := FromBuffer(buf) + + /* + * Check if wave file was read created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to read wave file: %s", msg) + } else { + sampleRate := w.SampleRate() + + /* + * Check if sample rate was determined successfully. + */ + if sampleRate != 96000 { + t.Errorf("Attempt to determine sample rate failed. Expected %d, got %d.", 96000, sampleRate) + } + + numChannels := w.ChannelCount() + + /* + * Check if sample rate was determined successfully. + */ + if numChannels != 1 { + t.Errorf("Attempt to determine channel count failed. Expected %d, got %d.", 1, numChannels) + } + + sampleFormat := w.SampleFormat() + + /* + * Check if sample format was determined successfully. + */ + if sampleFormat != AUDIO_PCM { + t.Errorf("Attempt to determine sample format failed. Expected %d, got %d.", AUDIO_PCM, sampleFormat) + } + + bitDepth := w.BitDepth() + + /* + * Check if bit depth was determined successfully. + */ + if bitDepth != 16 { + t.Errorf("Attempt to determine bit depth failed. Expected %d, got %d.", 16, bitDepth) + } + + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + samples := c.Floats() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if samples == nil { + t.Errorf("%s", "Sample buffer is nil.") + } else { + equal, diff := areSlicesClose(samples, expectedSamples, 3.0518e-5) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + t.Errorf("Sample buffers are not similar. Expected: %v Got: %v Difference: %v", expectedSamples, samples, diff) + } + + } + + } + + } + + } + +} + +/* + * Test creating a 24-bit mono PCM wave file. + */ +func TestExportPCM24Mono(t *testing.T) { + + /* + * Sample data for testing. + */ + samples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + /* + * Expected output buffer. + */ + expectedOutput := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x60, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0x65, 0x04, 0x00, + 0x03, 0x00, 0x18, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x3c, 0x00, 0x00, 0x00, 0x9d, 0xfb, 0xd5, 0xac, + 0xe5, 0x07, 0x01, 0x00, 0x80, 0xf7, 0x68, 0x9e, + 0x84, 0x3d, 0x56, 0x3c, 0x33, 0xdb, 0xe3, 0x68, + 0x6b, 0x9e, 0x03, 0xb3, 0x4e, 0xb8, 0x84, 0x7d, + 0x48, 0xbf, 0x49, 0x5c, 0xec, 0xff, 0xff, 0x7f, + 0x4f, 0xf0, 0x1a, 0x86, 0x74, 0x0d, 0xb6, 0x1a, + 0x15, 0xdf, 0x20, 0x27, 0x00, 0x00, 0x00, 0x51, + 0xbb, 0xf5, 0x79, 0xa9, 0x57, 0x37, 0x54, 0x0c, + } + + w, err := CreateEmpty(96000, AUDIO_PCM, 24, 1) + + /* + * Check if wave file was successfully created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to create wave file: %s", msg) + } else { + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + c.WriteFloats(samples) + buf, err := w.Bytes() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain byte buffer failed.") + } else { + + /* + * Make sure that buffer is non-nil. + */ + if buf == nil { + t.Errorf("%s", "Byte buffer is nil.") + } else { + equal := areSlicesEqual(buf, expectedOutput) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + expectedOutputString := bufferToHex(expectedOutput) + actualOutputString := bufferToHex(buf) + t.Errorf("Byte buffers are not equal. Expected: %s Got: %s", expectedOutputString, actualOutputString) + } + + } + + } + + } + + } + + } + +} + +/* + * Test reading a 24-bit mono PCM wave file. + */ +func TestImportPCM24Mono(t *testing.T) { + + /* + * Input buffer. + */ + buf := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x60, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0x65, 0x04, 0x00, + 0x03, 0x00, 0x18, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x3c, 0x00, 0x00, 0x00, 0x9d, 0xfb, 0xd5, 0xac, + 0xe5, 0x07, 0x01, 0x00, 0x80, 0xf7, 0x68, 0x9e, + 0x84, 0x3d, 0x56, 0x3c, 0x33, 0xdb, 0xe3, 0x68, + 0x6b, 0x9e, 0x03, 0xb3, 0x4e, 0xb8, 0x84, 0x7d, + 0x48, 0xbf, 0x49, 0x5c, 0xec, 0xff, 0xff, 0x7f, + 0x4f, 0xf0, 0x1a, 0x86, 0x74, 0x0d, 0xb6, 0x1a, + 0x15, 0xdf, 0x20, 0x27, 0x00, 0x00, 0x00, 0x51, + 0xbb, 0xf5, 0x79, 0xa9, 0x57, 0x37, 0x54, 0x0c, + } + + /* + * Expected sample data. + */ + expectedSamples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + w, err := FromBuffer(buf) + + /* + * Check if wave file was read created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to read wave file: %s", msg) + } else { + sampleRate := w.SampleRate() + + /* + * Check if sample rate was determined successfully. + */ + if sampleRate != 96000 { + t.Errorf("Attempt to determine sample rate failed. Expected %d, got %d.", 96000, sampleRate) + } + + numChannels := w.ChannelCount() + + /* + * Check if sample rate was determined successfully. + */ + if numChannels != 1 { + t.Errorf("Attempt to determine channel count failed. Expected %d, got %d.", 1, numChannels) + } + + sampleFormat := w.SampleFormat() + + /* + * Check if sample format was determined successfully. + */ + if sampleFormat != AUDIO_PCM { + t.Errorf("Attempt to determine sample format failed. Expected %d, got %d.", AUDIO_PCM, sampleFormat) + } + + bitDepth := w.BitDepth() + + /* + * Check if bit depth was determined successfully. + */ + if bitDepth != 24 { + t.Errorf("Attempt to determine bit depth failed. Expected %d, got %d.", 24, bitDepth) + } + + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + samples := c.Floats() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if samples == nil { + t.Errorf("%s", "Sample buffer is nil.") + } else { + equal, diff := areSlicesClose(samples, expectedSamples, 1.1921e-7) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + t.Errorf("Sample buffers are not similar. Expected: %v Got: %v Difference: %v", expectedSamples, samples, diff) + } + + } + + } + + } + + } + +} + +/* + * Test creating a 32-bit mono PCM wave file. + */ +func TestExportPCM32Mono(t *testing.T) { + + /* + * Sample data for testing. + */ + samples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + /* + * Expected output buffer. + */ + expectedOutput := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x74, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0xdc, 0x05, 0x00, + 0x04, 0x00, 0x20, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x50, 0x00, 0x00, 0x00, 0xaf, 0x9c, 0xfb, 0xd5, + 0x97, 0xac, 0xe5, 0x07, 0x01, 0x00, 0x00, 0x80, + 0xe4, 0xf5, 0x68, 0x9e, 0x46, 0x85, 0x3d, 0x56, + 0x6c, 0x3b, 0x33, 0xdb, 0x6d, 0xe3, 0x68, 0x6b, + 0x19, 0x9d, 0x03, 0xb3, 0xe4, 0x4c, 0xb8, 0x84, + 0xdb, 0x7b, 0x48, 0xbf, 0x96, 0x48, 0x5c, 0xec, + 0xff, 0xff, 0xff, 0x7f, 0x5d, 0x4f, 0xf0, 0x1a, + 0x0e, 0x86, 0x74, 0x0d, 0x10, 0xb7, 0x1a, 0x15, + 0x73, 0xdf, 0x20, 0x27, 0x00, 0x00, 0x00, 0x00, + 0x79, 0x50, 0xbb, 0xf5, 0x0c, 0x7a, 0xa9, 0x57, + 0x8f, 0x37, 0x54, 0x0c, + } + + w, err := CreateEmpty(96000, AUDIO_PCM, 32, 1) + + /* + * Check if wave file was successfully created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to create wave file: %s", msg) + } else { + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + c.WriteFloats(samples) + buf, err := w.Bytes() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain byte buffer failed.") + } else { + + /* + * Make sure that buffer is non-nil. + */ + if buf == nil { + t.Errorf("%s", "Byte buffer is nil.") + } else { + equal := areSlicesEqual(buf, expectedOutput) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + expectedOutputString := bufferToHex(expectedOutput) + actualOutputString := bufferToHex(buf) + t.Errorf("Byte buffers are not equal. Expected: %s Got: %s", expectedOutputString, actualOutputString) + } + + } + + } + + } + + } + + } + +} + +/* + * Test reading a 32-bit mono PCM wave file. + */ +func TestImportPCM32Mono(t *testing.T) { + + /* + * Input buffer. + */ + buf := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x74, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0xdc, 0x05, 0x00, + 0x04, 0x00, 0x20, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x50, 0x00, 0x00, 0x00, 0xaf, 0x9c, 0xfb, 0xd5, + 0x97, 0xac, 0xe5, 0x07, 0x01, 0x00, 0x00, 0x80, + 0xe4, 0xf5, 0x68, 0x9e, 0x46, 0x85, 0x3d, 0x56, + 0x6c, 0x3b, 0x33, 0xdb, 0x6d, 0xe3, 0x68, 0x6b, + 0x19, 0x9d, 0x03, 0xb3, 0xe4, 0x4c, 0xb8, 0x84, + 0xdb, 0x7b, 0x48, 0xbf, 0x96, 0x48, 0x5c, 0xec, + 0xff, 0xff, 0xff, 0x7f, 0x5d, 0x4f, 0xf0, 0x1a, + 0x0e, 0x86, 0x74, 0x0d, 0x10, 0xb7, 0x1a, 0x15, + 0x73, 0xdf, 0x20, 0x27, 0x00, 0x00, 0x00, 0x00, + 0x79, 0x50, 0xbb, 0xf5, 0x0c, 0x7a, 0xa9, 0x57, + 0x8f, 0x37, 0x54, 0x0c, + } + + /* + * Expected sample data. + */ + expectedSamples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + w, err := FromBuffer(buf) + + /* + * Check if wave file was read created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to read wave file: %s", msg) + } else { + sampleRate := w.SampleRate() + + /* + * Check if sample rate was determined successfully. + */ + if sampleRate != 96000 { + t.Errorf("Attempt to determine sample rate failed. Expected %d, got %d.", 96000, sampleRate) + } + + numChannels := w.ChannelCount() + + /* + * Check if sample rate was determined successfully. + */ + if numChannels != 1 { + t.Errorf("Attempt to determine channel count failed. Expected %d, got %d.", 1, numChannels) + } + + sampleFormat := w.SampleFormat() + + /* + * Check if sample format was determined successfully. + */ + if sampleFormat != AUDIO_PCM { + t.Errorf("Attempt to determine sample format failed. Expected %d, got %d.", AUDIO_PCM, sampleFormat) + } + + bitDepth := w.BitDepth() + + /* + * Check if bit depth was determined successfully. + */ + if bitDepth != 32 { + t.Errorf("Attempt to determine bit depth failed. Expected %d, got %d.", 32, bitDepth) + } + + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + samples := c.Floats() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if samples == nil { + t.Errorf("%s", "Sample buffer is nil.") + } else { + equal, diff := areSlicesClose(samples, expectedSamples, 4.6567e-10) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + t.Errorf("Sample buffers are not similar. Expected: %v Got: %v Difference: %v", expectedSamples, samples, diff) + } + + } + + } + + } + + } + +} + +/* + * Test creating a 32-bit IEEE floating-point wave file. + */ +func TestExportIEEE32Mono(t *testing.T) { + + /* + * Sample data for testing. + */ + samples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + /* + * Expected output buffer. + */ + expectedOutput := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x74, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0xdc, 0x05, 0x00, + 0x04, 0x00, 0x20, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x50, 0x00, 0x00, 0x00, 0x8d, 0x11, 0xa8, 0xbe, + 0x93, 0xb5, 0x7c, 0x3d, 0x00, 0x00, 0x80, 0xbf, + 0x14, 0x2e, 0x43, 0xbf, 0x0b, 0x7b, 0x2c, 0x3f, + 0x12, 0x33, 0x93, 0xbe, 0xc7, 0xd1, 0x56, 0x3f, + 0xc6, 0xf8, 0x19, 0xbf, 0x66, 0x8f, 0x76, 0xbf, + 0x08, 0x6f, 0x01, 0xbf, 0xbb, 0x1d, 0x1d, 0xbe, + 0x00, 0x00, 0x80, 0x3f, 0x7b, 0x82, 0x57, 0x3e, + 0x61, 0x48, 0xd7, 0x3d, 0xb9, 0xd5, 0x28, 0x3e, + 0x7e, 0x83, 0x9c, 0x3e, 0x00, 0x00, 0x00, 0x00, + 0xf8, 0x4a, 0xa4, 0xbd, 0xf4, 0x52, 0x2f, 0x3f, + 0x79, 0x43, 0xc5, 0x3d, + } + + w, err := CreateEmpty(96000, AUDIO_IEEE_FLOAT, 32, 1) + + /* + * Check if wave file was successfully created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to create wave file: %s", msg) + } else { + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + c.WriteFloats(samples) + buf, err := w.Bytes() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain byte buffer failed.") + } else { + + /* + * Make sure that buffer is non-nil. + */ + if buf == nil { + t.Errorf("%s", "Byte buffer is nil.") + } else { + equal := areSlicesEqual(buf, expectedOutput) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + expectedOutputString := bufferToHex(expectedOutput) + actualOutputString := bufferToHex(buf) + t.Errorf("Byte buffers are not equal. Expected: %s Got: %s", expectedOutputString, actualOutputString) + } + + } + + } + + } + + } + + } + +} + +/* + * Test reading an 32-bit IEEE floating-point wave file. + */ +func TestImportIEEE32Mono(t *testing.T) { + + /* + * Input buffer. + */ + buf := []byte{ + 0x52, 0x49, 0x46, 0x46, 0x74, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0xdc, 0x05, 0x00, + 0x04, 0x00, 0x20, 0x00, 0x64, 0x61, 0x74, 0x61, + 0x50, 0x00, 0x00, 0x00, 0x8d, 0x11, 0xa8, 0xbe, + 0x93, 0xb5, 0x7c, 0x3d, 0x00, 0x00, 0x80, 0xbf, + 0x14, 0x2e, 0x43, 0xbf, 0x0b, 0x7b, 0x2c, 0x3f, + 0x12, 0x33, 0x93, 0xbe, 0xc7, 0xd1, 0x56, 0x3f, + 0xc6, 0xf8, 0x19, 0xbf, 0x66, 0x8f, 0x76, 0xbf, + 0x08, 0x6f, 0x01, 0xbf, 0xbb, 0x1d, 0x1d, 0xbe, + 0x00, 0x00, 0x80, 0x3f, 0x7b, 0x82, 0x57, 0x3e, + 0x61, 0x48, 0xd7, 0x3d, 0xb9, 0xd5, 0x28, 0x3e, + 0x7e, 0x83, 0x9c, 0x3e, 0x00, 0x00, 0x00, 0x00, + 0xf8, 0x4a, 0xa4, 0xbd, 0xf4, 0x52, 0x2f, 0x3f, + 0x79, 0x43, 0xc5, 0x3d, + } + + /* + * Expected sample data. + */ + expectedSamples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + w, err := FromBuffer(buf) + + /* + * Check if wave file was read created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to read wave file: %s", msg) + } else { + sampleRate := w.SampleRate() + + /* + * Check if sample rate was determined successfully. + */ + if sampleRate != 96000 { + t.Errorf("Attempt to determine sample rate failed. Expected %d, got %d.", 96000, sampleRate) + } + + numChannels := w.ChannelCount() + + /* + * Check if sample rate was determined successfully. + */ + if numChannels != 1 { + t.Errorf("Attempt to determine channel count failed. Expected %d, got %d.", 1, numChannels) + } + + sampleFormat := w.SampleFormat() + + /* + * Check if sample format was determined successfully. + */ + if sampleFormat != AUDIO_IEEE_FLOAT { + t.Errorf("Attempt to determine sample format failed. Expected %d, got %d.", AUDIO_IEEE_FLOAT, sampleFormat) + } + + bitDepth := w.BitDepth() + + /* + * Check if bit depth was determined successfully. + */ + if bitDepth != 32 { + t.Errorf("Attempt to determine bit depth failed. Expected %d, got %d.", 32, bitDepth) + } + + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + samples := c.Floats() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if samples == nil { + t.Errorf("%s", "Sample buffer is nil.") + } else { + equal, diff := areSlicesClose(samples, expectedSamples, 1.1921e-7) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + t.Errorf("Sample buffers are not similar. Expected: %v Got: %v Difference: %v", expectedSamples, samples, diff) + } + + } + + } + + } + + } + +} + +/* + * Test creating a 64-bit IEEE floating-point wave file. + */ +func TestExportIEEE64Mono(t *testing.T) { + + /* + * Sample data for testing. + */ + samples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + /* + * Expected output buffer. + */ + expectedOutput := []byte{ + 0x52, 0x49, 0x46, 0x46, 0xc4, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0xb8, 0x0b, 0x00, + 0x08, 0x00, 0x40, 0x00, 0x64, 0x61, 0x74, 0x61, + 0xa0, 0x00, 0x00, 0x00, 0xd5, 0x84, 0xc4, 0xa8, + 0x31, 0x02, 0xd5, 0xbf, 0x51, 0x7d, 0x8c, 0x5e, + 0xb2, 0x96, 0xaf, 0x3f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0xbf, 0x61, 0x93, 0x4e, 0x87, + 0xc2, 0x65, 0xe8, 0xbf, 0xe6, 0x95, 0xa9, 0x51, + 0x61, 0x8f, 0xe5, 0x3f, 0x1b, 0x0d, 0x22, 0x4a, + 0x62, 0x66, 0xd2, 0xbf, 0x07, 0xba, 0x93, 0xdb, + 0x38, 0xda, 0xea, 0x3f, 0x0b, 0x36, 0xe0, 0xb9, + 0x18, 0x3f, 0xe3, 0xbf, 0x93, 0x17, 0x3e, 0xc7, + 0xec, 0xd1, 0xee, 0xbf, 0x57, 0xc0, 0x6f, 0x09, + 0xe1, 0x2d, 0xe0, 0xbf, 0x8a, 0x05, 0x3a, 0x6a, + 0xb7, 0xa3, 0xc3, 0xbf, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0x3f, 0xb4, 0x31, 0xe1, 0x5d, + 0x4f, 0xf0, 0xca, 0x3f, 0xa0, 0x9a, 0x9a, 0x1d, + 0x0c, 0xe9, 0xba, 0x3f, 0x55, 0xf2, 0x77, 0x10, + 0xb7, 0x1a, 0xc5, 0x3f, 0x08, 0x3f, 0xcc, 0xb9, + 0x6f, 0x90, 0xd3, 0x3f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xbe, 0x36, 0xd9, 0x0e, + 0x5f, 0x89, 0xb4, 0xbf, 0xe2, 0x22, 0x18, 0x83, + 0x5e, 0xea, 0xe5, 0x3f, 0x0f, 0x8c, 0x72, 0x1f, + 0x6f, 0xa8, 0xb8, 0x3f, + } + + w, err := CreateEmpty(96000, AUDIO_IEEE_FLOAT, 64, 1) + + /* + * Check if wave file was successfully created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to create wave file: %s", msg) + } else { + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + c.WriteFloats(samples) + buf, err := w.Bytes() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain byte buffer failed.") + } else { + + /* + * Make sure that buffer is non-nil. + */ + if buf == nil { + t.Errorf("%s", "Byte buffer is nil.") + } else { + equal := areSlicesEqual(buf, expectedOutput) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + expectedOutputString := bufferToHex(expectedOutput) + actualOutputString := bufferToHex(buf) + t.Errorf("Byte buffers are not equal. Expected: %s Got: %s", expectedOutputString, actualOutputString) + } + + } + + } + + } + + } + + } + +} + +/* + * Test reading an 64-bit IEEE floating-point wave file. + */ +func TestImportIEEE64Mono(t *testing.T) { + + /* + * Input buffer. + */ + buf := []byte{ + 0x52, 0x49, 0x46, 0x46, 0xc4, 0x00, 0x00, 0x00, + 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, + 0x00, 0x77, 0x01, 0x00, 0x00, 0xb8, 0x0b, 0x00, + 0x08, 0x00, 0x40, 0x00, 0x64, 0x61, 0x74, 0x61, + 0xa0, 0x00, 0x00, 0x00, 0xd5, 0x84, 0xc4, 0xa8, + 0x31, 0x02, 0xd5, 0xbf, 0x51, 0x7d, 0x8c, 0x5e, + 0xb2, 0x96, 0xaf, 0x3f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0xbf, 0x61, 0x93, 0x4e, 0x87, + 0xc2, 0x65, 0xe8, 0xbf, 0xe6, 0x95, 0xa9, 0x51, + 0x61, 0x8f, 0xe5, 0x3f, 0x1b, 0x0d, 0x22, 0x4a, + 0x62, 0x66, 0xd2, 0xbf, 0x07, 0xba, 0x93, 0xdb, + 0x38, 0xda, 0xea, 0x3f, 0x0b, 0x36, 0xe0, 0xb9, + 0x18, 0x3f, 0xe3, 0xbf, 0x93, 0x17, 0x3e, 0xc7, + 0xec, 0xd1, 0xee, 0xbf, 0x57, 0xc0, 0x6f, 0x09, + 0xe1, 0x2d, 0xe0, 0xbf, 0x8a, 0x05, 0x3a, 0x6a, + 0xb7, 0xa3, 0xc3, 0xbf, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0x3f, 0xb4, 0x31, 0xe1, 0x5d, + 0x4f, 0xf0, 0xca, 0x3f, 0xa0, 0x9a, 0x9a, 0x1d, + 0x0c, 0xe9, 0xba, 0x3f, 0x55, 0xf2, 0x77, 0x10, + 0xb7, 0x1a, 0xc5, 0x3f, 0x08, 0x3f, 0xcc, 0xb9, + 0x6f, 0x90, 0xd3, 0x3f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xbe, 0x36, 0xd9, 0x0e, + 0x5f, 0x89, 0xb4, 0xbf, 0xe2, 0x22, 0x18, 0x83, + 0x5e, 0xea, 0xe5, 0x3f, 0x0f, 0x8c, 0x72, 0x1f, + 0x6f, 0xa8, 0xb8, 0x3f, + } + + /* + * Expected sample data. + */ + expectedSamples := []float64{ + -0.32825891, 0.0616966, -1.0, -0.76242186, + 0.67375246, -0.28749902, 0.83913844, -0.60145222, + -0.9631256, -0.50560047, -0.15343373, 1.0, + 0.21045868, 0.10511852, 0.16487778, 0.3056907, + 0.0, -0.08022112, 0.68485952, 0.0963201, + } + + w, err := FromBuffer(buf) + + /* + * Check if wave file was read created. + */ + if err != nil { + msg := err.Error() + t.Errorf("Failed to read wave file: %s", msg) + } else { + sampleRate := w.SampleRate() + + /* + * Check if sample rate was determined successfully. + */ + if sampleRate != 96000 { + t.Errorf("Attempt to determine sample rate failed. Expected %d, got %d.", 96000, sampleRate) + } + + numChannels := w.ChannelCount() + + /* + * Check if sample rate was determined successfully. + */ + if numChannels != 1 { + t.Errorf("Attempt to determine channel count failed. Expected %d, got %d.", 1, numChannels) + } + + sampleFormat := w.SampleFormat() + + /* + * Check if sample format was determined successfully. + */ + if sampleFormat != AUDIO_IEEE_FLOAT { + t.Errorf("Attempt to determine sample format failed. Expected %d, got %d.", AUDIO_IEEE_FLOAT, sampleFormat) + } + + bitDepth := w.BitDepth() + + /* + * Check if bit depth was determined successfully. + */ + if bitDepth != 64 { + t.Errorf("Attempt to determine bit depth failed. Expected %d, got %d.", 64, bitDepth) + } + + c, err := w.Channel(1) + + /* + * Attempt to obtain non-existing channel must return nil reference. + */ + if c != nil { + t.Errorf("Attempt to obtain non-existant channel did not return nil.") + } + + /* + * Attempt to obtain non-existing channel must return error. + */ + if err == nil { + t.Errorf("%s", "Attempt to obtain non-existant channel did not return error.") + } + + c, err = w.Channel(0) + + /* + * Attempt to obtain existing channel must not return error. + */ + if err != nil { + t.Errorf("%s", "Attempt to obtain existing channel returned error.") + } else { + + /* + * Attempt to obtain existing channel must not return nil reference. + */ + if c == nil { + t.Errorf("%s", "Attempt to obtain existing channel returned nil.") + } else { + samples := c.Floats() + + /* + * Check if attempt to obtain byte buffer was successful. + */ + if samples == nil { + t.Errorf("%s", "Sample buffer is nil.") + } else { + equal, diff := areSlicesClose(samples, expectedSamples, 1.0e-16) + + /* + * If buffers are not equal, report failure. + */ + if !equal { + t.Errorf("Sample buffers are not similar. Expected: %v Got: %v Difference: %v", expectedSamples, samples, diff) + } + + } + + } + + } + + } + +} diff --git a/webroot/css/style.css b/webroot/css/style.css new file mode 100644 index 0000000..176d4e2 --- /dev/null +++ b/webroot/css/style.css @@ -0,0 +1,357 @@ +.active:hover +{ + color: #ffcc44; +} + +.active +{ + color: #ff8800; + cursor: pointer; +} + +.active.io:hover +{ + color: #ccccff; +} + +.active.io +{ + color: #8888ff; + cursor: pointer; +} + +.body +{ + background-color: #000000; + color: #ffffff; + font-family: sans-serif; + font-size: 14px; + font-style: normal; + font-variant: normal; + font-weight: normal; + margin: 0px; + padding: 15px; + text-align: left; + vertical-align: top; + white-space: normal; +} + +.blockercontent +{ + background-color: #333333; + border-color: #777777; + border-radius: 15px; + border-style: solid; + border-width: 2px; + color: #ffffff; + left: 50%; + padding: 15px; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); +} + +.blockerdiv +{ + bottom: 0px; + cursor: wait; + display: none; + left: 0px; + position: fixed; + right: 0px; + text-align: center; + top: 0px; + vertical-align: middle; +} + +.button +{ + -moz-appearance: none; + -webkit-appearance: none; + border-radius: 5px; + border-style: solid; + border-width: 1px; + cursor: pointer; + left: 0px; + margin-bottom: 0px; + margin-left: 0px; + margin-right: 10px; + margin-top: 0px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + padding-top: 5px; + position: relative; + right: 0px; + text-align: left; +} + +.buttonactive +{ + background-color: #99dd99; + border-color: #337733; + color: #337733; +} + +.buttonactive:hover +{ + background-color: #aaeeaa; + border-color: #559955; + color: #559955; +} + +.buttonnormal +{ + background-color: #333333; + border-color: #777777; + color: #ffffff; +} + +.buttonnormal:hover +{ + background-color: #555555; + border-color: #999999; +} + +.buttonremove +{ + background-color: #773333; + border-color: #bb7777; + color: #ffffff; +} + +.buttonremove:hover +{ + background-color: #995555; + border-color: #dd9999; +} + +.captiondiv +{ + background-color: #222222; + border-color: #444444; + border-radius: 10px; + border-style: solid; + border-width: 2px; + display: inline-block; + color: #ffffff; + left: 0px; + margin: 10px; + padding: 5px; + position: relative; + right: 0px; +} + +.centered +{ + text-align: center; +} + +.contentdiv +{ + background-color: #222222; + border-color: #444444; + border-radius: 10px; + border-style: solid; + border-width: 2px; + color: #ffffff; + left: 0px; + margin: 10px; + padding-bottom: 13px; + padding-left: 13px; + padding-right: 13px; + padding-top: 13px; + position: relative; + right: 0px; + text-align: left; +} + +.contentdiv.addunitdiv +{ + background-color: #111111; + color: #ffffff; + border-color: #333333; +} + +.contentdiv.iodiv +{ + background-color: #444444; + color: #ffffff; + border-color: #666666; +} + +.contentdiv.masterdiv +{ + background-color: #444466; + color: #ffffff; + border-color: #666688; +} + +.contentdiv.masterunitdiv +{ + background-color: #222244; + color: #ffffff; + border-color: #444466; +} + +.controlsdiv +{ + display: none; + margin-top: 5px; +} + +.documentationdiv +{ + margin: 5px; +} + +.dropdown +{ + -moz-appearance: none; + -webkit-appearance: none; + background-color: #000000; + border-color: #666666; + border-radius: 5px; + border-style: solid; + border-width: 1px; + color: #ffffff; + cursor: pointer; + margin-bottom: 0px; + margin-left: 0px; + margin-right: 10px; + margin-top: 0px; + position: relative; + height: 30px; +} + +.dropdownlabel +{ + display: inline-block; + margin-right: 10px; +} + +.headerdiv +{ +} + +.knobdiv +{ + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + padding-top: 5px; +} + +.knoblabel +{ + text-align: center; + width: 160px; +} + +.keydiv +{ + word-wrap: break-word; +} + +.labeldiv +{ + display: inline-block; +} + +.link +{ + color: #4444ff; + cursor: pointer; + text-decoration: underline; +} + +.link:hover +{ + color: #8888ff; +} + +.paramdiv +{ + display: inline-block; +} + +.pointer +{ + cursor: pointer; +} + +.programnamediv +{ + color: #ffffff; + font-size: 30px; + text-align: center; +} + +.spacerdiv +{ + height: 15px; +} + +.subcaptiondiv +{ + color: #ffffff; + font-size: 12px; + text-align: center; +} + +.textarea +{ + -moz-appearance: none; + -webkit-appearance: none; + background-color: #000000; + border-color: #666666; + border-radius: 5px; + border-style: solid; + border-width: 1px; + color: #ffffff; + font-family: monospace; + margin: 0px; + position: relative; + height: 100px; + width: 1000px; +} + +.textfield +{ + -moz-appearance: none; + -webkit-appearance: none; + background-color: #000000; + border-color: #666666; + border-radius: 5px; + border-style: solid; + border-width: 1px; + color: #ffffff; + font-family: monospace; + margin: 0px; + position: relative; + height: 30px; + width: 80px; +} + +.tunercentsknob +{ +} + +.tunerfrequencydiv +{ + display: inline-block; + padding-left: 5px; +} + +.tunernotediv +{ + display: inline-block; + font-weight: bold; + padding-left: 5px; +} + +.wide +{ + width: 160px; +} + diff --git a/webroot/doc/css/style.css b/webroot/doc/css/style.css new file mode 100644 index 0000000..1844000 --- /dev/null +++ b/webroot/doc/css/style.css @@ -0,0 +1,68 @@ +.body +{ + background-color: #000000; + color: #aaaaaa; + font-family: sans-serif; + font-size: 16px; + font-style: normal; + font-variant: normal; + font-weight: normal; + margin: 0px; + padding: 15px; + text-align: left; + vertical-align: top; + white-space: normal; +} + +.code +{ + background-color: #222222; + color: #ffffff; + font-family: monospace; +} + +.emphasis +{ + font-style: italic; +} + +.heading +{ + color: #ffffff; + font-size: 20px; + font-weight: bold; +} + +.item +{ + padding-bottom: 5px; +} + +.highlight +{ + color: #ffffff; +} + +.key +{ + color: #ffffff; + font-weight: bold; +} + +.section +{ + margin-bottom: 15px; +} + +.title +{ + color: #ffffff; + font-size: 32px; + font-weight: bold; + text-align: center; +} + +.value +{ +} + diff --git a/webroot/doc/index.xhtml b/webroot/doc/index.xhtml new file mode 100644 index 0000000..cf76648 --- /dev/null +++ b/webroot/doc/index.xhtml @@ -0,0 +1,741 @@ + + + +
+