package org.open2jam.parsers; import java.io.*; import java.util.*; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.open2jam.parsers.utils.CharsetDetector; import org.open2jam.parsers.utils.Filters; import org.open2jam.parsers.utils.Logger; import org.open2jam.parsers.utils.SampleData; class SMParser { public static Pattern key_value = Pattern.compile("(,|;)?( *(\\d+\\.\\d+) *= *(\\d+\\.\\d+) *)?(,|;)?"); public static Pattern note_line = Pattern.compile("^(,|;)?([01234ML]+)?(,|;)?.*$", Pattern.CASE_INSENSITIVE); public static boolean canRead(File f) { return f.getName().toLowerCase().endsWith(".sm"); } public static ChartList parseFile(File file) { ChartList list = new ChartList(); list.source_file = file; try { list = parseSMheader(file); } catch (IOException ex) { Logger.global.log(Level.WARNING, "{0}", ex); } Collections.sort(list); if (list.isEmpty()) return null; return list; } private static ChartList parseSMheader(File file) throws IOException { ChartList list = new ChartList(); list.source_file = file; String charset = CharsetDetector.analyze(file); BufferedReader r; try{ r = new BufferedReader(new InputStreamReader(new FileInputStream(file), charset)); }catch(FileNotFoundException e){ Logger.global.log(Level.WARNING, "File {0} not found !!", file.getName()); return null; } HashMap<Integer, String> sample_files = new HashMap<Integer, String>(); String title = "", subtitle = "", artist = ""; double bpm = 130; String cover_name = null; File image_cover = null; String line; StringTokenizer st; try{ while((line = r.readLine()) != null) { line = line.trim(); if(!line.startsWith("#"))continue; st = new StringTokenizer(line, ":;"); String cmd = st.nextToken().toUpperCase(); try{ if(cmd.equals("#TITLE")){ title = st.nextToken().trim(); continue; } if(cmd.equals("#SUBTITLE")){ subtitle = st.nextToken().trim(); continue; } if(cmd.equals("#ARTIST")){ artist = st.nextToken().trim(); continue; } if(cmd.equals("#BPMS")){ //first bpm, others bpms will be readed when the parse of the events StringTokenizer sb = new StringTokenizer(st.nextToken().trim(), "=,"); if(Double.parseDouble(sb.nextToken().trim()) == 0) bpm = Double.parseDouble(sb.nextToken().trim()); continue; } if(cmd.equals("#BANNER")){ File cover = new File(file.getParent(), st.nextToken().trim()); if(cover.exists()) { cover_name = cover.getName(); image_cover = cover; } else { String target = cover.getName(); int idx = target.lastIndexOf('.'); if(idx > 0) { target = target.substring(0, idx); } for(File ff : file.getParentFile().listFiles(Filters.imageFilter)) { String s = ff.getName(); idx = s.lastIndexOf('.'); if (idx > 0) { s = s.substring(0, idx); } if (target.equalsIgnoreCase(s)) { cover_name = ff.getName(); image_cover = ff; break; } } } } if(cmd.startsWith("#MUSIC")){ int id = 1; String name = st.nextToken().trim(); sample_files.put(id, name); continue; } if(cmd.startsWith("#NOTES")){ SMChart chart = new SMChart(); chart.source = file; chart.title = title+" "+subtitle; chart.artist = artist; chart.bpm = bpm; chart.cover_name = cover_name; chart.image_cover = image_cover; chart.sample_index = sample_files; for(int i = 0; i<5;i++) { String s; if((s = r.readLine()) != null) { s = s.replace(":", "").trim(); switch(i) { case 0: chart.keys = getKeys(s); break; case 3: chart.level = Integer.parseInt(s); break; } } } list.add(chart); continue; } }catch(NoSuchElementException ignored){} catch(NumberFormatException e){ Logger.global.log(Level.WARNING, "unparsable number @ {0} on file {1}", new Object[]{cmd, file.getName()}); } } }catch(IOException e){ Logger.global.log(Level.WARNING, "IO exception on file parsing ! {0}", e.getMessage()); } return list; } public static EventList parseChart(SMChart chart) { BufferedReader r; try{ r = new BufferedReader(new FileReader(chart.source)); }catch(FileNotFoundException e){ Logger.global.log(Level.WARNING, "File {0} not found !!", chart.source); return null; } EventList event_list = new EventList(); String line; StringTokenizer st; boolean founded = false; boolean parsed = false; int startMeasure = 0; double offset = 0; try { while ((line = r.readLine()) != null && !parsed) { line = line.trim(); if(!line.startsWith("#")) continue; st = new StringTokenizer(line, ":;"); String cmd = st.nextToken().toUpperCase().trim(); if(cmd.equals("#OFFSET")){ offset = Double.parseDouble(st.nextToken().trim()) * 1000d; // if(offset < 0) startMeasure = 1; continue; } if(cmd.equals("#BPMS")){ if(!st.hasMoreTokens()) continue; r.mark(8192); StringTokenizer sb = new StringTokenizer(st.nextToken().trim(), "=,"); setBPM(sb, event_list); //same line bpm //now, other lines bpm String s; while((s = r.readLine()) != null && !s.trim().startsWith("#")) { s = s.trim(); if(s.endsWith(";")) s = s.replace(";", "").trim(); if(s.isEmpty()) continue; sb = new StringTokenizer(s, "=,"); setBPM(sb, event_list); //same line bpm } r.reset(); continue; } if(cmd.equals("#STOPS")){ if(!st.hasMoreTokens()) continue; r.mark(8192); StringTokenizer sb = new StringTokenizer(st.nextToken().trim(), "=,"); setStop(sb, event_list); //same line bpm //now, other lines stops String s; while((s = r.readLine()) != null && !s.trim().startsWith("#")) { s = s.trim(); if(s.endsWith(";")) s = s.replace(";", "").trim(); if(s.isEmpty()) continue; sb = new StringTokenizer(s, "=,"); setStop(sb, event_list); //same line bpm } r.reset(); continue; } if(cmd.startsWith("#NOTES")) { String s; for(int i = 0; i<5;i++) { if((s = r.readLine()) != null) { s = s.replace(":", "").trim(); if(i == 3 && chart.level == Integer.parseInt(s)) { founded = true; } } } if(!founded) continue; int measure = startMeasure; List<String> notes = new ArrayList<String>(); while((s = r.readLine()) != null) { s = s.trim().toUpperCase(); if(s.isEmpty()) continue; Matcher match = note_line.matcher(s); if(match.find()) { String front = match.group(1); String nline = match.group(2); String tail = match.group(3); if(front != null) { //It's a , or a ; dump the events if(!notes.isEmpty()) { fillEvents(event_list, notes, measure); } measure++; if(front.equals(";")) { //line start with a ; end of parsing parsed = true; break; } } //add line if any if(nline != null) { notes.add(nline); } if(tail != null) { //It's a , or a ; dump the events if(!notes.isEmpty()) { fillEvents(event_list, notes, measure); } measure++; if(tail.equals(";")) { //line end with a ; end of parsing parsed = true; break; } } } } } } } catch (IOException ex) { Logger.global.log(Level.SEVERE, "{0}", ex); } catch(NoSuchElementException ignored) {} //add the music event_list.add(new Event(Event.Channel.AUTO_PLAY, startMeasure, 0, 1, offset, Event.Flag.NONE)); Collections.sort(event_list); return event_list; } public static HashMap<Integer, SampleData> getSamples(SMChart chart) { HashMap<Integer, SampleData> samples = new HashMap<Integer, SampleData>(); File[] files = chart.source.getParentFile().listFiles(Filters.sampleFilter); for(Map.Entry<Integer, String> entry : chart.sample_index.entrySet()) { try { for(File f : files) { String sn = entry.getValue().toLowerCase(); String fn = f.getName().toLowerCase(); String ext = fn.substring(fn.lastIndexOf("."), fn.length()); sn = sn.substring(0, sn.lastIndexOf(".")); fn = fn.substring(0,fn.lastIndexOf(".")); if(sn.equals(fn)) { SampleData.Type t; if (ext.equals(".wav")) t = SampleData.Type.WAV; else if (ext.equals(".ogg")) t = SampleData.Type.OGG; else if (ext.equals(".mp3")) t = SampleData.Type.MP3; else { //not a music file so continue continue; } samples.put(entry.getKey(), new SampleData(new FileInputStream(f), t, f.getName())); } } } catch (IOException ex) { Logger.global.log(Level.SEVERE, "{0}", ex); } } return samples; } private static void fillEvents(EventList event_list, List<String> notes, int measure) { int size = notes.size(); for(int pos=0; pos<size; pos++) { String[] n = notes.get(pos).split("(?<=\\G.)"); double position = (double)pos/size; for(int i=0; i<n.length;i++) { if(n[i].equals("0")) continue; Event.Flag flag; if(n[i].equals("1")) flag = Event.Flag.NONE; else if(n[i].equals("2")) flag = Event.Flag.HOLD; else if(n[i].equals("3")) flag = Event.Flag.RELEASE; else if(n[i].equals("4")) flag = Event.Flag.ROLL; else if(n[i].equals("M")) flag = Event.Flag.MINE; else if(n[i].equals("L")) flag = Event.Flag.LIFT; else { Logger.global.log(Level.WARNING, "{0} not supported :/", n[i]); continue; } event_list.add(new Event(getChannel(i), measure, position, 0, flag)); } } notes.clear(); } private static void setStop(StringTokenizer sb, EventList event_list) { while(sb.hasMoreTokens()) { double beat = Double.parseDouble(sb.nextToken().trim()); double stop = Double.parseDouble(sb.nextToken().trim()) * 1000; double measure = beat/4; double position = Math.abs(((int)measure)-measure); event_list.add(new Event(Event.Channel.STOP, (int)measure, position, stop, Event.Flag.NONE)); } } private static void setBPM(StringTokenizer sb, EventList event_list) { while(sb.hasMoreTokens()) { double beat = Double.parseDouble(sb.nextToken().trim()); double bpm = Double.parseDouble(sb.nextToken().trim()); double measure = beat/4; double position = Math.abs(((int)measure)-measure); event_list.add(new Event(Event.Channel.BPM_CHANGE, (int)measure, position, bpm, Event.Flag.NONE)); } } private static int getKeys(String s) { s = s.toLowerCase(); if(s.equals("dance-single")) return 4; if(s.equals("pump-single") || s.equals("ez2-single") || s.equals("para-single")) return 5; if(s.equals("dance-solo")) return 6; if(s.equals("ez2-real")) return 7; if(s.equals("dance-double") || s.equals("dance-couple")) return 8; if(s.equals("pump-double") || s.equals("pump-couple") || s.equals("ez2-double")) return 10; Logger.global.log(Level.WARNING, "Trying to get the key numbers from '{0}' is not supported", s); return 0; } private static Event.Channel getChannel(int i) { switch(i) { default: return Event.Channel.NONE; case 0: return Event.Channel.NOTE_1; case 1: return Event.Channel.NOTE_2; case 2: return Event.Channel.NOTE_3; case 3: return Event.Channel.NOTE_4; case 4: return Event.Channel.NOTE_5; case 5: return Event.Channel.NOTE_6; case 6: return Event.Channel.NOTE_7; case 7: return Event.Channel.NOTE_8; case 8: return Event.Channel.NOTE_9; case 9: return Event.Channel.NOTE_10; } } }