Giter VIP home page Giter VIP logo

sointu's Introduction

Sointu

Tests Binaries

A cross-architecture and cross-platform modular software synthesizer for small intros, forked from 4klang. Targetable architectures include 386, amd64, and WebAssembly; targetable platforms include Windows, Mac, Linux (and related) + browser.

User manual will be in the Wiki.

Installation

You can either 1) download the prebuilt release binaries from the releases; or 2) download the latest build from the master branch from the actions (find workflow "Binaries" and scroll down for .zip files containing the artifacts). Then just run one of the executables or, in the case of the VST plugins library files, copy them wherever you keep you VST2 plugins.

The pre 1.0 version tags are mostly for reference: no backwards compatibility will be guaranteed while upgrading to a newer version. Backwards compatibility will be attempted from 1.0 onwards.

Uninstallation: Sointu stores recovery data in OS-specific folders e.g. AppData/Roaming/Sointu on Windows. For clean uninstall, delete also this folder. See here where to find those folders on other platforms.

Summary

Sointu is work-in-progress. It is a fork and an evolution of 4klang, a modular software synthesizer intended to easily produce music for 4k intros — small executables with a maximum filesize of 4096 bytes containing realtime audio and visuals. Like 4klang, the sound is produced by a virtual machine that executes small bytecode to produce the audio; however, by now the internal virtual machine has been heavily rewritten and extended. It is actually extended so much that you will never fit all the features at the same time in a 4k intro, but a fairly capable synthesis engine can already be fitted in 600 bytes (386, compressed), with another few hundred bytes for the patch and pattern data.

Sointu consists of two core elements:

  • A cross-platform synth-tracker that runs as either VSTi or stand-alone app for composing music, written in go. The app is still heavily work in progress. The app exports the projects as .yml files.
  • A compiler, likewise written in go, which can be invoked from the command line to compile these .yml files into .asm or .wat code. For x86/amd64, the resulting .asm can be then compiled by nasm. For browsers, the resulting .wat can be compiled by wat2wasm.

This is how the current prototype app looks like:

Screenshot of the tracker

Building

Various aspects of the project have different tool dependencies, which are listed below.

Sointu-track

This is the stand-alone version of the synth-tracker. Sointu-track uses the gioui for the GUI and oto for the audio, so the portability is currently limited by these.

Prerequisites

  • go
  • If you want to use the faster x86 assembly written synthesizer:
    • Follow the instructions to build the x86 native virtual machine before building the tracker.
    • cgo compatible compiler e.g. gcc. On windows, you best bet is MinGW. We use the tdm-gcc. The compiler can be in PATH or you can use the environment variable CC to help go find the compiler.
    • Setting environment variable CGO_ENABLED=1 is a good idea, because if it is not set and go fails to find the compiler, go just excludes all files with import "C" from the build, resulting in lots of errors about missing types.

Running

go run cmd/sointu-track/main.go

Building an executable

go build -o sointu-track.exe cmd/sointu-track/main.go

On other platforms than Windows, replace -o sointu-track.exe with -o sointu-track.

If you want to use the x86 native virtual machine, add -tags=native to all the commands e.g.

go build -o sointu-track.exe -tags=native cmd/sointu-track/main.go

Sointu-vsti

This is the VST instrument plugin version of the tracker, compiled into a dynamically linked library and ran inside a VST host.

Prerequisites

  • go
  • cgo compatible compiler e.g. gcc. On windows, you best bet is MinGW. We use the tdm-gcc. The compiler can be in PATH or you can use the environment variable CC to help go find the compiler.
  • Setting environment variable CGO_ENABLED=1 is a good idea, because if it is not set and go fails to find the compiler, go just excludes all files with import "C" from the build, resulting in lots of errors about missing types.
  • If you want to use the faster x86 assembly written synthesizer:

Building

go build -buildmode=c-shared -tags=plugin -o sointu-vsti.dll .\cmd\sointu-vsti\

On other platforms than Windows, replace -o sointu-vsti.dll appropriately e.g. -o sointu-vsti.so; so far, the VST instrument has been built & tested on Windows and Linux.

Notice the -tags=plugin build tag definition. This is required by the vst2 library; otherwise, you will get a lot of build errors.

Add -tags=native,plugin to use the x86 native virtual machine instead of the virtual machine written in Go.

Sointu-compile

The command line interface to it is sointu-compile and the actual code resides in the compiler package, which is an ordinary go package with no other tool dependencies.

Running

go run cmd/sointu-compile/main.go

Building an executable

