Implement sample-based oscillators, with sample import from gm.dls.

This commit is contained in:
Veikko Sariola 2020-05-19 18:29:47 +03:00
parent 77b989d88d
commit adc4a6e45f
13 changed files with 280 additions and 6 deletions

5
.gitignore vendored
View File

@ -15,4 +15,7 @@
build/
# Project specific
old/
old/
# VS Code
.vscode/

View File

@ -67,6 +67,12 @@ New features since fork
FLD 0 with modulation basically achieved what RECEIVE does, except that
RECEIVE can also handle stereo signals.
- **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 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.asm), and this Python
[script](scripts/parse_gmdls.py) parses the gm.dls file and dumps the
sample offsets from it.
Future goals
------------
@ -92,10 +98,7 @@ Future goals
Nice-to-have ideas
------------------
- **Sample import from gm.dls**. This is Windows only, but implementing it
should be easy and the potential payoffs pretty high for Windows users, so
it is a nice prospect.
- **Tracker**. If the list of primary goals is ever exhausted, a browser-based
tracker would be nice to take advantage of all the features.

54
scripts/parse_gmdls.py Normal file
View File

@ -0,0 +1,54 @@
# Usage: python parse_gmdls.py <path-to-gmdls>
# Parses the GMDLs sample and loop locations and dumps them ready to be included in the SU_BEGIN_SAMPLE_OFFSETS block
import sys
def read_chunk(file,indent=0):
name = f.read(4)
length_bytes = f.read(4)
length = int.from_bytes(length_bytes,byteorder='little')
data = None
start = f.tell()
if name == b"RIFF" or name == b"LIST":
name = f.read(4)
data = list()
while f.tell() < start+length:
data.append(read_chunk(file,indent + 4))
if name == b"wave":
datablock = next((x[1] for x in data if x[0] == b"data"))
wsmp = next((x[1] for x in data if x[0] == b"wsmp"), None)
if "loopstart" not in wsmp:
loopstart,looplength = datablock["length"]/2-1,1 # For samples without loop, keep on repeating the last sample
else:
loopstart,looplength = wsmp["loopstart"],wsmp["looplength"]
INFO = next((x[1] for x in data if x[0] == b"INFO"), None)
INAM = next((x[1] for x in INFO if x[0] == b"INAM"), None)
name = ""
if INAM is not None:
name = INAM["name"]
t = 60 - wsmp["unitynote"] # in MIDI, the middle C = 263 Hz is 60. In Sointu/4klang, it's 72.
print("SAMPLE_OFFSET START(%d),LOOPSTART(%d),LOOPLENGTH(%d) ; name %s, unitynote %d (transpose to %d), data length %d" % (datablock["start"],loopstart,looplength,name,wsmp["unitynote"],t,datablock["length"]))
# Something is oddly off: LOOPSTART + LOOPLENGTH != DATA LENGTH /2, but rather LOOPSTART + LOOPLENGTH != DATA LENGTH /2 - 1
# Logically, LOOPSTART = 0 would mean the sample loops from the beginning. But then why would they store one extra sample?
# Or, maybe start+length is the index of the last sample included in the loop. For now, I'm assuming that start+length-1
# is the last sample and there's one unused sample.
elif name == b"wsmp":
f.read(4)
data = dict()
data["unitynote"] = int.from_bytes(f.read(2),byteorder='little')
f.read(10)
numloops = int.from_bytes(f.read(4),byteorder='little')
if numloops > 0:
f.read(8)
data["loopstart"] = int.from_bytes(f.read(4),byteorder='little')
data["looplength"] = int.from_bytes(f.read(4),byteorder='little')
elif name == b"data":
data = {"start": f.tell()/2, "length": length/2}
elif name == b"INAM":
data = {"name": f.read(length-1).decode("ascii")}
f.read(length - f.tell() + start + (length & 1))
return (name,data)
if __name__ == "__main__":
with open(sys.argv[1], "rb") as f:
read_chunk(f)

42
src/gmdls.asm Normal file
View File

