9 Commits

Author SHA1 Message Date
36df18e2ae try making tiny link button next to each parameter 2024-09-22 09:31:13 +03:00
6aa6d8813c draft: move parameter unit conversions to UnitParameter table 2024-09-22 09:31:13 +03:00
964b2adbab further drafting 2024-09-22 09:31:13 +03:00
bd20440661 draft parameteter linking to vst 2024-09-22 09:31:13 +03:00
ce673578fd fix(amd64-386): crash with sample-based oscillator in 32-bit library 2024-09-22 09:30:42 +03:00
0e10cd2ae8 fix(amd64-386): sample oscillator hard crash
The sample-based oscillators converted the samplepos to an integer
and did samplepos < loop_end comparison to check if we are past
looping. Unfortunately, the < comparison was done in signed math.
Normally, this should never happen, but if the x87 FPU stack
overflowed exactly at right position, we then got 0x80000000 in
samplepos, which is equal to -2147483648. Thus, we considered that
sample is not looping and read the sample table at position
-2147483648, well out of bound. TL;DR changing jl to jb makes sure
we always wrap within to sample table, no matter what.

Fixes #149.
2024-09-22 09:04:47 +03:00
4ee355bb45 fix(tracker/gioui): DPI scaling of the numeric updown icons
Closes #150.
2024-09-21 14:01:32 +03:00
7d6daba3d2 fix(vm/compiler/bridge): empty patch should not crash native synth
Fixes #148.
2024-09-16 19:58:23 +03:00
2b38e11643 feat: include version info in the binaries 2024-09-15 19:45:00 +03:00
22 changed files with 518 additions and 200 deletions

View File

@ -42,7 +42,8 @@ jobs:
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-track.exe
params: -ldflags -H=windowsgui cmd/sointu-track/main.go
params: cmd/sointu-track/main.go
ldflags: -H=windowsgui
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-compile.exe
@ -50,7 +51,8 @@ jobs:
- 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
params: -tags=native cmd/sointu-track/main.go
ldflags: -H=windowsgui
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-vsti.dll
@ -103,6 +105,8 @@ jobs:
length: 7
- uses: lukka/get-cmake@latest
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5 # has to be after checkout, see https://medium.com/@s0k0mata/github-actions-and-go-the-new-cache-feature-in-actions-setup-go-v4-and-what-to-watch-out-for-aeea373ed07d
with:
go-version: '>=1.21.0'
@ -122,7 +126,7 @@ jobs:
ninja sointu
- name: Build binary
run: |
go build -o ${{ matrix.config.output }} ${{ matrix.config.params }}
go build -ldflags "-X github.com/vsariola/sointu/version.Version=$(git describe) ${{ matrix.config.ldflags}}" -o ${{ matrix.config.output }} ${{ matrix.config.params }}
- name: Upload binary
uses: actions/upload-artifact@v4
with:

View File

