492 Commits

Author SHA1 Message Date
c4d0683be7 docs: update Changelog for v0.6.0 2026-03-12 08:10:27 +02:00
e57e55fde6 ci: add sointu-play binary builds to GitHub actions (#229)
Closes #226.
2026-03-12 07:59:59 +02:00
c793d29592 feat(examples): add example songs from intros by epoqe, Team210 and farbrausch
This is the first step to collect a corpus of real intro songs to test various size optimizations.

Related to #227
2026-03-08 19:04:24 +02:00
c52c074aa1 feat(tracker): show * next to the file path to indicate unsaved changes
Related to #224.
2026-03-06 15:01:30 +02:00
4cb9308af3 feat: save only the units and comment to instrument/preset files 2026-02-28 20:45:28 +02:00
f3bb0001cd fix(tracker): show FPU over/underflow error instead of generic nan error 2026-02-28 19:20:04 +02:00
4d29a191c8 fix(vm): avoid NaNs in trisaw oscillator 2026-02-28 16:55:16 +02:00
92859a5e58 test: fix the prerequirements of the gainmod tests 2026-02-28 16:55:01 +02:00
558ca05236 feat(tracker/presets): new and modified presets 2026-02-28 16:53:12 +02:00
bf29421246 doc: improve comments 2026-02-16 13:17:02 +02:00
a994d831ee doc: update README.md 2026-02-16 12:21:23 +02:00
b8d9ca09f1 fix(cmd/sointu-vsti): query sample rate through host.GetTimeInfo
Closes #222
2026-02-15 12:02:39 +02:00
f2f76c0e18 feat(tracker/gioui): show scroll bar in the instrument properties 2026-02-14 20:57:19 +02:00
cd4b85a66b fix(tracker): keep instrument properties when loading a preset
Also when loading an instrument from the disk. We only load units,
instrument name and comment. MIDI and the number of voices are kept.
2026-02-14 20:49:30 +02:00
942da94982 fix(tracker): reset Player.prevVal when new recording is started 2026-02-14 20:28:16 +02:00
e66ff8be9f feat(cmd): recovery files moved to [...]/sointu/recovery/ 2026-02-14 19:42:25 +02:00
b349474c4d feat: MIDI velocity, keyboard splits, and fixing instrument channel
Closes #124
Closes #215
Closes #221
2026-02-14 15:22:30 +02:00
0179b24fd4 refactor(tracker): remove ControlChange event struct as unneeded 2026-02-03 21:27:49 +02:00
77b27257fe fix(tracker): Player routes MIDImsgs so always handled in same block 2026-02-03 21:20:01 +02:00
cc8d737f8a change(tracker): midi controller value 64 maps to 64 in Sointu side 2026-02-01 13:02:42 +02:00
f2ef57a845 feat(tracker): ability to bind MIDI controllers to parameters
Closes #152
2026-02-01 12:07:00 +02:00
6e8acc8f9b feat(tracker): plot envelope shape in scope when envelope selected 2026-01-31 20:54:19 +02:00
287bd036a6 feat(tracker): multithreading is enabled with a separate bool toggle 2026-01-31 20:50:00 +02:00
4bb5df9c87 feat(tracker): enum-style values and menus to choose one option 2026-01-31 13:57:09 +02:00
ca4b87d43d fix(vm): nans in the Go VM trisaw oscillator when the color was 0 2026-01-27 23:10:53 +02:00
86ca3fb300 refactor(tracker): group Model methods, with each group in one source file 2026-01-27 22:16:14 +02:00
b93304adab refactor(tracker) make StringValue implementations private 2026-01-24 00:22:21 +02:00
173648fbdb refactor(tracker): use strings to identify MIDI ports 2026-01-23 23:48:16 +02:00
651ceb3cbb refactor(tracker): make Doer implementations private 2026-01-23 23:02:45 +02:00
1693d7ed5e refactor(tracker): make Model methods return List, avoiding .List() 2026-01-23 22:42:25 +02:00
74beb6760c refactor(tracker): remove unused DoFunc type 2026-01-21 20:50:35 +02:00
6629a9fdfa refactor(tracker): add MakeBoolFromPtr constructor for Bool 2026-01-21 20:45:03 +02:00
60222dded4 refactor(tracker): Enabler is optionally implemented when needed 2026-01-21 19:53:24 +02:00
810998d95b build: upgrade oto, which now uses WASAPI's better resampler 2026-01-19 19:36:10 +02:00
3a7010f897 feat(tracker): spectrum analyzer
Closes #67
2026-01-18 17:09:57 +02:00
4d09e04a49 feat: implement bell filter unit for equalizing 2025-12-29 16:33:00 +02:00
33ee80a908 ci: remove native synth from MacOS and compile arm64 2025-12-29 13:08:12 +02:00
9b87589f7b docs(examples/code/C): add comment showing how to loop the song
Closes #216
2025-11-12 13:44:49 +02:00
16c652b2ba refactor(tracker): clear RailError alert with ClearNamed 2025-11-02 16:51:26 +02:00
dafd45fd81 feat(tracker): allow instrument have no thread, but warn about it 2025-11-02 16:42:30 +02:00
05b64dadc8 fix(tracker/gioui): unit comment editor flashing while cursor moved 2025-11-02 16:20:30 +02:00
6978dd4afe refactor(tracker/gioui): remove unused function parameters 2025-11-02 16:18:24 +02:00
fa9654d311 refactor(tracker/gioui): Surface is given relative Height, not Gray 2025-11-02 15:52:51 +02:00
3495d91a4a refactor(tracker/gioui): remove addUnitAction from InstrumentEditor 2025-11-02 15:51:23 +02:00
628365c486 refactor(tracker): use pointers to bools for simple booleans 2025-11-02 13:09:25 +02:00
9db6b669c9 refactor(tracker/gioui): clean up ScrollTableStyle layouts 2025-11-02 12:05:38 +02:00
a37990a7fa feat(tracker): make clear unit action clear all selected units 2025-11-01 01:06:34 +02:00
4a46d601f2 feat(tracker): decrease the display duration of RailErrors 2025-11-01 00:50:41 +02:00
da6226d3ff feat(tracker/gioui): unit comment in the rack is editable 2025-11-01 00:37:25 +02:00
1dbe351beb fix(tracker/gioui): crash when recovered synth was panicced 2025-11-01 00:29:27 +02:00
91c9701f14 refactor(tracker/gioui): combine UnitList & UnitEditor structs 2025-10-31 21:57:17 +02:00
48dc4a35bb feat(tracker/presets): rename two presets to harmonize naming 2025-10-31 20:11:23 +02:00
9b9dc3548f feat: add multithreaded rendering to the tracker side
The compiled player does not support multithreading, but with this,
users can already start composing songs with slightly less powerful
machines, even when targeting high-end machines.

Related to #199
2025-10-31 19:40:02 +02:00
c583156d1b style(tracker/presets): set the voices of all presets to 1
The voices are anyway set during presets loading, so this value is
not honored.
2025-10-26 19:18:46 +02:00
c1ea47a509 feat(tracker/presets): improving and adding presets 2025-10-26 19:13:19 +02:00
60ae8645b6 fix: tracker thought sync pops a value even if it didn't 2025-10-25 12:25:31 +03:00
362bc3029f feat(tracker/presets): add Noise sweep up & down presets 2025-10-23 15:27:54 +03:00
213202a7e0 feat(tracker/presets): add a modified version of Piano Whitespace 2025-10-19 20:00:36 +03:00
e08af03fb2 feat(tracker/presets): Sine bells and Chaos bass presets from Reaby 2025-10-19 19:34:53 +03:00
8e99c93d14 refactor: use yaml.v3 everywhere and remove dependency on yaml.v2 2025-10-19 17:13:00 +03:00
f4bb2bc754 feat(tracker/presets): add piano from noby's Whitespace intro 2025-10-19 17:02:03 +03:00
de366316d4 fix(tracker/gioui): short action key pressed without handler crash 2025-10-19 16:22:07 +03:00
7a43aec50e fix(tracker/presets): avoid NaNs from color:0 in Alpha_omega preset
The color was just flipped i.e. by using color:128.
2025-10-19 13:21:18 +03:00
82cf34a28f docs: update CHANGELOG.md 2025-10-19 12:07:28 +03:00
2336a135c6 feat(tracker): add a preset explorer with search and filters
Closes #91
2025-10-19 12:04:41 +03:00
3f365707c2 build: update gioui to latest 2025-10-15 10:04:24 +03:00
34c0045652 docs: update CHANGELOG.md 2025-10-14 22:26:31 +03:00
c64422767e fix(tracker/gioui): show minimized loudness in red when > 0 dB 2025-10-14 22:23:16 +03:00
1dcd3fe3c6 refactor(tracker/presets): clean name-fields from presets
The file name will be used as their name anyways.
2025-10-14 17:14:59 +03:00
c0488226d2 fix(tracker/presets): avoid NaNs (pulse instead of trisaw&shape:128) 2025-10-14 16:58:52 +03:00
f894e2ee86 feat(tracker/presets): remove dbgain units from presets 2025-10-14 16:29:10 +03:00
54a8358522 feat(tracker/presets): rework presets & normalize to approx. -12 dBFS true peak 2025-10-14 16:06:05 +03:00
bdfe2d37bf feat(tracker): panic synth if Inf or NaN, and handle these in detectors
Closes #210.
2025-10-08 08:53:46 +03:00
167f541a52 docs: update CHANGELOG.md 2025-10-06 20:49:20 +03:00
be48f5824f docs: update CHANGELOG.md for v0.5.0 2025-10-05 14:12:47 +03:00
989b6e605b feat(tracker/presets): improved and new presets from Reaby 2025-10-05 14:06:00 +03:00
7459437822 feat(tracker): don't save instrument name in instrument files
The filename is used as the instrument name when it is loaded.
2025-10-05 14:06:00 +03:00
55f9c36bd5 feat(tracker/gioui): show file explorer error messages to user 2025-09-27 21:06:26 +03:00
a09b52a912 feat: phase parameter hint is displayed in degrees (0 .. 360) 2025-09-17 20:53:51 +03:00
74fea4138f docs: Update README.md (-o . is not needed; it's the default behavior) 2025-08-13 15:18:39 +03:00
f13a5cd2df docs: Update README.md (#208)
Now it seems bulk memory is enabled by default in wat2wasm

See: https://github.com/WebAssembly/wabt/pull/1728
2025-08-13 07:31:23 +03:00
7f3010a4a6 docs: update CHANGELOG.md 2025-08-08 15:16:50 +03:00
5839471bcc fix(tracker/gioui): limit comments in unit list to single line 2025-07-30 18:10:15 +03:00
fe0106bb60 fix(tracker/gioui): show unit search list after clicking "Add Unit"
Closes #204
2025-07-24 11:03:34 +03:00
3163f46447 feat!: both native & Go synths are included in the same executables
Closes #200
2025-07-10 17:46:00 +03:00
13102aa7d6 docs: update README.md 2025-07-10 10:34:55 +03:00
399bac481c docs: update screenshot 2025-07-10 09:07:40 +03:00
072e4ee208 fix(tracker/gioui): knobs/switches capture scrollwheel only active 2025-07-09 01:37:18 +03:00
edc0782f5f feat: show resonance Q-factor as resonance peak height in dB 2025-07-09 01:17:47 +03:00
697fb05b5c feat: display various gain parameter values in decibels 2025-07-09 00:47:09 +03:00
cf86f3f1c8 feat(tracker/gioui): make knob/switch/port hit box the entire cell 2025-07-08 23:53:03 +03:00
8e5f3098a4 feat(tracker/gioui): switches just clickable & cycle between states 2025-07-08 23:01:55 +03:00
452a4cf04f feat(tracker/gioui): ctrl+drag changes knob value faster
Slow down the normal dragging 4 x slower.
2025-07-08 22:33:19 +03:00
5841848813 feat(tracker): reduce speed of parameter changes when ctrl pressed 2025-07-08 22:26:30 +03:00
0ce79978d5 feat(tracker/gioui): right click resets knobs instead of doubleclick 2025-07-08 22:22:01 +03:00
4138c34574 fix(tracker): make signal stack errors show for much longer time 2025-07-08 22:21:17 +03:00
172fbaeb2a feat(tracker/gioui): make switches left&right clickable when active 2025-07-08 22:10:54 +03:00
666af9433e feat!: display the parameters as knobs in a grid
Also removed the negbandpass & neghighpass parameters
and replaced them with bandpass & highpass set to -1, to
fit the switches better to the GUI.

Closes #51, closes #173
2025-07-08 19:47:32 +03:00
c3caa8de11 fix(tracker/gioui): backspace / delete reset param value 2025-06-26 09:04:05 +03:00
18d7848367 fix(tracker/gioui): using keys to choose Unit Type and tab ordering 2025-06-26 00:33:18 +03:00
192909328c fix(tracker/gioui): try to ensure that tooltip are never left behind
Closes #141
2025-06-25 19:13:52 +03:00
cb4c020061 style(tracker/gioui): rename songpanel.go to song_panel.go 2025-06-25 19:05:36 +03:00
d78ef98e73 refactor(tracker/gioui): upgrade gio & store Tracker to gtx.Values 2025-06-25 18:54:00 +03:00
08c36ed462 feat(tracker/gioui): new tab order logic and refactor instrument editor 2025-06-25 16:32:56 +03:00
d276f52942 docs: update README.md 2025-06-25 13:44:58 +03:00
b8cf70e8e9 refactor(tracker/gioui): use min(max(... instead of ifs 2025-06-24 20:43:48 +03:00
e59fbb50cf refactor(tracker/gioui): separate SplitStyle from SplitState 2025-06-24 20:39:27 +03:00
ba281ca7c0 fix(tracker/gioui): cancel dialog when user clicks outside it 2025-06-24 20:26:14 +03:00
b4ec136ab1 refactor(tracker/gioui): Popup in same style as other widgets 2025-06-24 20:25:52 +03:00
18d198d764 refactor(tracker/gioui): bind Alerts to Model during Layout 2025-06-24 19:59:31 +03:00
355ccefb6f refactor(tracker/gioui): refactor Scope in same style as others 2025-06-24 19:28:53 +03:00
7a030683c6 refactor(tracker/gioui): use precreated hex strings in OrderEditor 2025-06-24 19:10:29 +03:00
17ca15b205 refactor(tracker/gioui): minor optimizations in NoteEditor 2025-06-24 19:04:47 +03:00
58f6cceb9a refactor(tracker/gioui): Menu binds to Model during Layout 2025-06-24 18:39:40 +03:00
b79de95f91 refactor(tracker/gioui): remove unnecessary caching of Strings 2025-06-24 11:07:42 +03:00
f6bc5fffcd docs: update LICENSE to include reaby, for updating the presets 2025-06-24 10:41:57 +03:00
33f7b5fb6a refactor(tracker/gioui): Dialog binds to Model during Layout 2025-06-24 10:15:46 +03:00
5f43bc3067 feat(tracker/gioui): "Ask Help", "Report Bug" and "Manual" menuitems 2025-06-23 19:17:00 +03:00
fb0fa4af92 feat: embed license in executable and add menu item to show it 2025-06-23 18:45:13 +03:00
6f1db6b392 fix(tracker/gioui): make own TipArea ensuring tips don't stay around
Closes #141.
2025-06-23 18:02:05 +03:00
31007515b5 refactor(tracker/gioui): avoid heap escapes in NumericUpDown 2025-06-23 09:43:10 +03:00
db2ccf977d refactor(tracker/gioui): rewrote Button(s) to bind to Model during layout
The old mechanism made it difficult to follow exactly what happens
when a button was clicked, because the Action/Bool that gets
executed / toggled was declared ages ago, in the constructor. In the
new mechanism, the Action / Bool is bound to the button at the last
minute, right before Layout. ActionButton, ToggleButton,
ActionIconButton and ToggleIconButton were done to avoid heap
escapes: if the corresponding functions woudl've returned
layout.Widget, a heap allocation would've been needed.
2025-06-23 08:56:37 +03:00
0ea20ea5bf refactor(tracker/gioui): use enums (iota) for EditorEvent 2025-06-21 12:04:08 +03:00
beef8fe1e0 refactor(tracker/gioui): bind tracker.Int to NumericUpDown on Layout 2025-06-21 11:45:31 +03:00
289bfb0605 refactor: fix all unused parameter / variable warnings 2025-06-21 10:33:08 +03:00
a601b98b74 ci: switch to using clang++ on Mac for rtmidi and add missing libs 2025-06-21 09:57:58 +03:00
602b3b05cc feat(tracker): compile with midi support only when CGO is available
Also add the midi context to the VSTI, so VSTI can use MIDI if they
wish so.
2025-06-20 19:38:06 +03:00
3881b8eb22 fix(tracker/gioui): if user clears unit search box, set unit to "" 2025-06-20 19:10:10 +03:00
4fa0e04788 refactor(tracker/gioui): make iconCache part of Theme 2025-06-20 19:05:40 +03:00
b291959a97 refactor(tracker/gioui): rewrote Editor to link to String.Value() 2025-06-20 18:50:44 +03:00
840fe3ef0e refactor(tracker): remove SetCursorFloat method from TableData 2025-06-20 15:17:21 +03:00
430b01d143 refactor(tracker): remove unused code and improve style 2025-06-20 15:05:22 +03:00
28a0006b6a docs(tracker): improve comments and formatting 2025-06-20 14:57:12 +03:00
8eb5f17f73 style(tracker): oneline functions and remove spurious comments 2025-06-20 14:52:46 +03:00
f47bee37b0 style(tracker): clean up Alerts Push and Pop 2025-06-20 14:49:07 +03:00
4f2c73d0db refactor(tracker): Player sends PlayerStatus to the Model 2025-06-19 11:37:11 +03:00
c77d541dc6 docs: update README.md; claim that native synth is faster was false 2025-06-17 18:22:14 +03:00
340620ed49 feat(tracker): show CPU load percentage in the song panel 2025-06-17 17:59:54 +03:00
1a13fadd75 docs: add link to Discussions to the README.md 2025-06-17 14:12:23 +03:00
b6e8ab5c25 style(examples/code/C): rename .unix. examples to .linux.
Closes #191
2025-06-16 18:55:01 +03:00
c6b70560f6 fix(tracker): update derived data after undo/redo 2025-06-16 18:44:33 +03:00
1eea263dc9 fix(tracker/gioui): show muted instruments in different style 2025-06-16 18:35:06 +03:00
c023dc08b8 fix(tracker/gioui): BPM tooltip showed "Song length", not BPM 2025-06-11 19:43:05 +03:00
0e32608872 refactor(tracker): remove unnecessary Enabled function 2025-06-11 19:41:58 +03:00
283fbc1171 feat(tracker): rework the MIDI input and note event handling 2025-06-11 19:14:11 +03:00
7ef868a434 refactor(tracker): rewrite params to avoid heap allocations 2025-06-11 19:14:11 +03:00
4f779edb88 perf(tracker/gioui): avoid heap escapes in the menubar 2025-06-11 19:14:11 +03:00
d20a23d57b refactor(tracker/gioui): move element etc. functions away from style
Now the element / fg / bg functions are passed to the actual Layout
function, not first put to the style. This avoids moving of the
element function to heap.
2025-06-11 19:14:11 +03:00
de2e64533d refactor(tracker): refactor StringData to StringValue 2025-06-11 19:14:11 +03:00
74f37318d6 refactor(tracker): refactor IntData to IntValue, following Bool example 2025-06-11 19:14:11 +03:00
fb3a0da3ed refactor(tracker): make Bool have separate BoolValue and Enabler 2025-06-11 19:14:11 +03:00
036cb1f34d refactor(tracker): Make Action have separate Doer and Enabler 2025-06-11 19:14:11 +03:00
d6badb97be feat(examples): added playback timestamp extraction to ALSA example. (#190) 2025-06-04 16:44:31 +03:00
d342c9961d build: update gioui to latest version for performance optimization 2025-05-27 12:37:22 +03:00
32f1e1baea refactor(tracker/gioui): unify default & user config yaml handling 2025-05-23 23:35:51 +03:00
5b260d19f5 refactor(tracker/gioui): rename keyevent.go to keybindings.go
This way all the .go files that embed .yml files have matching
file names.
2025-05-23 21:46:34 +03:00
ddbaf6a4bb refactor(tracker): use UnmarshalStrict when decoding embedded yamls
Since we have 100% control over what data gets embedded, there is no
reason to embed anything that doesn't pass the strict yaml parsing
and it's better we throw a panic right away so it's easy to catch
this during development.
2025-05-23 21:44:23 +03:00
27bf8220c0 ci: use go version < 1.23.9 due to duplicate dlopen link error on mac
https://github.com/golang/go/issues/73617
2025-05-21 15:56:43 +03:00
448bc9f236 fix(tracker): OOB checks used index > len, but should've used >= 2025-05-20 19:05:17 +03:00
afb1fee4ed feat(tracker/gioui): add theme.yml which contains all styling 2025-05-20 19:02:16 +03:00
8245fbda24 feat(track/gioui): ctrl + scrollwheel adjusts global GUI zoom
Closes #153
2025-05-01 19:16:39 +03:00
0f42a993dc feat(tracker/gioui): oscilloscope allows y-scaling and shows limits
Closes #61
2025-05-01 12:05:32 +03:00
554a840982 refactor(tracker): new closing mechanism logic 2025-05-01 10:20:41 +03:00
9f89c37956 refactor(tracker): rename trySend to TrySend to make it public 2025-04-30 22:00:34 +03:00
0199658025 style(tracker): use for range loops everywhere in detector.go 2025-04-30 16:24:48 +03:00
afc6b1f4a9 build: update gioui to v0.8.0 2025-04-30 16:22:13 +03:00
3623bdf5b2 refactor(tracker): bake 1 kHz gain offset into filter coeffs 2025-04-29 20:45:14 +03:00
fe9daf7988 fix(tracker): loudness A- and C-weighting did not have proper scale 2025-04-29 15:12:57 +03:00
bf0d697b80 fix(tracker): reset also biquad filter states to avoid endless nans 2025-04-28 15:23:00 +03:00
f72f29188b feat(vm/compiler): increase native synth delaylines to 128
Closes #155
2025-04-27 21:47:27 +03:00
5fd78d8362 feat(tracker): buttons for loudness weighting and peak oversampling
Closes #186
2025-04-27 21:30:10 +03:00
805b98524c fix(tracker/gioui): use Clickables instead of widget.Clickables 2025-04-27 20:24:40 +03:00
54176cc2b3 refactor(tracker/gioui): separate MenuBar from SongPanel 2025-04-27 20:16:35 +03:00
845f0119c8 fix(tracker): peak amplitude dBs should be 20*log10, not 10*log10 2025-04-27 19:55:25 +03:00
5a3c859a51 fix(tracker): also peak detector windows were in wrong order 2025-04-27 19:29:23 +03:00
5c0b86a0f0 fix(tracker): the peak detector result was in wrong layout 2025-04-27 14:13:31 +03:00
e0392323c0 feat(tracker/gioui): add expander panel showing peaks 2025-04-27 14:13:31 +03:00
bb605ffa0b feat(tracker/gioui): add expanders into song panel 2025-04-27 13:03:34 +03:00
40be82de46 feat(tracker/gioui): refactor & rework playbar with the play buttons 2025-04-27 11:34:00 +03:00
42c95ab8ee feat(tracker/gioui): rework the labels of numeric updowns 2025-04-27 09:07:46 +03:00
d0413e0a13 feat(tracker/gioui): rewrite the numeric updown, with new appearance 2025-04-27 09:00:13 +03:00
bdf9e2ba0c feat(tracker/gioui): UI splitter bars snap better to window edges 2025-04-26 01:48:42 +03:00
95af8da939 fix(vm)!: first modulate delay time, then notetracking
BREAKING CHANGE: the order of these operations was inconsistent
across the different VMs. Go VM was the only one to first modulate
and then apply note tracking multiplication. But that made most
sense. So now all different VM versions work in this same way.
2025-04-16 23:17:08 +03:00
78fc6302a0 fix(tracker/gomidi): static cgo linking to avoid DLL dependencies
The linker flags -static -static-libgcc -static-libstdc++ tell mingw
to link statically; otherwise gcc_s_seh-1, stdc++-6 and winpthread-1
are needed.

Fixes #188.
2025-02-28 15:15:17 +02:00
ea4dee9285 docs: add reaby to contributors 2025-02-25 18:57:32 +02:00
ae217665bf feat(tracker/presets): new and tweaked presets from Reaby
The presets are also organized by their type into subfolders.

Closes #136
2025-02-25 18:53:28 +02:00
46a9c7dab3 feat(tracker): preset names include their directories 2025-01-25 22:52:11 +02:00
5ee7e44ed7 fix(tracker): ReadInstrument forgot to close the file 2025-01-25 22:18:28 +02:00
dd7b5ddc84 build: use macos-13 runners, as macos-12 is deprecated 2024-12-07 14:19:50 +02:00
ee229d8d94 build: build vst bundle binaries on macos
Closes #167
2024-12-07 14:13:28 +02:00
6ba595e7ff fix(vm/compiler): produce position independent code on amd64 2024-12-07 14:13:28 +02:00
7ff3c942cb feat(tracker/gioui): preferences.yml for window size or maximized (#185) 2024-12-07 13:54:08 +02:00
4169356845 docs: update CHANGELOG.md 2024-11-15 19:56:18 +02:00
8d71cf3ca7 fix(tracker): MakeSetLength did not handle invalid parameters
(cherry picked from commit 1b824f77ab40dbabba4586de4b97bb113e8ee264)
2024-11-10 00:10:17 +02:00
b255a68ebc fix: changes after review (see PR #176) 2024-11-10 00:02:13 +02:00
d517576a65 feat: introduce "cache" for derived model information 2024-11-10 00:02:13 +02:00
4d7c998fc2 doc: changelog 2024-11-10 00:02:13 +02:00
55c062a390 feat: highlight sliders that are controlled by a send, and add tooltip (over value) 2024-11-10 00:02:13 +02:00
b423d04c17 feat: separate unit type from comment (now in quotes) in target dropdowns 2024-11-10 00:02:13 +02:00
639b2266e3 feat: focus search editor after "add unit" 2024-11-10 00:02:13 +02:00
8d7d896375 docs: update README.md 2024-11-08 10:49:17 +02:00
04deac5722 fix(tracker): use non-blocking sends from Model to Player
This ensures that the GUI can never hang, even if the Player has
completely crashed.
2024-11-03 00:57:05 +02:00
6337101985 feat(tracker/gioui): remove maximum length from unit comment
Related to #115.
2024-11-03 00:05:57 +02:00
8074fd71d3 refactor(tracker): split NewModelPlayer into NewModel, NewPlayer 2024-11-02 23:58:38 +02:00
37769fcc9c refactor(tracker): get rid of execChan, use broker.ToModel instead 2024-11-02 23:44:52 +02:00
76322bb541 fix(tracker): the scope length is in beats, not in rows
Already the oscilloscope calculated its length in beats, but
everywhere the variable was called "lengthInRows." Renamed the
variable to lengthInBeats and also changed the tooltip to be correct
2024-11-02 23:13:48 +02:00
1c601858ae docs: update screenshot 2024-11-02 23:02:02 +02:00
65a7f060ec feat(tracker/gioui): make buttons never have focus
The exception to the rule is the dialog buttons (which use still
the default material buttons), because when there is a modal dialog
on screen, there is not much else the user would want to do.

Fixes #156
2024-11-02 22:57:09 +02:00
b08f5d4b1e fix: make the buttons non-responsive to the spacebar 2024-11-02 22:14:50 +02:00
2a2934b4e4 docs: update CHANGELOG.md 2024-11-02 22:12:06 +02:00
9d59cfb3b6 fix(tracker): unmarshal always into fresh, empty structs
A somewhat gotcha: when unmarshaling into &m.d with json.Unmarshal
or yaml.Unmarshal, maps were "merged" with the existing maps, which
is how we ended up with send units with color parameters, among
other things. This fix always unmarshals into fresh var data
modelData and only then sets m.d = data if the unmarshaling was
succesful.
2024-11-02 22:01:12 +02:00
19661f90ea feat(tracker/gioui): move panic button to the right of MIDI menu 2024-11-02 21:22:40 +02:00
94058c2603 fix(tracker): do not close Broker but rather just close the detector 2024-11-02 20:45:10 +02:00
943073d0cc perf: do not use TotalVoices as it causes heap allocations 2024-11-02 20:44:45 +02:00
b73fc0b95b refactor(tracker): use the Broker to communicate when exporting wav 2024-11-02 20:08:48 +02:00
ee3ab3bf86 feat(tracker): try to honor MIDI message timestamps 2024-11-02 19:55:40 +02:00
2aa0aaee0c refactor: AudioSource is a func instead of single function interface
This avoids defining Processor altogether.
2024-11-02 19:50:20 +02:00
3eb4d86d52 style(tracker/gioui): remove old commented code 2024-11-02 17:00:56 +02:00
ec222bd67d feat(tracker): oscilloscope and LUFS / true peak detection
In addition to the oscilloscope and loudness/peak detections, this
commit refactors all the channels between components (i.e.
ModelMessages and PlayerMessages) etc. into a new class Broker. This
was done because now we have one more goroutine running: a Detector,
where the loudness / true peak detection is done in another thread.
The different threads/components are only aware of the Broker and
communicate through it. Currently, it's just a collection of
channels, so it's many-to-one communication, but in the future,
we could change Broker to have many-to-one-to-many communication.

Related to #61
2024-11-02 15:08:09 +02:00
86c65939bb docs: Update CHANGELOG.md 2024-11-02 15:01:30 +02:00
7417170a8b docs: update README.md 2024-10-28 21:01:18 +02:00
daf7fb1519 feat(tracker/gioui): user-defined keybindings.yml override defaults
The behaviour of the user-defined keybindings.yml was changed.
It now documents just the changes from the default keybindings.
An empty line with no action means "unbind the key".
2024-10-28 10:04:13 +02:00
eb9413b9a0 fix: sointu-play should use cmd.MainSynther (#174)
This way you can choose if you want to use the native synth or the 
go synth by defining the build tag native.
2024-10-25 11:03:08 +03:00
8dfadacafe feat: midi note input for the tracker 2024-10-22 07:56:36 +03:00
216cde2365 feat: keeping instruments and tracks linked & splitting them
Also includes a refactoring of the List: all methods that accepted
or returned a [from, to] range now return a Range, which is
non-inclusive i.e. [start,end).

Also the assignUnitIds was slightly refactored & a new function
called assignUnitIdsForPatch was added, to assign all unit IDs for
an patch at once.

Closes #157, #163.
2024-10-20 12:23:25 +03:00
025f8832d9 fix(tracker): adding order row moved cursor incorrectly 2024-10-19 00:00:29 +03:00
1c42a51cc6 refactor(tracker): use built-in min & max instead of intMin & intMax 2024-10-18 23:43:27 +03:00
0ba6557f65 fix(tracker/presets): kick-adam.yml had some invalid parameters 2024-10-18 23:40:21 +03:00
3306c431c3 refactor(tracker): use List.DeleteElements to delete tracks/instrs 2024-10-16 13:44:50 +03:00
9bce1cb3d5 build: change go version in go.mod to fix manual linux builds. (#169) 2024-10-16 11:31:12 +03:00
63c08d53fe feat(tracker): solo and mute can toggle multiple instruments 2024-10-16 01:09:28 +03:00
063b2c29c5 feat: add mute and solo toggles for instruments
Closes #168
2024-10-16 00:44:34 +03:00
7b213bd8b0 feat(sointu): display compressor invgain and threshold in dB 2024-10-15 23:27:58 +03:00
27b6bc57d2 doc: update CHANGELOG.md 2024-10-15 14:49:47 +03:00
00b8e1872a feat(tracker/gioui): using mouse to select rectangles in tables 2024-10-15 13:24:14 +03:00
04ca0a3f6e fix(tracker/gioui): changing a hex played the previous value 2024-10-15 13:08:02 +03:00
08386323ed fix(tracker/gioui): all key filters for hexadecimals in note editor 2024-10-15 12:56:10 +03:00
7470413ad8 fix(tracker): click on hex track low/high nibble selects that nibble
Closes #160
2024-10-15 09:37:21 +03:00
5099c61705 chore: fix linter problems in work space (remove unuseds etc.) 2024-10-15 09:18:41 +03:00
b494a69a76 refactor(tracker): change Iterate() func(yield):s to Iterate(yield) 2024-10-15 09:09:17 +03:00
3986bbede7 fix(tracker/gomidi): consume all available midi.Messages 2024-10-15 00:25:07 +03:00
97e59c5650 refactor(tracker): use go v1.23 style iterators throughout 2024-10-15 00:01:02 +03:00
2b7ce39069 refactor(tracker/gioui): give Editor Text / SetText methods 2024-10-14 23:36:32 +03:00
03c994e4da refactor(tracker/gioui): wrap Editor to include common key.Filters 2024-10-14 23:12:58 +03:00
cd88ea0680 test(tracker): fix FuzzModel: NullContext implements MIDIContext 2024-10-14 17:25:56 +03:00
f8f0e11b76 docs: update CHANGELOG.md 2024-10-14 17:09:12 +03:00
2809526de6 refactor(tracker): ask for midiContext in the model constructor 2024-10-14 17:03:17 +03:00
f427eca1f4 fix(sointu-vsti): VST crashed due to Model.MIDI being nil 2024-10-14 16:57:21 +03:00
c07d8000c6 refactor(tracker): harmonize naming and use iterators in MIDI
using iterators requires go 1.23
2024-10-14 15:00:55 +03:00
577265b250 feat(tracker): add support for a MIDI controller to the standalone tracker
Closes #132.
2024-10-14 14:11:50 +03:00
9779beee99 feat: units can have comments
Closes #114
2024-10-13 23:02:13 +03:00
160eb8eea9 fix(tracker/gioui): typing notes respects the keybinding modifiers 2024-10-13 15:14:04 +03:00
3fb7f07c2c feat(tracker/gioui): keybindings file is keybindings.yml, not .yaml 2024-10-13 14:54:42 +03:00
10f021a497 feat: toggle button to duplicate non-unique patterns when changed
Closes #77.
2024-10-13 14:47:22 +03:00
3a7ab0416a fix(presets): remove invalid parameters from snare-adam.yml 2024-10-13 14:39:04 +03:00
4c096a3fac refactor(tracker): rename Notetracking to Follow
Notetracking was used in two completely different meanings: the
pitch/bpm notetracking in the delay unit and the cursor follow in
when playing. The word for the second meaning was changed to Follow,
to avoid confusion.
2024-10-12 21:35:45 +03:00
59c04ed4a1 refactor(tracker): shorten the names of model.PlayFrom... methods 2024-10-12 21:31:56 +03:00
a6bb5c2afc feat(tracker): make keybindings user configurable
Closes #94, closes #151.
2024-10-12 21:08:30 +03:00
5c51932f60 fix(tracker): autofix malformed songs with useless params 2024-10-11 20:34:04 +03:00
773655ef9c fix(tracker/gioui): avoid deadlock while changing window title 2024-10-11 16:08:55 +03:00
91b7850bf7 feat(tracker): change keyboard shortcuts to mimic old trackers 2024-10-11 15:31:54 +03:00
b4a63ce362 feat(tracker/gioui): label identifying instrument MIDI channel
Closes #154.
2024-10-11 12:16:40 +03:00
a94703deea fix(tracker/gioui): pressing a or 1 in hex mode created note off
Closes #162
2024-10-08 12:31:05 +03:00
ad5f7628a5 doc: improve filterFrequencyDispFunc comments 2024-10-08 11:45:27 +03:00
b538737643 feat(sointu): show filter frequency in Hz
Closes #158.
2024-10-06 21:54:19 +03:00
47d7568552 refactor: remove ParamHintString, add DisplayFunc for each param 2024-10-06 19:04:10 +03:00
81a6d1acea feat: upgrade oto and output float audio 2024-10-06 18:58:08 +03:00
890ebe3294 refactor(tracker/gioui): use layout.Background, not layout.Stacked 2024-10-05 13:24:31 +03:00
bf5579a2d2 build: upgrade to latest gioui 2024-10-05 12:34:02 +03:00
8fd2df19a1 fix(sointu-vsti): warn about sample rate only after plugin init 2024-09-22 09:59:58 +03:00
ce673578fd fix(amd64-386): crash with sample-based oscillator in 32-bit library 2024-09-22 09:30:42 +03:00
0e10cd2ae8 fix(amd64-386): sample oscillator hard crash
The sample-based oscillators converted the samplepos to an integer
and did samplepos < loop_end comparison to check if we are past
looping. Unfortunately, the < comparison was done in signed math.
Normally, this should never happen, but if the x87 FPU stack
overflowed exactly at right position, we then got 0x80000000 in
samplepos, which is equal to -2147483648. Thus, we considered that
sample is not looping and read the sample table at position
-2147483648, well out of bound. TL;DR changing jl to jb makes sure
we always wrap within to sample table, no matter what.

Fixes #149.
2024-09-22 09:04:47 +03:00
4ee355bb45 fix(tracker/gioui): DPI scaling of the numeric updown icons
Closes #150.
2024-09-21 14:01:32 +03:00
7d6daba3d2 fix(vm/compiler/bridge): empty patch should not crash native synth
Fixes #148.
2024-09-16 19:58:23 +03:00
2b38e11643 feat: include version info in the binaries 2024-09-15 19:45:00 +03:00
f8c522873c docs: update CHANGELOG.md for v0.4.1 2024-09-08 19:27:42 +03:00
e49f699f62 feat(tracker/gioui): clicking a parameter slider (etc.) selects it
Closes #112.
2024-09-08 14:46:24 +03:00
6924b63e02 test(vm): disabled units should not affect NecessaryFeatures for vm
Closes #140.
2024-09-08 11:22:05 +03:00
6fc9277113 fix(tracker): unit search gains focus when adding a unit on last row 2024-09-07 21:29:37 +03:00
877556b428 feat(tracker): do not wrap around when playing or moving cursor
The wrapping was usually unwanted behaviour. The user can use the
looping (Ctrl-L) to loop the song forever if this is really desired.
2024-09-07 18:52:52 +03:00
5e65410d27 fix(sointu): use proper modulo in SongPos
The previous implementations used remained, not modulo, which could
cause issues with negative values.
2024-09-07 18:45:14 +03:00
4e1fdf57d9 fix(tracker/gioui): advance row by step when inputting a note
Closes #144.
2024-09-07 17:45:32 +03:00
1daaf1829c fix(tracker): ensure numVoices of loaded instrument is ok 2024-09-07 15:25:06 +03:00
74972b5ff4 fix(tracker): ID collisions in ClearUnit and Instruments.unmarshal 2024-09-07 15:25:06 +03:00
9da6c2216c test(tracker): fuzz testing of ID collisions and file read/writes 2024-09-07 15:16:53 +03:00
61e7da5dab test(tracker): test loading presets in fuzz tests 2024-09-06 22:03:52 +03:00
59fb39d9b3 fix(tracker/gioui): move alert popups north to not overlay buttons
Closes #142.
2024-09-06 22:01:00 +03:00
9cb573d965 feat(tracker/gioui): cursor indicates split bars can be resized
Closes #145.
2024-09-06 20:29:33 +03:00
d46605c638 fix: assign new IDs to loaded instruments
Fixes #146.
2024-09-06 20:19:27 +03:00
569958547e fix(amd64-386): do not optimize away phase modulations with unisons 2024-08-17 11:06:18 +03:00
012ed10851 test: add unit test for unisons with phase = 0
This demonstrates a bug found by Virgill:
the x86 templates optimize away the phase
modulation when all phases are set to 0,
but the unisons need the phase modulation
internally to offset the phase of the different
unison oscillators.
2024-08-17 11:06:17 +03:00
5bc6dc6015 test(vm): test that disabled units do not affect results 2024-08-14 19:43:13 +03:00
350402f8f3 fix(vm): prevent crash when only disabled delay units & test it 2024-08-14 19:41:44 +03:00
75bd9c591e fix: do not include delay times from disabled delay units
Closes #139.
2024-08-14 15:41:13 +03:00
2667c3c72c docs: update CHANGELOG.md for v0.4.0 2024-08-10 15:29:49 +03:00
e09af5ab34 fix(tracker): loading preset did not update the ids
When a preset was loaded, its IDs were not updated,
causing ID collisions in the song and send targets
going wrong.
2024-08-10 15:20:25 +03:00
db2d9cac9d fix(vm): x87 native filter unit was denormalizing and eating up CPU
When voice was silent, the exponential decays in the filter unit
were causing the high pass component to eventually denormalize,
causing high CPU loads. The solution is the same as in the delay
unit: add and subtract a small number from the value, causing
essentially a flush to zero.
https://en.wikipedia.org/wiki/Subnormal_number

Fixes #68.
2024-06-19 18:58:20 +03:00
a14e21dff6 ci: macos-latest is now arm64 and breaks, use macos-12 for now 2024-05-05 13:19:25 +03:00
58916d3c6d docs: update README.md, to recommend using nasm instead of yasm 2024-05-05 11:37:42 +03:00
84d90cf0f3 fix(vm/compiler): use more yasm-compatible syntax
Closes #134.
2024-05-05 11:34:47 +03:00
10d20cd26f fix(vm/compiler): export as su_pow, instead of su_power
The export redefinition of label, even though the labels were on the
same line. This was an issue for yasm.

Related to #134.
2024-05-05 11:33:47 +03:00
4a8d4c5a29 fix(vm/compiler/templates): modulating delaytime in wasm could crash
The modulated delay time was converted to int with i32.trunc_f32_u.
This throws runtime error if the modulations caused the delaytime
to become negative, because _u implied that it should be unsigned
integer and negative numbers were out of range. Using
i32.trunc_f32_s fixed this.
2024-04-08 20:06:20 +03:00
f074c392f6 docs: add anticore to contributors 2024-04-08 19:05:20 +03:00
20fc12c529 feat(examples): add example demonstrating wasm playback in browser 2024-04-08 19:01:39 +03:00
6d4529971c feat(vm/compiler): export su_power function in case user needs it 2024-04-05 15:49:00 +03:00
beb84d7652 fix(tracker/gioui): deleting a cell in the order list did not work 2024-03-14 20:37:09 +02:00
c55b27b23b fix(tracker): recording creates empty track when no notes triggered 2024-03-10 20:01:13 +02:00
e488cd391b fix(gioui): scroll wheel works in tables, not just table row titles 2024-03-02 00:40:06 +02:00
7f20bd8baf fix(tracker): remember to tell player when m.d.Loop is updated 2024-03-01 23:54:19 +02:00
07bf8f6cdf fix(gioui): draw cursor in front of play marker in order editor 2024-03-01 23:31:05 +02:00
f0f391356c fix(gioui): arrow keys leave table row/col titles 2024-03-01 23:25:30 +02:00
b18a284252 feat(gioui): + and - keys add/subtract elements in tables
Closes #65.
2024-03-01 22:43:27 +02:00
1c020fffa3 refactor(gioui): update gioui to v0.5.0 2024-03-01 22:11:44 +02:00
267973e061 build: upgrade deprecated actions and fix warnings 2024-02-24 19:06:42 +02:00
6b3aaf6cc9 docs: update README.md 2024-02-24 15:34:40 +02:00
dfc72cd2c4 build: build VSTi instrument binaries also for linux 2024-02-23 20:15:44 +02:00
8a9cbdea62 build: update vst2 to latest version, because it compiles on linux 2024-02-23 19:56:09 +02:00
edee3452f4 feat(tracker): load presets from os.UserConfigDir()/sointu/presets/
This is related to #125, but is very crude way of implementing it.
2024-02-20 20:17:59 +02:00
b70db4d394 docs: add links to 21 and Tausendeins 2024-02-20 19:39:32 +02:00
d5af39e324 docs: add link to Phosphorescent Purple Pixel Peaks 2024-02-20 19:33:46 +02:00
aa1b4d371b fix(tracker): notify player that loop changed when resetting song 2024-02-20 19:28:59 +02:00
dc12f58082 feat(tracker): add ability to loop part of song during playback
Closes #128.
2024-02-20 19:10:15 +02:00
aa7a2e56fa feat(gioui): flip the unit parameter slider scroll wheel behaviour
Closes #112.
2024-02-19 21:49:51 +02:00
17312bbe4e feat: add ability to disable units temporarily
Quite often the user wants to experiment what particular unit(s) add
to the sound. This commit adds ability to disable any set of units
temporarily, without actually deleting them. Ctrl-D disables and
re-enables the units. Disabled units are considered non-existent in
the patch.

Closes #116.
2024-02-19 21:36:14 +02:00
2b3f6d8200 fix(tracker): unit searching to work more reliably 2024-02-17 20:54:46 +02:00
db6c9f6052 fix: warn user if sample rate other than 44100 Hz
Closes #129.
2024-02-17 19:45:36 +02:00
954b306cc8 docs: update CHANGELOG.md to have links to issues 2024-02-17 19:22:14 +02:00
aec756f921 feat(sointu-track): accept filename as command line parameter
Closes #122.
2024-02-17 19:17:47 +02:00
ca4a98eb50 fix(gioui): reduce the default height of popup menus so they fit
Closes #121.
2024-02-17 19:03:49 +02:00
65cfcb045c build: update setup-go to v5 and ask go version >=1.21.0 2024-02-17 18:28:25 +02:00
bb32403c78 build: require go 1.21 as it is needed by slices package 2024-02-17 18:22:31 +02:00
d92426a100 feat!: rewrote the GUI and model for better testability
The Model was getting unmaintanable mess. This is an attempt to refactor/rewrite the Model so that data of certain type is exposed in standardized way, offering certain standard manipulations for that data type, and on the GUI side, certain standard widgets to tied to that data.

This rewrite closes #72, #106 and #120.
2024-02-17 18:16:06 +02:00
6d3c65e11d fix(templates): avoid clobbering ebx in su_load_gmdls
Fixes #130
2024-02-16 20:09:20 +02:00
c08a319eb7 docs: add link to NR4's tool 2023-11-20 08:55:22 +02:00
8227691523 test: the filenames of test_gain and test_gain_stereo were flipped 2023-10-23 22:05:28 +03:00
04fbc9f6a7 feat(vm): add dbgain unit, where gain is defined in decibels
Closes #78
2023-10-23 21:57:29 +03:00
f698986718 docs: update CHANGELOG.md 2023-10-23 18:22:15 +03:00
a38a0f4235 fix(tracker/gioui): text.Shaper should not be a global variable
text.Shaper is not thread safe, which caused crash when adding
multiple VSTI plugins to a DAW project. This change fixes that
crash. Further refactorings need to consider where that text.Shaper
should actually reside.
2023-10-22 19:10:24 +03:00
3c85f1155c perf(cmd/sointu-vsti): avoid reallocations of events array
Always appending to the end and consuming from the front cause the
capacity of the slice regularly running out, resulting in new
allocation. With this change, we increment index when consuming
events and append to the end, and when we reset, we move index to 0
and empty slice. This way, we always reuse the allocated memory.
2023-10-21 10:47:26 +03:00
1040eb585d build: require go 1.19; did not build on go 1.18 2023-10-21 00:11:58 +03:00
6eb025d7ba refactor(tracker): remove unused variable RECOVERY_FILE 2023-10-20 18:41:21 +03:00
9ec8f48f82 feat(tracker): move unnecessary members from modelData to Model 2023-10-20 17:59:27 +03:00
391b14493c feat(tracker): undo entire modelData, not just Song
The modelData is moving towards clear meaning: it's the part of the
GUI state that is undone and also recovered from disk. This changes
the recovery data so that the undo and redo stacks are not undone,
but that is unlikely a good idea anyway, as it grows the recovery
data into unreasonable sizes.

This has also the nice benefit of undoing the cursor position, which
closes #64.
2023-10-20 17:59:26 +03:00
486bab4185 style(tracker): rename NUM_RENDER_TRIES to numRenderTries 2023-10-20 16:38:44 +03:00
1e47c5004c style(tracker): remove unused PlayerPlayingMessage type 2023-10-20 16:37:19 +03:00
900f1611b1 docs(tracker): add go doc comments to GmDlsEntry/-Entries 2023-10-20 16:36:09 +03:00
beb06727b0 refactor: move UnitNames to top level package 2023-10-20 01:59:30 +03:00
b6ec5d1a04 style(tracker): group code into less number of files 2023-10-20 01:50:38 +03:00
ff8e662857 refactor(tracker): move NoteStr and NoteAsValue to gioui package 2023-10-20 01:40:14 +03:00
a60814bab7 refactor(tracker): make struct to hold all per voice data in Player 2023-10-20 01:26:41 +03:00
0ce5ca3003 refactor(tracker): send Songs/Patches etc. from Model to Player 2023-10-20 00:54:03 +03:00
14a0306064 docs(tracker): add go doc comments to Player 2023-10-19 23:28:57 +03:00
d342fb860b refactor(tracker): put all recording data into struct Recording 2023-10-19 23:28:57 +03:00
453f45c48a refactor(tracker): rename SongPoint to ScorePoint etc. 2023-10-19 22:28:44 +03:00
50ccfe03da refactor(tracker): split Volume to PeakVolume and AverageVolume 2023-10-19 22:28:44 +03:00
1a8a317464 docs(tracker): improve package go doc comments 2023-10-19 22:28:44 +03:00
a9517f1511 docs: make the list of prods using sointu a markdown list 2023-10-19 14:30:01 +03:00
9073adadb3 fix(tracker/gioui): scroll bars move in sync with the cursor 2023-10-19 14:25:03 +03:00
b772940b1f fix(tracker/gioui): preset menu scrollbar fits on screen 2023-10-19 14:22:19 +03:00
d6abb14b08 feat(tracker/gioui): add scrollbars to menus 2023-10-19 14:07:09 +03:00
64270eaf68 refactor: rename FindSendTarget to FindUnit 2023-10-19 13:31:34 +03:00
43707e5fd6 style(vm): group public/private member types; delete unused types 2023-10-19 13:07:24 +03:00
fdad626279 style(vm): replace sointu.Unit{...} with {...} when allowed 2023-10-19 13:00:34 +03:00
960bddfae0 refactor(vm): use vm.GoSynther{}.Synth instead of vm.Synth
vm.Synth was used only in a few places, reduce the number of
exported functions. Also, if we ever add some global configuration
to GoSynther e.g. samplerate, we have a mechanism to do so, instead
of the Synth function.
2023-10-19 12:52:05 +03:00
7675121a78 style(vm): group public/private member types and rename privates 2023-10-19 12:47:38 +03:00
e28891abd5 refactor: move ConstructPatterns into compiler package
ConstructPatterns was never used except during compilation, so it
makes sense to have it closer where it is used. We could consider
making it even private function, as the pattern table construction
is quite specific to how compiler compiles and probably not that
reusable elsewhere.
2023-10-19 12:38:18 +03:00
e010b2da9d docs: improve go doc comments for vm package 2023-10-19 11:54:12 +03:00
98a73795c7 style: move Play and Synth to audio.go
With this grouping, everything that deals with AudioBuffers is in
Audio. song.go and patch.go do not know anything about AudioBuffers
or Synths.
2023-10-19 11:32:30 +03:00
5bbec75120 refactor: rename sointu.Render as AudioBuffer.Fill
The Render name misleading as it did not do the same thing as normal
Synth.Render, because it disregarded time limits. Conceptually, as
the function modifies the state of the synth, it would be better to
be synth.Fill(audioBuffer), but we cannot define methods on
interfaces; therefore, it is audioBuffer.Fill(synth) now.
2023-10-19 11:14:44 +03:00
ff4155a08e fix(tracker): notify player when recovery file is loaded 2023-10-19 10:59:07 +03:00
15a340317f docs: add go doc comments to 4klang conversion functions 2023-10-19 10:52:52 +03:00
b6815f70cb feat: remove unreleased parameter from Play function
The VMs now release all envelopes by default, so this mechanism was
useless / did not actually start them as unreleased even when you
thought they did.
2023-10-19 10:42:20 +03:00
9f7bbce761 refactor(vm): rename Encode to NewBytecode 2023-10-19 10:32:34 +03:00
01bf409929 refactor(vm): rename Commands/Values to Opcodes/Operands
The commands and values were not very good names to what the
byte sequences actually are: opcodes and their operands. In
many other places, we were already calling the byte in the Command
stream as Opcode, so a logical name for a sequence of these is
Opcodes. Values is such a generic name that it's not immediately
clear that this sequence is related to the opcodes. Operands is not
perfect but clearly suggests that this sequence is related to
the Opcodes.
2023-10-18 19:53:47 +03:00
87604dd92e refactor(vm): rename BytePatch to Bytecode 2023-10-18 19:12:34 +03:00
ccd283d2ea docs: update comments 2023-10-18 18:34:14 +03:00
0a67129a0c refactor!: rename SynthService to Synther and related types
The -er suffix is more idiomatic for single method interfaces, and
the interface is not doing much more than converting the patch to a
synth. Names were updated throughout the project to reflect this
change. In particular, the "Service" in SynthService was not telling
anything helpful.
2023-10-18 17:32:13 +03:00
e4a2ed9f32 style: group types into fewer, logical files 2023-10-18 15:02:25 +03:00
0187cc66ec refactor: move Wav and Raw methods as members of AudioBuffer 2023-10-18 14:40:16 +03:00
33625c6f40 fix(vm): stereo delay flipped taps for right and left channel 2023-10-18 13:54:26 +03:00
38e9007bf8 refactor: use [][2] as audio buffers, instead of []float32
Throughout sointu, we assume stereo audiobuffers, but were passing
around []float32. This had several issues, including len(buf)/2 and
numSamples*2 type of length conversion in many places. Also, it
caused one bug in a test case, causing it to succeed when it should
have not (the test had +-1 when it should have had +-2). This
refactoring makes it impossible to have odd length buffer issues.
2023-10-18 13:51:02 +03:00
bb0d4d6800 docs(tracker): update comments 2023-10-17 20:31:57 +03:00
b97d269cc4 build: update Gio to v0.3.1 2023-10-17 20:30:06 +03:00
192b31917a docs: update README.md 2023-10-17 15:07:31 +03:00
462faf5f4e feat: save recovery data to disk and/or DAW project 2023-10-17 10:26:36 +03:00
97a1b2f766 perf(tracker): use json recovery files instead of yaml for less garbage
The yaml marshaling and umarshaling seems to allocate a lot of memory. When saving the recovery file, the memory use jumped up by hundreds of megabytes. Switch to using json marshaling for the recovery file, as it does waste memory so badly. Binary marshaling was also an option, but its nice in emergency situations that the user can glance the recovery file and perhaps, with some effort, recover stuff from it. Json is good enough for manual recovery during emergency situations.
2023-10-15 11:11:26 +03:00
4899b027ff perf(tracker/gioui): use pointer receivers in numericupdown to avoid garbage 2023-10-15 09:49:30 +03:00
1a256b1f01 feat(cmd/sointu-track): add command line parameters for cpu & mem profiling 2023-10-15 09:07:22 +03:00
b455ef0f3c feat(tracker): add reverb presets for delay unit
The options are stereo, left and right. Similar to oscillator sample settings, if you tamper with these, it starts to show "custom". Used some of the generic features of go1.18, so had to update go.mod to require go1.18.
2023-10-14 14:58:38 +03:00
94589eb2eb feat(examples): add example playing sointu tracks from Python (#108)
* Added Python code example.
* Added pyinstaller build.
* Clarified debugging steps in README.md.
* Added linux implementation.
* Cosmetics.
* Updated README with correct steps.
2023-10-11 09:37:00 +03:00
f5eeabe5f3 fix(tracker/gioui): respawn window if VSTI accidentally closes it 2023-10-08 16:27:17 +03:00
61ebd89da0 fix(tracker): set PrevUndoType to "" when undoing and redoing 2023-10-08 15:59:33 +03:00
e5691d670a feat(vm): add frequency modulation for oscillators
Closes #105
2023-10-07 21:48:03 +03:00
12dd3dada0 refactor(vm): rewrote BytePatch Encode to use a builder struct
(cherry picked from commit fdf119e50ce62619f508cc423c2ebaa000a1d540)
2023-10-07 14:07:47 +03:00
8c8232f76e feat(vm)!: implement cross-instrument modulation of all voices
The "auto" was misleading, as it meant self modulation when targetting a unit within instrument itself and just voice 0 when cross-instrument modulation. This feature changes the "auto" meaning "self" for instruments self-modulating, and "all" voices for cross-instrument modulations. "all" is implemented by compiling a single send into multiple repeated sends, with only the last popping the stack (if necessary).

Closes #107
2023-10-07 14:07:39 +03:00
7ee43f199a fix(tracker): make sure undo & redo stack never grow beyond limit 2023-10-02 15:43:52 +03:00
048de55f00 fix(tracker): save recovery in GUI thread and reduce recovery size&frequency 2023-10-02 13:40:26 +03:00
905637eee3 update README.md: add links to prods using sointu 2023-10-02 11:15:15 +03:00
ce7c8a0d3e feat(tracker): add menu to load instrument presets
The presets are embedded in the executable, so there's no additional files.

Closes #91
2023-10-01 18:54:50 +03:00
b65d11cbb7 update CHANGELOG.md and README.md 2023-10-01 15:07:50 +03:00
df2605fddd feat(tracker): save recovery file regularly & load it on startup 2023-10-01 14:45:45 +03:00
12f15d1066 fix(tracker/gioui): make VSTI close event wait that gioui actually quit 2023-10-01 12:42:12 +03:00
e3c7d2cba4 fix(cmd/sointu-vsti): use different name and ID for native vsti plugin 2023-09-24 17:39:30 +03:00
545f32bcc3 release v0.2.0 and add automated releases to CI 2023-09-24 13:11:00 +03:00
ee2c83e2cb update CHANGELOG.md 2023-09-24 11:20:53 +03:00
00850c8001 code/text formatting and cleaning up whitespace 2023-09-24 10:47:54 +03:00
f35f948118 fix(vm/compiler/templates/wasm): add support for mono out
Adds also a test case to make sure mono out also works.
2023-09-24 10:27:34 +03:00
7df8103bf9 fix(vm): change crush resolution to bits (closes #79)
BREAKING CHANGE: The problem with crush was that it had very few usable values. This changes the crush to map the value nonlinearly, so the crush resolution is bits. Still the upper portion of the values is not very usable (bits 12-24 i.e. hardly any crushing), but at least the lower portion is usable. But now crush resolution has slightly different meaning.
2023-09-23 21:23:05 +03:00
1ac2ad3c75 fix(vm/compiler): invert the logic of the release flag in the voices (closes #102)
This makes all envelopes released by default, instead of attacking. Add also test to demonstrate the buggy behaviour.
2023-09-23 15:56:46 +03:00
20b0598a57 upgrade gioui to latest version 2023-09-23 14:43:09 +03:00
14e548c4c1 fix(tracker/gioui): CopyUnitBtn marshaled unit incorrectly 2023-09-20 14:57:23 +03:00
c692ff0f16 build: allow optional use of Crinkler when linking examples 2023-09-02 22:33:48 +03:00
b028fea59a build: make targets properly rebuild when templates or compiler changed 2023-09-02 20:14:52 +03:00
231e055faf fix(gioui/tracker): song files were not truncated when opened for writing (closes #103) 2023-09-02 14:32:23 +03:00
de3f4d987f fix(tracker/gioui): hitting enter/return to focus on the instrument name editor 2023-09-01 22:51:59 +03:00
8c59ea1b4c add ParamHintString for loadval.value showing range [-1,1] 2023-09-01 22:01:53 +03:00
98fedd0ed2 make ParamHintString show range [-1,1] for send.amount 2023-09-01 21:54:55 +03:00
607e5b5da0 Added x86 asm and C wav writer and player examples.
Specifically:
* Added win32, elf32 and elf64 asm player and wav writers using winmm.
* Added dsound player in C.
* Separated the ALL target and the examples; introduced a new examples target.
2023-08-31 14:15:52 +03:00
a439a4fa48 update README.md: credits
(cherry picked from commit 1d89dd0e99fdfce01fbee47e1d409118c4fa1ce2)
2023-08-31 13:06:45 +03:00
29a33a154b update README.md: add prod link for "Physics Girl St." 2023-08-29 09:27:04 +03:00
aba8ff2c85 CI: don't build examples as part of tests 2023-08-29 09:10:57 +03:00
d0efcc3001 Added usage examples in C; Added asm include file with track info to sointu-compile. 2023-08-29 09:09:02 +03:00
dff484739c feat(sointu): add better ParamHintString for in and aux unit channels 2023-08-28 23:10:11 +03:00
7dd2c246a0 feat(vm): add support for gm.dls samples in the go virtual machine (closes #75) 2023-08-28 22:44:37 +03:00
6ec06c760a CI: add builds for linux and macos (closes #82) 2023-08-28 16:26:15 +03:00
4135286ed0 update README.md 2023-08-28 13:42:14 +03:00
c7d79035ce fix: copying and pasting units messed selection and Ctrl-C actually cut 2023-08-27 16:05:11 +03:00
568aa1d76d update README.md 2023-08-27 14:54:14 +03:00
d82d151f49 fix: native synth building on go 1.21
go v1.21 is more strict about giving methods to C.structs and was complaining about "cannot define new methods on non-local type *C.Synth". The solution was a local type alias: type BridgeSynth C.Synth
2023-08-27 12:24:06 +03:00
c040bdedee fix: when just saving a file, open the file in write mode (fixes #99) 2023-08-27 11:29:55 +03:00
a0bcac3904 fix: check that MIDI triggered instrument is within patch limits (#98) 2023-08-27 11:10:54 +03:00
33221b5203 fix: upgrade to latest gioui (closes #97)
Latest gioui has the fix that enter key up event alone does not trigger a button
2023-08-27 10:59:11 +03:00
94926c5596 update README.md - add link to | by epoqe 2023-08-19 20:21:52 +03:00
61776f397a feat: add ability to select & move multiple units (closes #71) 2023-07-21 00:39:45 +03:00
5884a8d195 feat(tracker/gioui): add tooltips
Currently, only iconbtns and numeric updowns have tooltips. Closes #84
2023-07-19 22:31:29 +03:00
cafb43f8c8 feat(tracker/gioui): add ability to scroll parameter values (closes #92) 2023-07-18 23:28:20 +03:00
5a2e87982e feat(tracker): add ability to copy, cut and paste units 2023-07-18 17:17:50 +03:00
338529012a update README.md 2023-07-18 16:22:43 +03:00
ffb2f18c68 fix(cmd/sointu-vsti): upgrade vst2 package & request tempo properly from host 2023-07-18 10:37:32 +03:00
ccc8dc906f fix(tracker): guard for malformed songs in SetSong 2023-07-17 23:51:14 +03:00
c421748db9 fix: if fetching BPM from vsti host fails, keep the previous BPM as defined by the user 2023-07-17 23:26:11 +03:00
9db6ecb3da CI: build sointu-compile.exe as a part of the binaries 2023-07-08 16:44:00 +03:00
8ffe4a70dd feat(vm/compiler): embed templates to executable 2023-07-08 16:39:41 +03:00
d2ddba3944 fix(tracker/gioui): mouse clicks passed through the new unit button. closes #93 2023-07-08 16:07:17 +03:00
7af7d4332d change: do not respect polyphony when importing 4klang patches
Using polyphony 2 gave errors: 16 instruments with polyphony 2 + 1 global was a total of 33 voices and gave errors when sointu compiling. User will set anyway polyphony as needed for every instrument, like 1 for most instruments, so just use NumVoices 1 always.
2023-07-08 15:22:18 +03:00
9d6ca519a2 rename examples/fourklang to examples/fourklang_patches 2023-07-08 15:15:24 +03:00
3da62179e4 refactor(tracker/gioui): use gioui/x/explorer instead of home made file explorer 2023-07-08 15:12:45 +03:00
8c4f7ee61f refactor(tracker/gioui): update gioui to newer version 2023-07-08 11:57:19 +03:00
f5980ecb79 Update README.md 2023-07-07 01:44:01 +03:00
63fc3d0d08 Update CHANGELOG.md 2023-07-07 01:09:29 +03:00
9ef271f1a8 fix(tracker/gioui): display voice states crashed if somehow patch had more than 32 voices 2023-07-06 23:49:40 +03:00
cd00067da8 change(tracker): default delay to use BPM-tracking and make reverb similar to 4klang 2023-07-06 23:49:40 +03:00
248ba483c6 feat: add ability to import 4klang patches and instruments 2023-07-06 23:47:55 +03:00
c06ac6ea5e update README.md 2023-05-15 21:22:27 +03:00
a3dcc829c0 CI: rename binaries-zip to sointu-binaries-<hash>.zip 2023-05-13 18:43:20 +03:00
e7dbb0289c CI: add action to build artifacts 2023-05-13 18:05:52 +03:00
9efddd673d fix(tracker): when reassigning unit IDs, update send targets. fixes instrument loading 2023-05-13 17:56:13 +03:00
cd700ed954 feat!: implement vsti, along with various refactorings and api changes for it
The RPC and sync library mechanisms were removed for now; they never really worked and contained several obvious bugs. Need to consider if syncs are useful at all during the compose time, or just used during intro.
2023-05-13 17:56:13 +03:00
70080c2b9d fix(templates/wasm): $WRK was modified by stereo oscillators, messing up all modulations targeting units after the unit
add also tests to make sure we don't mess it up again
2023-04-06 15:03:16 +03:00
61c2e980a2 fix(templates/wasm): anyfunc should be funcref nowadays in .wat 2023-04-06 14:50:18 +03:00
6129076e97 upgrade ilammy/setup-nasm to v1.4.0 and wat2wasm to v1.0.29
wat2wasm doesn't support --enable-bulk-memory anymore, presumably because it is part of the standard nowadays
2023-04-06 14:50:18 +03:00
e73365b980 Merge pull request #87 from kendfss/master
fix: instrumenteditor starting expansion state
2022-04-07 14:06:58 +03:00
7eb473e67e fix: instrumenteditor starting expansion state 2022-03-23 21:45:09 +01:00
1a5251dbf6 refactor(sointu): change the name of AudioSink into AudioOutput
The interface is never used as anything else as Output so trying to generalize as something more vague like Sink made no sense.
2021-08-30 23:11:33 +03:00
eda48491e2 refactor(sointu): move engineeringTime helper function to the file where it is actually used 2021-08-30 22:27:38 +03:00
a8f8911f03 refactor(sointu): Change the signature of Play to accept SynthService instead of Synth
This is more logical as every single use of Play started with compiling the patch of a song with a SynthService.
2021-08-30 22:24:42 +03:00
a9b90c4db8 style: add comments to the public methods and members in the root package. 2021-08-30 20:34:56 +03:00
60e4518230 feat(tracker, gioui): make + and - keys adjust order numbers
Holding ctrl down while adjusting the order number keeps the song effectively same, but juggles pattern numbers. Useful for reorganizing song.
2021-05-15 14:19:46 +03:00
7885c306ee feat(tracker, gioui): make a Editor for inputting the unit type manually
The keyboard shortcuts were too wonky, so removed them altogether. Had to remove also unit wrapping from model (now it just clamps the parameter to the current units) as it did not play nice with the new editor.

Closes #70.
2021-05-13 19:50:23 +03:00
ede70380f2 feat(tracker, gioui): add menu item to remove all unused data from song
Reorders patterns and cuts them short and the order list short to remove all unused / unuseful (all holds) patterns.
2021-05-13 00:00:54 +03:00
8a94058d44 feat(gioui): make split bars snap to window edges 2021-05-12 23:14:48 +03:00
203e8a3ccc refactor(vm): simplify flattenSequence code 2021-05-12 22:44:03 +03:00
a2723829da refactor: implement Order and Pattern types: slices returning default values for out of bound indices 2021-05-12 12:08:55 +03:00
ce6e5d4942 tracker: move gmdlsentries.go generation under tracker/generate folder 2021-05-12 09:18:48 +03:00
1a89fee665 CI: don't test oto & remove libasound2-dev dependency
Installing this dependency failed in the cloud and caused tests to fail, so for now, we do not install it and do not test oto package.
The tests were actually about some float / int16 conversions, which should not anyway be in oto package, so future solution will be to refactor those functions somewhere else.
2021-05-08 17:34:16 +03:00
e9834110ec fix(bridge): respect the hard limit of 64 delay lines to avoid crashes. 2021-05-08 16:51:45 +03:00
e649b9ec54 fix(gioui): unnamed instruments on tracks with multiple voices crashed.
Closes #62.
2021-05-08 16:40:06 +03:00
d5f413c5dc Update CHANGELOG.md 2021-04-29 14:29:48 +03:00
5aa16b4a97 feat(tracker, gioui): add the ability to reorder / drag tracks in order list 2021-04-24 22:47:45 +03:00
442715334e feat(gioui): add grab cursor to DragList 2021-04-24 22:31:32 +03:00
d55e9e9880 fix(instruments): make transpose neutral in supersaw instrument 2021-04-24 22:10:55 +03:00
15cf8a750c Update README.md 2021-04-24 22:09:30 +03:00
b2b15f825d refactor(tracker, gioui): get rid of EditMode, use gio focus instead 2021-04-24 22:07:56 +03:00
e544e955cb refactor(gioui): move common button code to two functions 2021-04-20 18:21:21 +03:00
c0a0a5d501 refactor(gioui): move common iconbutton code to a function 2021-04-20 17:57:36 +03:00
8ba9fb1f00 fix(gioui): make editors lose focus when Escape is pressed. 2021-04-19 22:46:35 +03:00
56ceafdaa6 tracker: make a slightly more sensible default song 2021-04-19 22:19:51 +03:00
cbc07764a0 feat(instruments): add a few example instruments 2021-04-19 22:00:55 +03:00
40d4d6576e feat(sointu, tracker, gioui): add a comment field to the instrument 2021-04-19 21:24:29 +03:00
147e8a2513 feat(gioui): implement own file save / load dialogs
Removes the dependency on sqweek/dialogs, which was always very buggy.

Closes #12
2021-04-18 19:10:41 +03:00
ac95fb65c4 fix(gioui): prevent crashing when loading malformed song 2021-04-17 23:30:13 +03:00
485b783341 feat(gioui): add buttons to save and load instrument 2021-04-17 23:08:12 +03:00
461 changed files with 34169 additions and 7186 deletions

216
.github/workflows/binaries.yml vendored Normal file
View File

@ -0,0 +1,216 @@
name: Binaries
on:
push:
branches:
- master
- dev
tags:
- 'v*'
pull_request:
branches:
- master
- dev
jobs:
create_release:
name: Create release
runs-on: ubuntu-latest
# Note this. We are going to use that in further jobs.
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- uses: actions/checkout@v4
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref_name }}
body_path: CHANGELOG.md
draft: false
prerelease: false
if: startsWith(github.ref, 'refs/tags/')
binaries:
needs: create_release # we need to know the upload URL
runs-on: ${{ matrix.config.os }}
strategy:
matrix:
config:
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-compile.exe
params: cmd/sointu-compile/main.go
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-track.exe
params: -tags=native cmd/sointu-track/main.go
ldflags: -H=windowsgui
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-vsti.dll
params: -buildmode=c-shared -tags="plugin,native" ./cmd/sointu-vsti/
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-play.exe
params: cmd/sointu-play/main.go
- os: ubuntu-latest
asmnasm: /home/runner/nasm/nasm
output: sointu-compile
params: cmd/sointu-compile/main.go
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
- os: ubuntu-latest
asmnasm: /home/runner/nasm/nasm
output: sointu-track
params: -tags=native cmd/sointu-track/main.go
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
- os: ubuntu-latest
asmnasm: /home/runner/nasm/nasm
output: sointu-vsti.so
params: -buildmode=c-shared -tags="plugin,native" ./cmd/sointu-vsti/
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
- os: ubuntu-latest
asmnasm: /home/runner/nasm/nasm
output: sointu-play
params: cmd/sointu-play/main.go
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
- os: macos-latest
asmnasm: /Users/runner/nasm/nasm
output: sointu-compile
params: cmd/sointu-compile/main.go
- os: macos-latest
asmnasm: /Users/runner/nasm/nasm
output: sointu-track
params: cmd/sointu-track/main.go
- os: macos-latest
asmnasm: /Users/runner/nasm/nasm
output: sointu-vsti.a
bundleoutput: sointu-vsti
params: -buildmode=c-archive -tags="plugin" ./cmd/sointu-vsti/
bundle: true
- os: macos-latest
asmnasm: /Users/runner/nasm/nasm
output: sointu-play
params: cmd/sointu-play/main.go
steps:
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- uses: lukka/get-cmake@latest
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
with:
go-version: '>=1.23.8 <1.23.9'
- uses: ilammy/setup-nasm@v1.5.1
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: ${{ matrix.config.packages }}
version: 1.0
if: runner.os == 'Linux'
- name: Build library
env:
ASM_NASM: ${{ matrix.config.asmnasm }}
run: |
mkdir build
cd build
cmake -GNinja ..
ninja sointu
- name: Build binary
run: |
go build -ldflags "-X github.com/vsariola/sointu/version.Version=$(git describe) ${{ matrix.config.ldflags}}" -o ${{ matrix.config.output }} ${{ matrix.config.params }}
- name: Upload binary
if: matrix.config.bundle != true
uses: actions/upload-artifact@v4
with:
name: ${{ runner.os }}-${{ steps.short-sha.outputs.sha }}-${{ matrix.config.output }}
path: ${{ matrix.config.output }}
- name: Bundle VST
if: matrix.config.bundle
run: | # following https://github.com/RustAudio/vst-rs/blob/master/osx_vst_bundler.sh
mkdir -p "bundle/${{ matrix.config.bundleoutput }}.vst/Contents/MacOS"
clang++ -D__MACOSX_CORE__ -framework CoreServices -framework CoreAudio -framework CoreMIDI -framework CoreFoundation -L./build/ -lsointu -bundle -o bundle/${{ matrix.config.bundleoutput }} -all_load ${{ matrix.config.output }}
echo "BNDL????" > "bundle/${{ matrix.config.bundleoutput }}.vst/Contents/PkgInfo"
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${{ matrix.config.bundleoutput }}</string>
<key>CFBundleGetInfoString</key>
<string>vst</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>com.vsariola.${{ matrix.config.bundleoutput }}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${{ matrix.config.bundleoutput }}</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>$((RANDOM % 9999))</string>
<key>CSResourcesFileMapped</key>
<string></string>
</dict>
</plist>" > "bundle/${{ matrix.config.bundleoutput }}.vst/Contents/Info.plist"
mv "bundle/${{ matrix.config.bundleoutput }}" "bundle/${{ matrix.config.bundleoutput }}.vst/Contents/MacOS/${{ matrix.config.bundleoutput }}"
- name: Upload bundle
if: matrix.config.bundle
uses: actions/upload-artifact@v4
with:
name: ${{ runner.os }}-${{ steps.short-sha.outputs.sha }}-${{ matrix.config.bundleoutput }}
path: bundle
upload_release_asset:
needs: [create_release, binaries]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
strategy:
matrix:
config:
- os: Windows
- os: Linux
- os: macOS
steps:
- uses: benjlevesque/short-sha@v2.2
id: short-sha
with:
length: 7
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: ${{ matrix.config.os }}-${{ steps.short-sha.outputs.sha }}-*
merge-multiple: true
path: sointu-${{ matrix.config.os }}
- name: Zip binaries
run: |
zip ./sointu-${{ matrix.config.os }}.zip sointu-${{ matrix.config.os }}/*
- name: Upload release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create_release.outputs.upload_url }}
asset_name: sointu-${{ matrix.config.os }}.zip
asset_path: ./sointu-${{ matrix.config.os }}.zip
asset_content_type: application/octet-stream

View File

@ -19,52 +19,48 @@ jobs:
config:
- os: ubuntu-latest
asmnasm: /home/runner/nasm/nasm
cmakeflags: -GNinja
maker: ninja
gotests: yes
gotestcases: ./vm ./vm/compiler/bridge ./vm/compiler
cgo_ldflags:
- os: windows-latest
cmakeflags: -GNinja
maker: ninja
asmnasm: C:\Users\runneradmin\nasm\nasm
gotests: yes
gotestcases: ./vm ./vm/compiler/bridge ./vm/compiler
cgo_ldflags:
- os: macos-latest
cmakeflags: -GNinja
maker: ninja
asmnasm: /Users/runner/nasm/nasm
gotests: yes
gotestcases: ./vm ./vm/compiler
cgo_ldflags: # -Wl,-no_pie
# ld on mac is complaining about position dependent code so this would take the errors away, BUT
# suddenly this causes an error, even though worked last week. Let's accept the warnings rather
# than let the tests fail because of this.
# TODO: win32 builds didn't quite work out, complains gcc broken
steps:
- uses: lukka/get-cmake@v3.18.3
- uses: vsariola/setup-wabt@v1.0.1
- uses: lukka/get-cmake@latest
- uses: vsariola/setup-wabt@v1.0.2
with:
version: 1.0.20
- uses: actions/setup-go@v2
- uses: actions/setup-node@v2
version: 1.0.29
- uses: actions/checkout@v4
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
with:
node-version: '15'
- uses: actions/checkout@v2
- uses: ilammy/setup-nasm@v1.2.0
- name: Install libasound2-dev # sointu-cli has alsa as dependency for playing sound and
if: ${{ matrix.config.os == 'ubuntu-latest' }} # ubuntu was complaining about "Package alsa was not found in the pkg-config search path.",
run: sudo apt install libasound2-dev # leading to tests failing. This fixes that.
go-version: '>=1.21.0'
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: ilammy/setup-nasm@v1.5.1
- name: Run ctest
env:
ASM_NASM: ${{ matrix.config.asmnasm }}
run: |
mkdir build
cd build
cmake ${{ matrix.config.cmakeflags }} ..
${{ matrix.config.maker }}
cmake -GNinja ..
ninja tests/all sointu
ctest --output-on-failure
- name: Run go test
if: ${{ matrix.config.gotests == 'yes' }}
env:
CGO_LDFLAGS: ${{ matrix.config.cgo_ldflags }}
run: |
go test ./vm ./vm/compiler/bridge ./vm/compiler ./oto
go test ${{ matrix.config.gotestcases }}

8
.gitignore vendored
View File

@ -17,8 +17,9 @@ build/
# Project specific
old/
# VS Code
# IDEs
.vscode/
.idea/
# project specific
# this is autogenerated from bridge.go.in
@ -29,3 +30,8 @@ out/
.cache/
actual_output/
**/__debug_bin
*.exe
*.dll
**/testdata/fuzz/
.DS_Store

520
4klang.go Normal file
View File

@ -0,0 +1,520 @@
package sointu
import (
"bytes"
"encoding/binary"
"fmt"
"io"
)
// Read4klangPatch reads a 4klang patch (a file usually with .4kp extension)
// from r and returns a Patch, making best attempt to convert 4klang file to a
// sointu Patch. It returns an error if the file is malformed or if the 4kp file
// version is not supported.
func Read4klangPatch(r io.Reader) (patch Patch, err error) {
var versionTag uint32
var version int
var polyphonyUint32 uint32
var instrumentNames [_4KLANG_MAX_INSTRS]string
patch = make(Patch, 0)
if err := binary.Read(r, binary.LittleEndian, &versionTag); err != nil {
return nil, fmt.Errorf("binary.Read: %w", err)
}
var ok bool
if version, ok = _4klangVersionTags[versionTag]; !ok {
return nil, fmt.Errorf("unknown 4klang version tag: %d", versionTag)
}
if err := binary.Read(r, binary.LittleEndian, &polyphonyUint32); err != nil {
return nil, fmt.Errorf("binary.Read: %w", err)
}
for i := range instrumentNames {
instrumentNames[i], err = read4klangName(r)
if err != nil {
return nil, fmt.Errorf("read4klangName: %w", err)
}
}
m := make(_4klangTargetMap)
id := 1
for instrIndex := 0; instrIndex < _4KLANG_MAX_INSTRS; instrIndex++ {
var units []Unit
if units, err = read4klangUnits(r, version, instrIndex, m, &id); err != nil {
return nil, fmt.Errorf("read4klangUnits: %w", err)
}
if len(units) > 0 {
patch = append(patch, Instrument{Name: instrumentNames[instrIndex], NumVoices: 1, Units: units})
}
}
var units []Unit
if units, err = read4klangUnits(r, version, _4KLANG_MAX_INSTRS, m, &id); err != nil {
return nil, fmt.Errorf("read4klangUnits: %w", err)
}
if len(units) > 0 {
patch = append(patch, Instrument{Name: "Global", NumVoices: 1, Units: units})
}
for i, instr := range patch {
fix4klangTargets(i, instr, m)
}
return
}
// Read4klangInstrument reads a 4klang instrument (a file usually with .4ki
// extension) from r and returns an Instrument, making best attempt to convert
// 4ki file to a sointu Instrument. It returns an error if the file is malformed
// or if the 4ki file version is not supported.
func Read4klangInstrument(r io.Reader) (instr Instrument, err error) {
var versionTag uint32
var version int
var name string
if err := binary.Read(r, binary.LittleEndian, &versionTag); err != nil {
return Instrument{}, fmt.Errorf("binary.Read: %w", err)
}
var ok bool
if version, ok = _4klangVersionTags[versionTag]; !ok {
return Instrument{}, fmt.Errorf("unknown 4klang version tag: %d", versionTag)
}
if name, err = read4klangName(r); err != nil {
return Instrument{}, fmt.Errorf("read4klangName: %w", err)
}
var units []Unit
id := 1
m := make(_4klangTargetMap)
if units, err = read4klangUnits(r, version, 0, m, &id); err != nil {
return Instrument{}, fmt.Errorf("read4klangUnits: %w", err)
}
ret := Instrument{Name: name, NumVoices: 1, Units: units}
fix4klangTargets(0, ret, m)
return ret, nil
}
type (
_4klangStackUnit struct {
stack, unit int
}
_4klangTargetMap map[_4klangStackUnit]int
_4klangPorts struct {
UnitType string
PortName [8]string
}
)
const (
_4KLANG_MAX_INSTRS = 16
_4KLANG_MAX_UNITS = 64
_4KLANG_MAX_SLOTS = 16
_4KLANG_MAX_NAME_LEN = 64
)
var (
_4klangVersionTags map[uint32]int = map[uint32]int{
0x31316b34: 11, // 4k11
0x32316b34: 12, // 4k12
0x33316b34: 13, // 4k13
0x34316b34: 14, // 4k14
}
_4klangDelays []int = []int{ // these are the numerators, if denominator is 48, fraction of beat time
4, // 0 = 4.0f * (1.0f/32.0f) * (2.0f/3.0f)
6, // 1 = 4.0f * (1.0f/32.0f),
9, // 2 = 4.0f * (1.0f/32.0f) * (3.0f/2.0f),
8, // 3 = 4.0f * (1.0f/16.0f) * (2.0f/3.0f),
12, // 4 = 4.0f * (1.0f/16.0f),
18, // 5 = 4.0f * (1.0f/16.0f) * (3.0f/2.0f),
16, // 6 = 4.0f * (1.0f/8.0f) * (2.0f/3.0f),
24, // 7 = 4.0f * (1.0f/8.0f),
36, // 8 = 4.0f * (1.0f/8.0f) * (3.0f/2.0f),
32, // 9 = 4.0f * (1.0f/4.0f) * (2.0f/3.0f),
48, // 10 = 4.0f * (1.0f/4.0f),
72, // 11 = 4.0f * (1.0f/4.0f) * (3.0f/2.0f),
64, // 12 = 4.0f * (1.0f/2.0f) * (2.0f/3.0f),
96, // 13 = 4.0f * (1.0f/2.0f),
144, // 14 = 4.0f * (1.0f/2.0f) * (3.0f/2.0f),
128, // 15 = 4.0f * (1.0f) * (2.0f/3.0f),
192, // 16 = 4.0f * (1.0f),
288, // 17 = 4.0f * (1.0f) * (3.0f/2.0f),
256, // 18 = 4.0f * (2.0f) * (2.0f/3.0f),
384, // 19 = 4.0f * (2.0f),
576, // 20 = 4.0f * (2.0f) * (3.0f/2.0f),
72, // 21 = 4.0f * (3.0f/8.0f),
120, // 22 = 4.0f * (5.0f/8.0f),
168, // 23 = 4.0f * (7.0f/8.0f),
216, // 24 = 4.0f * (9.0f/8.0f),
264, // 25 = 4.0f * (11.0f/8.0f),
312, // 26 = 4.0f * (13.0f/8.0f),
360, // 27 = 4.0f * (15.0f/8.0f),
144, // 28 = 4.0f * (3.0f/4.0f),
240, // 29 = 4.0f * (5.0f/4.0f),
336, // 30 = 4.0f * (7.0f/4.0f),
288, // 31 = 4.0f * (3.0f/2.0f),
288, // 32 = 4.0f * (3.0f/2.0f),
}
_4klangUnitPorts []_4klangPorts = []_4klangPorts{
{"", [8]string{"", "", "", "", "", "", "", ""}},
{"envelope", [8]string{"", "", "gain", "attack", "decay", "", "release", ""}},
{"oscillator", [8]string{"", "transpose", "detune", "", "phase", "color", "shape", "gain"}},
{"filter", [8]string{"", "", "", "", "frequency", "resonance", "", ""}},
{"envelope", [8]string{"", "", "drive", "frequency", "", "", "", ""}},
{"delay", [8]string{"pregain", "feedback", "dry", "damp", "", "", "", ""}},
{"", [8]string{"", "", "", "", "", "", "", ""}},
{"", [8]string{"", "", "", "", "", "", "", ""}},
{"pan", [8]string{"panning", "", "", "", "", "", "", ""}},
{"outaux", [8]string{"auxgain", "outgain", "", "", "", "", "", ""}},
{"", [8]string{"", "", "", "", "", "", "", ""}},
{"load", [8]string{"value", "", "", "", "", "", "", ""}},
}
)
func read4klangName(r io.Reader) (string, error) {
var name [_4KLANG_MAX_NAME_LEN]byte
if err := binary.Read(r, binary.LittleEndian, &name); err != nil {
return "", fmt.Errorf("binary.Read: %w", err)
}
n := bytes.IndexByte(name[:], 0)
if n == -1 {
n = _4KLANG_MAX_NAME_LEN
}
return string(name[:n]), nil
}
func read4klangUnits(r io.Reader, version, instrIndex int, m _4klangTargetMap, id *int) (units []Unit, err error) {
numUnits := _4KLANG_MAX_UNITS
if version <= 13 {
numUnits = 32
}
units = make([]Unit, 0, numUnits)
for unitIndex := 0; unitIndex < numUnits; unitIndex++ {
var u []Unit
if u, err = read4klangUnit(r, version); err != nil {
return nil, fmt.Errorf("read4klangUnit: %w", err)
}
if u == nil {
continue
}
m[_4klangStackUnit{instrIndex, unitIndex}] = *id
for i := range u {
u[i].ID = *id
*id++
}
units = append(units, u...)
}
return
}
func read4klangUnit(r io.Reader, version int) ([]Unit, error) {
var unitType byte
if err := binary.Read(r, binary.LittleEndian, &unitType); err != nil {
return nil, fmt.Errorf("binary.Read: %w", err)
}
var vals [15]byte
if err := binary.Read(r, binary.LittleEndian, &vals); err != nil {
return nil, fmt.Errorf("binary.Read: %w", err)
}
if version <= 13 {
// versions <= 13 had 16 unused slots for each unit
if written, err := io.CopyN(io.Discard, r, 16); err != nil || written < 16 {
return nil, fmt.Errorf("io.CopyN: %w", err)
}
}
switch unitType {
case 1:
return read4klangENV(vals, version), nil
case 2:
return read4klangVCO(vals, version), nil
case 3:
return read4klangVCF(vals, version), nil
case 4:
return read4klangDST(vals, version), nil
case 5:
return read4klangDLL(vals, version), nil
case 6:
return read4klangFOP(vals, version), nil
case 7:
return read4klangFST(vals, version), nil
case 8:
return read4klangPAN(vals, version), nil
case 9:
return read4klangOUT(vals, version), nil
case 10:
return read4klangACC(vals, version), nil
case 11:
return read4klangFLD(vals, version), nil
default:
return nil, nil
}
}
func read4klangENV(vals [15]byte, _ int) []Unit {
return []Unit{{
Type: "envelope",
Parameters: map[string]int{
"stereo": 0,
"attack": int(vals[0]),
"decay": int(vals[1]),
"sustain": int(vals[2]),
"release": int(vals[3]),
"gain": int(vals[4]),
},
}}
}
func read4klangVCO(vals [15]byte, version int) []Unit {
v := vals[:8]
var transpose, detune, phase, color, gate, shape, gain, flags, stereo, typ, lfo int
transpose, v = int(v[0]), v[1:]
detune, v = int(v[0]), v[1:]
phase, v = int(v[0]), v[1:]
if version <= 11 {
gate = 0x55
} else {
gate, v = int(v[0]), v[1:]
}
color, v = int(v[0]), v[1:]
shape, v = int(v[0]), v[1:]
gain, v = int(v[0]), v[1:]
flags, _ = int(v[0]), v[1:]
if flags&0x10 == 0x10 {
lfo = 1
}
if flags&0x40 == 0x40 {
stereo = 1
}
switch {
case flags&0x01 == 0x01: // Sine
typ = Sine
if version <= 13 {
color = 128
}
case flags&0x02 == 0x02: // Trisaw
typ = Trisaw
case flags&0x04 == 0x04: // Pulse
typ = Pulse
case flags&0x08 == 0x08: // Noise is handled differently in sointu
return []Unit{{
Type: "noise",
Parameters: map[string]int{
"stereo": stereo,
"shape": shape,
"gain": gain,
},
}}
case flags&0x20 == 0x20: // Gate
color = gate
}
return []Unit{{
Type: "oscillator",
Parameters: map[string]int{
"stereo": stereo,
"transpose": transpose,
"detune": detune,
"phase": phase,
"color": color,
"shape": shape,
"gain": gain,
"type": typ,
"lfo": lfo,
},
}}
}
func read4klangVCF(vals [15]byte, _ int) []Unit {
flags := vals[2]
var stereo, lowpass, bandpass, highpass int
if flags&0x01 == 0x01 {
lowpass = 1
}
if flags&0x02 == 0x02 {
highpass = 1
}
if flags&0x04 == 0x04 {
bandpass = 1
}
if flags&0x08 == 0x08 {
lowpass = 1
highpass = -1
}
if flags&0x10 == 0x10 {
stereo = 1
}
return []Unit{{
Type: "filter",
Parameters: map[string]int{
"stereo": stereo,
"frequency": int(vals[0]),
"resonance": int(vals[1]),
"lowpass": lowpass,
"bandpass": bandpass,
"highpass": highpass,
}},
}
}
func read4klangDST(vals [15]byte, _ int) []Unit {
return []Unit{
{Type: "distort", Parameters: map[string]int{"drive": int(vals[0]), "stereo": int(vals[2])}},
{Type: "hold", Parameters: map[string]int{"holdfreq": int(vals[1]), "stereo": int(vals[2])}},
}
}
func read4klangDLL(vals [15]byte, _ int) []Unit {
var delaytimes []int
var notetracking int
if vals[11] > 0 {
if vals[10] > 0 { // left reverb
delaytimes = []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618}
} else { // right reverb
delaytimes = []int{1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642}
}
} else {
synctype := vals[9]
switch synctype {
case 0:
delaytimes = []int{int(vals[8]) * 16}
case 1: // relative to BPM
notetracking = 2
index := vals[8] >> 2
delaytime := 48
if int(index) < len(_4klangDelays) {
delaytime = _4klangDelays[index]
}
delaytimes = []int{delaytime}
case 2: // notetracking
notetracking = 1
delaytimes = []int{10787}
}
}
return []Unit{{
Type: "delay",
Parameters: map[string]int{
"stereo": 0,
"pregain": int(vals[0]),
"dry": int(vals[1]),
"feedback": int(vals[2]),
"damp": int(vals[3]),
"notetracking": notetracking,
},
VarArgs: delaytimes,
}}
}
func read4klangFOP(vals [15]byte, _ int) []Unit {
var t string
var stereo int
switch vals[0] {
case 1:
t, stereo = "pop", 0
case 2:
t, stereo = "addp", 0
case 3:
t, stereo = "mulp", 0
case 4:
t, stereo = "push", 0
case 5:
t, stereo = "xch", 0
case 6:
t, stereo = "add", 0
case 7:
t, stereo = "mul", 0
case 8:
t, stereo = "addp", 1
case 9:
return []Unit{{Type: "loadnote", Parameters: map[string]int{"stereo": stereo}}, // 4klang loadnote gives 0..1, sointu gives -1..1
{Type: "loadval", Parameters: map[string]int{"value": 128, "stereo": stereo}},
{Type: "addp", Parameters: map[string]int{"stereo": stereo}},
{Type: "gain", Parameters: map[string]int{"stereo": stereo, "gain": 64}}}
default:
t, stereo = "mulp", 1
}
return []Unit{{
Type: t,
Parameters: map[string]int{"stereo": stereo},
}}
}
func read4klangFST(vals [15]byte, _ int) []Unit {
sendpop := 0
if vals[1]&0x40 == 0x40 {
sendpop = 1
}
return []Unit{{
Type: "send",
Parameters: map[string]int{
"amount": int(vals[0]),
"sendpop": sendpop,
"dest_stack": int(vals[2]),
"dest_unit": int(vals[3]),
"dest_slot": int(vals[4]),
"dest_id": int(vals[5]),
}}}
}
func fix4klangTargets(instrIndex int, instr Instrument, m _4klangTargetMap) {
for _, u := range instr.Units {
if u.Type == "send" {
destStack := u.Parameters["dest_stack"]
if destStack == 255 {
destStack = instrIndex
}
fourKlangTarget := _4klangStackUnit{
destStack,
u.Parameters["dest_unit"]}
u.Parameters["target"] = m[fourKlangTarget]
if u.Parameters["dest_id"] < len(_4klangUnitPorts) && u.Parameters["dest_slot"] < 8 {
if u.Parameters["dest_id"] == 4 && u.Parameters["dest_slot"] == 3 { // distortion is split into 2 units
u.Parameters["target"]++
u.Parameters["port"] = 0
} else {
modTarget := _4klangUnitPorts[u.Parameters["dest_id"]]
for i, s := range Ports[modTarget.UnitType] {
if s == modTarget.PortName[u.Parameters["dest_slot"]] {
u.Parameters["port"] = i
break
}
}
}
}
delete(u.Parameters, "dest_stack")
delete(u.Parameters, "dest_unit")
delete(u.Parameters, "dest_slot")
delete(u.Parameters, "dest_id")
}
}
}
func read4klangPAN(vals [15]byte, _ int) []Unit {
return []Unit{{
Type: "pan",
Parameters: map[string]int{
"stereo": 0,
"panning": int(vals[0]),
}}}
}
func read4klangOUT(vals [15]byte, _ int) []Unit {
return []Unit{{
Type: "outaux",
Parameters: map[string]int{
"stereo": 1,
"outgain": int(vals[0]),
"auxgain": int(vals[1])},
}}
}
func read4klangACC(vals [15]byte, _ int) []Unit {
c := 0
if vals[0] != 0 {
c = 2
}
return []Unit{{
Type: "in",
Parameters: map[string]int{"stereo": 1, "channel": c},
}}
}
func read4klangFLD(vals [15]byte, _ int) []Unit {
return []Unit{{
Type: "loadval",
Parameters: map[string]int{"stereo": 0, "value": int(vals[0])},
}}
}

View File

@ -3,23 +3,441 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [0.6.0]
### Added
- An instrument (set of opcodes & accompanying values) can have any number of voices.
- A track can trigger any number of voices (polyphonism).
- Pattern length does not have to be a power of 2.
- Macros for defining patches, so that only the necessary parts of the synth are compiled in.
- Harmonized support for stereo signals: every opcode supports stereo variant.
- New opcodes: bit-crusher, gain, inverse gain, clip, speed (bpm modulation), compressor.
- Support for sample-based oscillators; samples loaded from gm.dls.
- Unison oscillators: multiple copies of the oscillator running sligthly detuned and added up to together.
- Support for 32 and 64 bit builds.
- Regression tests for opcodes, using CTests.
- Switch to CMake for builds.
- Compiling as a static library & an API to call Sointu
- Running all tests (win/linux/mac) in the cloud, using Github workflows
- go: a Go package to call Sointu
- go: Importing and exporting Sointu .asm songs
- go: asmfmt, a command line utility to format/process Sointu .asm song files
- Binary builds for sointu-play from GitHub Actions on all platforms.
([#226][i226])
- Song corpus with songs from real intros for testing size optimizations in
Sointu systematically. ([#227][i227])
- MIDI velocity, keyboard splitting, forcing specific instrument to use
particular MIDI channel, and ability to transpose the incoming note values.
These settings can be configured under instrument properties. ([#124][i124],
[#215][i215], [#221][i221])
- Ability to bind MIDI controllers to specific parameters. The MIDI menu has the
options to bind/unbind parameters. When the user starts binding a parameter,
Sointu waits for the next MIDI Control Change event and binds the currently
selected parameter to that controller. ([#152][i152])
- Plot the envelope shape on top of the oscilloscope when the envelope unit is
selected.
- Spectrum analyzer showing the spectrum. When the user has a filter or belleq
unit selected, it's frequency response is plotted on top. ([#67][i67])
- belleq unit: a bell-shaped second-order filter for equalization. Belleq unit
takes the center frequency, bandwidth (inverse of Q-factor) and gain (+-40
dB). Useful for boosting or reducing specific frequency ranges. Kudos to Reaby
for the initial implementation!
- Multithreaded synths: the user can split the patch up to four threads.
Selecting the thread can be done on the instrument properties pane.
Multithreading works only on the multithreaded synths, selectable from the CPU
panel. Currently the multithreaded rendering has not yet been implemented in
the compiled player and the thread information is disregarded while compiling
the song. ([#199][i199])
- Preset explorer, whichs allows 1) searching the presets by name; 2) filtering
them by category (directory); 3) filtering them by being builtin vs. user;
4) filtering them if they need gm.dls (for Linux/Mac users, who don't have
it); and 5) saving and deleting user presets. ([#91][i91])
- Panic the synth if it outputs NaN or Inf, and handle these more gracefully in
the loudness and peak detector. ([#210][i210])
- More presets from Reaby, and all new and existing presets were normalized
roughly to -12 dBFS true peak. ([#211][i211])
[Unreleased]: https://github.com/vsariola/sointu/compare/4klang-3.11...HEAD
### Fixed
- VSTi queries the host sample rate more robustly. Cubase previously reported
the sample rate as 0 Hz, leading to persistent error message about the sample
rate not being 44100 Hz. ([#222][i222])
- Occasional NaNs in the Trisaw oscillator when color was = 0 or color = 128
- The tracker thought that "sync" unit pops the value from stack, even if the VM
did not, resulting it claiming errors in patches that worked once compiled.
### Changed
- Save only units and comment to instrument files, as we keep all the other
fields while loading a new instrument / preset and the name comes from the
filename.
- Recovery files were moved to `os.UserConfigDir()/sointu/recovery/` instead of
`os.UserConfigDir()/sointu/` so that they don't pollute the main configuration
directory and so that it's easy to delete just the recovery files.
- Tracker model supports now enum-style values, which are integers that have a
name associated with them. These enums are used to display menus where you
select one of the options, for example in the MIDI menu to choose one of the
ports; a context menu in to choose which instrument triggers the oscilloscope;
and a context menu to choose the weighting type in the loudness detector.
- The song panel can scroll if all the widgets don't fit into it
- The provided MacOS executables are now arm64, which means the x86 native
synths are not compiled in.
## [0.5.0]
### BREAKING CHANGES
- BREAKING CHANGE: always first modulate delay time, then apply notetracking. In
a delay unit, modulation adds to the delay time, while note tracking
multiplies it with a multiplier dependent on the note. The order of these
operations was different in the Go VM vs. x86 VM & WebAssembly VM. In the Go
VM, it first modulated, and then applied the note tracking multiplication. In
the two assembly VMs, it first applied the note tracking and then modulated.
Of these two behaviours, the Go VM behaviour made more sense: if you make a
vibrato of +-50 cents for C4, you probably want a vibrato of +-50 cents for C6
also. Thus, first modulating and then applying the note tracking
multiplication is now the behaviour accross all VMs.
- BREAKING CHANGE: the negbandpass and neghighpass parameters of the filter unit
were removed. Setting bandpass or highpass to -1 achieves now the same end
result. Setting both negbandpass and bandpass to 1 was previously a no-op. Old
patch and instrument files are converted to the new format when loaded, but
newer Sointu files should not be compiled with an old version of
sointu-compile.
### Added
- Signal rail that visualizes what happens in the stack, shown on the left side
of each unit in the rack.
- The parameters are now displayed in a grid as knobs, with units of the
instrument going from the top to the bottom. Bezier lines are used to indicate
which sends modulate which ports. ([#173][i173])
- Tabbing works more consistently, with widgets placed in a "tree", and plain
Tab moves to the next widget on the same level or more shallow in the tree,
while ctrl-Tab moves to next widget, regardless of its depth. This allows the
user to quickly move between different panels, but also tabbing into every
tiny widget if needed. Shift-* tab backwards.
- Help menu, with a menu item to show the license in a dialog, and also menu
items to open manual, Github Discussions & Github Issues in a browser
([#196][i196])
- Show CPU load percentage in the song panel ([#192][i192])
- Theme can be user configured, in theme.yml. This theme.yml should be placed in
the usual sointu config directory (i.e.
`os.UserConfigDir()/sointu/theme.yml`). See
[theme.yml](tracker/gioui/theme.yml) for the default theme, and
[theme.go](tracker/gioui/theme.go) for what can be changed.
- Ctrl + scroll wheel adjusts the global scaling of the GUI ([#153][i153])
- The loudness detection supports LUFS, A-weighting, C-weighting or
RMS-weighting, and peak detection supports true peak or sample peak detection.
The loudness and peak values are displayed in the song panel ([#186][i186])
- Oscilloscope to visualize the outputted waveform ([#61][i61])
- Toggle button to keep instruments and tracks linked, and buttons to split
instruments and tracks with more than 1 voice into parallel ones
([#163][i163], [#157][i157])
- Mute and solo toggles for instruments ([#168][i168])
- Many units (e.g. envelopes, oscillators and compressors) display values dB
- Dragging mouse to select rectangles in the tables
- The standalone tracker can open a MIDI port for receiving MIDI notes
([#166][i166])
- The note editor has a button to allow entering notes by MIDI. ([#170][i170])
- Units can have comments, to make it easier to distinguish between units of
same type within an instrument and to use these as subsection titles.
([#114][i114])
- A toggle button for copying non-unique patterns before editing. When enabled
and if the pattern is used in multiple places, the pattern is copied first.
([#77][i77])
- User can define own keybindings in `os.UserConfigDir()/sointu/keybindings.yml`
([#94][i94], [#151][i151])
- User can define preferred window size in
`os.UserConfigDir()/sointu/preferences.yml` ([#184][i184])
- A small number above the instrument name identifies the MIDI channel /
instrument number, with numbering starting from 1 ([#154][i154])
- The filter unit frequency parameter is displayed in Hz, corresponding roughly
to the resonant frequency of the filter ([#158][i158])
- Include version info in the binaries, as given be `git describe`. This version
info is shown as a label in the tracker and can be checked with `-v` flag in
the command line tools.
- Performance improvement: values needed by the UI that are derived from the
score or patch are cached when score or patch changes, so they don't have to
be computed every draw. ([#176][i176])
### Fixed
- Tooltips will be hidden after certain amount of time has passed, to ensure
that the tooltips don't stay around ([#141][i141])
- Loading instrument forgot to close the file (in model.ReadInstrument)
- We try to honor the MIDI event time stamps, so that the timing between MIDI
events (as reported to us by RTMIDI) will be correct.
- When unmarshaling the recovery file, the unit parameter maps were "merged"
with the existing parameter maps, instead of overwriting. This created units
with unnecessary parameters, which was harmless, but would cause a warning to
the user.
- When changing a nibble of a hexadecimal note, the note played was the note
before changing the nibble
- Clicking on low nibble or high nibble of a hex track selects that nibble
([#160][i160])
- If units have useless parameters in their parameter maps, from bugs or from a
malformed yaml file, they are removed and user is warned about it
- Pressing `a` or `1` when editing note values in hex mode created a note off
line ([#162][i162])
- Warn about plugin sample rate being different from 44100 only after
ProcessFloatFunc has been called, so that host has time to set the sample rate
after initialization.
- Crashes with sample-based oscillators in the 32-bit library, as the pointer to
sample-table (edi) got accidentally overwritten by detune
- Sample-based oscillators could hard crash if a x87 stack overflow happened
when calculating the current position in the sample ([#149][i149])
- Numeric updown widget calculated dp-to-px conversion incorrectly, resulting in
wrong scaling ([#150][i150])
- Empty patch should not crash the native synth ([#148][i148])
- sointu-play allows choosing between the synths, assuming it was compiled with
`-tags=native`
- Most buttons never gain focus, so that clicking a button does not stop
whatever the user was currently doing and so that the user does not
accidentally trigger the buttons by having them focused and e.g. hitting space
([#156][i156])
### Changed
- When saving instrument to a file, the instrument name is not saved to the name
field, as Sointu will anyway use the filename as the instrument's name when it
is loaded.
- Native version of the tracker/VSTi was removed. Instead, you can change
between the two versions of the synth on the fly, by clicking on the "Synth"
option under the CPU group in the song panel ([#200][i200])
- Send amount defaults to 64 = 0.0 ([#178][i178])
- The maximum number of delaylines in the native synth was increased to 128,
with slight increase in memory usage ([#155][i155])
- The numeric updown widget has a new appearance.
- The draggable UI splitters snap more controllably to the window edges.
- New & better presets, organized by their type to subfolders (thanks Reaby!)
([#136][i136])
- Presets get their name by concatenating their subdirectory path (with path
separators replaced with spaces) to their filename
- The keyboard shortcuts are now again closer to what they were old trackers
([#151][i151])
- The stand-alone apps now output floating point sound, as made possible by
upgrading oto-library to latest version. This way the tracker sound output
matches the compiled output better, as usually compiled intros output sound in
floating point. This might be important if OS sound drivers apply some audio
enhancemenets e.g. compressors to the audio.
## [0.4.1]
### Added
- Clicking the parameter slider also selects that parameter ([#112][i112])
- The vertical and horizontal split bars indicate with a cursor that they can be
resized ([#145][i145])
### Fixed
- When adding a unit on the last row of the unit list, the editor for entering
the type of the unit by text did gain focus.
- When inputting a note to the note editor, advance the cursor by step
([#144][i144])
- When loading an instrument, make sure the total number of voices does not go
over the maximum number allowed by vm, and make sure a loaded instrument has
at least 1 voice
- Potential ID collisions when clearing unit or pasteing instruments
- Assign new IDs to loaded instruments, and fix ID collisions in case they
somehow still appear ([#146][i146])
- In x86 templates, do not optimize away phase modulations when unisons are used
even if all phase inputs are zeros, as unisons use the phase modulation
mechanism to offset the different oscillators
- Do not include delay times in the delay time table if the delay unit is
disabled ([#139][i139])
- Moved the error and warning popups slightly up so they don't block the unit
control buttons ([#142][i142])
### Changed
- Do not automatically wrap around the song when playing as it was usually
unwanted behaviour. There is already the looping mechanism if the user really
wants to loop the song forever.
## [0.4.0]
### Added
- User can drop preset instruments into `os.UserConfigDir()/sointu/presets/` and
they appear in the list of presets next time sointu is started.
([#125][i125])
- Ability to loop certain section of the song when playing. The loop can be set
by using the toggle button in the song panel, or by hitting Ctrl+L.
([#128][i128])
- Disable units temporarily. The disabled units are shown in gray and are not
compiled into the patch and are considered for all purposes non-existent.
Hitting Ctrl-D disables/re-enables the selected unit(s). The yaml file has
field `disabled: true` for the unit. ([#116][i116])
- Passing a file name on command line immediately tries loading that file ([#122][i122])
- Massive rewrite of the GUI, in particular allowing better copying, pasting and
scrolling of table-based data (order list and note data).
- Dbgain unit, which allows defining the gain in decibels (-40 dB to +40dB)
- `+` and `-` keys add/subtract values in order editor and pattern editor
([#65][i65])
- The function `su_power` is exported so people can reuse it in the main code;
however, as it assumes the parameter passed in st0 on the x87 stack and
similarly returns it value in st0 on the x87 stack, to my knowledge there is
no calling convention that would correspond this behaviour, so you need to
define a header for it yourself and take care of putting the float value on
x87 stack.
### Fixed
- Loading a preset did not update the IDs of the newly loaded instrument,
causing ID collisions and sends target wrong units.
- The x87 native filter unit was denormalizing and eating up a lot of CPU ([#68][i68])
- Modulating delaytime in wasm could crash, because delay time was converted to
int with i32.trunc_f32_u. Using i32.trunc_f32_s fixed this.
- When recording notes from VSTI, no track was created for instruments that had
no notes triggered, resulting in misalignment of the tracks from instruments.
- 32-bit su_load_gmdls clobbered ebx, even though __stdcall demands it to be not
touched ([#130][i130])
- Spaces are allowed in instrument names ([#120][i120])
- Fixed the dropdown for targeting sends making it impossible to choose certain
ops. This was done just by reducing the default height of popup menus so they
fit on screen ([#121][i121])
- Warn user about sample rate being other than 44100 Hz, as this lead to weird
behaviour. Sointu assumes the samplerate always to be 44100 Hz. ([#129][i129])
### Changed
- The scroll wheel behavior for unit integer parameters was flipped: scrolling
up now increases the value, while scrolling down decreases the value. It was
vice versa. ([#112][i112])
## [0.3.0]
### Added
- Scroll bars to menus, shown when a menu is too long to fit.
- Save the GUI state periodically to a recovery file and load it on
startup of the app, if present. The recovery files are located in the
app config directory (e.g. AppData/Roaming/Sointu on Windows).
- Save the VSTI GUI state to the DAW project file, through GetChunk /
SetChunk mechanisms.
- Instrument presets. The presets are embedded in the executable and
there's a button to open a menu to load one of the presets.
- Frequency modulation target for oscillator, as it was in 4klang
- Reverb preset settings for a delay unit, with stereo, left and right
options
### Fixed
- Crash when running more than one sointu VSTI plugins in parallel
- The scroll bars move in sync with the cursor.
- The stereo version of delay in the go virtual machine (executables / plugins
not ending with -native) applied the left delay taps on the right channel, and
the right delay taps on the left channel.
- The sointu-vsti-native plugin has different plugin ID and plugin name
to not confuse it with the non-native one
- The VSTI waits for the gioui actually have quit when closing the
plugin
### Changed
- BREAKING CHANGE: The meaning of default modulation mode ("auto") has
been changed for cross-instrument modulations: it now means "all"
voices, instead of first voice (which was redundant, as it was same as
defining voice = 0). This means that for cross-instrument modulations,
one "all vocies" send gets actually compiled into multiple sends, one
for each targeted voice. For intra-instrument modulations, the meaning
stays the same, but the label was changed to "self", to highlight that
this means the voice modulates only itself and not other voices.
## [0.2.0]
### Added
- Saving and loading instruments
- Comment field to instruments
- Ability to reorder tracks
- Add menu command to delete all unused data from song file
- Ability to search a unit by typing its name
- Ability to run sointu as a vsti plugin, inside vsti host
- Ability to lock delay relative to beat duration
- Ability to import 4klang patches (.4kp) and instruments (.4ki)
- The repository has example instruments, including all patches and
instruments from 4klang
- The compiler templates are embedded in the sointu-compile, so no
installation is needed beyond copying sointu-compile to PATH
- Ability to select multiple units and cut, copy & paste them
- Mousewheel adjusts unit parameters
- Tooltips to many buttons
- Support for gm.dls samples in the go-written virtual machine
- x86 and C written examples how to play a sointu song on various
platforms. On Windows, the examples can optionally be linked with
Crinkler to get Crinkler reports.
### Fixed
- Unnamed instruments with multiple voices caused crashes
- In the native version, exceeding the 64 delaylines caused crashes
- wat2wasm nowadays uses funcref instead of anyfunc
- In the WebAssembly core, $WRK was messed after stereo oscillators,
making modulations not work
- The Webassembly implementation of mono version of the "out" unit
### Changed
- The release flag in the voice is now a sustain flag i.e. the logic has
been inverted. This was done so that when the synth is initialized
with zeros, all voices start with sustain = 0 i.e. in released state.
- The crush resolution is now in bits instead of linear range; this is a
breaking change and changes the meaning of the resolution values. But
now there are more usable values in the resolution.
## [0.1.0]
### Added
- An instrument (set of opcodes & accompanying values) can have any
number of voices.
- A track can trigger any number of voices, releasing the previous when
new one is triggered.
- Pattern length does not have to be a power of 2.
- Only the necessary opcodes and functions of the synth are compiled in the final executable.
- Harmonized support for stereo signals: every opcode supports stereo
variant.
- New opcodes: crush, gain, inverse gain, clip, speed (bpm modulation),
compressor.
- Support for sample-based oscillators (samples loaded from gm.dls).
- Unison oscillators: multiple copies of the oscillator running with
different detuning and added up to together.
- Support for 32 and 64 bit builds.
- Support different platforms: Windows, Linux and Mac (Intel).
- Experimental support for compiling songs into WebAssembly.
- Switch to CMake for builds.
- Regression tests for every VM instruction, using CTests.
- Compiling as a static library & an API to call Sointu
- Running all tests (win/linux/mac/wasm) in the cloud, using Github
workflows
- Tools written in Go-lang:
- a tracker for composing songs as .yml
- a command line utility to convert .yml songs to .asm
- a command line utility to play the songs on command line
[Unreleased]: https://github.com/vsariola/sointu/compare/v0.6.0...HEAD
[0.6.0]: https://github.com/vsariola/sointu/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/vsariola/sointu/compare/v0.4.1...v0.5.0
[0.4.1]: https://github.com/vsariola/sointu/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/vsariola/sointu/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/vsariola/sointu/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/vsariola/sointu/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
[i61]: https://github.com/vsariola/sointu/issues/61
[i65]: https://github.com/vsariola/sointu/issues/65
[i67]: https://github.com/vsariola/sointu/issues/67
[i68]: https://github.com/vsariola/sointu/issues/68
[i77]: https://github.com/vsariola/sointu/issues/77
[i91]: https://github.com/vsariola/sointu/issues/91
[i94]: https://github.com/vsariola/sointu/issues/94
[i112]: https://github.com/vsariola/sointu/issues/112
[i114]: https://github.com/vsariola/sointu/issues/114
[i116]: https://github.com/vsariola/sointu/issues/116
[i120]: https://github.com/vsariola/sointu/issues/120
[i121]: https://github.com/vsariola/sointu/issues/121
[i122]: https://github.com/vsariola/sointu/issues/122
[i124]: https://github.com/vsariola/sointu/issues/124
[i125]: https://github.com/vsariola/sointu/issues/125
[i128]: https://github.com/vsariola/sointu/issues/128
[i129]: https://github.com/vsariola/sointu/issues/129
[i130]: https://github.com/vsariola/sointu/issues/130
[i136]: https://github.com/vsariola/sointu/issues/136
[i139]: https://github.com/vsariola/sointu/issues/139
[i141]: https://github.com/vsariola/sointu/issues/141
[i142]: https://github.com/vsariola/sointu/issues/142
[i144]: https://github.com/vsariola/sointu/issues/144
[i145]: https://github.com/vsariola/sointu/issues/145
[i146]: https://github.com/vsariola/sointu/issues/146
[i148]: https://github.com/vsariola/sointu/issues/148
[i149]: https://github.com/vsariola/sointu/issues/149
[i150]: https://github.com/vsariola/sointu/issues/150
[i151]: https://github.com/vsariola/sointu/issues/151
[i152]: https://github.com/vsariola/sointu/issues/152
[i153]: https://github.com/vsariola/sointu/issues/153
[i154]: https://github.com/vsariola/sointu/issues/154
[i155]: https://github.com/vsariola/sointu/issues/155
[i156]: https://github.com/vsariola/sointu/issues/156
[i157]: https://github.com/vsariola/sointu/issues/157
[i158]: https://github.com/vsariola/sointu/issues/158
[i160]: https://github.com/vsariola/sointu/issues/160
[i162]: https://github.com/vsariola/sointu/issues/162
[i163]: https://github.com/vsariola/sointu/issues/163
[i166]: https://github.com/vsariola/sointu/issues/166
[i168]: https://github.com/vsariola/sointu/issues/168
[i170]: https://github.com/vsariola/sointu/issues/170
[i173]: https://github.com/vsariola/sointu/issues/173
[i176]: https://github.com/vsariola/sointu/issues/176
[i178]: https://github.com/vsariola/sointu/issues/178
[i184]: https://github.com/vsariola/sointu/issues/184
[i186]: https://github.com/vsariola/sointu/issues/186
[i192]: https://github.com/vsariola/sointu/issues/192
[i196]: https://github.com/vsariola/sointu/issues/196
[i199]: https://github.com/vsariola/sointu/issues/199
[i200]: https://github.com/vsariola/sointu/issues/200
[i210]: https://github.com/vsariola/sointu/issues/210
[i211]: https://github.com/vsariola/sointu/issues/211
[i215]: https://github.com/vsariola/sointu/issues/215
[i221]: https://github.com/vsariola/sointu/issues/221
[i222]: https://github.com/vsariola/sointu/issues/222
[i226]: https://github.com/vsariola/sointu/issues/226
[i227]: https://github.com/vsariola/sointu/issues/227

View File

@ -26,6 +26,8 @@ endif()
IF(APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-no_pie")
# https://stackoverflow.com/questions/69803659/what-is-the-proper-way-to-build-for-macos-x86-64-using-cmake-on-apple-m1-arm
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE INTERNAL "" FORCE)
endif()
find_program(GO NAMES go)
@ -62,8 +64,8 @@ else()
endif()
# the tests include the entire ASM but we still want to rebuild when they change
file(GLOB x86templates ${PROJECT_SOURCE_DIR}/templates/amd64-386/*.asm)
file(GLOB wasmtemplates ${PROJECT_SOURCE_DIR}/templates/wasm/*.wat)
file(GLOB x86templates "${PROJECT_SOURCE_DIR}/vm/compiler/templates/amd64-386/*.asm")
file(GLOB wasmtemplates "${PROJECT_SOURCE_DIR}/vm/compiler/templates/wasm/*.wat")
file(GLOB sointusrc "${PROJECT_SOURCE_DIR}/*.go")
file(GLOB compilersrc "${PROJECT_SOURCE_DIR}/compiler/*.go")
file(GLOB compilecmdsrc "${PROJECT_SOURCE_DIR}/cmd/sointu-compile/*.go")
@ -83,27 +85,34 @@ set(sointuasm sointu.asm)
# Build sointu-cli only once because go run has everytime quite a bit of delay when
# starting
add_custom_command(
OUTPUT
"${compilecmd}"
COMMAND
${GO} build -o "${compilecmd}" ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go
DEPENDS ${x86templates} ${wasmtemplates} ${sointusrc} ${compilersrc} ${compilecmdsrc}
)
add_custom_target(
sointu-compiler
COMMAND ${GO} build -o ${compilecmd} ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go
SOURCES "${sointusrc}" "${compilersrc}" "${compilecmdsrc}"
DEPENDS ${compilecmd}
)
add_custom_command(
OUTPUT ${sointuasm}
COMMAND ${compilecmd} -arch=${arch} -a -o ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS "${templates}" sointu-compiler
DEPENDS ${compilecmd}
)
add_library(${STATICLIB} ${sointuasm})
set_target_properties(${STATICLIB} PROPERTIES LINKER_LANGUAGE C)
target_include_directories(${STATICLIB} INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
# We should put examples here
# add_subdirectory(examples)
# Examples are now available.
add_subdirectory(examples)
# Testing only available if this is the main app
# Emergency override 4KLANG_CMAKE_BUILD_TESTING provided as well
if((CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME OR SOINTU_CMAKE_BUILD_TESTING) AND BUILD_TESTING)
add_subdirectory(tests)
endif()
endif()

View File

@ -1,7 +1,7 @@
MIT License
Copyright (c) 2018 Dominik Ries
(c) 2020 Veikko Sariola
(c) 2020-2025 Veikko Sariola, moitias, qm210, LeStahl, petersalomonsen, anticore, reaby
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

541
README.md
View File

@ -1,13 +1,45 @@
# Sointu
![Tests](https://github.com/vsariola/sointu/workflows/Tests/badge.svg)
![Binaries](https://github.com/vsariola/sointu/workflows/Binaries/badge.svg)
A cross-architecture and cross-platform modular software synthesizer for small
intros, forked from [4klang](https://github.com/hzdgopher/4klang). Targetable
architectures include 386, amd64, and WebAssembly; targetable platforms include
Windows, Mac, Linux (and related) + browser.
Pull requests / suggestions / issues welcome, through Github! You can also
contact me through email (firstname.lastname@gmail.com).
- [User manual](https://github.com/vsariola/sointu/wiki) is in the Wiki
- [Discussions](https://github.com/vsariola/sointu/discussions) is for asking
help, sharing patches/instruments and brainstorming ideas
- [Issues](https://github.com/vsariola/sointu/issues) is for reporting bugs
Installation
------------
You can either:
1) Download the latest build from the master branch from the
[actions](https://github.com/vsariola/sointu/actions). Find workflow
"Binaries" and scroll down for .zip files containing the artifacts.
**Note:** You have to be logged into Github to download artifacts!
or
2) Download one of the tagged
[releases](https://github.com/vsariola/sointu/releases).
In both cases, you can then just run one of the executables (no need to install
anything); or in the case of the VST plugins library files, copy them wherever
you keep you VST2 plugins.
The pre 1.0 version release 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 configuration and recovery data files in
OS-specific folders e.g. `AppData/Roaming/Sointu` on Windows. For clean
uninstall, delete also this folder. See
[here](https://pkg.go.dev/os#UserConfigDir) where to find those folders on other
platforms.
Summary
-------
@ -24,20 +56,17 @@ 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 app for composing music, written in
[go](https://golang.org/). The app is still heavily work in progress. The app
exports the projects as .yml files. There are two versions of the app:
[cmd/sointu-track/](sointu-track), using a plain Go VM bytecode interpreter,
and [cmd/sointu-nativetrack/](sointu-nativetrack), using cgo to bridge calls
to the Sointu compiled VM. The former should be highly portable, the latter
currently works only on x86/amd64 platforms.
- A cross-platform synth-tracker that runs as either VSTi or stand-alone
app for composing music, written in [go](https://golang.org/). 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](https://www.nasm.us/) or
[yasm](https://yasm.tortall.net). For browsers, the resulting .wat can be
compiled by [wat2wasm](https://github.com/WebAssembly/wabt).
resulting .asm can be then compiled by [nasm](https://www.nasm.us/). For
browsers, the resulting .wat can be compiled by
[wat2wasm](https://github.com/WebAssembly/wabt).
This is how the current prototype tracker looks like:
This is how the current prototype app looks like:
![Screenshot of the tracker](screenshot.png)
@ -49,24 +78,196 @@ listed below.
### Sointu-track
This version of the tracker is the version that uses the bytecode interpreter
written in plain Go. Running the tracker:
This is the stand-alone version of the synth-tracker. Sointu-track uses
the [gioui](https://gioui.org/) for the GUI and [oto](https://github.com/hajimehoshi/oto)
for the audio, so the portability is currently limited by these.
#### Prerequisites
- [go](https://golang.org/)
- If you want to also use the x86 assembly written synthesizer, to test that the
patch also works once compiled:
- Follow the instructions to build the [x86 native virtual machine](#native-virtual-machine)
before building the tracker.
- cgo compatible compiler e.g. [gcc](https://gcc.gnu.org/). On windows, you
best bet is [MinGW](http://www.mingw.org/). 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 the tracker:
#### Building an executable
```
go build -o sointu-track cmd/sointu-track/main.go
go build -o sointu-track.exe cmd/sointu-track/main.go
```
On windows, replace `-o sointu-track` with `-o sointu-track.exe`.
On other platforms than Windows, replace `-o sointu-track.exe` with
`-o sointu-track`.
Sointu-track uses the [gioui](https://gioui.org/) for the GUI and
[oto](https://github.com/hajimehoshi/oto) for the audio, so the portability is
currently limited by these.
If you want to include the [x86 native virtual machine](#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](https://golang.org/)
- cgo compatible compiler e.g. [gcc](https://gcc.gnu.org/). On windows, you best
bet is [MinGW](http://www.mingw.org/). 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 build the VSTI with the native x86 assembly written synthesizer:
- Follow the instructions to build the [x86 native virtual machine](#native-virtual-machine)
before building the plugin itself
#### 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](https://github.com/pipelined/vst2); otherwise, you will get a lot of
build errors.
Add `-tags=native,plugin` to use the [x86 native virtual
machine](#native-virtual-machine) instead of the virtual machine written in Go.
### Sointu-compile
The command line interface to it is [sointu-compile](cmd/sointu-compile/main.go)
and the actual code resides in the [compiler](vm/compiler/) package, which is an
ordinary [go](https://golang.org/) 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 -arch=386 tests/test_chords.yml
nasm -f win32 test_chords.asm
```
WebAssembly example:
```
sointu-compile -arch=wasm tests/test_chords.yml
wat2wasm 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](https://github.com/LeStahL/sointu-executable-msx) 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](https://github.com/runestubbe/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. With
the latest Go compiler, the native virtual machine is actually slower than the
Go-written one, but importantly, the native virtual machine allows you to test
that the patch also works within the stack limits of the x87 virtual machine,
which is the VM used in the compiled intros. In the tracker/VSTi, you can switch
between the native synth and the Go synth under the CPU panel in the Song
settings.
Before you can actually run it, you need to build the bridge using CMake (thus,
***this will not work with go get***).
#### Prerequisites
- [CMake](https://cmake.org)
- [nasm](https://www.nasm.us/)
- *cgo compatible compiler* e.g. [gcc](https://gcc.gnu.org/). On windows, you
best bet is [MinGW](http://www.mingw.org/). We use the
[tdm-gcc](https://jmeubank.github.io/tdm-gcc/)
The last point is because the command line player and the tracker use
[cgo](https://golang.org/cmd/cgo/) to interface with the synth core, which is
compiled into a library. The cgo bridge resides in the package
[bridge](vm/compiler/bridge/).
#### Building
Assuming you are using [ninja](https://ninja-build.org/):
```
mkdir build
cd build
cmake .. -GNinja
ninja sointu
```
> :warning: *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
```
> :warning: 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
@ -76,53 +277,35 @@ currently limited by these.
> opcodes). In future, the app should give warnings if the user is about to
> exceed the capabilities of a target platform.
### Compiler
> :warning: **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](https://tortall.lighthouseapp.com/projects/78676/tickets/274-bss-problem-with-windows-win64)
> and the fix
> [here](https://github.com/yasm/yasm/commit/1910e914792399137dec0b047c59965207245df5).
> 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.
The command line interface to it is [sointu-compile](cmd/sointu-compile/main.go)
and the actual code resides in the [compiler](vm/compiler/) package, which is an
ordinary [go](https://golang.org/) package with no other tool dependencies.
### Tests
Running the compiler:
There are [regression tests](tests/) that are built as executables,
testing that they work the same way when you would link them in an
intro.
```
go run cmd/sointu-compile/main.go
```
#### Prerequisites
Building the compiler:
```
go build -o sointu-compile cmd/sointu-compile/main.go
```
On windows, replace `-o sointu-compile` with `-o sointu-compile.exe`.
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
```
### Building and running the tests as executables
Building the [regression tests](tests/) as executables (testing that they work
the same way when you would link them in an intro) requires:
- [go](https://golang.org/)
- [CMake](https://cmake.org) with CTest
- [nasm](https://www.nasm.us/) or [yasm](https://yasm.tortall.net)
- [nasm](https://www.nasm.us/)
- 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.
For example, using [ninja](https://ninja-build.org/):
#### Building and running
Assuming you are using [ninja](https://ninja-build.org/):
```
mkdir build
@ -142,93 +325,26 @@ 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.
### Native bridge & sointu-nativetrack
The native bridge allows Go to call the sointu compiled virtual machine, through
cgo, instead of using the Go written bytecode interpreter. It's likely slightly
faster than the interpreter. The version of the tracker that uses the native
bridge is [sointu-nativetrack](cmd/sointu-nativetrack/). Before you can actually
run it, you need to build the bridge using CMake (thus, the nativetrack does not
work with go get)
Building the native bridge requires:
- [go](https://golang.org/)
- [CMake](https://cmake.org)
- [nasm](https://www.nasm.us/) or [yasm](https://yasm.tortall.net)
- *cgo compatible compiler* e.g. [gcc](https://gcc.gnu.org/). On windows, you
best bet is [MinGW](http://www.mingw.org/). We use the
[tdm-gcc](https://jmeubank.github.io/tdm-gcc/)
The last point is because the command line player and the tracker use
[cgo](https://golang.org/cmd/cgo/) to interface with the synth core, which is
compiled into a library. The cgo bridge resides in the package
[bridge](vm/compiler/bridge/).
> :warning: *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.
So, to build the library, run (this example is using
[ninja](https://ninja-build.org/) for the build; adapt for other build tools
accordingly):
```
mkdir build
cd build
cmake .. -GNinja
ninja sointu
```
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 cmd/sointu-play/main.go tests/test_chords.yml
```
Run the tracker using the native bridge
```
go run cmd/sointu-nativetrack/main.go
```
> :warning: **If you are using MinGW and Yasm**: Yasm 1.3.0 (currently still the
> latest stable release) and GNU linker do not play nicely along, trashing the
> BSS layout. See
> [here](https://tortall.lighthouseapp.com/projects/78676/tickets/274-bss-problem-with-windows-win64)
> and the fix
> [here](https://github.com/yasm/yasm/commit/1910e914792399137dec0b047c59965207245df5).
> Use a newer nightly build of yasm that includes the fix. The linker had placed
> our synth object overlapping with DLL call addresses; very funny stuff to
> debug.
> :warning: The sointu-nativetrack cannot be used with the syncs at the moment.
> For syncs, use the Go VM (sointu-track).
### Building and running the WebAssembly tests
### WebAssembly tests
These are automatically invoked by CTest if [node](https://nodejs.org) and
[wat2wasm](https://github.com/WebAssembly/wabt) 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).
bpm (proper triplets!), compressor (can be used for side-chaining), bell eq
(for more versatile EQuing of the sounds).
- **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](templates/) and the compiler lives
"macros". The templates are [here](vm/compiler/templates/) and the compiler lives
[here](vm/compiler/).
- **Tracker**. Written in go. A crude version exists.
- **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
@ -242,9 +358,9 @@ New features since fork
opcodes. So, you can have a single instrument with three voices, and three
tracks that use this instrument, to make chords. See
[here](tests/test_chords.yml) for an example and
[here](templates/amd64-386/patch.asm) 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.
[here](vm/compiler/templates/amd64-386/patch.asm) 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
@ -253,21 +369,21 @@ New features since fork
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.
- **Reasonably 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](tests/test_oscillat_trisaw.yml) for an example). The sointu-cli
compiler 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-cli & nasm.
[here](tests/test_oscillat_trisaw.yml) 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
@ -278,7 +394,7 @@ New features since fork
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 compressably.
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
@ -297,12 +413,13 @@ New features since fork
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**. Reading
gm.dls is obviously Windows only, but with some effort the sample mechanism
can be used also without it, in case you are working on a 64k and have some
kilobytes to spare. See [this example](tests/test_oscillat_sample.yml), and
this go generate [program](cmd/sointu-generate/main.go) parses the gm.dls
file and dumps the sample offsets from it.
- **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](tests/test_oscillat_sample.yml),
and this go generate [program](cmd/sointu-generate/main.go) 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
@ -313,54 +430,9 @@ New features since fork
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.
- **Using Sointu as a sync-tracker**. Similar to [GNU
Rocket](https://github.com/yupferris/gnurocket), but (ab)using the tracker
we already have for music. We use the Go "rpc" package to send current sync
values from the new "sync" opcode + optionally the current fractional row
the song is on. The syncs are saved every 256th sample (approximately 172
Hz). For 4k intro development, the idea is to write a debug version of the
intro that merely loads the shader and listens to the RPC messages, and then
draws the shader with those as the uniforms. Then, during the actual 4k
intro, one can get the sync data from Sointu: if the song uses syncs,
su_render_song writes the syncs to a float array. During each time step, a
slice of this array can be sent to the shader as a uniform float array. A
track with two voices, triggering an instrument with a single envelope and a
slow filter can even be used as a cheap smooth interpolation mechanism,
provided the syncs are added to each other in the shader.
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.
- **Ability to run Sointu as a DAW plugin (VSTi3)**. Now that Renoise supports
VSTi3, there's no fundamental objection to compiling Sointu as a VSTi3. But
don't expect it any soon; I need to digest the idea of having to learn the
horrors of the VSTi3 C++ API.
- **A bytecode interpreter written in pure go**. With the latest Go compiler,
it's slightly faster hand-written one using x87 opcodes. With this, the
tracker is ultraportable and does not need cgo calls.
Design philosophy
-----------------
@ -382,8 +454,8 @@ Design philosophy
- 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](https://github.com/armak/Leviathan-2.0) and observing how the
optimizations affect the byte size.
[one of the examples](examples/code/C) and observing how the optimizations
affect the byte size.
Background and history
----------------------
@ -415,19 +487,56 @@ so I thought it would fun to learn some Finnish for a change. And
Prods using Sointu
------------------
[Adam](https://github.com/vsariola/adam) by brainlez Coders! - My first test-driving of Sointu. Some ideas how to integrate Sointu to the build chain.
- [Adam](https://github.com/vsariola/adam) by brainlez Coders! My first
test-driving of Sointu. The repository has some ideas how to integrate
Sointu to the build chain.
- [Roadtrip](https://www.pouet.net/prod.php?which=94105) by LJ & Virgill
- [|](https://www.pouet.net/prod.php?which=94721) by epoqe. Likely the first
Linux 4k intro using sointu.
- [Physics Girl St.](https://www.pouet.net/prod.php?which=94890) by Team210
- [Delusions of mediocrity](https://www.pouet.net/prod.php?which=95222) by
mrange & Virgill
- [Xorverse](https://www.pouet.net/prod.php?which=95221) by Alcatraz
- [l'enveloppe](https://www.pouet.net/prod.php?which=95215) by Team210 & epoqe
- [Phosphorescent Purple Pixel Peaks](https://www.pouet.net/prod.php?which=96198) by mrange & Virgill
- [21](https://demozoo.org/music/338597/) by NR4 / Team210
- [Tausendeins](https://www.pouet.net/prod.php?which=96192) by epoqe & Team210
- [Radiant](https://www.pouet.net/prod.php?which=97200) by Team210
- [Aurora Florae](https://www.pouet.net/prod.php?which=97516) by Team210 and
epoqe
- [Night Ride](https://www.pouet.net/prod.php?which=98212) by Ctrl-Alt-Test &
Alcatraz
- [Bicolor Challenge](https://demozoo.org/competitions/19410/) with [Sointu
song](https://files.scene.org/view/parties/2024/deadline24/bicolor_challenge/wayfinder_-_bicolor_soundtrack.zip)
provided by wayfinder
- [napolnitel](https://www.pouet.net/prod.php?which=104336) by jetlag
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](LICENSE) for more information.
Contact
-------
Veikko Sariola - pestis_bc on Demoscene discord - firstname.lastname@gmail.com
Project Link: [https://github.com/vsariola/sointu](https://github.com/vsariola/sointu)
Credits
-------
The original 4klang was developed by Dominik Ries
([gopher](https://github.com/hzdgopher/4klang)) and Paul Kraus (pOWL) of
Alcatraz. :heart:
The original 4klang: Dominik Ries ([gopher/Alcatraz](https://github.com/hzdgopher/4klang))
& Paul Kraus (pOWL/Alcatraz) :heart:
Sointu was initiated by Veikko Sariola (pestis/bC!).
Apollo/bC! put the project on the path to Go, and wrote the prototype of the
tracker GUI.
PoroCYon's [4klang fork](https://github.com/PoroCYon/4klang) inspired the macros
for better cross-platform support.
Sointu: Veikko Sariola (pestis/bC!), [Apollo/bC!](https://github.com/moitias),
[NR4/Team210](https://github.com/LeStahL/),
[PoroCYon](https://github.com/PoroCYon/4klang),
[kendfss](https://github.com/kendfss), [anticore](https://github.com/anticore),
[qm210](https://github.com/qm210), [reaby](https://github.com/reaby)

304
audio.go
View File

@ -1,11 +1,303 @@
package sointu
type AudioSink interface {
WriteAudio(buffer []float32) error
Close() error
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"time"
)
type (
// AudioBuffer is a buffer of stereo audio samples of variable length, each
// sample represented by [2]float32. [0] is left channel, [1] is right
AudioBuffer [][2]float32
CloserWaiter interface {
io.Closer
Wait()
}
// AudioContext represents the low-level audio drivers. There should be at
// most one AudioContext at a time. The interface is implemented at least by
// oto.OtoContext, but in future we could also mock it.
//
// AudioContext is used to play one or more AudioSources. Playing can be
// stopped by closing the returned io.Closer.
AudioContext interface {
Play(r AudioSource) CloserWaiter
}
// AudioSource is an function for reading audio samples into an AudioBuffer.
// Returns error if the buffer is not filled.
AudioSource func(buf AudioBuffer) error
BufferSource struct {
buffer AudioBuffer
pos int
}
// Synth represents a state of a synthesizer, compiled from a Patch.
Synth interface {
// Render tries to fill a stereo signal buffer with sound from the
// synthesizer, until either the buffer is full or a given number of
// timesteps is advanced. Normally, 1 sample = 1 unit of time, but speed
// modulations may change this. It returns the number of samples filled (in
// stereo samples i.e. number of elements of AudioBuffer filled), the
// number of sync outputs written, the number of time steps time advanced,
// and a possible error.
Render(buffer AudioBuffer, maxtime int) (sample int, time int, err error)
// Update recompiles a patch, but should maintain as much as possible of its
// state as reasonable. For example, filters should keep their state and
// delaylines should keep their content. Every change in the Patch triggers
// an Update and if the Patch would be started fresh every time, it would
// lead to very choppy audio.
Update(patch Patch, bpm int) error
// Trigger triggers a note for a given voice. Called between synth.Renders.
Trigger(voice int, note byte)
// Release releases the currently playing note for a given voice. Called
// between synth.Renders.
Release(voice int)
// Close disposes the synth, freeing any resources. No other functions should be called after Close.
Close()
// Populates the given array with the current CPU load of each thread,
// returning the number of threads / elements populated
CPULoad([]CPULoad) int
}
// Synther compiles a given Patch into a Synth, throwing errors if the
// Patch is malformed.
Synther interface {
Name() string // Name of the synther, e.g. "Go" or "Native"
Synth(patch Patch, bpm int) (Synth, error)
SupportsMultithreading() bool
}
CPULoad float32
)
// Play plays the Song by first compiling the patch with the given Synther,
// returning the stereo audio buffer as a result (and possible errors).
func Play(synther Synther, song Song, progress func(float32)) (AudioBuffer, error) {
err := song.Validate()
if err != nil {
return nil, err
}
synth, err := synther.Synth(song.Patch, song.BPM)
if err != nil {
return nil, fmt.Errorf("sointu.Play failed: %v", err)
}
defer synth.Close()
curVoices := make([]int, len(song.Score.Tracks))
for i := range curVoices {
curVoices[i] = song.Score.FirstVoiceForTrack(i)
}
initialCapacity := song.Score.LengthInRows() * song.SamplesPerRow()
buffer := make(AudioBuffer, 0, initialCapacity)
rowbuffer := make(AudioBuffer, song.SamplesPerRow())
for row := 0; row < song.Score.LengthInRows(); row++ {
patternRow := row % song.Score.RowsPerPattern
pattern := row / song.Score.RowsPerPattern
for t := range song.Score.Tracks {
order := song.Score.Tracks[t].Order
if pattern < 0 || pattern >= len(order) {
continue
}
patternIndex := song.Score.Tracks[t].Order[pattern]
patterns := song.Score.Tracks[t].Patterns
if patternIndex < 0 || int(patternIndex) >= len(patterns) {
continue
}
pattern := patterns[patternIndex]
if patternRow < 0 || patternRow >= len(pattern) {
continue
}
note := pattern[patternRow]
if note > 0 && note <= 1 { // anything but hold causes an action.
continue
}
synth.Release(curVoices[t])
if note > 1 {
curVoices[t]++
first := song.Score.FirstVoiceForTrack(t)
if curVoices[t] >= first+song.Score.Tracks[t].NumVoices {
curVoices[t] = first
}
synth.Trigger(curVoices[t], note)
}
}
tries := 0
for rowtime := 0; rowtime < song.SamplesPerRow(); {
samples, time, err := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
if err != nil {
return buffer, fmt.Errorf("render failed: %v", err)
}
rowtime += time
buffer = append(buffer, rowbuffer[:samples]...)
if tries > 100 {
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
}
}
if progress != nil {
progress(float32(row+1) / float32(song.Score.LengthInRows()))
}
}
return buffer, nil
}
type AudioContext interface {
Output() AudioSink
Close() error
// Fill fills the AudioBuffer using a Synth, disregarding all syncs and time
// limits. Note that this will change the state of the Synth.
func (buffer AudioBuffer) Fill(synth Synth) error {
s, _, err := synth.Render(buffer, math.MaxInt32)
if err != nil {
return fmt.Errorf("synth.Render failed: %v", err)
}
if s != len(buffer) {
return errors.New("in AudioBuffer.Fill, synth.Render should have filled the whole buffer but did not")
}
return nil
}
func (b AudioBuffer) Source() AudioSource {
return func(buf AudioBuffer) error {
n := copy(buf, b)
b = b[n:]
if n < len(buf) {
return io.EOF
}
return nil
}
}
// ReadAudio reads audio samples from an AudioSource into an AudioBuffer.
// Returns an error when the buffer is fully consumed.
func (a *BufferSource) ReadAudio(buf AudioBuffer) error {
n := copy(buf, a.buffer[a.pos:])
a.pos += n
if a.pos >= len(a.buffer) {
return io.EOF
}
return nil
}
// Wav converts an AudioBuffer into a valid WAV-file, returned as a []byte
// array.
//
// If pcm16 is set to true, the samples in the WAV-file will be 16-bit signed
// integers; otherwise the samples will be 32-bit floats
func (buffer AudioBuffer) Wav(pcm16 bool) ([]byte, error) {
buf := new(bytes.Buffer)
wavHeader(len(buffer)*2, pcm16, buf)
err := buffer.rawToBuffer(pcm16, buf)
if err != nil {
return nil, fmt.Errorf("Wav failed: %v", err)
}
return buf.Bytes(), nil
}
// Raw converts an AudioBuffer into a raw audio file, returned as a []byte
// array.
//
// If pcm16 is set to true, the samples will be 16-bit signed integers;
// otherwise the samples will be 32-bit floats
func (buffer AudioBuffer) Raw(pcm16 bool) ([]byte, error) {
buf := new(bytes.Buffer)
err := buffer.rawToBuffer(pcm16, buf)
if err != nil {
return nil, fmt.Errorf("Raw failed: %v", err)
}
return buf.Bytes(), nil
}
func (p *CPULoad) Update(duration time.Duration, frames int64) {
if frames <= 0 {
return // no frames rendered, so cannot compute CPU load
}
realtime := float64(duration) / 1e9
songtime := float64(frames) / 44100
newload := realtime / songtime
alpha := math.Exp(-songtime) // smoothing factor, time constant of 1 second
*p = CPULoad(float64(*p)*alpha + newload*(1-alpha))
}
func (data AudioBuffer) rawToBuffer(pcm16 bool, buf *bytes.Buffer) error {
var err error
if pcm16 {
int16data := make([][2]int16, len(data))
for i, v := range data {
int16data[i][0] = int16(clamp(int(v[0]*math.MaxInt16), math.MinInt16, math.MaxInt16))
int16data[i][1] = int16(clamp(int(v[1]*math.MaxInt16), math.MinInt16, math.MaxInt16))
}
err = binary.Write(buf, binary.LittleEndian, int16data)
} else {
err = binary.Write(buf, binary.LittleEndian, data)
}
if err != nil {
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
}
return nil
}
// wavHeader writes a wave header for either float32 or int16 .wav file into the
// bytes.buffer. It needs to know the length of the buffer and assumes stereo
// sound, so the length in stereo samples (L + R) is bufferlength / 2. If pcm16
// = true, then the header is for int16 audio; pcm16 = false means the header is
// for float32 audio. Assumes 44100 Hz sample rate.
func wavHeader(bufferLength int, pcm16 bool, buf *bytes.Buffer) {
// Refer to: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
numChannels := 2
sampleRate := 44100
var bytesPerSample, chunkSize, fmtChunkSize, waveFormat int
var factChunk bool
if pcm16 {
bytesPerSample = 2
chunkSize = 36 + bytesPerSample*bufferLength
fmtChunkSize = 16
waveFormat = 1 // PCM
factChunk = false
} else {
bytesPerSample = 4
chunkSize = 50 + bytesPerSample*bufferLength
fmtChunkSize = 18
waveFormat = 3 // IEEE float
factChunk = true
}
buf.Write([]byte("RIFF"))
binary.Write(buf, binary.LittleEndian, uint32(chunkSize))
buf.Write([]byte("WAVE"))
buf.Write([]byte("fmt "))
binary.Write(buf, binary.LittleEndian, uint32(fmtChunkSize))
binary.Write(buf, binary.LittleEndian, uint16(waveFormat))
binary.Write(buf, binary.LittleEndian, uint16(numChannels))
binary.Write(buf, binary.LittleEndian, uint32(sampleRate))
binary.Write(buf, binary.LittleEndian, uint32(sampleRate*numChannels*bytesPerSample)) // avgBytesPerSec
binary.Write(buf, binary.LittleEndian, uint16(numChannels*bytesPerSample)) // blockAlign
binary.Write(buf, binary.LittleEndian, uint16(8*bytesPerSample)) // bits per sample
if fmtChunkSize > 16 {
binary.Write(buf, binary.LittleEndian, uint16(0)) // size of extension
}
if factChunk {
buf.Write([]byte("fact"))
binary.Write(buf, binary.LittleEndian, uint32(4)) // fact chunk size
binary.Write(buf, binary.LittleEndian, uint32(bufferLength)) // sample length
}
buf.Write([]byte("data"))
binary.Write(buf, binary.LittleEndian, uint32(bytesPerSample*bufferLength))
}
func clamp(value, min, max int) int {
if value < min {
return min
}
if value > max {
return max
}
return value
}

View File

@ -1,101 +0,0 @@
package sointu
import (
"bytes"
"encoding/binary"
"fmt"
"math"
)
func Wav(buffer []float32, pcm16 bool) ([]byte, error) {
buf := new(bytes.Buffer)
wavHeader(len(buffer), pcm16, buf)
err := rawToBuffer(buffer, pcm16, buf)
if err != nil {
return nil, fmt.Errorf("Wav failed: %v", err)
}
return buf.Bytes(), nil
}
func Raw(buffer []float32, pcm16 bool) ([]byte, error) {
buf := new(bytes.Buffer)
err := rawToBuffer(buffer, pcm16, buf)
if err != nil {
return nil, fmt.Errorf("Raw failed: %v", err)
}
return buf.Bytes(), nil
}
func rawToBuffer(data []float32, pcm16 bool, buf *bytes.Buffer) error {
var err error
if pcm16 {
int16data := make([]int16, len(data))
for i, v := range data {
int16data[i] = int16(clamp(int(v*math.MaxInt16), math.MinInt16, math.MaxInt16))
}
err = binary.Write(buf, binary.LittleEndian, int16data)
} else {
err = binary.Write(buf, binary.LittleEndian, data)
}
if err != nil {
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
}
return nil
}
// wavHeader writes a wave header for either float32 or int16 .wav file into the
// bytes.buffer. It needs to know the length of the buffer and assumes stereo
// sound, so the length in stereo samples (L + R) is bufferlength / 2. If pcm16
// = true, then the header is for int16 audio; pcm16 = false means the header is
// for float32 audio. Assumes 44100 Hz sample rate.
func wavHeader(bufferLength int, pcm16 bool, buf *bytes.Buffer) {
// Refer to: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
numChannels := 2
sampleRate := 44100
var bytesPerSample, chunkSize, fmtChunkSize, waveFormat int
var factChunk bool
if pcm16 {
bytesPerSample = 2
chunkSize = 36 + bytesPerSample*bufferLength
fmtChunkSize = 16
waveFormat = 1 // PCM
factChunk = false
} else {
bytesPerSample = 4
chunkSize = 50 + bytesPerSample*bufferLength
fmtChunkSize = 18
waveFormat = 3 // IEEE float
factChunk = true
}
buf.Write([]byte("RIFF"))
binary.Write(buf, binary.LittleEndian, uint32(chunkSize))
buf.Write([]byte("WAVE"))
buf.Write([]byte("fmt "))
binary.Write(buf, binary.LittleEndian, uint32(fmtChunkSize))
binary.Write(buf, binary.LittleEndian, uint16(waveFormat))
binary.Write(buf, binary.LittleEndian, uint16(numChannels))
binary.Write(buf, binary.LittleEndian, uint32(sampleRate))
binary.Write(buf, binary.LittleEndian, uint32(sampleRate*numChannels*bytesPerSample)) // avgBytesPerSec
binary.Write(buf, binary.LittleEndian, uint16(numChannels*bytesPerSample)) // blockAlign
binary.Write(buf, binary.LittleEndian, uint16(8*bytesPerSample)) // bits per sample
if fmtChunkSize > 16 {
binary.Write(buf, binary.LittleEndian, uint16(0)) // size of extension
}
if factChunk {
buf.Write([]byte("fact"))
binary.Write(buf, binary.LittleEndian, uint32(4)) // fact chunk size
binary.Write(buf, binary.LittleEndian, uint32(bufferLength)) // sample length
}
buf.Write([]byte("data"))
binary.Write(buf, binary.LittleEndian, uint32(bytesPerSample*bufferLength))
}
func clamp(value, min, max int) int {
if value < min {
return min
}
if value > max {
return max
}
return value
}

12
cmd/midi_cgo.go Normal file
View File

@ -0,0 +1,12 @@
//go:build cgo
package cmd
import (
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/tracker/gomidi"
)
func NewMidiContext(broker *tracker.Broker) tracker.MIDIContext {
return gomidi.NewContext(broker)
}

12
cmd/midi_not_cgo.go Normal file
View File

@ -0,0 +1,12 @@
//go:build !cgo
package cmd
import (
"github.com/vsariola/sointu/tracker"
)
func NewMidiContext(broker *tracker.Broker) tracker.MIDIContext {
// with no cgo, we cannot use MIDI, so return a null context
return tracker.NullMIDIContext{}
}

View File

@ -14,6 +14,7 @@ import (
"gopkg.in/yaml.v3"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/version"
"github.com/vsariola/sointu/vm/compiler"
)
@ -43,8 +44,13 @@ func main() {
targetArch := flag.String("arch", runtime.GOARCH, "Target architecture. Defaults to OS architecture. Possible values: 386, amd64, wasm")
output16bit := flag.Bool("i", false, "Compiled song should output 16-bit integers, instead of floats.")
targetOs := flag.String("os", runtime.GOOS, "Target OS. Defaults to current OS. Possible values: windows, darwin, linux. Anything else is assumed linuxy. Ignored when targeting wasm.")
versionFlag := flag.Bool("v", false, "Print version.")
flag.Usage = printUsage
flag.Parse()
if *versionFlag {
fmt.Println(version.VersionOrHash)
os.Exit(0)
}
if (flag.NArg() == 0 && !*library) || *help {
flag.Usage()
os.Exit(0)

View File

@ -1,23 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/tracker/gioui"
"github.com/vsariola/sointu/vm/compiler/bridge"
)
func main() {
audioContext, err := oto.NewContext()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer audioContext.Close()
synthService := bridge.BridgeService{}
// TODO: native track does not support syncing at the moment (which is why
// we pass nil), as the native bridge does not support sync data
gioui.Main(audioContext, synthService, nil)
}

View File

@ -9,11 +9,13 @@ import (
"path/filepath"
"strings"
"github.com/vsariola/sointu/cmd"
"gopkg.in/yaml.v3"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/vm/compiler/bridge"
"github.com/vsariola/sointu/version"
)
func main() {
@ -21,15 +23,27 @@ func main() {
help := flag.Bool("h", false, "Show help.")
directory := flag.String("o", "", "Directory where to output all files. The directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
play := flag.Bool("p", false, "Play the input songs (default behaviour when no other output is defined).")
unreleased := flag.Bool("u", false, "Start song with all oscillators unreleased.")
//start := flag.Float64("start", 0, "Start playing from part; given in the units defined by parameter `unit`.")
//stop := flag.Float64("stop", -1, "Stop playing at part; given in the units defined by parameter `unit`. Negative values indicate render until end.")
//units := flag.String("unit", "pattern", "Units for parameters start and stop. Possible values: second, sample, pattern, beat. Warning: beat and pattern do not take SPEED modulations into account.")
rawOut := flag.Bool("r", false, "Output the rendered song as .raw file. By default, saves stereo float32 buffer to disk.")
wavOut := flag.Bool("w", false, "Output the rendered song as .wav file. By default, saves stereo float32 buffer to disk.")
pcm := flag.Bool("c", false, "Convert audio to 16-bit signed PCM when outputting.")
versionFlag := flag.Bool("v", false, "Print version.")
syntherInt := flag.Int("synth", 0, "Select the synther to use. By default, uses the first one in the list of available synthers.")
flag.Usage = printUsage
flag.Parse()
if *versionFlag {
fmt.Println(version.VersionOrHash)
os.Exit(0)
}
if *syntherInt < 0 || *syntherInt >= len(cmd.Synthers) {
fmt.Fprintf(os.Stderr, "synth index %d is out of range; available synthers:\n", *syntherInt)
for i, s := range cmd.Synthers {
fmt.Fprintf(os.Stderr, " %d: %s\n", i, s.Name())
}
os.Exit(1)
}
if flag.NArg() == 0 || *help {
flag.Usage()
os.Exit(0)
@ -38,6 +52,7 @@ func main() {
*play = true // if the user gives nothing to output, then the default behaviour is just to play the file
}
var audioContext sointu.AudioContext
var playWaiter sointu.CloserWaiter
if *play {
var err error
audioContext, err = oto.NewContext()
@ -45,7 +60,6 @@ func main() {
fmt.Fprintf(os.Stderr, "could not acquire oto AudioContext: %v\n", err)
os.Exit(1)
}
defer audioContext.Close()
}
process := func(filename string) error {
output := func(extension string, contents []byte) error {
@ -88,28 +102,15 @@ func main() {
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
synth, err := bridge.Synth(song.Patch)
if err != nil {
return fmt.Errorf("could not create synth based on the patch: %v", err)
}
if !*unreleased {
for i := 0; i < 32; i++ {
synth.Release(i)
}
}
buffer, _, err := sointu.Play(synth, song) // render the song to calculate its length
buffer, err := sointu.Play(cmd.Synthers[*syntherInt], song, nil) // render the song to calculate its length
if err != nil {
return fmt.Errorf("sointu.Play failed: %v", err)
}
if *play {
output := audioContext.Output()
defer output.Close()
if err := output.WriteAudio(buffer); err != nil {
return fmt.Errorf("error playing: %v", err)
}
playWaiter = audioContext.Play(buffer.Source())
}
if *rawOut {
raw, err := sointu.Raw(buffer, *pcm)
raw, err := buffer.Raw(*pcm)
if err != nil {
return fmt.Errorf("could not generate .raw file: %v", err)
}
@ -118,7 +119,7 @@ func main() {
}
}
if *wavOut {
wav, err := sointu.Wav(buffer, *pcm)
wav, err := buffer.Wav(*pcm)
if err != nil {
return fmt.Errorf("could not generate .wav file: %v", err)
}
@ -126,6 +127,9 @@ func main() {
return fmt.Errorf("error outputting .wav file: %v", err)
}
}
if *play {
playWaiter.Wait()
}
return nil
}
retval := 0

View File

@ -3,31 +3,106 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"strings"
"gioui.org/app"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/cmd"
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/rpc"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/tracker/gioui"
"github.com/vsariola/sointu/vm"
)
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
var defaultMidiInput = flag.String("midi-input", "", "connect MIDI input to matching device name prefix")
func main() {
syncAddress := flag.String("address", "", "remote RPC server where to send sync data")
flag.Parse()
var f *os.File
if *cpuprofile != "" {
var err error
f, err = os.Create(*cpuprofile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
}
audioContext, err := oto.NewContext()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer audioContext.Close()
var syncChannel chan<- []float32
if *syncAddress != "" {
syncChannel, err = rpc.Sender(*syncAddress)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
recoveryFile := ""
if configDir, err := os.UserConfigDir(); err == nil {
recoveryFile = filepath.Join(configDir, "sointu", "recovery", "sointu-track-recovery.json")
}
synthService := vm.SynthService{}
gioui.Main(audioContext, synthService, syncChannel)
broker := tracker.NewBroker()
midiContext := cmd.NewMidiContext(broker)
defer midiContext.Close()
model := tracker.NewModel(broker, cmd.Synthers, midiContext, recoveryFile)
player := tracker.NewPlayer(broker, cmd.Synthers[0])
if isFlagPassed("midi-input") {
for i, s := range model.MIDI().Input().Values {
if strings.HasPrefix(s, *defaultMidiInput) {
model.MIDI().Input().SetValue(i)
goto found
}
}
model.Alerts().Add(fmt.Sprintf("MIDI command line argument passed, but device with given prefix not found: %s", *defaultMidiInput), tracker.Error)
found:
}
if a := flag.Args(); len(a) > 0 {
f, err := os.Open(a[0])
if err == nil {
model.Song().Read(f)
}
f.Close()
}
trackerUi := gioui.NewTracker(model)
audioCloser := audioContext.Play(func(buf sointu.AudioBuffer) error {
player.Process(buf, tracker.NullPlayerProcessContext{})
return nil
})
go func() {
trackerUi.Main()
audioCloser.Close()
model.Close()
if *cpuprofile != "" {
pprof.StopCPUProfile()
f.Close()
}
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer f.Close() // error handling omitted for example
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write memory profile: ", err)
}
}
os.Exit(0)
}()
app.Main()
}
func isFlagPassed(name string) bool {
found := false
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}

140
cmd/sointu-vsti/main.go Normal file
View File

@ -0,0 +1,140 @@
//go:build plugin
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"math"
"os"
"path/filepath"
"time"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/cmd"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/tracker/gioui"
"pipelined.dev/audio/vst2"
)
type (
VSTIProcessContext struct {
events []vst2.MIDIEvent
eventIndex int
host vst2.Host
}
)
func (c *VSTIProcessContext) BPM() (bpm float64, ok bool) {
timeInfo := c.host.GetTimeInfo(vst2.TempoValid)
if timeInfo == nil || timeInfo.Flags&vst2.TempoValid == 0 || timeInfo.Tempo == 0 {
return 0, false
}
return timeInfo.Tempo, true
}
func (c *VSTIProcessContext) SampleRate() (samplerate float64, ok bool) {
timeInfo := c.host.GetTimeInfo(0)
if timeInfo == nil || timeInfo.SampleRate == 0 {
return 0, false
}
return timeInfo.SampleRate, true
}
func init() {
var (
version = int32(100)
)
vst2.PluginAllocator = func(h vst2.Host) (vst2.Plugin, vst2.Dispatcher) {
recoveryFile := ""
if configDir, err := os.UserConfigDir(); err == nil {
randBytes := make([]byte, 16)
rand.Read(randBytes)
recoveryFile = filepath.Join(configDir, "sointu", "recovery", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes)+".json")
}
broker := tracker.NewBroker()
model := tracker.NewModel(broker, cmd.Synthers, cmd.NewMidiContext(broker), recoveryFile)
player := tracker.NewPlayer(broker, cmd.Synthers[0])
t := gioui.NewTracker(model)
model.Play().TrackerHidden().SetValue(true)
// since the VST is usually working without any regard for the tracks
// until recording, disable the Instrument-Track linking by default
// because it might just confuse the user why instrument cannot be
// swapped/added etc.
model.Track().LinkInstrument().SetValue(false)
go t.Main()
context := &VSTIProcessContext{host: h}
buf := make(sointu.AudioBuffer, 1024)
var totalFrames int64 = 0
start := time.Now()
return vst2.Plugin{
UniqueID: [4]byte{'S', 'n', 't', 'u'},
Version: version,
InputChannels: 0,
OutputChannels: 2,
Name: "Sointu",
Vendor: "vsariola/sointu",
Category: vst2.PluginCategorySynth,
Flags: vst2.PluginIsSynth,
ProcessFloatFunc: func(in, out vst2.FloatBuffer) {
if time.Since(start) > 2*time.Second { // limit the rate we query the samplerate from the host and send alerts
if s, ok := context.SampleRate(); ok && math.Abs(float64(s-44100.0)) > 1e-6 {
player.SendAlert("WrongSampleRate", fmt.Sprintf("VSTi host sample rate is %.0f Hz; Sointu supports 44100 Hz only", s), tracker.Error)
}
start = time.Now()
}
left := out.Channel(0)
right := out.Channel(1)
if len(buf) < out.Frames {
buf = append(buf, make(sointu.AudioBuffer, out.Frames-len(buf))...)
}
buf = buf[:out.Frames]
player.Process(buf, context)
for i := 0; i < out.Frames; i++ {
left[i], right[i] = buf[i][0], buf[i][1]
}
totalFrames += int64(out.Frames)
},
}, vst2.Dispatcher{
CanDoFunc: func(pcds vst2.PluginCanDoString) vst2.CanDoResponse {
switch pcds {
case vst2.PluginCanReceiveEvents, vst2.PluginCanReceiveMIDIEvent, vst2.PluginCanReceiveTimeInfo:
return vst2.YesCanDo
}
return vst2.NoCanDo
},
ProcessEventsFunc: func(events *vst2.EventsPtr) {
for i := 0; i < events.NumEvents(); i++ {
switch ev := events.Event(i).(type) {
case *vst2.MIDIEvent:
if (ev.Data[0] >= 0x80 && ev.Data[0] <= 0x9F) || (ev.Data[0] >= 0xB0 && ev.Data[0] <= 0xBF) {
player.EmitMIDIMsg(&tracker.MIDIMessage{Timestamp: int64(ev.DeltaFrames) + totalFrames, Data: ev.Data, Source: &context})
}
}
}
},
CloseFunc: func() {
tracker.TrySend(broker.CloseGUI, struct{}{})
model.Close()
tracker.TimeoutReceive(broker.FinishedGUI, 3*time.Second)
},
GetChunkFunc: func(isPreset bool) []byte {
retChn := make(chan []byte)
if !tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { retChn <- t.History().MarshalRecovery() }}) {
return nil
}
ret, _ := tracker.TimeoutReceive(retChn, 5*time.Second) // ret will be nil if timeout or channel closed
return ret
},
SetChunkFunc: func(data []byte, isPreset bool) {
tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { t.History().UnmarshalRecovery(data) }})
},
}
}
}
func main() {}

8
cmd/synthers.go Normal file
View File

@ -0,0 +1,8 @@
package cmd
import (
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
)
var Synthers = []sointu.Synther{vm.GoSynther{}}

11
cmd/synthers_native.go Normal file
View File

@ -0,0 +1,11 @@
//go:build native
package cmd
import (
"github.com/vsariola/sointu/vm/compiler/bridge"
)
func init() {
Synthers = append(Synthers, bridge.NativeSynther{})
}

1
examples/CMakeLists.txt Normal file
View File

@ -0,0 +1 @@
add_subdirectory(code)

View File

@ -0,0 +1,68 @@
# this fixes a bug in creating a static library from asm, similar to
# https://discourse.cmake.org/t/building-lib-file-from-asm-cmake-bug/1959
# but for NASM
if(MSVC)
set(CMAKE_ASM_NASM_CREATE_STATIC_LIBRARY "<CMAKE_AR> /OUT:<TARGET> <LINK_FLAGS> <OBJECTS>")
endif()
add_custom_command(
COMMAND
${compilecmd} -arch=${arch} -o physics_girl_st.asm "${PROJECT_SOURCE_DIR}/examples/patches/physics_girl_st.yml"
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
DEPENDS
"${PROJECT_SOURCE_DIR}/examples/patches/physics_girl_st.yml"
${compilecmd}
OUTPUT
physics_girl_st.asm
physics_girl_st.h
physics_girl_st.inc
COMMENT
"Compiling ${PROJECT_SOURCE_DIR}/examples/patches/physics-girl-st.yml..."
)
add_library(physics_girl_st physics_girl_st.asm)
if(WIN32)
add_executable(cplay-winmm
cplay.windows.winmm.c
physics_girl_st.h
)
target_link_libraries(cplay-winmm PRIVATE winmm)
target_link_libraries(cplay-winmm PRIVATE physics_girl_st)
target_include_directories(cplay-winmm PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
add_dependencies(examples cplay-winmm)
add_executable(cplay-directsound
cplay.windows.directsound.c
physics_girl_st.h
)
target_link_libraries(cplay-directsound PRIVATE dsound ws2_32 ucrt)
target_link_libraries(cplay-directsound PRIVATE physics_girl_st)
target_include_directories(cplay-directsound PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
add_dependencies(examples cplay-directsound)
elseif(UNIX)
add_executable(cplay
cplay.linux.c
physics_girl_st.h
)
target_link_libraries(cplay PRIVATE asound pthread)
target_link_options(cplay PRIVATE -z noexecstack -no-pie)
target_link_libraries(cplay PRIVATE physics_girl_st)
target_include_directories(cplay PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
add_dependencies(examples cplay)
endif()
add_executable(cwav
cwav.c
physics_girl_st.h
)
if(WIN32)
target_compile_definitions(cwav PRIVATE _CRT_SECURE_NO_WARNINGS)
elseif(UNIX)
target_link_options(cwav PRIVATE -z noexecstack -no-pie)
endif()
target_link_libraries(cwav PRIVATE physics_girl_st)
target_include_directories(cwav PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
add_dependencies(examples cwav)

View File

@ -0,0 +1,58 @@
#include <alsa/asoundlib.h>
#include <pthread.h>
#include <stdio.h>
#include <stdint.h>
#include <time.h>
#include "physics_girl_st.h"
static SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
static snd_pcm_t *pcm_handle;
static pthread_t render_thread, playback_thread;
static uint32_t render_thread_handle, playback_thread_handle;
void play() {
snd_pcm_writei(pcm_handle, sound_buffer, SU_LENGTH_IN_SAMPLES);
}
int main(int argc, char **args) {
// Unix does not have gm.dls, no need to ifdef and setup here.
// We render in the background while playing already.
render_thread_handle = pthread_create(&render_thread, 0, (void * (*)(void *))su_render_song, sound_buffer);
// We can't start playing too early or the missing samples will be audible.
sleep(2.);
// Play the track.
snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
snd_pcm_set_params(
pcm_handle,
#ifdef SU_SAMPLE_FLOAT
SND_PCM_FORMAT_FLOAT,
#else // SU_SAMPLE_FLOAT
SND_PCM_FORMAT_S16_LE,
#endif // SU_SAMPLE_FLOAT
SND_PCM_ACCESS_RW_INTERLEAVED,
SU_CHANNEL_COUNT,
SU_SAMPLE_RATE,
0,
SU_LENGTH_IN_SAMPLES
);
playback_thread_handle = pthread_create(&playback_thread, 0, (void *(*)(void *))play, 0);
// This is for obtaining the playback time.
snd_pcm_status_t *status;
snd_pcm_status_malloc(&status);
snd_htimestamp_t htime, htstart;
snd_pcm_status(pcm_handle, status);
snd_pcm_status_get_htstamp(status, &htstart);
for(int sample; sample < SU_LENGTH_IN_SAMPLES; sample = (int)(((float)htime.tv_sec + (float)htime.tv_nsec * 1.e-9 - (float)htstart.tv_sec - (float)htstart.tv_nsec * 1.e-9) * SU_SAMPLE_RATE)) {
snd_pcm_status(pcm_handle, status);
snd_pcm_status_get_htstamp(status, &htime);
printf("Sample: %d\n", sample);
usleep(1000000 / 30);
}
snd_pcm_status_free(status);
return 0;
}

View File

@ -0,0 +1,75 @@
#include <stdio.h>
#include <stdint.h>
#include "physics_girl_st.h"
#define WIN32_LEAN_AND_MEAN
#define WIN32_EXTRA_LEAN
#include <Windows.h>
#include "mmsystem.h"
#include "mmreg.h"
#define CINTERFACE
#include <dsound.h>
#ifndef DSBCAPS_TRUEPLAYPOSITION // Not defined in MinGW dsound headers, so let's add it
#define DSBCAPS_TRUEPLAYPOSITION 0x00080000
#endif
SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
WAVEFORMATEX wave_format = {
#ifdef SU_SAMPLE_FLOAT
WAVE_FORMAT_IEEE_FLOAT,
#else
WAVE_FORMAT_PCM,
#endif
SU_CHANNEL_COUNT,
SU_SAMPLE_RATE,
SU_SAMPLE_RATE * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
SU_SAMPLE_SIZE*8,
0
};
DSBUFFERDESC buffer_description = {
sizeof(DSBUFFERDESC),
DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS | DSBCAPS_TRUEPLAYPOSITION,
SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
0,
&wave_format,
0
};
int main(int argc, char **args) {
// Load gm.dls if necessary.
#ifdef SU_LOAD_GMDLS
su_load_gmdls();
#endif // SU_LOAD_GMDLS
HWND hWnd = GetForegroundWindow();
if(hWnd == NULL) {
hWnd = GetDesktopWindow();
}
LPDIRECTSOUND direct_sound;
LPDIRECTSOUNDBUFFER direct_sound_buffer;
DirectSoundCreate(0, &direct_sound, 0);
IDirectSound_SetCooperativeLevel(direct_sound, hWnd, DSSCL_PRIORITY);
IDirectSound_CreateSoundBuffer(direct_sound, &buffer_description, &direct_sound_buffer, NULL);
LPVOID p1;
DWORD l1;
IDirectSoundBuffer_Lock(direct_sound_buffer, 0, SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE, &p1, &l1, NULL, NULL, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)su_render_song, p1, 0, 0);
IDirectSoundBuffer_Play(direct_sound_buffer, 0, 0, 0);
// We need to handle windows messages properly while playing, as waveOutWrite is async.
MSG msg = {0};
DWORD last_play_cursor = 0;
for(DWORD play_cursor = 0; play_cursor >= last_play_cursor; IDirectSoundBuffer_GetCurrentPosition(direct_sound_buffer, (DWORD*)&play_cursor, NULL)) {
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
last_play_cursor = play_cursor;
}
return 0;
}

View File

@ -0,0 +1,68 @@
#include <stdio.h>
#include <stdint.h>
#include "physics_girl_st.h"
#define WIN32_LEAN_AND_MEAN
#define WIN32_EXTRA_LEAN
#include <Windows.h>
#include "mmsystem.h"
#include "mmreg.h"
SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
HWAVEOUT wave_out_handle;
WAVEFORMATEX wave_format = {
#ifdef SU_SAMPLE_FLOAT
WAVE_FORMAT_IEEE_FLOAT,
#else
WAVE_FORMAT_PCM,
#endif
SU_CHANNEL_COUNT,
SU_SAMPLE_RATE,
SU_SAMPLE_RATE * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
SU_SAMPLE_SIZE*8,
0
};
// If you want to loop the song:
// 1) Change WHDR_PREPARED -> WHDR_BEGINLOOP | WHDR_ENDLOOP | WHDR_PREPARED
// 2) The next field should then contain the number of loops (for example, 4)
// 3) Remember also change the exit condition for main, e.g. if you plan to loop 4 times:
// mmtime.u.sample != SU_LENGTH_IN_SAMPLES -> mmtime.u.sample != 4 * SU_LENGTH_IN_SAMPLES
WAVEHDR wave_header = {
(LPSTR)sound_buffer,
SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
0,
0,
WHDR_PREPARED,
0,
0,
0
};
MMTIME mmtime = {
TIME_SAMPLES,
0
};
int main(int argc, char **args) {
// Load gm.dls if necessary.
#ifdef SU_LOAD_GMDLS
su_load_gmdls();
#endif // SU_LOAD_GMDLS
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)su_render_song, sound_buffer, 0, 0);
// We render in the background while playing already. Fortunately,
// Windows is slow with the calls below, so we're not worried that
// we don't have enough samples ready before the track starts.
waveOutOpen(&wave_out_handle, WAVE_MAPPER, &wave_format, 0, 0, CALLBACK_NULL);
waveOutWrite(wave_out_handle, &wave_header, sizeof(wave_header));
// We need to handle windows messages properly while playing, as waveOutWrite is async.
for(MSG msg = {0}; mmtime.u.sample != SU_LENGTH_IN_SAMPLES; waveOutGetPosition(wave_out_handle, &mmtime, sizeof(MMTIME))) {
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
}
return 0;
}

72
examples/code/C/cwav.c Normal file
View File

@ -0,0 +1,72 @@
#include <stdio.h>
#include <stdint.h>
#include "physics_girl_st.h"
#define WAVE_FORMAT_PCM 0x1
#define WAVE_FORMAT_IEEE_FLOAT 0x3
static SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
#pragma pack(push, 1)
typedef struct {
char riff[4];
uint32_t file_size;
char wavefmt[8];
} riff_header_t;
typedef struct {
char data[4];
uint32_t data_size;
} data_header_t;
typedef struct {
riff_header_t riff_header;
uint32_t riff_header_size;
uint16_t sample_type;
uint16_t channel_count;
uint32_t sample_rate;
uint32_t bytes_per_second;
uint16_t bytes_per_channel;
uint16_t bits_per_sample;
data_header_t data_header;
} wave_header_t;
#pragma pack(pop)
int main(int argc, char **args) {
wave_header_t wave_header = {
.riff_header = (riff_header_t) {
.riff = "RIFF",
.file_size = sizeof(wave_header_t) + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
.wavefmt = "WAVEfmt ",
},
.riff_header_size = sizeof(riff_header_t),
#ifdef SU_SAMPLE_FLOAT
.sample_type = WAVE_FORMAT_IEEE_FLOAT,
#else // SU_SAMPLE_FLOAT
.sample_type = WAVE_FORMAT_PCM,
#endif // SU_SAMPLE_FLOAT
.channel_count = SU_CHANNEL_COUNT,
.sample_rate = SU_SAMPLE_RATE,
.bytes_per_second = SU_SAMPLE_SIZE * SU_SAMPLE_RATE * SU_CHANNEL_COUNT,
.bytes_per_channel = SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
.bits_per_sample = SU_SAMPLE_SIZE * 8,
.data_header = (data_header_t) {
.data = "data",
.data_size = sizeof(data_header_t) + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
}
};
// Load gm.dls if necessary.
#ifdef SU_LOAD_GMDLS
su_load_gmdls();
#endif // SU_LOAD_GMDLS
su_render_song(sound_buffer);
FILE *file = fopen("physics_girl_st.wav", "wb");
fwrite(&wave_header, sizeof(wave_header_t), 1, file);
fwrite((uint8_t *)sound_buffer, 1, SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT, file);
fclose(file);
return 0;
}

View File

@ -0,0 +1,34 @@
if(("${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") AND MSVC)
# in 32-bit mode with MSVC toolset, we can use Crinkler to compress the executable
set(CRINKLER_LEVEL "off" CACHE STRING "Crinkler compression level: off, light, medium, heavy")
if(NOT CRINKLER_LEVEL STREQUAL OFF)
find_program(CRINKLER NAMES Crinkler)
if (NOT CRINKLER)
message(WARNING "Crinkler not found. Cannot compress executable; using default linker. Get Crinkler from https://github.com/runestubbe/Crinkler & put it in path (as Crinkler.exe)")
set(CRINKLER_LEVEL OFF)
endif()
endif()
if (NOT CRINKLER_LEVEL STREQUAL OFF)
message(STATUS "Crinkler found at: ${CRINKLER}")
set(CRINKLER_FLAGS "/PROGRESSGUI /UNSAFEIMPORT /UNALIGNCODE /HASHSIZE:1000 /REPORT:<TARGET>.report.html")
# TBD: do we add /SATURATE
if (CRINKLER_LEVEL STREQUAL LIGHT)
set(CRINKLER_FLAGS "${CRINKLER_FLAGS} /HASHTRIES:100 /COMPMODE:INSTANT /ORDERTRIES:2000")
elseif (CRINKLER_LEVEL STREQUAL HEAVY)
set(CRINKLER_FLAGS "${CRINKLER_FLAGS} /HASHTRIES:1000 /COMPMODE:VERYSLOW /ORDERTRIES:30000")
else()
set(CRINKLER_FLAGS "${CRINKLER_FLAGS} /HASHTRIES:300 /COMPMODE:SLOW /ORDERTRIES:9000")
endif()
# we drop the whole manifest creation from the front; did not find a way to disable it from CMake otherwise
set (CMAKE_C_LINK_EXECUTABLE "${CRINKLER} <OBJECTS> /out:<TARGET> ${CRINKLER_FLAGS} <LINK_LIBRARIES>")
endif()
endif()
set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL ON)
add_custom_target(examples)
add_subdirectory(asm)
add_subdirectory(C)

7
examples/code/Python/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.venv
build
dist
__pycache__
setup.py
*.egg-info
*.so

View File

@ -0,0 +1,18 @@
# Embed Sointu in Python
This is an example for embedding Sointu into Python code.
# Configure the track
Edit the `track` variable in `build.py` according to your needs.
# Build
* Install Python 3.11 and poetry.
* Download nasm and golang; place both of them in your system `PATH`.
* Enable cgo by downloading a gcc and placing it into your system `PATH`.
* Get the dependencies with `poetry install`.
* Run the player using `poetry run python -m sointu_python`.
* Pack everything into an executable using `poetry run pyinstaller sointu_python/sointu_python.spec`. The executable will be built in the `dist` subfolder.
# Rebuild after changes
* Rebuild the example track bindings with `poetry build`.
* Update the bindings module with `poetry install`.
* Proceed iteration.

View File

@ -0,0 +1,149 @@
from distutils.command.build_ext import build_ext
from distutils.errors import (
CCompilerError,
DistutilsExecError,
DistutilsPlatformError,
)
from distutils.core import Extension
from os.path import (
dirname,
join,
abspath,
exists,
basename,
splitext,
)
from os import mkdir
from subprocess import run
from platform import system
from sys import exit
track = "../../patches/physics_girl_st.yml"
class BuildFailed(Exception):
pass
class ExtBuilder(build_ext):
def run(self):
try:
build_ext.run(self)
except (DistutilsPlatformError, FileNotFoundError):
raise BuildFailed('File not found. Could not compile C extension.')
def build_extension(self, ext):
try:
build_ext.build_extension(self, ext)
except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError):
raise BuildFailed('Could not compile C extension.')
def build(setup_kwargs):
# Make sure the build directory exists and setup the
# relative paths correctly.
cwd = abspath(".")
print("Running from:", cwd)
current_source_dir = abspath(dirname(__file__))
project_source_dir = abspath(join(current_source_dir, "..", "..", ".."))
current_binary_dir = join(current_source_dir, 'build')
if not exists(current_binary_dir):
mkdir(current_binary_dir)
host_is_windows = system() == "Windows"
executable_suffix = ".exe" if host_is_windows else ""
object_suffix = ".obj" if host_is_windows else ".o"
# Build the sointu compiler first.
compiler_executable = join(current_binary_dir, "sointu-compile{}".format(executable_suffix))
result = run(
args=[
"go", "build",
"-o", compiler_executable,
"cmd/sointu-compile/main.go",
],
cwd=project_source_dir,
shell=True if host_is_windows else False,
)
if result.returncode != 0:
print("sointu-compile build process exited with:", result.returncode)
print(result.stdout)
exit(1)
track_file_name = abspath(join(current_source_dir, track))
(track_name_base, _) = splitext(basename(track_file_name))
print("Compiling track:", track_file_name)
# Compile the track.
sointu_compiler_arch = "amd64"
track_asm_file = join(current_binary_dir, '{}.asm'.format(track_name_base))
result = run(
args=[
compiler_executable,
"-o", track_asm_file,
"-arch={}".format(sointu_compiler_arch),
track_file_name,
],
)
if result.returncode != 0:
print("sointu-compile process exited with:", result.returncode)
print(result.stdout)
exit(1)
# Assemble the track.
nasm_abi = "Win64" if host_is_windows else "Elf64"
track_object_file = join(current_binary_dir, '{}{}'.format(track_name_base, object_suffix))
print("Assembling track asm source:", track_asm_file)
result = run(
args=[
'nasm',
'-o', track_object_file,
'-f', nasm_abi,
track_asm_file,
],
)
if result.returncode != 0:
print("nasm process exited with:", result.returncode)
print(result.stdout)
exit(1)
# Export the plugin.
print("Linking object file into Python extension module:", track_object_file)
setup_kwargs.update({
"ext_modules": [
Extension(
"sointu",
include_dirs=[
current_binary_dir,
current_source_dir,
],
sources=[
"sointu.c",
],
extra_compile_args=[
"-DTRACK_HEADER=\"{}.h\"".format(track_name_base),
] + ([
"-DWIN32",
] if host_is_windows else [
"-DUNIX",
"-fPIC",
]),
extra_objects=[
track_object_file,
],
extra_link_args=[
"dsound.lib",
"ws2_32.lib",
"ucrt.lib",
"user32.lib",
] if host_is_windows else [
"-z", "noexecstack",
"--no-pie",
"-lasound",
"-lpthread",
"-lpython3.11",
],
),
],
"cmdclass": {
"build_ext": ExtBuilder,
},
})

132
examples/code/Python/poetry.lock generated Normal file
View File

@ -0,0 +1,132 @@
# This file is automatically @generated by Poetry and should not be changed by hand.
[[package]]
name = "altgraph"
version = "0.17.4"
description = "Python graph (network) package"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"},
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
]
[[package]]
name = "macholib"
version = "1.16.3"
description = "Mach-O header analysis and editing"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"},
{file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"},
]
[package.dependencies]
altgraph = ">=0.17"
[[package]]
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]]
name = "pefile"
version = "2023.2.7"
description = "Python PE parsing module"
category = "dev"
optional = false
python-versions = ">=3.6.0"
files = [
{file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"},
{file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"},
]
[[package]]
name = "pyinstaller"
version = "6.0.0"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
category = "dev"
optional = false
python-versions = "<3.13,>=3.8"
files = [
{file = "pyinstaller-6.0.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d84b06fb9002109bfc542e76860b81459a8585af0bbdabcfc5dcf272ef230de7"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa922d1d73881d0820a341d2c406a571cc94630bdcdc275427c844a12e6e376e"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:52e5b3a2371d7231de17515c7c78d8d4a39d70c8c095e71d55b3b83434a193a8"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4a75bde5cda259bb31f2294960d75b9d5c148001b2b0bd20a91f9c2116675a6c"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:5314f6f08d2bcbc031778618ba97d9098d106119c2e616b3b081171fe42f5415"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0ad7cc3776ca17d0bededcc352cba2b1c89eb4817bfabaf05972b9da8c424935"},
{file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cccdad6cfe7a5db7d7eb8df2e5678f8375268739d5933214e180da300aa54e37"},
{file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fb6af82989dac7c58bd25ed9ba3323bc443f8c1f03804f69c9f5e363bf4a021c"},
{file = "pyinstaller-6.0.0-py3-none-win32.whl", hash = "sha256:68769f5e6722474bb1038e35560444659db8b951388bfe0c669bb52a640cd0eb"},
{file = "pyinstaller-6.0.0-py3-none-win_amd64.whl", hash = "sha256:438a9e0d72a57d5bba4f112d256e39ea4033c76c65414c0693d8311faa14b090"},
{file = "pyinstaller-6.0.0-py3-none-win_arm64.whl", hash = "sha256:16a473065291dd7879bf596fa20e65bd9d1e8aafc2cef1bffa3e42e707e2e68e"},
{file = "pyinstaller-6.0.0.tar.gz", hash = "sha256:d702cff041f30e7a53500b630e07b081e5328d4655023319253d73935e75ade2"},
]
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=20.0"
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2021.4"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
[package.extras]
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2023.9"
description = "Community maintained hooks for PyInstaller"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyinstaller-hooks-contrib-2023.9.tar.gz", hash = "sha256:76084b5988e3957a9df169d2a935d65500136967e710ddebf57263f1a909cd80"},
{file = "pyinstaller_hooks_contrib-2023.9-py2.py3-none-any.whl", hash = "sha256:f34f4c6807210025c8073ebe665f422a3aa2ac5f4c7ebf4c2a26cc77bebf63b5"},
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.2"
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
]
[[package]]
name = "setuptools"
version = "68.2.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
{file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.11,<3.13"
content-hash = "797bde9c30c55b3ddb24b1d3eceedd093d8a89eb934e6fe8fe7191dc9247224d"

View File

@ -0,0 +1,27 @@
[tool.poetry]
name = "sointu-python"
version = "0.1.0"
description = "Play back Sointu tracks in Python."
authors = ["Alexander Kraus <nr4@z10.info>"]
license = "MIT"
readme = "README.md"
packages = [
{ include = "sointu_python" },
]
include = [
{ path = "sointu*.so", format="wheel" }
]
[tool.poetry.build]
script = "build.py"
generate-setup-file = true
[tool.poetry.dependencies]
python = ">=3.11,<3.13"
[tool.poetry.group.dev.dependencies]
pyinstaller = "^6.0.0"
[build-system]
requires = ["poetry-core>=1.0.0a3", "poetry>=0.12", "setuptools", "wheel"]
build-backend = "poetry.core.masonry.api"

View File

@ -0,0 +1,212 @@
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#include TRACK_HEADER
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
#ifdef WIN32
#define WIN32_LEAN_AND_MEAN
#define WIN32_EXTRA_LEAN
#include <Windows.h>
#include "mmsystem.h"
#include "mmreg.h"
#define CINTERFACE
#include <dsound.h>
static WAVEFORMATEX wave_format = {
#ifdef SU_SAMPLE_FLOAT
WAVE_FORMAT_IEEE_FLOAT,
#else
WAVE_FORMAT_PCM,
#endif
SU_CHANNEL_COUNT,
SU_SAMPLE_RATE,
SU_SAMPLE_RATE * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
SU_SAMPLE_SIZE*8,
0
};
DSBUFFERDESC buffer_description = {
sizeof(DSBUFFERDESC),
DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS | DSBCAPS_TRUEPLAYPOSITION,
SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
0,
&wave_format,
0
};
static HWND hWnd;
static LPDIRECTSOUND direct_sound;
static LPDIRECTSOUNDBUFFER direct_sound_buffer;
static LPVOID p1;
static DWORD l1;
static DWORD last_play_cursor = 0;
#endif /* WIN32 */
#ifdef UNIX
#include <alsa/asoundlib.h>
#include <pthread.h>
#include <time.h>
static snd_pcm_t *pcm_handle;
static pthread_t render_thread;
static uint32_t render_thread_handle;
static pthread_t playback_thread;
static uint32_t playback_thread_handle;
snd_htimestamp_t start_ts;
static int _snd_pcm_writei(void *params) {
(void) params;
snd_pcm_writei(pcm_handle, sound_buffer, SU_LENGTH_IN_SAMPLES);
return 0;
}
#endif /* UNIX */
static PyObject *sointuError;
static PyObject *sointu_play_song(PyObject *self, PyObject *args) {
#ifdef WIN32
#ifdef SU_LOAD_GMDLS
su_load_gmdls();
#endif // SU_LOAD_GMDLS
hWnd = GetForegroundWindow();
if(hWnd == NULL) {
hWnd = GetDesktopWindow();
}
DirectSoundCreate(0, &direct_sound, 0);
IDirectSound_SetCooperativeLevel(direct_sound, hWnd, DSSCL_PRIORITY);
IDirectSound_CreateSoundBuffer(direct_sound, &buffer_description, &direct_sound_buffer, NULL);
IDirectSoundBuffer_Lock(direct_sound_buffer, 0, SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE, &p1, &l1, NULL, NULL, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)su_render_song, p1, 0, 0);
IDirectSoundBuffer_Play(direct_sound_buffer, 0, 0, 0);
#endif /* WIN32 */
#ifdef UNIX
render_thread_handle = pthread_create(&render_thread, 0, (void * (*)(void *))su_render_song, sound_buffer);
// We can't start playing too early or the missing samples will be audible.
sleep(2.);
// Play the track.
snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
snd_pcm_set_params(
pcm_handle,
#ifdef SU_SAMPLE_FLOAT
SND_PCM_FORMAT_FLOAT,
#else // SU_SAMPLE_FLOAT
SND_PCM_FORMAT_S16_LE,
#endif // SU_SAMPLE_FLOAT
SND_PCM_ACCESS_RW_INTERLEAVED,
SU_CHANNEL_COUNT,
SU_SAMPLE_RATE,
0,
SU_LENGTH_IN_SAMPLES
);
// Enable playback time querying.
snd_pcm_sw_params_t *swparams;
snd_pcm_sw_params_alloca(&swparams);
snd_pcm_sw_params_current(pcm_handle, swparams);
snd_pcm_sw_params_set_tstamp_mode(pcm_handle, swparams, SND_PCM_TSTAMP_ENABLE);
snd_pcm_sw_params_set_tstamp_type(pcm_handle, swparams, SND_PCM_TSTAMP_TYPE_GETTIMEOFDAY);
snd_pcm_sw_params(pcm_handle, swparams);
playback_thread_handle = pthread_create(&playback_thread, 0, (void *(*)(void *))_snd_pcm_writei, 0);
// Get the start time stamp.
snd_pcm_uframes_t avail;
snd_pcm_htimestamp(pcm_handle, &avail, &start_ts);
#endif /* UNIX */
return PyLong_FromLong(0);
}
static PyObject *sointu_playback_position(PyObject *self, PyObject *args) {
#ifdef WIN32
DWORD play_cursor = 0;
IDirectSoundBuffer_GetCurrentPosition(direct_sound_buffer, (DWORD*)&play_cursor, NULL);
return Py_BuildValue("i", play_cursor / SU_CHANNEL_COUNT / sizeof(SUsample));
#endif /* WIN32 */
#ifdef UNIX
snd_htimestamp_t ts;
snd_pcm_uframes_t avail;
snd_pcm_htimestamp(pcm_handle, &avail, &ts);
return Py_BuildValue("i", (int)((ts.tv_sec - start_ts.tv_sec + 1.e-9 * (ts.tv_nsec - start_ts.tv_nsec)) * SU_SAMPLE_RATE));
#endif /* UNIX */
}
static PyObject *sointu_playback_finished(PyObject *self, PyObject *args) {
bool result = false;
#ifdef WIN32
DWORD play_cursor = 0;
IDirectSoundBuffer_GetCurrentPosition(direct_sound_buffer, (DWORD*)&play_cursor, NULL);
result = play_cursor < last_play_cursor;
last_play_cursor = play_cursor;
#endif /* WIN32 */
#ifdef UNIX
snd_htimestamp_t ts;
snd_pcm_uframes_t avail;
snd_pcm_htimestamp(pcm_handle, &avail, &ts);
result = ts.tv_sec - start_ts.tv_sec < 0;
#endif /* UNIX */
return PyBool_FromLong(result);
}
static PyObject *sointu_sample_rate(PyObject *self, PyObject *args) {
return Py_BuildValue("i", SU_SAMPLE_RATE);
}
static PyObject *sointu_track_length(PyObject *self, PyObject *args) {
return Py_BuildValue("i", SU_LENGTH_IN_SAMPLES);
}
static PyMethodDef sointuMethods[] = {
{"play_song", sointu_play_song, METH_VARARGS, "Play sointu track."},
{"playback_position", sointu_playback_position, METH_VARARGS, "Get playback position of sointu track currently playing."},
{"playback_finished", sointu_playback_finished, METH_VARARGS, "Check if currently playing sointu track has finished playing."},
{"sample_rate", sointu_sample_rate, METH_VARARGS, "Return the sample rate of the track compiled into this module."},
{"track_length", sointu_track_length, METH_VARARGS, "Return the track length in samples."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef sointumodule = {
PyModuleDef_HEAD_INIT,
"sointu",
NULL,
-1,
sointuMethods
};
PyMODINIT_FUNC PyInit_sointu(void) {
PyObject *module = PyModule_Create(&sointumodule);
if(module == NULL) {
return NULL;
}
sointuError = PyErr_NewException("sointu.sointuError", NULL, NULL);
Py_XINCREF(sointuError);
if(PyModule_AddObject(module, "error", sointuError) < 0) {
Py_XDECREF(sointuError);
Py_CLEAR(sointuError);
Py_DECREF(module);
return NULL;
}
return module;
}

View File

@ -0,0 +1,16 @@
from sointu import (
play_song,
playback_position,
playback_finished,
sample_rate,
track_length,
)
from sys import exit
if __name__ == '__main__':
play_song()
while not playback_finished():
print("Playback time:", playback_position() / sample_rate())
exit(0)

View File

@ -0,0 +1,58 @@
# -*- mode: python ; coding: utf-8 -*-
from os.path import abspath, join
from zipfile import ZipFile
from platform import system
moduleName = 'sointu_python'
rootPath = abspath('.')
buildPath = join(rootPath, 'build')
distPath = join(rootPath, 'dist')
sourcePath = join(rootPath, moduleName)
block_cipher = None
a = Analysis(
[
join(sourcePath, '__main__.py'),
],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='{}'.format(moduleName),
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=None,
)
exeFileName = '{}{}'.format(moduleName, '.exe' if system() == 'Windows' else '')
zipFileName = '{}-{}.zip'.format(moduleName, 'windows' if system() == 'Windows' else 'linux')
ZipFile(join(distPath, zipFileName), mode='w').write(join(distPath, exeFileName), arcname=exeFileName)

View File

@ -0,0 +1,10 @@
if(WIN32)
set(CMAKE_ASM_NASM_OBJECT_FORMAT win32)
elseif(UNIX)
set(CMAKE_ASM_NASM_OBJECT_FORMAT elf32)
endif()
set(CMAKE_ASM_NASM_COMPILE_OBJECT "<CMAKE_ASM_NASM_COMPILER> <INCLUDES> <DEFINES> <FLAGS> -f ${CMAKE_ASM_NASM_OBJECT_FORMAT} -o <OBJECT> <SOURCE>")
add_asm_example(asmplay "${PROJECT_SOURCE_DIR}/examples/patches/physics_girl_st.yml" 386 32 "winmm" "asound;pthread")
add_asm_example(asmwav "${PROJECT_SOURCE_DIR}/examples/patches/physics_girl_st.yml" 386 32 "" "")
target_compile_definitions(asmwav-386 PRIVATE FILENAME="physics_girl_st.wav")

View File

@ -0,0 +1,81 @@
%include TRACK_INCLUDE
%define SND_PCM_FORMAT_S16_LE 0x2
%define SND_PCM_FORMAT_FLOAT 0xE
%define SND_PCM_ACCESS_RW_INTERLEAVED 0x3
%define SND_PCM_STREAM_PLAYBACK 0x0
section .bss
sound_buffer:
resb SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
render_thread:
resd 1
pcm_handle:
resd 1
section .data
default_device:
db "default", 0
section .text
symbols:
extern pthread_create
extern sleep
extern snd_pcm_open
extern snd_pcm_set_params
extern snd_pcm_writei
global main
main:
; elf32 uses the cdecl calling convention. This is more readable imo ;)
; Prologue
push ebp
mov ebp, esp
sub esp, 0x10
; Unix does not have gm.dls, no need to ifdef and setup here.
; We render in the background while playing already.
push sound_buffer
lea eax, su_render_song
push eax
push 0
push render_thread
call pthread_create
; We can't start playing too early or the missing samples will be audible.
push 0x2
call sleep
; Play the track.
push 0x0
push SND_PCM_STREAM_PLAYBACK
push default_device
push pcm_handle
call snd_pcm_open
push SU_LENGTH_IN_SAMPLES
push 0
push SU_SAMPLE_RATE
push SU_CHANNEL_COUNT
push SND_PCM_ACCESS_RW_INTERLEAVED
%ifdef SU_SAMPLE_FLOAT
push SND_PCM_FORMAT_FLOAT
%else ; SU_SAMPLE_FLOAT
push SND_PCM_FORMAT_S16_LE
%endif ; SU_SAMPLE_FLOAT
push dword [pcm_handle]
call snd_pcm_set_params
push SU_LENGTH_IN_SAMPLES
push sound_buffer
push dword [pcm_handle]
call snd_pcm_writei
exit:
; At least we can skip the epilogue :)
leave
ret

View File

@ -0,0 +1,120 @@
%define MANGLED
%include TRACK_INCLUDE
%define WAVE_FORMAT_PCM 0x1
%define WAVE_FORMAT_IEEE_FLOAT 0x3
%define WHDR_PREPARED 0x2
%define WAVE_MAPPER 0xFFFFFFFF
%define TIME_SAMPLES 0x2
%define PM_REMOVE 0x1
section .bss
sound_buffer:
resb SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
wave_out_handle:
resd 1
msg:
resd 1
message:
resd 7
section .data
wave_format:
%ifdef SU_SAMPLE_FLOAT
dw WAVE_FORMAT_IEEE_FLOAT
%else ; SU_SAMPLE_FLOAT
dw WAVE_FORMAT_PCM
%endif ; SU_SAMPLE_FLOAT
dw SU_CHANNEL_COUNT
dd SU_SAMPLE_RATE
dd SU_SAMPLE_SIZE * SU_SAMPLE_RATE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * 8
dw 0
wave_header:
dd sound_buffer
dd SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
times 2 dd 0
dd WHDR_PREPARED
times 4 dd 0
wave_header_end:
mmtime:
dd TIME_SAMPLES
sample:
times 2 dd 0
mmtime_end:
section .text
symbols:
extern _CreateThread@24
extern _waveOutOpen@24
extern _waveOutWrite@12
extern _waveOutGetPosition@12
extern _PeekMessageA@20
extern _TranslateMessage@4
extern _DispatchMessageA@4
global _mainCRTStartup
_mainCRTStartup:
; win32 uses the cdecl calling convention. This is more readable imo ;)
; We can also skip the prologue; Windows doesn't mind.
%ifdef SU_LOAD_GMDLS
call _su_load_gmdls
%endif ; SU_LOAD_GMDLS
times 2 push 0
push sound_buffer
lea eax, _su_render_song@4
push eax
times 2 push 0
call _CreateThread@24
; We render in the background while playing already. Fortunately,
; Windows is slow with the calls below, so we're not worried that
; we don't have enough samples ready before the track starts.
times 3 push 0
push wave_format
push WAVE_MAPPER
push wave_out_handle
call _waveOutOpen@24
push wave_header_end - wave_header
push wave_header
push dword [wave_out_handle]
call _waveOutWrite@12
; We need to handle windows messages properly while playing, as waveOutWrite is async.
mainloop:
dispatchloop:
push PM_REMOVE
times 3 push 0
push msg
call _PeekMessageA@20
jz dispatchloop_end
push msg
call _TranslateMessage@4
push msg
call _DispatchMessageA@4
jmp dispatchloop
dispatchloop_end:
push mmtime_end - mmtime
push mmtime
push dword [wave_out_handle]
call _waveOutGetPosition@12
cmp dword [sample], SU_LENGTH_IN_SAMPLES
jne mainloop
exit:
; At least we can skip the epilogue :)
leave
ret

View File

@ -0,0 +1,91 @@
%include TRACK_INCLUDE
%define WAVE_FORMAT_PCM 0x1
%define WAVE_FORMAT_IEEE_FLOAT 0x3
section .bss
sound_buffer:
resb SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
file:
resd 1
section .data
; Change the filename over -DFILENAME="yourfilename.wav"
filename:
db FILENAME, 0
format:
db "wb", 0
; This is the wave file header.
wave_file:
db "RIFF"
dd wave_file_end + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT - wave_file
db "WAVE"
db "fmt "
wave_format_end:
dd wave_format_end - wave_file
%ifdef SU_SAMPLE_FLOAT
dw WAVE_FORMAT_IEEE_FLOAT
%else ; SU_SAMPLE_FLOAT
dw WAVE_FORMAT_PCM
%endif ; SU_SAMPLE_FLOAT
dw SU_CHANNEL_COUNT
dd SU_SAMPLE_RATE
dd SU_SAMPLE_SIZE * SU_SAMPLE_RATE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * 8
wave_header_end:
db "data"
dd wave_file_end + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT - wave_header_end
wave_file_end:
section .text
symbols:
extern fopen
extern fwrite
extern fclose
global main
main:
; elf32 uses the cdecl calling convention. This is more readable imo ;)
; Prologue
push ebp
mov ebp, esp
sub esp, 0x10
; Unix does not have gm.dls, no need to ifdef and setup here.
; We render the complete track here.
push sound_buffer
call su_render_song
; Now we open the file and save the track.
push format
push filename
call fopen
mov dword [file], eax
; Write header
push dword [file]
push 0x1
push wave_file_end - wave_file
push wave_file
call fwrite
; write data
push dword [file]
push 0x1
push SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
push sound_buffer
call fwrite
push dword [file]
call fclose
exit:
; At least we can skip the epilogue :)
leave
ret

View File

@ -0,0 +1,102 @@
%define MANGLED
%include TRACK_INCLUDE
%define WAVE_FORMAT_PCM 0x1
%define WAVE_FORMAT_IEEE_FLOAT 0x3
%define FILE_ATTRIBUTE_NORMAL 0x00000080
%define CREATE_ALWAYS 2
%define GENERIC_WRITE 0x40000000
section .bss
sound_buffer:
resb SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
file:
resd 1
bytes_written:
resd 1
section .data
; Change the filename over -DFILENAME="yourfilename.wav"
filename:
db FILENAME, 0
; This is the wave file header.
wave_file:
db "RIFF"
dd wave_file_end + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT - wave_file
db "WAVE"
db "fmt "
wave_format_end:
dd wave_format_end - wave_file
%ifdef SU_SAMPLE_FLOAT
dw WAVE_FORMAT_IEEE_FLOAT
%else ; SU_SAMPLE_FLOAT
dw WAVE_FORMAT_PCM
%endif ; SU_SAMPLE_FLOAT
dw SU_CHANNEL_COUNT
dd SU_SAMPLE_RATE
dd SU_SAMPLE_SIZE * SU_SAMPLE_RATE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * 8
wave_header_end:
db "data"
dd wave_file_end + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT - wave_header_end
wave_file_end:
section .text
symbols:
extern _CreateFileA@28
extern _WriteFile@20
extern _CloseHandle@4
global _mainCRTStartup
_mainCRTStartup:
; Prologue
push ebp
mov ebp, esp
sub esp, 0x10
%ifdef SU_LOAD_GMDLS
call _su_load_gmdls
%endif ; SU_LOAD_GMDLS
; We render the complete track here.
push sound_buffer
call _su_render_song@4
; Now we open the file and save the track.
push 0x0
push FILE_ATTRIBUTE_NORMAL
push CREATE_ALWAYS
push 0x0
push 0x0
push GENERIC_WRITE
push filename
call _CreateFileA@28
mov dword [file], eax
; This is the WAV header
push 0x0
push bytes_written
push wave_file_end - wave_file
push wave_file
push dword [file]
call _WriteFile@20
; There we write the actual samples
push 0x0
push bytes_written
push SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE
push sound_buffer
push dword [file]
call _WriteFile@20
push dword [file]
call _CloseHandle@4
exit:
; At least we can skip the epilogue :)
leave
ret

View File

@ -0,0 +1,58 @@
# identifier: Name of the example
# songfile: File path of the song YAML file.
# architecture: 386 or amd64
# abi: 32 or 64
# windows_libraries: All libraries that you need to link on Windows
# unix_libraries: All libraries that you need to link on unix
function(add_asm_example identifier songfile architecture sizeof_void_ptr windows_libraries unix_libraries)
get_filename_component(songprefix ${songfile} NAME_WE)
# Generate the song assembly file
add_custom_command(
COMMAND
${compilecmd} -arch=${architecture} -o ${songprefix}_${architecture}.asm ${songfile}
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
DEPENDS
${songfile}
${compilecmd}
OUTPUT
${songprefix}_${architecture}.asm
${songprefix}_${architecture}.h
${songprefix}_${architecture}.inc
COMMENT
"Compiling ${PROJECT_SOURCE_DIR}/examples/patches/physics-girl-st.yml..."
)
# Platform dependent options
if(WIN32)
set(abi win)
set(libraries ${windows_libraries})
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(link_options -nostartfiles)
endif()
elseif(UNIX)
set(abi elf)
set(link_options -z noexecstack -no-pie)
set(libraries ${unix_libraries})
endif()
# Add target
add_executable(${identifier}-${architecture}
${identifier}.${abi}${sizeof_void_ptr}.asm
${songprefix}_${architecture}.asm
${songprefix}_${architecture}.inc
)
set_target_properties(${identifier}-${architecture} PROPERTIES ASM_NASM_COMPILE_OPTIONS -f${abi}${sizeof_void_ptr})
target_include_directories(${identifier}-${architecture} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
set_target_properties(${identifier}-${architecture} PROPERTIES LINKER_LANGUAGE C)
target_link_options(${identifier}-${architecture} PRIVATE -m${sizeof_void_ptr} ${link_options})
target_link_libraries(${identifier}-${architecture} PRIVATE ${libraries})
target_compile_definitions(${identifier}-${architecture} PRIVATE TRACK_INCLUDE="${songprefix}_${architecture}.inc")
# Set up dependencies
add_dependencies(examples ${identifier}-${architecture})
endfunction()
add_subdirectory(386)
add_subdirectory(amd64)

View File

@ -0,0 +1,12 @@
if(WIN32)
set(CMAKE_ASM_NASM_OBJECT_FORMAT win64)
elseif(UNIX)
set(CMAKE_ASM_NASM_OBJECT_FORMAT elf64)
endif()
set(CMAKE_ASM_NASM_COMPILE_OBJECT "<CMAKE_ASM_NASM_COMPILER> <INCLUDES> <DEFINES> <FLAGS> -f ${CMAKE_ASM_NASM_OBJECT_FORMAT} -o <OBJECT> <SOURCE>")
if(UNIX)
add_asm_example(asmplay "${PROJECT_SOURCE_DIR}/examples/patches/physics_girl_st.yml" amd64 64 "winmm" "asound;pthread")
add_asm_example(asmwav "${PROJECT_SOURCE_DIR}/examples/patches/physics_girl_st.yml" amd64 64 "" "")
target_compile_definitions(asmwav-amd64 PRIVATE FILENAME="physics_girl_st.wav")
endif()

View File

@ -0,0 +1,81 @@
%include TRACK_INCLUDE
%define SND_PCM_FORMAT_S16_LE 0x2
%define SND_PCM_FORMAT_FLOAT 0xE
%define SND_PCM_ACCESS_RW_INTERLEAVED 0x3
%define SND_PCM_STREAM_PLAYBACK 0x0
section .bss
sound_buffer:
resb SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
render_thread:
resq 1
pcm_handle:
resq 1
section .data
default_device:
db "default", 0
section .text
symbols:
extern pthread_create
extern sleep
extern snd_pcm_open
extern snd_pcm_set_params
extern snd_pcm_writei
global main
main:
; Prologue
push rbp
mov rbp, rsp
sub rsp, 0x10
; Unix does not have gm.dls, no need to ifdef and setup here.
; We render in the background while playing already.
mov rcx, sound_buffer
lea rdx, su_render_song
mov rsi, 0x0
mov rdi, render_thread
call pthread_create
; We can't start playing too early or the missing samples will be audible.
mov edi, 0x2
call sleep
; Play the track.
mov rdi, pcm_handle
mov rsi, default_device
mov rdx, SND_PCM_STREAM_PLAYBACK
mov rcx, 0x0
call snd_pcm_open
; This is unfortunate. amd64 ABI calling convention kicks in.
; now we have to maintain the stack pointer :/
mov rdi, qword [pcm_handle]
sub rsp, 0x8
push SU_LENGTH_IN_SAMPLES
%ifdef SU_SAMPLE_FLOAT
mov rsi, SND_PCM_FORMAT_FLOAT
%else ; SU_SAMPLE_FLOAT
mov rsi, SND_PCM_FORMAT_S16_LE
%endif ; SU_SAMPLE_FLOAT
mov rdx, SND_PCM_ACCESS_RW_INTERLEAVED
mov rcx, SU_CHANNEL_COUNT
mov r8d, SU_SAMPLE_RATE
mov r9d, 0x0
call snd_pcm_set_params
mov rdi, qword [pcm_handle]
mov rsi, sound_buffer
mov rdx, SU_LENGTH_IN_SAMPLES
call snd_pcm_writei
exit:
; At least we can skip the epilogue :)
leave
ret

View File

@ -0,0 +1,91 @@
%include TRACK_INCLUDE
%define WAVE_FORMAT_PCM 0x1
%define WAVE_FORMAT_IEEE_FLOAT 0x3
section .bss
sound_buffer:
resb SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
file:
resq 1
section .data
; Change the filename over -DFILENAME="yourfilename.wav"
filename:
db FILENAME, 0
format:
db "wb", 0
; This is the wave file header.
wave_file:
db "RIFF"
dd wave_file_end + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT - wave_file
db "WAVE"
db "fmt "
wave_format_end:
dd wave_format_end - wave_file
%ifdef SU_SAMPLE_FLOAT
dw WAVE_FORMAT_IEEE_FLOAT
%else ; SU_SAMPLE_FLOAT
dw WAVE_FORMAT_PCM
%endif ; SU_SAMPLE_FLOAT
dw SU_CHANNEL_COUNT
dd SU_SAMPLE_RATE
dd SU_SAMPLE_SIZE * SU_SAMPLE_RATE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * 8
wave_header_end:
db "data"
dd wave_file_end + SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT - wave_header_end
wave_file_end:
section .text
symbols:
extern fopen
extern fwrite
extern fclose
global main
main:
; elf32 uses the cdecl calling convention. This is more readable imo ;)
; Prologue
push rbp
mov rbp, rsp
sub rsp, 0x10
; Unix does not have gm.dls, no need to ifdef and setup here.
; We render the complete track here.
mov rdi, sound_buffer
call su_render_song
; Now we open the file and save the track.
mov rsi, format
mov rdi, filename
call fopen
mov qword [file], rax
; Write header
mov rcx, qword [file]
mov rdx, 0x1
mov rsi, wave_file_end - wave_file
mov rdi, wave_file
call fwrite
; write data
mov rcx, qword [file]
mov rdx, 0x1
mov rsi, SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
mov rdi, sound_buffer
call fwrite
mov rdi, qword [file]
call fclose
exit:
; At least we can skip the epilogue :)
leave
ret

View File

@ -0,0 +1,14 @@
Requirements: sointu binaries, `wabt`
To generate the .wasm file:
```
sointu-compile -o . -arch=wasm tests/test_chords.yml
wat2wasm --enable-bulk-memory test_chords.wat
```
To run the example:
```
npx serve examples/code/wasm
```

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>sointu WASM example</title>
</head>
<body>
<script type="module">
// button to start audio context
const button = document.createElement("button");
button.innerHTML = "start";
document.body.appendChild(button);
button.onclick = () => {
document.body.removeChild(button);
fetch("test_chords.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, { m: Math }))
.then(({ instance }) => {
const context = new AudioContext({ sampleRate: 44100 });
let frames = instance.exports.t.value
? instance.exports.l.value / 4
: instance.exports.l.value / 8;
let wasmBuffer = new Float32Array(
instance.exports.m.buffer,
instance.exports.s.value,
frames * 2
);
const buffer = context.createBuffer(2, frames, context.sampleRate);
// convert wasm buffer to audio buffer
for (let channel = 0; channel < 2; channel++) {
const buffering = buffer.getChannelData(channel);
for (let i = 0; i < frames; i++) {
buffering[i] = wasmBuffer[i * 2 + channel];
}
}
// connect to output and start playing
const src = context.createBufferSource();
src.buffer = buffer;
src.connect(context.destination);
src.start();
});
};
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More