feat(vm): add support for gm.dls samples in the go virtual machine (closes #75)

This commit is contained in:
5684185+vsariola@users.noreply.github.com 2023-08-28 22:44:37 +03:00
parent 6ec06c760a
commit 7dd2c246a0
4 changed files with 78 additions and 36 deletions

View File

@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Added
- Support for gm.dls samples in the go-written virtual machine
- Ability to lock delay relative to beat duration
- Ability to import 4klang patches (.4kp) and instruments (.4ki)
- Ability to run sointu as a vsti plugin, inside vsti host

View File

@ -358,12 +358,13 @@ New features since fork
ports / 4 stereo ports, so even this method of routing is unlikely to run
out of ports in small intros.
- **Pattern length does not have to be a power of 2**.
- **Sample-based oscillators, with samples imported from gm.dls**. Reading
gm.dls is obviously Windows only, but with some effort the sample mechanism
can be used also without it, in case you are working on a 64k and have some
kilobytes to spare. See [this example](tests/test_oscillat_sample.yml), and
this go generate [program](cmd/sointu-generate/main.go) parses the gm.dls
file and dumps the sample offsets from it.
- **Sample-based oscillators, with samples imported from gm.dls**. The
gm.dls is available from system folder only on Windows, but the
non-native tracker looks for it also in the current folder, so
should you somehow magically get hold of gm.dls on Linux or Mac, you
can drop it in the same folder with the tracker. See [this example](tests/test_oscillat_sample.yml),
and this go generate [program](cmd/sointu-generate/main.go) parses
the gm.dls file and dumps the sample offsets from it.
- **Unison oscillators**. Multiple copies of the oscillator running slightly
detuned and added up to together. Great for trance leads (supersaw). Unison
of up to 4, or 8 if you make stereo unison oscillator and add up both left

View File

@ -1,9 +1,12 @@
package vm
import (
"encoding/binary"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"github.com/vsariola/sointu"
)
@ -63,6 +66,27 @@ const (
envStateRelease
)
var su_sample_table [3440660]byte
func init() {
var f *os.File
var err error
if f, err = os.Open("gm.dls"); err == nil { // try to open from current directory first
goto success
}
if f, err = os.Open(filepath.Join(os.Getenv("SystemRoot"), "system32", "drivers", "gm.dls")); err == nil {
goto success
}
if f, err = os.Open(filepath.Join(os.Getenv("SystemRoot"), "SysWOW64", "drivers", "gm.dls")); err == nil {
goto success
}
return
success:
defer f.Close()
// read file, ignoring errors
f.Read(su_sample_table[:])
}
func Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
bytePatch, err := Encode(patch, AllFeatures{}, bpm)
if err != nil {
@ -429,37 +453,53 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
} else {
omega *= 0.000038 // pretty random scaling constant to get LFOs into reasonable range. Historical reasons, goes all the way back to 4klang
}
*statevar += float32(omega)
*statevar -= float32(int(*statevar+1) - 1)
phase := *statevar
phase += params[2]
phase -= float32(int(phase))
color := params[3]
var amplitude float32
switch {
case flags&0x40 == 0x40: // Sine
if phase < color {
amplitude = float32(math.Sin(2 * math.Pi * float64(phase/color)))
*statevar += float32(omega)
if flags&0x80 == 0x80 { // if this is a sample oscillator
phase := *statevar
phase += params[2]
sampleno := valuesAtTransform[3] // reuse color as the sample number
sampleoffset := s.bytePatch.SampleOffsets[sampleno]
sampleindex := int(phase*84.28074964676522 + 0.5)
loopstart := int(sampleoffset.LoopStart)
if sampleindex >= loopstart {
sampleindex -= loopstart
sampleindex %= int(sampleoffset.LoopLength)
sampleindex += loopstart
}
case flags&0x20 == 0x20: // Trisaw
if phase >= color {
phase = 1 - phase
color = 1 - color
sampleindex += int(sampleoffset.Start)
amplitude = float32(int16(binary.LittleEndian.Uint16(su_sample_table[sampleindex*2:]))) / 32767.0
} else {
*statevar -= float32(int(*statevar+1) - 1)
phase := *statevar
phase += params[2]
phase -= float32(int(phase))
color := params[3]
switch {
case flags&0x40 == 0x40: // Sine
if phase < color {
amplitude = float32(math.Sin(2 * math.Pi * float64(phase/color)))
}
case flags&0x20 == 0x20: // Trisaw
if phase >= color {
phase = 1 - phase
color = 1 - color
}
amplitude = phase/color*2 - 1
case flags&0x10 == 0x10: // Pulse
if phase >= color {
amplitude = -1
} else {
amplitude = 1
}
case flags&0x4 == 0x4: // Gate
maskLow, maskHigh := valuesAtTransform[3], valuesAtTransform[4]
gateBits := (int(maskHigh) << 8) + int(maskLow)
amplitude = float32((gateBits >> (int(phase*16+.5) & 15)) & 1)
g := unit.state[4+i] // warning: still fucks up with unison = 3
amplitude += 0.99609375 * (g - amplitude)
unit.state[4+i] = amplitude
}
amplitude = phase/color*2 - 1
case flags&0x10 == 0x10: // Pulse
if phase >= color {
amplitude = -1
} else {
amplitude = 1
}
case flags&0x4 == 0x4: // Gate
maskLow, maskHigh := valuesAtTransform[3], valuesAtTransform[4]
gateBits := (int(maskHigh) << 8) + int(maskLow)
amplitude = float32((gateBits >> (int(phase*16+.5) & 15)) & 1)
g := unit.state[4+i] // warning: still fucks up with unison = 3
amplitude += 0.99609375 * (g - amplitude)
unit.state[4+i] = amplitude
}
if flags&0x4 == 0 {
output += waveshape(amplitude, params[4]) * params[5]

View File

@ -28,8 +28,8 @@ func TestAllRegressionTests(t *testing.T) {
basename := filepath.Base(filename)
testname := strings.TrimSuffix(basename, path.Ext(basename))
t.Run(testname, func(t *testing.T) {
if strings.Contains(testname, "sample") {
t.Skip("Samples (gm.dls) not available in the interpreter VM at the moment")
if runtime.GOOS != "windows" && strings.Contains(testname, "sample") {
t.Skip("Samples (gm.dls) available only on Windows")
return
}
asmcode, err := ioutil.ReadFile(filename)