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 ## Unreleased
### Added ### Added
- Support for gm.dls samples in the go-written virtual machine
- Ability to lock delay relative to beat duration - Ability to lock delay relative to beat duration
- Ability to import 4klang patches (.4kp) and instruments (.4ki) - Ability to import 4klang patches (.4kp) and instruments (.4ki)
- Ability to run sointu as a vsti plugin, inside vsti host - 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 ports / 4 stereo ports, so even this method of routing is unlikely to run
out of ports in small intros. out of ports in small intros.
- **Pattern length does not have to be a power of 2**. - **Pattern length does not have to be a power of 2**.
- **Sample-based oscillators, with samples imported from gm.dls**. Reading - **Sample-based oscillators, with samples imported from gm.dls**. The
gm.dls is obviously Windows only, but with some effort the sample mechanism gm.dls is available from system folder only on Windows, but the
can be used also without it, in case you are working on a 64k and have some non-native tracker looks for it also in the current folder, so
kilobytes to spare. See [this example](tests/test_oscillat_sample.yml), and should you somehow magically get hold of gm.dls on Linux or Mac, you
this go generate [program](cmd/sointu-generate/main.go) parses the gm.dls can drop it in the same folder with the tracker. See [this example](tests/test_oscillat_sample.yml),
file and dumps the sample offsets from it. 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 - **Unison oscillators**. Multiple copies of the oscillator running slightly
detuned and added up to together. Great for trance leads (supersaw). Unison 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 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 package vm
import ( import (
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"math" "math"
"os"
"path/filepath"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
) )
@ -63,6 +66,27 @@ const (
envStateRelease 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) { func Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
bytePatch, err := Encode(patch, AllFeatures{}, bpm) bytePatch, err := Encode(patch, AllFeatures{}, bpm)
if err != nil { if err != nil {
@ -429,37 +453,53 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
} else { } else {
omega *= 0.000038 // pretty random scaling constant to get LFOs into reasonable range. Historical reasons, goes all the way back to 4klang 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 var amplitude float32
switch { *statevar += float32(omega)
case flags&0x40 == 0x40: // Sine if flags&0x80 == 0x80 { // if this is a sample oscillator
if phase < color { phase := *statevar
amplitude = float32(math.Sin(2 * math.Pi * float64(phase/color))) 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 sampleindex += int(sampleoffset.Start)
if phase >= color { amplitude = float32(int16(binary.LittleEndian.Uint16(su_sample_table[sampleindex*2:]))) / 32767.0
phase = 1 - phase } else {
color = 1 - color *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 { if flags&0x4 == 0 {
output += waveshape(amplitude, params[4]) * params[5] output += waveshape(amplitude, params[4]) * params[5]

View File

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