go build -o sointu-compile.exe cmd/sointu-compile/main.go

On other platforms than Windows, replace -o sointu-compile.exe with -o sointu-compile.

Usage

The compiler can then be used to compile a .yml song into .asm and .h files. For example:

sointu-compile -o . -arch=386 tests/test_chords.yml
nasm -f win32 test_chords.asm

WebAssembly example:

sointu-compile -o . -arch=wasm tests/test_chords.yml
wat2wasm --enable-bulk-memory test_chords.wat

If you are looking for an easy way to compile an executable from a Sointu song (e.g. for a executable music compo), take a look at NR4's Python-based tool for it.

Examples

The folder examples/code contains usage examples for Sointu with winmm and dsound playback under Windows and asound playback under Unix. Source code is available in C and x86 assembly (win32, elf32 and elf64 versions).

To build the examples, use ninja examples.

If you want to target smaller executable sizes, using a compressing linker like Crinkler on Windows is recommended.

The linux examples use ALSA and need libasound2-dev (or libasound2-dev:386) installed. The 386 version also needs pipewire-alsa:386 installed, which is not there by default.

Native virtual machine

The native bridge allows Go to call the sointu compiled x86 native virtual machine, through cgo, instead of using the Go written bytecode interpreter. It's likely slightly faster than the interpreter. Before you can actually run it, you need to build the bridge using CMake (thus, this will not work with go get).

Prerequisites

The last point is because the command line player and the tracker use cgo to interface with the synth core, which is compiled into a library. The cgo bridge resides in the package bridge.

Building

Assuming you are using ninja:

mkdir build
cd build
cmake .. -GNinja
ninja sointu

⚠️ you must build the library inside a directory called 'build' at the root of the project. This is because the path where cgo looks for the library is hard coded to point to build/ in the go files.

Running ninja sointu only builds the static library that Go needs. This is a lot faster than building all the CTests.

You and now run all the Go tests, even the ones that test the native bridge. From the project root folder, run:

go test ./...

Play a song from the command line:

go run -tags=native cmd/sointu-play/main.go tests/test_chords.yml

⚠️ Unlike the x86/amd64 VM compiled by Sointu, the Go written VM bytecode interpreter uses a software stack. Thus, unlike x87 FPU stack, it is not limited to 8 items. If you intent to compile the patch to x86/amd64 targets, make sure not to use too much stack. Keeping at most 5 signals in the stack is presumably fine (reserving 3 for the temporary variables of the opcodes). In future, the app should give warnings if the user is about to exceed the capabilities of a target platform.

⚠️ If you are using Yasm instead of Nasm, and you are using MinGW: Yasm 1.3.0 (currently still the latest stable release) and GNU linker do not play nicely along, trashing the BSS layout. The linker had placed our synth object overlapping with DLL call addresses; very funny stuff to debug. See here and the fix here. Since Nasm is nowadays under BSD license, there is absolutely no reason to use Yasm. However, if you do, use a newer nightly build of Yasm that includes the fix.

Tests

There are regression tests that are built as executables, testing that they work the same way when you would link them in an intro.

Prerequisites

  • go
  • CMake with CTest
  • nasm
  • Your favorite CMake compatible c-compiler & build tool. Results have been obtained using Visual Studio 2019, gcc&make on linux, MinGW&mingw32-make, and ninja&AppleClang.

Building and running

Assuming you are using ninja:

mkdir build
cd build
cmake .. -GNinja
ninja
ninja test

Note that this builds 64-bit binaries on 64-bit Windows. To build 32-bit binaries on 64-bit Windows, replace in above:

cmake .. -DCMAKE_C_FLAGS="-m32" -DCMAKE_ASM_NASM_OBJECT_FORMAT="win32" -GNinja

Another example: on Visual Studio 2019 Community, just open the folder, choose either Debug or Release and either x86 or x64 build, and hit build all.

WebAssembly tests

These are automatically invoked by CTest if node and wat2wasm are found in the path.

New features since fork

  • New units. For example: bit-crusher, gain, inverse gain, clip, modulate bpm (proper triplets!), compressor (can be used for side-chaining).
  • Compiler. Written in go. The input is a .yml file and the output is an .asm. It works by inputting the song data to the excellent go text/template package, effectively working as a preprocessor. This allows quite powerful combination: we can handcraft the assembly code to keep the entropy as low as possible, yet we can call arbitrary go functions as "macros". The templates are here and the compiler lives here.
  • Tracker. Written in go. Can run either as a stand-alone app or a vsti plugin.
  • Supports 32 and 64 bit builds. The 64-bit version is done with minimal changes to get it work, using template macros to change the lines between 32-bit and 64-bit modes. Mostly, it's as easy as writing {{.AX}} instead of eax; the macro {{.AX}} compiles to eax in 32-bit and rax in 64-bit.
  • Supports compiling into WebAssembly. This is a complete reimplementation of the core, written in WebAssembly text format (.wat).
  • Supports Windows, Linux and MacOS. On all three 64-bit platforms, all tests are passing. Additionally, all tests are passing on windows 32.
  • Per instrument polyphonism. An instrument has the possibility to have any number of voices, meaning that multiple voices can reuse the same opcodes. So, you can have a single instrument with three voices, and three tracks that use this instrument, to make chords. See here for an example and here for the implementation. The maximum total number of voices is 32: you can have 32 monophonic instruments or any combination of polyphonic instruments adding up to 32.
  • Any number of voices per track. A single track can trigger more than one voice. At every note, a new voice from the assigned voices is triggered and the previous released. Combined with the previous, you can have a single track trigger 3 voices and all these three voices use the same instrument, useful to do polyphonic arpeggios (see here). Not only that, a track can even trigger voices of different instruments, alternating between these two; maybe useful for example as an easy way to alternate between an open and a closed hihat.
  • Easily extensible. Instead of %ifdef hell, the primary extension mechanism is through new opcodes for the virtual machine. Only the opcodes actually used in a song are compiled into the virtual machine. The goal is to try to write the code so that if two similar opcodes are used, the common code in both is reused by moving it to a function. Macro and linker magic ensure that also helper functions are only compiled in if they are actually used.
  • Songs are YAML files. These markup files are simple data files, describing the tracks, patterns and patch structure (see here for an example). The sointu-compile then reads these files and compiles them into .asm code. This has the nice implication that, in future, there will be no need for a binary format to save patches, nor should you need to commit .o or .asm to repo: just put the .yml in the repo and automate the .yml -> .asm -> .o steps using sointu-compile & nasm.
  • Harmonized support for stereo signals. Every opcode supports a stereo variant: the stereo bit is hidden in the least significant bit of the command stream and passed in carry to the opcode. This has several nice advantages: 1) the opcodes that don't need any parameters do not need an entire byte in the value stream to define whether it is stereo; 2) stereo variants of opcodes can be implemented rather efficiently; in some cases, the extra cost of stereo variant is only 5 bytes (uncompressed). 3) Since stereo opcodes usually follow stereo opcodes (and mono opcodes follow mono opcodes), the stereo bits of the command bytes will be highly correlated and if crinkler or any other modeling compressor is doing its job, that should make them highly predictable i.e. highly compressable.
  • Test-driven development. Given that 4klang was already a mature project, the first thing actually implemented was a set of regression tests to avoid breaking everything beyond any hope of repair. Done, using go test (runs the .yml regression tests through the library) and CTest (compiles each .yml into executable and ensures that when run like this, the test case produces identical output). The tests are also ran in the cloud using github actions.
  • Arbitrary signal routing. SEND (used to be called FST in 4klang) opcode normally sends the signal as a modulation to another opcode. But with the new RECEIVE opcode, you just receive the plain signal there. So you can connect signals in an arbitrary way. Actually, 4klang could already do this but in a very awkward way: it had FLD (load value) opcode that could be modulated; FLD 0 with modulation basically achieved what RECEIVE does, except that RECEIVE can also handle stereo signals. Additionally, we have OUTAUX, AUX and IN opcodes, which route the signals through global main or aux ports, more closer to how 4klang does. But this time we have 8 mono ports / 4 stereo ports, so even this method of routing is unlikely to run out of ports in small intros.
  • Pattern length does not have to be a power of 2.
  • Sample-based oscillators, with samples imported from gm.dls. The gm.dls is available from system folder only on Windows, but the non-native tracker looks for it also in the current folder, so should you somehow magically get hold of gm.dls on Linux or Mac, you can drop it in the same folder with the tracker. See this example, and this go generate program parses the gm.dls file and dumps the sample offsets from it.
  • Unison oscillators. Multiple copies of the oscillator running slightly detuned and added up to together. Great for trance leads (supersaw). Unison of up to 4, or 8 if you make stereo unison oscillator and add up both left and right channels. See this example.
  • Compiling as a library. The API is very rudimentary, a single function render, and between calls, the user is responsible for manipulating the synth state in a similar way as the actual player does (e.g. triggering/ releasing voices etc.)
  • Calling Sointu as a library from Go language. The Go API is slighty more sane than the low-level library API, offering more Go-like experience.
  • A bytecode interpreter written in pure go. It's slightly slower than the hand-written assembly code by sointu compiler, but with this, the tracker is ultraportable and does not need cgo calls.

