diff --git a/.gitignore b/.gitignore index e5fc080..cbbbb0a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,7 @@ build/ # Project specific -old/ \ No newline at end of file +old/ + +# VS Code +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index cf1ca49..7b7e8be 100644 --- a/README.md +++ b/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. diff --git a/scripts/parse_gmdls.py b/scripts/parse_gmdls.py new file mode 100644 index 0000000..5b56a47 --- /dev/null +++ b/scripts/parse_gmdls.py @@ -0,0 +1,54 @@ +# Usage: python parse_gmdls.py +# 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) \ No newline at end of file diff --git a/src/gmdls.asm b/src/gmdls.asm new file mode 100644 index 0000000..7670f78 --- /dev/null +++ b/src/gmdls.asm @@ -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 \ No newline at end of file diff --git a/src/opcodes/sources.asm b/src/opcodes/sources.asm index d7d2065..5f66714 100644 --- a/src/opcodes/sources.asm +++ b/src/opcodes/sources.asm @@ -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 ;------------------------------------------------------------------------------- diff --git a/src/opcodes/sources.inc b/src/opcodes/sources.inc index f7fce85..f75b017 100644 --- a/src/opcodes/sources.inc +++ b/src/opcodes/sources.inc @@ -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 ;------------------------------------------------------------------------------- diff --git a/src/player.asm b/src/player.asm index 0b5d1f8..2cf3f2c 100644 --- a/src/player.asm +++ b/src/player.asm @@ -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 diff --git a/src/sointu.asm b/src/sointu.asm index d1c27a2..b57ba57 100644 --- a/src/sointu.asm +++ b/src/sointu.asm @@ -191,3 +191,4 @@ EXPORT MANGLE_FUNC(su_power,0) %include "opcodes/effects.asm" %include "player.asm" %include "introspection.asm" +%include "gmdls.asm" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 33119a3..fe4c268 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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") diff --git a/tests/expected_output/test_oscillat_sample.raw b/tests/expected_output/test_oscillat_sample.raw new file mode 100644 index 0000000..403b433 Binary files /dev/null and b/tests/expected_output/test_oscillat_sample.raw differ diff --git a/tests/expected_output/test_oscillat_sample_stereo.raw b/tests/expected_output/test_oscillat_sample_stereo.raw new file mode 100644 index 0000000..6dad331 Binary files /dev/null and b/tests/expected_output/test_oscillat_sample_stereo.raw differ diff --git a/tests/test_oscillat_sample.asm b/tests/test_oscillat_sample.asm new file mode 100644 index 0000000..1061979 --- /dev/null +++ b/tests/test_oscillat_sample.asm @@ -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" diff --git a/tests/test_oscillat_sample_stereo.asm b/tests/test_oscillat_sample_stereo.asm new file mode 100644 index 0000000..aa31b73 --- /dev/null +++ b/tests/test_oscillat_sample_stereo.asm @@ -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"