mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
155 lines
4.6 KiB
Go
155 lines
4.6 KiB
Go
//go:build plugin
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/vsariola/sointu"
|
|
"github.com/vsariola/sointu/cmd"
|
|
"github.com/vsariola/sointu/tracker"
|
|
"github.com/vsariola/sointu/tracker/gioui"
|
|
"pipelined.dev/audio/vst2"
|
|
)
|
|
|
|
type (
|
|
VSTIProcessContext struct {
|
|
events []vst2.MIDIEvent
|
|
eventIndex int
|
|
host vst2.Host
|
|
}
|
|
|
|
NullMIDIContext struct{}
|
|
)
|
|
|
|
func (m NullMIDIContext) InputDevices(yield func(tracker.MIDIDevice) bool) {}
|
|
|
|
func (m NullMIDIContext) Close() {}
|
|
|
|
func (m NullMIDIContext) HasDeviceOpen() bool { return false }
|
|
|
|
func (c *VSTIProcessContext) NextEvent(frame int) (event tracker.MIDINoteEvent, ok bool) {
|
|
for c.eventIndex < len(c.events) {
|
|
ev := c.events[c.eventIndex]
|
|
c.eventIndex++
|
|
switch {
|
|
case ev.Data[0] >= 0x80 && ev.Data[0] < 0x90:
|
|
channel := ev.Data[0] - 0x80
|
|
note := ev.Data[1]
|
|
return tracker.MIDINoteEvent{Frame: int(ev.DeltaFrames), On: false, Channel: int(channel), Note: note}, true
|
|
case ev.Data[0] >= 0x90 && ev.Data[0] < 0xA0:
|
|
channel := ev.Data[0] - 0x90
|
|
note := ev.Data[1]
|
|
return tracker.MIDINoteEvent{Frame: int(ev.DeltaFrames), On: true, Channel: int(channel), Note: note}, true
|
|
default:
|
|
// ignore all other MIDI messages
|
|
}
|
|
}
|
|
return tracker.MIDINoteEvent{}, false
|
|
}
|
|
|
|
func (c *VSTIProcessContext) FinishBlock(frame int) {}
|
|
|
|
func (c *VSTIProcessContext) BPM() (bpm float64, ok bool) {
|
|
timeInfo := c.host.GetTimeInfo(vst2.TempoValid)
|
|
if timeInfo == nil || timeInfo.Flags&vst2.TempoValid == 0 || timeInfo.Tempo == 0 {
|
|
return 0, false
|
|
}
|
|
return timeInfo.Tempo, true
|
|
}
|
|
|
|
func init() {
|
|
var (
|
|
version = int32(100)
|
|
)
|
|
vst2.PluginAllocator = func(h vst2.Host) (vst2.Plugin, vst2.Dispatcher) {
|
|
recoveryFile := ""
|
|
if configDir, err := os.UserConfigDir(); err == nil {
|
|
randBytes := make([]byte, 16)
|
|
rand.Read(randBytes)
|
|
recoveryFile = filepath.Join(configDir, "sointu", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes))
|
|
}
|
|
broker := tracker.NewBroker()
|
|
model := tracker.NewModel(broker, cmd.MainSynther, NullMIDIContext{}, recoveryFile)
|
|
player := tracker.NewPlayer(broker, cmd.MainSynther)
|
|
detector := tracker.NewDetector(broker)
|
|
go detector.Run()
|
|
|
|
t := gioui.NewTracker(model)
|
|
model.InstrEnlarged().Bool().Set(true)
|
|
// since the VST is usually working without any regard for the tracks
|
|
// until recording, disable the Instrument-Track linking by default
|
|
// because it might just confuse the user why instrument cannot be
|
|
// swapped/added etc.
|
|
model.LinkInstrTrack().Bool().Set(false)
|
|
go t.Main()
|
|
context := VSTIProcessContext{host: h}
|
|
buf := make(sointu.AudioBuffer, 1024)
|
|
return vst2.Plugin{
|
|
UniqueID: PLUGIN_ID,
|
|
Version: version,
|
|
InputChannels: 0,
|
|
OutputChannels: 2,
|
|
Name: PLUGIN_NAME,
|
|
Vendor: "vsariola/sointu",
|
|
Category: vst2.PluginCategorySynth,
|
|
Flags: vst2.PluginIsSynth,
|
|
ProcessFloatFunc: func(in, out vst2.FloatBuffer) {
|
|
if s := h.GetSampleRate(); math.Abs(float64(h.GetSampleRate()-44100.0)) > 1e-6 {
|
|
player.SendAlert("WrongSampleRate", fmt.Sprintf("VSTi host sample rate is %.0f Hz; sointu supports 44100 Hz only", s), tracker.Error)
|
|
}
|
|
left := out.Channel(0)
|
|
right := out.Channel(1)
|
|
if len(buf) < out.Frames {
|
|
buf = append(buf, make(sointu.AudioBuffer, out.Frames-len(buf))...)
|
|
}
|
|
buf = buf[:out.Frames]
|
|
player.Process(buf, &context, nil)
|
|
for i := 0; i < out.Frames; i++ {
|
|
left[i], right[i] = buf[i][0], buf[i][1]
|
|
}
|
|
context.events = context.events[:0] // reset buffer, but keep the allocated memory
|
|
context.eventIndex = 0
|
|
},
|
|
}, vst2.Dispatcher{
|
|
CanDoFunc: func(pcds vst2.PluginCanDoString) vst2.CanDoResponse {
|
|
switch pcds {
|
|
case vst2.PluginCanReceiveEvents, vst2.PluginCanReceiveMIDIEvent, vst2.PluginCanReceiveTimeInfo:
|
|
return vst2.YesCanDo
|
|
}
|
|
return vst2.NoCanDo
|
|
},
|
|
ProcessEventsFunc: func(ev *vst2.EventsPtr) {
|
|
for i := 0; i < ev.NumEvents(); i++ {
|
|
a := ev.Event(i)
|
|
switch v := a.(type) {
|
|
case *vst2.MIDIEvent:
|
|
context.events = append(context.events, *v)
|
|
}
|
|
}
|
|
},
|
|
CloseFunc: func() {
|
|
broker.ToModel <- tracker.MsgToModel{Data: func() { t.ForceQuit().Do() }}
|
|
t.WaitQuitted()
|
|
detector.Close()
|
|
},
|
|
GetChunkFunc: func(isPreset bool) []byte {
|
|
retChn := make(chan []byte)
|
|
broker.ToModel <- tracker.MsgToModel{Data: func() { retChn <- t.MarshalRecovery() }}
|
|
return <-retChn
|
|
},
|
|
SetChunkFunc: func(data []byte, isPreset bool) {
|
|
broker.ToModel <- tracker.MsgToModel{Data: func() { t.UnmarshalRecovery(data) }}
|
|
},
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func main() {}
|