feat: add ability to import 4klang patches and instruments

This commit is contained in:
5684185+vsariola@users.noreply.github.com 2023-07-06 23:47:55 +03:00
parent c06ac6ea5e
commit 248ba483c6
87 changed files with 643 additions and 55 deletions

519
4klang.go Normal file
View File

@ -0,0 +1,519 @@
package sointu
import (
"bytes"
"encoding/binary"
"fmt"
"io"
)
func Read4klangPatch(r io.Reader) (patch Patch, err error) {
var versionTag uint32
var version int
var polyphonyUint32 uint32
var polyphony int
var instrumentNames [_4KLANG_MAX_INSTRS]string
patch = make(Patch, 0)
if err := binary.Read(r, binary.LittleEndian, &versionTag); err != nil {
return nil, fmt.Errorf("binary.Read: %w", err)
}
var ok bool
if version, ok = _4klangVersionTags[versionTag]; !ok {
return nil, fmt.Errorf("unknown 4klang version tag: %d", versionTag)
}
if err := binary.Read(r, binary.LittleEndian, &polyphonyUint32); err != nil {
return nil, fmt.Errorf("binary.Read: %w", err)
}
polyphony = int(polyphonyUint32)
if polyphony < 1 {
polyphony = 1
}
for i := range instrumentNames {
instrumentNames[i], err = read4klangName(r)
if err != nil {
return nil, fmt.Errorf("read4klangName: %w", err)
}
}
m := make(_4klangTargetMap)
id := 1
for instrIndex := 0; instrIndex < _4KLANG_MAX_INSTRS; instrIndex++ {
var units []Unit
if units, err = read4klangUnits(r, version, instrIndex, m, &id); err != nil {
return nil, fmt.Errorf("read4klangUnits: %w", err)
}
if len(units) > 0 {
patch = append(patch, Instrument{Name: instrumentNames[instrIndex], NumVoices: polyphony, Units: units})
}
}
var units []Unit
if units, err = read4klangUnits(r, version, _4KLANG_MAX_INSTRS, m, &id); err != nil {
return nil, fmt.Errorf("read4klangUnits: %w", err)
}
if len(units) > 0 {
patch = append(patch, Instrument{Name: "Global", NumVoices: 1, Units: units})
}
for i, instr := range patch {
fix4klangTargets(i, instr, m)
}
return
}
func Read4klangInstrument(r io.Reader) (instr Instrument, err error) {
var versionTag uint32
var version int
var name string
if err := binary.Read(r, binary.LittleEndian, &versionTag); err != nil {
return Instrument{}, fmt.Errorf("binary.Read: %w", err)
}
var ok bool
if version, ok = _4klangVersionTags[versionTag]; !ok {
return Instrument{}, fmt.Errorf("unknown 4klang version tag: %d", versionTag)
}
if name, err = read4klangName(r); err != nil {
return Instrument{}, fmt.Errorf("read4klangName: %w", err)
}
var units []Unit
id := 1
m := make(_4klangTargetMap)
if units, err = read4klangUnits(r, version, 0, m, &id); err != nil {
return Instrument{}, fmt.Errorf("read4klangUnits: %w", err)
}
ret := Instrument{Name: name, NumVoices: 1, Units: units}
fix4klangTargets(0, ret, m)
return ret, nil
}
type (
_4klangStackUnit struct {
stack, unit int
}
_4klangTargetMap map[_4klangStackUnit]int
_4klangPorts struct {
UnitType string
PortName [8]string
}
)
const (
_4KLANG_MAX_INSTRS = 16
_4KLANG_MAX_UNITS = 64
_4KLANG_MAX_SLOTS = 16
_4KLANG_MAX_NAME_LEN = 64
)
var (
_4klangVersionTags map[uint32]int = map[uint32]int{
0x31316b34: 11, // 4k11
0x32316b34: 12, // 4k12
0x33316b34: 13, // 4k13
0x34316b34: 14, // 4k14
}
_4klangDelays []int = []int{ // these are the numerators, if denominator is 48, fraction of beat time
4, // 0 = 4.0f * (1.0f/32.0f) * (2.0f/3.0f)
6, // 1 = 4.0f * (1.0f/32.0f),
9, // 2 = 4.0f * (1.0f/32.0f) * (3.0f/2.0f),
8, // 3 = 4.0f * (1.0f/16.0f) * (2.0f/3.0f),
12, // 4 = 4.0f * (1.0f/16.0f),
18, // 5 = 4.0f * (1.0f/16.0f) * (3.0f/2.0f),
16, // 6 = 4.0f * (1.0f/8.0f) * (2.0f/3.0f),
24, // 7 = 4.0f * (1.0f/8.0f),
36, // 8 = 4.0f * (1.0f/8.0f) * (3.0f/2.0f),
32, // 9 = 4.0f * (1.0f/4.0f) * (2.0f/3.0f),
48, // 10 = 4.0f * (1.0f/4.0f),
72, // 11 = 4.0f * (1.0f/4.0f) * (3.0f/2.0f),
64, // 12 = 4.0f * (1.0f/2.0f) * (2.0f/3.0f),
96, // 13 = 4.0f * (1.0f/2.0f),
144, // 14 = 4.0f * (1.0f/2.0f) * (3.0f/2.0f),
128, // 15 = 4.0f * (1.0f) * (2.0f/3.0f),
192, // 16 = 4.0f * (1.0f),
288, // 17 = 4.0f * (1.0f) * (3.0f/2.0f),
256, // 18 = 4.0f * (2.0f) * (2.0f/3.0f),
384, // 19 = 4.0f * (2.0f),
576, // 20 = 4.0f * (2.0f) * (3.0f/2.0f),
72, // 21 = 4.0f * (3.0f/8.0f),
120, // 22 = 4.0f * (5.0f/8.0f),
168, // 23 = 4.0f * (7.0f/8.0f),
216, // 24 = 4.0f * (9.0f/8.0f),
264, // 25 = 4.0f * (11.0f/8.0f),
312, // 26 = 4.0f * (13.0f/8.0f),
360, // 27 = 4.0f * (15.0f/8.0f),
144, // 28 = 4.0f * (3.0f/4.0f),
240, // 29 = 4.0f * (5.0f/4.0f),
336, // 30 = 4.0f * (7.0f/4.0f),
288, // 31 = 4.0f * (3.0f/2.0f),
288, // 32 = 4.0f * (3.0f/2.0f),
}
_4klangUnitPorts []_4klangPorts = []_4klangPorts{
{"", [8]string{"", "", "", "", "", "", "", ""}},
{"envelope", [8]string{"", "", "gain", "attack", "decay", "", "release", ""}},
{"oscillator", [8]string{"", "transpose", "detune", "", "phase", "color", "shape", "gain"}},
{"filter", [8]string{"", "", "", "", "frequency", "resonance", "", ""}},
{"envelope", [8]string{"", "", "drive", "frequency", "", "", "", ""}},
{"delay", [8]string{"pregain", "feedback", "dry", "damp", "", "", "", ""}},
{"", [8]string{"", "", "", "", "", "", "", ""}},
{"", [8]string{"", "", "", "", "", "", "", ""}},
{"pan", [8]string{"panning", "", "", "", "", "", "", ""}},
{"outaux", [8]string{"auxgain", "outgain", "", "", "", "", "", ""}},
{"", [8]string{"", "", "", "", "", "", "", ""}},
{"load", [8]string{"value", "", "", "", "", "", "", ""}},
}
)
func read4klangName(r io.Reader) (string, error) {
var name [_4KLANG_MAX_NAME_LEN]byte
if err := binary.Read(r, binary.LittleEndian, &name); err != nil {
return "", fmt.Errorf("binary.Read: %w", err)
}
n := bytes.IndexByte(name[:], 0)
if n == -1 {
n = _4KLANG_MAX_NAME_LEN
}
return string(name[:n]), nil
}
func read4klangUnits(r io.Reader, version, instrIndex int, m _4klangTargetMap, id *int) (units []Unit, err error) {
numUnits := _4KLANG_MAX_UNITS
if version <= 13 {
numUnits = 32
}
units = make([]Unit, 0, numUnits)
for unitIndex := 0; unitIndex < numUnits; unitIndex++ {
var u []Unit
if u, err = read4klangUnit(r, version); err != nil {
return nil, fmt.Errorf("read4klangUnit: %w", err)
}
if u == nil {
continue
}
m[_4klangStackUnit{instrIndex, unitIndex}] = *id
for i := range u {
u[i].ID = *id
*id++
}
units = append(units, u...)
}
return
}
func read4klangUnit(r io.Reader, version int) ([]Unit, error) {
var unitType byte
if err := binary.Read(r, binary.LittleEndian, &unitType); err != nil {
return nil, fmt.Errorf("binary.Read: %w", err)
}
var vals [15]byte
if err := binary.Read(r, binary.LittleEndian, &vals); err != nil {
return nil, fmt.Errorf("binary.Read: %w", err)
}
if version <= 13 {
// versions <= 13 had 16 unused slots for each unit
if written, err := io.CopyN(io.Discard, r, 16); err != nil || written < 16 {
return nil, fmt.Errorf("io.CopyN: %w", err)
}
}
switch unitType {
case 1:
return read4klangENV(vals, version), nil
case 2:
return read4klangVCO(vals, version), nil
case 3:
return read4klangVCF(vals, version), nil
case 4:
return read4klangDST(vals, version), nil
case 5:
return read4klangDLL(vals, version), nil
case 6:
return read4klangFOP(vals, version), nil
case 7:
return read4klangFST(vals, version), nil
case 8:
return read4klangPAN(vals, version), nil
case 9:
return read4klangOUT(vals, version), nil
case 10:
return read4klangACC(vals, version), nil
case 11:
return read4klangFLD(vals, version), nil
default:
return nil, nil
}
}
func read4klangENV(vals [15]byte, version int) []Unit {
return []Unit{{
Type: "envelope",
Parameters: map[string]int{
"stereo": 0,
"attack": int(vals[0]),
"decay": int(vals[1]),
"sustain": int(vals[2]),
"release": int(vals[3]),
"gain": int(vals[4]),
},
}}
}
func read4klangVCO(vals [15]byte, version int) []Unit {
v := vals[:8]
var transpose, detune, phase, color, gate, shape, gain, flags, stereo, typ, lfo int
transpose, v = int(v[0]), v[1:]
detune, v = int(v[0]), v[1:]
phase, v = int(v[0]), v[1:]
if version <= 11 {
gate = 0x55
} else {
gate, v = int(v[0]), v[1:]
}
color, v = int(v[0]), v[1:]
shape, v = int(v[0]), v[1:]
gain, v = int(v[0]), v[1:]
flags, v = int(v[0]), v[1:]
if flags&0x10 == 0x10 {
lfo = 1
}
if flags&0x40 == 0x40 {
stereo = 1
}
switch {
case flags&0x01 == 0x01: // Sine
typ = Sine
if version <= 13 {
color = 128
}
case flags&0x02 == 0x02: // Trisaw
typ = Trisaw
case flags&0x04 == 0x04: // Pulse
typ = Pulse
case flags&0x08 == 0x08: // Noise is handled differently in sointu
return []Unit{{
Type: "noise",
Parameters: map[string]int{
"stereo": stereo,
"shape": shape,
"gain": gain,
},
}}
case flags&0x20 == 0x20: // Gate
color = gate
}
return []Unit{{
Type: "oscillator",
Parameters: map[string]int{
"stereo": stereo,
"transpose": transpose,
"detune": detune,
"phase": phase,
"color": color,
"shape": shape,
"gain": gain,
"type": typ,
"lfo": lfo,
},
}}
}
func read4klangVCF(vals [15]byte, version int) []Unit {
flags := vals[2]
var stereo, lowpass, bandpass, highpass, neghighpass int
if flags&0x01 == 0x01 {
lowpass = 1
}
if flags&0x02 == 0x02 {
highpass = 1
}
if flags&0x04 == 0x04 {
bandpass = 1
}
if flags&0x08 == 0x08 {
lowpass = 1
neghighpass = 1
}
if flags&0x10 == 0x10 {
stereo = 1
}
return []Unit{{
Type: "filter",
Parameters: map[string]int{
"stereo": stereo,
"frequency": int(vals[0]),
"resonance": int(vals[1]),
"lowpass": lowpass,
"bandpass": bandpass,
"highpass": highpass,
"negbandpass": 0,
"neghighpass": neghighpass,
}},
}
}
func read4klangDST(vals [15]byte, version int) []Unit {
return []Unit{
{Type: "distort", Parameters: map[string]int{"drive": int(vals[0]), "stereo": int(vals[2])}},
{Type: "hold", Parameters: map[string]int{"holdfreq": int(vals[1]), "stereo": int(vals[2])}},
}
}
func read4klangDLL(vals [15]byte, version int) []Unit {
var delaytimes []int
var notetracking int
if vals[11] > 0 {
if vals[10] > 0 { // left reverb
delaytimes = []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618}
} else { // right reverb
delaytimes = []int{1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642}
}
} else {
synctype := vals[9]
switch synctype {
case 0:
delaytimes = []int{int(vals[8]) * 16}
case 1: // relative to BPM
notetracking = 2
index := vals[8] >> 2
delaytime := 48
if int(index) < len(_4klangDelays) {
delaytime = _4klangDelays[index]
}
delaytimes = []int{delaytime}
case 2: // notetracking
notetracking = 1
delaytimes = []int{10787}
}
}
return []Unit{{
Type: "delay",
Parameters: map[string]int{
"stereo": 0,
"pregain": int(vals[0]),
"dry": int(vals[1]),
"feedback": int(vals[2]),
"damp": int(vals[3]),
"notetracking": notetracking,
},
VarArgs: delaytimes,
}}
}
func read4klangFOP(vals [15]byte, version int) []Unit {
var t string
var stereo int
switch vals[0] {
case 1:
t, stereo = "pop", 0
case 2:
t, stereo = "addp", 0
case 3:
t, stereo = "mulp", 0
case 4:
t, stereo = "push", 0
case 5:
t, stereo = "xch", 0
case 6:
t, stereo = "add", 0
case 7:
t, stereo = "mul", 0
case 8:
t, stereo = "addp", 1
case 9:
return []Unit{{Type: "loadnote", Parameters: map[string]int{"stereo": stereo}}, // 4klang loadnote gives 0..1, sointu gives -1..1
{Type: "loadval", Parameters: map[string]int{"value": 128, "stereo": stereo}},
{Type: "addp", Parameters: map[string]int{"stereo": stereo}},
{Type: "gain", Parameters: map[string]int{"stereo": stereo, "gain": 64}}}
default:
t, stereo = "mulp", 1
}
return []Unit{{
Type: t,
Parameters: map[string]int{"stereo": stereo},
}}
}
func read4klangFST(vals [15]byte, version int) []Unit {
sendpop := 0
if vals[1]&0x40 == 0x40 {
sendpop = 1
}
return []Unit{{
Type: "send",
Parameters: map[string]int{
"amount": int(vals[0]),
"sendpop": sendpop,
"dest_stack": int(vals[2]),
"dest_unit": int(vals[3]),
"dest_slot": int(vals[4]),
"dest_id": int(vals[5]),
}}}
}
func fix4klangTargets(instrIndex int, instr Instrument, m _4klangTargetMap) {
for _, u := range instr.Units {
if u.Type == "send" {
destStack := u.Parameters["dest_stack"]
if destStack == 255 {
destStack = instrIndex
}
fourKlangTarget := _4klangStackUnit{
destStack,
u.Parameters["dest_unit"]}
u.Parameters["target"] = m[fourKlangTarget]
if u.Parameters["dest_id"] < len(_4klangUnitPorts) && u.Parameters["dest_slot"] < 8 {
if u.Parameters["dest_id"] == 4 && u.Parameters["dest_slot"] == 3 { // distortion is split into 2 units
u.Parameters["target"]++
u.Parameters["port"] = 0
} else {
modTarget := _4klangUnitPorts[u.Parameters["dest_id"]]
for i, s := range Ports[modTarget.UnitType] {
if s == modTarget.PortName[u.Parameters["dest_slot"]] {
u.Parameters["port"] = i
break
}
}
}
}
delete(u.Parameters, "dest_stack")
delete(u.Parameters, "dest_unit")
delete(u.Parameters, "dest_slot")
delete(u.Parameters, "dest_id")
}
}
}
func read4klangPAN(vals [15]byte, version int) []Unit {
return []Unit{{
Type: "pan",
Parameters: map[string]int{
"stereo": 0,
"panning": int(vals[0]),
}}}
}
func read4klangOUT(vals [15]byte, version int) []Unit {
return []Unit{{
Type: "outaux",
Parameters: map[string]int{
"stereo": 1,
"outgain": int(vals[0]),
"auxgain": int(vals[1])},
}}
}
func read4klangACC(vals [15]byte, version int) []Unit {
c := 0
if vals[0] != 0 {
c = 2
}
return []Unit{{
Type: "in",
Parameters: map[string]int{"stereo": 1, "channel": c},
}}
}
func read4klangFLD(vals [15]byte, version int) []Unit {
return []Unit{{
Type: "loadval",
Parameters: map[string]int{"stereo": 0, "value": int(vals[0])},
}}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -192,6 +192,18 @@ func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string {
} }
return fmt.Sprintf(portList[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"
}
}
} }
return "" return ""
} }

