├── 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 | --------------------------------------------------------------------------------