mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-12 17:14:43 -04:00
Compare commits
217 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
163
.github/workflows/binaries.yml
vendored
Normal file
163
.github/workflows/binaries.yml
vendored
Normal file
@ -0,0 +1,163 @@
|
||||
name: Binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
name: Create release
|
||||
runs-on: ubuntu-latest
|
||||
# Note this. We are going to use that in further jobs.
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref_name }}
|
||||
body_path: CHANGELOG.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
binaries:
|
||||
needs: create_release # we need to know the upload URL
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-track.exe
|
||||
params: -ldflags -H=windowsgui cmd/sointu-track/main.go
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-compile.exe
|
||||
params: cmd/sointu-compile/main.go
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-track-native.exe
|
||||
params: -ldflags -H=windowsgui -tags=native cmd/sointu-track/main.go
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-vsti.dll
|
||||
params: -buildmode=c-shared -tags=plugin ./cmd/sointu-vsti/
|
||||
- os: windows-latest
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
output: sointu-vsti-native.dll
|
||||
params: -buildmode=c-shared -tags="plugin,native" ./cmd/sointu-vsti/
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-track
|
||||
params: cmd/sointu-track/main.go
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-compile
|
||||
params: cmd/sointu-compile/main.go
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-track-native
|
||||
params: -tags=native cmd/sointu-track/main.go
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-vsti.so
|
||||
params: -buildmode=c-shared -tags=plugin ./cmd/sointu-vsti/
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
output: sointu-vsti-native.so
|
||||
params: -buildmode=c-shared -tags="plugin,native" ./cmd/sointu-vsti/
|
||||
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
|
||||
- os: macos-latest
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-track
|
||||
params: cmd/sointu-track/main.go
|
||||
- os: macos-latest
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-compile
|
||||
params: cmd/sointu-compile/main.go
|
||||
- os: macos-12 # this is intel still
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
output: sointu-track-native
|
||||
params: -tags=native cmd/sointu-track/main.go
|
||||
steps:
|
||||
- uses: benjlevesque/short-sha@v3.0
|
||||
id: short-sha
|
||||
with:
|
||||
length: 7
|
||||
- uses: lukka/get-cmake@latest
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
|
||||
with:
|
||||
go-version: '>=1.21.0'
|
||||
- uses: ilammy/setup-nasm@v1.5.1
|
||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: ${{ matrix.config.packages }}
|
||||
version: 1.0
|
||||
if: runner.os == 'Linux'
|
||||
- name: Build library
|
||||
env:
|
||||
ASM_NASM: ${{ matrix.config.asmnasm }}
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -GNinja ..
|
||||
ninja sointu
|
||||
- name: Build binary
|
||||
run: |
|
||||
go build -o ${{ matrix.config.output }} ${{ matrix.config.params }}
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ runner.os }}-${{ steps.short-sha.outputs.sha }}-${{ matrix.config.output }}
|
||||
path: ${{ matrix.config.output }}
|
||||
upload_release_asset:
|
||||
needs: [create_release, binaries]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: Windows
|
||||
- os: Linux
|
||||
- os: macOS
|
||||
steps:
|
||||
- uses: benjlevesque/short-sha@v2.2
|
||||
id: short-sha
|
||||
with:
|
||||
length: 7
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: ${{ matrix.config.os }}-${{ steps.short-sha.outputs.sha }}-*
|
||||
merge-multiple: true
|
||||
path: sointu-${{ matrix.config.os }}
|
||||
- name: Zip binaries
|
||||
run: |
|
||||
zip ./sointu-${{ matrix.config.os }}.zip sointu-${{ matrix.config.os }}/*
|
||||
- name: Upload release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_name: sointu-${{ matrix.config.os }}.zip
|
||||
asset_path: ./sointu-${{ matrix.config.os }}.zip
|
||||
asset_content_type: application/octet-stream
|
||||
33
.github/workflows/tests.yml
vendored
33
.github/workflows/tests.yml
vendored
@ -19,19 +19,13 @@ jobs:
|
||||
config:
|
||||
- os: ubuntu-latest
|
||||
asmnasm: /home/runner/nasm/nasm
|
||||
cmakeflags: -GNinja
|
||||
maker: ninja
|
||||
gotests: yes
|
||||
cgo_ldflags:
|
||||
- os: windows-latest
|
||||
cmakeflags: -GNinja
|
||||
maker: ninja
|
||||
asmnasm: C:\Users\runneradmin\nasm\nasm
|
||||
gotests: yes
|
||||
cgo_ldflags:
|
||||
- os: macos-latest
|
||||
cmakeflags: -GNinja
|
||||
maker: ninja
|
||||
- os: macos-12 # this is intel still
|
||||
asmnasm: /Users/runner/nasm/nasm
|
||||
gotests: yes
|
||||
cgo_ldflags: # -Wl,-no_pie
|
||||
@ -40,31 +34,30 @@ jobs:
|
||||
# than let the tests fail because of this.
|
||||
# TODO: win32 builds didn't quite work out, complains gcc broken
|
||||
steps:
|
||||
- uses: lukka/get-cmake@v3.18.3
|
||||
- uses: vsariola/setup-wabt@v1.0.1
|
||||
- uses: lukka/get-cmake@latest
|
||||
- uses: vsariola/setup-wabt@v1.0.2
|
||||
with:
|
||||
version: 1.0.20
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-node@v2
|
||||
version: 1.0.29
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
|
||||
with:
|
||||
go-version: '>=1.21.0'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '15'
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ilammy/setup-nasm@v1.2.0
|
||||
- name: Install libasound2-dev # sointu-cli has alsa as dependency for playing sound and
|
||||
if: ${{ matrix.config.os == 'ubuntu-latest' }} # ubuntu was complaining about "Package alsa was not found in the pkg-config search path.",
|
||||
run: sudo apt install libasound2-dev # leading to tests failing. This fixes that.
|
||||
- uses: ilammy/setup-nasm@v1.5.1
|
||||
- name: Run ctest
|
||||
env:
|
||||
ASM_NASM: ${{ matrix.config.asmnasm }}
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ${{ matrix.config.cmakeflags }} ..
|
||||
${{ matrix.config.maker }}
|
||||
cmake -GNinja ..
|
||||
ninja tests/all sointu
|
||||
ctest --output-on-failure
|
||||
- name: Run go test
|
||||
if: ${{ matrix.config.gotests == 'yes' }}
|
||||
env:
|
||||
CGO_LDFLAGS: ${{ matrix.config.cgo_ldflags }}
|
||||
run: |
|
||||
go test ./vm ./vm/compiler/bridge ./vm/compiler ./oto
|
||||
go test ./vm ./vm/compiler/bridge ./vm/compiler
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -29,3 +29,8 @@ out/
|
||||
.cache/
|
||||
actual_output/
|
||||
**/__debug_bin
|
||||
*.exe
|
||||
*.dll
|
||||
|
||||
**/testdata/fuzz/
|
||||
.DS_Store
|
||||
522
4klang.go
Normal file
522
4klang.go
Normal file
@ -0,0 +1,522 @@
|
||||
package sointu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Read4klangPatch reads a 4klang patch (a file usually with .4kp extension)
|
||||
// from r and returns a Patch, making best attempt to convert 4klang file to a
|
||||
// sointu Patch. It returns an error if the file is malformed or if the 4kp file
|
||||
// version is not supported.
|
||||
func Read4klangPatch(r io.Reader) (patch Patch, err error) {
|
||||
var versionTag uint32
|
||||
var version int
|
||||
var polyphonyUint32 uint32
|
||||
var instrumentNames [_4KLANG_MAX_INSTRS]string
|
||||
patch = make(Patch, 0)
|
||||
if err := binary.Read(r, binary.LittleEndian, &versionTag); err != nil {
|
||||
return nil, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
var ok bool
|
||||
if version, ok = _4klangVersionTags[versionTag]; !ok {
|
||||
return nil, fmt.Errorf("unknown 4klang version tag: %d", versionTag)
|
||||
}
|
||||
if err := binary.Read(r, binary.LittleEndian, &polyphonyUint32); err != nil {
|
||||
return nil, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
for i := range instrumentNames {
|
||||
instrumentNames[i], err = read4klangName(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read4klangName: %w", err)
|
||||
}
|
||||
}
|
||||
m := make(_4klangTargetMap)
|
||||
id := 1
|
||||
for instrIndex := 0; instrIndex < _4KLANG_MAX_INSTRS; instrIndex++ {
|
||||
var units []Unit
|
||||
if units, err = read4klangUnits(r, version, instrIndex, m, &id); err != nil {
|
||||
return nil, fmt.Errorf("read4klangUnits: %w", err)
|
||||
}
|
||||
if len(units) > 0 {
|
||||
patch = append(patch, Instrument{Name: instrumentNames[instrIndex], NumVoices: 1, Units: units})
|
||||
}
|
||||
}
|
||||
var units []Unit
|
||||
if units, err = read4klangUnits(r, version, _4KLANG_MAX_INSTRS, m, &id); err != nil {
|
||||
return nil, fmt.Errorf("read4klangUnits: %w", err)
|
||||
}
|
||||
if len(units) > 0 {
|
||||
patch = append(patch, Instrument{Name: "Global", NumVoices: 1, Units: units})
|
||||
}
|
||||
for i, instr := range patch {
|
||||
fix4klangTargets(i, instr, m)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Read4klangInstrument reads a 4klang instrument (a file usually with .4ki
|
||||
// extension) from r and returns an Instrument, making best attempt to convert
|
||||
// 4ki file to a sointu Instrument. It returns an error if the file is malformed
|
||||
// or if the 4ki file version is not supported.
|
||||
func Read4klangInstrument(r io.Reader) (instr Instrument, err error) {
|
||||
var versionTag uint32
|
||||
var version int
|
||||
var name string
|
||||
if err := binary.Read(r, binary.LittleEndian, &versionTag); err != nil {
|
||||
return Instrument{}, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
var ok bool
|
||||
if version, ok = _4klangVersionTags[versionTag]; !ok {
|
||||
return Instrument{}, fmt.Errorf("unknown 4klang version tag: %d", versionTag)
|
||||
}
|
||||
if name, err = read4klangName(r); err != nil {
|
||||
return Instrument{}, fmt.Errorf("read4klangName: %w", err)
|
||||
}
|
||||
var units []Unit
|
||||
id := 1
|
||||
m := make(_4klangTargetMap)
|
||||
if units, err = read4klangUnits(r, version, 0, m, &id); err != nil {
|
||||
return Instrument{}, fmt.Errorf("read4klangUnits: %w", err)
|
||||
}
|
||||
ret := Instrument{Name: name, NumVoices: 1, Units: units}
|
||||
fix4klangTargets(0, ret, m)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type (
|
||||
_4klangStackUnit struct {
|
||||
stack, unit int
|
||||
}
|
||||
|
||||
_4klangTargetMap map[_4klangStackUnit]int
|
||||
|
||||
_4klangPorts struct {
|
||||
UnitType string
|
||||
PortName [8]string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
_4KLANG_MAX_INSTRS = 16
|
||||
_4KLANG_MAX_UNITS = 64
|
||||
_4KLANG_MAX_SLOTS = 16
|
||||
_4KLANG_MAX_NAME_LEN = 64
|
||||
)
|
||||
|
||||
var (
|
||||
_4klangVersionTags map[uint32]int = map[uint32]int{
|
||||
0x31316b34: 11, // 4k11
|
||||
0x32316b34: 12, // 4k12
|
||||
0x33316b34: 13, // 4k13
|
||||
0x34316b34: 14, // 4k14
|
||||
}
|
||||
|
||||
_4klangDelays []int = []int{ // these are the numerators, if denominator is 48, fraction of beat time
|
||||
4, // 0 = 4.0f * (1.0f/32.0f) * (2.0f/3.0f)
|
||||
6, // 1 = 4.0f * (1.0f/32.0f),
|
||||
9, // 2 = 4.0f * (1.0f/32.0f) * (3.0f/2.0f),
|
||||
8, // 3 = 4.0f * (1.0f/16.0f) * (2.0f/3.0f),
|
||||
12, // 4 = 4.0f * (1.0f/16.0f),
|
||||
18, // 5 = 4.0f * (1.0f/16.0f) * (3.0f/2.0f),
|
||||
16, // 6 = 4.0f * (1.0f/8.0f) * (2.0f/3.0f),
|
||||
24, // 7 = 4.0f * (1.0f/8.0f),
|
||||
36, // 8 = 4.0f * (1.0f/8.0f) * (3.0f/2.0f),
|
||||
32, // 9 = 4.0f * (1.0f/4.0f) * (2.0f/3.0f),
|
||||
48, // 10 = 4.0f * (1.0f/4.0f),
|
||||
72, // 11 = 4.0f * (1.0f/4.0f) * (3.0f/2.0f),
|
||||
64, // 12 = 4.0f * (1.0f/2.0f) * (2.0f/3.0f),
|
||||
96, // 13 = 4.0f * (1.0f/2.0f),
|
||||
144, // 14 = 4.0f * (1.0f/2.0f) * (3.0f/2.0f),
|
||||
128, // 15 = 4.0f * (1.0f) * (2.0f/3.0f),
|
||||
192, // 16 = 4.0f * (1.0f),
|
||||
288, // 17 = 4.0f * (1.0f) * (3.0f/2.0f),
|
||||
256, // 18 = 4.0f * (2.0f) * (2.0f/3.0f),
|
||||
384, // 19 = 4.0f * (2.0f),
|
||||
576, // 20 = 4.0f * (2.0f) * (3.0f/2.0f),
|
||||
72, // 21 = 4.0f * (3.0f/8.0f),
|
||||
120, // 22 = 4.0f * (5.0f/8.0f),
|
||||
168, // 23 = 4.0f * (7.0f/8.0f),
|
||||
216, // 24 = 4.0f * (9.0f/8.0f),
|
||||
264, // 25 = 4.0f * (11.0f/8.0f),
|
||||
312, // 26 = 4.0f * (13.0f/8.0f),
|
||||
360, // 27 = 4.0f * (15.0f/8.0f),
|
||||
144, // 28 = 4.0f * (3.0f/4.0f),
|
||||
240, // 29 = 4.0f * (5.0f/4.0f),
|
||||
336, // 30 = 4.0f * (7.0f/4.0f),
|
||||
288, // 31 = 4.0f * (3.0f/2.0f),
|
||||
288, // 32 = 4.0f * (3.0f/2.0f),
|
||||
}
|
||||
|
||||
_4klangUnitPorts []_4klangPorts = []_4klangPorts{
|
||||
{"", [8]string{"", "", "", "", "", "", "", ""}},
|
||||
{"envelope", [8]string{"", "", "gain", "attack", "decay", "", "release", ""}},
|
||||
{"oscillator", [8]string{"", "transpose", "detune", "", "phase", "color", "shape", "gain"}},
|
||||
{"filter", [8]string{"", "", "", "", "frequency", "resonance", "", ""}},
|
||||
{"envelope", [8]string{"", "", "drive", "frequency", "", "", "", ""}},
|
||||
{"delay", [8]string{"pregain", "feedback", "dry", "damp", "", "", "", ""}},
|
||||
{"", [8]string{"", "", "", "", "", "", "", ""}},
|
||||
{"", [8]string{"", "", "", "", "", "", "", ""}},
|
||||
{"pan", [8]string{"panning", "", "", "", "", "", "", ""}},
|
||||
{"outaux", [8]string{"auxgain", "outgain", "", "", "", "", "", ""}},
|
||||
{"", [8]string{"", "", "", "", "", "", "", ""}},
|
||||
{"load", [8]string{"value", "", "", "", "", "", "", ""}},
|
||||
}
|
||||
)
|
||||
|
||||
func read4klangName(r io.Reader) (string, error) {
|
||||
var name [_4KLANG_MAX_NAME_LEN]byte
|
||||
if err := binary.Read(r, binary.LittleEndian, &name); err != nil {
|
||||
return "", fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
n := bytes.IndexByte(name[:], 0)
|
||||
if n == -1 {
|
||||
n = _4KLANG_MAX_NAME_LEN
|
||||
}
|
||||
return string(name[:n]), nil
|
||||
}
|
||||
|
||||
func read4klangUnits(r io.Reader, version, instrIndex int, m _4klangTargetMap, id *int) (units []Unit, err error) {
|
||||
numUnits := _4KLANG_MAX_UNITS
|
||||
if version <= 13 {
|
||||
numUnits = 32
|
||||
}
|
||||
units = make([]Unit, 0, numUnits)
|
||||
for unitIndex := 0; unitIndex < numUnits; unitIndex++ {
|
||||
var u []Unit
|
||||
if u, err = read4klangUnit(r, version); err != nil {
|
||||
return nil, fmt.Errorf("read4klangUnit: %w", err)
|
||||
}
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
m[_4klangStackUnit{instrIndex, unitIndex}] = *id
|
||||
for i := range u {
|
||||
u[i].ID = *id
|
||||
*id++
|
||||
}
|
||||
units = append(units, u...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func read4klangUnit(r io.Reader, version int) ([]Unit, error) {
|
||||
var unitType byte
|
||||
if err := binary.Read(r, binary.LittleEndian, &unitType); err != nil {
|
||||
return nil, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
var vals [15]byte
|
||||
if err := binary.Read(r, binary.LittleEndian, &vals); err != nil {
|
||||
return nil, fmt.Errorf("binary.Read: %w", err)
|
||||
}
|
||||
if version <= 13 {
|
||||
// versions <= 13 had 16 unused slots for each unit
|
||||
if written, err := io.CopyN(io.Discard, r, 16); err != nil || written < 16 {
|
||||
return nil, fmt.Errorf("io.CopyN: %w", err)
|
||||
}
|
||||
}
|
||||
switch unitType {
|
||||
case 1:
|
||||
return read4klangENV(vals, version), nil
|
||||
case 2:
|
||||
return read4klangVCO(vals, version), nil
|
||||
case 3:
|
||||
return read4klangVCF(vals, version), nil
|
||||
case 4:
|
||||
return read4klangDST(vals, version), nil
|
||||
case 5:
|
||||
return read4klangDLL(vals, version), nil
|
||||
case 6:
|
||||
return read4klangFOP(vals, version), nil
|
||||
case 7:
|
||||
return read4klangFST(vals, version), nil
|
||||
case 8:
|
||||
return read4klangPAN(vals, version), nil
|
||||
case 9:
|
||||
return read4klangOUT(vals, version), nil
|
||||
case 10:
|
||||
return read4klangACC(vals, version), nil
|
||||
case 11:
|
||||
return read4klangFLD(vals, version), nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func read4klangENV(vals [15]byte, version int) []Unit {
|
||||
return []Unit{{
|
||||
Type: "envelope",
|
||||
Parameters: map[string]int{
|
||||
"stereo": 0,
|
||||
"attack": int(vals[0]),
|
||||
"decay": int(vals[1]),
|
||||
"sustain": int(vals[2]),
|
||||
"release": int(vals[3]),
|
||||
"gain": int(vals[4]),
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangVCO(vals [15]byte, version int) []Unit {
|
||||
v := vals[:8]
|
||||
var transpose, detune, phase, color, gate, shape, gain, flags, stereo, typ, lfo int
|
||||
transpose, v = int(v[0]), v[1:]
|
||||
detune, v = int(v[0]), v[1:]
|
||||
phase, v = int(v[0]), v[1:]
|
||||
if version <= 11 {
|
||||
gate = 0x55
|
||||
} else {
|
||||
gate, v = int(v[0]), v[1:]
|
||||
}
|
||||
color, v = int(v[0]), v[1:]
|
||||
shape, v = int(v[0]), v[1:]
|
||||
gain, v = int(v[0]), v[1:]
|
||||
flags, v = int(v[0]), v[1:]
|
||||
if flags&0x10 == 0x10 {
|
||||
lfo = 1
|
||||
}
|
||||
if flags&0x40 == 0x40 {
|
||||
stereo = 1
|
||||
}
|
||||
switch {
|
||||
case flags&0x01 == 0x01: // Sine
|
||||
typ = Sine
|
||||
if version <= 13 {
|
||||
color = 128
|
||||
}
|
||||
case flags&0x02 == 0x02: // Trisaw
|
||||
typ = Trisaw
|
||||
case flags&0x04 == 0x04: // Pulse
|
||||
typ = Pulse
|
||||
case flags&0x08 == 0x08: // Noise is handled differently in sointu
|
||||
return []Unit{{
|
||||
Type: "noise",
|
||||
Parameters: map[string]int{
|
||||
"stereo": stereo,
|
||||
"shape": shape,
|
||||
"gain": gain,
|
||||
},
|
||||
}}
|
||||
case flags&0x20 == 0x20: // Gate
|
||||
color = gate
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "oscillator",
|
||||
Parameters: map[string]int{
|
||||
"stereo": stereo,
|
||||
"transpose": transpose,
|
||||
"detune": detune,
|
||||
"phase": phase,
|
||||
"color": color,
|
||||
"shape": shape,
|
||||
"gain": gain,
|
||||
"type": typ,
|
||||
"lfo": lfo,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangVCF(vals [15]byte, version int) []Unit {
|
||||
flags := vals[2]
|
||||
var stereo, lowpass, bandpass, highpass, neghighpass int
|
||||
if flags&0x01 == 0x01 {
|
||||
lowpass = 1
|
||||
}
|
||||
if flags&0x02 == 0x02 {
|
||||
highpass = 1
|
||||
}
|
||||
if flags&0x04 == 0x04 {
|
||||
bandpass = 1
|
||||
}
|
||||
if flags&0x08 == 0x08 {
|
||||
lowpass = 1
|
||||
neghighpass = 1
|
||||
}
|
||||
if flags&0x10 == 0x10 {
|
||||
stereo = 1
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "filter",
|
||||
Parameters: map[string]int{
|
||||
"stereo": stereo,
|
||||
"frequency": int(vals[0]),
|
||||
"resonance": int(vals[1]),
|
||||
"lowpass": lowpass,
|
||||
"bandpass": bandpass,
|
||||
"highpass": highpass,
|
||||
"negbandpass": 0,
|
||||
"neghighpass": neghighpass,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func read4klangDST(vals [15]byte, version int) []Unit {
|
||||
return []Unit{
|
||||
{Type: "distort", Parameters: map[string]int{"drive": int(vals[0]), "stereo": int(vals[2])}},
|
||||
{Type: "hold", Parameters: map[string]int{"holdfreq": int(vals[1]), "stereo": int(vals[2])}},
|
||||
}
|
||||
}
|
||||
|
||||
func read4klangDLL(vals [15]byte, version int) []Unit {
|
||||
var delaytimes []int
|
||||
var notetracking int
|
||||
if vals[11] > 0 {
|
||||
if vals[10] > 0 { // left reverb
|
||||
delaytimes = []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618}
|
||||
} else { // right reverb
|
||||
delaytimes = []int{1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642}
|
||||
}
|
||||
} else {
|
||||
synctype := vals[9]
|
||||
switch synctype {
|
||||
case 0:
|
||||
delaytimes = []int{int(vals[8]) * 16}
|
||||
case 1: // relative to BPM
|
||||
notetracking = 2
|
||||
index := vals[8] >> 2
|
||||
delaytime := 48
|
||||
if int(index) < len(_4klangDelays) {
|
||||
delaytime = _4klangDelays[index]
|
||||
}
|
||||
delaytimes = []int{delaytime}
|
||||
case 2: // notetracking
|
||||
notetracking = 1
|
||||
delaytimes = []int{10787}
|
||||
}
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "delay",
|
||||
Parameters: map[string]int{
|
||||
"stereo": 0,
|
||||
"pregain": int(vals[0]),
|
||||
"dry": int(vals[1]),
|
||||
"feedback": int(vals[2]),
|
||||
"damp": int(vals[3]),
|
||||
"notetracking": notetracking,
|
||||
},
|
||||
VarArgs: delaytimes,
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangFOP(vals [15]byte, version int) []Unit {
|
||||
var t string
|
||||
var stereo int
|
||||
switch vals[0] {
|
||||
case 1:
|
||||
t, stereo = "pop", 0
|
||||
case 2:
|
||||
t, stereo = "addp", 0
|
||||
case 3:
|
||||
t, stereo = "mulp", 0
|
||||
case 4:
|
||||
t, stereo = "push", 0
|
||||
case 5:
|
||||
t, stereo = "xch", 0
|
||||
case 6:
|
||||
t, stereo = "add", 0
|
||||
case 7:
|
||||
t, stereo = "mul", 0
|
||||
case 8:
|
||||
t, stereo = "addp", 1
|
||||
case 9:
|
||||
return []Unit{{Type: "loadnote", Parameters: map[string]int{"stereo": stereo}}, // 4klang loadnote gives 0..1, sointu gives -1..1
|
||||
{Type: "loadval", Parameters: map[string]int{"value": 128, "stereo": stereo}},
|
||||
{Type: "addp", Parameters: map[string]int{"stereo": stereo}},
|
||||
{Type: "gain", Parameters: map[string]int{"stereo": stereo, "gain": 64}}}
|
||||
default:
|
||||
t, stereo = "mulp", 1
|
||||
}
|
||||
return []Unit{{
|
||||
Type: t,
|
||||
Parameters: map[string]int{"stereo": stereo},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangFST(vals [15]byte, version int) []Unit {
|
||||
sendpop := 0
|
||||
if vals[1]&0x40 == 0x40 {
|
||||
sendpop = 1
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "send",
|
||||
Parameters: map[string]int{
|
||||
"amount": int(vals[0]),
|
||||
"sendpop": sendpop,
|
||||
"dest_stack": int(vals[2]),
|
||||
"dest_unit": int(vals[3]),
|
||||
"dest_slot": int(vals[4]),
|
||||
"dest_id": int(vals[5]),
|
||||
}}}
|
||||
}
|
||||
|
||||
func fix4klangTargets(instrIndex int, instr Instrument, m _4klangTargetMap) {
|
||||
for _, u := range instr.Units {
|
||||
if u.Type == "send" {
|
||||
destStack := u.Parameters["dest_stack"]
|
||||
if destStack == 255 {
|
||||
destStack = instrIndex
|
||||
}
|
||||
fourKlangTarget := _4klangStackUnit{
|
||||
destStack,
|
||||
u.Parameters["dest_unit"]}
|
||||
u.Parameters["target"] = m[fourKlangTarget]
|
||||
if u.Parameters["dest_id"] < len(_4klangUnitPorts) && u.Parameters["dest_slot"] < 8 {
|
||||
if u.Parameters["dest_id"] == 4 && u.Parameters["dest_slot"] == 3 { // distortion is split into 2 units
|
||||
u.Parameters["target"]++
|
||||
u.Parameters["port"] = 0
|
||||
} else {
|
||||
modTarget := _4klangUnitPorts[u.Parameters["dest_id"]]
|
||||
for i, s := range Ports[modTarget.UnitType] {
|
||||
if s == modTarget.PortName[u.Parameters["dest_slot"]] {
|
||||
u.Parameters["port"] = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(u.Parameters, "dest_stack")
|
||||
delete(u.Parameters, "dest_unit")
|
||||
delete(u.Parameters, "dest_slot")
|
||||
delete(u.Parameters, "dest_id")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func read4klangPAN(vals [15]byte, version int) []Unit {
|
||||
return []Unit{{
|
||||
Type: "pan",
|
||||
Parameters: map[string]int{
|
||||
"stereo": 0,
|
||||
"panning": int(vals[0]),
|
||||
}}}
|
||||
}
|
||||
|
||||
func read4klangOUT(vals [15]byte, version int) []Unit {
|
||||
return []Unit{{
|
||||
Type: "outaux",
|
||||
Parameters: map[string]int{
|
||||
"stereo": 1,
|
||||
"outgain": int(vals[0]),
|
||||
"auxgain": int(vals[1])},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangACC(vals [15]byte, version int) []Unit {
|
||||
c := 0
|
||||
if vals[0] != 0 {
|
||||
c = 2
|
||||
}
|
||||
return []Unit{{
|
||||
Type: "in",
|
||||
Parameters: map[string]int{"stereo": 1, "channel": c},
|
||||
}}
|
||||
}
|
||||
|
||||
func read4klangFLD(vals [15]byte, version int) []Unit {
|
||||
return []Unit{{
|
||||
Type: "loadval",
|
||||
Parameters: map[string]int{"stereo": 0, "value": int(vals[0])},
|
||||
}}
|
||||
}
|
||||
216
CHANGELOG.md
216
CHANGELOG.md
@ -3,23 +3,203 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
## [0.4.1]
|
||||
### Added
|
||||
- An instrument (set of opcodes & accompanying values) can have any number of voices.
|
||||
- A track can trigger any number of voices (polyphonism).
|
||||
- Pattern length does not have to be a power of 2.
|
||||
- Macros for defining patches, so that only the necessary parts of the synth are compiled in.
|
||||
- Harmonized support for stereo signals: every opcode supports stereo variant.
|
||||
- New opcodes: bit-crusher, gain, inverse gain, clip, speed (bpm modulation), compressor.
|
||||
- Support for sample-based oscillators; samples loaded from gm.dls.
|
||||
- Unison oscillators: multiple copies of the oscillator running sligthly detuned and added up to together.
|
||||
- Support for 32 and 64 bit builds.
|
||||
- Regression tests for opcodes, using CTests.
|
||||
- Switch to CMake for builds.
|
||||
- Compiling as a static library & an API to call Sointu
|
||||
- Running all tests (win/linux/mac) in the cloud, using Github workflows
|
||||
- go: a Go package to call Sointu
|
||||
- go: Importing and exporting Sointu .asm songs
|
||||
- go: asmfmt, a command line utility to format/process Sointu .asm song files
|
||||
- Clicking the parameter slider also selects that parameter ([#112][i112])
|
||||
- The vertical and horizontal split bars indicate with a cursor that they can be
|
||||
resized ([#145][i145])
|
||||
|
||||
[Unreleased]: https://github.com/vsariola/sointu/compare/4klang-3.11...HEAD
|
||||
### Fixed
|
||||
- When adding a unit on the last row of the unit list, the editor for entering
|
||||
the type of the unit by text did gain focus.
|
||||
- When inputting a note to the note editor, advance the cursor by step
|
||||
([#144][i144])
|
||||
- When loading an instrument, make sure the total number of voices does not go
|
||||
over the maximum number allowed by vm, and make sure a loaded instrument has
|
||||
at least 1 voice
|
||||
- Potential ID collisions when clearing unit or pasteing instruments
|
||||
- Assign new IDs to loaded instruments, and fix ID collisions in case they
|
||||
somehow still appear ([#146][i146])
|
||||
- In x86 templates, do not optimize away phase modulations when unisons are used
|
||||
even if all phase inputs are zeros, as unisons use the phase modulation
|
||||
mechanism to offset the different oscillators
|
||||
- Do not include delay times in the delay time table if the delay unit is
|
||||
disabled ([#139][i139])
|
||||
- Moved the error and warning popups slightly up so they don't block the unit
|
||||
control buttons ([#142][i142])
|
||||
|
||||
### Changed
|
||||
- Do not automatically wrap around the song when playing as it was usually
|
||||
unwanted behaviour. There is already the looping mechanism if the user really
|
||||
wants to loop the song forever.
|
||||
|
||||
## [0.4.0]
|
||||
### Added
|
||||
- User can drop preset instruments into `os.UserConfigDir()/sointu/presets/` and
|
||||
they appear in the list of presets next time sointu is started.
|
||||
([#125][i125])
|
||||
- Ability to loop certain section of the song when playing. The loop can be set
|
||||
by using the toggle button in the song panel, or by hitting Ctrl+L.
|
||||
([#128][i128])
|
||||
- Disable units temporarily. The disabled units are shown in gray and are not
|
||||
compiled into the patch and are considered for all purposes non-existent.
|
||||
Hitting Ctrl-D disables/re-enables the selected unit(s). The yaml file has
|
||||
field `disabled: true` for the unit. ([#116][i116])
|
||||
- Passing a file name on command line immediately tries loading that file ([#122][i122])
|
||||
- Massive rewrite of the GUI, in particular allowing better copying, pasting and
|
||||
scrolling of table-based data (order list and note data).
|
||||
- Dbgain unit, which allows defining the gain in decibels (-40 dB to +40dB)
|
||||
- `+` and `-` keys add/subtract values in order editor and pattern editor
|
||||
([#65][i65])
|
||||
- The function `su_power` is exported so people can reuse it in the main code;
|
||||
however, as it assumes the parameter passed in st0 on the x87 stack and
|
||||
similarly returns it value in st0 on the x87 stack, to my knowledge there is
|
||||
no calling convention that would correspond this behaviour, so you need to
|
||||
define a header for it yourself and take care of putting the float value on
|
||||
x87 stack.
|
||||
|
||||
### Fixed
|
||||
- Loading a preset did not update the IDs of the newly loaded instrument,
|
||||
causing ID collisions and sends target wrong units.
|
||||
- The x87 native filter unit was denormalizing and eating up a lot of CPU ([#68][i68])
|
||||
- Modulating delaytime in wasm could crash, because delay time was converted to
|
||||
int with i32.trunc_f32_u. Using i32.trunc_f32_s fixed this.
|
||||
- When recording notes from VSTI, no track was created for instruments that had
|
||||
no notes triggered, resulting in misalignment of the tracks from instruments.
|
||||
- 32-bit su_load_gmdls clobbered ebx, even though __stdcall demands it to be not
|
||||
touched ([#130][i130])
|
||||
- Spaces are allowed in instrument names ([#120][i120])
|
||||
- Fixed the dropdown for targeting sends making it impossible to choose certain
|
||||
ops. This was done just by reducing the default height of popup menus so they
|
||||
fit on screen ([#121][i121])
|
||||
- Warn user about sample rate being other than 44100 Hz, as this lead to weird
|
||||
behaviour. Sointu assumes the samplerate always to be 44100 Hz. ([#129][i129])
|
||||
|
||||
### Changed
|
||||
- The scroll wheel behavior for unit integer parameters was flipped: scrolling
|
||||
up now increases the value, while scrolling down decreases the value. It was
|
||||
vice versa. ([#112][i112])
|
||||
|
||||
## [0.3.0]
|
||||
### Added
|
||||
- Scroll bars to menus, shown when a menu is too long to fit.
|
||||
- Save the GUI state periodically to a recovery file and load it on
|
||||
startup of the app, if present. The recovery files are located in the
|
||||
app config directory (e.g. AppData/Roaming/Sointu on Windows).
|
||||
- Save the VSTI GUI state to the DAW project file, through GetChunk /
|
||||
SetChunk mechanisms.
|
||||
- Instrument presets. The presets are embedded in the executable and
|
||||
there's a button to open a menu to load one of the presets.
|
||||
- Frequency modulation target for oscillator, as it was in 4klang
|
||||
- Reverb preset settings for a delay unit, with stereo, left and right
|
||||
options
|
||||
|
||||
### Fixed
|
||||
- Crash when running more than one sointu VSTI plugins in parallel
|
||||
- The scroll bars move in sync with the cursor.
|
||||
- The stereo version of delay in the go virtual machine (executables / plugins
|
||||
not ending with -native) applied the left delay taps on the right channel, and
|
||||
the right delay taps on the left channel.
|
||||
- The sointu-vsti-native plugin has different plugin ID and plugin name
|
||||
to not confuse it with the non-native one
|
||||
- The VSTI waits for the gioui actually have quit when closing the
|
||||
plugin
|
||||
|
||||
### Changed
|
||||
- BREAKING CHANGE: The meaning of default modulation mode ("auto") has
|
||||
been changed for cross-instrument modulations: it now means "all"
|
||||
voices, instead of first voice (which was redundant, as it was same as
|
||||
defining voice = 0). This means that for cross-instrument modulations,
|
||||
one "all vocies" send gets actually compiled into multiple sends, one
|
||||
for each targeted voice. For intra-instrument modulations, the meaning
|
||||
stays the same, but the label was changed to "self", to highlight that
|
||||
this means the voice modulates only itself and not other voices.
|
||||
|
||||
## [0.2.0]
|
||||
### Added
|
||||
- Saving and loading instruments
|
||||
- Comment field to instruments
|
||||
- Ability to reorder tracks
|
||||
- Add menu command to delete all unused data from song file
|
||||
- Ability to search a unit by typing its name
|
||||
- Ability to run sointu as a vsti plugin, inside vsti host
|
||||
- Ability to lock delay relative to beat duration
|
||||
- Ability to import 4klang patches (.4kp) and instruments (.4ki)
|
||||
- The repository has example instruments, including all patches and
|
||||
instruments from 4klang
|
||||
- The compiler templates are embedded in the sointu-compile, so no
|
||||
installation is needed beyond copying sointu-compile to PATH
|
||||
- Ability to select multiple units and cut, copy & paste them
|
||||
- Mousewheel adjusts unit parameters
|
||||
- Tooltips to many buttons
|
||||
- Support for gm.dls samples in the go-written virtual machine
|
||||
- x86 and C written examples how to play a sointu song on various
|
||||
platforms. On Windows, the examples can optionally be linked with
|
||||
Crinkler to get Crinkler reports.
|
||||
|
||||
### Fixed
|
||||
- Unnamed instruments with multiple voices caused crashes
|
||||
- In the native version, exceeding the 64 delaylines caused crashes
|
||||
- wat2wasm nowadays uses funcref instead of anyfunc
|
||||
- In the WebAssembly core, $WRK was messed after stereo oscillators,
|
||||
making modulations not work
|
||||
- The Webassembly implementation of mono version of the "out" unit
|
||||
|
||||
### Changed
|
||||
- The release flag in the voice is now a sustain flag i.e. the logic has
|
||||
been inverted. This was done so that when the synth is initialized
|
||||
with zeros, all voices start with sustain = 0 i.e. in released state.
|
||||
- The crush resolution is now in bits instead of linear range; this is a
|
||||
breaking change and changes the meaning of the resolution values. But
|
||||
now there are more usable values in the resolution.
|
||||
|
||||
## [0.1.0]
|
||||
### Added
|
||||
- An instrument (set of opcodes & accompanying values) can have any
|
||||
number of voices.
|
||||
- A track can trigger any number of voices, releasing the previous when
|
||||
new one is triggered.
|
||||
- Pattern length does not have to be a power of 2.
|
||||
- Only the necessary opcodes and functions of the synth are compiled in the final executable.
|
||||
- Harmonized support for stereo signals: every opcode supports stereo
|
||||
variant.
|
||||
- New opcodes: crush, gain, inverse gain, clip, speed (bpm modulation),
|
||||
compressor.
|
||||
- Support for sample-based oscillators (samples loaded from gm.dls).
|
||||
- Unison oscillators: multiple copies of the oscillator running with
|
||||
different detuning and added up to together.
|
||||
- Support for 32 and 64 bit builds.
|
||||
- Support different platforms: Windows, Linux and Mac (Intel).
|
||||
- Experimental support for compiling songs into WebAssembly.
|
||||
- Switch to CMake for builds.
|
||||
- Regression tests for every VM instruction, using CTests.
|
||||
- Compiling as a static library & an API to call Sointu
|
||||
- Running all tests (win/linux/mac/wasm) in the cloud, using Github
|
||||
workflows
|
||||
- Tools written in Go-lang:
|
||||
- a tracker for composing songs as .yml
|
||||
- a command line utility to convert .yml songs to .asm
|
||||
- a command line utility to play the songs on command line
|
||||
|
||||
[Unreleased]: https://github.com/vsariola/sointu/compare/v0.4.1...HEAD
|
||||
[0.4.1]: https://github.com/vsariola/sointu/compare/v0.4.0...v0.4.1
|
||||
[0.4.0]: https://github.com/vsariola/sointu/compare/v0.3.0...v0.4.0
|
||||
[0.3.0]: https://github.com/vsariola/sointu/compare/v0.2.0...v0.3.0
|
||||
[0.2.0]: https://github.com/vsariola/sointu/compare/v0.1.0...v0.2.0
|
||||
[0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
|
||||
[i65]: https://github.com/vsariola/sointu/issues/65
|
||||
[i68]: https://github.com/vsariola/sointu/issues/68
|
||||
[i112]: https://github.com/vsariola/sointu/issues/112
|
||||
[i116]: https://github.com/vsariola/sointu/issues/116
|
||||
[i120]: https://github.com/vsariola/sointu/issues/120
|
||||
[i121]: https://github.com/vsariola/sointu/issues/121
|
||||
[i122]: https://github.com/vsariola/sointu/issues/122
|
||||
[i125]: https://github.com/vsariola/sointu/issues/125
|
||||
[i128]: https://github.com/vsariola/sointu/issues/128
|
||||
[i129]: https://github.com/vsariola/sointu/issues/129
|
||||
[i130]: https://github.com/vsariola/sointu/issues/130
|
||||
[i139]: https://github.com/vsariola/sointu/issues/139
|
||||
[i142]: https://github.com/vsariola/sointu/issues/142
|
||||
[i144]: https://github.com/vsariola/sointu/issues/144
|
||||
[i145]: https://github.com/vsariola/sointu/issues/145
|
||||
[i146]: https://github.com/vsariola/sointu/issues/146
|
||||
|
||||
@ -26,6 +26,8 @@ endif()
|
||||
|
||||
IF(APPLE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-no_pie")
|
||||
# https://stackoverflow.com/questions/69803659/what-is-the-proper-way-to-build-for-macos-x86-64-using-cmake-on-apple-m1-arm
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE INTERNAL "" FORCE)
|
||||
endif()
|
||||
|
||||
find_program(GO NAMES go)
|
||||
@ -62,8 +64,8 @@ else()
|
||||
endif()
|
||||
|
||||
# the tests include the entire ASM but we still want to rebuild when they change
|
||||
file(GLOB x86templates ${PROJECT_SOURCE_DIR}/templates/amd64-386/*.asm)
|
||||
file(GLOB wasmtemplates ${PROJECT_SOURCE_DIR}/templates/wasm/*.wat)
|
||||
file(GLOB x86templates "${PROJECT_SOURCE_DIR}/vm/compiler/templates/amd64-386/*.asm")
|
||||
file(GLOB wasmtemplates "${PROJECT_SOURCE_DIR}/vm/compiler/templates/wasm/*.wat")
|
||||
file(GLOB sointusrc "${PROJECT_SOURCE_DIR}/*.go")
|
||||
file(GLOB compilersrc "${PROJECT_SOURCE_DIR}/compiler/*.go")
|
||||
file(GLOB compilecmdsrc "${PROJECT_SOURCE_DIR}/cmd/sointu-compile/*.go")
|
||||
@ -83,27 +85,34 @@ set(sointuasm sointu.asm)
|
||||
|
||||
# Build sointu-cli only once because go run has everytime quite a bit of delay when
|
||||
# starting
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"${compilecmd}"
|
||||
COMMAND
|
||||
${GO} build -o "${compilecmd}" ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go
|
||||
DEPENDS ${x86templates} ${wasmtemplates} ${sointusrc} ${compilersrc} ${compilecmdsrc}
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
sointu-compiler
|
||||
COMMAND ${GO} build -o ${compilecmd} ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go
|
||||
SOURCES "${sointusrc}" "${compilersrc}" "${compilecmdsrc}"
|
||||
DEPENDS ${compilecmd}
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${sointuasm}
|
||||
COMMAND ${compilecmd} -arch=${arch} -a -o ${CMAKE_CURRENT_BINARY_DIR}
|
||||
DEPENDS "${templates}" sointu-compiler
|
||||
DEPENDS ${compilecmd}
|
||||
)
|
||||
|
||||
add_library(${STATICLIB} ${sointuasm})
|
||||
set_target_properties(${STATICLIB} PROPERTIES LINKER_LANGUAGE C)
|
||||
target_include_directories(${STATICLIB} INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
# We should put examples here
|
||||
# add_subdirectory(examples)
|
||||
# Examples are now available.
|
||||
add_subdirectory(examples)
|
||||
|
||||
# Testing only available if this is the main app
|
||||
# Emergency override 4KLANG_CMAKE_BUILD_TESTING provided as well
|
||||
if((CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME OR SOINTU_CMAKE_BUILD_TESTING) AND BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
411
README.md
411
README.md
@ -1,13 +1,33 @@
|
||||
# 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 will be in the [Wiki](https://github.com/vsariola/sointu/wiki).
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
You can either 1) download the prebuilt release binaries from the
|
||||
[releases](https://github.com/vsariola/sointu/releases); or 2) download the
|
||||
latest build from the master branch from the
|
||||
[actions](https://github.com/vsariola/sointu/actions) (find workflow "Binaries"
|
||||
and scroll down for .zip files containing the artifacts). Then just run one of
|
||||
the executables or, in the case of the VST plugins library files, copy them
|
||||
wherever you keep you VST2 plugins.
|
||||
|
||||
The pre 1.0 version tags are mostly for reference: no backwards
|
||||
compatibility will be guaranteed while upgrading to a newer version.
|
||||
Backwards compatibility will be attempted from 1.0 onwards.
|
||||
|
||||
**Uninstallation**: Sointu stores recovery data in OS-specific folders
|
||||
e.g. `AppData/Roaming/Sointu` on Windows. For clean uninstall, delete
|
||||
also this folder. See [here](https://pkg.go.dev/os#UserConfigDir) where
|
||||
to find those folders on other platforms.
|
||||
|
||||
Summary
|
||||
-------
|
||||
@ -24,20 +44,17 @@ synthesis engine can already be fitted in 600 bytes (386, compressed), with
|
||||
another few hundred bytes for the patch and pattern data.
|
||||
|
||||
Sointu consists of two core elements:
|
||||
- A cross-platform synth-tracker app for composing music, written in
|
||||
[go](https://golang.org/). The app is still heavily work in progress. The app
|
||||
exports the projects as .yml files. There are two versions of the app:
|
||||
[cmd/sointu-track/](sointu-track), using a plain Go VM bytecode interpreter,
|
||||
and [cmd/sointu-nativetrack/](sointu-nativetrack), using cgo to bridge calls
|
||||
to the Sointu compiled VM. The former should be highly portable, the latter
|
||||
currently works only on x86/amd64 platforms.
|
||||
- A cross-platform synth-tracker that runs as either VSTi or stand-alone
|
||||
app for composing music, written in [go](https://golang.org/). The app
|
||||
is still heavily work in progress. The app exports the projects as
|
||||
.yml files.
|
||||
- A compiler, likewise written in go, which can be invoked from the command line
|
||||
to compile these .yml files into .asm or .wat code. For x86/amd64, the
|
||||
resulting .asm can be then compiled by [nasm](https://www.nasm.us/) or
|
||||
[yasm](https://yasm.tortall.net). For browsers, the resulting .wat can be
|
||||
compiled by [wat2wasm](https://github.com/WebAssembly/wabt).
|
||||
resulting .asm can be then compiled by [nasm](https://www.nasm.us/). For
|
||||
browsers, the resulting .wat can be compiled by
|
||||
[wat2wasm](https://github.com/WebAssembly/wabt).
|
||||
|
||||
This is how the current prototype tracker looks like:
|
||||
This is how the current prototype app looks like:
|
||||
|
||||

|
||||
|
||||
@ -49,52 +66,106 @@ listed below.
|
||||
|
||||
### Sointu-track
|
||||
|
||||
This version of the tracker is the version that uses the bytecode interpreter
|
||||
written in plain Go. Running the tracker:
|
||||
This is the stand-alone version of the synth-tracker. Sointu-track uses
|
||||
the [gioui](https://gioui.org/) for the GUI and [oto](https://github.com/hajimehoshi/oto)
|
||||
for the audio, so the portability is currently limited by these.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- [go](https://golang.org/)
|
||||
- If you want to use the faster x86 assembly written synthesizer:
|
||||
- Follow the instructions to build the [x86 native virtual machine](#native-virtual-machine)
|
||||
before building the tracker.
|
||||
- cgo compatible compiler e.g. [gcc](https://gcc.gnu.org/). On
|
||||
windows, you best bet is [MinGW](http://www.mingw.org/). We use the [tdm-gcc](https://jmeubank.github.io/tdm-gcc/).
|
||||
The compiler can be in PATH or you can use the environment variable
|
||||
`CC` to help go find the compiler.
|
||||
- Setting environment variable `CGO_ENABLED=1` is a good idea,
|
||||
because if it is not set and go fails to find the compiler, go just
|
||||
excludes all files with `import "C"` from the build, resulting in
|
||||
lots of errors about missing types.
|
||||
|
||||
#### Running
|
||||
|
||||
```
|
||||
go run cmd/sointu-track/main.go
|
||||
```
|
||||
|
||||
Building the tracker:
|
||||
#### Building an executable
|
||||
|
||||
```
|
||||
go build -o sointu-track cmd/sointu-track/main.go
|
||||
go build -o sointu-track.exe cmd/sointu-track/main.go
|
||||
```
|
||||
|
||||
On windows, replace `-o sointu-track` with `-o sointu-track.exe`.
|
||||
On other platforms than Windows, replace `-o sointu-track.exe` with
|
||||
`-o sointu-track`.
|
||||
|
||||
Sointu-track uses the [gioui](https://gioui.org/) for the GUI and
|
||||
[oto](https://github.com/hajimehoshi/oto) for the audio, so the portability is
|
||||
currently limited by these.
|
||||
If you want to use the [x86 native virtual machine](#native-virtual-machine),
|
||||
add `-tags=native` to all the commands e.g.
|
||||
|
||||
> :warning: Unlike the x86/amd64 VM compiled by Sointu, the Go written VM
|
||||
> bytecode interpreter uses a software stack. Thus, unlike x87 FPU stack, it is
|
||||
> not limited to 8 items. If you intent to compile the patch to x86/amd64
|
||||
> targets, make sure not to use too much stack. Keeping at most 5 signals in the
|
||||
> stack is presumably fine (reserving 3 for the temporary variables of the
|
||||
> opcodes). In future, the app should give warnings if the user is about to
|
||||
> exceed the capabilities of a target platform.
|
||||
```
|
||||
go build -o sointu-track.exe -tags=native cmd/sointu-track/main.go
|
||||
```
|
||||
|
||||
### Compiler
|
||||
### Sointu-vsti
|
||||
|
||||
This is the VST instrument plugin version of the tracker, compiled into
|
||||
a dynamically linked library and ran inside a VST host.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- [go](https://golang.org/)
|
||||
- cgo compatible compiler e.g. [gcc](https://gcc.gnu.org/). On windows,
|
||||
you best bet is [MinGW](http://www.mingw.org/). We use the [tdm-gcc](https://jmeubank.github.io/tdm-gcc/).
|
||||
The compiler can be in PATH or you can use the environment variable
|
||||
`CC` to help go find the compiler.
|
||||
- Setting environment variable `CGO_ENABLED=1` is a good idea, because
|
||||
if it is not set and go fails to find the compiler, go just excludes
|
||||
all files with `import "C"` from the build, resulting in lots of
|
||||
errors about missing types.
|
||||
- If you want to use the faster x86 assembly written synthesizer:
|
||||
- Follow the instructions to build the [x86 native virtual machine](#native-virtual-machine)
|
||||
before building the plugin itself
|
||||
|
||||
#### Building
|
||||
|
||||
```
|
||||
go build -buildmode=c-shared -tags=plugin -o sointu-vsti.dll .\cmd\sointu-vsti\
|
||||
```
|
||||
|
||||
On other platforms than Windows, replace `-o sointu-vsti.dll` appropriately e.g.
|
||||
`-o sointu-vsti.so`; so far, the VST instrument has been built & tested on
|
||||
Windows and Linux.
|
||||
|
||||
Notice the `-tags=plugin` build tag definition. This is required by the [vst2
|
||||
library](https://github.com/pipelined/vst2); otherwise, you will get a lot of
|
||||
build errors.
|
||||
|
||||
Add `-tags=native,plugin` to use the [x86 native virtual
|
||||
machine](#native-virtual-machine) instead of the virtual machine written in Go.
|
||||
|
||||
### Sointu-compile
|
||||
|
||||
The command line interface to it is [sointu-compile](cmd/sointu-compile/main.go)
|
||||
and the actual code resides in the [compiler](vm/compiler/) package, which is an
|
||||
ordinary [go](https://golang.org/) package with no other tool dependencies.
|
||||
|
||||
Running the compiler:
|
||||
#### Running
|
||||
|
||||
```
|
||||
go run cmd/sointu-compile/main.go
|
||||
```
|
||||
|
||||
Building the compiler:
|
||||
#### Building an executable
|
||||
|
||||
```
|
||||
go build -o sointu-compile cmd/sointu-compile/main.go
|
||||
go build -o sointu-compile.exe cmd/sointu-compile/main.go
|
||||
```
|
||||
|
||||
On windows, replace `-o sointu-compile` with `-o sointu-compile.exe`.
|
||||
On other platforms than Windows, replace `-o sointu-compile.exe` with
|
||||
`-o sointu-compile`.
|
||||
|
||||
#### Usage
|
||||
|
||||
The compiler can then be used to compile a .yml song into .asm and .h files. For
|
||||
example:
|
||||
@ -111,18 +182,113 @@ sointu-compile -o . -arch=wasm tests/test_chords.yml
|
||||
wat2wasm --enable-bulk-memory test_chords.wat
|
||||
```
|
||||
|
||||
### Building and running the tests as executables
|
||||
If you are looking for an easy way to compile an executable from a Sointu song
|
||||
(e.g. for a executable music compo), take a look at [NR4's Python-based
|
||||
tool](https://github.com/LeStahL/sointu-executable-msx) for it.
|
||||
|
||||
#### Examples
|
||||
|
||||
The folder `examples/code` contains usage examples for Sointu with winmm and
|
||||
dsound playback under Windows and asound playback under Unix. Source code is
|
||||
available in C and x86 assembly (win32, elf32 and elf64 versions).
|
||||
|
||||
To build the examples, use `ninja examples`.
|
||||
|
||||
If you want to target smaller executable sizes, using a compressing linker like
|
||||
[Crinkler](https://github.com/runestubbe/Crinkler) on Windows is recommended.
|
||||
|
||||
The linux examples use ALSA and need libasound2-dev (or libasound2-dev:386)
|
||||
installed. The 386 version also needs pipewire-alsa:386 installed, which is not
|
||||
there by default.
|
||||
|
||||
### Native virtual machine
|
||||
|
||||
The native bridge allows Go to call the sointu compiled x86 native virtual
|
||||
machine, through cgo, instead of using the Go written bytecode interpreter. It's
|
||||
likely slightly faster than the interpreter. Before you can actually run it, you
|
||||
need to build the bridge using CMake (thus, ***this will not work with go
|
||||
get***).
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- [CMake](https://cmake.org)
|
||||
- [nasm](https://www.nasm.us/)
|
||||
- *cgo compatible compiler* e.g. [gcc](https://gcc.gnu.org/). On windows, you
|
||||
best bet is [MinGW](http://www.mingw.org/). We use the
|
||||
[tdm-gcc](https://jmeubank.github.io/tdm-gcc/)
|
||||
|
||||
The last point is because the command line player and the tracker use
|
||||
[cgo](https://golang.org/cmd/cgo/) to interface with the synth core, which is
|
||||
compiled into a library. The cgo bridge resides in the package
|
||||
[bridge](vm/compiler/bridge/).
|
||||
|
||||
#### Building
|
||||
|
||||
Assuming you are using [ninja](https://ninja-build.org/):
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -GNinja
|
||||
ninja sointu
|
||||
```
|
||||
|
||||
> :warning: *you must build the library inside a directory called 'build' at the
|
||||
> root of the project*. This is because the path where cgo looks for the library
|
||||
> is hard coded to point to build/ in the go files.
|
||||
|
||||
Running `ninja sointu` only builds the static library that Go needs. This is a
|
||||
lot faster than building all the CTests.
|
||||
|
||||
You and now run all the Go tests, even the ones that test the native bridge.
|
||||
From the project root folder, run:
|
||||
|
||||
```
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Play a song from the command line:
|
||||
```
|
||||
go run -tags=native cmd/sointu-play/main.go tests/test_chords.yml
|
||||
```
|
||||
|
||||
> :warning: Unlike the x86/amd64 VM compiled by Sointu, the Go written VM
|
||||
> bytecode interpreter uses a software stack. Thus, unlike x87 FPU stack, it is
|
||||
> not limited to 8 items. If you intent to compile the patch to x86/amd64
|
||||
> targets, make sure not to use too much stack. Keeping at most 5 signals in the
|
||||
> stack is presumably fine (reserving 3 for the temporary variables of the
|
||||
> opcodes). In future, the app should give warnings if the user is about to
|
||||
> exceed the capabilities of a target platform.
|
||||
|
||||
> :warning: **If you are using Yasm instead of Nasm, and you are using MinGW**:
|
||||
> Yasm 1.3.0 (currently still the latest stable release) and GNU linker do not
|
||||
> play nicely along, trashing the BSS layout. The linker had placed our synth
|
||||
> object overlapping with DLL call addresses; very funny stuff to debug. See
|
||||
> [here](https://tortall.lighthouseapp.com/projects/78676/tickets/274-bss-problem-with-windows-win64)
|
||||
> and the fix
|
||||
> [here](https://github.com/yasm/yasm/commit/1910e914792399137dec0b047c59965207245df5).
|
||||
> Since Nasm is nowadays under BSD license, there is absolutely no reason to use
|
||||
> Yasm. However, if you do, use a newer nightly build of Yasm that includes the
|
||||
> fix.
|
||||
|
||||
### Tests
|
||||
|
||||
There are [regression tests](tests/) that are built as executables,
|
||||
testing that they work the same way when you would link them in an
|
||||
intro.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
Building the [regression tests](tests/) as executables (testing that they work
|
||||
the same way when you would link them in an intro) requires:
|
||||
- [go](https://golang.org/)
|
||||
- [CMake](https://cmake.org) with CTest
|
||||
- [nasm](https://www.nasm.us/) or [yasm](https://yasm.tortall.net)
|
||||
- [nasm](https://www.nasm.us/)
|
||||
- Your favorite CMake compatible c-compiler & build tool. Results have been
|
||||
obtained using Visual Studio 2019, gcc&make on linux, MinGW&mingw32-make, and
|
||||
ninja&AppleClang.
|
||||
|
||||
For example, using [ninja](https://ninja-build.org/):
|
||||
#### Building and running
|
||||
|
||||
Assuming you are using [ninja](https://ninja-build.org/):
|
||||
|
||||
```
|
||||
mkdir build
|
||||
@ -142,83 +308,14 @@ cmake .. -DCMAKE_C_FLAGS="-m32" -DCMAKE_ASM_NASM_OBJECT_FORMAT="win32" -GNinja
|
||||
Another example: on Visual Studio 2019 Community, just open the folder, choose
|
||||
either Debug or Release and either x86 or x64 build, and hit build all.
|
||||
|
||||
### Native bridge & sointu-nativetrack
|
||||
|
||||
The native bridge allows Go to call the sointu compiled virtual machine, through
|
||||
cgo, instead of using the Go written bytecode interpreter. It's likely slightly
|
||||
faster than the interpreter. The version of the tracker that uses the native
|
||||
bridge is [sointu-nativetrack](cmd/sointu-nativetrack/). Before you can actually
|
||||
run it, you need to build the bridge using CMake (thus, the nativetrack does not
|
||||
work with go get)
|
||||
|
||||
Building the native bridge requires:
|
||||
- [go](https://golang.org/)
|
||||
- [CMake](https://cmake.org)
|
||||
- [nasm](https://www.nasm.us/) or [yasm](https://yasm.tortall.net)
|
||||
- *cgo compatible compiler* e.g. [gcc](https://gcc.gnu.org/). On windows, you
|
||||
best bet is [MinGW](http://www.mingw.org/). We use the
|
||||
[tdm-gcc](https://jmeubank.github.io/tdm-gcc/)
|
||||
|
||||
The last point is because the command line player and the tracker use
|
||||
[cgo](https://golang.org/cmd/cgo/) to interface with the synth core, which is
|
||||
compiled into a library. The cgo bridge resides in the package
|
||||
[bridge](vm/compiler/bridge/).
|
||||
|
||||
> :warning: *you must build the library inside a directory called 'build' at the
|
||||
> root of the project*. This is because the path where cgo looks for the library
|
||||
> is hard coded to point to build/ in the go files.
|
||||
|
||||
So, to build the library, run (this example is using
|
||||
[ninja](https://ninja-build.org/) for the build; adapt for other build tools
|
||||
accordingly):
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -GNinja
|
||||
ninja sointu
|
||||
```
|
||||
|
||||
Running `ninja sointu` only builds the static library that Go needs. This is a
|
||||
lot faster than building all the CTests.
|
||||
|
||||
You and now run all the Go tests, even the ones that test the native bridge.
|
||||
From the project root folder, run:
|
||||
|
||||
```
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Play a song from the command line:
|
||||
```
|
||||
go run cmd/sointu-play/main.go tests/test_chords.yml
|
||||
```
|
||||
|
||||
Run the tracker using the native bridge
|
||||
```
|
||||
go run cmd/sointu-nativetrack/main.go
|
||||
```
|
||||
|
||||
> :warning: **If you are using MinGW and Yasm**: Yasm 1.3.0 (currently still the
|
||||
> latest stable release) and GNU linker do not play nicely along, trashing the
|
||||
> BSS layout. See
|
||||
> [here](https://tortall.lighthouseapp.com/projects/78676/tickets/274-bss-problem-with-windows-win64)
|
||||
> and the fix
|
||||
> [here](https://github.com/yasm/yasm/commit/1910e914792399137dec0b047c59965207245df5).
|
||||
> Use a newer nightly build of yasm that includes the fix. The linker had placed
|
||||
> our synth object overlapping with DLL call addresses; very funny stuff to
|
||||
> debug.
|
||||
|
||||
> :warning: The sointu-nativetrack cannot be used with the syncs at the moment.
|
||||
> For syncs, use the Go VM (sointu-track).
|
||||
|
||||
### Building and running the WebAssembly tests
|
||||
### WebAssembly tests
|
||||
|
||||
These are automatically invoked by CTest if [node](https://nodejs.org) and
|
||||
[wat2wasm](https://github.com/WebAssembly/wabt) are found in the path.
|
||||
|
||||
New features since fork
|
||||
-----------------------
|
||||
|
||||
- **New units**. For example: bit-crusher, gain, inverse gain, clip, modulate
|
||||
bpm (proper triplets!), compressor (can be used for side-chaining).
|
||||
- **Compiler**. Written in go. The input is a .yml file and the output is an
|
||||
@ -228,7 +325,8 @@ New features since fork
|
||||
entropy as low as possible, yet we can call arbitrary go functions as
|
||||
"macros". The templates are [here](templates/) and the compiler lives
|
||||
[here](vm/compiler/).
|
||||
- **Tracker**. Written in go. A crude version exists.
|
||||
- **Tracker**. Written in go. Can run either as a stand-alone app or a vsti
|
||||
plugin.
|
||||
- **Supports 32 and 64 bit builds**. The 64-bit version is done with minimal
|
||||
changes to get it work, using template macros to change the lines between
|
||||
32-bit and 64-bit modes. Mostly, it's as easy as writing {{.AX}} instead of
|
||||
@ -262,12 +360,12 @@ New features since fork
|
||||
used.
|
||||
- **Songs are YAML files**. These markup files are simple data files,
|
||||
describing the tracks, patterns and patch structure (see
|
||||
[here](tests/test_oscillat_trisaw.yml) for an example). The sointu-cli
|
||||
compiler then reads these files and compiles them into .asm code. This has
|
||||
the nice implication that, in future, there will be no need for a binary
|
||||
format to save patches, nor should you need to commit .o or .asm to repo:
|
||||
just put the .yml in the repo and automate the .yml -> .asm -> .o steps
|
||||
using sointu-cli & nasm.
|
||||
[here](tests/test_oscillat_trisaw.yml) for an example). The sointu-compile
|
||||
then reads these files and compiles them into .asm code. This has the nice
|
||||
implication that, in future, there will be no need for a binary format to
|
||||
save patches, nor should you need to commit .o or .asm to repo: just put the
|
||||
.yml in the repo and automate the .yml -> .asm -> .o steps using
|
||||
sointu-compile & nasm.
|
||||
- **Harmonized support for stereo signals**. Every opcode supports a stereo
|
||||
variant: the stereo bit is hidden in the least significant bit of the
|
||||
command stream and passed in carry to the opcode. This has several nice
|
||||
@ -278,7 +376,7 @@ New features since fork
|
||||
stereo opcodes usually follow stereo opcodes (and mono opcodes follow mono
|
||||
opcodes), the stereo bits of the command bytes will be highly correlated and
|
||||
if crinkler or any other modeling compressor is doing its job, that should
|
||||
make them highly predictable i.e. highly compressably.
|
||||
make them highly predictable i.e. highly compressable.
|
||||
- **Test-driven development**. Given that 4klang was already a mature project,
|
||||
the first thing actually implemented was a set of regression tests to avoid
|
||||
breaking everything beyond any hope of repair. Done, using go test (runs the
|
||||
@ -297,12 +395,13 @@ New features since fork
|
||||
ports / 4 stereo ports, so even this method of routing is unlikely to run
|
||||
out of ports in small intros.
|
||||
- **Pattern length does not have to be a power of 2**.
|
||||
- **Sample-based oscillators, with samples imported from gm.dls**. Reading
|
||||
gm.dls is obviously Windows only, but with some effort the sample mechanism
|
||||
can be used also without it, in case you are working on a 64k and have some
|
||||
kilobytes to spare. See [this example](tests/test_oscillat_sample.yml), and
|
||||
this go generate [program](cmd/sointu-generate/main.go) parses the gm.dls
|
||||
file and dumps the sample offsets from it.
|
||||
- **Sample-based oscillators, with samples imported from gm.dls**. The
|
||||
gm.dls is available from system folder only on Windows, but the
|
||||
non-native tracker looks for it also in the current folder, so
|
||||
should you somehow magically get hold of gm.dls on Linux or Mac, you
|
||||
can drop it in the same folder with the tracker. See [this example](tests/test_oscillat_sample.yml),
|
||||
and this go generate [program](cmd/sointu-generate/main.go) parses
|
||||
the gm.dls file and dumps the sample offsets from it.
|
||||
- **Unison oscillators**. Multiple copies of the oscillator running slightly
|
||||
detuned and added up to together. Great for trance leads (supersaw). Unison
|
||||
of up to 4, or 8 if you make stereo unison oscillator and add up both left
|
||||
@ -316,20 +415,6 @@ New features since fork
|
||||
- **A bytecode interpreter written in pure go**. It's slightly slower than the
|
||||
hand-written assembly code by sointu compiler, but with this, the tracker is
|
||||
ultraportable and does not need cgo calls.
|
||||
- **Using Sointu as a sync-tracker**. Similar to [GNU
|
||||
Rocket](https://github.com/yupferris/gnurocket), but (ab)using the tracker
|
||||
we already have for music. We use the Go "rpc" package to send current sync
|
||||
values from the new "sync" opcode + optionally the current fractional row
|
||||
the song is on. The syncs are saved every 256th sample (approximately 172
|
||||
Hz). For 4k intro development, the idea is to write a debug version of the
|
||||
intro that merely loads the shader and listens to the RPC messages, and then
|
||||
draws the shader with those as the uniforms. Then, during the actual 4k
|
||||
intro, one can get the sync data from Sointu: if the song uses syncs,
|
||||
su_render_song writes the syncs to a float array. During each time step, a
|
||||
slice of this array can be sent to the shader as a uniform float array. A
|
||||
track with two voices, triggering an instrument with a single envelope and a
|
||||
slow filter can even be used as a cheap smooth interpolation mechanism,
|
||||
provided the syncs are added to each other in the shader.
|
||||
|
||||
Future goals
|
||||
------------
|
||||
@ -357,10 +442,6 @@ Long-shot ideas
|
||||
-----------
|
||||
- **Hack deeper into audio sources from the OS**. Speech synthesis, I'm eyeing
|
||||
at you.
|
||||
- **Ability to run Sointu as a DAW plugin (VSTi3)**. Now that Renoise supports
|
||||
VSTi3, there's no fundamental objection to compiling Sointu as a VSTi3. But
|
||||
don't expect it any soon; I need to digest the idea of having to learn the
|
||||
horrors of the VSTi3 C++ API.
|
||||
|
||||
Design philosophy
|
||||
-----------------
|
||||
@ -415,19 +496,45 @@ so I thought it would fun to learn some Finnish for a change. And
|
||||
Prods using Sointu
|
||||
------------------
|
||||
|
||||
[Adam](https://github.com/vsariola/adam) by brainlez Coders! - My first test-driving of Sointu. Some ideas how to integrate Sointu to the build chain.
|
||||
- [Adam](https://github.com/vsariola/adam) by brainlez Coders! My first
|
||||
test-driving of Sointu. The repository has some ideas how to integrate
|
||||
Sointu to the build chain.
|
||||
- [Roadtrip](https://www.pouet.net/prod.php?which=94105) by LJ & Virgill
|
||||
- [|](https://www.pouet.net/prod.php?which=94721) by epoqe. Likely the first
|
||||
Linux 4k intro using sointu.
|
||||
- [Physics Girl St.](https://www.pouet.net/prod.php?which=94890) by Team210
|
||||
- [Delusions of mediocrity](https://www.pouet.net/prod.php?which=95222) by
|
||||
mrange & Virgill
|
||||
- [Xorverse](https://www.pouet.net/prod.php?which=95221) by Alcatraz
|
||||
- [l'enveloppe](https://www.pouet.net/prod.php?which=95215) by Team210 & epoqe
|
||||
- [Phosphorescent Purple Pixel Peaks](https://www.pouet.net/prod.php?which=96198) by mrange & Virgill
|
||||
- [21](https://demozoo.org/music/338597/) by NR4 / Team210
|
||||
- [Tausendeins](https://www.pouet.net/prod.php?which=96192) by epoqe & Team210
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Pull requests / suggestions / issues welcome, through Github! Or just DM
|
||||
me on Discord (see contact information below).
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Distributed under the MIT License. See [LICENSE](LICENSE) for more information.
|
||||
|
||||
Contact
|
||||
-------
|
||||
|
||||
Veikko Sariola - pestis_bc on Demoscene discord - firstname.lastname@gmail.com
|
||||
|
||||
Project Link: [https://github.com/vsariola/sointu](https://github.com/vsariola/sointu)
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
The original 4klang was developed by Dominik Ries
|
||||
([gopher](https://github.com/hzdgopher/4klang)) and Paul Kraus (pOWL) of
|
||||
Alcatraz. :heart:
|
||||
The original 4klang: Dominik Ries ([gopher/Alcatraz](https://github.com/hzdgopher/4klang))
|
||||
& Paul Kraus (pOWL/Alcatraz) :heart:
|
||||
|
||||
Sointu was initiated by Veikko Sariola (pestis/bC!).
|
||||
|
||||
Apollo/bC! put the project on the path to Go, and wrote the prototype of the
|
||||
tracker GUI.
|
||||
|
||||
PoroCYon's [4klang fork](https://github.com/PoroCYon/4klang) inspired the macros
|
||||
for better cross-platform support.
|
||||
Sointu: Veikko Sariola (pestis/bC!), [Apollo/bC!](https://github.com/moitias),
|
||||
[NR4/Team210](https://github.com/LeStahL/), [PoroCYon](https://github.com/PoroCYon/4klang),
|
||||
[kendfss](https://github.com/kendfss), [anticore](https://github.com/anticore)
|
||||
|
||||
251
audio.go
251
audio.go
@ -1,11 +1,250 @@
|
||||
package sointu
|
||||
|
||||
type AudioSink interface {
|
||||
WriteAudio(buffer []float32) error
|
||||
Close() error
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type (
|
||||
// AudioBuffer is a buffer of stereo audio samples of variable length, each
|
||||
// sample represented by [2]float32. [0] is left channel, [1] is right
|
||||
AudioBuffer [][2]float32
|
||||
|
||||
// AudioOutput represents something where we can send audio e.g. audio output.
|
||||
// WriteAudio should block if not ready to accept audio e.g. buffer full.
|
||||
AudioOutput interface {
|
||||
WriteAudio(buffer AudioBuffer) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// AudioContext represents the low-level audio drivers. There should be at most
|
||||
// one AudioContext at a time. The interface is implemented at least by
|
||||
// oto.OtoContext, but in future we could also mock it.
|
||||
//
|
||||
// AudioContext is used to create one or more AudioOutputs with Output(); each
|
||||
// can be used to output separate sound & closed when done.
|
||||
AudioContext interface {
|
||||
Output() AudioOutput
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Synth represents a state of a synthesizer, compiled from a Patch.
|
||||
Synth interface {
|
||||
// Render tries to fill a stereo signal buffer with sound from the
|
||||
// synthesizer, until either the buffer is full or a given number of
|
||||
// timesteps is advanced. Normally, 1 sample = 1 unit of time, but speed
|
||||
// modulations may change this. It returns the number of samples filled (in
|
||||
// stereo samples i.e. number of elements of AudioBuffer filled), the
|
||||
// number of sync outputs written, the number of time steps time advanced,
|
||||
// and a possible error.
|
||||
Render(buffer AudioBuffer, maxtime int) (sample int, time int, err error)
|
||||
|
||||
// Update recompiles a patch, but should maintain as much as possible of its
|
||||
// state as reasonable. For example, filters should keep their state and
|
||||
// delaylines should keep their content. Every change in the Patch triggers
|
||||
// an Update and if the Patch would be started fresh every time, it would
|
||||
// lead to very choppy audio.
|
||||
Update(patch Patch, bpm int) error
|
||||
|
||||
// Trigger triggers a note for a given voice. Called between synth.Renders.
|
||||
Trigger(voice int, note byte)
|
||||
|
||||
// Release releases the currently playing note for a given voice. Called
|
||||
// between synth.Renders.
|
||||
Release(voice int)
|
||||
}
|
||||
|
||||
// Synther compiles a given Patch into a Synth, throwing errors if the
|
||||
// Patch is malformed.
|
||||
Synther interface {
|
||||
Synth(patch Patch, bpm int) (Synth, error)
|
||||
}
|
||||
)
|
||||
|
||||
// Play plays the Song by first compiling the patch with the given Synther,
|
||||
// returning the stereo audio buffer as a result (and possible errors).
|
||||
func Play(synther Synther, song Song, progress func(float32)) (AudioBuffer, error) {
|
||||
err := song.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
synth, err := synther.Synth(song.Patch, song.BPM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sointu.Play failed: %v", err)
|
||||
}
|
||||
curVoices := make([]int, len(song.Score.Tracks))
|
||||
for i := range curVoices {
|
||||
curVoices[i] = song.Score.FirstVoiceForTrack(i)
|
||||
}
|
||||
initialCapacity := song.Score.LengthInRows() * song.SamplesPerRow()
|
||||
buffer := make(AudioBuffer, 0, initialCapacity)
|
||||
rowbuffer := make(AudioBuffer, song.SamplesPerRow())
|
||||
for row := 0; row < song.Score.LengthInRows(); row++ {
|
||||
patternRow := row % song.Score.RowsPerPattern
|
||||
pattern := row / song.Score.RowsPerPattern
|
||||
for t := range song.Score.Tracks {
|
||||
order := song.Score.Tracks[t].Order
|
||||
if pattern < 0 || pattern >= len(order) {
|
||||
continue
|
||||
}
|
||||
patternIndex := song.Score.Tracks[t].Order[pattern]
|
||||
patterns := song.Score.Tracks[t].Patterns
|
||||
if patternIndex < 0 || int(patternIndex) >= len(patterns) {
|
||||
continue
|
||||
}
|
||||
pattern := patterns[patternIndex]
|
||||
if patternRow < 0 || patternRow >= len(pattern) {
|
||||
continue
|
||||
}
|
||||
note := pattern[patternRow]
|
||||
if note > 0 && note <= 1 { // anything but hold causes an action.
|
||||
continue
|
||||
}
|
||||
synth.Release(curVoices[t])
|
||||
if note > 1 {
|
||||
curVoices[t]++
|
||||
first := song.Score.FirstVoiceForTrack(t)
|
||||
if curVoices[t] >= first+song.Score.Tracks[t].NumVoices {
|
||||
curVoices[t] = first
|
||||
}
|
||||
synth.Trigger(curVoices[t], note)
|
||||
}
|
||||
}
|
||||
tries := 0
|
||||
for rowtime := 0; rowtime < song.SamplesPerRow(); {
|
||||
samples, time, err := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
|
||||
if err != nil {
|
||||
return buffer, fmt.Errorf("render failed: %v", err)
|
||||
}
|
||||
rowtime += time
|
||||
buffer = append(buffer, rowbuffer[:samples]...)
|
||||
if tries > 100 {
|
||||
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
|
||||
}
|
||||
}
|
||||
if progress != nil {
|
||||
progress(float32(row+1) / float32(song.Score.LengthInRows()))
|
||||
}
|
||||
}
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
type AudioContext interface {
|
||||
Output() AudioSink
|
||||
Close() error
|
||||
// Fill fills the AudioBuffer using a Synth, disregarding all syncs and time
|
||||
// limits. Note that this will change the state of the Synth.
|
||||
func (buffer AudioBuffer) Fill(synth Synth) error {
|
||||
s, _, err := synth.Render(buffer, math.MaxInt32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("synth.Render failed: %v", err)
|
||||
}
|
||||
if s != len(buffer) {
|
||||
return errors.New("in AudioBuffer.Fill, synth.Render should have filled the whole buffer but did not")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wav converts an AudioBuffer into a valid WAV-file, returned as a []byte
|
||||
// array.
|
||||
//
|
||||
// If pcm16 is set to true, the samples in the WAV-file will be 16-bit signed
|
||||
// integers; otherwise the samples will be 32-bit floats
|
||||
func (buffer AudioBuffer) Wav(pcm16 bool) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
wavHeader(len(buffer)*2, pcm16, buf)
|
||||
err := buffer.rawToBuffer(pcm16, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Wav failed: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Raw converts an AudioBuffer into a raw audio file, returned as a []byte
|
||||
// array.
|
||||
//
|
||||
// If pcm16 is set to true, the samples will be 16-bit signed integers;
|
||||
// otherwise the samples will be 32-bit floats
|
||||
func (buffer AudioBuffer) Raw(pcm16 bool) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := buffer.rawToBuffer(pcm16, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Raw failed: %v", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (data AudioBuffer) rawToBuffer(pcm16 bool, buf *bytes.Buffer) error {
|
||||
var err error
|
||||
if pcm16 {
|
||||
int16data := make([][2]int16, len(data))
|
||||
for i, v := range data {
|
||||
int16data[i][0] = int16(clamp(int(v[0]*math.MaxInt16), math.MinInt16, math.MaxInt16))
|
||||
int16data[i][1] = int16(clamp(int(v[1]*math.MaxInt16), math.MinInt16, math.MaxInt16))
|
||||
}
|
||||
err = binary.Write(buf, binary.LittleEndian, int16data)
|
||||
} else {
|
||||
err = binary.Write(buf, binary.LittleEndian, data)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wavHeader writes a wave header for either float32 or int16 .wav file into the
|
||||
// bytes.buffer. It needs to know the length of the buffer and assumes stereo
|
||||
// sound, so the length in stereo samples (L + R) is bufferlength / 2. If pcm16
|
||||
// = true, then the header is for int16 audio; pcm16 = false means the header is
|
||||
// for float32 audio. Assumes 44100 Hz sample rate.
|
||||
func wavHeader(bufferLength int, pcm16 bool, buf *bytes.Buffer) {
|
||||
// Refer to: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
|
||||
numChannels := 2
|
||||
sampleRate := 44100
|
||||
var bytesPerSample, chunkSize, fmtChunkSize, waveFormat int
|
||||
var factChunk bool
|
||||
if pcm16 {
|
||||
bytesPerSample = 2
|
||||
chunkSize = 36 + bytesPerSample*bufferLength
|
||||
fmtChunkSize = 16
|
||||
waveFormat = 1 // PCM
|
||||
factChunk = false
|
||||
} else {
|
||||
bytesPerSample = 4
|
||||
chunkSize = 50 + bytesPerSample*bufferLength
|
||||
fmtChunkSize = 18
|
||||
waveFormat = 3 // IEEE float
|
||||
factChunk = true
|
||||
}
|
||||
buf.Write([]byte("RIFF"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(chunkSize))
|
||||
buf.Write([]byte("WAVE"))
|
||||
buf.Write([]byte("fmt "))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(fmtChunkSize))
|
||||
binary.Write(buf, binary.LittleEndian, uint16(waveFormat))
|
||||
binary.Write(buf, binary.LittleEndian, uint16(numChannels))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(sampleRate))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(sampleRate*numChannels*bytesPerSample)) // avgBytesPerSec
|
||||
binary.Write(buf, binary.LittleEndian, uint16(numChannels*bytesPerSample)) // blockAlign
|
||||
binary.Write(buf, binary.LittleEndian, uint16(8*bytesPerSample)) // bits per sample
|
||||
if fmtChunkSize > 16 {
|
||||
binary.Write(buf, binary.LittleEndian, uint16(0)) // size of extension
|
||||
}
|
||||
if factChunk {
|
||||
buf.Write([]byte("fact"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(4)) // fact chunk size
|
||||
binary.Write(buf, binary.LittleEndian, uint32(bufferLength)) // sample length
|
||||
}
|
||||
buf.Write([]byte("data"))
|
||||
binary.Write(buf, binary.LittleEndian, uint32(bytesPerSample*bufferLength))
|
||||
}
|
||||
|
||||
func clamp(value, min, max int) int {
|
||||
if value < min {
|
||||
return min
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
7
cmd/main_synther_native.go
Normal file
7
cmd/main_synther_native.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build native
|
||||
|
||||
package cmd
|
||||
|
||||
import "github.com/vsariola/sointu/vm/compiler/bridge"
|
||||
|
||||
var MainSynther = bridge.NativeSynther{}
|
||||
7
cmd/main_synther_not_native.go
Normal file
7
cmd/main_synther_not_native.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build !native
|
||||
|
||||
package cmd
|
||||
|
||||
import "github.com/vsariola/sointu/vm"
|
||||
|
||||
var MainSynther = vm.GoSynther{}
|
||||
@ -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)
|
||||
}
|
||||
@ -21,7 +21,6 @@ func main() {
|
||||
help := flag.Bool("h", false, "Show help.")
|
||||
directory := flag.String("o", "", "Directory where to output all files. The directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
|
||||
play := flag.Bool("p", false, "Play the input songs (default behaviour when no other output is defined).")
|
||||
unreleased := flag.Bool("u", false, "Start song with all oscillators unreleased.")
|
||||
//start := flag.Float64("start", 0, "Start playing from part; given in the units defined by parameter `unit`.")
|
||||
//stop := flag.Float64("stop", -1, "Stop playing at part; given in the units defined by parameter `unit`. Negative values indicate render until end.")
|
||||
//units := flag.String("unit", "pattern", "Units for parameters start and stop. Possible values: second, sample, pattern, beat. Warning: beat and pattern do not take SPEED modulations into account.")
|
||||
@ -88,16 +87,7 @@ func main() {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
synth, err := bridge.Synth(song.Patch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create synth based on the patch: %v", err)
|
||||
}
|
||||
if !*unreleased {
|
||||
for i := 0; i < 32; i++ {
|
||||
synth.Release(i)
|
||||
}
|
||||
}
|
||||
buffer, _, err := sointu.Play(synth, song) // render the song to calculate its length
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song, nil) // render the song to calculate its length
|
||||
if err != nil {
|
||||
return fmt.Errorf("sointu.Play failed: %v", err)
|
||||
}
|
||||
@ -109,7 +99,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
if *rawOut {
|
||||
raw, err := sointu.Raw(buffer, *pcm)
|
||||
raw, err := buffer.Raw(*pcm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate .raw file: %v", err)
|
||||
}
|
||||
@ -118,7 +108,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
if *wavOut {
|
||||
wav, err := sointu.Wav(buffer, *pcm)
|
||||
wav, err := buffer.Wav(*pcm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate .wav file: %v", err)
|
||||
}
|
||||
|
||||
@ -3,31 +3,94 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"gioui.org/app"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/cmd"
|
||||
"github.com/vsariola/sointu/oto"
|
||||
"github.com/vsariola/sointu/rpc"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/tracker/gioui"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type NullContext struct {
|
||||
}
|
||||
|
||||
func (NullContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
|
||||
return tracker.MIDINoteEvent{}, false
|
||||
}
|
||||
|
||||
func (NullContext) BPM() (bpm float64, ok bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
|
||||
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
|
||||
|
||||
func main() {
|
||||
syncAddress := flag.String("address", "", "remote RPC server where to send sync data")
|
||||
flag.Parse()
|
||||
var f *os.File
|
||||
if *cpuprofile != "" {
|
||||
var err error
|
||||
f, err = os.Create(*cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("could not start CPU profile: ", err)
|
||||
}
|
||||
}
|
||||
audioContext, err := oto.NewContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer audioContext.Close()
|
||||
var syncChannel chan<- []float32
|
||||
if *syncAddress != "" {
|
||||
syncChannel, err = rpc.Sender(*syncAddress)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
recoveryFile := ""
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery")
|
||||
}
|
||||
synthService := vm.SynthService{}
|
||||
gioui.Main(audioContext, synthService, syncChannel)
|
||||
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
|
||||
if a := flag.Args(); len(a) > 0 {
|
||||
f, err := os.Open(a[0])
|
||||
if err == nil {
|
||||
model.ReadSong(f)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
tracker := gioui.NewTracker(model)
|
||||
output := audioContext.Output()
|
||||
defer output.Close()
|
||||
go func() {
|
||||
buf := make(sointu.AudioBuffer, 1024)
|
||||
ctx := NullContext{}
|
||||
for {
|
||||
player.Process(buf, ctx)
|
||||
output.WriteAudio(buf)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
tracker.Main()
|
||||
if *cpuprofile != "" {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
}
|
||||
if *memprofile != "" {
|
||||
f, err := os.Create(*memprofile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create memory profile: ", err)
|
||||
}
|
||||
defer f.Close() // error handling omitted for example
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
log.Fatal("could not write memory profile: ", err)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
app.Main()
|
||||
}
|
||||
|
||||
136
cmd/sointu-vsti/main.go
Normal file
136
cmd/sointu-vsti/main.go
Normal file
@ -0,0 +1,136 @@
|
||||
//go:build plugin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/cmd"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/tracker/gioui"
|
||||
"pipelined.dev/audio/vst2"
|
||||
)
|
||||
|
||||
type VSTIProcessContext struct {
|
||||
events []vst2.MIDIEvent
|
||||
eventIndex int
|
||||
host vst2.Host
|
||||
}
|
||||
|
||||
func (c *VSTIProcessContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
|
||||
for c.eventIndex < len(c.events) {
|
||||
ev := c.events[c.eventIndex]
|
||||
c.eventIndex++
|
||||
switch {
|
||||
case ev.Data[0] >= 0x80 && ev.Data[0] < 0x90:
|
||||
channel := ev.Data[0] - 0x80
|
||||
note := ev.Data[1]
|
||||
return tracker.MIDINoteEvent{Frame: int(ev.DeltaFrames), On: false, Channel: int(channel), Note: note}, true
|
||||
case ev.Data[0] >= 0x90 && ev.Data[0] < 0xA0:
|
||||
channel := ev.Data[0] - 0x90
|
||||
note := ev.Data[1]
|
||||
return tracker.MIDINoteEvent{Frame: int(ev.DeltaFrames), On: true, Channel: int(channel), Note: note}, true
|
||||
default:
|
||||
// ignore all other MIDI messages
|
||||
}
|
||||
}
|
||||
return tracker.MIDINoteEvent{}, false
|
||||
}
|
||||
|
||||
func (c *VSTIProcessContext) BPM() (bpm float64, ok bool) {
|
||||
timeInfo := c.host.GetTimeInfo(vst2.TempoValid)
|
||||
if timeInfo == nil || timeInfo.Flags&vst2.TempoValid == 0 || timeInfo.Tempo == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return timeInfo.Tempo, true
|
||||
}
|
||||
|
||||
func init() {
|
||||
var (
|
||||
version = int32(100)
|
||||
)
|
||||
vst2.PluginAllocator = func(h vst2.Host) (vst2.Plugin, vst2.Dispatcher) {
|
||||
recoveryFile := ""
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
randBytes := make([]byte, 16)
|
||||
rand.Read(randBytes)
|
||||
recoveryFile = filepath.Join(configDir, "sointu", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes))
|
||||
}
|
||||
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
|
||||
t := gioui.NewTracker(model)
|
||||
tracker.Bool{BoolData: (*tracker.InstrEnlarged)(model)}.Set(true)
|
||||
if s := h.GetSampleRate(); math.Abs(float64(h.GetSampleRate()-44100.0)) > 1e-6 {
|
||||
model.Alerts().AddAlert(tracker.Alert{
|
||||
Message: fmt.Sprintf("VSTi host sample rate is %.0f Hz; sointu supports 44100 Hz only", s),
|
||||
Priority: tracker.Error,
|
||||
Duration: 10 * time.Second,
|
||||
})
|
||||
}
|
||||
go t.Main()
|
||||
context := VSTIProcessContext{host: h}
|
||||
buf := make(sointu.AudioBuffer, 1024)
|
||||
return vst2.Plugin{
|
||||
UniqueID: PLUGIN_ID,
|
||||
Version: version,
|
||||
InputChannels: 0,
|
||||
OutputChannels: 2,
|
||||
Name: PLUGIN_NAME,
|
||||
Vendor: "vsariola/sointu",
|
||||
Category: vst2.PluginCategorySynth,
|
||||
Flags: vst2.PluginIsSynth,
|
||||
ProcessFloatFunc: func(in, out vst2.FloatBuffer) {
|
||||
left := out.Channel(0)
|
||||
right := out.Channel(1)
|
||||
if len(buf) < out.Frames {
|
||||
buf = append(buf, make(sointu.AudioBuffer, out.Frames-len(buf))...)
|
||||
}
|
||||
buf = buf[:out.Frames]
|
||||
player.Process(buf, &context)
|
||||
for i := 0; i < out.Frames; i++ {
|
||||
left[i], right[i] = buf[i][0], buf[i][1]
|
||||
}
|
||||
context.events = context.events[:0] // reset buffer, but keep the allocated memory
|
||||
context.eventIndex = 0
|
||||
},
|
||||
}, vst2.Dispatcher{
|
||||
CanDoFunc: func(pcds vst2.PluginCanDoString) vst2.CanDoResponse {
|
||||
switch pcds {
|
||||
case vst2.PluginCanReceiveEvents, vst2.PluginCanReceiveMIDIEvent, vst2.PluginCanReceiveTimeInfo:
|
||||
return vst2.YesCanDo
|
||||
}
|
||||
return vst2.NoCanDo
|
||||
},
|
||||
ProcessEventsFunc: func(ev *vst2.EventsPtr) {
|
||||
for i := 0; i < ev.NumEvents(); i++ {
|
||||
a := ev.Event(i)
|
||||
switch v := a.(type) {
|
||||
case *vst2.MIDIEvent:
|
||||
context.events = append(context.events, *v)
|
||||
}
|
||||
}
|
||||
},
|
||||
CloseFunc: func() {
|
||||
t.Exec() <- func() { t.ForceQuit().Do() }
|
||||
t.WaitQuitted()
|
||||
},
|
||||
GetChunkFunc: func(isPreset bool) []byte {
|
||||
retChn := make(chan []byte)
|
||||
t.Exec() <- func() { retChn <- t.MarshalRecovery() }
|
||||
return <-retChn
|
||||
},
|
||||
SetChunkFunc: func(data []byte, isPreset bool) {
|
||||
t.Exec() <- func() { t.UnmarshalRecovery(data) }
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func main() {}
|
||||
6
cmd/sointu-vsti/nameid_native.go
Normal file
6
cmd/sointu-vsti/nameid_native.go
Normal file
@ -0,0 +1,6 @@
|
||||
//go:build native
|
||||
|
||||
package main
|
||||
|
||||
var PLUGIN_ID = [4]byte{'S', 'n', 't', 'N'}
|
||||
var PLUGIN_NAME = "Sointu Native"
|
||||
6
cmd/sointu-vsti/nameid_not_native.go
Normal file
6
cmd/sointu-vsti/nameid_not_native.go
Normal file
@ -0,0 +1,6 @@
|
||||
//go:build !native
|
||||
|
||||
package main
|
||||
|
||||
var PLUGIN_ID = [4]byte{'S', 'n', 't', 'u'}
|
||||
var PLUGIN_NAME = "Sointu"
|
||||
1
examples/CMakeLists.txt
Normal file
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.unix.c
|
||||
physics_girl_st.h
|
||||
)
|
||||
target_link_libraries(cplay PRIVATE asound pthread)
|
||||
target_link_options(cplay PRIVATE -z noexecstack -no-pie)
|
||||
target_link_libraries(cplay PRIVATE physics_girl_st)
|
||||
target_include_directories(cplay PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
add_dependencies(examples cplay)
|
||||
endif()
|
||||
|
||||
add_executable(cwav
|
||||
cwav.c
|
||||
physics_girl_st.h
|
||||
)
|
||||
if(WIN32)
|
||||
target_compile_definitions(cwav PRIVATE _CRT_SECURE_NO_WARNINGS)
|
||||
elseif(UNIX)
|
||||
target_link_options(cwav PRIVATE -z noexecstack -no-pie)
|
||||
endif()
|
||||
target_link_libraries(cwav PRIVATE physics_girl_st)
|
||||
target_include_directories(cwav PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
add_dependencies(examples cwav)
|
||||
39
examples/code/C/cplay.unix.c
Normal file
39
examples/code/C/cplay.unix.c
Normal file
@ -0,0 +1,39 @@
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include "physics_girl_st.h"
|
||||
|
||||
static SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
|
||||
static snd_pcm_t *pcm_handle;
|
||||
static pthread_t render_thread;
|
||||
static uint32_t render_thread_handle;
|
||||
|
||||
int main(int argc, char **args) {
|
||||
// Unix does not have gm.dls, no need to ifdef and setup here.
|
||||
|
||||
// We render in the background while playing already.
|
||||
render_thread_handle = pthread_create(&render_thread, 0, (void * (*)(void *))su_render_song, sound_buffer);
|
||||
|
||||
// We can't start playing too early or the missing samples will be audible.
|
||||
sleep(2.);
|
||||
|
||||
// Play the track.
|
||||
snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
|
||||
snd_pcm_set_params(
|
||||
pcm_handle,
|
||||
#ifdef SU_SAMPLE_FLOAT
|
||||
SND_PCM_FORMAT_FLOAT,
|
||||
#else // SU_SAMPLE_FLOAT
|
||||
SND_PCM_FORMAT_S16_LE,
|
||||
#endif // SU_SAMPLE_FLOAT
|
||||
SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
SU_CHANNEL_COUNT,
|
||||
SU_SAMPLE_RATE,
|
||||
0,
|
||||
SU_LENGTH_IN_SAMPLES
|
||||
);
|
||||
snd_pcm_writei(pcm_handle, sound_buffer, SU_LENGTH_IN_SAMPLES);
|
||||
|
||||
return 0;
|
||||
}
|
||||
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;
|
||||
}
|
||||
63
examples/code/C/cplay.windows.winmm.c
Normal file
63
examples/code/C/cplay.windows.winmm.c
Normal file
@ -0,0 +1,63 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include "physics_girl_st.h"
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_EXTRA_LEAN
|
||||
#include <Windows.h>
|
||||
#include "mmsystem.h"
|
||||
#include "mmreg.h"
|
||||
|
||||
SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
|
||||
HWAVEOUT wave_out_handle;
|
||||
WAVEFORMATEX wave_format = {
|
||||
#ifdef SU_SAMPLE_FLOAT
|
||||
WAVE_FORMAT_IEEE_FLOAT,
|
||||
#else
|
||||
WAVE_FORMAT_PCM,
|
||||
#endif
|
||||
SU_CHANNEL_COUNT,
|
||||
SU_SAMPLE_RATE,
|
||||
SU_SAMPLE_RATE * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
|
||||
SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
|
||||
SU_SAMPLE_SIZE*8,
|
||||
0
|
||||
};
|
||||
WAVEHDR wave_header = {
|
||||
(LPSTR)sound_buffer,
|
||||
SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
|
||||
0,
|
||||
0,
|
||||
WHDR_PREPARED,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
};
|
||||
MMTIME mmtime = {
|
||||
TIME_SAMPLES,
|
||||
0
|
||||
};
|
||||
|
||||
int main(int argc, char **args) {
|
||||
// Load gm.dls if necessary.
|
||||
#ifdef SU_LOAD_GMDLS
|
||||
su_load_gmdls();
|
||||
#endif // SU_LOAD_GMDLS
|
||||
|
||||
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)su_render_song, sound_buffer, 0, 0);
|
||||
|
||||
// We render in the background while playing already. Fortunately,
|
||||
// Windows is slow with the calls below, so we're not worried that
|
||||
// we don't have enough samples ready before the track starts.
|
||||
waveOutOpen(&wave_out_handle, WAVE_MAPPER, &wave_format, 0, 0, CALLBACK_NULL);
|
||||
waveOutWrite(wave_out_handle, &wave_header, sizeof(wave_header));
|
||||
|
||||
// We need to handle windows messages properly while playing, as waveOutWrite is async.
|
||||
for(MSG msg = {0}; mmtime.u.sample != SU_LENGTH_IN_SAMPLES; waveOutGetPosition(wave_out_handle, &mmtime, sizeof(MMTIME))) {
|
||||
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageA(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
72
examples/code/C/cwav.c
Normal file
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.
BIN
examples/fourklang_instruments/snare4.4ki
Normal file
BIN
examples/fourklang_instruments/snare4.4ki
Normal file
Binary file not shown.
BIN
examples/fourklang_instruments/snare5.4ki
Normal file
BIN
examples/fourklang_instruments/snare5.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