@ -3,6 +3,21 @@ 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]
### Added
- Include version info in the binaries, as given be `git describe`. This version
info is shown as a label in the tracker and can be checked with `-v` flag in
the command line tools.
### Fixed
- Crashes with sample-based oscillators in the 32-bit library, as the pointer to
sample-table (edi) got accidentally overwritten by detune
- Sample-based oscillators could hard crash if a x87 stack overflow happened
when calculating the current position in the sample ([#149][i149])
- Numeric updown widget calculated dp-to-px conversion incorrectly, resulting in
wrong scaling ([#150][i150])
- Empty patch should not crash the native synth ([#148][i148])
## [0.4.1]
### Added
- Clicking the parameter slider also selects that parameter ([#112][i112])
@ -203,3 +218,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[i144]: https://github.com/vsariola/sointu/issues/144
[i145]: https://github.com/vsariola/sointu/issues/145
[i146]: https://github.com/vsariola/sointu/issues/146
[i148]: https://github.com/vsariola/sointu/issues/148
[i149]: https://github.com/vsariola/sointu/issues/149
[i150]: https://github.com/vsariola/sointu/issues/150

View File

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

View File

@ -13,6 +13,7 @@ import (
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/version"
"github.com/vsariola/sointu/vm/compiler/bridge"
)
@ -27,8 +28,13 @@ func main() {
rawOut := flag.Bool("r", false, "Output the rendered song as .raw file. By default, saves stereo float32 buffer to disk.")
wavOut := flag.Bool("w", false, "Output the rendered song as .wav file. By default, saves stereo float32 buffer to disk.")
pcm := flag.Bool("c", false, "Convert audio to 16-bit signed PCM when outputting.")
versionFlag := flag.Bool("v", false, "Print version.")
flag.Usage = printUsage
flag.Parse()
if *versionFlag {
fmt.Println(version.VersionOrHash)
os.Exit(0)
}
if flag.NArg() == 0 || *help {
flag.Usage()
os.Exit(0)

View File

@ -28,6 +28,14 @@ func (NullContext) BPM() (bpm float64, ok bool) {
return 0, false
}
func (NullContext) Params() (ret tracker.ExtValueArray, ok bool) {
return tracker.ExtValueArray{}, false
}
func (NullContext) SetParams(params tracker.ExtParamArray) bool {
return false
}
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")

View File

@ -9,6 +9,7 @@ import (
"math"
"os"
"path/filepath"
"strconv"
"time"
"github.com/vsariola/sointu"
@ -22,6 +23,7 @@ type VSTIProcessContext struct {
events []vst2.MIDIEvent
eventIndex int
host vst2.Host
parameters []*vst2.Parameter
}
func (c *VSTIProcessContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
@ -52,6 +54,45 @@ func (c *VSTIProcessContext) BPM() (bpm float64, ok bool) {
return timeInfo.Tempo, true
}
func (c *VSTIProcessContext) Params() (ret tracker.ExtValueArray, ok bool) {
for i, p := range c.parameters {
ret[i] = p.Value
}
return ret, true
}
func (c *VSTIProcessContext) SetParams(a tracker.ExtParamArray) bool {
changed := false
for i, p := range c.parameters {
i := i
name := a[i].Param.Name
if name == "" {
name = fmt.Sprintf("P%d", i)
}
if p.Value != a[i].Val || p.Name != name {
p.Value = a[i].Val
p.Name = name
p.GetValueFunc = func(value float32) float32 {
return float32(a[i].Param.MinValue) + value*float32(a[i].Param.MaxValue-a[i].Param.MinValue)
}
p.GetValueLabelFunc = func(v float32) string {
if f := a[i].Param.DisplayFunc; f != nil {
s, u := a[i].Param.DisplayFunc(int(v + .5))
c.parameters[i].Unit = u
return s
}
c.parameters[i].Unit = ""
return strconv.Itoa(int(v + .5))
}
changed = true
}
}
if changed {
c.host.UpdateDisplay()
}
return true
}
func init() {
var (
version = int32(100)
@ -74,7 +115,15 @@ func init() {
})
}
go t.Main()
context := VSTIProcessContext{host: h}
parameters := make([]*vst2.Parameter, 0, tracker.ExtParamCount)
for i := 0; i < tracker.ExtParamCount; i++ {
parameters = append(parameters,
&vst2.Parameter{
Name: fmt.Sprintf("P%d", i),
NotAutomated: true,
})
}
context := VSTIProcessContext{host: h, parameters: parameters}
buf := make(sointu.AudioBuffer, 1024)
return vst2.Plugin{
UniqueID: PLUGIN_ID,
@ -85,6 +134,7 @@ func init() {
Vendor: "vsariola/sointu",
Category: vst2.PluginCategorySynth,
Flags: vst2.PluginIsSynth,
Parameters: parameters,
ProcessFloatFunc: func(in, out vst2.FloatBuffer) {
left := out.Channel(0)
right := out.Channel(1)

2
go.mod
View File

@ -2,6 +2,8 @@ module github.com/vsariola/sointu
go 1.21
replace pipelined.dev/audio/vst2 => ../vst2
require (
gioui.org v0.5.0
gioui.org/x v0.5.0

233
patch.go
View File

@ -5,6 +5,7 @@ import (
"fmt"
"math"
"sort"
"strconv"
)
type (
@ -57,7 +58,10 @@ type (
MaxValue int // maximum value of the parameter, inclusive
CanSet bool // if this parameter can be set before hand i.e. through the gui
CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit
DisplayFunc UnitParameterDisplayFunc
}
UnitParameterDisplayFunc func(int) (value string, unit string)
)
// UnitTypes documents all the available unit types and if they support stereo variant
@ -79,7 +83,7 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"crush": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(24 * float64(v) / 128), "bits" }}},
"gain": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
@ -88,7 +92,7 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"dbgain": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "decibels", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
{Name: "decibels", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(40 * (float64(v)/64 - 1)), "dB" }}},
"filter": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
@ -108,15 +112,15 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "notetracking", MinValue: 0, MaxValue: 2, CanSet: true, CanModulate: false},
{Name: "notetracking", MinValue: 0, MaxValue: 2, CanSet: true, CanModulate: false, DisplayFunc: arrDispFunc(noteTrackingNames[:])},
{Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"compressor": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(1 - float64(v)/128), "" }}},
"speed": []UnitParameter{},
"out": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
@ -128,20 +132,20 @@ var UnitTypes = map[string]([]UnitParameter){
"aux": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false, DisplayFunc: arrDispFunc(channelNames[:])}},
"send": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(float64(v)/64 - 1), "" }},
{Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false},
{Name: "target", MinValue: 0, MaxValue: math.MaxInt32, CanSet: true, CanModulate: false},
{Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false},
{Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"envelope": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
{Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"noise": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
@ -149,14 +153,14 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"oscillator": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: oscillatorTransposeDispFunc},
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(float64(v-64) / 64), "st" }},
{Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "frequency", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false},
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false, DisplayFunc: arrDispFunc(oscTypes[:])},
{Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false},
{Name: "samplestart", MinValue: 0, MaxValue: 1720329, CanSet: true, CanModulate: false},
@ -164,17 +168,59 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}},
"loadval": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(float64(v)/64 - 1), "" }}},
"receive": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
{Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"in": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false, DisplayFunc: arrDispFunc(channelNames[:])}},
"sync": []UnitParameter{},
}
var channelNames = [...]string{"left", "right", "aux1 left", "aux1 right", "aux2 left", "aux2 right", "aux3 left", "aux3 right"}
var noteTrackingNames = [...]string{"fixed", "pitch", "BPM"}
var oscTypes = [...]string{"sine", "trisaw", "pulse", "gate", "sample"}
func arrDispFunc(arr []string) UnitParameterDisplayFunc {
return func(v int) (string, string) {
if v < 0 || v >= len(arr) {
return "???", ""
}
return arr[v], ""
}
}
func compressorTimeDispFunc(v int) (string, string) {
alpha := math.Pow(2, -24*float64(v)/128) // alpha is the "smoothing factor" of first order low pass iir
sec := -1 / (44100 * math.Log(1-alpha)) // from smoothing factor to time constant, https://en.wikipedia.org/wiki/Exponential_smoothing
return engineeringTime(sec)
}
func oscillatorTransposeDispFunc(v int) (string, string) {
relvalue := v - 64
octaves := relvalue / 12
semitones := relvalue % 12
if semitones == 0 {
return strconv.Itoa(octaves), "oct"
}
return strconv.Itoa(semitones), "st"
}
func engineeringTime(sec float64) (string, string) {
if sec < 1e-3 {
return fmt.Sprintf("%.2f", sec*1e6), "us"
} else if sec < 1 {
return fmt.Sprintf("%.2f", sec*1e3), "ms"
}
return fmt.Sprintf("%.2f", sec), "s"
}
func formatFloat(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
// When unit.Type = "oscillator", its unit.Parameter["Type"] tells the type of
// the oscillator. There is five different oscillator types, so these consts
// just enumerate them.
@ -374,158 +420,3 @@ func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) {
}
return 0, 0, fmt.Errorf("could not find a unit with id %v", id)
}
// ParamHintString returns a human readable string representing the current
// value of a given unit parameter.
func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string {
if instrIndex < 0 || instrIndex >= len(p) {
return ""
}
instr := p[instrIndex]
if unitIndex < 0 || unitIndex >= len(instr.Units) {
return ""
}
unit := instr.Units[unitIndex]
value := unit.Parameters[param]
switch unit.Type {
case "envelope":
switch param {
case "attack":
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100)
case "decay":
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100 * (1 - float64(unit.Parameters["sustain"])/128))
case "release":
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100 * float64(unit.Parameters["sustain"]) / 128)
}
case "oscillator":
switch param {
case "type":
switch value {
case Sine:
return "Sine"
case Trisaw:
return "Trisaw"
case Pulse:
return "Pulse"
case Gate:
return "Gate"
case Sample:
return "Sample"
default:
return "Unknown"
}
case "transpose":
relvalue := value - 64
octaves := relvalue / 12
semitones := relvalue % 12
if octaves != 0 {
return fmt.Sprintf("%v oct, %v st", octaves, semitones)
}
return fmt.Sprintf("%v st", semitones)
case "detune":
return fmt.Sprintf("%v st", float32(value-64)/64.0)
}
case "compressor":
switch param {
case "attack":
fallthrough
case "release":
alpha := math.Pow(2, -24*float64(value)/128) // alpha is the "smoothing factor" of first order low pass iir
sec := -1 / (44100 * math.Log(1-alpha)) // from smoothing factor to time constant, https://en.wikipedia.org/wiki/Exponential_smoothing
return engineeringTime(sec)
case "ratio":
return fmt.Sprintf("1 : %.3f", 1-float64(value)/128)
}
case "loadval":
switch param {
case "value":
return fmt.Sprintf("%.2f", float32(value)/64-1)
}
case "send":
switch param {
case "amount":
return fmt.Sprintf("%.2f", float32(value)/64-1)
case "voice":
if value == 0 {
targetIndex, _, err := p.FindUnit(unit.Parameters["target"])
if err == nil && targetIndex != instrIndex {
return "all"
}
return "self"
}
return fmt.Sprintf("%v", value)
case "target":
instrIndex, unitIndex, err := p.FindUnit(unit.Parameters["target"])
if err != nil {
return "invalid target"
}
instr := p[instrIndex]
unit := instr.Units[unitIndex]
return fmt.Sprintf("%v / %v%v", instr.Name, unit.Type, unitIndex)
case "port":
instrIndex, unitIndex, err := p.FindUnit(unit.Parameters["target"])
if err != nil {
return fmt.Sprintf("%v ???", value)
}
portList := Ports[p[instrIndex].Units[unitIndex].Type]
if value < 0 || value >= len(portList) {
return fmt.Sprintf("%v ???", value)
}
return fmt.Sprintf(portList[value])
}
case "delay":
switch param {
case "notetracking":
switch value {
case 0:
return "fixed"
case 1:
return "tracks pitch"
case 2:
return "tracks BPM"
}
}
case "in", "aux":
switch param {
case "channel":
switch value {
case 0:
return "left"
case 1:
return "right"
case 2:
return "aux1 left"
case 3:
return "aux1 right"
case 4:
return "aux2 left"
case 5:
return "aux2 right"
case 6:
return "aux3 left"
case 7:
return "aux3 right"
}
}
case "dbgain":
switch param {
case "decibels":
return fmt.Sprintf("%.2f dB", 40*(float32(value)/64-1))
}
case "crush":
switch param {
case "resolution":
return fmt.Sprintf("%v bits", 24*float32(value)/128)
}
}
return ""
}
func engineeringTime(sec float64) string {
if sec < 1e-3 {
return fmt.Sprintf("%.2f us", sec*1e6)
} else if sec < 1 {
return fmt.Sprintf("%.2f ms", sec*1e3)
}
return fmt.Sprintf("%.2f s", sec)
}

