Skip to content

Commit d37cec4

Browse files
committed
v0.82: fix parsing bugs & improve parsing robustness
add chunkedogg_extract utility script to repo
1 parent af48577 commit d37cec4

File tree

3 files changed

+120
-48
lines changed

3 files changed

+120
-48
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ Uses pydub: https://github.com/jiaaro/pydub
66
Usage: `python bms_to_rpp.py chart_file.bms [output_file.rpp]` \
77
Or just drag-and-drop the chart onto `bms_to_rpp.py`
88

9-
Supports WAV keysounds. \
9+
Supports WAV (PCM) keysounds. \
1010
If your BMS does not include WAV keysounds, recommend converting them to WAV first. \
1111
OGG keysounds supported only if ffmpeg is installed, and processing will be very slow.
1212

1313
Supports BPMs, extended BPMs, measure lengths/time signatures, and STOPs. \
1414
Negative BPMs untested. Other BMS features may not be implemented.
1515

1616
Major props to the BMS command memo: http://hitkey.nekokan.dyndns.info/cmds.htm
17+
Major props to the DTX data format spec: https://ja.osdn.net/projects/dtxmania/wiki/DTX%2520data%2520format

bms_to_rpp.py

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# BMS to RPP v0.8
1+
# BMS to RPP v0.82
22
# Copyright (C) 2020 shockdude
33
# REAPER is property of Cockos Incorporated
44

@@ -23,7 +23,7 @@
2323
from pydub import AudioSegment
2424

2525
def usage():
26-
print("BMS to RPP v0.8")
26+
print("BMS to RPP v0.82")
2727
print("Convert a BMS or DTX chart into a playable REAPER project")
2828
print("WAV keysounds recommended, OGG keysounds require ffmpeg/avconv and are slow to parse.")
2929
print("Usage: {} chart_file.bms [output_filename.rpp]".format(sys.argv[0]))
@@ -113,73 +113,62 @@ def find_tag(line, tag):
113113
return line[len(tag):]
114114
return None
115115

116+
# parse header value
117+
def get_header_value(line, header):
118+
header_re = re.compile("#{}([\\w\\d][\\w\\d])(:\\s*|\\s+)(.+)\\s*".format(header))
119+
re_match = header_re.match(line)
120+
if re_match != None and re_match.start() == 0:
121+
index = re_match.group(1)
122+
value = re_match.group(3)
123+
return index, value
124+
return None, None
125+
116126
# create dictionary of keysounds
117127
def add_keysound(line):
118-
wav_re = re.compile("#WAV[\\w\\d][\\w\\d]")
119-
re_match = wav_re.match(line)
120-
if re_match != None and re_match.start() == 0:
121-
line_split = line.split(" ")
122-
keysound_index = line_split[0][-2:]
123-
keysound_origname = " ".join(line_split[1:]).strip()
124-
# look for wav or ogg, even if the original chart uses a different format
125-
keysound_basename = os.path.splitext(keysound_origname)[0]
128+
index, value = get_header_value(line, "WAV")
129+
if index != None and value != None:
130+
keysound_basename = os.path.splitext(value)[0]
126131
keysound_filename = keysound_basename + WAV_EXT
127132
if os.path.isfile(keysound_filename):
128-
keysound_dict[keysound_index] = keysound_filename
133+
keysound_dict[index] = keysound_filename
129134
return True
130135
keysound_filename = keysound_basename + OGG_EXT
131136
if os.path.isfile(keysound_filename):
132-
keysound_dict[keysound_index] = keysound_filename
137+
keysound_dict[index] = keysound_filename
133138
return True
134139
print("Error: could not find .wav or .ogg for {}".format(keysound_origname))
135140
usage()
136141
return False
137142

138143
# create dictionary of keysound volume percentages
139144
def add_keysoundvolume(line):
140-
bpm_re = re.compile("#VOLUME[\\w\\d][\\w\\d]")
141-
re_match = bpm_re.match(line)
142-
if re_match != None and re_match.start() == 0:
143-
line_split = line.split(" ")
144-
vol_index = line_split[0][-2:]
145-
vol = float(line_split[1].strip()) / 100.0
146-
keysoundvol_dict[vol_index] = vol
145+
index, value = get_header_value(line, "VOLUME")
146+
if index != None and value != None:
147+
keysoundvol_dict[index] = float(value) / 100.0
147148
return True
148149
return False
149150

150151
# create dictionary of keysound pan percentages
151152
def add_keysoundpan(line):
152-
bpm_re = re.compile("#PAN[\\w\\d][\\w\\d]")
153-
re_match = bpm_re.match(line)
154-
if re_match != None and re_match.start() == 0:
155-
line_split = line.split(" ")
156-
pan_index = line_split[0][-2:]
157-
pan = float(line_split[1].strip()) / 100.0
158-
keysoundpan_dict[pan_index] = pan
153+
index, value = get_header_value(line, "PAN")
154+
if index != None and value != None:
155+
keysoundpan_dict[index] = float(value) / 100.0
159156
return True
160157
return False
161158

