feat!: both native & Go synths are included in the same executables

Closes #200
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-07-10 17:46:00 +03:00
parent 13102aa7d6
commit 3163f46447
21 changed files with 106 additions and 83 deletions

View File

@ -39,33 +39,19 @@ jobs:
strategy:
matrix:
config:
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-track.exe
params: cmd/sointu-track/main.go
ldflags: -H=windowsgui
- 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
output: sointu-track.exe
params: -tags=native cmd/sointu-track/main.go
ldflags: -H=windowsgui
- os: windows-latest
asmnasm: C:\Users\runneradmin\nasm\nasm
output: sointu-vsti.dll
params: -buildmode=c-shared -tags=plugin ./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
@ -73,43 +59,27 @@ jobs:
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
output: sointu-track
params: -tags=native cmd/sointu-track/main.go
packages: libegl-dev libvulkan-dev libxkbcommon-x11-dev libwayland-dev libasound2-dev libx11-xcb-dev libxcursor-dev libxfixes-dev
- os: ubuntu-latest
asmnasm: /home/runner/nasm/nasm
output: sointu-vsti.so
params: -buildmode=c-shared -tags=plugin ./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-13
asmnasm: /Users/runner/nasm/nasm
output: sointu-track-native
output: sointu-track
params: -tags=native cmd/sointu-track/main.go
- os: macos-13
asmnasm: /Users/runner/nasm/nasm
output: sointu-vsti.a
bundleoutput: sointu-vsti
MACOSX_DEPLOYMENT_TARGET: 11
params: -buildmode=c-archive -tags=plugin ./cmd/sointu-vsti/
bundle: true
- os: macos-13
asmnasm: /Users/runner/nasm/nasm
output: sointu-vsti-native.a
bundleoutput: sointu-vsti-native
MACOSX_DEPLOYMENT_TARGET: 11
params: -buildmode=c-archive -tags="plugin,native" ./cmd/sointu-vsti/
bundle: true
steps:

View File