Future goals

  • Find a more general solution for skipping opcodes / early outs. It might be a new opcode "skip" that skips from the opcode to the next out in case the signal entering skip and the signal leaving out are both close to zero. Need to investigate the best way to implement this.
  • Even more opcodes. Some potentially useful additions could be:
    • Equalizer / more flexible filters
    • Very slow filters (~ DC-offset removal). Can be implemented using a single bit flag in the existing filter
    • Arbitrary envelopes; for easier automation.
  • MIDI support for the tracker.
  • Find a solution for denormalized signals. Denormalized floating point numbers (floating point numbers that are very very small) can result in 100x CPU slow down. We got hit by this already: the damp filters in delay units were denormalizing, resulting in the synth being unusable in real time. Need to investigate a) where denormalization can happen; b) how to prevent it: add & substract value; c) make this optional to the user. For quick explanation about the potential massive CPU hit, see https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch

Long-shot ideas

  • Hack deeper into audio sources from the OS. Speech synthesis, I'm eyeing at you.

Design philosophy

  • Make sure the assembly code is readable after compiling: it should have liberally comments in the outputted .asm file. This allows humans to study the outputted code and figure out more easily if there's still way to squeeze out instructions from the code.
  • Instead of prematurely adding %ifdef toggles to optimize away unused features, start with the most advanced featureset and see if you can implement it in a generalized way. For example, all the modulations are now added into the values when they are converted from integers, in a standardized way. This got rid of most of the %ifdefs in 4klang. Also, with no %ifdefs cluttering the view, many opportunities to shave away instructions became apparent. Also, by making the most advanced synth cheaply available to the scene, we promote better music in future 4ks :)
  • Size first, speed second. Speed will only considered if the situation becomes untolerable.
  • Benchmark optimizations. Compression results are sometimes slightly nonintuitive so alternative implementations should always be benchmarked e.g. by compiling and linking a real-world song with Leviathan and observing how the optimizations affect the byte size.

Background and history

4klang development was started in 2007 by Dominik Ries (gopher) and Paul Kraus (pOWL) of Alcatraz. The write-up will still be helpful for anyone looking to understand how 4klang and Sointu use the FPU stack to manipulate the signals. Since then, 4klang has been used in countless of scene productions and people use it even today.

However, 4klang seems not to be actively developed anymore and polyphonism was implemented only in a rather limited way (you could have exactly 2 voices per instrument if you enable it). Also, reading through the code, I spotted several avenues to squeeze away more bytes. These observations triggered project Sointu. That, and I just wanted to learn x86 assembly, and needed a real-world project to work on.

What's with the name

"Sointu" means a chord, in Finnish; a reference to the polyphonic capabilities of the synth. I assume we have all learned by now what "klang" means in German, so I thought it would fun to learn some Finnish for a change. And there's enough klangs already.

Prods using Sointu

Contributing

Pull requests / suggestions / issues welcome, through Github! Or just DM me on Discord (see contact information below).

License

Distributed under the MIT License. See LICENSE for more information.

Contact

Veikko Sariola - pestis_bc on Demoscene discord - [email protected]

Project Link: https://github.com/vsariola/sointu

Credits

The original 4klang: Dominik Ries (gopher/Alcatraz) & Paul Kraus (pOWL/Alcatraz) ❤️

Sointu: Veikko Sariola (pestis/bC!), Apollo/bC!, NR4/Team210, PoroCYon, kendfss, anticore

sointu's People

Contributors

anticore avatar gopher-atz avatar kebby avatar lestahl avatar moitias avatar petersalomonsen avatar vsariola avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

sointu's Issues

Unit send targets should be by ID

The internal data format should be changed: units should be optionally given a running ID and send targets should be identified by their ID. Trying to keep the send targets synced to all mutations currently is a pain and likely source of bugs.

IDs can be stripped for units that don't need them during save, to keep the overall file format clean.

