mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
Implement sample-based oscillators, with sample import from gm.dls.
This commit is contained in:
parent
77b989d88d
commit
adc4a6e45f
5
.gitignore
vendored
5
.gitignore
vendored
@ -15,4 +15,7 @@
|
||||
build/
|
||||
|
||||
# Project specific
|
||||
old/
|
||||
old/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
11
README.md
11
README.md
@ -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
54
scripts/parse_gmdls.py
Normal 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
42
src/gmdls.asm
Normal 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
|
@ -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
|
||||
;-------------------------------------------------------------------------------
|
||||
|
@ -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
|
||||
;-------------------------------------------------------------------------------
|
||||
|
@ -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
|
||||
|
@ -191,3 +191,4 @@ EXPORT MANGLE_FUNC(su_power,0)
|
||||
%include "opcodes/effects.asm"
|
||||
%include "player.asm"
|
||||
%include "introspection.asm"
|
||||
%include "gmdls.asm"
|
||||
|
@ -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")
|
||||
|
BIN
tests/expected_output/test_oscillat_sample.raw
Normal file
BIN
tests/expected_output/test_oscillat_sample.raw
Normal file
Binary file not shown.
BIN
tests/expected_output/test_oscillat_sample_stereo.raw
Normal file
BIN
tests/expected_output/test_oscillat_sample_stereo.raw
Normal file
Binary file not shown.
44
tests/test_oscillat_sample.asm
Normal file
44
tests/test_oscillat_sample.asm
Normal 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"
|
41
tests/test_oscillat_sample_stereo.asm
Normal file
41
tests/test_oscillat_sample_stereo.asm
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user