@ -109,6 +109,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
([#156][i156])
### Changed
- Native version of the tracker/VSTi was removed. Instead, you can change
between the two versions of the synth on the fly, by clicking on the "Synth"
option under the CPU group in the song panel ([#200][i200])
- Send amount defaults to 64 = 0.0 ([#178][i178])
- BREAKING CHANGE: the negbandpass and neghighpass parameters of the filter unit
were removed. Setting bandpass or highpass to -1 achieves now the same end
@ -360,3 +363,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[i186]: https://github.com/vsariola/sointu/issues/186
[i192]: https://github.com/vsariola/sointu/issues/192
[i196]: https://github.com/vsariola/sointu/issues/196
[i200]: https://github.com/vsariola/sointu/issues/200

View File

@ -82,7 +82,7 @@ for the audio, so the portability is currently limited by these.
#### Prerequisites
- [go](https://golang.org/)
- If you want to use the x86 assembly written synthesizer, to test that the
- If you want to also use the x86 assembly written synthesizer, to test that the
patch also works once compiled:
- Follow the instructions to build the [x86 native virtual machine](#native-virtual-machine)
before building the tracker.
@ -110,7 +110,7 @@ go build -o sointu-track.exe cmd/sointu-track/main.go
On other platforms than Windows, replace `-o sointu-track.exe` with
`-o sointu-track`.
If you want to use the [x86 native virtual machine](#native-virtual-machine),
If you want to include the [x86 native virtual machine](#native-virtual-machine),
add `-tags=native` to all the commands e.g.
```
@ -133,7 +133,7 @@ a dynamically linked library and ran inside a VST host.
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 x86 assembly written synthesizer:
- If you want to build the VSTI with the native x86 assembly written synthesizer:
- Follow the instructions to build the [x86 native virtual machine](#native-virtual-machine)
before building the plugin itself
@ -218,7 +218,9 @@ machine, through cgo, instead of using the Go written bytecode interpreter. With
the latest Go compiler, the native virtual machine is actually slower than the
Go-written one, but importantly, the native virtual machine allows you to test
that the patch also works within the stack limits of the x87 virtual machine,
which is the VM used in the compiled intros.
which is the VM used in the compiled intros. In the tracker/VSTi, you can switch
between the native synth and the Go synth under the CPU panel in the Song
settings.
Before you can actually run it, you need to build the bridge using CMake (thus,
***this will not work with go get***).

View File

@ -67,6 +67,7 @@ type (
// Synther compiles a given Patch into a Synth, throwing errors if the
// Patch is malformed.
Synther interface {
Name() string // Name of the synther, e.g. "Go" or "Native"
Synth(patch Patch, bpm int) (Synth, error)
}
)

View File

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

View File

@ -4,12 +4,13 @@ import (
"encoding/json"
"flag"
"fmt"
"github.com/vsariola/sointu/cmd"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/vsariola/sointu/cmd"
"gopkg.in/yaml.v3"
"github.com/vsariola/sointu"
@ -29,12 +30,20 @@ func main() {
wavOut := flag.Bool("w", false, "Output the rendered song as .wav file. By default, saves stereo float32 buffer to disk.")
pcm := flag.Bool("c", false, "Convert audio to 16-bit signed PCM when outputting.")
versionFlag := flag.Bool("v", false, "Print version.")
syntherInt := flag.Int("synth", 0, "Select the synther to use. By default, uses the first one in the list of available synthers.")
flag.Usage = printUsage
flag.Parse()
if *versionFlag {
fmt.Println(version.VersionOrHash)
os.Exit(0)
}
if *syntherInt < 0 || *syntherInt >= len(cmd.Synthers) {
fmt.Fprintf(os.Stderr, "synth index %d is out of range; available synthers:\n", *syntherInt)
for i, s := range cmd.Synthers {
fmt.Fprintf(os.Stderr, " %d: %s\n", i, s.Name())
}
os.Exit(1)
}
if flag.NArg() == 0 || *help {
flag.Usage()
os.Exit(0)
@ -93,7 +102,7 @@ func main() {
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
buffer, err := sointu.Play(cmd.MainSynther, song, nil) // render the song to calculate its length
buffer, err := sointu.Play(cmd.Synthers[*syntherInt], song, nil) // render the song to calculate its length
if err != nil {
return fmt.Errorf("sointu.Play failed: %v", err)
}

View File

@ -49,8 +49,8 @@ func main() {
midiContext := cmd.NewMidiContext(broker)
defer midiContext.Close()
midiContext.TryToOpenBy(*defaultMidiInput, *firstMidiInput)
model := tracker.NewModel(broker, cmd.MainSynther, midiContext, recoveryFile)
player := tracker.NewPlayer(broker, cmd.MainSynther)
model := tracker.NewModel(broker, cmd.Synthers, midiContext, recoveryFile)
player := tracker.NewPlayer(broker, cmd.Synthers[0])
detector := tracker.NewDetector(broker)
go detector.Run()

View File

@ -46,8 +46,8 @@ func init() {
recoveryFile = filepath.Join(configDir, "sointu", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes))
}
broker := tracker.NewBroker()
model := tracker.NewModel(broker, cmd.MainSynther, cmd.NewMidiContext(broker), recoveryFile)
player := tracker.NewPlayer(broker, cmd.MainSynther)
model := tracker.NewModel(broker, cmd.Synthers, cmd.NewMidiContext(broker), recoveryFile)
player := tracker.NewPlayer(broker, cmd.Synthers[0])
detector := tracker.NewDetector(broker)
go detector.Run()
@ -63,11 +63,11 @@ func init() {
buf := make(sointu.AudioBuffer, 1024)
var totalFrames int64 = 0
return vst2.Plugin{
UniqueID: PLUGIN_ID,
UniqueID: [4]byte{'S', 'n', 't', 'u'},
Version: version,
InputChannels: 0,
OutputChannels: 2,
Name: PLUGIN_NAME,
Name: "Sointu",
Vendor: "vsariola/sointu",
Category: vst2.PluginCategorySynth,
Flags: vst2.PluginIsSynth,

View File

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

View File

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

10
cmd/synthers.go Normal file
View File

@ -0,0 +1,10 @@
package cmd
import (
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
)
var Synthers = []sointu.Synther{
vm.GoSynther{},
}

View File

@ -4,4 +4,6 @@ package cmd
import "github.com/vsariola/sointu/vm/compiler/bridge"
var MainSynther = bridge.NativeSynther{}
func init() {
Synthers = append(Synthers, bridge.NativeSynther{})
}

View File

@ -82,7 +82,7 @@ func (m *Model) WriteWav(w io.WriteCloser, pcm16 bool) {
b := make([]byte, 32+2)
rand.Read(b)
name := fmt.Sprintf("%x", b)[2 : 32+2]
data, err := sointu.Play(m.synther, song, func(p float32) {
data, err := sointu.Play(m.synthers[m.syntherIndex], song, func(p float32) {
txt := fmt.Sprintf("Exporting song: %.0f%%", p*100)
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Info, Name: name, Duration: defaultAlertDuration}})
}) // render the song to calculate its length

View File

@ -21,9 +21,11 @@ type SongPanel struct {
ScopeExpander *Expander
LoudnessExpander *Expander
PeakExpander *Expander
CPUExpander *Expander
WeightingTypeBtn *Clickable
OversamplingBtn *Clickable
SynthBtn *Clickable
BPM *NumericUpDownState
RowsPerPattern *NumericUpDownState
@ -50,11 +52,13 @@ func NewSongPanel(tr *Tracker) *SongPanel {
WeightingTypeBtn: new(Clickable),
OversamplingBtn: new(Clickable),
SynthBtn: new(Clickable),
SongSettingsExpander: &Expander{Expanded: true},
ScopeExpander: &Expander{},
LoudnessExpander: &Expander{},
PeakExpander: &Expander{},
CPUExpander: &Expander{},
}
return ret
}
@ -66,6 +70,10 @@ func (s *SongPanel) Update(gtx C, t *Tracker) {
for s.OversamplingBtn.Clicked(gtx) {
t.Model.Oversampling().SetValue(!t.Oversampling().Value())
}
for s.SynthBtn.Clicked(gtx) {
r := t.Model.SyntherIndex().Range()
t.Model.SyntherIndex().SetValue((t.SyntherIndex().Value()+1)%(r.Max-r.Min+1) + r.Min)
}
}
func (s *SongPanel) Layout(gtx C) D {
@ -102,6 +110,14 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
}
oversamplingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.OversamplingBtn, oversamplingTxt, "")
cpuload := tr.Model.CPULoad()
cpuLabel := Label(tr.Theme, &tr.Theme.SongPanel.RowValue, fmt.Sprintf("%.0f %%", cpuload*100))
if cpuload >= 1 {
cpuLabel.Color = tr.Theme.SongPanel.ErrorColor
}
synthBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.SynthBtn, tr.Model.SyntherName(), "")
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return t.SongSettingsExpander.Layout(gtx, tr.Theme, "Song",
@ -130,17 +146,19 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
step := NumUpDown(tr.Step(), tr.Theme, t.Step, "Cursor step")
return layoutSongOptionRow(gtx, tr.Theme, "Cursor step", step.Layout)
}),
layout.Rigid(func(gtx C) D {
cpuload := tr.Model.CPULoad()
label := Label(tr.Theme, &tr.Theme.SongPanel.RowValue, fmt.Sprintf("%.0f %%", cpuload*100))
if cpuload >= 1 {
label.Color = tr.Theme.SongPanel.ErrorColor
}
return layoutSongOptionRow(gtx, tr.Theme, "CPU load", label.Layout)
}),
)
})
}),
layout.Rigid(func(gtx C) D {
return t.CPUExpander.Layout(gtx, tr.Theme, "CPU", cpuLabel.Layout,
func(gtx C) D {
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Load", cpuLabel.Layout) }),
layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Synth", synthBtn.Layout) }),
)
},
)
}),
layout.Rigid(func(gtx C) D {
return t.LoudnessExpander.Layout(gtx, tr.Theme, "Loudness",
func(gtx C) D {

View File

@ -113,7 +113,6 @@ func NewTracker(model *tracker.Model) *Tracker {
Duration: 10 * time.Second,
})
}
t.TrackEditor.scrollTable.Focus()
return t
}

View File

@ -33,6 +33,7 @@ type (
Step Model
Octave Model
DetectorWeighting Model
SyntherIndex Model
)
func MakeInt(value IntValue) Int {
@ -81,6 +82,7 @@ func (m *Model) RowsPerBeat() Int { return MakeInt((*RowsPerBeat)(m)) }
func (m *Model) Step() Int { return MakeInt((*Step)(m)) }
func (m *Model) Octave() Int { return MakeInt((*Octave)(m)) }
func (m *Model) DetectorWeighting() Int { return MakeInt((*DetectorWeighting)(m)) }
func (m *Model) SyntherIndex() Int { return MakeInt((*SyntherIndex)(m)) }
// BeatsPerMinuteInt
@ -205,3 +207,17 @@ func (v *TrackVoices) Range() IntRange {
}
return IntRange{1, (*Model)(v).remainingVoices(v.linkInstrTrack, true) + v.d.Song.Score.Tracks[t].NumVoices}
}
// SyntherIndex
func (v *SyntherIndex) Value() int { return v.syntherIndex }
func (v *SyntherIndex) Range() IntRange { return IntRange{0, len(v.synthers) - 1} }
func (v *Model) SyntherName() string { return v.synthers[v.syntherIndex].Name() }
func (v *SyntherIndex) SetValue(value int) bool {
if value < 0 || value >= len(v.synthers) {
return false
}
v.syntherIndex = value
TrySend(v.broker.ToPlayer, any(v.synthers[value]))
return true
}

View File

@ -75,9 +75,11 @@ type (
weightingType WeightingType
oversampling bool
alerts []Alert
dialog Dialog
synther sointu.Synther // the synther used to create new synths
alerts []Alert
dialog Dialog
syntherIndex int // the index of the synther used to create new synths
synthers []sointu.Synther // the synther used to create new synths
broker *Broker
@ -171,9 +173,9 @@ func (m *Model) Quitted() bool { return m.quitted }
func (m *Model) DetectorResult() DetectorResult { return m.detectorResult }
// NewModelPlayer creates a new model and a player that communicates with it
func NewModel(broker *Broker, synther sointu.Synther, midiContext MIDIContext, recoveryFilePath string) *Model {
func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext, recoveryFilePath string) *Model {
m := new(Model)
m.synther = synther
m.synthers = synthers
m.MIDI = midiContext
m.broker = broker
m.d.Octave = 4

View File

@ -7,6 +7,7 @@ import (
"io"
"testing"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/vm"
)
@ -251,10 +252,10 @@ func FuzzModel(f *testing.F) {
f.Add(seed)
f.Fuzz(func(t *testing.T, slice []byte) {
reader := bytes.NewReader(slice)
synther := vm.GoSynther{}
synthers := []sointu.Synther{vm.GoSynther{}}
broker := tracker.NewBroker()
model := tracker.NewModel(broker, synther, tracker.NullMIDIContext{}, "")
player := tracker.NewPlayer(broker, synther)
model := tracker.NewModel(broker, synthers, tracker.NullMIDIContext{}, "")
player := tracker.NewPlayer(broker, synthers[0])
buf := make([][2]float32, 2048)
closeChan := make(chan struct{})
go func() {

View File

@ -268,6 +268,10 @@ loop:
}
p.recording = Recording{} // reset recording
}
case sointu.Synther:
p.synther = m
p.synth = nil
p.compileOrUpdateSynth()
default:
// ignore unknown messages
}

View File

@ -18,6 +18,8 @@ type NativeSynther struct {
type NativeSynth C.Synth
func (s NativeSynther) Name() string { return "Native" }
func (s NativeSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
synth, err := Synth(patch, bpm)
return synth, err

View File

@ -93,6 +93,8 @@ success:
f.Read(su_sample_table[:])
}
func (s GoSynther) Name() string { return "Go" }
func (s GoSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
bytecode, err := NewBytecode(patch, AllFeatures{}, bpm)
if err != nil {