View File

@ -22,7 +22,7 @@ type Synth interface {
// delaylines should keep their content. Every change in the Patch triggers // delaylines should keep their content. Every change in the Patch triggers
// an Update and if the Patch would be started fresh every time, it would // an Update and if the Patch would be started fresh every time, it would
// lead to very choppy audio. // lead to very choppy audio.
Update(patch Patch) error Update(patch Patch, bpm int) error
// Trigger triggers a note for a given voice. Called between synth.Renders. // Trigger triggers a note for a given voice. Called between synth.Renders.
Trigger(voice int, note byte) Trigger(voice int, note byte)
@ -35,7 +35,7 @@ type Synth interface {
// SynthService compiles a given Patch into a Synth, throwing errors if the // SynthService compiles a given Patch into a Synth, throwing errors if the
// Patch is malformed. // Patch is malformed.
type SynthService interface { type SynthService interface {
Compile(patch Patch) (Synth, error) Compile(patch Patch, bpm int) (Synth, error)
} }
// Render fills an stereo audio buffer using a Synth, disregarding all syncs and // Render fills an stereo audio buffer using a Synth, disregarding all syncs and
@ -62,7 +62,7 @@ func Play(synthService SynthService, song Song, release bool) ([]float32, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
synth, err := synthService.Compile(song.Patch) synth, err := synthService.Compile(song.Patch, song.BPM)
if err != nil { if err != nil {
return nil, fmt.Errorf("sointu.Play failed: %v", err) return nil, fmt.Errorf("sointu.Play failed: %v", err)
} }

View File

@ -106,9 +106,9 @@ func (f *FileDialogStyle) Layout(gtx C) D {
n = n[0 : len(n)-len(extension)] n = n[0 : len(n)-len(extension)]
switch f.dialog.UseAltExt.Value { switch f.dialog.UseAltExt.Value {
case true: case true:
n += ".json" n += f.ExtAlt
default: default:
n += ".yml" n += f.ExtMain
} }
f.dialog.FileName.SetText(n) f.dialog.FileName.SetText(n)
} }

View File

@ -4,6 +4,7 @@
package gioui package gioui
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -66,15 +67,23 @@ func (t *Tracker) SaveInstrument() {
} }
func (t *Tracker) loadSong(filename string) { func (t *Tracker) loadSong(filename string) {
bytes, err := ioutil.ReadFile(filename) b, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return return
} }
var song sointu.Song var song sointu.Song
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil { if errJSON := json.Unmarshal(b, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil { if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error, time.Second*3) var err4kp error
return var patch sointu.Patch
if patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b)); err4kp != nil {
t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v / %v", errYaml, errJSON, err4kp), Error, time.Second*3)
return
} else {
song = t.Song()
song.Score = t.Song().Score.Copy()
song.Patch = patch
}
} }
} }
if song.Score.Length <= 0 || len(song.Score.Tracks) == 0 || len(song.Patch) == 0 { if song.Score.Length <= 0 || len(song.Score.Tracks) == 0 || len(song.Patch) == 0 {
@ -148,17 +157,22 @@ func (t *Tracker) saveInstrument(filename string) bool {
} }
func (t *Tracker) loadInstrument(filename string) bool { func (t *Tracker) loadInstrument(filename string) bool {
bytes, err := ioutil.ReadFile(filename) b, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return false return false
} }
var instrument sointu.Instrument var instrument sointu.Instrument
if errJSON := json.Unmarshal(bytes, &instrument); errJSON != nil { if errJSON := json.Unmarshal(b, &instrument); errJSON != nil {
if errYaml := yaml.Unmarshal(bytes, &instrument); errYaml != nil { if errYaml := yaml.Unmarshal(b, &instrument); errYaml != nil {
t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v", errYaml, errJSON), Error, time.Second*3) var err4ki error
return false if instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b)); err4ki != nil {
t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v", errYaml, errJSON, err4ki), Error, time.Second*3)
return false
}
} }
} }
// the 4klang instrument names are junk, replace them with the filename without extension
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
if len(instrument.Units) == 0 { if len(instrument.Units) == 0 {
t.Alert.Update("The instrument file is malformed", Error, time.Second*3) t.Alert.Update("The instrument file is malformed", Error, time.Second*3)
return false return false

View File

@ -58,6 +58,7 @@ func (t *Tracker) Layout(gtx layout.Context) {
} }
fstyle := OpenFileDialog(t.Theme, t.OpenSongDialog) fstyle := OpenFileDialog(t.Theme, t.OpenSongDialog)
fstyle.Title = "Open Song File" fstyle.Title = "Open Song File"
fstyle.ExtAlt = ".4kp"
fstyle.Layout(gtx) fstyle.Layout(gtx)
for ok, file := t.OpenSongDialog.FileSelected(); ok; ok, file = t.OpenSongDialog.FileSelected() { for ok, file := t.OpenSongDialog.FileSelected(); ok; ok, file = t.OpenSongDialog.FileSelected() {
t.loadSong(file) t.loadSong(file)
@ -89,6 +90,7 @@ func (t *Tracker) Layout(gtx layout.Context) {
fstyle.Layout(gtx) fstyle.Layout(gtx)
fstyle = OpenFileDialog(t.Theme, t.OpenInstrumentDialog) fstyle = OpenFileDialog(t.Theme, t.OpenInstrumentDialog)
fstyle.Title = "Open Instrument File" fstyle.Title = "Open Instrument File"
fstyle.ExtAlt = ".4ki"
for ok, file := t.OpenInstrumentDialog.FileSelected(); ok; ok, file = t.OpenInstrumentDialog.FileSelected() { for ok, file := t.OpenInstrumentDialog.FileSelected(); ok; ok, file = t.OpenInstrumentDialog.FileSelected() {
t.loadInstrument(file) t.loadInstrument(file)
} }

View File

@ -66,7 +66,7 @@ type (
} }
ModelSamplesPerRowChangedMessage struct { ModelSamplesPerRowChangedMessage struct {
int BPM, RowsPerBeat int
} }
ModelPanicMessage struct { ModelPanicMessage struct {
@ -1088,14 +1088,43 @@ func (m *Model) Param(index int) (Parameter, error) {
if index < len(unit.VarArgs) { if index < len(unit.VarArgs) {
val := unit.VarArgs[index] val := unit.VarArgs[index]
var text string var text string
if unit.Parameters["notetracking"] == 1 { switch unit.Parameters["notetracking"] {
default:
case 0:
text = fmt.Sprintf("%v / %.3f rows", val, float32(val)/float32(m.song.SamplesPerRow()))
return Parameter{Type: IntegerParameter, Min: 1, Max: 65535, Name: "delaytime", Hint: text, Value: val, LargeStep: 256}, nil
case 1:
relPitch := float64(val) / 10787 relPitch := float64(val) / 10787
semitones := -math.Log2(relPitch) * 12 semitones := -math.Log2(relPitch) * 12
text = fmt.Sprintf("%v / %.3f st", val, semitones) text = fmt.Sprintf("%v / %.3f st", val, semitones)
} else { return Parameter{Type: IntegerParameter, Min: 1, Max: 65535, Name: "delaytime", Hint: text, Value: val, LargeStep: 256}, nil
text = fmt.Sprintf("%v / %.3f rows", val, float32(val)/float32(m.song.SamplesPerRow())) case 2:
k := 0
v := val
for v&1 == 0 { // divide val by 2 until it is odd
v >>= 1
k++
}
text := ""
switch v {
case 1:
if k <= 7 {
text = fmt.Sprintf(" (1/%d triplet)", 1<<(7-k))
}
case 3:
if k <= 6 {
text = fmt.Sprintf(" (1/%d)", 1<<(6-k))
}
break
case 9:
if k <= 5 {
text = fmt.Sprintf(" (1/%d dotted)", 1<<(5-k))
}
}
text = fmt.Sprintf("%v / %.3f beats%s", val, float32(val)/48.0, text)
return Parameter{Type: IntegerParameter, Min: 1, Max: 576, Name: "delaytime", Hint: text, Value: val, LargeStep: 16}, nil
} }
return Parameter{Type: IntegerParameter, Min: 1, Max: 65535, Name: "delaytime", Hint: text, Value: val, LargeStep: 256}, nil
} }
} }
return Parameter{}, errors.New("invalid parameter") return Parameter{}, errors.New("invalid parameter")
@ -1240,7 +1269,7 @@ func (m *Model) notifyScoreChange() {
func (m *Model) notifySamplesPerRowChange() { func (m *Model) notifySamplesPerRowChange() {
select { select {
case m.modelMessages <- ModelSamplesPerRowChangedMessage{m.song.SamplesPerRow()}: case m.modelMessages <- ModelSamplesPerRowChangedMessage{m.song.BPM, m.song.RowsPerBeat}:
default: default:
} }
} }

View File

@ -20,6 +20,7 @@ type (
position SongRow position SongRow
samplesSinceEvent []int samplesSinceEvent []int
samplesPerRow int samplesPerRow int
bpm int
volume Volume volume Volume
voiceStates [vm.MAX_VOICES]float32 voiceStates [vm.MAX_VOICES]float32
@ -242,7 +243,9 @@ loop:
} }
} }
case ModelSamplesPerRowChangedMessage: case ModelSamplesPerRowChangedMessage:
p.samplesPerRow = m.int p.samplesPerRow = 44100 * 60 / (m.BPM * m.RowsPerBeat)
p.bpm = m.BPM
p.compileOrUpdateSynth()
case ModelPlayFromPositionMessage: case ModelPlayFromPositionMessage:
p.playing = true p.playing = true
p.position = m.SongRow p.position = m.SongRow
@ -297,7 +300,7 @@ loop:
func (p *Player) compileOrUpdateSynth() { func (p *Player) compileOrUpdateSynth() {
if p.synth != nil { if p.synth != nil {
err := p.synth.Update(p.patch) err := p.synth.Update(p.patch, p.bpm)
if err != nil { if err != nil {
p.synth = nil p.synth = nil
p.trySend(PlayerCrashMessage{fmt.Errorf("synth.Update: %w", err)}) p.trySend(PlayerCrashMessage{fmt.Errorf("synth.Update: %w", err)})
@ -305,7 +308,7 @@ func (p *Player) compileOrUpdateSynth() {
} }
} else { } else {
var err error var err error
p.synth, err = p.synthService.Compile(p.patch) p.synth, err = p.synthService.Compile(p.patch, p.bpm)
if err != nil { if err != nil {
p.synth = nil p.synth = nil
p.trySend(PlayerCrashMessage{fmt.Errorf("synthService.Compile: %w", err)}) p.trySend(PlayerCrashMessage{fmt.Errorf("synthService.Compile: %w", err)})

View File

@ -58,7 +58,7 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, {Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "feedback", 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: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "notetracking", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, {Name: "notetracking", MinValue: 0, MaxValue: 2, CanSet: true, CanModulate: false},
{Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}}, {Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"compressor": []UnitParameter{ "compressor": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},

View File

@ -33,7 +33,7 @@ type SampleOffset struct {
LoopLength uint16 LoopLength uint16
} }
func Encode(patch sointu.Patch, featureSet FeatureSet) (*BytePatch, error) { func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, error) {
c := BytePatch{PolyphonyBitmask: polyphonyBitmask(patch), NumVoices: uint32(patch.NumVoices())} c := BytePatch{PolyphonyBitmask: polyphonyBitmask(patch), NumVoices: uint32(patch.NumVoices())}
if c.NumVoices > 32 { if c.NumVoices > 32 {
return nil, fmt.Errorf("Sointu does not support more than 32 concurrent voices; patch uses %v", c.NumVoices) return nil, fmt.Errorf("Sointu does not support more than 32 concurrent voices; patch uses %v", c.NumVoices)
@ -42,7 +42,7 @@ func Encode(patch sointu.Patch, featureSet FeatureSet) (*BytePatch, error) {
globalAddrs := map[int]uint16{} globalAddrs := map[int]uint16{}
globalFixups := map[int]([]int){} globalFixups := map[int]([]int){}
voiceNo := 0 voiceNo := 0
delayTable, delayIndices := constructDelayTimeTable(patch) delayTable, delayIndices := constructDelayTimeTable(patch, bpm)
c.DelayTimes = make([]uint16, len(delayTable)) c.DelayTimes = make([]uint16, len(delayTable))
for i := range delayTable { for i := range delayTable {
c.DelayTimes[i] = uint16(delayTable[i]) c.DelayTimes[i] = uint16(delayTable[i])
@ -171,7 +171,7 @@ func Encode(patch sointu.Patch, featureSet FeatureSet) (*BytePatch, error) {
if count == 0 { if count == 0 {
continue // skip encoding delays without any delay lines continue // skip encoding delays without any delay lines
} }
countTrack := count*2 - 1 + unit.Parameters["notetracking"] // 1 means no note tracking and 1 delay, 2 means notetracking with 1 delay, 3 means no note tracking and 2 delays etc. countTrack := count*2 - 1 + (unit.Parameters["notetracking"] & 1) // 1 means no note tracking and 1 delay, 2 means notetracking with 1 delay, 3 means no note tracking and 2 delays etc.
values = append(values, byte(delayIndices[instrIndex][unitIndex]), byte(countTrack)) values = append(values, byte(delayIndices[instrIndex][unitIndex]), byte(countTrack))
} }
c.Commands = append(c.Commands, byte(opcode+unit.Parameters["stereo"])) c.Commands = append(c.Commands, byte(opcode+unit.Parameters["stereo"]))

View File

@ -16,17 +16,17 @@ import (
type BridgeService struct { type BridgeService struct {
} }
func (s BridgeService) Compile(patch sointu.Patch) (sointu.Synth, error) { func (s BridgeService) Compile(patch sointu.Patch, bpm int) (sointu.Synth, error) {
synth, err := Synth(patch) synth, err := Synth(patch, bpm)
return synth, err return synth, err
} }
func Synth(patch sointu.Patch) (*C.Synth, error) { func Synth(patch sointu.Patch, bpm int) (*C.Synth, error) {
s := new(C.Synth) s := new(C.Synth)
if n := patch.NumDelayLines(); n > 64 { if n := patch.NumDelayLines(); n > 64 {
return nil, fmt.Errorf("native bridge has currently a hard limit of 64 delaylines; patch uses %v", n) return nil, fmt.Errorf("native bridge has currently a hard limit of 64 delaylines; patch uses %v", n)
} }
comPatch, err := vm.Encode(patch, vm.AllFeatures{}) comPatch, err := vm.Encode(patch, vm.AllFeatures{}, bpm)
if err != nil { if err != nil {
return nil, fmt.Errorf("error compiling patch: %v", err) return nil, fmt.Errorf("error compiling patch: %v", err)
} }
@ -109,11 +109,11 @@ func (s *C.Synth) Release(voice int) {
} }
// Update // Update
func (s *C.Synth) Update(patch sointu.Patch) error { func (s *C.Synth) Update(patch sointu.Patch, bpm int) error {
if n := patch.NumDelayLines(); n > 64 { if n := patch.NumDelayLines(); n > 64 {
return fmt.Errorf("native bridge has currently a hard limit of 64 delaylines; patch uses %v", n) return fmt.Errorf("native bridge has currently a hard limit of 64 delaylines; patch uses %v", n)
} }
comPatch, err := vm.Encode(patch, vm.AllFeatures{}) comPatch, err := vm.Encode(patch, vm.AllFeatures{}, bpm)
if err != nil { if err != nil {
return fmt.Errorf("error compiling patch: %v", err) return fmt.Errorf("error compiling patch: %v", err)
} }

View File

@ -54,7 +54,7 @@ func TestRenderSamples(t *testing.T) {
sointu.Unit{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}}, sointu.Unit{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
}}} }}}
synth, err := bridge.Synth(patch) synth, err := bridge.Synth(patch, 120)
if err != nil { if err != nil {
t.Fatalf("bridge compile error: %v", err) t.Fatalf("bridge compile error: %v", err)
} }
@ -130,7 +130,7 @@ func TestStackUnderflow(t *testing.T) {
patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{ patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
sointu.Unit{Type: "pop", Parameters: map[string]int{}}, sointu.Unit{Type: "pop", Parameters: map[string]int{}},
}}} }}}
synth, err := bridge.Synth(patch) synth, err := bridge.Synth(patch, 120)
if err != nil { if err != nil {
t.Fatalf("bridge compile error: %v", err) t.Fatalf("bridge compile error: %v", err)
} }
@ -146,7 +146,7 @@ func TestStackBalancing(t *testing.T) {
sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{ sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
sointu.Unit{Type: "push", Parameters: map[string]int{}}, sointu.Unit{Type: "push", Parameters: map[string]int{}},
}}} }}}
synth, err := bridge.Synth(patch) synth, err := bridge.Synth(patch, 120)
if err != nil { if err != nil {
t.Fatalf("bridge compile error: %v", err) t.Fatalf("bridge compile error: %v", err)
} }
@ -179,7 +179,7 @@ func TestStackOverflow(t *testing.T) {
sointu.Unit{Type: "pop", Parameters: map[string]int{}}, sointu.Unit{Type: "pop", Parameters: map[string]int{}},
sointu.Unit{Type: "pop", Parameters: map[string]int{}}, sointu.Unit{Type: "pop", Parameters: map[string]int{}},
}}} }}}
synth, err := bridge.Synth(patch) synth, err := bridge.Synth(patch, 120)
if err != nil { if err != nil {
t.Fatalf("bridge compile error: %v", err) t.Fatalf("bridge compile error: %v", err)
} }
@ -196,7 +196,7 @@ func TestDivideByZero(t *testing.T) {
sointu.Unit{Type: "invgain", Parameters: map[string]int{"invgain": 0}}, sointu.Unit{Type: "invgain", Parameters: map[string]int{"invgain": 0}},
sointu.Unit{Type: "pop", Parameters: map[string]int{}}, sointu.Unit{Type: "pop", Parameters: map[string]int{}},
}}} }}}
synth, err := bridge.Synth(patch) synth, err := bridge.Synth(patch, 120)
if err != nil { if err != nil {
t.Fatalf("bridge compile error: %v", err) t.Fatalf("bridge compile error: %v", err)
} }

