bm-1
Folders and files
Name | Name | Last commit date | ||
---|---|---|---|---|
parent directory.. | ||||
******************************************************************************** BM-1, aka BeepModular-1 by utz 02'2017 * irrlichtproject.de | github.com/utz82 ******************************************************************************** About ===== BM-1 is an experimental sound routine for the ZX Spectrum beeper. It features a highly versatile synthesis core that can be modified during runtime, which makes it possible to generate a near-endless range of different sounds. Features include: - 2 tone channels, 12-bit or 15-bit frequency dividers - patches: on-the-fly modifications of the synthesis algorithm - volume control (8 levels per channel, availability depends on patch) - tables: change pitch and fx parameters per tick - functions: arbitrary code execution/modification once per tick - customizable click drums - per-step tempo control - compact player size (375 bytes, can be reduced by disabling features) - optimized data format With patches, you can produce - variable duty cycles - phatness/harmonics control - fake chords - bytebeat-like glitches - SIDsound (duty sweep) - PFM (classic pin pulse sound) - noise Usage ===== Unfortunately no editor exists for this engine, so any music must be composed directly in assembly. Furthermore, at least a basic understand of Z80 machine code is required, as BM-1 makes use of actual code snippets (called "patches") embedded within the song data. Note that it is perfectly possible to crash the player through use of invalid code. The code is intended to be assembled with Pasmo. Music Data Format ================= Music data for BM-1 consists of a sequence, one or more patterns, one or more patches, one or more fx tables (omitted when USETABLES is set to 0 in main.asm), and optionally blocks of code to be executed by fx tables. SEQUENCE: A list of pattern pointers in the order in which they are to be played. The list is terminated with a 0-word. Unless USELOOP is set to 0 in main asm, a label called "mloop" must be used within the sequence to specify the position the player will loop to after completing the sequence. A simple valid sequence thus may look like this: mloop dw ptn0 dw 0 PATTERNS: Patterns contain the actual music data. Each pattern row contains 2-11 word- length entries. The layout is as follows: word 0: drum_parameter << 8 | control_byte0 ctrl0 bit 7 set: trigger noise click drum -> drum_param bit 0..6 sets volume drum_param bit 7 toggles high/low pitch (set = hi) ctrl0 bit 6 set: end of pattern (all patterns should terminate with db #40). ctrl0 bit 2 set: trigger kick click drum (ignored when bit 7 is set) -> drum_param set starting pitch ctrl0 bit 0 set: skip loading all channel parameters (omit words 1..8) drum_param should be 0 if no click drums are triggered (affects lo-byte of tempo counter) word 1: patch_param1_7 << 8 | control_byte1 ctrl1 bit 7 set: skip patch_param1_8..11 ctrl1 bit 6 set: no patch update (omit word 2) ctrl1 bit 2 set: skip patch_param1_1..6 ctrl1 bit 0 set: skip all updates for ch1 (omit words 2..4) word 2: patch_pointer_ch1 word 3: frequency_divider_ch1 if bit 15 is reset, omit word 4 word 4: generic_parameter_ch1 word 5: patch_param2_7 << 8 | control_byte2 ctrl1 bit 7 set: skip patch_param2_8..11 ctrl1 bit 6 set: no patch update (omit word 6) ctrl1 bit 2 set: skip patch_param2_1..6 ctrl1 bit 0 set: skip all updates for ch2 (omit word 6..8) word 6: patch_pointer_ch2 word 7: frequency_divider_ch2 if bit 15 is reset, omit word 8 word 8: generic_parameter_ch2 word 9: row_tempo << 8 | control_byte3 ctrl3 bit 6 set: skip table_pointer update (omit word 10) word 10: table_pointer All values except the generic parameters must be initialized at the beginning of the sequence. Each pattern must end with and end marker (= db #40, see ctrl0), unless followed by another pattern (which will be loaded once the current one is completed). TABLES: Tables contain additional data, which is parsed once per row tick (eg. at a rate of about 61 Hz). Tables can modify the frequency dividers, and the generic parameters. They can also modify everything else via function execution. The layout is as follows: word 0: control_byte0 ctrl0 bit 7 set: perform table jump (pointer to table location follows) ctrl0 bit 6 set: stop table execution (hi-byte can be omitted) ctrl0 bit 2 set: execute function (pointer to function follows) ctrl0 bit 0 set: no update on this tick (hi-byte is omitted) any of the above, omit word 1..5 word 1: control_byte1 ctrl1 bit 7 set: skip freq_div2 update (omit word 4) ctrl1 bit 6 set: skip freq_div1 update (omit word 2) ctrl1 bit 2 set: skip generic_param2 update (omit word 5) ctrl1 bit 0 set: skip generic_param1 update (omit word 3) word 2: frequency_divider_ch1 word 3: generic_parameter_ch1 word 4: frequency_divider_ch2 word 5: generic_parameter_ch2 Each tables must end with either a table jump, or a table stop (= db #40), unless followed by another table (which will be loaded once the current one is completed). PATCHES: Patches are code templates, which are copied into the synthesis core at runtime. They consist 10 single-byte, 4-cycle instructions, or an equivalent amout of 2-byte, 8-cycle instructioins. The first 6 instructions or the last 4 instructions may be omitted, if the control bytes of pattern row that sets the patch (ctrl1 resp. ctrl2) are set accordingly. An additional instruction is set directly by the control byte. Instructions are executed as follows: - Instruction 1..6 are executed before the first OUT command (patchX_1..6), ie. before the channel starts playing. At this point the channel frequency counter has been updated, and the high-byte of the counter has been loaded into the accumulator A. - The additional instruction set by the ctrl1/2 is executed between the first and the second OUT command (patchX_7), ie. after the channel has played for 16 cycles - Instruction 8-10 are executed between the second and the third OUT command (patchX_8..11), ie. after the channel has played for 16+32=48 cycles. After the third OUT command, the channel will continue to play for another 64 cycles, resulting in a total playtime of 128 cycles per sound loop iteration. As mentioned before, only instructions that align to 4 cycles per instruction byte can be used. It is entirely possible to break the player with patch code, hence caution is advised. Some general rules of thumb: - Stick to instructions that modify either the accumulator A, or the generic parameters (IXH/IXL for ch1, IYH/IYL for ch2). - Be extra careful when modifying registers D,E,H,L and their shadow counterparts. - It is almost certainly a bad idea to use indirect jumps (jp (hl/ix/iy)). - It is almost certainly a bad idea to modify registers B, C, B', C'. Some standard patches are provided as macros in patches.h, check them for further reference. FUNCTIONS: Functions can contain arbitrary code, which may modify any sound parameter and/ or the synthesis core. Function code is triggered by table execution. Each function must end with a jump to either noTableExec or tblStdUpdate. When jumping to noTableExec, the player will return to the synthesis core. When jumping to tblStdUpdate, the player will immediately parse another row of table data instead. Using functions poses a significant risk of breaking/crashing the engine, of course. Do not use this feature unless you have a good understanding of how the engine code works. Generally speaking, any operations involving the stack is almost guaranteed to crash the player. Assembler Switches ================== At the top of main.asm, you will find 3 switches: USETABLES - enables tables USEDRUMS - enables click drums USELOOP - enables looping Set any of these switches to 0 to disable the feature. This will reduce the player size. Disabling tables will reduce the player size by 63 bytes, disabling click drums will reduce the size by 99 bytes, and disabling looping will reduce the size by 5 bytes.