aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Chris Xiong <chirs241097@gmail.com> 2022-08-09 02:56:10 -0400
committerGravatar Chris Xiong <chirs241097@gmail.com> 2022-08-09 02:56:10 -0400
commitaefdc8d0edad5d77d73ac40d8002363e12da00da (patch)
tree9b4de068a127034436998c970eb9337c5c54d2b0
downloadfifteen-thieves-aefdc8d0edad5d77d73ac40d8002363e12da00da.tar.xz
fures, ostendite se!
-rw-r--r--.gitignore1
-rw-r--r--LICENSE19
-rw-r--r--README.md119
-rw-r--r--amaps/gs.amap101
-rw-r--r--amaps/sd-80.amap441
-rw-r--r--amaps/xv-5080.amap471
-rw-r--r--data/.gitignore3
-rw-r--r--data/gm2pcbanks2
-rw-r--r--dump_patches.py54
-rw-r--r--inspect_memoryregion.py76
-rw-r--r--srx_convert.py280
-rw-r--r--test_amap.py9
-rw-r--r--wave_stats.py64
-rw-r--r--xv/__init__.py1
-rw-r--r--xv/amap.py265
-rw-r--r--xv/model.py140
-rw-r--r--xv/util.py90
17 files changed, 2136 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bee8a64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..89eb657
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2022 Chris Xiong
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..973d5fd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,119 @@
+# Fifteen Thieves
+
+**XV fures**
+
+Interface for talking to Roland synthesizers, and a few extra utilities (mostly in Python).
+
+## List of tools
+
+- `dump_patches.py`: Dump factory patches from a SD-80 or SD-90.
+- `inspect_memoryregion.py`: Show the content of a `.memoryregion` file generated from patch dumps
+ in a somewhat human-readable form.
+- `srx_convert.py`: Convert dumped SD-80 / SD-90 patches to Roland SRX VSTi plugin.
+- `test_amap.py`: Load an address map file and print the memory structure specified by it.
+- `wave_stats.py`: Generate stats on usage of SD-80 / SD-90 waveforms from the dumps.
+
+## Other files
+
+- `amap`: System exclusive address maps.
+ - `gs.amap`: Address map for SOUNDCanvas GS devices.
+ - `sd-80.amap`: Address map for STUDIOCanvas SD-80 and SD-90.
+ - `xv-5080.amap`: Address map for XV-5080 and other models in the XV lineup.
+- `xv`: Python library for interfacing with Roland DT1/RQ1 system exclusive messages.
+
+## Caution
+
+- Many scripts contain hardcoded MIDI ports. This is subject to change, but before such changes
+ happen, you'll have to modify them manually.
+
+- pypy is recommended for running all the scripts whenever possible.
+
+- All scripts are developed under Linux. There's no reason they wouldn't work on other major
+ platforms, though (given you have specified the correct MIDI port).
+
+## Information for individual tools
+
+### `dump_patches.py`
+
+Requires hardware SD-90 or SD-80.
+
+Give this script a few minutes to run, then you'll get the following files:
+
+```
+data
+ +--patchparam
+ | +--classical
+ | | +--00-000-Piano 1.memoryregion
+ | | +--...
+ | +--contemporary
+ | | +--00-000-Ac.Piano.memoryregion
+ | | +--...
+ | +--...
+ +--rhythmparam
+ +--classical
+ | +--00-Standard Set.memoryregion
+ | +--...
+ +--contemporary
+ | +--00-StandardSet2.memoryregion
+ | +--...
+ +--...
+```
+
+These files contain preset data for each individual factory patch and can be used
+by other tools.
+
+### `inspect_memoryregion.py`
+
+Requires one argument (path to the `.memoryregion` file). Prints the content of the given file
+in a semi-human-readable form.
+
+Needs `data/sdwavelist` to work. This file is still distributed by Roland Japan. You need to
+remove the header from the file (the first line contains name of the first waveform).
+
+### `srx_convert.py`
+
+Requires populated `data/patchparam` and `data/rhythmparam` folder. Converts patch data in
+these folders into a format that can be used by Roland SRX VSTi plugins. You need to mod
+the plugin with wave ROM dump of SD-90 or SD-80 for these patches to work. You'll have to
+obtain the dump yourself, as they are not in this repository for obvious reasons. Of course,
+you can use these patches with unmodified SRX VSTi plugins as well.
+
+Takes one argument, specifying the SRX VSTi title you want to generate the patches for.
+
+ | SRX Plguin | Parameter Used |
+ | --------------- | -------------- |
+ | Piano 1 | 2 |
+ | Studio | 3 |
+ | Strings | 4 |
+ | Dance Trax | 5 |
+ | Orchestra | 6 |
+ | Keyboards | 7 |
+ | World | 9 |
+ | Brass | 10 |
+ | Piano 2 | 11 |
+ | Electric Piano | 12 |
+
+Generated patches are in `data/srxsdpresets`. Subfolders indicates different organization
+of patches.
+
+### `test_amap.py`
+
+Load an address map file and print the memory structure specified by it. The path to the amap
+file is hardcoded.
+
+### `wave_stats.py`
+
+Requires populated `data/patchparam` and `data/rhythmparam` folder. Print how many times each
+waveform is used by all the patches.
+
+Needs `data/sdwavelist` to work. This file is still distributed by Roland Japan. You need to
+remove the header from the file (the first line contains name of the first waveform).
+
+## Disclaimer
+
+This project is in no way endorsed by or affiliated with Roland Corporation.
+This project contains no reverse engineered code, or data originated from Roland.
+All memory address maps are transcribed from public documentation. Misuse of this
+project can lead to legal disputes, and the author of this project shall not be
+held responsible for such misuse.
+
diff --git a/amaps/gs.amap b/amaps/gs.amap
new file mode 100644
index 0000000..f398df9
--- /dev/null
+++ b/amaps/gs.amap
@@ -0,0 +1,101 @@
+# Address mapping for Roland SoundCanvas/GS Devices
+# Reference: MIDI Implementation of Roland SC-8850/SC-8820/SC-88Pro/SC-88/SC-55 (MkII)
+
+sc_root*
+ 000000, system # 88/88p/8850/8820 only
+ 100000, display # 55/55M2/88/88p
+ 101000, function_control # 55M2 only
+ 200000, user_patches # 88/88p/8850/8820 only
+ 210000, user_drums # 88/88p/8850/8820 only
+ 400000, patch_common
+ 401000, patch_parts
+ 403000, info # 55 only
+ 410000, drum_setup
+ 480000, patch_bulk # 55/55M2 only
+ 490000, drum_bulk # 55/55M2 only
+ 500000, patch_common # 88/88p only, alternate port
+ 501000, patch_parts # 88/88p only, alternate port
+ 510000, drum_setup # 88/88p only, alternate port
+
+display
+ 0000, 07, char_display[20]
+ 0100, 07, dot_display[64]
+# 88/88p only
+ 0140, 07, dot_display2[64]
+ 0200, 07, dot_display3[64]
+ 0240, 07, dot_display4[64]
+ 0300, 07, dot_display5[64]
+ 0340, 07, dot_display6[64]
+ 0400, 07, dot_display7[64]
+ 0440, 07, dot_display8[64]
+ 0500, 07, dot_display9[64]
+ 0540, 07, dot_display10[64]
+# end 88/88p only
+# 55M2 only
+ 0800, 03, display_type
+ 0801, 02, peak_hold
+ 0810, 03, parameter_displayed
+# end 55M2 only
+# 88/88p only
+ 2000, 04, page_select
+ 2001, 04, display_time
+# end 88/88p only
+
+function_control
+ 0000, 04, part_select
+ 0001, 01, minus_one
+ 0002, 01, solo
+ 0100, 01, all_mute
+ 0111, 01, part_mute[16]
+
+system*
+ 007F, 01, system_mode
+ 0100, 02, part_port[64]
+
+patch_common
+ 0000, p4, master_tune
+ 0004, 07, master_volume
+ 0005, 07, master_key_shift
+ 0006, 07, master_pan
+ 007F, 01, mode_set
+ 0100, 07, patch_name[16]
+ 0130, 03, reverb_macro
+ 0131, 03, reverb_character
+ 0132, 03, reverb_filter
+ 0133, 07, reverb_level
+ 0134, 07, reverb_time
+ 0135, 07, reverb_feedback
+ 0137, 07, reverb_predelay
+ 0138, 03, chorus_macro
+ 0139, 03, chorus_filter
+ 013a, 07, chorus_level
+ 013b, 07, chorus_feedback
+ 013c, 07, chorus_delay
+ 013d, 07, chorus_rate
+ 013e, 07, chorus_depth
+ 013f, 07, chorus_send_reverb
+ 0140, 07, chorus_send_delay
+ 0150, 04, delay_macro
+ 0151, 03, delay_filter
+ 0152, 07, delay_time_c
+ 0153, 07, delay_time_ratio_l
+ 0154, 07, delay_time_ratio_r
+ 0155, 07, delay_level_c
+ 0156, 07, delay_level_l
+ 0157, 07, delay_level_r
+ 0158, 07, delay_level
+ 0159, 07, delay_feedback
+ 015a, 07, delay_send_reverb
+ 0200, 01, eq_low_freq
+ 0201, 07, eq_low_gain
+ 0202, 01, eq_high_freq
+ 0203, 07, eq_high_gain
+ 0300, 07, efx_type
+ 0303, 07, efx_param[20]
+ 0317, 07, efx_send_reverb
+ 0318, 07, efx_send_chorus
+ 0319, 07, efx_send_delay
+ 031b, 07, efx_ctrl_src[2+2]
+ 031c, 07, efx_ctrl_depth[2+2]
+ 031f, 01, efx_send_eq
+
diff --git a/amaps/sd-80.amap b/amaps/sd-80.amap
new file mode 100644
index 0000000..5f442eb
--- /dev/null
+++ b/amaps/sd-80.amap
@@ -0,0 +1,441 @@
+# Address mapping for EDIROL SD-80 (without User Patch/User Rhythm block)
+# Address mapping for EDIROL SD-90 (without Audio block)
+# Reference: MIDI Implementation of Edirol SD-90
+
+sd_root*
+ 00000000, setup
+ 01000000, system
+# 02000000, audio{SD-90 only}
+ 10000000, multitimbre
+ 11000000, part[32+200000]
+# 30000000, user_patch[128+10000]{SD-80 only}
+# 40000000, user_rhythm[16+100000]{SD-80 only}
+
+system*
+ 000000, sys_common
+ 000200, sys_eq
+
+part*
+ 000000, patch
+ 100000, rhythm
+
+multitimbre*
+ 000000, mt_common
+ 000200, chorus
+ 000400, reverb
+ 000600, mfx
+ 002000, mt_part[32+100]
+ 004000, mt_midi[32+100]
+
+patch*
+ 000000, pc_common
+ 000200, chorus
+ 000400, reverb
+ 000600, mfx
+ 001000, tmt
+ 002000, tone[4+200]
+
+tmt*
+ 0000, tmt_common
+ 0005, tmt_block[4+9]
+
+tone*
+ 0000, tn_common
+ 000c, tn_send
+ 0012, tn_rx
+ 0027, tn_wave
+ 003a, tn_pitch_env
+ 0048, tn_filter
+ 005e, tn_amp
+ 006d, tn_lfo[2+e]
+
+rhythm*
+ 000000, ry_common
+ 000200, chorus
+ 000400, reverb
+ 000600, mfx
+ 001000, rtone[88+200]
+
+rtone*
+ 0000, rt_common
+ 0016, rt_send
+ 001c, rt_rx
+ 0021, rt_wmt[4+1d]
+ 0115, rt_pitch_env
+ 0122, rt_filter
+ 0136, rt_amp
+
+setup
+ 0000, 07, native_on
+
+sys_common
+ 0000, p4, master_tune
+ 0004, 06, master_key_shift
+ 0005, 07, master_level
+ 0006, 07, sys_ctrl_src[4]
+# The following four switches are exclusive to SD-80
+ 000a, 01, mfx_switch
+ 000b, 01, chorus_switch
+ 000c, 01, reverb_switch
+ 000d, 01, scale_tune_switch
+
+sys_eq
+ 0000, 01, switch
+ 0001, 01, low_freq[8+4]
+ 0002, 05, low_gain[8+4]
+ 0003, 02, high_freq[8+4]
+ 0004, 05, high_gain[8+4]
+
+mt_common
+ 0000, 07, name[12]
+ 000c, 06, solo_part
+ 0010, 07, reserved[32]
+ 0030, 06, mfx_src[3]
+ 0033, 06, chorus_src
+ 0034, 06, reverb_src
+ 0035, 05, mfx_ctrl_ch[3+2]
+ 0036, 01, mfx_ctrl_port[3+2]
+
+mt_midi
+ 0000, 01, rx_program_change
+ 0001, 01, rx_bank_select
+ 0002, 01, rx_wheel
+ 0003, 01, rx_key_aftertouch
+ 0004, 01, rx_channel_aftertouch
+ 0005, 01, rx_modulation
+ 0006, 01, rx_volume
+ 0007, 01, rx_pan
+ 0008, 01, rx_expression
+ 0009, 01, rx_hold
+ 000a, 01, phase_lock
+ 000b, 03, velocity_curve
+
+mt_part
+ 0000, 04, rx_channel
+ 0001, 01, rx_switch
+ 0003, 01, rx_port
+ 0004, 07, bank_msb
+ 0005, 07, bank_lsb
+ 0006, 07, program_number
+ 0007, 07, volume
+ 0008, 07, pan
+ 0009, 07, coarse_tune
+ 000a, 07, fine_tune
+ 000b, 02, mono
+ 000c, 02, legato
+ 000d, 05, pitch_bend_range
+ 000e, 02, portamento_switch
+ 000f, p2, portamento_time
+ 0011, 07, cutoff
+ 0012, 07, resonance
+ 0013, 07, attack
+ 0014, 07, release
+ 0015, 03, octave_shift
+ 0016, 07, velocity_sens
+ 0017, 07, key_range_low
+ 0018, 07, key_range_high
+ 0019, 07, key_fade_lower
+ 001a, 07, key_fade_upper
+ 001b, 07, mute
+ 001c, 07, dry_send
+ 001d, 07, chorus_send
+ 001e, 07, reverb_send
+ 001f, 04, output
+ 0020, 02, dest_mfx
+ 0021, 07, decay
+ 0022, 07, vibrato_rate
+ 0023, 07, vibrato_depth
+ 0024, 07, vibrato_delay
+ 0025, 07, modulation_depth
+ 0026, 06, chaft_pitch
+ 0027, 07, chaft_filter
+ 0028, 07, chaft_amp
+ 0029, 07, chaft_lfo_pitch
+ 002a, 07, chaft_lfo_filter
+ 002b, 07, chaft_lfo_amp
+ 002c, 07, cc_assign
+ 002d, 06, cc_pitch
+ 002e, 07, cc_filter
+ 002f, 07, cc_amp
+ 0030, 07, cc_lfo_pitch
+ 0031, 07, cc_lfo_filter
+ 0032, 07, cc_lfo_amp
+ 0033, 07, scale_tune[12]
+ 003f, 02, gm2_inst_set
+
+pc_common
+ 0000, 07, name[12]
+ 000c, 07, category
+ 000d, 07, tone_type
+ 000e, 07, level
+ 000f, 07, pan
+ 0010, 07, priority
+ 0011, 07, coarse_tune
+ 0012, 07, fine_tune
+ 0013, 03, octave_shift
+ 0014, 02, stretch_tune
+ 0015, 07, analog_feel
+ 0016, 01, mono
+ 0017, 01, legato
+ 0018, 01, retrigger
+ 0019, 01, portamento_switch
+ 001a, 01, portamento_mode
+ 001b, 01, portamento_type
+ 001c, 01, portamento_start
+ 001d, 01, portamento_time
+ 001e, 01, clock_source
+ 001f, p2, tempo
+ 0021, 01, oneshot
+ 0022, 07, cutoff
+ 0023, 07, resonance
+ 0024, 07, attack
+ 0025, 07, release
+ 0026, 07, velocity_sens
+ 0027, 07, output
+ 0028, 01, tmt_control
+ 0029, 06, pbr_up
+ 002a, 06, pbr_down
+ 002b, 06, matrix_ctrl_1_src
+ 002c, 06, matrix_ctrl_1_dest[4+2]
+ 002d, 07, matrix_ctrl_1_sens[4+2]
+ 0034, 06, matrix_ctrl_2_src
+ 0035, 06, matrix_ctrl_2_dest[4+2]
+ 0036, 07, matrix_ctrl_2_sens[4+2]
+ 003d, 06, matrix_ctrl_3_src
+ 003e, 06, matrix_ctrl_3_dest[4+2]
+ 003f, 07, matrix_ctrl_3_sens[4+2]
+ 0046, 06, matrix_ctrl_4_src
+ 0047, 06, matrix_ctrl_4_dest[4+2]
+ 0048, 07, matrix_ctrl_4_sens[4+2]
+ 004f, 03, vibrato_lfo_waveform
+ 0050, 07, vibrato_rate
+ 0051, 07, vibrato_delay
+ 0052, 07, vibrato_attack
+ 0053, 07, vibrato_pitch
+ 0054, 07, vibrato_cutoff
+ 0055, 07, vibrato_amp
+
+tmt_common
+ 0000, 04, structure_12
+ 0001, 02, booster_12
+ 0002, 04, structure_34
+ 0003, 02, booster_34
+ 0004, 02, velocity_control
+
+tmt_block
+ 0000, 01, tone_switch
+ 0001, 07, key_range_lower
+ 0002, 07, key_range_upper
+ 0003, 07, key_fade_lower
+ 0004, 07, key_fade_upper
+ 0005, 07, velo_range_lower
+ 0006, 07, velo_range_upper
+ 0007, 07, velo_fade_lower
+ 0008, 07, velo_fade_upper
+
+tn_common
+ 0000, 07, level
+ 0001, 07, coarse_tune
+ 0002, 07, fine_tune
+ 0003, 05, pitch_random
+ 0004, 07, pan
+ 0005, 05, pan_keyfollow
+ 0006, 06, pan_random
+ 0007, 07, pan_alt
+ 0008, 01, env_mode
+ 0009, 02, delay_mode
+ 000a, p2, delay_time
+
+tn_send
+ 0000, 07, dry_send
+ 0001, 07, mfx_chorus_send
+ 0002, 07, mfx_reverb_send
+ 0003, 07, chorus_send
+ 0004, 07, reverb_send
+ 0005, 04, output
+
+tn_rx
+ 0000, 01, rx_wheel
+ 0001, 01, rx_expression
+ 0002, 01, rx_hold
+ 0003, 01, rx_pan_mode
+ 0004, 01, rx_redamper
+ 0005, 02, tone_control_switch[16]
+
+tn_wave
+ 0000, 02, wave_group_type
+ 0001, p4, wave_group_id
+ 0005, p4, wave_number_l
+ 0009, p4, wave_number_r
+ 000d, 02, gain
+ 000e, 01, fm
+ 000f, 02, fm_color
+ 0010, 05, fm_depth
+ 0011, 01, tempo_sync
+ 0012, 06, pitch_keyfollow
+
+tn_pitch_env
+ 0000, 07, env_depth
+ 0001, 07, env_velocity_sens
+ 0002, 07, env_t1_velocity_sens
+ 0003, 07, env_t4_velocity_sens
+ 0004, 07, env_time_keyfollow
+ 0005, 07, env_time[4]
+ 0009, 07, env_level[5]
+
+tn_filter
+ 0000, 03, type
+ 0001, 07, cutoff
+ 0002, 06, cutoff_keyfollow
+ 0003, 03, cutoff_velocity_curve
+ 0004, 07, cutoff_velocity_sens
+ 0005, 07, resonance
+ 0006, 07, resonance_velocity_sens
+ 0007, 07, env_depth
+ 0008, 07, env_velocity_curve
+ 0009, 07, env_velocity_sens
+ 000a, 07, env_t1_velocity_sens
+ 000b, 07, env_t4_velocity_sens
+ 000c, 07, env_time_keyfollow
+ 000d, 07, env_time[4]
+ 0011, 07, env_level[5]
+
+tn_amp
+ 0000, 05, bias_level
+ 0001, 07, bias_position
+ 0002, 02, bias_direction
+ 0003, 03, env_velocity_curve
+ 0004, 07, env_velocity_sens
+ 0005, 07, env_t1_velocity_sens
+ 0006, 07, env_t4_velocity_sens
+ 0007, 07, env_time_keyfollow
+ 0008, 07, env_time[4]
+ 000c, 07, env_level[3]
+
+tn_lfo
+ 0000, 07, waveform
+ 0001, p2, rate
+ 0003, 03, offset
+ 0004, 07, rate_detune
+ 0005, 07, delay
+ 0006, 07, delay_keyfollow
+ 0007, 02, fade_mode
+ 0008, 07, fade_time
+ 0009, 01, key_trigger
+ 000a, 07, pitch_depth
+ 000b, 07, filter_depth
+ 000c, 07, amp_depth
+ 000d, 07, pan_depth
+
+ry_common
+ 0000, 07, name[12]
+ 000c, 07, level
+ 000d, 07, clock_source
+ 000e, p2, tempo
+ 0010, 01, oneshot
+ 0011, 04, output
+
+rt_common
+ 0000, 07, name[12]
+ 000c, 01, assign_type
+ 000d, 05, mute_group
+ 000e, 07, level
+ 000f, 07, coarse_tune
+ 0010, 07, fine_tune
+ 0011, 05, pitch_random
+ 0012, 05, pan
+ 0013, 06, pan_random
+ 0014, 07, pan_alt
+ 0015, 01, env_mode
+
+rt_send
+ 0000, 07, dry_send
+ 0001, 07, mfx_chorus_send
+ 0002, 07, mfx_reverb_send
+ 0003, 07, chorus_send
+ 0004, 07, reverb_send
+ 0005, 04, output
+
+rt_rx
+ 0000, 06, pitch_bend_range
+ 0001, 01, rx_expression
+ 0002, 01, rx_hold
+ 0003, 01, rx_pan_mode
+ 0004, 02, velocity_control
+
+rt_wmt
+ 0000, 01, switch
+ 0001, 02, wave_group_type
+ 0002, p4, wave_group_id
+ 0006, p4, wave_number_l
+ 000a, p4, wave_number_r
+ 000e, 02, gain
+ 000f, 01, fm
+ 0010, 02, fm_color
+ 0011, 05, fm_depth
+ 0012, 01, tempo_sync
+ 0013, 07, coarse_tune
+ 0014, 07, fine_tune
+ 0015, 07, pan
+ 0016, 01, pan_random_switch
+ 0017, 02, pan_alt_switch
+ 0018, 07, level
+ 0019, 07, velo_range_lower
+ 001a, 07, velo_range_upper
+ 001b, 07, velo_fade_lower
+ 001c, 07, velo_fade_upper
+
+rt_pitch_env
+ 0000, 05, env_depth
+ 0001, 07, env_velocity_sens
+ 0002, 07, env_t1_velocity_sens
+ 0003, 07, env_t4_velocity_sens
+ 0004, 07, env_time[4]
+ 0008, 07, env_level[5]
+
+rt_filter
+ 0000, 03, type
+ 0001, 07, cutoff
+ 0002, 03, cutoff_velocity_curve
+ 0003, 07, cutoff_velocity_sens
+ 0004, 07, resonance
+ 0005, 07, resonance_velocity_sens
+ 0006, 07, env_depth
+ 0007, 07, env_velocity_curve
+ 0008, 07, env_velocity_sens
+ 0009, 07, env_t1_velocity_sens
+ 000a, 07, env_t4_velocity_sens
+ 000b, 07, env_time[4]
+ 000f, 07, env_level[5]
+
+rt_amp
+ 0000, 03, env_velocity_curve
+ 0001, 07, env_velocity_sens
+ 0002, 07, env_t1_velocity_sens
+ 0003, 07, env_t4_velocity_sens
+ 0004, 07, env_time[4]
+ 0008, 07, env_level[3]
+
+mfx
+ 0000, 07, type
+ 0001, 07, dry_send
+ 0002, 07, chorus_send
+ 0003, 07, reverb_send
+ 0004, 01, output
+ 0005, 07, ctrl_src[4+2]
+ 0006, 07, ctrl_sens[4+2]
+ 000d, 05, ctrl_assign[4]
+ 0011, p4, param[32+4]
+
+chorus
+ 0000, 04, type
+ 0001, 07, level
+ 0002, 02, output
+ 0003, 02, route
+ 0004, p4, param[12+4]
+
+reverb
+ 0000, 04, type
+ 0001, 07, level
+ 0002, 02, output
+ 0003, p4, param[20+4]
diff --git a/amaps/xv-5080.amap b/amaps/xv-5080.amap
new file mode 100644
index 0000000..4eff57c
--- /dev/null
+++ b/amaps/xv-5080.amap
@@ -0,0 +1,471 @@
+# Address mapping for Roland XV-5080
+# Reference: MIDI Implementation of Roland XV-5080
+
+xv_root*
+ 00000000, system
+ 10000000, performance
+ 11000000, part_perform[32+200000]
+ 1F000000, patch_or_rhythm
+ 20000000, user_performance[64+10000]
+ 30000000, user_patch[128+10000]
+ 40000000, user_rhythm[4+100000]
+
+system*
+ 000000, sys_common
+ 000200, sys_eq
+ 001000, sys_part[32+100]
+
+part_perform*
+ 000000, patch_or_rhythm
+
+user_performance*
+ 000000, performance
+
+user_patch*
+ 000000, patch
+
+user_rhythm*
+ 000000, rhythm
+
+patch_or_rhythm*
+ 000000, patch
+ 100000, rhythm
+
+performance*
+ 000000, pf_common
+ 000200, mfx
+ 000400, chorus
+ 000600, reverb
+ 001000, pf_midi[16+100]
+ 002000, pf_part[32+100]
+
+patch*
+ 000000, pc_common
+ 000200, mfx
+ 000400, chorus
+ 000600, reverb
+ 001000, tmt
+ 002000, tone[4+200]
+ 003000, split[88+20]
+
+rhythm*
+ 000000, ry_common
+ 000200, mfx
+ 000400, chorus
+ 000600, reverb
+ 001000, rtone[88+200]
+
+sys_common
+ 0000, 03, mode
+ 0001, p4, master_tune
+ 0005, 06, master_key_shift
+ 0006, 07, master_level
+ 0007, 01, scale_tune_switch
+ 0008, 01, patch_remain
+ 0009, 01, mix_parallel
+#=============================
+ 000a, 01, mfx_switch
+ 000b, 01, chorus_switch
+ 000c, 01, reverb_switch
+#=============================
+ 000d, 05, pf_ctrl_channel
+ 000e, 07, pf_msb
+ 000f, 07, pf_lsb
+ 0010, 07, pf_pc
+#=============================
+ 0011, 04, patch_rx_channel
+ 0012, 07, pt_msb
+ 0013, 07, pt_lsb
+ 0014, 07, pt_pc
+#=============================
+ 0015, 01, clock_source
+ 0016, p2, sys_tempo
+#=============================
+ 0018, 07, sys_ctrl_src[4]
+#=============================
+ 001c, 01, rx_program_change
+ 001d, 01, rx_bank_select
+
+sys_eq
+ 0000, 01, switch
+ 0001, 01, low_freq[8+4]
+ 0002, 05, low_gain[8+4]
+ 0003, 02, high_freq[8+4]
+ 0004, 05, high_gain[8+4]
+
+sys_part
+ 0000, 07, scale_tune[12]
+
+pf_common
+ 0000, 07, name[12]
+ 000c, 06, solo_part
+ 000d, 05, mfx_ctrl_ch
+ 000e, 01, mfx_ctrl_midi1
+ 000f, 01, mfx_ctrl_midi1
+ 0010, 07, voice_reserve[32]
+ 0030, 06, mfx_src[3]
+ 0033, 06, chorus_src
+ 0034, 06, reverb_src
+
+pf_midi
+ 0000, 01, rx_program_change
+ 0001, 01, rx_bank_select
+ 0002, 01, rx_wheel
+ 0003, 01, rx_key_aftertouch
+ 0004, 01, rx_channel_aftertouch
+ 0005, 01, rx_modulation
+ 0006, 01, rx_volume
+ 0007, 01, rx_pan
+ 0008, 01, rx_expression
+ 0009, 01, rx_hold
+ 000a, 01, phase_lock
+ 000b, 03, velocity_curve
+
+pf_part
+ 0000, 04, rx_channel
+ 0001, 01, rx_switch
+ 0003, 01, rx_port
+#=============================
+ 0004, 07, bank_msb
+ 0005, 07, bank_lsb
+ 0006, 07, program_number
+#=============================
+ 0007, 07, volume
+ 0008, 07, pan
+ 0009, 07, coarse_tune
+ 000a, 07, fine_tune
+ 000b, 02, mono
+ 000c, 02, legato
+ 000d, 05, pitch_bend_range
+ 000e, 02, portamento_switch
+ 000f, p2, portamento_time
+ 0011, 07, cutoff
+ 0012, 07, resonance
+ 0013, 07, attack
+ 0014, 07, release
+#=============================
+ 0015, 03, octave_shift
+ 0016, 07, velocity_sens
+ 0017, 07, key_range_low
+ 0018, 07, key_range_high
+ 0019, 07, key_fade_lower
+ 001a, 07, key_fade_upper
+ 001b, 01, mute
+#=============================
+ 001c, 07, dry_send
+ 001d, 07, chorus_send
+ 001e, 07, reverb_send
+ 001f, 04, output
+ 0020, 02, dest_mfx
+
+pc_common
+ 0000, 07, name[12]
+ 000c, 07, category
+#=============================
+ 000d, 01, tone_type
+#=============================
+ 000e, 07, level
+ 000f, 07, pan
+ 0010, 07, priority
+ 0011, 07, coarse_tune
+ 0012, 07, fine_tune
+ 0013, 03, octave_shift
+ 0014, 02, stretch_tune_depth
+ 0015, 07, analog_feel
+ 0016, 01, mono
+ 0017, 01, legato
+ 0018, 01, retrigger
+ 0019, 01, portamento_switch
+ 001a, 01, portamento_mode
+ 001b, 01, portamento_type
+ 001c, 01, portamento_start
+ 001d, 01, portamento_time
+ 001e, 01, clock_source
+ 001f, p2, tempo
+ 0021, 01, oneshot
+#=============================
+ 0022, 07, cutoff
+ 0023, 07, resonance
+ 0024, 07, attack
+ 0025, 07, release
+ 0026, 07, velocity_sens
+#=============================
+ 0027, 07, output
+#=============================
+ 0028, 01, tmt_control
+ 0029, 06, pbr_up
+ 002a, 06, pbr_down
+#=============================
+ 002b, 06, matrix_ctrl_1_src
+ 002c, 06, matrix_ctrl_1_dest[4+2]
+ 002d, 07, matrix_ctrl_1_sens[4+2]
+#=============================
+ 0034, 06, matrix_ctrl_2_src
+ 0035, 06, matrix_ctrl_2_dest[4+2]
+ 0036, 07, matrix_ctrl_2_sens[4+2]
+#=============================
+ 003d, 06, matrix_ctrl_3_src
+ 003e, 06, matrix_ctrl_3_dest[4+2]
+ 003f, 07, matrix_ctrl_3_sens[4+2]
+#=============================
+ 0046, 06, matrix_ctrl_4_src
+ 0047, 06, matrix_ctrl_4_dest[4+2]
+ 0048, 07, matrix_ctrl_4_sens[4+2]
+
+tmt*
+ 0000, tmt_common
+ 0005, tmt_block[4+9]
+
+tmt_common
+ 0000, 04, structure_12
+ 0001, 02, booster_12
+ 0002, 04, structure_34
+ 0003, 02, booster_34
+ 0004, 02, velocity_control
+
+tmt_block
+ 0000, 01, tone_switch
+ 0001, 07, key_range_lower
+ 0002, 07, key_range_upper
+ 0003, 07, key_fade_lower
+ 0004, 07, key_fade_upper
+ 0005, 07, velo_range_lower
+ 0006, 07, velo_range_upper
+ 0007, 07, velo_fade_lower
+ 0008, 07, velo_fade_upper
+
+tone*
+ 0000, tn_common
+ 000c, tn_send
+ 0012, tn_rx
+ 0027, tn_wave
+ 003a, tn_pitch_env
+ 0048, tn_filter
+ 005e, tn_amp
+ 006d, tn_lfo[2+e]
+
+tn_common
+ 0000, 07, level
+ 0001, 07, coarse_tune
+ 0002, 07, fine_tune
+ 0003, 05, pitch_random
+ 0004, 07, pan
+ 0005, 05, pan_keyfollow
+ 0006, 06, pan_random
+ 0007, 07, pan_alt
+ 0008, 01, env_mode
+ 0009, 02, delay_mode
+ 000a, p2, delay_time
+
+tn_send
+ 0000, 07, dry_send
+ 0001, 07, mfx_chorus_send
+ 0002, 07, mfx_reverb_send
+ 0003, 07, chorus_send
+ 0004, 07, reverb_send
+ 0005, 04, output
+
+tn_rx
+ 0000, 01, rx_wheel
+ 0001, 01, rx_expression
+ 0002, 01, rx_hold
+ 0003, 01, rx_pan_mode
+ 0004, 01, rx_redamper
+ 0005, 02, tone_control_switch[16]
+
+tn_wave
+ 0000, 02, wave_group_type
+ 0001, p4, wave_group_id
+ 0005, p4, wave_number_l
+ 0009, p4, wave_number_r
+ 000d, 02, gain
+ 000e, 01, fm
+ 000f, 02, fm_color
+ 0010, 05, fm_depth
+ 0011, 01, tempo_sync
+ 0012, 06, pitch_keyfollow
+
+tn_pitch_env
+ 0000, 07, env_depth
+ 0001, 07, env_velocity_sens
+ 0002, 07, env_t1_velocity_sens
+ 0003, 07, env_t4_velocity_sens
+ 0004, 07, env_time_keyfollow
+ 0005, 07, env_time[4]
+ 0009, 07, env_level[5]
+
+tn_filter
+ 0000, 03, type
+ 0001, 07, cutoff
+ 0002, 06, cutoff_keyfollow
+ 0003, 03, cutoff_velocity_curve
+ 0004, 07, cutoff_velocity_sens
+ 0005, 07, resonance
+ 0006, 07, resonance_velocity_sens
+ 0007, 07, env_depth
+ 0008, 07, env_velocity_curve
+ 0009, 07, env_velocity_sens
+ 000a, 07, env_t1_velocity_sens
+ 000b, 07, env_t4_velocity_sens
+ 000c, 07, env_time_keyfollow
+ 000d, 07, env_time[4]
+ 0011, 07, env_level[5]
+
+tn_amp
+ 0000, 05, bias_level
+ 0001, 07, bias_position
+ 0002, 02, bias_direction
+ 0003, 03, env_velocity_curve
+ 0004, 07, env_velocity_sens
+ 0005, 07, env_t1_velocity_sens
+ 0006, 07, env_t4_velocity_sens
+ 0007, 07, env_time_keyfollow
+ 0008, 07, env_time[4]
+ 000c, 07, env_level[3]
+
+tn_lfo
+ 0000, 07, waveform
+ 0001, p2, rate
+ 0003, 03, offset
+ 0004, 07, rate_detune
+ 0005, 07, delay
+ 0006, 05, delay_keyfollow
+ 0007, 02, fade_mode
+ 0008, 07, fade_time
+ 0009, 01, key_trigger
+ 000a, 07, pitch_depth
+ 000b, 07, filter_depth
+ 000c, 07, amp_depth
+ 000d, 07, pan_depth
+
+split
+ 0000, p4, partial_no
+ 0004, 01, assign_type
+ 0005, 05, assign_group
+ 0006, 07, dry_send
+ 0007, 07, mfx_chorus_send
+ 0008, 07, mfx_reverb_send
+ 0009, 07, chorus_send
+ 000a, 07, reverb_send
+ 000b, 04, output
+
+rtone*
+ 0000, rt_common
+ 0016, rt_send
+ 001c, rt_rx
+ 0021, rt_wmt[4+1d]
+ 0115, rt_pitch_env
+ 0122, rt_filter
+ 0136, rt_amp
+
+ry_common
+ 0000, 07, name[12]
+ 000c, 07, level
+ 000d, 07, clock_source
+ 000e, p2, tempo
+ 0010, 01, oneshot
+ 0011, 04, output
+
+rt_common
+ 0000, 07, name[12]
+ 000c, 01, assign_type
+ 000d, 05, mute_group
+ 000e, 07, level
+ 000f, 07, coarse_tune
+ 0010, 07, fine_tune
+ 0011, 05, pitch_random
+ 0012, 05, pan
+ 0013, 06, pan_random
+ 0014, 07, pan_alt
+ 0015, 01, env_mode
+
+rt_send
+ 0000, 07, dry_send
+ 0001, 07, mfx_chorus_send
+ 0002, 07, mfx_reverb_send
+ 0003, 07, chorus_send
+ 0004, 07, reverb_send
+ 0005, 04, output
+
+rt_rx
+ 0000, 06, pitch_bend_range
+ 0001, 01, rx_expression
+ 0002, 01, rx_hold
+ 0003, 01, rx_pan_mode
+ 0004, 02, velocity_control
+
+rt_wmt
+ 0000, 01, switch
+ 0001, 02, wave_group_type
+ 0002, p4, wave_group_id
+ 0006, p4, wave_number_l
+ 000a, p4, wave_number_r
+ 000e, 02, gain
+ 000f, 01, fm
+ 0010, 02, fm_color
+ 0011, 05, fm_depth
+ 0012, 01, tempo_sync
+ 0013, 07, coarse_tune
+ 0014, 07, fine_tune
+ 0015, 07, pan
+ 0016, 01, pan_random_switch
+ 0017, 02, pan_alt_switch
+ 0018, 07, level
+ 0019, 07, velo_range_lower
+ 001a, 07, velo_range_upper
+ 001b, 07, velo_fade_lower
+ 001c, 07, velo_fade_upper
+
+rt_pitch_env
+ 0000, 05, env_depth
+ 0001, 07, env_velocity_sens
+ 0002, 07, env_t1_velocity_sens
+ 0003, 07, env_t4_velocity_sens
+ 0004, 07, env_time[4]
+ 0008, 07, env_level[5]
+
+rt_filter
+ 0000, 03, type
+ 0001, 07, cutoff
+ 0002, 03, cutoff_velocity_curve
+ 0003, 07, cutoff_velocity_sens
+ 0004, 07, resonance
+ 0005, 07, resonance_velocity_sens
+ 0006, 07, env_depth
+ 0007, 07, env_velocity_curve
+ 0008, 07, env_velocity_sens
+ 0009, 07, env_t1_velocity_sens
+ 000a, 07, env_t4_velocity_sens
+ 000b, 07, env_time[4]
+ 000f, 07, env_level[5]
+
+rt_amp
+ 0000, 03, env_velocity_curve
+ 0001, 07, env_velocity_sens
+ 0002, 07, env_t1_velocity_sens
+ 0003, 07, env_t4_velocity_sens
+ 0004, 07, env_time[4]
+ 0008, 07, env_level[3]
+
+mfx
+ 0000, 07, type
+ 0001, 07, dry_send
+ 0002, 07, chorus_send
+ 0003, 07, reverb_send
+ 0004, 02, output
+ 0005, 07, ctrl_src[4+2]
+ 0006, 07, ctrl_sens[4+2]
+ 000d, 05, ctrl_assign[4]
+ 0011, p4, param[32+4]
+
+chorus
+ 0000, 04, type
+ 0001, 07, level
+ 0003, 02, output
+ 0004, p4, param[12+4]
+
+reverb
+ 0000, 04, type
+ 0001, 07, level
+ 0002, 02, output
+ 0003, p4, param[20+4]
diff --git a/data/.gitignore b/data/.gitignore
new file mode 100644
index 0000000..d5ea748
--- /dev/null
+++ b/data/.gitignore
@@ -0,0 +1,3 @@
+*
+!.gitignore
+!gm2pcbanks
diff --git a/data/gm2pcbanks b/data/gm2pcbanks
new file mode 100644
index 0000000..7143a9c
--- /dev/null
+++ b/data/gm2pcbanks
@@ -0,0 +1,2 @@
+3,2,2,2,4,5,4,2,1,1,1,2,2,1,3,1,4,3,1,3,2,2,1,1,4,4,2,3,4,2,3,2,1,2,1,1,1,1,5,4,2,1,1,1,1,1,2,1,3,1,2,1,2,2,2,4,2,3,1,2,2,2,4,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,5,1,1,2,1,1,2,1,2,1,2,1,1,1,1,1,1,2,1,1,1,3,1,2,1,1,2,1,1,1,1,1,1,1,2,2,2,3,1,3,2,6,4,6,a,6,4
+0,8,16,24,25,32,40,48,56
diff --git a/dump_patches.py b/dump_patches.py
new file mode 100644
index 0000000..22416c8
--- /dev/null
+++ b/dump_patches.py
@@ -0,0 +1,54 @@
+# Part of the Fifteen-Thieves Project
+# Chris Xiong 2020
+# License: Expat (MIT)
+
+import time
+import math
+import sys
+import xv.model
+import xv.amap
+import xv.util
+
+inst_sets=[(96,"classical"),(97,"contemporary"),(98,"solo"),(99,"enhanced"),(80,"special_1"),(81,"special_2")]
+rhyt_sets=[(104,"classical"),(105,"contemporary"),(106,"solo"),(107,"enhanced")]
+
+if __name__ == "__main__":
+ print("loading address mapping...", flush=True)
+ m = xv.amap.AddrMapTemplate("amaps/sd-80.amap")
+ t = xv.amap.create_addr_map_tree(m)
+ print("creating device...", flush=True)
+ x = xv.model.Model("SD-80 Part A",[0x00, 0x48],0x10,t)
+ print("initializing device...", flush=True)
+ x.write_locations("sd_root.setup.native_on", 0)
+ time.sleep(0.05)
+
+ gm2pcb=[]
+ gm2rhy=[]
+ with open("data/gm2pcbanks","r") as f:
+ gm2pcb = [int(x.strip(), 16) for x in f.readline().split(',')]
+ gm2rhy = [int(x.strip()) for x in f.readline().split(',')]
+
+ for iset in inst_sets:
+ for pc in range(0, 128):
+ for bk in range(0, 1 if iset[0] < 96 else gm2pcb[pc]):
+ x.midio.send_message([0xb0,0x00,iset[0]])
+ x.midio.send_message([0xb0,0x20,bk])
+ x.midio.send_message([0xc0,pc])
+ mr = x.read_locations("sd_root.part[0]", 1)
+ pcn = x.aroot.value("sd_root.part[0].patch.pc_common.name")
+ pcn = pcn.decode().strip().replace('/','∕')
+ print(f"{bk:02d}:{pc:03d} {pcn}")
+ fn = f"data/patchparam/{iset[1]}/{bk:02d}-{pc:03d}-{pcn}.memoryregion"
+ xv.util.write_memoryregion(fn, mr)
+
+ for rset in rhyt_sets:
+ for pc in gm2rhy:
+ x.midio.send_message([0xb0,0x00,rset[0]])
+ x.midio.send_message([0xb0,0x20,0])
+ x.midio.send_message([0xc0,pc])
+ mr = x.read_locations("sd_root.part[0]", 2)
+ pcn = x.aroot.value("sd_root.part[0].rhythm.ry_common.name")
+ pcn = pcn.decode().strip().replace('/','∕')
+ print(f"{pc:02d} {pcn}")
+ fn = f"data/rhythmparam/{rset[1]}/{pc:02d}-{pcn}.memoryregion"
+ xv.util.write_memoryregion(fn, mr)
diff --git a/inspect_memoryregion.py b/inspect_memoryregion.py
new file mode 100644
index 0000000..7fdccd2
--- /dev/null
+++ b/inspect_memoryregion.py
@@ -0,0 +1,76 @@
+# Part of the Fifteen-Thieves Project
+# Chris Xiong 2020
+# License: Expat (MIT)
+import sys
+import xv.util
+import xv.amap
+
+wavenames = ['None']
+
+def dfs_checkpopulated(node):
+ node.populated = False
+ for ch in node.children_nodes:
+ if node.template.is_aggregate_node:
+ if type(ch) == list:
+ for el in ch:
+ node.populated |= dfs_checkpopulated(el)
+ else:
+ node.populated |= dfs_checkpopulated(ch)
+ else:
+ for a in node.get_data():
+ if a != 0xff:
+ node.populated = True
+ break
+ return node.populated
+
+def dfs_print(node, level=0, instance=None):
+ if not node.populated:
+ return
+ ba_rep = ''.join([format(x, "02x") for x in xv.util.padbelist(xv.util.int2b7belist(node.base_address))])
+ print(f"{ba_rep}{level*' '} {node.template.name}{'['+str(instance)+']' if instance is not None else ''}")
+ for child in node.children_nodes:
+ if node.template.is_aggregate_node:
+ if type(child) == list:
+ for i, element in enumerate(child):
+ dfs_print(element, level + 1, i)
+ else:
+ dfs_print(child, level + 1)
+ else:
+ if type(child) == list:
+ if child[0].name == "name":
+ print(f"{ba_rep}{(level + 1)*' '} {child[0].name} {node.data[child[0].offset:child[0].offset + 12].decode()}")
+ else:
+ for i, element in enumerate(child):
+ ba_rep = ''.join([format(x, "02x") for x in xv.util.padbelist(xv.util.int2b7belist(element.offset))])
+ print(f"{ba_rep}{(level + 1)*' '} {element.name}[{i}] {xv.amap.get_leaf_data(node.data, element)}")
+ else:
+ ba_rep = ''.join([format(x, "02x") for x in xv.util.padbelist(xv.util.int2b7belist(child.offset))])
+ if child.name.startswith("wave_number"):
+ try:
+ print(f"{ba_rep}{(level + 1)*' '} {child.name} {xv.amap.get_leaf_data(node.data, child)}: {wavenames[xv.amap.get_leaf_data(node.data, child)]}")
+ except IndexError:
+ print(f"{ba_rep}{(level + 1)*' '} {child.name} {xv.amap.get_leaf_data(node.data, child)}: unknown")
+ else:
+ print(f"{ba_rep}{(level + 1)*' '} {child.name} {xv.amap.get_leaf_data(node.data, child)}")
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print(f"Usage: {sys.argv[0]} <memoryregion file>")
+ print("loading address mapping...", flush=True, file=sys.stderr)
+ m = xv.amap.AddrMapTemplate("amaps/sd-80.amap")
+ t = xv.amap.create_addr_map_tree(m)
+ t.data[:] = [0xff for i in range(0, len(t.data))]
+
+ print("loading wave names...", file=sys.stderr)
+ with open("data/sdwavelist","r") as f:
+ for l in f:
+ wavenames += [l.split('\t')[1].strip()]
+
+ print("loading memoryregion...", file=sys.stderr)
+ mr = xv.util.load_memoryregion(sys.argv[1])
+ for addr, data in mr:
+ ra = xv.util.b7belist2int(addr)
+ t.data[ra:ra + len(data)] = data
+
+ dfs_checkpopulated(t)
+ dfs_print(t)
diff --git a/srx_convert.py b/srx_convert.py
new file mode 100644
index 0000000..e7a98c1
--- /dev/null
+++ b/srx_convert.py
@@ -0,0 +1,280 @@
+# Part of the Fifteen-Thieves Project
+# Chris Xiong 2022
+# License: Expat (MIT)
+
+import time
+import math
+import sys
+from glob import glob
+import xv.amap
+import xv.util
+import os
+
+inst_sets = [(96,"classical"),(97,"contemporary"),(98,"solo"),(99,"enhanced"),(80,"special_1"),(81,"special_2")]
+rhyt_sets = [(104,"classical"),(105,"contemporary"),(106,"solo"),(107,"enhanced")]
+mfx_map = [ 0, 1, 35, 36, -1, # 0
+ -1, 7, 8, 21, -1, # 5
+ -1, 26, 27, 28, 23, # 10
+ 24, 25, 43, 46, 47, # 15
+ 48, 53, 62, 61, 64, # 20
+ 65, 66, 67, 68, 69, # 25
+ 70, 71, 72, 73, 74, # 30
+ 75, 76, 77, -1, -1, # 35
+ -1, 11, -1, -1, 15, # 40
+ 49, 50, 51, 52, -1, # 45
+ 57, 56, 10, 37, 38, # 50
+ 40, 41, 42, 20, 3, # 55
+ 29, 30, 17, 18, -1, # 60
+ -1, 9, -1, -1, -1, # 65
+ -1, 22, -1, -1, -1, # 70
+ -1, -1, -1, 39, -1, # 75
+ -1, -1, -1, -1, -1, # 80
+ -1, -1, -1, 2, -1, # 85
+ -1] # 90
+mfx_offsets = [
+ 0x0000, 0x48c3, 0x48f3, 0x491f, 0x494b, # 0
+ 0x4967, 0x499f, 0x4a07, 0x4a1f, 0x4a57, # 5
+ 0x4a97, 0x4aaf, 0x4ae7, 0x4b2b, 0x4b5f, # 10
+ 0x4b83, 0x4ba3, 0x4c07, 0x4c2b, 0x4c4f, # 15
+ 0x4caf, 0x4d17, 0x4d47, 0x4d8b, 0x4dbf, # 20
+ 0x4df7, 0x4e3b, 0x4e67, 0x4e9b, 0x4ec7, # 25
+ 0x4eff, 0x4f3b, 0x4f83, 0x4fc3, 0x500b, # 30
+ 0x506b, 0x5087, 0x50a3, 0x50c7, 0x50eb, # 35
+ 0x5133, 0x514f, 0x516f, 0x518f, 0x51cf, # 40
+ 0x51ff, 0x523f, 0x528b, 0x52d7, 0x5333, # 45
+ 0x539f, 0x540f, 0x544f, 0x549f, 0x54cb, # 50
+ 0x54fb, 0x5537, 0x557b, 0x559f, 0x55c7, # 55
+ 0x55db, 0x5613, 0x563f, 0x5693, 0x570f, # 60
+ 0x5733, 0x5753, 0x577b, 0x57a7, 0x57cf, # 65
+ 0x57f7, 0x5823, 0x584b, 0x5873, 0x589f, # 70
+ 0x58c7, 0x58ff, 0x593b, 0x5977, 0x59c3 # 75
+]
+# off chorus delay gm2 end
+chorus_offsets = [0x0000, 0x59c3, 0x59e7, 0x5a1f, 0x5a3b]
+# off reverb s.room s.hall s.plat gm2 end
+reverb_offsets = [0x0000, 0x5a3b, 0x5a4b, 0x5a73, 0x5a9b, 0x5ac3, 0x5ad7]
+
+#srx-09 is NEX04, srx-07 is NEX02
+
+headers = [
+ b"",
+ b"",
+ b"KoaBankFile00003PG-NEX09", # Piano 1 / Concert Piano
+ b"KoaBankFile00003PG-NEX05", # Studio / Studio SRX
+ b"KoaBankFile00003PG-NEX07", # Strings / Symphonique Strings
+ b"KoaBankFile00003PG-NEX03", # Dance Trax / Supreme Dance + Platinum Trax
+ b"KoaBankFile00003PG-NEX01", # Orchestra / Complete Orchestra
+ b"KoaBankFile00003PG-NEX02", # Keyboards / Ultimate Keys
+ b"KoaBankFile00003PG-NEX03", # Dance Trax / Supreme Dance + Platinum Trax
+ b"KoaBankFile00003PG-NEX04", # World / World Collection
+ b"KoaBankFile00003PG-NEX08", # Brass / Big Brass Ensemble
+ b"KoaBankFile00003PG-NEX10", # Piano 2 / Complete Piano
+ b"KoaBankFile00003PG-NEX06", # Electric Piano / Classic EPs
+ ]
+
+header = b"KoaBankFile00003PG-NEX02"
+template = b""
+amap = xv.amap.AddrMapTemplate("amaps/sd-80.amap")
+tree = xv.amap.create_addr_map_tree(amap)
+tree.data[:] = [0xff for i in range(0, len(tree.data))]
+
+def load_memregion(path):
+ mr = xv.util.load_memoryregion(path)
+ for addr, data in mr:
+ ra = xv.util.b7belist2int(addr)
+ tree.data[ra:ra + len(data)] = data
+
+def clip_or_pad(b, l):
+ if len(b) < l:
+ return b + b'\00' * (l - len(b))
+ else: return b[0:l]
+
+def convert_patch(iset, bk, pc):
+ ret = bytearray(template)
+ name = tree.value("sd_root.part[0].patch.pc_common.name")
+ ret[0:12] = name
+
+ common = tree.value("sd_root.part[0].patch.pc_common")
+ ret[0x54 : 0x54 + len(common)] = common
+
+ patch_common = tree.find_node("sd_root.part[0].patch.pc_common")
+ do_mod_lfo = patch_common.value("vibrato_pitch") != 64 or \
+ patch_common.value("vibrato_cutoff") != 64 or \
+ patch_common.value("vibrato_amp") != 64
+
+ tmt = tree.value("sd_root.part[0].patch.tmt")
+ ret[0x1dc : 0x1dc + len(tmt)] = tmt
+
+ tone_offsets = [0x205, 0x29f, 0x339, 0x3d3]
+ for t in range(0, 4):
+ tree.set_value(f"sd_root.part[0].patch.tone[{t}].tn_wave.wave_group_type", 1)
+ if do_mod_lfo:
+ lfo = []
+ lfo.append(tree.find_node(f"sd_root.part[0].patch.tone[{t}].tn_lfo[0]"))
+ lfo.append(tree.find_node(f"sd_root.part[0].patch.tone[{t}].tn_lfo[1]"))
+ dst_lfo = -1
+ for i in range(0, 2):
+ if lfo[i].value("pitch_depth") == 64 and \
+ lfo[i].value("filter_depth") == 64 and \
+ lfo[i].value("amp_depth") == 64 and \
+ lfo[i].value("pan_depth") == 64:
+ dst_lfo = i
+ break
+ if dst_lfo == -1:
+ print(f"WARNING ({iset}, {bk}, {pc}): cannot relocate modulation LFO for tone {t}")
+ else:
+ lfo[dst_lfo].set_value("waveform", patch_common.value("vibrato_lfo_waveform"))
+ lfo[dst_lfo].set_value("rate", patch_common.value("vibrato_rate"))
+ lfo[dst_lfo].set_value("offset", 2)
+ lfo[dst_lfo].set_value("rate_detune", 0)
+ lfo[dst_lfo].set_value("delay", patch_common.value("vibrato_delay"))
+ lfo[dst_lfo].set_value("delay_keyfollow", 64)
+ lfo[dst_lfo].set_value("fade_mode", 0)
+ lfo[dst_lfo].set_value("fade_time", patch_common.value("vibrato_attack"))
+ lfo[dst_lfo].set_value("key_trigger", 0)
+ lfo[dst_lfo].set_value("pitch_depth", patch_common.value("vibrato_pitch"))
+ lfo[dst_lfo].set_value("filter_depth", patch_common.value("vibrato_cutoff"))
+ lfo[dst_lfo].set_value("amp_depth", patch_common.value("vibrato_amp"))
+ lfo[dst_lfo].set_value("pan_depth", 64)
+ tone = tree.value(f"sd_root.part[0].patch.tone[{t}]")
+ ret[tone_offsets[t] : tone_offsets[t] + len(tone)] = tone
+
+ mfx = tree.value("sd_root.part[0].patch.mfx")
+ mfx_type = tree.value("sd_root.part[0].patch.mfx.type")
+ ret[0xa4 : 0xa4 + 13] = mfx[0:13]
+ mapped_mfx = mfx_map[tree.value("sd_root.part[0].patch.mfx.type")]
+ if mapped_mfx == -1:
+ print(f"WARNING ({iset}, {bk}, {pc}): no valid MFX mapped (XV MFX #{mfx_type})")
+ else:
+ ret[0xa4] = mapped_mfx
+ if mapped_mfx != 0:
+ ret[mfx_offsets[mapped_mfx] : mfx_offsets[mapped_mfx + 1]] = mfx[13 : 13 + mfx_offsets[mapped_mfx + 1] - mfx_offsets[mapped_mfx]]
+
+ chorus = tree.value("sd_root.part[0].patch.chorus")
+ chorus_type = tree.value("sd_root.part[0].patch.chorus.type")
+ ret[0x135 : 0x135 + 4] = chorus[0:4]
+ if chorus_type != 0:
+ ret[chorus_offsets[chorus_type] : chorus_offsets[chorus_type + 1]] = clip_or_pad(chorus[4:], chorus_offsets[chorus_type + 1] - chorus_offsets[chorus_type])
+
+ reverb = tree.value("sd_root.part[0].patch.reverb")
+ reverb_type = tree.value("sd_root.part[0].patch.reverb.type")
+ ret[0x189 : 0x189 + 3] = reverb[0:3]
+ if reverb_type != 0:
+ ret[reverb_offsets[reverb_type] : reverb_offsets[reverb_type + 1]] = reverb[3 : 3 + reverb_offsets[reverb_type + 1] - reverb_offsets[reverb_type]]
+
+ if len(ret) != 0x5ad7:
+ print(f"ERROR: ({iset}, {bk}, {pc}) messed up: {len(ret):x}!!! {chorus_type}")
+ return ret
+
+def convert_rhythm(rset, pc):
+ ret = bytearray(template)
+ name = tree.value("sd_root.part[0].rhythm.ry_common.name")
+ ret[0:12] = name
+ ret[0x0c] = 1
+
+ common = tree.value("sd_root.part[0].rhythm.ry_common")
+ ret[0x46d : 0x46d + len(common)] = common
+
+ for key in range(0, 88):
+ for tone in range(0, 4):
+ tree.set_value(f"sd_root.part[0].rhythm.rtone[{key}].rt_wmt[{tone}].wave_group_type", 1)
+ rtone = tree.value(f"sd_root.part[0].rhythm.rtone[{key}]")
+ ret[0x5b7 + 0xc3 * key : 0x5b7 + 0xc3 * key + len(rtone)] = rtone
+
+ mfx = tree.value("sd_root.part[0].rhythm.mfx")
+ mfx_type = tree.value("sd_root.part[0].rhythm.mfx.type")
+ ret[0x47f : 0x47f + 13] = mfx[0:13]
+ mapped_mfx = mfx_map[tree.value("sd_root.part[0].rhythm.mfx.type")]
+ if mapped_mfx == -1:
+ print(f"WARNING ({rset}, {pc}): no valid MFX mapped (XV MFX #{mfx_type})")
+ else:
+ ret[0xa4] = mapped_mfx
+ if mapped_mfx != 0:
+ ret[mfx_offsets[mapped_mfx] : mfx_offsets[mapped_mfx + 1]] = mfx[13 : 13 + mfx_offsets[mapped_mfx + 1] - mfx_offsets[mapped_mfx]]
+
+ chorus = tree.value("sd_root.part[0].rhythm.chorus")
+ chorus_type = tree.value("sd_root.part[0].rhythm.chorus.type")
+ ret[0x510 : 0x510 + 4] = chorus[0:4]
+ if chorus_type != 0:
+ ret[chorus_offsets[chorus_type] : chorus_offsets[chorus_type + 1]] = clip_or_pad(chorus[4:], chorus_offsets[chorus_type + 1] - chorus_offsets[chorus_type])
+
+ reverb = tree.value("sd_root.part[0].rhythm.reverb")
+ reverb_type = tree.value("sd_root.part[0].rhythm.reverb.type")
+ ret[0x564 : 0x564 + 3] = reverb[0:3]
+ if reverb_type != 0:
+ ret[reverb_offsets[reverb_type] : reverb_offsets[reverb_type + 1]] = reverb[3 : 3 + reverb_offsets[reverb_type + 1] - reverb_offsets[reverb_type]]
+
+ if len(ret) != 0x5ad7:
+ print(f"ERROR: ({iset}, {bk}, {pc}) messed up: {len(ret):x}!!!")
+ return ret
+
+def write_bank(fn, b):
+ if len(b) != 0x2d6b80:
+ print(f"ERROR: Invalid bank file: {len(b):x}")
+ return
+ with open(fn, "wb") as f:
+ f.write(header)
+ f.write(b)
+
+if __name__ == "__main__":
+ try:
+ os.mkdir("data/srxsdpresets")
+ except Exception: pass
+ try:
+ os.mkdir("data/srxsdpresets/by_order")
+ except Exception: pass
+ try:
+ os.mkdir("data/srxsdpresets/by_set")
+ except Exception: pass
+
+ if len(sys.argv) > 1:
+ if int(sys.argv[1]) >= 2 and int(sys.argv[1]) <= 12:
+ header = headers[int(sys.argv[1])]
+
+ gm2pcb = []
+ gm2rhy = []
+ with open("data/gm2pcbanks", "r") as f:
+ gm2pcb = [int(x.strip(), 16) for x in f.readline().split(',')]
+ gm2rhy = [int(x.strip()) for x in f.readline().split(',')]
+ with open("data/srx_vsti_preset_template", "rb") as f:
+ template = f.read()
+
+ cur_prs = [bytearray() for i in range(0, 10)]
+ for iset in inst_sets:
+ patchdata = [bytearray(), bytearray()]
+ c = 0
+ for pc in range(0, 128):
+ for bk in range(0, 1 if iset[0] < 96 else gm2pcb[pc]):
+ fn = glob(f"data/patchparam/{iset[1]}/{bk:02d}-{pc:03d}-*.memoryregion")[0]
+ load_memregion(fn)
+ p = convert_patch(iset[0], bk, pc)
+ patchdata[int(bk > 0)] += p
+ if iset[0] < 96:
+ cur_prs[iset[0] - 80 + 8] += p
+ else:
+ cur_prs[c // 32] += p
+ c += 1
+ for bank in range(0, 1 + int(iset[0] >= 96)):
+ typ = "variation" if bank else "capital"
+ fn = f"data/srxsdpresets/by_set/{iset[1]}_{typ}.bin"
+ if iset[0] < 96:
+ fn = f"data/srxsdpresets/by_set/{iset[1]}.bin"
+ write_bank(fn, patchdata[bank])
+ for i, b in enumerate(cur_prs):
+ write_bank(f"data/srxsdpresets/by_order/PR-{chr(ord('A') + i)}.bin", b)
+
+ rhy_prs = bytearray()
+ for rset in rhyt_sets:
+ rhydata = bytearray()
+ for pc in range(0, 128):
+ if pc in gm2rhy:
+ fn = glob(f"data/rhythmparam/{rset[1]}/{pc:02d}-*.memoryregion")[0]
+ load_memregion(fn)
+ p = convert_rhythm(rset[0], pc)
+ rhydata += p
+ rhy_prs += p
+ else: rhydata += template
+ rhy_prs += 23 * template
+ fn = f"data/srxsdpresets/by_set/{rset[1]}_rhythm.bin"
+ write_bank(fn, rhydata)
+ write_bank(f"data/srxsdpresets/by_order/PR-K.bin", rhy_prs)
diff --git a/test_amap.py b/test_amap.py
new file mode 100644
index 0000000..1d028ce
--- /dev/null
+++ b/test_amap.py
@@ -0,0 +1,9 @@
+# Part of the Fifteen-Thieves Project
+# Chris Xiong 2020
+# License: Expat (MIT)
+
+import xv.amap
+
+m = xv.amap.AddrMapTemplate("amaps/sd-80.amap")
+t = xv.amap.create_addr_map_tree(m)
+t.dump()
diff --git a/wave_stats.py b/wave_stats.py
new file mode 100644
index 0000000..26dd2dc
--- /dev/null
+++ b/wave_stats.py
@@ -0,0 +1,64 @@
+# Part of the Fifteen-Thieves Project
+# Chris Xiong 2020
+# License: Expat (MIT)
+import sys
+import os
+import xv.util
+import xv.amap
+
+wavecnt = [0] * 1024
+wavenames = []
+
+def process_preset(t, is_rhy=False):
+ if is_rhy:
+ for rkey in range(0, 85):
+ for rtone in range(0, 4):
+ if t.value(f"sd_root.part[0].rhythm.rtone[{rkey}].rt_wmt[{rtone}].switch"):
+ try:
+ wavecnt[t.value(f"sd_root.part[0].rhythm.rtone[{rkey}].rt_wmt[{rtone}].wave_number_l")] += 1
+ wavecnt[t.value(f"sd_root.part[0].rhythm.rtone[{rkey}].rt_wmt[{rtone}].wave_number_r")] += 1
+ except IndexError:
+ pass
+ else:
+ for tone in range(0, 4):
+ if t.value(f"sd_root.part[0].patch.tmt.tmt_block[{tone}].tone_switch"):
+ try:
+ wavecnt[t.value(f"sd_root.part[0].patch.tone[{tone}].tn_wave.wave_number_l")] += 1
+ wavecnt[t.value(f"sd_root.part[0].patch.tone[{tone}].tn_wave.wave_number_r")] += 1
+ except IndexError:
+ pass
+
+if __name__ == "__main__":
+ print("loading address mapping...", flush=True, file=sys.stderr)
+ m = xv.amap.AddrMapTemplate("amaps/sd-80.amap")
+ t = xv.amap.create_addr_map_tree(m)
+
+ print("loading wave names...", file=sys.stderr)
+ with open("data/sdwavelist","r") as f:
+ for l in f:
+ wavenames += [l.split('\t')[1].strip()]
+
+ inst_dir = ["./data/patchparam/classical/", "./data/patchparam/contemporary/", "./data/patchparam/solo/"]
+ #inst_dir += ["./data/patchparam/enhanced/", "./data/patchparam/special_1/", "./data/patchparam/special_2/"]
+
+ rhy_dir = ["./data/rhythmparam/classical/", "./data/rhythmparam/contemporary/", "./data/rhythmparam/solo/"]
+ #rhy_dir += ["./data/rhythmparam/enhanced/"]
+
+ for i in inst_dir:
+ for fn in os.listdir(i):
+ mr = xv.util.load_memoryregion(os.path.join(i, fn))
+ for addr, data in mr:
+ ra = xv.util.b7belist2int(addr)
+ t.data[ra:ra + len(data)] = data
+ process_preset(t)
+
+ for i in rhy_dir:
+ for fn in os.listdir(i):
+ mr = xv.util.load_memoryregion(os.path.join(i, fn))
+ for addr, data in mr:
+ ra = xv.util.b7belist2int(addr)
+ t.data[ra:ra + len(data)] = data
+ process_preset(t, True)
+
+ for i, w in enumerate(wavenames):
+ print(f"{w.ljust(16)}{wavecnt[i + 1]}")
diff --git a/xv/__init__.py b/xv/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xv/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xv/amap.py b/xv/amap.py
new file mode 100644
index 0000000..4ad7ff5
--- /dev/null
+++ b/xv/amap.py
@@ -0,0 +1,265 @@
+# Part of the Fifteen-Thieves Project
+# Chris Xiong 2020
+# License: Expat (MIT)
+
+import re
+from collections import namedtuple
+import xv.util as u
+
+LeafNode = namedtuple("LeafNode", ["offset", "name", "size", "elements", "stride"])
+
+def strsize2bytesize(s):
+ if s.startswith('0'):
+ return 1
+ else:
+ return int(s[1:])
+
+def get_leaf_data(data, leaf):
+ if leaf.size.startswith('0'):
+ return data[leaf.offset]
+ else:
+ ret = 0
+ for i in range(0, int(leaf.size[1:])):
+ ret <<= 4
+ ret |= data[leaf.offset + i] & 0xf
+ return ret
+
+def set_leaf_data(data, leaf, value):
+ sz = int(leaf.size[1:])
+ if leaf.size.startswith('0'):
+ if value >= (1 << sz):
+ raise ValueError("Value overflow")
+ data[leaf.offset] = value
+ else:
+ if value >= (1 << (sz * 4)):
+ raise ValueError("Value overflow")
+ for i in range(0, sz):
+ data[leaf.offset + sz - 1 - i] = value & 0xf
+ value >>= 4
+
+class AddrMapTemplateNode:
+ def __init__(self, lines, is_root=False):
+ self.valid = True
+ self.name = lines[0].strip()
+ self.is_root = is_root
+ self.is_aggregate_node = False
+ self.children_nodes = []
+ self.children_names = dict()
+ if self.name.endswith('*'):
+ self.is_aggregate_node = True
+ self.name = self.name[:-1]
+ for line in lines[1:]:
+ el = line.split(',')
+ if not 2 <= len(el) <= 3:
+ self.valid = False
+ break
+ offset = u.hexrep2b7val(int(el[0].strip(), 16))
+ size = None if len(el) == 2 else el[1].strip()
+ m = re.match("([a-z_0-9]*)(\[([0-9]*)(\+[0-9a-f]*)?\])?", el[-1].strip())
+ if not m:
+ self.valid = False
+ break
+ name = m.groups()[0]
+ elements = None if not m.groups()[2] else int(m.groups()[2])
+ stride = None if not m.groups()[-1] else u.hexrep2b7val(int(m.groups()[-1][1:], 16))
+
+ child = LeafNode(offset, name, size, elements, stride)
+ self.children_names[name] = len(self.children_nodes)
+ self.children_nodes.append(child)
+
+class AddrMapTemplate:
+ def __init__(self, amapfile=None):
+ self.root_node_name = None
+ self.nodes = dict()
+ self.parse(amapfile)
+ if not self.verify():
+ raise ValueError("invalid address map file")
+
+ def parse(self,amapfile=None):
+ linebuf = []
+ with open(amapfile, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line[:line.find('#')]
+ if len(line.strip()) == 0:
+ continue
+ if re.match("^[a-z]", line):
+ if len(linebuf) > 0:
+ newnode = AddrMapTemplateNode(linebuf, len(self.nodes) == 0)
+ if newnode.valid:
+ self.nodes[newnode.name] = newnode
+ if newnode.is_root:
+ self.root_node_name = newnode.name
+ linebuf = [line]
+ else:
+ linebuf.append(line)
+ if len(linebuf) > 0:
+ newnode = AddrMapTemplateNode(linebuf)
+ if newnode.valid:
+ self.nodes[newnode.name] = newnode
+
+ def verify(self):
+ for nodename, node in self.nodes.items():
+ if not node.is_aggregate_node:
+ continue
+ for child in node.children_nodes:
+ if not child.name in self.nodes:
+ print(f"referencing undefined node {child.name} in {node.name}")
+ return False
+ return True
+
+class AddrMapNode:
+ def __init__(self, template_node, base_address, data, template):
+ self.template = template_node
+ self.base_address = base_address
+ self.upper_address = base_address
+ self.children_nodes = []
+ self.data = data
+ self._populate(template)
+
+ def _populate(self, template):
+ for t in self.template.children_nodes:
+ if self.template.is_aggregate_node:
+ ba = self.base_address + t.offset
+ if t.elements:
+ stride = t.stride if t.stride else 1
+ l = []
+ for i in range(0, t.elements):
+ l.append(AddrMapNode(template.nodes[t.name], ba, self.data, template))
+ ba += stride
+ self.children_nodes.append(l)
+ if l[-1].upper_address > self.upper_address:
+ self.upper_address = l[-1].upper_address
+ else:
+ self.children_nodes.append(AddrMapNode(template.nodes[t.name], ba, self.data, template))
+ if self.children_nodes[-1].upper_address > self.upper_address:
+ self.upper_address = self.children_nodes[-1].upper_address
+ else:
+ ba = self.base_address + t.offset
+ if t.elements:
+ stride = t.stride if t.stride else 1
+ l = []
+ for i in range(0, t.elements):
+ cn = LeafNode(ba, t.name, t.size, t.elements, t.stride)
+ l.append(cn)
+ ba += stride
+ self.children_nodes.append(l)
+ if ba - stride + strsize2bytesize(t.size) > self.upper_address:
+ self.upper_address = ba - stride + strsize2bytesize(t.size)
+ else:
+ cn = LeafNode(ba, t.name, t.size, t.elements, t.stride)
+ self.children_nodes.append(cn)
+ if ba + strsize2bytesize(t.size) > self.upper_address:
+ self.upper_address = ba + strsize2bytesize(t.size)
+
+ def dump(self, level=0, instance=None):
+ ba_rep = ''.join([format(x, "02x") for x in u.padbelist(u.int2b7belist(self.base_address))])
+ print(f"{ba_rep}{level*' '} {self.template.name}{'['+str(instance)+']' if instance is not None else ''}")
+ for child in self.children_nodes:
+ if self.template.is_aggregate_node:
+ if type(child) == list:
+ for i, element in enumerate(child):
+ element.dump(level + 1, i)
+ else:
+ child.dump(level + 1)
+ else:
+ if type(child) == list:
+ for i, element in enumerate(child):
+ ba_rep = ''.join([format(x, "02x") for x in u.padbelist(u.int2b7belist(element.offset))])
+ print(f"{ba_rep}{(level + 1)*' '} {element.name}[{i}] {element.size}")
+ else:
+ ba_rep = ''.join([format(x, "02x") for x in u.padbelist(u.int2b7belist(child.offset))])
+ print(f"{ba_rep}{(level + 1)*' '} {child.name} {child.size}")
+
+ def set_data(self, data):
+ if len(data) > self.size():
+ raise ValueError("excessive data")
+ self.data[self.base_address:self.base_address + len(data)] = data
+
+ def get_data(self):
+ return self.data[self.base_address:self.upper_address + 1]
+
+ def size(self):
+ return self.upper_address - self.base_address + 1
+
+ def find_node(self, key):
+ if type(key) != list:
+ key = key.split('.')
+ if len(key) == 0:
+ raise ValueError("???")
+ if key[0] == self.template.name:
+ key = key[1:]
+ if len(key) == 0:
+ return self
+ m = re.match("([a-z_0-9]*)(\[([0-9]*)\])?", key[0])
+ if not m:
+ raise ValueError(f"Invalid key: {key[0]}")
+ if m.groups()[0] not in self.template.children_names:
+ raise ValueError(f"No such region: {m.groups()[0]}")
+ child = self.children_nodes[self.template.children_names[m.groups()[0]]]
+ if m.groups()[2] and type(child) != list:
+ raise ValueError(f"{m.groups()[0]} is not a list")
+ if not m.groups()[2] and type(child) == list:
+ if len(key) > 1:
+ raise ValueError(f"index needed for {m.groups()[0]}")
+ if not self.template.is_aggregate_node and len(key) > 1:
+ raise ValueError(f"{key[0]} has no more subregions")
+ if type(child) == list:
+ if len(key) > 1:
+ return child[int(m.groups()[2])].find_node(key[1:])
+ else:
+ return child if not m.groups()[2] else child[int(m.groups()[2])]
+ else:
+ if len(key) > 1:
+ return child.find_node(key[1:])
+ else:
+ return child
+
+ def value(self, key):
+ node = self.find_node(key)
+ if type(node) == list:
+ if type(node[0]) == AddrMapNode:
+ return node.data[node[0].base_address:node[-1].upper_address + 1]
+ else:
+ stride = node[0].stride if node[0].stride else 1
+ return self.data[node[0].offset:node[0].offset + len(node) * stride:stride]
+ else:
+ if type(node) == AddrMapNode:
+ return node.data[node.base_address:node.upper_address + 1]
+ else:
+ return get_leaf_data(self.data, node)
+
+ def set_value(self, key, data):
+ node = self.find_node(key)
+ if type(node) == list:
+ if type(node[0]) == AddrMapNode:
+ if len(data) > node[-1].upper_address + 1 - node[0].base_address:
+ raise ValueError("excessive data")
+ node.data[node[0].base_address:node[0].base_address + len(data)] = data
+ else:
+ stride = node[0].stride if node[0].stride else 1
+ if len(data) > len(self.data[node[0].offset:node[0].offset + len(node) * stride:stride]):
+ raise ValueError("excessive data")
+ self.data[node[0].offset:node[0].offset + len(node) * stride:stride] = data
+ else:
+ if type(node) == AddrMapNode:
+ if len(data) > node.upper_address + 1 - node.base_address:
+ raise ValueError("excessive data")
+ node.data[node.base_address:node.base_address + len(data)] = data
+ else:
+ return set_leaf_data(self.data, node, data)
+
+ def __getitem__(self, key):
+ return self.children_nodes[self.template.children_names[key]]
+
+ def __iter__(self):
+ return self.children_nodes.__iter__()
+
+ def __len__(self):
+ return len(self.children_nodes)
+
+def create_addr_map_tree(template):
+ r = AddrMapNode(template.nodes[template.root_node_name], 0, bytearray(), template)
+ r.data.extend(bytearray(r.upper_address))
+ return r
+
+# vim: expandtab shiftwidth=4 tabstop=4
diff --git a/xv/model.py b/xv/model.py
new file mode 100644
index 0000000..0c0c014
--- /dev/null
+++ b/xv/model.py
@@ -0,0 +1,140 @@
+# Part of the Fifteen-Thieves Project
+# Chris Xiong 2020
+# License: Expat (MIT)
+
+import rtmidi.midiutil as r
+import xv.util as u
+import xv.amap
+import threading
+
+class Model:
+ def __init__(self, port_name=None, model=[0x00, 0x00], devn=0x10, address_mapping=None):
+ self.midii,_ = r.open_midiinput(port_name)
+ self.midio,_ = r.open_midioutput(port_name)
+ self.midii.ignore_types(sysex=False)
+ self.model = model
+ self.devn = devn
+ self.aroot = address_mapping
+
+ def __del__(self):
+ del self.midii
+ del self.midio
+
+ def writeraw(self, addr, data):
+ if type(addr) == int:
+ addr = u.int2belist(addr)
+ addr = u.padbelist(addr)
+ if len(addr) > 4:
+ raise ValueError("invalid address")
+
+ for a in addr:
+ if a > 0x80 or a < 0:
+ raise ValueError("invalid address")
+
+ cs = u.roland_checksum(addr + data)
+ d = [0xf0, 0x41, self.devn] + self.model + [0x12] + addr + data + [cs, 0xf7]
+ #print(' '.join([format(a, "02x") for a in d]))
+
+ self.midio.send_message(d)
+
+ def readraw(self, addr, size, timeout=None):
+ if type(addr) == int:
+ addr = u.int2belist(addr)
+ addr = u.padbelist(addr)
+ if len(addr) > 4:
+ raise ValueError("invalid address")
+ if type(size) == int:
+ size = u.padbelist(u.int2b7belist(size))
+ rsize = u.b7belist2int(size)
+
+ for a in addr:
+ if a > 0x80 or a < 0:
+ raise ValueError("invalid address")
+
+ cs = u.roland_checksum(addr + size)
+ d = [0xf0, 0x41, self.devn] + self.model + [0x11] + addr + size + [cs, 0xf7]
+ #print(' '.join([format(a, "02x") for a in d]))
+
+ cv = threading.Condition()
+ m = []
+ def msgcb(d, cd):
+ _m, dt = d
+ nonlocal m, cv, addr
+ is_last = False
+
+ if u.check_roland_checksum(_m):
+ m.append( (_m[6:10], _m[10:-2]) )
+ if u.b7belist2int(_m[6:10]) - u.b7belist2int(addr) + len(_m[10:-2]) >= rsize:
+ is_last = True
+ else:
+ raise RuntimeError("checksum error")
+
+ if is_last:
+ cv.acquire()
+ cv.notify()
+ cv.release()
+
+ self.midii.set_callback(msgcb)
+ cv.acquire()
+ self.midio.send_message(d)
+ cv.wait(timeout)
+ cv.release()
+ self.midii.cancel_callback()
+ return m
+
+ #current implementation doesn't support reading entire strided list of primitives
+ def read_locations(self, key, timeout=None):
+ node = self.aroot.find_node(key)
+ rqsz = 0
+ rqba = 0
+ if type(node) == list:
+ if type(node[0]) == xv.amap.AddrMapNode:
+ rqsz = node[-1].upper_address + 1 - node[0].base_address
+ rqba = node[0].base_address
+ else:
+ rqsz = node[-1].offset - node[0].offset + xv.amap.strsize2bytesize(node[-1].size) + 1
+ rqba = node[0].offset
+ else:
+ if type(node) == xv.amap.AddrMapNode:
+ rqba = node.base_address
+ rqsz = node.upper_address + 1 - node.base_address
+ else:
+ rqba = node.offset
+ rqsz = xv.amap.strsize2bytesize(node.size)
+ rqba = u.padbelist(u.int2b7belist(rqba))
+ r = self.readraw(rqba, rqsz, timeout)
+ for m in r:
+ mba = u.b7belist2int(m[0])
+ self.aroot.data[mba:mba + len(m[1])] = m[1]
+ return r
+
+ #current implementation doesn't support writing entire strided list of primitives
+ def write_locations(self, key, data):
+ node = self.aroot.find_node(key)
+ dstsz = 0
+ dstba = 0
+ if type(node) == list:
+ if type(node[0]) == xv.amap.AddrMapNode:
+ dstsz = node[-1].upper_address + 1 - node[0].base_address
+ dstba = node[0].base_address
+ else:
+ dstsz = node[-1].offset - node[0].offset + xv.amap.strsize2bytesize(node[-1].size) + 1
+ dstba = node[0].offset
+ else:
+ if type(node) == xv.amap.AddrMapNode:
+ dstba = node.base_address
+ dstsz = node.upper_address + 1 - node.base_address
+ else:
+ dstba = node.offset
+ dstsz = xv.amap.strsize2bytesize(node.size)
+ if type(data) != list:
+ self.aroot.set_value(key, data)
+ data = list(self.aroot.data[node.offset:node.offset + dstsz])
+ if len(data) > dstsz:
+ raise ValueError("excessive data")
+ self.aroot.data[dstba:dstba + len(data)] = data
+
+ dstba = u.padbelist(u.int2b7belist(dstba))
+ self.writeraw(dstba, data)
+
+# vim: expandtab shiftwidth=4 tabstop=4
diff --git a/xv/util.py b/xv/util.py
new file mode 100644
index 0000000..0b3f2e6
--- /dev/null
+++ b/xv/util.py
@@ -0,0 +1,90 @@
+# Part of the Fifteen-Thieves Project
+# Chris Xiong 2020
+# License: Expat (MIT)
+
+def roland_checksum(payload):
+ return (0x80 - sum(payload) % 0x80) % 0x80
+
+def check_roland_checksum(msg):
+ return (0x80 - sum(msg[6:-2]) % 0x80) % 0x80 == msg[-2]
+
+def int2belist(x):
+ ret = []
+ while x > 0:
+ ret = [x & 0xff] + ret
+ x >>= 8
+ return ret
+
+def belist2int(x):
+ ret = 0
+ for i in x:
+ ret <<= 8
+ ret |= i
+ return ret
+
+def int2b7belist(x):
+ ret = []
+ while x > 0:
+ ret = [x & 0x7f] + ret
+ x >>= 7
+ return ret
+
+def strb7list(x):
+ return ' '.join([format(a, "02x") for a in x])
+
+def b7belist2int(x):
+ ret = 0
+ for i in x:
+ ret <<= 7
+ ret |= i
+ return ret
+
+def hexrep2b7val(x):
+ ret = 0
+ b = 0
+ while x > 0:
+ if x & 0xff > 0x7f:
+ raise ValueError("invalid value")
+ ret |= (x & 0xff) << (7 * b)
+ b += 1
+ x >>= 8
+ return ret
+
+def padbelist(x, l=4):
+ return ([0]*(l-len(x))) + x
+
+def int2packetint(x):
+ ret = []
+ while x > 0:
+ ret = [x & 0x0f] + ret
+ x >>= 4
+ return ret
+
+def packetint2int(x):
+ ret = 0
+ for i in x:
+ ret <<= 4
+ ret |= i
+ return ret
+
+def load_memoryregion(fn):
+ ret = []
+ with open(fn, "rb") as f:
+ while True:
+ addr = list(f.read(4))
+ if len(addr) < 4:
+ break
+ l = belist2int(list(f.read(4)))
+ data = list(f.read(l))
+ ret.append( (addr, data) )
+ return ret
+
+def write_memoryregion(fn, mr):
+ with open(fn, "wb") as f:
+ for addr, data in mr:
+ l = padbelist(int2belist(len(data)))
+ f.write(bytearray(addr))
+ f.write(bytearray(l))
+ f.write(bytearray(data))
+
+# vim: expandtab shiftwidth=4 tabstop=4