@ -0,0 +1,42 @@
%ifdef INCLUDE_GMDLS
%define SAMPLE_TABLE_SIZE 3440660 ; size of gmdls
extern _OpenFile@12 ; requires windows
extern _ReadFile@20 ; requires windows
SECT_TEXT(sugmdls)
su_gmdls_load:
mov edi, MANGLE_DATA(su_sample_table)
mov esi, su_gmdls_path1
su_gmdls_pathloop:
push 0 ; OF_READ
push edi ; &ofstruct, blatantly reuse the sample table
push esi ; path
call _OpenFile@12 ; eax = OpenFile(path,&ofstruct,OF_READ)
add esi, su_gmdls_path2 - su_gmdls_path1 ; if we ever get to third, then crash
cmp eax, -1 ; eax == INVALID?
je su_gmdls_pathloop
push 0 ; NULL
push edi ; &bytes_read, reusing sample table again; it does not matter that the first four bytes are trashed
push SAMPLE_TABLE_SIZE ; number of bytes to read
push edi ; here we actually pass the sample table to readfile
push eax ; handle to file
call _ReadFile@20 ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL)
ret
SECT_DATA(sugmpath)
su_gmdls_path1:
db 'drivers/gm.dls',0
su_gmdls_path2:
db 'drivers/etc/gm.dls',0
SECT_DATA(suconst)
c_samplefreq_scaling dd 84.28074964676522 ; o = 0.000092696138, n = 72, f = 44100*o*2**(n/12), scaling = 22050/f <- so note 72 plays at the "normal rate"
SECT_BSS(susamtbl)
EXPORT MANGLE_DATA(su_sample_table) resb SAMPLE_TABLE_SIZE ; size of gmdls.
%endif

View File

@ -136,6 +136,13 @@ su_op_oscillat_normalized:
fadd dword [WRK+su_osc_wrk.phase]
fst dword [WRK+su_osc_wrk.phase]
fadd dword [edx+su_osc_ports.phaseofs]
%ifdef INCLUDE_SAMPLES
test al, byte SAMPLE
jz short su_op_oscillat_not_sample
call su_oscillat_sample
jmp su_op_oscillat_shaping ; skip the rest to avoid color phase normalization and colorloading
su_op_oscillat_not_sample:
%endif
fld1
fadd st1, st0
fxch
@ -168,6 +175,7 @@ su_op_oscillat_not_pulse:
jmp su_op_oscillat_gain ; skip waveshaping as the shape parameter is reused for gateshigh
su_op_oscillat_not_gate:
%endif
su_op_oscillat_shaping:
; finally, shape the oscillator and apply gain
fld dword [edx+su_osc_ports.shape]
call su_waveshaper
@ -285,6 +293,42 @@ SECT_DATA(suconst)
%endif
; SAMPLES
%ifdef INCLUDE_SAMPLES
SECT_TEXT(suoscsam)
su_oscillat_sample: ; p
pushad ; edx must be saved, eax & ecx if this is stereo osc
push edx
mov al, byte [VAL-4] ; reuse "color" as the sample number
lea edi, [MANGLE_DATA(su_sample_offsets) + eax*8] ; edi points now to the sample table entry
fmul dword [c_samplefreq_scaling] ; p*r
fistp dword [esp]
pop edx ; edx is now the sample number
movzx ebx, word [edi + su_sample_offset.loopstart] ; ecx = loopstart
sub edx, ebx ; if sample number < loop start
jl su_oscillat_sample_not_looping ; then we're not looping yet
mov eax, edx ; eax = sample number
movzx ecx, word [edi + su_sample_offset.looplength] ; edi is now the loop length
xor edx, edx ; div wants edx to be empty
div ecx ; edx is now the remainder
su_oscillat_sample_not_looping:
add edx, ebx ; sampleno += loopstart
add edx, dword [edi + su_sample_offset.start]
fild word [MANGLE_DATA(su_sample_table) + edx*2]
fdiv dword [c_32767]
popad
ret
SECT_DATA(suconst)
%ifndef C_32767
c_32767 dd 32767.0
%define C_32767
%endif
%endif
;-------------------------------------------------------------------------------
; LOADVAL opcode
;-------------------------------------------------------------------------------

View File

