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 @@ + + + + + + go-dsp-guitar: Documentation + + + + +
+ go-dsp-guitar: Guitar amplification for the 21st century +
+
+ Introduction +
+
+ go-dsp-guitar is a virtual effects rack, mainly for guitarists. It allows you to simulate the behaviour of a guitar amplifier and associated effects units (stompboxes, rack effects, ...) on your computer. Although the software focuses on guitar effects, it can also process other instruments and even voice. +
+
+ go-dsp-guitar can operate in two different modes, real-time mode and batch processing mode. +
+
+ When operating in real-time mode, it connects to a sound server called JACK to capture audio signals from either your computer's sound card or audio interface or from other JACK-aware applications. go-dsp-guitar will then process these signals using a number of user-configurable chains of effects, one for each input channel, simulate the behaviour of each effects circuit and generate the corresponding output. The output will be fed back to JACK and either output through your computer's sound card or audio interface or forwarded to another JACK-aware application. All signal processing happens in real-time (i. e. as you play your instrument), so that this mode is suitable for practice or live performances, but also for direct recording of the processed (or "wet") signal in a digital-audio workstation (DAW). The downside of this mode of operation is that it limits the algorithmic complexity of the processing you can perform. If the calculations do not finish in real-time, you will get clicks and pops or even distortion in the output. +
+
+ On the other hand, batch processing mode allows you to load pre-existing, unprocessed (or "dry") guitar (or other instrument) tracks from a set of files, process them and store them in another set of files. Since the processing does not need to happen in real-time, it can basically be as algorithmically complex as you want. This allows you to build long and complex effects chains and faithfully simulate each effects unit with very high precision. In addition, you can use this mode to process audio files without JACK running. You can even process signals on systems which do not have any audio hardware installed. +
+
+ Setting up JACK for real-time processing +
+
+ In order to perform real-time audio processing on your system, you first need to install and configure the JACK audio server on your system. How this is done depends on your operating system or distribution. We suggest that you also install Qjackctl, which is a graphical application to configure and control JACK, and configure the audio server using this utility. Normally, you will have a separate audio device, such as an external audio interface, that you use for real-time processing, while you keep the normal system sound running on your internal on-board audio device. +
+
+ In Qjackctl, open the settings window, enable real-time mode and set the sampling rate. You will usually set this to the native sampling rate of your interface, which is usually the highest rate your audio hardware supports and often printed on your device. We suggest you to select either 48 kHz, 96 kHz or 192 kHz as the sampling rate. These sampling rates are much more common today, than sampling rates like 44.1 kHz or 88.2 kHz, which stem from the 90s, when CD audio was the norm. Most modern converters will usually operate "better" when running at the former rates than the latter. If you choose a higher sampling rate, the algorithms in go-dsp-guitar will run with a narrower time-domain discretization, which will (slightly) improve simulation accuracy. However, keep in mind that this will also result in your computer having to perform more intensive processing. If in doubt, we recommend that you choose a sampling rate of 96 kHz, if your audio hardware supports it. +
+
+ Next, set the frames per period value. This determines how many samples your converter will collect, before it calls into your applications (like go-dsp-guitar) to process these. There is a certain tradeoff to be done here. Smaller values reduce latency, the time it takes, for example, from when you hit a string on your guitar until you hear a sound from your speakers or headphones, but allow your system less time to perform the actual processing, so you will run into dropouts more frequently and won't be able to perform as much complex processing than if your latency was higher. We suggest that you start at a value of 1024, which gives you about 20 milliseconds of round-trip latency when sampling at 96 kHz, and see how it goes. If your DSP load indicator is constantly below about 40 percent, you may reduce this value to 512, 256, 128 or even 64 to achieve even lower (e. g. 10 ms, 5 ms, 2.5 ms or even 1.25 ms round-trip) latency. On the other hand, if you experience dropouts, you might have to increase this value to 2048, 4096 or even 8192, especially if your converter runs on a very high sampling rate (e. g. 192 kHz). The setting you select here is not all that important though, since you can change the frames per period settings from within go-dsp-guitar. Keep the periods per buffer value at 2. +
+
+ Next, go to the advanced tab, set the maximum port value to 128 and select the audio interface, which you want to use for signal processing, under the input device and output device option. We suggest that you do not select the device, over which your system sound runs. Instead, use a separate, dedicated audio device for real-time processing. For example, use your on-board sound for the system sound and your USB audio interface to process your guitar signal. +
+
+ Suggested settings: +
+
+ +
+
+ Now, hit Ok and start the audio server. +
+
+ Running go-dsp-guitar in real-time mode +
+
+ To run go-dsp-guitar in real-time mode, you first need to have JACK running. Afterwards, navigate to the place where you installed the software and run the dsp executable. Substitute the path in the first line with the directory where you built the software or extracted the binary package. +
+
+
$ cd ~/go/src/github.com/andrepxx/go-dsp-guitar/
+
$ ./dsp
+
+
+ The command should not return to the prompt of the shell. Should you get any error message and the program terminate (return to the shell prompt), there is likely either a problem with JACK or the TCP ports, which go-dsp-guitar uses (ports 8080 and 8443), are already in use by another process on your machine. You may change these port numbers by editing the file in config/config.json, relative to the path of your go-dsp-guitar installation, though we advise against this, unless you know what you are doing. +
+
+ Once go-dsp-guitar is running, you can connect to its web interface by opening one of the following URLs in your web browser. +
+
+ +
+
+ In real-time mode, go-dsp-guitar will register the following ports with JACK. +
+
+ Input ports: +
+
+ +
+
+ Output ports: +
+
+ +
+
+ In a typical live performance situation, where you play guitar and do vocal performance, you might connect your guitar to in_0 and your microphone to in_1. Say you wish to distort your guitar signal and reverbarate your vocal signal so you configure the effects chains this way. (More on that later.) The output out_0 will then carry your processed (e. g. distorted) guitar signal, while the output out_1 will carry your processed (e. g. reverberated) vocal signal. These "direct output" signals may be used for recording or for forwarding them to a dedicated mixing desk at the venue. The outputs master_left and master_right will carry the stereo mixdown, i. e. the sum of all outputs, with their respective level adjustments and room simulation applied, as configured in the Spatializer unit. (More on that in the section on effects units.) You will usually use the master outputs for monitoring. However, you may also use them in case you would like to forward a "pre-mixed" signal to the mixing desk. Finally, the metronome output is an auxiliary output, which carries the metronome signal only. +
+
+ Running go-dsp-guitar in batch-processing mode +
+
+ To run go-dsp-guitar in batch-processing mode, you navigate to the place where you installed the software and run the dsp executable with the parameter -channels, followed by the number of input channels (i. e. instruments) you wish to process. Substitute the path in the first line with the directory where you built the software or extracted the binary package. +
+
+
$ cd ~/go/src/github.com/andrepxx/go-dsp-guitar/
+
$ ./dsp -channels [n]
+
+
+ go-dsp-guitar will then ask you to enter the target sample rate (in Hertz) of your project. This determines the time discretization, in which all processing will be performed and also the sample rate of the output files generated. All files supplied as input will be resampled to this sample rate before processing using the Lanczos resampling method. +
+
+ The command should not return to the prompt of the shell. Should you get any error message and the program terminate (return to the shell prompt), the TCP ports, which go-dsp-guitar uses (ports 8080 and 8443), are probably already in use by another process on your machine. You may change these port numbers by editing the file in config/config.json, relative to the path of your go-dsp-guitar installation, though we advise against this, unless you know what you are doing. +
+
+ Once go-dsp-guitar is running, you can connect to its web interface by opening one of the following URLs in your web browser. +
+
+ +
+
+ After you set up your signal flow, you can then hit the Process now button in the Batch processing section of the web interface. After this, the web interface will lock up and the command line program will prompt you for the target format (linear PCM 'lpcm' or floating-point 'float') and bit depth (8, 16, 24 or 32 for 'lpcm', 32 or 64 for 'float'), followed by the file path to each file used as an input, as well as the channel from that file to be used. +
+
+ After each channel has been loaded from file, it will be resampled to the appropriate target sampling rate, then the audio data will be processed and finally, each processed (output) channel will be written to a (monophonic) wave file of your choosing. If the audio data loaded into the input channels is of different lengths, the shorter sequences will be extended by zero-padding them to the length of the longest sequence. Also, the two master-outputs (left and right), as well as the metronome channel can be written to files. Leaving a file path empty discards that particular output. After all results have been either written to file or discarded, no further prompt will appear. The web interface can then be reloaded, the signal chain can be reconfigured and another batch processing step can be started by clicking on the Process now button. If you do not wish to perform further processing, hit Ctrl + C on the command line where go-dsp-guitar is running. +
+
+ The web interface +
+
+ To configure your signal chains, open the web interface in your web browser. The web interface consists of a number of channels, as well as a global master section. Each of these can contain a number of effects units, which process or alter the signal flowing through them. When running in real-time mode, there are normally two channels. When running in batch-processing mode, the desired number of channels is passed as an argument to the dsp executable. +
+
+ Signal flow +
+
+ The signal from each of the (monophonic) inputs (in_0, in_1, ...) is fed through a series of effects units. The ordered arrangement of these effects units is what we call the signal chain. The signal flows through each of the individual signal chains from top to bottom, while all of the chains are run in parallel. Each of the signal chain produces an individual (monophonic) output (out_0, out_1, ...). The order of the individual effects units within each signal chain is important. For example, a reverberation unit will usually be placed after a distortion unit, since it is much more common to reverberate a distorted signal, than to distort a reverberated signal. We will discuss the most common ordering of the effects in the section where we describe each effects unit and this is usually a nice way to get started. However, sometimes, it is necessary to go against common rules to get the desired sound. Therefore, there is no incorrect ordering of effects in the strict sense. Because of this, go-dsp-guitar gives you full control and freedom over your signal routing and effects order. Just keep in mind that unexpected things might happen, if you choose to leave well-trodden paths. +
+
+ Signal flow deviates from the usual rules regarding the master section located at the bottom of the user interface. The Tuner is always fed the direct signal from the selected input and does not alter the signal itself in any way. The output signals from all individual effects chains are routed out to their respective outputs, and also fed into the Spatializer, which mixes them down to a stereo signal with a simple phase-shift based room simulation and feeds them to the master section. Finally, the Metronome has its own, individual output, but may optionally be mixed into the master output by enabling the Master button. This makes sense when the master output is used for monitoring purposes by the musician, who will want to hear the metronome ticking. +
+
+ Description of all effects units +
+
+ In the following, we will describe each of the effects units. We will describe what these units do, how they are used, and possibly how they achieve what they are doing. We will also describe each of their parameters and possibly how you should configure them in order to achieve certain effects. The order in which we describe the effects units represents the order in which they appear in the drop-down menu of go-dsp-guitar, which is also the usual order, in which they are arranged. The order in which we describe the paramters is the order in which they appear in the user interface. Note that normally, you will only have a subset of these effects units in your signal chain. Also note that you are allowed to have multiple instances of the same effects unit in your signal chain if you desire so. And of course, you can arrange them in any order you like, though certainly not all configurations will result in usable sound. +
+
+ Effects unit: Signal generator +
+
+ The signal generator is a function generator, which allows you to mix your original input with the synthesized signal. It is mainly used for testing purposes, since it can provide a highly predictable, synthetic input into the following effects units, but may also be used creatively, for example as an LFO (low-frequency oscillator). Since it is a signal source, it is normally used at the beginning of the signal chain. However, as with all effects unit, it may be placed anywhere in the chain. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Noise gate +
+
+ A noise gate cuts the input signal when its amplitude falls below a certain threshold for a certain amount of time and starts passing it through again once its amplitude rises above another threshold. It is useful to cut the signal from the guitar when the musician is not playing. Otherwise, hum or static noise from the pickups may be heard, especially when the signal is later amplified by distortion units or preamplifiers with high gain. To properly cut the signal, the noise gate is normally placed first in the signal chain, before all other effects units. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Bandpass +
+
+ A bandpass is a certain type of filter, which attenuates signal components with frequencies above and below certain thresholds. It may be used early in the signal path as a sort of noise reduction by cutting the signal energy in frequency bands which are known not to be important for a certain instrument. For this purpose, it is best to be directly placed after the noise gate (if present). However, it may also be used later in the signal path for effects, for example to achieve a kind of "mid-rangy", nasal, "tinny" or otherwise lo-fi sound when placed after the pre-amplifier or distortion units. The bandpass filter in go-dsp-guitar is implemented as an IIR (infinite impulse response) filter, which means that, after the filter is excited by a sharp transient, it takes the filter an infinite amount of time to "settle" to an output of zero again. This means that the filter, once excited, reaches steady state only asymptotically. The filter topologies are based on actual analog active filter designs. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Auto wah +
+
+ The auto wah is a voltage-controlled filter effect. It implements a 16th-order (-48 dB per octave) bandpass filter whose resonance frequency is controlled by either the average or peak amplitude of the input signal. Wah effects are used a lot by guitarists, especially within old-school rock and funk music. Because it is an amplitude-controlled effect, the auto wah unit should be placed before distortion effects in the signal chain, as these compress or reduce the dynamic range of the signal. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Octaver +
+
+ An octaver creates several notes at octaves above or below the original note played on the guitar. It therefore allows your guitar to achieve a fuller sound or gives the impression of other instruments, especially bass guitars, playing along your guitar track even when there are actually no other instruments present. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Excess +
+
+ Excess is an effect which creates non-linear distortion by phase-modulating the input signal with itself. When operated on low intensity, it can achieve an effect similar to distortion. When operated on high intensity, the signal will almost turn into white noise, especially at high amplitudes. This effect can be very useful when creating non-conventional, more progressive sounds. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Fuzz +
+
+ Fuzz is a non-linear distortion effect with asymmetric clipping, which produces both even- and odd-order harmonics. Traditional fuzz-boxes are based upon simple Silicon- or Germanium-based transistor circuits and typically employ hard clipping, while tube circuits typically employ softer clipping. The effects unit in go-fx-guitar allows you to tune the fuzz unit for the "hardness" or "softness" of the clipping. It enables a guitarist to achieve anything from smooth and "blusey" to rough-sounding distortion tones typically associated with old-school rock music or blues. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Overdrive +
+
+ Overdrive is a non-linear distortion effect with symmetric soft-clipping. It enables a guitarist to achieve the smooth saturation tones typically associated with overdriven valve amplifiers and genres from blues over rock to hard rock. Due to the symmetric clipping, the effects unit produces odd-order harmonics only. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Distortion +
+
+ Distortion is a non-linear distortion effect with symmetric hard-clipping. It enables a guitarist to achieve more modern saturation tones typically associated with contemporary hard rock and heavy metal music. Due to the symmetric clipping, the effects unit produces odd-order harmonics only. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Tone stack +
+
+ A tone stack is a simple tone control or equalizer circuit as it is commonly found in the pre-amp section of guitar amplifiers. It is usually placed directly after the clipping stage, so in go-dsp-guitar, it would come directly after a Fuzz, Overdrive or Distortion unit. It consists of a configurable 8th-order filter network with four tunable values. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Chorus +
+
+ The chorus unit creates the impression that the melody is played by a multitude of instruments at once. To achieve this, it replicates the original signal from the instrument, slightly modulates the pitch of each of the signals and finally mixes them all together. As a modulation effect, this unit is normally placed after both pre-amplifier and tone stack. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Flanger +
+
+ A flanger is a comb filter controlled by a low-frequency oscillator (LFO). A comb filter creates a series of peaks and troughs in the frequency response. Comb filters operate by duplicating the signal, sending one of the copies through a series of all-pass filter networks, which alter only the phase, but not the amplitude of each (sinusoid) signal component, and finally averaging over both the unaltered and the phase-shifted signal by summing both signals up and cutting the resulting amplitude in half. The LFO signal controls the amount of (frequency-dependant) phase-shift experienced by one of the signals and therefore the placement and spacing of these peaks and troughs within the frequency spectrum, which get slowly shifted around. As a modulation effect, this unit is normally placed after both pre-amplifier and tone stack. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Phaser +
+
+ A phaser is an effects unit very similar to a flanger. It also implements a comb filter controlled by a low-frequency oscillator (LFO). The difference is that the maximum amount of phase shift the signal experiences can also be controlled. When the signals are (+)90 degrees out of phase, it achieves the same results as a flanger effect. However, the phaser also allows for lower, or even negative phase shifts, resulting in a slightly different effect. As a modulation effect, this unit is normally placed after both pre-amplifier and tone stack. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Tremolo +
+
+ Tremolo is an effect, which amplitude-modulates the input signal with another signal from a low-frequency oscillator (LFO). It can be used to create staccato-like effects. As a modulation effect, this unit is normally placed after both pre-amplifier and tone stack. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Ring modulator +
+
+ A ring modulator multiplies the input signal with a sinusoidal carrier wave, therefore creating additional frequency components (sum and difference frequencies), which are not harmonically related to the input signal. Like the Excess unit, this effect can be very useful when creating non-conventional, more progressive sounds. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Delay +
+
+ A delay unit repeats the input signal after a short amount of time, normally with reduced amplitude. It is used for creating simple echos for increased ambience or to achieve layered, complex sounds with a single instrument. Long delay times basically allow a guitarist to overdub his/her own playing. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Power amp +
+
+ The power amp unit is one of the most complex effects available in go-dsp-guitar. It is capable of simulating different (tube-/valve-based) power-amplifier topologies, as well as speaker cabinets connected to these and picked up by microphones placed in different positions, relative to the speaker cone. It is actually a hybrid of a power amplifier and speaker simulator. It recreates both the frequency and phase response of the simulated devices by approximating them with high-order filters, which are then simulated in real-time. +
+
+ Parameters: +
+
+ +
+
+ The following power amplifier and/or speaker simulations are currently implemented in go-dsp-guitar. +
+
+ +
+
+ Most of the time, you will probably run multiple variations of a particular speaker cabinet (i. e. different microphone placements for a guitar cabinet or all drivers of a PA cabinet) in parallel and adjust their respective levels to tune your sound. However, go-dsp-guitar will also let you combine speakers from different cabinets and/or amplifiers and run them in parallel. Specifically, by running the signal through multiple guitar amplifiers at once, you can achieve an effect similar to "dual-amping" a guitar. +
+
+ Effects unit: Latency +
+
+ This is a "pseudo-unit", which is is only present when go-dsp-guitar runs in real-time mode (see the respective section above). It allows the user to change the JACK buffer size (frames per period) on-the-fly from within the application. Increase the buffer size when DSP load is too high for reliable operation in order to prevent audio dropouts and distortion. Decrease buffer size when DSP load is very low (below 50 %) to reduce processing latency. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Tuner +
+
+ This is a chromatic instrument tuner, which allows you to tune your instrument (e. g. guitar) to a variety of scales. To operate the tuner, select the input channel, to which the instrument you want to tune is connected and hit a note, e. g. by strumming a single open string on your guitar. Then turn the tuning pegs on your instrument, until the correct note appears and the deviation in cents is close to zero. Finally, set the channel value back to - NONE - to deactivate the tuner, as this frees additional processing ressources. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Spatializer +
+
+ The spatializer mixes the output signal from the individual effects chains (i. e. the processed instrument and/or microphone signals) and combines them into a stereo mix with room simulation. The mixed signal is then fed to the master outputs, of which there are two, one for the left stereo channel and one for the right stereo channel. The unit contains the following controls for each of your input channels. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Metronome +
+
+ The metronome allows you to play a click track for timing and synchronisation. The metronome has its own output and may be routed separately via JACK. Depending on whether the Master button is enabled (highlighted) or disabled, the metronome's output will optionally be added (panned dead-center and with unity level) to the master output. +
+
+ Parameters: +
+
+ +
+
+ Effects unit: Signal levels +
+
+ This is a unit, which features level meters, which try to adopt the parameters specified in DIN IEC 60268-18. The meters display the peak signal levels of each input and output channel. They are implemented as sampled peak programme meters (SPPMs). An additional meter shown at the top displays the system's current DSP load. Level meters and the DSP load indicator are only available when go-dsp-guitar runs in real-time mode. +
+
+ Effects unit: Batch processing +
+
+ This is a "pseudo-unit", which is only present when go-dsp-guitar runs in batch-processing mode (see the respective section above). Click on the Process now button to apply the current setup to a set of audio files. Refresh the page after you processed the first set of files to modify the configuration. +
+ + + diff --git a/webroot/index.xhtml b/webroot/index.xhtml new file mode 100644 index 0000000..f233e5a --- /dev/null +++ b/webroot/index.xhtml @@ -0,0 +1,49 @@ + + + + + + go-dsp-guitar: Web interface + + + + + + +
+
+
+ go-dsp-guitar +
+
+ Guitar amplification for the 21st century +
+
+
+ + View documentation + +
+
+
+
+
+
+
+
+
+
+
+
+
+ To: Master out +
+
+
+
+ Synchronizing ... +
+
+ + + diff --git a/webroot/js/dsp.js b/webroot/js/dsp.js new file mode 100644 index 0000000..fac078a --- /dev/null +++ b/webroot/js/dsp.js @@ -0,0 +1,3109 @@ +"use strict"; + +/* + * A class for storing global state required by the application. + */ +function Globals() { + this.cgi = '/cgi-bin/dsp'; + this.unitTypes = new Array(); +} + +/* + * The global state object. + */ +var globals = new Globals(); + +/* + * A class implementing data storage. + */ +function Storage() { + var g_map = new WeakMap(); + + /* + * Store a value under a key inside an element. + */ + this.put = function(elem, key, value) { + var map = g_map.get(elem); + + /* + * Check if element is still unknown. + */ + if (map == null) { + map = new Map(); + g_map.set(elem, map); + } + + map.set(key, value); + }; + + /* + * Fetch a value from a key inside an element. + */ + this.get = function(elem, key, value) { + var map = g_map.get(elem); + + /* + * Check if element is unknown. + */ + if (map == null) { + return null; + } else { + var value = map.get(key); + return value; + } + + }; + + /* + * Check if a certain key exists inside an element. + */ + this.has = function(elem, key) { + var map = g_map.get(elem); + + /* + * Check if element is unknown. + */ + if (map == null) { + return false; + } else { + var value = map.has(key); + return value; + } + + }; + + /* + * Remove a certain key from an element. + */ + this.remove = function(elem, key) { + var map = g_map.get(elem); + + /* + * Check if element is known. + */ + if (map != null) { + map.delete(key); + + /* + * If inner map is now empty, remove it from outer map. + */ + if (map.size == 0) { + g_map.delete(elem); + } + + } + + }; + +} + +var storage = new Storage(); + +/* + * A class supposed to make life a little easier. + */ +function Helper() { + + /* + * Blocks or unblocks the site for user interactions. + */ + this.blockSite = function(blocked) { + var blocker = document.getElementById('blocker'); + var displayStyle = ''; + + /* + * If we should block the site, display blocker, otherwise hide it. + */ + if (blocked) + displayStyle = 'block'; + else + displayStyle = 'none'; + + /* + * Apply style if the site has a blocker. + */ + if (blocker != null) + blocker.style.display = displayStyle; + + }; + + /* + * Removes all child nodes from an element. + */ + this.clearElement = function(elem) { + + /* + * As long as the element has child nodes, remove one. + */ + while (elem.hasChildNodes()) { + var child = elem.firstChild; + elem.removeChild(child); + } + + }; + + /* + * Parse JSON string into an object without raising exceptions. + */ + this.parseJSON = function(jsonString) { + + /* + * Try to parse JSON structure. + */ + try { + var obj = JSON.parse(jsonString); + return obj; + } catch (ex) { + return null; + } + + }; + +} + +/* + * The (global) helper object. + */ +var helper = new Helper(); + +/* + * A class implementing an Ajax engine. + */ +function Ajax() { + + /* + * Sends an Ajax request to the server. + * + * Parameters: + * - method (string): The request method (e. g. 'GET', 'POST', ...). + * - url (string): The request URL. + * - data (string): Data to be passed along the request (e. g. form data). + * - callback (function): The function to be called when a response is + * returned from the server. + * - block (boolean): Whether the site should be blocked. + * + * Returns: Nothing. + */ + this.request = function(method, url, data, callback, block) { + var xhr = new XMLHttpRequest(); + + /* + * Event handler for ReadyStateChange event. + */ + xhr.onreadystatechange = function() { + helper.blockSite(block); + + /* + * If we got a response, pass the response text to + * the callback function. + */ + if (this.readyState == 4) { + + /* + * If we blocked the site on the request, + * unblock it on the response. + */ + if (block) + helper.blockSite(false); + + /* + * Check if callback is registered. + */ + if (callback != null) { + var content = xhr.responseText; + callback(content); + } + + } + + }; + + xhr.open(method, url, true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send(data); + }; + +} + +/* + * The (global) Ajax engine. + */ +var ajax = new Ajax(); + +/* + * A class implementing a key-value-pair. + */ +function KeyValuePair(key, value) { + var g_key = key; + var g_value = value; + + /* + * Returns the key stored in this key-value pair. + */ + this.getKey = function() { + return g_key; + }; + + /* + * Returns the value stored in this key-value pair. + */ + this.getValue = function() { + return g_value; + }; + +} + +/* + * A class implementing a JSON request. + */ +function Request() { + var g_keyValues = Array(); + + /* + * Append a key-value-pair to a request. + */ + this.append = function(key, value) { + var kv = new KeyValuePair(key, value); + g_keyValues.push(kv); + } + + /* + * Returns the URL encoded data for this request. + */ + this.getData = function() { + var numPairs = g_keyValues.length; + var s = ''; + + /* + * Iterate over the key-value pairs. + */ + for (var i = 0; i < numPairs; i++) { + var keyValue = g_keyValues[i]; + var key = keyValue.getKey(); + var keyEncoded = encodeURIComponent(key); + var value = keyValue.getValue(); + var valueEncoded = encodeURIComponent(value); + + /* + * If this is not the first key-value pair, we need a separator. + */ + if (i > 0) + s += '&'; + + s += keyEncoded + '=' + valueEncoded; + } + + return s; + }; + +} + +/* + * This class implements helper functions to build a user interface. + */ +function UI() { + + /* + * Strings for the user interface. + */ + var strings = { + 'add': 'Add', + 'add_unit': 'Add unit', + 'auto_wah': 'Auto wah', + 'azimuth': 'Azimuth', + 'bandpass': 'Bandpass', + 'batch_processing': 'Batch processing', + 'beats_per_period': 'Beats per period', + 'bias': 'Bias', + 'boost': 'Boost', + 'bypass': 'Bypass', + 'cents': 'Cents', + 'channel': 'Channel', + 'chorus': 'Chorus', + 'delay': 'Delay', + 'delay_time': 'Delay time', + 'depth': 'Depth', + 'distance': 'Distance', + 'distortion': 'Distortion', + 'drive': 'Drive', + 'dsp_load': 'DSP load', + 'excess': 'Excess', + 'feedback': 'Feedback', + 'filter_1': 'Filter 1', + 'filter_2': 'Filter 2', + 'filter_3': 'Filter 3', + 'filter_4': 'Filter 4', + 'filter_5': 'Filter 5', + 'filter_6': 'Filter 6', + 'filter_7': 'Filter 7', + 'filter_8': 'Filter 8', + 'filter_order': 'Filter order', + 'flanger': 'Flanger', + 'follow': 'Follow', + 'frames_per_period': 'Frames per period', + 'frequency': 'Frequency', + 'frequency_1': 'Frequency 1', + 'frequency_2': 'Frequency 2', + 'from_input': 'From: Input', + 'fuzz': 'Fuzz', + 'gain': 'Gain', + 'high': 'High', + 'hold_time': 'Hold time', + 'input_amplitude': 'Input amplitude', + 'input_gain': 'Input gain', + 'latency': 'Latency', + 'level': 'Level', + 'level_1': 'Level 1', + 'level_2': 'Level 2', + 'level_3': 'Level 3', + 'level_4': 'Level 4', + 'level_5': 'Level 5', + 'level_6': 'Level 6', + 'level_7': 'Level 7', + 'level_8': 'Level 8', + 'level_clean': 'Level clean', + 'level_dist': 'Level dist', + 'level_hysteresis': 'Level hysteresis', + 'level_octave_down_first': 'Level octave down (I)', + 'level_octave_down_second': 'Level octave down (II)', + 'level_octave_up': 'Level octave up', + 'low': 'Low', + 'master': 'Master', + 'metronome': 'Metronome', + 'middle': 'Middle', + 'move_down': 'Move down', + 'move_up': 'Move up', + 'noise_gate': 'Noise gate', + 'note': 'Note', + 'octaver': 'Octaver', + 'overdrive': 'Overdrive', + 'phase': 'Phase', + 'phaser': 'Phaser', + 'polarity': 'Polarity', + 'power_amp': 'Power amp', + 'presence': 'Presence', + 'process_now': 'Process now', + 'remove': 'Remove', + 'ring_modulator': 'Ring modulator', + 'signal_amplitude': 'Signal amplitude', + 'signal_frequency': 'Signal frequency', + 'signal_gain': 'Signal gain', + 'signal_generator': 'Signal generator', + 'signal_levels': 'Signal levels', + 'signal_type': 'Signal type', + 'spatializer': 'Spatializer', + 'speed': 'Speed', + 'threshold_close': 'Threshold close', + 'threshold_open': 'Threshold open', + 'tick_sound': 'Tick sound', + 'tock_sound': 'Tock sound', + 'to_output': 'To: Output', + 'tone_stack': 'Tone stack', + 'tremolo': 'Tremolo', + 'tuner': 'Tuner' + }; + + /* + * Obtains a string for the user interface. + */ + this.getString = function(key) { + + /* + * Check whether key is defined in strings. + */ + if (key in strings) { + var s = strings[key]; + return s; + } else + return key; + + } + + /* + * Creates a turnable knob. + */ + this.createKnob = function(params) { + var label = params.label; + var valueMin = params.valueMin; + var valueMax = params.valueMax; + var valueDefault = params.valueDefault; + var valueWidth = params.valueWidth; + var valueHeight = params.valueHeight; + var valueAngle = params.angle; + var valueAngleArc = (valueAngle / 180.0) * Math.PI; + var valueCursor = params.cursor; + var valueReadonly = params.readonly; + var colorScheme = params.colorScheme; + var angleArc = (valueAngle / 180.0) * Math.PI; + var halfAngleArc = 0.5 * valueAngleArc; + var paramDiv = document.createElement('div'); + paramDiv.classList.add('paramdiv'); + var labelDiv = document.createElement('div'); + labelDiv.classList.add('knoblabel'); + var labelNode = document.createTextNode(label); + labelDiv.appendChild(labelNode); + paramDiv.appendChild(labelDiv); + var knobDiv = document.createElement('div'); + knobDiv.classList.add('knobdiv'); + var fgColor = '#ff8800'; + var bgColor = '#181818'; + + /* + * Check if we want a blue or green color scheme. + */ + if (colorScheme == 'blue') { + fgColor = '#8888ff'; + bgColor = '#181830'; + } else if (colorScheme == 'green') { + fgColor = '#88ff88'; + bgColor = '#181830'; + } + + var knobElem = pureknob.createKnob(valueHeight, valueWidth); + knobElem.setProperty('angleStart', -halfAngleArc); + knobElem.setProperty('angleEnd', halfAngleArc); + knobElem.setProperty('colorBG', bgColor); + knobElem.setProperty('colorFG', fgColor); + knobElem.setProperty('needle', valueCursor); + knobElem.setProperty('readonly', valueReadonly); + knobElem.setProperty('valMin', valueMin); + knobElem.setProperty('valMax', valueMax); + knobElem.setValue(valueDefault); + var knobNode = knobElem.node(); + knobDiv.appendChild(knobNode); + paramDiv.appendChild(knobDiv); + + /* + * Create knob. + */ + var knob = { + 'div': paramDiv, + 'node': knobNode, + 'obj': knobElem + }; + + return knob; + } + + /* + * Creates a drop down menu. + */ + this.createDropDown = function(params) { + var label = params.label; + var options = params.options; + var numOptions = options.length; + var selectedIndex = params.selectedIndex; + var paramDiv = document.createElement('div'); + paramDiv.classList.add('paramdiv'); + + /* + * Check if we should apply a label. + */ + if (label != null) { + var labelDiv = document.createElement('div'); + labelDiv.classList.add('dropdownlabel'); + var labelNode = document.createTextNode(label); + labelDiv.appendChild(labelNode); + paramDiv.appendChild(labelDiv); + } + + var selectElem = document.createElement('select'); + selectElem.classList.add('dropdown'); + + /* + * Add all options. + */ + for (var i = 0; i < numOptions; i++) { + var optionElem = document.createElement('option'); + optionElem.text = options[i]; + selectElem.add(optionElem); + } + + selectElem.selectedIndex = selectedIndex; + paramDiv.appendChild(selectElem); + + /* + * Create dropdown. + */ + var dropdown = { + 'div': paramDiv, + 'input': selectElem + }; + + return dropdown; + } + + /* + * Creates a button. + */ + this.createButton = function(params) { + var caption = params.caption; + var active = params.active; + var elem = document.createElement('button'); + elem.classList.add('button'); + + /* + * Check whether the button should be active + */ + if (active) + elem.classList.add('buttonactive'); + else + elem.classList.add('buttonnormal'); + + var captionNode = document.createTextNode(caption); + elem.appendChild(captionNode); + + /* + * Create button. + */ + var button = { + 'input': elem + }; + + return button; + } + + /* + * Creates a unit. + */ + this.createUnit = function(params) { + var typeString = params.type; + var buttonsParam = params.buttons; + var numButtonsParam = buttonsParam.length; + var buttons = []; + + /* + * Iterate over the buttons; + */ + for (var i = 0; i < numButtonsParam; i++) { + var buttonParam = buttonsParam[i]; + var label = buttonParam.label; + var active = buttonParam.active; + + /* + * Parameters for the button. + */ + var params = { + 'caption': label, + 'active': active + }; + + var button = this.createButton(params); + buttons.push(button); + } + + var unitDiv = document.createElement('div'); + unitDiv.classList.add('contentdiv'); + var headerDiv = document.createElement('div'); + headerDiv.classList.add('headerdiv'); + var numButtons = buttons.length; + + /* + * Add buttons to header. + */ + for (var i = 0; i < numButtons; i++) { + var button = buttons[i]; + headerDiv.appendChild(button.input); + } + + var labelDiv = document.createElement('div'); + labelDiv.classList.add('labeldiv'); + labelDiv.classList.add('active'); + var typeNode = document.createTextNode(typeString); + labelDiv.appendChild(typeNode); + headerDiv.appendChild(labelDiv); + unitDiv.appendChild(headerDiv); + var controlsDiv = document.createElement('div'); + controlsDiv.classList.add('controlsdiv'); + unitDiv.appendChild(controlsDiv); + + /* + * Create unit. + */ + var unit = { + 'div': unitDiv, + 'controls': controlsDiv, + 'buttons': buttons, + 'expanded': false + }; + + /* + * Adds a control to a unit. + */ + unit.addControl = function(control) { + var controlDiv = control.div; + this.controls.appendChild(controlDiv); + } + + /* + * Adds a row with controls to a unit. + */ + unit.addControlRow = function(controls) { + var rowDiv = document.createElement('div'); + var numControls = controls.length; + + /* + * Insert controls into the row. + */ + for (var i = 0; i < numControls; i++) { + var control = controls[i]; + var controlDiv = control.div; + rowDiv.appendChild(controlDiv); + } + + this.controls.appendChild(rowDiv); + } + + /* + * Expands or collapses a unit. + */ + unit.setExpanded = function(value) { + var controlsDiv = this.controls; + var displayValue = ''; + + /* + * Check whether we should expand or collapse the unit. + */ + if (value) + displayValue = 'block'; + else + displayValue = 'none'; + + controlsDiv.style.display = displayValue; + this.expanded = value; + } + + /* + * Returns whether a unit is expanded. + */ + unit.getExpanded = function() { + return this.expanded; + } + + /* + * Toggles a unit between expanded and collapsed state. + */ + unit.toggleExpanded = function() { + var state = this.getExpanded(); + this.setExpanded(!state); + } + + /* + * This is called when a user clicks on the label div. + */ + labelDiv.onclick = function(event) { + var unit = storage.get(this, 'unit'); + unit.toggleExpanded(); + } + + storage.put(labelDiv, 'unit', unit); + return unit; + } + + /* + * Checks whether a certain combination of unit type and parameter name requires special handling. + */ + this.isSpecialParameter = function(unitType, paramName) { + + /* + * Discern unit type. + */ + switch (unitType) { + case 'power_amp': + + if (paramName.startsWith('level_')) + return true; + else if (paramName.startsWith('filter_')) { + var suffix = paramName.substring(7); + var isNumeric = isFinite(suffix); + return isNumeric; + } + + default: + return false; + } + + } + + /* + * Renders a unit given chain and unit ID, as well as a description returned from the server. + */ + this.renderUnit = function(chainId, unitId, description) { + var bypassButtonLabel = ui.getString('bypass'); + var moveUpButtonLabel = ui.getString('move_up'); + var moveDownButtonLabel = ui.getString('move_down'); + var removeButtonLabel = ui.getString('remove'); + var unitTypes = globals.unitTypes; + var unitTypeId = description.Type; + var unitType = unitTypes[unitTypeId]; + var unitTypeString = ui.getString(unitType); + var bypassActive = description.Bypass; + + /* + * Buttons for this unit. + */ + var buttons = [ + { + 'label': bypassButtonLabel, + 'active': bypassActive + }, + { + 'label': moveUpButtonLabel, + 'active': false + }, + { + 'label': moveDownButtonLabel, + 'active': false + }, + { + 'label': removeButtonLabel, + 'active': false + } + ]; + + /* + * Parameters for the unit UI element. + */ + var paramsUnit = { + 'type': unitTypeString, + 'buttons': buttons + }; + + var unit = ui.createUnit(paramsUnit); + var btnBypass = unit.buttons[0].input; + storage.put(btnBypass, 'chain', chainId); + storage.put(btnBypass, 'unit', unitId); + storage.put(btnBypass, 'active', bypassActive); + + /* + * This is invoked when someone clicks on the 'bypass' button. + */ + btnBypass.onclick = function(event) { + var chainId = storage.get(this, 'chain'); + var unitId = storage.get(this, 'unit'); + var active = !storage.get(this, 'active'); + + /* + * Check whether the control should be active. + */ + if (active) { + this.classList.remove('buttonnormal'); + this.classList.add('buttonactive'); + } else { + this.classList.remove('buttonactive'); + this.classList.add('buttonnormal'); + } + + storage.put(this, 'active', active); + handler.setBypass(chainId, unitId, active); + }; + + var btnMoveUp = unit.buttons[1].input; + storage.put(btnMoveUp, 'chain', chainId); + storage.put(btnMoveUp, 'unit', unitId); + + /* + * This is invoked when someone clicks on the 'move up' button. + */ + btnMoveUp.onclick = function(event) { + var chainId = storage.get(this, 'chain'); + var unitId = storage.get(this, 'unit'); + handler.moveUp(chainId, unitId); + }; + + var btnMoveDown = unit.buttons[2].input; + storage.put(btnMoveDown, 'chain', chainId); + storage.put(btnMoveDown, 'unit', unitId); + + /* + * This is invoked when someone clicks on the 'move down' button. + */ + btnMoveDown.onclick = function(event) { + var chainId = storage.get(this, 'chain'); + var unitId = storage.get(this, 'unit'); + handler.moveDown(chainId, unitId); + }; + + var btnRemove = unit.buttons[3].input; + btnRemove.classList.add('buttonremove'); + storage.put(btnRemove, 'chain', chainId); + storage.put(btnRemove, 'unit', unitId); + + /* + * This is invoked when someone clicks on the 'remove' button. + */ + btnRemove.onclick = function(event) { + var chainId = storage.get(this, 'chain'); + var unitId = storage.get(this, 'unit'); + handler.removeUnit(chainId, unitId); + }; + + var unitParams = description.Parameters; + var numParams = unitParams.length; + + /* + * Iterate over the parameters and add all 'ordinary' (non-special) ones to the unit. + */ + for (var i = 0; i < numParams; i++) { + var currentParam = unitParams[i]; + var paramType = currentParam.Type; + var paramName = currentParam.Name; + var isSpecial = this.isSpecialParameter(unitType, paramName); + + /* + * Only handle 'ordinary' (non-special) parameters on the first pass. + */ + if (!isSpecial) { + var isFloating = (i != 0); + var label = ui.getString(paramName); + + /* + * Handle numeric parameter. + */ + if (paramType == 'numeric') { + + /* + * Parameters for the knob. + */ + var params = { + 'label': label, + 'valueMin': currentParam.Minimum, + 'valueMax': currentParam.Maximum, + 'valueDefault': currentParam.NumericValue, + 'valueWidth': 150, + 'valueHeight': 150, + 'angle': 270, + 'cursor': false, + 'colorScheme': 'default', + 'readonly': false + }; + + var knob = ui.createKnob(params); + unit.addControl(knob); + var knobNode = knob.node; + storage.put(knobNode, 'chain', chainId); + storage.put(knobNode, 'unit', unitId); + storage.put(knobNode, 'param', paramName); + + /* + * This is called when a numeric value changes. + */ + var knobHandler = function(knob, value) { + var knobNode = knob.node(); + var chain = storage.get(knobNode, 'chain'); + var unit = storage.get(knobNode, 'unit'); + var param = storage.get(knobNode, 'param'); + handler.setNumericValue(chain, unit, param, value); + }; + + var knobObj = knob.obj; + knobObj.addListener(knobHandler); + } + + /* + * Handle discrete parameter. + */ + if (paramType == 'discrete') { + + /* + * Parameters for the drop down menu. + */ + var params = { + 'label': label, + 'options': currentParam.DiscreteValues, + 'selectedIndex': currentParam.DiscreteValueIndex + }; + + var dropDown = ui.createDropDown(params); + var dropDownInput = dropDown.input; + storage.put(dropDownInput, 'chain', chainId); + storage.put(dropDownInput, 'unit', unitId); + storage.put(dropDownInput, 'param', paramName); + + /* + * This is called when a discrete value changes. + */ + dropDownInput.onchange = function(event) { + var chain = storage.get(this, 'chain'); + var unit = storage.get(this, 'unit'); + var param = storage.get(this, 'param'); + var idx = this.selectedIndex; + var option = this.options[idx]; + var value = option.text; + handler.setDiscreteValue(chain, unit, param, value); + }; + + unit.addControl(dropDown); + } + + } + + } + + /* + * Iterate over the parameters and add all special discrete ones to the unit. + */ + for (var i = 0; i < numParams; i++) { + var param = unitParams[i]; + var paramType = param.Type; + var paramName = param.Name; + var isSpecial = this.isSpecialParameter(unitType, paramName); + + /* + * Only handle special discrete parameters on the second pass. + */ + if (isSpecial & (paramType == 'discrete')) { + var label = ui.getString(paramName); + + /* + * Parameters for the drop down menu. + */ + var params = { + 'label': label, + 'options': param.DiscreteValues, + 'selectedIndex': param.DiscreteValueIndex + }; + + var dropDown = ui.createDropDown(params); + var dropDownInput = dropDown.input; + storage.put(dropDownInput, 'chain', chainId); + storage.put(dropDownInput, 'unit', unitId); + storage.put(dropDownInput, 'param', paramName); + + /* + * This is called when a discrete value changes. + */ + dropDownInput.onchange = function(event) { + var chain = storage.get(this, 'chain'); + var unit = storage.get(this, 'unit'); + var param = storage.get(this, 'param'); + var idx = this.selectedIndex; + var option = this.options[idx]; + var value = option.text; + handler.setDiscreteValue(chain, unit, param, value); + }; + + var controlRow = Array(); + controlRow.push(dropDown); + unit.addControlRow(controlRow); + } + + } + + /* + * Iterate over the parameters and add all special numeric ones to the unit. + */ + for (var i = 0; i < numParams; i++) { + var param = unitParams[i]; + var paramType = param.Type; + var paramName = param.Name; + var isSpecial = this.isSpecialParameter(unitType, paramName); + + /* + * Only handle special numeric parameters on the third pass. + */ + if (isSpecial & (paramType == 'numeric')) { + var label = ui.getString(paramName); + + /* + * Parameters for the knob. + */ + var params = { + 'label': label, + 'valueMin': param.Minimum, + 'valueMax': param.Maximum, + 'valueDefault': param.NumericValue, + 'valueWidth': 150, + 'valueHeight': 150, + 'angle': 270, + 'cursor': false, + 'colorScheme': 'default', + 'readonly': false + }; + + var knob = ui.createKnob(params); + unit.addControl(knob); + var knobNode = knob.node; + storage.put(knobNode, 'chain', chainId); + storage.put(knobNode, 'unit', unitId); + storage.put(knobNode, 'param', paramName); + + /* + * This is called when a numeric value changes. + */ + var knobHandler = function(knob, value) { + var knobNode = knob.node(); + var chain = storage.get(knobNode, 'chain'); + var unit = storage.get(knobNode, 'unit'); + var param = storage.get(knobNode, 'param'); + handler.setNumericValue(chain, unit, param, value); + }; + + var knobObj = knob.obj; + knobObj.addListener(knobHandler); + } + + } + + return unit; + } + + /* + * Renders a signal chain, given its ID and a chain description returned from the server. + */ + this.renderSignalChain = function(id, description) { + var idString = id.toString(); + var chainDiv = document.createElement('div'); + var beginDiv = document.createElement('div'); + beginDiv.classList.add('contentdiv'); + beginDiv.classList.add('iodiv'); + var beginHeaderDiv = document.createElement('div'); + beginHeaderDiv.classList.add('headerdiv'); + var beginLabelDiv = document.createElement('div'); + beginLabelDiv.classList.add('labeldiv'); + var labelFromInput = ui.getString('from_input'); + var beginLabelText = labelFromInput + ' ' + idString; + var beginLabelNode = document.createTextNode(beginLabelText); + beginLabelDiv.appendChild(beginLabelNode); + beginHeaderDiv.appendChild(beginLabelDiv); + beginDiv.appendChild(beginHeaderDiv); + chainDiv.appendChild(beginDiv); + var units = description.Units; + var numUnits = units.length; + + /* + * Iterate over the units in this chain. + */ + for (var i = 0; i < numUnits; i++) { + var unit = units[i]; + var result = this.renderUnit(id, i, unit); + var unitDiv = result.div; + chainDiv.appendChild(unitDiv); + } + + var labelDropdown = ui.getString('add_unit'); + var labelButton = ui.getString('add'); + var unitTypes = globals.unitTypes; + var numUnitTypes = unitTypes.length; + var unitTypeNames = new Array(); + + /* + * Look up the name of the unit types. + */ + for (var i = 0; i < numUnitTypes; i++) { + var unitType = unitTypes[i]; + var unitTypeName = ui.getString(unitType); + unitTypeNames.push(unitTypeName); + } + + /* + * Parameters for the drop down menu. + */ + var paramsDropDown = { + 'label': labelDropdown, + 'options': unitTypeNames, + 'selectedIndex': 0 + }; + + var dropDown = ui.createDropDown(paramsDropDown); + + /* + * Parameters for the 'create' button. + */ + var paramsButton = { + 'caption': labelButton, + 'active': false + }; + + var button = ui.createButton(paramsButton); + var buttonElem = button.input; + + /* + * What happens when we click on the 'add' button. + */ + buttonElem.onclick = function(event) { + var chainId = storage.get(this, 'chain'); + var dropdown = storage.get(this, 'dropdown'); + var unitType = dropdown.selectedIndex; + handler.addUnit(unitType, chainId); + } + + storage.put(buttonElem, 'chain', id); + storage.put(buttonElem, 'dropdown', dropDown.input); + var dropDownDiv = document.createElement('div'); + dropDownDiv.classList.add('contentdiv'); + dropDownDiv.classList.add('addunitdiv'); + dropDownDiv.appendChild(dropDown.div); + dropDownDiv.appendChild(buttonElem); + chainDiv.appendChild(dropDownDiv); + var endDiv = document.createElement('div'); + endDiv.classList.add('contentdiv'); + endDiv.classList.add('iodiv'); + var endHeaderDiv = document.createElement('div'); + endHeaderDiv.classList.add('headerdiv'); + var endLabelDiv = document.createElement('div'); + endLabelDiv.classList.add('labeldiv'); + var labelToOutput = ui.getString('to_output'); + var endLabelText = labelToOutput + ' ' + idString; + var endLabelNode = document.createTextNode(endLabelText); + endLabelDiv.appendChild(endLabelNode); + endHeaderDiv.appendChild(endLabelDiv); + endDiv.appendChild(endHeaderDiv); + chainDiv.appendChild(endDiv); + + /* + * This object represents the signal chain. + */ + var chain = { + 'div': chainDiv + }; + + return chain; + } + + /* + * Renders the signal chains given a configuration returned from the server. + */ + this.renderSignalChains = function(configuration) { + var elem = document.getElementById('signal_chains'); + helper.clearElement(elem); + var chains = configuration.Chains; + var numChains = chains.length; + + /* + * Iterate over the signal chains. + */ + for (var i = 0; i < numChains; i++) { + var chain = chains[i]; + var result = this.renderSignalChain(i, chain); + var chainDiv = result.div; + elem.append(chainDiv); + var spacerDiv = document.createElement('div'); + spacerDiv.classList.add('spacerdiv'); + elem.appendChild(spacerDiv); + } + + } + + /* + * Renders the latency configuration given a configuration returned from the server. + */ + this.renderLatency = function(configuration) { + var elem = document.getElementById('latency'); + helper.clearElement(elem); + var unitDiv = document.createElement('div'); + unitDiv.classList.add('contentdiv'); + unitDiv.classList.add('masterunitdiv'); + var headerDiv = document.createElement('div'); + var labelDiv = document.createElement('div'); + labelDiv.classList.add('labeldiv'); + labelDiv.classList.add('active'); + labelDiv.classList.add('io'); + var label = ui.getString('latency'); + var labelNode = document.createTextNode(label); + labelDiv.appendChild(labelNode); + headerDiv.appendChild(labelDiv); + headerDiv.classList.add('headerdiv'); + unitDiv.appendChild(headerDiv); + var controlsDiv = document.createElement('div'); + controlsDiv.classList.add('controlsdiv'); + unitDiv.appendChild(controlsDiv); + elem.appendChild(unitDiv); + var labelFramesPerPeriod = ui.getString('frames_per_period'); + var framesPerPeriod = configuration.FramesPerPeriod; + var dropdownRow = document.createElement('div'); + var values = [64, 128, 256, 512, 1024, 2048, 4096, 8192]; + var valueIdx = 0; + + /* + * Iterate over all possible values. + */ + for (var i = 0; i < values.length; i++) { + var currentValue = values[i]; + + /* + * If we have a match, store index. + */ + if (framesPerPeriod == currentValue) + valueIdx = i; + + } + + /* + * Parameters for the frames per period drop down menu. + */ + var paramsFramesPerPeriod = { + 'label': labelFramesPerPeriod, + 'options': values, + 'selectedIndex': valueIdx + }; + + var dropDownFramesPerPeriod = ui.createDropDown(paramsFramesPerPeriod); + var dropDownFramesPerPeriodElem = dropDownFramesPerPeriod.input; + + /* + * This is called when the period size changes. + */ + dropDownFramesPerPeriodElem.onchange = function(event) { + var idx = this.selectedIndex; + var option = this.options[idx]; + var value = option.text; + handler.setFramesPerPeriod(value); + }; + + dropdownRow.appendChild(dropDownFramesPerPeriod.div); + controlsDiv.appendChild(dropdownRow); + + /* + * Create unit object. + */ + var unit = { + 'controls': controlsDiv, + 'expanded': false + }; + + /* + * Expands or collapses a unit. + */ + unit.setExpanded = function(value) { + var controlsDiv = this.controls; + var displayValue = ''; + + /* + * Check whether we should expand or collapse the unit. + */ + if (value) + displayValue = 'block'; + else + displayValue = 'none'; + + controlsDiv.style.display = displayValue; + this.expanded = value; + } + + /* + * Returns whether a unit is expanded. + */ + unit.getExpanded = function() { + return this.expanded; + } + + /* + * Toggles a unit between expanded and collapsed state. + */ + unit.toggleExpanded = function() { + var state = this.getExpanded(); + this.setExpanded(!state); + } + + /* + * This is called when a user clicks on the label div. + */ + labelDiv.onclick = function(event) { + var unit = storage.get(this, 'unit'); + unit.toggleExpanded(); + } + + storage.put(labelDiv, 'unit', unit); + } + + /* + * Updates the tuner display based on information returned from the server. + */ + this.updateTuner = function(result) { + var cents = result.Cents; + var frequency = result.Frequency; + var note = result.Note; + var centsDiv = document.querySelector('.tunercentsknob'); + var centsKnob = storage.get(centsDiv, 'knob'); + centsKnob.setValue(cents); + var frequencyDiv = document.querySelector('.tunerfrequencydiv'); + var frequencyString = frequency.toFixed(4); + frequencyDiv.innerHTML = frequencyString; + var noteDiv = document.querySelector('.tunernotediv'); + var noteString = note.toString(); + noteDiv.innerHTML = noteString; + } + + /* + * Renders the tuner given a configuration returned from the server. + */ + this.renderTuner = function(configuration) { + var chainsConfiguration = configuration.Chains; + var numChannels = chainsConfiguration.length; + var tunerConfiguration = configuration.Tuner; + var elem = document.getElementById('tuner'); + helper.clearElement(elem); + var unitDiv = document.createElement('div'); + unitDiv.classList.add('contentdiv'); + unitDiv.classList.add('masterunitdiv'); + var headerDiv = document.createElement('div'); + var labelDiv = document.createElement('div'); + labelDiv.classList.add('labeldiv'); + labelDiv.classList.add('active'); + labelDiv.classList.add('io'); + var label = ui.getString('tuner'); + var labelNode = document.createTextNode(label); + labelDiv.appendChild(labelNode); + headerDiv.appendChild(labelDiv); + headerDiv.classList.add('headerdiv'); + unitDiv.appendChild(headerDiv); + var controlsDiv = document.createElement('div'); + controlsDiv.classList.add('controlsdiv'); + unitDiv.appendChild(controlsDiv); + elem.appendChild(unitDiv); + var centsString = ui.getString('cents'); + var frequencyString = ui.getString('frequency'); + var noteString = ui.getString('note'); + var centsValue = tunerConfiguration.BeatsPerPeriod; + + /* + * Parameters for the cents knob. + */ + var centsParams = { + 'label': centsString, + 'valueMin': -50, + 'valueMax': 50, + 'valueDefault': 0, + 'valueWidth': 150, + 'valueHeight': 150, + 'angle': 270, + 'cursor': true, + 'colorScheme': 'green', + 'readonly': true + }; + + var centsKnob = ui.createKnob(centsParams); + var centsKnobNode = centsKnob.node; + centsKnobNode.classList.add('tunercentsknob'); + var centsKnobObj = centsKnob.obj; + storage.put(centsKnobNode, 'knob', centsKnobObj); + var centsKnobDiv = centsKnob.div; + controlsDiv.appendChild(centsKnobDiv); + var frequencyRow = document.createElement('div'); + var labelFrequency = ui.getString('frequency'); + var frequencyLabelDiv = document.createElement('div'); + frequencyLabelDiv.classList.add('labeldiv'); + var frequencyLabelNode = document.createTextNode(labelFrequency); + frequencyLabelDiv.appendChild(frequencyLabelNode); + frequencyRow.appendChild(frequencyLabelDiv); + var frequencyValueDiv = document.createElement('div'); + frequencyValueDiv.classList.add('tunerfrequencydiv'); + frequencyRow.appendChild(frequencyValueDiv); + controlsDiv.appendChild(frequencyRow); + var noteRow = document.createElement('div'); + var labelNote = ui.getString('note'); + var noteLabelDiv = document.createElement('div'); + noteLabelDiv.classList.add('labeldiv'); + var noteLabelNode = document.createTextNode(labelNote); + noteLabelDiv.appendChild(noteLabelNode); + noteRow.appendChild(noteLabelDiv); + var noteNameDiv = document.createElement('div'); + noteNameDiv.classList.add('tunernotediv'); + noteRow.appendChild(noteNameDiv); + controlsDiv.appendChild(noteRow); + var channelRow = document.createElement('div'); + var labelChannel = ui.getString('channel'); + var channels = ['- NONE -']; + + /* + * Append indices for all channels. + */ + for (var i = 0; i < numChannels; i++) { + var idxString = i.toString(); + channels.push(idxString); + } + + var channelIdx = tunerConfiguration.Channel; + var channelIdxInc = channelIdx + 1; + + /* + * Parameters for the channel drop down menu. + */ + var paramsChannel = { + 'label': labelChannel, + 'options': channels, + 'selectedIndex': channelIdxInc + }; + + var dropDownChannel = ui.createDropDown(paramsChannel); + var dropDownChannelElem = dropDownChannel.input; + + /* + * This is called when the channel number changes. + */ + dropDownChannelElem.onchange = function(event) { + var idx = this.selectedIndex; + var option = this.options[idx]; + var value = option.text; + var interval = storage.get(this, 'interval'); + window.clearInterval(interval); + + /* + * This gets executed whenever the timer ticks. + */ + var callback = function() { + handler.refreshTuner(); + } + + /* + * Handle special case of no channel and register timer + * for updating readings for the UI. + */ + if (value == '- NONE -') + value = '-1'; + else { + var intervalNew = window.setInterval(callback, 250); + storage.put(this, 'interval', intervalNew); + } + + handler.setTunerValue('channel', value); + }; + + dropDownChannelElem.onchange(null); + var dropDownChannelDiv = dropDownChannel.div; + channelRow.appendChild(dropDownChannelDiv); + controlsDiv.appendChild(channelRow); + + /* + * Create unit object. + */ + var unit = { + 'controls': controlsDiv, + 'expanded': false + }; + + /* + * Expands or collapses a unit. + */ + unit.setExpanded = function(value) { + var controlsDiv = this.controls; + var displayValue = ''; + + /* + * Check whether we should expand or collapse the unit. + */ + if (value) + displayValue = 'block'; + else + displayValue = 'none'; + + controlsDiv.style.display = displayValue; + this.expanded = value; + } + + /* + * Returns whether a unit is expanded. + */ + unit.getExpanded = function() { + return this.expanded; + } + + /* + * Toggles a unit between expanded and collapsed state. + */ + unit.toggleExpanded = function() { + var state = this.getExpanded(); + this.setExpanded(!state); + } + + /* + * This is called when a user clicks on the label div. + */ + labelDiv.onclick = function(event) { + var unit = storage.get(this, 'unit'); + unit.toggleExpanded(); + } + + storage.put(labelDiv, 'unit', unit); + } + + /* + * Renders the spatializer given a configuration returned from the server. + */ + this.renderSpatializer = function(configuration) { + var spatializer = configuration.Spatializer; + var channels = spatializer.Channels; + var numChannels = channels.length; + var elem = document.getElementById('spatializer'); + helper.clearElement(elem); + var unitDiv = document.createElement('div'); + unitDiv.classList.add('contentdiv'); + unitDiv.classList.add('masterunitdiv'); + var headerDiv = document.createElement('div'); + var labelDiv = document.createElement('div'); + labelDiv.classList.add('labeldiv'); + labelDiv.classList.add('active'); + labelDiv.classList.add('io'); + var label = ui.getString('spatializer'); + var labelNode = document.createTextNode(label); + labelDiv.appendChild(labelNode); + headerDiv.appendChild(labelDiv); + headerDiv.classList.add('headerdiv'); + unitDiv.appendChild(headerDiv); + var controlsDiv = document.createElement('div'); + controlsDiv.classList.add('controlsdiv'); + unitDiv.appendChild(controlsDiv); + elem.appendChild(unitDiv); + + /* + * Iterate over the channels. + */ + for (var i = 0; i < numChannels; i++) { + var iString = i.toString(); + var channel = channels[i]; + var azimuth = channel.Azimuth; + var distance = 10 * channel.Distance; + var level = 100 * channel.Level; + var azimuthString = ui.getString('azimuth'); + var azimuthLabel = azimuthString + ' ' + iString; + + /* + * Parameters for the azimuth knob. + */ + var azimuthParams = { + 'label': azimuthLabel, + 'valueMin': -90, + 'valueMax': 90, + 'valueDefault': azimuth, + 'valueWidth': 150, + 'valueHeight': 150, + 'angle': 180, + 'cursor': true, + 'colorScheme': 'blue', + 'readonly': false + }; + + var azimuthKnob = ui.createKnob(azimuthParams); + var azimuthKnobDiv = azimuthKnob.div; + controlsDiv.appendChild(azimuthKnobDiv); + var distanceString = ui.getString('distance'); + var distanceLabel = distanceString + ' ' + iString; + + /* + * Parameters for the distance knob. + */ + var distanceParams = { + 'label': distanceLabel, + 'valueMin': 0, + 'valueMax': 100, + 'valueDefault': distance, + 'valueWidth': 150, + 'valueHeight': 150, + 'angle': 270, + 'cursor': false, + 'colorScheme': 'blue', + 'readonly': false + }; + + var distanceKnob = ui.createKnob(distanceParams); + var distanceKnobDiv = distanceKnob.div; + controlsDiv.append(distanceKnobDiv); + var levelString = ui.getString('level'); + var levelLabel = levelString + ' ' + iString; + + /* + * Parameters for the level knob. + */ + var levelParams = { + 'label': levelLabel, + 'valueMin': 0, + 'valueMax': 100, + 'valueDefault': level, + 'valueWidth': 150, + 'valueHeight': 150, + 'angle': 270, + 'cursor': false, + 'colorScheme': 'blue', + 'readonly': false + }; + + var levelKnob = ui.createKnob(levelParams); + var levelKnobDiv = levelKnob.div; + controlsDiv.append(levelKnobDiv); + var azimuthKnobNode = azimuthKnob.node; + var distanceKnobNode = distanceKnob.node; + var levelKnobNode = levelKnob.node; + storage.put(azimuthKnobNode, 'channel', i); + storage.put(distanceKnobNode, 'channel', i); + storage.put(levelKnobNode, 'channel', i); + + /* + * This gets executed when the azimuth value changes. + */ + var azimuthHandler = function(knob, value) { + var node = knob.node(); + var channel = storage.get(node, 'channel'); + handler.setAzimuth(channel, value); + }; + + /* + * This gets executed when the distance value changes. + */ + var distanceHandler = function(knob, value) { + var node = knob.node(); + var channel = storage.get(node, 'channel'); + var distanceValue = (0.1 * value).toFixed(1); + handler.setDistance(channel, distanceValue); + }; + + /* + * This gets executed when the level value changes. + */ + var levelHandler = function(knob, value) { + var node = knob.node(); + var channel = storage.get(node, 'channel'); + var levelValue = (0.01 * value).toFixed(2); + handler.setLevel(channel, levelValue); + }; + + var azimuthKnobObj = azimuthKnob.obj; + azimuthKnobObj.addListener(azimuthHandler); + var distanceKnobObj = distanceKnob.obj; + distanceKnobObj.addListener(distanceHandler); + var levelKnobObj = levelKnob.obj; + levelKnobObj.addListener(levelHandler); + } + + /* + * Create unit object. + */ + var unit = { + 'controls': controlsDiv, + 'expanded': false + }; + + /* + * Expands or collapses a unit. + */ + unit.setExpanded = function(value) { + var controlsDiv = this.controls; + var displayValue = ''; + + /* + * Check whether we should expand or collapse the unit. + */ + if (value) + displayValue = 'block'; + else + displayValue = 'none'; + + controlsDiv.style.display = displayValue; + this.expanded = value; + } + + /* + * Returns whether a unit is expanded. + */ + unit.getExpanded = function() { + return this.expanded; + } + + /* + * Toggles a unit between expanded and collapsed state. + */ + unit.toggleExpanded = function() { + var state = this.getExpanded(); + this.setExpanded(!state); + } + + /* + * This is called when a user clicks on the label div. + */ + labelDiv.onclick = function(event) { + var unit = storage.get(this, 'unit'); + unit.toggleExpanded(); + } + + storage.put(labelDiv, 'unit', unit); + } + + /* + * Renders the metronome given a configuration returned from the server. + */ + this.renderMetronome = function(configuration) { + var metronomeConfiguration = configuration.Metronome; + var masterOutput = metronomeConfiguration.MasterOutput; + var elem = document.getElementById('metronome'); + helper.clearElement(elem); + var unitDiv = document.createElement('div'); + unitDiv.classList.add('contentdiv'); + unitDiv.classList.add('masterunitdiv'); + var headerDiv = document.createElement('div'); + var masterString = ui.getString('master'); + + /* + * Parameters for metronome button. + */ + var paramsButton = { + caption: masterString, + active: masterOutput + }; + + var button = ui.createButton(paramsButton); + var buttonElem = button.input; + storage.put(buttonElem, 'active', masterOutput); + + /* + * This is called when the user clicks on the 'master' button of the metronome. + */ + buttonElem.onclick = function(event) { + var active = !storage.get(this, 'active'); + + /* + * Check whether the control should be active. + */ + if (active) { + this.classList.remove('buttonnormal'); + this.classList.add('buttonactive'); + } else { + this.classList.remove('buttonactive'); + this.classList.add('buttonnormal'); + } + + storage.put(this, 'active', active); + handler.setMetronomeValue('master-output', active); + } + + headerDiv.appendChild(buttonElem); + var labelDiv = document.createElement('div'); + labelDiv.classList.add('labeldiv'); + labelDiv.classList.add('active'); + labelDiv.classList.add('io'); + var label = ui.getString('metronome'); + var labelNode = document.createTextNode(label); + labelDiv.appendChild(labelNode); + headerDiv.appendChild(labelDiv); + headerDiv.classList.add('headerdiv'); + unitDiv.appendChild(headerDiv); + var controlsDiv = document.createElement('div'); + controlsDiv.classList.add('controlsdiv'); + unitDiv.appendChild(controlsDiv); + elem.appendChild(unitDiv); + var beatsString = ui.getString('beats_per_period'); + var beatsValue = metronomeConfiguration.BeatsPerPeriod; + + /* + * Parameters for the beats per period knob. + */ + var beatsParams = { + 'label': beatsString, + 'valueMin': 1, + 'valueMax': 16, + 'valueDefault': beatsValue, + 'valueWidth': 150, + 'valueHeight': 150, + 'angle': 270, + 'cursor': false, + 'colorScheme': 'blue', + 'readonly': false + }; + + var beatsKnob = ui.createKnob(beatsParams); + var beatsKnobDiv = beatsKnob.div; + controlsDiv.appendChild(beatsKnobDiv); + var speedString = ui.getString('speed'); + var speedValue = metronomeConfiguration.Speed; + + /* + * Parameters for the speed knob. + */ + var speedParams = { + 'label': speedString, + 'valueMin': 40, + 'valueMax': 360, + 'valueDefault': speedValue, + 'valueWidth': 150, + 'valueHeight': 150, + 'angle': 270, + 'cursor': false, + 'colorScheme': 'blue', + 'readonly': false + }; + + var speedKnob = ui.createKnob(speedParams); + var speedKnobDiv = speedKnob.div; + controlsDiv.appendChild(speedKnobDiv); + + /* + * This gets executed when the beats per period value changes. + */ + var beatsHandler = function(knob, value) { + handler.setMetronomeValue('beats-per-period', value); + }; + + /* + * This gets executed when the beats per period value changes. + */ + var speedHandler = function(knob, value) { + handler.setMetronomeValue('speed', value); + }; + + var beatsKnobObj = beatsKnob.obj; + beatsKnobObj.addListener(beatsHandler); + var speedKnobObj = speedKnob.obj; + speedKnobObj.addListener(speedHandler); + var sounds = metronomeConfiguration.Sounds; + var numSounds = sounds.length; + var tickSound = metronomeConfiguration.TickSound; + var tockSound = metronomeConfiguration.TockSound; + var tickIdx = 0; + var tockIdx = 0; + + /* + * Iterate over all sounds and find the tick and tock sound. + */ + for (var i = 0; i < numSounds; i++) { + var sound = sounds[i]; + + /* + * If we found the tick sound, store index. + */ + if (sound == tickSound) + tickIdx = i; + + /* + * If we found the tock sound, store index. + */ + if (sound == tockSound) + tockIdx = i; + + } + + var labelTick = ui.getString('tick_sound'); + var labelTock = ui.getString('tock_sound'); + + /* + * Parameters for the tick sound drop down menu. + */ + var paramsTick = { + 'label': labelTick, + 'options': sounds, + 'selectedIndex': tickIdx + }; + + /* + * Parameters for the tock sound drop down menu. + */ + var paramsTock = { + 'label': labelTock, + 'options': sounds, + 'selectedIndex': tockIdx + }; + + var dropDownTick = ui.createDropDown(paramsTick); + var dropDownTock = ui.createDropDown(paramsTock); + var dropDownTickElem = dropDownTick.input; + var dropDownTockElem = dropDownTock.input; + + /* + * This is called when the tick sound changes. + */ + dropDownTickElem.onchange = function(event) { + var idx = this.selectedIndex; + var option = this.options[idx]; + var value = option.text; + handler.setMetronomeValue('tick-sound', value); + }; + + /* + * This is called when the tock sound changes. + */ + dropDownTockElem.onchange = function(event) { + var idx = this.selectedIndex; + var option = this.options[idx]; + var value = option.text; + handler.setMetronomeValue('tock-sound', value); + }; + + var controlRowTick = document.createElement('div'); + controlRowTick.appendChild(dropDownTick.div); + controlsDiv.appendChild(controlRowTick); + var controlRowTock = document.createElement('div'); + controlRowTock.appendChild(dropDownTock.div); + controlsDiv.appendChild(controlRowTock); + + /* + * Create unit object. + */ + var unit = { + 'controls': controlsDiv, + 'expanded': false + }; + + /* + * Expands or collapses a unit. + */ + unit.setExpanded = function(value) { + var controlsDiv = this.controls; + var displayValue = ''; + + /* + * Check whether we should expand or collapse the unit. + */ + if (value) + displayValue = 'block'; + else + displayValue = 'none'; + + controlsDiv.style.display = displayValue; + this.expanded = value; + } + + /* + * Returns whether a unit is expanded. + */ + unit.getExpanded = function() { + return this.expanded; + } + + /* + * Toggles a unit between expanded and collapsed state. + */ + unit.toggleExpanded = function() { + var state = this.getExpanded(); + this.setExpanded(!state); + } + + /* + * This is called when a user clicks on the label div. + */ + labelDiv.onclick = function(event) { + var unit = storage.get(this, 'unit'); + unit.toggleExpanded(); + } + + storage.put(labelDiv, 'unit', unit); + } + + /* + * Renders the signal level analysis section given a configuration returned from the server. + */ + this.renderSignalLevels = function(configuration) { + var batchProcessing = configuration.BatchProcessing; + var elem = document.getElementById('levels'); + helper.clearElement(elem); + + /* + * Only display levels if batch processing is disabled on the server. + */ + if (!batchProcessing) { + var unitDiv = document.createElement('div'); + unitDiv.classList.add('contentdiv'); + unitDiv.classList.add('masterunitdiv'); + var headerDiv = document.createElement('div'); + var labelDiv = document.createElement('div'); + labelDiv.classList.add('labeldiv'); + labelDiv.classList.add('active'); + labelDiv.classList.add('io'); + var label = ui.getString('signal_levels'); + var labelNode = document.createTextNode(label); + labelDiv.appendChild(labelNode); + headerDiv.appendChild(labelDiv); + headerDiv.classList.add('headerdiv'); + unitDiv.appendChild(headerDiv); + var controlsDiv = document.createElement('div'); + controlsDiv.classList.add('controlsdiv'); + unitDiv.appendChild(controlsDiv); + elem.appendChild(unitDiv); + + /* + * This is called when the unit is collapsed or expanded. + */ + var expansionListener = function(unit, value) { + + /* + * Check if unit shall expand or contract. + * + * If unit gets expanded, register timer, which requests + * 'get-level-analysis' CGI regularly (every 250 ms). + * + * If unit gets contracted, unregister timer. + */ + if (value) { + + /* + * This is executed on each timer tick. + * + * Query 'get-level-analysis' CGI. + * + * On response, check if all channel names are already known and + * in the same order as in the response. + * + * If not, clear controls DIV, generate new channel controls and + * store new channel names. + * + * Update controls to represent new level and peak values obtained + * from the response. + */ + var callback = function() { + + /* + * This is called when the server returns a response. + */ + var responseListener = function(response) { + var dspLoadControl = unit.dspLoadControl; + var channelNames = unit.channelNames; + var numNames = channelNames.length; + var channelControls = unit.channelControls; + var idx = 0; + var mismatch = (dspLoadControl == null); + + /* + * Iterate over all categories ("Inputs", "Outputs", + * "Metronome", "Master", ...) in the response. + */ + for (var key in response) { + var keyNative = response.hasOwnProperty(key); + + /* + * Verify that the key is an actual property of + * the response (and not of the prototype). + */ + if (keyNative) { + var category = response[key]; + var numChannels = category.length; + + /* + * Iterate over the channels. + */ + for (var i = 0; i < numChannels; i++) { + var channel = category[i]; + var channelNameResponse = channel.ChannelName; + + /* + * If one of the channels does not match, + * report mismatch. + */ + if (numNames <= idx) + mismatch = true; + else { + var channelNameControls = channelNames[idx]; + + /* + * Check if name of the response matches name + * of the control. + */ + if (channelNameResponse != channelNameControls) + mismatch = true; + + } + + idx++; + } + + } + + } + + /* + * If the channel mapping has changed, create new controls. + */ + if (mismatch) { + var controlsDiv = unit.controls; + helper.clearElement(controlsDiv); + var dspLoadString = ui.getString('dsp_load'); + var dspLoadLabelDiv = document.createElement('div'); + var dspLoadLabelNode = document.createTextNode(dspLoadString); + dspLoadLabelDiv.appendChild(dspLoadLabelNode); + dspLoadControl = pureknob.createBarGraph(400, 40); + dspLoadControl.setProperty('colorFG', '#ff4444'); + dspLoadControl.setProperty('colorMarkers', '#ffffff'); + dspLoadControl.setProperty('markerStart', 0); + dspLoadControl.setProperty('markerEnd', 100); + dspLoadControl.setProperty('markerStep', 25); + dspLoadControl.setProperty('valMin', 0); + dspLoadControl.setProperty('valMax', 100); + dspLoadControl.setValue(0); + var node = dspLoadControl.node(); + var nodeWrapper = document.createElement('div'); + nodeWrapper.appendChild(node); + var container = document.createElement('div'); + container.appendChild(dspLoadLabelDiv); + container.appendChild(nodeWrapper); + controlsDiv.appendChild(container); + channelNames = []; + channelControls = []; + + /* + * Iterate over all categories ("Inputs", "Outputs", + * "Metronome", "Master", ...) in the response. + */ + for (var key in response) { + var isProperty = response.hasOwnProperty(key); + + /* + * Verify that the key is an actual property of + * the response (and not of the prototype). + */ + if (isProperty) { + var category = response[key]; + var numChannels = category.length; + + /* + * Iterate over the channels. + */ + for (var i = 0; i < numChannels; i++) { + var channel = category[i]; + var channelName = channel.ChannelName; + var channelControl = pureknob.createBarGraph(400, 40); + channelControl.setProperty('colorFG', '#44ff44'); + channelControl.setProperty('colorMarkers', '#ffffff'); + channelControl.setProperty('markerStart', -60); + channelControl.setProperty('markerEnd', 0); + channelControl.setProperty('markerStep', 10); + channelControl.setProperty('valMin', -145); + channelControl.setProperty('valMax', 0); + channelControl.setValue(-145); + channelNames.push(channelName); + channelControls.push(channelControl); + var channelNameDiv = document.createElement('div'); + var channelNameNode = document.createTextNode(channelName); + channelNameDiv.appendChild(channelNameNode); + var node = channelControl.node(); + var nodeWrapper = document.createElement('div'); + nodeWrapper.appendChild(node); + var container = document.createElement('div'); + container.appendChild(channelNameDiv); + container.appendChild(nodeWrapper); + controlsDiv.appendChild(container); + } + + } + + } + + } + + /* + * Display DSP load. + */ + if (dspLoadControl != null) { + var dspLoad = response.DSPLoad; + dspLoadControl.setValue(dspLoad); + } + + idx = 0; + + /* + * Iterate over all categories ("Inputs", "Outputs", + * "Metronome", "Master", ...) in the response. + */ + for (var key in response) { + var isProperty = response.hasOwnProperty(key); + + /* + * Verify that the key is an actual property of + * the response (and not of the prototype). + */ + if (isProperty) { + var category = response[key]; + var numChannels = category.length; + + /* + * Iterate over the channels. + */ + for (var i = 0; i < numChannels; i++) { + var channel = category[i]; + var channelLevel = channel.Level; + var channelPeak = channel.Peak; + var channelControl = channelControls[idx]; + channelControl.setValue(channelLevel); + channelControl.setPeaks([channelPeak]); + idx++; + } + + } + + } + + unit.dspLoadControl = dspLoadControl; + unit.channelNames = channelNames; + unit.channelControls = channelControls; + } + + handler.getLevelAnalysis(responseListener); + }; + + var timer = window.setInterval(callback, 200); + unit.timer = timer; + } else { + var timer = unit.timer; + + /* + * If a timer is registered, clear it. + */ + if (timer != null) + window.clearInterval(timer); + + unit.timer = null; + } + + }; + + /* + * Create unit object. + */ + var unit = { + 'channelNames': [], + 'channelControls': [], + 'controls': controlsDiv, + 'expanded': false, + 'listeners': [expansionListener], + 'timer': null + }; + + /* + * Expands or collapses a unit. + */ + unit.setExpanded = function(value) { + var controlsDiv = this.controls; + var displayValue = ''; + + /* + * Check whether we should expand or collapse the unit. + */ + if (value) + displayValue = 'block'; + else + displayValue = 'none'; + + controlsDiv.style.display = displayValue; + this.expanded = value; + var listeners = this.listeners; + + /* + * Check if there are listeners resistered. + */ + if (listeners != null) { + var numListeners = listeners.length; + + /* + * Invoke each listener. + */ + for (var i = 0; i < numListeners; i++) { + var listener = listeners[i]; + listener(this, value); + } + + } + + } + + /* + * Returns whether a unit is expanded. + */ + unit.getExpanded = function() { + return this.expanded; + } + + /* + * Toggles a unit between expanded and collapsed state. + */ + unit.toggleExpanded = function() { + var state = this.getExpanded(); + this.setExpanded(!state); + } + + /* + * This is called when a user clicks on the label div. + */ + labelDiv.onclick = function(event) { + var unit = storage.get(this, 'unit'); + unit.toggleExpanded(); + } + + storage.put(labelDiv, 'unit', unit); + } + + } + + /* + * Renders the 'processing' button given a configuration returned from the server. + */ + this.renderProcessing = function(configuration) { + var batchProcessing = configuration.BatchProcessing; + var elem = document.getElementById('processing'); + helper.clearElement(elem); + + /* + * Only display this if batch processing is enabled on the server. + */ + if (batchProcessing) { + var unitDiv = document.createElement('div'); + unitDiv.classList.add('contentdiv'); + unitDiv.classList.add('masterunitdiv'); + var headerDiv = document.createElement('div'); + var processString = ui.getString('process_now'); + + /* + * Parameters for process button. + */ + var paramsButton = { + caption: processString, + active: false + }; + + var button = ui.createButton(paramsButton); + var buttonElem = button.input; + storage.put(buttonElem, 'active', batchProcessing); + + /* + * This is called when the user clicks on the 'process' button. + */ + buttonElem.onclick = function(event) { + var active = storage.get(this, 'active'); + + /* + * Trigger batch processing if the control is active. + */ + if (active) + handler.process(); + + } + + headerDiv.appendChild(buttonElem); + var labelDiv = document.createElement('div'); + labelDiv.classList.add('labeldiv'); + labelDiv.classList.add('active'); + labelDiv.classList.add('io'); + var label = ui.getString('batch_processing'); + var labelNode = document.createTextNode(label); + labelDiv.appendChild(labelNode); + headerDiv.appendChild(labelDiv); + headerDiv.classList.add('headerdiv'); + unitDiv.appendChild(headerDiv); + var controlsDiv = document.createElement('div'); + controlsDiv.classList.add('controlsdiv'); + unitDiv.appendChild(controlsDiv); + elem.appendChild(unitDiv); + + /* + * Create unit object. + */ + var unit = { + 'controls': controlsDiv, + 'expanded': false + }; + + /* + * Expands or collapses a unit. + */ + unit.setExpanded = function(value) { + var controlsDiv = this.controls; + var displayValue = ''; + + /* + * Check whether we should expand or collapse the unit. + */ + if (value) + displayValue = 'block'; + else + displayValue = 'none'; + + controlsDiv.style.display = displayValue; + this.expanded = value; + } + + /* + * Returns whether a unit is expanded. + */ + unit.getExpanded = function() { + return this.expanded; + } + + /* + * Toggles a unit between expanded and collapsed state. + */ + unit.toggleExpanded = function() { + var state = this.getExpanded(); + this.setExpanded(!state); + } + + /* + * This is called when a user clicks on the label div. + */ + labelDiv.onclick = function(event) { + var unit = storage.get(this, 'unit'); + unit.toggleExpanded(); + } + + storage.put(labelDiv, 'unit', unit); + } + + } + +} + +var ui = new UI(); + +/* + * This class implements all handler functions for user interaction. + */ +function Handler() { + var self = this; + + /* + * This is called when a new effects unit should be added. + */ + this.addUnit = function(unitType, chain) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt, otherwise refresh rack. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Adding new unit failed: ' + reason; + console.log(msg); + } else + self.refresh(); + + } + + }; + + var unitTypeString = unitType.toString(); + var chainString = chain.toString(); + var request = new Request(); + request.append('cgi', 'add-unit'); + request.append('type', unitTypeString); + request.append('chain', chainString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a new level analysis should be obtained. + */ + this.getLevelAnalysis = function(callback) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var levels = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (levels != null) + callback(levels); + + }; + + var request = new Request(); + request.append('cgi', 'get-level-analysis'); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, false); + } + + /* + * This is called when a unit should be moved down the chain. + */ + this.moveDown = function(chain, unit) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Moving unit down failed: ' + reason; + console.log(msg); + } else + self.refresh(); + + } + + }; + + var chainString = chain.toString(); + var unitString = unit.toString(); + var request = new Request(); + request.append('cgi', 'move-down'); + request.append('chain', chainString); + request.append('unit', unitString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a unit should be moved up the chain. + */ + this.moveUp = function(chain, unit) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Moving unit up failed: ' + reason; + console.log(msg); + } else + self.refresh(); + + } + + }; + + var chainString = chain.toString(); + var unitString = unit.toString(); + var request = new Request(); + request.append('cgi', 'move-up'); + request.append('chain', chainString); + request.append('unit', unitString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a unit should be removed from a chain. + */ + this.removeUnit = function(chain, unit) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Removing unit failed: ' + reason; + console.log(msg); + } else + self.refresh(); + + } + + }; + + var chainString = chain.toString(); + var unitString = unit.toString(); + var request = new Request(); + request.append('cgi', 'remove-unit'); + request.append('chain', chainString); + request.append('unit', unitString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a new azimuth value should be set. + */ + this.setAzimuth = function(chain, value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting azimuth value failed: ' + reason; + console.log(msg); + } + + } + + }; + + var chainString = chain.toString(); + var valueString = value.toString() + var request = new Request(); + request.append('cgi', 'set-azimuth'); + request.append('chain', chainString); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a unit should be bypassed or bypass should be disabled for a unit. + */ + this.setBypass = function(chain, unit, value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting bypass value failed: ' + reason; + console.log(msg); + } + + } + + }; + + var chainString = chain.toString(); + var unitString = unit.toString(); + var valueString = value.toString(); + var request = new Request(); + request.append('cgi', 'set-bypass'); + request.append('chain', chainString); + request.append('unit', unitString); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a new distance value should be set. + */ + this.setDistance = function(chain, value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting distance value failed: ' + reason; + console.log(msg); + } + + } + + }; + + var chainString = chain.toString(); + var valueString = value.toString(); + var request = new Request(); + request.append('cgi', 'set-distance'); + request.append('chain', chainString); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a discrete value should be set. + */ + this.setDiscreteValue = function(chain, unit, param, value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting discrete value failed: ' + reason; + console.log(msg); + } + + } + + }; + + var chainString = chain.toString(); + var unitString = unit.toString(); + var paramString = param.toString(); + var valueString = value.toString(); + var request = new Request(); + request.append('cgi', 'set-discrete-value'); + request.append('chain', chainString); + request.append('unit', unitString); + request.append('param', paramString); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when period size should be changed. + */ + this.setFramesPerPeriod = function(value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting frames per period failed: ' + reason; + console.log(msg); + } + + } + + }; + + var valueString = value.toString(); + var request = new Request(); + request.append('cgi', 'set-frames-per-period'); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a new level value should be set. + */ + this.setLevel = function(chain, value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting level value failed: ' + reason; + console.log(msg); + } + + } + + }; + + var chainString = chain.toString(); + var valueString = value.toString(); + var request = new Request(); + request.append('cgi', 'set-level'); + request.append('chain', chainString); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a metronome value should be changed. + */ + this.setMetronomeValue = function(param, value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting metronome value failed: ' + reason; + console.log(msg); + } + + } + + }; + + var paramString = param.toString(); + var valueString = value.toString(); + var request = new Request(); + request.append('cgi', 'set-metronome-value'); + request.append('param', paramString); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a tuner value should be changed. + */ + this.setTunerValue = function(param, value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting tuner value failed: ' + reason; + console.log(msg); + } + + } + + }; + + var paramString = param.toString(); + var valueString = value.toString(); + var request = new Request(); + request.append('cgi', 'set-tuner-value'); + request.append('param', paramString); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a numeric value should be set. + */ + this.setNumericValue = function(chain, unit, param, value) { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var webResponse = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (webResponse != null) { + + /* + * If we were not successful, log failed attempt. + */ + if (webResponse.Success != true) { + var reason = webResponse.Reason; + var msg = 'Setting numeric value failed: ' + reason; + console.log(msg); + } + + } + + }; + + var chainString = chain.toString(); + var unitString = unit.toString(); + var paramString = param.toString(); + var valueString = value.toString(); + var request = new Request(); + request.append('cgi', 'set-numeric-value'); + request.append('chain', chainString); + request.append('unit', unitString); + request.append('param', paramString); + request.append('value', valueString); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when the configuration needs to be refreshed. + */ + this.refresh = function() { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var configuration = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (configuration != null) { + ui.renderSignalChains(configuration); + ui.renderLatency(configuration); + ui.renderTuner(configuration); + ui.renderSpatializer(configuration); + ui.renderMetronome(configuration); + ui.renderSignalLevels(configuration); + ui.renderProcessing(configuration); + } + + }; + + var request = new Request(); + request.append('cgi', 'get-configuration'); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + + /* + * This is called when a new analysis should be performed by the tuner. + */ + this.refreshTuner = function() { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var analysis = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (analysis != null) { + ui.updateTuner(analysis); + } + + }; + + var request = new Request(); + request.append('cgi', 'get-tuner-analysis'); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, false); + } + + /* + * This is called when the user clicks on the 'process' button. + */ + this.process = function() { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + helper.blockSite(true); + }; + + var request = new Request(); + request.append('cgi', 'process'); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, false); + } + + /* + * This is called when the user interface initializes. + */ + this.initialize = function() { + + /* + * This gets called when the server returns a response. + */ + var responseHandler = function(response) { + var unitTypes = helper.parseJSON(response); + + /* + * Check if the response is valid JSON. + */ + if (unitTypes != null) { + var numUnitTypes = unitTypes.length; + + /* + * Iterate over the unit types and add them to the global list. + */ + for (var i = 0; i < numUnitTypes; i++) { + var t = unitTypes[i]; + globals.unitTypes.push(t); + } + + self.refresh(); + } + + }; + + var request = new Request(); + request.append('cgi', 'get-unit-types'); + var requestBody = request.getData(); + ajax.request('POST', globals.cgi, requestBody, responseHandler, true); + } + +} + +/* + * The (global) event handlers. + */ +var handler = new Handler(); +document.addEventListener('DOMContentLoaded', handler.initialize); + diff --git a/webroot/js/include/pureknob.js b/webroot/js/include/pureknob.js new file mode 100644 index 0000000..ff918f9 --- /dev/null +++ b/webroot/js/include/pureknob.js @@ -0,0 +1,1081 @@ +/* + * pure-knob + * + * Canvas-based JavaScript UI element implementing touch, + * keyboard, mouse and scroll wheel support. + * + * Copyright 2018 Andre Plötze + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use strict"; + +/* + * Custom user interface elements for pure knob. + */ +function PureKnob() { + + /* + * Creates a bar graph element. + */ + this.createBarGraph = function(width, height) { + var heightString = height.toString(); + var widthString = width.toString(); + var canvas = document.createElement('canvas'); + var div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.height = heightString + 'px'; + div.style.position = 'relative'; + div.style.textAlign = 'center'; + div.style.width = widthString + 'px'; + div.appendChild(canvas); + + /* + * The bar graph object. + */ + var graph = { + '_canvas': canvas, + '_div': div, + '_height': height, + '_width': width, + + /* + * Properties of this bar graph. + */ + '_properties': { + 'colorBG': '#181818', + 'colorFG': '#ff8800', + 'colorMarkers': '#888888', + 'markerStart': 0, + 'markerEnd': 100, + 'markerStep': 20, + 'trackWidth': 0.5, + 'valMin': 0, + 'valMax': 100, + 'valPeaks': [], + 'val': 0 + }, + + /* + * Returns the peak values for this bar graph. + */ + 'getPeaks': function() { + var properties = this._properties; + var peaks = properties.valPeaks; + var numPeaks = peaks.length; + var peaksCopy = []; + + /* + * Iterate over the peak values and copy them. + */ + for (var i = 0; i < numPeaks; i++) { + var peak = peaks[i]; + peaksCopy.push(peak); + } + + return peaksCopy; + }, + + /* + * Returns the value of a property of this bar graph. + */ + 'getProperty': function(key) { + var properties = this._properties; + var value = properties[key]; + return value; + }, + + /* + * Returns the current value of the bar graph. + */ + 'getValue': function() { + var properties = this._properties; + var value = properties.val; + return value; + }, + + /* + * Return the DOM node representing this bar graph. + */ + 'node': function() { + var div = this._div; + return div; + }, + + /* + * Redraw the bar graph on the canvas. + */ + 'redraw': function() { + this.resize(); + var properties = this._properties; + var colorTrack = properties.colorBG; + var colorFilling = properties.colorFG; + var colorMarkers = properties.colorMarkers; + var markerStart = properties.markerStart; + var markerEnd = properties.markerEnd; + var markerStep = properties.markerStep; + var trackWidth = properties.trackWidth; + var valMin = properties.valMin; + var valMax = properties.valMax; + var peaks = properties.valPeaks; + var value = properties.val; + var height = this._height; + var width = this._width; + var lineWidth = Math.round(trackWidth * height); + var halfWidth = 0.5 * lineWidth; + var centerY = 0.5 * height; + var lineTop = centerY - halfWidth; + var lineBottom = centerY + halfWidth; + var relativeValue = (value - valMin) / (valMax - valMin); + var fillingEnd = width * relativeValue; + var numPeaks = peaks.length; + var canvas = this._canvas; + var ctx = canvas.getContext('2d'); + + /* + * Clear the canvas. + */ + ctx.clearRect(0, 0, width, height); + + /* + * Check if markers should be drawn. + */ + if ((markerStart !== null) & (markerEnd !== null) & (markerStep !== null) & (markerStep !== 0)) { + + /* + * Draw the markers. + */ + for (var v = markerStart; v <= markerEnd; v += markerStep) { + var relativePos = (v - valMin) / (valMax - valMin); + var pos = Math.round(width * relativePos); + ctx.beginPath(); + ctx.moveTo(pos, 0); + ctx.lineTo(pos, height); + ctx.lineCap = 'butt'; + ctx.lineWidth = '2'; + ctx.strokeStyle = colorMarkers; + ctx.stroke(); + } + + } + + /* + * Draw the track. + */ + ctx.beginPath(); + ctx.rect(0, lineTop, width, lineWidth); + ctx.fillStyle = colorTrack; + ctx.fill(); + + /* + * Draw the filling. + */ + ctx.beginPath(); + ctx.rect(0, lineTop, fillingEnd, lineWidth); + ctx.fillStyle = colorFilling; + ctx.fill(); + + /* + * Draw the peaks. + */ + for (var i = 0; i < numPeaks; i++) { + var peak = peaks[i]; + var relativePeak = (peak - valMin) / (valMax - valMin); + var pos = Math.round(width * relativePeak); + ctx.beginPath(); + ctx.moveTo(pos, lineTop); + ctx.lineTo(pos, lineBottom); + ctx.lineCap = 'butt'; + ctx.lineWidth = '2'; + ctx.strokeStyle = colorFilling; + ctx.stroke(); + } + + }, + + /* + * This is called as the canvas or the surrounding DIV is resized. + */ + 'resize': function() { + var canvas = this._canvas; + canvas.style.height = '100%'; + canvas.style.width = '100%'; + canvas.height = this._height; + canvas.width = this._width; + }, + + /* + * Sets the peak values of this bar graph. + */ + 'setPeaks': function(peaks) { + var properties = this._properties; + var peaksCopy = []; + var numPeaks = peaks.length; + + /* + * Iterate over the peak values and append them to the array. + */ + for (var i = 0; i < numPeaks; i++) { + var peak = peaks[i]; + peaksCopy.push(peak); + } + + this.setProperty('valPeaks', peaksCopy); + }, + + /* + * Sets the value of a property of this bar graph. + */ + 'setProperty': function(key, value) { + this._properties[key] = value; + this.redraw(); + }, + + /* + * Sets the value of this bar graph. + */ + 'setValue': function(value) { + var properties = this._properties; + var valMin = properties.valMin; + var valMax = properties.valMax; + + /* + * Clamp the actual value into the [valMin; valMax] range. + */ + if (value < valMin) + value = valMin; + else if (value > valMax) + value = valMax; + + value = Math.round(value); + this.setProperty('val', value); + } + + }; + + /* + * This is called when the size of the canvas changes. + */ + var resizeListener = function(e) { + graph.redraw(); + }; + + canvas.addEventListener('resize', resizeListener); + return graph; + } + + /* + * Creates a knob element. + */ + this.createKnob = function(width, height) { + var heightString = height.toString(); + var widthString = width.toString(); + var smaller = width < height ? width : height; + var fontSize = 0.2 * smaller; + var fontSizeString = fontSize.toString(); + var canvas = document.createElement('canvas'); + var div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.height = heightString + 'px'; + div.style.position = 'relative'; + div.style.textAlign = 'center'; + div.style.width = widthString + 'px'; + div.appendChild(canvas); + var input = document.createElement('input'); + input.style.appearance = 'textfield'; + input.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; + input.style.border = 'none'; + input.style.color = '#ff8800'; + input.style.fontFamily = 'sans-serif'; + input.style.fontSize = fontSizeString + 'px'; + input.style.height = heightString + 'px'; + input.style.margin = 'auto'; + input.style.padding = '0px'; + input.style.textAlign = 'center'; + input.style.width = widthString + 'px'; + var inputMode = document.createAttribute('inputmode'); + inputMode.value = 'numeric'; + input.setAttributeNode(inputMode); + var inputDiv = document.createElement('div'); + inputDiv.style.bottom = '0px'; + inputDiv.style.display = 'none'; + inputDiv.style.left = '0px'; + inputDiv.style.position = 'absolute'; + inputDiv.style.right = '0px'; + inputDiv.style.top = '0px'; + inputDiv.appendChild(input); + div.appendChild(inputDiv); + + /* + * The knob object. + */ + var knob = { + '_canvas': canvas, + '_div': div, + '_height': height, + '_input': input, + '_inputDiv': inputDiv, + '_listeners': [], + '_mousebutton': false, + '_previousVal': 0, + '_timeout': null, + '_timeoutDoubleTap': null, + '_touchCount': 0, + '_width': width, + + /* + * Notify listeners about value changes. + */ + '_notifyUpdate': function() { + var properties = this._properties; + var value = properties.val; + var listeners = this._listeners; + var numListeners = listeners.length; + + /* + * Call all listeners. + */ + for (var i = 0; i < numListeners; i++) { + var listener = listeners[i]; + + /* + * Call listener, if it exists. + */ + if (listener !== null) + listener(this, value); + + } + + }, + + /* + * Properties of this knob. + */ + '_properties': { + 'angleEnd': 2.0 * Math.PI, + 'angleOffset': -0.5 * Math.PI, + 'angleStart': 0, + 'colorBG': '#181818', + 'colorFG': '#ff8800', + 'needle': false, + 'readonly': false, + 'trackWidth': 0.4, + 'valMin': 0, + 'valMax': 100, + 'val': 0 + }, + + /* + * Abort value change, restoring the previous value. + */ + 'abort': function() { + var previousValue = this._previousVal; + var properties = this._properties; + properties.val = previousValue; + this.redraw(); + }, + + /* + * Adds an event listener. + */ + 'addListener': function(listener) { + var listeners = this._listeners; + listeners.push(listener); + }, + + /* + * Commit value, indicating that it is no longer temporary. + */ + 'commit': function() { + var properties = this._properties; + var value = properties.val; + this._previousVal = value; + this.redraw(); + this._notifyUpdate(); + }, + + /* + * Returns the value of a property of this knob. + */ + 'getProperty': function(key) { + var properties = this._properties; + var value = properties[key]; + return value; + }, + + /* + * Returns the current value of the knob. + */ + 'getValue': function() { + var properties = this._properties; + var value = properties.val; + return value; + }, + + /* + * Return the DOM node representing this knob. + */ + 'node': function() { + var div = this._div; + return div; + }, + + /* + * Redraw the knob on the canvas. + */ + 'redraw': function() { + this.resize(); + var properties = this._properties; + var needle = properties.needle; + var angleStart = properties.angleStart; + var angleOffset = properties.angleOffset; + var angleEnd = properties.angleEnd; + var actualStart = angleStart + angleOffset; + var actualEnd = angleEnd + angleOffset; + var value = properties.val; + var valueStr = value.toString(); + var valMin = properties.valMin; + var valMax = properties.valMax; + var relValue = (value - valMin) / (valMax - valMin); + var relAngle = relValue * (angleEnd - angleStart); + var angleVal = actualStart + relAngle; + var colorTrack = properties.colorBG; + var colorFilling = properties.colorFG; + var trackWidth = properties.trackWidth; + var height = this._height; + var width = this._width; + var smaller = width < height ? width : height; + var centerX = 0.5 * width; + var centerY = 0.5 * height; + var radius = 0.4 * smaller; + var lineWidth = Math.round(trackWidth * radius); + var fontSize = 0.2 * smaller; + var fontSizeString = fontSize.toString(); + var canvas = this._canvas; + var ctx = canvas.getContext('2d'); + + /* + * Clear the canvas. + */ + ctx.clearRect(0, 0, width, height); + + /* + * Draw the track. + */ + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, actualStart, actualEnd); + ctx.lineCap = 'butt'; + ctx.lineWidth = lineWidth; + ctx.strokeStyle = colorTrack; + ctx.stroke(); + + /* + * Draw the filling. + */ + ctx.beginPath(); + + /* + * Check if we're in needle mode. + */ + if (needle) + ctx.arc(centerX, centerY, radius, angleVal - 0.1, angleVal + 0.1); + else + ctx.arc(centerX, centerY, radius, actualStart, angleVal); + + ctx.lineCap = 'butt'; + ctx.lineWidth = lineWidth; + ctx.strokeStyle = colorFilling; + ctx.stroke(); + + /* + * Draw the number. + */ + ctx.font = fontSizeString + 'px sans-serif'; + ctx.fillStyle = colorFilling; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(valueStr, centerX, centerY); + + /* + * Set the color of the input element. + */ + var elemInput = this._input; + elemInput.style.color = colorFilling; + }, + + /* + * This is called as the canvas or the surrounding DIV is resized. + */ + 'resize': function() { + var canvas = this._canvas; + canvas.style.height = '100%'; + canvas.style.width = '100%'; + canvas.height = this._height; + canvas.width = this._width; + }, + + /* + * Sets the value of a property of this knob. + */ + 'setProperty': function(key, value) { + this._properties[key] = value; + this.redraw(); + }, + + /* + * Sets the value of this knob. + */ + 'setValue': function(value) { + this.setValueFloating(value); + this.commit(); + }, + + /* + * Sets floating (temporary) value of this knob. + */ + 'setValueFloating': function(value) { + var properties = this._properties; + var valMin = properties.valMin; + var valMax = properties.valMax; + + /* + * Clamp the actual value into the [valMin; valMax] range. + */ + if (value < valMin) + value = valMin; + else if (value > valMax) + value = valMax; + + value = Math.round(value); + this.setProperty('val', value); + } + + }; + + /* + * Convert mouse event to value. + */ + var mouseEventToValue = function(e, properties) { + var canvas = e.target; + var width = canvas.scrollWidth; + var height = canvas.scrollHeight; + var centerX = 0.5 * width; + var centerY = 0.5 * height; + var x = e.offsetX; + var y = e.offsetY; + var relX = x - centerX; + var relY = y - centerY; + var angleStart = properties.angleStart; + var angleEnd = properties.angleEnd; + var angleDiff = angleEnd - angleStart; + var angle = Math.atan2(relX, -relY) - angleStart; + var twoPi = 2.0 * Math.PI; + + /* + * Make negative angles positive. + */ + if (angle < 0) { + + if (angleDiff >= twoPi) + angle += twoPi; + else + angle = 0; + + } + + var valMin = properties.valMin; + var valMax = properties.valMax; + var value = ((angle / angleDiff) * (valMax - valMin)) + valMin; + + /* + * Clamp values into valid interval. + */ + if (value < valMin) + value = valMin; + else if (value > valMax) + value = valMax; + + return value; + }; + + /* + * Convert touch event to value. + */ + var touchEventToValue = function(e, properties) { + var canvas = e.target; + var rect = canvas.getBoundingClientRect(); + var offsetX = rect.left; + var offsetY = rect.top; + var width = canvas.scrollWidth; + var height = canvas.scrollHeight; + var centerX = 0.5 * width; + var centerY = 0.5 * height; + var touches = e.targetTouches; + var touch = null; + + /* + * If there are touches, extract the first one. + */ + if (touches.length > 0) + touch = touches.item(0); + + var x = 0.0; + var y = 0.0; + + /* + * If a touch was extracted, calculate coordinates relative to + * the element position. + */ + if (touch !== null) { + var touchX = touch.pageX; + x = touchX - offsetX; + var touchY = touch.pageY; + y = touchY - offsetY; + } + + var relX = x - centerX; + var relY = y - centerY; + var angleStart = properties.angleStart; + var angleEnd = properties.angleEnd; + var angleDiff = angleEnd - angleStart; + var angle = Math.atan2(relX, -relY) - angleStart; + var twoPi = 2.0 * Math.PI; + + /* + * Make negative angles positive. + */ + if (angle < 0) { + + if (angleDiff >= twoPi) + angle += twoPi; + else + angle = 0; + + } + + var valMin = properties.valMin; + var valMax = properties.valMax; + var value = ((angle / angleDiff) * (valMax - valMin)) + valMin; + + /* + * Clamp values into valid interval. + */ + if (value < valMin) + value = valMin; + else if (value > valMax) + value = valMax; + + return value; + }; + + /* + * Show input element on double click. + */ + var doubleClickListener = function(e) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read-only, display input element. + */ + if (!readonly) { + e.preventDefault(); + var inputDiv = knob._inputDiv; + inputDiv.style.display = 'block'; + var inputElem = knob._input; + inputElem.focus(); + knob.redraw(); + } + + }; + + /* + * This is called when the mouse button is depressed. + */ + var mouseDownListener = function(e) { + var btn = e.buttons; + + /* + * It is a left-click. + */ + if (btn === 1) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read-only, process mouse event. + */ + if (!readonly) { + e.preventDefault(); + var val = mouseEventToValue(e, properties); + knob.setValueFloating(val); + } + + knob._mousebutton = true; + } + + /* + * It is a middle click. + */ + if (btn === 4) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read-only, display input element. + */ + if (!readonly) { + e.preventDefault(); + var inputDiv = knob._inputDiv; + inputDiv.style.display = 'block'; + var inputElem = knob._input; + inputElem.focus(); + knob.redraw(); + } + + } + + }; + + /* + * This is called when the mouse cursor is moved. + */ + var mouseMoveListener = function(e) { + var btn = knob._mousebutton; + + /* + * Only process event, if mouse button is depressed. + */ + if (btn) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read-only, process mouse event. + */ + if (!readonly) { + e.preventDefault(); + var val = mouseEventToValue(e, properties); + knob.setValueFloating(val); + } + + } + + }; + + /* + * This is called when the mouse button is released. + */ + var mouseUpListener = function(e) { + var btn = knob._mousebutton; + + /* + * Only process event, if mouse button was depressed. + */ + if (btn) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read only, process mouse event. + */ + if (!readonly) { + e.preventDefault(); + var val = mouseEventToValue(e, properties); + knob.setValue(val); + } + + } + + knob._mousebutton = false; + }; + + /* + * This is called when the drag action is canceled. + */ + var mouseCancelListener = function(e) { + var btn = knob._mousebutton; + + /* + * Abort action if mouse button was depressed. + */ + if (btn) { + knob.abort(); + knob._mousebutton = false; + } + + }; + + /* + * This is called when a user touches the element. + */ + var touchStartListener = function(e) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read-only, process touch event. + */ + if (!readonly) { + var touches = e.touches; + var numTouches = touches.length; + var singleTouch = (numTouches == 1); + + /* + * Only process single touches, not multi-touch + * gestures. + */ + if (singleTouch) { + knob._mousebutton = true; + + /* + * If this is the first touch, bind double tap + * interval. + */ + if (knob._touchCount == 0) { + + /* + * This is executed when the double tap + * interval times out. + */ + var f = function() { + + /* + * If control was tapped exactly + * twice, enable on-screen keyboard. + */ + if (knob._touchCount == 2) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read-only, + * display input element. + */ + if (!readonly) { + e.preventDefault(); + var inputDiv = knob._inputDiv; + inputDiv.style.display = 'block'; + var inputElem = knob._input; + inputElem.focus(); + knob.redraw(); + } + + } + + knob._touchCount = 0; + }; + + var timeout = knob._timeoutDoubleTap; + window.clearTimeout(timeout); + timeout = window.setTimeout(f, 500); + knob._timeoutDoubleTap = timeout; + } + + knob._touchCount++; + var val = touchEventToValue(e, properties); + knob.setValueFloating(val); + } + + } + + }; + + /* + * This is called when a user moves a finger on the element. + */ + var touchMoveListener = function(e) { + var btn = knob._mousebutton; + + /* + * Only process event, if mouse button is depressed. + */ + if (btn) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read-only, process touch event. + */ + if (!readonly) { + var touches = e.touches; + var numTouches = touches.length; + var singleTouch = (numTouches == 1); + + /* + * Only process single touches, not multi-touch + * gestures. + */ + if (singleTouch) { + e.preventDefault(); + var val = touchEventToValue(e, properties); + knob.setValueFloating(val); + } + + } + + } + + }; + + /* + * This is called when a user lifts a finger off the element. + */ + var touchEndListener = function(e) { + var btn = knob._mousebutton; + + /* + * Only process event, if mouse button was depressed. + */ + if (btn) { + var properties = knob._properties; + var readonly = properties.readonly; + + /* + * If knob is not read only, process touch event. + */ + if (!readonly) { + var touches = e.touches; + var numTouches = touches.length; + var singleTouch = (numTouches == 1); + + /* + * Only process single touches, not multi-touch + * gestures. + */ + if (singleTouch) { + e.preventDefault(); + knob._mousebutton = false; + knob.commit(); + } + + } + + } + + knob._mousebutton = false; + }; + + /* + * This is called when a user cancels a touch action. + */ + var touchCancelListener = function(e) { + var btn = knob._mousebutton; + + /* + * Abort action if mouse button was depressed. + */ + if (btn) { + knob.abort(); + knob._touchCount = 0; + var timeout = knob._timeoutDoubleTap; + window.clearTimeout(timeout); + } + + knob._mousebutton = false; + }; + + /* + * This is called when the size of the canvas changes. + */ + var resizeListener = function(e) { + knob.redraw(); + }; + + /* + * This is called when the mouse wheel is moved. + */ + var scrollListener = function(e) { + var readonly = knob.getProperty('readonly'); + + /* + * If knob is not read only, process mouse wheel event. + */ + if (!readonly) { + e.preventDefault(); + var delta = e.deltaY; + var direction = delta > 0 ? 1 : (delta < 0 ? -1 : 0); + var val = knob.getValue(); + val += direction; + knob.setValueFloating(val); + + /* + * Perform delayed commit. + */ + var commit = function() { + knob.commit(); + }; + + var timeout = knob._timeout; + window.clearTimeout(timeout); + timeout = window.setTimeout(commit, 250); + knob._timeout = timeout; + } + + }; + + /* + * This is called when the user presses a key on the keyboard. + */ + var keyPressListener = function(e) { + var kc = e.keyCode; + + /* + * Hide input element when user presses enter or escape. + */ + if ((kc === 13) || (kc === 27)) { + var inputDiv = knob._inputDiv; + inputDiv.style.display = 'none'; + var input = e.target; + + /* + * Only evaluate value when user pressed enter. + */ + if (kc === 13) { + var value = input.value; + var val = parseInt(value); + var valid = isFinite(val); + + /* + * Check if input is a valid number. + */ + if (valid) + knob.setValue(val); + + } + + input.value = ''; + } + + }; + + canvas.addEventListener('dblclick', doubleClickListener); + canvas.addEventListener('mousedown', mouseDownListener); + canvas.addEventListener('mouseleave', mouseCancelListener); + canvas.addEventListener('mousemove', mouseMoveListener); + canvas.addEventListener('mouseup', mouseUpListener); + canvas.addEventListener('resize', resizeListener); + canvas.addEventListener('touchstart', touchStartListener); + canvas.addEventListener('touchmove', touchMoveListener); + canvas.addEventListener('touchend', touchEndListener); + canvas.addEventListener('touchcancel', touchCancelListener); + canvas.addEventListener('wheel', scrollListener); + input.addEventListener('keypress', keyPressListener); + return knob; + }; + +} + +var pureknob = new PureKnob(); + diff --git a/webserver/webserver.go b/webserver/webserver.go new file mode 100644 index 0000000..479be7e --- /dev/null +++ b/webserver/webserver.go @@ -0,0 +1,322 @@ +package webserver + +import ( + "crypto/tls" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +/* + * Exchange format for HTTP requests. + */ +type HttpRequest struct { + Protocol string + Method string + Path string + Host string + Params map[string]string +} + +/* + * Exchange format for HTTP responses. + */ +type HttpResponse struct { + Header map[string]string + Body []byte +} + +/* + * Data structure holding channels for communication between a CGI and the web + * server. + */ +type WebChannels struct { + Requests chan HttpRequest + Responses chan HttpResponse +} + +/* + * Data structure for web server configuration. + */ +type Config struct { + Name string + Port string + TLSPort string + TLSPrivateKey string + TLSPublicKey string + WebRoot string + Index string + MimeTypes map[string]string + DefaultMime string + ErrorMime string +} + +/* + * Data structure holding the web server's internal state. + */ +type webServerStruct struct { + cgis map[string]WebChannels + config Config +} + +/* + * The public interface of the web server. + */ +type WebServer interface { + RegisterCgi(path string) WebChannels + GetCgis() []string + RemoveCgi(path string) + Run() +} + +/* + * Set default headers for HTTP(S) responses so that we don't have to set them + * in every handler. This sets a name for the server, a default MIME type, and + * disables all forms of caching (local and via proxies). + */ +func (this *webServerStruct) setDefaultHeaders(writer http.ResponseWriter, request *http.Request) { + cfg := this.config + srv := cfg.Name + mime := cfg.DefaultMime + hdr := writer.Header() + hdr.Set("Server", srv) + hdr.Set("Content-type", mime) + hdr.Set("Cache-control", "max-age=0, no-cache, no-store") + hdr.Set("Pragma", "no-cache") +} + +/* + * A handler for CGI requests. + */ +func (this *webServerStruct) cgiHandler(writer http.ResponseWriter, request *http.Request) { + request.ParseForm() + + /* + * The parsed HTTP request. + */ + hrequest := HttpRequest{ + Protocol: request.Proto, + Method: request.Method, + Path: request.URL.Path, + Host: request.Host, + Params: map[string]string{}, + } + + /* + * Iterate over all form values and parse parameters. + */ + for key, values := range request.Form { + params := strings.Join(values, ",") + hrequest.Params[key] = params + } + + /* + * Interact with the CGI via channels to send request, fetch response. + */ + cgi := this.cgis[hrequest.Path] + cgi.Requests <- hrequest + response := <-cgi.Responses + this.setDefaultHeaders(writer, request) + hdr := writer.Header() + + /* + * Write response headers. + */ + for key, value := range response.Header { + hdr.Set(key, value) + } + + writer.Write(response.Body) +} + +/* + * A handler for file requests. This allows, e. g. (X)HTML, CSS, JavaScript + * content and images to be served. + */ +func (this *webServerStruct) fileHandler(writer http.ResponseWriter, request *http.Request) { + url := request.URL.Path + this.setDefaultHeaders(writer, request) + cfg := this.config + + /* + * If navigated to web root, redirect to index file, otherwise serve file. + */ + if (url == "") || (url == "/") { + hdr := writer.Header() + hdr.Set("Location", cfg.Index) + writer.WriteHeader(http.StatusFound) + } else { + dotPos := strings.LastIndex(url, ".") + extension := "" + + /* + * Check for file extension. + */ + if dotPos != -1 { + dotPosInc := dotPos + 1 + extension = url[dotPosInc:] + } + + mimetype, present := cfg.MimeTypes[extension] + + /* + * Check if a MIME type is registered for this extension. + */ + if !present { + mimetype = cfg.DefaultMime + } + + path := cfg.WebRoot + url + fd, err := os.Open(path) + hdr := writer.Header() + + /* + * Check if file exists in web root. + */ + if err != nil { + hdr.Set("Content-type", cfg.ErrorMime) + fmt.Fprintf(writer, "[ERROR] - '%s' does not exist!\n", url) + } else { + hdr.Set("Content-type", mimetype) + io.Copy(writer, fd) + } + + } + +} + +/* + * Redirect insecure requests to TLS. + */ +func (this *webServerStruct) redirect(writer http.ResponseWriter, request *http.Request) { + split := strings.SplitN(request.Host, ":", 2) + host := split[0] + this.setDefaultHeaders(writer, request) + uri := request.RequestURI + uriChars := []rune(uri) + + /* + * Ensure that the URI starts with a slash. + */ + if string(uriChars[0]) != "/" { + uri = "/" + uri + } + + url := fmt.Sprintf("https://%s:%s%s", host, this.config.TLSPort, uri) + http.Redirect(writer, request, url, http.StatusFound) +} + +/* + * Registers a CGI with the web server. The 'path' given specifies the URL + * under which the CGI is available. When the CGI is called, the web server + * generates a WebRequest and puts it into the request queue. + */ +func (this *webServerStruct) RegisterCgi(path string) WebChannels { + requests := make(chan HttpRequest) + responses := make(chan HttpResponse) + channels := WebChannels{Requests: requests, Responses: responses} + + /* + * If no CGI map exists, create one. + */ + if this.cgis == nil { + this.cgis = make(map[string]WebChannels) + } + + this.cgis[path] = channels + return channels +} + +/* + * Returns a list of the URLs of all currently registered CGIs. + */ +func (this *webServerStruct) GetCgis() []string { + cgis := []string{} + + /* + * Append all CGI paths to list. + */ + for path, _ := range this.cgis { + cgis = append(cgis, path) + } + + return cgis +} + +/* + * Remove all CGIs currently registered with the web server. + */ +func (this *webServerStruct) RemoveCgi(path string) { + delete(this.cgis, path) +} + +/* + * The main function of the web server. This loads the web server configuration + * from the file system, sets up the HTTP request handlers and runs the HTTP + * listener. + */ +func (this *webServerStruct) Run() { + + /* + * Use only GCM (no CBC) and only SHA-2 (no SHA-1!). + */ + ciphersuites := []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + } + + /* + * Curves to use for elliptic curve cryptography. + */ + curves := []tls.CurveID{ + tls.X25519, + } + + /* + * Use at least TLS 1.2 and Curve25519 (no NIST-Curves!). + */ + tlsConfig := tls.Config{ + MinVersion: tls.VersionTLS12, + CurvePreferences: curves, + CipherSuites: ciphersuites, + } + + cfg := this.config + tlsAddr := fmt.Sprintf(":%s", cfg.TLSPort) + + /* + * The TLS server. + */ + tlsServer := http.Server{ + Addr: tlsAddr, + TLSConfig: &tlsConfig, + } + + /* + * Register all CGI paths to HTTP handler. + */ + for path, _ := range this.cgis { + http.HandleFunc(path, this.cgiHandler) + } + + http.HandleFunc("/", this.fileHandler) + go tlsServer.ListenAndServeTLS(cfg.TLSPublicKey, cfg.TLSPrivateKey) + httpAddr := fmt.Sprintf(":%s", cfg.Port) + go http.ListenAndServe(httpAddr, http.HandlerFunc(this.redirect)) +} + +/* + * Creates a new web server. + */ +func CreateWebServer(cfg Config) WebServer { + server := webServerStruct{config: cfg} + return &server +}