View File

@ -84,7 +84,7 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) {
} }
features := vm.NecessaryFeaturesFor(song.Patch) features := vm.NecessaryFeaturesFor(song.Patch)
retmap := map[string]string{} retmap := map[string]string{}
encodedPatch, err := vm.Encode(song.Patch, features) encodedPatch, err := vm.Encode(song.Patch, features, song.BPM)
if err != nil { if err != nil {
return nil, fmt.Errorf(`could not encode patch: %v`, err) return nil, fmt.Errorf(`could not encode patch: %v`, err)
} }

View File

@ -107,7 +107,7 @@ func findSuperIntArray(arrays [][]int) ([]int, []int) {
// Returns the delay time table and two dimensional array of integers where // Returns the delay time table and two dimensional array of integers where
// element [i][u] is the index for instrument i / unit u in the delay table if // element [i][u] is the index for instrument i / unit u in the delay table if
// the unit was a delay unit. For non-delay untis, the element is just 0. // the unit was a delay unit. For non-delay untis, the element is just 0.
func constructDelayTimeTable(patch sointu.Patch) ([]int, [][]int) { func constructDelayTimeTable(patch sointu.Patch, bpm int) ([]int, [][]int) {
ind := make([][]int, len(patch)) ind := make([][]int, len(patch))
var subarrays [][]int var subarrays [][]int
// flatten the delay times into one array of arrays // flatten the delay times into one array of arrays
@ -119,11 +119,18 @@ func constructDelayTimeTable(patch sointu.Patch) ([]int, [][]int) {
// should use delay times // should use delay times
if unit.Type == "delay" { if unit.Type == "delay" {
ind[i][j] = len(subarrays) ind[i][j] = len(subarrays)
end := unit.Parameters["count"] converted := make([]int, len(unit.VarArgs))
if unit.Parameters["stereo"] > 0 { copy(converted, unit.VarArgs)
end *= 2 if unit.Parameters["notetracking"] == 2 {
for i, t := range converted {
delay := 44100 * 60 * t / 48 / bpm
if delay > 65535 {
delay = 65535
}
converted[i] = delay
}
} }
subarrays = append(subarrays, unit.VarArgs) subarrays = append(subarrays, converted)
} }
} }
} }