@ -64,6 +64,7 @@ endstruc
%endif
%endmacro
%define SAMPLE 0x80 ; in this case, all the rest of the bits is the sample index
%define SINE 0x40
%define TRISAW 0x20
%define PULSE 0x10
@ -95,6 +96,9 @@ endstruc
%if (%8) & GATE == GATE
%define INCLUDE_GATE
%endif
%if (%8) & SAMPLE == SAMPLE
%define INCLUDE_SAMPLES
%endif
%endmacro
struc su_osc_ports
@ -119,9 +123,39 @@ endstruc
%define GATESLOW(val) val
%define GATESHIGH(val) val
%define COLOR(val) val
%define SAMPLENO(val) val
%define SHAPE(val) val
%define FLAGS(val) val
;-------------------------------------------------------------------------------
; Sample related defines
;-------------------------------------------------------------------------------
%macro SU_BEGIN_SAMPLE_OFFSETS 0
SECT_DATA(susamoff)
EXPORT MANGLE_DATA(su_sample_offsets)
%endmacro
%macro SAMPLE_OFFSET 3
dd %1
dw %2
dw %3
%endmacro
%define START(val) val
%define LOOPSTART(val) val
%define LOOPLENGTH(val) val
%define SU_END_SAMPLE_OFFSETS
struc su_sample_offset ; length conveniently 8, so easy to index
.start resd 1
.loopstart resw 1
.looplength resw 1
.size
endstruc
;-------------------------------------------------------------------------------
; NOISE structs
;-------------------------------------------------------------------------------

View File

@ -19,7 +19,10 @@ su_voicetrack_bitmask dd VOICETRACK_BITMASK; does the following voice bel
SECT_DATA(suconst)
%ifdef SU_USE_16BIT_OUTPUT
c_32767 dd 32767.0
%ifndef C_32767
c_32767 dd 32767.0
%define C_32767
%endif
%endif
;-------------------------------------------------------------------------------
@ -86,6 +89,9 @@ SECT_TEXT(surender)
EXPORT MANGLE_FUNC(su_render,4) ; Stack: ptr
pushad ; Stack: pushad ptr
%ifdef INCLUDE_GMDLS
call su_gmdls_load
%endif
xor eax, eax ; ecx is the current row
su_render_rowloop: ; loop through every row in the song
push eax ; Stack: row pushad ptr

View File

@ -191,3 +191,4 @@ EXPORT MANGLE_FUNC(su_power,0)
%include "opcodes/effects.asm"
%include "player.asm"
%include "introspection.asm"
%include "gmdls.asm"

View File

@ -78,6 +78,8 @@ regression_test(test_oscillat_trisaw ENVELOPE)
regression_test(test_oscillat_pulse ENVELOPE VCO_PULSE)
regression_test(test_oscillat_gate ENVELOPE)
regression_test(test_oscillat_stereo ENVELOPE)
regression_test(test_oscillat_sample ENVELOPE)
regression_test(test_oscillat_sample_stereo ENVELOPE)
regression_test(test_oscillat_lfo "ENVELOPE;VCO_SINE;VCO_PULSE;FOP_MULP2")
regression_test(test_oscillat_transposemod "VCO_SINE;ENVELOPE;FOP_MULP;FOP_PUSH;SEND")
regression_test(test_oscillat_detunemod "VCO_SINE;ENVELOPE;FOP_MULP;FOP_PUSH;SEND")

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,44 @@
%define BPM 100
%define USE_SECTIONS
%define INCLUDE_GMDLS
%include "../src/sointu.inc"
SU_BEGIN_PATTERNS
PATTERN 0,0,0,0,0,0,0,0,
PATTERN 72, HLD, HLD, HLD, HLD, HLD, HLD, 0,
PATTERN 64, HLD, HLD, HLD, HLD, HLD, HLD, 0,
PATTERN 60, HLD, HLD, HLD, HLD, HLD, HLD, 0,
PATTERN 40, HLD, HLD, HLD, HLD, HLD, HLD, 0,
SU_END_PATTERNS
SU_BEGIN_TRACKS
TRACK VOICES(1),1,0,2,0,3,0,4,0
TRACK VOICES(1),0,1,0,2,0,3,0,4 ; an ordinary sine oscillator, to compare we calculate the pitch right
SU_END_TRACKS
SU_BEGIN_PATCH
SU_BEGIN_INSTRUMENT VOICES(1) ; Instrument0
SU_ENVELOPE MONO,ATTAC(32),DECAY(32),SUSTAIN(64),RELEASE(64),GAIN(128)
SU_ENVELOPE MONO,ATTAC(32),DECAY(32),SUSTAIN(64),RELEASE(64),GAIN(128)
SU_OSCILLAT MONO,TRANSPOSE(64+4),DETUNE(64),PHASE(64),SAMPLENO(0),SHAPE(64),GAIN(128), FLAGS(SAMPLE)
SU_OSCILLAT MONO,TRANSPOSE(64+2),DETUNE(64),PHASE(64),SAMPLENO(1),SHAPE(64),GAIN(128), FLAGS(SAMPLE)
SU_MULP STEREO
SU_OUT STEREO,GAIN(128)
SU_END_INSTRUMENT
SU_BEGIN_INSTRUMENT VOICES(1) ; Instrument1 to compare that the pitch is ok
SU_ENVELOPE MONO,ATTAC(32),DECAY(32),SUSTAIN(64),RELEASE(64),GAIN(128)
SU_ENVELOPE MONO,ATTAC(32),DECAY(32),SUSTAIN(64),RELEASE(64),GAIN(128)
SU_OSCILLAT MONO,TRANSPOSE(64),DETUNE(64),PHASE(0),COLOR(128),SHAPE(64),GAIN(128), FLAGS(SINE)
SU_OSCILLAT MONO,TRANSPOSE(64),DETUNE(64),PHASE(0),COLOR(128),SHAPE(64),GAIN(128), FLAGS(SINE)
SU_MULP STEREO
SU_OUT STEREO,GAIN(128)
SU_END_INSTRUMENT
SU_END_PATCH
SU_BEGIN_SAMPLE_OFFSETS
SAMPLE_OFFSET START(1678611),LOOPSTART(1341),LOOPLENGTH(106) ; name VIOLN68, unitynote 56 (transpose to 4), data length 1448
SAMPLE_OFFSET START(1680142),LOOPSTART(1483),LOOPLENGTH(95) ; name VIOLN70, unitynote 58 (transpose to 2), data length 1579
SU_END_SAMPLE_OFFSETS
%include "../src/sointu.asm"

