├── README.md
├── build.xml
├── lib
├── jackson-annotations.jar
├── jackson-core.jar
└── jackson-databind.jar
└── src
└── bms
└── model
├── BMSDecoder.java
├── BMSGenerator.java
├── BMSModel.java
├── BMSModelUtils.java
├── BMSONDecoder.java
├── ChartDecoder.java
├── ChartInformation.java
├── DecodeLog.java
├── EventLane.java
├── Lane.java
├── Layer.java
├── LongNote.java
├── MineNote.java
├── Mode.java
├── NormalNote.java
├── Note.java
├── Section.java
├── TimeLine.java
└── bmson
├── BGA.java
├── BGAHeader.java
├── BGASequence.java
├── BMSInfo.java
├── BMSONObject.java
├── BNote.java
├── BarLine.java
├── Bmson.java
├── BpmEvent.java
├── MineChannel.java
├── MineNote.java
├── Note.java
├── ScrollEvent.java
├── Sequence.java
├── SoundChannel.java
└── StopEvent.java
/README.md:
--------------------------------------------------------------------------------
1 | # jbms-parser
2 | BMS and BMSON Parser based on Java.
3 |
--------------------------------------------------------------------------------
/build.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/lib/jackson-annotations.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/exch-bms2/jbms-parser/ca7b666c7446d2775fc539f652b493f778de50a5/lib/jackson-annotations.jar
--------------------------------------------------------------------------------
/lib/jackson-core.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/exch-bms2/jbms-parser/ca7b666c7446d2775fc539f652b493f778de50a5/lib/jackson-core.jar
--------------------------------------------------------------------------------
/lib/jackson-databind.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/exch-bms2/jbms-parser/ca7b666c7446d2775fc539f652b493f778de50a5/lib/jackson-databind.jar
--------------------------------------------------------------------------------
/src/bms/model/BMSDecoder.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.io.*;
4 | import java.nio.file.Files;
5 | import java.nio.file.Path;
6 | import java.security.DigestInputStream;
7 | import java.security.MessageDigest;
8 | import java.security.NoSuchAlgorithmException;
9 | import java.util.*;
10 | import java.util.function.BiFunction;
11 | import java.util.logging.Logger;
12 |
13 | import static bms.model.DecodeLog.State.*;
14 |
15 | /**
16 | * BMSファイルをBMSModelにデコードするクラス
17 | *
18 | * @author exch
19 | */
20 | public class BMSDecoder extends ChartDecoder {
21 |
22 | final List wavlist = new ArrayList(62 * 62);
23 | private final int[] wm = new int[62 * 62];
24 |
25 | final List bgalist = new ArrayList(62 * 62);
26 | private final int[] bm = new int[62 * 62];
27 |
28 | public BMSDecoder() {
29 | this(BMSModel.LNTYPE_LONGNOTE);
30 | }
31 |
32 | public BMSDecoder(int lntype) {
33 | this.lntype = lntype;
34 | // 予約語の登録
35 | }
36 |
37 | public BMSModel decode(Path f) {
38 | Logger.getGlobal().fine("BMSファイル解析開始 :" + f.toString());
39 | try {
40 | BMSModel model = this.decode(f, Files.readAllBytes(f), f.toString().toLowerCase().endsWith(".pms"), null);
41 | if (model == null) {
42 | return null;
43 | }
44 | Logger.getGlobal().fine("BMSファイル解析完了 :" + f.toString() + " - TimeLine数:" + model.getAllTimes().length);
45 | return model;
46 | } catch (IOException e) {
47 | log.add(new DecodeLog(ERROR, "BMSファイルが見つかりません"));
48 | Logger.getGlobal().severe("BMSファイル解析中の例外 : " + e.getClass().getName() + " - " + e.getMessage());
49 | }
50 | return null;
51 | }
52 |
53 | public BMSModel decode(ChartInformation info) {
54 | try {
55 | this.lntype = info.lntype;
56 | return decode(info.path, Files.readAllBytes(info.path), info.path.toString().toLowerCase().endsWith(".pms"), info.selectedRandoms);
57 | } catch (IOException e) {
58 | log.add(new DecodeLog(ERROR, "BMSファイルが見つかりません"));
59 | Logger.getGlobal().severe("BMSファイル解析中の例外 : " + e.getClass().getName() + " - " + e.getMessage());
60 | }
61 | return null;
62 | }
63 |
64 |
65 | private final List[] lines = new List[1000];
66 |
67 | private final Map scrolltable = new TreeMap();
68 | private final Map stoptable = new TreeMap();
69 | private final Map bpmtable = new TreeMap();
70 | private final Deque randoms = new ArrayDeque();
71 | private final Deque srandoms = new ArrayDeque();
72 | private final Deque crandom = new ArrayDeque();
73 | private final Deque skip = new ArrayDeque();
74 |
75 | private static final CommandWord[] commandWords = CommandWord.values();
76 |
77 | /**
78 | * 指定したBMSファイルをモデルにデコードする
79 | *
80 | * @param data
81 | * @return
82 | */
83 | public BMSModel decode(byte[] data, boolean ispms, int[] random) {
84 | return this.decode(null, data, ispms, random);
85 | }
86 |
87 | /**
88 | * 指定したBMSファイルをモデルにデコードする
89 | *
90 | * @param data
91 | * @return
92 | */
93 | private BMSModel decode(Path path, byte[] data, boolean ispms, int[] selectedRandom) {
94 | log.clear();
95 | final long time = System.currentTimeMillis();
96 | BMSModel model = new BMSModel();
97 | scrolltable.clear();
98 | stoptable.clear();
99 | bpmtable.clear();
100 |
101 | MessageDigest md5digest, sha256digest;
102 | try {
103 | md5digest = MessageDigest.getInstance("MD5");
104 | sha256digest = MessageDigest.getInstance("SHA-256");
105 | } catch (NoSuchAlgorithmException e1) {
106 | e1.printStackTrace();
107 | return null;
108 | }
109 |
110 | int maxsec = 0;
111 | // BMS読み込み、ハッシュ値取得
112 | try (BufferedReader br = new BufferedReader(new InputStreamReader(
113 | new DigestInputStream(new DigestInputStream(new ByteArrayInputStream(data), md5digest), sha256digest),
114 | "MS932"));) {
115 | model.setMode(ispms ? Mode.POPN_9K : Mode.BEAT_5K);
116 | // Logger.getGlobal().info(
117 | // "BMSデータ読み込み時間(ms) :" + (System.currentTimeMillis() - time));
118 |
119 | String line = null;
120 | wavlist.clear();
121 | Arrays.fill(wm, -2);
122 | bgalist.clear();
123 | Arrays.fill(bm, -2);
124 | for (List l : lines) {
125 | if (l != null) {
126 | l.clear();
127 | }
128 | }
129 |
130 | randoms.clear();
131 | srandoms.clear();
132 | crandom.clear();
133 |
134 | skip.clear();
135 | while ((line = br.readLine()) != null) {
136 | if (line.length() < 2) {
137 | continue;
138 | }
139 |
140 | if(line.charAt(0) == '#') {
141 | // line = line.substring(1, line.length());
142 | // RANDOM制御系
143 | if (matchesReserveWord(line, "RANDOM")) {
144 | try {
145 | final int r = Integer.parseInt(line.substring(8).trim());
146 | randoms.add(r);
147 | if (selectedRandom != null) {
148 | crandom.add(selectedRandom[randoms.size() - 1]);
149 | } else {
150 | crandom.add((int) (Math.random() * r) + 1);
151 | srandoms.add(crandom.getLast());
152 | }
153 | } catch (NumberFormatException e) {
154 | log.add(new DecodeLog(WARNING, "#RANDOMに数字が定義されていません"));
155 | }
156 | } else if (matchesReserveWord(line, "IF")) {
157 | // RANDOM分岐開始
158 | if(!crandom.isEmpty()) {
159 | try {
160 | skip.add((crandom.getLast() != Integer.parseInt(line.substring(4).trim())));
161 | } catch (NumberFormatException e) {
162 | log.add(new DecodeLog(WARNING, "#IFに数字が定義されていません"));
163 | }
164 | } else {
165 | log.add(new DecodeLog(WARNING, "#IFに対応する#RANDOMが定義されていません"));
166 | }
167 | } else if (matchesReserveWord(line, "ENDIF")) {
168 | if (!skip.isEmpty()) {
169 | skip.removeLast();
170 | } else {
171 | log.add(new DecodeLog(WARNING, "ENDIFに対応するIFが存在しません: " + line));
172 | }
173 | } else if (matchesReserveWord(line, "ENDRANDOM")) {
174 | if (!crandom.isEmpty()) {
175 | crandom.removeLast();
176 | } else {
177 | log.add(new DecodeLog(WARNING, "ENDRANDOMに対応するRANDOMが存在しません: " + line));
178 | }
179 | } else if (skip.isEmpty() || !skip.getLast()) {
180 | final char c = line.charAt(1);
181 | final int base = model.getBase();
182 | if ('0' <= c && c <= '9' && line.length() > 6) {
183 | // line = line.toUpperCase();
184 | // 楽譜
185 | final char c2 = line.charAt(2);
186 | final char c3 = line.charAt(3);
187 | if ('0' <= c2 && c2 <= '9' && '0' <= c3 && c3 <= '9') {
188 | final int bar_index = (c - '0') * 100 + (c2 - '0') * 10 + (c3 - '0');
189 | List l = lines[bar_index];
190 | if (l == null) {
191 | l = lines[bar_index] = new ArrayList();
192 | }
193 | l.add(line);
194 | maxsec = (maxsec > bar_index) ? maxsec : bar_index;
195 | } else {
196 | log.add(new DecodeLog(WARNING, "小節に数字が定義されていません : " + line));
197 | }
198 | } else if (matchesReserveWord(line, "BPM")) {
199 | if (line.charAt(4) == ' ') {
200 | // BPMは小数点のケースがある(FREEDOM DiVE)
201 | try {
202 | final String arg = line.substring(5).trim();
203 | double bpm = Double.parseDouble(arg);
204 | if(bpm > 0) {
205 | model.setBpm(bpm);
206 | } else {
207 | log.add(new DecodeLog(WARNING, "#negative BPMはサポートされていません : " + line));
208 | }
209 | } catch (NumberFormatException e) {
210 | log.add(new DecodeLog(WARNING, "#BPMに数字が定義されていません : " + line));
211 | }
212 | } else {
213 | try {
214 | double bpm = Double.parseDouble(line.substring(7).trim());
215 | if(bpm > 0) {
216 | if(base == 62) {
217 | bpmtable.put(ChartDecoder.parseInt62(line, 4), bpm);
218 | } else {
219 | bpmtable.put(ChartDecoder.parseInt36(line, 4), bpm);
220 | }
221 | } else {
222 | log.add(new DecodeLog(WARNING, "#negative BPMはサポートされていません : " + line));
223 | }
224 | } catch (NumberFormatException e) {
225 | log.add(new DecodeLog(WARNING, "#BPMxxに数字が定義されていません : " + line));
226 | }
227 | }
228 | } else if (matchesReserveWord(line, "WAV")) {
229 | // 音源ファイル
230 | if (line.length() >= 8) {
231 | try {
232 | final String file_name = line.substring(7).trim().replace('\\', '/');
233 | if(base == 62) {
234 | wm[ChartDecoder.parseInt62(line, 4)] = wavlist.size();
235 | } else {
236 | wm[ChartDecoder.parseInt36(line, 4)] = wavlist.size();
237 | }
238 | wavlist.add(file_name);
239 | } catch (NumberFormatException e) {
240 | log.add(new DecodeLog(WARNING, "#WAVxxは不十分な定義です : " + line));
241 | }
242 | } else {
243 | log.add(new DecodeLog(WARNING, "#WAVxxは不十分な定義です : " + line));
244 | }
245 | } else if (matchesReserveWord(line, "BMP")) {
246 | // BGAファイル
247 | if (line.length() >= 8) {
248 | try {
249 | final String file_name = line.substring(7).trim().replace('\\', '/');
250 | if(base == 62) {
251 | bm[ChartDecoder.parseInt62(line, 4)] = bgalist.size();
252 | } else {
253 | bm[ChartDecoder.parseInt36(line, 4)] = bgalist.size();
254 | }
255 | bgalist.add(file_name);
256 | } catch (NumberFormatException e) {
257 | log.add(new DecodeLog(WARNING, "#BMPxxは不十分な定義です : " + line));
258 | }
259 | } else {
260 | log.add(new DecodeLog(WARNING, "#BMPxxは不十分な定義です : " + line));
261 | }
262 | } else if (matchesReserveWord(line, "STOP")) {
263 | if (line.length() >= 9) {
264 | try {
265 | double stop = Double.parseDouble(line.substring(8).trim()) / 192;
266 | if(stop < 0) {
267 | stop = Math.abs(stop);
268 | log.add(new DecodeLog(WARNING, "#negative STOPはサポートされていません : " + line));
269 | }
270 | if(base == 62) {
271 | stoptable.put(ChartDecoder.parseInt62(line, 5), stop);
272 | } else {
273 | stoptable.put(ChartDecoder.parseInt36(line, 5), stop);
274 | }
275 | } catch (NumberFormatException e) {
276 | log.add(new DecodeLog(WARNING, "#STOPxxに数字が定義されていません : " + line));
277 | }
278 | } else {
279 | log.add(new DecodeLog(WARNING, "#STOPxxは不十分な定義です : " + line));
280 | }
281 | } else if (matchesReserveWord(line, "SCROLL")) {
282 | if (line.length() >= 11) {
283 | try {
284 | double scroll = Double.parseDouble(line.substring(10).trim());
285 | if(base == 62) {
286 | scrolltable.put(ChartDecoder.parseInt62(line, 7), scroll);
287 | } else {
288 | scrolltable.put(ChartDecoder.parseInt36(line, 7), scroll);
289 | }
290 | } catch (NumberFormatException e) {
291 | log.add(new DecodeLog(WARNING, "#SCROLLxxに数字が定義されていません : " + line));
292 | }
293 | } else {
294 | log.add(new DecodeLog(WARNING, "#SCROLLxxは不十分な定義です : " + line));
295 | }
296 | } else {
297 | for (CommandWord cw : commandWords) {
298 | if (line.length() > cw.name().length() + 2 && matchesReserveWord(line, cw.name())) {
299 | DecodeLog log = cw.function.apply(model, line.substring(cw.name().length() + 2).trim());
300 | if (log != null) {
301 | this.log.add(log);
302 | Logger.getGlobal().warning(model.getTitle() + " - " + log.getMessage() + " : " + line);
303 | }
304 | break;
305 | }
306 | }
307 | }
308 | }
309 | } else if(line.charAt(0) == '%') {
310 | final int index = line.indexOf(' ');
311 | if(index > 0 && line.length() > index + 1) {
312 | model.getValues().put(line.substring(1, index), line.substring(index + 1));
313 | }
314 | } else if(line.charAt(0) == '@') {
315 | final int index = line.indexOf(' ');
316 | if(index > 0 && line.length() > index + 1) {
317 | model.getValues().put(line.substring(1, index), line.substring(index + 1));
318 | }
319 | }
320 | }
321 |
322 | model.setWavList(wavlist.toArray(new String[wavlist.size()]));
323 | model.setBgaList(bgalist.toArray(new String[bgalist.size()]));
324 |
325 | Section prev = null;
326 | Section[] sections = new Section[maxsec + 1];
327 | for (int i = 0; i <= maxsec; i++) {
328 | sections[i] = new Section(model, prev, lines[i] != null ? lines[i] : Collections.EMPTY_LIST, bpmtable,
329 | stoptable, scrolltable, log);
330 | prev = sections[i];
331 | }
332 |
333 | final TreeMap timelines = new TreeMap();
334 | final List[] lnlist = new List[model.getMode().key];
335 | LongNote[] lnendstatus = new LongNote[model.getMode().key];
336 | final TimeLine basetl = new TimeLine(0, 0, model.getMode().key);
337 | basetl.setBPM(model.getBpm());
338 | timelines.put(0.0, new TimeLineCache(0.0, basetl));
339 | for (Section section : sections) {
340 | section.makeTimeLines(wm, bm, timelines, lnlist, lnendstatus);
341 | }
342 | // Logger.getGlobal().info(
343 | // "Section生成時間(ms) :" + (System.currentTimeMillis() - time));
344 | TimeLine[] tl = new TimeLine[timelines.size()];
345 | int tlcount = 0;
346 | for(TimeLineCache tlc : timelines.values()) {
347 | tl[tlcount] = tlc.timeline;
348 | tlcount++;
349 | }
350 | model.setAllTimeLine(tl);
351 |
352 | if(tl[0].getBPM() == 0) {
353 | log.add(new DecodeLog(ERROR, "開始BPMが定義されていないため、BMS解析に失敗しました"));
354 | Logger.getGlobal().severe(path + ":BMSファイル解析失敗: 開始BPMが定義されていません");
355 | return null;
356 | }
357 |
358 | for (int i = 0; i < lnendstatus.length; i++) {
359 | if (lnendstatus[i] != null) {
360 | log.add(new DecodeLog(WARNING, "曲の終端までにLN終端定義されていないLNがあります。lane:" + (i + 1)));
361 | if(lnendstatus[i].getSection() != Double.MIN_VALUE) {
362 | timelines.get(lnendstatus[i].getSection()).timeline.setNote(i, null);
363 | }
364 | }
365 | }
366 |
367 | if (model.getTotalType() != BMSModel.TotalType.BMS) {
368 | log.add(new DecodeLog(WARNING, "TOTALが未定義です"));
369 | }
370 | if (model.getTotal() <= 60.0) {
371 | log.add(new DecodeLog(WARNING, "TOTAL値が少なすぎます"));
372 | }
373 | if (tl.length > 0) {
374 | if (tl[tl.length - 1].getTime() >= model.getLastTime() + 30000) {
375 | log.add(new DecodeLog(WARNING, "最後のノート定義から30秒以上の余白があります"));
376 | }
377 | }
378 | if (model.getPlayer() > 1 && (model.getMode() == Mode.BEAT_5K || model.getMode() == Mode.BEAT_7K)) {
379 | log.add(new DecodeLog(WARNING, "#PLAYER定義が2以上にもかかわらず2P側のノーツ定義が一切ありません"));
380 | }
381 | if (model.getPlayer() == 1 && (model.getMode() == Mode.BEAT_10K || model.getMode() == Mode.BEAT_14K)) {
382 | log.add(new DecodeLog(WARNING, "#PLAYER定義が1にもかかわらず2P側のノーツ定義が存在します"));
383 | }
384 | model.setMD5(convertHexString(md5digest.digest()));
385 | model.setSHA256(convertHexString(sha256digest.digest()));
386 | log.add(new DecodeLog(INFO, "#PLAYER定義が1にもかかわらず2P側のノーツ定義が存在します"));
387 | Logger.getGlobal().fine("BMSデータ解析時間(ms) :" + (System.currentTimeMillis() - time));
388 |
389 | if (selectedRandom == null) {
390 | selectedRandom = new int[srandoms.size()];
391 | final Iterator ri = srandoms.iterator();
392 | for (int i = 0; i < selectedRandom.length; i++) {
393 | selectedRandom[i] = ri.next();
394 | }
395 | }
396 |
397 | model.setChartInformation(new ChartInformation(path, lntype, selectedRandom));
398 | printLog(path);
399 | return model;
400 | } catch (IOException e) {
401 | log.add(new DecodeLog(ERROR, "BMSファイルへのアクセスに失敗しました"));
402 | Logger.getGlobal()
403 | .severe(path + ":BMSファイル解析失敗: " + e.getClass().getName() + " - " + e.getMessage());
404 | } catch (Exception e) {
405 | log.add(new DecodeLog(ERROR, "何らかの異常によりBMS解析に失敗しました"));
406 | Logger.getGlobal()
407 | .severe(path + ":BMSファイル解析失敗: " + e.getClass().getName() + " - " + e.getMessage());
408 | e.printStackTrace();
409 | }
410 | return null;
411 | }
412 |
413 | private boolean matchesReserveWord(String line, String s) {
414 | final int len = s.length();
415 | if (line.length() <= len) {
416 | return false;
417 | }
418 | for (int i = 0; i < len; i++) {
419 | final char c = line.charAt(i + 1);
420 | final char c2 = s.charAt(i);
421 | if (c != c2 && c != c2 + 32) {
422 | return false;
423 | }
424 | }
425 | return true;
426 | }
427 |
428 | /**
429 | * バイトデータを16進数文字列表現に変換する
430 | *
431 | * @param data
432 | * バイトデータ
433 | * @returnバイトデータの16進数文字列表現
434 | */
435 | public static String convertHexString(byte[] data) {
436 | final StringBuilder sb = new StringBuilder(data.length * 2);
437 | for (byte b : data) {
438 | sb.append(Character.forDigit(b >> 4 & 0xf, 16));
439 | sb.append(Character.forDigit(b & 0xf, 16));
440 | }
441 | return sb.toString();
442 | }
443 | }
444 |
445 | /**
446 | * 予約語
447 | *
448 | * @author exch
449 | */
450 | enum CommandWord {
451 |
452 | PLAYER ((model, arg) -> {
453 | try {
454 | final int player = Integer.parseInt(arg);
455 | // TODO playerの許容幅は?
456 | if (player >= 1 && player < 3) {
457 | model.setPlayer(player);
458 | } else {
459 | return new DecodeLog(WARNING, "#PLAYERに規定外の数字が定義されています : " + player);
460 | }
461 | } catch (NumberFormatException e) {
462 | return new DecodeLog(WARNING, "#PLAYERに数字が定義されていません");
463 | }
464 | return null;
465 | }),
466 | GENRE ((model, arg) -> {
467 | model.setGenre(arg);
468 | return null;
469 | }),
470 | TITLE ((model, arg) -> {
471 | model.setTitle(arg);
472 | return null;
473 | }),
474 | SUBTITLE ((model, arg) -> {
475 | model.setSubTitle(arg);
476 | return null;
477 | }),
478 | ARTIST ((model, arg) -> {
479 | model.setArtist(arg);
480 | return null;
481 | }),
482 | SUBARTIST ((model, arg) -> {
483 | model.setSubArtist(arg);
484 | return null;
485 | }),
486 | PLAYLEVEL ((model, arg) -> {
487 | model.setPlaylevel(arg);
488 | return null;
489 | }),
490 | RANK ((model, arg) -> {
491 | try {
492 | final int rank = Integer.parseInt(arg);
493 | if (rank >= 0 && rank < 5) {
494 | model.setJudgerank(rank);
495 | model.setJudgerankType(BMSModel.JudgeRankType.BMS_RANK);
496 | } else {
497 | return new DecodeLog(WARNING, "#RANKに規定外の数字が定義されています : " + rank);
498 | }
499 | } catch (NumberFormatException e) {
500 | return new DecodeLog(WARNING, "#RANKに数字が定義されていません");
501 | }
502 | return null;
503 | }),
504 | DEFEXRANK ((model, arg) -> {
505 | try {
506 | final int rank = Integer.parseInt(arg);
507 | if (rank >= 1) {
508 | model.setJudgerank(rank);
509 | model.setJudgerankType(BMSModel.JudgeRankType.BMS_DEFEXRANK);
510 | } else {
511 | return new DecodeLog(WARNING, "#DEFEXRANK 1以下はサポートしていません" + rank);
512 | }
513 | } catch (NumberFormatException e) {
514 | return new DecodeLog(WARNING, "#DEFEXRANKに数字が定義されていません");
515 | }
516 | return null;
517 | }),
518 | TOTAL ((model, arg) -> {
519 | try {
520 | final double total = Double.parseDouble(arg);
521 | if(total > 0) {
522 | model.setTotal(total);
523 | model.setTotalType(BMSModel.TotalType.BMS);
524 | } else {
525 | return new DecodeLog(WARNING, "#TOTALが0以下です");
526 | }
527 | } catch (NumberFormatException e) {
528 | return new DecodeLog(WARNING, "#TOTALに数字が定義されていません");
529 | }
530 | return null;
531 | }),
532 | VOLWAV ((model, arg) -> {
533 | try {
534 | model.setVolwav(Integer.parseInt(arg));
535 | } catch (NumberFormatException e) {
536 | return new DecodeLog(WARNING, "#VOLWAVに数字が定義されていません");
537 | }
538 | return null;
539 | }),
540 | STAGEFILE ((model, arg) -> {
541 | model.setStagefile(arg.replace('\\', '/'));
542 | return null;
543 | }),
544 | BACKBMP ((model, arg) -> {
545 | model.setBackbmp(arg.replace('\\', '/'));
546 | return null;
547 | }),
548 | PREVIEW ((model, arg) -> {
549 | model.setPreview(arg.replace('\\', '/'));
550 | return null;
551 | }),
552 | LNOBJ ((model, arg) -> {
553 | try {
554 | if (model.getBase() == 62) {
555 | model.setLnobj(ChartDecoder.parseInt62(arg, 0));
556 | } else {
557 | model.setLnobj(Integer.parseInt(arg.toUpperCase(), 36));
558 | }
559 | } catch (NumberFormatException e) {
560 | return new DecodeLog(WARNING, "#LNOBJに数字が定義されていません");
561 | }
562 | return null;
563 | }),
564 | LNMODE ((model, arg) -> {
565 | try {
566 | int lnmode = Integer.parseInt(arg);
567 | if(lnmode < 0 || lnmode > 3) {
568 | return new DecodeLog(WARNING, "#LNMODEに無効な数字が定義されています");
569 | }
570 | model.setLnmode(lnmode);
571 | } catch (NumberFormatException e) {
572 | return new DecodeLog(WARNING, "#LNMODEに数字が定義されていません");
573 | }
574 | return null;
575 | }),
576 | DIFFICULTY ((model, arg) -> {
577 | try {
578 | model.setDifficulty(Integer.parseInt(arg));
579 | } catch (NumberFormatException e) {
580 | return new DecodeLog(WARNING, "#DIFFICULTYに数字が定義されていません");
581 | }
582 | return null;
583 | }),
584 | BANNER ((model, arg) -> {
585 | model.setBanner(arg.replace('\\', '/'));
586 | return null;
587 | }),
588 | COMMENT ((model, arg) -> {
589 | // TODO 未実装
590 | return null;
591 | }),
592 | BASE ((model, arg) -> {
593 | try {
594 | int base = Integer.parseInt(arg);
595 | if(base != 62) {
596 | return new DecodeLog(WARNING, "#BASEに無効な数字が定義されています");
597 | }
598 | model.setBase(base);
599 | } catch (NumberFormatException e) {
600 | return new DecodeLog(WARNING, "#BASEに数字が定義されていません");
601 | }
602 | return null;
603 | });
604 |
605 | public final BiFunction function;
606 |
607 | private CommandWord(BiFunction function) {
608 | this.function = function;
609 | }
610 | }
611 |
612 | /**
613 | * 予約語
614 | *
615 | * @author exch
616 | */
617 | enum OptionWord {
618 |
619 | URL ((model, arg) -> {
620 | // TODO 未実装
621 | return null;
622 | });
623 |
624 | public final BiFunction function;
625 |
626 | private OptionWord(BiFunction function) {
627 | this.function = function;
628 | }
629 | }
630 |
--------------------------------------------------------------------------------
/src/bms/model/BMSGenerator.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | public class BMSGenerator {
4 |
5 | private int[] random;
6 |
7 | private byte[] data;
8 |
9 | private boolean ispms;
10 |
11 | public BMSGenerator(byte[] data, boolean ispms, int[] random) {
12 | this.data = data;
13 | this.random = random;
14 | this.ispms = ispms;
15 | }
16 |
17 | public BMSModel generate(int[] random) {
18 | BMSDecoder decoder = new BMSDecoder();
19 | return decoder.decode(data, ispms, random);
20 | }
21 |
22 | public int[] getRandom() {
23 | return random;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/bms/model/BMSModel.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | /**
7 | * BMSモデル
8 | *
9 | * @author exch
10 | */
11 | public class BMSModel implements Comparable {
12 |
13 | /**
14 | * プレイヤー数
15 | */
16 | private int player;
17 | /**
18 | * 使用するキー数
19 | */
20 | private Mode mode;
21 | /**
22 | * タイトル名
23 | */
24 | private String title = "";
25 | /**
26 | * サブタイトル名
27 | */
28 | private String subTitle = "";
29 | /**
30 | * ジャンル名
31 | */
32 | private String genre = "";
33 | /**
34 | * アーティスト
35 | */
36 | private String artist = "";
37 | /**
38 | * サブアーティスト
39 | */
40 | private String subartist = "";
41 |
42 | /**
43 | * バナー
44 | */
45 | private String banner = "";
46 | /**
47 | * ステージ画像
48 | */
49 | private String stagefile = "";
50 | private String backbmp = "";
51 | private String preview = "";
52 | /**
53 | * 標準BPM
54 | */
55 | private double bpm;
56 | /**
57 | * 表記レベル
58 | */
59 | private String playlevel = "";
60 | /**
61 | * 表記ランク(0:beginner, 1:normal, 2:hyper, 3:another, 4:insane)
62 | */
63 | private int difficulty = 0;
64 | /**
65 | * 判定ランク
66 | */
67 | private int judgerank = 2;
68 | /**
69 | * 判定ランクのタイプ
70 | */
71 | private JudgeRankType judgerankType = JudgeRankType.BMS_RANK;
72 | /**
73 | * TOTAL値
74 | */
75 | private double total = 100;
76 | /**
77 | * TOTALのタイプ
78 | */
79 | private TotalType totalType = TotalType.BMSON;
80 | /**
81 | * 標準ボリューム
82 | */
83 | private int volwav;
84 | /**
85 | * MD5値
86 | */
87 | private String md5 = "";
88 | /**
89 | * SHA256値
90 | */
91 | private String sha256 = "";
92 | /**
93 | * WAV定義のIDとファイル名のマップ
94 | */
95 | private String[] wavmap = new String[0];
96 | /**
97 | * BGA定義のIDとファイル名のマップ
98 | */
99 | private String[] bgamap = new String[0];
100 | /**
101 | * 進数指定
102 | */
103 | private int base = 36;
104 |
105 | private int lnmode = LongNote.TYPE_UNDEFINED;
106 |
107 | private int lnobj = -1;
108 |
109 | public static final int LNTYPE_LONGNOTE = 0;
110 | public static final int LNTYPE_CHARGENOTE = 1;
111 | public static final int LNTYPE_HELLCHARGENOTE = 2;
112 |
113 | /**
114 | * 時間とTimeLineのマッピング
115 | */
116 | private TimeLine[] timelines = new TimeLine[0];
117 |
118 | private ChartInformation info;
119 |
120 | private Map values = new HashMap<>();
121 |
122 | public BMSModel() {
123 | }
124 |
125 | public int getPlayer() {
126 | return player;
127 | }
128 |
129 | public void setPlayer(int player) {
130 | this.player = player;
131 | }
132 |
133 | public String getTitle() {
134 | return title;
135 | }
136 |
137 | public void setTitle(String title) {
138 | if (title == null) {
139 | this.title = "";
140 | return;
141 | }
142 | this.title = title;
143 | }
144 |
145 | public String getSubTitle() {
146 | return subTitle;
147 | }
148 |
149 | public void setSubTitle(String subTitle) {
150 | if (subTitle == null) {
151 | this.subTitle = "";
152 | return;
153 | }
154 | this.subTitle = subTitle;
155 | }
156 |
157 | public String getGenre() {
158 | return genre;
159 | }
160 |
161 | public void setGenre(String genre) {
162 | if (genre == null) {
163 | this.genre = "";
164 | return;
165 | }
166 | this.genre = genre;
167 | }
168 |
169 | public String getArtist() {
170 | return artist;
171 | }
172 |
173 | public void setArtist(String artist) {
174 | if (artist == null) {
175 | this.artist = "";
176 | return;
177 | }
178 | this.artist = artist;
179 | }
180 |
181 | public String getSubArtist() {
182 | return subartist;
183 | }
184 |
185 | public void setSubArtist(String artist) {
186 | if (artist == null) {
187 | this.subartist = "";
188 | return;
189 | }
190 | this.subartist = artist;
191 | }
192 |
193 | public void setBanner(String banner) {
194 | if (banner == null) {
195 | this.banner = "";
196 | return;
197 | }
198 | this.banner = banner;
199 | }
200 |
201 | public String getBanner() {
202 | return banner;
203 | }
204 |
205 | public double getBpm() {
206 | return bpm;
207 | }
208 |
209 | public void setBpm(double bpm) {
210 | ;
211 | this.bpm = bpm;
212 | }
213 |
214 | public String getPlaylevel() {
215 | return playlevel;
216 | }
217 |
218 | public void setPlaylevel(String playlevel) {
219 | this.playlevel = playlevel;
220 | }
221 |
222 | public int getJudgerank() {
223 | return judgerank;
224 | }
225 |
226 | public void setJudgerank(int judgerank) {
227 | this.judgerank = judgerank;
228 | }
229 |
230 | public double getTotal() {
231 | return total;
232 | }
233 |
234 | public void setTotal(double total) {
235 | this.total = total;
236 | }
237 |
238 | public int getVolwav() {
239 | return volwav;
240 | }
241 |
242 | public void setVolwav(int volwav) {
243 | this.volwav = volwav;
244 | }
245 |
246 | public double getMinBPM() {
247 | double bpm = this.getBpm();
248 | for (TimeLine time : timelines) {
249 | final double d = time.getBPM();
250 | bpm = (bpm <= d) ? bpm : d;
251 | }
252 | return bpm;
253 | }
254 |
255 | public double getMaxBPM() {
256 | double bpm = this.getBpm();
257 | for (TimeLine time : timelines) {
258 | final double d = time.getBPM();
259 | bpm = (bpm >= d) ? bpm : d;
260 | }
261 | return bpm;
262 | }
263 |
264 | public void setAllTimeLine(TimeLine[] timelines) {
265 | this.timelines = timelines;
266 | }
267 |
268 | public TimeLine[] getAllTimeLines() {
269 | return timelines;
270 | }
271 |
272 | public long[] getAllTimes() {
273 | TimeLine[] times = getAllTimeLines();
274 | long[] result = new long[times.length];
275 | for (int i = 0; i < times.length; i++) {
276 | result[i] = times[i].getTime();
277 | }
278 | return result;
279 | }
280 |
281 | public int getLastTime() {
282 | return (int) getLastMilliTime();
283 | }
284 |
285 | public long getLastMilliTime() {
286 | final int keys = mode.key;
287 | for (int i = timelines.length - 1;i >= 0;i--) {
288 | final TimeLine tl = timelines[i];
289 | for (int lane = 0; lane < keys; lane++) {
290 | if (tl.existNote(lane) || tl.getHiddenNote(lane) != null
291 | || tl.getBackGroundNotes().length > 0 || tl.getBGA() != -1
292 | || tl.getLayer() != -1) {
293 | return tl.getMilliTime();
294 | }
295 | }
296 | }
297 | return 0;
298 | }
299 |
300 | public int getLastNoteTime() {
301 | return (int) getLastNoteMilliTime();
302 | }
303 |
304 | public long getLastNoteMilliTime() {
305 | final int keys = mode.key;
306 | for (int i = timelines.length - 1;i >= 0;i--) {
307 | final TimeLine tl = timelines[i];
308 | for (int lane = 0; lane < keys; lane++) {
309 | if (tl.existNote(lane)) {
310 | return tl.getMilliTime();
311 | }
312 | }
313 | }
314 | return 0;
315 | }
316 |
317 | public int getDifficulty() {
318 | return difficulty;
319 | }
320 |
321 | public void setDifficulty(int difficulty) {
322 | this.difficulty = difficulty;
323 | }
324 |
325 | public int compareTo(BMSModel model) {
326 | return this.title.compareTo(model.title);
327 | }
328 |
329 | public String getFullTitle() {
330 | return title + (subTitle != null && subTitle.length() > 0 ? " " + subTitle : "");
331 | }
332 |
333 | public String getFullArtist() {
334 | return artist + (subartist != null && subartist.length() > 0 ? " " + subartist : "");
335 | }
336 |
337 | public void setMD5(String hash) {
338 | this.md5 = hash;
339 | }
340 |
341 | public String getMD5() {
342 | return md5;
343 | }
344 |
345 | public String getSHA256() {
346 | return sha256;
347 | }
348 |
349 | public void setSHA256(String sha256) {
350 | this.sha256 = sha256;
351 | }
352 |
353 | public void setMode(Mode mode) {
354 | this.mode = mode;
355 | for(TimeLine tl : timelines) {
356 | tl.setLaneCount(mode.key);
357 | }
358 | }
359 |
360 | public Mode getMode() {
361 | return mode;
362 | }
363 |
364 | public String[] getWavList() {
365 | return wavmap;
366 | }
367 |
368 | public void setWavList(String[] wavmap) {
369 | this.wavmap = wavmap;
370 | }
371 |
372 | public String[] getBgaList() {
373 | return bgamap;
374 | }
375 |
376 | public void setBgaList(String[] bgamap) {
377 | this.bgamap = bgamap;
378 | }
379 |
380 | public ChartInformation getChartInformation() {
381 | return info;
382 | }
383 |
384 | public void setChartInformation(ChartInformation info) {
385 | this.info = info;
386 | }
387 |
388 | public int[] getRandom() {
389 | return info != null ? info.selectedRandoms : null;
390 | }
391 |
392 | public String getPath() {
393 | return info != null && info.path != null ? info.path.toString() : null;
394 | }
395 |
396 | public int getLntype() {
397 | return info != null ? info.lntype : LNTYPE_LONGNOTE;
398 | }
399 |
400 | public String getStagefile() {
401 | return stagefile;
402 | }
403 |
404 | public void setStagefile(String stagefile) {
405 | if (stagefile == null) {
406 | this.stagefile = "";
407 | return;
408 | }
409 | this.stagefile = stagefile;
410 | }
411 |
412 | public String getBackbmp() {
413 | return backbmp;
414 | }
415 |
416 | public void setBackbmp(String backbmp) {
417 | if (backbmp == null) {
418 | this.backbmp = "";
419 | return;
420 | }
421 | this.backbmp = backbmp;
422 | }
423 |
424 | public int getTotalNotes() {
425 | return BMSModelUtils.getTotalNotes(this);
426 | }
427 |
428 | public boolean containsUndefinedLongNote() {
429 | final int keys = mode.key;
430 | for (TimeLine tl : timelines) {
431 | for (int i = 0; i < keys; i++) {
432 | if (tl.getNote(i) != null && tl.getNote(i) instanceof LongNote
433 | && ((LongNote) tl.getNote(i)).getType() == LongNote.TYPE_UNDEFINED) {
434 | return true;
435 | }
436 | }
437 | }
438 | return false;
439 | }
440 |
441 | public boolean containsLongNote() {
442 | final int keys = mode.key;
443 | for (TimeLine tl : timelines) {
444 | for (int i = 0; i < keys; i++) {
445 | if (tl.getNote(i) instanceof LongNote) {
446 | return true;
447 | }
448 | }
449 | }
450 | return false;
451 | }
452 |
453 | public boolean containsMineNote() {
454 | final int keys = mode.key;
455 | for (TimeLine tl : timelines) {
456 | for (int i = 0; i < keys; i++) {
457 | if (tl.getNote(i) instanceof MineNote) {
458 | return true;
459 | }
460 | }
461 | }
462 | return false;
463 | }
464 |
465 | public String getPreview() {
466 | return preview;
467 | }
468 |
469 | public void setPreview(String preview) {
470 | this.preview = preview;
471 | }
472 |
473 | public EventLane getEventLane() {
474 | return new EventLane(this);
475 | }
476 |
477 | public Lane[] getLanes() {
478 | Lane[] lanes = new Lane[mode.key];
479 | for(int i = 0;i < lanes.length;i++) {
480 | lanes[i] = new Lane(this, i);
481 | }
482 | return lanes;
483 | }
484 |
485 | public int getLnobj() {
486 | return lnobj;
487 | }
488 |
489 | public void setLnobj(int lnobj) {
490 | this.lnobj = lnobj;
491 | }
492 |
493 | public int getLnmode() {
494 | return lnmode;
495 | }
496 |
497 | public void setLnmode(int lnmode) {
498 | this.lnmode = lnmode;
499 | }
500 |
501 | public Map getValues() {
502 | return values;
503 | }
504 |
505 | public String toChartString() {
506 | StringBuilder sb = new StringBuilder();
507 | sb.append("JUDGERANK:" + judgerank + "\n");
508 | sb.append("TOTAL:" + total + "\n");
509 | if(lnmode != 0) {
510 | sb.append("LNMODE:" + lnmode + "\n");
511 | }
512 | double nowbpm = -Double.MIN_VALUE;
513 | StringBuilder tlsb = new StringBuilder();
514 | for(TimeLine tl : timelines) {
515 | tlsb.setLength(0);
516 | tlsb.append(tl.getTime() + ":");
517 | boolean write = false;
518 | if(nowbpm != tl.getBPM()) {
519 | nowbpm = tl.getBPM();
520 | tlsb.append("B(" + nowbpm + ")");
521 | write = true;
522 | }
523 | if(tl.getStop() != 0) {
524 | tlsb.append("S(" + tl.getStop() + ")");
525 | write = true;
526 | }
527 | if(tl.getSectionLine()) {
528 | tlsb.append("L");
529 | write = true;
530 | }
531 |
532 | tlsb.append("[");
533 | for(int lane = 0;lane < mode.key;lane++) {
534 | Note n = tl.getNote(lane);
535 | if(n instanceof NormalNote) {
536 | tlsb.append("1");
537 | write = true;
538 | } else if(n instanceof LongNote) {
539 | LongNote ln = (LongNote)n;
540 | if(!ln.isEnd()) {
541 | final char[] lnchars = {'l','L','C','H'};
542 | tlsb.append(lnchars[ln.getType()] + ln.getMilliDuration());
543 | write = true;
544 | }
545 | } else if(n instanceof MineNote) {
546 | tlsb.append("m" + ((MineNote)n).getDamage());
547 | write = true;
548 | } else {
549 | tlsb.append("0");
550 | }
551 | if(lane < mode.key - 1) {
552 | tlsb.append(",");
553 | }
554 | }
555 | tlsb.append("]\n");
556 |
557 | if(write) {
558 | sb.append(tlsb);
559 | }
560 | }
561 | return sb.toString();
562 | }
563 |
564 | public JudgeRankType getJudgerankType() {
565 | return judgerankType;
566 | }
567 |
568 | public void setJudgerankType(JudgeRankType judgerankType) {
569 | this.judgerankType = judgerankType;
570 | }
571 |
572 | public TotalType getTotalType() {
573 | return totalType;
574 | }
575 |
576 | public void setTotalType(TotalType totalType) {
577 | this.totalType = totalType;
578 | }
579 |
580 | public enum JudgeRankType {
581 | BMS_RANK, BMS_DEFEXRANK, BMSON_JUDGERANK;
582 | }
583 |
584 | public enum TotalType {
585 | BMS, BMSON;
586 | }
587 |
588 | public int getBase() {
589 | return base;
590 | }
591 |
592 | public void setBase(int base) {
593 | if (base == 62) {
594 | this.base = base;
595 | } else {
596 | this.base = 36;
597 | }
598 | return;
599 | }
600 | }
601 |
--------------------------------------------------------------------------------
/src/bms/model/BMSModelUtils.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | public class BMSModelUtils {
4 |
5 | public static final int TOTALNOTES_ALL = 0;
6 | public static final int TOTALNOTES_KEY = 1;
7 | public static final int TOTALNOTES_LONG_KEY = 2;
8 | public static final int TOTALNOTES_SCRATCH = 3;
9 | public static final int TOTALNOTES_LONG_SCRATCH = 4;
10 | public static final int TOTALNOTES_MINE = 5;
11 |
12 | /**
13 | * 総ノート数を返す。
14 | *
15 | * @return 総ノート数
16 | */
17 | public static int getTotalNotes(BMSModel model) {
18 | return getTotalNotes(model, 0, Integer.MAX_VALUE);
19 | }
20 |
21 | public static int getTotalNotes(BMSModel model, int type) {
22 | return getTotalNotes(model, 0, Integer.MAX_VALUE, type);
23 | }
24 |
25 | /**
26 | * 指定の時間範囲の総ノート数を返す
27 | *
28 | * @param start
29 | * 開始時間(ms)
30 | * @param end
31 | * 終了時間(ms)
32 | * @return 指定の時間範囲の総ノート数
33 | */
34 | public static int getTotalNotes(BMSModel model, int start, int end) {
35 | return getTotalNotes(model, start, end, TOTALNOTES_ALL);
36 | }
37 |
38 | /**
39 | * 指定の時間範囲、指定の種類のノートの総数を返す
40 | *
41 | * @param start
42 | * 開始時間(ms)
43 | * @param end
44 | * 終了時間(ms)
45 | * @param type
46 | * ノートの種類
47 | * @return 指定の時間範囲、指定の種類のの総ノート数
48 | */
49 | public static int getTotalNotes(BMSModel model, int start, int end, int type) {
50 | return getTotalNotes(model, start, end, type, 0);
51 | }
52 |
53 | /**
54 | * 指定の時間範囲、指定の種類、指定のプレイサイドのノートの総数を返す
55 | *
56 | * @param start
57 | * 開始時間(ms)
58 | * @param end
59 | * 終了時間(ms)
60 | * @param type
61 | * ノートの種類
62 | * @param side
63 | * プレイサイド(0:両方, 1:1P側, 2:2P側)
64 | * @return 指定の時間範囲、指定の種類のの総ノート数
65 | */
66 | public static int getTotalNotes(BMSModel model, int start, int end, int type, int side) {
67 | Mode mode = model.getMode();
68 | if(mode.player == 1 && side == 2) {
69 | return 0;
70 | }
71 | int[] slane = new int[mode.scratchKey.length / (side == 0 ? 1 : mode.player)];
72 | for(int i = (side == 2 ? slane.length: 0), index = 0;index < slane.length;i++) {
73 | slane[index] = mode.scratchKey[i];
74 | index++;
75 | }
76 | int[] nlane = new int[(mode.key - mode.scratchKey.length) / (side == 0 ? 1 : mode.player)];
77 | for(int i = 0, index = 0;index < nlane.length;i++) {
78 | if(!mode.isScratchKey(i)) {
79 | nlane[index] = i;
80 | index++;
81 | }
82 | }
83 |
84 | int count = 0;
85 | for (TimeLine tl : model.getAllTimeLines()) {
86 | if (tl.getTime() >= start && tl.getTime() < end) {
87 | switch (type) {
88 | case TOTALNOTES_ALL:
89 | count += tl.getTotalNotes(model.getLntype());
90 | break;
91 | case TOTALNOTES_KEY:
92 | for (int lane : nlane) {
93 | if (tl.existNote(lane) && (tl.getNote(lane) instanceof NormalNote)) {
94 | count++;
95 | }
96 | }
97 | break;
98 | case TOTALNOTES_LONG_KEY:
99 | for (int lane : nlane) {
100 | if (tl.existNote(lane) && (tl.getNote(lane) instanceof LongNote)) {
101 | LongNote ln = (LongNote) tl.getNote(lane);
102 | if (ln.getType() == LongNote.TYPE_CHARGENOTE
103 | || ln.getType() == LongNote.TYPE_HELLCHARGENOTE
104 | || (ln.getType() == LongNote.TYPE_UNDEFINED && model.getLntype() != BMSModel.LNTYPE_LONGNOTE)
105 | || !ln.isEnd()) {
106 | count++;
107 | }
108 | }
109 | }
110 | break;
111 | case TOTALNOTES_SCRATCH:
112 | for (int lane : slane) {
113 | if (tl.existNote(lane) && (tl.getNote(lane) instanceof NormalNote)) {
114 | count++;
115 | }
116 | }
117 | break;
118 | case TOTALNOTES_LONG_SCRATCH:
119 | for (int lane : slane) {
120 | final Note n = tl.getNote(lane);
121 | if (n instanceof LongNote) {
122 | final LongNote ln = (LongNote) n;
123 | if (ln.getType() == LongNote.TYPE_CHARGENOTE
124 | || ln.getType() == LongNote.TYPE_HELLCHARGENOTE
125 | || (ln.getType() == LongNote.TYPE_UNDEFINED && model.getLntype() != BMSModel.LNTYPE_LONGNOTE)
126 | || !ln.isEnd()) {
127 | count++;
128 | }
129 | }
130 | }
131 | break;
132 | case TOTALNOTES_MINE:
133 | for (int lane : nlane) {
134 | if (tl.existNote(lane) && (tl.getNote(lane) instanceof MineNote)) {
135 | count++;
136 | }
137 | }
138 | for (int lane : slane) {
139 | if (tl.existNote(lane) && (tl.getNote(lane) instanceof MineNote)) {
140 | count++;
141 | }
142 | }
143 | break;
144 | }
145 | }
146 | }
147 | return count;
148 | }
149 |
150 | public double getAverageNotesPerTime(BMSModel model, int start, int end) {
151 | return (double) this.getTotalNotes(model, start, end) * 1000 / (end - start);
152 | }
153 |
154 | public static void changeFrequency(BMSModel model, float freq) {
155 | model.setBpm(model.getBpm() * freq);
156 | for (TimeLine tl : model.getAllTimeLines()) {
157 | tl.setBPM(tl.getBPM() * freq);
158 | tl.setStop((long) (tl.getMicroStop() / freq));
159 | tl.setMicroTime((long) (tl.getMicroTime() / freq));
160 | }
161 | }
162 |
163 | public static double getMaxNotesPerTime(BMSModel model, int range) {
164 | int maxnotes = 0;
165 | TimeLine[] tl = model.getAllTimeLines();
166 | for (int i = 0; i < tl.length; i++) {
167 | int notes = 0;
168 | for (int j = i; j < tl.length && tl[j].getTime() < tl[i].getTime() + range; j++) {
169 | notes += tl[j].getTotalNotes(model.getLntype());
170 | }
171 | maxnotes = (maxnotes < notes) ? notes : maxnotes;
172 | }
173 | return maxnotes;
174 | }
175 |
176 | public static long setStartNoteTime(BMSModel model, long starttime) {
177 | long marginTime = 0;
178 | for (TimeLine tl : model.getAllTimeLines()) {
179 | if(tl.getMilliTime() >= starttime) {
180 | break;
181 | }
182 | if(tl.existNote()) {
183 | marginTime = starttime - tl.getMilliTime();
184 | break;
185 | }
186 | }
187 |
188 | if(marginTime > 0) {
189 | double marginSection = marginTime * model.getAllTimeLines()[0].getBPM() / 240000;
190 | for (TimeLine tl : model.getAllTimeLines()) {
191 | tl.setSection(tl.getSection() + marginSection);
192 | tl.setMicroTime(tl.getMicroTime() + marginTime * 1000);
193 | }
194 |
195 | TimeLine[] tl2 = new TimeLine[model.getAllTimeLines().length + 1];
196 | tl2[0] = new TimeLine(0, 0, model.getMode().key);
197 | tl2[0].setBPM(model.getBpm());
198 | for(int i = 1;i < tl2.length;i++) {
199 | tl2[i] = model.getAllTimeLines()[i - 1];
200 | }
201 | model.setAllTimeLine(tl2);
202 | }
203 |
204 | return marginTime;
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/bms/model/BMSONDecoder.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.io.*;
4 | import java.nio.file.Files;
5 | import java.nio.file.Path;
6 | import java.security.DigestInputStream;
7 | import java.security.MessageDigest;
8 | import java.security.NoSuchAlgorithmException;
9 | import java.util.*;
10 | import java.util.Map.Entry;
11 | import java.util.logging.Logger;
12 | import java.util.stream.Collectors;
13 |
14 | import static bms.model.DecodeLog.State.*;
15 |
16 | import bms.model.Layer.EventType;
17 | import bms.model.bmson.*;
18 | import bms.model.bmson.Note;
19 |
20 | import com.fasterxml.jackson.databind.ObjectMapper;
21 |
22 | /**
23 | * bmsonデコーダー
24 | *
25 | * @author exch
26 | */
27 | public class BMSONDecoder extends ChartDecoder {
28 |
29 | private final ObjectMapper mapper = new ObjectMapper();
30 |
31 | private BMSModel model;
32 |
33 | private final TreeMap tlcache = new TreeMap();
34 |
35 | public BMSONDecoder(int lntype) {
36 | this.lntype = lntype;
37 | }
38 |
39 | public BMSModel decode(ChartInformation info) {
40 | this.lntype = info.lntype;
41 | return decode(info.path);
42 | }
43 |
44 | public BMSModel decode(Path f) {
45 | Logger.getGlobal().fine("BMSONファイル解析開始 :" + f.toString());
46 | log.clear();
47 | tlcache.clear();
48 | final long currnttime = System.currentTimeMillis();
49 | // BMS読み込み、ハッシュ値取得
50 | model = new BMSModel();
51 | Bmson bmson = null;
52 | try {
53 | MessageDigest digest = MessageDigest.getInstance("SHA-256");
54 | bmson = mapper.readValue(new DigestInputStream(new BufferedInputStream(Files.newInputStream(f)), digest),
55 | Bmson.class);
56 | model.setSHA256(BMSDecoder.convertHexString(digest.digest()));
57 | } catch (NoSuchAlgorithmException | IOException e) {
58 | e.printStackTrace();
59 | return null;
60 | }
61 |
62 | model.setTitle(bmson.info.title);
63 | model.setSubTitle((bmson.info.subtitle != null ? bmson.info.subtitle : "")
64 | + (bmson.info.subtitle != null && bmson.info.subtitle.length() > 0 && bmson.info.chart_name != null
65 | && bmson.info.chart_name.length() > 0 ? " " : "")
66 | + (bmson.info.chart_name != null && bmson.info.chart_name.length() > 0
67 | ? "[" + bmson.info.chart_name + "]" : ""));
68 | model.setArtist(bmson.info.artist);
69 | StringBuilder subartist = new StringBuilder();
70 | for (String s : bmson.info.subartists) {
71 | subartist.append((subartist.length() > 0 ? "," : "") + s);
72 | }
73 | model.setSubArtist(subartist.toString());
74 | model.setGenre(bmson.info.genre);
75 |
76 | if (bmson.info.judge_rank < 0) {
77 | log.add(new DecodeLog(WARNING, "judge_rankが0以下です。judge_rank = " + bmson.info.judge_rank));
78 | } else if (bmson.info.judge_rank < 5) {
79 | model.setJudgerank(bmson.info.judge_rank);
80 | log.add(new DecodeLog(WARNING, "judge_rankの定義が仕様通りでない可能性があります。judge_rank = " + bmson.info.judge_rank));
81 | model.setJudgerankType(BMSModel.JudgeRankType.BMS_RANK);
82 | } else {
83 | model.setJudgerank(bmson.info.judge_rank);
84 | model.setJudgerankType(BMSModel.JudgeRankType.BMSON_JUDGERANK);
85 | }
86 |
87 | if(bmson.info.total > 0) {
88 | model.setTotal(bmson.info.total);
89 | model.setTotalType(BMSModel.TotalType.BMSON);
90 | } else {
91 | log.add(new DecodeLog(WARNING, "totalが0以下です。total = " + bmson.info.total));
92 | }
93 |
94 | model.setBpm(bmson.info.init_bpm);
95 | model.setPlaylevel(String.valueOf(bmson.info.level));
96 | final Mode mode = Mode.getMode(bmson.info.mode_hint);
97 | if(mode != null) {
98 | model.setMode(mode);
99 | } else {
100 | log.add(new DecodeLog(WARNING, "非対応のmode_hintです。mode_hint = " + bmson.info.mode_hint));
101 | model.setMode(Mode.BEAT_7K);
102 | }
103 | if (bmson.info.ln_type > 0 && bmson.info.ln_type <= 3) {
104 | model.setLnmode(bmson.info.ln_type);
105 | }
106 | final int[] keyassign;
107 | switch (model.getMode()) {
108 | case BEAT_5K:
109 | keyassign = new int[] { 0, 1, 2, 3, 4, -1, -1, 5 };
110 | break;
111 | case BEAT_10K:
112 | keyassign = new int[] { 0, 1, 2, 3, 4, -1, -1, 5, 6, 7, 8, 9, 10, -1, -1, 11 };
113 | break;
114 | default:
115 | keyassign = new int[model.getMode().key];
116 | for (int i = 0; i < keyassign.length; i++) {
117 | keyassign[i] = i;
118 | }
119 | }
120 | List[] lnlist = new List[model.getMode().key];
121 | Map lnup = new HashMap();
122 |
123 | model.setBanner(bmson.info.banner_image);
124 | model.setBackbmp(bmson.info.back_image);
125 | model.setStagefile(bmson.info.eyecatch_image);
126 | model.setPreview(bmson.info.preview_music);
127 | final TimeLine basetl = new TimeLine(0, 0, model.getMode().key);
128 | basetl.setBPM(model.getBpm());
129 | tlcache.put(0, new TimeLineCache(0.0, basetl));
130 |
131 | if (bmson.bpm_events == null) {
132 | bmson.bpm_events = new BpmEvent[0];
133 | }
134 | if (bmson.stop_events == null) {
135 | bmson.stop_events = new StopEvent[0];
136 | }
137 | if (bmson.scroll_events == null) {
138 | bmson.scroll_events = new ScrollEvent[0];
139 | }
140 |
141 | final double resolution = bmson.info.resolution > 0 ? bmson.info.resolution * 4 : 960;
142 | final Comparator comparator = (n1,n2) -> (n1.y - n2.y);
143 |
144 | int bpmpos = 0;
145 | int stoppos = 0;
146 | int scrollpos = 0;
147 | // bpmNotes, stopNotes処理
148 | Arrays.sort(bmson.bpm_events, comparator);
149 | Arrays.sort(bmson.stop_events, comparator);
150 | Arrays.sort(bmson.scroll_events, comparator);
151 |
152 | while (bpmpos < bmson.bpm_events.length || stoppos < bmson.stop_events.length || scrollpos < bmson.scroll_events.length) {
153 | final int bpmy = bpmpos < bmson.bpm_events.length ? bmson.bpm_events[bpmpos].y : Integer.MAX_VALUE;
154 | final int stopy = stoppos < bmson.stop_events.length ? bmson.stop_events[stoppos].y : Integer.MAX_VALUE;
155 | final int scrolly = scrollpos < bmson.scroll_events.length ? bmson.scroll_events[scrollpos].y : Integer.MAX_VALUE;
156 | if (scrolly <= stopy && scrolly <= bpmy) {
157 | getTimeLine(scrolly, resolution).setScroll(bmson.scroll_events[scrollpos].rate);
158 | scrollpos++;
159 | } else if (bpmy <= stopy) {
160 | if(bmson.bpm_events[bpmpos].bpm > 0) {
161 | getTimeLine(bpmy, resolution).setBPM(bmson.bpm_events[bpmpos].bpm);
162 | } else {
163 | log.add(new DecodeLog(WARNING,
164 | "negative BPMはサポートされていません - y : " + bmson.bpm_events[bpmpos].y + " bpm : " + bmson.bpm_events[bpmpos].bpm));
165 | }
166 | bpmpos++;
167 | } else if (stopy != Integer.MAX_VALUE) {
168 | if(bmson.stop_events[stoppos].duration >= 0) {
169 | final TimeLine tl = getTimeLine(stopy, resolution);
170 | tl.setStop((long) ((1000.0 * 1000 * 60 * 4 * bmson.stop_events[stoppos].duration)
171 | / (tl.getBPM() * resolution)));
172 | } else {
173 | log.add(new DecodeLog(WARNING,
174 | "negative STOPはサポートされていません - y : " + bmson.stop_events[stoppos].y + " bpm : " + bmson.stop_events[stoppos].duration));
175 | }
176 | stoppos++;
177 | }
178 | }
179 | // lines処理(小節線)
180 | if (bmson.lines != null) {
181 | for (BarLine bl : bmson.lines) {
182 | getTimeLine(bl.y, resolution).setSectionLine(true);
183 | }
184 | }
185 |
186 | String[] wavmap = new String[bmson.sound_channels.length + bmson.key_channels.length + bmson.mine_channels.length];
187 | int id = 0;
188 | long starttime = 0;
189 | for (SoundChannel sc : bmson.sound_channels) {
190 | wavmap[id] = sc.name;
191 | Arrays.sort(sc.notes, comparator);
192 | final int length = sc.notes.length;
193 | for (int i = 0; i < length; i++) {
194 | final bms.model.bmson.Note n = sc.notes[i];
195 | bms.model.bmson.Note next = null;
196 | for (int j = i + 1; j < length; j++) {
197 | if (sc.notes[j].y > n.y) {
198 | next = sc.notes[j];
199 | break;
200 | }
201 | }
202 | long duration = 0;
203 | if (!n.c) {
204 | starttime = 0;
205 | }
206 | TimeLine tl = getTimeLine(n.y, resolution);
207 | if (next != null && next.c) {
208 | duration = getTimeLine(next.y, resolution).getMicroTime() - tl.getMicroTime();
209 | }
210 |
211 | final int key = n.x > 0 && n.x <= keyassign.length ? keyassign[n.x - 1] : -1;
212 | if (key < 0) {
213 | // BGノート
214 | tl.addBackGroundNote(new NormalNote(id, starttime, duration));
215 | } else if (n.up) {
216 | // LN終端音定義
217 | boolean assigned = false;
218 | if (lnlist[key] != null) {
219 | final double section = (n.y / resolution);
220 | for (LongNote ln : lnlist[key]) {
221 | if (section == ln.getPair().getSection()) {
222 | ln.getPair().setWav(id);
223 | ln.getPair().setMicroStarttime(starttime);
224 | ln.getPair().setMicroDuration(duration);
225 | assigned = true;
226 | break;
227 | }
228 | }
229 | if(!assigned) {
230 | lnup.put(n, new LongNote(id, starttime, duration));
231 | }
232 | }
233 | } else {
234 | boolean insideln = false;
235 | if (lnlist[key] != null) {
236 | final double section = (n.y / resolution);
237 | for (LongNote ln : lnlist[key]) {
238 | if (ln.getSection() < section && section <= ln.getPair().getSection()) {
239 | insideln = true;
240 | break;
241 | }
242 | }
243 | }
244 |
245 | if (insideln) {
246 | log.add(new DecodeLog(WARNING,
247 | "LN内にノートを定義しています - x : " + n.x + " y : " + n.y));
248 | tl.addBackGroundNote(new NormalNote(id, starttime, duration));
249 | } else {
250 | if (n.l > 0) {
251 | // ロングノート
252 | TimeLine end = getTimeLine(n.y + n.l, resolution);
253 | LongNote ln = new LongNote(id, starttime, duration);
254 | if (tl.getNote(key) != null) {
255 | // レイヤーノート判定
256 | bms.model.Note en = tl.getNote(key);
257 | if (en instanceof LongNote && end.getNote(key) == ((LongNote) en).getPair()) {
258 | en.addLayeredNote(ln);
259 | } else {
260 | log.add(new DecodeLog(WARNING,
261 | "同一の位置にノートが複数定義されています - x : " + n.x + " y : " + n.y));
262 | }
263 | } else {
264 | boolean existNote = false;
265 | for (TimeLineCache tl2 : tlcache.subMap(n.y, false, n.y + n.l, true).values()) {
266 | if (tl2.timeline.existNote(key)) {
267 | existNote = true;
268 | break;
269 | }
270 | }
271 | if (existNote) {
272 | log.add(new DecodeLog(WARNING,
273 | "LN内にノートを定義しています - x : " + n.x + " y : " + n.y));
274 | tl.addBackGroundNote(new NormalNote(id, starttime, duration));
275 | } else {
276 | tl.setNote(key, ln);
277 | // ln.setDuration(end.getTime() -
278 | // start.getTime());
279 | LongNote lnend = null;
280 | for (Entry up : lnup.entrySet()) {
281 | if (up.getKey().y == n.y + n.l && up.getKey().x == n.x) {
282 | lnend = up.getValue();
283 | break;
284 | }
285 | }
286 | if(lnend == null) {
287 | lnend = new LongNote(-2);
288 | }
289 |
290 | end.setNote(key, lnend);
291 | ln.setType(n.t > 0 && n.t <= 3 ? n.t : model.getLnmode());
292 | ln.setPair(lnend);
293 | if (lnlist[key] == null) {
294 | lnlist[key] = new ArrayList();
295 | }
296 | lnlist[key].add(ln);
297 | }
298 | }
299 | } else {
300 | // 通常ノート
301 | if (tl.existNote(key)) {
302 | if (tl.getNote(key) instanceof NormalNote) {
303 | tl.getNote(key).addLayeredNote(new NormalNote(id, starttime, duration));
304 | } else {
305 | log.add(new DecodeLog(WARNING,
306 | "同一の位置にノートが複数定義されています - x : " + n.x + " y : " + n.y));
307 | }
308 | } else {
309 | tl.setNote(key, new NormalNote(id, starttime, duration));
310 | }
311 | }
312 | }
313 | }
314 | starttime += duration;
315 | }
316 | id++;
317 | }
318 |
319 | for (MineChannel sc : bmson.key_channels) {
320 | wavmap[id] = sc.name;
321 | Arrays.sort(sc.notes, comparator);
322 | final int length = sc.notes.length;
323 | for (int i = 0; i < length; i++) {
324 | final bms.model.bmson.MineNote n = sc.notes[i];
325 | TimeLine tl = getTimeLine(n.y, resolution);
326 |
327 | final int key = n.x > 0 && n.x <= keyassign.length ? keyassign[n.x - 1] : -1;
328 | if (key >= 0) {
329 | // BGノート
330 | tl.setHiddenNote(key, new NormalNote(id));
331 | }
332 | }
333 | id++;
334 | }
335 | for (MineChannel sc : bmson.mine_channels) {
336 | wavmap[id] = sc.name;
337 | Arrays.sort(sc.notes, comparator);
338 | final int length = sc.notes.length;
339 | for (int i = 0; i < length; i++) {
340 | final bms.model.bmson.MineNote n = sc.notes[i];
341 | TimeLine tl = getTimeLine(n.y, resolution);
342 |
343 | final int key = n.x > 0 && n.x <= keyassign.length ? keyassign[n.x - 1] : -1;
344 | if (key >= 0) {
345 | boolean insideln = false;
346 | if (lnlist[key] != null) {
347 | final double section = (n.y / resolution);
348 | for (LongNote ln : lnlist[key]) {
349 | if (ln.getSection() < section && section <= ln.getPair().getSection()) {
350 | insideln = true;
351 | break;
352 | }
353 | }
354 | }
355 |
356 | if (insideln) {
357 | log.add(new DecodeLog(WARNING,
358 | "LN内に地雷ノートを定義しています - x : " + n.x + " y : " + n.y));
359 | } else if(tl.existNote(key)){
360 | log.add(new DecodeLog(WARNING,
361 | "地雷ノートを定義している位置に通常ノートが存在します - x : " + n.x + " y : " + n.y));
362 | } else {
363 | tl.setNote(key, new MineNote(id, n.damage));
364 | }
365 | }
366 | }
367 | id++;
368 | }
369 |
370 | model.setWavList(wavmap);
371 | // BGA処理
372 | if (bmson.bga != null && bmson.bga.bga_header != null) {
373 | final String[] bgamap = new String[bmson.bga.bga_header.length];
374 | final Map idmap = new HashMap(bmson.bga.bga_header.length);
375 | final Map seqmap = new HashMap();
376 | for (int i = 0; i < bmson.bga.bga_header.length; i++) {
377 | BGAHeader bh = bmson.bga.bga_header[i];
378 | bgamap[i] = bh.name;
379 | idmap.put(bh.id, i);
380 | }
381 | if (bmson.bga.bga_sequence != null) {
382 | for (BGASequence n : bmson.bga.bga_sequence) {
383 | if(n != null) {
384 | Layer.Sequence[] sequence = new Layer.Sequence[n.sequence.length];
385 | for(int i =0;i < sequence.length;i++) {
386 | Sequence seq = n.sequence[i];
387 | if(seq.id != Integer.MIN_VALUE) {
388 | sequence[i] = new Layer.Sequence(seq.time, seq.id);
389 | } else {
390 | sequence[i] = new Layer.Sequence(seq.time);
391 | }
392 | }
393 | seqmap.put(n.id, sequence);
394 | }
395 | }
396 | }
397 | if (bmson.bga.bga_events != null) {
398 | for (BNote n : bmson.bga.bga_events) {
399 | getTimeLine(n.y, resolution).setBGA(idmap.get(n.id));
400 | }
401 | }
402 | if (bmson.bga.layer_events != null) {
403 | for (BNote n : bmson.bga.layer_events) {
404 | int[] idset = n.id_set != null ? n.id_set : new int[] {n.id};
405 | Layer.Sequence[][] seqs = new Layer.Sequence[idset.length][];
406 | Layer.Event event = null;
407 | switch(n.condition != null ? n.condition : "") {
408 | case "play":
409 | event = new Layer.Event(EventType.PLAY, n.interval);
410 | break;
411 | case "miss":
412 | event = new Layer.Event(EventType.MISS, n.interval);
413 | break;
414 | default:
415 | event = new Layer.Event(EventType.ALWAYS, n.interval);
416 | }
417 | for(int seqindex = 0; seqindex < seqs.length;seqindex++) {
418 | int nid = idset[seqindex];
419 | if(seqmap.containsKey(nid) ) {
420 | seqs[seqindex] = seqmap.get(nid);
421 | } else {
422 | seqs[seqindex] = new Layer.Sequence[] {new Layer.Sequence(0, idmap.get(n.id)),new Layer.Sequence(500)};
423 | }
424 | }
425 | getTimeLine(n.y, resolution).setEventlayer(new Layer[] {new Layer(event, seqs)});
426 | }
427 | }
428 | if (bmson.bga.poor_events != null) {
429 | for (BNote n : bmson.bga.poor_events) {
430 | if(seqmap.containsKey(n.id) ) {
431 | getTimeLine(n.y, resolution).setEventlayer(new Layer[] {new Layer(new Layer.Event(EventType.MISS, 1),
432 | new Layer.Sequence[][] {seqmap.get(n.id)})});
433 | } else {
434 | getTimeLine(n.y, resolution).setEventlayer(new Layer[] {new Layer(new Layer.Event(EventType.MISS, 1),
435 | new Layer.Sequence[][] {{new Layer.Sequence(0, idmap.get(n.id)),new Layer.Sequence(500)}})});
436 | }
437 | }
438 | }
439 | model.setBgaList(bgamap);
440 | }
441 | model.setAllTimeLine(tlcache.values().stream().map(tlc -> tlc.timeline).collect(Collectors.toList()).toArray(new TimeLine[tlcache.size()]));
442 |
443 | Logger.getGlobal().fine("BMSONファイル解析完了 :" + f.toString() + " - TimeLine数:" + tlcache.size() + " 時間(ms):"
444 | + (System.currentTimeMillis() - currnttime));
445 |
446 | model.setChartInformation(new ChartInformation(f, lntype, null));
447 | printLog(f);
448 | return model;
449 | }
450 |
451 | private TimeLine getTimeLine(int y, double resolution) {
452 | // Timeをus単位にする場合はこのメソッド内部だけ変更すればOK
453 | final TimeLineCache tlc = tlcache.get(y);
454 | if (tlc != null) {
455 | return tlc.timeline;
456 | }
457 |
458 | Entry le = tlcache.lowerEntry(y);
459 | double bpm = le.getValue().timeline.getBPM();
460 | double time = le.getValue().time + le.getValue().timeline.getMicroStop()
461 | + (240000.0 * 1000 * ((y - le.getKey()) / resolution)) / bpm;
462 |
463 | TimeLine tl = new TimeLine(y / resolution, (long) time, model.getMode().key);
464 | tl.setBPM(bpm);
465 | tlcache.put(y, new TimeLineCache(time, tl));
466 | // System.out.println("y = " + y + " , bpm = " + bpm + " , time = " +
467 | // tl.getTime());
468 | return tl;
469 | }
470 | }
471 |
--------------------------------------------------------------------------------
/src/bms/model/ChartDecoder.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.io.File;
4 | import java.nio.file.Path;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.logging.Logger;
8 |
9 | /**
10 | * 譜面デコーダー
11 | *
12 | * @author exch
13 | */
14 | public abstract class ChartDecoder {
15 |
16 | int lntype;
17 |
18 | List log = new ArrayList();
19 |
20 | /**
21 | * パスで指定したファイルをBMSModelに変換する
22 | *
23 | * @param file
24 | * 譜面ファイル
25 | * @return 変換したBMSModel。失敗した場合はnull
26 | */
27 | public BMSModel decode(File file) {
28 | return decode(file.toPath());
29 | }
30 |
31 | /**
32 | * パスで指定したファイルをBMSModelに変換する
33 | *
34 | * @param path
35 | * 譜面ファイルのパス
36 | * @return 変換したBMSModel。失敗した場合はnull
37 | */
38 | public BMSModel decode(Path path) {
39 | return decode(new ChartInformation(path, lntype, null));
40 | }
41 |
42 | /**
43 | * デコードログを取得する
44 | *
45 | * @return デコードログ
46 | */
47 | public DecodeLog[] getDecodeLog() {
48 | return log.toArray(new DecodeLog[log.size()]);
49 | }
50 |
51 | public abstract BMSModel decode(ChartInformation info);
52 |
53 | /**
54 | * パスで指定したファイルに対応するChartDecoderを取得する
55 | *
56 | * @param p
57 | * 譜面ファイルのパス
58 | * @return 対応するChartDecoder。存在しない場合はnull
59 | */
60 | public static ChartDecoder getDecoder(Path p) {
61 | final String s = p.getFileName().toString().toLowerCase();
62 | if (s.endsWith(".bms") || s.endsWith(".bme") || s.endsWith(".bml") || s.endsWith(".pms")) {
63 | return new BMSDecoder(BMSModel.LNTYPE_LONGNOTE);
64 | } else if (s.endsWith(".bmson")) {
65 | return new BMSONDecoder(BMSModel.LNTYPE_LONGNOTE);
66 | }
67 | return null;
68 | }
69 |
70 | public static int parseInt36(String s, int index) throws NumberFormatException {
71 | int result = parseInt36(s.charAt(index), s.charAt(index + 1));
72 | if (result == -1) {
73 | throw new NumberFormatException();
74 | }
75 | return result;
76 | }
77 |
78 | public static int parseInt36(char c1, char c2) {
79 | int result = 0;
80 | if (c1 >= '0' && c1 <= '9') {
81 | result = (c1 - '0') * 36;
82 | } else if (c1 >= 'a' && c1 <= 'z') {
83 | result = ((c1 - 'a') + 10) * 36;
84 | } else if (c1 >= 'A' && c1 <= 'Z') {
85 | result = ((c1 - 'A') + 10) * 36;
86 | } else {
87 | return -1;
88 | }
89 |
90 | if (c2 >= '0' && c2 <= '9') {
91 | result += (c2 - '0');
92 | } else if (c2 >= 'a' && c2 <= 'z') {
93 | result += (c2 - 'a') + 10;
94 | } else if (c2 >= 'A' && c2 <= 'Z') {
95 | result += (c2 - 'A') + 10;
96 | } else {
97 | return -1;
98 | }
99 |
100 | return result;
101 | }
102 |
103 | public static int parseInt62(String s, int index) throws NumberFormatException {
104 | int result = parseInt62(s.charAt(index), s.charAt(index + 1));
105 | if (result == -1) {
106 | throw new NumberFormatException();
107 | }
108 | return result;
109 | }
110 |
111 | public static int parseInt62(char c1, char c2) {
112 | int result = 0;
113 | if (c1 >= '0' && c1 <= '9') {
114 | result = (c1 - '0') * 62;
115 | } else if (c1 >= 'A' && c1 <= 'Z') {
116 | result = ((c1 - 'A') + 10) * 62;
117 | } else if (c1 >= 'a' && c1 <= 'z') {
118 | result = ((c1 - 'a') + 36) * 62;
119 | } else {
120 | return -1;
121 | }
122 |
123 | if (c2 >= '0' && c2 <= '9') {
124 | result += (c2 - '0');
125 | } else if (c2 >= 'A' && c2 <= 'Z') {
126 | result += (c2 - 'A') + 10;
127 | } else if (c2 >= 'a' && c2 <= 'z') {
128 | result += (c2 - 'a') + 36;
129 | } else {
130 | return -1;
131 | }
132 |
133 | return result;
134 | }
135 |
136 | public static String toBase62(int decimal) {
137 | StringBuilder sb = new StringBuilder();
138 | for (int i = 0;i < 2;i++) {
139 | int mod = (int)(decimal % 62);
140 | if (mod < 10) {
141 | sb.append(mod);
142 | } else if (mod < 36) {
143 | mod = mod - 10 + 'A';
144 | sb.append((char) mod);
145 | } else if (mod < 62) {
146 | mod = mod - 36 + 'a';
147 | sb.append((char) mod);
148 | } else {
149 | sb.append("0");
150 | }
151 | decimal = (int)(decimal / 62);
152 | }
153 | return new String(sb.reverse());
154 | }
155 |
156 | protected void printLog(Path path) {
157 | log.forEach(log -> {
158 | switch(log.getState()) {
159 | case INFO:
160 | Logger.getGlobal().info(path + " : " + log.getMessage());
161 | break;
162 | case WARNING:
163 | Logger.getGlobal().warning(path + " : " + log.getMessage());
164 | break;
165 | case ERROR:
166 | Logger.getGlobal().severe(path + " : " + log.getMessage());
167 | break;
168 | }
169 | });
170 | }
171 |
172 | public static class TimeLineCache {
173 |
174 | public final double time;
175 | public final TimeLine timeline;
176 |
177 | public TimeLineCache(double time, TimeLine timeline) {
178 | this.time = time;
179 | this.timeline = timeline;
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/bms/model/ChartInformation.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.nio.file.Path;
4 |
5 | public class ChartInformation {
6 |
7 | public final Path path;
8 |
9 | public final int lntype;
10 |
11 | public final int[] selectedRandoms;
12 |
13 | public ChartInformation(Path path, int lntype, int[] selectedRandoms) {
14 | this.path = path;
15 | this.lntype = lntype;
16 | this.selectedRandoms = selectedRandoms;
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/bms/model/DecodeLog.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | /**
4 | *
5 | *
6 | * @author exch
7 | */
8 | public class DecodeLog {
9 |
10 | private final String message;
11 |
12 | private final State state;
13 |
14 | public DecodeLog(State state, String message) {
15 | this.message = message;
16 | this.state = state;
17 | }
18 |
19 | public State getState() {
20 | return state;
21 | }
22 |
23 | public String getMessage() {
24 | return message;
25 | }
26 |
27 | public enum State {
28 | INFO, WARNING, ERROR;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/bms/model/EventLane.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.util.ArrayDeque;
4 | import java.util.Collection;
5 |
6 | public class EventLane {
7 |
8 | private TimeLine[] sections;
9 | private int sectionbasepos;
10 | private int sectionseekpos;
11 |
12 | private TimeLine[] bpms;
13 | private int bpmbasepos;
14 | private int bpmseekpos;
15 |
16 | private TimeLine[] stops;
17 | private int stopbasepos;
18 | private int stopseekpos;
19 |
20 | public EventLane(BMSModel model) {
21 | Collection section = new ArrayDeque();
22 | Collection bpm = new ArrayDeque();
23 | Collection stop = new ArrayDeque();
24 |
25 | TimeLine prev = null;
26 | for (TimeLine tl : model.getAllTimeLines()) {
27 | if (tl.getSectionLine()) {
28 | section.add(tl);
29 | }
30 | if (tl.getBPM() != (prev != null ? prev.getBPM() : model.getBpm())) {
31 | bpm.add(tl);
32 | }
33 | if (tl.getStop() != 0) {
34 | stop.add(tl);
35 | }
36 | prev = tl;
37 | }
38 | sections = section.toArray(new TimeLine[section.size()]);
39 | bpms = bpm.toArray(new TimeLine[bpm.size()]);
40 | stops = stop.toArray(new TimeLine[stop.size()]);
41 | }
42 |
43 | public TimeLine[] getSections() {
44 | return sections;
45 | }
46 |
47 | public TimeLine[] getBpmChanges() {
48 | return bpms;
49 | }
50 |
51 | public TimeLine[] getStops() {
52 | return stops;
53 | }
54 |
55 | public TimeLine getSection() {
56 | if (sectionseekpos < sections.length) {
57 | return sections[sectionseekpos++];
58 | }
59 | return null;
60 | }
61 |
62 | public TimeLine getBpm() {
63 | if (bpmseekpos < bpms.length) {
64 | return bpms[bpmseekpos++];
65 | }
66 | return null;
67 | }
68 |
69 | public TimeLine getStop() {
70 | if (stopseekpos < stops.length) {
71 | return stops[stopseekpos++];
72 | }
73 | return null;
74 | }
75 |
76 | public void reset() {
77 | sectionseekpos = sectionbasepos;
78 | bpmseekpos = bpmbasepos;
79 | stopseekpos = stopbasepos;
80 | }
81 |
82 | public void mark(int time) {
83 | for (; sectionbasepos < sections.length - 1 && sections[sectionbasepos + 1].getTime() > time; sectionbasepos++)
84 | ;
85 | for (; sectionbasepos > 0 && sections[sectionbasepos].getTime() < time; sectionbasepos--)
86 | ;
87 | for (; bpmbasepos < bpms.length - 1 && bpms[bpmbasepos + 1].getTime() > time; bpmbasepos++)
88 | ;
89 | for (; bpmbasepos > 0 && bpms[bpmbasepos].getTime() < time; bpmbasepos--)
90 | ;
91 | for (; stopbasepos < stops.length - 1 && stops[stopbasepos + 1].getTime() > time; stopbasepos++)
92 | ;
93 | for (; stopbasepos > 0 && stops[stopbasepos].getTime() < time; stopbasepos--)
94 | ;
95 | sectionseekpos = sectionbasepos;
96 | bpmseekpos = bpmbasepos;
97 | stopseekpos = stopbasepos;
98 | }
99 |
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/src/bms/model/Lane.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.util.*;
4 |
5 | public class Lane {
6 |
7 | private Note[] notes;
8 | private int notebasepos;
9 | private int noteseekpos;
10 |
11 | private Note[] hiddens;
12 | private int hiddenbasepos;
13 | private int hiddenseekpos;
14 |
15 | public Lane(BMSModel model, int lane) {
16 | Collection note = new ArrayDeque();
17 | Collection hnote = new ArrayDeque();
18 | for (TimeLine tl : model.getAllTimeLines()) {
19 | if (tl.existNote(lane)) {
20 | note.add(tl.getNote(lane));
21 | }
22 | if (tl.getHiddenNote(lane) != null) {
23 | hnote.add(tl.getHiddenNote(lane));
24 | }
25 | }
26 | notes = note.toArray(new Note[note.size()]);
27 | hiddens = hnote.toArray(new Note[hnote.size()]);
28 | }
29 |
30 | public Note[] getNotes() {
31 | return notes;
32 | }
33 |
34 | public Note[] getHiddens() {
35 | return hiddens;
36 | }
37 |
38 | public Note getNote() {
39 | if (noteseekpos < notes.length) {
40 | return notes[noteseekpos++];
41 | }
42 | return null;
43 | }
44 |
45 | public Note getHidden() {
46 | if (hiddenseekpos < hiddens.length) {
47 | return hiddens[hiddenseekpos++];
48 | }
49 | return null;
50 | }
51 |
52 | public void reset() {
53 | noteseekpos = notebasepos;
54 | hiddenseekpos = hiddenbasepos;
55 | }
56 |
57 | public void mark(int time) {
58 | for (; notebasepos < notes.length - 1 && notes[notebasepos + 1].getTime() < time; notebasepos++)
59 | ;
60 | for (; notebasepos > 0 && notes[notebasepos].getTime() > time; notebasepos--)
61 | ;
62 | noteseekpos = notebasepos;
63 | for (; hiddenbasepos < hiddens.length - 1
64 | && hiddens[hiddenbasepos + 1].getTime() < time; hiddenbasepos++)
65 | ;
66 | for (; hiddenbasepos > 0 && hiddens[hiddenbasepos].getTime() > time; hiddenbasepos--)
67 | ;
68 | hiddenseekpos = hiddenbasepos;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/bms/model/Layer.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | public class Layer {
4 |
5 | public static final Layer[] EMPTY = new Layer[0];
6 |
7 | public final Event event;
8 |
9 | public final Sequence[][] sequence;
10 |
11 | public Layer(Event event, Sequence[][] sequence) {
12 | this.event = event;
13 | this.sequence = sequence;
14 | }
15 |
16 | public static class Event {
17 | public final EventType type;
18 | public final int interval;
19 |
20 | public Event(EventType type, int interval) {
21 | this.type = type;
22 | this.interval = interval;
23 | }
24 | }
25 |
26 | public enum EventType {
27 | ALWAYS,PLAY,MISS
28 | }
29 |
30 | public static class Sequence {
31 |
32 | public static final int END = Integer.MIN_VALUE;
33 |
34 | public final long time;
35 | public final int id;
36 |
37 | public Sequence(long time) {
38 | this.time = time;
39 | this.id = END;
40 | }
41 |
42 | public Sequence(long time, int id) {
43 | this.time = time;
44 | this.id = id;
45 | }
46 |
47 | public boolean isEnd() {
48 | return id == END;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/bms/model/LongNote.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | /**
4 | * ロングノート
5 | *
6 | * @author exch
7 | */
8 | public class LongNote extends Note {
9 |
10 | /**
11 | * ロングノート終端かどうか
12 | */
13 | private boolean end;
14 | /**
15 | * ペアになっているロングノート
16 | */
17 | private LongNote pair;
18 | /**
19 | * ロングノートの種類
20 | */
21 | private int type;
22 |
23 | /**
24 | * ロングノートの種類:未定義
25 | */
26 | public static final int TYPE_UNDEFINED = 0;
27 | /**
28 | * ロングノートの種類:ロングノート
29 | */
30 | public static final int TYPE_LONGNOTE = 1;
31 | /**
32 | * ロングノートの種類:チャージノート
33 | */
34 | public static final int TYPE_CHARGENOTE = 2;
35 | /**
36 | * ロングノートの種類:ヘルチャージノート
37 | */
38 | public static final int TYPE_HELLCHARGENOTE = 3;
39 |
40 | /**
41 | * 指定のTimeLineを始点としたロングノートを作成する
42 | * @param start
43 | */
44 | public LongNote(int wav) {
45 | this.setWav(wav);
46 | }
47 |
48 | public LongNote(int wav,long starttime, long duration) {
49 | this.setWav(wav);
50 | this.setMicroStarttime(starttime);
51 | this.setMicroDuration(duration);
52 | }
53 |
54 | public int getType() {
55 | return type;
56 | }
57 |
58 | public void setType(int type) {
59 | this.type = type;
60 | }
61 |
62 | public void setPair(LongNote pair) {
63 | pair.pair = this;
64 | this.pair = pair;
65 |
66 | pair.end = pair.getSection() > this.getSection();
67 | this.end = !pair.end;
68 | type = pair.type = (type != TYPE_UNDEFINED ? type : pair.type);
69 | }
70 |
71 | public LongNote getPair() {
72 | return pair;
73 | }
74 |
75 | public boolean isEnd() {
76 | return end;
77 | }
78 |
79 | @Override
80 | public Object clone() {
81 | return clone(true);
82 | }
83 |
84 | private Object clone(boolean copypair) {
85 | LongNote ln = (LongNote) super.clone();
86 | if(copypair) {
87 | ln.setPair((LongNote) pair.clone(false));
88 | }
89 | return ln;
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/bms/model/MineNote.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | /**
4 | * 地雷ノート
5 | *
6 | * @author exch
7 | */
8 | public class MineNote extends Note {
9 |
10 | /**
11 | * 地雷のダメージ量
12 | */
13 | private double damage;
14 |
15 | public MineNote(int wav, double damage) {
16 | this.setWav(wav);
17 | this.setDamage(damage);
18 | }
19 |
20 | /**
21 | * 地雷ノーツのダメージ量を取得する
22 | * @return 地雷ノーツのダメージ量
23 | */
24 | public double getDamage() {
25 | return damage;
26 | }
27 |
28 | /**
29 | * 地雷ノーツのダメージ量を設定する
30 | * @param damage 地雷ノーツのダメージ量
31 | */
32 | public void setDamage(double damage) {
33 | this.damage = damage;
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/src/bms/model/Mode.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | /**
4 | * プレイモード
5 | *
6 | * @author exch
7 | */
8 | public enum Mode {
9 |
10 | BEAT_5K(5, "beat-5k", 1, 6, new int[] { 5 }),
11 | BEAT_7K(7, "beat-7k", 1, 8, new int[] { 7 }),
12 | BEAT_10K(10, "beat-10k", 2, 12, new int[] { 5, 11 }),
13 | BEAT_14K(14, "beat-14k", 2, 16, new int[] { 7, 15 }),
14 | POPN_5K(9, "popn-5k", 1, 5, new int[] {}),
15 | POPN_9K(9, "popn-9k", 1, 9, new int[] {}),
16 | KEYBOARD_24K(25, "keyboard-24k", 1, 26, new int[] { 24, 25 }),
17 | KEYBOARD_24K_DOUBLE(50, "keyboard-24k-double", 2, 52, new int[] { 24, 25, 50, 51 }),
18 | ;
19 |
20 | public final int id;
21 | /**
22 | * モードの名称。bmsonのmode_hintに対応
23 | */
24 | public final String hint;
25 | /**
26 | * プレイヤー数
27 | */
28 | public final int player;
29 | /**
30 | * 使用するキーの数
31 | */
32 | public final int key;
33 | /**
34 | * スクラッチキーアサイン
35 | */
36 | public final int[] scratchKey;
37 |
38 | private Mode(int id, String hint, int player, int key, int[] scratchKey) {
39 | this.id = id;
40 | this.hint = hint;
41 | this.player = player;
42 | this.key = key;
43 | this.scratchKey = scratchKey;
44 | }
45 |
46 | /**
47 | * 指定するkeyがスクラッチキーかどうかを返す
48 | *
49 | * @param key キー番号
50 | * @return スクラッチであればtrue
51 | */
52 | public boolean isScratchKey(int key) {
53 | for (int sc : scratchKey) {
54 | if (key == sc) {
55 | return true;
56 | }
57 | }
58 | return false;
59 | }
60 |
61 | /**
62 | * mode_hintに対応するModeを取得する
63 | *
64 | * @param hint
65 | * mode_hint
66 | * @return 対応するMode
67 | */
68 | public static Mode getMode(String hint) {
69 | for (Mode mode : values()) {
70 | if (mode.hint.equals(hint)) {
71 | return mode;
72 | }
73 | }
74 | return null;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/bms/model/NormalNote.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | /**
4 | * 通常ノート
5 | *
6 | * @author exch
7 | */
8 | public class NormalNote extends Note {
9 |
10 | public NormalNote(int wav) {
11 | this.setWav(wav);
12 | }
13 |
14 | public NormalNote(int wav, long start, long duration) {
15 | this.setWav(wav);
16 | this.setMicroStarttime(start);
17 | this.setMicroDuration(duration);
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/src/bms/model/Note.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.util.Arrays;
4 |
5 | /**
6 | * ノート
7 | *
8 | * @author exch
9 | */
10 | public abstract class Note implements Cloneable {
11 |
12 | public static final Note[] EMPTYARRAY = new Note[0];
13 | /**
14 | * ノートが配置されている小節
15 | */
16 | private double section;
17 | /**
18 | * ノートが配置されている時間(us)
19 | */
20 | private long time;
21 |
22 | /**
23 | * アサインされている 音源ID
24 | */
25 | private int wav;
26 | /**
27 | * 音源IDの音の開始時間(us)
28 | */
29 | private long start;
30 | /**
31 | * 音源IDの音を鳴らす長さ(us)
32 | */
33 | private long duration;
34 | /**
35 | * ノーツの状態
36 | */
37 | private int state;
38 | /**
39 | * ノーツの演奏時間
40 | */
41 | private long playtime;
42 | /**
43 | * 同時演奏されるノート
44 | */
45 | private Note[] layerednotes = EMPTYARRAY;
46 |
47 | public int getWav() {
48 | return wav;
49 | }
50 |
51 | public void setWav(int wav) {
52 | this.wav = wav;
53 | }
54 |
55 | public int getState() {
56 | return state;
57 | }
58 |
59 | public void setState(int state) {
60 | this.state = state;
61 | }
62 |
63 | public long getMilliStarttime() {
64 | return start / 1000;
65 | }
66 |
67 | public long getMicroStarttime() {
68 | return start;
69 | }
70 |
71 | public void setMicroStarttime(long start) {
72 | this.start = start;
73 | }
74 |
75 | public long getMilliDuration() {
76 | return duration / 1000;
77 | }
78 |
79 | public long getMicroDuration() {
80 | return duration;
81 | }
82 |
83 | public void setMicroDuration(long duration) {
84 | this.duration = duration;
85 | }
86 |
87 | public int getPlayTime() {
88 | return (int) (playtime / 1000);
89 | }
90 |
91 | public long getMilliPlayTime() {
92 | return playtime / 1000;
93 | }
94 |
95 | public long getMicroPlayTime() {
96 | return playtime;
97 | }
98 |
99 | public void setPlayTime(int playtime) {
100 | this.playtime = playtime * 1000;
101 | }
102 |
103 | public void setMicroPlayTime(long playtime) {
104 | this.playtime = playtime;
105 | }
106 |
107 | public double getSection() {
108 | return section;
109 | }
110 |
111 | public void setSection(double section) {
112 | this.section = section;
113 | }
114 |
115 | public int getTime() {
116 | return (int) (time / 1000);
117 | }
118 |
119 | public long getMilliTime() {
120 | return time / 1000;
121 | }
122 |
123 | public long getMicroTime() {
124 | return time;
125 | }
126 |
127 | public void setMicroTime(long time) {
128 | this.time = time;
129 | }
130 |
131 | public void addLayeredNote(Note n) {
132 | if(n == null) {
133 | return;
134 | }
135 | n.setSection(section);
136 | n.setMicroTime(time);
137 | layerednotes = Arrays.copyOf(layerednotes, layerednotes.length + 1);
138 | layerednotes[layerednotes.length - 1] = n;
139 | }
140 |
141 | public Note[] getLayeredNotes() {
142 | return layerednotes;
143 | }
144 |
145 | @Override
146 | public Object clone() {
147 | try {
148 | return super.clone();
149 | } catch (CloneNotSupportedException e) {
150 | }
151 | return null;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/bms/model/Section.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.util.*;
4 | import java.util.Map.Entry;
5 |
6 | import bms.model.ChartDecoder.TimeLineCache;
7 | import bms.model.Layer.EventType;
8 |
9 | import static bms.model.DecodeLog.State.*;
10 |
11 | /**
12 | * 小節
13 | *
14 | * @author exch
15 | */
16 | public class Section {
17 |
18 | public static final int ILLEGAL = -1;
19 | public static final int LANE_AUTOPLAY = 1;
20 | public static final int SECTION_RATE = 2;
21 | public static final int BPM_CHANGE = 3;
22 | public static final int BGA_PLAY = 4;
23 | public static final int POOR_PLAY = 6;
24 | public static final int LAYER_PLAY = 7;
25 | public static final int BPM_CHANGE_EXTEND = 8;
26 | public static final int STOP = 9;
27 |
28 | public static final int P1_KEY_BASE = 1 * 36 + 1;
29 | public static final int P2_KEY_BASE = 2 * 36 + 1;
30 | public static final int P1_INVISIBLE_KEY_BASE = 3 * 36 + 1;
31 | public static final int P2_INVISIBLE_KEY_BASE = 4 * 36 + 1;
32 | public static final int P1_LONG_KEY_BASE = 5 * 36 + 1;
33 | public static final int P2_LONG_KEY_BASE = 6 * 36 + 1;
34 | public static final int P1_MINE_KEY_BASE = 13 * 36 + 1;
35 | public static final int P2_MINE_KEY_BASE = 14 * 36 + 1;
36 |
37 | public static final int SCROLL = 1020;
38 |
39 | public static final int[] NOTE_CHANNELS = {P1_KEY_BASE, P2_KEY_BASE ,P1_INVISIBLE_KEY_BASE, P2_INVISIBLE_KEY_BASE,
40 | P1_LONG_KEY_BASE, P2_LONG_KEY_BASE, P1_MINE_KEY_BASE, P2_MINE_KEY_BASE};
41 |
42 | /**
43 | * 小節の拡大倍率
44 | */
45 | private double rate = 1.0;
46 | /**
47 | * POORアニメーション
48 | */
49 | private int[] poor = new int[0];
50 |
51 | private final BMSModel model;
52 |
53 | private final double sectionnum;
54 |
55 | private final List log;
56 |
57 | private List channellines;
58 |
59 | public Section(BMSModel model, Section prev, List lines, Map bpmtable,
60 | Map stoptable, Map scrolltable, List log) {
61 | this.model = model;
62 | this.log = log;
63 | final int base = model.getBase();
64 |
65 | channellines = new ArrayList(lines.size());
66 | if (prev != null) {
67 | sectionnum = prev.sectionnum + prev.rate;
68 | } else {
69 | sectionnum = 0;
70 | }
71 | for (String line : lines) {
72 | int channel = ChartDecoder.parseInt36(line.charAt(4), line.charAt(5));
73 | switch (channel) {
74 | case ILLEGAL:
75 | log.add(new DecodeLog(WARNING, "チャンネル定義が無効です : " + line));
76 | break;
77 | // BGレーン
78 | case LANE_AUTOPLAY:
79 | // BGAレーン
80 | case BGA_PLAY:
81 | // レイヤー
82 | case LAYER_PLAY:
83 | channellines.add(line);
84 | break;
85 | // 小節の拡大率
86 | case SECTION_RATE:
87 | int colon_index = line.indexOf(":");
88 | try {
89 | rate = Double.valueOf(line.substring(colon_index + 1, line.length()));
90 | } catch (NumberFormatException e) {
91 | log.add(new DecodeLog(WARNING, "小節の拡大率が不正です : " + line));
92 | }
93 | break;
94 | // BPM変化
95 | case BPM_CHANGE:
96 | this.processData(line, (pos, data) -> {
97 | if(base == 62) {
98 | data = ChartDecoder.parseInt36(ChartDecoder.toBase62(data), 0); //間違った数値を再計算、62進数文字に戻して36進数数値化。
99 | }
100 | bpmchange.put(pos, (double) (data / 36) * 16 + (data % 36));
101 | });
102 | break;
103 | // POORアニメーション
104 | case POOR_PLAY:
105 | poor = this.splitData(line);
106 | // アニメーションが単一画像のみの定義の場合、0を除外する(ミスレイヤーチャンネルの定義が曖昧)
107 | int singleid = 0;
108 | for(int id : poor) {
109 | if(id != 0) {
110 | if(singleid != 0 && singleid != id) {
111 | singleid = -1;
112 | break;
113 | } else {
114 | singleid = id;
115 | }
116 | }
117 | }
118 | if(singleid != -1) {
119 | poor = new int[] {singleid};
120 | }
121 | break;
122 | // BPM変化(拡張)
123 | case BPM_CHANGE_EXTEND:
124 | this.processData(line, (pos, data) -> {
125 | Double bpm = bpmtable.get(data);
126 | if (bpm != null) {
127 | bpmchange.put(pos, bpm);
128 | } else {
129 | log.add(new DecodeLog(WARNING, "未定義のBPM変化を参照しています : " + data));
130 | }
131 | });
132 | break;
133 | // ストップシーケンス
134 | case STOP:
135 | this.processData(line, (pos, data) -> {
136 | Double st = stoptable.get(data);
137 | if (st != null) {
138 | stop.put(pos, st);
139 | } else {
140 | log.add(new DecodeLog(WARNING, "未定義のSTOPを参照しています : " + data));
141 | }
142 | });
143 | break;
144 | // scroll
145 | case SCROLL:
146 | this.processData(line, (pos, data) -> {
147 | Double st = scrolltable.get(data);
148 | if (st != null) {
149 | scroll.put(pos, st);
150 | } else {
151 | log.add(new DecodeLog(WARNING, "未定義のSCROLLを参照しています : " + data));
152 | }
153 | });
154 | break;
155 | }
156 |
157 | int basech = 0;
158 | int ch2 = -1;
159 | for(int ch : NOTE_CHANNELS) {
160 | if (ch <= channel && channel <= ch + 8) {
161 | basech = ch;
162 | ch2 = channel - ch;
163 | channellines.add(line);
164 | break;
165 | }
166 | }
167 | // 5/10KEY -> 7/14KEY
168 | if(ch2 == 7 || ch2 == 8) {
169 | final Mode mode = (model.getMode() == Mode.BEAT_5K) ? Mode.BEAT_7K : (model.getMode() == Mode.BEAT_10K ? Mode.BEAT_14K : null);
170 | if(mode != null) {
171 | this.processData(line, (pos, data) -> {
172 | model.setMode(mode);
173 | });
174 | }
175 | }
176 | // 5/7KEY -> 10/14KEY
177 | if(basech == P2_KEY_BASE || basech == P2_INVISIBLE_KEY_BASE || basech == P2_LONG_KEY_BASE || basech ==P2_MINE_KEY_BASE) {
178 | final Mode mode = (model.getMode() == Mode.BEAT_5K) ? Mode.BEAT_10K : (model.getMode() == Mode.BEAT_7K ? Mode.BEAT_14K : null);
179 | if(mode != null) {
180 | this.processData(line, (pos, data) -> {
181 | model.setMode(mode);
182 | });
183 | }
184 | }
185 | }
186 | }
187 |
188 | private int[] splitData(String line) {
189 | final int base = model.getBase();
190 | final int findex = line.indexOf(":") + 1;
191 | final int lindex = line.length();
192 | final int split = (lindex - findex) / 2;
193 | int[] result = new int[split];
194 | for (int i = 0; i < split; i++) {
195 | if (base == 62) {
196 | result[i] = ChartDecoder.parseInt62(line.charAt(findex + i * 2), line.charAt(findex + i * 2 + 1));
197 | } else {
198 | result[i] = ChartDecoder.parseInt36(line.charAt(findex + i * 2), line.charAt(findex + i * 2 + 1));
199 | }
200 | if(result[i] == -1) {
201 | log.add(new DecodeLog(WARNING, model.getTitle() + ":チャンネル定義中の不正な値:" + line));
202 | result[i] = 0;
203 | }
204 | }
205 | return result;
206 | }
207 |
208 | private void processData(String line, DataProcessor processor) {
209 | final int base = model.getBase();
210 | final int findex = line.indexOf(":") + 1;
211 | final int lindex = line.length();
212 | final int split = (lindex - findex) / 2;
213 | int result;
214 | for (int i = 0; i < split; i++) {
215 | if (base == 62) {
216 | result = ChartDecoder.parseInt62(line.charAt(findex + i * 2), line.charAt(findex + i * 2 + 1));
217 | } else {
218 | result = ChartDecoder.parseInt36(line.charAt(findex + i * 2), line.charAt(findex + i * 2 + 1));
219 | }
220 | if(result > 0) {
221 | processor.process((double)i / split, result);
222 | } else if(result == -1){
223 | log.add(new DecodeLog(WARNING, model.getTitle() + ":チャンネル定義中の不正な値:" + line));
224 | }
225 | }
226 | }
227 |
228 | interface DataProcessor {
229 | public void process(double pos, int data);
230 | }
231 |
232 | private final TreeMap bpmchange = new TreeMap();
233 | private final TreeMap stop = new TreeMap();
234 | private final TreeMap scroll = new TreeMap();
235 |
236 | private static final int[] CHANNELASSIGN_BEAT5 = { 0, 1, 2, 3, 4, 5, -1, -1, -1, 6, 7, 8, 9, 10, 11, -1, -1, -1 };
237 | private static final int[] CHANNELASSIGN_BEAT7 = { 0, 1, 2, 3, 4, 7, -1, 5, 6, 8, 9, 10, 11, 12, 15, -1, 13, 14 };
238 | private static final int[] CHANNELASSIGN_POPN = { 0, 1, 2, 3, 4, -1,-1,-1,-1,-1, 5, 6, 7, 8,-1,-1,-1,-1 };
239 |
240 | private TreeMap tlcache;
241 |
242 | /**
243 | * SectionモデルからTimeLineモデルを作成し、BMSModelに登録する
244 | */
245 | public void makeTimeLines(int[] wavmap, int[] bgamap, TreeMap tlcache, List[] lnlist, LongNote[] startln) {
246 | final int lnobj = model.getLnobj();
247 | final int lnmode = model.getLnmode();
248 | this.tlcache = tlcache;
249 | final int[] cassign = model.getMode() == Mode.POPN_9K ? CHANNELASSIGN_POPN :
250 | (model.getMode() == Mode.BEAT_7K || model.getMode() == Mode.BEAT_14K ? CHANNELASSIGN_BEAT7 : CHANNELASSIGN_BEAT5);
251 | final int base = model.getBase();
252 | // 小節線追加
253 | final TimeLine basetl = getTimeLine(sectionnum);
254 | basetl.setSectionLine(true);
255 |
256 | if(poor.length > 0) {
257 | final Layer.Sequence[] poors = new Layer.Sequence[poor.length + 1];
258 | final int poortime = 500;
259 |
260 | for (int i = 0; i < poor.length; i++) {
261 | if (bgamap[poor[i]] != -2) {
262 | poors[i] = new Layer.Sequence((long)(i * poortime / poor.length), bgamap[poor[i]]);
263 | } else {
264 | poors[i] = new Layer.Sequence((long)(i * poortime / poor.length), -1);
265 | }
266 | }
267 | poors[poors.length - 1] = new Layer.Sequence(poortime);
268 | basetl.setEventlayer(new Layer[] {new Layer(new Layer.Event(EventType.MISS, 1),new Layer.Sequence[][] {poors})});
269 | }
270 | // BPM変化。ストップシーケンステーブル準備
271 | Iterator> stops = stop.entrySet().iterator();
272 | Map.Entry ste = stops.hasNext() ? stops.next() : null;
273 | Iterator> bpms = bpmchange.entrySet().iterator();
274 | Map.Entry bce = bpms.hasNext() ? bpms.next() : null;
275 | Iterator> scrolls = scroll.entrySet().iterator();
276 | Map.Entry sce = scrolls.hasNext() ? scrolls.next() : null;
277 |
278 | while(ste != null || bce != null || sce != null) {
279 | final double bc = bce != null ? bce.getKey() : 2;
280 | final double st = ste != null ? ste.getKey() : 2;
281 | final double sc = sce != null ? sce.getKey() : 2;
282 | if(sc <= st && sc <= bc) {
283 | getTimeLine(sectionnum + sc * rate).setScroll(sce.getValue());
284 | sce = scrolls.hasNext() ? scrolls.next() : null;
285 | } else if(bc <= st) {
286 | getTimeLine(sectionnum + bc * rate).setBPM(bce.getValue());
287 | bce = bpms.hasNext() ? bpms.next() : null;
288 | } else if(st <= 1){
289 | final TimeLine tl = getTimeLine(sectionnum + ste.getKey() * rate);
290 | tl.setStop((long) (1000.0 * 1000 * 60 * 4 * ste.getValue() / (tl.getBPM())));
291 | ste = stops.hasNext() ? stops.next() : null;
292 | }
293 | }
294 |
295 | for(String line : channellines) {
296 | int channel = ChartDecoder.parseInt36(line.charAt(4), line.charAt(5));
297 | int tmpkey = 0;
298 | if(channel >= P1_KEY_BASE && channel < P1_KEY_BASE + 9) {
299 | tmpkey = cassign[channel - P1_KEY_BASE];
300 | channel = P1_KEY_BASE;
301 | } else if(channel >= P2_KEY_BASE && channel < P2_KEY_BASE + 9) {
302 | tmpkey = cassign[channel - P2_KEY_BASE + 9];
303 | channel = P1_KEY_BASE;
304 | } else if(channel >= P1_INVISIBLE_KEY_BASE && channel < P1_INVISIBLE_KEY_BASE + 9) {
305 | tmpkey = cassign[channel - P1_INVISIBLE_KEY_BASE];
306 | channel = P1_INVISIBLE_KEY_BASE;
307 | } else if(channel >= P2_INVISIBLE_KEY_BASE && channel < P2_INVISIBLE_KEY_BASE + 9) {
308 | tmpkey = cassign[channel - P2_INVISIBLE_KEY_BASE + 9];
309 | channel = P1_INVISIBLE_KEY_BASE;
310 | } else if(channel >= P1_LONG_KEY_BASE && channel < P1_LONG_KEY_BASE + 9) {
311 | tmpkey = cassign[channel - P1_LONG_KEY_BASE];
312 | channel = P1_LONG_KEY_BASE;
313 | } else if(channel >= P2_LONG_KEY_BASE && channel < P2_LONG_KEY_BASE + 9) {
314 | tmpkey = cassign[channel - P2_LONG_KEY_BASE + 9];
315 | channel = P1_LONG_KEY_BASE;
316 | } else if(channel >= P1_MINE_KEY_BASE && channel < P1_MINE_KEY_BASE + 9) {
317 | tmpkey = cassign[channel - P1_MINE_KEY_BASE];
318 | channel = P1_MINE_KEY_BASE;
319 | } else if(channel >= P2_MINE_KEY_BASE && channel < P2_MINE_KEY_BASE + 9) {
320 | tmpkey = cassign[channel - P2_MINE_KEY_BASE + 9];
321 | channel = P1_MINE_KEY_BASE;
322 | }
323 | final int key = tmpkey;
324 | if(key == -1) {
325 | continue;
326 | }
327 | switch (channel) {
328 | case P1_KEY_BASE:
329 | this.processData(line, (pos, data) -> {
330 | // normal note, lnobj
331 | final TimeLine tl = getTimeLine(sectionnum + rate * pos);
332 | if (tl.existNote(key)) {
333 | log.add(new DecodeLog(WARNING, "通常ノート追加時に衝突が発生しました : " + (key + 1) + ":"
334 | + tl.getTime()));
335 | }
336 | if (data == lnobj) {
337 | // LN終端処理
338 | // TODO 高速化のために直前のノートを記録しておく
339 | for (Map.Entry e : tlcache.descendingMap().entrySet()) {
340 | if(e.getKey() >= tl.getSection() ) {
341 | continue;
342 | }
343 | final TimeLine tl2 = e.getValue().timeline;
344 | if (tl2.existNote(key)) {
345 | final Note note = tl2.getNote(key);
346 | if (note instanceof NormalNote) {
347 | // LNOBJの直前のノートをLNに差し替える
348 | LongNote ln = new LongNote(note.getWav());
349 | ln.setType(lnmode);
350 | tl2.setNote(key, ln);
351 | LongNote lnend = new LongNote(-2);
352 | tl.setNote(key, lnend);
353 | ln.setPair(lnend);
354 |
355 | if (lnlist[key] == null) {
356 | lnlist[key] = new ArrayList();
357 | }
358 | lnlist[key].add(ln);
359 | break;
360 | } else if (note instanceof LongNote && ((LongNote) note).getPair() == null) {
361 | log.add(new DecodeLog(WARNING,
362 | "LNレーンで開始定義し、LNオブジェクトで終端定義しています。レーン: " + (key + 1) + " - Section : "
363 | + tl2.getSection() + " - " + tl.getSection()));
364 | LongNote lnend = new LongNote(-2);
365 | tl.setNote(key, lnend);
366 | ((LongNote) note).setPair(lnend);
367 |
368 | if (lnlist[key] == null) {
369 | lnlist[key] = new ArrayList();
370 | }
371 | lnlist[key].add((LongNote) note);
372 | startln[key] = null;
373 | break;
374 | } else {
375 | log.add(new DecodeLog(WARNING, "LNオブジェクトの対応が取れません。レーン: " + key
376 | + " - Time(ms):" + tl2.getTime()));
377 | break;
378 | }
379 | }
380 | }
381 | } else {
382 | tl.setNote(key, new NormalNote(wavmap[data]));
383 | }
384 | });
385 | break;
386 | case P1_INVISIBLE_KEY_BASE:
387 | this.processData(line, (pos, data) -> {
388 | getTimeLine(sectionnum + rate * pos).setHiddenNote(key, new NormalNote(wavmap[data]));
389 | });
390 | break;
391 | case P1_LONG_KEY_BASE:
392 | this.processData(line, (pos, data) -> {
393 | // long note
394 | final TimeLine tl = getTimeLine(sectionnum + rate * pos);
395 | boolean insideln = false;
396 | if (!insideln && lnlist[key] != null) {
397 | final double section = tl.getSection();
398 | for (LongNote ln : lnlist[key]) {
399 | if (ln.getSection() <= section && section <= ln.getPair().getSection()) {
400 | insideln = true;
401 | break;
402 | }
403 | }
404 | }
405 |
406 | if(!insideln) {
407 | // LN処理
408 | if (startln[key] == null) {
409 | if(tl.existNote(key)) {
410 | Note note = tl.getNote(key);
411 | log.add(new DecodeLog(WARNING, "LN開始位置に通常ノートが存在します。レーン: "
412 | + (key + 1) + " - Time(ms):" + tl.getTime()));
413 | if(note instanceof NormalNote && note.getWav() != wavmap[data]) {
414 | tl.addBackGroundNote(note);
415 | }
416 | }
417 | LongNote ln = new LongNote(wavmap[data]);
418 | tl.setNote(key, ln);
419 | startln[key] = ln;
420 | } else if(startln[key].getSection() == Double.MIN_VALUE){
421 | startln[key] = null;
422 | } else {
423 | // LN終端処理
424 | for (Map.Entry e : tlcache.descendingMap().entrySet()) {
425 | if(e.getKey() >= tl.getSection()) {
426 | continue;
427 | }
428 |
429 | final TimeLine tl2 = e.getValue().timeline;
430 | if(tl2.getSection() == startln[key].getSection()) {
431 | Note note = startln[key];
432 | ((LongNote)note).setType(lnmode);
433 | LongNote noteend = new LongNote(startln[key].getWav() != wavmap[data] ? wavmap[data] : -2);
434 | tl.setNote(key, noteend);
435 | ((LongNote)note).setPair(noteend);
436 | if (lnlist[key] == null) {
437 | lnlist[key] = new ArrayList();
438 | }
439 | lnlist[key].add((LongNote) note);
440 |
441 | startln[key] = null;
442 | break;
443 | } else if(tl2.existNote(key)){
444 | Note note = tl2.getNote(key);
445 | log.add(new DecodeLog(WARNING, "LN内に通常ノートが存在します。レーン: "
446 | + (key + 1) + " - Time(ms):" + tl2.getTime()));
447 | tl2.setNote(key, null);
448 | if(note instanceof NormalNote) {
449 | tl2.addBackGroundNote(note);
450 | }
451 | }
452 | }
453 | }
454 | } else {
455 | if (startln[key] == null) {
456 | LongNote ln = new LongNote(wavmap[data]);
457 | ln.setSection(Double.MIN_VALUE);
458 | startln[key] = ln;
459 | log.add(new DecodeLog(WARNING, "LN内にLN開始ノートを定義しようとしています : "
460 | + (key + 1) + " - Section : " + tl.getSection() + " - Time(ms):" + tl.getTime()));
461 | } else {
462 | if(startln[key].getSection() != Double.MIN_VALUE) {
463 | tlcache.get(startln[key].getSection()).timeline.setNote(key, null);
464 | }
465 | startln[key] = null;
466 | log.add(new DecodeLog(WARNING, "LN内にLN終端ノートを定義しようとしています : "
467 | + (key + 1) + " - Section : " + tl.getSection() + " - Time(ms):" + tl.getTime()));
468 | }
469 | }
470 | });
471 | break;
472 | case P1_MINE_KEY_BASE:
473 | // mine note
474 | this.processData(line, (pos, data) -> {
475 | final TimeLine tl = getTimeLine(sectionnum + rate * pos);
476 | boolean insideln = tl.existNote(key);
477 | if (!insideln && lnlist[key] != null) {
478 | final double section = tl.getSection();
479 | for (LongNote ln : lnlist[key]) {
480 | if (ln.getSection() <= section && section <= ln.getPair().getSection()) {
481 | insideln = true;
482 | break;
483 | }
484 | }
485 | }
486 |
487 | if(!insideln) {
488 | if(base == 62) {
489 | data = ChartDecoder.parseInt36(ChartDecoder.toBase62(data), 0); //間違った数値を再計算、62進数文字に戻して36進数数値化。
490 | }
491 | tl.setNote(key, new MineNote(wavmap[0], data));
492 | } else {
493 | log.add(new DecodeLog(WARNING, "地雷ノート追加時に衝突が発生しました : " + (key + 1) + ":"
494 | + tl.getTime()));
495 | }
496 | });
497 | break;
498 | case LANE_AUTOPLAY:
499 | // BGレーン
500 | this.processData(line, (pos, data) -> {
501 | getTimeLine(sectionnum + rate * pos).addBackGroundNote(new NormalNote(wavmap[data]));
502 | });
503 | break;
504 | case BGA_PLAY:
505 | this.processData(line, (pos, data) -> {
506 | getTimeLine(sectionnum + rate * pos).setBGA(bgamap[data]);
507 | });
508 | break;
509 | case LAYER_PLAY:
510 | this.processData(line, (pos, data) -> {
511 | getTimeLine(sectionnum + rate * pos).setLayer(bgamap[data]);
512 | });
513 | break;
514 |
515 | }
516 | }
517 | }
518 |
519 | private TimeLine getTimeLine(double section) {
520 | final TimeLineCache tlc = tlcache.get(section);
521 | if (tlc != null) {
522 | return tlc.timeline;
523 | }
524 |
525 | Entry le = tlcache.lowerEntry(section);
526 | double scroll = le.getValue().timeline.getScroll();
527 | double bpm = le.getValue().timeline.getBPM();
528 | double time = le.getValue().time + le.getValue().timeline.getMicroStop() + (240000.0 * 1000 * (section - le.getKey())) / bpm;
529 |
530 | TimeLine tl = new TimeLine(section, (long)time, model.getMode().key);
531 | tl.setBPM(bpm);
532 | tl.setScroll(scroll);
533 | tlcache.put(section, new TimeLineCache(time, tl));
534 | return tl;
535 | }
536 | }
537 |
--------------------------------------------------------------------------------
/src/bms/model/TimeLine.java:
--------------------------------------------------------------------------------
1 | package bms.model;
2 |
3 | import java.util.*;
4 |
5 | /**
6 | * タイムライン
7 | *
8 | * @author exch
9 | */
10 | public class TimeLine {
11 |
12 | /**
13 | * タイムラインの時間(us)
14 | */
15 | private long time;
16 | /**
17 | * タイムラインの小節
18 | */
19 | private double section;
20 | /**
21 | * タイムライン上に配置されている演奏レーン分のノート。配置されていないレーンにはnullを入れる。
22 | */
23 | private Note[] notes;
24 |
25 | /**
26 | * タイムライン上に配置されている演奏レーン分の不可視ノート。配置されていないレーンにはnullを入れる。
27 | */
28 | private Note[] hiddennotes;
29 | /**
30 | * タイムライン上に配置されているBGMノート
31 | */
32 | private Note[] bgnotes = Note.EMPTYARRAY;
33 | /**
34 | * 小節線の有無
35 | */
36 | private boolean sectionLine = false;
37 | /**
38 | * タイムライン上からのBPM変化
39 | */
40 | private double bpm;
41 | /**
42 | * ストップ時間(us)
43 | */
44 | private long stop;
45 | /**
46 | * スクロールスピード
47 | */
48 | private double scroll = 1.0;
49 |
50 | /**
51 | * 表示するBGAのID
52 | */
53 | private int bga = -1;
54 | /**
55 | * 表示するレイヤーのID
56 | */
57 | private int layer = -1;
58 | /**
59 | * POORレイヤー
60 | */
61 | private Layer[] eventlayer = Layer.EMPTY;
62 |
63 | public TimeLine(double section, long time, int notesize) {
64 | this.section = section;
65 | this.time = time;
66 | notes = new Note[notesize];
67 | hiddennotes = new Note[notesize];
68 | }
69 |
70 | public int getTime() {
71 | return (int) (time / 1000);
72 | }
73 |
74 | public long getMilliTime() {
75 | return time / 1000;
76 | }
77 |
78 | public long getMicroTime() {
79 | return time;
80 | }
81 |
82 | protected void setMicroTime(long time) {
83 | this.time = time;
84 | for(Note n : notes) {
85 | if(n != null) {
86 | n.setMicroTime(time);
87 | }
88 | }
89 | for(Note n : hiddennotes) {
90 | if(n != null) {
91 | n.setMicroTime(time);
92 | }
93 | }
94 | for(Note n : bgnotes) {
95 | n.setMicroTime(time);
96 | }
97 | }
98 |
99 | public int getLaneCount() {
100 | return notes.length;
101 | }
102 |
103 | protected void setLaneCount(int lanes) {
104 | if(notes.length != lanes) {
105 | Note[] newnotes = new Note[lanes];
106 | Note[] newhiddennotes = new Note[lanes];
107 | for(int i = 0;i < lanes;i++) {
108 | if(i < notes.length) {
109 | newnotes[i] = notes[i];
110 | newhiddennotes[i] = hiddennotes[i];
111 | }
112 | }
113 | notes = newnotes;
114 | hiddennotes = newhiddennotes;
115 | }
116 | }
117 |
118 | /**
119 | * タイムライン上の総ノート数を返す
120 | *
121 | * @return
122 | */
123 | public int getTotalNotes() {
124 | return getTotalNotes(BMSModel.LNTYPE_LONGNOTE);
125 | }
126 |
127 | /**
128 | * タイムライン上の総ノート数を返す
129 | *
130 | * @return
131 | */
132 | public int getTotalNotes(int lntype) {
133 | int count = 0;
134 | for (Note note : notes) {
135 | if (note != null) {
136 | if (note instanceof LongNote) {
137 | final LongNote ln = (LongNote) note;
138 | if (ln.getType() == LongNote.TYPE_CHARGENOTE || ln.getType() == LongNote.TYPE_HELLCHARGENOTE
139 | || (ln.getType() == LongNote.TYPE_UNDEFINED && lntype != BMSModel.LNTYPE_LONGNOTE)
140 | || !ln.isEnd()) {
141 | count++;
142 | }
143 | } else if (note instanceof NormalNote) {
144 | count++;
145 | }
146 | }
147 | }
148 | return count;
149 | }
150 |
151 | public boolean existNote() {
152 | for (Note n : notes) {
153 | if (n != null) {
154 | return true;
155 | }
156 | }
157 | return false;
158 | }
159 |
160 | public boolean existNote(int lane) {
161 | return notes[lane] != null;
162 | }
163 |
164 | public Note getNote(int lane) {
165 | return notes[lane];
166 | }
167 |
168 | public void setNote(int lane, Note note) {
169 | notes[lane] = note;
170 | if(note == null) {
171 | return;
172 | }
173 | note.setSection(section);
174 | note.setMicroTime(time);
175 | }
176 |
177 | public void setHiddenNote(int lane, Note note) {
178 | hiddennotes[lane] = note;
179 | if(note == null) {
180 | return;
181 | }
182 | note.setSection(section);
183 | note.setMicroTime(time);
184 | }
185 |
186 | public boolean existHiddenNote() {
187 | for (Note n : hiddennotes) {
188 | if (n != null) {
189 | return true;
190 | }
191 | }
192 | return false;
193 | }
194 |
195 | public Note getHiddenNote(int lane) {
196 | return hiddennotes[lane];
197 | }
198 |
199 | public void addBackGroundNote(Note note) {
200 | if(note == null) {
201 | return;
202 | }
203 | note.setSection(section);
204 | note.setMicroTime(time);
205 | bgnotes = Arrays.copyOf(bgnotes, bgnotes.length + 1);
206 | bgnotes[bgnotes.length - 1] = note;
207 | }
208 |
209 | public void removeBackGroundNote(Note note) {
210 | for(int i = 0;i < bgnotes.length;i++) {
211 | if(bgnotes[i] == note) {
212 | final Note[] newbg = new Note[bgnotes.length - 1];
213 | for(int j = 0, index = 0;j < bgnotes.length;j++) {
214 | if(i != j) {
215 | newbg[index] = bgnotes[j];
216 | index++;
217 | }
218 | }
219 | bgnotes = newbg;
220 | break;
221 | }
222 | }
223 | }
224 |
225 | public Note[] getBackGroundNotes() {
226 | return bgnotes;
227 | }
228 |
229 | public void setBPM(double bpm) {
230 | this.bpm = bpm;
231 | }
232 |
233 | public double getBPM() {
234 | return bpm;
235 | }
236 |
237 | public void setSectionLine(boolean section) {
238 | this.sectionLine = section;
239 | }
240 |
241 | public boolean getSectionLine() {
242 | return sectionLine;
243 | }
244 |
245 | /**
246 | * 表示するBGAのIDを取得する
247 | *
248 | * @return BGAのID
249 | */
250 | public int getBGA() {
251 | return bga;
252 | }
253 |
254 | /**
255 | * 表示するBGAのIDを設定する
256 | *
257 | * @param bga
258 | * BGAのID
259 | */
260 | public void setBGA(int bga) {
261 | this.bga = bga;
262 | }
263 |
264 | /**
265 | * 表示するレイヤーBGAのIDを取得する
266 | *
267 | * @return レイヤーBGAのID
268 | */
269 | public int getLayer() {
270 | return layer;
271 | }
272 |
273 | public void setLayer(int layer) {
274 | this.layer = layer;
275 | }
276 |
277 | public Layer[] getEventlayer() {
278 | return eventlayer;
279 | }
280 |
281 | public void setEventlayer(Layer[] eventlayer) {
282 | this.eventlayer = eventlayer;
283 | }
284 |
285 | public double getSection() {
286 | return section;
287 | }
288 |
289 | public void setSection(double section) {
290 | for(Note n : notes) {
291 | if(n != null) {
292 | n.setSection(section);
293 | }
294 | }
295 | for(Note n : hiddennotes) {
296 | if(n != null) {
297 | n.setSection(section);
298 | }
299 | }
300 | for(Note n : bgnotes) {
301 | n.setSection(section);
302 | }
303 | this.section = section;
304 | }
305 |
306 | public int getStop() {
307 | return (int) (stop / 1000);
308 | }
309 |
310 | public long getMilliStop() {
311 | return stop / 1000;
312 | }
313 |
314 | public long getMicroStop() {
315 | return stop;
316 | }
317 |
318 | public void setStop(long stop) {
319 | this.stop = stop;
320 | }
321 |
322 | public double getScroll() {
323 | return scroll;
324 | }
325 |
326 | public void setScroll(double scroll) {
327 | this.scroll = scroll;
328 | }
329 | }
--------------------------------------------------------------------------------
/src/bms/model/bmson/BGA.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | // BGA.
8 | public class BGA {
9 | public BGAHeader[] bga_header; // picture id and filename
10 | public BGASequence[] bga_sequence; // picture id and filename
11 | public BNote[] bga_events; // as notes using this sound.
12 | public BNote[] layer_events; // as notes using this sound.
13 | public BNote[] poor_events; // as notes using this sound.
14 |
15 | public BGAHeader[] getBgaHeader() {
16 | return bga_header;
17 | }
18 | public void setBgaHeader(BGAHeader[] bga_header) {
19 | this.bga_header = bga_header;
20 | }
21 |
22 | public BNote[] getBgaNotes() {
23 | return bga_events;
24 | }
25 |
26 | public void setBgaNotes(BNote[] bga_events) {
27 | this.bga_events = bga_events;
28 | }
29 |
30 | public BNote[] getLayerNotes() {
31 | return layer_events;
32 | }
33 |
34 | public void setLayerNotes(BNote[] layer_events) {
35 | this.layer_events = layer_events;
36 | }
37 |
38 | public BNote[] getPoorNotes() {
39 | return poor_events;
40 | }
41 |
42 | public void setPoorNotes(BNote[] poor_events) {
43 | this.poor_events = poor_events;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/BGAHeader.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | public class BGAHeader {
8 | public int id;
9 | public String name;
10 | }
11 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/BGASequence.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | public class BGASequence {
4 | public int id;
5 | public Sequence[] sequence;
6 | }
7 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/BMSInfo.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | public class BMSInfo {
8 | public String title = ""; // as it is.
9 | public String subtitle = ""; // self-explanatory
10 | public String genre = ""; // as it is.
11 | public String artist = ""; // as it is.
12 | public String[] subartists = {}; // ["key:value"]
13 | public String mode_hint = "beat-7k"; // layout hints, e.g. "beat-7k",
14 | // "popn-5k", "generic-nkeys"
15 | public String chart_name = ""; // e.g. "HYPER", "FOUR DIMENSIONS"
16 | public int judge_rank = 100; // as defined standard judge width is 100
17 | public double total = 100; // as it is.
18 | public double init_bpm; // as it is
19 | public int level; // as it is?
20 |
21 | public String back_image = ""; // background image filename
22 | public String eyecatch_image = ""; // eyecatch image filename
23 | public String banner_image = ""; // banner image filename
24 | public String preview_music = ""; // preview music filename
25 | public int resolution = 240; // pulses per quarter note
26 |
27 | public int ln_type; // LN type
28 |
29 | public int getJudgeRank() {
30 | return judge_rank;
31 | }
32 |
33 | public void setJudgeRank(int value) {
34 | judge_rank = value;
35 | }
36 |
37 | public double getInitBPM() {
38 | return init_bpm;
39 | }
40 |
41 | public void setInitBPM(double bpm) {
42 | init_bpm = bpm;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/BMSONObject.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | public class BMSONObject {
4 | public int y; // as locate( 240BPM,1sec = 960 )
5 | }
6 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/BNote.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | public class BNote extends BMSONObject {
8 | public int id; // as it is.
9 | public int[] id_set; // as it is.
10 | public String condition;
11 | public int interval;
12 | }
13 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/BarLine.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | // event note
8 | public class BarLine {
9 | public int y; // as locate( 240BPM,1sec = 960 )
10 | public int k; // as kind.
11 | }
12 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/Bmson.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | public class Bmson {
8 | public String version; // bmson version
9 | public BMSInfo info = new BMSInfo(); // as bmson informations.
10 | public BarLine[] lines = {}; // as line locates.
11 | public BpmEvent[] bpm_events = {}; // change BPM. value is BPM.
12 | public StopEvent[] stop_events = {}; // Stop flow. value is StopTime.
13 | public ScrollEvent[] scroll_events = {}; // Stop flow. value is StopTime.
14 | public SoundChannel[] sound_channels = {}; // as Note data.
15 | public BGA bga = new BGA(); // as BGA(movie) data.
16 | public MineChannel[] mine_channels = {}; // as Mine Note data.
17 | public MineChannel[] key_channels = {}; // as Key Note data.
18 |
19 | public BpmEvent[] getBpmNotes() {
20 | return bpm_events;
21 | }
22 |
23 | public void setBpmNotes(BpmEvent[] bpm_events) {
24 | this.bpm_events = bpm_events;
25 | }
26 |
27 | public StopEvent[] getStopNotes() {
28 | return stop_events;
29 | }
30 |
31 | public void setStopNotes(StopEvent[] stop_events) {
32 | this.stop_events = stop_events;
33 | }
34 |
35 | public SoundChannel[] getSoundChannel() {
36 | return sound_channels;
37 | }
38 |
39 | public void setSoundChannel(SoundChannel[] sound_channels) {
40 | this.sound_channels = sound_channels;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/BpmEvent.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | public class BpmEvent extends BMSONObject {
8 | /**
9 | * 変更するBPM
10 | */
11 | public double bpm;
12 | }
13 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/MineChannel.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | public class MineChannel {
4 | public String name; // as sound file name
5 | public MineNote[] notes; // as notes using this sound.
6 | }
7 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/MineNote.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | public class MineNote extends BMSONObject {
4 | /**
5 | * レーン番号
6 | */
7 | public int x;
8 |
9 | public double damage;
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/Note.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | public class Note extends BMSONObject {
8 | /**
9 | * レーン番号(BGM:0 1P 1-8 2P 11-18 ?)
10 | */
11 | public int x;
12 | /**
13 | * ノーツの長さ( 0:normal note 1- : long note)
14 | */
15 | public int l;
16 | /**
17 | * 鳴らしている音源の続きから再生するかどうか
18 | */
19 | public boolean c;
20 |
21 | public int t; // as type
22 |
23 | public boolean up = false;
24 | }
25 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/ScrollEvent.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | public class ScrollEvent extends BMSONObject {
4 | /**
5 | * スクロールスピード倍率
6 | */
7 | public double rate = 1.0;
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/Sequence.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | public class Sequence {
4 | public long time;
5 | public int id = Integer.MIN_VALUE;
6 | }
7 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/SoundChannel.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | // sound channel.
8 | public class SoundChannel {
9 | public String name; // as sound file name
10 | public Note[] notes; // as notes using this sound.
11 | }
12 |
--------------------------------------------------------------------------------
/src/bms/model/bmson/StopEvent.java:
--------------------------------------------------------------------------------
1 | package bms.model.bmson;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | @JsonIgnoreProperties(ignoreUnknown=true)
6 |
7 | public class StopEvent extends BMSONObject {
8 | public long duration; // as value. Meaning of value depends on Channel.
9 | }
10 |
--------------------------------------------------------------------------------