diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b2147..c44ba78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 5f25b17..2da549c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/vm/interpreter.go b/vm/interpreter.go index 01adeff..d46b560 100644 --- a/vm/interpreter.go +++ b/vm/interpreter.go @@ -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] diff --git a/vm/interpreter_test.go b/vm/interpreter_test.go index aa112c3..b714c24 100644 --- a/vm/interpreter_test.go +++ b/vm/interpreter_test.go @@ -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)