当前位置: 首页 > 工具软件 > MusicXML > 使用案例 >

Android开发记录:MusicXML处理成音高

仲璞瑜
2023-12-01

简介

文章介绍

本文是对我在实际开发过程中的记录,记录了在Android中处理单乐器MusicXML文件,从中取出有用的音符并且将音符顺序还原为实际演奏顺序的音符数组的过程。主要内容有SAX解析,多音轨处理,重复音符处理,小段重复处理。

MusicXML

MusicXML(Music Extensible Markup Language 音乐扩展标记语言)是一个开放的基于XML 的音乐符号文件格式,MusicXML可以记录18世纪以来所有乐谱的展示和演奏细节。直白来讲,MusicXML本质上还是XML。
更详细的了解

MusicXML解析过程

获取MusicXML文件

  1. 本地:将MusicXML文件放入assets后,直接通过文件名称获取InputStream
    InputStream is = getAssets().open(name);
    
  2. 远程:后台给出MusicXML文件的的URL后,先下载文件保存到sd卡
 //下载具体操作
   public static void downloadFile(String downloadUrl,String name) {
       try {
           Logger.d("url"+downloadUrl+"   name:"+name+"   event:"+event);
           URL url = new URL(downloadUrl);
           //打开连接
           URLConnection conn = url.openConnection();
           //打开输入流
           InputStream is = conn.getInputStream();
           //这是我自定义的文件夹  :Environment.getExternalStorageDirectory() + "/virtualPiano/"
           String dirName = Constants.FILE_STORAGE_LOCATION;     
           File file = new File(dirName);
           //不存在创建
           if (!file.exists()) {
               file.mkdirs();
           }
           //下载后的文件名
           String fileName = dirName + name;
           Logger.d(fileName);
           File file1 = new File(fileName);
           if (file1.exists()) {
               file1.delete();
           }
           //创建字节流
           byte[] bs = new byte[1024];
           int len;
           OutputStream os = new FileOutputStream(fileName);
           //写数据
           while ((len = is.read(bs)) != -1) {
               os.write(bs, 0, len);
           }
           //完成后关闭流
           Logger.d("download-finish");
           os.close();
           is.close();
         
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

再通过文件名字获取文件

   File dir = Constants.FILE_STORAGE_LOCATION;
   File file = new File(dir, name);

SAX解析

对于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;
    }
}

处理过程

小段重复处理及转化为实体类

  1. 我们需要写一个bean实体类来存储每个音符信息

   public class MusicXmlBean {

    private String pitch; //音符

    private List<String> pitchList = new ArrayList<>(); //某时刻的音符列表

    private int  staff; //音轨

    private long duration; //下一个音符间隔

    private boolean isChord;  //音符是否重复


    public void setChord(boolean chord) {
        isChord = chord;
    }

    public void setPitchList(List<String> pitchList) {
        this.pitchList = pitchList;
    }

    public List<String> getPitchList() {
        return pitchList;
    }

    public boolean isChord() {
        return isChord;
    }

    public void setPitch(String pitch) {
        this.pitch = pitch;
        pitchList.add(pitch);

    }

    public void setStaff(int staff) {
        this.staff = staff;
    }

    public void setDuration(long duration) {
        this.duration = duration;
    }

    public String getPitch() {
        return pitch;
    }

    public int getStaff() {
        return staff;
    }

    public long getDuration() {
        return duration;
    }
}
  1. 遍历我们得到的解析结果list,先处理小段重复问题并且得到实体类list,小段重复的处理方式是当这个map里面有repeat值为"forward",就记录小段重复开始。当有repeat值为“backward”时,就记录小段重复记录结束,并把这中间的部分取出来添加到repeat值为“backward”的后面。
public static List<MusicXmlBean> parseXML(File file, InputStream is,String name){

        List<MusicXmlBean> xmlBeanList = new ArrayList<>();

        List<MusicXmlBean> tempList = new ArrayList<>();

        boolean repeat = false;

        try {
            long duration1 = 0;
            long duration2 = 0;
            List<HashMap<String, String>> listNote;
            if(file!=null){
                listNote = SaxService.readXML(file, name);
            }else listNote = SaxService.readXML(is, name);

            for (HashMap<String, String> map : listNote) {
                Log.d("listNoteMap", "listNote:" + map.toString());
                String direction = map.get("direction");

                //小段重复处理
                if (direction != null && direction.equals("forward")) {
                    repeat = true;

                } else if (direction != null && direction.equals("backward")) {
                    xmlBeanList.addAll(tempList);
                    repeat = false;
                } else {
                    MusicXmlBean bean = new MusicXmlBean();
                    String staff = map.get("staff");
                    String chord = map.get("chord");
                    Log.d("listChord", "chord" + chord);  //查看当前chord
                    if (staff == null) staff = "1";
                    bean.setStaff(Integer.parseInt(staff));

                    String halfPitch = map.get("alter");
                    Log.d("halfPitch", "alter: "+halfPitch);
                    String pitch = map.get("step") + map.get("octave");

                    if(halfPitch==null){
                        bean.setPitch(pitch);
                    }else if(halfPitch.equals("-1"))
                        bean.setPitch(pitch + "-");
                    else if(halfPitch.equals("1")) {
                        bean.setPitch(pitch + "+");
                    }

                    //有重复音节的间隔时间都为上一个的时间
                    if (bean.getStaff() == 1) {
                        if (chord == null) {
                            bean.setDuration(duration1);
                            bean.setChord(false);
                            duration1 += Integer.parseInt(map.get("duration"));
                        } else {
                            bean.setDuration(duration1);
                            bean.setChord(true);
                        }

                    }

                    if (bean.getStaff() == 2) {
                        if (chord == null) {
                            bean.setDuration(duration2);
                            bean.setChord(false);
                            duration2 += Integer.parseInt(map.get("duration"));
                        } else {
                            bean.setDuration(duration2);
                            bean.setChord(true);
                        }


                    }

                    Log.d("listNoteBase","stuff:"+staff+"   duration:"+bean.getDuration()+"   pitch:"+bean.getPitch()+"  chord:"+chord);


                    if (repeat == true) {

                        tempList.add(bean);
                    }
                    xmlBeanList.add(bean);

                }


            }


        } catch (Exception e) {
            Log.d("listNote", e.getMessage());
            // TODO: handle exception

        }
        return xmlBeanList;
    }

重复音符处理

当前所记录的音轨一和音轨二是混在一起的,每一小节一段音轨一,一段音轨二次序排列。我们得先分开音轨,再去处理重复音符。

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;
    }

总结

虽然最终是得到我想要的结果了,但是由于时间关系和我水平限制这个处理流程与合并算法还是有很大的优化空间,经过这一番处理也学到了很多新的知识,继续加油!

 类似资料: