mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-12 17:14:43 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4e763e543 | |||
| 40ae9c98fe | |||
| 9678108fd1 | |||
| cf05e68471 | |||
| 15c1126d11 | |||
| f4742cb02d | |||
| da83478733 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -17,8 +17,9 @@ build/
|
||||
# Project specific
|
||||
old/
|
||||
|
||||
# VS Code
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# project specific
|
||||
# this is autogenerated from bridge.go.in
|
||||
|
||||
@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Dragging mouse to select rectangles in the tables
|
||||
- The standalone tracker can open a MIDI port for receiving MIDI notes
|
||||
([#166][i166])
|
||||
- the note editor has a button to allow entering notes by MIDI.
|
||||
Polyphony is supported if there are tracks available. ([#170][i170])
|
||||
- Units can have comments, to make it easier to distinguish between units of
|
||||
same type within an instrument. These comments are also shown when choosing
|
||||
the send target. ([#114][i114])
|
||||
|
||||
21
README.md
21
README.md
@ -511,6 +511,27 @@ Prods using Sointu
|
||||
- [21](https://demozoo.org/music/338597/) by NR4 / Team210
|
||||
- [Tausendeins](https://www.pouet.net/prod.php?which=96192) by epoqe & Team210
|
||||
|
||||
Notes on Adding New OpCodes
|
||||
---------------------------
|
||||
Think of a great new, not already taken name, and extend
|
||||
* ./patch.go
|
||||
* `UnitTypes`
|
||||
* `StackChange()`
|
||||
* `StackNeed()`
|
||||
* ./tracker/presets.go
|
||||
* `defaultUnits`
|
||||
|
||||
Call `./vm/generate/generate.go` (e.g. `generate go` inside the ./vm folder)
|
||||
|
||||
Now, you need a case distinction inside
|
||||
* ./vm/go_synth.go -> `Render()`
|
||||
* for the Go Bytecode VM
|
||||
* this should work with the next `go build` / `go run`
|
||||
* ./vm/compiler/templates/<architecture>/sources.asm
|
||||
* with your given architecture for `-tags=native`, e.g "amd64-386" or "wasm"
|
||||
* this needs a `ninja sointu` inside your build directory, then `go build` / `go run`
|
||||
* it seems not to be necessary to call `go clean -cache`, but in doubt, give it a try.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
|
||||
15
build_vst_plugin.ps1
Normal file
15
build_vst_plugin.ps1
Normal file
@ -0,0 +1,15 @@
|
||||
# Specify "native" or "go" if you only want one VST version.
|
||||
|
||||
if ($args -notcontains "go") {
|
||||
|
||||
Write-Host "Build VST with ASM synth"
|
||||
go build -buildmode=c-shared -tags="plugin","native" -o sointu-vsti-native.dll .\cmd\sointu-vsti\
|
||||
|
||||
}
|
||||
if ($args -notcontains "native") {
|
||||
|
||||
Write-Host "Build VST with GO synth"
|
||||
go build -buildmode=c-shared -tags="plugin" -o sointu-vsti-go.dll .\cmd\sointu-vsti\
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/vsariola/sointu/cmd"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -14,7 +15,6 @@ import (
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/oto"
|
||||
"github.com/vsariola/sointu/version"
|
||||
"github.com/vsariola/sointu/vm/compiler/bridge"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -93,7 +93,7 @@ func main() {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song, nil) // render the song to calculate its length
|
||||
buffer, err := sointu.Play(cmd.MainSynther, song, nil) // render the song to calculate its length
|
||||
if err != nil {
|
||||
return fmt.Errorf("sointu.Play failed: %v", err)
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"runtime/pprof"
|
||||
|
||||
"gioui.org/app"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/cmd"
|
||||
"github.com/vsariola/sointu/oto"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
@ -18,18 +17,10 @@ import (
|
||||
"github.com/vsariola/sointu/tracker/gomidi"
|
||||
)
|
||||
|
||||
type PlayerAudioSource struct {
|
||||
*tracker.Player
|
||||
playerProcessContext tracker.PlayerProcessContext
|
||||
}
|
||||
|
||||
func (p *PlayerAudioSource) ReadAudio(buf sointu.AudioBuffer) error {
|
||||
p.Player.Process(buf, p.playerProcessContext)
|
||||
return nil
|
||||
}
|
||||
|
||||
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
|
||||
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
|
||||
var defaultMidiInput = flag.String("midi-input", "", "connect MIDI input to matching device name")
|
||||
var firstMidiInput = flag.Bool("first-midi-input", false, "connect MIDI input to first device found")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
@ -54,8 +45,10 @@ func main() {
|
||||
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery")
|
||||
}
|
||||
midiContext := gomidi.NewContext()
|
||||
defer midiContext.Close()
|
||||
midiContext.TryToOpenBy(*defaultMidiInput, *firstMidiInput)
|
||||
model, player := tracker.NewModelPlayer(cmd.MainSynther, midiContext, recoveryFile)
|
||||
defer model.MIDI.Close()
|
||||
|
||||
if a := flag.Args(); len(a) > 0 {
|
||||
f, err := os.Open(a[0])
|
||||
if err == nil {
|
||||
@ -63,10 +56,13 @@ func main() {
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
tracker := gioui.NewTracker(model)
|
||||
audioCloser := audioContext.Play(&PlayerAudioSource{player, midiContext})
|
||||
|
||||
trackerUi := gioui.NewTracker(model)
|
||||
processor := tracker.NewProcessor(player, midiContext, trackerUi)
|
||||
audioCloser := audioContext.Play(processor)
|
||||
|
||||
go func() {
|
||||
tracker.Main()
|
||||
trackerUi.Main()
|
||||
audioCloser.Close()
|
||||
if *cpuprofile != "" {
|
||||
pprof.StopCPUProfile()
|
||||
|
||||
@ -31,6 +31,8 @@ func (m NullMIDIContext) InputDevices(yield func(tracker.MIDIDevice) bool) {}
|
||||
|
||||
func (m NullMIDIContext) Close() {}
|
||||
|
||||
func (m NullMIDIContext) HasDeviceOpen() bool { return false }
|
||||
|
||||
func (c *VSTIProcessContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
|
||||
for c.eventIndex < len(c.events) {
|
||||
ev := c.events[c.eventIndex]
|
||||
@ -101,7 +103,7 @@ func init() {
|
||||
buf = append(buf, make(sointu.AudioBuffer, out.Frames-len(buf))...)
|
||||
}
|
||||
buf = buf[:out.Frames]
|
||||
player.Process(buf, &context)
|
||||
player.Process(buf, &context, nil)
|
||||
for i := 0; i < out.Frames; i++ {
|
||||
left[i], right[i] = buf[i][0], buf[i][1]
|
||||
}
|
||||
|
||||
17
patch.go
17
patch.go
@ -157,6 +157,15 @@ var UnitTypes = map[string]([]UnitParameter){
|
||||
{Name: "sustain", 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}},
|
||||
"envelopexp": []UnitParameter{
|
||||
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
||||
{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: "exp_attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: envelopExpDisplayFunc},
|
||||
{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: "exp_decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: envelopExpDisplayFunc},
|
||||
{Name: "sustain", 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},
|
||||
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
||||
@ -248,6 +257,10 @@ func engineeringTime(sec float64) (string, string) {
|
||||
return fmt.Sprintf("%.2f", sec), "s"
|
||||
}
|
||||
|
||||
func envelopExpDisplayFunc(v int) (string, string) {
|
||||
return fmt.Sprintf("= %.3f", math.Pow(2, 2*float64(64-v)/32)), ""
|
||||
}
|
||||
|
||||
func formatFloat(f float64) string {
|
||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||
}
|
||||
@ -317,7 +330,7 @@ func (u *Unit) StackChange() int {
|
||||
switch u.Type {
|
||||
case "addp", "mulp", "pop", "out", "outaux", "aux":
|
||||
return -1 - u.Parameters["stereo"]
|
||||
case "envelope", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor":
|
||||
case "envelope", "envelopexp", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor":
|
||||
return 1 + u.Parameters["stereo"]
|
||||
case "pan":
|
||||
return 1 - u.Parameters["stereo"]
|
||||
@ -337,7 +350,7 @@ func (u *Unit) StackNeed() int {
|
||||
return 0
|
||||
}
|
||||
switch u.Type {
|
||||
case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in":
|
||||
case "", "envelope", "envelopexp", "oscillator", "noise", "receive", "loadnote", "loadval", "in":
|
||||
return 0
|
||||
case "mulp", "mul", "add", "addp", "xch":
|
||||
return 2 * (1 + u.Parameters["stereo"])
|
||||
|
||||
BIN
sointu-vsti-go-v1.0.dll
Normal file
BIN
sointu-vsti-go-v1.0.dll
Normal file
Binary file not shown.
BIN
sointu-vsti-native-v1.0.dll
Normal file
BIN
sointu-vsti-native-v1.0.dll
Normal file
Binary file not shown.
30
song.go
30
song.go
@ -2,6 +2,7 @@ package sointu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"iter"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -331,3 +332,32 @@ func TotalVoices[T any, S ~[]T, P NumVoicerPointer[T]](slice S) (ret int) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Song) InstrumentForTrack(trackIndex int) (int, bool) {
|
||||
voiceIndex := s.Score.FirstVoiceForTrack(trackIndex)
|
||||
instrument, err := s.Patch.InstrumentForVoice(voiceIndex)
|
||||
return instrument, err == nil
|
||||
}
|
||||
|
||||
func (s *Song) AllTracksWithSameInstrument(trackIndex int) iter.Seq[int] {
|
||||
return func(yield func(int) bool) {
|
||||
|
||||
currentInstrument, currentExists := s.InstrumentForTrack(trackIndex)
|
||||
if !currentExists {
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < len(s.Score.Tracks); i++ {
|
||||
instrument, exists := s.InstrumentForTrack(i)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
if instrument != currentInstrument {
|
||||
continue
|
||||
}
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -475,12 +475,12 @@ func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFl
|
||||
func (m *Model) ExportInt16() Action { return Allow(func() { m.dialog = ExportInt16Explorer }) }
|
||||
func (m *Model) SelectMidiInput(item MIDIDevice) Action {
|
||||
return Allow(func() {
|
||||
if err := item.Open(); err != nil {
|
||||
message := fmt.Sprintf("Could not open MIDI device: %s", item)
|
||||
m.Alerts().Add(message, Error)
|
||||
} else {
|
||||
if err := item.Open(); err == nil {
|
||||
message := fmt.Sprintf("Opened MIDI device: %s", item)
|
||||
m.Alerts().Add(message, Info)
|
||||
} else {
|
||||
message := fmt.Sprintf("Could not open MIDI device: %s", item)
|
||||
m.Alerts().Add(message, Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ type (
|
||||
Playing Model
|
||||
InstrEnlarged Model
|
||||
Effect Model
|
||||
TrackMidiIn Model
|
||||
CommentExpanded Model
|
||||
Follow Model
|
||||
UnitSearching Model
|
||||
@ -44,6 +45,7 @@ func (m *Model) IsRecording() *IsRecording { return (*IsRecording)(m) }
|
||||
func (m *Model) Playing() *Playing { return (*Playing)(m) }
|
||||
func (m *Model) InstrEnlarged() *InstrEnlarged { return (*InstrEnlarged)(m) }
|
||||
func (m *Model) Effect() *Effect { return (*Effect)(m) }
|
||||
func (m *Model) TrackMidiIn() *TrackMidiIn { return (*TrackMidiIn)(m) }
|
||||
func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) }
|
||||
func (m *Model) Follow() *Follow { return (*Follow)(m) }
|
||||
func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
|
||||
@ -110,6 +112,13 @@ func (m *Follow) Value() bool { return m.follow }
|
||||
func (m *Follow) setValue(val bool) { m.follow = val }
|
||||
func (m *Follow) Enabled() bool { return true }
|
||||
|
||||
// TrackMidiIn (Midi Input for notes in the tracks)
|
||||
|
||||
func (m *TrackMidiIn) Bool() Bool { return Bool{m} }
|
||||
func (m *TrackMidiIn) Value() bool { return m.trackMidiIn }
|
||||
func (m *TrackMidiIn) setValue(val bool) { m.trackMidiIn = val }
|
||||
func (m *TrackMidiIn) Enabled() bool { return m.MIDI.HasDeviceOpen() }
|
||||
|
||||
// Effect methods
|
||||
|
||||
func (m *Effect) Bool() Bool { return Bool{m} }
|
||||
|
||||
@ -3,6 +3,7 @@ package gioui
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -62,6 +63,7 @@ type NoteEditor struct {
|
||||
NoteOffBtn *ActionClickable
|
||||
EffectBtn *BoolClickable
|
||||
UniqueBtn *BoolClickable
|
||||
TrackMidiInBtn *BoolClickable
|
||||
|
||||
scrollTable *ScrollTable
|
||||
eventFilters []event.Filter
|
||||
@ -85,6 +87,7 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||
NoteOffBtn: NewActionClickable(model.EditNoteOff()),
|
||||
EffectBtn: NewBoolClickable(model.Effect().Bool()),
|
||||
UniqueBtn: NewBoolClickable(model.UniquePatterns().Bool()),
|
||||
TrackMidiInBtn: NewBoolClickable(model.TrackMidiIn().Bool()),
|
||||
scrollTable: NewScrollTable(
|
||||
model.Notes().Table(),
|
||||
model.Tracks().List(),
|
||||
@ -162,6 +165,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
||||
}
|
||||
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex")
|
||||
uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
|
||||
midiInBtnStyle := ToggleButton(gtx, t.Theme, te.TrackMidiInBtn, "MIDI")
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||
@ -175,6 +179,8 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Rigid(splitTrackBtnStyle.Layout),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(midiInBtnStyle.Layout),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(deleteTrackBtnStyle.Layout),
|
||||
layout.Rigid(newTrackBtnStyle.Layout))
|
||||
})
|
||||
@ -217,7 +223,13 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
h := gtx.Dp(unit.Dp(trackColTitleHeight))
|
||||
title := ((*tracker.Order)(t.Model)).Title(i)
|
||||
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
|
||||
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
|
||||
LabelStyle{
|
||||
Alignment: layout.N,
|
||||
Text: title,
|
||||
FontSize: unit.Sp(12),
|
||||
Color: mediumEmphasisTextColor,
|
||||
Shaper: t.Theme.Shaper,
|
||||
}.Layout(gtx)
|
||||
return D{Size: image.Pt(pxWidth, h)}
|
||||
}
|
||||
|
||||
@ -253,8 +265,10 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
return D{Size: image.Pt(w, pxHeight)}
|
||||
}
|
||||
|
||||
drawSelection := te.scrollTable.Table.Cursor() != te.scrollTable.Table.Cursor2()
|
||||
cursor := te.scrollTable.Table.Cursor()
|
||||
drawSelection := cursor != te.scrollTable.Table.Cursor2()
|
||||
selection := te.scrollTable.Table.Range()
|
||||
hasTrackMidiIn := te.TrackMidiInBtn.Bool.Value()
|
||||
|
||||
cell := func(gtx C, x, y int) D {
|
||||
// draw the background, to indicate selection
|
||||
@ -268,21 +282,25 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
// draw the cursor
|
||||
if point == te.scrollTable.Table.Cursor() {
|
||||
cw := gtx.Constraints.Min.X
|
||||
cx := 0
|
||||
if t.Model.Notes().Effect(x) {
|
||||
cw /= 2
|
||||
if t.Model.Notes().LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
if point == cursor {
|
||||
c := inactiveSelectionColor
|
||||
if te.scrollTable.Focused() {
|
||||
c = cursorColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(cx, 0), Max: image.Pt(cx+cw, gtx.Constraints.Min.Y)}.Op())
|
||||
if hasTrackMidiIn {
|
||||
c = cursorForTrackMidiInColor
|
||||
}
|
||||
te.paintColumnCell(gtx, x, t, c)
|
||||
}
|
||||
// draw the corresponding "fake cursors" for instrument-track-groups (for polyphony)
|
||||
if hasTrackMidiIn {
|
||||
for trackIndex := range ((*tracker.Order)(t.Model)).TrackIndicesForCurrentInstrument() {
|
||||
if x == trackIndex && y == cursor.Y {
|
||||
te.paintColumnCell(gtx, x, t, cursorNeighborForTrackMidiInColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw the pattern marker
|
||||
rpp := max(t.RowsPerPattern().Value(), 1)
|
||||
pat := y / rpp
|
||||
@ -317,6 +335,18 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
return table.Layout(gtx)
|
||||
}
|
||||
|
||||
func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA) {
|
||||
cw := gtx.Constraints.Min.X
|
||||
cx := 0
|
||||
if t.Model.Notes().Effect(x) {
|
||||
cw /= 2
|
||||
if t.Model.Notes().LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(cx, 0), Max: image.Pt(cx+cw, gtx.Constraints.Min.Y)}.Op())
|
||||
}
|
||||
|
||||
func mod(x, d int) int {
|
||||
x = x % d
|
||||
if x >= 0 {
|
||||
@ -338,7 +368,7 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
||||
if nibbleValue, err := strconv.ParseInt(string(e.Name), 16, 8); err == nil {
|
||||
t.Model.Notes().FillNibble(byte(nibbleValue), t.Model.Notes().LowNibble())
|
||||
n = t.Model.Notes().Value(te.scrollTable.Table.Cursor())
|
||||
goto validNote
|
||||
te.finishNoteInsert(t, n, e.Name)
|
||||
}
|
||||
} else {
|
||||
action, ok := keyBindingMap[e]
|
||||
@ -347,11 +377,7 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
||||
}
|
||||
if action == "NoteOff" {
|
||||
t.Model.Notes().Table().Fill(0)
|
||||
if step := t.Model.Step().Value(); step > 0 {
|
||||
te.scrollTable.Table.MoveCursor(0, step)
|
||||
te.scrollTable.Table.SetCursor2(te.scrollTable.Table.Cursor())
|
||||
}
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
te.finishNoteInsert(t, 0, "")
|
||||
return
|
||||
}
|
||||
if action[:4] == "Note" {
|
||||
@ -361,20 +387,43 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
||||
}
|
||||
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val-12)
|
||||
t.Model.Notes().Table().Fill(int(n))
|
||||
goto validNote
|
||||
te.finishNoteInsert(t, n, e.Name)
|
||||
}
|
||||
}
|
||||
return
|
||||
validNote:
|
||||
}
|
||||
|
||||
func (te *NoteEditor) finishNoteInsert(t *Tracker, note byte, keyName key.Name) {
|
||||
if step := t.Model.Step().Value(); step > 0 {
|
||||
te.scrollTable.Table.MoveCursor(0, step)
|
||||
te.scrollTable.Table.SetCursor2(te.scrollTable.Table.Cursor())
|
||||
}
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
trk := te.scrollTable.Table.Cursor().X
|
||||
t.KeyPlaying[e.Name] = t.TrackNoteOn(trk, n)
|
||||
|
||||
if keyName == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := t.KeyPlaying[keyName]; !ok {
|
||||
trk := te.scrollTable.Table.Cursor().X
|
||||
t.KeyPlaying[keyName] = t.TrackNoteOn(trk, note)
|
||||
}
|
||||
}
|
||||
|
||||
func (te *NoteEditor) HandleMidiInput(t *Tracker) {
|
||||
inputDeactivated := !t.Model.TrackMidiIn().Value()
|
||||
if inputDeactivated {
|
||||
return
|
||||
}
|
||||
te.scrollTable.Table.SetCursor2(te.scrollTable.Table.Cursor())
|
||||
remaining := (*tracker.Order)(t.Model).CountNextTracksForCurrentInstrument()
|
||||
for i, note := range t.MidiNotePlaying {
|
||||
t.Model.Notes().Table().Set(note)
|
||||
te.scrollTable.Table.MoveCursor(1, 0)
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
if i >= remaining {
|
||||
break
|
||||
}
|
||||
}
|
||||
te.scrollTable.Table.SetCursor(te.scrollTable.Table.Cursor2())
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@ -120,11 +120,17 @@ 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, Alignment: layout.End}.Layout(gtx,
|
||||
menuLayouts := []layout.FlexChild{
|
||||
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...)),
|
||||
layout.Rigid(tr.layoutMenu(gtx, "MIDI", &t.MenuBar[2], &t.Menus[2], unit.Dp(200), t.midiMenuItems...)),
|
||||
)
|
||||
}
|
||||
if len(t.midiMenuItems) > 0 {
|
||||
menuLayouts = append(
|
||||
menuLayouts,
|
||||
layout.Rigid(tr.layoutMenu(gtx, "MIDI", &t.MenuBar[2], &t.Menus[2], unit.Dp(200), t.midiMenuItems...)),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.End}.Layout(gtx, menuLayouts...)
|
||||
}
|
||||
|
||||
func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
|
||||
|
||||
@ -60,6 +60,8 @@ var activeLightSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255}
|
||||
var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
|
||||
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12}
|
||||
var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16}
|
||||
var cursorForTrackMidiInColor = color.NRGBA{R: 255, G: 100, B: 140, A: 48}
|
||||
var cursorNeighborForTrackMidiInColor = color.NRGBA{R: 255, G: 100, B: 140, A: 24}
|
||||
|
||||
var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255}
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ type (
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
KeyPlaying map[key.Name]tracker.NoteID
|
||||
MidiNotePlaying []byte
|
||||
PopupAlert *PopupAlert
|
||||
|
||||
SaveChangesDialog *Dialog
|
||||
@ -77,6 +78,7 @@ func NewTracker(model *tracker.Model) *Tracker {
|
||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||
|
||||
KeyPlaying: make(map[key.Name]tracker.NoteID),
|
||||
MidiNotePlaying: make([]byte, 0, 32),
|
||||
SaveChangesDialog: NewDialog(model.SaveSong(), model.DiscardSong(), model.Cancel()),
|
||||
WaveTypeDialog: NewDialog(model.ExportInt16(), model.ExportFloat(), model.Cancel()),
|
||||
InstrumentEditor: NewInstrumentEditor(model),
|
||||
@ -306,3 +308,46 @@ func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Event Handling (for UI updates when playing etc.)
|
||||
|
||||
func (t *Tracker) ProcessMessage(msg interface{}) {
|
||||
switch msg.(type) {
|
||||
case tracker.StartPlayMsg:
|
||||
fmt.Println("Tracker received StartPlayMsg")
|
||||
case tracker.RecordingMsg:
|
||||
fmt.Println("Tracker received RecordingMsg")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) ProcessEvent(event tracker.MIDINoteEvent) {
|
||||
// MIDINoteEvent can be only NoteOn / NoteOff, i.e. its On field
|
||||
if event.On {
|
||||
t.addToMidiNotePlaying(event.Note)
|
||||
} else {
|
||||
t.removeFromMidiNotePlaying(event.Note)
|
||||
}
|
||||
t.TrackEditor.HandleMidiInput(t)
|
||||
}
|
||||
|
||||
func (t *Tracker) addToMidiNotePlaying(note byte) {
|
||||
for _, n := range t.MidiNotePlaying {
|
||||
if n == note {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.MidiNotePlaying = append(t.MidiNotePlaying, note)
|
||||
}
|
||||
|
||||
func (t *Tracker) removeFromMidiNotePlaying(note byte) {
|
||||
for i, n := range t.MidiNotePlaying {
|
||||
if n == note {
|
||||
t.MidiNotePlaying = append(
|
||||
t.MidiNotePlaying[:i],
|
||||
t.MidiNotePlaying[i+1:]...,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"gitlab.com/gomidi/midi/v2"
|
||||
"gitlab.com/gomidi/midi/v2/drivers"
|
||||
"gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -56,7 +57,7 @@ func (m RTMIDIDevice) Open() error {
|
||||
if m.context.driver == nil {
|
||||
return errors.New("no driver available")
|
||||
}
|
||||
if m.context.currentIn != nil && m.context.currentIn.IsOpen() {
|
||||
if m.context.HasDeviceOpen() {
|
||||
m.context.currentIn.Close()
|
||||
}
|
||||
m.context.currentIn = m.in
|
||||
@ -120,3 +121,24 @@ func (c *RTMIDIContext) Close() {
|
||||
}
|
||||
c.driver.Close()
|
||||
}
|
||||
|
||||
func (c *RTMIDIContext) HasDeviceOpen() bool {
|
||||
return c.currentIn != nil && c.currentIn.IsOpen()
|
||||
}
|
||||
|
||||
func (c *RTMIDIContext) TryToOpenBy(namePrefix string, takeFirst bool) {
|
||||
if namePrefix == "" && !takeFirst {
|
||||
return
|
||||
}
|
||||
for input := range c.InputDevices {
|
||||
if takeFirst || strings.HasPrefix(input.String(), namePrefix) {
|
||||
input.Open()
|
||||
return
|
||||
}
|
||||
}
|
||||
if takeFirst {
|
||||
fmt.Errorf("Could not find any MIDI Input.\n")
|
||||
} else {
|
||||
fmt.Errorf("Could not find any default MIDI Input starting with \"%s\".\n", namePrefix)
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,9 +78,10 @@ type (
|
||||
synther sointu.Synther // the synther used to create new synths
|
||||
|
||||
PlayerMessages chan PlayerMsg
|
||||
modelMessages chan<- interface{}
|
||||
ModelMessages chan<- interface{}
|
||||
|
||||
MIDI MIDIContext
|
||||
MIDI MIDIContext
|
||||
trackMidiIn bool
|
||||
}
|
||||
|
||||
// Cursor identifies a row and a track in a song score.
|
||||
@ -130,6 +131,7 @@ type (
|
||||
MIDIContext interface {
|
||||
InputDevices(yield func(MIDIDevice) bool)
|
||||
Close()
|
||||
HasDeviceOpen() bool
|
||||
}
|
||||
|
||||
MIDIDevice interface {
|
||||
@ -183,9 +185,10 @@ func NewModelPlayer(synther sointu.Synther, midiContext MIDIContext, recoveryFil
|
||||
m := new(Model)
|
||||
m.synther = synther
|
||||
m.MIDI = midiContext
|
||||
m.trackMidiIn = midiContext.HasDeviceOpen()
|
||||
modelMessages := make(chan interface{}, 1024)
|
||||
playerMessages := make(chan PlayerMsg, 1024)
|
||||
m.modelMessages = modelMessages
|
||||
m.ModelMessages = modelMessages
|
||||
m.PlayerMessages = playerMessages
|
||||
m.d.Octave = 4
|
||||
m.linkInstrTrack = true
|
||||
@ -421,7 +424,7 @@ func (m *Model) resetSong() {
|
||||
|
||||
// send sends a message to the player
|
||||
func (m *Model) send(message interface{}) {
|
||||
m.modelMessages <- message
|
||||
m.ModelMessages <- message
|
||||
}
|
||||
|
||||
func (m *Model) maxID() int {
|
||||
|
||||
@ -271,7 +271,7 @@ func FuzzModel(f *testing.F) {
|
||||
break loop
|
||||
default:
|
||||
ctx := NullContext{}
|
||||
player.Process(buf, ctx)
|
||||
player.Process(buf, ctx, nil)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@ -41,6 +41,11 @@ type (
|
||||
BPM() (bpm float64, ok bool)
|
||||
}
|
||||
|
||||
EventProcessor interface {
|
||||
ProcessMessage(msg interface{})
|
||||
ProcessEvent(event MIDINoteEvent)
|
||||
}
|
||||
|
||||
// MIDINoteEvent is a MIDI event triggering or releasing a note. In
|
||||
// processing, the Frame is relative to the start of the current buffer. In
|
||||
// a Recording, the Frame is relative to the start of the recording.
|
||||
@ -89,9 +94,11 @@ const numRenderTries = 10000
|
||||
// model. context tells the player which MIDI events happen during the current
|
||||
// buffer. It is used to trigger and release notes during processing. The
|
||||
// context is also used to get the current BPM from the host.
|
||||
func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext) {
|
||||
p.processMessages(context)
|
||||
func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext, ui EventProcessor) {
|
||||
p.processMessages(context, ui)
|
||||
|
||||
midi, midiOk := context.NextEvent()
|
||||
|
||||
frame := 0
|
||||
|
||||
if p.recState == recStateRecording {
|
||||
@ -116,6 +123,10 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
||||
} else {
|
||||
p.releaseInstrument(midi.Channel, midi.Note)
|
||||
}
|
||||
if ui != nil {
|
||||
ui.ProcessEvent(midi)
|
||||
}
|
||||
|
||||
midi, midiOk = context.NextEvent()
|
||||
}
|
||||
framesUntilMidi := len(buffer)
|
||||
@ -224,7 +235,7 @@ func (p *Player) advanceRow() {
|
||||
p.rowtime = 0
|
||||
}
|
||||
|
||||
func (p *Player) processMessages(context PlayerProcessContext) {
|
||||
func (p *Player) processMessages(context PlayerProcessContext, uiProcessor EventProcessor) {
|
||||
loop:
|
||||
for { // process new message
|
||||
select {
|
||||
@ -296,6 +307,9 @@ loop:
|
||||
default:
|
||||
// ignore unknown messages
|
||||
}
|
||||
if uiProcessor != nil {
|
||||
uiProcessor.ProcessMessage(msg)
|
||||
}
|
||||
default:
|
||||
break loop
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ func init() {
|
||||
|
||||
var defaultUnits = map[string]sointu.Unit{
|
||||
"envelope": {Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 64, "gain": 64}},
|
||||
"envelopexp": {Type: "envelopexp", Parameters: map[string]int{"stereo": 0, "attack": 64, "exp_attack": 64, "decay": 64, "exp_decay": 64, "sustain": 64, "release": 64, "gain": 64}},
|
||||
"oscillator": {Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 64, "shape": 64, "gain": 64, "type": sointu.Sine}},
|
||||
"noise": {Type: "noise", Parameters: map[string]int{"stereo": 0, "shape": 64, "gain": 64}},
|
||||
"mulp": {Type: "mulp", Parameters: map[string]int{"stereo": 0}},
|
||||
|
||||
20
tracker/processor.go
Normal file
20
tracker/processor.go
Normal file
@ -0,0 +1,20 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
*Player
|
||||
playerProcessContext PlayerProcessContext
|
||||
uiProcessor EventProcessor
|
||||
}
|
||||
|
||||
func NewProcessor(player *Player, context PlayerProcessContext, uiProcessor EventProcessor) *Processor {
|
||||
return &Processor{player, context, uiProcessor}
|
||||
}
|
||||
|
||||
func (p *Processor) ReadAudio(buf sointu.AudioBuffer) error {
|
||||
p.Player.Process(buf, p.playerProcessContext, p.uiProcessor)
|
||||
return nil
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"math"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
@ -117,6 +118,13 @@ func (v Table) Clear() {
|
||||
}
|
||||
}
|
||||
|
||||
func (v Table) Set(value byte) {
|
||||
defer v.change("Set", MajorChange)()
|
||||
cursor := v.Cursor()
|
||||
// TODO: might check for visibility
|
||||
v.set(cursor, int(value))
|
||||
}
|
||||
|
||||
func (v Table) Fill(value int) {
|
||||
defer v.change("Fill", MajorChange)()
|
||||
rect := v.Range()
|
||||
@ -364,12 +372,8 @@ func (e *Order) Title(x int) (title string) {
|
||||
if x < 0 || x >= len(e.d.Song.Score.Tracks) {
|
||||
return
|
||||
}
|
||||
t := e.d.Song.Score.Tracks[x]
|
||||
firstVoice := e.d.Song.Score.FirstVoiceForTrack(x)
|
||||
lastVoice := firstVoice + t.NumVoices - 1
|
||||
firstIndex, err := e.d.Song.Patch.InstrumentForVoice(firstVoice)
|
||||
lastIndex, err2 := e.d.Song.Patch.InstrumentForVoice(lastVoice)
|
||||
if err != nil || err2 != nil {
|
||||
firstIndex, lastIndex, err := e.instrumentListFor(x)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch diff := lastIndex - firstIndex; diff {
|
||||
@ -397,6 +401,37 @@ func (e *Order) Title(x int) (title string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Order) instrumentListFor(trackIndex int) (int, int, error) {
|
||||
track := e.d.Song.Score.Tracks[trackIndex]
|
||||
firstVoice := e.d.Song.Score.FirstVoiceForTrack(trackIndex)
|
||||
lastVoice := firstVoice + track.NumVoices - 1
|
||||
firstIndex, err1 := e.d.Song.Patch.InstrumentForVoice(firstVoice)
|
||||
if err1 != nil {
|
||||
return trackIndex, trackIndex, err1
|
||||
}
|
||||
lastIndex, err2 := e.d.Song.Patch.InstrumentForVoice(lastVoice)
|
||||
if err2 != nil {
|
||||
return trackIndex, trackIndex, err2
|
||||
}
|
||||
return firstIndex, lastIndex, nil
|
||||
}
|
||||
|
||||
func (e *Order) TrackIndicesForCurrentInstrument() iter.Seq[int] {
|
||||
currentTrack := e.d.Cursor.Track
|
||||
return e.d.Song.AllTracksWithSameInstrument(currentTrack)
|
||||
}
|
||||
|
||||
func (e *Order) CountNextTracksForCurrentInstrument() int {
|
||||
currentTrack := e.d.Cursor.Track
|
||||
count := 0
|
||||
for t := range e.TrackIndicesForCurrentInstrument() {
|
||||
if t > currentTrack {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// NoteTable
|
||||
|
||||
func (v *Notes) Table() Table {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
; ENVELOPE opcode: pushes an ADSR envelope value on stack [0,1]
|
||||
;-------------------------------------------------------------------------------
|
||||
; Mono: push the envelope value on stack
|
||||
; Stereo: push the envelope valeu on stack twice
|
||||
; Stereo: push the envelope value on stack twice
|
||||
;-------------------------------------------------------------------------------
|
||||
{{.Func "su_op_envelope" "Opcode"}}
|
||||
{{- if .StereoAndMono "envelope"}}
|
||||
@ -63,6 +63,130 @@ su_op_envelope_leave2:
|
||||
{{end}}
|
||||
|
||||
|
||||
{{if .HasOp "envelopexp" -}}
|
||||
;-------------------------------------------------------------------------------
|
||||
; envelopexp opcode: pushes an ADSR envelopeXPERIMENTAL value on stack [0,1]
|
||||
;-------------------------------------------------------------------------------
|
||||
; Mono: push the envelopexp value on stack
|
||||
; Stereo: push the envelopexp value on stack twice
|
||||
;-------------------------------------------------------------------------------
|
||||
{{.Func "su_op_envelopexp" "Opcode"}}
|
||||
{{- if .StereoAndMono "envelopexp"}}
|
||||
jnc su_op_envelopexp_mono ; Carry Flag tells us whether Stereo
|
||||
{{- end}}
|
||||
{{- if .Stereo "envelopexp"}}
|
||||
call su_op_envelopexp_mono
|
||||
fld st0 ; clone the mono value to the stack -> makes it stereo
|
||||
ret
|
||||
su_op_envelopexp_mono:
|
||||
{{- end}}
|
||||
; qm210: I read that the general registers are fastest, so for this calculation, store
|
||||
; - r10: the exponent of the current segment (A, D, or the default 0.5)
|
||||
; - in unit.state[3] : the baseline of the current segment ( = sustain for D, S and 0 otherwise)
|
||||
; ( I tried r8 beforehand, didn't make it work. now it is .WRK + 12 )
|
||||
; PS: r9 is always used as temporary space for constants or su_nonlinear_map
|
||||
; and if you change registers that are use somewhere unknown... you know -- danger zone :)
|
||||
{{.Prepare (.Float 0.5)}} ; this produces mov r9, qword FCONST_0_500000
|
||||
mov r10, {{.Use (.Float 0.5)}} ; default exponent = 0.5
|
||||
mov dword [{{.WRK}} + 12], 0 ; default baseline = 0
|
||||
; <-- qm210
|
||||
mov eax, dword [{{.INP}}-su_voice.inputs+su_voice.sustain] ; eax = su_instrument.sustain
|
||||
test eax, eax ; if (eax != 0)
|
||||
jne su_op_envelopexp_process ; goto process
|
||||
mov al, {{.InputNumber "envelopexp" "release"}} ; [state]=RELEASE
|
||||
mov dword [{{.WRK}}], eax ; note that mov al, XXX; mov ..., eax is less bytes than doing it directly
|
||||
su_op_envelopexp_process:
|
||||
mov eax, dword [{{.WRK}}] ; al=[state]
|
||||
fld dword [{{.WRK}}+4] ; x=[level]
|
||||
cmp al, {{.InputNumber "envelopexp" "sustain"}} ; if (al==SUSTAIN)
|
||||
je su_op_envelopexp_sustain
|
||||
su_op_envelopexp_attac:
|
||||
cmp al, {{.InputNumber "envelopexp" "attack"}} ; if (al!=ATTAC)
|
||||
jne short su_op_envelopexp_decay ; goto decay
|
||||
; qm210: see above. if in attack, let r10 point to exp_attack
|
||||
lea r10, [{{.Input "envelopexp" "exp_attack"}}]
|
||||
{{.Call "su_nonlinear_map"}} ; a x, where a=attack
|
||||
faddp st1, st0 ; a+x
|
||||
fld1 ; 1 a+x
|
||||
fucomi st1 ; if (a+x<=1) // is attack complete?
|
||||
fcmovnb st0, st1 ; a+x a+x
|
||||
jbe short su_op_envelopexp_statechange ; else goto statechange
|
||||
su_op_envelopexp_decay:
|
||||
; <-- qm210: storing baseline
|
||||
cmp al, {{.InputNumber "envelopexp" "decay"}} ; if (al!=DECAY)
|
||||
jne short su_op_envelopexp_release ; goto release
|
||||
; qm210: see above. if in decay, let r10 point to exp_decay, and load the sustain into unit.state[3]
|
||||
lea r10, [{{.Input "envelopexp" "exp_decay"}}]
|
||||
fld dword [{{.Input "envelopexp" "sustain"}}]
|
||||
fstp dword[{{.WRK}} + 12]
|
||||
; <-- qm210
|
||||
{{.Call "su_nonlinear_map"}} ; d x, where d=decay
|
||||
fsubp st1, st0 ; x-d
|
||||
; qm210: we can ignore the sustain here, it will be applied via the "baseline" (cf. above / below)
|
||||
fldz ; 0 x-d
|
||||
fucomi st1 ; if (x-d>0) // is decay complete?
|
||||
fcmovb st0, st1 ; x-d x-d
|
||||
jnc short su_op_envelopexp_statechange ; else goto statechange
|
||||
su_op_envelopexp_release:
|
||||
cmp al, {{.InputNumber "envelopexp" "release"}} ; if (al!=RELEASE)
|
||||
jne short su_op_envelopexp_applyexp ; goto leave
|
||||
mov dword [{{.WRK}} + 12], 0 ; <-- qm210: no baseline anymore
|
||||
{{.Call "su_nonlinear_map"}} ; r x, where r=release
|
||||
fsubp st1, st0 ; x-r
|
||||
fldz ; 0 x-r
|
||||
fucomi st1 ; if (x-r>0) // is release complete?
|
||||
fcmovb st0, st1 ; x-r x-r, then goto leave
|
||||
jc short su_op_envelopexp_skipexp
|
||||
su_op_envelopexp_statechange:
|
||||
; qm210: this was:
|
||||
; inc dword [{{.WRK}}] ; [state]++
|
||||
; but as we land here after attack and decay, which now have another exp_ parameter, skip 2 to reach next state
|
||||
add dword [{{.WRK}}], 2 ; [state]+=2
|
||||
su_op_envelopexp_applyexp:
|
||||
fstp st1 ; x', where x' is the new value
|
||||
; qm120: store the linear envelope in [level] because this is read again for the next value (cf. "envelope")
|
||||
fst dword [{{.WRK}} + 4] ; [level]=x'
|
||||
; qm210: NOW THE ACTUAL EXPONENTIAL SCALING
|
||||
; - scale the exponent in [0; 1] to [0.125; 8], call that kappa = 2^(6*(expo-0.5))
|
||||
fld dword [r10] ; stack: [ expo, x' ]
|
||||
{{.Prepare (.Float 0.5)}}
|
||||
fld dword [{{.Use (.Float 0.5)}}] ; stack: [ 0.5, expo, x' ]
|
||||
fsubrp st1, st0 ; stack: [ expo-0.5, x' ]
|
||||
{{.Prepare (.Int 6)}}
|
||||
fimul dword [{{.Use (.Int 6)}}] ; stack: [ 6*(expo-0.5), x' ]
|
||||
{{.Call "su_power"}} ; stack: [ kappa, x' ]
|
||||
fxch st1 ; stack: [ x', kappa ]
|
||||
; - now we need (x')^(kappa), but care for x' == 0 first
|
||||
fldz ; stack: [ 0, x', kappa ]
|
||||
fucomip st1 ; stack [ x', kappa ] and ZF = (x' == 0)
|
||||
jz su_op_envelopexp_avoid_zero_glitch
|
||||
; - still around? calculate the actual x'' = x^kappa then
|
||||
fyl2x ; stack: [ kappa * log2 x' ]
|
||||
{{.Call "su_power"}} ; stack: [ x ^ kappa ]
|
||||
jmp short su_op_envelopexp_applybaseline
|
||||
su_op_envelopexp_avoid_zero_glitch:
|
||||
fstp st1
|
||||
su_op_envelopexp_applybaseline:
|
||||
; - and scale the result to a different baseline: x''' = (B + (1 - B) * x'') for B != 0 (check not required)
|
||||
fld dword [{{.WRK}} + 12] ; stack: [ B, x'' ]
|
||||
fld1 ; stack: [ 1, B, x'' ]
|
||||
fsub st0, st1 ; stack: [ 1-B, B, x'' ]
|
||||
fmulp st2, st0 ; stack: [ (1-B) * x'', B ]
|
||||
faddp st1, st0 ; stack: [ (1-B) * x'' + B ]
|
||||
jmp short su_op_envelopexp_leave
|
||||
su_op_envelopexp_sustain:
|
||||
; qm210: overwrite level, because else the release cannot work
|
||||
fld dword [{{.Input "envelopexp" "sustain"}}]
|
||||
su_op_envelopexp_skipexp:
|
||||
fst dword [{{.WRK}} + 4]
|
||||
fstp st1
|
||||
su_op_envelopexp_leave:
|
||||
; qm210: scaling because I use my wave editor as a function plotter ;)
|
||||
fmul dword [{{.Input "envelopexp" "gain"}}] ; [gain]*x''
|
||||
ret
|
||||
{{end}}
|
||||
|
||||
|
||||
{{- if .HasOp "noise"}}
|
||||
;-------------------------------------------------------------------------------
|
||||
; NOISE opcode: creates noise
|
||||
|
||||
@ -329,6 +329,45 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
||||
if stereo {
|
||||
stack = append(stack, output)
|
||||
}
|
||||
case opEnvelopexp:
|
||||
if !voices[0].sustain {
|
||||
unit.state[0] = envStateRelease // set state to release
|
||||
}
|
||||
state := unit.state[0]
|
||||
level := unit.state[1]
|
||||
exponent := float64(1)
|
||||
baseline := float32(0)
|
||||
switch state {
|
||||
case envStateAttack:
|
||||
exponent = scaledEnvelopExponent(params[1])
|
||||
level += nonLinearMap(params[0])
|
||||
if level >= 1 {
|
||||
level = 1
|
||||
state = envStateDecay
|
||||
}
|
||||
case envStateDecay:
|
||||
exponent = scaledEnvelopExponent(params[3])
|
||||
sustain := params[4]
|
||||
baseline = sustain
|
||||
level -= nonLinearMap(params[2])
|
||||
if level <= sustain {
|
||||
level = sustain
|
||||
}
|
||||
case envStateRelease:
|
||||
level -= nonLinearMap(params[5])
|
||||
if level <= 0 {
|
||||
level = 0
|
||||
}
|
||||
}
|
||||
unit.state[0] = state
|
||||
unit.state[1] = level
|
||||
expLevel := float32(math.Pow(float64(level), exponent))
|
||||
output := (baseline + (1-baseline)*expLevel) * params[6]
|
||||
stack = append(stack, output)
|
||||
if stereo {
|
||||
stack = append(stack, output)
|
||||
}
|
||||
// <-- END TODO @qm210 -- ACTUALLY IMPLEMENT, BUT FIRST DO THE NATIVE ASM PART
|
||||
case opNoise:
|
||||
if stereo {
|
||||
value := waveshape(synth.rand(), params[0]) * params[1]
|
||||
@ -610,6 +649,10 @@ func nonLinearMap(value float32) float32 {
|
||||
return float32(math.Exp2(float64(-24 * value)))
|
||||
}
|
||||
|
||||
func scaledEnvelopExponent(value float32) float64 {
|
||||
return math.Pow(2, 6*(0.5-float64(value)))
|
||||
}
|
||||
|
||||
func clip(value float32) float32 {
|
||||
if value < -1 {
|
||||
return -1
|
||||
|
||||
@ -12,27 +12,28 @@ const (
|
||||
opDelay = 8
|
||||
opDistort = 9
|
||||
opEnvelope = 10
|
||||
opFilter = 11
|
||||
opGain = 12
|
||||
opHold = 13
|
||||
opIn = 14
|
||||
opInvgain = 15
|
||||
opLoadnote = 16
|
||||
opLoadval = 17
|
||||
opMul = 18
|
||||
opMulp = 19
|
||||
opNoise = 20
|
||||
opOscillator = 21
|
||||
opOut = 22
|
||||
opOutaux = 23
|
||||
opPan = 24
|
||||
opPop = 25
|
||||
opPush = 26
|
||||
opReceive = 27
|
||||
opSend = 28
|
||||
opSpeed = 29
|
||||
opSync = 30
|
||||
opXch = 31
|
||||
opEnvelopexp = 11
|
||||
opFilter = 12
|
||||
opGain = 13
|
||||
opHold = 14
|
||||
opIn = 15
|
||||
opInvgain = 16
|
||||
opLoadnote = 17
|
||||
opLoadval = 18
|
||||
opMul = 19
|
||||
opMulp = 20
|
||||
opNoise = 21
|
||||
opOscillator = 22
|
||||
opOut = 23
|
||||
opOutaux = 24
|
||||
opPan = 25
|
||||
opPop = 26
|
||||
opPush = 27
|
||||
opReceive = 28
|
||||
opSend = 29
|
||||
opSpeed = 30
|
||||
opSync = 31
|
||||
opXch = 32
|
||||
)
|
||||
|
||||
var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 1, 4, 1, 5, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0, 0}
|
||||
var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 1, 4, 1, 5, 7, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0, 0}
|
||||
|
||||
19
write_envelexp_wav.ps1
Normal file
19
write_envelexp_wav.ps1
Normal file
@ -0,0 +1,19 @@
|
||||
cd build
|
||||
ninja sointu
|
||||
cd ..
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
if ($args -contains "native") {
|
||||
|
||||
Write-Host "Render with ASM synth"
|
||||
go run -tags=native .\cmd\sointu-play\main.go -w .\examples\envelopexp_dev.yml
|
||||
|
||||
} elseif ($args -contains "go") {
|
||||
|
||||
Write-Host "Render with GO synth"
|
||||
go run .\cmd\sointu-play\main.go -w .\examples\envelopexp_dev.yml
|
||||
|
||||
} else {
|
||||
Write-Host "specify either ""native"" or ""go"" argument."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user