本文是对我在实际开发过程中的记录,记录了在Android中处理单乐器MusicXML文件,从中取出有用的音符并且将音符顺序还原为实际演奏顺序的音符数组的过程。主要内容有SAX解析,多音轨处理,重复音符处理,小段重复处理。
MusicXML(Music Extensible Markup Language 音乐扩展标记语言)是一个开放的基于XML 的音乐符号文件格式,MusicXML可以记录18世纪以来所有乐谱的展示和演奏细节。直白来讲,MusicXML本质上还是XML。 更详细的了解
再通过文件名字获取文件
File dir = Constants.FILE_STORAGE_LOCATION; File file = new File(dir, name);对于XML文件的解析方式分为四种:1、DOM解析;2、SAX解析;3、JDOM解析;4、DOM4J解析。详细使用方法及对比 根据我们的需求,我们只读取数据且文件可能会比较大,所以我选择了SAX解析方式。 我们主要解析出的字段有 staff:音轨 chord:音符重复标识 repeat:小段重复 pitch:音符 alter:升降调,(音高组成之一,在pitch标签下) step: 音阶(音高组成之一,在pitch标签下) octave: 八度(音高组成之一,在pitch标签下) duration:表示下一个音符距此音符的弹奏时间间隔 以上字段都会在 “note”标签里面,通过下面的readXML方法取解析一个xml文件或者文件流,最终会得到解析结果List<HashMap<String, String>>,里面每一个“note”都是一个hashMap,hashMap里面存储了一个note里面的所有内容。
public static List<HashMap<String, String>> readXML(File file) { try { //实例化SAX工厂类 SAXParserFactory factory=SAXParserFactory.newInstance(); //实例化SAX解析器。 SAXParser sParser=factory.newSAXParser(); //实例化DefaultHandler,设置需要解析的节点 MyHandler myHandler=new MyHandler(); // 开始解析 sParser.parse(file, myHandler); // 解析完成之后,关闭流 //inputStream.close(); //若使用inputStream在这里需要关闭流 //返回解析结果。 return myHandler.getList(); } catch (Exception e) { e.printStackTrace(); // TODO: handle exception } return null; } public class MyHandler extends DefaultHandler { private List<HashMap<String, String>> list = null; //解析后的XML内容 private HashMap<String, String> map = null; //存放当前需要记录的节点的XML内容 private String currentTag = null;//当前读取的XML节点 private String currentValue = null;//当前节点的XML文本值 private String nodeName = “note”;//需要解析的节点数组名称,这个是MusicXml里的包含音符等重要信息节点 @Override public void startDocument() throws SAXException { // 接收文档开始的通知。 // 实例化ArrayList用于存放解析XML后的数据 list = new ArrayList<HashMap<String, String>>(); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // 接收元素开始的通知。 if (qName.equals(nodeName)) { // 如果当前运行的节点名称与设定需要读取的节点名称相同,则实例化HashMap map = new HashMap<String, String>(); } if(qName.equals("repeat")){ //这个表示小段重复 map = new HashMap<String, String>(); } //Attributes为当前节点的属性值,如果存在属性值,则属性值也读取。 if (attributes != null && map != null) { for (int i = 0; i < attributes.getLength(); i++) { //读取到的属性值,插入到Map中。 map.put(attributes.getQName(i), attributes.getValue(i)); } } if(qName=="repeat"){ Logger.d(map.toString()); list.add(map); map = null; } //记录当前节点的名称。 currentTag = qName; } @Override public void characters(char[] ch, int start, int length) throws SAXException { // 接收元素中字符数据的通知。 //当前节点有值的情况下才继续执行 if (currentTag != null && map != null) { //获取当前节点的文本值,ch这个直接数组就是存放的文本值。 currentValue = new String(ch, start, length); if (currentValue != null && !currentValue.equals("") && !currentValue.equals("\n")) { //读取的文本需要判断不能为null、不能等于”“、不能等于”\n“ map.put(currentTag, currentValue); } if(currentTag.equals("chord")){ //在note节点里面,它是一个空标签,他表示与上一个音符在同一时刻演奏,是无法直接读取的,所以当每个note里有这个标签时就记录一下 map.put(currentTag,"true"); } } //读取完成后,需要清空当前节点的标签值和所包含的文本值。 currentTag = null; currentValue = null; } @Override public void endElement(String uri, String localName, String qName) throws SAXException { // 接收元素结束的通知。 if (qName.equals(nodeName)) { //如果读取的结合节点是我们需要关注的节点,则把map加入到list中保存 list.add(map); //使用之后清空map,开始新一轮的读取person。 map = null; } } public List<HashMap<String, String>> getList() { return list; } }当前所记录的音轨一和音轨二是混在一起的,每一小节一段音轨一,一段音轨二次序排列。我们得先分开音轨,再去处理重复音符。
if (xmlBeanList != null && xmlBeanList.size() > 0) { //数据获取正常后才进行处理 for (MusicXmlBean bean : xmlBeanList) { if (bean.getStaff() == 1) { //将音轨一与音轨二区分开 stuffOneList.add(bean); } else if (bean.getStaff() == 2) { stuffTwoList.add(bean); } }在将分开的音轨再单独去处理重复音符
/** * 处理重复音符 * @param list * @return */ public static ArrayList<MusicXmlBean> dealWithChord(ArrayList<MusicXmlBean> list){ int tempIndex = 0; for (int i =0;i<list.size();i++){ if(!list.get(i).isChord()){ tempIndex = i; }else { list.get(tempIndex).getPitchList().add(list.get(i).getPitch()); //把重复处的音符取出放入上一个音符串里去 list.remove(i); i--; } } return list; }这一步为得是将同一时刻的音符存入同一list,处理算法待优化。
/** * 音轨处理 * @param list1 音轨1 * @param list2 音轨2 * @return */ public static ArrayList<ArrayList<MusicXmlBean>> mergeStuff(ArrayList<MusicXmlBean> list1, ArrayList<MusicXmlBean> list2) { ArrayList<ArrayList<MusicXmlBean>> pitchList = new ArrayList<>(); //整个音符列的列表 /** * 多音轨处理 */ int k = 0; //列表二已经处理的数 for (int i = 0; i < list1.size(); i++) { long t1 = list1.get(i).getDuration(); if (k<list2.size()&&t1 >= list2.get(k).getDuration()) { for (int j = k; list2.get(j).getDuration() <= t1&&j<list2.size(); j++) { ArrayList<MusicXmlBean> timePitchList = new ArrayList<>(); ArrayList<MusicXmlBean> timePitchList1 = new ArrayList<>(); long t2 = list2.get(j).getDuration(); long t3 =0; if(j<list2.size()-1){ t3 = list2.get(j+1).getDuration(); } if (t1 > t2) { timePitchList.add(list2.get(j)); if(t3>t1) { timePitchList1.add(list1.get(i)); } k++; } else if (t1 < t2) { timePitchList.add(list1.get(i)); } else { timePitchList.add(list1.get(i)); timePitchList.add(list2.get(j)); k++; } pitchList.add(timePitchList); if(timePitchList1.size()>0){ pitchList.add(timePitchList1); } if(j==list2.size()-1) break; } } else { ArrayList<MusicXmlBean> timePitchList = new ArrayList<>(); timePitchList.add(list1.get(i)); pitchList.add(timePitchList); } } for(int i =0;i<pitchList.size();i++){ Log.d("mergeStuff","第"+i+"时刻有元素:"); for (MusicXmlBean b :pitchList.get(i)){ Log.d("mergeStuff","音符:"+b.getPitch()+ " 时刻:"+b.getDuration()+"音轨:" +b.getStaff()); } } return pitchList; }最后一步就是取出音符list了
/** * 得到最终的音符结果 * @param list * @return */ public static ArrayList<ArrayList<String>> getPitchList(ArrayList<ArrayList<MusicXmlBean>> list){ ArrayList<ArrayList<String>> pitchStringList = new ArrayList<>(); for(ArrayList<MusicXmlBean> listBean :list){ ArrayList<String> strList = new ArrayList<>(); for(MusicXmlBean bean:listBean){ strList.addAll(strList.size(),bean.getPitchList()); } pitchStringList.add(strList); } for(ArrayList<String> listBean :pitchStringList){ Log.d("getPitchList", "getPitchList: "+listBean); } return pitchStringList; }虽然最终是得到我想要的结果了,但是由于时间关系和我水平限制这个处理流程与合并算法还是有很大的优化空间,经过这一番处理也学到了很多新的知识,继续加油!
