217 Commits

Author SHA1 Message Date
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
303 changed files with 16027 additions and 6095 deletions

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

@ -0,0 +1,163 @@
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-track.exe
params: -ldflags -H=windowsgui cmd/sointu-track/main.go
- 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-native.exe
params: -ldflags -H=windowsgui -tags=native cmd/sointu-track/main.go
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-vsti.dll
params: -buildmode=c-shared -tags=plugin ./cmd/sointu-vsti/
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-vsti-native.dll
params: -buildmode=c-shared -tags="plugin,native" ./cmd/sointu-vsti/
- os: ubuntu-latest
asmnasm: /home/runner/nasm/nasm
output: sointu-track
params: 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-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-native
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 ./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-vsti-native.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: 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-compile
params: cmd/sointu-compile/main.go
- os: macos-12 # this is intel still
asmnasm: /Users/runner/nasm/nasm
output: sointu-track-native
params: -tags=native cmd/sointu-track/main.go
steps:
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- uses: lukka/get-cmake@latest
- 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:
go-version: '>=1.21.0'
- 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 -o ${{ matrix.config.output }} ${{ matrix.config.params }}
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: ${{ runner.os }}-${{ steps.short-sha.outputs.sha }}-${{ matrix.config.output }}
path: ${{ matrix.config.output }}
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,19 +19,13 @@ jobs:
config:
- os: ubuntu-latest
asmnasm: /home/runner/nasm/nasm
cmakeflags: -GNinja
maker: ninja
gotests: yes
cgo_ldflags:
- os: windows-latest
cmakeflags: -GNinja
maker: ninja
asmnasm: C:\Users\runneradmin\nasm\nasm
gotests: yes
cgo_ldflags:
- os: macos-latest
cmakeflags: -GNinja
maker: ninja
- os: macos-12 # this is intel still
asmnasm: /Users/runner/nasm/nasm
gotests: yes
cgo_ldflags: # -Wl,-no_pie
@ -40,31 +34,30 @@ jobs:
# 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:
go-version: '>=1.21.0'
- uses: actions/setup-node@v4
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.
- 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 ./vm ./vm/compiler/bridge ./vm/compiler

5
.gitignore vendored
View File

@ -29,3 +29,8 @@ out/
.cache/
actual_output/
**/__debug_bin
*.exe
*.dll
**/testdata/fuzz/
.DS_Store

522
4klang.go Normal file
View File

@ -0,0 +1,522 @@
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, version 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, v = 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, version int) []Unit {
flags := vals[2]
var stereo, lowpass, bandpass, highpass, neghighpass 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
neghighpass = 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,
"negbandpass": 0,
"neghighpass": neghighpass,
}},
}
}
func read4klangDST(vals [15]byte, version 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, version 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, version 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, version 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, version int) []Unit {
return []Unit{{
Type: "pan",
Parameters: map[string]int{
"stereo": 0,
"panning": int(vals[0]),
}}}
}
func read4klangOUT(vals [15]byte, version 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, version 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, version int) []Unit {
return []Unit{{
Type: "loadval",
Parameters: map[string]int{"stereo": 0, "value": int(vals[0])},
}}
}

View File