View File

@ -181,3 +181,8 @@ target_compile_definitions(test_render_samples PUBLIC TEST_HEADER="test_render_s
add_executable(test_render_samples_api test_render_samples_api.c)
target_link_libraries(test_render_samples_api ${STATICLIB})
add_test(test_render_samples_api test_render_samples_api)
add_executable(test_oscillator_crash test_oscillator_crash.c)
target_link_libraries(test_oscillator_crash ${STATICLIB})
add_test(test_oscillator_crash test_oscillator_crash)

View File

@ -0,0 +1,55 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sointu.h>
#define BPM 100
#define SAMPLE_RATE 44100
#define LENGTH_IN_ROWS 16
#define SAMPLES_PER_ROW SAMPLE_RATE * 4 * 60 / (BPM * 16)
const int su_max_samples = SAMPLES_PER_ROW * LENGTH_IN_ROWS;
int main(int argc, char* argv[])
{
Synth* synth;
float* buffer;
// The patch is invalid and overflows the stack. This should still exit cleanly, but used to hard crash.
// See: https://github.com/vsariola/sointu/issues/149
const unsigned char opcodes[] = { SU_OSCILLATOR_ID + 1, // STEREO
SU_ADVANCE_ID };
const unsigned char operands[] = { 69, 74, 0, 0, 82, 128, 128 };
int errcode;
int time;
int samples;
int totalrendered;
int retval;
// initialize Synth
synth = (Synth*)malloc(sizeof(Synth));
memset(synth, 0, sizeof(Synth));
memcpy(synth->Opcodes, opcodes, sizeof(opcodes));
memcpy(synth->Operands, operands, sizeof(operands));
synth->NumVoices = 3;
synth->Polyphony = 6;
synth->RandSeed = 1;
synth->SampleOffsets[0].Start = 91507;
synth->SampleOffsets[0].LoopStart = 5448;
synth->SampleOffsets[0].LoopLength = 563;
// initialize Buffer
buffer = (float*)malloc(2 * sizeof(float) * su_max_samples);
// triger first voice
synth->SynthWrk.Voices[0].Note = 64;
synth->SynthWrk.Voices[0].Sustain = 1;
totalrendered = 0;
samples = su_max_samples;
time = INT32_MAX;
retval = 0;
errcode = su_render(synth, buffer, &samples, &time);
if (errcode != 0x1041) {
retval = 1;
printf("su_render should have return errcode 0x1401, got 0x%08x\n", errcode);
}
free(synth);
free(buffer);
return retval;
}

View File

@ -176,6 +176,7 @@ func (m *Model) Undo() Action {
m.undoStack = m.undoStack[:len(m.undoStack)-1]
m.prevUndoKind = ""
(*Model)(m).send(m.d.Song.Copy())
(*Model)(m).send(m.ExtParams())
},
}
}
@ -193,6 +194,7 @@ func (m *Model) Redo() Action {
m.redoStack = m.redoStack[:len(m.redoStack)-1]
m.prevUndoKind = ""
(*Model)(m).send(m.d.Song.Copy())
(*Model)(m).send(m.ExtParams())
},
}
}
@ -408,3 +410,30 @@ func (m *Model) completeAction(checkSave bool) {
m.dialog = NoDialog
}
}
func (m *Model) SetExtLink(index int) Action {
return Action{
do: func() {
defer m.change("SetExtLink", ExtParamLinkChange, MajorChange)()
p, _ := m.Params().SelectedItem().(NamedParameter)
for i := range p.m.d.ExtParamLinks {
if i == index {
continue
}
if p.m.d.ExtParamLinks[i].UnitID == p.unit.ID && p.m.d.ExtParamLinks[i].ParamName == p.up.Name {
p.m.d.ExtParamLinks[i] = ExtParamLink{}
}
}
if index > -1 {
p.m.d.ExtParamLinks[index] = ExtParamLink{UnitID: p.unit.ID, ParamName: p.up.Name}
}
},
allowed: func() bool {
if index < -1 || index >= len(m.d.ExtParamLinks) {
return false
}
_, ok := m.Params().SelectedItem().(NamedParameter)
return ok
},
}
}