View File

@ -63,8 +63,8 @@ const (
envStateRelease envStateRelease
) )
func Synth(patch sointu.Patch) (sointu.Synth, error) { func Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
bytePatch, err := Encode(patch, AllFeatures{}) bytePatch, err := Encode(patch, AllFeatures{}, bpm)
if err != nil { if err != nil {
return nil, fmt.Errorf("error compiling %v", err) return nil, fmt.Errorf("error compiling %v", err)
} }
@ -73,8 +73,8 @@ func Synth(patch sointu.Patch) (sointu.Synth, error) {
return ret, nil return ret, nil
} }
func (s SynthService) Compile(patch sointu.Patch) (sointu.Synth, error) { func (s SynthService) Compile(patch sointu.Patch, bpm int) (sointu.Synth, error) {
synth, err := Synth(patch) synth, err := Synth(patch, bpm)
return synth, err return synth, err
} }
@ -87,8 +87,8 @@ func (s *Interpreter) Release(voiceIndex int) {
s.synth.voices[voiceIndex].release = true s.synth.voices[voiceIndex].release = true
} }
func (s *Interpreter) Update(patch sointu.Patch) error { func (s *Interpreter) Update(patch sointu.Patch, bpm int) error {
bytePatch, err := Encode(patch, AllFeatures{}) bytePatch, err := Encode(patch, AllFeatures{}, bpm)
if err != nil { if err != nil {
return fmt.Errorf("error compiling %v", err) return fmt.Errorf("error compiling %v", err)
} }
@ -140,9 +140,11 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
stereo := channels == 2 stereo := channels == 2
opNoStereo := (op & 0xFE) >> 1 opNoStereo := (op & 0xFE) >> 1
if opNoStereo == 0 { if opNoStereo == 0 {
voices = voices[1:]
units = voices[0].units[:]
voicesRemaining-- voicesRemaining--
if voicesRemaining > 0 {
voices = voices[1:]
units = voices[0].units[:]
}
if mask := uint32(1) << uint32(voicesRemaining); s.bytePatch.PolyphonyBitmask&mask == mask { if mask := uint32(1) << uint32(voicesRemaining); s.bytePatch.PolyphonyBitmask&mask == mask {
commands, values = commandInstr, valuesInstr commands, values = commandInstr, valuesInstr
} else { } else {

View File

@ -76,7 +76,7 @@ func TestStackUnderflow(t *testing.T) {
patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{ patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
sointu.Unit{Type: "pop", Parameters: map[string]int{}}, sointu.Unit{Type: "pop", Parameters: map[string]int{}},
}}} }}}
synth, err := vm.Synth(patch) synth, err := vm.Synth(patch, 120)
if err != nil { if err != nil {
t.Fatalf("bridge compile error: %v", err) t.Fatalf("bridge compile error: %v", err)
} }
@ -92,7 +92,7 @@ func TestStackBalancing(t *testing.T) {
sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{ sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
sointu.Unit{Type: "push", Parameters: map[string]int{}}, sointu.Unit{Type: "push", Parameters: map[string]int{}},
}}} }}}
synth, err := vm.Synth(patch) synth, err := vm.Synth(patch, 120)
if err != nil { if err != nil {
t.Fatalf("bridge compile error: %v", err) t.Fatalf("bridge compile error: %v", err)
} }