View File

@ -0,0 +1,41 @@
%define BPM 100
%define USE_SECTIONS
%define INCLUDE_GMDLS
%include "../src/sointu.inc"
SU_BEGIN_PATTERNS
PATTERN 0,0,0,0,0,0,0,0,
PATTERN 72, HLD, HLD, HLD, HLD, HLD, HLD, 0,
PATTERN 64, HLD, HLD, HLD, HLD, HLD, HLD, 0,
PATTERN 60, HLD, HLD, HLD, HLD, HLD, HLD, 0,
PATTERN 40, HLD, HLD, HLD, HLD, HLD, HLD, 0,
SU_END_PATTERNS
SU_BEGIN_TRACKS
TRACK VOICES(1),1,0,2,0,3,0,4,0
TRACK VOICES(1),0,1,0,2,0,3,0,4 ; an ordinary sine oscillator, to compare we calculate the pitch right
SU_END_TRACKS
SU_BEGIN_PATCH
SU_BEGIN_INSTRUMENT VOICES(1) ; Instrument0
SU_ENVELOPE MONO,ATTAC(32),DECAY(32),SUSTAIN(64),RELEASE(64),GAIN(128)
SU_ENVELOPE MONO,ATTAC(32),DECAY(32),SUSTAIN(64),RELEASE(64),GAIN(128)
SU_OSCILLAT STEREO,TRANSPOSE(64+4),DETUNE(32),PHASE(64),SAMPLENO(0),SHAPE(64),GAIN(128), FLAGS(SAMPLE)
SU_MULP STEREO
SU_OUT STEREO,GAIN(128)
SU_END_INSTRUMENT
SU_BEGIN_INSTRUMENT VOICES(1) ; Instrument1 to compare that the pitch is ok
SU_ENVELOPE MONO,ATTAC(32),DECAY(32),SUSTAIN(64),RELEASE(64),GAIN(128)
SU_ENVELOPE MONO,ATTAC(32),DECAY(32),SUSTAIN(64),RELEASE(64),GAIN(128)
SU_OSCILLAT STEREO,TRANSPOSE(64),DETUNE(32),PHASE(0),COLOR(128),SHAPE(64),GAIN(128), FLAGS(SINE)
SU_MULP STEREO
SU_OUT STEREO,GAIN(128)
SU_END_INSTRUMENT
SU_END_PATCH
SU_BEGIN_SAMPLE_OFFSETS
SAMPLE_OFFSET START(1678611),LOOPSTART(1341),LOOPLENGTH(106) ; name VIOLN68, unitynote 56 (transpose to 4), data length 1448
SU_END_SAMPLE_OFFSETS
%include "../src/sointu.asm"