mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-12 17:14:43 -04:00
Compare commits
9 Commits
v0.4.1
...
feat/vsti-
| Author | SHA1 | Date | |
|---|---|---|---|
| 36df18e2ae | |||
| 6aa6d8813c | |||
| 964b2adbab | |||
| bd20440661 | |||
| ce673578fd | |||
| 0e10cd2ae8 | |||
| 4ee355bb45 | |||
| 7d6daba3d2 | |||
| 2b38e11643 |
10
.github/workflows/binaries.yml
vendored
10
.github/workflows/binaries.yml
vendored
@ -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:
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`")
|
||||
|
||||
|
||||
@ -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
2
go.mod
@ -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
233
patch.go
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
55
tests/test_oscillator_crash.c
Normal file
55
tests/test_oscillator_crash.c
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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
37
version/version.go
Normal 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
|
||||
}()
|
||||
@ -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 {
|
||||
|
||||
@ -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}},
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user