The question of polyphonic sends remain. There are a few possible solutions:

  1. Still have optional "voice" parameter, which defines target voice (relative to the instrument). 0 = auto ("if this is local send, then target the same voice that is currently getting run, otherwise just voice 1"), 1 = voice 1, 2 = voice 2 etc.
  2. Try to use the polyphony bitmask to repeat sends during run time
  3. Try to pack the number of voices sent into lodsw data. Unlikely to work, as we already use all 16 bits (3 for ports, 1 for POP, 1 for global send, 6 for unit, 5 for voice)... LEB128 encoding might be future proof, but could theoretically make some send addresses 3 bytes long, while sometimes making them 1 byte (first bit must be 0, 3 bits for unit, 1 for POP, 3 for port... actually might work in some cases)... The LEB128 decoding in the beginning of send might result in further juggling for the carry flag.
  4. Multisends, with completely new address for every send, and one bit snipped from unit number to indicate if there's a new address coming. If so, repeat the entire send, starting from lodsw.

after adding instrument data, pressing cursor-right crashed

panic: runtime error: index out of range [1] with length 1

goroutine 34 [running]:
github.com/vsariola/sointu/tracker.(*Tracker).GetUnitParam(...)
/Users/esaruoho/work/sointu/tracker/tracker.go:555
github.com/vsariola/sointu/tracker.(*Tracker).KeyEvent(0xc00009c800, 0xc00009c000, 0x43be02b, 0x3, 0x0, 0x4000000040000000)
/Users/esaruoho/work/sointu/tracker/keyevent.go:335 +0x227c
github.com/vsariola/sointu/tracker.(*Tracker).Run(0xc00009c800, 0xc00009c000, 0x444f540, 0x4b69ea0)
/Users/esaruoho/work/sointu/tracker/run.go:23 +0x3ba
main.main.func1(0xc00000e8c0)
/Users/esaruoho/work/sointu/cmd/sointu-track/main.go:29 +0x136
created by main.main
/Users/esaruoho/work/sointu/cmd/sointu-track/main.go:22 +0xe5
exit status 2

Improve NaN handling / playback death

Currently, any single NaN in the signal path usually leads to complete audio death, due to the presence of global reverb: NaNs get stuck in the delaylines, causing all future output to be NaNs.

For efficiency reasons, we probably cannot handle NaN checks everywhere in the playback code, but at least we could:

  1. Give proper error message and panic when there is NaN, so the user at least knows the reason
  2. Look into the most common culprits (waveshaper, invgain) and see if we at least add some optional protections there

3 osc = mac stops playing and never resumes until restarting app

Steps to replicate:
on a M1 Mac, I add Oscs until I have 3 Oscs in the song.

Expected result: 3 Oscs play, or anything plays
Current result: 3 Oscs? Bye, nothing plays. Not even after pressing panic. Not even after removing the 2 Oscs and pressing panic. Just gone. Copy Song, Paste Song to new sointu-session, if 2 oscs = they play, if more than 2, silence.

Edit_Step 1/0 setting

While I like that Sointu can be in "always stay in same lane", I'd prefer there to be a possibility to set edit_step to 1

Adding Voices in tracker does not add more "channels"

Steps to replicate:

  1. Launch app
  2. Click on -> on Voices to go from 1 to 5
  3. Click on <- on Voices to go from 5 to 1

Expected: More tracks added to tracker view when going from 1...5, track count reduced in tracker when 5...1
Current: Nothing happens
2021-02-15 11 52 19

need a visual saying which instrument is in use on which track

hi, i'd like to be shown which instrumentname is in use on which track. so far i'm working in the blind.

example, i had 4 tracks.. then i had selected track3, pressed + twice, then suddenly the track4 sound has changed to something completely different (=destructive) and i have no way of removing tracks to get track4 to sound like it should.

autoselect "---" after pressing + in instrument details

Steps to replicate:

  1. Click on (+) to add new element to instrument details

Expected: "---" is automatically selected
Current: "---" needs to be selected by hand

Suggestion: Select "---" when creating a new element to instrument details
2021-02-15 12 17 41

Make "Backspace" & "Delete" do the same thing

On Mac, I need to press FN-Backspace to be able to delete. I'd prefer if Backspace also did the same thing as deleted.
i.e. "just delete what's on the row the cursor is on". no "move notes below the row up one step" needed at all.

add scrollbars to Inst

Inst could have scrollbars, so can figure out if there's extra stuff there below what's displayed on the screen or not

Small TODOs

  • Fix stereo and unison oscillators screwing up $WRK
  • Test envelope gain parameter also
  • Do not hard code addresses in WebAssembly
  • Write test errors to STDERR

Ability to edit delay times

The GUI for adjusting delay times is not implemented. Delay is such a crucial unit that this should be pretty high priority, to actually use the delay for useful stuff (delay, reverb).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.