diff --git a/assets/xml/audio/sequences/seq_0.xml b/assets/xml/audio/sequences/seq_0.xml new file mode 100644 index 0000000000..d2050652c6 --- /dev/null +++ b/assets/xml/audio/sequences/seq_0.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_1.xml b/assets/xml/audio/sequences/seq_1.xml new file mode 100644 index 0000000000..668006ad5e --- /dev/null +++ b/assets/xml/audio/sequences/seq_1.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_10.xml b/assets/xml/audio/sequences/seq_10.xml new file mode 100644 index 0000000000..7a7d5454a5 --- /dev/null +++ b/assets/xml/audio/sequences/seq_10.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_100.xml b/assets/xml/audio/sequences/seq_100.xml new file mode 100644 index 0000000000..554641913d --- /dev/null +++ b/assets/xml/audio/sequences/seq_100.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_101.xml b/assets/xml/audio/sequences/seq_101.xml new file mode 100644 index 0000000000..a8dc3db486 --- /dev/null +++ b/assets/xml/audio/sequences/seq_101.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_102.xml b/assets/xml/audio/sequences/seq_102.xml new file mode 100644 index 0000000000..264a9fb3b8 --- /dev/null +++ b/assets/xml/audio/sequences/seq_102.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_103.xml b/assets/xml/audio/sequences/seq_103.xml new file mode 100644 index 0000000000..27b66e955b --- /dev/null +++ b/assets/xml/audio/sequences/seq_103.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_104.xml b/assets/xml/audio/sequences/seq_104.xml new file mode 100644 index 0000000000..c0560a0fd3 --- /dev/null +++ b/assets/xml/audio/sequences/seq_104.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_105.xml b/assets/xml/audio/sequences/seq_105.xml new file mode 100644 index 0000000000..11f30d8f76 --- /dev/null +++ b/assets/xml/audio/sequences/seq_105.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_106.xml b/assets/xml/audio/sequences/seq_106.xml new file mode 100644 index 0000000000..3efe9de557 --- /dev/null +++ b/assets/xml/audio/sequences/seq_106.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_107.xml b/assets/xml/audio/sequences/seq_107.xml new file mode 100644 index 0000000000..1b37819a3e --- /dev/null +++ b/assets/xml/audio/sequences/seq_107.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_108.xml b/assets/xml/audio/sequences/seq_108.xml new file mode 100644 index 0000000000..d02cc02095 --- /dev/null +++ b/assets/xml/audio/sequences/seq_108.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_109.xml b/assets/xml/audio/sequences/seq_109.xml new file mode 100644 index 0000000000..27064158e7 --- /dev/null +++ b/assets/xml/audio/sequences/seq_109.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_11.xml b/assets/xml/audio/sequences/seq_11.xml new file mode 100644 index 0000000000..233917f2a0 --- /dev/null +++ b/assets/xml/audio/sequences/seq_11.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_12.xml b/assets/xml/audio/sequences/seq_12.xml new file mode 100644 index 0000000000..de48f97302 --- /dev/null +++ b/assets/xml/audio/sequences/seq_12.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_13.xml b/assets/xml/audio/sequences/seq_13.xml new file mode 100644 index 0000000000..048b3efda6 --- /dev/null +++ b/assets/xml/audio/sequences/seq_13.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_14.xml b/assets/xml/audio/sequences/seq_14.xml new file mode 100644 index 0000000000..79d3238c61 --- /dev/null +++ b/assets/xml/audio/sequences/seq_14.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_15.xml b/assets/xml/audio/sequences/seq_15.xml new file mode 100644 index 0000000000..c54018295e --- /dev/null +++ b/assets/xml/audio/sequences/seq_15.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_16.xml b/assets/xml/audio/sequences/seq_16.xml new file mode 100644 index 0000000000..5267d04126 --- /dev/null +++ b/assets/xml/audio/sequences/seq_16.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_17.xml b/assets/xml/audio/sequences/seq_17.xml new file mode 100644 index 0000000000..0d6d306b3b --- /dev/null +++ b/assets/xml/audio/sequences/seq_17.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_18.xml b/assets/xml/audio/sequences/seq_18.xml new file mode 100644 index 0000000000..af445ff0dc --- /dev/null +++ b/assets/xml/audio/sequences/seq_18.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_19.xml b/assets/xml/audio/sequences/seq_19.xml new file mode 100644 index 0000000000..000b84c2c6 --- /dev/null +++ b/assets/xml/audio/sequences/seq_19.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_2.xml b/assets/xml/audio/sequences/seq_2.xml new file mode 100644 index 0000000000..35b67cbddb --- /dev/null +++ b/assets/xml/audio/sequences/seq_2.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_20.xml b/assets/xml/audio/sequences/seq_20.xml new file mode 100644 index 0000000000..35824b77fd --- /dev/null +++ b/assets/xml/audio/sequences/seq_20.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_21.xml b/assets/xml/audio/sequences/seq_21.xml new file mode 100644 index 0000000000..c4ab27602f --- /dev/null +++ b/assets/xml/audio/sequences/seq_21.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_22.xml b/assets/xml/audio/sequences/seq_22.xml new file mode 100644 index 0000000000..cac61f1e64 --- /dev/null +++ b/assets/xml/audio/sequences/seq_22.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_23.xml b/assets/xml/audio/sequences/seq_23.xml new file mode 100644 index 0000000000..ac69e86045 --- /dev/null +++ b/assets/xml/audio/sequences/seq_23.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_24.xml b/assets/xml/audio/sequences/seq_24.xml new file mode 100644 index 0000000000..2d3386a35d --- /dev/null +++ b/assets/xml/audio/sequences/seq_24.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_25.xml b/assets/xml/audio/sequences/seq_25.xml new file mode 100644 index 0000000000..ef233be23d --- /dev/null +++ b/assets/xml/audio/sequences/seq_25.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_26.xml b/assets/xml/audio/sequences/seq_26.xml new file mode 100644 index 0000000000..590a5165f4 --- /dev/null +++ b/assets/xml/audio/sequences/seq_26.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_27.xml b/assets/xml/audio/sequences/seq_27.xml new file mode 100644 index 0000000000..5d8ff12348 --- /dev/null +++ b/assets/xml/audio/sequences/seq_27.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_28.xml b/assets/xml/audio/sequences/seq_28.xml new file mode 100644 index 0000000000..4520532db0 --- /dev/null +++ b/assets/xml/audio/sequences/seq_28.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_29.xml b/assets/xml/audio/sequences/seq_29.xml new file mode 100644 index 0000000000..2ca7f3e712 --- /dev/null +++ b/assets/xml/audio/sequences/seq_29.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_3.xml b/assets/xml/audio/sequences/seq_3.xml new file mode 100644 index 0000000000..8aa3b1ff0f --- /dev/null +++ b/assets/xml/audio/sequences/seq_3.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_30.xml b/assets/xml/audio/sequences/seq_30.xml new file mode 100644 index 0000000000..a3dead9ba4 --- /dev/null +++ b/assets/xml/audio/sequences/seq_30.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_31.xml b/assets/xml/audio/sequences/seq_31.xml new file mode 100644 index 0000000000..9a1cfe9f27 --- /dev/null +++ b/assets/xml/audio/sequences/seq_31.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_32.xml b/assets/xml/audio/sequences/seq_32.xml new file mode 100644 index 0000000000..bb76497a80 --- /dev/null +++ b/assets/xml/audio/sequences/seq_32.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_33.xml b/assets/xml/audio/sequences/seq_33.xml new file mode 100644 index 0000000000..15bc25a25c --- /dev/null +++ b/assets/xml/audio/sequences/seq_33.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_34.xml b/assets/xml/audio/sequences/seq_34.xml new file mode 100644 index 0000000000..4d21886431 --- /dev/null +++ b/assets/xml/audio/sequences/seq_34.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_35.xml b/assets/xml/audio/sequences/seq_35.xml new file mode 100644 index 0000000000..4d1e7327e4 --- /dev/null +++ b/assets/xml/audio/sequences/seq_35.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_36.xml b/assets/xml/audio/sequences/seq_36.xml new file mode 100644 index 0000000000..63173ecbb4 --- /dev/null +++ b/assets/xml/audio/sequences/seq_36.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_37.xml b/assets/xml/audio/sequences/seq_37.xml new file mode 100644 index 0000000000..bd6813f620 --- /dev/null +++ b/assets/xml/audio/sequences/seq_37.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_38.xml b/assets/xml/audio/sequences/seq_38.xml new file mode 100644 index 0000000000..dc85ce3953 --- /dev/null +++ b/assets/xml/audio/sequences/seq_38.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_39.xml b/assets/xml/audio/sequences/seq_39.xml new file mode 100644 index 0000000000..5cef308792 --- /dev/null +++ b/assets/xml/audio/sequences/seq_39.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_4.xml b/assets/xml/audio/sequences/seq_4.xml new file mode 100644 index 0000000000..6311553183 --- /dev/null +++ b/assets/xml/audio/sequences/seq_4.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_40.xml b/assets/xml/audio/sequences/seq_40.xml new file mode 100644 index 0000000000..c58c83c2a8 --- /dev/null +++ b/assets/xml/audio/sequences/seq_40.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_41.xml b/assets/xml/audio/sequences/seq_41.xml new file mode 100644 index 0000000000..a74ce40600 --- /dev/null +++ b/assets/xml/audio/sequences/seq_41.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_42.xml b/assets/xml/audio/sequences/seq_42.xml new file mode 100644 index 0000000000..3298f00c4d --- /dev/null +++ b/assets/xml/audio/sequences/seq_42.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_43.xml b/assets/xml/audio/sequences/seq_43.xml new file mode 100644 index 0000000000..9e0d2f9280 --- /dev/null +++ b/assets/xml/audio/sequences/seq_43.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_44.xml b/assets/xml/audio/sequences/seq_44.xml new file mode 100644 index 0000000000..7f4efdd764 --- /dev/null +++ b/assets/xml/audio/sequences/seq_44.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_45.xml b/assets/xml/audio/sequences/seq_45.xml new file mode 100644 index 0000000000..1f23ccd5fa --- /dev/null +++ b/assets/xml/audio/sequences/seq_45.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_46.xml b/assets/xml/audio/sequences/seq_46.xml new file mode 100644 index 0000000000..af7340d3d5 --- /dev/null +++ b/assets/xml/audio/sequences/seq_46.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_47.xml b/assets/xml/audio/sequences/seq_47.xml new file mode 100644 index 0000000000..7b9b46489a --- /dev/null +++ b/assets/xml/audio/sequences/seq_47.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_48.xml b/assets/xml/audio/sequences/seq_48.xml new file mode 100644 index 0000000000..0cd313d2bd --- /dev/null +++ b/assets/xml/audio/sequences/seq_48.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_49.xml b/assets/xml/audio/sequences/seq_49.xml new file mode 100644 index 0000000000..c5aa4bbb6a --- /dev/null +++ b/assets/xml/audio/sequences/seq_49.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_5.xml b/assets/xml/audio/sequences/seq_5.xml new file mode 100644 index 0000000000..a1a398f463 --- /dev/null +++ b/assets/xml/audio/sequences/seq_5.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_50.xml b/assets/xml/audio/sequences/seq_50.xml new file mode 100644 index 0000000000..e2e3adcd26 --- /dev/null +++ b/assets/xml/audio/sequences/seq_50.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_51.xml b/assets/xml/audio/sequences/seq_51.xml new file mode 100644 index 0000000000..2b6ba4fcee --- /dev/null +++ b/assets/xml/audio/sequences/seq_51.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_52.xml b/assets/xml/audio/sequences/seq_52.xml new file mode 100644 index 0000000000..17f6f8ea8f --- /dev/null +++ b/assets/xml/audio/sequences/seq_52.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_53.xml b/assets/xml/audio/sequences/seq_53.xml new file mode 100644 index 0000000000..e9f0a82eae --- /dev/null +++ b/assets/xml/audio/sequences/seq_53.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_54.xml b/assets/xml/audio/sequences/seq_54.xml new file mode 100644 index 0000000000..a902858ca6 --- /dev/null +++ b/assets/xml/audio/sequences/seq_54.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_55.xml b/assets/xml/audio/sequences/seq_55.xml new file mode 100644 index 0000000000..361afcebd2 --- /dev/null +++ b/assets/xml/audio/sequences/seq_55.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_56.xml b/assets/xml/audio/sequences/seq_56.xml new file mode 100644 index 0000000000..5710f771e5 --- /dev/null +++ b/assets/xml/audio/sequences/seq_56.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_57.xml b/assets/xml/audio/sequences/seq_57.xml new file mode 100644 index 0000000000..b0f2c8573c --- /dev/null +++ b/assets/xml/audio/sequences/seq_57.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_58.xml b/assets/xml/audio/sequences/seq_58.xml new file mode 100644 index 0000000000..39cc260786 --- /dev/null +++ b/assets/xml/audio/sequences/seq_58.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_59.xml b/assets/xml/audio/sequences/seq_59.xml new file mode 100644 index 0000000000..cf7393a72e --- /dev/null +++ b/assets/xml/audio/sequences/seq_59.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_6.xml b/assets/xml/audio/sequences/seq_6.xml new file mode 100644 index 0000000000..f1d1176256 --- /dev/null +++ b/assets/xml/audio/sequences/seq_6.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_60.xml b/assets/xml/audio/sequences/seq_60.xml new file mode 100644 index 0000000000..2fdaac058f --- /dev/null +++ b/assets/xml/audio/sequences/seq_60.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_61.xml b/assets/xml/audio/sequences/seq_61.xml new file mode 100644 index 0000000000..f7add6ba57 --- /dev/null +++ b/assets/xml/audio/sequences/seq_61.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_62.xml b/assets/xml/audio/sequences/seq_62.xml new file mode 100644 index 0000000000..ca00f1bf03 --- /dev/null +++ b/assets/xml/audio/sequences/seq_62.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_63.xml b/assets/xml/audio/sequences/seq_63.xml new file mode 100644 index 0000000000..d7cb6e7fe6 --- /dev/null +++ b/assets/xml/audio/sequences/seq_63.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_64.xml b/assets/xml/audio/sequences/seq_64.xml new file mode 100644 index 0000000000..95eb98c73f --- /dev/null +++ b/assets/xml/audio/sequences/seq_64.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_65.xml b/assets/xml/audio/sequences/seq_65.xml new file mode 100644 index 0000000000..897347d02c --- /dev/null +++ b/assets/xml/audio/sequences/seq_65.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_66.xml b/assets/xml/audio/sequences/seq_66.xml new file mode 100644 index 0000000000..85f5ff5222 --- /dev/null +++ b/assets/xml/audio/sequences/seq_66.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_67.xml b/assets/xml/audio/sequences/seq_67.xml new file mode 100644 index 0000000000..36dfd9bbfb --- /dev/null +++ b/assets/xml/audio/sequences/seq_67.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_68.xml b/assets/xml/audio/sequences/seq_68.xml new file mode 100644 index 0000000000..6644e0b601 --- /dev/null +++ b/assets/xml/audio/sequences/seq_68.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_69.xml b/assets/xml/audio/sequences/seq_69.xml new file mode 100644 index 0000000000..59ee837c80 --- /dev/null +++ b/assets/xml/audio/sequences/seq_69.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_7.xml b/assets/xml/audio/sequences/seq_7.xml new file mode 100644 index 0000000000..5b099b03e8 --- /dev/null +++ b/assets/xml/audio/sequences/seq_7.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_70.xml b/assets/xml/audio/sequences/seq_70.xml new file mode 100644 index 0000000000..5e7966d811 --- /dev/null +++ b/assets/xml/audio/sequences/seq_70.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_71.xml b/assets/xml/audio/sequences/seq_71.xml new file mode 100644 index 0000000000..f0b9c5166c --- /dev/null +++ b/assets/xml/audio/sequences/seq_71.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_72.xml b/assets/xml/audio/sequences/seq_72.xml new file mode 100644 index 0000000000..f674c78d6a --- /dev/null +++ b/assets/xml/audio/sequences/seq_72.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_73.xml b/assets/xml/audio/sequences/seq_73.xml new file mode 100644 index 0000000000..2548961701 --- /dev/null +++ b/assets/xml/audio/sequences/seq_73.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_74.xml b/assets/xml/audio/sequences/seq_74.xml new file mode 100644 index 0000000000..b1da5b9923 --- /dev/null +++ b/assets/xml/audio/sequences/seq_74.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_75.xml b/assets/xml/audio/sequences/seq_75.xml new file mode 100644 index 0000000000..2bbceeb826 --- /dev/null +++ b/assets/xml/audio/sequences/seq_75.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_76.xml b/assets/xml/audio/sequences/seq_76.xml new file mode 100644 index 0000000000..368881c430 --- /dev/null +++ b/assets/xml/audio/sequences/seq_76.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_77.xml b/assets/xml/audio/sequences/seq_77.xml new file mode 100644 index 0000000000..0eed7ea92c --- /dev/null +++ b/assets/xml/audio/sequences/seq_77.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_78.xml b/assets/xml/audio/sequences/seq_78.xml new file mode 100644 index 0000000000..372b10db22 --- /dev/null +++ b/assets/xml/audio/sequences/seq_78.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_79.xml b/assets/xml/audio/sequences/seq_79.xml new file mode 100644 index 0000000000..cfd002b51b --- /dev/null +++ b/assets/xml/audio/sequences/seq_79.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_8.xml b/assets/xml/audio/sequences/seq_8.xml new file mode 100644 index 0000000000..d5b49b5d12 --- /dev/null +++ b/assets/xml/audio/sequences/seq_8.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_80.xml b/assets/xml/audio/sequences/seq_80.xml new file mode 100644 index 0000000000..2d7f51a5a6 --- /dev/null +++ b/assets/xml/audio/sequences/seq_80.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_81.xml b/assets/xml/audio/sequences/seq_81.xml new file mode 100644 index 0000000000..2534949364 --- /dev/null +++ b/assets/xml/audio/sequences/seq_81.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_82.xml b/assets/xml/audio/sequences/seq_82.xml new file mode 100644 index 0000000000..98d1bff9b7 --- /dev/null +++ b/assets/xml/audio/sequences/seq_82.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_83.xml b/assets/xml/audio/sequences/seq_83.xml new file mode 100644 index 0000000000..e44b8d6ff5 --- /dev/null +++ b/assets/xml/audio/sequences/seq_83.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_84.xml b/assets/xml/audio/sequences/seq_84.xml new file mode 100644 index 0000000000..55dc374a4c --- /dev/null +++ b/assets/xml/audio/sequences/seq_84.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_85.xml b/assets/xml/audio/sequences/seq_85.xml new file mode 100644 index 0000000000..1f024e0923 --- /dev/null +++ b/assets/xml/audio/sequences/seq_85.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_86.xml b/assets/xml/audio/sequences/seq_86.xml new file mode 100644 index 0000000000..f6acb8a9fe --- /dev/null +++ b/assets/xml/audio/sequences/seq_86.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_88.xml b/assets/xml/audio/sequences/seq_88.xml new file mode 100644 index 0000000000..a96383096c --- /dev/null +++ b/assets/xml/audio/sequences/seq_88.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_89.xml b/assets/xml/audio/sequences/seq_89.xml new file mode 100644 index 0000000000..62026869f8 --- /dev/null +++ b/assets/xml/audio/sequences/seq_89.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_9.xml b/assets/xml/audio/sequences/seq_9.xml new file mode 100644 index 0000000000..f6671b1e6e --- /dev/null +++ b/assets/xml/audio/sequences/seq_9.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_90.xml b/assets/xml/audio/sequences/seq_90.xml new file mode 100644 index 0000000000..1f1d9bde08 --- /dev/null +++ b/assets/xml/audio/sequences/seq_90.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_91.xml b/assets/xml/audio/sequences/seq_91.xml new file mode 100644 index 0000000000..e2bee2cb32 --- /dev/null +++ b/assets/xml/audio/sequences/seq_91.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_92.xml b/assets/xml/audio/sequences/seq_92.xml new file mode 100644 index 0000000000..3129a417d5 --- /dev/null +++ b/assets/xml/audio/sequences/seq_92.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_93.xml b/assets/xml/audio/sequences/seq_93.xml new file mode 100644 index 0000000000..0c6b204cb9 --- /dev/null +++ b/assets/xml/audio/sequences/seq_93.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_94.xml b/assets/xml/audio/sequences/seq_94.xml new file mode 100644 index 0000000000..59805445e3 --- /dev/null +++ b/assets/xml/audio/sequences/seq_94.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_95.xml b/assets/xml/audio/sequences/seq_95.xml new file mode 100644 index 0000000000..cdc31a61b1 --- /dev/null +++ b/assets/xml/audio/sequences/seq_95.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_96.xml b/assets/xml/audio/sequences/seq_96.xml new file mode 100644 index 0000000000..402fc8b8b9 --- /dev/null +++ b/assets/xml/audio/sequences/seq_96.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_97.xml b/assets/xml/audio/sequences/seq_97.xml new file mode 100644 index 0000000000..5726322125 --- /dev/null +++ b/assets/xml/audio/sequences/seq_97.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_98.xml b/assets/xml/audio/sequences/seq_98.xml new file mode 100644 index 0000000000..5bd56596b0 --- /dev/null +++ b/assets/xml/audio/sequences/seq_98.xml @@ -0,0 +1,2 @@ + + diff --git a/assets/xml/audio/sequences/seq_99.xml b/assets/xml/audio/sequences/seq_99.xml new file mode 100644 index 0000000000..d550855a0a --- /dev/null +++ b/assets/xml/audio/sequences/seq_99.xml @@ -0,0 +1,2 @@ + + diff --git a/tools/audio/extraction/audio_extract.py b/tools/audio/extraction/audio_extract.py index ea4f8612c3..05ab781072 100644 --- a/tools/audio/extraction/audio_extract.py +++ b/tools/audio/extraction/audio_extract.py @@ -6,7 +6,6 @@ import os, shutil, time from dataclasses import dataclass -from enum import auto, Enum from multiprocessing.pool import ThreadPool from typing import Dict, List, Tuple, Union from xml.etree import ElementTree @@ -15,11 +14,8 @@ from xml.etree.ElementTree import Element from .audio_tables import AudioCodeTable, AudioCodeTableEntry, AudioStorageMedium from .audiotable import AudioTableData, AudioTableFile, AudioTableSample from .audiobank_file import AudiobankFile -from .util import align, debugm, error, incbin, program_get - -class MMLVersion(Enum): - OOT = auto() - MM = auto() +from .disassemble_sequence import CMD_SPEC, SequenceDisassembler, SequenceTableSpec, MMLVersion +from .util import align, debugm, error, incbin, program_get, XMLWriter @dataclass class GameVersionInfo: @@ -41,6 +37,8 @@ class GameVersionInfo: fake_banks : Dict[int, int] # Contains audiotable indices that suffer from a buffer clearing bug audiotable_buffer_bugs : Tuple[int] + # Sequence disassembly table specs + seq_disas_tables : Dict[int, Tuple[SequenceTableSpec]] SAMPLECONV_PATH = f"{os.path.dirname(os.path.realpath(__file__))}/../sampleconv/sampleconv" @@ -176,6 +174,137 @@ def extract_samplebank(pool : ThreadPool, extracted_dir : str, sample_banks : Li if not BASEROM_DEBUG: shutil.rmtree(f"{base_path}/aifc") +def disassemble_one_sequence(extracted_dir : str, version_info : GameVersionInfo, soundfonts : List[AudiobankFile], + enum_names : List[str], id : int, data : bytes, name : str, filename : str, + fonts : memoryview): + out_filename = f"{extracted_dir}/assets/audio/sequences/{filename}.seq" + disas = SequenceDisassembler(id, data, version_info.seq_disas_tables.get(id, None), CMD_SPEC, + version_info.mml_version, out_filename, name, + [soundfonts[i] for i in fonts], enum_names) + disas.analyze() + disas.emit() + +def extract_sequences(audioseq_seg : memoryview, extracted_dir : str, version_info : GameVersionInfo, write_xml : bool, + sequence_table : AudioCodeTable, sequence_font_table : memoryview, + sequence_xmls : Dict[int, Element], soundfonts : List[AudiobankFile]): + + sequence_font_table_cvg = [0] * len(sequence_font_table) + + seq_enum_names = version_info.seq_enum_names + handwritten_sequences = version_info.handwritten_sequences + + # We should have as many enum names as sequences that require extraction + assert len(seq_enum_names) == len(sequence_table) + + if BASEROM_DEBUG: + os.makedirs(f"{extracted_dir}/baserom_audiotest/audioseq_files", exist_ok=True) + + os.makedirs(f"{extracted_dir}/assets/audio/sequences", exist_ok=True) + if write_xml: + os.makedirs(f"assets/xml/audio/sequences", exist_ok=True) + + all_fonts = [] + disas_jobs = [] + + t = time.time() + + for i,entry in enumerate(sequence_table): + entry : AudioCodeTableEntry + + # extract font indices + font_data_offset = (sequence_font_table[2 * i + 0] << 8) | (sequence_font_table[2 * i + 1]) + num_fonts = sequence_font_table[font_data_offset] + font_data_offset += 1 + fonts = sequence_font_table[font_data_offset:font_data_offset+num_fonts] + + all_fonts.append(fonts) + + # mark coverage for sequence font table + sequence_font_table_cvg[2 * i + 0] = 1 + sequence_font_table_cvg[2 * i + 1] = 1 + for j in range(font_data_offset-1,font_data_offset+num_fonts): + sequence_font_table_cvg[j] = 1 + + if entry.size != 0: + # Real sequence, queue extraction + + seq_data = bytearray(entry.data(audioseq_seg, sequence_table.rom_addr)) + + ext = ".prg" if i in handwritten_sequences else "" + + if BASEROM_DEBUG: + # Extract original sequence binary for comparison + with open(f"{extracted_dir}/baserom_audiotest/audioseq_files/seq_{i}{ext}.aseq", "wb") as outfile: + outfile.write(seq_data) + + extraction_xml = sequence_xmls.get(i, None) + if extraction_xml is None: + sequence_filename = f"seq_{i}" + sequence_name = f"Sequence_{i}" + else: + sequence_filename = extraction_xml[0] + sequence_name = extraction_xml[1].attrib["Name"] + + # Write extraction xml entry + if write_xml: + xml = XMLWriter() + + xml.write_comment("This file is only for extraction of vanilla data.") + + xml.write_element("Sequence", { + "Name" : sequence_name, + "Index" : i, + }) + + with open(f"assets/xml/audio/sequences/{sequence_filename}.xml", "w") as outfile: + outfile.write(str(xml)) + + if i in handwritten_sequences: + # skip "handwritten" sequences + continue + + disas_jobs.append((i, seq_data, sequence_name, sequence_filename, fonts)) + else: + # Pointer to another sequence, checked later + pass + + # Check full coverage + try: + if align(sequence_font_table_cvg.index(0), 16) != len(sequence_font_table_cvg): + # does not pad to full size, fail + assert False, "Sequence font table missing data" + # pads to full size, good + except ValueError: + pass # fully covered, good + + # Check consistency of font data for the same sequence accessed via pointers + + for i,entry in enumerate(sequence_table): + entry : AudioCodeTableEntry + + # Fonts for this entry + fonts = all_fonts[i] + + if entry.size != 0: + # real, ignore + pass + else: + # pointer, check that the fonts for this entry are the same as the fonts for the other + j = entry.rom_addr + + fonts2 = all_fonts[j] + + assert fonts == fonts2, \ + f"Font mismatch: Pointer {i} against Real {j}. This is a limitation of the build process." + + # Disassemble to text + + for job in disas_jobs: + disassemble_one_sequence(extracted_dir, version_info, soundfonts, seq_enum_names, *job) + + dt = time.time() - t + print(f"Sequences extraction took {dt:.3f}") + def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : str, read_xml : bool, write_xml : bool): print("Setting up...") @@ -184,6 +313,7 @@ def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : st code_seg = None audiotable_seg = None audiobank_seg = None + audioseq_seg = None with open(f"{extracted_dir}/baserom/code", "rb") as infile: code_seg = memoryview(infile.read()) @@ -194,6 +324,9 @@ def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : st with open(f"{extracted_dir}/baserom/Audiobank", "rb") as infile: audiobank_seg = memoryview(infile.read()) + with open(f"{extracted_dir}/baserom/Audioseq", "rb") as infile: + audioseq_seg = memoryview(infile.read()) + # ================================================================================================================== # Collect audio tables # ================================================================================================================== @@ -318,3 +451,12 @@ def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : st # write the extraction xml if specified if write_xml: sf.write_extraction_xml(f"assets/xml/audio/soundfonts/{sf.file_name}.xml") + + # ================================================================================================================== + # Extract sequences + # ================================================================================================================== + + print("Extracting sequences...") + + extract_sequences(audioseq_seg, extracted_dir, version_info, write_xml, sequence_table, sequence_font_table, + sequence_xmls, soundfonts) diff --git a/tools/audio/extraction/disassemble_sequence.py b/tools/audio/extraction/disassemble_sequence.py new file mode 100644 index 0000000000..af92598e0b --- /dev/null +++ b/tools/audio/extraction/disassemble_sequence.py @@ -0,0 +1,1276 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: © 2024 ZeldaRET +# SPDX-License-Identifier: CC0-1.0 +# +# Audio Sequence Disassembler +# + +""" +The approach for sequence disassembly is roughly as follows: + +``` + Set COVERAGE=[0 for _ in range(len(data))] + Set REF_QUEUE=[] + + Set OFFSET=0 + Set SECTION=SEQ +1: + Begin sequential disassembly at OFFSET using section type SECTION + Collect reference labels and section types into REF_QUEUE + Update entries in COVERAGE to 1 as bytes are read + End disassembly at `end` instruction + + If REF_QUEUE is not empty: + Pop a reference from REF_QUEUE + Set OFFSET=loc(reference) + Set SECTION=section(reference) + goto 1 + + If Any 0s in COVERAGE: + Set OFFSET=(index of first 0 in COVERAGE) + Set SECTION=guess_section(OFFSET) (make a heuristic guess for section based on neighbors) + goto 1 +``` + +There are some additional subtleties for handling padding and uncommon sections like `array`. + +For tables used in `dyncall`s, we have to rely on external information to provide the location and size of tables as +there is no reliable heuristic for identifying table sizes. + + +TODO + +sequence beginning with testchan 0 is a buffer (?) +OR any ldseq is an array and an array of 0 is a buffer (?) + +detect section overlaps and mark them as bugged in the output +""" + +from dataclasses import dataclass +from enum import Enum, auto +from typing import Callable, Dict, List, Optional, Tuple + +from .audiobank_file import AudiobankFile + +pitch_names = ( + "A0", "BF0", "B0", "C1", "DF1", "D1", "EF1", "E1", "F1", "GF1", "G1", "AF1", "A1", "BF1", "B1", "C2", + "DF2", "D2", "EF2", "E2", "F2", "GF2", "G2", "AF2", "A2", "BF2", "B2", "C3", "DF3", "D3", "EF3", "E3", + "F3", "GF3", "G3", "AF3", "A3", "BF3", "B3", "C4", "DF4", "D4", "EF4", "E4", "F4", "GF4", "G4", "AF4", + "A4", "BF4", "B4", "C5", "DF5", "D5", "EF5", "E5", "F5", "GF5", "G5", "AF5", "A5", "BF5", "B5", "C6", + "DF6", "D6", "EF6", "E6", "F6", "GF6", "G6", "AF6", "A6", "BF6", "B6", "C7", "DF7", "D7", "EF7", "E7", + "F7", "GF7", "G7", "AF7", "A7", "BF7", "B7", "C8", "DF8", "D8", "EF8", "E8", "F8", "GF8", "G8", "AF8", + "A8", "BF8", "B8", "C9", "DF9", "D9", "EF9", "E9", "F9", "GF9", "G9", "AF9", "A9", "BF9", "B9", "C10", + "DF10", "D10", "EF10", "E10", "F10", "BFNEG1", "BNEG1", "C0", "DF0", "D0", "EF0", "E0", "F0", "GF0", "G0", "AF0", +) + +# +# VERSIONS +# + +class MMLVersion(Enum): + OOT = auto() + MM = auto() + +VERSION_ALL = (MMLVersion.OOT, MMLVersion.MM) + +# +# SECTIONS +# + +class SqSection(Enum): + SEQ = ("SEQ", ".sequence") + CHAN = ("CHAN", ".channel") + LAYER = ("LAYER", ".layer") + ARRAY = ("ARRAY", ".array") + TABLE = ("TABLE", ".table") + ENVELOPE = ("ENVELOPE", ".envelope") + FILTER = ("FILTER", ".filter") + UNKNOWN = ("UNK", "") + + def __init__(self, prefix, lbl_prefix): + self.prefix = prefix + self.lbl_prefix = lbl_prefix + +SECTION_ALL = (SqSection.SEQ, SqSection.CHAN, SqSection.LAYER) + +# +# ARGS +# + +def maybe_hex(n): + if n < 10: + return f"{n}" + else: + return f"0x{n:X}" + +def sign_extend(x, n): + sgn = 1 << (n - 1) + return (x & (sgn - 1)) - (x & sgn) + +class MMLArg: + def __init__(self, disas): + self.value = self.read(disas) + + def read(self, disas): + raise NotImplementedError() + + def analyze(self, disas): + pass + + def emit(self, disas): + return str(self.value) + +class MMLArgBits(MMLArg): + def read(self, disas): + return disas.read_bits(type(self).NBITS) + +class ArgU8(MMLArg): + def read(self, disas): + return disas.read_u8() + +class ArgU4x2(MMLArg): + def read(self, disas): + return disas.read_u8() + + def emit(self, disas): + return f"{(self.value >> 4) & 0xF}, {self.value & 0xF}" + +class ArgSeqId(ArgU8): + def emit(self, disas): + return disas.all_seq_names[self.value] + +class ArgFontId(ArgU8): # TODO + def read(self, disas): + return disas.read_u8() + +class ArgPitchU8(ArgU8): + def emit(self, disas): + return f"PITCH_{pitch_names[self.value]}" + +class ArgS8(MMLArg): + def read(self, disas): + return sign_extend(disas.read_u8(), 8) + +class ArgU16(MMLArg): + def read(self, disas): + return disas.read_u16() + +class ArgS16(MMLArg): + def read(self, disas): + return sign_extend(disas.read_u16(), 16) + +class ArgHex8(ArgU8): + def emit(self, disas): + return f"0x{self.value:02X}" + +class ArgHex16(ArgU16): + def emit(self, disas): + return f"0x{self.value:04X}" + +class ArgBitField16(ArgU16): + def emit(self, disas): + return bin(self.value) + +class ArgInstr(ArgU8): + def emit(self, disas): + builtins = { + 126 : "FONTANY_INSTR_SFX", + 127 : "FONTANY_INSTR_DRUM", + 128 : "FONTANY_INSTR_SAWTOOTH", + 129 : "FONTANY_INSTR_TRIANGLE", + 130 : "FONTANY_INSTR_SINE", + 131 : "FONTANY_INSTR_SQUARE", + 132 : "FONTANY_INSTR_NOISE", + 133 : "FONTANY_INSTR_BELL", + 134 : "FONTANY_INSTR_8PULSE", + 135 : "FONTANY_INSTR_4PULSE", + 136 : "FONTANY_INSTR_ASM_NOISE", + } + if self.value in builtins: + return builtins[self.value] + + # Check against first font only, this is fine for 99% of cases since most sequences use just one font + font0 : AudiobankFile = disas.used_fonts[0] + + if self.value in font0.instrument_index_map: + name = f"SF{font0.bank_num}_{font0.instrument_name(self.value)}" + else: + print(f"Invalid instrument sourced from {font0.name}: {self.value}") + name = f"{self.value} /* invalid instrument */" + return name + +class ArgVar(MMLArg): + def read(self, disas): + ret = disas.read_u8() + if ret & 0x80: + ret = ((ret << 8) & 0x7F00) | disas.read_u8() + if ret < 128 and disas.insn_begin not in disas.force_long: + print(f"Unnecessary use of long immediate encoding @ 0x{disas.insn_begin:X}: {ret}") + disas.force_long.add(disas.insn_begin) + + return ret + +class ArgPortamentoMode(ArgHex8): + def read(self, disas): + ret = disas.read_u8() + disas.portamento_is_special = (ret & 0x80) != 0 + return ret + +class ArgStereoConfig(ArgU8): + def emit(self, disas): + assert (self.value & 0b11000000) == 0 + type = (self.value >> 4) & 0b11 + strong_right = (self.value >> 3) & 1 + strong_left = (self.value >> 2) & 1 + strong_rvrb_right = (self.value >> 1) & 1 + strong_rvrb_left = (self.value >> 0) & 1 + return f"{type}, {strong_right}, {strong_left}, {strong_rvrb_right}, {strong_rvrb_left}" + +class ArgPortamentoTime(ArgVar): + def read(self, disas): + if disas.portamento_is_special: + return disas.read_u8() + else: + return super().read(disas) + +class ArgBits3(MMLArgBits): + NBITS = 3 + +class ArgBits4(MMLArgBits): + NBITS = 4 + +class IOPort3(ArgBits3): + def emit(self, disas): + assert self.value in range(0,8) + return f"IO_PORT_{self.value}" + +class IOPort8(ArgU8): + def emit(self, disas): + if self.value in range(0,8): + return f"IO_PORT_{self.value}" + else: + return f"{self.value} # BAD IO PORT NUMBER" + +class ArgPitch(MMLArgBits): + NBITS = 6 + + def emit(self, disas): + return f"PITCH_{pitch_names[self.value]}" + +class ArgAddr(ArgHex16): + def analyze(self, disas): + disas.add_ref(self.value) + + def emit(self, disas): + value = self.value + + target_section = SqSection.UNKNOWN + for frag in disas.fragments: + if value in range(frag.start,frag.end): + target_section = frag.section + break + + addend = disas.addends.get(disas.pos, 0) + if disas.cur_section in (SqSection.SEQ, SqSection.CHAN, SqSection.LAYER, SqSection.ENVELOPE) and addend == 0: + # turn a label that's partway inside an instruction into a label beginning at the instruction + an addend + for start,end in disas.insn_ranges: + if value in range(start,end): + addend = value - start + value = start + break + + prefix = target_section.prefix + if addend != 0: + return f"{prefix}_{value:04X} + {maybe_hex(addend)}" + else: + return f"{prefix}_{value:04X}" + +class ArgRelAddr8(ArgAddr): + def read(self, disas): + rel_offset = sign_extend(disas.read_u8(), 8) + return disas.pos + rel_offset + +class ArgRelAddr16(ArgAddr): + def read(self, disas): + rel_offset = sign_extend(disas.read_u16(), 16) + return disas.pos + rel_offset + +class ArgSectionPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, disas.cur_section) + +class ArgBigSectionPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, disas.cur_section, big=True) + +class ArgRelSectionPtr(ArgRelAddr8): + def analyze(self, disas): + disas.add_ref(self.value, disas.cur_section) + +class ArgSeqPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.SEQ, big=True) + +class ArgChanPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.CHAN, big=True) + + def emit(self, disas): + return f"CHAN_{self.value:04X}" + +class ArgRelChanPtr(ArgRelAddr16): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.CHAN, big=True) + + def emit(self, disas): + return f"CHAN_{self.value:04X}" + +class ArgLayerPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.LAYER, big=True) + + def emit(self, disas): + return f"LAYER_{self.value:04X}" + +class ArgRelLayerPtr(ArgRelAddr16): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.LAYER, big=True) + + def emit(self, disas): + return f"LAYER_{self.value:04X}" + +class ArgArrayPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.ARRAY, big=True) + +class ArgEnvPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.ENVELOPE, big=True) + + def emit(self, disas): + return f"ENVELOPE_{self.value:04X}" + +class ArgFilterPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.FILTER, big=True) + + def emit(self, disas): + return f"FILTER_{self.value:04X}" + +class ArgTblPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.TABLE, big=True) + + def emit(self, disas): + return f"TABLE_{self.value:04X}" + +class ArgUnkPtr(ArgAddr): + def analyze(self, disas): + disas.add_ref(self.value, SqSection.UNKNOWN) + +# +# COMMANDS +# + +@dataclass +class MMLCmd: + cmd_id : int + mnemonic : str + args : Tuple[MMLArg] = () + is_branch : bool = False + is_branch_unconditional : bool = False + is_terminal : bool = False + handler : Callable = None + sections : Tuple[SqSection] = SECTION_ALL + version : Tuple[MMLVersion] = VERSION_ALL + +def nesting_decr(cmd, disas): + disas.nesting -= 1 + if disas.nesting < 0: + disas.nesting = 0 + +def nesting_incr(cmd, disas): + disas.nesting += 1 + +def set_short(cmd, disas): + disas.large_notes = False + +def set_large(cmd, disas): + disas.large_notes = True + +# +# NOTE: Changes here must be reflected in aseq.h for re-assembly +# +CMD_SPEC = ( + # + # Control Flow Commands + # + MMLCmd(0xFF, 'end', is_terminal=True, handler=nesting_decr), + MMLCmd(0xFE, 'delay1'), + MMLCmd(0xFD, 'delay', args=(ArgVar,), sections=(SqSection.SEQ, SqSection.CHAN,)), + MMLCmd(0xFC, 'call', args=(ArgBigSectionPtr,)), + MMLCmd(0xFB, 'jump', args=(ArgSectionPtr,), is_branch=True, is_branch_unconditional=True), + MMLCmd(0xFA, 'beqz', args=(ArgSectionPtr,), is_branch=True), + MMLCmd(0xF9, 'bltz', args=(ArgSectionPtr,), is_branch=True), + MMLCmd(0xF8, 'loop', args=(ArgU8,), handler=nesting_incr), + MMLCmd(0xF7, 'loopend', handler=nesting_decr), + MMLCmd(0xF6, 'break', handler=nesting_decr), + MMLCmd(0xF5, 'bgez', args=(ArgSectionPtr,), is_branch=True), + MMLCmd(0xF4, 'rjump', args=(ArgRelSectionPtr,), is_branch=True, is_branch_unconditional=True), + MMLCmd(0xF3, 'rbeqz', args=(ArgRelSectionPtr,), is_branch=True), + MMLCmd(0xF2, 'rbltz', args=(ArgRelSectionPtr,), is_branch=True), + + # + # SEQ commands + # + # non-argbits commands + MMLCmd(0xF1, 'allocnotelist', sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xF0, 'freenotelist', sections=(SqSection.SEQ,)), + MMLCmd(0xEF, 'unk_EF', sections=(SqSection.SEQ,), args=(ArgS16, ArgU8,)), + MMLCmd(0xDF, 'transpose', sections=(SqSection.SEQ,), args=(ArgS8,)), + MMLCmd(0xDE, 'rtranspose', sections=(SqSection.SEQ,), args=(ArgS8,)), + MMLCmd(0xDD, 'tempo', sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xDC, 'tempochg', sections=(SqSection.SEQ,), args=(ArgS8,)), + MMLCmd(0xDB, 'vol', sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xDA, 'volmode', sections=(SqSection.SEQ,), args=(ArgU8, ArgS16)), + MMLCmd(0xD9, "volscale", sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xD7, 'initchan', sections=(SqSection.SEQ,), args=(ArgBitField16,)), + MMLCmd(0xD6, 'freechan', sections=(SqSection.SEQ,), args=(ArgBitField16,)), + MMLCmd(0xD5, 'mutescale', sections=(SqSection.SEQ,), args=(ArgS8,)), + MMLCmd(0xD4, 'mute', sections=(SqSection.SEQ,)), + MMLCmd(0xD3, 'mutebhv', sections=(SqSection.SEQ,), args=(ArgHex8,)), + MMLCmd(0xD2, 'ldshortvelarr', sections=(SqSection.SEQ,), args=(ArgArrayPtr,)), # length 16 + MMLCmd(0xD1, 'ldshortgatearr', sections=(SqSection.SEQ,), args=(ArgArrayPtr,)), # length 16 + MMLCmd(0xD0, 'notealloc', sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xCE, 'rand', sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xCD, 'dyncall', sections=(SqSection.SEQ,), args=(ArgTblPtr,)), + MMLCmd(0xCC, 'ldi', sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xC9, 'and', sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xC8, 'sub', sections=(SqSection.SEQ,), args=(ArgU8,)), + MMLCmd(0xC7, 'stseq', sections=(SqSection.SEQ,), args=(ArgU8, ArgAddr,)), + MMLCmd(0xC6, 'stop', sections=(SqSection.SEQ,)), + MMLCmd(0xC5, 'scriptctr', sections=(SqSection.SEQ,), args=(ArgU16,)), + MMLCmd(0xC4, 'runseq', sections=(SqSection.SEQ,), args=(ArgU8, ArgSeqId,)), + MMLCmd(0xC3, 'mutechan', sections=(SqSection.SEQ,), args=(ArgS16,), version=(MMLVersion.MM,)), + # argbits commands + MMLCmd(0x00, 'testchan', sections=(SqSection.SEQ,), args=(ArgBits4,)), + MMLCmd(0x40, 'stopchan', sections=(SqSection.SEQ,), args=(ArgBits4,)), + MMLCmd(0x50, 'subio', sections=(SqSection.SEQ,), args=(IOPort3,)), + MMLCmd(0x60, 'ldres', sections=(SqSection.SEQ,), args=(ArgBits4, ArgU8, ArgU8,)), + MMLCmd(0x70, 'stio', sections=(SqSection.SEQ,), args=(IOPort3,)), + MMLCmd(0x80, 'ldio', sections=(SqSection.SEQ,), args=(IOPort3,)), + MMLCmd(0x90, 'ldchan', sections=(SqSection.SEQ,), args=(ArgBits4, ArgChanPtr,)), + MMLCmd(0xA0, 'rldchan', sections=(SqSection.SEQ,), args=(ArgBits4, ArgRelChanPtr,)), + MMLCmd(0xB0, 'ldseq', sections=(SqSection.SEQ,), args=(ArgBits4, ArgSeqId, ArgUnkPtr,)), + + # + # CHAN commands + # + # non-argbits commands + MMLCmd(0xF1, 'allocnotelist', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xF0, 'freenotelist', sections=(SqSection.CHAN,)), + MMLCmd(0xEE, 'bendfine', sections=(SqSection.CHAN,), args=(ArgS8,)), + MMLCmd(0xED, 'gain', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xEC, 'vibreset', sections=(SqSection.CHAN,)), + MMLCmd(0xEB, 'fontinstr', sections=(SqSection.CHAN,), args=(ArgFontId, ArgInstr)), + MMLCmd(0xEA, 'stop', sections=(SqSection.CHAN,)), + MMLCmd(0xE9, 'notepri', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xE8, 'params', sections=(SqSection.CHAN,), args=(ArgU8, ArgU8, ArgU8, ArgS8, ArgS8, ArgU8, ArgU8, ArgU8,)), + MMLCmd(0xE7, 'ldparams', sections=(SqSection.CHAN,), args=(ArgAddr,)), + MMLCmd(0xE6, 'samplebook', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xE5, 'reverbidx', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xE4, 'dyncall', sections=(SqSection.CHAN,)), + MMLCmd(0xE3, 'vibdelay', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xE2, 'vibdepthgrad', sections=(SqSection.CHAN,), args=(ArgU8, ArgU8, ArgU8,)), + MMLCmd(0xE1, 'vibfreqgrad', sections=(SqSection.CHAN,), args=(ArgU8, ArgU8, ArgU8,)), + MMLCmd(0xE0, 'volexp', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xDF, 'vol', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xDE, 'freqscale', sections=(SqSection.CHAN,), args=(ArgU16,)), + MMLCmd(0xDD, 'pan', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xDC, 'panweight', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xDB, 'transpose', sections=(SqSection.CHAN,), args=(ArgS8,)), + MMLCmd(0xDA, 'env', sections=(SqSection.CHAN,), args=(ArgEnvPtr,)), + MMLCmd(0xD9, 'releaserate', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xD8, 'vibdepth', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xD7, 'vibfreq', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xD4, 'reverb', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xD3, 'bend', sections=(SqSection.CHAN,), args=(ArgS8,)), + MMLCmd(0xD2, 'sustain', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xD1, 'notealloc', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xD0, 'effects', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xCF, 'stptrtoseq', sections=(SqSection.CHAN,), args=(ArgAddr,)), + MMLCmd(0xCE, 'ldptr', sections=(SqSection.CHAN,), args=(ArgAddr,)), + MMLCmd(0xCD, 'stopchan', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xCC, 'ldi', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xCB, 'ldseq', sections=(SqSection.CHAN,), args=(ArgUnkPtr,)), + MMLCmd(0xCA, 'mutebhv', sections=(SqSection.CHAN,), args=(ArgHex8,)), + MMLCmd(0xC9, 'and', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xC8, 'sub', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xC7, 'stseq', sections=(SqSection.CHAN,), args=(ArgU8, ArgAddr,)), + MMLCmd(0xC6, 'font', sections=(SqSection.CHAN,), args=(ArgFontId,)), + MMLCmd(0xC5, 'dyntbllookup', sections=(SqSection.CHAN,)), + MMLCmd(0xC4, 'noshort', sections=(SqSection.CHAN,), handler=set_large), + MMLCmd(0xC3, 'short', sections=(SqSection.CHAN,), handler=set_short), + MMLCmd(0xC2, 'dyntbl', sections=(SqSection.CHAN,), args=(ArgTblPtr,)), + MMLCmd(0xC1, 'instr', sections=(SqSection.CHAN,), args=(ArgInstr,)), + MMLCmd(0xBE, 'unk_BE', sections=(SqSection.CHAN,), args=(ArgU8,), version=(MMLVersion.MM,)), + MMLCmd(0xBD, 'randptr', sections=(SqSection.CHAN,), args=(ArgU16, ArgU16,), version=(MMLVersion.OOT,)), + MMLCmd(0xBD, 'samplestart', sections=(SqSection.CHAN,), args=(ArgU8,), version=(MMLVersion.MM,)), + MMLCmd(0xBC, 'ptradd', sections=(SqSection.CHAN,), args=(ArgHex16,)), + MMLCmd(0xBB, 'combfilter', sections=(SqSection.CHAN,), args=(ArgU8, ArgU16)), + MMLCmd(0xBA, 'randgate', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xB9, 'randvel', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xB8, 'rand', sections=(SqSection.CHAN,), args=(ArgU8,)), + MMLCmd(0xB7, 'randtoptr', sections=(SqSection.CHAN,), args=(ArgU16,)), + MMLCmd(0xB6, 'dyntblv', sections=(SqSection.CHAN,)), + MMLCmd(0xB5, 'dyntbltoptr', sections=(SqSection.CHAN,)), + MMLCmd(0xB4, 'ptrtodyntbl', sections=(SqSection.CHAN,)), + MMLCmd(0xB3, 'filter', sections=(SqSection.CHAN,), args=(ArgU4x2,)), + MMLCmd(0xB2, 'ldseqtoptr', sections=(SqSection.CHAN,), args=(ArgTblPtr,)), + MMLCmd(0xB1, 'freefilter', sections=(SqSection.CHAN,)), + MMLCmd(0xB0, 'ldfilter', sections=(SqSection.CHAN,), args=(ArgFilterPtr,)), + MMLCmd(0xAA, 'unk_AA', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)), + MMLCmd(0xA8, 'randptr', sections=(SqSection.CHAN,), args=(ArgU16, ArgU16,), version=(MMLVersion.MM,)), + MMLCmd(0xA7, 'unk_A7', sections=(SqSection.CHAN,), args=(ArgVar,), version=(MMLVersion.MM,)), + MMLCmd(0xA6, 'unk_A6', sections=(SqSection.CHAN,), args=(ArgVar, ArgVar,), version=(MMLVersion.MM,)), + MMLCmd(0xA5, 'unk_A5', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)), + MMLCmd(0xA4, 'unk_A4', sections=(SqSection.CHAN,), args=(ArgVar,), version=(MMLVersion.MM,)), + MMLCmd(0xA3, 'unk_A3', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)), + MMLCmd(0xA2, 'unk_A2', sections=(SqSection.CHAN,), args=(ArgVar,), version=(MMLVersion.MM,)), + MMLCmd(0xA1, 'unk_A1', sections=(SqSection.CHAN,), args=(), version=(MMLVersion.MM,)), + MMLCmd(0xA0, 'unk_A0', sections=(SqSection.CHAN,), args=(ArgVar,), version=(MMLVersion.MM,)), + # argbits commands + MMLCmd(0x00, 'cdelay', sections=(SqSection.CHAN,), args=(ArgBits4,)), + MMLCmd(0x10, 'sample', sections=(SqSection.CHAN,), args=(ArgBits3, ArgAddr,)), + MMLCmd(0x18, 'sampleptr', sections=(SqSection.CHAN,), args=(ArgBits3, ArgAddr,)), + MMLCmd(0x20, 'ldchan', sections=(SqSection.CHAN,), args=(ArgBits4, ArgChanPtr,)), + MMLCmd(0x30, 'stcio', sections=(SqSection.CHAN,), args=(ArgBits4, IOPort8,)), + MMLCmd(0x40, 'ldcio', sections=(SqSection.CHAN,), args=(ArgBits4, IOPort8,)), + MMLCmd(0x50, 'subio', sections=(SqSection.CHAN,), args=(IOPort3,)), + MMLCmd(0x60, 'ldio', sections=(SqSection.CHAN,), args=(IOPort3,)), + MMLCmd(0x70, 'stio', sections=(SqSection.CHAN,), args=(IOPort3,)), + MMLCmd(0x78, 'rldlayer', sections=(SqSection.CHAN,), args=(ArgBits3, ArgRelLayerPtr,)), + MMLCmd(0x80, 'testlayer', sections=(SqSection.CHAN,), args=(ArgBits3,)), + MMLCmd(0x88, 'ldlayer', sections=(SqSection.CHAN,), args=(ArgBits3, ArgLayerPtr,)), + MMLCmd(0x90, 'dellayer', sections=(SqSection.CHAN,), args=(ArgBits3,)), + MMLCmd(0x98, 'dynldlayer', sections=(SqSection.CHAN,), args=(ArgBits3,)), + + # + # LAYER commands + # + # non-argbits commands + MMLCmd(0xC0, 'ldelay', sections=(SqSection.LAYER,), args=(ArgVar,)), + MMLCmd(0xC1, 'shortvel', sections=(SqSection.LAYER,), args=(ArgU8,)), + MMLCmd(0xC2, 'transpose', sections=(SqSection.LAYER,), args=(ArgS8,)), + MMLCmd(0xC3, 'shortdelay', sections=(SqSection.LAYER,), args=(ArgVar,)), + MMLCmd(0xC4, 'legato', sections=(SqSection.LAYER,)), + MMLCmd(0xC5, 'nolegato', sections=(SqSection.LAYER,)), + MMLCmd(0xC6, 'instr', sections=(SqSection.LAYER,), args=(ArgInstr,)), + MMLCmd(0xC7, 'portamento', sections=(SqSection.LAYER,), args=(ArgPortamentoMode, ArgPitchU8, ArgPortamentoTime,)), + MMLCmd(0xC8, 'noportamento', sections=(SqSection.LAYER,)), + MMLCmd(0xC9, 'shortgate', sections=(SqSection.LAYER,), args=(ArgU8,)), + MMLCmd(0xCA, 'notepan', sections=(SqSection.LAYER,), args=(ArgU8,)), + MMLCmd(0xCB, 'env', sections=(SqSection.LAYER,), args=(ArgEnvPtr, ArgU8,)), + MMLCmd(0xCC, 'nodrumpan', sections=(SqSection.LAYER,)), + MMLCmd(0xCD, 'stereo', sections=(SqSection.LAYER,), args=(ArgStereoConfig,)), + MMLCmd(0xCE, 'bendfine', sections=(SqSection.LAYER,), args=(ArgS8,)), + MMLCmd(0xCF, 'releaserate', sections=(SqSection.LAYER,), args=(ArgU8,)), + MMLCmd(0xD0, 'ldshortvel', sections=(SqSection.LAYER,), args=(ArgBits4,)), + MMLCmd(0xE0, 'ldshortgate', sections=(SqSection.LAYER,), args=(ArgBits4,)), + MMLCmd(0xF0, 'unk_F0', sections=(SqSection.LAYER,), args=(ArgS16,), version=(MMLVersion.MM,)), + MMLCmd(0xF1, 'surroundeffect', sections=(SqSection.LAYER,), args=(ArgU8,), version=(MMLVersion.MM,)), + # argbits commands + # large layer + MMLCmd(0x00, 'notedvg', sections=(SqSection.LAYER,), args=(ArgPitch, ArgVar, ArgU8, ArgU8,)), + MMLCmd(0x40, 'notedv', sections=(SqSection.LAYER,), args=(ArgPitch, ArgVar, ArgU8,)), + MMLCmd(0x80, 'notevg', sections=(SqSection.LAYER,), args=(ArgPitch, ArgU8, ArgU8,)), + # small layer + MMLCmd(0x00, 'shortdvg', sections=(SqSection.LAYER,), args=(ArgPitch, ArgVar,)), + MMLCmd(0x40, 'shortdv', sections=(SqSection.LAYER,), args=(ArgPitch,)), + MMLCmd(0x80, 'shortvg', sections=(SqSection.LAYER,), args=(ArgPitch,)), +) + +# +# DISASSEMBLER +# + +class SequenceFragment: + def __init__(self, disas, section, data, start, end): + assert len(data) == end - start , f"Bad: got {len(data)} bytes for range [{start}:{end}] {data}" + + self.section = section + self.data = data + self.start = start + self.end = end + self.disas = disas + + def __str__(self): + return f"Fragment ({self.section}) [{self.start}, {self.end}]" + + def __lt__(self, other): + return self.start < other.start + + @staticmethod + def merge(frag1, frag2): + if frag1 == frag2: + return frag1 + + if frag1.section != frag2.section: + return None + + # don't merge envelopes or tables ever + if frag1.section in (SqSection.ENVELOPE, SqSection.TABLE): + return None + + min_start = min(frag1.start, frag2.start) + max_start = max(frag1.start, frag2.start) + min_end = min(frag1.end, frag2.end) + max_end = max(frag1.end, frag2.end) + + if max_start > min_end: + return None + + data1, data2 = frag1.data, frag2.data + if frag2.start < frag1.start: + data1, data2 = data2, data1 + + if min_end == max_end: + # data1 contains data2 + return SequenceFragment(frag1.disas, frag1.section, data1, min_start, max_end) + + assert data1[max_start:] == data2[:len(data1)-max_start] , \ + f"Data does not agree on overlap between\n{frag1}\n{frag2}\n{data2[:len(data1)-max_start]}" + + return SequenceFragment(frag1.disas, frag1.section, data1[:max_start] + data2, min_start, max_end) + +@dataclass +class SequenceTableSpec: + start_offset : int + num_entries : int + addend : int + sectype : SqSection + + def contains_loc(self, pos): + return pos in range(self.start_offset, self.start_offset + 2 * self.num_entries) + +class SequenceDisassembler: + + def __init__(self, seq_num : int, data : bytes, tables : Optional[Tuple[SequenceTableSpec]], cmds : Tuple[MMLCmd], + version : MMLVersion, outpath : str, seq_name : str, used_fonts : List[AudiobankFile], all_seq_names): + self.seq_num = seq_num + self.seq_name = seq_name + self.used_fonts = used_fonts + + self.all_seq_names = all_seq_names + + self.pos = 0 + self.insn_begin = 0 + self.data = data + self.hit_eof = False + self.cur_section = SqSection.SEQ + self.nesting = 0 + self.portamento_is_special = False + self.large_notes = True + + self.outpath = outpath + + self.cmds : Dict[SqSection, Dict[int, MMLCmd]] = { + SqSection.SEQ : {}, + SqSection.CHAN : {}, + SqSection.LAYER : {}, + } + + # preprocess command list into dictionary, possibly duplicating into + # several id keys if any lsbits are used as an arg + for cmd in cmds: + # ignore commands not in this version + if version not in cmd.version: + continue + + # find number of lsbits that don't contribute to the command id + if len(cmd.args) > 0 and issubclass(cmd.args[0], MMLArgBits): + nbits = cmd.args[0].NBITS + else: + nbits = 0 + + id = cmd.cmd_id + + for section in cmd.sections: + cmds_s = self.cmds[section] + + for i in range(1 << nbits): + new = cmd + old = cmds_s.get(id + i, None) + if old is not None: + assert old.mnemonic in ("notedvg", "notedv", "notevg"), (old.mnemonic, cmd.mnemonic) + new = (old, cmd) + + cmds_s[id + i] = new + + self.force_long = set() + + self.insn_ranges = [] + + self.coverage = [0] * len(self.data) + + self.fragments = [] + + self.branch_targets = {} + self.big_labels = set() + + self.all_ranges = [] + + self.decode_list = [] + self.all_seen = [] + + self.tables : Optional[Tuple[SequenceTableSpec]] = tables + self.table_cache = set() + + self.addends = {} + + self.unused = [] + + # general helpers + + def read_bits(self, nbits): + return self.bits_val + + def read_u8(self): + if self.hit_eof: + raise Exception() + + if self.pos == len(self.data): + self.hit_eof = True + ret = None + else: + ret = self.data[self.pos] + self.pos += 1 + return ret + + def read_u16(self): + return (self.read_u8() << 8) | self.read_u8() + + def read_s16(self): + return sign_extend(self.read_u16(), 16) + + def lookup_cmd(self, id : int) -> MMLCmd: + # lookup command info + cmd : MMLCmd = self.cmds[self.cur_section].get(id, None) + assert cmd is not None , (self.cur_section, id, self.cmds) + + if isinstance(cmd, tuple): + # select based on whether we're dealing with large or short notes + cmd = cmd[int(not self.large_notes)] + + # part of the command byte may be an arg, save the value + mask = 0 + if len(cmd.args) > 0 and issubclass(cmd.args[0], MMLArgBits): + mask = (1 << cmd.args[0].NBITS) - 1 + self.bits_val = id & mask + + return cmd + + # + # analysis helpers + # + + def register_addend(self, pos, value): + self.addends[pos] = value + + def add_branch_target(self, value, section, big=False): + self.branch_targets[value] = section + if big: + self.big_labels.add(value) + + def add_ref(self, value, section=None, big=False): + if section is None: + self.add_branch_target(value, SqSection.UNKNOWN) + return + + self.add_branch_target(value, section, big=big) + + self.add_job(value, section, self.cur_section) + + def add_job(self, value, section, from_section=None): + if value not in self.all_seen: + self.all_seen.append(value) + self.decode_list.append((value, section, from_section or section)) + + def merge_frags(self): + self.fragments = list(sorted(self.fragments)) + + if len(self.fragments) < 2: + return + + i = 0 + while i != len(self.fragments) - 1: + frag1 = self.fragments[i] + frag2 = self.fragments[i + 1] + merged = SequenceFragment.merge(frag1, frag2) + if merged is not None: + self.fragments[i] = merged + del self.fragments[i + 1] + else: + i += 1 + + # + # analysis handlers + # + + def analyze_code(self): # sequence, channel, layer + start_pos = self.pos + + # print(f" << [0x{start_pos:X}/0x{len(self.data):X}] :: {self.cur_section} >>") + + self.insn_begin = start_pos + cmd_byte = self.read_u8() + cmd = self.lookup_cmd(cmd_byte) + + assert cmd is not None , f"Bad command ID 0x{cmd_byte:02X} for section {self.cur_section.name} at 0x{start_pos:X}" + # print(hex(cmd_byte)) + + args = [argtype(self) for argtype in cmd.args] + + raw_data = self.data[start_pos:self.pos] + + self.insn_ranges.append((start_pos, self.pos)) + + for i in range(start_pos,self.pos): + self.coverage[i] = self.cur_section + + # print(f"/* 0x{start_pos:04X} [{' '.join([f'0x{b:02X}' for b in raw_data]):24}] */ {cmd.mnemonic:11} {', '.join([arg.emit(self) for arg in args])}".strip()) + + for arg in args: + arg.analyze(self) + + if cmd.handler is not None: + cmd.handler(cmd, self) + + self.fragments.append(SequenceFragment(self, self.cur_section, raw_data, start_pos, self.pos)) + + if not (cmd.is_branch_unconditional or cmd.is_terminal): + self.decode_list.append((self.pos, self.cur_section, self.cur_section)) + + def analyze_table(self): + assert self.tables is not None, "Found a table but no table spec provided." + + for table_spec in self.tables: + if table_spec.contains_loc(self.pos): + break + else: + assert False , f"Found table at {self.pos:04X} but no entry number provided" + + start_pos = self.pos = table_spec.offset + if start_pos in self.table_cache: + return + + for _ in range(table_spec.num_entries): + curpos = self.pos + cur = self.read_u16() - table_spec.addend + if cur >= len(self.data) - 1: + assert False , "Bad table pointer" + + self.add_branch_target(cur, table_spec.sectype, big=True) + self.add_job(cur, table_spec.sectype, table_spec.sectype) + self.register_addend(curpos, table_spec.addend) + + self.fragments.append(SequenceFragment(self, self.cur_section, self.data[start_pos:self.pos], start_pos, self.pos)) + self.table_cache.add(start_pos) + + def analyze_array(self): + start_pos = self.pos + + # TODO better heuristic than just hardcoding 16... + # it would be better to wait until later to resize arrays though, up to the next identified fragment + # ARRAY + UNK + OTHER -> ARRAY + OTHER + for _ in range(16): + assert self.read_u8() == 0 + + self.fragments.append(SequenceFragment(self, self.cur_section, self.data[start_pos:self.pos], start_pos, self.pos)) + + def analyze_filter(self): + start_pos = self.pos + + for _ in range(8): + assert self.read_u16() == 0 + + self.fragments.append(SequenceFragment(self, self.cur_section, self.data[start_pos:self.pos], start_pos, self.pos)) + + def analyze_envelope(self): + start_pos = self.pos + + while True: # dangerous + delay = self.read_s16() + arg = self.read_s16() + if delay < 0: + break + + self.fragments.append(SequenceFragment(self, self.cur_section, self.data[start_pos:self.pos], start_pos, self.pos)) + + def analyze_unknown(self): + self.fragments.append(SequenceFragment(self, self.cur_section, self.data[self.pos:self.pos+2], self.pos, self.pos+2)) + + def analyze(self): + # mark offset 0 as a SEQ section + self.add_branch_target(0, SqSection.SEQ, big=True) + self.decode_list.append((0, SqSection.SEQ, SqSection.SEQ)) + + # analyze all sections, following branches to locate new sections + while len(self.decode_list) != 0: + self.pos, self.cur_section, self.refd_from = self.decode_list.pop() + + if self.pos >= len(self.data): + # ignore sections that begin past the end of the file + # TODO should be an error or warning? + continue + + # execute handler based on section + { + SqSection.SEQ : self.analyze_code, + SqSection.CHAN : self.analyze_code, + SqSection.LAYER : self.analyze_code, + SqSection.TABLE : self.analyze_table, + SqSection.ARRAY : self.analyze_array, + SqSection.FILTER : self.analyze_filter, + SqSection.ENVELOPE : self.analyze_envelope, + SqSection.UNKNOWN : self.analyze_unknown, + }[self.cur_section]() + + # merge fragments + self.merge_frags() + + # update coverage + self.final_cvg = [0] * len(self.data) + for frag in self.fragments: + for i in range(frag.start,frag.end): + self.final_cvg[i] = frag.section + + # resolve gaps in coverage + while True: + # keeps going until there's no zeros except for padding + try: + first_zero_idx = self.final_cvg.index(0) + except ValueError: + break # no more gaps + + # there was a gap, handle it + + if ((first_zero_idx + 0xF) & ~0xF) == len(self.data) and \ + all(b == 0 for b in self.final_cvg[first_zero_idx:]) and \ + all(b == 0 for b in self.data[first_zero_idx:]): + # there's only padding left, we're done + break + else: + # resolve non-padding gaps with heuristics + + # TODO any unknown data after a `jump` in a sequence frag should extend the sequence frag + # TODO any unknown data before a filter should be a balign 16 + + last_zero_idx = first_zero_idx + while self.final_cvg[last_zero_idx] == 0 and last_zero_idx < len(self.final_cvg)-1: + self.final_cvg[last_zero_idx] = SqSection.UNKNOWN + last_zero_idx += 1 + + num_unk = last_zero_idx - first_zero_idx + + emit_unk = True + + prev_frag = None + prev_frag_idx = None + next_frag = None + next_frag_idx = None + + for i,frag in enumerate(self.fragments): + if frag.end == first_zero_idx: + prev_frag = frag + prev_frag_idx = i + elif frag.start == last_zero_idx: + next_frag = frag + next_frag_idx = i + + # SEQ + UNK -> SEQ + if prev_frag is not None: + if prev_frag.section == SqSection.SEQ: + self.fragments[prev_frag_idx] = SequenceFragment(self, SqSection.SEQ, + self.data[prev_frag.start:last_zero_idx], + prev_frag.start, last_zero_idx) + emit_unk = False + + if next_frag is not None: + # UNK + FILTER -> FILTER + if next_frag.section == SqSection.FILTER and num_unk < 16: + emit_unk = False + + # UNK + TABLE -> TABLE + if next_frag.section == SqSection.TABLE and num_unk < 2: + emit_unk = False + + if prev_frag is not None and next_frag is not None: + # LAYER + UNK + LAYER -> LAYER LAYER LAYER + if prev_frag.section == SqSection.LAYER and next_frag.section == SqSection.LAYER: + self.fragments.append(SequenceFragment(self, SqSection.LAYER, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) + emit_unk = False + + # LAYER + UNK + CHANNEL -> LAYER LAYER CHANNEL + if prev_frag.section == SqSection.LAYER and next_frag.section == SqSection.CHAN: + self.fragments.append(SequenceFragment(self, SqSection.LAYER, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) + emit_unk = False + + # TABLE + UNK + ENVELOPE -> TABLE + ENVELOPE.. + ENVELOPE + if prev_frag.section == SqSection.TABLE and next_frag.section == SqSection.ENVELOPE: + self.fragments.append(SequenceFragment(self, SqSection.ENVELOPE, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) + emit_unk = False + + # ENVELOPE + UNK + ENVELOPE -> ENVELOPE + ENVELOPE.. + ENVELOPE + if prev_frag.section == SqSection.ENVELOPE and next_frag.section == SqSection.ENVELOPE: + self.fragments.append(SequenceFragment(self, SqSection.ENVELOPE, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) + emit_unk = False + + if prev_frag is not None and next_frag is None: + # ENVELOPE + UNK + END -> ENVELOPE + ENVELOPE.. + FILTER.. + END + if prev_frag.section == SqSection.ENVELOPE: + if all(b == 0 for b in self.data[first_zero_idx:]): + for k in range(first_zero_idx, len(self.data), 16): + if k + 16 > len(self.data): + # padding + break + self.fragments.append(SequenceFragment(self, SqSection.FILTER, self.data[k:k+16], k, k + 16)) + else: + self.fragments.append(SequenceFragment(self, SqSection.ENVELOPE, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) + emit_unk = False + + if emit_unk: + # leave it unknown for now, TODO make reasonable guesses + self.add_branch_target(first_zero_idx, SqSection.UNKNOWN) + self.fragments.append(SequenceFragment(self, SqSection.UNKNOWN, self.data[first_zero_idx:last_zero_idx], first_zero_idx, last_zero_idx)) + + # + # disas helpers + # + + def label_name(self, value, section, force_big=False): + if value in self.big_labels or force_big: + lbl_prefix = section.lbl_prefix + " " + suffix = "" + else: + lbl_prefix = "" + suffix = ":" + + return f"{lbl_prefix}{section.prefix}_{value:04X}{suffix}" + + def emit_branch_target_real(self, outfile, value, section, force_big=False): + if section is SqSection.UNKNOWN: + for frag in self.fragments: + if value in range(frag.start, frag.end): + section = frag.section + break + + outfile.write(f"{self.label_name(value, section, force_big)}\n") + + def emit_branch_target(self, outfile, start, end, force_big=False): + did_emit = False + for b_tgt in self.branch_targets: + if b_tgt in range(start,end): + self.emit_branch_target_real(outfile, start, self.branch_targets[b_tgt], force_big) + did_emit = True + return did_emit + + # + # disas handlers + # + + def disas_section(self, frag : SequenceFragment, outfile): + force_big_lbl = False + + if self.pos == frag.start: + # If the previous frag is not the same type as this frag, force the first label to be a big label + for frag2 in self.fragments: + frag2 : SequenceFragment + + if frag2.end == frag.start: + if frag2.section != frag.section: + force_big_lbl = True + break + + while self.pos < frag.end: + start_pos = self.pos + self.insn_begin = start_pos + + cmd_byte = self.read_u8() + cmd = self.lookup_cmd(cmd_byte) + mnemonic = cmd.mnemonic + + # Hacky fixups for commands using long var encodings when it was not necessary for them to do so, the usual + # macros for re-assembly only select the long encoding when necessary so switch to special macros that + # always use the long encoding unconditionally. + if self.insn_begin in self.force_long: + if mnemonic == "notedv": + mnemonic = "noteldv" + elif mnemonic in ("delay", "ldelay"): + mnemonic = "lldelay" + else: + assert False , mnemonic + + args = [argtype(self) for argtype in cmd.args] + raw_data = self.data[start_pos:self.pos] + + self.emit_branch_target(outfile, start_pos, self.pos, force_big_lbl) + force_big_lbl = False + + args_str = ', '.join([arg.emit(self) for arg in args]) + data_str = ' '.join([f'0x{b:02X}' for b in raw_data]) + + outfile.write(f"/* 0x{start_pos:04X} [{data_str:24}] */ {mnemonic:11} {args_str}".strip() + "\n") + + if cmd.is_terminal or cmd.is_branch_unconditional: + outfile.write("\n") + + def disas_table(self, frag : SequenceFragment, outfile): + base_pos = self.pos + + while self.pos < frag.end: + start_pos = self.pos + + addend = self.addends.get(start_pos, 0) + + ent = self.read_u16() - addend + + self.emit_branch_target(outfile, start_pos, self.pos) + + section = self.branch_targets.get(ent, None) + + # TODO + if section is None: + section = SqSection.UNKNOWN + + if addend != 0: + addend_str = f" + {addend}" + else: + addend_str = "" + + # TODO proper label name + outfile.write(f" entry {section.prefix}_{ent:04X}{addend_str}\n") + + outfile.write("\n") + + def disas_filter(self, frag : SequenceFragment, outfile): + start_pos = self.pos + + num_filters, align = divmod(len(frag.data), 2 * 8) + + assert all(b == 0 for b in frag.data) + assert align == 0 + + for n in range(num_filters): + self.emit_branch_target_real(outfile, start_pos + n * 2 * 8, SqSection.FILTER, force_big=True) + outfile.write(" filter 0, 0, 0, 0, 0, 0, 0, 0\n\n") + + def disas_envelope(self, frag : SequenceFragment, outfile): + start_pos = self.pos + + self.emit_branch_target(outfile, start_pos, frag.end) + + while self.pos < frag.end: + delay = self.read_s16() + arg = self.read_s16() + + if delay == 0 and arg == 0: + outfile.write(" disable\n") + elif delay == -1 and arg == 0: + outfile.write(" hang\n") + elif delay == -2: + outfile.write(f" goto {arg}\n") + elif delay == -3 and arg == 0: + outfile.write(" restart\n") + else: + assert delay > 0 + outfile.write(f" point {delay}, {arg}\n") + + if delay < 0 and self.pos not in self.branch_targets: + outfile.write("\n") + self.emit_branch_target_real(outfile, self.pos, frag.section) + + outfile.write("\n") + + def disas_array(self, frag : SequenceFragment, outfile): + self.emit_branch_target(outfile, frag.start, frag.end) + + array_data = self.data[frag.start:frag.end] + if all(b == 0 for b in array_data): + outfile.write(f".fill 0x{len(array_data):X}\n\n") + else: + for b in array_data: + outfile.write(f".byte 0x{b:2X}\n") + outfile.write("\n") + + def disas_unknown(self, frag : SequenceFragment, outfile): + start_pos = self.pos + + prev = start_pos + for b_tgt in sorted(self.branch_targets): + if b_tgt in range(start_pos+1,frag.end): + # emit data between this branch target and the previous + outfile.write(" .byte " + ", ".join(f"0x{b:02X}" for b in self.data[prev:b_tgt]) + "\n\n") + if b_tgt in range(start_pos,frag.end): + # emit the branch target + self.emit_branch_target_real(outfile, b_tgt, SqSection.UNKNOWN) + prev = b_tgt + + # write any remaining data if the final branch target was not the end of the frag + if prev != frag.end: + outfile.write(" .byte " + ", ".join(f"0x{b:02X}" for b in self.data[prev:frag.end]) + "\n\n") + + # + # emit disassembled text + # + + def emit(self): + with open(self.outpath, "w") as outfile: + # emit header + outfile.write("#include \"aseq.h\"\n") + + # emit fonts + for font in self.used_fonts: + outfile.write(f"#include \"{font.file_name}.h\"\n") + outfile.write("\n") + + outfile.write(f".startseq {self.seq_name}\n\n") + + # emit fragments + for frag in sorted(self.fragments): + frag : SequenceFragment + + self.cur_section = frag.section + self.pos = frag.start + + { + SqSection.SEQ : self.disas_section, + SqSection.CHAN : self.disas_section, + SqSection.LAYER : self.disas_section, + SqSection.TABLE : self.disas_table, + SqSection.ARRAY : self.disas_array, + SqSection.FILTER : self.disas_filter, + SqSection.ENVELOPE : self.disas_envelope, + SqSection.UNKNOWN : self.disas_unknown, + }[frag.section](frag, outfile) + + outfile.write(f".endseq {self.seq_name}\n") + +if __name__ == '__main__': + import sys + + in_path = sys.argv[1] + out_path = sys.argv[2] + + with open(in_path, "rb") as infile: + data = bytearray(infile.read()) + + class FontDummy: + def __init__(self, file_name) -> None: + self.name = file_name + self.file_name = file_name + self.instrument_index_map = {} + + disas = SequenceDisassembler(0, data, None, CMD_SPEC, MMLVersion.MM, out_path, "", [FontDummy("wow")], []) + disas.analyze() + disas.emit() diff --git a/tools/audio_extraction.py b/tools/audio_extraction.py index da30273b75..9c3a202648 100644 --- a/tools/audio_extraction.py +++ b/tools/audio_extraction.py @@ -9,7 +9,8 @@ import argparse import version_config -from audio.extraction.audio_extract import extract_audio_for_version, GameVersionInfo, MMLVersion +from audio.extraction.audio_extract import extract_audio_for_version, GameVersionInfo +from audio.extraction.disassemble_sequence import MMLVersion, SequenceTableSpec, SqSection if __name__ == '__main__': parser = argparse.ArgumentParser(description="baserom audio asset extractor") @@ -153,6 +154,36 @@ if __name__ == '__main__': # Some audiotable banks have a buffer clearing bug. Indicate which banks suffer from this. audiotable_buffer_bugs = (0,) + # Tables have no clear start and end in a sequence. Mark the locations of all tables that appear in sequences. + seq_disas_tables = { + # sequence number : (spec, ...) + 0 : ( + SequenceTableSpec(0x00E1, 128, 0, SqSection.CHAN), + SequenceTableSpec(0x0EE3, 80, 0, SqSection.CHAN), + SequenceTableSpec(0x16D5, 248, 0, SqSection.CHAN), + SequenceTableSpec(0x315E, 499, 0, SqSection.CHAN), + SequenceTableSpec(0x5729, 72, 0, SqSection.CHAN), + SequenceTableSpec(0x5EE5, 8, 0, SqSection.CHAN), + SequenceTableSpec(0x5FF2, 128, 0, SqSection.CHAN), + ), + 1 : ( + SequenceTableSpec(0x0192, 20, 0, SqSection.LAYER), + SequenceTableSpec(0x01BA, 20, 0, SqSection.LAYER), + SequenceTableSpec(0x01E2, 20, 0, SqSection.LAYER), + SequenceTableSpec(0x020A, 20, 0, SqSection.LAYER), + SequenceTableSpec(0x0232, 20, 1, SqSection.LAYER), + SequenceTableSpec(0x025A, 20, 1, SqSection.LAYER), + SequenceTableSpec(0x0282, 20, 1, SqSection.LAYER), + ), + 2 : ( + SequenceTableSpec(0x00BC, 2, 0, SqSection.SEQ), + SequenceTableSpec(0x00C0, 2, 0, SqSection.ARRAY), + ), + 109 : ( + SequenceTableSpec(0x0646, 16, 0, SqSection.CHAN), + ), + } + version_info = GameVersionInfo(MMLVersion.OOT, soundfont_table_code_offset, seq_font_table_code_offset, @@ -161,6 +192,7 @@ if __name__ == '__main__': seq_enum_names, handwritten_sequences, fake_banks, - audiotable_buffer_bugs) + audiotable_buffer_bugs, + seq_disas_tables) extract_audio_for_version(version_info, args.extracted_dir, args.read_xml, args.write_xml)