View File

@ -128,7 +128,7 @@ func (s *NumericUpDownStyle) button(height int, icon *widget.Icon, delta int, cl
size = 1
}
if icon != nil {
p := gtx.Dp(unit.Dp(size))
p := size
if p < 1 {
p = 1
}

View File

@ -9,6 +9,7 @@ import (
"gioui.org/unit"
"gioui.org/widget"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/version"
"golang.org/x/exp/shiny/materialdesign/icons"
)
@ -92,7 +93,7 @@ func (t *SongPanel) layoutMenuBar(gtx C, tr *Tracker) D {
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.End}.Layout(gtx,
layout.Rigid(tr.layoutMenu(gtx, "File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200), t.fileMenuItems...)),
layout.Rigid(tr.layoutMenu(gtx, "Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200), t.editMenuItems...)),
)
@ -181,5 +182,10 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
)
}),
layout.Rigid(panicBtnStyle.Layout),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(func(gtx C) D {
labelStyle := LabelStyle{Text: version.VersionOrHash, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: tr.Theme.Shaper}
return labelStyle.Layout(gtx)
}),
)
}

View File

@ -24,27 +24,39 @@ import (
)
type UnitEditor struct {
sliderList *DragList
searchList *DragList
Parameters []*ParameterWidget
DeleteUnitBtn *ActionClickable
CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable
caser cases.Caser
sliderList *DragList
searchList *DragList
Parameters []*ParameterWidget
DeleteUnitBtn *ActionClickable
CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable
ExtLinkMenuItems []MenuItem
ExtLinkMenu Menu
caser cases.Caser
}
func NewUnitEditor(m *tracker.Model) *UnitEditor {
ret := &UnitEditor{
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable),
sliderList: NewDragList(m.Params().List(), layout.Vertical),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable),
sliderList: NewDragList(m.Params().List(), layout.Vertical),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
ExtLinkMenu: Menu{},
ExtLinkMenuItems: []MenuItem{{Text: "None", IconBytes: icons.HardwarePhoneLinkOff, Doer: m.SetExtLink(-1)}},
}
for i := 0; i < tracker.ExtParamCount; i++ {
ret.ExtLinkMenuItems = append(ret.ExtLinkMenuItems, MenuItem{
Text: fmt.Sprintf("P%v", i),
IconBytes: icons.HardwarePhoneLink,
Doer: m.SetExtLink(i),
})
}
ret.caser = cases.Title(language.English)
return ret
}
@ -104,6 +116,9 @@ func (pe *UnitEditor) layoutSliders(gtx C, t *Tracker) D {
}
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[index])
paramStyle.Focus = pe.sliderList.TrackerList.Selected() == index
if m, ok := pe.Parameters[index].Parameter.(tracker.NamedParameter); ok {
paramStyle.Linked = m.Linked() == index
}
dims := paramStyle.Layout(gtx)
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
}
@ -132,6 +147,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
text = pe.caser.String(text)
}
hintText := Label(text, white, t.Theme.Shaper)
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(deleteUnitBtnStyle.Layout),
layout.Rigid(copyUnitBtnStyle.Layout),
@ -214,6 +230,7 @@ type ParameterWidget struct {
unitBtn widget.Clickable
unitMenu Menu
Parameter tracker.Parameter
extLinkBtn TipClickable
}
type ParameterStyle struct {
@ -221,6 +238,7 @@ type ParameterStyle struct {
w *ParameterWidget
Theme *material.Theme
Focus bool
Linked bool
}
func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) ParameterStyle {
@ -233,6 +251,29 @@ func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) P
func (p ParameterStyle) Layout(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
ue := p.tracker.InstrumentEditor.unitEditor
extLinkMenuBtnStyle := TipIcon(p.Theme, &p.w.extLinkBtn, icons.ContentLink, "Link to external parameter")
if !p.Linked {
extLinkMenuBtnStyle.IconButtonStyle.Color = disabledTextColor
if !p.Focus {
extLinkMenuBtnStyle.IconButtonStyle.Color = transparent
}
}
dims := extLinkMenuBtnStyle.Layout(gtx)
if p.Focus {
m := PopupMenu(&ue.ExtLinkMenu, p.Theme.Shaper)
for p.w.extLinkBtn.Clickable.Clicked(gtx) {
m.Menu.Visible = true
}
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(200))
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
m.Layout(gtx, ue.ExtLinkMenuItems...)
}
return dims
}),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
return layout.E.Layout(gtx, Label(p.w.Parameter.Name(), white, p.tracker.Theme.Shaper))

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"os"
"path/filepath"
@ -11,6 +12,8 @@ import (
"github.com/vsariola/sointu/vm"
)
const ExtParamCount = 16
// Model implements the mutable state for the tracker program GUI.
//
// Go does not have immutable slices, so there's no efficient way to guarantee
@ -37,6 +40,7 @@ type (
RecoveryFilePath string
ChangedSinceRecovery bool
Loop Loop
ExtParamLinks [ExtParamCount]ExtParamLink
}
Model struct {
@ -106,6 +110,20 @@ type (
model *Model
}
// ExtParamLink is for linking VST parameters to the patch parameters. There's
// fixed number of parameters in the VST plugin and these are linked to
// particular parameters of a unit.
ExtParamLink struct {
UnitID int
ParamName string
}
ExtValueArray [ExtParamCount]float32
ExtParamArray [ExtParamCount]struct {
Val float32
Param sointu.UnitParameter
}
IsPlayingMsg struct{ bool }
StartPlayMsg struct{ sointu.SongPos }
BPMMsg struct{ int }
@ -133,6 +151,7 @@ const (
BPMChange
RowsPerBeatChange
LoopChange
ExtParamLinkChange
SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange
)
@ -247,6 +266,9 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
if m.changeType&LoopChange != 0 {
m.send(m.d.Loop)
}
if m.changeType&ExtParamLinkChange != 0 || m.changeType&PatchChange != 0 {
m.send(m.ExtParams())
}
m.undoSkipCounter++
var limit int
switch m.changeSeverity {
@ -322,6 +344,7 @@ func (m *Model) UnmarshalRecovery(bytes []byte) {
}
m.d.ChangedSinceRecovery = false
m.send(m.d.Song.Copy())
m.send(m.ExtParams())
m.send(m.d.Loop)
m.updatePatternUseCount()
}
@ -355,6 +378,8 @@ func (m *Model) ProcessPlayerMessage(msg PlayerMsg) {
m.Alerts().AddAlert(e)
case IsPlayingMsg:
m.playing = e.bool
case ExtValueArray:
m.SetExtValues(e)
default:
}
}
@ -389,6 +414,51 @@ func (m *Model) Instrument(index int) sointu.Instrument {
return m.d.Song.Patch[index].Copy()
}
func (m *Model) ExtParams() (params ExtParamArray) {
for i, l := range m.d.ExtParamLinks {
instrIndex, unitIndex, err := m.d.Song.Patch.FindUnit(l.UnitID)
if err != nil {
continue
}
unit := m.d.Song.Patch[instrIndex].Units[unitIndex]
if up, ok := sointu.UnitTypes[unit.Type]; ok {
for _, p := range up {
if p.Name == l.ParamName && p.CanSet && p.MaxValue > p.MinValue {
params[i].Val = float32(unit.Parameters[l.ParamName]-p.MinValue) / float32(p.MaxValue-p.MinValue)
params[i].Param = p
}
}
}
}
return
}
func (m *Model) SetExtValues(vals ExtValueArray) {
defer m.change("SetExtValues", PatchChange, MinorChange)()
changed := false
for i, l := range m.d.ExtParamLinks {
instrIndex, unitIndex, err := m.d.Song.Patch.FindUnit(l.UnitID)
if err != nil {
continue
}
unit := m.d.Song.Patch[instrIndex].Units[unitIndex]
if up, ok := sointu.UnitTypes[unit.Type]; ok {
for _, p := range up {
if p.Name == l.ParamName && p.CanSet && p.MaxValue > p.MinValue {
newVal := int(math.Round(float64(vals[i])*float64(p.MaxValue-p.MinValue))) + p.MinValue
if unit.Parameters[l.ParamName] != newVal {
unit.Parameters[l.ParamName] = newVal
changed = true
}
}
}
}
}
if !changed {
m.changeCancel = true
}
}
func (d *modelData) Copy() modelData {
ret := *d
ret.Song = d.Song.Copy()

View File

@ -21,6 +21,14 @@ func (NullContext) BPM() (bpm float64, ok bool) {
return 0, false
}
func (NullContext) Params() (params tracker.ExtValueArray, ok bool) {
return tracker.ExtValueArray{}, false
}
func (NullContext) SetParams(params tracker.ExtParamArray) bool {
return false
}
type modelFuzzState struct {
model *tracker.Model
clipboard []byte

View File

@ -162,13 +162,29 @@ func (p NamedParameter) Type() ParameterType {
func (p NamedParameter) Hint() string {
val := p.Value()
text := p.m.d.Song.Patch.ParamHintString(p.m.d.InstrIndex, p.m.d.UnitIndex, p.up.Name)
if text != "" {
text = fmt.Sprintf("%v / %v", val, text)
} else {
text = strconv.Itoa(val)
if p.up.DisplayFunc != nil {
valueInUnits, units := p.up.DisplayFunc(val)
return fmt.Sprintf("%d / %s %s", val, valueInUnits, units)
}
return text
if p.unit.Type == "send" && p.up.Name == "voice" && val == 0 {
targetIndex, _, err := p.m.FindUnit(p.unit.Parameters["target"])
if err == nil && targetIndex != p.m.d.InstrIndex {
return "all"
}
return "self"
}
if p.unit.Type == "send" && p.up.Name == "port" {
instrIndex, unitIndex, err := p.m.FindUnit(p.unit.Parameters["target"])
if err != nil {
return strconv.Itoa(val)
}
portList := sointu.Ports[p.m.d.Song.Patch[instrIndex].Units[unitIndex].Type]
if val < 0 || val >= len(portList) {
return strconv.Itoa(val)
}
return fmt.Sprintf(portList[val])
}
return strconv.Itoa(val)
}
func (p NamedParameter) LargeStep() int {
@ -178,6 +194,15 @@ func (p NamedParameter) LargeStep() int {
return 16
}
func (p NamedParameter) Linked() int {
for i, l := range p.m.d.ExtParamLinks {
if l.UnitID == p.unit.ID && l.ParamName == p.up.Name {
return i
}
}
return -1
}
// GmDlsEntryParameter
func (p GmDlsEntryParameter) Name() string { return "sample" }

View File

@ -25,6 +25,7 @@ type (
voiceLevels [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice
voices [vm.MAX_VOICES]voice
loop Loop
extParamValues ExtValueArray
recState recState // is the recording off; are we waiting for a note; or are we recording
recording Recording // the recorded MIDI events and BPM
@ -39,6 +40,8 @@ type (
PlayerProcessContext interface {
NextEvent() (event MIDINoteEvent, ok bool)
BPM() (bpm float64, ok bool)
Params() (params ExtValueArray, ok bool)
SetParams(params ExtParamArray) bool
}
// MIDINoteEvent is a MIDI event triggering or releasing a note. In
@ -225,6 +228,10 @@ func (p *Player) advanceRow() {
}
func (p *Player) processMessages(context PlayerProcessContext) {
if value, ok := context.Params(); ok && value != p.extParamValues {
p.extParamValues = value
p.send(p.extParamValues)
}
loop:
for { // process new message
select {
@ -293,6 +300,8 @@ loop:
}
p.recState = recStateNone
}
case ExtParamArray:
context.SetParams(m)
default:
// ignore unknown messages
}

37
version/version.go Normal file
View File

@ -0,0 +1,37 @@
package version
import "runtime/debug"
// You can set the version at build time using something like:
// go build -ldflags "-X github.com/vsariola/sointu/version.Version=$(git describe --dirty)"
var Version string
var Hash = func() string {
if info, ok := debug.ReadBuildInfo(); ok {
modified := false
for _, setting := range info.Settings {
if setting.Key == "vcs.modified" && setting.Value == "true" {
modified = true
break
}
}
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
shortHash := setting.Value[:7]
if modified {
return shortHash + "-dirty"
}
return shortHash
}
}
}
return ""
}()
var VersionOrHash = func() string {
if Version != "" {
return Version
}
return Hash
}()

View File

@ -38,6 +38,13 @@ func Synth(patch sointu.Patch, bpm int) (*NativeSynth, error) {
if len(comPatch.Operands) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return nil, errors.New("bridge supports at most 16384 operands; the compiled patch has more")
}
// if the patch is empty, we still need to initialize the synth with a single opcode
if len(comPatch.Opcodes) == 0 {
s.Opcodes[0] = 0
s.NumVoices = 1
s.Polyphony = 0
return (*NativeSynth)(s), nil
}
for i, v := range comPatch.Opcodes {
s.Opcodes[i] = (C.uchar)(v)
}
@ -130,6 +137,13 @@ func (bridgesynth *NativeSynth) Update(patch sointu.Patch, bpm int) error {
if len(comPatch.Operands) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return errors.New("bridge supports at most 16384 operands; the compiled patch has more")
}
// if the patch is empty, we still need to initialize the synth with a single opcode
if len(comPatch.Opcodes) == 0 {
s.Opcodes[0] = 0
s.NumVoices = 1
s.Polyphony = 0
return nil
}
needsRefresh := false
for i, v := range comPatch.Opcodes {
if cmdChar := (C.uchar)(v); s.Opcodes[i] != cmdChar {

View File

@ -28,6 +28,34 @@ const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
// const bufsize = su_max_samples * 2
func TestEmptyPatch(t *testing.T) {
patch := sointu.Patch{}
tracks := []sointu.Track{{NumVoices: 0, Order: []int{0}, Patterns: []sointu.Pattern{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}}}
song := sointu.Song{BPM: 100, RowsPerBeat: 4, Score: sointu.Score{RowsPerPattern: 16, Length: 1, Tracks: tracks}, Patch: patch}
// make sure that the empty patch does not crash the synth
sointu.Play(bridge.NativeSynther{}, song, nil)
}
func TestUpdatingEmptyPatch(t *testing.T) {
patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 95, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
}}}
tracks := []sointu.Track{{NumVoices: 0, Order: []int{0}, Patterns: []sointu.Pattern{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}}}
song := sointu.Song{BPM: 100, RowsPerBeat: 4, Score: sointu.Score{RowsPerPattern: 16, Length: 1, Tracks: tracks}, Patch: patch}
synth, err := bridge.NativeSynther{}.Synth(patch, song.BPM)
if err != nil {
t.Fatalf("Synth creation failed: %v", err)
}
synth.Update(sointu.Patch{}, song.BPM)
buffer := make(sointu.AudioBuffer, su_max_samples)
err = buffer[:len(buffer)/2].Fill(synth)
if err != nil {
t.Fatalf("render gave an error: %v", err)
}
}
func TestOscillatSine(t *testing.T) {
patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},

View File

@ -127,6 +127,9 @@ su_op_oscillat_mono:
{{- end}}
{{- if .SupportsParamValueOtherThan "oscillator" "unison" 0}}
{{.PushRegs .AX "" .WRK "OscWRK" .AX "OscFlags"}}
{{- if and (not .Amd64) .Library}}
push {{.AX}} ; pushregs is pushad in 32-bit, and pushes edi last, so decrease SP because library needs to save edi and we can store detune there
{{- end}}
fldz ; 0 d
fxch ; d a=0, "accumulated signal"
su_op_oscillat_unison_loop:
@ -147,6 +150,9 @@ su_op_oscillat_unison_loop:
dec eax
jmp short su_op_oscillat_unison_loop
su_op_oscillat_unison_out:
{{- if and (not .Amd64) .Library}}
pop {{.AX}} ; pushregs is pushad in 32-bit, and pushes edi last, so we inscrease SP to avoid destroying edi
{{- end}}
{{.PopRegs .AX .WRK .AX}}
ret
su_op_oscillat_single:
@ -338,7 +344,7 @@ su_oscillat_gate_bit: ; stack: 0/1, let's call it x
pop {{.DX}} ; edx is now the sample number
movzx ebx, word [{{.DI}} + 4] ; ecx = loopstart
sub edx, ebx ; if sample number < loop start
jl su_oscillat_sample_not_looping ; then we're not looping yet
jb su_oscillat_sample_not_looping ; then we're not looping yet
mov eax, edx ; eax = sample number
movzx ecx, word [{{.DI}} + 6] ; edi is now the loop length
xor edx, edx ; div wants edx to be empty