diff --git a/4klang.go b/4klang.go new file mode 100644 index 0000000..7fb7eda --- /dev/null +++ b/4klang.go @@ -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])}, + }} +} diff --git a/examples/fourklang/2010_ergon_5.4kp b/examples/fourklang/2010_ergon_5.4kp new file mode 100644 index 0000000..f7e900c Binary files /dev/null and b/examples/fourklang/2010_ergon_5.4kp differ diff --git a/examples/fourklang/LightRythm.4kp b/examples/fourklang/LightRythm.4kp new file mode 100644 index 0000000..abe9b0b Binary files /dev/null and b/examples/fourklang/LightRythm.4kp differ diff --git a/examples/fourklang/baghdad.4kp b/examples/fourklang/baghdad.4kp new file mode 100644 index 0000000..b7fb5a3 Binary files /dev/null and b/examples/fourklang/baghdad.4kp differ diff --git a/examples/fourklang/bf-enlighten.4kp b/examples/fourklang/bf-enlighten.4kp new file mode 100644 index 0000000..180c886 Binary files /dev/null and b/examples/fourklang/bf-enlighten.4kp differ diff --git a/examples/fourklang/c0c00n_001.4kp b/examples/fourklang/c0c00n_001.4kp new file mode 100644 index 0000000..16f28a3 Binary files /dev/null and b/examples/fourklang/c0c00n_001.4kp differ diff --git a/examples/fourklang/dollop.4kp b/examples/fourklang/dollop.4kp new file mode 100644 index 0000000..8070f5c Binary files /dev/null and b/examples/fourklang/dollop.4kp differ diff --git a/examples/fourklang/example.4kp b/examples/fourklang/example.4kp new file mode 100644 index 0000000..9d5ded7 Binary files /dev/null and b/examples/fourklang/example.4kp differ diff --git a/examples/fourklang/example2.4kp b/examples/fourklang/example2.4kp new file mode 100644 index 0000000..cc3bf82 Binary files /dev/null and b/examples/fourklang/example2.4kp differ diff --git a/examples/fourklang/kevinspacy.4kp b/examples/fourklang/kevinspacy.4kp new file mode 100644 index 0000000..2b3372a Binary files /dev/null and b/examples/fourklang/kevinspacy.4kp differ diff --git a/examples/fourklang/punqtured-sundowner.4kp b/examples/fourklang/punqtured-sundowner.4kp new file mode 100644 index 0000000..e218541 Binary files /dev/null and b/examples/fourklang/punqtured-sundowner.4kp differ diff --git a/examples/fourklang/untitled2.4kp b/examples/fourklang/untitled2.4kp new file mode 100644 index 0000000..e8c5d6d Binary files /dev/null and b/examples/fourklang/untitled2.4kp differ diff --git a/examples/fourklang/virgill - 4klang basics.4kp b/examples/fourklang/virgill - 4klang basics.4kp new file mode 100644 index 0000000..2f02041 Binary files /dev/null and b/examples/fourklang/virgill - 4klang basics.4kp differ diff --git a/examples/fourklang_instruments/BA_Dark.4ki b/examples/fourklang_instruments/BA_Dark.4ki new file mode 100644 index 0000000..882c2ca Binary files /dev/null and b/examples/fourklang_instruments/BA_Dark.4ki differ diff --git a/examples/fourklang_instruments/BA_DarkChorus.4ki b/examples/fourklang_instruments/BA_DarkChorus.4ki new file mode 100644 index 0000000..797649d Binary files /dev/null and b/examples/fourklang_instruments/BA_DarkChorus.4ki differ diff --git a/examples/fourklang_instruments/BA_Deepness.4ki b/examples/fourklang_instruments/BA_Deepness.4ki new file mode 100644 index 0000000..15f3842 Binary files /dev/null and b/examples/fourklang_instruments/BA_Deepness.4ki differ diff --git a/examples/fourklang_instruments/BA_DirectPunchMS.4ki b/examples/fourklang_instruments/BA_DirectPunchMS.4ki new file mode 100644 index 0000000..28657eb Binary files /dev/null and b/examples/fourklang_instruments/BA_DirectPunchMS.4ki differ diff --git a/examples/fourklang_instruments/BA_Mighty.4ki b/examples/fourklang_instruments/BA_Mighty.4ki new file mode 100644 index 0000000..4eba539 Binary files /dev/null and b/examples/fourklang_instruments/BA_Mighty.4ki differ diff --git a/examples/fourklang_instruments/BA_Mighty_Feedback.4ki b/examples/fourklang_instruments/BA_Mighty_Feedback.4ki new file mode 100644 index 0000000..d716e68 Binary files /dev/null and b/examples/fourklang_instruments/BA_Mighty_Feedback.4ki differ diff --git a/examples/fourklang_instruments/BA_NotFromThisWorld.4ki b/examples/fourklang_instruments/BA_NotFromThisWorld.4ki new file mode 100644 index 0000000..8c8d820 Binary files /dev/null and b/examples/fourklang_instruments/BA_NotFromThisWorld.4ki differ diff --git a/examples/fourklang_instruments/BA_NotFromThisWorld2.4ki b/examples/fourklang_instruments/BA_NotFromThisWorld2.4ki new file mode 100644 index 0000000..382879a Binary files /dev/null and b/examples/fourklang_instruments/BA_NotFromThisWorld2.4ki differ diff --git a/examples/fourklang_instruments/BA_SawBass.4ki b/examples/fourklang_instruments/BA_SawBass.4ki new file mode 100644 index 0000000..2a17c29 Binary files /dev/null and b/examples/fourklang_instruments/BA_SawBass.4ki differ diff --git a/examples/fourklang_instruments/BA_SawBassFlanger.4ki b/examples/fourklang_instruments/BA_SawBassFlanger.4ki new file mode 100644 index 0000000..e449bbd Binary files /dev/null and b/examples/fourklang_instruments/BA_SawBassFlanger.4ki differ diff --git a/examples/fourklang_instruments/GA_RestInPeaceMS.4ki b/examples/fourklang_instruments/GA_RestInPeaceMS.4ki new file mode 100644 index 0000000..cd3cb67 Binary files /dev/null and b/examples/fourklang_instruments/GA_RestInPeaceMS.4ki differ diff --git a/examples/fourklang_instruments/KY_GarageOrgan.4ki b/examples/fourklang_instruments/KY_GarageOrgan.4ki new file mode 100644 index 0000000..da129ca Binary files /dev/null and b/examples/fourklang_instruments/KY_GarageOrgan.4ki differ diff --git a/examples/fourklang_instruments/KY_GarageOrganChorus.4ki b/examples/fourklang_instruments/KY_GarageOrganChorus.4ki new file mode 100644 index 0000000..a35e405 Binary files /dev/null and b/examples/fourklang_instruments/KY_GarageOrganChorus.4ki differ diff --git a/examples/fourklang_instruments/KY_Lullaby.4ki b/examples/fourklang_instruments/KY_Lullaby.4ki new file mode 100644 index 0000000..ca0eb2e Binary files /dev/null and b/examples/fourklang_instruments/KY_Lullaby.4ki differ diff --git a/examples/fourklang_instruments/KY_Lullaby2.4ki b/examples/fourklang_instruments/KY_Lullaby2.4ki new file mode 100644 index 0000000..0871245 Binary files /dev/null and b/examples/fourklang_instruments/KY_Lullaby2.4ki differ diff --git a/examples/fourklang_instruments/KY_Rhodes.4ki b/examples/fourklang_instruments/KY_Rhodes.4ki new file mode 100644 index 0000000..0292b87 Binary files /dev/null and b/examples/fourklang_instruments/KY_Rhodes.4ki differ diff --git a/examples/fourklang_instruments/LD_AlphaOmegaMS.4ki b/examples/fourklang_instruments/LD_AlphaOmegaMS.4ki new file mode 100644 index 0000000..d46a643 Binary files /dev/null and b/examples/fourklang_instruments/LD_AlphaOmegaMS.4ki differ diff --git a/examples/fourklang_instruments/LD_Farscape.4ki b/examples/fourklang_instruments/LD_Farscape.4ki new file mode 100644 index 0000000..c06a76f Binary files /dev/null and b/examples/fourklang_instruments/LD_Farscape.4ki differ diff --git a/examples/fourklang_instruments/LD_More&MoreMS.4ki b/examples/fourklang_instruments/LD_More&MoreMS.4ki new file mode 100644 index 0000000..7041a2e Binary files /dev/null and b/examples/fourklang_instruments/LD_More&MoreMS.4ki differ diff --git a/examples/fourklang_instruments/LD_Morpher.4ki b/examples/fourklang_instruments/LD_Morpher.4ki new file mode 100644 index 0000000..2669398 Binary files /dev/null and b/examples/fourklang_instruments/LD_Morpher.4ki differ diff --git a/examples/fourklang_instruments/LD_RestInPeaceMS.4ki b/examples/fourklang_instruments/LD_RestInPeaceMS.4ki new file mode 100644 index 0000000..7987ceb Binary files /dev/null and b/examples/fourklang_instruments/LD_RestInPeaceMS.4ki differ diff --git a/examples/fourklang_instruments/LD_Short&PunchyMS.4ki b/examples/fourklang_instruments/LD_Short&PunchyMS.4ki new file mode 100644 index 0000000..b22df3e Binary files /dev/null and b/examples/fourklang_instruments/LD_Short&PunchyMS.4ki differ diff --git a/examples/fourklang_instruments/PA_Fairies.4ki b/examples/fourklang_instruments/PA_Fairies.4ki new file mode 100644 index 0000000..aa4416c Binary files /dev/null and b/examples/fourklang_instruments/PA_Fairies.4ki differ diff --git a/examples/fourklang_instruments/PA_Jarresque.4ki b/examples/fourklang_instruments/PA_Jarresque.4ki new file mode 100644 index 0000000..a1be725 Binary files /dev/null and b/examples/fourklang_instruments/PA_Jarresque.4ki differ diff --git a/examples/fourklang_instruments/PA_JarresqueChorus.4ki b/examples/fourklang_instruments/PA_JarresqueChorus.4ki new file mode 100644 index 0000000..200d74a Binary files /dev/null and b/examples/fourklang_instruments/PA_JarresqueChorus.4ki differ diff --git a/examples/fourklang_instruments/PA_LoFiChoir.4ki b/examples/fourklang_instruments/PA_LoFiChoir.4ki new file mode 100644 index 0000000..8cc7e24 Binary files /dev/null and b/examples/fourklang_instruments/PA_LoFiChoir.4ki differ diff --git a/examples/fourklang_instruments/PA_LongPad.4ki b/examples/fourklang_instruments/PA_LongPad.4ki new file mode 100644 index 0000000..b073699 Binary files /dev/null and b/examples/fourklang_instruments/PA_LongPad.4ki differ diff --git a/examples/fourklang_instruments/PA_Minorium.4ki b/examples/fourklang_instruments/PA_Minorium.4ki new file mode 100644 index 0000000..1a95f09 Binary files /dev/null and b/examples/fourklang_instruments/PA_Minorium.4ki differ diff --git a/examples/fourklang_instruments/PA_Strangeland.4ki b/examples/fourklang_instruments/PA_Strangeland.4ki new file mode 100644 index 0000000..b9339c4 Binary files /dev/null and b/examples/fourklang_instruments/PA_Strangeland.4ki differ diff --git a/examples/fourklang_instruments/PA_StrangelandChorus.4ki b/examples/fourklang_instruments/PA_StrangelandChorus.4ki new file mode 100644 index 0000000..213ffe1 Binary files /dev/null and b/examples/fourklang_instruments/PA_StrangelandChorus.4ki differ diff --git a/examples/fourklang_instruments/PA_SynastasiaMS.4ki b/examples/fourklang_instruments/PA_SynastasiaMS.4ki new file mode 100644 index 0000000..3afd9ba Binary files /dev/null and b/examples/fourklang_instruments/PA_SynastasiaMS.4ki differ diff --git a/examples/fourklang_instruments/SY_RandomArp.4ki b/examples/fourklang_instruments/SY_RandomArp.4ki new file mode 100644 index 0000000..e5a8515 Binary files /dev/null and b/examples/fourklang_instruments/SY_RandomArp.4ki differ diff --git a/examples/fourklang_instruments/SY_RandomArpFlanger.4ki b/examples/fourklang_instruments/SY_RandomArpFlanger.4ki new file mode 100644 index 0000000..138fd02 Binary files /dev/null and b/examples/fourklang_instruments/SY_RandomArpFlanger.4ki differ diff --git a/examples/fourklang_instruments/airy.4ki b/examples/fourklang_instruments/airy.4ki new file mode 100644 index 0000000..e409315 Binary files /dev/null and b/examples/fourklang_instruments/airy.4ki differ diff --git a/examples/fourklang_instruments/basedrum.4ki b/examples/fourklang_instruments/basedrum.4ki new file mode 100644 index 0000000..48e2431 Binary files /dev/null and b/examples/fourklang_instruments/basedrum.4ki differ diff --git a/examples/fourklang_instruments/basedrum2.4ki b/examples/fourklang_instruments/basedrum2.4ki new file mode 100644 index 0000000..3725851 Binary files /dev/null and b/examples/fourklang_instruments/basedrum2.4ki differ diff --git a/examples/fourklang_instruments/basedrum3.4ki b/examples/fourklang_instruments/basedrum3.4ki new file mode 100644 index 0000000..53ad49e Binary files /dev/null and b/examples/fourklang_instruments/basedrum3.4ki differ diff --git a/examples/fourklang_instruments/basedrum4.4ki b/examples/fourklang_instruments/basedrum4.4ki new file mode 100644 index 0000000..b31cde7 Binary files /dev/null and b/examples/fourklang_instruments/basedrum4.4ki differ diff --git a/examples/fourklang_instruments/bass.4ki b/examples/fourklang_instruments/bass.4ki new file mode 100644 index 0000000..a4aa49e Binary files /dev/null and b/examples/fourklang_instruments/bass.4ki differ diff --git a/examples/fourklang_instruments/bass2.4ki b/examples/fourklang_instruments/bass2.4ki new file mode 100644 index 0000000..ada27f2 Binary files /dev/null and b/examples/fourklang_instruments/bass2.4ki differ diff --git a/examples/fourklang_instruments/clap.4ki b/examples/fourklang_instruments/clap.4ki new file mode 100644 index 0000000..9f6400d Binary files /dev/null and b/examples/fourklang_instruments/clap.4ki differ diff --git a/examples/fourklang_instruments/guitar.4ki b/examples/fourklang_instruments/guitar.4ki new file mode 100644 index 0000000..5738488 Binary files /dev/null and b/examples/fourklang_instruments/guitar.4ki differ diff --git a/examples/fourklang_instruments/guitar2.4ki b/examples/fourklang_instruments/guitar2.4ki new file mode 100644 index 0000000..407bec7 Binary files /dev/null and b/examples/fourklang_instruments/guitar2.4ki differ diff --git a/examples/fourklang_instruments/hihat.4ki b/examples/fourklang_instruments/hihat.4ki new file mode 100644 index 0000000..4ff4110 Binary files /dev/null and b/examples/fourklang_instruments/hihat.4ki differ diff --git a/examples/fourklang_instruments/hihat2.4ki b/examples/fourklang_instruments/hihat2.4ki new file mode 100644 index 0000000..d524356 Binary files /dev/null and b/examples/fourklang_instruments/hihat2.4ki differ diff --git a/examples/fourklang_instruments/pad.4ki b/examples/fourklang_instruments/pad.4ki new file mode 100644 index 0000000..e0c5f88 Binary files /dev/null and b/examples/fourklang_instruments/pad.4ki differ diff --git a/examples/fourklang_instruments/pad2.4ki b/examples/fourklang_instruments/pad2.4ki new file mode 100644 index 0000000..fe653a2 Binary files /dev/null and b/examples/fourklang_instruments/pad2.4ki differ diff --git a/examples/fourklang_instruments/piano.4ki b/examples/fourklang_instruments/piano.4ki new file mode 100644 index 0000000..d2ec64c Binary files /dev/null and b/examples/fourklang_instruments/piano.4ki differ diff --git a/examples/fourklang_instruments/piano2.4ki b/examples/fourklang_instruments/piano2.4ki new file mode 100644 index 0000000..9b9f28d Binary files /dev/null and b/examples/fourklang_instruments/piano2.4ki differ diff --git a/examples/fourklang_instruments/rimshot.4ki b/examples/fourklang_instruments/rimshot.4ki new file mode 100644 index 0000000..e4ca03d Binary files /dev/null and b/examples/fourklang_instruments/rimshot.4ki differ diff --git a/examples/fourklang_instruments/snare.4ki b/examples/fourklang_instruments/snare.4ki new file mode 100644 index 0000000..54de39b Binary files /dev/null and b/examples/fourklang_instruments/snare.4ki differ diff --git a/examples/fourklang_instruments/snare2.4ki b/examples/fourklang_instruments/snare2.4ki new file mode 100644 index 0000000..a4669a5 Binary files /dev/null and b/examples/fourklang_instruments/snare2.4ki differ diff --git a/examples/fourklang_instruments/snare3.4ki b/examples/fourklang_instruments/snare3.4ki new file mode 100644 index 0000000..7fc562b Binary files /dev/null and b/examples/fourklang_instruments/snare3.4ki differ diff --git a/examples/fourklang_instruments/snare4.4ki b/examples/fourklang_instruments/snare4.4ki new file mode 100644 index 0000000..da4d6a8 Binary files /dev/null and b/examples/fourklang_instruments/snare4.4ki differ diff --git a/examples/fourklang_instruments/snare5.4ki b/examples/fourklang_instruments/snare5.4ki new file mode 100644 index 0000000..e4a044c Binary files /dev/null and b/examples/fourklang_instruments/snare5.4ki differ diff --git a/examples/fourklang_instruments/snare6.4ki b/examples/fourklang_instruments/snare6.4ki new file mode 100644 index 0000000..bdc3c53 Binary files /dev/null and b/examples/fourklang_instruments/snare6.4ki differ diff --git a/examples/fourklang_instruments/string.4ki b/examples/fourklang_instruments/string.4ki new file mode 100644 index 0000000..c6ab016 Binary files /dev/null and b/examples/fourklang_instruments/string.4ki differ diff --git a/examples/fourklang_instruments/synth.4ki b/examples/fourklang_instruments/synth.4ki new file mode 100644 index 0000000..2fa6ec1 Binary files /dev/null and b/examples/fourklang_instruments/synth.4ki differ diff --git a/examples/fourklang_instruments/synthFlanger.4ki b/examples/fourklang_instruments/synthFlanger.4ki new file mode 100644 index 0000000..452b958 Binary files /dev/null and b/examples/fourklang_instruments/synthFlanger.4ki differ diff --git a/patch.go b/patch.go index 36c814d..c7317e2 100644 --- a/patch.go +++ b/patch.go @@ -192,6 +192,18 @@ func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string { } 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 "" } diff --git a/synth.go b/synth.go index 3fcb5f2..2242e2b 100644 --- a/synth.go +++ b/synth.go @@ -22,7 +22,7 @@ type Synth interface { // 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 // 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(voice int, note byte) @@ -35,7 +35,7 @@ type Synth interface { // SynthService compiles a given Patch into a Synth, throwing errors if the // Patch is malformed. 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 @@ -62,7 +62,7 @@ func Play(synthService SynthService, song Song, release bool) ([]float32, error) if err != nil { return nil, err } - synth, err := synthService.Compile(song.Patch) + synth, err := synthService.Compile(song.Patch, song.BPM) if err != nil { return nil, fmt.Errorf("sointu.Play failed: %v", err) } diff --git a/tracker/gioui/filedialog.go b/tracker/gioui/filedialog.go index b191585..85ec40c 100644 --- a/tracker/gioui/filedialog.go +++ b/tracker/gioui/filedialog.go @@ -106,9 +106,9 @@ func (f *FileDialogStyle) Layout(gtx C) D { n = n[0 : len(n)-len(extension)] switch f.dialog.UseAltExt.Value { case true: - n += ".json" + n += f.ExtAlt default: - n += ".yml" + n += f.ExtMain } f.dialog.FileName.SetText(n) } diff --git a/tracker/gioui/files.go b/tracker/gioui/files.go index 3a12a33..be5a283 100644 --- a/tracker/gioui/files.go +++ b/tracker/gioui/files.go @@ -4,6 +4,7 @@ package gioui import ( + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -66,15 +67,23 @@ func (t *Tracker) SaveInstrument() { } func (t *Tracker) loadSong(filename string) { - bytes, err := ioutil.ReadFile(filename) + b, err := ioutil.ReadFile(filename) if err != nil { return } var song sointu.Song - if errJSON := json.Unmarshal(bytes, &song); errJSON != nil { - if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil { - t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error, time.Second*3) - return + if errJSON := json.Unmarshal(b, &song); errJSON != nil { + if errYaml := yaml.Unmarshal(b, &song); errYaml != nil { + var err4kp error + 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 { @@ -148,17 +157,22 @@ func (t *Tracker) saveInstrument(filename string) bool { } func (t *Tracker) loadInstrument(filename string) bool { - bytes, err := ioutil.ReadFile(filename) + b, err := ioutil.ReadFile(filename) if err != nil { return false } var instrument sointu.Instrument - if errJSON := json.Unmarshal(bytes, &instrument); errJSON != nil { - if errYaml := yaml.Unmarshal(bytes, &instrument); errYaml != nil { - t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v", errYaml, errJSON), Error, time.Second*3) - return false + if errJSON := json.Unmarshal(b, &instrument); errJSON != nil { + if errYaml := yaml.Unmarshal(b, &instrument); errYaml != nil { + var err4ki error + 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 { t.Alert.Update("The instrument file is malformed", Error, time.Second*3) return false diff --git a/tracker/gioui/layout.go b/tracker/gioui/layout.go index a97bf57..de32015 100644 --- a/tracker/gioui/layout.go +++ b/tracker/gioui/layout.go @@ -58,6 +58,7 @@ func (t *Tracker) Layout(gtx layout.Context) { } fstyle := OpenFileDialog(t.Theme, t.OpenSongDialog) fstyle.Title = "Open Song File" + fstyle.ExtAlt = ".4kp" fstyle.Layout(gtx) for ok, file := t.OpenSongDialog.FileSelected(); ok; ok, file = t.OpenSongDialog.FileSelected() { t.loadSong(file) @@ -89,6 +90,7 @@ func (t *Tracker) Layout(gtx layout.Context) { fstyle.Layout(gtx) fstyle = OpenFileDialog(t.Theme, t.OpenInstrumentDialog) fstyle.Title = "Open Instrument File" + fstyle.ExtAlt = ".4ki" for ok, file := t.OpenInstrumentDialog.FileSelected(); ok; ok, file = t.OpenInstrumentDialog.FileSelected() { t.loadInstrument(file) } diff --git a/tracker/model.go b/tracker/model.go index 4c741f9..88fb985 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -66,7 +66,7 @@ type ( } ModelSamplesPerRowChangedMessage struct { - int + BPM, RowsPerBeat int } ModelPanicMessage struct { @@ -1088,14 +1088,43 @@ func (m *Model) Param(index int) (Parameter, error) { if index < len(unit.VarArgs) { val := unit.VarArgs[index] 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 semitones := -math.Log2(relPitch) * 12 text = fmt.Sprintf("%v / %.3f st", val, semitones) - } else { - 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 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") @@ -1240,7 +1269,7 @@ func (m *Model) notifyScoreChange() { func (m *Model) notifySamplesPerRowChange() { select { - case m.modelMessages <- ModelSamplesPerRowChangedMessage{m.song.SamplesPerRow()}: + case m.modelMessages <- ModelSamplesPerRowChangedMessage{m.song.BPM, m.song.RowsPerBeat}: default: } } diff --git a/tracker/player.go b/tracker/player.go index e1b9095..4bb4b18 100644 --- a/tracker/player.go +++ b/tracker/player.go @@ -20,6 +20,7 @@ type ( position SongRow samplesSinceEvent []int samplesPerRow int + bpm int volume Volume voiceStates [vm.MAX_VOICES]float32 @@ -242,7 +243,9 @@ loop: } } case ModelSamplesPerRowChangedMessage: - p.samplesPerRow = m.int + p.samplesPerRow = 44100 * 60 / (m.BPM * m.RowsPerBeat) + p.bpm = m.BPM + p.compileOrUpdateSynth() case ModelPlayFromPositionMessage: p.playing = true p.position = m.SongRow @@ -297,7 +300,7 @@ loop: func (p *Player) compileOrUpdateSynth() { if p.synth != nil { - err := p.synth.Update(p.patch) + err := p.synth.Update(p.patch, p.bpm) if err != nil { p.synth = nil p.trySend(PlayerCrashMessage{fmt.Errorf("synth.Update: %w", err)}) @@ -305,7 +308,7 @@ func (p *Player) compileOrUpdateSynth() { } } else { var err error - p.synth, err = p.synthService.Compile(p.patch) + p.synth, err = p.synthService.Compile(p.patch, p.bpm) if err != nil { p.synth = nil p.trySend(PlayerCrashMessage{fmt.Errorf("synthService.Compile: %w", err)}) diff --git a/unittype.go b/unittype.go index 338b444..4aa037a 100644 --- a/unittype.go +++ b/unittype.go @@ -58,7 +58,7 @@ var UnitTypes = map[string]([]UnitParameter){ {Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, {Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, {Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, - {Name: "notetracking", MinValue: 0, MaxValue: 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}}, "compressor": []UnitParameter{ {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, diff --git a/vm/bytepatch.go b/vm/bytepatch.go index 3e6dea8..cc3e9b7 100644 --- a/vm/bytepatch.go +++ b/vm/bytepatch.go @@ -33,7 +33,7 @@ type SampleOffset struct { 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())} if c.NumVoices > 32 { 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{} globalFixups := map[int]([]int){} voiceNo := 0 - delayTable, delayIndices := constructDelayTimeTable(patch) + delayTable, delayIndices := constructDelayTimeTable(patch, bpm) c.DelayTimes = make([]uint16, len(delayTable)) for i := range delayTable { c.DelayTimes[i] = uint16(delayTable[i]) @@ -171,7 +171,7 @@ func Encode(patch sointu.Patch, featureSet FeatureSet) (*BytePatch, error) { if count == 0 { 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)) } c.Commands = append(c.Commands, byte(opcode+unit.Parameters["stereo"])) diff --git a/vm/compiler/bridge/bridge.go b/vm/compiler/bridge/bridge.go index 9e939da..7814b8e 100644 --- a/vm/compiler/bridge/bridge.go +++ b/vm/compiler/bridge/bridge.go @@ -16,17 +16,17 @@ import ( type BridgeService struct { } -func (s BridgeService) Compile(patch sointu.Patch) (sointu.Synth, error) { - synth, err := Synth(patch) +func (s BridgeService) Compile(patch sointu.Patch, bpm int) (sointu.Synth, error) { + synth, err := Synth(patch, bpm) 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) if n := patch.NumDelayLines(); n > 64 { 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 { return nil, fmt.Errorf("error compiling patch: %v", err) } @@ -109,11 +109,11 @@ func (s *C.Synth) Release(voice int) { } // 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 { 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 { return fmt.Errorf("error compiling patch: %v", err) } diff --git a/vm/compiler/bridge/bridge_test.go b/vm/compiler/bridge/bridge_test.go index 0ff1839..360584f 100644 --- a/vm/compiler/bridge/bridge_test.go +++ b/vm/compiler/bridge/bridge_test.go @@ -54,7 +54,7 @@ func TestRenderSamples(t *testing.T) { 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 { 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{ sointu.Unit{Type: "pop", Parameters: map[string]int{}}, }}} - synth, err := bridge.Synth(patch) + synth, err := bridge.Synth(patch, 120) if err != nil { t.Fatalf("bridge compile error: %v", err) } @@ -146,7 +146,7 @@ func TestStackBalancing(t *testing.T) { sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{ sointu.Unit{Type: "push", Parameters: map[string]int{}}, }}} - synth, err := bridge.Synth(patch) + synth, err := bridge.Synth(patch, 120) if err != nil { 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{}}, }}} - synth, err := bridge.Synth(patch) + synth, err := bridge.Synth(patch, 120) if err != nil { 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: "pop", Parameters: map[string]int{}}, }}} - synth, err := bridge.Synth(patch) + synth, err := bridge.Synth(patch, 120) if err != nil { t.Fatalf("bridge compile error: %v", err) } diff --git a/vm/compiler/compiler.go b/vm/compiler/compiler.go index 8049c41..23bcd8f 100644 --- a/vm/compiler/compiler.go +++ b/vm/compiler/compiler.go @@ -84,7 +84,7 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) { } features := vm.NecessaryFeaturesFor(song.Patch) retmap := map[string]string{} - encodedPatch, err := vm.Encode(song.Patch, features) + encodedPatch, err := vm.Encode(song.Patch, features, song.BPM) if err != nil { return nil, fmt.Errorf(`could not encode patch: %v`, err) } diff --git a/vm/delaytable.go b/vm/delaytable.go index afed1d9..cfdd12b 100644 --- a/vm/delaytable.go +++ b/vm/delaytable.go @@ -107,7 +107,7 @@ func findSuperIntArray(arrays [][]int) ([]int, []int) { // 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 // 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)) var subarrays [][]int // flatten the delay times into one array of arrays @@ -119,11 +119,18 @@ func constructDelayTimeTable(patch sointu.Patch) ([]int, [][]int) { // should use delay times if unit.Type == "delay" { ind[i][j] = len(subarrays) - end := unit.Parameters["count"] - if unit.Parameters["stereo"] > 0 { - end *= 2 + converted := make([]int, len(unit.VarArgs)) + copy(converted, unit.VarArgs) + 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) } } } diff --git a/vm/interpreter.go b/vm/interpreter.go index adb24f4..01adeff 100644 --- a/vm/interpreter.go +++ b/vm/interpreter.go @@ -63,8 +63,8 @@ const ( envStateRelease ) -func Synth(patch sointu.Patch) (sointu.Synth, error) { - bytePatch, err := Encode(patch, AllFeatures{}) +func Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) { + bytePatch, err := Encode(patch, AllFeatures{}, bpm) if err != nil { return nil, fmt.Errorf("error compiling %v", err) } @@ -73,8 +73,8 @@ func Synth(patch sointu.Patch) (sointu.Synth, error) { return ret, nil } -func (s SynthService) Compile(patch sointu.Patch) (sointu.Synth, error) { - synth, err := Synth(patch) +func (s SynthService) Compile(patch sointu.Patch, bpm int) (sointu.Synth, error) { + synth, err := Synth(patch, bpm) return synth, err } @@ -87,8 +87,8 @@ func (s *Interpreter) Release(voiceIndex int) { s.synth.voices[voiceIndex].release = true } -func (s *Interpreter) Update(patch sointu.Patch) error { - bytePatch, err := Encode(patch, AllFeatures{}) +func (s *Interpreter) Update(patch sointu.Patch, bpm int) error { + bytePatch, err := Encode(patch, AllFeatures{}, bpm) if err != nil { 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 opNoStereo := (op & 0xFE) >> 1 if opNoStereo == 0 { - voices = voices[1:] - units = voices[0].units[:] voicesRemaining-- + if voicesRemaining > 0 { + voices = voices[1:] + units = voices[0].units[:] + } if mask := uint32(1) << uint32(voicesRemaining); s.bytePatch.PolyphonyBitmask&mask == mask { commands, values = commandInstr, valuesInstr } else { diff --git a/vm/interpreter_test.go b/vm/interpreter_test.go index 65906ad..aa112c3 100644 --- a/vm/interpreter_test.go +++ b/vm/interpreter_test.go @@ -76,7 +76,7 @@ func TestStackUnderflow(t *testing.T) { patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{ sointu.Unit{Type: "pop", Parameters: map[string]int{}}, }}} - synth, err := vm.Synth(patch) + synth, err := vm.Synth(patch, 120) if err != nil { t.Fatalf("bridge compile error: %v", err) } @@ -92,7 +92,7 @@ func TestStackBalancing(t *testing.T) { sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{ sointu.Unit{Type: "push", Parameters: map[string]int{}}, }}} - synth, err := vm.Synth(patch) + synth, err := vm.Synth(patch, 120) if err != nil { t.Fatalf("bridge compile error: %v", err) }