mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-12 17:14:43 -04:00
Compare commits
492 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4d0683be7 | |||
| e57e55fde6 | |||
| c793d29592 | |||
| c52c074aa1 | |||
| 4cb9308af3 | |||
| f3bb0001cd | |||
| 4d29a191c8 | |||
| 92859a5e58 | |||
| 558ca05236 | |||
| bf29421246 | |||
| a994d831ee | |||
| b8d9ca09f1 | |||
| f2f76c0e18 | |||
| cd4b85a66b | |||
| 942da94982 | |||
| e66ff8be9f | |||
| b349474c4d | |||
| 0179b24fd4 | |||
| 77b27257fe | |||
| cc8d737f8a | |||
| f2ef57a845 | |||
| 6e8acc8f9b | |||
| 287bd036a6 | |||
| 4bb5df9c87 | |||
| ca4b87d43d | |||
| 86ca3fb300 | |||
| b93304adab | |||
| 173648fbdb | |||
| 651ceb3cbb | |||
| 1693d7ed5e | |||
| 74beb6760c | |||
| 6629a9fdfa | |||
| 60222dded4 | |||
| 810998d95b | |||
| 3a7010f897 | |||
| 4d09e04a49 | |||
| 33ee80a908 | |||
| 9b87589f7b | |||
| 16c652b2ba | |||
| dafd45fd81 | |||
| 05b64dadc8 | |||
| 6978dd4afe | |||
| fa9654d311 | |||
| 3495d91a4a | |||
| 628365c486 | |||
| 9db6b669c9 | |||
| a37990a7fa | |||
| 4a46d601f2 | |||
| da6226d3ff | |||
| 1dbe351beb | |||
| 91c9701f14 | |||
| 48dc4a35bb | |||
| 9b9dc3548f | |||
| c583156d1b | |||
| c1ea47a509 | |||
| 60ae8645b6 | |||
| 362bc3029f | |||
| 213202a7e0 | |||
| e08af03fb2 | |||
| 8e99c93d14 | |||
| f4bb2bc754 | |||
| de366316d4 | |||
| 7a43aec50e | |||
| 82cf34a28f | |||
| 2336a135c6 | |||
| 3f365707c2 | |||
| 34c0045652 | |||
| c64422767e | |||
| 1dcd3fe3c6 | |||
| c0488226d2 | |||
| f894e2ee86 | |||
| 54a8358522 | |||
| bdfe2d37bf | |||
| 167f541a52 | |||
| be48f5824f | |||
| 989b6e605b | |||
| 7459437822 | |||
| 55f9c36bd5 | |||
| a09b52a912 | |||
| 74fea4138f | |||
| f13a5cd2df | |||
| 7f3010a4a6 | |||
| 5839471bcc | |||
| fe0106bb60 | |||
| 3163f46447 | |||
| 13102aa7d6 | |||
| 399bac481c | |||
| 072e4ee208 | |||
| edc0782f5f | |||
| 697fb05b5c | |||
| cf86f3f1c8 | |||
| 8e5f3098a4 | |||
| 452a4cf04f | |||
| 5841848813 | |||
| 0ce79978d5 | |||
| 4138c34574 | |||
| 172fbaeb2a | |||
| 666af9433e | |||
| c3caa8de11 | |||
| 18d7848367 | |||
| 192909328c | |||
| cb4c020061 | |||
| d78ef98e73 | |||
| 08c36ed462 | |||
| d276f52942 | |||
| b8cf70e8e9 | |||
| e59fbb50cf | |||
| ba281ca7c0 | |||
| b4ec136ab1 | |||
| 18d198d764 | |||
| 355ccefb6f | |||
| 7a030683c6 | |||
| 17ca15b205 | |||
| 58f6cceb9a | |||
| b79de95f91 | |||
| f6bc5fffcd | |||
| 33f7b5fb6a | |||
| 5f43bc3067 | |||
| fb0fa4af92 | |||
| 6f1db6b392 | |||
| 31007515b5 | |||
| db2ccf977d | |||
| 0ea20ea5bf | |||
| beef8fe1e0 | |||
| 289bfb0605 | |||
| a601b98b74 | |||
| 602b3b05cc | |||
| 3881b8eb22 | |||
| 4fa0e04788 | |||
| b291959a97 | |||
| 840fe3ef0e | |||
| 430b01d143 | |||
| 28a0006b6a | |||
| 8eb5f17f73 | |||
| f47bee37b0 | |||
| 4f2c73d0db | |||
| c77d541dc6 | |||
| 340620ed49 | |||
| 1a13fadd75 | |||
| b6e8ab5c25 | |||
| c6b70560f6 | |||
| 1eea263dc9 | |||
| c023dc08b8 | |||
| 0e32608872 | |||
| 283fbc1171 | |||
| 7ef868a434 | |||
| 4f779edb88 | |||
| d20a23d57b | |||
| de2e64533d | |||
| 74f37318d6 | |||
| fb3a0da3ed | |||
| 036cb1f34d | |||
| d6badb97be | |||
| d342c9961d | |||
| 32f1e1baea | |||
| 5b260d19f5 | |||
| ddbaf6a4bb | |||
| 27bf8220c0 | |||
| 448bc9f236 | |||
| afb1fee4ed | |||
| 8245fbda24 | |||
| 0f42a993dc | |||
| 554a840982 | |||
| 9f89c37956 | |||
| 0199658025 | |||
| afc6b1f4a9 | |||
| 3623bdf5b2 | |||
| fe9daf7988 | |||
| bf0d697b80 | |||
| f72f29188b | |||
| 5fd78d8362 | |||
| 805b98524c | |||
| 54176cc2b3 | |||
| 845f0119c8 | |||
| 5a3c859a51 | |||
| 5c0b86a0f0 | |||
| e0392323c0 | |||
| bb605ffa0b | |||
| 40be82de46 | |||
| 42c95ab8ee | |||
| d0413e0a13 | |||
| bdf9e2ba0c | |||
| 95af8da939 | |||
| 78fc6302a0 | |||
| ea4dee9285 | |||
| ae217665bf | |||
| 46a9c7dab3 | |||
| 5ee7e44ed7 | |||
| dd7b5ddc84 | |||
| ee229d8d94 | |||
| 6ba595e7ff | |||
| 7ff3c942cb | |||
| 4169356845 | |||
| 8d71cf3ca7 | |||
| b255a68ebc | |||
| d517576a65 | |||
| 4d7c998fc2 | |||
| 55c062a390 | |||
| b423d04c17 | |||
| 639b2266e3 | |||
| 8d7d896375 | |||
| 04deac5722 | |||
| 6337101985 | |||
| 8074fd71d3 | |||
| 37769fcc9c | |||
| 76322bb541 | |||
| 1c601858ae | |||
| 65a7f060ec | |||
| b08f5d4b1e | |||
| 2a2934b4e4 | |||
| 9d59cfb3b6 | |||
| 19661f90ea | |||
| 94058c2603 | |||
| 943073d0cc | |||
| b73fc0b95b | |||
| ee3ab3bf86 | |||
| 2aa0aaee0c | |||
| 3eb4d86d52 | |||
| ec222bd67d | |||
| 86c65939bb | |||
| 7417170a8b | |||
| daf7fb1519 | |||
| eb9413b9a0 | |||
| 8dfadacafe | |||
| 216cde2365 | |||
| 025f8832d9 | |||
| 1c42a51cc6 | |||
| 0ba6557f65 | |||
| 3306c431c3 | |||
| 9bce1cb3d5 | |||
| 63c08d53fe | |||
| 063b2c29c5 | |||
| 7b213bd8b0 | |||
| 27b6bc57d2 | |||
| 00b8e1872a | |||
| 04ca0a3f6e | |||
| 08386323ed | |||
| 7470413ad8 | |||
| 5099c61705 | |||
| b494a69a76 | |||
| 3986bbede7 | |||
| 97e59c5650 | |||
| 2b7ce39069 | |||
| 03c994e4da | |||
| cd88ea0680 | |||
| f8f0e11b76 | |||
| 2809526de6 | |||
| f427eca1f4 | |||
| c07d8000c6 | |||
| 577265b250 | |||
| 9779beee99 | |||
| 160eb8eea9 | |||
| 3fb7f07c2c | |||
| 10f021a497 | |||
| 3a7ab0416a | |||
| 4c096a3fac | |||
| 59c04ed4a1 | |||
| a6bb5c2afc | |||
| 5c51932f60 | |||
| 773655ef9c | |||
| 91b7850bf7 | |||
| b4a63ce362 | |||
| a94703deea | |||
| ad5f7628a5 | |||
| b538737643 | |||
| 47d7568552 | |||
| 81a6d1acea | |||
| 890ebe3294 | |||
| bf5579a2d2 | |||
| 8fd2df19a1 | |||
| ce673578fd | |||
| 0e10cd2ae8 | |||
| 4ee355bb45 | |||
| 7d6daba3d2 | |||
| 2b38e11643 | |||
| f8c522873c | |||
| e49f699f62 | |||
| 6924b63e02 | |||
| 6fc9277113 | |||
| 877556b428 | |||
| 5e65410d27 | |||
| 4e1fdf57d9 | |||
| 1daaf1829c | |||
| 74972b5ff4 | |||
| 9da6c2216c | |||
| 61e7da5dab | |||
| 59fb39d9b3 | |||
| 9cb573d965 | |||
| d46605c638 | |||
| 569958547e | |||
| 012ed10851 | |||
| 5bc6dc6015 | |||
| 350402f8f3 | |||
| 75bd9c591e | |||
| 2667c3c72c | |||
| e09af5ab34 | |||
| db2d9cac9d | |||
| a14e21dff6 | |||
| 58916d3c6d | |||
| 84d90cf0f3 | |||
| 10d20cd26f | |||
| 4a8d4c5a29 | |||
| f074c392f6 | |||
| 20fc12c529 | |||
| 6d4529971c | |||
| beb84d7652 | |||
| c55b27b23b | |||
| e488cd391b | |||
| 7f20bd8baf | |||
| 07bf8f6cdf | |||
| f0f391356c | |||
| b18a284252 | |||
| 1c020fffa3 | |||
| 267973e061 | |||
| 6b3aaf6cc9 | |||
| dfc72cd2c4 | |||
| 8a9cbdea62 | |||
| edee3452f4 | |||
| b70db4d394 | |||
| d5af39e324 | |||
| aa1b4d371b | |||
| dc12f58082 | |||
| aa7a2e56fa | |||
| 17312bbe4e | |||
| 2b3f6d8200 | |||
| db6c9f6052 | |||
| 954b306cc8 | |||
| aec756f921 | |||
| ca4a98eb50 | |||
| 65cfcb045c | |||
| bb32403c78 | |||
| d92426a100 | |||
| 6d3c65e11d | |||
| c08a319eb7 | |||
| 8227691523 | |||
| 04fbc9f6a7 | |||
| f698986718 | |||
| a38a0f4235 | |||
| 3c85f1155c | |||
| 1040eb585d | |||
| 6eb025d7ba | |||
| 9ec8f48f82 | |||
| 391b14493c | |||
| 486bab4185 | |||
| 1e47c5004c | |||
| 900f1611b1 | |||
| beb06727b0 | |||
| b6ec5d1a04 | |||
| ff8e662857 | |||
| a60814bab7 | |||
| 0ce5ca3003 | |||
| 14a0306064 | |||
| d342fb860b | |||
| 453f45c48a | |||
| 50ccfe03da | |||
| 1a8a317464 | |||
| a9517f1511 | |||
| 9073adadb3 | |||
| b772940b1f | |||
| d6abb14b08 | |||
| 64270eaf68 | |||
| 43707e5fd6 | |||
| fdad626279 | |||
| 960bddfae0 | |||
| 7675121a78 | |||
| e28891abd5 | |||
| e010b2da9d | |||
| 98a73795c7 | |||
| 5bbec75120 | |||
| ff4155a08e | |||
| 15a340317f | |||
| b6815f70cb | |||
| 9f7bbce761 | |||
| 01bf409929 | |||
| 87604dd92e | |||
| ccd283d2ea | |||
| 0a67129a0c | |||
| e4a2ed9f32 | |||
| 0187cc66ec | |||
| 33625c6f40 | |||
| 38e9007bf8 | |||
| bb0d4d6800 | |||
| b97d269cc4 | |||
| 192b31917a | |||
| 462faf5f4e | |||
| 97a1b2f766 | |||
| 4899b027ff | |||
| 1a256b1f01 | |||
| b455ef0f3c | |||
| 94589eb2eb | |||
| f5eeabe5f3 | |||
| 61ebd89da0 | |||
| e5691d670a | |||
| 12dd3dada0 | |||
| 8c8232f76e | |||
| 7ee43f199a | |||
| 048de55f00 | |||
| 905637eee3 | |||
| ce7c8a0d3e | |||
| b65d11cbb7 | |||
| df2605fddd | |||
| 12f15d1066 | |||
| e3c7d2cba4 | |||
| 545f32bcc3 | |||
| ee2c83e2cb | |||
| 00850c8001 | |||
| f35f948118 | |||
| 7df8103bf9 | |||
| 1ac2ad3c75 | |||
| 20b0598a57 | |||
| 14e548c4c1 | |||
| c692ff0f16 | |||
| b028fea59a | |||
| 231e055faf | |||
| de3f4d987f | |||
| 8c59ea1b4c | |||
| 98fedd0ed2 | |||
| 607e5b5da0 | |||
| a439a4fa48 | |||
| 29a33a154b | |||
| aba8ff2c85 | |||
| d0efcc3001 | |||
| dff484739c | |||
| 7dd2c246a0 | |||
| 6ec06c760a | |||
| 4135286ed0 | |||
| c7d79035ce | |||
| 568aa1d76d | |||
| d82d151f49 | |||
| c040bdedee | |||
| a0bcac3904 | |||
| 33221b5203 | |||
| 94926c5596 | |||
| 61776f397a | |||
| 5884a8d195 | |||
| cafb43f8c8 | |||
| 5a2e87982e | |||
| 338529012a | |||
| ffb2f18c68 | |||
| ccc8dc906f | |||
| c421748db9 | |||
| 9db6ecb3da | |||
| 8ffe4a70dd | |||
| d2ddba3944 | |||
| 7af7d4332d | |||
| 9d6ca519a2 | |||
| 3da62179e4 | |||
| 8c4f7ee61f | |||
| f5980ecb79 | |||
| 63fc3d0d08 | |||
| 9ef271f1a8 | |||
| cd00067da8 | |||
| 248ba483c6 | |||
| c06ac6ea5e | |||
| a3dcc829c0 | |||
| e7dbb0289c | |||
| 9efddd673d | |||
| cd700ed954 | |||
| 70080c2b9d | |||
| 61c2e980a2 | |||
| 6129076e97 | |||
| e73365b980 | |||
| 7eb473e67e | |||
| 1a5251dbf6 | |||
| eda48491e2 | |||
| a8f8911f03 | |||
| a9b90c4db8 | |||
| 60e4518230 | |||
| 7885c306ee | |||
| ede70380f2 | |||
| 8a94058d44 | |||
| 203e8a3ccc | |||
| a2723829da | |||
| ce6e5d4942 | |||
| 1a89fee665 | |||
| e9834110ec | |||
| e649b9ec54 | |||
| d5f413c5dc | |||
| 5aa16b4a97 | |||
| 442715334e | |||
| d55e9e9880 | |||
| 15cf8a750c | |||
| b2b15f825d | |||
| e544e955cb | |||
| c0a0a5d501 | |||
| 8ba9fb1f00 | |||
| 56ceafdaa6 | |||
| cbc07764a0 | |||
| 40d4d6576e | |||
| 147e8a2513 | |||
| ac95fb65c4 | |||
| 485b783341 |
216
.github/workflows/binaries.yml
vendored
Normal file
216
.github/workflows/binaries.yml
vendored
Normal file
@ -0,0 +1,216 @@
|
||||
name: Binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
name: Create release
|
||||
runs-on: ubuntu-latest
|
||||
# Note this. We are going to use that in further jobs.
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref_name }}
|
||||
body_path: CHANGELOG.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
binaries:
|
||||
needs: create_release # we need to know the upload URL
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-compile.exe
|
||||
params: cmd/sointu-compile/main.go
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-track.exe
|
||||
params: -tags=native cmd/sointu-track/main.go
|
||||
ldflags: -H=windowsgui
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-vsti.dll
|
||||
params: -buildmode=c-shared -tags="plugin,native" ./cmd/sointu-vsti/
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-play.exe
|
||||
params: cmd/sointu-play/main.go
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-compile
|
||||
params: cmd/sointu-compile/main.go
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-track
|
||||
params: -tags=native cmd/sointu-track/main.go
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-vsti.so
|
||||
params: -buildmode=c-shared -tags="plugin,native" ./cmd/sointu-vsti/
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-play
|
||||
params: cmd/sointu-play/main.go
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: macos-latest
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-compile
|
||||
params: cmd/sointu-compile/main.go
|
||||
- os: macos-latest
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-track
|
||||
params: cmd/sointu-track/main.go
|
||||
- os: macos-latest
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-vsti.a
|
||||
bundleoutput: sointu-vsti
|
||||
params: -buildmode=c-archive -tags="plugin" ./cmd/sointu-vsti/
|
||||
bundle: true
|
||||
- os: macos-latest
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-play
|
||||
params: cmd/sointu-play/main.go
|
||||
steps:
|
||||
- uses: benjlevesque/short-sha@v3.0
|
||||
id: short-sha
|
||||
with:
|
||||
length: 7
|
||||
- uses: lukka/get-cmake@latest
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
|
||||
with:
|
||||
go-version: '>=1.23.8 <1.23.9'
|
||||
- uses: ilammy/setup-nasm@v1.5.1
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: ${{ matrix.config.packages }}
|
||||
version: 1.0
|
||||
if: runner.os == 'Linux'
|
||||
- name: Build library
|
||||
env:
|
||||
ASM_NASM: ${{ matrix.config.asmnasm }}
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -GNinja ..
|
||||
ninja sointu
|
||||
- name: Build binary
|
||||
run: |
|
||||
go build -ldflags "-X github.com/vsariola/sointu/version.Version=$(git describe) ${{ matrix.config.ldflags}}" -o ${{ matrix.config.output }} ${{ matrix.config.params }}
|
||||
- name: Upload binary
|
||||
if: matrix.config.bundle != true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ runner.os }}-${{ steps.short-sha.outputs.sha }}-${{ matrix.config.output }}
|
||||
path: ${{ matrix.config.output }}
|
||||
- name: Bundle VST
|
||||
if: matrix.config.bundle
|
||||
run: | # following https://github.com/RustAudio/vst-rs/blob/master/osx_vst_bundler.sh
|
||||
mkdir -p "bundle/${{ matrix.config.bundleoutput }}.vst/Contents/MacOS"
|
||||
clang++ -D__MACOSX_CORE__ -framework CoreServices -framework CoreAudio -framework CoreMIDI -framework CoreFoundation -L./build/ -lsointu -bundle -o bundle/${{ matrix.config.bundleoutput }} -all_load ${{ matrix.config.output }}
|
||||
echo "BNDL????" > "bundle/${{ matrix.config.bundleoutput }}.vst/Contents/PkgInfo"
|
||||
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||||
<plist version=\"1.0\">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${{ matrix.config.bundleoutput }}</string>
|
||||
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>vst</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.vsariola.${{ matrix.config.bundleoutput }}</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>${{ matrix.config.bundleoutput }}</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
|
||||
<key>CFBundleSignature</key>
|
||||
<string>$((RANDOM % 9999))</string>
|
||||
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<string></string>
|
||||
|
||||
</dict>
|
||||
</plist>" > "bundle/${{ matrix.config.bundleoutput }}.vst/Contents/Info.plist"
|
||||
mv "bundle/${{ matrix.config.bundleoutput }}" "bundle/${{ matrix.config.bundleoutput }}.vst/Contents/MacOS/${{ matrix.config.bundleoutput }}"
|
||||
- name: Upload bundle
|
||||
if: matrix.config.bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ runner.os }}-${{ steps.short-sha.outputs.sha }}-${{ matrix.config.bundleoutput }}
|
||||
path: bundle
|
||||
upload_release_asset:
|
||||
needs: [create_release, binaries]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: Windows
|
||||
- os: Linux
|
||||
- os: macOS
|
||||
steps:
|
||||
- uses: benjlevesque/short-sha@v2.2
|
||||
id: short-sha
|
||||
with:
|
||||
length: 7
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: ${{ matrix.config.os }}-${{ steps.short-sha.outputs.sha }}-*
|
||||
merge-multiple: true
|
||||
path: sointu-${{ matrix.config.os }}
|
||||
- name: Zip binaries
|
||||
run: |
|
||||
zip ./sointu-${{ matrix.config.os }}.zip sointu-${{ matrix.config.os }}/*
|
||||
- name: Upload release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_name: sointu-${{ matrix.config.os }}.zip
|
||||
asset_path: ./sointu-${{ matrix.config.os }}.zip
|
||||
asset_content_type: application/octet-stream
|
||||
36
.github/workflows/tests.yml
vendored
36
.github/workflows/tests.yml
vendored
@ -19,52 +19,48 @@ jobs:
|
||||
config:
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
cmakeflags: -GNinja
|
||||
maker: ninja
|
||||
gotests: yes
|
||||
gotestcases: ./vm ./vm/compiler/bridge ./vm/compiler
|
||||
cgo_ldflags:
|
||||
- os: windows-latest
|
||||
cmakeflags: -GNinja
|
||||
maker: ninja
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
gotests: yes
|
||||
gotestcases: ./vm ./vm/compiler/bridge ./vm/compiler
|
||||
cgo_ldflags:
|
||||
- os: macos-latest
|
||||
cmakeflags: -GNinja
|
||||
maker: ninja
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
gotests: yes
|
||||
gotestcases: ./vm ./vm/compiler
|
||||
cgo_ldflags: # -Wl,-no_pie
|
||||
# ld on mac is complaining about position dependent code so this would take the errors away, BUT
|
||||
# suddenly this causes an error, even though worked last week. Let's accept the warnings rather
|
||||
# than let the tests fail because of this.
|
||||
# TODO: win32 builds didn't quite work out, complains gcc broken
|
||||
steps:
|
||||
- uses: lukka/get-cmake@v3.18.3
|
||||
- uses: vsariola/setup-wabt@v1.0.1
|
||||
- uses: lukka/get-cmake@latest
|
||||
- uses: vsariola/setup-wabt@v1.0.2
|
||||
with:
|
||||
version: 1.0.20
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-node@v2
|
||||
version: 1.0.29
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
|
||||
with:
|
||||
node-version: '15'
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ilammy/setup-nasm@v1.2.0
|
||||
- name: Install libasound2-dev # sointu-cli has alsa as dependency for playing sound and
|
||||
if: ${{ matrix.config.os == 'ubuntu-latest' }} # ubuntu was complaining about "Package alsa was not found in the pkg-config search path.",
|
||||
run: sudo apt install libasound2-dev # leading to tests failing. This fixes that.
|
||||
go-version: '>=1.21.0'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- uses: ilammy/setup-nasm@v1.5.1
|
||||
- name: Run ctest
|
||||
env:
|
||||
ASM_NASM: ${{ matrix.config.asmnasm }}
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ${{ matrix.config.cmakeflags }} ..
|
||||
${{ matrix.config.maker }}
|
||||
cmake -GNinja ..
|
||||
ninja tests/all sointu
|
||||
ctest --output-on-failure
|
||||
- name: Run go test
|
||||
if: ${{ matrix.config.gotests == 'yes' }}
|
||||
env:
|
||||
CGO_LDFLAGS: ${{ matrix.config.cgo_ldflags }}
|
||||
run: |
|
||||
go test ./vm ./vm/compiler/bridge ./vm/compiler ./oto
|
||||
go test ${{ matrix.config.gotestcases }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -17,8 +17,9 @@ build/
|
||||
# Project specific
|
||||
old/
|
||||
|
||||
# VS Code
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# project specific
|
||||
# this is autogenerated from bridge.go.in
|
||||
@ -29,3 +30,8 @@ out/
|
||||
.cache/
|
||||
actual_output/
|
||||
**/__debug_bin
|
||||
*.exe
|
||||
*.dll
|
||||
|
||||
**/testdata/fuzz/
|
||||
.DS_Store
|
||||
520
4klang.go
Normal file
520
4klang.go
Normal file
@ -0,0 +1,520 @@
|
||||
package sointu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Read4klangPatch reads a 4klang patch (a file usually with .4kp extension)
|
||||
// from r and returns a Patch, making best attempt to convert 4klang file to a
|
||||
// sointu Patch. It returns an error if the file is malformed or if the 4kp file
|
||||
// version is not supported.
|
||||
func Read4klangPatch(r io.Reader) (patch Patch, err error) {
|
||||
var versionTag uint32
|
||||
var version int
|
||||
var polyphonyUint32 uint32
|
||||
var instrumentNames [_4KLANG_MAX_INSTRS]string
|
||||
patch = make(Patch, 0)
|
||||
if err := binary.Read(r, binary.LittleEndian, &versionTag); err != nil {
|
||||
return nil, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
var ok bool
|
||||
if version, ok = _4klangVersionTags[versionTag]; !ok {
|
||||
return nil, fmt.Errorf("unknown 4klang version tag: %d", versionTag)
|
||||
}
|
||||
if err := binary.Read(r, binary.LittleEndian, &polyphonyUint32); err != nil {
|
||||
return nil, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
for i := range instrumentNames {
|
||||
instrumentNames[i], err = read4klangName(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read4klangName: %w", err)
|
||||
}
|
||||
}
|
||||
m := make(_4klangTargetMap)
|
||||
id := 1
|
||||
for instrIndex := 0; instrIndex < _4KLANG_MAX_INSTRS; instrIndex++ {
|
||||
var units []Unit
|
||||
if units, err = read4klangUnits(r, version, instrIndex, m, &id); err != nil {
|
||||
return nil, fmt.Errorf("read4klangUnits: %w", err)
|
||||
}
|
||||
if len(units) > 0 {
|
||||
patch = append(patch, Instrument{Name: instrumentNames[instrIndex], NumVoices: 1, Units: units})
|
||||
}
|
||||
}
|
||||
var units []Unit
|
||||
if units, err = read4klangUnits(r, version, _4KLANG_MAX_INSTRS, m, &id); err != nil {
|
||||
return nil, fmt.Errorf("read4klangUnits: %w", err)
|
||||
}
|
||||
if len(units) > 0 {
|
||||
patch = append(patch, Instrument{Name: "Global", NumVoices: 1, Units: units})
|
||||
}
|
||||
for i, instr := range patch {
|
||||
fix4klangTargets(i, instr, m)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Read4klangInstrument reads a 4klang instrument (a file usually with .4ki
|
||||
// extension) from r and returns an Instrument, making best attempt to convert
|
||||
// 4ki file to a sointu Instrument. It returns an error if the file is malformed
|
||||
// or if the 4ki file version is not supported.
|
||||
func Read4klangInstrument(r io.Reader) (instr Instrument, err error) {
|
||||
var versionTag uint32
|
||||
var version int
|
||||
var name string
|
||||
if err := binary.Read(r, binary.LittleEndian, &versionTag); err != nil {
|
||||
return Instrument{}, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
var ok bool
|
||||
if version, ok = _4klangVersionTags[versionTag]; !ok {
|
||||
return Instrument{}, fmt.Errorf("unknown 4klang version tag: %d", versionTag)
|
||||
}
|
||||
if name, err = read4klangName(r); err != nil {
|
||||
return Instrument{}, fmt.Errorf("read4klangName: %w", err)
|
||||
}
|
||||
var units []Unit
|
||||
id := 1
|
||||
m := make(_4klangTargetMap)
|
||||
if units, err = read4klangUnits(r, version, 0, m, &id); err != nil {
|
||||
return Instrument{}, fmt.Errorf("read4klangUnits: %w", err)
|
||||
}
|
||||
ret := Instrument{Name: name, NumVoices: 1, Units: units}
|
||||
fix4klangTargets(0, ret, m)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type (
|
||||
_4klangStackUnit struct {
|
||||
stack, unit int
|
||||
}
|
||||
|
||||
_4klangTargetMap map[_4klangStackUnit]int
|
||||
|
||||
_4klangPorts struct {
|
||||
UnitType string
|
||||
PortName [8]string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
_4KLANG_MAX_INSTRS = 16
|
||||
_4KLANG_MAX_UNITS = 64
|
||||
_4KLANG_MAX_SLOTS = 16
|
||||
_4KLANG_MAX_NAME_LEN = 64
|
||||
)
|
||||
|
||||
var (
|
||||
_4klangVersionTags map[uint32]int = map[uint32]int{
|
||||
0x31316b34: 11, // 4k11
|
||||
0x32316b34: 12, // 4k12
|
||||
0x33316b34: 13, // 4k13
|
||||
0x34316b34: 14, // 4k14
|
||||
}
|
||||
|
||||
_4klangDelays []int = []int{ // these are the numerators, if denominator is 48, fraction of beat time
|
||||
4, // 0 = 4.0f * (1.0f/32.0f) * (2.0f/3.0f)
|
||||
6, // 1 = 4.0f * (1.0f/32.0f),
|
||||
9, // 2 = 4.0f * (1.0f/32.0f) * (3.0f/2.0f),
|
||||
8, // 3 = 4.0f * (1.0f/16.0f) * (2.0f/3.0f),
|
||||
12, // 4 = 4.0f * (1.0f/16.0f),
|
||||
18, // 5 = 4.0f * (1.0f/16.0f) * (3.0f/2.0f),
|
||||
16, // 6 = 4.0f * (1.0f/8.0f) * (2.0f/3.0f),
|
||||
24, // 7 = 4.0f * (1.0f/8.0f),
|
||||
36, // 8 = 4.0f * (1.0f/8.0f) * (3.0f/2.0f),
|
||||
32, // 9 = 4.0f * (1.0f/4.0f) * (2.0f/3.0f),
|
||||
48, // 10 = 4.0f * (1.0f/4.0f),
|
||||
72, // 11 = 4.0f * (1.0f/4.0f) * (3.0f/2.0f),
|
||||
64, // 12 = 4.0f * (1.0f/2.0f) * (2.0f/3.0f),
|
||||
96, // 13 = 4.0f * (1.0f/2.0f),
|
||||
144, // 14 = 4.0f * (1.0f/2.0f) * (3.0f/2.0f),
|
||||
128, // 15 = 4.0f * (1.0f) * (2.0f/3.0f),
|
||||
192, // 16 = 4.0f * (1.0f),
|
||||
288, // 17 = 4.0f * (1.0f) * (3.0f/2.0f),
|
||||
256, // 18 = 4.0f * (2.0f) * (2.0f/3.0f),
|
||||
384, // 19 = 4.0f * (2.0f),
|
||||
576, // 20 = 4.0f * (2.0f) * (3.0f/2.0f),
|
||||
72, // 21 = 4.0f * (3.0f/8.0f),
|
||||
120, // 22 = 4.0f * (5.0f/8.0f),
|
||||
168, // 23 = 4.0f * (7.0f/8.0f),
|
||||
216, // 24 = 4.0f * (9.0f/8.0f),
|
||||
264, // 25 = 4.0f * (11.0f/8.0f),
|
||||
312, // 26 = 4.0f * (13.0f/8.0f),
|
||||
360, // 27 = 4.0f * (15.0f/8.0f),
|
||||
144, // 28 = 4.0f * (3.0f/4.0f),
|
||||
240, // 29 = 4.0f * (5.0f/4.0f),
|
||||
336, // 30 = 4.0f * (7.0f/4.0f),
|
||||
288, // 31 = 4.0f * (3.0f/2.0f),
|
||||
288, // 32 = 4.0f * (3.0f/2.0f),
|
||||
}
|
||||
|
||||
_4klangUnitPorts []_4klangPorts = []_4klangPorts{
|
||||
{"", [8]string{"", "", "", "", "", "", "", ""}},
|
||||
{"envelope", [8]string{"", "", "gain", "attack", "decay", "", "release", ""}},
|
||||
{"oscillator", [8]string{"", "transpose", "detune", "", "phase", "color", "shape", "gain"}},
|
||||
{"filter", [8]string{"", "", "", "", "frequency", "resonance", "", ""}},
|
||||
{"envelope", [8]string{"", "", "drive", "frequency", "", "", "", ""}},
|
||||
{"delay", [8]string{"pregain", "feedback", "dry", "damp", "", "", "", ""}},
|
||||
{"", [8]string{"", "", "", "", "", "", "", ""}},
|
||||
{"", [8]string{"", "", "", "", "", "", "", ""}},
|
||||
{"pan", [8]string{"panning", "", "", "", "", "", "", ""}},
|
||||
{"outaux", [8]string{"auxgain", "outgain", "", "", "", "", "", ""}},
|
||||
{"", [8]string{"", "", "", "", "", "", "", ""}},
|
||||
{"load", [8]string{"value", "", "", "", "", "", "", ""}},
|
||||
}
|
||||
)
|
||||
|
||||
func read4klangName(r io.Reader) (string, error) {
|
||||
var name [_4KLANG_MAX_NAME_LEN]byte
|
||||
if err := binary.Read(r, binary.LittleEndian, &name); err != nil {
|
||||
return "", fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
n := bytes.IndexByte(name[:], 0)
|
||||
if n == -1 {
|
||||
n = _4KLANG_MAX_NAME_LEN
|
||||
}
|
||||
return string(name[:n]), nil
|
||||
}
|
||||
|
||||
func read4klangUnits(r io.Reader, version, instrIndex int, m _4klangTargetMap, id *int) (units []Unit, err error) {
|
||||
numUnits := _4KLANG_MAX_UNITS
|
||||
if version <= 13 {
|
||||
numUnits = 32
|
||||
}
|
||||
units = make([]Unit, 0, numUnits)
|
||||
for unitIndex := 0; unitIndex < numUnits; unitIndex++ {
|
||||
var u []Unit
|
||||
if u, err = read4klangUnit(r, version); err != nil {
|
||||
return nil, fmt.Errorf("read4klangUnit: %w", err)
|
||||
}
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
m[_4klangStackUnit{instrIndex, unitIndex}] = *id
|
||||
for i := range u {
|
||||
u[i].ID = *id
|
||||
*id++
|
||||
}
|
||||
units = append(units, u...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func read4klangUnit(r io.Reader, version int) ([]Unit, error) {
|
||||
var unitType byte
|
||||
if err := binary.Read(r, binary.LittleEndian, &unitType); err != nil {
|
||||
return nil, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
var vals [15]byte
|
||||
if err := binary.Read(r, binary.LittleEndian, &vals); err != nil {
|
||||
return nil, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
if version <= 13 {
|
||||
// versions <= 13 had 16 unused slots for each unit
|
||||
if written, err := io.CopyN(io.Discard, r, 16); err != nil || written < 16 {
|
||||
return nil, fmt.Errorf("io.CopyN: %w", err)
|
||||
}
|
||||
}
|
||||
switch unitType {
|
||||
case 1:
|
||||
return read4klangENV(vals, version), nil
|
||||
case 2:
|
||||
return read4klangVCO(vals, version), nil
|
||||
case 3:
|
||||
return read4klangVCF(vals, version), nil
|
||||
case 4:
|
||||
return read4klangDST(vals, version), nil
|
||||
case 5:
|
||||
return read4klangDLL(vals, version), nil
|
||||
case 6:
|
||||
return read4klangFOP(vals, version), nil
|
||||
case 7:
|
||||
return read4klangFST(vals, version), nil
|
||||
case 8:
|
||||
return read4klangPAN(vals, version), nil
|
||||
case 9:
|
||||
return read4klangOUT(vals, version), nil
|
||||
case 10:
|
||||
return read4klangACC(vals, version), nil
|
||||
case 11:
|
||||
return read4klangFLD(vals, version), nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func read4klangENV(vals [15]byte, _ int) []Unit {
|
||||
return []Unit{{
|
||||
Type: "envelope",
|
||||
Parameters: map[string]int{
|
||||
"stereo": 0,
|
||||
"attack": int(vals[0]),
|
||||
"decay": int(vals[1]),
|
||||
"sustain": int(vals[2]),
|
||||
"release": int(vals[3]),
|
||||
"gain": int(vals[4]),
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangVCO(vals [15]byte, version int) []Unit {
|
||||
v := vals[:8]
|
||||
var transpose, detune, phase, color, gate, shape, gain, flags, stereo, typ, lfo int
|
||||
transpose, v = int(v[0]), v[1:]
|
||||
detune, v = int(v[0]), v[1:]
|
||||
phase, v = int(v[0]), v[1:]
|
||||
if version <= 11 {
|
||||
gate = 0x55
|
||||
} else {
|
||||
gate, v = int(v[0]), v[1:]
|
||||
}
|
||||
color, v = int(v[0]), v[1:]
|
||||
shape, v = int(v[0]), v[1:]
|
||||
gain, v = int(v[0]), v[1:]
|
||||
flags, _ = int(v[0]), v[1:]
|
||||
if flags&0x10 == 0x10 {
|
||||
lfo = 1
|
||||
}
|
||||
if flags&0x40 == 0x40 {
|
||||
stereo = 1
|
||||
}
|
||||
switch {
|
||||
case flags&0x01 == 0x01: // Sine
|
||||
typ = Sine
|
||||
if version <= 13 {
|
||||
color = 128
|
||||
}
|
||||
case flags&0x02 == 0x02: // Trisaw
|
||||
typ = Trisaw
|
||||
case flags&0x04 == 0x04: // Pulse
|
||||
typ = Pulse
|
||||
case flags&0x08 == 0x08: // Noise is handled differently in sointu
|
||||
return []Unit{{
|
||||
Type: "noise",
|
||||
Parameters: map[string]int{
|
||||
"stereo": stereo,
|
||||
"shape": shape,
|
||||
"gain": gain,
|
||||
},
|
||||
}}
|
||||
case flags&0x20 == 0x20: // Gate
|
||||
color = gate
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "oscillator",
|
||||
Parameters: map[string]int{
|
||||
"stereo": stereo,
|
||||
"transpose": transpose,
|
||||
"detune": detune,
|
||||
"phase": phase,
|
||||
"color": color,
|
||||
"shape": shape,
|
||||
"gain": gain,
|
||||
"type": typ,
|
||||
"lfo": lfo,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangVCF(vals [15]byte, _ int) []Unit {
|
||||
flags := vals[2]
|
||||
var stereo, lowpass, bandpass, highpass int
|
||||
if flags&0x01 == 0x01 {
|
||||
lowpass = 1
|
||||
}
|
||||
if flags&0x02 == 0x02 {
|
||||
highpass = 1
|
||||
}
|
||||
if flags&0x04 == 0x04 {
|
||||
bandpass = 1
|
||||
}
|
||||
if flags&0x08 == 0x08 {
|
||||
lowpass = 1
|
||||
highpass = -1
|
||||
}
|
||||
if flags&0x10 == 0x10 {
|
||||
stereo = 1
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "filter",
|
||||
Parameters: map[string]int{
|
||||
"stereo": stereo,
|
||||
"frequency": int(vals[0]),
|
||||
"resonance": int(vals[1]),
|
||||
"lowpass": lowpass,
|
||||
"bandpass": bandpass,
|
||||
"highpass": highpass,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func read4klangDST(vals [15]byte, _ int) []Unit {
|
||||
return []Unit{
|
||||
{Type: "distort", Parameters: map[string]int{"drive": int(vals[0]), "stereo": int(vals[2])}},
|
||||
{Type: "hold", Parameters: map[string]int{"holdfreq": int(vals[1]), "stereo": int(vals[2])}},
|
||||
}
|
||||
}
|
||||
|
||||
func read4klangDLL(vals [15]byte, _ int) []Unit {
|
||||
var delaytimes []int
|
||||
var notetracking int
|
||||
if vals[11] > 0 {
|
||||
if vals[10] > 0 { // left reverb
|
||||
delaytimes = []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618}
|
||||
} else { // right reverb
|
||||
delaytimes = []int{1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642}
|
||||
}
|
||||
} else {
|
||||
synctype := vals[9]
|
||||
switch synctype {
|
||||
case 0:
|
||||
delaytimes = []int{int(vals[8]) * 16}
|
||||
case 1: // relative to BPM
|
||||
notetracking = 2
|
||||
index := vals[8] >> 2
|
||||
delaytime := 48
|
||||
if int(index) < len(_4klangDelays) {
|
||||
delaytime = _4klangDelays[index]
|
||||
}
|
||||
delaytimes = []int{delaytime}
|
||||
case 2: // notetracking
|
||||
notetracking = 1
|
||||
delaytimes = []int{10787}
|
||||
}
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "delay",
|
||||
Parameters: map[string]int{
|
||||
"stereo": 0,
|
||||
"pregain": int(vals[0]),
|
||||
"dry": int(vals[1]),
|
||||
"feedback": int(vals[2]),
|
||||
"damp": int(vals[3]),
|
||||
"notetracking": notetracking,
|
||||
},
|
||||
VarArgs: delaytimes,
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangFOP(vals [15]byte, _ int) []Unit {
|
||||
var t string
|
||||
var stereo int
|
||||
switch vals[0] {
|
||||
case 1:
|
||||
t, stereo = "pop", 0
|
||||
case 2:
|
||||
t, stereo = "addp", 0
|
||||
case 3:
|
||||
t, stereo = "mulp", 0
|
||||
case 4:
|
||||
t, stereo = "push", 0
|
||||
case 5:
|
||||
t, stereo = "xch", 0
|
||||
case 6:
|
||||
t, stereo = "add", 0
|
||||
case 7:
|
||||
t, stereo = "mul", 0
|
||||
case 8:
|
||||
t, stereo = "addp", 1
|
||||
case 9:
|
||||
return []Unit{{Type: "loadnote", Parameters: map[string]int{"stereo": stereo}}, // 4klang loadnote gives 0..1, sointu gives -1..1
|
||||
{Type: "loadval", Parameters: map[string]int{"value": 128, "stereo": stereo}},
|
||||
{Type: "addp", Parameters: map[string]int{"stereo": stereo}},
|
||||
{Type: "gain", Parameters: map[string]int{"stereo": stereo, "gain": 64}}}
|
||||
default:
|
||||
t, stereo = "mulp", 1
|
||||
}
|
||||
return []Unit{{
|
||||
Type: t,
|
||||
Parameters: map[string]int{"stereo": stereo},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangFST(vals [15]byte, _ int) []Unit {
|
||||
sendpop := 0
|
||||
if vals[1]&0x40 == 0x40 {
|
||||
sendpop = 1
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "send",
|
||||
Parameters: map[string]int{
|
||||
"amount": int(vals[0]),
|
||||
"sendpop": sendpop,
|
||||
"dest_stack": int(vals[2]),
|
||||
"dest_unit": int(vals[3]),
|
||||
"dest_slot": int(vals[4]),
|
||||
"dest_id": int(vals[5]),
|
||||
}}}
|
||||
}
|
||||
|
||||
func fix4klangTargets(instrIndex int, instr Instrument, m _4klangTargetMap) {
|
||||
for _, u := range instr.Units {
|
||||
if u.Type == "send" {
|
||||
destStack := u.Parameters["dest_stack"]
|
||||
if destStack == 255 {
|
||||
destStack = instrIndex
|
||||
}
|
||||
fourKlangTarget := _4klangStackUnit{
|
||||
destStack,
|
||||
u.Parameters["dest_unit"]}
|
||||
u.Parameters["target"] = m[fourKlangTarget]
|
||||
if u.Parameters["dest_id"] < len(_4klangUnitPorts) && u.Parameters["dest_slot"] < 8 {
|
||||
if u.Parameters["dest_id"] == 4 && u.Parameters["dest_slot"] == 3 { // distortion is split into 2 units
|
||||
u.Parameters["target"]++
|
||||
u.Parameters["port"] = 0
|
||||
} else {
|
||||
modTarget := _4klangUnitPorts[u.Parameters["dest_id"]]
|
||||
for i, s := range Ports[modTarget.UnitType] {
|
||||
if s == modTarget.PortName[u.Parameters["dest_slot"]] {
|
||||
u.Parameters["port"] = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(u.Parameters, "dest_stack")
|
||||
delete(u.Parameters, "dest_unit")
|
||||
delete(u.Parameters, "dest_slot")
|
||||
delete(u.Parameters, "dest_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func read4klangPAN(vals [15]byte, _ int) []Unit {
|
||||
return []Unit{{
|
||||
Type: "pan",
|
||||
Parameters: map[string]int{
|
||||
"stereo": 0,
|
||||
"panning": int(vals[0]),
|
||||
}}}
|
||||
}
|
||||
|
||||
func read4klangOUT(vals [15]byte, _ int) []Unit {
|
||||
return []Unit{{
|
||||
Type: "outaux",
|
||||
Parameters: map[string]int{
|
||||
"stereo": 1,
|
||||
"outgain": int(vals[0]),
|
||||
"auxgain": int(vals[1])},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangACC(vals [15]byte, _ int) []Unit {
|
||||
c := 0
|
||||
if vals[0] != 0 {
|
||||
c = 2
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "in",
|
||||
Parameters: map[string]int{"stereo": 1, "channel": c},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangFLD(vals [15]byte, _ int) []Unit {
|
||||
return []Unit{{
|
||||
Type: "loadval",
|
||||
Parameters: map[string]int{"stereo": 0, "value": int(vals[0])},
|
||||
}}
|
||||
}
|
||||
454
CHANGELOG.md
454
CHANGELOG.md
@ -3,23 +3,441 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
## [0.6.0]
|
||||
### Added
|
||||
- An instrument (set of opcodes & accompanying values) can have any number of voices.
|
||||
- A track can trigger any number of voices (polyphonism).
|
||||
- Pattern length does not have to be a power of 2.
|
||||
- Macros for defining patches, so that only the necessary parts of the synth are compiled in.
|
||||
- Harmonized support for stereo signals: every opcode supports stereo variant.
|
||||
- New opcodes: bit-crusher, gain, inverse gain, clip, speed (bpm modulation), compressor.
|
||||
- Support for sample-based oscillators; samples loaded from gm.dls.
|
||||
- Unison oscillators: multiple copies of the oscillator running sligthly detuned and added up to together.
|
||||
- Support for 32 and 64 bit builds.
|
||||
- Regression tests for opcodes, using CTests.
|
||||
- Switch to CMake for builds.
|
||||
- Compiling as a static library & an API to call Sointu
|
||||
- Running all tests (win/linux/mac) in the cloud, using Github workflows
|
||||
- go: a Go package to call Sointu
|
||||
- go: Importing and exporting Sointu .asm songs
|
||||
- go: asmfmt, a command line utility to format/process Sointu .asm song files
|
||||
- Binary builds for sointu-play from GitHub Actions on all platforms.
|
||||
([#226][i226])
|
||||
- Song corpus with songs from real intros for testing size optimizations in
|
||||
Sointu systematically. ([#227][i227])
|
||||
- MIDI velocity, keyboard splitting, forcing specific instrument to use
|
||||
particular MIDI channel, and ability to transpose the incoming note values.
|
||||
These settings can be configured under instrument properties. ([#124][i124],
|
||||
[#215][i215], [#221][i221])
|
||||
- Ability to bind MIDI controllers to specific parameters. The MIDI menu has the
|
||||
options to bind/unbind parameters. When the user starts binding a parameter,
|
||||
Sointu waits for the next MIDI Control Change event and binds the currently
|
||||
selected parameter to that controller. ([#152][i152])
|
||||
- Plot the envelope shape on top of the oscilloscope when the envelope unit is
|
||||
selected.
|
||||
- Spectrum analyzer showing the spectrum. When the user has a filter or belleq
|
||||
unit selected, it's frequency response is plotted on top. ([#67][i67])
|
||||
- belleq unit: a bell-shaped second-order filter for equalization. Belleq unit
|
||||
takes the center frequency, bandwidth (inverse of Q-factor) and gain (+-40
|
||||
dB). Useful for boosting or reducing specific frequency ranges. Kudos to Reaby
|
||||
for the initial implementation!
|
||||
- Multithreaded synths: the user can split the patch up to four threads.
|
||||
Selecting the thread can be done on the instrument properties pane.
|
||||
Multithreading works only on the multithreaded synths, selectable from the CPU
|
||||
panel. Currently the multithreaded rendering has not yet been implemented in
|
||||
the compiled player and the thread information is disregarded while compiling
|
||||
the song. ([#199][i199])
|
||||
- Preset explorer, whichs allows 1) searching the presets by name; 2) filtering
|
||||
them by category (directory); 3) filtering them by being builtin vs. user;
|
||||
4) filtering them if they need gm.dls (for Linux/Mac users, who don't have
|
||||
it); and 5) saving and deleting user presets. ([#91][i91])
|
||||
- Panic the synth if it outputs NaN or Inf, and handle these more gracefully in
|
||||
the loudness and peak detector. ([#210][i210])
|
||||
- More presets from Reaby, and all new and existing presets were normalized
|
||||
roughly to -12 dBFS true peak. ([#211][i211])
|
||||
|
||||
[Unreleased]: https://github.com/vsariola/sointu/compare/4klang-3.11...HEAD
|
||||
### Fixed
|
||||
- VSTi queries the host sample rate more robustly. Cubase previously reported
|
||||
the sample rate as 0 Hz, leading to persistent error message about the sample
|
||||
rate not being 44100 Hz. ([#222][i222])
|
||||
- Occasional NaNs in the Trisaw oscillator when color was = 0 or color = 128
|
||||
- The tracker thought that "sync" unit pops the value from stack, even if the VM
|
||||
did not, resulting it claiming errors in patches that worked once compiled.
|
||||
|
||||
### Changed
|
||||
- Save only units and comment to instrument files, as we keep all the other
|
||||
fields while loading a new instrument / preset and the name comes from the
|
||||
filename.
|
||||
- Recovery files were moved to `os.UserConfigDir()/sointu/recovery/` instead of
|
||||
`os.UserConfigDir()/sointu/` so that they don't pollute the main configuration
|
||||
directory and so that it's easy to delete just the recovery files.
|
||||
- Tracker model supports now enum-style values, which are integers that have a
|
||||
name associated with them. These enums are used to display menus where you
|
||||
select one of the options, for example in the MIDI menu to choose one of the
|
||||
ports; a context menu in to choose which instrument triggers the oscilloscope;
|
||||
and a context menu to choose the weighting type in the loudness detector.
|
||||
- The song panel can scroll if all the widgets don't fit into it
|
||||
- The provided MacOS executables are now arm64, which means the x86 native
|
||||
synths are not compiled in.
|
||||
|
||||
## [0.5.0]
|
||||
### BREAKING CHANGES
|
||||
- BREAKING CHANGE: always first modulate delay time, then apply notetracking. In
|
||||
a delay unit, modulation adds to the delay time, while note tracking
|
||||
multiplies it with a multiplier dependent on the note. The order of these
|
||||
operations was different in the Go VM vs. x86 VM & WebAssembly VM. In the Go
|
||||
VM, it first modulated, and then applied the note tracking multiplication. In
|
||||
the two assembly VMs, it first applied the note tracking and then modulated.
|
||||
Of these two behaviours, the Go VM behaviour made more sense: if you make a
|
||||
vibrato of +-50 cents for C4, you probably want a vibrato of +-50 cents for C6
|
||||
also. Thus, first modulating and then applying the note tracking
|
||||
multiplication is now the behaviour accross all VMs.
|
||||
- BREAKING CHANGE: the negbandpass and neghighpass parameters of the filter unit
|
||||
were removed. Setting bandpass or highpass to -1 achieves now the same end
|
||||
result. Setting both negbandpass and bandpass to 1 was previously a no-op. Old
|
||||
patch and instrument files are converted to the new format when loaded, but
|
||||
newer Sointu files should not be compiled with an old version of
|
||||
sointu-compile.
|
||||
|
||||
### Added
|
||||
- Signal rail that visualizes what happens in the stack, shown on the left side
|
||||
of each unit in the rack.
|
||||
- The parameters are now displayed in a grid as knobs, with units of the
|
||||
instrument going from the top to the bottom. Bezier lines are used to indicate
|
||||
which sends modulate which ports. ([#173][i173])
|
||||
- Tabbing works more consistently, with widgets placed in a "tree", and plain
|
||||
Tab moves to the next widget on the same level or more shallow in the tree,
|
||||
while ctrl-Tab moves to next widget, regardless of its depth. This allows the
|
||||
user to quickly move between different panels, but also tabbing into every
|
||||
tiny widget if needed. Shift-* tab backwards.
|
||||
- Help menu, with a menu item to show the license in a dialog, and also menu
|
||||
items to open manual, Github Discussions & Github Issues in a browser
|
||||
([#196][i196])
|
||||
- Show CPU load percentage in the song panel ([#192][i192])
|
||||
- Theme can be user configured, in theme.yml. This theme.yml should be placed in
|
||||
the usual sointu config directory (i.e.
|
||||
`os.UserConfigDir()/sointu/theme.yml`). See
|
||||
[theme.yml](tracker/gioui/theme.yml) for the default theme, and
|
||||
[theme.go](tracker/gioui/theme.go) for what can be changed.
|
||||
- Ctrl + scroll wheel adjusts the global scaling of the GUI ([#153][i153])
|
||||
- The loudness detection supports LUFS, A-weighting, C-weighting or
|
||||
RMS-weighting, and peak detection supports true peak or sample peak detection.
|
||||
The loudness and peak values are displayed in the song panel ([#186][i186])
|
||||
- Oscilloscope to visualize the outputted waveform ([#61][i61])
|
||||
- Toggle button to keep instruments and tracks linked, and buttons to split
|
||||
instruments and tracks with more than 1 voice into parallel ones
|
||||
([#163][i163], [#157][i157])
|
||||
- Mute and solo toggles for instruments ([#168][i168])
|
||||
- Many units (e.g. envelopes, oscillators and compressors) display values dB
|
||||
- Dragging mouse to select rectangles in the tables
|
||||
- The standalone tracker can open a MIDI port for receiving MIDI notes
|
||||
([#166][i166])
|
||||
- The note editor has a button to allow entering notes by MIDI. ([#170][i170])
|
||||
- Units can have comments, to make it easier to distinguish between units of
|
||||
same type within an instrument and to use these as subsection titles.
|
||||
([#114][i114])
|
||||
- A toggle button for copying non-unique patterns before editing. When enabled
|
||||
and if the pattern is used in multiple places, the pattern is copied first.
|
||||
([#77][i77])
|
||||
- User can define own keybindings in `os.UserConfigDir()/sointu/keybindings.yml`
|
||||
([#94][i94], [#151][i151])
|
||||
- User can define preferred window size in
|
||||
`os.UserConfigDir()/sointu/preferences.yml` ([#184][i184])
|
||||
- A small number above the instrument name identifies the MIDI channel /
|
||||
instrument number, with numbering starting from 1 ([#154][i154])
|
||||
- The filter unit frequency parameter is displayed in Hz, corresponding roughly
|
||||
to the resonant frequency of the filter ([#158][i158])
|
||||
- Include version info in the binaries, as given be `git describe`. This version
|
||||
info is shown as a label in the tracker and can be checked with `-v` flag in
|
||||
the command line tools.
|
||||
- Performance improvement: values needed by the UI that are derived from the
|
||||
score or patch are cached when score or patch changes, so they don't have to
|
||||
be computed every draw. ([#176][i176])
|
||||
|
||||
### Fixed
|
||||
- Tooltips will be hidden after certain amount of time has passed, to ensure
|
||||
that the tooltips don't stay around ([#141][i141])
|
||||
- Loading instrument forgot to close the file (in model.ReadInstrument)
|
||||
- We try to honor the MIDI event time stamps, so that the timing between MIDI
|
||||
events (as reported to us by RTMIDI) will be correct.
|
||||
- When unmarshaling the recovery file, the unit parameter maps were "merged"
|
||||
with the existing parameter maps, instead of overwriting. This created units
|
||||
with unnecessary parameters, which was harmless, but would cause a warning to
|
||||
the user.
|
||||
- When changing a nibble of a hexadecimal note, the note played was the note
|
||||
before changing the nibble
|
||||
- Clicking on low nibble or high nibble of a hex track selects that nibble
|
||||
([#160][i160])
|
||||
- If units have useless parameters in their parameter maps, from bugs or from a
|
||||
malformed yaml file, they are removed and user is warned about it
|
||||
- Pressing `a` or `1` when editing note values in hex mode created a note off
|
||||
line ([#162][i162])
|
||||
- Warn about plugin sample rate being different from 44100 only after
|
||||
ProcessFloatFunc has been called, so that host has time to set the sample rate
|
||||
after initialization.
|
||||
- Crashes with sample-based oscillators in the 32-bit library, as the pointer to
|
||||
sample-table (edi) got accidentally overwritten by detune
|
||||
- Sample-based oscillators could hard crash if a x87 stack overflow happened
|
||||
when calculating the current position in the sample ([#149][i149])
|
||||
- Numeric updown widget calculated dp-to-px conversion incorrectly, resulting in
|
||||
wrong scaling ([#150][i150])
|
||||
- Empty patch should not crash the native synth ([#148][i148])
|
||||
- sointu-play allows choosing between the synths, assuming it was compiled with
|
||||
`-tags=native`
|
||||
- Most buttons never gain focus, so that clicking a button does not stop
|
||||
whatever the user was currently doing and so that the user does not
|
||||
accidentally trigger the buttons by having them focused and e.g. hitting space
|
||||
([#156][i156])
|
||||
|
||||
### Changed
|
||||
- When saving instrument to a file, the instrument name is not saved to the name
|
||||
field, as Sointu will anyway use the filename as the instrument's name when it
|
||||
is loaded.
|
||||
- Native version of the tracker/VSTi was removed. Instead, you can change
|
||||
between the two versions of the synth on the fly, by clicking on the "Synth"
|
||||
option under the CPU group in the song panel ([#200][i200])
|
||||
- Send amount defaults to 64 = 0.0 ([#178][i178])
|
||||
- The maximum number of delaylines in the native synth was increased to 128,
|
||||
with slight increase in memory usage ([#155][i155])
|
||||
- The numeric updown widget has a new appearance.
|
||||
- The draggable UI splitters snap more controllably to the window edges.
|
||||
- New & better presets, organized by their type to subfolders (thanks Reaby!)
|
||||
([#136][i136])
|
||||
- Presets get their name by concatenating their subdirectory path (with path
|
||||
separators replaced with spaces) to their filename
|
||||
- The keyboard shortcuts are now again closer to what they were old trackers
|
||||
([#151][i151])
|
||||
- The stand-alone apps now output floating point sound, as made possible by
|
||||
upgrading oto-library to latest version. This way the tracker sound output
|
||||
matches the compiled output better, as usually compiled intros output sound in
|
||||
floating point. This might be important if OS sound drivers apply some audio
|
||||
enhancemenets e.g. compressors to the audio.
|
||||
|
||||
## [0.4.1]
|
||||
### Added
|
||||
- Clicking the parameter slider also selects that parameter ([#112][i112])
|
||||
- The vertical and horizontal split bars indicate with a cursor that they can be
|
||||
resized ([#145][i145])
|
||||
|
||||
### Fixed
|
||||
- When adding a unit on the last row of the unit list, the editor for entering
|
||||
the type of the unit by text did gain focus.
|
||||
- When inputting a note to the note editor, advance the cursor by step
|
||||
([#144][i144])
|
||||
- When loading an instrument, make sure the total number of voices does not go
|
||||
over the maximum number allowed by vm, and make sure a loaded instrument has
|
||||
at least 1 voice
|
||||
- Potential ID collisions when clearing unit or pasteing instruments
|
||||
- Assign new IDs to loaded instruments, and fix ID collisions in case they
|
||||
somehow still appear ([#146][i146])
|
||||
- In x86 templates, do not optimize away phase modulations when unisons are used
|
||||
even if all phase inputs are zeros, as unisons use the phase modulation
|
||||
mechanism to offset the different oscillators
|
||||
- Do not include delay times in the delay time table if the delay unit is
|
||||
disabled ([#139][i139])
|
||||
- Moved the error and warning popups slightly up so they don't block the unit
|
||||
control buttons ([#142][i142])
|
||||
|
||||
### Changed
|
||||
- Do not automatically wrap around the song when playing as it was usually
|
||||
unwanted behaviour. There is already the looping mechanism if the user really
|
||||
wants to loop the song forever.
|
||||
|
||||
## [0.4.0]
|
||||
### Added
|
||||
- User can drop preset instruments into `os.UserConfigDir()/sointu/presets/` and
|
||||
they appear in the list of presets next time sointu is started.
|
||||
([#125][i125])
|
||||
- Ability to loop certain section of the song when playing. The loop can be set
|
||||
by using the toggle button in the song panel, or by hitting Ctrl+L.
|
||||
([#128][i128])
|
||||
- Disable units temporarily. The disabled units are shown in gray and are not
|
||||
compiled into the patch and are considered for all purposes non-existent.
|
||||
Hitting Ctrl-D disables/re-enables the selected unit(s). The yaml file has
|
||||
field `disabled: true` for the unit. ([#116][i116])
|
||||
- Passing a file name on command line immediately tries loading that file ([#122][i122])
|
||||
- Massive rewrite of the GUI, in particular allowing better copying, pasting and
|
||||
scrolling of table-based data (order list and note data).
|
||||
- Dbgain unit, which allows defining the gain in decibels (-40 dB to +40dB)
|
||||
- `+` and `-` keys add/subtract values in order editor and pattern editor
|
||||
([#65][i65])
|
||||
- The function `su_power` is exported so people can reuse it in the main code;
|
||||
however, as it assumes the parameter passed in st0 on the x87 stack and
|
||||
similarly returns it value in st0 on the x87 stack, to my knowledge there is
|
||||
no calling convention that would correspond this behaviour, so you need to
|
||||
define a header for it yourself and take care of putting the float value on
|
||||
x87 stack.
|
||||
|
||||
### Fixed
|
||||
- Loading a preset did not update the IDs of the newly loaded instrument,
|
||||
causing ID collisions and sends target wrong units.
|
||||
- The x87 native filter unit was denormalizing and eating up a lot of CPU ([#68][i68])
|
||||
- Modulating delaytime in wasm could crash, because delay time was converted to
|
||||
int with i32.trunc_f32_u. Using i32.trunc_f32_s fixed this.
|
||||
- When recording notes from VSTI, no track was created for instruments that had
|
||||
no notes triggered, resulting in misalignment of the tracks from instruments.
|
||||
- 32-bit su_load_gmdls clobbered ebx, even though __stdcall demands it to be not
|
||||
touched ([#130][i130])
|
||||
- Spaces are allowed in instrument names ([#120][i120])
|
||||
- Fixed the dropdown for targeting sends making it impossible to choose certain
|
||||
ops. This was done just by reducing the default height of popup menus so they
|
||||
fit on screen ([#121][i121])
|
||||
- Warn user about sample rate being other than 44100 Hz, as this lead to weird
|
||||
behaviour. Sointu assumes the samplerate always to be 44100 Hz. ([#129][i129])
|
||||
|
||||
### Changed
|
||||
- The scroll wheel behavior for unit integer parameters was flipped: scrolling
|
||||
up now increases the value, while scrolling down decreases the value. It was
|
||||
vice versa. ([#112][i112])
|
||||
|
||||
## [0.3.0]
|
||||
### Added
|
||||
- Scroll bars to menus, shown when a menu is too long to fit.
|
||||
- Save the GUI state periodically to a recovery file and load it on
|
||||
startup of the app, if present. The recovery files are located in the
|
||||
app config directory (e.g. AppData/Roaming/Sointu on Windows).
|
||||
- Save the VSTI GUI state to the DAW project file, through GetChunk /
|
||||
SetChunk mechanisms.
|
||||
- Instrument presets. The presets are embedded in the executable and
|
||||
there's a button to open a menu to load one of the presets.
|
||||
- Frequency modulation target for oscillator, as it was in 4klang
|
||||
- Reverb preset settings for a delay unit, with stereo, left and right
|
||||
options
|
||||
|
||||
### Fixed
|
||||
- Crash when running more than one sointu VSTI plugins in parallel
|
||||
- The scroll bars move in sync with the cursor.
|
||||
- The stereo version of delay in the go virtual machine (executables / plugins
|
||||
not ending with -native) applied the left delay taps on the right channel, and
|
||||
the right delay taps on the left channel.
|
||||
- The sointu-vsti-native plugin has different plugin ID and plugin name
|
||||
to not confuse it with the non-native one
|
||||
- The VSTI waits for the gioui actually have quit when closing the
|
||||
plugin
|
||||
|
||||
### Changed
|
||||
- BREAKING CHANGE: The meaning of default modulation mode ("auto") has
|
||||
been changed for cross-instrument modulations: it now means "all"
|
||||
voices, instead of first voice (which was redundant, as it was same as
|
||||
defining voice = 0). This means that for cross-instrument modulations,
|
||||
one "all vocies" send gets actually compiled into multiple sends, one
|
||||
for each targeted voice. For intra-instrument modulations, the meaning
|
||||
stays the same, but the label was changed to "self", to highlight that
|
||||
this means the voice modulates only itself and not other voices.
|
||||
|
||||
## [0.2.0]
|
||||
### Added
|
||||
- Saving and loading instruments
|
||||
- Comment field to instruments
|
||||
- Ability to reorder tracks
|
||||
- Add menu command to delete all unused data from song file
|
||||
- Ability to search a unit by typing its name
|
||||
- Ability to run sointu as a vsti plugin, inside vsti host
|
||||
- Ability to lock delay relative to beat duration
|
||||
- Ability to import 4klang patches (.4kp) and instruments (.4ki)
|
||||
- The repository has example instruments, including all patches and
|
||||
instruments from 4klang
|
||||
- The compiler templates are embedded in the sointu-compile, so no
|
||||
installation is needed beyond copying sointu-compile to PATH
|
||||
- Ability to select multiple units and cut, copy & paste them
|
||||
- Mousewheel adjusts unit parameters
|
||||
- Tooltips to many buttons
|
||||
- Support for gm.dls samples in the go-written virtual machine
|
||||
- x86 and C written examples how to play a sointu song on various
|
||||
platforms. On Windows, the examples can optionally be linked with
|
||||
Crinkler to get Crinkler reports.
|
||||
|
||||
### Fixed
|
||||
- Unnamed instruments with multiple voices caused crashes
|
||||
- In the native version, exceeding the 64 delaylines caused crashes
|
||||
- wat2wasm nowadays uses funcref instead of anyfunc
|
||||
- In the WebAssembly core, $WRK was messed after stereo oscillators,
|
||||
making modulations not work
|
||||
- The Webassembly implementation of mono version of the "out" unit
|
||||
|
||||
### Changed
|
||||
- The release flag in the voice is now a sustain flag i.e. the logic has
|
||||
been inverted. This was done so that when the synth is initialized
|
||||
with zeros, all voices start with sustain = 0 i.e. in released state.
|
||||
- The crush resolution is now in bits instead of linear range; this is a
|
||||
breaking change and changes the meaning of the resolution values. But
|
||||
now there are more usable values in the resolution.
|
||||
|
||||
## [0.1.0]
|
||||
### Added
|
||||
- An instrument (set of opcodes & accompanying values) can have any
|
||||
number of voices.
|
||||
- A track can trigger any number of voices, releasing the previous when
|
||||
new one is triggered.
|
||||
- Pattern length does not have to be a power of 2.
|
||||
- Only the necessary opcodes and functions of the synth are compiled in the final executable.
|
||||
- Harmonized support for stereo signals: every opcode supports stereo
|
||||
variant.
|
||||
- New opcodes: crush, gain, inverse gain, clip, speed (bpm modulation),
|
||||
compressor.
|
||||
- Support for sample-based oscillators (samples loaded from gm.dls).
|
||||
- Unison oscillators: multiple copies of the oscillator running with
|
||||
different detuning and added up to together.
|
||||
- Support for 32 and 64 bit builds.
|
||||
- Support different platforms: Windows, Linux and Mac (Intel).
|
||||
- Experimental support for compiling songs into WebAssembly.
|
||||
- Switch to CMake for builds.
|
||||
- Regression tests for every VM instruction, using CTests.
|
||||
- Compiling as a static library & an API to call Sointu
|
||||
- Running all tests (win/linux/mac/wasm) in the cloud, using Github
|
||||
workflows
|
||||
- Tools written in Go-lang:
|
||||
- a tracker for composing songs as .yml
|
||||
- a command line utility to convert .yml songs to .asm
|
||||
- a command line utility to play the songs on command line
|
||||
|
||||
[Unreleased]: https://github.com/vsariola/sointu/compare/v0.6.0...HEAD
|
||||
[0.6.0]: https://github.com/vsariola/sointu/compare/v0.5.0...v0.6.0
|
||||
[0.5.0]: https://github.com/vsariola/sointu/compare/v0.4.1...v0.5.0
|
||||
[0.4.1]: https://github.com/vsariola/sointu/compare/v0.4.0...v0.4.1
|
||||
[0.4.0]: https://github.com/vsariola/sointu/compare/v0.3.0...v0.4.0
|
||||
[0.3.0]: https://github.com/vsariola/sointu/compare/v0.2.0...v0.3.0
|
||||
[0.2.0]: https://github.com/vsariola/sointu/compare/v0.1.0...v0.2.0
|
||||
[0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
|
||||
[i61]: https://github.com/vsariola/sointu/issues/61
|
||||
[i65]: https://github.com/vsariola/sointu/issues/65
|
||||
[i67]: https://github.com/vsariola/sointu/issues/67
|
||||
[i68]: https://github.com/vsariola/sointu/issues/68
|
||||
[i77]: https://github.com/vsariola/sointu/issues/77
|
||||
[i91]: https://github.com/vsariola/sointu/issues/91
|
||||
[i94]: https://github.com/vsariola/sointu/issues/94
|
||||
[i112]: https://github.com/vsariola/sointu/issues/112
|
||||
[i114]: https://github.com/vsariola/sointu/issues/114
|
||||
[i116]: https://github.com/vsariola/sointu/issues/116
|
||||
[i120]: https://github.com/vsariola/sointu/issues/120
|
||||
[i121]: https://github.com/vsariola/sointu/issues/121
|
||||
[i122]: https://github.com/vsariola/sointu/issues/122
|
||||
[i124]: https://github.com/vsariola/sointu/issues/124
|
||||
[i125]: https://github.com/vsariola/sointu/issues/125
|
||||
[i128]: https://github.com/vsariola/sointu/issues/128
|
||||
[i129]: https://github.com/vsariola/sointu/issues/129
|
||||
[i130]: https://github.com/vsariola/sointu/issues/130
|
||||
[i136]: https://github.com/vsariola/sointu/issues/136
|
||||
[i139]: https://github.com/vsariola/sointu/issues/139
|
||||
[i141]: https://github.com/vsariola/sointu/issues/141
|
||||
[i142]: https://github.com/vsariola/sointu/issues/142
|
||||
[i144]: https://github.com/vsariola/sointu/issues/144
|
||||
[i145]: https://github.com/vsariola/sointu/issues/145
|
||||
[i146]: https://github.com/vsariola/sointu/issues/146
|
||||
[i148]: https://github.com/vsariola/sointu/issues/148
|
||||
[i149]: https://github.com/vsariola/sointu/issues/149
|
||||
[i150]: https://github.com/vsariola/sointu/issues/150
|
||||
[i151]: https://github.com/vsariola/sointu/issues/151
|
||||
[i152]: https://github.com/vsariola/sointu/issues/152
|
||||
[i153]: https://github.com/vsariola/sointu/issues/153
|
||||
[i154]: https://github.com/vsariola/sointu/issues/154
|
||||
[i155]: https://github.com/vsariola/sointu/issues/155
|
||||
[i156]: https://github.com/vsariola/sointu/issues/156
|
||||
[i157]: https://github.com/vsariola/sointu/issues/157
|
||||
[i158]: https://github.com/vsariola/sointu/issues/158
|
||||
[i160]: https://github.com/vsariola/sointu/issues/160
|
||||
[i162]: https://github.com/vsariola/sointu/issues/162
|
||||
[i163]: https://github.com/vsariola/sointu/issues/163
|
||||
[i166]: https://github.com/vsariola/sointu/issues/166
|
||||
[i168]: https://github.com/vsariola/sointu/issues/168
|
||||
[i170]: https://github.com/vsariola/sointu/issues/170
|
||||
[i173]: https://github.com/vsariola/sointu/issues/173
|
||||
[i176]: https://github.com/vsariola/sointu/issues/176
|
||||
[i178]: https://github.com/vsariola/sointu/issues/178
|
||||
[i184]: https://github.com/vsariola/sointu/issues/184
|
||||
[i186]: https://github.com/vsariola/sointu/issues/186
|
||||
[i192]: https://github.com/vsariola/sointu/issues/192
|
||||
[i196]: https://github.com/vsariola/sointu/issues/196
|
||||
[i199]: https://github.com/vsariola/sointu/issues/199
|
||||
[i200]: https://github.com/vsariola/sointu/issues/200
|
||||
[i210]: https://github.com/vsariola/sointu/issues/210
|
||||
[i211]: https://github.com/vsariola/sointu/issues/211
|
||||
[i215]: https://github.com/vsariola/sointu/issues/215
|
||||
[i221]: https://github.com/vsariola/sointu/issues/221
|
||||
[i222]: https://github.com/vsariola/sointu/issues/222
|
||||
[i226]: https://github.com/vsariola/sointu/issues/226
|
||||
[i227]: https://github.com/vsariola/sointu/issues/227
|
||||
|
||||
@ -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()
|
||||
|
||||
2
LICENSE
2
LICENSE
@ -1,7 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Dominik Ries
|
||||
(c) 2020 Veikko Sariola
|
||||
(c) 2020-2025 Veikko Sariola, moitias, qm210, LeStahl, petersalomonsen, anticore, reaby
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
541
README.md
541
README.md
@ -1,13 +1,45 @@
|
||||
# Sointu
|
||||

|
||||

|
||||
|
||||
A cross-architecture and cross-platform modular software synthesizer for small
|
||||
intros, forked from [4klang](https://github.com/hzdgopher/4klang). Targetable
|
||||
architectures include 386, amd64, and WebAssembly; targetable platforms include
|
||||
Windows, Mac, Linux (and related) + browser.
|
||||
|
||||
Pull requests / suggestions / issues welcome, through Github! You can also
|
||||
contact me through email (firstname.lastname@gmail.com).
|
||||
- [User manual](https://github.com/vsariola/sointu/wiki) is in the Wiki
|
||||
- [Discussions](https://github.com/vsariola/sointu/discussions) is for asking
|
||||
help, sharing patches/instruments and brainstorming ideas
|
||||
- [Issues](https://github.com/vsariola/sointu/issues) is for reporting bugs
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
You can either:
|
||||
|
||||
1) Download the latest build from the master branch from the
|
||||
[actions](https://github.com/vsariola/sointu/actions). Find workflow
|
||||
"Binaries" and scroll down for .zip files containing the artifacts.
|
||||
**Note:** You have to be logged into Github to download artifacts!
|
||||
|
||||
or
|
||||
|
||||
2) Download one of the tagged
|
||||
[releases](https://github.com/vsariola/sointu/releases).
|
||||
|
||||
In both cases, you can then just run one of the executables (no need to install
|
||||
anything); or in the case of the VST plugins library files, copy them wherever
|
||||
you keep you VST2 plugins.
|
||||
|
||||
The pre 1.0 version release tags are mostly for reference: no backwards
|
||||
compatibility will be guaranteed while upgrading to a newer version. Backwards
|
||||
compatibility will be attempted from 1.0 onwards.
|
||||
|
||||
**Uninstallation**: Sointu stores configuration and recovery data files in
|
||||
OS-specific folders e.g. `AppData/Roaming/Sointu` on Windows. For clean
|
||||
uninstall, delete also this folder. See
|
||||
[here](https://pkg.go.dev/os#UserConfigDir) where to find those folders on other
|
||||
platforms.
|
||||
|
||||
Summary
|
||||
-------
|
||||
@ -24,20 +56,17 @@ synthesis engine can already be fitted in 600 bytes (386, compressed), with
|
||||
another few hundred bytes for the patch and pattern data.
|
||||
|
||||
Sointu consists of two core elements:
|
||||
- A cross-platform synth-tracker app for composing music, written in
|
||||
[go](https://golang.org/). The app is still heavily work in progress. The app
|
||||
exports the projects as .yml files. There are two versions of the app:
|
||||
[cmd/sointu-track/](sointu-track), using a plain Go VM bytecode interpreter,
|
||||
and [cmd/sointu-nativetrack/](sointu-nativetrack), using cgo to bridge calls
|
||||
to the Sointu compiled VM. The former should be highly portable, the latter
|
||||
currently works only on x86/amd64 platforms.
|
||||
- A cross-platform synth-tracker that runs as either VSTi or stand-alone
|
||||
app for composing music, written in [go](https://golang.org/). The app
|
||||
is still heavily work in progress. The app exports the projects as
|
||||
.yml files.
|
||||
- A compiler, likewise written in go, which can be invoked from the command line
|
||||
to compile these .yml files into .asm or .wat code. For x86/amd64, the
|
||||
resulting .asm can be then compiled by [nasm](https://www.nasm.us/) or
|
||||
[yasm](https://yasm.tortall.net). For browsers, the resulting .wat can be
|
||||
compiled by [wat2wasm](https://github.com/WebAssembly/wabt).
|
||||
resulting .asm can be then compiled by [nasm](https://www.nasm.us/). For
|
||||
browsers, the resulting .wat can be compiled by
|
||||
[wat2wasm](https://github.com/WebAssembly/wabt).
|
||||
|
||||
This is how the current prototype tracker looks like:
|
||||
This is how the current prototype app looks like:
|
||||
|
||||

|
||||
|
||||
@ -49,24 +78,196 @@ listed below.
|
||||
|
||||
### Sointu-track
|
||||
|
||||
This version of the tracker is the version that uses the bytecode interpreter
|
||||
written in plain Go. Running the tracker:
|
||||
This is the stand-alone version of the synth-tracker. Sointu-track uses
|
||||
the [gioui](https://gioui.org/) for the GUI and [oto](https://github.com/hajimehoshi/oto)
|
||||
for the audio, so the portability is currently limited by these.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- [go](https://golang.org/)
|
||||
- If you want to also use the x86 assembly written synthesizer, to test that the
|
||||
patch also works once compiled:
|
||||
- Follow the instructions to build the [x86 native virtual machine](#native-virtual-machine)
|
||||
before building the tracker.
|
||||
- cgo compatible compiler e.g. [gcc](https://gcc.gnu.org/). On windows, you
|
||||
best bet is [MinGW](http://www.mingw.org/). The compiler can be in PATH or
|
||||
you can use the environment variable `CC` to help go find the compiler.
|
||||
- Setting environment variable `CGO_ENABLED=1` is a good idea,
|
||||
because if it is not set and go fails to find the compiler, go just
|
||||
excludes all files with `import "C"` from the build, resulting in
|
||||
lots of errors about missing types.
|
||||
|
||||
#### Running
|
||||
|
||||
```
|
||||
go run cmd/sointu-track/main.go
|
||||
```
|
||||
|
||||
Building the tracker:
|
||||
#### Building an executable
|
||||
|
||||
```
|
||||
go build -o sointu-track cmd/sointu-track/main.go
|
||||
go build -o sointu-track.exe cmd/sointu-track/main.go
|
||||
```
|
||||
|
||||
On windows, replace `-o sointu-track` with `-o sointu-track.exe`.
|
||||
On other platforms than Windows, replace `-o sointu-track.exe` with
|
||||
`-o sointu-track`.
|
||||
|
||||
Sointu-track uses the [gioui](https://gioui.org/) for the GUI and
|
||||
[oto](https://github.com/hajimehoshi/oto) for the audio, so the portability is
|
||||
currently limited by these.
|
||||
If you want to include the [x86 native virtual machine](#native-virtual-machine),
|
||||
add `-tags=native` to all the commands e.g.
|
||||
|
||||
```
|
||||
go build -o sointu-track.exe -tags=native cmd/sointu-track/main.go
|
||||
```
|
||||
|
||||
### Sointu-vsti
|
||||
|
||||
This is the VST instrument plugin version of the tracker, compiled into
|
||||
a dynamically linked library and ran inside a VST host.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- [go](https://golang.org/)
|
||||
- cgo compatible compiler e.g. [gcc](https://gcc.gnu.org/). On windows, you best
|
||||
bet is [MinGW](http://www.mingw.org/). The compiler can be in PATH or you can
|
||||
use the environment variable `CC` to help go find the compiler.
|
||||
- Setting environment variable `CGO_ENABLED=1` is a good idea, because
|
||||
if it is not set and go fails to find the compiler, go just excludes
|
||||
all files with `import "C"` from the build, resulting in lots of
|
||||
errors about missing types.
|
||||
- If you want to build the VSTI with the native x86 assembly written synthesizer:
|
||||
- Follow the instructions to build the [x86 native virtual machine](#native-virtual-machine)
|
||||
before building the plugin itself
|
||||
|
||||
#### Building
|
||||
|
||||
```
|
||||
go build -buildmode=c-shared -tags=plugin -o sointu-vsti.dll .\cmd\sointu-vsti\
|
||||
```
|
||||
|
||||
On other platforms than Windows, replace `-o sointu-vsti.dll` appropriately e.g.
|
||||
`-o sointu-vsti.so`; so far, the VST instrument has been built & tested on
|
||||
Windows and Linux.
|
||||
|
||||
Notice the `-tags=plugin` build tag definition. This is required by the [vst2
|
||||
library](https://github.com/pipelined/vst2); otherwise, you will get a lot of
|
||||
build errors.
|
||||
|
||||
Add `-tags=native,plugin` to use the [x86 native virtual
|
||||
machine](#native-virtual-machine) instead of the virtual machine written in Go.
|
||||
|
||||
### Sointu-compile
|
||||
|
||||
The command line interface to it is [sointu-compile](cmd/sointu-compile/main.go)
|
||||
and the actual code resides in the [compiler](vm/compiler/) package, which is an
|
||||
ordinary [go](https://golang.org/) package with no other tool dependencies.
|
||||
|
||||
#### Running
|
||||
|
||||
```
|
||||
go run cmd/sointu-compile/main.go
|
||||
```
|
||||
|
||||
#### Building an executable
|
||||
|
||||
```
|
||||
go build -o sointu-compile.exe cmd/sointu-compile/main.go
|
||||
```
|
||||
|
||||
On other platforms than Windows, replace `-o sointu-compile.exe` with
|
||||
`-o sointu-compile`.
|
||||
|
||||
#### Usage
|
||||
|
||||
The compiler can then be used to compile a .yml song into .asm and .h files. For
|
||||
example:
|
||||
|
||||
```
|
||||
sointu-compile -arch=386 tests/test_chords.yml
|
||||
nasm -f win32 test_chords.asm
|
||||
```
|
||||
|
||||
WebAssembly example:
|
||||
|
||||
```
|
||||
sointu-compile -arch=wasm tests/test_chords.yml
|
||||
wat2wasm test_chords.wat
|
||||
```
|
||||
|
||||
If you are looking for an easy way to compile an executable from a Sointu song
|
||||
(e.g. for a executable music compo), take a look at [NR4's Python-based
|
||||
tool](https://github.com/LeStahL/sointu-executable-msx) for it.
|
||||
|
||||
#### Examples
|
||||
|
||||
The folder `examples/code` contains usage examples for Sointu with winmm and
|
||||
dsound playback under Windows and asound playback under Unix. Source code is
|
||||
available in C and x86 assembly (win32, elf32 and elf64 versions).
|
||||
|
||||
To build the examples, use `ninja examples`.
|
||||
|
||||
If you want to target smaller executable sizes, using a compressing linker like
|
||||
[Crinkler](https://github.com/runestubbe/Crinkler) on Windows is recommended.
|
||||
|
||||
The linux examples use ALSA and need libasound2-dev (or libasound2-dev:386)
|
||||
installed. The 386 version also needs pipewire-alsa:386 installed, which is not
|
||||
there by default.
|
||||
|
||||
### Native virtual machine
|
||||
|
||||
The native bridge allows Go to call the Sointu compiled x86 native virtual
|
||||
machine, through cgo, instead of using the Go written bytecode interpreter. With
|
||||
the latest Go compiler, the native virtual machine is actually slower than the
|
||||
Go-written one, but importantly, the native virtual machine allows you to test
|
||||
that the patch also works within the stack limits of the x87 virtual machine,
|
||||
which is the VM used in the compiled intros. In the tracker/VSTi, you can switch
|
||||
between the native synth and the Go synth under the CPU panel in the Song
|
||||
settings.
|
||||
|
||||
Before you can actually run it, you need to build the bridge using CMake (thus,
|
||||
***this will not work with go get***).
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- [CMake](https://cmake.org)
|
||||
- [nasm](https://www.nasm.us/)
|
||||
- *cgo compatible compiler* e.g. [gcc](https://gcc.gnu.org/). On windows, you
|
||||
best bet is [MinGW](http://www.mingw.org/). We use the
|
||||
[tdm-gcc](https://jmeubank.github.io/tdm-gcc/)
|
||||
|
||||
The last point is because the command line player and the tracker use
|
||||
[cgo](https://golang.org/cmd/cgo/) to interface with the synth core, which is
|
||||
compiled into a library. The cgo bridge resides in the package
|
||||
[bridge](vm/compiler/bridge/).
|
||||
|
||||
#### Building
|
||||
|
||||
Assuming you are using [ninja](https://ninja-build.org/):
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -GNinja
|
||||
ninja sointu
|
||||
```
|
||||
|
||||
> :warning: *you must build the library inside a directory called 'build' at the
|
||||
> root of the project*. This is because the path where cgo looks for the library
|
||||
> is hard coded to point to build/ in the go files.
|
||||
|
||||
Running `ninja sointu` only builds the static library that Go needs. This is a
|
||||
lot faster than building all the CTests.
|
||||
|
||||
You and now run all the Go tests, even the ones that test the native bridge.
|
||||
From the project root folder, run:
|
||||
|
||||
```
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Play a song from the command line:
|
||||
```
|
||||
go run -tags=native cmd/sointu-play/main.go tests/test_chords.yml
|
||||
```
|
||||
|
||||
> :warning: Unlike the x86/amd64 VM compiled by Sointu, the Go written VM
|
||||
> bytecode interpreter uses a software stack. Thus, unlike x87 FPU stack, it is
|
||||
@ -76,53 +277,35 @@ currently limited by these.
|
||||
> opcodes). In future, the app should give warnings if the user is about to
|
||||
> exceed the capabilities of a target platform.
|
||||
|
||||
### Compiler
|
||||
> :warning: **If you are using Yasm instead of Nasm, and you are using MinGW**:
|
||||
> Yasm 1.3.0 (currently still the latest stable release) and GNU linker do not
|
||||
> play nicely along, trashing the BSS layout. The linker had placed our synth
|
||||
> object overlapping with DLL call addresses; very funny stuff to debug. See
|
||||
> [here](https://tortall.lighthouseapp.com/projects/78676/tickets/274-bss-problem-with-windows-win64)
|
||||
> and the fix
|
||||
> [here](https://github.com/yasm/yasm/commit/1910e914792399137dec0b047c59965207245df5).
|
||||
> Since Nasm is nowadays under BSD license, there is absolutely no reason to use
|
||||
> Yasm. However, if you do, use a newer nightly build of Yasm that includes the
|
||||
> fix.
|
||||
|
||||
The command line interface to it is [sointu-compile](cmd/sointu-compile/main.go)
|
||||
and the actual code resides in the [compiler](vm/compiler/) package, which is an
|
||||
ordinary [go](https://golang.org/) package with no other tool dependencies.
|
||||
### Tests
|
||||
|
||||
Running the compiler:
|
||||
There are [regression tests](tests/) that are built as executables,
|
||||
testing that they work the same way when you would link them in an
|
||||
intro.
|
||||
|
||||
```
|
||||
go run cmd/sointu-compile/main.go
|
||||
```
|
||||
#### Prerequisites
|
||||
|
||||
Building the compiler:
|
||||
|
||||
```
|
||||
go build -o sointu-compile cmd/sointu-compile/main.go
|
||||
```
|
||||
|
||||
On windows, replace `-o sointu-compile` with `-o sointu-compile.exe`.
|
||||
|
||||
The compiler can then be used to compile a .yml song into .asm and .h files. For
|
||||
example:
|
||||
|
||||
```
|
||||
sointu-compile -o . -arch=386 tests/test_chords.yml
|
||||
nasm -f win32 test_chords.asm
|
||||
```
|
||||
|
||||
WebAssembly example:
|
||||
|
||||
```
|
||||
sointu-compile -o . -arch=wasm tests/test_chords.yml
|
||||
wat2wasm --enable-bulk-memory test_chords.wat
|
||||
```
|
||||
|
||||
### Building and running the tests as executables
|
||||
|
||||
Building the [regression tests](tests/) as executables (testing that they work
|
||||
the same way when you would link them in an intro) requires:
|
||||
- [go](https://golang.org/)
|
||||
- [CMake](https://cmake.org) with CTest
|
||||
- [nasm](https://www.nasm.us/) or [yasm](https://yasm.tortall.net)
|
||||
- [nasm](https://www.nasm.us/)
|
||||
- Your favorite CMake compatible c-compiler & build tool. Results have been
|
||||
obtained using Visual Studio 2019, gcc&make on linux, MinGW&mingw32-make, and
|
||||
ninja&AppleClang.
|
||||
|
||||
For example, using [ninja](https://ninja-build.org/):
|
||||
#### Building and running
|
||||
|
||||
Assuming you are using [ninja](https://ninja-build.org/):
|
||||
|
||||
```
|
||||
mkdir build
|
||||
@ -142,93 +325,26 @@ cmake .. -DCMAKE_C_FLAGS="-m32" -DCMAKE_ASM_NASM_OBJECT_FORMAT="win32" -GNinja
|
||||
Another example: on Visual Studio 2019 Community, just open the folder, choose
|
||||
either Debug or Release and either x86 or x64 build, and hit build all.
|
||||
|
||||
### Native bridge & sointu-nativetrack
|
||||
|
||||
The native bridge allows Go to call the sointu compiled virtual machine, through
|
||||
cgo, instead of using the Go written bytecode interpreter. It's likely slightly
|
||||
faster than the interpreter. The version of the tracker that uses the native
|
||||
bridge is [sointu-nativetrack](cmd/sointu-nativetrack/). Before you can actually
|
||||
run it, you need to build the bridge using CMake (thus, the nativetrack does not
|
||||
work with go get)
|
||||
|
||||
Building the native bridge requires:
|
||||
- [go](https://golang.org/)
|
||||
- [CMake](https://cmake.org)
|
||||
- [nasm](https://www.nasm.us/) or [yasm](https://yasm.tortall.net)
|
||||
- *cgo compatible compiler* e.g. [gcc](https://gcc.gnu.org/). On windows, you
|
||||
best bet is [MinGW](http://www.mingw.org/). We use the
|
||||
[tdm-gcc](https://jmeubank.github.io/tdm-gcc/)
|
||||
|
||||
The last point is because the command line player and the tracker use
|
||||
[cgo](https://golang.org/cmd/cgo/) to interface with the synth core, which is
|
||||
compiled into a library. The cgo bridge resides in the package
|
||||
[bridge](vm/compiler/bridge/).
|
||||
|
||||
> :warning: *you must build the library inside a directory called 'build' at the
|
||||
> root of the project*. This is because the path where cgo looks for the library
|
||||
> is hard coded to point to build/ in the go files.
|
||||
|
||||
So, to build the library, run (this example is using
|
||||
[ninja](https://ninja-build.org/) for the build; adapt for other build tools
|
||||
accordingly):
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -GNinja
|
||||
ninja sointu
|
||||
```
|
||||
|
||||
Running `ninja sointu` only builds the static library that Go needs. This is a
|
||||
lot faster than building all the CTests.
|
||||
|
||||
You and now run all the Go tests, even the ones that test the native bridge.
|
||||
From the project root folder, run:
|
||||
|
||||
```
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Play a song from the command line:
|
||||
```
|
||||
go run cmd/sointu-play/main.go tests/test_chords.yml
|
||||
```
|
||||
|
||||
Run the tracker using the native bridge
|
||||
```
|
||||
go run cmd/sointu-nativetrack/main.go
|
||||
```
|
||||
|
||||
> :warning: **If you are using MinGW and Yasm**: Yasm 1.3.0 (currently still the
|
||||
> latest stable release) and GNU linker do not play nicely along, trashing the
|
||||
> BSS layout. See
|
||||
> [here](https://tortall.lighthouseapp.com/projects/78676/tickets/274-bss-problem-with-windows-win64)
|
||||
> and the fix
|
||||
> [here](https://github.com/yasm/yasm/commit/1910e914792399137dec0b047c59965207245df5).
|
||||
> Use a newer nightly build of yasm that includes the fix. The linker had placed
|
||||
> our synth object overlapping with DLL call addresses; very funny stuff to
|
||||
> debug.
|
||||
|
||||
> :warning: The sointu-nativetrack cannot be used with the syncs at the moment.
|
||||
> For syncs, use the Go VM (sointu-track).
|
||||
|
||||
### Building and running the WebAssembly tests
|
||||
### WebAssembly tests
|
||||
|
||||
These are automatically invoked by CTest if [node](https://nodejs.org) and
|
||||
[wat2wasm](https://github.com/WebAssembly/wabt) are found in the path.
|
||||
|
||||
New features since fork
|
||||
-----------------------
|
||||
|
||||
- **New units**. For example: bit-crusher, gain, inverse gain, clip, modulate
|
||||
bpm (proper triplets!), compressor (can be used for side-chaining).
|
||||
bpm (proper triplets!), compressor (can be used for side-chaining), bell eq
|
||||
(for more versatile EQuing of the sounds).
|
||||
- **Compiler**. Written in go. The input is a .yml file and the output is an
|
||||
.asm. It works by inputting the song data to the excellent go
|
||||
`text/template` package, effectively working as a preprocessor. This allows
|
||||
quite powerful combination: we can handcraft the assembly code to keep the
|
||||
entropy as low as possible, yet we can call arbitrary go functions as
|
||||
"macros". The templates are [here](templates/) and the compiler lives
|
||||
"macros". The templates are [here](vm/compiler/templates/) and the compiler lives
|
||||
[here](vm/compiler/).
|
||||
- **Tracker**. Written in go. A crude version exists.
|
||||
- **Tracker**. Written in go. Can run either as a stand-alone app or a vsti
|
||||
plugin.
|
||||
- **Supports 32 and 64 bit builds**. The 64-bit version is done with minimal
|
||||
changes to get it work, using template macros to change the lines between
|
||||
32-bit and 64-bit modes. Mostly, it's as easy as writing {{.AX}} instead of
|
||||
@ -242,9 +358,9 @@ New features since fork
|
||||
opcodes. So, you can have a single instrument with three voices, and three
|
||||
tracks that use this instrument, to make chords. See
|
||||
[here](tests/test_chords.yml) for an example and
|
||||
[here](templates/amd64-386/patch.asm) for the implementation. The maximum
|
||||
total number of voices is 32: you can have 32 monophonic instruments or any
|
||||
combination of polyphonic instruments adding up to 32.
|
||||
[here](vm/compiler/templates/amd64-386/patch.asm) for the implementation.
|
||||
The maximum total number of voices is 32: you can have 32 monophonic
|
||||
instruments or any combination of polyphonic instruments adding up to 32.
|
||||
- **Any number of voices per track**. A single track can trigger more than one
|
||||
voice. At every note, a new voice from the assigned voices is triggered and
|
||||
the previous released. Combined with the previous, you can have a single
|
||||
@ -253,21 +369,21 @@ New features since fork
|
||||
Not only that, a track can even trigger voices of different instruments,
|
||||
alternating between these two; maybe useful for example as an easy way to
|
||||
alternate between an open and a closed hihat.
|
||||
- **Easily extensible**. Instead of %ifdef hell, the primary extension
|
||||
mechanism is through new opcodes for the virtual machine. Only the opcodes
|
||||
actually used in a song are compiled into the virtual machine. The goal is
|
||||
to try to write the code so that if two similar opcodes are used, the common
|
||||
code in both is reused by moving it to a function. Macro and linker magic
|
||||
ensure that also helper functions are only compiled in if they are actually
|
||||
used.
|
||||
- **Reasonably easily extensible**. Instead of %ifdef hell, the primary
|
||||
extension mechanism is through new opcodes for the virtual machine. Only the
|
||||
opcodes actually used in a song are compiled into the virtual machine. The
|
||||
goal is to try to write the code so that if two similar opcodes are used,
|
||||
the common code in both is reused by moving it to a function. Macro and
|
||||
linker magic ensure that also helper functions are only compiled in if they
|
||||
are actually used.
|
||||
- **Songs are YAML files**. These markup files are simple data files,
|
||||
describing the tracks, patterns and patch structure (see
|
||||
[here](tests/test_oscillat_trisaw.yml) for an example). The sointu-cli
|
||||
compiler then reads these files and compiles them into .asm code. This has
|
||||
the nice implication that, in future, there will be no need for a binary
|
||||
format to save patches, nor should you need to commit .o or .asm to repo:
|
||||
just put the .yml in the repo and automate the .yml -> .asm -> .o steps
|
||||
using sointu-cli & nasm.
|
||||
[here](tests/test_oscillat_trisaw.yml) for an example). The sointu-compile
|
||||
then reads these files and compiles them into .asm code. This has the nice
|
||||
implication that, in future, there will be no need for a binary format to
|
||||
save patches, nor should you need to commit .o or .asm to repo: just put the
|
||||
.yml in the repo and automate the .yml -> .asm -> .o steps using
|
||||
sointu-compile & nasm.
|
||||
- **Harmonized support for stereo signals**. Every opcode supports a stereo
|
||||
variant: the stereo bit is hidden in the least significant bit of the
|
||||
command stream and passed in carry to the opcode. This has several nice
|
||||
@ -278,7 +394,7 @@ New features since fork
|
||||
stereo opcodes usually follow stereo opcodes (and mono opcodes follow mono
|
||||
opcodes), the stereo bits of the command bytes will be highly correlated and
|
||||
if crinkler or any other modeling compressor is doing its job, that should
|
||||
make them highly predictable i.e. highly compressably.
|
||||
make them highly predictable i.e. highly compressable.
|
||||
- **Test-driven development**. Given that 4klang was already a mature project,
|
||||
the first thing actually implemented was a set of regression tests to avoid
|
||||
breaking everything beyond any hope of repair. Done, using go test (runs the
|
||||
@ -297,12 +413,13 @@ New features since fork
|
||||
ports / 4 stereo ports, so even this method of routing is unlikely to run
|
||||
out of ports in small intros.
|
||||
- **Pattern length does not have to be a power of 2**.
|
||||
- **Sample-based oscillators, with samples imported from gm.dls**. Reading
|
||||
gm.dls is obviously Windows only, but with some effort the sample mechanism
|
||||
can be used also without it, in case you are working on a 64k and have some
|
||||
kilobytes to spare. See [this example](tests/test_oscillat_sample.yml), and
|
||||
this go generate [program](cmd/sointu-generate/main.go) parses the gm.dls
|
||||
file and dumps the sample offsets from it.
|
||||
- **Sample-based oscillators, with samples imported from gm.dls**. The
|
||||
gm.dls is available from system folder only on Windows, but the
|
||||
non-native tracker looks for it also in the current folder, so
|
||||
should you somehow magically get hold of gm.dls on Linux or Mac, you
|
||||
can drop it in the same folder with the tracker. See [this example](tests/test_oscillat_sample.yml),
|
||||
and this go generate [program](cmd/sointu-generate/main.go) parses
|
||||
the gm.dls file and dumps the sample offsets from it.
|
||||
- **Unison oscillators**. Multiple copies of the oscillator running slightly
|
||||
detuned and added up to together. Great for trance leads (supersaw). Unison
|
||||
of up to 4, or 8 if you make stereo unison oscillator and add up both left
|
||||
@ -313,54 +430,9 @@ New features since fork
|
||||
releasing voices etc.)
|
||||
- **Calling Sointu as a library from Go language**. The Go API is slighty more
|
||||
sane than the low-level library API, offering more Go-like experience.
|
||||
- **A bytecode interpreter written in pure go**. It's slightly slower than the
|
||||
hand-written assembly code by sointu compiler, but with this, the tracker is
|
||||
ultraportable and does not need cgo calls.
|
||||
- **Using Sointu as a sync-tracker**. Similar to [GNU
|
||||
Rocket](https://github.com/yupferris/gnurocket), but (ab)using the tracker
|
||||
we already have for music. We use the Go "rpc" package to send current sync
|
||||
values from the new "sync" opcode + optionally the current fractional row
|
||||
the song is on. The syncs are saved every 256th sample (approximately 172
|
||||
Hz). For 4k intro development, the idea is to write a debug version of the
|
||||
intro that merely loads the shader and listens to the RPC messages, and then
|
||||
draws the shader with those as the uniforms. Then, during the actual 4k
|
||||
intro, one can get the sync data from Sointu: if the song uses syncs,
|
||||
su_render_song writes the syncs to a float array. During each time step, a
|
||||
slice of this array can be sent to the shader as a uniform float array. A
|
||||
track with two voices, triggering an instrument with a single envelope and a
|
||||
slow filter can even be used as a cheap smooth interpolation mechanism,
|
||||
provided the syncs are added to each other in the shader.
|
||||
|
||||
Future goals
|
||||
------------
|
||||
|
||||
- **Find a more general solution for skipping opcodes / early outs**. It might
|
||||
be a new opcode "skip" that skips from the opcode to the next out in case
|
||||
the signal entering skip and the signal leaving out are both close to zero.
|
||||
Need to investigate the best way to implement this.
|
||||
- **Even more opcodes**. Some potentially useful additions could be:
|
||||
- Equalizer / more flexible filters
|
||||
- Very slow filters (~ DC-offset removal). Can be implemented using a single
|
||||
bit flag in the existing filter
|
||||
- Arbitrary envelopes; for easier automation.
|
||||
- **MIDI support for the tracker**.
|
||||
- **Find a solution for denormalized signals**. Denormalized floating point
|
||||
numbers (floating point numbers that are very very small) can result in 100x
|
||||
CPU slow down. We got hit by this already: the damp filters in delay units
|
||||
were denormalizing, resulting in the synth being unusable in real time. Need
|
||||
to investigate a) where denormalization can happen; b) how to prevent it:
|
||||
add & substract value; c) make this optional to the user. For quick
|
||||
explanation about the potential massive CPU hit, see
|
||||
https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch
|
||||
|
||||
Long-shot ideas
|
||||
-----------
|
||||
- **Hack deeper into audio sources from the OS**. Speech synthesis, I'm eyeing
|
||||
at you.
|
||||
- **Ability to run Sointu as a DAW plugin (VSTi3)**. Now that Renoise supports
|
||||
VSTi3, there's no fundamental objection to compiling Sointu as a VSTi3. But
|
||||
don't expect it any soon; I need to digest the idea of having to learn the
|
||||
horrors of the VSTi3 C++ API.
|
||||
- **A bytecode interpreter written in pure go**. With the latest Go compiler,
|
||||
it's slightly faster hand-written one using x87 opcodes. With this, the
|
||||
tracker is ultraportable and does not need cgo calls.
|
||||
|
||||
Design philosophy
|
||||
-----------------
|
||||
@ -382,8 +454,8 @@ Design philosophy
|
||||
- Benchmark optimizations. Compression results are sometimes slightly
|
||||
nonintuitive so alternative implementations should always be benchmarked
|
||||
e.g. by compiling and linking a real-world song with
|
||||
[Leviathan](https://github.com/armak/Leviathan-2.0) and observing how the
|
||||
optimizations affect the byte size.
|
||||
[one of the examples](examples/code/C) and observing how the optimizations
|
||||
affect the byte size.
|
||||
|
||||
Background and history
|
||||
----------------------
|
||||
@ -415,19 +487,56 @@ so I thought it would fun to learn some Finnish for a change. And
|
||||
Prods using Sointu
|
||||
------------------
|
||||
|
||||
[Adam](https://github.com/vsariola/adam) by brainlez Coders! - My first test-driving of Sointu. Some ideas how to integrate Sointu to the build chain.
|
||||
- [Adam](https://github.com/vsariola/adam) by brainlez Coders! My first
|
||||
test-driving of Sointu. The repository has some ideas how to integrate
|
||||
Sointu to the build chain.
|
||||
- [Roadtrip](https://www.pouet.net/prod.php?which=94105) by LJ & Virgill
|
||||
- [|](https://www.pouet.net/prod.php?which=94721) by epoqe. Likely the first
|
||||
Linux 4k intro using sointu.
|
||||
- [Physics Girl St.](https://www.pouet.net/prod.php?which=94890) by Team210
|
||||
- [Delusions of mediocrity](https://www.pouet.net/prod.php?which=95222) by
|
||||
mrange & Virgill
|
||||
- [Xorverse](https://www.pouet.net/prod.php?which=95221) by Alcatraz
|
||||
- [l'enveloppe](https://www.pouet.net/prod.php?which=95215) by Team210 & epoqe
|
||||
- [Phosphorescent Purple Pixel Peaks](https://www.pouet.net/prod.php?which=96198) by mrange & Virgill
|
||||
- [21](https://demozoo.org/music/338597/) by NR4 / Team210
|
||||
- [Tausendeins](https://www.pouet.net/prod.php?which=96192) by epoqe & Team210
|
||||
- [Radiant](https://www.pouet.net/prod.php?which=97200) by Team210
|
||||
- [Aurora Florae](https://www.pouet.net/prod.php?which=97516) by Team210 and
|
||||
epoqe
|
||||
- [Night Ride](https://www.pouet.net/prod.php?which=98212) by Ctrl-Alt-Test &
|
||||
Alcatraz
|
||||
- [Bicolor Challenge](https://demozoo.org/competitions/19410/) with [Sointu
|
||||
song](https://files.scene.org/view/parties/2024/deadline24/bicolor_challenge/wayfinder_-_bicolor_soundtrack.zip)
|
||||
provided by wayfinder
|
||||
- [napolnitel](https://www.pouet.net/prod.php?which=104336) by jetlag
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Pull requests / suggestions / issues welcome, through Github! Or just DM
|
||||
me on Discord (see contact information below).
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Distributed under the MIT License. See [LICENSE](LICENSE) for more information.
|
||||
|
||||
Contact
|
||||
-------
|
||||
|
||||
Veikko Sariola - pestis_bc on Demoscene discord - firstname.lastname@gmail.com
|
||||
|
||||
Project Link: [https://github.com/vsariola/sointu](https://github.com/vsariola/sointu)
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
The original 4klang was developed by Dominik Ries
|
||||
([gopher](https://github.com/hzdgopher/4klang)) and Paul Kraus (pOWL) of
|
||||
Alcatraz. :heart:
|
||||
The original 4klang: Dominik Ries ([gopher/Alcatraz](https://github.com/hzdgopher/4klang))
|
||||
& Paul Kraus (pOWL/Alcatraz) :heart:
|
||||
|
||||
Sointu was initiated by Veikko Sariola (pestis/bC!).
|
||||
|
||||
Apollo/bC! put the project on the path to Go, and wrote the prototype of the
|
||||
tracker GUI.
|
||||
|
||||
PoroCYon's [4klang fork](https://github.com/PoroCYon/4klang) inspired the macros
|
||||
for better cross-platform support.
|
||||
Sointu: Veikko Sariola (pestis/bC!), [Apollo/bC!](https://github.com/moitias),
|
||||
[NR4/Team210](https://github.com/LeStahL/),
|
||||
[PoroCYon](https://github.com/PoroCYon/4klang),
|
||||
[kendfss](https://github.com/kendfss), [anticore](https://github.com/anticore),
|
||||
[qm210](https://github.com/qm210), [reaby](https://github.com/reaby)
|
||||
|
||||
304
audio.go
304
audio.go
@ -1,11 +1,303 @@
|
||||
package sointu
|
||||
|
||||
type AudioSink interface {
|
||||
WriteAudio(buffer []float32) error
|
||||
Close() error
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
// AudioBuffer is a buffer of stereo audio samples of variable length, each
|
||||
// sample represented by [2]float32. [0] is left channel, [1] is right
|
||||
AudioBuffer [][2]float32
|
||||
|
||||
CloserWaiter interface {
|
||||
io.Closer
|
||||
Wait()
|
||||
}
|
||||
|
||||
// AudioContext represents the low-level audio drivers. There should be at
|
||||
// most one AudioContext at a time. The interface is implemented at least by
|
||||
// oto.OtoContext, but in future we could also mock it.
|
||||
//
|
||||
// AudioContext is used to play one or more AudioSources. Playing can be
|
||||
// stopped by closing the returned io.Closer.
|
||||
AudioContext interface {
|
||||
Play(r AudioSource) CloserWaiter
|
||||
}
|
||||
|
||||
// AudioSource is an function for reading audio samples into an AudioBuffer.
|
||||
// Returns error if the buffer is not filled.
|
||||
AudioSource func(buf AudioBuffer) error
|
||||
|
||||
BufferSource struct {
|
||||
buffer AudioBuffer
|
||||
pos int
|
||||
}
|
||||
|
||||
// Synth represents a state of a synthesizer, compiled from a Patch.
|
||||
Synth interface {
|
||||
// Render tries to fill a stereo signal buffer with sound from the
|
||||
// synthesizer, until either the buffer is full or a given number of
|
||||
// timesteps is advanced. Normally, 1 sample = 1 unit of time, but speed
|
||||
// modulations may change this. It returns the number of samples filled (in
|
||||
// stereo samples i.e. number of elements of AudioBuffer filled), the
|
||||
// number of sync outputs written, the number of time steps time advanced,
|
||||
// and a possible error.
|
||||
Render(buffer AudioBuffer, maxtime int) (sample int, time int, err error)
|
||||
|
||||
// Update recompiles a patch, but should maintain as much as possible of its
|
||||
// state as reasonable. For example, filters should keep their state and
|
||||
// delaylines should keep their content. Every change in the Patch triggers
|
||||
// an Update and if the Patch would be started fresh every time, it would
|
||||
// lead to very choppy audio.
|
||||
Update(patch Patch, bpm int) error
|
||||
|
||||
// Trigger triggers a note for a given voice. Called between synth.Renders.
|
||||
Trigger(voice int, note byte)
|
||||
|
||||
// Release releases the currently playing note for a given voice. Called
|
||||
// between synth.Renders.
|
||||
Release(voice int)
|
||||
|
||||
// Close disposes the synth, freeing any resources. No other functions should be called after Close.
|
||||
Close()
|
||||
|
||||
// Populates the given array with the current CPU load of each thread,
|
||||
// returning the number of threads / elements populated
|
||||
CPULoad([]CPULoad) int
|
||||
}
|
||||
|
||||
// Synther compiles a given Patch into a Synth, throwing errors if the
|
||||
// Patch is malformed.
|
||||
Synther interface {
|
||||
Name() string // Name of the synther, e.g. "Go" or "Native"
|
||||
Synth(patch Patch, bpm int) (Synth, error)
|
||||
SupportsMultithreading() bool
|
||||
}
|
||||
|
||||
CPULoad float32
|
||||
)
|
||||
|
||||
// Play plays the Song by first compiling the patch with the given Synther,
|
||||
// returning the stereo audio buffer as a result (and possible errors).
|
||||
func Play(synther Synther, song Song, progress func(float32)) (AudioBuffer, error) {
|
||||
err := song.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
synth, err := synther.Synth(song.Patch, song.BPM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sointu.Play failed: %v", err)
|
||||
}
|
||||
defer synth.Close()
|
||||
curVoices := make([]int, len(song.Score.Tracks))
|
||||
for i := range curVoices {
|
||||
curVoices[i] = song.Score.FirstVoiceForTrack(i)
|
||||
}
|
||||
initialCapacity := song.Score.LengthInRows() * song.SamplesPerRow()
|
||||
buffer := make(AudioBuffer, 0, initialCapacity)
|
||||
rowbuffer := make(AudioBuffer, song.SamplesPerRow())
|
||||
for row := 0; row < song.Score.LengthInRows(); row++ {
|
||||
patternRow := row % song.Score.RowsPerPattern
|
||||
pattern := row / song.Score.RowsPerPattern
|
||||
for t := range song.Score.Tracks {
|
||||
order := song.Score.Tracks[t].Order
|
||||
if pattern < 0 || pattern >= len(order) {
|
||||
continue
|
||||
}
|
||||
patternIndex := song.Score.Tracks[t].Order[pattern]
|
||||
patterns := song.Score.Tracks[t].Patterns
|
||||
if patternIndex < 0 || int(patternIndex) >= len(patterns) {
|
||||
continue
|
||||
}
|
||||
pattern := patterns[patternIndex]
|
||||
if patternRow < 0 || patternRow >= len(pattern) {
|
||||
continue
|
||||
}
|
||||
note := pattern[patternRow]
|
||||
if note > 0 && note <= 1 { // anything but hold causes an action.
|
||||
continue
|
||||
}
|
||||
synth.Release(curVoices[t])
|
||||
if note > 1 {
|
||||
curVoices[t]++
|
||||
first := song.Score.FirstVoiceForTrack(t)
|
||||
if curVoices[t] >= first+song.Score.Tracks[t].NumVoices {
|
||||
curVoices[t] = first
|
||||
}
|
||||
synth.Trigger(curVoices[t], note)
|
||||
}
|
||||
}
|
||||
tries := 0
|
||||
for rowtime := 0; rowtime < song.SamplesPerRow(); {
|
||||
samples, time, err := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
|
||||
if err != nil {
|
||||
return buffer, fmt.Errorf("render failed: %v", err)
|
||||
}
|
||||
rowtime += time
|
||||
buffer = append(buffer, rowbuffer[:samples]...)
|
||||
if tries > 100 {
|
||||
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
|
||||
}
|
||||
}
|
||||
if progress != nil {
|
||||
progress(float32(row+1) / float32(song.Score.LengthInRows()))
|
||||
}
|
||||
}
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
type AudioContext interface {
|
||||
Output() AudioSink
|
||||
Close() error
|
||||
// Fill fills the AudioBuffer using a Synth, disregarding all syncs and time
|
||||
// limits. Note that this will change the state of the Synth.
|
||||
func (buffer AudioBuffer) Fill(synth Synth) error {
|
||||
s, _, err := synth.Render(buffer, math.MaxInt32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("synth.Render failed: %v", err)
|
||||
}
|
||||
if s != len(buffer) {
|
||||
return errors.New("in AudioBuffer.Fill, synth.Render should have filled the whole buffer but did not")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b AudioBuffer) Source() AudioSource {
|
||||
return func(buf AudioBuffer) error {
|
||||
n := copy(buf, b)
|
||||
b = b[n:]
|
||||
if n < len(buf) {
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAudio reads audio samples from an AudioSource into an AudioBuffer.
|
||||
// Returns an error when the buffer is fully consumed.
|
||||
func (a *BufferSource) ReadAudio(buf AudioBuffer) error {
|
||||
n := copy(buf, a.buffer[a.pos:])
|
||||
a.pos += n
|
||||
if a.pos >= len(a.buffer) {
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wav converts an AudioBuffer into a valid WAV-file, returned as a []byte
|
||||
// array.
|
||||
//
|
||||
// If pcm16 is set to true, the samples in the WAV-file will be 16-bit signed
|
||||
// integers; otherwise the samples will be 32-bit floats
|
||||
func (buffer AudioBuffer) Wav(pcm16 bool) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
wavHeader(len(buffer)*2, pcm16, buf)
|
||||
err := buffer.rawToBuffer(pcm16, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Wav failed: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Raw converts an AudioBuffer into a raw audio file, returned as a []byte
|
||||
// array.
|
||||
//
|
||||
// If pcm16 is set to true, the samples will be 16-bit signed integers;
|
||||
// otherwise the samples will be 32-bit floats
|
||||
func (buffer AudioBuffer) Raw(pcm16 bool) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := buffer.rawToBuffer(pcm16, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Raw failed: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (p *CPULoad) Update(duration time.Duration, frames int64) {
|
||||
if frames <= 0 {
|
||||
return // no frames rendered, so cannot compute CPU load
|
||||
}
|
||||
realtime := float64(duration) / 1e9
|
||||
songtime := float64(frames) / 44100
|
||||
newload := realtime / songtime
|
||||
alpha := math.Exp(-songtime) // smoothing factor, time constant of 1 second
|
||||
*p = CPULoad(float64(*p)*alpha + newload*(1-alpha))
|
||||
}
|
||||
|
||||
func (data AudioBuffer) rawToBuffer(pcm16 bool, buf *bytes.Buffer) error {
|
||||
var err error
|
||||
if pcm16 {
|
||||
int16data := make([][2]int16, len(data))
|
||||
for i, v := range data {
|
||||
int16data[i][0] = int16(clamp(int(v[0]*math.MaxInt16), math.MinInt16, math.MaxInt16))
|
||||
int16data[i][1] = int16(clamp(int(v[1]*math.MaxInt16), math.MinInt16, math.MaxInt16))
|
||||
}
|
||||
err = binary.Write(buf, binary.LittleEndian, int16data)
|
||||
} else {
|
||||
err = binary.Write(buf, binary.LittleEndian, data)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wavHeader writes a wave header for either float32 or int16 .wav file into the
|
||||
// bytes.buffer. It needs to know the length of the buffer and assumes stereo
|
||||
// sound, so the length in stereo samples (L + R) is bufferlength / 2. If pcm16
|
||||
// = true, then the header is for int16 audio; pcm16 = false means the header is
|
||||
// for float32 audio. Assumes 44100 Hz sample rate.
|
||||
func wavHeader(bufferLength int, pcm16 bool, buf *bytes.Buffer) {
|
||||
// Refer to: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
|
||||
numChannels := 2
|
||||
sampleRate := 44100
|
||||
var bytesPerSample, chunkSize, fmtChunkSize, waveFormat int
|
||||
var factChunk bool
|
||||
if pcm16 {
|
||||
bytesPerSample = 2
|
||||
chunkSize = 36 + bytesPerSample*bufferLength
|
||||
fmtChunkSize = 16
|
||||
waveFormat = 1 // PCM
|
||||
factChunk = false
|
||||
} else {
|
||||
bytesPerSample = 4
|
||||
chunkSize = 50 + bytesPerSample*bufferLength
|
||||
fmtChunkSize = 18
|
||||
waveFormat = 3 // IEEE float
|
||||
factChunk = true
|
||||
}
|
||||
buf.Write([]byte("RIFF"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(chunkSize))
|
||||
buf.Write([]byte("WAVE"))
|
||||
buf.Write([]byte("fmt "))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(fmtChunkSize))
|
||||
binary.Write(buf, binary.LittleEndian, uint16(waveFormat))
|
||||
binary.Write(buf, binary.LittleEndian, uint16(numChannels))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(sampleRate))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(sampleRate*numChannels*bytesPerSample)) // avgBytesPerSec
|
||||
binary.Write(buf, binary.LittleEndian, uint16(numChannels*bytesPerSample)) // blockAlign
|
||||
binary.Write(buf, binary.LittleEndian, uint16(8*bytesPerSample)) // bits per sample
|
||||
if fmtChunkSize > 16 {
|
||||
binary.Write(buf, binary.LittleEndian, uint16(0)) // size of extension
|
||||
}
|
||||
if factChunk {
|
||||
buf.Write([]byte("fact"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(4)) // fact chunk size
|
||||
binary.Write(buf, binary.LittleEndian, uint32(bufferLength)) // sample length
|
||||
}
|
||||
buf.Write([]byte("data"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(bytesPerSample*bufferLength))
|
||||
}
|
||||
|
||||
func clamp(value, min, max int) int {
|
||||
if value < min {
|
||||
return min
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
101
audioexport.go
101
audioexport.go
@ -1,101 +0,0 @@
|
||||
package sointu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
func Wav(buffer []float32, pcm16 bool) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
wavHeader(len(buffer), pcm16, buf)
|
||||
err := rawToBuffer(buffer, pcm16, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Wav failed: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func Raw(buffer []float32, pcm16 bool) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := rawToBuffer(buffer, pcm16, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Raw failed: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func rawToBuffer(data []float32, pcm16 bool, buf *bytes.Buffer) error {
|
||||
var err error
|
||||
if pcm16 {
|
||||
int16data := make([]int16, len(data))
|
||||
for i, v := range data {
|
||||
int16data[i] = int16(clamp(int(v*math.MaxInt16), math.MinInt16, math.MaxInt16))
|
||||
}
|
||||
err = binary.Write(buf, binary.LittleEndian, int16data)
|
||||
} else {
|
||||
err = binary.Write(buf, binary.LittleEndian, data)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wavHeader writes a wave header for either float32 or int16 .wav file into the
|
||||
// bytes.buffer. It needs to know the length of the buffer and assumes stereo
|
||||
// sound, so the length in stereo samples (L + R) is bufferlength / 2. If pcm16
|
||||
// = true, then the header is for int16 audio; pcm16 = false means the header is
|
||||
// for float32 audio. Assumes 44100 Hz sample rate.
|
||||
func wavHeader(bufferLength int, pcm16 bool, buf *bytes.Buffer) {
|
||||
// Refer to: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
|
||||
numChannels := 2
|
||||
sampleRate := 44100
|
||||
var bytesPerSample, chunkSize, fmtChunkSize, waveFormat int
|
||||
var factChunk bool
|
||||
if pcm16 {
|
||||
bytesPerSample = 2
|
||||
chunkSize = 36 + bytesPerSample*bufferLength
|
||||
fmtChunkSize = 16
|
||||
waveFormat = 1 // PCM
|
||||
factChunk = false
|
||||
} else {
|
||||
bytesPerSample = 4
|
||||
chunkSize = 50 + bytesPerSample*bufferLength
|
||||
fmtChunkSize = 18
|
||||
waveFormat = 3 // IEEE float
|
||||
factChunk = true
|
||||
}
|
||||
buf.Write([]byte("RIFF"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(chunkSize))
|
||||
buf.Write([]byte("WAVE"))
|
||||
buf.Write([]byte("fmt "))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(fmtChunkSize))
|
||||
binary.Write(buf, binary.LittleEndian, uint16(waveFormat))
|
||||
binary.Write(buf, binary.LittleEndian, uint16(numChannels))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(sampleRate))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(sampleRate*numChannels*bytesPerSample)) // avgBytesPerSec
|
||||
binary.Write(buf, binary.LittleEndian, uint16(numChannels*bytesPerSample)) // blockAlign
|
||||
binary.Write(buf, binary.LittleEndian, uint16(8*bytesPerSample)) // bits per sample
|
||||
if fmtChunkSize > 16 {
|
||||
binary.Write(buf, binary.LittleEndian, uint16(0)) // size of extension
|
||||
}
|
||||
if factChunk {
|
||||
buf.Write([]byte("fact"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(4)) // fact chunk size
|
||||
binary.Write(buf, binary.LittleEndian, uint32(bufferLength)) // sample length
|
||||
}
|
||||
buf.Write([]byte("data"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(bytesPerSample*bufferLength))
|
||||
}
|
||||
|
||||
func clamp(value, min, max int) int {
|
||||
if value < min {
|
||||
return min
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
12
cmd/midi_cgo.go
Normal file
12
cmd/midi_cgo.go
Normal file
@ -0,0 +1,12 @@
|
||||
//go:build cgo
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/tracker/gomidi"
|
||||
)
|
||||
|
||||
func NewMidiContext(broker *tracker.Broker) tracker.MIDIContext {
|
||||
return gomidi.NewContext(broker)
|
||||
}
|
||||
12
cmd/midi_not_cgo.go
Normal file
12
cmd/midi_not_cgo.go
Normal file
@ -0,0 +1,12 @@
|
||||
//go:build !cgo
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
func NewMidiContext(broker *tracker.Broker) tracker.MIDIContext {
|
||||
// with no cgo, we cannot use MIDI, so return a null context
|
||||
return tracker.NullMIDIContext{}
|
||||
}
|
||||
@ -14,6 +14,7 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/version"
|
||||
"github.com/vsariola/sointu/vm/compiler"
|
||||
)
|
||||
|
||||
@ -43,8 +44,13 @@ func main() {
|
||||
targetArch := flag.String("arch", runtime.GOARCH, "Target architecture. Defaults to OS architecture. Possible values: 386, amd64, wasm")
|
||||
output16bit := flag.Bool("i", false, "Compiled song should output 16-bit integers, instead of floats.")
|
||||
targetOs := flag.String("os", runtime.GOOS, "Target OS. Defaults to current OS. Possible values: windows, darwin, linux. Anything else is assumed linuxy. Ignored when targeting wasm.")
|
||||
versionFlag := flag.Bool("v", false, "Print version.")
|
||||
flag.Usage = printUsage
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
fmt.Println(version.VersionOrHash)
|
||||
os.Exit(0)
|
||||
}
|
||||
if (flag.NArg() == 0 && !*library) || *help {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -9,11 +9,13 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/vsariola/sointu/cmd"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/oto"
|
||||
"github.com/vsariola/sointu/vm/compiler/bridge"
|
||||
"github.com/vsariola/sointu/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -21,15 +23,27 @@ func main() {
|
||||
help := flag.Bool("h", false, "Show help.")
|
||||
directory := flag.String("o", "", "Directory where to output all files. The directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
|
||||
play := flag.Bool("p", false, "Play the input songs (default behaviour when no other output is defined).")
|
||||
unreleased := flag.Bool("u", false, "Start song with all oscillators unreleased.")
|
||||
//start := flag.Float64("start", 0, "Start playing from part; given in the units defined by parameter `unit`.")
|
||||
//stop := flag.Float64("stop", -1, "Stop playing at part; given in the units defined by parameter `unit`. Negative values indicate render until end.")
|
||||
//units := flag.String("unit", "pattern", "Units for parameters start and stop. Possible values: second, sample, pattern, beat. Warning: beat and pattern do not take SPEED modulations into account.")
|
||||
rawOut := flag.Bool("r", false, "Output the rendered song as .raw file. By default, saves stereo float32 buffer to disk.")
|
||||
wavOut := flag.Bool("w", false, "Output the rendered song as .wav file. By default, saves stereo float32 buffer to disk.")
|
||||
pcm := flag.Bool("c", false, "Convert audio to 16-bit signed PCM when outputting.")
|
||||
versionFlag := flag.Bool("v", false, "Print version.")
|
||||
syntherInt := flag.Int("synth", 0, "Select the synther to use. By default, uses the first one in the list of available synthers.")
|
||||
flag.Usage = printUsage
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
fmt.Println(version.VersionOrHash)
|
||||
os.Exit(0)
|
||||
}
|
||||
if *syntherInt < 0 || *syntherInt >= len(cmd.Synthers) {
|
||||
fmt.Fprintf(os.Stderr, "synth index %d is out of range; available synthers:\n", *syntherInt)
|
||||
for i, s := range cmd.Synthers {
|
||||
fmt.Fprintf(os.Stderr, " %d: %s\n", i, s.Name())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if flag.NArg() == 0 || *help {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
@ -38,6 +52,7 @@ func main() {
|
||||
*play = true // if the user gives nothing to output, then the default behaviour is just to play the file
|
||||
}
|
||||
var audioContext sointu.AudioContext
|
||||
var playWaiter sointu.CloserWaiter
|
||||
if *play {
|
||||
var err error
|
||||
audioContext, err = oto.NewContext()
|
||||
@ -45,7 +60,6 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "could not acquire oto AudioContext: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer audioContext.Close()
|
||||
}
|
||||
process := func(filename string) error {
|
||||
output := func(extension string, contents []byte) error {
|
||||
@ -88,28 +102,15 @@ func main() {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
synth, err := bridge.Synth(song.Patch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create synth based on the patch: %v", err)
|
||||
}
|
||||
if !*unreleased {
|
||||
for i := 0; i < 32; i++ {
|
||||
synth.Release(i)
|
||||
}
|
||||
}
|
||||
buffer, _, err := sointu.Play(synth, song) // render the song to calculate its length
|
||||
buffer, err := sointu.Play(cmd.Synthers[*syntherInt], song, nil) // render the song to calculate its length
|
||||
if err != nil {
|
||||
return fmt.Errorf("sointu.Play failed: %v", err)
|
||||
}
|
||||
if *play {
|
||||
output := audioContext.Output()
|
||||
defer output.Close()
|
||||
if err := output.WriteAudio(buffer); err != nil {
|
||||
return fmt.Errorf("error playing: %v", err)
|
||||
}
|
||||
playWaiter = audioContext.Play(buffer.Source())
|
||||
}
|
||||
if *rawOut {
|
||||
raw, err := sointu.Raw(buffer, *pcm)
|
||||
raw, err := buffer.Raw(*pcm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate .raw file: %v", err)
|
||||
}
|
||||
@ -118,7 +119,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
if *wavOut {
|
||||
wav, err := sointu.Wav(buffer, *pcm)
|
||||
wav, err := buffer.Wav(*pcm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate .wav file: %v", err)
|
||||
}
|
||||
@ -126,6 +127,9 @@ func main() {
|
||||
return fmt.Errorf("error outputting .wav file: %v", err)
|
||||
}
|
||||
}
|
||||
if *play {
|
||||
playWaiter.Wait()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
retval := 0
|
||||
|
||||
@ -3,31 +3,106 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
|
||||
"gioui.org/app"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/cmd"
|
||||
"github.com/vsariola/sointu/oto"
|
||||
"github.com/vsariola/sointu/rpc"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/tracker/gioui"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
|
||||
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
|
||||
var defaultMidiInput = flag.String("midi-input", "", "connect MIDI input to matching device name prefix")
|
||||
|
||||
func main() {
|
||||
syncAddress := flag.String("address", "", "remote RPC server where to send sync data")
|
||||
flag.Parse()
|
||||
var f *os.File
|
||||
if *cpuprofile != "" {
|
||||
var err error
|
||||
f, err = os.Create(*cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("could not start CPU profile: ", err)
|
||||
}
|
||||
}
|
||||
audioContext, err := oto.NewContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer audioContext.Close()
|
||||
var syncChannel chan<- []float32
|
||||
if *syncAddress != "" {
|
||||
syncChannel, err = rpc.Sender(*syncAddress)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
recoveryFile := ""
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
recoveryFile = filepath.Join(configDir, "sointu", "recovery", "sointu-track-recovery.json")
|
||||
}
|
||||
synthService := vm.SynthService{}
|
||||
gioui.Main(audioContext, synthService, syncChannel)
|
||||
broker := tracker.NewBroker()
|
||||
midiContext := cmd.NewMidiContext(broker)
|
||||
defer midiContext.Close()
|
||||
model := tracker.NewModel(broker, cmd.Synthers, midiContext, recoveryFile)
|
||||
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
||||
if isFlagPassed("midi-input") {
|
||||
for i, s := range model.MIDI().Input().Values {
|
||||
if strings.HasPrefix(s, *defaultMidiInput) {
|
||||
model.MIDI().Input().SetValue(i)
|
||||
goto found
|
||||
}
|
||||
}
|
||||
model.Alerts().Add(fmt.Sprintf("MIDI command line argument passed, but device with given prefix not found: %s", *defaultMidiInput), tracker.Error)
|
||||
found:
|
||||
}
|
||||
if a := flag.Args(); len(a) > 0 {
|
||||
f, err := os.Open(a[0])
|
||||
if err == nil {
|
||||
model.Song().Read(f)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
trackerUi := gioui.NewTracker(model)
|
||||
audioCloser := audioContext.Play(func(buf sointu.AudioBuffer) error {
|
||||
player.Process(buf, tracker.NullPlayerProcessContext{})
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
trackerUi.Main()
|
||||
audioCloser.Close()
|
||||
model.Close()
|
||||
if *cpuprofile != "" {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
}
|
||||
if *memprofile != "" {
|
||||
f, err := os.Create(*memprofile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create memory profile: ", err)
|
||||
}
|
||||
defer f.Close() // error handling omitted for example
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
log.Fatal("could not write memory profile: ", err)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
app.Main()
|
||||
}
|
||||
|
||||
func isFlagPassed(name string) bool {
|
||||
found := false
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
if f.Name == name {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
140
cmd/sointu-vsti/main.go
Normal file
140
cmd/sointu-vsti/main.go
Normal file
@ -0,0 +1,140 @@
|
||||
//go:build plugin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/cmd"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/tracker/gioui"
|
||||
"pipelined.dev/audio/vst2"
|
||||
)
|
||||
|
||||
type (
|
||||
VSTIProcessContext struct {
|
||||
events []vst2.MIDIEvent
|
||||
eventIndex int
|
||||
host vst2.Host
|
||||
}
|
||||
)
|
||||
|
||||
func (c *VSTIProcessContext) BPM() (bpm float64, ok bool) {
|
||||
timeInfo := c.host.GetTimeInfo(vst2.TempoValid)
|
||||
if timeInfo == nil || timeInfo.Flags&vst2.TempoValid == 0 || timeInfo.Tempo == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return timeInfo.Tempo, true
|
||||
}
|
||||
|
||||
func (c *VSTIProcessContext) SampleRate() (samplerate float64, ok bool) {
|
||||
timeInfo := c.host.GetTimeInfo(0)
|
||||
if timeInfo == nil || timeInfo.SampleRate == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return timeInfo.SampleRate, true
|
||||
}
|
||||
|
||||
func init() {
|
||||
var (
|
||||
version = int32(100)
|
||||
)
|
||||
vst2.PluginAllocator = func(h vst2.Host) (vst2.Plugin, vst2.Dispatcher) {
|
||||
recoveryFile := ""
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
randBytes := make([]byte, 16)
|
||||
rand.Read(randBytes)
|
||||
recoveryFile = filepath.Join(configDir, "sointu", "recovery", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes)+".json")
|
||||
}
|
||||
broker := tracker.NewBroker()
|
||||
model := tracker.NewModel(broker, cmd.Synthers, cmd.NewMidiContext(broker), recoveryFile)
|
||||
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
||||
|
||||
t := gioui.NewTracker(model)
|
||||
model.Play().TrackerHidden().SetValue(true)
|
||||
// since the VST is usually working without any regard for the tracks
|
||||
// until recording, disable the Instrument-Track linking by default
|
||||
// because it might just confuse the user why instrument cannot be
|
||||
// swapped/added etc.
|
||||
model.Track().LinkInstrument().SetValue(false)
|
||||
go t.Main()
|
||||
context := &VSTIProcessContext{host: h}
|
||||
buf := make(sointu.AudioBuffer, 1024)
|
||||
var totalFrames int64 = 0
|
||||
start := time.Now()
|
||||
return vst2.Plugin{
|
||||
UniqueID: [4]byte{'S', 'n', 't', 'u'},
|
||||
Version: version,
|
||||
InputChannels: 0,
|
||||
OutputChannels: 2,
|
||||
Name: "Sointu",
|
||||
Vendor: "vsariola/sointu",
|
||||
Category: vst2.PluginCategorySynth,
|
||||
Flags: vst2.PluginIsSynth,
|
||||
ProcessFloatFunc: func(in, out vst2.FloatBuffer) {
|
||||
if time.Since(start) > 2*time.Second { // limit the rate we query the samplerate from the host and send alerts
|
||||
if s, ok := context.SampleRate(); ok && math.Abs(float64(s-44100.0)) > 1e-6 {
|
||||
player.SendAlert("WrongSampleRate", fmt.Sprintf("VSTi host sample rate is %.0f Hz; Sointu supports 44100 Hz only", s), tracker.Error)
|
||||
}
|
||||
start = time.Now()
|
||||
}
|
||||
left := out.Channel(0)
|
||||
right := out.Channel(1)
|
||||
if len(buf) < out.Frames {
|
||||
buf = append(buf, make(sointu.AudioBuffer, out.Frames-len(buf))...)
|
||||
}
|
||||
buf = buf[:out.Frames]
|
||||
player.Process(buf, context)
|
||||
for i := 0; i < out.Frames; i++ {
|
||||
left[i], right[i] = buf[i][0], buf[i][1]
|
||||
}
|
||||
totalFrames += int64(out.Frames)
|
||||
},
|
||||
}, vst2.Dispatcher{
|
||||
CanDoFunc: func(pcds vst2.PluginCanDoString) vst2.CanDoResponse {
|
||||
switch pcds {
|
||||
case vst2.PluginCanReceiveEvents, vst2.PluginCanReceiveMIDIEvent, vst2.PluginCanReceiveTimeInfo:
|
||||
return vst2.YesCanDo
|
||||
}
|
||||
return vst2.NoCanDo
|
||||
},
|
||||
ProcessEventsFunc: func(events *vst2.EventsPtr) {
|
||||
for i := 0; i < events.NumEvents(); i++ {
|
||||
switch ev := events.Event(i).(type) {
|
||||
case *vst2.MIDIEvent:
|
||||
if (ev.Data[0] >= 0x80 && ev.Data[0] <= 0x9F) || (ev.Data[0] >= 0xB0 && ev.Data[0] <= 0xBF) {
|
||||
player.EmitMIDIMsg(&tracker.MIDIMessage{Timestamp: int64(ev.DeltaFrames) + totalFrames, Data: ev.Data, Source: &context})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
CloseFunc: func() {
|
||||
tracker.TrySend(broker.CloseGUI, struct{}{})
|
||||
model.Close()
|
||||
tracker.TimeoutReceive(broker.FinishedGUI, 3*time.Second)
|
||||
},
|
||||
GetChunkFunc: func(isPreset bool) []byte {
|
||||
retChn := make(chan []byte)
|
||||
|
||||
if !tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { retChn <- t.History().MarshalRecovery() }}) {
|
||||
return nil
|
||||
}
|
||||
ret, _ := tracker.TimeoutReceive(retChn, 5*time.Second) // ret will be nil if timeout or channel closed
|
||||
return ret
|
||||
},
|
||||
SetChunkFunc: func(data []byte, isPreset bool) {
|
||||
tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { t.History().UnmarshalRecovery(data) }})
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func main() {}
|
||||
8
cmd/synthers.go
Normal file
8
cmd/synthers.go
Normal file
@ -0,0 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
var Synthers = []sointu.Synther{vm.GoSynther{}}
|
||||
11
cmd/synthers_native.go
Normal file
11
cmd/synthers_native.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build native
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/vsariola/sointu/vm/compiler/bridge"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Synthers = append(Synthers, bridge.NativeSynther{})
|
||||
}
|
||||
1
examples/CMakeLists.txt
Normal file
1
examples/CMakeLists.txt
Normal file
@ -0,0 +1 @@
|
||||
add_subdirectory(code)
|
||||
68
examples/code/C/CMakeLists.txt
Normal file
68
examples/code/C/CMakeLists.txt
Normal file
@ -0,0 +1,68 @@
|
||||
# this fixes a bug in creating a static library from asm, similar to
|
||||
# https://discourse.cmake.org/t/building-lib-file-from-asm-cmake-bug/1959
|
||||
# but for NASM
|
||||
if(MSVC)
|
||||
set(CMAKE_ASM_NASM_CREATE_STATIC_LIBRARY "<CMAKE_AR> /OUT:<TARGET> <LINK_FLAGS> <OBJECTS>")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
COMMAND
|
||||
${compilecmd} -arch=${arch} -o physics_girl_st.asm "${PROJECT_SOURCE_DIR}/examples/patches/physics_girl_st.yml"
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
DEPENDS
|
||||
"${PROJECT_SOURCE_DIR}/examples/patches/physics_girl_st.yml"
|
||||
${compilecmd}
|
||||
OUTPUT
|
||||
physics_girl_st.asm
|
||||
physics_girl_st.h
|
||||
physics_girl_st.inc
|
||||
COMMENT
|
||||
"Compiling ${PROJECT_SOURCE_DIR}/examples/patches/physics-girl-st.yml..."
|
||||
)
|
||||
|
||||
add_library(physics_girl_st physics_girl_st.asm)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(cplay-winmm
|
||||
cplay.windows.winmm.c
|
||||
physics_girl_st.h
|
||||
)
|
||||
target_link_libraries(cplay-winmm PRIVATE winmm)
|
||||
target_link_libraries(cplay-winmm PRIVATE physics_girl_st)
|
||||
target_include_directories(cplay-winmm PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
add_dependencies(examples cplay-winmm)
|
||||
|
||||
add_executable(cplay-directsound
|
||||
cplay.windows.directsound.c
|
||||
physics_girl_st.h
|
||||
)
|
||||
target_link_libraries(cplay-directsound PRIVATE dsound ws2_32 ucrt)
|
||||
target_link_libraries(cplay-directsound PRIVATE physics_girl_st)
|
||||
target_include_directories(cplay-directsound PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
add_dependencies(examples cplay-directsound)
|
||||
elseif(UNIX)
|
||||
add_executable(cplay
|
||||
cplay.linux.c
|
||||
physics_girl_st.h
|
||||
)
|
||||
target_link_libraries(cplay PRIVATE asound pthread)
|
||||
target_link_options(cplay PRIVATE -z noexecstack -no-pie)
|
||||
target_link_libraries(cplay PRIVATE physics_girl_st)
|
||||
target_include_directories(cplay PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
add_dependencies(examples cplay)
|
||||
endif()
|
||||
|
||||
add_executable(cwav
|
||||
cwav.c
|
||||
physics_girl_st.h
|
||||
)
|
||||
if(WIN32)
|
||||
target_compile_definitions(cwav PRIVATE _CRT_SECURE_NO_WARNINGS)
|
||||
elseif(UNIX)
|
||||
target_link_options(cwav PRIVATE -z noexecstack -no-pie)
|
||||
endif()
|
||||
target_link_libraries(cwav PRIVATE physics_girl_st)
|
||||
target_include_directories(cwav PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
add_dependencies(examples cwav)
|
||||
58
examples/code/C/cplay.linux.c
Normal file
58
examples/code/C/cplay.linux.c
Normal file
@ -0,0 +1,58 @@
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "physics_girl_st.h"
|
||||
|
||||
static SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
|
||||
static snd_pcm_t *pcm_handle;
|
||||
static pthread_t render_thread, playback_thread;
|
||||
static uint32_t render_thread_handle, playback_thread_handle;
|
||||
|
||||
void play() {
|
||||
snd_pcm_writei(pcm_handle, sound_buffer, SU_LENGTH_IN_SAMPLES);
|
||||
}
|
||||
|
||||
int main(int argc, char **args) {
|
||||
// Unix does not have gm.dls, no need to ifdef and setup here.
|
||||
|
||||
// We render in the background while playing already.
|
||||
render_thread_handle = pthread_create(&render_thread, 0, (void * (*)(void *))su_render_song, sound_buffer);
|
||||
|
||||
// We can't start playing too early or the missing samples will be audible.
|
||||
sleep(2.);
|
||||
|
||||
// Play the track.
|
||||
snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
|
||||
snd_pcm_set_params(
|
||||
pcm_handle,
|
||||
#ifdef SU_SAMPLE_FLOAT
|
||||
SND_PCM_FORMAT_FLOAT,
|
||||
#else // SU_SAMPLE_FLOAT
|
||||
SND_PCM_FORMAT_S16_LE,
|
||||
#endif // SU_SAMPLE_FLOAT
|
||||
SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
SU_CHANNEL_COUNT,
|
||||
SU_SAMPLE_RATE,
|
||||
0,
|
||||
SU_LENGTH_IN_SAMPLES
|
||||
);
|
||||
playback_thread_handle = pthread_create(&playback_thread, 0, (void *(*)(void *))play, 0);
|
||||
|
||||
// This is for obtaining the playback time.
|
||||
snd_pcm_status_t *status;
|
||||
snd_pcm_status_malloc(&status);
|
||||
snd_htimestamp_t htime, htstart;
|
||||
snd_pcm_status(pcm_handle, status);
|
||||
snd_pcm_status_get_htstamp(status, &htstart);
|
||||
for(int sample; sample < SU_LENGTH_IN_SAMPLES; sample = (int)(((float)htime.tv_sec + (float)htime.tv_nsec * 1.e-9 - (float)htstart.tv_sec - (float)htstart.tv_nsec * 1.e-9) * SU_SAMPLE_RATE)) {
|
||||
snd_pcm_status(pcm_handle, status);
|
||||
snd_pcm_status_get_htstamp(status, &htime);
|
||||
printf("Sample: %d\n", sample);
|
||||
usleep(1000000 / 30);
|
||||
}
|
||||
snd_pcm_status_free(status);
|
||||
|
||||
return 0;
|
||||
}
|
||||
75
examples/code/C/cplay.windows.directsound.c
Normal file
75
examples/code/C/cplay.windows.directsound.c
Normal 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;
|
||||
}
|
||||
68
examples/code/C/cplay.windows.winmm.c
Normal file
68
examples/code/C/cplay.windows.winmm.c
Normal file
@ -0,0 +1,68 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include "physics_girl_st.h"
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_EXTRA_LEAN
|
||||
#include <Windows.h>
|
||||
#include "mmsystem.h"
|
||||
#include "mmreg.h"
|
||||
|
||||
SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
|
||||
HWAVEOUT wave_out_handle;
|
||||
WAVEFORMATEX wave_format = {
|
||||
#ifdef SU_SAMPLE_FLOAT
|
||||
WAVE_FORMAT_IEEE_FLOAT,
|
||||
#else
|
||||
WAVE_FORMAT_PCM,
|
||||
#endif
|
||||
SU_CHANNEL_COUNT,
|
||||
SU_SAMPLE_RATE,
|
||||
SU_SAMPLE_RATE * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
|
||||
SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
|
||||
SU_SAMPLE_SIZE*8,
|
||||
0
|
||||
};
|
||||
// If you want to loop the song:
|
||||
// 1) Change WHDR_PREPARED -> WHDR_BEGINLOOP | WHDR_ENDLOOP | WHDR_PREPARED
|
||||
// 2) The next field should then contain the number of loops (for example, 4)
|
||||
// 3) Remember also change the exit condition for main, e.g. if you plan to loop 4 times:
|
||||
// mmtime.u.sample != SU_LENGTH_IN_SAMPLES -> mmtime.u.sample != 4 * SU_LENGTH_IN_SAMPLES
|
||||
WAVEHDR wave_header = {
|
||||
(LPSTR)sound_buffer,
|
||||
SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
|
||||
0,
|
||||
0,
|
||||
WHDR_PREPARED,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
};
|
||||
MMTIME mmtime = {
|
||||
TIME_SAMPLES,
|
||||
0
|
||||
};
|
||||
|
||||
int main(int argc, char **args) {
|
||||
// Load gm.dls if necessary.
|
||||
#ifdef SU_LOAD_GMDLS
|
||||
su_load_gmdls();
|
||||
#endif // SU_LOAD_GMDLS
|
||||
|
||||
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)su_render_song, sound_buffer, 0, 0);
|
||||
|
||||
// We render in the background while playing already. Fortunately,
|
||||
// Windows is slow with the calls below, so we're not worried that
|
||||
// we don't have enough samples ready before the track starts.
|
||||
waveOutOpen(&wave_out_handle, WAVE_MAPPER, &wave_format, 0, 0, CALLBACK_NULL);
|
||||
waveOutWrite(wave_out_handle, &wave_header, sizeof(wave_header));
|
||||
|
||||
// We need to handle windows messages properly while playing, as waveOutWrite is async.
|
||||
for(MSG msg = {0}; mmtime.u.sample != SU_LENGTH_IN_SAMPLES; waveOutGetPosition(wave_out_handle, &mmtime, sizeof(MMTIME))) {
|
||||
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageA(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
72
examples/code/C/cwav.c
Normal file
72
examples/code/C/cwav.c
Normal 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;
|
||||
}
|
||||
34
examples/code/CMakeLists.txt
Normal file
34
examples/code/CMakeLists.txt
Normal 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
7
examples/code/Python/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.venv
|
||||
build
|
||||
dist
|
||||
__pycache__
|
||||
setup.py
|
||||
*.egg-info
|
||||
*.so
|
||||
18
examples/code/Python/README.md
Normal file
18
examples/code/Python/README.md
Normal 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.
|
||||
149
examples/code/Python/build.py
Normal file
149
examples/code/Python/build.py
Normal 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
132
examples/code/Python/poetry.lock
generated
Normal 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"
|
||||
27
examples/code/Python/pyproject.toml
Normal file
27
examples/code/Python/pyproject.toml
Normal 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"
|
||||
212
examples/code/Python/sointu.c
Normal file
212
examples/code/Python/sointu.c
Normal 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;
|
||||
}
|
||||
0
examples/code/Python/sointu_python/__init__.py
Normal file
0
examples/code/Python/sointu_python/__init__.py
Normal file
16
examples/code/Python/sointu_python/__main__.py
Normal file
16
examples/code/Python/sointu_python/__main__.py
Normal 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)
|
||||
58
examples/code/Python/sointu_python/sointu_python.spec
Normal file
58
examples/code/Python/sointu_python/sointu_python.spec
Normal 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)
|
||||
10
examples/code/asm/386/CMakeLists.txt
Normal file
10
examples/code/asm/386/CMakeLists.txt
Normal 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")
|
||||
81
examples/code/asm/386/asmplay.elf32.asm
Normal file
81
examples/code/asm/386/asmplay.elf32.asm
Normal 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
|
||||
120
examples/code/asm/386/asmplay.win32.asm
Normal file
120
examples/code/asm/386/asmplay.win32.asm
Normal 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
|
||||
91
examples/code/asm/386/asmwav.elf32.asm
Normal file
91
examples/code/asm/386/asmwav.elf32.asm
Normal 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
|
||||
102
examples/code/asm/386/asmwav.win32.asm
Normal file
102
examples/code/asm/386/asmwav.win32.asm
Normal 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
|
||||
58
examples/code/asm/CMakeLists.txt
Normal file
58
examples/code/asm/CMakeLists.txt
Normal 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)
|
||||
12
examples/code/asm/amd64/CMakeLists.txt
Normal file
12
examples/code/asm/amd64/CMakeLists.txt
Normal 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()
|
||||
81
examples/code/asm/amd64/asmplay.elf64.asm
Normal file
81
examples/code/asm/amd64/asmplay.elf64.asm
Normal 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
|
||||
91
examples/code/asm/amd64/asmwav.elf64.asm
Normal file
91
examples/code/asm/amd64/asmwav.elf64.asm
Normal 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
|
||||
14
examples/code/wasm/README.md
Normal file
14
examples/code/wasm/README.md
Normal 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
|
||||
```
|
||||
51
examples/code/wasm/index.html
Normal file
51
examples/code/wasm/index.html
Normal 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>
|
||||
BIN
examples/code/wasm/test_chords.wasm
Normal file
BIN
examples/code/wasm/test_chords.wasm
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_Dark.4ki
Normal file
BIN
examples/fourklang_instruments/BA_Dark.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_DarkChorus.4ki
Normal file
BIN
examples/fourklang_instruments/BA_DarkChorus.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_Deepness.4ki
Normal file
BIN
examples/fourklang_instruments/BA_Deepness.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_DirectPunchMS.4ki
Normal file
BIN
examples/fourklang_instruments/BA_DirectPunchMS.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_Mighty.4ki
Normal file
BIN
examples/fourklang_instruments/BA_Mighty.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_Mighty_Feedback.4ki
Normal file
BIN
examples/fourklang_instruments/BA_Mighty_Feedback.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_NotFromThisWorld.4ki
Normal file
BIN
examples/fourklang_instruments/BA_NotFromThisWorld.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_NotFromThisWorld2.4ki
Normal file
BIN
examples/fourklang_instruments/BA_NotFromThisWorld2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_SawBass.4ki
Normal file
BIN
examples/fourklang_instruments/BA_SawBass.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/BA_SawBassFlanger.4ki
Normal file
BIN
examples/fourklang_instruments/BA_SawBassFlanger.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/GA_RestInPeaceMS.4ki
Normal file
BIN
examples/fourklang_instruments/GA_RestInPeaceMS.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/KY_GarageOrgan.4ki
Normal file
BIN
examples/fourklang_instruments/KY_GarageOrgan.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/KY_GarageOrganChorus.4ki
Normal file
BIN
examples/fourklang_instruments/KY_GarageOrganChorus.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/KY_Lullaby.4ki
Normal file
BIN
examples/fourklang_instruments/KY_Lullaby.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/KY_Lullaby2.4ki
Normal file
BIN
examples/fourklang_instruments/KY_Lullaby2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/KY_Rhodes.4ki
Normal file
BIN
examples/fourklang_instruments/KY_Rhodes.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/LD_AlphaOmegaMS.4ki
Normal file
BIN
examples/fourklang_instruments/LD_AlphaOmegaMS.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/LD_Farscape.4ki
Normal file
BIN
examples/fourklang_instruments/LD_Farscape.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/LD_More&MoreMS.4ki
Normal file
BIN
examples/fourklang_instruments/LD_More&MoreMS.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/LD_Morpher.4ki
Normal file
BIN
examples/fourklang_instruments/LD_Morpher.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/LD_RestInPeaceMS.4ki
Normal file
BIN
examples/fourklang_instruments/LD_RestInPeaceMS.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/LD_Short&PunchyMS.4ki
Normal file
BIN
examples/fourklang_instruments/LD_Short&PunchyMS.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_Fairies.4ki
Normal file
BIN
examples/fourklang_instruments/PA_Fairies.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_Jarresque.4ki
Normal file
BIN
examples/fourklang_instruments/PA_Jarresque.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_JarresqueChorus.4ki
Normal file
BIN
examples/fourklang_instruments/PA_JarresqueChorus.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_LoFiChoir.4ki
Normal file
BIN
examples/fourklang_instruments/PA_LoFiChoir.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_LongPad.4ki
Normal file
BIN
examples/fourklang_instruments/PA_LongPad.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_Minorium.4ki
Normal file
BIN
examples/fourklang_instruments/PA_Minorium.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_Strangeland.4ki
Normal file
BIN
examples/fourklang_instruments/PA_Strangeland.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_StrangelandChorus.4ki
Normal file
BIN
examples/fourklang_instruments/PA_StrangelandChorus.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/PA_SynastasiaMS.4ki
Normal file
BIN
examples/fourklang_instruments/PA_SynastasiaMS.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/SY_RandomArp.4ki
Normal file
BIN
examples/fourklang_instruments/SY_RandomArp.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/SY_RandomArpFlanger.4ki
Normal file
BIN
examples/fourklang_instruments/SY_RandomArpFlanger.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/airy.4ki
Normal file
BIN
examples/fourklang_instruments/airy.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/basedrum.4ki
Normal file
BIN
examples/fourklang_instruments/basedrum.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/basedrum2.4ki
Normal file
BIN
examples/fourklang_instruments/basedrum2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/basedrum3.4ki
Normal file
BIN
examples/fourklang_instruments/basedrum3.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/basedrum4.4ki
Normal file
BIN
examples/fourklang_instruments/basedrum4.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/bass.4ki
Normal file
BIN
examples/fourklang_instruments/bass.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/bass2.4ki
Normal file
BIN
examples/fourklang_instruments/bass2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/clap.4ki
Normal file
BIN
examples/fourklang_instruments/clap.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/guitar.4ki
Normal file
BIN
examples/fourklang_instruments/guitar.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/guitar2.4ki
Normal file
BIN
examples/fourklang_instruments/guitar2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/hihat.4ki
Normal file
BIN
examples/fourklang_instruments/hihat.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/hihat2.4ki
Normal file
BIN
examples/fourklang_instruments/hihat2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/pad.4ki
Normal file
BIN
examples/fourklang_instruments/pad.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/pad2.4ki
Normal file
BIN
examples/fourklang_instruments/pad2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/piano.4ki
Normal file
BIN
examples/fourklang_instruments/piano.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/piano2.4ki
Normal file
BIN
examples/fourklang_instruments/piano2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/rimshot.4ki
Normal file
BIN
examples/fourklang_instruments/rimshot.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/snare.4ki
Normal file
BIN
examples/fourklang_instruments/snare.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/snare2.4ki
Normal file
BIN
examples/fourklang_instruments/snare2.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/snare3.4ki
Normal file
BIN
examples/fourklang_instruments/snare3.4ki
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user