@ -3,23 +3,203 @@ 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.4.1]
### 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
- 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])
[Unreleased]: https://github.com/vsariola/sointu/compare/4klang-3.11...HEAD
### 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.4.1...HEAD
[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
[i65]: https://github.com/vsariola/sointu/issues/65
[i68]: https://github.com/vsariola/sointu/issues/68
[i112]: https://github.com/vsariola/sointu/issues/112
[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
[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
[i139]: https://github.com/vsariola/sointu/issues/139
[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

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()

411
README.md
View File

@ -1,13 +1,33 @@
# 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 will be in the [Wiki](https://github.com/vsariola/sointu/wiki).
Installation
------------
You can either 1) download the prebuilt release binaries from the
[releases](https://github.com/vsariola/sointu/releases); or 2) 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). Then just run one of
the executables or, in the case of the VST plugins library files, copy them
wherever you keep you VST2 plugins.
The pre 1.0 version tags are mostly for reference: no backwards
compatibility will be guaranteed while upgrading to a newer version.
Backwards compatibility will be attempted from 1.0 onwards.
**Uninstallation**: Sointu stores recovery data in OS-specific folders
e.g. `AppData/Roaming/Sointu` on Windows. For clean uninstall, delete
also this folder. See [here](https://pkg.go.dev/os#UserConfigDir) where
to find those folders on other platforms.
Summary
-------
@ -24,20 +44,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,52 +66,106 @@ 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 use the faster x86 assembly written synthesizer:
- 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/). We use the [tdm-gcc](https://jmeubank.github.io/tdm-gcc/).
The compiler can be in PATH or you can use the environment variable
`CC` to help go find the compiler.
- Setting environment variable `CGO_ENABLED=1` is a good idea,
because if it is not set and go fails to find the compiler, go just
excludes all files with `import "C"` from the build, resulting in
lots of errors about missing types.
#### Running
```
go run cmd/sointu-track/main.go
```
Building 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 use the [x86 native virtual machine](#native-virtual-machine),
add `-tags=native` to all the commands e.g.
> :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
> not limited to 8 items. If you intent to compile the patch to x86/amd64
> targets, make sure not to use too much stack. Keeping at most 5 signals in the
> stack is presumably fine (reserving 3 for the temporary variables of the
> opcodes). In future, the app should give warnings if the user is about to
> exceed the capabilities of a target platform.
```
go build -o sointu-track.exe -tags=native cmd/sointu-track/main.go
```
### Compiler
### 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/). We use the [tdm-gcc](https://jmeubank.github.io/tdm-gcc/).
The compiler can be in PATH or you can use the environment variable
`CC` to help go find the compiler.
- Setting environment variable `CGO_ENABLED=1` is a good idea, because
if it is not set and go fails to find the compiler, go just excludes
all files with `import "C"` from the build, resulting in lots of
errors about missing types.
- If you want to use the faster x86 assembly written synthesizer:
- 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 the compiler:
#### Running
```
go run cmd/sointu-compile/main.go
```
Building the compiler:
#### Building an executable
```
go build -o sointu-compile cmd/sointu-compile/main.go
go build -o sointu-compile.exe cmd/sointu-compile/main.go
```
On windows, replace `-o sointu-compile` with `-o sointu-compile.exe`.
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:
@ -111,18 +182,113 @@ sointu-compile -o . -arch=wasm tests/test_chords.yml
wat2wasm --enable-bulk-memory test_chords.wat
```
### Building and running the tests as executables
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. It's
likely slightly faster than the interpreter. Before you can actually run it, you
need to build the bridge using CMake (thus, ***this will not work with go
get***).
#### Prerequisites
- [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
> not limited to 8 items. If you intent to compile the patch to x86/amd64
> targets, make sure not to use too much stack. Keeping at most 5 signals in the
> stack is presumably fine (reserving 3 for the temporary variables of the
> opcodes). In future, the app should give warnings if the user is about to
> exceed the capabilities of a target platform.
> :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.
### Tests
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.
#### Prerequisites
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,83 +308,14 @@ 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).
- **Compiler**. Written in go. The input is a .yml file and the output is an
@ -228,7 +325,8 @@ New features since fork
entropy as low as possible, yet we can call arbitrary go functions as
"macros". The templates are [here](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
@ -262,12 +360,12 @@ New features since fork
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 +376,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 +395,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
@ -316,20 +415,6 @@ New features since fork
- **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
------------
@ -357,10 +442,6 @@ 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.
Design philosophy
-----------------
@ -415,19 +496,45 @@ 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
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)

251
audio.go
View File

@ -1,11 +1,250 @@
package sointu
type AudioSink interface {
WriteAudio(buffer []float32) error
Close() error
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
)
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
// AudioOutput represents something where we can send audio e.g. audio output.
// WriteAudio should block if not ready to accept audio e.g. buffer full.
AudioOutput interface {
WriteAudio(buffer AudioBuffer) error
Close() error
}
// 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 create one or more AudioOutputs with Output(); each
// can be used to output separate sound & closed when done.
AudioContext interface {
Output() AudioOutput
Close() error
}
// 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)
}
// Synther compiles a given Patch into a Synth, throwing errors if the
// Patch is malformed.
Synther interface {
Synth(patch Patch, bpm int) (Synth, error)
}
)
// 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)
}
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
}
// 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 (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
}

View File

@ -0,0 +1,7 @@
//go:build native
package cmd
import "github.com/vsariola/sointu/vm/compiler/bridge"
var MainSynther = bridge.NativeSynther{}

View File

@ -0,0 +1,7 @@
//go:build !native
package cmd
import "github.com/vsariola/sointu/vm"
var MainSynther = vm.GoSynther{}

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

@ -21,7 +21,6 @@ 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.")
@ -88,16 +87,7 @@ 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(bridge.NativeSynther{}, song, nil) // render the song to calculate its length
if err != nil {
return fmt.Errorf("sointu.Play failed: %v", err)
}
@ -109,7 +99,7 @@ func main() {
}
}
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 +108,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)
}

View File

@ -3,31 +3,94 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"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"
)
type NullContext struct {
}
func (NullContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
return tracker.MIDINoteEvent{}, false
}
func (NullContext) BPM() (bpm float64, ok bool) {
return 0, false
}
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
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", "sointu-track-recovery")
}
synthService := vm.SynthService{}
gioui.Main(audioContext, synthService, syncChannel)
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
if a := flag.Args(); len(a) > 0 {
f, err := os.Open(a[0])
if err == nil {
model.ReadSong(f)
}
f.Close()
}
tracker := gioui.NewTracker(model)
output := audioContext.Output()
defer output.Close()
go func() {
buf := make(sointu.AudioBuffer, 1024)
ctx := NullContext{}
for {
player.Process(buf, ctx)
output.WriteAudio(buf)
}
}()
go func() {
tracker.Main()
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()
}

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

@ -0,0 +1,136 @@
//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) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
for c.eventIndex < len(c.events) {
ev := c.events[c.eventIndex]
c.eventIndex++
switch {
case ev.Data[0] >= 0x80 && ev.Data[0] < 0x90:
channel := ev.Data[0] - 0x80
note := ev.Data[1]
return tracker.MIDINoteEvent{Frame: int(ev.DeltaFrames), On: false, Channel: int(channel), Note: note}, true
case ev.Data[0] >= 0x90 && ev.Data[0] < 0xA0:
channel := ev.Data[0] - 0x90
note := ev.Data[1]
return tracker.MIDINoteEvent{Frame: int(ev.DeltaFrames), On: true, Channel: int(channel), Note: note}, true
default:
// ignore all other MIDI messages
}
}
return tracker.MIDINoteEvent{}, false
}
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 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", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes))
}
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
t := gioui.NewTracker(model)
tracker.Bool{BoolData: (*tracker.InstrEnlarged)(model)}.Set(true)
if s := h.GetSampleRate(); math.Abs(float64(h.GetSampleRate()-44100.0)) > 1e-6 {
model.Alerts().AddAlert(tracker.Alert{
Message: fmt.Sprintf("VSTi host sample rate is %.0f Hz; sointu supports 44100 Hz only", s),
Priority: tracker.Error,
Duration: 10 * time.Second,
})
}
go t.Main()
context := VSTIProcessContext{host: h}
buf := make(sointu.AudioBuffer, 1024)
return vst2.Plugin{
UniqueID: PLUGIN_ID,
Version: version,
InputChannels: 0,
OutputChannels: 2,
Name: PLUGIN_NAME,
Vendor: "vsariola/sointu",
Category: vst2.PluginCategorySynth,
Flags: vst2.PluginIsSynth,
ProcessFloatFunc: func(in, out vst2.FloatBuffer) {
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]
}
context.events = context.events[:0] // reset buffer, but keep the allocated memory
context.eventIndex = 0
},
}, 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(ev *vst2.EventsPtr) {
for i := 0; i < ev.NumEvents(); i++ {
a := ev.Event(i)
switch v := a.(type) {
case *vst2.MIDIEvent:
context.events = append(context.events, *v)
}
}
},
CloseFunc: func() {
t.Exec() <- func() { t.ForceQuit().Do() }
t.WaitQuitted()
},
GetChunkFunc: func(isPreset bool) []byte {
retChn := make(chan []byte)
t.Exec() <- func() { retChn <- t.MarshalRecovery() }
return <-retChn
},
SetChunkFunc: func(data []byte, isPreset bool) {
t.Exec() <- func() { t.UnmarshalRecovery(data) }
},
}
}
}
func main() {}

View File

@ -0,0 +1,6 @@
//go:build native
package main
var PLUGIN_ID = [4]byte{'S', 'n', 't', 'N'}
var PLUGIN_NAME = "Sointu Native"

View File

@ -0,0 +1,6 @@
//go:build !native
package main
var PLUGIN_ID = [4]byte{'S', 'n', 't', 'u'}
var PLUGIN_NAME = "Sointu"

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.unix.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,39 @@
#include <alsa/asoundlib.h>
#include <pthread.h>
#include <stdio.h>
#include <stdint.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;
static uint32_t render_thread_handle;
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
);
snd_pcm_writei(pcm_handle, sound_buffer, SU_LENGTH_IN_SAMPLES);
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,63 @@
#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
};
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.

Binary file not shown.

Binary file not shown.

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