162159
# create dictionary of extended bpm values
163160
def add_bpmvalue(line):
164-
bpm_re = re.compile("#BPM[\\w\\d][\\w\\d]")
165-
re_match = bpm_re.match(line)
166-
if re_match != None and re_match.start() == 0:
167-
line_split = line.split(" ")
168-
bpmvalue_index = line_split[0][-2:]
169-
bpmvalue = float(line_split[1].strip())
170-
extbpm_dict[bpmvalue_index] = bpmvalue
161+
index, value = get_header_value(line, "BPM")
162+
if index != None and value != None:
163+
extbpm_dict[index] = float(value)
171164
return True
172165
return False
173166

174167
# create dictionary of stop values
175168
def add_stopvalue(line):
176-
bpm_re = re.compile("#STOP[\\w\\d][\\w\\d]")
177-
re_match = bpm_re.match(line)
178-
if re_match != None and re_match.start() == 0:
179-
line_split = line.split(" ")
180-
stopvalue_index = line_split[0][-2:]
181-
stopvalue = float(line_split[1].strip())
182-
stop_dict[stopvalue_index] = stopvalue
169+
index, value = get_header_value(line, "STOP")
170+
if index != None and value != None:
171+
stop_dict[index] = float(value)
183172
return True
184173
return False
185174

@@ -222,27 +211,26 @@ def update_data(data1, data2):
222211
def add_channel(line):
223212
global max_measure
224213
# use regular expression to match the channel format
225-
note_re = re.compile("#\\d\\d\\d\\d\\d:")
214+
note_re = re.compile("#(\\d\\d\\d[\\d\\w][\\d\\w])(:\\s*|\\s+)(\\S+)")
226215
re_match = note_re.match(line)
227216
if re_match != None and re_match.start() == 0:
228-
line_split = line.split(":")
229-
header = line_split[0][1:]
217+
header = re_match.group(1)
230218
measure = int(header[0:3])
231219
channel = header[3:5]
232-
data = line_split[1].strip()
233-
data_array = data_to_array(data)
220+
data = re_match.group(3)
234221

235222
# set the largest measure found
236223
if measure > max_measure:
237224
max_measure = measure
238225

239-
# channel with data array
240226
if parsing_mode == MODE_BMS:
241227
playable_channels = BMS_PLAYABLE_CHANNELS
242228
elif parsing_mode == MODE_DTX:
243229
playable_channels = DTX_PLAYABLE_CHANNELS
244-
230+
231+
# check for channel with data array
245232
if channel in (playable_channels + (BPM_CHANNEL, EXTBPM_CHANNEL, STOP_CHANNEL)) and data != "00":
233+
data_array = data_to_array(data)
246234
if channel == "01":
247235
# bgm tracks are special and shouldn't be merged
248236
# dictionary maps to array of arrays instead

chunkedogg_extract.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Chunked OGG Extractor v0.1
2+
# Copyright (C) 2020 shockdude
3+
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
import sys
18+
import os
19+
import time
20+
import struct
21+
22+
OGG_EXT = ".ogg"
23+
OGG_MAGIC = b"OggS"
24+
DATA_MAGIC = b"data"
25+
26+
def usage():
27+
print("Chunked OGG extractor v0.1")
28+
print('Get a playable OGG out of a "chunked vorbis" WAV')
29+
print("Usage: {} file.wav [out.ogg]".format(sys.argv[0]))
30+
time.sleep(3)
31+
sys.exit(1)
32+
33+
def find_ogg(in_filename, out_filename):
34+
print("Writing ogg {} from {}".format(out_filename, in_filename))
35+
with open(in_filename, "rb") as in_file:
36+
with open(out_filename, "wb") as out_file:
37+
buf = in_file.read(4)
38+
while len(buf) == 4:
39+
if buf != OGG_MAGIC:
40+
# look for the OggS magic keyword
41+
buf = buf[1:] + in_file.read(1)
42+
else: # found OggS
43+
page_data = buf
44+
page_data += in_file.read(2) # stream structure, header type flag
45+
page_data += in_file.read(8) # absolute granule position
46+
stream_serial_number = in_file.read(4)
47+
page_data += stream_serial_number
48+
page_data += in_file.read(8) # page sequence number, page checksum
49+
50+
# count number of segments in oggs page
51+
num_segments_byte = in_file.read(1)
52+
page_data += num_segments_byte
53+
num_segments = int.from_bytes(num_segments_byte, "little")
54+
55+
# count lengths of segments in oggs page
56+
total_segments_length = 0
57+
for i in range(num_segments):
58+
segment_length_byte = in_file.read(1)
59+
page_data += segment_length_byte
60+
total_segments_length += int.from_bytes(segment_length_byte, "little")
61+
62+
page_data += in_file.read(total_segments_length)
63+
# skip page if the serial number is all Fs
64+
if stream_serial_number != b"\xff\xff\xff\xff":
65+
out_file.write(page_data)
66+
67+
# move through the loop again
68+
buf = in_file.read(4)
69+
70+
def main():
71+
if len(sys.argv) < 2:
72+
usage()
73+
else:
74+
in_filename = sys.argv[1]
75+
in_file, in_ext = os.path.splitext(in_filename)
76+
if len(sys.argv) > 2:
77+
out_filename = sys.argv[2]
78+
else:
79+
out_filename = os.path.splitext(os.path.basename(in_file))[0] + OGG_EXT
80+
find_ogg(in_filename, out_filename)
81+
82+
if __name__ == "__main__":
83+
main()

0 commit comments

Comments
 (0)